/
githubmirror
/
pdf.js
Обзор
Документация
Войти
/
githubmirror
/
pdf.js
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/display/api.js
3 549 строк
110 KB
Jonas Jenwald
[api-minor] Convert `getPermissions` to return data in a Set
08 авг 2026, 19:09
08 авг 2026, 19:09
f250d69
Код
Авторство
О чём код?
/* Copyright 2012 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @module pdfjsLib */ import { AbortException, AnnotationMode, assert, getVerbosityLevel, info, isNodeJS, makeObj, RenderingIntentFlag, setVerbosityLevel, shadow, unreachable, warn, } from "../shared/util.js"; import { AnnotationStorage, PrintAnnotationStorage, SerializableEmpty, } from "./annotation_storage.js"; import { CanvasBBoxTracker, CanvasDependencyTracker, CanvasImagesTracker, } from "./canvas_dependency_tracker.js"; import { FontFaceObject, FontLoader } from "./font_loader.js"; import { FontInfo, FontPathInfo, PatternInfo, } from "./obj_bin_transform_display.js"; import { getDataProp, getFactoryUrlProp, getUrlProp, isRefProxy, LoopbackPort, } from "./api_utils.js"; import { isDataScheme, isValidFetchUrl, RenderingCancelledException, StatTimer, } from "./display_utils.js"; import { MessageHandler, wrapReason } from "../shared/message_handler.js"; import { NodeBinaryDataFactory, NodeCanvasFactory, NodeFilterFactory, } from "display-node_utils"; import { CanvasGraphics } from "./canvas.js"; import { DOMBinaryDataFactory } from "display-binary_data_factory"; import { DOMCanvasFactory } from "./canvas_factory.js"; import { DOMFilterFactory } from "./filter_factory.js"; import { getNetworkStream } from "display-network_stream"; import { GlobalWorkerOptions } from "./worker_options.js"; import { initGPU } from "./webgpu.js"; import { MathClamp } from "../shared/math_clamp.js"; import { Metadata } from "./metadata.js"; import { OptionalContentConfig } from "./optional_content_config.js"; import { PagesMapper } from "./pages_mapper.js"; import { PageViewport } from "./page_viewport.js"; import { PDFDataTransportStream } from "./transport_stream.js"; import { PDFObjects } from "./pdf_objects.js"; import { TextLayer } from "./text_layer.js"; import { XfaText } from "./xfa_text.js"; const RENDERING_CANCELLED_TIMEOUT = 100; // ms /** * @import { * CatalogAttachmentContent, * CatalogAttachment * } from "../core/catalog.js"; */ /** * @typedef { Int8Array | Uint8Array | Uint8ClampedArray | * Int16Array | Uint16Array | * Int32Array | Uint32Array | Float32Array | * Float64Array * } TypedArray */ /** * @typedef {Object} RefProxy * @property {number} num * @property {number} gen */ /** * Document initialization / loading parameters object. * * @typedef {Object} DocumentInitParameters * @property {string | URL} [url] - The URL of the PDF. * @property {TypedArray | ArrayBuffer | Array<number> | string} [data] - * Binary PDF data. * Use TypedArrays (Uint8Array) to improve the memory usage. If PDF data is * BASE64-encoded, use `atob()` to convert it to a binary string first. * * NOTE: If TypedArrays are used they will generally be transferred to the * worker-thread. This will help reduce main-thread memory usage, however * it will take ownership of the TypedArrays. * @property {Object} [httpHeaders] - Basic authentication headers. * @property {boolean} [withCredentials] - Indicates whether or not * cross-site Access-Control requests should be made using credentials such * as cookies or authorization headers. The default is `false`. * @property {string} [password] - For decrypting password-protected PDFs. * @property {PDFDataRangeTransport} [range] - Allows for using a custom range * transport implementation. * @property {number} [rangeChunkSize] - Specify maximum number of bytes fetched * per range request. The default value is 65536 (= 2^16). * @property {PDFWorker} [worker] - The worker that will be used for loading and * parsing the PDF data. * @property {number} [verbosity] - Controls the logging level; the constants * from {@link VerbosityLevel} should be used. * @property {string} [docBaseUrl] - The base URL of the document, used when * attempting to recover valid absolute URLs for annotations, and outline * items, that (incorrectly) only specify relative URLs. * @property {string} [cMapUrl] - The URL where the predefined Adobe CMaps are * located. Include the trailing slash. * @property {boolean} [cMapPacked] - Specifies if the Adobe CMaps are binary * packed or not. The default value is `true`. * @property {string} [iccUrl] - The URL where the predefined ICC profiles are * located. Include the trailing slash. * @property {boolean} [useSystemFonts] - When `true`, fonts that aren't * embedded in the PDF document will fallback to a system font. * The default value is `true` in web environments and `false` in Node.js; * unless `disableFontFace === true` in which case this defaults to `false` * regardless of the environment (to prevent completely broken fonts). * @property {string} [standardFontDataUrl] - The URL where the standard font * files are located. Include the trailing slash. * @property {string} [wasmUrl] - The URL where the wasm files are located. * Include the trailing slash. * @property {boolean} [useWorkerFetch] - Enable using the Fetch API in the * worker-thread when reading built-in CMap files, standard font files, * and wasm files. If `true`, the `BinaryDataFactory` option is ignored. * The default value is `true` in web environments and `false` in Node.js. * @property {boolean} [useWasm] - Attempt to use WebAssembly in order to * improve e.g. image decoding performance. * The default value is `true`. * @property {boolean} [stopAtErrors] - Reject certain promises, e.g. * `getOperatorList`, `getTextContent`, and `RenderTask`, when the associated * PDF data cannot be successfully parsed, instead of attempting to recover * whatever possible of the data. The default value is `false`. * @property {number} [maxImageSize] - The maximum allowed image size in total * pixels, i.e. width * height. Images above this value will not be rendered. * Use -1 for no limit, which is also the default value. * @property {boolean} [isOffscreenCanvasSupported] - Determines if we can use * `OffscreenCanvas` in the worker. Primarily used to improve performance of * image conversion/rendering. * The default value is `true` in web environments and `false` in Node.js. * @property {boolean} [isImageDecoderSupported] - Determines if we can use * `ImageDecoder` in the worker. Primarily used to improve performance of * image conversion/rendering. * The default value is `true` in web environments and `false` in Node.js. * @property {number} [canvasMaxAreaInBytes] - The integer value is used to * know when an image must be resized (uses `OffscreenCanvas` in the worker). * If it's -1 then a possibly slow algorithm is used to guess the max value. * @property {boolean} [disableFontFace] - By default fonts are converted to * OpenType fonts and loaded via the Font Loading API or `@font-face` rules. * If disabled, fonts will be rendered using a built-in font renderer that * constructs the glyphs with primitive path commands. * The default value is `false` in web environments and `true` in Node.js. * @property {boolean} [fontExtraProperties] - Include additional properties, * which are unused during rendering of PDF documents, when exporting the * parsed font data from the worker-thread. This may be useful for debugging * purposes (and backwards compatibility), but note that it will lead to * increased memory usage. The default value is `false`. * @property {boolean} [enableXfa] - Render Xfa forms if any. * The default value is `false`. * @property {HTMLDocument} [ownerDocument] - Specify an explicit document * context to create elements with and to load resources, such as fonts, * into. Defaults to the current document. * @property {boolean} [disableRange] - Disable range request loading of PDF * files. When enabled, and if the server supports partial content requests, * then the PDF will be fetched in chunks. The default value is `false`. * @property {boolean} [disableStream] - Disable streaming of PDF file data. * By default PDF.js attempts to load PDF files in chunks. The default value * is `false`. * @property {boolean} [disableAutoFetch] - Disable pre-fetching of PDF file * data. When range requests are enabled PDF.js will automatically keep * fetching more data even if it isn't needed to display the current page. * The default value is `false`. * * NOTE: It is also necessary to disable streaming, see above, in order for * disabling of pre-fetching to work correctly. * @property {boolean} [pdfBug] - Enables special hooks for debugging PDF.js * (see `web/debugger.js`). The default value is `false`. * @property {Object} [CanvasFactory] - The factory that will be used when * creating canvases. The default value is {DOMCanvasFactory}. * @property {Object} [FilterFactory] - The factory that will be used to * create SVG filters when rendering some images on the main canvas. * The default value is {DOMFilterFactory}. * @property {Object} [BinaryDataFactory] - The factory that will be used when * falling back to reading built-in CMap files, standard font files, * and wasm files in the main-thread. * The default value is {DOMBinaryDataFactory}. * @property {boolean} [enableHWA] - Enables hardware acceleration for * rendering. The default value is `false`. * @property {Object} [pagesMapper] - The pages mapper that will be used to map * page ids and page numbers. It's used when the page order is changed or some * pages are removed, cloned, etc. */ /** * This is the main entry point for loading a PDF and interacting with it. * * NOTE: If a URL is used to fetch the PDF data a standard Fetch API call (or * XHR as fallback) is used, which means it must follow same origin rules, * e.g. no cross-domain requests without CORS. * * @param {DocumentInitParameters} src - Parameter object. * @returns {PDFDocumentLoadingTask} */ function getDocument(src = {}) { const task = new PDFDocumentLoadingTask(); const { docId } = task; const url = src.url ? getUrlProp(src.url) : null; const data = src.data ? getDataProp(src.data) : null; const httpHeaders = src.httpHeaders || null; const withCredentials = src.withCredentials === true; const password = src.password ?? null; const rangeTransport = src.range instanceof PDFDataRangeTransport ? src.range : null; const rangeChunkSize = Number.isInteger(src.rangeChunkSize) && src.rangeChunkSize > 0 ? src.rangeChunkSize : 2 ** 16; let worker = src.worker instanceof PDFWorker ? src.worker : null; const verbosity = src.verbosity; // Ignore "data:"-URLs, since they can't be used to recover valid absolute // URLs anyway. We want to avoid sending them to the worker-thread, since // they contain the *entire* PDF document and can thus be arbitrarily long. const docBaseUrl = typeof src.docBaseUrl === "string" && !isDataScheme(src.docBaseUrl) ? src.docBaseUrl : null; const cMapUrl = getFactoryUrlProp(src.cMapUrl); const cMapPacked = src.cMapPacked !== false; const iccUrl = getFactoryUrlProp(src.iccUrl); const standardFontDataUrl = getFactoryUrlProp(src.standardFontDataUrl); const wasmUrl = getFactoryUrlProp(src.wasmUrl); const ignoreErrors = src.stopAtErrors !== true; const maxImageSize = Number.isInteger(src.maxImageSize) && src.maxImageSize > -1 ? src.maxImageSize : -1; const isOffscreenCanvasSupported = typeof src.isOffscreenCanvasSupported === "boolean" ? src.isOffscreenCanvasSupported : !isNodeJS; const isImageDecoderSupported = typeof src.isImageDecoderSupported === "boolean" ? src.isImageDecoderSupported : !isNodeJS; const canvasMaxAreaInBytes = Number.isInteger(src.canvasMaxAreaInBytes) ? src.canvasMaxAreaInBytes : -1; const disableFontFace = typeof src.disableFontFace === "boolean" ? src.disableFontFace : isNodeJS; const fontExtraProperties = src.fontExtraProperties === true; const enableXfa = src.enableXfa === true; const ownerDocument = src.ownerDocument || globalThis.document; const disableRange = src.disableRange === true; const disableStream = src.disableStream === true; const disableAutoFetch = src.disableAutoFetch === true; const pdfBug = src.pdfBug === true; const CanvasFactory = src.CanvasFactory || (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC") && isNodeJS ? NodeCanvasFactory : DOMCanvasFactory); const FilterFactory = src.FilterFactory || (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC") && isNodeJS ? NodeFilterFactory : DOMFilterFactory); const BinaryDataFactory = src.BinaryDataFactory || (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC") && isNodeJS ? NodeBinaryDataFactory : DOMBinaryDataFactory); const enableHWA = src.enableHWA === true; const enableWebGPU = src.enableWebGPU === true; // Start GPU initialisation immediately so it runs in parallel with the // worker bootstrap; the resolved boolean is forwarded to the worker. const gpuPromise = enableWebGPU ? initGPU() : Promise.resolve(false); const useWasm = src.useWasm !== false; const pagesMapper = src.pagesMapper || new PagesMapper(); // Parameters whose default values depend on other parameters. const useSystemFonts = typeof src.useSystemFonts === "boolean" ? src.useSystemFonts : !isNodeJS && !disableFontFace; const useWorkerFetch = typeof src.useWorkerFetch === "boolean" ? src.useWorkerFetch : (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) || !!( BinaryDataFactory === DOMBinaryDataFactory && cMapUrl && cMapPacked && standardFontDataUrl && wasmUrl && isValidFetchUrl(cMapUrl, document.baseURI) && isValidFetchUrl(standardFontDataUrl, document.baseURI) && isValidFetchUrl(wasmUrl, document.baseURI) ); // Parameters only intended for development/testing purposes. const styleElement = typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING") ? src.styleElement : null; // Set the main-thread verbosity level. setVerbosityLevel(verbosity); // Ensure that the various factories can be initialized, when necessary, // since the user may provide *custom* ones. const transportFactory = { canvasFactory: new CanvasFactory({ ownerDocument, enableHWA }), filterFactory: new FilterFactory({ docId, ownerDocument }), binaryDataFactory: (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) || useWorkerFetch ? null : new BinaryDataFactory({ cMapUrl, standardFontDataUrl, wasmUrl }), }; if (!worker) { // Worker was not provided -- creating and owning our own. If message port // is specified in global worker options, using it. worker = PDFWorker.create({ verbosity, port: GlobalWorkerOptions.workerPort, }); task._worker = worker; } const docParams = { docId, apiVersion: typeof PDFJSDev !== "undefined" && !PDFJSDev.test("TESTING") ? PDFJSDev.eval("BUNDLE_VERSION") : null, data, password, disableAutoFetch, rangeChunkSize, docBaseUrl, enableXfa, evaluatorOptions: { maxImageSize, disableFontFace, ignoreErrors, isOffscreenCanvasSupported, isImageDecoderSupported, canvasMaxAreaInBytes, fontExtraProperties, useSystemFonts, useWasm, useWorkerFetch, cMapUrl, cMapPacked, iccUrl, standardFontDataUrl, wasmUrl, hasGPU: false, // Set below. }, }; const transportParams = { ownerDocument, pdfBug, styleElement, enableHWA, loadingParams: { disableAutoFetch, enableXfa, }, }; Promise.all([worker.promise, gpuPromise]) .then(function ([, hasGPU]) { if (worker.destroyed) { throw new Error("Worker was destroyed"); } docParams.evaluatorOptions.hasGPU = hasGPU; const workerIdPromise = worker.messageHandler.sendWithPromise( "GetDocRequest", docParams, data ? [data.buffer] : null ); let networkStream; if (data) { // The entire PDF was provided, no `networkStream` necessary. } else if (rangeTransport) { networkStream = new PDFDataTransportStream({ pdfDataRangeTransport: rangeTransport, disableRange, disableStream, }); } else if (url) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { throw new Error("Not implemented: NetworkStream"); } const NetworkStream = getNetworkStream(url); networkStream = new NetworkStream({ url, httpHeaders, withCredentials, rangeChunkSize, disableRange, disableStream, }); } else { throw new Error( "getDocument - expected either `data`, `range`, or `url` parameter." ); } return workerIdPromise.then(workerId => { if (worker.destroyed) { throw new Error("Worker was destroyed"); } const messageHandler = new MessageHandler(docId, workerId, worker.port); const transport = new WorkerTransport( messageHandler, task, networkStream, transportParams, transportFactory, pagesMapper ); task._transport = transport; if (task.destroyed) { // `destroy()` was called during the worker handshake; the orderly // shutdown (including the "Terminate" message) will be issued // through the transport once destroy resumes. throw new Error("Loading aborted"); } messageHandler.send("Ready", null); }); }) .catch(task._capability.reject) .finally(task._setupCapability.resolve); return task; } /** * @typedef {Object} OnProgressParameters * @property {number} loaded - Currently loaded number of bytes. * @property {number} total - Total number of bytes in the PDF file. * @property {number} percent - Currently loaded percentage, as an integer value * in the [0, 100] range. If `total` is undefined, the percentage is `NaN`. */ /** * The loading task controls the operations required to load a PDF document * (such as network requests) and provides a way to listen for completion, * after which individual pages can be rendered. */ class PDFDocumentLoadingTask { static #docId = 0; /** * @private */ _capability = Promise.withResolvers(); /** * Resolves once the load-time setup chain has settled, regardless of * outcome; used by `destroy()` to wait until `_transport` is either set * or definitely never going to be. * @private */ _setupCapability = Promise.withResolvers(); /** * @private */ _transport = null; /** * @private */ _worker = null; /** * Unique identifier for the document loading task. * @type {string} */ docId = `d${PDFDocumentLoadingTask.#docId++}`; /** * Whether the loading task is destroyed or not. * @type {boolean} */ destroyed = false; /** * Callback to request a password if a wrong or no password was provided. * The callback receives two parameters: a function that should be called * with the new password, and a reason (see {@link PasswordResponses}). * @type {function} */ onPassword = null; /** * Callback to be able to monitor the loading progress of the PDF file * (necessary to implement e.g. a loading bar). * The callback receives an {@link OnProgressParameters} argument. * @type {function} */ onProgress = null; /** * Promise for document loading task completion. * @type {Promise<PDFDocumentProxy>} */ get promise() { return this._capability.promise; } /** * Abort all network requests and destroy the worker. * @returns {Promise<void>} A promise that is resolved when destruction is * completed. */ async destroy() { this.destroyed = true; // The setup chain rejects `_capability` with "Loading aborted" once the // load-time chain unwinds (see `getDocument`). Claim that rejection // here so it isn't reported as unhandled during the awaits below; // callers awaiting `task.promise` still see it. this._capability.promise.catch(() => {}); try { // `_pendingDestroy` must be set synchronously, before any `await`, // so subsequent `PDFWorker.create()` calls on the shared `workerPort` // observe it and throw (see issue 16777). if (this._worker?.port) { this._worker._pendingDestroy = true; } // Wait for the load-time setup chain to settle so `_transport` is set // (when applicable) before we tear down. This is what guarantees the // "Terminate" message gets sent through `WorkerTransport.destroy` if // `destroy` races with the initial worker handshake. await this._setupCapability.promise; await this._transport?.destroy(); } catch (ex) { if (this._worker?.port) { delete this._worker._pendingDestroy; } throw ex; } this._transport = null; this._worker?.destroy(); this._worker = null; } /** * Attempt to fetch the raw data of the PDF document, when e.g. * - An exception was thrown during document initialization. * - An `onPassword` callback is delaying initialization. * @returns {Promise<Uint8Array>} */ async getData() { return this._transport.getData(); } } /** * Abstract class to support range requests file loading. * * NOTE: The TypedArrays passed to the constructor and relevant methods below * will generally be transferred to the worker-thread. This will help reduce * main-thread memory usage, however it will take ownership of the TypedArrays. */ class PDFDataRangeTransport { #capability = Promise.withResolvers(); #listener = null; /** * @param {number} length * @param {Uint8Array|null} initialData * @param {boolean} [progressiveDone] * @param {string} [contentDispositionFilename] */ constructor( length, initialData, progressiveDone = false, contentDispositionFilename = null ) { this.length = length; this.initialData = initialData; this.progressiveDone = progressiveDone; this.contentDispositionFilename = contentDispositionFilename; } /** * @param {number} begin * @param {Uint8Array|null} chunk */ onDataRange(begin, chunk) { this.#listener({ type: "range", begin, chunk }); } /** * @param {Uint8Array|null} chunk */ onDataProgressiveRead(chunk) { this.#capability.promise.then(() => { this.#listener({ type: "progressiveRead", chunk }); }); } onDataProgressiveDone() { this.#capability.promise.then(() => { this.#listener({ type: "progressiveDone" }); }); } transportReady(listener) { this.#listener = listener; this.#capability.resolve(); } /** * @param {number} begin * @param {number} end */ requestDataRange(begin, end) { unreachable("Abstract method PDFDataRangeTransport.requestDataRange"); } abort() {} } /** * Proxy to a `PDFDocument` in the worker thread. */ class PDFDocumentProxy { constructor(pdfInfo, transport) { this._pdfInfo = pdfInfo; this._transport = transport; if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { // For testing purposes. Object.defineProperty(this, "getNetworkStreamName", { value: () => this._transport.getNetworkStreamName(), }); Object.defineProperty(this, "getXFADatasets", { value: () => this._transport.getXFADatasets(), }); Object.defineProperty(this, "getStartXRefPos", { value: () => this._transport.getStartXRefPos(), }); Object.defineProperty(this, "getAnnotArray", { value: pageIndex => this._transport.getAnnotArray(pageIndex), }); } } /** * @type {PagesMapper} The pages mapper instance. */ get pagesMapper() { return this._transport.pagesMapper; } /** * @type {AnnotationStorage} Storage for annotation data in forms. */ get annotationStorage() { return this._transport.annotationStorage; } /** * @type {Object} The canvas factory instance. */ get canvasFactory() { return this._transport.canvasFactory; } /** * @type {Object} The filter factory instance. */ get filterFactory() { return this._transport.filterFactory; } /** * @type {number} Total number of pages in the PDF file. */ get numPages() { return this._pdfInfo.numPages; } /** * @type {Array<string | null>} A (not guaranteed to be) unique ID to identify * the PDF document. * NOTE: The first element will always be defined for all PDF documents, * whereas the second element is only defined for *modified* PDF documents. */ get fingerprints() { return this._pdfInfo.fingerprints; } /** * @type {boolean} True if only XFA form. */ get isPureXfa() { return shadow(this, "isPureXfa", !!this._transport._htmlForXfa); } /** * NOTE: This is (mostly) intended to support printing of XFA forms. * * @type {Object | null} An object representing a HTML tree structure * to render the XFA, or `null` when no XFA form exists. */ get allXfaHtml() { return this._transport._htmlForXfa; } /** * @param {number} pageNumber - The page number to get. The first page is 1. * @returns {Promise<PDFPageProxy>} A promise that is resolved with * a {@link PDFPageProxy} object. */ getPage(pageNumber) { return this._transport.getPage(pageNumber); } /** * @param {RefProxy} ref - The page reference. * @returns {Promise<number>} A promise that is resolved with the page index, * starting from zero, that is associated with the reference. */ getPageIndex(ref) { return this._transport.getPageIndex(ref); } /** * @returns {Promise<Map<string, Array<any>>>} A promise that is resolved * with a mapping from named destinations to references. * * This can be slow for large documents. Use `getDestination` instead. */ getDestinations() { return this._transport.getDestinations(); } /** * @param {string} id - The named destination to get. * @returns {Promise<Array<any> | null>} A promise that is resolved with all * information of the given named destination, or `null` when the named * destination is not present in the PDF file. */ getDestination(id) { return this._transport.getDestination(id); } /** * @returns {Promise<Array<string> | null>} A promise that is resolved with * an {Array} containing the page labels that correspond to the page * indexes, or `null` when no page labels are present in the PDF file. */ getPageLabels() { return this._transport.getPageLabels(); } /** * @returns {Promise<string>} A promise that is resolved with a {string} * containing the page layout name. */ getPageLayout() { return this._transport.getPageLayout(); } /** * @returns {Promise<string>} A promise that is resolved with a {string} * containing the page mode name. */ getPageMode() { return this._transport.getPageMode(); } /** * @returns {Promise<Map | null>} A promise that is resolved with a {Map} * containing the viewer preferences, or `null` when no viewer preferences * are present in the PDF file. */ getViewerPreferences() { return this._transport.getViewerPreferences(); } /** * @returns {Promise<Map | null>} A promise that is resolved with a {Map} * containing a destination or action, or `null` when no open action is * present in the PDF. */ getOpenAction() { return this._transport.getOpenAction(); } /** * @returns {Promise<Map<string, CatalogAttachment> | null>} * Promise that is resolved with a lookup table for mapping named * attachments to their content. */ getAttachments() { return this._transport.getAttachments(); } /** * @param {string} id * Unique attachment identifier (required). * @returns {Promise<CatalogAttachmentContent>} * Promise that resolves to attachment content. */ getAttachmentContent(id) { return this._transport.getAttachmentContent(id); } /** * @param {Set<number>} types - The annotation types to retrieve. * @param {Set<number>} pageIndexesToSkip * @returns {Promise<Array<Object>>} A promise that is resolved with a list of * annotations data. */ getAnnotationsByType(types, pageIndexesToSkip) { return this._transport.getAnnotationsByType(types, pageIndexesToSkip); } /** * @returns {Promise<Map | null>} A promise that is resolved with a {Map} with * the JavaScript actions: * - from the name tree. * - from A or AA entries in the catalog dictionary. * , or `null` if no JavaScript exists. */ getJSActions() { return this._transport.getDocJSActions(); } /** * @typedef {Object} OutlineNode * @property {string} title * @property {boolean} bold * @property {boolean} italic * @property {Uint8ClampedArray} color - The color in RGB format to use for * display purposes. * @property {string | Array<any> | null} dest * @property {string | null} url * @property {string | undefined} unsafeUrl * @property {boolean | undefined} newWindow * @property {number | undefined} count * @property {Array<OutlineNode>} items */ /** * @returns {Promise<Array<OutlineNode>>} A promise that is resolved with an * {Array} that is a tree outline (if it has one) of the PDF file. */ getOutline() { return this._transport.getOutline(); } /** * @typedef {Object} GetOptionalContentConfigParameters * @property {string} [intent] - Determines the optional content groups that * are visible by default; valid values are: * - 'display' (viewable groups). * - 'print' (printable groups). * - 'any' (all groups). * The default value is 'display'. */ /** * @param {GetOptionalContentConfigParameters} [params] - Optional content * config parameters. * @returns {Promise<OptionalContentConfig>} A promise that is resolved with * an {@link OptionalContentConfig} that contains all the optional content * groups (assuming that the document has any). */ getOptionalContentConfig({ intent = "display" } = {}) { const { renderingIntent } = this._transport.getRenderingIntent(intent); return this._transport.getOptionalContentConfig(renderingIntent); } /** * @returns {Promise<Set<number> | null>} A promise that is resolved with * a {Set} that contains the permission flags for the PDF document, * or `null` when no permissions are present in the PDF file. */ getPermissions() { return this._transport.getPermissions(); } /** * @returns {Promise<{ info: Object, metadata: Metadata }>} A promise that is * resolved with an {Object} that has `info` and `metadata` properties. * `info` is an {Object} filled with anything available in the information * dictionary and similarly `metadata` is a {Metadata} object with * information from the metadata section of the PDF. */ getMetadata() { return this._transport.getMetadata(); } /** * @typedef {Object} MarkInfo * Properties correspond to Table 321 of the PDF 32000-1:2008 spec. * @property {boolean} Marked * @property {boolean} UserProperties * @property {boolean} Suspects */ /** * @returns {Promise<MarkInfo | null>} A promise that is resolved with * a {MarkInfo} object that contains the MarkInfo flags for the PDF * document, or `null` when no MarkInfo values are present in the PDF file. */ getMarkInfo() { return this._transport.getMarkInfo(); } /** * @returns {Promise<Uint8Array>} A promise that is resolved with a * {Uint8Array} containing the raw data of the PDF document. */ getData() { return this._transport.getData(); } /** * @returns {Promise<Uint8Array<ArrayBuffer>>} A promise that is * resolved with a {Uint8Array<ArrayBuffer>} containing the * full data of the saved document. */ saveDocument() { return this._transport.saveDocument(); } /** * @typedef {Object} PageInfo * @property {null|Uint8Array} [document] * @property {ImageBitmap} [image] Image to insert as a synthetic page. * @property {Array<Array<number>|number>} [includePages] * included ranges or indices. * @property {Array<Array<number>|number>} [excludePages] * excluded ranges or indices. * @property {Array<number>} [pageIndices] Explicit 0-based positions in the * final document for pages contributed by this entry. If shorter than the * filtered page list, the remaining pages are placed in the first free * slots at extraction time. Positions must not overlap with those of * other entries, and the union of all explicit/auto-filled positions * across the call must form a dense `[0, N)` range (where `N` is the * total page count of the final document) — sparse layouts leave empty * slots and are not supported. Cannot be combined with `insertAfter` on * the same entry, and must fully cover the filtered page list when any * entry in the same call specifies `insertAfter` (partial arrays are * rejected in that case). * @property {number} [insertAfter] 0-based index in the base sequential * sequence (the concatenation of entries that have neither `pageIndices` * nor `insertAfter`) after which to insert the pages. When every * contributing entry carries explicit `pageIndices`, this is interpreted * against that explicit layout instead, shifting any existing positions * beyond the insertion point to make room. Use `-1` to insert before * everything. Values beyond the current layout are clamped so the pages * are appended at the end. Cannot be combined with `pageIndices` on the * same entry. */ /** * @param {Array<PageInfo>} pageInfos - The pages to extract. * @param {Int32Array} [copyLevels] - For each viewer page, its rank among the * extracted pages sharing the same source page, or -1 if it isn't extracted. * This routes editor annotations when the viewer contains multiple copies * of a source page. * @returns {Promise<Uint8Array>} A promise that is resolved with a * {Uint8Array} containing the full data of the saved document. */ extractPages(pageInfos, copyLevels = null) { return this._transport.extractPages(pageInfos, copyLevels); } /** * @returns {Promise<{ length: number }>} A promise that is resolved when the * document's data is loaded. It is resolved with an {Object} that contains * the `length` property that indicates size of the PDF data in bytes. */ getDownloadInfo() { return this._transport.downloadInfoCapability.promise; } getRawData(data) { return this._transport.getRawData(data); } /** * Cleans up resources allocated by the document on both the main and worker * threads. * * NOTE: Do not, under any circumstances, call this method when rendering is * currently ongoing since that may lead to rendering errors. * * @param {boolean} [keepLoadedFonts] - Let fonts remain attached to the DOM. * NOTE: This will increase persistent memory usage, hence don't use this * option unless absolutely necessary. The default value is `false`. * @returns {Promise} A promise that is resolved when clean-up has finished. */ cleanup(keepLoadedFonts = false) { return this._transport.startCleanup(keepLoadedFonts || this.isPureXfa); } /** * @param {RefProxy} ref - The page reference. * @returns {number | null} The page number, if it's cached. */ cachedPageNumber(ref) { return this._transport.cachedPageNumber(ref); } /** * @type {DocumentInitParameters} A subset of the current * {DocumentInitParameters}, which are needed in the viewer. */ get loadingParams() { return this._transport.loadingParams; } /** * @type {PDFDocumentLoadingTask} The loadingTask for the current document. */ get loadingTask() { return this._transport.loadingTask; } /** * @returns {Promise<Map<string, Array<Object>> | null>} A promise that is * resolved with a {Map} containing /AcroForm field data for the JS sandbox, * or `null` when no field data is present in the PDF file. */ getFieldObjects() { return this._transport.getFieldObjects(); } /** * @returns {Promise<Array<Object> | null>} A promise that is resolved * with an {Array} of digital signature metadata (signerName, reason, * signingTime, byteRange, subFilter, …), or `null` when the document * has no signatures. The PKCS#7 blob and signed-data byte spans * needed for verification are fetched separately via * {@link PDFDocumentProxy.getSignatureData} so they don't ride the * worker boundary unless verification is actually requested. */ getSignatures() { return this._transport.getSignatures(); } /** * @param {string} id Signature `id` from a {@link getSignatures} entry. * @returns {Promise<{ data: Uint8Array[], pkcs7: Uint8Array } | null>} * The byte payload needed to verify the signature, or `null` if the * id is unknown. */ getSignatureData(id) { return this._transport.getSignatureData(id); } /** * @returns {Promise<boolean>} A promise that is resolved with `true` * if some /AcroForm fields have JavaScript actions. */ hasJSActions() { return this._transport.hasJSActions(); } /** * @returns {Promise<Array<string> | null>} A promise that is resolved with an * {Array<string>} containing IDs of annotations that have a calculation * action, or `null` when no such annotations are present in the PDF file. */ getCalculationOrderIds() { return this._transport.getCalculationOrderIds(); } } /** * Page getViewport parameters. * * @typedef {Object} GetViewportParameters * @property {number} scale - The desired scale of the viewport. * @property {number} [rotation] - The desired rotation, in degrees, of * the viewport. If omitted it defaults to the page rotation. * @property {number} [offsetX] - The horizontal, i.e. x-axis, offset. * The default value is `0`. * @property {number} [offsetY] - The vertical, i.e. y-axis, offset. * The default value is `0`. * @property {boolean} [dontFlip] - If true, the y-axis will not be * flipped. The default value is `false`. */ /** * Page getTextContent parameters. * * @typedef {Object} getTextContentParameters * @property {boolean} [includeMarkedContent] - When true include marked * content items in the items array of TextContent. The default is `false`. * @property {boolean} [disableNormalization] - When true the text is *not* * normalized in the worker-thread. The default is `false`. */ /** * Page text content. * * @typedef {Object} TextContent * @property {Array<TextItem | TextMarkedContent>} items - Array of * {@link TextItem} and {@link TextMarkedContent} objects. TextMarkedContent * items are included when includeMarkedContent is true. * @property {Object<string, TextStyle>} styles - {@link TextStyle} objects, * indexed by font name. * @property {string | null} lang - The document /Lang attribute. */ /** * Page text content part. * * @typedef {Object} TextItem * @property {string} str - Text content. * @property {string} dir - Text direction: 'ttb', 'ltr' or 'rtl'. * @property {Array<any>} transform - Transformation matrix. * @property {number} width - Width in device space. * @property {number} height - Height in device space. * @property {string} fontName - Font name used by PDF.js for converted font. * @property {boolean} hasEOL - Indicating if the text content is followed by a * line-break. */ /** * Page text marked content part. * * @typedef {Object} TextMarkedContent * @property {string} type - Either 'beginMarkedContent', * 'beginMarkedContentProps', or 'endMarkedContent'. * @property {string} id - The marked content identifier. Only used for type * 'beginMarkedContentProps'. */ /** * Text style. * * @typedef {Object} TextStyle * @property {number} ascent - Font ascent. * @property {number} descent - Font descent. * @property {boolean} vertical - Whether or not the text is in vertical mode. * @property {string} fontFamily - The possible font family. */ /** * Page annotation parameters. * * @typedef {Object} GetAnnotationsParameters * @property {string} [intent] - Determines the annotations that are fetched, * can be 'display' (viewable annotations), 'print' (printable annotations), * or 'any' (all annotations). The default value is 'display'. */ /** * Page render parameters. * * @typedef {Object} RenderParameters * @property {HTMLCanvasElement|null} canvas - A DOM Canvas object. The default * value is the canvas associated with the `canvasContext` parameter if no * value is provided explicitly. * @property {PageViewport} viewport - Rendering viewport obtained by calling * the `PDFPageProxy.getViewport` method. * @property {CanvasRenderingContext2D} [canvasContext] - 2D context of a DOM * Canvas object for backwards compatibility; it is recommended to use the * `canvas` parameter instead. * If the context must absolutely be used to render the page, the canvas must * be null. * @property {string} [intent] - Rendering intent, can be 'display', 'print', * or 'any'. The default value is 'display'. * @property {number} [annotationMode] Controls which annotations are rendered * onto the canvas, for annotations with appearance-data; the values from * {@link AnnotationMode} should be used. The following values are supported: * - `AnnotationMode.DISABLE`, which disables all annotations. * - `AnnotationMode.ENABLE`, which includes all possible annotations (thus * it also depends on the `intent`-option, see above). * - `AnnotationMode.ENABLE_FORMS`, which excludes annotations that contain * interactive form elements (those will be rendered in the display layer). * - `AnnotationMode.ENABLE_STORAGE`, which includes all possible annotations * (as above) but where interactive form elements are updated with data * from the {@link AnnotationStorage}-instance; useful e.g. for printing. * The default value is `AnnotationMode.ENABLE`. * @property {Array<any>} [transform] - Additional transform, applied just * before viewport transform. * @property {CanvasGradient | CanvasPattern | string} [background] - Background * to use for the canvas. * Any valid `canvas.fillStyle` can be used: a `DOMString` parsed as CSS * <color> value, a `CanvasGradient` object (a linear or radial gradient) or * a `CanvasPattern` object (a repetitive image). The default value is * 'rgb(255,255,255)'. * * NOTE: This option may be partially, or completely, ignored when the * `pageColors`-option is used. * @property {Object} [pageColors] - Overwrites background and foreground colors * with user defined ones in order to improve readability in high contrast * mode. * @property {Promise<OptionalContentConfig>} [optionalContentConfigPromise] - * A promise that should resolve with an {@link OptionalContentConfig} * created from `PDFDocumentProxy.getOptionalContentConfig`. If `null`, * the configuration will be fetched automatically with the default visibility * states set. * @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] - Map some * annotation ids with canvases used to render them. * @property {PrintAnnotationStorage} [printAnnotationStorage] * @property {boolean} [isEditing] - Render the page in editing mode. * @property {boolean} [recordImages] - Record the location of images in the PDF * @property {boolean} [recordOperations] - Record the dependencies and bounding * boxes of all PDF operations that render onto the canvas. * @property {OperationsFilter} [operationsFilter] - If provided, only * run for which this function returns `true`. */ /** * @callback OperationsFilter * @param {number} index - The index of the operation. * @returns {boolean} If false, the operation is ignored. */ /** * Page getOperatorList parameters. * * @typedef {Object} GetOperatorListParameters * @property {string} [intent] - Rendering intent, can be 'display', 'print', * or 'any'. The default value is 'display'. * @property {number} [annotationMode] Controls which annotations are included * in the operatorList, for annotations with appearance-data; the values from * {@link AnnotationMode} should be used. The following values are supported: * - `AnnotationMode.DISABLE`, which disables all annotations. * - `AnnotationMode.ENABLE`, which includes all possible annotations (thus * it also depends on the `intent`-option, see above). * - `AnnotationMode.ENABLE_FORMS`, which excludes annotations that contain * interactive form elements (those will be rendered in the display layer). * - `AnnotationMode.ENABLE_STORAGE`, which includes all possible annotations * (as above) but where interactive form elements are updated with data * from the {@link AnnotationStorage}-instance; useful e.g. for printing. * The default value is `AnnotationMode.ENABLE`. * @property {PrintAnnotationStorage} [printAnnotationStorage] * @property {boolean} [isEditing] - Render the page in editing mode. */ /** * Structure tree node. The root node will have a role "Root". * * @typedef {Object} StructTreeNode * @property {Array<StructTreeNode | StructTreeContent>} children - Array of * {@link StructTreeNode} and {@link StructTreeContent} objects. * @property {string} role - element's role, already mapped if a role map exists * in the PDF. * @property {string} [structId] - A table header's structure element * identifier, i.e. its `ID` entry. Note that this is unrelated to the `id` * property of a {@link StructTreeContent} object. * @property {number} [rowSpan] - The number of rows spanned by a table cell. * @property {number} [colSpan] - The number of columns spanned by a table cell. * @property {Array<string>} [headers] - The `structId` values of the table * headers associated with a table cell. * @property {"Row" | "Column" | "Both"} [scope] - The cells to which a table * header applies. * @property {string} [short] - An abbreviated version of a table header's * content. * @property {string} [summary] - A summary of a table's purpose and structure. */ /** * Structure tree content. * * @typedef {Object} StructTreeContent * @property {string} type - either "content" for page and stream structure * elements or "object" for object references. * @property {string} id - unique id that will map to the text layer. */ /** * PDF page operator list. * * @typedef {Object} PDFOperatorList * @property {Array<number>} fnArray - Array containing the operator functions. * @property {Array<any>} argsArray - Array containing the arguments of the * functions. */ /** * Proxy to a `PDFPage` in the worker thread. */ class PDFPageProxy { #pendingCleanup = false; #pagesMapper = null; constructor(pageIndex, pageInfo, transport, pagesMapper, pdfBug = false) { this._pageIndex = pageIndex; this._pageInfo = pageInfo; this._transport = transport; this._stats = pdfBug ? new StatTimer() : null; this._pdfBug = pdfBug; /** @type {PDFObjects} */ this.commonObjs = transport.commonObjs; this.objs = new PDFObjects(); this._intentStates = new Map(); this.destroyed = false; this.recordedBBoxes = null; this.#pagesMapper = pagesMapper; this.imageCoordinates = null; } clone(id) { const clone = new PDFPageProxy( id, this._pageInfo, this._transport, this.#pagesMapper, this._pdfBug ); clone.clonedFromIndex = this.clonedFromIndex ?? this._pageIndex; this._transport.updatePage(clone); return clone; } /** * @type {number} Page number of the page. First page is 1. */ get pageNumber() { return this._pageIndex + 1; } /** * @param {number} value - The page number to set. First page is 1. */ set pageNumber(value) { this._pageIndex = value - 1; this._transport.updatePage(this); } /** * @type {number} The number of degrees the page is rotated clockwise. */ get rotate() { return this._pageInfo.rotate; } /** * @type {RefProxy | null} The reference that points to this page. */ get ref() { return this._pageInfo.ref; } /** * @type {number} The default size of units in 1/72nds of an inch. */ get userUnit() { return this._pageInfo.userUnit; } /** * @type {Array<number>} An array of the visible portion of the PDF page in * user space units [x1, y1, x2, y2]. */ get view() { return this._pageInfo.view; } /** * @param {GetViewportParameters} params - Viewport parameters. * @returns {PageViewport} Contains 'width' and 'height' properties * along with transforms required for rendering. */ getViewport({ scale, rotation = this.rotate, offsetX = 0, offsetY = 0, dontFlip = false, } = {}) { return new PageViewport({ viewBox: this.view, userUnit: this.userUnit, scale, rotation, offsetX, offsetY, dontFlip, }); } /** * @param {GetAnnotationsParameters} [params] - Annotation parameters. * @returns {Promise<Array<any>>} A promise that is resolved with an * {Array} of the annotation objects. */ getAnnotations({ intent = "display" } = {}) { const { renderingIntent } = this._transport.getRenderingIntent(intent); return this._transport.getAnnotations(this._pageIndex, renderingIntent); } /** * @returns {Promise<Map | null>} A promise that is resolved with a {Map} with * the JavaScript actions, or `null` if no JavaScript exists. */ getJSActions() { return this._transport.getPageJSActions(this._pageIndex); } /** * @type {Object} The filter factory instance. */ get filterFactory() { return this._transport.filterFactory; } /** * @type {boolean} True if only XFA form. */ get isPureXfa() { return shadow(this, "isPureXfa", !!this._transport._htmlForXfa); } /** * @returns {Promise<Object | null>} A promise that is resolved with * an {Object} with a fake DOM object (a tree structure where elements * are {Object} with a name, attributes (class, style, ...), value and * children, very similar to a HTML DOM tree), or `null` if no XFA exists. */ async getXfa() { return this._transport._htmlForXfa?.children[this._pageIndex] || null; } /** * Begins the process of rendering a page to the desired context. * * @param {RenderParameters} params - Page render parameters. * @returns {RenderTask} An object that contains a promise that is * resolved when the page finishes rendering. */ render({ canvasContext, canvas = canvasContext.canvas, viewport, intent = "display", annotationMode = AnnotationMode.ENABLE, transform = null, background = null, optionalContentConfigPromise = null, annotationCanvasMap = null, pageColors = null, printAnnotationStorage = null, isEditing = false, recordImages = false, recordOperations = false, operationsFilter = null, }) { this._stats?.time("Overall"); const intentArgs = this._transport.getRenderingIntent( intent, annotationMode, printAnnotationStorage, isEditing ); const { renderingIntent, cacheKey } = intentArgs; // If there was a pending destroy, cancel it so no cleanup happens during // this call to render. this.#pendingCleanup = false; optionalContentConfigPromise ||= this._transport.getOptionalContentConfig(renderingIntent); const intentState = this._intentStates.getOrInsertComputed( cacheKey, makeObj ); // Ensure that a pending `streamReader` cancel timeout is always aborted. if (intentState.streamReaderCancelTimeout) { clearTimeout(intentState.streamReaderCancelTimeout); intentState.streamReaderCancelTimeout = null; } const intentPrint = !!(renderingIntent & RenderingIntentFlag.PRINT); // If there's no displayReadyCapability yet, then the operatorList // was never requested before. Make the request and create the promise. if (!intentState.displayReadyCapability) { intentState.displayReadyCapability = Promise.withResolvers(); intentState.operatorList = { fnArray: [], argsArray: [], lastChunk: false, separateAnnots: null, }; this._stats?.time("Page Request"); this._pumpOperatorList(intentArgs); } const recordForDebugger = !!( this._pdfBug && globalThis.StepperManager?.enabled ); const shouldRecordOperations = !!canvas && !this.recordedBBoxes && (recordOperations || recordForDebugger); const shouldRecordImages = !!canvas && !this.imageCoordinates && recordImages; const complete = error => { intentState.renderTasks.delete(internalRenderTask); if (shouldRecordOperations) { const recordedBBoxes = internalRenderTask.gfx?.dependencyTracker.take(); if (recordedBBoxes) { internalRenderTask.stepper?.setOperatorBBoxes( recordedBBoxes, internalRenderTask.gfx.dependencyTracker.takeDebugMetadata() ); if (recordOperations) { this.recordedBBoxes = recordedBBoxes; } } } if (shouldRecordImages && !error) { this.imageCoordinates = internalRenderTask.gfx?.imagesTracker.take(); } // Attempt to reduce memory usage during *printing*, by always running // cleanup immediately once rendering has finished. if (intentPrint) { this.#pendingCleanup = true; } this.#tryCleanup(); if (error) { internalRenderTask.capability.reject(error); this._abortOperatorList({ intentState, reason: error instanceof Error ? error : new Error(error), }); } else { internalRenderTask.capability.resolve(); } if (this._stats) { this._stats.timeEnd("Rendering"); this._stats.timeEnd("Overall"); if (globalThis.Stats?.enabled) { globalThis.Stats.add(this.pageNumber, this._stats); } } }; let dependencyTracker = null; let bboxTracker = null; if (shouldRecordOperations || shouldRecordImages) { bboxTracker = new CanvasBBoxTracker( canvas, intentState.operatorList.length ); } if (shouldRecordOperations) { dependencyTracker = new CanvasDependencyTracker( bboxTracker, recordForDebugger ); } const internalRenderTask = new InternalRenderTask({ callback: complete, // Only include the required properties, and *not* the entire object. params: { canvas, canvasContext, dependencyTracker: dependencyTracker ?? bboxTracker, imagesTracker: shouldRecordImages ? new CanvasImagesTracker(canvas) : null, viewport, transform, background, }, objs: this.objs, commonObjs: this.commonObjs, annotationCanvasMap, operatorList: intentState.operatorList, pageIndex: this._pageIndex, canvasFactory: this._transport.canvasFactory, filterFactory: this._transport.filterFactory, useRequestAnimationFrame: !intentPrint, pdfBug: this._pdfBug, pageColors, enableHWA: this._transport.enableHWA, operationsFilter, }); (intentState.renderTasks ||= new Set()).add(internalRenderTask); const renderTask = internalRenderTask.task; Promise.all([ intentState.displayReadyCapability.promise, optionalContentConfigPromise, ]) .then(([transparency, optionalContentConfig]) => { if (this.destroyed) { complete(); return; } this._stats?.time("Rendering"); if (!(optionalContentConfig.renderingIntent & renderingIntent)) { throw new Error( "Must use the same `intent`-argument when calling the `PDFPageProxy.render` " + "and `PDFDocumentProxy.getOptionalContentConfig` methods." ); } internalRenderTask.initializeGraphics({ transparency, optionalContentConfig, }); internalRenderTask.operatorListChanged(); }) .catch(complete); return renderTask; } /** * @param {GetOperatorListParameters} params - Page getOperatorList * parameters. * @returns {Promise<PDFOperatorList>} A promise resolved with an * {@link PDFOperatorList} object that represents the page's operator list. */ getOperatorList({ intent = "display", annotationMode = AnnotationMode.ENABLE, printAnnotationStorage = null, isEditing = false, } = {}) { if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) { throw new Error("Not implemented: getOperatorList"); } function operatorListChanged() { if (intentState.operatorList.lastChunk) { intentState.opListReadCapability.resolve(intentState.operatorList); intentState.renderTasks.delete(opListTask); } } const intentArgs = this._transport.getRenderingIntent( intent, annotationMode, printAnnotationStorage, isEditing, /* isOpList = */ true ); const intentState = this._intentStates.getOrInsertComputed( intentArgs.cacheKey, makeObj ); let opListTask; if (!intentState.opListReadCapability) { opListTask = Object.create(null); opListTask.operatorListChanged = operatorListChanged; intentState.opListReadCapability = Promise.withResolvers(); (intentState.renderTasks ||= new Set()).add(opListTask); intentState.operatorList = { fnArray: [], argsArray: [], lastChunk: false, separateAnnots: null, }; this._stats?.time("Page Request"); this._pumpOperatorList(intentArgs); } return intentState.opListReadCapability.promise; } /** * NOTE: All occurrences of whitespace will be replaced by * standard spaces (0x20). * * @param {getTextContentParameters} params - getTextContent parameters. * @returns {ReadableStream} Stream for reading text content chunks. */ streamTextContent({ includeMarkedContent = false, disableNormalization = false, } = {}) { const TEXT_CONTENT_CHUNK_SIZE = 100; return this._transport.messageHandler.sendWithStream( "GetTextContent", { pageId: this.#pagesMapper.getPageId(this._pageIndex + 1) - 1, pageIndex: this._pageIndex, includeMarkedContent: includeMarkedContent === true, disableNormalization: disableNormalization === true, }, { highWaterMark: TEXT_CONTENT_CHUNK_SIZE, size(textContent) { return textContent.items.length; }, } ); } /** * NOTE: All occurrences of whitespace will be replaced by * standard spaces (0x20). * * @param {getTextContentParameters} params - getTextContent parameters. * @returns {Promise<TextContent>} A promise that is resolved with a * {@link TextContent} object that represents the page's text content. */ async getTextContent(params = {}) { if (this._transport._htmlForXfa) { // TODO: We need to revisit this once the XFA foreground patch lands and // only do this for non-foreground XFA. return this.getXfa().then(xfa => XfaText.textContent(xfa)); } const readableStream = this.streamTextContent(params); const textContent = { items: [], styles: Object.create(null), lang: null, }; for await (const value of readableStream) { textContent.lang ??= value.lang; Object.assign(textContent.styles, value.styles); textContent.items.push(...value.items); } return textContent; } /** * @returns {Promise<StructTreeNode>} A promise that is resolved with a * {@link StructTreeNode} object that represents the page's structure tree, * or `null` when no structure tree is present for the current page. */ getStructTree() { return this._transport.getStructTree(this._pageIndex); } /** * Destroys the page object. * @private */ _destroy() { this.destroyed = true; const waitOn = []; for (const intentState of this._intentStates.values()) { this._abortOperatorList({ intentState, reason: new Error("Page was destroyed."), force: true, }); if (intentState.opListReadCapability) { // Avoid errors below, since the renderTasks are just stubs. continue; } for (const internalRenderTask of intentState.renderTasks) { waitOn.push(internalRenderTask.completed); internalRenderTask.cancel(); } } this.objs.clear(); this.#pendingCleanup = false; return Promise.all(waitOn); } /** * Cleans up resources allocated by the page. * * @param {boolean} [resetStats] - Reset page stats, if enabled. * The default value is `false`. * @returns {boolean} Indicates if clean-up was successfully run. */ cleanup(resetStats = false) { this.#pendingCleanup = true; const success = this.#tryCleanup(); if (resetStats && success) { this._stats &&= new StatTimer(); } return success; } /** * Attempts to clean up if rendering is in a state where that's possible. * @returns {boolean} Indicates if clean-up was successfully run. */ #tryCleanup() { if (!this.#pendingCleanup || this.destroyed) { return false; } for (const { renderTasks, operatorList } of this._intentStates.values()) { if (renderTasks.size > 0 || !operatorList.lastChunk) { return false; } } this._intentStates.clear(); this.objs.clear(); this.#pendingCleanup = false; return true; } /** * @private */ _startRenderPage(transparency, cacheKey) { const intentState = this._intentStates.get(cacheKey); if (!intentState) { return; // Rendering was cancelled. } this._stats?.timeEnd("Page Request"); // TODO Refactor RenderPageRequest to separate rendering // and operator list logic intentState.displayReadyCapability?.resolve(transparency); } /** * @private */ _renderPageChunk(operatorListChunk, intentState) { // Add the new chunk to the current operator list. for (let i = 0, ii = operatorListChunk.length; i < ii; i++) { intentState.operatorList.fnArray.push(operatorListChunk.fnArray[i]); intentState.operatorList.argsArray.push(operatorListChunk.argsArray[i]); } intentState.operatorList.lastChunk = operatorListChunk.lastChunk; intentState.operatorList.separateAnnots = operatorListChunk.separateAnnots; // Notify all the rendering tasks there are more operators to be consumed. for (const internalRenderTask of intentState.renderTasks) { internalRenderTask.operatorListChanged(); } if (operatorListChunk.lastChunk) { this.#tryCleanup(); } } /** * @private */ _pumpOperatorList({ renderingIntent, cacheKey, annotationStorageSerializable, modifiedIds, }) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { assert( Number.isInteger(renderingIntent) && renderingIntent > 0, '_pumpOperatorList: Expected valid "renderingIntent" argument.' ); } const { map, transfer } = annotationStorageSerializable; const readableStream = this._transport.messageHandler.sendWithStream( "GetOperatorList", { pageId: this.#pagesMapper.getPageId(this._pageIndex + 1) - 1, pageIndex: this._pageIndex, intent: renderingIntent, cacheKey, annotationStorage: map, modifiedIds, }, /* queueingStrategy = */ undefined, transfer ); const reader = readableStream.getReader(); const intentState = this._intentStates.get(cacheKey); intentState.streamReader = reader; const pump = () => { reader.read().then( ({ value, done }) => { if (done) { intentState.streamReader = null; return; } if (this._transport.destroyed) { return; // Ignore any pending requests if the worker was terminated. } this._renderPageChunk(value, intentState); pump(); }, reason => { intentState.streamReader = null; if (this._transport.destroyed) { return; // Ignore any pending requests if the worker was terminated. } if (intentState.operatorList) { // Mark operator list as complete. intentState.operatorList.lastChunk = true; for (const internalRenderTask of intentState.renderTasks) { internalRenderTask.operatorListChanged(); } this.#tryCleanup(); } if (intentState.displayReadyCapability) { intentState.displayReadyCapability.reject(reason); } else if (intentState.opListReadCapability) { intentState.opListReadCapability.reject(reason); } else { throw reason; } } ); }; pump(); } /** * @private */ _abortOperatorList({ intentState, reason, force = false }) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { assert( reason instanceof Error, '_abortOperatorList: Expected valid "reason" argument.' ); } if (!intentState.streamReader) { return; } // Ensure that a pending `streamReader` cancel timeout is always aborted. if (intentState.streamReaderCancelTimeout) { clearTimeout(intentState.streamReaderCancelTimeout); intentState.streamReaderCancelTimeout = null; } if (!force) { // Ensure that an Error occurring in *only* one `InternalRenderTask`, e.g. // multiple render() calls on the same canvas, won't break all rendering. if (intentState.renderTasks.size > 0) { return; } // Don't immediately abort parsing on the worker-thread when rendering is // cancelled, since that will unnecessarily delay re-rendering when (for // partially parsed pages) e.g. zooming/rotation occurs in the viewer. if (reason instanceof RenderingCancelledException) { let delay = RENDERING_CANCELLED_TIMEOUT; if (reason.extraDelay > 0 && reason.extraDelay < /* ms = */ 1000) { // Above, we prevent the total delay from becoming arbitrarily large. delay += reason.extraDelay; } intentState.streamReaderCancelTimeout = setTimeout(() => { intentState.streamReaderCancelTimeout = null; this._abortOperatorList({ intentState, reason, force: true }); }, delay); return; } } intentState.streamReader .cancel(new AbortException(reason.message)) .catch(() => { // Avoid "Uncaught promise" messages in the console. }); intentState.streamReader = null; if (this._transport.destroyed) { return; // Ignore any pending requests if the worker was terminated. } // Remove the current `intentState`, since a cancelled `getOperatorList` // call on the worker-thread cannot be re-started... for (const [curCacheKey, curIntentState] of this._intentStates) { if (curIntentState === intentState) { this._intentStates.delete(curCacheKey); break; } } // ... and force clean-up to ensure that any old state is always removed. this.cleanup(); } /** * @type {StatTimer | null} Returns page stats, if enabled; returns `null` * otherwise. */ get stats() { return this._stats; } } /** * @typedef {Object} PDFWorkerParameters * @property {string} [name] - The name of the worker. * @property {Worker} [port] - The `workerPort` object. * @property {number} [verbosity] - Controls the logging level; * the constants from {@link VerbosityLevel} should be used. */ /** * PDF.js web worker abstraction that controls the instantiation of PDF * documents. Message handlers are used to pass information from the main * thread to the worker thread and vice versa. If the creation of a web * worker is not possible, a "fake" worker will be used instead. * * @param {PDFWorkerParameters} params - The worker initialization parameters. */ class PDFWorker { #capability = Promise.withResolvers(); #messageHandler = null; #port = null; #webWorker = null; static #fakeWorkerId = 0; static #isWorkerDisabled = false; static #workerPorts = new WeakMap(); static { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { if (isNodeJS) { // Workers aren't supported in Node.js, force-disabling them there. this.#isWorkerDisabled = true; GlobalWorkerOptions.workerSrc ||= PDFJSDev.test("LIB") ? "../pdf.worker.js" : "./pdf.worker.mjs"; } // Check if URLs have the same origin. For non-HTTP based URLs, returns // false. this._isSameOrigin = (baseUrl, otherUrl) => { const base = URL.parse(baseUrl); if (!base?.origin || base.origin === "null") { return false; // non-HTTP url } const other = new URL(otherUrl, base); return base.origin === other.origin; }; this._createCDNWrapper = url => { // We will rely on blob URL's property to specify origin. // We want this function to fail in case if createObjectURL or Blob do // not exist or fail for some reason -- our Worker creation will fail // anyway. const wrapper = `await import("${url}");`; return URL.createObjectURL( new Blob([wrapper], { type: "text/javascript" }) ); }; } if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { this._resetGlobalState = () => { this.#isWorkerDisabled = false; delete globalThis.pdfjsWorker; }; } } constructor({ name = null, port = null, verbosity = getVerbosityLevel(), } = {}) { this.name = name; this.destroyed = false; this.verbosity = verbosity; if (port) { if (PDFWorker.#workerPorts.has(port)) { throw new Error("Cannot use more than one PDFWorker per port."); } PDFWorker.#workerPorts.set(port, this); this.#initializeFromPort(port); } else { this.#initialize(); } if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { // For testing purposes. Object.defineProperty(this, "_webWorker", { get() { return this.#webWorker; }, }); } } /** * Promise for worker initialization completion. * @type {Promise<void>} */ get promise() { return this.#capability.promise; } #resolve() { this.#capability.resolve(); // Send global setting, e.g. verbosity level. this.#messageHandler.send("configure", { verbosity: this.verbosity, }); } /** * The current `workerPort`, when it exists. * @type {Worker} */ get port() { return this.#port; } /** * The current MessageHandler-instance. * @type {MessageHandler} */ get messageHandler() { return this.#messageHandler; } #initializeFromPort(port) { this.#port = port; this.#messageHandler = new MessageHandler("main", "worker", port); this.#messageHandler.on("ready", () => { // Ignoring "ready" event -- MessageHandler should already be initialized // and ready to accept messages. }); this.#resolve(); } #initialize() { // If worker support isn't disabled explicit and the browser has worker // support, create a new web worker and test if it/the browser fulfills // all requirements to run parts of pdf.js in a web worker. // Right now, the requirement is, that an Uint8Array is still an // Uint8Array as it arrives on the worker. if ( PDFWorker.#isWorkerDisabled || PDFWorker.#mainThreadWorkerMessageHandler ) { this.#setupFakeWorker(); return; } let { workerSrc } = PDFWorker; try { // Wraps workerSrc path into blob URL, if the former does not belong // to the same origin. if ( typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC") && !PDFWorker._isSameOrigin(window.location, workerSrc) ) { workerSrc = PDFWorker._createCDNWrapper( new URL(workerSrc, window.location).href ); } const worker = new Worker(workerSrc, { type: "module" }); const messageHandler = new MessageHandler("main", "worker", worker); const terminateEarly = () => { ac.abort(); messageHandler.destroy(); worker.terminate(); if (this.destroyed) { this.#capability.reject(new Error("Worker was destroyed")); } else { // Fall back to fake worker if the termination is caused by an // error (e.g. NetworkError / SecurityError). this.#setupFakeWorker(); } }; const ac = new AbortController(); worker.addEventListener( "error", () => { if (!this.#webWorker) { // Worker failed to initialize due to an error. Clean up and fall // back to the fake worker. terminateEarly(); } }, { signal: ac.signal } ); messageHandler.on("test", data => { ac.abort(); if (this.destroyed || !data) { terminateEarly(); return; } this.#messageHandler = messageHandler; this.#port = worker; this.#webWorker = worker; this.#resolve(); }); messageHandler.on("ready", data => { ac.abort(); if (this.destroyed) { terminateEarly(); return; } try { sendTest(); } catch { // We need fallback to a faked worker. this.#setupFakeWorker(); } }); const sendTest = () => { const testObj = new Uint8Array(); // Ensure that we can use `postMessage` transfers. messageHandler.send("test", testObj, [testObj.buffer]); }; // It might take time for the worker to initialize. We will try to send // the "test" message immediately, and once the "ready" message arrives. // The worker shall process only the first received "test" message. sendTest(); return; } catch { info("The worker has been disabled."); } // Either workers are not supported or have thrown an exception. // Thus, we fallback to a faked worker. this.#setupFakeWorker(); } #setupFakeWorker() { if (!PDFWorker.#isWorkerDisabled) { warn("Setting up fake worker."); PDFWorker.#isWorkerDisabled = true; } PDFWorker._setupFakeWorkerGlobal .then(WorkerMessageHandler => { if (this.destroyed) { this.#capability.reject(new Error("Worker was destroyed")); return; } const port = new LoopbackPort(); this.#port = port; // All fake workers use the same port, making id unique. const id = `fake${PDFWorker.#fakeWorkerId++}`; // If the main thread is our worker, setup the handling for the // messages -- the main thread sends to it self. const workerHandler = new MessageHandler(id + "_worker", id, port); WorkerMessageHandler.setup(workerHandler, port); this.#messageHandler = new MessageHandler(id, id + "_worker", port); this.#resolve(); }) .catch(reason => { this.#capability.reject( new Error(`Setting up fake worker failed: "${reason.message}".`) ); }); } /** * Destroys the worker instance. */ destroy() { this.destroyed = true; // We need to terminate only web worker created resource. this.#webWorker?.terminate(); this.#webWorker = null; PDFWorker.#workerPorts.delete(this.#port); this.#port = null; this.#messageHandler?.destroy(); this.#messageHandler = null; } /** * @param {PDFWorkerParameters} params - The worker initialization parameters. * @returns {PDFWorker} */ static create(params) { const cachedPort = this.#workerPorts.get(params?.port); if (cachedPort) { if (cachedPort._pendingDestroy) { throw new Error( "PDFWorker.create - the worker is being destroyed.\n" + "Please remember to await `PDFDocumentLoadingTask.destroy()`-calls." ); } return cachedPort; } return new PDFWorker(params); } /** * The current `workerSrc`, when it exists. * @type {string} */ static get workerSrc() { if (GlobalWorkerOptions.workerSrc) { return GlobalWorkerOptions.workerSrc; } throw new Error('No "GlobalWorkerOptions.workerSrc" specified.'); } static get #mainThreadWorkerMessageHandler() { try { return globalThis.pdfjsWorker?.WorkerMessageHandler || null; } catch { return null; } } // Loads worker code into the main-thread. static get _setupFakeWorkerGlobal() { const loader = async () => { if (this.#mainThreadWorkerMessageHandler) { // The worker was already loaded using e.g. a `<script>` tag. return this.#mainThreadWorkerMessageHandler; } const worker = typeof PDFJSDev === "undefined" ? await import("pdfjs/pdf.worker.js") : await __raw_import__(this.workerSrc); return worker.WorkerMessageHandler; }; return shadow(this, "_setupFakeWorkerGlobal", loader()); } } /** * For internal use only. * @ignore */ class WorkerTransport { downloadInfoCapability = Promise.withResolvers(); #fullReader = null; #methodPromises = new Map(); #networkStream = null; #pageCache = new Map(); #pagePromises = new Map(); #pageRefCache = new Map(); #passwordCapability = null; constructor( messageHandler, loadingTask, networkStream, params, factory, pagesMapper ) { this.messageHandler = messageHandler; this.loadingTask = loadingTask; this.#networkStream = networkStream; this.commonObjs = new PDFObjects(); this.fontLoader = new FontLoader({ ownerDocument: params.ownerDocument, styleElement: params.styleElement, }); this.enableHWA = params.enableHWA; this.loadingParams = params.loadingParams; this._params = params; this.canvasFactory = factory.canvasFactory; this.filterFactory = factory.filterFactory; if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { this.binaryDataFactory = factory.binaryDataFactory; } this.pagesMapper = pagesMapper; this.destroyed = false; this.destroyCapability = null; this.setupMessageHandler(); if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { // For testing purposes. Object.defineProperty(this, "getNetworkStreamName", { value: () => networkStream?.constructor?.name || null, }); Object.defineProperty(this, "getXFADatasets", { value: () => this.messageHandler.sendWithPromise("GetXFADatasets", null), }); Object.defineProperty(this, "getXRefPrevValue", { value: () => this.messageHandler.sendWithPromise("GetXRefPrevValue", null), }); Object.defineProperty(this, "getStartXRefPos", { value: () => this.messageHandler.sendWithPromise("GetStartXRefPos", null), }); Object.defineProperty(this, "getAnnotArray", { value: pageIndex => this.messageHandler.sendWithPromise("GetAnnotArray", { pageIndex }), }); } } updatePage(page) { const { _pageIndex } = page; this.#pageCache.set(_pageIndex, page); this.#pagePromises.set(_pageIndex, Promise.resolve(page)); } #cacheSimpleMethod(name, data = null) { return this.#methodPromises.getOrInsertComputed(name, () => this.messageHandler.sendWithPromise(name, data) ); } #onProgress({ loaded, total }) { this.loadingTask.onProgress?.({ loaded, total, percent: total ? MathClamp(Math.round((loaded / total) * 100), 0, 100) : NaN, }); } get annotationStorage() { return shadow(this, "annotationStorage", new AnnotationStorage()); } getRenderingIntent( intent, annotationMode = AnnotationMode.ENABLE, printAnnotationStorage = null, isEditing = false, isOpList = false ) { let renderingIntent = RenderingIntentFlag.DISPLAY; // Default value. let annotationStorageSerializable = SerializableEmpty; switch (intent) { case "any": renderingIntent = RenderingIntentFlag.ANY; break; case "display": break; case "print": renderingIntent = RenderingIntentFlag.PRINT; break; default: warn(`getRenderingIntent - invalid intent: ${intent}`); } const annotationStorage = renderingIntent & RenderingIntentFlag.PRINT && printAnnotationStorage instanceof PrintAnnotationStorage ? printAnnotationStorage : this.annotationStorage; switch (annotationMode) { case AnnotationMode.DISABLE: renderingIntent += RenderingIntentFlag.ANNOTATIONS_DISABLE; break; case AnnotationMode.ENABLE: break; case AnnotationMode.ENABLE_FORMS: renderingIntent += RenderingIntentFlag.ANNOTATIONS_FORMS; break; case AnnotationMode.ENABLE_STORAGE: renderingIntent += RenderingIntentFlag.ANNOTATIONS_STORAGE; annotationStorageSerializable = annotationStorage.serializable; break; default: warn(`getRenderingIntent - invalid annotationMode: ${annotationMode}`); } if (isEditing) { renderingIntent += RenderingIntentFlag.IS_EDITING; } if (isOpList) { renderingIntent += RenderingIntentFlag.OPLIST; } const { ids: modifiedIds, hash: modifiedIdsHash } = annotationStorage.modifiedIds; const cacheKeyBuf = [ renderingIntent, annotationStorageSerializable.hash, modifiedIdsHash, ]; return { renderingIntent, cacheKey: cacheKeyBuf.join("_"), annotationStorageSerializable, modifiedIds, }; } destroy() { if (this.destroyCapability) { return this.destroyCapability.promise; } this.destroyed = true; this.destroyCapability = Promise.withResolvers(); this.#passwordCapability?.reject( new Error("Worker was destroyed during onPassword callback") ); const waitOn = []; // We need to wait for all renderings to be completed, e.g. // timeout/rAF can take a long time. for (const page of this.#pageCache.values()) { waitOn.push(page._destroy()); } this.#pageCache.clear(); this.#pagePromises.clear(); this.#pageRefCache.clear(); // Allow `AnnotationStorage`-related clean-up when destroying the document. if (Object.hasOwn(this, "annotationStorage")) { this.annotationStorage.resetModified(); } // We also need to wait for the worker to finish its long running tasks. const terminated = this.messageHandler.sendWithPromise("Terminate", null); waitOn.push(terminated); Promise.all(waitOn).then(() => { this.commonObjs.clear(); this.fontLoader.clear(); this.#methodPromises.clear(); this.filterFactory.destroy(); TextLayer.cleanup(); this.#networkStream?.cancelAllRequests( new AbortException("Worker was terminated.") ); this.messageHandler?.destroy(); this.messageHandler = null; this.destroyCapability.resolve(); }, this.destroyCapability.reject); return this.destroyCapability.promise; } setupMessageHandler() { const { messageHandler, loadingTask } = this; messageHandler.on("GetReader", (data, sink) => { assert( this.#networkStream, "GetReader - no `BasePDFStream` instance available." ); this.#fullReader = this.#networkStream.getFullReader(); // If stream or range turn out to be disabled, once `headersReady` is // resolved, this is our only way to report loading progress. this.#fullReader.onProgress = evt => this.#onProgress(evt); sink.onPull = () => { this.#fullReader .read() .then(function ({ value, done }) { if (done) { sink.close(); return; } assert( value instanceof ArrayBuffer, "GetReader - expected an ArrayBuffer." ); // Enqueue data chunk into sink, and transfer it // to other side as `Transferable` object. sink.enqueue(new Uint8Array(value), 1, [value]); }) .catch(reason => { sink.error(reason); }); }; sink.onCancel = reason => { this.#fullReader.cancel(reason); sink.ready.catch(readyReason => { if (this.destroyed) { return; // Ignore any pending requests if the worker was terminated. } throw readyReason; }); }; }); messageHandler.on("ReaderHeadersReady", async data => { await this.#fullReader.headersReady; const { isStreamingSupported, isRangeSupported, contentLength } = this.#fullReader; if (isStreamingSupported && isRangeSupported) { this.#fullReader.onProgress = null; // See comment in "GetReader" above. } return { isStreamingSupported, isRangeSupported, contentLength }; }); messageHandler.on("GetRangeReader", (data, sink) => { assert( this.#networkStream, "GetRangeReader - no `BasePDFStream` instance available." ); const rangeReader = this.#networkStream.getRangeReader( data.begin, data.end ); // When streaming is enabled, it's possible that the data requested here // has already been fetched via the `#fullReader` implementation. // However, given that the PDF data is loaded asynchronously on the // main-thread and then sent via `postMessage` to the worker-thread, // it may not have been available during parsing (hence the attempt to // use range requests here). // // To avoid wasting time and resources here, we'll thus *not* dispatch // range requests if the data was already loaded but has not been sent to // the worker-thread yet (which will happen via the `#fullReader`). if (!rangeReader) { sink.close(); return; } sink.onPull = () => { rangeReader .read() .then(function ({ value, done }) { if (done) { sink.close(); return; } assert( value instanceof ArrayBuffer, "GetRangeReader - expected an ArrayBuffer." ); sink.enqueue(new Uint8Array(value), 1, [value]); }) .catch(reason => { sink.error(reason); }); }; sink.onCancel = reason => { rangeReader.cancel(reason); sink.ready.catch(readyReason => { if (this.destroyed) { return; // Ignore any pending requests if the worker was terminated. } throw readyReason; }); }; }); messageHandler.on("GetDoc", ({ pdfInfo }) => { this.pagesMapper.pagesNumber = pdfInfo.numPages; this._numPages = pdfInfo.numPages; this._htmlForXfa = pdfInfo.htmlForXfa; delete pdfInfo.htmlForXfa; loadingTask._capability.resolve(new PDFDocumentProxy(pdfInfo, this)); }); messageHandler.on("DocException", ex => { loadingTask._capability.reject(wrapReason(ex)); }); messageHandler.on("PasswordRequest", ex => { this.#passwordCapability = Promise.withResolvers(); try { if (!loadingTask.onPassword) { throw wrapReason(ex); } const updatePassword = password => { if (password instanceof Error) { this.#passwordCapability.reject(password); } else { this.#passwordCapability.resolve({ password }); } }; loadingTask.onPassword(updatePassword, ex.code); } catch (err) { this.#passwordCapability.reject(err); } return this.#passwordCapability.promise; }); messageHandler.on("DataLoaded", data => { // For consistency: Ensure that progress is always reported when the // entire PDF file has been loaded, regardless of how it was fetched. this.#onProgress({ loaded: data.length, total: data.length }); this.downloadInfoCapability.resolve(data); }); messageHandler.on("StartRenderPage", data => { if (this.destroyed) { return; // Ignore any pending requests if the worker was terminated. } const page = this.#pageCache.get(data.pageIndex); page._startRenderPage(data.transparency, data.cacheKey); }); messageHandler.on("commonobj", ([id, type, exportedData]) => { if (this.destroyed) { return null; // Ignore any pending requests if the worker was terminated. } if (this.commonObjs.has(id)) { return null; } switch (type) { case "Font": if ("error" in exportedData) { const exportedError = exportedData.error; warn(`Error during font loading: ${exportedError}`); this.commonObjs.resolve(id, exportedError); break; } const fontData = new FontInfo(exportedData); const inspectFont = this._params.pdfBug && globalThis.FontInspector?.enabled ? (font, url) => globalThis.FontInspector.fontAdded(font, url) : null; const font = new FontFaceObject( fontData, inspectFont, exportedData.charProcOperatorList, exportedData.extra ); this.fontLoader .bind(font) .catch(() => messageHandler.sendWithPromise("FontFallback", { id })) .finally(() => { if (!font.fontExtraProperties) { // Immediately release the `font.data` property once the font // has been attached to the DOM, since it's no longer needed, // rather than waiting for a `PDFDocumentProxy.cleanup` call. // Since `font.data` could be very large, e.g. in some cases // multiple megabytes, this will help reduce memory usage. font.clearData(); } this.commonObjs.resolve(id, font); }); break; case "CopyLocalImage": const { imageRef } = exportedData; assert(imageRef, "The imageRef must be defined."); for (const pageProxy of this.#pageCache.values()) { for (const [, data] of pageProxy.objs) { if (data?.ref !== imageRef) { continue; } if (!data.dataLen) { return null; } const copy = structuredClone(data); if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { copy.CopyLocalImage = true; } this.commonObjs.resolve(id, copy); return data.dataLen; } } break; case "FontPath": this.commonObjs.resolve(id, new FontPathInfo(exportedData)); break; case "Image": this.commonObjs.resolve(id, exportedData); break; case "Pattern": const pattern = new PatternInfo(exportedData); this.commonObjs.resolve(id, pattern.getIR()); break; default: throw new Error(`Got unknown common object type ${type}`); } return null; }); messageHandler.on("obj", ([id, pageIndex, type, imageData]) => { if (this.destroyed) { // Ignore any pending requests if the worker was terminated. return; } const pageProxy = this.#pageCache.get(pageIndex); if (pageProxy.objs.has(id)) { return; } // Don't store data *after* cleanup has successfully run, see bug 1854145. if (pageProxy._intentStates.size === 0) { imageData?.bitmap?.close(); // Release any `ImageBitmap` data. return; } switch (type) { case "Image": case "Pattern": pageProxy.objs.resolve(id, imageData); break; default: throw new Error(`Got unknown object type ${type}`); } }); messageHandler.on("DocProgress", data => { if (this.destroyed) { return; // Ignore any pending requests if the worker was terminated. } this.#onProgress(data); }); if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { messageHandler.on("FetchBinaryData", async data => { if (this.destroyed) { throw new Error("Worker was destroyed."); } if (!this.binaryDataFactory) { throw new Error( "`BinaryDataFactory` not initialized, see the `useWorkerFetch` parameter." ); } return this.binaryDataFactory.fetch(data); }); } } getData() { return this.messageHandler.sendWithPromise("GetData", null); } saveDocument() { if (this.annotationStorage.size <= 0) { warn( "saveDocument called while `annotationStorage` is empty, " + "please use the getData-method instead." ); } const { map, transfer } = this.annotationStorage.serializable; return this.messageHandler .sendWithPromise( "SaveDocument", { isPureXfa: !!this._htmlForXfa, numPages: this._numPages, annotationStorage: map, filename: this.#fullReader?.filename ?? null, }, transfer ) .finally(() => { this.annotationStorage.resetModified(); }); } extractPages(pageInfos, copyLevels = null) { const params = { pageInfos, }; let transfer; const ImageBitmapCtor = globalThis.ImageBitmap; if (typeof ImageBitmapCtor === "function") { const infos = Array.isArray(pageInfos) ? pageInfos : [pageInfos]; for (const pageInfo of infos) { if (pageInfo?.image instanceof ImageBitmapCtor) { (transfer ||= []).push(pageInfo.image); } } } if (this.annotationStorage.size > 0) { const serialized = this.annotationStorage.serializable; let { map } = serialized; if (serialized.transfer?.length) { if (transfer) { transfer.push(...serialized.transfer); } else { transfer = serialized.transfer; } } // Annotation pageIndex tracks the editor's current viewer position; the // worker keys lookups by source index. Remap UI -> source via pagesMapper // so reorganized pages still receive their annotations after extraction. // Multiple viewer pages can share a source page. The copy level routes // each editor annotation to the corresponding extracted copy. const mapping = this.pagesMapper.getMapping(); if (mapping) { const remapped = new Map(); for (const [k, v] of map) { if ( v?.pageIndex !== undefined && v.pageIndex >= 0 && v.pageIndex < mapping.length ) { // copyLevels uses -1 for non-extracted pages. Keep their entries // because an extracted stamp may share their bitmapId; the worker // uses the negative level to skip the annotation itself. const copyLevel = copyLevels?.[v.pageIndex] ?? 0; const sourceIdx = mapping[v.pageIndex] - 1; if (sourceIdx !== v.pageIndex || copyLevel !== 0) { remapped.set(k, { ...v, pageIndex: sourceIdx, copyLevel }); continue; } } remapped.set(k, v); } map = remapped; } params.annotationStorage = map; } return this.messageHandler .sendWithPromise("ExtractPages", params, transfer) .finally(() => { this.annotationStorage.resetModified(); }); } getPage(pageNumber) { if ( !Number.isInteger(pageNumber) || pageNumber <= 0 || pageNumber > this.pagesMapper.pagesNumber ) { return Promise.reject(new Error("Invalid page request.")); } const pageIndex = pageNumber - 1; const newPageIndex = this.pagesMapper.getPageId(pageNumber) - 1; const cachedPromise = this.#pagePromises.get(pageIndex); if (cachedPromise) { return cachedPromise; } const promise = this.messageHandler .sendWithPromise("GetPage", { pageIndex: newPageIndex, }) .then(pageInfo => { if (this.destroyed) { throw new Error("Transport destroyed"); } if (pageInfo.refStr) { this.#pageRefCache.set(pageInfo.refStr, newPageIndex); } const page = new PDFPageProxy( pageIndex, pageInfo, this, this.pagesMapper, this._params.pdfBug ); this.#pageCache.set(pageIndex, page); return page; }); this.#pagePromises.set(pageIndex, promise); return promise; } async getPageIndex(ref) { if (!isRefProxy(ref)) { throw new Error("Invalid pageIndex request."); } const index = await this.messageHandler.sendWithPromise("GetPageIndex", { num: ref.num, gen: ref.gen, }); const pageNumber = this.pagesMapper.getPageNumber(index + 1); if (pageNumber === 0) { throw new Error("GetPageIndex: page has been removed."); } return pageNumber - 1; } getAnnotations(pageIndex, intent) { return this.messageHandler.sendWithPromise("GetAnnotations", { pageIndex: this.pagesMapper.getPageId(pageIndex + 1) - 1, intent, }); } getFieldObjects() { return this.#cacheSimpleMethod("GetFieldObjects"); } getSignatures() { return this.#cacheSimpleMethod("GetSignatures"); } getSignatureData(id) { // Not cached: bytes should be one-shot. Holding them in the // `#methodPromises` map would keep them alive for the document's // lifetime, which defeats the metadata/data split. return this.messageHandler.sendWithPromise("GetSignatureData", id); } hasJSActions() { return this.#cacheSimpleMethod("HasJSActions"); } getCalculationOrderIds() { return this.messageHandler.sendWithPromise("GetCalculationOrderIds", null); } getDestinations() { return this.messageHandler.sendWithPromise("GetDestinations", null); } getDestination(id) { if (typeof id !== "string") { return Promise.reject(new Error("Invalid destination request.")); } return this.messageHandler.sendWithPromise("GetDestination", { id }); } getPageLabels() { return this.messageHandler.sendWithPromise("GetPageLabels", null); } getPageLayout() { return this.messageHandler.sendWithPromise("GetPageLayout", null); } getPageMode() { return this.messageHandler.sendWithPromise("GetPageMode", null); } getViewerPreferences() { return this.messageHandler.sendWithPromise("GetViewerPreferences", null); } getOpenAction() { return this.messageHandler.sendWithPromise("GetOpenAction", null); } /** * @returns {Promise<Map<string, CatalogAttachment> | null>} * Promise that is resolved with a lookup table for mapping named * attachments to their content. */ getAttachments() { return this.messageHandler.sendWithPromise("GetAttachments", null); } /** * @param {string} id * Unique attachment identifier (required). * @returns {Promise<CatalogAttachmentContent>} * Promise that resolves to attachment content. */ getAttachmentContent(id) { return this.messageHandler.sendWithPromise("GetAttachmentContent", id); } getAnnotationsByType(types, pageIndexesToSkip) { return this.messageHandler.sendWithPromise("GetAnnotationsByType", { types, pageIndexesToSkip, }); } getDocJSActions() { return this.#cacheSimpleMethod("GetDocJSActions"); } getPageJSActions(pageIndex) { return this.messageHandler.sendWithPromise("GetPageJSActions", { pageIndex: this.pagesMapper.getPageId(pageIndex + 1) - 1, }); } getStructTree(pageIndex) { return this.messageHandler.sendWithPromise("GetStructTree", { pageIndex: this.pagesMapper.getPageId(pageIndex + 1) - 1, }); } getOutline() { return this.messageHandler.sendWithPromise("GetOutline", null); } getOptionalContentConfig(renderingIntent) { return this.#cacheSimpleMethod("GetOptionalContentConfig").then( data => new OptionalContentConfig(data, renderingIntent) ); } getPermissions() { return this.messageHandler.sendWithPromise("GetPermissions", null); } getMetadata() { const name = "GetMetadata"; return this.#methodPromises.getOrInsertComputed(name, () => this.messageHandler.sendWithPromise(name, null).then(results => ({ info: results[0], metadata: results[1] ? new Metadata(results[1]) : null, contentDispositionFilename: this.#fullReader?.filename ?? null, contentLength: this.#fullReader?.contentLength ?? null, hasStructTree: results[2], })) ); } getMarkInfo() { return this.messageHandler.sendWithPromise("GetMarkInfo", null); } getRawData(data) { return this.messageHandler.sendWithPromise("GetRawData", data); } async startCleanup(keepLoadedFonts = false) { if (this.destroyed) { return; // No need to manually clean-up when destruction has started. } await this.messageHandler.sendWithPromise("Cleanup", null); for (const page of this.#pageCache.values()) { const cleanupSuccessful = page.cleanup(); if (!cleanupSuccessful) { throw new Error( `startCleanup: Page ${page.pageNumber} is currently rendering.` ); } } this.commonObjs.clear(); if (!keepLoadedFonts) { this.fontLoader.clear(); } this.#methodPromises.clear(); this.filterFactory.destroy(/* keepHCM = */ true); TextLayer.cleanup(); } cachedPageNumber(ref) { if (!isRefProxy(ref)) { return null; } const refStr = ref.gen === 0 ? `${ref.num}R` : `${ref.num}R${ref.gen}`; const pageIndex = this.#pageRefCache.get(refStr); if (pageIndex >= 0) { const pageNumber = this.pagesMapper.getPageNumber(pageIndex + 1); if (pageNumber !== 0) { return pageNumber; } } return null; } } /** * Allows controlling of the rendering tasks. */ class RenderTask { _internalRenderTask = null; /** * Callback for incremental rendering -- a function that will be called * each time the rendering is paused. To continue rendering call the * function that is the first argument to the callback. * @type {function} */ onContinue = null; /** * A function that will be synchronously called when the rendering tasks * finishes with an error (either because of an actual error, or because the * rendering is cancelled). * * @type {function} * @param {Error} error */ onError = null; constructor(internalRenderTask) { this._internalRenderTask = internalRenderTask; if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { // For testing purposes. Object.defineProperty(this, "getOperatorList", { value: () => this._internalRenderTask.operatorList, }); } } /** * Promise for rendering task completion. * @type {Promise<void>} */ get promise() { return this._internalRenderTask.capability.promise; } /** * Cancels the rendering task. If the task is currently rendering it will * not be cancelled until graphics pauses with a timeout. The promise that * this object extends will be rejected when cancelled. * * @param {number} [extraDelay] */ cancel(extraDelay = 0) { this._internalRenderTask.cancel(/* error = */ null, extraDelay); } /** * Whether form fields are rendered separately from the main operatorList. * @type {boolean} */ get separateAnnots() { const { separateAnnots } = this._internalRenderTask.operatorList; if (!separateAnnots) { return false; } const { annotationCanvasMap } = this._internalRenderTask; return ( separateAnnots.form || (separateAnnots.canvas && annotationCanvasMap?.size > 0) ); } get imageCoordinates() { return this._internalRenderTask.imageCoordinates || null; } } /** * For internal use only. * @ignore */ class InternalRenderTask { #rAF = null; static #canvasInUse = new WeakSet(); constructor({ callback, params, objs, commonObjs, annotationCanvasMap, operatorList, pageIndex, canvasFactory, filterFactory, useRequestAnimationFrame = false, pdfBug = false, pageColors = null, enableHWA = false, operationsFilter = null, }) { this.callback = callback; this.params = params; this.objs = objs; this.commonObjs = commonObjs; this.annotationCanvasMap = annotationCanvasMap; this.operatorListIdx = null; this.operatorList = operatorList; this._pageIndex = pageIndex; this.canvasFactory = canvasFactory; this.filterFactory = filterFactory; this._pdfBug = pdfBug; this.pageColors = pageColors; this.running = false; this.graphicsReadyCallback = null; this.graphicsReady = false; this._useRequestAnimationFrame = useRequestAnimationFrame === true && typeof window !== "undefined"; this.cancelled = false; this.capability = Promise.withResolvers(); this.task = new RenderTask(this); // caching this-bound methods this._cancelBound = this.cancel.bind(this); this._continueBound = this._continue.bind(this); this._scheduleNextBound = this._scheduleNext.bind(this); this._nextBound = this._next.bind(this); this._canvas = params.canvas; this._canvasContext = params.canvas ? null : params.canvasContext; this._enableHWA = enableHWA; this._dependencyTracker = params.dependencyTracker; this._imagesTracker = params.imagesTracker; this._operationsFilter = operationsFilter; } get completed() { return this.capability.promise.catch(function () { // Ignoring errors, since we only want to know when rendering is // no longer pending. }); } initializeGraphics({ transparency = false, optionalContentConfig }) { if (this.cancelled) { return; } if (this._canvas) { if (InternalRenderTask.#canvasInUse.has(this._canvas)) { throw new Error( "Cannot use the same canvas during multiple render() operations. " + "Use different canvas or ensure previous operations were " + "cancelled or completed." ); } InternalRenderTask.#canvasInUse.add(this._canvas); } if (this._pdfBug && globalThis.StepperManager?.enabled) { this.stepper = globalThis.StepperManager.create(this._pageIndex); this.stepper.init(this.operatorList); this.stepper.nextBreakPoint = this.stepper.getNextBreakPoint(); } const { viewport, transform, background, dependencyTracker, imagesTracker, } = this.params; // When printing in Firefox, we get a specific context in mozPrintCallback // which cannot be created from the canvas itself. const canvasContext = this._canvasContext || this._canvas.getContext("2d", { alpha: false, willReadFrequently: !this._enableHWA, }); this.gfx = new CanvasGraphics( canvasContext, this.commonObjs, this.objs, this.canvasFactory, this.filterFactory, { optionalContentConfig }, this.annotationCanvasMap, this.pageColors, dependencyTracker, imagesTracker ); this.gfx.beginDrawing({ transform, viewport, transparency, background, }); this.operatorListIdx = 0; this.graphicsReady = true; this.graphicsReadyCallback?.(); } cancel(error = null, extraDelay = 0) { this.running = false; this.cancelled = true; this.gfx?.endDrawing(); if (this.#rAF) { window.cancelAnimationFrame(this.#rAF); this.#rAF = null; } InternalRenderTask.#canvasInUse.delete(this._canvas); error ||= new RenderingCancelledException( `Rendering cancelled, page ${this._pageIndex + 1}`, extraDelay ); this.callback(error); this.task.onError?.(error); } operatorListChanged() { if (!this.graphicsReady) { this.graphicsReadyCallback ||= this._continueBound; return; } this.gfx.dependencyTracker?.growOperationsCount( this.operatorList.fnArray.length ); this.stepper?.updateOperatorList(this.operatorList); if (this.running) { return; } this._continue(); } _continue() { this.running = true; if (this.cancelled) { return; } if (this.task.onContinue) { this.task.onContinue(this._scheduleNextBound); } else { this._scheduleNext(); } } _scheduleNext() { if (this._useRequestAnimationFrame) { this.#rAF = window.requestAnimationFrame(() => { this.#rAF = null; this._nextBound().catch(this._cancelBound); }); } else { Promise.resolve().then(this._nextBound).catch(this._cancelBound); } } async _next() { if (this.cancelled) { return; } this.operatorListIdx = this.gfx.executeOperatorList( this.operatorList, this.operatorListIdx, this._continueBound, this.stepper, this._operationsFilter ); if (this.operatorListIdx === this.operatorList.argsArray.length) { this.running = false; if (this.operatorList.lastChunk) { this.gfx.endDrawing(); InternalRenderTask.#canvasInUse.delete(this._canvas); this.callback(); } } } } /** @type {string} */ const version = typeof PDFJSDev !== "undefined" ? PDFJSDev.eval("BUNDLE_VERSION") : null; /** @type {string} */ const build = typeof PDFJSDev !== "undefined" ? PDFJSDev.eval("BUNDLE_BUILD") : null; export { build, getDocument, PDFDataRangeTransport, PDFDocumentLoadingTask, PDFDocumentProxy, PDFPageProxy, PDFWorker, RenderTask, version, };