@jasy/pdf 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +171 -0
  2. package/dist/api/args.d.ts +10 -0
  3. package/dist/api/args.js +13 -0
  4. package/dist/api/color.d.ts +24 -0
  5. package/dist/api/color.js +215 -0
  6. package/dist/api/content.d.ts +36 -0
  7. package/dist/api/content.js +50 -0
  8. package/dist/api/descriptor.d.ts +25 -0
  9. package/dist/api/descriptor.js +71 -0
  10. package/dist/api/index.d.ts +8 -0
  11. package/dist/api/index.js +27 -0
  12. package/dist/api/insets.d.ts +16 -0
  13. package/dist/api/insets.js +21 -0
  14. package/dist/api/layout.d.ts +72 -0
  15. package/dist/api/layout.js +99 -0
  16. package/dist/api/structure.d.ts +80 -0
  17. package/dist/api/structure.js +125 -0
  18. package/dist/api/table.d.ts +37 -0
  19. package/dist/api/table.js +87 -0
  20. package/dist/api/text.d.ts +28 -0
  21. package/dist/api/text.js +61 -0
  22. package/dist/assets/Courier-Bold.afm +342 -0
  23. package/dist/assets/Courier-BoldOblique.afm +342 -0
  24. package/dist/assets/Courier-Oblique.afm +342 -0
  25. package/dist/assets/Courier.afm +342 -0
  26. package/dist/assets/Helvetica-Bold.afm +2827 -0
  27. package/dist/assets/Helvetica-BoldOblique.afm +2827 -0
  28. package/dist/assets/Helvetica-Oblique.afm +3051 -0
  29. package/dist/assets/Helvetica.afm +3051 -0
  30. package/dist/assets/Symbol.afm +213 -0
  31. package/dist/assets/Times-Bold.afm +2588 -0
  32. package/dist/assets/Times-BoldItalic.afm +2384 -0
  33. package/dist/assets/Times-Italic.afm +2667 -0
  34. package/dist/assets/Times-Roman.afm +2419 -0
  35. package/dist/assets/ZapfDingbats.afm +225 -0
  36. package/dist/assets/agl.txt +695 -0
  37. package/dist/common/color.d.ts +37 -0
  38. package/dist/common/color.js +62 -0
  39. package/dist/constants/page-sizes.d.ts +44 -0
  40. package/dist/constants/page-sizes.js +92 -0
  41. package/dist/constants/pdf-parts.d.ts +5 -0
  42. package/dist/constants/pdf-parts.js +8 -0
  43. package/dist/elements/container-element.d.ts +35 -0
  44. package/dist/elements/container-element.js +91 -0
  45. package/dist/elements/image-element.d.ts +56 -0
  46. package/dist/elements/image-element.js +151 -0
  47. package/dist/elements/index.d.ts +10 -0
  48. package/dist/elements/index.js +28 -0
  49. package/dist/elements/layout/deferred-element.d.ts +19 -0
  50. package/dist/elements/layout/deferred-element.js +33 -0
  51. package/dist/elements/layout/expanded-element.d.ts +30 -0
  52. package/dist/elements/layout/expanded-element.js +68 -0
  53. package/dist/elements/layout/padding-element.d.ts +29 -0
  54. package/dist/elements/layout/padding-element.js +76 -0
  55. package/dist/elements/layout/repeating-header-element.d.ts +29 -0
  56. package/dist/elements/layout/repeating-header-element.js +60 -0
  57. package/dist/elements/layout/sized-container-element.d.ts +14 -0
  58. package/dist/elements/layout/sized-container-element.js +37 -0
  59. package/dist/elements/line-element.d.ts +31 -0
  60. package/dist/elements/line-element.js +44 -0
  61. package/dist/elements/page-element.d.ts +58 -0
  62. package/dist/elements/page-element.js +90 -0
  63. package/dist/elements/pdf-document-element.d.ts +13 -0
  64. package/dist/elements/pdf-document-element.js +22 -0
  65. package/dist/elements/pdf-element.d.ts +67 -0
  66. package/dist/elements/pdf-element.js +55 -0
  67. package/dist/elements/rectangle-element.d.ts +55 -0
  68. package/dist/elements/rectangle-element.js +120 -0
  69. package/dist/elements/row-element.d.ts +42 -0
  70. package/dist/elements/row-element.js +54 -0
  71. package/dist/elements/text-element.d.ts +59 -0
  72. package/dist/elements/text-element.js +137 -0
  73. package/dist/index.d.ts +3 -0
  74. package/dist/index.js +21 -0
  75. package/dist/ir/display-list.d.ts +74 -0
  76. package/dist/ir/display-list.js +2 -0
  77. package/dist/layout/box-constraints.d.ts +56 -0
  78. package/dist/layout/box-constraints.js +73 -0
  79. package/dist/layout/fragmentation.d.ts +42 -0
  80. package/dist/layout/fragmentation.js +61 -0
  81. package/dist/renderer/container-renderer.d.ts +6 -0
  82. package/dist/renderer/container-renderer.js +30 -0
  83. package/dist/renderer/deferred-renderer.d.ts +6 -0
  84. package/dist/renderer/deferred-renderer.js +25 -0
  85. package/dist/renderer/expanded-renderer.d.ts +6 -0
  86. package/dist/renderer/expanded-renderer.js +23 -0
  87. package/dist/renderer/image-renderer.d.ts +6 -0
  88. package/dist/renderer/image-renderer.js +85 -0
  89. package/dist/renderer/index.d.ts +10 -0
  90. package/dist/renderer/index.js +26 -0
  91. package/dist/renderer/line-renderer.d.ts +6 -0
  92. package/dist/renderer/line-renderer.js +30 -0
  93. package/dist/renderer/padding-renderer.d.ts +6 -0
  94. package/dist/renderer/padding-renderer.js +23 -0
  95. package/dist/renderer/page-renderer.d.ts +5 -0
  96. package/dist/renderer/page-renderer.js +81 -0
  97. package/dist/renderer/pdf-backend.d.ts +45 -0
  98. package/dist/renderer/pdf-backend.js +184 -0
  99. package/dist/renderer/pdf-config.d.ts +8 -0
  100. package/dist/renderer/pdf-config.js +17 -0
  101. package/dist/renderer/pdf-document-class.d.ts +42 -0
  102. package/dist/renderer/pdf-document-class.js +118 -0
  103. package/dist/renderer/pdf-document-renderer.d.ts +20 -0
  104. package/dist/renderer/pdf-document-renderer.js +104 -0
  105. package/dist/renderer/pdf-renderer.d.ts +5 -0
  106. package/dist/renderer/pdf-renderer.js +92 -0
  107. package/dist/renderer/rectangle-renderer.d.ts +8 -0
  108. package/dist/renderer/rectangle-renderer.js +61 -0
  109. package/dist/renderer/repeating-header-renderer.d.ts +6 -0
  110. package/dist/renderer/repeating-header-renderer.js +28 -0
  111. package/dist/renderer/row-renderer.d.ts +6 -0
  112. package/dist/renderer/row-renderer.js +30 -0
  113. package/dist/renderer/text-renderer.d.ts +9 -0
  114. package/dist/renderer/text-renderer.js +125 -0
  115. package/dist/text/line-breaker.d.ts +40 -0
  116. package/dist/text/line-breaker.js +106 -0
  117. package/dist/utils/afm-parser.d.ts +12 -0
  118. package/dist/utils/afm-parser.js +91 -0
  119. package/dist/utils/flex-layout.d.ts +53 -0
  120. package/dist/utils/flex-layout.js +119 -0
  121. package/dist/utils/font-metrics.d.ts +12 -0
  122. package/dist/utils/font-metrics.js +2 -0
  123. package/dist/utils/font-path.d.ts +1 -0
  124. package/dist/utils/font-path.js +19 -0
  125. package/dist/utils/image-helper.d.ts +43 -0
  126. package/dist/utils/image-helper.js +206 -0
  127. package/dist/utils/pdf-object-manager.d.ts +97 -0
  128. package/dist/utils/pdf-object-manager.js +645 -0
  129. package/dist/utils/renderer-registry.d.ts +6 -0
  130. package/dist/utils/renderer-registry.js +19 -0
  131. package/dist/utils/ttf-parser.d.ts +29 -0
  132. package/dist/utils/ttf-parser.js +191 -0
  133. package/dist/utils/ttf-subsetter.d.ts +1 -0
  134. package/dist/utils/ttf-subsetter.js +161 -0
  135. package/dist/utils/utf8-to-windows1252-encoder.d.ts +2 -0
  136. package/dist/utils/utf8-to-windows1252-encoder.js +55 -0
  137. package/dist/validators/element-validator.d.ts +8 -0
  138. package/dist/validators/element-validator.js +61 -0
  139. package/package.json +50 -0
@@ -0,0 +1,645 @@
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");
45
+ // Enums come from the leaf config module (never in a cycle); the config type is
46
+ // erased at runtime so it can come from the cyclic module safely.
47
+ const pdf_config_1 = require("../renderer/pdf-config");
48
+ var FontStyle;
49
+ (function (FontStyle) {
50
+ FontStyle["Normal"] = "normal";
51
+ FontStyle["Bold"] = "bold";
52
+ FontStyle["Italic"] = "italic";
53
+ FontStyle["BoldItalic"] = "boldItalic";
54
+ })(FontStyle || (exports.FontStyle = FontStyle = {}));
55
+ // Suffix appended to an embedded font's PDF /BaseFont so each style variant of a family keeps
56
+ // a distinct name (the variants are otherwise selected by the font resource, not the name).
57
+ const STYLE_SUFFIX = {
58
+ [FontStyle.Normal]: "",
59
+ [FontStyle.Bold]: "-Bold",
60
+ [FontStyle.Italic]: "-Italic",
61
+ [FontStyle.BoldItalic]: "-BoldItalic",
62
+ };
63
+ class ImageManager {
64
+ constructor() {
65
+ this.images = new Map();
66
+ }
67
+ addImage(resourceNumber) {
68
+ this.images.set("IM" + resourceNumber.toString(), resourceNumber);
69
+ }
70
+ getAllImages() {
71
+ return this.images;
72
+ }
73
+ }
74
+ /** Graphics-state objects for transparency, deduped by their (fill, stroke) alpha pair. */
75
+ class ExtGStateManager {
76
+ constructor() {
77
+ // alpha-pair key -> { resource name, indirect-object number }
78
+ this.states = new Map();
79
+ }
80
+ get(key) {
81
+ return this.states.get(key);
82
+ }
83
+ add(key, name, objectNumber) {
84
+ this.states.set(key, { name, objectNumber });
85
+ }
86
+ size() {
87
+ return this.states.size;
88
+ }
89
+ /** name -> object number, for the page `/Resources /ExtGState` dict. */
90
+ getAll() {
91
+ const out = new Map();
92
+ this.states.forEach((value) => out.set(value.name, value.objectNumber));
93
+ return out;
94
+ }
95
+ }
96
+ class FontManager {
97
+ constructor() {
98
+ this.fonts = new Map();
99
+ }
100
+ addFont(fontName, fontIndex, resourceIndex, fontStyle = FontStyle.Normal, fullName = fontName, force = false) {
101
+ const fontKey = this._createFontKey(fontName, fontStyle);
102
+ // `force` lets an embedded custom font override a same-named standard-14 entry (e.g. embedding
103
+ // "Helvetica" as Liberation for PDF/A) - otherwise the standard /Type1 resource would win and
104
+ // the CID text would be drawn against the wrong font.
105
+ if (force || !this.fonts.has(fontKey)) {
106
+ this.fonts.set(fontKey, {
107
+ fontIndex,
108
+ resourceIndex,
109
+ fontStyle,
110
+ fullName,
111
+ });
112
+ }
113
+ }
114
+ hasFont(fontName, fontStyle = FontStyle.Normal) {
115
+ const fontKey = this._createFontKey(fontName, fontStyle);
116
+ return this.fonts.has(fontKey);
117
+ }
118
+ getFont(fontName, fontStyle = FontStyle.Normal) {
119
+ const fontKey = this._createFontKey(fontName, fontStyle);
120
+ return this.fonts.get(fontKey);
121
+ }
122
+ addCustomFont(fontName, fontStyle, fullName = fontName) {
123
+ const fontIndex = this.getLastFontIndex() + 1;
124
+ const resourceIndex = this.getLastResourceIndex() + 1;
125
+ this.addFont(fontName, fontIndex, resourceIndex, fontStyle, fullName);
126
+ }
127
+ getAllFonts() {
128
+ return this.fonts;
129
+ }
130
+ size() {
131
+ return this.fonts.size;
132
+ }
133
+ getLastFontIndex() {
134
+ let maxFontIndex = 0;
135
+ this.fonts.forEach((value) => {
136
+ if (value.fontIndex > maxFontIndex) {
137
+ maxFontIndex = value.fontIndex;
138
+ }
139
+ });
140
+ return maxFontIndex;
141
+ }
142
+ getLastResourceIndex() {
143
+ let maxResourceIndex = 0;
144
+ this.fonts.forEach((value) => {
145
+ if (value.resourceIndex > maxResourceIndex) {
146
+ maxResourceIndex = value.resourceIndex;
147
+ }
148
+ });
149
+ return maxResourceIndex;
150
+ }
151
+ _createFontKey(fontName, fontStyle) {
152
+ return `${fontName}-${fontStyle}`;
153
+ }
154
+ }
155
+ class PDFObjectManager {
156
+ constructor(pageSize) {
157
+ this.objects = [];
158
+ this.objectPositions = [];
159
+ this.parentObjectNumber = 0;
160
+ this.fonts = new FontManager(); // Stores the fonts
161
+ this.images = new ImageManager(); // Stores the images (object numbers and names)
162
+ this.extGStates = new ExtGStateManager(); // Transparency states
163
+ this.pageFormat = page_sizes_1.pageFormats[page_sizes_1.PageSize.A4];
164
+ this.afmParsers = [];
165
+ // Embedded TrueType fonts, keyed by the name the user registers them under. When a font
166
+ // name is in here the metric/emission paths take the TTF branch instead of the AFM one.
167
+ this.customFonts = new Map();
168
+ // Per-registered face: the reserved font-object numbers + the glyph ids actually used. The font
169
+ // program is subsetted and the objects filled in by finalizeCustomFonts(), after the render pass.
170
+ this.customFontEmit = new Map();
171
+ // Embedded file attachments (their /Filespec object numbers + display names). Referenced from
172
+ // the catalog's /Names/EmbeddedFiles + /AF. Drives ZUGFeRD's embedded factur-x.xml.
173
+ this.attachments = [];
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.
176
+ this.pdfVersion = "1.4";
177
+ this.documentId = false;
178
+ this.compress = false;
179
+ if (pageSize)
180
+ this.pageFormat = page_sizes_1.pageFormats[pageSize];
181
+ this.fillConfigWithStandardValues();
182
+ }
183
+ // Adds an object and returns its number
184
+ addObject(content) {
185
+ // The text is encoded in Windows-1252 if necessary
186
+ const objectNumber = this.objects.length + 1;
187
+ const position = this.getCurrentByteLength(); // Calculate the current byte length
188
+ this.objectPositions.push(position);
189
+ this.objects.push(content);
190
+ return objectNumber;
191
+ }
192
+ // Replaces an object at the index `objectNumber`
193
+ replaceObject(objectNumber, content) {
194
+ this.objects[objectNumber - 1] = content;
195
+ }
196
+ // FlateDecode compression for stream payloads (default off; renderPdf turns it on). Off keeps the
197
+ // PDF greppable for debugging and the internal tests; the XMP metadata stream is never routed here.
198
+ setCompress(on) {
199
+ this.compress = on;
200
+ }
201
+ // 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.
204
+ stream(extraDict, data) {
205
+ const head = extraDict ? extraDict + " " : "";
206
+ if (this.compress) {
207
+ const z = (0, zlib_1.deflateSync)(data);
208
+ if (z.length < data.length) {
209
+ return `<< ${head}/Filter /FlateDecode /Length ${z.length} >>\nstream\n${z.toString("latin1")}\nendstream`;
210
+ }
211
+ }
212
+ return `<< ${head}/Length ${data.length} >>\nstream\n${data.toString("latin1")}\nendstream`;
213
+ }
214
+ // Adds a page content stream (compressed when enabled). The caller passes the raw operator string.
215
+ addContentStream(content) {
216
+ return this.addObject(this.stream("", Buffer.from(content, "latin1")));
217
+ }
218
+ changePDFConfig(config) {
219
+ this.pdfConfig = Object.assign(Object.assign({}, this.pdfConfig), config);
220
+ }
221
+ getPDFConfig() {
222
+ return this.pdfConfig;
223
+ }
224
+ fillConfigWithStandardValues() {
225
+ this.pdfConfig = {
226
+ orientation: pdf_config_1.Orientation.portrait,
227
+ defaultFont: {
228
+ fontFamily: "Helvetica",
229
+ fontSize: 12,
230
+ fontStyle: FontStyle.Normal,
231
+ },
232
+ pageSize: page_sizes_1.PageSize.A4,
233
+ margin: { left: 72, top: 72, bottom: 72, right: 72 },
234
+ colorMode: pdf_config_1.ColorMode.color,
235
+ };
236
+ }
237
+ // Calculates the total length of the document in bytes (for XRef)
238
+ getCurrentByteLength() {
239
+ let length = this.getHeader().length; // Start with the header
240
+ for (let i = 0; i < this.objects.length; i++) {
241
+ const obj = this.objects[i];
242
+ 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;
247
+ }
248
+ return length;
249
+ }
250
+ // Sets the parent object number
251
+ setParentObjectNumber(number) {
252
+ this.parentObjectNumber = number;
253
+ }
254
+ // Returns the parent object number
255
+ getParentObjectNumber() {
256
+ return this.parentObjectNumber;
257
+ }
258
+ // Registers an image
259
+ registerImage(width, height, imageType, imageData) {
260
+ const imageObject = `<< /Type /XObject
261
+ /Subtype /Image
262
+ /Width ${width}
263
+ /Height ${height}
264
+ /ColorSpace /DeviceRGB
265
+ /BitsPerComponent 8
266
+ /Filter /${imageType}
267
+ /Length ${imageData.length} >>
268
+ stream
269
+ ${imageData}
270
+ endstream`;
271
+ // Add the image and its object number to the image manager - return the object number
272
+ const imageObjectNumber = this.addObject(imageObject);
273
+ this.images.addImage(imageObjectNumber);
274
+ return imageObjectNumber;
275
+ }
276
+ // Embeds a file as an associated file (PDF/A-3 / PDF 2.0): an /EmbeddedFile stream + a /Filespec.
277
+ // The catalog wiring (/Names/EmbeddedFiles + /AF) is added by PDFRenderer when attachments exist.
278
+ // `relationship` is the /AFRelationship (e.g. "Data" for the ZUGFeRD factur-x.xml). Binary bytes
279
+ // ride as a latin1 string, like images/fonts (the final encoder passes 0x00-0xFF through).
280
+ 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");
283
+ 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} ` +
288
+ `/EF << /F ${embedded} 0 R /UF ${embedded} 0 R >> >>`);
289
+ this.attachments.push({ name, filespec });
290
+ }
291
+ // Filespec object numbers + names, for the catalog's /Names/EmbeddedFiles and /AF.
292
+ getAttachments() {
293
+ return this.attachments;
294
+ }
295
+ // Sets the document XMP packet (catalog /Metadata). ASCII expected (see the field comment).
296
+ setXmpMetadata(xml) {
297
+ this.xmpMetadata = xml;
298
+ }
299
+ getXmpMetadata() {
300
+ return this.xmpMetadata;
301
+ }
302
+ // Embeds an ICC profile and a PDF/A /OutputIntent that points at it (catalog /OutputIntents).
303
+ // `icc` are the raw profile bytes (an RGB profile, /N 3 - e.g. sRGB).
304
+ setOutputIntent(icc, opts = {}) {
305
+ var _a, _b;
306
+ const profile = this.addObject(this.stream("/N 3", icc));
307
+ 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 >>`);
310
+ }
311
+ getOutputIntent() {
312
+ return this.outputIntent;
313
+ }
314
+ // PDF header version, e.g. "1.7" for PDF/A-3. Always "1.X" so the 9-byte header length (and the
315
+ // xref offsets that depend on it) never change.
316
+ setPdfVersion(version) {
317
+ this.pdfVersion = version;
318
+ }
319
+ getPdfVersion() {
320
+ return this.pdfVersion;
321
+ }
322
+ // The full PDF header: the version line + a binary-marker comment whose 4 bytes are all > 127
323
+ // (PDF/A clause 6.1.2). Every char is <= 0xFF, so the string length equals the emitted byte
324
+ // length - which the xref offset calculation relies on.
325
+ getHeader() {
326
+ // The marker is `%` + â ã Ï Ó (Latin-1 0xE2 0xE3 0xCF 0xD3, all > 127); each char is <= 0xFF so
327
+ // the final encoder emits it as that exact byte.
328
+ return `%PDF-${this.pdfVersion}\n%âãÏÓ\n`;
329
+ }
330
+ // Enables a trailer /ID (required by PDF/A). The id is a content hash, so it is deterministic.
331
+ enableDocumentId() {
332
+ this.documentId = true;
333
+ }
334
+ // Registers (or reuses) a transparency graphics state and returns its resource name
335
+ // (e.g. "GS1"). `fillAlpha` -> /ca, `strokeAlpha` -> /CA. Deduped by the alpha pair.
336
+ registerExtGState(fillAlpha, strokeAlpha) {
337
+ const ca = fillAlpha.toFixed(3);
338
+ const CA = strokeAlpha.toFixed(3);
339
+ const key = `${ca}:${CA}`;
340
+ const existing = this.extGStates.get(key);
341
+ if (existing)
342
+ return existing.name;
343
+ const name = `GS${this.extGStates.size() + 1}`;
344
+ const objectNumber = this.addObject(`<< /Type /ExtGState /ca ${ca} /CA ${CA} >>`);
345
+ this.extGStates.add(key, name, objectNumber);
346
+ return name;
347
+ }
348
+ // Returns all registered transparency states (name -> object number) for the page
349
+ getAllExtGStatesRaw() {
350
+ return this.extGStates.getAll();
351
+ }
352
+ // Registers a font
353
+ registerFont(fontName, fontStyle = FontStyle.Normal, fullName = fontName) {
354
+ if (this.fonts.hasFont(fontName, fontStyle)) {
355
+ return this.fonts.getFont(fontName, fontStyle); // Already exists? Return it!
356
+ }
357
+ const afmFilePath = path.resolve(__dirname, "../", `assets/${fullName}.afm`);
358
+ if (fs.existsSync(afmFilePath)) {
359
+ const data = fs.readFileSync(afmFilePath, "utf-8");
360
+ this.afmParsers.push({
361
+ fontName,
362
+ fontStyle,
363
+ fullFontName: fullName,
364
+ parser: new afm_parser_1.AFMParser(data),
365
+ });
366
+ }
367
+ const resourceNumber = this.objects.length + 1; // New resource number
368
+ const fontNumber = this.fonts.getLastFontIndex() + 1; // New font index number
369
+ this.fonts.addFont(fontName, fontNumber, resourceNumber, fontStyle); // Store it
370
+ const fontObject = `<</BaseFont/${fullName}/Type/Font\n/Encoding/WinAnsiEncoding\n/Subtype/Type1>>`;
371
+ this.addObject(fontObject);
372
+ return {
373
+ fontIndex: fontNumber,
374
+ resourceIndex: resourceNumber,
375
+ fontStyle: fontStyle,
376
+ fullName: fullName,
377
+ };
378
+ }
379
+ // Registers one variant (default Normal) of an embedded TrueType family `name`: stores it for
380
+ // metrics and emits its PDF font objects. All variants share the family `name`; bold/italic are
381
+ // separate .ttf files registered under the same name with a different style.
382
+ registerCustomFont(name, data, style = FontStyle.Normal) {
383
+ const key = this.customKey(name, style);
384
+ if (this.customFonts.has(key))
385
+ return;
386
+ this.customFonts.set(key, new ttf_parser_1.TTFParser(data));
387
+ // Emission (the PDF font objects + page resource) is DEFERRED to first use via ensureEmitted(),
388
+ // so a registered-but-never-rendered face - e.g. a bundled family the document doesn't touch -
389
+ // costs nothing in the output. The metrics above are still available for layout immediately.
390
+ }
391
+ // Reserves the font objects and registers the page resource the FIRST time a face is actually
392
+ // used. Idempotent; `style` is the already-resolved variant.
393
+ ensureEmitted(name, style) {
394
+ const key = this.customKey(name, style);
395
+ if (this.customFontEmit.has(key))
396
+ return;
397
+ // Reserve the font objects (dependency order so each reference points at an already-numbered
398
+ // object); their content - a SUBSET of the font - is filled in by finalizeCustomFonts().
399
+ const fontFile = this.addObject("<< >>");
400
+ const descriptor = this.addObject("<< >>");
401
+ const cidFont = this.addObject("<< >>");
402
+ const toUnicode = this.addObject("<< >>");
403
+ const type0 = this.addObject("<< >>");
404
+ // The Type0 dict is the resource the page references (/F{index} -> type0 object). `force` so an
405
+ // embedded font overrides a same-named standard-14 entry instead of being silently dropped.
406
+ this.fonts.addFont(name, this.fonts.getLastFontIndex() + 1, type0, style, name, true);
407
+ this.customFontEmit.set(key, {
408
+ pdfName: name + STYLE_SUFFIX[style],
409
+ fontFile,
410
+ descriptor,
411
+ cidFont,
412
+ toUnicode,
413
+ type0,
414
+ used: new Set([0]), // .notdef always present
415
+ });
416
+ }
417
+ // Fills the reserved font objects with a SUBSET of each font (only the glyphs the document used),
418
+ // tagged "ABCDEF+" as PDF/A requires for subsets. Call once, after the render pass, before output.
419
+ finalizeCustomFonts() {
420
+ for (const [key, e] of this.customFontEmit) {
421
+ const ttf = this.customFonts.get(key);
422
+ const base = `${this.subsetTag(e.pdfName, e.used)}+${e.pdfName}`;
423
+ this.replaceObject(e.fontFile, this.buildFontFile2(ttf, e.used));
424
+ this.replaceObject(e.descriptor, this.buildFontDescriptor(base, ttf, e.fontFile));
425
+ this.replaceObject(e.cidFont, this.buildCIDFont(base, ttf, e.descriptor, e.used));
426
+ this.replaceObject(e.toUnicode, this.buildToUnicode(ttf, e.used));
427
+ this.replaceObject(e.type0, this.buildType0(base, e.cidFont, e.toUnicode));
428
+ }
429
+ }
430
+ // A deterministic 6-uppercase-letter subset tag (PDF/A wants the "TAG+FontName" form).
431
+ subsetTag(pdfName, used) {
432
+ const h = (0, crypto_1.createHash)("md5")
433
+ .update(pdfName + [...used].sort((a, b) => a - b).join(","))
434
+ .digest();
435
+ let tag = "";
436
+ for (let i = 0; i < 6; i++)
437
+ tag += String.fromCharCode(65 + (h[i] % 26));
438
+ return tag;
439
+ }
440
+ customKey(name, style) {
441
+ return `${name}-${style}`;
442
+ }
443
+ // The variant to actually use for (name, style): the requested style if registered, else the
444
+ // family's Normal as a clean fallback (e.g. bold chosen but no bold file), else undefined when
445
+ // `name` is not a custom family at all.
446
+ resolveCustomStyle(name, style = FontStyle.Normal) {
447
+ if (!name)
448
+ return undefined;
449
+ if (this.customFonts.has(this.customKey(name, style)))
450
+ return style;
451
+ if (this.customFonts.has(this.customKey(name, FontStyle.Normal)))
452
+ return FontStyle.Normal;
453
+ return undefined;
454
+ }
455
+ getCustomFont(name, style = FontStyle.Normal) {
456
+ const resolved = this.resolveCustomStyle(name, style);
457
+ return resolved ? this.customFonts.get(this.customKey(name, resolved)) : undefined;
458
+ }
459
+ isCustomFont(name, style = FontStyle.Normal) {
460
+ return !!this.resolveCustomStyle(name, style);
461
+ }
462
+ // The page font resource for the resolved variant - same resolution as getCustomFont, so the
463
+ // selected Type0 object and the emitted glyph ids always come from the SAME font file.
464
+ getCustomFontResource(name, style = FontStyle.Normal) {
465
+ const resolved = this.resolveCustomStyle(name, style);
466
+ if (!resolved)
467
+ return undefined;
468
+ this.ensureEmitted(name, resolved);
469
+ return this.fonts.getFont(name, resolved);
470
+ }
471
+ // Encodes text as a hex Identity-H string for an embedded font's Tj operator: each codepoint
472
+ // becomes its 2-byte glyph id (CID == GID under /CIDToGIDMap /Identity).
473
+ encodeCustomText(name, text, style = FontStyle.Normal) {
474
+ const resolved = this.resolveCustomStyle(name, style);
475
+ if (!resolved)
476
+ return "";
477
+ 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;
481
+ let hex = "";
482
+ for (const ch of text) {
483
+ const gid = ttf.getGlyphIndex(ch.codePointAt(0));
484
+ used.add(gid); // record the glyph so the subset keeps it
485
+ hex += gid.toString(16).padStart(4, "0").toUpperCase();
486
+ }
487
+ return hex;
488
+ }
489
+ // The SUBSET font program (only the used glyphs' outlines). Binary bytes survive as a latin1
490
+ // string - the final Windows-1252 encoder passes 0x00-0xFF through unchanged (see getArrayBuffer).
491
+ buildFontFile2(ttf, used) {
492
+ const bytes = (0, ttf_subsetter_1.subsetTTF)(ttf.getData(), used);
493
+ // /Length1 is the UNCOMPRESSED font-program length (required even when FlateDecode'd).
494
+ return this.stream(`/Length1 ${bytes.length}`, bytes);
495
+ }
496
+ buildFontDescriptor(name, ttf, fontFile) {
497
+ const [x0, y0, x1, y1] = ttf.bbox;
498
+ return (`<< /Type /FontDescriptor /FontName /${name} /Flags 4 ` +
499
+ `/FontBBox [${x0} ${y0} ${x1} ${y1}] /ItalicAngle 0 ` +
500
+ `/Ascent ${ttf.ascent} /Descent ${ttf.descent} /CapHeight ${ttf.ascent} /StemV 80 ` +
501
+ `/FontFile2 ${fontFile} 0 R >>`);
502
+ }
503
+ // CIDToGIDMap /Identity means CID == GID. /W lists only the USED glyphs' widths (sparse, one
504
+ // `gid [w]` entry each); everything else falls back to /DW.
505
+ buildCIDFont(name, ttf, descriptor, used) {
506
+ const all = ttf.glyphWidths();
507
+ const w = [...used]
508
+ .sort((a, b) => a - b)
509
+ .map((g) => { var _a; return `${g} [${(_a = all[g]) !== null && _a !== void 0 ? _a : 1000}]`; })
510
+ .join(" ");
511
+ return (`<< /Type /Font /Subtype /CIDFontType2 /BaseFont /${name} ` +
512
+ `/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> ` +
513
+ `/FontDescriptor ${descriptor} 0 R /CIDToGIDMap /Identity ` +
514
+ `/DW 1000 /W [${w}] >>`);
515
+ }
516
+ // Maps glyph id -> Unicode so the text stays copy-/searchable (rendering doesn't need it). Only
517
+ // the used glyphs are listed, to match the subset.
518
+ buildToUnicode(ttf, used) {
519
+ const hex4 = (n) => n.toString(16).padStart(4, "0").toUpperCase();
520
+ const rev = ttf.reverseCmap();
521
+ const entries = [...used]
522
+ .sort((a, b) => a - b)
523
+ .filter((g) => rev.has(g))
524
+ .map((g) => [g, rev.get(g)]);
525
+ const blocks = [];
526
+ for (let i = 0; i < entries.length; i += 100) {
527
+ const block = entries.slice(i, i + 100);
528
+ const lines = block.map(([gid, code]) => `<${hex4(gid)}> <${hex4(code)}>`);
529
+ blocks.push(`${block.length} beginbfchar\n${lines.join("\n")}\nendbfchar`);
530
+ }
531
+ const body = `/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n` +
532
+ `/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n` +
533
+ `/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n` +
534
+ `1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n${blocks.join("\n")}\n` +
535
+ `endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend`;
536
+ return this.stream("", Buffer.from(body, "latin1"));
537
+ }
538
+ buildType0(name, cidFont, toUnicode) {
539
+ return (`<< /Type /Font /Subtype /Type0 /BaseFont /${name} /Encoding /Identity-H ` +
540
+ `/DescendantFonts [${cidFont} 0 R] /ToUnicode ${toUnicode} 0 R >>`);
541
+ }
542
+ // Returns the current width of a text, included kernings
543
+ getStringWidth(text, fontFamily, fontSize, fontStyle) {
544
+ 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
+ }
557
+ }
558
+ return width;
559
+ }
560
+ getCharCode(char) {
561
+ return char.charCodeAt(0).toString();
562
+ }
563
+ getAVMParserByFont(fullFontName, fontName, fontStyle) {
564
+ if (!fullFontName && (!fontName || !fontStyle)) {
565
+ throw new Error("No font family is given. Please set a full font name or a font with font style");
566
+ }
567
+ let result;
568
+ if (fullFontName) {
569
+ result = this.afmParsers.find((f) => f.fullFontName === fullFontName);
570
+ }
571
+ else {
572
+ result = this.afmParsers.find((f) => f.fontName === fontName && f.fontStyle === fontStyle);
573
+ }
574
+ if (!result)
575
+ throw new Error(`Cannot find a parser for the given font family ${fullFontName || fontName || "No given font"}`);
576
+ return result;
577
+ }
578
+ // Methode zur Berechnung der Zeichenbreite anhand der Schriftgröße
579
+ getCharWidth(char, fontSize, fullFontName, fontName, fontStyle) {
580
+ const ttf = this.getCustomFont(fullFontName !== null && fullFontName !== void 0 ? fullFontName : fontName, fontStyle);
581
+ if (ttf)
582
+ return ttf.getCharWidth(char, fontSize);
583
+ 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"
586
+ if (!advanceWidth) {
587
+ throw new Error(`Kein Metrik-Eintrag für Zeichen: ${char} ${this.getCharCode(char)}`);
588
+ }
589
+ // Width of the character multiplied by the font size (scaled proportionally)
590
+ return (advanceWidth / 1000) * fontSize;
591
+ }
592
+ // 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
+ // Returns all fonts
601
+ getAllFontsRaw() {
602
+ return this.fonts.getAllFonts();
603
+ }
604
+ // Returns all images
605
+ getAllImagesRaw() {
606
+ return this.images.getAllImages();
607
+ }
608
+ // Returns all rendered objects as a string
609
+ getRenderedObjects() {
610
+ let result = "";
611
+ this.objectPositions = [];
612
+ this.objects.forEach((content, index) => {
613
+ const position = result.length + this.getHeader().length; // Calculate positions after the header
614
+ this.objectPositions.push(position);
615
+ result += `${index + 1} 0 obj\n${content}\nendobj\n`;
616
+ });
617
+ return result;
618
+ }
619
+ // Creates the cross-reference table
620
+ getXRefTable() {
621
+ let xref = "xref\n";
622
+ xref += `0 ${this.objects.length + 1}\n`;
623
+ xref += `0000000000 65535 f \n`; // Free object
624
+ this.objectPositions.forEach((pos) => {
625
+ xref += `${pos.toString().padStart(10, "0")} 00000 n \n`;
626
+ });
627
+ return xref;
628
+ }
629
+ // Calculates the position of the XRef table and returns the trailer
630
+ getTrailerAndXRef(startxref) {
631
+ const objectCount = this.getObjectCount();
632
+ const root = this.objects.findIndex((f) => f.toLowerCase().includes("catalog")) + 1;
633
+ // A fresh document uses the same hash for both /ID strings; they only diverge on an update.
634
+ 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`;
636
+ }
637
+ contentId() {
638
+ return `<${(0, crypto_1.createHash)("md5").update(this.objects.join("")).digest("hex").toUpperCase()}>`;
639
+ }
640
+ // Returns the number of objects
641
+ getObjectCount() {
642
+ return this.objects.length;
643
+ }
644
+ }
645
+ exports.PDFObjectManager = PDFObjectManager;
@@ -0,0 +1,6 @@
1
+ export declare class RendererRegistry {
2
+ private static renderers;
3
+ static register(elementClass: Function, renderer: Function): void;
4
+ static getRenderer(element: object): Function | undefined;
5
+ static isRendererAsync(renderer: Function): boolean;
6
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RendererRegistry = void 0;
4
+ class RendererRegistry {
5
+ static register(elementClass, renderer) {
6
+ if (!RendererRegistry.renderers.has(elementClass)) {
7
+ RendererRegistry.renderers.set(elementClass, renderer);
8
+ }
9
+ }
10
+ // Keyed on the element's constructor, so it only needs an object - not `any`.
11
+ static getRenderer(element) {
12
+ return RendererRegistry.renderers.get(element.constructor);
13
+ }
14
+ static isRendererAsync(renderer) {
15
+ return renderer.constructor.name === "AsyncFunction";
16
+ }
17
+ }
18
+ exports.RendererRegistry = RendererRegistry;
19
+ RendererRegistry.renderers = new Map();