@jasy/pdf 1.0.0-alpha.7 → 1.0.0-alpha.9

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 (140) hide show
  1. package/README.md +11 -10
  2. package/dist/api/content.d.ts +5 -4
  3. package/dist/api/content.js +13 -7
  4. package/dist/api/descriptor.js +23 -0
  5. package/dist/api/dimension.d.ts +56 -0
  6. package/dist/api/dimension.js +27 -0
  7. package/dist/api/font-url.d.ts +28 -0
  8. package/dist/api/font-url.js +134 -0
  9. package/dist/api/forms.d.ts +347 -0
  10. package/dist/api/forms.js +189 -0
  11. package/dist/api/gradient.d.ts +53 -0
  12. package/dist/api/gradient.js +97 -0
  13. package/dist/api/index.d.ts +3 -0
  14. package/dist/api/index.js +3 -0
  15. package/dist/api/insets.d.ts +19 -12
  16. package/dist/api/insets.js +18 -7
  17. package/dist/api/layout.d.ts +20 -6
  18. package/dist/api/layout.js +31 -6
  19. package/dist/api/page-builder.d.ts +8 -0
  20. package/dist/api/page-builder.js +12 -0
  21. package/dist/api/structure.d.ts +11 -0
  22. package/dist/api/structure.js +17 -4
  23. package/dist/api/text.d.ts +42 -6
  24. package/dist/api/text.js +29 -5
  25. package/dist/crypto/legacy-handler.d.ts +43 -0
  26. package/dist/crypto/legacy-handler.js +145 -0
  27. package/dist/crypto/security-handler.d.ts +16 -0
  28. package/dist/crypto/security-handler.js +26 -0
  29. package/dist/edit/acroform-reader.d.ts +70 -0
  30. package/dist/edit/acroform-reader.js +126 -0
  31. package/dist/edit/appearance.d.ts +47 -0
  32. package/dist/edit/appearance.js +171 -0
  33. package/dist/edit/document.d.ts +145 -0
  34. package/dist/edit/document.js +642 -0
  35. package/dist/edit/fill.d.ts +65 -0
  36. package/dist/edit/fill.js +424 -0
  37. package/dist/edit/flatten.d.ts +54 -0
  38. package/dist/edit/flatten.js +462 -0
  39. package/dist/edit/index.d.ts +20 -0
  40. package/dist/edit/index.js +18 -0
  41. package/dist/edit/lexer.d.ts +30 -0
  42. package/dist/edit/lexer.js +285 -0
  43. package/dist/edit/objects.d.ts +68 -0
  44. package/dist/edit/objects.js +83 -0
  45. package/dist/edit/writer.d.ts +60 -0
  46. package/dist/edit/writer.js +283 -0
  47. package/dist/elements/container-element.d.ts +21 -1
  48. package/dist/elements/container-element.js +25 -10
  49. package/dist/elements/forms/checkbox-element.d.ts +51 -0
  50. package/dist/elements/forms/checkbox-element.js +48 -0
  51. package/dist/elements/forms/choice-element.d.ts +53 -0
  52. package/dist/elements/forms/choice-element.js +59 -0
  53. package/dist/elements/forms/push-button-element.d.ts +56 -0
  54. package/dist/elements/forms/push-button-element.js +49 -0
  55. package/dist/elements/forms/radio-element.d.ts +52 -0
  56. package/dist/elements/forms/radio-element.js +49 -0
  57. package/dist/elements/forms/signature-element.d.ts +54 -0
  58. package/dist/elements/forms/signature-element.js +47 -0
  59. package/dist/elements/forms/text-field-element.d.ts +61 -0
  60. package/dist/elements/forms/text-field-element.js +54 -0
  61. package/dist/elements/image-element.d.ts +18 -4
  62. package/dist/elements/image-element.js +27 -12
  63. package/dist/elements/index.d.ts +6 -0
  64. package/dist/elements/index.js +6 -0
  65. package/dist/elements/layout/expanded-element.d.ts +3 -1
  66. package/dist/elements/layout/expanded-element.js +12 -3
  67. package/dist/elements/layout/padding-element.d.ts +4 -2
  68. package/dist/elements/layout/padding-element.js +4 -2
  69. package/dist/elements/layout/page-builder-element.js +2 -0
  70. package/dist/elements/pdf-element.d.ts +59 -1
  71. package/dist/elements/pdf-element.js +62 -1
  72. package/dist/elements/rectangle-element.d.ts +20 -6
  73. package/dist/elements/rectangle-element.js +25 -12
  74. package/dist/elements/row-element.d.ts +24 -1
  75. package/dist/elements/row-element.js +25 -10
  76. package/dist/elements/text-element.d.ts +60 -1
  77. package/dist/elements/text-element.js +132 -33
  78. package/dist/forms/acroform.d.ts +63 -0
  79. package/dist/forms/acroform.js +427 -0
  80. package/dist/forms/appearance.d.ts +58 -0
  81. package/dist/forms/appearance.js +204 -0
  82. package/dist/forms/field.d.ts +164 -0
  83. package/dist/forms/field.js +1 -0
  84. package/dist/forms/pdf.d.ts +19 -0
  85. package/dist/forms/pdf.js +28 -0
  86. package/dist/ir/display-list.d.ts +40 -7
  87. package/dist/ir/display-list.js +8 -1
  88. package/dist/layout/alignment.d.ts +10 -0
  89. package/dist/layout/alignment.js +6 -0
  90. package/dist/layout/box-constraints.d.ts +53 -0
  91. package/dist/layout/box-constraints.js +72 -0
  92. package/dist/layout/insets.d.ts +28 -0
  93. package/dist/layout/insets.js +20 -0
  94. package/dist/renderer/form-field-renderer.d.ts +20 -0
  95. package/dist/renderer/form-field-renderer.js +12 -0
  96. package/dist/renderer/image-renderer.js +2 -1
  97. package/dist/renderer/page-renderer.js +8 -2
  98. package/dist/renderer/pdf-backend.d.ts +6 -0
  99. package/dist/renderer/pdf-backend.js +86 -31
  100. package/dist/renderer/pdf-document-renderer.js +8 -1
  101. package/dist/renderer/pdf-renderer.js +19 -1
  102. package/dist/renderer/rectangle-renderer.js +8 -2
  103. package/dist/renderer/text-renderer.d.ts +2 -2
  104. package/dist/renderer/text-renderer.js +150 -42
  105. package/dist/text/advance.js +5 -2
  106. package/dist/text/arabic.d.ts +20 -0
  107. package/dist/text/arabic.js +101 -0
  108. package/dist/text/bidi.d.ts +33 -0
  109. package/dist/text/bidi.js +94 -0
  110. package/dist/text/font-fallback.d.ts +25 -0
  111. package/dist/text/font-fallback.js +38 -0
  112. package/dist/text/line-breaker.d.ts +35 -1
  113. package/dist/text/line-breaker.js +76 -16
  114. package/dist/text/orphans-widows.d.ts +27 -0
  115. package/dist/text/orphans-widows.js +53 -0
  116. package/dist/text/shape.d.ts +29 -0
  117. package/dist/text/shape.js +67 -0
  118. package/dist/text/text-style.d.ts +25 -0
  119. package/dist/text/text-style.js +44 -1
  120. package/dist/utils/dest-registry.d.ts +5 -2
  121. package/dist/utils/dest-registry.js +5 -5
  122. package/dist/utils/flex-layout.d.ts +24 -4
  123. package/dist/utils/flex-layout.js +169 -7
  124. package/dist/utils/font-metrics.d.ts +5 -0
  125. package/dist/utils/gsub.d.ts +36 -0
  126. package/dist/utils/gsub.js +178 -0
  127. package/dist/utils/inflate.d.ts +16 -0
  128. package/dist/utils/inflate.js +52 -0
  129. package/dist/utils/outline.js +1 -2
  130. package/dist/utils/pdf-object-manager.d.ts +39 -0
  131. package/dist/utils/pdf-object-manager.js +166 -15
  132. package/dist/utils/struct-tree.js +2 -3
  133. package/dist/utils/ttf-parser.d.ts +6 -0
  134. package/dist/utils/ttf-parser.js +19 -0
  135. package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
  136. package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
  137. package/dist/utils/woff.d.ts +10 -0
  138. package/dist/utils/woff.js +116 -0
  139. package/dist/validators/element-validator.js +14 -3
  140. package/package.json +10 -2
@@ -0,0 +1,283 @@
1
+ import { isDict, isRef, isStream, isString, stringsIn, } from "./objects.js";
2
+ /**
3
+ * Writing changes back into an existing PDF, as an **incremental update**.
4
+ *
5
+ * The original bytes are never rewritten. Changed objects are APPENDED, followed by a fresh
6
+ * cross-reference section whose `/Prev` points back at the old one. That is what the format was designed
7
+ * for, and it buys three things: we never have to understand (let alone re-emit) the parts of a foreign
8
+ * file we did not come for; an existing signature over the original bytes stays intact; and the result
9
+ * carries a property worth testing - **the original file is a literal PREFIX of the output**.
10
+ */
11
+ const enc = (s) => new TextEncoder().encode(s);
12
+ /**
13
+ * A PDF string, written back byte-faithfully.
14
+ *
15
+ * Only printable ASCII goes into a `(literal)`. Anything else becomes a `<hex>` string, which carries
16
+ * the SAME bytes but survives the UTF-8 encoder this file is finally written through - a literal holding
17
+ * byte 0xE4 would otherwise leave as the two bytes 0xC3 0xA4 and read back as mojibake.
18
+ */
19
+ const stringToken = (b) => {
20
+ const printable = b.every((byte) => byte >= 0x20 && byte <= 0x7e);
21
+ if (!printable) {
22
+ return `<${Array.from(b, (byte) => byte.toString(16).padStart(2, "0").toUpperCase()).join("")}>`;
23
+ }
24
+ let out = "";
25
+ for (const byte of b) {
26
+ if (byte === 0x28 || byte === 0x29 || byte === 0x5c)
27
+ out += "\\" + String.fromCharCode(byte);
28
+ else
29
+ out += String.fromCharCode(byte);
30
+ }
31
+ return `(${out})`;
32
+ };
33
+ /** Escape a PDF name: everything outside the regular characters becomes `#XX`. */
34
+ const escNameToken = (s) => Array.from(enc(s))
35
+ .map((b) => b >= 0x21 && b <= 0x7e && !"()<>[]{}/%#".includes(String.fromCharCode(b))
36
+ ? String.fromCharCode(b)
37
+ : "#" + b.toString(16).padStart(2, "0").toUpperCase())
38
+ .join("");
39
+ const num = (n) => (Number.isInteger(n) ? String(n) : String(Number(n.toFixed(6))));
40
+ /**
41
+ * Serialise an object back to PDF syntax. Used only for objects we deliberately change - everything we
42
+ * do not touch stays in the original bytes, untouched and unre-encoded.
43
+ *
44
+ * A string keeps the spelling it was read with: a name written as `<FEFF…>` stays hex, because that is
45
+ * how the producer encodes unicode and rewriting it as a literal would corrupt it.
46
+ */
47
+ export function serialize(o, cipher) {
48
+ if (o === undefined || o === null)
49
+ return "null";
50
+ if (typeof o === "number")
51
+ return num(o);
52
+ if (typeof o === "boolean")
53
+ return String(o);
54
+ if (Array.isArray(o))
55
+ return `[${o.map((e) => serialize(e, cipher)).join(" ")}]`;
56
+ if (isRef(o))
57
+ return `${o.num} ${o.gen} R`;
58
+ if (isString(o)) {
59
+ // In an encrypted document every string was deciphered on open, so it is PLAINTEXT here. Writing it
60
+ // back as-is would leak it; the caller pre-enciphers each one and passes the result in.
61
+ const enciphered = cipher?.get(o);
62
+ if (enciphered !== undefined)
63
+ return enciphered;
64
+ // A string the producer wrote as hex stays hex; the rest is decided by its bytes.
65
+ return o.hex
66
+ ? `<${Array.from(o.bytes, (b) => b.toString(16).padStart(2, "0").toUpperCase()).join("")}>`
67
+ : stringToken(o.bytes);
68
+ }
69
+ if (isStream(o))
70
+ return `${serialize(o.dict, cipher)}\nstream\n<<stream data>>\nendstream`;
71
+ if (isDict(o)) {
72
+ const parts = [];
73
+ for (const [k, v] of o.map)
74
+ parts.push(`/${escNameToken(k)} ${serialize(v, cipher)}`);
75
+ return `<< ${parts.join(" ")} >>`;
76
+ }
77
+ return `/${escNameToken(o.name)}`;
78
+ }
79
+ export class IncrementalWriter {
80
+ constructor(doc) {
81
+ this.doc = doc;
82
+ /** Object number -> its new body, in PDF syntax. */
83
+ this.changed = new Map();
84
+ /** Numbers this writer invented, as opposed to ones it replaces - they have no recorded generation. */
85
+ this.added = new Set();
86
+ // New objects continue after the highest number the file already uses.
87
+ this.nextNum = Math.max(0, ...doc.objectNumbers()) + 1;
88
+ }
89
+ /** Replace an existing object. */
90
+ update(objNum, body) {
91
+ this.changed.set(objNum, body);
92
+ }
93
+ /** Append a new object and return its number. */
94
+ add(body) {
95
+ const n = this.nextNum++;
96
+ this.added.add(n);
97
+ this.changed.set(n, body);
98
+ return n;
99
+ }
100
+ /** A shared Helvetica for regenerated appearances, added once and only when one is drawn. */
101
+ helv() {
102
+ return (this.helvNum ?? (this.helvNum = this.add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>")));
103
+ }
104
+ /** The generation to write for an object: the file's own for one we replace, 0 for one we add. */
105
+ generationOf(objNum) {
106
+ return this.added.has(objNum) ? 0 : this.doc.generationOf(objNum);
107
+ }
108
+ get hasChanges() {
109
+ return this.changed.size > 0;
110
+ }
111
+ /**
112
+ * Produce the updated file: the original bytes, then every changed object, then a new cross-reference
113
+ * section chained to the old one.
114
+ */
115
+ save() {
116
+ if (this.changed.size === 0)
117
+ return this.doc.bytes;
118
+ const original = this.doc.bytes;
119
+ const chunks = [original];
120
+ let offset = original.length;
121
+ // A file that does not end in a newline would run its last line into our first object.
122
+ if (original[original.length - 1] !== 10 && original[original.length - 1] !== 13) {
123
+ chunks.push(enc("\n"));
124
+ offset += 1;
125
+ }
126
+ const offsets = new Map();
127
+ for (const [objNum, body] of [...this.changed].sort((a, b) => a[0] - b[0])) {
128
+ offsets.set(objNum, offset);
129
+ // The generation has to match what the file records, or the new entry describes a different
130
+ // object than the one being replaced. Objects we ADD are new numbers, and those start at 0.
131
+ const head = enc(`${objNum} ${this.generationOf(objNum)} obj\n`);
132
+ chunks.push(head);
133
+ offset += head.length;
134
+ if (typeof body === "string") {
135
+ const b = enc(`${body}\nendobj\n`);
136
+ chunks.push(b);
137
+ offset += b.length;
138
+ }
139
+ else {
140
+ const open = enc(`<< ${body.dict} /Length ${body.data.length} >>\nstream\n`);
141
+ const close = enc(`\nendstream\nendobj\n`);
142
+ chunks.push(open, body.data, close);
143
+ offset += open.length + body.data.length + close.length;
144
+ }
145
+ }
146
+ const xrefOffset = offset;
147
+ chunks.push(this.xrefSection(offsets, xrefOffset));
148
+ let total = 0;
149
+ for (const c of chunks)
150
+ total += c.length;
151
+ const out = new Uint8Array(total);
152
+ let at = 0;
153
+ for (const c of chunks) {
154
+ out.set(c, at);
155
+ at += c.length;
156
+ }
157
+ return out;
158
+ }
159
+ /**
160
+ * The new cross-reference section, as BYTES.
161
+ *
162
+ * Bytes, not a string: an xref stream is binary, and running it through a UTF-8 encoder would turn
163
+ * every value above 127 into two bytes - which is most offsets, so the whole index becomes garbage.
164
+ *
165
+ * A classic table is written when the file already had one; an xref-STREAM file gets a stream,
166
+ * because mixing the two leaves readers that follow only one kind unable to walk the chain back.
167
+ */
168
+ xrefSection(offsets, xrefOffset) {
169
+ const prev = this.previousStartXref();
170
+ const rootRef = this.doc.trailer.map.get("Root");
171
+ const idRef = this.doc.trailer.map.get("ID");
172
+ const infoRef = this.doc.trailer.map.get("Info");
173
+ // /Encrypt MUST be carried over. Dropping it declares the file plaintext while its contents are
174
+ // still ciphertext, so every reader - including ours - hands back raw bytes as field names.
175
+ const encryptRef = this.doc.trailer.map.get("Encrypt");
176
+ // An xref stream is itself an object and has to appear in its own index, so its number is taken
177
+ // first and its offset is where this whole section begins.
178
+ const all = new Map(offsets);
179
+ let selfNum;
180
+ if (this.doc.usesXrefStream) {
181
+ selfNum = this.nextNum++;
182
+ all.set(selfNum, xrefOffset);
183
+ }
184
+ const size = Math.max(this.nextNum, ...all.keys()) + 1;
185
+ // Group consecutive object numbers into subsections, which is what both formats want.
186
+ const nums = [...all.keys()].sort((a, b) => a - b);
187
+ const runs = [];
188
+ for (const n of nums) {
189
+ const last = runs[runs.length - 1];
190
+ if (last && n === last[0] + last[1].length)
191
+ last[1].push(n);
192
+ else
193
+ runs.push([n, [n]]);
194
+ }
195
+ const entry = (key, ref) => ref === undefined ? "" : `${key} ${serialize(ref)}`;
196
+ if (selfNum !== undefined) {
197
+ // Rows of `/W [1 4 2]`: in-use marker, 4-byte offset, 2-byte generation. Left uncompressed - a
198
+ // filter is optional, and a readable appended section is worth more here than a few hundred bytes.
199
+ const rows = new Uint8Array(nums.length * 7);
200
+ nums.forEach((n, i) => {
201
+ const off = all.get(n);
202
+ const g = n === selfNum ? 0 : this.generationOf(n);
203
+ rows.set([
204
+ 1,
205
+ (off >>> 24) & 0xff,
206
+ (off >>> 16) & 0xff,
207
+ (off >>> 8) & 0xff,
208
+ off & 0xff,
209
+ (g >>> 8) & 0xff,
210
+ g & 0xff,
211
+ ], i * 7);
212
+ });
213
+ const index = runs.map(([start, group]) => `${start} ${group.length}`).join(" ");
214
+ const dict = [
215
+ "/Type /XRef",
216
+ `/Size ${size}`,
217
+ `/Index [${index}]`,
218
+ "/W [1 4 2]",
219
+ entry("/Root", rootRef),
220
+ entry("/Info", infoRef),
221
+ entry("/ID", idRef),
222
+ entry("/Encrypt", encryptRef),
223
+ prev !== undefined ? `/Prev ${prev}` : "",
224
+ `/Length ${rows.length}`,
225
+ ].filter(Boolean);
226
+ const head = enc(`${selfNum} 0 obj\n<< ${dict.join(" ")} >>\nstream\n`);
227
+ const tail = enc(`\nendstream\nendobj\nstartxref\n${xrefOffset}\n%%EOF\n`);
228
+ const out = new Uint8Array(head.length + rows.length + tail.length);
229
+ out.set(head, 0);
230
+ out.set(rows, head.length);
231
+ out.set(tail, head.length + rows.length);
232
+ return out;
233
+ }
234
+ let table = "xref\n";
235
+ for (const [start, group] of runs) {
236
+ table += `${start} ${group.length}\n`;
237
+ for (const n of group) {
238
+ const g = String(this.generationOf(n)).padStart(5, "0");
239
+ table += `${String(all.get(n)).padStart(10, "0")} ${g} n \n`;
240
+ }
241
+ }
242
+ const trailer = [
243
+ `/Size ${size}`,
244
+ entry("/Root", rootRef),
245
+ entry("/Info", infoRef),
246
+ entry("/ID", idRef),
247
+ entry("/Encrypt", encryptRef),
248
+ prev !== undefined ? `/Prev ${prev}` : "",
249
+ ].filter(Boolean);
250
+ return enc(`${table}trailer\n<< ${trailer.join(" ")} >>\nstartxref\n${xrefOffset}\n%%EOF\n`);
251
+ }
252
+ /** The offset the ORIGINAL file's `startxref` pointed at - our `/Prev`. */
253
+ previousStartXref() {
254
+ const tail = new TextDecoder("latin1").decode(this.doc.bytes.subarray(Math.max(0, this.doc.bytes.length - 2048)));
255
+ const at = tail.lastIndexOf("startxref");
256
+ if (at < 0)
257
+ return undefined;
258
+ const m = /startxref\s+(\d+)/.exec(tail.slice(at));
259
+ return m ? Number(m[1]) : undefined;
260
+ }
261
+ }
262
+ /** Bytes as an enciphered PDF hex string, the form every rewritten string takes in an encrypted file. */
263
+ const hexString = (b) => `<${Array.from(b, (x) => x.toString(16).padStart(2, "0").toUpperCase()).join("")}>`;
264
+ /**
265
+ * Encipher every string the given objects carry, into the cipher map `serialize` consults.
266
+ *
267
+ * Not optional on an encrypted document: `serialize` writes any string missing from the map as
268
+ * PLAINTEXT, so an object rewritten without this pass leaks whatever it holds.
269
+ */
270
+ export async function carryStrings(doc, objects, into = new Map()) {
271
+ if (!doc.isEncrypted)
272
+ return into;
273
+ for (const o of objects) {
274
+ for (const str of stringsIn(o)) {
275
+ if (into.has(str))
276
+ continue;
277
+ const cipher = await doc.encryptForWrite(str.bytes);
278
+ if (cipher)
279
+ into.set(str, hexString(cipher));
280
+ }
281
+ }
282
+ return into;
283
+ }
@@ -9,9 +9,26 @@ interface ContainerElementParams extends SizedElement, WithChildren {
9
9
  main?: MainAlign;
10
10
  /** Horizontal alignment of each child (cross axis); defaults to `stretch`. */
11
11
  cross?: CrossAlign;
12
+ /** Lay the children out backwards along the main axis (CSS `row-reverse`). */
13
+ reverse?: boolean;
14
+ /** Let the children flow onto further lines when they do not fit (CSS `flex-wrap`). */
15
+ wrap?: boolean;
16
+ /** How the block of wrapped lines sits across the axis (CSS `align-content`). */
17
+ alignContent?: MainAlign;
12
18
  /** Width/height as a fraction (0..1) of the offered box instead of `width`/`height` (relative sizing). */
13
19
  widthFactor?: number;
14
20
  heightFactor?: number;
21
+ /** Lower / upper bounds per axis, in points or as a fraction; see `SizingParams`. */
22
+ minWidth?: number;
23
+ maxWidth?: number;
24
+ minHeight?: number;
25
+ maxHeight?: number;
26
+ minWidthFactor?: number;
27
+ maxWidthFactor?: number;
28
+ minHeightFactor?: number;
29
+ maxHeightFactor?: number;
30
+ /** width / height; derives whichever axis is left open (CSS `aspect-ratio`). */
31
+ aspectRatio?: number;
15
32
  /** Start this stack on a fresh page (CSS `break-before: page`). */
16
33
  breakBefore?: boolean;
17
34
  /** Start everything after this stack on a fresh page (CSS `break-after: page`). */
@@ -22,10 +39,13 @@ export declare class ContainerElement extends SizedPDFElement implements Fragmen
22
39
  private gap;
23
40
  private main;
24
41
  private cross;
42
+ private reverse;
43
+ private wrap;
44
+ private alignContent?;
25
45
  private breakBefore;
26
46
  private breakAfter;
27
47
  private requested;
28
- constructor({ x, y, width, height, children, gap, main, cross, widthFactor, heightFactor, breakBefore, breakAfter, }: ContainerElementParams);
48
+ constructor({ x, y, width, height, children, gap, main, cross, reverse, wrap, alignContent, breakBefore, breakAfter, ...sizing }: ContainerElementParams);
29
49
  breaksBefore(): boolean;
30
50
  breaksAfter(): boolean;
31
51
  /**
@@ -1,15 +1,18 @@
1
1
  import { FlexLayoutHelper, VERTICAL_AXIS } from "../utils/flex-layout.js";
2
- import { resolveExtent } from "../layout/box-constraints.js";
2
+ import { extentSpecs, resolveSize, } from "../layout/box-constraints.js";
3
3
  import { childrenForceBreak, packChildren, } from "../layout/fragmentation.js";
4
4
  import { FlexiblePDFElement, SizedPDFElement, } from "./pdf-element.js";
5
5
  export class ContainerElement extends SizedPDFElement {
6
- constructor({ x, y, width, height, children, gap, main, cross, widthFactor, heightFactor, breakBefore, breakAfter, }) {
6
+ constructor({ x, y, width, height, children, gap, main, cross, reverse, wrap, alignContent, breakBefore, breakAfter, ...sizing }) {
7
7
  super({ x, y, width, height });
8
8
  this.children = children;
9
9
  this.gap = gap ?? 0;
10
10
  this.main = main ?? "start";
11
11
  this.cross = cross ?? "stretch";
12
- this.requested = { width, height, widthFactor, heightFactor };
12
+ this.reverse = reverse ?? false;
13
+ this.wrap = wrap ?? false;
14
+ this.alignContent = alignContent;
15
+ this.requested = { ...sizing, width, height };
13
16
  this.breakBefore = breakBefore ?? false;
14
17
  this.breakAfter = breakAfter ?? false;
15
18
  }
@@ -62,6 +65,9 @@ export class ContainerElement extends SizedPDFElement {
62
65
  gap: this.gap,
63
66
  main: this.main,
64
67
  cross: this.cross,
68
+ reverse: this.reverse,
69
+ wrap: this.wrap,
70
+ alignContent: this.alignContent,
65
71
  breakAfter,
66
72
  });
67
73
  }
@@ -93,17 +99,19 @@ export class ContainerElement extends SizedPDFElement {
93
99
  // Relative sizing: a pinned extent (fixed points or a fraction of the offered box, clamped into
94
100
  // the constraints) wins; else fill a bounded axis; else stay `undefined` and shrink-wrap below.
95
101
  // A Column nested in a Row gets unbounded width - passing 0 there would collapse children to 0.
96
- const explicitWidth = resolveExtent(this.requested.width, this.requested.widthFactor, constraints.maxWidth, constraints.hasBoundedWidth);
97
- const explicitHeight = resolveExtent(this.requested.height, this.requested.heightFactor, constraints.maxHeight, constraints.hasBoundedHeight);
102
+ const specs = extentSpecs(this.requested);
103
+ const resolved = resolveSize(specs.width, specs.height, this.requested.aspectRatio, constraints);
104
+ const { width: explicitWidth, height: explicitHeight, constraints: bounds } = resolved;
105
+ // Fill-or-shrink-wrap is decided by the constraints we were GIVEN; a min/max only caps the result.
98
106
  const boundedWidth = explicitWidth !== undefined
99
- ? constraints.constrainWidth(explicitWidth)
107
+ ? bounds.constrainWidth(explicitWidth)
100
108
  : constraints.hasBoundedWidth
101
- ? constraints.maxWidth
109
+ ? bounds.maxWidth
102
110
  : undefined;
103
111
  const boundedHeight = explicitHeight !== undefined
104
- ? constraints.constrainHeight(explicitHeight)
112
+ ? bounds.constrainHeight(explicitHeight)
105
113
  : constraints.hasBoundedHeight
106
- ? constraints.maxHeight
114
+ ? bounds.maxHeight
107
115
  : undefined;
108
116
  // Vertical stack: cross = width (children fill it), main = height (the stacking extent).
109
117
  const crossAvail = boundedWidth ?? Infinity;
@@ -113,7 +121,14 @@ export class ContainerElement extends SizedPDFElement {
113
121
  // Vertical flex stack (main = height, cross = width). The shared helper measures
114
122
  // fixed children, distributes the leftover to flex children, and places everything
115
123
  // in source order.
116
- result = FlexLayoutHelper.layout(this.children, VERTICAL_AXIS, mainAvail, crossAvail, this.y, this.x, { gap: this.gap, main: this.main, cross: this.cross }, ctx);
124
+ result = FlexLayoutHelper.layout(this.children, VERTICAL_AXIS, mainAvail, crossAvail, this.y, this.x, {
125
+ gap: this.gap,
126
+ main: this.main,
127
+ cross: this.cross,
128
+ reverse: this.reverse,
129
+ wrap: this.wrap,
130
+ alignContent: this.alignContent,
131
+ }, ctx);
117
132
  }
118
133
  // Pinned/bounded axis takes that extent; an unbounded one shrink-wraps to the children (height =
119
134
  // the stack, width = the widest child). Top-left coordinates; the container draws nothing, and
@@ -0,0 +1,51 @@
1
+ import { Color } from "../../common/color.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ import { LayoutContext, SizedPDFElement } from "../pdf-element.js";
4
+ import type { CheckboxSpec, FieldStyle } from "../../forms/field.js";
5
+ export interface CheckboxParams {
6
+ /** The field name (/T) - unique in the document. */
7
+ name: string;
8
+ /** Whether it starts checked. */
9
+ checked?: boolean;
10
+ /** The "on" export value (default `"Yes"`). */
11
+ onValue?: string;
12
+ /** Tooltip / accessible name. */
13
+ tooltip?: string;
14
+ /** Show but do not allow toggling. */
15
+ readOnly?: boolean;
16
+ /** Mark it as a field that must be filled in before submitting. */
17
+ required?: boolean;
18
+ /** Hide the widget entirely - neither on screen nor in print. */
19
+ hidden?: boolean;
20
+ /** Include the widget when printing (default true). */
21
+ print?: boolean;
22
+ /** Box side length in points (default 14). */
23
+ size?: number;
24
+ /** Checkmark colour (default black). */
25
+ color?: Color;
26
+ /** Box border colour (default a mid grey, so the box is visible). */
27
+ border?: Color;
28
+ /** Box background fill. */
29
+ background?: Color;
30
+ /** Border thickness (default 1). */
31
+ borderWidth?: number;
32
+ }
33
+ /**
34
+ * A checkbox (AcroForm /Btn). A small square leaf that draws nothing itself: it reserves its box and
35
+ * emits a `formfield` IR node whose checkbox spec bakes an /AP checkmark at the seam.
36
+ */
37
+ export declare class CheckboxElement extends SizedPDFElement {
38
+ private spec;
39
+ private style;
40
+ private size;
41
+ constructor(p: CheckboxParams);
42
+ calculateLayout(_constraints: BoxConstraints, offset: Offset, _ctx: LayoutContext): Size;
43
+ getProps(): {
44
+ x: number;
45
+ y: number;
46
+ width: number;
47
+ height: number;
48
+ spec: CheckboxSpec;
49
+ style: FieldStyle;
50
+ };
51
+ }
@@ -0,0 +1,48 @@
1
+ import { Color } from "../../common/color.js";
2
+ import { SizedPDFElement } from "../pdf-element.js";
3
+ /**
4
+ * A checkbox (AcroForm /Btn). A small square leaf that draws nothing itself: it reserves its box and
5
+ * emits a `formfield` IR node whose checkbox spec bakes an /AP checkmark at the seam.
6
+ */
7
+ export class CheckboxElement extends SizedPDFElement {
8
+ constructor(p) {
9
+ const size = p.size ?? 14;
10
+ super({ x: 0, y: 0, width: size, height: size });
11
+ this.size = size;
12
+ this.spec = {
13
+ kind: "checkbox",
14
+ name: p.name,
15
+ checked: p.checked,
16
+ onValue: p.onValue,
17
+ tooltip: p.tooltip,
18
+ readOnly: p.readOnly,
19
+ required: p.required,
20
+ hidden: p.hidden,
21
+ print: p.print,
22
+ };
23
+ this.style = {
24
+ border: p.border ?? new Color(102, 102, 102),
25
+ background: p.background,
26
+ color: p.color ?? new Color(0, 0, 0),
27
+ fontSize: 0,
28
+ borderWidth: p.borderWidth ?? 1,
29
+ };
30
+ }
31
+ calculateLayout(_constraints, offset, _ctx) {
32
+ this.x = offset.x;
33
+ this.y = offset.y;
34
+ this.width = this.size;
35
+ this.height = this.size;
36
+ return { width: this.size, height: this.size };
37
+ }
38
+ getProps() {
39
+ return {
40
+ x: this.x,
41
+ y: this.y,
42
+ width: this.width ?? 0,
43
+ height: this.height ?? 0,
44
+ spec: this.spec,
45
+ style: this.style,
46
+ };
47
+ }
48
+ }
@@ -0,0 +1,53 @@
1
+ import { Color } from "../../common/color.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ import { LayoutContext, SizedPDFElement } from "../pdf-element.js";
4
+ import type { ChoiceOption, ChoiceSpec, FieldAlign, FieldStyle } from "../../forms/field.js";
5
+ export interface ChoiceParams {
6
+ name: string;
7
+ options: ChoiceOption[];
8
+ /** `true` = dropdown (combo box), `false` = list box. */
9
+ combo: boolean;
10
+ editable?: boolean;
11
+ multiSelect?: boolean;
12
+ value?: string;
13
+ values?: string[];
14
+ tooltip?: string;
15
+ readOnly?: boolean;
16
+ /** How the value sits in the box (default left). */
17
+ align?: FieldAlign;
18
+ /** Mark it as a field that must be filled in before submitting. */
19
+ required?: boolean;
20
+ /** Hide the widget entirely - neither on screen nor in print. */
21
+ hidden?: boolean;
22
+ /** Include the widget when printing (default true). */
23
+ print?: boolean;
24
+ /** Box width; omit to fill the offered width. */
25
+ width?: number;
26
+ /** Box height; omit for a single-line default (dropdown) / a few rows (list box). */
27
+ height?: number;
28
+ fontSize?: number;
29
+ color?: Color;
30
+ border?: Color;
31
+ background?: Color;
32
+ borderWidth?: number;
33
+ }
34
+ /**
35
+ * A choice field (AcroForm /Ch) - a dropdown or a list box, chosen by `combo`. A leaf that draws nothing
36
+ * itself: it reserves its box and emits a `formfield` IR node. The selected value is rendered by the
37
+ * viewer via /NeedAppearances (Step 1); Step 4 bakes its /AP.
38
+ */
39
+ export declare class ChoiceElement extends SizedPDFElement {
40
+ private spec;
41
+ private style;
42
+ private requested;
43
+ constructor(p: ChoiceParams);
44
+ calculateLayout(constraints: BoxConstraints, offset: Offset, _ctx: LayoutContext): Size;
45
+ getProps(): {
46
+ x: number;
47
+ y: number;
48
+ width: number;
49
+ height: number;
50
+ spec: ChoiceSpec;
51
+ style: FieldStyle;
52
+ };
53
+ }
@@ -0,0 +1,59 @@
1
+ import { Color } from "../../common/color.js";
2
+ import { SizedPDFElement } from "../pdf-element.js";
3
+ /**
4
+ * A choice field (AcroForm /Ch) - a dropdown or a list box, chosen by `combo`. A leaf that draws nothing
5
+ * itself: it reserves its box and emits a `formfield` IR node. The selected value is rendered by the
6
+ * viewer via /NeedAppearances (Step 1); Step 4 bakes its /AP.
7
+ */
8
+ export class ChoiceElement extends SizedPDFElement {
9
+ constructor(p) {
10
+ super({ x: 0, y: 0, width: p.width, height: p.height });
11
+ const fontSize = p.fontSize ?? 12;
12
+ this.spec = {
13
+ kind: "choice",
14
+ name: p.name,
15
+ options: p.options,
16
+ combo: p.combo,
17
+ editable: p.editable,
18
+ multiSelect: p.multiSelect,
19
+ value: p.value,
20
+ values: p.values,
21
+ tooltip: p.tooltip,
22
+ readOnly: p.readOnly,
23
+ align: p.align,
24
+ required: p.required,
25
+ hidden: p.hidden,
26
+ print: p.print,
27
+ };
28
+ this.style = {
29
+ border: p.border ?? new Color(154, 164, 178),
30
+ background: p.background,
31
+ color: p.color ?? new Color(0, 0, 0),
32
+ fontSize,
33
+ borderWidth: p.borderWidth ?? 1,
34
+ };
35
+ this.requested = { width: p.width, height: p.height, fontSize, combo: p.combo };
36
+ }
37
+ calculateLayout(constraints, offset, _ctx) {
38
+ this.x = offset.x;
39
+ this.y = offset.y;
40
+ this.width = this.requested.width ?? (constraints.hasBoundedWidth ? constraints.maxWidth : 200);
41
+ // Dropdown: one line. List box: a few rows tall so several options are visible.
42
+ const oneLine = this.requested.fontSize + 9;
43
+ const rows = Math.max(3, Math.min(this.spec.options.length, 5));
44
+ this.height =
45
+ this.requested.height ??
46
+ (this.requested.combo ? oneLine : rows * (this.requested.fontSize + 4));
47
+ return { width: this.width, height: this.height };
48
+ }
49
+ getProps() {
50
+ return {
51
+ x: this.x,
52
+ y: this.y,
53
+ width: this.width ?? 0,
54
+ height: this.height ?? 0,
55
+ spec: this.spec,
56
+ style: this.style,
57
+ };
58
+ }
59
+ }
@@ -0,0 +1,56 @@
1
+ import { Color } from "../../common/color.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ import { LayoutContext, SizedPDFElement } from "../pdf-element.js";
4
+ import type { ButtonAction, FieldStyle, PushButtonSpec } from "../../forms/field.js";
5
+ export interface PushButtonParams {
6
+ /** The field name (/T) - unique in the document. */
7
+ name: string;
8
+ /** The caption drawn on the button. */
9
+ label: string;
10
+ /** What clicking it does; omit for a button that does nothing. */
11
+ action?: ButtonAction;
12
+ /** Tooltip / accessible name. */
13
+ tooltip?: string;
14
+ /** Draw it but do not let it be clicked. */
15
+ readOnly?: boolean;
16
+ /** Mark it as a field that must be filled in before submitting. */
17
+ required?: boolean;
18
+ /** Hide the widget entirely - neither on screen nor in print. */
19
+ hidden?: boolean;
20
+ /** Include the widget when printing (default true). */
21
+ print?: boolean;
22
+ /** Button width in points; omit to fill the offered width. */
23
+ width?: number;
24
+ /** Button height in points; omit for a comfortable default. */
25
+ height?: number;
26
+ /** Caption font size (default 12). */
27
+ fontSize?: number;
28
+ /** Caption colour (default black). */
29
+ color?: Color;
30
+ /** Border colour (default a mid grey). */
31
+ border?: Color;
32
+ /** Button face fill (default a light grey, so it reads as a button). */
33
+ background?: Color;
34
+ /** Border thickness (default 1). */
35
+ borderWidth?: number;
36
+ }
37
+ /**
38
+ * A push button (AcroForm /Btn, Pushbutton flag). Holds no value - it fires an action. Draws nothing
39
+ * itself: it reserves its box and emits a `formfield` IR node whose caption is baked into an /AP at the
40
+ * seam, so the button looks identical in every viewer and in print.
41
+ */
42
+ export declare class PushButtonElement extends SizedPDFElement {
43
+ private spec;
44
+ private style;
45
+ private requested;
46
+ constructor(p: PushButtonParams);
47
+ calculateLayout(constraints: BoxConstraints, offset: Offset, _ctx: LayoutContext): Size;
48
+ getProps(): {
49
+ x: number;
50
+ y: number;
51
+ width: number;
52
+ height: number;
53
+ spec: PushButtonSpec;
54
+ style: FieldStyle;
55
+ };
56
+ }