@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
@@ -5,9 +5,9 @@
5
5
  * same contract, differing only in being async. So the failure lands where you asked for the font,
6
6
  * not somewhere inside a later render.
7
7
  *
8
- * `fetch` is used directly: it is in every browser and in Node since 18, so this needs no platform
9
- * port, unlike the filesystem read beside it.
8
+ * The fetch itself - timeout, size ceiling - is `utils/fetch-bytes.ts`, shared with images.
10
9
  */
10
+ import { fetchBytes } from "../utils/fetch-bytes.js";
11
11
  /** Thrown when a font could not be fetched or is not one we can parse. Always says which URL. */
12
12
  export class FontUrlError extends Error {
13
13
  constructor(message) {
@@ -36,82 +36,16 @@ function describeSignature(bytes) {
36
36
  return { ok: false, what: "not a font at all" };
37
37
  return { ok: false, what: "not a font we recognise" };
38
38
  }
39
- /** How long one font may take to arrive before we give up. A hung server must not hang a render. */
39
+ /** How long one font may take to arrive before we give up. */
40
40
  const TIMEOUT_MS = 15000;
41
- /**
42
- * How large a font file may be. Generous on purpose - a full CJK face is tens of megabytes and is a
43
- * legitimate thing to embed - but not unbounded: a URL pointing at the wrong file (a video, a disk
44
- * image) would otherwise be read into memory in full before anyone notices it is not a font.
45
- */
41
+ /** How large a font file may be: generous on purpose - a full CJK face is tens of megabytes. */
46
42
  const MAX_BYTES = 32 * 1024 * 1024;
47
- /** Read the body with a running ceiling, so an oversized response is dropped WHILE it arrives. */
48
- async function readBounded(response, url) {
49
- // `Content-Length` is the cheap early exit; a server may omit or misstate it, so it is a hint only
50
- // and the real ceiling is enforced below while reading.
51
- const declared = Number(response.headers?.get?.("content-length") ?? NaN);
52
- if (Number.isFinite(declared) && declared > MAX_BYTES) {
53
- throw new FontUrlError(`the font at ${url} is ${declared} bytes; the limit is ${MAX_BYTES}`);
54
- }
55
- const body = response.body;
56
- if (!body?.getReader) {
57
- // No streaming body (an older runtime, or a stubbed response): fall back to reading it whole and
58
- // checking afterwards. Weaker, but never worse than not checking at all.
59
- const whole = new Uint8Array(await response.arrayBuffer());
60
- if (whole.length > MAX_BYTES) {
61
- throw new FontUrlError(`the font at ${url} is larger than the ${MAX_BYTES} byte limit`);
62
- }
63
- return whole;
64
- }
65
- const reader = body.getReader();
66
- const chunks = [];
67
- let total = 0;
68
- for (;;) {
69
- const { done, value } = await reader.read();
70
- if (done)
71
- break;
72
- total += value.length;
73
- if (total > MAX_BYTES) {
74
- await reader.cancel();
75
- throw new FontUrlError(`the font at ${url} is larger than the ${MAX_BYTES} byte limit`);
76
- }
77
- chunks.push(value);
78
- }
79
- if (chunks.length === 1)
80
- return chunks[0];
81
- const out = new Uint8Array(total);
82
- let at = 0;
83
- for (const c of chunks) {
84
- out.set(c, at);
85
- at += c.length;
86
- }
87
- return out;
88
- }
89
43
  /** Fetch one font file and hand back its bytes, refusing anything we could not embed. */
90
44
  async function fetchFont(url) {
91
- let response;
92
- try {
93
- response = await fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
94
- }
95
- catch (e) {
96
- const why = e?.name === "TimeoutError" || e?.name === "AbortError"
97
- ? `it did not respond within ${TIMEOUT_MS} ms`
98
- : String(e?.message ?? e);
99
- throw new FontUrlError(`could not fetch the font at ${url}: ${why}`);
100
- }
101
- if (!response.ok) {
102
- throw new FontUrlError(`the font at ${url} could not be fetched: HTTP ${response.status}`);
103
- }
104
- // Everything past here still talks to the network, so a failure mid-body must not escape as a raw
105
- // TypeError - a caller catching FontUrlError would miss it.
106
- let bytes;
107
- try {
108
- bytes = await readBounded(response, url);
109
- }
110
- catch (e) {
111
- if (e instanceof FontUrlError)
112
- throw e;
113
- throw new FontUrlError(`the font at ${url} could not be read: ${String(e?.message ?? e)}`);
114
- }
45
+ const bytes = await fetchBytes(url, "font", FontUrlError, {
46
+ timeoutMs: TIMEOUT_MS,
47
+ maxBytes: MAX_BYTES,
48
+ });
115
49
  if (bytes.length < 4) {
116
50
  throw new FontUrlError(`the font at ${url} is empty`);
117
51
  }
@@ -297,6 +297,9 @@ export interface PushButtonOptions {
297
297
  * A clickable push button (AcroForm /Btn). It holds no value - it fires an action. Its caption is baked
298
298
  * into the appearance, so the button looks the same in every viewer and in print.
299
299
  *
300
+ * Not for PDF/A: PDF/A forbids an action on a form field (ISO 19005-2/-3, 6.4.1), and an action is all a
301
+ * push button does.
302
+ *
300
303
  * ```ts
301
304
  * PushButton({ name: "send", label: "Submit", action: { submit: "https://example.com/f" }, width: 120 })
302
305
  * PushButton({ name: "clear", label: "Reset", action: "reset", width: 90 })
package/dist/api/forms.js CHANGED
@@ -146,6 +146,9 @@ const toAction = (a) => {
146
146
  * A clickable push button (AcroForm /Btn). It holds no value - it fires an action. Its caption is baked
147
147
  * into the appearance, so the button looks the same in every viewer and in print.
148
148
  *
149
+ * Not for PDF/A: PDF/A forbids an action on a form field (ISO 19005-2/-3, 6.4.1), and an action is all a
150
+ * push button does.
151
+ *
149
152
  * ```ts
150
153
  * PushButton({ name: "send", label: "Submit", action: { submit: "https://example.com/f" }, width: 120 })
151
154
  * PushButton({ name: "clear", label: "Reset", action: "reset", width: 90 })
@@ -1,53 +1 @@
1
- import { type ColorInput } from "./color.js";
2
- import type { Gradient } from "../ir/display-list.js";
3
- /**
4
- * Gradients for a box background.
5
- *
6
- * The public form is BOX-RELATIVE - an angle and some stops - because that is what an author knows
7
- * when writing the document. The engine's `Gradient` wants absolute page coordinates, so the renderer
8
- * resolves one against the box it is painting; see `resolveGradient`.
9
- */
10
- /** A stop as written: a colour, optionally pinned to a position 0..1 along the axis. */
11
- export type StopInput = ColorInput | {
12
- color: ColorInput;
13
- at: number;
14
- };
15
- export interface LinearGradientInput {
16
- kind: "linear";
17
- /** Direction in degrees, CSS convention: 0 = to the top, 90 = to the right (default 180, downwards). */
18
- angle: number;
19
- stops: StopInput[];
20
- }
21
- export interface RadialGradientInput {
22
- kind: "radial";
23
- /** Centre as a fraction of the box, `[0.5, 0.5]` being the middle (the default). */
24
- center: [number, number];
25
- /** Outer radius as a fraction of the box's larger side (default 0.5, i.e. it touches the edges). */
26
- radius: number;
27
- stops: StopInput[];
28
- }
29
- export type GradientInput = LinearGradientInput | RadialGradientInput;
30
- export declare const isGradientInput: (v: unknown) => v is GradientInput;
31
- /**
32
- * A linear gradient. `linearGradient("#fff", "#000")` runs top to bottom; pass `{ angle }` for any
33
- * other direction, and `{ color, at }` for a stop that is not evenly spaced.
34
- */
35
- export declare function linearGradient(...args: [...StopInput[]] | [{
36
- angle?: number;
37
- stops: StopInput[];
38
- }]): LinearGradientInput;
39
- /** A radial gradient, centred by default and reaching the edges. */
40
- export declare function radialGradient(...args: [...StopInput[]] | [{
41
- center?: [number, number];
42
- radius?: number;
43
- stops: StopInput[];
44
- }]): RadialGradientInput;
45
- /**
46
- * Resolve a box-relative gradient against the box it paints, into the absolute page coordinates the
47
- * IR wants. Called by the renderer, which is the first place the box's geometry is known.
48
- *
49
- * The angle follows CSS: 0 points to the top and it turns clockwise, so 90 goes to the right. The
50
- * axis runs through the box centre and is scaled so it spans the box on that diagonal - which is why
51
- * a 45-degree gradient reaches the corners rather than stopping short of them.
52
- */
53
- export declare function resolveGradient(g: GradientInput, x: number, y: number, width: number, height: number): Gradient;
1
+ export * from "../common/gradient.js";
@@ -1,97 +1,2 @@
1
- import { toColor } from "./color.js";
2
- export const isGradientInput = (v) => typeof v === "object" && v !== null && "kind" in v && "stops" in v;
3
- /**
4
- * A linear gradient. `linearGradient("#fff", "#000")` runs top to bottom; pass `{ angle }` for any
5
- * other direction, and `{ color, at }` for a stop that is not evenly spaced.
6
- */
7
- export function linearGradient(...args) {
8
- const first = args[0];
9
- if (args.length === 1 && typeof first === "object" && first !== null && "stops" in first) {
10
- return { kind: "linear", angle: first.angle ?? 180, stops: first.stops };
11
- }
12
- return { kind: "linear", angle: 180, stops: args };
13
- }
14
- /** A radial gradient, centred by default and reaching the edges. */
15
- export function radialGradient(...args) {
16
- const first = args[0];
17
- if (args.length === 1 && typeof first === "object" && first !== null && "stops" in first) {
18
- return {
19
- kind: "radial",
20
- center: first.center ?? [0.5, 0.5],
21
- radius: first.radius ?? 0.5,
22
- stops: first.stops,
23
- };
24
- }
25
- return { kind: "radial", center: [0.5, 0.5], radius: 0.5, stops: args };
26
- }
27
- /**
28
- * Spread the stops that carry no `at` evenly, first to last, the way CSS does - and check the pinned
29
- * ones, because they end up in a PDF stitching function's `/Bounds`, which the format requires to be
30
- * strictly increasing inside the domain. A bad offset there is not a wrong-looking gradient, it is a
31
- * malformed file, so it is refused by name here rather than emitted.
32
- */
33
- function toStops(stops) {
34
- if (stops.length < 2) {
35
- throw new Error("A gradient needs at least two colour stops.");
36
- }
37
- const last = stops.length - 1;
38
- const out = stops.map((s, i) => {
39
- const pinned = typeof s === "object" && s !== null && "at" in s;
40
- const at = pinned ? s.at : i / last;
41
- if (!Number.isFinite(at) || at < 0 || at > 1) {
42
- throw new Error(`Invalid gradient stop position ${at}: it must be a number between 0 and 1.`);
43
- }
44
- return {
45
- offset: at,
46
- color: toColor(pinned ? s.color : s),
47
- };
48
- });
49
- for (let i = 1; i < out.length; i++) {
50
- if (out[i].offset <= out[i - 1].offset) {
51
- throw new Error(`Gradient stops must move forward: stop ${i} sits at ${out[i].offset}, which is not after ` +
52
- `${out[i - 1].offset}. (Two stops at the SAME position would be a hard colour edge in CSS; ` +
53
- `PDF cannot express that in one shading, so it is refused rather than drawn wrongly.)`);
54
- }
55
- }
56
- // A pinned first or last stop would otherwise be IGNORED - the shading's domain is always 0..1 and
57
- // only the INTERIOR offsets reach /Bounds. Carrying the edge colour outwards is what CSS does.
58
- if (out[0].offset > 0)
59
- out.unshift({ offset: 0, color: out[0].color });
60
- if (out[out.length - 1].offset < 1)
61
- out.push({ offset: 1, color: out[out.length - 1].color });
62
- return out;
63
- }
64
- /**
65
- * Resolve a box-relative gradient against the box it paints, into the absolute page coordinates the
66
- * IR wants. Called by the renderer, which is the first place the box's geometry is known.
67
- *
68
- * The angle follows CSS: 0 points to the top and it turns clockwise, so 90 goes to the right. The
69
- * axis runs through the box centre and is scaled so it spans the box on that diagonal - which is why
70
- * a 45-degree gradient reaches the corners rather than stopping short of them.
71
- */
72
- export function resolveGradient(g, x, y, width, height) {
73
- const stops = toStops(g.stops);
74
- if (g.kind === "radial") {
75
- const cx = x + g.center[0] * width;
76
- const cy = y + g.center[1] * height;
77
- const r = g.radius * Math.max(width, height);
78
- return { type: "radial", x0: cx, y0: cy, r0: 0, x1: cx, y1: cy, r1: r, stops, extend: "pad" };
79
- }
80
- const rad = ((g.angle % 360) * Math.PI) / 180;
81
- // CSS angles: 0 = up, clockwise. The engine lays out top-left down, so "up" is -y here.
82
- const dx = Math.sin(rad);
83
- const dy = -Math.cos(rad);
84
- // Half the box's extent along the axis, so the line covers the whole box for any angle.
85
- const half = (Math.abs(dx) * width + Math.abs(dy) * height) / 2;
86
- const cx = x + width / 2;
87
- const cy = y + height / 2;
88
- return {
89
- type: "linear",
90
- x0: cx - dx * half,
91
- y0: cy - dy * half,
92
- x1: cx + dx * half,
93
- y1: cy + dy * half,
94
- stops,
95
- extend: "pad",
96
- };
97
- }
1
+ // Lives in `common/` because the engine reads it too; this is its public face.
2
+ export * from "../common/gradient.js";
@@ -2,12 +2,16 @@ export * from "./color.js";
2
2
  export * from "./insets.js";
3
3
  export * from "./gradient.js";
4
4
  export * from "./font-url.js";
5
+ export { FontFormatError } from "../utils/ttf-parser.js";
6
+ export { JpegFormatError } from "../utils/image-helper.js";
5
7
  export * from "./dimension.js";
6
8
  export * from "./text.js";
7
9
  export * from "./layout.js";
8
10
  export * from "./content.js";
9
11
  export * from "./page-builder.js";
12
+ export * from "./flow.js";
10
13
  export * from "./structure.js";
14
+ export type { Diagnostic, DiagnosticCode, DiagnosticHandler } from "../common/diagnostics.js";
11
15
  export * from "./table.js";
12
16
  export * from "./forms.js";
13
17
  export * from "./descriptor.js";
package/dist/api/index.js CHANGED
@@ -5,11 +5,14 @@ export * from "./color.js";
5
5
  export * from "./insets.js";
6
6
  export * from "./gradient.js";
7
7
  export * from "./font-url.js";
8
+ export { FontFormatError } from "../utils/ttf-parser.js";
9
+ export { JpegFormatError } from "../utils/image-helper.js";
8
10
  export * from "./dimension.js";
9
11
  export * from "./text.js";
10
12
  export * from "./layout.js";
11
13
  export * from "./content.js";
12
14
  export * from "./page-builder.js";
15
+ export * from "./flow.js";
13
16
  export * from "./structure.js";
14
17
  export * from "./table.js";
15
18
  export * from "./forms.js";
@@ -187,6 +187,12 @@ export interface ExpandedOptions {
187
187
  * rest`, so `flexBasis: 120` with `flex: 0` is simply a fixed 120pt slot. Default 0.
188
188
  */
189
189
  flexBasis?: SizeInput;
190
+ /**
191
+ * How willingly this child gives up its `flexBasis` when the line overflows (CSS `flex-shrink`). Default
192
+ * 1, as for every flex item: an overflowing line pulls a `flexBasis: 120` slot back down to what its
193
+ * content can hold. `0` keeps the basis and lets the line overflow. A basis of 0 has nothing to give.
194
+ */
195
+ flexShrink?: number;
190
196
  }
191
197
  /**
192
198
  * Makes a child fill the leftover space along the stack's main axis (height in a Column,
@@ -11,6 +11,7 @@ import { BookmarkElement } from "../elements/layout/bookmark-element.js";
11
11
  import { AnchorElement } from "../elements/layout/anchor-element.js";
12
12
  import { RotatedElement } from "../elements/layout/rotated-element.js";
13
13
  import { RotatedBoxElement } from "../elements/layout/rotated-box-element.js";
14
+ import { flexibleOf } from "../layout/flex-item.js";
14
15
  import { toColor } from "./color.js";
15
16
  import { isGradientInput } from "./gradient.js";
16
17
  import { toEdges } from "./insets.js";
@@ -89,10 +90,15 @@ export function Box(a, b) {
89
90
  ? [
90
91
  new PaddingElement({
91
92
  margin: toEdges(opts.padding),
92
- child: children.length === 1 ? children[0] : Column(children),
93
+ child: children.length === 1 && !flexibleOf(children[0]) ? children[0] : Column(children),
94
+ ofBox: true,
93
95
  }),
94
96
  ]
95
- : children;
97
+ : // A Spacer/Expanded - also one inside a transparent wrapper such as a Link - needs a flex parent,
98
+ // alone as much as among siblings.
99
+ children.some((child) => flexibleOf(child) !== undefined)
100
+ ? [Column(children)]
101
+ : children;
96
102
  // A side is set if it (or the uniform `border`) is given. If ANY differs from a plain
97
103
  // uniform border, we hand the engine per-side colors (which draws individual lines).
98
104
  const sideKeys = [opts.borderTop, opts.borderRight, opts.borderBottom, opts.borderLeft];
@@ -226,5 +232,5 @@ export function Expanded(a, b) {
226
232
  basis: basis?.points,
227
233
  basisFactor: basis?.factor,
228
234
  child,
229
- });
235
+ }).withFlexShrink(opts.flexShrink);
230
236
  }
@@ -24,6 +24,10 @@ export type { PageInfo };
24
24
  * content decides the count that the content displays): in the flowing BODY the box is reserved before the
25
25
  * total is known, so a much wider final string can paint slightly past it; and a conditional header may
26
26
  * SHRINK on later pages but must not GROW, because the body band was measured against the first build.
27
+ *
28
+ * What it builds is placed WHOLE: it never splits across pages, and a `PageBreak` inside it has no
29
+ * effect. Its content belongs to one page by construction - a piece continued on the next page would
30
+ * have been built for the wrong one. Keep it to what fits on a page; flowing content goes beside it.
27
31
  */
28
32
  export declare function PageBuilder(build: (info: PageInfo) => PDFElement): PageBuilderElement;
29
33
  /** `Text` options plus an `offset` added to the number (e.g. `-1` to not count a cover page). */
@@ -22,6 +22,10 @@ import { Text } from "./text.js";
22
22
  * content decides the count that the content displays): in the flowing BODY the box is reserved before the
23
23
  * total is known, so a much wider final string can paint slightly past it; and a conditional header may
24
24
  * SHRINK on later pages but must not GROW, because the body band was measured against the first build.
25
+ *
26
+ * What it builds is placed WHOLE: it never splits across pages, and a `PageBreak` inside it has no
27
+ * effect. Its content belongs to one page by construction - a piece continued on the next page would
28
+ * have been built for the wrong one. Keep it to what fits on a page; flowing content goes beside it.
25
29
  */
26
30
  export function PageBuilder(build) {
27
31
  return new PageBuilderElement({ build });
@@ -3,6 +3,7 @@ import { PageElement } from "../elements/page-element.js";
3
3
  import { PDFElement } from "../elements/pdf-element.js";
4
4
  import { DefaultTextStyleElement } from "../elements/layout/default-text-style-element.js";
5
5
  import type { OverflowPolicy } from "../layout/fragmentation.js";
6
+ import type { DiagnosticHandler } from "../common/diagnostics.js";
6
7
  import { PageSize } from "../constants/page-sizes.js";
7
8
  import { type EncryptOptions } from "../crypto/security-handler.js";
8
9
  export type { EncryptOptions, Permissions } from "../crypto/security-handler.js";
@@ -38,8 +39,6 @@ export interface PageOptions extends StackOptions {
38
39
  */
39
40
  export declare function Page(children: PDFElement[]): PageElement;
40
41
  export declare function Page(opts: PageOptions, children: PDFElement[]): PageElement;
41
- /** Document options: PDF metadata plus the inheritable text defaults (font, size, color, lineHeight,
42
- * align, bold/italic) every `Text` inherits unless it sets its own - Flutter's `DefaultTextStyle`. */
43
42
  /** Where color emoji come from when a code point isn't in the current text font, so `Text("Hi 😅")`
44
43
  * works in one string. Either a **font** (native vector, offline - recommended; register it in
45
44
  * `fonts`) as a string name or `{ font }`, or an **image/CDN source** `{ url, format }` (react-pdf
@@ -51,6 +50,8 @@ export type EmojiSource = string | {
51
50
  url: string;
52
51
  format?: "png";
53
52
  };
53
+ /** Document options: PDF metadata plus the inheritable text defaults (font, size, color, lineHeight,
54
+ * align, bold/italic) every `Text` inherits unless it sets its own - Flutter's `DefaultTextStyle`. */
54
55
  export interface DocumentOptions extends TextDefaults {
55
56
  /** PDF metadata. */
56
57
  meta?: {
@@ -158,6 +159,13 @@ export interface RenderOptions {
158
159
  * able to SAY which character went missing, where a server log would never be read.
159
160
  */
160
161
  onMissingGlyphs?: (characters: string[]) => void;
162
+ /**
163
+ * Everything the engine has to say about this document, one call per finding: a page break that had
164
+ * no effect, a group that could not be kept together, clipped overflow, removed characters, ... -
165
+ * each with a stable `code`, the kind of element and its first text. Without it, what warned before
166
+ * still goes to `console.warn`. `onOverflow: "error"` still throws.
167
+ */
168
+ onDiagnostic?: DiagnosticHandler;
161
169
  /** Encrypt the PDF with a password (AES-256, the newest standard). NOT compatible with PDF/A
162
170
  * (ZUGFeRD invoices) - encrypting one throws, since PDF/A forbids encryption. */
163
171
  encrypt?: EncryptOptions;
@@ -180,6 +180,7 @@ export async function renderPdf(doc, options) {
180
180
  manager = om;
181
181
  om.setCompress(options?.compress !== false); // FlateDecode streams by default
182
182
  om.setOverflowPolicy(options?.onOverflow ?? "error");
183
+ om.diagnostics.setHandler(options?.onDiagnostic);
183
184
  om.setKerning(options?.kerning !== false); // on by default (experimental); `kerning:false` opts out
184
185
  om.acroform.setBakeAppearances(options?.fieldAppearances !== false); // baked unless opted out
185
186
  for (const [name, value] of Object.entries(fonts)) {
@@ -196,6 +197,14 @@ export async function renderPdf(doc, options) {
196
197
  om.registerCustomFont(name, value.boldItalic, FontStyle.BoldItalic);
197
198
  }
198
199
  }
200
+ // Version, id and encryption first - they create no objects, and an attachment's stream and name
201
+ // are written below and must already be enciphered. The handler after the version: R6 needs PDF 2.0.
202
+ if (options?.pdfVersion)
203
+ om.setPdfVersion(options.pdfVersion);
204
+ if (options?.documentId)
205
+ om.enableDocumentId();
206
+ if (securityHandler)
207
+ om.setSecurityHandler(securityHandler);
199
208
  for (const a of attachments) {
200
209
  om.attachFile(a.name, a.data, {
201
210
  relationship: a.relationship,
@@ -215,12 +224,6 @@ export async function renderPdf(doc, options) {
215
224
  om.setXmpMetadata(options.xmp);
216
225
  if (options?.outputIntent)
217
226
  om.setOutputIntent(options.outputIntent);
218
- if (options?.pdfVersion)
219
- om.setPdfVersion(options.pdfVersion);
220
- if (options?.documentId)
221
- om.enableDocumentId();
222
- if (securityHandler)
223
- om.setSecurityHandler(securityHandler);
224
227
  if (options?.accessible) {
225
228
  om.struct.enabled = true;
226
229
  if (options.lang)
@@ -2,14 +2,16 @@ import { PDFElement } from "../elements/pdf-element.js";
2
2
  import { Insets } from "./insets.js";
3
3
  import { ColorInput } from "./color.js";
4
4
  /**
5
- * A column width: a `number` of points (fixed), an `"Nfr"` fraction (splits the leftover
6
- * width), or `"auto"` (as wide as the widest cell in that column - measured at layout time).
5
+ * A column width, as in CSS grid with points for units: a `number` of points (fixed), an `"Nfr"`
6
+ * fraction (splits the leftover width, never narrower than its longest word), `"auto"` (as wide as the
7
+ * widest cell in that column, giving way down to its longest word when the table would not fit), or
8
+ * `"minmax(40, auto)"` (like `auto`, but never narrower than 40pt). Measured at layout time.
7
9
  */
8
10
  export type ColumnWidth = number | string;
9
11
  /** A table cell: any element, or a string (wrapped in `Text`). */
10
12
  export type Cell = PDFElement | string;
11
13
  export interface TableOptions {
12
- /** One entry per column: a fixed point width, an `"Nfr"` fraction, or `"auto"`. */
14
+ /** One entry per column: a fixed point width, an `"Nfr"` fraction, `"auto"` or `"minmax(<pt>, auto)"`. */
13
15
  columns: ColumnWidth[];
14
16
  /** A header row that REPEATS at the top of every page the table flows onto. */
15
17
  header?: Cell[];
package/dist/api/table.js CHANGED
@@ -14,27 +14,100 @@ function fractionOf(col) {
14
14
  const n = parseFloat(col);
15
15
  return Number.isFinite(n) ? n : 1;
16
16
  }
17
- /** The cell's unbounded (max-content) width - what an `"auto"` column sizes to. */
18
- function naturalWidth(cell, cellPadding, ctx) {
19
- const el = cellPadding !== undefined ? Padding(cellPadding, asElement(cell)) : asElement(cell);
20
- return el.calculateLayout(BoxConstraints.loose(Infinity, Infinity), { x: 0, y: 0 }, ctx).width;
17
+ const MINMAX_AUTO = /^minmax\(\s*(\d+(?:\.\d+)?)\s*,\s*auto\s*\)$/;
18
+ /**
19
+ * The floor of a content-sized column: 0 for `"auto"`, N for `"minmax(N, auto)"`, null for any other
20
+ * width. Other `minmax()` forms are not supported yet and say so, rather than being read as something else.
21
+ */
22
+ function autoMinimum(col) {
23
+ if (col === "auto")
24
+ return 0;
25
+ if (typeof col !== "string" || !col.startsWith("minmax("))
26
+ return null;
27
+ const min = MINMAX_AUTO.exec(col);
28
+ if (!min) {
29
+ throw new Error(`Unsupported column width "${col}" - minmax() takes a minimum in points and "auto", e.g. "minmax(40, auto)"`);
30
+ }
31
+ return parseFloat(min[1]);
21
32
  }
22
- /** Replaces every `"auto"` column with the widest cell width measured down that column. */
23
- function resolveAutoColumns(opts, rows, ctx) {
33
+ /**
34
+ * What the cells of column `i` need: `max` is the widest cell on one line (max-content), `min` the
35
+ * widest piece that cannot break (min-content, its longest word).
36
+ */
37
+ function cellWidths(opts, rows, i, ctx) {
24
38
  // The composed cell sits in a 1pt-border Box (border-box) that eats 1pt each side, which the
25
39
  // measured content doesn't include; add it back, and `ceil` so a single-word cell can't land
26
40
  // a hair over the width and wrap to an empty first line.
27
41
  const slack = opts.cellBorder !== undefined || opts.rule !== undefined ? 2 : 0;
28
- return opts.columns.map((col, i) => {
29
- if (col !== "auto")
30
- return col;
31
- let w = opts.header ? naturalWidth(opts.header[i], opts.cellPadding, ctx) : 0;
32
- for (const r of rows) {
33
- if (r[i] !== undefined)
34
- w = Math.max(w, naturalWidth(r[i], opts.cellPadding, ctx));
42
+ let min = 0;
43
+ let max = 0;
44
+ for (const cell of [opts.header?.[i], ...rows.map((r) => r[i])]) {
45
+ if (cell === undefined)
46
+ continue;
47
+ const el = opts.cellPadding !== undefined ? Padding(opts.cellPadding, asElement(cell)) : asElement(cell);
48
+ max = Math.max(max, el.measure(BoxConstraints.loose(Infinity, Infinity), ctx).width);
49
+ min = Math.max(min, el.minIntrinsicMain(true, ctx));
50
+ }
51
+ return { min: Math.ceil(min + slack), max: Math.ceil(max + slack) };
52
+ }
53
+ /** Grows `lo` toward `hi` by `extra`, in equal steps - each column stops at its own `hi`. */
54
+ function growEvenly(lo, hi, extra) {
55
+ const out = [...lo];
56
+ let open = out.map((_, k) => k).filter((k) => hi[k] > out[k]);
57
+ while (extra > 1e-9 && open.length > 0) {
58
+ const each = extra / open.length;
59
+ for (const k of open) {
60
+ const add = Math.min(each, hi[k] - out[k]);
61
+ out[k] += add;
62
+ extra -= add;
35
63
  }
36
- return Math.ceil(w + slack);
37
- });
64
+ open = open.filter((k) => hi[k] - out[k] > 1e-9);
65
+ }
66
+ return out;
67
+ }
68
+ /**
69
+ * The column widths for a table `width` wide, as CSS grid sizes its tracks. An `"auto"` column takes
70
+ * its widest cell while that leaves every `fr` column its longest word; when it does not, the `auto`
71
+ * columns give way evenly, down to their own longest word. The `fr` columns then share what is left -
72
+ * one whose share would be narrower than its longest word gets exactly that, and the table overflows
73
+ * instead of drawing words over each other. With no width to fit (a table in a Row), nothing gives way.
74
+ */
75
+ function resolveColumns(opts, rows, ctx, width) {
76
+ const columns = opts.columns;
77
+ const colGap = opts.colGap ?? opts.gap ?? 0;
78
+ const room = width - Math.max(0, columns.length - 1) * colGap;
79
+ const fixed = columns.reduce((n, c) => n + (typeof c === "number" ? c : 0), 0);
80
+ const autos = columns.flatMap((c, i) => (autoMinimum(c) === null ? [] : [i]));
81
+ const fractions = Number.isFinite(room)
82
+ ? columns.flatMap((c, i) => (fractionOf(c) === null ? [] : [i]))
83
+ : [];
84
+ const need = new Map();
85
+ for (const i of [...autos, ...fractions])
86
+ need.set(i, cellWidths(opts, rows, i, ctx));
87
+ const resolved = [...columns];
88
+ // 1. The auto columns: their widest cell, or less while an fr column would lose its longest word.
89
+ const lo = autos.map((i) => Math.max(autoMinimum(columns[i]), need.get(i).min));
90
+ const hi = autos.map((i) => Math.max(autoMinimum(columns[i]), need.get(i).max));
91
+ const frMin = fractions.reduce((n, i) => n + need.get(i).min, 0);
92
+ const free = room - fixed - frMin;
93
+ const sum = (xs) => xs.reduce((n, x) => n + x, 0);
94
+ const autoWidths = free >= sum(hi) ? hi : free <= sum(lo) ? lo : growEvenly(lo, hi, free - sum(lo));
95
+ autos.forEach((i, k) => (resolved[i] = autoWidths[k]));
96
+ // 2. The fr columns share the rest; one that would get less than its longest word is fixed at it.
97
+ let left = room - fixed - sum(autoWidths);
98
+ let flexible = fractions;
99
+ for (;;) {
100
+ const unit = left / flexible.reduce((n, i) => n + fractionOf(columns[i]), 0);
101
+ const short = flexible.filter((i) => need.get(i).min > fractionOf(columns[i]) * unit);
102
+ if (short.length === 0)
103
+ break;
104
+ for (const i of short) {
105
+ resolved[i] = need.get(i).min;
106
+ left -= need.get(i).min;
107
+ }
108
+ flexible = flexible.filter((i) => !short.includes(i));
109
+ }
110
+ return resolved;
38
111
  }
39
112
  /** Builds the table tree for already-resolved column widths (no `"auto"`). */
40
113
  function composeTable(opts, rows, columns) {
@@ -64,12 +137,10 @@ function composeTable(opts, rows, columns) {
64
137
  ...(ruled && rule !== undefined ? { borderBottom: rule } : {}),
65
138
  };
66
139
  const boxed = cb !== undefined || (ruled && rule !== undefined);
140
+ // The columns are the grid: a row never shrinks one on its own, or its cells leave their columns.
67
141
  if (typeof col === "number")
68
- return Box({ width: col, ...border }, [inner]);
69
- const fr = fractionOf(col);
70
- if (fr !== null)
71
- return Expanded({ flex: fr }, boxed ? Box({ ...border }, [inner]) : inner);
72
- throw new Error(`Unsupported column width "${col}" - use a number of points or "<n>fr"`);
142
+ return Box({ width: col, flexShrink: 0, ...border }, [inner]);
143
+ return Expanded({ flex: fractionOf(col) }, boxed ? Box({ ...border }, [inner]) : inner);
73
144
  };
74
145
  // align:stretch → equal-height cells, so a wrapping cell keeps the row's bottom rule straight.
75
146
  const buildRow = (cells, firstRow, ruled = false, isHeader = false) => new StructGroup({
@@ -92,8 +163,16 @@ function composeTable(opts, rows, columns) {
92
163
  * columns are resolved at layout time (a `DeferredElement`), once metrics are available.
93
164
  */
94
165
  export function Table(opts, rows) {
95
- if (opts.columns.some((c) => c === "auto")) {
96
- return new DeferredElement((ctx) => composeTable(opts, rows, resolveAutoColumns(opts, rows, ctx)));
166
+ // Checked here, where the table is written, not at layout: a typo names its line.
167
+ for (const col of opts.columns) {
168
+ if (typeof col !== "number" && fractionOf(col) === null && autoMinimum(col) === null) {
169
+ throw new Error(`Unsupported column width "${col}" - use a number of points, "<n>fr", "auto" or "minmax(<pt>, auto)"`);
170
+ }
171
+ }
172
+ if (opts.columns.some((c) => typeof c !== "number")) {
173
+ return new DeferredElement((ctx, width) => composeTable(opts, rows, resolveColumns(opts, rows, ctx, width)), [opts.header ?? [], ...rows]
174
+ .flat()
175
+ .filter((cell) => typeof cell !== "string"));
97
176
  }
98
177
  return composeTable(opts, rows, opts.columns);
99
178
  }