@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/text.d.ts
CHANGED
|
@@ -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
|
|
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,
|
package/dist/canvas/painter.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type ColorInput } from "../
|
|
2
|
-
import { type GradientInput } from "../
|
|
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.
|
package/dist/canvas/painter.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { toColor } from "../
|
|
2
|
-
import { isGradientInput, resolveGradient } from "../
|
|
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
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
/** A `ColumnBreak` outside a Region of a Flow - drawn as nothing. */
|
|
13
|
+
| "column-break-ignored"
|
|
14
|
+
/** `breakBefore` / `breakAfter` on a child of a Row or a table row, which never breaks. */
|
|
15
|
+
| "break-ignored"
|
|
16
|
+
/** A `keepTogether` group contains a forced break; the break wins and the group is split. */
|
|
17
|
+
| "keep-together-split"
|
|
18
|
+
/** Content taller than its page region that cannot break, placed whole and clipped. */
|
|
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"
|
|
23
|
+
/** Characters no font could draw, removed (PDF/A forbids `.notdef`). */
|
|
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";
|
|
28
|
+
export interface Diagnostic {
|
|
29
|
+
code: DiagnosticCode;
|
|
30
|
+
severity: "warning" | "info";
|
|
31
|
+
message: string;
|
|
32
|
+
/** The kind of element it is about, by the factory name an author writes: `"Row"`, `"Box"`, `"Text"`. */
|
|
33
|
+
element?: string;
|
|
34
|
+
/** The first text inside that element, shortened - so the place can be found in the document. */
|
|
35
|
+
text?: string;
|
|
36
|
+
/** The 1-based page, when the element is being laid out on a known page. */
|
|
37
|
+
page?: number;
|
|
38
|
+
}
|
|
39
|
+
export type DiagnosticHandler = (diagnostic: Diagnostic) => void;
|
|
40
|
+
/** Whether a diagnostic goes to `console.warn` when no handler is set - only what warned before. */
|
|
41
|
+
type Fallback = "warn" | "silent";
|
|
42
|
+
export declare class Diagnostics {
|
|
43
|
+
private handler?;
|
|
44
|
+
private readonly seen;
|
|
45
|
+
setHandler(handler: DiagnosticHandler | undefined): void;
|
|
46
|
+
/** Report once per `about` and code. `about` is the element the message is about, or any key. */
|
|
47
|
+
report(d: Omit<Diagnostic, "element" | "text"> & {
|
|
48
|
+
about?: object;
|
|
49
|
+
fallback?: Fallback;
|
|
50
|
+
}): void;
|
|
51
|
+
}
|
|
52
|
+
/** The 1-based page a layout context is on, as far as it knows: drawn (`pageInfo`) or being paginated. */
|
|
53
|
+
export declare function pageOf(ctx: {
|
|
54
|
+
pageInfo?: {
|
|
55
|
+
pageNumber: number;
|
|
56
|
+
};
|
|
57
|
+
paginatingPage?: number;
|
|
58
|
+
}): number | undefined;
|
|
59
|
+
/** Report through the render's channel, or - laid out on its own - warn as before. */
|
|
60
|
+
export declare function diagnose(diagnostics: Diagnostics | undefined, d: Parameters<Diagnostics["report"]>[0]): void;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
const EXCERPT = 40;
|
|
10
|
+
/** Engine classes whose factory carries another name. */
|
|
11
|
+
const FACTORY = { Rectangle: "Box", Container: "Column" };
|
|
12
|
+
export class Diagnostics {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.seen = new WeakMap();
|
|
15
|
+
}
|
|
16
|
+
setHandler(handler) {
|
|
17
|
+
this.handler = handler;
|
|
18
|
+
}
|
|
19
|
+
/** Report once per `about` and code. `about` is the element the message is about, or any key. */
|
|
20
|
+
report(d) {
|
|
21
|
+
const { about, fallback = "warn", ...rest } = d;
|
|
22
|
+
if (about) {
|
|
23
|
+
const codes = this.seen.get(about) ?? new Set();
|
|
24
|
+
if (codes.has(rest.code))
|
|
25
|
+
return;
|
|
26
|
+
codes.add(rest.code);
|
|
27
|
+
this.seen.set(about, codes);
|
|
28
|
+
}
|
|
29
|
+
const diagnostic = { ...rest, ...(about ? describe(about) : {}) };
|
|
30
|
+
if (this.handler)
|
|
31
|
+
this.handler(diagnostic);
|
|
32
|
+
else if (fallback === "warn")
|
|
33
|
+
console.warn(diagnostic.message);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The element's kind and the first text inside it, from what every element exposes. */
|
|
37
|
+
function describe(about) {
|
|
38
|
+
const name = about.constructor?.name;
|
|
39
|
+
if (!name || name === "Object")
|
|
40
|
+
return {};
|
|
41
|
+
const text = firstText(about, 0);
|
|
42
|
+
const kind = name.replace(/Element$/, "");
|
|
43
|
+
return {
|
|
44
|
+
element: FACTORY[kind] ?? kind,
|
|
45
|
+
...(text ? { text: text.length > EXCERPT ? `${text.slice(0, EXCERPT - 1)}…` : text } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function firstText(el, depth) {
|
|
49
|
+
if (depth > 32)
|
|
50
|
+
return undefined;
|
|
51
|
+
const node = el;
|
|
52
|
+
if (node.constructor?.name === "TextElement") {
|
|
53
|
+
const content = node.getProps?.().content;
|
|
54
|
+
const s = typeof content === "string"
|
|
55
|
+
? content
|
|
56
|
+
: Array.isArray(content)
|
|
57
|
+
? content.map((c) => c.content ?? "").join("")
|
|
58
|
+
: "";
|
|
59
|
+
return s.replace(/\s+/g, " ").trim() || undefined;
|
|
60
|
+
}
|
|
61
|
+
for (const child of node.layoutChildren?.() ?? []) {
|
|
62
|
+
const t = firstText(child, depth + 1);
|
|
63
|
+
if (t)
|
|
64
|
+
return t;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
/** The 1-based page a layout context is on, as far as it knows: drawn (`pageInfo`) or being paginated. */
|
|
69
|
+
export function pageOf(ctx) {
|
|
70
|
+
return ctx.pageInfo?.pageNumber ?? ctx.paginatingPage;
|
|
71
|
+
}
|
|
72
|
+
/** Outside a render (an element laid out on its own) there is no document channel: warn, once each. */
|
|
73
|
+
const bare = new Diagnostics();
|
|
74
|
+
/** Report through the render's channel, or - laid out on its own - warn as before. */
|
|
75
|
+
export function diagnose(diagnostics, d) {
|
|
76
|
+
(diagnostics ?? bare).report(d);
|
|
77
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { toColor } from "./color-input.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
|
+
}
|