@jasy/pdf 1.0.0-alpha.7 → 1.0.0-alpha.8
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 +7 -7
- package/dist/api/descriptor.js +23 -0
- package/dist/api/forms.d.ts +347 -0
- package/dist/api/forms.js +189 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/structure.d.ts +6 -0
- package/dist/api/structure.js +1 -0
- package/dist/crypto/legacy-handler.d.ts +43 -0
- package/dist/crypto/legacy-handler.js +145 -0
- package/dist/crypto/security-handler.d.ts +16 -0
- package/dist/crypto/security-handler.js +26 -0
- package/dist/edit/acroform-reader.d.ts +70 -0
- package/dist/edit/acroform-reader.js +126 -0
- package/dist/edit/appearance.d.ts +47 -0
- package/dist/edit/appearance.js +171 -0
- package/dist/edit/document.d.ts +135 -0
- package/dist/edit/document.js +649 -0
- package/dist/edit/fill.d.ts +48 -0
- package/dist/edit/fill.js +395 -0
- package/dist/edit/flatten.d.ts +42 -0
- package/dist/edit/flatten.js +437 -0
- package/dist/edit/index.d.ts +18 -0
- package/dist/edit/index.js +16 -0
- package/dist/edit/lexer.d.ts +30 -0
- package/dist/edit/lexer.js +285 -0
- package/dist/edit/objects.d.ts +63 -0
- package/dist/edit/objects.js +56 -0
- package/dist/edit/writer.d.ts +53 -0
- package/dist/edit/writer.js +261 -0
- package/dist/elements/forms/checkbox-element.d.ts +51 -0
- package/dist/elements/forms/checkbox-element.js +48 -0
- package/dist/elements/forms/choice-element.d.ts +53 -0
- package/dist/elements/forms/choice-element.js +59 -0
- package/dist/elements/forms/push-button-element.d.ts +56 -0
- package/dist/elements/forms/push-button-element.js +49 -0
- package/dist/elements/forms/radio-element.d.ts +52 -0
- package/dist/elements/forms/radio-element.js +49 -0
- package/dist/elements/forms/signature-element.d.ts +54 -0
- package/dist/elements/forms/signature-element.js +47 -0
- package/dist/elements/forms/text-field-element.d.ts +61 -0
- package/dist/elements/forms/text-field-element.js +54 -0
- package/dist/elements/index.d.ts +6 -0
- package/dist/elements/index.js +6 -0
- package/dist/forms/acroform.d.ts +63 -0
- package/dist/forms/acroform.js +427 -0
- package/dist/forms/appearance.d.ts +58 -0
- package/dist/forms/appearance.js +204 -0
- package/dist/forms/field.d.ts +164 -0
- package/dist/forms/field.js +1 -0
- package/dist/forms/pdf.d.ts +19 -0
- package/dist/forms/pdf.js +28 -0
- package/dist/ir/display-list.d.ts +17 -1
- package/dist/renderer/form-field-renderer.d.ts +20 -0
- package/dist/renderer/form-field-renderer.js +12 -0
- package/dist/renderer/page-renderer.js +8 -2
- package/dist/renderer/pdf-backend.js +8 -0
- package/dist/renderer/pdf-renderer.js +19 -1
- package/dist/utils/dest-registry.d.ts +5 -2
- package/dist/utils/dest-registry.js +5 -5
- package/dist/utils/outline.js +1 -2
- package/dist/utils/pdf-object-manager.d.ts +18 -0
- package/dist/utils/pdf-object-manager.js +59 -6
- package/dist/utils/struct-tree.js +2 -3
- package/dist/validators/element-validator.js +7 -3
- package/package.json +9 -2
|
@@ -7,6 +7,7 @@ import type { FontDecoration } from "../text/text-decoration.js";
|
|
|
7
7
|
import { StructTree } from "./struct-tree.js";
|
|
8
8
|
import { OutlineBuilder } from "./outline.js";
|
|
9
9
|
import { DestRegistry } from "./dest-registry.js";
|
|
10
|
+
import { AcroFormCollector } from "../forms/acroform.js";
|
|
10
11
|
import type { PDFConfig } from "../renderer/pdf-document-class.js";
|
|
11
12
|
import type { FontMetrics } from "./font-metrics.js";
|
|
12
13
|
interface FontIndexes {
|
|
@@ -47,6 +48,7 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
47
48
|
private overflowPolicy;
|
|
48
49
|
private security?;
|
|
49
50
|
private encJobs;
|
|
51
|
+
private strJobs;
|
|
50
52
|
private encryptObjNum?;
|
|
51
53
|
private _struct;
|
|
52
54
|
get struct(): StructTree;
|
|
@@ -54,6 +56,8 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
54
56
|
get outline(): OutlineBuilder;
|
|
55
57
|
private _dests;
|
|
56
58
|
get dests(): DestRegistry;
|
|
59
|
+
private _acroform;
|
|
60
|
+
get acroform(): AcroFormCollector;
|
|
57
61
|
constructor();
|
|
58
62
|
addObject(content: string): number;
|
|
59
63
|
replaceObject(objectNumber: number, content: string): void;
|
|
@@ -61,9 +65,23 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
61
65
|
setOverflowPolicy(policy: OverflowPolicy): void;
|
|
62
66
|
getOverflowPolicy(): OverflowPolicy;
|
|
63
67
|
private encToken;
|
|
68
|
+
private strToken;
|
|
69
|
+
/**
|
|
70
|
+
* The single choke-point every literal string in an OBJECT goes through - a field's `/T` and `/V`, a
|
|
71
|
+
* bookmark `/Title`, a link `/URI`, an `/Alt` text, a destination name.
|
|
72
|
+
*
|
|
73
|
+
* Without a security handler this is the escaped literal we have always written, so output stays
|
|
74
|
+
* byte-identical. With one, the raw bytes are registered for the finalize pass and a placeholder takes
|
|
75
|
+
* their place: ISO 32000-1 7.6.2 requires ALL strings and streams to be encrypted, and the ones missed
|
|
76
|
+
* here were exactly the values a password is supposed to protect (ISSUE-7).
|
|
77
|
+
*
|
|
78
|
+
* A string INSIDE a content stream needs none of this - the stream is enciphered as a whole.
|
|
79
|
+
*/
|
|
80
|
+
pdfString(text: string): string;
|
|
64
81
|
private streamPayload;
|
|
65
82
|
private stream;
|
|
66
83
|
addContentStream(content: string): number;
|
|
84
|
+
addFormXObject(bbox: string, content: string, resources?: string): number;
|
|
67
85
|
changePDFConfig(config: PDFConfig): void;
|
|
68
86
|
getPDFConfig(): PDFConfig;
|
|
69
87
|
private fillConfigWithStandardValues;
|
|
@@ -11,6 +11,7 @@ import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
|
|
|
11
11
|
import { StructTree } from "./struct-tree.js";
|
|
12
12
|
import { OutlineBuilder } from "./outline.js";
|
|
13
13
|
import { DestRegistry } from "./dest-registry.js";
|
|
14
|
+
import { AcroFormCollector } from "../forms/acroform.js";
|
|
14
15
|
// Enums come from the leaf config module (never in a cycle); the config type is
|
|
15
16
|
// erased at runtime so it can come from the cyclic module safely.
|
|
16
17
|
import { ColorMode, Orientation } from "../renderer/pdf-config.js";
|
|
@@ -134,6 +135,9 @@ export class PDFObjectManager {
|
|
|
134
135
|
get dests() {
|
|
135
136
|
return this._dests;
|
|
136
137
|
}
|
|
138
|
+
get acroform() {
|
|
139
|
+
return this._acroform;
|
|
140
|
+
}
|
|
137
141
|
constructor(pageSize) {
|
|
138
142
|
this.objects = [];
|
|
139
143
|
this.objectPositions = [];
|
|
@@ -167,6 +171,7 @@ export class PDFObjectManager {
|
|
|
167
171
|
this.compress = false;
|
|
168
172
|
this.overflowPolicy = "error";
|
|
169
173
|
this.encJobs = [];
|
|
174
|
+
this.strJobs = [];
|
|
170
175
|
// Accessible (PDF/UA) tagging, off by default (byte-identical output); the API turns it on.
|
|
171
176
|
this._struct = new StructTree();
|
|
172
177
|
// Document outline (bookmarks). Empty unless a `Bookmark` was placed; then finalize() emits /Outlines.
|
|
@@ -174,6 +179,9 @@ export class PDFObjectManager {
|
|
|
174
179
|
// Named destinations (internal-link jump targets). Empty unless an `Anchor` was placed; then it emits
|
|
175
180
|
// a /Names /Dests tree.
|
|
176
181
|
this._dests = new DestRegistry();
|
|
182
|
+
// Interactive form fields (AcroForm). Empty unless a form field was placed; then finalize() emits the
|
|
183
|
+
// catalog /AcroForm dict and each field's widget lands in its page /Annots.
|
|
184
|
+
this._acroform = new AcroFormCollector();
|
|
177
185
|
// Whether to kern. Document-level: measuring (via `runAdvance`, which reads this off the metrics)
|
|
178
186
|
// and drawing (the backend emits a `TJ`) both consult it, so they can never disagree. OFF while the
|
|
179
187
|
// embedded-font path (kern/GPOS) is unbuilt - a standard-14-only kerning would set the 14 system
|
|
@@ -213,6 +221,32 @@ export class PDFObjectManager {
|
|
|
213
221
|
encToken(id) {
|
|
214
222
|
return `JENC:${id}`;
|
|
215
223
|
}
|
|
224
|
+
// The same trick for a literal STRING. A separate marker because the two are swapped differently: a
|
|
225
|
+
// stream keeps its delimiters and only the payload changes, a string is replaced whole by a hex one.
|
|
226
|
+
strToken(id) {
|
|
227
|
+
return `\x00\x02JSTR:${id}\x02\x00`;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The single choke-point every literal string in an OBJECT goes through - a field's `/T` and `/V`, a
|
|
231
|
+
* bookmark `/Title`, a link `/URI`, an `/Alt` text, a destination name.
|
|
232
|
+
*
|
|
233
|
+
* Without a security handler this is the escaped literal we have always written, so output stays
|
|
234
|
+
* byte-identical. With one, the raw bytes are registered for the finalize pass and a placeholder takes
|
|
235
|
+
* their place: ISO 32000-1 7.6.2 requires ALL strings and streams to be encrypted, and the ones missed
|
|
236
|
+
* here were exactly the values a password is supposed to protect (ISSUE-7).
|
|
237
|
+
*
|
|
238
|
+
* A string INSIDE a content stream needs none of this - the stream is enciphered as a whole.
|
|
239
|
+
*/
|
|
240
|
+
pdfString(text) {
|
|
241
|
+
if (!this.security) {
|
|
242
|
+
return `(${text.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)")})`;
|
|
243
|
+
}
|
|
244
|
+
// Encrypt the bytes the unencrypted path would have written, so a viewer decrypts to the same value.
|
|
245
|
+
// No escaping is applied: the result is emitted as a HEX string, which has no escapes to get wrong.
|
|
246
|
+
const bytes = new Uint8Array(getArrayBuffer(text));
|
|
247
|
+
const id = this.strJobs.push(bytes) - 1;
|
|
248
|
+
return this.strToken(id);
|
|
249
|
+
}
|
|
216
250
|
// The single encryption choke-point every stream emitter routes through (stream(), registerImage). Without
|
|
217
251
|
// a security handler: the bytes as a latin1 string + their length. With one: register the raw bytes for the
|
|
218
252
|
// finalize pass and return a placeholder + the pre-computable encrypted length (16-byte IV + PKCS#7 padding).
|
|
@@ -246,6 +280,14 @@ export class PDFObjectManager {
|
|
|
246
280
|
addContentStream(content) {
|
|
247
281
|
return this.addObject(this.stream("", new Uint8Array(getArrayBuffer(content))));
|
|
248
282
|
}
|
|
283
|
+
// A Form XObject: a self-contained content stream with its own /BBox (+ optional /Resources). Used for
|
|
284
|
+
// form-field appearance streams (/AP). Routes through the same choke-point as page content, so
|
|
285
|
+
// compression and encryption apply. Returns the object number.
|
|
286
|
+
addFormXObject(bbox, content, resources) {
|
|
287
|
+
const dict = `/Type /XObject /Subtype /Form /BBox ${bbox}` +
|
|
288
|
+
(resources ? ` /Resources << ${resources} >>` : "");
|
|
289
|
+
return this.addObject(this.stream(dict, new Uint8Array(getArrayBuffer(content))));
|
|
290
|
+
}
|
|
249
291
|
changePDFConfig(config) {
|
|
250
292
|
this.pdfConfig = { ...this.pdfConfig, ...config };
|
|
251
293
|
}
|
|
@@ -327,9 +369,8 @@ endstream`;
|
|
|
327
369
|
attachFile(name, data, opts = {}) {
|
|
328
370
|
const subtype = (opts.mimeType ?? "application/octet-stream").replace(/\//g, "#2F");
|
|
329
371
|
const embedded = this.addObject(this.stream(`/Type /EmbeddedFile /Subtype /${subtype} /Params << /Size ${data.length} >>`, data));
|
|
330
|
-
const
|
|
331
|
-
const
|
|
332
|
-
const filespec = this.addObject(`<< /Type /Filespec /F (${escaped(name)}) /UF (${escaped(name)}) ` +
|
|
372
|
+
const desc = opts.description ? ` /Desc ${this.pdfString(opts.description)}` : "";
|
|
373
|
+
const filespec = this.addObject(`<< /Type /Filespec /F ${this.pdfString(name)} /UF ${this.pdfString(name)} ` +
|
|
333
374
|
`/AFRelationship /${opts.relationship ?? "Unspecified"}${desc} ` +
|
|
334
375
|
`/EF << /F ${embedded} 0 R /UF ${embedded} 0 R >> >>`);
|
|
335
376
|
this.attachments.push({ name, filespec });
|
|
@@ -350,8 +391,8 @@ endstream`;
|
|
|
350
391
|
setOutputIntent(icc, opts = {}) {
|
|
351
392
|
const profile = this.addObject(this.stream("/N 3", icc));
|
|
352
393
|
this.outputIntent = this.addObject(`<< /Type /OutputIntent /S /GTS_PDFA1 ` +
|
|
353
|
-
`/OutputConditionIdentifier
|
|
354
|
-
`/Info
|
|
394
|
+
`/OutputConditionIdentifier ${this.pdfString(opts.identifier ?? "sRGB")} ` +
|
|
395
|
+
`/Info ${this.pdfString(opts.info ?? "sRGB IEC61966-2.1")} /DestOutputProfile ${profile} 0 R >>`);
|
|
355
396
|
}
|
|
356
397
|
getOutputIntent() {
|
|
357
398
|
return this.outputIntent;
|
|
@@ -399,6 +440,18 @@ endstream`;
|
|
|
399
440
|
if (idx >= 0)
|
|
400
441
|
this.objects[idx] = this.objects[idx].replace(token, () => ciphertext);
|
|
401
442
|
}
|
|
443
|
+
// Strings: the same idea, but a string is replaced WHOLE - `(text)` becomes `<ciphertext>`. Hex,
|
|
444
|
+
// because the ciphertext is binary and a hex string has no escape rules to get wrong.
|
|
445
|
+
for (let id = 0; id < this.strJobs.length; id++) {
|
|
446
|
+
const cipher = await this.security.encrypt(this.strJobs[id]);
|
|
447
|
+
const hex = Array.from(cipher, (b) => b.toString(16).padStart(2, "0").toUpperCase()).join("");
|
|
448
|
+
const token = this.strToken(id);
|
|
449
|
+
const idx = this.objects.findIndex((o) => o.includes(token));
|
|
450
|
+
if (idx >= 0)
|
|
451
|
+
this.objects[idx] = this.objects[idx].replace(token, () => `<${hex}>`);
|
|
452
|
+
}
|
|
453
|
+
// Added LAST on purpose: the /Encrypt dictionary's own strings must stay in the clear (7.6.2), and
|
|
454
|
+
// they would otherwise be swept up by the pass above.
|
|
402
455
|
this.encryptObjNum = this.addObject(`<< ${this.security.encryptDict()} >>`);
|
|
403
456
|
}
|
|
404
457
|
// Registers (or reuses) a transparency graphics state and returns its resource name
|
|
@@ -673,7 +726,7 @@ endstream`;
|
|
|
673
726
|
.map((g) => `${g} [${all[g] ?? 1000}]`)
|
|
674
727
|
.join(" ");
|
|
675
728
|
return (`<< /Type /Font /Subtype /CIDFontType2 /BaseFont /${pdfName(name)} ` +
|
|
676
|
-
`/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> ` +
|
|
729
|
+
`/CIDSystemInfo << /Registry ${this.pdfString("Adobe")} /Ordering ${this.pdfString("Identity")} /Supplement 0 >> ` +
|
|
677
730
|
`/FontDescriptor ${descriptor} 0 R /CIDToGIDMap /Identity ` +
|
|
678
731
|
`/DW 1000 /W [${w}] >>`);
|
|
679
732
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// Escapes a PDF literal string ( ... ) - the language tag is safe ASCII, but /Alt can carry arbitrary text.
|
|
2
|
-
const esc = (s) => s.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
|
|
3
2
|
// A structure role reaches the PDF as a raw name (/H1, /S /Table). Roles come from a controlled set,
|
|
4
3
|
// but restrict them to a safe PDF-name token anyway (defence-in-depth) so a stray "/", whitespace, or
|
|
5
4
|
// operator can never corrupt the content stream / object dictionary. Empty after stripping -> /Span.
|
|
@@ -90,7 +89,7 @@ export class StructTree {
|
|
|
90
89
|
...childrenOf(e.key).map((c) => `${objNum.get(c.key)} 0 R`),
|
|
91
90
|
...e.mc.map((m) => `<< /Type /MCR /Pg ${pg(m.structParents)} 0 R /MCID ${m.mcid} >>`),
|
|
92
91
|
].join(" ");
|
|
93
|
-
const alt = e.alt ? ` /Alt
|
|
92
|
+
const alt = e.alt ? ` /Alt ${om.pdfString(e.alt)}` : "";
|
|
94
93
|
// PDF/UA 7.5: a header cell needs a Scope so a reader can associate data cells with it. Our headers
|
|
95
94
|
// are a header ROW (the Table `header` option), so each TH is the head of its Column.
|
|
96
95
|
const attr = e.role === "TH" ? " /A << /O /Table /Scope /Column >>" : "";
|
|
@@ -125,6 +124,6 @@ export class StructTree {
|
|
|
125
124
|
om.replaceObject(parentTreeNum, `<< /Nums [ ${nums.join(" ")} ] >>`);
|
|
126
125
|
om.replaceObject(rootNum, `<< /Type /StructTreeRoot /K ${docNum} 0 R /ParentTree ${parentTreeNum} 0 R ` +
|
|
127
126
|
`/ParentTreeNextKey ${this.structParentsCounter} >>`);
|
|
128
|
-
return `/MarkInfo << /Marked true >> /StructTreeRoot ${rootNum} 0 R /Lang
|
|
127
|
+
return `/MarkInfo << /Marked true >> /StructTreeRoot ${rootNum} 0 R /Lang ${om.pdfString(this.lang)}`;
|
|
129
128
|
}
|
|
130
129
|
}
|
|
@@ -41,9 +41,13 @@ export class Validator {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
static validateFlexElement(element) {
|
|
44
|
-
// Ensure flexible elements have valid flex values
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
// Ensure flexible elements have valid flex values. The message names what the CALLER wrote, not the
|
|
45
|
+
// element class: `Spacer` and `Expanded` are what exists in their document, `ExpandedElement` is not.
|
|
46
|
+
const flex = element.getFlex();
|
|
47
|
+
if (flex <= 0) {
|
|
48
|
+
throw new Error(`@jasy/pdf: Spacer/Expanded needs a flex above 0, got ${flex}. Flex is a SHARE of the leftover ` +
|
|
49
|
+
"space, so 0 would claim none - which is what leaving the element out does. For a fixed gap " +
|
|
50
|
+
"use the `gap` of the surrounding Column or Row, or a Box with a height.");
|
|
47
51
|
}
|
|
48
52
|
// Ensure a flexible element does not contain another flexible element
|
|
49
53
|
if (hasChildProp(element)) {
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jasy/pdf",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.8",
|
|
4
4
|
"description": "Declarative, component-based PDF generation in pure TypeScript - Flutter-style components, real AFM font metrics, a hand-rolled PDF writer. No headless browser, no Java.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"declarative",
|
|
7
|
+
"e-invoice",
|
|
7
8
|
"factur-x",
|
|
8
9
|
"invoice",
|
|
9
10
|
"layout",
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
"pdf-a",
|
|
13
14
|
"pdf-generation",
|
|
14
15
|
"typescript",
|
|
16
|
+
"xrechnung",
|
|
15
17
|
"zugferd"
|
|
16
18
|
],
|
|
17
19
|
"license": "MIT",
|
|
@@ -30,7 +32,12 @@
|
|
|
30
32
|
".": {
|
|
31
33
|
"types": "./dist/index.d.ts",
|
|
32
34
|
"default": "./dist/index.js"
|
|
33
|
-
}
|
|
35
|
+
},
|
|
36
|
+
"./edit": {
|
|
37
|
+
"types": "./dist/edit/index.d.ts",
|
|
38
|
+
"default": "./dist/edit/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
34
41
|
},
|
|
35
42
|
"dependencies": {
|
|
36
43
|
"fflate": "^0.8.3",
|