modern-pdf-lib 0.30.0 → 0.31.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.
@@ -17060,7 +17060,7 @@ const SECTION_TYPES = /* @__PURE__ */ new Set([
17060
17060
  "BlockQuote"
17061
17061
  ]);
17062
17062
  /** Illustration types that require alt text per PDF/UA. */
17063
- const ILLUSTRATION_TYPES = /* @__PURE__ */ new Set([
17063
+ const ILLUSTRATION_TYPES$1 = /* @__PURE__ */ new Set([
17064
17064
  "Figure",
17065
17065
  "Formula",
17066
17066
  "Form"
@@ -17403,7 +17403,7 @@ function findPreviousHeadingInSection(current, sectionHeadings) {
17403
17403
  */
17404
17404
  function checkAltText(elements, errors) {
17405
17405
  for (const elem of elements) {
17406
- if (!ILLUSTRATION_TYPES.has(elem.type)) continue;
17406
+ if (!ILLUSTRATION_TYPES$1.has(elem.type)) continue;
17407
17407
  if (elem.options.artifact) continue;
17408
17408
  if (elem.options.altText === void 0 && elem.options.actualText === void 0) errors.push({
17409
17409
  code: "UA-STRUCT-005",
@@ -30416,6 +30416,531 @@ function buildBlackPointCompensationExtGState(mode) {
30416
30416
  return gs;
30417
30417
  }
30418
30418
  //#endregion
30419
+ //#region src/accessibility/taggingHelpers.ts
30420
+ /**
30421
+ * The property key under which {@link tagList} records the chosen
30422
+ * {@link ListNumbering} on a list element's `options` object.
30423
+ *
30424
+ * `StructureElementOptions` has no dedicated `listNumbering` field, so
30425
+ * the value is stored as an extra (string-keyed) property on the same
30426
+ * options object. When the structure tree is serialized this is the
30427
+ * value an attribute writer should emit as the list's `/ListNumbering`
30428
+ * entry inside an `/A` attribute dictionary whose owner is `/List`
30429
+ * (ISO 32000, Table 384):
30430
+ *
30431
+ * ```
30432
+ * /A << /O /List /ListNumbering /Decimal >>
30433
+ * ```
30434
+ *
30435
+ * Using a well-known string key (rather than a `Symbol`) keeps the
30436
+ * value plain-serializable and inspectable from tests.
30437
+ */
30438
+ const LIST_NUMBERING_KEY = "__listNumbering";
30439
+ /**
30440
+ * Heading level → structure-type mapping for {@link tagHeading}.
30441
+ *
30442
+ * Indexed by the numeric heading level (`1`..`6`); the result is the
30443
+ * corresponding standard heading structure type (`'H1'`..`'H6'`).
30444
+ */
30445
+ const HEADING_TYPE = {
30446
+ 1: "H1",
30447
+ 2: "H2",
30448
+ 3: "H3",
30449
+ 4: "H4",
30450
+ 5: "H5",
30451
+ 6: "H6"
30452
+ };
30453
+ /**
30454
+ * Tag a heading element of the given level.
30455
+ *
30456
+ * @param tree The structure tree to add the element to.
30457
+ * @param parent The parent element, or `null` to add under the root
30458
+ * `Document` element.
30459
+ * @param level The heading level (`1`..`6`); maps to `H1`..`H6`.
30460
+ * @param options Optional attributes (title, language, id, …).
30461
+ * @returns The newly created heading element (type `H1`..`H6`).
30462
+ *
30463
+ * @example
30464
+ * ```ts
30465
+ * const tree = doc.createStructureTree();
30466
+ * const h2 = tagHeading(tree, null, 2, { title: 'Background' });
30467
+ * // h2.type === 'H2'
30468
+ * ```
30469
+ */
30470
+ function tagHeading(tree, parent, level, options) {
30471
+ return tree.addElement(parent, HEADING_TYPE[level], options);
30472
+ }
30473
+ /**
30474
+ * Tag a paragraph (`P`) element.
30475
+ *
30476
+ * @param tree The structure tree to add the element to.
30477
+ * @param parent The parent element, or `null` for the root.
30478
+ * @param options Optional attributes.
30479
+ * @returns The newly created `P` element.
30480
+ */
30481
+ function tagParagraph(tree, parent, options) {
30482
+ return tree.addElement(parent, "P", options);
30483
+ }
30484
+ /**
30485
+ * Tag a figure (`Figure`) element with alternative text.
30486
+ *
30487
+ * The `altText` argument is required because PDF/UA mandates
30488
+ * alternative text for illustration elements. If the caller also
30489
+ * supplies `altText` in `options`, that explicit value takes
30490
+ * precedence over the positional argument.
30491
+ *
30492
+ * @param tree The structure tree to add the element to.
30493
+ * @param parent The parent element, or `null` for the root.
30494
+ * @param altText Alternative text describing the figure.
30495
+ * @param options Optional additional attributes.
30496
+ * @returns The newly created `Figure` element with `/Alt` set.
30497
+ */
30498
+ function tagFigure(tree, parent, altText, options) {
30499
+ const merged = {
30500
+ altText,
30501
+ ...options
30502
+ };
30503
+ return tree.addElement(parent, "Figure", merged);
30504
+ }
30505
+ /**
30506
+ * Tag a link (`Link`) element.
30507
+ *
30508
+ * @param tree The structure tree to add the element to.
30509
+ * @param parent The parent element, or `null` for the root.
30510
+ * @param options Optional attributes.
30511
+ * @returns The newly created `Link` element.
30512
+ */
30513
+ function tagLink(tree, parent, options) {
30514
+ return tree.addElement(parent, "Link", options);
30515
+ }
30516
+ /**
30517
+ * Tag a list (`L`) element and record its numbering style.
30518
+ *
30519
+ * The chosen {@link ListNumbering} is stored on the returned element's
30520
+ * `options` object under {@link LIST_NUMBERING_KEY} (since
30521
+ * `StructureElementOptions` has no dedicated field). A serializer can
30522
+ * emit it as the list's `/ListNumbering` attribute (ISO 32000,
30523
+ * Table 384) inside an `/A` dictionary owned by `/List`.
30524
+ *
30525
+ * @param tree The structure tree to add the element to.
30526
+ * @param parent The parent element, or `null` for the root.
30527
+ * @param numbering The list numbering style (default `'None'`).
30528
+ * @param options Optional additional attributes.
30529
+ * @returns The newly created `L` element.
30530
+ */
30531
+ function tagList(tree, parent, numbering = "None", options) {
30532
+ const list = tree.addElement(parent, "L", options);
30533
+ list.options[LIST_NUMBERING_KEY] = numbering;
30534
+ return list;
30535
+ }
30536
+ /**
30537
+ * Tag a list item under a list, building the conventional
30538
+ * `LI` → (`Lbl`, `LBody`) substructure.
30539
+ *
30540
+ * @param tree The structure tree to add the elements to.
30541
+ * @param list The parent `L` (list) element.
30542
+ * @param options Optional attributes for the `LI` element.
30543
+ * @returns The created `item` (`LI`), `label` (`Lbl`) and
30544
+ * `body` (`LBody`) elements.
30545
+ *
30546
+ * @example
30547
+ * ```ts
30548
+ * const list = tagList(tree, null, 'Decimal');
30549
+ * const { item, label, body } = tagListItem(tree, list);
30550
+ * // item.type === 'LI', label.type === 'Lbl', body.type === 'LBody'
30551
+ * ```
30552
+ */
30553
+ function tagListItem(tree, list, options) {
30554
+ const item = tree.addElement(list, "LI", options);
30555
+ return {
30556
+ item,
30557
+ label: tree.addElement(item, "Lbl"),
30558
+ body: tree.addElement(item, "LBody")
30559
+ };
30560
+ }
30561
+ /**
30562
+ * Tag a table (`Table`) element.
30563
+ *
30564
+ * @param tree The structure tree to add the element to.
30565
+ * @param parent The parent element, or `null` for the root.
30566
+ * @param options Optional attributes.
30567
+ * @returns The newly created `Table` element.
30568
+ */
30569
+ function tagTable(tree, parent, options) {
30570
+ return tree.addElement(parent, "Table", options);
30571
+ }
30572
+ /**
30573
+ * Tag a table row (`TR`) under a table.
30574
+ *
30575
+ * @param tree The structure tree to add the element to.
30576
+ * @param table The parent `Table` element.
30577
+ * @returns The newly created `TR` element.
30578
+ */
30579
+ function tagTableRow(tree, table) {
30580
+ return tree.addElement(table, "TR");
30581
+ }
30582
+ /**
30583
+ * Tag a table header cell (`TH`) under a row, recording its scope.
30584
+ *
30585
+ * The `scope` is stored in the element's options (`/Scope` attribute,
30586
+ * one of `Row`, `Column`, or `Both`) and is used by table-header
30587
+ * validation to associate header cells with the cells they describe.
30588
+ *
30589
+ * @param tree The structure tree to add the element to.
30590
+ * @param row The parent `TR` element.
30591
+ * @param scope The header scope (default `'Column'`).
30592
+ * @param options Optional additional attributes (e.g. colSpan/rowSpan).
30593
+ * @returns The newly created `TH` element with `scope` set.
30594
+ */
30595
+ function tagTableHeaderCell(tree, row, scope = "Column", options) {
30596
+ const merged = {
30597
+ ...options,
30598
+ scope
30599
+ };
30600
+ return tree.addElement(row, "TH", merged);
30601
+ }
30602
+ /**
30603
+ * Tag a table data cell (`TD`) under a row.
30604
+ *
30605
+ * @param tree The structure tree to add the element to.
30606
+ * @param row The parent `TR` element.
30607
+ * @param options Optional attributes (e.g. colSpan/rowSpan).
30608
+ * @returns The newly created `TD` element.
30609
+ */
30610
+ function tagTableDataCell(tree, row, options) {
30611
+ return tree.addElement(row, "TD", options);
30612
+ }
30613
+ //#endregion
30614
+ //#region src/accessibility/pdfUa2.ts
30615
+ /** The AIIM PDF/UA identification namespace URI (used in the XMP packet). */
30616
+ const PDFUA_ID_NS = "http://www.aiim.org/pdfua/ns/id/";
30617
+ /** The PDF/UA part identified by this module's XMP. */
30618
+ const PDFUA2_PART = 2;
30619
+ /**
30620
+ * The PDF/UA-2 revision year. ISO 14289-2 was published in 2024, which the
30621
+ * AIIM `pdfuaid:rev` field records as a four-digit year.
30622
+ */
30623
+ const PDFUA2_REV = 2024;
30624
+ /** Illustration types that require alternative text under PDF/UA-2. */
30625
+ const ILLUSTRATION_TYPES = /* @__PURE__ */ new Set([
30626
+ "Figure",
30627
+ "Formula",
30628
+ "Form"
30629
+ ]);
30630
+ /**
30631
+ * Validate a PDF document against PDF/UA-2 (ISO 14289-2) requirements.
30632
+ *
30633
+ * The check reuses {@link validatePdfUa} for the shared tagging, language,
30634
+ * MarkInfo, and metadata requirements, then layers the PDF 2.0 / UA-2
30635
+ * specific requirements on top:
30636
+ *
30637
+ * - **UA2-STRUCT-001** — a structure tree must exist (tagged PDF).
30638
+ * - **UA2-NS-001** — the structure tree must declare structure
30639
+ * `/Namespaces` (PDF/UA-2 builds on PDF 2.0 namespaces).
30640
+ * - **UA2-LANG-001** — the document must declare a natural language
30641
+ * (`/Lang`).
30642
+ * - **UA2-FIG-001** — every figure must carry alternative text.
30643
+ *
30644
+ * Each failure is mapped to an ISO 14289-2 clause string. The returned
30645
+ * result is `conformant` only when there are no issues.
30646
+ *
30647
+ * @param doc The PDF document to validate.
30648
+ * @returns A {@link PdfUa2Result} describing conformance and issues.
30649
+ *
30650
+ * @example
30651
+ * ```ts
30652
+ * import { createPdf } from 'modern-pdf-lib';
30653
+ * import { validatePdfUa2 } from 'modern-pdf-lib/accessibility';
30654
+ *
30655
+ * const doc = createPdf();
30656
+ * const result = validatePdfUa2(doc);
30657
+ * if (!result.conformant) {
30658
+ * for (const issue of result.issues) {
30659
+ * console.error(`[${issue.code}] ${issue.message} (§${issue.clause})`);
30660
+ * }
30661
+ * }
30662
+ * ```
30663
+ */
30664
+ function validatePdfUa2(doc) {
30665
+ const issues = [];
30666
+ const tree = doc.getStructureTree();
30667
+ if (!tree) issues.push({
30668
+ code: "UA2-STRUCT-001",
30669
+ message: "Document has no structure tree (/StructTreeRoot). PDF/UA-2 requires a fully tagged PDF 2.0 document with logical structure.",
30670
+ clause: "8.2"
30671
+ });
30672
+ if (!hasDeclaredNamespaces(tree)) issues.push({
30673
+ code: "UA2-NS-001",
30674
+ message: "Structure tree does not declare structure /Namespaces. PDF/UA-2 builds on PDF 2.0 structure namespaces (ISO 32000-2 §14.7.4); the StructTreeRoot must declare its namespaces.",
30675
+ clause: "8.2.2"
30676
+ });
30677
+ const lang = doc.getLanguage();
30678
+ if (lang === void 0 || lang.trim().length === 0) issues.push({
30679
+ code: "UA2-LANG-001",
30680
+ message: "Document has no natural language (/Lang). PDF/UA-2 requires the document catalog to specify a default natural language.",
30681
+ clause: "7.2"
30682
+ });
30683
+ if (tree) for (const elem of tree.getAllElements()) {
30684
+ if (!ILLUSTRATION_TYPES.has(elem.type)) continue;
30685
+ if (elem.options.artifact) continue;
30686
+ const alt = elem.options.altText;
30687
+ const actual = elem.options.actualText;
30688
+ const hasAlt = alt !== void 0 && alt.trim().length > 0;
30689
+ const hasActual = actual !== void 0 && actual.trim().length > 0;
30690
+ if (!hasAlt && !hasActual) issues.push({
30691
+ code: "UA2-FIG-001",
30692
+ message: `${elem.type} element has no /Alt or /ActualText. PDF/UA-2 requires alternative text for all non-decorative illustration elements.`,
30693
+ clause: "7.3"
30694
+ });
30695
+ }
30696
+ const ua1 = validatePdfUa(doc);
30697
+ for (const err of ua1.errors) {
30698
+ if (isCoveredByUa2(err.code)) continue;
30699
+ issues.push({
30700
+ code: `UA2-${err.code}`,
30701
+ message: err.message,
30702
+ clause: mapUa1Clause(err.clause)
30703
+ });
30704
+ }
30705
+ return {
30706
+ conformant: issues.length === 0,
30707
+ issues
30708
+ };
30709
+ }
30710
+ /**
30711
+ * Build an XMP packet (RDF/XML) that identifies a document as PDF/UA-2.
30712
+ *
30713
+ * The packet declares the AIIM PDF/UA identification schema
30714
+ * (`pdfuaid`, namespace `http://www.aiim.org/pdfua/ns/id/`) with:
30715
+ *
30716
+ * - `pdfuaid:part` = `2` — the PDF/UA part (ISO 14289-2);
30717
+ * - `pdfuaid:rev` = the revision year of the standard.
30718
+ *
30719
+ * The result is a serialized, packet-wrapped XMP string suitable for
30720
+ * embedding as the document's `/Metadata` stream.
30721
+ *
30722
+ * @returns The serialized PDF/UA-2 identification XMP packet.
30723
+ *
30724
+ * @example
30725
+ * ```ts
30726
+ * import { buildPdfUa2Xmp } from 'modern-pdf-lib/accessibility';
30727
+ *
30728
+ * doc.setXmpMetadata(buildPdfUa2Xmp());
30729
+ * ```
30730
+ */
30731
+ function buildPdfUa2Xmp() {
30732
+ return [
30733
+ "<?xpacket begin=\"\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>",
30734
+ "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\">",
30735
+ " <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">",
30736
+ ` <rdf:Description rdf:about=""`,
30737
+ ` xmlns:pdfuaid="${PDFUA_ID_NS}">`,
30738
+ ` <pdfuaid:part>${PDFUA2_PART}</pdfuaid:part>`,
30739
+ ` <pdfuaid:rev>${PDFUA2_REV}</pdfuaid:rev>`,
30740
+ " </rdf:Description>",
30741
+ " </rdf:RDF>",
30742
+ "</x:xmpmeta>",
30743
+ "<?xpacket end=\"w\"?>"
30744
+ ].join("\n");
30745
+ }
30746
+ /**
30747
+ * Determine whether a structure tree declares one or more structure
30748
+ * namespaces.
30749
+ *
30750
+ * The in-memory {@link PdfStructureTree} model does not yet carry a
30751
+ * first-class namespaces field, so callers declare namespaces by attaching
30752
+ * a `namespaces` array (of namespace URI strings, or namespace descriptor
30753
+ * objects) to the tree. This helper reads that field defensively and also
30754
+ * recognizes a per-element `namespace` option as evidence of namespace use.
30755
+ *
30756
+ * @internal
30757
+ */
30758
+ function hasDeclaredNamespaces(tree) {
30759
+ if (!tree) return false;
30760
+ const declared = tree.namespaces;
30761
+ if (Array.isArray(declared) && declared.length > 0) return true;
30762
+ for (const elem of tree.getAllElements()) {
30763
+ const ns = elem.options.namespace;
30764
+ if (typeof ns === "string" && ns.length > 0) return true;
30765
+ }
30766
+ return false;
30767
+ }
30768
+ /**
30769
+ * Whether a PDF/UA-1 error code is already represented by a dedicated
30770
+ * UA-2 issue produced above (to avoid duplicate structure-tree / language
30771
+ * findings).
30772
+ *
30773
+ * @internal
30774
+ */
30775
+ function isCoveredByUa2(ua1Code) {
30776
+ return ua1Code === "UA-STRUCT-001" || ua1Code === "UA-STRUCT-002" || ua1Code === "UA-META-001" || ua1Code === "UA-META-002" || ua1Code === "UA-META-003";
30777
+ }
30778
+ /**
30779
+ * Map a PDF/UA-1 (ISO 14289-1) clause reference onto the corresponding
30780
+ * PDF/UA-2 (ISO 14289-2) clause. The two standards share most clause
30781
+ * numbering; when a UA-1 clause is absent we fall back to the general
30782
+ * UA-2 conformance clause.
30783
+ *
30784
+ * @internal
30785
+ */
30786
+ function mapUa1Clause(ua1Clause) {
30787
+ if (ua1Clause === void 0 || ua1Clause.length === 0) return "5";
30788
+ return ua1Clause;
30789
+ }
30790
+ //#endregion
30791
+ //#region src/accessibility/autoTag.ts
30792
+ /** Default heading-detection scale relative to body text size. */
30793
+ const DEFAULT_HEADING_SCALE = 1.2;
30794
+ /**
30795
+ * Maximum vertical gap (in page-space units, as a fraction of the run's
30796
+ * font size) within which two text runs are considered to belong to the
30797
+ * same visual line. Runs whose baselines are within this fraction of an
30798
+ * em are merged onto one line.
30799
+ */
30800
+ const SAME_LINE_FRACTION = .5;
30801
+ /** Highest heading level tag supported. */
30802
+ const MAX_HEADING_LEVEL = 6;
30803
+ /**
30804
+ * Infer a coarse logical structure for a single untagged page and add the
30805
+ * inferred `H1`..`H6` and `P` elements to the document's structure tree.
30806
+ *
30807
+ * The procedure:
30808
+ * 1. Interpret the page into positioned text runs.
30809
+ * 2. Group runs into visual lines by their baseline y-position.
30810
+ * 3. Determine the body font size as the median run font size.
30811
+ * 4. Order lines top-to-bottom (reading order on a y-up page).
30812
+ * 5. Classify each line as a heading (font size `>= headingScale × body`)
30813
+ * or body text; map distinct heading sizes to `H1`..`H6`
30814
+ * (largest → `H1`).
30815
+ * 6. Emit one heading element per heading line and one `P` element per
30816
+ * contiguous block of body lines.
30817
+ *
30818
+ * Never throws on an empty (or text-free) page — it returns all-zero
30819
+ * counts and leaves the structure tree untouched.
30820
+ *
30821
+ * @param doc The document containing the page.
30822
+ * @param pageIndex Zero-based index of the page to tag.
30823
+ * @param options Optional heuristic tuning ({@link AutoTagOptions}).
30824
+ * @returns Counts of the elements that were added.
30825
+ */
30826
+ function autoTagPage(doc, pageIndex, options) {
30827
+ const headingScale = options?.headingScale ?? DEFAULT_HEADING_SCALE;
30828
+ const textItems = collectTextItems(doc, pageIndex);
30829
+ if (textItems.length === 0) return {
30830
+ headings: 0,
30831
+ paragraphs: 0,
30832
+ elements: 0
30833
+ };
30834
+ const lines = groupIntoLines(textItems);
30835
+ if (lines.length === 0) return {
30836
+ headings: 0,
30837
+ paragraphs: 0,
30838
+ elements: 0
30839
+ };
30840
+ const bodySize = medianFontSize(textItems);
30841
+ const ordered = [...lines].sort((a, b) => b.y - a.y);
30842
+ const headingThreshold = bodySize * headingScale;
30843
+ const headingLevelForSize = buildHeadingLevelMap(ordered, headingThreshold);
30844
+ const tree = doc.createStructureTree();
30845
+ let headings = 0;
30846
+ let paragraphs = 0;
30847
+ let paragraphOpen = false;
30848
+ for (const line of ordered) if (line.fontSize >= headingThreshold) {
30849
+ paragraphOpen = false;
30850
+ const type = `H${headingLevelForSize.get(line.fontSize) ?? 1}`;
30851
+ tree.addElement(null, type, { title: line.text });
30852
+ headings++;
30853
+ } else if (!paragraphOpen) {
30854
+ tree.addElement(null, "P");
30855
+ paragraphs++;
30856
+ paragraphOpen = true;
30857
+ }
30858
+ return {
30859
+ headings,
30860
+ paragraphs,
30861
+ elements: headings + paragraphs
30862
+ };
30863
+ }
30864
+ /**
30865
+ * Interpret the page and return only its text runs. Returns an empty
30866
+ * array (never throws) if the page cannot be interpreted.
30867
+ *
30868
+ * @internal
30869
+ */
30870
+ function collectTextItems(doc, pageIndex) {
30871
+ const pageCount = doc.getPageCount();
30872
+ if (pageIndex < 0 || pageIndex >= pageCount) return [];
30873
+ let items = [];
30874
+ try {
30875
+ items = interpretPage(doc.getPage(pageIndex)).items.filter((it) => it.type === "text" && it.text.trim().length > 0);
30876
+ } catch {
30877
+ return [];
30878
+ }
30879
+ return [...items];
30880
+ }
30881
+ /**
30882
+ * Group text runs into visual lines by their baseline y-position.
30883
+ *
30884
+ * The baseline is taken from the run's text-space → page-space transform
30885
+ * (`transform[5]`, the `f` translation component). Runs whose baselines
30886
+ * fall within {@link SAME_LINE_FRACTION} of an em of an existing line are
30887
+ * merged onto that line; the line's font size is the maximum run size.
30888
+ *
30889
+ * @internal
30890
+ */
30891
+ function groupIntoLines(items) {
30892
+ const sorted = [...items].sort((a, b) => b.transform[5] - a.transform[5]);
30893
+ const lines = [];
30894
+ for (const item of sorted) {
30895
+ const y = item.transform[5];
30896
+ const size = item.fontSize > 0 ? item.fontSize : Math.abs(item.transform[3]) || 1;
30897
+ const last = lines[lines.length - 1];
30898
+ const tolerance = size * SAME_LINE_FRACTION;
30899
+ if (last && Math.abs(last.y - y) <= tolerance) {
30900
+ last.text = `${last.text} ${item.text}`.trim();
30901
+ if (size > last.fontSize) last.fontSize = size;
30902
+ } else lines.push({
30903
+ y,
30904
+ fontSize: size,
30905
+ text: item.text.trim()
30906
+ });
30907
+ }
30908
+ return lines.map((l) => ({
30909
+ y: l.y,
30910
+ fontSize: l.fontSize,
30911
+ text: l.text
30912
+ }));
30913
+ }
30914
+ /**
30915
+ * Compute the median font size across all text runs. The median is more
30916
+ * robust than the mean against a handful of oversized heading runs, so it
30917
+ * gives a reliable estimate of the body text size.
30918
+ *
30919
+ * @internal
30920
+ */
30921
+ function medianFontSize(items) {
30922
+ const sizes = items.map((it) => it.fontSize > 0 ? it.fontSize : Math.abs(it.transform[3])).filter((s) => s > 0).sort((a, b) => a - b);
30923
+ if (sizes.length === 0) return 1;
30924
+ const mid = sizes.length >> 1;
30925
+ if (sizes.length % 2 === 1) return sizes[mid];
30926
+ return (sizes[mid - 1] + sizes[mid]) / 2;
30927
+ }
30928
+ /**
30929
+ * Build a map from each distinct heading font size to a heading level.
30930
+ * The largest heading size becomes `H1`, the next-largest `H2`, and so on,
30931
+ * capped at {@link MAX_HEADING_LEVEL} (`H6`).
30932
+ *
30933
+ * @internal
30934
+ */
30935
+ function buildHeadingLevelMap(lines, headingThreshold) {
30936
+ const distinct = [...new Set(lines.filter((l) => l.fontSize >= headingThreshold).map((l) => l.fontSize))].sort((a, b) => b - a);
30937
+ const map = /* @__PURE__ */ new Map();
30938
+ distinct.forEach((size, index) => {
30939
+ map.set(size, Math.min(index + 1, MAX_HEADING_LEVEL));
30940
+ });
30941
+ return map;
30942
+ }
30943
+ //#endregion
30419
30944
  //#region src/index.ts
30420
30945
  /** Whether initWasm has already completed successfully. */
30421
30946
  let wasmInitialized = false;
@@ -30461,6 +30986,6 @@ async function initWasm(options) {
30461
30986
  wasmInitialized = true;
30462
30987
  }
30463
30988
  //#endregion
30464
- export { decodeTileRegion as $, ChangeTracker as $a, addCounterSignature as $i, InvalidColorError as $n, encodeCode128Values as $r, buildType5Halftone as $t, extractEmbeddedRevocationData as A, PdfSquareAnnotation as Aa, getSupportedLevels as Ai, timestampPlugin as An, readEan13 as Ar, h as At, layoutParagraph as B, generateLineAppearance as Ba, flattenTransparency as Bi, removeAllBookmarks as Bn, upcAToOperators as Br, sampleShadingColor as Bt, generateThumbnail as C, PdfRedactAnnotation as Ca, stripProhibitedFeatures as Ci, buildRequirements as Cn, borderedPreset as Cr, isWoff2 as Ct, renderPageToImage as D, PdfLineAnnotation as Da, parseXmpMetadata as Di, tableToJson as Dn, readBarcode as Dr, buildPdfVtDParts as Dt, rasterize as E, PdfCircleAnnotation as Ea, extractXmpMetadata as Ei, tableToCsv as En, stripedPreset as Er, createWorkerPool as Et, validateKeyUsage as F, PdfFreeTextAnnotation as Fa, getToUnicodeCmap as Fi, batchFlatten as Fn, pdf417ToOperators as Fr, generateOrderX as Ft, downscale16To8 as G, PdfLinkAnnotation as Ga, isLinearized as Gi, BatchProcessingError as Gn, encodeEan8 as Gr, buildBoxDict as Gt, buildPdfXOutputIntent as H, generateSquigglyAppearance as Ha, validatePdfA as Hi, flattenField as Hn, ean13ToOperators as Hr, levenshtein as Ht, buildCertificateChain as I, generateCircleAppearance as Ia, buildOutputIntent as Ii, batchMerge as In, dataMatrixToOperators as Ir, generateXRechnungCii as It, offsetSignedToUnsigned as J, createXmpStream as Ja, findChangedObjects as Ji, ExceededMaxLengthError as Jn, code39ToOperators as Jr, validateBoxGeometry as Jt, getComponentDepths as K, PdfTextAnnotation as Ka, linearizePdf as Ki, CombedTextLayoutError as Kn, encodeItf as Kr, buildGtsPdfxVersion as Kt, validateCertificateChain as L, generateFreeTextAppearance as La, SRGB_ICC_PROFILE as Li, processBatch as Ln, encodeDataMatrix as Lr, buildPdfA4Xmp as Lt, EKU_OIDS as M, PdfSquigglyAnnotation as Ma, generateSymbolToUnicodeCmap as Mi, PdfDocumentBuilder as Mn, calculateBarcodeDimensions as Mr, createRangeFetcher as Mt, validateCertificatePolicy as N, PdfStrikeOutAnnotation as Na, generateWinAnsiToUnicodeCmap as Ni, enforcePdfUa as Nn, renderStyledBarcode as Nr, resolveFallback as Nt, interpretContentStream as O, PdfPolyLineAnnotation as Oa, validateXmpMetadata as Oi, accessibilityPlugin as On, readCode128 as Or, buildVtDpm as Ot, validateExtendedKeyUsage as P, PdfUnderlineAnnotation as Pa, generateZapfDingbatsToUnicodeCmap as Pi, validatePdfUa as Pn, encodePdf417 as Pr, splitByScript as Pt, decodeTile as Q, buildSeparationColorSpace as Qa, hasLtvData as Qi, ForeignPageError as Qn, encodeCode128 as Qr, buildType1Halftone as Qt, findHyphenationPoints as R, generateHighlightAppearance as Ra, generateSrgbIccProfile as Ri, addBookmark as Rn, calculateUpcCheckDigit as Rr, pdfA4Rules as Rt, extractImages as S, PdfPopupAnnotation as Sa, countOccurrences as Si, buildRequirement as Sn, applyTablePreset as Sr, isWoff as St, renderPageToCanvas as T, PdfStampAnnotation as Ta, createAssociatedFile as Ti, extractTables as Tn, professionalPreset as Tr, signDeferred as Tt, enforcePdfX as U, generateStrikeOutAppearance as Ua, delinearizePdf as Ui, flattenFields as Un, ean8ToOperators as Ur, renderCodeFrame as Ut, layoutTextFlow as V, generateSquareAppearance as Va, enforcePdfA as Vi, removeBookmark as Vn, calculateEanCheckDigit as Vr, didYouMean as Vt, validatePdfX as W, generateUnderlineAppearance as Wa, getLinearizationInfo as Wi, flattenForm as Wn, encodeEan13 as Wr, generateCiiXml as Wt, upscale8To16 as X, PDFOperator as Xa, buildDssDictionary as Xi, FieldExistsAsNonTerminalError as Xn, encodeCode39 as Xr, buildSampledTransferFunction as Xt, summarizeBitDepth as Y, parseXmpMetadata$1 as Ya, optimizeIncrementalSave as Yi, FieldAlreadyExistsError as Yn, computeCode39CheckDigit as Yr, STANDARD_SPOT_FUNCTIONS as Yt, assembleTiles as Z, buildDeviceNColorSpace as Za, embedLtvData as Zi, FontNotEmbeddedError as Zn, code128ToOperators as Zr, buildThresholdHalftone as Zt, redactRegions as _, parseTimestampResponse as _a, recompressImage as _i, MATHML_NAMESPACE as _n, estimateTextWidth as _r, formatDate as _t, buildStencilMask as a, detectModifications as aa, recompressWebP as ai, reconstructLines as an, RemovePageFromEmptyDocumentError as ar, isCertificateRevoked as at, comparePages as b, PdfFileAttachmentAnnotation as ba, generatePdfAXmp as bi, buildNamespacesArray as bn, wrapText$2 as br, formatNumber$1 as bt, buildEncryptedPayload as c, getCertificationLevel as ca, convertTiffCmykToRgb as ci, buildDocTimeStampDict as cn, UnexpectedFieldTypeError as cr, createSandbox as ct, buildPageOutputIntent as d, appendIncrementalUpdate as da, extractJpegMetadata as di, buildLab as dn, formatDate$1 as dr, setFieldValue as dt, getCounterSignatures as ea, valuesToModules as ei, identityTransferFunction as en, saveDocumentIncremental as eo, InvalidFieldNamePartError as er, parseTileInfo as et, attachAssociatedFiles as f, findExistingSignatures as fa, injectJpegMetadata as fi, labToRgb as fn, replaceTemplateVariables as fr, addVisibilityAction as ft, renderPageTile as g, buildTimestampRequest as ga, optimizeImage as gi, toSarif as gn, ellipsisText as gr, AFDate_FormatEx as gt, computeTileGrid as h, validateByteRangeIntegrity as ha, estimateJpegQuality as hi, toJsonReport as hn, applyOverflow as hr, AFSpecial_Format as ht, buildImageSoftMask as i, getFieldLocks as ia, encodePngFromPixels as ii, buildCollection as in, PluginError as ir, extractCrlUrls as it, verifyOfflineRevocation as j, PdfHighlightAnnotation as ja, isValidLevel as ji, StreamingPdfParser as jn, readEan8 as jr, renderToPdf as jt, interpretPage as k, PdfPolygonAnnotation as ka, getProfile as ki, metadataPlugin as kn, readCode39 as kr, gtsPdfVtVersion as kt, buildUnencryptedWrapper as l, setCertificationLevel as la, embedTiffCmyk as li, buildCalGray as ln, applyHeaderFooter as lr, getFieldValue as lt, RenderCache as m, saveIncrementalWithSignaturePreservation as ma, downscaleImage as mi, SARIF_SCHEMA_URI as mn, toRoman as mr, validateFieldValue as mt, buildBlackPointCompensationExtGState as n, addFieldLock as na, canDirectEmbed as ni, evaluateFunction as nn, MissingOnValueCheckError as nr, TrustStore as nt, buildSoftMaskGroupExtGState as o, MdpPermission as oa, webpToJpeg as oi, reconstructParagraphs as on, RichTextFieldReadError as or, checkCertificateStatus as ot, registerEmbeddedFile as p, parseExistingTrailer as pa, analyzeJpegMarkers as pi, DEFAULT_SARIF_TOOL_NAME as pn, toAlpha as pr, setFieldVisibility as pt, normalizeComponentDepth as q, buildXmpMetadata as qa, computeObjectHash as qi, EncryptedPdfError as qn, itfToOperators as qr, buildPdfX6OutputIntent as qt, buildColorKeyMask as r, buildFieldLockDict as ra, embedTiffDirect as ri, markdownToPdf as rn, NoSuchFieldError as rr, downloadCrl as rt, buildSoftMaskNone as s, buildDocMdpReference as sa, webpToPng as si, DEFAULT_DOC_TIMESTAMP_CONTENTS_SIZE as sn, StreamingParseError as sr, extractOcspUrl as st, initWasm as t, diffSignedContent as ta, PdfWorker as ti, nameHalftone as tn, saveIncremental as to, InvalidPageSizeError as tr, verifySignatureDetailed as tt, attachOutputIntents as u, validateSignatureChain as ua, isCmykTiff as ui, buildCalRGB as un, applyHeaderFooterToPage as ur, resolveFieldReference as ut, applyOcr as v, requestTimestamp as va, applyRedaction as vi, PDF2_NAMESPACE as vn, shrinkFontSize as vr, parseAcrobatDate as vt, renderDisplayListToCanvas as w, PdfInkAnnotation as wa, buildAfArray as wi, buildDPartRoot as wn, minimalPreset as wr, readWoffHeader as wt, extractFonts as x, PdfCaretAnnotation as xa, generatePdfAXmpBytes as xi, buildPieceInfo as xn, applyPreset as xr, decodeWoff as xt, compareImages as y, searchTextItems as ya, enforcePdfAFull as yi, buildNamespace as yn, truncateText as yr, AFNumber_Format as yt, layoutColumns as z, generateInkAppearance as za, detectTransparency as zi, getBookmarks as zn, encodeUpcA as zr, buildFunctionShading as zt };
30989
+ export { layoutColumns as $, generateInkAppearance as $a, detectTransparency as $i, getBookmarks as $n, encodeUpcA as $r, buildFunctionShading as $t, renderPageTile as A, buildTimestampRequest as Aa, optimizeImage as Ai, toSarif as An, ellipsisText as Ar, AFDate_FormatEx as At, rasterize as B, PdfCircleAnnotation as Ba, extractXmpMetadata as Bi, tableToCsv as Bn, stripedPreset as Br, createWorkerPool as Bt, buildUnencryptedWrapper as C, setCertificationLevel as Ca, embedTiffCmyk as Ci, buildCalGray as Cn, applyHeaderFooter as Cr, getFieldValue as Ct, registerEmbeddedFile as D, parseExistingTrailer as Da, analyzeJpegMarkers as Di, DEFAULT_SARIF_TOOL_NAME as Dn, toAlpha as Dr, setFieldVisibility as Dt, attachAssociatedFiles as E, findExistingSignatures as Ea, injectJpegMetadata as Ei, labToRgb as En, replaceTemplateVariables as Er, addVisibilityAction as Et, extractFonts as F, PdfCaretAnnotation as Fa, generatePdfAXmpBytes as Fi, buildPieceInfo as Fn, applyPreset as Fr, decodeWoff as Ft, verifyOfflineRevocation as G, PdfHighlightAnnotation as Ga, isValidLevel as Gi, StreamingPdfParser as Gn, readEan8 as Gr, renderToPdf as Gt, interpretContentStream as H, PdfPolyLineAnnotation as Ha, validateXmpMetadata as Hi, accessibilityPlugin as Hn, readCode128 as Hr, buildVtDpm as Ht, extractImages as I, PdfPopupAnnotation as Ia, countOccurrences as Ii, buildRequirement as In, applyTablePreset as Ir, isWoff as It, validateExtendedKeyUsage as J, PdfUnderlineAnnotation as Ja, generateZapfDingbatsToUnicodeCmap as Ji, validatePdfUa as Jn, encodePdf417 as Jr, splitByScript as Jt, EKU_OIDS as K, PdfSquigglyAnnotation as Ka, generateSymbolToUnicodeCmap as Ki, PdfDocumentBuilder as Kn, calculateBarcodeDimensions as Kr, createRangeFetcher as Kt, generateThumbnail as L, PdfRedactAnnotation as La, stripProhibitedFeatures as Li, buildRequirements as Ln, borderedPreset as Lr, isWoff2 as Lt, applyOcr as M, requestTimestamp as Ma, applyRedaction as Mi, PDF2_NAMESPACE as Mn, shrinkFontSize as Mr, parseAcrobatDate as Mt, compareImages as N, searchTextItems as Na, enforcePdfAFull as Ni, buildNamespace as Nn, truncateText as Nr, AFNumber_Format as Nt, RenderCache as O, saveIncrementalWithSignaturePreservation as Oa, downscaleImage as Oi, SARIF_SCHEMA_URI as On, toRoman as Or, validateFieldValue as Ot, comparePages as P, PdfFileAttachmentAnnotation as Pa, generatePdfAXmp as Pi, buildNamespacesArray as Pn, wrapText$2 as Pr, formatNumber$1 as Pt, findHyphenationPoints as Q, generateHighlightAppearance as Qa, generateSrgbIccProfile as Qi, addBookmark as Qn, calculateUpcCheckDigit as Qr, pdfA4Rules as Qt, renderDisplayListToCanvas as R, PdfInkAnnotation as Ra, buildAfArray as Ri, buildDPartRoot as Rn, minimalPreset as Rr, readWoffHeader as Rt, buildEncryptedPayload as S, getCertificationLevel as Sa, convertTiffCmykToRgb as Si, buildDocTimeStampDict as Sn, UnexpectedFieldTypeError as Sr, createSandbox as St, buildPageOutputIntent as T, appendIncrementalUpdate as Ta, extractJpegMetadata as Ti, buildLab as Tn, formatDate$1 as Tr, setFieldValue as Tt, interpretPage as U, PdfPolygonAnnotation as Ua, getProfile as Ui, metadataPlugin as Un, readCode39 as Ur, gtsPdfVtVersion as Ut, renderPageToImage as V, PdfLineAnnotation as Va, parseXmpMetadata as Vi, tableToJson as Vn, readBarcode as Vr, buildPdfVtDParts as Vt, extractEmbeddedRevocationData as W, PdfSquareAnnotation as Wa, getSupportedLevels as Wi, timestampPlugin as Wn, readEan13 as Wr, h as Wt, buildCertificateChain as X, generateCircleAppearance as Xa, buildOutputIntent as Xi, batchMerge as Xn, dataMatrixToOperators as Xr, generateXRechnungCii as Xt, validateKeyUsage as Y, PdfFreeTextAnnotation as Ya, getToUnicodeCmap as Yi, batchFlatten as Yn, pdf417ToOperators as Yr, generateOrderX as Yt, validateCertificateChain as Z, generateFreeTextAppearance as Za, SRGB_ICC_PROFILE as Zi, processBatch as Zn, encodeDataMatrix as Zr, buildPdfA4Xmp as Zt, buildColorKeyMask as _, buildFieldLockDict as _a, embedTiffDirect as _i, markdownToPdf as _n, NoSuchFieldError as _r, downloadCrl as _t, LIST_NUMBERING_KEY as a, isLinearized as aa, encodeEan8 as ai, buildBoxDict as an, PdfLinkAnnotation as ao, BatchProcessingError as ar, downscale16To8 as at, buildSoftMaskGroupExtGState as b, MdpPermission as ba, webpToJpeg as bi, reconstructParagraphs as bn, RichTextFieldReadError as br, checkCertificateStatus as bt, tagLink as c, findChangedObjects as ca, code39ToOperators as ci, validateBoxGeometry as cn, createXmpStream as co, ExceededMaxLengthError as cr, offsetSignedToUnsigned as ct, tagParagraph as d, embedLtvData as da, code128ToOperators as di, buildThresholdHalftone as dn, buildDeviceNColorSpace as do, FontNotEmbeddedError as dr, assembleTiles as dt, flattenTransparency as ea, upcAToOperators as ei, sampleShadingColor as en, generateLineAppearance as eo, removeAllBookmarks as er, layoutParagraph as et, tagTable as f, hasLtvData as fa, encodeCode128 as fi, buildType1Halftone as fn, buildSeparationColorSpace as fo, ForeignPageError as fr, decodeTile as ft, buildBlackPointCompensationExtGState as g, addFieldLock as ga, canDirectEmbed as gi, evaluateFunction as gn, MissingOnValueCheckError as gr, TrustStore as gt, tagTableRow as h, diffSignedContent as ha, PdfWorker as hi, nameHalftone as hn, saveIncremental as ho, InvalidPageSizeError as hr, verifySignatureDetailed as ht, validatePdfUa2 as i, getLinearizationInfo as ia, encodeEan13 as ii, generateCiiXml as in, generateUnderlineAppearance as io, flattenForm as ir, validatePdfX as it, redactRegions as j, parseTimestampResponse as ja, recompressImage as ji, MATHML_NAMESPACE as jn, estimateTextWidth as jr, formatDate as jt, computeTileGrid as k, validateByteRangeIntegrity as ka, estimateJpegQuality as ki, toJsonReport as kn, applyOverflow as kr, AFSpecial_Format as kt, tagList as l, optimizeIncrementalSave as la, computeCode39CheckDigit as li, STANDARD_SPOT_FUNCTIONS as ln, parseXmpMetadata$1 as lo, FieldAlreadyExistsError as lr, summarizeBitDepth as lt, tagTableHeaderCell as m, getCounterSignatures as ma, valuesToModules as mi, identityTransferFunction as mn, saveDocumentIncremental as mo, InvalidFieldNamePartError as mr, parseTileInfo as mt, autoTagPage as n, validatePdfA as na, ean13ToOperators as ni, levenshtein as nn, generateSquigglyAppearance as no, flattenField as nr, buildPdfXOutputIntent as nt, tagFigure as o, linearizePdf as oa, encodeItf as oi, buildGtsPdfxVersion as on, PdfTextAnnotation as oo, CombedTextLayoutError as or, getComponentDepths as ot, tagTableDataCell as p, addCounterSignature as pa, encodeCode128Values as pi, buildType5Halftone as pn, ChangeTracker as po, InvalidColorError as pr, decodeTileRegion as pt, validateCertificatePolicy as q, PdfStrikeOutAnnotation as qa, generateWinAnsiToUnicodeCmap as qi, enforcePdfUa as qn, renderStyledBarcode as qr, resolveFallback as qt, buildPdfUa2Xmp as r, delinearizePdf as ra, ean8ToOperators as ri, renderCodeFrame as rn, generateStrikeOutAppearance as ro, flattenFields as rr, enforcePdfX as rt, tagHeading as s, computeObjectHash as sa, itfToOperators as si, buildPdfX6OutputIntent as sn, buildXmpMetadata as so, EncryptedPdfError as sr, normalizeComponentDepth as st, initWasm as t, enforcePdfA as ta, calculateEanCheckDigit as ti, didYouMean as tn, generateSquareAppearance as to, removeBookmark as tr, layoutTextFlow as tt, tagListItem as u, buildDssDictionary as ua, encodeCode39 as ui, buildSampledTransferFunction as un, PDFOperator as uo, FieldExistsAsNonTerminalError as ur, upscale8To16 as ut, buildImageSoftMask as v, getFieldLocks as va, encodePngFromPixels as vi, buildCollection as vn, PluginError as vr, extractCrlUrls as vt, attachOutputIntents as w, validateSignatureChain as wa, isCmykTiff as wi, buildCalRGB as wn, applyHeaderFooterToPage as wr, resolveFieldReference as wt, buildSoftMaskNone as x, buildDocMdpReference as xa, webpToPng as xi, DEFAULT_DOC_TIMESTAMP_CONTENTS_SIZE as xn, StreamingParseError as xr, extractOcspUrl as xt, buildStencilMask as y, detectModifications as ya, recompressWebP as yi, reconstructLines as yn, RemovePageFromEmptyDocumentError as yr, isCertificateRevoked as yt, renderPageToCanvas as z, PdfStampAnnotation as za, createAssociatedFile as zi, extractTables as zn, professionalPreset as zr, signDeferred as zt };
30465
30990
 
30466
- //# sourceMappingURL=src-s9zNsZAT.mjs.map
30991
+ //# sourceMappingURL=src-B7SQNDvy.mjs.map