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

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 (210) hide show
  1. package/dist/api/content.d.ts +5 -5
  2. package/dist/api/content.js +12 -6
  3. package/dist/api/font-url.d.ts +1 -2
  4. package/dist/api/font-url.js +8 -74
  5. package/dist/api/forms.d.ts +3 -0
  6. package/dist/api/forms.js +3 -0
  7. package/dist/api/index.d.ts +2 -0
  8. package/dist/api/index.js +1 -0
  9. package/dist/api/layout.d.ts +6 -0
  10. package/dist/api/layout.js +7 -2
  11. package/dist/api/page-builder.d.ts +4 -0
  12. package/dist/api/page-builder.js +4 -0
  13. package/dist/api/structure.d.ts +10 -2
  14. package/dist/api/structure.js +9 -6
  15. package/dist/api/table.d.ts +5 -3
  16. package/dist/api/table.js +99 -22
  17. package/dist/common/diagnostics.d.ts +53 -0
  18. package/dist/common/diagnostics.js +77 -0
  19. package/dist/edit/acroform-reader.js +7 -7
  20. package/dist/edit/appearance.js +3 -3
  21. package/dist/edit/attachments.d.ts +52 -0
  22. package/dist/edit/attachments.js +178 -0
  23. package/dist/edit/document.d.ts +38 -8
  24. package/dist/edit/document.js +208 -51
  25. package/dist/edit/fill.js +8 -23
  26. package/dist/edit/filters.d.ts +15 -0
  27. package/dist/edit/filters.js +157 -0
  28. package/dist/edit/index.d.ts +4 -1
  29. package/dist/edit/index.js +3 -1
  30. package/dist/edit/objects.d.ts +3 -4
  31. package/dist/edit/objects.js +5 -18
  32. package/dist/elements/canvas-element.d.ts +3 -0
  33. package/dist/elements/canvas-element.js +14 -3
  34. package/dist/elements/container-element.d.ts +5 -1
  35. package/dist/elements/container-element.js +35 -15
  36. package/dist/elements/forms/checkbox-element.d.ts +2 -0
  37. package/dist/elements/forms/checkbox-element.js +12 -3
  38. package/dist/elements/forms/choice-element.d.ts +6 -1
  39. package/dist/elements/forms/choice-element.js +17 -6
  40. package/dist/elements/forms/field-font.d.ts +8 -0
  41. package/dist/elements/forms/field-font.js +5 -0
  42. package/dist/elements/forms/push-button-element.d.ts +6 -1
  43. package/dist/elements/forms/push-button-element.js +16 -4
  44. package/dist/elements/forms/radio-element.d.ts +2 -0
  45. package/dist/elements/forms/radio-element.js +12 -3
  46. package/dist/elements/forms/signature-element.d.ts +6 -1
  47. package/dist/elements/forms/signature-element.js +16 -4
  48. package/dist/elements/forms/text-field-element.d.ts +6 -1
  49. package/dist/elements/forms/text-field-element.js +16 -4
  50. package/dist/elements/image-element.d.ts +39 -1
  51. package/dist/elements/image-element.js +95 -12
  52. package/dist/elements/layout/anchor-element.d.ts +8 -10
  53. package/dist/elements/layout/anchor-element.js +10 -18
  54. package/dist/elements/layout/bookmark-element.d.ts +9 -11
  55. package/dist/elements/layout/bookmark-element.js +11 -20
  56. package/dist/elements/layout/default-text-style-element.d.ts +11 -13
  57. package/dist/elements/layout/default-text-style-element.js +14 -31
  58. package/dist/elements/layout/deferred-element.d.ts +12 -4
  59. package/dist/elements/layout/deferred-element.js +24 -7
  60. package/dist/elements/layout/expanded-element.d.ts +7 -0
  61. package/dist/elements/layout/expanded-element.js +40 -18
  62. package/dist/elements/layout/keep-together-element.d.ts +8 -18
  63. package/dist/elements/layout/keep-together-element.js +21 -45
  64. package/dist/elements/layout/link-element.d.ts +8 -13
  65. package/dist/elements/layout/link-element.js +9 -19
  66. package/dist/elements/layout/padding-element.d.ts +15 -1
  67. package/dist/elements/layout/padding-element.js +55 -27
  68. package/dist/elements/layout/page-break-element.d.ts +4 -1
  69. package/dist/elements/layout/page-break-element.js +6 -1
  70. package/dist/elements/layout/page-builder-element.d.ts +5 -0
  71. package/dist/elements/layout/page-builder-element.js +11 -0
  72. package/dist/elements/layout/positioned-element.d.ts +14 -2
  73. package/dist/elements/layout/positioned-element.js +36 -10
  74. package/dist/elements/layout/proxy-element.d.ts +43 -0
  75. package/dist/elements/layout/proxy-element.js +94 -0
  76. package/dist/elements/layout/repeating-header-element.d.ts +6 -0
  77. package/dist/elements/layout/repeating-header-element.js +40 -13
  78. package/dist/elements/layout/rotated-box-element.d.ts +6 -0
  79. package/dist/elements/layout/rotated-box-element.js +27 -14
  80. package/dist/elements/layout/rotated-element.d.ts +7 -0
  81. package/dist/elements/layout/rotated-element.js +25 -2
  82. package/dist/elements/layout/struct-group.d.ts +9 -17
  83. package/dist/elements/layout/struct-group.js +10 -35
  84. package/dist/elements/line-element.d.ts +1 -0
  85. package/dist/elements/line-element.js +6 -0
  86. package/dist/elements/page-element.d.ts +10 -3
  87. package/dist/elements/page-element.js +39 -31
  88. package/dist/elements/pdf-document-element.d.ts +3 -0
  89. package/dist/elements/pdf-document-element.js +7 -0
  90. package/dist/elements/pdf-element.d.ts +75 -15
  91. package/dist/elements/pdf-element.js +68 -18
  92. package/dist/elements/rectangle-element.d.ts +14 -0
  93. package/dist/elements/rectangle-element.js +97 -52
  94. package/dist/elements/row-element.d.ts +11 -0
  95. package/dist/elements/row-element.js +58 -8
  96. package/dist/elements/svg-element.d.ts +30 -6
  97. package/dist/elements/svg-element.js +88 -17
  98. package/dist/elements/text-element.d.ts +32 -13
  99. package/dist/elements/text-element.js +119 -97
  100. package/dist/forms/acroform.d.ts +9 -1
  101. package/dist/forms/acroform.js +83 -34
  102. package/dist/forms/appearance.d.ts +17 -6
  103. package/dist/forms/appearance.js +18 -16
  104. package/dist/ir/display-list.d.ts +12 -2
  105. package/dist/layout/box-constraints.d.ts +5 -0
  106. package/dist/layout/box-constraints.js +14 -0
  107. package/dist/layout/collect-images.d.ts +6 -4
  108. package/dist/layout/collect-images.js +6 -4
  109. package/dist/layout/flex-item.d.ts +9 -0
  110. package/dist/layout/flex-item.js +39 -0
  111. package/dist/layout/fragmentation.d.ts +12 -4
  112. package/dist/layout/fragmentation.js +37 -14
  113. package/dist/layout/layout-cache.d.ts +53 -0
  114. package/dist/layout/layout-cache.js +107 -0
  115. package/dist/renderer/anchor-renderer.d.ts +2 -1
  116. package/dist/renderer/anchor-renderer.js +5 -4
  117. package/dist/renderer/bookmark-renderer.d.ts +2 -1
  118. package/dist/renderer/bookmark-renderer.js +5 -4
  119. package/dist/renderer/canvas-renderer.d.ts +2 -1
  120. package/dist/renderer/canvas-renderer.js +5 -2
  121. package/dist/renderer/container-renderer.d.ts +2 -1
  122. package/dist/renderer/container-renderer.js +6 -5
  123. package/dist/renderer/default-text-style-renderer.d.ts +2 -1
  124. package/dist/renderer/default-text-style-renderer.js +2 -3
  125. package/dist/renderer/deferred-renderer.d.ts +2 -1
  126. package/dist/renderer/deferred-renderer.js +2 -3
  127. package/dist/renderer/expanded-renderer.d.ts +2 -1
  128. package/dist/renderer/expanded-renderer.js +2 -3
  129. package/dist/renderer/form-field-renderer.d.ts +2 -1
  130. package/dist/renderer/form-field-renderer.js +16 -3
  131. package/dist/renderer/image-renderer.d.ts +2 -1
  132. package/dist/renderer/image-renderer.js +5 -2
  133. package/dist/renderer/keep-together-renderer.d.ts +2 -1
  134. package/dist/renderer/keep-together-renderer.js +2 -3
  135. package/dist/renderer/line-renderer.d.ts +2 -1
  136. package/dist/renderer/line-renderer.js +7 -2
  137. package/dist/renderer/link-renderer.d.ts +2 -1
  138. package/dist/renderer/link-renderer.js +36 -5
  139. package/dist/renderer/padding-renderer.d.ts +2 -1
  140. package/dist/renderer/padding-renderer.js +5 -5
  141. package/dist/renderer/page-break-renderer.d.ts +6 -5
  142. package/dist/renderer/page-break-renderer.js +14 -7
  143. package/dist/renderer/page-builder-renderer.d.ts +2 -1
  144. package/dist/renderer/page-builder-renderer.js +2 -3
  145. package/dist/renderer/page-renderer.js +28 -7
  146. package/dist/renderer/pdf-backend.d.ts +4 -3
  147. package/dist/renderer/pdf-backend.js +5 -5
  148. package/dist/renderer/pdf-document-renderer.d.ts +1 -8
  149. package/dist/renderer/pdf-document-renderer.js +15 -16
  150. package/dist/renderer/pdf-renderer.js +11 -11
  151. package/dist/renderer/positioned-renderer.d.ts +6 -1
  152. package/dist/renderer/positioned-renderer.js +6 -5
  153. package/dist/renderer/rectangle-renderer.d.ts +2 -1
  154. package/dist/renderer/rectangle-renderer.js +9 -4
  155. package/dist/renderer/repeating-header-renderer.d.ts +2 -1
  156. package/dist/renderer/repeating-header-renderer.js +6 -5
  157. package/dist/renderer/rotated-renderer.d.ts +2 -1
  158. package/dist/renderer/rotated-renderer.js +8 -5
  159. package/dist/renderer/row-renderer.d.ts +2 -1
  160. package/dist/renderer/row-renderer.js +6 -5
  161. package/dist/renderer/struct-group-renderer.d.ts +2 -1
  162. package/dist/renderer/struct-group-renderer.js +2 -3
  163. package/dist/renderer/svg-renderer.d.ts +2 -1
  164. package/dist/renderer/svg-renderer.js +8 -2
  165. package/dist/renderer/text-renderer.d.ts +4 -6
  166. package/dist/renderer/text-renderer.js +101 -111
  167. package/dist/svg/gradients.js +5 -0
  168. package/dist/svg/index.js +10 -1
  169. package/dist/svg/style.d.ts +2 -0
  170. package/dist/svg/style.js +11 -0
  171. package/dist/text/advance.d.ts +4 -0
  172. package/dist/text/advance.js +17 -4
  173. package/dist/text/bidi.d.ts +0 -2
  174. package/dist/text/bidi.js +0 -10
  175. package/dist/text/glyph-coverage.d.ts +1 -1
  176. package/dist/text/glyph-coverage.js +7 -3
  177. package/dist/text/hyphen-breaks.d.ts +17 -0
  178. package/dist/text/hyphen-breaks.js +54 -0
  179. package/dist/text/line-breaker.d.ts +34 -31
  180. package/dist/text/line-breaker.js +364 -176
  181. package/dist/text/line-metrics.d.ts +6 -3
  182. package/dist/text/line-metrics.js +7 -2
  183. package/dist/text/pdf-text-string.d.ts +21 -0
  184. package/dist/text/pdf-text-string.js +114 -0
  185. package/dist/text/text-decoration.d.ts +5 -5
  186. package/dist/text/text-decoration.js +5 -5
  187. package/dist/text/word-splitting.d.ts +15 -2
  188. package/dist/text/word-splitting.js +34 -8
  189. package/dist/utils/afm-parser.d.ts +2 -0
  190. package/dist/utils/afm-parser.js +10 -1
  191. package/dist/utils/dest-registry.d.ts +1 -1
  192. package/dist/utils/dest-registry.js +3 -2
  193. package/dist/utils/fetch-bytes.d.ts +19 -0
  194. package/dist/utils/fetch-bytes.js +79 -0
  195. package/dist/utils/flex-layout.d.ts +13 -13
  196. package/dist/utils/flex-layout.js +318 -216
  197. package/dist/utils/font-metrics.d.ts +4 -3
  198. package/dist/utils/image-helper.js +2 -1
  199. package/dist/utils/pdf-object-manager.d.ts +11 -3
  200. package/dist/utils/pdf-object-manager.js +39 -13
  201. package/dist/utils/renderer-registry.d.ts +43 -2
  202. package/dist/utils/renderer-registry.js +40 -0
  203. package/dist/utils/struct-tree.d.ts +24 -0
  204. package/dist/utils/struct-tree.js +61 -9
  205. package/dist/utils/ttf-parser.d.ts +4 -0
  206. package/dist/utils/ttf-parser.js +27 -4
  207. package/dist/validators/element-validator.js +2 -1
  208. package/package.json +1 -1
  209. package/dist/elements/layout/sized-container-element.d.ts +0 -14
  210. package/dist/elements/layout/sized-container-element.js +0 -32
@@ -27,7 +27,7 @@ export interface SvgOptions extends BoundsInput {
27
27
  /** Alternate text (tagged PDF). With it the drawing is a `Figure`; without it, decoration. */
28
28
  alt?: string;
29
29
  }
30
- /** An image source: a local file path (Node), raw bytes (e.g. a browser fetch/upload), or a `CustomImage`. */
30
+ /** An image source: a URL (`http(s):`, `data:`), a local file path (Node), raw bytes, or a `CustomImage`. */
31
31
  export type ImageSource = string | Uint8Array | CustomImage;
32
32
  /** How the image fills its box (locked §4). Mirrors CSS `object-fit`. */
33
33
  export type ImageFit = "none" | "contain" | "cover" | "fill";
@@ -36,8 +36,8 @@ export interface ImageOptions extends BoundsInput {
36
36
  * space). Pin exactly ONE axis and the other follows the image's aspect ratio (CSS `height: auto`). */
37
37
  width?: SizeInput;
38
38
  height?: SizeInput;
39
- /** Fit within the box. Default `none`, except where the box was DERIVED rather than given outright -
40
- * exactly one axis pinned, or an `aspectRatio` - which defaults to `fill` so the image scales into it. */
39
+ /** Fit within the box. Default `none` (the image's own size); with any explicit size or an
40
+ * `aspectRatio`, `fill` - the image scales into its box, as `<img width height>` does. */
41
41
  fit?: ImageFit;
42
42
  /** Corner radius in points (rounds the image box). */
43
43
  radius?: RadiusInput;
@@ -73,8 +73,8 @@ export declare function Canvas(paint: CanvasPaint): CanvasElement;
73
73
  */
74
74
  export declare function Svg(source: SvgSource, opts?: SvgOptions): SvgElement;
75
75
  /**
76
- * An image. `src` is a local file path (wrapped in a `CustomLocalImage`) or a ready
77
- * `CustomImage` for non-filesystem sources. Maps to an `ImageElement`.
76
+ * An image. `src` is a URL (fetched when the document renders), a local file path, raw bytes, or a
77
+ * ready `CustomImage`. Maps to an `ImageElement`.
78
78
  *
79
79
  * An SVG source is routed to `Svg` instead, so `Image({ src: "logo.svg" })` just works and a logo
80
80
  * stays a VECTOR in the PDF rather than becoming a bitmap.
@@ -1,6 +1,6 @@
1
1
  import { LineElement } from "../elements/line-element.js";
2
2
  import { PaddingElement } from "../elements/layout/padding-element.js";
3
- import { ImageElement, CustomLocalImage, CustomBytesImage, BoxFit, } from "../elements/image-element.js";
3
+ import { ImageElement, CustomLocalImage, CustomBytesImage, CustomUrlImage, isImageUrl, BoxFit, } from "../elements/image-element.js";
4
4
  import { toColor } from "./color.js";
5
5
  import { toEdges } from "./insets.js";
6
6
  import { toBounds, toDimension, toRadius, } from "./dimension.js";
@@ -84,13 +84,17 @@ export function Svg(source, opts = {}) {
84
84
  /** SVG is recognised by its extension or by its own opening bytes - never by a caller having to say. */
85
85
  function isSvg(src) {
86
86
  if (typeof src === "string") {
87
+ // A URL by its media type (`data:image/svg+xml`) or the extension of its PATH - a query may follow.
88
+ const path = isImageUrl(src) ? src.trim().replace(/[?#].*$/, "") : src.trim();
89
+ if (/^data:image\/svg\+xml[;,]/i.test(src.trim()))
90
+ return true;
87
91
  // `.svgz` is a GZIPPED svg. It is caught here rather than left to the image path, because the
88
92
  // error a user needs says what the file is - not "unsupported image bytes".
89
- if (/\.svgz$/i.test(src.trim())) {
93
+ if (/\.svgz$/i.test(path)) {
90
94
  throw new Error("@jasy/pdf: SVGZ (a gzip-compressed SVG) is not supported. Decompress it first " +
91
95
  "(`gunzip logo.svgz`) and pass the plain .svg.");
92
96
  }
93
- return /\.svg$/i.test(src.trim()) || /^\s*[<\ufeff]/.test(src);
97
+ return /\.svg$/i.test(path) || /^\s*[<\ufeff]/.test(src);
94
98
  }
95
99
  if (src instanceof Uint8Array) {
96
100
  const head = new TextDecoder().decode(src.subarray(0, 200)).trimStart();
@@ -99,8 +103,8 @@ function isSvg(src) {
99
103
  return false;
100
104
  }
101
105
  /**
102
- * An image. `src` is a local file path (wrapped in a `CustomLocalImage`) or a ready
103
- * `CustomImage` for non-filesystem sources. Maps to an `ImageElement`.
106
+ * An image. `src` is a URL (fetched when the document renders), a local file path, raw bytes, or a
107
+ * ready `CustomImage`. Maps to an `ImageElement`.
104
108
  *
105
109
  * An SVG source is routed to `Svg` instead, so `Image({ src: "logo.svg" })` just works and a logo
106
110
  * stays a VECTOR in the PDF rather than becoming a bitmap.
@@ -129,7 +133,9 @@ function rasterImage(src, opts = {}) {
129
133
  const fit = opts.fit ? FIT[opts.fit] : sized ? BoxFit.fill : undefined;
130
134
  return new ImageElement({
131
135
  image: typeof src === "string"
132
- ? new CustomLocalImage(src)
136
+ ? isImageUrl(src)
137
+ ? new CustomUrlImage(src)
138
+ : new CustomLocalImage(src)
133
139
  : src instanceof Uint8Array
134
140
  ? new CustomBytesImage(src)
135
141
  : src,
@@ -5,8 +5,7 @@
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
  */
11
10
  /** One font file by URL, or a styled family of them. Mirrors `FontSource`, with strings meaning URLs. */
12
11
  export type UrlFontSource = string | {
@@ -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 })
@@ -2,12 +2,14 @@ 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";
5
6
  export * from "./dimension.js";
6
7
  export * from "./text.js";
7
8
  export * from "./layout.js";
8
9
  export * from "./content.js";
9
10
  export * from "./page-builder.js";
10
11
  export * from "./structure.js";
12
+ export type { Diagnostic, DiagnosticCode, DiagnosticHandler } from "../common/diagnostics.js";
11
13
  export * from "./table.js";
12
14
  export * from "./forms.js";
13
15
  export * from "./descriptor.js";
package/dist/api/index.js CHANGED
@@ -5,6 +5,7 @@ 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";
8
9
  export * from "./dimension.js";
9
10
  export * from "./text.js";
10
11
  export * from "./layout.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";
@@ -90,9 +91,13 @@ export function Box(a, b) {
90
91
  new PaddingElement({
91
92
  margin: toEdges(opts.padding),
92
93
  child: children.length === 1 ? 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
+ children.length > 1 && children.some((child) => flexibleOf(child) !== undefined)
99
+ ? [Column(children)]
100
+ : children;
96
101
  // A side is set if it (or the uniform `border`) is given. If ANY differs from a plain
97
102
  // uniform border, we hand the engine per-side colors (which draws individual lines).
98
103
  const sideKeys = [opts.borderTop, opts.borderRight, opts.borderBottom, opts.borderLeft];
@@ -226,5 +231,5 @@ export function Expanded(a, b) {
226
231
  basis: basis?.points,
227
232
  basisFactor: basis?.factor,
228
233
  child,
229
- });
234
+ }).withFlexShrink(opts.flexShrink);
230
235
  }
@@ -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,14 @@ 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)));
97
174
  }
98
175
  return composeTable(opts, rows, opts.columns);
99
176
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * What the engine tells the author about a document it rendered anyway - one channel for every such
3
+ * signal, so an application (or an agent, which cannot see the page) can act on it by `code`.
4
+ *
5
+ * Without a handler nothing changes: what warned before still goes to `console.warn`, what was silent
6
+ * stays silent. A handler receives everything, once per element and code - layout runs several times
7
+ * per render, and the same mistake must not be reported for each pass.
8
+ */
9
+ export type DiagnosticCode =
10
+ /** A `PageBreak` outside a paginating flow (in a Row, in a PageBuilder) - drawn as nothing. */
11
+ "page-break-ignored"
12
+ /** `breakBefore` / `breakAfter` on a child of a Row or a table row, which never breaks. */
13
+ | "break-ignored"
14
+ /** A `keepTogether` group contains a forced break; the break wins and the group is split. */
15
+ | "keep-together-split"
16
+ /** Content taller than its page region that cannot break, placed whole and clipped. */
17
+ | "overflow-clipped"
18
+ /** Characters no font could draw, removed (PDF/A forbids `.notdef`). */
19
+ | "missing-glyphs";
20
+ export interface Diagnostic {
21
+ code: DiagnosticCode;
22
+ severity: "warning" | "info";
23
+ message: string;
24
+ /** The kind of element it is about, by the factory name an author writes: `"Row"`, `"Box"`, `"Text"`. */
25
+ element?: string;
26
+ /** The first text inside that element, shortened - so the place can be found in the document. */
27
+ text?: string;
28
+ /** The 1-based page, when the element is being laid out on a known page. */
29
+ page?: number;
30
+ }
31
+ export type DiagnosticHandler = (diagnostic: Diagnostic) => void;
32
+ /** Whether a diagnostic goes to `console.warn` when no handler is set - only what warned before. */
33
+ type Fallback = "warn" | "silent";
34
+ export declare class Diagnostics {
35
+ private handler?;
36
+ private readonly seen;
37
+ setHandler(handler: DiagnosticHandler | undefined): void;
38
+ /** Report once per `about` and code. `about` is the element the message is about, or any key. */
39
+ report(d: Omit<Diagnostic, "element" | "text"> & {
40
+ about?: object;
41
+ fallback?: Fallback;
42
+ }): void;
43
+ }
44
+ /** The 1-based page a layout context is on, as far as it knows: drawn (`pageInfo`) or being paginated. */
45
+ export declare function pageOf(ctx: {
46
+ pageInfo?: {
47
+ pageNumber: number;
48
+ };
49
+ paginatingPage?: number;
50
+ }): number | undefined;
51
+ /** Report through the render's channel, or - laid out on its own - warn as before. */
52
+ export declare function diagnose(diagnostics: Diagnostics | undefined, d: Parameters<Diagnostics["report"]>[0]): void;
53
+ export {};