blitz-pdf 0.1.1 → 0.1.3
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 +292 -12
- 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/docs/pagination-regressions.md +88 -0
- package/examples/zugferd.mjs +41 -0
- package/licenses/RUST_DEPENDENCIES.txt +27 -0
- package/package.json +4 -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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Pagination regressions in 0.1.2
|
|
2
|
+
|
|
3
|
+
The regressions are exercised through `renderPdf` in `tests/pagination.test.ts`.
|
|
4
|
+
Rebuild the WASM before running them.
|
|
5
|
+
|
|
6
|
+
## Final breaks and empty lines
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
await renderPdf("<p>Text<br></p>", { format: "A4" });
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
With the pinned Blitz `e72650a` and Parley 0.12.0, instrumenting this layout
|
|
13
|
+
shows a paragraph height of 19 CSS pixels. Parley supplies two line ranges:
|
|
14
|
+
`[0, 19]` for `Text\n`, and `[19, 38]` with an empty text range and no positioned
|
|
15
|
+
items. Both have a finite, ordinary line height of 19 pixels.
|
|
16
|
+
|
|
17
|
+
Blitz's `packages/blitz-dom/src/layout/inline.rs` excludes that second line
|
|
18
|
+
from the block's height: it exists to position an editor cursor after a final
|
|
19
|
+
newline, not as another CSS line box. Pagination previously kept it. With the
|
|
20
|
+
default paragraph margins, the document ends inside this extra line; repeated
|
|
21
|
+
attempts to move the break backward eventually stop making progress. The old
|
|
22
|
+
error incorrectly classified this as a line taller than A4.
|
|
23
|
+
|
|
24
|
+
`css_lines` now uses the same terminal-line test as Blitz, including for
|
|
25
|
+
keep-with-next edges and widow/orphan counts. A real blank line has a newline in
|
|
26
|
+
its text range and remains in pagination with its measured line box. No text,
|
|
27
|
+
invisible anchor, or other character is inserted. Position assertions cover
|
|
28
|
+
multiple blank lines, table cells, and automatic/forced page boundaries.
|
|
29
|
+
|
|
30
|
+
## Height-limited tables
|
|
31
|
+
|
|
32
|
+
Use the following HTML in both a browser and `renderPdf(html, { format: "A4" })`:
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<style>
|
|
36
|
+
.info { width: 75mm; max-height: 75mm; overflow: hidden; }
|
|
37
|
+
.info table { width: 100%; border-collapse: collapse; }
|
|
38
|
+
.info td {
|
|
39
|
+
max-width: 0;
|
|
40
|
+
word-break: break-all;
|
|
41
|
+
overflow-wrap: break-word;
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
44
|
+
<div class="info">
|
|
45
|
+
<table>
|
|
46
|
+
<tr><td>Text</td><td>Text</td></tr>
|
|
47
|
+
<tr><td>Text</td><td>Text</td></tr>
|
|
48
|
+
<tr><td>Text</td><td>Text</td></tr>
|
|
49
|
+
<tr><td>Text</td><td>Text</td></tr>
|
|
50
|
+
<tr><td>Text</td><td>Text</td></tr>
|
|
51
|
+
<tr><td>Text</td><td>Text</td></tr>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Measured in Chromium 154 on macOS, `getBoundingClientRect()` gives a table
|
|
57
|
+
width of about 283.46 pixels, cell widths of about 141.73 pixels and a total
|
|
58
|
+
height of 123 pixels. Changing `max-width` to `none` produces the same
|
|
59
|
+
dimensions for these equal cell contents.
|
|
60
|
+
|
|
61
|
+
Blitz's final cell layout instead has width 2 pixels (zero content width plus
|
|
62
|
+
padding), height 78 pixels, and four 19-pixel lines for `Text`. Its table grid
|
|
63
|
+
adapts cells to grid items while retaining `max_size.width`; the inline layout
|
|
64
|
+
therefore receives a zero-width constraint. This confirms a browser
|
|
65
|
+
compatibility difference. It does not establish a general correction to the
|
|
66
|
+
CSS table-width algorithm, which remains a separate upstream task.
|
|
67
|
+
|
|
68
|
+
The resulting tall table exceeds its container's height. Pagination previously
|
|
69
|
+
used line and row ranges beyond the overflow clip; the document end inside a
|
|
70
|
+
clipped line could cause the same misleading error as the first reproduction.
|
|
71
|
+
Pagination now clips those ranges to the visible padding box and intersects
|
|
72
|
+
glyph bounds with the scene's nested clips. PDF text emission uses the same
|
|
73
|
+
clip bounds to omit fully hidden runs. Regression coverage includes both
|
|
74
|
+
`max-width` values, nested `hidden`/`clip` containers and clipped oversized ink.
|
|
75
|
+
|
|
76
|
+
The zero-width case now exports, but its column widths still differ from the
|
|
77
|
+
browser. Its test deliberately does not claim that all twelve cells are visible.
|
|
78
|
+
|
|
79
|
+
## Diagnostic distinctions
|
|
80
|
+
|
|
81
|
+
`PAGINATION_LINE_TOO_TALL` requires a blocking line whose visible height exceeds
|
|
82
|
+
the available height. `PAGINATION_NO_PROGRESS` covers, for example, staggered
|
|
83
|
+
columns of overlapping 40-pixel lines on an 80-pixel page. A forced break through
|
|
84
|
+
a line has its own `PAGINATION_FORCED_BREAK_INTERSECTS_LINE` code. Tests assert
|
|
85
|
+
the element, page/line indexes, available height and line range, and verify that
|
|
86
|
+
rendering remains usable after an error.
|
|
87
|
+
|
|
88
|
+
The Argon asset-path issue is outside this change.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { renderPdf } from "blitz-pdf";
|
|
5
|
+
|
|
6
|
+
// Use HTML and validated CII XML for the same invoice. This example packages
|
|
7
|
+
// supplied invoice data; it does not generate or validate business information.
|
|
8
|
+
const [htmlPath, xmlPath, profile, outputPath = "output/pdf/zugferd.pdf"] = process.argv.slice(2);
|
|
9
|
+
if (!htmlPath || !xmlPath || !profile) {
|
|
10
|
+
console.error('Usage: node examples/zugferd.mjs invoice.html invoice.xml "EN 16931" [output.pdf]');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
if (!["MINIMUM", "BASIC WL", "BASIC", "EN 16931", "EXTENDED"].includes(profile)) {
|
|
14
|
+
throw new Error("Choose MINIMUM, BASIC WL, BASIC, EN 16931, or EXTENDED for this example");
|
|
15
|
+
}
|
|
16
|
+
const [html, xml] = await Promise.all([readFile(htmlPath, "utf8"), readFile(xmlPath)]);
|
|
17
|
+
const bytes = await renderPdf(html, {
|
|
18
|
+
format: "A4", margin: "15mm",
|
|
19
|
+
baseUrl: pathToFileURL(resolve(htmlPath)).href,
|
|
20
|
+
assets: async url => readFile(new URL(url)),
|
|
21
|
+
attachments: [{
|
|
22
|
+
name: "factur-x.xml", data: xml, mimeType: "text/xml", description: "Invoice XML",
|
|
23
|
+
relationship: ["MINIMUM", "BASIC WL"].includes(profile) ? "Data" : "Alternative",
|
|
24
|
+
}],
|
|
25
|
+
metadata: {
|
|
26
|
+
title: "Invoice", creator: "blitz-pdf invoice example", language: "de-DE",
|
|
27
|
+
xmp: [{
|
|
28
|
+
name: "Factur-X PDFA Extension Schema", prefix: "fx",
|
|
29
|
+
namespaceUri: "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#",
|
|
30
|
+
properties: [
|
|
31
|
+
{ name: "DocumentFileName", value: "factur-x.xml", description: "Embedded invoice filename" },
|
|
32
|
+
{ name: "DocumentType", value: "INVOICE", description: "Document type" },
|
|
33
|
+
{ name: "Version", value: "1.0", description: "Schema version" },
|
|
34
|
+
{ name: "ConformanceLevel", value: profile, description: "Invoice profile" },
|
|
35
|
+
],
|
|
36
|
+
}],
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
await mkdir(dirname(resolve(outputPath)), { recursive: true });
|
|
40
|
+
await writeFile(outputPath, bytes);
|
|
41
|
+
console.log(`Created ${outputPath} (${bytes.length} bytes, PDF/A-3u with ${profile} Factur-X packaging).`);
|
|
@@ -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.3",
|
|
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",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"dist",
|
|
48
48
|
"wasm",
|
|
49
49
|
"src",
|
|
50
|
+
"docs",
|
|
50
51
|
"examples",
|
|
51
52
|
"assets/icon.svg",
|
|
52
53
|
"README.md",
|
|
@@ -71,7 +72,8 @@
|
|
|
71
72
|
"example:node": "node examples/node.mjs",
|
|
72
73
|
"example:bun": "bun examples/bun.ts",
|
|
73
74
|
"example:pagination": "node examples/pagination.mjs",
|
|
74
|
-
"example:german-invoice": "node examples/german-invoice.mjs"
|
|
75
|
+
"example:german-invoice": "node examples/german-invoice.mjs",
|
|
76
|
+
"example:zugferd": "node examples/zugferd.mjs"
|
|
75
77
|
},
|
|
76
78
|
"keywords": [
|
|
77
79
|
"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
|
+
}
|