reamkit 1.27.0 → 1.29.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 (68) hide show
  1. package/README.md +6 -5
  2. package/dist/esm/core/converter/ream.d.ts +8 -9
  3. package/dist/esm/core/converter/ream.js +0 -1
  4. package/dist/esm/core/document-model/types.d.ts +5 -3
  5. package/dist/esm/core/font/index.d.ts +1 -0
  6. package/dist/esm/core/font/ligatures.d.ts +17 -0
  7. package/dist/esm/core/font/ligatures.js +49 -0
  8. package/dist/esm/core/font/ttf-parser.d.ts +2 -1
  9. package/dist/esm/core/font/ttf-parser.js +11 -2
  10. package/dist/esm/core/fonts/remote-fonts.d.ts +1 -1
  11. package/dist/esm/core/fonts/remote-fonts.js +47 -2
  12. package/dist/esm/core/fonts/scripts.js +10 -5
  13. package/dist/esm/excel/header-footer.js +53 -8
  14. package/dist/esm/layout/styled-layout.js +53 -3
  15. package/dist/esm/pdf/cid-font.js +35 -8
  16. package/dist/esm/pdf-reader/annot-draw.js +114 -1
  17. package/dist/esm/pdf-reader/annots.d.ts +0 -14
  18. package/dist/esm/pdf-reader/annots.js +13 -1
  19. package/dist/esm/pdf-reader/ccitt.d.ts +2 -3
  20. package/dist/esm/pdf-reader/ccitt.js +32 -4
  21. package/dist/esm/pdf-reader/cff-outline.d.ts +36 -0
  22. package/dist/esm/pdf-reader/cff-outline.js +1122 -0
  23. package/dist/esm/pdf-reader/cmap.js +16 -6
  24. package/dist/esm/pdf-reader/content.d.ts +91 -0
  25. package/dist/esm/pdf-reader/content.js +156 -60
  26. package/dist/esm/pdf-reader/dingbats.d.ts +11 -0
  27. package/dist/esm/pdf-reader/dingbats.js +1033 -0
  28. package/dist/esm/pdf-reader/display.js +41 -1
  29. package/dist/esm/pdf-reader/document.d.ts +8 -0
  30. package/dist/esm/pdf-reader/document.js +26 -9
  31. package/dist/esm/pdf-reader/embedded-fonts.d.ts +11 -0
  32. package/dist/esm/pdf-reader/embedded-fonts.js +14 -1
  33. package/dist/esm/pdf-reader/encodings.d.ts +25 -0
  34. package/dist/esm/pdf-reader/encodings.js +110 -0
  35. package/dist/esm/pdf-reader/flow-build.d.ts +15 -4
  36. package/dist/esm/pdf-reader/flow-build.js +24 -11
  37. package/dist/esm/pdf-reader/font.js +329 -8
  38. package/dist/esm/pdf-reader/glyf-outline.d.ts +43 -0
  39. package/dist/esm/pdf-reader/glyf-outline.js +351 -0
  40. package/dist/esm/pdf-reader/glyph-names.js +20 -0
  41. package/dist/esm/pdf-reader/icc.d.ts +10 -0
  42. package/dist/esm/pdf-reader/icc.js +210 -0
  43. package/dist/esm/pdf-reader/image-decode.d.ts +4 -3
  44. package/dist/esm/pdf-reader/image-decode.js +85 -64
  45. package/dist/esm/pdf-reader/images.d.ts +6 -0
  46. package/dist/esm/pdf-reader/images.js +50 -7
  47. package/dist/esm/pdf-reader/jbig2.js +104 -19
  48. package/dist/esm/pdf-reader/layout.js +1531 -55
  49. package/dist/esm/pdf-reader/math-rows.d.ts +23 -0
  50. package/dist/esm/pdf-reader/math-rows.js +198 -0
  51. package/dist/esm/pdf-reader/predefined-cmap.d.ts +21 -0
  52. package/dist/esm/pdf-reader/predefined-cmap.js +102 -0
  53. package/dist/esm/pdf-reader/reader.d.ts +6 -9
  54. package/dist/esm/pdf-reader/reader.js +34 -21
  55. package/dist/esm/pdf-reader/shading.d.ts +81 -2
  56. package/dist/esm/pdf-reader/shading.js +191 -25
  57. package/dist/esm/pdf-reader/stream-filters.d.ts +6 -0
  58. package/dist/esm/pdf-reader/stream-filters.js +67 -0
  59. package/dist/esm/pdf-reader/tagged.js +1 -1
  60. package/dist/esm/pdf-reader/text-rules.js +83 -10
  61. package/dist/esm/pdf-reader/text.js +82 -3
  62. package/dist/esm/pdf-reader/type1-outline.d.ts +21 -0
  63. package/dist/esm/pdf-reader/type1-outline.js +576 -0
  64. package/dist/esm/pdf-reader/vector.d.ts +2 -0
  65. package/dist/esm/pdf-reader/vector.js +44 -4
  66. package/dist/esm/word/document-parser.js +4 -1
  67. package/dist/esm/word/docx-writer.js +28 -11
  68. package/package.json +1 -1
package/README.md CHANGED
@@ -203,16 +203,17 @@ stencil image masks, constant alpha, and the Type 3 glyphs that are drawings
203
203
  rather than letters. It honours the layers a file turns off (§8.11 optional
204
204
  content) and the box it says to show (`/CropBox`), and it decides for itself
205
205
  whether a file is a document to re-flow or a page to keep — a paper is mostly
206
- lines, a form is mostly marks — which `pdfLayout` overrides. It reads modern compressed files (cross-reference
206
+ lines, a form is mostly marks — and there is nothing to configure: a caller
207
+ cannot know which of the two it was handed. It reads modern compressed files (cross-reference
207
208
  + object streams) and encrypted ones (RC4 / AES — the user password is passed to
208
209
  `Ream.parse(bytes, { password })`, defaulting to the permissions-only case); a
209
210
  filter it does not carry can be handed to it through
210
211
  `Ream.parse(bytes, { filters })`.
211
212
 
212
- A form or a drawing is not a reflowable document, so
213
- `Ream.parse(bytes, { pdfLayout: 'positional' })` reads the page instead: every
214
- line stands where its glyphs stand, beside the artwork, the way up the page is
215
- shown.
213
+ A form or a drawing is not a reflowable document, so the reader keeps it as a
214
+ page instead: every line stands where its glyphs stand, beside the artwork, the
215
+ way up the page is shown. Which of the two readings a file gets it decides
216
+ itself and records in the losses.
216
217
 
217
218
  **Reads PowerPoint, too.** `Ream.parse` accepts a `.pptx` and turns each slide
218
219
  into a page at the deck size — text boxes (with run formatting, alignment,
@@ -21,15 +21,6 @@ export interface ReamParseOptions {
21
21
  * encryption (EP14); an encrypted OOXML package always names a password.
22
22
  */
23
23
  readonly password?: string;
24
- /**
25
- * PDF only: what to read out of the page. `'flow'` (the default) reconstructs
26
- * a re-flowable document — paragraphs and tables in reading order, from the
27
- * structure tree where the file has one. `'positional'` keeps the page as a
28
- * page: every line stands where its glyphs stand, beside the rules and fills
29
- * the page draws. A form or a drawing needs the second; a report to be edited
30
- * or converted to markdown needs the first.
31
- */
32
- readonly pdfLayout?: 'flow' | 'positional';
33
24
  /**
34
25
  * PDF only: decoders for `/Filter` names the reader does not implement
35
26
  * (§7.4). A filter it cannot undo leaves that stream unread — and when the
@@ -38,6 +29,14 @@ export interface ReamParseOptions {
38
29
  * carrying one for every filter anyone might write:
39
30
  *
40
31
  * ```ts
32
+ * // browser: any wasm/JS decoder you ship — brotli-dec-wasm is ~200 KB
33
+ * import brotliPromise from 'brotli-dec-wasm';
34
+ * const brotli = await brotliPromise;
35
+ * Ream.parse(pdf, { filters: { BrotliDecode: (b) => brotli.decompress(b) } });
36
+ * ```
37
+ *
38
+ * ```ts
39
+ * // node, where the runtime already carries one
41
40
  * import { brotliDecompressSync } from 'node:zlib';
42
41
  * Ream.parse(pdf, { filters: { BrotliDecode: (b) => brotliDecompressSync(b) } });
43
42
  * ```
@@ -88,7 +88,6 @@ var Ream = class Ream {
88
88
  if (!reader) throw new Error(`Unrecognized document format (readers: ${readers.map((r) => r.id).join(", ")})`);
89
89
  const { doc, losses } = reader.read(source, {
90
90
  password: options.password,
91
- ...options.pdfLayout ? { pdfLayout: options.pdfLayout } : {},
92
91
  ...options.filters ? { filters: options.filters } : {}
93
92
  });
94
93
  const sheet = doc.kind === "sheet" ? doc : void 0;
@@ -1571,10 +1571,12 @@ export interface SectionProperties {
1571
1571
  };
1572
1572
  /**
1573
1573
  * §17.6.22 `w:type` — where the section starts. `continuous` starts it on the
1574
- * page already in hand rather than a fresh one; everything else (nextPage,
1575
- * and the odd/even/column variants we do not distinguish) starts a page.
1574
+ * page already in hand rather than a fresh one; `nextPage` on the next sheet;
1575
+ * `oddPage` and `evenPage` on the next sheet of that PARITY, which means a
1576
+ * blank one in between when the count falls wrong. (`nextColumn` is not
1577
+ * distinguished from `nextPage`.)
1576
1578
  */
1577
- readonly sectionStart?: 'continuous' | 'nextPage';
1579
+ readonly sectionStart?: 'continuous' | 'nextPage' | 'oddPage' | 'evenPage';
1578
1580
  /**
1579
1581
  * §17.6.5 `w:docGrid` — the line grid a `lines`/`linesAndChars` section rules
1580
1582
  * its text onto, as the pitch in points. Every line of the section's text is
@@ -6,5 +6,6 @@ export type { FontVariant, FontBytesByVariant } from './font-registry.js';
6
6
  export { pickVariant } from './font-registry.js';
7
7
  export { parseGposKerning, parseGsubLigatures, shapeText } from './opentype-layout.js';
8
8
  export type { KerningMap, LigatureMap, ShapedRun } from './opentype-layout.js';
9
+ export { hasSubstitutable, lettersForLigature, substituteLetters } from './ligatures.js';
9
10
  export { createFontMeasure } from './measure.js';
10
11
  export type { FontMeasure } from './measure.js';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The letters a character stands for, for a character nothing can draw.
3
+ *
4
+ * @param cp The code point.
5
+ * @returns The letters, or `undefined` when the character is a letter itself.
6
+ */
7
+ export declare function substituteLetters(cp: number): string | undefined;
8
+ /** Whether any code point of `text` is one {@link substituteLetters} answers for. */
9
+ export declare function hasSubstitutable(text: string): boolean;
10
+ /**
11
+ * The letters a LIGATURE stands for, for the one place that asks only about
12
+ * those: the `/ToUnicode` a drawn ligature glyph is stated by.
13
+ *
14
+ * @param cp The code point.
15
+ * @returns The letters, or `undefined` when it is not one of these ligatures.
16
+ */
17
+ export declare function lettersForLigature(cp: number): string | undefined;
@@ -0,0 +1,49 @@
1
+ //#region src/core/font/ligatures.ts
2
+ /**
3
+ * The letters a character stands for, for a character nothing can draw.
4
+ *
5
+ * @param cp The code point.
6
+ * @returns The letters, or `undefined` when the character is a letter itself.
7
+ */
8
+ function substituteLetters(cp) {
9
+ const ligature = LIGATURE_LETTERS.get(cp);
10
+ if (ligature !== void 0) return ligature;
11
+ if (cp >= 119808 && cp <= 120831) return decomposed(cp);
12
+ if (cp >= 8448 && cp <= 8527) {
13
+ const letters = decomposed(cp);
14
+ return letters?.length === 1 ? letters : void 0;
15
+ }
16
+ }
17
+ /** Whether any code point of `text` is one {@link substituteLetters} answers for. */
18
+ function hasSubstitutable(text) {
19
+ for (const ch of text) if (substituteLetters(ch.codePointAt(0)) !== void 0) return true;
20
+ return false;
21
+ }
22
+ /**
23
+ * The letters a LIGATURE stands for, for the one place that asks only about
24
+ * those: the `/ToUnicode` a drawn ligature glyph is stated by.
25
+ *
26
+ * @param cp The code point.
27
+ * @returns The letters, or `undefined` when it is not one of these ligatures.
28
+ */
29
+ function lettersForLigature(cp) {
30
+ return LIGATURE_LETTERS.get(cp);
31
+ }
32
+ /** U+FB00..FB06 — `ſt` opens with a long s, which is an s. */
33
+ var LIGATURE_LETTERS = new Map([
34
+ [64256, "ff"],
35
+ [64257, "fi"],
36
+ [64258, "fl"],
37
+ [64259, "ffi"],
38
+ [64260, "ffl"],
39
+ [64261, "st"],
40
+ [64262, "st"]
41
+ ]);
42
+ /** What Unicode says the character is a form of, or `undefined` if nothing. */
43
+ function decomposed(cp) {
44
+ const ch = String.fromCodePoint(cp);
45
+ const base = ch.normalize("NFKD");
46
+ return base === ch ? void 0 : base;
47
+ }
48
+ //#endregion
49
+ export { hasSubstitutable, lettersForLigature, substituteLetters };
@@ -63,7 +63,8 @@ export interface ParsedTtf {
63
63
  /**
64
64
  * Parse TrueType / OpenType font bytes into a {@link ParsedTtf}.
65
65
  *
66
- * @param raw The font bytes (sfnt: TrueType `0x00010000` or OpenType-CFF `OTTO`).
66
+ * @param raw The font bytes (sfnt: TrueType `0x00010000` or `'true'`, or
67
+ * OpenType-CFF `OTTO`).
67
68
  * @returns The parsed metrics, mappings and layout tables.
68
69
  * @throws Error when the bytes are not a TrueType or OpenType font.
69
70
  */
@@ -4,16 +4,25 @@ import { parseGposKerning, parseGsubArabicForms, parseGsubLigatures } from "./op
4
4
  var SFNT_TRUETYPE = 65536;
5
5
  var SFNT_OPENTYPE_CFF = 1330926671;
6
6
  /**
7
+ * `'true'` — Apple's own sfnt tag, and as much a TrueType font as `0x00010000`.
8
+ * Rejected outright, the reader could not read the program a file embedded and
9
+ * so could not say what its glyphs were: complex_ttf_font.pdf,
10
+ * PDFJS-7562-reduced.pdf and arial_unicode_en_cidfont.pdf each carry one and
11
+ * each reconstructed to a blank page.
12
+ */
13
+ var SFNT_APPLE_TRUE = 1953658213;
14
+ /**
7
15
  * Parse TrueType / OpenType font bytes into a {@link ParsedTtf}.
8
16
  *
9
- * @param raw The font bytes (sfnt: TrueType `0x00010000` or OpenType-CFF `OTTO`).
17
+ * @param raw The font bytes (sfnt: TrueType `0x00010000` or `'true'`, or
18
+ * OpenType-CFF `OTTO`).
10
19
  * @returns The parsed metrics, mappings and layout tables.
11
20
  * @throws Error when the bytes are not a TrueType or OpenType font.
12
21
  */
13
22
  function parseTtf(raw) {
14
23
  const r = new BigEndianReader(raw);
15
24
  const sfntVersion = r.u32();
16
- if (sfntVersion !== SFNT_TRUETYPE && sfntVersion !== SFNT_OPENTYPE_CFF) throw new Error(`Not a TrueType or OpenType font (sfntVersion=0x${sfntVersion.toString(16).padStart(8, "0")})`);
25
+ if (sfntVersion !== SFNT_TRUETYPE && sfntVersion !== SFNT_OPENTYPE_CFF && sfntVersion !== SFNT_APPLE_TRUE) throw new Error(`Not a TrueType or OpenType font (sfntVersion=0x${sfntVersion.toString(16).padStart(8, "0")})`);
17
26
  const numTables = r.u16();
18
27
  r.skip(6);
19
28
  const tables = /* @__PURE__ */ new Map();
@@ -11,7 +11,7 @@ export type FamilyKey = 'arimo' | 'tinos' | 'cousine' | 'carlito' | 'caladea';
11
11
  * only in the regular weight: Noto Sans SC is ten megabytes, and a bold run in
12
12
  * it is better stroked (see `SyntheticFace`) than downloaded four times over.
13
13
  */
14
- export type ScriptKey = 'jp' | 'kr' | 'sc' | 'arabic' | 'hebrew' | 'thai' | 'symbols';
14
+ export type ScriptKey = 'jp' | 'kr' | 'sc' | 'arabic' | 'hebrew' | 'thai' | 'symbols1' | 'symbols2';
15
15
  /** Either kind of substitute — a Latin family or a per-script face. */
16
16
  export type SubstituteKey = FamilyKey | ScriptKey;
17
17
  /** Whether a substitute key names a writing system rather than a Latin family. */
@@ -31,7 +31,11 @@ var SCRIPTS = {
31
31
  pkg: "noto-sans-thai",
32
32
  file: "NotoSansThai"
33
33
  },
34
- symbols: {
34
+ symbols1: {
35
+ pkg: "noto-sans-symbols",
36
+ file: "NotoSansSymbols"
37
+ },
38
+ symbols2: {
35
39
  pkg: "noto-sans-symbols-2",
36
40
  file: "NotoSansSymbols2"
37
41
  }
@@ -102,6 +106,47 @@ var MONO = new Set([
102
106
  "dejavu sans mono",
103
107
  "monospace"
104
108
  ]);
109
+ var STEMS = [
110
+ ["latinmodernroman", "tinos"],
111
+ ["latinmodernmath", "tinos"],
112
+ ["latinmodernmono", "cousine"],
113
+ ["latinmodernsans", "arimo"],
114
+ ["lmroman", "tinos"],
115
+ ["lmmath", "tinos"],
116
+ ["lmmono", "cousine"],
117
+ ["lmsans", "arimo"],
118
+ ["cmbx", "tinos"],
119
+ ["cmcsc", "tinos"],
120
+ ["cmti", "tinos"],
121
+ ["cmsl", "tinos"],
122
+ ["cmmi", "tinos"],
123
+ ["cmsy", "tinos"],
124
+ ["cmss", "arimo"],
125
+ ["cmtt", "cousine"],
126
+ ["cmr", "tinos"],
127
+ ["nimbusrom", "tinos"],
128
+ ["nimbussan", "arimo"],
129
+ ["nimbusmon", "cousine"],
130
+ ["urwpalladio", "tinos"],
131
+ ["urwbookman", "tinos"],
132
+ ["centuryschlbk", "tinos"],
133
+ ["centuryschoolbook", "tinos"],
134
+ ["newcenturyschlbk", "tinos"],
135
+ ["bookman", "tinos"],
136
+ ["palatino", "tinos"],
137
+ ["utopia", "tinos"],
138
+ ["charter", "tinos"],
139
+ ["baskerville", "tinos"],
140
+ ["caslon", "tinos"],
141
+ ["minionpro", "tinos"],
142
+ ["stoneserif", "tinos"],
143
+ ["stonesans", "arimo"],
144
+ ["myriadpro", "arimo"]
145
+ ];
146
+ /** The family a name STARTS with, for the families named by stem and size. */
147
+ function familyFromStem(name) {
148
+ for (const [stem, key] of STEMS) if (name.startsWith(stem)) return key;
149
+ }
105
150
  var WEIGHT_WORDS = new Map([
106
151
  ["black", "bold"],
107
152
  ["heavy", "bold"],
@@ -159,7 +204,7 @@ function resolveFamilyStyle(name) {
159
204
  ];
160
205
  let key = "arimo";
161
206
  for (const n of tries) {
162
- const found = EXACT[n] ?? (MONO.has(n) ? "cousine" : SERIF.has(n) ? "tinos" : void 0);
207
+ const found = EXACT[n] ?? (MONO.has(n) ? "cousine" : SERIF.has(n) ? "tinos" : void 0) ?? familyFromStem(n.replace(/[^a-z]/gu, ""));
163
208
  if (found) {
164
209
  key = found;
165
210
  break;
@@ -33,27 +33,27 @@ var RANGES = [
33
33
  [
34
34
  8592,
35
35
  8703,
36
- "symbols"
36
+ "symbols1"
37
37
  ],
38
38
  [
39
39
  8960,
40
40
  9215,
41
- "symbols"
41
+ "symbols1"
42
42
  ],
43
43
  [
44
44
  9312,
45
45
  9471,
46
- "symbols"
46
+ "symbols1"
47
47
  ],
48
48
  [
49
49
  9472,
50
50
  10175,
51
- "symbols"
51
+ "symbols2"
52
52
  ],
53
53
  [
54
54
  11008,
55
55
  11263,
56
- "symbols"
56
+ "symbols2"
57
57
  ],
58
58
  [
59
59
  12288,
@@ -149,6 +149,11 @@ function scriptsInFlow(flow) {
149
149
  marks.add("han");
150
150
  continue;
151
151
  }
152
+ if (key === "symbols1" || key === "symbols2") {
153
+ scripts.add("symbols1");
154
+ scripts.add("symbols2");
155
+ continue;
156
+ }
152
157
  scripts.add(key);
153
158
  }
154
159
  };
@@ -53,20 +53,65 @@ function applyHeaderTint(hex, amount, darken) {
53
53
  function buildHeaderFooterContent(formatString, sheetName, scale = 1, basePt = DEFAULT_HEADER_PT, fileName, themePalette, now) {
54
54
  const regions = parseHeaderFooterString(formatString, sheetName, scale, basePt, fileName, themePalette, now);
55
55
  const out = [];
56
- const para = (runs, alignment) => {
57
- for (const line of splitLines(runs)) if (line.length > 0) out.push({
56
+ const lines = {
57
+ left: splitLines(regions.left),
58
+ center: splitLines(regions.center),
59
+ right: splitLines(regions.right)
60
+ };
61
+ const filled = (region) => lines[region].filter((line) => line.length > 0).length;
62
+ if ((filled("left") > 0 ? 1 : 0) + (filled("center") > 0 ? 1 : 0) + (filled("right") > 0 ? 1 : 0) <= 1) {
63
+ const para = (runs, alignment) => {
64
+ for (const line of splitLines(runs)) if (line.length > 0) out.push({
65
+ kind: "paragraph",
66
+ paragraph: {
67
+ properties: { alignment },
68
+ runs: line
69
+ }
70
+ });
71
+ };
72
+ para(regions.left, "left");
73
+ para(regions.center, "center");
74
+ para(regions.right, "right");
75
+ return out;
76
+ }
77
+ const deep = Math.max(lines.left.length, lines.center.length, lines.right.length);
78
+ for (let i = 0; i < deep; i++) {
79
+ const left = lines.left[i] ?? [];
80
+ const center = lines.center[i] ?? [];
81
+ const right = lines.right[i] ?? [];
82
+ if (left.length + center.length + right.length === 0) continue;
83
+ const runs = [...left];
84
+ if (center.length > 0) runs.push(TAB_RUN, ...center);
85
+ if (right.length > 0) runs.push(TAB_RUN, ...right);
86
+ out.push({
58
87
  kind: "paragraph",
59
88
  paragraph: {
60
- properties: { alignment },
61
- runs: line
89
+ properties: { tabs: HEADER_STOPS },
90
+ runs
62
91
  }
63
92
  });
64
- };
65
- para(regions.left, "left");
66
- para(regions.center, "center");
67
- para(regions.right, "right");
93
+ }
68
94
  return out;
69
95
  }
96
+ /** The tab a header's regions are separated by. */
97
+ var TAB_RUN = {
98
+ text: " ",
99
+ properties: {}
100
+ };
101
+ /**
102
+ * §17.3.1.38 — the two stops a header's regions stand on: the middle of the
103
+ * band and its far edge, both stated against the band itself rather than at a
104
+ * distance, since only the layout knows how wide the band is.
105
+ */
106
+ var HEADER_STOPS = [{
107
+ positionPt: pt(0),
108
+ relativeTo: "center",
109
+ alignment: "center"
110
+ }, {
111
+ positionPt: pt(0),
112
+ relativeTo: "right",
113
+ alignment: "right"
114
+ }];
70
115
  /**
71
116
  * Break a region's runs at the line breaks Excel allows inside one — a header
72
117
  * region is not necessarily one line.
@@ -1,6 +1,7 @@
1
1
  import { resolveFamilyStyle } from "../core/fonts/remote-fonts.js";
2
2
  import { scriptForCodepoint } from "../core/fonts/scripts.js";
3
3
  import { shapeText } from "../core/font/opentype-layout.js";
4
+ import { hasSubstitutable, substituteLetters } from "../core/font/ligatures.js";
4
5
  import { createFontMeasure } from "../core/font/measure.js";
5
6
  import { halfPtToPt, pt } from "../core/ir/units.js";
6
7
  import { ResourceStore } from "../core/ir/resources.js";
@@ -2434,6 +2435,40 @@ function fallbackFaceKey(options, cp, bold, italic) {
2434
2435
  }
2435
2436
  }
2436
2437
  /**
2438
+ * A character no face on hand can draw, set as the letters it stands for.
2439
+ *
2440
+ * `ff` (U+FB00) is one glyph for two f's — a shape of the FACE, not a letter of
2441
+ * the alphabet, and most faces carry no such code point at all: they form the
2442
+ * ligature from `ff` through their own `liga` table. `𝑝` (U+1D45D) is a lower
2443
+ * case p set in italic, given a code point of its own so that mathematics can
2444
+ * name a variable in plain text; no ordinary face carries that either. So the
2445
+ * run's own face drew nothing, no other loaded face answered, and the character
2446
+ * went out as a box or as nothing at all: bug1873345.pdf reads "different" and
2447
+ * we set "di erent", bug1529502.pdf reads "p ← trim(p)" and we set "□ ← □".
2448
+ * The letters are the same text — the ligature the face is free to form again,
2449
+ * the slant is lost, and both beat a box.
2450
+ *
2451
+ * Only where nobody can draw the character itself: one a face DOES carry is
2452
+ * drawn as written.
2453
+ *
2454
+ * @param options The render options (the loaded registries).
2455
+ * @param primary The run's own face.
2456
+ * @param resolved Its resolved properties (weight and slant).
2457
+ * @param text The run's text.
2458
+ * @returns The text to set, which is `text` itself unless something was undrawable.
2459
+ */
2460
+ function drawableText(options, primary, resolved, text) {
2461
+ if (!hasSubstitutable(text)) return text;
2462
+ let out = "";
2463
+ for (const ch of text) {
2464
+ const cp = ch.codePointAt(0);
2465
+ const letters = substituteLetters(cp);
2466
+ const drawable = letters === void 0 || primary.glyphForCodepoint(cp) !== 0 || fallbackFaceKey(options, cp, resolved.bold, resolved.italic) !== void 0;
2467
+ out += drawable ? ch : letters;
2468
+ }
2469
+ return out;
2470
+ }
2471
+ /**
2437
2472
  * §9.2.2 — the face a request had to settle for, and what is left to draw.
2438
2473
  *
2439
2474
  * A registry may hold one face and be asked for four: `FontBytesByVariant`
@@ -2580,7 +2615,7 @@ function collectFontResources(body, headersFooters, options, imageResources) {
2580
2615
  };
2581
2616
  used.set(fontKey, bucket);
2582
2617
  }
2583
- const text = run.listMarker === true ? markerText(run.text, parsed) : run.text;
2618
+ const text = drawableText(options, parsed, resolved, run.listMarker === true ? markerText(run.text, parsed) : run.text);
2584
2619
  for (const span of faceSpans(options, {
2585
2620
  fontKey,
2586
2621
  parsed
@@ -3008,9 +3043,16 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
3008
3043
  const { fontKey, synthetic } = runFontKeyAndParsed(options, resolvedRun.fontFamily.ascii, resolvedRun.bold, resolvedRun.italic);
3009
3044
  const script = SCRIPT_OFFSET[resolvedRun.verticalAlign] ?? 0;
3010
3045
  const own = lookupFont(fontResources, fontKey);
3011
- const faceFor = facesForRun(options, fontResources, own.parsed, resolvedRun, run.text);
3046
+ const drawn = (() => {
3047
+ const text = drawableText(options, own.parsed, resolvedRun, run.text);
3048
+ return text === run.text ? run : {
3049
+ ...run,
3050
+ text
3051
+ };
3052
+ })();
3053
+ const faceFor = facesForRun(options, fontResources, own.parsed, resolvedRun, drawn.text);
3012
3054
  return {
3013
- run,
3055
+ run: drawn,
3014
3056
  resolvedRun,
3015
3057
  font: own,
3016
3058
  ...faceFor ? { faceFor } : {},
@@ -5114,6 +5156,14 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
5114
5156
  }
5115
5157
  if (asm.pageHasContent() || asm.pageInSection === 0) asm.flushPage(true);
5116
5158
  else asm.dropPage();
5159
+ const opening = sectionCtxs[asm.secIdx + 1]?.properties;
5160
+ const wants = opening?.sectionStart;
5161
+ if (wants === "oddPage" || wants === "evenPage") {
5162
+ if ((opening?.pageNumberStart ?? asm.pageNumber) % 2 === 1 !== (wants === "oddPage")) {
5163
+ asm.cursorY = asm.ctx.pageHeight - asm.ctx.marginTop;
5164
+ asm.flushPage(true);
5165
+ }
5166
+ }
5117
5167
  asm.secIdx++;
5118
5168
  asm.ctx = sectionCtxs[asm.secIdx];
5119
5169
  asm.restartPageNumbers(asm.ctx);
@@ -1,4 +1,5 @@
1
1
  import { glyphClosure, subsetTtf } from "../core/font/ttf-subset.js";
2
+ import { lettersForLigature } from "../core/font/ligatures.js";
2
3
  import { createFontMeasure } from "../core/font/measure.js";
3
4
  import { deflatedStream, dict, name, ref } from "./objects.js";
4
5
  //#region src/pdf/cid-font.ts
@@ -109,6 +110,22 @@ function consecutiveRuns(ids) {
109
110
  }
110
111
  return runs;
111
112
  }
113
+ /**
114
+ * How much a code point says about the character a glyph stands for.
115
+ *
116
+ * A presentation form is a shape — the ligature two letters make, the form a
117
+ * letter takes where it joins — but a shape that names its letters. A
118
+ * private-use code is a font's own numbering and names nothing at all. A glyph
119
+ * several codes reach keeps the one that says the most.
120
+ *
121
+ * @param cp The code point.
122
+ * @returns 0 for a character, 1 for a presentation form, 2 for private use.
123
+ */
124
+ function codeRank(cp) {
125
+ if (cp >= 57344 && cp <= 63743) return 2;
126
+ if (cp >= 64256 && cp <= 65023 || cp >= 65136 && cp <= 65279) return 1;
127
+ return 0;
128
+ }
112
129
  function buildToUnicodeCMap(parsed, subsetGids) {
113
130
  const inSubset = subsetGids ? new Set(subsetGids) : void 0;
114
131
  const gidToCps = /* @__PURE__ */ new Map();
@@ -116,30 +133,40 @@ function buildToUnicodeCMap(parsed, subsetGids) {
116
133
  for (let cp = 9; cp <= 65535; cp++) {
117
134
  if (cp >= 55296 && cp <= 57343) continue;
118
135
  const gid = parsed.glyphForCodepoint(cp);
119
- if (gid === 0 || gidToCps.has(gid)) continue;
136
+ if (gid === 0) continue;
137
+ const had = gidToCps.get(gid);
138
+ if (had) {
139
+ if (had.length === 1 && codeRank(cp) < codeRank(had[0])) gidToCps.set(gid, [cp]);
140
+ continue;
141
+ }
120
142
  gidToCps.set(gid, [cp]);
121
143
  order.push(gid);
122
144
  }
145
+ for (const [gid, cps] of gidToCps) {
146
+ const letters = cps.length === 1 ? lettersForLigature(cps[0]) : void 0;
147
+ if (letters !== void 0) gidToCps.set(gid, [...letters].map((c) => c.codePointAt(0)));
148
+ }
123
149
  let changed = true;
124
150
  while (changed) {
125
151
  changed = false;
126
152
  for (const [key, ligGid] of parsed.ligatures) {
127
- if (gidToCps.has(ligGid)) continue;
153
+ const have = gidToCps.get(ligGid);
154
+ if (have && !(have.length === 1 && codeRank(have[0]) > 0)) continue;
128
155
  const cps = [];
129
156
  let ok = true;
130
157
  for (const comp of key.split(",")) {
131
158
  const c = gidToCps.get(Number(comp));
132
- if (!c) {
159
+ if (!c || c === have) {
133
160
  ok = false;
134
161
  break;
135
162
  }
136
163
  cps.push(...c);
137
164
  }
138
- if (ok && cps.length > 0) {
139
- gidToCps.set(ligGid, cps);
140
- order.push(ligGid);
141
- changed = true;
142
- }
165
+ if (!ok || cps.length === 0) continue;
166
+ if (have?.join(",") === cps.join(",")) continue;
167
+ gidToCps.set(ligGid, cps);
168
+ if (!have) order.push(ligGid);
169
+ changed = true;
143
170
  }
144
171
  }
145
172
  const pairs = order.filter((g) => inSubset === void 0 || inSubset.has(g)).map((g) => [g, gidToCps.get(g)]).sort((a, b) => a[0] - b[0]);