@jasy/pdf 1.0.0-alpha.8 → 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 +29 -10
- package/dist/api/color.js +69 -0
- package/dist/api/content.d.ts +45 -5
- package/dist/api/content.js +96 -7
- package/dist/api/descriptor.js +6 -1
- package/dist/api/dimension.d.ts +57 -0
- package/dist/api/dimension.js +27 -0
- package/dist/api/font-url.d.ts +28 -0
- package/dist/api/font-url.js +135 -0
- package/dist/api/gradient.d.ts +53 -0
- package/dist/api/gradient.js +97 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/insets.d.ts +19 -12
- package/dist/api/insets.js +18 -7
- package/dist/api/layout.d.ts +20 -6
- package/dist/api/layout.js +31 -6
- package/dist/api/page-builder.d.ts +8 -0
- package/dist/api/page-builder.js +12 -0
- package/dist/api/structure.d.ts +12 -0
- package/dist/api/structure.js +54 -7
- package/dist/api/text.d.ts +65 -8
- package/dist/api/text.js +38 -5
- package/dist/canvas/painter.d.ts +102 -0
- package/dist/canvas/painter.js +183 -0
- package/dist/edit/document.d.ts +14 -4
- package/dist/edit/document.js +15 -22
- package/dist/edit/fill.d.ts +19 -2
- package/dist/edit/fill.js +63 -34
- package/dist/edit/flatten.d.ts +13 -1
- package/dist/edit/flatten.js +38 -13
- package/dist/edit/index.d.ts +2 -0
- package/dist/edit/index.js +3 -1
- package/dist/edit/objects.d.ts +5 -0
- package/dist/edit/objects.js +27 -0
- package/dist/edit/writer.d.ts +7 -0
- package/dist/edit/writer.js +23 -1
- package/dist/elements/canvas-element.d.ts +38 -0
- package/dist/elements/canvas-element.js +47 -0
- package/dist/elements/container-element.d.ts +23 -1
- package/dist/elements/container-element.js +31 -10
- package/dist/elements/image-element.d.ts +18 -4
- package/dist/elements/image-element.js +27 -12
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.js +2 -0
- package/dist/elements/layout/expanded-element.d.ts +5 -1
- package/dist/elements/layout/expanded-element.js +16 -3
- 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 +9 -2
- package/dist/elements/layout/padding-element.js +12 -2
- package/dist/elements/layout/page-builder-element.js +2 -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 +75 -1
- package/dist/elements/pdf-element.js +80 -1
- package/dist/elements/rectangle-element.d.ts +22 -6
- package/dist/elements/rectangle-element.js +31 -12
- package/dist/elements/row-element.d.ts +26 -1
- package/dist/elements/row-element.js +31 -10
- package/dist/elements/svg-element.d.ts +41 -0
- package/dist/elements/svg-element.js +93 -0
- package/dist/elements/text-element.d.ts +92 -1
- package/dist/elements/text-element.js +208 -33
- package/dist/ir/display-list.d.ts +67 -11
- package/dist/ir/display-list.js +8 -1
- package/dist/layout/alignment.d.ts +10 -0
- package/dist/layout/alignment.js +6 -0
- package/dist/layout/box-constraints.d.ts +53 -0
- package/dist/layout/box-constraints.js +72 -0
- package/dist/layout/insets.d.ts +28 -0
- package/dist/layout/insets.js +20 -0
- 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/image-renderer.js +2 -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 +21 -4
- package/dist/renderer/pdf-backend.js +198 -61
- package/dist/renderer/pdf-document-renderer.js +8 -1
- package/dist/renderer/pdf-renderer.js +5 -1
- package/dist/renderer/positioned-renderer.js +1 -1
- package/dist/renderer/rectangle-renderer.js +9 -5
- 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.d.ts +2 -2
- package/dist/renderer/text-renderer.js +176 -57
- 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 +8 -4
- package/dist/text/arabic.d.ts +20 -0
- package/dist/text/arabic.js +101 -0
- package/dist/text/bidi.d.ts +33 -0
- package/dist/text/bidi.js +94 -0
- package/dist/text/font-fallback.d.ts +25 -0
- package/dist/text/font-fallback.js +38 -0
- package/dist/text/glyph-coverage.d.ts +15 -0
- package/dist/text/glyph-coverage.js +77 -0
- package/dist/text/line-breaker.d.ts +43 -1
- package/dist/text/line-breaker.js +181 -45
- package/dist/text/orphans-widows.d.ts +27 -0
- package/dist/text/orphans-widows.js +53 -0
- package/dist/text/shape.d.ts +29 -0
- package/dist/text/shape.js +91 -0
- package/dist/text/text-style.d.ts +38 -0
- package/dist/text/text-style.js +50 -1
- 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.d.ts +24 -4
- package/dist/utils/flex-layout.js +218 -7
- package/dist/utils/font-metrics.d.ts +13 -2
- package/dist/utils/gsub.d.ts +36 -0
- package/dist/utils/gsub.js +178 -0
- package/dist/utils/inflate.d.ts +16 -0
- package/dist/utils/inflate.js +52 -0
- package/dist/utils/pdf-object-manager.d.ts +44 -3
- package/dist/utils/pdf-object-manager.js +176 -14
- package/dist/utils/renderer-registry.d.ts +13 -1
- package/dist/utils/renderer-registry.js +22 -1
- package/dist/utils/ttf-parser.d.ts +6 -0
- package/dist/utils/ttf-parser.js +19 -0
- package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
- package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
- package/dist/utils/woff.d.ts +10 -0
- package/dist/utils/woff.js +116 -0
- package/dist/utils/woff2.d.ts +6 -0
- package/dist/utils/woff2.js +651 -0
- package/dist/validators/element-validator.js +9 -2
- package/dist/vector/path.d.ts +33 -0
- package/dist/vector/path.js +81 -0
- package/package.json +6 -2
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import svgpath from "svgpath";
|
|
2
|
+
import { quadToCubic } from "../vector/path.js";
|
|
3
|
+
import { SvgParseError } from "./errors.js";
|
|
4
|
+
import { length } from "./units.js";
|
|
5
|
+
/**
|
|
6
|
+
* Every drawable SVG shape becomes one list of path commands, so the rest of the pipeline knows about
|
|
7
|
+
* exactly one kind of geometry. A rect is a path, a circle is four arcs, `<path>` is a path already.
|
|
8
|
+
*
|
|
9
|
+
* The `d` grammar - relative coordinates, the `S`/`T` shorthands, and elliptical arcs - is handled by
|
|
10
|
+
* `svgpath`: `.abs().unshort().unarc()` leaves only M/L/H/V/C/Q/Z. Arcs are the reason: converting one
|
|
11
|
+
* to Béziers is the SVG spec's own appendix, and a subtly wrong version looks ALMOST right.
|
|
12
|
+
*/
|
|
13
|
+
const num = (attributes, name, fallback = 0) => length(attributes[name], `the "${name}" attribute`, fallback);
|
|
14
|
+
/** Four cubic arcs approximating a full ellipse. The constant is the standard circle-to-Bézier one. */
|
|
15
|
+
const KAPPA = 0.5522847498307936;
|
|
16
|
+
function ellipse(cx, cy, rx, ry) {
|
|
17
|
+
if (rx <= 0 || ry <= 0)
|
|
18
|
+
return [];
|
|
19
|
+
const [ox, oy] = [rx * KAPPA, ry * KAPPA];
|
|
20
|
+
return [
|
|
21
|
+
{ op: "m", x: cx - rx, y: cy },
|
|
22
|
+
{ op: "c", x1: cx - rx, y1: cy - oy, x2: cx - ox, y2: cy - ry, x: cx, y: cy - ry },
|
|
23
|
+
{ op: "c", x1: cx + ox, y1: cy - ry, x2: cx + rx, y2: cy - oy, x: cx + rx, y: cy },
|
|
24
|
+
{ op: "c", x1: cx + rx, y1: cy + oy, x2: cx + ox, y2: cy + ry, x: cx, y: cy + ry },
|
|
25
|
+
{ op: "c", x1: cx - ox, y1: cy + ry, x2: cx - rx, y2: cy + oy, x: cx - rx, y: cy },
|
|
26
|
+
{ op: "z" },
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
function rect(attributes) {
|
|
30
|
+
const [x, y] = [num(attributes, "x"), num(attributes, "y")];
|
|
31
|
+
const [w, h] = [num(attributes, "width"), num(attributes, "height")];
|
|
32
|
+
if (w <= 0 || h <= 0)
|
|
33
|
+
return [];
|
|
34
|
+
// An `rx` alone implies the same `ry`, and vice versa; both clamp to half the side.
|
|
35
|
+
const rxRaw = attributes["rx"] !== undefined ? num(attributes, "rx") : num(attributes, "ry");
|
|
36
|
+
const ryRaw = attributes["ry"] !== undefined ? num(attributes, "ry") : rxRaw;
|
|
37
|
+
const rx = Math.min(rxRaw, w / 2);
|
|
38
|
+
const ry = Math.min(ryRaw, h / 2);
|
|
39
|
+
if (rx <= 0 || ry <= 0) {
|
|
40
|
+
return [
|
|
41
|
+
{ op: "m", x, y },
|
|
42
|
+
{ op: "l", x: x + w, y },
|
|
43
|
+
{ op: "l", x: x + w, y: y + h },
|
|
44
|
+
{ op: "l", x, y: y + h },
|
|
45
|
+
{ op: "z" },
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
const [ox, oy] = [rx * KAPPA, ry * KAPPA];
|
|
49
|
+
return [
|
|
50
|
+
{ op: "m", x: x + rx, y },
|
|
51
|
+
{ op: "l", x: x + w - rx, y },
|
|
52
|
+
{ op: "c", x1: x + w - rx + ox, y1: y, x2: x + w, y2: y + ry - oy, x: x + w, y: y + ry },
|
|
53
|
+
{ op: "l", x: x + w, y: y + h - ry },
|
|
54
|
+
{
|
|
55
|
+
op: "c",
|
|
56
|
+
x1: x + w,
|
|
57
|
+
y1: y + h - ry + oy,
|
|
58
|
+
x2: x + w - rx + ox,
|
|
59
|
+
y2: y + h,
|
|
60
|
+
x: x + w - rx,
|
|
61
|
+
y: y + h,
|
|
62
|
+
},
|
|
63
|
+
{ op: "l", x: x + rx, y: y + h },
|
|
64
|
+
{ op: "c", x1: x + rx - ox, y1: y + h, x2: x, y2: y + h - ry + oy, x, y: y + h - ry },
|
|
65
|
+
{ op: "l", x, y: y + ry },
|
|
66
|
+
{ op: "c", x1: x, y1: y + ry - oy, x2: x + rx - ox, y2: y, x: x + rx, y },
|
|
67
|
+
{ op: "z" },
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
function points(value) {
|
|
71
|
+
const found = String(value ?? "").match(/[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g);
|
|
72
|
+
const out = [];
|
|
73
|
+
for (let i = 0; found && i + 1 < found.length; i += 2) {
|
|
74
|
+
out.push([Number(found[i]), Number(found[i + 1])]);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function polyline(value, close) {
|
|
79
|
+
const list = points(value);
|
|
80
|
+
if (list.length === 0)
|
|
81
|
+
return [];
|
|
82
|
+
const out = [{ op: "m", x: list[0][0], y: list[0][1] }];
|
|
83
|
+
for (const [x, y] of list.slice(1))
|
|
84
|
+
out.push({ op: "l", x, y });
|
|
85
|
+
if (close)
|
|
86
|
+
out.push({ op: "z" });
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
/** `d` through svgpath, then the remaining segment kinds mapped onto the IR's m/l/c/z. */
|
|
90
|
+
export function pathData(d) {
|
|
91
|
+
const out = [];
|
|
92
|
+
let [cx, cy] = [0, 0];
|
|
93
|
+
let [startX, startY] = [0, 0];
|
|
94
|
+
try {
|
|
95
|
+
const parsed = svgpath(d);
|
|
96
|
+
// svgpath REPORTS a malformed `d` on the instance instead of throwing, so an unchecked call
|
|
97
|
+
// quietly yields an empty path - the shape simply disappears.
|
|
98
|
+
// Its bundled types omit `err`, though it IS the API: svgpath never throws on malformed input,
|
|
99
|
+
// it records the reason and yields no segments - so the shape would just disappear.
|
|
100
|
+
const err = parsed.err;
|
|
101
|
+
if (err)
|
|
102
|
+
throw new SvgParseError(`path data could not be read: ${err}`);
|
|
103
|
+
parsed
|
|
104
|
+
.abs()
|
|
105
|
+
.unshort()
|
|
106
|
+
.unarc()
|
|
107
|
+
.iterate((segment) => {
|
|
108
|
+
const [op, ...a] = segment;
|
|
109
|
+
switch (op) {
|
|
110
|
+
case "M":
|
|
111
|
+
[cx, cy] = [a[0], a[1]];
|
|
112
|
+
[startX, startY] = [cx, cy];
|
|
113
|
+
out.push({ op: "m", x: cx, y: cy });
|
|
114
|
+
break;
|
|
115
|
+
case "L":
|
|
116
|
+
[cx, cy] = [a[0], a[1]];
|
|
117
|
+
out.push({ op: "l", x: cx, y: cy });
|
|
118
|
+
break;
|
|
119
|
+
case "H":
|
|
120
|
+
cx = a[0];
|
|
121
|
+
out.push({ op: "l", x: cx, y: cy });
|
|
122
|
+
break;
|
|
123
|
+
case "V":
|
|
124
|
+
cy = a[0];
|
|
125
|
+
out.push({ op: "l", x: cx, y: cy });
|
|
126
|
+
break;
|
|
127
|
+
case "C":
|
|
128
|
+
out.push({ op: "c", x1: a[0], y1: a[1], x2: a[2], y2: a[3], x: a[4], y: a[5] });
|
|
129
|
+
[cx, cy] = [a[4], a[5]];
|
|
130
|
+
break;
|
|
131
|
+
case "Q":
|
|
132
|
+
// PDF has no quadratic operator; the same conversion a TrueType outline needs.
|
|
133
|
+
out.push(quadToCubic(cx, cy, a[0], a[1], a[2], a[3]));
|
|
134
|
+
[cx, cy] = [a[2], a[3]];
|
|
135
|
+
break;
|
|
136
|
+
case "Z":
|
|
137
|
+
out.push({ op: "z" });
|
|
138
|
+
[cx, cy] = [startX, startY];
|
|
139
|
+
break;
|
|
140
|
+
default:
|
|
141
|
+
throw new SvgParseError(`path data: unexpected segment "${op}"`);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error instanceof SvgParseError)
|
|
147
|
+
throw error;
|
|
148
|
+
throw new SvgParseError(`path data could not be read: ${error.message}`);
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/** The geometry of one shape element, or null if the tag is not a shape. */
|
|
153
|
+
export function shapeOf(tagName, attributes) {
|
|
154
|
+
switch (tagName) {
|
|
155
|
+
case "path":
|
|
156
|
+
return attributes["d"] ? pathData(String(attributes["d"])) : [];
|
|
157
|
+
case "rect":
|
|
158
|
+
return rect(attributes);
|
|
159
|
+
case "circle": {
|
|
160
|
+
const r = num(attributes, "r");
|
|
161
|
+
return ellipse(num(attributes, "cx"), num(attributes, "cy"), r, r);
|
|
162
|
+
}
|
|
163
|
+
case "ellipse":
|
|
164
|
+
return ellipse(num(attributes, "cx"), num(attributes, "cy"), num(attributes, "rx"), num(attributes, "ry"));
|
|
165
|
+
case "line":
|
|
166
|
+
return [
|
|
167
|
+
{ op: "m", x: num(attributes, "x1"), y: num(attributes, "y1") },
|
|
168
|
+
{ op: "l", x: num(attributes, "x2"), y: num(attributes, "y2") },
|
|
169
|
+
];
|
|
170
|
+
case "polyline":
|
|
171
|
+
return polyline(attributes["points"], false);
|
|
172
|
+
case "polygon":
|
|
173
|
+
return polyline(attributes["points"], true);
|
|
174
|
+
default:
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Color } from "../common/color.js";
|
|
2
|
+
import type { StrokeStyle } from "../ir/display-list.js";
|
|
3
|
+
/**
|
|
4
|
+
* The presentation style of one element, resolved against its parent's.
|
|
5
|
+
*
|
|
6
|
+
* SVG styles two ways at once: presentation ATTRIBUTES (`fill="red"`) and a CSS `style=""` bag, with
|
|
7
|
+
* the style bag winning. Most of them INHERIT, which is what lets a logo set `fill` once on the root
|
|
8
|
+
* `<g>`. A `<style>` block with class selectors is a third way and is not resolved yet - it is a
|
|
9
|
+
* NAMED error, not a silent loss, because that is exactly the case that leaves a logo blank.
|
|
10
|
+
*/
|
|
11
|
+
/** A paint is a colour, or a `url(#id)` reference that only the SHAPE can resolve - a gradient in
|
|
12
|
+
* the default `objectBoundingBox` units is positioned on the bounding box of the thing it fills. */
|
|
13
|
+
export type PaintRef = {
|
|
14
|
+
ref: string;
|
|
15
|
+
};
|
|
16
|
+
export type SvgPaint = Color | PaintRef;
|
|
17
|
+
export declare const isPaintRef: (paint: SvgPaint | undefined) => paint is PaintRef;
|
|
18
|
+
export interface SvgStyle {
|
|
19
|
+
/** Absent = `fill="none"`. The RAW paint - see `paintAlpha` for why the opacity is not in it. */
|
|
20
|
+
fill?: SvgPaint;
|
|
21
|
+
fillRule: "nonzero" | "evenodd";
|
|
22
|
+
stroke?: StrokeStyle;
|
|
23
|
+
/** `currentColor` resolves to this; set by the `color` attribute, black at the root. */
|
|
24
|
+
color: Color;
|
|
25
|
+
/**
|
|
26
|
+
* `fill-opacity` / `stroke-opacity` (both inherit) and the accumulated `opacity` of this element
|
|
27
|
+
* and its ancestors. They are kept OUT of the colours and applied once, at the leaf that paints:
|
|
28
|
+
* folding them in at every level would multiply a group's opacity in again for each generation.
|
|
29
|
+
*/
|
|
30
|
+
fillOpacity: number;
|
|
31
|
+
strokeOpacity: number;
|
|
32
|
+
opacity: number;
|
|
33
|
+
}
|
|
34
|
+
/** The colour a shape actually paints with: its raw colour, times the opacities that reached it. */
|
|
35
|
+
export declare function paintAlpha(color: Color, opacity: number): Color;
|
|
36
|
+
export declare const ROOT_STYLE: SvgStyle;
|
|
37
|
+
/** Raw attributes, exactly as written. Our own XML reader never coerces them (`svg-parser` did,
|
|
38
|
+
* turning `id="58310095e0"` into a number). */
|
|
39
|
+
export type Attributes = Record<string, string | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* Reads a property from all three sources, in CSS order: a presentation attribute is the weakest, a
|
|
42
|
+
* matching `<style>` rule beats it, and the inline `style=""` bag beats both.
|
|
43
|
+
*
|
|
44
|
+
* Exported because anything that INSPECTS a property has to see the same value the style resolver
|
|
45
|
+
* would - `filter="url(#f)"` and `style="filter:url(#f)"` are the same instruction, and checking only
|
|
46
|
+
* the attribute lets the second one through unnoticed.
|
|
47
|
+
*/
|
|
48
|
+
export declare function cascadedReader(attributes: Attributes, css?: Record<string, string>): (name: string) => string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* A colour, with the value quoted in the message. `toColor` throws a bare `Unknown color: "x"`, which
|
|
51
|
+
* inside a 900-element logo says nothing about WHERE - and a malformed file reaches here with pieces
|
|
52
|
+
* of its own markup as the value, so the raw error is actively misleading.
|
|
53
|
+
*/
|
|
54
|
+
export declare function svgColor(value: string): Color;
|
|
55
|
+
/**
|
|
56
|
+
* Resolves one element's style against its parent's. Everything SVG inherits is inherited; `opacity`
|
|
57
|
+
* is the exception - it applies to the element itself, so it is MULTIPLIED down and folded into the
|
|
58
|
+
* paint of each leaf. That differs from a browser for a `<g opacity>` whose children OVERLAP (a real
|
|
59
|
+
* group needs one transparency group), and is the one approximation in this file.
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveStyle(parent: SvgStyle, attributes: Attributes, css?: Record<string, string>): SvgStyle;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Color } from "../common/color.js";
|
|
2
|
+
import { toColor } from "../api/color.js";
|
|
3
|
+
import { SvgParseError, SvgUnsupportedError } from "./errors.js";
|
|
4
|
+
import { length, ratio } from "./units.js";
|
|
5
|
+
export const isPaintRef = (paint) => paint !== undefined && !(paint instanceof Color);
|
|
6
|
+
/** The colour a shape actually paints with: its raw colour, times the opacities that reached it. */
|
|
7
|
+
export function paintAlpha(color, opacity) {
|
|
8
|
+
if (opacity >= 1)
|
|
9
|
+
return color;
|
|
10
|
+
const [r, g, b] = color.toArray();
|
|
11
|
+
return new Color(r, g, b, color.getAlpha() * opacity);
|
|
12
|
+
}
|
|
13
|
+
export const ROOT_STYLE = {
|
|
14
|
+
fill: new Color(0, 0, 0),
|
|
15
|
+
fillRule: "nonzero",
|
|
16
|
+
stroke: undefined,
|
|
17
|
+
color: new Color(0, 0, 0),
|
|
18
|
+
fillOpacity: 1,
|
|
19
|
+
strokeOpacity: 1,
|
|
20
|
+
opacity: 1,
|
|
21
|
+
};
|
|
22
|
+
/** Splits a `style=""` bag into declarations. It wins over both other sources, as in CSS. */
|
|
23
|
+
function styleBag(value) {
|
|
24
|
+
if (typeof value !== "string")
|
|
25
|
+
return {};
|
|
26
|
+
const out = {};
|
|
27
|
+
for (const declaration of value.split(";")) {
|
|
28
|
+
const colon = declaration.indexOf(":");
|
|
29
|
+
if (colon === -1)
|
|
30
|
+
continue;
|
|
31
|
+
const property = declaration.slice(0, colon).trim();
|
|
32
|
+
if (property)
|
|
33
|
+
out[property] = declaration.slice(colon + 1).trim();
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Reads a property from all three sources, in CSS order: a presentation attribute is the weakest, a
|
|
39
|
+
* matching `<style>` rule beats it, and the inline `style=""` bag beats both.
|
|
40
|
+
*
|
|
41
|
+
* Exported because anything that INSPECTS a property has to see the same value the style resolver
|
|
42
|
+
* would - `filter="url(#f)"` and `style="filter:url(#f)"` are the same instruction, and checking only
|
|
43
|
+
* the attribute lets the second one through unnoticed.
|
|
44
|
+
*/
|
|
45
|
+
export function cascadedReader(attributes, css = {}) {
|
|
46
|
+
const bag = styleBag(attributes["style"]);
|
|
47
|
+
return (name) => {
|
|
48
|
+
const value = bag[name] ?? css[name] ?? attributes[name];
|
|
49
|
+
return value === undefined ? undefined : String(value);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const dashNumbers = (list) => {
|
|
53
|
+
const found = list.match(/[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g);
|
|
54
|
+
return found ? found.map(Number) : [];
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* A paint value. `none` is the absence of paint, which is NOT the same as transparent black - a shape
|
|
58
|
+
* with `fill="none"` and a stroke draws its outline only. A `url(#...)` reference means a gradient or
|
|
59
|
+
* pattern, which we do not resolve yet and therefore name.
|
|
60
|
+
*/
|
|
61
|
+
function paint(value, current) {
|
|
62
|
+
const v = value.trim();
|
|
63
|
+
if (v === "none")
|
|
64
|
+
return undefined;
|
|
65
|
+
if (v === "currentColor")
|
|
66
|
+
return current;
|
|
67
|
+
const reference = /^url\(\s*#([^)\s]+)\s*\)/.exec(v);
|
|
68
|
+
// A paint reference may name a fallback after it (`url(#g) red`), which is what a renderer uses
|
|
69
|
+
// when the reference is broken - so it is kept as the reference, and resolution decides.
|
|
70
|
+
if (reference)
|
|
71
|
+
return { ref: reference[1] };
|
|
72
|
+
if (v.startsWith("url(")) {
|
|
73
|
+
throw new SvgUnsupportedError(`the paint "${v}"`, "Only a `url(#id)` reference to a gradient is resolved.");
|
|
74
|
+
}
|
|
75
|
+
return svgColor(v);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A colour, with the value quoted in the message. `toColor` throws a bare `Unknown color: "x"`, which
|
|
79
|
+
* inside a 900-element logo says nothing about WHERE - and a malformed file reaches here with pieces
|
|
80
|
+
* of its own markup as the value, so the raw error is actively misleading.
|
|
81
|
+
*/
|
|
82
|
+
export function svgColor(value) {
|
|
83
|
+
try {
|
|
84
|
+
return toColor(value);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new SvgParseError(`"${value}" is not a colour this SVG can use - expected a name, #hex, rgb() or hsl().`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolves one element's style against its parent's. Everything SVG inherits is inherited; `opacity`
|
|
92
|
+
* is the exception - it applies to the element itself, so it is MULTIPLIED down and folded into the
|
|
93
|
+
* paint of each leaf. That differs from a browser for a `<g opacity>` whose children OVERLAP (a real
|
|
94
|
+
* group needs one transparency group), and is the one approximation in this file.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveStyle(parent, attributes, css = {}) {
|
|
97
|
+
const read = cascadedReader(attributes, css);
|
|
98
|
+
const color = read("color") !== undefined ? svgColor(read("color")) : parent.color;
|
|
99
|
+
// `Number("50%")` is NaN and `Number("")` is 0, both of which travel silently into the paint.
|
|
100
|
+
const opacity = parent.opacity * ratio(read("opacity"));
|
|
101
|
+
const fillValue = read("fill");
|
|
102
|
+
const strokeValue = read("stroke");
|
|
103
|
+
const dashValue = read("stroke-dasharray");
|
|
104
|
+
const strokePaint = strokeValue !== undefined ? paint(strokeValue, color) : parent.stroke?.color;
|
|
105
|
+
if (isPaintRef(strokePaint)) {
|
|
106
|
+
// PDF strokes with a colour, never with a shading; painting the outline solid would be a colour
|
|
107
|
+
// the file did not name.
|
|
108
|
+
throw new SvgUnsupportedError("a gradient on a stroke", "PDF strokes with a colour only. Use a solid stroke, or convert the outline to a filled shape.");
|
|
109
|
+
}
|
|
110
|
+
const parts = {
|
|
111
|
+
color: strokePaint,
|
|
112
|
+
width: length(read("stroke-width"), 'the "stroke-width" attribute', parent.stroke?.width ?? 1),
|
|
113
|
+
cap: read("stroke-linecap") ?? parent.stroke?.cap,
|
|
114
|
+
join: read("stroke-linejoin") ?? parent.stroke?.join,
|
|
115
|
+
miterLimit: length(read("stroke-miterlimit"), 'the "stroke-miterlimit" attribute', parent.stroke?.miterLimit ?? 4),
|
|
116
|
+
dash: dashValue !== undefined
|
|
117
|
+
? dashValue.trim() === "none"
|
|
118
|
+
? undefined
|
|
119
|
+
: dashNumbers(dashValue)
|
|
120
|
+
: parent.stroke?.dash,
|
|
121
|
+
dashOffset: length(read("stroke-dashoffset"), 'the "stroke-dashoffset" attribute', parent.stroke?.dashOffset ?? 0),
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
fill: fillValue !== undefined ? paint(fillValue, color) : parent.fill,
|
|
125
|
+
fillRule: read("fill-rule") ?? parent.fillRule,
|
|
126
|
+
// A stroke with no colour or no width paints nothing; dropping it here keeps every consumer from
|
|
127
|
+
// having to ask twice.
|
|
128
|
+
stroke: parts.color && parts.width > 0
|
|
129
|
+
? {
|
|
130
|
+
color: parts.color,
|
|
131
|
+
width: parts.width,
|
|
132
|
+
cap: parts.cap,
|
|
133
|
+
join: parts.join,
|
|
134
|
+
miterLimit: parts.miterLimit,
|
|
135
|
+
dash: parts.dash,
|
|
136
|
+
dashOffset: parts.dashOffset,
|
|
137
|
+
}
|
|
138
|
+
: undefined,
|
|
139
|
+
color,
|
|
140
|
+
fillOpacity: ratio(read("fill-opacity"), parent.fillOpacity),
|
|
141
|
+
strokeOpacity: ratio(read("stroke-opacity"), parent.strokeOpacity),
|
|
142
|
+
opacity,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Affine } from "../utils/ttf-parser.js";
|
|
2
|
+
/**
|
|
3
|
+
* The `transform` attribute. SVG's `matrix(a b c d e f)` is exactly the PDF `cm` operand and exactly
|
|
4
|
+
* our IR's `TransformPush.matrix`, both read in a top-left, y-down space - so a transform maps across
|
|
5
|
+
* with no sign flips. That is also why we keep transforms in the graphics state instead of baking them
|
|
6
|
+
* into the coordinates: a stroke inside a scaled group scales WITH it, which is what SVG specifies.
|
|
7
|
+
*/
|
|
8
|
+
export declare const IDENTITY: Affine;
|
|
9
|
+
/** `a` then `b` - the matrix that applies `b` in the space `a` established. */
|
|
10
|
+
export declare function multiply(a: Affine, b: Affine): Affine;
|
|
11
|
+
export declare const isIdentity: (m: Affine) => boolean;
|
|
12
|
+
/** Parses a whole `transform` attribute; several functions compose left to right, as in SVG. */
|
|
13
|
+
export declare function parseTransform(value: string): Affine;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { SvgParseError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* The `transform` attribute. SVG's `matrix(a b c d e f)` is exactly the PDF `cm` operand and exactly
|
|
4
|
+
* our IR's `TransformPush.matrix`, both read in a top-left, y-down space - so a transform maps across
|
|
5
|
+
* with no sign flips. That is also why we keep transforms in the graphics state instead of baking them
|
|
6
|
+
* into the coordinates: a stroke inside a scaled group scales WITH it, which is what SVG specifies.
|
|
7
|
+
*/
|
|
8
|
+
export const IDENTITY = [1, 0, 0, 1, 0, 0];
|
|
9
|
+
/** `a` then `b` - the matrix that applies `b` in the space `a` established. */
|
|
10
|
+
export function multiply(a, b) {
|
|
11
|
+
return [
|
|
12
|
+
a[0] * b[0] + a[2] * b[1],
|
|
13
|
+
a[1] * b[0] + a[3] * b[1],
|
|
14
|
+
a[0] * b[2] + a[2] * b[3],
|
|
15
|
+
a[1] * b[2] + a[3] * b[3],
|
|
16
|
+
a[0] * b[4] + a[2] * b[5] + a[4],
|
|
17
|
+
a[1] * b[4] + a[3] * b[5] + a[5],
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
export const isIdentity = (m) => m.every((n, i) => n === IDENTITY[i]);
|
|
21
|
+
const TRANSFORM = /([a-zA-Z]+)\s*\(([^)]*)\)/g;
|
|
22
|
+
const radians = (deg) => (deg * Math.PI) / 180;
|
|
23
|
+
/** Numbers in an SVG list: separated by commas, whitespace, or nothing at all before a minus sign. */
|
|
24
|
+
function numbers(list) {
|
|
25
|
+
const found = list.match(/[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g);
|
|
26
|
+
return found ? found.map(Number) : [];
|
|
27
|
+
}
|
|
28
|
+
/** Parses a whole `transform` attribute; several functions compose left to right, as in SVG. */
|
|
29
|
+
export function parseTransform(value) {
|
|
30
|
+
let out = IDENTITY;
|
|
31
|
+
TRANSFORM.lastIndex = 0;
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = TRANSFORM.exec(value)) !== null) {
|
|
34
|
+
out = multiply(out, single(match[1], numbers(match[2])));
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function single(name, a) {
|
|
39
|
+
switch (name) {
|
|
40
|
+
case "matrix":
|
|
41
|
+
if (a.length < 6)
|
|
42
|
+
throw new SvgParseError(`transform: matrix() needs six numbers`);
|
|
43
|
+
return [a[0], a[1], a[2], a[3], a[4], a[5]];
|
|
44
|
+
case "translate":
|
|
45
|
+
return [1, 0, 0, 1, a[0] ?? 0, a[1] ?? 0];
|
|
46
|
+
case "scale": {
|
|
47
|
+
const sx = a[0] ?? 1;
|
|
48
|
+
// One number scales both axes - the SVG rule, and a common source of squashed logos if missed.
|
|
49
|
+
return [sx, 0, 0, a[1] ?? sx, 0, 0];
|
|
50
|
+
}
|
|
51
|
+
case "rotate": {
|
|
52
|
+
const t = radians(a[0] ?? 0);
|
|
53
|
+
const [cos, sin] = [Math.cos(t), Math.sin(t)];
|
|
54
|
+
const r = [cos, sin, -sin, cos, 0, 0];
|
|
55
|
+
if (a.length < 3)
|
|
56
|
+
return r;
|
|
57
|
+
// rotate(angle cx cy) = translate(cx,cy) rotate(angle) translate(-cx,-cy).
|
|
58
|
+
const [cx, cy] = [a[1], a[2]];
|
|
59
|
+
return multiply(multiply([1, 0, 0, 1, cx, cy], r), [1, 0, 0, 1, -cx, -cy]);
|
|
60
|
+
}
|
|
61
|
+
case "skewX":
|
|
62
|
+
return [1, 0, Math.tan(radians(a[0] ?? 0)), 1, 0, 0];
|
|
63
|
+
case "skewY":
|
|
64
|
+
return [1, Math.tan(radians(a[0] ?? 0)), 0, 1, 0, 0];
|
|
65
|
+
default:
|
|
66
|
+
throw new SvgParseError(`transform: unknown function "${name}()"`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** A plain number, with nothing else in the string. */
|
|
2
|
+
export declare function strictNumber(text: string): number | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* A length in user units. `undefined` and `""` mean "not given" and return `fallback`; a resolvable
|
|
5
|
+
* absolute unit is converted; a relative one (`%`, `em`, `rem`, `ex`, `ch`, `vw`, …) is NAMED,
|
|
6
|
+
* because resolving it needs a font or a viewport that a shape attribute does not carry.
|
|
7
|
+
*/
|
|
8
|
+
export declare function length(value: string | undefined, where: string, fallback?: number): number;
|
|
9
|
+
/** A 0..1 ratio, written as a number or a percentage, clamped - an opacity outside the range is a
|
|
10
|
+
* malformed file, and CSS clamps rather than rejecting. */
|
|
11
|
+
export declare function ratio(value: string | undefined, fallback?: number): number;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { SvgUnsupportedError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reading NUMBERS out of an SVG.
|
|
4
|
+
*
|
|
5
|
+
* `Number.parseFloat` is the wrong tool on its own: it stops at the first character it cannot use and
|
|
6
|
+
* returns what it had, so `"10px"` is 10 and `"20abc"` is 20. That is convenient exactly once and
|
|
7
|
+
* wrong everywhere else - a length we silently misread draws a shape the file did not describe.
|
|
8
|
+
*
|
|
9
|
+
* So the rules here are explicit: a plain number is a number, `px` is the user unit and means the
|
|
10
|
+
* same thing, a percentage is only valid where a ratio is expected, and anything else is NAMED. In
|
|
11
|
+
* 10,819 real files, 53 use `px` on a shape attribute (we drew nothing for them) and one uses a
|
|
12
|
+
* percentage opacity.
|
|
13
|
+
*/
|
|
14
|
+
/** Absolute CSS units are defined against the user unit, which SVG pins to 1/96 inch. */
|
|
15
|
+
const ABSOLUTE = {
|
|
16
|
+
px: 1,
|
|
17
|
+
pt: 96 / 72,
|
|
18
|
+
pc: 16,
|
|
19
|
+
in: 96,
|
|
20
|
+
cm: 96 / 2.54,
|
|
21
|
+
mm: 96 / 25.4,
|
|
22
|
+
q: 96 / 101.6,
|
|
23
|
+
};
|
|
24
|
+
const NUMBER = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
|
|
25
|
+
const WITH_UNIT = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)\s*([a-zA-Z%]+)$/;
|
|
26
|
+
/** A plain number, with nothing else in the string. */
|
|
27
|
+
export function strictNumber(text) {
|
|
28
|
+
const t = text.trim();
|
|
29
|
+
if (!NUMBER.test(t))
|
|
30
|
+
return undefined;
|
|
31
|
+
const n = Number(t);
|
|
32
|
+
// `1e400` matches the grammar and overflows to Infinity. A non-finite number reaches the content
|
|
33
|
+
// stream as the literal text `Infinity`, which viewers discard the rest of the page over.
|
|
34
|
+
return Number.isFinite(n) ? n : undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A length in user units. `undefined` and `""` mean "not given" and return `fallback`; a resolvable
|
|
38
|
+
* absolute unit is converted; a relative one (`%`, `em`, `rem`, `ex`, `ch`, `vw`, …) is NAMED,
|
|
39
|
+
* because resolving it needs a font or a viewport that a shape attribute does not carry.
|
|
40
|
+
*/
|
|
41
|
+
export function length(value, where, fallback = 0) {
|
|
42
|
+
if (value === undefined || value.trim() === "")
|
|
43
|
+
return fallback;
|
|
44
|
+
const plain = strictNumber(value);
|
|
45
|
+
if (plain !== undefined)
|
|
46
|
+
return plain;
|
|
47
|
+
const parts = WITH_UNIT.exec(value.trim());
|
|
48
|
+
if (parts) {
|
|
49
|
+
const factor = ABSOLUTE[parts[2].toLowerCase()];
|
|
50
|
+
if (factor !== undefined) {
|
|
51
|
+
// The conversion itself can overflow (`1e308in`), and a non-finite number reaches the content
|
|
52
|
+
// stream as the literal text `Infinity`.
|
|
53
|
+
const converted = Number(parts[1]) * factor;
|
|
54
|
+
return Number.isFinite(converted) ? converted : fallback;
|
|
55
|
+
}
|
|
56
|
+
throw new SvgUnsupportedError(`the length "${value.trim()}" on ${where}`, "Only absolute units (px, pt, pc, in, cm, mm, q) and plain numbers are resolved.");
|
|
57
|
+
}
|
|
58
|
+
// Not a number at all: the file is malformed here. Treat it as absent rather than as zero-with-a-
|
|
59
|
+
// shrug, so a caller sees the same thing as for a missing attribute.
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
/** A 0..1 ratio, written as a number or a percentage, clamped - an opacity outside the range is a
|
|
63
|
+
* malformed file, and CSS clamps rather than rejecting. */
|
|
64
|
+
export function ratio(value, fallback = 1) {
|
|
65
|
+
if (value === undefined || value.trim() === "")
|
|
66
|
+
return fallback;
|
|
67
|
+
const t = value.trim();
|
|
68
|
+
const raw = t.endsWith("%") ? strictNumber(t.slice(0, -1)) : strictNumber(t);
|
|
69
|
+
if (raw === undefined)
|
|
70
|
+
return fallback;
|
|
71
|
+
const scaled = t.endsWith("%") ? raw / 100 : raw;
|
|
72
|
+
return Math.max(0, Math.min(1, scaled));
|
|
73
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal XML reader for SVG.
|
|
3
|
+
*
|
|
4
|
+
* We used `svg-parser` first. It COERCES an attribute that looks numeric, so `id="58310095e0"` came
|
|
5
|
+
* back as the number 58310095 and `id="1e999"` as Infinity - the original string simply gone. That
|
|
6
|
+
* breaks the one thing it is needed for, resolving `url(#id)` references, and no amount of care on
|
|
7
|
+
* our side can undo it. An XML attribute is a string; this reader treats it as one.
|
|
8
|
+
*
|
|
9
|
+
* The scope is what SVG actually needs: elements, attributes, text, CDATA, comments, the XML
|
|
10
|
+
* declaration and a doctype. No entity expansion beyond the five predefined ones plus numeric
|
|
11
|
+
* references, no DTD processing, no namespace resolution (the caller strips prefixes itself).
|
|
12
|
+
*/
|
|
13
|
+
export interface XmlElement {
|
|
14
|
+
type: "element";
|
|
15
|
+
tagName: string;
|
|
16
|
+
attributes: Record<string, string>;
|
|
17
|
+
children: XmlNode[];
|
|
18
|
+
}
|
|
19
|
+
export interface XmlText {
|
|
20
|
+
type: "text";
|
|
21
|
+
value: string;
|
|
22
|
+
}
|
|
23
|
+
export type XmlNode = XmlElement | XmlText;
|
|
24
|
+
export declare function parseXml(source: string): XmlElement[];
|