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

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