@emasoft/svg-matrix 1.0.27 → 1.0.29

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.
Files changed (46) hide show
  1. package/README.md +325 -0
  2. package/bin/svg-matrix.js +994 -378
  3. package/bin/svglinter.cjs +4172 -433
  4. package/bin/svgm.js +744 -184
  5. package/package.json +16 -4
  6. package/src/animation-references.js +71 -52
  7. package/src/arc-length.js +160 -96
  8. package/src/bezier-analysis.js +257 -117
  9. package/src/bezier-intersections.js +411 -148
  10. package/src/browser-verify.js +240 -100
  11. package/src/clip-path-resolver.js +350 -142
  12. package/src/convert-path-data.js +279 -134
  13. package/src/css-specificity.js +78 -70
  14. package/src/flatten-pipeline.js +751 -263
  15. package/src/geometry-to-path.js +511 -182
  16. package/src/index.js +191 -46
  17. package/src/inkscape-support.js +404 -0
  18. package/src/marker-resolver.js +278 -164
  19. package/src/mask-resolver.js +209 -98
  20. package/src/matrix.js +147 -67
  21. package/src/mesh-gradient.js +187 -96
  22. package/src/off-canvas-detection.js +201 -104
  23. package/src/path-analysis.js +187 -107
  24. package/src/path-data-plugins.js +628 -167
  25. package/src/path-simplification.js +0 -1
  26. package/src/pattern-resolver.js +125 -88
  27. package/src/polygon-clip.js +111 -66
  28. package/src/svg-boolean-ops.js +194 -118
  29. package/src/svg-collections.js +48 -19
  30. package/src/svg-flatten.js +282 -164
  31. package/src/svg-parser.js +427 -200
  32. package/src/svg-rendering-context.js +147 -104
  33. package/src/svg-toolbox.js +16411 -3298
  34. package/src/svg2-polyfills.js +114 -245
  35. package/src/transform-decomposition.js +46 -41
  36. package/src/transform-optimization.js +89 -68
  37. package/src/transforms2d.js +49 -16
  38. package/src/transforms3d.js +58 -22
  39. package/src/use-symbol-resolver.js +150 -110
  40. package/src/vector.js +67 -15
  41. package/src/vendor/README.md +110 -0
  42. package/src/vendor/inkscape-hatch-polyfill.js +401 -0
  43. package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
  44. package/src/vendor/inkscape-mesh-polyfill.js +843 -0
  45. package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
  46. package/src/verification.js +288 -124
@@ -7,6 +7,8 @@
7
7
  * @module inkscape-support
8
8
  */
9
9
 
10
+ import { SVGElement } from './svg-parser.js';
11
+
10
12
  // Inkscape namespace URIs
11
13
  export const INKSCAPE_NS = 'http://www.inkscape.org/namespaces/inkscape';
12
14
  export const SODIPODI_NS = 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd';
@@ -23,6 +25,8 @@ export const INKSCAPE_PREFIXES = ['inkscape', 'sodipodi'];
23
25
  */
24
26
  export function isInkscapeLayer(element) {
25
27
  if (!element || element.tagName !== 'g') return false;
28
+ // Safety check: ensure getAttribute method exists before calling it
29
+ if (typeof element.getAttribute !== 'function') return false;
26
30
  return element.getAttribute('inkscape:groupmode') === 'layer';
27
31
  }
28
32
 
@@ -221,7 +225,9 @@ export function getTiledCloneSource(element) {
221
225
  * @returns {boolean} True if Inkscape namespaces are present
222
226
  */
223
227
  export function hasInkscapeNamespaces(doc) {
228
+ if (!doc) return false;
224
229
  const svg = doc.documentElement || doc;
230
+ if (!svg || typeof svg.getAttribute !== 'function') return false;
225
231
  const hasInkscape = svg.getAttribute('xmlns:inkscape') === INKSCAPE_NS;
226
232
  const hasSodipodi = svg.getAttribute('xmlns:sodipodi') === SODIPODI_NS;
227
233
  return hasInkscape || hasSodipodi;
@@ -237,6 +243,11 @@ export function hasInkscapeNamespaces(doc) {
237
243
  export function ensureInkscapeNamespaces(doc) {
238
244
  const svg = doc.documentElement || doc;
239
245
 
246
+ // Safety check: ensure getAttribute and setAttribute methods exist
247
+ if (typeof svg.getAttribute !== 'function' || typeof svg.setAttribute !== 'function') {
248
+ return doc;
249
+ }
250
+
240
251
  if (!svg.getAttribute('xmlns:inkscape')) {
241
252
  svg.setAttribute('xmlns:inkscape', INKSCAPE_NS);
242
253
  }
@@ -246,3 +257,396 @@ export function ensureInkscapeNamespaces(doc) {
246
257
 
247
258
  return doc;
248
259
  }
260
+
261
+ // ============================================================================
262
+ // LAYER EXTRACTION
263
+ // ============================================================================
264
+
265
+ /**
266
+ * Find all IDs referenced by an element and its descendants.
267
+ * Looks for url(#id) references in fill, stroke, clip-path, mask, marker-*, filter, etc.
268
+ * Also checks xlink:href and href attributes for #id references.
269
+ *
270
+ * @param {Object} element - SVG element to scan
271
+ * @returns {Set<string>} Set of referenced IDs
272
+ */
273
+ export function findReferencedIds(element) {
274
+ const ids = new Set();
275
+
276
+ // Attributes that can contain url(#id) references
277
+ const urlRefAttrs = [
278
+ 'fill', 'stroke', 'clip-path', 'mask', 'filter',
279
+ 'marker-start', 'marker-mid', 'marker-end',
280
+ 'fill-opacity', 'stroke-opacity' // Sometimes reference paint servers
281
+ ];
282
+
283
+ // Attributes that can contain #id or url(#id) references
284
+ const hrefAttrs = ['href', 'xlink:href'];
285
+
286
+ const extractUrlId = (value) => {
287
+ if (!value) return null;
288
+ // Match url(#id) or url("#id")
289
+ const match = value.match(/url\(["']?#([^"')]+)["']?\)/);
290
+ return match ? match[1] : null;
291
+ };
292
+
293
+ const extractHrefId = (value) => {
294
+ if (!value) return null;
295
+ // Match #id references
296
+ if (value.startsWith('#')) {
297
+ return value.slice(1);
298
+ }
299
+ return null;
300
+ };
301
+
302
+ const walk = (el) => {
303
+ if (!el) return;
304
+
305
+ // Check url() references
306
+ for (const attr of urlRefAttrs) {
307
+ const id = extractUrlId(el.getAttribute?.(attr));
308
+ if (id) ids.add(id);
309
+ }
310
+
311
+ // Check href references
312
+ for (const attr of hrefAttrs) {
313
+ const id = extractHrefId(el.getAttribute?.(attr));
314
+ if (id) ids.add(id);
315
+ }
316
+
317
+ // Check style attribute for url() references
318
+ const style = el.getAttribute?.('style');
319
+ if (style) {
320
+ const urlMatches = style.matchAll(/url\(["']?#([^"')]+)["']?\)/g);
321
+ for (const match of urlMatches) {
322
+ ids.add(match[1]);
323
+ }
324
+ }
325
+
326
+ // Recurse into children
327
+ if (el.children) {
328
+ for (const child of el.children) {
329
+ walk(child);
330
+ }
331
+ }
332
+ };
333
+
334
+ walk(element);
335
+ return ids;
336
+ }
337
+
338
+ /**
339
+ * Build a map of all defs elements by their ID.
340
+ *
341
+ * @param {Object} doc - Parsed SVG document
342
+ * @returns {Map<string, Object>} Map of ID to element
343
+ */
344
+ export function buildDefsMapFromDefs(doc) {
345
+ const defsMap = new Map();
346
+
347
+ const walk = (el) => {
348
+ if (!el) return;
349
+
350
+ // If element has an ID, add to map
351
+ const id = el.getAttribute?.('id');
352
+ if (id) {
353
+ defsMap.set(id, el);
354
+ }
355
+
356
+ // Recurse
357
+ if (el.children) {
358
+ for (const child of el.children) {
359
+ walk(child);
360
+ }
361
+ }
362
+ };
363
+
364
+ // Only scan defs elements for efficiency
365
+ const findDefs = (el) => {
366
+ if (!el) return;
367
+ if (el.tagName === 'defs') {
368
+ walk(el);
369
+ }
370
+ if (el.children) {
371
+ for (const child of el.children) {
372
+ findDefs(child);
373
+ }
374
+ }
375
+ };
376
+
377
+ findDefs(doc);
378
+ return defsMap;
379
+ }
380
+
381
+ /**
382
+ * Recursively resolve all dependencies for a set of IDs.
383
+ * Defs elements can reference other defs (e.g., gradient with xlink:href to another gradient).
384
+ *
385
+ * @param {Set<string>} initialIds - Initial set of IDs to resolve
386
+ * @param {Map<string, Object>} defsMap - Map of all defs elements
387
+ * @returns {Set<string>} Complete set of IDs including all nested dependencies
388
+ */
389
+ export function resolveDefsDependencies(initialIds, defsMap) {
390
+ const resolved = new Set();
391
+ const toProcess = [...initialIds];
392
+
393
+ while (toProcess.length > 0) {
394
+ const id = toProcess.pop();
395
+ if (resolved.has(id)) continue;
396
+
397
+ const element = defsMap.get(id);
398
+ if (!element) continue;
399
+
400
+ resolved.add(id);
401
+
402
+ // Find references within this def element
403
+ const nestedRefs = findReferencedIds(element);
404
+ for (const nestedId of nestedRefs) {
405
+ if (!resolved.has(nestedId) && defsMap.has(nestedId)) {
406
+ toProcess.push(nestedId);
407
+ }
408
+ }
409
+ }
410
+
411
+ return resolved;
412
+ }
413
+
414
+ /**
415
+ * Deep clone an SVG element and all its children using proper SVGElement class.
416
+ *
417
+ * @param {Object} element - Element to clone
418
+ * @returns {SVGElement} Cloned element with serialize() method
419
+ */
420
+ export function cloneElement(element) {
421
+ if (!element) return null;
422
+
423
+ // Get attributes as plain object
424
+ const attrs = {};
425
+ if (element._attributes) {
426
+ Object.assign(attrs, element._attributes);
427
+ } else if (element.getAttributeNames) {
428
+ for (const name of element.getAttributeNames()) {
429
+ attrs[name] = element.getAttribute(name);
430
+ }
431
+ }
432
+
433
+ // Clone children recursively
434
+ const clonedChildren = [];
435
+ if (element.children) {
436
+ for (const child of element.children) {
437
+ const clonedChild = cloneElement(child);
438
+ if (clonedChild) {
439
+ clonedChildren.push(clonedChild);
440
+ }
441
+ }
442
+ }
443
+
444
+ // Create proper SVGElement with serialize() method
445
+ const clone = new SVGElement(
446
+ element.tagName,
447
+ attrs,
448
+ clonedChildren,
449
+ element.textContent || null
450
+ );
451
+
452
+ return clone;
453
+ }
454
+
455
+ /**
456
+ * Extract a single layer as a standalone SVG document.
457
+ * Includes only the defs elements that are referenced by the layer.
458
+ *
459
+ * @param {Object} doc - Source parsed SVG document
460
+ * @param {Object|string} layerOrId - Layer element or layer ID to extract
461
+ * @param {Object} [options] - Options
462
+ * @param {boolean} [options.includeHiddenLayers=false] - Include hidden layers in output
463
+ * @param {boolean} [options.preserveTransform=true] - Preserve layer transform attribute
464
+ * @returns {{svg: SVGElement, layerInfo: {id: string, label: string}}} Extracted SVG and layer info
465
+ */
466
+ export function extractLayer(doc, layerOrId, options = {}) {
467
+ const { preserveTransform = true } = options;
468
+
469
+ // Find the layer element
470
+ let layer;
471
+ if (typeof layerOrId === 'string') {
472
+ const layers = findLayers(doc);
473
+ const found = layers.find(l => l.id === layerOrId || l.label === layerOrId);
474
+ if (!found || !found.element) {
475
+ throw new Error(`Layer not found or invalid: ${layerOrId}`);
476
+ }
477
+ layer = found.element;
478
+ } else {
479
+ layer = layerOrId;
480
+ }
481
+
482
+ if (!isInkscapeLayer(layer)) {
483
+ throw new Error('Element is not an Inkscape layer');
484
+ }
485
+
486
+ // Get SVG root element
487
+ const svgRoot = doc.documentElement || doc;
488
+
489
+ // Build defs map from source document
490
+ const defsMap = buildDefsMapFromDefs(doc);
491
+
492
+ // Find all IDs referenced by this layer
493
+ const referencedIds = findReferencedIds(layer);
494
+
495
+ // Resolve all nested dependencies
496
+ const requiredDefIds = resolveDefsDependencies(referencedIds, defsMap);
497
+
498
+ // Get SVG root attributes
499
+ const svgAttrs = {};
500
+ if (svgRoot._attributes) {
501
+ Object.assign(svgAttrs, svgRoot._attributes);
502
+ } else if (svgRoot.getAttributeNames) {
503
+ for (const name of svgRoot.getAttributeNames()) {
504
+ svgAttrs[name] = svgRoot.getAttribute(name);
505
+ }
506
+ }
507
+
508
+ // Build children array for new SVG
509
+ const svgChildren = [];
510
+
511
+ // Create defs element with required definitions
512
+ if (requiredDefIds.size > 0) {
513
+ const defsChildren = [];
514
+ for (const id of requiredDefIds) {
515
+ const defElement = defsMap.get(id);
516
+ if (defElement) {
517
+ defsChildren.push(cloneElement(defElement));
518
+ }
519
+ }
520
+ if (defsChildren.length > 0) {
521
+ const newDefs = new SVGElement('defs', {}, defsChildren, null);
522
+ svgChildren.push(newDefs);
523
+ }
524
+ }
525
+
526
+ // Clone the layer
527
+ const clonedLayer = cloneElement(layer);
528
+
529
+ // Optionally remove the transform
530
+ if (!preserveTransform && clonedLayer._attributes) {
531
+ delete clonedLayer._attributes.transform;
532
+ }
533
+
534
+ svgChildren.push(clonedLayer);
535
+
536
+ // Create new SVG document using SVGElement
537
+ const newSvg = new SVGElement('svg', svgAttrs, svgChildren, null);
538
+
539
+ // Get layer info
540
+ const layerInfo = {
541
+ id: layer.getAttribute('id'),
542
+ label: getLayerLabel(layer)
543
+ };
544
+
545
+ return { svg: newSvg, layerInfo };
546
+ }
547
+
548
+ /**
549
+ * Extract all layers from an Inkscape SVG as separate documents.
550
+ *
551
+ * @param {Object} doc - Source parsed SVG document
552
+ * @param {Object} [options] - Options
553
+ * @param {boolean} [options.includeHidden=false] - Include hidden layers (display:none or visibility:hidden)
554
+ * @param {boolean} [options.preserveTransform=true] - Preserve layer transform attributes
555
+ * @returns {Array<{svg: Object, layerInfo: {id: string, label: string}}>} Array of extracted SVGs
556
+ */
557
+ export function extractAllLayers(doc, options = {}) {
558
+ const { includeHidden = false } = options;
559
+ const layers = findLayers(doc);
560
+ const results = [];
561
+
562
+ for (const layerData of layers) {
563
+ const layer = layerData.element;
564
+
565
+ // Skip hidden layers unless requested
566
+ if (!includeHidden) {
567
+ const style = layer.getAttribute('style') || '';
568
+ const display = layer.getAttribute('display');
569
+ const visibility = layer.getAttribute('visibility');
570
+
571
+ if (display === 'none' ||
572
+ visibility === 'hidden' ||
573
+ style.includes('display:none') ||
574
+ style.includes('visibility:hidden')) {
575
+ continue;
576
+ }
577
+ }
578
+
579
+ try {
580
+ const extracted = extractLayer(doc, layer, options);
581
+ results.push(extracted);
582
+ } catch (e) {
583
+ // Skip layers that fail to extract
584
+ console.warn(`Failed to extract layer ${layerData.id || layerData.label}: ${e.message}`);
585
+ }
586
+ }
587
+
588
+ return results;
589
+ }
590
+
591
+ /**
592
+ * Get a summary of shared resources between layers.
593
+ * Useful for understanding what defs are shared across layers.
594
+ *
595
+ * @param {Object} doc - Parsed SVG document
596
+ * @returns {Object} Summary of shared resources
597
+ */
598
+ export function analyzeLayerDependencies(doc) {
599
+ const layers = findLayers(doc);
600
+ const defsMap = buildDefsMapFromDefs(doc);
601
+ const layerRefs = new Map(); // layer ID -> Set of referenced def IDs
602
+ const defUsage = new Map(); // def ID -> Set of layer IDs that use it
603
+
604
+ for (const layerData of layers) {
605
+ const layer = layerData.element;
606
+ const layerId = layerData.id || layerData.label || 'unnamed';
607
+
608
+ // Find refs for this layer
609
+ const refs = findReferencedIds(layer);
610
+ const resolved = resolveDefsDependencies(refs, defsMap);
611
+
612
+ layerRefs.set(layerId, resolved);
613
+
614
+ // Track which defs are used by which layers
615
+ for (const defId of resolved) {
616
+ if (!defUsage.has(defId)) {
617
+ defUsage.set(defId, new Set());
618
+ }
619
+ defUsage.get(defId).add(layerId);
620
+ }
621
+ }
622
+
623
+ // Find shared defs (used by more than one layer)
624
+ const sharedDefs = [];
625
+ const exclusiveDefs = new Map(); // layer ID -> defs only used by that layer
626
+
627
+ for (const [defId, layerSet] of defUsage) {
628
+ if (layerSet.size > 1) {
629
+ sharedDefs.push({
630
+ id: defId,
631
+ usedBy: [...layerSet]
632
+ });
633
+ } else {
634
+ const layerId = [...layerSet][0];
635
+ if (!exclusiveDefs.has(layerId)) {
636
+ exclusiveDefs.set(layerId, []);
637
+ }
638
+ exclusiveDefs.get(layerId).push(defId);
639
+ }
640
+ }
641
+
642
+ return {
643
+ layers: layers.map(l => ({
644
+ id: l.id,
645
+ label: l.label,
646
+ referencedDefs: [...(layerRefs.get(l.id || l.label || 'unnamed') || [])]
647
+ })),
648
+ sharedDefs,
649
+ exclusiveDefs: Object.fromEntries(exclusiveDefs),
650
+ totalDefs: defsMap.size
651
+ };
652
+ }