@weasel-js/svg 1.0.3 → 1.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/dist/index.js +240 -111
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -78,6 +78,16 @@ function parseTransform(attr, onWarn) {
|
|
|
78
78
|
}
|
|
79
79
|
return result;
|
|
80
80
|
}
|
|
81
|
+
function invertMatrix(m) {
|
|
82
|
+
const det = m[0] * m[3] - m[1] * m[2];
|
|
83
|
+
if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return null;
|
|
84
|
+
const a = m[3] / det, b = -m[1] / det, c = -m[2] / det, d = m[0] / det;
|
|
85
|
+
return [a, b, c, d, -(a * m[4] + c * m[5]), -(b * m[4] + d * m[5])];
|
|
86
|
+
}
|
|
87
|
+
function rebaseTransform(ctm, local) {
|
|
88
|
+
const inv = invertMatrix(ctm);
|
|
89
|
+
return inv ? multiply(multiply(ctm, local), inv) : local;
|
|
90
|
+
}
|
|
81
91
|
function decomposeRotation(m, cx, cy, eps = 1e-4) {
|
|
82
92
|
const [a, b, c, d, e, f] = m;
|
|
83
93
|
if (Math.abs(a - d) > eps) return null;
|
|
@@ -243,6 +253,22 @@ function parsePaintAttr(raw) {
|
|
|
243
253
|
return null;
|
|
244
254
|
}
|
|
245
255
|
}
|
|
256
|
+
|
|
257
|
+
// src/elements.ts
|
|
258
|
+
function collectElementsByTag(root, tags) {
|
|
259
|
+
const out = /* @__PURE__ */ new Map();
|
|
260
|
+
const consider = (el) => {
|
|
261
|
+
if (!tags.has(el.tagName.toLowerCase())) return;
|
|
262
|
+
const id = el.getAttribute("id");
|
|
263
|
+
if (id && !out.has(id)) out.set(id, el);
|
|
264
|
+
};
|
|
265
|
+
consider(root);
|
|
266
|
+
const all = root.getElementsByTagName("*");
|
|
267
|
+
for (let i = 0; i < all.length; i++) consider(all[i]);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/patterns.ts
|
|
246
272
|
function patternSpecOf(paint) {
|
|
247
273
|
return "tile" in paint.pattern ? paint.pattern : null;
|
|
248
274
|
}
|
|
@@ -295,20 +321,13 @@ function shapeXml(shape) {
|
|
|
295
321
|
}
|
|
296
322
|
function collectPatterns(svg, onWarn) {
|
|
297
323
|
const out = /* @__PURE__ */ new Map();
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
for (let i = 0; i < root.children.length; i++) {
|
|
302
|
-
const child = root.children[i];
|
|
303
|
-
if (child.tagName.toLowerCase() !== "pattern") continue;
|
|
304
|
-
const id = child.getAttribute("id");
|
|
305
|
-
if (!id) continue;
|
|
306
|
-
const paint = readPattern(child, onWarn);
|
|
307
|
-
if (paint) out.set(id, paint);
|
|
308
|
-
}
|
|
324
|
+
for (const [id, el] of collectElementsByTag(svg, PATTERN_TAGS)) {
|
|
325
|
+
const paint = readPattern(el, onWarn);
|
|
326
|
+
if (paint) out.set(id, paint);
|
|
309
327
|
}
|
|
310
328
|
return out;
|
|
311
329
|
}
|
|
330
|
+
var PATTERN_TAGS = /* @__PURE__ */ new Set(["pattern"]);
|
|
312
331
|
function readPattern(el, onWarn) {
|
|
313
332
|
const raw = el.getAttribute("data-weasel-tile");
|
|
314
333
|
if (!raw) {
|
|
@@ -337,41 +356,69 @@ function escapeAttr(s) {
|
|
|
337
356
|
}
|
|
338
357
|
|
|
339
358
|
// src/gradients.ts
|
|
359
|
+
var GRADIENT_TAGS = /* @__PURE__ */ new Set(["lineargradient", "radialgradient"]);
|
|
360
|
+
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
340
361
|
function collectGradients(svg, onWarn) {
|
|
341
362
|
const out = /* @__PURE__ */ new Map();
|
|
363
|
+
const elements = collectElementsByTag(svg, GRADIENT_TAGS);
|
|
364
|
+
for (const [id, el] of elements) {
|
|
365
|
+
const paint = el.tagName.toLowerCase() === "lineargradient" ? readLinearGradient(el, elements, onWarn) : readRadialGradient(el, elements, onWarn);
|
|
366
|
+
if (paint) out.set(id, paint);
|
|
367
|
+
}
|
|
368
|
+
warnUnsupportedDefsChildren(svg, onWarn);
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
function warnUnsupportedDefsChildren(svg, onWarn) {
|
|
372
|
+
if (!onWarn) return;
|
|
342
373
|
const defs = svg.getElementsByTagName("defs");
|
|
343
374
|
for (let d = 0; d < defs.length; d++) {
|
|
344
375
|
const root = defs[d];
|
|
345
376
|
for (let i = 0; i < root.children.length; i++) {
|
|
346
377
|
const child = root.children[i];
|
|
347
378
|
const tag = child.tagName.toLowerCase();
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (tag === "lineargradient") {
|
|
351
|
-
const paint = readLinearGradient(child, onWarn);
|
|
352
|
-
if (paint) out.set(id, paint);
|
|
353
|
-
} else if (tag === "radialgradient") {
|
|
354
|
-
const paint = readRadialGradient(child, onWarn);
|
|
355
|
-
if (paint) out.set(id, paint);
|
|
356
|
-
} else if (tag !== "pattern") {
|
|
357
|
-
onWarn?.(`unsupported <defs> child: <${child.tagName}>`);
|
|
358
|
-
}
|
|
379
|
+
if (GRADIENT_TAGS.has(tag) || tag === "pattern") continue;
|
|
380
|
+
onWarn(`unsupported <defs> child: <${child.tagName}>`);
|
|
359
381
|
}
|
|
360
382
|
}
|
|
361
|
-
|
|
383
|
+
}
|
|
384
|
+
function hrefTarget(el, elements) {
|
|
385
|
+
const raw = el.getAttribute("href") ?? el.getAttributeNS(XLINK_NS, "href") ?? el.getAttribute("xlink:href");
|
|
386
|
+
if (!raw || !raw.startsWith("#")) return null;
|
|
387
|
+
return elements.get(raw.slice(1)) ?? null;
|
|
388
|
+
}
|
|
389
|
+
function inheritedAttr(el, elements, name, seen = /* @__PURE__ */ new Set()) {
|
|
390
|
+
if (el.hasAttribute(name)) return el.getAttribute(name);
|
|
391
|
+
seen.add(el);
|
|
392
|
+
const ref = hrefTarget(el, elements);
|
|
393
|
+
if (!ref || seen.has(ref)) return null;
|
|
394
|
+
return inheritedAttr(ref, elements, name, seen);
|
|
395
|
+
}
|
|
396
|
+
function inheritedStops(el, elements, onWarn, seen = /* @__PURE__ */ new Set()) {
|
|
397
|
+
const own = readStops(el, onWarn);
|
|
398
|
+
if (own.length > 0) return own;
|
|
399
|
+
seen.add(el);
|
|
400
|
+
const ref = hrefTarget(el, elements);
|
|
401
|
+
if (!ref || seen.has(ref)) return own;
|
|
402
|
+
return inheritedStops(ref, elements, onWarn, seen);
|
|
403
|
+
}
|
|
404
|
+
function parseRatio(raw) {
|
|
405
|
+
const n = parseFloat(raw);
|
|
406
|
+
if (!Number.isFinite(n)) return NaN;
|
|
407
|
+
return raw.trimEnd().endsWith("%") ? n / 100 : n;
|
|
362
408
|
}
|
|
363
409
|
function readStops(el, onWarn) {
|
|
364
410
|
const stops = [];
|
|
365
411
|
for (let i = 0; i < el.children.length; i++) {
|
|
366
412
|
const c = el.children[i];
|
|
367
413
|
if (c.tagName.toLowerCase() !== "stop") continue;
|
|
368
|
-
const offsetRaw = c.getAttribute("offset") ?? "0";
|
|
369
|
-
const offset =
|
|
414
|
+
const offsetRaw = parseRatio(c.getAttribute("offset") ?? "0");
|
|
415
|
+
const offset = Number.isFinite(offsetRaw) ? offsetRaw : 0;
|
|
370
416
|
const colorAttr = c.getAttribute("stop-color") ?? "#000000";
|
|
371
417
|
const parsed = parsePaintAttr(colorAttr);
|
|
372
418
|
if (parsed && parsed.kind === "solid") {
|
|
373
419
|
const opacityAttr = c.getAttribute("stop-opacity");
|
|
374
|
-
const
|
|
420
|
+
const own = opacityAttr != null ? parseRatio(opacityAttr) : NaN;
|
|
421
|
+
const alpha = Number.isFinite(own) ? own : parsed.alpha;
|
|
375
422
|
stops.push({ offset, color: alpha < 1 ? applyAlpha(parsed.color, alpha) : parsed.color });
|
|
376
423
|
} else {
|
|
377
424
|
onWarn?.(`gradient stop has unrecognized stop-color: ${colorAttr}`);
|
|
@@ -384,34 +431,47 @@ function applyAlpha(hex, alpha) {
|
|
|
384
431
|
const a = Math.max(0, Math.min(255, Math.round(alpha * 255)));
|
|
385
432
|
return `${hex}${a.toString(16).padStart(2, "0")}`;
|
|
386
433
|
}
|
|
387
|
-
function readGradientUnits(el) {
|
|
388
|
-
return el
|
|
434
|
+
function readGradientUnits(el, elements) {
|
|
435
|
+
return inheritedAttr(el, elements, "gradientUnits") === "userSpaceOnUse" ? "world" : "bounds";
|
|
389
436
|
}
|
|
390
|
-
function
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
437
|
+
function warnGradientTransform(el, elements, onWarn) {
|
|
438
|
+
const t = inheritedAttr(el, elements, "gradientTransform");
|
|
439
|
+
if (t != null && t.trim() !== "") {
|
|
440
|
+
onWarn?.(`gradientTransform="${t}" is not modeled; the gradient paints untransformed`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function num(raw, fallback) {
|
|
444
|
+
if (raw == null) return fallback;
|
|
445
|
+
const n = parseRatio(raw);
|
|
446
|
+
return Number.isFinite(n) ? n : fallback;
|
|
447
|
+
}
|
|
448
|
+
function readLinearGradient(el, elements, onWarn) {
|
|
449
|
+
warnGradientTransform(el, elements, onWarn);
|
|
396
450
|
return {
|
|
397
451
|
fill: "linear-gradient",
|
|
398
|
-
from: {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
452
|
+
from: {
|
|
453
|
+
x: num(inheritedAttr(el, elements, "x1"), 0),
|
|
454
|
+
y: num(inheritedAttr(el, elements, "y1"), 0)
|
|
455
|
+
},
|
|
456
|
+
to: {
|
|
457
|
+
x: num(inheritedAttr(el, elements, "x2"), 1),
|
|
458
|
+
y: num(inheritedAttr(el, elements, "y2"), 0)
|
|
459
|
+
},
|
|
460
|
+
stops: inheritedStops(el, elements, onWarn),
|
|
461
|
+
units: readGradientUnits(el, elements)
|
|
402
462
|
};
|
|
403
463
|
}
|
|
404
|
-
function readRadialGradient(el, onWarn) {
|
|
405
|
-
|
|
406
|
-
const cy = parseFloat(el.getAttribute("cy") ?? "0.5");
|
|
407
|
-
const r = parseFloat(el.getAttribute("r") ?? "0.5");
|
|
408
|
-
const stops = readStops(el, onWarn);
|
|
464
|
+
function readRadialGradient(el, elements, onWarn) {
|
|
465
|
+
warnGradientTransform(el, elements, onWarn);
|
|
409
466
|
return {
|
|
410
467
|
fill: "radial-gradient",
|
|
411
|
-
center: {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
468
|
+
center: {
|
|
469
|
+
x: num(inheritedAttr(el, elements, "cx"), 0.5),
|
|
470
|
+
y: num(inheritedAttr(el, elements, "cy"), 0.5)
|
|
471
|
+
},
|
|
472
|
+
radius: num(inheritedAttr(el, elements, "r"), 0.5),
|
|
473
|
+
stops: inheritedStops(el, elements, onWarn),
|
|
474
|
+
units: readGradientUnits(el, elements)
|
|
415
475
|
};
|
|
416
476
|
}
|
|
417
477
|
var PaintServerRegistry = class {
|
|
@@ -523,7 +583,15 @@ var SUPPORTED_LEAF_TAGS = /* @__PURE__ */ new Set([
|
|
|
523
583
|
"path"
|
|
524
584
|
]);
|
|
525
585
|
var SUPPORTED_GROUP_TAGS = /* @__PURE__ */ new Set(["g", "svg"]);
|
|
526
|
-
var IGNORED_TAGS = /* @__PURE__ */ new Set([
|
|
586
|
+
var IGNORED_TAGS = /* @__PURE__ */ new Set([
|
|
587
|
+
"defs",
|
|
588
|
+
"lineargradient",
|
|
589
|
+
"radialgradient",
|
|
590
|
+
"pattern",
|
|
591
|
+
"title",
|
|
592
|
+
"desc",
|
|
593
|
+
"metadata"
|
|
594
|
+
]);
|
|
527
595
|
function parseSvg(svg, opts = {}) {
|
|
528
596
|
const warnings = [];
|
|
529
597
|
const onWarn = (m) => {
|
|
@@ -557,16 +625,10 @@ function parseSvg(svg, opts = {}) {
|
|
|
557
625
|
if (documentMeta) result.documentMeta = documentMeta;
|
|
558
626
|
const viewBox = parseViewBoxAttr(root.getAttribute("viewBox"));
|
|
559
627
|
if (viewBox) result.viewBox = viewBox;
|
|
560
|
-
const
|
|
561
|
-
if (
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
const heightAttr = root.getAttribute("height");
|
|
566
|
-
if (heightAttr != null) {
|
|
567
|
-
const n = parseFloat(heightAttr);
|
|
568
|
-
if (Number.isFinite(n)) result.height = n;
|
|
569
|
-
}
|
|
628
|
+
const width = parseRootLength(root.getAttribute("width"), "width", onWarn);
|
|
629
|
+
if (width != null) result.width = width;
|
|
630
|
+
const height = parseRootLength(root.getAttribute("height"), "height", onWarn);
|
|
631
|
+
if (height != null) result.height = height;
|
|
570
632
|
for (let i = 0; i < root.children.length; i++) {
|
|
571
633
|
const c = root.children[i];
|
|
572
634
|
if (c.tagName.toLowerCase() === "title") {
|
|
@@ -584,6 +646,14 @@ function parseViewBoxAttr(raw) {
|
|
|
584
646
|
if (nums.some((n) => !Number.isFinite(n))) return void 0;
|
|
585
647
|
return { x: nums[0], y: nums[1], width: nums[2], height: nums[3] };
|
|
586
648
|
}
|
|
649
|
+
function parseRootLength(raw, name, onWarn) {
|
|
650
|
+
if (raw == null) return void 0;
|
|
651
|
+
const n = parseFloat(raw);
|
|
652
|
+
if (!Number.isFinite(n)) return void 0;
|
|
653
|
+
const unit = /^\s*[-+]?[\d.eE+-]+\s*([a-z%]+)\s*$/i.exec(raw)?.[1];
|
|
654
|
+
if (unit) onWarn(`<svg ${name}="${raw}"> unit "${unit}" is not converted; read as ${n}`);
|
|
655
|
+
return n;
|
|
656
|
+
}
|
|
587
657
|
function collectDocumentMeta(root, uriToPrefix) {
|
|
588
658
|
if (uriToPrefix.size === 0) return void 0;
|
|
589
659
|
const meta = {};
|
|
@@ -684,7 +754,13 @@ function parseElement(el, ctm, style, gradients, onWarn, uriToPrefix) {
|
|
|
684
754
|
}
|
|
685
755
|
if (SUPPORTED_GROUP_TAGS.has(tag)) {
|
|
686
756
|
const childStyle = deriveStyle(style, el);
|
|
687
|
-
|
|
757
|
+
const x = parseFloat(el.getAttribute("x") ?? "0");
|
|
758
|
+
const y = parseFloat(el.getAttribute("y") ?? "0");
|
|
759
|
+
const offset = [1, 0, 0, 1, Number.isFinite(x) ? x : 0, Number.isFinite(y) ? y : 0];
|
|
760
|
+
if (el.hasAttribute("viewBox")) {
|
|
761
|
+
onWarn("nested <svg> viewBox is not modeled; its children are not rescaled");
|
|
762
|
+
}
|
|
763
|
+
return parseChildren(el, multiply(ctm, offset), childStyle, gradients, onWarn, uriToPrefix);
|
|
688
764
|
}
|
|
689
765
|
if (tag === "text") {
|
|
690
766
|
const textNode = parseTextElement(el, ctm, style, gradients, onWarn);
|
|
@@ -711,15 +787,16 @@ function parseElement(el, ctm, style, gradients, onWarn, uriToPrefix) {
|
|
|
711
787
|
if (!path) return null;
|
|
712
788
|
let rotation;
|
|
713
789
|
if (!isIdentity(localTransform)) {
|
|
790
|
+
const worldLocal = rebaseTransform(ctm, localTransform);
|
|
714
791
|
const bounds = pathAabb(path);
|
|
715
792
|
const cx = bounds.x + bounds.width / 2;
|
|
716
793
|
const cy = bounds.y + bounds.height / 2;
|
|
717
|
-
const angle = decomposeRotation(
|
|
794
|
+
const angle = decomposeRotation(worldLocal, cx, cy);
|
|
718
795
|
if (angle != null) {
|
|
719
796
|
rotation = angle;
|
|
720
797
|
} else {
|
|
721
|
-
path = transformPath(path,
|
|
722
|
-
const rotComp = rotationComponent(
|
|
798
|
+
path = transformPath(path, worldLocal);
|
|
799
|
+
const rotComp = rotationComponent(worldLocal);
|
|
723
800
|
if (Math.abs(rotComp) > 1e-4) {
|
|
724
801
|
onWarn(`leaf transform has a rotational component that can't be cleanly stored as rotation; baked into geometry (may not round-trip identically)`);
|
|
725
802
|
}
|
|
@@ -740,30 +817,29 @@ function parseElement(el, ctm, style, gradients, onWarn, uriToPrefix) {
|
|
|
740
817
|
if (tag === "line" && (leafStyle["fill"] ?? null) == null) {
|
|
741
818
|
node.fill = { kind: "none" };
|
|
742
819
|
}
|
|
743
|
-
if (tag === "polyline" && !el.hasAttribute("fill")) ;
|
|
744
820
|
const meta = collectElementMeta(el, uriToPrefix);
|
|
745
821
|
if (meta) node.meta = meta;
|
|
746
822
|
return node;
|
|
747
823
|
}
|
|
748
824
|
function lowerLeaf(el, tag, m, onWarn) {
|
|
749
|
-
const
|
|
825
|
+
const num2 = (name, fallback = 0) => {
|
|
750
826
|
const v = el.getAttribute(name);
|
|
751
827
|
if (v == null) return fallback;
|
|
752
828
|
const n = parseFloat(v);
|
|
753
829
|
return Number.isFinite(n) ? n : fallback;
|
|
754
830
|
};
|
|
755
831
|
if (tag === "rect") {
|
|
756
|
-
const x =
|
|
757
|
-
const y =
|
|
758
|
-
const w =
|
|
759
|
-
const h =
|
|
760
|
-
const rx =
|
|
761
|
-
const ry =
|
|
832
|
+
const x = num2("x", 0);
|
|
833
|
+
const y = num2("y", 0);
|
|
834
|
+
const w = num2("width", 0);
|
|
835
|
+
const h = num2("height", 0);
|
|
836
|
+
const rx = num2("rx", 0);
|
|
837
|
+
const ry = num2("ry", 0);
|
|
762
838
|
return rectElementToPath(x, y, w, h, rx, ry, m);
|
|
763
839
|
}
|
|
764
|
-
if (tag === "circle") return circleToPath(
|
|
765
|
-
if (tag === "ellipse") return ellipseToPath(
|
|
766
|
-
if (tag === "line") return lineToPath(
|
|
840
|
+
if (tag === "circle") return circleToPath(num2("cx"), num2("cy"), num2("r"), m);
|
|
841
|
+
if (tag === "ellipse") return ellipseToPath(num2("cx"), num2("cy"), num2("rx"), num2("ry"), m);
|
|
842
|
+
if (tag === "line") return lineToPath(num2("x1"), num2("y1"), num2("x2"), num2("y2"), m);
|
|
767
843
|
if (tag === "polyline") return polylineToPath(parsePoints(el.getAttribute("points") ?? ""), m);
|
|
768
844
|
if (tag === "polygon") return polygonToPath(parsePoints(el.getAttribute("points") ?? ""), m);
|
|
769
845
|
if (tag === "path") {
|
|
@@ -925,7 +1001,7 @@ function parseDashArray(s) {
|
|
|
925
1001
|
return nums.length % 2 === 1 ? [...nums, ...nums] : nums;
|
|
926
1002
|
}
|
|
927
1003
|
function readOpacityAttr(el, name) {
|
|
928
|
-
const raw = el
|
|
1004
|
+
const raw = ownProp(el, name);
|
|
929
1005
|
if (raw == null) return void 0;
|
|
930
1006
|
const n = parseFloat(raw);
|
|
931
1007
|
return Number.isFinite(n) ? clamp01(n) : void 0;
|
|
@@ -950,22 +1026,22 @@ function parseTextDecoration(raw) {
|
|
|
950
1026
|
if (tokens.includes("line-through")) out.strikethrough = true;
|
|
951
1027
|
return out;
|
|
952
1028
|
}
|
|
953
|
-
var
|
|
1029
|
+
var XLINK_NS2 = "http://www.w3.org/1999/xlink";
|
|
954
1030
|
function parseImageElement(el, ctm, onWarn) {
|
|
955
|
-
const href = el.getAttribute("href") ?? el.getAttributeNS(
|
|
1031
|
+
const href = el.getAttribute("href") ?? el.getAttributeNS(XLINK_NS2, "href") ?? el.getAttribute("xlink:href");
|
|
956
1032
|
if (!href) {
|
|
957
1033
|
onWarn("<image> without href; dropped");
|
|
958
1034
|
return null;
|
|
959
1035
|
}
|
|
960
|
-
const
|
|
1036
|
+
const num2 = (raw, fallback) => {
|
|
961
1037
|
if (raw == null) return fallback;
|
|
962
1038
|
const n = parseFloat(raw);
|
|
963
1039
|
return Number.isFinite(n) ? n : fallback;
|
|
964
1040
|
};
|
|
965
|
-
const rawX =
|
|
966
|
-
const rawY =
|
|
967
|
-
const rawW =
|
|
968
|
-
const rawH =
|
|
1041
|
+
const rawX = num2(el.getAttribute("x"), 0);
|
|
1042
|
+
const rawY = num2(el.getAttribute("y"), 0);
|
|
1043
|
+
const rawW = num2(el.getAttribute("width"), 0);
|
|
1044
|
+
const rawH = num2(el.getAttribute("height"), 0);
|
|
969
1045
|
const px = (x, y) => [ctm[0] * x + ctm[2] * y + ctm[4], ctm[1] * x + ctm[3] * y + ctm[5]];
|
|
970
1046
|
const [x0, y0] = px(rawX, rawY);
|
|
971
1047
|
const [x1, y1] = px(rawX + rawW, rawY + rawH);
|
|
@@ -981,13 +1057,14 @@ function parseImageElement(el, ctm, onWarn) {
|
|
|
981
1057
|
if (opacity != null) node.opacity = opacity;
|
|
982
1058
|
const localTransform = parseTransform(el.getAttribute("transform"), onWarn);
|
|
983
1059
|
if (!isIdentity(localTransform)) {
|
|
1060
|
+
const worldLocal = rebaseTransform(ctm, localTransform);
|
|
984
1061
|
const cx = node.x + node.width / 2;
|
|
985
1062
|
const cy = node.y + node.height / 2;
|
|
986
|
-
const angle = decomposeRotation(
|
|
1063
|
+
const angle = decomposeRotation(worldLocal, cx, cy);
|
|
987
1064
|
if (angle != null) {
|
|
988
1065
|
node.rotation = angle;
|
|
989
1066
|
} else {
|
|
990
|
-
const rotComp = rotationComponent(
|
|
1067
|
+
const rotComp = rotationComponent(worldLocal);
|
|
991
1068
|
if (Math.abs(rotComp) > 1e-4) {
|
|
992
1069
|
onWarn("<image> transform has a rotational component that can't be cleanly stored as rotation; dropped (may not round-trip identically)");
|
|
993
1070
|
}
|
|
@@ -998,16 +1075,52 @@ function parseImageElement(el, ctm, onWarn) {
|
|
|
998
1075
|
}
|
|
999
1076
|
return node;
|
|
1000
1077
|
}
|
|
1078
|
+
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
1079
|
+
function preservesSpace(el) {
|
|
1080
|
+
for (let e = el; e; e = e.parentElement) {
|
|
1081
|
+
const v = e.getAttributeNS(XML_NS, "space") ?? e.getAttribute("xml:space");
|
|
1082
|
+
if (v === "preserve") return true;
|
|
1083
|
+
if (v === "default") return false;
|
|
1084
|
+
}
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
function collapseRunWhitespace(runs) {
|
|
1088
|
+
let atStart = true;
|
|
1089
|
+
let lastWasSpace = false;
|
|
1090
|
+
for (const run of runs) {
|
|
1091
|
+
let out = "";
|
|
1092
|
+
for (const ch of run.text.replace(/\n/g, "").replace(/[\t\r]/g, " ")) {
|
|
1093
|
+
if (ch === " ") {
|
|
1094
|
+
if (atStart || lastWasSpace) continue;
|
|
1095
|
+
out += " ";
|
|
1096
|
+
lastWasSpace = true;
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
out += ch;
|
|
1100
|
+
lastWasSpace = false;
|
|
1101
|
+
atStart = false;
|
|
1102
|
+
}
|
|
1103
|
+
run.text = out;
|
|
1104
|
+
}
|
|
1105
|
+
for (let i = runs.length - 1; i >= 0; i--) {
|
|
1106
|
+
if (runs[i].text === "") continue;
|
|
1107
|
+
if (runs[i].text.endsWith(" ")) runs[i].text = runs[i].text.slice(0, -1);
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
for (let i = runs.length - 1; i >= 0; i--) {
|
|
1111
|
+
if (runs[i].text === "") runs.splice(i, 1);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1001
1114
|
function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
1002
|
-
const
|
|
1115
|
+
const num2 = (raw, fallback) => {
|
|
1003
1116
|
if (raw == null) return fallback;
|
|
1004
1117
|
const n = parseFloat(raw);
|
|
1005
1118
|
return Number.isFinite(n) ? n : fallback;
|
|
1006
1119
|
};
|
|
1007
1120
|
const localTransform = parseTransform(el.getAttribute("transform"), onWarn);
|
|
1008
1121
|
const m = ctm;
|
|
1009
|
-
const rawX =
|
|
1010
|
-
const rawY =
|
|
1122
|
+
const rawX = num2(el.getAttribute("x"), 0);
|
|
1123
|
+
const rawY = num2(el.getAttribute("y"), 0);
|
|
1011
1124
|
const ax = m[0] * rawX + m[2] * rawY + m[4];
|
|
1012
1125
|
const ay = m[1] * rawX + m[3] * rawY + m[5];
|
|
1013
1126
|
const leafStyle = deriveStyle(style, el);
|
|
@@ -1018,40 +1131,35 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
1018
1131
|
const explicitTopAnchor = dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
|
|
1019
1132
|
const topY = explicitTopAnchor ? ay : ay - fontSize;
|
|
1020
1133
|
const runs = [];
|
|
1021
|
-
let plain = "";
|
|
1022
1134
|
for (let i = 0; i < el.childNodes.length; i++) {
|
|
1023
1135
|
const child = el.childNodes[i];
|
|
1024
1136
|
if (child.nodeType === 3) {
|
|
1025
1137
|
const t = child.textContent ?? "";
|
|
1026
1138
|
if (!t) continue;
|
|
1027
1139
|
runs.push({ text: t });
|
|
1028
|
-
plain += t;
|
|
1029
1140
|
} else if (child.nodeType === 1) {
|
|
1030
1141
|
const sp = child;
|
|
1031
1142
|
if (sp.tagName.toLowerCase() !== "tspan") {
|
|
1032
1143
|
onWarn(`<text> child <${sp.tagName}> not supported; flattening text content`);
|
|
1033
1144
|
const t = sp.textContent ?? "";
|
|
1034
|
-
if (t) {
|
|
1035
|
-
runs.push({ text: t });
|
|
1036
|
-
plain += t;
|
|
1037
|
-
}
|
|
1145
|
+
if (t) runs.push({ text: t });
|
|
1038
1146
|
continue;
|
|
1039
1147
|
}
|
|
1040
1148
|
const run = readTspanRun(sp, gradients, leafStyle, onWarn);
|
|
1041
1149
|
runs.push(run);
|
|
1042
|
-
plain += run.text;
|
|
1043
1150
|
}
|
|
1044
1151
|
}
|
|
1045
|
-
|
|
1046
|
-
const
|
|
1152
|
+
if (!preservesSpace(el)) collapseRunWhitespace(runs);
|
|
1153
|
+
const plain = runs.map((r) => r.text).join("");
|
|
1154
|
+
const dataW = num2(el.getAttribute("data-weasel-width"), NaN);
|
|
1155
|
+
const dataH = num2(el.getAttribute("data-weasel-height"), NaN);
|
|
1047
1156
|
const width = Number.isFinite(dataW) ? dataW : UNBOUNDED_TEXT_WIDTH;
|
|
1048
1157
|
const lines = (plain.match(/\n/g)?.length ?? 0) + 1;
|
|
1049
1158
|
const height = Number.isFinite(dataH) ? dataH : fontSize * lineHeight * lines;
|
|
1050
1159
|
const opacity = readOpacityAttr(el, "opacity");
|
|
1051
1160
|
const node = {
|
|
1052
1161
|
kind: "text",
|
|
1053
|
-
x:
|
|
1054
|
-
// x is unchanged by the baseline shift
|
|
1162
|
+
x: ax,
|
|
1055
1163
|
y: topY,
|
|
1056
1164
|
width,
|
|
1057
1165
|
height,
|
|
@@ -1064,13 +1172,14 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
|
|
|
1064
1172
|
if (Object.keys(textStyle).length > 0) node.style = textStyle;
|
|
1065
1173
|
if (opacity != null) node.opacity = opacity;
|
|
1066
1174
|
if (!isIdentity(localTransform)) {
|
|
1175
|
+
const worldLocal = rebaseTransform(ctm, localTransform);
|
|
1067
1176
|
const cx = node.x + node.width / 2;
|
|
1068
1177
|
const cy = node.y + node.height / 2;
|
|
1069
|
-
const angle = decomposeRotation(
|
|
1178
|
+
const angle = decomposeRotation(worldLocal, cx, cy);
|
|
1070
1179
|
if (angle != null) {
|
|
1071
1180
|
node.rotation = angle;
|
|
1072
1181
|
} else {
|
|
1073
|
-
const rotComp = rotationComponent(
|
|
1182
|
+
const rotComp = rotationComponent(worldLocal);
|
|
1074
1183
|
if (Math.abs(rotComp) > 1e-4) {
|
|
1075
1184
|
onWarn(`<text> transform has a rotational component that can't be cleanly stored as rotation; dropped (may not round-trip identically)`);
|
|
1076
1185
|
}
|
|
@@ -1362,11 +1471,11 @@ function pathXml(node, registry, namespaces) {
|
|
|
1362
1471
|
}
|
|
1363
1472
|
return `<path ${attrs.join(" ")}${metaAttrs}/>`;
|
|
1364
1473
|
}
|
|
1365
|
-
function paintAttrs(paint, name, registry) {
|
|
1474
|
+
function paintAttrs(paint, name, registry, includeOpacity = true) {
|
|
1366
1475
|
if (paint.kind === "none") return [`${name}="none"`];
|
|
1367
1476
|
if (paint.kind === "solid") {
|
|
1368
1477
|
const out = [`${name}="${paint.color}"`];
|
|
1369
|
-
if (paint.opacity != null && paint.opacity !== 1) {
|
|
1478
|
+
if (includeOpacity && paint.opacity != null && paint.opacity !== 1) {
|
|
1370
1479
|
out.push(`${name}-opacity="${trimNumber(paint.opacity)}"`);
|
|
1371
1480
|
}
|
|
1372
1481
|
return out;
|
|
@@ -1397,10 +1506,11 @@ function coreStrokeAttrs(stroke, registry) {
|
|
|
1397
1506
|
return attrs;
|
|
1398
1507
|
}
|
|
1399
1508
|
function strokeAttrsFor(stroke, registry) {
|
|
1400
|
-
const attrs = paintAttrs(stroke.paint, "stroke", registry);
|
|
1509
|
+
const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
|
|
1401
1510
|
attrs.push(`stroke-width="${trimNumber(stroke.width)}"`);
|
|
1402
|
-
|
|
1403
|
-
|
|
1511
|
+
const opacity = stroke.opacity ?? (stroke.paint.kind === "solid" ? stroke.paint.opacity : void 0);
|
|
1512
|
+
if (opacity != null && opacity !== 1) {
|
|
1513
|
+
attrs.push(`stroke-opacity="${trimNumber(opacity)}"`);
|
|
1404
1514
|
}
|
|
1405
1515
|
if (stroke.cap) {
|
|
1406
1516
|
attrs.push(`stroke-linecap="${stroke.cap}"`);
|
|
@@ -1418,28 +1528,47 @@ function strokeAttrsFor(stroke, registry) {
|
|
|
1418
1528
|
}
|
|
1419
1529
|
function computeBounds(nodes) {
|
|
1420
1530
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1421
|
-
const visit = (n) => {
|
|
1531
|
+
const visit = (n, ctm) => {
|
|
1422
1532
|
if (n.kind === "group") {
|
|
1423
|
-
n.
|
|
1533
|
+
const childCtm = n.transform ? multiply(ctm, n.transform) : ctm;
|
|
1534
|
+
n.children.forEach((c) => visit(c, childCtm));
|
|
1424
1535
|
return;
|
|
1425
1536
|
}
|
|
1426
1537
|
const b = n.kind === "text" || n.kind === "image" ? { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height } : pathBounds(n.path);
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1538
|
+
const local = n.rotation ? rotateAboutCenter(ctm, b, n.rotation) : ctm;
|
|
1539
|
+
for (const [x, y] of corners(b)) {
|
|
1540
|
+
const px = local[0] * x + local[2] * y + local[4];
|
|
1541
|
+
const py = local[1] * x + local[3] * y + local[5];
|
|
1542
|
+
if (px < minX) minX = px;
|
|
1543
|
+
if (py < minY) minY = py;
|
|
1544
|
+
if (px > maxX) maxX = px;
|
|
1545
|
+
if (py > maxY) maxY = py;
|
|
1546
|
+
}
|
|
1431
1547
|
};
|
|
1432
|
-
nodes.forEach(visit);
|
|
1548
|
+
nodes.forEach((n) => visit(n, IDENTITY_MATRIX));
|
|
1433
1549
|
if (!Number.isFinite(minX)) {
|
|
1434
1550
|
return { x: 0, y: 0, width: 0, height: 0 };
|
|
1435
1551
|
}
|
|
1436
1552
|
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
1437
1553
|
}
|
|
1554
|
+
function corners(b) {
|
|
1555
|
+
return [[b.minX, b.minY], [b.maxX, b.minY], [b.maxX, b.maxY], [b.minX, b.maxY]];
|
|
1556
|
+
}
|
|
1557
|
+
function rotateAboutCenter(ctm, b, angle) {
|
|
1558
|
+
const cx = (b.minX + b.maxX) / 2;
|
|
1559
|
+
const cy = (b.minY + b.maxY) / 2;
|
|
1560
|
+
const c = Math.cos(angle);
|
|
1561
|
+
const s = Math.sin(angle);
|
|
1562
|
+
return multiply(ctm, [c, s, -s, c, cx - cx * c + cy * s, cy - cx * s - cy * c]);
|
|
1563
|
+
}
|
|
1438
1564
|
function textXml(node, registry, namespaces) {
|
|
1439
1565
|
const attrs = [
|
|
1440
1566
|
`x="${trimNumber(node.x)}"`,
|
|
1441
1567
|
`y="${trimNumber(node.y)}"`,
|
|
1442
1568
|
`dominant-baseline="text-before-edge"`,
|
|
1569
|
+
// The runs model stores real line breaks; SVG's default whitespace
|
|
1570
|
+
// handling would collapse them (and any indentation) away on re-import.
|
|
1571
|
+
`xml:space="preserve"`,
|
|
1443
1572
|
`data-weasel-width="${trimNumber(node.width)}"`,
|
|
1444
1573
|
`data-weasel-height="${trimNumber(node.height)}"`
|
|
1445
1574
|
];
|