blitz-pdf 0.1.1 → 0.1.2

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 (114) hide show
  1. package/README.md +266 -12
  2. package/THIRD_PARTY_NOTICES.md +4 -2
  3. package/dist/browser.d.ts +1 -1
  4. package/dist/browser.d.ts.map +1 -1
  5. package/dist/document-options.d.ts +36 -0
  6. package/dist/document-options.d.ts.map +1 -0
  7. package/dist/document-options.js +236 -0
  8. package/dist/document-options.js.map +1 -0
  9. package/dist/node.d.ts +1 -1
  10. package/dist/node.d.ts.map +1 -1
  11. package/dist/options.d.ts +35 -1
  12. package/dist/options.d.ts.map +1 -1
  13. package/dist/options.js +21 -14
  14. package/dist/options.js.map +1 -1
  15. package/dist/types.d.ts +71 -1
  16. package/dist/types.d.ts.map +1 -1
  17. package/dist/validation.d.ts +2 -0
  18. package/dist/validation.d.ts.map +1 -0
  19. package/dist/validation.js +7 -0
  20. package/dist/validation.js.map +1 -0
  21. package/examples/zugferd.mjs +41 -0
  22. package/licenses/RUST_DEPENDENCIES.txt +27 -0
  23. package/package.json +3 -2
  24. package/src/browser.ts +1 -1
  25. package/src/document-options.ts +218 -0
  26. package/src/node.ts +1 -1
  27. package/src/options.ts +18 -13
  28. package/src/types.ts +76 -1
  29. package/src/validation.ts +6 -0
  30. package/vendor/README.md +27 -2
  31. package/vendor/krilla/.cargo-ok +1 -0
  32. package/vendor/krilla/.cargo_vcs_info.json +6 -0
  33. package/vendor/krilla/Cargo.lock +1357 -0
  34. package/vendor/krilla/Cargo.toml +165 -0
  35. package/vendor/krilla/LICENSE_APACHE +202 -0
  36. package/vendor/krilla/LICENSE_MIT +21 -0
  37. package/vendor/krilla/README.md +156 -0
  38. package/vendor/krilla/examples/basic.rs +117 -0
  39. package/vendor/krilla/examples/empty_document.rs +25 -0
  40. package/vendor/krilla/examples/parley.rs +174 -0
  41. package/vendor/krilla/examples/simple_text.rs +118 -0
  42. package/vendor/krilla/examples/stream_builder.rs +93 -0
  43. package/vendor/krilla/icc/LICENSE +116 -0
  44. package/vendor/krilla/icc/README.md +1 -0
  45. package/vendor/krilla/icc/sGrey-v2-magic.icc +0 -0
  46. package/vendor/krilla/icc/sGrey-v4.icc +0 -0
  47. package/vendor/krilla/icc/sRGB-v2-magic.icc +0 -0
  48. package/vendor/krilla/icc/sRGB-v4.icc +0 -0
  49. package/vendor/krilla/src/chunk_container.rs +503 -0
  50. package/vendor/krilla/src/configure/PDF_A1.md +227 -0
  51. package/vendor/krilla/src/configure/PDF_A2.md +269 -0
  52. package/vendor/krilla/src/configure/PDF_A3.md +31 -0
  53. package/vendor/krilla/src/configure/PDF_A4.md +211 -0
  54. package/vendor/krilla/src/configure/PDF_UA1.md +202 -0
  55. package/vendor/krilla/src/configure/README.md +23 -0
  56. package/vendor/krilla/src/configure/mod.rs +197 -0
  57. package/vendor/krilla/src/configure/validate.rs +1244 -0
  58. package/vendor/krilla/src/configure/version.rs +134 -0
  59. package/vendor/krilla/src/content.rs +1352 -0
  60. package/vendor/krilla/src/data.rs +49 -0
  61. package/vendor/krilla/src/document.rs +156 -0
  62. package/vendor/krilla/src/error.rs +170 -0
  63. package/vendor/krilla/src/geom.rs +341 -0
  64. package/vendor/krilla/src/graphics/blend.rs +46 -0
  65. package/vendor/krilla/src/graphics/color.rs +552 -0
  66. package/vendor/krilla/src/graphics/graphic.rs +30 -0
  67. package/vendor/krilla/src/graphics/graphics_state.rs +251 -0
  68. package/vendor/krilla/src/graphics/icc.rs +218 -0
  69. package/vendor/krilla/src/graphics/image.rs +835 -0
  70. package/vendor/krilla/src/graphics/mask.rs +128 -0
  71. package/vendor/krilla/src/graphics/mod.rs +14 -0
  72. package/vendor/krilla/src/graphics/paint.rs +434 -0
  73. package/vendor/krilla/src/graphics/separation.rs +86 -0
  74. package/vendor/krilla/src/graphics/shading_function.rs +865 -0
  75. package/vendor/krilla/src/graphics/shading_pattern.rs +60 -0
  76. package/vendor/krilla/src/graphics/tiling_pattern.rs +119 -0
  77. package/vendor/krilla/src/graphics/xobject.rs +150 -0
  78. package/vendor/krilla/src/interactive/action.rs +68 -0
  79. package/vendor/krilla/src/interactive/annotation.rs +283 -0
  80. package/vendor/krilla/src/interactive/destination.rs +289 -0
  81. package/vendor/krilla/src/interactive/mod.rs +3 -0
  82. package/vendor/krilla/src/interchange/embed.rs +240 -0
  83. package/vendor/krilla/src/interchange/metadata.rs +564 -0
  84. package/vendor/krilla/src/interchange/mod.rs +4 -0
  85. package/vendor/krilla/src/interchange/outline.rs +263 -0
  86. package/vendor/krilla/src/interchange/tagging/fmt.rs +733 -0
  87. package/vendor/krilla/src/interchange/tagging/generate.toml +474 -0
  88. package/vendor/krilla/src/interchange/tagging/generated.rs +4754 -0
  89. package/vendor/krilla/src/interchange/tagging/mod.rs +1324 -0
  90. package/vendor/krilla/src/interchange/tagging/tag.rs +686 -0
  91. package/vendor/krilla/src/lib.rs +179 -0
  92. package/vendor/krilla/src/num.rs +26 -0
  93. package/vendor/krilla/src/page.rs +612 -0
  94. package/vendor/krilla/src/pdf.rs +313 -0
  95. package/vendor/krilla/src/resource.rs +436 -0
  96. package/vendor/krilla/src/serialize.rs +1101 -0
  97. package/vendor/krilla/src/stream.rs +368 -0
  98. package/vendor/krilla/src/surface.rs +664 -0
  99. package/vendor/krilla/src/text/cid.rs +488 -0
  100. package/vendor/krilla/src/text/font.rs +351 -0
  101. package/vendor/krilla/src/text/glyph/bitmap.rs +81 -0
  102. package/vendor/krilla/src/text/glyph/colr.rs +581 -0
  103. package/vendor/krilla/src/text/glyph/mod.rs +205 -0
  104. package/vendor/krilla/src/text/glyph/outline.rs +72 -0
  105. package/vendor/krilla/src/text/glyph/svg.rs +34 -0
  106. package/vendor/krilla/src/text/group.rs +463 -0
  107. package/vendor/krilla/src/text/mod.rs +232 -0
  108. package/vendor/krilla/src/text/shape.rs +102 -0
  109. package/vendor/krilla/src/text/type3.rs +514 -0
  110. package/vendor/krilla/src/util.rs +286 -0
  111. package/vendor/stylo/properties/longhands.toml +0 -1
  112. package/vendor/stylo/stylesheets/rule_parser.rs +1 -1
  113. package/wasm/blitz_pdf.js +10 -5
  114. package/wasm/blitz_pdf_bg.wasm +0 -0
@@ -0,0 +1,218 @@
1
+ import { assetBytes } from "./assets.js";
2
+ import { record } from "./validation.js";
3
+ import type { AssetData, AttachmentRelationship, PdfAttachment, PdfDate, PdfOptions, PdfXmpSchema } from "./types.js";
4
+
5
+ const MAX_ATTACHMENT_BYTES = 64 * 1024 * 1024;
6
+ const encoder = new TextEncoder();
7
+ const relationships = new Set(["Source", "Data", "Alternative", "Supplement", "Unspecified"]);
8
+ const reservedPrefixes = new Set(["x", "rdf", "dc", "pdf", "pdfaid", "pdfuaid", "pdfxid", "stRef", "stEvt", "stVer", "stJob", "stFnt", "stDim", "pdfaExtension", "pdfaSchema", "pdfaProperty", "pdfaType", "pdfaField", "xmp", "xmpRights", "xmpG", "xmpMM", "xmpBJ", "xmpTPg", "xmpDM", "xmpGImg", "xmpidq"]);
9
+ const reservedNamespaces = new Set([
10
+ "adobe:ns:meta/",
11
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
12
+ "http://purl.org/dc/elements/1.1/",
13
+ "http://www.w3.org/XML/1998/namespace",
14
+ "http://www.w3.org/2000/xmlns/",
15
+ "http://ns.adobe.com/pdf/1.3/",
16
+ "http://ns.adobe.com/xmp/Identifier/qual/1.0/",
17
+ "http://www.npes.org/pdfx/ns/id/",
18
+ "http://www.aiim.org/pdfua/ns/id/",
19
+ "http://ns.adobe.com/xap/1.0/",
20
+ "http://ns.adobe.com/xap/1.0/rights/",
21
+ "http://ns.adobe.com/xap/1.0/sType/ResourceRef#",
22
+ "http://ns.adobe.com/xap/1.0/sType/ResourceEvent#",
23
+ "http://ns.adobe.com/xap/1.0/sType/Version#",
24
+ "http://ns.adobe.com/xap/1.0/sType/Job#",
25
+ "http://ns.adobe.com/xap/1.0/g/",
26
+ "http://ns.adobe.com/xap/1.0/sType/Font#",
27
+ "http://ns.adobe.com/xap/1.0/sType/Dimensions#",
28
+ "http://ns.adobe.com/xap/1.0/mm/",
29
+ "http://ns.adobe.com/xap/1.0/bj/",
30
+ "http://ns.adobe.com/xap/1.0/t/pg/",
31
+ "http://ns.adobe.com/xap/1.0/DynamicMedia/",
32
+ "http://ns.adobe.com/xap/1.0/g/img/",
33
+ "http://www.aiim.org/pdfa/ns/id/",
34
+ "http://www.aiim.org/pdfa/ns/extension/",
35
+ "http://www.aiim.org/pdfa/ns/schema#",
36
+ "http://www.aiim.org/pdfa/ns/property#",
37
+ "http://www.aiim.org/pdfa/ns/type#",
38
+ "http://www.aiim.org/pdfa/ns/field#",
39
+ ]);
40
+
41
+ function keys(value: object, allowed: readonly string[], name: string) {
42
+ record(value, name);
43
+ for (const key of Object.keys(value)) {
44
+ if (!allowed.includes(key)) throw new TypeError(`Unknown ${name} option: ${key}`);
45
+ }
46
+ }
47
+
48
+ function text(value: unknown, name: string, maxBytes = 16 * 1024): string {
49
+ if (typeof value !== "string") throw new TypeError(`${name} must be a string`);
50
+ // XML 1.0 characters only, including rejection of unpaired UTF-16 surrogates.
51
+ if (/[^\u0009\u000a\u000d\u0020-\ud7ff\ue000-\ufffd\u{10000}-\u{10ffff}]/u.test(value)) {
52
+ throw new TypeError(`${name} contains invalid XML characters`);
53
+ }
54
+ if (value.length > maxBytes || encoder.encode(value).byteLength > maxBytes) {
55
+ throw new RangeError(`${name} exceeds the ${maxBytes} byte limit`);
56
+ }
57
+ // XML normalizes literal CR/CRLF to LF. Match that in the PDF Info dictionary.
58
+ return value.replace(/\r\n?/g, "\n");
59
+ }
60
+
61
+ function optionalText(value: unknown, name: string) {
62
+ return value === undefined ? null : text(value, name);
63
+ }
64
+
65
+ function textList(value: unknown, name: string): string[] {
66
+ if (value === undefined) return [];
67
+ if (!Array.isArray(value)) throw new TypeError(`${name} must be an array of strings`);
68
+ if (value.length > 256) throw new RangeError(`${name} must contain at most 256 entries`);
69
+ const result = Array.from(value, (entry, i) => text(entry, `${name}[${i}]`));
70
+ text(result.join(", "), name);
71
+ return result;
72
+ }
73
+
74
+ function xmlName(value: unknown, label: string): string {
75
+ const name = text(value, label, 128);
76
+ if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(name) || /^xml/i.test(name)) {
77
+ throw new TypeError(`${label} must be an ASCII XML name without a colon or xml prefix`);
78
+ }
79
+ return name;
80
+ }
81
+
82
+ function xmpSchemas(value: readonly PdfXmpSchema[] | undefined) {
83
+ if (value === undefined) return [];
84
+ if (!Array.isArray(value)) throw new TypeError("metadata.xmp must be an array of schemas");
85
+ if (value.length > 32) throw new RangeError("metadata.xmp exceeds the 32 schema limit");
86
+ const prefixes = new Set<string>();
87
+ const namespaces = new Set<string>();
88
+ let count = 0, size = 0;
89
+ return Array.from(value, (schema: PdfXmpSchema, i) => {
90
+ const label = `metadata.xmp[${i}]`;
91
+ keys(schema, ["name", "namespaceUri", "prefix", "properties"], label);
92
+ const name = text(schema.name, `${label}.name`);
93
+ if (!name.trim()) throw new TypeError(`${label}.name must not be empty`);
94
+ const prefix = xmlName(schema.prefix, `${label}.prefix`);
95
+ const namespaceUri = text(schema.namespaceUri, `${label}.namespaceUri`);
96
+ // xmp-writer writes namespace declarations as attributes. Require an absolute,
97
+ // ASCII URI without XML delimiters; callers can percent-encode these characters.
98
+ if (!/^[a-z][a-z0-9+.-]*:[\x21-\x7e]+$/i.test(namespaceUri) || /[<>"'&]/.test(namespaceUri)) {
99
+ throw new TypeError(`${label}.namespaceUri must be an absolute URI with XML delimiters percent-encoded`);
100
+ }
101
+ if (reservedPrefixes.has(prefix) || reservedNamespaces.has(namespaceUri)) {
102
+ throw new TypeError(`${label} uses a reserved XMP prefix or namespace`);
103
+ }
104
+ if (prefixes.has(prefix) || namespaces.has(namespaceUri)) throw new TypeError(`${label} has a duplicate XMP prefix or namespace`);
105
+ prefixes.add(prefix); namespaces.add(namespaceUri);
106
+ if (!Array.isArray(schema.properties) || schema.properties.length === 0) throw new TypeError(`${label}.properties must be a non-empty array`);
107
+ count += schema.properties.length;
108
+ if (count > 256) throw new RangeError("metadata.xmp exceeds the 256 property limit");
109
+ const propertyNames = new Set<string>();
110
+ size += encoder.encode(name + namespaceUri + prefix).byteLength;
111
+ const properties = Array.from(schema.properties, (property, j) => {
112
+ const key = `${label}.properties[${j}]`;
113
+ keys(property, ["name", "value", "description", "category"], key);
114
+ const name = xmlName(property.name, `${key}.name`);
115
+ if (propertyNames.has(name)) throw new TypeError(`${key} has a duplicate XMP property name`);
116
+ propertyNames.add(name);
117
+ const value = text(property.value, `${key}.value`);
118
+ const description = text(property.description, `${key}.description`);
119
+ if (!description.trim()) throw new TypeError(`${key}.description must not be empty`);
120
+ const category = property.category === undefined ? "external" : property.category;
121
+ if (category !== "internal" && category !== "external") throw new TypeError(`${key}.category must be internal or external`);
122
+ size += encoder.encode(name + value + description).byteLength;
123
+ if (size > 1024 * 1024) throw new RangeError("metadata.xmp exceeds the 1 MiB limit");
124
+ return { name, value, description, category };
125
+ });
126
+ return { name, prefix, namespaceUri, properties };
127
+ });
128
+ }
129
+
130
+ function date(value: PdfDate | undefined, name: string, fallback: number): number {
131
+ if (value === undefined) return fallback;
132
+ let result: number;
133
+ if (value instanceof Date) result = value.getTime();
134
+ else if (typeof value === "string") {
135
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?(Z|[+-]\d{2}:\d{2})$/.exec(value);
136
+ if (!match) throw new TypeError(`${name} must be a Date or ISO timestamp with a timezone`);
137
+ const [, y, mo, d, h, mi, s, zone] = match;
138
+ const year = Number(y), month = Number(mo), day = Number(d);
139
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
140
+ const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
141
+ if (month < 1 || month > 12 || day < 1 || day > days[month - 1]! ||
142
+ Number(h) > 23 || Number(mi) > 59 || Number(s) > 59 ||
143
+ (zone !== "Z" && (Number(zone!.slice(1, 3)) > 23 || Number(zone!.slice(4)) > 59))) {
144
+ throw new RangeError(`${name} is not a valid calendar date`);
145
+ }
146
+ result = Date.parse(value);
147
+ } else throw new TypeError(`${name} must be a Date or ISO timestamp with a timezone`);
148
+ if (!Number.isFinite(result) || result < -62135596800000 || result > 253402300799999) {
149
+ throw new RangeError(`${name} must be a valid date in years 0001 through 9999`);
150
+ }
151
+ return Math.floor(result / 1000) * 1000;
152
+ }
153
+
154
+ export function normalizeDocumentOptions(options: PdfOptions) {
155
+ const legacyTitle = optionalText(options.title, "title");
156
+ const input = options.metadata === undefined ? {} : options.metadata;
157
+ keys(input, ["title", "author", "subject", "keywords", "creator", "producer", "language", "creationDate", "modificationDate", "documentId", "xmp"], "metadata");
158
+ const creationDate = date(input.creationDate, "metadata.creationDate", Math.floor(Date.now() / 1000) * 1000);
159
+ const modificationDate = date(input.modificationDate, "metadata.modificationDate", creationDate);
160
+ if (modificationDate < creationDate) throw new RangeError("metadata.modificationDate must not precede creationDate");
161
+ const language = optionalText(input.language, "metadata.language");
162
+ if (language !== null && !/^(?:[a-z]{2,8}|x)(?:-[a-z0-9]{1,8})*$/i.test(language)) {
163
+ throw new TypeError("metadata.language must be a language tag such as de-DE");
164
+ }
165
+ const metadata = {
166
+ title: input.title === undefined ? legacyTitle : text(input.title, "metadata.title"),
167
+ authors: textList(typeof input.author === "string" ? [input.author] : input.author, "metadata.author"),
168
+ subject: optionalText(input.subject, "metadata.subject"),
169
+ keywords: textList(input.keywords, "metadata.keywords"),
170
+ creator: optionalText(input.creator, "metadata.creator"),
171
+ producer: optionalText(input.producer, "metadata.producer"),
172
+ language, creationDate, modificationDate,
173
+ documentId: optionalText(input.documentId, "metadata.documentId"),
174
+ xmp: xmpSchemas(input.xmp),
175
+ };
176
+
177
+ if (options.attachments !== undefined && !Array.isArray(options.attachments)) {
178
+ throw new TypeError("attachments must be an array");
179
+ }
180
+ if ((options.attachments?.length ?? 0) > 100) {
181
+ throw new RangeError("Attachments exceed the 100 file limit");
182
+ }
183
+ let totalBytes = 0;
184
+ const names = new Set<string>();
185
+ function attachment(file: PdfAttachment, label: string) {
186
+ keys(file, ["name", "data", "mimeType", "description", "relationship", "modificationDate", "compress"], label);
187
+ const name = text(file.name, `${label}.name`, 255);
188
+ if (!name.trim() || name === "." || name === ".." || /[/\\:\u0000-\u001f\u007f-\u009f]/u.test(name)) {
189
+ throw new TypeError(`${label}.name must be a filename without directories or control characters`);
190
+ }
191
+ const normalizedName = name.normalize("NFC");
192
+ if (names.has(normalizedName)) throw new TypeError(`Duplicate attachment name: ${name}`);
193
+ names.add(normalizedName);
194
+ const mimeType = file.mimeType === undefined ? "application/octet-stream" : file.mimeType;
195
+ if (typeof mimeType !== "string" || mimeType.length > 255 || !/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i.test(mimeType)) {
196
+ throw new TypeError(`${label}.mimeType must be a MIME type without parameters`);
197
+ }
198
+ const relationship = file.relationship === undefined ? "Unspecified" : file.relationship;
199
+ if (!relationships.has(relationship)) throw new TypeError(`${label}.relationship is invalid`);
200
+ if (file.compress !== undefined && typeof file.compress !== "boolean") throw new TypeError(`${label}.compress must be a boolean`);
201
+ const data = file.data;
202
+ // Check binary sizes before allocating the defensive copy.
203
+ const size = typeof data === "string" ? data.length : data instanceof Uint8Array || data instanceof ArrayBuffer ? data.byteLength : 0;
204
+ if (totalBytes + size > MAX_ATTACHMENT_BYTES) throw new RangeError("Attachments exceed the 64 MiB limit");
205
+ const bytes = assetBytes(data as AssetData, `${label}.data`);
206
+ totalBytes += bytes.byteLength;
207
+ if (totalBytes > MAX_ATTACHMENT_BYTES) throw new RangeError("Attachments exceed the 64 MiB limit");
208
+ return {
209
+ name, data: bytes, mimeType,
210
+ description: file.description === undefined ? name : text(file.description, `${label}.description`),
211
+ relationship: relationship as AttachmentRelationship,
212
+ modificationDate: date(file.modificationDate, `${label}.modificationDate`, modificationDate),
213
+ compress: file.compress ?? null,
214
+ };
215
+ }
216
+ const attachments = Array.from(options.attachments ?? [], (file, i) => attachment(file, `attachments[${i}]`));
217
+ return { metadata, attachments };
218
+ }
package/src/node.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { createApi } from "./api.js";
3
3
 
4
- export type { AssetData, AssetMap, AssetResolver, BinaryData, InitOptions, Length, PageFormat, PageNumberOptions, PdfOptions, WasmSource } from "./types.js";
4
+ export type { AssetData, AssetMap, AssetResolver, AttachmentRelationship, BinaryData, InitOptions, Length, PageFormat, PageNumberOptions, PdfAttachment, PdfDate, PdfMetadata, PdfOptions, PdfXmpProperty, PdfXmpSchema, WasmSource } from "./types.js";
5
5
  export const { init, renderPdf, getEngineVersion } = createApi(
6
6
  () => readFile(new URL("../wasm/blitz_pdf_bg.wasm", import.meta.url)),
7
7
  async source => {
package/src/options.ts CHANGED
@@ -1,13 +1,19 @@
1
1
  import { assetBytes, copyBytes, MAX_ASSET_BYTES } from "./assets.js";
2
+ import { normalizeDocumentOptions } from "./document-options.js";
3
+ import { record } from "./validation.js";
2
4
  import type { Length, PageFormat, PageNumberOptions, PdfOptions } from "./types.js";
3
5
 
4
6
  const formats: Record<PageFormat, readonly [number, number]> = {
7
+ A0: [841 * 96 / 25.4, 1189 * 96 / 25.4],
8
+ A1: [594 * 96 / 25.4, 841 * 96 / 25.4],
9
+ A2: [420 * 96 / 25.4, 594 * 96 / 25.4],
5
10
  A3: [297 * 96 / 25.4, 420 * 96 / 25.4],
6
11
  A4: [210 * 96 / 25.4, 297 * 96 / 25.4],
7
12
  A5: [148 * 96 / 25.4, 210 * 96 / 25.4],
8
- Letter: [8.5 * 96, 11 * 96], Legal: [8.5 * 96, 14 * 96], Tabloid: [11 * 96, 17 * 96],
13
+ A6: [105 * 96 / 25.4, 148 * 96 / 25.4],
14
+ Letter: [8.5 * 96, 11 * 96], Legal: [8.5 * 96, 14 * 96], Tabloid: [11 * 96, 17 * 96], Ledger: [17 * 96, 11 * 96],
9
15
  };
10
- const optionKeys = new Set(["format", "width", "height", "landscape", "margin", "media", "baseUrl", "assets", "fonts", "title", "pageNumbers", "maxPages"]);
16
+ const optionKeys = new Set(["format", "width", "height", "landscape", "preferCSSPageSize", "margin", "media", "baseUrl", "assets", "fonts", "title", "metadata", "attachments", "pageNumbers", "headerTemplate", "footerTemplate", "maxPages"]);
11
17
  const unitScale: Record<string, number> = { px: 1, pt: 96 / 72, mm: 96 / 25.4, cm: 96 / 2.54, in: 96 };
12
18
 
13
19
  function length(value: Length, name: string): number {
@@ -22,19 +28,17 @@ function length(value: Length, name: string): number {
22
28
  return result;
23
29
  }
24
30
 
25
- function record(value: unknown, name: string): void {
26
- if (value === null || typeof value !== "object" || Array.isArray(value) ||
27
- ![Object.prototype, null].includes(Object.getPrototypeOf(value))) {
28
- throw new TypeError(`${name} must be a plain object`);
29
- }
30
- }
31
-
32
31
  export function normalizeOptions(options: PdfOptions = {}) {
33
32
  record(options, "options");
34
33
  for (const key of Object.keys(options)) {
35
34
  if (!optionKeys.has(key)) throw new TypeError(`Unknown PDF option: ${key}`);
36
35
  }
37
36
  if (options.landscape !== undefined && typeof options.landscape !== "boolean") throw new TypeError("landscape must be a boolean");
37
+ if (options.preferCSSPageSize !== undefined && typeof options.preferCSSPageSize !== "boolean") throw new TypeError("preferCSSPageSize must be a boolean");
38
+ for (const key of ["headerTemplate", "footerTemplate"] as const) {
39
+ if (options[key] !== undefined && typeof options[key] !== "string") throw new TypeError(`${key} must be an HTML string`);
40
+ if (new TextEncoder().encode(options[key] ?? "").byteLength > 1024 * 1024) throw new RangeError(`${key} exceeds the 1 MiB limit`);
41
+ }
38
42
  const format = options.format ?? "A4";
39
43
  if (!Object.hasOwn(formats, format)) throw new TypeError(`Unknown page format: ${String(format)}`);
40
44
  if ((options.width === undefined) !== (options.height === undefined)) throw new TypeError("width and height must be provided together");
@@ -67,13 +71,12 @@ export function normalizeOptions(options: PdfOptions = {}) {
67
71
  if (fontSize < 1 || fontSize > 48) throw new RangeError("pageNumbers.fontSize must be between 1 and 48 CSS pixels");
68
72
  pageNumbers = { template, align, fontSize };
69
73
  }
70
- const footerHeight = pageNumbers ? pageNumbers.fontSize * 2 + 8 : 0;
71
- if (width - margin.left - margin.right < 1 || height - margin.top - margin.bottom - footerHeight < 1) throw new RangeError("Margins and pageNumbers must leave at least one CSS pixel of content");
74
+ // Printable geometry is validated after external CSS @page rules have loaded.
72
75
  const media = options.media ?? "print";
73
76
  if (media !== "print" && media !== "screen") throw new TypeError("media must be print or screen");
74
77
  const maxPages = options.maxPages ?? 100;
75
78
  if (!Number.isInteger(maxPages) || maxPages < 1 || maxPages > 1000) throw new RangeError("maxPages must be an integer between 1 and 1000");
76
- if (options.title !== undefined && typeof options.title !== "string") throw new TypeError("title must be a string");
79
+ const document = normalizeDocumentOptions(options);
77
80
  const baseUrl = options.baseUrl ?? "https://blitz-pdf.invalid/";
78
81
  if (typeof baseUrl !== "string") throw new TypeError("baseUrl must be an absolute URL string");
79
82
  let base: URL;
@@ -97,5 +100,7 @@ export function normalizeOptions(options: PdfOptions = {}) {
97
100
  if (options.fonts !== undefined && !Array.isArray(options.fonts)) throw new TypeError("fonts must be an array of font bytes");
98
101
  const fonts = (options.fonts ?? []).map((font, i) => copyBytes(font, `fonts[${i}]`));
99
102
  if (fonts.reduce((total, font) => total + font.byteLength, 0) > 32 * 1024 * 1024) throw new RangeError("Fonts exceed the 32 MiB limit");
100
- return { width, height, margin, media, maxPages, baseUrl: base.href, assets, assetResolver, fonts, pageNumbers, title: options.title ?? null };
103
+ return { width, height, margin, media, maxPages, baseUrl: base.href, assets, assetResolver, fonts, pageNumbers,
104
+ useCSSPageSize: options.preferCSSPageSize === true || (options.format === undefined && options.width === undefined),
105
+ headerTemplate: options.headerTemplate ?? null, footerTemplate: options.footerTemplate ?? null, ...document };
101
106
  }
package/src/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Numbers are CSS pixels (96 px = 1 inch). */
2
2
  export type Length = number | `${number}${"px" | "pt" | "mm" | "cm" | "in"}`;
3
- export type PageFormat = "A3" | "A4" | "A5" | "Letter" | "Legal" | "Tabloid";
3
+ export type PageFormat = "A0" | "A1" | "A2" | "A3" | "A4" | "A5" | "A6" | "Letter" | "Legal" | "Tabloid" | "Ledger";
4
4
  export type BinaryData = Uint8Array | ArrayBuffer;
5
5
  /** UTF-8 text (for CSS/SVG) or binary resource contents. */
6
6
  export type AssetData = BinaryData | string;
@@ -23,10 +23,77 @@ export interface PageNumberOptions {
23
23
  fontSize?: Length;
24
24
  }
25
25
 
26
+ /** A Date or an ISO 8601 timestamp with an explicit timezone. Serialized in UTC to whole seconds. */
27
+ export type PdfDate = Date | string;
28
+ export type AttachmentRelationship = "Source" | "Data" | "Alternative" | "Supplement" | "Unspecified";
29
+
30
+ export interface PdfAttachment {
31
+ /** Unique filename, without directories. Unicode is supported. */
32
+ name: string;
33
+ /** File contents, not a path or URL. Strings are encoded as UTF-8. */
34
+ data: AssetData;
35
+ /** MIME type without parameters. Default: application/octet-stream. */
36
+ mimeType?: string;
37
+ /** Human-readable description. Default: name. */
38
+ description?: string;
39
+ /** PDF/A association with the whole document. Default: Unspecified. */
40
+ relationship?: AttachmentRelationship;
41
+ /** Default: the document modification date. */
42
+ modificationDate?: PdfDate;
43
+ /** Omit to let the engine choose whether compression helps. */
44
+ compress?: boolean;
45
+ }
46
+
47
+ /** A custom text property and its PDF/A schema description. */
48
+ export interface PdfXmpProperty {
49
+ /** XML local name, e.g. DocumentFileName. */
50
+ name: string;
51
+ value: string;
52
+ description: string;
53
+ /** Default: external (supplied by the user). */
54
+ category?: "internal" | "external";
55
+ }
56
+
57
+ /** Custom XMP text properties, emitted with their PDF/A extension schema. */
58
+ export interface PdfXmpSchema {
59
+ /** Human-readable schema name. */
60
+ name: string;
61
+ /** Absolute namespace URI, e.g. urn:example:documents:1.0#. */
62
+ namespaceUri: string;
63
+ /** Unique XML namespace prefix, e.g. acme. Built-in prefixes are reserved. */
64
+ prefix: string;
65
+ properties: readonly PdfXmpProperty[];
66
+ }
67
+
68
+ export interface PdfMetadata {
69
+ /** Overrides the legacy top-level title option. */
70
+ title?: string;
71
+ author?: string | readonly string[];
72
+ subject?: string;
73
+ keywords?: readonly string[];
74
+ /** Application that created the document. */
75
+ creator?: string;
76
+ /** Default: blitz-pdf and its version. */
77
+ producer?: string;
78
+ /** Document language, e.g. de-DE. */
79
+ language?: string;
80
+ /** Default: render start time. */
81
+ creationDate?: PdfDate;
82
+ /** Default: creationDate. Must not precede it. */
83
+ modificationDate?: PdfDate;
84
+ /** Stable input used to derive the PDF document ID across revisions. */
85
+ documentId?: string;
86
+ /** Up to 32 custom schemas / 256 text properties / 1 MiB combined. */
87
+ xmp?: readonly PdfXmpSchema[];
88
+ }
89
+
26
90
  export interface PdfOptions {
27
91
  // All output uses PDF/A-3u conformance, including Unicode mappings and ICC colors.
28
92
  /** Default: A4. Use width and height together for a custom page. */
29
93
  format?: PageFormat;
94
+ /** Let CSS @page size override explicit format/width/height. Default: false.
95
+ * Without explicit dimensions, CSS page size is always used when available. */
96
+ preferCSSPageSize?: boolean;
30
97
  width?: Length;
31
98
  height?: Length;
32
99
  landscape?: boolean;
@@ -42,8 +109,16 @@ export interface PdfOptions {
42
109
  fonts?: readonly BinaryData[];
43
110
  /** PDF document title. */
44
111
  title?: string;
112
+ /** Synchronized PDF information dictionary and XMP metadata. */
113
+ metadata?: PdfMetadata;
114
+ /** Direct file attachments. Maximum 100 files and 64 MiB combined. */
115
+ attachments?: readonly PdfAttachment[];
45
116
  /** Selectable page-number footer. Reserves space above the bottom margin. Default: false. */
46
117
  pageNumbers?: boolean | PageNumberOptions;
118
+ /** HTML rendered on every page inside the top margin. Styles are template-local. */
119
+ headerTemplate?: string;
120
+ /** HTML rendered on every page inside the bottom margin. Styles are template-local. */
121
+ footerTemplate?: string;
47
122
  /** Reject larger documents. Default: 100; maximum: 1000. */
48
123
  maxPages?: number;
49
124
  }
@@ -0,0 +1,6 @@
1
+ export function record(value: unknown, name: string): void {
2
+ if (value === null || typeof value !== "object" || Array.isArray(value) ||
3
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value))) {
4
+ throw new TypeError(`${name} must be a plain object`);
5
+ }
6
+ }
package/vendor/README.md CHANGED
@@ -3,10 +3,12 @@
3
3
  `stylo/` is the unmodified crates.io `stylo` 0.22.0 source except for:
4
4
 
5
5
  - `properties/longhands.toml`: enable `break-before`, `break-after`, and
6
- `break-inside` for Servo; add inherited positive-integer `widows` and `orphans`.
6
+ `break-inside` and the `@page size` descriptor for Servo; add inherited
7
+ positive-integer `widows` and `orphans`.
7
8
  - `properties/shorthands.toml`: enable the three legacy `page-break-*` aliases.
8
9
  - `properties/counted_unknown_properties.py`: stop classifying widows/orphans as unknown.
9
10
  - `values/specified/box.rs`: parse `avoid-page`, `recto`, and `verso` break values.
11
+ - `stylesheets/rule_parser.rs`: enable Stylo's existing `@page` parser in Servo.
10
12
 
11
13
  Upstream exposes break properties only in Gecko builds. Enabling them here lets
12
14
  Blitz's actual Stylo cascade handle selectors, inheritance, media queries, CSS
@@ -14,4 +16,27 @@ variables, inline styles, and `!important`; no CSS rewriting is used.
14
16
 
15
17
  This directory retains its upstream MPL-2.0 license. The modified source ships in
16
18
  the npm archive. Update the patch when upgrading Stylo and run the pagination
17
- suite, especially the cascade and legacy-alias tests.
19
+ suite and `tests/reports.test.ts`, especially cascade, page geometry and legacy aliases.
20
+
21
+ # Krilla document metadata patch
22
+
23
+ `krilla/` contains the crates.io `krilla` 0.8.2 source (original crate checksum
24
+ `27da593198b20eeba65caeb73c2bbeec3e53ab08fa549898312ce81c4fce5e33`). Its
25
+ MIT/Apache-2.0 licenses are retained alongside the ICC profile license.
26
+
27
+ Changes are limited to:
28
+
29
+ - `src/interchange/metadata.rs`: separate creation/modification dates and an
30
+ API for custom XMP text properties with PDF/A extension schema descriptions.
31
+ - `src/configure/validate.rs`: merge custom schema descriptions into the same
32
+ container as the engine's built-in schemas.
33
+ - `src/chunk_container.rs`: include custom schemas, language, and document
34
+ identity in the instance-ID hash, and pass schemas to XMP serialization.
35
+
36
+ Krilla 0.8.2 already writes associated files and embedded-file name trees. The
37
+ patch leaves that implementation intact. The renderer validates custom schema
38
+ names, namespace URIs, prefixes, and values before passing them to Krilla.
39
+ No invoice-specific behavior is added to the engine or public API.
40
+
41
+ When upgrading Krilla, review this patch and run `tests/document.test.ts`, the
42
+ Node/browser checks, and `npm run test:pdfa` against the rebuilt WASM.
@@ -0,0 +1 @@
1
+ {"v":1}
@@ -0,0 +1,6 @@
1
+ {
2
+ "git": {
3
+ "sha1": "3ffdf0588cf98050aad6edba51ca70162e1fb5b5"
4
+ },
5
+ "path_in_vcs": "crates/krilla"
6
+ }