pdf-codec 4.3.0 → 4.4.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.
package/README.md CHANGED
@@ -234,7 +234,7 @@ The package is layered from generic primitives outward to the codec itself:
234
234
  - **`src/util/`** — two small independently-duplicated copies of family-shared logic: `base64.ts` (verbatim copy of `odf.js`'s own, replacing a former `ooxml.js` dependency for this one helper) and `abort.ts` (`throwIfAborted`, called at every page loop boundary — there is no `await` point in this synchronous pipeline for cancellation to hook into implicitly).
235
235
  - **`src/crypto/`** — MD5, SHA-256/384/512, RC4, and AES-CBC, hand-written with zero local imports, plus a thin `random.ts` wrapper around `globalThis.crypto.getRandomValues` for the writer's own salts/file keys/IVs. Not a preference: ISO 32000-1's key-derivation algorithms name MD5 and RC4 directly, neither offered by any portable platform crypto API, and `crypto.subtle` is asynchronous where this codec's read and write paths are both synchronous end to end — `getRandomValues` itself, unlike `crypto.subtle`, is synchronous and portable, so it is used directly rather than hand-written. Reaching for `node:crypto` would break the browser bundle. Each hash/cipher module cites its specification (RFC 1321, FIPS 180-4, FIPS 197) and is tested against published conformance vectors.
236
236
  - **The codec itself, importing only `layout`/`bytes`/`image`/`crypto`/`util` plus `document-schema.js`'s port types (no OOXML or ODF knowledge):**
237
- - **Write**: `objects.ts` (the `PdfObject` discriminated union), `afm-widths.ts`/`encoding.ts`/`winansi.ts`/`fonts.ts` (standard-14 metrics, WinAnsi encoding, family resolution), `font-registry.ts` (resolution port plus `resolveFaceWithRegistry`, the one step both measurer and writer resolve through so they can never disagree about which face a `LayoutFont` means), `font-face.ts` (`readFontFace`, reading a standalone font file's family/bold/italic triple off its `name`/`OS/2`/`head` tables), `measure.ts`/`text-layout.ts` (greedy line-wrapping against either standard-14 AFM widths plus per-family correction or a resolved face's own real `hmtx` advances — never both), `content-write.ts` (`LayoutItem[]` → content-stream operators, with text branching on standard-14 vs embedded face encoding, pair-kerning split into `TJ` arrays, and stroke `style` becoming real dash/line-cap state), `write.ts` (the full object graph, cross-reference table, trailer, and embedded font groups).
237
+ - **Write**: `objects.ts` (the `PdfObject` discriminated union), `afm-widths.ts`/`encoding.ts`/`winansi.ts`/`fonts.ts` (standard-14 metrics, WinAnsi encoding, family resolution), `font-registry.ts` (resolution port plus `resolveFaceWithRegistry`, the one step both measurer and writer resolve through so they can never disagree about which face a `LayoutFont` means), `font-face.ts` (`readFontFace`, reading a standalone font file's family/bold/italic triple off its `name`/`OS/2`/`head` tables), `measure.ts`/`text-layout.ts` (greedy line-wrapping against either standard-14 AFM widths plus per-family correction or a resolved face's own real `hmtx` advances — never both), `content-write.ts` (`LayoutItem[]` → content-stream operators, with text branching on standard-14 vs embedded face encoding, pair-kerning split into `TJ` arrays, stroke `style` becoming real dash/line-cap state, and `/P <</MCID n /OC R>> BDC`…`EMC` spans around items carrying a structure owner or an optional-content layer — the one marked-content spelling both read-side channels resolve through), `write.ts` (the full object graph, cross-reference table, trailer, and embedded font groups, plus the document-level round-trip surfaces: the `/Names /EmbeddedFiles` attachments tree and `/Outlines` bookmark tree, `/OCProperties` optional-content groups with each layer stated explicitly into the default configuration's `/ON` or `/OFF` list, the `/AcroForm` field tree with the merged-field/widget spelling (a single widget merges into its field dict; further widgets are separate `/Subtype /Widget` kids, and fully-qualified names decompose back into the `/T` chain), the `/StructTreeRoot` element tree with its `/ParentTree` number tree associating each marked item's MCID to its owning element, and the package-level residue rows restored inline onto the Catalog or trailer — the XMP packet as an uncompressed `/Metadata` stream, and any row whose serialisation references source-file objects skipped rather than emitted as a dangling reference).
238
238
  - **sfnt font tables**: `sfnt.ts` (bounds-checked table-directory reader), `cmap-table.ts` (character code → glyph ID, formats 0/4/6/12, exposed both as the best Unicode lookup and as every subtable individually, since a (3, 0) or (1, 0) subtable is keyed by a font's own codes rather than by code points), `hmtx-table.ts` (per-glyph advance widths), `font-tables.ts` (`head`/`maxp`/`OS/2`/`post`/`name`, including `post`'s own glyph names where a font still carries them), `glyf.ts` (`loca` offset index, per-glyph headers, composite component records, `glyphInkBounds`), `math-table.ts` (OpenType `MATH` constants/glyph-info/variants subtables). `ot-layout-common.ts` (Coverage/ClassDef, stored as sorted glyph ranges searched by bisection). `gpos-table.ts` reads `GPOS` for exactly one thing: pair-advance kerning through the `kern` feature, both PairPos formats and LookupType 9 Extension indirection — mark attachment, cursive joining, and contextual positioning have no consumer here. Every parser degrades to `undefined` on a missing/truncated table rather than throwing.
239
239
  - **sfnt subsetting**: `sfnt-subset.ts` — a TrueType-outline glyph subsetter (Unicode code points → glyph IDs via `cmap`, transitive closure over `glyf` composite components, rebuilt sfnt carrying only used outlines). **Glyph IDs are preserved, never renumbered**, keeping composite component references valid and making CID == GID trivially true. Output rebuilds `head`/`hhea`/`maxp`/`loca`/`glyf`/`hmtx`, copies hinting programs verbatim, stubs `post`, omits `cmap`/`name`/`OS/2`/`GSUB`/`GPOS`/`kern` (none read through a `CIDFontType2` program per ISO 32000-1 9.9). Applies to `glyf`-flavoured fonts only; CFF returns `undefined`.
240
240
  - **Embedded math font**: `math-font.ts` (parses/caches the vendored STIX Two Math font, exposing size-specific `MathFontMetrics` and stretchy-glyph entry points), `math-stretch.ts` (OpenType MATH two-stage stretching: pick smallest pre-built variant reaching target, else assemble from repeated parts with seams overlapped), `math-font-write.ts` (builds the `/Type0`/`/CIDFontType0`/`/FontDescriptor`/`/FontFile3`/ToUnicode group), `math-content-write.ts` (`PositionedFormula[]` → content-stream bytes, Identity-H CIDs for text, `re`/`m`/`l` operators for rules, glyph-ID-addressed text objects for stretched constructions wrapped in `/ActualText`).
@@ -313,16 +313,38 @@ function writeContentStream(items, context) {
313
313
  const writer = new require_bytes_writer.ByteWriter();
314
314
  const substitutions = [];
315
315
  const missingGlyphs = [];
316
- for (const item of items) if (item.kind === "text") writeText(writer, item, context, substitutions, missingGlyphs);
317
- else if (item.kind === "image") writeImage(writer, item, context);
318
- else if (item.kind === "rect") writeRect(writer, item);
319
- else if (item.kind === "line") writeLine(writer, item);
320
- else if (item.kind === "ellipse") writeEllipse(writer, item);
321
- else if (item.kind === "path") writePath(writer, item);
316
+ const markedStructure = [];
317
+ for (const item of items) {
318
+ const itemLayer = "layer" in item ? item.layer : void 0;
319
+ const itemStructure = "structure" in item ? item.structure : void 0;
320
+ const layerObjectNum = itemLayer !== void 0 ? context.layerObjectNumberOf?.(itemLayer) : void 0;
321
+ const marked = itemStructure !== void 0 || layerObjectNum !== void 0;
322
+ if (marked) {
323
+ writer.writeAscii("/P <<");
324
+ if (itemStructure !== void 0 && context.nextMcid !== void 0) {
325
+ const mcid = context.nextMcid();
326
+ markedStructure.push({
327
+ mcid,
328
+ structureId: itemStructure
329
+ });
330
+ writer.writeAscii(` /MCID ${mcid}`);
331
+ }
332
+ if (layerObjectNum !== void 0) writer.writeAscii(` /OC ${layerObjectNum} 0 R`);
333
+ writer.writeAscii(" >> BDC\n");
334
+ }
335
+ if (item.kind === "text") writeText(writer, item, context, substitutions, missingGlyphs);
336
+ else if (item.kind === "image") writeImage(writer, item, context);
337
+ else if (item.kind === "rect") writeRect(writer, item);
338
+ else if (item.kind === "line") writeLine(writer, item);
339
+ else if (item.kind === "ellipse") writeEllipse(writer, item);
340
+ else if (item.kind === "path") writePath(writer, item);
341
+ if (marked) writer.writeAscii("EMC\n");
342
+ }
322
343
  return {
323
344
  bytes: writer.toBytes(),
324
345
  substitutions,
325
- missingGlyphs
346
+ missingGlyphs,
347
+ markedStructure
326
348
  };
327
349
  }
328
350
  //#endregion
@@ -20,12 +20,19 @@ interface ContentWriteContext {
20
20
  readonly measurer: TextMeasurer;
21
21
  resolveFont(font: LayoutFont): ResolvedFontResource;
22
22
  resolveImage(imageId: string): ResolvedImageResource;
23
+ readonly layerObjectNumberOf?: (name: string) => number | undefined;
24
+ readonly nextMcid?: () => number;
23
25
  }
24
26
  interface ContentStreamResult {
25
27
  readonly bytes: Uint8Array<ArrayBuffer>;
28
+ readonly markedStructure: readonly MarkedStructureItem[];
26
29
  readonly substitutions: readonly WinAnsiSubstitution[];
27
30
  readonly missingGlyphs: readonly EmbeddedFaceSubstitution[];
28
31
  }
29
32
  declare function writeContentStream(items: readonly LayoutItem[], context: ContentWriteContext): ContentStreamResult;
33
+ interface MarkedStructureItem {
34
+ readonly mcid: number;
35
+ readonly structureId: string;
36
+ }
30
37
  //#endregion
31
- export { ContentStreamResult, ContentWriteContext, ResolvedFontResource, ResolvedImageResource, writeContentStream };
38
+ export { ContentStreamResult, ContentWriteContext, MarkedStructureItem, ResolvedFontResource, ResolvedImageResource, writeContentStream };
@@ -20,12 +20,19 @@ interface ContentWriteContext {
20
20
  readonly measurer: TextMeasurer;
21
21
  resolveFont(font: LayoutFont): ResolvedFontResource;
22
22
  resolveImage(imageId: string): ResolvedImageResource;
23
+ readonly layerObjectNumberOf?: (name: string) => number | undefined;
24
+ readonly nextMcid?: () => number;
23
25
  }
24
26
  interface ContentStreamResult {
25
27
  readonly bytes: Uint8Array<ArrayBuffer>;
28
+ readonly markedStructure: readonly MarkedStructureItem[];
26
29
  readonly substitutions: readonly WinAnsiSubstitution[];
27
30
  readonly missingGlyphs: readonly EmbeddedFaceSubstitution[];
28
31
  }
29
32
  declare function writeContentStream(items: readonly LayoutItem[], context: ContentWriteContext): ContentStreamResult;
33
+ interface MarkedStructureItem {
34
+ readonly mcid: number;
35
+ readonly structureId: string;
36
+ }
30
37
  //#endregion
31
- export { ContentStreamResult, ContentWriteContext, ResolvedFontResource, ResolvedImageResource, writeContentStream };
38
+ export { ContentStreamResult, ContentWriteContext, MarkedStructureItem, ResolvedFontResource, ResolvedImageResource, writeContentStream };
@@ -312,16 +312,38 @@ function writeContentStream(items, context) {
312
312
  const writer = new ByteWriter();
313
313
  const substitutions = [];
314
314
  const missingGlyphs = [];
315
- for (const item of items) if (item.kind === "text") writeText(writer, item, context, substitutions, missingGlyphs);
316
- else if (item.kind === "image") writeImage(writer, item, context);
317
- else if (item.kind === "rect") writeRect(writer, item);
318
- else if (item.kind === "line") writeLine(writer, item);
319
- else if (item.kind === "ellipse") writeEllipse(writer, item);
320
- else if (item.kind === "path") writePath(writer, item);
315
+ const markedStructure = [];
316
+ for (const item of items) {
317
+ const itemLayer = "layer" in item ? item.layer : void 0;
318
+ const itemStructure = "structure" in item ? item.structure : void 0;
319
+ const layerObjectNum = itemLayer !== void 0 ? context.layerObjectNumberOf?.(itemLayer) : void 0;
320
+ const marked = itemStructure !== void 0 || layerObjectNum !== void 0;
321
+ if (marked) {
322
+ writer.writeAscii("/P <<");
323
+ if (itemStructure !== void 0 && context.nextMcid !== void 0) {
324
+ const mcid = context.nextMcid();
325
+ markedStructure.push({
326
+ mcid,
327
+ structureId: itemStructure
328
+ });
329
+ writer.writeAscii(` /MCID ${mcid}`);
330
+ }
331
+ if (layerObjectNum !== void 0) writer.writeAscii(` /OC ${layerObjectNum} 0 R`);
332
+ writer.writeAscii(" >> BDC\n");
333
+ }
334
+ if (item.kind === "text") writeText(writer, item, context, substitutions, missingGlyphs);
335
+ else if (item.kind === "image") writeImage(writer, item, context);
336
+ else if (item.kind === "rect") writeRect(writer, item);
337
+ else if (item.kind === "line") writeLine(writer, item);
338
+ else if (item.kind === "ellipse") writeEllipse(writer, item);
339
+ else if (item.kind === "path") writePath(writer, item);
340
+ if (marked) writer.writeAscii("EMC\n");
341
+ }
321
342
  return {
322
343
  bytes: writer.toBytes(),
323
344
  substitutions,
324
- missingGlyphs
345
+ missingGlyphs,
346
+ markedStructure
325
347
  };
326
348
  }
327
349
  //#endregion
package/dist/write.cjs CHANGED
@@ -4,8 +4,10 @@ const require_notes_annotation_author = require("./notes-annotation-author.cjs")
4
4
  const require_objects = require("./objects.cjs");
5
5
  const require_bytes_writer = require("./bytes/writer.cjs");
6
6
  const require_serialize = require("./serialize.cjs");
7
+ const require_bytes_reader = require("./bytes/reader.cjs");
7
8
  const require_bytes_flate = require("./bytes/flate.cjs");
8
9
  const require_util_base64 = require("./util/base64.cjs");
10
+ const require_parse = require("./parse.cjs");
9
11
  const require_util_abort = require("./util/abort.cjs");
10
12
  const require_crypto_random = require("./crypto/random.cjs");
11
13
  const require_encrypt_write = require("./encrypt-write.cjs");
@@ -252,6 +254,24 @@ function buildNotesAnnotDict(notes) {
252
254
  function xrefEntry(offset, generation, inUse) {
253
255
  return `${offset.toString().padStart(10, "0")} ${generation.toString().padStart(5, "0")} ${inUse ? "n" : "f"} \n`;
254
256
  }
257
+ function parseResidueRow(residue) {
258
+ const reader = new require_bytes_reader.ByteReader(new TextEncoder().encode(residue.xml));
259
+ return require_parse.parseValue(reader, () => {});
260
+ }
261
+ function objectContainsReference(obj) {
262
+ if (obj.kind === "ref") return true;
263
+ if (obj.kind === "array") return obj.items.some(objectContainsReference);
264
+ if (obj.kind === "dict") return [...obj.entries.values()].some(objectContainsReference);
265
+ if (obj.kind === "stream") return [...obj.dict.entries.values()].some(objectContainsReference);
266
+ return false;
267
+ }
268
+ function restoreResidueRow(source, key) {
269
+ const row = source?.[key];
270
+ if (row === void 0) return;
271
+ const parsed = parseResidueRow(row);
272
+ if (parsed === void 0 || objectContainsReference(parsed)) return;
273
+ return parsed;
274
+ }
255
275
  function writePdf(doc, options = {}) {
256
276
  const compress = options.compress ?? true;
257
277
  const registry = options.fonts;
@@ -340,10 +360,80 @@ function writePdf(doc, options = {}) {
340
360
  };
341
361
  for (let i = 0; i < countItems(doc.outline ?? []); i += 1) outlineItemNums.push(nextObjNum++);
342
362
  }
363
+ const layerNumByName = /* @__PURE__ */ new Map();
364
+ for (const layer of doc.layers ?? []) layerNumByName.set(layer.name, nextObjNum++);
365
+ const formObjectNums = [];
366
+ const countFieldObjects = (fields) => {
367
+ let n = 0;
368
+ for (const field of fields) n += 1 + (field.fieldType !== "group" && field.widgets.length > 1 ? field.widgets.length - 1 : 0) + countFieldObjects(field.children);
369
+ return n;
370
+ };
371
+ for (let i = 0; i < countFieldObjects(doc.form ?? []); i += 1) formObjectNums.push(nextObjNum++);
372
+ const formNumByField = /* @__PURE__ */ new Map();
373
+ const formExtraWidgetNums = /* @__PURE__ */ new Map();
374
+ let formNumCursor = 0;
375
+ const claimFormNums = (fields) => {
376
+ for (const field of fields) {
377
+ formNumByField.set(field, formObjectNums[formNumCursor++]);
378
+ if (field.fieldType !== "group" && field.widgets.length > 1) formExtraWidgetNums.set(field, field.widgets.slice(1).map(() => formObjectNums[formNumCursor++]));
379
+ claimFormNums(field.children);
380
+ }
381
+ };
382
+ claimFormNums(doc.form ?? []);
383
+ const formNumOf = (field) => {
384
+ const num = formNumByField.get(field);
385
+ if (num === void 0) throw new Error("AcroForm field object number was not claimed -- this is a writePdf internal invariant violation");
386
+ return num;
387
+ };
388
+ const structElementNumById = /* @__PURE__ */ new Map();
389
+ const countElements = (elements) => {
390
+ let n = 0;
391
+ for (const element of elements) {
392
+ structElementNumById.set(element.id, nextObjNum++);
393
+ n += 1 + countElements(element.children);
394
+ }
395
+ return n;
396
+ };
397
+ const structElementCount = countElements(doc.structure ?? []);
398
+ const structRootNum = structElementCount > 0 ? nextObjNum++ : void 0;
399
+ const structParentTreeNum = structElementCount > 0 ? nextObjNum++ : void 0;
400
+ const residueXmpNum = doc.source?.xmp !== void 0 ? nextObjNum++ : void 0;
343
401
  const objects = [];
344
402
  const catalogEntries = [["Type", require_objects.pdfName("Catalog")], ["Pages", require_objects.pdfRef(pagesNum, 0)]];
345
403
  if (attachmentsNamesNum !== void 0) catalogEntries.push(["Names", require_objects.pdfRef(attachmentsNamesNum, 0)]);
346
404
  if (outlineRootNum !== void 0) catalogEntries.push(["Outlines", require_objects.pdfRef(outlineRootNum, 0)]);
405
+ if (layerNumByName.size > 0) {
406
+ const ocgRefs = [...layerNumByName.values()].map((num) => require_objects.pdfRef(num, 0));
407
+ const visible = [];
408
+ const hidden = [];
409
+ for (const layer of doc.layers ?? []) {
410
+ const num = layerNumByName.get(layer.name);
411
+ (layer.visible ? visible : hidden).push(require_objects.pdfRef(num, 0));
412
+ }
413
+ const defaultConfigEntries = [];
414
+ if (visible.length > 0) defaultConfigEntries.push(["ON", require_objects.pdfArray(visible)]);
415
+ if (hidden.length > 0) defaultConfigEntries.push(["OFF", require_objects.pdfArray(hidden)]);
416
+ catalogEntries.push(["OCProperties", require_objects.pdfDict({
417
+ OCGs: require_objects.pdfArray(ocgRefs),
418
+ D: require_objects.pdfDict(Object.fromEntries(defaultConfigEntries))
419
+ })]);
420
+ }
421
+ if (doc.form !== void 0 && doc.form.length > 0) catalogEntries.push(["AcroForm", require_objects.pdfDict({ Fields: require_objects.pdfArray(doc.form.map((field) => require_objects.pdfRef(formNumOf(field), 0))) })]);
422
+ if (structRootNum !== void 0) catalogEntries.push(["StructTreeRoot", require_objects.pdfRef(structRootNum, 0)]);
423
+ if (residueXmpNum !== void 0) catalogEntries.push(["Metadata", require_objects.pdfRef(residueXmpNum, 0)]);
424
+ const trailerIdRestore = restoreResidueRow(doc.source, "trailer-id");
425
+ for (const [rowKey, catalogKey] of [
426
+ ["viewer-preferences", "ViewerPreferences"],
427
+ ["page-mode", "PageMode"],
428
+ ["page-layout", "PageLayout"],
429
+ ["output-intents", "OutputIntents"],
430
+ ["piece-info", "PieceInfo"],
431
+ ["legal", "Legal"],
432
+ ["collection", "Collection"]
433
+ ]) {
434
+ const restored = restoreResidueRow(doc.source, rowKey);
435
+ if (restored !== void 0) catalogEntries.push([catalogKey, restored]);
436
+ }
347
437
  objects.push({
348
438
  num: catalogNum,
349
439
  value: require_objects.pdfDict(Object.fromEntries(catalogEntries))
@@ -427,6 +517,118 @@ function writePdf(doc, options = {}) {
427
517
  })
428
518
  });
429
519
  }
520
+ for (const layer of doc.layers ?? []) {
521
+ const num = layerNumByName.get(layer.name);
522
+ objects.push({
523
+ num,
524
+ value: require_objects.pdfDict({
525
+ Type: require_objects.pdfName("OCG"),
526
+ Name: require_objects.pdfLiteralString(new TextEncoder().encode(layer.name))
527
+ })
528
+ });
529
+ }
530
+ const widgetRectArray = (widget) => require_objects.pdfArray([
531
+ widget.xPt,
532
+ widget.yPt,
533
+ widget.xPt + widget.widthPt,
534
+ widget.yPt + widget.heightPt
535
+ ].map((n) => require_objects.pdfNum(n)));
536
+ const widgetDict = (widget) => require_objects.pdfDict({
537
+ Subtype: require_objects.pdfName("Widget"),
538
+ Rect: widgetRectArray(widget),
539
+ P: require_objects.pdfRef(pageAllocs[widget.pageIndex].pageNum, 0)
540
+ });
541
+ const FIELD_TYPE_PDF_NAME = {
542
+ text: "Tx",
543
+ checkbox: "Btn",
544
+ radio: "Btn",
545
+ button: "Btn",
546
+ listbox: "Ch",
547
+ combobox: "Ch",
548
+ signature: "Sig"
549
+ };
550
+ const emitFormFieldObjects = (fields, parentName) => {
551
+ for (const field of fields) {
552
+ const ownName = parentName.length > 0 && field.name.startsWith(`${parentName}.`) ? field.name.slice(parentName.length + 1) : field.name;
553
+ const entries = [];
554
+ if (ownName.length > 0) entries.push(["T", require_objects.pdfLiteralString(new TextEncoder().encode(ownName))]);
555
+ if (field.alias !== void 0) entries.push(["TU", require_objects.pdfLiteralString(new TextEncoder().encode(field.alias))]);
556
+ if (field.fieldType === "group") entries.push(["Kids", require_objects.pdfArray(field.children.map((child) => require_objects.pdfRef(formNumOf(child), 0)))]);
557
+ else {
558
+ entries.push(["FT", require_objects.pdfName(FIELD_TYPE_PDF_NAME[field.fieldType])]);
559
+ const FLAG_READ_ONLY = 1;
560
+ const FLAG_PUSHBUTTON = 4;
561
+ const FLAG_RADIO = 32768;
562
+ const FLAG_COMBO = 131072;
563
+ let flags = 0;
564
+ if (field.readOnly === true) flags |= FLAG_READ_ONLY;
565
+ if (field.fieldType === "button") flags |= FLAG_PUSHBUTTON;
566
+ if (field.fieldType === "radio") flags |= FLAG_RADIO;
567
+ if (field.fieldType === "combobox") flags |= FLAG_COMBO;
568
+ if (flags !== 0) entries.push(["Ff", require_objects.pdfNum(flags)]);
569
+ if (field.fieldType === "text" || field.fieldType === "listbox" || field.fieldType === "combobox") {
570
+ if (field.value !== void 0) entries.push(["V", require_objects.pdfLiteralString(new TextEncoder().encode(field.value))]);
571
+ } else if (field.fieldType === "checkbox" || field.fieldType === "radio") entries.push(["V", require_objects.pdfName(field.checked === false || field.value === void 0 ? field.value ?? (field.checked === true ? "Yes" : "Off") : field.value)]);
572
+ if (field.options !== void 0) entries.push(["Opt", require_objects.pdfArray(field.options.map((option) => require_objects.pdfLiteralString(new TextEncoder().encode(option))))]);
573
+ const firstWidget = field.widgets[0];
574
+ if (field.widgets.length === 1 && firstWidget !== void 0) {
575
+ entries.push(["Subtype", require_objects.pdfName("Widget")]);
576
+ entries.push(["Rect", widgetRectArray(firstWidget)]);
577
+ entries.push(["P", require_objects.pdfRef(pageAllocs[firstWidget.pageIndex].pageNum, 0)]);
578
+ } else if (field.widgets.length > 1 && firstWidget !== void 0) {
579
+ const extraNums = formExtraWidgetNums.get(field) ?? [];
580
+ entries.push(["Kids", require_objects.pdfArray([widgetDict(firstWidget), ...extraNums.map((num) => require_objects.pdfRef(num, 0))])]);
581
+ }
582
+ }
583
+ objects.push({
584
+ num: formNumOf(field),
585
+ value: require_objects.pdfDict(Object.fromEntries(entries))
586
+ });
587
+ const extraNums = formExtraWidgetNums.get(field) ?? [];
588
+ for (const [index, num] of extraNums.entries()) {
589
+ const widget = field.widgets[index + 1];
590
+ objects.push({
591
+ num,
592
+ value: widgetDict(widget)
593
+ });
594
+ }
595
+ emitFormFieldObjects(field.children, field.name);
596
+ }
597
+ };
598
+ emitFormFieldObjects(doc.form ?? [], "");
599
+ if (structRootNum !== void 0 && structParentTreeNum !== void 0) {
600
+ const emitStructureElement = (element, parentNum) => {
601
+ const entries = [
602
+ ["Type", require_objects.pdfName("StructElem")],
603
+ ["S", require_objects.pdfName(element.type)],
604
+ ["P", require_objects.pdfRef(parentNum, 0)]
605
+ ];
606
+ if (element.title !== void 0) entries.push(["T", require_objects.pdfLiteralString(new TextEncoder().encode(element.title))]);
607
+ if (element.language !== void 0) entries.push(["Lang", require_objects.pdfLiteralString(new TextEncoder().encode(element.language))]);
608
+ if (element.alt !== void 0) entries.push(["Alt", require_objects.pdfLiteralString(new TextEncoder().encode(element.alt))]);
609
+ if (element.actualText !== void 0) entries.push(["ActualText", require_objects.pdfLiteralString(new TextEncoder().encode(element.actualText))]);
610
+ if (element.children.length > 0) entries.push(["K", require_objects.pdfArray(element.children.map((child) => {
611
+ const num = structElementNumById.get(child.id);
612
+ if (num === void 0) throw new Error(`structure element "${child.id}" was not allocated -- this is a writePdf internal invariant violation`);
613
+ return require_objects.pdfRef(num, 0);
614
+ }))]);
615
+ const ownNum = structElementNumById.get(element.id);
616
+ if (ownNum === void 0) throw new Error(`structure element "${element.id}" was not allocated -- this is a writePdf internal invariant violation`);
617
+ objects.push({
618
+ num: ownNum,
619
+ value: require_objects.pdfDict(Object.fromEntries(entries))
620
+ });
621
+ for (const child of element.children) emitStructureElement(child, ownNum);
622
+ };
623
+ for (const element of doc.structure ?? []) emitStructureElement(element, structRootNum);
624
+ }
625
+ if (residueXmpNum !== void 0 && doc.source?.xmp !== void 0) objects.push({
626
+ num: residueXmpNum,
627
+ value: require_objects.pdfStream(require_objects.pdfDict({
628
+ Type: require_objects.pdfName("Metadata"),
629
+ Subtype: require_objects.pdfName("XML")
630
+ }), new TextEncoder().encode(doc.source.xmp.xml))
631
+ });
430
632
  for (const [standardName, alloc] of fontAllocs) {
431
633
  const { font, descriptor } = buildFontObjects(standardName, require_objects.pdfRef(alloc.descNum, 0));
432
634
  objects.push({
@@ -521,6 +723,7 @@ function writePdf(doc, options = {}) {
521
723
  }
522
724
  if (imageAllocs.size > 0) resourceEntries.set("XObject", require_objects.pdfDict(new Map([...imageAllocs.values()].map((alloc) => [alloc.resourceName, require_objects.pdfRef(alloc.imageNum, 0)]))));
523
725
  const resourcesDict = require_objects.pdfDict(resourceEntries);
726
+ const markedStructureByPage = /* @__PURE__ */ new Map();
524
727
  const formulasByPage = /* @__PURE__ */ new Map();
525
728
  for (const formula of formulas) {
526
729
  const forPage = formulasByPage.get(formula.pageIndex);
@@ -557,7 +760,14 @@ function writePdf(doc, options = {}) {
557
760
  doc.pages.forEach((page, pageIndex) => {
558
761
  require_util_abort.throwIfAborted(options.signal);
559
762
  const { pageNum, contentsNum } = pageAllocs[pageIndex];
560
- const { bytes: contentBytes, substitutions, missingGlyphs } = require_content_write.writeContentStream(page.items, context);
763
+ let pageMcid = 0;
764
+ const pageContext = {
765
+ ...context,
766
+ nextMcid: () => pageMcid++,
767
+ layerObjectNumberOf: (name) => layerNumByName.get(name)
768
+ };
769
+ const { bytes: contentBytes, substitutions, missingGlyphs, markedStructure } = require_content_write.writeContentStream(page.items, pageContext);
770
+ if (markedStructure.length > 0) markedStructureByPage.set(pageIndex, [...markedStructure]);
561
771
  for (const substitution of substitutions) options.onSubstitution?.(substitution, { pageIndex });
562
772
  for (const missing of missingGlyphs) options.onMissingGlyph?.(missing, { pageIndex });
563
773
  const pageFormulas = formulasByPage.get(pageIndex);
@@ -587,11 +797,41 @@ function writePdf(doc, options = {}) {
587
797
  ["Contents", require_objects.pdfRef(contentsNum, 0)]
588
798
  ]);
589
799
  if (annots.length > 0) pageEntries.set("Annots", require_objects.pdfArray(annots));
800
+ if ((markedStructureByPage.get(pageIndex) ?? []).length > 0) pageEntries.set("StructParents", require_objects.pdfNum(pageIndex));
590
801
  objects.push({
591
802
  num: pageNum,
592
803
  value: require_objects.pdfDict(pageEntries)
593
804
  });
594
805
  });
806
+ if (structRootNum !== void 0 && structParentTreeNum !== void 0) {
807
+ const nums = [];
808
+ for (const [pageIndex, marks] of markedStructureByPage) {
809
+ const maxMcid = Math.max(...marks.map((mark) => mark.mcid));
810
+ const byMcid = Array.from({ length: maxMcid + 1 }, () => require_objects.pdfNull());
811
+ for (const mark of marks) {
812
+ const elementNum = structElementNumById.get(mark.structureId);
813
+ if (elementNum !== void 0) byMcid[mark.mcid] = require_objects.pdfRef(elementNum, 0);
814
+ }
815
+ nums.push(require_objects.pdfNum(pageIndex), require_objects.pdfArray(byMcid));
816
+ }
817
+ objects.push({
818
+ num: structParentTreeNum,
819
+ value: require_objects.pdfDict({ Nums: require_objects.pdfArray(nums) })
820
+ });
821
+ const rootEntries = [
822
+ ["Type", require_objects.pdfName("StructTreeRoot")],
823
+ ["K", require_objects.pdfArray((doc.structure ?? []).map((element) => {
824
+ const num = structElementNumById.get(element.id);
825
+ if (num === void 0) throw new Error(`structure element "${element.id}" was not allocated -- this is a writePdf internal invariant violation`);
826
+ return require_objects.pdfRef(num, 0);
827
+ }))],
828
+ ["ParentTree", require_objects.pdfRef(structParentTreeNum, 0)]
829
+ ];
830
+ objects.push({
831
+ num: structRootNum,
832
+ value: require_objects.pdfDict(Object.fromEntries(rootEntries))
833
+ });
834
+ }
595
835
  let fileId;
596
836
  let encryptedObjects = objects;
597
837
  if (options.encryption !== void 0 && encryptDictNum !== void 0) {
@@ -633,7 +873,7 @@ function writePdf(doc, options = {}) {
633
873
  if (fileId !== void 0 && encryptDictNum !== void 0) {
634
874
  trailerEntries.set("ID", require_objects.pdfArray([require_objects.pdfHexString(fileId), require_objects.pdfHexString(fileId)]));
635
875
  trailerEntries.set("Encrypt", require_objects.pdfRef(encryptDictNum, 0));
636
- }
876
+ } else if (trailerIdRestore !== void 0) trailerEntries.set("ID", trailerIdRestore);
637
877
  writer.writeAscii("trailer\n");
638
878
  require_serialize.writeObject(writer, require_objects.pdfDict(trailerEntries));
639
879
  writer.writeAscii("\nstartxref\n");
package/dist/write.js CHANGED
@@ -3,8 +3,10 @@ import { NOTES_ANNOTATION_AUTHOR } from "./notes-annotation-author.js";
3
3
  import { pdfArray, pdfDict, pdfHexString, pdfLiteralString, pdfName, pdfNull, pdfNum, pdfRef, pdfStream } from "./objects.js";
4
4
  import { ByteWriter, concatBytes } from "./bytes/writer.js";
5
5
  import { writeObject } from "./serialize.js";
6
+ import { ByteReader } from "./bytes/reader.js";
6
7
  import { deflate } from "./bytes/flate.js";
7
8
  import { base64ToBytes } from "./util/base64.js";
9
+ import { parseValue } from "./parse.js";
8
10
  import { throwIfAborted } from "./util/abort.js";
9
11
  import { randomBytes } from "./crypto/random.js";
10
12
  import { createStandardEncryptor, encryptIndirectObject } from "./encrypt-write.js";
@@ -251,6 +253,24 @@ function buildNotesAnnotDict(notes) {
251
253
  function xrefEntry(offset, generation, inUse) {
252
254
  return `${offset.toString().padStart(10, "0")} ${generation.toString().padStart(5, "0")} ${inUse ? "n" : "f"} \n`;
253
255
  }
256
+ function parseResidueRow(residue) {
257
+ const reader = new ByteReader(new TextEncoder().encode(residue.xml));
258
+ return parseValue(reader, () => {});
259
+ }
260
+ function objectContainsReference(obj) {
261
+ if (obj.kind === "ref") return true;
262
+ if (obj.kind === "array") return obj.items.some(objectContainsReference);
263
+ if (obj.kind === "dict") return [...obj.entries.values()].some(objectContainsReference);
264
+ if (obj.kind === "stream") return [...obj.dict.entries.values()].some(objectContainsReference);
265
+ return false;
266
+ }
267
+ function restoreResidueRow(source, key) {
268
+ const row = source?.[key];
269
+ if (row === void 0) return;
270
+ const parsed = parseResidueRow(row);
271
+ if (parsed === void 0 || objectContainsReference(parsed)) return;
272
+ return parsed;
273
+ }
254
274
  function writePdf(doc, options = {}) {
255
275
  const compress = options.compress ?? true;
256
276
  const registry = options.fonts;
@@ -339,10 +359,80 @@ function writePdf(doc, options = {}) {
339
359
  };
340
360
  for (let i = 0; i < countItems(doc.outline ?? []); i += 1) outlineItemNums.push(nextObjNum++);
341
361
  }
362
+ const layerNumByName = /* @__PURE__ */ new Map();
363
+ for (const layer of doc.layers ?? []) layerNumByName.set(layer.name, nextObjNum++);
364
+ const formObjectNums = [];
365
+ const countFieldObjects = (fields) => {
366
+ let n = 0;
367
+ for (const field of fields) n += 1 + (field.fieldType !== "group" && field.widgets.length > 1 ? field.widgets.length - 1 : 0) + countFieldObjects(field.children);
368
+ return n;
369
+ };
370
+ for (let i = 0; i < countFieldObjects(doc.form ?? []); i += 1) formObjectNums.push(nextObjNum++);
371
+ const formNumByField = /* @__PURE__ */ new Map();
372
+ const formExtraWidgetNums = /* @__PURE__ */ new Map();
373
+ let formNumCursor = 0;
374
+ const claimFormNums = (fields) => {
375
+ for (const field of fields) {
376
+ formNumByField.set(field, formObjectNums[formNumCursor++]);
377
+ if (field.fieldType !== "group" && field.widgets.length > 1) formExtraWidgetNums.set(field, field.widgets.slice(1).map(() => formObjectNums[formNumCursor++]));
378
+ claimFormNums(field.children);
379
+ }
380
+ };
381
+ claimFormNums(doc.form ?? []);
382
+ const formNumOf = (field) => {
383
+ const num = formNumByField.get(field);
384
+ if (num === void 0) throw new Error("AcroForm field object number was not claimed -- this is a writePdf internal invariant violation");
385
+ return num;
386
+ };
387
+ const structElementNumById = /* @__PURE__ */ new Map();
388
+ const countElements = (elements) => {
389
+ let n = 0;
390
+ for (const element of elements) {
391
+ structElementNumById.set(element.id, nextObjNum++);
392
+ n += 1 + countElements(element.children);
393
+ }
394
+ return n;
395
+ };
396
+ const structElementCount = countElements(doc.structure ?? []);
397
+ const structRootNum = structElementCount > 0 ? nextObjNum++ : void 0;
398
+ const structParentTreeNum = structElementCount > 0 ? nextObjNum++ : void 0;
399
+ const residueXmpNum = doc.source?.xmp !== void 0 ? nextObjNum++ : void 0;
342
400
  const objects = [];
343
401
  const catalogEntries = [["Type", pdfName("Catalog")], ["Pages", pdfRef(pagesNum, 0)]];
344
402
  if (attachmentsNamesNum !== void 0) catalogEntries.push(["Names", pdfRef(attachmentsNamesNum, 0)]);
345
403
  if (outlineRootNum !== void 0) catalogEntries.push(["Outlines", pdfRef(outlineRootNum, 0)]);
404
+ if (layerNumByName.size > 0) {
405
+ const ocgRefs = [...layerNumByName.values()].map((num) => pdfRef(num, 0));
406
+ const visible = [];
407
+ const hidden = [];
408
+ for (const layer of doc.layers ?? []) {
409
+ const num = layerNumByName.get(layer.name);
410
+ (layer.visible ? visible : hidden).push(pdfRef(num, 0));
411
+ }
412
+ const defaultConfigEntries = [];
413
+ if (visible.length > 0) defaultConfigEntries.push(["ON", pdfArray(visible)]);
414
+ if (hidden.length > 0) defaultConfigEntries.push(["OFF", pdfArray(hidden)]);
415
+ catalogEntries.push(["OCProperties", pdfDict({
416
+ OCGs: pdfArray(ocgRefs),
417
+ D: pdfDict(Object.fromEntries(defaultConfigEntries))
418
+ })]);
419
+ }
420
+ if (doc.form !== void 0 && doc.form.length > 0) catalogEntries.push(["AcroForm", pdfDict({ Fields: pdfArray(doc.form.map((field) => pdfRef(formNumOf(field), 0))) })]);
421
+ if (structRootNum !== void 0) catalogEntries.push(["StructTreeRoot", pdfRef(structRootNum, 0)]);
422
+ if (residueXmpNum !== void 0) catalogEntries.push(["Metadata", pdfRef(residueXmpNum, 0)]);
423
+ const trailerIdRestore = restoreResidueRow(doc.source, "trailer-id");
424
+ for (const [rowKey, catalogKey] of [
425
+ ["viewer-preferences", "ViewerPreferences"],
426
+ ["page-mode", "PageMode"],
427
+ ["page-layout", "PageLayout"],
428
+ ["output-intents", "OutputIntents"],
429
+ ["piece-info", "PieceInfo"],
430
+ ["legal", "Legal"],
431
+ ["collection", "Collection"]
432
+ ]) {
433
+ const restored = restoreResidueRow(doc.source, rowKey);
434
+ if (restored !== void 0) catalogEntries.push([catalogKey, restored]);
435
+ }
346
436
  objects.push({
347
437
  num: catalogNum,
348
438
  value: pdfDict(Object.fromEntries(catalogEntries))
@@ -426,6 +516,118 @@ function writePdf(doc, options = {}) {
426
516
  })
427
517
  });
428
518
  }
519
+ for (const layer of doc.layers ?? []) {
520
+ const num = layerNumByName.get(layer.name);
521
+ objects.push({
522
+ num,
523
+ value: pdfDict({
524
+ Type: pdfName("OCG"),
525
+ Name: pdfLiteralString(new TextEncoder().encode(layer.name))
526
+ })
527
+ });
528
+ }
529
+ const widgetRectArray = (widget) => pdfArray([
530
+ widget.xPt,
531
+ widget.yPt,
532
+ widget.xPt + widget.widthPt,
533
+ widget.yPt + widget.heightPt
534
+ ].map((n) => pdfNum(n)));
535
+ const widgetDict = (widget) => pdfDict({
536
+ Subtype: pdfName("Widget"),
537
+ Rect: widgetRectArray(widget),
538
+ P: pdfRef(pageAllocs[widget.pageIndex].pageNum, 0)
539
+ });
540
+ const FIELD_TYPE_PDF_NAME = {
541
+ text: "Tx",
542
+ checkbox: "Btn",
543
+ radio: "Btn",
544
+ button: "Btn",
545
+ listbox: "Ch",
546
+ combobox: "Ch",
547
+ signature: "Sig"
548
+ };
549
+ const emitFormFieldObjects = (fields, parentName) => {
550
+ for (const field of fields) {
551
+ const ownName = parentName.length > 0 && field.name.startsWith(`${parentName}.`) ? field.name.slice(parentName.length + 1) : field.name;
552
+ const entries = [];
553
+ if (ownName.length > 0) entries.push(["T", pdfLiteralString(new TextEncoder().encode(ownName))]);
554
+ if (field.alias !== void 0) entries.push(["TU", pdfLiteralString(new TextEncoder().encode(field.alias))]);
555
+ if (field.fieldType === "group") entries.push(["Kids", pdfArray(field.children.map((child) => pdfRef(formNumOf(child), 0)))]);
556
+ else {
557
+ entries.push(["FT", pdfName(FIELD_TYPE_PDF_NAME[field.fieldType])]);
558
+ const FLAG_READ_ONLY = 1;
559
+ const FLAG_PUSHBUTTON = 4;
560
+ const FLAG_RADIO = 32768;
561
+ const FLAG_COMBO = 131072;
562
+ let flags = 0;
563
+ if (field.readOnly === true) flags |= FLAG_READ_ONLY;
564
+ if (field.fieldType === "button") flags |= FLAG_PUSHBUTTON;
565
+ if (field.fieldType === "radio") flags |= FLAG_RADIO;
566
+ if (field.fieldType === "combobox") flags |= FLAG_COMBO;
567
+ if (flags !== 0) entries.push(["Ff", pdfNum(flags)]);
568
+ if (field.fieldType === "text" || field.fieldType === "listbox" || field.fieldType === "combobox") {
569
+ if (field.value !== void 0) entries.push(["V", pdfLiteralString(new TextEncoder().encode(field.value))]);
570
+ } else if (field.fieldType === "checkbox" || field.fieldType === "radio") entries.push(["V", pdfName(field.checked === false || field.value === void 0 ? field.value ?? (field.checked === true ? "Yes" : "Off") : field.value)]);
571
+ if (field.options !== void 0) entries.push(["Opt", pdfArray(field.options.map((option) => pdfLiteralString(new TextEncoder().encode(option))))]);
572
+ const firstWidget = field.widgets[0];
573
+ if (field.widgets.length === 1 && firstWidget !== void 0) {
574
+ entries.push(["Subtype", pdfName("Widget")]);
575
+ entries.push(["Rect", widgetRectArray(firstWidget)]);
576
+ entries.push(["P", pdfRef(pageAllocs[firstWidget.pageIndex].pageNum, 0)]);
577
+ } else if (field.widgets.length > 1 && firstWidget !== void 0) {
578
+ const extraNums = formExtraWidgetNums.get(field) ?? [];
579
+ entries.push(["Kids", pdfArray([widgetDict(firstWidget), ...extraNums.map((num) => pdfRef(num, 0))])]);
580
+ }
581
+ }
582
+ objects.push({
583
+ num: formNumOf(field),
584
+ value: pdfDict(Object.fromEntries(entries))
585
+ });
586
+ const extraNums = formExtraWidgetNums.get(field) ?? [];
587
+ for (const [index, num] of extraNums.entries()) {
588
+ const widget = field.widgets[index + 1];
589
+ objects.push({
590
+ num,
591
+ value: widgetDict(widget)
592
+ });
593
+ }
594
+ emitFormFieldObjects(field.children, field.name);
595
+ }
596
+ };
597
+ emitFormFieldObjects(doc.form ?? [], "");
598
+ if (structRootNum !== void 0 && structParentTreeNum !== void 0) {
599
+ const emitStructureElement = (element, parentNum) => {
600
+ const entries = [
601
+ ["Type", pdfName("StructElem")],
602
+ ["S", pdfName(element.type)],
603
+ ["P", pdfRef(parentNum, 0)]
604
+ ];
605
+ if (element.title !== void 0) entries.push(["T", pdfLiteralString(new TextEncoder().encode(element.title))]);
606
+ if (element.language !== void 0) entries.push(["Lang", pdfLiteralString(new TextEncoder().encode(element.language))]);
607
+ if (element.alt !== void 0) entries.push(["Alt", pdfLiteralString(new TextEncoder().encode(element.alt))]);
608
+ if (element.actualText !== void 0) entries.push(["ActualText", pdfLiteralString(new TextEncoder().encode(element.actualText))]);
609
+ if (element.children.length > 0) entries.push(["K", pdfArray(element.children.map((child) => {
610
+ const num = structElementNumById.get(child.id);
611
+ if (num === void 0) throw new Error(`structure element "${child.id}" was not allocated -- this is a writePdf internal invariant violation`);
612
+ return pdfRef(num, 0);
613
+ }))]);
614
+ const ownNum = structElementNumById.get(element.id);
615
+ if (ownNum === void 0) throw new Error(`structure element "${element.id}" was not allocated -- this is a writePdf internal invariant violation`);
616
+ objects.push({
617
+ num: ownNum,
618
+ value: pdfDict(Object.fromEntries(entries))
619
+ });
620
+ for (const child of element.children) emitStructureElement(child, ownNum);
621
+ };
622
+ for (const element of doc.structure ?? []) emitStructureElement(element, structRootNum);
623
+ }
624
+ if (residueXmpNum !== void 0 && doc.source?.xmp !== void 0) objects.push({
625
+ num: residueXmpNum,
626
+ value: pdfStream(pdfDict({
627
+ Type: pdfName("Metadata"),
628
+ Subtype: pdfName("XML")
629
+ }), new TextEncoder().encode(doc.source.xmp.xml))
630
+ });
429
631
  for (const [standardName, alloc] of fontAllocs) {
430
632
  const { font, descriptor } = buildFontObjects(standardName, pdfRef(alloc.descNum, 0));
431
633
  objects.push({
@@ -520,6 +722,7 @@ function writePdf(doc, options = {}) {
520
722
  }
521
723
  if (imageAllocs.size > 0) resourceEntries.set("XObject", pdfDict(new Map([...imageAllocs.values()].map((alloc) => [alloc.resourceName, pdfRef(alloc.imageNum, 0)]))));
522
724
  const resourcesDict = pdfDict(resourceEntries);
725
+ const markedStructureByPage = /* @__PURE__ */ new Map();
523
726
  const formulasByPage = /* @__PURE__ */ new Map();
524
727
  for (const formula of formulas) {
525
728
  const forPage = formulasByPage.get(formula.pageIndex);
@@ -556,7 +759,14 @@ function writePdf(doc, options = {}) {
556
759
  doc.pages.forEach((page, pageIndex) => {
557
760
  throwIfAborted(options.signal);
558
761
  const { pageNum, contentsNum } = pageAllocs[pageIndex];
559
- const { bytes: contentBytes, substitutions, missingGlyphs } = writeContentStream(page.items, context);
762
+ let pageMcid = 0;
763
+ const pageContext = {
764
+ ...context,
765
+ nextMcid: () => pageMcid++,
766
+ layerObjectNumberOf: (name) => layerNumByName.get(name)
767
+ };
768
+ const { bytes: contentBytes, substitutions, missingGlyphs, markedStructure } = writeContentStream(page.items, pageContext);
769
+ if (markedStructure.length > 0) markedStructureByPage.set(pageIndex, [...markedStructure]);
560
770
  for (const substitution of substitutions) options.onSubstitution?.(substitution, { pageIndex });
561
771
  for (const missing of missingGlyphs) options.onMissingGlyph?.(missing, { pageIndex });
562
772
  const pageFormulas = formulasByPage.get(pageIndex);
@@ -586,11 +796,41 @@ function writePdf(doc, options = {}) {
586
796
  ["Contents", pdfRef(contentsNum, 0)]
587
797
  ]);
588
798
  if (annots.length > 0) pageEntries.set("Annots", pdfArray(annots));
799
+ if ((markedStructureByPage.get(pageIndex) ?? []).length > 0) pageEntries.set("StructParents", pdfNum(pageIndex));
589
800
  objects.push({
590
801
  num: pageNum,
591
802
  value: pdfDict(pageEntries)
592
803
  });
593
804
  });
805
+ if (structRootNum !== void 0 && structParentTreeNum !== void 0) {
806
+ const nums = [];
807
+ for (const [pageIndex, marks] of markedStructureByPage) {
808
+ const maxMcid = Math.max(...marks.map((mark) => mark.mcid));
809
+ const byMcid = Array.from({ length: maxMcid + 1 }, () => pdfNull());
810
+ for (const mark of marks) {
811
+ const elementNum = structElementNumById.get(mark.structureId);
812
+ if (elementNum !== void 0) byMcid[mark.mcid] = pdfRef(elementNum, 0);
813
+ }
814
+ nums.push(pdfNum(pageIndex), pdfArray(byMcid));
815
+ }
816
+ objects.push({
817
+ num: structParentTreeNum,
818
+ value: pdfDict({ Nums: pdfArray(nums) })
819
+ });
820
+ const rootEntries = [
821
+ ["Type", pdfName("StructTreeRoot")],
822
+ ["K", pdfArray((doc.structure ?? []).map((element) => {
823
+ const num = structElementNumById.get(element.id);
824
+ if (num === void 0) throw new Error(`structure element "${element.id}" was not allocated -- this is a writePdf internal invariant violation`);
825
+ return pdfRef(num, 0);
826
+ }))],
827
+ ["ParentTree", pdfRef(structParentTreeNum, 0)]
828
+ ];
829
+ objects.push({
830
+ num: structRootNum,
831
+ value: pdfDict(Object.fromEntries(rootEntries))
832
+ });
833
+ }
594
834
  let fileId;
595
835
  let encryptedObjects = objects;
596
836
  if (options.encryption !== void 0 && encryptDictNum !== void 0) {
@@ -632,7 +872,7 @@ function writePdf(doc, options = {}) {
632
872
  if (fileId !== void 0 && encryptDictNum !== void 0) {
633
873
  trailerEntries.set("ID", pdfArray([pdfHexString(fileId), pdfHexString(fileId)]));
634
874
  trailerEntries.set("Encrypt", pdfRef(encryptDictNum, 0));
635
- }
875
+ } else if (trailerIdRestore !== void 0) trailerEntries.set("ID", trailerIdRestore);
636
876
  writer.writeAscii("trailer\n");
637
877
  writeObject(writer, pdfDict(trailerEntries));
638
878
  writer.writeAscii("\nstartxref\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-codec",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "Hand-written, dependency-minimal PDF codec: parses arbitrary real-world PDFs and generates new ones, built on its own codec-owned LayoutDocument item model and Zod 4 codecs.",
5
5
  "type": "module",
6
6
  "repository": {