reamkit 1.33.2 → 1.34.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.
Files changed (71) hide show
  1. package/README.md +13 -10
  2. package/dist/esm/core/converter/facade.js +1 -0
  3. package/dist/esm/core/converter/ream.d.ts +6 -6
  4. package/dist/esm/core/converter/ream.js +11 -5
  5. package/dist/esm/core/document-model/index.d.ts +1 -1
  6. package/dist/esm/core/document-model/types.d.ts +15 -0
  7. package/dist/esm/core/drawingml/text-warp.d.ts +34 -0
  8. package/dist/esm/core/drawingml/text-warp.js +49 -1
  9. package/dist/esm/core/font/index.d.ts +1 -1
  10. package/dist/esm/core/font/index.js +8 -0
  11. package/dist/esm/core/font/indic-shaper.d.ts +47 -0
  12. package/dist/esm/core/font/indic-shaper.js +1146 -0
  13. package/dist/esm/core/font/indic-table.d.ts +12 -0
  14. package/dist/esm/core/font/indic-table.js +463 -0
  15. package/dist/esm/core/font/measure.d.ts +45 -4
  16. package/dist/esm/core/font/measure.js +253 -14
  17. package/dist/esm/core/font/opentype-lookups.d.ts +214 -0
  18. package/dist/esm/core/font/opentype-lookups.js +1112 -0
  19. package/dist/esm/core/fonts/families.d.ts +4 -0
  20. package/dist/esm/core/fonts/families.js +43 -12
  21. package/dist/esm/core/fonts/provider.js +1 -0
  22. package/dist/esm/core/fonts/remote-fonts.d.ts +4 -2
  23. package/dist/esm/core/fonts/remote-fonts.js +36 -0
  24. package/dist/esm/core/fonts/scripts.d.ts +19 -0
  25. package/dist/esm/core/fonts/scripts.js +96 -1
  26. package/dist/esm/core/numbering/apply.js +12 -8
  27. package/dist/esm/core/style-cascade/index.d.ts +1 -1
  28. package/dist/esm/core/style-cascade/index.js +2 -2
  29. package/dist/esm/core/style-cascade/resolver.d.ts +9 -0
  30. package/dist/esm/core/style-cascade/resolver.js +31 -3
  31. package/dist/esm/core/style-cascade/table.js +3 -8
  32. package/dist/esm/excel/xlsx-to-pdf.js +1 -0
  33. package/dist/esm/index.js +1 -0
  34. package/dist/esm/layout/line-paint.d.ts +5 -4
  35. package/dist/esm/layout/line-paint.js +6 -5
  36. package/dist/esm/layout/page-doc.d.ts +26 -0
  37. package/dist/esm/layout/styled-layout.d.ts +8 -1
  38. package/dist/esm/layout/styled-layout.js +459 -112
  39. package/dist/esm/layout/undrawn.d.ts +18 -0
  40. package/dist/esm/layout/undrawn.js +59 -0
  41. package/dist/esm/pdf/cid-font.d.ts +5 -0
  42. package/dist/esm/pdf/cid-font.js +8 -2
  43. package/dist/esm/pdf/styled-page-emitter.js +117 -28
  44. package/dist/esm/pdf/text-page-renderer.js +1 -0
  45. package/dist/esm/pdf-reader/content.d.ts +1 -1
  46. package/dist/esm/pdf-reader/content.js +48 -13
  47. package/dist/esm/pdf-reader/embedded-fonts.js +1 -0
  48. package/dist/esm/pdf-reader/encodings.d.ts +9 -0
  49. package/dist/esm/pdf-reader/encodings.js +66 -1
  50. package/dist/esm/pdf-reader/face-outlines.js +1 -0
  51. package/dist/esm/pdf-reader/text.d.ts +10 -0
  52. package/dist/esm/pdf-reader/text.js +25 -3
  53. package/dist/esm/pdf-reader/vector.js +2 -2
  54. package/dist/esm/pptx/embedded-fonts.js +1 -0
  55. package/dist/esm/pptx/sp-helpers.js +4 -1
  56. package/dist/esm/svg/svg-text.js +30 -7
  57. package/dist/esm/word/doc/doc-reader.js +148 -57
  58. package/dist/esm/word/doc/doc-text.d.ts +80 -11
  59. package/dist/esm/word/doc/doc-text.js +438 -75
  60. package/dist/esm/word/document-parser.js +48 -28
  61. package/dist/esm/word/docx-to-pdf.d.ts +5 -5
  62. package/dist/esm/word/docx-to-pdf.js +9 -7
  63. package/dist/esm/word/docx-writer.js +3 -5
  64. package/dist/esm/word/font-embed.js +1 -0
  65. package/dist/esm/word/font-table.js +1 -0
  66. package/dist/esm/word/numbering-parser.js +3 -0
  67. package/dist/esm/word/paragraph-properties.js +2 -2
  68. package/dist/esm/word/run-properties.js +2 -2
  69. package/dist/esm/word/xml-helpers.d.ts +18 -0
  70. package/dist/esm/word/xml-helpers.js +48 -1
  71. package/package.json +2 -2
@@ -0,0 +1,18 @@
1
+ import { Loss } from '../core/ir/index.js';
2
+ import { LaidOutPage } from './page-doc.js';
3
+ /**
4
+ * The characters the pages draw with no glyph, each once, in the order they
5
+ * first appear.
6
+ *
7
+ * @param pages The laid-out pages.
8
+ * @returns The code points, and the 1-based page each first appears on.
9
+ */
10
+ export declare function undrawnCharacters(pages: ReadonlyArray<LaidOutPage>): Map<number, number>;
11
+ /**
12
+ * The loss for the characters the pages draw as boxes, or `undefined` when
13
+ * every character has a glyph.
14
+ *
15
+ * @param pages The laid-out pages.
16
+ * @returns One loss that names the characters, at the page the first is on.
17
+ */
18
+ export declare function undrawnLoss(pages: ReadonlyArray<LaidOutPage>): Loss | undefined;
@@ -0,0 +1,59 @@
1
+ //#region src/layout/undrawn.ts
2
+ /** How many of the characters a loss names before it only counts the rest. */
3
+ var NAMED = 8;
4
+ /**
5
+ * The characters the pages draw with no glyph, each once, in the order they
6
+ * first appear.
7
+ *
8
+ * @param pages The laid-out pages.
9
+ * @returns The code points, and the 1-based page each first appears on.
10
+ */
11
+ function undrawnCharacters(pages) {
12
+ const out = /* @__PURE__ */ new Map();
13
+ let pageNo = 0;
14
+ const readLine = (line) => {
15
+ for (const t of line.tokens) if (t.kind === "text") for (const ch of t.text) {
16
+ const cp = ch.codePointAt(0);
17
+ if (isControl(cp) || out.has(cp)) continue;
18
+ if (t.font.parsed.glyphForCodepoint(cp) === 0) out.set(cp, pageNo);
19
+ }
20
+ else if (t.kind === "math") for (const item of t.items) {
21
+ if (item.kind !== "glyph") continue;
22
+ for (const ch of item.text) {
23
+ const cp = ch.codePointAt(0);
24
+ if (isControl(cp) || out.has(cp)) continue;
25
+ if (item.font.parsed.glyphForCodepoint(cp) === 0) out.set(cp, pageNo);
26
+ }
27
+ }
28
+ else for (const text of t.metafile?.texts ?? []) readLine(text.line);
29
+ };
30
+ for (const page of pages) {
31
+ pageNo++;
32
+ for (const item of page.commands) if (item.type === "line") readLine(item.line);
33
+ }
34
+ return out;
35
+ }
36
+ /**
37
+ * The loss for the characters the pages draw as boxes, or `undefined` when
38
+ * every character has a glyph.
39
+ *
40
+ * @param pages The laid-out pages.
41
+ * @returns One loss that names the characters, at the page the first is on.
42
+ */
43
+ function undrawnLoss(pages) {
44
+ const undrawn = undrawnCharacters(pages);
45
+ if (undrawn.size === 0) return void 0;
46
+ const cps = [...undrawn.keys()];
47
+ return {
48
+ severity: "dropped",
49
+ feature: "text",
50
+ detail: `no available font has a glyph for ${cps.slice(0, NAMED).map((cp) => `U+${cp.toString(16).toUpperCase().padStart(4, "0")}`).join(", ")}${cps.length > NAMED ? ` and ${cps.length - NAMED} more` : ""}; they are drawn as empty boxes and are missing from the text layer`,
51
+ where: `page ${undrawn.get(cps[0])}`
52
+ };
53
+ }
54
+ /** C0 and C1 controls: a line break or a tab is never drawn as a glyph. */
55
+ function isControl(cp) {
56
+ return cp < 32 || cp >= 127 && cp < 160;
57
+ }
58
+ //#endregion
59
+ export { undrawnCharacters, undrawnLoss };
@@ -10,6 +10,11 @@ export interface EmbedTtfOptions {
10
10
  * otherwise, since PDF/A-2/3 only constrain it when present).
11
11
  */
12
12
  readonly cidSet?: boolean;
13
+ /**
14
+ * The text of glyphs no character maps to — a conjunct, a reph, a vowel
15
+ * sign's contextual form — so that /ToUnicode names them too.
16
+ */
17
+ readonly glyphText?: ReadonlyMap<number, string>;
13
18
  }
14
19
  /** The result of {@link embedTtfFont}: the Type 0 font reference plus measurement/encoding helpers. */
15
20
  export interface EmbeddedFont {
@@ -1,6 +1,7 @@
1
1
  import { glyphClosure, subsetTtf } from "../core/font/ttf-subset.js";
2
2
  import { lettersForLigature } from "../core/font/ligatures.js";
3
3
  import { createFontMeasure } from "../core/font/measure.js";
4
+ import "../core/font/index.js";
4
5
  import { deflatedStream, dict, name, ref } from "./objects.js";
5
6
  //#region src/pdf/cid-font.ts
6
7
  var encoder = new TextEncoder();
@@ -57,7 +58,7 @@ function embedTtfFont(doc, parsed, options = {}) {
57
58
  W: widthRuns.flatMap(([first, ws]) => [first, ws]),
58
59
  CIDToGIDMap: name("Identity")
59
60
  }));
60
- const toUnicodeRef = doc.add(deflatedStream({}, buildToUnicodeCMap(parsed, subsetGids)));
61
+ const toUnicodeRef = doc.add(deflatedStream({}, buildToUnicodeCMap(parsed, subsetGids, options.glyphText)));
61
62
  const type0Ref = doc.add(dict({
62
63
  Type: name("Font"),
63
64
  Subtype: name("Type0"),
@@ -126,7 +127,7 @@ function codeRank(cp) {
126
127
  if (cp >= 64256 && cp <= 65023 || cp >= 65136 && cp <= 65279) return 1;
127
128
  return 0;
128
129
  }
129
- function buildToUnicodeCMap(parsed, subsetGids) {
130
+ function buildToUnicodeCMap(parsed, subsetGids, glyphText) {
130
131
  const inSubset = subsetGids ? new Set(subsetGids) : void 0;
131
132
  const gidToCps = /* @__PURE__ */ new Map();
132
133
  const order = [];
@@ -169,6 +170,11 @@ function buildToUnicodeCMap(parsed, subsetGids) {
169
170
  changed = true;
170
171
  }
171
172
  }
173
+ for (const [gid, text] of glyphText ?? []) {
174
+ if (gidToCps.has(gid)) continue;
175
+ gidToCps.set(gid, [...text].map((c) => c.codePointAt(0)));
176
+ order.push(gid);
177
+ }
172
178
  const pairs = order.filter((g) => inSubset === void 0 || inSubset.has(g)).map((g) => [g, gidToCps.get(g)]).sort((a, b) => a[0] - b[0]);
173
179
  const utf16beHex = (cps) => {
174
180
  let hex = "";
@@ -7,7 +7,7 @@ import "../layout/styled-layout.js";
7
7
  import { BORDER_DASHES, FAUX_ITALIC_SHEAR, computeJustifyExtra, lineVisualOrder, settableInOneMatrix, washVeil } from "../layout/line-paint.js";
8
8
  import { emitClipPath, emitVectorShape, shadowBlurLayers } from "./vector-graphics.js";
9
9
  import { buildGradientAlphaMask, buildGradientPattern, shapeBbox } from "./shading.js";
10
- import { warpGlyphMatrix } from "../core/drawingml/text-warp.js";
10
+ import { warpGlyphMatrix, warpGlyphRun } from "../core/drawingml/text-warp.js";
11
11
  import { sanitizeHref } from "../core/links.js";
12
12
  import { embedAssociatedFile } from "./embedded-file.js";
13
13
  import { buildSrgbIccProfile } from "./icc-profile.js";
@@ -523,7 +523,8 @@ function embedFontResources(doc, laid, pdfaProfile) {
523
523
  if (res.gids.size === 0) continue;
524
524
  out.set(variant, embedTtfFont(doc, res.parsed, {
525
525
  usedGids: res.gids,
526
- cidSet
526
+ cidSet,
527
+ ...res.glyphText ? { glyphText: res.glyphText } : {}
527
528
  }));
528
529
  }
529
530
  return out;
@@ -677,7 +678,52 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
677
678
  let lastColor = "";
678
679
  let lastFauxWidth = 0;
679
680
  let lastStroke;
681
+ let strokeFollowsFill = false;
680
682
  let lastTz = 100;
683
+ /** A run's letter spacing, in the text space `Tz` scales (§9.4.4). */
684
+ const spacingOf = (tok) => {
685
+ const tz = Math.round((tok.synthetic?.widthScale ?? 1) * (tok.resolvedRun.widthScale ?? 1) * 100);
686
+ return (tok.resolvedRun.letterSpacingPt ?? 0) / (tz > 0 ? tz / 100 : 1);
687
+ };
688
+ /**
689
+ * The `rg` that makes `hex` the fill colour, when it is not already, and
690
+ * the `RG` with it while a faked bold strokes the glyphs in their own colour.
691
+ */
692
+ const fillTo = (hex) => {
693
+ const ops = [];
694
+ const [r, g, b] = hexToRgb01(hex);
695
+ const rgb = `${formatNumber(r)} ${formatNumber(g)} ${formatNumber(b)}`;
696
+ if (hex !== lastColor) {
697
+ ops.push(`${rgb} rg`);
698
+ lastColor = hex;
699
+ }
700
+ if (strokeFollowsFill && hex !== lastStroke) {
701
+ ops.push(`${rgb} RG`);
702
+ lastStroke = hex;
703
+ }
704
+ return ops.length > 0 ? ops.join(" ") : void 0;
705
+ };
706
+ /**
707
+ * The show operators for a token's text, with what the Indic shaper needs
708
+ * passed on. A syllable a run boundary cut is drawn in its runs' colours: the
709
+ * fill colour switches between its glyphs, each glyph in the colour of the
710
+ * first character it stands for, and the text state is left in the last.
711
+ *
712
+ * @param colours Per code point of `text`, its colour (the token's own,
713
+ * reversed with the text where it is).
714
+ */
715
+ const showTextOf = (tok, text, colours = tok.charColorsHex) => {
716
+ const measure = tok.font.measure;
717
+ if (!measure.clustered(text)) return measure.showText(text);
718
+ const sources = colours ? measure.glyphSources(text) : void 0;
719
+ if (!colours || !sources) return measure.showText(text, tok.fontSizePt, spacingOf(tok));
720
+ const before = /* @__PURE__ */ new Map();
721
+ sources.forEach((s, i) => {
722
+ const op = fillTo(colours[s] ?? tok.resolvedRun.colorHex);
723
+ if (op !== void 0) before.set(i, op);
724
+ });
725
+ return measure.showText(text, tok.fontSizePt, spacingOf(tok), before);
726
+ };
681
727
  const switchFontIfNeeded = (tok) => {
682
728
  const fontKey = tok.font.resourceName;
683
729
  if (fontKey !== lastFont || tok.fontSizePt !== lastSize) {
@@ -691,13 +737,11 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
691
737
  out.push(fauxWidth > 0 ? `2 Tr ${formatNumber(fauxWidth)} w` : "0 Tr");
692
738
  lastFauxWidth = fauxWidth;
693
739
  }
694
- const strokeHex = outline?.colorHex;
695
- if (strokeHex !== lastStroke) {
696
- if (strokeHex !== void 0) {
697
- const [sr, sg, sb] = hexToRgb01(strokeHex);
698
- out.push(`${formatNumber(sr)} ${formatNumber(sg)} ${formatNumber(sb)} RG`);
699
- }
700
- lastStroke = strokeHex;
740
+ strokeFollowsFill = outline === void 0 && fauxWidth > 0;
741
+ if (outline !== void 0 && outline.colorHex !== lastStroke) {
742
+ const [sr, sg, sb] = hexToRgb01(outline.colorHex);
743
+ out.push(`${formatNumber(sr)} ${formatNumber(sg)} ${formatNumber(sb)} RG`);
744
+ lastStroke = outline.colorHex;
701
745
  }
702
746
  const scale = (tok.synthetic?.widthScale ?? 1) * (tok.resolvedRun.widthScale ?? 1);
703
747
  const tz = Math.round(scale * 100);
@@ -705,16 +749,13 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
705
749
  out.push(`${formatNumber(tz)} Tz`);
706
750
  lastTz = tz;
707
751
  }
708
- const tc = (tok.resolvedRun.letterSpacingPt ?? 0) / (tz > 0 ? tz / 100 : 1);
752
+ const tc = tok.font.measure.clustered(tok.text) ? 0 : spacingOf(tok);
709
753
  if (tc !== lastTc) {
710
754
  out.push(`${formatNumber(tc)} Tc`);
711
755
  lastTc = tc;
712
756
  }
713
- if (tok.resolvedRun.colorHex !== lastColor) {
714
- const [r, g, b] = hexToRgb01(tok.resolvedRun.colorHex);
715
- out.push(`${formatNumber(r)} ${formatNumber(g)} ${formatNumber(b)} rg`);
716
- lastColor = tok.resolvedRun.colorHex;
717
- }
757
+ const fill = fillTo(tok.resolvedRun.colorHex);
758
+ if (fill !== void 0) out.push(fill);
718
759
  };
719
760
  const emitImageToken = (tok, x, baselineY) => {
720
761
  const meta = tok.metafile;
@@ -1024,12 +1065,32 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
1024
1065
  const hasImageToken = line.tokens.some((t) => t.kind === "image");
1025
1066
  const hasMathToken = line.tokens.some((t) => t.kind === "math");
1026
1067
  const hasRtl = line.tokens.some((t) => t.bidiLevel % 2 === 1);
1027
- const hasTab = line.tokens.some((t) => t.kind === "text" && t.tab === true);
1068
+ const hasTab = line.tokens.some((t) => t.kind === "text" && (t.tab === true || t.listMarkerSpace === true));
1028
1069
  /** A text token carrying underline or strikethrough. */
1029
1070
  const isDecorated = (tok) => tok.kind === "text" && (tok.resolvedRun.underline !== "none" || tok.resolvedRun.strike);
1030
1071
  const showToken = (tok) => {
1031
- const text = tok.bidiLevel % 2 === 1 ? reverseByCodePoint(tok.text) : tok.text;
1032
- return tok.font.measure.showText(text);
1072
+ if (tok.bidiLevel % 2 === 0) return showTextOf(tok, tok.text);
1073
+ return showTextOf(tok, reverseByCodePoint(tok.text), tok.charColorsHex?.slice().reverse());
1074
+ };
1075
+ const actualText = (tokens) => {
1076
+ const open = tokens.map(() => "");
1077
+ const close = tokens.map(() => false);
1078
+ const shaped = (t) => t?.kind === "text" && !t.isSpace && t.tab !== true && t.font.measure.clustered(t.text);
1079
+ for (let i = 0; i < tokens.length; i++) {
1080
+ if (!shaped(tokens[i])) continue;
1081
+ let j = i;
1082
+ let text = "";
1083
+ for (let t = tokens[j]; shaped(t); t = tokens[++j]) text += t.text;
1084
+ let hex = "";
1085
+ for (const b of unicodeString(text).bytes) hex += b.toString(16).padStart(2, "0");
1086
+ open[i] = `/Span <</ActualText <${hex.toUpperCase()}>>> BDC`;
1087
+ close[j - 1] = true;
1088
+ i = j - 1;
1089
+ }
1090
+ return {
1091
+ open,
1092
+ close
1093
+ };
1033
1094
  };
1034
1095
  if (line.tokens.some((t) => t.kind === "text" && t.resolvedRun.shadingColorHex)) {
1035
1096
  if (inBT) {
@@ -1129,13 +1190,31 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
1129
1190
  out.push("BT");
1130
1191
  inBT = true;
1131
1192
  }
1193
+ const placeGlyph = (m) => `${formatNumber(m[0])} ${formatNumber(-m[1])} ${formatNumber(m[2])} ${formatNumber(-m[3])} ${formatNumber(m[4])} ${formatNumber(H - m[5])} Tm`;
1194
+ const at = actualText(line.tokens);
1132
1195
  let cursor = originX;
1133
- for (const tok of line.tokens) {
1196
+ for (let ti = 0; ti < line.tokens.length; ti++) {
1197
+ const tok = line.tokens[ti];
1134
1198
  if (tok.kind !== "text" || tok.isSpace) {
1135
1199
  cursor += tok.widthPt;
1136
1200
  continue;
1137
1201
  }
1138
1202
  switchFontIfNeeded(tok);
1203
+ if (tok.font.measure.clustered(tok.text)) {
1204
+ if (at.open[ti]) out.push(at.open[ti] ?? "");
1205
+ const glyphs = warpGlyphRun(warp, tok.font.measure.glyphRun(tok.text), cursor, tok.widthPt, tok.fontSizePt / tok.font.parsed.unitsPerEm, cmd.baselineY);
1206
+ const colours = tok.charColorsHex;
1207
+ const sources = colours ? tok.font.measure.glyphSources(tok.text) : void 0;
1208
+ for (const g of glyphs) {
1209
+ const fill = sources ? fillTo(colours?.[sources[g.index]] ?? tok.resolvedRun.colorHex) : void 0;
1210
+ if (fill !== void 0) out.push(fill);
1211
+ out.push(placeGlyph(g.matrix));
1212
+ out.push(`<${g.gid.toString(16).padStart(4, "0").toUpperCase()}> Tj`);
1213
+ }
1214
+ if (at.close[ti]) out.push("EMC");
1215
+ cursor += tok.widthPt;
1216
+ continue;
1217
+ }
1139
1218
  const chars = [...tok.text];
1140
1219
  const raw = chars.map((c) => tok.font.measure.textWidthPt(c, tok.fontSizePt));
1141
1220
  const total = raw.reduce((a, b) => a + b, 0);
@@ -1145,7 +1224,7 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
1145
1224
  const m = warpGlyphMatrix(warp, cursor, adv, cmd.baselineY);
1146
1225
  cursor += adv;
1147
1226
  if (!m) continue;
1148
- out.push(`${formatNumber(m[0])} ${formatNumber(-m[1])} ${formatNumber(m[2])} ${formatNumber(-m[3])} ${formatNumber(m[4])} ${formatNumber(H - m[5])} Tm`);
1227
+ out.push(placeGlyph(m));
1149
1228
  out.push(tok.font.measure.showText(chars[i]));
1150
1229
  }
1151
1230
  }
@@ -1160,16 +1239,20 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
1160
1239
  const cos = formatNumber(Math.cos(rad));
1161
1240
  const sin = formatNumber(Math.sin(rad));
1162
1241
  out.push(`${cos} ${sin} ${formatNumber(-Math.sin(rad))} ${cos} ${formatNumber(originX)} ${formatNumber(baselineY)} Tm`);
1163
- for (const tok of line.tokens) {
1164
- if (tok.kind !== "text") continue;
1242
+ const at = actualText(line.tokens);
1243
+ line.tokens.forEach((tok, i) => {
1244
+ if (tok.kind !== "text") return;
1165
1245
  switchFontIfNeeded(tok);
1166
- out.push(tok.font.measure.showText(tok.text));
1167
- }
1246
+ if (at.open[i]) out.push(at.open[i] ?? "");
1247
+ out.push(showTextOf(tok, tok.text));
1248
+ if (at.close[i]) out.push("EMC");
1249
+ });
1168
1250
  } else if (extraPerSpace !== 0 || hasImageToken || hasMathToken || hasRtl || hasRise || hasFauxItalic || hasTab) {
1169
1251
  const order = hasRtl ? lineVisualOrder(line) : line.tokens.map((_, i) => i);
1252
+ const at = actualText(order.map((i) => line.tokens[i]));
1170
1253
  let x = originX;
1171
- for (const ti of order) {
1172
- const tok = line.tokens[ti];
1254
+ for (let oi = 0; oi < order.length; oi++) {
1255
+ const tok = line.tokens[order[oi]];
1173
1256
  if (tok.kind === "image") {
1174
1257
  flushLink();
1175
1258
  emitImageToken(tok, x, baselineY);
@@ -1189,7 +1272,9 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
1189
1272
  switchFontIfNeeded(tok);
1190
1273
  const slant = tok.synthetic?.italic === true ? FAUX_ITALIC_SHEAR : 0;
1191
1274
  out.push(`1 0 ${formatNumber(slant)} 1 ${formatNumber(x)} ${formatNumber(baselineY + (tok.risePt ?? 0))} Tm`);
1275
+ if (at.open[oi]) out.push(at.open[oi] ?? "");
1192
1276
  out.push(showToken(tok));
1277
+ if (at.close[oi]) out.push("EMC");
1193
1278
  const tokenX0 = x;
1194
1279
  x += tok.widthPt;
1195
1280
  if (tok.isSpace) x += extraPerSpace;
@@ -1202,11 +1287,15 @@ function emitPageContent(page, tagging, gradientNames, alphaStateNames, duotoneS
1202
1287
  inBT = true;
1203
1288
  }
1204
1289
  out.push(`1 0 0 1 ${formatNumber(originX)} ${formatNumber(baselineY)} Tm`);
1290
+ const at = actualText(line.tokens);
1205
1291
  let x = originX;
1206
- for (const tok of line.tokens) {
1292
+ for (let i = 0; i < line.tokens.length; i++) {
1293
+ const tok = line.tokens[i];
1207
1294
  if (tok.kind !== "text") continue;
1208
1295
  switchFontIfNeeded(tok);
1209
- out.push(tok.font.measure.showText(tok.text));
1296
+ if (at.open[i]) out.push(at.open[i] ?? "");
1297
+ out.push(showTextOf(tok, tok.text));
1298
+ if (at.close[i]) out.push("EMC");
1210
1299
  trackLink(tok.href, tok.anchor, tok.text, x, x + tok.widthPt);
1211
1300
  x += tok.widthPt;
1212
1301
  }
@@ -1,3 +1,4 @@
1
+ import "../core/font/index.js";
1
2
  import "./objects.js";
2
3
  import "./cid-font.js";
3
4
  import "./writer.js";
@@ -423,7 +423,7 @@ export type Matrix = readonly [number, number, number, number, number, number];
423
423
  export declare const IDENTITY: Matrix;
424
424
  /** Compose two {@link Matrix matrices}: `a` applied first, then `b`. */
425
425
  export declare function multiply(a: Matrix, b: Matrix): Matrix;
426
- export declare function interpretContent(bytes: Uint8Array, fonts: ReadonlyMap<string, ContentFont>, initialCtm?: Matrix, shadings?: ReadonlyMap<string, PageGradient>, alphas?: ReadonlyMap<string, GsPaint>, spaces?: ReadonlyMap<string, ColorSpaceInfo>, hiddenOc?: ReadonlySet<string>): InterpretResult;
426
+ export declare function interpretContent(bytes: Uint8Array, fonts: ReadonlyMap<string, ContentFont>, initialCtm?: Matrix, shadings?: ReadonlyMap<string, PageGradient>, alphas?: ReadonlyMap<string, GsPaint>, spaces?: ReadonlyMap<string, ColorSpaceInfo>, hiddenOc?: ReadonlySet<string>, actualTexts?: ReadonlyMap<string, string>): InterpretResult;
427
427
  /**
428
428
  * Whether a string is wholly right-to-left: at least one letter of an RTL
429
429
  * script and nothing of any other, spaces and joiners aside.
@@ -2,6 +2,7 @@ import { PDF_NULL, PdfHexString, PdfName } from "../pdf/objects.js";
2
2
  import { bidiClass } from "../core/bidi/char-types.js";
3
3
  import { reorderVisual } from "../core/bidi/algorithm.js";
4
4
  import { analyzeString, hasBidiCharacters } from "../core/bidi/index.js";
5
+ import { pdfTextString } from "./encodings.js";
5
6
  import { Lexer } from "./lexer.js";
6
7
  import { cmykHex, dashLengths, grayHex, rgbHex, spaceColor } from "./shading.js";
7
8
  //#region src/pdf-reader/content.ts
@@ -184,7 +185,7 @@ function midGradient(gradient) {
184
185
  function colorOfOperands(operands, space) {
185
186
  return spaceColor(operands.filter((o) => typeof o === "number"), space);
186
187
  }
187
- function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__PURE__ */ new Map(), alphas = /* @__PURE__ */ new Map(), spaces = /* @__PURE__ */ new Map(), hiddenOc = /* @__PURE__ */ new Set()) {
188
+ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__PURE__ */ new Map(), alphas = /* @__PURE__ */ new Map(), spaces = /* @__PURE__ */ new Map(), hiddenOc = /* @__PURE__ */ new Set(), actualTexts = /* @__PURE__ */ new Map()) {
188
189
  const runs = [];
189
190
  const images = [];
190
191
  const vectors = [];
@@ -206,6 +207,7 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
206
207
  const ocStack = [];
207
208
  let hiddenDepth = 0;
208
209
  const visible = () => hiddenDepth === 0;
210
+ let replacing;
209
211
  const toPage = (x, y) => [x * state.ctm[0] + y * state.ctm[2] + state.ctm[4], x * state.ctm[1] + y * state.ctm[3] + state.ctm[5]];
210
212
  const moveTo = (x, y) => {
211
213
  const [px, py] = toPage(x, y);
@@ -379,17 +381,20 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
379
381
  if (!set) codesShown.set(state.font, set = /* @__PURE__ */ new Set());
380
382
  for (const code of codes) set.add(code);
381
383
  }
382
- return state.font.decode(codes);
384
+ return {
385
+ text: state.font.decode(codes),
386
+ glyphs: codes.length
387
+ };
383
388
  };
384
389
  const emitAt = (origin, shown, end) => {
385
- const text = logicalOrder(shown);
386
- if (text.length === 0) return;
390
+ const text = replacing ? "" : logicalOrder(shown.text);
391
+ if (replacing ? shown.glyphs === 0 : text.length === 0) return;
387
392
  const scaleY = Math.hypot(origin[2], origin[3]) || 1;
388
393
  const scaleX = (Math.hypot(origin[0], origin[1]) || 1) * state.hScale;
389
394
  const mcid = mcStack.length > 0 ? mcStack[mcStack.length - 1] : void 0;
390
395
  const angle = Math.atan2(origin[1], origin[0]) * 180 / Math.PI;
391
396
  if (!visible()) return;
392
- runs.push({
397
+ const run = {
393
398
  text,
394
399
  x: origin[4],
395
400
  y: origin[5],
@@ -413,19 +418,36 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
413
418
  ...state.font.italic ? { italic: true } : {},
414
419
  colorHex: state.fillGradient ? midGradient(state.fillGradient) : state.fillColor,
415
420
  ...mcid !== void 0 ? { mcid } : {}
421
+ };
422
+ if (replacing) {
423
+ replacing.first ??= run;
424
+ replacing.endX = end[4];
425
+ replacing.endY = end[5];
426
+ } else runs.push(run);
427
+ };
428
+ const closeReplacement = () => {
429
+ const open = replacing;
430
+ replacing = void 0;
431
+ if (!open?.first || open.text.length === 0) return;
432
+ outlines.splice(open.outlinesFrom);
433
+ runs.push({
434
+ ...open.first,
435
+ text: open.text.replace(/[\t\n\v\f\r]/gu, " "),
436
+ endX: open.endX ?? open.first.endX,
437
+ endY: open.endY ?? open.first.endY
416
438
  });
417
439
  };
418
440
  const showString = (operand) => {
419
441
  const origin = multiply(tm, state.ctm);
420
- const text = consume(operand);
421
- emitAt(origin, text, multiply(tm, state.ctm));
442
+ const shown = consume(operand);
443
+ emitAt(origin, shown, multiply(tm, state.ctm));
422
444
  };
423
445
  const showArray = (arr) => {
424
446
  for (const el of arr) if (typeof el === "number") tm = multiply(translation(-el / 1e3 * state.fontSize * state.hScale, 0), tm);
425
447
  else if (typeof el === "string" || el instanceof PdfHexString) {
426
448
  const origin = multiply(tm, state.ctm);
427
- const text = consume(el);
428
- emitAt(origin, text, multiply(tm, state.ctm));
449
+ const shown = consume(el);
450
+ emitAt(origin, shown, multiply(tm, state.ctm));
429
451
  }
430
452
  };
431
453
  const exec = (op) => {
@@ -507,20 +529,32 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
507
529
  case "BDC": {
508
530
  const tag = operands[operands.length - 2];
509
531
  const props = operands[operands.length - 1];
510
- const mcidVal = !(tag instanceof PdfName && tag.value === "Artifact") && props instanceof Map ? props.get("MCID") : void 0;
511
- mcStack.push(typeof mcidVal === "number" ? mcidVal : void 0);
532
+ const isArtifact = tag instanceof PdfName && tag.value === "Artifact";
533
+ const mcidVal = !isArtifact && props instanceof Map ? props.get("MCID") : void 0;
534
+ mcStack.push(typeof mcidVal === "number" ? mcidVal : isArtifact ? void 0 : mcStack[mcStack.length - 1]);
535
+ const own = props instanceof Map ? props.get("ActualText") : void 0;
536
+ const actual = typeof own === "string" || own instanceof PdfHexString ? pdfTextString(toBytes(own)) : props instanceof PdfName ? actualTexts.get(props.value) : void 0;
537
+ if (actual !== void 0 && replacing === void 0) replacing = {
538
+ text: actual,
539
+ depth: mcStack.length - 1,
540
+ outlinesFrom: outlines.length
541
+ };
512
542
  const isOc = tag instanceof PdfName && tag.value === "OC" && props instanceof PdfName && hiddenOc.has(props.value);
513
543
  ocStack.push(isOc);
514
544
  if (isOc) hiddenDepth++;
515
545
  break;
516
546
  }
517
- case "BMC":
518
- mcStack.push(void 0);
547
+ case "BMC": {
548
+ const tag = operands[operands.length - 1];
549
+ const isArtifact = tag instanceof PdfName && tag.value === "Artifact";
550
+ mcStack.push(isArtifact ? void 0 : mcStack[mcStack.length - 1]);
519
551
  ocStack.push(false);
520
552
  break;
553
+ }
521
554
  case "EMC":
522
555
  mcStack.pop();
523
556
  if (ocStack.pop() === true) hiddenDepth--;
557
+ if (replacing && mcStack.length <= replacing.depth) closeReplacement();
524
558
  break;
525
559
  case "Do": {
526
560
  const nm = operands[0];
@@ -747,6 +781,7 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
747
781
  default: operands = [];
748
782
  }
749
783
  }
784
+ closeReplacement();
750
785
  return {
751
786
  texts: runs,
752
787
  images,
@@ -1,5 +1,6 @@
1
1
  import { parseTtf } from "../core/font/ttf-parser.js";
2
2
  import { FontRegistry } from "../core/font/font-registry.js";
3
+ import "../core/font/index.js";
3
4
  import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
4
5
  import { FEATURES } from "../core/ir/features.js";
5
6
  //#region src/pdf-reader/embedded-fonts.ts
@@ -31,3 +31,12 @@ export declare function macGlyphName(index: number): string | undefined;
31
31
  * @param baseFont The name as the font dictionary states it.
32
32
  */
33
33
  export declare function isStandardLatinFace(baseFont: string): boolean;
34
+ /**
35
+ * §7.9.2.2 — the characters of a text string: UTF-16BE behind its byte-order
36
+ * mark, UTF-8 behind its own (PDF 2.0), and otherwise PDFDocEncoding, one byte
37
+ * to a character.
38
+ *
39
+ * @param bytes The string's bytes, as written literal or in hex.
40
+ * @returns The text it stands for.
41
+ */
42
+ export declare function pdfTextString(bytes: Uint8Array): string;
@@ -128,5 +128,70 @@ function macGlyphName(index) {
128
128
  function isStandardLatinFace(baseFont) {
129
129
  return STANDARD_LATIN_FACES.has(baseFont.trim().replace(/^[A-Za-z]{6}\+/u, "").toLowerCase());
130
130
  }
131
+ var PDF_DOC = new Map([
132
+ ...[
133
+ 728,
134
+ 711,
135
+ 710,
136
+ 729,
137
+ 733,
138
+ 731,
139
+ 730,
140
+ 732
141
+ ].map((cp, i) => [24 + i, cp]),
142
+ ...[
143
+ 8226,
144
+ 8224,
145
+ 8225,
146
+ 8230,
147
+ 8212,
148
+ 8211,
149
+ 402,
150
+ 8260,
151
+ 8249,
152
+ 8250,
153
+ 8722,
154
+ 8240,
155
+ 8222,
156
+ 8220,
157
+ 8221,
158
+ 8216,
159
+ 8217,
160
+ 8218,
161
+ 8482,
162
+ 64257,
163
+ 64258,
164
+ 321,
165
+ 338,
166
+ 352,
167
+ 376,
168
+ 381,
169
+ 305,
170
+ 322,
171
+ 339,
172
+ 353,
173
+ 382
174
+ ].map((cp, i) => [128 + i, cp]),
175
+ [160, 8364]
176
+ ]);
177
+ /**
178
+ * §7.9.2.2 — the characters of a text string: UTF-16BE behind its byte-order
179
+ * mark, UTF-8 behind its own (PDF 2.0), and otherwise PDFDocEncoding, one byte
180
+ * to a character.
181
+ *
182
+ * @param bytes The string's bytes, as written literal or in hex.
183
+ * @returns The text it stands for.
184
+ */
185
+ function pdfTextString(bytes) {
186
+ if (bytes[0] === 254 && bytes[1] === 255) {
187
+ let out = "";
188
+ for (let i = 2; i + 1 < bytes.length; i += 2) out += String.fromCharCode(bytes[i] << 8 | bytes[i + 1]);
189
+ return out;
190
+ }
191
+ if (bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) return new TextDecoder("utf-8").decode(bytes.subarray(3));
192
+ let out = "";
193
+ for (const b of bytes) out += String.fromCodePoint(PDF_DOC.get(b) ?? b);
194
+ return out;
195
+ }
131
196
  //#endregion
132
- export { baseEncodingTable, isStandardLatinFace, macGlyphName, standardEncodingTable, winAnsiLatinName };
197
+ export { baseEncodingTable, isStandardLatinFace, macGlyphName, pdfTextString, standardEncodingTable, winAnsiLatinName };
@@ -1,4 +1,5 @@
1
1
  import { editableEmbedding } from "../core/font/ttf-build.js";
2
+ import "../core/font/index.js";
2
3
  import { isRightToLeft } from "./content.js";
3
4
  import { ASCENDER, NATURAL_LINE_EM } from "./flow-build.js";
4
5
  //#region src/pdf-reader/face-outlines.ts
@@ -16,6 +16,16 @@ import { PdfFile, PdfPage } from './document.js';
16
16
  * @returns The page's runs, each carrying an `href` when it sits under a link.
17
17
  */
18
18
  export declare function extractPageText(file: PdfFile, page: PdfPage, painted?: ShownCodes): Array<TextRun>;
19
+ /**
20
+ * §14.9.4 — the replacement text of each property list the page's
21
+ * `/Properties` names: the content says `/Span /P1 BDC` and the `/ActualText`
22
+ * is in the resources, not in the stream.
23
+ *
24
+ * @param file The owning file.
25
+ * @param resources The resource dictionary in force.
26
+ * @returns Name → the text, for the lists that hold one.
27
+ */
28
+ export declare function actualTextsOf(file: PdfFile, resources: PdfDict | undefined): Map<string, string>;
19
29
  /**
20
30
  * The `/Font` resources of one dictionary, built into interpreter fonts. Shared
21
31
  * with the path and picture walks, which need them for one thing only: a Type 3