@jasy/pdf 1.0.0-beta.4 → 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 (109) 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 +7 -2
  4. package/dist/api/content.js +17 -7
  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/gradient.d.ts +1 -53
  9. package/dist/api/gradient.js +2 -97
  10. package/dist/api/index.d.ts +2 -0
  11. package/dist/api/index.js +2 -0
  12. package/dist/api/layout.js +4 -3
  13. package/dist/api/table.js +3 -1
  14. package/dist/api/text.d.ts +8 -8
  15. package/dist/api/text.js +6 -0
  16. package/dist/canvas/painter.d.ts +2 -2
  17. package/dist/canvas/painter.js +2 -2
  18. package/dist/common/color-input.d.ts +24 -0
  19. package/dist/common/color-input.js +278 -0
  20. package/dist/common/diagnostics.d.ts +9 -1
  21. package/dist/common/gradient.d.ts +53 -0
  22. package/dist/common/gradient.js +97 -0
  23. package/dist/elements/container-element.d.ts +3 -0
  24. package/dist/elements/container-element.js +17 -5
  25. package/dist/elements/image-element.d.ts +37 -26
  26. package/dist/elements/image-element.js +88 -50
  27. package/dist/elements/index.d.ts +3 -0
  28. package/dist/elements/index.js +3 -0
  29. package/dist/elements/layout/column-break-element.d.ts +17 -0
  30. package/dist/elements/layout/column-break-element.js +26 -0
  31. package/dist/elements/layout/deferred-element.d.ts +9 -1
  32. package/dist/elements/layout/deferred-element.js +18 -3
  33. package/dist/elements/layout/expanded-element.d.ts +4 -0
  34. package/dist/elements/layout/expanded-element.js +14 -1
  35. package/dist/elements/layout/flow-element.d.ts +109 -0
  36. package/dist/elements/layout/flow-element.js +317 -0
  37. package/dist/elements/layout/keep-together-element.js +14 -2
  38. package/dist/elements/layout/padding-element.d.ts +3 -0
  39. package/dist/elements/layout/padding-element.js +9 -2
  40. package/dist/elements/layout/page-builder-element.js +2 -2
  41. package/dist/elements/layout/positioned-element.d.ts +9 -2
  42. package/dist/elements/layout/positioned-element.js +6 -2
  43. package/dist/elements/layout/proxy-element.d.ts +4 -0
  44. package/dist/elements/layout/proxy-element.js +12 -0
  45. package/dist/elements/layout/region-element.d.ts +53 -0
  46. package/dist/elements/layout/region-element.js +108 -0
  47. package/dist/elements/layout/repeating-header-element.d.ts +3 -0
  48. package/dist/elements/layout/repeating-header-element.js +16 -1
  49. package/dist/elements/layout/rotated-element.d.ts +2 -0
  50. package/dist/elements/layout/rotated-element.js +8 -0
  51. package/dist/elements/pdf-element.d.ts +32 -3
  52. package/dist/elements/pdf-element.js +50 -1
  53. package/dist/elements/rectangle-element.d.ts +12 -6
  54. package/dist/elements/rectangle-element.js +31 -12
  55. package/dist/elements/row-element.d.ts +6 -12
  56. package/dist/elements/row-element.js +34 -4
  57. package/dist/elements/svg-element.js +4 -10
  58. package/dist/elements/text-element.d.ts +17 -1
  59. package/dist/elements/text-element.js +63 -40
  60. package/dist/ir/display-list.d.ts +6 -1
  61. package/dist/layout/box-constraints.d.ts +12 -0
  62. package/dist/layout/box-constraints.js +17 -0
  63. package/dist/layout/collect-images.js +8 -4
  64. package/dist/layout/flex-item.d.ts +2 -0
  65. package/dist/layout/flex-item.js +14 -2
  66. package/dist/layout/fragmentation.d.ts +11 -5
  67. package/dist/layout/fragmentation.js +27 -7
  68. package/dist/layout/layout-cache.d.ts +8 -8
  69. package/dist/layout/layout-cache.js +10 -7
  70. package/dist/platform/browser-fs.d.ts +1 -2
  71. package/dist/platform/browser-fs.js +3 -6
  72. package/dist/platform/node-fs.d.ts +0 -1
  73. package/dist/platform/node-fs.js +0 -4
  74. package/dist/renderer/column-break-renderer.d.ts +8 -0
  75. package/dist/renderer/column-break-renderer.js +15 -0
  76. package/dist/renderer/emoji-image.js +3 -1
  77. package/dist/renderer/flow-renderer.d.ts +16 -0
  78. package/dist/renderer/flow-renderer.js +21 -0
  79. package/dist/renderer/image-renderer.js +31 -54
  80. package/dist/renderer/index.d.ts +2 -0
  81. package/dist/renderer/index.js +2 -0
  82. package/dist/renderer/page-renderer.js +3 -4
  83. package/dist/renderer/pdf-backend.js +9 -2
  84. package/dist/renderer/pdf-document-renderer.js +2 -1
  85. package/dist/renderer/pdf-renderer.js +9 -0
  86. package/dist/renderer/positioned-renderer.d.ts +14 -2
  87. package/dist/renderer/positioned-renderer.js +40 -4
  88. package/dist/renderer/rectangle-renderer.d.ts +1 -1
  89. package/dist/renderer/rectangle-renderer.js +11 -6
  90. package/dist/renderer/rotated-renderer.js +3 -2
  91. package/dist/renderer/text-renderer.js +25 -28
  92. package/dist/svg/index.d.ts +1 -1
  93. package/dist/svg/index.js +26 -5
  94. package/dist/svg/style.js +1 -1
  95. package/dist/text/line-breaker.d.ts +23 -0
  96. package/dist/text/line-breaker.js +142 -65
  97. package/dist/text/line-metrics.d.ts +2 -0
  98. package/dist/text/line-metrics.js +5 -1
  99. package/dist/text/word-splitting.js +11 -5
  100. package/dist/utils/flex-layout.d.ts +3 -1
  101. package/dist/utils/flex-layout.js +141 -33
  102. package/dist/utils/image-helper.d.ts +59 -11
  103. package/dist/utils/image-helper.js +207 -80
  104. package/dist/utils/pdf-object-manager.d.ts +6 -3
  105. package/dist/utils/pdf-object-manager.js +31 -7
  106. package/dist/utils/renderer-registry.d.ts +6 -0
  107. package/dist/utils/renderer-registry.js +1 -1
  108. package/dist/utils/ttf-parser.js +3 -2
  109. package/package.json +1 -1
@@ -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";
@@ -3,11 +3,13 @@ export * from "./insets.js";
3
3
  export * from "./gradient.js";
4
4
  export * from "./font-url.js";
5
5
  export { FontFormatError } from "../utils/ttf-parser.js";
6
+ export { JpegFormatError } from "../utils/image-helper.js";
6
7
  export * from "./dimension.js";
7
8
  export * from "./text.js";
8
9
  export * from "./layout.js";
9
10
  export * from "./content.js";
10
11
  export * from "./page-builder.js";
12
+ export * from "./flow.js";
11
13
  export * from "./structure.js";
12
14
  export type { Diagnostic, DiagnosticCode, DiagnosticHandler } from "../common/diagnostics.js";
13
15
  export * from "./table.js";
package/dist/api/index.js CHANGED
@@ -6,11 +6,13 @@ export * from "./insets.js";
6
6
  export * from "./gradient.js";
7
7
  export * from "./font-url.js";
8
8
  export { FontFormatError } from "../utils/ttf-parser.js";
9
+ export { JpegFormatError } from "../utils/image-helper.js";
9
10
  export * from "./dimension.js";
10
11
  export * from "./text.js";
11
12
  export * from "./layout.js";
12
13
  export * from "./content.js";
13
14
  export * from "./page-builder.js";
15
+ export * from "./flow.js";
14
16
  export * from "./structure.js";
15
17
  export * from "./table.js";
16
18
  export * from "./forms.js";
@@ -90,12 +90,13 @@ export function Box(a, b) {
90
90
  ? [
91
91
  new PaddingElement({
92
92
  margin: toEdges(opts.padding),
93
- child: children.length === 1 ? children[0] : Column(children),
93
+ child: children.length === 1 && !flexibleOf(children[0]) ? children[0] : Column(children),
94
94
  ofBox: true,
95
95
  }),
96
96
  ]
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)
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)
99
100
  ? [Column(children)]
100
101
  : children;
101
102
  // A side is set if it (or the uniform `border`) is given. If ANY differs from a plain
package/dist/api/table.js CHANGED
@@ -170,7 +170,9 @@ export function Table(opts, rows) {
170
170
  }
171
171
  }
172
172
  if (opts.columns.some((c) => typeof c !== "number")) {
173
- return new DeferredElement((ctx, width) => composeTable(opts, rows, resolveColumns(opts, rows, ctx, width)));
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"));
174
176
  }
175
177
  return composeTable(opts, rows, opts.columns);
176
178
  }
@@ -5,8 +5,8 @@ import { ResolvedTextStyle, type TextTransform } from "../text/text-style.js";
5
5
  import { ColorInput } from "./color.js";
6
6
  import type { Hyphenator } from "../text/word-splitting.js";
7
7
  export type { TextOverflow, Direction, TextTransform, Hyphenator };
8
- /** Text styling shared by `Text`, `Paragraph` and `span`. `bold`/`italic` are booleans
9
- * (locked §7.3) and combine into one engine `FontStyle`. */
8
+ /** Text styling shared by `Text`, `Paragraph` and `span` - what applies to a run. `bold`/`italic` are
9
+ * booleans (locked §7.3) and combine into one engine `FontStyle`. */
10
10
  export interface TextStyle {
11
11
  size?: number;
12
12
  /** Font family, or a STACK: each code point is drawn by the first family in the list that has a
@@ -26,16 +26,10 @@ export interface TextStyle {
26
26
  skipInk?: boolean;
27
27
  /** Extra space after every glyph, in points (CSS `letter-spacing`). Negative tightens. Default 0. */
28
28
  letterSpacing?: number;
29
- /** Base writing direction (CSS `direction`): `"rtl"` starts the line on the right, for Hebrew or
30
- * Arabic. Mixed text is reordered per Unicode UAX #9 either way, so Hebrew inside an `ltr`
31
- * paragraph already comes out right - this decides where the LINE starts. */
32
- direction?: Direction;
33
29
  /** CSS `text-transform`: recase the text. `capitalize` upper-cases the first letter of each word. */
34
30
  textTransform?: TextTransform;
35
31
  /** CSS `word-spacing`, in points: extra advance at every space. Negative tightens. */
36
32
  wordSpacing?: number;
37
- /** CSS `text-indent`, in points: how far the FIRST line of a paragraph starts in. */
38
- textIndent?: number;
39
33
  /** Split a word that is wider than its box, anywhere, no hyphen (CSS `overflow-wrap:
40
34
  * break-word`). The floor under `hyphenate`: an e-mail, an IBAN or an invoice number has no
41
35
  * valid hyphenation point anywhere. Off by default - a too-wide word overflows, as in CSS. */
@@ -57,6 +51,12 @@ export interface TextStyle {
57
51
  to?: string;
58
52
  }
59
53
  export interface TextOptions extends Omit<TextStyle, "verticalAlign"> {
54
+ /** Base writing direction (CSS `direction`): `"rtl"` starts the line on the right, for Hebrew or
55
+ * Arabic. Mixed text is reordered per Unicode UAX #9 either way, so Hebrew inside an `ltr`
56
+ * paragraph already comes out right - this decides where the LINE starts. */
57
+ direction?: Direction;
58
+ /** CSS `text-indent`, in points: how far the FIRST line of a paragraph starts in. */
59
+ textIndent?: number;
60
60
  /** Text-internal alignment (left/center/right) - independent of a parent's `cross` (§5). */
61
61
  align?: "left" | "center" | "right" | "justify";
62
62
  /** Cap the number of wrapped lines (default: unlimited - the text grows down as far as it needs,
package/dist/api/text.js CHANGED
@@ -58,7 +58,13 @@ export function span(text, style = {}) {
58
58
  fontColor: style.color !== undefined ? toColor(style.color) : undefined,
59
59
  underline: style.underline,
60
60
  strikethrough: style.strikethrough,
61
+ skipInk: style.skipInk,
61
62
  letterSpacing: style.letterSpacing,
63
+ textTransform: style.textTransform,
64
+ wordSpacing: style.wordSpacing,
65
+ ligatures: style.ligatures,
66
+ breakWord: style.breakWord,
67
+ hyphenate: style.hyphenate,
62
68
  verticalAlign: style.verticalAlign,
63
69
  href: style.href,
64
70
  dest: style.to,
@@ -1,5 +1,5 @@
1
- import { type ColorInput } from "../api/color.js";
2
- import { type GradientInput } from "../api/gradient.js";
1
+ import { type ColorInput } from "../common/color-input.js";
2
+ import { type GradientInput } from "../common/gradient.js";
3
3
  import type { IRNode, StrokeStyle } from "../ir/display-list.js";
4
4
  /**
5
5
  * The imperative escape hatch: a box in the layout that you draw into yourself.
@@ -1,5 +1,5 @@
1
- import { toColor } from "../api/color.js";
2
- import { isGradientInput, resolveGradient } from "../api/gradient.js";
1
+ import { toColor } from "../common/color-input.js";
2
+ import { isGradientInput, resolveGradient } from "../common/gradient.js";
3
3
  import { pathData } from "../svg/shapes.js";
4
4
  import { boundsOf, quadToCubic } from "../vector/path.js";
5
5
  const DEGREES = Math.PI / 180;
@@ -0,0 +1,24 @@
1
+ import { Color } from "./color.js";
2
+ /**
3
+ * Every way to name a color. The FORM picks the convention, so there is never a guess
4
+ * (locked design §2):
5
+ *
6
+ * | form | example | meaning |
7
+ * |---------------------------|--------------------------|--------------------------------|
8
+ * | named (full CSS set) | `"steelblue"`, `"transparent"` | the ~148 CSS color names |
9
+ * | hex 6 / 3 | `"#1450aa"` / `"#14a"` | CSS RGB |
10
+ * | hex 8 / 4 | `"#1450aacc"` / `"#14ac"`| CSS RGBA (alpha LAST) |
11
+ * | number | `0xff1450aa` | Flutter ARGB (alpha FIRST) |
12
+ * | `rgb()` / `rgba()` | `rgb(20,90,170)` | channels 0–255 (`rgba` a=0–1) |
13
+ * | `Color` instance | `new Color(20,90,170)` | the engine layer, still valid |
14
+ */
15
+ export type ColorInput = string | number | Color;
16
+ /** Channels 0–255, fully opaque. */
17
+ export declare function rgb(r: number, g: number, b: number): Color;
18
+ /** Channels 0–255, alpha 0–1. */
19
+ export declare function rgba(r: number, g: number, b: number, a: number): Color;
20
+ /**
21
+ * Normalizes any `ColorInput` to an engine `Color`. The single funnel every factory uses,
22
+ * so a color means the same thing no matter how it was written.
23
+ */
24
+ export declare function toColor(input: ColorInput): Color;
@@ -0,0 +1,278 @@
1
+ import { Color } from "./color.js";
2
+ /** Channels 0–255, fully opaque. */
3
+ export function rgb(r, g, b) {
4
+ return new Color(r, g, b, 1);
5
+ }
6
+ /** Channels 0–255, alpha 0–1. */
7
+ export function rgba(r, g, b, a) {
8
+ return new Color(r, g, b, a);
9
+ }
10
+ /**
11
+ * Normalizes any `ColorInput` to an engine `Color`. The single funnel every factory uses,
12
+ * so a color means the same thing no matter how it was written.
13
+ */
14
+ export function toColor(input) {
15
+ if (input instanceof Color)
16
+ return input;
17
+ if (typeof input === "number")
18
+ return fromArgbNumber(input);
19
+ const s = input.trim().toLowerCase();
20
+ if (s.startsWith("#"))
21
+ return fromHex(s);
22
+ const named = CSS_COLORS[s];
23
+ if (named)
24
+ return new Color(named[0], named[1], named[2], named[3] ?? 1);
25
+ const call = /^([a-z-]+)\(([^)]*)\)$/.exec(s);
26
+ if (call)
27
+ return fromFunction(call[1], call[2], input);
28
+ throw new Error(`Unknown color: "${input}"`);
29
+ }
30
+ /**
31
+ * The CSS colour FUNCTIONS. `rgb()`/`rgba()` and `hsl()`/`hsla()` are the sRGB family, so they convert
32
+ * exactly and are supported; a wide-gamut function (`color()`, `lab()`, `oklch()`) is named and
33
+ * refused rather than silently squashed into sRGB, since the result would be a colour nobody chose.
34
+ *
35
+ * Both syntaxes are accepted, because both occur in the wild: the legacy comma form
36
+ * `rgb(20, 90, 170)` and the modern space form with an optional slash-alpha `rgb(20 90 170 / 50%)`.
37
+ */
38
+ function fromFunction(name, body, original) {
39
+ if (name !== "rgb" && name !== "rgba" && name !== "hsl" && name !== "hsla") {
40
+ throw new Error(`Unsupported color function "${name}()" in "${String(original)}". ` +
41
+ `Use a hex value, a CSS colour name, rgb()/rgba() or hsl()/hsla().`);
42
+ }
43
+ const [main, alphaPart] = body.split("/");
44
+ const parts = main
45
+ .trim()
46
+ .split(/[\s,]+/)
47
+ .filter(Boolean);
48
+ const alphaText = alphaPart !== undefined ? alphaPart.trim() : parts[3];
49
+ if (parts.length < 3)
50
+ throw new Error(`Unknown color: "${String(original)}"`);
51
+ const alpha = alphaText === undefined ? 1 : ratio(alphaText, original);
52
+ const [a, b, c] = parts;
53
+ if (name === "rgb" || name === "rgba") {
54
+ return new Color(channel(a, original), channel(b, original), channel(c, original), alpha);
55
+ }
56
+ const [r, g, bl] = hslToRgb(number(a, original), ratio(b, original), ratio(c, original));
57
+ return new Color(r, g, bl, alpha);
58
+ }
59
+ const NUMERIC = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
60
+ /**
61
+ * One component of a colour function. `Number.parseFloat` stops at the first character it cannot use
62
+ * and returns what it had, so `rgb(20abc, 90, 170)` would silently become `rgb(20, 90, 170)` - a
63
+ * colour nobody wrote. The whole token has to be a number.
64
+ */
65
+ function number(text, original) {
66
+ const t = text.trim();
67
+ if (!NUMERIC.test(t))
68
+ throw new Error(`Unknown color: "${String(original)}"`);
69
+ return Number(t);
70
+ }
71
+ /** An rgb channel: 0-255, or a percentage of 255. */
72
+ const channel = (text, original) => text.trim().endsWith("%")
73
+ ? (number(text.trim().slice(0, -1), original) / 100) * 255
74
+ : number(text, original);
75
+ /** A 0..1 ratio written either as a number or as a percentage (alpha, saturation, lightness). */
76
+ const ratio = (text, original) => text.trim().endsWith("%")
77
+ ? number(text.trim().slice(0, -1), original) / 100
78
+ : number(text, original);
79
+ /** CSS hue-saturation-lightness to 0-255 channels. */
80
+ function hslToRgb(hue, s, l) {
81
+ const h = ((hue % 360) + 360) % 360;
82
+ const c = (1 - Math.abs(2 * l - 1)) * s;
83
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
84
+ const m = l - c / 2;
85
+ const sector = Math.floor(h / 60) % 6;
86
+ const rgbPrime = [
87
+ [c, x, 0],
88
+ [x, c, 0],
89
+ [0, c, x],
90
+ [0, x, c],
91
+ [x, 0, c],
92
+ [c, 0, x],
93
+ ][sector];
94
+ return rgbPrime.map((v) => Math.round((v + m) * 255));
95
+ }
96
+ /** Flutter ARGB: 0xAARRGGBB, alpha FIRST. A 6-digit number has alpha 0x00 = transparent. */
97
+ function fromArgbNumber(n) {
98
+ const a = (n >>> 24) & 0xff;
99
+ const r = (n >>> 16) & 0xff;
100
+ const g = (n >>> 8) & 0xff;
101
+ const b = n & 0xff;
102
+ return new Color(r, g, b, a / 255);
103
+ }
104
+ /** CSS hex: #RGB, #RGBA, #RRGGBB, #RRGGBBAA. Alpha is LAST. */
105
+ function fromHex(s) {
106
+ const hex = s.slice(1);
107
+ const expand = (h) => h
108
+ .split("")
109
+ .map((c) => c + c)
110
+ .join("");
111
+ let full;
112
+ if (hex.length === 3 || hex.length === 4)
113
+ full = expand(hex);
114
+ else if (hex.length === 6 || hex.length === 8)
115
+ full = hex;
116
+ else
117
+ throw new Error(`Invalid hex color: "${s}"`);
118
+ const r = parseInt(full.slice(0, 2), 16);
119
+ const g = parseInt(full.slice(2, 4), 16);
120
+ const b = parseInt(full.slice(4, 6), 16);
121
+ const a = full.length === 8 ? parseInt(full.slice(6, 8), 16) / 255 : 1;
122
+ if ([r, g, b].some(Number.isNaN))
123
+ throw new Error(`Invalid hex color: "${s}"`);
124
+ return new Color(r, g, b, a);
125
+ }
126
+ // The full CSS named-color set (~148, incl. the synonyms grey/gray and `transparent`).
127
+ // [r, g, b] or [r, g, b, alpha]. Lower-cased keys; `toColor` lower-cases the input.
128
+ const CSS_COLORS = {
129
+ transparent: [0, 0, 0, 0],
130
+ aliceblue: [240, 248, 255],
131
+ antiquewhite: [250, 235, 215],
132
+ aqua: [0, 255, 255],
133
+ aquamarine: [127, 255, 212],
134
+ azure: [240, 255, 255],
135
+ beige: [245, 245, 220],
136
+ bisque: [255, 228, 196],
137
+ black: [0, 0, 0],
138
+ blanchedalmond: [255, 235, 205],
139
+ blue: [0, 0, 255],
140
+ blueviolet: [138, 43, 226],
141
+ brown: [165, 42, 42],
142
+ burlywood: [222, 184, 135],
143
+ cadetblue: [95, 158, 160],
144
+ chartreuse: [127, 255, 0],
145
+ chocolate: [210, 105, 30],
146
+ coral: [255, 127, 80],
147
+ cornflowerblue: [100, 149, 237],
148
+ cornsilk: [255, 248, 220],
149
+ crimson: [220, 20, 60],
150
+ cyan: [0, 255, 255],
151
+ darkblue: [0, 0, 139],
152
+ darkcyan: [0, 139, 139],
153
+ darkgoldenrod: [184, 134, 11],
154
+ darkgray: [169, 169, 169],
155
+ darkgrey: [169, 169, 169],
156
+ darkgreen: [0, 100, 0],
157
+ darkkhaki: [189, 183, 107],
158
+ darkmagenta: [139, 0, 139],
159
+ darkolivegreen: [85, 107, 47],
160
+ darkorange: [255, 140, 0],
161
+ darkorchid: [153, 50, 204],
162
+ darkred: [139, 0, 0],
163
+ darksalmon: [233, 150, 122],
164
+ darkseagreen: [143, 188, 143],
165
+ darkslateblue: [72, 61, 139],
166
+ darkslategray: [47, 79, 79],
167
+ darkslategrey: [47, 79, 79],
168
+ darkturquoise: [0, 206, 209],
169
+ darkviolet: [148, 0, 211],
170
+ deeppink: [255, 20, 147],
171
+ deepskyblue: [0, 191, 255],
172
+ dimgray: [105, 105, 105],
173
+ dimgrey: [105, 105, 105],
174
+ dodgerblue: [30, 144, 255],
175
+ firebrick: [178, 34, 34],
176
+ floralwhite: [255, 250, 240],
177
+ forestgreen: [34, 139, 34],
178
+ fuchsia: [255, 0, 255],
179
+ gainsboro: [220, 220, 220],
180
+ ghostwhite: [248, 248, 255],
181
+ gold: [255, 215, 0],
182
+ goldenrod: [218, 165, 32],
183
+ gray: [128, 128, 128],
184
+ grey: [128, 128, 128],
185
+ green: [0, 128, 0],
186
+ greenyellow: [173, 255, 47],
187
+ honeydew: [240, 255, 240],
188
+ hotpink: [255, 105, 180],
189
+ indianred: [205, 92, 92],
190
+ indigo: [75, 0, 130],
191
+ ivory: [255, 255, 240],
192
+ khaki: [240, 230, 140],
193
+ lavender: [230, 230, 250],
194
+ lavenderblush: [255, 240, 245],
195
+ lawngreen: [124, 252, 0],
196
+ lemonchiffon: [255, 250, 205],
197
+ lightblue: [173, 216, 230],
198
+ lightcoral: [240, 128, 128],
199
+ lightcyan: [224, 255, 255],
200
+ lightgoldenrodyellow: [250, 250, 210],
201
+ lightgray: [211, 211, 211],
202
+ lightgrey: [211, 211, 211],
203
+ lightgreen: [144, 238, 144],
204
+ lightpink: [255, 182, 193],
205
+ lightsalmon: [255, 160, 122],
206
+ lightseagreen: [32, 178, 170],
207
+ lightskyblue: [135, 206, 250],
208
+ lightslategray: [119, 136, 153],
209
+ lightslategrey: [119, 136, 153],
210
+ lightsteelblue: [176, 196, 222],
211
+ lightyellow: [255, 255, 224],
212
+ lime: [0, 255, 0],
213
+ limegreen: [50, 205, 50],
214
+ linen: [250, 240, 230],
215
+ magenta: [255, 0, 255],
216
+ maroon: [128, 0, 0],
217
+ mediumaquamarine: [102, 205, 170],
218
+ mediumblue: [0, 0, 205],
219
+ mediumorchid: [186, 85, 211],
220
+ mediumpurple: [147, 112, 219],
221
+ mediumseagreen: [60, 179, 113],
222
+ mediumslateblue: [123, 104, 238],
223
+ mediumspringgreen: [0, 250, 154],
224
+ mediumturquoise: [72, 209, 204],
225
+ mediumvioletred: [199, 21, 133],
226
+ midnightblue: [25, 25, 112],
227
+ mintcream: [245, 255, 250],
228
+ mistyrose: [255, 228, 225],
229
+ moccasin: [255, 228, 181],
230
+ navajowhite: [255, 222, 173],
231
+ navy: [0, 0, 128],
232
+ oldlace: [253, 245, 230],
233
+ olive: [128, 128, 0],
234
+ olivedrab: [107, 142, 35],
235
+ orange: [255, 165, 0],
236
+ orangered: [255, 69, 0],
237
+ orchid: [218, 112, 214],
238
+ palegoldenrod: [238, 232, 170],
239
+ palegreen: [152, 251, 152],
240
+ paleturquoise: [175, 238, 238],
241
+ palevioletred: [219, 112, 147],
242
+ papayawhip: [255, 239, 213],
243
+ peachpuff: [255, 218, 185],
244
+ peru: [205, 133, 63],
245
+ pink: [255, 192, 203],
246
+ plum: [221, 160, 221],
247
+ powderblue: [176, 224, 230],
248
+ purple: [128, 0, 128],
249
+ rebeccapurple: [102, 51, 153],
250
+ red: [255, 0, 0],
251
+ rosybrown: [188, 143, 143],
252
+ royalblue: [65, 105, 225],
253
+ saddlebrown: [139, 69, 19],
254
+ salmon: [250, 128, 114],
255
+ sandybrown: [244, 164, 96],
256
+ seagreen: [46, 139, 87],
257
+ seashell: [255, 245, 238],
258
+ sienna: [160, 82, 45],
259
+ silver: [192, 192, 192],
260
+ skyblue: [135, 206, 235],
261
+ slateblue: [106, 90, 205],
262
+ slategray: [112, 128, 144],
263
+ slategrey: [112, 128, 144],
264
+ snow: [255, 250, 250],
265
+ springgreen: [0, 255, 127],
266
+ steelblue: [70, 130, 180],
267
+ tan: [210, 180, 140],
268
+ teal: [0, 128, 128],
269
+ thistle: [216, 191, 216],
270
+ tomato: [255, 99, 71],
271
+ turquoise: [64, 224, 208],
272
+ violet: [238, 130, 238],
273
+ wheat: [245, 222, 179],
274
+ white: [255, 255, 255],
275
+ whitesmoke: [245, 245, 245],
276
+ yellow: [255, 255, 0],
277
+ yellowgreen: [154, 205, 50],
278
+ };
@@ -9,14 +9,22 @@
9
9
  export type DiagnosticCode =
10
10
  /** A `PageBreak` outside a paginating flow (in a Row, in a PageBuilder) - drawn as nothing. */
11
11
  "page-break-ignored"
12
+ /** A `ColumnBreak` outside a Region of a Flow - drawn as nothing. */
13
+ | "column-break-ignored"
12
14
  /** `breakBefore` / `breakAfter` on a child of a Row or a table row, which never breaks. */
13
15
  | "break-ignored"
14
16
  /** A `keepTogether` group contains a forced break; the break wins and the group is split. */
15
17
  | "keep-together-split"
16
18
  /** Content taller than its page region that cannot break, placed whole and clipped. */
17
19
  | "overflow-clipped"
20
+ /** Content wider than its box (a word that does not break, a Row whose children need more), drawn
21
+ * past the box's edge - as CSS draws it; `overflow: "hidden"` on a Box clips it. */
22
+ | "overflow-wide"
18
23
  /** Characters no font could draw, removed (PDF/A forbids `.notdef`). */
19
- | "missing-glyphs";
24
+ | "missing-glyphs"
25
+ /** An image whose own size was not known at layout (a URL first built in a `PageBuilder`), so the
26
+ * axis it would have derived fills what it is offered instead. */
27
+ | "image-unsized";
20
28
  export interface Diagnostic {
21
29
  code: DiagnosticCode;
22
30
  severity: "warning" | "info";
@@ -0,0 +1,53 @@
1
+ import { type ColorInput } from "./color-input.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;