StreamPDFStreamPDF
Quay lại blog

PDF.js Khám phá sâu từ Demo 2: Toàn cảnh phương thức getDocument

27 tháng 10, 2026 · StreamPDF Team

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 srcGiá 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ị
datanullNếu truyền vào thì qua getDataProp chuyển đồng bộ thành Uint8Array
httpHeadersnullTruyền nguyên, không truyền thì null
withCredentialsfalseChỉ chấp nhận === true
passwordnull?? null
rangeTransportnullChỉ chấp nhận thực thể PDFDataRangeTransport
rangeChunkSize2 ** 16 (64KB)Chỉ chấp nhận giá trị truyền vào nếu là số nguyên dương
workernullChỉ chấp nhận thực thể PDFWorker
verbosityundefinedLấy nguyên từ src.verbosity
docBaseUrlnullChỉ chấp nhận chuỗi không phải giao thức data:
cMapUrlnullgetFactoryUrlProp: không phải chuỗi thì null, và phải kết thúc bằng /
cMapPackedtrue!== false
iccUrlnullGiống cMapUrl
standardFontDataUrlnullGiống cMapUrl
wasmUrlnullGiống cMapUrl
ignoreErrorstruesrc.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
isOffscreenCanvasSupportedtrueKhông truyền thì lấy !isNodeJS
isImageDecoderSupportedtrueKhông truyền thì lấy !isNodeJS
canvasMaxAreaInBytes-1 (không giới hạn)Chỉ chấp nhận là số nguyên
disableFontFacefalseKhông truyền thì lấy isNodeJS
fontExtraPropertiesfalseChỉ chấp nhận === true
enableXfafalseChỉ chấp nhận === true
ownerDocumentglobalThis.documentKhông truyền thì lấy document toàn cục
disableRangefalseChỉ chấp nhận === true
disableStreamfalseChỉ chấp nhận === true
disableAutoFetchfalseChỉ chấp nhận === true
pdfBugfalseChỉ chấp nhận === true
CanvasFactoryDOMCanvasFactoryNhánh môi trường, xem bên dưới
FilterFactoryDOMFilterFactoryGiống trên
BinaryDataFactoryDOMBinaryDataFactoryGiống trên
enableHWAfalseChỉ chấp nhận === true
enableWebGPUfalseChỉ chấp nhận === true
gpuPromisePromise.resolve(false)WebGPU tắt, không khởi tạo GPU
useWasmtrue!== false
pagesMappernew PagesMapper()Không truyền thì tạo mới
useSystemFontstrueMặc định phụ thuộc tham số khác, xem bên dưới
useWorkerFetchfalseKiểm tra tổ hợp, xem Hỏi & Đáp bên dưới
styleElementnullChỉ giữ giá trị truyền vào ở bản dựng TESTING, trong生产 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à 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 để render
  • FilterFactory: 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ới useWorkerFetch ở 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:

  1. Không có BinaryDataFactory tùy biến —— vì khi useWorkerFetch là true, tham số này sẽ bị bỏ qua trực tiếp (bên trong transportFactory, binaryDataFactory được đặt thành null). 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;
  2. cMapUrl / standardFontDataUrl / wasmUrl đều được cung cấp, và cMapPacked là true —— không có URL thì không có gì để fetch;
  3. Ba URL này sau khi resolve tương đối với document.baseURI là 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, cMapUrl là null, điều kiện 2 không thỏa mãn, nên useWorkerFetch = false: đi theo đường dẫn luồng chính, DOMBinaryDataFactory tải trên luồng chính, worker khi cần sẽ yêu cầu từ luồng chính qua tin nhắn FetchBinaryData.

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 —— 呼应 với Hỏi & Đáp ở phần trước: 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ỉ留在 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:

  1. Điền lại hasGPU = false vào evaluatorOptions;
  2. Gửi tin nhắn GetDocRequest cho worker, payload là docParams. data là null, nên không có buffer nào cần chuyển quyền sở hữu;
  3. Chọn luồng mạng: data là null và rangeTransport là null, đi theo nhánh url, 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ệnLớp stream được trả về
isValidFetchUrl(url): giao thức là http / httpsPDFFetchStream
Ngược lại, bản dựng GENERIC và đang chạy trên NodePDFNodeStream
Các trường hợp còn lạiPDFNetworkStream (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:

  1. 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ến workerId
  2. 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ến messageHandler
  3. Tạo WorkerTransport, truyền tham số { messageHandler, task, networkStream, transportParams, transportFactory, pagesMapper }: nhấp để xem lời gọi, và gắn vào task._transport
  4. Gửi tin nhắn Ready cho 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:

  1. Tạo PDFDocumentLoadingTask, lấy docId = 'd0';
  2. Bung src thành 35+ biến cục bộ, demo chỉ truyền url, còn lại đều rơi vào giá trị mặc định;
  3. Khởi tạo ba factory DOM, gói thành transportFactory;
  4. PDFWorker.create tạo worker và khởi động bắt tay;
  5. Lắp docParams (gửi cho worker) và transportParams (giữ lại ở luồng chính);
  6. Sau khi worker sẵn sàng, gửi GetDocRequest, thiết lập luồng mạng, tạo MessageHandler và WorkerTransport, cuối cùng gửi Ready;
  7. Trả về task đồng bộ, demo đợi tài liệu sẵn sàng qua await 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


About me: 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.

Tiếp tục PDF.js Khám phá sâu từ Demo