@reportwright/pdf 0.1.0-beta.0 → 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,6 +38,7 @@ writer to be ready. `pdf.end()` ends a Node stream and closes a `WritableStream`
38
38
  | option | meaning |
39
39
  |---|---|
40
40
  | `title`, `author`, `subject`, `keywords` | document info, mirrored in the XMP metadata (every value escaped) |
41
+ | `metadata` | `true` (default): an XMP metadata stream mirrors the info. `false`: the Info dictionary only, no XMP (a one-page invoice goes from 2.5 KB to 1.6 KB). Refused with `pdfa` or `tagged`: PDF/A and PDF/UA require the XMP |
41
42
  | `creationDate` | a `Date` (default: now). With the same input and date the output is byte-for-byte identical |
42
43
  | `compress` | `true` (default): content, fonts, object streams and the xref stream are Flate-compressed |
43
44
  | `pdfa` | `true`: PDF/A-2b (sRGB output intent bundled, XMP identification). Fonts must be embedded. `{ part, conformance, outputIntent }`: a level (see PDF/A levels) and/or your RGB or CMYK output profile instead of sRGB (a CMYK intent allows CMYK colour and refuses RGB) |
@@ -61,6 +62,9 @@ writer to be ready. `pdf.end()` ends a Node stream and closes a `WritableStream`
61
62
  CID-keyed CFF gets an identity charset so every reader maps its glyphs alike); `false` embeds the whole font; or a
62
63
  function `(font, codePoints, { glyphs }) => Promise<Uint8Array>` (e.g. a HarfBuzz subsetter, which also
63
64
  desubroutinises CFF) that must keep glyph IDs. `shaper`: see Shaping.
65
+ A variable font (an `fvar` table) is drawn at its default instance, which may be its thinnest weight (Noto Sans JP's
66
+ is Thin), with a warning naming the axes' values: for another weight, embed a static instance of it (fonttools
67
+ `varLib.instancer`, or `hb-subset --instance`).
64
68
  - `font.widthOfText(text, size, { kerning, characterSpacing, wordSpacing, horizontalScaling, direction })` — the width
65
69
  text is drawn at (kerned, shaped, spaced); `font.widthOf(text, size)` — plain advances; `font.lineHeight(size)` —
66
70
  ascent − descent + line gap (1.2 × size for standard fonts); `font.ascent`, `font.descent` (fractions of the size).
@@ -123,14 +127,31 @@ Text options (`page.text` and `page.textBox`):
123
127
  returns the glyphs in visual order, in font units: `[{ g, cl, ax, dx, dy }]` (glyph ID, cluster = UTF-16 index of
124
128
  its first character, x advance, offsets: what HarfBuzz's `hb_shape` gives). So Arabic, Indic and other complex
125
129
  scripts draw correctly. The glyphs are drawn by ID with their advances and offsets (TJ adjustments, `Ts` for vertical
126
- offsets); a glyph standing for its cluster's text maps it in ToUnicode (a ligature maps to all its letters); a cluster
127
- of several glyphs (reordered matras, conjuncts) or a glyph already standing for other text is wrapped in an
128
- `/ActualText` span, so copied text is the source text. The shaper's output is checked (glyph IDs in the font, finite
129
- numbers, clusters inside the text, at most 4 glyphs a character). `examples/harfbuzz-shaper.mjs` is an adapter for
130
- harfbuzzjs (not a dependency):
130
+ offsets); copied and extracted text is the source text, once, in logical order (checked with Acrobat-style
131
+ ActualText readers, poppler and pdf.js): a cluster of several glyphs (a conjunct, a reordered matra, a letter and its
132
+ dots) has its text on one glyph and WJ (U+2060) on the others, which are drawn after the run or inside an
133
+ `/ActualText` span, and a glyph that stands for different text in different places gets a code for each (a
134
+ `CIDToGIDMap`; a CFF font cannot, so there it gets an ActualText span). The shaper's output is checked (glyph IDs in
135
+ the font, finite numbers, clusters inside the text, at most 4 glyphs a character).
136
+
137
+ An adapter for harfbuzzjs v1 (`npm i harfbuzzjs`; not a dependency of this package). The same file ships in the package
138
+ as `examples/harfbuzz-shaper.mjs`:
131
139
 
132
140
  ```js
133
- import { harfbuzzShaper } from './harfbuzz-shaper.mjs'; // copy it from examples/
141
+ import * as hb from 'harfbuzzjs';
142
+ function harfbuzzShaper(bytes) {
143
+ const font = new hb.Font(new hb.Face(new hb.Blob(bytes))); // positions in font units
144
+ return (text, _font, { direction, kerning }) => {
145
+ const buf = new hb.Buffer();
146
+ buf.addText(text); // clusters are UTF-16 indices
147
+ buf.guessSegmentProperties(); // script and language from the text
148
+ buf.setDirection(direction === 'rtl' ? hb.Direction.RTL : hb.Direction.LTR);
149
+ hb.shape(font, buf, kerning ? [] : [new hb.Feature('kern', 0)]);
150
+ const pos = buf.getGlyphPositions();
151
+ return buf.getGlyphInfos().map((g, i) => ({ g: g.codepoint, cl: g.cluster, ax: pos[i].xAdvance, dx: pos[i].xOffset, dy: pos[i].yOffset }));
152
+ };
153
+ }
154
+
134
155
  const bytes = fs.readFileSync('NotoSansDevanagari-Regular.ttf');
135
156
  const hindi = await pdf.embedFont(bytes, { shaper: harfbuzzShaper(bytes) });
136
157
  page.text('किताब हिन्दी', { x: 40, y: 40, font: hindi, size: 18 });
@@ -657,7 +678,11 @@ const signed = await doc.saveIncremental({ sign: { field: { name: 'approval', pa
657
678
  sign: an empty signature field by name, or a new one (`{ name, page, x, y, width, height }`; no size: invisible), with
658
679
  group F's signers, the `/ByteRange` over the whole file including the original bytes. An encrypted file's new objects
659
680
  use its own AES key; RC4 files cannot be updated (writing RC4 is refused).
660
- - `mergePdfs(docs, options)`: every page of each, into a new file.
681
+ - `mergePdfs(docs, options)`: every page of each, into a new file. **A merged PDF/UA (or PDF/A-a) file is no longer
682
+ tagged**: the structure tree is kept only when every page comes from one document (and the form is not flattened),
683
+ so a merge drops it with `/MarkInfo` and `/Lang`, the XMP's PDF/UA identification (`pdfuaid`) goes too, PDF/A-a is
684
+ claimed as PDF/A-b, and `warnings` says "the structure tree (tags) was dropped … the result is not tagged (no
685
+ longer PDF/UA …)". Tag the merged content again with the writer if the result must be PDF/UA.
661
686
  - Errors from the input are `PdfReadError` with a `code`: `E_MALFORMED`, `E_BUDGET`, `E_DEPTH`, `E_CYCLE`,
662
687
  `E_TIMEOUT`, `E_ABORTED`, `E_PASSWORD`, `E_UNSUPPORTED`, `E_ACTIVE`.
663
688
 
@@ -689,7 +714,7 @@ JavaScript, launch and submit actions cannot be written at all (see Forms and Li
689
714
  |---|---|
690
715
  | Decoded stream bytes, the whole document / one stream (every filter and predictor counts as it produces, so a zip bomb stops at the cap: a 302-byte stream that inflates to 10 GB stops at 128 MB) | `maxDecoded` 512 MB / `maxStream` 128 MB |
691
716
  | Parsing steps (tokens, xref rows, tree nodes, operators); time | `maxSteps` 200 M; `timeoutMs`, `signal` |
692
- | Cross-reference entries and scanned objects held; one page's text pieces | `maxObjects` 5 M; `maxTextItems` 2 M |
717
+ | Cross-reference entries and scanned objects held; one page's text pieces | `maxObjects` 5 M; `maxTextItems` 1 M (and 16 M characters a page, ActualText included) |
693
718
  | Nesting of arrays and dictionaries (an explicit stack, never recursion) | 256 |
694
719
  | A chain of indirect references | 32 |
695
720
 
@@ -738,3 +763,11 @@ widgets may cover at most 40% of a page in all, and a widget with an opaque back
738
763
 
739
764
  Image masks (stencils) and colour-managed images (embedded ICC profiles of PNG/JPEG files are ignored); the Unicode
740
765
  bidi algorithm, hyphenation, kerning of the standard fonts, WOFF fonts; popup annotations; RC4 encryption (refused; read only), public-key (certificate) encryption (neither written nor read), verifying signatures' cryptography (they are listed with their coverage); PDF/A-1a, PDF/X, attachment annotations in PDF/A-3 (use `pdf.attach`), validating a Factur-X invoice's content; layout-perfect text extraction; page edits in an incremental update (use `save()`); reading encrypted files in browsers (WebCrypto has no synchronous AES; Node only). See NOTES.md for the plan. Extending the library: ARCHITECTURE.md.
766
+
767
+ ## Changes
768
+
769
+ 0.1.0-beta.1: shaped text (Indic, Arabic) extracts once and in order in pdf.js, poppler and `extractText`;
770
+ `extractText` honours ActualText, reads unmapped glyphs as U+FFFD, falls back to an embedded TrueType cmap, spaces
771
+ separate text objects (spreadsheet cells), orders a line along its baseline (rotated text too), and stops a
772
+ 2 M-operator page at 170 MB (`maxTextItems` 1 M); standard-font text writes 40% faster; variable fonts warn; the
773
+ package ships `examples/`. The list with details: NOTES.md ("Changes") in the repository.
package/dist/index.js CHANGED
@@ -1663,14 +1663,14 @@ function createDoc(out, o) {
1663
1663
  }
1664
1664
  await writeNodes(level2);
1665
1665
  const I = o.info, when = I.creationDate;
1666
- const xmp = doc.xmp ?? new TextEncoder().encode(xmpPacket({ title: I.title, author: I.author, subject: I.subject, keywords: I.keywords, stamp: when.toISOString().replace(/\.\d+Z$/, "Z"), pdfa: doc.pdfaLevel, ua: o.tagged, extensions: doc.xmpExtensions }));
1667
- const meta = objs.reserve();
1668
- await objs.write(meta, "<< /Type /Metadata /Subtype /XML >>", xmp);
1666
+ const xmp = o.metadata === false ? null : doc.xmp ?? new TextEncoder().encode(xmpPacket({ title: I.title, author: I.author, subject: I.subject, keywords: I.keywords, stamp: when.toISOString().replace(/\.\d+Z$/, "Z"), pdfa: doc.pdfaLevel, ua: o.tagged, extensions: doc.xmpExtensions }));
1667
+ const meta = xmp ? objs.reserve() : 0;
1668
+ if (xmp) await objs.write(meta, "<< /Type /Metadata /Subtype /XML >>", xmp);
1669
1669
  const info = objs.reserve();
1670
1670
  if (doc.info != null) await objs.write(info, doc.info);
1671
1671
  else await objs.write(info, `<< /Producer ${text(PRODUCER)} /Creator ${text(PRODUCER)}${I.title != null ? ` /Title ${text(I.title)}` : ""}${I.author != null ? ` /Author ${text(I.author)}` : ""}${I.subject != null ? ` /Subject ${text(I.subject)}` : ""}${I.keywords != null ? ` /Keywords ${text(I.keywords)}` : ""} /CreationDate ${pdfDate(when)} /ModDate ${pdfDate(when)} >>`);
1672
1672
  const root = objs.reserve();
1673
- let cat = `<< /Type /Catalog /Pages ${level2[0].ref} 0 R /Metadata ${meta} 0 R`;
1673
+ let cat = `<< /Type /Catalog /Pages ${level2[0].ref} 0 R${meta ? ` /Metadata ${meta} 0 R` : ""}`;
1674
1674
  for (const [k, v] of catalog) cat += ` ${pdfName(k)} ${v}`;
1675
1675
  await objs.write(root, cat + " >>");
1676
1676
  const r = await objs.finish({ root, info, id: o.id, last: doc.last });
@@ -2028,13 +2028,12 @@ function segments(str3, fonts, o) {
2028
2028
  }
2029
2029
  function runWidth(run2, size, sp) {
2030
2030
  let a = 0, n = 0, s = 0;
2031
- if (Array.isArray(run2)) {
2032
- n = run2.length;
2033
- for (const it of run2) {
2034
- a += it.a;
2035
- if (it.sp) s++;
2036
- }
2037
- } else ({ a, n, sp: s } = run2);
2031
+ if (Array.isArray(run2)) for (const it of run2) {
2032
+ a += it.a;
2033
+ if (it.sp) s++;
2034
+ if (!it.c) n++;
2035
+ }
2036
+ else ({ a, n, sp: s } = run2);
2038
2037
  return (a * size / 1e3 + n * sp.tc + s * sp.tw) * sp.tz / 100;
2039
2038
  }
2040
2039
  function measure(impl, str3, size, o) {
@@ -2042,11 +2041,11 @@ function measure(impl, str3, size, o) {
2042
2041
  const sp = spacing(o);
2043
2042
  return runWidth(impl.items(str3, { kerning: o?.kerning, rtl: o?.direction === "rtl", ws: sp.tw, measure: true }), size, sp);
2044
2043
  }
2045
- function runOps(run2, size, rise, tw, standard) {
2044
+ function runOps(run2, size, rise, tw, standard, tc = 0) {
2046
2045
  if (!Array.isArray(run2)) return run2.body ? `(${run2.body}) Tj
2047
2046
  ` : "";
2048
- const wsT = standard ? 0 : tw * 1e3 / size;
2049
- let s = "", arr = "", lit = "", cur = 0, pos = 0, r = rise, left = 0, adjusted = false;
2047
+ const wsT = standard ? 0 : tw * 1e3 / size, tcT = tc * 1e3 / size;
2048
+ let s = "", arr = "", lit = "", cur = 0, pos = 0, r = rise, left = 0, adjusted = false, out = false;
2050
2049
  const endLit = () => {
2051
2050
  if (lit) {
2052
2051
  arr += `(${lit})`;
@@ -2063,13 +2062,7 @@ function runOps(run2, size, rise, tw, standard) {
2063
2062
  adjusted = false;
2064
2063
  }
2065
2064
  };
2066
- for (const it of run2) {
2067
- if (it.at !== void 0) {
2068
- flush();
2069
- s += `/Span << /ActualText ${text(it.at)} >> BDC
2070
- `;
2071
- left = it.n || 1;
2072
- }
2065
+ const put = (it) => {
2073
2066
  const want = rise + it.dy * size / 1e3;
2074
2067
  if (Math.abs(want - r) >= 5e-4) {
2075
2068
  flush();
@@ -2087,6 +2080,20 @@ function runOps(run2, size, rise, tw, standard) {
2087
2080
  }
2088
2081
  lit += it.b;
2089
2082
  cur += it.w;
2083
+ };
2084
+ for (const it of run2) {
2085
+ if (it.c) {
2086
+ pos += it.a;
2087
+ out = true;
2088
+ continue;
2089
+ }
2090
+ if (it.at !== void 0) {
2091
+ flush();
2092
+ s += `/Span << /ActualText ${text(it.at)} >> BDC
2093
+ `;
2094
+ left = it.n || 1;
2095
+ }
2096
+ put(it);
2090
2097
  pos += it.a + (it.sp ? wsT : 0);
2091
2098
  if (left && --left === 0) {
2092
2099
  flush();
@@ -2094,6 +2101,23 @@ function runOps(run2, size, rise, tw, standard) {
2094
2101
  }
2095
2102
  }
2096
2103
  flush();
2104
+ if (out) {
2105
+ s += `/Span << /ActualText ${text("")} >> BDC
2106
+ 0 0 Td
2107
+ `;
2108
+ cur = pos = 0;
2109
+ for (const it of run2) {
2110
+ if (!it.c) {
2111
+ pos += it.a + (it.sp ? wsT : 0) + tcT;
2112
+ continue;
2113
+ }
2114
+ put(it);
2115
+ cur += tcT;
2116
+ pos += it.a;
2117
+ }
2118
+ flush();
2119
+ s += "EMC\n";
2120
+ }
2097
2121
  if (r !== rise) s += `${num(rise)} Ts
2098
2122
  `;
2099
2123
  return s;
@@ -2148,7 +2172,7 @@ function drawLine(page, fonts, str3, x2, base3, size, ts, o) {
2148
2172
  }
2149
2173
  let at = x2;
2150
2174
  for (const s of segs) {
2151
- const ops = runOps(s.run, size, ts.rise, ts.sp.tw, s.impl.standard);
2175
+ const ops = runOps(s.run, size, ts.rise, ts.sp.tw, s.impl.standard, ts.sp.tc);
2152
2176
  if (ops) c.run(s.font.name, size, at, page.height - base3, ops, ts === PLAIN ? PLAIN_STATE : { Tc: ts.sp.tc, Tw: s.impl.standard ? ts.sp.tw : 0, Tz: ts.sp.tz, Ts: ts.rise, Tr: ts.Tr });
2153
2177
  if (segs.length > 1) at += runWidth(s.run, size, ts.sp);
2154
2178
  }
@@ -10009,6 +10033,13 @@ function standardFont(doc, key) {
10009
10033
  /** @type {number[]} */
10010
10034
  m.widths
10011
10035
  );
10036
+ const W8 = new Float64Array(256), SAME = new Uint8Array(256);
10037
+ for (let c = 0; c < 256; c++) {
10038
+ const code = codeOf.get(c);
10039
+ W8[c] = code === void 0 ? -1 : widths[code] || 0;
10040
+ SAME[c] = code === c && esc(code) === String.fromCharCode(c) ? 1 : 0;
10041
+ }
10042
+ const SP32 = codeOf.get(32) === 32;
10012
10043
  const ref = doc.reserve();
10013
10044
  const missing = /* @__PURE__ */ new Set();
10014
10045
  const memo2 = /* @__PURE__ */ new Map();
@@ -10068,8 +10099,31 @@ function standardFont(doc, key) {
10068
10099
  lineHeight: 1.2,
10069
10100
  has: (cp) => codeOf.has(cp),
10070
10101
  items(text3, o) {
10071
- let body = "", a = 0, n = 0, sp = 0;
10072
10102
  const draw = !o.measure;
10103
+ if (!o.rtl) {
10104
+ let a2 = 0, n2 = 0, sp2 = 0, same = true;
10105
+ for (let i2 = 0; i2 < text3.length; i2++) {
10106
+ const c = text3.charCodeAt(i2);
10107
+ if (c > 255) {
10108
+ same = false;
10109
+ break;
10110
+ }
10111
+ const w = W8[c];
10112
+ if (w < 0) {
10113
+ same = false;
10114
+ break;
10115
+ }
10116
+ a2 += w;
10117
+ n2++;
10118
+ if (c === 32 && SP32) sp2++;
10119
+ if (!SAME[c]) {
10120
+ same = false;
10121
+ break;
10122
+ }
10123
+ }
10124
+ if (same) return { body: draw ? text3 : "", a: a2, n: n2, sp: sp2 };
10125
+ }
10126
+ let body = "", a = 0, n = 0, sp = 0;
10073
10127
  for (const ch of o.rtl ? [...text3].reverse() : text3) {
10074
10128
  const b = byte(ch);
10075
10129
  if (!b) {
@@ -10938,9 +10992,15 @@ function embedFont(doc, input, opt = {}) {
10938
10992
  use(0);
10939
10993
  const two = (g) => esc(g >> 8) + esc(g & 255);
10940
10994
  const kern = kerning(f.tables, f.numGlyphs, (m) => doc.warn(`${f.postscriptName.slice(0, 63)}: ${m}`));
10995
+ const axes = variation(f.tables.get("fvar"));
10996
+ if (axes) doc.warn(`${f.postscriptName.slice(0, 63)} is a variable font: it is drawn at its default instance (${axes}); to draw another weight or width, embed a static instance of it (fonttools varLib.instancer, or HarfBuzz's hb-subset --instance)`);
10941
10997
  const k = 1e3 / f.unitsPerEm;
10942
10998
  const ref = doc.reserve();
10943
- const glyphs2 = /* @__PURE__ */ new Map();
10999
+ const codes = /* @__PURE__ */ new Map();
11000
+ const codeOf = /* @__PURE__ */ new Map();
11001
+ const copies = !f.cff;
11002
+ const EXACT = [];
11003
+ let next = f.numGlyphs;
10944
11004
  const missing = /* @__PURE__ */ new Set();
10945
11005
  const memo2 = /* @__PURE__ */ new Map();
10946
11006
  const strict = doc.pdfa || doc.tagged;
@@ -10957,24 +11017,31 @@ function embedFont(doc, input, opt = {}) {
10957
11017
  );
10958
11018
  if (!g) lack(ch);
10959
11019
  if (!g && missing.size < 100) missing.add(ch);
10960
- if (!glyphs2.has(g) && (g || !strict)) {
10961
- use(g);
10962
- glyphs2.set(g, g ? ch : "");
10963
- }
10964
- memo2.set(ch, b = !g && strict ? "" : two(g));
11020
+ memo2.set(ch, b = !g && strict ? "" : two(code(g, ch)[0]));
10965
11021
  }
10966
11022
  return b;
10967
11023
  };
10968
- const record = (g, t) => {
11024
+ const code = (g, t) => {
10969
11025
  const m = t.length <= 32 ? t : String.fromCodePoint(
10970
11026
  /** @type {number} */
10971
11027
  t.codePointAt(0)
10972
11028
  );
10973
- if (!glyphs2.has(g)) {
10974
- use(g);
10975
- glyphs2.set(g, g ? m : "");
11029
+ if (codes.get(g)?.t === m) return m === t ? EXACT[g] ??= [g, true] : [g, false];
11030
+ let c = codeOf.get(`${g}:${m}`);
11031
+ if (c === void 0) {
11032
+ if (!codes.has(g)) {
11033
+ use(g);
11034
+ c = g;
11035
+ } else if (copies && next <= 65535) c = next++;
11036
+ else return [
11037
+ g,
11038
+ /** @type {any} */
11039
+ codes.get(g).t === t
11040
+ ];
11041
+ codes.set(c, { g, t: m });
11042
+ codeOf.set(`${g}:${m}`, c);
10976
11043
  }
10977
- return glyphs2.get(g) === t;
11044
+ return [c, m === t];
10978
11045
  };
10979
11046
  let handle;
10980
11047
  const items = (text3, o) => {
@@ -11025,9 +11092,9 @@ function embedFont(doc, input, opt = {}) {
11025
11092
  if (missing.size < 100 && !o.measure) missing.add(ch);
11026
11093
  if (strict) continue;
11027
11094
  }
11028
- const ok = o.measure || record(g, ch), w = W(g);
11095
+ const [c, ok] = o.measure ? [g, true] : code(g, ch), w = W(g);
11029
11096
  if (kerned && prev >= 0) out[out.length - 1].a += kern(prev, g) * k;
11030
- out.push({ b: two(g), w, a: w, dx: 0, dy: 0, sp: ch === " ", at: ok ? void 0 : ch, n: 1 });
11097
+ out.push({ b: two(c), w, a: w, dx: 0, dy: 0, sp: ch === " ", at: ok ? void 0 : ch, n: 1 });
11031
11098
  prev = g;
11032
11099
  }
11033
11100
  return out;
@@ -11051,33 +11118,61 @@ function embedFont(doc, input, opt = {}) {
11051
11118
  });
11052
11119
  const starts = [...new Set(gl.map((x2) => x2.cl))].sort((a, b) => a - b);
11053
11120
  const end = new Map(starts.map((c, i2) => [c, starts[i2 + 1] ?? text3.length]));
11054
- const out = [];
11121
+ const out = [], said = /* @__PURE__ */ new Set();
11122
+ const at = (a) => {
11123
+ let b = a;
11124
+ while (b < gl.length && gl[b].cl === gl[a].cl) b++;
11125
+ return [b, said.has(gl[a].cl) ? "" : text3.slice(gl[a].cl, end.get(gl[a].cl))];
11126
+ };
11127
+ let glued = null;
11055
11128
  for (let i2 = 0; i2 < gl.length; ) {
11056
- let j = i2;
11057
- while (j < gl.length && gl[j].cl === gl[i2].cl) j++;
11058
- const t = text3.slice(gl[i2].cl, end.get(gl[i2].cl)), first = out.length;
11059
- let ok = j - i2 === 1;
11129
+ const [j, t0] = at(i2), cl = gl[i2].cl, first = out.length;
11130
+ if (glued && t0 === " " && j - i2 === 1) {
11131
+ const { g, ax, dx, dy } = gl[i2];
11132
+ out.push({ b: two(o.measure ? g : code(g, "⁠")[0]), w: W(g), a: ax * k, dx: dx * k, dy: dy * k, sp: true });
11133
+ if (glued.n) glued.n++;
11134
+ glued = null;
11135
+ said.add(cl);
11136
+ i2 = j;
11137
+ continue;
11138
+ }
11139
+ glued = null;
11140
+ const glue = !o.rtl && !!t0 && t0 !== " " && j < gl.length && at(j)[1] === " " && at(j)[0] - j === 1 && at(j)[0] < gl.length && MN.test(at(at(j)[0])[1]);
11141
+ const t = glue ? `${t0} ` : t0;
11142
+ let lead = -1, span = glue;
11143
+ if (t0) {
11144
+ for (let q = i2; q < j; q++) if ((gl[q].g || !strict) && (lead < 0 || gl[q].ax > gl[lead].ax)) lead = q;
11145
+ }
11060
11146
  for (let q = i2; q < j; q++) {
11061
11147
  const { g, ax, dx, dy } = gl[q];
11062
11148
  if (!g) {
11063
11149
  if (!o.measure) lack(t);
11064
- if (missing.size < 100 && !o.measure) missing.add(t);
11150
+ if (missing.size < 100 && !o.measure && t) missing.add(t);
11065
11151
  if (strict) {
11066
11152
  if (out.length > first) out[out.length - 1].a += ax * k;
11067
11153
  continue;
11068
11154
  }
11069
11155
  }
11070
- ok = (o.measure || record(g, t)) && ok;
11071
- out.push({ b: two(g), w: W(g), a: ax * k, dx: dx * k, dy: dy * k, sp: t === " " });
11072
- }
11073
- if (out.length > first && !ok) {
11074
- out[first].at = t;
11075
- out[first].n = out.length - first;
11156
+ const [c, exact] = o.measure ? [g, true] : code(g, q !== lead ? "⁠" : o.rtl && RTL3.test(t) ? [...t].reverse().join("") : t);
11157
+ if (!exact) span = true;
11158
+ if (q !== lead && Math.abs(ax * k) >= 1) span ||= !o.rtl;
11159
+ out.push({ b: two(c), w: W(g), a: ax * k, dx: dx * k, dy: dy * k, sp: q === lead && t === " ", c: q !== lead && Math.abs(ax * k) < 1 || void 0 });
11076
11160
  }
11161
+ if (out.length > first) said.add(cl);
11162
+ if (span && !o.measure) {
11163
+ const flow = out.slice(first).filter((x2) => !x2.c);
11164
+ if (flow.length) {
11165
+ flow[0].at = t;
11166
+ flow[0].n = flow.length;
11167
+ if (glue) glued = flow[0];
11168
+ }
11169
+ } else if (glue) glued = /** @type {any} */
11170
+ {};
11077
11171
  i2 = j;
11078
11172
  }
11079
11173
  return out;
11080
11174
  };
11175
+ const MN = /\p{Mn}/u, RTL3 = /[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufeff]/;
11081
11176
  const impl = {
11082
11177
  standard: false,
11083
11178
  items,
@@ -11114,12 +11209,12 @@ function embedFont(doc, input, opt = {}) {
11114
11209
  return s;
11115
11210
  },
11116
11211
  async write() {
11117
- if (!glyphs2.size) glyphs2.set(0, "");
11118
- const used = [...glyphs2.keys()].sort((a, b) => a - b);
11119
- const cps = [...glyphs2.values()].filter(Boolean).flatMap((t) => [...t].map((c) => (
11212
+ const cids = [...codes.keys()].sort((a, b) => a - b);
11213
+ const used = [.../* @__PURE__ */ new Set([0, ...[...codes.values()].map((x2) => x2.g)])].sort((a, b) => a - b);
11214
+ const cps = [...new Set([...codes.values()].flatMap((x2) => [...x2.t].map((c) => (
11120
11215
  /** @type {number} */
11121
11216
  c.codePointAt(0)
11122
- )));
11217
+ ))))];
11123
11218
  let file = bytes;
11124
11219
  if (sub && !(f.fsType & 256)) {
11125
11220
  if (typeof sub === "function") {
@@ -11144,8 +11239,8 @@ function embedFont(doc, input, opt = {}) {
11144
11239
  ) : file);
11145
11240
  let cidSet = "";
11146
11241
  if (subset && doc.pdfaLevel?.part === 1) {
11147
- const bits2 = new Uint8Array((used[used.length - 1] >> 3) + 1), n = doc.reserve();
11148
- for (const g of used) bits2[g >> 3] |= 128 >> (g & 7);
11242
+ const all = [.../* @__PURE__ */ new Set([...used, ...cids])], bits2 = new Uint8Array((Math.max(...all) >> 3) + 1), n = doc.reserve();
11243
+ for (const g of all) bits2[g >> 3] |= 128 >> (g & 7);
11149
11244
  await doc.writeStream(n, "<< >>", bits2);
11150
11245
  cidSet = ` /CIDSet ${n} 0 R`;
11151
11246
  }
@@ -11153,16 +11248,27 @@ function embedFont(doc, input, opt = {}) {
11153
11248
  const flags = (f.fixedPitch ? 1 : 0) | 4 | (f.italicAngle ? 64 : 0);
11154
11249
  const [x0, y0, x1, y1] = f.bbox;
11155
11250
  await doc.write(desc, `<< /Type /FontDescriptor /FontName ${base3} /Flags ${flags} /FontBBox [${s(x0)} ${s(y0)} ${s(x1)} ${s(y1)}] /ItalicAngle ${num(f.italicAngle)} /Ascent ${s(f.ascent)} /Descent ${s(f.descent)} /CapHeight ${s(f.capHeight)} /XHeight ${s(f.xHeight)} /StemV 0 /${cff ? "FontFile3" : "FontFile2"} ${fileRef} 0 R${cidSet} >>`);
11156
- const codes = used.map((g) => [g, g]);
11251
+ const wc = (cids.length ? cids : [0]).map((c) => [c, codes.get(c)?.g ?? 0]);
11157
11252
  let Wa = "";
11158
- for (let i2 = 0; i2 < codes.length; ) {
11253
+ for (let i2 = 0; i2 < wc.length; ) {
11159
11254
  let j = i2;
11160
- while (j + 1 < codes.length && codes[j + 1][0] === codes[j][0] + 1) j++;
11161
- Wa += `${codes[i2][0]} [${codes.slice(i2, j + 1).map(([, g]) => s(f.advance(g))).join(" ")}] `;
11255
+ while (j + 1 < wc.length && wc[j + 1][0] === wc[j][0] + 1) j++;
11256
+ Wa += `${wc[i2][0]} [${wc.slice(i2, j + 1).map(([, g]) => s(f.advance(g))).join(" ")}] `;
11162
11257
  i2 = j + 1;
11163
11258
  }
11164
- await doc.write(cid, `<< /Type /Font /Subtype /${cff ? "CIDFontType0" : "CIDFontType2"} /BaseFont ${base3} /CIDSystemInfo << /Registry ${ascii("Adobe")} /Ordering ${ascii("Identity")} /Supplement 0 >> /FontDescriptor ${desc} 0 R /W [${Wa}]${cff ? "" : " /CIDToGIDMap /Identity"} >>`);
11165
- await doc.writeStream(cmap, "<< >>", latin1(toUnicode(glyphs2)));
11259
+ let c2g = " /CIDToGIDMap /Identity";
11260
+ if (!cff && next > f.numGlyphs) {
11261
+ const m = new Uint8Array(2 * next), r = doc.reserve();
11262
+ for (let c = 0; c < next; c++) {
11263
+ const g = c < f.numGlyphs ? c : codes.get(c)?.g ?? 0;
11264
+ m[2 * c] = g >> 8;
11265
+ m[2 * c + 1] = g & 255;
11266
+ }
11267
+ await doc.writeStream(r, "<< >>", m);
11268
+ c2g = ` /CIDToGIDMap ${r} 0 R`;
11269
+ }
11270
+ await doc.write(cid, `<< /Type /Font /Subtype /${cff ? "CIDFontType0" : "CIDFontType2"} /BaseFont ${base3} /CIDSystemInfo << /Registry ${ascii("Adobe")} /Ordering ${ascii("Identity")} /Supplement 0 >> /FontDescriptor ${desc} 0 R /W [${Wa}]${cff ? "" : c2g} >>`);
11271
+ await doc.writeStream(cmap, "<< >>", latin1(toUnicode(codes)));
11166
11272
  await doc.write(ref, `<< /Type /Font /Subtype /Type0 /BaseFont ${base3} /Encoding /Identity-H /DescendantFonts [${cid} 0 R] /ToUnicode ${cmap} 0 R >>`);
11167
11273
  }
11168
11274
  };
@@ -11193,8 +11299,11 @@ function subsetOtf(bytes, n, used, seac) {
11193
11299
  out.set("CFF ", cff);
11194
11300
  return buildSfnt(out, [79, 84, 84, 79]);
11195
11301
  }
11196
- function toUnicode(glyphs2) {
11197
- const entries = [...glyphs2].filter(([, t]) => t).sort((a, b) => a[0] - b[0]);
11302
+ function toUnicode(codes) {
11303
+ const entries = [...codes].map(([c, x2]) => (
11304
+ /** @type {[number, string]} */
11305
+ [c, x2.t]
11306
+ )).sort((a, b) => a[0] - b[0]);
11198
11307
  let body = "";
11199
11308
  for (let i2 = 0; i2 < entries.length; i2 += 100) {
11200
11309
  const chunk = entries.slice(i2, i2 + 100);
@@ -11218,6 +11327,16 @@ end
11218
11327
  end
11219
11328
  `;
11220
11329
  }
11330
+ function variation(t) {
11331
+ if (!t || t.length < 16) return "";
11332
+ const dv = new DataView(t.buffer, t.byteOffset, t.byteLength), off = dv.getUint16(4), n = dv.getUint16(8), size = dv.getUint16(10);
11333
+ const out = [];
11334
+ for (let i2 = 0; i2 < Math.min(n, 16) && size >= 20 && off + i2 * size + 20 <= t.length; i2++) {
11335
+ const p = off + i2 * size;
11336
+ out.push(`${String.fromCharCode(...t.subarray(p, p + 4)).replace(/[^\x20-\x7e]/g, "?")} ${Math.round(dv.getInt32(p + 8) / 655.36) / 100}`);
11337
+ }
11338
+ return out.join(", ") || "no axes read";
11339
+ }
11221
11340
  function tag2(bytes) {
11222
11341
  let h = 2166136261;
11223
11342
  for (let i2 = 0; i2 < bytes.length; i2++) h = Math.imul(h ^ bytes[i2], 16777619);
@@ -12883,7 +13002,7 @@ var init_budget = __esm({
12883
13002
  fail = (code, msg) => {
12884
13003
  throw new PdfReadError(code, msg);
12885
13004
  };
12886
- DEFAULTS = { maxDecoded: 512 << 20, maxStream: 128 << 20, maxSteps: 2e8, maxObjects: 5e6, maxTextItems: 2e6 };
13005
+ DEFAULTS = { maxDecoded: 512 << 20, maxStream: 128 << 20, maxSteps: 2e8, maxObjects: 5e6, maxTextItems: 1e6 };
12887
13006
  }
12888
13007
  });
12889
13008
 
@@ -13648,9 +13767,45 @@ var init_crypt = __esm({
13648
13767
  async function openSource(bytes, budget, o = {}) {
13649
13768
  const size = bytes.length;
13650
13769
  let xref = /* @__PURE__ */ new Map();
13651
- const cache = /* @__PURE__ */ new Map();
13770
+ let young = /* @__PURE__ */ new Map(), old = /* @__PURE__ */ new Map();
13771
+ const weak = /* @__PURE__ */ new Map();
13772
+ const cache = {
13773
+ /** @param {number} n @returns {any} undefined: not cached */
13774
+ get(n) {
13775
+ let v = young.get(n);
13776
+ if (v !== void 0) return v;
13777
+ v = old.get(n);
13778
+ if (v === void 0) {
13779
+ v = weak.get(n)?.deref();
13780
+ if (v === void 0) {
13781
+ weak.delete(n);
13782
+ return void 0;
13783
+ }
13784
+ weak.delete(n);
13785
+ }
13786
+ cache.set(n, v);
13787
+ return v;
13788
+ },
13789
+ /**
13790
+ * Held strongly for two turns of CACHE_N (a cheap LRU: no reordering per hit), then weakly
13791
+ * @param {number} n @param {any} v
13792
+ */
13793
+ set(n, v) {
13794
+ young.set(n, v);
13795
+ if (young.size < CACHE_N) return;
13796
+ for (const [k, o2] of old) if (o2 !== null && typeof o2 === "object" && !young.has(k)) weak.set(k, new WeakRef(o2));
13797
+ old = young;
13798
+ young = /* @__PURE__ */ new Map();
13799
+ },
13800
+ clear() {
13801
+ young.clear();
13802
+ old.clear();
13803
+ weak.clear();
13804
+ }
13805
+ };
13652
13806
  const loading = /* @__PURE__ */ new Set();
13653
13807
  const objstms = /* @__PURE__ */ new Map();
13808
+ let objstmBytes = 0;
13654
13809
  let endstreams = null;
13655
13810
  let recovered = false;
13656
13811
  let security = null, encryptRef = -1;
@@ -13768,12 +13923,17 @@ async function openSource(bytes, budget, o = {}) {
13768
13923
  if (lx2.next() !== T.NUM) break;
13769
13924
  offs[k] = lx2.val;
13770
13925
  }
13771
- os = { data, nums: nums.subarray(0, k), offs: offs.subarray(0, k), first };
13772
- if (objstms.size >= OBJSTM_CACHE) objstms.delete(
13773
- /** @type {number} */
13774
- objstms.keys().next().value
13775
- );
13926
+ os = { data, nums: nums.slice(0, k), offs: offs.slice(0, k), first };
13927
+ while (objstms.size && (objstms.size >= OBJSTM_CACHE || objstmBytes + data.length > OBJSTM_BYTES)) {
13928
+ const [k0, o0] = (
13929
+ /** @type {[number, any]} */
13930
+ objstms.entries().next().value
13931
+ );
13932
+ objstms.delete(k0);
13933
+ objstmBytes -= o0.data.length;
13934
+ }
13776
13935
  objstms.set(s, os);
13936
+ objstmBytes += data.length;
13777
13937
  }
13778
13938
  let j = os.nums[i2] === n ? i2 : -1;
13779
13939
  if (j < 0) {
@@ -13792,7 +13952,8 @@ async function openSource(bytes, budget, o = {}) {
13792
13952
  return v === void 0 || v instanceof Op ? null : v;
13793
13953
  }
13794
13954
  function get(n) {
13795
- if (cache.has(n)) return cache.get(n);
13955
+ const c = cache.get(n);
13956
+ if (c !== void 0) return c;
13796
13957
  if (loading.has(n)) return null;
13797
13958
  loading.add(n);
13798
13959
  try {
@@ -14046,6 +14207,7 @@ async function openSource(bytes, budget, o = {}) {
14046
14207
  }
14047
14208
  cache.clear();
14048
14209
  objstms.clear();
14210
+ objstmBytes = 0;
14049
14211
  }
14050
14212
  function recoverDeep() {
14051
14213
  const found = xref, tr = trailer;
@@ -14116,6 +14278,7 @@ async function openSource(bytes, budget, o = {}) {
14116
14278
  const keep = encryptRef >= 0 ? cache.get(encryptRef) : void 0;
14117
14279
  cache.clear();
14118
14280
  objstms.clear();
14281
+ objstmBytes = 0;
14119
14282
  if (keep !== void 0) cache.set(encryptRef, keep);
14120
14283
  }
14121
14284
  }
@@ -14124,7 +14287,13 @@ async function openSource(bytes, budget, o = {}) {
14124
14287
  recover();
14125
14288
  recoverDeep();
14126
14289
  }
14127
- const root = resolveSafe(trailer.get("Root"));
14290
+ let root = null;
14291
+ try {
14292
+ root = resolve2(trailer.get("Root"));
14293
+ } catch (e) {
14294
+ if (!(e instanceof PdfReadError) || e.code === "E_TIMEOUT" || e.code === "E_ABORTED" || e.code === "E_BUDGET") throw e;
14295
+ if (e.code === "E_DEPTH") fail("E_DEPTH", `The document catalog cannot be read: ${e.message}`);
14296
+ }
14128
14297
  if (!(root instanceof Map)) fail("E_MALFORMED", "The PDF has no document catalog (/Root)");
14129
14298
  return {
14130
14299
  bytes,
@@ -14305,7 +14474,7 @@ function inRegion(regions, budget) {
14305
14474
  return reach > off;
14306
14475
  };
14307
14476
  }
14308
- var MAX_SECTIONS, MAX_RESOLVE, MAX_OBJSTM_N, OBJSTM_CACHE, P48, P16, pack, unpack;
14477
+ var MAX_SECTIONS, MAX_RESOLVE, MAX_OBJSTM_N, OBJSTM_CACHE, OBJSTM_BYTES, CACHE_N, P48, P16, pack, unpack;
14309
14478
  var init_source = __esm({
14310
14479
  "src/reader/source.js"() {
14311
14480
  init_lexer();
@@ -14316,6 +14485,8 @@ var init_source = __esm({
14316
14485
  MAX_RESOLVE = 32;
14317
14486
  MAX_OBJSTM_N = 1e6;
14318
14487
  OBJSTM_CACHE = 32;
14488
+ OBJSTM_BYTES = 8 << 20;
14489
+ CACHE_N = 1024;
14319
14490
  P48 = 2 ** 48;
14320
14491
  P16 = 2 ** 16;
14321
14492
  pack = (t, a, b) => t * P48 + a * P16 + b;
@@ -14432,7 +14603,10 @@ function parseCMap(data, budget, held = { n: 0 }) {
14432
14603
  }
14433
14604
  return { map, spaces };
14434
14605
  }
14435
- function fontOf(src, font, held = { n: 0 }) {
14606
+ function fontOf(src, font, held = (
14607
+ /** @type {Held} */
14608
+ { n: 0, cmaps: /* @__PURE__ */ new Map() }
14609
+ )) {
14436
14610
  const R = src.resolve;
14437
14611
  const sub = R(font.get("Subtype"));
14438
14612
  let toU = null, spaces = (
@@ -14489,6 +14663,26 @@ function fontOf(src, font, held = { n: 0 }) {
14489
14663
  }
14490
14664
  }
14491
14665
  const lens = spaces.length ? [...new Set(spaces.map((s) => s[0]))].sort() : [2];
14666
+ let back;
14667
+ const fromFont = (c) => {
14668
+ if (back === void 0) back = fontCmap(src, cid, held);
14669
+ if (!back) return void 0;
14670
+ const m = cid instanceof Map ? R(cid.get("CIDToGIDMap")) : null;
14671
+ let g = c;
14672
+ if (m instanceof PdfStream) {
14673
+ if (!gidMap) {
14674
+ try {
14675
+ gidMap = src.decodeStream(m).data.slice(0, 131072);
14676
+ } catch (e) {
14677
+ if (stops(e)) throw e;
14678
+ gidMap = new Uint8Array(0);
14679
+ }
14680
+ }
14681
+ g = 2 * c + 1 < gidMap.length ? gidMap[2 * c] << 8 | gidMap[2 * c + 1] : 0;
14682
+ }
14683
+ return back.get(g);
14684
+ };
14685
+ let gidMap;
14492
14686
  return {
14493
14687
  /** each glyph to emit, none held (a 16 MB string is not 16 M objects) @param {Uint8Array} b @param {Emit} emit */
14494
14688
  decode(b, emit) {
@@ -14504,7 +14698,7 @@ function fontOf(src, font, held = { n: 0 }) {
14504
14698
  }
14505
14699
  for (let k = 0; k < L && i2 + k < b.length; k++) c = c * 256 + b[i2 + k];
14506
14700
  i2 += L;
14507
- emit(toU?.get(c) ?? "", W.get(c) ?? dw, false);
14701
+ emit(toU?.get(c) ?? fromFont(c) ?? NONE, W.get(c) ?? dw, false);
14508
14702
  }
14509
14703
  }
14510
14704
  };
@@ -14527,7 +14721,7 @@ function fontOf(src, font, held = { n: 0 }) {
14527
14721
  for (const x0 of d.slice(0, 4096)) {
14528
14722
  const x2 = R(x0);
14529
14723
  if (typeof x2 === "number") c = x2;
14530
- else if (typeof x2 === "string") diff.set(c++ & 255, glyphText(x2));
14724
+ else if (typeof x2 === "string") diff.set(c++ & 255, x2 === ".notdef" ? "" : glyphText(x2) || NONE);
14531
14725
  }
14532
14726
  }
14533
14727
  const symbolic = base3 === "Symbol" || base3 === "ZapfDingbats";
@@ -14547,7 +14741,7 @@ function fontOf(src, font, held = { n: 0 }) {
14547
14741
  if (t === void 0) t = diff.get(c);
14548
14742
  if (t === void 0) {
14549
14743
  const u = symMap ? symMap.get(c) : baseFn(c);
14550
- t = u === void 0 ? "" : String.fromCodePoint(u);
14744
+ t = u === void 0 ? NONE : String.fromCodePoint(u);
14551
14745
  }
14552
14746
  let w = Array.isArray(widths) && typeof fc === "number" && c >= fc && c - fc < widths.length ? R(widths[c - fc]) : std ? std.widths[c] : 500;
14553
14747
  if (typeof w !== "number" || !Number.isFinite(w)) w = 0;
@@ -14556,6 +14750,34 @@ function fontOf(src, font, held = { n: 0 }) {
14556
14750
  }
14557
14751
  };
14558
14752
  }
14753
+ function fontCmap(src, cid, held) {
14754
+ const R = src.resolve;
14755
+ if (!(cid instanceof Map) || R(cid.get("Subtype")) !== "CIDFontType2") return null;
14756
+ const fd2 = R(cid.get("FontDescriptor")), ff = fd2 instanceof Map ? R(fd2.get("FontFile2")) : null;
14757
+ if (!(ff instanceof PdfStream)) return null;
14758
+ const was = held.cmaps.get(ff);
14759
+ if (was !== void 0) return was;
14760
+ if (held.cmaps.size >= MAX_CMAP_FONTS) fail("E_BUDGET", `A page's text reads the character maps of more than ${MAX_CMAP_FONTS} embedded fonts (fonts with no ToUnicode)`);
14761
+ held.cmaps.set(ff, null);
14762
+ let f;
14763
+ try {
14764
+ f = parseFont(src.decodeStream(ff).data);
14765
+ } catch (e) {
14766
+ if (stops(e)) throw e;
14767
+ return null;
14768
+ }
14769
+ src.budget.tick(65536);
14770
+ const back = /* @__PURE__ */ new Map();
14771
+ for (let cp = 32; cp <= 65535; cp++) {
14772
+ if (cp === 55296) cp = 63744;
14773
+ const g = f.glyphFor(cp);
14774
+ if (g && !back.has(g)) back.set(g, String.fromCharCode(cp));
14775
+ }
14776
+ held.n += 9 * back.size;
14777
+ if (held.n > MAX_HELD) fail("E_BUDGET", `A page's fonts hold more than ${MAX_HELD.toLocaleString("en")} units of code mappings (an entry 8, a character 1)`);
14778
+ held.cmaps.set(ff, back);
14779
+ return back;
14780
+ }
14559
14781
  function contentBytes(src, contents) {
14560
14782
  const c = src.resolve(contents);
14561
14783
  const list = Array.isArray(c) ? c.slice(0, 1e4).map(src.resolve) : [c];
@@ -14582,6 +14804,36 @@ function contentBytes(src, contents) {
14582
14804
  }
14583
14805
  return out;
14584
14806
  }
14807
+ function logical(parts) {
14808
+ if (!parts.some((t) => RTL2.test(t))) return parts;
14809
+ const kind = (t) => RTL2.test(t) ? "R" : NUM.test(t) ? "N" : STRONG_L.test(t) ? "L" : "N";
14810
+ const k = parts.map(kind), res = parts.slice();
14811
+ for (let i2 = 0; i2 < parts.length; ) {
14812
+ if (k[i2] !== "R") {
14813
+ i2++;
14814
+ continue;
14815
+ }
14816
+ let j = i2, e = i2;
14817
+ while (j < parts.length && k[j] !== "L") {
14818
+ if (k[j] === "R") e = j;
14819
+ j++;
14820
+ }
14821
+ const run2 = parts.slice(i2, e + 1).reverse().map(rev2);
14822
+ for (let a = 0; a < run2.length; ) {
14823
+ if (!NUM.test(run2[a])) {
14824
+ a++;
14825
+ continue;
14826
+ }
14827
+ let b = a;
14828
+ while (b < run2.length && NUM.test(run2[b])) b++;
14829
+ run2.splice(a, b - a, ...run2.slice(a, b).reverse().map(rev2));
14830
+ a = b;
14831
+ }
14832
+ res.splice(i2, run2.length, ...run2);
14833
+ i2 = e + 1;
14834
+ }
14835
+ return res;
14836
+ }
14585
14837
  function skipInline(lx) {
14586
14838
  const b = lx.buf;
14587
14839
  let i2 = lx.pos + 1;
@@ -14611,9 +14863,31 @@ function textPieces(src, pageDict, resources, o = {}) {
14611
14863
  }
14612
14864
  function pageText(src, pageDict, resources, raw = false) {
14613
14865
  const R = src.resolve, budget = src.budget;
14614
- const pieces = [];
14866
+ const PX = [], PY = [], PE = [], PS = [], PG = [];
14867
+ const PA = [], PAE = [], PC = [], PD = [];
14868
+ const PT = [];
14869
+ let chars2 = 0;
14870
+ let moved = true;
14615
14871
  const fonts = /* @__PURE__ */ new Map();
14616
- const held = { n: 0 };
14872
+ const add2 = (x2, y, end, size, text3, dir) => {
14873
+ if (PT.length >= budget.maxTextItems) throw new PdfReadError("E_BUDGET", `A page has more than ${budget.maxTextItems.toLocaleString("en")} pieces of text (budget.maxTextItems)`);
14874
+ if ((chars2 += text3.length) > MAX_TEXT_CHARS) throw new PdfReadError("E_BUDGET", `A page makes more than ${MAX_TEXT_CHARS.toLocaleString("en")} characters of text`);
14875
+ PX.push(x2);
14876
+ PY.push(y);
14877
+ PE.push(end);
14878
+ PS.push(size);
14879
+ PT.push(text3);
14880
+ PG.push(moved ? 2 : 0);
14881
+ moved = false;
14882
+ PA.push(dir[0]);
14883
+ PAE.push(dir[1]);
14884
+ PC.push(dir[2]);
14885
+ PD.push(dir[3]);
14886
+ };
14887
+ const held = (
14888
+ /** @type {Held} */
14889
+ { n: 0, cmaps: /* @__PURE__ */ new Map() }
14890
+ );
14617
14891
  const fontFor = (res, name) => {
14618
14892
  const fd2 = R(res instanceof Map ? R(res.get("Font")) instanceof Map ? R(res.get("Font")).get(name) : null : null);
14619
14893
  const key = fd2 instanceof Map ? fd2 : null;
@@ -14632,6 +14906,11 @@ function pageText(src, pageDict, resources, raw = false) {
14632
14906
  ), size: 0, Tc: 0, Tw: 0, Th: 1, TL: 0, Ts: 0 };
14633
14907
  const tsStack = [];
14634
14908
  let Tm = ID, Tlm = ID, dos = 0;
14909
+ const marks = [];
14910
+ let deep = 0, actual = (
14911
+ /** @type {any} */
14912
+ null
14913
+ );
14635
14914
  let args = [];
14636
14915
  const show = (b, ctm) => {
14637
14916
  if (!ts.font) return;
@@ -14642,10 +14921,20 @@ function pageText(src, pageDict, resources, raw = false) {
14642
14921
  const x2 = M0[4], y = M0[5], size = Math.hypot(M0[2], M0[3]) || ts.size;
14643
14922
  const tx = (w / 1e3 * ts.size + ts.Tc + (space ? ts.Tw : 0)) * ts.Th;
14644
14923
  Tm = mul2([1, 0, 0, 1, tx, 0], Tm);
14645
- const end = mul2([ts.size * ts.Th, 0, 0, ts.size, 0, ts.Ts], mul2(Tm, ctm))[4];
14646
- if (text3) {
14647
- if (pieces.length >= budget.maxTextItems) throw new PdfReadError("E_BUDGET", `A page has more than ${budget.maxTextItems.toLocaleString("en")} pieces of text (budget.maxTextItems)`);
14648
- pieces.push({ x: x2, y, end, size, text: text3, gap: false });
14924
+ const M1 = mul2([ts.size * ts.Th, 0, 0, ts.size, 0, ts.Ts], mul2(Tm, ctm)), end = M1[4];
14925
+ const L = Math.hypot(M0[0], M0[1]), ux = L ? M0[0] / L : 1, uy = L ? M0[1] / L : 0;
14926
+ const dir = [x2 * ux + y * uy, M1[4] * ux + M1[5] * uy, y * ux - x2 * uy, Math.round(Math.atan2(uy, ux) * 180 / Math.PI)];
14927
+ if (actual) {
14928
+ if (!actual.seen) Object.assign(actual, { x: x2, y, size, dir, seen: true });
14929
+ actual.end = end;
14930
+ actual.dir[1] = dir[1];
14931
+ return;
14932
+ }
14933
+ const n = PT.length - 1;
14934
+ if (text3) add2(x2, y, end, size, text3, dir);
14935
+ else if (n >= 0 && PAE[n] < dir[1] && PD[n] === dir[3] && Math.abs(PC[n] - dir[2]) < 0.01) {
14936
+ PE[n] = end;
14937
+ PAE[n] = dir[1];
14649
14938
  }
14650
14939
  });
14651
14940
  budget.tick(1 + (k & 1023));
@@ -14686,6 +14975,7 @@ function pageText(src, pageDict, resources, raw = false) {
14686
14975
  }
14687
14976
  case "BT":
14688
14977
  Tm = Tlm = ID;
14978
+ moved = true;
14689
14979
  break;
14690
14980
  case "Tf":
14691
14981
  ts = { ...ts, font: typeof a[0] === "string" ? fontFor(f.res, a[0]) : null, size: typeof a[1] === "number" ? a[1] : 0 };
@@ -14710,19 +15000,23 @@ function pageText(src, pageDict, resources, raw = false) {
14710
15000
  // falls through
14711
15001
  case "Td":
14712
15002
  if (typeof a[0] === "number" && typeof a[1] === "number") Tm = Tlm = mul2([1, 0, 0, 1, a[0], a[1]], Tlm);
15003
+ moved = true;
14713
15004
  break;
14714
15005
  case "Tm": {
14715
15006
  const m = matrixOf(a);
14716
15007
  if (m) Tm = Tlm = m;
15008
+ moved = true;
14717
15009
  break;
14718
15010
  }
14719
15011
  case "T*":
14720
15012
  Tm = Tlm = mul2([1, 0, 0, 1, 0, -ts.TL], Tlm);
15013
+ moved = true;
14721
15014
  break;
14722
15015
  case "Tj":
14723
15016
  if (a[0] instanceof PdfString) show(a[0].bytes, f.ctm);
14724
15017
  break;
14725
15018
  case "'":
15019
+ moved = true;
14726
15020
  Tm = Tlm = mul2([1, 0, 0, 1, 0, -ts.TL], Tlm);
14727
15021
  if (a[0] instanceof PdfString) show(a[0].bytes, f.ctm);
14728
15022
  break;
@@ -14730,6 +15024,7 @@ function pageText(src, pageDict, resources, raw = false) {
14730
15024
  if (typeof a[0] === "number") ts = { ...ts, Tw: a[0] };
14731
15025
  if (typeof a[1] === "number") ts = { ...ts, Tc: a[1] };
14732
15026
  Tm = Tlm = mul2([1, 0, 0, 1, 0, -ts.TL], Tlm);
15027
+ moved = true;
14733
15028
  if (a[2] instanceof PdfString) show(a[2].bytes, f.ctm);
14734
15029
  break;
14735
15030
  case "TJ":
@@ -14737,10 +15032,38 @@ function pageText(src, pageDict, resources, raw = false) {
14737
15032
  if (x2 instanceof PdfString) show(x2.bytes, f.ctm);
14738
15033
  else if (typeof x2 === "number") {
14739
15034
  Tm = mul2([1, 0, 0, 1, -x2 / 1e3 * ts.size * ts.Th, 0], Tm);
14740
- if (x2 < -180 && pieces.length) pieces[pieces.length - 1].gap = true;
15035
+ if (x2 < -180 && PT.length) PG[PT.length - 1] |= 1;
14741
15036
  }
14742
15037
  }
14743
15038
  break;
15039
+ case "BMC":
15040
+ case "BDC": {
15041
+ if (marks.length >= MAX_MARKS) {
15042
+ deep++;
15043
+ break;
15044
+ }
15045
+ let at = null;
15046
+ if (op === "BDC" && !actual) {
15047
+ const pr = a[1] instanceof Map ? a[1] : typeof a[1] === "string" && f.res instanceof Map ? R(R(f.res.get("Properties")) instanceof Map ? R(f.res.get("Properties")).get(a[1]) : null) : null;
15048
+ const t = pr instanceof Map ? R(pr.get("ActualText")) : null;
15049
+ if (t instanceof PdfString && t.bytes.length > 2 * MAX_ACTUAL + 2) src.warn(`an /ActualText longer than ${MAX_ACTUAL} characters was ignored`);
15050
+ else if (t instanceof PdfString) at = actual = { at: decodeText(t), x: 0, y: 0, end: 0, size: 0, dir: [0, 0, 0, 0], seen: false };
15051
+ }
15052
+ marks.push(at);
15053
+ break;
15054
+ }
15055
+ case "EMC": {
15056
+ if (deep) {
15057
+ deep--;
15058
+ break;
15059
+ }
15060
+ const m = marks.pop();
15061
+ if (m && m === actual) {
15062
+ actual = null;
15063
+ if (m.seen && m.at) add2(m.x, m.y, m.end, m.size, m.at, m.dir);
15064
+ }
15065
+ break;
15066
+ }
14744
15067
  case "BI": {
14745
15068
  for (let k = 0; k < 4096; k++) {
14746
15069
  const t = parseValue(f.lx);
@@ -14774,30 +15097,44 @@ function pageText(src, pageDict, resources, raw = false) {
14774
15097
  default:
14775
15098
  }
14776
15099
  }
14777
- if (raw) return pieces;
15100
+ if (raw) return PT.map((text3, i2) => ({ x: PX[i2], y: PY[i2], end: PE[i2], size: PS[i2], text: text3, gap: !!(PG[i2] & 1) }));
14778
15101
  const out = [];
14779
- let ly = NaN, lend = 0, lsize = 0, lgap = false, last = "";
14780
- for (const p of pieces) {
14781
- const tol = Math.max(p.size, lsize, 1) * 0.5;
14782
- if (out.length && Math.abs(p.y - ly) > tol) out.push("\n");
14783
- else if (out.length && last !== " " && !p.text.startsWith(" ") && (lgap || p.x - lend > Math.max(p.size, 1) * 0.25 || p.x < lend - Math.max(p.size, 1) * 2)) out.push(" ");
14784
- out.push(p.text);
14785
- last = p.text[p.text.length - 1];
14786
- ly = p.y;
14787
- lend = p.end;
14788
- lsize = p.size;
14789
- lgap = p.gap;
15102
+ for (let i2 = 0; i2 < PT.length; ) {
15103
+ let j = i2 + 1;
15104
+ while (j < PT.length && PD[j] === PD[j - 1] && Math.abs(PC[j] - PC[j - 1]) <= Math.max(PS[j], PS[j - 1], 1) * 0.5) j++;
15105
+ const ix = [];
15106
+ for (let k = i2; k < j; k++) ix.push(k);
15107
+ let sorted = true;
15108
+ for (let k = i2 + 1; k < j && sorted; k++) if (PA[k] < PA[k - 1] - 0.01) sorted = false;
15109
+ if (!sorted) ix.sort((a, b) => PA[a] - PA[b]);
15110
+ const line3 = [];
15111
+ let lend = 0, lgap = false, last = "";
15112
+ for (const k of ix) {
15113
+ const text3 = PT[k], size = Math.max(PS[k], 1);
15114
+ if (line3.length && last !== " " && !text3.startsWith(" ") && (lgap || PA[k] - lend > size * (PG[k] & 2 ? 0.07 : 0.25))) line3.push(" ");
15115
+ line3.push(text3);
15116
+ last = text3[text3.length - 1];
15117
+ lend = Math.max(PAE[k], line3.length > 1 ? lend : -Infinity);
15118
+ lgap = !!(PG[k] & 1);
15119
+ }
15120
+ if (out.length) out.push("\n");
15121
+ out.push(logical(line3).join(""));
15122
+ i2 = j;
14790
15123
  }
14791
15124
  return out.join("");
14792
15125
  }
14793
- var MAX_FORMS, MAX_CMAP, MAX_DST, MAX_CMAP_CHARS, MAX_HELD, MAX_W, MAX_DO, TEXT_TIMEOUT_MS, DOC, WIN, MAC_HIGH, BASE, STD_HIGH, NAMES2, ACCENT, own, STD_ALIAS, stops, mul2, ID, matrixOf;
15126
+ var MAX_FORMS, MAX_CMAP, MAX_MARKS, MAX_ACTUAL, MAX_TEXT_CHARS, MAX_DST, MAX_CMAP_CHARS, MAX_HELD, MAX_W, MAX_DO, TEXT_TIMEOUT_MS, DOC, WIN, MAC_HIGH, BASE, STD_HIGH, NAMES2, ACCENT, own, STD_ALIAS, NONE, stops, MAX_CMAP_FONTS, mul2, ID, matrixOf, RTL2, STRONG_L, NUM, rev2;
14794
15127
  var init_text2 = __esm({
14795
15128
  "src/reader/text.js"() {
14796
15129
  init_lexer();
14797
15130
  init_standard_metrics();
14798
15131
  init_budget();
15132
+ init_truetype();
14799
15133
  MAX_FORMS = 16;
14800
15134
  MAX_CMAP = 1e6;
15135
+ MAX_MARKS = 1e3;
15136
+ MAX_ACTUAL = 4096;
15137
+ MAX_TEXT_CHARS = 16e6;
14801
15138
  MAX_DST = 128;
14802
15139
  MAX_CMAP_CHARS = 4e6;
14803
15140
  MAX_HELD = 16e6;
@@ -15002,7 +15339,9 @@ var init_text2 = __esm({
15002
15339
  t[k]
15003
15340
  ) : void 0;
15004
15341
  STD_ALIAS = { Arial: "Helvetica", "Arial-BoldMT": "Helvetica-Bold", ArialMT: "Helvetica", TimesNewRoman: "Times-Roman", TimesNewRomanPSMT: "Times-Roman", CourierNew: "Courier" };
15342
+ NONE = "�";
15005
15343
  stops = (e) => e?.code === "E_TIMEOUT" || e?.code === "E_ABORTED" || e?.code === "E_BUDGET";
15344
+ MAX_CMAP_FONTS = 100;
15006
15345
  mul2 = (A, B) => (
15007
15346
  /** @type {M} */
15008
15347
  [A[0] * B[0] + A[1] * B[2], A[0] * B[1] + A[1] * B[3], A[2] * B[0] + A[3] * B[2], A[2] * B[1] + A[3] * B[3], A[4] * B[0] + A[5] * B[2] + B[4], A[4] * B[1] + A[5] * B[3] + B[5]]
@@ -15013,6 +15352,10 @@ var init_text2 = __esm({
15013
15352
  /** @type {M} */
15014
15353
  a.slice(0, 6)
15015
15354
  ) : null;
15355
+ RTL2 = /[\u0590-\u05ff\u0600-\u065f\u066a-\u06ef\u06fa-\u08ff\ufb1d-\ufdff\ufe70-\ufeff]/;
15356
+ STRONG_L = /[\p{L}]/u;
15357
+ NUM = /^[\p{Nd}.,٫٬]+$/u;
15358
+ rev2 = (t) => [...t].reverse().join("");
15016
15359
  }
15017
15360
  });
15018
15361
 
@@ -15254,7 +15597,7 @@ function createCopier(t) {
15254
15597
  let t0 = trees.get(owner);
15255
15598
  if (t0) return t0;
15256
15599
  const src = owner.r.src, R = src.resolve;
15257
- t0 = { objs: /* @__PURE__ */ new WeakSet(), nums: /* @__PURE__ */ new Set(), types: new Set(STRUCT_TYPES) };
15600
+ t0 = { objs: /* @__PURE__ */ new Set(), nums: /* @__PURE__ */ new Set(), types: new Set(STRUCT_TYPES) };
15258
15601
  trees.set(owner, t0);
15259
15602
  const root = R(owner.r.root.get("StructTreeRoot"));
15260
15603
  if (!(root instanceof Map)) return t0;
@@ -15476,6 +15819,7 @@ function createCopier(t) {
15476
15819
  continue;
15477
15820
  }
15478
15821
  const rep = t.replace?.(owner, f.n);
15822
+ f.rep = rep;
15479
15823
  f.v = rep !== void 0 ? rep : owner.r.src.get(f.n);
15480
15824
  if (!t.keepActive && f.v instanceof Map) {
15481
15825
  const r = judge(f.v, owner.r.src.resolve);
@@ -15500,7 +15844,7 @@ function createCopier(t) {
15500
15844
  continue;
15501
15845
  }
15502
15846
  stack.pop();
15503
- const v = f.v;
15847
+ const v = f.v, rep0 = f.rep;
15504
15848
  let open = false;
15505
15849
  const refText = (o, r) => {
15506
15850
  if (mapOf(o).get(r.n) === GRAY) open = true;
@@ -15515,7 +15859,7 @@ function createCopier(t) {
15515
15859
  let key = "";
15516
15860
  const raw = v instanceof PdfStream ? owner.r.src.rawStream(v) : null;
15517
15861
  if (t.dedupe && early === void 0 && !open && shareable(v)) {
15518
- key = hex(await (t.hash ?? sha256)(raw ? concat2(dict, raw) : new TextEncoder().encode(dict)));
15862
+ key = `${dict.length} ${fnv(dict)} ${raw ? `${raw.length} ${fnv(raw)}` : ""}`;
15519
15863
  const hit = seen.get(key);
15520
15864
  if (hit && hit.dict === dict && same(hit.raw(), raw)) {
15521
15865
  m.set(f.n, hit.n);
@@ -15526,8 +15870,11 @@ function createCopier(t) {
15526
15870
  const n = early ?? t.reserve();
15527
15871
  await t.write(n, dict, data, v instanceof PdfStream ? v.dict : v instanceof Map ? v : null);
15528
15872
  if (key) {
15529
- const o2 = owner, v2 = v;
15530
- seen.set(key, { n, dict, raw: () => v2 instanceof PdfStream ? o2.r.src.rawStream(v2) : null });
15873
+ const o2 = owner, n2 = f.n, mine = rep0 === void 0 ? null : v;
15874
+ seen.set(key, { n, dict, raw: () => {
15875
+ const v2 = mine ?? o2.r.src.get(n2);
15876
+ return v2 instanceof PdfStream ? o2.r.src.rawStream(v2) : null;
15877
+ } });
15531
15878
  }
15532
15879
  m.set(f.n, n);
15533
15880
  }
@@ -15547,13 +15894,11 @@ function createCopier(t) {
15547
15894
  }
15548
15895
  return typeof v !== "object" || v === null || v instanceof PdfString;
15549
15896
  };
15550
- const concat2 = (s, b) => {
15551
- const a = new TextEncoder().encode(s);
15552
- const o = new Uint8Array(a.length + b.length + 1);
15553
- o.set(a);
15554
- o[a.length] = 0;
15555
- o.set(b, a.length + 1);
15556
- return o;
15897
+ const fnv = (x2) => {
15898
+ let h = 2166136261;
15899
+ if (typeof x2 === "string") for (let i2 = 0; i2 < x2.length; i2++) h = Math.imul(h ^ x2.charCodeAt(i2), 16777619);
15900
+ else for (let i2 = 0; i2 < x2.length; i2++) h = Math.imul(h ^ x2[i2], 16777619);
15901
+ return (h >>> 0).toString(36);
15557
15902
  };
15558
15903
  function streamDict(owner, s) {
15559
15904
  const R = owner.r.src.resolve;
@@ -15622,16 +15967,14 @@ function createCopier(t) {
15622
15967
  }
15623
15968
  };
15624
15969
  }
15625
- var sha256, SHARE, GRAY, HEXD, Raw, Foreign, OPEN_A, CLOSE_A, OPEN_D, CLOSE_D;
15970
+ var SHARE, GRAY, HEXD, Raw, Foreign, OPEN_A, CLOSE_A, OPEN_D, CLOSE_D;
15626
15971
  var init_copy = __esm({
15627
15972
  "src/reader/copy.js"() {
15628
15973
  init_lexer();
15629
15974
  init_pdf();
15630
- init_crypto();
15631
15975
  init_policy();
15632
15976
  init_deflate();
15633
15977
  init_policy();
15634
- sha256 = (b) => sha("SHA-256", b);
15635
15978
  SHARE = /* @__PURE__ */ new Set(["Font", "FontDescriptor", "ExtGState", "Encoding", "Pattern", "Shading", "Halftone", "Function", "XObject", "Mask", "CMap", "OutputIntent", "Filespec", "EmbeddedFile"]);
15636
15979
  GRAY = -2;
15637
15980
  HEXD = "0123456789abcdef";
@@ -15926,7 +16269,8 @@ async function run(self, sink, o) {
15926
16269
  const keepStruct = single && o.keepStructure !== false && !self.edits.flatten;
15927
16270
  const info = self.info();
15928
16271
  const edited = !!self.edits.info || level?.conformance !== self.pdfa()?.conformance;
15929
- const srcXmp = self.edits.xmp ?? (edited || !level && self.pdfa() ? null : self.xmp());
16272
+ const uaLost = !keepStruct && /pdfuaid:part/.test(self.xmp() ?? "");
16273
+ const srcXmp = self.edits.xmp ?? (edited || uaLost || !level && self.pdfa() ? null : self.xmp());
15930
16274
  const created = o.creationDate ?? (info.creationDate instanceof Date ? info.creationDate : /* @__PURE__ */ new Date());
15931
16275
  let intentOpt = null;
15932
16276
  if (level && profile) {
@@ -16006,7 +16350,8 @@ async function run(self, sink, o) {
16006
16350
  }
16007
16351
  }
16008
16352
  for (let k = 0; k < plan.length; k++) {
16009
- const p = plan[k], owner = p.owner, src = owner.r.src, OR = src.resolve, pg = owner.r.pages[p.index], d = pg.dict;
16353
+ const p = plan[k], owner = p.owner, src = owner.r.src, OR = src.resolve, pg = pageAt(owner.r, p.index), d = pg.dict;
16354
+ if (pg.emptied) warn(pg.emptied);
16010
16355
  const b = owner.boxes(p.index);
16011
16356
  const W = b.mediaBox[2] - b.mediaBox[0], H = b.mediaBox[3] - b.mediaBox[1];
16012
16357
  const page = (
@@ -16130,7 +16475,7 @@ async function run(self, sink, o) {
16130
16475
  };
16131
16476
  for (const k of ["ViewerPreferences", "PageMode", "PageLayout", "OCProperties", "Collection", "URI"]) await cat(k);
16132
16477
  if (keepStruct) for (const k of ["StructTreeRoot", "MarkInfo", "Lang"]) await cat(k);
16133
- else if (root.has("StructTreeRoot")) warn("the structure tree (tags) was dropped: the pages come from several documents, or the form was flattened");
16478
+ else if (root.has("StructTreeRoot")) warn("the structure tree (tags) was dropped: the pages come from several documents, the form was flattened, or keepStructure is false; the result is not tagged (no longer PDF/UA; PDF/A-a is claimed as PDF/A-b)");
16134
16479
  if (level && level.part === 3) await cat("AF");
16135
16480
  if (single && plan.length === self.r.pages.length && plan.every((p, i2) => p.index === i2)) await cat("PageLabels");
16136
16481
  const oa = R(root.get("OpenAction"));
@@ -16943,6 +17288,26 @@ function flatTree(rootNode, src, kind = "Names") {
16943
17288
  }
16944
17289
  return out;
16945
17290
  }
17291
+ function pageAt(r, i2) {
17292
+ const pg = r.pages[i2];
17293
+ if (pg.checked) return pg;
17294
+ pg.checked = true;
17295
+ const R = r.src.resolve;
17296
+ try {
17297
+ const c = R(pg.dict.get("Contents"));
17298
+ if (Array.isArray(c)) {
17299
+ r.src.budget.tick(Math.min(c.length, 1e4));
17300
+ for (const x2 of c.slice(0, 1e4)) R(x2);
17301
+ }
17302
+ } catch (e) {
17303
+ if (!(e instanceof PdfReadError) || e.code !== "E_CYCLE") throw e;
17304
+ pg.emptied = `page ${i2 + 1}'s /Contents has a reference loop; the page is read as empty`;
17305
+ r.src.warn(pg.emptied);
17306
+ pg.dict = new Map(pg.dict);
17307
+ pg.dict.delete("Contents");
17308
+ }
17309
+ return pg;
17310
+ }
16946
17311
  function makeDoc(r, plan, edits) {
16947
17312
  const { src, root, pages, pageIndex: pageIndex2 } = r;
16948
17313
  const R = src.resolve;
@@ -17029,7 +17394,7 @@ function makeDoc(r, plan, edits) {
17029
17394
  * document's timeoutMs when sooner) @param {{ timeoutMs?: number }} [o]
17030
17395
  */
17031
17396
  extractText: (o) => {
17032
- const pg = owner.r.pages[p.index];
17397
+ const pg = pageAt(owner.r, p.index);
17033
17398
  return extractText(owner.r.src, pg.dict, owner.r.src.resolve(pg.inh.Resources), o);
17034
17399
  },
17035
17400
  /** the annotations: subtype, rect, contents, uri, the page index a link goes to */
@@ -17215,7 +17580,7 @@ function makeDoc(r, plan, edits) {
17215
17580
  const contents = R(v.get("Contents"));
17216
17581
  const end = range ? range[2] + range[3] : 0;
17217
17582
  const ok = !!range && range[0] === 0 && range[1] <= range[2] && end <= src.size;
17218
- const rev2 = ok ? src.revisions.findIndex((e) => e >= end) : -1;
17583
+ const rev3 = ok ? src.revisions.findIndex((e) => e >= end) : -1;
17219
17584
  out.push({
17220
17585
  field: f.name,
17221
17586
  subFilter: typeof R(v.get("SubFilter")) === "string" ? R(v.get("SubFilter")) : null,
@@ -17229,7 +17594,7 @@ function makeDoc(r, plan, edits) {
17229
17594
  /** the signature covers every byte of the file but its /Contents (no change after it) */
17230
17595
  coversWholeFile: ok && end === src.size,
17231
17596
  /** the revision (0 first) whose end it covers, -1 when none */
17232
- revision: rev2,
17597
+ revision: rev3,
17233
17598
  /** the bytes after it: an incremental update was appended after signing */
17234
17599
  bytesAfter: ok ? src.size - end : null,
17235
17600
  cms: contents instanceof PdfString ? contents.bytes : null,
@@ -17551,6 +17916,8 @@ function createPdf(sink, options2 = {}) {
17551
17916
  /** @type {const} */
17552
17917
  ["title", "author", "subject", "keywords"]
17553
17918
  ) if (o[k] != null && (typeof o[k] !== "string" || o[k].length > 65536)) throw new TypeError(`createPdf: ${k} is a string of at most 65,536 characters`);
17919
+ if (o.metadata != null && typeof o.metadata !== "boolean") throw new TypeError("createPdf: metadata is true (the default: an XMP stream) or false (Info only)");
17920
+ if (o.metadata === false && (o.pdfa || o.tagged)) throw new TypeError("createPdf: metadata: false cannot be combined with pdfa or tagged: PDF/A and PDF/UA require the XMP metadata stream");
17554
17921
  const level = pdfaLevel(o.pdfa, o.tagged);
17555
17922
  o.signal?.throwIfAborted();
17556
17923
  const ctl = new AbortController();
@@ -17575,6 +17942,7 @@ function createPdf(sink, options2 = {}) {
17575
17942
  pdfa: !!o.pdfa,
17576
17943
  pdfaLevel: level,
17577
17944
  tagged: !!o.tagged,
17945
+ metadata: o.metadata !== false,
17578
17946
  signal: ctl.signal,
17579
17947
  encrypt: enc,
17580
17948
  hash: signHash ? hasher(
@@ -0,0 +1,37 @@
1
+ // A Factur-X (ZUGFeRD 2.1+) invoice: a tagged PDF/A-3a page with the CII XML embedded as factur-x.xml.
2
+ //
3
+ // node examples/factur-x.mjs invoice.xml font.ttf out.pdf [profile]
4
+ //
5
+ // The XML must be a UN/CEFACT CrossIndustryInvoice whose GuidelineSpecifiedDocumentContextParameter ID names the
6
+ // profile (EN 16931 by default: urn:cen.eu:en16931:2017). pdf.facturX checks that, refuses any DOCTYPE or entity, and
7
+ // writes the fx XMP schema; it does not validate the invoice itself (run the Factur-X XSD and Schematron for that).
8
+ // The page here only shows a few fields; a real one draws the whole invoice from the same data as the XML.
9
+ import fs from 'node:fs';
10
+ import { createPdf } from '../dist/index.js';
11
+
12
+ const [xmlFile, fontFile, outFile, profile = 'EN 16931'] = process.argv.slice(2);
13
+ if (!outFile) { console.error('node examples/factur-x.mjs invoice.xml font.ttf out.pdf [profile]'); process.exit(2); }
14
+ const xml = fs.readFileSync(xmlFile);
15
+ const text = xml.toString('utf8');
16
+ /** the first <ram:tag> after anchor (display only: the page shows a few fields of the XML) */
17
+ const field = (tag, anchor = '') => new RegExp(`${anchor}[\\s\\S]*?<ram:${tag}[^>]*>([^<]*)<`).exec(text)?.[1] ?? '';
18
+ const number = field('ID', '<rsm:ExchangedDocument>'), seller = field('Name', '<ram:SellerTradeParty>');
19
+
20
+ const pdf = createPdf(fs.createWriteStream(outFile), {
21
+ title: `Invoice ${number}`, author: seller, subject: 'Factur-X invoice', keywords: `invoice, Factur-X, ${profile}`,
22
+ pdfa: { part: 3, conformance: 'a' }, tagged: { lang: 'en' },
23
+ });
24
+ const font = await pdf.embedFont(fs.readFileSync(fontFile));
25
+ const page = pdf.addPage({ trimBox: [0, 0, 595.28, 841.89] });
26
+ const line = (s, y, size = 11) => page.text(s, { x: 56, y, font, size });
27
+ page.artifact({ type: 'Pagination', subtype: 'Header' }, () => line('Factur-X invoice: the XML inside this PDF is the legal invoice', 24, 8));
28
+ page.tag('H1', () => line(`Invoice ${number}`, 60, 22));
29
+ page.tag('P', () => line(`Seller: ${seller}`, 110));
30
+ page.tag('Table', () => {
31
+ page.tag('TR', () => ['Net', 'VAT', 'Total'].forEach((h, i) => page.tag('TH', () => page.text(h, { x: 56 + i * 140, y: 150, font }), { scope: 'Column', id: `h${i}` })));
32
+ page.tag('TR', () => [field('TaxBasisTotalAmount'), field('TaxTotalAmount'), field('GrandTotalAmount')].forEach((v, i) =>
33
+ page.tag('TD', () => page.text(`${v} ${field('InvoiceCurrencyCode')}`, { x: 56 + i * 140, y: 170, font }), { headers: [`h${i}`] })));
34
+ });
35
+ await pdf.facturX(xml, { profile });
36
+ await pdf.end();
37
+ console.log(`wrote ${outFile}: PDF/A-3a with factur-x.xml (${profile})`);
@@ -0,0 +1,27 @@
1
+ // A shaper for @reportwright/pdf's embedFont(bytes, { shaper }) built on harfbuzzjs (npm i harfbuzzjs). harfbuzzjs is
2
+ // not a dependency of @reportwright/pdf: copy this file, or import it where harfbuzzjs is installed.
3
+ //
4
+ // import { harfbuzzShaper } from './harfbuzz-shaper.mjs';
5
+ // const bytes = fs.readFileSync('NotoSansArabic-Regular.ttf');
6
+ // const arabic = await pdf.embedFont(bytes, { shaper: harfbuzzShaper(bytes) });
7
+ // page.text('مرحبا بالعالم', { x: 40, y: 40, font: arabic, size: 18, direction: 'rtl' });
8
+ //
9
+ // The shaper gets one run (one font, one direction) and returns its glyphs in visual order, in font units:
10
+ // { g: glyph ID, cl: cluster (UTF-16 index of its first character), ax: x advance, dx, dy: offsets }.
11
+ import * as hb from 'harfbuzzjs';
12
+
13
+ const NO_KERN = [new hb.Feature('kern', 0)];
14
+
15
+ /** @param {Uint8Array} fontBytes the same bytes passed to embedFont */
16
+ export function harfbuzzShaper(fontBytes) {
17
+ const font = new hb.Font(new hb.Face(new hb.Blob(fontBytes))); // scale: units per em, so positions are font units
18
+ return (text, _font, { direction, kerning }) => {
19
+ const b = new hb.Buffer();
20
+ b.addText(text); // clusters are UTF-16 indices, as JavaScript strings count
21
+ b.guessSegmentProperties(); // script and language from the text
22
+ b.setDirection(direction === 'rtl' ? hb.Direction.RTL : hb.Direction.LTR);
23
+ hb.shape(font, b, kerning ? [] : NO_KERN);
24
+ const pos = b.getGlyphPositions();
25
+ return b.getGlyphInfos().map((g, i) => ({ g: g.codepoint, cl: g.cluster, ax: pos[i].xAdvance, dx: pos[i].xOffset, dy: pos[i].yOffset }));
26
+ };
27
+ }
package/index.d.ts CHANGED
@@ -74,6 +74,11 @@ export interface PdfOptions {
74
74
  pdfa?: boolean | { part?: 1 | 2 | 3; conformance?: 'a' | 'b' | 'u'; outputIntent?: Uint8Array | ArrayBuffer };
75
75
  /** PDF/UA-1: a structure tree from page.tag(). Needs title; fonts must be embedded. */
76
76
  tagged?: boolean | { lang?: string };
77
+ /**
78
+ * false: no XMP metadata stream, the Info dictionary only (about 1 KB smaller: a one-page invoice). Default true.
79
+ * Refused with pdfa or tagged: PDF/A and PDF/UA require the XMP.
80
+ */
81
+ metadata?: boolean;
77
82
  /** stop the export: pending and later calls reject with signal.reason */
78
83
  signal?: AbortSignal;
79
84
  /** stop the export after this many milliseconds (a TimeoutError, code 'ETIMEDOUT') */
@@ -703,7 +708,7 @@ export interface ReadBudget {
703
708
  maxSteps?: number;
704
709
  /** cross-reference entries and scanned objects held (default 5,000,000) */
705
710
  maxObjects?: number;
706
- /** pieces of text one page's extraction holds (default 2,000,000) */
711
+ /** pieces of text one page's extraction holds (default 1,000,000; a page also stops at 16,000,000 characters) */
707
712
  maxTextItems?: number;
708
713
  }
709
714
  export interface LoadOptions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reportwright/pdf",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.0-beta.1",
4
4
  "description": "A streaming PDF writer for Node and browsers with no runtime dependencies: flat memory, PDF/A-1b to 3a, PDF/UA-1, Factur-X, embedded TrueType with Unicode text.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,7 +15,7 @@
15
15
  "./package.json": "./package.json"
16
16
  },
17
17
  "types": "./index.d.ts",
18
- "files": ["dist", "index.d.ts", "README.md", "LICENSE", "THIRD-PARTY-NOTICES.md"],
18
+ "files": ["dist", "index.d.ts", "examples", "README.md", "LICENSE", "THIRD-PARTY-NOTICES.md"],
19
19
  "engines": { "node": ">=20.16" },
20
20
  "sideEffects": false
21
21
  }