web-doc 0.12.1 → 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.d.ts +3 -3
- package/dist/edit/pdf/engine/document.js +109 -36
- package/dist/edit/pdf/engine/layout.d.ts +2 -0
- package/dist/edit/pdf/engine/layout.js +26 -3
- 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/pdf/types.d.ts +23 -1
- package/dist/edit/session.js +48 -11
- package/dist/workers/pdf-edit-worker.js +152 -50
- package/package.json +1 -1
|
@@ -36,9 +36,9 @@ export declare class PdfEditDocument {
|
|
|
36
36
|
get pageCount(): number;
|
|
37
37
|
get pdfium(): Pdfium;
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* Preflights a batch against its starting document. Applying revalidates
|
|
40
|
+
* each operation against earlier changes in the batch; created-element
|
|
41
|
+
* references are checked once their targets exist.
|
|
42
42
|
*/
|
|
43
43
|
validate(operations: readonly PdfOrUnknownOperation[]): OperationIssue[];
|
|
44
44
|
/**
|
|
@@ -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) {
|
|
@@ -167,9 +175,9 @@ export class PdfEditDocument {
|
|
|
167
175
|
return this.#pdfium;
|
|
168
176
|
}
|
|
169
177
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
178
|
+
* Preflights a batch against its starting document. Applying revalidates
|
|
179
|
+
* each operation against earlier changes in the batch; created-element
|
|
180
|
+
* references are checked once their targets exist.
|
|
173
181
|
*/
|
|
174
182
|
validate(operations) {
|
|
175
183
|
const issues = [];
|
|
@@ -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,11 +236,13 @@ 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}`);
|
|
231
244
|
const context = this.#context(stateId, operationIndex);
|
|
232
|
-
const operation = this.#
|
|
245
|
+
const operation = this.#resolveAndValidate(raw, operationIndex, createdByOperation, handler, context);
|
|
233
246
|
const result = handler.apply(operation, context);
|
|
234
247
|
createdIds.push(...result.createdIds);
|
|
235
248
|
createdByOperation[operationIndex] = [...result.createdIds];
|
|
@@ -251,23 +264,23 @@ export class PdfEditDocument {
|
|
|
251
264
|
};
|
|
252
265
|
}
|
|
253
266
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* validation would have.
|
|
267
|
+
* Resolves a `"$<n>"` target, then checks every operation against the
|
|
268
|
+
* current document. Earlier operations may have moved, resized or removed
|
|
269
|
+
* an ordinary target since the batch's initial preflight.
|
|
258
270
|
*/
|
|
259
|
-
#
|
|
271
|
+
#resolveAndValidate(operation, operationIndex, createdByOperation, handler, context) {
|
|
260
272
|
const reference = referenceOf(operation);
|
|
261
|
-
if (reference === undefined)
|
|
262
|
-
return operation;
|
|
263
273
|
const issues = [];
|
|
264
274
|
const issue = issueCollector(operationIndex, issues);
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
275
|
+
let resolved = operation;
|
|
276
|
+
if (reference !== undefined && "target" in operation) {
|
|
277
|
+
const id = createdByOperation[reference]?.[0];
|
|
278
|
+
if (id === undefined) {
|
|
279
|
+
issue("/target", "unknown-target", `Operation ${reference} created no element for "$${reference}"`);
|
|
280
|
+
throw invalidOperationError(issues);
|
|
281
|
+
}
|
|
282
|
+
resolved = { ...operation, target: id };
|
|
269
283
|
}
|
|
270
|
-
const resolved = { ...operation, target: id };
|
|
271
284
|
handler.validate(resolved, context, issue);
|
|
272
285
|
if (issues.length > 0)
|
|
273
286
|
throw invalidOperationError(issues);
|
|
@@ -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() {
|
|
@@ -888,12 +944,28 @@ export class PdfEditDocument {
|
|
|
888
944
|
const records = this.#objectsOf(pageIndex, page);
|
|
889
945
|
const { byObject, elements } = scanPage(this.#pdfium, page, textPage, pageIndex, geometry, records);
|
|
890
946
|
const offsets = this.#charOffsets(page, textPage, byObject, new Map(elements.map((element) => [element.id, element.text ?? ""])));
|
|
891
|
-
|
|
947
|
+
// A text box lays its lines out in the rect its mark keeps; only
|
|
948
|
+
// fresh marks are still on the records.
|
|
949
|
+
const frames = new Map();
|
|
950
|
+
for (const record of records)
|
|
951
|
+
if (record.mark?.kind === "textBox" && !frames.has(record.id))
|
|
952
|
+
frames.set(record.id, roundRect(record.mark.rect));
|
|
953
|
+
return use({
|
|
954
|
+
page,
|
|
955
|
+
textPage,
|
|
956
|
+
geometry,
|
|
957
|
+
byObject,
|
|
958
|
+
elements,
|
|
959
|
+
offsets,
|
|
960
|
+
frames,
|
|
961
|
+
});
|
|
892
962
|
});
|
|
893
963
|
}
|
|
894
964
|
#withPage(pageIndex, use) {
|
|
895
965
|
const { lib } = this.#pdfium;
|
|
896
|
-
|
|
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);
|
|
897
969
|
if (!page)
|
|
898
970
|
throw new ViewerError("render-failed", "PDFium could not load the page", {
|
|
899
971
|
details: { pageIndex },
|
|
@@ -908,7 +980,8 @@ export class PdfEditDocument {
|
|
|
908
980
|
}
|
|
909
981
|
}
|
|
910
982
|
finally {
|
|
911
|
-
|
|
983
|
+
if (written === undefined)
|
|
984
|
+
lib.FPDF_ClosePage(page);
|
|
912
985
|
}
|
|
913
986
|
}
|
|
914
987
|
*#matches(textPage, query, options) {
|
|
@@ -12,6 +12,8 @@ export interface TextPageScan {
|
|
|
12
12
|
readonly elements: readonly PdfElement[];
|
|
13
13
|
/** Per character of the text page: its element and offset, when an object draws it. */
|
|
14
14
|
readonly offsets: readonly (TextPosition | undefined)[];
|
|
15
|
+
/** Element id → the rectangle its text is laid out in, for text boxes. */
|
|
16
|
+
readonly frames?: ReadonlyMap<string, PageRect>;
|
|
15
17
|
}
|
|
16
18
|
/** The layout of one element, or `undefined` when it draws no text. */
|
|
17
19
|
export declare function layoutOf(pdfium: Pdfium, scan: TextPageScan, element: PdfElement): TextLayout | undefined;
|
|
@@ -8,10 +8,12 @@ export function layoutOf(pdfium, scan, element) {
|
|
|
8
8
|
if (!TEXT_KINDS.has(element.kind))
|
|
9
9
|
return undefined;
|
|
10
10
|
const glyphs = glyphsOf(pdfium, scan, (position) => position.elementId === element.id);
|
|
11
|
+
const lines = linesOf(pdfium, scan, glyphs);
|
|
11
12
|
return {
|
|
12
13
|
elementId: element.id,
|
|
13
14
|
pageIndex: element.pageIndex,
|
|
14
|
-
|
|
15
|
+
frame: frameOf(scan, element, lines),
|
|
16
|
+
lines,
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
/** The layouts of every text element of the page, in reading order, from one pass. */
|
|
@@ -30,14 +32,30 @@ export function layoutsOf(pdfium, scan) {
|
|
|
30
32
|
const own = byElement.get(element.id);
|
|
31
33
|
if (!own || !TEXT_KINDS.has(element.kind))
|
|
32
34
|
continue;
|
|
35
|
+
const lines = linesOf(pdfium, scan, own);
|
|
33
36
|
layouts.push({
|
|
34
37
|
elementId: element.id,
|
|
35
38
|
pageIndex: element.pageIndex,
|
|
36
|
-
|
|
39
|
+
frame: frameOf(scan, element, lines),
|
|
40
|
+
lines,
|
|
37
41
|
});
|
|
38
42
|
}
|
|
39
43
|
return layouts;
|
|
40
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The box an element's text is laid out in: a paragraph's frame (its bounds
|
|
47
|
+
* already start at the pen and span its widest advance), a text box's own
|
|
48
|
+
* rect, else the union of the lines' advance boxes.
|
|
49
|
+
*/
|
|
50
|
+
function frameOf(scan, element, lines) {
|
|
51
|
+
if (element.kind === "paragraph")
|
|
52
|
+
return element.bounds;
|
|
53
|
+
const own = scan.frames?.get(element.id);
|
|
54
|
+
if (own)
|
|
55
|
+
return own;
|
|
56
|
+
const boxes = lines.flatMap((line) => line.advanceBounds ? [line.advanceBounds] : []);
|
|
57
|
+
return boxes.length > 0 ? roundRect(unionRects(boxes)) : element.bounds;
|
|
58
|
+
}
|
|
41
59
|
/** The caret position nearest to a page-space point, or none without text. */
|
|
42
60
|
export function positionIn(pdfium, scan, point) {
|
|
43
61
|
const glyphs = glyphsOf(pdfium, scan, () => true);
|
|
@@ -105,6 +123,7 @@ function glyphsOf(pdfium, scan, keep) {
|
|
|
105
123
|
position,
|
|
106
124
|
char: String.fromCodePoint(lib.FPDFText_GetUnicode(textPage, index)),
|
|
107
125
|
box: roundRect(userRectToPage(geometry, left, bottom, right, top)),
|
|
126
|
+
loose: roundRect(userRectToPage(geometry, looseLeft, looseBottom, looseRight, looseTop)),
|
|
108
127
|
advance: round(Math.abs(looseRight - looseLeft)),
|
|
109
128
|
origin: roundPoint(userToPage(geometry, origin[0], origin[1])),
|
|
110
129
|
});
|
|
@@ -136,11 +155,15 @@ function linesOf(pdfium, scan, glyphs) {
|
|
|
136
155
|
},
|
|
137
156
|
text: members.map((glyph) => glyph.char).join(""),
|
|
138
157
|
bounds: roundRect(unionRects(members.map((glyph) => glyph.box))),
|
|
158
|
+
// PDFium's loose boxes run from each origin to origin plus advance,
|
|
159
|
+
// ascent to descent: their union is the line a text field must hold.
|
|
160
|
+
advanceBounds: roundRect(unionRects(members.map((glyph) => glyph.loose))),
|
|
139
161
|
baseline: members[0].origin,
|
|
140
|
-
glyphs: members.map(({ position, box, advance }) => ({
|
|
162
|
+
glyphs: members.map(({ position, box, advance, origin }) => ({
|
|
141
163
|
offset: position.offset,
|
|
142
164
|
box,
|
|
143
165
|
advance,
|
|
166
|
+
origin,
|
|
144
167
|
})),
|
|
145
168
|
fontFamily: style.fontFamily,
|
|
146
169
|
fontSize: style.fontSize,
|
|
@@ -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/pdf/types.d.ts
CHANGED
|
@@ -255,6 +255,12 @@ export interface TextLayoutGlyph {
|
|
|
255
255
|
readonly box: PageRect;
|
|
256
256
|
/** Advance width along the baseline, in points. */
|
|
257
257
|
readonly advance: number;
|
|
258
|
+
/**
|
|
259
|
+
* Pen position on the baseline where the glyph starts, in page space. The
|
|
260
|
+
* steps between neighbours are the advances the file draws with, character
|
|
261
|
+
* and word spacing and TJ kerning included.
|
|
262
|
+
*/
|
|
263
|
+
readonly origin?: PagePoint;
|
|
258
264
|
}
|
|
259
265
|
/** One line of a text element: one PDFium text object, as the file stores it. */
|
|
260
266
|
export interface TextLayoutLine {
|
|
@@ -263,6 +269,15 @@ export interface TextLayoutLine {
|
|
|
263
269
|
readonly text: string;
|
|
264
270
|
/** Union of the glyph boxes, in page space. */
|
|
265
271
|
readonly bounds: PageRect;
|
|
272
|
+
/**
|
|
273
|
+
* The box the line's advances fill, the size a text field needs to hold the
|
|
274
|
+
* line unwrapped: from the first glyph's origin to the last glyph's origin
|
|
275
|
+
* plus its advance, and from the font's ascent to its descent at the line's
|
|
276
|
+
* size. It differs from `bounds` by the glyphs' side bearings, usually
|
|
277
|
+
* wider; in page space and axis-aligned like `bounds`, so it turns with the
|
|
278
|
+
* text.
|
|
279
|
+
*/
|
|
280
|
+
readonly advanceBounds?: PageRect;
|
|
266
281
|
/** Start of the baseline, in page space. */
|
|
267
282
|
readonly baseline: PagePoint;
|
|
268
283
|
readonly glyphs: readonly TextLayoutGlyph[];
|
|
@@ -305,10 +320,17 @@ export interface TextFont {
|
|
|
305
320
|
*/
|
|
306
321
|
readonly missing?: "not-embedded" | "cid-keyed" | "type1" | "no-unicode" | "unreadable";
|
|
307
322
|
}
|
|
308
|
-
/** The drawn geometry of a `text`, `textBox` or `table` element. */
|
|
323
|
+
/** The drawn geometry of a `text`, `textBox`, `paragraph` or `table` element. */
|
|
309
324
|
export interface TextLayout {
|
|
310
325
|
readonly elementId: string;
|
|
311
326
|
readonly pageIndex: number;
|
|
327
|
+
/**
|
|
328
|
+
* The box the element's text is laid out in, in page space: a text box's or
|
|
329
|
+
* a paragraph's own rectangle, otherwise the union of its lines'
|
|
330
|
+
* `advanceBounds`. Place an inline text field here rather than on the ink
|
|
331
|
+
* `bounds`, which a browser's advance-based layout outgrows.
|
|
332
|
+
*/
|
|
333
|
+
readonly frame?: PageRect;
|
|
312
334
|
/** Lines in reading order: a text box's lines, a table's cells. */
|
|
313
335
|
readonly lines: readonly TextLayoutLine[];
|
|
314
336
|
}
|
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) {
|
|
@@ -6759,10 +6759,12 @@ function layoutOf(pdfium, scan, element) {
|
|
|
6759
6759
|
scan,
|
|
6760
6760
|
(position) => position.elementId === element.id
|
|
6761
6761
|
);
|
|
6762
|
+
const lines = linesOf(pdfium, scan, glyphs);
|
|
6762
6763
|
return {
|
|
6763
6764
|
elementId: element.id,
|
|
6764
6765
|
pageIndex: element.pageIndex,
|
|
6765
|
-
|
|
6766
|
+
frame: frameOf(scan, element, lines),
|
|
6767
|
+
lines
|
|
6766
6768
|
};
|
|
6767
6769
|
}
|
|
6768
6770
|
function layoutsOf(pdfium, scan) {
|
|
@@ -6777,14 +6779,25 @@ function layoutsOf(pdfium, scan) {
|
|
|
6777
6779
|
for (const element of scan.elements) {
|
|
6778
6780
|
const own = byElement.get(element.id);
|
|
6779
6781
|
if (!own || !TEXT_KINDS.has(element.kind)) continue;
|
|
6782
|
+
const lines = linesOf(pdfium, scan, own);
|
|
6780
6783
|
layouts.push({
|
|
6781
6784
|
elementId: element.id,
|
|
6782
6785
|
pageIndex: element.pageIndex,
|
|
6783
|
-
|
|
6786
|
+
frame: frameOf(scan, element, lines),
|
|
6787
|
+
lines
|
|
6784
6788
|
});
|
|
6785
6789
|
}
|
|
6786
6790
|
return layouts;
|
|
6787
6791
|
}
|
|
6792
|
+
function frameOf(scan, element, lines) {
|
|
6793
|
+
if (element.kind === "paragraph") return element.bounds;
|
|
6794
|
+
const own = scan.frames?.get(element.id);
|
|
6795
|
+
if (own) return own;
|
|
6796
|
+
const boxes = lines.flatMap(
|
|
6797
|
+
(line) => line.advanceBounds ? [line.advanceBounds] : []
|
|
6798
|
+
);
|
|
6799
|
+
return boxes.length > 0 ? roundRect(unionRects(boxes)) : element.bounds;
|
|
6800
|
+
}
|
|
6788
6801
|
function positionIn(pdfium, scan, point) {
|
|
6789
6802
|
const glyphs = glyphsOf(pdfium, scan, () => true);
|
|
6790
6803
|
if (glyphs.length === 0) return void 0;
|
|
@@ -6859,6 +6872,9 @@ function glyphsOf(pdfium, scan, keep) {
|
|
|
6859
6872
|
position,
|
|
6860
6873
|
char: String.fromCodePoint(lib.FPDFText_GetUnicode(textPage, index)),
|
|
6861
6874
|
box: roundRect(userRectToPage(geometry, left, bottom, right, top)),
|
|
6875
|
+
loose: roundRect(
|
|
6876
|
+
userRectToPage(geometry, looseLeft, looseBottom, looseRight, looseTop)
|
|
6877
|
+
),
|
|
6862
6878
|
advance: round(Math.abs(looseRight - looseLeft)),
|
|
6863
6879
|
origin: roundPoint(userToPage(geometry, origin[0], origin[1]))
|
|
6864
6880
|
});
|
|
@@ -6886,11 +6902,15 @@ function linesOf(pdfium, scan, glyphs) {
|
|
|
6886
6902
|
},
|
|
6887
6903
|
text: members.map((glyph) => glyph.char).join(""),
|
|
6888
6904
|
bounds: roundRect(unionRects(members.map((glyph) => glyph.box))),
|
|
6905
|
+
// PDFium's loose boxes run from each origin to origin plus advance,
|
|
6906
|
+
// ascent to descent: their union is the line a text field must hold.
|
|
6907
|
+
advanceBounds: roundRect(unionRects(members.map((glyph) => glyph.loose))),
|
|
6889
6908
|
baseline: members[0].origin,
|
|
6890
|
-
glyphs: members.map(({ position, box, advance: advance2 }) => ({
|
|
6909
|
+
glyphs: members.map(({ position, box, advance: advance2, origin }) => ({
|
|
6891
6910
|
offset: position.offset,
|
|
6892
6911
|
box,
|
|
6893
|
-
advance: advance2
|
|
6912
|
+
advance: advance2,
|
|
6913
|
+
origin
|
|
6894
6914
|
})),
|
|
6895
6915
|
fontFamily: style.fontFamily,
|
|
6896
6916
|
fontSize: style.fontSize,
|
|
@@ -9069,14 +9089,25 @@ function definedFields(style) {
|
|
|
9069
9089
|
Object.entries(style).filter(([, value]) => value !== void 0)
|
|
9070
9090
|
);
|
|
9071
9091
|
}
|
|
9072
|
-
function validateRect(rect, geometry, issue, path = "/rect") {
|
|
9092
|
+
function validateRect(rect, geometry, issue, path = "/rect", current) {
|
|
9073
9093
|
const size = displayedSize(geometry);
|
|
9074
9094
|
const tolerance = 0.01;
|
|
9075
|
-
|
|
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)
|
|
9076
9107
|
issue(
|
|
9077
9108
|
path,
|
|
9078
9109
|
"range",
|
|
9079
|
-
`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`
|
|
9080
9111
|
);
|
|
9081
9112
|
}
|
|
9082
9113
|
|
|
@@ -10277,7 +10308,8 @@ var moveElement = {
|
|
|
10277
10308
|
shifted(target.element.bounds, delta.dx, delta.dy),
|
|
10278
10309
|
context.geometry(target.location.pageIndex),
|
|
10279
10310
|
issue,
|
|
10280
|
-
operation2.to ? "/to" : "/by"
|
|
10311
|
+
operation2.to ? "/to" : "/by",
|
|
10312
|
+
target.element.bounds
|
|
10281
10313
|
);
|
|
10282
10314
|
},
|
|
10283
10315
|
apply(operation2, context) {
|
|
@@ -10330,7 +10362,9 @@ var resizeElement = {
|
|
|
10330
10362
|
validateRect(
|
|
10331
10363
|
operation2.rect,
|
|
10332
10364
|
context.geometry(target.location.pageIndex),
|
|
10333
|
-
issue
|
|
10365
|
+
issue,
|
|
10366
|
+
"/rect",
|
|
10367
|
+
target.element.bounds
|
|
10334
10368
|
);
|
|
10335
10369
|
},
|
|
10336
10370
|
apply(operation2, context) {
|
|
@@ -11422,6 +11456,11 @@ var CREATING_OPERATIONS = /* @__PURE__ */ new Set([
|
|
|
11422
11456
|
"insertImage",
|
|
11423
11457
|
"insertTable"
|
|
11424
11458
|
]);
|
|
11459
|
+
var PAGE_ORDER_OPERATIONS = /* @__PURE__ */ new Set([
|
|
11460
|
+
"insertPage",
|
|
11461
|
+
"deletePage",
|
|
11462
|
+
"movePage"
|
|
11463
|
+
]);
|
|
11425
11464
|
function referenceOf(operation2) {
|
|
11426
11465
|
const target = operation2.target;
|
|
11427
11466
|
return typeof target === "string" ? parseReference(target) : void 0;
|
|
@@ -11464,6 +11503,8 @@ var PdfEditDocument = class {
|
|
|
11464
11503
|
#batches = 0;
|
|
11465
11504
|
/** Browser faces of the document's fonts, by font program; see `textFont`. */
|
|
11466
11505
|
#faces = /* @__PURE__ */ new Map();
|
|
11506
|
+
/** Pages the running batch changed, by index; see `#batch`. */
|
|
11507
|
+
#written;
|
|
11467
11508
|
constructor(pdfium, original, fonts = new FontLibrary(async () => {
|
|
11468
11509
|
throw new Error("No font source is configured");
|
|
11469
11510
|
}), limits = defaultResourceLimits, assets = new AssetStore(), compact = compactPdf) {
|
|
@@ -11506,9 +11547,9 @@ var PdfEditDocument = class {
|
|
|
11506
11547
|
return this.#pdfium;
|
|
11507
11548
|
}
|
|
11508
11549
|
/**
|
|
11509
|
-
*
|
|
11510
|
-
*
|
|
11511
|
-
*
|
|
11550
|
+
* Preflights a batch against its starting document. Applying revalidates
|
|
11551
|
+
* each operation against earlier changes in the batch; created-element
|
|
11552
|
+
* references are checked once their targets exist.
|
|
11512
11553
|
*/
|
|
11513
11554
|
validate(operations) {
|
|
11514
11555
|
const issues = [];
|
|
@@ -11545,6 +11586,9 @@ var PdfEditDocument = class {
|
|
|
11545
11586
|
* the next batch number as their state id.
|
|
11546
11587
|
*/
|
|
11547
11588
|
apply(input) {
|
|
11589
|
+
return this.#batch(() => this.#apply(input));
|
|
11590
|
+
}
|
|
11591
|
+
#apply(input) {
|
|
11548
11592
|
const batch = Array.isArray(input) ? { stateId: this.#batches + 1, operations: input } : input;
|
|
11549
11593
|
const { stateId, operations } = batch;
|
|
11550
11594
|
const createdIds = [];
|
|
@@ -11560,11 +11604,12 @@ var PdfEditDocument = class {
|
|
|
11560
11604
|
details: { signatures: this.#signatures, features: this.#features }
|
|
11561
11605
|
});
|
|
11562
11606
|
operations.forEach((raw, operationIndex) => {
|
|
11607
|
+
if (PAGE_ORDER_OPERATIONS.has(raw.op)) this.#writeBack();
|
|
11563
11608
|
const handler = handlers[raw.op];
|
|
11564
11609
|
if (!handler)
|
|
11565
11610
|
throw new ViewerError("internal", `Unknown pdf operation ${raw.op}`);
|
|
11566
11611
|
const context = this.#context(stateId, operationIndex);
|
|
11567
|
-
const operation2 = this.#
|
|
11612
|
+
const operation2 = this.#resolveAndValidate(
|
|
11568
11613
|
raw,
|
|
11569
11614
|
operationIndex,
|
|
11570
11615
|
createdByOperation,
|
|
@@ -11591,26 +11636,27 @@ var PdfEditDocument = class {
|
|
|
11591
11636
|
};
|
|
11592
11637
|
}
|
|
11593
11638
|
/**
|
|
11594
|
-
*
|
|
11595
|
-
*
|
|
11596
|
-
*
|
|
11597
|
-
* validation would have.
|
|
11639
|
+
* Resolves a `"$<n>"` target, then checks every operation against the
|
|
11640
|
+
* current document. Earlier operations may have moved, resized or removed
|
|
11641
|
+
* an ordinary target since the batch's initial preflight.
|
|
11598
11642
|
*/
|
|
11599
|
-
#
|
|
11643
|
+
#resolveAndValidate(operation2, operationIndex, createdByOperation, handler, context) {
|
|
11600
11644
|
const reference = referenceOf(operation2);
|
|
11601
|
-
if (reference === void 0) return operation2;
|
|
11602
11645
|
const issues = [];
|
|
11603
11646
|
const issue = issueCollector(operationIndex, issues);
|
|
11604
|
-
|
|
11605
|
-
if (
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
11611
|
-
|
|
11647
|
+
let resolved = operation2;
|
|
11648
|
+
if (reference !== void 0 && "target" in operation2) {
|
|
11649
|
+
const id = createdByOperation[reference]?.[0];
|
|
11650
|
+
if (id === void 0) {
|
|
11651
|
+
issue(
|
|
11652
|
+
"/target",
|
|
11653
|
+
"unknown-target",
|
|
11654
|
+
`Operation ${reference} created no element for "$${reference}"`
|
|
11655
|
+
);
|
|
11656
|
+
throw invalidOperationError(issues);
|
|
11657
|
+
}
|
|
11658
|
+
resolved = { ...operation2, target: id };
|
|
11612
11659
|
}
|
|
11613
|
-
const resolved = { ...operation2, target: id };
|
|
11614
11660
|
handler.validate(resolved, context, issue);
|
|
11615
11661
|
if (issues.length > 0) throw invalidOperationError(issues);
|
|
11616
11662
|
return resolved;
|
|
@@ -12030,30 +12076,73 @@ var PdfEditDocument = class {
|
|
|
12030
12076
|
record: { ...first, id, mark: paragraph.spec }
|
|
12031
12077
|
} : void 0;
|
|
12032
12078
|
}
|
|
12033
|
-
/**
|
|
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
|
+
*/
|
|
12034
12083
|
#writePage(pageIndex, use) {
|
|
12035
|
-
|
|
12036
|
-
|
|
12037
|
-
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
|
|
12041
|
-
try {
|
|
12042
|
-
this.#objectsOf(pageIndex, page);
|
|
12043
|
-
const result3 = use(page);
|
|
12044
|
-
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)
|
|
12045
12090
|
throw new ViewerError(
|
|
12046
|
-
"
|
|
12047
|
-
"PDFium could not
|
|
12048
|
-
{
|
|
12049
|
-
details: { stage: "apply", pageIndex }
|
|
12050
|
-
}
|
|
12091
|
+
"render-failed",
|
|
12092
|
+
"PDFium could not load the page",
|
|
12093
|
+
{ details: { pageIndex } }
|
|
12051
12094
|
);
|
|
12052
|
-
|
|
12053
|
-
|
|
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();
|
|
12054
12117
|
return result3;
|
|
12055
12118
|
} finally {
|
|
12056
|
-
|
|
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;
|
|
12057
12146
|
}
|
|
12058
12147
|
}
|
|
12059
12148
|
#originalPages() {
|
|
@@ -12244,12 +12333,25 @@ var PdfEditDocument = class {
|
|
|
12244
12333
|
byObject,
|
|
12245
12334
|
new Map(elements.map((element) => [element.id, element.text ?? ""]))
|
|
12246
12335
|
);
|
|
12247
|
-
|
|
12336
|
+
const frames = /* @__PURE__ */ new Map();
|
|
12337
|
+
for (const record of records)
|
|
12338
|
+
if (record.mark?.kind === "textBox" && !frames.has(record.id))
|
|
12339
|
+
frames.set(record.id, roundRect(record.mark.rect));
|
|
12340
|
+
return use({
|
|
12341
|
+
page,
|
|
12342
|
+
textPage,
|
|
12343
|
+
geometry,
|
|
12344
|
+
byObject,
|
|
12345
|
+
elements,
|
|
12346
|
+
offsets,
|
|
12347
|
+
frames
|
|
12348
|
+
});
|
|
12248
12349
|
});
|
|
12249
12350
|
}
|
|
12250
12351
|
#withPage(pageIndex, use) {
|
|
12251
12352
|
const { lib } = this.#pdfium;
|
|
12252
|
-
const
|
|
12353
|
+
const written = this.#written?.get(pageIndex);
|
|
12354
|
+
const page = written ?? lib.FPDF_LoadPage(this.#document.handle, pageIndex);
|
|
12253
12355
|
if (!page)
|
|
12254
12356
|
throw new ViewerError("render-failed", "PDFium could not load the page", {
|
|
12255
12357
|
details: { pageIndex }
|
|
@@ -12262,7 +12364,7 @@ var PdfEditDocument = class {
|
|
|
12262
12364
|
lib.FPDFText_ClosePage(textPage);
|
|
12263
12365
|
}
|
|
12264
12366
|
} finally {
|
|
12265
|
-
lib.FPDF_ClosePage(page);
|
|
12367
|
+
if (written === void 0) lib.FPDF_ClosePage(page);
|
|
12266
12368
|
}
|
|
12267
12369
|
}
|
|
12268
12370
|
*#matches(textPage, query, options) {
|