@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/dist/svg/css.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { SvgUnsupportedError } from "./errors.js";
|
|
2
|
+
const COMMENTS = /\/\*[\s\S]*?\*\//g;
|
|
3
|
+
/** One compound selector, e.g. `rect.st0#a`. Anything structural is refused, not approximated. */
|
|
4
|
+
function parseSelector(selector, order) {
|
|
5
|
+
const text = selector.trim();
|
|
6
|
+
if (/[\s>+~]/.test(text)) {
|
|
7
|
+
throw new SvgUnsupportedError(`the CSS selector "${text}" (it combines elements)`, "Only flat selectors are resolved. Re-export with presentation attributes instead.");
|
|
8
|
+
}
|
|
9
|
+
if (text.includes(":") || text.includes("[")) {
|
|
10
|
+
throw new SvgUnsupportedError(`the CSS selector "${text}" (pseudo-class or attribute selector)`, "Only a tag, .class and #id are resolved. Re-export with presentation attributes instead.");
|
|
11
|
+
}
|
|
12
|
+
const classes = [...text.matchAll(/\.([\w-]+)/g)].map((m) => m[1]);
|
|
13
|
+
const id = /#([\w-]+)/.exec(text)?.[1];
|
|
14
|
+
const tag = /^[\w-]+/.exec(text)?.[0];
|
|
15
|
+
// The usual a-b-c weights, flattened: an id beats any number of classes, a class beats a tag.
|
|
16
|
+
const weight = (id ? 10000 : 0) + classes.length * 100 + (tag ? 1 : 0);
|
|
17
|
+
return { tag, id, classes, weight, order };
|
|
18
|
+
}
|
|
19
|
+
function parseDeclarations(body) {
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const declaration of body.split(";")) {
|
|
22
|
+
const colon = declaration.indexOf(":");
|
|
23
|
+
if (colon === -1)
|
|
24
|
+
continue;
|
|
25
|
+
const property = declaration.slice(0, colon).trim();
|
|
26
|
+
// `!important` would need a second cascade level; exporters do not emit it, so it is refused
|
|
27
|
+
// rather than silently treated as a normal declaration.
|
|
28
|
+
const value = declaration.slice(colon + 1).trim();
|
|
29
|
+
if (!property)
|
|
30
|
+
continue;
|
|
31
|
+
if (/!\s*important$/i.test(value)) {
|
|
32
|
+
throw new SvgUnsupportedError(`"!important" in a <style> block`, "Remove it, or re-export with presentation attributes.");
|
|
33
|
+
}
|
|
34
|
+
out[property] = value;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parses every `<style>` block's text into rules, in source order.
|
|
40
|
+
*
|
|
41
|
+
* The brace matching is done by hand rather than with one regex: `@media print { .a { … } }` nests,
|
|
42
|
+
* and a regex that skips to the first balanced-looking block silently applies the INNER rule while
|
|
43
|
+
* never seeing the at-rule that guards it.
|
|
44
|
+
*/
|
|
45
|
+
export function parseStylesheet(css) {
|
|
46
|
+
const text = css.replace(COMMENTS, "");
|
|
47
|
+
const rules = [];
|
|
48
|
+
let order = 0;
|
|
49
|
+
let i = 0;
|
|
50
|
+
while (i < text.length) {
|
|
51
|
+
const open = text.indexOf("{", i);
|
|
52
|
+
if (open === -1)
|
|
53
|
+
break;
|
|
54
|
+
// A statement at-rule (`@import "a";`) ends at its semicolon. Taking everything up to the next
|
|
55
|
+
// brace as one prelude would swallow the rule that follows it.
|
|
56
|
+
const head = text.slice(i, open);
|
|
57
|
+
const prelude = head.slice(head.lastIndexOf(";") + 1).trim();
|
|
58
|
+
let depth = 1;
|
|
59
|
+
let j = open + 1;
|
|
60
|
+
while (j < text.length && depth > 0) {
|
|
61
|
+
if (text[j] === "{")
|
|
62
|
+
depth++;
|
|
63
|
+
else if (text[j] === "}")
|
|
64
|
+
depth--;
|
|
65
|
+
j++;
|
|
66
|
+
}
|
|
67
|
+
if (prelude.startsWith("@")) {
|
|
68
|
+
// An at-rule is DROPPED, not reported - which is what CSS itself does with one it cannot
|
|
69
|
+
// evaluate, and what the corpus demanded: 68 of 10,819 real files carry a `@media
|
|
70
|
+
// (prefers-color-scheme)` or a `@keyframes` that can never apply to a static page, and
|
|
71
|
+
// refusing them meant those files did not render at all.
|
|
72
|
+
// The exception is a query that always holds for print, whose rules DO apply.
|
|
73
|
+
const [at, ...rest] = prelude.split(/\s+/);
|
|
74
|
+
// A media list applies if ANY of its components does - `@media print, screen` is for print too.
|
|
75
|
+
const applies = at === "@media" &&
|
|
76
|
+
rest
|
|
77
|
+
.join(" ")
|
|
78
|
+
.split(",")
|
|
79
|
+
.some((q) => ["print", "all", ""].includes(q.trim()));
|
|
80
|
+
if (applies) {
|
|
81
|
+
// Its body is a nested stylesheet; parse it in place so the rules keep their source order.
|
|
82
|
+
for (const rule of parseStylesheet(text.slice(open + 1, j - 1))) {
|
|
83
|
+
rules.push({ ...rule, order: order++ });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
i = j;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const declarations = parseDeclarations(text.slice(open + 1, j - 1));
|
|
90
|
+
for (const selector of prelude.split(",")) {
|
|
91
|
+
if (selector.trim() === "")
|
|
92
|
+
continue;
|
|
93
|
+
rules.push({ ...parseSelector(selector, order++), declarations });
|
|
94
|
+
}
|
|
95
|
+
i = j;
|
|
96
|
+
}
|
|
97
|
+
return rules;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The declarations that apply to one element, weakest first - so a caller can just assign them in
|
|
101
|
+
* order and let the last one win, as the cascade does.
|
|
102
|
+
*/
|
|
103
|
+
export function declarationsFor(rules, tagName, attributes) {
|
|
104
|
+
if (rules.length === 0)
|
|
105
|
+
return {};
|
|
106
|
+
const classes = (attributes.class ?? "").trim().split(/\s+/).filter(Boolean);
|
|
107
|
+
const matched = rules.filter((rule) => (rule.tag === undefined || rule.tag === tagName) &&
|
|
108
|
+
(rule.id === undefined || rule.id === attributes.id) &&
|
|
109
|
+
rule.classes.every((name) => classes.includes(name)));
|
|
110
|
+
matched.sort((a, b) => a.weight - b.weight || a.order - b.order);
|
|
111
|
+
const out = {};
|
|
112
|
+
for (const rule of matched)
|
|
113
|
+
Object.assign(out, rule.declarations);
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG is a document format, not a graphics format: it carries CSS, text layout, filters, masks and
|
|
3
|
+
* animation. Filters and masks are PIXEL operations, so no vector backend can express them - which is
|
|
4
|
+
* why every complete SVG renderer owns a rasterizer, and why every PDF library ships a subset instead.
|
|
5
|
+
*
|
|
6
|
+
* We ship a subset too. The difference is what happens at its edge: an element we cannot draw is a
|
|
7
|
+
* NAMED ERROR, never a silent skip. Two reasons, and the second is the important one.
|
|
8
|
+
*
|
|
9
|
+
* 1. A silently dropped element leaves a logo that renders and is quietly wrong - the fill missing,
|
|
10
|
+
* the icon half there - and nobody finds out. (react-pdf skips, and its `<style>` handling loses
|
|
11
|
+
* every fill of an Illustrator export made with the default "Internal CSS" option.)
|
|
12
|
+
* 2. It keeps every later stage a MINOR. Supporting something new turns an error into a picture, which
|
|
13
|
+
* can never break a document that worked. Going the other way - from silently skipping to drawing -
|
|
14
|
+
* would change existing output.
|
|
15
|
+
*/
|
|
16
|
+
/** An element or attribute value we deliberately do not support yet. */
|
|
17
|
+
export declare class SvgUnsupportedError extends Error {
|
|
18
|
+
readonly feature: string;
|
|
19
|
+
constructor(feature: string, hint: string);
|
|
20
|
+
}
|
|
21
|
+
/** The file is not SVG we can read at all - malformed, or with no root `<svg>`. */
|
|
22
|
+
export declare class SvgParseError extends Error {
|
|
23
|
+
constructor(message: string);
|
|
24
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG is a document format, not a graphics format: it carries CSS, text layout, filters, masks and
|
|
3
|
+
* animation. Filters and masks are PIXEL operations, so no vector backend can express them - which is
|
|
4
|
+
* why every complete SVG renderer owns a rasterizer, and why every PDF library ships a subset instead.
|
|
5
|
+
*
|
|
6
|
+
* We ship a subset too. The difference is what happens at its edge: an element we cannot draw is a
|
|
7
|
+
* NAMED ERROR, never a silent skip. Two reasons, and the second is the important one.
|
|
8
|
+
*
|
|
9
|
+
* 1. A silently dropped element leaves a logo that renders and is quietly wrong - the fill missing,
|
|
10
|
+
* the icon half there - and nobody finds out. (react-pdf skips, and its `<style>` handling loses
|
|
11
|
+
* every fill of an Illustrator export made with the default "Internal CSS" option.)
|
|
12
|
+
* 2. It keeps every later stage a MINOR. Supporting something new turns an error into a picture, which
|
|
13
|
+
* can never break a document that worked. Going the other way - from silently skipping to drawing -
|
|
14
|
+
* would change existing output.
|
|
15
|
+
*/
|
|
16
|
+
/** An element or attribute value we deliberately do not support yet. */
|
|
17
|
+
export class SvgUnsupportedError extends Error {
|
|
18
|
+
constructor(feature, hint) {
|
|
19
|
+
super(`@jasy/pdf: ${feature} is not supported yet. ${hint}`);
|
|
20
|
+
this.feature = feature;
|
|
21
|
+
this.name = "SvgUnsupportedError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** The file is not SVG we can read at all - malformed, or with no root `<svg>`. */
|
|
25
|
+
export class SvgParseError extends Error {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(`@jasy/pdf: ${message}`);
|
|
28
|
+
this.name = "SvgParseError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Gradient, GradientStop, PathCommand } from "../ir/display-list.js";
|
|
2
|
+
import { type Attributes } from "./style.js";
|
|
3
|
+
import type { XmlNode } from "./xml.js";
|
|
4
|
+
/**
|
|
5
|
+
* `<linearGradient>` and `<radialGradient>`.
|
|
6
|
+
*
|
|
7
|
+
* The backend has painted shadings since colour emoji, and `Box({ bg: linearGradient(...) })` uses
|
|
8
|
+
* the same node - so this file only has to turn a `url(#id)` reference into that node. Two things
|
|
9
|
+
* make it more than a lookup:
|
|
10
|
+
*
|
|
11
|
+
* - **`gradientUnits` defaults to `objectBoundingBox`**, i.e. the coordinates are fractions of the
|
|
12
|
+
* SHAPE's bounding box, not of the page. So a gradient cannot be resolved where it is defined,
|
|
13
|
+
* only where it is used - which is why this takes the shape's commands.
|
|
14
|
+
* - **`href` chains.** Illustrator emits one gradient with the stops and a second that references it
|
|
15
|
+
* and only moves the coordinates. Missing that leaves a shape with no stops at all.
|
|
16
|
+
*/
|
|
17
|
+
export interface GradientDef {
|
|
18
|
+
kind: "linear" | "radial";
|
|
19
|
+
attributes: Attributes;
|
|
20
|
+
stops: GradientStop[];
|
|
21
|
+
/** The `stop-opacity` of each stop, in the same order. */
|
|
22
|
+
opacities: number[];
|
|
23
|
+
/** The id this definition inherits from (`href` / `xlink:href`), without the `#`. */
|
|
24
|
+
href?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Every gradient definition in the document, by id - they are referenced, not drawn in place. */
|
|
27
|
+
export declare function gradientsOf(node: XmlNode, into?: Map<string, GradientDef>, depth?: number): Map<string, GradientDef>;
|
|
28
|
+
/**
|
|
29
|
+
* Resolves a reference into an IR gradient positioned on `commands`.
|
|
30
|
+
*
|
|
31
|
+
* The anchors are mapped into the shape's own space, then through `gradientTransform` - so they end
|
|
32
|
+
* up in the SAME coordinates as the path, which is what the backend and the Y-flip both assume.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveSvgGradient(def: GradientDef, defs: ReadonlyMap<string, GradientDef>, commands: readonly PathCommand[]): Gradient;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Color } from "../common/color.js";
|
|
2
|
+
import { SvgUnsupportedError } from "./errors.js";
|
|
3
|
+
import { IDENTITY, isIdentity, parseTransform } from "./transform.js";
|
|
4
|
+
import { svgColor } from "./style.js";
|
|
5
|
+
import { boundsOf } from "../vector/path.js";
|
|
6
|
+
import { ratio } from "./units.js";
|
|
7
|
+
const localName = (tagName) => {
|
|
8
|
+
const colon = tagName.indexOf(":");
|
|
9
|
+
return colon === -1 ? tagName : tagName.slice(colon + 1);
|
|
10
|
+
};
|
|
11
|
+
const num = (value, fallback) => {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return fallback;
|
|
14
|
+
const n = Number.parseFloat(value);
|
|
15
|
+
if (!Number.isFinite(n))
|
|
16
|
+
return fallback;
|
|
17
|
+
return value.trim().endsWith("%") ? n / 100 : n;
|
|
18
|
+
};
|
|
19
|
+
/** One `<stop>`, plus its own opacity - which is kept apart because only a UNIFORM one survives. */
|
|
20
|
+
function readStop(element) {
|
|
21
|
+
const attributes = element.attributes;
|
|
22
|
+
const style = {};
|
|
23
|
+
for (const declaration of (attributes["style"] ?? "").split(";")) {
|
|
24
|
+
const colon = declaration.indexOf(":");
|
|
25
|
+
if (colon !== -1)
|
|
26
|
+
style[declaration.slice(0, colon).trim()] = declaration.slice(colon + 1).trim();
|
|
27
|
+
}
|
|
28
|
+
const read = (name) => style[name] ?? attributes[name];
|
|
29
|
+
const opacity = ratio(read("stop-opacity"));
|
|
30
|
+
const colorText = read("stop-color") ?? "#000000";
|
|
31
|
+
return {
|
|
32
|
+
stop: {
|
|
33
|
+
// Both are clamped: a PDF stitching function needs its bounds inside [0,1] and increasing, so
|
|
34
|
+
// a malformed file must not be able to produce one that a reader rejects.
|
|
35
|
+
offset: Math.max(0, Math.min(1, num(read("offset"), 0))),
|
|
36
|
+
color: colorText === "currentColor" ? new Color(0, 0, 0) : svgColor(colorText),
|
|
37
|
+
},
|
|
38
|
+
opacity,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Every gradient definition in the document, by id - they are referenced, not drawn in place. */
|
|
42
|
+
export function gradientsOf(node, into = new Map(), depth = 0) {
|
|
43
|
+
// Bounded like the walk in `index.ts`: this recurses, and a pathological file must not blow the
|
|
44
|
+
// call stack with a raw RangeError.
|
|
45
|
+
if (node.type !== "element" || depth > 256)
|
|
46
|
+
return into;
|
|
47
|
+
const name = localName(node.tagName);
|
|
48
|
+
if (name === "linearGradient" || name === "radialGradient") {
|
|
49
|
+
const id = node.attributes["id"];
|
|
50
|
+
if (id !== undefined) {
|
|
51
|
+
const href = node.attributes["href"] ?? node.attributes["xlink:href"];
|
|
52
|
+
const read = node.children
|
|
53
|
+
.filter((c) => c.type === "element" && localName(c.tagName) === "stop")
|
|
54
|
+
.map(readStop);
|
|
55
|
+
into.set(id, {
|
|
56
|
+
kind: name === "linearGradient" ? "linear" : "radial",
|
|
57
|
+
attributes: node.attributes,
|
|
58
|
+
stops: read.map((r) => r.stop),
|
|
59
|
+
opacities: read.map((r) => r.opacity),
|
|
60
|
+
href: href?.startsWith("#") ? href.slice(1) : undefined,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return into;
|
|
64
|
+
}
|
|
65
|
+
for (const child of node.children)
|
|
66
|
+
gradientsOf(child, into, depth + 1);
|
|
67
|
+
return into;
|
|
68
|
+
}
|
|
69
|
+
/** Walks the `href` chain: the nearest definition wins for each attribute, stops come from the first
|
|
70
|
+
* ancestor that has any. A cycle stops the walk rather than hanging. */
|
|
71
|
+
function inherited(def, defs) {
|
|
72
|
+
const attributes = {};
|
|
73
|
+
let stops = [];
|
|
74
|
+
let opacities = [];
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
let current = def;
|
|
77
|
+
while (current && !seen.has(current)) {
|
|
78
|
+
seen.add(current);
|
|
79
|
+
for (const [key, value] of Object.entries(current.attributes)) {
|
|
80
|
+
if (attributes[key] === undefined)
|
|
81
|
+
attributes[key] = value;
|
|
82
|
+
}
|
|
83
|
+
if (stops.length === 0) {
|
|
84
|
+
stops = current.stops;
|
|
85
|
+
opacities = current.opacities;
|
|
86
|
+
}
|
|
87
|
+
current = current.href === undefined ? undefined : defs.get(current.href);
|
|
88
|
+
}
|
|
89
|
+
return { attributes, stops, opacities };
|
|
90
|
+
}
|
|
91
|
+
const SPREAD = {
|
|
92
|
+
pad: "pad",
|
|
93
|
+
repeat: "repeat",
|
|
94
|
+
reflect: "reflect",
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Resolves a reference into an IR gradient positioned on `commands`.
|
|
98
|
+
*
|
|
99
|
+
* The anchors are mapped into the shape's own space, then through `gradientTransform` - so they end
|
|
100
|
+
* up in the SAME coordinates as the path, which is what the backend and the Y-flip both assume.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveSvgGradient(def, defs, commands) {
|
|
103
|
+
const { attributes, stops, opacities } = inherited(def, defs);
|
|
104
|
+
if (stops.length === 0) {
|
|
105
|
+
throw new SvgUnsupportedError("a gradient with no <stop> of its own or inherited", "Add stops, or replace the fill with a solid colour.");
|
|
106
|
+
}
|
|
107
|
+
// A PDF shading is DeviceRGB and has no alpha channel. One opacity across all stops IS
|
|
108
|
+
// expressible - it is just the shape's fill alpha - and 87 of the 125 real files that use
|
|
109
|
+
// `stop-opacity` at all are that case. Stops with DIFFERENT opacities would need a soft-mask
|
|
110
|
+
// luminosity group, so that stays named rather than drawn opaque.
|
|
111
|
+
const alpha = opacities[0] ?? 1;
|
|
112
|
+
if (opacities.some((o) => o !== alpha)) {
|
|
113
|
+
throw new SvgUnsupportedError("a gradient whose stops have DIFFERENT stop-opacity values", "A PDF shading has no alpha channel; only one opacity for the whole gradient is expressible.");
|
|
114
|
+
}
|
|
115
|
+
// SVG says an offset lower than the one before it is raised to match; a decreasing pair would
|
|
116
|
+
// otherwise become an invalid Domain in the shading function.
|
|
117
|
+
let previous = 0;
|
|
118
|
+
for (const stop of stops) {
|
|
119
|
+
stop.offset = previous = Math.max(previous, stop.offset);
|
|
120
|
+
}
|
|
121
|
+
const box = boundsOf(commands);
|
|
122
|
+
const onBox = (attributes["gradientUnits"] ?? "objectBoundingBox") === "objectBoundingBox";
|
|
123
|
+
// In objectBoundingBox units every coordinate is a FRACTION of the shape's box.
|
|
124
|
+
const px = (v) => (onBox ? box.x + v * box.width : v);
|
|
125
|
+
const py = (v) => (onBox ? box.y + v * box.height : v);
|
|
126
|
+
// A radius in box units is a fraction of the box's diagonal-ish size, per the SVG formula.
|
|
127
|
+
const pr = (v) => (onBox ? v * Math.sqrt((box.width ** 2 + box.height ** 2) / 2) : v);
|
|
128
|
+
const matrix = attributes["gradientTransform"]
|
|
129
|
+
? parseTransform(attributes["gradientTransform"])
|
|
130
|
+
: IDENTITY;
|
|
131
|
+
const skewed = !isIdentity(matrix) && (matrix[1] !== 0 || matrix[2] !== 0 || matrix[0] !== matrix[3]);
|
|
132
|
+
const point = (x, y) => [
|
|
133
|
+
matrix[0] * x + matrix[2] * y + matrix[4],
|
|
134
|
+
matrix[1] * x + matrix[3] * y + matrix[5],
|
|
135
|
+
];
|
|
136
|
+
const scale = Math.abs(matrix[0]);
|
|
137
|
+
const extend = SPREAD[attributes["spreadMethod"] ?? "pad"] ?? "pad";
|
|
138
|
+
if (def.kind === "linear") {
|
|
139
|
+
// Both anchors go through the transform, so a rotation or a shear lands correctly by itself.
|
|
140
|
+
const [x0, y0] = point(px(num(attributes["x1"], 0)), py(num(attributes["y1"], 0)));
|
|
141
|
+
const [x1, y1] = point(px(num(attributes["x2"], 1)), py(num(attributes["y2"], 0)));
|
|
142
|
+
return { type: "linear", x0, y0, x1, y1, stops, extend, alpha };
|
|
143
|
+
}
|
|
144
|
+
if (skewed) {
|
|
145
|
+
// A sheared or non-uniformly scaled radial gradient is an ELLIPSE. PDF's radial shading is
|
|
146
|
+
// circles only, so it cannot be expressed - naming it beats drawing a different shape.
|
|
147
|
+
throw new SvgUnsupportedError(`a radial gradient with gradientTransform="${attributes["gradientTransform"]}"`, "A non-uniform transform makes it elliptical, which a PDF shading cannot express.");
|
|
148
|
+
}
|
|
149
|
+
const cx = px(num(attributes["cx"], 0.5));
|
|
150
|
+
const cy = py(num(attributes["cy"], 0.5));
|
|
151
|
+
const r = pr(num(attributes["r"], 0.5)) * scale;
|
|
152
|
+
// The focal point defaults to the centre; it is where the gradient starts, at radius 0.
|
|
153
|
+
const [fx, fy] = point(attributes["fx"] === undefined ? cx : px(num(attributes["fx"], 0.5)), attributes["fy"] === undefined ? cy : py(num(attributes["fy"], 0.5)));
|
|
154
|
+
const [ox, oy] = point(cx, cy);
|
|
155
|
+
return { type: "radial", x0: fx, y0: fy, r0: 0, x1: ox, y1: oy, r1: r, stops, extend, alpha };
|
|
156
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IRNode } from "../ir/display-list.js";
|
|
2
|
+
export { SvgParseError, SvgUnsupportedError } from "./errors.js";
|
|
3
|
+
/** Where the drawing goes on the page, in engine (top-left) points. */
|
|
4
|
+
export interface SvgTarget {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
/** The intrinsic size of an SVG: its `width`/`height`, else its `viewBox`. */
|
|
11
|
+
export interface SvgSize {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
}
|
|
15
|
+
/** The intrinsic size, for sizing an `<Image>` that gives only one dimension (or none). */
|
|
16
|
+
export declare function svgSize(source: string): SvgSize;
|
|
17
|
+
/**
|
|
18
|
+
* An SVG document as display-list nodes, placed in `target`. Coordinates stay in the SVG's own user
|
|
19
|
+
* space and the mapping rides in the graphics state, so a stroke scales with the drawing - which is
|
|
20
|
+
* what SVG specifies and what a flattened implementation gets wrong.
|
|
21
|
+
*/
|
|
22
|
+
export declare function svgToIr(source: string, target: SvgTarget): IRNode[];
|