@jasy/pdf 1.0.0-alpha.9 → 1.0.0-beta.1
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/README.md +27 -9
- package/dist/api/color.js +69 -0
- package/dist/api/content.d.ts +40 -1
- package/dist/api/content.js +83 -0
- package/dist/api/descriptor.js +6 -1
- package/dist/api/dimension.d.ts +3 -2
- package/dist/api/font-url.js +3 -2
- package/dist/api/structure.d.ts +7 -0
- package/dist/api/structure.js +38 -3
- package/dist/api/text.d.ts +23 -2
- package/dist/api/text.js +9 -0
- package/dist/canvas/painter.d.ts +102 -0
- package/dist/canvas/painter.js +183 -0
- package/dist/elements/canvas-element.d.ts +38 -0
- package/dist/elements/canvas-element.js +47 -0
- package/dist/elements/container-element.d.ts +2 -0
- package/dist/elements/container-element.js +6 -0
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.js +2 -0
- package/dist/elements/layout/expanded-element.d.ts +2 -0
- package/dist/elements/layout/expanded-element.js +4 -0
- package/dist/elements/layout/keep-together-element.d.ts +2 -0
- package/dist/elements/layout/keep-together-element.js +4 -0
- package/dist/elements/layout/padding-element.d.ts +5 -0
- package/dist/elements/layout/padding-element.js +8 -0
- package/dist/elements/layout/struct-group.d.ts +2 -0
- package/dist/elements/layout/struct-group.js +4 -0
- package/dist/elements/pdf-element.d.ts +19 -3
- package/dist/elements/pdf-element.js +22 -4
- package/dist/elements/rectangle-element.d.ts +2 -0
- package/dist/elements/rectangle-element.js +6 -0
- package/dist/elements/row-element.d.ts +2 -0
- package/dist/elements/row-element.js +6 -0
- package/dist/elements/svg-element.d.ts +41 -0
- package/dist/elements/svg-element.js +93 -0
- package/dist/elements/text-element.d.ts +33 -1
- package/dist/elements/text-element.js +80 -4
- package/dist/ir/display-list.d.ts +44 -5
- package/dist/layout/min-intrinsic.d.ts +29 -0
- package/dist/layout/min-intrinsic.js +37 -0
- package/dist/renderer/anchor-renderer.js +1 -1
- package/dist/renderer/bookmark-renderer.js +1 -1
- package/dist/renderer/canvas-renderer.d.ts +14 -0
- package/dist/renderer/canvas-renderer.js +36 -0
- package/dist/renderer/container-renderer.js +1 -3
- package/dist/renderer/default-text-style-renderer.js +1 -1
- package/dist/renderer/deferred-renderer.js +1 -1
- package/dist/renderer/expanded-renderer.js +1 -1
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/keep-together-renderer.js +1 -1
- package/dist/renderer/link-renderer.js +1 -1
- package/dist/renderer/padding-renderer.js +1 -1
- package/dist/renderer/page-builder-renderer.js +1 -1
- package/dist/renderer/page-renderer.js +1 -3
- package/dist/renderer/pdf-backend.d.ts +15 -4
- package/dist/renderer/pdf-backend.js +125 -35
- package/dist/renderer/pdf-renderer.js +5 -1
- package/dist/renderer/positioned-renderer.js +1 -1
- package/dist/renderer/rectangle-renderer.js +1 -3
- package/dist/renderer/repeating-header-renderer.js +1 -2
- package/dist/renderer/rotated-renderer.js +1 -1
- package/dist/renderer/row-renderer.js +1 -3
- package/dist/renderer/struct-group-renderer.js +1 -1
- package/dist/renderer/svg-renderer.d.ts +11 -0
- package/dist/renderer/svg-renderer.js +27 -0
- package/dist/renderer/text-renderer.js +30 -19
- package/dist/svg/css.d.ts +38 -0
- package/dist/svg/css.js +115 -0
- package/dist/svg/errors.d.ts +24 -0
- package/dist/svg/errors.js +30 -0
- package/dist/svg/gradients.d.ts +34 -0
- package/dist/svg/gradients.js +156 -0
- package/dist/svg/index.d.ts +22 -0
- package/dist/svg/index.js +331 -0
- package/dist/svg/shapes.d.ts +6 -0
- package/dist/svg/shapes.js +177 -0
- package/dist/svg/style.d.ts +61 -0
- package/dist/svg/style.js +144 -0
- package/dist/svg/transform.d.ts +13 -0
- package/dist/svg/transform.js +68 -0
- package/dist/svg/units.d.ts +11 -0
- package/dist/svg/units.js +73 -0
- package/dist/svg/xml.d.ts +24 -0
- package/dist/svg/xml.js +158 -0
- package/dist/text/advance.d.ts +3 -0
- package/dist/text/advance.js +4 -3
- package/dist/text/glyph-coverage.d.ts +15 -0
- package/dist/text/glyph-coverage.js +77 -0
- package/dist/text/line-breaker.d.ts +8 -0
- package/dist/text/line-breaker.js +146 -70
- package/dist/text/shape.d.ts +3 -3
- package/dist/text/shape.js +34 -10
- package/dist/text/text-style.d.ts +13 -0
- package/dist/text/text-style.js +6 -0
- package/dist/text/whitespace.d.ts +4 -0
- package/dist/text/whitespace.js +43 -0
- package/dist/text/word-splitting.d.ts +42 -0
- package/dist/text/word-splitting.js +72 -0
- package/dist/utils/flex-layout.js +67 -18
- package/dist/utils/font-metrics.d.ts +9 -3
- package/dist/utils/pdf-object-manager.d.ts +25 -5
- package/dist/utils/pdf-object-manager.js +82 -18
- package/dist/utils/renderer-registry.d.ts +13 -1
- package/dist/utils/renderer-registry.js +22 -1
- package/dist/utils/woff2.d.ts +6 -0
- package/dist/utils/woff2.js +651 -0
- package/dist/vector/path.d.ts +33 -0
- package/dist/vector/path.js +81 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -116,9 +116,11 @@ const pdf = await renderToBytes(
|
|
|
116
116
|
);
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
Real text layout (
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
Real text layout (real font metrics, kerning, ligatures, word-wrap), full flexbox (`gap` / `justify` /
|
|
120
|
+
`align`, wrap, `flexBasis`, `flexShrink`, `order`), boxes with per-corner radius, gradients and alpha, images and
|
|
121
|
+
**SVG**, a `Canvas` to draw into, custom fonts from a file, bytes or a URL, AcroForm fields,
|
|
122
|
+
**AES-256 password encryption**, tagged PDFs for screen readers, and **real pagination** - content that
|
|
123
|
+
overflows flows onto the next page, headers and footers repeat, and a block can refuse to be split.
|
|
122
124
|
|
|
123
125
|
---
|
|
124
126
|
|
|
@@ -140,9 +142,19 @@ verify all of it:
|
|
|
140
142
|
- **PDF/A-3, matched not approximated.** The conformance graph is hand-built and **passes veraPDF**, the
|
|
141
143
|
official ISO 19005 validator.
|
|
142
144
|
- **Byte-exact round-trips.** Generate and parse are inverses: `generate → parse → regenerate` reproduces
|
|
143
|
-
the identical XML. Over
|
|
145
|
+
the identical XML. Over 1,600 tests hold the line across the workspace.
|
|
144
146
|
- **Schematron, local.** The official EN-16931 + XRechnung rules run via saxon-js (the real XSLT, in pure
|
|
145
147
|
JS) - no Java, no upload.
|
|
148
|
+
- **Vectors, not bitmaps.** `Image("logo.svg")` keeps a logo a VECTOR - our own SVG reader and our own
|
|
149
|
+
XML parser, checked against 10,819 real files and compared pixel for pixel with headless Chrome. Need
|
|
150
|
+
a chart instead? `Canvas` hands you a typed pen for the same vector layer.
|
|
151
|
+
- **Fonts, whatever the container.** `.ttf`, `.woff` and `.woff2` are the same thing at the call site.
|
|
152
|
+
The WOFF2 container and its `glyf` transform are ours; only Brotli is a dependency, and it is loaded
|
|
153
|
+
only if a WOFF2 actually turns up.
|
|
154
|
+
- **Byte-stable output.** The same document rendered twice is byte-identical, PDF/A and ZUGFeRD
|
|
155
|
+
included, across separate processes. An archived invoice stays re-derivable and hashable years later.
|
|
156
|
+
- **Accessible if you ask.** `renderToBytes(doc, { accessible })` emits a full tagged structure tree,
|
|
157
|
+
and the output passes veraPDF's machine-verifiable PDF/UA-1 checks.
|
|
146
158
|
|
|
147
159
|
---
|
|
148
160
|
|
|
@@ -153,12 +165,14 @@ verify all of it:
|
|
|
153
165
|
| **[@jasy/e-invoice](https://npmx.dev/@jasy/e-invoice)** | ZUGFeRD / XRechnung: your data → PDF/A-3 + EN-16931 XML, with local validation. **The prize.** |
|
|
154
166
|
| **[@jasy/cli](https://npmx.dev/@jasy/cli)** | the `jasy` terminal: read · validate · export, headless **and** interactive |
|
|
155
167
|
| **[@jasy/pdf](https://npmx.dev/@jasy/pdf)** | the declarative, Flutter-style PDF layout engine that powers them |
|
|
168
|
+
| **[@jasy/vue](https://npmx.dev/@jasy/vue)** | author PDFs as Vue components - and render them in the browser |
|
|
169
|
+
| **[@jasy/nuxt](https://npmx.dev/@jasy/nuxt)** | the Nuxt module: zero-config PDFs, client **or** server |
|
|
156
170
|
|
|
157
171
|
---
|
|
158
172
|
|
|
159
173
|
## Why you can trust it
|
|
160
174
|
|
|
161
|
-
- **Over
|
|
175
|
+
- **Over 1,600 tests, green** across the workspace (1,190 in the engine alone). The generator and the parser
|
|
162
176
|
are **byte-exact inverses**: `generate → parse → regenerate` reproduces the identical XML. Nothing is
|
|
163
177
|
silently lost.
|
|
164
178
|
- **The same rules the authorities use** - the official KoSIT EN-16931 + XRechnung Schematron, and the
|
|
@@ -170,10 +184,14 @@ verify all of it:
|
|
|
170
184
|
|
|
171
185
|
## Honest scope
|
|
172
186
|
|
|
173
|
-
jasy targets the documents that matter here: invoices, reports, quotes, datasheets
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
187
|
+
jasy targets the documents that matter here: invoices, reports, quotes, datasheets - and it now sets
|
|
188
|
+
text properly: kerning and Latin ligatures from the font's own tables, justification, pluggable
|
|
189
|
+
hyphenation, right-to-left and Arabic shaping, underlines that step around descenders. Pagination is
|
|
190
|
+
real: text splits at line boxes, `keepTogether` moves a card whole, orphans and widows are honoured.
|
|
191
|
+
|
|
192
|
+
It is still **not** a LaTeX or WeasyPrint replacement, and does not try to be: no automatic
|
|
193
|
+
microtypography, no floats, no CSS engine. What it will not draw, it **names** - an SVG filter, a font
|
|
194
|
+
without the glyph, a word that cannot be split - rather than quietly drawing something else.
|
|
177
195
|
|
|
178
196
|
> **Status:** young and pre-1.0. The API can still shift between minor versions. Everything shown here
|
|
179
197
|
> works and is tested.
|
package/dist/api/color.js
CHANGED
|
@@ -22,8 +22,77 @@ export function toColor(input) {
|
|
|
22
22
|
const named = CSS_COLORS[s];
|
|
23
23
|
if (named)
|
|
24
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);
|
|
25
28
|
throw new Error(`Unknown color: "${input}"`);
|
|
26
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
|
+
}
|
|
27
96
|
/** Flutter ARGB: 0xAARRGGBB, alpha FIRST. A 6-digit number has alpha 0x00 = transparent. */
|
|
28
97
|
function fromArgbNumber(n) {
|
|
29
98
|
const a = (n >>> 24) & 0xff;
|
package/dist/api/content.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { PDFElement } from "../elements/pdf-element.js";
|
|
|
3
3
|
import { ColorInput } from "./color.js";
|
|
4
4
|
import { Insets } from "./insets.js";
|
|
5
5
|
import { BoundsInput, SizeInput, RadiusInput } from "./dimension.js";
|
|
6
|
+
import { SvgElement, type SvgSource } from "../elements/svg-element.js";
|
|
7
|
+
import { CanvasElement, type CanvasPaint } from "../elements/canvas-element.js";
|
|
6
8
|
/** A horizontal rule (locked §4). */
|
|
7
9
|
export interface DividerOptions {
|
|
8
10
|
/** Line color (default a light gray). */
|
|
@@ -18,6 +20,13 @@ export interface DividerOptions {
|
|
|
18
20
|
* own, so the padding gives it vertical room and centers the rule. Use inside a Column.
|
|
19
21
|
*/
|
|
20
22
|
export declare function Divider(opts?: DividerOptions): PDFElement;
|
|
23
|
+
/** Options for `Svg`. The same sizing and bounds as an image; `fit` and `radius` do not apply yet. */
|
|
24
|
+
export interface SvgOptions extends BoundsInput {
|
|
25
|
+
width?: SizeInput;
|
|
26
|
+
height?: SizeInput;
|
|
27
|
+
/** Alternate text (tagged PDF). With it the drawing is a `Figure`; without it, decoration. */
|
|
28
|
+
alt?: string;
|
|
29
|
+
}
|
|
21
30
|
/** An image source: a local file path (Node), raw bytes (e.g. a browser fetch/upload), or a `CustomImage`. */
|
|
22
31
|
export type ImageSource = string | Uint8Array | CustomImage;
|
|
23
32
|
/** How the image fills its box (locked §4). Mirrors CSS `object-fit`. */
|
|
@@ -36,8 +45,38 @@ export interface ImageOptions extends BoundsInput {
|
|
|
36
45
|
* the image is a `Figure`; without it (and when rendered `accessible`) it counts as decoration. */
|
|
37
46
|
alt?: string;
|
|
38
47
|
}
|
|
48
|
+
/** Options for `Canvas`. Sizing only - what is drawn is entirely the callback's business. */
|
|
49
|
+
export interface CanvasOptions extends BoundsInput {
|
|
50
|
+
width?: SizeInput;
|
|
51
|
+
height?: SizeInput;
|
|
52
|
+
/** Alternate text (tagged PDF). With it the drawing is a `Figure`; without it, decoration. */
|
|
53
|
+
alt?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A box you draw into yourself - the escape hatch for what the declarative layer has no component
|
|
57
|
+
* for: a sparkline, a seal, a chart, a signature flourish.
|
|
58
|
+
*
|
|
59
|
+
* With no size it FILLS the box it is offered, because there is nothing to derive an intrinsic size
|
|
60
|
+
* from. The callback is handed that resolved size, so a drawing written against `size.width` works
|
|
61
|
+
* at any width. It is clipped to its own box: a canvas is a block IN the layout, never a way around
|
|
62
|
+
* it - text still belongs in `Text`, and this still paginates as one unbreakable block.
|
|
63
|
+
*/
|
|
64
|
+
export declare function Canvas(opts: CanvasOptions, paint: CanvasPaint): CanvasElement;
|
|
65
|
+
export declare function Canvas(paint: CanvasPaint): CanvasElement;
|
|
66
|
+
/**
|
|
67
|
+
* A vector drawing from SVG. `source` is markup, a file path (Node) or the file's bytes. It is read
|
|
68
|
+
* and parsed HERE, not at render time, so a broken file fails on the line that named it.
|
|
69
|
+
*
|
|
70
|
+
* With no size it draws at its intrinsic size; pin one axis and the other follows the `viewBox`. The
|
|
71
|
+
* drawing is scaled uniformly and centred inside its box - SVG's own `preserveAspectRatio` default,
|
|
72
|
+
* which is `fit: "contain"` by another name.
|
|
73
|
+
*/
|
|
74
|
+
export declare function Svg(source: SvgSource, opts?: SvgOptions): SvgElement;
|
|
39
75
|
/**
|
|
40
76
|
* An image. `src` is a local file path (wrapped in a `CustomLocalImage`) or a ready
|
|
41
77
|
* `CustomImage` for non-filesystem sources. Maps to an `ImageElement`.
|
|
78
|
+
*
|
|
79
|
+
* An SVG source is routed to `Svg` instead, so `Image({ src: "logo.svg" })` just works and a logo
|
|
80
|
+
* stays a VECTOR in the PDF rather than becoming a bitmap.
|
|
42
81
|
*/
|
|
43
|
-
export declare function Image(src: ImageSource, opts?: ImageOptions): ImageElement;
|
|
82
|
+
export declare function Image(src: ImageSource, opts?: ImageOptions): ImageElement | SvgElement;
|
package/dist/api/content.js
CHANGED
|
@@ -4,6 +4,8 @@ import { ImageElement, CustomLocalImage, CustomBytesImage, BoxFit, } from "../el
|
|
|
4
4
|
import { toColor } from "./color.js";
|
|
5
5
|
import { toEdges } from "./insets.js";
|
|
6
6
|
import { toBounds, toDimension, toRadius, } from "./dimension.js";
|
|
7
|
+
import { SvgElement } from "../elements/svg-element.js";
|
|
8
|
+
import { CanvasElement } from "../elements/canvas-element.js";
|
|
7
9
|
const DEFAULT_DIVIDER_COLOR = "lightgray";
|
|
8
10
|
const DEFAULT_DIVIDER_MARGIN = { y: 6 };
|
|
9
11
|
/**
|
|
@@ -31,11 +33,92 @@ const FIT = {
|
|
|
31
33
|
cover: BoxFit.cover,
|
|
32
34
|
fill: BoxFit.fill,
|
|
33
35
|
};
|
|
36
|
+
export function Canvas(a, b) {
|
|
37
|
+
const opts = typeof a === "function" ? {} : a;
|
|
38
|
+
const paint = typeof a === "function" ? a : b;
|
|
39
|
+
// Without it the renderer would call `undefined` and hand back a bare "paint is not a function",
|
|
40
|
+
// which says nothing about WHICH element or what to pass. A framework binding cannot cover this:
|
|
41
|
+
// Vue's `required: true` only warns, it still renders.
|
|
42
|
+
if (typeof paint !== "function") {
|
|
43
|
+
throw new Error("@jasy/pdf: Canvas needs a paint callback - Canvas({ width, height }, (c, size) => …).");
|
|
44
|
+
}
|
|
45
|
+
const w = opts.width !== undefined ? toDimension(opts.width) : undefined;
|
|
46
|
+
const h = opts.height !== undefined ? toDimension(opts.height) : undefined;
|
|
47
|
+
return new CanvasElement({
|
|
48
|
+
paint,
|
|
49
|
+
width: w?.points,
|
|
50
|
+
height: h?.points,
|
|
51
|
+
widthFactor: w?.factor,
|
|
52
|
+
heightFactor: h?.factor,
|
|
53
|
+
...toBounds(opts),
|
|
54
|
+
alt: opts.alt,
|
|
55
|
+
})
|
|
56
|
+
.withAlignSelf(opts.alignSelf)
|
|
57
|
+
.withOrder(opts.order)
|
|
58
|
+
.withFlexShrink(opts.flexShrink);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A vector drawing from SVG. `source` is markup, a file path (Node) or the file's bytes. It is read
|
|
62
|
+
* and parsed HERE, not at render time, so a broken file fails on the line that named it.
|
|
63
|
+
*
|
|
64
|
+
* With no size it draws at its intrinsic size; pin one axis and the other follows the `viewBox`. The
|
|
65
|
+
* drawing is scaled uniformly and centred inside its box - SVG's own `preserveAspectRatio` default,
|
|
66
|
+
* which is `fit: "contain"` by another name.
|
|
67
|
+
*/
|
|
68
|
+
export function Svg(source, opts = {}) {
|
|
69
|
+
const w = opts.width !== undefined ? toDimension(opts.width) : undefined;
|
|
70
|
+
const h = opts.height !== undefined ? toDimension(opts.height) : undefined;
|
|
71
|
+
return new SvgElement({
|
|
72
|
+
source,
|
|
73
|
+
width: w?.points,
|
|
74
|
+
height: h?.points,
|
|
75
|
+
widthFactor: w?.factor,
|
|
76
|
+
heightFactor: h?.factor,
|
|
77
|
+
...toBounds(opts),
|
|
78
|
+
alt: opts.alt,
|
|
79
|
+
})
|
|
80
|
+
.withAlignSelf(opts.alignSelf)
|
|
81
|
+
.withOrder(opts.order)
|
|
82
|
+
.withFlexShrink(opts.flexShrink);
|
|
83
|
+
}
|
|
84
|
+
/** SVG is recognised by its extension or by its own opening bytes - never by a caller having to say. */
|
|
85
|
+
function isSvg(src) {
|
|
86
|
+
if (typeof src === "string") {
|
|
87
|
+
// `.svgz` is a GZIPPED svg. It is caught here rather than left to the image path, because the
|
|
88
|
+
// error a user needs says what the file is - not "unsupported image bytes".
|
|
89
|
+
if (/\.svgz$/i.test(src.trim())) {
|
|
90
|
+
throw new Error("@jasy/pdf: SVGZ (a gzip-compressed SVG) is not supported. Decompress it first " +
|
|
91
|
+
"(`gunzip logo.svgz`) and pass the plain .svg.");
|
|
92
|
+
}
|
|
93
|
+
return /\.svg$/i.test(src.trim()) || /^\s*[<\ufeff]/.test(src);
|
|
94
|
+
}
|
|
95
|
+
if (src instanceof Uint8Array) {
|
|
96
|
+
const head = new TextDecoder().decode(src.subarray(0, 200)).trimStart();
|
|
97
|
+
return head.startsWith("<?xml") || head.startsWith("<svg");
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
34
101
|
/**
|
|
35
102
|
* An image. `src` is a local file path (wrapped in a `CustomLocalImage`) or a ready
|
|
36
103
|
* `CustomImage` for non-filesystem sources. Maps to an `ImageElement`.
|
|
104
|
+
*
|
|
105
|
+
* An SVG source is routed to `Svg` instead, so `Image({ src: "logo.svg" })` just works and a logo
|
|
106
|
+
* stays a VECTOR in the PDF rather than becoming a bitmap.
|
|
37
107
|
*/
|
|
38
108
|
export function Image(src, opts = {}) {
|
|
109
|
+
if (isSvg(src)) {
|
|
110
|
+
if (opts.fit !== undefined && opts.fit !== "contain") {
|
|
111
|
+
throw new Error(`@jasy/pdf: fit: "${opts.fit}" is not supported for an SVG yet - it is always drawn ` +
|
|
112
|
+
`contained, which is SVG's own default. Remove the fit, or rasterise the file.`);
|
|
113
|
+
}
|
|
114
|
+
if (opts.radius !== undefined) {
|
|
115
|
+
throw new Error("@jasy/pdf: radius is not supported for an SVG yet - wrap it in a Box instead.");
|
|
116
|
+
}
|
|
117
|
+
return Svg(src, opts);
|
|
118
|
+
}
|
|
119
|
+
return rasterImage(src, opts);
|
|
120
|
+
}
|
|
121
|
+
function rasterImage(src, opts = {}) {
|
|
39
122
|
const w = opts.width !== undefined ? toDimension(opts.width) : undefined;
|
|
40
123
|
const h = opts.height !== undefined ? toDimension(opts.height) : undefined;
|
|
41
124
|
// ANY explicit size scales the image into the box (fit: fill), which is what `<img width height>`
|
package/dist/api/descriptor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Text, Paragraph, span } from "./text.js";
|
|
2
2
|
import { Column, Row, Box, Padding, Spacer, PageBreak, keepTogether, Expanded, Positioned, Link, Anchor, Bookmark, Rotated, RotatedBox, } from "./layout.js";
|
|
3
|
-
import { Divider, Image } from "./content.js";
|
|
3
|
+
import { Canvas, Divider, Image, Svg } from "./content.js";
|
|
4
4
|
import { Page, Document, DefaultTextStyle } from "./structure.js";
|
|
5
5
|
import { PageNumber, PageCount } from "./page-builder.js";
|
|
6
6
|
import { Table } from "./table.js";
|
|
@@ -90,6 +90,11 @@ const REGISTRY = {
|
|
|
90
90
|
"keep-together": (_props, children) => keepTogether(elementChildren(children)),
|
|
91
91
|
divider: (props) => Divider(props),
|
|
92
92
|
image: (props) => Image(props.src, props),
|
|
93
|
+
// `src` is markup, a path or bytes - the same three the factory takes.
|
|
94
|
+
svg: (props) => Svg(props.src, props),
|
|
95
|
+
// `paint` is a function PROP - `<Canvas :paint="drawChart" />` in a template passes one like any
|
|
96
|
+
// other value. (Unlike `PageBuilder`, whose closure is the CHILD and has no template form.)
|
|
97
|
+
canvas: (props) => Canvas(props, props.paint),
|
|
93
98
|
text: (props, children) => Text(textContent(children), props),
|
|
94
99
|
paragraph: (props, children) => Paragraph(textContent(children), props),
|
|
95
100
|
// `<Table>` reads its `<TableRow>`/`<TableCell>` structure raw; one row may be marked `header`.
|
package/dist/api/dimension.d.ts
CHANGED
|
@@ -36,8 +36,9 @@ export interface BoundsInput {
|
|
|
36
36
|
order?: number;
|
|
37
37
|
/**
|
|
38
38
|
* How willingly this child gives up main-axis space when the line overflows (CSS `flex-shrink`),
|
|
39
|
-
* weighted by its own size.
|
|
40
|
-
*
|
|
39
|
+
* weighted by its own size. Default 1, as in CSS: an overflowing line pulls its children back inside
|
|
40
|
+
* instead of letting them run past the edge. Set `0` to keep a child at its natural size and let it
|
|
41
|
+
* overflow. A child is never squeezed below what it can hold - a text stops at its longest word.
|
|
41
42
|
*/
|
|
42
43
|
flexShrink?: number;
|
|
43
44
|
minWidth?: SizeInput;
|
package/dist/api/font-url.js
CHANGED
|
@@ -29,7 +29,7 @@ function describeSignature(bytes) {
|
|
|
29
29
|
if (tag === "wOFF")
|
|
30
30
|
return { ok: true, what: "WOFF" };
|
|
31
31
|
if (tag === "wOF2")
|
|
32
|
-
return { ok:
|
|
32
|
+
return { ok: true, what: "WOFF2" };
|
|
33
33
|
if (tag === "ttcf")
|
|
34
34
|
return { ok: false, what: "a TrueType Collection, which jasy does not parse yet" };
|
|
35
35
|
if (tag.startsWith("<") || tag === "%PDF")
|
|
@@ -117,7 +117,8 @@ async function fetchFont(url) {
|
|
|
117
117
|
}
|
|
118
118
|
const { ok, what } = describeSignature(bytes);
|
|
119
119
|
if (!ok) {
|
|
120
|
-
throw new FontUrlError(`the file at ${url} is ${what}. jasy embeds TrueType-flavoured fonts
|
|
120
|
+
throw new FontUrlError(`the file at ${url} is ${what}. jasy embeds TrueType-flavoured fonts ` +
|
|
121
|
+
`(.ttf, .woff and .woff2)`);
|
|
121
122
|
}
|
|
122
123
|
return bytes;
|
|
123
124
|
}
|
package/dist/api/structure.d.ts
CHANGED
|
@@ -151,6 +151,13 @@ export interface RenderOptions {
|
|
|
151
151
|
/** What to do when content is taller than a page and cannot break: `"error"` throws (default),
|
|
152
152
|
* `"warn"` logs and clips, `"ignore"` clips silently. It is always clipped either way. */
|
|
153
153
|
onOverflow?: OverflowPolicy;
|
|
154
|
+
/**
|
|
155
|
+
* Called once, after the render, with the characters no font could draw - they were removed rather
|
|
156
|
+
* than emitted as `.notdef`, which PDF/A forbids. Deliberately a callback and not a policy: a
|
|
157
|
+
* missing tick mark must never stop an invoice from being produced, but the application should be
|
|
158
|
+
* able to SAY which character went missing, where a server log would never be read.
|
|
159
|
+
*/
|
|
160
|
+
onMissingGlyphs?: (characters: string[]) => void;
|
|
154
161
|
/** Encrypt the PDF with a password (AES-256, the newest standard). NOT compatible with PDF/A
|
|
155
162
|
* (ZUGFeRD invoices) - encrypting one throws, since PDF/A forbids encryption. */
|
|
156
163
|
encrypt?: EncryptOptions;
|
package/dist/api/structure.js
CHANGED
|
@@ -14,6 +14,7 @@ import { Column } from "./layout.js";
|
|
|
14
14
|
import { loadFontFromUrl } from "./font-url.js";
|
|
15
15
|
import { toEdges } from "./insets.js";
|
|
16
16
|
import { toTextStyleOverride } from "./text.js";
|
|
17
|
+
import { isWoff2, woff2ToSfnt } from "../utils/woff2.js";
|
|
17
18
|
const MM_TO_PT = 72 / 25.4; // 1 mm in PDF points
|
|
18
19
|
/** Millimetres → a custom page size, e.g. a 50×65 mm label: `Page({ size: mm(50, 65) }, …)`. */
|
|
19
20
|
export function mm(width, height) {
|
|
@@ -67,6 +68,29 @@ const docMeta = new WeakMap();
|
|
|
67
68
|
const docFonts = new WeakMap();
|
|
68
69
|
// The document's color-emoji fallback source, kept beside the element and applied at render time.
|
|
69
70
|
const docEmoji = new WeakMap();
|
|
71
|
+
/**
|
|
72
|
+
* Unwraps a WOFF2 into plain sfnt bytes. WOFF1 is left alone - `registerCustomFont` handles it
|
|
73
|
+
* synchronously, having no dependency to load - so only the async case needs to happen up here.
|
|
74
|
+
*/
|
|
75
|
+
async function unwrapFontContainers(fonts) {
|
|
76
|
+
const convert = async (bytes) => isWoff2(bytes) ? await woff2ToSfnt(bytes) : bytes;
|
|
77
|
+
const out = {};
|
|
78
|
+
for (const [name, value] of Object.entries(fonts)) {
|
|
79
|
+
if (isFontBytes(value)) {
|
|
80
|
+
out[name] = await convert(value);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const family = { normal: await convert(value.normal) };
|
|
84
|
+
if (value.bold)
|
|
85
|
+
family.bold = await convert(value.bold);
|
|
86
|
+
if (value.italic)
|
|
87
|
+
family.italic = await convert(value.italic);
|
|
88
|
+
if (value.boldItalic)
|
|
89
|
+
family.boldItalic = await convert(value.boldItalic);
|
|
90
|
+
out[name] = family;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
70
94
|
/** Reads any path sources to bytes, leaving bytes / families as-is. */
|
|
71
95
|
function resolveFontSource(source) {
|
|
72
96
|
const read = (s) => (typeof s === "string" ? readFileBytes(s) : s);
|
|
@@ -130,10 +154,13 @@ export async function renderPdf(doc, options) {
|
|
|
130
154
|
};
|
|
131
155
|
// Fonts registered via doc.addFont(...) plus any passed in options (options win on a name clash).
|
|
132
156
|
const registered = docFonts.get(doc);
|
|
133
|
-
|
|
157
|
+
// Containers are unwrapped HERE, before the (synchronous) document constructor registers anything.
|
|
158
|
+
// WOFF2 needs Brotli, which is imported lazily and is therefore async - and this is the last async
|
|
159
|
+
// point every font passes through, whether it came from `addFont`, a URL, or these options.
|
|
160
|
+
const fonts = await unwrapFontContainers({
|
|
134
161
|
...Object.fromEntries(registered ?? []),
|
|
135
162
|
...options?.fonts,
|
|
136
|
-
};
|
|
163
|
+
});
|
|
137
164
|
const attachments = options?.attachments ?? [];
|
|
138
165
|
// The security handler's key derivation is async (WebCrypto), so build it here, before the sync
|
|
139
166
|
// PDFDocument constructor wires it into the object manager.
|
|
@@ -143,10 +170,14 @@ export async function renderPdf(doc, options) {
|
|
|
143
170
|
// A throwaway PDFDocument whose build() yields this tree, reusing the engine's standard
|
|
144
171
|
// font registration + config handling (the constructor does both). Custom fonts are
|
|
145
172
|
// registered here, before layout/render, so both the metrics and the backend see them.
|
|
173
|
+
// Captured so the caller can be told what had to be dropped: `render()` is static and builds its own
|
|
174
|
+
// instance, so this is the only place the object manager surfaces.
|
|
175
|
+
let manager;
|
|
146
176
|
const Anon = class extends PDFDocument {
|
|
147
177
|
constructor() {
|
|
148
178
|
super(config);
|
|
149
179
|
const om = this.objectManager;
|
|
180
|
+
manager = om;
|
|
150
181
|
om.setCompress(options?.compress !== false); // FlateDecode streams by default
|
|
151
182
|
om.setOverflowPolicy(options?.onOverflow ?? "error");
|
|
152
183
|
om.setKerning(options?.kerning !== false); // on by default (experimental); `kerning:false` opts out
|
|
@@ -207,7 +238,11 @@ export async function renderPdf(doc, options) {
|
|
|
207
238
|
return doc;
|
|
208
239
|
}
|
|
209
240
|
};
|
|
210
|
-
|
|
241
|
+
const pdf = await Anon.render();
|
|
242
|
+
const missing = manager?.getMissingGlyphs() ?? [];
|
|
243
|
+
if (missing.length > 0)
|
|
244
|
+
options?.onMissingGlyphs?.(missing.map((cp) => String.fromCodePoint(cp)));
|
|
245
|
+
return pdf;
|
|
211
246
|
}
|
|
212
247
|
/** Renders a `Document(...)` tree to PDF bytes (e.g. for a download / save dialog). */
|
|
213
248
|
export async function renderToBytes(doc, options) {
|
package/dist/api/text.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { TextOverflow } from "../text/line-breaker.js";
|
|
|
3
3
|
import type { Direction } from "../text/bidi.js";
|
|
4
4
|
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
|
+
export type { TextOverflow, Direction, TextTransform, Hyphenator };
|
|
7
8
|
/** Text styling shared by `Text`, `Paragraph` and `span`. `bold`/`italic` are booleans
|
|
8
9
|
* (locked §7.3) and combine into one engine `FontStyle`. */
|
|
9
10
|
export interface TextStyle {
|
|
@@ -35,6 +36,16 @@ export interface TextStyle {
|
|
|
35
36
|
wordSpacing?: number;
|
|
36
37
|
/** CSS `text-indent`, in points: how far the FIRST line of a paragraph starts in. */
|
|
37
38
|
textIndent?: number;
|
|
39
|
+
/** Split a word that is wider than its box, anywhere, no hyphen (CSS `overflow-wrap:
|
|
40
|
+
* break-word`). The floor under `hyphenate`: an e-mail, an IBAN or an invoice number has no
|
|
41
|
+
* valid hyphenation point anywhere. Off by default - a too-wide word overflows, as in CSS. */
|
|
42
|
+
breakWord?: boolean;
|
|
43
|
+
/** The ligatures the font's designer drew (`fi`, `fl`, `ffi`). ON by default, like kerning -
|
|
44
|
+
* `false` opts this text out. Embedded fonts only; the standard-14 have no GSUB. */
|
|
45
|
+
ligatures?: boolean;
|
|
46
|
+
/** Language-aware splitting: given a word, return its parts. A hyphen is drawn at the break.
|
|
47
|
+
* Pluggable, so no pattern data ships here: pass `hyphen`, `hyphenopoly`, or your own. */
|
|
48
|
+
hyphenate?: Hyphenator;
|
|
38
49
|
/** CSS `vertical-align` for a SPAN: raises a footnote marker or lowers an index. It shifts the
|
|
39
50
|
* baseline only - pass a smaller `size` too if you want the browser's `<sup>` look. */
|
|
40
51
|
verticalAlign?: VerticalTextAlign;
|
|
@@ -45,7 +56,7 @@ export interface TextStyle {
|
|
|
45
56
|
* same-document /GoTo instead of a URL). Use for a clickable table of contents / cross-reference. */
|
|
46
57
|
to?: string;
|
|
47
58
|
}
|
|
48
|
-
export interface TextOptions extends TextStyle {
|
|
59
|
+
export interface TextOptions extends Omit<TextStyle, "verticalAlign"> {
|
|
49
60
|
/** Text-internal alignment (left/center/right) - independent of a parent's `cross` (§5). */
|
|
50
61
|
align?: "left" | "center" | "right" | "justify";
|
|
51
62
|
/** Cap the number of wrapped lines (default: unlimited - the text grows down as far as it needs,
|
|
@@ -108,6 +119,16 @@ export interface TextDefaults {
|
|
|
108
119
|
wordSpacing?: number;
|
|
109
120
|
/** CSS `text-indent`, in points: how far the FIRST line of a paragraph starts in. */
|
|
110
121
|
textIndent?: number;
|
|
122
|
+
/** Split a word that is wider than its box, anywhere, no hyphen (CSS `overflow-wrap:
|
|
123
|
+
* break-word`). The floor under `hyphenate`: an e-mail, an IBAN or an invoice number has no
|
|
124
|
+
* valid hyphenation point anywhere. Off by default - a too-wide word overflows, as in CSS. */
|
|
125
|
+
breakWord?: boolean;
|
|
126
|
+
/** The ligatures the font's designer drew (`fi`, `fl`, `ffi`). ON by default, like kerning -
|
|
127
|
+
* `false` opts this text out. Embedded fonts only; the standard-14 have no GSUB. */
|
|
128
|
+
ligatures?: boolean;
|
|
129
|
+
/** Language-aware splitting: given a word, return its parts. A hyphen is drawn at the break.
|
|
130
|
+
* Pluggable, so no pattern data ships here: pass `hyphen`, `hyphenopoly`, or your own. */
|
|
131
|
+
hyphenate?: Hyphenator;
|
|
111
132
|
}
|
|
112
133
|
/** Maps the `Text`-style option names onto a partial engine `ResolvedTextStyle` (only the set
|
|
113
134
|
* fields), for seeding the inheritance cascade. */
|
package/dist/api/text.js
CHANGED
|
@@ -100,6 +100,9 @@ export function Text(content, opts = {}) {
|
|
|
100
100
|
textTransform: opts.textTransform,
|
|
101
101
|
wordSpacing: opts.wordSpacing,
|
|
102
102
|
textIndent: opts.textIndent,
|
|
103
|
+
breakWord: opts.breakWord,
|
|
104
|
+
ligatures: opts.ligatures,
|
|
105
|
+
hyphenate: opts.hyphenate,
|
|
103
106
|
role: opts.role,
|
|
104
107
|
});
|
|
105
108
|
}
|
|
@@ -142,5 +145,11 @@ export function toTextStyleOverride(opts) {
|
|
|
142
145
|
style.wordSpacing = opts.wordSpacing;
|
|
143
146
|
if (opts.textIndent !== undefined)
|
|
144
147
|
style.textIndent = opts.textIndent;
|
|
148
|
+
if (opts.breakWord !== undefined)
|
|
149
|
+
style.breakWord = opts.breakWord;
|
|
150
|
+
if (opts.ligatures !== undefined)
|
|
151
|
+
style.ligatures = opts.ligatures;
|
|
152
|
+
if (opts.hyphenate !== undefined)
|
|
153
|
+
style.hyphenate = opts.hyphenate;
|
|
145
154
|
return style;
|
|
146
155
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type ColorInput } from "../api/color.js";
|
|
2
|
+
import { type GradientInput } from "../api/gradient.js";
|
|
3
|
+
import type { IRNode, StrokeStyle } from "../ir/display-list.js";
|
|
4
|
+
/**
|
|
5
|
+
* The imperative escape hatch: a box in the layout that you draw into yourself.
|
|
6
|
+
*
|
|
7
|
+
* It is the SECOND producer of the vector layer (SVG is the first), so it needs no new IR and no
|
|
8
|
+
* backend change - everything it draws is a `Path` node like any other, which means it tags, it
|
|
9
|
+
* paginates, and it works in the browser.
|
|
10
|
+
*
|
|
11
|
+
* Three deliberate departures from the PDFKit-shaped painters (react-pdf's included):
|
|
12
|
+
*
|
|
13
|
+
* 1. **Typed to the value.** `cap: "round"` autocompletes and a wrong one is a compile error, where
|
|
14
|
+
* react-pdf's `paint` callback receives `painter: any` - no help on the one thing you use.
|
|
15
|
+
* 2. **No hidden state.** There is no `lineWidth(2)` that a later `stroke()` happens to pick up, and
|
|
16
|
+
* no `save()`/`restore()` pair to forget: a transform or a clip is a SCOPE (`group`, `clipped`)
|
|
17
|
+
* that closes itself.
|
|
18
|
+
* 3. **One coordinate system.** Top-left, y down, points - the same as every other element in jasy
|
|
19
|
+
* and the same as SVG. The origin is the canvas box, so `0,0` is its own corner.
|
|
20
|
+
*/
|
|
21
|
+
/** Where a shape ends up. A gradient resolves against the shape's own bounding box, as in SVG. */
|
|
22
|
+
export type PaintInput = ColorInput | GradientInput;
|
|
23
|
+
export interface StrokeOptions {
|
|
24
|
+
/** Line width in points. Default 1. */
|
|
25
|
+
width?: number;
|
|
26
|
+
cap?: StrokeStyle["cap"];
|
|
27
|
+
join?: StrokeStyle["join"];
|
|
28
|
+
miterLimit?: number;
|
|
29
|
+
/** Dash lengths in points, plus the phase to start at. */
|
|
30
|
+
dash?: number[];
|
|
31
|
+
dashOffset?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface FillOptions {
|
|
34
|
+
/** `evenodd` cuts a hole where subpaths overlap an odd number of times. Default `nonzero`. */
|
|
35
|
+
rule?: "nonzero" | "evenodd";
|
|
36
|
+
}
|
|
37
|
+
/** A transform applied for the duration of a `group`. Composed in this order: translate, rotate, scale. */
|
|
38
|
+
export interface TransformOptions {
|
|
39
|
+
translate?: [number, number];
|
|
40
|
+
/** Degrees, clockwise on the page. */
|
|
41
|
+
rotate?: number;
|
|
42
|
+
/** One number scales both axes. */
|
|
43
|
+
scale?: number | [number, number];
|
|
44
|
+
/** The point `rotate` and `scale` happen around. Default `[0, 0]`. */
|
|
45
|
+
origin?: [number, number];
|
|
46
|
+
}
|
|
47
|
+
/** The size the callback was given, so a drawing can be written relative to its box. */
|
|
48
|
+
export interface CanvasSize {
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
}
|
|
52
|
+
export declare class CanvasPainter {
|
|
53
|
+
readonly size: CanvasSize;
|
|
54
|
+
/** The nodes drawn so far, in order. */
|
|
55
|
+
private readonly nodes;
|
|
56
|
+
/** The path being built. Emptied by every painting call, so a shape cannot leak into the next. */
|
|
57
|
+
private current;
|
|
58
|
+
private cursor;
|
|
59
|
+
private start;
|
|
60
|
+
constructor(size: CanvasSize);
|
|
61
|
+
/** Everything drawn, for the renderer. */
|
|
62
|
+
drawn(): IRNode[];
|
|
63
|
+
move(x: number, y: number): this;
|
|
64
|
+
line(x: number, y: number): this;
|
|
65
|
+
/** A cubic Bézier - the curve PDF draws natively. */
|
|
66
|
+
curve(x1: number, y1: number, x2: number, y2: number, x: number, y: number): this;
|
|
67
|
+
/** A quadratic Bézier. PDF has no operator for one, so it is raised to a cubic exactly. */
|
|
68
|
+
quad(cx: number, cy: number, x: number, y: number): this;
|
|
69
|
+
close(): this;
|
|
70
|
+
rect(x: number, y: number, width: number, height: number): this;
|
|
71
|
+
circle(cx: number, cy: number, r: number): this;
|
|
72
|
+
/** Four cubics; the constant is the standard circle-to-Bézier one. */
|
|
73
|
+
ellipse(cx: number, cy: number, rx: number, ry: number): this;
|
|
74
|
+
polygon(points: readonly (readonly [number, number])[]): this;
|
|
75
|
+
/** SVG path data, through the same reader `Svg()` uses - arcs and shorthands included. */
|
|
76
|
+
path(d: string): this;
|
|
77
|
+
/** Fills the path built so far and starts a new one. Default black, as in SVG and PDF. */
|
|
78
|
+
fill(paint?: PaintInput, options?: FillOptions): this;
|
|
79
|
+
/** Strokes the path built so far and starts a new one. */
|
|
80
|
+
stroke(color?: ColorInput, options?: StrokeOptions): this;
|
|
81
|
+
fillAndStroke(paint: PaintInput, color: ColorInput, options?: StrokeOptions & FillOptions): this;
|
|
82
|
+
/**
|
|
83
|
+
* Draws inside a transform. It closes itself, so there is no `restore()` to forget - and a nested
|
|
84
|
+
* `group` composes, exactly like a `<g transform>` in SVG.
|
|
85
|
+
*/
|
|
86
|
+
group(transform: TransformOptions, draw: (c: CanvasPainter) => void): this;
|
|
87
|
+
/**
|
|
88
|
+
* Draws clipped to a path. `build` draws the clip shape (nothing is painted from it), `draw` draws
|
|
89
|
+
* what the clip applies to.
|
|
90
|
+
*/
|
|
91
|
+
clipped(build: (c: CanvasPainter) => void, draw: (c: CanvasPainter) => void, options?: FillOptions): this;
|
|
92
|
+
private paint;
|
|
93
|
+
/**
|
|
94
|
+
* A path may not cross the edge of a scope. It would be built in one coordinate space and painted
|
|
95
|
+
* in another, and the version that just dropped it made a shape vanish without a word - exactly
|
|
96
|
+
* the silence this codebase refuses everywhere else.
|
|
97
|
+
*/
|
|
98
|
+
private assertNoPendingPath;
|
|
99
|
+
private resolve;
|
|
100
|
+
private strokeStyle;
|
|
101
|
+
private matrixOf;
|
|
102
|
+
}
|