Skip to content

Commit 4e573be

Browse files
committed
Refactor a bit page mapping stuff in order to be able to support delete/copy pages
1 parent 6a4a3b0 commit 4e573be

21 files changed

Lines changed: 378 additions & 313 deletions

src/display/annotation_layer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ class AnnotationElement {
293293
this.annotationStorage.setValue(`${AnnotationEditorPrefix}${data.id}`, {
294294
id: data.id,
295295
annotationType: data.annotationType,
296-
pageIndex: this.parent.page._pageIndex,
296+
page: this.parent.page,
297297
popup,
298298
popupRef: data.popupRef,
299299
modificationDate: new Date(),

src/display/annotation_storage.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ class AnnotationStorage {
196196
val instanceof AnnotationEditor
197197
? val.serialize(/* isForCopying = */ false, context)
198198
: val;
199+
if (val.page) {
200+
val.pageIndex = val.page._pageIndex;
201+
delete val.page;
202+
}
199203
if (serialized) {
200204
map.set(key, serialized);
201205

src/display/api.js

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
deprecated,
4141
isDataScheme,
4242
isValidFetchUrl,
43+
PagesMapper,
4344
PageViewport,
4445
RenderingCancelledException,
4546
StatTimer,
@@ -1328,6 +1329,8 @@ class PDFDocumentProxy {
13281329
class PDFPageProxy {
13291330
#pendingCleanup = false;
13301331

1332+
#pagesMapper = PagesMapper.instance;
1333+
13311334
constructor(pageIndex, pageInfo, transport, pdfBug = false) {
13321335
this._pageIndex = pageIndex;
13331336
this._pageInfo = pageInfo;
@@ -1350,6 +1353,13 @@ class PDFPageProxy {
13501353
return this._pageIndex + 1;
13511354
}
13521355

1356+
/**
1357+
* @param {number} value - The page number to set. First page is 1.
1358+
*/
1359+
set pageNumber(value) {
1360+
this._pageIndex = value - 1;
1361+
}
1362+
13531363
/**
13541364
* @type {number} The number of degrees the page is rotated clockwise.
13551365
*/
@@ -1699,7 +1709,7 @@ class PDFPageProxy {
16991709
return this._transport.messageHandler.sendWithStream(
17001710
"GetTextContent",
17011711
{
1702-
pageIndex: this._pageIndex,
1712+
pageIndex: this.#pagesMapper.getPageId(this._pageIndex + 1) - 1,
17031713
includeMarkedContent: includeMarkedContent === true,
17041714
disableNormalization: disableNormalization === true,
17051715
},
@@ -1884,7 +1894,7 @@ class PDFPageProxy {
18841894
const readableStream = this._transport.messageHandler.sendWithStream(
18851895
"GetOperatorList",
18861896
{
1887-
pageIndex: this._pageIndex,
1897+
pageIndex: this.#pagesMapper.getPageId(this._pageIndex + 1) - 1,
18881898
intent: renderingIntent,
18891899
cacheKey,
18901900
annotationStorage: map,
@@ -2389,6 +2399,8 @@ class WorkerTransport {
23892399

23902400
#passwordCapability = null;
23912401

2402+
#pagesMapper = PagesMapper.instance;
2403+
23922404
constructor(
23932405
messageHandler,
23942406
loadingTask,
@@ -2710,6 +2722,7 @@ class WorkerTransport {
27102722
});
27112723

27122724
messageHandler.on("GetDoc", ({ pdfInfo }) => {
2725+
this.#pagesMapper.pagesNumber = pdfInfo.numPages;
27132726
this._numPages = pdfInfo.numPages;
27142727
this._htmlForXfa = pdfInfo.htmlForXfa;
27152728
delete pdfInfo.htmlForXfa;
@@ -2932,26 +2945,27 @@ class WorkerTransport {
29322945
if (
29332946
!Number.isInteger(pageNumber) ||
29342947
pageNumber <= 0 ||
2935-
pageNumber > this._numPages
2948+
pageNumber > this.#pagesMapper.pagesNumber
29362949
) {
29372950
return Promise.reject(new Error("Invalid page request."));
29382951
}
2952+
const pageIndex = pageNumber - 1;
2953+
const newPageIndex = this.#pagesMapper.getPageId(pageNumber) - 1;
29392954

2940-
const pageIndex = pageNumber - 1,
2941-
cachedPromise = this.#pagePromises.get(pageIndex);
2955+
const cachedPromise = this.#pagePromises.get(pageIndex);
29422956
if (cachedPromise) {
29432957
return cachedPromise;
29442958
}
29452959
const promise = this.messageHandler
29462960
.sendWithPromise("GetPage", {
2947-
pageIndex,
2961+
pageIndex: newPageIndex,
29482962
})
29492963
.then(pageInfo => {
29502964
if (this.destroyed) {
29512965
throw new Error("Transport destroyed");
29522966
}
29532967
if (pageInfo.refStr) {
2954-
this.#pageRefCache.set(pageInfo.refStr, pageNumber);
2968+
this.#pageRefCache.set(pageInfo.refStr, newPageIndex);
29552969
}
29562970

29572971
const page = new PDFPageProxy(
@@ -2967,19 +2981,20 @@ class WorkerTransport {
29672981
return promise;
29682982
}
29692983

2970-
getPageIndex(ref) {
2984+
async getPageIndex(ref) {
29712985
if (!isRefProxy(ref)) {
2972-
return Promise.reject(new Error("Invalid pageIndex request."));
2986+
throw new Error("Invalid pageIndex request.");
29732987
}
2974-
return this.messageHandler.sendWithPromise("GetPageIndex", {
2988+
const index = await this.messageHandler.sendWithPromise("GetPageIndex", {
29752989
num: ref.num,
29762990
gen: ref.gen,
29772991
});
2992+
return this.#pagesMapper.getPageNumber(index + 1) - 1;
29782993
}
29792994

29802995
getAnnotations(pageIndex, intent) {
29812996
return this.messageHandler.sendWithPromise("GetAnnotations", {
2982-
pageIndex,
2997+
pageIndex: this.#pagesMapper.getPageId(pageIndex + 1) - 1,
29832998
intent,
29842999
});
29853000
}
@@ -3046,13 +3061,13 @@ class WorkerTransport {
30463061

30473062
getPageJSActions(pageIndex) {
30483063
return this.messageHandler.sendWithPromise("GetPageJSActions", {
3049-
pageIndex,
3064+
pageIndex: this.#pagesMapper.getPageId(pageIndex + 1) - 1,
30503065
});
30513066
}
30523067

30533068
getStructTree(pageIndex) {
30543069
return this.messageHandler.sendWithPromise("GetStructTree", {
3055-
pageIndex,
3070+
pageIndex: this.#pagesMapper.getPageId(pageIndex + 1) - 1,
30563071
});
30573072
}
30583073

@@ -3122,7 +3137,10 @@ class WorkerTransport {
31223137
return null;
31233138
}
31243139
const refStr = ref.gen === 0 ? `${ref.num}R` : `${ref.num}R${ref.gen}`;
3125-
return this.#pageRefCache.get(refStr) ?? null;
3140+
const pageIndex = this.#pageRefCache.get(refStr);
3141+
return pageIndex >= 0
3142+
? this.#pagesMapper.getPageNumber(pageIndex + 1)
3143+
: null;
31263144
}
31273145
}
31283146

src/display/display_utils.js

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
BaseException,
1818
DrawOPS,
1919
FeatureTest,
20+
MathClamp,
2021
shadow,
2122
Util,
2223
warn,
@@ -1034,6 +1035,171 @@ function makePathFromDrawOPS(data) {
10341035
return path;
10351036
}
10361037

1038+
/**
1039+
* Maps between page IDs and page numbers, allowing bidirectional conversion
1040+
* between the two representations. This is useful when the page numbering
1041+
* in the PDF document doesn't match the default sequential ordering.
1042+
*/
1043+
class PagesMapper {
1044+
/**
1045+
* Maps page IDs to their corresponding page numbers.
1046+
* @type {Uint32Array|null}
1047+
*/
1048+
static #idToPageNumber = null;
1049+
1050+
/**
1051+
* Maps page numbers to their corresponding page IDs.
1052+
* @type {Uint32Array|null}
1053+
*/
1054+
static #pageNumberToId = null;
1055+
1056+
/**
1057+
* Previous mapping of page IDs to page numbers.
1058+
* @type {Uint32Array|null}
1059+
*/
1060+
static #prevIdToPageNumber = null;
1061+
1062+
/**
1063+
* The total number of pages.
1064+
* @type {number}
1065+
*/
1066+
static #pagesNumber = 0;
1067+
1068+
/**
1069+
* Gets the total number of pages.
1070+
* @returns {number} The number of pages.
1071+
*/
1072+
get pagesNumber() {
1073+
return PagesMapper.#pagesNumber;
1074+
}
1075+
1076+
/**
1077+
* Sets the total number of pages and initializes default mappings
1078+
* where page IDs equal page numbers (1-indexed).
1079+
* @param {number} n - The total number of pages.
1080+
*/
1081+
set pagesNumber(n) {
1082+
if (PagesMapper.#pagesNumber === n) {
1083+
return;
1084+
}
1085+
PagesMapper.#pagesNumber = n;
1086+
if (n === 0) {
1087+
PagesMapper.#pageNumberToId = null;
1088+
PagesMapper.#idToPageNumber = null;
1089+
}
1090+
}
1091+
1092+
#init() {
1093+
if (PagesMapper.#pageNumberToId) {
1094+
return;
1095+
}
1096+
const n = PagesMapper.#pagesNumber;
1097+
1098+
// Allocate a single array for better memory locality.
1099+
const array = new Uint32Array(3 * n);
1100+
const pageNumberToId = (PagesMapper.#pageNumberToId = array.subarray(0, n));
1101+
const idToPageNumber = (PagesMapper.#idToPageNumber = array.subarray(
1102+
n,
1103+
2 * n
1104+
));
1105+
for (let i = 0; i < n; i++) {
1106+
pageNumberToId[i] = idToPageNumber[i] = i + 1;
1107+
}
1108+
PagesMapper.#prevIdToPageNumber = array.subarray(2 * n);
1109+
}
1110+
1111+
/**
1112+
* Move a set of pages to a new position while keeping ID→number mappings in
1113+
* sync.
1114+
*
1115+
* @param {Set<number>} selectedPages - Page numbers being moved (1-indexed).
1116+
* @param {number[]} pagesToMove - Ordered list of page numbers to move.
1117+
* @param {number} index - Zero-based insertion index in the page-number list.
1118+
*/
1119+
movePages(selectedPages, pagesToMove, index) {
1120+
this.#init();
1121+
const pageNumberToId = PagesMapper.#pageNumberToId;
1122+
const idToPageNumber = PagesMapper.#idToPageNumber;
1123+
PagesMapper.#prevIdToPageNumber.set(idToPageNumber);
1124+
const movedCount = pagesToMove.length;
1125+
const mappedPagesToMove = new Uint32Array(movedCount);
1126+
let removedBeforeTarget = 0;
1127+
1128+
for (let i = 0; i < movedCount; i++) {
1129+
const pageIndex = pagesToMove[i] - 1;
1130+
mappedPagesToMove[i] = pageNumberToId[pageIndex];
1131+
if (pageIndex < index) {
1132+
removedBeforeTarget += 1;
1133+
}
1134+
}
1135+
1136+
const pagesNumber = PagesMapper.#pagesNumber;
1137+
// target index after removing elements that were before it
1138+
let adjustedTarget = index - removedBeforeTarget;
1139+
const remainingLen = pagesNumber - movedCount;
1140+
adjustedTarget = MathClamp(adjustedTarget, 0, remainingLen);
1141+
1142+
// Create the new mapping.
1143+
// First copy over the pages that are not being moved.
1144+
// Then insert the moved pages at the target position.
1145+
for (let i = 0, r = 0; i < pagesNumber; i++) {
1146+
if (!selectedPages.has(i + 1)) {
1147+
pageNumberToId[r++] = pageNumberToId[i];
1148+
}
1149+
}
1150+
1151+
// Shift the pages after the target position.
1152+
pageNumberToId.copyWithin(
1153+
adjustedTarget + movedCount,
1154+
adjustedTarget,
1155+
remainingLen
1156+
);
1157+
// Finally insert the moved pages.
1158+
pageNumberToId.set(mappedPagesToMove, adjustedTarget);
1159+
1160+
for (let i = 0, ii = pagesNumber; i < ii; i++) {
1161+
idToPageNumber[pageNumberToId[i] - 1] = i + 1;
1162+
}
1163+
}
1164+
1165+
getNewPageNumber(pageNumber) {
1166+
return PagesMapper.#prevIdToPageNumber[
1167+
PagesMapper.#pageNumberToId[pageNumber - 1] - 1
1168+
];
1169+
}
1170+
1171+
/**
1172+
* Gets the page number for a given page ID.
1173+
* @param {number} id - The page ID (1-indexed).
1174+
* @returns {number} The page number, or the ID itself if no mapping exists.
1175+
*/
1176+
getPageNumber(id) {
1177+
return PagesMapper.#idToPageNumber?.[id - 1] ?? id;
1178+
}
1179+
1180+
/**
1181+
* Gets the page ID for a given page number.
1182+
* @param {number} pageNumber - The page number (1-indexed).
1183+
* @returns {number} The page ID, or the page number itself if no mapping
1184+
* exists.
1185+
*/
1186+
getPageId(pageNumber) {
1187+
return PagesMapper.#pageNumberToId?.[pageNumber - 1] ?? pageNumber;
1188+
}
1189+
1190+
/**
1191+
* Gets or creates a singleton instance of PagesMapper.
1192+
* @returns {PagesMapper} The singleton instance.
1193+
*/
1194+
static get instance() {
1195+
return shadow(this, "instance", new PagesMapper());
1196+
}
1197+
1198+
getMapping() {
1199+
return PagesMapper.#pageNumberToId.subarray(0, this.pagesNumber);
1200+
}
1201+
}
1202+
10371203
export {
10381204
applyOpacity,
10391205
ColorScheme,
@@ -1054,6 +1220,7 @@ export {
10541220
makePathFromDrawOPS,
10551221
noContextMenu,
10561222
OutputScale,
1223+
PagesMapper,
10571224
PageViewport,
10581225
PDFDateString,
10591226
PixelsPerInch,

src/display/draw_layer.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ class DrawLayer {
3030

3131
static #id = 0;
3232

33-
constructor({ pageIndex }) {
34-
this.pageIndex = pageIndex;
35-
}
36-
3733
setParent(parent) {
3834
if (!this.#parent) {
3935
this.#parent = parent;
@@ -103,7 +99,7 @@ class DrawLayer {
10399
root.append(defs);
104100
const path = DrawLayer._svgFactory.createElement("path");
105101
defs.append(path);
106-
const pathId = `path_p${this.pageIndex}_${id}`;
102+
const pathId = `path_${id}`;
107103
path.setAttribute("id", pathId);
108104
path.setAttribute("vector-effect", "non-scaling-stroke");
109105

@@ -135,15 +131,15 @@ class DrawLayer {
135131
root.append(defs);
136132
const path = DrawLayer._svgFactory.createElement("path");
137133
defs.append(path);
138-
const pathId = `path_p${this.pageIndex}_${id}`;
134+
const pathId = `path_${id}`;
139135
path.setAttribute("id", pathId);
140136
path.setAttribute("vector-effect", "non-scaling-stroke");
141137

142138
let maskId;
143139
if (mustRemoveSelfIntersections) {
144140
const mask = DrawLayer._svgFactory.createElement("mask");
145141
defs.append(mask);
146-
maskId = `mask_p${this.pageIndex}_${id}`;
142+
maskId = `mask_${id}`;
147143
mask.setAttribute("id", maskId);
148144
mask.setAttribute("maskUnits", "objectBoundingBox");
149145
const rect = DrawLayer._svgFactory.createElement("rect");

0 commit comments

Comments
 (0)