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.
@@ -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
- /** Loads a page, lets `use` change it, regenerates its content stream. */
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
- const { lib } = this.#pdfium;
699
- const page = lib.FPDF_LoadPage(this.#document.handle, pageIndex);
700
- if (!page)
701
- throw new ViewerError("render-failed", "PDFium could not load the page", {
702
- details: { pageIndex },
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
- // Objects are assigned before the change so appended ones follow them.
706
- this.#objectsOf(pageIndex, page);
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
- lib.FPDF_ClosePage(page);
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
- const page = lib.FPDF_LoadPage(this.#document.handle, pageIndex);
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
- lib.FPDF_ClosePage(page);
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
- if (rect.x < -tolerance ||
326
- rect.y < -tolerance ||
327
- rect.x + rect.width > size.width + tolerance ||
328
- rect.y + rect.height > size.height + tolerance)
329
- issue(path, "range", `The rectangle must lie within the ${size.width}×${size.height} pt page`);
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);
@@ -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
- #commit(reason, changedPages, shown) {
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
- /** Keeps every stride-th committed state's bytes within the memory budget. */
599
- #keepCheckpoint(bytes) {
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
- if (stateId === 0 ||
609
- this.#checkpoints.has(stateId) ||
610
- stateId % stride !== 0)
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.#retain(stateId, bytes);
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
- if (rect.x < -tolerance || rect.y < -tolerance || rect.x + rect.width > size.width + tolerance || rect.y + rect.height > size.height + tolerance)
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
- /** Loads a page, lets `use` change it, regenerates its content stream. */
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
- const { lib } = this.#pdfium;
12057
- const page = lib.FPDF_LoadPage(this.#document.handle, pageIndex);
12058
- if (!page)
12059
- throw new ViewerError("render-failed", "PDFium could not load the page", {
12060
- details: { pageIndex }
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
- "edit-failed",
12068
- "PDFium could not rewrite the page",
12069
- {
12070
- details: { stage: "apply", pageIndex }
12071
- }
12091
+ "render-failed",
12092
+ "PDFium could not load the page",
12093
+ { details: { pageIndex } }
12072
12094
  );
12073
- delete this.#pages[pageIndex].elements;
12074
- delete this.#pages[pageIndex].paragraphs;
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
- lib.FPDF_ClosePage(page);
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 page = lib.FPDF_LoadPage(this.#document.handle, pageIndex);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-doc",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "web-doc — embeddable browser-only document viewer with Rust/WASM adapters (a fork of Zrimo)",
5
5
  "keywords": [
6
6
  "document-viewer",