Set a pages mapper per loaded document

It fixes #20629.
This commit is contained in:
calixteman 2026-02-08 19:40:17 +01:00
parent 2b95a8eb38
commit 4b4ab10c54
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
9 changed files with 104 additions and 80 deletions

View File

@ -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;
}
}

View File

@ -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<function>}
*/
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);
}
}

View File

@ -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,

View File

@ -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,
});

View File

@ -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 = [];

View File

@ -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,

View File

@ -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",

View File

@ -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;

View File

@ -49,7 +49,6 @@ const {
normalizeUnicode,
OPS,
OutputScale,
PagesMapper,
PasswordResponses,
PDFDataRangeTransport,
PDFDateString,
@ -109,7 +108,6 @@ export {
normalizeUnicode,
OPS,
OutputScale,
PagesMapper,
PasswordResponses,
PDFDataRangeTransport,
PDFDateString,