postext-pdf 0.1.0
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/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/__bench__/render.bench.test.d.ts +2 -0
- package/dist/__bench__/render.bench.test.d.ts.map +1 -0
- package/dist/__bench__/render.bench.test.js +86 -0
- package/dist/__bench__/render.bench.test.js.map +1 -0
- package/dist/__tests__/accessibility.test.d.ts +2 -0
- package/dist/__tests__/accessibility.test.d.ts.map +1 -0
- package/dist/__tests__/accessibility.test.js +364 -0
- package/dist/__tests__/accessibility.test.js.map +1 -0
- package/dist/__tests__/contentsLinks.test.d.ts +2 -0
- package/dist/__tests__/contentsLinks.test.d.ts.map +1 -0
- package/dist/__tests__/contentsLinks.test.js +131 -0
- package/dist/__tests__/contentsLinks.test.js.map +1 -0
- package/dist/__tests__/fontCache.test.d.ts +2 -0
- package/dist/__tests__/fontCache.test.d.ts.map +1 -0
- package/dist/__tests__/fontCache.test.js +42 -0
- package/dist/__tests__/fontCache.test.js.map +1 -0
- package/dist/__tests__/headerFooter.test.d.ts +2 -0
- package/dist/__tests__/headerFooter.test.d.ts.map +1 -0
- package/dist/__tests__/headerFooter.test.js +68 -0
- package/dist/__tests__/headerFooter.test.js.map +1 -0
- package/dist/__tests__/rotatedTable.test.d.ts +2 -0
- package/dist/__tests__/rotatedTable.test.d.ts.map +1 -0
- package/dist/__tests__/rotatedTable.test.js +112 -0
- package/dist/__tests__/rotatedTable.test.js.map +1 -0
- package/dist/__tests__/svgVector.test.d.ts +2 -0
- package/dist/__tests__/svgVector.test.d.ts.map +1 -0
- package/dist/__tests__/svgVector.test.js +322 -0
- package/dist/__tests__/svgVector.test.js.map +1 -0
- package/dist/__tests__/toUnicode.test.d.ts +2 -0
- package/dist/__tests__/toUnicode.test.d.ts.map +1 -0
- package/dist/__tests__/toUnicode.test.js +57 -0
- package/dist/__tests__/toUnicode.test.js.map +1 -0
- package/dist/colors.d.ts +23 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +47 -0
- package/dist/colors.js.map +1 -0
- package/dist/fontCache.d.ts +41 -0
- package/dist/fontCache.d.ts.map +1 -0
- package/dist/fontCache.js +154 -0
- package/dist/fontCache.js.map +1 -0
- package/dist/fontString.d.ts +14 -0
- package/dist/fontString.d.ts.map +1 -0
- package/dist/fontString.js +43 -0
- package/dist/fontString.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/pdf-backend/blockRender.d.ts +15 -0
- package/dist/pdf-backend/blockRender.d.ts.map +1 -0
- package/dist/pdf-backend/blockRender.js +244 -0
- package/dist/pdf-backend/blockRender.js.map +1 -0
- package/dist/pdf-backend/fontHelpers.d.ts +6 -0
- package/dist/pdf-backend/fontHelpers.d.ts.map +1 -0
- package/dist/pdf-backend/fontHelpers.js +81 -0
- package/dist/pdf-backend/fontHelpers.js.map +1 -0
- package/dist/pdf-backend/headerFooter.d.ts +15 -0
- package/dist/pdf-backend/headerFooter.d.ts.map +1 -0
- package/dist/pdf-backend/headerFooter.js +145 -0
- package/dist/pdf-backend/headerFooter.js.map +1 -0
- package/dist/pdf-backend/index.d.ts +47 -0
- package/dist/pdf-backend/index.d.ts.map +1 -0
- package/dist/pdf-backend/index.js +227 -0
- package/dist/pdf-backend/index.js.map +1 -0
- package/dist/pdf-backend/links.d.ts +44 -0
- package/dist/pdf-backend/links.d.ts.map +1 -0
- package/dist/pdf-backend/links.js +110 -0
- package/dist/pdf-backend/links.js.map +1 -0
- package/dist/pdf-backend/outlines.d.ts +8 -0
- package/dist/pdf-backend/outlines.d.ts.map +1 -0
- package/dist/pdf-backend/outlines.js +145 -0
- package/dist/pdf-backend/outlines.js.map +1 -0
- package/dist/pdf-backend/pageDecorations.d.ts +13 -0
- package/dist/pdf-backend/pageDecorations.d.ts.map +1 -0
- package/dist/pdf-backend/pageDecorations.js +88 -0
- package/dist/pdf-backend/pageDecorations.js.map +1 -0
- package/dist/pdf-backend/pageLabels.d.ts +8 -0
- package/dist/pdf-backend/pageLabels.d.ts.map +1 -0
- package/dist/pdf-backend/pageLabels.js +61 -0
- package/dist/pdf-backend/pageLabels.js.map +1 -0
- package/dist/pdf-backend/primitives.d.ts +46 -0
- package/dist/pdf-backend/primitives.d.ts.map +1 -0
- package/dist/pdf-backend/primitives.js +162 -0
- package/dist/pdf-backend/primitives.js.map +1 -0
- package/dist/pdf-backend/renderResourceBlock.d.ts +87 -0
- package/dist/pdf-backend/renderResourceBlock.d.ts.map +1 -0
- package/dist/pdf-backend/renderResourceBlock.js +753 -0
- package/dist/pdf-backend/renderResourceBlock.js.map +1 -0
- package/dist/pdf-backend/structureFlow.d.ts +55 -0
- package/dist/pdf-backend/structureFlow.d.ts.map +1 -0
- package/dist/pdf-backend/structureFlow.js +180 -0
- package/dist/pdf-backend/structureFlow.js.map +1 -0
- package/dist/pdf-backend/svgVector.d.ts +144 -0
- package/dist/pdf-backend/svgVector.d.ts.map +1 -0
- package/dist/pdf-backend/svgVector.js +1455 -0
- package/dist/pdf-backend/svgVector.js.map +1 -0
- package/dist/pdf-backend/tagging.d.ts +136 -0
- package/dist/pdf-backend/tagging.d.ts.map +1 -0
- package/dist/pdf-backend/tagging.js +296 -0
- package/dist/pdf-backend/tagging.js.map +1 -0
- package/dist/woff2.d.ts +11 -0
- package/dist/woff2.d.ts.map +1 -0
- package/dist/woff2.js +47 -0
- package/dist/woff2.js.map +1 -0
- package/dist/worker/client.d.ts +30 -0
- package/dist/worker/client.d.ts.map +1 -0
- package/dist/worker/client.js +112 -0
- package/dist/worker/client.js.map +1 -0
- package/dist/worker/pdf.worker.d.ts +2 -0
- package/dist/worker/pdf.worker.d.ts.map +1 -0
- package/dist/worker/pdf.worker.js +67 -0
- package/dist/worker/pdf.worker.js.map +1 -0
- package/dist/worker/protocol.d.ts +49 -0
- package/dist/worker/protocol.d.ts.map +1 -0
- package/dist/worker/protocol.js +2 -0
- package/dist/worker/protocol.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,1455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector emission of SVG resources into the PDF content stream.
|
|
3
|
+
*
|
|
4
|
+
* Converts a conservative subset of SVG into a flat list of filled / stroked
|
|
5
|
+
* paths in the SVG's root user space, which {@link drawVectorDrawing} then
|
|
6
|
+
* paints with native PDF path operators — so the figure stays vector (sharp at
|
|
7
|
+
* any zoom, tiny on disk) instead of going through a raster fallback.
|
|
8
|
+
*
|
|
9
|
+
* Supported: `svg` (root, `viewBox`, `preserveAspectRatio`), `g` / `a`,
|
|
10
|
+
* `defs`, `path`, `rect`, `circle`, `ellipse`, `line`, `polyline`, `polygon`,
|
|
11
|
+
* `use` (of any of the above), `clipPath` (`userSpaceOnUse`), `transform`
|
|
12
|
+
* lists, solid `fill` / `stroke` paints (hex, `rgb()`, named colours,
|
|
13
|
+
* `currentColor`), `fill-rule`, opacities, line caps / joins / miter limit,
|
|
14
|
+
* dash patterns, `display` / `visibility`, the same properties inside a
|
|
15
|
+
* `style` attribute, and — when the caller supplies a {@link VectorFontResolver}
|
|
16
|
+
* — `text` / `tspan` runs (`x`, `y`, single `dx` / `dy`, `font-family`,
|
|
17
|
+
* `font-size`, `font-weight`, `font-style`, `text-anchor`), which are set as
|
|
18
|
+
* real, selectable text in the document's embedded fonts.
|
|
19
|
+
*
|
|
20
|
+
* Anything else — images, gradients, patterns, masks, filters, markers,
|
|
21
|
+
* symbols, nested `svg`, style sheets, `objectBoundingBox` clips, text on a
|
|
22
|
+
* path or with per-glyph positioning, text in a font the resolver cannot
|
|
23
|
+
* provide — makes {@link svgToVectorDrawing} return `null`, and the caller
|
|
24
|
+
* falls back to a raster embed. Bailing out (rather than drawing an
|
|
25
|
+
* approximation) keeps the printed figure faithful to what the screen
|
|
26
|
+
* backends show.
|
|
27
|
+
*
|
|
28
|
+
* Known approximations: group `opacity` is folded into each descendant's fill
|
|
29
|
+
* and stroke alpha (PDF group transparency would need a transparency group
|
|
30
|
+
* XObject); a `clipPath` with several children is the union of its shapes in
|
|
31
|
+
* SVG but is emitted as one nonzero path here; strokes under a non-uniform
|
|
32
|
+
* transform get a uniform width (the geometric mean of the scale factors).
|
|
33
|
+
*/
|
|
34
|
+
import { PDFOperator, PDFOperatorNames, LineCapStyle, LineJoinStyle, appendBezierCurve, drawObject, beginText, clip, clipEvenOdd, closePath, concatTransformationMatrix, endPath, endText, fill, fillAndStroke, lineTo, moveTo, popGraphicsState, pushGraphicsState, rectangle, setDashPattern, setFillingColor, setFontAndSize, setGraphicsState, setLineCap, setLineJoin, setLineWidth, setStrokingColor, setTextMatrix, showText, stroke, } from 'pdf-lib';
|
|
35
|
+
import { colorFromHex } from './primitives';
|
|
36
|
+
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
|
|
37
|
+
function decodeEntities(s) {
|
|
38
|
+
if (!s.includes('&'))
|
|
39
|
+
return s;
|
|
40
|
+
return s.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (m, body) => {
|
|
41
|
+
if (body[0] === '#') {
|
|
42
|
+
const code = body[1] === 'x' || body[1] === 'X' ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
|
|
43
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : m;
|
|
44
|
+
}
|
|
45
|
+
return ENTITIES[body] ?? m;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Local name of a qualified name (`svg:path` → `path`); `null` for names in
|
|
49
|
+
* foreign namespaces we can safely skip (`inkscape:*`, `sodipodi:*`, ...). */
|
|
50
|
+
function localName(qualified) {
|
|
51
|
+
const i = qualified.indexOf(':');
|
|
52
|
+
if (i < 0)
|
|
53
|
+
return { local: qualified, foreign: false };
|
|
54
|
+
const prefix = qualified.slice(0, i);
|
|
55
|
+
return { local: qualified.slice(i + 1), foreign: prefix !== 'svg' };
|
|
56
|
+
}
|
|
57
|
+
/** Minimal non-validating XML parser: elements, attributes, comments, CDATA,
|
|
58
|
+
* processing instructions and doctype. Character data is discarded (no
|
|
59
|
+
* supported element renders text). Returns `null` on malformed input. */
|
|
60
|
+
function parseXml(text) {
|
|
61
|
+
const root = { name: '#root', attrs: {}, children: [], content: [] };
|
|
62
|
+
const stack = [root];
|
|
63
|
+
let i = 0;
|
|
64
|
+
const n = text.length;
|
|
65
|
+
const markText = (from, to, raw = false) => {
|
|
66
|
+
if (to <= from)
|
|
67
|
+
return;
|
|
68
|
+
const top = stack[stack.length - 1];
|
|
69
|
+
const chunk = text.slice(from, to);
|
|
70
|
+
if (chunk.trim() !== '')
|
|
71
|
+
top.hasText = true;
|
|
72
|
+
top.content.push(raw ? chunk : decodeEntities(chunk));
|
|
73
|
+
};
|
|
74
|
+
while (i < n) {
|
|
75
|
+
const lt = text.indexOf('<', i);
|
|
76
|
+
if (lt < 0) {
|
|
77
|
+
markText(i, n);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
markText(i, lt);
|
|
81
|
+
i = lt;
|
|
82
|
+
if (text.startsWith('<!--', i)) {
|
|
83
|
+
const end = text.indexOf('-->', i + 4);
|
|
84
|
+
if (end < 0)
|
|
85
|
+
return null;
|
|
86
|
+
i = end + 3;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (text.startsWith('<![CDATA[', i)) {
|
|
90
|
+
const end = text.indexOf(']]>', i + 9);
|
|
91
|
+
if (end < 0)
|
|
92
|
+
return null;
|
|
93
|
+
markText(i + 9, end, true);
|
|
94
|
+
i = end + 3;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (text.startsWith('<?', i)) {
|
|
98
|
+
const end = text.indexOf('?>', i + 2);
|
|
99
|
+
if (end < 0)
|
|
100
|
+
return null;
|
|
101
|
+
i = end + 2;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (text.startsWith('<!', i)) {
|
|
105
|
+
// <!DOCTYPE ...> possibly with an internal subset in [ ... ].
|
|
106
|
+
let depth = 0;
|
|
107
|
+
let j = i + 2;
|
|
108
|
+
for (; j < n; j++) {
|
|
109
|
+
const ch = text[j];
|
|
110
|
+
if (ch === '[')
|
|
111
|
+
depth++;
|
|
112
|
+
else if (ch === ']')
|
|
113
|
+
depth--;
|
|
114
|
+
else if (ch === '>' && depth === 0)
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
if (j >= n)
|
|
118
|
+
return null;
|
|
119
|
+
i = j + 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (text.startsWith('</', i)) {
|
|
123
|
+
const end = text.indexOf('>', i + 2);
|
|
124
|
+
if (end < 0)
|
|
125
|
+
return null;
|
|
126
|
+
const name = text.slice(i + 2, end).trim();
|
|
127
|
+
const open = stack.pop();
|
|
128
|
+
if (!open || open.name !== name || stack.length === 0)
|
|
129
|
+
return null;
|
|
130
|
+
i = end + 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
// Start tag.
|
|
134
|
+
let j = i + 1;
|
|
135
|
+
while (j < n && !/[\s/>]/.test(text[j]))
|
|
136
|
+
j++;
|
|
137
|
+
const name = text.slice(i + 1, j);
|
|
138
|
+
if (!name)
|
|
139
|
+
return null;
|
|
140
|
+
const attrs = {};
|
|
141
|
+
let selfClosing = false;
|
|
142
|
+
for (;;) {
|
|
143
|
+
while (j < n && /\s/.test(text[j]))
|
|
144
|
+
j++;
|
|
145
|
+
if (j >= n)
|
|
146
|
+
return null;
|
|
147
|
+
if (text[j] === '/') {
|
|
148
|
+
selfClosing = true;
|
|
149
|
+
j++;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (text[j] === '>') {
|
|
153
|
+
j++;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
let k = j;
|
|
157
|
+
while (k < n && !/[\s=/>]/.test(text[k]))
|
|
158
|
+
k++;
|
|
159
|
+
const attrName = text.slice(j, k);
|
|
160
|
+
if (!attrName)
|
|
161
|
+
return null;
|
|
162
|
+
j = k;
|
|
163
|
+
while (j < n && /\s/.test(text[j]))
|
|
164
|
+
j++;
|
|
165
|
+
if (text[j] !== '=') {
|
|
166
|
+
// Attribute without a value (not valid XML, but harmless).
|
|
167
|
+
attrs[attrName] = '';
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
j++;
|
|
171
|
+
while (j < n && /\s/.test(text[j]))
|
|
172
|
+
j++;
|
|
173
|
+
const quote = text[j];
|
|
174
|
+
if (quote !== '"' && quote !== "'")
|
|
175
|
+
return null;
|
|
176
|
+
const close = text.indexOf(quote, j + 1);
|
|
177
|
+
if (close < 0)
|
|
178
|
+
return null;
|
|
179
|
+
attrs[attrName] = decodeEntities(text.slice(j + 1, close));
|
|
180
|
+
j = close + 1;
|
|
181
|
+
}
|
|
182
|
+
const el = { name, attrs, children: [], content: [] };
|
|
183
|
+
stack[stack.length - 1].children.push(el);
|
|
184
|
+
stack[stack.length - 1].content.push(el);
|
|
185
|
+
if (!selfClosing)
|
|
186
|
+
stack.push(el);
|
|
187
|
+
i = j;
|
|
188
|
+
}
|
|
189
|
+
if (stack.length !== 1)
|
|
190
|
+
return null;
|
|
191
|
+
return root;
|
|
192
|
+
}
|
|
193
|
+
const IDENTITY = [1, 0, 0, 1, 0, 0];
|
|
194
|
+
function mul(a, b) {
|
|
195
|
+
return [
|
|
196
|
+
a[0] * b[0] + a[2] * b[1],
|
|
197
|
+
a[1] * b[0] + a[3] * b[1],
|
|
198
|
+
a[0] * b[2] + a[2] * b[3],
|
|
199
|
+
a[1] * b[2] + a[3] * b[3],
|
|
200
|
+
a[0] * b[4] + a[2] * b[5] + a[4],
|
|
201
|
+
a[1] * b[4] + a[3] * b[5] + a[5],
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
function apply(m, x, y) {
|
|
205
|
+
return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
|
|
206
|
+
}
|
|
207
|
+
/** Uniform length scale of a matrix (geometric mean of its singular values). */
|
|
208
|
+
function lengthScale(m) {
|
|
209
|
+
return Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
|
|
210
|
+
}
|
|
211
|
+
function transformSegs(segs, m) {
|
|
212
|
+
return segs.map((s) => {
|
|
213
|
+
switch (s[0]) {
|
|
214
|
+
case 'M':
|
|
215
|
+
case 'L': {
|
|
216
|
+
const [x, y] = apply(m, s[1], s[2]);
|
|
217
|
+
return [s[0], x, y];
|
|
218
|
+
}
|
|
219
|
+
case 'C': {
|
|
220
|
+
const [x1, y1] = apply(m, s[1], s[2]);
|
|
221
|
+
const [x2, y2] = apply(m, s[3], s[4]);
|
|
222
|
+
const [x, y] = apply(m, s[5], s[6]);
|
|
223
|
+
return ['C', x1, y1, x2, y2, x, y];
|
|
224
|
+
}
|
|
225
|
+
default:
|
|
226
|
+
return s;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
class Unsupported extends Error {
|
|
231
|
+
}
|
|
232
|
+
function unsupported(what) {
|
|
233
|
+
throw new Unsupported(what);
|
|
234
|
+
}
|
|
235
|
+
function parseTransform(value) {
|
|
236
|
+
if (!value)
|
|
237
|
+
return IDENTITY;
|
|
238
|
+
let m = IDENTITY;
|
|
239
|
+
const re = /([a-zA-Z]+)\s*\(([^)]*)\)/g;
|
|
240
|
+
let match;
|
|
241
|
+
let consumed = 0;
|
|
242
|
+
while ((match = re.exec(value)) !== null) {
|
|
243
|
+
if (value.slice(consumed, match.index).trim().replace(/,/g, '') !== '')
|
|
244
|
+
unsupported('transform syntax');
|
|
245
|
+
consumed = match.index + match[0].length;
|
|
246
|
+
const args = match[2].trim().split(/[\s,]+/).filter((s) => s !== '').map(Number);
|
|
247
|
+
if (args.some((v) => !Number.isFinite(v)))
|
|
248
|
+
unsupported('transform args');
|
|
249
|
+
let t;
|
|
250
|
+
switch (match[1]) {
|
|
251
|
+
case 'matrix':
|
|
252
|
+
if (args.length !== 6)
|
|
253
|
+
unsupported('matrix()');
|
|
254
|
+
t = [args[0], args[1], args[2], args[3], args[4], args[5]];
|
|
255
|
+
break;
|
|
256
|
+
case 'translate':
|
|
257
|
+
t = [1, 0, 0, 1, args[0] ?? 0, args[1] ?? 0];
|
|
258
|
+
break;
|
|
259
|
+
case 'scale':
|
|
260
|
+
t = [args[0] ?? 1, 0, 0, args[1] ?? args[0] ?? 1, 0, 0];
|
|
261
|
+
break;
|
|
262
|
+
case 'rotate': {
|
|
263
|
+
const a = ((args[0] ?? 0) * Math.PI) / 180;
|
|
264
|
+
const cos = Math.cos(a);
|
|
265
|
+
const sin = Math.sin(a);
|
|
266
|
+
const r = [cos, sin, -sin, cos, 0, 0];
|
|
267
|
+
if (args.length >= 3) {
|
|
268
|
+
const cx = args[1];
|
|
269
|
+
const cy = args[2];
|
|
270
|
+
t = mul(mul([1, 0, 0, 1, cx, cy], r), [1, 0, 0, 1, -cx, -cy]);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
t = r;
|
|
274
|
+
}
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case 'skewX':
|
|
278
|
+
t = [1, 0, Math.tan(((args[0] ?? 0) * Math.PI) / 180), 1, 0, 0];
|
|
279
|
+
break;
|
|
280
|
+
case 'skewY':
|
|
281
|
+
t = [1, Math.tan(((args[0] ?? 0) * Math.PI) / 180), 0, 1, 0, 0];
|
|
282
|
+
break;
|
|
283
|
+
default:
|
|
284
|
+
return unsupported(`transform ${match[1]}`);
|
|
285
|
+
}
|
|
286
|
+
m = mul(m, t);
|
|
287
|
+
}
|
|
288
|
+
if (value.slice(consumed).trim() !== '')
|
|
289
|
+
unsupported('transform syntax');
|
|
290
|
+
return m;
|
|
291
|
+
}
|
|
292
|
+
// ------------------------------------------------------------ path data
|
|
293
|
+
const KAPPA = 0.5522847498307936;
|
|
294
|
+
/** Endpoint-parameterised elliptical arc → cubic Béziers (SVG 1.1 §F.6). */
|
|
295
|
+
function arcToCubics(x1, y1, rx, ry, phiDeg, largeArc, sweep, x2, y2) {
|
|
296
|
+
if (x1 === x2 && y1 === y2)
|
|
297
|
+
return [];
|
|
298
|
+
rx = Math.abs(rx);
|
|
299
|
+
ry = Math.abs(ry);
|
|
300
|
+
if (rx === 0 || ry === 0)
|
|
301
|
+
return [['L', x2, y2]];
|
|
302
|
+
const phi = (phiDeg * Math.PI) / 180;
|
|
303
|
+
const cosPhi = Math.cos(phi);
|
|
304
|
+
const sinPhi = Math.sin(phi);
|
|
305
|
+
const dx = (x1 - x2) / 2;
|
|
306
|
+
const dy = (y1 - y2) / 2;
|
|
307
|
+
const x1p = cosPhi * dx + sinPhi * dy;
|
|
308
|
+
const y1p = -sinPhi * dx + cosPhi * dy;
|
|
309
|
+
const lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
|
|
310
|
+
if (lambda > 1) {
|
|
311
|
+
const s = Math.sqrt(lambda);
|
|
312
|
+
rx *= s;
|
|
313
|
+
ry *= s;
|
|
314
|
+
}
|
|
315
|
+
const rx2 = rx * rx;
|
|
316
|
+
const ry2 = ry * ry;
|
|
317
|
+
const num = Math.max(0, rx2 * ry2 - rx2 * y1p * y1p - ry2 * x1p * x1p);
|
|
318
|
+
const den = rx2 * y1p * y1p + ry2 * x1p * x1p;
|
|
319
|
+
let coef = den === 0 ? 0 : Math.sqrt(num / den);
|
|
320
|
+
if (largeArc === sweep)
|
|
321
|
+
coef = -coef;
|
|
322
|
+
const cxp = (coef * rx * y1p) / ry;
|
|
323
|
+
const cyp = (-coef * ry * x1p) / rx;
|
|
324
|
+
const cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
|
|
325
|
+
const cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
|
|
326
|
+
const angle = (ux, uy, vx, vy) => {
|
|
327
|
+
const dot = ux * vx + uy * vy;
|
|
328
|
+
const len = Math.hypot(ux, uy) * Math.hypot(vx, vy);
|
|
329
|
+
let a = Math.acos(Math.max(-1, Math.min(1, dot / len)));
|
|
330
|
+
if (ux * vy - uy * vx < 0)
|
|
331
|
+
a = -a;
|
|
332
|
+
return a;
|
|
333
|
+
};
|
|
334
|
+
const theta1 = angle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
335
|
+
let dTheta = angle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
|
|
336
|
+
if (!sweep && dTheta > 0)
|
|
337
|
+
dTheta -= 2 * Math.PI;
|
|
338
|
+
else if (sweep && dTheta < 0)
|
|
339
|
+
dTheta += 2 * Math.PI;
|
|
340
|
+
const segments = Math.max(1, Math.ceil(Math.abs(dTheta) / (Math.PI / 2) - 1e-9));
|
|
341
|
+
const delta = dTheta / segments;
|
|
342
|
+
const t = (4 / 3) * Math.tan(delta / 4);
|
|
343
|
+
const out = [];
|
|
344
|
+
let theta = theta1;
|
|
345
|
+
for (let i = 0; i < segments; i++) {
|
|
346
|
+
const cos1 = Math.cos(theta);
|
|
347
|
+
const sin1 = Math.sin(theta);
|
|
348
|
+
const cos2 = Math.cos(theta + delta);
|
|
349
|
+
const sin2 = Math.sin(theta + delta);
|
|
350
|
+
// Derivative points on the unit circle, mapped through the ellipse.
|
|
351
|
+
const p = (c, s) => [
|
|
352
|
+
cx + rx * cosPhi * c - ry * sinPhi * s,
|
|
353
|
+
cy + rx * sinPhi * c + ry * cosPhi * s,
|
|
354
|
+
];
|
|
355
|
+
const [ex1, ey1] = p(cos1, sin1);
|
|
356
|
+
const [ex2, ey2] = p(cos2, sin2);
|
|
357
|
+
const d1x = -rx * cosPhi * sin1 - ry * sinPhi * cos1;
|
|
358
|
+
const d1y = -rx * sinPhi * sin1 + ry * cosPhi * cos1;
|
|
359
|
+
const d2x = -rx * cosPhi * sin2 - ry * sinPhi * cos2;
|
|
360
|
+
const d2y = -rx * sinPhi * sin2 + ry * cosPhi * cos2;
|
|
361
|
+
const last = i === segments - 1;
|
|
362
|
+
out.push(['C', ex1 + t * d1x, ey1 + t * d1y, ex2 - t * d2x, ey2 - t * d2y, last ? x2 : ex2, last ? y2 : ey2]);
|
|
363
|
+
theta += delta;
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
/** Parse SVG path data into absolute M / L / C / Z segments. Relative
|
|
368
|
+
* commands, H / V, smooth curves, quadratics and arcs are all normalised. */
|
|
369
|
+
export function parsePathData(d) {
|
|
370
|
+
const out = [];
|
|
371
|
+
let i = 0;
|
|
372
|
+
const n = d.length;
|
|
373
|
+
let cmd = '';
|
|
374
|
+
let cx = 0;
|
|
375
|
+
let cy = 0;
|
|
376
|
+
let sx = 0;
|
|
377
|
+
let sy = 0;
|
|
378
|
+
// Last cubic / quadratic control point, for S / T reflection.
|
|
379
|
+
let lastC = null;
|
|
380
|
+
let lastQ = null;
|
|
381
|
+
const skipSep = () => {
|
|
382
|
+
while (i < n && /[\s,]/.test(d[i]))
|
|
383
|
+
i++;
|
|
384
|
+
};
|
|
385
|
+
const number = () => {
|
|
386
|
+
skipSep();
|
|
387
|
+
const m = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?/.exec(d.slice(i));
|
|
388
|
+
if (!m)
|
|
389
|
+
unsupported('path number');
|
|
390
|
+
i += m[0].length;
|
|
391
|
+
return Number(m[0]);
|
|
392
|
+
};
|
|
393
|
+
const flag = () => {
|
|
394
|
+
skipSep();
|
|
395
|
+
const ch = d[i];
|
|
396
|
+
if (ch !== '0' && ch !== '1')
|
|
397
|
+
unsupported('arc flag');
|
|
398
|
+
i++;
|
|
399
|
+
return ch === '1';
|
|
400
|
+
};
|
|
401
|
+
while (i < n) {
|
|
402
|
+
skipSep();
|
|
403
|
+
if (i >= n)
|
|
404
|
+
break;
|
|
405
|
+
const ch = d[i];
|
|
406
|
+
if (/[a-zA-Z]/.test(ch)) {
|
|
407
|
+
cmd = ch;
|
|
408
|
+
i++;
|
|
409
|
+
}
|
|
410
|
+
else if (!cmd) {
|
|
411
|
+
unsupported('path start');
|
|
412
|
+
}
|
|
413
|
+
else if (cmd === 'M')
|
|
414
|
+
cmd = 'L';
|
|
415
|
+
else if (cmd === 'm')
|
|
416
|
+
cmd = 'l';
|
|
417
|
+
const rel = cmd === cmd.toLowerCase();
|
|
418
|
+
const upper = cmd.toUpperCase();
|
|
419
|
+
const ox = rel ? cx : 0;
|
|
420
|
+
const oy = rel ? cy : 0;
|
|
421
|
+
switch (upper) {
|
|
422
|
+
case 'M': {
|
|
423
|
+
const x = number() + ox;
|
|
424
|
+
const y = number() + oy;
|
|
425
|
+
out.push(['M', x, y]);
|
|
426
|
+
cx = sx = x;
|
|
427
|
+
cy = sy = y;
|
|
428
|
+
lastC = lastQ = null;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
case 'L': {
|
|
432
|
+
const x = number() + ox;
|
|
433
|
+
const y = number() + oy;
|
|
434
|
+
out.push(['L', x, y]);
|
|
435
|
+
cx = x;
|
|
436
|
+
cy = y;
|
|
437
|
+
lastC = lastQ = null;
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
case 'H': {
|
|
441
|
+
const x = number() + ox;
|
|
442
|
+
out.push(['L', x, cy]);
|
|
443
|
+
cx = x;
|
|
444
|
+
lastC = lastQ = null;
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
case 'V': {
|
|
448
|
+
const y = number() + oy;
|
|
449
|
+
out.push(['L', cx, y]);
|
|
450
|
+
cy = y;
|
|
451
|
+
lastC = lastQ = null;
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
case 'C': {
|
|
455
|
+
const x1 = number() + ox, y1 = number() + oy;
|
|
456
|
+
const x2 = number() + ox, y2 = number() + oy;
|
|
457
|
+
const x = number() + ox, y = number() + oy;
|
|
458
|
+
out.push(['C', x1, y1, x2, y2, x, y]);
|
|
459
|
+
lastC = [x2, y2];
|
|
460
|
+
lastQ = null;
|
|
461
|
+
cx = x;
|
|
462
|
+
cy = y;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
case 'S': {
|
|
466
|
+
const x2 = number() + ox, y2 = number() + oy;
|
|
467
|
+
const x = number() + ox, y = number() + oy;
|
|
468
|
+
const x1 = lastC ? 2 * cx - lastC[0] : cx;
|
|
469
|
+
const y1 = lastC ? 2 * cy - lastC[1] : cy;
|
|
470
|
+
out.push(['C', x1, y1, x2, y2, x, y]);
|
|
471
|
+
lastC = [x2, y2];
|
|
472
|
+
lastQ = null;
|
|
473
|
+
cx = x;
|
|
474
|
+
cy = y;
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
case 'Q':
|
|
478
|
+
case 'T': {
|
|
479
|
+
let qx, qy;
|
|
480
|
+
if (upper === 'Q') {
|
|
481
|
+
qx = number() + ox;
|
|
482
|
+
qy = number() + oy;
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
qx = lastQ ? 2 * cx - lastQ[0] : cx;
|
|
486
|
+
qy = lastQ ? 2 * cy - lastQ[1] : cy;
|
|
487
|
+
}
|
|
488
|
+
const x = number() + ox, y = number() + oy;
|
|
489
|
+
out.push(['C',
|
|
490
|
+
cx + (2 / 3) * (qx - cx), cy + (2 / 3) * (qy - cy),
|
|
491
|
+
x + (2 / 3) * (qx - x), y + (2 / 3) * (qy - y),
|
|
492
|
+
x, y]);
|
|
493
|
+
lastQ = [qx, qy];
|
|
494
|
+
lastC = null;
|
|
495
|
+
cx = x;
|
|
496
|
+
cy = y;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
case 'A': {
|
|
500
|
+
const rx = number(), ry = number(), rot = number();
|
|
501
|
+
const large = flag(), sweep = flag();
|
|
502
|
+
const x = number() + ox, y = number() + oy;
|
|
503
|
+
out.push(...arcToCubics(cx, cy, rx, ry, rot, large, sweep, x, y));
|
|
504
|
+
lastC = lastQ = null;
|
|
505
|
+
cx = x;
|
|
506
|
+
cy = y;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case 'Z': {
|
|
510
|
+
out.push(['Z']);
|
|
511
|
+
cx = sx;
|
|
512
|
+
cy = sy;
|
|
513
|
+
lastC = lastQ = null;
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
default:
|
|
517
|
+
unsupported(`path command ${cmd}`);
|
|
518
|
+
}
|
|
519
|
+
if (upper === 'Z') {
|
|
520
|
+
// A closepath takes no arguments; the next token must be a command.
|
|
521
|
+
skipSep();
|
|
522
|
+
if (i < n && !/[a-zA-Z]/.test(d[i]))
|
|
523
|
+
unsupported('path data after Z');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return out;
|
|
527
|
+
}
|
|
528
|
+
// ------------------------------------------------------------ shapes
|
|
529
|
+
function ellipseSegs(cx, cy, rx, ry) {
|
|
530
|
+
const kx = rx * KAPPA;
|
|
531
|
+
const ky = ry * KAPPA;
|
|
532
|
+
return [
|
|
533
|
+
['M', cx + rx, cy],
|
|
534
|
+
['C', cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry],
|
|
535
|
+
['C', cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy],
|
|
536
|
+
['C', cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry],
|
|
537
|
+
['C', cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy],
|
|
538
|
+
['Z'],
|
|
539
|
+
];
|
|
540
|
+
}
|
|
541
|
+
function rectSegs(x, y, w, h, rx, ry) {
|
|
542
|
+
if (rx <= 0 || ry <= 0) {
|
|
543
|
+
return [['M', x, y], ['L', x + w, y], ['L', x + w, y + h], ['L', x, y + h], ['Z']];
|
|
544
|
+
}
|
|
545
|
+
rx = Math.min(rx, w / 2);
|
|
546
|
+
ry = Math.min(ry, h / 2);
|
|
547
|
+
const kx = rx * KAPPA;
|
|
548
|
+
const ky = ry * KAPPA;
|
|
549
|
+
return [
|
|
550
|
+
['M', x + rx, y],
|
|
551
|
+
['L', x + w - rx, y],
|
|
552
|
+
['C', x + w - rx + kx, y, x + w, y + ry - ky, x + w, y + ry],
|
|
553
|
+
['L', x + w, y + h - ry],
|
|
554
|
+
['C', x + w, y + h - ry + ky, x + w - rx + kx, y + h, x + w - rx, y + h],
|
|
555
|
+
['L', x + rx, y + h],
|
|
556
|
+
['C', x + rx - kx, y + h, x, y + h - ry + ky, x, y + h - ry],
|
|
557
|
+
['L', x, y + ry],
|
|
558
|
+
['C', x, y + ry - ky, x + rx - kx, y, x + rx, y],
|
|
559
|
+
['Z'],
|
|
560
|
+
];
|
|
561
|
+
}
|
|
562
|
+
function pointsSegs(points, close) {
|
|
563
|
+
const nums = points.trim().split(/[\s,]+/).filter((s) => s !== '').map(Number);
|
|
564
|
+
if (nums.length < 2 || nums.length % 2 !== 0 || nums.some((v) => !Number.isFinite(v))) {
|
|
565
|
+
unsupported('points');
|
|
566
|
+
}
|
|
567
|
+
const segs = [['M', nums[0], nums[1]]];
|
|
568
|
+
for (let i = 2; i < nums.length; i += 2)
|
|
569
|
+
segs.push(['L', nums[i], nums[i + 1]]);
|
|
570
|
+
if (close)
|
|
571
|
+
segs.push(['Z']);
|
|
572
|
+
return segs;
|
|
573
|
+
}
|
|
574
|
+
// ------------------------------------------------------------ styling
|
|
575
|
+
/** Parse a length in user units. Absolute CSS units are converted at 96 dpi
|
|
576
|
+
* (the SVG user unit is a CSS px); relative units are unsupported. */
|
|
577
|
+
function parseLength(value, fallback) {
|
|
578
|
+
if (value === undefined || value.trim() === '')
|
|
579
|
+
return fallback;
|
|
580
|
+
const m = /^\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)\s*([a-zA-Z%]*)\s*$/.exec(value);
|
|
581
|
+
if (!m)
|
|
582
|
+
return unsupported(`length ${value}`);
|
|
583
|
+
const num = Number(m[1]);
|
|
584
|
+
const unit = m[2].toLowerCase();
|
|
585
|
+
const factor = { '': 1, px: 1, pt: 96 / 72, pc: 16, in: 96, cm: 96 / 2.54, mm: 96 / 25.4 };
|
|
586
|
+
const f = factor[unit];
|
|
587
|
+
if (f === undefined)
|
|
588
|
+
unsupported(`length unit ${unit}`);
|
|
589
|
+
return num * f;
|
|
590
|
+
}
|
|
591
|
+
function parseNumberOrPercent(value, fallback) {
|
|
592
|
+
const v = value.trim();
|
|
593
|
+
if (v === '')
|
|
594
|
+
return fallback;
|
|
595
|
+
if (v.endsWith('%')) {
|
|
596
|
+
const p = Number(v.slice(0, -1));
|
|
597
|
+
return Number.isFinite(p) ? p / 100 : fallback;
|
|
598
|
+
}
|
|
599
|
+
const num = Number(v);
|
|
600
|
+
return Number.isFinite(num) ? num : fallback;
|
|
601
|
+
}
|
|
602
|
+
const NAMED_COLORS = {
|
|
603
|
+
aliceblue: '#f0f8ff', antiquewhite: '#faebd7', aqua: '#00ffff', aquamarine: '#7fffd4', azure: '#f0ffff',
|
|
604
|
+
beige: '#f5f5dc', bisque: '#ffe4c4', black: '#000000', blanchedalmond: '#ffebcd', blue: '#0000ff',
|
|
605
|
+
blueviolet: '#8a2be2', brown: '#a52a2a', burlywood: '#deb887', cadetblue: '#5f9ea0', chartreuse: '#7fff00',
|
|
606
|
+
chocolate: '#d2691e', coral: '#ff7f50', cornflowerblue: '#6495ed', cornsilk: '#fff8dc', crimson: '#dc143c',
|
|
607
|
+
cyan: '#00ffff', darkblue: '#00008b', darkcyan: '#008b8b', darkgoldenrod: '#b8860b', darkgray: '#a9a9a9',
|
|
608
|
+
darkgreen: '#006400', darkgrey: '#a9a9a9', darkkhaki: '#bdb76b', darkmagenta: '#8b008b', darkolivegreen: '#556b2f',
|
|
609
|
+
darkorange: '#ff8c00', darkorchid: '#9932cc', darkred: '#8b0000', darksalmon: '#e9967a', darkseagreen: '#8fbc8f',
|
|
610
|
+
darkslateblue: '#483d8b', darkslategray: '#2f4f4f', darkslategrey: '#2f4f4f', darkturquoise: '#00ced1',
|
|
611
|
+
darkviolet: '#9400d3', deeppink: '#ff1493', deepskyblue: '#00bfff', dimgray: '#696969', dimgrey: '#696969',
|
|
612
|
+
dodgerblue: '#1e90ff', firebrick: '#b22222', floralwhite: '#fffaf0', forestgreen: '#228b22', fuchsia: '#ff00ff',
|
|
613
|
+
gainsboro: '#dcdcdc', ghostwhite: '#f8f8ff', gold: '#ffd700', goldenrod: '#daa520', gray: '#808080',
|
|
614
|
+
green: '#008000', greenyellow: '#adff2f', grey: '#808080', honeydew: '#f0fff0', hotpink: '#ff69b4',
|
|
615
|
+
indianred: '#cd5c5c', indigo: '#4b0082', ivory: '#fffff0', khaki: '#f0e68c', lavender: '#e6e6fa',
|
|
616
|
+
lavenderblush: '#fff0f5', lawngreen: '#7cfc00', lemonchiffon: '#fffacd', lightblue: '#add8e6', lightcoral: '#f08080',
|
|
617
|
+
lightcyan: '#e0ffff', lightgoldenrodyellow: '#fafad2', lightgray: '#d3d3d3', lightgreen: '#90ee90', lightgrey: '#d3d3d3',
|
|
618
|
+
lightpink: '#ffb6c1', lightsalmon: '#ffa07a', lightseagreen: '#20b2aa', lightskyblue: '#87cefa', lightslategray: '#778899',
|
|
619
|
+
lightslategrey: '#778899', lightsteelblue: '#b0c4de', lightyellow: '#ffffe0', lime: '#00ff00', limegreen: '#32cd32',
|
|
620
|
+
linen: '#faf0e6', magenta: '#ff00ff', maroon: '#800000', mediumaquamarine: '#66cdaa', mediumblue: '#0000cd',
|
|
621
|
+
mediumorchid: '#ba55d3', mediumpurple: '#9370db', mediumseagreen: '#3cb371', mediumslateblue: '#7b68ee',
|
|
622
|
+
mediumspringgreen: '#00fa9a', mediumturquoise: '#48d1cc', mediumvioletred: '#c71585', midnightblue: '#191970',
|
|
623
|
+
mintcream: '#f5fffa', mistyrose: '#ffe4e1', moccasin: '#ffe4b5', navajowhite: '#ffdead', navy: '#000080',
|
|
624
|
+
oldlace: '#fdf5e6', olive: '#808000', olivedrab: '#6b8e23', orange: '#ffa500', orangered: '#ff4500',
|
|
625
|
+
orchid: '#da70d6', palegoldenrod: '#eee8aa', palegreen: '#98fb98', paleturquoise: '#afeeee', palevioletred: '#db7093',
|
|
626
|
+
papayawhip: '#ffefd5', peachpuff: '#ffdab9', peru: '#cd853f', pink: '#ffc0cb', plum: '#dda0dd',
|
|
627
|
+
powderblue: '#b0e0e6', purple: '#800080', rebeccapurple: '#663399', red: '#ff0000', rosybrown: '#bc8f8f',
|
|
628
|
+
royalblue: '#4169e1', saddlebrown: '#8b4513', salmon: '#fa8072', sandybrown: '#f4a460', seagreen: '#2e8b57',
|
|
629
|
+
seashell: '#fff5ee', sienna: '#a0522d', silver: '#c0c0c0', skyblue: '#87ceeb', slateblue: '#6a5acd',
|
|
630
|
+
slategray: '#708090', slategrey: '#708090', snow: '#fffafa', springgreen: '#00ff7f', steelblue: '#4682b4',
|
|
631
|
+
tan: '#d2b48c', teal: '#008080', thistle: '#d8bfd8', tomato: '#ff6347', turquoise: '#40e0d0',
|
|
632
|
+
violet: '#ee82ee', wheat: '#f5deb3', white: '#ffffff', whitesmoke: '#f5f5f5', yellow: '#ffff00',
|
|
633
|
+
yellowgreen: '#9acd32',
|
|
634
|
+
};
|
|
635
|
+
function parsePaint(value) {
|
|
636
|
+
const v = value.trim();
|
|
637
|
+
const lower = v.toLowerCase();
|
|
638
|
+
if (lower === 'none')
|
|
639
|
+
return 'none';
|
|
640
|
+
if (lower === 'transparent')
|
|
641
|
+
return { hex: '#000000', alpha: 0 };
|
|
642
|
+
if (lower === 'currentcolor')
|
|
643
|
+
return 'currentColor';
|
|
644
|
+
if (v.startsWith('#')) {
|
|
645
|
+
const h = v.slice(1);
|
|
646
|
+
if (!/^[0-9a-fA-F]+$/.test(h))
|
|
647
|
+
unsupported(`colour ${v}`);
|
|
648
|
+
const expand = (c) => c + c;
|
|
649
|
+
if (h.length === 3)
|
|
650
|
+
return { hex: `#${expand(h[0])}${expand(h[1])}${expand(h[2])}`.toLowerCase(), alpha: 1 };
|
|
651
|
+
if (h.length === 4) {
|
|
652
|
+
return {
|
|
653
|
+
hex: `#${expand(h[0])}${expand(h[1])}${expand(h[2])}`.toLowerCase(),
|
|
654
|
+
alpha: parseInt(expand(h[3]), 16) / 255,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
if (h.length === 6)
|
|
658
|
+
return { hex: `#${h}`.toLowerCase(), alpha: 1 };
|
|
659
|
+
if (h.length === 8)
|
|
660
|
+
return { hex: `#${h.slice(0, 6)}`.toLowerCase(), alpha: parseInt(h.slice(6), 16) / 255 };
|
|
661
|
+
return unsupported(`colour ${v}`);
|
|
662
|
+
}
|
|
663
|
+
const fn = /^rgba?\(([^)]*)\)$/i.exec(v);
|
|
664
|
+
if (fn) {
|
|
665
|
+
const parts = fn[1].split(/[\s,/]+/).filter((s) => s !== '');
|
|
666
|
+
if (parts.length < 3 || parts.length > 4)
|
|
667
|
+
unsupported(`colour ${v}`);
|
|
668
|
+
const chan = (s) => {
|
|
669
|
+
const c = s.endsWith('%') ? (Number(s.slice(0, -1)) / 100) * 255 : Number(s);
|
|
670
|
+
if (!Number.isFinite(c))
|
|
671
|
+
unsupported(`colour ${v}`);
|
|
672
|
+
return Math.max(0, Math.min(255, Math.round(c)));
|
|
673
|
+
};
|
|
674
|
+
const hex = `#${[parts[0], parts[1], parts[2]].map((p) => chan(p).toString(16).padStart(2, '0')).join('')}`;
|
|
675
|
+
const alpha = parts.length === 4 ? Math.max(0, Math.min(1, parseNumberOrPercent(parts[3], 1))) : 1;
|
|
676
|
+
return { hex, alpha };
|
|
677
|
+
}
|
|
678
|
+
const named = NAMED_COLORS[lower];
|
|
679
|
+
if (named)
|
|
680
|
+
return { hex: named, alpha: 1 };
|
|
681
|
+
// url(#gradient), var(), hsl(), … — no vector equivalent here.
|
|
682
|
+
return unsupported(`paint ${v}`);
|
|
683
|
+
}
|
|
684
|
+
const ROOT_STYLE = {
|
|
685
|
+
fill: { hex: '#000000', alpha: 1 },
|
|
686
|
+
fillOpacity: 1,
|
|
687
|
+
evenOdd: false,
|
|
688
|
+
stroke: 'none',
|
|
689
|
+
strokeOpacity: 1,
|
|
690
|
+
strokeWidth: 1,
|
|
691
|
+
cap: LineCapStyle.Butt,
|
|
692
|
+
join: LineJoinStyle.Miter,
|
|
693
|
+
miterLimit: 4,
|
|
694
|
+
dash: null,
|
|
695
|
+
dashOffset: 0,
|
|
696
|
+
opacity: 1,
|
|
697
|
+
color: { hex: '#000000', alpha: 1 },
|
|
698
|
+
visible: true,
|
|
699
|
+
clipEvenOdd: false,
|
|
700
|
+
fontFamilies: [],
|
|
701
|
+
fontSize: 16,
|
|
702
|
+
fontWeight: 400,
|
|
703
|
+
italic: false,
|
|
704
|
+
textAnchor: 'start',
|
|
705
|
+
};
|
|
706
|
+
/** Presentation properties honoured from attributes and `style`. */
|
|
707
|
+
const STYLE_PROPS = new Set([
|
|
708
|
+
'fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-opacity', 'stroke-width', 'stroke-linecap',
|
|
709
|
+
'stroke-linejoin', 'stroke-miterlimit', 'stroke-dasharray', 'stroke-dashoffset', 'opacity', 'color',
|
|
710
|
+
'visibility', 'display', 'clip-path', 'clip-rule',
|
|
711
|
+
'font-family', 'font-size', 'font-weight', 'font-style', 'text-anchor',
|
|
712
|
+
// Text properties that change glyph placement: accepted only at their defaults.
|
|
713
|
+
'letter-spacing', 'word-spacing', 'writing-mode', 'text-decoration',
|
|
714
|
+
]);
|
|
715
|
+
/** Properties that may appear in `style` and are harmless to ignore
|
|
716
|
+
* (typography for text we never render, rendering hints, Inkscape extras). */
|
|
717
|
+
const IGNORED_STYLE_PROPS = new Set([
|
|
718
|
+
'font-variant', 'font-stretch', 'font-size-adjust',
|
|
719
|
+
'font-feature-settings', 'font-variant-ligatures', 'font-variant-caps', 'font-variant-numeric',
|
|
720
|
+
'font-variant-east-asian', 'line-height', 'text-align',
|
|
721
|
+
'text-rendering', 'direction', 'unicode-bidi', 'dominant-baseline',
|
|
722
|
+
'baseline-shift', 'kerning', 'glyph-orientation-vertical', 'glyph-orientation-horizontal', 'white-space',
|
|
723
|
+
'inline-size', 'shape-padding', 'shape-margin', 'shape-rendering', 'color-rendering', 'image-rendering',
|
|
724
|
+
'color-interpolation', 'color-interpolation-filters', 'overflow', 'paint-order', 'vector-effect',
|
|
725
|
+
'mix-blend-mode', 'isolation', 'enable-background', 'stop-color', 'stop-opacity', 'cursor', 'pointer-events',
|
|
726
|
+
'transform-origin', 'transform-box',
|
|
727
|
+
]);
|
|
728
|
+
/** Merge presentation attributes and the `style` attribute (which wins) into
|
|
729
|
+
* a flat property map. Unknown properties in `style` bail out unless known
|
|
730
|
+
* to be harmless. */
|
|
731
|
+
function collectProps(attrs) {
|
|
732
|
+
const props = new Map();
|
|
733
|
+
for (const [k, v] of Object.entries(attrs))
|
|
734
|
+
if (STYLE_PROPS.has(k))
|
|
735
|
+
props.set(k, v);
|
|
736
|
+
const style = attrs['style'];
|
|
737
|
+
if (style) {
|
|
738
|
+
for (const decl of style.split(';')) {
|
|
739
|
+
const colon = decl.indexOf(':');
|
|
740
|
+
if (colon < 0) {
|
|
741
|
+
if (decl.trim() !== '')
|
|
742
|
+
unsupported('style syntax');
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const name = decl.slice(0, colon).trim().toLowerCase();
|
|
746
|
+
const value = decl.slice(colon + 1).trim().replace(/\s*!important$/i, '');
|
|
747
|
+
if (name.startsWith('-'))
|
|
748
|
+
continue; // vendor / Inkscape prefixed
|
|
749
|
+
if (STYLE_PROPS.has(name))
|
|
750
|
+
props.set(name, value);
|
|
751
|
+
else if (!IGNORED_STYLE_PROPS.has(name))
|
|
752
|
+
unsupported(`style ${name}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return props;
|
|
756
|
+
}
|
|
757
|
+
function cascade(parent, attrs) {
|
|
758
|
+
const props = collectProps(attrs);
|
|
759
|
+
const next = { ...parent, opacity: parent.opacity };
|
|
760
|
+
const get = (name) => {
|
|
761
|
+
const v = props.get(name);
|
|
762
|
+
if (v === undefined || v.trim().toLowerCase() === 'inherit')
|
|
763
|
+
return undefined;
|
|
764
|
+
return v;
|
|
765
|
+
};
|
|
766
|
+
const color = get('color');
|
|
767
|
+
if (color !== undefined) {
|
|
768
|
+
const c = parsePaint(color);
|
|
769
|
+
if (c === 'none' || c === 'currentColor')
|
|
770
|
+
unsupported('color value');
|
|
771
|
+
next.color = c;
|
|
772
|
+
}
|
|
773
|
+
const fill = get('fill');
|
|
774
|
+
if (fill !== undefined)
|
|
775
|
+
next.fill = parsePaint(fill);
|
|
776
|
+
const fillOpacity = get('fill-opacity');
|
|
777
|
+
if (fillOpacity !== undefined)
|
|
778
|
+
next.fillOpacity = Math.max(0, Math.min(1, parseNumberOrPercent(fillOpacity, 1)));
|
|
779
|
+
const fillRule = get('fill-rule');
|
|
780
|
+
if (fillRule !== undefined)
|
|
781
|
+
next.evenOdd = fillRule.trim() === 'evenodd';
|
|
782
|
+
const clipRule = get('clip-rule');
|
|
783
|
+
if (clipRule !== undefined)
|
|
784
|
+
next.clipEvenOdd = clipRule.trim() === 'evenodd';
|
|
785
|
+
const stroke = get('stroke');
|
|
786
|
+
if (stroke !== undefined)
|
|
787
|
+
next.stroke = parsePaint(stroke);
|
|
788
|
+
const strokeOpacity = get('stroke-opacity');
|
|
789
|
+
if (strokeOpacity !== undefined)
|
|
790
|
+
next.strokeOpacity = Math.max(0, Math.min(1, parseNumberOrPercent(strokeOpacity, 1)));
|
|
791
|
+
const strokeWidth = get('stroke-width');
|
|
792
|
+
if (strokeWidth !== undefined) {
|
|
793
|
+
if (strokeWidth.trim().endsWith('%'))
|
|
794
|
+
unsupported('percent stroke width');
|
|
795
|
+
next.strokeWidth = Math.max(0, parseLength(strokeWidth, 1));
|
|
796
|
+
}
|
|
797
|
+
const cap = get('stroke-linecap');
|
|
798
|
+
if (cap !== undefined) {
|
|
799
|
+
const v = cap.trim();
|
|
800
|
+
next.cap = v === 'round' ? LineCapStyle.Round : v === 'square' ? LineCapStyle.Projecting : LineCapStyle.Butt;
|
|
801
|
+
}
|
|
802
|
+
const join = get('stroke-linejoin');
|
|
803
|
+
if (join !== undefined) {
|
|
804
|
+
const v = join.trim();
|
|
805
|
+
next.join = v === 'round' ? LineJoinStyle.Round : v === 'bevel' ? LineJoinStyle.Bevel : LineJoinStyle.Miter;
|
|
806
|
+
}
|
|
807
|
+
const miter = get('stroke-miterlimit');
|
|
808
|
+
if (miter !== undefined)
|
|
809
|
+
next.miterLimit = Math.max(1, Number(miter) || 4);
|
|
810
|
+
const dash = get('stroke-dasharray');
|
|
811
|
+
if (dash !== undefined) {
|
|
812
|
+
const v = dash.trim().toLowerCase();
|
|
813
|
+
if (v === 'none' || v === '')
|
|
814
|
+
next.dash = null;
|
|
815
|
+
else {
|
|
816
|
+
const parts = v.split(/[\s,]+/).filter((s) => s !== '').map((s) => parseLength(s, 0));
|
|
817
|
+
if (parts.some((p) => p < 0))
|
|
818
|
+
unsupported('negative dash');
|
|
819
|
+
next.dash = parts.every((p) => p === 0) ? null : parts;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
const dashOffset = get('stroke-dashoffset');
|
|
823
|
+
if (dashOffset !== undefined)
|
|
824
|
+
next.dashOffset = parseLength(dashOffset, 0);
|
|
825
|
+
const opacity = get('opacity');
|
|
826
|
+
if (opacity !== undefined)
|
|
827
|
+
next.opacity = parent.opacity * Math.max(0, Math.min(1, parseNumberOrPercent(opacity, 1)));
|
|
828
|
+
const visibility = get('visibility');
|
|
829
|
+
if (visibility !== undefined)
|
|
830
|
+
next.visible = visibility.trim() !== 'hidden' && visibility.trim() !== 'collapse';
|
|
831
|
+
const fontFamily = get('font-family');
|
|
832
|
+
if (fontFamily !== undefined) {
|
|
833
|
+
next.fontFamilies = fontFamily.split(',').map((f) => f.trim().replace(/^['"]|['"]$/g, '')).filter((f) => f !== '');
|
|
834
|
+
}
|
|
835
|
+
const fontSize = get('font-size');
|
|
836
|
+
if (fontSize !== undefined) {
|
|
837
|
+
const v = fontSize.trim();
|
|
838
|
+
if (/[%]$|em$|rem$|ex$|ch$/.test(v) || /^[a-z-]+$/i.test(v))
|
|
839
|
+
unsupported('relative font-size');
|
|
840
|
+
next.fontSize = parseLength(v, 16);
|
|
841
|
+
}
|
|
842
|
+
const fontWeight = get('font-weight');
|
|
843
|
+
if (fontWeight !== undefined) {
|
|
844
|
+
const v = fontWeight.trim().toLowerCase();
|
|
845
|
+
if (v === 'normal')
|
|
846
|
+
next.fontWeight = 400;
|
|
847
|
+
else if (v === 'bold')
|
|
848
|
+
next.fontWeight = 700;
|
|
849
|
+
else if (v === 'bolder')
|
|
850
|
+
next.fontWeight = Math.min(900, parent.fontWeight + 300);
|
|
851
|
+
else if (v === 'lighter')
|
|
852
|
+
next.fontWeight = Math.max(100, parent.fontWeight - 300);
|
|
853
|
+
else if (/^\d+$/.test(v))
|
|
854
|
+
next.fontWeight = Number(v);
|
|
855
|
+
else
|
|
856
|
+
unsupported('font-weight');
|
|
857
|
+
}
|
|
858
|
+
const fontStyle = get('font-style');
|
|
859
|
+
if (fontStyle !== undefined)
|
|
860
|
+
next.italic = fontStyle.trim() !== 'normal';
|
|
861
|
+
const anchor = get('text-anchor');
|
|
862
|
+
if (anchor !== undefined) {
|
|
863
|
+
const v = anchor.trim();
|
|
864
|
+
next.textAnchor = v === 'middle' ? 'middle' : v === 'end' ? 'end' : 'start';
|
|
865
|
+
}
|
|
866
|
+
for (const name of ['letter-spacing', 'word-spacing']) {
|
|
867
|
+
const v = get(name);
|
|
868
|
+
if (v !== undefined && v.trim() !== 'normal' && parseLength(v, 0) !== 0)
|
|
869
|
+
unsupported(name);
|
|
870
|
+
}
|
|
871
|
+
const writingMode = get('writing-mode');
|
|
872
|
+
if (writingMode !== undefined && !/^(horizontal-tb|lr|lr-tb|rl|rl-tb)$/.test(writingMode.trim()))
|
|
873
|
+
unsupported('writing-mode');
|
|
874
|
+
const decoration = get('text-decoration');
|
|
875
|
+
if (decoration !== undefined && decoration.trim() !== 'none')
|
|
876
|
+
unsupported('text-decoration');
|
|
877
|
+
return next;
|
|
878
|
+
}
|
|
879
|
+
// ------------------------------------------------------------ conversion
|
|
880
|
+
/** Elements that never render and can be skipped with their subtrees. */
|
|
881
|
+
const SKIPPED = new Set(['title', 'desc', 'metadata', 'defs', 'clipPath', 'style']);
|
|
882
|
+
/** Structural elements traversed like a group. */
|
|
883
|
+
const GROUPS = new Set(['g', 'a']);
|
|
884
|
+
/** Basic shapes. */
|
|
885
|
+
const SHAPES = new Set(['path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon']);
|
|
886
|
+
/** Walk a child; in probe mode an unsupported subtree is skipped, not fatal. */
|
|
887
|
+
function walkChild(w, el, ctm, style, clips) {
|
|
888
|
+
if (!w.probe) {
|
|
889
|
+
walk(w, el, ctm, style, clips);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
walk(w, el, ctm, style, clips);
|
|
894
|
+
}
|
|
895
|
+
catch (err) {
|
|
896
|
+
if (!(err instanceof Unsupported))
|
|
897
|
+
throw err;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
/** Attributes that reposition glyphs individually; none are supported. */
|
|
901
|
+
const TEXT_UNSUPPORTED_ATTRS = ['rotate', 'textLength', 'lengthAdjust'];
|
|
902
|
+
/** Single numeric value of an `x` / `y` / `dx` / `dy` list, or undefined. */
|
|
903
|
+
function singleCoord(value) {
|
|
904
|
+
if (value === undefined)
|
|
905
|
+
return undefined;
|
|
906
|
+
const parts = value.trim().split(/[\s,]+/).filter((v) => v !== '');
|
|
907
|
+
if (parts.length === 0)
|
|
908
|
+
return undefined;
|
|
909
|
+
if (parts.length > 1)
|
|
910
|
+
unsupported('per-glyph positioning');
|
|
911
|
+
return parseLength(parts[0], 0);
|
|
912
|
+
}
|
|
913
|
+
/** Collapse whitespace the way `xml:space="default"` does for SVG text. */
|
|
914
|
+
function collapseWhitespace(raw) {
|
|
915
|
+
return raw.replace(/[\n\r\t]/g, ' ').replace(/ {2,}/g, ' ');
|
|
916
|
+
}
|
|
917
|
+
/** Lay out a `text` element: walks its runs and `tspan`s, positions each run
|
|
918
|
+
* on the current pen, then applies `text-anchor` per absolutely positioned
|
|
919
|
+
* chunk. Text in a font the resolver cannot provide bails out. */
|
|
920
|
+
function layoutText(w, el, m, style, clips) {
|
|
921
|
+
if (!w.fonts)
|
|
922
|
+
unsupported('text (no font resolver)');
|
|
923
|
+
const resolver = w.fonts;
|
|
924
|
+
const chunks = [];
|
|
925
|
+
let pen = { x: 0, y: 0 };
|
|
926
|
+
let chunk = null;
|
|
927
|
+
let pendingSpace = false;
|
|
928
|
+
let started = false;
|
|
929
|
+
const visit = (node, nm, ns, isRoot) => {
|
|
930
|
+
for (const a of TEXT_UNSUPPORTED_ATTRS)
|
|
931
|
+
if (node.attrs[a] !== undefined)
|
|
932
|
+
unsupported(a);
|
|
933
|
+
if (node.attrs['transform'] !== undefined && !isRoot)
|
|
934
|
+
unsupported('tspan transform');
|
|
935
|
+
const x = singleCoord(node.attrs['x']);
|
|
936
|
+
const y = singleCoord(node.attrs['y']);
|
|
937
|
+
const dx = singleCoord(node.attrs['dx']) ?? 0;
|
|
938
|
+
const dy = singleCoord(node.attrs['dy']) ?? 0;
|
|
939
|
+
if (x !== undefined)
|
|
940
|
+
pen = { ...pen, x };
|
|
941
|
+
if (y !== undefined)
|
|
942
|
+
pen = { ...pen, y };
|
|
943
|
+
pen = { x: pen.x + dx, y: pen.y + dy };
|
|
944
|
+
if (isRoot || x !== undefined || y !== undefined) {
|
|
945
|
+
chunk = { runs: [], anchor: ns.textAnchor, matrix: nm };
|
|
946
|
+
chunks.push(chunk);
|
|
947
|
+
}
|
|
948
|
+
for (const c of node.content) {
|
|
949
|
+
if (typeof c === 'string') {
|
|
950
|
+
let text = collapseWhitespace(c);
|
|
951
|
+
if (text === '')
|
|
952
|
+
continue;
|
|
953
|
+
if (!started) {
|
|
954
|
+
text = text.replace(/^ +/, '');
|
|
955
|
+
if (text === '')
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (pendingSpace && !text.startsWith(' '))
|
|
959
|
+
text = ' ' + text;
|
|
960
|
+
pendingSpace = text.endsWith(' ');
|
|
961
|
+
text = text.replace(/ +$/, '');
|
|
962
|
+
if (text === '')
|
|
963
|
+
continue;
|
|
964
|
+
started = true;
|
|
965
|
+
const font = resolver(ns.fontFamilies, ns.fontWeight, ns.italic);
|
|
966
|
+
if (!font)
|
|
967
|
+
unsupported(`font ${ns.fontFamilies.join(', ') || '(default)'}`);
|
|
968
|
+
const fillPaint = ns.fill === 'none' ? null : ns.fill === 'currentColor' ? ns.color : ns.fill;
|
|
969
|
+
const alpha = fillPaint ? fillPaint.alpha * ns.fillOpacity * ns.opacity : 0;
|
|
970
|
+
if (ns.stroke !== 'none')
|
|
971
|
+
unsupported('stroked text');
|
|
972
|
+
const width = font.widthOf(text, ns.fontSize);
|
|
973
|
+
if (fillPaint && alpha > 0 && ns.visible) {
|
|
974
|
+
chunk.runs.push({
|
|
975
|
+
text, font: font, size: ns.fontSize, x: pen.x, y: pen.y, matrix: nm,
|
|
976
|
+
fill: { hex: fillPaint.hex, alpha },
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
pen = { x: pen.x + width, y: pen.y };
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
const { local, foreign } = localName(c.name);
|
|
983
|
+
if (foreign)
|
|
984
|
+
continue;
|
|
985
|
+
if (local !== 'tspan')
|
|
986
|
+
unsupported(`text child ${local}`);
|
|
987
|
+
if (c.attrs['display']?.trim() === 'none')
|
|
988
|
+
continue;
|
|
989
|
+
const cs = cascade(ns, c.attrs);
|
|
990
|
+
visit(c, nm, cs, false);
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
visit(el, m, style, true);
|
|
994
|
+
const runs = [];
|
|
995
|
+
for (const ch of chunks) {
|
|
996
|
+
if (ch.runs.length === 0)
|
|
997
|
+
continue;
|
|
998
|
+
let shift = 0;
|
|
999
|
+
if (ch.anchor !== 'start') {
|
|
1000
|
+
const first = ch.runs[0];
|
|
1001
|
+
const last = ch.runs[ch.runs.length - 1];
|
|
1002
|
+
const width = last.x + last.font.widthOf(last.text, last.size) - first.x;
|
|
1003
|
+
shift = ch.anchor === 'middle' ? -width / 2 : -width;
|
|
1004
|
+
}
|
|
1005
|
+
for (const r of ch.runs)
|
|
1006
|
+
runs.push(shift === 0 ? r : { ...r, x: r.x + shift });
|
|
1007
|
+
}
|
|
1008
|
+
if (runs.length > 0)
|
|
1009
|
+
w.shapes.push({ kind: 'text', runs, clips });
|
|
1010
|
+
}
|
|
1011
|
+
function shapeSegs(el) {
|
|
1012
|
+
const a = el.attrs;
|
|
1013
|
+
const len = (name, fallback = 0) => parseLength(a[name], fallback);
|
|
1014
|
+
switch (el.name) {
|
|
1015
|
+
case 'path': {
|
|
1016
|
+
const d = a['d'];
|
|
1017
|
+
if (!d || d.trim() === '')
|
|
1018
|
+
return [];
|
|
1019
|
+
return parsePathData(d);
|
|
1020
|
+
}
|
|
1021
|
+
case 'rect': {
|
|
1022
|
+
const w = len('width');
|
|
1023
|
+
const h = len('height');
|
|
1024
|
+
if (w <= 0 || h <= 0)
|
|
1025
|
+
return [];
|
|
1026
|
+
let rx = a['rx'] !== undefined && a['rx'] !== 'auto' ? len('rx') : -1;
|
|
1027
|
+
let ry = a['ry'] !== undefined && a['ry'] !== 'auto' ? len('ry') : -1;
|
|
1028
|
+
if (rx < 0 && ry < 0)
|
|
1029
|
+
rx = ry = 0;
|
|
1030
|
+
else if (rx < 0)
|
|
1031
|
+
rx = ry;
|
|
1032
|
+
else if (ry < 0)
|
|
1033
|
+
ry = rx;
|
|
1034
|
+
return rectSegs(len('x'), len('y'), w, h, rx, ry);
|
|
1035
|
+
}
|
|
1036
|
+
case 'circle': {
|
|
1037
|
+
const r = len('r');
|
|
1038
|
+
return r > 0 ? ellipseSegs(len('cx'), len('cy'), r, r) : [];
|
|
1039
|
+
}
|
|
1040
|
+
case 'ellipse': {
|
|
1041
|
+
const rx = a['rx'] === 'auto' ? len('ry') : len('rx');
|
|
1042
|
+
const ry = a['ry'] === 'auto' ? len('rx') : len('ry');
|
|
1043
|
+
return rx > 0 && ry > 0 ? ellipseSegs(len('cx'), len('cy'), rx, ry) : [];
|
|
1044
|
+
}
|
|
1045
|
+
case 'line':
|
|
1046
|
+
return [['M', len('x1'), len('y1')], ['L', len('x2'), len('y2')]];
|
|
1047
|
+
case 'polyline':
|
|
1048
|
+
return a['points'] ? pointsSegs(a['points'], false) : [];
|
|
1049
|
+
case 'polygon':
|
|
1050
|
+
return a['points'] ? pointsSegs(a['points'], true) : [];
|
|
1051
|
+
default:
|
|
1052
|
+
return unsupported(el.name);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function hrefOf(el) {
|
|
1056
|
+
return el.attrs['href'] ?? el.attrs['xlink:href'];
|
|
1057
|
+
}
|
|
1058
|
+
function resolveClip(w, attrs, ctm, style) {
|
|
1059
|
+
const raw = attrs['clip-path'];
|
|
1060
|
+
if (raw === undefined)
|
|
1061
|
+
return null;
|
|
1062
|
+
const v = raw.trim();
|
|
1063
|
+
if (v === 'none' || v === '')
|
|
1064
|
+
return null;
|
|
1065
|
+
const m = /^url\(\s*['"]?#([^'")\s]+)['"]?\s*\)$/.exec(v);
|
|
1066
|
+
if (!m)
|
|
1067
|
+
return unsupported(`clip-path ${v}`);
|
|
1068
|
+
const target = w.byId.get(m[1]);
|
|
1069
|
+
if (!target || target.name !== 'clipPath')
|
|
1070
|
+
return unsupported('clip-path target');
|
|
1071
|
+
const units = target.attrs['clipPathUnits'];
|
|
1072
|
+
if (units !== undefined && units !== 'userSpaceOnUse')
|
|
1073
|
+
unsupported('clipPathUnits');
|
|
1074
|
+
if (target.attrs['clip-path'] !== undefined)
|
|
1075
|
+
unsupported('nested clip-path');
|
|
1076
|
+
const clipCtm = mul(ctm, parseTransform(target.attrs['transform']));
|
|
1077
|
+
const clipStyle = cascade(style, target.attrs);
|
|
1078
|
+
const segs = [];
|
|
1079
|
+
let evenOdd = clipStyle.clipEvenOdd;
|
|
1080
|
+
const visit = (el, m2, s, depth) => {
|
|
1081
|
+
if (depth > 16)
|
|
1082
|
+
unsupported('clip recursion');
|
|
1083
|
+
const { local, foreign } = localName(el.name);
|
|
1084
|
+
if (foreign || local === 'title' || local === 'desc' || local === 'metadata')
|
|
1085
|
+
return;
|
|
1086
|
+
const s2 = cascade(s, el.attrs);
|
|
1087
|
+
if (el.attrs['display']?.trim() === 'none')
|
|
1088
|
+
return;
|
|
1089
|
+
const m3 = mul(m2, parseTransform(el.attrs['transform']));
|
|
1090
|
+
if (SHAPES.has(local)) {
|
|
1091
|
+
evenOdd = s2.clipEvenOdd;
|
|
1092
|
+
segs.push(...transformSegs(shapeSegs({ ...el, name: local }), m3));
|
|
1093
|
+
}
|
|
1094
|
+
else if (local === 'use') {
|
|
1095
|
+
const href = hrefOf(el);
|
|
1096
|
+
const id = href?.startsWith('#') ? href.slice(1) : undefined;
|
|
1097
|
+
const ref = id ? w.byId.get(id) : undefined;
|
|
1098
|
+
if (!ref)
|
|
1099
|
+
unsupported('use target');
|
|
1100
|
+
const m4 = mul(m3, [1, 0, 0, 1, parseLength(el.attrs['x'], 0), parseLength(el.attrs['y'], 0)]);
|
|
1101
|
+
visit(ref, m4, s2, depth + 1);
|
|
1102
|
+
}
|
|
1103
|
+
else {
|
|
1104
|
+
unsupported(`clipPath child ${local}`);
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
for (const child of target.children)
|
|
1108
|
+
visit(child, clipCtm, clipStyle, 0);
|
|
1109
|
+
return { segs, evenOdd };
|
|
1110
|
+
}
|
|
1111
|
+
function walk(w, el, ctm, parentStyle, clips) {
|
|
1112
|
+
if (w.depth > 64)
|
|
1113
|
+
unsupported('nesting depth');
|
|
1114
|
+
const { local, foreign } = localName(el.name);
|
|
1115
|
+
if (foreign)
|
|
1116
|
+
return; // inkscape:*, sodipodi:*, rdf:* …
|
|
1117
|
+
if (SKIPPED.has(local)) {
|
|
1118
|
+
if (local === 'style' && el.hasText)
|
|
1119
|
+
unsupported('style sheet');
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const attrs = el.attrs;
|
|
1123
|
+
if (attrs['display']?.trim() === 'none')
|
|
1124
|
+
return;
|
|
1125
|
+
if (!w.probe) {
|
|
1126
|
+
// Painting-only concerns: a probe still wants the text underneath.
|
|
1127
|
+
if (attrs['mask'] !== undefined && attrs['mask'].trim() !== 'none')
|
|
1128
|
+
unsupported('mask');
|
|
1129
|
+
if (attrs['filter'] !== undefined && attrs['filter'].trim() !== 'none')
|
|
1130
|
+
unsupported('filter');
|
|
1131
|
+
if (attrs['marker-start'] || attrs['marker-mid'] || attrs['marker-end'])
|
|
1132
|
+
unsupported('marker');
|
|
1133
|
+
}
|
|
1134
|
+
const style = cascade(parentStyle, attrs);
|
|
1135
|
+
const m = mul(ctm, parseTransform(attrs['transform']));
|
|
1136
|
+
const clipHere = w.probe ? null : resolveClip(w, attrs, m, style);
|
|
1137
|
+
const nextClips = clipHere ? [...clips, clipHere] : clips;
|
|
1138
|
+
if (GROUPS.has(local) || (local === 'svg' && w.depth === 0)) {
|
|
1139
|
+
w.depth++;
|
|
1140
|
+
for (const child of el.children)
|
|
1141
|
+
walkChild(w, child, m, style, nextClips);
|
|
1142
|
+
w.depth--;
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
if (local === 'use') {
|
|
1146
|
+
const href = hrefOf(el);
|
|
1147
|
+
const id = href?.startsWith('#') ? href.slice(1) : undefined;
|
|
1148
|
+
const ref = id ? w.byId.get(id) : undefined;
|
|
1149
|
+
if (!ref)
|
|
1150
|
+
return unsupported('use target');
|
|
1151
|
+
const refLocal = localName(ref.name).local;
|
|
1152
|
+
if (refLocal === 'symbol' || refLocal === 'svg')
|
|
1153
|
+
unsupported('use of symbol');
|
|
1154
|
+
const m2 = mul(m, [1, 0, 0, 1, parseLength(attrs['x'], 0), parseLength(attrs['y'], 0)]);
|
|
1155
|
+
w.depth++;
|
|
1156
|
+
walk(w, ref, m2, style, nextClips);
|
|
1157
|
+
w.depth--;
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
if (SHAPES.has(local)) {
|
|
1161
|
+
if (!style.visible)
|
|
1162
|
+
return;
|
|
1163
|
+
const segs = shapeSegs({ ...el, name: local });
|
|
1164
|
+
if (segs.length === 0)
|
|
1165
|
+
return;
|
|
1166
|
+
const resolve = (p) => {
|
|
1167
|
+
if (p === 'none')
|
|
1168
|
+
return null;
|
|
1169
|
+
return p === 'currentColor' ? style.color : p;
|
|
1170
|
+
};
|
|
1171
|
+
const fillPaint = resolve(style.fill);
|
|
1172
|
+
const strokePaint = resolve(style.stroke);
|
|
1173
|
+
const fillAlpha = fillPaint ? fillPaint.alpha * style.fillOpacity * style.opacity : 0;
|
|
1174
|
+
const strokeAlpha = strokePaint ? strokePaint.alpha * style.strokeOpacity * style.opacity : 0;
|
|
1175
|
+
const scale = lengthScale(m);
|
|
1176
|
+
const strokeWidth = style.strokeWidth * scale;
|
|
1177
|
+
const fillOut = fillPaint && fillAlpha > 0 ? { hex: fillPaint.hex, alpha: fillAlpha } : null;
|
|
1178
|
+
const strokeOut = strokePaint && strokeAlpha > 0 && strokeWidth > 0
|
|
1179
|
+
? {
|
|
1180
|
+
hex: strokePaint.hex,
|
|
1181
|
+
alpha: strokeAlpha,
|
|
1182
|
+
width: strokeWidth,
|
|
1183
|
+
cap: style.cap,
|
|
1184
|
+
join: style.join,
|
|
1185
|
+
miterLimit: style.miterLimit,
|
|
1186
|
+
dash: style.dash ? style.dash.map((d) => d * scale) : null,
|
|
1187
|
+
dashOffset: style.dashOffset * scale,
|
|
1188
|
+
}
|
|
1189
|
+
: null;
|
|
1190
|
+
if (!fillOut && !strokeOut)
|
|
1191
|
+
return;
|
|
1192
|
+
w.shapes.push({ kind: 'path', segs: transformSegs(segs, m), fill: fillOut, evenOdd: style.evenOdd, stroke: strokeOut, clips: nextClips });
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
if (local === 'text') {
|
|
1196
|
+
layoutText(w, el, m, style, nextClips);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
if (local === 'image') {
|
|
1200
|
+
// A data-URI PNG / JPEG stretched into its box (`preserveAspectRatio`
|
|
1201
|
+
// is not honoured: the box is taken as the picture's extent).
|
|
1202
|
+
if (!style.visible)
|
|
1203
|
+
return;
|
|
1204
|
+
const href = hrefOf(el) ?? '';
|
|
1205
|
+
const dm = /^data:image\/(png|jpeg|jpg);base64,([\s\S]*)$/i.exec(href.trim());
|
|
1206
|
+
if (!dm)
|
|
1207
|
+
return unsupported('image source');
|
|
1208
|
+
const width = parseLength(attrs['width'], 0);
|
|
1209
|
+
const height = parseLength(attrs['height'], 0);
|
|
1210
|
+
if (width <= 0 || height <= 0)
|
|
1211
|
+
return;
|
|
1212
|
+
if (w.probe)
|
|
1213
|
+
return;
|
|
1214
|
+
let data;
|
|
1215
|
+
try {
|
|
1216
|
+
const bin = atob(dm[2].replace(/\s+/g, ''));
|
|
1217
|
+
data = new Uint8Array(bin.length);
|
|
1218
|
+
for (let i = 0; i < bin.length; i++)
|
|
1219
|
+
data[i] = bin.charCodeAt(i);
|
|
1220
|
+
}
|
|
1221
|
+
catch {
|
|
1222
|
+
return unsupported('image data');
|
|
1223
|
+
}
|
|
1224
|
+
w.shapes.push({
|
|
1225
|
+
kind: 'image',
|
|
1226
|
+
data,
|
|
1227
|
+
format: dm[1].toLowerCase() === 'png' ? 'png' : 'jpeg',
|
|
1228
|
+
x: parseLength(attrs['x'], 0),
|
|
1229
|
+
y: parseLength(attrs['y'], 0),
|
|
1230
|
+
width,
|
|
1231
|
+
height,
|
|
1232
|
+
matrix: m,
|
|
1233
|
+
clips: nextClips,
|
|
1234
|
+
});
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
// tspan outside text, svg (nested), symbol, switch, foreignObject,
|
|
1238
|
+
// linearGradient outside defs, pattern, mask, filter, marker, …
|
|
1239
|
+
unsupported(local);
|
|
1240
|
+
}
|
|
1241
|
+
function indexIds(el, byId) {
|
|
1242
|
+
const id = el.attrs['id'];
|
|
1243
|
+
if (id && !byId.has(id))
|
|
1244
|
+
byId.set(id, el);
|
|
1245
|
+
for (const c of el.children)
|
|
1246
|
+
indexIds(c, byId);
|
|
1247
|
+
}
|
|
1248
|
+
function parseAspect(value) {
|
|
1249
|
+
const v = (value ?? 'xMidYMid meet').trim().split(/\s+/);
|
|
1250
|
+
if (v[0] === 'none')
|
|
1251
|
+
return null;
|
|
1252
|
+
const m = /^x(Min|Mid|Max)Y(Min|Mid|Max)$/.exec(v[0] ?? '');
|
|
1253
|
+
if (!m)
|
|
1254
|
+
return unsupported('preserveAspectRatio');
|
|
1255
|
+
const pos = (s) => (s === 'Min' ? 0 : s === 'Mid' ? 0.5 : 1);
|
|
1256
|
+
return { xAlign: pos(m[1]), yAlign: pos(m[2]), slice: v[1] === 'slice' };
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Convert SVG markup to a {@link VectorDrawing}, or `null` when the document
|
|
1260
|
+
* uses anything outside the supported subset (see the module comment). Never
|
|
1261
|
+
* throws.
|
|
1262
|
+
*/
|
|
1263
|
+
export function svgToVectorDrawing(svgText, options = {}) {
|
|
1264
|
+
try {
|
|
1265
|
+
const doc = parseXml(svgText);
|
|
1266
|
+
if (!doc)
|
|
1267
|
+
return null;
|
|
1268
|
+
const root = doc.children.find((c) => localName(c.name).local === 'svg' && !localName(c.name).foreign);
|
|
1269
|
+
if (!root || doc.children.some((c) => localName(c.name).local !== 'svg'))
|
|
1270
|
+
return null;
|
|
1271
|
+
const byId = new Map();
|
|
1272
|
+
indexIds(root, byId);
|
|
1273
|
+
let viewBox;
|
|
1274
|
+
const vb = root.attrs['viewBox'];
|
|
1275
|
+
if (vb) {
|
|
1276
|
+
const parts = vb.trim().split(/[\s,]+/).map(Number);
|
|
1277
|
+
if (parts.length !== 4 || parts.some((p) => !Number.isFinite(p)) || parts[2] <= 0 || parts[3] <= 0)
|
|
1278
|
+
return null;
|
|
1279
|
+
viewBox = [parts[0], parts[1], parts[2], parts[3]];
|
|
1280
|
+
}
|
|
1281
|
+
else {
|
|
1282
|
+
const w = root.attrs['width'];
|
|
1283
|
+
const h = root.attrs['height'];
|
|
1284
|
+
if (w?.trim().endsWith('%') || h?.trim().endsWith('%'))
|
|
1285
|
+
return null;
|
|
1286
|
+
const width = parseLength(w, 300);
|
|
1287
|
+
const height = parseLength(h, 150);
|
|
1288
|
+
if (width <= 0 || height <= 0)
|
|
1289
|
+
return null;
|
|
1290
|
+
viewBox = [0, 0, width, height];
|
|
1291
|
+
}
|
|
1292
|
+
const aspect = parseAspect(root.attrs['preserveAspectRatio']);
|
|
1293
|
+
const walker = { byId, shapes: [], depth: 0, fonts: options.fonts, probe: options.probe === true };
|
|
1294
|
+
// The root's own transform / clip / style apply like a group's.
|
|
1295
|
+
walk(walker, root, IDENTITY, ROOT_STYLE, []);
|
|
1296
|
+
return { viewBox, aspect, shapes: walker.shapes };
|
|
1297
|
+
}
|
|
1298
|
+
catch (err) {
|
|
1299
|
+
if (typeof process !== 'undefined' && process.env?.POSTEXT_SVG_DEBUG)
|
|
1300
|
+
console.warn('svgToVectorDrawing:', err.message);
|
|
1301
|
+
if (err instanceof Unsupported)
|
|
1302
|
+
return null;
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
// ------------------------------------------------------------ emission
|
|
1307
|
+
function round(v) {
|
|
1308
|
+
return Math.round(v * 10000) / 10000;
|
|
1309
|
+
}
|
|
1310
|
+
function pathOps(segs) {
|
|
1311
|
+
const ops = [];
|
|
1312
|
+
for (const s of segs) {
|
|
1313
|
+
switch (s[0]) {
|
|
1314
|
+
case 'M':
|
|
1315
|
+
ops.push(moveTo(round(s[1]), round(s[2])));
|
|
1316
|
+
break;
|
|
1317
|
+
case 'L':
|
|
1318
|
+
ops.push(lineTo(round(s[1]), round(s[2])));
|
|
1319
|
+
break;
|
|
1320
|
+
case 'C':
|
|
1321
|
+
ops.push(appendBezierCurve(round(s[1]), round(s[2]), round(s[3]), round(s[4]), round(s[5]), round(s[6])));
|
|
1322
|
+
break;
|
|
1323
|
+
case 'Z':
|
|
1324
|
+
ops.push(closePath());
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
return ops;
|
|
1329
|
+
}
|
|
1330
|
+
/** ExtGState per distinct (fill alpha, stroke alpha) pair, per page. */
|
|
1331
|
+
const alphaStates = new WeakMap();
|
|
1332
|
+
/** Font resource name per (page, font). */
|
|
1333
|
+
const fontNames = new WeakMap();
|
|
1334
|
+
function fontName(ctx, font) {
|
|
1335
|
+
let names = fontNames.get(ctx.page);
|
|
1336
|
+
if (!names) {
|
|
1337
|
+
names = new Map();
|
|
1338
|
+
fontNames.set(ctx.page, names);
|
|
1339
|
+
}
|
|
1340
|
+
let name = names.get(font);
|
|
1341
|
+
if (!name) {
|
|
1342
|
+
name = ctx.page.node.newFontDictionary(font.name, font.ref);
|
|
1343
|
+
names.set(font, name);
|
|
1344
|
+
}
|
|
1345
|
+
return name;
|
|
1346
|
+
}
|
|
1347
|
+
function alphaState(ctx, ca, CA) {
|
|
1348
|
+
if (ca >= 1 && CA >= 1)
|
|
1349
|
+
return null;
|
|
1350
|
+
let states = alphaStates.get(ctx.page);
|
|
1351
|
+
if (!states) {
|
|
1352
|
+
states = new Map();
|
|
1353
|
+
alphaStates.set(ctx.page, states);
|
|
1354
|
+
}
|
|
1355
|
+
const key = `${round(ca)}|${round(CA)}`;
|
|
1356
|
+
let name = states.get(key);
|
|
1357
|
+
if (!name) {
|
|
1358
|
+
const dict = ctx.page.doc.context.obj({ Type: 'ExtGState', ca: round(ca), CA: round(CA) });
|
|
1359
|
+
name = ctx.page.node.newExtGState('GSa', dict);
|
|
1360
|
+
states.set(key, name);
|
|
1361
|
+
}
|
|
1362
|
+
return setGraphicsState(name);
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Paint a {@link VectorDrawing} into the page, fitted to the given box in the
|
|
1366
|
+
* document's pixel space (top-down, like every other `PageCtx` primitive).
|
|
1367
|
+
*/
|
|
1368
|
+
export function drawVectorDrawing(ctx, drawing, xPx, yPx, wPx, hPx) {
|
|
1369
|
+
const { scale, pageHeightPt } = ctx;
|
|
1370
|
+
const x0 = xPx * scale;
|
|
1371
|
+
const yTop = pageHeightPt - yPx * scale;
|
|
1372
|
+
const w = wPx * scale;
|
|
1373
|
+
const h = hPx * scale;
|
|
1374
|
+
if (w <= 0 || h <= 0)
|
|
1375
|
+
return;
|
|
1376
|
+
const [vx, vy, vw, vh] = drawing.viewBox;
|
|
1377
|
+
let sx = w / vw;
|
|
1378
|
+
let sy = h / vh;
|
|
1379
|
+
let ox = 0;
|
|
1380
|
+
let oy = 0;
|
|
1381
|
+
if (drawing.aspect) {
|
|
1382
|
+
const s = drawing.aspect.slice ? Math.max(sx, sy) : Math.min(sx, sy);
|
|
1383
|
+
ox = (w - vw * s) * drawing.aspect.xAlign;
|
|
1384
|
+
oy = (h - vh * s) * drawing.aspect.yAlign;
|
|
1385
|
+
sx = sy = s;
|
|
1386
|
+
}
|
|
1387
|
+
const ops = [pushGraphicsState()];
|
|
1388
|
+
// Clip to the viewport (overflow: hidden on the root svg).
|
|
1389
|
+
ops.push(rectangle(round(x0), round(yTop - h), round(w), round(h)), clip(), endPath());
|
|
1390
|
+
// SVG user space → PDF points: scale, flip y, place the viewBox origin.
|
|
1391
|
+
ops.push(concatTransformationMatrix(round(sx), 0, 0, round(-sy), round(x0 + ox - vx * sx), round(yTop - oy + vy * sy)));
|
|
1392
|
+
for (const shape of drawing.shapes) {
|
|
1393
|
+
ops.push(pushGraphicsState());
|
|
1394
|
+
for (const c of shape.clips) {
|
|
1395
|
+
ops.push(...pathOps(c.segs), c.evenOdd ? clipEvenOdd() : clip(), endPath());
|
|
1396
|
+
}
|
|
1397
|
+
if (shape.kind === 'image') {
|
|
1398
|
+
if (shape.pdfImage) {
|
|
1399
|
+
// The unit square maps onto the picture's box; the image's rows run
|
|
1400
|
+
// bottom-up in PDF, so the box is flipped inside the y-down root
|
|
1401
|
+
// space to keep the picture upright.
|
|
1402
|
+
const [a, b, c, d, e, f] = mul(shape.matrix, [shape.width, 0, 0, -shape.height, shape.x, shape.y + shape.height]);
|
|
1403
|
+
const name = ctx.page.node.newXObject('Image', shape.pdfImage.ref);
|
|
1404
|
+
ops.push(concatTransformationMatrix(round(a), round(b), round(c), round(d), round(e), round(f)), drawObject(name));
|
|
1405
|
+
}
|
|
1406
|
+
ops.push(popGraphicsState());
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
if (shape.kind === 'text') {
|
|
1410
|
+
for (const run of shape.runs) {
|
|
1411
|
+
const font = run.font.pdfFont;
|
|
1412
|
+
if (!font)
|
|
1413
|
+
continue;
|
|
1414
|
+
const gs = alphaState(ctx, run.fill.alpha, 1);
|
|
1415
|
+
ops.push(pushGraphicsState());
|
|
1416
|
+
if (gs)
|
|
1417
|
+
ops.push(gs);
|
|
1418
|
+
// Text matrix: local → root, then undo the root y-flip so glyphs
|
|
1419
|
+
// stand upright, with the pen at the run's baseline start.
|
|
1420
|
+
const [a, b, c, d, e, f] = mul(run.matrix, [1, 0, 0, -1, run.x, run.y]);
|
|
1421
|
+
ops.push(setFillingColor(colorFromHex(run.fill.hex, ctx.colorSpace)), beginText(), setFontAndSize(fontName(ctx, font), round(run.size)), setTextMatrix(round(a), round(b), round(c), round(d), round(e), round(f)), showText(font.encodeText(run.text)), endText(), popGraphicsState());
|
|
1422
|
+
}
|
|
1423
|
+
ops.push(popGraphicsState());
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
const gs = alphaState(ctx, shape.fill?.alpha ?? 1, shape.stroke?.alpha ?? 1);
|
|
1427
|
+
if (gs)
|
|
1428
|
+
ops.push(gs);
|
|
1429
|
+
if (shape.fill)
|
|
1430
|
+
ops.push(setFillingColor(colorFromHex(shape.fill.hex, ctx.colorSpace)));
|
|
1431
|
+
if (shape.stroke) {
|
|
1432
|
+
const st = shape.stroke;
|
|
1433
|
+
ops.push(setStrokingColor(colorFromHex(st.hex, ctx.colorSpace)), setLineWidth(round(st.width)), setLineCap(st.cap), setLineJoin(st.join), PDFOperator.of(PDFOperatorNames.SetLineMiterLimit, [ctx.page.doc.context.obj(round(st.miterLimit))]));
|
|
1434
|
+
if (st.dash)
|
|
1435
|
+
ops.push(setDashPattern(st.dash.map(round), round(st.dashOffset)));
|
|
1436
|
+
}
|
|
1437
|
+
ops.push(...pathOps(shape.segs));
|
|
1438
|
+
if (shape.fill && shape.stroke) {
|
|
1439
|
+
ops.push(shape.evenOdd ? PDFOperator.of(PDFOperatorNames.FillEvenOddAndStroke) : fillAndStroke());
|
|
1440
|
+
}
|
|
1441
|
+
else if (shape.fill) {
|
|
1442
|
+
ops.push(shape.evenOdd ? PDFOperator.of(PDFOperatorNames.FillEvenOdd) : fill());
|
|
1443
|
+
}
|
|
1444
|
+
else {
|
|
1445
|
+
ops.push(stroke());
|
|
1446
|
+
}
|
|
1447
|
+
ops.push(popGraphicsState());
|
|
1448
|
+
}
|
|
1449
|
+
ops.push(popGraphicsState());
|
|
1450
|
+
// In slices: a drawing of many thousands of paths would overflow the
|
|
1451
|
+
// call stack as one spread argument list.
|
|
1452
|
+
for (let i = 0; i < ops.length; i += 4000)
|
|
1453
|
+
ctx.page.pushOperators(...ops.slice(i, i + 4000));
|
|
1454
|
+
}
|
|
1455
|
+
//# sourceMappingURL=svgVector.js.map
|