PDF.js Khám phá sâu từ Demo 4: Toàn cảnh phương thức getViewport
Lời nói đầu
Trong bài viết trước, chúng ta đã lấy được biến page qua await pdf.getPage(1) — nó là một thực thể PDFPageProxy, với _pageInfo lưu metadata của trang: rotate: 0, userUnit: 1, view: [0, 0, 595, 842].
Quay lại demo của bài thứ nhất, dòng 10-11:
const viewport = page.getViewport({ scale: 0.5 });
Lời gọi getViewport
Truyền vào tham số object:
| Tham số | Giá trị | Ý nghĩa |
|---|---|---|
scale |
0.5 |
Tỷ lệ thu phóng |
Khác với getPage, getViewport là lời gọi đồng bộ: không đụng đến worker, không làm IO, chỉ là một phép tính toán thuần túy. Bài này xuất phát từ lời gọi này, đi qua toàn bộ chuỗi thực thi của getViewport.
Dựa trên định nghĩa của PDFPageProxy trong api.js, luồng thực thi mã như sau.
Dòng 1423-1439: Phân rã tham số
getViewport({
scale, // 0.5, tham số duy nhất được truyền vào
rotation = this.rotate, // this._pageInfo.rotate = 0
offsetX = 0,
offsetY = 0,
dontFlip = false,
} = {}) {
return new PageViewport({
viewBox: this.view, // [0, 0, 595, 842]
userUnit: this.userUnit, // 1
scale,
rotation,
offsetX,
offsetY,
dontFlip,
});
}
Lưu ý giá trị mặc định của rotation lấy từ chính trang: thuộc tính /Rotate của trang PDF sẽ được tự động áp dụng, phía gọi không cần quan tâm. this.view và this.userUnit đến từ pageInfo do tin nhắn GetPage trả về ở bài trước.
Sau khi thu thập xong tham số, tất cả được giao cho hàm khởi tạo PageViewport (display/page_viewport.js): xem mã nguồn. Các phép tính tiếp theo đều nằm trong đó.
H:
offsetX/offsetYdùng để làm gì?Đ: Dịch chuyển kết quả render trên canvas, ví dụ ghép nhiều trang lên cùng một canvas lớn, hoặc chừa lề xung quanh trang. Demo không truyền, nên lấy giá trị mặc định
0.
Dòng 70: scale hấp thụ userUnit (page_viewport.js)
scale *= userUnit; // 0.5 * 1 = 0.5
H:
userUnitlà gì?Đ: Hệ số quy đổi đơn vị được giới thiệu trong PDF 1.6: 1 đơn vị trong không gian người dùng PDF =
userUnit/ 72 inch, giá trị mặc định là1. Nhân nó vàoscale, các phép tính sau không cần phân biệt「đơn vị không gian người dùng」và「kích thước thực」nữa.
Dòng 74-75: Tính điểm trung tâm của trang (page_viewport.js)
const centerX = (viewBox[2] + viewBox[0]) / 2; // (595 + 0) / 2 = 297.5
const centerY = (viewBox[3] + viewBox[1]) / 2; // (842 + 0) / 2 = 421
Việc xoay quay quanh tâm trang, nên tính sẵn điểm trung tâm để dùng sau.
Dòng 78-116: Chuẩn hóa góc xoay và vector xoay (page_viewport.js)
rotation %= 360; // 0
if (rotation < 0) {
rotation += 360;
}
switch (rotation) {
case 0:
rotateA = 1;
rotateB = 0;
rotateC = 0;
rotateD = -1;
break;
// case 90 / 180 / 270 lược bỏ
default:
throw new Error('PageViewport: Invalid rotation, must be a multiple of 90 degrees.');
}
Góc xoay được chuẩn hóa về khoảng [0, 360), và chỉ cho phép bội số của 90, các giá trị khác ném lỗi trực tiếp — đây là giới hạn của chuẩn PDF.
rotation của demo là 0, trúng case 0: rotateA = 1, rotateB = 0, rotateC = 0, rotateD = -1.
H: Tại sao rotation là 0 thì rotateD lại là -1? Đ: Đây là phép lật trục y. Hệ tọa độ PDF có trục y hướng lên (hệ tọa độ toán học, gốc ở góc trái dưới), hệ tọa độ canvas có trục y hướng xuống (hệ tọa độ màn hình, gốc ở góc trái trên). Ngay cả khi không xoay, cũng cần một phép lật để hai bên khớp nhau,
rotateD = -1chính là phép lật đó.Tương ứng,
dontFlip = truesẽ đảo dấurotateC,rotateD, hủy phép lật này — áp dụng cho trường hợp hệ tọa độ đích cũng có trục y hướng lên.
Dòng 120-130: Tính chiều rộng, chiều cao và offset canvas (page_viewport.js)
if (rotateA === 0) {
// Nhánh khi rotation là 90/270: chiều rộng và chiều cao hoán đổi
offsetCanvasX = Math.abs(centerY - viewBox[1]) * scale + offsetX;
offsetCanvasY = Math.abs(centerX - viewBox[0]) * scale + offsetY;
width = (viewBox[3] - viewBox[1]) * scale;
height = (viewBox[2] - viewBox[0]) * scale;
} else {
offsetCanvasX = Math.abs(centerX - viewBox[0]) * scale + offsetX; // 297.5 * 0.5 + 0 = 148.75
offsetCanvasY = Math.abs(centerY - viewBox[1]) * scale + offsetY; // 421 * 0.5 + 0 = 210.5
width = (viewBox[2] - viewBox[0]) * scale; // 595 * 0.5 = 297.5
height = (viewBox[3] - viewBox[1]) * scale; // 842 * 0.5 = 421
}
Nhánh rotateA === 0 tương ứng với xoay 90/270 độ: trang nằm ngang, phép tính width và height hoán đổi cho nhau. Demo xoay 0 nên đi nhánh else.
Dòng 134-141: Lắp ma trận transform (page_viewport.js)
this.transform = [
rotateA * scale, // 0.5
rotateB * scale, // 0
rotateC * scale, // 0
rotateD * scale, // -0.5
offsetCanvasX - rotateA * scale * centerX - rotateC * scale * centerY, // 148.75 - 148.75 - 0 = 0
offsetCanvasY - rotateB * scale * centerX - rotateD * scale * centerY, // 210.5 - 0 + 210.5 = 421
];
// transform = [0.5, 0, 0, -0.5, 0, 421]
Bình luận trong mã nguồn giải thích phép biến đổi tổng hợp mà 6 thành phần này tương ứng:
translate(-centerX, -centerY), rotate and flip vertically, scale, and translate(offsetCanvasX, offsetCanvasY)
Tức là: đưa tâm trang về gốc tọa độ → xoay và lật dọc → thu phóng → dịch chuyển đến vị trí mục tiêu trên canvas. Nó chính là 6 tham số mà setTransform(a, b, c, d, e, f) của canvas cần, và sẽ được áp dụng trực tiếp khi render (xem bài tiếp theo).
Kiểm chứng tính đúng đắn của ma trận: lấy góc trái trên của trang trong hệ tọa độ PDF (0, 842) (trục y hướng lên,「trên」là giá trị lớn), qua phép biến đổi ma trận:
x' = 0.5 * 0 + 0 * 842 + 0 = 0y' = 0 * 0 + (-0.5) * 842 + 421 = 0
Vừa khít ánh xạ đến tọa độ canvas (0, 0), tức góc trái trên của canvas. ✓
Thành phần thứ 6 421 vừa hay bằng height: sau khi lật trục y, toàn bộ trang chạy lên phía trên canvas (y âm), cần dịch xuống một chiều cao trang mới rơi lại vào trong canvas.
Dòng 143-144: Lưu chiều rộng và chiều cao (page_viewport.js)
this.width = width; // 297.5
this.height = height; // 421
Hai giá trị này chính là nguồn gốc của canvas.width = Math.floor(viewport.width) trong demo bài thứ nhất: Math.floor(297.5) là 297, kích thước thực của canvas là 297 x 421.
Đến đây, phép tính của getViewport đã hoàn tất, thực thể PageViewport trả về được gán cho biến viewport của demo.
Tổng kết
Trong phần này, chúng ta theo demo đi qua toàn bộ phép tính của getViewport({ scale: 0.5 }):
- Phân rã tham số:
scale = 0.5,rotationmặc định lấy giá trị/Rotatecủa trang là0, còn lại là0/false; scalehấp thụuserUnit, vẫn là0.5;- Tính điểm trung tâm trang
(297.5, 421); - Sau khi chuẩn hóa góc xoay, lấy vector xoay
(1, 0, 0, -1),rotateD = -1đảm nhiệm phép lật trục y; - Tính chiều rộng chiều cao
297.5 x 421và offset canvas(148.75, 210.5); - Lắp
transform = [0.5, 0, 0, -0.5, 0, 421], hoàn thành ánh xạ「tọa độ PDF → tọa độ canvas」.
Trên viewport còn gắn các phương thức như convertToViewportPoint / convertToPdfPoint (chuyển đổi tọa độ hai chiều, dùng cho bôi chọn văn bản và định vị chú thích: xem mã nguồn), clone (tạo viewport mới: xem mã nguồn), demo không dùng đến nên không mở rộng.
Sau khi có viewport, demo đóng gói nó cùng canvasContext rồi truyền cho page.render(renderContext) — ma trận transform sẽ được áp dụng vào ngữ cảnh canvas tại đó. Bài tiếp theo sẽ trình bày.
Về tôi: Người sáng tạo StreamPDF, tập trung vào nghiên cứu và phát triển trong lĩnh vực AI và PDF.
Liên kết bài viết gốc: PDF.js Khám phá sâu từ Demo 4: Toàn cảnh phương thức getViewport
All rights reserved