mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-27 09:27:20 +02:00
Move the PagesMapper class into its own file
The `PagesMapper` class currently makes up one third of the `src/display/display_utils.js` file size, and since its introduction it's grown (a fair bit) in size. Note that the intention with files such as `src/display/display_utils.js` was to have somewhere to place functionality too small/simple to deserve its own file.
This commit is contained in:
parent
79df166e06
commit
9aa1ce8f14
@ -47,7 +47,6 @@ import {
|
|||||||
deprecated,
|
deprecated,
|
||||||
isDataScheme,
|
isDataScheme,
|
||||||
isValidFetchUrl,
|
isValidFetchUrl,
|
||||||
PagesMapper,
|
|
||||||
PageViewport,
|
PageViewport,
|
||||||
RenderingCancelledException,
|
RenderingCancelledException,
|
||||||
StatTimer,
|
StatTimer,
|
||||||
@ -82,6 +81,7 @@ import { DOMWasmFactory } from "display-wasm_factory";
|
|||||||
import { GlobalWorkerOptions } from "./worker_options.js";
|
import { GlobalWorkerOptions } from "./worker_options.js";
|
||||||
import { Metadata } from "./metadata.js";
|
import { Metadata } from "./metadata.js";
|
||||||
import { OptionalContentConfig } from "./optional_content_config.js";
|
import { OptionalContentConfig } from "./optional_content_config.js";
|
||||||
|
import { PagesMapper } from "./pages_mapper.js";
|
||||||
import { PDFDataTransportStream } from "./transport_stream.js";
|
import { PDFDataTransportStream } from "./transport_stream.js";
|
||||||
import { PDFFetchStream } from "display-fetch_stream";
|
import { PDFFetchStream } from "display-fetch_stream";
|
||||||
import { PDFNetworkStream } from "display-network";
|
import { PDFNetworkStream } from "display-network";
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import {
|
|||||||
BaseException,
|
BaseException,
|
||||||
DrawOPS,
|
DrawOPS,
|
||||||
FeatureTest,
|
FeatureTest,
|
||||||
makeArr,
|
|
||||||
MathClamp,
|
MathClamp,
|
||||||
shadow,
|
shadow,
|
||||||
stripPath,
|
stripPath,
|
||||||
@ -1033,464 +1032,6 @@ function makePathFromDrawOPS(data) {
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps between page IDs and page numbers, allowing bidirectional conversion
|
|
||||||
* between the two representations. This is useful when the page numbering
|
|
||||||
* in the PDF document doesn't match the default sequential ordering.
|
|
||||||
*/
|
|
||||||
class PagesMapper {
|
|
||||||
/**
|
|
||||||
* Maps page IDs to their corresponding page numbers.
|
|
||||||
* @type {Map<number, Array<number>>|null}
|
|
||||||
*/
|
|
||||||
#idToPageNumber = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps page numbers to their corresponding page IDs.
|
|
||||||
* @type {Uint32Array|null}
|
|
||||||
*/
|
|
||||||
#pageNumberToId = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Previous mapping of page IDs to page numbers.
|
|
||||||
* @type {Int32Array|null}
|
|
||||||
*/
|
|
||||||
#prevPageNumbers = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The total number of pages.
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
#pagesNumber = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listeners for page changes.
|
|
||||||
* @type {Array<function>}
|
|
||||||
*/
|
|
||||||
#listeners = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps page numbers to their corresponding page IDs (used in copy
|
|
||||||
* operations).
|
|
||||||
* @type {Uint32Array|null}
|
|
||||||
*/
|
|
||||||
#copiedPageIds = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps page IDs to their corresponding page numbers, used in copy operations.
|
|
||||||
* @type {Uint32Array|null}
|
|
||||||
*/
|
|
||||||
#copiedPageNumbers = null;
|
|
||||||
|
|
||||||
#savedData = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the total number of pages.
|
|
||||||
* @returns {number} The number of pages.
|
|
||||||
*/
|
|
||||||
get pagesNumber() {
|
|
||||||
return this.#pagesNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the total number of pages and initializes default mappings
|
|
||||||
* where page IDs equal page numbers (1-indexed).
|
|
||||||
* @param {number} n - The total number of pages.
|
|
||||||
*/
|
|
||||||
set pagesNumber(n) {
|
|
||||||
if (this.#pagesNumber === n) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.#pagesNumber = n;
|
|
||||||
this.#reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the page mappings to their default state, where page IDs equal page
|
|
||||||
* numbers (1-indexed). This is called when the number of pages changes, or
|
|
||||||
* when the current mapping matches the default mapping after a move
|
|
||||||
* operation.
|
|
||||||
*/
|
|
||||||
#reset() {
|
|
||||||
this.#pageNumberToId = null;
|
|
||||||
this.#idToPageNumber = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a listener function that will be called whenever the page mappings
|
|
||||||
* are updated.
|
|
||||||
* @param {function} listener
|
|
||||||
*/
|
|
||||||
addListener(listener) {
|
|
||||||
this.#listeners.push(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a previously added listener function.
|
|
||||||
* @param {function} listener
|
|
||||||
*/
|
|
||||||
removeListener(listener) {
|
|
||||||
const index = this.#listeners.indexOf(listener);
|
|
||||||
if (index >= 0) {
|
|
||||||
this.#listeners.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls all registered listener functions to notify them of changes to the
|
|
||||||
* page mappings.
|
|
||||||
* @param {Object} data - An object containing information about the update.
|
|
||||||
*/
|
|
||||||
#updateListeners(data) {
|
|
||||||
for (const listener of this.#listeners) {
|
|
||||||
listener(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the page mappings if they haven't been initialized yet.
|
|
||||||
* @param {boolean} mustInit
|
|
||||||
*/
|
|
||||||
#init(mustInit) {
|
|
||||||
if (this.#pageNumberToId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const n = this.#pagesNumber;
|
|
||||||
|
|
||||||
const pageNumberToId = (this.#pageNumberToId = new Uint32Array(n));
|
|
||||||
this.#prevPageNumbers = new Int32Array(pageNumberToId);
|
|
||||||
const idToPageNumber = (this.#idToPageNumber = new Map());
|
|
||||||
if (mustInit) {
|
|
||||||
for (let i = 1; i <= n; i++) {
|
|
||||||
pageNumberToId[i - 1] = i;
|
|
||||||
idToPageNumber.set(i, [i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the mapping from page IDs to page numbers based on the current
|
|
||||||
* mapping from page numbers to page IDs. This should be called after any
|
|
||||||
* changes to the page-number-to-ID mapping to keep the two mappings in sync.
|
|
||||||
*/
|
|
||||||
#updateIdToPageNumber() {
|
|
||||||
const idToPageNumber = this.#idToPageNumber;
|
|
||||||
const pageNumberToId = this.#pageNumberToId;
|
|
||||||
idToPageNumber.clear();
|
|
||||||
for (let i = 0, ii = this.#pagesNumber; i < ii; i++) {
|
|
||||||
const id = pageNumberToId[i];
|
|
||||||
const pageNumbers = idToPageNumber.get(id);
|
|
||||||
if (pageNumbers) {
|
|
||||||
pageNumbers.push(i + 1);
|
|
||||||
} else {
|
|
||||||
idToPageNumber.set(id, [i + 1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move a set of pages to a new position while keeping ID→number mappings in
|
|
||||||
* sync.
|
|
||||||
*
|
|
||||||
* @param {Set<number>} selectedPages - Page numbers being moved (1-indexed).
|
|
||||||
* @param {number[]} pagesToMove - Ordered list of page numbers to move.
|
|
||||||
* @param {number} index - Zero-based insertion index in the page-number list.
|
|
||||||
*/
|
|
||||||
movePages(selectedPages, pagesToMove, index) {
|
|
||||||
this.#init(true);
|
|
||||||
const pageNumberToId = this.#pageNumberToId;
|
|
||||||
const idToPageNumber = this.#idToPageNumber;
|
|
||||||
const movedCount = pagesToMove.length;
|
|
||||||
const mappedPagesToMove = new Uint32Array(movedCount);
|
|
||||||
let removedBeforeTarget = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < movedCount; i++) {
|
|
||||||
const pageIndex = pagesToMove[i] - 1;
|
|
||||||
mappedPagesToMove[i] = pageNumberToId[pageIndex];
|
|
||||||
if (pageIndex < index) {
|
|
||||||
removedBeforeTarget += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pagesNumber = this.#pagesNumber;
|
|
||||||
// target index after removing elements that were before it
|
|
||||||
let adjustedTarget = index - removedBeforeTarget;
|
|
||||||
const remainingLen = pagesNumber - movedCount;
|
|
||||||
adjustedTarget = MathClamp(adjustedTarget, 0, remainingLen);
|
|
||||||
|
|
||||||
// Create the new mapping.
|
|
||||||
// First copy over the pages that are not being moved.
|
|
||||||
// Then insert the moved pages at the target position.
|
|
||||||
for (let i = 0, r = 0; i < pagesNumber; i++) {
|
|
||||||
if (!selectedPages.has(i + 1)) {
|
|
||||||
pageNumberToId[r++] = pageNumberToId[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shift the pages after the target position.
|
|
||||||
pageNumberToId.copyWithin(
|
|
||||||
adjustedTarget + movedCount,
|
|
||||||
adjustedTarget,
|
|
||||||
remainingLen
|
|
||||||
);
|
|
||||||
// Finally insert the moved pages.
|
|
||||||
pageNumberToId.set(mappedPagesToMove, adjustedTarget);
|
|
||||||
|
|
||||||
this.#setPrevPageNumbers(idToPageNumber, null);
|
|
||||||
this.#updateIdToPageNumber();
|
|
||||||
this.#updateListeners({ type: "move" });
|
|
||||||
|
|
||||||
if (pageNumberToId.every((id, i) => id === i + 1)) {
|
|
||||||
this.#reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a set of pages while keeping ID→number mappings in sync.
|
|
||||||
* @param {Array<number>} pagesToDelete - Page numbers to delete (1-indexed).
|
|
||||||
* These must be unique and sorted in ascending order.
|
|
||||||
*/
|
|
||||||
deletePages(pagesToDelete) {
|
|
||||||
this.#init(true);
|
|
||||||
const pageNumberToId = this.#pageNumberToId;
|
|
||||||
const prevIdToPageNumber = this.#idToPageNumber;
|
|
||||||
|
|
||||||
this.#savedData = {
|
|
||||||
pageNumberToId: pageNumberToId.slice(),
|
|
||||||
idToPageNumber: new Map(prevIdToPageNumber),
|
|
||||||
pageNumber: this.#pagesNumber,
|
|
||||||
prevPageNumbers: this.#prevPageNumbers.slice(),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pagesNumber -= pagesToDelete.length;
|
|
||||||
this.#init(false);
|
|
||||||
const newPageNumberToId = this.#pageNumberToId;
|
|
||||||
|
|
||||||
let sourceIndex = 0;
|
|
||||||
let destIndex = 0;
|
|
||||||
for (const pageNumber of pagesToDelete) {
|
|
||||||
const pageIndex = pageNumber - 1;
|
|
||||||
if (pageIndex !== sourceIndex) {
|
|
||||||
newPageNumberToId.set(
|
|
||||||
pageNumberToId.subarray(sourceIndex, pageIndex),
|
|
||||||
destIndex
|
|
||||||
);
|
|
||||||
destIndex += pageIndex - sourceIndex;
|
|
||||||
}
|
|
||||||
sourceIndex = pageIndex + 1;
|
|
||||||
}
|
|
||||||
if (sourceIndex < pageNumberToId.length) {
|
|
||||||
newPageNumberToId.set(pageNumberToId.subarray(sourceIndex), destIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#setPrevPageNumbers(prevIdToPageNumber, null);
|
|
||||||
this.#updateIdToPageNumber();
|
|
||||||
this.#updateListeners({ type: "delete", pageNumbers: pagesToDelete });
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelDelete() {
|
|
||||||
if (this.#savedData) {
|
|
||||||
this.#pageNumberToId = this.#savedData.pageNumberToId;
|
|
||||||
this.#idToPageNumber = this.#savedData.idToPageNumber;
|
|
||||||
this.pagesNumber = this.#savedData.pageNumber;
|
|
||||||
this.#prevPageNumbers = this.#savedData.prevPageNumbers;
|
|
||||||
this.#savedData = null;
|
|
||||||
this.#updateListeners({ type: "cancelDelete" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanSavedData() {
|
|
||||||
this.#savedData = null;
|
|
||||||
this.#updateListeners({ type: "cleanSavedData" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies a set of pages while keeping ID→number mappings in sync.
|
|
||||||
* @param {Uint32Array} pagesToCopy - Page numbers to copy (1-indexed).
|
|
||||||
*/
|
|
||||||
copyPages(pagesToCopy) {
|
|
||||||
this.#init(true);
|
|
||||||
this.#copiedPageNumbers = pagesToCopy;
|
|
||||||
this.#copiedPageIds = pagesToCopy.map(
|
|
||||||
pageNumber => this.#pageNumberToId[pageNumber - 1]
|
|
||||||
);
|
|
||||||
this.#updateListeners({ type: "copy", pageNumbers: pagesToCopy });
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelCopy() {
|
|
||||||
this.#copiedPageIds = null;
|
|
||||||
this.#copiedPageNumbers = null;
|
|
||||||
this.#updateListeners({ type: "cancelCopy" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pastes a set of pages while keeping ID→number mappings in sync.
|
|
||||||
* @param {number} index - Zero-based insertion index in the page-number list.
|
|
||||||
*/
|
|
||||||
pastePages(index) {
|
|
||||||
this.#init(true);
|
|
||||||
const pageNumberToId = this.#pageNumberToId;
|
|
||||||
const prevIdToPageNumber = this.#idToPageNumber;
|
|
||||||
const copiedPageNumbers = this.#copiedPageNumbers;
|
|
||||||
|
|
||||||
const copiedPageMapping = new Map();
|
|
||||||
let base = index;
|
|
||||||
for (const pageNumber of copiedPageNumbers) {
|
|
||||||
copiedPageMapping.set(++base, pageNumber);
|
|
||||||
}
|
|
||||||
this.pagesNumber += copiedPageNumbers.length;
|
|
||||||
this.#init(false);
|
|
||||||
const newPageNumberToId = this.#pageNumberToId;
|
|
||||||
|
|
||||||
newPageNumberToId.set(pageNumberToId.subarray(0, index), 0);
|
|
||||||
newPageNumberToId.set(this.#copiedPageIds, index);
|
|
||||||
newPageNumberToId.set(
|
|
||||||
pageNumberToId.subarray(index),
|
|
||||||
index + copiedPageNumbers.length
|
|
||||||
);
|
|
||||||
|
|
||||||
this.#setPrevPageNumbers(prevIdToPageNumber, copiedPageMapping);
|
|
||||||
this.#updateIdToPageNumber();
|
|
||||||
this.#updateListeners({ type: "paste" });
|
|
||||||
|
|
||||||
this.#copiedPageIds = null;
|
|
||||||
this.#copiedPageNumbers = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the previous page numbers based on the current page-number-to-ID
|
|
||||||
* mapping and the provided previous ID-to-page-number mapping.
|
|
||||||
* This is used to keep track of the original page numbers for each page ID.
|
|
||||||
* @param {Map<number, Array<number>} prevIdToPageNumber
|
|
||||||
* @param {Map<number, number>|null} copiedPageMapping
|
|
||||||
*/
|
|
||||||
#setPrevPageNumbers(prevIdToPageNumber, copiedPageMapping) {
|
|
||||||
const prevPageNumbers = this.#prevPageNumbers;
|
|
||||||
const newPageNumberToId = this.#pageNumberToId;
|
|
||||||
const idsIndices = new Map();
|
|
||||||
for (let i = 0, ii = this.#pagesNumber; i < ii; i++) {
|
|
||||||
const oldPageNumber = copiedPageMapping?.get(i + 1);
|
|
||||||
if (oldPageNumber) {
|
|
||||||
prevPageNumbers[i] = -oldPageNumber;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const id = newPageNumberToId[i];
|
|
||||||
const j = idsIndices.get(id) || 0;
|
|
||||||
prevPageNumbers[i] = prevIdToPageNumber.get(id)?.[j];
|
|
||||||
idsIndices.set(id, j + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the page mappings have been altered from their initial state.
|
|
||||||
* @returns {boolean} True if the mappings have been altered, false otherwise.
|
|
||||||
*/
|
|
||||||
hasBeenAltered() {
|
|
||||||
return this.#pageNumberToId !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current page mapping suitable for saving.
|
|
||||||
* @returns {Object} An object containing the page indices.
|
|
||||||
*/
|
|
||||||
getPageMappingForSaving(idToPageNumber = this.#idToPageNumber) {
|
|
||||||
// idToPageNumber maps used 1-based IDs to 1-based page numbers.
|
|
||||||
// For example if the final pdf contains page 3 twice and they are moved at
|
|
||||||
// page 1 and 4, then it contains:
|
|
||||||
// pageNumberToId = [3, ., ., 3, ...,]
|
|
||||||
// idToPageNumber = {3: [1, 4], ...}
|
|
||||||
// In such a case we need to take a page 3 from the original pdf and take
|
|
||||||
// page 3 from a "copy".
|
|
||||||
// So we need to pass to the api something like:
|
|
||||||
// [ {
|
|
||||||
// document: null // this pdf
|
|
||||||
// includePages: [ 2, ... ], // page 3 is at index 2
|
|
||||||
// pageIndices: [0, ...], // page 3 will be at index 0 in the new pdf
|
|
||||||
// }, {
|
|
||||||
// document: null // this pdf
|
|
||||||
// includePages: [ 2, ... ], // page 3 is at index 2
|
|
||||||
// pageIndices: [3, ...], // page 3 will be at index 3 in the new pdf
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
|
|
||||||
let nCopy = 0;
|
|
||||||
for (const pageNumbers of idToPageNumber.values()) {
|
|
||||||
nCopy = Math.max(nCopy, pageNumbers.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractParams = new Array(nCopy);
|
|
||||||
for (let i = 0; i < nCopy; i++) {
|
|
||||||
extractParams[i] = {
|
|
||||||
document: null,
|
|
||||||
pageIndices: [],
|
|
||||||
includePages: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [id, pageNumbers] of idToPageNumber) {
|
|
||||||
for (let i = 0, ii = pageNumbers.length; i < ii; i++) {
|
|
||||||
extractParams[i].includePages.push([id - 1, pageNumbers[i] - 1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const { includePages, pageIndices } of extractParams) {
|
|
||||||
includePages.sort((a, b) => a[0] - b[0]);
|
|
||||||
for (let i = 0, ii = includePages.length; i < ii; i++) {
|
|
||||||
pageIndices.push(includePages[i][1]);
|
|
||||||
includePages[i] = includePages[i][0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return extractParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
extractPages(extractedPageNumbers) {
|
|
||||||
extractedPageNumbers = Array.from(extractedPageNumbers).sort(
|
|
||||||
(a, b) => a - b
|
|
||||||
);
|
|
||||||
const usedIds = new Map();
|
|
||||||
for (let i = 0, ii = extractedPageNumbers.length; i < ii; i++) {
|
|
||||||
const id = this.getPageId(extractedPageNumbers[i]);
|
|
||||||
const usedPageNumbers = usedIds.getOrInsertComputed(id, makeArr);
|
|
||||||
usedPageNumbers.push(i + 1);
|
|
||||||
}
|
|
||||||
return this.getPageMappingForSaving(usedIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the previous page number for a given page number.
|
|
||||||
* @param {number} pageNumber
|
|
||||||
* @returns {number} The previous page number for the given page number, or 0
|
|
||||||
* if no mapping exists.
|
|
||||||
*/
|
|
||||||
getPrevPageNumber(pageNumber) {
|
|
||||||
return this.#prevPageNumbers[pageNumber - 1] ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the page number for a given page ID.
|
|
||||||
* @param {number} id - The page ID (1-indexed).
|
|
||||||
* @returns {number} The page number, or 0 if no mapping exists.
|
|
||||||
*/
|
|
||||||
getPageNumber(id) {
|
|
||||||
return this.#idToPageNumber ? (this.#idToPageNumber.get(id)?.[0] ?? 0) : id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the page ID for a given page number.
|
|
||||||
* @param {number} pageNumber - The page number (1-indexed).
|
|
||||||
* @returns {number} The page ID, or the page number itself if no mapping
|
|
||||||
* exists.
|
|
||||||
*/
|
|
||||||
getPageId(pageNumber) {
|
|
||||||
return this.#pageNumberToId?.[pageNumber - 1] ?? pageNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
getMapping() {
|
|
||||||
return this.#pageNumberToId?.subarray(0, this.pagesNumber);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
applyOpacity,
|
applyOpacity,
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
@ -1511,7 +1052,6 @@ export {
|
|||||||
makePathFromDrawOPS,
|
makePathFromDrawOPS,
|
||||||
noContextMenu,
|
noContextMenu,
|
||||||
OutputScale,
|
OutputScale,
|
||||||
PagesMapper,
|
|
||||||
PageViewport,
|
PageViewport,
|
||||||
PDFDateString,
|
PDFDateString,
|
||||||
PixelsPerInch,
|
PixelsPerInch,
|
||||||
|
|||||||
476
src/display/pages_mapper.js
Normal file
476
src/display/pages_mapper.js
Normal file
@ -0,0 +1,476 @@
|
|||||||
|
/* Copyright 2026 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { makeArr, MathClamp } from "../shared/util.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps between page IDs and page numbers, allowing bidirectional conversion
|
||||||
|
* between the two representations. This is useful when the page numbering
|
||||||
|
* in the PDF document doesn't match the default sequential ordering.
|
||||||
|
*/
|
||||||
|
class PagesMapper {
|
||||||
|
/**
|
||||||
|
* Maps page IDs to their corresponding page numbers.
|
||||||
|
* @type {Map<number, Array<number>>|null}
|
||||||
|
*/
|
||||||
|
#idToPageNumber = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps page numbers to their corresponding page IDs.
|
||||||
|
* @type {Uint32Array|null}
|
||||||
|
*/
|
||||||
|
#pageNumberToId = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Previous mapping of page IDs to page numbers.
|
||||||
|
* @type {Int32Array|null}
|
||||||
|
*/
|
||||||
|
#prevPageNumbers = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of pages.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
#pagesNumber = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listeners for page changes.
|
||||||
|
* @type {Array<function>}
|
||||||
|
*/
|
||||||
|
#listeners = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps page numbers to their corresponding page IDs (used in copy
|
||||||
|
* operations).
|
||||||
|
* @type {Uint32Array|null}
|
||||||
|
*/
|
||||||
|
#copiedPageIds = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps page IDs to their corresponding page numbers, used in copy operations.
|
||||||
|
* @type {Uint32Array|null}
|
||||||
|
*/
|
||||||
|
#copiedPageNumbers = null;
|
||||||
|
|
||||||
|
#savedData = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the total number of pages.
|
||||||
|
* @returns {number} The number of pages.
|
||||||
|
*/
|
||||||
|
get pagesNumber() {
|
||||||
|
return this.#pagesNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the total number of pages and initializes default mappings
|
||||||
|
* where page IDs equal page numbers (1-indexed).
|
||||||
|
* @param {number} n - The total number of pages.
|
||||||
|
*/
|
||||||
|
set pagesNumber(n) {
|
||||||
|
if (this.#pagesNumber === n) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#pagesNumber = n;
|
||||||
|
this.#reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the page mappings to their default state, where page IDs equal page
|
||||||
|
* numbers (1-indexed). This is called when the number of pages changes, or
|
||||||
|
* when the current mapping matches the default mapping after a move
|
||||||
|
* operation.
|
||||||
|
*/
|
||||||
|
#reset() {
|
||||||
|
this.#pageNumberToId = null;
|
||||||
|
this.#idToPageNumber = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a listener function that will be called whenever the page mappings
|
||||||
|
* are updated.
|
||||||
|
* @param {function} listener
|
||||||
|
*/
|
||||||
|
addListener(listener) {
|
||||||
|
this.#listeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a previously added listener function.
|
||||||
|
* @param {function} listener
|
||||||
|
*/
|
||||||
|
removeListener(listener) {
|
||||||
|
const index = this.#listeners.indexOf(listener);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.#listeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls all registered listener functions to notify them of changes to the
|
||||||
|
* page mappings.
|
||||||
|
* @param {Object} data - An object containing information about the update.
|
||||||
|
*/
|
||||||
|
#updateListeners(data) {
|
||||||
|
for (const listener of this.#listeners) {
|
||||||
|
listener(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the page mappings if they haven't been initialized yet.
|
||||||
|
* @param {boolean} mustInit
|
||||||
|
*/
|
||||||
|
#init(mustInit) {
|
||||||
|
if (this.#pageNumberToId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = this.#pagesNumber;
|
||||||
|
|
||||||
|
const pageNumberToId = (this.#pageNumberToId = new Uint32Array(n));
|
||||||
|
this.#prevPageNumbers = new Int32Array(pageNumberToId);
|
||||||
|
const idToPageNumber = (this.#idToPageNumber = new Map());
|
||||||
|
if (mustInit) {
|
||||||
|
for (let i = 1; i <= n; i++) {
|
||||||
|
pageNumberToId[i - 1] = i;
|
||||||
|
idToPageNumber.set(i, [i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the mapping from page IDs to page numbers based on the current
|
||||||
|
* mapping from page numbers to page IDs. This should be called after any
|
||||||
|
* changes to the page-number-to-ID mapping to keep the two mappings in sync.
|
||||||
|
*/
|
||||||
|
#updateIdToPageNumber() {
|
||||||
|
const idToPageNumber = this.#idToPageNumber;
|
||||||
|
const pageNumberToId = this.#pageNumberToId;
|
||||||
|
idToPageNumber.clear();
|
||||||
|
for (let i = 0, ii = this.#pagesNumber; i < ii; i++) {
|
||||||
|
const id = pageNumberToId[i];
|
||||||
|
const pageNumbers = idToPageNumber.get(id);
|
||||||
|
if (pageNumbers) {
|
||||||
|
pageNumbers.push(i + 1);
|
||||||
|
} else {
|
||||||
|
idToPageNumber.set(id, [i + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a set of pages to a new position while keeping ID→number mappings in
|
||||||
|
* sync.
|
||||||
|
*
|
||||||
|
* @param {Set<number>} selectedPages - Page numbers being moved (1-indexed).
|
||||||
|
* @param {number[]} pagesToMove - Ordered list of page numbers to move.
|
||||||
|
* @param {number} index - Zero-based insertion index in the page-number list.
|
||||||
|
*/
|
||||||
|
movePages(selectedPages, pagesToMove, index) {
|
||||||
|
this.#init(true);
|
||||||
|
const pageNumberToId = this.#pageNumberToId;
|
||||||
|
const idToPageNumber = this.#idToPageNumber;
|
||||||
|
const movedCount = pagesToMove.length;
|
||||||
|
const mappedPagesToMove = new Uint32Array(movedCount);
|
||||||
|
let removedBeforeTarget = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < movedCount; i++) {
|
||||||
|
const pageIndex = pagesToMove[i] - 1;
|
||||||
|
mappedPagesToMove[i] = pageNumberToId[pageIndex];
|
||||||
|
if (pageIndex < index) {
|
||||||
|
removedBeforeTarget += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagesNumber = this.#pagesNumber;
|
||||||
|
// target index after removing elements that were before it
|
||||||
|
let adjustedTarget = index - removedBeforeTarget;
|
||||||
|
const remainingLen = pagesNumber - movedCount;
|
||||||
|
adjustedTarget = MathClamp(adjustedTarget, 0, remainingLen);
|
||||||
|
|
||||||
|
// Create the new mapping.
|
||||||
|
// First copy over the pages that are not being moved.
|
||||||
|
// Then insert the moved pages at the target position.
|
||||||
|
for (let i = 0, r = 0; i < pagesNumber; i++) {
|
||||||
|
if (!selectedPages.has(i + 1)) {
|
||||||
|
pageNumberToId[r++] = pageNumberToId[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift the pages after the target position.
|
||||||
|
pageNumberToId.copyWithin(
|
||||||
|
adjustedTarget + movedCount,
|
||||||
|
adjustedTarget,
|
||||||
|
remainingLen
|
||||||
|
);
|
||||||
|
// Finally insert the moved pages.
|
||||||
|
pageNumberToId.set(mappedPagesToMove, adjustedTarget);
|
||||||
|
|
||||||
|
this.#setPrevPageNumbers(idToPageNumber, null);
|
||||||
|
this.#updateIdToPageNumber();
|
||||||
|
this.#updateListeners({ type: "move" });
|
||||||
|
|
||||||
|
if (pageNumberToId.every((id, i) => id === i + 1)) {
|
||||||
|
this.#reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a set of pages while keeping ID→number mappings in sync.
|
||||||
|
* @param {Array<number>} pagesToDelete - Page numbers to delete (1-indexed).
|
||||||
|
* These must be unique and sorted in ascending order.
|
||||||
|
*/
|
||||||
|
deletePages(pagesToDelete) {
|
||||||
|
this.#init(true);
|
||||||
|
const pageNumberToId = this.#pageNumberToId;
|
||||||
|
const prevIdToPageNumber = this.#idToPageNumber;
|
||||||
|
|
||||||
|
this.#savedData = {
|
||||||
|
pageNumberToId: pageNumberToId.slice(),
|
||||||
|
idToPageNumber: new Map(prevIdToPageNumber),
|
||||||
|
pageNumber: this.#pagesNumber,
|
||||||
|
prevPageNumbers: this.#prevPageNumbers.slice(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pagesNumber -= pagesToDelete.length;
|
||||||
|
this.#init(false);
|
||||||
|
const newPageNumberToId = this.#pageNumberToId;
|
||||||
|
|
||||||
|
let sourceIndex = 0;
|
||||||
|
let destIndex = 0;
|
||||||
|
for (const pageNumber of pagesToDelete) {
|
||||||
|
const pageIndex = pageNumber - 1;
|
||||||
|
if (pageIndex !== sourceIndex) {
|
||||||
|
newPageNumberToId.set(
|
||||||
|
pageNumberToId.subarray(sourceIndex, pageIndex),
|
||||||
|
destIndex
|
||||||
|
);
|
||||||
|
destIndex += pageIndex - sourceIndex;
|
||||||
|
}
|
||||||
|
sourceIndex = pageIndex + 1;
|
||||||
|
}
|
||||||
|
if (sourceIndex < pageNumberToId.length) {
|
||||||
|
newPageNumberToId.set(pageNumberToId.subarray(sourceIndex), destIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#setPrevPageNumbers(prevIdToPageNumber, null);
|
||||||
|
this.#updateIdToPageNumber();
|
||||||
|
this.#updateListeners({ type: "delete", pageNumbers: pagesToDelete });
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelDelete() {
|
||||||
|
if (this.#savedData) {
|
||||||
|
this.#pageNumberToId = this.#savedData.pageNumberToId;
|
||||||
|
this.#idToPageNumber = this.#savedData.idToPageNumber;
|
||||||
|
this.pagesNumber = this.#savedData.pageNumber;
|
||||||
|
this.#prevPageNumbers = this.#savedData.prevPageNumbers;
|
||||||
|
this.#savedData = null;
|
||||||
|
this.#updateListeners({ type: "cancelDelete" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanSavedData() {
|
||||||
|
this.#savedData = null;
|
||||||
|
this.#updateListeners({ type: "cleanSavedData" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a set of pages while keeping ID→number mappings in sync.
|
||||||
|
* @param {Uint32Array} pagesToCopy - Page numbers to copy (1-indexed).
|
||||||
|
*/
|
||||||
|
copyPages(pagesToCopy) {
|
||||||
|
this.#init(true);
|
||||||
|
this.#copiedPageNumbers = pagesToCopy;
|
||||||
|
this.#copiedPageIds = pagesToCopy.map(
|
||||||
|
pageNumber => this.#pageNumberToId[pageNumber - 1]
|
||||||
|
);
|
||||||
|
this.#updateListeners({ type: "copy", pageNumbers: pagesToCopy });
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelCopy() {
|
||||||
|
this.#copiedPageIds = null;
|
||||||
|
this.#copiedPageNumbers = null;
|
||||||
|
this.#updateListeners({ type: "cancelCopy" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pastes a set of pages while keeping ID→number mappings in sync.
|
||||||
|
* @param {number} index - Zero-based insertion index in the page-number list.
|
||||||
|
*/
|
||||||
|
pastePages(index) {
|
||||||
|
this.#init(true);
|
||||||
|
const pageNumberToId = this.#pageNumberToId;
|
||||||
|
const prevIdToPageNumber = this.#idToPageNumber;
|
||||||
|
const copiedPageNumbers = this.#copiedPageNumbers;
|
||||||
|
|
||||||
|
const copiedPageMapping = new Map();
|
||||||
|
let base = index;
|
||||||
|
for (const pageNumber of copiedPageNumbers) {
|
||||||
|
copiedPageMapping.set(++base, pageNumber);
|
||||||
|
}
|
||||||
|
this.pagesNumber += copiedPageNumbers.length;
|
||||||
|
this.#init(false);
|
||||||
|
const newPageNumberToId = this.#pageNumberToId;
|
||||||
|
|
||||||
|
newPageNumberToId.set(pageNumberToId.subarray(0, index), 0);
|
||||||
|
newPageNumberToId.set(this.#copiedPageIds, index);
|
||||||
|
newPageNumberToId.set(
|
||||||
|
pageNumberToId.subarray(index),
|
||||||
|
index + copiedPageNumbers.length
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#setPrevPageNumbers(prevIdToPageNumber, copiedPageMapping);
|
||||||
|
this.#updateIdToPageNumber();
|
||||||
|
this.#updateListeners({ type: "paste" });
|
||||||
|
|
||||||
|
this.#copiedPageIds = null;
|
||||||
|
this.#copiedPageNumbers = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the previous page numbers based on the current page-number-to-ID
|
||||||
|
* mapping and the provided previous ID-to-page-number mapping.
|
||||||
|
* This is used to keep track of the original page numbers for each page ID.
|
||||||
|
* @param {Map<number, Array<number>} prevIdToPageNumber
|
||||||
|
* @param {Map<number, number>|null} copiedPageMapping
|
||||||
|
*/
|
||||||
|
#setPrevPageNumbers(prevIdToPageNumber, copiedPageMapping) {
|
||||||
|
const prevPageNumbers = this.#prevPageNumbers;
|
||||||
|
const newPageNumberToId = this.#pageNumberToId;
|
||||||
|
const idsIndices = new Map();
|
||||||
|
for (let i = 0, ii = this.#pagesNumber; i < ii; i++) {
|
||||||
|
const oldPageNumber = copiedPageMapping?.get(i + 1);
|
||||||
|
if (oldPageNumber) {
|
||||||
|
prevPageNumbers[i] = -oldPageNumber;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const id = newPageNumberToId[i];
|
||||||
|
const j = idsIndices.get(id) || 0;
|
||||||
|
prevPageNumbers[i] = prevIdToPageNumber.get(id)?.[j];
|
||||||
|
idsIndices.set(id, j + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the page mappings have been altered from their initial state.
|
||||||
|
* @returns {boolean} True if the mappings have been altered, false otherwise.
|
||||||
|
*/
|
||||||
|
hasBeenAltered() {
|
||||||
|
return this.#pageNumberToId !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current page mapping suitable for saving.
|
||||||
|
* @returns {Object} An object containing the page indices.
|
||||||
|
*/
|
||||||
|
getPageMappingForSaving(idToPageNumber = this.#idToPageNumber) {
|
||||||
|
// idToPageNumber maps used 1-based IDs to 1-based page numbers.
|
||||||
|
// For example if the final pdf contains page 3 twice and they are moved at
|
||||||
|
// page 1 and 4, then it contains:
|
||||||
|
// pageNumberToId = [3, ., ., 3, ...,]
|
||||||
|
// idToPageNumber = {3: [1, 4], ...}
|
||||||
|
// In such a case we need to take a page 3 from the original pdf and take
|
||||||
|
// page 3 from a "copy".
|
||||||
|
// So we need to pass to the api something like:
|
||||||
|
// [ {
|
||||||
|
// document: null // this pdf
|
||||||
|
// includePages: [ 2, ... ], // page 3 is at index 2
|
||||||
|
// pageIndices: [0, ...], // page 3 will be at index 0 in the new pdf
|
||||||
|
// }, {
|
||||||
|
// document: null // this pdf
|
||||||
|
// includePages: [ 2, ... ], // page 3 is at index 2
|
||||||
|
// pageIndices: [3, ...], // page 3 will be at index 3 in the new pdf
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
|
||||||
|
let nCopy = 0;
|
||||||
|
for (const pageNumbers of idToPageNumber.values()) {
|
||||||
|
nCopy = Math.max(nCopy, pageNumbers.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractParams = new Array(nCopy);
|
||||||
|
for (let i = 0; i < nCopy; i++) {
|
||||||
|
extractParams[i] = {
|
||||||
|
document: null,
|
||||||
|
pageIndices: [],
|
||||||
|
includePages: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, pageNumbers] of idToPageNumber) {
|
||||||
|
for (let i = 0, ii = pageNumbers.length; i < ii; i++) {
|
||||||
|
extractParams[i].includePages.push([id - 1, pageNumbers[i] - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { includePages, pageIndices } of extractParams) {
|
||||||
|
includePages.sort((a, b) => a[0] - b[0]);
|
||||||
|
for (let i = 0, ii = includePages.length; i < ii; i++) {
|
||||||
|
pageIndices.push(includePages[i][1]);
|
||||||
|
includePages[i] = includePages[i][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
extractPages(extractedPageNumbers) {
|
||||||
|
extractedPageNumbers = Array.from(extractedPageNumbers).sort(
|
||||||
|
(a, b) => a - b
|
||||||
|
);
|
||||||
|
const usedIds = new Map();
|
||||||
|
for (let i = 0, ii = extractedPageNumbers.length; i < ii; i++) {
|
||||||
|
const id = this.getPageId(extractedPageNumbers[i]);
|
||||||
|
const usedPageNumbers = usedIds.getOrInsertComputed(id, makeArr);
|
||||||
|
usedPageNumbers.push(i + 1);
|
||||||
|
}
|
||||||
|
return this.getPageMappingForSaving(usedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the previous page number for a given page number.
|
||||||
|
* @param {number} pageNumber
|
||||||
|
* @returns {number} The previous page number for the given page number, or 0
|
||||||
|
* if no mapping exists.
|
||||||
|
*/
|
||||||
|
getPrevPageNumber(pageNumber) {
|
||||||
|
return this.#prevPageNumbers[pageNumber - 1] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the page number for a given page ID.
|
||||||
|
* @param {number} id - The page ID (1-indexed).
|
||||||
|
* @returns {number} The page number, or 0 if no mapping exists.
|
||||||
|
*/
|
||||||
|
getPageNumber(id) {
|
||||||
|
return this.#idToPageNumber ? (this.#idToPageNumber.get(id)?.[0] ?? 0) : id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the page ID for a given page number.
|
||||||
|
* @param {number} pageNumber - The page number (1-indexed).
|
||||||
|
* @returns {number} The page ID, or the page number itself if no mapping
|
||||||
|
* exists.
|
||||||
|
*/
|
||||||
|
getPageId(pageNumber) {
|
||||||
|
return this.#pageNumberToId?.[pageNumber - 1] ?? pageNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMapping() {
|
||||||
|
return this.#pageNumberToId?.subarray(0, this.pagesNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PagesMapper };
|
||||||
Loading…
x
Reference in New Issue
Block a user