From 4b4ab10c54f57d6f0aa8bdbc74a4c6dfad6247d0 Mon Sep 17 00:00:00 2001 From: calixteman Date: Sun, 8 Feb 2026 19:40:17 +0100 Subject: [PATCH] Set a pages mapper per loaded document It fixes #20629. --- src/display/api.js | 54 +++++++++++----- src/display/display_utils.js | 73 +++++++++------------- src/pdf.js | 3 - test/integration/reorganize_pages_spec.mjs | 8 +-- test/unit/api_spec.js | 30 +++++++++ test/unit/pdf_spec.js | 2 - web/pdf_thumbnail_viewer.js | 5 +- web/pdf_viewer.js | 7 --- web/pdfjs.js | 2 - 9 files changed, 104 insertions(+), 80 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index 9ffb982f0a512..4b89e228ed696 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -233,6 +233,9 @@ const RENDERING_CANCELLED_TIMEOUT = 100; // ms * The default value is {DOMFilterFactory}. * @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. */ /** @@ -342,6 +345,7 @@ function getDocument(src = {}) { : DOMFilterFactory); const enableHWA = src.enableHWA === true; const useWasm = src.useWasm !== false; + const pagesMapper = src.pagesMapper || new PagesMapper(); // Parameters whose default values depend on other parameters. const length = rangeTransport ? rangeTransport.length : (src.length ?? NaN); @@ -511,7 +515,8 @@ function getDocument(src = {}) { task, networkStream, transportParams, - transportFactory + transportFactory, + pagesMapper ); task._transport = transport; messageHandler.send("Ready", null); @@ -761,6 +766,13 @@ class PDFDocumentProxy { } } + /** + * @type {PagesMapper} The pages mapper instance. + */ + get pagesMapper() { + return this._transport.pagesMapper; + } + /** * @type {AnnotationStorage} Storage for annotation data in forms. */ @@ -1324,9 +1336,9 @@ class PDFDocumentProxy { class PDFPageProxy { #pendingCleanup = false; - #pagesMapper = PagesMapper.instance; + #pagesMapper = null; - constructor(pageIndex, pageInfo, transport, pdfBug = false) { + constructor(pageIndex, pageInfo, transport, pagesMapper, pdfBug = false) { this._pageIndex = pageIndex; this._pageInfo = pageInfo; this._transport = transport; @@ -1339,6 +1351,7 @@ class PDFPageProxy { this._intentStates = new Map(); this.destroyed = false; this.recordedBBoxes = null; + this.#pagesMapper = pagesMapper; } /** @@ -2402,9 +2415,14 @@ class WorkerTransport { #passwordCapability = null; - #pagesMapper = PagesMapper.instance; - - constructor(messageHandler, loadingTask, networkStream, params, factory) { + constructor( + messageHandler, + loadingTask, + networkStream, + params, + factory, + pagesMapper + ) { this.messageHandler = messageHandler; this.loadingTask = loadingTask; this.#networkStream = networkStream; @@ -2429,7 +2447,8 @@ class WorkerTransport { this.setupMessageHandler(); - this.#pagesMapper.addListener(this.#updateCaches.bind(this)); + this.pagesMapper = pagesMapper; + this.pagesMapper.addListener(this.#updateCaches.bind(this)); if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { // For testing purposes. @@ -2458,8 +2477,8 @@ class WorkerTransport { #updateCaches() { const newPageCache = new Map(); const newPromiseCache = new Map(); - for (let i = 0, ii = this.#pagesMapper.pagesNumber; i < ii; i++) { - const prevPageIndex = this.#pagesMapper.getPrevPageNumber(i + 1) - 1; + for (let i = 0, ii = this.pagesMapper.pagesNumber; i < ii; i++) { + const prevPageIndex = this.pagesMapper.getPrevPageNumber(i + 1) - 1; const page = this.#pageCache.get(prevPageIndex); if (page) { newPageCache.set(i, page); @@ -2730,7 +2749,7 @@ class WorkerTransport { }); messageHandler.on("GetDoc", ({ pdfInfo }) => { - this.#pagesMapper.pagesNumber = pdfInfo.numPages; + this.pagesMapper.pagesNumber = pdfInfo.numPages; this._numPages = pdfInfo.numPages; this._htmlForXfa = pdfInfo.htmlForXfa; delete pdfInfo.htmlForXfa; @@ -2947,12 +2966,12 @@ class WorkerTransport { if ( !Number.isInteger(pageNumber) || pageNumber <= 0 || - pageNumber > this.#pagesMapper.pagesNumber + pageNumber > this.pagesMapper.pagesNumber ) { return Promise.reject(new Error("Invalid page request.")); } const pageIndex = pageNumber - 1; - const newPageIndex = this.#pagesMapper.getPageId(pageNumber) - 1; + const newPageIndex = this.pagesMapper.getPageId(pageNumber) - 1; const cachedPromise = this.#pagePromises.get(pageIndex); if (cachedPromise) { @@ -2974,6 +2993,7 @@ class WorkerTransport { pageIndex, pageInfo, this, + this.pagesMapper, this._params.pdfBug ); this.#pageCache.set(pageIndex, page); @@ -2991,12 +3011,12 @@ class WorkerTransport { num: ref.num, gen: ref.gen, }); - return this.#pagesMapper.getPageNumber(index + 1) - 1; + return this.pagesMapper.getPageNumber(index + 1) - 1; } getAnnotations(pageIndex, intent) { return this.messageHandler.sendWithPromise("GetAnnotations", { - pageIndex: this.#pagesMapper.getPageId(pageIndex + 1) - 1, + pageIndex: this.pagesMapper.getPageId(pageIndex + 1) - 1, intent, }); } @@ -3063,13 +3083,13 @@ class WorkerTransport { getPageJSActions(pageIndex) { return this.messageHandler.sendWithPromise("GetPageJSActions", { - pageIndex: this.#pagesMapper.getPageId(pageIndex + 1) - 1, + pageIndex: this.pagesMapper.getPageId(pageIndex + 1) - 1, }); } getStructTree(pageIndex) { return this.messageHandler.sendWithPromise("GetStructTree", { - pageIndex: this.#pagesMapper.getPageId(pageIndex + 1) - 1, + pageIndex: this.pagesMapper.getPageId(pageIndex + 1) - 1, }); } @@ -3141,7 +3161,7 @@ class WorkerTransport { const refStr = ref.gen === 0 ? `${ref.num}R` : `${ref.num}R${ref.gen}`; const pageIndex = this.#pageRefCache.get(refStr); return pageIndex >= 0 - ? this.#pagesMapper.getPageNumber(pageIndex + 1) + ? this.pagesMapper.getPageNumber(pageIndex + 1) : null; } } diff --git a/src/display/display_utils.js b/src/display/display_utils.js index a529acef42816..16181b3e5e51b 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -1045,38 +1045,38 @@ class PagesMapper { * Maps page IDs to their corresponding page numbers. * @type {Uint32Array|null} */ - static #idToPageNumber = null; + #idToPageNumber = null; /** * Maps page numbers to their corresponding page IDs. * @type {Uint32Array|null} */ - static #pageNumberToId = null; + #pageNumberToId = null; /** * Previous mapping of page IDs to page numbers. * @type {Uint32Array|null} */ - static #prevIdToPageNumber = null; + #prevIdToPageNumber = null; /** * The total number of pages. * @type {number} */ - static #pagesNumber = 0; + #pagesNumber = 0; /** * Listeners for page changes. * @type {Array} */ - static #listeners = []; + #listeners = []; /** * Gets the total number of pages. * @returns {number} The number of pages. */ get pagesNumber() { - return PagesMapper.#pagesNumber; + return this.#pagesNumber; } /** @@ -1085,52 +1085,49 @@ class PagesMapper { * @param {number} n - The total number of pages. */ set pagesNumber(n) { - if (PagesMapper.#pagesNumber === n) { + if (this.#pagesNumber === n) { return; } - PagesMapper.#pagesNumber = n; + this.#pagesNumber = n; if (n === 0) { - PagesMapper.#pageNumberToId = null; - PagesMapper.#idToPageNumber = null; + this.#pageNumberToId = null; + this.#idToPageNumber = null; } } addListener(listener) { - PagesMapper.#listeners.push(listener); + this.#listeners.push(listener); } removeListener(listener) { - const index = PagesMapper.#listeners.indexOf(listener); + const index = this.#listeners.indexOf(listener); if (index >= 0) { - PagesMapper.#listeners.splice(index, 1); + this.#listeners.splice(index, 1); } } #updateListeners() { - for (const listener of PagesMapper.#listeners) { + for (const listener of this.#listeners) { listener(); } } #init(mustInit) { - if (PagesMapper.#pageNumberToId) { + if (this.#pageNumberToId) { return; } - const n = PagesMapper.#pagesNumber; + const n = this.#pagesNumber; // Allocate a single array for better memory locality. const array = new Uint32Array(3 * n); - const pageNumberToId = (PagesMapper.#pageNumberToId = array.subarray(0, n)); - const idToPageNumber = (PagesMapper.#idToPageNumber = array.subarray( - n, - 2 * n - )); + const pageNumberToId = (this.#pageNumberToId = array.subarray(0, n)); + const idToPageNumber = (this.#idToPageNumber = array.subarray(n, 2 * n)); if (mustInit) { for (let i = 0; i < n; i++) { pageNumberToId[i] = idToPageNumber[i] = i + 1; } } - PagesMapper.#prevIdToPageNumber = array.subarray(2 * n); + this.#prevIdToPageNumber = array.subarray(2 * n); } /** @@ -1143,9 +1140,9 @@ class PagesMapper { */ movePages(selectedPages, pagesToMove, index) { this.#init(true); - const pageNumberToId = PagesMapper.#pageNumberToId; - const idToPageNumber = PagesMapper.#idToPageNumber; - PagesMapper.#prevIdToPageNumber.set(idToPageNumber); + const pageNumberToId = this.#pageNumberToId; + const idToPageNumber = this.#idToPageNumber; + this.#prevIdToPageNumber.set(idToPageNumber); const movedCount = pagesToMove.length; const mappedPagesToMove = new Uint32Array(movedCount); let removedBeforeTarget = 0; @@ -1158,7 +1155,7 @@ class PagesMapper { } } - const pagesNumber = PagesMapper.#pagesNumber; + const pagesNumber = this.#pagesNumber; // target index after removing elements that were before it let adjustedTarget = index - removedBeforeTarget; const remainingLen = pagesNumber - movedCount; @@ -1201,7 +1198,7 @@ class PagesMapper { * @returns {boolean} True if the mappings have been altered, false otherwise. */ hasBeenAltered() { - return PagesMapper.#pageNumberToId !== null; + return this.#pageNumberToId !== null; } /** @@ -1211,16 +1208,14 @@ class PagesMapper { getPageMappingForSaving() { // Saving is index-based. return { - pageIndices: PagesMapper.#idToPageNumber - ? PagesMapper.#idToPageNumber.map(x => x - 1) + pageIndices: this.#idToPageNumber + ? this.#idToPageNumber.map(x => x - 1) : null, }; } getPrevPageNumber(pageNumber) { - return PagesMapper.#prevIdToPageNumber[ - PagesMapper.#pageNumberToId[pageNumber - 1] - 1 - ]; + return this.#prevIdToPageNumber[this.#pageNumberToId[pageNumber - 1] - 1]; } /** @@ -1229,7 +1224,7 @@ class PagesMapper { * @returns {number} The page number, or the ID itself if no mapping exists. */ getPageNumber(id) { - return PagesMapper.#idToPageNumber?.[id - 1] ?? id; + return this.#idToPageNumber?.[id - 1] ?? id; } /** @@ -1239,19 +1234,11 @@ class PagesMapper { * exists. */ getPageId(pageNumber) { - return PagesMapper.#pageNumberToId?.[pageNumber - 1] ?? pageNumber; - } - - /** - * Gets or creates a singleton instance of PagesMapper. - * @returns {PagesMapper} The singleton instance. - */ - static get instance() { - return shadow(this, "instance", new PagesMapper()); + return this.#pageNumberToId?.[pageNumber - 1] ?? pageNumber; } getMapping() { - return PagesMapper.#pageNumberToId.subarray(0, this.pagesNumber); + return this.#pageNumberToId.subarray(0, this.pagesNumber); } } diff --git a/src/pdf.js b/src/pdf.js index c5fd11f9f9070..707add654bcf9 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -57,7 +57,6 @@ import { isPdfFile, noContextMenu, OutputScale, - PagesMapper, PDFDateString, PixelsPerInch, RenderingCancelledException, @@ -129,7 +128,6 @@ globalThis.pdfjsLib = { normalizeUnicode, OPS, OutputScale, - PagesMapper, PasswordResponses, PDFDataRangeTransport, PDFDateString, @@ -189,7 +187,6 @@ export { normalizeUnicode, OPS, OutputScale, - PagesMapper, PasswordResponses, PDFDataRangeTransport, PDFDateString, diff --git a/test/integration/reorganize_pages_spec.mjs b/test/integration/reorganize_pages_spec.mjs index 647bea1cfabf8..937e28e46c332 100644 --- a/test/integration/reorganize_pages_spec.mjs +++ b/test/integration/reorganize_pages_spec.mjs @@ -104,12 +104,12 @@ function movePages(page, selectedPages, atIndex) { return page.evaluate( (selected, index) => { const viewer = window.PDFViewerApplication.pdfViewer; + const pagesMapper = viewer.pdfDocument.pagesMapper; const pagesToMove = Array.from(selected).sort((a, b) => a - b); - viewer.pagesMapper.pagesNumber = - document.querySelectorAll(".page").length; - viewer.pagesMapper.movePages(new Set(pagesToMove), pagesToMove, index); + pagesMapper.pagesNumber = document.querySelectorAll(".page").length; + pagesMapper.movePages(new Set(pagesToMove), pagesToMove, index); window.PDFViewerApplication.eventBus.dispatch("pagesedited", { - pagesMapper: viewer.pagesMapper, + pagesMapper, index, pagesToMove, }); diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 9829ae9d37a8c..e9961a16d7bc9 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -5403,6 +5403,36 @@ small scripts as well as for`); }); }); + describe("Multiple documents and pages mapper", function () { + it("should load multiple documents in parallel", async function () { + const loadingTask1 = getDocument(buildGetDocumentParams("pdkids.pdf")); + const loadingTask2 = getDocument( + buildGetDocumentParams("page_with_number.pdf") + ); + const loadingTask3 = getDocument(buildGetDocumentParams("empty.pdf")); + + const [pdfDoc1, pdfDoc2, pdfDoc3] = await Promise.all([ + loadingTask1.promise, + loadingTask2.promise, + loadingTask3.promise, + ]); + // Each document has its own pages mapper, so the number of pages + // should be correct for each document. + expect(pdfDoc1.numPages).toEqual(55); + expect(pdfDoc1.pagesMapper.pagesNumber).toEqual(55); + expect(pdfDoc2.numPages).toEqual(17); + expect(pdfDoc2.pagesMapper.pagesNumber).toEqual(17); + expect(pdfDoc3.numPages).toEqual(1); + expect(pdfDoc3.pagesMapper.pagesNumber).toEqual(1); + + await Promise.all([ + loadingTask1.destroy(), + loadingTask2.destroy(), + loadingTask3.destroy(), + ]); + }); + }); + describe("PDF page editing", function () { const getPageRefs = async pdfDoc => { const refs = []; diff --git a/test/unit/pdf_spec.js b/test/unit/pdf_spec.js index 1a3b32581c2f7..35e92b9ae6ebf 100644 --- a/test/unit/pdf_spec.js +++ b/test/unit/pdf_spec.js @@ -48,7 +48,6 @@ import { isPdfFile, noContextMenu, OutputScale, - PagesMapper, PDFDateString, PixelsPerInch, RenderingCancelledException, @@ -113,7 +112,6 @@ const expectedAPI = Object.freeze({ normalizeUnicode, OPS, OutputScale, - PagesMapper, PasswordResponses, PDFDataRangeTransport, PDFDateString, diff --git a/web/pdf_thumbnail_viewer.js b/web/pdf_thumbnail_viewer.js index fe0816af3c8ae..23263ac702c80 100644 --- a/web/pdf_thumbnail_viewer.js +++ b/web/pdf_thumbnail_viewer.js @@ -25,7 +25,7 @@ import { isValidRotation, watchScroll, } from "./ui_utils.js"; -import { MathClamp, noContextMenu, PagesMapper, stopEvent } from "pdfjs-lib"; +import { MathClamp, noContextMenu, stopEvent } from "pdfjs-lib"; import { Menu } from "./menu.js"; import { PDFThumbnailView } from "./pdf_thumbnail_view.js"; import { RenderingStates } from "./renderable_view.js"; @@ -109,7 +109,7 @@ class PDFThumbnailViewer { #currentScrollTop = 0; - #pagesMapper = PagesMapper.instance; + #pagesMapper = null; #manageSaveAsButton = null; @@ -275,6 +275,7 @@ class PDFThumbnailViewer { if (!pdfDocument) { return; } + this.#pagesMapper = pdfDocument.pagesMapper; const firstPagePromise = pdfDocument.getPage(1); const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({ intent: "display", diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 1d97482d13cdb..5937ee2105101 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -30,7 +30,6 @@ import { AnnotationEditorUIManager, AnnotationMode, MathClamp, - PagesMapper, PermissionFlag, PixelsPerInch, shadow, @@ -286,8 +285,6 @@ class PDFViewer { #viewerAlert = null; - #pagesMapper = PagesMapper.instance; - /** * @param {PDFViewerOptions} options */ @@ -299,9 +296,6 @@ class PDFViewer { `The API version "${version}" does not match the Viewer version "${viewerVersion}".` ); } - if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { - this.pagesMapper = PagesMapper.instance; - } this.container = options.container; this.viewer = options.viewer || options.container.firstElementChild; @@ -880,7 +874,6 @@ class PDFViewer { this.#annotationEditorMode = AnnotationEditorType.NONE; this.#printingAllowed = true; - this.#pagesMapper.pagesNumber = 0; } this.pdfDocument = pdfDocument; diff --git a/web/pdfjs.js b/web/pdfjs.js index f85e5bcede613..e7a9fa3bcf169 100644 --- a/web/pdfjs.js +++ b/web/pdfjs.js @@ -49,7 +49,6 @@ const { normalizeUnicode, OPS, OutputScale, - PagesMapper, PasswordResponses, PDFDataRangeTransport, PDFDateString, @@ -109,7 +108,6 @@ export { normalizeUnicode, OPS, OutputScale, - PagesMapper, PasswordResponses, PDFDataRangeTransport, PDFDateString,