@jasy/pdf 1.0.0-beta.3 → 1.0.0-beta.5

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 (246) hide show
  1. package/dist/api/color.d.ts +1 -24
  2. package/dist/api/color.js +2 -278
  3. package/dist/api/content.d.ts +10 -5
  4. package/dist/api/content.js +29 -13
  5. package/dist/api/descriptor.js +30 -3
  6. package/dist/api/flow.d.ts +55 -0
  7. package/dist/api/flow.js +71 -0
  8. package/dist/api/font-url.d.ts +1 -2
  9. package/dist/api/font-url.js +8 -74
  10. package/dist/api/forms.d.ts +3 -0
  11. package/dist/api/forms.js +3 -0
  12. package/dist/api/gradient.d.ts +1 -53
  13. package/dist/api/gradient.js +2 -97
  14. package/dist/api/index.d.ts +4 -0
  15. package/dist/api/index.js +3 -0
  16. package/dist/api/layout.d.ts +6 -0
  17. package/dist/api/layout.js +9 -3
  18. package/dist/api/page-builder.d.ts +4 -0
  19. package/dist/api/page-builder.js +4 -0
  20. package/dist/api/structure.d.ts +10 -2
  21. package/dist/api/structure.js +9 -6
  22. package/dist/api/table.d.ts +5 -3
  23. package/dist/api/table.js +101 -22
  24. package/dist/api/text.d.ts +8 -8
  25. package/dist/api/text.js +6 -0
  26. package/dist/canvas/painter.d.ts +2 -2
  27. package/dist/canvas/painter.js +2 -2
  28. package/dist/common/color-input.d.ts +24 -0
  29. package/dist/common/color-input.js +278 -0
  30. package/dist/common/diagnostics.d.ts +61 -0
  31. package/dist/common/diagnostics.js +77 -0
  32. package/dist/common/gradient.d.ts +53 -0
  33. package/dist/common/gradient.js +97 -0
  34. package/dist/edit/acroform-reader.js +7 -7
  35. package/dist/edit/appearance.js +3 -3
  36. package/dist/edit/attachments.d.ts +52 -0
  37. package/dist/edit/attachments.js +178 -0
  38. package/dist/edit/document.d.ts +38 -8
  39. package/dist/edit/document.js +208 -51
  40. package/dist/edit/fill.js +8 -23
  41. package/dist/edit/filters.d.ts +15 -0
  42. package/dist/edit/filters.js +157 -0
  43. package/dist/edit/index.d.ts +4 -1
  44. package/dist/edit/index.js +3 -1
  45. package/dist/edit/objects.d.ts +3 -4
  46. package/dist/edit/objects.js +5 -18
  47. package/dist/elements/canvas-element.d.ts +3 -0
  48. package/dist/elements/canvas-element.js +14 -3
  49. package/dist/elements/container-element.d.ts +8 -1
  50. package/dist/elements/container-element.js +49 -17
  51. package/dist/elements/forms/checkbox-element.d.ts +2 -0
  52. package/dist/elements/forms/checkbox-element.js +12 -3
  53. package/dist/elements/forms/choice-element.d.ts +6 -1
  54. package/dist/elements/forms/choice-element.js +17 -6
  55. package/dist/elements/forms/field-font.d.ts +8 -0
  56. package/dist/elements/forms/field-font.js +5 -0
  57. package/dist/elements/forms/push-button-element.d.ts +6 -1
  58. package/dist/elements/forms/push-button-element.js +16 -4
  59. package/dist/elements/forms/radio-element.d.ts +2 -0
  60. package/dist/elements/forms/radio-element.js +12 -3
  61. package/dist/elements/forms/signature-element.d.ts +6 -1
  62. package/dist/elements/forms/signature-element.js +16 -4
  63. package/dist/elements/forms/text-field-element.d.ts +6 -1
  64. package/dist/elements/forms/text-field-element.js +16 -4
  65. package/dist/elements/image-element.d.ts +70 -21
  66. package/dist/elements/image-element.js +173 -52
  67. package/dist/elements/index.d.ts +3 -0
  68. package/dist/elements/index.js +3 -0
  69. package/dist/elements/layout/anchor-element.d.ts +8 -10
  70. package/dist/elements/layout/anchor-element.js +10 -18
  71. package/dist/elements/layout/bookmark-element.d.ts +9 -11
  72. package/dist/elements/layout/bookmark-element.js +11 -20
  73. package/dist/elements/layout/column-break-element.d.ts +17 -0
  74. package/dist/elements/layout/column-break-element.js +26 -0
  75. package/dist/elements/layout/default-text-style-element.d.ts +11 -13
  76. package/dist/elements/layout/default-text-style-element.js +14 -31
  77. package/dist/elements/layout/deferred-element.d.ts +20 -4
  78. package/dist/elements/layout/deferred-element.js +41 -9
  79. package/dist/elements/layout/expanded-element.d.ts +11 -0
  80. package/dist/elements/layout/expanded-element.js +53 -18
  81. package/dist/elements/layout/flow-element.d.ts +109 -0
  82. package/dist/elements/layout/flow-element.js +317 -0
  83. package/dist/elements/layout/keep-together-element.d.ts +8 -18
  84. package/dist/elements/layout/keep-together-element.js +33 -45
  85. package/dist/elements/layout/link-element.d.ts +8 -13
  86. package/dist/elements/layout/link-element.js +9 -19
  87. package/dist/elements/layout/padding-element.d.ts +18 -1
  88. package/dist/elements/layout/padding-element.js +63 -28
  89. package/dist/elements/layout/page-break-element.d.ts +4 -1
  90. package/dist/elements/layout/page-break-element.js +6 -1
  91. package/dist/elements/layout/page-builder-element.d.ts +5 -0
  92. package/dist/elements/layout/page-builder-element.js +13 -2
  93. package/dist/elements/layout/positioned-element.d.ts +23 -4
  94. package/dist/elements/layout/positioned-element.js +42 -12
  95. package/dist/elements/layout/proxy-element.d.ts +47 -0
  96. package/dist/elements/layout/proxy-element.js +106 -0
  97. package/dist/elements/layout/region-element.d.ts +53 -0
  98. package/dist/elements/layout/region-element.js +108 -0
  99. package/dist/elements/layout/repeating-header-element.d.ts +9 -0
  100. package/dist/elements/layout/repeating-header-element.js +55 -13
  101. package/dist/elements/layout/rotated-box-element.d.ts +6 -0
  102. package/dist/elements/layout/rotated-box-element.js +27 -14
  103. package/dist/elements/layout/rotated-element.d.ts +9 -0
  104. package/dist/elements/layout/rotated-element.js +33 -2
  105. package/dist/elements/layout/struct-group.d.ts +9 -17
  106. package/dist/elements/layout/struct-group.js +10 -35
  107. package/dist/elements/line-element.d.ts +1 -0
  108. package/dist/elements/line-element.js +6 -0
  109. package/dist/elements/page-element.d.ts +10 -3
  110. package/dist/elements/page-element.js +39 -31
  111. package/dist/elements/pdf-document-element.d.ts +3 -0
  112. package/dist/elements/pdf-document-element.js +7 -0
  113. package/dist/elements/pdf-element.d.ts +105 -16
  114. package/dist/elements/pdf-element.js +118 -19
  115. package/dist/elements/rectangle-element.d.ts +22 -2
  116. package/dist/elements/rectangle-element.js +119 -55
  117. package/dist/elements/row-element.d.ts +17 -12
  118. package/dist/elements/row-element.js +89 -9
  119. package/dist/elements/svg-element.d.ts +30 -6
  120. package/dist/elements/svg-element.js +91 -26
  121. package/dist/elements/text-element.d.ts +48 -13
  122. package/dist/elements/text-element.js +161 -116
  123. package/dist/forms/acroform.d.ts +9 -1
  124. package/dist/forms/acroform.js +83 -34
  125. package/dist/forms/appearance.d.ts +17 -6
  126. package/dist/forms/appearance.js +18 -16
  127. package/dist/ir/display-list.d.ts +18 -3
  128. package/dist/layout/box-constraints.d.ts +17 -0
  129. package/dist/layout/box-constraints.js +31 -0
  130. package/dist/layout/collect-images.d.ts +6 -4
  131. package/dist/layout/collect-images.js +14 -8
  132. package/dist/layout/flex-item.d.ts +11 -0
  133. package/dist/layout/flex-item.js +51 -0
  134. package/dist/layout/fragmentation.d.ts +23 -9
  135. package/dist/layout/fragmentation.js +62 -19
  136. package/dist/layout/layout-cache.d.ts +53 -0
  137. package/dist/layout/layout-cache.js +110 -0
  138. package/dist/platform/browser-fs.d.ts +1 -2
  139. package/dist/platform/browser-fs.js +3 -6
  140. package/dist/platform/node-fs.d.ts +0 -1
  141. package/dist/platform/node-fs.js +0 -4
  142. package/dist/renderer/anchor-renderer.d.ts +2 -1
  143. package/dist/renderer/anchor-renderer.js +5 -4
  144. package/dist/renderer/bookmark-renderer.d.ts +2 -1
  145. package/dist/renderer/bookmark-renderer.js +5 -4
  146. package/dist/renderer/canvas-renderer.d.ts +2 -1
  147. package/dist/renderer/canvas-renderer.js +5 -2
  148. package/dist/renderer/column-break-renderer.d.ts +8 -0
  149. package/dist/renderer/column-break-renderer.js +15 -0
  150. package/dist/renderer/container-renderer.d.ts +2 -1
  151. package/dist/renderer/container-renderer.js +6 -5
  152. package/dist/renderer/default-text-style-renderer.d.ts +2 -1
  153. package/dist/renderer/default-text-style-renderer.js +2 -3
  154. package/dist/renderer/deferred-renderer.d.ts +2 -1
  155. package/dist/renderer/deferred-renderer.js +2 -3
  156. package/dist/renderer/emoji-image.js +3 -1
  157. package/dist/renderer/expanded-renderer.d.ts +2 -1
  158. package/dist/renderer/expanded-renderer.js +2 -3
  159. package/dist/renderer/flow-renderer.d.ts +16 -0
  160. package/dist/renderer/flow-renderer.js +21 -0
  161. package/dist/renderer/form-field-renderer.d.ts +2 -1
  162. package/dist/renderer/form-field-renderer.js +16 -3
  163. package/dist/renderer/image-renderer.d.ts +2 -1
  164. package/dist/renderer/image-renderer.js +33 -53
  165. package/dist/renderer/index.d.ts +2 -0
  166. package/dist/renderer/index.js +2 -0
  167. package/dist/renderer/keep-together-renderer.d.ts +2 -1
  168. package/dist/renderer/keep-together-renderer.js +2 -3
  169. package/dist/renderer/line-renderer.d.ts +2 -1
  170. package/dist/renderer/line-renderer.js +7 -2
  171. package/dist/renderer/link-renderer.d.ts +2 -1
  172. package/dist/renderer/link-renderer.js +36 -5
  173. package/dist/renderer/padding-renderer.d.ts +2 -1
  174. package/dist/renderer/padding-renderer.js +5 -5
  175. package/dist/renderer/page-break-renderer.d.ts +6 -5
  176. package/dist/renderer/page-break-renderer.js +14 -7
  177. package/dist/renderer/page-builder-renderer.d.ts +2 -1
  178. package/dist/renderer/page-builder-renderer.js +2 -3
  179. package/dist/renderer/page-renderer.js +30 -10
  180. package/dist/renderer/pdf-backend.d.ts +4 -3
  181. package/dist/renderer/pdf-backend.js +14 -7
  182. package/dist/renderer/pdf-document-renderer.d.ts +1 -8
  183. package/dist/renderer/pdf-document-renderer.js +17 -17
  184. package/dist/renderer/pdf-renderer.js +20 -11
  185. package/dist/renderer/positioned-renderer.d.ts +18 -1
  186. package/dist/renderer/positioned-renderer.js +43 -6
  187. package/dist/renderer/rectangle-renderer.d.ts +2 -1
  188. package/dist/renderer/rectangle-renderer.js +18 -8
  189. package/dist/renderer/repeating-header-renderer.d.ts +2 -1
  190. package/dist/renderer/repeating-header-renderer.js +6 -5
  191. package/dist/renderer/rotated-renderer.d.ts +2 -1
  192. package/dist/renderer/rotated-renderer.js +9 -5
  193. package/dist/renderer/row-renderer.d.ts +2 -1
  194. package/dist/renderer/row-renderer.js +6 -5
  195. package/dist/renderer/struct-group-renderer.d.ts +2 -1
  196. package/dist/renderer/struct-group-renderer.js +2 -3
  197. package/dist/renderer/svg-renderer.d.ts +2 -1
  198. package/dist/renderer/svg-renderer.js +8 -2
  199. package/dist/renderer/text-renderer.d.ts +4 -6
  200. package/dist/renderer/text-renderer.js +114 -127
  201. package/dist/svg/gradients.js +5 -0
  202. package/dist/svg/index.d.ts +1 -1
  203. package/dist/svg/index.js +36 -6
  204. package/dist/svg/style.d.ts +2 -0
  205. package/dist/svg/style.js +12 -1
  206. package/dist/text/advance.d.ts +4 -0
  207. package/dist/text/advance.js +17 -4
  208. package/dist/text/bidi.d.ts +0 -2
  209. package/dist/text/bidi.js +0 -10
  210. package/dist/text/glyph-coverage.d.ts +1 -1
  211. package/dist/text/glyph-coverage.js +7 -3
  212. package/dist/text/hyphen-breaks.d.ts +17 -0
  213. package/dist/text/hyphen-breaks.js +54 -0
  214. package/dist/text/line-breaker.d.ts +55 -29
  215. package/dist/text/line-breaker.js +453 -188
  216. package/dist/text/line-metrics.d.ts +8 -3
  217. package/dist/text/line-metrics.js +11 -2
  218. package/dist/text/pdf-text-string.d.ts +21 -0
  219. package/dist/text/pdf-text-string.js +114 -0
  220. package/dist/text/text-decoration.d.ts +5 -5
  221. package/dist/text/text-decoration.js +5 -5
  222. package/dist/text/word-splitting.d.ts +15 -2
  223. package/dist/text/word-splitting.js +45 -13
  224. package/dist/utils/afm-parser.d.ts +2 -0
  225. package/dist/utils/afm-parser.js +10 -1
  226. package/dist/utils/dest-registry.d.ts +1 -1
  227. package/dist/utils/dest-registry.js +3 -2
  228. package/dist/utils/fetch-bytes.d.ts +19 -0
  229. package/dist/utils/fetch-bytes.js +79 -0
  230. package/dist/utils/flex-layout.d.ts +16 -14
  231. package/dist/utils/flex-layout.js +427 -217
  232. package/dist/utils/font-metrics.d.ts +4 -3
  233. package/dist/utils/image-helper.d.ts +59 -11
  234. package/dist/utils/image-helper.js +209 -81
  235. package/dist/utils/pdf-object-manager.d.ts +17 -6
  236. package/dist/utils/pdf-object-manager.js +70 -20
  237. package/dist/utils/renderer-registry.d.ts +49 -2
  238. package/dist/utils/renderer-registry.js +40 -0
  239. package/dist/utils/struct-tree.d.ts +24 -0
  240. package/dist/utils/struct-tree.js +61 -9
  241. package/dist/utils/ttf-parser.d.ts +4 -0
  242. package/dist/utils/ttf-parser.js +30 -6
  243. package/dist/validators/element-validator.js +2 -1
  244. package/package.json +1 -1
  245. package/dist/elements/layout/sized-container-element.d.ts +0 -14
  246. package/dist/elements/layout/sized-container-element.js +0 -32
@@ -1,5 +1,6 @@
1
+ import { coverText } from "../text/glyph-coverage.js";
1
2
  import { NORMAL_STYLE, escName, num2, pdfColor } from "./pdf.js";
2
- import { checkboxOff, checkboxOn, listBoxFace, pushButtonFace, radioOff, radioOn, signatureFace, fieldDrawSize, textFieldFace, wrapFieldValue, } from "./appearance.js";
3
+ import { HELV, checkboxOff, checkboxOn, listBoxFace, pushButtonFace, radioOff, radioOn, signatureFace, fieldDrawSize, textFieldFace, wrapFieldValue, } from "./appearance.js";
3
4
  // AcroForm field flags (/Ff), by 1-based bit position per the PDF spec.
4
5
  const FF_READ_ONLY = 1 << 0; // bit 1
5
6
  const FF_REQUIRED = 1 << 1; // bit 2: must be filled in before the form is submitted
@@ -28,8 +29,12 @@ function boxChrome(node, extraMK = "") {
28
29
  out += ` /BS << /W ${num2(style.borderWidth)} /S /S >>`;
29
30
  return out;
30
31
  }
31
- /** The annotation flags (/F). Bit 2 = Hidden, bit 3 = Print. A widget prints by default; `hidden`
32
- * removes it from screen AND print, `print: false` keeps it on screen only. */
32
+ /** A widget in the structure tree (accessible tagging) names its slot in the ParentTree. */
33
+ function withStructParent(dict, structParent) {
34
+ return structParent === undefined
35
+ ? dict
36
+ : dict.replace(/^<< /, `<< /StructParent ${structParent} `);
37
+ }
33
38
  function annotFlags(f) {
34
39
  if (f.hidden)
35
40
  return 2;
@@ -162,9 +167,9 @@ function actionDict(a, om) {
162
167
  case "submit":
163
168
  // /Flags 4 = ExportFormat: post the field values as HTML form data rather than FDF, which is what
164
169
  // an ordinary web endpoint expects.
165
- return `/A << /S /SubmitForm /F << /FS /URL /F ${om.pdfString(a.url)} >> /Flags 4 >>`;
170
+ return `/A << /S /SubmitForm /F << /FS /URL /F ${om.pdfUri(a.url)} >> /Flags 4 >>`;
166
171
  case "url":
167
- return `/A << /S /URI /URI ${om.pdfString(a.url)} >>`;
172
+ return `/A << /S /URI /URI ${om.pdfUri(a.url)} >>`;
168
173
  }
169
174
  }
170
175
  /**
@@ -196,27 +201,66 @@ export class AcroFormCollector {
196
201
  drawSize(style, height, capHeight) {
197
202
  return fieldDrawSize(style.fontSize, height, style.borderWidth, capHeight);
198
203
  }
204
+ /**
205
+ * The font a field's baked appearance draws its text in: the font the field inherits when it is
206
+ * EMBEDDED, else the standard Helvetica. PDF/A and PDF/UA require every font that draws to be
207
+ * embedded, and a document that embeds its fonts means that. The /DA keeps naming Helvetica - it is
208
+ * what a viewer redraws with once someone types, and a subset font could not draw the new characters.
209
+ * Measuring and drawing both take the font from here.
210
+ */
211
+ faceFont(node, om) {
212
+ const inherited = node.font;
213
+ if (inherited && om.isCustomFont(inherited.family, inherited.style)) {
214
+ const { family, style } = inherited;
215
+ const resource = om.getCustomFontResource(family, style);
216
+ return {
217
+ family,
218
+ style,
219
+ // Drawn SHAPED (ligatures, joining forms) - the shaping `getStringWidth` measures with.
220
+ face: {
221
+ res: `F${resource.fontIndex}`,
222
+ encode: (text) => `<${om.encodeCustomText(family, text, style)}>`,
223
+ },
224
+ resources: `/Font << /F${resource.fontIndex} ${resource.resourceIndex} 0 R >>`,
225
+ // A glyph the font lacks would draw as .notdef, which PDF/A forbids.
226
+ cover: (text) => {
227
+ const { text: drawable, dropped } = coverText(text, family, style, om, false);
228
+ if (dropped.length > 0)
229
+ om.reportMissingGlyph?.(dropped);
230
+ return drawable;
231
+ },
232
+ };
233
+ }
234
+ return {
235
+ family: "Helvetica",
236
+ style: NORMAL_STYLE,
237
+ face: HELV,
238
+ resources: `/Font << /Helv ${this.helv(om)} 0 R >>`,
239
+ cover: (text) => text,
240
+ };
241
+ }
199
242
  /** Measure one line for the appearance generator. */
200
- line(om, text, size) {
201
- return { text, width: om.getStringWidth(text, "Helvetica", size, NORMAL_STYLE) };
243
+ line(om, font, text, size) {
244
+ return { text, width: om.getStringWidth(text, font.family, size, font.style) };
202
245
  }
203
246
  /** Bake a text field's value into an appearance stream; returns the XObject number. */
204
247
  bakeText(node, om) {
205
248
  const f = node.field;
206
249
  if (f.kind !== "text")
207
250
  throw new Error("bakeText: not a text field");
208
- const { capHeight } = om.getFontDecoration("Helvetica", NORMAL_STYLE);
251
+ const font = this.faceFont(node, om);
252
+ const { capHeight } = om.getFontDecoration(font.family, font.style);
209
253
  const size = this.drawSize(node.style, node.height, capHeight);
210
254
  // A password field shows the mask, never the characters - the value still lives in /V.
211
- const value = f.password ? "•".repeat([...(f.value ?? "")].length) : (f.value ?? "");
255
+ const value = font.cover(f.password ? "•".repeat([...(f.value ?? "")].length) : (f.value ?? ""));
212
256
  const innerWidth = Math.max(1, node.width - 2 * (2 + node.style.borderWidth));
213
257
  let lines = [];
214
258
  if (value) {
215
- const texts = wrapFieldValue(value, "Helvetica", size, NORMAL_STYLE, innerWidth, om, f.multiline ?? false);
216
- lines = texts.map((t) => this.line(om, t, size));
259
+ const texts = wrapFieldValue(value, font.family, size, font.style, innerWidth, om, f.multiline ?? false);
260
+ lines = texts.map((t) => this.line(om, font, t, size));
217
261
  }
218
- const face = textFieldFace(node.width, node.height, node.style, lines, capHeight, size, "Helv", f.multiline ?? false, f.align);
219
- return om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, face, `/Font << /Helv ${this.helv(om)} 0 R >>`);
262
+ const face = textFieldFace(node.width, node.height, node.style, lines, capHeight, size, font.face, f.multiline ?? false, f.align);
263
+ return om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, face, font.resources);
220
264
  }
221
265
  /** Bake a choice field: a combo shows its current value on one line, a list box shows its options
222
266
  * with the selected rows highlighted. */
@@ -224,24 +268,27 @@ export class AcroFormCollector {
224
268
  const f = node.field;
225
269
  if (f.kind !== "choice")
226
270
  throw new Error("bakeChoice: not a choice");
227
- const { capHeight } = om.getFontDecoration("Helvetica", NORMAL_STYLE);
271
+ const font = this.faceFont(node, om);
272
+ const { capHeight } = om.getFontDecoration(font.family, font.style);
228
273
  const size = this.drawSize(node.style, node.height, capHeight);
229
274
  const shown = (v) => f.options.find((o) => o.value === v)?.label ?? v;
230
275
  const chosen = choiceSelection(f);
231
276
  const selected = new Set(chosen);
232
277
  let face;
233
278
  if (f.combo) {
234
- const current = chosen.length ? [this.line(om, shown(chosen[0]), size)] : [];
235
- face = textFieldFace(node.width, node.height, node.style, current, capHeight, size, "Helv", false, f.align);
279
+ const current = chosen.length
280
+ ? [this.line(om, font, font.cover(shown(chosen[0])), size)]
281
+ : [];
282
+ face = textFieldFace(node.width, node.height, node.style, current, capHeight, size, font.face, false, f.align);
236
283
  }
237
284
  else {
238
285
  const rows = f.options.map((o) => ({
239
- ...this.line(om, o.label ?? o.value, size),
286
+ ...this.line(om, font, font.cover(o.label ?? o.value), size),
240
287
  selected: selected.has(o.value),
241
288
  }));
242
- face = listBoxFace(node.width, node.height, node.style, rows, capHeight, size, "Helv", f.align);
289
+ face = listBoxFace(node.width, node.height, node.style, rows, capHeight, size, font.face, f.align);
243
290
  }
244
- return om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, face, `/Font << /Helv ${this.helv(om)} 0 R >>`);
291
+ return om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, face, font.resources);
245
292
  }
246
293
  get isEmpty() {
247
294
  return this.fieldRefs.length === 0;
@@ -278,9 +325,9 @@ export class AcroFormCollector {
278
325
  }
279
326
  /** Emit the widget object for one form-field IR node, register it as a field, and return its object
280
327
  * number so the PageRenderer can add it to that page's /Annots. */
281
- addField(node, om) {
328
+ addField(node, om, structParent) {
282
329
  if (node.field.kind === "radio")
283
- return this.addRadio(node, om);
330
+ return this.addRadio(node, om, structParent);
284
331
  this.claimName(node.field.name, node.field.kind);
285
332
  let dict;
286
333
  if (node.field.kind === "checkbox") {
@@ -307,7 +354,7 @@ export class AcroFormCollector {
307
354
  if (!this.bake)
308
355
  this.needAppearances = true;
309
356
  }
310
- const objNum = om.addObject(dict);
357
+ const objNum = om.addObject(withStructParent(dict, structParent));
311
358
  this.fieldRefs.push(objNum);
312
359
  return objNum;
313
360
  }
@@ -318,13 +365,15 @@ export class AcroFormCollector {
318
365
  const f = node.field;
319
366
  if (f.kind !== "pushbutton")
320
367
  throw new Error("buildPushButtonWidget: not a push button");
321
- const { capHeight } = om.getFontDecoration("Helvetica", NORMAL_STYLE);
368
+ const font = this.faceFont(node, om);
369
+ const { capHeight } = om.getFontDecoration(font.family, font.style);
322
370
  // `fontSize: 0` means auto-size, here as everywhere: resolve it before measuring or drawing.
323
371
  const size = this.drawSize(node.style, node.height, capHeight);
324
- const width = om.getStringWidth(f.label, "Helvetica", size, NORMAL_STYLE);
325
- const fontNum = this.helv(om);
326
- const face = pushButtonFace(node.width, node.height, node.style, f.label, width, capHeight, "Helv", size);
327
- const apRef = om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, face, `/Font << /Helv ${fontNum} 0 R >>`);
372
+ const caption = font.cover(f.label);
373
+ const width = om.getStringWidth(caption, font.family, size, font.style);
374
+ this.helv(om); // the /DA below names it
375
+ const face = pushButtonFace(node.width, node.height, node.style, caption, width, capHeight, font.face, size);
376
+ const apRef = om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, face, font.resources);
328
377
  const parts = [`/Type /Annot /Subtype /Widget /FT /Btn`, `/T ${om.pdfString(f.name)}`];
329
378
  if (f.tooltip !== undefined)
330
379
  parts.push(`/TU ${om.pdfString(f.tooltip)}`);
@@ -343,7 +392,7 @@ export class AcroFormCollector {
343
392
  /** A radio button: one KID widget of its group's shared field. The group field is reserved on first
344
393
  * sight (and added to /Fields); each button adds itself to that field's /Kids. Returns the kid widget
345
394
  * number, which goes into the PAGE /Annots (the parent field does not). */
346
- addRadio(node, om) {
395
+ addRadio(node, om, structParent) {
347
396
  const f = node.field;
348
397
  if (f.kind !== "radio")
349
398
  throw new Error("addRadio: not a radio");
@@ -375,7 +424,7 @@ export class AcroFormCollector {
375
424
  `/AS /${escName(as)}`,
376
425
  `/DA ${om.pdfString(`/ZaDb 0 Tf ${pdfColor(node.style.color)} rg`)}`,
377
426
  ];
378
- const kidNum = om.addObject(`<< ${parts.join(" ")}${boxChrome(node)} >>`);
427
+ const kidNum = om.addObject(withStructParent(`<< ${parts.join(" ")}${boxChrome(node)} >>`, structParent));
379
428
  g.kids.push(kidNum);
380
429
  return kidNum;
381
430
  }
@@ -386,12 +435,12 @@ export class AcroFormCollector {
386
435
  if (f.kind !== "signature")
387
436
  throw new Error("buildSignatureWidget: not a signature");
388
437
  this.hasSignature = true;
389
- const label = f.label ?? "";
390
- const { capHeight } = om.getFontDecoration("Helvetica", NORMAL_STYLE);
438
+ const font = this.faceFont(node, om);
439
+ const label = font.cover(f.label ?? "");
440
+ const { capHeight } = om.getFontDecoration(font.family, font.style);
391
441
  const size = this.drawSize(node.style, node.height, capHeight);
392
- const labelWidth = label ? om.getStringWidth(label, "Helvetica", size, NORMAL_STYLE) : 0;
393
- const fontNum = this.helv(om);
394
- const apRef = om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, signatureFace(node.width, node.height, node.style, label, labelWidth, "Helv", size), `/Font << /Helv ${fontNum} 0 R >>`);
442
+ const labelWidth = label ? om.getStringWidth(label, font.family, size, font.style) : 0;
443
+ const apRef = om.addFormXObject(`[0 0 ${num2(node.width)} ${num2(node.height)}]`, signatureFace(node.width, node.height, node.style, label, labelWidth, font.face, size), font.resources);
395
444
  const parts = [`/Type /Annot /Subtype /Widget /FT /Sig`, `/T ${om.pdfString(f.name)}`];
396
445
  if (f.tooltip !== undefined)
397
446
  parts.push(`/TU ${om.pdfString(f.tooltip)}`);
@@ -1,6 +1,17 @@
1
1
  import type { FieldAlign, FieldStyle } from "./field.js";
2
2
  import type { FontMetrics } from "../utils/font-metrics.js";
3
3
  import type { FontStyle } from "../utils/pdf-object-manager.js";
4
+ /**
5
+ * The font an appearance draws its text in: its resource name inside the appearance stream's own
6
+ * /Resources, and how a string becomes a `Tj` operand in it. One object, so the caller that picks the
7
+ * font also decides its encoding - a WinAnsi literal for Helvetica, glyph ids for an embedded font.
8
+ */
9
+ export interface FaceFont {
10
+ res: string;
11
+ encode(text: string): string;
12
+ }
13
+ /** The standard Helvetica (`/Helv`) a field's /DA names: WinAnsi literal strings. */
14
+ export declare const HELV: FaceFont;
4
15
  /** The unchecked appearance: just the box. */
5
16
  export declare function checkboxOff(w: number, h: number, style: FieldStyle): string;
6
17
  /** The checked appearance: the box plus a vector checkmark (font-free, so it renders everywhere). */
@@ -12,16 +23,16 @@ export declare function radioOn(w: number, h: number, style: FieldStyle): string
12
23
  /**
13
24
  * A push button's face: the box plus its caption, centred. The caller measures the caption (this module
14
25
  * stays free of font metrics and the writer) and passes `captionWidth` in points and `capHeight` as an em
15
- * fraction; the baseline is placed so the CAPITALS sit optically centred, not the em box. `fontRes` is
16
- * the font's resource name inside the appearance stream's own /Resources.
26
+ * fraction; the baseline is placed so the CAPITALS sit optically centred, not the em box. `font` is
27
+ * what the caption is drawn in.
17
28
  */
18
- export declare function pushButtonFace(w: number, h: number, style: FieldStyle, caption: string, captionWidth: number, capHeight: number, fontRes: string, size: number): string;
29
+ export declare function pushButtonFace(w: number, h: number, style: FieldStyle, caption: string, captionWidth: number, capHeight: number, font: FaceFont, size: number): string;
19
30
  /**
20
31
  * An EMPTY signature field: the box, a signing rule across the lower third, and an optional hint centred
21
32
  * just above it. This is the placeholder look - once someone actually signs, the signing tool replaces
22
33
  * this appearance with its own.
23
34
  */
24
- export declare function signatureFace(w: number, h: number, style: FieldStyle, label: string, labelWidth: number, fontRes: string, size: number): string;
35
+ export declare function signatureFace(w: number, h: number, style: FieldStyle, label: string, labelWidth: number, font: FaceFont, size: number): string;
25
36
  /** One pre-measured line of field text. The caller owns wrapping + measuring, so this module stays free
26
37
  * of font metrics (and can be reused verbatim when filling an existing PDF). */
27
38
  /**
@@ -50,9 +61,9 @@ export interface FieldLine {
50
61
  * single line sits optically centred; a multi-line field stacks from the top, both measured off the
51
62
  * font's cap height rather than a guessed ratio.
52
63
  */
53
- export declare function textFieldFace(w: number, h: number, style: FieldStyle, lines: FieldLine[], capHeight: number, size: number, fontRes: string, multiline: boolean, align?: FieldAlign): string;
64
+ export declare function textFieldFace(w: number, h: number, style: FieldStyle, lines: FieldLine[], capHeight: number, size: number, font: FaceFont, multiline: boolean, align?: FieldAlign): string;
54
65
  /**
55
66
  * A list box, baked: the visible options stacked from the top, with a highlight behind the selected
56
67
  * ones. A combo box (dropdown) uses `textFieldFace` instead - it shows only its current value.
57
68
  */
58
- export declare function listBoxFace(w: number, h: number, style: FieldStyle, lines: FieldLine[], capHeight: number, size: number, fontRes: string, align?: FieldAlign): string;
69
+ export declare function listBoxFace(w: number, h: number, style: FieldStyle, lines: FieldLine[], capHeight: number, size: number, font: FaceFont, align?: FieldAlign): string;
@@ -1,5 +1,7 @@
1
1
  import { escPdf, num2, pdfColor } from "./pdf.js";
2
2
  import { wrapStringIntoLines } from "../text/line-breaker.js";
3
+ /** The standard Helvetica (`/Helv`) a field's /DA names: WinAnsi literal strings. */
4
+ export const HELV = { res: "Helv", encode: (text) => `(${escPdf(text)})` };
3
5
  // Appearance streams (/AP): the actual drawn content of a field's box, in the field's OWN coordinate
4
6
  // space (bottom-left origin, [0 0 w h]). Shared between CREATE (bake the field's look now) and, later,
5
7
  // FILL-existing (regenerate a field's look after setting its value). This is why it is its own module.
@@ -63,34 +65,34 @@ export function radioOn(w, h, style) {
63
65
  /**
64
66
  * A push button's face: the box plus its caption, centred. The caller measures the caption (this module
65
67
  * stays free of font metrics and the writer) and passes `captionWidth` in points and `capHeight` as an em
66
- * fraction; the baseline is placed so the CAPITALS sit optically centred, not the em box. `fontRes` is
67
- * the font's resource name inside the appearance stream's own /Resources.
68
+ * fraction; the baseline is placed so the CAPITALS sit optically centred, not the em box. `font` is
69
+ * what the caption is drawn in.
68
70
  */
69
- export function pushButtonFace(w, h, style, caption, captionWidth, capHeight, fontRes, size) {
71
+ export function pushButtonFace(w, h, style, caption, captionWidth, capHeight, font, size) {
70
72
  // Baseline placed so the CAPITALS sit optically centred in the box, not the em square.
71
73
  const y = (h - capHeight * size) / 2;
72
- const caption_ = caption ? centredText(w, h, style, caption, captionWidth, y, fontRes, size) : "";
74
+ const caption_ = caption ? centredText(w, h, style, caption, captionWidth, y, font, size) : "";
73
75
  return `${box(w, h, style)}\n${caption_}`;
74
76
  }
75
77
  /** One centred, clipped text run inside a field box. Clipping means an over-long caption is cut at the
76
78
  * border instead of bleeding across the page. Shared by the push button and the signature placeholder. */
77
- function centredText(w, h, style, text, textWidth, baselineY, fontRes, size) {
79
+ function centredText(w, h, style, text, textWidth, baselineY, font, size) {
78
80
  return (`q 0 0 ${num2(w)} ${num2(h)} re W n ` +
79
- `BT /${fontRes} ${num2(size)} Tf ${pdfColor(style.color)} rg ` +
80
- `${num2((w - textWidth) / 2)} ${num2(baselineY)} Td (${escPdf(text)}) Tj ET Q`);
81
+ `BT /${font.res} ${num2(size)} Tf ${pdfColor(style.color)} rg ` +
82
+ `${num2((w - textWidth) / 2)} ${num2(baselineY)} Td ${font.encode(text)} Tj ET Q`);
81
83
  }
82
84
  /**
83
85
  * An EMPTY signature field: the box, a signing rule across the lower third, and an optional hint centred
84
86
  * just above it. This is the placeholder look - once someone actually signs, the signing tool replaces
85
87
  * this appearance with its own.
86
88
  */
87
- export function signatureFace(w, h, style, label, labelWidth, fontRes, size) {
89
+ export function signatureFace(w, h, style, label, labelWidth, font, size) {
88
90
  const ruleY = h * 0.3;
89
91
  const inset = w * 0.08;
90
92
  const stroke = style.border ?? style.color;
91
93
  const rule = `${pdfColor(stroke)} RG ${num2(Math.max(0.5, style.borderWidth * 0.75))} w ` +
92
94
  `${num2(inset)} ${num2(ruleY)} m ${num2(w - inset)} ${num2(ruleY)} l S`;
93
- const hint = label ? centredText(w, h, style, label, labelWidth, ruleY + 6, fontRes, size) : "";
95
+ const hint = label ? centredText(w, h, style, label, labelWidth, ruleY + 6, font, size) : "";
94
96
  return `${box(w, h, style)}\n${rule}\n${hint}`;
95
97
  }
96
98
  // Horizontal padding inside a field box, on top of the border - the same small inset Acrobat leaves so
@@ -147,11 +149,11 @@ function alignX(boxWidth, pad, width, align = "left") {
147
149
  * visibly shift on first click. The measurement matches: `getStringWidth` is the plain glyph sum, so
148
150
  * measured still equals drawn.
149
151
  */
150
- function textBlock(lines, style, size, fontRes) {
152
+ function textBlock(lines, style, size, font) {
151
153
  const runs = lines
152
- .map((l) => `1 0 0 1 ${num2(l.x)} ${num2(l.y)} Tm (${escPdf(l.text)}) Tj`)
154
+ .map((l) => `1 0 0 1 ${num2(l.x)} ${num2(l.y)} Tm ${font.encode(l.text)} Tj`)
153
155
  .join("\n");
154
- return `BT /${fontRes} ${num2(size)} Tf ${pdfColor(style.color)} rg\n${runs}\nET`;
156
+ return `BT /${font.res} ${num2(size)} Tf ${pdfColor(style.color)} rg\n${runs}\nET`;
155
157
  }
156
158
  /** Everything a field draws is clipped to its own box, so a long value is cut at the frame rather than
157
159
  * bleeding across the page. */
@@ -163,7 +165,7 @@ function clipped(w, h, ops) {
163
165
  * single line sits optically centred; a multi-line field stacks from the top, both measured off the
164
166
  * font's cap height rather than a guessed ratio.
165
167
  */
166
- export function textFieldFace(w, h, style, lines, capHeight, size, fontRes, multiline, align = "left") {
168
+ export function textFieldFace(w, h, style, lines, capHeight, size, font, multiline, align = "left") {
167
169
  const face = box(w, h, style);
168
170
  if (lines.length === 0)
169
171
  return face;
@@ -176,13 +178,13 @@ export function textFieldFace(w, h, style, lines, capHeight, size, fontRes, mult
176
178
  // Single line: centre the capitals in the box.
177
179
  y: multiline ? h - pad - capHeight * size - i * lineHeight : (h - capHeight * size) / 2,
178
180
  }));
179
- return `${face}\n${clipped(w, h, textBlock(placed, style, size, fontRes))}`;
181
+ return `${face}\n${clipped(w, h, textBlock(placed, style, size, font))}`;
180
182
  }
181
183
  /**
182
184
  * A list box, baked: the visible options stacked from the top, with a highlight behind the selected
183
185
  * ones. A combo box (dropdown) uses `textFieldFace` instead - it shows only its current value.
184
186
  */
185
- export function listBoxFace(w, h, style, lines, capHeight, size, fontRes, align = "left") {
187
+ export function listBoxFace(w, h, style, lines, capHeight, size, font, align = "left") {
186
188
  const pad = FIELD_PAD + style.borderWidth;
187
189
  const lineHeight = size * 1.15;
188
190
  const rowTop = (i) => h - pad - (i + 1) * lineHeight;
@@ -199,6 +201,6 @@ export function listBoxFace(w, h, style, lines, capHeight, size, fontRes, align
199
201
  // Baseline inside the row: lift it off the row bottom by the slack under the capitals.
200
202
  y: rowTop(i) + (lineHeight - capHeight * size) / 2,
201
203
  }));
202
- const inner = [highlights, textBlock(placed, style, size, fontRes)].filter(Boolean).join("\n");
204
+ const inner = [highlights, textBlock(placed, style, size, font)].filter(Boolean).join("\n");
203
205
  return `${box(w, h, style)}\n${clipped(w, h, inner)}`;
204
206
  }
@@ -1,3 +1,4 @@
1
+ import type { ImageColor, Orientation } from "../utils/image-helper.js";
1
2
  import { Color } from "../common/color.js";
2
3
  import { FontStyle } from "../utils/pdf-object-manager.js";
3
4
  import type { FormFieldSpec, FieldStyle } from "../forms/field.js";
@@ -9,8 +10,8 @@ import type { FormFieldSpec, FieldStyle } from "../forms/field.js";
9
10
  * are intentionally "dumb": absolute geometry + semantic style, with no PDF
10
11
  * operators, no font indices, and no object numbers - those are the backend's job.
11
12
  *
12
- * Coordinates are in PDF points (1/72"). Which origin they use is still the
13
- * producer's concern for now; centralizing the Y-flip at this seam is Phase 3.
13
+ * Coordinates are in PDF points (1/72") with the engine's top-left origin. The Y-flip to PDF's
14
+ * bottom-left happens once per page, at this seam (`PdfBackend.flipY`).
14
15
  */
15
16
  /**
16
17
  * Structure tag for accessible (PDF/UA) tagging. `role` is the PDF structure type the content maps to
@@ -62,7 +63,7 @@ export interface Rect {
62
63
  y: number;
63
64
  width: number;
64
65
  height: number;
65
- /** Solid colour, or a gradient whose anchor points are in absolute page coordinates. */
66
+ /** Solid color, or a gradient whose anchor points are in absolute page coordinates. */
66
67
  fill?: Color | Gradient;
67
68
  stroke?: Color;
68
69
  strokeWidth: number;
@@ -94,6 +95,10 @@ export interface Image {
94
95
  imageType: string;
95
96
  /** Flate-compressed DeviceGray alpha channel for a transparent PNG, embedded as the XObject's /SMask. */
96
97
  smask?: string;
98
+ /** What the samples mean (a JPEG keeps the space it was saved in); absent = RGB. */
99
+ color?: ImageColor;
100
+ /** How the stored pixels turn upright inside the placement box (EXIF); absent = as stored. */
101
+ orientation?: Orientation;
97
102
  /** cover/contain fits clip the placement to the element's original frame. */
98
103
  clip?: {
99
104
  x: number;
@@ -140,6 +145,9 @@ export interface Link {
140
145
  height: number;
141
146
  href?: string;
142
147
  dest?: string;
148
+ /** Accessible tagging only: the `/Link` structure element the annotation belongs to, and its text. */
149
+ structKey?: number;
150
+ alt?: string;
143
151
  }
144
152
  /**
145
153
  * A named destination anchor - a jump TARGET for an internal `Link({ to })`. Like `Link` it draws
@@ -177,6 +185,13 @@ export interface FormFieldNode {
177
185
  height: number;
178
186
  field: FormFieldSpec;
179
187
  style: FieldStyle;
188
+ /** Accessible tagging only: the `/Form` structure element the widget belongs to. */
189
+ structKey?: number;
190
+ /** The font the field's text inherits; its baked appearance draws in it when it is embedded. */
191
+ font?: {
192
+ family: string;
193
+ style: FontStyle;
194
+ };
180
195
  }
181
196
  /**
182
197
  * Pushes an affine transform (a `q` + `cm` in the content stream): everything between this and the
@@ -84,6 +84,11 @@ export interface SizingParams {
84
84
  /** width / height. Derives whichever axis is left open; see `resolveSize`. */
85
85
  aspectRatio?: number;
86
86
  }
87
+ /**
88
+ * The width rules of a box, for a piece of it split across pages: it keeps how wide it may be, and
89
+ * its height comes from what it holds on that page (CSS `box-decoration-break: clone`).
90
+ */
91
+ export declare function widthRules(s: SizingParams): SizingParams;
87
92
  /** Split `SizingParams` into the per-axis form `resolveSize` takes. */
88
93
  export declare function extentSpecs(p: SizingParams): {
89
94
  width: ExtentSpec;
@@ -105,6 +110,18 @@ export declare function resolveSize(width: ExtentSpec, height: ExtentSpec, aspec
105
110
  height?: number;
106
111
  constraints: BoxConstraints;
107
112
  };
113
+ /**
114
+ * The open axes of a box that has its own size (an image, an SVG), after `resolveSize`: one pinned axis
115
+ * derives the other from the own ratio; none pinned is the own size, scaled down to `bounds` and never
116
+ * up - the cap is what keeps it on the page.
117
+ */
118
+ export declare function sizeAtOwnRatio(width: number | undefined, height: number | undefined, own: {
119
+ width: number;
120
+ height: number;
121
+ }, bounds: BoxConstraints): {
122
+ width?: number;
123
+ height?: number;
124
+ };
108
125
  /** The size an element resolves to and returns UP the tree. */
109
126
  export interface Size {
110
127
  width: number;
@@ -91,6 +91,20 @@ export function resolveExtent(fixed, factor, boundedMax, hasBounded) {
91
91
  return boundedMax * factor;
92
92
  return undefined;
93
93
  }
94
+ /**
95
+ * The width rules of a box, for a piece of it split across pages: it keeps how wide it may be, and
96
+ * its height comes from what it holds on that page (CSS `box-decoration-break: clone`).
97
+ */
98
+ export function widthRules(s) {
99
+ return {
100
+ width: s.width,
101
+ widthFactor: s.widthFactor,
102
+ minWidth: s.minWidth,
103
+ maxWidth: s.maxWidth,
104
+ minWidthFactor: s.minWidthFactor,
105
+ maxWidthFactor: s.maxWidthFactor,
106
+ };
107
+ }
94
108
  /** Split `SizingParams` into the per-axis form `resolveSize` takes. */
95
109
  export function extentSpecs(p) {
96
110
  return {
@@ -153,3 +167,20 @@ export function resolveSize(width, height, aspectRatio, constraints) {
153
167
  constraints: bounds,
154
168
  };
155
169
  }
170
+ /**
171
+ * The open axes of a box that has its own size (an image, an SVG), after `resolveSize`: one pinned axis
172
+ * derives the other from the own ratio; none pinned is the own size, scaled down to `bounds` and never
173
+ * up - the cap is what keeps it on the page.
174
+ */
175
+ export function sizeAtOwnRatio(width, height, own, bounds) {
176
+ const ratio = own.width / own.height;
177
+ if (width === undefined && height === undefined) {
178
+ // The width clamp does the width; a height cap is turned into a width at the ratio.
179
+ width = bounds.constrainWidth(Math.min(own.width, bounds.maxHeight * ratio));
180
+ }
181
+ if (width !== undefined && height === undefined)
182
+ height = bounds.constrainHeight(width / ratio);
183
+ else if (height !== undefined && width === undefined)
184
+ width = bounds.constrainWidth(height * ratio);
185
+ return { width, height };
186
+ }
@@ -1,9 +1,11 @@
1
1
  import { PDFElement } from "../elements/pdf-element.js";
2
2
  import { ImageElement } from "../elements/image-element.js";
3
+ import { SvgElement } from "../elements/svg-element.js";
3
4
  /**
4
5
  * Walks the element tree (via `getProps`, the same shallow-reflection style as the validator) and
5
- * returns every `ImageElement`. A pre-layout pass uses this to resolve each image's intrinsic pixel
6
- * size asynchronously: layout is synchronous and cannot await jimp, yet it needs the aspect ratio to
7
- * give a width-only image its proportional height. Cheap - images are few and this runs once.
6
+ * returns every image - a bitmap, or an SVG (which needs this only when it comes from a URL). A
7
+ * pre-layout pass uses this to resolve each one's intrinsic size asynchronously: layout is synchronous
8
+ * and cannot await a decoder or a fetch, yet it needs the aspect ratio to give a width-only image its
9
+ * proportional height. Cheap - images are few and this runs once.
8
10
  */
9
- export declare function collectImageElements(root: PDFElement): ImageElement[];
11
+ export declare function collectImageElements(root: PDFElement): (ImageElement | SvgElement)[];
@@ -1,15 +1,17 @@
1
1
  import { PDFElement, hasChildrenProp, hasChildProp } from "../elements/pdf-element.js";
2
2
  import { ImageElement } from "../elements/image-element.js";
3
+ import { SvgElement } from "../elements/svg-element.js";
3
4
  /**
4
5
  * Walks the element tree (via `getProps`, the same shallow-reflection style as the validator) and
5
- * returns every `ImageElement`. A pre-layout pass uses this to resolve each image's intrinsic pixel
6
- * size asynchronously: layout is synchronous and cannot await jimp, yet it needs the aspect ratio to
7
- * give a width-only image its proportional height. Cheap - images are few and this runs once.
6
+ * returns every image - a bitmap, or an SVG (which needs this only when it comes from a URL). A
7
+ * pre-layout pass uses this to resolve each one's intrinsic size asynchronously: layout is synchronous
8
+ * and cannot await a decoder or a fetch, yet it needs the aspect ratio to give a width-only image its
9
+ * proportional height. Cheap - images are few and this runs once.
8
10
  */
9
11
  export function collectImageElements(root) {
10
12
  const out = [];
11
13
  const visit = (el) => {
12
- if (el instanceof ImageElement)
14
+ if (el instanceof ImageElement || el instanceof SvgElement)
13
15
  out.push(el);
14
16
  const props = el.getProps();
15
17
  if (hasChildrenProp(props))
@@ -17,10 +19,10 @@ export function collectImageElements(root) {
17
19
  visit(c);
18
20
  else if (hasChildProp(props))
19
21
  visit(props.child);
20
- // Wrappers that hold a subtree outside `children`/`child`: a Page's header/footer and a
21
- // RepeatingHeaderElement's `body` (a Table with a header wraps its rows there). A
22
- // DeferredElement's `composed` is null until layout runs, so it cannot be reached from this
23
- // pre-layout pass; an image inside a deferred subtree just skips aspect pre-resolution.
22
+ // Wrappers that hold a subtree outside `children`/`child`: a Page's header/footer, a
23
+ // RepeatingHeaderElement's `body` (a Table with a header wraps its rows there), and the `sources` a
24
+ // DeferredElement builds from. What a PageBuilder builds does not exist yet; an image there that
25
+ // cannot tell its size without waiting is reported by its layout (`image-unsized`).
24
26
  const w = props;
25
27
  if (w.header instanceof PDFElement)
26
28
  visit(w.header);
@@ -28,6 +30,10 @@ export function collectImageElements(root) {
28
30
  visit(w.footer);
29
31
  if (w.body instanceof PDFElement)
30
32
  visit(w.body);
33
+ if (Array.isArray(w.sources))
34
+ for (const s of w.sources)
35
+ if (s instanceof PDFElement)
36
+ visit(s);
31
37
  };
32
38
  visit(root);
33
39
  return out;
@@ -0,0 +1,11 @@
1
+ import { FlexiblePDFElement, PDFElement } from "../elements/pdf-element.js";
2
+ import type { CrossAlign } from "./alignment.js";
3
+ /** The `Expanded`/`Spacer` that makes this child flexible, if any. */
4
+ export declare function flexibleOf(child: PDFElement): FlexiblePDFElement | undefined;
5
+ export declare function alignSelfOf(child: PDFElement): CrossAlign | undefined;
6
+ /** CSS `order`, 0 when unset. */
7
+ export declare function orderOf(child: PDFElement): number;
8
+ /** The child is a `Box` with percentage padding. */
9
+ export declare function hasPercentPaddingItem(child: PDFElement): boolean;
10
+ /** CSS `flex-shrink`, 1 when unset (the CSS default). */
11
+ export declare function flexShrinkOf(child: PDFElement): number;
@@ -0,0 +1,51 @@
1
+ import { FlexiblePDFElement } from "../elements/pdf-element.js";
2
+ /**
3
+ * How a Row or Column sees a child as a flex item. A wrapper that only marks its child (a `Link`, an
4
+ * `Anchor`, a `DefaultTextStyle` - see `PDFElement.proxied`) is looked through, as CSS
5
+ * `display: contents` is: an `Expanded`, `alignSelf`, `order` or `flexShrink` inside it acts as if the
6
+ * wrapper were not there. A value set on the wrapper itself wins over one set inside it.
7
+ */
8
+ /**
9
+ * The child, then each element it is transparent for, outermost first. An `Expanded` is one of them:
10
+ * `Expanded(child)` is `flex: 1` on the child, so the child's `alignSelf` and `order` are the item's.
11
+ */
12
+ function* itemChain(child) {
13
+ for (let el = child; el;) {
14
+ yield el;
15
+ el = el.proxied() ?? (el instanceof FlexiblePDFElement ? el.layoutChildren()[0] : undefined);
16
+ }
17
+ }
18
+ /** The `Expanded`/`Spacer` that makes this child flexible, if any. */
19
+ export function flexibleOf(child) {
20
+ for (const el of itemChain(child))
21
+ if (el instanceof FlexiblePDFElement)
22
+ return el;
23
+ return undefined;
24
+ }
25
+ export function alignSelfOf(child) {
26
+ for (const el of itemChain(child))
27
+ if (el.alignSelf !== undefined)
28
+ return el.alignSelf;
29
+ return undefined;
30
+ }
31
+ /** CSS `order`, 0 when unset. */
32
+ export function orderOf(child) {
33
+ for (const el of itemChain(child))
34
+ if (el.order !== undefined)
35
+ return el.order;
36
+ return 0;
37
+ }
38
+ /** The child is a `Box` with percentage padding. */
39
+ export function hasPercentPaddingItem(child) {
40
+ for (const el of itemChain(child))
41
+ if (el.hasPercentPadding())
42
+ return true;
43
+ return false;
44
+ }
45
+ /** CSS `flex-shrink`, 1 when unset (the CSS default). */
46
+ export function flexShrinkOf(child) {
47
+ for (const el of itemChain(child))
48
+ if (el.flexShrink !== undefined)
49
+ return el.flexShrink;
50
+ return 1;
51
+ }