@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.
- package/dist/api/color.d.ts +1 -24
- package/dist/api/color.js +2 -278
- package/dist/api/content.d.ts +10 -5
- package/dist/api/content.js +29 -13
- package/dist/api/descriptor.js +30 -3
- package/dist/api/flow.d.ts +55 -0
- package/dist/api/flow.js +71 -0
- package/dist/api/font-url.d.ts +1 -2
- package/dist/api/font-url.js +8 -74
- package/dist/api/forms.d.ts +3 -0
- package/dist/api/forms.js +3 -0
- package/dist/api/gradient.d.ts +1 -53
- package/dist/api/gradient.js +2 -97
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.js +3 -0
- package/dist/api/layout.d.ts +6 -0
- package/dist/api/layout.js +9 -3
- package/dist/api/page-builder.d.ts +4 -0
- package/dist/api/page-builder.js +4 -0
- package/dist/api/structure.d.ts +10 -2
- package/dist/api/structure.js +9 -6
- package/dist/api/table.d.ts +5 -3
- package/dist/api/table.js +101 -22
- package/dist/api/text.d.ts +8 -8
- package/dist/api/text.js +6 -0
- package/dist/canvas/painter.d.ts +2 -2
- package/dist/canvas/painter.js +2 -2
- package/dist/common/color-input.d.ts +24 -0
- package/dist/common/color-input.js +278 -0
- package/dist/common/diagnostics.d.ts +61 -0
- package/dist/common/diagnostics.js +77 -0
- package/dist/common/gradient.d.ts +53 -0
- package/dist/common/gradient.js +97 -0
- package/dist/edit/acroform-reader.js +7 -7
- package/dist/edit/appearance.js +3 -3
- package/dist/edit/attachments.d.ts +52 -0
- package/dist/edit/attachments.js +178 -0
- package/dist/edit/document.d.ts +38 -8
- package/dist/edit/document.js +208 -51
- package/dist/edit/fill.js +8 -23
- package/dist/edit/filters.d.ts +15 -0
- package/dist/edit/filters.js +157 -0
- package/dist/edit/index.d.ts +4 -1
- package/dist/edit/index.js +3 -1
- package/dist/edit/objects.d.ts +3 -4
- package/dist/edit/objects.js +5 -18
- package/dist/elements/canvas-element.d.ts +3 -0
- package/dist/elements/canvas-element.js +14 -3
- package/dist/elements/container-element.d.ts +8 -1
- package/dist/elements/container-element.js +49 -17
- package/dist/elements/forms/checkbox-element.d.ts +2 -0
- package/dist/elements/forms/checkbox-element.js +12 -3
- package/dist/elements/forms/choice-element.d.ts +6 -1
- package/dist/elements/forms/choice-element.js +17 -6
- package/dist/elements/forms/field-font.d.ts +8 -0
- package/dist/elements/forms/field-font.js +5 -0
- package/dist/elements/forms/push-button-element.d.ts +6 -1
- package/dist/elements/forms/push-button-element.js +16 -4
- package/dist/elements/forms/radio-element.d.ts +2 -0
- package/dist/elements/forms/radio-element.js +12 -3
- package/dist/elements/forms/signature-element.d.ts +6 -1
- package/dist/elements/forms/signature-element.js +16 -4
- package/dist/elements/forms/text-field-element.d.ts +6 -1
- package/dist/elements/forms/text-field-element.js +16 -4
- package/dist/elements/image-element.d.ts +70 -21
- package/dist/elements/image-element.js +173 -52
- package/dist/elements/index.d.ts +3 -0
- package/dist/elements/index.js +3 -0
- package/dist/elements/layout/anchor-element.d.ts +8 -10
- package/dist/elements/layout/anchor-element.js +10 -18
- package/dist/elements/layout/bookmark-element.d.ts +9 -11
- package/dist/elements/layout/bookmark-element.js +11 -20
- package/dist/elements/layout/column-break-element.d.ts +17 -0
- package/dist/elements/layout/column-break-element.js +26 -0
- package/dist/elements/layout/default-text-style-element.d.ts +11 -13
- package/dist/elements/layout/default-text-style-element.js +14 -31
- package/dist/elements/layout/deferred-element.d.ts +20 -4
- package/dist/elements/layout/deferred-element.js +41 -9
- package/dist/elements/layout/expanded-element.d.ts +11 -0
- package/dist/elements/layout/expanded-element.js +53 -18
- package/dist/elements/layout/flow-element.d.ts +109 -0
- package/dist/elements/layout/flow-element.js +317 -0
- package/dist/elements/layout/keep-together-element.d.ts +8 -18
- package/dist/elements/layout/keep-together-element.js +33 -45
- package/dist/elements/layout/link-element.d.ts +8 -13
- package/dist/elements/layout/link-element.js +9 -19
- package/dist/elements/layout/padding-element.d.ts +18 -1
- package/dist/elements/layout/padding-element.js +63 -28
- package/dist/elements/layout/page-break-element.d.ts +4 -1
- package/dist/elements/layout/page-break-element.js +6 -1
- package/dist/elements/layout/page-builder-element.d.ts +5 -0
- package/dist/elements/layout/page-builder-element.js +13 -2
- package/dist/elements/layout/positioned-element.d.ts +23 -4
- package/dist/elements/layout/positioned-element.js +42 -12
- package/dist/elements/layout/proxy-element.d.ts +47 -0
- package/dist/elements/layout/proxy-element.js +106 -0
- package/dist/elements/layout/region-element.d.ts +53 -0
- package/dist/elements/layout/region-element.js +108 -0
- package/dist/elements/layout/repeating-header-element.d.ts +9 -0
- package/dist/elements/layout/repeating-header-element.js +55 -13
- package/dist/elements/layout/rotated-box-element.d.ts +6 -0
- package/dist/elements/layout/rotated-box-element.js +27 -14
- package/dist/elements/layout/rotated-element.d.ts +9 -0
- package/dist/elements/layout/rotated-element.js +33 -2
- package/dist/elements/layout/struct-group.d.ts +9 -17
- package/dist/elements/layout/struct-group.js +10 -35
- package/dist/elements/line-element.d.ts +1 -0
- package/dist/elements/line-element.js +6 -0
- package/dist/elements/page-element.d.ts +10 -3
- package/dist/elements/page-element.js +39 -31
- package/dist/elements/pdf-document-element.d.ts +3 -0
- package/dist/elements/pdf-document-element.js +7 -0
- package/dist/elements/pdf-element.d.ts +105 -16
- package/dist/elements/pdf-element.js +118 -19
- package/dist/elements/rectangle-element.d.ts +22 -2
- package/dist/elements/rectangle-element.js +119 -55
- package/dist/elements/row-element.d.ts +17 -12
- package/dist/elements/row-element.js +89 -9
- package/dist/elements/svg-element.d.ts +30 -6
- package/dist/elements/svg-element.js +91 -26
- package/dist/elements/text-element.d.ts +48 -13
- package/dist/elements/text-element.js +161 -116
- package/dist/forms/acroform.d.ts +9 -1
- package/dist/forms/acroform.js +83 -34
- package/dist/forms/appearance.d.ts +17 -6
- package/dist/forms/appearance.js +18 -16
- package/dist/ir/display-list.d.ts +18 -3
- package/dist/layout/box-constraints.d.ts +17 -0
- package/dist/layout/box-constraints.js +31 -0
- package/dist/layout/collect-images.d.ts +6 -4
- package/dist/layout/collect-images.js +14 -8
- package/dist/layout/flex-item.d.ts +11 -0
- package/dist/layout/flex-item.js +51 -0
- package/dist/layout/fragmentation.d.ts +23 -9
- package/dist/layout/fragmentation.js +62 -19
- package/dist/layout/layout-cache.d.ts +53 -0
- package/dist/layout/layout-cache.js +110 -0
- package/dist/platform/browser-fs.d.ts +1 -2
- package/dist/platform/browser-fs.js +3 -6
- package/dist/platform/node-fs.d.ts +0 -1
- package/dist/platform/node-fs.js +0 -4
- package/dist/renderer/anchor-renderer.d.ts +2 -1
- package/dist/renderer/anchor-renderer.js +5 -4
- package/dist/renderer/bookmark-renderer.d.ts +2 -1
- package/dist/renderer/bookmark-renderer.js +5 -4
- package/dist/renderer/canvas-renderer.d.ts +2 -1
- package/dist/renderer/canvas-renderer.js +5 -2
- package/dist/renderer/column-break-renderer.d.ts +8 -0
- package/dist/renderer/column-break-renderer.js +15 -0
- package/dist/renderer/container-renderer.d.ts +2 -1
- package/dist/renderer/container-renderer.js +6 -5
- package/dist/renderer/default-text-style-renderer.d.ts +2 -1
- package/dist/renderer/default-text-style-renderer.js +2 -3
- package/dist/renderer/deferred-renderer.d.ts +2 -1
- package/dist/renderer/deferred-renderer.js +2 -3
- package/dist/renderer/emoji-image.js +3 -1
- package/dist/renderer/expanded-renderer.d.ts +2 -1
- package/dist/renderer/expanded-renderer.js +2 -3
- package/dist/renderer/flow-renderer.d.ts +16 -0
- package/dist/renderer/flow-renderer.js +21 -0
- package/dist/renderer/form-field-renderer.d.ts +2 -1
- package/dist/renderer/form-field-renderer.js +16 -3
- package/dist/renderer/image-renderer.d.ts +2 -1
- package/dist/renderer/image-renderer.js +33 -53
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/keep-together-renderer.d.ts +2 -1
- package/dist/renderer/keep-together-renderer.js +2 -3
- package/dist/renderer/line-renderer.d.ts +2 -1
- package/dist/renderer/line-renderer.js +7 -2
- package/dist/renderer/link-renderer.d.ts +2 -1
- package/dist/renderer/link-renderer.js +36 -5
- package/dist/renderer/padding-renderer.d.ts +2 -1
- package/dist/renderer/padding-renderer.js +5 -5
- package/dist/renderer/page-break-renderer.d.ts +6 -5
- package/dist/renderer/page-break-renderer.js +14 -7
- package/dist/renderer/page-builder-renderer.d.ts +2 -1
- package/dist/renderer/page-builder-renderer.js +2 -3
- package/dist/renderer/page-renderer.js +30 -10
- package/dist/renderer/pdf-backend.d.ts +4 -3
- package/dist/renderer/pdf-backend.js +14 -7
- package/dist/renderer/pdf-document-renderer.d.ts +1 -8
- package/dist/renderer/pdf-document-renderer.js +17 -17
- package/dist/renderer/pdf-renderer.js +20 -11
- package/dist/renderer/positioned-renderer.d.ts +18 -1
- package/dist/renderer/positioned-renderer.js +43 -6
- package/dist/renderer/rectangle-renderer.d.ts +2 -1
- package/dist/renderer/rectangle-renderer.js +18 -8
- package/dist/renderer/repeating-header-renderer.d.ts +2 -1
- package/dist/renderer/repeating-header-renderer.js +6 -5
- package/dist/renderer/rotated-renderer.d.ts +2 -1
- package/dist/renderer/rotated-renderer.js +9 -5
- package/dist/renderer/row-renderer.d.ts +2 -1
- package/dist/renderer/row-renderer.js +6 -5
- package/dist/renderer/struct-group-renderer.d.ts +2 -1
- package/dist/renderer/struct-group-renderer.js +2 -3
- package/dist/renderer/svg-renderer.d.ts +2 -1
- package/dist/renderer/svg-renderer.js +8 -2
- package/dist/renderer/text-renderer.d.ts +4 -6
- package/dist/renderer/text-renderer.js +114 -127
- package/dist/svg/gradients.js +5 -0
- package/dist/svg/index.d.ts +1 -1
- package/dist/svg/index.js +36 -6
- package/dist/svg/style.d.ts +2 -0
- package/dist/svg/style.js +12 -1
- package/dist/text/advance.d.ts +4 -0
- package/dist/text/advance.js +17 -4
- package/dist/text/bidi.d.ts +0 -2
- package/dist/text/bidi.js +0 -10
- package/dist/text/glyph-coverage.d.ts +1 -1
- package/dist/text/glyph-coverage.js +7 -3
- package/dist/text/hyphen-breaks.d.ts +17 -0
- package/dist/text/hyphen-breaks.js +54 -0
- package/dist/text/line-breaker.d.ts +55 -29
- package/dist/text/line-breaker.js +453 -188
- package/dist/text/line-metrics.d.ts +8 -3
- package/dist/text/line-metrics.js +11 -2
- package/dist/text/pdf-text-string.d.ts +21 -0
- package/dist/text/pdf-text-string.js +114 -0
- package/dist/text/text-decoration.d.ts +5 -5
- package/dist/text/text-decoration.js +5 -5
- package/dist/text/word-splitting.d.ts +15 -2
- package/dist/text/word-splitting.js +45 -13
- package/dist/utils/afm-parser.d.ts +2 -0
- package/dist/utils/afm-parser.js +10 -1
- package/dist/utils/dest-registry.d.ts +1 -1
- package/dist/utils/dest-registry.js +3 -2
- package/dist/utils/fetch-bytes.d.ts +19 -0
- package/dist/utils/fetch-bytes.js +79 -0
- package/dist/utils/flex-layout.d.ts +16 -14
- package/dist/utils/flex-layout.js +427 -217
- package/dist/utils/font-metrics.d.ts +4 -3
- package/dist/utils/image-helper.d.ts +59 -11
- package/dist/utils/image-helper.js +209 -81
- package/dist/utils/pdf-object-manager.d.ts +17 -6
- package/dist/utils/pdf-object-manager.js +70 -20
- package/dist/utils/renderer-registry.d.ts +49 -2
- package/dist/utils/renderer-registry.js +40 -0
- package/dist/utils/struct-tree.d.ts +24 -0
- package/dist/utils/struct-tree.js +61 -9
- package/dist/utils/ttf-parser.d.ts +4 -0
- package/dist/utils/ttf-parser.js +30 -6
- package/dist/validators/element-validator.js +2 -1
- package/package.json +1 -1
- package/dist/elements/layout/sized-container-element.d.ts +0 -14
- package/dist/elements/layout/sized-container-element.js +0 -32
package/dist/api/font-url.js
CHANGED
|
@@ -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
|
-
*
|
|
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.
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
}
|
package/dist/api/forms.d.ts
CHANGED
|
@@ -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 })
|
package/dist/api/gradient.d.ts
CHANGED
|
@@ -1,53 +1 @@
|
|
|
1
|
-
|
|
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";
|
package/dist/api/gradient.js
CHANGED
|
@@ -1,97 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
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";
|
package/dist/api/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/api/layout.d.ts
CHANGED
|
@@ -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,
|
package/dist/api/layout.js
CHANGED
|
@@ -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
|
-
:
|
|
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). */
|
package/dist/api/page-builder.js
CHANGED
|
@@ -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 });
|
package/dist/api/structure.d.ts
CHANGED
|
@@ -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;
|
package/dist/api/structure.js
CHANGED
|
@@ -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)
|
package/dist/api/table.d.ts
CHANGED
|
@@ -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"`
|
|
6
|
-
* width),
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
/**
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
}
|