PDF.js Khám phá sâu từ Demo 2: Toàn cảnh phương thức getDocument
Lời nói đầu
Demo trong bài viết trước gọi getDocument với chỉ { url: '...' }. Bài viết này theo lời gọi đó đi qua toàn bộ chuỗi thực thi của getDocument.
Lời gọi getDocument
Tham số hình thức trong mã nguồn tên là src. Demo thực tế chỉ truyền một khóa:
Khóa trong src |
Giá trị |
|---|---|
url |
'https://assets-blog.streampdf.com/PDF32000_2008.pdf' |
Dựa trên định nghĩa của getDocument trong display/api.js, luồng thực thi mã như sau
Dòng 235-236: Khởi tạo tác vụ loading
Khởi tạo lớp PDFDocumentLoadingTask: xem mã nguồn, và gán giá trị trả về cho biến task
Lấy biến docId từ task, tại đây docId là d0
Dòng 238-335: Kiểm tra & xử lý tham số
Đoạn mã này bung đối tượng src do người dùng truyền vào thành 35+ biến cục bộ: giá trị truyền vào sẽ được kiểm tra và chuyển đổi kiểu, giá trị không truyền vào sẽ lấy mặc định. Demo chỉ truyền url, nên phần lớn tham số đi theo nhánh mặc định.
Tham số url duy nhất được truyền vào, qua getUrlProp chuyển thành đối tượng URL:
const url = URL.parse('https://assets-blog.streampdf.com/PDF32000_2008.pdf', window.location);
// const url = src.url ? getUrlProp(src.url) : null;
Các tham số còn lại đều không truyền vào. Theo thứ tự mã nguồn, kết quả tính toán như sau:
| Biến cục bộ | Kết quả | Logic lấy giá trị |
|---|---|---|
data |
null |
Nếu truyền vào thì qua getDataProp chuyển đồng bộ thành Uint8Array |
httpHeaders |
null |
Truyền nguyên, không truyền thì null |
withCredentials |
false |
Chỉ chấp nhận === true |
password |
null |
?? null |
rangeTransport |
null |
Chỉ chấp nhận thực thể PDFDataRangeTransport |
rangeChunkSize |
2 ** 16 (64KB) |
Chỉ chấp nhận giá trị truyền vào nếu là số nguyên dương |
worker |
null |
Chỉ chấp nhận thực thể PDFWorker |
verbosity |
undefined |
Lấy nguyên từ src.verbosity |
docBaseUrl |
null |
Chỉ chấp nhận chuỗi không phải giao thức data: |
cMapUrl |
null |
getFactoryUrlProp: không phải chuỗi thì null, và phải kết thúc bằng / |
cMapPacked |
true |
!== false |
iccUrl |
null |
Giống cMapUrl |
standardFontDataUrl |
null |
Giống cMapUrl |
wasmUrl |
null |
Giống cMapUrl |
ignoreErrors |
true |
src.stopAtErrors !== true, xem bên dưới |
maxImageSize |
-1 (không giới hạn) |
Chỉ chấp nhận là số nguyên và > -1 |
isOffscreenCanvasSupported |
true |
Không truyền thì lấy !isNodeJS |
isImageDecoderSupported |
true |
Không truyền thì lấy !isNodeJS |
canvasMaxAreaInBytes |
-1 (không giới hạn) |
Chỉ chấp nhận là số nguyên |
disableFontFace |
false |
Không truyền thì lấy isNodeJS |
fontExtraProperties |
false |
Chỉ chấp nhận === true |
enableXfa |
false |
Chỉ chấp nhận === true |
ownerDocument |
globalThis.document |
Không truyền thì lấy document toàn cục |
disableRange |
false |
Chỉ chấp nhận === true |
disableStream |
false |
Chỉ chấp nhận === true |
disableAutoFetch |
false |
Chỉ chấp nhận === true |
pdfBug |
false |
Chỉ chấp nhận === true |
CanvasFactory |
DOMCanvasFactory |
Nhánh môi trường, xem bên dưới |
FilterFactory |
DOMFilterFactory |
Giống trên |
BinaryDataFactory |
DOMBinaryDataFactory |
Giống trên |
enableHWA |
false |
Chỉ chấp nhận === true |
enableWebGPU |
false |
Chỉ chấp nhận === true |
gpuPromise |
Promise.resolve(false) |
WebGPU tắt, không khởi tạo GPU |
useWasm |
true |
!== false |
pagesMapper |
new PagesMapper() |
Không truyền thì tạo mới |
useSystemFonts |
true |
Mặc định phụ thuộc tham số khác, xem bên dưới |
useWorkerFetch |
false |
Kiểm tra tổ hợp, xem Hỏi & Đáp bên dưới |
styleElement |
null |
Chỉ giữ giá trị truyền vào ở bản dựng TESTING, trong bản production luôn là null |
Trên đây là tất cả biến cục bộ mà đoạn mã này tạo ra. Hầu hết các kiểm tra đều là trường hợp「không truyền thì lấy mặc định」thông thường, nhưng có vài thứ đáng để xem xét kỹ hơn.
ignoreErrors: biến nội bộ có nghĩa nghịch đảo
Tham số hướng đến người dùng tên là stopAtErrors, nhưng biến nội bộ lại tên là ignoreErrors, với logic lấy giá trị là src.stopAtErrors !== true. Nghĩa là hành vi mặc định là bỏ qua lỗi: khi parse lỗi, cố gắng khôi phục trạng thái thay vì reject các promise như getOperatorList / getTextContent / RenderTask. Khi đọc mã nguồn, đây là chỗ rất dễ nhầm lẫn giữa tên và giá trị.
rangeChunkSize: chuẩn bị 64KB cho phần sau
rangeChunkSize mặc định là 64KB. Do disableRange = false, khi tạo transport ở phần sau, sẽ phát các yêu cầu range đối với PDF, và mỗi chunk được lấy sẽ có đúng kích thước này. Hãy nhớ nó nhé.
docBaseUrl: tại sao loại trừ data: URL
Bình luận trong mã nguồn giải thích lý do: URL data: không thể dùng để khôi phục đường dẫn tuyệt đối, và nó chứa toàn bộ tài liệu PDF với độ dài không giới hạn, nên không gửi nó sang luồng worker.
gpuPromise: thiết kế chạy song song với khởi động worker
const gpuPromise = Promise.resolve(false); // enableWebGPU ? initGPU() : Promise.resolve(false);
Ngay cả khi WebGPU không được bật, tại đây vẫn trả về promise đã resolve ngay lập tức, thay vì trì hoãn kiểm tra cho đến khi sử dụng. Bình luận giải thích ý định: cho phép khởi tạo GPU chạy song song với khởi động worker, giá trị boolean sau khi resolve sẽ được chuyển tiếp cho worker.
Ba Factory: nhánh môi trường
const CanvasFactory = DOMCanvasFactory;
// src.CanvasFactory || (GENERIC build && isNodeJS ? NodeCanvasFactory : DOMCanvasFactory);
FilterFactory và BinaryDataFactory cũng tương tự. Kiểm tra có ba cấp: tùy biến của người dùng > triển khai môi trường Node > triển khai DOM. Demo chạy trên trình duyệt mà không có tùy biến, nên cả ba factory đều là phiên bản DOM. Nhiệm vụ của từng cái:
CanvasFactory: tạo canvas để renderFilterFactory: tạo bộ lọc hình ảnh (như bộ lọc SVG)BinaryDataFactory: tải tài nguyên tĩnh tích hợp, phối hợp vớiuseWorkerFetchở bên dưới
useSystemFonts: giá trị mặc định phụ thuộc tham số khác
const useSystemFonts = true;
// typeof src.useSystemFonts === "boolean" ? src.useSystemFonts : !isNodeJS && !disableFontFace;
Giá trị mặc định của nó phụ thuộc vào disableFontFace, vừa được parse ở trên. Tại sao? Phông chữ không được nhúng trong PDF sẽ được chuyển thành FontFace để tải; nếu disableFontFace là true, đường dẫn tải này bị tắt, và lúc đó dựa vào phông chữ hệ thống sẽ dẫn đến glyph hoàn toàn lộn xộn. Vì vậy trong trường hợp này giá trị mặc định cũng là false, để tránh render ra phông chữ bị hỏng hoàn toàn.
useWorkerFetch: kiểm tra tổ hợp
const useWorkerFetch = false;
// typeof src.useWorkerFetch === "boolean"
// ? src.useWorkerFetch
// : MOZCENTRAL ||
// !!(
// BinaryDataFactory === DOMBinaryDataFactory &&
// cMapUrl && cMapPacked && standardFontDataUrl && wasmUrl &&
// isValidFetchUrl(cMapUrl, document.baseURI) &&
// isValidFetchUrl(standardFontDataUrl, document.baseURI) &&
// isValidFetchUrl(wasmUrl, document.baseURI)
// );
H: Kiểm tra này đang làm gì? Đ: Quyết định ai sẽ tải các「tài nguyên tĩnh tích hợp」(tệp CMap, tệp phông chữ chuẩn, tệp wasm): luồng worker tự tải bằng Fetch API (
true), hay luồng chính tải rồi chuyển tiếp cho worker (false).Nếu người dùng truyền boolean rõ ràng, dùng giá trị của người dùng trực tiếp. Còn không thì tính giá trị mặc định: MOZCENTRAL (viewer tích hợp Firefox) luôn là
true; bản dựng thông thường phải đồng thời thỏa mãn ba điều kiện sau mới làtrue:
- Không có
BinaryDataFactorytùy biến —— vì khiuseWorkerFetchlàtrue, tham số này sẽ bị bỏ qua trực tiếp (bên trongtransportFactory,binaryDataFactoryđược đặt thànhnull). Factory tùy biến của người dùng phải được tôn trọng, nên mặc định không thể làtrue;cMapUrl/standardFontDataUrl/wasmUrlđều được cung cấp, vàcMapPackedlàtrue—— không có URL thì không có gì để fetch;- Ba URL này sau khi resolve tương đối với
document.baseURIlà http/https —— Fetch API chỉ hỗ trợ http/https, không hỗ trợ file/ftp.Demo của bài này chỉ truyền
url,cMapUrllànull, điều kiện 2 không thỏa mãn, nênuseWorkerFetch = false: đi theo đường dẫn luồng chính,DOMBinaryDataFactorytải trên luồng chính, worker khi cần sẽ yêu cầu từ luồng chính qua tin nhắnFetchBinaryData.
Dòng 338: Đặt mức độ nhật ký luồng chính
setVerbosityLevel(verbosity); // verbosity là undefined
verbosity là undefined, nên mức độ nhật ký luồng chính giữ mặc định. Lưu ý điều này chỉ ảnh hưởng đến luồng chính; mức độ nhật ký của luồng worker sẽ được truyền riêng khi tạo worker.
Dòng 342-350: Khởi tạo ba factory
const transportFactory = {
canvasFactory: new DOMCanvasFactory({ ownerDocument: document, enableHWA: false }),
filterFactory: new DOMFilterFactory({ docId: 'd0', ownerDocument: document }),
binaryDataFactory: new DOMBinaryDataFactory({
cMapUrl: null,
standardFontDataUrl: null,
wasmUrl: null,
}),
};
// binaryDataFactory: null khi MOZCENTRAL hoặc useWorkerFetch là true
Khởi tạo ba lớp Factory đã chọn trước đó, gói thành transportFactory, sau đó giao cho transport sử dụng. Do useWorkerFetch = false, binaryDataFactory không phải null —— nhất quán với phần Hỏi & Đáp ở trên: tài nguyên tĩnh tích hợp được tải trên luồng chính.
Khởi tạo DOMCanvasFactory: xem mã nguồn
Khởi tạo DOMFilterFactory: xem mã nguồn
Khởi tạo DOMBinaryDataFactory: xem mã nguồn
Dòng 352-360: Tạo Worker
worker = PDFWorker.create({ verbosity: undefined, port: undefined });
// port đến từ GlobalWorkerOptions.workerPort, demo không đặt
task._worker = worker;
Demo không truyền worker, nên PDF.js tự tạo và sở hữu một. PDFWorker.create trả về thực thể PDFWorker một cách đồng bộ, hàm khởi tạo của nó sẽ đọc GlobalWorkerOptions.workerSrc (tức là địa chỉ tệp worker cấu hình ở bài thứ nhất), tạo Worker và hoàn tất bắt tay khởi động qua tin nhắn ready / test —— đây là điểm trọng tâm, đã được trình bày chi tiết ở bài 8. Ở đây chỉ cần nhớ: worker.promise là một Promise<void>, resolve (với giá trị undefined) sau khi phản hồi test thành công, và reject nếu khởi động thất bại hoặc bị hủy.
Dòng 362-402: Lắp hai gói tham số
Gói đầu tiên docParams, sau đó sẽ được gửi xuyên luồng sang worker:
const docParams = {
docId: 'd0',
apiVersion: '6.3.289', // số phiên bản bản dựng
data: null,
password: null,
disableAutoFetch: false,
rangeChunkSize: 65536,
docBaseUrl: null,
enableXfa: false,
evaluatorOptions: {
maxImageSize: -1,
disableFontFace: false,
ignoreErrors: true,
isOffscreenCanvasSupported: true,
isImageDecoderSupported: true,
canvasMaxAreaInBytes: -1,
fontExtraProperties: false,
useSystemFonts: true,
useWasm: true,
useWorkerFetch: false,
cMapUrl: null,
cMapPacked: true,
iccUrl: null,
standardFontDataUrl: null,
wasmUrl: null,
hasGPU: false, // placeholder, sẽ điền lại sau
},
};
Gói thứ hai transportParams, chỉ lưu lại ở luồng chính:
const transportParams = {
ownerDocument: document,
pdfBug: false,
styleElement: null,
enableHWA: false,
loadingParams: { disableAutoFetch: false, enableXfa: false },
};
Một câu để phân biệt: docParams là「lệnh làm việc」gửi cho worker, còn transportParams là「cấu hình cục bộ」luồng chính tự giữ.
Dòng 404-473: Bắt tay sau khi worker sẵn sàng
Promise.all([worker.promise, gpuPromise]).then(...)
Sau khi cả worker sẵn sàng và phát hiện GPU (resolve là false) đều hoàn tất, tuần tự xảy ra:
- Điền lại
hasGPU = falsevàoevaluatorOptions; - Gửi tin nhắn
GetDocRequestcho worker, payload làdocParams.datalànull, nên không có buffer nào cần chuyển quyền sở hữu; - Chọn luồng mạng:
datalànullvàrangeTransportlànull, đi theo nhánhurl, khởi tạo luồng mạng (ưu tiên triển khai fetch, không hỗ trợ thì fallback XHR; cách phát yêu cầu range bên trong, xem bài 9):
networkStream = new NetworkStream({
url,
httpHeaders: null,
withCredentials: false,
rangeChunkSize: 65536,
disableRange: false,
disableStream: false,
});
NetworkStream ở đây không phải lớp cố định được import, mà được chọn tại thời điểm chạy trong nhánh này:
const NetworkStream = getNetworkStream(url); // api.js#L431
getNetworkStream chỉ thực hiện một lựa chọn ba nhánh:
function getNetworkStream(url) {
// eslint-disable-next-line no-nested-ternary
return isValidFetchUrl(url)
? PDFFetchStream
: typeof PDFJSDev !== 'undefined' && PDFJSDev.test('GENERIC') && isNodeJS
? PDFNodeStream
: PDFNetworkStream;
}
| Điều kiện | Lớp stream được trả về |
|---|---|
isValidFetchUrl(url): giao thức là http / https |
PDFFetchStream |
| Ngược lại, bản dựng GENERIC và đang chạy trên Node | PDFNodeStream |
| Các trường hợp còn lại | PDFNetworkStream (XHR) |
url của demo là https://..., nên thực tế khởi tạo PDFFetchStream. Hai loại đầu vào còn lại cũng có stream riêng: khi truyền rangeTransport dùng PDFDataTransportStream, khi truyền data thì toàn bộ PDF đã nằm trong bộ nhớ nên không tạo stream nào.
Tóm lại: const NetworkStream = PDFFetchStream.
Tiếp tục thực thi:
- Nhận phản hồi của worker đối với
GetDocRequest(workerId): nhấp để xem sự kiện. Giá trị trả về nhận được ở đây làd0_worker, gán cho biếnworkerId - Tạo MessageHandler, truyền tham số
{ docId: 'd0', workerId: 'd0_worker', port: Worker }: nhấp để xem lời gọi, gán cho biếnmessageHandler - Tạo WorkerTransport, truyền tham số
{ messageHandler, task, networkStream, transportParams, transportFactory, pagesMapper }: nhấp để xem lời gọi, và gắn vàotask._transport - Gửi tin nhắn
Readycho worker, phía luồng chính tuyên bố sẵn sàng: nhấp để xem sự kiện
Ở cuối chuỗi lời gọi, task._setupCapability.resolve được gọi, hoàn tất tác vụ bất đồng bộ của task.
Dòng 474-475: Giá trị trả về
Toàn bộ chuỗi .then trên tiến hành bất đồng bộ ở hậu trường, còn getDocument bản thân trả về đồng bộ task. Bước nào trong chuỗi gặp lỗi sẽ reject capability bên trong task, tức là promise mà demo đang đợi với await loadingTask.promise.
Đến đây getDocument đã thực thi xong.
Tổng kết
Trong phần này, chúng ta theo dõi demo đi qua toàn bộ lời gọi getDocument:
- Tạo
PDFDocumentLoadingTask, lấydocId = 'd0'; - Bung
srcthành 35+ biến cục bộ, demo chỉ truyềnurl, còn lại đều rơi vào giá trị mặc định; - Khởi tạo ba factory DOM, gói thành
transportFactory; PDFWorker.createtạo worker và khởi động bắt tay;- Lắp
docParams(gửi cho worker) vàtransportParams(lưu lại ở luồng chính); - Sau khi worker sẵn sàng, gửi
GetDocRequest, thiết lập luồng mạng, tạoMessageHandlervàWorkerTransport, cuối cùng gửiReady; - Trả về
taskđồng bộ, demo đợi tài liệu sẵn sàng quaawait loadingTask.promise.
Trở lại demo trong bài viết đầu tiên: dòng 4-7 gọi chính là getDocument được phân tích trong bài này.
Theo chuỗi lời gọi phía trên, điểm gọi tiếp theo là: getPage
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 2: Toàn cảnh phương thức getDocument
All rights reserved