@weasel-js/svg 1.5.1 → 1.6.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/README.md CHANGED
@@ -91,6 +91,11 @@ decodes it, so an external URL round-trips as a reference and resolves only
91
91
  when something downstream loads it. `unpackSvgFiles` maps the node onto the
92
92
  kit's `kit:image` painter (`data.image.src`), which does load it.
93
93
 
94
+ A source rect and flips (`source`, `flipX`, `flipY`) are written as a nested
95
+ `<svg viewBox>` viewport and read back as the same node. `unpackSvgFiles`
96
+ carries them onto `data.image` under the same names, and `svgImageFromKit`
97
+ writes a `kit:image` leaf back as an `SvgImageNode`.
98
+
94
99
  `preserveAspectRatio` is not modeled. The box is taken literally on the way
95
100
  in (a non-`none` value warns) and written back as `none`, so a source file
96
101
  that relied on letterboxing imports stretched.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { Path, FillStyle, StrokeAlign, StyledRun, TextStyle, TextVerticalAlign, Stroke, MarkerEntry, TilePatternSpec, IngestCtx } from '@weasel-js/core';
1
+ import { Mat3 } from '@weasel-js/geom';
2
+ import { Path, FillStyle, ScreenLength, StrokeAlign, StyledRun, TextStyle, TextVerticalAlign, Stroke, MarkerEntry, TilePatternSpec, ImageNodeData, IngestCtx } from '@weasel-js/core';
2
3
 
3
4
  /**
4
5
  * Public types for `@weasel-js/svg`. The package exposes a flat,
@@ -47,10 +48,15 @@ interface NamespacedElement {
47
48
  children?: Record<string, NamespacedElement[]>;
48
49
  }
49
50
  /**
50
- * 2x3 affine matrix in column-major form (SVG's `matrix(a b c d e f)`
51
- * order). Maps `[x', y'] = [a*x + c*y + e, b*x + d*y + f]`.
51
+ * 2x3 affine matrix in SVG's `matrix(a b c d e f)` order, mapping
52
+ * `[x', y'] = [a*x + c*y + e, b*x + d*y + f]`.
53
+ *
54
+ * The same order the kernel's affine tier uses, so this is that type under
55
+ * the name the SVG spec gives it — not a parallel one. An `SvgGroupNode`'s
56
+ * transform and a geom `Mat3` are interchangeable without a conversion.
52
57
  */
53
- type Matrix = readonly [number, number, number, number, number, number];
58
+ type Matrix = Mat3;
59
+
54
60
  /** Identity matrix — useful as a default in tests / constructors. */
55
61
  declare const IDENTITY_MATRIX: Matrix;
56
62
  /**
@@ -73,11 +79,9 @@ type SvgPaint = {
73
79
  interface SvgStroke {
74
80
  paint: SvgPaint;
75
81
  /** World units, or `{ px }` for a width that holds its rendered thickness
76
- * however the document is scaled — SVG's `vector-effect="non-scaling-stroke"`,
77
- * and the same unit system as the kit's `Stroke.width`. */
78
- width: number | {
79
- px: number;
80
- };
82
+ * however the document is scaled — SVG's `vector-effect="non-scaling-stroke"`.
83
+ * Literally the kit's own unit, not a second spelling of it. */
84
+ width: ScreenLength;
81
85
  opacity?: number;
82
86
  /** `stroke-linecap`. Default per SVG spec is `'butt'`. */
83
87
  cap?: 'butt' | 'round' | 'square';
@@ -422,6 +426,9 @@ type SvgSceneDraft = {
422
426
  * `userSpaceOnUse` gradient survives the fit-clamp and the drop-point
423
427
  * placement. */
424
428
  declare function strokeDataFromSvg(stroke: SvgStroke | undefined, box: SvgDraftBounds): Stroke | undefined;
429
+ /** Write a `kit:image` leaf as an `SvgImageNode` — the inverse of the image
430
+ * arm of {@link svgNodesToKitDrafts}. The pose is the image's box. */
431
+ declare function svgImageFromKit(image: ImageNodeData['image'], pose: DraftPose): SvgImageNode;
425
432
  /**
426
433
  * Walk an `SvgNode[]` tree and emit a flat, parent-before-child list of
427
434
  * {@link SvgSceneDraft}s. Each `<g>` becomes a container whose pose is the
@@ -437,4 +444,4 @@ declare function svgNodesToKitDrafts(nodes: readonly SvgNode[], nextId: () => st
437
444
  */
438
445
  declare function unpackSvgFiles(files: File[], ctx: IngestCtx): Promise<void>;
439
446
 
440
- export { IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type SvgDraftBounds, type SvgGroupNode, type SvgImageNode, type SvgNode, type SvgPaint, type SvgPathNode, type SvgSceneDraft, type SvgStroke, type SvgTextNode, parseSvg, serializeSvg, strokeDataFromSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
447
+ export { IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type SvgDraftBounds, type SvgGroupNode, type SvgImageNode, type SvgNode, type SvgPaint, type SvgPathNode, type SvgSceneDraft, type SvgStroke, type SvgTextNode, parseSvg, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
package/dist/index.js CHANGED
@@ -1,21 +1,9 @@
1
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 { invert } from '@weasel-js/geom';
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
  }
@@ -25,9 +13,9 @@ function isIdentity(m, eps = 1e-9) {
25
13
  var NUM_RE = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;
26
14
  function readNumbers(s) {
27
15
  const out = [];
28
- const matches = s.match(NUM_RE);
29
- if (!matches) return out;
30
- for (const m of matches) {
16
+ const matches2 = s.match(NUM_RE);
17
+ if (!matches2) return out;
18
+ for (const m of matches2) {
31
19
  const n = Number(m);
32
20
  if (Number.isFinite(n)) out.push(n);
33
21
  }
@@ -77,10 +65,9 @@ function parseTransform(attr, onWarn) {
77
65
  return result;
78
66
  }
79
67
  function rebaseTransform(ctm, local) {
80
- const inv = invert([...ctm]);
68
+ const inv = invert(ctm);
81
69
  if (!inv) return null;
82
- const [a, b, c, d, e, f] = inv;
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;
@@ -262,6 +249,302 @@ function parsePaintAttr(raw) {
262
249
  }
263
250
  }
264
251
 
252
+ // src/cascade.ts
253
+ var EMPTY_STYLE = {};
254
+ var INHERITABLE = [
255
+ "fill",
256
+ "fill-opacity",
257
+ "fill-rule",
258
+ "stroke",
259
+ "stroke-width",
260
+ "stroke-opacity",
261
+ "stroke-linecap",
262
+ "stroke-linejoin",
263
+ "stroke-dasharray",
264
+ "stroke-miterlimit",
265
+ "marker-start",
266
+ "marker-mid",
267
+ "marker-end",
268
+ "color",
269
+ "font-size",
270
+ "font-family",
271
+ "font-weight",
272
+ "font-style",
273
+ "text-anchor",
274
+ "letter-spacing",
275
+ "text-decoration",
276
+ "direction",
277
+ "text-transform"
278
+ ];
279
+ function skipString(s, i) {
280
+ const q = s[i];
281
+ for (let j = i + 1; j < s.length; j++) {
282
+ if (s[j] === "\\") j++;
283
+ else if (s[j] === q) return j + 1;
284
+ }
285
+ return s.length;
286
+ }
287
+ function stripComments(s) {
288
+ if (!s.includes("/*")) return s;
289
+ let out = "";
290
+ let i = 0;
291
+ while (i < s.length) {
292
+ const ch = s[i];
293
+ if (ch === '"' || ch === "'") {
294
+ const end = skipString(s, i);
295
+ out += s.slice(i, end);
296
+ i = end;
297
+ } else if (ch === "/" && s[i + 1] === "*") {
298
+ const end = s.indexOf("*/", i + 2);
299
+ i = end < 0 ? s.length : end + 2;
300
+ out += " ";
301
+ } else {
302
+ out += ch;
303
+ i++;
304
+ }
305
+ }
306
+ return out;
307
+ }
308
+ function splitTopLevel(s, sep, dropBlocks = false) {
309
+ const parts = [];
310
+ let depth = 0;
311
+ let start = 0;
312
+ let i = 0;
313
+ while (i < s.length) {
314
+ const ch = s[i];
315
+ if (ch === '"' || ch === "'") {
316
+ i = skipString(s, i);
317
+ continue;
318
+ }
319
+ if (ch === "(" || ch === "[") depth++;
320
+ else if ((ch === ")" || ch === "]") && depth > 0) depth--;
321
+ else if (depth === 0 && ch === sep) {
322
+ parts.push(s.slice(start, i));
323
+ start = i + 1;
324
+ } else if (depth === 0 && dropBlocks && ch === "{") {
325
+ i = skipBlock(s, i);
326
+ start = i;
327
+ continue;
328
+ }
329
+ i++;
330
+ }
331
+ parts.push(s.slice(start));
332
+ return parts;
333
+ }
334
+ function skipBlock(s, i) {
335
+ let depth = 0;
336
+ while (i < s.length) {
337
+ const ch = s[i];
338
+ if (ch === '"' || ch === "'") {
339
+ i = skipString(s, i);
340
+ continue;
341
+ }
342
+ if (ch === "{") depth++;
343
+ else if (ch === "}" && --depth === 0) return i + 1;
344
+ i++;
345
+ }
346
+ return s.length;
347
+ }
348
+ var PROP_NAME = /^-?[a-z_][a-z0-9_-]*$/;
349
+ var IMPORTANT = /!\s*important\s*$/i;
350
+ function parseDeclarations(block) {
351
+ const out = [];
352
+ for (const part of splitTopLevel(stripComments(block), ";", true)) {
353
+ const colon = part.indexOf(":");
354
+ if (colon < 0) continue;
355
+ const prop = part.slice(0, colon).trim().toLowerCase();
356
+ if (!PROP_NAME.test(prop)) continue;
357
+ let value = part.slice(colon + 1).trim();
358
+ const important = IMPORTANT.test(value);
359
+ if (important) value = value.replace(IMPORTANT, "").trim();
360
+ if (!value) continue;
361
+ out.push({ prop, value, important });
362
+ }
363
+ return out;
364
+ }
365
+ function identEnd(s, i) {
366
+ while (i < s.length) {
367
+ const ch = s[i];
368
+ if (ch === "\\") i += 2;
369
+ else if (/[\w-]/.test(ch) || ch.charCodeAt(0) > 127) i++;
370
+ else break;
371
+ }
372
+ return i;
373
+ }
374
+ function parenEnd(s, i) {
375
+ let depth = 0;
376
+ while (i < s.length) {
377
+ const ch = s[i];
378
+ if (ch === '"' || ch === "'") {
379
+ i = skipString(s, i);
380
+ continue;
381
+ }
382
+ if (ch === "(") depth++;
383
+ else if (ch === ")" && --depth === 0) return i + 1;
384
+ i++;
385
+ }
386
+ return s.length;
387
+ }
388
+ var LEGACY_PSEUDO_ELEMENTS = /* @__PURE__ */ new Set(["before", "after", "first-line", "first-letter"]);
389
+ var MAX_ARG_PSEUDOS = /* @__PURE__ */ new Set(["not", "is", "matches", "-webkit-any", "has"]);
390
+ function compareSpecificity(a, b) {
391
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
392
+ }
393
+ function maxSpecificity(list) {
394
+ let best = [0, 0, 0];
395
+ for (const sel of splitTopLevel(list, ",")) {
396
+ const s = specificity(sel);
397
+ if (compareSpecificity(s, best) > 0) best = s;
398
+ }
399
+ return best;
400
+ }
401
+ function specificity(selector) {
402
+ let a = 0, b = 0, c = 0;
403
+ const s = selector;
404
+ let i = 0;
405
+ while (i < s.length) {
406
+ const ch = s[i];
407
+ if (ch === "#") {
408
+ a++;
409
+ i = identEnd(s, i + 1);
410
+ } else if (ch === ".") {
411
+ b++;
412
+ i = identEnd(s, i + 1);
413
+ } else if (ch === "[") {
414
+ b++;
415
+ while (i < s.length && s[i] !== "]") {
416
+ i = s[i] === '"' || s[i] === "'" ? skipString(s, i) : i + 1;
417
+ }
418
+ i++;
419
+ } else if (ch === ":") {
420
+ const element = s[i + 1] === ":";
421
+ const nameStart = i + (element ? 2 : 1);
422
+ const nameEnd = identEnd(s, nameStart);
423
+ const name = s.slice(nameStart, nameEnd).toLowerCase();
424
+ let arg = null;
425
+ i = nameEnd;
426
+ if (s[i] === "(") {
427
+ const end = parenEnd(s, i);
428
+ arg = s.slice(i + 1, end - 1);
429
+ i = end;
430
+ }
431
+ if (element || LEGACY_PSEUDO_ELEMENTS.has(name)) c++;
432
+ else if (name === "where") ; else if (MAX_ARG_PSEUDOS.has(name) && arg != null) {
433
+ const m = maxSpecificity(arg);
434
+ a += m[0];
435
+ b += m[1];
436
+ c += m[2];
437
+ } else b++;
438
+ } else if (/[a-zA-Z_\\-]/.test(ch) || ch.charCodeAt(0) > 127) {
439
+ c++;
440
+ i = identEnd(s, i);
441
+ } else {
442
+ i++;
443
+ }
444
+ }
445
+ return [a, b, c];
446
+ }
447
+ function parseStylesheet(text, firstOrder = 0) {
448
+ const src = stripComments(text).replace(/<!\[CDATA\[|\]\]>|<!--|-->/g, " ");
449
+ const rules = [];
450
+ let order = firstOrder;
451
+ let i = 0;
452
+ let preludeStart = 0;
453
+ while (i < src.length) {
454
+ const ch = src[i];
455
+ if (ch === '"' || ch === "'") {
456
+ i = skipString(src, i);
457
+ continue;
458
+ }
459
+ if (ch === ";") {
460
+ i++;
461
+ preludeStart = i;
462
+ continue;
463
+ }
464
+ if (ch !== "{") {
465
+ i++;
466
+ continue;
467
+ }
468
+ const prelude = src.slice(preludeStart, i).trim();
469
+ const end = skipBlock(src, i);
470
+ if (!prelude.startsWith("@") && prelude) {
471
+ const declarations = parseDeclarations(src.slice(i + 1, end - 1));
472
+ for (const raw of splitTopLevel(prelude, ",")) {
473
+ const selector = raw.trim();
474
+ if (selector) {
475
+ rules.push({ selector, declarations, specificity: specificity(selector), order: order++ });
476
+ }
477
+ }
478
+ }
479
+ i = end;
480
+ preludeStart = end;
481
+ }
482
+ return rules;
483
+ }
484
+ function appliesAsCss(style) {
485
+ const type = style.getAttribute("type")?.trim().toLowerCase();
486
+ if (type && type !== "text/css") return false;
487
+ const media = style.getAttribute("media")?.trim().toLowerCase();
488
+ if (!media) return true;
489
+ return media.split(",").some((m) => m.trim() === "all" || m.trim() === "screen");
490
+ }
491
+ var sheetCache = /* @__PURE__ */ new WeakMap();
492
+ function documentRules(doc) {
493
+ const cached = sheetCache.get(doc);
494
+ if (cached) return cached;
495
+ const rules = [];
496
+ const styles = doc.getElementsByTagNameNS("*", "style");
497
+ for (let i = 0; i < styles.length; i++) {
498
+ if (!appliesAsCss(styles[i])) continue;
499
+ rules.push(...parseStylesheet(styles[i].textContent ?? "", rules.length));
500
+ }
501
+ rules.sort((x, y) => compareSpecificity(x.specificity, y.specificity) || x.order - y.order);
502
+ sheetCache.set(doc, rules);
503
+ return rules;
504
+ }
505
+ function matches(el, selector) {
506
+ try {
507
+ return el.matches(selector);
508
+ } catch {
509
+ return false;
510
+ }
511
+ }
512
+ var elementCache = /* @__PURE__ */ new WeakMap();
513
+ function authorDeclarations(el) {
514
+ const cached = elementCache.get(el);
515
+ if (cached) return cached;
516
+ const rules = el.ownerDocument ? documentRules(el.ownerDocument) : [];
517
+ const matched = rules.filter((r) => matches(el, r.selector));
518
+ const inline = parseDeclarations(el.getAttribute("style") ?? "");
519
+ const out = /* @__PURE__ */ new Map();
520
+ for (const important of [false, true]) {
521
+ for (const r of matched) {
522
+ for (const d of r.declarations) if (d.important === important) out.set(d.prop, d.value);
523
+ }
524
+ for (const d of inline) if (d.important === important) out.set(d.prop, d.value);
525
+ }
526
+ elementCache.set(el, out);
527
+ return out;
528
+ }
529
+ function ownProp(el, prop) {
530
+ return authorDeclarations(el).get(prop) ?? el.getAttribute(prop);
531
+ }
532
+ function deriveStyle(parent, el) {
533
+ let next = null;
534
+ for (const prop of INHERITABLE) {
535
+ const own = ownProp(el, prop);
536
+ if (own == null || own === "inherit") continue;
537
+ if (!next) next = { ...parent };
538
+ next[prop] = own;
539
+ }
540
+ return next ?? parent;
541
+ }
542
+ function resolveCurrentColor(raw, style) {
543
+ if (raw == null) return null;
544
+ if (raw.trim().toLowerCase() === "currentcolor") return style["color"] ?? "#000000";
545
+ return raw;
546
+ }
547
+
265
548
  // src/elements.ts
266
549
  function collectElementsByTag(root, tags) {
267
550
  const out = /* @__PURE__ */ new Map();
@@ -450,7 +733,7 @@ function warnUnsupportedDefsChildren(svg, onWarn) {
450
733
  const child = root.children[i];
451
734
  const tag = child.tagName.toLowerCase();
452
735
  if (GRADIENT_TAGS.has(tag) || GRADIENT_TAGS.has(localName(tag))) continue;
453
- if (tag === "pattern" || tag === "marker" || tag === "clippath") continue;
736
+ if (tag === "pattern" || tag === "marker" || tag === "clippath" || tag === "style") continue;
454
737
  onWarn(`unsupported <defs> child: <${child.tagName}>`);
455
738
  }
456
739
  }
@@ -487,10 +770,10 @@ function readStops(el, onWarn) {
487
770
  if (c.tagName.toLowerCase() !== "stop") continue;
488
771
  const offsetRaw = parseRatio(c.getAttribute("offset") ?? "0");
489
772
  const offset = Number.isFinite(offsetRaw) ? offsetRaw : 0;
490
- const colorAttr = c.getAttribute("stop-color") ?? "#000000";
773
+ const colorAttr = ownProp(c, "stop-color") ?? "#000000";
491
774
  const parsed = parsePaintAttr(colorAttr);
492
775
  if (parsed && parsed.kind === "solid") {
493
- const opacityAttr = c.getAttribute("stop-opacity");
776
+ const opacityAttr = ownProp(c, "stop-opacity");
494
777
  const own = opacityAttr != null ? parseRatio(opacityAttr) : NaN;
495
778
  const alpha = Number.isFinite(own) ? own : parsed.alpha;
496
779
  stops.push({ offset, color: alpha < 1 ? applyAlpha(parsed.color, alpha) : parsed.color });
@@ -734,59 +1017,6 @@ function stopXml(s) {
734
1017
  return `<stop offset="${trimNumber(s.offset)}" stop-color="${s.color}"/>`;
735
1018
  }
736
1019
 
737
- // src/cascade.ts
738
- var EMPTY_STYLE = {};
739
- var INHERITABLE = [
740
- "fill",
741
- "fill-opacity",
742
- "fill-rule",
743
- "stroke",
744
- "stroke-width",
745
- "stroke-opacity",
746
- "stroke-linecap",
747
- "stroke-linejoin",
748
- "stroke-dasharray",
749
- "stroke-miterlimit",
750
- "marker-start",
751
- "marker-mid",
752
- "marker-end",
753
- "color",
754
- "font-size",
755
- "font-family",
756
- "font-weight",
757
- "font-style",
758
- "text-anchor",
759
- "letter-spacing",
760
- "text-decoration",
761
- "direction"
762
- ];
763
- function readStyleProp(el, prop) {
764
- const style = el.getAttribute("style");
765
- if (!style) return null;
766
- const re = new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*([^;]+)`, "i");
767
- const m = re.exec(style);
768
- if (!m) return null;
769
- return m[1].trim();
770
- }
771
- function ownProp(el, prop) {
772
- return readStyleProp(el, prop) ?? el.getAttribute(prop);
773
- }
774
- function deriveStyle(parent, el) {
775
- let next = null;
776
- for (const prop of INHERITABLE) {
777
- const own = ownProp(el, prop);
778
- if (own == null || own === "inherit") continue;
779
- if (!next) next = { ...parent };
780
- next[prop] = own;
781
- }
782
- return next ?? parent;
783
- }
784
- function resolveCurrentColor(raw, style) {
785
- if (raw == null) return null;
786
- if (raw.trim().toLowerCase() === "currentcolor") return style["color"] ?? "#000000";
787
- return raw;
788
- }
789
-
790
1020
  // src/parse.ts
791
1021
  var SUPPORTED_LEAF_TAGS = /* @__PURE__ */ new Set([
792
1022
  "rect",
@@ -807,7 +1037,8 @@ var IGNORED_TAGS = /* @__PURE__ */ new Set([
807
1037
  "clippath",
808
1038
  "title",
809
1039
  "desc",
810
- "metadata"
1040
+ "metadata",
1041
+ "style"
811
1042
  ]);
812
1043
  function parseSvg(svg, opts = {}) {
813
1044
  const warnings = [];
@@ -1559,6 +1790,11 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1559
1790
  }
1560
1791
  return node;
1561
1792
  }
1793
+ var TEXT_TRANSFORMS = /* @__PURE__ */ new Set(["none", "uppercase", "lowercase", "capitalize"]);
1794
+ function parseTextTransform(raw) {
1795
+ const v = raw?.trim().toLowerCase();
1796
+ return v !== void 0 && TEXT_TRANSFORMS.has(v) ? v : void 0;
1797
+ }
1562
1798
  function readTspanRun(el, gradients, style, onWarn) {
1563
1799
  const text = el.textContent ?? "";
1564
1800
  const run = { text };
@@ -1589,6 +1825,8 @@ function readTspanRun(el, gradients, style, onWarn) {
1589
1825
  if (decoration.underline) run.underline = true;
1590
1826
  if (decoration.strikethrough) run.strikethrough = true;
1591
1827
  if (decoration.overline) run.overline = true;
1828
+ const transform = parseTextTransform(ownProp(el, "text-transform"));
1829
+ if (transform) run.textTransform = transform;
1592
1830
  const tspanStyle = deriveStyle(style, el);
1593
1831
  const fillAttr = resolveCurrentColor(ownProp(el, "fill"), tspanStyle);
1594
1832
  if (fillAttr) {
@@ -1636,6 +1874,8 @@ function readTextStyle(style, onWarn) {
1636
1874
  if (decoration.underline) out.underline = true;
1637
1875
  if (decoration.strikethrough) out.strikethrough = true;
1638
1876
  if (decoration.overline) out.overline = true;
1877
+ const transform = parseTextTransform(style["text-transform"] ?? null);
1878
+ if (transform && transform !== "none") out.textTransform = transform;
1639
1879
  return out;
1640
1880
  }
1641
1881
  function readTextPaint(style, gradients, onWarn, nonScalingStroke = false) {
@@ -2201,6 +2441,9 @@ function textXml(node, registry, namespaces, warn) {
2201
2441
  }
2202
2442
  const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
2203
2443
  if (decoration) attrs.push(`text-decoration="${decoration}"`);
2444
+ if (style?.textTransform && style.textTransform !== "none") {
2445
+ attrs.push(`style="text-transform:${style.textTransform}"`);
2446
+ }
2204
2447
  if (style?.wrap) {
2205
2448
  attrs.push('data-weasel-wrap="true"');
2206
2449
  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)");
@@ -2263,6 +2506,7 @@ function runXml(run, registry, warn) {
2263
2506
  }
2264
2507
  }
2265
2508
  for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
2509
+ if (run.textTransform) attrs.push(`style="text-transform:${run.textTransform}"`);
2266
2510
  const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
2267
2511
  return `${head}${escapeText(run.text)}</tspan>`;
2268
2512
  }
@@ -2329,6 +2573,22 @@ function strokeDataFromSvg(stroke, box) {
2329
2573
  ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {}
2330
2574
  };
2331
2575
  }
2576
+ function svgImageFromKit(image, pose) {
2577
+ const node = {
2578
+ kind: "image",
2579
+ href: image.src,
2580
+ x: pose.x,
2581
+ y: pose.y,
2582
+ width: pose.width,
2583
+ height: pose.height
2584
+ };
2585
+ if (image.source) node.source = { ...image.source };
2586
+ if (image.flipX) node.flipX = true;
2587
+ if (image.flipY) node.flipY = true;
2588
+ if (image.opacity != null) node.opacity = image.opacity;
2589
+ if (pose.rotation) node.rotation = pose.rotation;
2590
+ return node;
2591
+ }
2332
2592
  function fillInBoxFrame(fill, box) {
2333
2593
  const units = "units" in fill ? fill.units : void 0;
2334
2594
  return units === "world" ? fillToBoundsFrame(fill, box) : fill;
@@ -2410,7 +2670,10 @@ function svgNodesToKitDrafts(nodes, nextId) {
2410
2670
  data: {
2411
2671
  image: {
2412
2672
  src: n.href,
2413
- ...n.opacity != null ? { opacity: n.opacity } : {}
2673
+ ...n.opacity != null ? { opacity: n.opacity } : {},
2674
+ ...n.source ? { source: { ...n.source } } : {},
2675
+ ...n.flipX ? { flipX: true } : {},
2676
+ ...n.flipY ? { flipY: true } : {}
2414
2677
  }
2415
2678
  }
2416
2679
  });
@@ -2522,6 +2785,6 @@ async function unpackSvgFiles(files, ctx) {
2522
2785
  }
2523
2786
  }
2524
2787
 
2525
- export { IDENTITY_MATRIX, parseSvg, serializeSvg, strokeDataFromSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
2788
+ export { IDENTITY_MATRIX, parseSvg, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
2526
2789
  //# sourceMappingURL=index.js.map
2527
2790
  //# sourceMappingURL=index.js.map