web-doc 0.8.0 → 0.9.0

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.
@@ -580,7 +580,9 @@ function describeOperation(format, pages, operation, id) {
580
580
  case "moveSlide":
581
581
  return `Moved ${pages} ${Number(fields.from) + 1} to ${Number(fields.to) + 1}`;
582
582
  case "rotatePage":
583
- return `Rotated ${pages} ${Number(fields.pageIndex) + 1} to ${String(fields.rotation)}°`;
583
+ return fields.by === undefined
584
+ ? `Rotated ${pages} ${Number(fields.pageIndex) + 1} to ${String(fields.rotation)}°`
585
+ : `Turned ${pages} ${Number(fields.pageIndex) + 1} by ${String(fields.by)}°`;
584
586
  default:
585
587
  return `Applied ${operation.op}${target === "the element" ? "" : ` to ${target}`}${tag}`;
586
588
  }
@@ -81,10 +81,28 @@ function paragraphElement(model, record, base) {
81
81
  id: record.elementId,
82
82
  kind: "paragraph",
83
83
  text: record.text.text,
84
- ...(record.table ? { parentId: record.table.elementId } : {}),
84
+ ...(record.table
85
+ ? { parentId: record.table.elementId, ...cellOf(record.table, record) }
86
+ : {}),
85
87
  textStyle: resolveTextStyle(model.styles, record.pPr, first?.rPr),
86
88
  paragraphStyle: resolveParagraphStyle(model.styles, record.pPr),
87
89
  ...(record.readOnlyReason ? { readOnlyReason: record.readOnlyReason } : {}),
88
90
  operations: operationsOf("paragraph", record.readOnlyReason !== undefined),
89
91
  };
90
92
  }
93
+ /** Where each paragraph of a table sits, read once per table. */
94
+ const CELLS = new WeakMap();
95
+ /** The cell of a table that holds a paragraph, when the table lists it. */
96
+ function cellOf(table, record) {
97
+ let cells = CELLS.get(table);
98
+ if (!cells) {
99
+ cells = new Map();
100
+ for (const [row, cellsOfRow] of table.rows.entries())
101
+ for (const [column, cell] of cellsOfRow.entries())
102
+ for (const paragraph of cell.paragraphs)
103
+ cells.set(paragraph, { row, column });
104
+ CELLS.set(table, cells);
105
+ }
106
+ const cell = cells.get(record);
107
+ return cell ? { cell } : {};
108
+ }
@@ -54,6 +54,14 @@ export interface DocxElement extends EditElement {
54
54
  readonly textStyle?: DocxTextStyle;
55
55
  /** Present for a paragraph. */
56
56
  readonly paragraphStyle?: DocxParagraphStyle;
57
+ /**
58
+ * Present for a paragraph in a table cell: the cell's row and its place in
59
+ * the row, both from 0. Paragraphs move only among those of the same cell.
60
+ */
61
+ readonly cell?: {
62
+ readonly row: number;
63
+ readonly column: number;
64
+ };
57
65
  /** Present for a table: cell text by row. */
58
66
  readonly table?: {
59
67
  readonly rows: readonly (readonly string[])[];
@@ -535,8 +535,11 @@ export class PdfEditDocument {
535
535
  },
536
536
  // Ids name the state the batch leads to, the operation and the item,
537
537
  // so replaying the history reproduces them and an undone id is never
538
- // handed out again.
539
- newId: (pageIndex, suffix = "") => `${this.#pages[pageIndex].key}:n${stateId}.${operationIndex}.${created++}${suffix}`,
538
+ // handed out again. A file saved by an earlier session carries the ids
539
+ // that session numbered from 1 as well, so one already on the page
540
+ // gets the first free `~n` instead (ACTION-886); the saved objects are
541
+ // part of the base, so a replay meets them and picks the same id.
542
+ newId: (pageIndex, suffix = "") => `${this.#unusedId(pageIndex, `${this.#pages[pageIndex].key}:n${stateId}.${operationIndex}.${created++}`)}${suffix}`,
540
543
  withPage: (pageIndex, use) => this.#writePage(pageIndex, use),
541
544
  appendObjects: (pageIndex, records) => {
542
545
  const page = this.#pages[pageIndex];
@@ -686,10 +689,52 @@ export class PdfEditDocument {
686
689
  }
687
690
  }
688
691
  record.objects = objects.map((object, index) => object.mark && stale.has(object.id)
689
- ? { id: `${record.key}:o${index}`, type: object.type }
692
+ ? {
693
+ id: `${record.key}:o${index}`,
694
+ type: object.type,
695
+ staleMarkId: object.id,
696
+ }
690
697
  : object);
691
698
  return record.objects;
692
699
  }
700
+ /**
701
+ * `id`, or its first free `~n` when an object saved by an earlier session
702
+ * has it already. Only ids a session numbered (`<key>:n…`) can collide, so
703
+ * only those are gathered; the objects are read without the page's text.
704
+ */
705
+ #unusedId(pageIndex, id) {
706
+ const record = this.#pages[pageIndex];
707
+ const objects = record.objects ?? this.#loadObjects(pageIndex);
708
+ const numbered = `${record.key}:n`;
709
+ const taken = new Set();
710
+ for (const object of objects) {
711
+ if (object.id.startsWith(numbered))
712
+ taken.add(object.id);
713
+ if (object.staleMarkId?.startsWith(numbered))
714
+ taken.add(object.staleMarkId);
715
+ }
716
+ if (!taken.has(id))
717
+ return id;
718
+ let suffix = 1;
719
+ while (taken.has(`${id}~${suffix}`))
720
+ suffix += 1;
721
+ return `${id}~${suffix}`;
722
+ }
723
+ /** A page's objects, loading the page alone when they are not known yet. */
724
+ #loadObjects(pageIndex) {
725
+ const { lib } = this.#pdfium;
726
+ const page = lib.FPDF_LoadPage(this.#document.handle, pageIndex);
727
+ if (!page)
728
+ throw new ViewerError("render-failed", "PDFium could not load the page", {
729
+ details: { pageIndex },
730
+ });
731
+ try {
732
+ return this.#objectsOf(pageIndex, page);
733
+ }
734
+ finally {
735
+ lib.FPDF_ClosePage(page);
736
+ }
737
+ }
693
738
  /** Marks from other sessions keep their id only if it cannot collide with ours. */
694
739
  #ownMark(mark, pageKey) {
695
740
  return mark.id.startsWith(`${pageKey}:`)
@@ -21,6 +21,12 @@ export interface ObjectRecord {
21
21
  readonly id: string;
22
22
  readonly type: number;
23
23
  readonly mark?: MarkParams;
24
+ /**
25
+ * The id a mark the file still carries names, on an object listed as plain
26
+ * because the mark failed its check: a new id must not take it, or saving
27
+ * would join the new object to this one.
28
+ */
29
+ readonly staleMarkId?: string;
24
30
  }
25
31
  /**
26
32
  * Reads the objects of a loaded page in drawing order and turns them into
@@ -110,7 +110,13 @@ export class FontLibrary {
110
110
  return undefined;
111
111
  if (this.#fallback && this.#covers(this.#fallback, request.text))
112
112
  return undefined;
113
- const bad = firstNonWinAnsi(request.text) ?? firstUncovered(request.text, this);
113
+ // Name the character that actually stops the text: the first one the
114
+ // fallback lacks when it was tried, else the first the standard fonts
115
+ // cannot encode. Naming the first Cyrillic letter for a dash the
116
+ // fallback lacked sent people after the wrong character (ACTION-879).
117
+ const bad = this.#fallback
118
+ ? this.#firstMissing(this.#fallback, request.text)
119
+ : firstNonWinAnsi(request.text);
114
120
  return {
115
121
  path: "/text",
116
122
  code: "font-unavailable",
@@ -175,6 +181,10 @@ export class FontLibrary {
175
181
  return candidates.sort((a, b) => score(b.font) - score(a.font))[0];
176
182
  }
177
183
  #covers(bytes, text) {
184
+ return this.#firstMissing(bytes, text) === undefined;
185
+ }
186
+ /** The first character of `text` the font has no glyph for; layout handles line breaks and spaces. */
187
+ #firstMissing(bytes, text) {
178
188
  let coverage = this.#coverage.get(bytes);
179
189
  if (!coverage) {
180
190
  coverage = parseCmap(bytes);
@@ -185,9 +195,9 @@ export class FontLibrary {
185
195
  if (code === 0x0a || code === 0x20)
186
196
  continue;
187
197
  if (!coverage.has(code))
188
- return false;
198
+ return character;
189
199
  }
190
- return true;
200
+ return undefined;
191
201
  }
192
202
  #load(pdfium, document, bytes) {
193
203
  let handles = this.#handles.get(document);
@@ -242,13 +252,6 @@ function isBold(font) {
242
252
  function isItalic(font) {
243
253
  return font.style !== "normal";
244
254
  }
245
- function firstUncovered(text, library) {
246
- void library;
247
- for (const character of text)
248
- if (character !== "\n" && character !== " ")
249
- return character;
250
- return undefined;
251
- }
252
255
  /** The code points a TrueType font maps to glyphs, from its cmap table. */
253
256
  /**
254
257
  * Whether glyphs of a TrueType font have outline data: a subset font can keep
@@ -74,11 +74,19 @@ export const rotatePage = {
74
74
  validate(operation, context, issue) {
75
75
  if (operation.pageIndex >= context.pageCount)
76
76
  issue("/pageIndex", "unknown-target", `No page ${operation.pageIndex}`);
77
+ // A field given as undefined counts as absent, as it does everywhere else.
78
+ if ((operation.rotation === undefined) === (operation.by === undefined))
79
+ issue("", "one-of", "Give exactly one of `rotation` and `by`");
77
80
  },
78
81
  apply(operation, context) {
79
82
  const { lib } = context.pdfium;
80
83
  context.withPage(operation.pageIndex, (page) => {
81
- lib.FPDFPage_SetRotation(page, operation.rotation / 90);
84
+ // A turn reads the angle the page has now, so it composes with what
85
+ // the file was saved with and with turns earlier in the history.
86
+ const quarters = operation.by !== undefined
87
+ ? (lib.FPDFPage_GetRotation(page) + operation.by / 90) % 4
88
+ : operation.rotation / 90;
89
+ lib.FPDFPage_SetRotation(page, quarters);
82
90
  });
83
91
  context.invalidatePage(operation.pageIndex);
84
92
  return {
@@ -170,10 +170,13 @@ export const pdfOperationSchemas = Object.freeze({
170
170
  from: { type: "integer", minimum: 0 },
171
171
  to: { type: "integer", minimum: 0 },
172
172
  }, ["from", "to"]),
173
+ // Exactly one of `rotation` (absolute) and `by` (a turn from the current
174
+ // angle); the handler reports a batch that gives both or neither.
173
175
  rotatePage: operation("rotatePage", {
174
176
  pageIndex: { type: "integer", minimum: 0 },
175
177
  rotation: { enum: [0, 90, 180, 270] },
176
- }, ["pageIndex", "rotation"]),
178
+ by: { enum: [90, 180, 270] },
179
+ }, ["pageIndex"]),
177
180
  insertShape: operation("insertShape", {
178
181
  pageIndex: { type: "integer", minimum: 0 },
179
182
  shape: { enum: ["rectangle", "ellipse", "line"] },
@@ -192,12 +192,24 @@ export interface SetShapeStyleOperation {
192
192
  /** A new fill, `null` to remove it, or absent to keep it. */
193
193
  readonly fill?: PdfFill | null;
194
194
  }
195
- export interface RotatePageOperation {
195
+ /**
196
+ * Sets a page's rotation, or turns it from where it stands: a host offering
197
+ * "rotate" does not need to know the angle a file was saved with, and a
198
+ * replay of the history turns from the same angle again.
199
+ */
200
+ export type RotatePageOperation = {
196
201
  readonly op: "rotatePage";
197
202
  readonly pageIndex: number;
198
203
  /** Absolute clockwise rotation in degrees. */
199
204
  readonly rotation: 0 | 90 | 180 | 270;
200
- }
205
+ readonly by?: never;
206
+ } | {
207
+ readonly op: "rotatePage";
208
+ readonly pageIndex: number;
209
+ /** Clockwise turn in degrees from the page's current rotation. */
210
+ readonly by: 90 | 180 | 270;
211
+ readonly rotation?: never;
212
+ };
201
213
  /** The PDF operation union; operations are added as they ship. */
202
214
  export type PdfOperation = InsertTextBoxOperation | ReplaceTextOperation | SetTextStyleOperation | ResizeElementOperation | MoveElementOperation | DeleteElementOperation | InsertPageOperation | DeletePageOperation | MovePageOperation | RotatePageOperation | InsertShapeOperation | SetShapeStyleOperation | InsertImageOperation | InsertTableOperation | SetTableCellOperation;
203
215
  export interface PdfSaveOptions extends SaveOptions {
@@ -321,7 +333,7 @@ export interface PdfEditSession extends EditSessionBase<PdfOperation, PdfElement
321
333
  deletePage(fields: Fields<DeletePageOperation>, options?: ApplyOptions): Promise<EditReceipt>;
322
334
  /** Reorders pages. */
323
335
  movePage(fields: Fields<MovePageOperation>, options?: ApplyOptions): Promise<EditReceipt>;
324
- /** Sets a page's rotation. */
336
+ /** Sets a page's rotation, or turns it from the one it has. */
325
337
  rotatePage(fields: Fields<RotatePageOperation>, options?: ApplyOptions): Promise<EditReceipt>;
326
338
  /** Draws a rectangle, ellipse or line as a path object. */
327
339
  insertShape(fields: Fields<InsertShapeOperation>, options?: ApplyOptions): Promise<EditReceipt>;
@@ -6,8 +6,11 @@ The WOFF2 files in this directory are modified/subset builds of Noto Sans and No
6
6
  - Noto Sans CJK sources: `notofonts/noto-cjk` commit `f8d157532fbfaeda587e826d4cd5b21a49186f7c`.
7
7
  - License: SIL Open Font License 1.1; see `OFL-1.1.txt`.
8
8
  - Modified font family used by the viewer: `Zrimo Noto`.
9
- - `noto-sans-latin-cyrillic.ttf` is the same Latin/Cyrillic subset saved as an
10
- uncompressed TrueType file; the PDF editor embeds it into documents whose
11
- text the standard fonts cannot encode.
9
+ - `noto-sans-latin-cyrillic.ttf` is the same Latin/Cyrillic subset plus Noto
10
+ Sans's General Punctuation, Superscripts and Subscripts, Currency Symbols,
11
+ Letterlike Symbols and Number Forms (ranges in `manifest.json`; the Arrows
12
+ and Mathematical Operators ranges are requested too, but Noto Sans has no
13
+ glyphs there), saved as an uncompressed TrueType file; the PDF editor embeds it into
14
+ documents whose text the standard fonts cannot encode.
12
15
 
13
16
  No proprietary Microsoft font is included.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "family": "Zrimo Noto",
4
- "generated": "2026-07-16",
5
- "tooling": "fonttools pyftsubset with all layout features, then woff2_compress; the .ttf is the Latin/Cyrillic subset saved uncompressed with fonttools for embedding into PDFs",
4
+ "generated": "2026-10-02",
5
+ "tooling": "fonttools pyftsubset with all layout features, then woff2_compress; the .ttf is the Latin/Cyrillic subset saved uncompressed with fonttools for embedding into PDFs, from hinted/ttf/NotoSans/NotoSans-Regular.ttf with U+0000,U+000D,U+0020-007E,U+00A0-024F,U+0300-036F,U+0400-052F,U+1E00-1EFF plus General Punctuation, Superscripts and Subscripts, Currency Symbols, Letterlike Symbols, Number Forms, Arrows, Mathematical Operators, U+25CC and U+FFFD (U+2000-206F,U+2070-209F,U+20A0-20CF,U+2100-214F,U+2150-218F,U+2190-21FF,U+2200-22FF,U+25CC,U+FFFD) so text typed with dashes, quotes, currency signs and the numero sign can be embedded; Noto Sans has no glyphs in the Arrows and Mathematical Operators ranges, so those two add none",
6
6
  "license": "OFL-1.1",
7
7
  "sources": {
8
8
  "noto-fonts": "https://github.com/notofonts/noto-fonts/tree/ffebf8c1ee449e544955a7e813c54f9b73848eac",
@@ -81,8 +81,8 @@
81
81
  },
82
82
  {
83
83
  "file": "noto-sans-latin-cyrillic.ttf",
84
- "bytes": 218796,
85
- "sha256": "6a23e5413988b4b523ceb4aa81a1de85dad21336868997f52eb6d42159345262"
84
+ "bytes": 260552,
85
+ "sha256": "94094981da300fdb704d64cb91354360d37bf69b26f8584da7d44fdc2e0bdc84"
86
86
  }
87
87
  ]
88
88
  }
@@ -3300,13 +3300,27 @@ function paragraphElement(model, record, base) {
3300
3300
  id: record.elementId,
3301
3301
  kind: "paragraph",
3302
3302
  text: record.text.text,
3303
- ...record.table ? { parentId: record.table.elementId } : {},
3303
+ ...record.table ? { parentId: record.table.elementId, ...cellOf(record.table, record) } : {},
3304
3304
  textStyle: resolveTextStyle(model.styles, record.pPr, first?.rPr),
3305
3305
  paragraphStyle: resolveParagraphStyle(model.styles, record.pPr),
3306
3306
  ...record.readOnlyReason ? { readOnlyReason: record.readOnlyReason } : {},
3307
3307
  operations: operationsOf("paragraph", record.readOnlyReason !== void 0)
3308
3308
  };
3309
3309
  }
3310
+ var CELLS = /* @__PURE__ */ new WeakMap();
3311
+ function cellOf(table, record) {
3312
+ let cells = CELLS.get(table);
3313
+ if (!cells) {
3314
+ cells = /* @__PURE__ */ new Map();
3315
+ for (const [row, cellsOfRow] of table.rows.entries())
3316
+ for (const [column, cell2] of cellsOfRow.entries())
3317
+ for (const paragraph of cell2.paragraphs)
3318
+ cells.set(paragraph, { row, column });
3319
+ CELLS.set(table, cells);
3320
+ }
3321
+ const cell = cells.get(record);
3322
+ return cell ? { cell } : {};
3323
+ }
3310
3324
 
3311
3325
  // src/edit/docx/write.ts
3312
3326
  var MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006";
@@ -5850,13 +5850,16 @@ var pdfOperationSchemas = Object.freeze({
5850
5850
  },
5851
5851
  ["from", "to"]
5852
5852
  ),
5853
+ // Exactly one of `rotation` (absolute) and `by` (a turn from the current
5854
+ // angle); the handler reports a batch that gives both or neither.
5853
5855
  rotatePage: operation(
5854
5856
  "rotatePage",
5855
5857
  {
5856
5858
  pageIndex: { type: "integer", minimum: 0 },
5857
- rotation: { enum: [0, 90, 180, 270] }
5859
+ rotation: { enum: [0, 90, 180, 270] },
5860
+ by: { enum: [90, 180, 270] }
5858
5861
  },
5859
- ["pageIndex", "rotation"]
5862
+ ["pageIndex"]
5860
5863
  ),
5861
5864
  insertShape: operation(
5862
5865
  "insertShape",
@@ -6897,7 +6900,7 @@ var FontLibrary = class {
6897
6900
  if (this.#coveringRegistered(request)) return void 0;
6898
6901
  if (this.#fallback && this.#covers(this.#fallback, request.text))
6899
6902
  return void 0;
6900
- const bad = firstNonWinAnsi(request.text) ?? firstUncovered(request.text, this);
6903
+ const bad = this.#fallback ? this.#firstMissing(this.#fallback, request.text) : firstNonWinAnsi(request.text);
6901
6904
  return {
6902
6905
  path: "/text",
6903
6906
  code: "font-unavailable",
@@ -6955,6 +6958,10 @@ var FontLibrary = class {
6955
6958
  return candidates.sort((a, b) => score(b.font) - score(a.font))[0];
6956
6959
  }
6957
6960
  #covers(bytes, text) {
6961
+ return this.#firstMissing(bytes, text) === void 0;
6962
+ }
6963
+ /** The first character of `text` the font has no glyph for; layout handles line breaks and spaces. */
6964
+ #firstMissing(bytes, text) {
6958
6965
  let coverage = this.#coverage.get(bytes);
6959
6966
  if (!coverage) {
6960
6967
  coverage = parseCmap(bytes);
@@ -6963,9 +6970,9 @@ var FontLibrary = class {
6963
6970
  for (const character of text) {
6964
6971
  const code = character.codePointAt(0);
6965
6972
  if (code === 10 || code === 32) continue;
6966
- if (!coverage.has(code)) return false;
6973
+ if (!coverage.has(code)) return character;
6967
6974
  }
6968
- return true;
6975
+ return void 0;
6969
6976
  }
6970
6977
  #load(pdfium, document2, bytes) {
6971
6978
  let handles = this.#handles.get(document2);
@@ -7017,12 +7024,6 @@ function isBold(font) {
7017
7024
  function isItalic(font) {
7018
7025
  return font.style !== "normal";
7019
7026
  }
7020
- function firstUncovered(text, library) {
7021
- void library;
7022
- for (const character of text)
7023
- if (character !== "\n" && character !== " ") return character;
7024
- return void 0;
7025
- }
7026
7027
  function glyphOutlines(view, tables) {
7027
7028
  const head = tables.get("head");
7028
7029
  const maxp = tables.get("maxp");
@@ -8663,11 +8664,14 @@ var rotatePage = {
8663
8664
  validate(operation2, context, issue) {
8664
8665
  if (operation2.pageIndex >= context.pageCount)
8665
8666
  issue("/pageIndex", "unknown-target", `No page ${operation2.pageIndex}`);
8667
+ if (operation2.rotation === void 0 === (operation2.by === void 0))
8668
+ issue("", "one-of", "Give exactly one of `rotation` and `by`");
8666
8669
  },
8667
8670
  apply(operation2, context) {
8668
8671
  const { lib } = context.pdfium;
8669
8672
  context.withPage(operation2.pageIndex, (page) => {
8670
- lib.FPDFPage_SetRotation(page, operation2.rotation / 90);
8673
+ const quarters = operation2.by !== void 0 ? (lib.FPDFPage_GetRotation(page) + operation2.by / 90) % 4 : operation2.rotation / 90;
8674
+ lib.FPDFPage_SetRotation(page, quarters);
8671
8675
  });
8672
8676
  context.invalidatePage(operation2.pageIndex);
8673
8677
  return {
@@ -10043,8 +10047,11 @@ var PdfEditDocument = class {
10043
10047
  },
10044
10048
  // Ids name the state the batch leads to, the operation and the item,
10045
10049
  // so replaying the history reproduces them and an undone id is never
10046
- // handed out again.
10047
- newId: (pageIndex, suffix = "") => `${this.#pages[pageIndex].key}:n${stateId}.${operationIndex}.${created++}${suffix}`,
10050
+ // handed out again. A file saved by an earlier session carries the ids
10051
+ // that session numbered from 1 as well, so one already on the page
10052
+ // gets the first free `~n` instead (ACTION-886); the saved objects are
10053
+ // part of the base, so a replay meets them and picks the same id.
10054
+ newId: (pageIndex, suffix = "") => `${this.#unusedId(pageIndex, `${this.#pages[pageIndex].key}:n${stateId}.${operationIndex}.${created++}`)}${suffix}`,
10048
10055
  withPage: (pageIndex, use) => this.#writePage(pageIndex, use),
10049
10056
  appendObjects: (pageIndex, records) => {
10050
10057
  const page = this.#pages[pageIndex];
@@ -10200,10 +10207,48 @@ var PdfEditDocument = class {
10200
10207
  }
10201
10208
  }
10202
10209
  record.objects = objects.map(
10203
- (object, index) => object.mark && stale.has(object.id) ? { id: `${record.key}:o${index}`, type: object.type } : object
10210
+ (object, index) => object.mark && stale.has(object.id) ? {
10211
+ id: `${record.key}:o${index}`,
10212
+ type: object.type,
10213
+ staleMarkId: object.id
10214
+ } : object
10204
10215
  );
10205
10216
  return record.objects;
10206
10217
  }
10218
+ /**
10219
+ * `id`, or its first free `~n` when an object saved by an earlier session
10220
+ * has it already. Only ids a session numbered (`<key>:n…`) can collide, so
10221
+ * only those are gathered; the objects are read without the page's text.
10222
+ */
10223
+ #unusedId(pageIndex, id) {
10224
+ const record = this.#pages[pageIndex];
10225
+ const objects = record.objects ?? this.#loadObjects(pageIndex);
10226
+ const numbered = `${record.key}:n`;
10227
+ const taken = /* @__PURE__ */ new Set();
10228
+ for (const object of objects) {
10229
+ if (object.id.startsWith(numbered)) taken.add(object.id);
10230
+ if (object.staleMarkId?.startsWith(numbered))
10231
+ taken.add(object.staleMarkId);
10232
+ }
10233
+ if (!taken.has(id)) return id;
10234
+ let suffix = 1;
10235
+ while (taken.has(`${id}~${suffix}`)) suffix += 1;
10236
+ return `${id}~${suffix}`;
10237
+ }
10238
+ /** A page's objects, loading the page alone when they are not known yet. */
10239
+ #loadObjects(pageIndex) {
10240
+ const { lib } = this.#pdfium;
10241
+ const page = lib.FPDF_LoadPage(this.#document.handle, pageIndex);
10242
+ if (!page)
10243
+ throw new ViewerError("render-failed", "PDFium could not load the page", {
10244
+ details: { pageIndex }
10245
+ });
10246
+ try {
10247
+ return this.#objectsOf(pageIndex, page);
10248
+ } finally {
10249
+ lib.FPDF_ClosePage(page);
10250
+ }
10251
+ }
10207
10252
  /** Marks from other sessions keep their id only if it cannot collide with ours. */
10208
10253
  #ownMark(mark, pageKey) {
10209
10254
  return mark.id.startsWith(`${pageKey}:`) ? mark : { ...mark, id: `${pageKey}:${mark.id}` };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-doc",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
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",