@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.
Files changed (66) hide show
  1. package/README.md +7 -7
  2. package/dist/api/descriptor.js +23 -0
  3. package/dist/api/forms.d.ts +347 -0
  4. package/dist/api/forms.js +189 -0
  5. package/dist/api/index.d.ts +1 -0
  6. package/dist/api/index.js +1 -0
  7. package/dist/api/structure.d.ts +6 -0
  8. package/dist/api/structure.js +1 -0
  9. package/dist/crypto/legacy-handler.d.ts +43 -0
  10. package/dist/crypto/legacy-handler.js +145 -0
  11. package/dist/crypto/security-handler.d.ts +16 -0
  12. package/dist/crypto/security-handler.js +26 -0
  13. package/dist/edit/acroform-reader.d.ts +70 -0
  14. package/dist/edit/acroform-reader.js +126 -0
  15. package/dist/edit/appearance.d.ts +47 -0
  16. package/dist/edit/appearance.js +171 -0
  17. package/dist/edit/document.d.ts +135 -0
  18. package/dist/edit/document.js +649 -0
  19. package/dist/edit/fill.d.ts +48 -0
  20. package/dist/edit/fill.js +395 -0
  21. package/dist/edit/flatten.d.ts +42 -0
  22. package/dist/edit/flatten.js +437 -0
  23. package/dist/edit/index.d.ts +18 -0
  24. package/dist/edit/index.js +16 -0
  25. package/dist/edit/lexer.d.ts +30 -0
  26. package/dist/edit/lexer.js +285 -0
  27. package/dist/edit/objects.d.ts +63 -0
  28. package/dist/edit/objects.js +56 -0
  29. package/dist/edit/writer.d.ts +53 -0
  30. package/dist/edit/writer.js +261 -0
  31. package/dist/elements/forms/checkbox-element.d.ts +51 -0
  32. package/dist/elements/forms/checkbox-element.js +48 -0
  33. package/dist/elements/forms/choice-element.d.ts +53 -0
  34. package/dist/elements/forms/choice-element.js +59 -0
  35. package/dist/elements/forms/push-button-element.d.ts +56 -0
  36. package/dist/elements/forms/push-button-element.js +49 -0
  37. package/dist/elements/forms/radio-element.d.ts +52 -0
  38. package/dist/elements/forms/radio-element.js +49 -0
  39. package/dist/elements/forms/signature-element.d.ts +54 -0
  40. package/dist/elements/forms/signature-element.js +47 -0
  41. package/dist/elements/forms/text-field-element.d.ts +61 -0
  42. package/dist/elements/forms/text-field-element.js +54 -0
  43. package/dist/elements/index.d.ts +6 -0
  44. package/dist/elements/index.js +6 -0
  45. package/dist/forms/acroform.d.ts +63 -0
  46. package/dist/forms/acroform.js +427 -0
  47. package/dist/forms/appearance.d.ts +58 -0
  48. package/dist/forms/appearance.js +204 -0
  49. package/dist/forms/field.d.ts +164 -0
  50. package/dist/forms/field.js +1 -0
  51. package/dist/forms/pdf.d.ts +19 -0
  52. package/dist/forms/pdf.js +28 -0
  53. package/dist/ir/display-list.d.ts +17 -1
  54. package/dist/renderer/form-field-renderer.d.ts +20 -0
  55. package/dist/renderer/form-field-renderer.js +12 -0
  56. package/dist/renderer/page-renderer.js +8 -2
  57. package/dist/renderer/pdf-backend.js +8 -0
  58. package/dist/renderer/pdf-renderer.js +19 -1
  59. package/dist/utils/dest-registry.d.ts +5 -2
  60. package/dist/utils/dest-registry.js +5 -5
  61. package/dist/utils/outline.js +1 -2
  62. package/dist/utils/pdf-object-manager.d.ts +18 -0
  63. package/dist/utils/pdf-object-manager.js +59 -6
  64. package/dist/utils/struct-tree.js +2 -3
  65. package/dist/validators/element-validator.js +7 -3
  66. 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 escaped = (s) => s.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
331
- const desc = opts.description ? ` /Desc (${escaped(opts.description)})` : "";
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 (${opts.identifier ?? "sRGB"}) ` +
354
- `/Info (${opts.info ?? "sRGB IEC61966-2.1"}) /DestOutputProfile ${profile} 0 R >>`);
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 (${esc(e.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 (${esc(this.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
- if (element.getFlex() <= 0) {
46
- throw new Error(`Flexible element ${element.constructor.name} has invalid flex value`);
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.7",
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",