blitz-pdf 0.1.0 → 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.
- package/README.md +300 -10
- package/THIRD_PARTY_NOTICES.md +4 -2
- package/dist/browser.d.ts +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/document-options.d.ts +36 -0
- package/dist/document-options.d.ts.map +1 -0
- package/dist/document-options.js +236 -0
- package/dist/document-options.js.map +1 -0
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/options.d.ts +35 -1
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +21 -14
- package/dist/options.js.map +1 -1
- package/dist/types.d.ts +71 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +2 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +7 -0
- package/dist/validation.js.map +1 -0
- package/examples/zugferd.mjs +41 -0
- package/licenses/RUST_DEPENDENCIES.txt +27 -0
- package/package.json +3 -2
- package/src/browser.ts +1 -1
- package/src/document-options.ts +218 -0
- package/src/node.ts +1 -1
- package/src/options.ts +18 -13
- package/src/types.ts +76 -1
- package/src/validation.ts +6 -0
- package/vendor/README.md +27 -2
- package/vendor/krilla/.cargo-ok +1 -0
- package/vendor/krilla/.cargo_vcs_info.json +6 -0
- package/vendor/krilla/Cargo.lock +1357 -0
- package/vendor/krilla/Cargo.toml +165 -0
- package/vendor/krilla/LICENSE_APACHE +202 -0
- package/vendor/krilla/LICENSE_MIT +21 -0
- package/vendor/krilla/README.md +156 -0
- package/vendor/krilla/examples/basic.rs +117 -0
- package/vendor/krilla/examples/empty_document.rs +25 -0
- package/vendor/krilla/examples/parley.rs +174 -0
- package/vendor/krilla/examples/simple_text.rs +118 -0
- package/vendor/krilla/examples/stream_builder.rs +93 -0
- package/vendor/krilla/icc/LICENSE +116 -0
- package/vendor/krilla/icc/README.md +1 -0
- package/vendor/krilla/icc/sGrey-v2-magic.icc +0 -0
- package/vendor/krilla/icc/sGrey-v4.icc +0 -0
- package/vendor/krilla/icc/sRGB-v2-magic.icc +0 -0
- package/vendor/krilla/icc/sRGB-v4.icc +0 -0
- package/vendor/krilla/src/chunk_container.rs +503 -0
- package/vendor/krilla/src/configure/PDF_A1.md +227 -0
- package/vendor/krilla/src/configure/PDF_A2.md +269 -0
- package/vendor/krilla/src/configure/PDF_A3.md +31 -0
- package/vendor/krilla/src/configure/PDF_A4.md +211 -0
- package/vendor/krilla/src/configure/PDF_UA1.md +202 -0
- package/vendor/krilla/src/configure/README.md +23 -0
- package/vendor/krilla/src/configure/mod.rs +197 -0
- package/vendor/krilla/src/configure/validate.rs +1244 -0
- package/vendor/krilla/src/configure/version.rs +134 -0
- package/vendor/krilla/src/content.rs +1352 -0
- package/vendor/krilla/src/data.rs +49 -0
- package/vendor/krilla/src/document.rs +156 -0
- package/vendor/krilla/src/error.rs +170 -0
- package/vendor/krilla/src/geom.rs +341 -0
- package/vendor/krilla/src/graphics/blend.rs +46 -0
- package/vendor/krilla/src/graphics/color.rs +552 -0
- package/vendor/krilla/src/graphics/graphic.rs +30 -0
- package/vendor/krilla/src/graphics/graphics_state.rs +251 -0
- package/vendor/krilla/src/graphics/icc.rs +218 -0
- package/vendor/krilla/src/graphics/image.rs +835 -0
- package/vendor/krilla/src/graphics/mask.rs +128 -0
- package/vendor/krilla/src/graphics/mod.rs +14 -0
- package/vendor/krilla/src/graphics/paint.rs +434 -0
- package/vendor/krilla/src/graphics/separation.rs +86 -0
- package/vendor/krilla/src/graphics/shading_function.rs +865 -0
- package/vendor/krilla/src/graphics/shading_pattern.rs +60 -0
- package/vendor/krilla/src/graphics/tiling_pattern.rs +119 -0
- package/vendor/krilla/src/graphics/xobject.rs +150 -0
- package/vendor/krilla/src/interactive/action.rs +68 -0
- package/vendor/krilla/src/interactive/annotation.rs +283 -0
- package/vendor/krilla/src/interactive/destination.rs +289 -0
- package/vendor/krilla/src/interactive/mod.rs +3 -0
- package/vendor/krilla/src/interchange/embed.rs +240 -0
- package/vendor/krilla/src/interchange/metadata.rs +564 -0
- package/vendor/krilla/src/interchange/mod.rs +4 -0
- package/vendor/krilla/src/interchange/outline.rs +263 -0
- package/vendor/krilla/src/interchange/tagging/fmt.rs +733 -0
- package/vendor/krilla/src/interchange/tagging/generate.toml +474 -0
- package/vendor/krilla/src/interchange/tagging/generated.rs +4754 -0
- package/vendor/krilla/src/interchange/tagging/mod.rs +1324 -0
- package/vendor/krilla/src/interchange/tagging/tag.rs +686 -0
- package/vendor/krilla/src/lib.rs +179 -0
- package/vendor/krilla/src/num.rs +26 -0
- package/vendor/krilla/src/page.rs +612 -0
- package/vendor/krilla/src/pdf.rs +313 -0
- package/vendor/krilla/src/resource.rs +436 -0
- package/vendor/krilla/src/serialize.rs +1101 -0
- package/vendor/krilla/src/stream.rs +368 -0
- package/vendor/krilla/src/surface.rs +664 -0
- package/vendor/krilla/src/text/cid.rs +488 -0
- package/vendor/krilla/src/text/font.rs +351 -0
- package/vendor/krilla/src/text/glyph/bitmap.rs +81 -0
- package/vendor/krilla/src/text/glyph/colr.rs +581 -0
- package/vendor/krilla/src/text/glyph/mod.rs +205 -0
- package/vendor/krilla/src/text/glyph/outline.rs +72 -0
- package/vendor/krilla/src/text/glyph/svg.rs +34 -0
- package/vendor/krilla/src/text/group.rs +463 -0
- package/vendor/krilla/src/text/mod.rs +232 -0
- package/vendor/krilla/src/text/shape.rs +102 -0
- package/vendor/krilla/src/text/type3.rs +514 -0
- package/vendor/krilla/src/util.rs +286 -0
- package/vendor/stylo/properties/longhands.toml +0 -1
- package/vendor/stylo/stylesheets/rule_parser.rs +1 -1
- package/wasm/blitz_pdf.js +10 -5
- package/wasm/blitz_pdf_bg.wasm +0 -0
|
@@ -4815,6 +4815,7 @@ DEALINGS IN THE SOFTWARE.
|
|
|
4815
4815
|
|
|
4816
4816
|
========================================================================
|
|
4817
4817
|
encoding_rs 0.8.42 (LICENSE-APACHE)
|
|
4818
|
+
krilla 0.8.2 (LICENSE_APACHE)
|
|
4818
4819
|
kurbo 0.13.1 (LICENSE-APACHE)
|
|
4819
4820
|
multiversion_no_op 1.0.0 (LICENSE-APACHE)
|
|
4820
4821
|
polycool 0.4.0 (LICENSE-APACHE)
|
|
@@ -6699,6 +6700,32 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
6699
6700
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
6700
6701
|
THE SOFTWARE.
|
|
6701
6702
|
|
|
6703
|
+
========================================================================
|
|
6704
|
+
krilla 0.8.2 (LICENSE_MIT)
|
|
6705
|
+
========================================================================
|
|
6706
|
+
|
|
6707
|
+
MIT License
|
|
6708
|
+
|
|
6709
|
+
Copyright (c) 2024 Laurenz Stampfl
|
|
6710
|
+
|
|
6711
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6712
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6713
|
+
in the Software without restriction, including without limitation the rights
|
|
6714
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
6715
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
6716
|
+
furnished to do so, subject to the following conditions:
|
|
6717
|
+
|
|
6718
|
+
The above copyright notice and this permission notice shall be included in all
|
|
6719
|
+
copies or substantial portions of the Software.
|
|
6720
|
+
|
|
6721
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
6722
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
6723
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
6724
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
6725
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
6726
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
6727
|
+
SOFTWARE.
|
|
6728
|
+
|
|
6702
6729
|
========================================================================
|
|
6703
6730
|
kurbo 0.13.1 (LICENSE-MIT)
|
|
6704
6731
|
peniko 0.6.1 (LICENSE-MIT)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blitz-pdf",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Render HTML and CSS to PDF with Blitz and WebAssembly in Node.js, Bun, and browsers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -71,7 +71,8 @@
|
|
|
71
71
|
"example:node": "node examples/node.mjs",
|
|
72
72
|
"example:bun": "bun examples/bun.ts",
|
|
73
73
|
"example:pagination": "node examples/pagination.mjs",
|
|
74
|
-
"example:german-invoice": "node examples/german-invoice.mjs"
|
|
74
|
+
"example:german-invoice": "node examples/german-invoice.mjs",
|
|
75
|
+
"example:zugferd": "node examples/zugferd.mjs"
|
|
75
76
|
},
|
|
76
77
|
"keywords": [
|
|
77
78
|
"pdf",
|
package/src/browser.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createApi } from "./api.js";
|
|
2
2
|
|
|
3
|
-
export type { AssetData, AssetMap, AssetResolver, BinaryData, InitOptions, Length, PageFormat, PageNumberOptions, PdfOptions, WasmSource } from "./types.js";
|
|
3
|
+
export type { AssetData, AssetMap, AssetResolver, AttachmentRelationship, BinaryData, InitOptions, Length, PageFormat, PageNumberOptions, PdfAttachment, PdfDate, PdfMetadata, PdfOptions, PdfXmpProperty, PdfXmpSchema, WasmSource } from "./types.js";
|
|
4
4
|
export const { init, renderPdf, getEngineVersion } = createApi(
|
|
5
5
|
() => new URL("../wasm/blitz_pdf_bg.wasm", import.meta.url),
|
|
6
6
|
);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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}
|