@jasy/pdf 1.0.0-alpha.1 → 1.0.0-alpha.11

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 (279) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -21
  3. package/dist/api/args.d.ts +1 -1
  4. package/dist/api/args.js +2 -5
  5. package/dist/api/color.d.ts +4 -4
  6. package/dist/api/color.js +11 -17
  7. package/dist/api/content.d.ts +20 -13
  8. package/dist/api/content.js +44 -29
  9. package/dist/api/descriptor.d.ts +2 -2
  10. package/dist/api/descriptor.js +121 -31
  11. package/dist/api/dimension.d.ts +76 -0
  12. package/dist/api/dimension.js +43 -0
  13. package/dist/api/font-url.d.ts +28 -0
  14. package/dist/api/font-url.js +134 -0
  15. package/dist/api/forms.d.ts +347 -0
  16. package/dist/api/forms.js +189 -0
  17. package/dist/api/gradient.d.ts +53 -0
  18. package/dist/api/gradient.js +97 -0
  19. package/dist/api/index.d.ts +13 -8
  20. package/dist/api/index.js +13 -24
  21. package/dist/api/insets.d.ts +19 -12
  22. package/dist/api/insets.js +19 -12
  23. package/dist/api/layout.d.ts +149 -25
  24. package/dist/api/layout.js +182 -51
  25. package/dist/api/page-builder.d.ts +40 -0
  26. package/dist/api/page-builder.js +44 -0
  27. package/dist/api/structure.d.ts +107 -13
  28. package/dist/api/structure.js +187 -88
  29. package/dist/api/table.d.ts +5 -5
  30. package/dist/api/table.js +38 -26
  31. package/dist/api/text.d.ts +90 -4
  32. package/dist/api/text.js +116 -31
  33. package/dist/assets/font-data.d.ts +2 -0
  34. package/dist/assets/font-data.js +21 -0
  35. package/dist/assets/font-data.ts +37 -0
  36. package/dist/common/color.js +1 -5
  37. package/dist/constants/page-sizes.js +3 -6
  38. package/dist/constants/pdf-parts.js +1 -4
  39. package/dist/crypto/legacy-handler.d.ts +43 -0
  40. package/dist/crypto/legacy-handler.js +145 -0
  41. package/dist/crypto/security-handler.d.ts +62 -0
  42. package/dist/crypto/security-handler.js +155 -0
  43. package/dist/crypto/webcrypto.d.ts +11 -0
  44. package/dist/crypto/webcrypto.js +62 -0
  45. package/dist/edit/acroform-reader.d.ts +70 -0
  46. package/dist/edit/acroform-reader.js +126 -0
  47. package/dist/edit/appearance.d.ts +47 -0
  48. package/dist/edit/appearance.js +171 -0
  49. package/dist/edit/document.d.ts +145 -0
  50. package/dist/edit/document.js +642 -0
  51. package/dist/edit/fill.d.ts +65 -0
  52. package/dist/edit/fill.js +424 -0
  53. package/dist/edit/flatten.d.ts +54 -0
  54. package/dist/edit/flatten.js +462 -0
  55. package/dist/edit/index.d.ts +20 -0
  56. package/dist/edit/index.js +18 -0
  57. package/dist/edit/lexer.d.ts +30 -0
  58. package/dist/edit/lexer.js +285 -0
  59. package/dist/edit/objects.d.ts +68 -0
  60. package/dist/edit/objects.js +83 -0
  61. package/dist/edit/writer.d.ts +60 -0
  62. package/dist/edit/writer.js +283 -0
  63. package/dist/elements/container-element.d.ts +50 -5
  64. package/dist/elements/container-element.js +100 -39
  65. package/dist/elements/forms/checkbox-element.d.ts +51 -0
  66. package/dist/elements/forms/checkbox-element.js +48 -0
  67. package/dist/elements/forms/choice-element.d.ts +53 -0
  68. package/dist/elements/forms/choice-element.js +59 -0
  69. package/dist/elements/forms/push-button-element.d.ts +56 -0
  70. package/dist/elements/forms/push-button-element.js +49 -0
  71. package/dist/elements/forms/radio-element.d.ts +52 -0
  72. package/dist/elements/forms/radio-element.js +49 -0
  73. package/dist/elements/forms/signature-element.d.ts +54 -0
  74. package/dist/elements/forms/signature-element.js +47 -0
  75. package/dist/elements/forms/text-field-element.d.ts +61 -0
  76. package/dist/elements/forms/text-field-element.js +54 -0
  77. package/dist/elements/image-element.d.ts +58 -6
  78. package/dist/elements/image-element.js +145 -111
  79. package/dist/elements/index.d.ts +26 -10
  80. package/dist/elements/index.js +26 -28
  81. package/dist/elements/layout/anchor-element.d.ts +23 -0
  82. package/dist/elements/layout/anchor-element.js +27 -0
  83. package/dist/elements/layout/bookmark-element.d.ts +27 -0
  84. package/dist/elements/layout/bookmark-element.js +30 -0
  85. package/dist/elements/layout/default-text-style-element.d.ts +30 -0
  86. package/dist/elements/layout/default-text-style-element.js +47 -0
  87. package/dist/elements/layout/deferred-element.d.ts +3 -3
  88. package/dist/elements/layout/deferred-element.js +4 -8
  89. package/dist/elements/layout/expanded-element.d.ts +6 -4
  90. package/dist/elements/layout/expanded-element.js +22 -17
  91. package/dist/elements/layout/keep-together-element.d.ts +42 -0
  92. package/dist/elements/layout/keep-together-element.js +85 -0
  93. package/dist/elements/layout/link-element.d.ts +33 -0
  94. package/dist/elements/layout/link-element.js +39 -0
  95. package/dist/elements/layout/padding-element.d.ts +10 -5
  96. package/dist/elements/layout/padding-element.js +25 -16
  97. package/dist/elements/layout/page-break-element.d.ts +17 -0
  98. package/dist/elements/layout/page-break-element.js +24 -0
  99. package/dist/elements/layout/page-builder-element.d.ts +32 -0
  100. package/dist/elements/layout/page-builder-element.js +45 -0
  101. package/dist/elements/layout/positioned-element.d.ts +47 -0
  102. package/dist/elements/layout/positioned-element.js +73 -0
  103. package/dist/elements/layout/repeating-header-element.d.ts +3 -3
  104. package/dist/elements/layout/repeating-header-element.js +8 -12
  105. package/dist/elements/layout/rotated-box-element.d.ts +35 -0
  106. package/dist/elements/layout/rotated-box-element.js +54 -0
  107. package/dist/elements/layout/rotated-element.d.ts +32 -0
  108. package/dist/elements/layout/rotated-element.js +41 -0
  109. package/dist/elements/layout/sized-container-element.d.ts +2 -2
  110. package/dist/elements/layout/sized-container-element.js +6 -11
  111. package/dist/elements/layout/struct-group.d.ts +28 -0
  112. package/dist/elements/layout/struct-group.js +42 -0
  113. package/dist/elements/line-element.d.ts +3 -3
  114. package/dist/elements/line-element.js +5 -10
  115. package/dist/elements/page-element.d.ts +26 -6
  116. package/dist/elements/page-element.js +72 -27
  117. package/dist/elements/pdf-document-element.d.ts +10 -4
  118. package/dist/elements/pdf-document-element.js +11 -10
  119. package/dist/elements/pdf-element.d.ts +150 -4
  120. package/dist/elements/pdf-element.js +138 -20
  121. package/dist/elements/rectangle-element.d.ts +52 -11
  122. package/dist/elements/rectangle-element.js +129 -36
  123. package/dist/elements/row-element.d.ts +45 -4
  124. package/dist/elements/row-element.js +66 -18
  125. package/dist/elements/text-element.d.ts +139 -11
  126. package/dist/elements/text-element.js +234 -57
  127. package/dist/forms/acroform.d.ts +63 -0
  128. package/dist/forms/acroform.js +427 -0
  129. package/dist/forms/appearance.d.ts +58 -0
  130. package/dist/forms/appearance.js +204 -0
  131. package/dist/forms/field.d.ts +164 -0
  132. package/dist/forms/field.js +1 -0
  133. package/dist/forms/pdf.d.ts +19 -0
  134. package/dist/forms/pdf.js +28 -0
  135. package/dist/index.d.ts +3 -3
  136. package/dist/index.js +3 -19
  137. package/dist/ir/display-list.d.ts +197 -8
  138. package/dist/ir/display-list.js +8 -2
  139. package/dist/layout/alignment.d.ts +10 -0
  140. package/dist/layout/alignment.js +6 -0
  141. package/dist/layout/box-constraints.d.ts +61 -0
  142. package/dist/layout/box-constraints.js +88 -6
  143. package/dist/layout/collect-images.d.ts +9 -0
  144. package/dist/layout/collect-images.js +34 -0
  145. package/dist/layout/fragmentation.d.ts +20 -1
  146. package/dist/layout/fragmentation.js +74 -18
  147. package/dist/layout/insets.d.ts +28 -0
  148. package/dist/layout/insets.js +20 -0
  149. package/dist/platform/browser-fs.d.ts +2 -0
  150. package/dist/platform/browser-fs.js +9 -0
  151. package/dist/platform/browser-image.d.ts +5 -0
  152. package/dist/platform/browser-image.js +13 -0
  153. package/dist/platform/node-fs.d.ts +2 -0
  154. package/dist/platform/node-fs.js +10 -0
  155. package/dist/platform/node-image.d.ts +5 -0
  156. package/dist/platform/node-image.js +9 -0
  157. package/dist/renderer/anchor-renderer.d.ts +6 -0
  158. package/dist/renderer/anchor-renderer.js +11 -0
  159. package/dist/renderer/bookmark-renderer.d.ts +6 -0
  160. package/dist/renderer/bookmark-renderer.js +11 -0
  161. package/dist/renderer/container-renderer.d.ts +3 -3
  162. package/dist/renderer/container-renderer.js +11 -28
  163. package/dist/renderer/default-text-style-renderer.d.ts +6 -0
  164. package/dist/renderer/default-text-style-renderer.js +10 -0
  165. package/dist/renderer/deferred-renderer.d.ts +3 -3
  166. package/dist/renderer/deferred-renderer.js +8 -23
  167. package/dist/renderer/emoji-image.d.ts +9 -0
  168. package/dist/renderer/emoji-image.js +68 -0
  169. package/dist/renderer/expanded-renderer.d.ts +3 -3
  170. package/dist/renderer/expanded-renderer.js +6 -21
  171. package/dist/renderer/form-field-renderer.d.ts +20 -0
  172. package/dist/renderer/form-field-renderer.js +12 -0
  173. package/dist/renderer/image-renderer.d.ts +4 -4
  174. package/dist/renderer/image-renderer.js +86 -75
  175. package/dist/renderer/index.d.ts +10 -10
  176. package/dist/renderer/index.js +10 -26
  177. package/dist/renderer/keep-together-renderer.d.ts +10 -0
  178. package/dist/renderer/keep-together-renderer.js +12 -0
  179. package/dist/renderer/line-renderer.d.ts +3 -3
  180. package/dist/renderer/line-renderer.js +13 -28
  181. package/dist/renderer/link-renderer.d.ts +6 -0
  182. package/dist/renderer/link-renderer.js +11 -0
  183. package/dist/renderer/padding-renderer.d.ts +3 -3
  184. package/dist/renderer/padding-renderer.js +6 -21
  185. package/dist/renderer/page-break-renderer.d.ts +14 -0
  186. package/dist/renderer/page-break-renderer.js +15 -0
  187. package/dist/renderer/page-builder-renderer.d.ts +6 -0
  188. package/dist/renderer/page-builder-renderer.js +11 -0
  189. package/dist/renderer/page-renderer.d.ts +2 -2
  190. package/dist/renderer/page-renderer.js +115 -77
  191. package/dist/renderer/pdf-backend.d.ts +31 -4
  192. package/dist/renderer/pdf-backend.js +287 -49
  193. package/dist/renderer/pdf-config.js +4 -7
  194. package/dist/renderer/pdf-document-class.d.ts +5 -5
  195. package/dist/renderer/pdf-document-class.js +27 -41
  196. package/dist/renderer/pdf-document-renderer.d.ts +19 -10
  197. package/dist/renderer/pdf-document-renderer.js +135 -91
  198. package/dist/renderer/pdf-renderer.d.ts +2 -2
  199. package/dist/renderer/pdf-renderer.js +165 -90
  200. package/dist/renderer/positioned-renderer.d.ts +6 -0
  201. package/dist/renderer/positioned-renderer.js +10 -0
  202. package/dist/renderer/rectangle-renderer.d.ts +3 -3
  203. package/dist/renderer/rectangle-renderer.js +49 -44
  204. package/dist/renderer/repeating-header-renderer.d.ts +3 -3
  205. package/dist/renderer/repeating-header-renderer.js +10 -26
  206. package/dist/renderer/rotated-renderer.d.ts +7 -0
  207. package/dist/renderer/rotated-renderer.js +29 -0
  208. package/dist/renderer/row-renderer.d.ts +3 -3
  209. package/dist/renderer/row-renderer.js +11 -28
  210. package/dist/renderer/struct-group-renderer.d.ts +10 -0
  211. package/dist/renderer/struct-group-renderer.js +24 -0
  212. package/dist/renderer/text-renderer.d.ts +9 -5
  213. package/dist/renderer/text-renderer.js +437 -89
  214. package/dist/text/advance.d.ts +34 -0
  215. package/dist/text/advance.js +45 -0
  216. package/dist/text/arabic.d.ts +20 -0
  217. package/dist/text/arabic.js +101 -0
  218. package/dist/text/bidi.d.ts +33 -0
  219. package/dist/text/bidi.js +94 -0
  220. package/dist/text/emoji-codepoints.d.ts +1 -0
  221. package/dist/text/emoji-codepoints.js +29 -0
  222. package/dist/text/font-fallback.d.ts +25 -0
  223. package/dist/text/font-fallback.js +38 -0
  224. package/dist/text/glyph-coverage.d.ts +15 -0
  225. package/dist/text/glyph-coverage.js +77 -0
  226. package/dist/text/line-breaker.d.ts +50 -12
  227. package/dist/text/line-breaker.js +200 -54
  228. package/dist/text/line-metrics.d.ts +49 -0
  229. package/dist/text/line-metrics.js +57 -0
  230. package/dist/text/orphans-widows.d.ts +27 -0
  231. package/dist/text/orphans-widows.js +53 -0
  232. package/dist/text/shape.d.ts +29 -0
  233. package/dist/text/shape.js +67 -0
  234. package/dist/text/text-decoration.d.ts +50 -0
  235. package/dist/text/text-decoration.js +62 -0
  236. package/dist/text/text-style.d.ts +60 -0
  237. package/dist/text/text-style.js +79 -0
  238. package/dist/text/whitespace.d.ts +4 -0
  239. package/dist/text/whitespace.js +43 -0
  240. package/dist/utils/afm-parser.d.ts +34 -0
  241. package/dist/utils/afm-parser.js +76 -17
  242. package/dist/utils/bytes.d.ts +24 -0
  243. package/dist/utils/bytes.js +76 -0
  244. package/dist/utils/dest-registry.d.ts +22 -0
  245. package/dist/utils/dest-registry.js +32 -0
  246. package/dist/utils/flex-layout.d.ts +36 -9
  247. package/dist/utils/flex-layout.js +207 -30
  248. package/dist/utils/font-metrics.d.ts +26 -1
  249. package/dist/utils/font-metrics.js +1 -2
  250. package/dist/utils/font-path.js +3 -6
  251. package/dist/utils/gsub.d.ts +36 -0
  252. package/dist/utils/gsub.js +178 -0
  253. package/dist/utils/image-helper.d.ts +6 -5
  254. package/dist/utils/image-helper.js +101 -111
  255. package/dist/utils/inflate.d.ts +16 -0
  256. package/dist/utils/inflate.js +52 -0
  257. package/dist/utils/md5.d.ts +4 -0
  258. package/dist/utils/md5.js +79 -0
  259. package/dist/utils/outline.d.ts +21 -0
  260. package/dist/utils/outline.js +79 -0
  261. package/dist/utils/pdf-object-manager.d.ts +127 -8
  262. package/dist/utils/pdf-object-manager.js +615 -141
  263. package/dist/utils/renderer-registry.d.ts +13 -1
  264. package/dist/utils/renderer-registry.js +23 -6
  265. package/dist/utils/struct-tree.d.ts +44 -0
  266. package/dist/utils/struct-tree.js +129 -0
  267. package/dist/utils/ttf-parser.d.ts +134 -2
  268. package/dist/utils/ttf-parser.js +873 -50
  269. package/dist/utils/ttf-subsetter.d.ts +1 -1
  270. package/dist/utils/ttf-subsetter.js +40 -42
  271. package/dist/utils/ua-xmp.d.ts +9 -0
  272. package/dist/utils/ua-xmp.js +32 -0
  273. package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
  274. package/dist/utils/utf8-to-windows1252-encoder.js +11 -4
  275. package/dist/utils/woff.d.ts +10 -0
  276. package/dist/utils/woff.js +116 -0
  277. package/dist/validators/element-validator.d.ts +2 -2
  278. package/dist/validators/element-validator.js +31 -26
  279. package/package.json +22 -2
@@ -1,57 +1,32 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.PDFObjectManager = exports.FontStyle = void 0;
37
- const page_sizes_1 = require("../constants/page-sizes");
38
- const fs = __importStar(require("fs"));
39
- const path = __importStar(require("path"));
40
- const crypto_1 = require("crypto");
41
- const zlib_1 = require("zlib");
42
- const afm_parser_1 = require("./afm-parser");
43
- const ttf_parser_1 = require("./ttf-parser");
44
- const ttf_subsetter_1 = require("./ttf-subsetter");
1
+ import { pageFormats, PageSize } from "../constants/page-sizes.js";
2
+ import { STANDARD_AFM } from "../assets/font-data.js";
3
+ import { md5, md5Hex } from "./md5.js";
4
+ import { zlibSync } from "fflate";
5
+ import { bytesFromLatin1, latin1FromBytes } from "./bytes.js";
6
+ import { AFMParser } from "./afm-parser.js";
7
+ import { mergeSpans, TTFParser } from "./ttf-parser.js";
8
+ import { isWoff, woffToSfnt } from "./woff.js";
9
+ import { subsetTTF } from "./ttf-subsetter.js";
10
+ import { shapeRun } from "../text/shape.js";
11
+ import { getArrayBuffer, isWindows1252 } from "./utf8-to-windows1252-encoder.js";
12
+ import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
13
+ import { StructTree } from "./struct-tree.js";
14
+ import { OutlineBuilder } from "./outline.js";
15
+ import { DestRegistry } from "./dest-registry.js";
16
+ import { AcroFormCollector } from "../forms/acroform.js";
45
17
  // Enums come from the leaf config module (never in a cycle); the config type is
46
18
  // erased at runtime so it can come from the cyclic module safely.
47
- const pdf_config_1 = require("../renderer/pdf-config");
48
- var FontStyle;
19
+ import { ColorMode, Orientation } from "../renderer/pdf-config.js";
20
+ export var FontStyle;
49
21
  (function (FontStyle) {
50
22
  FontStyle["Normal"] = "normal";
51
23
  FontStyle["Bold"] = "bold";
52
24
  FontStyle["Italic"] = "italic";
53
25
  FontStyle["BoldItalic"] = "boldItalic";
54
- })(FontStyle || (exports.FontStyle = FontStyle = {}));
26
+ })(FontStyle || (FontStyle = {}));
27
+ // Escapes a string into a valid PDF /Name token. A raw space or delimiter in a /Name (e.g. a font
28
+ // family like "Great Vibes" or "Times New Roman") breaks the dictionary, so such chars become #XX.
29
+ const pdfName = (s) => s.replace(/[^\x21-\x7e]|[#()<>[\]{}/%]/g, (c) => "#" + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0"));
55
30
  // Suffix appended to an embedded font's PDF /BaseFont so each style variant of a family keeps
56
31
  // a distinct name (the variants are otherwise selected by the font resource, not the name).
57
32
  const STYLE_SUFFIX = {
@@ -152,7 +127,19 @@ class FontManager {
152
127
  return `${fontName}-${fontStyle}`;
153
128
  }
154
129
  }
155
- class PDFObjectManager {
130
+ export class PDFObjectManager {
131
+ get struct() {
132
+ return this._struct;
133
+ }
134
+ get outline() {
135
+ return this._outline;
136
+ }
137
+ get dests() {
138
+ return this._dests;
139
+ }
140
+ get acroform() {
141
+ return this._acroform;
142
+ }
156
143
  constructor(pageSize) {
157
144
  this.objects = [];
158
145
  this.objectPositions = [];
@@ -160,24 +147,64 @@ class PDFObjectManager {
160
147
  this.fonts = new FontManager(); // Stores the fonts
161
148
  this.images = new ImageManager(); // Stores the images (object numbers and names)
162
149
  this.extGStates = new ExtGStateManager(); // Transparency states
163
- this.pageFormat = page_sizes_1.pageFormats[page_sizes_1.PageSize.A4];
150
+ this.shadings = new Map(); // gradient resource name -> object number
151
+ this.pageFormat = pageFormats[PageSize.A4];
164
152
  this.afmParsers = [];
165
153
  // Embedded TrueType fonts, keyed by the name the user registers them under. When a font
166
154
  // name is in here the metric/emission paths take the TTF branch instead of the AFM one.
155
+ // Nested by family, then by style. The flat `${name}-${style}` key it replaced had to be BUILT on
156
+ // every lookup, and the lookups happen once per character - the string allocation alone was a third
157
+ // of a custom-font render. `customFontEmit` below keeps the flat key: it is touched once per face.
167
158
  this.customFonts = new Map();
159
+ // Memoised getFontVerticals answers, family -> style -> metrics. Cleared when a font registers.
160
+ this.verticalsCache = new Map();
161
+ // Same, for getFontDecoration.
162
+ this.decorationCache = new Map();
168
163
  // Per-registered face: the reserved font-object numbers + the glyph ids actually used. The font
169
164
  // program is subsetted and the objects filled in by finalizeCustomFonts(), after the render pass.
170
165
  this.customFontEmit = new Map();
166
+ // Shaped runs, keyed by font + text; `null` means "asked, and there was nothing to shape".
167
+ this.shapeCache = new Map();
168
+ // Per font, glyph -> the code points it stands for. A shaped glyph is often absent from the cmap (a
169
+ // ligature) or maps back to a presentation form, so `ToUnicode` cannot come from the cmap alone.
170
+ this.shapedOrigins = new Map();
171
171
  // Embedded file attachments (their /Filespec object numbers + display names). Referenced from
172
172
  // the catalog's /Names/EmbeddedFiles + /AF. Drives ZUGFeRD's embedded factur-x.xml.
173
173
  this.attachments = [];
174
174
  // PDF header version (PDF/A-3 needs 1.7) and whether to write a trailer /ID (PDF/A needs one).
175
- // Both default to the pre-PDF/A behaviour, so a normal document stays byte-identical.
175
+ // Both default to the pre-PDF/A behavior, so a normal document stays byte-identical.
176
176
  this.pdfVersion = "1.4";
177
177
  this.documentId = false;
178
178
  this.compress = false;
179
+ this.overflowPolicy = "error";
180
+ this.encJobs = [];
181
+ this.strJobs = [];
182
+ // Accessible (PDF/UA) tagging, off by default (byte-identical output); the API turns it on.
183
+ this._struct = new StructTree();
184
+ // Document outline (bookmarks). Empty unless a `Bookmark` was placed; then finalize() emits /Outlines.
185
+ this._outline = new OutlineBuilder();
186
+ // Named destinations (internal-link jump targets). Empty unless an `Anchor` was placed; then it emits
187
+ // a /Names /Dests tree.
188
+ this._dests = new DestRegistry();
189
+ // Interactive form fields (AcroForm). Empty unless a form field was placed; then finalize() emits the
190
+ // catalog /AcroForm dict and each field's widget lands in its page /Annots.
191
+ this._acroform = new AcroFormCollector();
192
+ // Overflow policy for unbreakable content taller than a page region (default "error"); the renderer
193
+ // seeds it into the layout-context root so packChildren can act on it.
194
+ /**
195
+ * Code points no font in the document could draw, so they were removed rather than emitted as
196
+ * `.notdef` (which PDF/A forbids). Collected here because this object is the one thing threaded
197
+ * through BOTH passes; the caller gets them back with the bytes, so an application can say which
198
+ * character went missing instead of a server log nobody reads.
199
+ */
200
+ this.missingGlyphs = new Set();
201
+ // Whether to kern. Document-level: measuring (via `runAdvance`, which reads this off the metrics)
202
+ // and drawing (the backend emits a `TJ`) both consult it, so they can never disagree. OFF while the
203
+ // embedded-font path (kern/GPOS) is unbuilt - a standard-14-only kerning would set the 14 system
204
+ // fonts finer than a user's own font, which is worse than none. Flip to on when GPOS lands.
205
+ this.kerningEnabled = false;
179
206
  if (pageSize)
180
- this.pageFormat = page_sizes_1.pageFormats[pageSize];
207
+ this.pageFormat = pageFormats[pageSize];
181
208
  this.fillConfigWithStandardValues();
182
209
  }
183
210
  // Adds an object and returns its number
@@ -198,40 +225,108 @@ class PDFObjectManager {
198
225
  setCompress(on) {
199
226
  this.compress = on;
200
227
  }
228
+ reportMissingGlyph(codePoints) {
229
+ for (const cp of codePoints)
230
+ this.missingGlyphs.add(cp);
231
+ }
232
+ /** The dropped code points, in the order first seen. Empty for a document that hit none. */
233
+ getMissingGlyphs() {
234
+ return [...this.missingGlyphs];
235
+ }
236
+ setOverflowPolicy(policy) {
237
+ this.overflowPolicy = policy;
238
+ }
239
+ getOverflowPolicy() {
240
+ return this.overflowPolicy;
241
+ }
242
+ // A unique, binary-safe placeholder for a stream's bytes, swapped for ciphertext in finalizeEncryption().
243
+ encToken(id) {
244
+ return `JENC:${id}`;
245
+ }
246
+ // The same trick for a literal STRING. A separate marker because the two are swapped differently: a
247
+ // stream keeps its delimiters and only the payload changes, a string is replaced whole by a hex one.
248
+ strToken(id) {
249
+ return `\x00\x02JSTR:${id}\x02\x00`;
250
+ }
251
+ /**
252
+ * The single choke-point every literal string in an OBJECT goes through - a field's `/T` and `/V`, a
253
+ * bookmark `/Title`, a link `/URI`, an `/Alt` text, a destination name.
254
+ *
255
+ * Without a security handler this is the escaped literal we have always written, so output stays
256
+ * byte-identical. With one, the raw bytes are registered for the finalize pass and a placeholder takes
257
+ * their place: ISO 32000-1 7.6.2 requires ALL strings and streams to be encrypted, and the ones missed
258
+ * here were exactly the values a password is supposed to protect (ISSUE-7).
259
+ *
260
+ * A string INSIDE a content stream needs none of this - the stream is enciphered as a whole.
261
+ */
262
+ pdfString(text) {
263
+ if (!this.security) {
264
+ return `(${text.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)")})`;
265
+ }
266
+ // Encrypt the bytes the unencrypted path would have written, so a viewer decrypts to the same value.
267
+ // No escaping is applied: the result is emitted as a HEX string, which has no escapes to get wrong.
268
+ const bytes = new Uint8Array(getArrayBuffer(text));
269
+ const id = this.strJobs.push(bytes) - 1;
270
+ return this.strToken(id);
271
+ }
272
+ // The single encryption choke-point every stream emitter routes through (stream(), registerImage). Without
273
+ // a security handler: the bytes as a latin1 string + their length. With one: register the raw bytes for the
274
+ // finalize pass and return a placeholder + the pre-computable encrypted length (16-byte IV + PKCS#7 padding).
275
+ streamPayload(bytes) {
276
+ if (!this.security)
277
+ return { body: latin1FromBytes(bytes), length: bytes.length };
278
+ const id = this.encJobs.push(bytes) - 1;
279
+ return { body: this.encToken(id), length: 16 + (Math.floor(bytes.length / 16) + 1) * 16 };
280
+ }
201
281
  // Builds a stream object body: `<< extraDict /Length n >> stream … endstream`, FlateDecode-compressed
202
- // when enabled AND it actually helps (tiny streams aren't inflated). Binary bytes ride as a latin1
203
- // string - the final encoder passes 0x00-0xFF through unchanged.
282
+ // when enabled AND it actually helps (tiny streams aren't inflated). The payload rides through
283
+ // streamPayload, so it is encrypted uniformly when a security handler is set.
204
284
  stream(extraDict, data) {
205
285
  const head = extraDict ? extraDict + " " : "";
286
+ let body = data;
287
+ let filter = "";
206
288
  if (this.compress) {
207
- const z = (0, zlib_1.deflateSync)(data);
289
+ const z = zlibSync(data);
208
290
  if (z.length < data.length) {
209
- return `<< ${head}/Filter /FlateDecode /Length ${z.length} >>\nstream\n${z.toString("latin1")}\nendstream`;
291
+ body = z;
292
+ filter = "/Filter /FlateDecode ";
210
293
  }
211
294
  }
212
- return `<< ${head}/Length ${data.length} >>\nstream\n${data.toString("latin1")}\nendstream`;
295
+ const p = this.streamPayload(body);
296
+ return `<< ${head}${filter}/Length ${p.length} >>\nstream\n${p.body}\nendstream`;
213
297
  }
214
298
  // Adds a page content stream (compressed when enabled). The caller passes the raw operator string.
299
+ // Bytes go through the Windows-1252 encoder (NOT a latin1 low-byte cast): a Tj literal may carry a
300
+ // CP1252 char like "…"/"—"/"€" whose codepoint is > 0xFF, and latin1 would mangle it (… -> "&").
301
+ // For every char <= 0xFF the encoder passes the byte through, so existing streams stay byte-identical.
215
302
  addContentStream(content) {
216
- return this.addObject(this.stream("", Buffer.from(content, "latin1")));
303
+ return this.addObject(this.stream("", new Uint8Array(getArrayBuffer(content))));
304
+ }
305
+ // A Form XObject: a self-contained content stream with its own /BBox (+ optional /Resources). Used for
306
+ // form-field appearance streams (/AP). Routes through the same choke-point as page content, so
307
+ // compression and encryption apply. Returns the object number.
308
+ addFormXObject(bbox, content, resources) {
309
+ const dict = `/Type /XObject /Subtype /Form /BBox ${bbox}` +
310
+ (resources ? ` /Resources << ${resources} >>` : "");
311
+ return this.addObject(this.stream(dict, new Uint8Array(getArrayBuffer(content))));
217
312
  }
218
313
  changePDFConfig(config) {
219
- this.pdfConfig = Object.assign(Object.assign({}, this.pdfConfig), config);
314
+ this.pdfConfig = { ...this.pdfConfig, ...config };
220
315
  }
221
316
  getPDFConfig() {
222
317
  return this.pdfConfig;
223
318
  }
224
319
  fillConfigWithStandardValues() {
225
320
  this.pdfConfig = {
226
- orientation: pdf_config_1.Orientation.portrait,
321
+ orientation: Orientation.portrait,
227
322
  defaultFont: {
228
323
  fontFamily: "Helvetica",
229
324
  fontSize: 12,
230
325
  fontStyle: FontStyle.Normal,
231
326
  },
232
- pageSize: page_sizes_1.PageSize.A4,
327
+ pageSize: PageSize.A4,
233
328
  margin: { left: 72, top: 72, bottom: 72, right: 72 },
234
- colorMode: pdf_config_1.ColorMode.color,
329
+ colorMode: ColorMode.color,
235
330
  };
236
331
  }
237
332
  // Calculates the total length of the document in bytes (for XRef)
@@ -240,10 +335,8 @@ class PDFObjectManager {
240
335
  for (let i = 0; i < this.objects.length; i++) {
241
336
  const obj = this.objects[i];
242
337
  const objectContent = `${i + 1} 0 obj\n${obj}\nendobj\n`;
243
- // Convert the object content into a buffer with the correct encoding
244
- const encodedContent = Buffer.from(objectContent, "binary");
245
- // Add the actual byte length to the total length
246
- length += encodedContent.length;
338
+ // The body is a latin1/binary string (every char <= 0xFF), so its length IS its byte length.
339
+ length += objectContent.length;
247
340
  }
248
341
  return length;
249
342
  }
@@ -256,7 +349,25 @@ class PDFObjectManager {
256
349
  return this.parentObjectNumber;
257
350
  }
258
351
  // Registers an image
259
- registerImage(width, height, imageType, imageData) {
352
+ registerImage(width, height, imageType, imageData, smaskData) {
353
+ // A transparent PNG carries its alpha as a separate DeviceGray /SMask image the viewer composites with.
354
+ let smaskEntry = "";
355
+ if (smaskData) {
356
+ const s = this.streamPayload(bytesFromLatin1(smaskData));
357
+ const smaskObject = `<< /Type /XObject
358
+ /Subtype /Image
359
+ /Width ${width}
360
+ /Height ${height}
361
+ /ColorSpace /DeviceGray
362
+ /BitsPerComponent 8
363
+ /Filter /FlateDecode
364
+ /Length ${s.length} >>
365
+ stream
366
+ ${s.body}
367
+ endstream`;
368
+ smaskEntry = ` /SMask ${this.addObject(smaskObject)} 0 R\n`;
369
+ }
370
+ const img = this.streamPayload(bytesFromLatin1(imageData));
260
371
  const imageObject = `<< /Type /XObject
261
372
  /Subtype /Image
262
373
  /Width ${width}
@@ -264,9 +375,9 @@ class PDFObjectManager {
264
375
  /ColorSpace /DeviceRGB
265
376
  /BitsPerComponent 8
266
377
  /Filter /${imageType}
267
- /Length ${imageData.length} >>
378
+ ${smaskEntry} /Length ${img.length} >>
268
379
  stream
269
- ${imageData}
380
+ ${img.body}
270
381
  endstream`;
271
382
  // Add the image and its object number to the image manager - return the object number
272
383
  const imageObjectNumber = this.addObject(imageObject);
@@ -278,13 +389,11 @@ endstream`;
278
389
  // `relationship` is the /AFRelationship (e.g. "Data" for the ZUGFeRD factur-x.xml). Binary bytes
279
390
  // ride as a latin1 string, like images/fonts (the final encoder passes 0x00-0xFF through).
280
391
  attachFile(name, data, opts = {}) {
281
- var _a, _b;
282
- const subtype = ((_a = opts.mimeType) !== null && _a !== void 0 ? _a : "application/octet-stream").replace(/\//g, "#2F");
392
+ const subtype = (opts.mimeType ?? "application/octet-stream").replace(/\//g, "#2F");
283
393
  const embedded = this.addObject(this.stream(`/Type /EmbeddedFile /Subtype /${subtype} /Params << /Size ${data.length} >>`, data));
284
- const escaped = (s) => s.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
285
- const desc = opts.description ? ` /Desc (${escaped(opts.description)})` : "";
286
- const filespec = this.addObject(`<< /Type /Filespec /F (${escaped(name)}) /UF (${escaped(name)}) ` +
287
- `/AFRelationship /${(_b = opts.relationship) !== null && _b !== void 0 ? _b : "Unspecified"}${desc} ` +
394
+ const desc = opts.description ? ` /Desc ${this.pdfString(opts.description)}` : "";
395
+ const filespec = this.addObject(`<< /Type /Filespec /F ${this.pdfString(name)} /UF ${this.pdfString(name)} ` +
396
+ `/AFRelationship /${opts.relationship ?? "Unspecified"}${desc} ` +
288
397
  `/EF << /F ${embedded} 0 R /UF ${embedded} 0 R >> >>`);
289
398
  this.attachments.push({ name, filespec });
290
399
  }
@@ -302,11 +411,10 @@ endstream`;
302
411
  // Embeds an ICC profile and a PDF/A /OutputIntent that points at it (catalog /OutputIntents).
303
412
  // `icc` are the raw profile bytes (an RGB profile, /N 3 - e.g. sRGB).
304
413
  setOutputIntent(icc, opts = {}) {
305
- var _a, _b;
306
414
  const profile = this.addObject(this.stream("/N 3", icc));
307
415
  this.outputIntent = this.addObject(`<< /Type /OutputIntent /S /GTS_PDFA1 ` +
308
- `/OutputConditionIdentifier (${(_a = opts.identifier) !== null && _a !== void 0 ? _a : "sRGB"}) ` +
309
- `/Info (${(_b = opts.info) !== null && _b !== void 0 ? _b : "sRGB IEC61966-2.1"}) /DestOutputProfile ${profile} 0 R >>`);
416
+ `/OutputConditionIdentifier ${this.pdfString(opts.identifier ?? "sRGB")} ` +
417
+ `/Info ${this.pdfString(opts.info ?? "sRGB IEC61966-2.1")} /DestOutputProfile ${profile} 0 R >>`);
310
418
  }
311
419
  getOutputIntent() {
312
420
  return this.outputIntent;
@@ -331,6 +439,43 @@ endstream`;
331
439
  enableDocumentId() {
332
440
  this.documentId = true;
333
441
  }
442
+ // Turn on encryption: the handler encrypts every stream and supplies the /Encrypt dict. Forces a trailer
443
+ // /ID (encrypted PDFs require one) and PDF 2.0 (AES-256 R6). PDF/A forbids encryption - guarded at finalize.
444
+ setSecurityHandler(handler) {
445
+ this.security = handler;
446
+ this.documentId = true;
447
+ this.setPdfVersion("2.0");
448
+ }
449
+ // One async pass at the very end: refuse to encrypt a PDF/A document, then encrypt every registered stream
450
+ // (swap each placeholder for its ciphertext) and add the /Encrypt object the trailer references.
451
+ async finalizeEncryption() {
452
+ if (!this.security)
453
+ return;
454
+ if (this.outputIntent !== undefined || this.attachments.length > 0) {
455
+ throw new Error("@jasy/pdf: cannot encrypt a PDF/A document - PDF/A forbids encryption.");
456
+ }
457
+ for (let id = 0; id < this.encJobs.length; id++) {
458
+ const ciphertext = latin1FromBytes(await this.security.encrypt(this.encJobs[id]));
459
+ const token = this.encToken(id);
460
+ const idx = this.objects.findIndex((o) => o.includes(token));
461
+ // Function replacement: a string replacement would interpret `$&`/`$\``/... in the random ciphertext.
462
+ if (idx >= 0)
463
+ this.objects[idx] = this.objects[idx].replace(token, () => ciphertext);
464
+ }
465
+ // Strings: the same idea, but a string is replaced WHOLE - `(text)` becomes `<ciphertext>`. Hex,
466
+ // because the ciphertext is binary and a hex string has no escape rules to get wrong.
467
+ for (let id = 0; id < this.strJobs.length; id++) {
468
+ const cipher = await this.security.encrypt(this.strJobs[id]);
469
+ const hex = Array.from(cipher, (b) => b.toString(16).padStart(2, "0").toUpperCase()).join("");
470
+ const token = this.strToken(id);
471
+ const idx = this.objects.findIndex((o) => o.includes(token));
472
+ if (idx >= 0)
473
+ this.objects[idx] = this.objects[idx].replace(token, () => `<${hex}>`);
474
+ }
475
+ // Added LAST on purpose: the /Encrypt dictionary's own strings must stay in the clear (7.6.2), and
476
+ // they would otherwise be swept up by the pass above.
477
+ this.encryptObjNum = this.addObject(`<< ${this.security.encryptDict()} >>`);
478
+ }
334
479
  // Registers (or reuses) a transparency graphics state and returns its resource name
335
480
  // (e.g. "GS1"). `fillAlpha` -> /ca, `strokeAlpha` -> /CA. Deduped by the alpha pair.
336
481
  registerExtGState(fillAlpha, strokeAlpha) {
@@ -349,25 +494,73 @@ endstream`;
349
494
  getAllExtGStatesRaw() {
350
495
  return this.extGStates.getAll();
351
496
  }
497
+ // Registers a gradient as a PDF shading (+ its color-stop function) and returns the resource name
498
+ // (e.g. "Sh1"). Used to fill a color-glyph layer with a COLR v1 gradient. Not deduped: each layer
499
+ // carries page-absolute coordinates, so two calls rarely match.
500
+ registerShading(g) {
501
+ const fn = this.buildShadingFunction(g.stops);
502
+ const n = (x) => x.toFixed(3);
503
+ const coords = g.type === "linear"
504
+ ? `${n(g.x0)} ${n(g.y0)} ${n(g.x1)} ${n(g.y1)}`
505
+ : `${n(g.x0)} ${n(g.y0)} ${n(g.r0)} ${n(g.x1)} ${n(g.y1)} ${n(g.r1)}`;
506
+ const shadingType = g.type === "linear" ? 2 : 3;
507
+ // PDF shadings extend by clamping the end colors; "repeat"/"reflect" would need a tiling pattern,
508
+ // so we approximate them as clamp (pad) for now - correct for the common pad case.
509
+ const objectNumber = this.addObject(`<< /ShadingType ${shadingType} /ColorSpace /DeviceRGB /Coords [${coords}] ` +
510
+ `/Function ${fn} 0 R /Extend [true true] >>`);
511
+ const name = `Sh${this.shadings.size + 1}`;
512
+ this.shadings.set(name, objectNumber);
513
+ return name;
514
+ }
515
+ // Builds the PDF function mapping t in [0,1] to a color along the stops. Two stops collapse to one
516
+ // exponential (linear) function; more stops stitch one linear piece per interval (FunctionType 3).
517
+ buildShadingFunction(stops) {
518
+ const c = (i) => stops[i].color.toPDFColorString(); // "r g b" in 0..1
519
+ // Malformed / empty color line: fall back to opaque black instead of indexing an empty array.
520
+ if (stops.length === 0) {
521
+ return this.addObject("<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0] /C1 [0 0 0] /N 1 >>");
522
+ }
523
+ if (stops.length <= 2) {
524
+ const c0 = c(0);
525
+ const c1 = c(stops.length - 1);
526
+ return this.addObject(`<< /FunctionType 2 /Domain [0 1] /C0 [${c0}] /C1 [${c1}] /N 1 >>`);
527
+ }
528
+ const pieces = [];
529
+ for (let i = 0; i < stops.length - 1; i++) {
530
+ pieces.push(this.addObject(`<< /FunctionType 2 /Domain [0 1] /C0 [${c(i)}] /C1 [${c(i + 1)}] /N 1 >>`));
531
+ }
532
+ const functions = pieces.map((p) => `${p} 0 R`).join(" ");
533
+ const bounds = stops
534
+ .slice(1, -1)
535
+ .map((s) => s.offset.toFixed(4))
536
+ .join(" ");
537
+ const encode = pieces.map(() => "0 1").join(" ");
538
+ return this.addObject(`<< /FunctionType 3 /Domain [0 1] /Functions [${functions}] /Bounds [${bounds}] /Encode [${encode}] >>`);
539
+ }
540
+ // Returns all registered gradient shadings (name -> object number) for the page /Resources.
541
+ getAllShadingsRaw() {
542
+ return this.shadings;
543
+ }
352
544
  // Registers a font
353
545
  registerFont(fontName, fontStyle = FontStyle.Normal, fullName = fontName) {
354
546
  if (this.fonts.hasFont(fontName, fontStyle)) {
355
547
  return this.fonts.getFont(fontName, fontStyle); // Already exists? Return it!
356
548
  }
357
- const afmFilePath = path.resolve(__dirname, "../", `assets/${fullName}.afm`);
358
- if (fs.existsSync(afmFilePath)) {
359
- const data = fs.readFileSync(afmFilePath, "utf-8");
549
+ this.verticalsCache.delete(fontName);
550
+ this.decorationCache.delete(fontName);
551
+ const data = STANDARD_AFM[fullName];
552
+ if (data !== undefined) {
360
553
  this.afmParsers.push({
361
554
  fontName,
362
555
  fontStyle,
363
556
  fullFontName: fullName,
364
- parser: new afm_parser_1.AFMParser(data),
557
+ parser: new AFMParser(data),
365
558
  });
366
559
  }
367
560
  const resourceNumber = this.objects.length + 1; // New resource number
368
561
  const fontNumber = this.fonts.getLastFontIndex() + 1; // New font index number
369
562
  this.fonts.addFont(fontName, fontNumber, resourceNumber, fontStyle); // Store it
370
- const fontObject = `<</BaseFont/${fullName}/Type/Font\n/Encoding/WinAnsiEncoding\n/Subtype/Type1>>`;
563
+ const fontObject = `<</BaseFont/${pdfName(fullName)}/Type/Font\n/Encoding/WinAnsiEncoding\n/Subtype/Type1>>`;
371
564
  this.addObject(fontObject);
372
565
  return {
373
566
  fontIndex: fontNumber,
@@ -380,10 +573,19 @@ endstream`;
380
573
  // metrics and emits its PDF font objects. All variants share the family `name`; bold/italic are
381
574
  // separate .ttf files registered under the same name with a different style.
382
575
  registerCustomFont(name, data, style = FontStyle.Normal) {
383
- const key = this.customKey(name, style);
384
- if (this.customFonts.has(key))
576
+ let byStyle = this.customFonts.get(name);
577
+ if (!byStyle) {
578
+ byStyle = new Map();
579
+ this.customFonts.set(name, byStyle);
580
+ }
581
+ if (byStyle.has(style))
385
582
  return;
386
- this.customFonts.set(key, new ttf_parser_1.TTFParser(data));
583
+ // A WOFF is the same sfnt behind a small wrapper, so it is unpacked HERE - the one place every
584
+ // font path meets (a file, raw bytes, a URL, a browser upload) - and TTFParser never learns a
585
+ // second container format.
586
+ byStyle.set(style, new TTFParser(isWoff(data) ? woffToSfnt(data) : data));
587
+ this.verticalsCache.delete(name); // this family now answers from the .ttf, not from an AFM
588
+ this.decorationCache.delete(name);
387
589
  // Emission (the PDF font objects + page resource) is DEFERRED to first use via ensureEmitted(),
388
590
  // so a registered-but-never-rendered face - e.g. a bundled family the document doesn't touch -
389
591
  // costs nothing in the output. The metrics above are still available for layout immediately.
@@ -406,6 +608,7 @@ endstream`;
406
608
  this.fonts.addFont(name, this.fonts.getLastFontIndex() + 1, type0, style, name, true);
407
609
  this.customFontEmit.set(key, {
408
610
  pdfName: name + STYLE_SUFFIX[style],
611
+ ttf: this.customFonts.get(name).get(style), // `style` is already resolved, so this exists
409
612
  fontFile,
410
613
  descriptor,
411
614
  cidFont,
@@ -418,20 +621,18 @@ endstream`;
418
621
  // tagged "ABCDEF+" as PDF/A requires for subsets. Call once, after the render pass, before output.
419
622
  finalizeCustomFonts() {
420
623
  for (const [key, e] of this.customFontEmit) {
421
- const ttf = this.customFonts.get(key);
624
+ const ttf = e.ttf;
422
625
  const base = `${this.subsetTag(e.pdfName, e.used)}+${e.pdfName}`;
423
626
  this.replaceObject(e.fontFile, this.buildFontFile2(ttf, e.used));
424
627
  this.replaceObject(e.descriptor, this.buildFontDescriptor(base, ttf, e.fontFile));
425
628
  this.replaceObject(e.cidFont, this.buildCIDFont(base, ttf, e.descriptor, e.used));
426
- this.replaceObject(e.toUnicode, this.buildToUnicode(ttf, e.used));
629
+ this.replaceObject(e.toUnicode, this.buildToUnicode(ttf, e.used, this.shapedOrigins.get(key)));
427
630
  this.replaceObject(e.type0, this.buildType0(base, e.cidFont, e.toUnicode));
428
631
  }
429
632
  }
430
633
  // A deterministic 6-uppercase-letter subset tag (PDF/A wants the "TAG+FontName" form).
431
634
  subsetTag(pdfName, used) {
432
- const h = (0, crypto_1.createHash)("md5")
433
- .update(pdfName + [...used].sort((a, b) => a - b).join(","))
434
- .digest();
635
+ const h = md5(new TextEncoder().encode(pdfName + [...used].sort((a, b) => a - b).join(",")));
435
636
  let tag = "";
436
637
  for (let i = 0; i < 6; i++)
437
638
  tag += String.fromCharCode(65 + (h[i] % 26));
@@ -443,22 +644,91 @@ endstream`;
443
644
  // The variant to actually use for (name, style): the requested style if registered, else the
444
645
  // family's Normal as a clean fallback (e.g. bold chosen but no bold file), else undefined when
445
646
  // `name` is not a custom family at all.
647
+ //
648
+ // This runs once per CHARACTER (getCharWidth -> getCustomFont), and every paragraph is measured
649
+ // several times (layout, pagination, drawing), so it is the hottest function in the whole renderer.
650
+ // The size check is what keeps a standard-14 document from paying for a feature it never uses: with
651
+ // no custom font registered, neither lookup could ever hit, so there is nothing to look up.
446
652
  resolveCustomStyle(name, style = FontStyle.Normal) {
447
- if (!name)
653
+ if (this.customFonts.size === 0 || !name)
654
+ return undefined;
655
+ const byStyle = this.customFonts.get(name);
656
+ if (!byStyle)
448
657
  return undefined;
449
- if (this.customFonts.has(this.customKey(name, style)))
658
+ if (byStyle.has(style))
450
659
  return style;
451
- if (this.customFonts.has(this.customKey(name, FontStyle.Normal)))
452
- return FontStyle.Normal;
453
- return undefined;
660
+ return byStyle.has(FontStyle.Normal) ? FontStyle.Normal : undefined;
454
661
  }
662
+ // The same lookup as above, but returning the font itself in ONE map walk instead of resolving the
663
+ // style and then looking the font up again under a freshly built key.
455
664
  getCustomFont(name, style = FontStyle.Normal) {
456
- const resolved = this.resolveCustomStyle(name, style);
457
- return resolved ? this.customFonts.get(this.customKey(name, resolved)) : undefined;
665
+ if (this.customFonts.size === 0 || !name)
666
+ return undefined;
667
+ const byStyle = this.customFonts.get(name);
668
+ if (!byStyle)
669
+ return undefined;
670
+ return byStyle.get(style) ?? byStyle.get(FontStyle.Normal);
458
671
  }
459
672
  isCustomFont(name, style = FontStyle.Normal) {
460
673
  return !!this.resolveCustomStyle(name, style);
461
674
  }
675
+ // The parsed font for a color-capable custom family (COLR/CPAL), for drawing emoji as vector
676
+ // layers. Returns undefined for standard-14 fonts, unknown names, or fonts with no color glyphs -
677
+ // the caller then draws normal monochrome text.
678
+ getColorFont(name, style = FontStyle.Normal) {
679
+ const ttf = this.getCustomFont(name, style);
680
+ return ttf?.hasColorGlyphs() ? ttf : undefined;
681
+ }
682
+ // The document's color-emoji fallback font: a code point the current text font can't color-render
683
+ // is drawn from this font's color glyphs instead (so `Text("Hallo 😅")` works in one string). Set
684
+ // via `Document({ emoji })`; must name a registered color font.
685
+ setEmojiFont(name) {
686
+ this.emojiFontName = name;
687
+ }
688
+ getEmojiFont() {
689
+ return this.emojiFontName;
690
+ }
691
+ // Alternative color-emoji source: raster images (a CDN like Twemoji, react-pdf style). The renderer
692
+ // fetches `${url}${hexCodePoint}.${format}` per emoji and embeds it. The font source is preferred
693
+ // (native vector); this exists for parity + as an escape hatch.
694
+ setEmojiImageSource(url, format) {
695
+ this.emojiImage = { url, format };
696
+ }
697
+ getEmojiImageSource() {
698
+ return this.emojiImage;
699
+ }
700
+ // Whether `ttf` has a COLOR glyph for a code point (i.e. it would render it as color emoji).
701
+ /**
702
+ * Whether a family can draw this code point at all. An embedded face answers from its `cmap`; a
703
+ * standard-14 one can only draw what Windows-1252 encodes, which is what its `/Widths` array covers.
704
+ * Used to pick a family out of a fallback stack, per code point.
705
+ */
706
+ hasGlyph(codePoint, fontFamily, fontStyle = FontStyle.Normal) {
707
+ const ttf = this.getCustomFont(fontFamily, fontStyle);
708
+ if (ttf)
709
+ return ttf.getGlyphIndex(codePoint) !== 0;
710
+ return isWindows1252(codePoint);
711
+ }
712
+ /**
713
+ * Whether the document draws this code point through the colour-emoji path instead of the text
714
+ * font - a fallback emoji FONT, or an image source (`Document({ emoji })`). Such a code point is
715
+ * missing from the text font ON PURPOSE, so glyph coverage must leave it alone. Mirrors exactly
716
+ * what `getCharWidth` measures it as, or a dropped emoji would have been measured and not drawn.
717
+ */
718
+ rendersAsEmoji(codePoint, fontFamily, fontStyle) {
719
+ const ttf = this.getCustomFont(fontFamily, fontStyle);
720
+ if (ttf && this.colorRenders(ttf, codePoint))
721
+ return true;
722
+ if (this.emojiFontName && this.emojiFontName !== fontFamily) {
723
+ const emoji = this.getColorFont(this.emojiFontName, fontStyle);
724
+ if (emoji && this.colorRenders(emoji, codePoint))
725
+ return true;
726
+ }
727
+ return this.emojiImage !== undefined && isEmojiCodePoint(codePoint);
728
+ }
729
+ colorRenders(ttf, codePoint) {
730
+ return ttf.getColorGlyph(ttf.getGlyphIndex(codePoint)) !== null;
731
+ }
462
732
  // The page font resource for the resolved variant - same resolution as getCustomFont, so the
463
733
  // selected Type0 object and the emitted glyph ids always come from the SAME font file.
464
734
  getCustomFontResource(name, style = FontStyle.Normal) {
@@ -468,6 +738,53 @@ endstream`;
468
738
  this.ensureEmitted(name, resolved);
469
739
  return this.fonts.getFont(name, resolved);
470
740
  }
741
+ /** How many glyphs a run draws - its code-point count unless a ligature merged some. `letterSpacing`
742
+ * is per glyph (`Tc`), so this is what it multiplies. */
743
+ shapedGlyphCount(text, fontFamily, fontStyle) {
744
+ return this.shapeText(text, fontFamily, fontStyle)?.length;
745
+ }
746
+ /**
747
+ * The shaped glyphs of a run, or undefined when nothing needed shaping. The ONE place shaping is
748
+ * decided, so measuring and drawing cannot disagree. Memoised: a paragraph is measured several
749
+ * times (layout, pagination, drawing).
750
+ */
751
+ shapeText(text, fontFamily, fontStyle = FontStyle.Normal) {
752
+ const resolved = this.resolveCustomStyle(fontFamily, fontStyle);
753
+ if (!resolved)
754
+ return undefined;
755
+ const key = `${this.customKey(fontFamily, resolved)}\u0000${text}`;
756
+ const cached = this.shapeCache.get(key);
757
+ if (cached !== undefined)
758
+ return cached ?? undefined;
759
+ const ttf = this.customFonts.get(fontFamily).get(resolved);
760
+ const shaped = shapeRun([...text].map((c) => c.codePointAt(0)), ttf) ?? null;
761
+ this.shapeCache.set(key, shaped);
762
+ if (shaped) {
763
+ // Recorded at shaping time - the only moment the glyph and its code points are known together.
764
+ const fontKey = this.customKey(fontFamily, resolved);
765
+ let origins = this.shapedOrigins.get(fontKey);
766
+ if (!origins)
767
+ this.shapedOrigins.set(fontKey, (origins = new Map()));
768
+ for (const g of shaped)
769
+ origins.set(g.glyph, g.codePoints);
770
+ }
771
+ return shaped ?? undefined;
772
+ }
773
+ /** Hex Identity-H for glyphs the producer already resolved (a shaped run), which cannot be given as
774
+ * text. Recording each keeps it in the subset. */
775
+ registerGlyphs(name, glyphs, style = FontStyle.Normal) {
776
+ const resolved = this.resolveCustomStyle(name, style);
777
+ if (!resolved)
778
+ return "";
779
+ this.ensureEmitted(name, resolved);
780
+ const emit = this.customFontEmit.get(this.customKey(name, resolved));
781
+ let out = "";
782
+ for (const g of glyphs) {
783
+ emit.used.add(g);
784
+ out += g.toString(16).padStart(4, "0").toUpperCase();
785
+ }
786
+ return out;
787
+ }
471
788
  // Encodes text as a hex Identity-H string for an embedded font's Tj operator: each codepoint
472
789
  // becomes its 2-byte glyph id (CID == GID under /CIDToGIDMap /Identity).
473
790
  encodeCustomText(name, text, style = FontStyle.Normal) {
@@ -475,27 +792,38 @@ endstream`;
475
792
  if (!resolved)
476
793
  return "";
477
794
  this.ensureEmitted(name, resolved);
478
- const key = this.customKey(name, resolved);
479
- const ttf = this.customFonts.get(key);
480
- const used = this.customFontEmit.get(key).used;
795
+ // Once per text run, not per character: the flat key is fine here.
796
+ const emit = this.customFontEmit.get(this.customKey(name, resolved));
797
+ const { ttf, used } = emit;
798
+ const hex4 = (gid) => gid.toString(16).padStart(4, "0").toUpperCase();
799
+ // The SAME shaped run the measuring path used, so the glyphs drawn are the glyphs measured.
800
+ const shaped = this.shapeText(text, name, style);
801
+ if (shaped) {
802
+ let out = "";
803
+ for (const g of shaped) {
804
+ used.add(g.glyph);
805
+ out += hex4(g.glyph);
806
+ }
807
+ return out;
808
+ }
481
809
  let hex = "";
482
810
  for (const ch of text) {
483
811
  const gid = ttf.getGlyphIndex(ch.codePointAt(0));
484
812
  used.add(gid); // record the glyph so the subset keeps it
485
- hex += gid.toString(16).padStart(4, "0").toUpperCase();
813
+ hex += hex4(gid);
486
814
  }
487
815
  return hex;
488
816
  }
489
817
  // The SUBSET font program (only the used glyphs' outlines). Binary bytes survive as a latin1
490
818
  // string - the final Windows-1252 encoder passes 0x00-0xFF through unchanged (see getArrayBuffer).
491
819
  buildFontFile2(ttf, used) {
492
- const bytes = (0, ttf_subsetter_1.subsetTTF)(ttf.getData(), used);
820
+ const bytes = subsetTTF(ttf.getData(), used);
493
821
  // /Length1 is the UNCOMPRESSED font-program length (required even when FlateDecode'd).
494
822
  return this.stream(`/Length1 ${bytes.length}`, bytes);
495
823
  }
496
824
  buildFontDescriptor(name, ttf, fontFile) {
497
825
  const [x0, y0, x1, y1] = ttf.bbox;
498
- return (`<< /Type /FontDescriptor /FontName /${name} /Flags 4 ` +
826
+ return (`<< /Type /FontDescriptor /FontName /${pdfName(name)} /Flags 4 ` +
499
827
  `/FontBBox [${x0} ${y0} ${x1} ${y1}] /ItalicAngle 0 ` +
500
828
  `/Ascent ${ttf.ascent} /Descent ${ttf.descent} /CapHeight ${ttf.ascent} /StemV 80 ` +
501
829
  `/FontFile2 ${fontFile} 0 R >>`);
@@ -506,26 +834,33 @@ endstream`;
506
834
  const all = ttf.glyphWidths();
507
835
  const w = [...used]
508
836
  .sort((a, b) => a - b)
509
- .map((g) => { var _a; return `${g} [${(_a = all[g]) !== null && _a !== void 0 ? _a : 1000}]`; })
837
+ .map((g) => `${g} [${all[g] ?? 1000}]`)
510
838
  .join(" ");
511
- return (`<< /Type /Font /Subtype /CIDFontType2 /BaseFont /${name} ` +
512
- `/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> ` +
839
+ return (`<< /Type /Font /Subtype /CIDFontType2 /BaseFont /${pdfName(name)} ` +
840
+ `/CIDSystemInfo << /Registry ${this.pdfString("Adobe")} /Ordering ${this.pdfString("Identity")} /Supplement 0 >> ` +
513
841
  `/FontDescriptor ${descriptor} 0 R /CIDToGIDMap /Identity ` +
514
842
  `/DW 1000 /W [${w}] >>`);
515
843
  }
516
844
  // Maps glyph id -> Unicode so the text stays copy-/searchable (rendering doesn't need it). Only
517
845
  // the used glyphs are listed, to match the subset.
518
- buildToUnicode(ttf, used) {
846
+ buildToUnicode(ttf, used, shapedOrigins = new Map()) {
519
847
  const hex4 = (n) => n.toString(16).padStart(4, "0").toUpperCase();
848
+ // A code point above the BMP needs its surrogate PAIR here - a `bfchar` destination is UTF-16BE.
849
+ const utf16 = (code) => code > 0xffff
850
+ ? hex4(0xd800 + ((code - 0x10000) >> 10)) + hex4(0xdc00 + ((code - 0x10000) & 0x3ff))
851
+ : hex4(code);
520
852
  const rev = ttf.reverseCmap();
853
+ // A SHAPED glyph wins: it is often absent from the cmap (a ligature) or maps back to a
854
+ // presentation form rather than the letter that was written (a joining form), so the reverse
855
+ // cmap alone would make copied text wrong.
521
856
  const entries = [...used]
522
857
  .sort((a, b) => a - b)
523
- .filter((g) => rev.has(g))
524
- .map((g) => [g, rev.get(g)]);
858
+ .filter((g) => shapedOrigins.has(g) || rev.has(g))
859
+ .map((g) => [g, shapedOrigins.get(g) ?? [rev.get(g)]]);
525
860
  const blocks = [];
526
861
  for (let i = 0; i < entries.length; i += 100) {
527
862
  const block = entries.slice(i, i + 100);
528
- const lines = block.map(([gid, code]) => `<${hex4(gid)}> <${hex4(code)}>`);
863
+ const lines = block.map(([gid, codes]) => `<${hex4(gid)}> <${codes.map(utf16).join("")}>`);
529
864
  blocks.push(`${block.length} beginbfchar\n${lines.join("\n")}\nendbfchar`);
530
865
  }
531
866
  const body = `/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n` +
@@ -533,34 +868,159 @@ endstream`;
533
868
  `/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n` +
534
869
  `1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n${blocks.join("\n")}\n` +
535
870
  `endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend`;
536
- return this.stream("", Buffer.from(body, "latin1"));
871
+ return this.stream("", bytesFromLatin1(body));
537
872
  }
538
873
  buildType0(name, cidFont, toUnicode) {
539
- return (`<< /Type /Font /Subtype /Type0 /BaseFont /${name} /Encoding /Identity-H ` +
874
+ return (`<< /Type /Font /Subtype /Type0 /BaseFont /${pdfName(name)} /Encoding /Identity-H ` +
540
875
  `/DescendantFonts [${cidFont} 0 R] /ToUnicode ${toUnicode} 0 R >>`);
541
876
  }
877
+ // The vertical metrics of a face, in em fractions - what seats the baseline and sizes the line
878
+ // box (see text/line-metrics.ts). An embedded font answers from its own `hhea`; a standard-14
879
+ // font from its AFM header. Nothing here is guessed: both numbers come out of the font data.
880
+ //
881
+ // Asked once per LINE (not per character), but a long document has a lot of lines and the
882
+ // standard-14 lookup below is a linear scan, so the answer is memoised per face. Registering a
883
+ // font clears the cache - a name can go from standard-14 to embedded.
884
+ getFontVerticals(fontFamily, fontStyle) {
885
+ let byStyle = this.verticalsCache.get(fontFamily);
886
+ const hit = byStyle?.get(fontStyle);
887
+ if (hit)
888
+ return hit;
889
+ const ttf = this.getCustomFont(fontFamily, fontStyle);
890
+ const verticals = ttf
891
+ ? {
892
+ ascent: ttf.ascent / 1000,
893
+ descent: -ttf.descent / 1000, // hhea writes the descent negative
894
+ lineGap: ttf.lineGap / 1000,
895
+ }
896
+ : this.getAVMParserByFont(undefined, fontFamily, fontStyle).parser.verticals();
897
+ if (!byStyle) {
898
+ byStyle = new Map();
899
+ this.verticalsCache.set(fontFamily, byStyle);
900
+ }
901
+ byStyle.set(fontStyle, verticals);
902
+ return verticals;
903
+ }
904
+ // Underline / strikethrough geometry of a face, in em fractions. Same shape as getFontVerticals:
905
+ // an embedded font answers from its `post`/`OS/2` tables, a standard-14 one from its AFM header,
906
+ // and the answer is memoised per face because it is asked once per decorated run.
907
+ getFontDecoration(fontFamily, fontStyle) {
908
+ let byStyle = this.decorationCache.get(fontFamily);
909
+ const hit = byStyle?.get(fontStyle);
910
+ if (hit)
911
+ return hit;
912
+ const ttf = this.getCustomFont(fontFamily, fontStyle);
913
+ const decoration = ttf
914
+ ? ttf.decoration()
915
+ : this.getAVMParserByFont(undefined, fontFamily, fontStyle).parser.decoration();
916
+ if (!byStyle) {
917
+ byStyle = new Map();
918
+ this.decorationCache.set(fontFamily, byStyle);
919
+ }
920
+ byStyle.set(fontStyle, decoration);
921
+ return decoration;
922
+ }
923
+ setKerning(enabled) {
924
+ this.kerningEnabled = enabled;
925
+ }
926
+ /**
927
+ * The kerning adjustment for each adjacent code-point pair of `text`, in em/1000 (the sign the
928
+ * font declares: negative tightens). Length is `codePointCount - 1`. Zero at any pair next to a
929
+ * space - letters are never kerned against a space, which also keeps a per-word measure equal to a
930
+ * whole-line one (the space pairs contribute nothing either way).
931
+ *
932
+ * Standard-14 answers from the AFM `KPX` pairs. An embedded font has no kerning yet (its `kern` /
933
+ * `GPOS` tables are unparsed), so it returns zeros - never a wrong value, just no adjustment.
934
+ */
935
+ getKernPairs(text, fontFamily, fontStyle) {
936
+ const chars = [...text];
937
+ if (chars.length < 2)
938
+ return [];
939
+ // A shaped run does not kern here: these pairs are keyed by the UNSHAPED glyphs, and the `TJ`
940
+ // path would re-shape each chunk on its own, turning every letter isolated. Arabic kerning is in
941
+ // GPOS, applied during shaping - not built (`todo.md`).
942
+ if (this.shapeText(text, fontFamily, fontStyle))
943
+ return [];
944
+ const out = [];
945
+ // An embedded font kerns by GLYPH id (kern table / GPOS); a standard-14 one by character (AFM).
946
+ const ttf = this.getCustomFont(fontFamily, fontStyle);
947
+ const parser = ttf
948
+ ? undefined
949
+ : this.getAVMParserByFont(undefined, fontFamily, fontStyle).parser;
950
+ for (let i = 0; i < chars.length - 1; i++) {
951
+ const a = chars[i];
952
+ const b = chars[i + 1];
953
+ if (a === " " || b === " ") {
954
+ out.push(0);
955
+ }
956
+ else if (ttf) {
957
+ out.push(ttf.getKerning(ttf.getGlyphIndex(a.codePointAt(0)), ttf.getGlyphIndex(b.codePointAt(0))));
958
+ }
959
+ else {
960
+ out.push(parser.getKerning(a, b));
961
+ }
962
+ }
963
+ return out;
964
+ }
965
+ /**
966
+ * Where the glyphs of `text` put ink inside a horizontal band, as x-intervals in POINTS measured
967
+ * from the start of the run. This is what an underline steps around ("skip-ink").
968
+ *
969
+ * `bandTop` / `bandBottom` are in points relative to the baseline, y UP (so both negative under
970
+ * it). Only an EMBEDDED font can answer: the standard-14 outlines live in the viewer, not here.
971
+ * Callers must check `isCustomFont` first rather than treat an empty result as "no descenders".
972
+ */
973
+ getInkSpansInBand(text, fontSize, fontFamily, fontStyle, bandTop, bandBottom, letterSpacing = 0) {
974
+ const ttf = this.getCustomFont(fontFamily, fontStyle);
975
+ if (!ttf)
976
+ return [];
977
+ const scale = fontSize / ttf.unitsPerEm;
978
+ const spans = [];
979
+ let pen = 0;
980
+ for (const ch of text) {
981
+ const codePoint = ch.codePointAt(0);
982
+ if (codePoint !== undefined) {
983
+ for (const [x0, x1] of ttf.inkSpansInBand(codePoint, bandTop / scale, bandBottom / scale)) {
984
+ spans.push([pen + x0 * scale, pen + x1 * scale]);
985
+ }
986
+ }
987
+ // The pen advances by the glyph AND its letter-spacing, so the ink of the next glyph lands
988
+ // where `Tc` actually draws it - otherwise the skipped gaps drift under the wrong letters.
989
+ pen += this.getCharWidth(ch, fontSize, undefined, fontFamily, fontStyle) + letterSpacing;
990
+ }
991
+ return mergeSpans(spans);
992
+ }
542
993
  // Returns the current width of a text, included kernings
543
994
  getStringWidth(text, fontFamily, fontSize, fontStyle) {
995
+ // Shaping first: a joined run is narrower than the same letters apart, so measuring the unshaped
996
+ // form while drawing the shaped one would break every line. Both sides ask `shapeText`.
997
+ const shaped = this.shapeText(text, fontFamily, fontStyle);
998
+ if (shaped) {
999
+ const ttf = this.getCustomFont(fontFamily, fontStyle);
1000
+ return shaped.reduce((w, g) => w + ttf.unitsToPoints(g.advance, fontSize), 0);
1001
+ }
544
1002
  let width = 0;
545
- // We must calculate each sign
546
- for (let i = 0; i < text.length; i++) {
547
- const char = text[i];
548
- const nextChar = text[i + 1] || null;
549
- // Get signs width
550
- const charWidth = this.getCharWidth(char, fontSize, undefined, fontFamily, fontStyle);
551
- width += charWidth;
552
- // If a next sign available calculate the kerning
553
- if (nextChar) {
554
- const kerning = this.getKerning(char, nextChar, undefined, fontFamily, fontStyle);
555
- width += kerning * fontSize; // Kerning must be scaled with the font size
556
- }
1003
+ // Iterate code points, not UTF-16 units: an astral char (emoji, CJK-ext) is a surrogate pair,
1004
+ // and indexing by unit would measure each half as its own (zero-width) "char". The spread splits
1005
+ // on code points; for BMP text this is one unit each, so the result is unchanged.
1006
+ //
1007
+ // NO KERNING. We write a run as a single `Tj`, and a viewer advances that by the font's plain
1008
+ // widths - PDF never kerns on its own; a producer has to say so with a `TJ` array. Folding the
1009
+ // AFM's kerning into the MEASUREMENT while the OUTPUT ignores it made every kerned string draw
1010
+ // wider than its box: "AVATAR Wave" at 40pt by 19pt, "Total" at 11pt by 5.7%. Measured must equal
1011
+ // drawn. The AFM kerning pairs stay parsed (`getKerning`) for the day we emit `TJ` - see the
1012
+ // "real kerning" item in todo.md, which must cover embedded fonts too (`kern`/`GPOS`).
1013
+ for (const char of text) {
1014
+ width += this.getCharWidth(char, fontSize, undefined, fontFamily, fontStyle);
557
1015
  }
558
1016
  return width;
559
1017
  }
560
- getCharCode(char) {
561
- return char.charCodeAt(0).toString();
562
- }
563
1018
  getAVMParserByFont(fullFontName, fontName, fontStyle) {
1019
+ // A non-string family means font bytes were passed where a name belongs - hint at the fix instead of
1020
+ // stringifying the byte blob into an unreadable "parser not found".
1021
+ for (const name of [fullFontName, fontName])
1022
+ if (name != null && typeof name !== "string")
1023
+ throw new Error(`Font family must be a string name, got ${typeof name}. Register font bytes via the document \`fonts\` map (or addFont) and reference them by name.`);
564
1024
  if (!fullFontName && (!fontName || !fontStyle)) {
565
1025
  throw new Error("No font family is given. Please set a full font name or a font with font style");
566
1026
  }
@@ -577,26 +1037,40 @@ endstream`;
577
1037
  }
578
1038
  // Methode zur Berechnung der Zeichenbreite anhand der Schriftgröße
579
1039
  getCharWidth(char, fontSize, fullFontName, fontName, fontStyle) {
580
- const ttf = this.getCustomFont(fullFontName !== null && fullFontName !== void 0 ? fullFontName : fontName, fontStyle);
1040
+ const name = fullFontName ?? fontName;
1041
+ const ttf = this.getCustomFont(name, fontStyle);
1042
+ // Color-emoji fallback: a code point the current font can't color-render is measured with the
1043
+ // emoji font's advance (matching the renderer, which draws it from that font). Guarded on
1044
+ // emojiFontName so a normal document's measuring stays byte-identical.
1045
+ if (this.emojiFontName && this.emojiFontName !== name) {
1046
+ const cp = char.codePointAt(0);
1047
+ if (cp !== undefined && !(ttf && this.colorRenders(ttf, cp))) {
1048
+ const emoji = this.getColorFont(this.emojiFontName, fontStyle);
1049
+ if (emoji && this.colorRenders(emoji, cp))
1050
+ return emoji.getCharWidth(char, fontSize);
1051
+ }
1052
+ }
1053
+ // Image emoji source: an emoji code point is drawn as a 1em-square image, so it advances one em.
1054
+ if (this.emojiImage) {
1055
+ const cp = char.codePointAt(0);
1056
+ if (cp !== undefined && isEmojiCodePoint(cp) && !(ttf && this.colorRenders(ttf, cp))) {
1057
+ return fontSize;
1058
+ }
1059
+ }
581
1060
  if (ttf)
582
1061
  return ttf.getCharWidth(char, fontSize);
583
1062
  const currentParser = this.getAVMParserByFont(fullFontName, fontName, fontStyle);
584
- const advanceWidth = currentParser.parser.getAdvanceWidth(char);
585
- // Normally we got still zero. TODO: Return a alternative width like the "space"
1063
+ let advanceWidth = currentParser.parser.getAdvanceWidth(char);
1064
+ // A character a standard font cannot represent (not in Windows-1252) is drawn as "?" by the
1065
+ // encoder; measure it as "?" too so the width matches the glyph - graceful instead of a crash.
586
1066
  if (!advanceWidth) {
587
- throw new Error(`Kein Metrik-Eintrag für Zeichen: ${char} ${this.getCharCode(char)}`);
1067
+ advanceWidth =
1068
+ currentParser.parser.getAdvanceWidth("?") || currentParser.parser.getAdvanceWidth(" ");
588
1069
  }
589
1070
  // Width of the character multiplied by the font size (scaled proportionally)
590
1071
  return (advanceWidth / 1000) * fontSize;
591
1072
  }
592
1073
  // Method to get the kerning, if available, between two signs
593
- getKerning(char, nextChar, fullFontName, fontName, fontStyle) {
594
- // TrueType kerning (the kern/GPOS tables) is not wired up yet - no kerning for custom fonts.
595
- if (this.getCustomFont(fullFontName !== null && fullFontName !== void 0 ? fullFontName : fontName, fontStyle))
596
- return 0;
597
- const currentParser = this.getAVMParserByFont(fullFontName, fontName, fontStyle);
598
- return currentParser.parser.getKerning(char, nextChar) / 1000;
599
- }
600
1074
  // Returns all fonts
601
1075
  getAllFontsRaw() {
602
1076
  return this.fonts.getAllFonts();
@@ -632,14 +1106,14 @@ endstream`;
632
1106
  const root = this.objects.findIndex((f) => f.toLowerCase().includes("catalog")) + 1;
633
1107
  // A fresh document uses the same hash for both /ID strings; they only diverge on an update.
634
1108
  const id = this.documentId ? ` /ID [${this.contentId()} ${this.contentId()}]` : "";
635
- return `trailer\n<< /Size ${objectCount + 1} /Root ${root} 0 R${id} >>\nstartxref\n${startxref}\n%%EOF`;
1109
+ const enc = this.encryptObjNum ? ` /Encrypt ${this.encryptObjNum} 0 R` : "";
1110
+ return `trailer\n<< /Size ${objectCount + 1} /Root ${root} 0 R${id}${enc} >>\nstartxref\n${startxref}\n%%EOF`;
636
1111
  }
637
1112
  contentId() {
638
- return `<${(0, crypto_1.createHash)("md5").update(this.objects.join("")).digest("hex").toUpperCase()}>`;
1113
+ return `<${md5Hex(new TextEncoder().encode(this.objects.join(""))).toUpperCase()}>`;
639
1114
  }
640
1115
  // Returns the number of objects
641
1116
  getObjectCount() {
642
1117
  return this.objects.length;
643
1118
  }
644
1119
  }
645
- exports.PDFObjectManager = PDFObjectManager;