@weasel-js/svg 1.5.0 → 1.5.2
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 +43 -0
- package/dist/index.d.ts +59 -12
- package/dist/index.js +543 -110
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath,
|
|
2
|
-
import { invert } from '@weasel-js/geom';
|
|
1
|
+
import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath, resolveScreenLength, SCRIPT_METRICS, dwarn, getMarker, registerMarker, createInsertOp, PathBuilder, parseColor, rgbaToHex, resolveTextStyle, layoutRuns, resolveRuns, resolveAlign, PATH_C, PATH_Q, solid, fillToBoundsFrame, getPaintKind } from '@weasel-js/core';
|
|
2
|
+
import { identity, multiply, invert } from '@weasel-js/geom';
|
|
3
3
|
import { tileGeometry } from '@weasel-js/core/patterns-builtin';
|
|
4
4
|
|
|
5
5
|
// src/parse.ts
|
|
6
|
-
|
|
7
|
-
// src/types.ts
|
|
8
|
-
var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
|
|
9
|
-
function multiply(a, b) {
|
|
10
|
-
return [
|
|
11
|
-
a[0] * b[0] + a[2] * b[1],
|
|
12
|
-
a[1] * b[0] + a[3] * b[1],
|
|
13
|
-
a[0] * b[2] + a[2] * b[3],
|
|
14
|
-
a[1] * b[2] + a[3] * b[3],
|
|
15
|
-
a[0] * b[4] + a[2] * b[5] + a[4],
|
|
16
|
-
a[1] * b[4] + a[3] * b[5] + a[5]
|
|
17
|
-
];
|
|
18
|
-
}
|
|
6
|
+
var IDENTITY_MATRIX = identity();
|
|
19
7
|
function applyMatrix(m, x, y) {
|
|
20
8
|
return { x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] };
|
|
21
9
|
}
|
|
@@ -77,10 +65,9 @@ function parseTransform(attr, onWarn) {
|
|
|
77
65
|
return result;
|
|
78
66
|
}
|
|
79
67
|
function rebaseTransform(ctm, local) {
|
|
80
|
-
const inv = invert(
|
|
68
|
+
const inv = invert(ctm);
|
|
81
69
|
if (!inv) return null;
|
|
82
|
-
|
|
83
|
-
return multiply(multiply(ctm, local), [a, b, c, d, e, f]);
|
|
70
|
+
return multiply(multiply(ctm, local), inv);
|
|
84
71
|
}
|
|
85
72
|
function decomposeRotation(m, cx, cy, eps = 1e-4) {
|
|
86
73
|
const [a, b, c, d, e, f] = m;
|
|
@@ -242,8 +229,18 @@ function parsePaintAttr(raw) {
|
|
|
242
229
|
if (s === "currentColor" || s === "currentcolor") {
|
|
243
230
|
return { kind: "solid", color: "#000000", alpha: 1 };
|
|
244
231
|
}
|
|
245
|
-
const urlMatch = /^url\(\s*#([^)\s]+)\s*\)
|
|
246
|
-
if (urlMatch)
|
|
232
|
+
const urlMatch = /^url\(\s*#([^)\s]+)\s*\)(.*)$/.exec(s);
|
|
233
|
+
if (urlMatch) {
|
|
234
|
+
const out = { kind: "ref", id: urlMatch[1] };
|
|
235
|
+
const rest = urlMatch[2].trim();
|
|
236
|
+
if (rest !== "" && rest !== "none") {
|
|
237
|
+
const fallback = parsePaintAttr(rest);
|
|
238
|
+
if (fallback?.kind === "solid") {
|
|
239
|
+
out.fallback = { color: fallback.color, alpha: fallback.alpha };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
247
244
|
try {
|
|
248
245
|
const [r, g, b, a] = parseColor(s);
|
|
249
246
|
return { kind: "solid", color: rgbaToHex([r, g, b]), alpha: a };
|
|
@@ -387,13 +384,45 @@ function serializePathD(path) {
|
|
|
387
384
|
}
|
|
388
385
|
|
|
389
386
|
// src/gradients.ts
|
|
390
|
-
var
|
|
387
|
+
var WEASEL_NS = "urn:weasel-js:svg";
|
|
388
|
+
var WEASEL_NS_PREFIX = "wzl";
|
|
389
|
+
function hasNativeSvgForm(paint) {
|
|
390
|
+
const kind = paint.fill ?? "solid";
|
|
391
|
+
return kind === "solid" || kind === "linear-gradient" || kind === "radial-gradient" || kind === "pattern";
|
|
392
|
+
}
|
|
393
|
+
var INTERPOLATE_SPACES = /* @__PURE__ */ new Set(["rgb", "oklab", "oklch"]);
|
|
394
|
+
function interpolateAttr(space) {
|
|
395
|
+
if (!space || space === "rgb") return "";
|
|
396
|
+
return ` ${WEASEL_NS_PREFIX}:interpolate="${space}"`;
|
|
397
|
+
}
|
|
398
|
+
function readInterpolate(el, elements) {
|
|
399
|
+
const raw = inheritedAttr(el, elements, `${WEASEL_NS_PREFIX}:interpolate`) ?? el.getAttributeNS(WEASEL_NS, "interpolate");
|
|
400
|
+
if (raw === "oklab" || raw === "oklch") return raw;
|
|
401
|
+
return void 0;
|
|
402
|
+
}
|
|
403
|
+
function usesInterpolateAttr(paint) {
|
|
404
|
+
const space = paint.interpolate;
|
|
405
|
+
return space != null && space !== "rgb" && INTERPOLATE_SPACES.has(space);
|
|
406
|
+
}
|
|
407
|
+
function localName(tag) {
|
|
408
|
+
const i = tag.indexOf(":");
|
|
409
|
+
return (i < 0 ? tag : tag.slice(i + 1)).toLowerCase();
|
|
410
|
+
}
|
|
411
|
+
var GRADIENT_TAGS = /* @__PURE__ */ new Set([
|
|
412
|
+
"lineargradient",
|
|
413
|
+
"radialgradient",
|
|
414
|
+
"conicgradient",
|
|
415
|
+
`${WEASEL_NS_PREFIX}:conicgradient`,
|
|
416
|
+
"meshgradient",
|
|
417
|
+
`${WEASEL_NS_PREFIX}:meshgradient`
|
|
418
|
+
]);
|
|
391
419
|
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
392
420
|
function collectGradients(svg, onWarn) {
|
|
393
421
|
const out = /* @__PURE__ */ new Map();
|
|
394
422
|
const elements = collectElementsByTag(svg, GRADIENT_TAGS);
|
|
395
423
|
for (const [id, el] of elements) {
|
|
396
|
-
const
|
|
424
|
+
const tag = localName(el.tagName);
|
|
425
|
+
const paint = tag === "lineargradient" ? readLinearGradient(el, elements, onWarn) : tag === "conicgradient" ? readConicGradient(el, elements, onWarn) : tag === "meshgradient" ? readMeshGradient(el, onWarn) : readRadialGradient(el, elements, onWarn);
|
|
397
426
|
if (paint) out.set(id, paint);
|
|
398
427
|
}
|
|
399
428
|
warnUnsupportedDefsChildren(svg, onWarn);
|
|
@@ -407,7 +436,8 @@ function warnUnsupportedDefsChildren(svg, onWarn) {
|
|
|
407
436
|
for (let i = 0; i < root.children.length; i++) {
|
|
408
437
|
const child = root.children[i];
|
|
409
438
|
const tag = child.tagName.toLowerCase();
|
|
410
|
-
if (GRADIENT_TAGS.has(tag) || tag
|
|
439
|
+
if (GRADIENT_TAGS.has(tag) || GRADIENT_TAGS.has(localName(tag))) continue;
|
|
440
|
+
if (tag === "pattern" || tag === "marker" || tag === "clippath") continue;
|
|
411
441
|
onWarn(`unsupported <defs> child: <${child.tagName}>`);
|
|
412
442
|
}
|
|
413
443
|
}
|
|
@@ -489,7 +519,8 @@ function readLinearGradient(el, elements, onWarn) {
|
|
|
489
519
|
y: num(inheritedAttr(el, elements, "y2"), 0)
|
|
490
520
|
},
|
|
491
521
|
stops: inheritedStops(el, elements, onWarn),
|
|
492
|
-
units: readGradientUnits(el, elements)
|
|
522
|
+
units: readGradientUnits(el, elements),
|
|
523
|
+
interpolate: readInterpolate(el, elements)
|
|
493
524
|
};
|
|
494
525
|
}
|
|
495
526
|
function readRadialGradient(el, elements, onWarn) {
|
|
@@ -502,7 +533,48 @@ function readRadialGradient(el, elements, onWarn) {
|
|
|
502
533
|
},
|
|
503
534
|
radius: num(inheritedAttr(el, elements, "r"), 0.5),
|
|
504
535
|
stops: inheritedStops(el, elements, onWarn),
|
|
505
|
-
units: readGradientUnits(el, elements)
|
|
536
|
+
units: readGradientUnits(el, elements),
|
|
537
|
+
interpolate: readInterpolate(el, elements)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function readConicGradient(el, elements, onWarn) {
|
|
541
|
+
warnGradientTransform(el, elements, onWarn);
|
|
542
|
+
return {
|
|
543
|
+
fill: "conic-gradient",
|
|
544
|
+
center: {
|
|
545
|
+
x: num(inheritedAttr(el, elements, "cx"), 0.5),
|
|
546
|
+
y: num(inheritedAttr(el, elements, "cy"), 0.5)
|
|
547
|
+
},
|
|
548
|
+
angle: num(inheritedAttr(el, elements, "angle"), 0),
|
|
549
|
+
stops: inheritedStops(el, elements, onWarn),
|
|
550
|
+
units: readGradientUnits(el, elements),
|
|
551
|
+
interpolate: readInterpolate(el, elements)
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function readMeshGradient(el, onWarn) {
|
|
555
|
+
const patches = [];
|
|
556
|
+
for (let i = 0; i < el.children.length; i++) {
|
|
557
|
+
const child = el.children[i];
|
|
558
|
+
if (localName(child.tagName) !== "patch") continue;
|
|
559
|
+
const points = (child.getAttribute("points") ?? "").trim().split(/\s+/).filter(Boolean).map((pair) => {
|
|
560
|
+
const [x, y] = pair.split(",").map(Number);
|
|
561
|
+
return { x, y };
|
|
562
|
+
});
|
|
563
|
+
const colors = (child.getAttribute("colors") ?? "").trim().split(/\s+/).filter(Boolean);
|
|
564
|
+
const sane = (points.length === 12 || points.length === 16) && points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y)) && colors.length === 4;
|
|
565
|
+
if (!sane) {
|
|
566
|
+
onWarn?.(`<${WEASEL_NS_PREFIX}:patch> with ${points.length} points and ${colors.length} colors \u2014 dropped`);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
patches.push({ points, colors });
|
|
570
|
+
}
|
|
571
|
+
if (patches.length === 0) return null;
|
|
572
|
+
const space = el.getAttribute("interpolate") ?? el.getAttributeNS(WEASEL_NS, "interpolate");
|
|
573
|
+
return {
|
|
574
|
+
fill: "mesh-gradient",
|
|
575
|
+
patches,
|
|
576
|
+
units: el.getAttribute("gradientUnits") === "objectBoundingBox" ? "bounds" : "world",
|
|
577
|
+
...space === "oklab" || space === "oklch" ? { interpolate: space } : {}
|
|
506
578
|
};
|
|
507
579
|
}
|
|
508
580
|
var PaintServerRegistry = class {
|
|
@@ -511,11 +583,14 @@ var PaintServerRegistry = class {
|
|
|
511
583
|
counter = 0;
|
|
512
584
|
// Marker keys referenced by any stroke, in first-use order. The `<defs>`
|
|
513
585
|
// id is the marker key itself, not a minted counter id: `parseSvg` reads a
|
|
514
|
-
// `marker-end="url(#id)"` fragment back as the marker key directly
|
|
515
|
-
//
|
|
516
|
-
// back unresolvable and warn on round-trip.
|
|
586
|
+
// `marker-end="url(#id)"` fragment back as the marker key directly when the
|
|
587
|
+
// registry knows it, so a synthetic id would come back as a stranger.
|
|
517
588
|
markerKeys = [];
|
|
518
589
|
markerKeySet = /* @__PURE__ */ new Set();
|
|
590
|
+
// Clip paths, in first-use order. Identity, not value: two groups clipped by
|
|
591
|
+
// equal outlines get a def each, which costs bytes and misleads nobody.
|
|
592
|
+
clips = [];
|
|
593
|
+
clipIds = /* @__PURE__ */ new Map();
|
|
519
594
|
register(paint) {
|
|
520
595
|
const existing = this.byPaint.get(paint);
|
|
521
596
|
if (existing) return existing;
|
|
@@ -524,6 +599,23 @@ var PaintServerRegistry = class {
|
|
|
524
599
|
this.order.push(paint);
|
|
525
600
|
return id;
|
|
526
601
|
}
|
|
602
|
+
/**
|
|
603
|
+
* The `fill` / `stroke` attribute value referencing this paint server: a
|
|
604
|
+
* `url(#id)` reference, plus the fallback paint SVG's own grammar allows
|
|
605
|
+
* after one for any kind SVG cannot express. A renderer that cannot resolve
|
|
606
|
+
* the reference takes the fallback instead of dropping the fill, which is
|
|
607
|
+
* the difference between a flat shape and a vanished one.
|
|
608
|
+
*/
|
|
609
|
+
ref(paint) {
|
|
610
|
+
const id = this.register(paint);
|
|
611
|
+
if (hasNativeSvgForm(paint)) return `url(#${id})`;
|
|
612
|
+
return `url(#${id}) ${paintFallback(paint)}`;
|
|
613
|
+
}
|
|
614
|
+
/** Whether any registered paint serializes outside SVG's own vocabulary, so
|
|
615
|
+
* the root has to declare {@link WEASEL_NS}. */
|
|
616
|
+
usesPrivateNamespace() {
|
|
617
|
+
return this.order.some((paint) => !hasNativeSvgForm(paint) || usesInterpolateAttr(paint));
|
|
618
|
+
}
|
|
527
619
|
/** The `<defs>` id for a marker key — the key itself, minting nothing.
|
|
528
620
|
* `undefined` when nothing is registered under `key`, so a caller emits no
|
|
529
621
|
* attribute at all rather than a `url(#…)` pointing at a def that will
|
|
@@ -536,9 +628,21 @@ var PaintServerRegistry = class {
|
|
|
536
628
|
}
|
|
537
629
|
return key;
|
|
538
630
|
}
|
|
539
|
-
/**
|
|
631
|
+
/** Mint (or recall) the `<defs>` id for a clip outline. */
|
|
632
|
+
clipId(path) {
|
|
633
|
+
const existing = this.clipIds.get(path);
|
|
634
|
+
if (existing) return existing;
|
|
635
|
+
const id = `clip${this.clips.length}`;
|
|
636
|
+
this.clipIds.set(path, id);
|
|
637
|
+
this.clips.push(path);
|
|
638
|
+
return id;
|
|
639
|
+
}
|
|
640
|
+
/** Emit `<defs>...</defs>` XML for every registered paint server, marker
|
|
641
|
+
* and clip path. */
|
|
540
642
|
toDefsXml(onWarn) {
|
|
541
|
-
if (this.order.length === 0 && this.markerKeys.length === 0)
|
|
643
|
+
if (this.order.length === 0 && this.markerKeys.length === 0 && this.clips.length === 0) {
|
|
644
|
+
return "";
|
|
645
|
+
}
|
|
542
646
|
const parts = ["<defs>"];
|
|
543
647
|
for (const paint of this.order) {
|
|
544
648
|
parts.push(paintServerXml(this.byPaint.get(paint), paint, onWarn));
|
|
@@ -546,18 +650,33 @@ var PaintServerRegistry = class {
|
|
|
546
650
|
for (const key of this.markerKeys) {
|
|
547
651
|
const entry = getMarker(key);
|
|
548
652
|
if (!entry) continue;
|
|
549
|
-
parts.push(entry.toSvg ? entry.toSvg(key, entry) : defaultMarkerXml(key, entry));
|
|
653
|
+
parts.push(entry.toSvg ? entry.toSvg(key, entry) : defaultMarkerXml(key, entry, onWarn));
|
|
654
|
+
}
|
|
655
|
+
for (const path of this.clips) {
|
|
656
|
+
parts.push(
|
|
657
|
+
`<clipPath id="${this.clipIds.get(path)}"><path d="${serializePathD(path)}"/></clipPath>`
|
|
658
|
+
);
|
|
550
659
|
}
|
|
551
660
|
parts.push("</defs>");
|
|
552
661
|
return parts.join("");
|
|
553
662
|
}
|
|
554
663
|
};
|
|
555
|
-
function defaultMarkerXml(id, entry) {
|
|
664
|
+
function defaultMarkerXml(id, entry, onWarn) {
|
|
556
665
|
const path = entry.path({ size: 1, stroke: { paint: { fill: "solid", color: "#000" } } });
|
|
557
666
|
const d = serializePathD(path);
|
|
558
|
-
const fill = entry.fill
|
|
559
|
-
const outline = entry.outline ? `
|
|
560
|
-
|
|
667
|
+
const fill = markerPaintAttrs("fill", entry.fill, id, onWarn);
|
|
668
|
+
const outline = entry.outline ? ` ${markerPaintAttrs("stroke", entry.outline.paint, id, onWarn)} stroke-width="${trimNumber(entry.outline.width)}" stroke-linecap="round" stroke-linejoin="round"` : "";
|
|
669
|
+
const orient = typeof entry.orient === "number" ? trimNumber(entry.orient * 180 / Math.PI) : "auto-start-reverse";
|
|
670
|
+
return `<marker id="${id}" markerUnits="strokeWidth" markerWidth="8" markerHeight="8" refX="0" refY="0" orient="${orient}" overflow="visible"><path d="${d}" ${fill}${outline}/></marker>`;
|
|
671
|
+
}
|
|
672
|
+
function markerPaintAttrs(attr, paint, id, onWarn) {
|
|
673
|
+
if (paint === "none") return `${attr}="none"`;
|
|
674
|
+
if (paint === void 0 || paint === "line") return `${attr}="context-stroke"`;
|
|
675
|
+
if ("color" in paint && (paint.fill === void 0 || paint.fill === "solid")) {
|
|
676
|
+
return paint.opacity != null && paint.opacity !== 1 ? `${attr}="${paint.color}" ${attr}-opacity="${trimNumber(paint.opacity)}"` : `${attr}="${paint.color}"`;
|
|
677
|
+
}
|
|
678
|
+
onWarn?.(`marker "${id}" ${attr} is a paint server, which a <marker> def cannot reference here; written as the line's paint`);
|
|
679
|
+
return `${attr}="context-stroke"`;
|
|
561
680
|
}
|
|
562
681
|
function paintServerXml(id, paint, onWarn) {
|
|
563
682
|
if (paint.fill === "pattern") return patternXml(id, paint, onWarn);
|
|
@@ -574,14 +693,25 @@ function gradientUnitsAttr(units) {
|
|
|
574
693
|
function gradientXml(id, paint) {
|
|
575
694
|
if (paint.fill === "linear-gradient") {
|
|
576
695
|
const stops = paint.stops.map(stopXml).join("");
|
|
577
|
-
return `<linearGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" x1="${trimNumber(paint.from.x)}" y1="${trimNumber(paint.from.y)}" x2="${trimNumber(paint.to.x)}" y2="${trimNumber(paint.to.y)}">${stops}</linearGradient>`;
|
|
696
|
+
return `<linearGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" x1="${trimNumber(paint.from.x)}" y1="${trimNumber(paint.from.y)}" x2="${trimNumber(paint.to.x)}" y2="${trimNumber(paint.to.y)}"${interpolateAttr(paint.interpolate)}>${stops}</linearGradient>`;
|
|
697
|
+
}
|
|
698
|
+
if (paint.fill === "conic-gradient") {
|
|
699
|
+
const stops = paint.stops.map(stopXml).join("");
|
|
700
|
+
const P = WEASEL_NS_PREFIX;
|
|
701
|
+
return `<${P}:conicGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" angle="${trimNumber(paint.angle)}"${interpolateAttr(paint.interpolate)}>${stops}</${P}:conicGradient>`;
|
|
578
702
|
}
|
|
579
703
|
if (paint.fill === "radial-gradient") {
|
|
580
704
|
const stops = paint.stops.map(stopXml).join("");
|
|
581
|
-
return `<radialGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" r="${trimNumber(paint.radius)}">${stops}</radialGradient>`;
|
|
705
|
+
return `<radialGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" r="${trimNumber(paint.radius)}"${interpolateAttr(paint.interpolate)}>${stops}</radialGradient>`;
|
|
582
706
|
}
|
|
583
707
|
return "";
|
|
584
708
|
}
|
|
709
|
+
function paintFallback(paint) {
|
|
710
|
+
const color = getPaintKind(paint.fill)?.colorOf(paint);
|
|
711
|
+
if (!color) return "none";
|
|
712
|
+
const packed = /^#([0-9a-f]{6})[0-9a-f]{2}$/i.exec(color);
|
|
713
|
+
return packed ? `#${packed[1].toLowerCase()}` : color;
|
|
714
|
+
}
|
|
585
715
|
function stopXml(s) {
|
|
586
716
|
const m = /^#([0-9a-f]{6})([0-9a-f]{2})$/i.exec(s.color);
|
|
587
717
|
if (m) {
|
|
@@ -661,6 +791,7 @@ var IGNORED_TAGS = /* @__PURE__ */ new Set([
|
|
|
661
791
|
"radialgradient",
|
|
662
792
|
"pattern",
|
|
663
793
|
"marker",
|
|
794
|
+
"clippath",
|
|
664
795
|
"title",
|
|
665
796
|
"desc",
|
|
666
797
|
"metadata"
|
|
@@ -693,8 +824,11 @@ function parseSvg(svg, opts = {}) {
|
|
|
693
824
|
const gradients = collectGradients(root, onWarn);
|
|
694
825
|
for (const [id, paint] of collectPatterns(root, onWarn)) gradients.set(id, paint);
|
|
695
826
|
const rootStyle = deriveStyle(EMPTY_STYLE, root);
|
|
696
|
-
const
|
|
827
|
+
const clips = collectClipPaths(root, onWarn);
|
|
828
|
+
const nodes = parseChildren(root, IDENTITY_MATRIX, rootStyle, gradients, clips, onWarn, uriToPrefix);
|
|
829
|
+
const markers = ingestMarkers(root, nodes, gradients, onWarn);
|
|
697
830
|
const result = { nodes, warnings };
|
|
831
|
+
if (markers.length > 0) result.markers = markers;
|
|
698
832
|
if (documentMeta) result.documentMeta = documentMeta;
|
|
699
833
|
const viewBox = parseViewBoxAttr(root.getAttribute("viewBox"));
|
|
700
834
|
if (viewBox) result.viewBox = viewBox;
|
|
@@ -796,13 +930,53 @@ function collectElementMeta(el, uriToPrefix) {
|
|
|
796
930
|
}
|
|
797
931
|
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
798
932
|
}
|
|
799
|
-
function
|
|
933
|
+
function collectClipPaths(root, onWarn) {
|
|
934
|
+
const out = /* @__PURE__ */ new Map();
|
|
935
|
+
for (const [id, el] of collectElementsByTag(root, /* @__PURE__ */ new Set(["clippath"]))) {
|
|
936
|
+
const units = el.getAttribute("clipPathUnits");
|
|
937
|
+
if (units && units !== "userSpaceOnUse") {
|
|
938
|
+
onWarn(`<clipPath id="${id}"> uses clipPathUnits="${units}", which is not modeled`);
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
const shapes = [];
|
|
942
|
+
for (let i = 0; i < el.children.length; i++) {
|
|
943
|
+
if (SUPPORTED_LEAF_TAGS.has(el.children[i].tagName.toLowerCase())) shapes.push(el.children[i]);
|
|
944
|
+
}
|
|
945
|
+
if (shapes.length !== 1) {
|
|
946
|
+
onWarn(
|
|
947
|
+
`<clipPath id="${id}"> holds ${shapes.length} shapes the kit can lower; only a single-shape clip is modeled, so it clips nothing`
|
|
948
|
+
);
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
const shape = shapes[0];
|
|
952
|
+
const local = parseTransform(shape.getAttribute("transform"), onWarn);
|
|
953
|
+
const path = lowerLeaf(shape, shape.tagName.toLowerCase(), local, onWarn);
|
|
954
|
+
if (path) out.set(id, path);
|
|
955
|
+
}
|
|
956
|
+
return out;
|
|
957
|
+
}
|
|
958
|
+
function clipFor(el, ctm, clips, onWarn) {
|
|
959
|
+
const attr = el.getAttribute("clip-path");
|
|
960
|
+
if (!attr || attr === "none") return void 0;
|
|
961
|
+
const ref = /^url\(\s*#([^)\s]+)\s*\)$/.exec(attr.trim());
|
|
962
|
+
if (!ref) {
|
|
963
|
+
onWarn(`clip-path="${attr}" is not a url(#id) reference; the clip is dropped`);
|
|
964
|
+
return void 0;
|
|
965
|
+
}
|
|
966
|
+
const path = clips.get(ref[1]);
|
|
967
|
+
if (!path) {
|
|
968
|
+
onWarn(`clip-path references #${ref[1]}, which no <clipPath> in the document defines`);
|
|
969
|
+
return void 0;
|
|
970
|
+
}
|
|
971
|
+
return transformPath(path, ctm);
|
|
972
|
+
}
|
|
973
|
+
function parseChildren(parent, ctm, style, gradients, clips, onWarn, uriToPrefix) {
|
|
800
974
|
const out = [];
|
|
801
975
|
for (let i = 0; i < parent.children.length; i++) {
|
|
802
976
|
const el = parent.children[i];
|
|
803
977
|
const ns = el.namespaceURI;
|
|
804
978
|
if (ns && ns !== "http://www.w3.org/2000/svg") continue;
|
|
805
|
-
const node = parseElement(el, ctm, style, gradients, onWarn, uriToPrefix);
|
|
979
|
+
const node = parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix);
|
|
806
980
|
if (node) {
|
|
807
981
|
if (Array.isArray(node)) out.push(...node);
|
|
808
982
|
else out.push(node);
|
|
@@ -810,16 +984,26 @@ function parseChildren(parent, ctm, style, gradients, onWarn, uriToPrefix) {
|
|
|
810
984
|
}
|
|
811
985
|
return out;
|
|
812
986
|
}
|
|
813
|
-
function parseElement(el, ctm, style, gradients, onWarn, uriToPrefix) {
|
|
987
|
+
function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
|
|
814
988
|
const tag = el.tagName.toLowerCase();
|
|
815
989
|
if (IGNORED_TAGS.has(tag)) return null;
|
|
990
|
+
if (tag === "g" && el.hasAttribute("data-weasel-image")) {
|
|
991
|
+
const image = parseCroppedImage(el, ctm, onWarn);
|
|
992
|
+
if (image) {
|
|
993
|
+
const meta2 = collectElementMeta(el, uriToPrefix);
|
|
994
|
+
if (meta2) image.meta = meta2;
|
|
995
|
+
return image;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
816
998
|
if (tag === "g") {
|
|
817
999
|
const local = parseTransform(el.getAttribute("transform"), onWarn);
|
|
818
1000
|
const childCtm = multiply(ctm, local);
|
|
819
1001
|
const childStyle = deriveStyle(style, el);
|
|
820
|
-
const children = parseChildren(el, childCtm, childStyle, gradients, onWarn, uriToPrefix);
|
|
1002
|
+
const children = parseChildren(el, childCtm, childStyle, gradients, clips, onWarn, uriToPrefix);
|
|
821
1003
|
const opacity2 = readOpacityAttr(el, "opacity");
|
|
822
1004
|
const group = { kind: "group", children };
|
|
1005
|
+
const clip = clipFor(el, childCtm, clips, onWarn);
|
|
1006
|
+
if (clip) group.clip = clip;
|
|
823
1007
|
if (opacity2 != null) group.opacity = opacity2;
|
|
824
1008
|
const meta2 = collectElementMeta(el, uriToPrefix);
|
|
825
1009
|
if (meta2) group.meta = meta2;
|
|
@@ -833,7 +1017,7 @@ function parseElement(el, ctm, style, gradients, onWarn, uriToPrefix) {
|
|
|
833
1017
|
if (el.hasAttribute("viewBox")) {
|
|
834
1018
|
onWarn("nested <svg> viewBox is not modeled; its children are not rescaled");
|
|
835
1019
|
}
|
|
836
|
-
return parseChildren(el, multiply(ctm, offset), childStyle, gradients, onWarn, uriToPrefix);
|
|
1020
|
+
return parseChildren(el, multiply(ctm, offset), childStyle, gradients, clips, onWarn, uriToPrefix);
|
|
837
1021
|
}
|
|
838
1022
|
if (tag === "text") {
|
|
839
1023
|
const textNode = parseTextElement(el, ctm, style, gradients, onWarn);
|
|
@@ -978,6 +1162,12 @@ function readPaint(style, attr, defaultColor, gradients, onWarn) {
|
|
|
978
1162
|
if (parsed.kind === "ref") {
|
|
979
1163
|
const paint = gradients.get(parsed.id);
|
|
980
1164
|
if (!paint) {
|
|
1165
|
+
if (parsed.fallback) {
|
|
1166
|
+
const flat = { kind: "solid", color: parsed.fallback.color };
|
|
1167
|
+
const a2 = opacity ?? (parsed.fallback.alpha < 1 ? parsed.fallback.alpha : void 0);
|
|
1168
|
+
if (a2 != null) flat.opacity = a2;
|
|
1169
|
+
return flat;
|
|
1170
|
+
}
|
|
981
1171
|
onWarn(`${attr} references unknown gradient #${parsed.id}`);
|
|
982
1172
|
return { kind: "solid", color: defaultColor };
|
|
983
1173
|
}
|
|
@@ -1034,12 +1224,7 @@ function readStroke(style, gradients, onWarn, nonScaling = false) {
|
|
|
1034
1224
|
["marker-end", "markerEnd"]
|
|
1035
1225
|
]) {
|
|
1036
1226
|
const id = parseMarkerRef(style[attr] ?? null);
|
|
1037
|
-
if (id
|
|
1038
|
-
if (getMarker(id) === void 0) {
|
|
1039
|
-
onWarn(`${attr} references a marker this kit has no entry for: #${id}`);
|
|
1040
|
-
continue;
|
|
1041
|
-
}
|
|
1042
|
-
stroke[field] = id;
|
|
1227
|
+
if (id !== void 0) stroke[field] = id;
|
|
1043
1228
|
}
|
|
1044
1229
|
return stroke;
|
|
1045
1230
|
}
|
|
@@ -1155,20 +1340,30 @@ function parseRunFontSize(raw, onWarn) {
|
|
|
1155
1340
|
}
|
|
1156
1341
|
var XLINK_NS2 = "http://www.w3.org/1999/xlink";
|
|
1157
1342
|
function parseImageElement(el, ctm, onWarn) {
|
|
1158
|
-
const href =
|
|
1343
|
+
const href = imageHref(el);
|
|
1159
1344
|
if (!href) {
|
|
1160
1345
|
onWarn("<image> without href; dropped");
|
|
1161
1346
|
return null;
|
|
1162
1347
|
}
|
|
1348
|
+
const node = imageNodeAt(el, el, href, ctm, onWarn);
|
|
1349
|
+
if (el.hasAttribute("preserveAspectRatio") && el.getAttribute("preserveAspectRatio") !== "none") {
|
|
1350
|
+
onWarn("<image> preserveAspectRatio is not modeled; the box is taken literally");
|
|
1351
|
+
}
|
|
1352
|
+
return node;
|
|
1353
|
+
}
|
|
1354
|
+
function imageHref(el) {
|
|
1355
|
+
return el.getAttribute("href") ?? el.getAttributeNS(XLINK_NS2, "href") ?? el.getAttribute("xlink:href");
|
|
1356
|
+
}
|
|
1357
|
+
function imageNodeAt(boxEl, ownerEl, href, ctm, onWarn) {
|
|
1163
1358
|
const num2 = (raw, fallback) => {
|
|
1164
1359
|
if (raw == null) return fallback;
|
|
1165
1360
|
const n = parseFloat(raw);
|
|
1166
1361
|
return Number.isFinite(n) ? n : fallback;
|
|
1167
1362
|
};
|
|
1168
|
-
const rawX = num2(
|
|
1169
|
-
const rawY = num2(
|
|
1170
|
-
const rawW = num2(
|
|
1171
|
-
const rawH = num2(
|
|
1363
|
+
const rawX = num2(boxEl.getAttribute("x"), 0);
|
|
1364
|
+
const rawY = num2(boxEl.getAttribute("y"), 0);
|
|
1365
|
+
const rawW = num2(boxEl.getAttribute("width"), 0);
|
|
1366
|
+
const rawH = num2(boxEl.getAttribute("height"), 0);
|
|
1172
1367
|
const px = (x, y) => [ctm[0] * x + ctm[2] * y + ctm[4], ctm[1] * x + ctm[3] * y + ctm[5]];
|
|
1173
1368
|
const [x0, y0] = px(rawX, rawY);
|
|
1174
1369
|
const [x1, y1] = px(rawX + rawW, rawY + rawH);
|
|
@@ -1180,9 +1375,9 @@ function parseImageElement(el, ctm, onWarn) {
|
|
|
1180
1375
|
width: Math.abs(x1 - x0),
|
|
1181
1376
|
height: Math.abs(y1 - y0)
|
|
1182
1377
|
};
|
|
1183
|
-
const opacity = readOpacityAttr(
|
|
1378
|
+
const opacity = readOpacityAttr(ownerEl, "opacity");
|
|
1184
1379
|
if (opacity != null) node.opacity = opacity;
|
|
1185
|
-
const localTransform = parseTransform(
|
|
1380
|
+
const localTransform = parseTransform(ownerEl.getAttribute("transform"), onWarn);
|
|
1186
1381
|
if (!isIdentity(localTransform)) {
|
|
1187
1382
|
const worldLocal = rebaseTransform(ctm, localTransform);
|
|
1188
1383
|
const cx = node.x + node.width / 2;
|
|
@@ -1199,9 +1394,21 @@ function parseImageElement(el, ctm, onWarn) {
|
|
|
1199
1394
|
}
|
|
1200
1395
|
}
|
|
1201
1396
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1397
|
+
return node;
|
|
1398
|
+
}
|
|
1399
|
+
function parseCroppedImage(g, ctm, onWarn) {
|
|
1400
|
+
const viewport = [...g.children].find((c) => c.tagName.toLowerCase() === "svg");
|
|
1401
|
+
const img = viewport && [...viewport.children].find((c) => c.tagName.toLowerCase() === "image");
|
|
1402
|
+
const href = img && imageHref(img);
|
|
1403
|
+
const source = viewport && parseViewBoxAttr(viewport.getAttribute("viewBox"));
|
|
1404
|
+
if (!img || !href || !source) return null;
|
|
1405
|
+
const node = imageNodeAt(viewport, g, href, ctm, onWarn);
|
|
1406
|
+
if (source.x !== 0 || source.y !== 0 || source.width !== 1 || source.height !== 1) {
|
|
1407
|
+
node.source = source;
|
|
1408
|
+
}
|
|
1409
|
+
const flip = parseTransform(img.getAttribute("transform"), onWarn);
|
|
1410
|
+
if (flip[0] < 0) node.flipX = true;
|
|
1411
|
+
if (flip[3] < 0) node.flipY = true;
|
|
1205
1412
|
return node;
|
|
1206
1413
|
}
|
|
1207
1414
|
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
@@ -1268,7 +1475,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
1268
1475
|
const leafStyle = deriveStyle(style, el);
|
|
1269
1476
|
const textStyle = readTextStyle(leafStyle, onWarn);
|
|
1270
1477
|
const textPaint = readTextPaint(leafStyle, gradients, onWarn, isNonScalingStroke(el));
|
|
1271
|
-
const fontSize = textStyle.fontSize ?? 16;
|
|
1478
|
+
const fontSize = resolveScreenLength(textStyle.fontSize ?? 16, 1);
|
|
1272
1479
|
const lineHeight = textStyle.lineHeight ?? 1.2;
|
|
1273
1480
|
const dominantBaseline = el.getAttribute("dominant-baseline");
|
|
1274
1481
|
const explicitTopAnchor = dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
|
|
@@ -1315,6 +1522,8 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
1315
1522
|
);
|
|
1316
1523
|
if (hasStyling) node.runs = runs;
|
|
1317
1524
|
if (el.getAttribute("data-weasel-wrap") === "true") textStyle.wrap = true;
|
|
1525
|
+
const va = el.getAttribute("data-weasel-vertical-align");
|
|
1526
|
+
if (va === "center" || va === "bottom") node.verticalAlign = va;
|
|
1318
1527
|
if (Object.keys(textStyle).length > 0) node.style = textStyle;
|
|
1319
1528
|
if (textPaint.fill !== void 0) node.fill = textPaint.fill;
|
|
1320
1529
|
if (textPaint.stroke != null) node.stroke = textPaint.stroke;
|
|
@@ -1376,6 +1585,7 @@ function readTspanRun(el, gradients, style, onWarn) {
|
|
|
1376
1585
|
} else if (parsed?.kind === "ref") {
|
|
1377
1586
|
const paint = gradients.get(parsed.id);
|
|
1378
1587
|
if (paint) run.fill = paint;
|
|
1588
|
+
else if (parsed.fallback) run.fill = { fill: "solid", color: parsed.fallback.color };
|
|
1379
1589
|
}
|
|
1380
1590
|
}
|
|
1381
1591
|
const stroke = coreStroke(readStroke(ownStrokeStyle(el), gradients, onWarn, isNonScalingStroke(el)));
|
|
@@ -1425,6 +1635,9 @@ function readTextPaint(style, gradients, onWarn, nonScalingStroke = false) {
|
|
|
1425
1635
|
} else if (parsed?.kind === "ref") {
|
|
1426
1636
|
const paint = gradients.get(parsed.id);
|
|
1427
1637
|
if (paint) out.fill = paint;
|
|
1638
|
+
else if (parsed.fallback) {
|
|
1639
|
+
out.fill = { fill: "solid", color: parsed.fallback.color };
|
|
1640
|
+
}
|
|
1428
1641
|
} else if (parsed?.kind === "none") {
|
|
1429
1642
|
out.fill = null;
|
|
1430
1643
|
}
|
|
@@ -1433,6 +1646,196 @@ function readTextPaint(style, gradients, onWarn, nonScalingStroke = false) {
|
|
|
1433
1646
|
if (stroke) out.stroke = stroke;
|
|
1434
1647
|
return out;
|
|
1435
1648
|
}
|
|
1649
|
+
var MARKER_FIELDS = [
|
|
1650
|
+
["markerStart", "start", "marker-start"],
|
|
1651
|
+
["markerMid", "mid", "marker-mid"],
|
|
1652
|
+
["markerEnd", "end", "marker-end"]
|
|
1653
|
+
];
|
|
1654
|
+
function ingestMarkers(root, nodes, gradients, onWarn) {
|
|
1655
|
+
const defs = /* @__PURE__ */ new Map();
|
|
1656
|
+
const all = root.getElementsByTagName("marker");
|
|
1657
|
+
for (let i = 0; i < all.length; i++) {
|
|
1658
|
+
const id = all[i].getAttribute("id");
|
|
1659
|
+
if (id && !defs.has(id)) defs.set(id, all[i]);
|
|
1660
|
+
}
|
|
1661
|
+
const minted = /* @__PURE__ */ new Map();
|
|
1662
|
+
const resolve = (stroke, width) => {
|
|
1663
|
+
const w = width === void 0 ? 1 : typeof width === "number" ? width : width.px;
|
|
1664
|
+
for (const [field, role, attr] of MARKER_FIELDS) {
|
|
1665
|
+
const id = stroke[field];
|
|
1666
|
+
if (typeof id !== "string" || getMarker(id) !== void 0) continue;
|
|
1667
|
+
const el = defs.get(id);
|
|
1668
|
+
if (!el) {
|
|
1669
|
+
onWarn(`${attr} references #${id}, which is neither a kit marker nor defined in the document`);
|
|
1670
|
+
delete stroke[field];
|
|
1671
|
+
continue;
|
|
1672
|
+
}
|
|
1673
|
+
const cacheKey = `${id}\0${role}\0${w}`;
|
|
1674
|
+
if (!minted.has(cacheKey)) minted.set(cacheKey, markerEntryFrom(el, id, role, w, gradients, onWarn));
|
|
1675
|
+
const entry = minted.get(cacheKey);
|
|
1676
|
+
if (entry) stroke[field] = entry.id;
|
|
1677
|
+
else delete stroke[field];
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
const visit = (n) => {
|
|
1681
|
+
if (n.kind === "group") n.children.forEach(visit);
|
|
1682
|
+
else if (n.kind === "path" && n.stroke) resolve(n.stroke, n.stroke.width);
|
|
1683
|
+
else if (n.kind === "text") {
|
|
1684
|
+
if (n.stroke) resolve(n.stroke, n.stroke.width);
|
|
1685
|
+
for (const run of n.runs ?? []) {
|
|
1686
|
+
if (run.stroke) resolve(run.stroke, run.stroke.width ?? n.stroke?.width);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
nodes.forEach(visit);
|
|
1691
|
+
const out = /* @__PURE__ */ new Map();
|
|
1692
|
+
for (const entry of minted.values()) if (entry) out.set(entry.id, entry);
|
|
1693
|
+
return [...out.values()];
|
|
1694
|
+
}
|
|
1695
|
+
function styleAt(el) {
|
|
1696
|
+
const chain = [];
|
|
1697
|
+
for (let e = el; e; e = e.parentElement) chain.unshift(e);
|
|
1698
|
+
return chain.reduce((style, e) => deriveStyle(style, e), EMPTY_STYLE);
|
|
1699
|
+
}
|
|
1700
|
+
function markerRef(raw, lo, span) {
|
|
1701
|
+
const v = raw?.trim();
|
|
1702
|
+
if (v === "left" || v === "top") return lo;
|
|
1703
|
+
if (v === "center") return lo + span / 2;
|
|
1704
|
+
if (v === "right" || v === "bottom") return lo + span;
|
|
1705
|
+
const n = v ? parseFloat(v) : 0;
|
|
1706
|
+
return Number.isFinite(n) ? n : 0;
|
|
1707
|
+
}
|
|
1708
|
+
function markerOrient(raw) {
|
|
1709
|
+
const v = raw?.trim() ?? "";
|
|
1710
|
+
if (v === "auto") return { orient: "auto", reverseAtStart: true };
|
|
1711
|
+
if (v === "auto-start-reverse") return { orient: "auto", reverseAtStart: false };
|
|
1712
|
+
const m = /^(-?[\d.]+(?:e-?\d+)?)(deg|rad|grad|turn)?$/i.exec(v);
|
|
1713
|
+
if (!m) return { orient: 0, reverseAtStart: false };
|
|
1714
|
+
const n = parseFloat(m[1]);
|
|
1715
|
+
const unit = (m[2] ?? "deg").toLowerCase();
|
|
1716
|
+
const rad = unit === "rad" ? n : unit === "grad" ? n * Math.PI / 200 : unit === "turn" ? n * 2 * Math.PI : n * Math.PI / 180;
|
|
1717
|
+
return { orient: rad, reverseAtStart: false };
|
|
1718
|
+
}
|
|
1719
|
+
function markerPaint(style, attr, gradients, onWarn) {
|
|
1720
|
+
const raw = style[attr]?.trim();
|
|
1721
|
+
if (raw === "context-stroke") return "line";
|
|
1722
|
+
if (raw === "context-fill") {
|
|
1723
|
+
onWarn(`a marker's ${attr}="context-fill" is read as the line's stroke paint`);
|
|
1724
|
+
return "line";
|
|
1725
|
+
}
|
|
1726
|
+
const paint = readPaint(style, attr, "#000000", gradients, onWarn);
|
|
1727
|
+
if (paint.kind === "none") return "none";
|
|
1728
|
+
if (paint.kind === "gradient") return paint.paint;
|
|
1729
|
+
return paint.opacity != null ? { ...solid(paint.color), opacity: paint.opacity } : solid(paint.color);
|
|
1730
|
+
}
|
|
1731
|
+
function asPolygon(path) {
|
|
1732
|
+
if (path.kind === "polygon") return path;
|
|
1733
|
+
const { x, y, width: w, height: h } = path;
|
|
1734
|
+
return {
|
|
1735
|
+
kind: "polygon",
|
|
1736
|
+
commands: new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]),
|
|
1737
|
+
coords: new Float32Array([x, y, x + w, y, x + w, y + h, x, y + h]),
|
|
1738
|
+
fillRule: "nonzero"
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
function shortHash(s) {
|
|
1742
|
+
let h = 2166136261;
|
|
1743
|
+
for (let i = 0; i < s.length; i++) {
|
|
1744
|
+
h ^= s.charCodeAt(i);
|
|
1745
|
+
h = Math.imul(h, 16777619);
|
|
1746
|
+
}
|
|
1747
|
+
return (h >>> 0).toString(36);
|
|
1748
|
+
}
|
|
1749
|
+
function markerEntryFrom(el, id, role, strokeWidth, gradients, onWarn) {
|
|
1750
|
+
const num2 = (name, fallback) => {
|
|
1751
|
+
const n = parseFloat(el.getAttribute(name) ?? "");
|
|
1752
|
+
return Number.isFinite(n) ? n : fallback;
|
|
1753
|
+
};
|
|
1754
|
+
const vb = parseViewBoxAttr(el.getAttribute("viewBox"));
|
|
1755
|
+
let sx = 1, sy = 1;
|
|
1756
|
+
if (vb && vb.width > 0 && vb.height > 0) {
|
|
1757
|
+
sx = num2("markerWidth", 3) / vb.width;
|
|
1758
|
+
sy = num2("markerHeight", 3) / vb.height;
|
|
1759
|
+
const par = el.getAttribute("preserveAspectRatio")?.trim() ?? "";
|
|
1760
|
+
if (!par.startsWith("none")) {
|
|
1761
|
+
const s = par.endsWith("slice") ? Math.max(sx, sy) : Math.min(sx, sy);
|
|
1762
|
+
sx = s;
|
|
1763
|
+
sy = s;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
const refX = markerRef(el.getAttribute("refX"), vb?.x ?? 0, vb?.width ?? 0);
|
|
1767
|
+
const refY = markerRef(el.getAttribute("refY"), vb?.y ?? 0, vb?.height ?? 0);
|
|
1768
|
+
const k = el.getAttribute("markerUnits") === "userSpaceOnUse" && strokeWidth > 0 ? 1 / strokeWidth : 1;
|
|
1769
|
+
const { orient, reverseAtStart } = markerOrient(el.getAttribute("orient"));
|
|
1770
|
+
const turn = role === "start" && reverseAtStart ? -1 : 1;
|
|
1771
|
+
const base = [turn * sx * k, 0, 0, turn * sy * k, -turn * sx * k * refX, -turn * sy * k * refY];
|
|
1772
|
+
const parts = [];
|
|
1773
|
+
const collect = (parent, m, style) => {
|
|
1774
|
+
for (let i = 0; i < parent.children.length; i++) {
|
|
1775
|
+
const child = parent.children[i];
|
|
1776
|
+
const tag = child.tagName.toLowerCase();
|
|
1777
|
+
const cm = multiply(m, parseTransform(child.getAttribute("transform"), onWarn));
|
|
1778
|
+
const cs = deriveStyle(style, child);
|
|
1779
|
+
if (tag === "g") collect(child, cm, cs);
|
|
1780
|
+
else if (SUPPORTED_LEAF_TAGS.has(tag)) {
|
|
1781
|
+
const path = lowerLeaf(child, tag, cm, onWarn);
|
|
1782
|
+
if (path) parts.push({ path: asPolygon(path), style: cs });
|
|
1783
|
+
} else if (!IGNORED_TAGS.has(tag)) {
|
|
1784
|
+
onWarn(`<marker id="${id}"> child <${child.tagName}> is not supported; skipped`);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
collect(el, base, styleAt(el));
|
|
1789
|
+
if (parts.length === 0) {
|
|
1790
|
+
onWarn(`<marker id="${id}"> draws nothing this kit can read; dropped`);
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
const first = parts[0].style;
|
|
1794
|
+
const fill = markerPaint(first, "fill", gradients, onWarn);
|
|
1795
|
+
const outlinePaint = markerPaint(first, "stroke", gradients, onWarn);
|
|
1796
|
+
const outlineWidth = parseFloat(first["stroke-width"] ?? "1") * Math.sqrt(sx * sy) * k;
|
|
1797
|
+
const outline = outlinePaint !== "none" && outlineWidth > 0 ? { width: outlineWidth, paint: outlinePaint } : false;
|
|
1798
|
+
if (parts.some((p) => JSON.stringify(p.style) !== JSON.stringify(first))) {
|
|
1799
|
+
onWarn(`<marker id="${id}"> paints its parts differently; all take the first part's paint`);
|
|
1800
|
+
}
|
|
1801
|
+
let commandCount = 0, coordCount = 0;
|
|
1802
|
+
for (const p of parts) {
|
|
1803
|
+
commandCount += p.path.commands.length;
|
|
1804
|
+
coordCount += p.path.coords.length;
|
|
1805
|
+
}
|
|
1806
|
+
const commands = new Uint8Array(commandCount);
|
|
1807
|
+
const coords = new Float32Array(coordCount);
|
|
1808
|
+
let ci = 0, pi = 0;
|
|
1809
|
+
for (const p of parts) {
|
|
1810
|
+
commands.set(p.path.commands, ci);
|
|
1811
|
+
ci += p.path.commands.length;
|
|
1812
|
+
coords.set(p.path.coords, pi);
|
|
1813
|
+
pi += p.path.coords.length;
|
|
1814
|
+
}
|
|
1815
|
+
const fillRule = parts[0].path.fillRule ?? "nonzero";
|
|
1816
|
+
const round = (v) => Math.round(v * 1e4) / 1e4 + 0;
|
|
1817
|
+
const hash = shortHash(JSON.stringify([
|
|
1818
|
+
Array.from(commands),
|
|
1819
|
+
Array.from(coords, round),
|
|
1820
|
+
fillRule,
|
|
1821
|
+
fill,
|
|
1822
|
+
outline,
|
|
1823
|
+
typeof orient === "number" ? round(orient) : orient
|
|
1824
|
+
]));
|
|
1825
|
+
const key = id.endsWith(`-${hash}`) ? id : `${id}-${hash}`;
|
|
1826
|
+
return {
|
|
1827
|
+
id: key,
|
|
1828
|
+
path: ({ size }) => {
|
|
1829
|
+
const scaled = new Float32Array(coords.length);
|
|
1830
|
+
for (let i = 0; i < coords.length; i++) scaled[i] = coords[i] * size;
|
|
1831
|
+
return { kind: "polygon", commands, coords: scaled, fillRule };
|
|
1832
|
+
},
|
|
1833
|
+
fill,
|
|
1834
|
+
outline,
|
|
1835
|
+
orient,
|
|
1836
|
+
inset: 0
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1436
1839
|
function serializeSvg(nodes, opts = {}) {
|
|
1437
1840
|
const registry = new PaintServerRegistry();
|
|
1438
1841
|
registerPaintServers(nodes, registry);
|
|
@@ -1440,6 +1843,9 @@ function serializeSvg(nodes, opts = {}) {
|
|
|
1440
1843
|
const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
|
|
1441
1844
|
const namespaces = opts.namespaces ?? {};
|
|
1442
1845
|
const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
|
|
1846
|
+
if (registry.usesPrivateNamespace() && namespaces[WEASEL_NS_PREFIX] == null) {
|
|
1847
|
+
rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
|
|
1848
|
+
}
|
|
1443
1849
|
for (const [prefix, uri] of Object.entries(namespaces)) {
|
|
1444
1850
|
rootAttrs.push(`xmlns:${prefix}="${escapeAttr2(uri)}"`);
|
|
1445
1851
|
}
|
|
@@ -1460,22 +1866,28 @@ function serializeSvg(nodes, opts = {}) {
|
|
|
1460
1866
|
for (const prefix of Object.keys(namespaces)) {
|
|
1461
1867
|
const bucket = opts.documentMeta[prefix];
|
|
1462
1868
|
if (!bucket?.elements) continue;
|
|
1463
|
-
for (const [
|
|
1464
|
-
for (const el of list) docMetaXml += namespacedElementXml(prefix,
|
|
1869
|
+
for (const [localName2, list] of Object.entries(bucket.elements)) {
|
|
1870
|
+
for (const el of list) docMetaXml += namespacedElementXml(prefix, localName2, el);
|
|
1465
1871
|
}
|
|
1466
1872
|
}
|
|
1467
1873
|
}
|
|
1468
1874
|
const defsXml = registry.toDefsXml(opts.onWarn);
|
|
1469
|
-
const
|
|
1875
|
+
const said = /* @__PURE__ */ new Set();
|
|
1876
|
+
const warn = (m) => {
|
|
1877
|
+
if (!opts.onWarn || said.has(m)) return;
|
|
1878
|
+
said.add(m);
|
|
1879
|
+
opts.onWarn(m);
|
|
1880
|
+
};
|
|
1881
|
+
const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn)).join("");
|
|
1470
1882
|
const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
|
|
1471
1883
|
return `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
|
|
1472
1884
|
}
|
|
1473
|
-
function namespacedElementXml(prefix,
|
|
1885
|
+
function namespacedElementXml(prefix, localName2, el) {
|
|
1474
1886
|
const attrs = [];
|
|
1475
1887
|
for (const [name, value] of Object.entries(el.attrs)) {
|
|
1476
1888
|
attrs.push(`${name}="${escapeAttr2(value)}"`);
|
|
1477
1889
|
}
|
|
1478
|
-
const head = attrs.length > 0 ? `<${prefix}:${
|
|
1890
|
+
const head = attrs.length > 0 ? `<${prefix}:${localName2} ${attrs.join(" ")}>` : `<${prefix}:${localName2}>`;
|
|
1479
1891
|
let body = "";
|
|
1480
1892
|
if (el.children) {
|
|
1481
1893
|
for (const [childName, list] of Object.entries(el.children)) {
|
|
@@ -1484,7 +1896,7 @@ function namespacedElementXml(prefix, localName, el) {
|
|
|
1484
1896
|
} else if (el.text != null) {
|
|
1485
1897
|
body = escapeText(el.text);
|
|
1486
1898
|
}
|
|
1487
|
-
return `${head}${body}</${prefix}:${
|
|
1899
|
+
return `${head}${body}</${prefix}:${localName2}>`;
|
|
1488
1900
|
}
|
|
1489
1901
|
function metaAttrsXml(meta, namespaces) {
|
|
1490
1902
|
if (!meta) return "";
|
|
@@ -1504,8 +1916,8 @@ function metaElementsXml(meta, namespaces) {
|
|
|
1504
1916
|
for (const prefix of Object.keys(namespaces)) {
|
|
1505
1917
|
const bucket = meta[prefix];
|
|
1506
1918
|
if (!bucket?.elements) continue;
|
|
1507
|
-
for (const [
|
|
1508
|
-
for (const el of list) out += namespacedElementXml(prefix,
|
|
1919
|
+
for (const [localName2, list] of Object.entries(bucket.elements)) {
|
|
1920
|
+
for (const el of list) out += namespacedElementXml(prefix, localName2, el);
|
|
1509
1921
|
}
|
|
1510
1922
|
}
|
|
1511
1923
|
return out;
|
|
@@ -1513,6 +1925,7 @@ function metaElementsXml(meta, namespaces) {
|
|
|
1513
1925
|
function registerPaintServers(nodes, registry) {
|
|
1514
1926
|
for (const n of nodes) {
|
|
1515
1927
|
if (n.kind === "group") {
|
|
1928
|
+
if (n.clip) registry.clipId(n.clip);
|
|
1516
1929
|
registerPaintServers(n.children, registry);
|
|
1517
1930
|
} else if (n.kind === "path") {
|
|
1518
1931
|
if (n.fill.kind === "gradient") registry.register(n.fill.paint);
|
|
@@ -1544,50 +1957,61 @@ function registerMarkers(stroke, registry) {
|
|
|
1544
1957
|
if (key) registry.markerId(key);
|
|
1545
1958
|
}
|
|
1546
1959
|
}
|
|
1547
|
-
function nodeXml(node, registry, namespaces) {
|
|
1548
|
-
if (node.kind === "group") return groupXml(node, registry, namespaces);
|
|
1549
|
-
if (node.kind === "text") return textXml(node, registry, namespaces);
|
|
1960
|
+
function nodeXml(node, registry, namespaces, warn) {
|
|
1961
|
+
if (node.kind === "group") return groupXml(node, registry, namespaces, warn);
|
|
1962
|
+
if (node.kind === "text") return textXml(node, registry, namespaces, warn);
|
|
1550
1963
|
if (node.kind === "image") return imageXml(node, namespaces);
|
|
1551
|
-
return pathXml(node, registry, namespaces);
|
|
1964
|
+
return pathXml(node, registry, namespaces, warn);
|
|
1965
|
+
}
|
|
1966
|
+
function warnStrokeAlign(align, warn) {
|
|
1967
|
+
if (align && align !== "center") {
|
|
1968
|
+
warn(`a stroke aligned "${align}" of its edge is written centered: SVG has no stroke alignment`);
|
|
1969
|
+
}
|
|
1552
1970
|
}
|
|
1553
1971
|
function imageXml(node, namespaces) {
|
|
1554
|
-
const
|
|
1555
|
-
|
|
1556
|
-
`x="${trimNumber(node.x)}"`,
|
|
1557
|
-
`y="${trimNumber(node.y)}"`,
|
|
1558
|
-
`width="${trimNumber(node.width)}"`,
|
|
1559
|
-
`height="${trimNumber(node.height)}"`,
|
|
1560
|
-
`preserveAspectRatio="none"`
|
|
1561
|
-
];
|
|
1972
|
+
const box = `x="${trimNumber(node.x)}" y="${trimNumber(node.y)}" width="${trimNumber(node.width)}" height="${trimNumber(node.height)}"`;
|
|
1973
|
+
const outer = [];
|
|
1562
1974
|
if (node.opacity != null && node.opacity !== 1) {
|
|
1563
|
-
|
|
1975
|
+
outer.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
1564
1976
|
}
|
|
1565
1977
|
if (node.rotation != null && node.rotation !== 0) {
|
|
1566
1978
|
const cx = node.x + node.width / 2;
|
|
1567
1979
|
const cy = node.y + node.height / 2;
|
|
1568
1980
|
const deg = node.rotation * 180 / Math.PI;
|
|
1569
|
-
|
|
1981
|
+
outer.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
|
|
1570
1982
|
}
|
|
1571
1983
|
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
1572
1984
|
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1985
|
+
const href = `href="${escapeAttr2(node.href)}"`;
|
|
1986
|
+
if (!node.source && !node.flipX && !node.flipY) {
|
|
1987
|
+
const attrs = [href, box, 'preserveAspectRatio="none"', ...outer].join(" ");
|
|
1988
|
+
if (metaEls) return `<image ${attrs}${metaAttrs}>${metaEls}</image>`;
|
|
1989
|
+
return `<image ${attrs}${metaAttrs}/>`;
|
|
1990
|
+
}
|
|
1991
|
+
const src = node.source ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
1992
|
+
const viewBox = [src.x, src.y, src.width, src.height].map(trimNumber).join(" ");
|
|
1993
|
+
const flip = node.flipX || node.flipY ? ` transform="matrix(${node.flipX ? -1 : 1} 0 0 ${node.flipY ? -1 : 1} ${trimNumber(node.flipX ? 2 * src.x + src.width : 0)} ${trimNumber(node.flipY ? 2 * src.y + src.height : 0)})"` : "";
|
|
1994
|
+
const head = ['<g data-weasel-image="true"', ...outer].join(" ");
|
|
1995
|
+
return `${head}${metaAttrs}><svg ${box} viewBox="${viewBox}" preserveAspectRatio="none"><image ${href} x="0" y="0" width="1" height="1" preserveAspectRatio="none"${flip}/></svg>${metaEls}</g>`;
|
|
1996
|
+
}
|
|
1997
|
+
function groupXml(node, registry, namespaces, warn) {
|
|
1577
1998
|
const attrs = [];
|
|
1578
1999
|
if (node.transform) {
|
|
1579
2000
|
const m = formatMatrix(node.transform);
|
|
1580
2001
|
if (m) attrs.push(`transform="${m}"`);
|
|
1581
2002
|
}
|
|
2003
|
+
if (node.clip) {
|
|
2004
|
+
attrs.push(`clip-path="url(#${registry.clipId(node.clip)})"`);
|
|
2005
|
+
}
|
|
1582
2006
|
if (node.opacity != null && node.opacity !== 1) {
|
|
1583
2007
|
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
1584
2008
|
}
|
|
1585
2009
|
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
1586
2010
|
const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
|
|
1587
|
-
const body = node.children.map((c) => nodeXml(c, registry, namespaces)).join("");
|
|
2011
|
+
const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn)).join("");
|
|
1588
2012
|
return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
|
|
1589
2013
|
}
|
|
1590
|
-
function pathXml(node, registry, namespaces) {
|
|
2014
|
+
function pathXml(node, registry, namespaces, warn) {
|
|
1591
2015
|
const attrs = [`d="${serializePathD(node.path)}"`];
|
|
1592
2016
|
const fillAttrs = paintAttrs(node.fill, "fill", registry);
|
|
1593
2017
|
for (const a of fillAttrs) attrs.push(a);
|
|
@@ -1595,7 +2019,7 @@ function pathXml(node, registry, namespaces) {
|
|
|
1595
2019
|
attrs.push(`fill-rule="evenodd"`);
|
|
1596
2020
|
}
|
|
1597
2021
|
if (node.stroke) {
|
|
1598
|
-
const strokeAttrs = strokeAttrsFor(node.stroke, registry);
|
|
2022
|
+
const strokeAttrs = strokeAttrsFor(node.stroke, registry, warn);
|
|
1599
2023
|
for (const a of strokeAttrs) attrs.push(a);
|
|
1600
2024
|
} else {
|
|
1601
2025
|
attrs.push('stroke="none"');
|
|
@@ -1626,18 +2050,18 @@ function paintAttrs(paint, name, registry, includeOpacity = true) {
|
|
|
1626
2050
|
}
|
|
1627
2051
|
return out;
|
|
1628
2052
|
}
|
|
1629
|
-
|
|
1630
|
-
return [`${name}="url(#${id})"`];
|
|
2053
|
+
return [`${name}="${registry.ref(paint.paint)}"`];
|
|
1631
2054
|
}
|
|
1632
2055
|
function strokeWidthAttrs(width) {
|
|
1633
2056
|
if (typeof width === "number") return [`stroke-width="${trimNumber(width)}"`];
|
|
1634
2057
|
return [`stroke-width="${trimNumber(width.px)}"`, 'vector-effect="non-scaling-stroke"'];
|
|
1635
2058
|
}
|
|
1636
|
-
function coreStrokeAttrs(stroke, registry) {
|
|
2059
|
+
function coreStrokeAttrs(stroke, registry, warn) {
|
|
1637
2060
|
const paint = stroke?.paint;
|
|
1638
2061
|
if (!stroke || !paint) return [];
|
|
1639
2062
|
const width = stroke.width ?? 1;
|
|
1640
2063
|
if (!((typeof width === "object" ? width.px : width) > 0)) return [];
|
|
2064
|
+
warnStrokeAlign(stroke.align, warn);
|
|
1641
2065
|
const attrs = [];
|
|
1642
2066
|
if ("color" in paint) {
|
|
1643
2067
|
attrs.push(`stroke="${paint.color}"`);
|
|
@@ -1645,7 +2069,7 @@ function coreStrokeAttrs(stroke, registry) {
|
|
|
1645
2069
|
attrs.push(`stroke-opacity="${trimNumber(paint.opacity)}"`);
|
|
1646
2070
|
}
|
|
1647
2071
|
} else {
|
|
1648
|
-
attrs.push(`stroke="
|
|
2072
|
+
attrs.push(`stroke="${registry.ref(paint)}"`);
|
|
1649
2073
|
}
|
|
1650
2074
|
attrs.push(...strokeWidthAttrs(width));
|
|
1651
2075
|
if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
|
|
@@ -1668,7 +2092,8 @@ function coreStrokeAttrs(stroke, registry) {
|
|
|
1668
2092
|
}
|
|
1669
2093
|
return attrs;
|
|
1670
2094
|
}
|
|
1671
|
-
function strokeAttrsFor(stroke, registry) {
|
|
2095
|
+
function strokeAttrsFor(stroke, registry, warn) {
|
|
2096
|
+
warnStrokeAlign(stroke.align, warn);
|
|
1672
2097
|
const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
|
|
1673
2098
|
attrs.push(...strokeWidthAttrs(stroke.width));
|
|
1674
2099
|
const opacity = stroke.opacity ?? (stroke.paint.kind === "solid" ? stroke.paint.opacity : void 0);
|
|
@@ -1735,7 +2160,7 @@ function rotateAboutCenter(ctm, b, angle) {
|
|
|
1735
2160
|
const s = Math.sin(angle);
|
|
1736
2161
|
return multiply(ctm, [c, s, -s, c, cx - cx * c + cy * s, cy - cx * s - cy * c]);
|
|
1737
2162
|
}
|
|
1738
|
-
function textXml(node, registry, namespaces) {
|
|
2163
|
+
function textXml(node, registry, namespaces, warn) {
|
|
1739
2164
|
const attrs = [
|
|
1740
2165
|
`x="${trimNumber(node.x + anchorOffset(node.style, node.width))}"`,
|
|
1741
2166
|
`y="${trimNumber(node.y)}"`,
|
|
@@ -1747,7 +2172,7 @@ function textXml(node, registry, namespaces) {
|
|
|
1747
2172
|
`data-weasel-height="${trimNumber(node.height)}"`
|
|
1748
2173
|
];
|
|
1749
2174
|
const style = node.style;
|
|
1750
|
-
if (style?.fontSize != null) attrs.push(`font-size="${trimNumber(style.fontSize)}"`);
|
|
2175
|
+
if (style?.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(style.fontSize, 1))}"`);
|
|
1751
2176
|
if (style?.fontFamily) attrs.push(`font-family="${escapeAttr2(style.fontFamily)}"`);
|
|
1752
2177
|
if (style?.fontWeight != null) attrs.push(`font-weight="${String(style.fontWeight)}"`);
|
|
1753
2178
|
if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
|
|
@@ -1759,11 +2184,18 @@ function textXml(node, registry, namespaces) {
|
|
|
1759
2184
|
if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
|
|
1760
2185
|
}
|
|
1761
2186
|
if (style?.letterSpacing != null && style.letterSpacing !== 0) {
|
|
1762
|
-
attrs.push(`letter-spacing="${trimNumber(style.letterSpacing)}"`);
|
|
2187
|
+
attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(style.letterSpacing, 1))}"`);
|
|
1763
2188
|
}
|
|
1764
2189
|
const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
|
|
1765
2190
|
if (decoration) attrs.push(`text-decoration="${decoration}"`);
|
|
1766
|
-
if (style?.wrap)
|
|
2191
|
+
if (style?.wrap) {
|
|
2192
|
+
attrs.push('data-weasel-wrap="true"');
|
|
2193
|
+
warn("text that wraps at its box width is written unwrapped: SVG text does not wrap, so other readers draw one line per hard break (weasel reads the wrap back)");
|
|
2194
|
+
}
|
|
2195
|
+
if (node.verticalAlign && node.verticalAlign !== "top") {
|
|
2196
|
+
attrs.push(`data-weasel-vertical-align="${node.verticalAlign}"`);
|
|
2197
|
+
warn(`text aligned to the ${node.verticalAlign} of its box is written top-aligned: SVG text has no box, so other readers draw it at the top (weasel reads the alignment back)`);
|
|
2198
|
+
}
|
|
1767
2199
|
if (node.fill === null) {
|
|
1768
2200
|
attrs.push('fill="none"');
|
|
1769
2201
|
} else if (node.fill) {
|
|
@@ -1773,11 +2205,10 @@ function textXml(node, registry, namespaces) {
|
|
|
1773
2205
|
attrs.push(`fill-opacity="${trimNumber(node.fill.opacity)}"`);
|
|
1774
2206
|
}
|
|
1775
2207
|
} else {
|
|
1776
|
-
|
|
1777
|
-
attrs.push(`fill="url(#${id})"`);
|
|
2208
|
+
attrs.push(`fill="${registry.ref(node.fill)}"`);
|
|
1778
2209
|
}
|
|
1779
2210
|
}
|
|
1780
|
-
for (const a of coreStrokeAttrs(node.stroke, registry)) attrs.push(a);
|
|
2211
|
+
for (const a of coreStrokeAttrs(node.stroke, registry, warn)) attrs.push(a);
|
|
1781
2212
|
if (node.opacity != null && node.opacity !== 1) {
|
|
1782
2213
|
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
1783
2214
|
}
|
|
@@ -1787,19 +2218,19 @@ function textXml(node, registry, namespaces) {
|
|
|
1787
2218
|
const deg = node.rotation * 180 / Math.PI;
|
|
1788
2219
|
attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
|
|
1789
2220
|
}
|
|
1790
|
-
const body = node.runs && node.runs.length > 0 ? node.runs.map((r) => runXml(r, registry)).join("") : escapeText(node.text);
|
|
2221
|
+
const body = node.runs && node.runs.length > 0 ? node.runs.map((r) => runXml(r, registry, warn)).join("") : escapeText(node.text);
|
|
1791
2222
|
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
1792
2223
|
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
1793
2224
|
return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
|
|
1794
2225
|
}
|
|
1795
|
-
function runXml(run, registry) {
|
|
2226
|
+
function runXml(run, registry, warn) {
|
|
1796
2227
|
const attrs = [];
|
|
1797
2228
|
if (run.bold) attrs.push(`font-weight="700"`);
|
|
1798
2229
|
if (run.italic) attrs.push(`font-style="italic"`);
|
|
1799
2230
|
if (run.fontFamily) attrs.push(`font-family="${escapeAttr2(run.fontFamily)}"`);
|
|
1800
2231
|
const presetScale = run.script != null && run.baselineShift != null ? SCRIPT_METRICS[run.script].size : void 0;
|
|
1801
2232
|
const scale = run.fontScale ?? presetScale;
|
|
1802
|
-
if (run.fontSize != null) attrs.push(`font-size="${trimNumber(run.fontSize)}"`);
|
|
2233
|
+
if (run.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(run.fontSize, 1))}"`);
|
|
1803
2234
|
else if (scale != null) attrs.push(`font-size="${trimNumber(scale * 100)}%"`);
|
|
1804
2235
|
if (run.baselineShift != null) {
|
|
1805
2236
|
attrs.push(`baseline-shift="${trimNumber(run.baselineShift * 100)}%"`);
|
|
@@ -1807,7 +2238,7 @@ function runXml(run, registry) {
|
|
|
1807
2238
|
attrs.push(`baseline-shift="${run.script}"`);
|
|
1808
2239
|
}
|
|
1809
2240
|
if (run.letterSpacing != null) {
|
|
1810
|
-
attrs.push(`letter-spacing="${trimNumber(run.letterSpacing)}"`);
|
|
2241
|
+
attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(run.letterSpacing, 1))}"`);
|
|
1811
2242
|
}
|
|
1812
2243
|
const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
|
|
1813
2244
|
if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
|
|
@@ -1815,11 +2246,10 @@ function runXml(run, registry) {
|
|
|
1815
2246
|
if ("color" in run.fill) {
|
|
1816
2247
|
attrs.push(`fill="${run.fill.color}"`);
|
|
1817
2248
|
} else {
|
|
1818
|
-
|
|
1819
|
-
attrs.push(`fill="url(#${id})"`);
|
|
2249
|
+
attrs.push(`fill="${registry.ref(run.fill)}"`);
|
|
1820
2250
|
}
|
|
1821
2251
|
}
|
|
1822
|
-
for (const a of coreStrokeAttrs(run.stroke, registry)) attrs.push(a);
|
|
2252
|
+
for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
|
|
1823
2253
|
const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
|
|
1824
2254
|
return `${head}${escapeText(run.text)}</tspan>`;
|
|
1825
2255
|
}
|
|
@@ -1946,6 +2376,7 @@ function svgNodesToKitDrafts(nodes, nextId) {
|
|
|
1946
2376
|
data: {
|
|
1947
2377
|
text: n.text,
|
|
1948
2378
|
...n.style ? { style: n.style } : {},
|
|
2379
|
+
...n.verticalAlign ? { verticalAlign: n.verticalAlign } : {},
|
|
1949
2380
|
...n.runs ? { runs: n.runs.map((r) => runInBoxFrame(r, box)) } : {},
|
|
1950
2381
|
// `!== undefined`, not a truthiness test: `null` is the document
|
|
1951
2382
|
// saying `fill="none"`, and absent takes the painter's default.
|
|
@@ -1996,13 +2427,14 @@ function svgNodesToKitDrafts(nodes, nextId) {
|
|
|
1996
2427
|
function scaleTextData(data, scale) {
|
|
1997
2428
|
if (scale === 1 || typeof data.text !== "string") return data;
|
|
1998
2429
|
const style = data.style ?? {};
|
|
2430
|
+
const scaleSize = (v) => typeof v === "number" ? v * scale : v;
|
|
1999
2431
|
const runs = data.runs;
|
|
2000
2432
|
return {
|
|
2001
2433
|
...data,
|
|
2002
2434
|
...runs ? {
|
|
2003
|
-
runs: runs.map((r) => r.fontSize !== void 0 ? { ...r, fontSize: r.fontSize
|
|
2435
|
+
runs: runs.map((r) => r.fontSize !== void 0 ? { ...r, fontSize: scaleSize(r.fontSize) } : r)
|
|
2004
2436
|
} : {},
|
|
2005
|
-
style: { ...style, fontSize: resolveTextStyle(style).fontSize
|
|
2437
|
+
style: { ...style, fontSize: scaleSize(style.fontSize ?? resolveTextStyle(style).fontSize) }
|
|
2006
2438
|
};
|
|
2007
2439
|
}
|
|
2008
2440
|
function unionRect(a, b) {
|
|
@@ -2020,6 +2452,7 @@ async function unpackSvgFiles(files, ctx) {
|
|
|
2020
2452
|
const text = await readFileText(file);
|
|
2021
2453
|
const parsed = parseSvg(text);
|
|
2022
2454
|
for (const w of parsed.warnings) dwarn("ingest", `svg "${file.name}":`, w);
|
|
2455
|
+
for (const m of parsed.markers ?? []) if (getMarker(m.id) === void 0) registerMarker(m);
|
|
2023
2456
|
let drafts = svgNodesToKitDrafts(parsed.nodes, freshSvgNodeId);
|
|
2024
2457
|
if (drafts.length === 0) {
|
|
2025
2458
|
console.warn(`weasel ingest: svg "${file.name}" parsed to no drawable nodes`);
|