@jasy/pdf 1.0.0-alpha.8 → 1.0.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.
Files changed (167) hide show
  1. package/README.md +29 -10
  2. package/dist/api/color.js +69 -0
  3. package/dist/api/content.d.ts +45 -5
  4. package/dist/api/content.js +96 -7
  5. package/dist/api/descriptor.js +6 -1
  6. package/dist/api/dimension.d.ts +57 -0
  7. package/dist/api/dimension.js +27 -0
  8. package/dist/api/font-url.d.ts +28 -0
  9. package/dist/api/font-url.js +135 -0
  10. package/dist/api/gradient.d.ts +53 -0
  11. package/dist/api/gradient.js +97 -0
  12. package/dist/api/index.d.ts +2 -0
  13. package/dist/api/index.js +2 -0
  14. package/dist/api/insets.d.ts +19 -12
  15. package/dist/api/insets.js +18 -7
  16. package/dist/api/layout.d.ts +20 -6
  17. package/dist/api/layout.js +31 -6
  18. package/dist/api/page-builder.d.ts +8 -0
  19. package/dist/api/page-builder.js +12 -0
  20. package/dist/api/structure.d.ts +12 -0
  21. package/dist/api/structure.js +54 -7
  22. package/dist/api/text.d.ts +65 -8
  23. package/dist/api/text.js +38 -5
  24. package/dist/canvas/painter.d.ts +102 -0
  25. package/dist/canvas/painter.js +183 -0
  26. package/dist/edit/document.d.ts +14 -4
  27. package/dist/edit/document.js +15 -22
  28. package/dist/edit/fill.d.ts +19 -2
  29. package/dist/edit/fill.js +63 -34
  30. package/dist/edit/flatten.d.ts +13 -1
  31. package/dist/edit/flatten.js +38 -13
  32. package/dist/edit/index.d.ts +2 -0
  33. package/dist/edit/index.js +3 -1
  34. package/dist/edit/objects.d.ts +5 -0
  35. package/dist/edit/objects.js +27 -0
  36. package/dist/edit/writer.d.ts +7 -0
  37. package/dist/edit/writer.js +23 -1
  38. package/dist/elements/canvas-element.d.ts +38 -0
  39. package/dist/elements/canvas-element.js +47 -0
  40. package/dist/elements/container-element.d.ts +23 -1
  41. package/dist/elements/container-element.js +31 -10
  42. package/dist/elements/image-element.d.ts +18 -4
  43. package/dist/elements/image-element.js +27 -12
  44. package/dist/elements/index.d.ts +2 -0
  45. package/dist/elements/index.js +2 -0
  46. package/dist/elements/layout/expanded-element.d.ts +5 -1
  47. package/dist/elements/layout/expanded-element.js +16 -3
  48. package/dist/elements/layout/keep-together-element.d.ts +2 -0
  49. package/dist/elements/layout/keep-together-element.js +4 -0
  50. package/dist/elements/layout/padding-element.d.ts +9 -2
  51. package/dist/elements/layout/padding-element.js +12 -2
  52. package/dist/elements/layout/page-builder-element.js +2 -0
  53. package/dist/elements/layout/struct-group.d.ts +2 -0
  54. package/dist/elements/layout/struct-group.js +4 -0
  55. package/dist/elements/pdf-element.d.ts +75 -1
  56. package/dist/elements/pdf-element.js +80 -1
  57. package/dist/elements/rectangle-element.d.ts +22 -6
  58. package/dist/elements/rectangle-element.js +31 -12
  59. package/dist/elements/row-element.d.ts +26 -1
  60. package/dist/elements/row-element.js +31 -10
  61. package/dist/elements/svg-element.d.ts +41 -0
  62. package/dist/elements/svg-element.js +93 -0
  63. package/dist/elements/text-element.d.ts +92 -1
  64. package/dist/elements/text-element.js +208 -33
  65. package/dist/ir/display-list.d.ts +67 -11
  66. package/dist/ir/display-list.js +8 -1
  67. package/dist/layout/alignment.d.ts +10 -0
  68. package/dist/layout/alignment.js +6 -0
  69. package/dist/layout/box-constraints.d.ts +53 -0
  70. package/dist/layout/box-constraints.js +72 -0
  71. package/dist/layout/insets.d.ts +28 -0
  72. package/dist/layout/insets.js +20 -0
  73. package/dist/layout/min-intrinsic.d.ts +29 -0
  74. package/dist/layout/min-intrinsic.js +37 -0
  75. package/dist/renderer/anchor-renderer.js +1 -1
  76. package/dist/renderer/bookmark-renderer.js +1 -1
  77. package/dist/renderer/canvas-renderer.d.ts +14 -0
  78. package/dist/renderer/canvas-renderer.js +36 -0
  79. package/dist/renderer/container-renderer.js +1 -3
  80. package/dist/renderer/default-text-style-renderer.js +1 -1
  81. package/dist/renderer/deferred-renderer.js +1 -1
  82. package/dist/renderer/expanded-renderer.js +1 -1
  83. package/dist/renderer/image-renderer.js +2 -1
  84. package/dist/renderer/index.d.ts +2 -0
  85. package/dist/renderer/index.js +2 -0
  86. package/dist/renderer/keep-together-renderer.js +1 -1
  87. package/dist/renderer/link-renderer.js +1 -1
  88. package/dist/renderer/padding-renderer.js +1 -1
  89. package/dist/renderer/page-builder-renderer.js +1 -1
  90. package/dist/renderer/page-renderer.js +1 -3
  91. package/dist/renderer/pdf-backend.d.ts +21 -4
  92. package/dist/renderer/pdf-backend.js +198 -61
  93. package/dist/renderer/pdf-document-renderer.js +8 -1
  94. package/dist/renderer/pdf-renderer.js +5 -1
  95. package/dist/renderer/positioned-renderer.js +1 -1
  96. package/dist/renderer/rectangle-renderer.js +9 -5
  97. package/dist/renderer/repeating-header-renderer.js +1 -2
  98. package/dist/renderer/rotated-renderer.js +1 -1
  99. package/dist/renderer/row-renderer.js +1 -3
  100. package/dist/renderer/struct-group-renderer.js +1 -1
  101. package/dist/renderer/svg-renderer.d.ts +11 -0
  102. package/dist/renderer/svg-renderer.js +27 -0
  103. package/dist/renderer/text-renderer.d.ts +2 -2
  104. package/dist/renderer/text-renderer.js +176 -57
  105. package/dist/svg/css.d.ts +38 -0
  106. package/dist/svg/css.js +115 -0
  107. package/dist/svg/errors.d.ts +24 -0
  108. package/dist/svg/errors.js +30 -0
  109. package/dist/svg/gradients.d.ts +34 -0
  110. package/dist/svg/gradients.js +156 -0
  111. package/dist/svg/index.d.ts +22 -0
  112. package/dist/svg/index.js +331 -0
  113. package/dist/svg/shapes.d.ts +6 -0
  114. package/dist/svg/shapes.js +177 -0
  115. package/dist/svg/style.d.ts +61 -0
  116. package/dist/svg/style.js +144 -0
  117. package/dist/svg/transform.d.ts +13 -0
  118. package/dist/svg/transform.js +68 -0
  119. package/dist/svg/units.d.ts +11 -0
  120. package/dist/svg/units.js +73 -0
  121. package/dist/svg/xml.d.ts +24 -0
  122. package/dist/svg/xml.js +158 -0
  123. package/dist/text/advance.d.ts +3 -0
  124. package/dist/text/advance.js +8 -4
  125. package/dist/text/arabic.d.ts +20 -0
  126. package/dist/text/arabic.js +101 -0
  127. package/dist/text/bidi.d.ts +33 -0
  128. package/dist/text/bidi.js +94 -0
  129. package/dist/text/font-fallback.d.ts +25 -0
  130. package/dist/text/font-fallback.js +38 -0
  131. package/dist/text/glyph-coverage.d.ts +15 -0
  132. package/dist/text/glyph-coverage.js +77 -0
  133. package/dist/text/line-breaker.d.ts +43 -1
  134. package/dist/text/line-breaker.js +181 -45
  135. package/dist/text/orphans-widows.d.ts +27 -0
  136. package/dist/text/orphans-widows.js +53 -0
  137. package/dist/text/shape.d.ts +29 -0
  138. package/dist/text/shape.js +91 -0
  139. package/dist/text/text-style.d.ts +38 -0
  140. package/dist/text/text-style.js +50 -1
  141. package/dist/text/whitespace.d.ts +4 -0
  142. package/dist/text/whitespace.js +43 -0
  143. package/dist/text/word-splitting.d.ts +42 -0
  144. package/dist/text/word-splitting.js +72 -0
  145. package/dist/utils/flex-layout.d.ts +24 -4
  146. package/dist/utils/flex-layout.js +218 -7
  147. package/dist/utils/font-metrics.d.ts +13 -2
  148. package/dist/utils/gsub.d.ts +36 -0
  149. package/dist/utils/gsub.js +178 -0
  150. package/dist/utils/inflate.d.ts +16 -0
  151. package/dist/utils/inflate.js +52 -0
  152. package/dist/utils/pdf-object-manager.d.ts +44 -3
  153. package/dist/utils/pdf-object-manager.js +176 -14
  154. package/dist/utils/renderer-registry.d.ts +13 -1
  155. package/dist/utils/renderer-registry.js +22 -1
  156. package/dist/utils/ttf-parser.d.ts +6 -0
  157. package/dist/utils/ttf-parser.js +19 -0
  158. package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
  159. package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
  160. package/dist/utils/woff.d.ts +10 -0
  161. package/dist/utils/woff.js +116 -0
  162. package/dist/utils/woff2.d.ts +6 -0
  163. package/dist/utils/woff2.js +651 -0
  164. package/dist/validators/element-validator.js +9 -2
  165. package/dist/vector/path.d.ts +33 -0
  166. package/dist/vector/path.js +81 -0
  167. package/package.json +6 -2
@@ -0,0 +1,178 @@
1
+ import { u16, u32, i16, latin1FromBytes } from "./bytes.js";
2
+ const EXTENSION = 7;
3
+ /** A coverage table may not expand past this: a glyph id is a uint16, so no honest one can. */
4
+ const MAX_COVERAGE_ENTRIES = 65536;
5
+ export class GsubTable {
6
+ constructor(data, base) {
7
+ this.data = data;
8
+ this.base = base;
9
+ this.lookupCache = new Map();
10
+ this.coverageCache = new Map();
11
+ this.scriptList = base + u16(data, base + 4);
12
+ this.featureList = base + u16(data, base + 6);
13
+ this.lookupList = base + u16(data, base + 8);
14
+ }
15
+ /** Whether the font carries this script at all - `arab` is the question the shaper asks. */
16
+ hasScript(script) {
17
+ return this.scriptOffset(script) !== undefined;
18
+ }
19
+ /** A feature's lookup indices, under the script's DEFAULT language system - Arabic shaping features
20
+ * are script-wide, so a language-specific override would only add typographic niceties. */
21
+ lookups(script, feature) {
22
+ const scriptOff = this.scriptOffset(script);
23
+ if (scriptOff === undefined)
24
+ return [];
25
+ // The default LangSys is optional; fall back to the first named one - a font declaring only
26
+ // `ARA ` still shapes Arabic.
27
+ const defaultOff = u16(this.data, scriptOff);
28
+ const langSys = defaultOff !== 0
29
+ ? scriptOff + defaultOff
30
+ : u16(this.data, scriptOff + 2) > 0
31
+ ? // The first LangSysRecord is at +4 and is tag(4) + offset(2), so its offset is at +8.
32
+ scriptOff + u16(this.data, scriptOff + 8)
33
+ : undefined;
34
+ if (langSys === undefined)
35
+ return [];
36
+ const out = [];
37
+ const count = u16(this.data, langSys + 4);
38
+ for (let i = 0; i < count; i++) {
39
+ const featureIndex = u16(this.data, langSys + 6 + i * 2);
40
+ const record = this.featureList + 2 + featureIndex * 6;
41
+ if (latin1FromBytes(this.data.subarray(record, record + 4)) !== feature)
42
+ continue;
43
+ const featureOff = this.featureList + u16(this.data, record + 4);
44
+ const lookupCount = u16(this.data, featureOff + 2);
45
+ for (let j = 0; j < lookupCount; j++)
46
+ out.push(u16(this.data, featureOff + 4 + j * 2));
47
+ }
48
+ return out;
49
+ }
50
+ /** Type 1: substitute one glyph. `null` when no subtable covers it - the feature does not apply. */
51
+ substituteSingle(lookupIndex, glyph) {
52
+ const lookup = this.lookup(lookupIndex);
53
+ if (!lookup || lookup.type !== 1)
54
+ return null;
55
+ for (const sub of lookup.subtables) {
56
+ const coverage = this.coverage(sub + u16(this.data, sub + 2));
57
+ const index = coverage.get(glyph);
58
+ if (index === undefined)
59
+ continue;
60
+ // Format 1 shifts every covered glyph by one delta; format 2 lists a replacement per glyph.
61
+ return u16(this.data, sub) === 1
62
+ ? (glyph + i16(this.data, sub + 4)) & 0xffff
63
+ : u16(this.data, sub + 6 + index * 2);
64
+ }
65
+ return null;
66
+ }
67
+ /**
68
+ * Type 4: collapse a run of glyphs into one, starting at `at`. Longest match wins, as the spec
69
+ * requires - a three-glyph ligature beats the two-glyph one that starts the same way.
70
+ */
71
+ substituteLigature(lookupIndex, glyphs, at) {
72
+ const lookup = this.lookup(lookupIndex);
73
+ if (!lookup || lookup.type !== 4)
74
+ return null;
75
+ let best = null;
76
+ for (const sub of lookup.subtables) {
77
+ const coverage = this.coverage(sub + u16(this.data, sub + 2));
78
+ const setIndex = coverage.get(glyphs[at]);
79
+ if (setIndex === undefined)
80
+ continue;
81
+ const set = sub + u16(this.data, sub + 6 + setIndex * 2);
82
+ const ligatureCount = u16(this.data, set);
83
+ for (let i = 0; i < ligatureCount; i++) {
84
+ const lig = set + u16(this.data, set + 2 + i * 2);
85
+ const components = u16(this.data, lig + 2); // includes the glyph coverage already matched
86
+ if (at + components > glyphs.length)
87
+ continue;
88
+ let matches = true;
89
+ for (let c = 1; c < components && matches; c++) {
90
+ matches = glyphs[at + c] === u16(this.data, lig + 2 + c * 2);
91
+ }
92
+ if (matches && (best === null || components > best.consumed)) {
93
+ best = { glyph: u16(this.data, lig), consumed: components };
94
+ }
95
+ }
96
+ }
97
+ return best;
98
+ }
99
+ /** The type of a lookup, for reporting what a font needs that we do not do. */
100
+ lookupType(lookupIndex) {
101
+ return this.lookup(lookupIndex)?.type;
102
+ }
103
+ scriptOffset(script) {
104
+ const count = u16(this.data, this.scriptList);
105
+ for (let i = 0; i < count; i++) {
106
+ const record = this.scriptList + 2 + i * 6;
107
+ if (latin1FromBytes(this.data.subarray(record, record + 4)) === script) {
108
+ return this.scriptList + u16(this.data, record + 4);
109
+ }
110
+ }
111
+ return undefined;
112
+ }
113
+ /**
114
+ * Resolve a lookup, unwrapping type 7 (extension) - which exists only because the 16-bit offsets
115
+ * elsewhere cannot reach far enough in a big font. Callers never learn it was wrapped.
116
+ */
117
+ lookup(index) {
118
+ const cached = this.lookupCache.get(index);
119
+ if (cached !== undefined)
120
+ return cached;
121
+ let result = null;
122
+ if (index < u16(this.data, this.lookupList)) {
123
+ const off = this.lookupList + u16(this.data, this.lookupList + 2 + index * 2);
124
+ const type = u16(this.data, off);
125
+ const count = u16(this.data, off + 4);
126
+ const subtables = [];
127
+ let realType = type;
128
+ for (let i = 0; i < count; i++) {
129
+ const sub = off + u16(this.data, off + 6 + i * 2);
130
+ if (type === EXTENSION) {
131
+ realType = u16(this.data, sub + 2);
132
+ subtables.push(sub + u32(this.data, sub + 4));
133
+ }
134
+ else {
135
+ subtables.push(sub);
136
+ }
137
+ }
138
+ result = { type: realType, subtables };
139
+ }
140
+ this.lookupCache.set(index, result);
141
+ return result;
142
+ }
143
+ /** Coverage table -> glyph id to its INDEX in the table, which is how subtables address glyphs. */
144
+ coverage(off) {
145
+ const cached = this.coverageCache.get(off);
146
+ if (cached)
147
+ return cached;
148
+ const map = new Map();
149
+ const count = u16(this.data, off + 2);
150
+ if (u16(this.data, off) === 1) {
151
+ for (let i = 0; i < count; i++)
152
+ map.set(u16(this.data, off + 4 + i * 2), i);
153
+ }
154
+ else {
155
+ // Format 2 is RANGES, and a malformed file can claim far more glyphs than any font holds. Such
156
+ // a table is refused whole rather than expanded - the feature then simply does not apply.
157
+ let total = 0;
158
+ for (let i = 0; i < count; i++) {
159
+ const record = off + 4 + i * 6;
160
+ const start = u16(this.data, record);
161
+ const end = u16(this.data, record + 2);
162
+ if (end < start)
163
+ continue; // an inverted range covers nothing
164
+ total += end - start + 1;
165
+ if (total > MAX_COVERAGE_ENTRIES)
166
+ return this.cacheCoverage(off, new Map());
167
+ const startIndex = u16(this.data, record + 4);
168
+ for (let g = start; g <= end; g++)
169
+ map.set(g, startIndex + (g - start));
170
+ }
171
+ }
172
+ return this.cacheCoverage(off, map);
173
+ }
174
+ cacheCoverage(off, map) {
175
+ this.coverageCache.set(off, map);
176
+ return map;
177
+ }
178
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Inflating with a ceiling, so a hostile or mistaken input cannot exhaust memory.
3
+ *
4
+ * Used by BOTH sides: the reader inflating a PDF stream, and the font path unpacking a WOFF table.
5
+ * It lives in `utils/` for exactly that reason - the generate path must not import from `edit/`.
6
+ *
7
+ * fflate's `unzlibSync(data, { out })` is NOT usable for this: it truncates silently instead of failing.
8
+ * Hence the streamed form - input in slices, so the total can be checked while it grows.
9
+ */
10
+ /** Thrown when a stream expands past the ceiling; distinct from a stream that simply will not decode. */
11
+ export declare class PdfStreamTooLargeError extends Error {
12
+ readonly limit: number;
13
+ constructor(limit: number, message: string);
14
+ }
15
+ export declare const DEFAULT_MAX_STREAM_SIZE: number;
16
+ export declare function inflateBounded(data: Uint8Array, limit: number): Uint8Array;
@@ -0,0 +1,52 @@
1
+ import { Unzlib, unzlibSync } from "fflate";
2
+ /**
3
+ * Inflating with a ceiling, so a hostile or mistaken input cannot exhaust memory.
4
+ *
5
+ * Used by BOTH sides: the reader inflating a PDF stream, and the font path unpacking a WOFF table.
6
+ * It lives in `utils/` for exactly that reason - the generate path must not import from `edit/`.
7
+ *
8
+ * fflate's `unzlibSync(data, { out })` is NOT usable for this: it truncates silently instead of failing.
9
+ * Hence the streamed form - input in slices, so the total can be checked while it grows.
10
+ */
11
+ /** Thrown when a stream expands past the ceiling; distinct from a stream that simply will not decode. */
12
+ export class PdfStreamTooLargeError extends Error {
13
+ constructor(limit, message) {
14
+ super(`@jasy/pdf: ${message}`);
15
+ this.limit = limit;
16
+ }
17
+ }
18
+ export const DEFAULT_MAX_STREAM_SIZE = 64 * 1024 * 1024;
19
+ /** Only the granularity of the check: fflate buffers internally, so the abort lands one block past. */
20
+ const INPUT_SLICE = 16 * 1024;
21
+ /** DEFLATE's maximum expansion (a 258-byte match in its shortest encoding). A format property. */
22
+ const MAX_DEFLATE_RATIO = 1032;
23
+ export function inflateBounded(data, limit) {
24
+ // Too small to reach the ceiling even at maximum compression, so checking as it grows is pointless.
25
+ // Nearly every stream in a real PDF lands here, which is why the guard costs nothing.
26
+ if (data.length <= limit / MAX_DEFLATE_RATIO)
27
+ return unzlibSync(data);
28
+ const parts = [];
29
+ let total = 0;
30
+ const inflater = new Unzlib();
31
+ inflater.ondata = (chunk) => {
32
+ total += chunk.length;
33
+ if (total > limit) {
34
+ throw new PdfStreamTooLargeError(limit, `a stream in this PDF expands past the ${limit} byte limit for a single stream; if the file is ` +
35
+ `genuinely that large, raise it with { maxStreamSize }`);
36
+ }
37
+ parts.push(chunk);
38
+ };
39
+ for (let at = 0; at < data.length; at += INPUT_SLICE) {
40
+ const end = Math.min(at + INPUT_SLICE, data.length);
41
+ inflater.push(data.subarray(at, end), end >= data.length);
42
+ }
43
+ if (parts.length === 1)
44
+ return parts[0];
45
+ const out = new Uint8Array(total);
46
+ let at = 0;
47
+ for (const part of parts) {
48
+ out.set(part, at);
49
+ at += part.length;
50
+ }
51
+ return out;
52
+ }
@@ -1,5 +1,6 @@
1
1
  import type { OverflowPolicy } from "../layout/fragmentation.js";
2
2
  import { TTFParser } from "./ttf-parser.js";
3
+ import { type ShapedGlyph } from "../text/shape.js";
3
4
  import type { SecurityHandler } from "../crypto/security-handler.js";
4
5
  import type { Gradient } from "../ir/display-list.js";
5
6
  import type { FontVerticals } from "../text/line-metrics.js";
@@ -39,6 +40,8 @@ export declare class PDFObjectManager implements FontMetrics {
39
40
  private verticalsCache;
40
41
  private decorationCache;
41
42
  private customFontEmit;
43
+ private shapeCache;
44
+ private shapedOrigins;
42
45
  private attachments;
43
46
  private xmpMetadata?;
44
47
  private outputIntent?;
@@ -62,6 +65,16 @@ export declare class PDFObjectManager implements FontMetrics {
62
65
  addObject(content: string): number;
63
66
  replaceObject(objectNumber: number, content: string): void;
64
67
  setCompress(on: boolean): void;
68
+ /**
69
+ * Code points no font in the document could draw, so they were removed rather than emitted as
70
+ * `.notdef` (which PDF/A forbids). Collected here because this object is the one thing threaded
71
+ * through BOTH passes; the caller gets them back with the bytes, so an application can say which
72
+ * character went missing instead of a server log nobody reads.
73
+ */
74
+ private readonly missingGlyphs;
75
+ reportMissingGlyph(codePoints: number[]): void;
76
+ /** The dropped code points, in the order first seen. Empty for a document that hit none. */
77
+ getMissingGlyphs(): number[];
65
78
  setOverflowPolicy(policy: OverflowPolicy): void;
66
79
  getOverflowPolicy(): OverflowPolicy;
67
80
  private encToken;
@@ -133,9 +146,34 @@ export declare class PDFObjectManager implements FontMetrics {
133
146
  url: string;
134
147
  format: string;
135
148
  } | undefined;
149
+ /**
150
+ * Whether a family can draw this code point at all. An embedded face answers from its `cmap`; a
151
+ * standard-14 one can only draw what Windows-1252 encodes, which is what its `/Widths` array covers.
152
+ * Used to pick a family out of a fallback stack, per code point.
153
+ */
154
+ hasGlyph(codePoint: number, fontFamily: string, fontStyle?: FontStyle): boolean;
155
+ /**
156
+ * Whether the document draws this code point through the colour-emoji path instead of the text
157
+ * font - a fallback emoji FONT, or an image source (`Document({ emoji })`). Such a code point is
158
+ * missing from the text font ON PURPOSE, so glyph coverage must leave it alone. Mirrors exactly
159
+ * what `getCharWidth` measures it as, or a dropped emoji would have been measured and not drawn.
160
+ */
161
+ rendersAsEmoji(codePoint: number, fontFamily: string, fontStyle: FontStyle): boolean;
136
162
  private colorRenders;
137
163
  getCustomFontResource(name: string, style?: FontStyle): FontIndexes | undefined;
138
- encodeCustomText(name: string, text: string, style?: FontStyle): string;
164
+ /** How many glyphs a run draws - its code-point count unless a ligature merged some. `letterSpacing`
165
+ * is per glyph (`Tc`), so this is what it multiplies. */
166
+ shapedGlyphCount(text: string, fontFamily?: string, fontStyle?: FontStyle, ligatures?: boolean): number | undefined;
167
+ /**
168
+ * The shaped glyphs of a run, or undefined when nothing needed shaping. The ONE place shaping is
169
+ * decided, so measuring and drawing cannot disagree. Memoised: a paragraph is measured several
170
+ * times (layout, pagination, drawing).
171
+ */
172
+ shapeText(text: string, fontFamily?: string, fontStyle?: FontStyle, features?: readonly string[]): ShapedGlyph[] | undefined;
173
+ /** Hex Identity-H for glyphs the producer already resolved (a shaped run), which cannot be given as
174
+ * text. Recording each keeps it in the subset. */
175
+ registerGlyphs(name: string, glyphs: readonly number[], style?: FontStyle): string;
176
+ encodeCustomText(name: string, text: string, style?: FontStyle, shape?: boolean): string;
139
177
  private buildFontFile2;
140
178
  private buildFontDescriptor;
141
179
  private buildCIDFont;
@@ -154,7 +192,10 @@ export declare class PDFObjectManager implements FontMetrics {
154
192
  * Standard-14 answers from the AFM `KPX` pairs. An embedded font has no kerning yet (its `kern` /
155
193
  * `GPOS` tables are unparsed), so it returns zeros - never a wrong value, just no adjustment.
156
194
  */
157
- getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle): number[];
195
+ /** Kerning between adjacent GLYPHS - what a shaped run needs, and what the backend already holds.
196
+ * Same lookup as the character path; `getKerning` is keyed by glyph id either way. */
197
+ getGlyphKernPairs(glyphs: readonly number[], fontFamily: string, fontStyle: FontStyle): number[];
198
+ getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle, ligatures?: boolean): number[];
158
199
  /**
159
200
  * Where the glyphs of `text` put ink inside a horizontal band, as x-intervals in POINTS measured
160
201
  * from the start of the run. This is what an underline steps around ("skip-ink").
@@ -164,7 +205,7 @@ export declare class PDFObjectManager implements FontMetrics {
164
205
  * Callers must check `isCustomFont` first rather than treat an empty result as "no descenders".
165
206
  */
166
207
  getInkSpansInBand(text: string, fontSize: number, fontFamily: string, fontStyle: FontStyle, bandTop: number, bandBottom: number, letterSpacing?: number): Array<[number, number]>;
167
- getStringWidth(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle): number;
208
+ getStringWidth(text: string, fontFamily: string, fontSize: number, fontStyle: FontStyle, ligatures?: boolean): number;
168
209
  private getAVMParserByFont;
169
210
  getCharWidth(char: string, fontSize: number, fullFontName?: string, fontName?: string, fontStyle?: FontStyle): number;
170
211
  getAllFontsRaw(): Map<string, FontIndexes>;
@@ -5,8 +5,17 @@ import { zlibSync } from "fflate";
5
5
  import { bytesFromLatin1, latin1FromBytes } from "./bytes.js";
6
6
  import { AFMParser } from "./afm-parser.js";
7
7
  import { mergeSpans, TTFParser } from "./ttf-parser.js";
8
+ import { WoffError, isWoff, woffToSfnt } from "./woff.js";
9
+ import { isWoff2 } from "./woff2.js";
8
10
  import { subsetTTF } from "./ttf-subsetter.js";
9
- import { getArrayBuffer } from "./utf8-to-windows1252-encoder.js";
11
+ import { shapeRun } from "../text/shape.js";
12
+ /** What Latin text is shaped with unless a run says otherwise: the ligatures a font's designer
13
+ * drew for it. On by default like kerning - CSS, browsers and every other renderer do the same,
14
+ * and nobody should have to know the word to get text that is set properly. */
15
+ const LATIN_FEATURES = ["liga"];
16
+ /** What a run is shaped with. Arabic joining is unconditional; this is only the Latin part. */
17
+ const features = (ligatures) => (ligatures ? LATIN_FEATURES : []);
18
+ import { getArrayBuffer, isWindows1252 } from "./utf8-to-windows1252-encoder.js";
10
19
  import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
11
20
  import { StructTree } from "./struct-tree.js";
12
21
  import { OutlineBuilder } from "./outline.js";
@@ -161,6 +170,11 @@ export class PDFObjectManager {
161
170
  // Per-registered face: the reserved font-object numbers + the glyph ids actually used. The font
162
171
  // program is subsetted and the objects filled in by finalizeCustomFonts(), after the render pass.
163
172
  this.customFontEmit = new Map();
173
+ // Shaped runs, keyed by font + text; `null` means "asked, and there was nothing to shape".
174
+ this.shapeCache = new Map();
175
+ // Per font, glyph -> the code points it stands for. A shaped glyph is often absent from the cmap (a
176
+ // ligature) or maps back to a presentation form, so `ToUnicode` cannot come from the cmap alone.
177
+ this.shapedOrigins = new Map();
164
178
  // Embedded file attachments (their /Filespec object numbers + display names). Referenced from
165
179
  // the catalog's /Names/EmbeddedFiles + /AF. Drives ZUGFeRD's embedded factur-x.xml.
166
180
  this.attachments = [];
@@ -182,6 +196,15 @@ export class PDFObjectManager {
182
196
  // Interactive form fields (AcroForm). Empty unless a form field was placed; then finalize() emits the
183
197
  // catalog /AcroForm dict and each field's widget lands in its page /Annots.
184
198
  this._acroform = new AcroFormCollector();
199
+ // Overflow policy for unbreakable content taller than a page region (default "error"); the renderer
200
+ // seeds it into the layout-context root so packChildren can act on it.
201
+ /**
202
+ * Code points no font in the document could draw, so they were removed rather than emitted as
203
+ * `.notdef` (which PDF/A forbids). Collected here because this object is the one thing threaded
204
+ * through BOTH passes; the caller gets them back with the bytes, so an application can say which
205
+ * character went missing instead of a server log nobody reads.
206
+ */
207
+ this.missingGlyphs = new Set();
185
208
  // Whether to kern. Document-level: measuring (via `runAdvance`, which reads this off the metrics)
186
209
  // and drawing (the backend emits a `TJ`) both consult it, so they can never disagree. OFF while the
187
210
  // embedded-font path (kern/GPOS) is unbuilt - a standard-14-only kerning would set the 14 system
@@ -209,8 +232,14 @@ export class PDFObjectManager {
209
232
  setCompress(on) {
210
233
  this.compress = on;
211
234
  }
212
- // Overflow policy for unbreakable content taller than a page region (default "error"); the renderer
213
- // seeds it into the layout-context root so packChildren can act on it.
235
+ reportMissingGlyph(codePoints) {
236
+ for (const cp of codePoints)
237
+ this.missingGlyphs.add(cp);
238
+ }
239
+ /** The dropped code points, in the order first seen. Empty for a document that hit none. */
240
+ getMissingGlyphs() {
241
+ return [...this.missingGlyphs];
242
+ }
214
243
  setOverflowPolicy(policy) {
215
244
  this.overflowPolicy = policy;
216
245
  }
@@ -558,7 +587,18 @@ endstream`;
558
587
  }
559
588
  if (byStyle.has(style))
560
589
  return;
561
- byStyle.set(style, new TTFParser(data));
590
+ // A WOFF is the same sfnt behind a small wrapper, so it is unpacked HERE - the one place every
591
+ // font path meets (a file, raw bytes, a URL, a browser upload) - and TTFParser never learns a
592
+ // second container format.
593
+ // WOFF2 cannot be unwrapped here: its Brotli decoder is imported lazily, which makes it async,
594
+ // and this is a synchronous constructor path. `renderPdf` unwraps it one step earlier - so the
595
+ // only way to arrive here still packed is by calling the engine directly.
596
+ if (isWoff2(data)) {
597
+ throw new WoffError("a WOFF2 font reached the object manager still packed. Pass it through " +
598
+ "renderToBytes(doc, { fonts }) or doc.addFont(...) - unpacking WOFF2 needs Brotli, which " +
599
+ "is loaded asynchronously, so it happens one step earlier in renderPdf.");
600
+ }
601
+ byStyle.set(style, new TTFParser(isWoff(data) ? woffToSfnt(data) : data));
562
602
  this.verticalsCache.delete(name); // this family now answers from the .ttf, not from an AFM
563
603
  this.decorationCache.delete(name);
564
604
  // Emission (the PDF font objects + page resource) is DEFERRED to first use via ensureEmitted(),
@@ -595,13 +635,13 @@ endstream`;
595
635
  // Fills the reserved font objects with a SUBSET of each font (only the glyphs the document used),
596
636
  // tagged "ABCDEF+" as PDF/A requires for subsets. Call once, after the render pass, before output.
597
637
  finalizeCustomFonts() {
598
- for (const e of this.customFontEmit.values()) {
638
+ for (const [key, e] of this.customFontEmit) {
599
639
  const ttf = e.ttf;
600
640
  const base = `${this.subsetTag(e.pdfName, e.used)}+${e.pdfName}`;
601
641
  this.replaceObject(e.fontFile, this.buildFontFile2(ttf, e.used));
602
642
  this.replaceObject(e.descriptor, this.buildFontDescriptor(base, ttf, e.fontFile));
603
643
  this.replaceObject(e.cidFont, this.buildCIDFont(base, ttf, e.descriptor, e.used));
604
- this.replaceObject(e.toUnicode, this.buildToUnicode(ttf, e.used));
644
+ this.replaceObject(e.toUnicode, this.buildToUnicode(ttf, e.used, this.shapedOrigins.get(key)));
605
645
  this.replaceObject(e.type0, this.buildType0(base, e.cidFont, e.toUnicode));
606
646
  }
607
647
  }
@@ -673,6 +713,34 @@ endstream`;
673
713
  return this.emojiImage;
674
714
  }
675
715
  // Whether `ttf` has a COLOR glyph for a code point (i.e. it would render it as color emoji).
716
+ /**
717
+ * Whether a family can draw this code point at all. An embedded face answers from its `cmap`; a
718
+ * standard-14 one can only draw what Windows-1252 encodes, which is what its `/Widths` array covers.
719
+ * Used to pick a family out of a fallback stack, per code point.
720
+ */
721
+ hasGlyph(codePoint, fontFamily, fontStyle = FontStyle.Normal) {
722
+ const ttf = this.getCustomFont(fontFamily, fontStyle);
723
+ if (ttf)
724
+ return ttf.getGlyphIndex(codePoint) !== 0;
725
+ return isWindows1252(codePoint);
726
+ }
727
+ /**
728
+ * Whether the document draws this code point through the colour-emoji path instead of the text
729
+ * font - a fallback emoji FONT, or an image source (`Document({ emoji })`). Such a code point is
730
+ * missing from the text font ON PURPOSE, so glyph coverage must leave it alone. Mirrors exactly
731
+ * what `getCharWidth` measures it as, or a dropped emoji would have been measured and not drawn.
732
+ */
733
+ rendersAsEmoji(codePoint, fontFamily, fontStyle) {
734
+ const ttf = this.getCustomFont(fontFamily, fontStyle);
735
+ if (ttf && this.colorRenders(ttf, codePoint))
736
+ return true;
737
+ if (this.emojiFontName && this.emojiFontName !== fontFamily) {
738
+ const emoji = this.getColorFont(this.emojiFontName, fontStyle);
739
+ if (emoji && this.colorRenders(emoji, codePoint))
740
+ return true;
741
+ }
742
+ return this.emojiImage !== undefined && isEmojiCodePoint(codePoint);
743
+ }
676
744
  colorRenders(ttf, codePoint) {
677
745
  return ttf.getColorGlyph(ttf.getGlyphIndex(codePoint)) !== null;
678
746
  }
@@ -685,9 +753,56 @@ endstream`;
685
753
  this.ensureEmitted(name, resolved);
686
754
  return this.fonts.getFont(name, resolved);
687
755
  }
756
+ /** How many glyphs a run draws - its code-point count unless a ligature merged some. `letterSpacing`
757
+ * is per glyph (`Tc`), so this is what it multiplies. */
758
+ shapedGlyphCount(text, fontFamily, fontStyle, ligatures = true) {
759
+ return this.shapeText(text, fontFamily, fontStyle, features(ligatures))?.length;
760
+ }
761
+ /**
762
+ * The shaped glyphs of a run, or undefined when nothing needed shaping. The ONE place shaping is
763
+ * decided, so measuring and drawing cannot disagree. Memoised: a paragraph is measured several
764
+ * times (layout, pagination, drawing).
765
+ */
766
+ shapeText(text, fontFamily, fontStyle = FontStyle.Normal, features = LATIN_FEATURES) {
767
+ const resolved = this.resolveCustomStyle(fontFamily, fontStyle);
768
+ if (!resolved)
769
+ return undefined;
770
+ const key = `${this.customKey(fontFamily, resolved)}\u0000${features.join(",")}\u0000${text}`;
771
+ const cached = this.shapeCache.get(key);
772
+ if (cached !== undefined)
773
+ return cached ?? undefined;
774
+ const ttf = this.customFonts.get(fontFamily).get(resolved);
775
+ const shaped = shapeRun([...text].map((c) => c.codePointAt(0)), ttf, features) ?? null;
776
+ this.shapeCache.set(key, shaped);
777
+ if (shaped) {
778
+ // Recorded at shaping time - the only moment the glyph and its code points are known together.
779
+ const fontKey = this.customKey(fontFamily, resolved);
780
+ let origins = this.shapedOrigins.get(fontKey);
781
+ if (!origins)
782
+ this.shapedOrigins.set(fontKey, (origins = new Map()));
783
+ for (const g of shaped)
784
+ origins.set(g.glyph, g.codePoints);
785
+ }
786
+ return shaped ?? undefined;
787
+ }
788
+ /** Hex Identity-H for glyphs the producer already resolved (a shaped run), which cannot be given as
789
+ * text. Recording each keeps it in the subset. */
790
+ registerGlyphs(name, glyphs, style = FontStyle.Normal) {
791
+ const resolved = this.resolveCustomStyle(name, style);
792
+ if (!resolved)
793
+ return "";
794
+ this.ensureEmitted(name, resolved);
795
+ const emit = this.customFontEmit.get(this.customKey(name, resolved));
796
+ let out = "";
797
+ for (const g of glyphs) {
798
+ emit.used.add(g);
799
+ out += g.toString(16).padStart(4, "0").toUpperCase();
800
+ }
801
+ return out;
802
+ }
688
803
  // Encodes text as a hex Identity-H string for an embedded font's Tj operator: each codepoint
689
804
  // becomes its 2-byte glyph id (CID == GID under /CIDToGIDMap /Identity).
690
- encodeCustomText(name, text, style = FontStyle.Normal) {
805
+ encodeCustomText(name, text, style = FontStyle.Normal, shape = true) {
691
806
  const resolved = this.resolveCustomStyle(name, style);
692
807
  if (!resolved)
693
808
  return "";
@@ -695,11 +810,24 @@ endstream`;
695
810
  // Once per text run, not per character: the flat key is fine here.
696
811
  const emit = this.customFontEmit.get(this.customKey(name, resolved));
697
812
  const { ttf, used } = emit;
813
+ const hex4 = (gid) => gid.toString(16).padStart(4, "0").toUpperCase();
814
+ // The SAME shaped run the measuring path used, so the glyphs drawn are the glyphs measured.
815
+ // `shape: false` is for a caller that has ALREADY decided the run is unshaped - the renderer puts
816
+ // shaped glyphs in the IR node, so the backend's text path is the unshaped one by definition.
817
+ const shaped = shape ? this.shapeText(text, name, style) : undefined;
818
+ if (shaped) {
819
+ let out = "";
820
+ for (const g of shaped) {
821
+ used.add(g.glyph);
822
+ out += hex4(g.glyph);
823
+ }
824
+ return out;
825
+ }
698
826
  let hex = "";
699
827
  for (const ch of text) {
700
828
  const gid = ttf.getGlyphIndex(ch.codePointAt(0));
701
829
  used.add(gid); // record the glyph so the subset keeps it
702
- hex += gid.toString(16).padStart(4, "0").toUpperCase();
830
+ hex += hex4(gid);
703
831
  }
704
832
  return hex;
705
833
  }
@@ -732,17 +860,24 @@ endstream`;
732
860
  }
733
861
  // Maps glyph id -> Unicode so the text stays copy-/searchable (rendering doesn't need it). Only
734
862
  // the used glyphs are listed, to match the subset.
735
- buildToUnicode(ttf, used) {
863
+ buildToUnicode(ttf, used, shapedOrigins = new Map()) {
736
864
  const hex4 = (n) => n.toString(16).padStart(4, "0").toUpperCase();
865
+ // A code point above the BMP needs its surrogate PAIR here - a `bfchar` destination is UTF-16BE.
866
+ const utf16 = (code) => code > 0xffff
867
+ ? hex4(0xd800 + ((code - 0x10000) >> 10)) + hex4(0xdc00 + ((code - 0x10000) & 0x3ff))
868
+ : hex4(code);
737
869
  const rev = ttf.reverseCmap();
870
+ // A SHAPED glyph wins: it is often absent from the cmap (a ligature) or maps back to a
871
+ // presentation form rather than the letter that was written (a joining form), so the reverse
872
+ // cmap alone would make copied text wrong.
738
873
  const entries = [...used]
739
874
  .sort((a, b) => a - b)
740
- .filter((g) => rev.has(g))
741
- .map((g) => [g, rev.get(g)]);
875
+ .filter((g) => shapedOrigins.has(g) || rev.has(g))
876
+ .map((g) => [g, shapedOrigins.get(g) ?? [rev.get(g)]]);
742
877
  const blocks = [];
743
878
  for (let i = 0; i < entries.length; i += 100) {
744
879
  const block = entries.slice(i, i + 100);
745
- const lines = block.map(([gid, code]) => `<${hex4(gid)}> <${hex4(code)}>`);
880
+ const lines = block.map(([gid, codes]) => `<${hex4(gid)}> <${codes.map(utf16).join("")}>`);
746
881
  blocks.push(`${block.length} beginbfchar\n${lines.join("\n")}\nendbfchar`);
747
882
  }
748
883
  const body = `/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n` +
@@ -814,10 +949,30 @@ endstream`;
814
949
  * Standard-14 answers from the AFM `KPX` pairs. An embedded font has no kerning yet (its `kern` /
815
950
  * `GPOS` tables are unparsed), so it returns zeros - never a wrong value, just no adjustment.
816
951
  */
817
- getKernPairs(text, fontFamily, fontStyle) {
952
+ /** Kerning between adjacent GLYPHS - what a shaped run needs, and what the backend already holds.
953
+ * Same lookup as the character path; `getKerning` is keyed by glyph id either way. */
954
+ getGlyphKernPairs(glyphs, fontFamily, fontStyle) {
955
+ const ttf = this.getCustomFont(fontFamily, fontStyle);
956
+ if (!ttf)
957
+ return [];
958
+ const pairs = [];
959
+ for (let i = 0; i < glyphs.length - 1; i++) {
960
+ pairs.push(ttf.getKerning(glyphs[i], glyphs[i + 1]));
961
+ }
962
+ return pairs;
963
+ }
964
+ getKernPairs(text, fontFamily, fontStyle, ligatures = true) {
818
965
  const chars = [...text];
819
966
  if (chars.length < 2)
820
967
  return [];
968
+ // A shaped run kerns over its SHAPED glyphs, not its characters: after shaping there may be fewer
969
+ // glyphs than code points (a ligature), and the pairs that matter are the ones actually drawn.
970
+ // `getKerning` is keyed by glyph id anyway, so this is the same lookup - and the backend now emits
971
+ // a `TJ` over glyph chunks, which is what used to be missing. One adjustment per adjacent DRAWN
972
+ // pair, either path, so `runAdvance` measures exactly what is drawn.
973
+ const shaped = this.shapeText(text, fontFamily, fontStyle, features(ligatures));
974
+ if (shaped)
975
+ return this.getGlyphKernPairs(shaped.map((g) => g.glyph), fontFamily, fontStyle);
821
976
  const out = [];
822
977
  // An embedded font kerns by GLYPH id (kern table / GPOS); a standard-14 one by character (AFM).
823
978
  const ttf = this.getCustomFont(fontFamily, fontStyle);
@@ -868,7 +1023,14 @@ endstream`;
868
1023
  return mergeSpans(spans);
869
1024
  }
870
1025
  // Returns the current width of a text, included kernings
871
- getStringWidth(text, fontFamily, fontSize, fontStyle) {
1026
+ getStringWidth(text, fontFamily, fontSize, fontStyle, ligatures = true) {
1027
+ // Shaping first: a joined run is narrower than the same letters apart, so measuring the unshaped
1028
+ // form while drawing the shaped one would break every line. Both sides ask `shapeText`.
1029
+ const shaped = this.shapeText(text, fontFamily, fontStyle, features(ligatures));
1030
+ if (shaped) {
1031
+ const ttf = this.getCustomFont(fontFamily, fontStyle);
1032
+ return shaped.reduce((w, g) => w + ttf.unitsToPoints(g.advance, fontSize), 0);
1033
+ }
872
1034
  let width = 0;
873
1035
  // Iterate code points, not UTF-16 units: an astral char (emoji, CJK-ext) is a surrogate pair,
874
1036
  // and indexing by unit would measure each half as its own (zero-width) "char". The spread splits
@@ -1,6 +1,18 @@
1
+ /**
2
+ * Thrown when an element reaches the render pass with no registered renderer.
3
+ *
4
+ * This used to return undefined and every call site skipped the element silently, so a whole document
5
+ * could serialize to an empty content stream and still be a valid PDF - a blank page, no warning
6
+ * (ISSUE-11). The registry is keyed on the CONSTRUCTOR, which makes it the one place in the engine that
7
+ * two loaded copies of the library break: elements built by copy A are invisible to copy B.
8
+ */
9
+ export declare class MissingRendererError extends Error {
10
+ readonly elementName: string;
11
+ constructor(elementName: string);
12
+ }
1
13
  export declare class RendererRegistry {
2
14
  private static renderers;
3
15
  static register(elementClass: Function, renderer: Function): void;
4
- static getRenderer(element: object): Function | undefined;
16
+ static getRenderer(element: object): Function;
5
17
  static isRendererAsync(renderer: Function): boolean;
6
18
  }