@jasy/pdf 1.0.0-alpha.8 → 1.0.0-beta.1

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 (167) hide show
  1. package/README.md +29 -10
  2. package/dist/api/color.js +69 -0
  3. package/dist/api/content.d.ts +45 -5
  4. package/dist/api/content.js +96 -7
  5. package/dist/api/descriptor.js +6 -1
  6. package/dist/api/dimension.d.ts +57 -0
  7. package/dist/api/dimension.js +27 -0
  8. package/dist/api/font-url.d.ts +28 -0
  9. package/dist/api/font-url.js +135 -0
  10. package/dist/api/gradient.d.ts +53 -0
  11. package/dist/api/gradient.js +97 -0
  12. package/dist/api/index.d.ts +2 -0
  13. package/dist/api/index.js +2 -0
  14. package/dist/api/insets.d.ts +19 -12
  15. package/dist/api/insets.js +18 -7
  16. package/dist/api/layout.d.ts +20 -6
  17. package/dist/api/layout.js +31 -6
  18. package/dist/api/page-builder.d.ts +8 -0
  19. package/dist/api/page-builder.js +12 -0
  20. package/dist/api/structure.d.ts +12 -0
  21. package/dist/api/structure.js +54 -7
  22. package/dist/api/text.d.ts +65 -8
  23. package/dist/api/text.js +38 -5
  24. package/dist/canvas/painter.d.ts +102 -0
  25. package/dist/canvas/painter.js +183 -0
  26. package/dist/edit/document.d.ts +14 -4
  27. package/dist/edit/document.js +15 -22
  28. package/dist/edit/fill.d.ts +19 -2
  29. package/dist/edit/fill.js +63 -34
  30. package/dist/edit/flatten.d.ts +13 -1
  31. package/dist/edit/flatten.js +38 -13
  32. package/dist/edit/index.d.ts +2 -0
  33. package/dist/edit/index.js +3 -1
  34. package/dist/edit/objects.d.ts +5 -0
  35. package/dist/edit/objects.js +27 -0
  36. package/dist/edit/writer.d.ts +7 -0
  37. package/dist/edit/writer.js +23 -1
  38. package/dist/elements/canvas-element.d.ts +38 -0
  39. package/dist/elements/canvas-element.js +47 -0
  40. package/dist/elements/container-element.d.ts +23 -1
  41. package/dist/elements/container-element.js +31 -10
  42. package/dist/elements/image-element.d.ts +18 -4
  43. package/dist/elements/image-element.js +27 -12
  44. package/dist/elements/index.d.ts +2 -0
  45. package/dist/elements/index.js +2 -0
  46. package/dist/elements/layout/expanded-element.d.ts +5 -1
  47. package/dist/elements/layout/expanded-element.js +16 -3
  48. package/dist/elements/layout/keep-together-element.d.ts +2 -0
  49. package/dist/elements/layout/keep-together-element.js +4 -0
  50. package/dist/elements/layout/padding-element.d.ts +9 -2
  51. package/dist/elements/layout/padding-element.js +12 -2
  52. package/dist/elements/layout/page-builder-element.js +2 -0
  53. package/dist/elements/layout/struct-group.d.ts +2 -0
  54. package/dist/elements/layout/struct-group.js +4 -0
  55. package/dist/elements/pdf-element.d.ts +75 -1
  56. package/dist/elements/pdf-element.js +80 -1
  57. package/dist/elements/rectangle-element.d.ts +22 -6
  58. package/dist/elements/rectangle-element.js +31 -12
  59. package/dist/elements/row-element.d.ts +26 -1
  60. package/dist/elements/row-element.js +31 -10
  61. package/dist/elements/svg-element.d.ts +41 -0
  62. package/dist/elements/svg-element.js +93 -0
  63. package/dist/elements/text-element.d.ts +92 -1
  64. package/dist/elements/text-element.js +208 -33
  65. package/dist/ir/display-list.d.ts +67 -11
  66. package/dist/ir/display-list.js +8 -1
  67. package/dist/layout/alignment.d.ts +10 -0
  68. package/dist/layout/alignment.js +6 -0
  69. package/dist/layout/box-constraints.d.ts +53 -0
  70. package/dist/layout/box-constraints.js +72 -0
  71. package/dist/layout/insets.d.ts +28 -0
  72. package/dist/layout/insets.js +20 -0
  73. package/dist/layout/min-intrinsic.d.ts +29 -0
  74. package/dist/layout/min-intrinsic.js +37 -0
  75. package/dist/renderer/anchor-renderer.js +1 -1
  76. package/dist/renderer/bookmark-renderer.js +1 -1
  77. package/dist/renderer/canvas-renderer.d.ts +14 -0
  78. package/dist/renderer/canvas-renderer.js +36 -0
  79. package/dist/renderer/container-renderer.js +1 -3
  80. package/dist/renderer/default-text-style-renderer.js +1 -1
  81. package/dist/renderer/deferred-renderer.js +1 -1
  82. package/dist/renderer/expanded-renderer.js +1 -1
  83. package/dist/renderer/image-renderer.js +2 -1
  84. package/dist/renderer/index.d.ts +2 -0
  85. package/dist/renderer/index.js +2 -0
  86. package/dist/renderer/keep-together-renderer.js +1 -1
  87. package/dist/renderer/link-renderer.js +1 -1
  88. package/dist/renderer/padding-renderer.js +1 -1
  89. package/dist/renderer/page-builder-renderer.js +1 -1
  90. package/dist/renderer/page-renderer.js +1 -3
  91. package/dist/renderer/pdf-backend.d.ts +21 -4
  92. package/dist/renderer/pdf-backend.js +198 -61
  93. package/dist/renderer/pdf-document-renderer.js +8 -1
  94. package/dist/renderer/pdf-renderer.js +5 -1
  95. package/dist/renderer/positioned-renderer.js +1 -1
  96. package/dist/renderer/rectangle-renderer.js +9 -5
  97. package/dist/renderer/repeating-header-renderer.js +1 -2
  98. package/dist/renderer/rotated-renderer.js +1 -1
  99. package/dist/renderer/row-renderer.js +1 -3
  100. package/dist/renderer/struct-group-renderer.js +1 -1
  101. package/dist/renderer/svg-renderer.d.ts +11 -0
  102. package/dist/renderer/svg-renderer.js +27 -0
  103. package/dist/renderer/text-renderer.d.ts +2 -2
  104. package/dist/renderer/text-renderer.js +176 -57
  105. package/dist/svg/css.d.ts +38 -0
  106. package/dist/svg/css.js +115 -0
  107. package/dist/svg/errors.d.ts +24 -0
  108. package/dist/svg/errors.js +30 -0
  109. package/dist/svg/gradients.d.ts +34 -0
  110. package/dist/svg/gradients.js +156 -0
  111. package/dist/svg/index.d.ts +22 -0
  112. package/dist/svg/index.js +331 -0
  113. package/dist/svg/shapes.d.ts +6 -0
  114. package/dist/svg/shapes.js +177 -0
  115. package/dist/svg/style.d.ts +61 -0
  116. package/dist/svg/style.js +144 -0
  117. package/dist/svg/transform.d.ts +13 -0
  118. package/dist/svg/transform.js +68 -0
  119. package/dist/svg/units.d.ts +11 -0
  120. package/dist/svg/units.js +73 -0
  121. package/dist/svg/xml.d.ts +24 -0
  122. package/dist/svg/xml.js +158 -0
  123. package/dist/text/advance.d.ts +3 -0
  124. package/dist/text/advance.js +8 -4
  125. package/dist/text/arabic.d.ts +20 -0
  126. package/dist/text/arabic.js +101 -0
  127. package/dist/text/bidi.d.ts +33 -0
  128. package/dist/text/bidi.js +94 -0
  129. package/dist/text/font-fallback.d.ts +25 -0
  130. package/dist/text/font-fallback.js +38 -0
  131. package/dist/text/glyph-coverage.d.ts +15 -0
  132. package/dist/text/glyph-coverage.js +77 -0
  133. package/dist/text/line-breaker.d.ts +43 -1
  134. package/dist/text/line-breaker.js +181 -45
  135. package/dist/text/orphans-widows.d.ts +27 -0
  136. package/dist/text/orphans-widows.js +53 -0
  137. package/dist/text/shape.d.ts +29 -0
  138. package/dist/text/shape.js +91 -0
  139. package/dist/text/text-style.d.ts +38 -0
  140. package/dist/text/text-style.js +50 -1
  141. package/dist/text/whitespace.d.ts +4 -0
  142. package/dist/text/whitespace.js +43 -0
  143. package/dist/text/word-splitting.d.ts +42 -0
  144. package/dist/text/word-splitting.js +72 -0
  145. package/dist/utils/flex-layout.d.ts +24 -4
  146. package/dist/utils/flex-layout.js +218 -7
  147. package/dist/utils/font-metrics.d.ts +13 -2
  148. package/dist/utils/gsub.d.ts +36 -0
  149. package/dist/utils/gsub.js +178 -0
  150. package/dist/utils/inflate.d.ts +16 -0
  151. package/dist/utils/inflate.js +52 -0
  152. package/dist/utils/pdf-object-manager.d.ts +44 -3
  153. package/dist/utils/pdf-object-manager.js +176 -14
  154. package/dist/utils/renderer-registry.d.ts +13 -1
  155. package/dist/utils/renderer-registry.js +22 -1
  156. package/dist/utils/ttf-parser.d.ts +6 -0
  157. package/dist/utils/ttf-parser.js +19 -0
  158. package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
  159. package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
  160. package/dist/utils/woff.d.ts +10 -0
  161. package/dist/utils/woff.js +116 -0
  162. package/dist/utils/woff2.d.ts +6 -0
  163. package/dist/utils/woff2.js +651 -0
  164. package/dist/validators/element-validator.js +9 -2
  165. package/dist/vector/path.d.ts +33 -0
  166. package/dist/vector/path.js +81 -0
  167. package/package.json +6 -2
package/dist/edit/fill.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { PdfDocument } from "./document.js";
2
2
  import { readAcroForm } from "./acroform-reader.js";
3
- import { IncrementalWriter, serialize } from "./writer.js";
3
+ import { carryStrings, IncrementalWriter, serialize } from "./writer.js";
4
4
  import { bakeAppearance, bakeButtonStates, fontMetrics, readLook } from "./appearance.js";
5
+ import { flattenInto } from "./flatten.js";
5
6
  import { getArrayBuffer } from "../utils/utf8-to-windows1252-encoder.js";
6
- import { get, isDict, isRef, isString, } from "./objects.js";
7
+ import { get, isDict, isRef } from "./objects.js";
7
8
  /** Field flags we have to honour when filling (see `forms/acroform.ts` for the writer's side). */
8
9
  const FF_READ_ONLY = 1 << 0;
9
10
  const FF_MULTI_SELECT = 1 << 21;
@@ -65,19 +66,6 @@ function withEntries(dict, changes, cipher) {
65
66
  return `<< ${parts.join(" ")} >>`;
66
67
  }
67
68
  /** Every string reachable inside an object, so a dictionary can be re-enciphered in one walk. */
68
- function stringsIn(o, out = [], depth = 0) {
69
- if (o === undefined || o === null || depth > 64)
70
- return out;
71
- if (isString(o))
72
- out.push(o);
73
- else if (Array.isArray(o))
74
- for (const e of o)
75
- stringsIn(e, out, depth + 1);
76
- else if (isDict(o))
77
- for (const v of o.map.values())
78
- stringsIn(v, out, depth + 1);
79
- return out;
80
- }
81
69
  /**
82
70
  * Draw the field again for its NEW value and append the XObject, returning its object number. Falls back
83
71
  * to `undefined` when the widget does not say enough about itself to draw it - then the caller drops the
@@ -117,7 +105,24 @@ async function bakeStatesFor(doc, writer, field, widgetNum, state) {
117
105
  data: (await doc.encryptForWrite(encoded)) ?? encoded,
118
106
  });
119
107
  };
120
- return `<< /N << /${escName(on)} ${await write(faces.on)} 0 R /Off ${await write(faces.off)} 0 R >> >>`;
108
+ // The object numbers come back too: a flatten in the same pass stamps one of these two states.
109
+ const onNum = await write(faces.on);
110
+ const offNum = await write(faces.off);
111
+ return {
112
+ ap: `<< /N << /${escName(on)} ${onNum} 0 R /Off ${offNum} 0 R >> >>`,
113
+ states: new Map([
114
+ [on, onNum],
115
+ ["Off", offNum],
116
+ ]),
117
+ };
118
+ }
119
+ /** The appearance a widget ALREADY carries for one of its states, from `/AP /N /<state>`. */
120
+ function stateAppearance(doc, widgetNum, state) {
121
+ const states = doc.resolve(get(doc.resolve(get(doc.getObject(widgetNum), "AP")), "N"));
122
+ if (states === undefined || !isDict(states))
123
+ return undefined;
124
+ const chosen = states.map.get(state);
125
+ return isRef(chosen) ? chosen : undefined;
121
126
  }
122
127
  class FillError extends Error {
123
128
  }
@@ -237,9 +242,22 @@ function plan(field, value, enciphered) {
237
242
  * @param values field name -> value. Names are the fully qualified ones `readAcroForm` reports.
238
243
  */
239
244
  export async function fillForm(bytes, values, options = {}) {
245
+ // Two option combinations describe a document nobody wants. Refused before the file is opened, since
246
+ // the answer does not depend on it.
247
+ if (options.fieldAppearances === false && options.needAppearances === false) {
248
+ throw new FillError("fieldAppearances and needAppearances are both false, which leaves the values invisible: nothing " +
249
+ "is drawn by jasy and the viewer is not asked to draw anything either. Turn one of them on");
250
+ }
251
+ if (options.flatten === true && options.fieldAppearances === false) {
252
+ throw new FillError("flatten needs fieldAppearances: flattening freezes the picture a field shows, and with " +
253
+ "fieldAppearances false there is no picture of the new value to freeze");
254
+ }
240
255
  // Async because an encrypted document has to be deciphered before its field names mean anything, and
241
256
  // because the values written back have to be enciphered again with the same file key.
242
- const doc = await PdfDocument.open(bytes, { password: options.password });
257
+ const doc = await PdfDocument.open(bytes, {
258
+ password: options.password,
259
+ maxStreamSize: options.maxStreamSize,
260
+ });
243
261
  if (!doc.canReEncrypt) {
244
262
  throw new FillError("this PDF is encrypted with RC4 or AES-128; jasy can open it but writes only AES-256, so filling it " +
245
263
  "would have to downgrade its protection - refused");
@@ -279,6 +297,8 @@ export async function fillForm(bytes, values, options = {}) {
279
297
  const bake = options.fieldAppearances !== false;
280
298
  // One metrics instance for the whole fill, not one per field and not one at module scope.
281
299
  const metrics = bake ? fontMetrics() : undefined;
300
+ // Only collected when a flatten follows in this same pass; see `FreshAppearances` for why it exists.
301
+ const fresh = options.flatten ? new Map() : undefined;
282
302
  // A widget we could not draw has no picture at all, so the viewer must be asked after all.
283
303
  let someBakeFailed = false;
284
304
  const filled = [];
@@ -323,6 +343,8 @@ export async function fillForm(bytes, values, options = {}) {
323
343
  if (bake && baked === undefined)
324
344
  someBakeFailed = true;
325
345
  editsFor(w.num).AP = baked !== undefined ? `<< /N ${baked} 0 R >>` : undefined;
346
+ if (baked !== undefined)
347
+ fresh?.set(w.num, { kind: "ref", num: baked, gen: 0 });
326
348
  }
327
349
  }
328
350
  for (const [widgetNum, state] of change.as ?? []) {
@@ -330,9 +352,20 @@ export async function fillForm(bytes, values, options = {}) {
330
352
  // A button whose widget ships NO state pictures (PDFKit, react-pdf write none) stays an empty box
331
353
  // in anything that does not redraw - and cannot be flattened at all. Draw the two states once.
332
354
  if (bake && !field.widgets.find((w) => w.num === widgetNum)?.hasAppearance) {
333
- const ap = await bakeStatesFor(doc, writer, field, widgetNum, state);
334
- if (ap !== undefined)
335
- editsFor(widgetNum).AP = ap;
355
+ const drawn = await bakeStatesFor(doc, writer, field, widgetNum, state);
356
+ if (drawn !== undefined) {
357
+ editsFor(widgetNum).AP = drawn.ap;
358
+ const num = drawn.states.get(state);
359
+ if (num !== undefined)
360
+ fresh?.set(widgetNum, { kind: "ref", num, gen: 0 });
361
+ }
362
+ }
363
+ else {
364
+ // The widget keeps its own pictures and only /AS moves, so the state to stamp is the NEW one -
365
+ // reading /AS back out of the document would give the state before this fill.
366
+ const ref = stateAppearance(doc, widgetNum, state);
367
+ if (ref !== undefined)
368
+ fresh?.set(widgetNum, ref);
336
369
  }
337
370
  }
338
371
  for (const [objNum, changes] of edits) {
@@ -355,18 +388,7 @@ export async function fillForm(bytes, values, options = {}) {
355
388
  const alsoRewritten = bake || options.needAppearances !== false ? [acro, catalog] : [];
356
389
  // One pass over every string that will be carried over unchanged. Without this a preserved tooltip or
357
390
  // a /DA would be written back in the clear, into a file where nothing else is.
358
- const carried = new Map();
359
- if (doc.isEncrypted) {
360
- for (const target of [...pending.map((p) => p.target), ...alsoRewritten]) {
361
- for (const str of stringsIn(target)) {
362
- if (carried.has(str))
363
- continue;
364
- const cipher = await doc.encryptForWrite(str.bytes);
365
- if (cipher)
366
- carried.set(str, hex(cipher));
367
- }
368
- }
369
- }
391
+ const carried = await carryStrings(doc, [...pending.map((p) => p.target), ...alsoRewritten]);
370
392
  for (const { objNum, target, changes } of pending) {
371
393
  writer.update(objNum, withEntries(target, changes, carried));
372
394
  }
@@ -374,8 +396,10 @@ export async function fillForm(bytes, values, options = {}) {
374
396
  // When we DID draw, the flag has to go: a form that still asks for regeneration gets it, and the
375
397
  // viewer throws our drawing away and redraws from /DA - which is how PDFKit's forms kept asking for a
376
398
  // ZapfDingbats they do not ship.
399
+ // With a flatten following there is nothing left to answer it for, and that pass rewrites the same
400
+ // /AcroForm dictionary anyway.
377
401
  const needAppearances = bake && !someBakeFailed ? "false" : "true";
378
- if (bake || options.needAppearances !== false) {
402
+ if (!options.flatten && (bake || options.needAppearances !== false)) {
379
403
  if (acro !== undefined && isDict(acro)) {
380
404
  if (isRef(acroRef)) {
381
405
  writer.update(acroRef.num, withEntries(acro, { NeedAppearances: needAppearances }, carried));
@@ -390,6 +414,11 @@ export async function fillForm(bytes, values, options = {}) {
390
414
  }
391
415
  }
392
416
  }
393
- return { bytes: writer.save(), warnings, filled };
417
+ // Same writer, so the whole operation stays one incremental update. Last, because it freezes the
418
+ // pictures drawn above.
419
+ const flattened = options.flatten
420
+ ? await flattenInto(doc, writer, form.fields, carried, fresh)
421
+ : [];
422
+ return { bytes: writer.save(), warnings, filled, flattened };
394
423
  }
395
424
  export { FillError };
@@ -1,6 +1,7 @@
1
1
  import { PdfDocument } from "./document.js";
2
2
  import { type ReadField } from "./acroform-reader.js";
3
3
  import { IncrementalWriter, type StringCipher } from "./writer.js";
4
+ import { type PdfRef } from "./objects.js";
4
5
  /**
5
6
  * Flattening a form: the values stop being editable fields and become part of the page.
6
7
  *
@@ -21,6 +22,8 @@ import { IncrementalWriter, type StringCipher } from "./writer.js";
21
22
  export interface FlattenOptions {
22
23
  /** The user password, for a document that is encrypted. */
23
24
  password?: string;
25
+ /** Ceiling on what ONE stream may inflate to, in bytes (default 64 MB) - the zip-bomb guard. */
26
+ maxStreamSize?: number;
24
27
  /** Flatten only these fields (fully qualified names). Omit for all of them. */
25
28
  fields?: string[];
26
29
  }
@@ -33,10 +36,19 @@ export interface FlattenResult {
33
36
  export declare class FlattenError extends Error {
34
37
  constructor(message: string);
35
38
  }
39
+ /**
40
+ * The appearance streams a fill has just written, keyed by widget object number.
41
+ *
42
+ * Flattening reads the picture a widget shows out of the DOCUMENT, but mid-pass the new one is still in
43
+ * the writer - without this map a filled-and-flattened form freezes the value from before the fill.
44
+ * Both widget kinds go stale, for different reasons: a text field's `/AP` is replaced, a button's `/AP`
45
+ * keeps every state and only `/AS` moves.
46
+ */
47
+ export type FreshAppearances = ReadonlyMap<number, PdfRef>;
36
48
  /**
37
49
  * Stamp the fields into their pages and take the widgets away. Shared by `flattenForm` and
38
50
  * `fillForm(..., { flatten: true })`, so filling and flattening produce ONE incremental update.
39
51
  */
40
- export declare function flattenInto(doc: PdfDocument, writer: IncrementalWriter, fields: ReadField[], cipher?: StringCipher): Promise<string[]>;
52
+ export declare function flattenInto(doc: PdfDocument, writer: IncrementalWriter, fields: ReadField[], carriedIn?: StringCipher, fresh?: FreshAppearances): Promise<string[]>;
41
53
  /** Flatten an existing form: its values become page content and stop being editable. */
42
54
  export declare function flattenForm(bytes: Uint8Array, options?: FlattenOptions): Promise<FlattenResult>;
@@ -3,7 +3,7 @@ import { readAcroForm } from "./acroform-reader.js";
3
3
  import { bakeAppearance, bakeButtonStates, readLook } from "./appearance.js";
4
4
  import { getArrayBuffer } from "../utils/utf8-to-windows1252-encoder.js";
5
5
  import { latin1FromBytes } from "../utils/bytes.js";
6
- import { IncrementalWriter, serialize } from "./writer.js";
6
+ import { carryStrings, IncrementalWriter, serialize } from "./writer.js";
7
7
  import { get, isDict, isRef, isStream, nameOf, } from "./objects.js";
8
8
  export class FlattenError extends Error {
9
9
  constructor(message) {
@@ -104,8 +104,11 @@ function appearanceOf(doc, widget) {
104
104
  * Collect what has to happen. A widget that ships NO appearance gets one drawn now, from its own style
105
105
  * and current value - the same drawing the fill path bakes. Without that, flattening a form from a
106
106
  * producer that writes no appearances (PDFKit, react-pdf) could only ever refuse.
107
+ *
108
+ * `fresh` overrides where a widget's picture comes from. It is empty for a plain `flattenForm`, and
109
+ * carries the just-baked appearances when a fill flattens in the same pass - see `FreshAppearances`.
107
110
  */
108
- async function planStamps(doc, writer, fields) {
111
+ async function planStamps(doc, writer, fields, fresh) {
109
112
  const pages = widgetPages(doc);
110
113
  const stamps = [];
111
114
  // Widgets that leave the page without leaving a picture: a push button with no face of its own. It
@@ -119,7 +122,9 @@ async function planStamps(doc, writer, fields) {
119
122
  if (w.num === undefined)
120
123
  continue;
121
124
  const widget = doc.getObject(w.num);
122
- let appearance = appearanceOf(doc, widget);
125
+ // A picture written during THIS pass wins: it lives in the writer, not in `doc`, so reading the
126
+ // document would freeze the value the field had BEFORE the fill.
127
+ let appearance = fresh?.get(w.num) ?? appearanceOf(doc, widget);
123
128
  if (appearance === undefined) {
124
129
  const drawnNum = await drawMissing(doc, writer, field, w.num);
125
130
  if (drawnNum === undefined) {
@@ -277,14 +282,19 @@ function withEntries(dict, changes, cipher) {
277
282
  * Stamp the fields into their pages and take the widgets away. Shared by `flattenForm` and
278
283
  * `fillForm(..., { flatten: true })`, so filling and flattening produce ONE incremental update.
279
284
  */
280
- export async function flattenInto(doc, writer, fields, cipher) {
285
+ export async function flattenInto(doc, writer, fields, carriedIn, fresh) {
281
286
  if (fields.length === 0)
282
287
  return [];
283
- const { stamps, removeOnly } = await planStamps(doc, writer, fields);
288
+ const { stamps, removeOnly } = await planStamps(doc, writer, fields, fresh);
284
289
  // Group by page: one appended content stream per page, not one per widget.
285
290
  const byPage = new Map();
286
291
  for (const s of stamps)
287
292
  (byPage.get(s.pageNum) ?? byPage.set(s.pageNum, []).get(s.pageNum)).push(s);
293
+ // Strings in the objects rewritten below have to be enciphered again; the caller's map is the seed.
294
+ // Untested because unreachable today: we can only flatten our own R6 files, and our page dictionaries
295
+ // hold no strings. It bites the moment a producer inlines an annotation (/Contents, /URI).
296
+ const acroDict = doc.resolve(get(doc.catalog, "AcroForm"));
297
+ const cipher = await carryStrings(doc, [...[...byPage.keys()].map((n) => doc.getObject(n)), acroDict, doc.catalog], new Map(carriedIn ?? []));
288
298
  for (const [pageNum, pageStamps] of byPage) {
289
299
  const page = doc.getObject(pageNum);
290
300
  if (page === undefined || !isDict(page))
@@ -353,10 +363,21 @@ export async function flattenInto(doc, writer, fields, cipher) {
353
363
  const gone = new Set(fields.map((f) => f.objNum).filter((n) => n !== undefined));
354
364
  // A flattened field may hang in a PARENT's /Kids rather than in the form's /Fields. Dropping it only
355
365
  // from the root leaves it reachable, so `readAcroForm` still reports a field with no widget left.
356
- pruneKids(doc, writer, get(acro, "Fields"), gone, cipher);
366
+ // Planned first, written after: a surviving parent is rewritten whole, carrying its own /T, /TU
367
+ // and /DA, and enciphering those needs an await the sync walk cannot do.
368
+ const prunes = pruneKids(doc, get(acro, "Fields"), gone);
369
+ await carryStrings(doc, prunes.map((p) => p.node), cipher);
370
+ for (const p of prunes) {
371
+ writer.update(p.num, withEntries(p.node, { Kids: `[${p.kept.map((k) => serialize(k, cipher)).join(" ")}]` }, cipher));
372
+ }
357
373
  const roots = doc.resolve(get(acro, "Fields"));
358
374
  const kept = Array.isArray(roots) ? roots.filter((r) => !(isRef(r) && gone.has(r.num))) : [];
359
- const newAcro = withEntries(acro, { Fields: `[${kept.map((r) => serialize(r, cipher)).join(" ")}]` }, cipher);
375
+ const newAcro = withEntries(acro, {
376
+ Fields: `[${kept.map((r) => serialize(r, cipher)).join(" ")}]`,
377
+ // A form with nothing left in it asking a viewer to regenerate appearances is a document
378
+ // contradicting itself, so the flag goes with the last field.
379
+ ...(kept.length === 0 ? { NeedAppearances: undefined } : {}),
380
+ }, cipher);
360
381
  if (isRef(acroRef)) {
361
382
  writer.update(acroRef.num, newAcro);
362
383
  }
@@ -377,12 +398,12 @@ export async function flattenInto(doc, writer, fields, cipher) {
377
398
  * Walk the field tree and rewrite every `/Kids` that still points at a flattened field. A branch left
378
399
  * with no children at all is added to `gone` so its own owner drops it too.
379
400
  */
380
- function pruneKids(doc, writer, list, gone, cipher, depth = 0) {
401
+ function pruneKids(doc, list, gone, out = [], depth = 0) {
381
402
  if (depth > 32)
382
- return;
403
+ return out;
383
404
  const entries = doc.resolve(list);
384
405
  if (!Array.isArray(entries))
385
- return;
406
+ return out;
386
407
  for (const ref of entries) {
387
408
  if (!isRef(ref) || gone.has(ref.num))
388
409
  continue;
@@ -392,7 +413,7 @@ function pruneKids(doc, writer, list, gone, cipher, depth = 0) {
392
413
  const kids = get(node, "Kids");
393
414
  if (kids === undefined)
394
415
  continue;
395
- pruneKids(doc, writer, kids, gone, cipher, depth + 1);
416
+ pruneKids(doc, kids, gone, out, depth + 1);
396
417
  const resolved = doc.resolve(kids);
397
418
  if (!Array.isArray(resolved))
398
419
  continue;
@@ -403,12 +424,16 @@ function pruneKids(doc, writer, list, gone, cipher, depth = 0) {
403
424
  gone.add(ref.num); // an empty branch is a field with nothing left to show
404
425
  continue;
405
426
  }
406
- writer.update(ref.num, withEntries(node, { Kids: `[${kept.map((k) => serialize(k, cipher)).join(" ")}]` }, cipher));
427
+ out.push({ num: ref.num, node, kept });
407
428
  }
429
+ return out;
408
430
  }
409
431
  /** Flatten an existing form: its values become page content and stop being editable. */
410
432
  export async function flattenForm(bytes, options = {}) {
411
- const doc = await PdfDocument.open(bytes, { password: options.password });
433
+ const doc = await PdfDocument.open(bytes, {
434
+ password: options.password,
435
+ maxStreamSize: options.maxStreamSize,
436
+ });
412
437
  if (!doc.canReEncrypt) {
413
438
  throw new FlattenError("this PDF is encrypted with RC4 or AES-128; jasy can open it but writes only AES-256, so " +
414
439
  "flattening it would have to downgrade its protection - refused");
@@ -15,4 +15,6 @@ export { flattenForm, FlattenError } from "./flatten.js";
15
15
  export type { FlattenOptions, FlattenResult } from "./flatten.js";
16
16
  export type { FieldValue, FillOptions, FillResult } from "./fill.js";
17
17
  export { PdfDocument, PdfEncryptedError } from "./document.js";
18
+ export type { LoadOptions, OpenOptions } from "./document.js";
19
+ export { DEFAULT_MAX_STREAM_SIZE, PdfStreamTooLargeError } from "../utils/inflate.js";
18
20
  export type { PdfDict, PdfName, PdfObject, PdfRef, PdfStream, PdfString } from "./objects.js";
@@ -12,5 +12,7 @@ export { readAcroForm } from "./acroform-reader.js";
12
12
  export { fillForm, FillError } from "./fill.js";
13
13
  export { flattenForm, FlattenError } from "./flatten.js";
14
14
  // The document itself, for callers who want to look further than the form.
15
- // PdfEncryptedError is what a caller catches to tell "wrong password" apart from a broken file.
15
+ // PdfEncryptedError is what a caller catches to tell "wrong password" apart from a broken file;
16
+ // PdfStreamTooLargeError tells "this file is attacking me" apart from both.
16
17
  export { PdfDocument, PdfEncryptedError } from "./document.js";
18
+ export { DEFAULT_MAX_STREAM_SIZE, PdfStreamTooLargeError } from "../utils/inflate.js";
@@ -61,3 +61,8 @@ export declare const numberOf: (o: PdfObject | undefined) => number | undefined;
61
61
  * which agrees with Latin-1 for everything a field name realistically holds.
62
62
  */
63
63
  export declare function textOf(o: PdfObject | undefined): string | undefined;
64
+ /**
65
+ * Every string inside an object, however deep. Both sides of the crypto seam use it: strings are
66
+ * deciphered on open and must be enciphered again before an object is written back.
67
+ */
68
+ export declare function stringsIn(o: PdfObject | undefined, out?: PdfString[], depth?: number): PdfString[];
@@ -54,3 +54,30 @@ export function textOf(o) {
54
54
  // value round-trippable, which is what a reader that may write the string back needs.
55
55
  return latin1FromBytes(b);
56
56
  }
57
+ /**
58
+ * Every string inside an object, however deep. Both sides of the crypto seam use it: strings are
59
+ * deciphered on open and must be enciphered again before an object is written back.
60
+ */
61
+ export function stringsIn(o, out = [], depth = 0) {
62
+ if (o === undefined || o === null)
63
+ return out;
64
+ // Throws rather than returning a short list: a missed string is one written back in the clear.
65
+ // Bounded at all only because the walk does not follow references, just inline containers.
66
+ if (depth > MAX_NESTING) {
67
+ throw new Error(`@jasy/pdf: this PDF nests objects more than ${MAX_NESTING} deep, which no legitimate producer ` +
68
+ `does; refusing to walk it`);
69
+ }
70
+ if (isString(o))
71
+ out.push(o);
72
+ else if (Array.isArray(o))
73
+ for (const e of o)
74
+ stringsIn(e, out, depth + 1);
75
+ // The dictionary only - a stream's DATA is covered by the stream's own encryption.
76
+ else if (isStream(o))
77
+ stringsIn(o.dict, out, depth + 1);
78
+ else if (isDict(o))
79
+ for (const v of o.map.values())
80
+ stringsIn(v, out, depth + 1);
81
+ return out;
82
+ }
83
+ const MAX_NESTING = 64;
@@ -51,3 +51,10 @@ export declare class IncrementalWriter {
51
51
  /** The offset the ORIGINAL file's `startxref` pointed at - our `/Prev`. */
52
52
  private previousStartXref;
53
53
  }
54
+ /**
55
+ * Encipher every string the given objects carry, into the cipher map `serialize` consults.
56
+ *
57
+ * Not optional on an encrypted document: `serialize` writes any string missing from the map as
58
+ * PLAINTEXT, so an object rewritten without this pass leaks whatever it holds.
59
+ */
60
+ export declare function carryStrings(doc: PdfDocument, objects: Array<PdfObject | undefined>, into?: Map<PdfString, string>): Promise<StringCipher>;
@@ -1,4 +1,4 @@
1
- import { isDict, isRef, isStream, isString } from "./objects.js";
1
+ import { isDict, isRef, isStream, isString, stringsIn, } from "./objects.js";
2
2
  /**
3
3
  * Writing changes back into an existing PDF, as an **incremental update**.
4
4
  *
@@ -259,3 +259,25 @@ export class IncrementalWriter {
259
259
  return m ? Number(m[1]) : undefined;
260
260
  }
261
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
+ }
@@ -0,0 +1,38 @@
1
+ import { BoxConstraints, Offset, Size, SizingParams } from "../layout/box-constraints.js";
2
+ import type { CanvasPainter, CanvasSize } from "../canvas/painter.js";
3
+ import { LayoutContext, SizedPDFElement } from "./pdf-element.js";
4
+ /** What the user draws. Runs at RENDER time, once the box is known. */
5
+ export type CanvasPaint = (painter: CanvasPainter, size: CanvasSize) => void;
6
+ interface CanvasElementParams extends SizingParams {
7
+ paint: CanvasPaint;
8
+ widthFactor?: number;
9
+ heightFactor?: number;
10
+ /** Alternate text (tagged PDF): with it the drawing is a Figure, without it decoration. */
11
+ alt?: string;
12
+ }
13
+ /**
14
+ * A box in the layout that the caller draws into.
15
+ *
16
+ * It has no children and no intrinsic size, so unlike an image it FILLS what it is offered when no
17
+ * size is given - there is nothing to derive one from. That is also why the callback is handed the
18
+ * resolved size: a drawing written against `size.width` works at any box.
19
+ */
20
+ export declare class CanvasElement extends SizedPDFElement {
21
+ private paintFn;
22
+ private alt?;
23
+ private requested;
24
+ private sizing;
25
+ private widthFactor?;
26
+ private heightFactor?;
27
+ constructor(params: CanvasElementParams);
28
+ getProps(): {
29
+ x: number;
30
+ y: number;
31
+ width: number | undefined;
32
+ height: number | undefined;
33
+ paint: CanvasPaint;
34
+ alt: string | undefined;
35
+ };
36
+ calculateLayout(constraints: BoxConstraints, offset: Offset, _ctx: LayoutContext): Size;
37
+ }
38
+ export {};
@@ -0,0 +1,47 @@
1
+ import { extentSpecs, resolveSize, } from "../layout/box-constraints.js";
2
+ import { SizedPDFElement } from "./pdf-element.js";
3
+ /**
4
+ * A box in the layout that the caller draws into.
5
+ *
6
+ * It has no children and no intrinsic size, so unlike an image it FILLS what it is offered when no
7
+ * size is given - there is nothing to derive one from. That is also why the callback is handed the
8
+ * resolved size: a drawing written against `size.width` works at any box.
9
+ */
10
+ export class CanvasElement extends SizedPDFElement {
11
+ constructor(params) {
12
+ super({ x: 0, y: 0, width: params.width, height: params.height });
13
+ const { paint, alt, widthFactor, heightFactor, ...sizing } = params;
14
+ this.paintFn = paint;
15
+ this.alt = alt;
16
+ this.widthFactor = widthFactor;
17
+ this.heightFactor = heightFactor;
18
+ this.sizing = sizing;
19
+ this.requested = { width: params.width, height: params.height };
20
+ }
21
+ getProps() {
22
+ return {
23
+ x: this.x,
24
+ y: this.y,
25
+ width: this.width,
26
+ height: this.height,
27
+ paint: this.paintFn,
28
+ alt: this.alt,
29
+ };
30
+ }
31
+ calculateLayout(constraints, offset, _ctx) {
32
+ this.x = offset.x;
33
+ this.y = offset.y;
34
+ const specs = extentSpecs({
35
+ ...this.sizing,
36
+ width: this.requested.width,
37
+ height: this.requested.height,
38
+ widthFactor: this.widthFactor,
39
+ heightFactor: this.heightFactor,
40
+ });
41
+ const resolved = resolveSize(specs.width, specs.height, this.sizing.aspectRatio, constraints);
42
+ const bounds = resolved.constraints;
43
+ this.width = resolved.width ?? (bounds.hasBoundedWidth ? bounds.maxWidth : 0);
44
+ this.height = resolved.height ?? (bounds.hasBoundedHeight ? bounds.maxHeight : 0);
45
+ return { width: this.width, height: this.height };
46
+ }
47
+ }
@@ -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
  /**
@@ -54,6 +74,8 @@ export declare class ContainerElement extends SizedPDFElement implements Fragmen
54
74
  * every existing layout untouched.
55
75
  */
56
76
  needsBoundedMain(horizontal: boolean): boolean;
77
+ /** A Column stacks vertically, so widths overlap and heights add up. */
78
+ minIntrinsicMain(horizontal: boolean, ctx: LayoutContext): number;
57
79
  calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
58
80
  getProps(): ContainerElementParams;
59
81
  }