svg-path-simplify 0.4.1 → 0.4.3

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 (48) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +6 -4
  3. package/dist/svg-path-simplify.esm.js +2450 -888
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +2450 -888
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +167 -85
  8. package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
  9. package/docs/privacy-webapp.md +24 -0
  10. package/index.html +333 -132
  11. package/package.json +5 -2
  12. package/src/css_parse.js +317 -0
  13. package/src/detect_input.js +34 -4
  14. package/src/pathData_simplify_harmonize_cpts.js +77 -1
  15. package/src/pathSimplify-main.js +246 -262
  16. package/src/pathSimplify-presets.js +243 -0
  17. package/src/poly-fit-curve-schneider.js +14 -7
  18. package/src/simplify_poly_RC.js +102 -0
  19. package/src/simplify_poly_RDP.js +109 -1
  20. package/src/simplify_poly_radial_distance.js +3 -3
  21. package/src/string_helpers.js +144 -0
  22. package/src/svgii/convert_units.js +8 -2
  23. package/src/svgii/geometry.js +182 -3
  24. package/src/svgii/geometry_length.js +237 -0
  25. package/src/svgii/pathData_convert.js +43 -1
  26. package/src/svgii/pathData_fix_directions.js +6 -0
  27. package/src/svgii/pathData_fromPoly.js +3 -3
  28. package/src/svgii/pathData_getLength.js +86 -0
  29. package/src/svgii/pathData_parse.js +2 -0
  30. package/src/svgii/pathData_parse_els.js +189 -189
  31. package/src/svgii/pathData_split_to_groups.js +168 -0
  32. package/src/svgii/pathData_stringify.js +26 -64
  33. package/src/svgii/pathData_toPolygon.js +3 -4
  34. package/src/svgii/poly_analyze.js +61 -0
  35. package/src/svgii/poly_normalize.js +11 -2
  36. package/src/svgii/poly_to_pathdata.js +85 -24
  37. package/src/svgii/rounding.js +8 -7
  38. package/src/svgii/svg-styles-to-attributes-const.js +1 -0
  39. package/src/svgii/svg_cleanup.js +467 -421
  40. package/src/svgii/svg_cleanup_convertPathLength.js +32 -0
  41. package/src/svgii/svg_cleanup_general_svg_atts.js +97 -0
  42. package/src/svgii/svg_cleanup_normalize_transforms.js +83 -0
  43. package/src/svgii/svg_cleanup_remove_els_and_atts.js +72 -0
  44. package/src/svgii/svg_cleanup_ungroup.js +36 -0
  45. package/src/svgii/svg_el_parse_style_props.js +76 -28
  46. package/src/svgii/svg_getElementLength.js +67 -0
  47. package/tests/testSVG_shape.js +59 -0
  48. package/tests/testSVG_transform.js +61 -0
@@ -52,12 +52,12 @@ function detectInputType(input) {
52
52
  // nested array
53
53
  if (Array.isArray(input[0])) {
54
54
 
55
- if(input[0].length===2){
55
+ if (input[0].length === 2) {
56
56
 
57
57
  return 'polyArray'
58
58
  }
59
59
 
60
- else if (Array.isArray(input[0][0]) && input[0][0].length === 2 ) {
60
+ else if (Array.isArray(input[0][0]) && input[0][0].length === 2) {
61
61
 
62
62
  return 'polyComplexArray'
63
63
  }
@@ -68,7 +68,7 @@ function detectInputType(input) {
68
68
  }
69
69
 
70
70
  // is point array
71
- else if (input[0].x!==undefined && input[0].y!==undefined) {
71
+ else if (input[0].x !== undefined && input[0].y !== undefined) {
72
72
 
73
73
  return 'polyObjectArray'
74
74
  }
@@ -86,12 +86,22 @@ function detectInputType(input) {
86
86
  if (typeof input === "string") {
87
87
  input = input.trim();
88
88
  let isSVG = input.includes('<svg') && input.includes('</svg');
89
+ let isSymbol = input.startsWith('<symbol') && input.includes('</symbol');
89
90
  let isPathData = input.startsWith('M') || input.startsWith('m');
90
91
  let isPolyString = !isNaN(input.substring(0, 1)) && !isNaN(input.substring(input.length - 1, input.length));
92
+ let isJson = isNumberJson(input);
91
93
 
92
94
  if (isSVG) {
93
95
  type = 'svgMarkup';
94
96
  }
97
+
98
+ else if (isJson) {
99
+ type = 'json';
100
+ }
101
+
102
+ else if (isSymbol) {
103
+ type = 'symbol';
104
+ }
95
105
  else if (isPathData) {
96
106
  type = 'pathDataString';
97
107
  }
@@ -114,6 +124,21 @@ function detectInputType(input) {
114
124
  return (constructor || type).toLowerCase();
115
125
  }
116
126
 
127
+ function isNumberJson(str) {
128
+
129
+ str = str.trim();
130
+
131
+ let hasNumber = /\d/.test(str);
132
+ let hasInvalid = /[abcdfghijklmnopqrstuvwz]/gi.test(str);
133
+ if (!hasNumber || hasInvalid) return false
134
+
135
+ // is JSON like
136
+ let isJson = str.startsWith('[') && str.endsWith(']');
137
+
138
+ return isJson
139
+
140
+ }
141
+
117
142
  const {
118
143
  abs: abs$1, acos: acos$1, asin: asin$1, atan: atan$1, atan2: atan2$1, ceil: ceil$1, cos: cos$1, exp: exp$1, floor: floor$1,
119
144
  log: log$1, hypot, max: max$1, min: min$1, pow: pow$1, random: random$1, round: round$1, sin: sin$1, sqrt: sqrt$1, tan: tan$1, PI: PI$1
@@ -438,7 +463,36 @@ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, returnArray
438
463
  /**
439
464
  * get vertices from path command final on-path points
440
465
  */
441
- function getPathDataVertices(pathData) {
466
+
467
+ function getPathDataVertices(pathData=[], includeCpts = false, decimals = -1) {
468
+ let polyPoints = [];
469
+
470
+ pathData.forEach((com) => {
471
+ let { type, values } = com;
472
+
473
+ // get final on path point from last 2 values
474
+ if (values.length) {
475
+
476
+ // round
477
+ if (decimals > -1) values = values.map(val => +val.toFixed(decimals));
478
+
479
+ if (includeCpts) {
480
+
481
+ for (let i = 1; i < values.length; i += 2) {
482
+ polyPoints.push({ x: values[i - 1], y: values[i] });
483
+ }
484
+
485
+ } else {
486
+ polyPoints.push({ x: values[values.length - 2], y: values[values.length - 1] });
487
+ }
488
+
489
+ }
490
+ });
491
+ return polyPoints;
492
+ }
493
+
494
+ /*
495
+ export function getPathDataVertices(pathData) {
442
496
  let polyPoints = [];
443
497
  let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
444
498
 
@@ -453,22 +507,30 @@ function getPathDataVertices(pathData) {
453
507
  }
454
508
  });
455
509
  return polyPoints;
456
- }
510
+ };
511
+ */
457
512
 
458
513
  /**
459
514
  * based on @cuixiping;
460
515
  * https://stackoverflow.com/questions/9017100/calculate-center-of-svg-arc/12329083#12329083
461
516
  */
462
- function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2, y2) {
517
+
518
+ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2, y2, normalize = true
519
+ ) {
463
520
 
464
521
  // helper for angle calculation
465
- const getAngle = (cx, cy, x, y) => {
466
- return atan2(y - cy, x - cx);
522
+ const getAngle = (cx, cy, x, y, normalize = true) => {
523
+ let angle = Math.atan2(y - cy, x - cx);
524
+ if (normalize && angle < 0) angle += Math.PI * 2;
525
+ return angle
467
526
  };
468
527
 
469
528
  // make sure rx, ry are positive
470
- rx = abs(rx);
471
- ry = abs(ry);
529
+ rx = Math.abs(rx);
530
+ ry = Math.abs(ry);
531
+
532
+ // normalize xAxis rotation
533
+ xAxisRotation = rx === ry ? 0 : (xAxisRotation < 0 && normalize ? xAxisRotation + 360 : xAxisRotation);
472
534
 
473
535
  // create data object
474
536
  let arcData = {
@@ -492,53 +554,11 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
492
554
  throw Error("rx and ry can not be 0");
493
555
  }
494
556
 
495
- let shortcut = true;
496
-
497
- if (rx === ry && shortcut) {
498
-
499
- // test semicircles
500
- let diffX = Math.abs(x2 - x1);
501
- let diffY = Math.abs(y2 - y1);
502
- let r = diffX;
503
-
504
- let xMin = Math.min(x1, x2),
505
- yMin = Math.min(y1, y2),
506
- PIHalf = Math.PI * 0.5;
507
-
508
- // semi circles
509
- if (diffX === 0 && diffY || diffY === 0 && diffX) {
510
-
511
- r = diffX === 0 && diffY ? diffY / 2 : diffX / 2;
512
- arcData.rx = r;
513
- arcData.ry = r;
514
-
515
- // verical
516
- if (diffX === 0 && diffY) {
517
- arcData.cx = x1;
518
- arcData.cy = yMin + diffY / 2;
519
- arcData.startAngle = y1 > y2 ? PIHalf : -PIHalf;
520
- arcData.endAngle = y1 > y2 ? -PIHalf : PIHalf;
521
- arcData.deltaAngle = sweep ? Math.PI : -Math.PI;
522
-
523
- }
524
- // horizontal
525
- else if (diffY === 0 && diffX) {
526
- arcData.cx = xMin + diffX / 2;
527
- arcData.cy = y1;
528
- arcData.startAngle = x1 > x2 ? Math.PI : 0;
529
- arcData.endAngle = x1 > x2 ? -Math.PI : Math.PI;
530
- arcData.deltaAngle = sweep ? Math.PI : -Math.PI;
531
- }
532
-
533
- return arcData;
534
- }
535
- }
536
-
537
557
  /**
538
558
  * if rx===ry x-axis rotation is ignored
539
559
  * otherwise convert degrees to radians
540
560
  */
541
- let phi = rx === ry ? 0 : (xAxisRotation * PI) / 180;
561
+ let phi = rx === ry ? 0 : xAxisRotation * deg2rad;
542
562
  let cx, cy;
543
563
 
544
564
  let s_phi = !phi ? 0 : Math.sin(phi);
@@ -557,8 +577,9 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
557
577
  // Step 3: Ensure radii are large enough
558
578
  let lambda = (x1_ * x1_) / (rx * rx) + (y1_ * y1_) / (ry * ry);
559
579
  if (lambda > 1) {
560
- rx = rx * Math.sqrt(lambda);
561
- ry = ry * Math.sqrt(lambda);
580
+ let lambdaRoot = Math.sqrt(lambda);
581
+ rx = rx * lambdaRoot;
582
+ ry = ry * lambdaRoot;
562
583
 
563
584
  // save real rx/ry
564
585
  arcData.rx = rx;
@@ -574,7 +595,7 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
574
595
  throw Error("start point can not be same as end point");
575
596
  }
576
597
  let coe = Math.sqrt(Math.abs((rxry * rxry - sum_of_sq) / sum_of_sq));
577
- if (largeArc == sweep) {
598
+ if (largeArc === sweep) {
578
599
  coe = -coe;
579
600
  }
580
601
 
@@ -594,24 +615,33 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
594
615
  * calculate angles between center point and
595
616
  * commands starting and final on path point
596
617
  */
597
- let startAngle = getAngle(cx, cy, x1, y1);
598
- let endAngle = getAngle(cx, cy, x2, y2);
618
+ let startAngle = getAngle(cx, cy, x1, y1, normalize);
619
+ let endAngle = getAngle(cx, cy, x2, y2, normalize);
599
620
 
600
621
  // adjust end angle
601
- if (!sweep && endAngle > startAngle) {
602
-
603
- endAngle -= Math.PI * 2;
604
- }
605
622
 
606
- if (sweep && startAngle > endAngle) {
607
-
608
- endAngle = endAngle <= 0 ? endAngle + Math.PI * 2 : endAngle;
623
+ // Adjust angles based on sweep direction
624
+ if (sweep) {
625
+ // Clockwise
626
+ if (endAngle < startAngle) {
627
+ endAngle += Math.PI * 2;
628
+ }
629
+ } else {
630
+ // Counterclockwise
631
+ if (endAngle > startAngle) {
632
+ endAngle -= Math.PI * 2;
633
+ }
609
634
  }
610
635
 
611
636
  let deltaAngle = endAngle - startAngle;
637
+
638
+ // The rest of your code remains the same
612
639
  arcData.startAngle = startAngle;
640
+ arcData.startAngle_deg = startAngle * rad2Deg;
613
641
  arcData.endAngle = endAngle;
642
+ arcData.endAngle_deg = endAngle * rad2Deg;
614
643
  arcData.deltaAngle = deltaAngle;
644
+ arcData.deltaAngle_deg = deltaAngle * rad2Deg;
615
645
 
616
646
  return arcData;
617
647
  }
@@ -1117,7 +1147,7 @@ function getDistance(p1, p2, isArray = false) {
1117
1147
  let dx = isArray ? p2[0] - p1[0] : (p2.x - p1.x);
1118
1148
  let dy = isArray ? p2[1] - p1[1] : (p2.y - p1.y);
1119
1149
 
1120
- return sqrt(dx * dx + dy * dy);
1150
+ return Math.sqrt(dx * dx + dy * dy);
1121
1151
  }
1122
1152
 
1123
1153
  function getSquareDistance(p1, p2) {
@@ -1637,7 +1667,7 @@ function detectAccuracy(pathData) {
1637
1667
 
1638
1668
  let dim_min = dims.sort();
1639
1669
 
1640
- let sliceIdx = Math.ceil(dim_min.length / 8);
1670
+ let sliceIdx = Math.ceil(dim_min.length / 6);
1641
1671
  dim_min = dim_min.slice(0, sliceIdx);
1642
1672
  let minVal = dim_min.reduce((a, b) => a + b, 0) / sliceIdx;
1643
1673
 
@@ -1650,6 +1680,7 @@ function detectAccuracy(pathData) {
1650
1680
  }
1651
1681
 
1652
1682
  function roundTo(num = 0, decimals = 3) {
1683
+ if(decimals<=-1) return num;
1653
1684
  if (!decimals) return Math.round(num);
1654
1685
  let factor = 10 ** decimals;
1655
1686
  return Math.round(num * factor) / factor;
@@ -1660,19 +1691,20 @@ function roundTo(num = 0, decimals = 3) {
1660
1691
  * floating point accuracy
1661
1692
  * based on numeric value
1662
1693
  */
1663
- function autoRound(val, integerThresh = 10){
1694
+ function autoRound(val, integerThresh = 50){
1664
1695
  let decimals=8;
1665
-
1666
1696
 
1667
- if(val>integerThresh){
1697
+ if(val>integerThresh*2){
1668
1698
  decimals=0;
1669
1699
  }
1670
- else if(val>5){
1700
+ else if(val>integerThresh){
1671
1701
  decimals=1;
1672
1702
  }else {
1673
- decimals=Math.ceil(integerThresh/val).toString().length;
1703
+ decimals=Math.ceil(500/val).toString().length;
1704
+
1674
1705
  }
1675
1706
 
1707
+
1676
1708
  let factor = 10 ** decimals;
1677
1709
  return Math.round(val * factor) / factor;
1678
1710
  }
@@ -1726,6 +1758,7 @@ const transHorizontal = ['scaleX', 'translateX', 'skewX'];
1726
1758
  const transVertical = ['scaleY', 'translateY', 'skewY'];
1727
1759
 
1728
1760
  const colorProps = ['fill', 'stroke', 'stop-color'];
1761
+ const geometryProps = ['d', 'points', 'cx', 'cy', 'x1', 'x2', 'y1', 'y2', 'width', 'height', 'r', 'rx', 'ry', 'x', 'y'];
1729
1762
 
1730
1763
  const geometryEls = [
1731
1764
  "path",
@@ -2067,7 +2100,14 @@ function svgElUnitsToPixel(el, {
2067
2100
 
2068
2101
  let attributes = [...el.attributes];
2069
2102
  let attNames = attributes.map(att => att.name);
2070
- let attValues = attributes.map(att => att.nodeValue);
2103
+
2104
+ // doesn't work in node!
2105
+
2106
+
2107
+ let attValues = [];
2108
+ attNames.forEach(att=>{
2109
+ attValues.push(el.getAttribute(att));
2110
+ });
2071
2111
 
2072
2112
  let isSquare = width === height;
2073
2113
 
@@ -2612,20 +2652,24 @@ function getPolygonArea(points, absolute=false) {
2612
2652
  * d attribute string
2613
2653
  */
2614
2654
 
2615
- function pathDataToD(pathData, optimize = 0) {
2616
-
2617
- optimize = parseFloat(optimize);
2655
+ function pathDataToD(pathData, mode = 0) {
2618
2656
 
2657
+ mode = parseFloat(mode);
2658
+ /*
2659
+ 0 = max minification
2660
+ 0.5 = safe
2661
+ 1 = verbose
2662
+ 2 = beautify
2663
+ */
2619
2664
  let len = pathData.length;
2620
- let beautify = optimize > 1;
2621
- let minify = beautify || optimize ? false : true;
2622
2665
 
2623
- let d = '';
2624
2666
  let valsString = pathData[0].values.join(" ");
2625
- let separator_command = beautify ? `\n` : (minify ? '' : ' ');
2626
- let separator_type = !minify ? ' ' : '';
2667
+ let separator_command = mode > 1 ? `\n` :
2668
+ ((mode < 1) ? '' : ' ');
2669
+ let separator_type = mode > 0.5 ? ' ' : '';
2627
2670
 
2628
- d = `${pathData[0].type}${separator_type}${valsString}${separator_command}`;
2671
+ // 1st command
2672
+ let d = `${pathData[0].type}${separator_type}${valsString}${separator_command}`;
2629
2673
 
2630
2674
  for (let i = 1; i < len; i++) {
2631
2675
  let com0 = pathData[i - 1];
@@ -2634,7 +2678,7 @@ function pathDataToD(pathData, optimize = 0) {
2634
2678
  valsString = '';
2635
2679
 
2636
2680
  // Minify Arc commands (A/a) – actually sucks!
2637
- if (minify && (type === 'A' || type === 'a')) {
2681
+ if (!mode && (type === 'A' || type === 'a')) {
2638
2682
  values = [
2639
2683
  values[0], values[1], values[2],
2640
2684
  `${values[3]}${values[4]}${values[5]}`,
@@ -2643,14 +2687,14 @@ function pathDataToD(pathData, optimize = 0) {
2643
2687
  }
2644
2688
 
2645
2689
  // Omit type for repeated commands
2646
- type = (minify && com0.type === com.type && com.type.toLowerCase() !== 'm')
2690
+ type = ((mode < 1) && com0.type === com.type && com.type.toLowerCase() !== 'm')
2647
2691
  ? " "
2648
- : (minify && com0.type === "M" && com.type === "L"
2692
+ : ((mode < 1) && com0.type === "M" && com.type === "L"
2649
2693
  ? " "
2650
2694
  : com.type);
2651
2695
 
2652
2696
  // concatenate subsequent floating point values
2653
- if (minify) {
2697
+ if (!mode) {
2654
2698
 
2655
2699
  let prevWasFloat = false;
2656
2700
 
@@ -2674,22 +2718,23 @@ function pathDataToD(pathData, optimize = 0) {
2674
2718
  prevWasFloat = isSmallFloat;
2675
2719
  }
2676
2720
 
2677
- d += `${type}${separator_type}${valsString}${separator_command}`;
2678
-
2679
2721
  }
2680
2722
  // regular non-minified output
2681
2723
  else {
2682
- d += `${type}${separator_type}${values.join(' ')}${separator_command}`;
2724
+ valsString = values.join(' ');
2683
2725
  }
2726
+
2727
+ if(i===len-1) separator_command='';
2728
+ d += `${type}${separator_type}${valsString}${separator_command}`;
2684
2729
  }
2685
2730
 
2686
- if (minify) {
2731
+ if (mode < 1) {
2687
2732
  d = d
2688
2733
  .replace(/[A-Za-z]0(?=\.)/g, m => m[0])
2689
2734
  .replace(/ 0\./g, " .") // Space before small decimals
2690
2735
  .replace(/ -/g, "-") // Remove space before negatives
2691
2736
  .replace(/-0\./g, "-.") // Remove leading zero from negative decimals
2692
- .replace(/Z/g, "z"); // Convert uppercase 'Z' to lowercase
2737
+ .replace(/Z/g, "z"); // Convert uppercase 'Z' to lowercase
2693
2738
  }
2694
2739
 
2695
2740
  return d;
@@ -3566,6 +3611,7 @@ const sanitizeArc = (val='', valueIndex=0) => {
3566
3611
  };
3567
3612
 
3568
3613
  function parsePathDataString(d, debug = true, limit=0) {
3614
+ if(!d) return []
3569
3615
  d = d.trim();
3570
3616
 
3571
3617
  if(limit) console.log('!!!limit', limit);
@@ -3901,6 +3947,7 @@ function convertPathData(pathData, {
3901
3947
  toShorthands = true,
3902
3948
  toLonghands = false,
3903
3949
  toRelative = true,
3950
+ toMixed = false,
3904
3951
  toAbsolute = false,
3905
3952
  decimals = 3,
3906
3953
  arcToCubic = false,
@@ -3911,11 +3958,14 @@ function convertPathData(pathData, {
3911
3958
  hasShorthands = true,
3912
3959
  hasQuadratics = true,
3913
3960
  hasArcs = true,
3961
+ isPoly = false,
3914
3962
  optimizeArcs = true,
3915
3963
  testTypes = false
3916
3964
 
3917
3965
  } = {}) {
3918
3966
 
3967
+ let pathDataAbs = [];
3968
+
3919
3969
  // pathdata properties - test= true adds a manual test
3920
3970
  if (testTypes) {
3921
3971
 
@@ -3929,6 +3979,7 @@ function convertPathData(pathData, {
3929
3979
 
3930
3980
  // some params exclude each other
3931
3981
  toRelative = toAbsolute ? false : toRelative;
3982
+
3932
3983
  toShorthands = toLonghands ? false : toShorthands;
3933
3984
 
3934
3985
  if (toAbsolute) pathData = pathDataToAbsolute(pathData);
@@ -3943,12 +3994,38 @@ function convertPathData(pathData, {
3943
3994
 
3944
3995
  if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
3945
3996
 
3997
+ if(toMixed) toRelative = true;
3998
+
3946
3999
  // pre round - before relative conversion to minimize distortions
3947
4000
  if (decimals > -1 && toRelative) pathData = roundPathData(pathData, decimals);
3948
4001
 
4002
+ // clone absolute pathdata
4003
+ if(toMixed){
4004
+ pathDataAbs = JSON.parse(JSON.stringify(pathData));
4005
+ }
4006
+
3949
4007
  if (toRelative) pathData = pathDataToRelative(pathData);
3950
4008
  if (decimals > -1) pathData = roundPathData(pathData, decimals);
3951
4009
 
4010
+ // choose most compact commands: relative or absolute
4011
+ if(toMixed){
4012
+ for(let i=0; i<pathData.length; i++){
4013
+ let com = pathData[i];
4014
+ let comA = pathDataAbs[i];
4015
+ // compare Lengths
4016
+ let comStr = [com.type, com.values.join(' ')].join('').replaceAll(' -', '-').replaceAll(' 0.', ' .');
4017
+ let comStrA = [comA.type, comA.values.join(' ')].join('').replaceAll(' -', '-').replaceAll(' 0.', ' .');
4018
+
4019
+ let lenR = comStr.length;
4020
+ let lenA = comStrA.length;
4021
+
4022
+ if(lenA<lenR){
4023
+
4024
+ pathData[i] = pathDataAbs[i];
4025
+ }
4026
+ }
4027
+ }
4028
+
3952
4029
  return pathData
3953
4030
  }
3954
4031
 
@@ -3959,6 +4036,9 @@ function convertPathData(pathData, {
3959
4036
  */
3960
4037
 
3961
4038
  function optimizeArcPathData(pathData = []) {
4039
+
4040
+ let remove =[];
4041
+
3962
4042
  pathData.forEach((com, i) => {
3963
4043
  let { type, values } = com;
3964
4044
  if (type === 'A') {
@@ -3968,6 +4048,12 @@ function optimizeArcPathData(pathData = []) {
3968
4048
  let M = { x: x0, y: y0 };
3969
4049
  let p = { x, y };
3970
4050
 
4051
+ if(rx===0 || ry===0){
4052
+ pathData[i]= null;
4053
+ remove.push(i);
4054
+
4055
+ }
4056
+
3971
4057
  // rx and ry are large enough
3972
4058
  if (rx >= 1 && (x === x0 || y === y0)) {
3973
4059
  let diff = Math.abs(rx - ry) / rx;
@@ -3990,6 +4076,8 @@ function optimizeArcPathData(pathData = []) {
3990
4076
  }
3991
4077
  }
3992
4078
  });
4079
+
4080
+ if(remove.length) pathData = pathData.filter(Boolean);
3993
4081
  return pathData;
3994
4082
  }
3995
4083
 
@@ -5615,153 +5703,20 @@ function qrDecomposeMatrix(matrix, precision = 4) {
5615
5703
  return transObj;
5616
5704
  }
5617
5705
 
5618
- function pathElToShape(el, {
5619
- convert_rects = false,
5620
- convert_ellipses = false,
5621
- convert_poly = false,
5622
- convert_lines = false
5623
- } = {}) {
5624
-
5625
- let pathData = parsePathDataNormalized(el.getAttribute('d'));
5626
- let coms = Array.from(new Set(pathData.map(com => com.type))).join('');
5627
-
5628
- let hasArcs = (/[a]/gi).test(coms);
5629
- let hasBeziers = (/[csqt]/gi).test(coms);
5630
- let hasLines = (/[l]/gi).test(coms);
5631
- let isPoly = !(/[acqts]/gi).test(coms);
5632
- let closed = (/[z]/gi).test(coms);
5633
- let shape = null;
5634
- let type = null;
5635
-
5636
- let attributes = getElementAtts(el);
5637
- let attsNew = {};
5638
- let decimals = 7;
5639
-
5640
- if (isPoly) {
5641
-
5642
- // is line
5643
- if (pathData.length === 2 && convert_lines) {
5644
- type = 'line';
5645
- shape = document.createElementNS(svgNs, type);
5646
- let [x1, y1, x2, y2] = [...pathData[0].values, ...pathData[1].values].map(val => roundTo(val, decimals));
5647
- attsNew = { x1, y1, x2, y2 };
5648
- }
5649
- // polygon, polyline or rect
5650
- else {
5651
-
5652
- let vertices = getPathDataVertices(pathData);
5653
- let bb = getPolyBBox(vertices);
5654
- let areaPoly = getPolygonArea(vertices, true);
5655
- let areaRect = bb.width * bb.height;
5656
- let areaDiff = Math.abs(1 - areaRect / areaPoly);
5657
-
5658
- // is rect
5659
- if (convert_rects && areaDiff < 0.01) {
5660
- type = 'rect';
5661
- shape = document.createElementNS(svgNs, type);
5662
- let { x, y, width, height } = bb;
5663
- attsNew = { x, y, width, height };
5664
-
5665
- }
5666
- // polyline or polygon
5667
- else if(convert_poly) {
5668
- type = closed ? 'polygon' : 'polyline';
5669
- shape = document.createElementNS(svgNs, type);
5670
- let points = vertices.map(pt => { return [pt.x, pt.y] }).flat().map(val => roundTo(val, decimals)).join(' ');
5671
- attsNew = { points };
5672
- }
5673
- }
5674
- }
5675
- // circles or ellipses
5676
- else if (!hasLines && convert_ellipses) {
5677
-
5678
- // try to convert cubics to arcs
5679
- if (!hasArcs && hasBeziers) {
5680
- pathData = pathDataCubicsToArc(pathData, { areaThreshold: 2.5 });
5681
- hasArcs = pathData.filter(com => com.type === 'A').length;
5682
- }
5683
-
5684
- if (hasArcs) {
5685
- let pathData2 = getPathDataVerbose(pathData, { addArcParams: true });
5686
- let arcComs = pathData2.filter(com => com.type === 'A');
5687
-
5688
- let cxVals = new Set();
5689
- let cyVals = new Set();
5690
- let rxVals = new Set();
5691
- let ryVals = new Set();
5692
-
5693
- if (arcComs.length > 1) {
5694
-
5695
- pathData2.forEach(com => {
5696
- if (com.type === 'A') {
5697
-
5698
- cxVals.add(roundTo(com.cx, decimals));
5699
- cyVals.add(roundTo(com.cy, decimals));
5700
- rxVals.add(roundTo(com.rx, decimals));
5701
- ryVals.add(roundTo(com.ry, decimals));
5702
- }
5703
- });
5704
- }
5705
-
5706
- cxVals = Array.from(cxVals);
5707
- cyVals = Array.from(cyVals);
5708
- rxVals = Array.from(rxVals);
5709
- ryVals = Array.from(ryVals);
5710
-
5711
- if(cxVals.length===1 && cyVals.length===1 && rxVals.length===1 && ryVals.length===1){
5712
- let [rx, ry, cx, cy] = [rxVals[0], ryVals[0], cxVals[0], cyVals[0]];
5713
- type = rx===ry ? 'circle' : 'ellipse';
5714
- shape = document.createElementNS(svgNs, type);
5715
- attsNew = type==='circle' ? { r:rx, cx, cy } : {rx, ry, cx, cy};
5716
- }
5717
- }
5718
- }
5719
-
5720
- // if el could be replaced
5721
- if (shape) {
5722
- let ignore = ['id', 'class'];
5723
-
5724
- // set shape attributes
5725
- for (let att in attsNew) {
5726
- shape.setAttribute(att, attsNew[att]);
5727
- }
5728
-
5729
- // copy old attributes
5730
- for (let att in attributes) {
5731
-
5732
- if (attLookup.atts[att].includes(type) || ignore.includes(att) || att.startsWith('data-')) {
5733
- shape.setAttribute(att, attributes[att]);
5734
- }
5735
- }
5736
-
5737
- // replace
5738
- el = shape;
5739
- }
5740
-
5741
- return el;
5742
-
5743
- }
5744
-
5706
+ /**
5707
+ * Convert shapes to paths
5708
+ * converts also transforms
5709
+ */
5745
5710
  function shapeElToPath(el, { width = 0,
5746
5711
  height = 0,
5747
- convert_rects = false,
5748
- convert_ellipses = false,
5749
- convert_poly = false,
5750
- convert_lines = false,
5751
-
5752
- matrix=null
5712
+ convertShapes = [],
5713
+ matrix = null
5753
5714
 
5754
5715
  } = {}) {
5755
5716
 
5756
5717
  let nodeName = el.nodeName.toLowerCase();
5757
5718
 
5758
- if (
5759
- nodeName === 'path' && !matrix ||
5760
- nodeName === 'rect' && !convert_rects ||
5761
- (nodeName === 'circle' || nodeName === 'ellipse') && !convert_ellipses ||
5762
- (nodeName === 'polygon' || nodeName === 'polyline') && !convert_poly ||
5763
- (nodeName === 'line') && !convert_lines
5764
- ) return el;
5719
+ if (!convertShapes.includes(nodeName)) return el;
5765
5720
 
5766
5721
  let pathData = getPathDataFromEl(el, { width, height });
5767
5722
 
@@ -5769,8 +5724,9 @@ function shapeElToPath(el, { width = 0,
5769
5724
  let exclude = ['d', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'dx', 'dy', 'r', 'rx', 'ry', 'width', 'height', 'points'];
5770
5725
 
5771
5726
  // transform pathData
5772
- if(matrix && Object.values(matrix).join('')!=='100100'){
5727
+ if (matrix && Object.values(matrix).join('') !== '100100') {
5773
5728
  pathData = transformPathData(pathData, matrix);
5729
+
5774
5730
  exclude.push('transform', 'transform-origin');
5775
5731
  }
5776
5732
 
@@ -5791,13 +5747,6 @@ function shapeElToPath(el, { width = 0,
5791
5747
  return pathN
5792
5748
 
5793
5749
  }
5794
- /*
5795
- export function copyAttributes(newEl, oldEl){
5796
-
5797
- let attributes = [...oldEl.attributes].map(att => att.name);
5798
-
5799
- }
5800
- */
5801
5750
 
5802
5751
  // retrieve pathdata from svg geometry elements
5803
5752
  function getPathDataFromEl(el, {
@@ -5828,39 +5777,7 @@ function getPathDataFromEl(el, {
5828
5777
 
5829
5778
  case 'rect':
5830
5779
  ({ x=0, y=0, width=0, height=0, rx=0, ry=0 } = atts);
5831
-
5832
- if (!rx && !ry) {
5833
- pathData = [
5834
- { type: "M", values: [x, y] },
5835
- { type: "L", values: [x + width, y] },
5836
- { type: "L", values: [x + width, y + height] },
5837
- { type: "L", values: [x, y + height] },
5838
- { type: "Z", values: [] }
5839
- ];
5840
- } else {
5841
-
5842
- rx = rx ? rx : ry;
5843
- ry = ry ? ry : rx;
5844
-
5845
- if (rx > width / 2) {
5846
- rx = width / 2;
5847
- }
5848
- if (ry > height / 2) {
5849
- ry = height / 2;
5850
- }
5851
- pathData = [
5852
- { type: "M", values: [x + rx, y] },
5853
- { type: "L", values: [x + width - rx, y] },
5854
- { type: "A", values: [rx, ry, 0, 0, 1, x + width, y + ry] },
5855
- { type: "L", values: [x + width, y + height - ry] },
5856
- { type: "A", values: [rx, ry, 0, 0, 1, x + width - rx, y + height] },
5857
- { type: "L", values: [x + rx, y + height] },
5858
- { type: "A", values: [rx, ry, 0, 0, 1, x, y + height - ry] },
5859
- { type: "L", values: [x, y + ry] },
5860
- { type: "A", values: [rx, ry, 0, 0, 1, x + rx, y] },
5861
- { type: "Z", values: [] }
5862
- ];
5863
- }
5780
+ pathData = rectToPathData(x, y, width, height, rx, ry);
5864
5781
  break;
5865
5782
 
5866
5783
  case 'circle':
@@ -5920,25 +5837,187 @@ function getPathDataFromEl(el, {
5920
5837
 
5921
5838
  }
5922
5839
 
5923
- function pathDataRemoveColinear(pathData, {
5924
- tolerance = 1,
5925
-
5926
- flatBezierToLinetos = true
5927
- } = {}) {
5840
+ function rectToPathData(x = 0, y = 0, width = 0, height = 0, rx = 0, ry = 0) {
5841
+ let pathData = [];
5928
5842
 
5929
- let pathDataN = [pathData[0]];
5843
+ if (!rx && !ry) {
5844
+ pathData = [
5845
+ { type: "M", values: [x, y] },
5846
+ { type: "L", values: [x + width, y] },
5847
+ { type: "L", values: [x + width, y + height] },
5848
+ { type: "L", values: [x, y + height] },
5849
+ { type: "Z", values: [] }
5850
+ ];
5851
+ } else {
5930
5852
 
5931
- let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
5932
- let p0 = M;
5933
- let p = M;
5934
- pathData[pathData.length - 1].type.toLowerCase() === 'z';
5853
+ rx = rx ? rx : ry;
5854
+ ry = ry ? ry : rx;
5935
5855
 
5936
- for (let c = 1, l = pathData.length; c < l; c++) {
5856
+ if (rx > width / 2) {
5857
+ rx = width / 2;
5858
+ }
5859
+ if (ry > height / 2) {
5860
+ ry = height / 2;
5861
+ }
5862
+ pathData = [
5863
+ { type: "M", values: [x + rx, y] },
5864
+ { type: "L", values: [x + width - rx, y] },
5865
+ { type: "A", values: [rx, ry, 0, 0, 1, x + width, y + ry] },
5866
+ { type: "L", values: [x + width, y + height - ry] },
5867
+ { type: "A", values: [rx, ry, 0, 0, 1, x + width - rx, y + height] },
5868
+ { type: "L", values: [x + rx, y + height] },
5869
+ { type: "A", values: [rx, ry, 0, 0, 1, x, y + height - ry] },
5870
+ { type: "L", values: [x, y + ry] },
5871
+ { type: "A", values: [rx, ry, 0, 0, 1, x + rx, y] },
5872
+ { type: "Z", values: [] }
5873
+ ];
5874
+ }
5937
5875
 
5938
- let com = pathData[c];
5939
- let comN = pathData[c + 1] || pathData[l - 1];
5876
+ return pathData
5877
+ }
5940
5878
 
5941
- let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
5879
+ function pathElToShape(el, {
5880
+ convertShapes = [],
5881
+ } = {}) {
5882
+
5883
+ let pathData = parsePathDataNormalized(el.getAttribute('d'));
5884
+ let coms = Array.from(new Set(pathData.map(com => com.type))).join('');
5885
+
5886
+ let hasArcs = (/[a]/gi).test(coms);
5887
+ let hasBeziers = (/[csqt]/gi).test(coms);
5888
+ let hasLines = (/[l]/gi).test(coms);
5889
+ let isPoly = !(/[acqts]/gi).test(coms);
5890
+ let closed = (/[z]/gi).test(coms);
5891
+ let shape = null;
5892
+ let type = null;
5893
+
5894
+ let attributes = getElementAtts(el);
5895
+ let attsNew = {};
5896
+ let decimals = 7;
5897
+
5898
+ if (isPoly) {
5899
+
5900
+ // is line
5901
+ if (pathData.length === 2 && convertShapes.includes('line')) {
5902
+ type = 'line';
5903
+ shape = document.createElementNS(svgNs, type);
5904
+ let [x1, y1, x2, y2] = [...pathData[0].values, ...pathData[1].values].map(val => roundTo(val, decimals));
5905
+ attsNew = { x1, y1, x2, y2 };
5906
+ }
5907
+ // polygon, polyline or rect
5908
+ else {
5909
+
5910
+ let vertices = getPathDataVertices(pathData);
5911
+ let bb = getPolyBBox(vertices);
5912
+ let areaPoly = getPolygonArea(vertices, true);
5913
+ let areaRect = bb.width * bb.height;
5914
+ let areaDiff = Math.abs(1 - areaRect / areaPoly);
5915
+
5916
+ // is rect
5917
+ if (convertShapes.includes('rect') && areaDiff < 0.01) {
5918
+ type = 'rect';
5919
+ shape = document.createElementNS(svgNs, type);
5920
+ let { x, y, width, height } = bb;
5921
+ attsNew = { x, y, width, height };
5922
+
5923
+ }
5924
+ // polyline or polygon
5925
+ else if (convertShapes.includes('polygon') || convertShapes.includes('polyline')) {
5926
+ type = closed ? 'polygon' : 'polyline';
5927
+ shape = document.createElementNS(svgNs, type);
5928
+ let points = vertices.map(pt => { return [pt.x, pt.y] }).flat().map(val => roundTo(val, decimals)).join(' ');
5929
+ attsNew = { points };
5930
+ }
5931
+ }
5932
+ }
5933
+ // circles or ellipses
5934
+ else if (!hasLines && (convertShapes.includes('circle') || convertShapes.includes('ellipse'))) {
5935
+
5936
+ // try to convert cubics to arcs
5937
+ if (!hasArcs && hasBeziers) {
5938
+ pathData = pathDataCubicsToArc(pathData, { areaThreshold: 2.5 });
5939
+ hasArcs = pathData.filter(com => com.type === 'A').length;
5940
+ }
5941
+
5942
+ if (hasArcs) {
5943
+ let pathData2 = getPathDataVerbose(pathData, { addArcParams: true });
5944
+ let arcComs = pathData2.filter(com => com.type === 'A');
5945
+
5946
+ let cxVals = new Set();
5947
+ let cyVals = new Set();
5948
+ let rxVals = new Set();
5949
+ let ryVals = new Set();
5950
+
5951
+ if (arcComs.length > 1) {
5952
+
5953
+ pathData2.forEach(com => {
5954
+ if (com.type === 'A') {
5955
+
5956
+ cxVals.add(roundTo(com.cx, decimals));
5957
+ cyVals.add(roundTo(com.cy, decimals));
5958
+ rxVals.add(roundTo(com.rx, decimals));
5959
+ ryVals.add(roundTo(com.ry, decimals));
5960
+ }
5961
+ });
5962
+ }
5963
+
5964
+ cxVals = Array.from(cxVals);
5965
+ cyVals = Array.from(cyVals);
5966
+ rxVals = Array.from(rxVals);
5967
+ ryVals = Array.from(ryVals);
5968
+
5969
+ if (cxVals.length === 1 && cyVals.length === 1 && rxVals.length === 1 && ryVals.length === 1) {
5970
+ let [rx, ry, cx, cy] = [rxVals[0], ryVals[0], cxVals[0], cyVals[0]];
5971
+ type = rx === ry ? 'circle' : 'ellipse';
5972
+ shape = document.createElementNS(svgNs, type);
5973
+ attsNew = type === 'circle' ? { r: rx, cx, cy } : { rx, ry, cx, cy };
5974
+ }
5975
+ }
5976
+ }
5977
+
5978
+ // if el could be replaced
5979
+ if (shape) {
5980
+ let ignore = ['id', 'class'];
5981
+
5982
+ // set shape attributes
5983
+ for (let att in attsNew) {
5984
+ shape.setAttribute(att, attsNew[att]);
5985
+ }
5986
+
5987
+ // copy old attributes
5988
+ for (let att in attributes) {
5989
+
5990
+ if (attLookup.atts[att].includes(type) || ignore.includes(att) || att.startsWith('data-')) {
5991
+ shape.setAttribute(att, attributes[att]);
5992
+ }
5993
+ }
5994
+ // replace
5995
+ el = shape;
5996
+ }
5997
+
5998
+ return el;
5999
+
6000
+ }
6001
+
6002
+ function pathDataRemoveColinear(pathData, {
6003
+ tolerance = 1,
6004
+
6005
+ flatBezierToLinetos = true
6006
+ } = {}) {
6007
+
6008
+ let pathDataN = [pathData[0]];
6009
+
6010
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
6011
+ let p0 = M;
6012
+ let p = M;
6013
+ pathData[pathData.length - 1].type.toLowerCase() === 'z';
6014
+
6015
+ for (let c = 1, l = pathData.length; c < l; c++) {
6016
+
6017
+ let com = pathData[c];
6018
+ let comN = pathData[c + 1] || pathData[l - 1];
6019
+
6020
+ let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
5942
6021
 
5943
6022
  let { type, values } = com;
5944
6023
  let valsL = values.slice(-2);
@@ -6722,8 +6801,17 @@ function polyPtsToArray(pts) {
6722
6801
  // convert flat point value array to point object array
6723
6802
  function toPointArray(pts) {
6724
6803
  let ptArr = [];
6725
- for (let i = 1, l = pts.length; i < l; i += 2) {
6726
- ptArr.push({ x: pts[i - 1], y: pts[i] });
6804
+
6805
+ if(pts[0].length===2){
6806
+ for (let i = 0, l = pts.length; i < l; i ++) {
6807
+ let pt = pts[i];
6808
+ ptArr.push({ x: pt[0], y:pt[1] });
6809
+ }
6810
+
6811
+ }else {
6812
+ for (let i = 1, l = pts.length; i < l; i += 2) {
6813
+ ptArr.push({ x: pts[i - 1], y: pts[i] });
6814
+ }
6727
6815
  }
6728
6816
  return ptArr;
6729
6817
  }
@@ -6779,29 +6867,63 @@ function parseStylesProperties(el, {
6779
6867
  autoRoundValues = false,
6780
6868
  minifyRgbColors = false,
6781
6869
  removeInvalid = true,
6870
+ allowDataAtts=true,
6871
+ allowAriaAtts=true,
6782
6872
  removeDefaults = true,
6783
6873
  cleanUpStrokes = true,
6784
6874
  normalizeTransforms = true,
6875
+ removeIds = false,
6876
+ removeClassNames = false,
6877
+
6878
+ include = [],
6785
6879
  exclude = [],
6786
6880
  width = 0,
6787
6881
  height = 0,
6882
+ stylesheetProps = {}
6788
6883
  } = {}) {
6789
6884
 
6790
6885
  let nodeName = el.nodeName.toLowerCase();
6791
6886
  let attProps = getSvgPresentationAtts(el);
6792
- let cssProps = getSvgCssProps(el);
6887
+ let inlineCssProps = getSvgCssProps(el);
6888
+
6889
+ // get CSS properties from SVG style element
6890
+ let cssRuleSelectors = el.cssRules || [];
6891
+ let cssProps = {};
6892
+
6893
+ cssRuleSelectors.forEach(selector => {
6894
+ for (let prop in stylesheetProps[selector]) {
6895
+ let val = stylesheetProps[selector][prop];
6896
+
6897
+ // check CSS vars
6898
+ if (val.startsWith('var(')) {
6899
+ let varName = val.replace(/[var\(|\)]/g, '').trim();
6900
+
6901
+ if (stylesheetProps[':root']) {
6902
+ val = `var(${varName}, ${stylesheetProps[':root'][varName]})`;
6903
+ }
6904
+ }
6905
+ cssProps[prop] = val;
6906
+ }
6907
+ });
6793
6908
 
6794
6909
  /**
6795
- * merge props
6796
- * CSS has higher specificity
6910
+ * merge props by specificity
6911
+ * 1. attributes
6912
+ * 2. CSS rules
6913
+ * 3. inline CSS
6797
6914
  */
6798
6915
  let props = {
6799
6916
  ...attProps,
6800
6917
  ...cssProps,
6918
+ ...inlineCssProps,
6801
6919
  };
6802
6920
 
6921
+ // obsolete/not style relevant anymore
6803
6922
  delete props['style'];
6804
- exclude.push('style');
6923
+ delete props['class'];
6924
+ delete props['id'];
6925
+
6926
+ exclude.push('style', 'class', 'id');
6805
6927
 
6806
6928
  let remove = ['style'];
6807
6929
  let transformsStandalone = ['scale', 'translate', 'rotate'];
@@ -6812,9 +6934,10 @@ function parseStylesProperties(el, {
6812
6934
  */
6813
6935
 
6814
6936
  if (removeInvalid || removeDefaults || removeNameSpaced) {
6815
- let propsFilteredObj = filterSvgElProps(nodeName, props, { removeDefaults, removeNameSpaced, exclude, cleanUpStrokes, include: transformsStandalone, cleanUpStrokes: false });
6937
+ let propsFilteredObj = filterSvgElProps(nodeName, props, {allowDataAtts, allowAriaAtts, removeIds, removeClassNames, removeDefaults, removeNameSpaced, exclude, cleanUpStrokes, include: [...transformsStandalone, ...include], cleanUpStrokes: false });
6816
6938
  props = propsFilteredObj.propsFiltered;
6817
6939
  remove.push(...propsFilteredObj.remove);
6940
+
6818
6941
  }
6819
6942
 
6820
6943
  // sanitized prop array
@@ -6833,11 +6956,14 @@ function parseStylesProperties(el, {
6833
6956
 
6834
6957
  // minify rgb values
6835
6958
  if (minifyRgbColors && colorProps.includes(prop)) {
6959
+
6836
6960
  let color = parseColor(valueStr);
6961
+
6837
6962
  if (color.mode === 'rgba' || color.mode === 'rgb') {
6838
6963
  let hex = rgba2Hex(color);
6839
6964
  valueStr = hex;
6840
6965
  }
6966
+
6841
6967
  }
6842
6968
 
6843
6969
  if (prop === 'transform') {
@@ -6877,7 +7003,6 @@ function parseStylesProperties(el, {
6877
7003
  else {
6878
7004
 
6879
7005
  item.values = parseValue(valueStr);
6880
-
6881
7006
  }
6882
7007
 
6883
7008
  if (item.values.length) {
@@ -6952,6 +7077,7 @@ function parseStylesProperties(el, {
6952
7077
 
6953
7078
  if (autoRoundValues && isNumeric) {
6954
7079
  valAbs = autoRound(valAbs);
7080
+
6955
7081
  }
6956
7082
 
6957
7083
  }
@@ -7146,16 +7272,24 @@ function filterSvgElProps(elNodename = '', props = {}, {
7146
7272
  removeInvalid = true,
7147
7273
  removeDefaults = true,
7148
7274
  allowDataAtts = true,
7275
+ allowMeta = false,
7276
+ allowAriaAtts = false,
7149
7277
  cleanUpStrokes = true,
7150
- include = ['id', 'class'],
7278
+
7279
+ include = [],
7280
+ removeIds = false,
7281
+ removeClassNames = false,
7151
7282
  exclude = [],
7152
7283
  } = {}) {
7153
7284
  let propsFiltered = {};
7154
7285
  let remove = [];
7155
7286
 
7287
+ if (!removeIds) include.push('id');
7288
+ if (!removeClassNames) include.push('class');
7289
+
7156
7290
  // allow defaults for nested
7157
7291
 
7158
- let noStrokeColor = cleanUpStrokes ? (props['stroke'] === undefined) : false;
7292
+ let noStrokeColor = cleanUpStrokes ? (props['stroke'] === undefined || props['stroke'][0] === 'none') : false;
7159
7293
 
7160
7294
  for (let prop in props) {
7161
7295
  let values = props[prop];
@@ -7167,43 +7301,41 @@ function filterSvgElProps(elNodename = '', props = {}, {
7167
7301
  false;
7168
7302
 
7169
7303
  // remove null transforms
7170
- if(prop==='transform' && value==='matrix(1 0 0 1 0 0)') isValid = false;
7304
+ if (prop === 'transform' && value === 'matrix(1 0 0 1 0 0)') isValid = false;
7171
7305
 
7172
7306
  // allow data attributes
7173
- let isDataAtt = allowDataAtts ? prop.startsWith('data-') : false;
7307
+ let isDataAtt = prop.startsWith('data-') ? true : false;
7308
+ let isMeta = prop === 'title';
7309
+ let isAria = prop.startsWith('aria-');
7310
+
7311
+ if( (allowDataAtts && isDataAtt) || (allowAriaAtts && isAria) || (allowMeta && isMeta ) ) continue
7174
7312
 
7175
7313
  // filter out defaults
7176
7314
  let isDefault = removeDefaults ?
7177
7315
  (attLookup.defaults[prop] ? attLookup.defaults[prop] !== undefined && attLookup.defaults[prop].includes(value) : false) :
7178
7316
  false;
7179
7317
 
7180
- if (isDataAtt || include.includes(prop)) isValid = true;
7181
- if (isDefault) isValid = false;
7182
- if (exclude.length && exclude.includes(prop)) isValid = false;
7183
- if (noStrokeColor && strokeAtts.includes(prop)) isValid = false;
7318
+ let isFutileStroke = noStrokeColor && strokeAtts.includes(prop);
7319
+
7320
+ if (isDefault || isDataAtt || isMeta || isAria || isFutileStroke) isValid = false;
7321
+ if (include.includes(prop)) isValid = true;
7184
7322
 
7185
7323
  if (isValid) {
7186
7324
  propsFiltered[prop] = props[prop];
7187
7325
  }
7188
7326
  else {
7327
+
7189
7328
  remove.push(prop);
7190
7329
  }
7191
7330
  }
7192
7331
 
7193
- /*
7194
- // set explicit stroke width when disabled by stroke color
7195
- if (propsFiltered['stroke'] && propsFiltered['stroke'][0] === 'none') {
7196
- propsFiltered['stroke-width'] = [1]
7197
- remove.push('stroke', 'stroke-width')
7198
- console.log('remove', remove);
7199
- }
7200
- */
7201
-
7202
7332
  return { propsFiltered, remove }
7203
7333
  }
7204
7334
 
7205
7335
  function parseValue(valStr = '') {
7206
- let valArr = valStr.split(/,| /);
7336
+
7337
+ valStr = valStr.replace('!important', '');
7338
+ let valArr = (valStr.includes("'") || valStr.includes('(') || valStr.includes(')')) ? [valStr] : valStr.split(/,| /).filter(Boolean);
7207
7339
 
7208
7340
  for (let i = 0; i < valArr.length; i++) {
7209
7341
 
@@ -7284,110 +7416,787 @@ function parseInlineCss(styleAtt = '') {
7284
7416
  return props
7285
7417
  }
7286
7418
 
7287
- function removeEmptySVGEls(svg) {
7288
- let els = svg.querySelectorAll('g, defs');
7289
- els.forEach(el => {
7290
- if (!el.children.length) el.remove();
7291
- });
7419
+ function toCamelCase(str) {
7420
+ return str
7421
+ .split(/[-| ]/)
7422
+ .map((e, i) => i
7423
+ ? e.charAt(0).toUpperCase() + e.slice(1).toLowerCase()
7424
+ : e.toLowerCase()
7425
+ )
7426
+ .join('')
7292
7427
  }
7293
7428
 
7294
- function cleanUpSVG(svgMarkup, {
7295
- removeHidden = true,
7429
+ function toShortStr(str) {
7430
+ if (isNumericValue(str)) return str
7431
+ let strShort = str.split('-').map(str => { return str.replace(/a|e|i|o|u/g, '') }).join('-');
7432
+ strShort = toCamelCase(strShort);
7433
+ return strShort
7434
+ }
7296
7435
 
7297
- stylesToAttributes = true,
7298
- removePrologue = true,
7299
- removeIds = false,
7300
- removeClassNames = false,
7301
- removeDimensions = false,
7302
- fixHref = false,
7303
- legacyHref = false,
7304
- cleanupDefs = true,
7305
- cleanupClip = true,
7306
- addViewBox = false,
7307
- addDimensions = false,
7308
- minifyRgbColors = false,
7436
+ function stringifySVG(svg, {
7437
+ omitNamespace = false,
7438
+ removeComments = true,
7439
+ format = 0,
7440
+ } = {}) {
7309
7441
 
7310
- normalizeTransforms = true,
7311
- autoRoundValues = true,
7442
+ let markup = '';
7312
7443
 
7313
- unGroup = false,
7444
+ if (format < 2) {
7445
+ markup = new XMLSerializer().serializeToString(svg);
7314
7446
 
7315
- mergePaths = false,
7316
- removeOffCanvas = true,
7317
- cleanupSVGAtts = true,
7318
- removeNameSpaced = true,
7319
- attributesToGroup = true,
7447
+ markup = minifySVGMarkup(markup, { removeComments });
7448
+
7449
+ } else {
7450
+ markup = serializeSVGPretty(svg);
7451
+ }
7320
7452
 
7321
- shapeConvert = false,
7322
- convert_rects = false,
7323
- convert_ellipses = false,
7324
- convert_poly = false,
7325
- convert_lines = false,
7453
+ if (omitNamespace) {
7454
+ markup = markup.replaceAll('xmlns="http://www.w3.org/2000/svg"', '');
7455
+ }
7326
7456
 
7327
- convertTransforms = false,
7328
- removeDefaults = true,
7329
- cleanUpStrokes = true,
7330
- decimals = -1,
7331
- excludedEls = [],
7332
- } = {}) {
7457
+ if (removeComments) {
7458
+ markup = markup
7459
+ .replace(/(<!--.*?-->)|(<!--[\S\s]+?-->)|(<!--[\S\s]*?$)/g, '');
7460
+ }
7333
7461
 
7334
- // replace namespaced refs
7335
- if (fixHref) svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
7462
+ /*
7463
+ markup = markup
7464
+ .replace(/\t/g, "")
7465
+ .replace(/[\n\r|]/g, "\n")
7466
+ .replace(/\n\s*\n/g, '\n')
7467
+ .replace(/ +/g, ' ')
7336
7468
 
7337
- let svg = new DOMParser()
7338
- .parseFromString(svgMarkup, "text/html")
7339
- .querySelector("svg");
7469
+ .replace(/> </g, '><')
7470
+ .trim()
7471
+ // sanitize linebreaks within pathdata
7472
+ .replaceAll('&#10;', '\n');
7473
+ */
7340
7474
 
7341
- let viewBox = getViewBox(svg);
7342
- let { x, y, width, height } = viewBox;
7475
+ return markup
7476
+ }
7343
7477
 
7344
- // get svg styles
7345
- let propOptions = {
7346
- width: width,
7347
- height: height,
7348
- normalizeTransforms,
7349
- removeDefaults: false,
7350
- cleanUpStrokes: false,
7351
- autoRoundValues,
7352
- minifyRgbColors,
7353
- };
7354
- let stylePropsSVG = parseStylesProperties(svg, propOptions);
7478
+ function minifySVGMarkup(svg, {
7479
+ removeComments = true,
7480
+ } = {}) {
7355
7481
 
7356
- // add svg font size for scaling relative
7357
- propOptions.fontSize = stylePropsSVG['font-size'] ? stylePropsSVG['font-size'][0] : 16;
7482
+ if (removeComments) {
7483
+ svg = svg.replace(/<!--[\s\S]*?-->/g, '');
7484
+ }
7358
7485
 
7359
- /**
7360
- * get group styles
7361
- * especially transformations to
7362
- * be inherited by children
7363
- */
7364
- let groups = svg.querySelectorAll('g');
7365
- let groupProps = [];
7486
+ // Remove whitespace between tags
7487
+ svg = svg.replace(/>\s+</g, '><')
7488
+ // Trim leading/trailing whitespace
7489
+ .trim()
7490
+ // Remove extra whitespace within tags (attributes)
7491
+ .replace(/\s+([=])\s+/g, '$1')
7492
+ .replace(/\s+(?=[^<]*>)/g, ' ')
7493
+ // Collapse multiple spaces to single space
7494
+ .replace(/\s{2,}/g, ' ')
7495
+ // Remove spaces around = signs in attributes
7496
+ .replace(/\s*=\s*/g, '=');
7366
7497
 
7367
- groups.forEach(g => {
7368
- let stylePropsG = parseStylesProperties(g, propOptions);
7369
- groupProps.push(stylePropsG);
7370
- let children = g.querySelectorAll(`${renderedEls.join(', ')}`);
7498
+ return svg
7499
+ }
7371
7500
 
7372
- // store parent styles to child property
7373
- children.forEach(child => {
7374
- if (child.parentStyleProps === undefined) {
7375
- child.parentStyleProps = [];
7501
+ function serializeSVGPretty(xmlDoc, {
7502
+ indentSize = 2 } = {}) {
7503
+ if (typeof xmlDoc === 'string') {
7504
+ xmlDoc = new DOMParser().parseFromString(xmlDoc, 'image/svg+xml').querySelector('svg');
7505
+ }
7506
+ return formatXMLNode(xmlDoc, 0, indentSize);
7507
+ }
7508
+
7509
+ function formatXMLNode(node, level, indentSize) {
7510
+ let indent = " ".repeat(level * indentSize);
7511
+
7512
+ if (node.nodeType === Node.TEXT_NODE) {
7513
+ let text = node.textContent.trim();
7514
+ return text ? text : "";
7515
+ }
7516
+
7517
+ if (node.nodeType === Node.ELEMENT_NODE) {
7518
+ let hasChildren = node.children.length > 0;
7519
+ let hasTextContent = node.textContent.trim().length > 0 && node.children.length === 0;
7520
+
7521
+ let result = `${indent}<${node.nodeName}`;
7522
+
7523
+ // Add attributes
7524
+ for (let i = 0; i < node.attributes.length; i++) {
7525
+ let att = node.attributes[i];
7526
+ result += ` ${att.name}="${att.value}"`;
7527
+ }
7528
+
7529
+ if (!hasChildren && !hasTextContent) {
7530
+ return result + " />\n";
7531
+ }
7532
+
7533
+ result += ">";
7534
+
7535
+ if (hasChildren) {
7536
+ result += "\n";
7537
+ for (let child of node.children) {
7538
+ result += formatXMLNode(child, level + 1, indentSize);
7376
7539
  }
7377
- child.parentStyleProps.push(stylePropsG);
7540
+ result += `${indent}</${node.nodeName}>\n`;
7541
+ } else if (hasTextContent) {
7542
+ result += node.textContent.trim();
7543
+ result += `</${node.nodeName}>\n`;
7544
+ } else {
7545
+ result += `</${node.nodeName}>\n`;
7546
+ }
7547
+
7548
+ return result;
7549
+ }
7550
+
7551
+ return "";
7552
+ }
7553
+
7554
+ function removeHiddenSvgEls(svg) {
7555
+ let els = svg.querySelectorAll('*');
7556
+ els.forEach(el => {
7557
+ el.nodeName.toLowerCase();
7558
+ let style = el.getAttribute('style') || '';
7559
+ let isHiddenByStyle = style ? style.trim().includes('display:none') : false;
7560
+ let isHidden = (el.getAttribute('display') && el.getAttribute('display') === 'none') || isHiddenByStyle;
7561
+ if (isHidden) el.remove();
7562
+ });
7563
+
7564
+ }
7565
+
7566
+ function removeSvgEls(svg, {
7567
+ removeElements = [],
7568
+ removeNameSpaced = true,
7569
+ } = {}) {
7570
+
7571
+ // always remove scripts
7572
+ removeElements.push('script');
7573
+
7574
+ let els = svg.querySelectorAll('*');
7575
+ let allowMeta = !removeElements.includes('metadata');
7576
+
7577
+ els.forEach(el => {
7578
+ let nodeName = el.nodeName;
7579
+ let isMeta = allowMeta && el.closest('metadata');
7580
+ if (
7581
+ !isMeta &&
7582
+ ((removeNameSpaced && nodeName.includes(':')) ||
7583
+ removeElements.includes(nodeName))
7584
+ ) {
7585
+ el.remove();
7586
+ }
7587
+ });
7588
+ }
7589
+
7590
+ /*export function removeSvgEls(svg, remove = []) {
7591
+ // remove elements
7592
+ if (remove.length) {
7593
+ let selector = remove.join(', ').replaceAll(':', '\\:');
7594
+ svg.querySelectorAll(selector).forEach(el => {
7595
+ el.remove()
7596
+ })
7597
+ }
7598
+ }
7599
+ */
7600
+
7601
+ function removeSvgAtts(svg, remove = []) {
7602
+ remove.forEach(att => {
7603
+ svg.removeAttribute(att);
7604
+ });
7605
+ }
7606
+
7607
+ function removeSvgChildAtts(svg, remove = []) {
7608
+ if (remove.length) {
7609
+ let selector = remove.map(att => { return `[${att}]` }).join(', ')
7610
+ // escape name spaced
7611
+ .replaceAll(':', '\\:');
7612
+
7613
+ svg.querySelectorAll(selector).forEach(el => {
7614
+ remove.forEach(att => {
7615
+ el.removeAttribute(att);
7616
+ });
7378
7617
  });
7618
+ }
7619
+ }
7620
+
7621
+ /**
7622
+ * general clean up to remove bullshit like
7623
+ * version or enable background
7624
+ */
7625
+
7626
+ function cleanupSVGAttributes(svg, {
7627
+ removeIds = false,
7628
+ removeClassNames = false,
7629
+ removeDimensions = false,
7630
+ stylesToAttributes = false,
7631
+ allowMeta = false,
7632
+ allowAriaAtts = false,
7633
+ allowDataAtts = false,
7634
+ } = {}) {
7635
+
7636
+ let allowed = new Set(['viewBox', 'xmlns', 'width', 'height']);
7637
+
7638
+ if (!removeIds) allowed.add('id');
7639
+ if (!removeClassNames) allowed.add('class');
7640
+ if (removeDimensions) {
7641
+ allowed.delete('width');
7642
+ allowed.delete('height');
7643
+ }
7644
+
7645
+ allowed = Array.from(allowed);
7646
+ if (!stylesToAttributes) {
7647
+ allowed.push('fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'font-size', 'font-family', 'font-style', 'style');
7648
+ }
7649
+
7650
+ removeExcludedAttribues(svg, { allowed, allowMeta, allowAriaAtts, allowDataAtts });
7651
+ }
7652
+
7653
+ function removeExcludedAttribues(el, {
7654
+ allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class'],
7655
+ allowAriaAtts = true,
7656
+ allowDataAtts = true,
7657
+ allowMeta = false
7658
+ } = {}) {
7659
+ let atts = [...el.attributes].map((att) => att.name);
7660
+ atts.forEach((att) => {
7661
+
7662
+ let isMeta = allowMeta && (att === 'title');
7663
+ let isAria = allowAriaAtts && att.startsWith('aria-');
7664
+ let isData = allowDataAtts && att.startsWith('data-');
7665
+
7666
+ if (
7667
+ !allowed.includes(att) &&
7668
+ !isAria && !isData && !isMeta
7669
+ ) {
7670
+ el.removeAttribute(att);
7671
+ }
7672
+ });
7673
+ }
7674
+
7675
+ function removeElAtts(el, exclude = [], include = []) {
7676
+ let atts = [...el.attributes].map((att) => att.name);
7677
+ atts.forEach((att) => {
7678
+ if (exclude.includes(att) && !include.includes(att)) {
7679
+ el.removeAttribute(att);
7680
+ }
7379
7681
  });
7682
+ }
7683
+
7684
+ /*
7685
+
7686
+ function cleanSvgPrologue(svgString) {
7687
+ return (
7688
+ svgString
7689
+ // Remove XML prologues like <?xml ... ?>
7690
+ .replace(/<\?xml[\s\S]*?\?>/gi, "")
7691
+ // Remove DOCTYPE declarations
7692
+ .replace(/<!DOCTYPE[\s\S]*?>/gi, "")
7693
+ // Remove comments <!-- ... -->
7694
+ .replace(/<!--[\s\S]*?-->/g, "")
7695
+ // Trim extra whitespace
7696
+ .trim()
7697
+ );
7698
+ }
7699
+ */
7700
+
7701
+ function convertPathLengthAtt(el, {
7702
+ styleProps = {}
7703
+ }={}) {
7704
+
7705
+ let pathLength = el.getAttribute('pathLength') ? +el.getAttribute('pathLength') : 0;
7706
+
7707
+ if (pathLength && (styleProps['stroke-dasharray'] || styleProps['stroke-dashoffset'])) {
7708
+ let elLength = getElementLength(el, {
7709
+ pathLength,
7710
+ props: styleProps
7711
+ });
7712
+
7713
+ let scale = elLength / pathLength;
7714
+
7715
+ styleProps = scaleProps(styleProps, { props: ['stroke-dasharray', 'stroke-dashoffset'], scale });
7716
+ [styleProps['stroke-dasharray'], styleProps['stroke-dashoffset']];
7380
7717
 
7381
- if (cleanupSVGAtts) {
7718
+ // tag for removal
7719
+ delete styleProps['pathLength'];
7720
+ styleProps.remove.push('pathLength');
7721
+ el.removeAttribute('pathLength');
7382
7722
 
7383
- let allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class'];
7384
- if (!stylesToAttributes) {
7385
- allowed.push('fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'font-size', 'font-family', 'font-style', 'style');
7386
7723
  }
7387
7724
 
7388
- removeExcludedAttribues(svg, allowed);
7725
+ return styleProps;
7726
+
7727
+ }
7728
+
7729
+ function ungroupElements(groups) {
7730
+ groups.forEach((g, i) => {
7731
+ let children = [...g.children];
7732
+
7733
+ children.forEach(child => {
7734
+ g.parentNode.insertBefore(child, g);
7735
+ });
7736
+ g.remove();
7737
+ });
7738
+ }
7739
+
7740
+ /**
7741
+ * Parse nested CSS text into a flat object structure
7742
+ * Supports arbitrary nesting depth and & parent selector reference
7743
+ * Respects !important modifiers and handles data URLs
7744
+ */
7745
+ function parseSvgCss(css, {
7746
+ parent=null,
7747
+ removeUnused=true,
7748
+ flatten = true
7749
+ }={}) {
7750
+
7751
+ let type = typeof css;
7752
+ if(type==='string') removeUnused = false;
7753
+
7754
+ // get style element text content
7755
+ if(type!=='string' ){
7756
+ if(css.nodeName==='style'){
7757
+ css = css.innerHTML;
7758
+ }
7759
+ else if(css.nodeName==='svg'){
7760
+ let styleEl = css.querySelector('style');
7761
+ if(!styleEl) return {}
7762
+ parent = css;
7763
+ css = styleEl.innerHTML;
7764
+ }
7765
+
7766
+ else {
7767
+ console.warn('invalid CSS input');
7768
+ return {}
7769
+ }
7770
+ }
7771
+
7772
+ css = css.trim();
7773
+ if (!css) return {};
7774
+
7775
+ // Remove comments
7776
+ css = css.replace(/\/\*[\s\S]*?\*\//g, "");
7777
+
7778
+ function parseBlock(text, parentSelector = "") {
7779
+ let i = 0;
7780
+ let rules = {};
7781
+ let l = text.length;
7782
+
7783
+ while (i < l) {
7784
+ // Skip whitespace
7785
+ while (/\s/.test(text[i])) i++;
7786
+ if (i >= l) break;
7787
+
7788
+ // Peek ahead to check if this is a selector or a declaration
7789
+ let peekIdx = i;
7790
+ let isSelector = false;
7791
+
7792
+ // Look for '{' before ';' to determine if it's a selector
7793
+ while (peekIdx < l && text[peekIdx] !== ";") {
7794
+ if (text[peekIdx] === "{") {
7795
+ isSelector = true;
7796
+ break;
7797
+ }
7798
+ peekIdx++;
7799
+ }
7800
+
7801
+ if (!isSelector) {
7802
+ // It's a declaration, skip it (will be handled below)
7803
+ i = peekIdx + 1;
7804
+ continue;
7805
+ }
7806
+
7807
+ // Read selector (up to '{')
7808
+ let selector = "";
7809
+ while (i < l && text[i] !== "{") {
7810
+ selector += text[i];
7811
+ i++;
7812
+ }
7813
+
7814
+ selector = selector.trim();
7815
+ if (!selector || text[i] !== "{") continue;
7816
+
7817
+ i++; // skip '{'
7818
+
7819
+ // Find matching closing brace
7820
+ let blockContent = "";
7821
+ let depth = 1;
7822
+
7823
+ while (i < l && depth > 0) {
7824
+ if (text[i] === "{") depth++;
7825
+ else if (text[i] === "}") depth--;
7826
+
7827
+ if (depth > 0) blockContent += text[i];
7828
+ i++;
7829
+ }
7830
+
7831
+ // Compose full selector
7832
+ let fullSelector = selector;
7833
+ if (parentSelector) {
7834
+ if (selector.includes("&")) {
7835
+ fullSelector = selector.replace(/&/g, parentSelector);
7836
+ } else {
7837
+ fullSelector = parentSelector + " " + selector;
7838
+ }
7839
+ }
7840
+ fullSelector = fullSelector.replace(/\s+/g, " ").trim();
7841
+
7842
+ // Separate declarations from nested rules
7843
+ let { declarations, hasNested } = extractDeclarations(blockContent);
7844
+
7845
+ // Add declarations for this selector (respect !important)
7846
+ if (Object.keys(declarations).length) {
7847
+ if (!rules[fullSelector]) {
7848
+ rules[fullSelector] = declarations;
7849
+ } else {
7850
+ // Merge declarations, preserving !important
7851
+ for (let prop in declarations) {
7852
+ let existingValue = rules[fullSelector][prop];
7853
+ let newValue = declarations[prop];
7854
+
7855
+ // Only override if existing doesn't have !important, or new has !important
7856
+ let existingHasImportant =
7857
+ existingValue && existingValue.includes("!important");
7858
+ let newHasImportant = newValue.includes("!important");
7859
+
7860
+ if (!existingHasImportant || newHasImportant) {
7861
+ rules[fullSelector][prop] = newValue;
7862
+ }
7863
+ }
7864
+ }
7865
+ }
7866
+
7867
+ // If block contains nested rules, parse them recursively
7868
+ if (hasNested) {
7869
+ parseBlock(blockContent, fullSelector);
7870
+ }
7871
+ }
7872
+
7873
+ return rules
7874
+
7875
+ }
7876
+
7877
+ function extractDeclarations(content) {
7878
+ let declarations = {};
7879
+ let i = 0;
7880
+ let l= content.length;
7881
+ let hasNested = false;
7882
+
7883
+ while (i < l) {
7884
+ // Skip whitespace
7885
+ while (i < l && /\s/.test(content[i])) i++;
7886
+ if (i >= l) break;
7887
+
7888
+ // Check if next thing is a nested selector or a declaration
7889
+ let checkIdx = i;
7890
+ let isNested = false;
7891
+
7892
+ // Scan until we hit ':' or '{' or ';'
7893
+ while (checkIdx < l) {
7894
+ if (content[checkIdx] === "{") {
7895
+ isNested = true;
7896
+ break;
7897
+ }
7898
+ if (content[checkIdx] === ":") {
7899
+ // It's a declaration
7900
+ break;
7901
+ }
7902
+ if (content[checkIdx] === ";") {
7903
+ // Empty or malformed
7904
+ break;
7905
+ }
7906
+ checkIdx++;
7907
+ }
7908
+
7909
+ if (isNested) {
7910
+ // Skip nested rule (will be handled by recursive call)
7911
+ hasNested = true;
7912
+ // Skip to closing brace of this nested rule
7913
+ let depth = 0;
7914
+ while (i < l) {
7915
+ if (content[i] === "{") depth++;
7916
+ if (content[i] === "}") depth--;
7917
+ i++;
7918
+ if (depth === 0) break;
7919
+ }
7920
+ } else {
7921
+ // It's a declaration, read until ';' (but respect url() and quotes)
7922
+ let decl = "";
7923
+ let inUrl = false;
7924
+ let inQuotes = false;
7925
+ let quoteChar = "";
7926
+
7927
+ while (i < l) {
7928
+ let char = content[i];
7929
+ let nextChar = content[i + 1];
7930
+
7931
+ // Track if we're inside url()
7932
+ if (
7933
+ char === "u" &&
7934
+ nextChar === "r" &&
7935
+ content.slice(i, i + 4) === "url("
7936
+ ) {
7937
+ inUrl = true;
7938
+ }
7939
+
7940
+ // Track quotes
7941
+ if (
7942
+ (char === '"' || char === "'") &&
7943
+ (i === 0 || content[i - 1] !== "\\")
7944
+ ) {
7945
+ if (!inQuotes) {
7946
+ inQuotes = true;
7947
+ quoteChar = char;
7948
+ } else if (char === quoteChar) {
7949
+ inQuotes = false;
7950
+ quoteChar = "";
7951
+ }
7952
+ }
7953
+
7954
+ // Check for end of url()
7955
+ if (inUrl && char === ")" && !inQuotes) {
7956
+ inUrl = false;
7957
+ }
7958
+
7959
+ // Only break on semicolon if we're not inside url() or quotes
7960
+ if (char === ";" && !inUrl && !inQuotes) {
7961
+ i++; // skip ';'
7962
+ break;
7963
+ }
7964
+
7965
+ decl += char;
7966
+ i++;
7967
+ }
7968
+
7969
+ decl = decl.trim();
7970
+ if (decl) {
7971
+ let colonIdx = decl.indexOf(":");
7972
+ if (colonIdx > -1) {
7973
+ let prop = decl.substring(0, colonIdx).trim();
7974
+ let value = decl.substring(colonIdx + 1).trim();
7975
+ if (prop && value) {
7976
+
7977
+ declarations[prop] = value;
7978
+ }
7979
+ }
7980
+ }
7981
+ }
7982
+ }
7983
+
7984
+ return { declarations, hasNested };
7985
+ }
7986
+
7987
+ let rules = parseBlock(css);
7988
+ if(parent && removeUnused) rules = removeUnusedSelectors(parent, rules);
7989
+ if(flatten) rules = flattenCssProps(rules);
7990
+
7991
+ // emulate specificity: prioritize ids and important
7992
+ let rulesID = {};
7993
+ let rulesImportant = {};
7994
+ for(let rule in rules){
7995
+ if(rule.startsWith('#')){
7996
+ rulesID[rule] = rules[rule];
7997
+ delete rules[rule];
7998
+ }
7999
+
8000
+ for(let prop in rules[rule]){
8001
+ let val = rules[rule][prop];
8002
+ if(val.includes('!important')){
8003
+ if(!rulesImportant[rule]) rulesImportant[rule]={};
8004
+ rulesImportant[rule][prop] = val;
8005
+ }
8006
+ }
8007
+ }
8008
+
8009
+ rules= {
8010
+ ...rules,
8011
+ ...rulesID,
8012
+ ...rulesImportant
8013
+ };
8014
+
8015
+ return rules;
8016
+ }
8017
+
8018
+ function flattenCssProps(rules) {
8019
+ for (let selector in rules) {
8020
+ let targets = selector.split(/,/).map((sel) => sel.trim());
8021
+ rules[selector];
8022
+ if (targets.length > 1) {
8023
+ targets.forEach((target) => {
8024
+ let props = rules[target];
8025
+ for (let prop in props) {
8026
+ let value = props[prop];
8027
+ if (!value.includes("!important")) {
8028
+ rules[target][prop] = value;
8029
+ }
8030
+ }
8031
+ });
8032
+ delete rules[selector];
8033
+ }
8034
+ }
8035
+ return rules;
8036
+ }
8037
+
8038
+ function removeUnusedSelectors(parent=null, props={}){
8039
+ let selectors = Object.keys(props);
8040
+ selectors.forEach(selector=>{
8041
+ let el = parent.querySelector(selector);
8042
+ // remove
8043
+ if(!el && selector!==':root') {
8044
+
8045
+ delete props[selector];
8046
+ }
8047
+ });
8048
+ return props
8049
+ }
8050
+
8051
+ function setNormalizedTransformsToEl(el, {
8052
+ styleProps = {},
8053
+ } = {}) {
8054
+ let { remove, matrix, transComponents } = styleProps;
8055
+ let name = el.nodeName.toLowerCase();
8056
+
8057
+ if(!matrix) return styleProps;
8058
+
8059
+ let { rotate, scaleX, scaleY, skewX, translateX, translateY } = transComponents;
8060
+
8061
+ // scale attributes instead of transform
8062
+ let hasRot = rotate !== 0 || skewX !== 0;
8063
+ let unProportional = scaleX !== scaleY;
8064
+ let scalableByAtt = ['circle', 'ellipse', 'rect'];
8065
+
8066
+ let needsTrans = (hasRot) || unProportional;
8067
+ needsTrans = true;
8068
+
8069
+ if (!needsTrans && scalableByAtt.includes(name)) {
8070
+
8071
+ if (name === 'circle' || name === 'ellipse') {
8072
+ styleProps.cx[0] = [styleProps.cx[0] * scaleX + translateX];
8073
+ styleProps.cy[0] = [styleProps.cy[0] * scaleX + translateY];
8074
+
8075
+ if (styleProps.r) styleProps.r[0] = [styleProps.r[0] * scaleX];
8076
+ if (styleProps.rx) styleProps.rx[0] = [styleProps.rx[0] * scaleX];
8077
+ if (styleProps.ry) styleProps.ry[0] = [styleProps.ry[0] * scaleX];
8078
+
8079
+ }
8080
+ else if (name === 'rect') {
8081
+ let x = styleProps.x ? styleProps.x[0] + translateX : translateX;
8082
+ let y = styleProps.y ? styleProps.y[0] + translateY : translateY;
8083
+
8084
+ let rx = styleProps.rx ? styleProps.rx[0] * scaleX : 0;
8085
+ let ry = styleProps.ry ? styleProps.ry[0] * scaleY : 0;
8086
+
8087
+ styleProps.x = [x];
8088
+ styleProps.y = [y];
8089
+
8090
+ styleProps.rx = [rx];
8091
+ styleProps.ry = [ry];
8092
+
8093
+ styleProps.width = [styleProps.width[0] * scaleX];
8094
+ styleProps.height = [styleProps.height[0] * scaleX];
8095
+ }
8096
+
8097
+ // remove now obsolete transform properties
8098
+ delete styleProps.matrix;
8099
+ delete styleProps.transformArr;
8100
+ delete styleProps.transComponents;
8101
+
8102
+ // mark transform attribute for removal
8103
+ styleProps.remove.push('transform');
8104
+
8105
+ // scale props like stroke width or dash-array
8106
+ styleProps = scaleProps$1(styleProps, { props: ['stroke-width', 'stroke-dasharray'], scale: scaleX });
8107
+
8108
+ } else {
8109
+ el.setAttribute('transform', transComponents.matrixAtt);
8110
+
7389
8111
  }
7390
8112
 
8113
+ return styleProps
8114
+
8115
+ }
8116
+
8117
+ function scaleProps$1(styleProps = {}, { props = [], scale = 1 } = {}, round = true) {
8118
+ if (scale === 1 || !props.length) return props;
8119
+
8120
+ for (let i = 0; i < props.length; i++) {
8121
+ let prop = props[i];
8122
+
8123
+ if (styleProps[prop] !== undefined) {
8124
+ styleProps[prop] = styleProps[prop].map(val => round ? roundTo(val * scale, 2) : val * scale);
8125
+ }
8126
+ }
8127
+ return styleProps
8128
+ }
8129
+
8130
+ function cleanUpSVG(svgMarkup, {
8131
+ removeHidden = true,
8132
+
8133
+ stylesToAttributes = true,
8134
+ attributesToGroup = false,
8135
+
8136
+ removePrologue = true,
8137
+ removeIds = false,
8138
+ removeClassNames = false,
8139
+ removeDimensions = false,
8140
+ fixHref = false,
8141
+ legacyHref = false,
8142
+ cleanupDefs = true,
8143
+ cleanupClip = true,
8144
+ addViewBox = false,
8145
+ addDimensions = false,
8146
+ minifyRgbColors = false,
8147
+
8148
+ normalizeTransforms = true,
8149
+ autoRoundValues = true,
8150
+
8151
+ unGroup = false,
8152
+
8153
+ mergePaths = false,
8154
+ removeOffCanvas = true,
8155
+
8156
+ cleanupSVGAtts = true,
8157
+ removeNameSpaced = true,
8158
+ removeNameSpacedAtts = true,
8159
+ convertPathLength = false,
8160
+
8161
+ // meta
8162
+ allowMeta = false,
8163
+ allowDataAtts = true,
8164
+ allowAriaAtts = true,
8165
+
8166
+ shapeConvert = false,
8167
+ convertShapes = [],
8168
+
8169
+ // remove elements
8170
+ removeElements = [],
8171
+
8172
+ // remove attributes
8173
+ removeSVGAttributes = [],
8174
+ removeElAttributes = [],
8175
+
8176
+ convertTransforms = false,
8177
+ removeDefaults = true,
8178
+ cleanUpStrokes = true,
8179
+ decimals = -1,
8180
+ excludedEls = [],
8181
+ } = {}) {
8182
+
8183
+ // resolve dependencies
8184
+ if (unGroup || convertTransforms || minifyRgbColors || attributesToGroup)
8185
+ stylesToAttributes = true;
8186
+
8187
+ if(stylesToAttributes) cleanUpStrokes = true;
8188
+
8189
+ // replace namespaced refs
8190
+ if (fixHref) svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
8191
+
8192
+ let svg = new DOMParser()
8193
+ .parseFromString(svgMarkup, "text/html")
8194
+ .querySelector("svg");
8195
+
8196
+ let viewBox = getViewBox(svg);
8197
+ let { x, y, width, height } = viewBox;
8198
+ let remove = [];
8199
+
7391
8200
  // add viewBox
7392
8201
  if (addViewBox) addSvgViewBox(svg, { x, y, width, height });
7393
8202
  if (addDimensions) {
@@ -7401,42 +8210,120 @@ function cleanUpSVG(svgMarkup, {
7401
8210
  // remove off canvas
7402
8211
  if (removeOffCanvas) removeOffCanvasEls(svg, { x, y, width, height });
7403
8212
 
7404
- // always remove scripts
7405
- let removeEls = ['metadata', 'script', ...excludedEls];
8213
+ /**
8214
+ * collect svg styles
8215
+ * and properties
8216
+ */
8217
+ let propOptions = {
8218
+ width,
8219
+ height,
8220
+ normalizeTransforms,
8221
+ removeDefaults: false,
8222
+ cleanUpStrokes: false,
8223
+
8224
+ allowMeta,
8225
+ allowDataAtts,
8226
+ allowAriaAtts,
8227
+ autoRoundValues,
8228
+ removeIds,
8229
+ removeClassNames,
8230
+ minifyRgbColors,
8231
+ stylesheetProps: {},
8232
+ exclude:[]
8233
+ };
8234
+
8235
+ // root svg inline style properties
8236
+ let stylePropsSVG = parseStylesProperties(svg, propOptions);
8237
+
8238
+ let styleEl = svg.querySelector('style');
8239
+ let cssStylePropsSVG = {};
8240
+
8241
+ if (styleEl) {
8242
+ cssStylePropsSVG = parseSvgCss(styleEl, { parent: svg });
8243
+
8244
+ for (let selector in cssStylePropsSVG) {
8245
+ let els = svg.querySelectorAll(`${selector}`);
8246
+ els.forEach(el => {
8247
+ if (!el['cssRules']) el['cssRules'] = [];
8248
+ el['cssRules'].push(selector);
8249
+
8250
+ // remove class names only used for styling
8251
+ if (stylesToAttributes) {
8252
+ let className = selector.substring(1);
8253
+ el.classList.remove(className);
8254
+ }
8255
+ });
8256
+ }
8257
+
8258
+ // remove style element from element
8259
+ if (stylesToAttributes) {
8260
+ styleEl.remove();
8261
+ }
8262
+ }
8263
+ // remove style element from root SVG
8264
+ if (stylesToAttributes) svg.removeAttribute('style');
7406
8265
 
7407
- removeSVGEls(svg, { removeEls, removeNameSpaced });
8266
+ // add stylesheet props
8267
+ propOptions.stylesheetProps = cssStylePropsSVG;
7408
8268
 
7409
- // an array of all elements' properties
8269
+ // add svg font size for scaling relative
8270
+ propOptions.fontSize = stylePropsSVG['font-size'] ? stylePropsSVG['font-size'][0] : 16;
8271
+
8272
+ /**
8273
+ * get group styles
8274
+ * especially transformations to
8275
+ * be inherited by children
8276
+ */
8277
+ let groups = svg.querySelectorAll('g');
8278
+
8279
+ groups.forEach(g => {
8280
+
8281
+ let stylePropsG = parseStylesProperties(g, propOptions);
8282
+ let children = g.querySelectorAll(`${renderedEls.join(', ')}`);
8283
+
8284
+ // store parent styles to child property
8285
+ children.forEach(child => {
8286
+ if (child.parentStyleProps === undefined) {
8287
+ child.parentStyleProps = [];
8288
+ }
8289
+ child.parentStyleProps.push(stylePropsG);
8290
+ });
8291
+ });
8292
+
8293
+ // collect all elements' properties
7410
8294
  let svgElProps = [];
7411
8295
  let els = svg.querySelectorAll(`${renderedEls.join(', ')}`);
7412
8296
 
8297
+ /**
8298
+ * loop all geometry elements
8299
+ */
7413
8300
  for (let i = 0; i < els.length; i++) {
7414
8301
  let el = els[i];
7415
8302
 
7416
8303
  let name = el.nodeName.toLowerCase();
7417
8304
 
7418
- // 1. remove hidden elements
7419
- let style = el.getAttribute('style') || '';
7420
- let isHiddenByStyle = style ? style.trim().includes('display:none') : false;
7421
- let isHidden = (el.getAttribute('display') && el.getAttribute('display') === 'none') || isHiddenByStyle;
7422
- if (name.includes(':') || removeEls.includes(name) || (removeHidden && isHidden)) {
7423
- el.remove();
7424
- continue;
7425
- }
7426
-
7427
8305
  /**
7428
- * get all style properties
8306
+ * get all element style properties
7429
8307
  * convert relative or physical units
7430
8308
  * to user units
7431
8309
  */
7432
8310
  let styleProps = parseStylesProperties(el, propOptions);
8311
+ let stylePropsFiltered = {};
8312
+
8313
+ // convert pathLength before transforming
8314
+ if (convertPathLength) {
8315
+ styleProps = convertPathLengthAtt(el, { styleProps });
8316
+ remove = [...new Set([...remove, ...styleProps.remove])];
8317
+ }
7433
8318
 
7434
8319
  // get parent styles
7435
8320
  let { parentStyleProps = [] } = el;
7436
8321
  let inheritedProps = {};
7437
8322
  let transFormInherited = [];
7438
8323
 
7439
- /** inherit transforms
8324
+ /**
8325
+ * consolidate all properties:
8326
+ * merge with inherited transforms
7440
8327
  * and styles from group
7441
8328
  */
7442
8329
  parentStyleProps.forEach(props => {
@@ -7451,9 +8338,14 @@ function cleanUpSVG(svgMarkup, {
7451
8338
  };
7452
8339
  });
7453
8340
 
8341
+ // merge all transforms
7454
8342
  transFormInherited = [...transFormInherited, ...styleProps.transformArr];
7455
8343
  styleProps.transformArr = transFormInherited;
7456
8344
 
8345
+ // don't inherit class from SVG
8346
+ if (stylePropsSVG['class']) delete stylePropsSVG['class'];
8347
+ if (stylePropsSVG['id']) delete stylePropsSVG['id'];
8348
+
7457
8349
  // merge with svg props
7458
8350
  styleProps = {
7459
8351
  ...stylePropsSVG,
@@ -7464,195 +8356,378 @@ function cleanUpSVG(svgMarkup, {
7464
8356
  // add combined transforms
7465
8357
  addTransFormProps(styleProps, transFormInherited);
7466
8358
 
7467
- let { remove, matrix, transComponents } = styleProps;
8359
+ remove = [...new Set([...remove, ...styleProps.remove])];
8360
+
8361
+ /**
8362
+ * remove els and attributes
8363
+ */
8364
+
8365
+ // remove meta
8366
+ if (!allowMeta) removeElements.push('meta', 'metadata', 'desc', 'title');
8367
+
8368
+ if (removeClassNames) {
8369
+ removeSVGAttributes.push('class');
8370
+ removeElAttributes.push('class');
8371
+ }
8372
+
8373
+ if (removeIds) {
8374
+ removeSVGAttributes.push('id');
8375
+ removeElAttributes.push('id');
8376
+ }
8377
+
8378
+ // remove hidden elements
8379
+ removeHiddenSvgEls(svg);
8380
+
8381
+ // remove SVG elements
8382
+ removeSvgEls(svg, { removeElements, removeNameSpaced });
8383
+
8384
+ // remove SVG attributes
8385
+ removeSvgAtts(svg, removeSVGAttributes);
8386
+
8387
+ // remove SVG child element attributes
8388
+ removeSvgChildAtts(svg, removeElAttributes);
8389
+
8390
+ // general cleanup
8391
+ if (cleanupSVGAtts) cleanupSVGAttributes(svg, { removeIds, removeClassNames, removeDimensions, stylesToAttributes, allowMeta, allowAriaAtts, allowDataAtts });
8392
+
8393
+ if (stylesToAttributes) {
8394
+
8395
+ /**
8396
+ * normalize transforms
8397
+ */
8398
+ if (normalizeTransforms) {
8399
+ styleProps = setNormalizedTransformsToEl(el, { styleProps });
8400
+
8401
+ remove = [...new Set([...remove, ...styleProps.remove])];
8402
+
8403
+ }
8404
+
8405
+ /**
8406
+ * apply consolidated
8407
+ * element attributes
8408
+ * remove non-supported element props
8409
+ */
8410
+ stylePropsFiltered = filterSvgElProps(name, styleProps,
8411
+ { removeDefaults: true, cleanUpStrokes, allowMeta, allowAriaAtts, allowDataAtts, removeIds });
8412
+
8413
+ remove = [...new Set([...remove, ...stylePropsFiltered.remove])];
8414
+
8415
+ for (let prop in stylePropsFiltered.propsFiltered) {
8416
+ let values = styleProps[prop];
8417
+ let val = values.length ? values.join(' ') : values[0];
8418
+ el.setAttribute(prop, val);
8419
+ }
8420
+
8421
+ /**
8422
+ * remove obsolete
8423
+ * attributes
8424
+ */
8425
+
8426
+ for (let i = 0; i < remove.length; i++) {
8427
+ let att = remove[i];
8428
+
8429
+ el.removeAttribute(att);
8430
+ }
8431
+
8432
+ } // endof style processing
8433
+
8434
+ /**
8435
+ * element conversions:
8436
+ * shapes to paths or
8437
+ * paths to shapes
8438
+ */
8439
+
8440
+ // force shape conversion when transform conversion is enabled
8441
+ if (convertTransforms) {
8442
+ shapeConvert = 'toPaths';
8443
+ convertShapes = ['path', 'rect', 'ellipse', 'circle', 'line', 'polygon', 'polyline'];
8444
+ }
8445
+
8446
+ // convert shapes to paths
8447
+ if (shapeConvert === 'toPaths') {
8448
+
8449
+ let { matrix = null, transComponents = null } = styleProps;
8450
+
8451
+ // scale props like stroke width or dash-array before conversion
8452
+ if (matrix && transComponents) {
8453
+ ['stroke-width', 'stroke-dasharray'].forEach(att => {
8454
+ let attVal = el.getAttribute(att);
8455
+ let vals = attVal ? attVal.split(' ').filter(Boolean).map(Number).map(val => val * transComponents.scaleX) : [];
8456
+ if (vals.length) el.setAttribute(att, vals.join(' '));
8457
+ });
8458
+ }
8459
+
8460
+ // convert paths only if a matrix transform is required
8461
+ if (matrix ? geometryEls.includes(name) : shapeEls.includes(name)) {
8462
+
8463
+ let path = shapeElToPath(el, { width, height, convertShapes, matrix });
8464
+ el.replaceWith(path);
8465
+ name = 'path';
8466
+ el = path; // required for node
8467
+
8468
+ }
8469
+
8470
+ }
8471
+
8472
+ /**
8473
+ * Reverse conversion:
8474
+ * paths to shapes
8475
+ */
8476
+ else if (shapeConvert === 'toShapes') {
8477
+ let paths = svg.querySelectorAll('path');
8478
+ paths.forEach(path => {
8479
+ let shape = pathElToShape(path, { convertShapes });
8480
+ path.replaceWith(shape);
8481
+ path = shape;
8482
+
8483
+ });
8484
+ }
8485
+
8486
+ /**
8487
+ * combine styles
8488
+ * store in node property
8489
+ */
8490
+ if (mergePaths || attributesToGroup) {
8491
+
8492
+ let options = { allowMeta, allowAriaAtts, removeIds, removeClassNames, allowDataAtts };
8493
+
8494
+ /**
8495
+ * exclude properties for
8496
+ * adjacent path merging
8497
+ * e.g ignore classnames or ids
8498
+ */
8499
+ if (mergePaths) {
8500
+ options.removeIds = true;
8501
+ options.removeClassNames = true;
8502
+ options.allowAriaAtts = false;
8503
+ options.allowMeta = false;
8504
+ }
8505
+
8506
+ stylePropsFiltered = filterSvgElProps(name, styleProps, options).propsFiltered;
8507
+
8508
+ for (let prop in stylePropsFiltered) {
8509
+
8510
+ if (geometryProps.includes(prop)) continue;
8511
+
8512
+ let values = stylePropsFiltered[prop];
8513
+ let val = values.length ? values.join(' ') : values[0];
8514
+
8515
+ if(prop!=='class' && prop!=='id'){
8516
+
8517
+ let propShort = toShortStr(prop);
8518
+ let valShort = toShortStr(val);
8519
+ let propStr = `${propShort}-${valShort}`;
8520
+
8521
+ // store in node property
8522
+ if (!el.styleSet) el.styleSet = new Set();
8523
+ if(propStr) el.styleSet.add(propStr);
8524
+ }
8525
+ }
8526
+
8527
+ }
8528
+
8529
+ }//endof element loop
8530
+
8531
+ /**
8532
+ * remove group styles
8533
+ * copied to children
8534
+ * or remove nesting
8535
+ */
8536
+
8537
+ if (unGroup) {
8538
+ ungroupElements(groups);
8539
+ } else {
7468
8540
 
7469
- // mark attributes for removal
7470
- if (removeClassNames) styleProps.remove.push('class');
7471
- if (removeIds) styleProps.remove.push('id');
7472
- if (removeDimensions) {
7473
- styleProps.remove.push('width');
7474
- styleProps.remove.push('height');
8541
+ if (stylesToAttributes) {
8542
+ groups.forEach(g => {
8543
+ removeElAtts(g, ['style', 'transform']);
8544
+ });
7475
8545
  }
7476
8546
 
7477
- // styles to atts
7478
- if (unGroup || convertTransforms || minifyRgbColors ) stylesToAttributes = true;
8547
+ }
7479
8548
 
7480
- if (stylesToAttributes) {
8549
+ // styles to group
8550
+ if (attributesToGroup) sharedAttributesToGroup(svg);
7481
8551
 
7482
- /**
7483
- * normalize transforms
7484
- */
7485
- if (normalizeTransforms && matrix) {
7486
- let { rotate, scaleX, scaleY, skewX, translateX, translateY } = transComponents;
8552
+ /**
8553
+ * merge paths with same styles
8554
+ */
8555
+ if (mergePaths) {
8556
+ mergePathsWithSameProps(svg);
8557
+ }
7487
8558
 
7488
- // scale attributes instead of transform
7489
- let hasRot = rotate !== 0 || skewX !== 0;
7490
- let unProportional = scaleX !== scaleY;
7491
- let scalableByAtt = ['circle', 'ellipse', 'rect'];
7492
- let needsTrans = convertTransforms || (name === 'g') || (hasRot) || unProportional;
8559
+ // remove futile clip-paths
8560
+ if (cleanupClip) removeFutileClipPaths(svg, { x, y, width, height });
7493
8561
 
7494
- if (!needsTrans && scalableByAtt.includes(name)) {
8562
+ // replace href attributes with namespace - required by many older applications
8563
+ if (legacyHref) hrefToXlink(svg);
7495
8564
 
7496
- if (name === 'circle' || name === 'ellipse') {
7497
- styleProps.cx[0] = [styleProps.cx[0] * scaleX + translateX];
7498
- styleProps.cy[0] = [styleProps.cy[0] * scaleX + translateY];
8565
+ // remove empty class attributes
8566
+ removeEmptyClassAtts(svg);
8567
+ return { svg, svgElProps }
7499
8568
 
7500
- if (styleProps.r) styleProps.r[0] = [styleProps.r[0] * scaleX];
8569
+ }
7501
8570
 
7502
- if (styleProps.rx) styleProps.rx[0] = [styleProps.rx[0] * scaleX];
7503
- if (styleProps.ry) styleProps.ry[0] = [styleProps.ry[0] * scaleX];
8571
+ function removeEmptyClassAtts(svg) {
8572
+ let emptyClassEls = svg.querySelectorAll('[class=""');
8573
+ emptyClassEls.forEach(el => {
8574
+ el.removeAttribute('class');
8575
+ });
8576
+ }
7504
8577
 
7505
- }
7506
- else if (name === 'rect') {
7507
- let x = styleProps.x ? styleProps.x[0] + translateX : translateX;
7508
- let y = styleProps.y ? styleProps.y[0] + translateY : translateY;
8578
+ /**
8579
+ * shared styles to group
8580
+ */
8581
+ function sharedAttributesToGroup(svg) {
7509
8582
 
7510
- let rx = styleProps.rx ? styleProps.rx[0] * scaleX : 0;
7511
- let ry = styleProps.ry ? styleProps.ry[0] * scaleY : 0;
8583
+ let els = svg.querySelectorAll(renderedEls.join(', '));
8584
+ let len = els.length;
8585
+ if(len===1) return;
7512
8586
 
7513
- styleProps.x = [x];
7514
- styleProps.y = [y];
8587
+ let el0 = els[0] || null;
8588
+ let stylePrev = el0.styleSet !== undefined ? [...el0.styleSet].join('_') : '';
7515
8589
 
7516
- styleProps.rx = [rx];
7517
- styleProps.ry = [ry];
8590
+ // all props
8591
+ let allProps = {};
7518
8592
 
7519
- styleProps.width = [styleProps.width[0] * scaleX];
7520
- styleProps.height = [styleProps.height[0] * scaleX];
7521
- }
8593
+ // find attributes shared by all
8594
+ let globalAtts = [];
7522
8595
 
7523
- remove.push('transform');
8596
+ if (len) {
7524
8597
 
7525
- // scale props like stroke width or dash-array
7526
- styleProps = scaleProps(styleProps, { props: ['stroke-width', 'stroke-dasharray'], scale: scaleX });
8598
+ let groups = [[el0]];
8599
+ let idx = 0;
8600
+ let elPrev = el0;
7527
8601
 
7528
- } else {
7529
- el.setAttribute('transform', transComponents.matrixAtt);
8602
+ for (let i = 0; i < len; i++) {
8603
+ let el = els[i];
8604
+ let atts = getElementAtts(el);
8605
+ for (let att in atts) {
8606
+ let att_str = `${att}_${atts[att]}`;
7530
8607
 
8608
+ if (!allProps[att_str]) {
8609
+ allProps[att_str] = [];
8610
+ }
8611
+ allProps[att_str].push(el);
8612
+ //
8613
+ if (allProps[att_str].length === len) {
8614
+ globalAtts.push(att);
7531
8615
  }
7532
8616
  }
8617
+ }
7533
8618
 
7534
- /**
7535
- * apply consolidated
7536
- * element attributes
7537
- */
8619
+ // apply global to parent SVG
8620
+ if (globalAtts.length) {
8621
+ let atts0 = getElementAtts(el0);
8622
+ for (let att in atts0) {
8623
+ if (globalAtts.includes(att) && att !== 'transform') {
8624
+ svg.setAttribute(att, atts0[att]);
8625
+ }
8626
+ }
8627
+ }
7538
8628
 
7539
- let stylePropsFiltered = filterSvgElProps(name, styleProps,
7540
- { removeDefaults: true, cleanUpStrokes });
8629
+ // detect groups
8630
+ for (let i = 1; i < len; i++) {
8631
+ let el = els[i];
8632
+ let styleArr = el.styleSet !== undefined ? [...el.styleSet] : [];
8633
+ let style = styleArr.length ? styleArr.join('_') : '';
7541
8634
 
7542
- remove = [...remove, ...stylePropsFiltered.remove];
8635
+ // same style add to group
8636
+ if (style === stylePrev && elPrev.nextElementSibling === el) {
8637
+ groups[idx].push(el);
8638
+ }
8639
+ // start new group
8640
+ else {
8641
+ groups.push([el]);
8642
+ idx++;
8643
+ }
8644
+ // update style
8645
+ stylePrev = style;
8646
+ elPrev = el;
7543
8647
 
7544
- for (let prop in stylePropsFiltered.propsFiltered) {
7545
- let values = styleProps[prop];
8648
+ }// endof el loop
7546
8649
 
7547
- let val = values.length ? values.join(' ') : values[0];
7548
- el.setAttribute(prop, val);
7549
- }
8650
+ // create groups
8651
+ for (let i = 0; i < groups.length; i++) {
8652
+ let children = groups[i];
8653
+ let child0 = children[0];
8654
+ let atts = getElementAtts(child0);
8655
+ let groupEl = child0.parentNode.closest('g');
7550
8656
 
7551
- // remove obsolete attributes
7552
- for (let i = 0; i < remove.length; i++) {
7553
- let att = remove[i];
7554
- if (!stylesToAttributes && att === 'style') continue
8657
+ // only 1 child - nothing to group
8658
+ if (children.length === 1) continue
7555
8659
 
7556
- el.removeAttribute(att);
7557
- }
8660
+ // create new group
8661
+ if (!groupEl || groups.length>1) {
7558
8662
 
7559
- /**
7560
- * remove group styles
7561
- * copied to children
7562
- * or remove nesting
7563
- */
8663
+ groupEl = document.createElementNS(svgNs, 'g');
8664
+ child0.parentNode.insertBefore(groupEl, child0);
8665
+ groupEl.append(...children);
8666
+ }
7564
8667
 
7565
- if (unGroup) {
7566
- groups.forEach((g, i) => {
7567
- let children = [...g.children];
8668
+ // move attributes to group
8669
+ for (let att in atts) {
8670
+ let val = atts[att];
7568
8671
 
8672
+ let excludeAtts = ['id', 'class'];
8673
+ if (!geometryProps.includes(att) && !excludeAtts.includes(att)) {
8674
+ if (!globalAtts.includes(att) || att === 'transform') {
8675
+ groupEl.setAttribute(att, val);
8676
+ }
7569
8677
  children.forEach(child => {
7570
- g.parentNode.insertBefore(child, g);
7571
- });
7572
- g.remove();
7573
- });
7574
- } else {
7575
- groups.forEach((g, i) => {
7576
- let atts = [...Object.keys(groupProps[i]), 'style', 'transform'];
7577
- atts.forEach(att => {
7578
- g.removeAttribute(att);
8678
+ child.removeAttribute(att);
7579
8679
  });
7580
- });
7581
-
8680
+ }
7582
8681
  }
7583
8682
 
7584
- } // endof style processing
8683
+ } // endof groups
7585
8684
 
7586
- /**
7587
- * element conversions:
7588
- * shapes to paths or
7589
- * paths to shapes
7590
- */
8685
+ }
8686
+ }
7591
8687
 
7592
- // force shape conversion when transform conversion is enabled
7593
- if (convertTransforms) {
7594
- shapeConvert = 'toPaths';
7595
- convert_rects = true;
7596
- convert_ellipses = true;
7597
- convert_poly = true;
7598
- convert_lines = true;
7599
- }
8688
+ // merge adjacent paths
8689
+ function mergePathsWithSameProps(svg) {
8690
+ let paths = svg.querySelectorAll('path');
8691
+ let len = paths.length;
7600
8692
 
7601
- // convert shapes to paths
7602
- if (shapeConvert === 'toPaths') {
8693
+ if (len) {
8694
+ let path0 = paths[0];
8695
+ let d0 = path0.getAttribute('d');
8696
+ let stylePrev = path0.styleSet !== undefined ? [...path0.styleSet].join(' ') : '';
7603
8697
 
7604
- let { matrix = null, transComponents = null } = styleProps;
8698
+ let remove = [];
7605
8699
 
7606
- if (matrix && transComponents) {
7607
- // scale props like stroke width or dash-array before conversion
7608
- ['stroke-width', 'stroke-dasharray'].forEach(att => {
7609
- let attVal = el.getAttribute(att);
7610
- let vals = attVal ? attVal.split(' ').filter(Boolean).map(Number).map(val => val * transComponents.scaleX) : [];
7611
- if (vals.length) el.setAttribute(att, vals.join(' '));
7612
- });
7613
- }
8700
+ for (let i = 1; i < len; i++) {
8701
+ let path = paths[i];
8702
+ let style = path.styleSet !== undefined ? [...path.styleSet].join(' ') : '';
8703
+ let isSibling = path.previousElementSibling === path0;
8704
+ let d = path.getAttribute('d');
8705
+ let isAbs = d.startsWith('M');
7614
8706
 
7615
- // convert paths only if a matrix transform is required
7616
- if (matrix ? geometryEls.includes(name) : shapeEls.includes(name)) {
8707
+ if (isSibling && style === stylePrev) {
8708
+ let dAbs = isAbs ? d : parsePathDataString(d).pathData.map(com => `${com.type} ${com.values.join(' ')}`).join(' ');
7617
8709
 
7618
- let path = shapeElToPath(el, { width, height, convert_rects, convert_ellipses, convert_poly, convert_lines, matrix });
7619
- el.replaceWith(path);
8710
+ d0 += dAbs;
8711
+ path0.setAttribute('d', d0);
7620
8712
 
7621
- name = 'path';
7622
- el = path;
8713
+ remove.push(path);
7623
8714
 
7624
- }
8715
+ } else {
8716
+ path0 = path;
7625
8717
 
7626
- }
8718
+ d0 = isAbs ? d : parsePathDataString(d).pathData.map(com => `${com.type} ${com.values.join(' ')}`).join(' ');
7627
8719
 
7628
- // convert paths to shapes
7629
- else if (shapeConvert === 'toShapes') {
7630
- let paths = svg.querySelectorAll('path');
7631
- paths.forEach(path => {
7632
- let shape = pathElToShape(path, { convert_rects, convert_ellipses, convert_poly, convert_lines });
7633
- path.replaceWith(shape);
7634
- path = shape;
7635
- });
8720
+ }
7636
8721
 
8722
+ // update style
8723
+ stylePrev = style;
7637
8724
  }
7638
8725
 
7639
- }//endof element loop
7640
-
7641
- // remove futile clip-paths
7642
- if (cleanupClip) removeFutileClipPaths(svg, { x, y, width, height });
7643
-
7644
- // replace href attributes with namespace - required by many older applications
7645
- if (legacyHref) {
7646
- svg.setAttribute('xmlns:xlink', "http://www.w3.org/1999/xlink");
7647
- let hrefs = svg.querySelectorAll('[href]');
7648
- hrefs.forEach(el => {
7649
- let href = el.getAttribute('href');
7650
- el.setAttribute('xlink:href', href);
7651
- el.removeAttribute('href');
8726
+ remove.forEach(el => {
8727
+ el.remove();
7652
8728
  });
7653
- }
7654
8729
 
7655
- return { svg, svgElProps }
8730
+ }
7656
8731
 
7657
8732
  }
7658
8733
 
@@ -7713,8 +8788,6 @@ function cleanupSvgDefs(svg, { x = 0, y = 0, width = 0, height = 0, cleanupClip
7713
8788
  }
7714
8789
  });
7715
8790
 
7716
- // remove futile clip-paths
7717
-
7718
8791
  }
7719
8792
 
7720
8793
  function removeFutileClipPaths(svg, { x = 0, y = 0, width = 0, height = 0 } = {}) {
@@ -7766,72 +8839,16 @@ function removeFutileClipPaths(svg, { x = 0, y = 0, width = 0, height = 0 } = {}
7766
8839
 
7767
8840
  }
7768
8841
 
7769
- function scaleProps(styleProps = {}, { props = [], scale = 1 } = {}) {
7770
- if (scale === 1 || !props.length) return props;
7771
-
7772
- for (let i = 0; i < props.length; i++) {
7773
- let prop = props[i];
7774
-
7775
- if (styleProps[prop] !== undefined) {
7776
- styleProps[prop] = styleProps[prop].map(val => val * scale);
7777
- }
7778
- }
7779
- return styleProps
7780
- }
7781
-
7782
- function removeSVGEls(svg, {
7783
- remove = ['metadata', 'script'],
7784
- removeNameSpaced = true,
7785
- } = {}) {
7786
- let els = svg.querySelectorAll('*');
7787
- els.forEach(el => {
7788
- let nodeName = el.nodeName;
7789
- if ((removeNameSpaced && nodeName.includes(':')) ||
7790
- remove.includes(nodeName)
7791
- ) {
7792
- el.remove();
7793
- }
7794
- });
7795
- }
8842
+ function hrefToXlink(svg) {
8843
+ svg.setAttribute('xmlns:xlink', "http://www.w3.org/1999/xlink");
8844
+ let hrefs = svg.querySelectorAll('[href]');
8845
+ hrefs.forEach(el => {
8846
+ let href = el.getAttribute('href');
8847
+ el.setAttribute('xlink:href', href);
7796
8848
 
7797
- function removeExcludedAttribues(el, allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class']) {
7798
- let atts = [...el.attributes].map((att) => att.name);
7799
- atts.forEach((att) => {
7800
- if (!allowed.includes(att)) {
7801
- el.removeAttribute(att);
7802
- }
7803
8849
  });
7804
8850
  }
7805
8851
 
7806
- function stringifySVG(svg, {
7807
- omitNamespace = false,
7808
- removeComments = true,
7809
- } = {}) {
7810
- let markup = new XMLSerializer().serializeToString(svg);
7811
-
7812
- if (omitNamespace) {
7813
- markup = markup.replaceAll('xmlns="http://www.w3.org/2000/svg"', '');
7814
- }
7815
-
7816
- if (removeComments) {
7817
- markup = markup
7818
- .replace(/(<!--.*?-->)|(<!--[\S\s]+?-->)|(<!--[\S\s]*?$)/g, '');
7819
- }
7820
-
7821
- markup = markup
7822
- .replace(/\t/g, "")
7823
- .replace(/[\n\r|]/g, "\n")
7824
- .replace(/\n\s*\n/g, '\n')
7825
- .replace(/ +/g, ' ')
7826
-
7827
- .replace(/> </g, '><')
7828
- .trim()
7829
- // sanitize linebreaks within pathdata
7830
- .replaceAll('&#10;', '\n');
7831
-
7832
- return markup
7833
- }
7834
-
7835
8852
  function refineRoundedCorners(pathData, {
7836
8853
  threshold = 0,
7837
8854
  tolerance = 1
@@ -8401,61 +9418,69 @@ function pathDataRevertCubicToQuadratic(pathData, tolerance=1) {
8401
9418
  return pathData
8402
9419
  }
8403
9420
 
8404
- function harmonizeCubicCptsThird(pathData = [], t = 0.666) {
9421
+ function fixIntersectingCpts(pathData = []) {
8405
9422
 
8406
9423
  let l = pathData.length;
8407
- for (let i = 0; i < l; i++) {
9424
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
9425
+ let p = p0;
9426
+
9427
+ for (let i = 1; i < l; i++) {
8408
9428
  let com = pathData[i];
8409
9429
  let comPrev = pathData[i - 1] || null;
8410
9430
  let { type, values } = com;
8411
9431
  pathData[i + 1] ? pathData[i + 1] : null;
8412
- let adjust = false;
8413
9432
 
8414
9433
  if (type === 'C') {
9434
+ let tx = 1.2;
8415
9435
  let cp1 = { x: values[0], y: values[1] };
9436
+ p = { x: values[4], y: values[5] };
8416
9437
  let cp2 = { x: values[2], y: values[3] };
8417
- let valuesL = comPrev.values.slice(-2);
8418
- let p0 = { x: valuesL[0], y: valuesL[1] };
8419
- let p = { x: values[4], y: values[5] };
8420
9438
 
8421
- let dist0 = getDistManhattan(p0, p);
8422
- getDistManhattan(p0, cp1);
8423
- getDistManhattan(p, cp2);
8424
- let dist3 = getDistManhattan(cp1, cp2);
9439
+ interpolate(p0, cp1, tx );
9440
+ let cp2_ex = interpolate( p, cp2, tx);
8425
9441
 
8426
- let ptIV = checkLineIntersection(p0, cp1, p, cp2, false);
9442
+ comPrev.values.slice(-2);
8427
9443
 
8428
- if (ptIV) {
9444
+ // extend tangents
8429
9445
 
8430
- // exact intersection
8431
- let ptI = ptIV ? checkLineIntersection(p0, cp1, p, cp2, true) : null;
9446
+ let ptI = checkLineIntersection(p0, cp1, p, cp2_ex, true, true);
8432
9447
 
8433
- // cpts are intersection
8434
- if (ptI) {
8435
- adjust = true;
8436
- }
9448
+
9449
+ /*
9450
+ renderPoly(markers, [p0, cp1], 'orange', '0.75')
9451
+ renderPoly(markers, [p, cp2], 'blue', '0.75')
9452
+ renderPoly(markers, [p, cp2_ex], 'magenta', '0.75')
8437
9453
 
8438
- else if (dist3 < dist0 / 5) {
8439
- adjust = true;
8440
- }
8441
- }
9454
+ renderPoint(markers, p0, 'orange', '0.75')
9455
+ renderPoint(markers, cp1, 'magenta', '0.75')
9456
+ renderPoint(markers, cp1_ex, 'blue', '0.5')
9457
+
9458
+ renderPoint(markers, cp2, 'cyan', '0.75')
9459
+ */
8442
9460
 
8443
- if (adjust) {
8444
- cp1 = pointAtT([p0, ptIV], t);
8445
- cp2 = pointAtT([p, ptIV], t);
9461
+
9462
+
9463
+ /*
9464
+ renderPoint(markers, p0, 'orange')
9465
+ renderPoint(markers, p, 'magenta', '0.5')
9466
+ */
9467
+
9468
+ if (ptI) {
9469
+ let t = 0.666;
9470
+ cp1 = interpolate(p0, ptI, t);
9471
+ cp2 = interpolate(p, ptI, t);
9472
+ com.values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
8446
9473
 
8447
- pathData[i].values[0] = cp1.x;
8448
- pathData[i].values[1] = cp1.y;
8449
- pathData[i].values[2] = cp2.x;
8450
- pathData[i].values[3] = cp2.y;
8451
9474
  }
8452
9475
 
8453
9476
  }
8454
9477
 
9478
+ if (values.length) {
9479
+ p0 = p;
9480
+ }
8455
9481
  }
8456
9482
 
8457
- return pathData
8458
-
9483
+ return pathData;
8459
9484
  }
8460
9485
 
8461
9486
  function simplifyPolyRDP(pts, {quality = 0.9, width = 0, height = 0}={}) {
@@ -8471,7 +9496,7 @@ function simplifyPolyRDP(pts, {quality = 0.9, width = 0, height = 0}={}) {
8471
9496
  quality = parseFloat(quality);
8472
9497
  }
8473
9498
 
8474
- if (pts.length < 4 || (!isAbsolute && quality) >= 1) return pts;
9499
+ if (pts.length < 4 ) return pts;
8475
9500
 
8476
9501
  // convert quality to squaredistance tolerance
8477
9502
  let tolerance = quality;
@@ -8572,7 +9597,8 @@ function simplifyPolyRD(pts, {quality = 0.9, width = 0, height = 0}={}) {
8572
9597
  }
8573
9598
 
8574
9599
  // nothing to do - exit
8575
- if (pts.length < 4 || (!isAbsolute && quality) >= 1) return pts;
9600
+
9601
+ if (pts.length < 4 ) return pts;
8576
9602
 
8577
9603
  let p0 = pts[0];
8578
9604
  let pt;
@@ -8584,7 +9610,7 @@ function simplifyPolyRD(pts, {quality = 0.9, width = 0, height = 0}={}) {
8584
9610
  if (!isAbsolute) {
8585
9611
 
8586
9612
  // quality to tolerance
8587
- tolerance = 1 - quality;
9613
+ tolerance = quality;
8588
9614
 
8589
9615
  /**
8590
9616
  * approximate dimensions
@@ -8631,14 +9657,15 @@ function pathDataFromPoly(pts, closed = true) {
8631
9657
 
8632
9658
  // complex polygon
8633
9659
  if (Array.isArray(pts[0])) {
8634
- pts.forEach(sub => {
9660
+ pts.forEach(sub => {
8635
9661
  subPath = [
8636
9662
  { type: 'M', values: [sub[0].x, sub[0].y] },
8637
9663
  ...sub.slice(1).map(pt => { return { type: 'L', values: [pt.x, pt.y] } })
8638
9664
  ];
9665
+ if (closed) subPath.push({ type: 'Z', values: [] });
8639
9666
  pathData.push(...subPath);
8640
9667
  });
8641
- }else {
9668
+ } else {
8642
9669
  pathData = [
8643
9670
  { type: 'M', values: [pts[0].x, pts[0].y] },
8644
9671
  ...pts.slice(1).map(pt => { return { type: 'L', values: [pt.x, pt.y] } })
@@ -8650,6 +9677,40 @@ function pathDataFromPoly(pts, closed = true) {
8650
9677
 
8651
9678
  }
8652
9679
 
9680
+ function getPolyCentroid(pts) {
9681
+
9682
+ let l = pts.length;
9683
+ let x = 0, y = 0;
9684
+ for (let i = 0; l && i < l; i++) {
9685
+ let pt = pts[i];
9686
+ x += pt.x;
9687
+ y += pt.y;
9688
+ }
9689
+
9690
+ let centroid = {x: x/l, y:y/l};
9691
+ return centroid
9692
+
9693
+ }
9694
+
9695
+ function detectRegularPolygon(pts, centroid={x:0, y:0}) {
9696
+ let rSq = getSquareDistance(pts[0], centroid);
9697
+ let isRegular = true;
9698
+
9699
+ for (let i = 1, l = pts.length; i < l; i++) {
9700
+ let pt1 = pts[i];
9701
+ let dist = getSquareDistance(pt1, centroid);
9702
+
9703
+ let diff = Math.abs(rSq-dist);
9704
+ let diffRel = diff/rSq;
9705
+
9706
+ if (diffRel > 0.05) {
9707
+ return false;
9708
+ }
9709
+
9710
+ }
9711
+ return isRegular;
9712
+ }
9713
+
8653
9714
  function analyzePoly(pts, {
8654
9715
  x = 0,
8655
9716
  y = 0,
@@ -8916,15 +9977,13 @@ function fitCurveSchneider(pts, {
8916
9977
 
8917
9978
  // create pathdata
8918
9979
  let pathData = bezierPtsToPathData(beziers);
8919
-
8920
9980
  let cp1, cp2;
8921
9981
 
8922
9982
  adjustCpts = false;
8923
- harmonize = false;
8924
9983
 
8925
- if (adjustCpts) {
9984
+ adjustCpts = true;
8926
9985
 
8927
- console.log('refine cpts');
9986
+ if (adjustCpts) {
8928
9987
 
8929
9988
  let len2 = pathData.length;
8930
9989
  let com1 = pathData[0];
@@ -8954,13 +10013,16 @@ function fitCurveSchneider(pts, {
8954
10013
  com2.values[3] = cp2.y;
8955
10014
  }
8956
10015
 
10016
+ /*
8957
10017
  // harmonize too tight tangents
8958
-
10018
+ let harmonize = true;
10019
+ harmonize = false;
8959
10020
  if (harmonize) {
8960
10021
  pathData = harmonizeCubicCptsThird([{ type: 'M', values: [pts[0].x, pts[0].y] },
8961
- ...pathData]);
8962
- pathData.shift();
10022
+ ...pathData])
10023
+ pathData.shift()
8963
10024
  }
10025
+ */
8964
10026
 
8965
10027
  }
8966
10028
 
@@ -9439,29 +10501,127 @@ function adjustTangentAngle(cp, p0, p1, p2) {
9439
10501
  return cp
9440
10502
  }
9441
10503
 
9442
- // convert to pathdata
9443
- function bezierPtsToPathData(beziers = []) {
9444
- let pathData = [];
9445
- beziers.forEach(bez => {
10504
+ // convert to pathdata
10505
+ function bezierPtsToPathData(beziers = []) {
10506
+ let pathData = [];
10507
+ beziers.forEach(bez => {
10508
+
10509
+ let type = bez.length === 4 ? 'C' : (bez.length === 3 ? 'Q' : 'L');
10510
+
10511
+ let cp1 = type === 'C' || type === 'Q' ? bez[1] : null;
10512
+ let cp2 = type === 'C' ? bez[2] : null;
10513
+ let p = bez[bez.length - 1];
10514
+
10515
+ let values = type === 'C' ?
10516
+ [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] :
10517
+ (type === 'Q' ?
10518
+ [cp1.x, cp1.y, p.x, p.y] :
10519
+ [p.x, p.y]
10520
+ );
10521
+
10522
+ let com = { type, values };
10523
+ pathData.push(com);
10524
+ });
10525
+
10526
+ return pathData
10527
+ }
10528
+
10529
+ function simplifyRC(pts, quality = 1, shiftStart = true) {
10530
+
10531
+ if (pts.length < 4) return pts;
10532
+
10533
+ let l = pts.length;
10534
+
10535
+ // starting point
10536
+ let M = pts[0];
10537
+
10538
+ // last point
10539
+ let Z = pts[l - 1];
10540
+
10541
+ // remove unnecessary closing point
10542
+ if (M.x === Z.x && M.y === Z.y) {
10543
+ pts.pop();
10544
+ l--;
10545
+ Z = pts[l - 1];
10546
+ }
10547
+
10548
+ // init new point array
10549
+ let ptsSmp = [M];
10550
+ let pt0 = M;
10551
+ let pt1, pt2;
10552
+
10553
+ // loop through vertices by triangles
10554
+ for (let i = 2; i < l; i++) {
10555
+ pt1 = pts[i - 1];
10556
+ pt2 = pts[i];
10557
+ let isLast = i === l - 1;
10558
+
10559
+ /**
10560
+ * 1. Skip zero-length segments
10561
+ */
10562
+ if ((pt1.x === pt0.x && pt1.y === pt0.y) || (pt1.x === pt2.x && pt1.y === pt2.y)) {
10563
+ continue;
10564
+ }
10565
+
10566
+ /**
10567
+ * 2. Check for perfectly flat
10568
+ * vertical/horizontal segments
10569
+ */
10570
+ let isVertical = (pt0.x === pt1.x);
10571
+ let isHorizontal = (pt0.y === pt1.y);
10572
+
10573
+ if (isVertical || isHorizontal) {
10574
+
10575
+ let isVertical2 = (pt1.x === pt2.x);
10576
+ let isHorizontal2 = (pt1.y === pt2.y);
10577
+
10578
+ if (((isVertical && isVertical2) || (isHorizontal && isHorizontal2))) {
10579
+
10580
+ // perfectly flat segment - skip
10581
+ if (!isLast) continue;
10582
+
10583
+ // flat but last – add last and skip colinearity check
10584
+ if (isLast && M.x !== pt2.x && M.y !== pt2.y) {
10585
+
10586
+ ptsSmp.push(pt2);
10587
+ continue
10588
+ }
10589
+
10590
+ }
10591
+ }
9446
10592
 
9447
- let type = bez.length === 4 ? 'C' : (bez.length === 3 ? 'Q' : 'L');
10593
+ // check area
10594
+ let area = getPolygonArea([pt0, pt1, pt2], true);
10595
+ let thresh = getSquareDistance(pt0, pt2) * 0.005;
9448
10596
 
9449
- let cp1 = type === 'C' || type === 'Q' ? bez[1] : null;
9450
- let cp2 = type === 'C' ? bez[2] : null;
9451
- let p = bez[bez.length - 1];
10597
+ // flat
10598
+ if ( area <= thresh && i<l-1) {
9452
10599
 
9453
- let values = type === 'C' ?
9454
- [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] :
9455
- (type === 'Q' ?
9456
- [cp1.x, cp1.y, p.x, p.y] :
9457
- [p.x, p.y]
9458
- );
10600
+ pt0 = pt1;
10601
+ continue
10602
+ }
9459
10603
 
9460
- let com = { type, values };
9461
- pathData.push(com);
9462
- });
10604
+ // no simplification - add mid pt
10605
+ ptsSmp.push(pt1);
9463
10606
 
9464
- return pathData
10607
+ // add last point if not first
10608
+ if (isLast && M.x !== pt2.x && M.y !== pt2.y) {
10609
+ // console.log('add last', M, pt2);
10610
+ ptsSmp.push(pt2);
10611
+ }
10612
+
10613
+ // update previous point
10614
+ pt0 = pt1;
10615
+
10616
+ }
10617
+
10618
+ // 1st and last are colinear
10619
+ let area0 = getPolygonArea([ptsSmp[1], M, ptsSmp[ptsSmp.length-1]], true);
10620
+ let thresh0 = getSquareDistance (ptsSmp[1], ptsSmp[ptsSmp.length-1]) * 0.005;
10621
+ // remove first point
10622
+ if(area0 < thresh0) ptsSmp.shift();
10623
+
10624
+ return ptsSmp;
9465
10625
  }
9466
10626
 
9467
10627
  function simplifyPolygonToPathData(pts, {
@@ -9480,29 +10640,80 @@ function simplifyPolygonToPathData(pts, {
9480
10640
  simplifyRDP = 1,
9481
10641
  } = {}) {
9482
10642
 
9483
- /*
9484
- // denoise via RDP
9485
- if (denoise && denoise !== 1) {
9486
- pts = simplifyPolyRDP(pts, {
9487
- width,
9488
- height,
9489
- quality: denoise,
9490
- manhattan,
9491
- absolute
9492
- })
9493
- }
9494
- */
10643
+ let polyPath = [];
10644
+ let l = pts.length;
10645
+ let M = pts[0];
10646
+ let Z = pts[l - 1];
9495
10647
 
9496
- /*
9497
- // simplify polygon
9498
- if (simplifyRD != 1) {
9499
- pts = simplifyPolyRD(pts, { quality: simplifyRD+'px' })
10648
+ // triangle
10649
+ if (pts.length === 3) {
10650
+
10651
+ let pM1 = interpolate(M, pts[1], 0.5);
10652
+ let pM2 = interpolate(pts[1], Z, 0.5);
10653
+ let pM3 = interpolate(Z, pts[0], 0.5);
10654
+
10655
+ /*
10656
+ console.log('triangle');
10657
+ renderPoint(markers, M)
10658
+ renderPoint(markers, pM1)
10659
+ renderPoint(markers, pM2)
10660
+ renderPoint(markers, pM3)
10661
+ */
10662
+
10663
+ if (closed) {
10664
+ let t = 0.6666;
10665
+ let cp1_1 = interpolate(pM1, pts[1], t);
10666
+ let cp2_1 = interpolate(pM2, pts[1], t);
10667
+ let cp1_2 = interpolate(pM2, Z, t);
10668
+ let cp2_2 = interpolate(pM3, Z, t);
10669
+ let cp1_3 = interpolate(pM3, M, t);
10670
+ let cp2_3 = interpolate(pM1, M, t);
10671
+
10672
+ polyPath = [
10673
+ { type: 'M', values: [pM1.x, pM1.y] },
10674
+ { type: 'C', values: [cp1_1.x, cp1_1.y, cp2_1.x, cp2_1.y, pM2.x, pM2.y] },
10675
+ { type: 'C', values: [cp1_2.x, cp1_2.y, cp2_2.x, cp2_2.y, pM3.x, pM3.y] },
10676
+ { type: 'C', values: [cp1_3.x, cp1_3.y, cp2_3.x, cp2_3.y, pM1.x, pM1.y] },
10677
+ { type: 'Z', values: [] },
10678
+ ];
10679
+
10680
+ } else {
10681
+ polyPath = [
10682
+
10683
+ { type: 'M', values: [M.x, M.y] },
10684
+ { type: 'C', values: [pts[1].x, pts[1].y, pts[1].x, pts[1].y, Z.x, Z.y] },
10685
+ ];
10686
+ }
10687
+ return polyPath;
9500
10688
  }
9501
10689
 
9502
- if (simplifyRDP != 1) {
9503
- pts = simplifyPolyRDP(pts, { quality: simplifyRDP+'px' })
10690
+ // remove colinear
10691
+
10692
+ /**
10693
+ * detect regular polygon
10694
+ * curved path is a circle
10695
+ */
10696
+ let centroid = getPolyCentroid(simplifyRC(pts));
10697
+ let isRegularPolygon = detectRegularPolygon(pts, centroid);
10698
+
10699
+ if (isRegularPolygon) {
10700
+
10701
+ let ptAd = rotatePoint(pts[0], centroid.x, centroid.y, Math.PI);
10702
+ let sweep = getPolygonArea(pts) > 0 ? 1 : 0;
10703
+
10704
+ polyPath = [
10705
+ { type: 'M', values: [pts[0].x, pts[0].y] },
10706
+ { type: 'A', values: [1, 1, 0, 0, sweep, ptAd.x, ptAd.y] },
10707
+ { type: 'A', values: [1, 1, 0, 0, sweep, pts[0].x, pts[0].y] }
10708
+ ];
10709
+
10710
+ if (closed) {
10711
+ polyPath.push({ type: 'Z', values: [] });
10712
+ }
10713
+ return polyPath;
9504
10714
  }
9505
- */
10715
+
10716
+ // remove colinear
9506
10717
 
9507
10718
  // get topology of poly
9508
10719
  let polyAnalyzed = !keepExtremes && !keepCorners ? pts : analyzePoly(pts, {
@@ -9516,12 +10727,15 @@ function simplifyPolygonToPathData(pts, {
9516
10727
  // Schneider curve fit
9517
10728
  let threshold = width && height ? (width + height) / 2 * 0.004 * tolerance : 2.5;
9518
10729
 
9519
- let polyPath = simplifyPolyChunks(chunks, {
10730
+ polyPath = simplifyPolyChunks(chunks, {
9520
10731
  closed,
9521
10732
  tolerance: threshold,
9522
- keepCorners
10733
+ keepCorners,
10734
+ keepExtremes: true,
9523
10735
  });
9524
10736
 
10737
+ polyPath = fixIntersectingCpts(polyPath);
10738
+
9525
10739
  return polyPath;
9526
10740
  }
9527
10741
 
@@ -9579,7 +10793,7 @@ function simplifyPolyChunks(chunks = [], {
9579
10793
  * creates precise polygon approximation from pathdata
9580
10794
  * converts arc to cubis
9581
10795
  */
9582
- function pathDataToPolygon(pathData, {
10796
+ function pathDataToPolygonOpt(pathData, {
9583
10797
  precisionPoly = 1,
9584
10798
  autoAccuracy=false,
9585
10799
  polyFormat='points',
@@ -9654,11 +10868,11 @@ simplifyRDP=1,
9654
10868
 
9655
10869
  // simplify polygon
9656
10870
  if(simplifyRD>0){
9657
- pts2 = simplifyPolyRD(pts2, {quality:simplifyRD+'px'});
10871
+ pts2 = simplifyPolyRD(pts2, {quality:simplifyRD});
9658
10872
  }
9659
10873
 
9660
10874
  if(simplifyRDP>0){
9661
- pts2 = simplifyPolyRDP(pts2, {quality:simplifyRDP+'px'});
10875
+ pts2 = simplifyPolyRDP(pts2, {quality:simplifyRDP});
9662
10876
  }
9663
10877
 
9664
10878
  pathDataPoly = pathDataFromPoly(pts2);
@@ -9666,7 +10880,6 @@ simplifyRDP=1,
9666
10880
 
9667
10881
  if(autoAccuracy){
9668
10882
  decimals = detectAccuracyPoly(pts);
9669
-
9670
10883
  }
9671
10884
 
9672
10885
  let poly = decimals>-1 ? pts2.map(pt => { return { x: roundTo(pt.x, decimals), y: roundTo(pt.y, decimals) } }) : pts2.map(pt => { return { x: pt.x, y: pt.y } });
@@ -9812,9 +11025,11 @@ function fixPathDataDirections(pathDataArr = [], toClockwise = false) {
9812
11025
  if (!includedIn.length && cw && !toClockwise
9813
11026
  || !includedIn.length && !cw && toClockwise
9814
11027
  ) {
11028
+
9815
11029
  pathDataArr[i].pathData = reversePathData(pathDataArr[i].pathData);
9816
11030
  polys[i].cw = polys[i].cw ? false : true;
9817
11031
  cw = polys[i].cw;
11032
+
9818
11033
  }
9819
11034
 
9820
11035
  // reverse inner sub paths
@@ -9823,6 +11038,7 @@ function fixPathDataDirections(pathDataArr = [], toClockwise = false) {
9823
11038
  let child = polys[ind];
9824
11039
 
9825
11040
  if (child.cw === cw) {
11041
+
9826
11042
  pathDataArr[ind].pathData = reversePathData(pathDataArr[ind].pathData);
9827
11043
  polys[ind].cw = polys[ind].cw ? false : true;
9828
11044
  }
@@ -9833,116 +11049,404 @@ function fixPathDataDirections(pathDataArr = [], toClockwise = false) {
9833
11049
 
9834
11050
  }
9835
11051
 
9836
- function svgPathSimplify(input = '', {
11052
+ let settingsDefaults = {
11053
+
11054
+ // SVG elements
11055
+ removeComments: true,
11056
+ removeOffCanvas: false,
11057
+
11058
+ // attributes
11059
+ removeDimensions: false,
11060
+ removeIds: false,
11061
+ removeClassNames: false,
11062
+ omitNamespace: false,
11063
+ cleanUpStrokes: true,
11064
+ addViewBox: true,
11065
+ addDimensions: false,
11066
+ removePrologue: true,
11067
+ removeHidden: true,
11068
+ removeUnused: true,
11069
+ cleanupDefs: true,
11070
+ cleanupClip: true,
11071
+ cleanupSVGAtts: true,
11072
+ removeNameSpaced: true,
11073
+ removeNameSpacedAtts: true,
11074
+ attributesToGroup: false,
11075
+ minifyRgbColors: true,
11076
+ stylesToAttributes: false,
11077
+ fixHref: false,
11078
+ legacyHref: false,
11079
+ allowMeta: false,
11080
+ allowDataAtts: true,
11081
+ allowAriaAtts: true,
11082
+
11083
+ convertPathLength: false,
11084
+
11085
+ // custom removal
11086
+ removeElements: [],
11087
+ removeSVGAttributes: [],
11088
+ removeElAttributes: [],
11089
+
11090
+ // merging/splitting
11091
+ unGroup: false,
11092
+ mergePaths: false,
11093
+ splitCompound: false,
9837
11094
 
9838
- // return svg markup or object
9839
- getObject = false,
11095
+ // shape conversions
11096
+ shapesToPaths: false,
11097
+ shapeConvert: 0,
11098
+ convertShapes: ['rect', 'ellipse', 'circle', 'line', 'polygon', 'polyline'],
11099
+
11100
+ // simplify
11101
+ keepSmaller: true,
11102
+ simplifyBezier: true,
11103
+ optimizeOrder: true,
11104
+ autoClose: false,
11105
+ removeZeroLength: true,
11106
+ refineClosing: true,
11107
+ removeColinear: true,
11108
+ flatBezierToLinetos: true,
11109
+ revertToQuadratics: true,
11110
+ refineExtremes: false,
11111
+ simplifyCorners: false,
11112
+ keepExtremes: true,
11113
+ keepCorners: true,
11114
+ keepInflections: false,
11115
+ addExtremes: false,
11116
+
11117
+ // draw direction
11118
+ fixDirections: false,
11119
+ reversePath: false,
11120
+
11121
+ // pathdata
11122
+ toAbsolute: false,
11123
+ toRelative: true,
11124
+ toMixed: false,
11125
+ toShorthands: true,
11126
+ toLonghands: false,
11127
+ quadraticToCubic: true,
11128
+ arcToCubic: false,
11129
+ cubicToArc: false,
11130
+ lineToCubic: false,
11131
+
11132
+ // minification
11133
+ decimals: 3,
11134
+ autoAccuracy: true,
11135
+ minifyD: 0,
11136
+ tolerance: 1,
11137
+
11138
+ // polygon
11139
+ toPolygon: false,
11140
+ smoothPoly: false,
11141
+ polyFormat: 'object',
11142
+ precisionPoly: 1,
11143
+ simplifyRD: 0,
11144
+ simplifyRDP: 0,
11145
+ harmonizeCpts: false,
11146
+ removeOrphanSubpaths: false,
11147
+ simplifyRound: false,
11148
+
11149
+ scale: 1,
11150
+ scaleTo: 0,
11151
+ crop: false,
11152
+ alignToOrigin: false,
9840
11153
 
9841
- toAbsolute = false,
11154
+ // flatten transforms
11155
+ convertTransforms: false,
11156
+
11157
+ };
11158
+
11159
+ const settingsNull = {};
11160
+
11161
+ for (let prop in settingsDefaults) {
11162
+ let val = settingsDefaults[prop];
11163
+ let isBoolean = val === false || val === true;
11164
+ let isNum = !isNaN(val);
11165
+ let isArray = Array.isArray(val);
11166
+
11167
+ if (isBoolean) val = false;
11168
+ else if (!isArray && isNum) val = val===1 ? 1 : (prop==='decimals'? -1 : 0);
11169
+ else if (isArray) val = [];
11170
+ settingsNull[prop] = val;
11171
+ }
11172
+
11173
+ const presetSettings = {
11174
+ default: settingsDefaults,
11175
+
11176
+ education: {
11177
+ ...settingsDefaults,
11178
+ ...{
11179
+ keepSmaller: false,
11180
+ toRelative: false,
11181
+ toMixed: false,
11182
+ toShorthands: false,
11183
+ fixHref: true,
11184
+ legacyHref: false,
11185
+ addViewBox: true,
11186
+ addDimensions: true,
11187
+ removeComments: false,
11188
+ decimals: 3,
11189
+ minifyD: 2
11190
+ }
11191
+ },
11192
+
11193
+ null: settingsNull,
11194
+
11195
+ editor: {
11196
+ ...settingsDefaults,
11197
+ ...{
11198
+ keepSmaller: false,
11199
+ toRelative: true,
11200
+ toMixed: true,
11201
+ toShorthands: true,
11202
+
11203
+ legacyHref: true,
11204
+ addViewBox: true,
11205
+ addDimensions: true,
11206
+ removeComments: true,
11207
+ autoAccuracy: true,
11208
+
11209
+ minifyD: 0.5
11210
+ }
11211
+ },
11212
+
11213
+ noSimplification: {
11214
+ ...settingsDefaults,
11215
+ ...{
11216
+ simplifyBezier: false,
11217
+ quadraticToCubic: false,
11218
+ toRelative: true,
11219
+ toShorthands: true,
11220
+ fixHref: true,
11221
+ optimizeOrder: false,
11222
+ removeZeroLength: false,
11223
+ refineExtremes: false,
11224
+ refineClosing: false,
11225
+ removeColinear: false,
11226
+ flatBezierToLinetos: false,
11227
+
11228
+ addDimensions: false,
11229
+ removeComments: true,
11230
+ minifyD: 0
11231
+ }
11232
+
11233
+ },
11234
+ path: {
11235
+ ...settingsDefaults,
11236
+ ...{
11237
+ shapeConvert: 'toPaths',
11238
+ convertShapes: ['rect', 'ellipse', 'circle', 'line', 'polygon', 'polyline'],
11239
+ addViewBox: true,
11240
+ minifyD: 0.5
11241
+ }
11242
+ },
11243
+
11244
+ poly: {
11245
+ ...settingsDefaults,
11246
+ ...{
11247
+ toPolygon: true,
11248
+ }
11249
+ },
11250
+
11251
+ curvefit: {
11252
+ ...settingsDefaults,
11253
+ ...{
11254
+ smoothPoly: true,
11255
+ }
11256
+ },
11257
+
11258
+ detransform: {
11259
+ ...settingsDefaults,
11260
+ ...{
11261
+ convertTransforms: true,
11262
+ addViewBox: true,
11263
+ minifyD: 0.5
11264
+ }
11265
+ },
11266
+
11267
+ high: {
11268
+ ...settingsDefaults,
11269
+ ...{
11270
+ tolerance: 1.2,
11271
+ toMixed: true,
11272
+ refineExtremes: true,
11273
+ simplifyCorners: true,
11274
+ simplifyRound: true,
11275
+ removeClassNames: true,
11276
+ cubicToArc: true,
11277
+ removeComments: true,
11278
+ removeHidden: true,
11279
+ removeOffCanvas: true,
11280
+ addViewBox: true,
11281
+ removeDimensions: true,
11282
+ minifyD: 0
11283
+ }
11284
+ }
11285
+
11286
+ };
11287
+
11288
+ function splitCompundGroups(pathDataPlusArr = [], {
9842
11289
  toRelative = true,
9843
11290
  toShorthands = true,
9844
- toLonghands = false,
11291
+ minifyD = 0,
11292
+ decimals = 3,
11293
+ addDimensions = false
11294
+ } = {}) {
11295
+ pathDataPlusArr = JSON.parse(JSON.stringify(pathDataPlusArr));
11296
+ let len = pathDataPlusArr.length;
9845
11297
 
9846
- // not necessary unless you need cubics only
9847
- quadraticToCubic = true,
11298
+ let xArr = [];
11299
+ let yArr = [];
9848
11300
 
9849
- // mostly a fallback if arc calculations fail
9850
- arcToCubic = false,
9851
- cubicToArc = false,
9852
-
9853
- simplifyBezier = true,
9854
- optimizeOrder = true,
9855
- autoClose = false,
9856
- removeZeroLength = true,
9857
- refineClosing = true,
9858
- removeColinear = true,
9859
- flatBezierToLinetos = true,
9860
- revertToQuadratics = true,
9861
-
9862
- refineExtremes = true,
9863
- simplifyCorners = false,
9864
- removeDimensions = false,
9865
- removeIds = false,
9866
- removeClassNames = false,
9867
- omitNamespace = false,
11301
+ // refine bbox and add cpt polygon
11302
+ for (let i = 0; i < len; i++) {
11303
+ let sub = pathDataPlusArr[i];
11304
+ let { pathData, bb } = sub;
9868
11305
 
9869
- fixDirections = false,
11306
+ // console.log(bb);
11307
+ // include control points for better overlapping approximation
9870
11308
 
9871
- keepExtremes = true,
9872
- keepCorners = true,
9873
- extrapolateDominant = true,
9874
- keepInflections = false,
9875
- addExtremes = false,
9876
- addSemiExtremes = false,
11309
+ if (bb.width && bb.height) ; else {
11310
+ let poly = getPathDataVertices(pathData, true);
11311
+ bb = getPolyBBox(poly);
11312
+ pathDataPlusArr[i].bb = bb;
9877
11313
 
9878
- toPolygon = false,
9879
- smoothPoly = false,
9880
- polyFormat = 'points',
9881
- precisionPoly = 1,
11314
+ }
9882
11315
 
9883
- simplifyRD = 1,
9884
- simplifyRDP = 1,
11316
+ xArr.push(bb.left, bb.right);
11317
+ yArr.push(bb.top, bb.bottom);
11318
+ sub.includes = [];
11319
+ }
11320
+
11321
+ /**
11322
+ * check overlapping
11323
+ * sub paths
11324
+ */
11325
+ for (let i = 0, l = pathDataPlusArr.length; i < l; i++) {
11326
+ let sub1 = pathDataPlusArr[i];
11327
+ let { bb, poly } = sub1;
9885
11328
 
9886
- harmonizeCpts = false,
11329
+ for (let j = 0; j < l; j++) {
9887
11330
 
9888
- removeOrphanSubpaths = false,
9889
- simplifyRound = false,
11331
+ let sub1 = pathDataPlusArr[j];
11332
+ if (i === j) continue;
9890
11333
 
9891
- scale = 1,
9892
- scaleTo = 0,
9893
- crop = false,
9894
- alignToOrigin = false,
11334
+ let bb1 = sub1.bb;
9895
11335
 
9896
- // flatten transforms
9897
- convertTransforms = false,
11336
+ // test sample on-path points
11337
+ let ptM = { x: bb1.x + bb1.width * 0.5, y: bb1.y + bb1.height * 0.5 };
11338
+ if (ptM.x >= bb.x && ptM.y >= bb.y && ptM.x <= bb.right && ptM.y <= bb.bottom) {
11339
+ pathDataPlusArr[i].includes.push(j);
11340
+ }
9898
11341
 
9899
- decimals = 3,
9900
- autoAccuracy = true,
11342
+ }
11343
+ }
9901
11344
 
9902
- // experimental
11345
+ /**
11346
+ * combine overlapping
11347
+ * compound paths
11348
+ */
11349
+ for (let i = 0, l = pathDataPlusArr.length; i < l; i++) {
11350
+ let sub = pathDataPlusArr[i];
11351
+ let { includes } = sub;
9903
11352
 
9904
- minifyD = 0,
9905
- tolerance = 1,
9906
- reversePath = false,
11353
+ includes.forEach(s => {
11354
+ let pathData = pathDataPlusArr[s].pathData;
11355
+ if (pathData.length) {
11356
+ pathDataPlusArr[i].pathData.push(...pathData);
11357
+ pathDataPlusArr[s].pathData = [];
11358
+ }
11359
+ });
11360
+ }
9907
11361
 
9908
- minifyRgbColors = false,
9909
- removePrologue = true,
9910
- removeHidden = true,
9911
- removeUnused = true,
9912
- cleanupDefs = true,
9913
- cleanupClip = true,
9914
- cleanupSVGAtts = true,
9915
-
9916
- stylesToAttributes = false,
9917
- fixHref = false,
9918
- legacyHref = false,
9919
- removeNameSpaced = true,
11362
+ // remove empty els due to grouping
11363
+ pathDataPlusArr = pathDataPlusArr.filter(sub => sub.pathData.length);
9920
11364
 
9921
- removeOffCanvas = false,
9922
- unGroup = false,
9923
- mergePaths = false,
11365
+ // try to find row left to right order
9924
11366
 
9925
- // shape conversions
9926
- shapesToPaths = false,
11367
+ pathDataPlusArr = pathDataPlusArr.sort((a, b) => ((a.bb.x ) - (b.bb.x)));
9927
11368
 
9928
- shapeConvert = 0,
9929
- convert_rects = false,
9930
- convert_ellipses = false,
9931
- convert_poly = false,
9932
- convert_lines = false,
11369
+ // create SVG
11370
+ let x = Math.min(...xArr);
11371
+ let y = Math.min(...yArr);
11372
+ let right = Math.max(...xArr);
11373
+ let bottom = Math.max(...yArr);
11374
+ let width = right - x;
11375
+ let height = bottom - y;
9933
11376
 
9934
- lineToCubic = false,
9935
- cleanUpStrokes = true,
9936
- addViewBox = false,
9937
- addDimensions = false,
11377
+ [x, y, width, height] = [x, y, width, height].map(val => roundTo(val, decimals));
9938
11378
 
9939
- removeComments = true,
11379
+ let dimensionAtts = addDimensions ? `width="${width}" height="${height}"` : '';
11380
+ let svgSplit = `<svg ${dimensionAtts} viewBox="${x} ${y} ${width} ${height}" xmlns="${svgNs}">`;
9940
11381
 
9941
- } = {}) {
11382
+ pathDataPlusArr.forEach(sub => {
11383
+ let { pathData } = sub;
11384
+
11385
+ pathData = convertPathData(pathData, { toRelative, toShorthands, decimals });
11386
+ let d = pathDataToD(pathData, minifyD);
11387
+ svgSplit += `<path d="${d}"/>`;
11388
+
11389
+ });
11390
+
11391
+ svgSplit += '</svg>';
11392
+
11393
+ let splitObj = { pathData: pathDataPlusArr, svg: svgSplit };
11394
+
11395
+ return splitObj
11396
+
11397
+ }
11398
+
11399
+ /*
11400
+ function checkBBoxIntersections2(bb, bb1) {
11401
+ let [x, y, width, height, right, bottom] = [
11402
+ bb.x,
11403
+ bb.y,
11404
+ bb.width,
11405
+ bb.height,
11406
+ bb.x + bb.width,
11407
+ bb.y + bb.height
11408
+ ];
11409
+ let [x1, y1, width1, height1, right1, bottom1] = [
11410
+ bb1.x,
11411
+ bb1.y,
11412
+ bb1.width,
11413
+ bb1.height,
11414
+ bb1.x + bb1.width,
11415
+ bb1.y + bb1.height
11416
+ ];
11417
+ let intersects = false;
11418
+
11419
+ if (x < x1 && right > right1 && y < y1 && bottom > bottom1) {
11420
+ intersects = true;
11421
+ }
11422
+
11423
+ console.log('???', intersects, 'dims', width, height, '2', width1, height1);
11424
+
11425
+ return intersects;
11426
+ }
11427
+ */
11428
+
11429
+ function svgPathSimplify(input = '', settings = {}) {
11430
+
11431
+ let preset = settings['preset'] !== undefined && settings['preset'] ? settings['preset'] : null;
11432
+ let defaults = preset && presetSettings[preset] !== undefined ? presetSettings[preset] : presetSettings['default'];
11433
+
11434
+ // merge settings
11435
+ settings = {
11436
+ ...defaults,
11437
+ ...settings
11438
+ };
11439
+
11440
+ let { getObject = false, removeComments, removeOffCanvas, unGroup, mergePaths, removeElements, removeDimensions, removeIds, removeClassNames, omitNamespace, cleanUpStrokes, addViewBox, addDimensions, removePrologue, removeHidden, removeUnused, cleanupDefs, cleanupClip, cleanupSVGAtts, removeNameSpaced, removeNameSpacedAtts, attributesToGroup, minifyRgbColors, stylesToAttributes, fixHref, legacyHref, allowMeta, allowDataAtts, allowAriaAtts, convertPathLength, removeSVGAttributes, removeElAttributes, shapesToPaths, shapeConvert, convertShapes, simplifyBezier, optimizeOrder, autoClose, removeZeroLength, refineClosing, removeColinear, flatBezierToLinetos, revertToQuadratics, refineExtremes, simplifyCorners, fixDirections, keepExtremes, keepCorners, keepInflections, addExtremes, reversePath, toAbsolute, toRelative, toMixed, toShorthands, toLonghands, quadraticToCubic, arcToCubic, cubicToArc, lineToCubic, decimals, autoAccuracy, minifyD, tolerance, toPolygon, smoothPoly, polyFormat, precisionPoly, simplifyRD, simplifyRDP, harmonizeCpts, removeOrphanSubpaths, simplifyRound, scale, scaleTo, crop, alignToOrigin, convertTransforms, keepSmaller, splitCompound } = settings;
9942
11441
 
9943
11442
  // clamp tolerance and scale
9944
11443
  tolerance = Math.max(0.1, tolerance);
9945
11444
  scale = Math.max(0.001, scale);
11445
+ if(fixDirections) keepSmaller = false;
11446
+ if (scale !== 1 || scaleTo || crop || alignToOrigin) {
11447
+ convertTransforms = true;
11448
+ settings.convertTransforms = true;
11449
+ }
9946
11450
 
9947
11451
  let inputType = detectInputType(input);
9948
11452
  let svg = '';
@@ -9957,7 +11461,9 @@ function svgPathSimplify(input = '', {
9957
11461
  let pathDataPlusArr_global = [];
9958
11462
  let paths = [];
9959
11463
  let polys = [];
11464
+ let poly = [];
9960
11465
  let dStr = '';
11466
+ let dOriginal = '';
9961
11467
 
9962
11468
  /**
9963
11469
  * normalize input
@@ -9981,25 +11487,55 @@ function svgPathSimplify(input = '', {
9981
11487
  autoClose = false;
9982
11488
  let accuracyArr = [];
9983
11489
 
11490
+ // validate point JSON
11491
+ if (inputType === 'json') {
11492
+ let pts = [];
11493
+ let needsQuotes = /([{,]\s*)(x|y)(\s*:)/.test(input);
11494
+ if (needsQuotes) input = input.replaceAll('x:', '"x":').replaceAll('y:', '"y":');
11495
+
11496
+ try {
11497
+ pts = JSON.parse(input);
11498
+ } catch {
11499
+ console.warn('No valid JSON');
11500
+ }
11501
+ if (pts.length) {
11502
+ inputType = 'polyArray';
11503
+ input = normalizePoly(pts);
11504
+ }
11505
+ }
11506
+
9984
11507
  // single path or polys
9985
- if (inputType !== 'svgMarkup') {
11508
+ if (inputType !== 'svgMarkup' && inputType !== 'symbol') {
9986
11509
  if (inputType === 'pathDataString') {
9987
11510
  d = input;
9988
11511
  } else if (inputType === 'polyString') {
9989
- d = 'M' + input;
11512
+ splitCompound = false;
11513
+ poly = normalizePoly(input);
11514
+ d = pathDataFromPoly(poly, closed);
11515
+
9990
11516
  }
9991
11517
 
9992
11518
  else if (inputType === 'polyArray' || inputType === 'polyObjectArray' || inputType === 'polyComplexArray' || inputType === 'polyComplexObjectArray') {
11519
+ splitCompound = false;
9993
11520
 
9994
11521
  // normalize poly input to object array
9995
- let poly = normalizePoly(input);
11522
+ poly = normalizePoly(input);
9996
11523
 
9997
11524
  // convert to pathdata
9998
- d = pathDataFromPoly(poly);
11525
+ let closed = true;
9999
11526
 
10000
11527
  // calculate size
11528
+ d = pathDataFromPoly(poly, closed);
10001
11529
  dStr = d.map(com => { return `${com.type} ${com.values.join(' ')}` }).join(' ');
11530
+ dOriginal = dStr;
10002
11531
  svgSize = dStr.length;
11532
+
11533
+ /*
11534
+ d=''
11535
+ dOriginal = '';
11536
+ svgSize = input.length;
11537
+ */
11538
+
10003
11539
  }
10004
11540
 
10005
11541
  else if (inputType === 'pathData') {
@@ -10008,6 +11544,11 @@ function svgPathSimplify(input = '', {
10008
11544
  // stringify to compare lengths
10009
11545
  dStr = Array.from(d).map(com => { return `${com.type} ${com.values.join(' ')}` }).join(' ');
10010
11546
  svgSize = dStr.length;
11547
+
11548
+ }
11549
+ // not valid - set dummy path data
11550
+ else {
11551
+ d = 'M0 0 h0';
10011
11552
  }
10012
11553
 
10013
11554
  paths.push({ d, el: null });
@@ -10016,27 +11557,34 @@ function svgPathSimplify(input = '', {
10016
11557
  // mode:1 – process complete svg DOM
10017
11558
  else {
10018
11559
 
11560
+ // convert symbol temporarily to SVG
11561
+ if (inputType === 'symbol') {
11562
+ input = input.replaceAll('<symbol', '<svg').replaceAll('</symbol', '</svg');
11563
+ // ids are mandatory for symbols
11564
+ removeIds = false;
11565
+ removeDimensions = true;
11566
+ }
11567
+
10019
11568
  // convert all shapes to paths
10020
11569
  if (shapesToPaths) {
10021
- shapeConvert = true;
11570
+ shapeConvert = 'toPaths';
10022
11571
  convert_rects = true;
10023
11572
  convert_ellipses = true;
10024
11573
  convert_poly = true;
10025
11574
  convert_lines = true;
10026
11575
  }
10027
11576
 
10028
- let svgPropObject = cleanUpSVG(input, {
10029
- removeIds, removeClassNames, removeDimensions, cleanupSVGAtts, cleanUpStrokes, removeHidden, removeUnused, removeNameSpaced, stylesToAttributes, removePrologue, fixHref, mergePaths, convertTransforms, legacyHref, cleanupDefs, cleanupClip, addViewBox, removeOffCanvas, addDimensions,
10030
- shapeConvert, convert_rects, convert_ellipses, convert_poly, convert_lines, minifyRgbColors, unGroup, convertTransforms
10031
- }
10032
- );
11577
+ // sanitize SVG - clone/decouple settings
11578
+ let svgPropObject = cleanUpSVG(input, JSON.parse(JSON.stringify(settings)));
10033
11579
  svg = svgPropObject.svg;
10034
11580
 
10035
11581
  // collect paths
10036
11582
  let pathEls = svg.querySelectorAll('path');
10037
11583
 
10038
11584
  pathEls.forEach((path, i) => {
10039
- paths.push({ d: path.getAttribute('d'), el: path, idx: i });
11585
+ let d = path.getAttribute('d');
11586
+
11587
+ paths.push({ d, el: path, idx: i });
10040
11588
  });
10041
11589
 
10042
11590
  // get viewBox/dimensions
@@ -10052,20 +11600,19 @@ function svgPathSimplify(input = '', {
10052
11600
  // SVG optimization options
10053
11601
  let pathOptions = {
10054
11602
  toRelative,
10055
- toAbsolute,
10056
- toLonghands,
11603
+ toMixed,
10057
11604
  toShorthands,
10058
11605
  decimals,
10059
11606
  };
10060
11607
 
10061
- // combinded path data for SVGs with mergePaths enabled
10062
- let pathData_merged = [];
10063
-
10064
11608
  for (let i = 0, l = paths.length; l && i < l; i++) {
10065
11609
 
10066
11610
  let pathDataPlusArr = [];
10067
11611
  let path = paths[i];
10068
11612
  let { d, el } = path;
11613
+ let isPoly = false;
11614
+
11615
+ // if polygon we already heave absolute coordinates
10069
11616
 
10070
11617
  let pathData = parsePathDataNormalized(d, { quadraticToCubic, arcToCubic });
10071
11618
 
@@ -10104,7 +11651,7 @@ function svgPathSimplify(input = '', {
10104
11651
  // count commands for evaluation
10105
11652
  let comCount = pathData.length;
10106
11653
 
10107
- if (removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
11654
+ if (!isPoly && removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
10108
11655
 
10109
11656
  /**
10110
11657
  * get sub paths
@@ -10117,24 +11664,26 @@ function svgPathSimplify(input = '', {
10117
11664
 
10118
11665
  let pathDataSub = subPathArr[i];
10119
11666
  let poly = [];
10120
-
10121
11667
  let coms = Array.from(new Set(pathDataSub.map(com => com.type))).join('');
10122
- let isPoly = !(/[acqts]/gi).test(coms);
10123
- let closed = (/[z]/gi).test(coms);
11668
+ isPoly = !(/[acqts]/gi).test(coms);
11669
+ let closed = isPoly ? true : false;
10124
11670
 
10125
- if (isPoly) {
11671
+ if (isPoly && !mode) {
10126
11672
 
10127
11673
  poly = getPathDataVertices(pathDataSub);
11674
+ let bb = getPolyBBox(reducePoints(poly, 64));
10128
11675
 
10129
11676
  // simplify polygon
10130
11677
  if (simplifyRD > 0) {
10131
- poly = simplifyPolyRD(poly, { quality: simplifyRD + 'px' });
11678
+ poly = simplifyPolyRD(poly, { quality: simplifyRD, width: bb.width, height: bb.height });
10132
11679
  }
10133
11680
 
10134
11681
  if (simplifyRDP > 0) {
10135
- poly = simplifyPolyRDP(poly, { quality: simplifyRDP + 'px' });
11682
+ poly = simplifyPolyRDP(poly, { quality: simplifyRDP, width: bb.width, height: bb.height });
11683
+
10136
11684
  }
10137
11685
 
11686
+ toPolygon = false;
10138
11687
  pathDataSub = pathDataFromPoly(poly, closed);
10139
11688
 
10140
11689
  }
@@ -10148,21 +11697,18 @@ function svgPathSimplify(input = '', {
10148
11697
  smoothPoly = false;
10149
11698
  harmonizeCpts = false;
10150
11699
 
10151
- let pathDataSubPlus = analyzePathData(pathDataSub);
10152
- let { bb, pathData } = pathDataSubPlus;
10153
- pathDataSub = pathData;
11700
+ pathDataSub = getPathDataVerbose(pathDataSub);
10154
11701
 
10155
- let polyData = pathDataToPolygon(pathDataSub, {
11702
+ let polyData = pathDataToPolygonOpt(pathDataSub, {
10156
11703
  precisionPoly,
10157
11704
  autoAccuracy,
10158
- polyFormat,
10159
- decimals,
11705
+
10160
11706
  simplifyRD,
10161
11707
  simplifyRDP
10162
11708
  });
10163
11709
 
10164
- polys.push(polyData.poly);
10165
11710
  pathDataSub = polyData.pathData;
11711
+ isPoly = true;
10166
11712
 
10167
11713
  }
10168
11714
 
@@ -10192,7 +11738,10 @@ function svgPathSimplify(input = '', {
10192
11738
  simplifyRD,
10193
11739
  simplifyRDP,
10194
11740
  };
11741
+
10195
11742
  pathDataSub = simplifyPolygonToPathData(poly, optionsPoly);
11743
+ // flag as non poly as we're smoothing to curves
11744
+
10196
11745
  }
10197
11746
  }
10198
11747
 
@@ -10209,27 +11758,43 @@ function svgPathSimplify(input = '', {
10209
11758
  if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, { tolerance, flatBezierToLinetos: false });
10210
11759
 
10211
11760
  let tMin = 0, tMax = 1;
10212
- if (addExtremes || addSemiExtremes) pathDataSub = addExtremePoints(pathDataSub,
10213
- { tMin, tMax, addExtremes, addSemiExtremes, angles: [30] });
11761
+ if (addExtremes) pathDataSub = addExtremePoints(pathDataSub,
11762
+ { tMin, tMax, addExtremes, angles: [30] });
10214
11763
 
10215
11764
  // reverse
10216
11765
  if (reversePath) {
10217
11766
  pathDataSub = reversePathData(pathDataSub);
10218
11767
  }
10219
11768
 
10220
- // analyze pathdata to add info about signicant properties such as extremes, corners
10221
- let pathDataPlus = analyzePathData(pathDataSub, {
10222
- detectSemiExtremes: addSemiExtremes,
10223
- });
11769
+ // analyze pathdata to add info about significant properties such as extremes, corners
11770
+ let pathDataPlus = { bb: {}, dimA: 0, pathData: [] };
11771
+
11772
+ if (!isPoly) {
11773
+ pathDataPlus = analyzePathData(pathDataSub);
11774
+ }
11775
+ // we skip detailed analysis for native polygons
11776
+ else {
11777
+ if (!poly.length) {
11778
+ let pathDataCubic = convertPathData(JSON.parse(JSON.stringify(pathDataSub)), { toLonghands: true, toAbsolute: true, arcToCubic: true, testTypes: true });
11779
+ pathDataPlus.bb = getPolyBBox(getPathDataVertices(pathDataCubic));
11780
+ }
11781
+ pathDataPlus.dimA = pathDataPlus.bb.width + pathDataPlus.bb.height;
11782
+ pathDataPlus.pathData = getPathDataVerbose(pathDataSub, {
11783
+ addSquareLength: false,
11784
+ addArea: false,
11785
+ addAverageDim: false
11786
+ });
11787
+ }
10224
11788
 
10225
11789
  // simplify beziers
10226
11790
  let { pathData, bb, dimA } = pathDataPlus;
11791
+
10227
11792
  xArr.push(bb.x, bb.x + bb.width);
10228
11793
  yArr.push(bb.y, bb.y + bb.height);
10229
11794
 
10230
11795
  if (refineClosing) pathData = refineClosingCommand(pathData, { threshold: dimA * 0.001 });
10231
11796
 
10232
- pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance }) : pathData;
11797
+ pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, revertToQuadratics, tolerance }) : pathData;
10233
11798
 
10234
11799
  // refine extremes
10235
11800
  if (refineExtremes) {
@@ -10269,8 +11834,6 @@ function svgPathSimplify(input = '', {
10269
11834
  let decimalsSub = detectAccuracy(pathData);
10270
11835
  accuracyArr.push(decimalsSub);
10271
11836
 
10272
- // pre round sub path
10273
-
10274
11837
  }
10275
11838
 
10276
11839
  // update
@@ -10287,16 +11850,16 @@ function svgPathSimplify(input = '', {
10287
11850
  bb_global = { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin };
10288
11851
  let isPortrait = bb_global.height > bb_global.width;
10289
11852
 
11853
+ // fix path directions - before reordering
11854
+ if (fixDirections) {
11855
+ pathDataPlusArr = fixPathDataDirections(pathDataPlusArr);
11856
+ }
11857
+
10290
11858
  // prefer top to bottom priority for portrait aspect ratios
10291
11859
  if (optimizeOrder) {
10292
11860
  pathDataPlusArr = isPortrait ? pathDataPlusArr.sort((a, b) => a.bb.y - b.bb.y || a.bb.x - b.bb.x) : pathDataPlusArr.sort((a, b) => a.bb.x - b.bb.x || a.bb.y - b.bb.y);
10293
11861
  }
10294
11862
 
10295
- // fix path directions
10296
- if (fixDirections) {
10297
- pathDataPlusArr = fixPathDataDirections(pathDataPlusArr);
10298
- }
10299
-
10300
11863
  // flatten compound paths
10301
11864
  pathData = [];
10302
11865
 
@@ -10315,57 +11878,69 @@ function svgPathSimplify(input = '', {
10315
11878
  pathOptions.decimals = decimals;
10316
11879
  }
10317
11880
 
10318
- // collect for merged svg paths
10319
- mergePaths = false;
10320
- if (el && mergePaths) {
10321
- pathData_merged.push(...pathData);
11881
+ // add simplified poly - if not populated by toPoly conversion
11882
+ if (isPoly) {
11883
+
11884
+ pathDataPlusArr.forEach(sub => {
11885
+ let poly = getPathDataVertices(sub.pathData, false, decimals);
11886
+ if (polyFormat === 'array') {
11887
+ poly = polyPtsToArray(poly);
11888
+ }
11889
+ polys.push(poly);
11890
+ });
11891
+
10322
11892
  }
10323
- // single output
10324
- else {
10325
11893
 
10326
- // clone pathdata
11894
+ // split into sub paths - returns svg with multiple paths
11895
+ if (splitCompound && !mode && pathDataPlusArr.length > 1) {
11896
+ let pathDataSplit = splitCompundGroups(pathDataPlusArr, { toRelative, toShorthands, decimals, addDimensions });
11897
+ svg = new DOMParser().parseFromString(pathDataSplit.svg, 'image/svg+xml').querySelector('svg');
11898
+ // switch output type
11899
+ mode = 1;
11900
+ inputType = 'splitPath';
11901
+ }
10327
11902
 
10328
- pathData = JSON.parse(JSON.stringify(pathData));
11903
+ // clone pathdata
11904
+ pathData = JSON.parse(JSON.stringify(pathData));
10329
11905
 
10330
- // optimize path data
10331
- pathData = convertPathData(pathData, pathOptions);
11906
+ // optimize path data
11907
+ pathData = convertPathData(pathData, pathOptions);
10332
11908
 
10333
- // remove zero-length segments introduced by rounding
10334
- if (removeZeroLength) pathData = removeZeroLengthLinetos(pathData);
11909
+ // remove zero-length segments introduced by rounding
11910
+ if (removeZeroLength) pathData = removeZeroLengthLinetos(pathData);
10335
11911
 
10336
- // realign path to zero origin
10337
- if (alignToOrigin) {
11912
+ // realign path to zero origin
11913
+ if (alignToOrigin) {
10338
11914
 
10339
- pathData[0].values[0] = (pathData[0].values[0] - bb_global.x).toFixed(decimals);
10340
- pathData[0].values[1] = (pathData[0].values[1] - bb_global.y).toFixed(decimals);
11915
+ pathData[0].values[0] = roundTo((pathData[0].values[0] - bb_global.x), decimals);
11916
+ pathData[0].values[1] = roundTo((pathData[0].values[1] - bb_global.y), decimals);
10341
11917
 
10342
- bb_global.x = 0;
10343
- bb_global.y = 0;
10344
- }
11918
+ bb_global.x = 0;
11919
+ bb_global.y = 0;
11920
+ }
10345
11921
 
10346
- // compare command count
10347
- let comCountS = pathData.length;
11922
+ // compare command count
11923
+ let comCountS = pathData.length;
10348
11924
 
10349
- let dOpt = pathDataToD(pathData, minifyD);
11925
+ let dOpt = pathDataToD(pathData, minifyD);
10350
11926
 
10351
- svgSizeOpt = dOpt.length;
11927
+ svgSizeOpt = dOpt.length;
10352
11928
 
10353
- compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
11929
+ compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
10354
11930
 
10355
- path.d = dOpt;
10356
- path.report = {
10357
- original: comCount,
10358
- new: comCountS,
10359
- saved: comCount - comCountS,
10360
- compression,
10361
- decimals,
11931
+ path.d = dOpt;
11932
+ path.report = {
11933
+ original: comCount,
11934
+ new: comCountS,
11935
+ saved: comCount - comCountS,
11936
+ compression,
11937
+ decimals,
10362
11938
 
10363
- };
11939
+ };
10364
11940
 
10365
- // apply new path for svgs
10366
- if (el) {
10367
- el.setAttribute('d', dOpt);
10368
- }
11941
+ // apply new path for svgs
11942
+ if (el) {
11943
+ el.setAttribute('d', dOpt);
10369
11944
  }
10370
11945
 
10371
11946
  } // end path array
@@ -10373,36 +11948,11 @@ function svgPathSimplify(input = '', {
10373
11948
  /**
10374
11949
  * stringify new SVG
10375
11950
  */
10376
- if (mode) {
10377
-
10378
- if (pathData_merged.length) {
10379
-
10380
- // optimize path data
10381
- let pathData = convertPathData(pathData_merged, pathOptions);
10382
-
10383
- // remove zero-length segments introduced by rounding
10384
- pathData = removeZeroLengthLinetos(pathData);
10385
-
10386
- let dOpt = pathDataToD(pathData, minifyD);
10387
-
10388
- // apply new path for svgs
10389
- paths[0].el.setAttribute('d', dOpt);
10390
-
10391
- // remove other paths
10392
- for (let i = 1; i < paths.length; i++) {
10393
- let pathEl = paths[i].el;
10394
- if (pathEl) pathEl.remove();
10395
- }
10396
-
10397
- // remove empty groups e.g groups
10398
- removeEmptySVGEls(svg);
10399
- }
11951
+ if (mode || inputType === 'symbol') {
10400
11952
 
10401
11953
  // adjust viewBox and width for scale
10402
11954
  if (scale) {
10403
-
10404
11955
  let { x, y, width, height, w, h, hasViewBox, hasWidth, hasHeight, widthUnit, heightUnit } = viewBox;
10405
-
10406
11956
  if (crop) {
10407
11957
  x = bb_global.x;
10408
11958
  y = bb_global.y;
@@ -10413,14 +11963,14 @@ function svgPathSimplify(input = '', {
10413
11963
  }
10414
11964
 
10415
11965
  if (hasViewBox) {
10416
- svg.setAttribute('viewBox', [x, y, width, height].map(val => +(val * scale).toFixed(decimals)).join(' '));
11966
+ svg.setAttribute('viewBox', [x, y, width, height].map(val => roundTo(val * scale, decimals)).join(' '));
10417
11967
  }
10418
11968
  if (hasWidth) {
10419
- svg.setAttribute('width', +(w * scale).toFixed(decimals) + widthUnit);
11969
+ svg.setAttribute('width', roundTo(w * scale, decimals) + widthUnit);
10420
11970
  }
10421
11971
 
10422
11972
  if (hasHeight) {
10423
- svg.setAttribute('height', +(h * scale).toFixed(decimals) + heightUnit);
11973
+ svg.setAttribute('height', roundTo(h * scale, decimals) + heightUnit);
10424
11974
  }
10425
11975
  }
10426
11976
 
@@ -10433,7 +11983,9 @@ function svgPathSimplify(input = '', {
10433
11983
  });
10434
11984
  }
10435
11985
 
10436
- svg = stringifySVG(svg, { omitNamespace, removeComments });
11986
+ if (removeSVGAttributes.includes('xmlns')) omitNamespace = true;
11987
+
11988
+ svg = stringifySVG(svg, { omitNamespace, removeComments, format: minifyD });
10437
11989
 
10438
11990
  svgSizeOpt = svg.length;
10439
11991
 
@@ -10446,9 +11998,15 @@ function svgPathSimplify(input = '', {
10446
11998
  svgSize,
10447
11999
  svgSizeOpt,
10448
12000
  compression,
10449
- decimals
12001
+ decimals,
10450
12002
  };
10451
12003
 
12004
+ if (keepSmaller && svgSize < svgSizeOpt && !splitCompound) {
12005
+
12006
+ svg = input;
12007
+ report.node = 'Original is smaller!';
12008
+ }
12009
+
10452
12010
  } else {
10453
12011
  ({ d, report } = paths[0]);
10454
12012
  }
@@ -10457,7 +12015,11 @@ function svgPathSimplify(input = '', {
10457
12015
  polys = polys[0];
10458
12016
  }
10459
12017
 
10460
- return !getObject ? (d ? d : svg) : { svg, d, polys, report, pathDataPlusArr: pathDataPlusArr_global, inputType };
12018
+ if (polyFormat === 'string' && polys.length) {
12019
+ polys = polys.flat().map(pt => `${pt.x},${pt.y}`).join(' ');
12020
+ }
12021
+
12022
+ return !getObject ? (d ? d : svg) : { svg, d, polys, report, pathDataPlusArr: pathDataPlusArr_global, inputType, dOriginal };
10461
12023
 
10462
12024
  }
10463
12025