web-doc 0.13.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/edit/pdf/engine/document.js +78 -19
- package/dist/edit/pdf/engine/text-box.d.ts +1 -1
- package/dist/edit/pdf/engine/text-box.js +21 -6
- package/dist/edit/pdf/engine/transform.js +2 -2
- package/dist/edit/session.js +48 -11
- package/dist/workers/pdf-edit-worker.js +95 -26
- package/package.json +1 -1
|
@@ -87,6 +87,12 @@ const CREATING_OPERATIONS = new Set([
|
|
|
87
87
|
"insertImage",
|
|
88
88
|
"insertTable",
|
|
89
89
|
]);
|
|
90
|
+
/** Operations that change page order, and with it the index a loaded page is kept by. */
|
|
91
|
+
const PAGE_ORDER_OPERATIONS = new Set([
|
|
92
|
+
"insertPage",
|
|
93
|
+
"deletePage",
|
|
94
|
+
"movePage",
|
|
95
|
+
]);
|
|
90
96
|
function referenceOf(operation) {
|
|
91
97
|
const target = operation.target;
|
|
92
98
|
return typeof target === "string" ? parseReference(target) : undefined;
|
|
@@ -129,6 +135,8 @@ export class PdfEditDocument {
|
|
|
129
135
|
#batches = 0;
|
|
130
136
|
/** Browser faces of the document's fonts, by font program; see `textFont`. */
|
|
131
137
|
#faces = new Map();
|
|
138
|
+
/** Pages the running batch changed, by index; see `#batch`. */
|
|
139
|
+
#written;
|
|
132
140
|
constructor(pdfium, original, fonts = new FontLibrary(async () => {
|
|
133
141
|
throw new Error("No font source is configured");
|
|
134
142
|
}), limits = defaultResourceLimits, assets = new AssetStore(), compact = compactPdf) {
|
|
@@ -205,6 +213,9 @@ export class PdfEditDocument {
|
|
|
205
213
|
* the next batch number as their state id.
|
|
206
214
|
*/
|
|
207
215
|
apply(input) {
|
|
216
|
+
return this.#batch(() => this.#apply(input));
|
|
217
|
+
}
|
|
218
|
+
#apply(input) {
|
|
208
219
|
const batch = Array.isArray(input)
|
|
209
220
|
? { stateId: this.#batches + 1, operations: input }
|
|
210
221
|
: input;
|
|
@@ -225,6 +236,8 @@ export class PdfEditDocument {
|
|
|
225
236
|
details: { signatures: this.#signatures, features: this.#features },
|
|
226
237
|
});
|
|
227
238
|
operations.forEach((raw, operationIndex) => {
|
|
239
|
+
if (PAGE_ORDER_OPERATIONS.has(raw.op))
|
|
240
|
+
this.#writeBack();
|
|
228
241
|
const handler = handlers[raw.op];
|
|
229
242
|
if (!handler)
|
|
230
243
|
throw new ViewerError("internal", `Unknown pdf operation ${raw.op}`);
|
|
@@ -693,28 +706,71 @@ export class PdfEditDocument {
|
|
|
693
706
|
}
|
|
694
707
|
: undefined;
|
|
695
708
|
}
|
|
696
|
-
/**
|
|
709
|
+
/**
|
|
710
|
+
* Loads a page and lets `use` change it. The page stays loaded until the
|
|
711
|
+
* batch ends, so what the batch reads next sees the change; see `#batch`.
|
|
712
|
+
*/
|
|
697
713
|
#writePage(pageIndex, use) {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
714
|
+
if (!this.#written)
|
|
715
|
+
return this.#batch(() => this.#writePage(pageIndex, use));
|
|
716
|
+
let page = this.#written.get(pageIndex);
|
|
717
|
+
if (page === undefined) {
|
|
718
|
+
page = this.#pdfium.lib.FPDF_LoadPage(this.#document.handle, pageIndex);
|
|
719
|
+
if (!page)
|
|
720
|
+
throw new ViewerError("render-failed", "PDFium could not load the page", { details: { pageIndex } });
|
|
721
|
+
this.#written.set(pageIndex, page);
|
|
722
|
+
}
|
|
723
|
+
// Objects are assigned before the change so appended ones follow them.
|
|
724
|
+
this.#objectsOf(pageIndex, page);
|
|
725
|
+
const result = use(page);
|
|
726
|
+
delete this.#pages[pageIndex].elements;
|
|
727
|
+
delete this.#pages[pageIndex].paragraphs;
|
|
728
|
+
return result;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Runs `run` with the pages it changes kept loaded, then regenerates each
|
|
732
|
+
* page's content stream once. PDFium rewrites a whole stream however
|
|
733
|
+
* little changed, which takes seconds on a page of thousands of objects,
|
|
734
|
+
* and object reads (text, bounds) need no regenerated content. A failing
|
|
735
|
+
* batch closes its pages unwritten: the caller restores the working copy.
|
|
736
|
+
*/
|
|
737
|
+
#batch(run) {
|
|
738
|
+
if (this.#written)
|
|
739
|
+
return run();
|
|
740
|
+
const written = new Map();
|
|
741
|
+
this.#written = written;
|
|
704
742
|
try {
|
|
705
|
-
|
|
706
|
-
this.#
|
|
707
|
-
const result = use(page);
|
|
708
|
-
if (!lib.FPDFPage_GenerateContent(page))
|
|
709
|
-
throw new ViewerError("edit-failed", "PDFium could not rewrite the page", {
|
|
710
|
-
details: { stage: "apply", pageIndex },
|
|
711
|
-
});
|
|
712
|
-
delete this.#pages[pageIndex].elements;
|
|
713
|
-
delete this.#pages[pageIndex].paragraphs;
|
|
743
|
+
const result = run();
|
|
744
|
+
this.#writeBack();
|
|
714
745
|
return result;
|
|
715
746
|
}
|
|
716
747
|
finally {
|
|
717
|
-
|
|
748
|
+
this.#written = undefined;
|
|
749
|
+
for (const [pageIndex, page] of written) {
|
|
750
|
+
this.#pdfium.lib.FPDF_ClosePage(page);
|
|
751
|
+
delete this.#pages[pageIndex]?.elements;
|
|
752
|
+
delete this.#pages[pageIndex]?.paragraphs;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/** Regenerates and closes the pages the running batch changed. */
|
|
757
|
+
#writeBack() {
|
|
758
|
+
const written = this.#written;
|
|
759
|
+
if (!written)
|
|
760
|
+
return;
|
|
761
|
+
const { lib } = this.#pdfium;
|
|
762
|
+
for (const [pageIndex, page] of written) {
|
|
763
|
+
written.delete(pageIndex);
|
|
764
|
+
try {
|
|
765
|
+
if (!lib.FPDFPage_GenerateContent(page))
|
|
766
|
+
throw new ViewerError("edit-failed", "PDFium could not rewrite the page", { details: { stage: "apply", pageIndex } });
|
|
767
|
+
}
|
|
768
|
+
finally {
|
|
769
|
+
lib.FPDF_ClosePage(page);
|
|
770
|
+
}
|
|
771
|
+
// Elements are read again from the regenerated content.
|
|
772
|
+
delete this.#pages[pageIndex].elements;
|
|
773
|
+
delete this.#pages[pageIndex].paragraphs;
|
|
718
774
|
}
|
|
719
775
|
}
|
|
720
776
|
#originalPages() {
|
|
@@ -907,7 +963,9 @@ export class PdfEditDocument {
|
|
|
907
963
|
}
|
|
908
964
|
#withPage(pageIndex, use) {
|
|
909
965
|
const { lib } = this.#pdfium;
|
|
910
|
-
|
|
966
|
+
// A page the running batch changed is read as it stands in memory.
|
|
967
|
+
const written = this.#written?.get(pageIndex);
|
|
968
|
+
const page = written ?? lib.FPDF_LoadPage(this.#document.handle, pageIndex);
|
|
911
969
|
if (!page)
|
|
912
970
|
throw new ViewerError("render-failed", "PDFium could not load the page", {
|
|
913
971
|
details: { pageIndex },
|
|
@@ -922,7 +980,8 @@ export class PdfEditDocument {
|
|
|
922
980
|
}
|
|
923
981
|
}
|
|
924
982
|
finally {
|
|
925
|
-
|
|
983
|
+
if (written === undefined)
|
|
984
|
+
lib.FPDF_ClosePage(page);
|
|
926
985
|
}
|
|
927
986
|
}
|
|
928
987
|
*#matches(textPage, query, options) {
|
|
@@ -68,4 +68,4 @@ export declare function placeUpright(pdfium: Pdfium, object: number, geometry: P
|
|
|
68
68
|
export declare function setText(pdfium: Pdfium, object: number, text: string): void;
|
|
69
69
|
export declare function parseColor(color: string): [number, number, number];
|
|
70
70
|
export declare function resolveStyle(style: PdfTextBoxStyle): TextBoxSpec["style"];
|
|
71
|
-
export declare function validateRect(rect: PageRect, geometry: PageGeometry, issue: Issue, path?: string): void;
|
|
71
|
+
export declare function validateRect(rect: PageRect, geometry: PageGeometry, issue: Issue, path?: string, current?: PageRect): void;
|
|
@@ -319,12 +319,27 @@ export function resolveStyle(style) {
|
|
|
319
319
|
function definedFields(style) {
|
|
320
320
|
return Object.fromEntries(Object.entries(style).filter(([, value]) => value !== undefined));
|
|
321
321
|
}
|
|
322
|
-
export function validateRect(rect, geometry, issue, path = "/rect") {
|
|
322
|
+
export function validateRect(rect, geometry, issue, path = "/rect", current) {
|
|
323
323
|
const size = displayedSize(geometry);
|
|
324
324
|
const tolerance = 0.01;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
325
|
+
// Imported objects may already bleed outside the visible crop. Transforms
|
|
326
|
+
// can keep or reduce that overflow; the page tolerance must not accumulate
|
|
327
|
+
// on their current edges and permit repeated small outward steps.
|
|
328
|
+
const left = Math.min(-tolerance, current?.x ?? 0);
|
|
329
|
+
const top = Math.min(-tolerance, current?.y ?? 0);
|
|
330
|
+
const right = Math.max(size.width + tolerance, current ? current.x + current.width : 0);
|
|
331
|
+
const bottom = Math.max(size.height + tolerance, current ? current.y + current.height : 0);
|
|
332
|
+
const missesPage = current !== undefined &&
|
|
333
|
+
(rect.x >= size.width ||
|
|
334
|
+
rect.y >= size.height ||
|
|
335
|
+
rect.x + rect.width <= 0 ||
|
|
336
|
+
rect.y + rect.height <= 0);
|
|
337
|
+
if (rect.x < left ||
|
|
338
|
+
rect.y < top ||
|
|
339
|
+
rect.x + rect.width > right ||
|
|
340
|
+
rect.y + rect.height > bottom ||
|
|
341
|
+
missesPage)
|
|
342
|
+
issue(path, "range", current
|
|
343
|
+
? `The rectangle must preserve or reduce existing overflow and remain partly visible within the ${size.width}×${size.height} pt page`
|
|
344
|
+
: `The rectangle must lie within the ${size.width}×${size.height} pt page`);
|
|
330
345
|
}
|
|
@@ -20,7 +20,7 @@ export const moveElement = {
|
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
const delta = moveDelta(operation, target.element.bounds);
|
|
23
|
-
validateRect(shifted(target.element.bounds, delta.dx, delta.dy), context.geometry(target.location.pageIndex), issue, operation.to ? "/to" : "/by");
|
|
23
|
+
validateRect(shifted(target.element.bounds, delta.dx, delta.dy), context.geometry(target.location.pageIndex), issue, operation.to ? "/to" : "/by", target.element.bounds);
|
|
24
24
|
},
|
|
25
25
|
apply(operation, context) {
|
|
26
26
|
const { location, element } = anyTarget(operation.target, context);
|
|
@@ -61,7 +61,7 @@ export const resizeElement = {
|
|
|
61
61
|
issue("/target", "unsupported-target", "Tables and imported paragraphs cannot be resized");
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
|
-
validateRect(operation.rect, context.geometry(target.location.pageIndex), issue);
|
|
64
|
+
validateRect(operation.rect, context.geometry(target.location.pageIndex), issue, "/rect", target.element.bounds);
|
|
65
65
|
},
|
|
66
66
|
apply(operation, context) {
|
|
67
67
|
const { location, element } = anyTarget(operation.target, context);
|
package/dist/edit/session.js
CHANGED
|
@@ -6,6 +6,11 @@ import { buildToolSet, callTool as runTool } from "./ai/tools.js";
|
|
|
6
6
|
import { assetIdOf, AssetStore, binaryFields, isAssetReference, } from "./assets.js";
|
|
7
7
|
import { batchOf, EditHistory, modeOf } from "./history.js";
|
|
8
8
|
import { assertBatchSize, checkOperations, freezeOperations, invalidOperationError, parseReference, } from "./operations.js";
|
|
9
|
+
/**
|
|
10
|
+
* A state the engine would take at least this long to rebuild by replaying
|
|
11
|
+
* batches keeps its bytes, so undo, redo and recovery reopen it instead.
|
|
12
|
+
*/
|
|
13
|
+
const SLOW_REPLAY_MS = 1000;
|
|
9
14
|
/**
|
|
10
15
|
* The format-independent editing session: validation, history, revisions,
|
|
11
16
|
* saving and the viewer refresh. Format modules wrap it to add typed methods.
|
|
@@ -26,6 +31,8 @@ export class EditSessionController {
|
|
|
26
31
|
/** Materialized bytes of some committed states, by state id, so restores replay less. */
|
|
27
32
|
#checkpoints = new Map();
|
|
28
33
|
#checkpointBytes = 0;
|
|
34
|
+
/** Engine time each reachable state took to build from the state before it. */
|
|
35
|
+
#buildMs = new Map();
|
|
29
36
|
/** Named checkpoints by id, in creation order. */
|
|
30
37
|
#named = new Map();
|
|
31
38
|
/** State ids named checkpoints pin, with how many name each; never evicted. */
|
|
@@ -106,10 +113,12 @@ export class EditSessionController {
|
|
|
106
113
|
});
|
|
107
114
|
}
|
|
108
115
|
const before = this.#history.pageCount;
|
|
109
|
-
const { change, shown } = await this.#transaction(signal, "apply", async () => {
|
|
116
|
+
const { change, shown, buildMs } = await this.#transaction(signal, "apply", async () => {
|
|
117
|
+
const started = performance.now();
|
|
110
118
|
const result = await this.#engine.apply(engineBatch, signal);
|
|
111
119
|
return {
|
|
112
120
|
change: result,
|
|
121
|
+
buildMs: performance.now() - started,
|
|
113
122
|
shown: await this.#show(signal, this.#pagesOf(result.changedPages, result.reflowFrom)),
|
|
114
123
|
};
|
|
115
124
|
});
|
|
@@ -126,7 +135,7 @@ export class EditSessionController {
|
|
|
126
135
|
pageCountBefore: before,
|
|
127
136
|
pageCountAfter: shown.pageCount,
|
|
128
137
|
});
|
|
129
|
-
this.#commit("apply", shown.changedPages, shown);
|
|
138
|
+
this.#commit("apply", shown.changedPages, shown, buildMs);
|
|
130
139
|
return this.#receipt(false, batch.length, change.createdIds, {
|
|
131
140
|
...change,
|
|
132
141
|
changedPages: shown.changedPages,
|
|
@@ -293,8 +302,11 @@ export class EditSessionController {
|
|
|
293
302
|
return this.#noop();
|
|
294
303
|
const before = this.#history.pageCount;
|
|
295
304
|
const changedPages = allPages(Math.max(before, named.pageCount));
|
|
305
|
+
let buildMs = 0;
|
|
296
306
|
const shown = await this.#transaction(signal, "apply", async () => {
|
|
307
|
+
const started = performance.now();
|
|
297
308
|
await this.#engine.restore(this.#targetFor(named.entries), signal);
|
|
309
|
+
buildMs = performance.now() - started;
|
|
298
310
|
return this.#show(signal, changedPages);
|
|
299
311
|
});
|
|
300
312
|
const diff = entryDiff(this.#history.entriesAt(this.#history.position), named.entries);
|
|
@@ -307,7 +319,7 @@ export class EditSessionController {
|
|
|
307
319
|
pageCountAfter: shown.pageCount,
|
|
308
320
|
base: { stateId: named.stateId, batches: named.batches },
|
|
309
321
|
}, named.stateId);
|
|
310
|
-
this.#commit("restore", shown.changedPages, shown);
|
|
322
|
+
this.#commit("restore", shown.changedPages, shown, buildMs);
|
|
311
323
|
return this.#receipt(false, 0, diff.created, {
|
|
312
324
|
removedIds: diff.removed,
|
|
313
325
|
changedPages: shown.changedPages,
|
|
@@ -509,10 +521,11 @@ export class EditSessionController {
|
|
|
509
521
|
clearTimeout(timer);
|
|
510
522
|
}
|
|
511
523
|
}
|
|
512
|
-
|
|
524
|
+
/** `buildMs`: the engine time the new state took, for an apply or a restore. */
|
|
525
|
+
#commit(reason, changedPages, shown, buildMs = 0) {
|
|
513
526
|
this.#committedBytes = shown.bytes;
|
|
514
527
|
if (reason === "apply" || reason === "restore")
|
|
515
|
-
this.#keepCheckpoint(shown.bytes);
|
|
528
|
+
this.#keepCheckpoint(shown.bytes, buildMs);
|
|
516
529
|
else if (reason === "reset")
|
|
517
530
|
this.#dropCheckpoints();
|
|
518
531
|
this.#revision += 1;
|
|
@@ -595,8 +608,11 @@ export class EditSessionController {
|
|
|
595
608
|
throw invalidOperationError(issues);
|
|
596
609
|
return Object.freeze(result);
|
|
597
610
|
}
|
|
598
|
-
/**
|
|
599
|
-
|
|
611
|
+
/**
|
|
612
|
+
* Keeps a committed state's bytes, within the memory budget, every
|
|
613
|
+
* stride-th state and whenever replaying to it would take SLOW_REPLAY_MS.
|
|
614
|
+
*/
|
|
615
|
+
#keepCheckpoint(bytes, buildMs) {
|
|
600
616
|
const stride = Math.max(1, Math.floor(this.#host.limits.maxEditHistory / 4));
|
|
601
617
|
const stateId = this.#history.stateId;
|
|
602
618
|
// Entries dropped by a new change after an undo can never be restored;
|
|
@@ -605,11 +621,32 @@ export class EditSessionController {
|
|
|
605
621
|
for (const [id, kept] of this.#checkpoints)
|
|
606
622
|
if (!reachable.has(id) && !this.#pinned.has(id))
|
|
607
623
|
this.#forgetCheckpoint(id, kept);
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
624
|
+
for (const id of this.#buildMs.keys())
|
|
625
|
+
if (!reachable.has(id))
|
|
626
|
+
this.#buildMs.delete(id);
|
|
627
|
+
if (stateId === 0 || this.#checkpoints.has(stateId))
|
|
611
628
|
return;
|
|
612
|
-
this.#
|
|
629
|
+
this.#buildMs.set(stateId, buildMs);
|
|
630
|
+
if (stateId % stride === 0 || this.#replayMs() >= SLOW_REPLAY_MS)
|
|
631
|
+
this.#retain(stateId, bytes);
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* The engine time a restore of the current state would spend replaying:
|
|
635
|
+
* what its entries took since the newest retained bytes, or since the
|
|
636
|
+
* original or a restore entry's rebuild.
|
|
637
|
+
*/
|
|
638
|
+
#replayMs() {
|
|
639
|
+
const entries = this.#history.entriesAt(this.#history.position);
|
|
640
|
+
let total = 0;
|
|
641
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
642
|
+
const entry = entries[index];
|
|
643
|
+
if (this.#checkpoints.has(entry.stateId))
|
|
644
|
+
break;
|
|
645
|
+
total += this.#buildMs.get(entry.stateId) ?? 0;
|
|
646
|
+
if (entry.base)
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
return total;
|
|
613
650
|
}
|
|
614
651
|
/** Stores a state's bytes when the budget, less the pinned states, can hold them. */
|
|
615
652
|
#retain(stateId, bytes) {
|
|
@@ -9089,14 +9089,25 @@ function definedFields(style) {
|
|
|
9089
9089
|
Object.entries(style).filter(([, value]) => value !== void 0)
|
|
9090
9090
|
);
|
|
9091
9091
|
}
|
|
9092
|
-
function validateRect(rect, geometry, issue, path = "/rect") {
|
|
9092
|
+
function validateRect(rect, geometry, issue, path = "/rect", current) {
|
|
9093
9093
|
const size = displayedSize(geometry);
|
|
9094
9094
|
const tolerance = 0.01;
|
|
9095
|
-
|
|
9095
|
+
const left = Math.min(-tolerance, current?.x ?? 0);
|
|
9096
|
+
const top = Math.min(-tolerance, current?.y ?? 0);
|
|
9097
|
+
const right = Math.max(
|
|
9098
|
+
size.width + tolerance,
|
|
9099
|
+
current ? current.x + current.width : 0
|
|
9100
|
+
);
|
|
9101
|
+
const bottom = Math.max(
|
|
9102
|
+
size.height + tolerance,
|
|
9103
|
+
current ? current.y + current.height : 0
|
|
9104
|
+
);
|
|
9105
|
+
const missesPage = current !== void 0 && (rect.x >= size.width || rect.y >= size.height || rect.x + rect.width <= 0 || rect.y + rect.height <= 0);
|
|
9106
|
+
if (rect.x < left || rect.y < top || rect.x + rect.width > right || rect.y + rect.height > bottom || missesPage)
|
|
9096
9107
|
issue(
|
|
9097
9108
|
path,
|
|
9098
9109
|
"range",
|
|
9099
|
-
`The rectangle must lie within the ${size.width}\xD7${size.height} pt page`
|
|
9110
|
+
current ? `The rectangle must preserve or reduce existing overflow and remain partly visible within the ${size.width}\xD7${size.height} pt page` : `The rectangle must lie within the ${size.width}\xD7${size.height} pt page`
|
|
9100
9111
|
);
|
|
9101
9112
|
}
|
|
9102
9113
|
|
|
@@ -10297,7 +10308,8 @@ var moveElement = {
|
|
|
10297
10308
|
shifted(target.element.bounds, delta.dx, delta.dy),
|
|
10298
10309
|
context.geometry(target.location.pageIndex),
|
|
10299
10310
|
issue,
|
|
10300
|
-
operation2.to ? "/to" : "/by"
|
|
10311
|
+
operation2.to ? "/to" : "/by",
|
|
10312
|
+
target.element.bounds
|
|
10301
10313
|
);
|
|
10302
10314
|
},
|
|
10303
10315
|
apply(operation2, context) {
|
|
@@ -10350,7 +10362,9 @@ var resizeElement = {
|
|
|
10350
10362
|
validateRect(
|
|
10351
10363
|
operation2.rect,
|
|
10352
10364
|
context.geometry(target.location.pageIndex),
|
|
10353
|
-
issue
|
|
10365
|
+
issue,
|
|
10366
|
+
"/rect",
|
|
10367
|
+
target.element.bounds
|
|
10354
10368
|
);
|
|
10355
10369
|
},
|
|
10356
10370
|
apply(operation2, context) {
|
|
@@ -11442,6 +11456,11 @@ var CREATING_OPERATIONS = /* @__PURE__ */ new Set([
|
|
|
11442
11456
|
"insertImage",
|
|
11443
11457
|
"insertTable"
|
|
11444
11458
|
]);
|
|
11459
|
+
var PAGE_ORDER_OPERATIONS = /* @__PURE__ */ new Set([
|
|
11460
|
+
"insertPage",
|
|
11461
|
+
"deletePage",
|
|
11462
|
+
"movePage"
|
|
11463
|
+
]);
|
|
11445
11464
|
function referenceOf(operation2) {
|
|
11446
11465
|
const target = operation2.target;
|
|
11447
11466
|
return typeof target === "string" ? parseReference(target) : void 0;
|
|
@@ -11484,6 +11503,8 @@ var PdfEditDocument = class {
|
|
|
11484
11503
|
#batches = 0;
|
|
11485
11504
|
/** Browser faces of the document's fonts, by font program; see `textFont`. */
|
|
11486
11505
|
#faces = /* @__PURE__ */ new Map();
|
|
11506
|
+
/** Pages the running batch changed, by index; see `#batch`. */
|
|
11507
|
+
#written;
|
|
11487
11508
|
constructor(pdfium, original, fonts = new FontLibrary(async () => {
|
|
11488
11509
|
throw new Error("No font source is configured");
|
|
11489
11510
|
}), limits = defaultResourceLimits, assets = new AssetStore(), compact = compactPdf) {
|
|
@@ -11565,6 +11586,9 @@ var PdfEditDocument = class {
|
|
|
11565
11586
|
* the next batch number as their state id.
|
|
11566
11587
|
*/
|
|
11567
11588
|
apply(input) {
|
|
11589
|
+
return this.#batch(() => this.#apply(input));
|
|
11590
|
+
}
|
|
11591
|
+
#apply(input) {
|
|
11568
11592
|
const batch = Array.isArray(input) ? { stateId: this.#batches + 1, operations: input } : input;
|
|
11569
11593
|
const { stateId, operations } = batch;
|
|
11570
11594
|
const createdIds = [];
|
|
@@ -11580,6 +11604,7 @@ var PdfEditDocument = class {
|
|
|
11580
11604
|
details: { signatures: this.#signatures, features: this.#features }
|
|
11581
11605
|
});
|
|
11582
11606
|
operations.forEach((raw, operationIndex) => {
|
|
11607
|
+
if (PAGE_ORDER_OPERATIONS.has(raw.op)) this.#writeBack();
|
|
11583
11608
|
const handler = handlers[raw.op];
|
|
11584
11609
|
if (!handler)
|
|
11585
11610
|
throw new ViewerError("internal", `Unknown pdf operation ${raw.op}`);
|
|
@@ -12051,30 +12076,73 @@ var PdfEditDocument = class {
|
|
|
12051
12076
|
record: { ...first, id, mark: paragraph.spec }
|
|
12052
12077
|
} : void 0;
|
|
12053
12078
|
}
|
|
12054
|
-
/**
|
|
12079
|
+
/**
|
|
12080
|
+
* Loads a page and lets `use` change it. The page stays loaded until the
|
|
12081
|
+
* batch ends, so what the batch reads next sees the change; see `#batch`.
|
|
12082
|
+
*/
|
|
12055
12083
|
#writePage(pageIndex, use) {
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
12060
|
-
|
|
12061
|
-
|
|
12062
|
-
try {
|
|
12063
|
-
this.#objectsOf(pageIndex, page);
|
|
12064
|
-
const result3 = use(page);
|
|
12065
|
-
if (!lib.FPDFPage_GenerateContent(page))
|
|
12084
|
+
if (!this.#written)
|
|
12085
|
+
return this.#batch(() => this.#writePage(pageIndex, use));
|
|
12086
|
+
let page = this.#written.get(pageIndex);
|
|
12087
|
+
if (page === void 0) {
|
|
12088
|
+
page = this.#pdfium.lib.FPDF_LoadPage(this.#document.handle, pageIndex);
|
|
12089
|
+
if (!page)
|
|
12066
12090
|
throw new ViewerError(
|
|
12067
|
-
"
|
|
12068
|
-
"PDFium could not
|
|
12069
|
-
{
|
|
12070
|
-
details: { stage: "apply", pageIndex }
|
|
12071
|
-
}
|
|
12091
|
+
"render-failed",
|
|
12092
|
+
"PDFium could not load the page",
|
|
12093
|
+
{ details: { pageIndex } }
|
|
12072
12094
|
);
|
|
12073
|
-
|
|
12074
|
-
|
|
12095
|
+
this.#written.set(pageIndex, page);
|
|
12096
|
+
}
|
|
12097
|
+
this.#objectsOf(pageIndex, page);
|
|
12098
|
+
const result3 = use(page);
|
|
12099
|
+
delete this.#pages[pageIndex].elements;
|
|
12100
|
+
delete this.#pages[pageIndex].paragraphs;
|
|
12101
|
+
return result3;
|
|
12102
|
+
}
|
|
12103
|
+
/**
|
|
12104
|
+
* Runs `run` with the pages it changes kept loaded, then regenerates each
|
|
12105
|
+
* page's content stream once. PDFium rewrites a whole stream however
|
|
12106
|
+
* little changed, which takes seconds on a page of thousands of objects,
|
|
12107
|
+
* and object reads (text, bounds) need no regenerated content. A failing
|
|
12108
|
+
* batch closes its pages unwritten: the caller restores the working copy.
|
|
12109
|
+
*/
|
|
12110
|
+
#batch(run2) {
|
|
12111
|
+
if (this.#written) return run2();
|
|
12112
|
+
const written = /* @__PURE__ */ new Map();
|
|
12113
|
+
this.#written = written;
|
|
12114
|
+
try {
|
|
12115
|
+
const result3 = run2();
|
|
12116
|
+
this.#writeBack();
|
|
12075
12117
|
return result3;
|
|
12076
12118
|
} finally {
|
|
12077
|
-
|
|
12119
|
+
this.#written = void 0;
|
|
12120
|
+
for (const [pageIndex, page] of written) {
|
|
12121
|
+
this.#pdfium.lib.FPDF_ClosePage(page);
|
|
12122
|
+
delete this.#pages[pageIndex]?.elements;
|
|
12123
|
+
delete this.#pages[pageIndex]?.paragraphs;
|
|
12124
|
+
}
|
|
12125
|
+
}
|
|
12126
|
+
}
|
|
12127
|
+
/** Regenerates and closes the pages the running batch changed. */
|
|
12128
|
+
#writeBack() {
|
|
12129
|
+
const written = this.#written;
|
|
12130
|
+
if (!written) return;
|
|
12131
|
+
const { lib } = this.#pdfium;
|
|
12132
|
+
for (const [pageIndex, page] of written) {
|
|
12133
|
+
written.delete(pageIndex);
|
|
12134
|
+
try {
|
|
12135
|
+
if (!lib.FPDFPage_GenerateContent(page))
|
|
12136
|
+
throw new ViewerError(
|
|
12137
|
+
"edit-failed",
|
|
12138
|
+
"PDFium could not rewrite the page",
|
|
12139
|
+
{ details: { stage: "apply", pageIndex } }
|
|
12140
|
+
);
|
|
12141
|
+
} finally {
|
|
12142
|
+
lib.FPDF_ClosePage(page);
|
|
12143
|
+
}
|
|
12144
|
+
delete this.#pages[pageIndex].elements;
|
|
12145
|
+
delete this.#pages[pageIndex].paragraphs;
|
|
12078
12146
|
}
|
|
12079
12147
|
}
|
|
12080
12148
|
#originalPages() {
|
|
@@ -12282,7 +12350,8 @@ var PdfEditDocument = class {
|
|
|
12282
12350
|
}
|
|
12283
12351
|
#withPage(pageIndex, use) {
|
|
12284
12352
|
const { lib } = this.#pdfium;
|
|
12285
|
-
const
|
|
12353
|
+
const written = this.#written?.get(pageIndex);
|
|
12354
|
+
const page = written ?? lib.FPDF_LoadPage(this.#document.handle, pageIndex);
|
|
12286
12355
|
if (!page)
|
|
12287
12356
|
throw new ViewerError("render-failed", "PDFium could not load the page", {
|
|
12288
12357
|
details: { pageIndex }
|
|
@@ -12295,7 +12364,7 @@ var PdfEditDocument = class {
|
|
|
12295
12364
|
lib.FPDFText_ClosePage(textPage);
|
|
12296
12365
|
}
|
|
12297
12366
|
} finally {
|
|
12298
|
-
lib.FPDF_ClosePage(page);
|
|
12367
|
+
if (written === void 0) lib.FPDF_ClosePage(page);
|
|
12299
12368
|
}
|
|
12300
12369
|
}
|
|
12301
12370
|
*#matches(textPage, query, options) {
|