svg-path-simplify 0.4.3 → 0.4.4

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 (39) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +1 -0
  3. package/dist/svg-path-simplify.esm.js +1610 -495
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +1611 -494
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +893 -456
  8. package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
  9. package/dist/svg-path-simplify.poly.cjs +9 -8
  10. package/index.html +58 -17
  11. package/package.json +1 -1
  12. package/src/constants.js +4 -0
  13. package/src/detect_input.js +47 -29
  14. package/src/index.js +8 -0
  15. package/src/pathData_simplify_cubic.js +26 -16
  16. package/src/pathData_simplify_revertToquadratics.js +0 -1
  17. package/src/pathSimplify-main.js +75 -20
  18. package/src/pathSimplify-only-pathdata.js +7 -2
  19. package/src/pathSimplify-presets.js +15 -4
  20. package/src/svg-getAttributes.js +4 -2
  21. package/src/svgii/convert_units.js +1 -1
  22. package/src/svgii/geometry.js +140 -2
  23. package/src/svgii/geometry_bbox_element.js +1 -1
  24. package/src/svgii/geometry_deduceRadius.js +116 -27
  25. package/src/svgii/geometry_length.js +17 -1
  26. package/src/svgii/pathData_analyze.js +18 -0
  27. package/src/svgii/pathData_convert.js +188 -88
  28. package/src/svgii/pathData_fix_directions.js +10 -18
  29. package/src/svgii/pathData_reorder.js +122 -16
  30. package/src/svgii/pathData_simplify_refineCorners.js +130 -35
  31. package/src/svgii/pathData_simplify_refine_round.js +420 -0
  32. package/src/svgii/rounding.js +79 -78
  33. package/src/svgii/svg_cleanup.js +68 -20
  34. package/src/svgii/svg_cleanup_convertPathLength.js +22 -15
  35. package/src/svgii/svg_cleanup_remove_els_and_atts.js +6 -1
  36. package/src/svgii/svg_el_parse_style_props.js +13 -10
  37. package/src/svgii/svg_validate.js +220 -0
  38. package/tests/testSVG.js +14 -1
  39. package/src/svgii/pathData_refine_round.js +0 -222
@@ -28,62 +28,276 @@
28
28
  }
29
29
  }
30
30
 
31
- function renderPath(svg, d = '', stroke = 'green', strokeWidth = '1%', opacity="1", render = true) {
31
+ const {
32
+ 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,
33
+ 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
34
+ } = Math;
35
+
36
+ const rad2Deg = 180/Math.PI;
37
+ const deg2rad = Math.PI/180;
38
+ const root2 = 1.4142135623730951;
39
+ const svgNs = 'http://www.w3.org/2000/svg';
40
+ const dummySVG = `<svg id="svgInvalid" xmlns="${svgNs}" viewBox="0 0 1 1"><path d="M0 0 h0" /></svg>`;
32
41
 
33
- let path = `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${opacity}" /> `;
42
+ // 1/2.54
43
+ const inch2cm = 0.39370078;
34
44
 
35
- if (render) {
36
- svg.insertAdjacentHTML("beforeend", path);
37
- } else {
38
- return path;
45
+ // 1/72
46
+ const inch2pt = 0.01388889;
47
+
48
+ function validateSVG(markup, allowed = {}) {
49
+ allowed = {
50
+ ...{
51
+
52
+ useElsNested: 5000,
53
+ hasScripts: false,
54
+ hasEntity: false,
55
+ fileSizeKB: 10000,
56
+ isSymbolSprite: false,
57
+ isSvgFont: false
58
+ },
59
+ ...allowed
60
+ };
61
+
62
+ let fileReport = analyzeSVG(markup, allowed);
63
+ let isValid = true;
64
+ let log = [];
65
+
66
+ if (!fileReport.hasEls) {
67
+ log.push("no elements");
68
+ isValid = false;
69
+ }
70
+
71
+ if (Object.keys(fileReport).length) {
72
+ if (fileReport.isBillionLaugh === true) {
73
+ log.push(`suspicious: might contain billion laugh attack`);
74
+ isValid = false;
75
+ }
76
+
77
+ for (let key in allowed) {
78
+ let val = allowed[key];
79
+ let valRep = fileReport[key];
80
+ if (typeof val === "number" && valRep > val) {
81
+ log.push(`allowed "${key}" exceeded: ${valRep} / ${val} `);
82
+ isValid = false;
83
+ }
84
+ if (valRep === true && val === false) {
85
+ log.push(`not allowed: "${key}" `);
86
+ isValid = false;
87
+ }
88
+ }
89
+ } else {
90
+ isValid = false;
91
+ }
92
+
93
+ /*
94
+ if (!isValid) {
95
+ log = ["SVG not valid"].concat(log);
96
+
97
+ if (Object.keys(fileReport).length) {
98
+ console.warn(fileReport);
99
+ }
100
+ }
101
+ */
102
+
103
+ return { isValid, log, fileReport };
104
+ }
105
+
106
+ function analyzeSVG(markup, allowed = {}) {
107
+ markup = markup.trim();
108
+ let doc, svg;
109
+ let fileSizeKB = +(markup.length / 1024).toFixed(3);
110
+
111
+ let fileReport = {
112
+ totalEls: 1,
113
+ hasEls: true,
114
+ hasDefs: false,
115
+ geometryEls: [],
116
+ useEls: 0,
117
+ useElsNested: 0,
118
+ nonsensePaths: 0,
119
+ isSuspicious: false,
120
+ isBillionLaugh: false,
121
+ hasScripts: false,
122
+ hasPrologue: false,
123
+ hasEntity: false,
124
+ isPathData:false,
125
+ fileSizeKB,
126
+ hasXmlns: markup.includes("http://www.w3.org/2000/svg"),
127
+ isSymbolSprite: false,
128
+ isSvgFont: markup.includes("<glyph>")
129
+ };
130
+
131
+ let maxNested = allowed.useElsNested ? allowed.useElsNested : 2000;
132
+
133
+ /**
134
+ * analyze nestes use references
135
+ */
136
+ const countUseRefs = (useEls, maxNested = 2000) => {
137
+ let nestedCount = 0;
138
+
139
+ for (let i = 0; i < useEls.length && nestedCount < maxNested; i++) {
140
+ let use = useEls[i];
141
+ let refId = use.getAttribute("xlink:href")
142
+ ? use.getAttribute("xlink:href")
143
+ : use.getAttribute("href");
144
+ refId = refId ? refId.replace("#", "") : "";
145
+
146
+ use.setAttribute("href", "#" + refId);
147
+
148
+ let refEl = svg.getElementById(refId);
149
+ let nestedUse = refEl.querySelectorAll("use");
150
+ let nestedUseLength = nestedUse.length;
151
+ nestedCount += nestedUseLength;
152
+
153
+ // query nested use references
154
+ for (let n = 0; n < nestedUse.length && nestedCount < maxNested; n++) {
155
+ let nested = nestedUse[n];
156
+ let id1 = nested.getAttribute("href").replace("#", "");
157
+ let refEl1 = svg.getElementById(id1);
158
+ let nestedUse1 = refEl1.querySelectorAll("use");
159
+ nestedCount += nestedUse1.length;
160
+ }
161
+ }
162
+ fileReport.useElsNested = nestedCount;
163
+ return nestedCount;
164
+ };
165
+ let hasEntity = /\<\!ENTITY/gi.test(markup);
166
+ let hasScripts = /\<script/gi.test(markup) ? true : false;
167
+ let hasUse = /\<use/gi.test(markup) ? true : false;
168
+ let hasEls = /[\<path|\<polygon|\<polyline|\<rect|\<circle|\<ellipse|\<line|\<text|\<foreignObject]/gi.test(markup);
169
+ let hasDefs = /[\<filter|\<linearGradient|\<radialGradient|\<pattern|\<animate|\<animateMotion|\<animateTransform|\<clipPath|\<mask|\<symbol|\<marker]/gi.test(markup);
170
+
171
+ let isPathData = (markup.startsWith('M') || markup.startsWith('m')) && !/[\<svg|\<\/svg]/gi.test(markup);
172
+ fileReport.isPathData = isPathData;
173
+
174
+ // seems OK
175
+ if (!hasEntity && !hasUse && !hasScripts && (hasEls || hasDefs) && fileSizeKB < allowed.fileSizeKB) {
176
+ fileReport.hasEls = hasEls;
177
+ fileReport.hasDefs = hasDefs;
178
+
179
+ return fileReport
180
+ }
181
+
182
+ // Contains xml entity definition: highly suspicious - stop parsing!
183
+ if (allowed.hasEntity === false && hasEntity) {
184
+ fileReport.hasEntity = true;
185
+
186
+ }
187
+
188
+ /**
189
+ * sanitizing for parsing:
190
+ * remove xml prologue and comments
191
+ */
192
+ markup = markup
193
+ .replace(/\<\?xml.+\?\>|\<\!DOCTYPE.+]\>/g, "")
194
+ .replace(/(<!--.*?-->)|(<!--[\S\s]+?-->)|(<!--[\S\s]*?$)/g, "");
195
+
196
+ /**
197
+ * Try to parse svg:
198
+ * invalid svg will return false via "catch"
199
+ */
200
+ try {
201
+
202
+ doc = new DOMParser().parseFromString(markup, "text/html");
203
+ svg = doc.querySelector("svg");
204
+
205
+ // paths containing only a M command
206
+ let nonsensePaths = svg.querySelectorAll('path[d="M0,0"], path[d="M0 0"]').length;
207
+ let useEls = svg.querySelectorAll("use").length;
208
+
209
+ // create analyzing object
210
+ fileReport.totalEls = svg.querySelectorAll("*").length;
211
+ fileReport.geometryEls = svg.querySelectorAll(
212
+ "path, rect, circle, ellipse, polygon, polyline, line"
213
+ ).length;
214
+
215
+ fileReport.hasScripts = hasScripts;
216
+ fileReport.useEls = useEls;
217
+ fileReport.nonsensePaths = nonsensePaths;
218
+ fileReport.isSuspicious = false;
219
+ fileReport.isBillionLaugh = false;
220
+ fileReport.hasXmlns = svg.getAttribute("xmlns")
221
+ ? svg.getAttribute("xmlns") === "http://www.w3.org/2000/svg"
222
+ ? true
223
+ : false
224
+ : false;
225
+ fileReport.isSymbolSprite =
226
+ svg.querySelectorAll("symbol").length &&
227
+ svg.querySelectorAll("use").length === 0
228
+ ? true
229
+ : false;
230
+ fileReport.isSvgFont = svg.querySelectorAll("glyph").length ? true : false;
231
+
232
+ let totalEls = fileReport.totalEls;
233
+ let totalUseEls = fileReport.useEls;
234
+ let usePercentage = (100 / totalEls) * totalUseEls;
235
+
236
+ // if percentage of use elements is higher than 75% - suspicious
237
+ if (usePercentage > 75) {
238
+ fileReport.isSuspicious = true;
239
+
240
+ // check nested use references
241
+ let nestedCount = countUseRefs(svg.querySelectorAll("use"), maxNested);
242
+ if (nestedCount >= maxNested) {
243
+ fileReport.isBillionLaugh = true;
244
+ }
39
245
  }
40
246
 
247
+ return fileReport;
248
+ } catch {
249
+ // svg file has malformed markup
250
+ console.warn("svg could not be parsed");
251
+ return false;
252
+ }
41
253
  }
42
254
 
43
255
  function detectInputType(input) {
44
- let type = 'string';
45
- /*
46
- if (input instanceof HTMLImageElement) return "img";
47
- if (input instanceof SVGElement) return "svg";
48
- if (input instanceof HTMLCanvasElement) return "canvas";
49
- if (input instanceof File) return "file";
50
- if (input instanceof ArrayBuffer) return "buffer";
51
- if (input instanceof Blob) return "blob";
52
- */
256
+ let log = '';
257
+ let isValid = true;
258
+
259
+ let result = {
260
+ inputType:'',
261
+ isValid:true,
262
+ fileReport:{},
263
+ };
264
+
53
265
  if (Array.isArray(input)) {
54
266
 
267
+ result.inputType = "array";
268
+
55
269
  // nested array
56
270
  if (Array.isArray(input[0])) {
57
271
 
58
272
  if (input[0].length === 2) {
59
273
 
60
- return 'polyArray'
274
+ result.inputType = 'polyArray';
61
275
  }
62
276
 
63
277
  else if (Array.isArray(input[0][0]) && input[0][0].length === 2) {
64
278
 
65
- return 'polyComplexArray'
279
+ result.inputType = 'polyComplexArray';
66
280
  }
67
281
  else if (input[0][0].x !== undefined && input[0][0].y !== undefined) {
68
282
 
69
- return 'polyComplexObjectArray'
283
+ result.inputType = 'polyComplexObjectArray';
70
284
  }
285
+
71
286
  }
72
287
 
73
288
  // is point array
74
289
  else if (input[0].x !== undefined && input[0].y !== undefined) {
75
290
 
76
- return 'polyObjectArray'
291
+ result.inputType = 'polyObjectArray';
77
292
  }
78
293
 
79
294
  // path data array
80
295
  else if (input[0]?.type && input[0]?.values
81
296
  ) {
82
- return "pathData"
83
-
297
+ result.inputType = "pathData";
84
298
  }
85
299
 
86
- return "array";
300
+ return result;
87
301
  }
88
302
 
89
303
  if (typeof input === "string") {
@@ -95,36 +309,48 @@
95
309
  let isJson = isNumberJson(input);
96
310
 
97
311
  if (isSVG) {
98
- type = 'svgMarkup';
312
+ let validate = validateSVG(input);
313
+ ({isValid, log} = validate) ;
314
+ if(!isValid){
315
+
316
+ result.inputType = 'invalid';
317
+ result.isValid=false,
318
+
319
+ result.log = log;
320
+ }else {
321
+ result.inputType = 'svgMarkup';
322
+ }
323
+
324
+ result.fileReport = validate.fileReport;
325
+
99
326
  }
100
327
 
101
328
  else if (isJson) {
102
- type = 'json';
329
+ result.inputType = 'json';
103
330
  }
104
331
 
105
332
  else if (isSymbol) {
106
- type = 'symbol';
333
+ result.inputType = 'symbol';
107
334
  }
108
335
  else if (isPathData) {
109
- type = 'pathDataString';
336
+ result.inputType = 'pathDataString';
110
337
  }
111
338
  else if (isPolyString) {
112
- type = 'polyString';
339
+ result.inputType = 'polyString';
113
340
  }
114
341
 
115
342
  else {
116
343
  let url = /^(file:|https?:\/\/|\/|\.\/|\.\.\/)/.test(input);
117
344
  let dataUrl = input.startsWith('data:image');
118
- type = url || dataUrl ? "url" : "string";
345
+ result.inputType = url || dataUrl ? "url" : "string";
119
346
  }
120
347
 
121
- return type
348
+ return result
122
349
  }
123
350
 
124
- type = typeof input;
125
- let constructor = input.constructor.name;
351
+ result.inputType = (input.constructor.name || typeof input ).toLowerCase();
126
352
 
127
- return (constructor || type).toLowerCase();
353
+ return result;
128
354
  }
129
355
 
130
356
  function isNumberJson(str) {
@@ -142,22 +368,6 @@
142
368
 
143
369
  }
144
370
 
145
- const {
146
- 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,
147
- 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
148
- } = Math;
149
-
150
- const rad2Deg = 180/Math.PI;
151
- const deg2rad = Math.PI/180;
152
- const root2 = 1.4142135623730951;
153
- const svgNs = 'http://www.w3.org/2000/svg';
154
-
155
- // 1/2.54
156
- const inch2cm = 0.39370078;
157
-
158
- // 1/72
159
- const inch2pt = 0.01388889;
160
-
161
371
  /*
162
372
  import {abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
163
373
  log, max, min, pow, random, round, sin, sqrt, tan, PI} from '/.constants.js';
@@ -412,6 +622,7 @@
412
622
  let t1 = 1 - t;
413
623
 
414
624
  // cubic beziers
625
+ /*
415
626
  if (isCubic) {
416
627
  pt = {
417
628
  x:
@@ -427,11 +638,29 @@
427
638
  };
428
639
 
429
640
  }
641
+ */
642
+
643
+ if (isCubic) {
644
+ pt = {
645
+ x:
646
+ t1 * t1 * t1 * p0.x +
647
+ 3 * t1 * t1 * t * cp1.x +
648
+ 3 * t1 * t * t * cp2.x +
649
+ t * t * t * p.x,
650
+ y:
651
+ t1 * t1 * t1 * p0.y +
652
+ 3 * t1 * t1 * t * cp1.y +
653
+ 3 * t1 * t * t * cp2.y +
654
+ t * t * t * p.y,
655
+ };
656
+
657
+ }
658
+
430
659
  // quadratic beziers
431
660
  else {
432
661
  pt = {
433
- x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
434
- y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t ** 2 * p.y,
662
+ x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t * t * p.x,
663
+ y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t * t * p.y,
435
664
  };
436
665
  }
437
666
 
@@ -747,12 +976,6 @@
747
976
  return tArr;
748
977
  }
749
978
 
750
- /**
751
- * based on Nikos M.'s answer
752
- * how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
753
- * https://stackoverflow.com/questions/87734/#75031511
754
- * See also: https://github.com/foo123/Geometrize
755
- */
756
979
  function getArcExtemes(p0, values) {
757
980
  // compute point on ellipse from angle around ellipse (theta)
758
981
  const arc = (theta, cx, cy, rx, ry, alpha) => {
@@ -1621,6 +1844,36 @@
1621
1844
  return `#${rHex}${gHex}${bHex}${aHex}`;
1622
1845
  }
1623
1846
 
1847
+ /**
1848
+ * round path data
1849
+ * either by explicit decimal value or
1850
+ * based on suggested accuracy in path data
1851
+ */
1852
+ function roundPathData(pathData, decimalsGlobal = -1) {
1853
+
1854
+ if (decimalsGlobal < 0) return pathData;
1855
+
1856
+ let len = pathData.length;
1857
+ let decimals = decimalsGlobal;
1858
+ let decimalsArc = decimals < 3 ? decimals+2 : decimals;
1859
+
1860
+ for (let c = 0; c < len; c++) {
1861
+ let com = pathData[c];
1862
+ let { type, values } = com;
1863
+ let valLen = values.length;
1864
+ if (!valLen) continue
1865
+
1866
+ let isArc = type.toLowerCase() === 'a';
1867
+
1868
+ for (let v = 0; v < valLen; v++) {
1869
+ // allow higher accuracy for arc radii (... it's always arcs)
1870
+ pathData[c].values[v] = isArc && v < 2 ? roundTo(values[v], decimalsArc) : roundTo(values[v], decimals);
1871
+ }
1872
+ }
1873
+
1874
+ return pathData;
1875
+ }
1876
+
1624
1877
  function detectAccuracyPoly(pts) {
1625
1878
  let dims = [];
1626
1879
 
@@ -1630,7 +1883,7 @@
1630
1883
  let { p0 = null, p = null, dimA = 0 } = pt;
1631
1884
 
1632
1885
  // use existing averave dimension value or calculate
1633
- if ( p && p0) {
1886
+ if (p && p0) {
1634
1887
  dimA = dimA ? dimA : getDistManhattan(p0, p);
1635
1888
 
1636
1889
  if (dimA) dims.push(dimA);
@@ -1682,9 +1935,27 @@
1682
1935
 
1683
1936
  }
1684
1937
 
1938
+ /**
1939
+ * rounding helper
1940
+ * allows for quantized rounding
1941
+ * e.g 0.5 decimals s
1942
+ */
1685
1943
  function roundTo(num = 0, decimals = 3) {
1686
- if(decimals<=-1) return num;
1944
+ if (decimals < 0) return num;
1945
+ // Normal integer rounding
1687
1946
  if (!decimals) return Math.round(num);
1947
+
1948
+ // stepped rounding
1949
+ let intPart = Math.floor(decimals);
1950
+
1951
+ if (intPart !== decimals) {
1952
+ let f = +(decimals - intPart).toFixed(2);
1953
+ f = f > 0.5 ? (Math.floor((f) / 0.5) * 0.5) : f;
1954
+
1955
+ let step = 10 ** -intPart * f;
1956
+ return +(Math.round(num / step) * step).toFixed(8);
1957
+ }
1958
+
1688
1959
  let factor = 10 ** decimals;
1689
1960
  return Math.round(num * factor) / factor;
1690
1961
  }
@@ -1694,50 +1965,21 @@
1694
1965
  * floating point accuracy
1695
1966
  * based on numeric value
1696
1967
  */
1697
- function autoRound(val, integerThresh = 50){
1698
- let decimals=8;
1699
-
1700
- if(val>integerThresh*2){
1701
- decimals=0;
1702
- }
1703
- else if(val>integerThresh){
1704
- decimals=1;
1705
- }else {
1706
- decimals=Math.ceil(500/val).toString().length;
1707
-
1708
- }
1709
-
1710
-
1711
- let factor = 10 ** decimals;
1712
- return Math.round(val * factor) / factor;
1713
- }
1714
-
1715
- /**
1716
- * round path data
1717
- * either by explicit decimal value or
1718
- * based on suggested accuracy in path data
1719
- */
1720
- function roundPathData(pathData, decimalsGlobal = -1) {
1721
-
1722
- if (decimalsGlobal < 0) return pathData;
1723
-
1724
- let len = pathData.length;
1725
-
1726
- let decimals = decimalsGlobal;
1727
-
1728
- for (let c = 0; c < len; c++) {
1729
- let com = pathData[c];
1730
- let {values} = com;
1968
+ function autoRound(val, integerThresh = 50) {
1969
+ let decimals = 8;
1731
1970
 
1732
- let valLen = values.length;
1733
- if (!valLen) continue
1971
+ if (val > integerThresh * 2) {
1972
+ decimals = 0;
1973
+ }
1974
+ else if (val > integerThresh) {
1975
+ decimals = 1;
1976
+ } else {
1977
+ decimals = Math.ceil(500 / val).toString().length;
1734
1978
 
1735
- for (let v = 0; v < valLen; v++) {
1736
- pathData[c].values[v] = roundTo(values[v], decimals);
1737
- }
1738
1979
  }
1739
1980
 
1740
- return pathData;
1981
+ let factor = 10 ** decimals;
1982
+ return Math.round(val * factor) / factor;
1741
1983
  }
1742
1984
 
1743
1985
  /**
@@ -2179,7 +2421,7 @@
2179
2421
  scale = height / 100;
2180
2422
  }
2181
2423
  else {
2182
- scale = normalizedDiagonal ? scaleRoot / 100 : 1;
2424
+ scale = normalizedDiagonal ? scaleRoot / 100 : width / 100;
2183
2425
  }
2184
2426
  break;
2185
2427
 
@@ -2248,11 +2490,13 @@
2248
2490
  }
2249
2491
 
2250
2492
  function getElementAtts(el, {x=0, y=0, width=0, height=0}={}){
2251
- let attributes = [...el.attributes];
2493
+
2494
+ let attributes = [...el.attributes].map(att=>att.name);
2252
2495
 
2253
2496
  let atts={};
2254
2497
  attributes.forEach(att=>{
2255
- let value = normalizeUnits(att.nodeValue, {x, y, width, height});
2498
+
2499
+ let value = normalizeUnits(el.getAttribute(att), {x, y, width, height});
2256
2500
  atts[att.name] = value;
2257
2501
  });
2258
2502
 
@@ -3077,9 +3321,9 @@
3077
3321
  let comS = getExtrapolatedCommand(com1, com2, t);
3078
3322
 
3079
3323
  // test new point-at-t against original mid segment starting point
3080
- let pt = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
3324
+ let ptI = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
3081
3325
 
3082
- let dist0 = getDistManhattan(com1.p, pt);
3326
+ let dist0 = getDistManhattan(com1.p, ptI);
3083
3327
  let dist1 = 0, dist2 = 0;
3084
3328
  let close = dist0 < maxDist;
3085
3329
  let success = false;
@@ -3094,29 +3338,40 @@
3094
3338
  * to prevent distortions
3095
3339
  */
3096
3340
 
3097
- // 2nd segment mid
3098
- let pt_2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
3341
+ // 1st segment mid
3342
+ let ptM_seg1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
3099
3343
 
3100
- // simplified path
3101
- let t3 = (1 + t) * 0.5;
3102
- let ptS_2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
3103
- dist1 = getDistManhattan(pt_2, ptS_2);
3344
+ let t2 = t * 0.5;
3345
+ // combined interpolated mid point
3346
+ let ptI_seg1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
3347
+ dist1 = getDistManhattan(ptM_seg1, ptI_seg1);
3104
3348
 
3105
3349
  error += dist1;
3106
3350
 
3107
3351
  if (dist1 < maxDist) {
3108
3352
 
3109
- // 1st segment mid
3110
- let pt_1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
3353
+ // 2nd segment mid
3354
+ let ptM_seg2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
3111
3355
 
3112
- let t2 = t * 0.5;
3113
- let ptS_1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
3114
- dist2 = getDistManhattan(pt_1, ptS_1);
3356
+ // simplified path
3357
+ let t3 = (1 + t) * 0.5;
3358
+ let ptI_seg2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
3359
+ dist2 = getDistManhattan(ptM_seg2, ptI_seg2);
3115
3360
 
3116
3361
  error += dist2;
3117
3362
 
3118
3363
  if (error < maxDist) success = true;
3119
3364
 
3365
+ /*
3366
+ renderPoint(markers, ptM_seg1, 'cyan')
3367
+ renderPoint(markers, pt, 'orange', '1.5%', '1')
3368
+ renderPoint(markers, ptM_seg2, 'orange')
3369
+
3370
+ renderPoint(markers, com1.p, 'green')
3371
+
3372
+ renderPoint(markers, ptI_seg1, 'purple')
3373
+ */
3374
+
3120
3375
  }
3121
3376
 
3122
3377
  } // end 1st try
@@ -3281,6 +3536,7 @@
3281
3536
  let com = pathData[c - 1];
3282
3537
  let { type, values, p0, p, cp1 = null, cp2 = null, squareDist = 0, cptArea = 0, dimA = 0 } = com;
3283
3538
 
3539
+ let comPrev = pathData[c-2];
3284
3540
  let comN = pathData[c] || null;
3285
3541
 
3286
3542
  // init properties
@@ -3299,6 +3555,7 @@
3299
3555
 
3300
3556
  // bezier types
3301
3557
  let isBezier = type === 'Q' || type === 'C';
3558
+ let isArc = type === 'A';
3302
3559
  let isBezierN = comN && (comN.type === 'Q' || comN.type === 'C');
3303
3560
 
3304
3561
  /**
@@ -3345,6 +3602,22 @@
3345
3602
  }
3346
3603
  }
3347
3604
 
3605
+ // check extremes introduce by small arcs
3606
+ else if(isArc && comN && ((comPrev.type==='C' || comPrev.type==='Q') || (comN.type==='C' || comN.type==='Q')) ){
3607
+ let distN = comN ? comN.dimA : 0;
3608
+ let isShort = com.dimA < (comPrev.dimA + distN) * 0.1;
3609
+ let smallRadius = com.values[0] === com.values[1] && (com.values[0] < 1);
3610
+
3611
+ if(isShort && smallRadius){
3612
+ let bb = getPolyBBox([comPrev.p0, comN.p]);
3613
+ if(p.x>bb.right || p.x<bb.x || p.y<bb.y || p.y>bb.bottom){
3614
+ hasExtremes = true;
3615
+
3616
+ }
3617
+ }
3618
+
3619
+ }
3620
+
3348
3621
  if (hasExtremes) com.extreme = true;
3349
3622
 
3350
3623
  // Corners and semi extremes
@@ -3902,50 +4175,10 @@
3902
4175
  return pathData.map(com => { return `${com.type} ${com.values.join(' ')}` }).join(' ');
3903
4176
  }
3904
4177
 
3905
- function parsePathDataNormalized(d,
3906
- {
3907
- // necessary for most calculations
3908
- toAbsolute = true,
3909
- toLonghands = true,
3910
-
3911
- // not necessary unless you need cubics only
3912
- quadraticToCubic = false,
3913
-
3914
- // mostly a fallback if arc calculations fail
3915
- arcToCubic = false,
3916
- // arc to cubic precision - adds more segments for better precision
3917
- arcAccuracy = 4,
3918
- } = {}
3919
- ) {
3920
-
3921
- // is already array
3922
- let isArray = Array.isArray(d);
3923
-
3924
- // normalize native pathData to regular array
3925
- let hasConstructor = isArray && typeof d[0] === 'object' && typeof d[0].constructor === 'function';
3926
- /*
3927
- if (hasConstructor) {
3928
- d = d.map(com => { return { type: com.type, values: com.values } })
3929
- console.log('hasConstructor', hasConstructor, (typeof d[0].constructor), d);
3930
- }
3931
- */
3932
-
3933
- let pathDataObj = isArray ? d : parsePathDataString(d);
3934
-
3935
- let { hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true } = pathDataObj;
3936
- let pathData = hasConstructor ? pathDataObj : pathDataObj.pathData;
3937
-
3938
- // normalize
3939
- pathData = normalizePathData(pathData,
3940
- {
3941
- toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy,
3942
- hasRelatives, hasShorthands, hasQuadratics, hasArcs
3943
- },
3944
- );
3945
-
3946
- return pathData;
3947
- }
3948
-
4178
+ /**
4179
+ * wrapper function for
4180
+ * all path data conversion
4181
+ */
3949
4182
  function convertPathData(pathData, {
3950
4183
  toShorthands = true,
3951
4184
  toLonghands = false,
@@ -3997,22 +4230,24 @@
3997
4230
 
3998
4231
  if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
3999
4232
 
4000
- if(toMixed) toRelative = true;
4233
+ if (toMixed) toRelative = true;
4001
4234
 
4002
4235
  // pre round - before relative conversion to minimize distortions
4003
4236
  if (decimals > -1 && toRelative) pathData = roundPathData(pathData, decimals);
4004
4237
 
4005
4238
  // clone absolute pathdata
4006
- if(toMixed){
4239
+ if (toMixed) {
4007
4240
  pathDataAbs = JSON.parse(JSON.stringify(pathData));
4008
4241
  }
4009
4242
 
4010
4243
  if (toRelative) pathData = pathDataToRelative(pathData);
4244
+
4245
+ // final rounding
4011
4246
  if (decimals > -1) pathData = roundPathData(pathData, decimals);
4012
4247
 
4013
4248
  // choose most compact commands: relative or absolute
4014
- if(toMixed){
4015
- for(let i=0; i<pathData.length; i++){
4249
+ if (toMixed) {
4250
+ for (let i = 0; i < pathData.length; i++) {
4016
4251
  let com = pathData[i];
4017
4252
  let comA = pathDataAbs[i];
4018
4253
  // compare Lengths
@@ -4022,7 +4257,7 @@
4022
4257
  let lenR = comStr.length;
4023
4258
  let lenA = comStrA.length;
4024
4259
 
4025
- if(lenA<lenR){
4260
+ if (lenA < lenR) {
4026
4261
 
4027
4262
  pathData[i] = pathDataAbs[i];
4028
4263
  }
@@ -4032,56 +4267,140 @@
4032
4267
  return pathData
4033
4268
  }
4034
4269
 
4035
- /**
4036
- *
4037
- * @param {*} pathData
4038
- * @returns
4039
- */
4040
-
4041
- function optimizeArcPathData(pathData = []) {
4270
+ function parsePathDataNormalized(d,
4271
+ {
4272
+ // necessary for most calculations
4273
+ toAbsolute = true,
4274
+ toLonghands = true,
4042
4275
 
4043
- let remove =[];
4276
+ // not necessary unless you need cubics only
4277
+ quadraticToCubic = false,
4044
4278
 
4045
- pathData.forEach((com, i) => {
4046
- let { type, values } = com;
4047
- if (type === 'A') {
4048
- let [rx, ry, largeArc, x, y] = [values[0], values[1], values[3], values[5], values[6]];
4049
- let comPrev = pathData[i - 1];
4050
- let [x0, y0] = [comPrev.values[comPrev.values.length - 2], comPrev.values[comPrev.values.length - 1]];
4051
- let M = { x: x0, y: y0 };
4052
- let p = { x, y };
4279
+ // mostly a fallback if arc calculations fail
4280
+ arcToCubic = false,
4281
+ // arc to cubic precision - adds more segments for better precision
4282
+ arcAccuracy = 4,
4283
+ } = {}
4284
+ ) {
4053
4285
 
4054
- if(rx===0 || ry===0){
4055
- pathData[i]= null;
4056
- remove.push(i);
4286
+ // is already array
4287
+ let isArray = Array.isArray(d);
4057
4288
 
4058
- }
4289
+ // normalize native pathData to regular array
4290
+ let hasConstructor = isArray && typeof d[0] === 'object' && typeof d[0].constructor === 'function';
4291
+ /*
4292
+ if (hasConstructor) {
4293
+ d = d.map(com => { return { type: com.type, values: com.values } })
4294
+ console.log('hasConstructor', hasConstructor, (typeof d[0].constructor), d);
4295
+ }
4296
+ */
4059
4297
 
4060
- // rx and ry are large enough
4061
- if (rx >= 1 && (x === x0 || y === y0)) {
4062
- let diff = Math.abs(rx - ry) / rx;
4298
+ let pathDataObj = isArray ? d : parsePathDataString(d);
4063
4299
 
4064
- // rx~==ry
4065
- if (diff < 0.01) {
4300
+ let { hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true } = pathDataObj;
4301
+ let pathData = hasConstructor ? pathDataObj : pathDataObj.pathData;
4066
4302
 
4067
- // test radius against mid point
4068
- let pMid = interpolate(M, p, 0.5);
4069
- let distM = getDistance(pMid, M);
4070
- let rDiff = Math.abs(distM - rx) / rx;
4303
+ // normalize
4304
+ pathData = normalizePathData(pathData,
4305
+ {
4306
+ toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy,
4307
+ hasRelatives, hasShorthands, hasQuadratics, hasArcs
4308
+ },
4309
+ );
4071
4310
 
4072
- // half distance between mid and start point should be ~ equal
4073
- if(rDiff<0.01){
4074
- pathData[i].values[0] = 1;
4075
- pathData[i].values[1] = 1;
4076
- pathData[i].values[2] = 0;
4077
- }
4078
- }
4311
+ return pathData;
4312
+ }
4313
+
4314
+ /**
4315
+ *
4316
+ * @param {*} pathData
4317
+ * @returns
4318
+ */
4319
+
4320
+ function optimizeArcPathData(pathData = []) {
4321
+ let l = pathData.length;
4322
+ let pathDataN = [];
4323
+
4324
+ for (let i = 0; i < l; i++) {
4325
+ let com = pathData[i];
4326
+ let { type, values } = com;
4327
+
4328
+ if (type !== 'A') {
4329
+ pathDataN.push(com);
4330
+ continue
4331
+ }
4332
+
4333
+ let [rx, ry, largeArc, x, y] = [values[0], values[1], values[3], values[5], values[6]];
4334
+ let comPrev = pathData[i - 1];
4335
+ let [x0, y0] = [comPrev.values[comPrev.values.length - 2], comPrev.values[comPrev.values.length - 1]];
4336
+ let M = { x: x0, y: y0 };
4337
+ let p = { x, y };
4338
+
4339
+ if (rx === 0 || ry === 0) {
4340
+ pathData[i] = null;
4341
+ }
4342
+
4343
+ // test for elliptic
4344
+ let rat = rx / ry;
4345
+ let error = rx !== ry ? Math.abs(1 - rat) : 0;
4346
+
4347
+ if (error > 0.01) {
4348
+
4349
+ pathDataN.push(com);
4350
+ continue
4351
+
4352
+ }
4353
+
4354
+ // xAxis rotation is futile for circular arcs - reset
4355
+ com.values[2] = 0;
4356
+
4357
+ /**
4358
+ * test semi circles
4359
+ * rx and ry are large enough
4360
+ */
4361
+
4362
+ // 1. horizontal or vertical
4363
+ let thresh = getDistManhattan(M, p) * 0.001;
4364
+ let diffX = Math.abs(x - x0);
4365
+ let diffY = Math.abs(y - y0);
4366
+
4367
+ let isHorizontal = diffY < thresh;
4368
+ let isVertical = diffX < thresh;
4369
+
4370
+ // minify rx and ry
4371
+ if (isHorizontal || isVertical) {
4372
+
4373
+ // check if semi circle
4374
+ let needsTrueR = isHorizontal ? rx*1.9 > diffX : ry*1.9 > diffY;
4375
+
4376
+ // is semicircle we can simplify rx
4377
+ if (!needsTrueR) {
4378
+
4379
+ rx = rx >= 1 ? 1 : (rx > 0.5 ? 0.5 : rx);
4079
4380
  }
4381
+
4382
+ com.values[0] = rx;
4383
+ com.values[1] = rx;
4384
+ pathDataN.push(com);
4385
+ continue
4386
+
4080
4387
  }
4081
- });
4082
4388
 
4083
- if(remove.length) pathData = pathData.filter(Boolean);
4084
- return pathData;
4389
+ // 2. get true radius - if rx ~= diameter/distance we have a semicircle
4390
+ let r = getDistance(M, p) * 0.5;
4391
+ error = rx / r;
4392
+
4393
+ if (error < 0.5) {
4394
+ rx = r >= 1 ? 1 : (r > 0.5 ? 0.5 : r);
4395
+ }
4396
+
4397
+ com.values[0] = rx;
4398
+ com.values[1] = rx;
4399
+ pathDataN.push(com);
4400
+
4401
+ }
4402
+
4403
+ return pathDataN;
4085
4404
  }
4086
4405
 
4087
4406
  /**
@@ -4146,6 +4465,44 @@
4146
4465
  }
4147
4466
  */
4148
4467
 
4468
+ function convertSmallArcsToLinetos(pathData) {
4469
+
4470
+ let l = pathData.length;
4471
+
4472
+ // add fist command
4473
+ let pathDataN = [pathData[0]];
4474
+
4475
+ for (let i = 1; i < l; i++) {
4476
+ let com = pathData[i];
4477
+ let comPrev = pathData[i - 1];
4478
+ let comN = pathData[i + 1] || null;
4479
+
4480
+ if (!comN) {
4481
+ pathDataN.push(com);
4482
+ break
4483
+ }
4484
+
4485
+ let { type, values, extreme = false, p0, p, dimA = 0 } = com;
4486
+ // for short segment detection
4487
+ let dimAN = comN.dimA;
4488
+ let dimA0 = comPrev.dimA + dimA + dimAN;
4489
+ let thresh = 0.05;
4490
+ let isShort = dimA < dimA0 * thresh;
4491
+
4492
+ if (type === 'A' && isShort && values[0] < 1 && values[1] < 1) {
4493
+
4494
+ com.type = 'L';
4495
+ com.values = [p.x, p.y];
4496
+ }
4497
+
4498
+ pathDataN.push(com);
4499
+
4500
+ }
4501
+
4502
+ return pathDataN;
4503
+
4504
+ }
4505
+
4149
4506
  function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}, tolerance = 1) {
4150
4507
 
4151
4508
  // test if cubic can be simplified to quadratic
@@ -6201,7 +6558,7 @@
6201
6558
  let { type, values } = com;
6202
6559
  let valsLen = values.length;
6203
6560
  if (valsLen) {
6204
- let p = { type: type, x: values[valsLen-2], y: values[valsLen-1], index: 0};
6561
+ let p = { type: type, x: values[valsLen - 2], y: values[valsLen - 1], index: 0 };
6205
6562
  p.index = i;
6206
6563
  indices.push(p);
6207
6564
  }
@@ -6209,113 +6566,111 @@
6209
6566
 
6210
6567
  // reorder to top left most
6211
6568
 
6212
- indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x-b.x );
6569
+ indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x - b.x);
6213
6570
  newIndex = indices[0].index;
6214
6571
 
6215
- return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
6572
+ return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
6216
6573
  }
6217
6574
 
6218
- function optimizeClosePath(pathData, {removeFinalLineto = true, autoClose = true}={}) {
6575
+ function optimizeClosePath(pathData, { removeFinalLineto = true, autoClose = true } = {}) {
6219
6576
 
6220
- let pathDataNew = [];
6577
+ let pathDataN = pathData;
6221
6578
  let l = pathData.length;
6222
6579
  let M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
6223
6580
  let isClosed = pathData[l - 1].type.toLowerCase() === 'z';
6224
6581
 
6225
- let linetos = pathData.filter(com => com.type === 'L');
6226
-
6227
- // check if order is ideal
6228
- let idxPenultimate = isClosed ? l-2 : l-1;
6582
+ let hasLinetos = false;
6229
6583
 
6584
+ // check if path is closed by explicit lineto
6585
+ let idxPenultimate = isClosed ? l - 2 : l - 1;
6230
6586
  let penultimateCom = pathData[idxPenultimate];
6231
6587
  let penultimateType = penultimateCom.type;
6232
6588
  let penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
6233
6589
 
6234
6590
  // last L command ends at M
6235
- let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
6236
-
6237
- // add closepath Z to enable order optimizations
6238
- if(!isClosed && autoClose && isClosingCommand){
6591
+ let hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
6592
+ let lastIsLine = penultimateType === 'L';
6239
6593
 
6240
- /*
6241
- // adjust final coords
6242
- let valsLast = pathData[idxPenultimate].values
6243
- let valsLastLen = valsLast.length;
6244
- pathData[idxPenultimate].values[valsLastLen-2] = M.x
6245
- pathData[idxPenultimate].values[valsLastLen-1] = M.y
6246
- */
6247
-
6248
- pathData.push({type:'Z', values:[]});
6249
- isClosed = true;
6250
- l++;
6251
- }
6594
+ // create index
6595
+ let indices = [];
6596
+ for (let i = 0; i < l; i++) {
6597
+ let com = pathData[i];
6598
+ let { type, values, p0, p } = com;
6252
6599
 
6253
- // if last segment is not closing or a lineto
6254
- let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand || penultimateCom.type === 'L');
6255
- skipReorder = false;
6600
+ if(type==='L') hasLinetos = true;
6256
6601
 
6257
- // we can't change starting point for non closed paths
6258
- if (!isClosed) {
6259
- return pathData
6260
- }
6602
+ // exclude Z
6603
+ if (values.length) {
6604
+ values.slice(-2);
6261
6605
 
6262
- let newIndex = 0;
6606
+ let x = Math.min(p0.x, p.x);
6607
+ let y = Math.min(p0.y, p.y);
6263
6608
 
6264
- if (!skipReorder) {
6609
+ let prevCom = pathData[i - 1] ? pathData[i - 1] : pathData[idxPenultimate];
6610
+ let prevType = prevCom.type;
6265
6611
 
6266
- let indices = [];
6267
- for (let i = 0; i < l; i++) {
6268
- let com = pathData[i];
6269
- let { type, values } = com;
6270
- if (values.length) {
6271
- let valsL = values.slice(-2);
6272
- let prevL = pathData[i - 1] && pathData[i - 1].type === 'L';
6273
- let nextL = pathData[i + 1] && pathData[i + 1].type === 'L';
6274
- let prevCom = pathData[i - 1] ? pathData[i - 1].type.toUpperCase() : null;
6275
- let nextCom = pathData[i + 1] ? pathData[i + 1].type.toUpperCase() : null;
6276
- let p = { type: type, x: valsL[0], y: valsL[1], dist: 0, index: 0, prevL, nextL, prevCom, nextCom };
6277
- p.index = i;
6278
- indices.push(p);
6279
- }
6612
+ let item = { type: type, x, y, index: 0, prevType };
6613
+ item.index = i;
6614
+ indices.push(item);
6280
6615
  }
6281
6616
 
6282
- // find top most lineto
6617
+ }
6283
6618
 
6284
- if (linetos.length) {
6285
- let curveAfterLine = indices.filter(com => (com.type !== 'L' && com.type !== 'M') && com.prevCom &&
6286
- com.prevCom === 'L' || com.prevCom === 'M' && penultimateType === 'L').sort((a, b) => a.y - b.y || a.x - b.x)[0];
6619
+ let xMin = Infinity;
6620
+ let yMin = Infinity;
6621
+ let idx_top = null;
6622
+ let len = indices.length;
6287
6623
 
6288
- newIndex = curveAfterLine ? curveAfterLine.index - 1 : 0;
6624
+ for (let i = 0; i < len; i++) {
6625
+ let com = indices[i];
6626
+ let { type, index, x, y, prevType } = com;
6289
6627
 
6290
- }
6291
- // use top most command
6292
- else {
6293
- indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x - b.x);
6294
- newIndex = indices[0].index;
6295
- }
6628
+ if (hasLinetos && prevType === 'L') {
6629
+ if (x < xMin && y < yMin) {
6630
+ idx_top = index-1;
6631
+ }
6296
6632
 
6297
- // reorder
6298
- pathData = newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
6633
+ if (y < yMin) {
6634
+ yMin = y;
6635
+ }
6636
+
6637
+ if (x < xMin) {
6638
+ xMin = x;
6639
+ }
6640
+ }
6299
6641
  }
6300
6642
 
6301
- M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
6643
+ // shift to better starting point
6644
+ if (idx_top) {
6645
+ pathDataN = shiftSvgStartingPoint(pathDataN, idx_top);
6302
6646
 
6303
- l = pathData.length;
6647
+ // update penultimate - reorder might have added new close paths
6648
+ l = pathDataN.length;
6649
+ M = { x: +pathDataN[0].values[0].toFixed(8), y: +pathDataN[0].values[1].toFixed(8) };
6650
+
6651
+ idxPenultimate = isClosed ? l - 2 : l - 1;
6652
+ penultimateCom = pathDataN[idxPenultimate];
6653
+ penultimateType = penultimateCom.type;
6654
+ penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
6655
+ lastIsLine = penultimateType ==='L';
6304
6656
 
6305
- // remove last lineto
6306
- penultimateCom = pathData[l - 2];
6307
- penultimateType = penultimateCom.type;
6308
- penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8));
6657
+ // last L command ends at M
6658
+ hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
6309
6659
 
6310
- isClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
6660
+ }
6311
6661
 
6312
- if (removeFinalLineto && isClosingCommand) {
6313
- pathData.splice(l - 2, 1);
6662
+ // remove unnecessary closing lineto
6663
+ if (removeFinalLineto && hasClosingCommand && lastIsLine) {
6664
+ pathDataN.splice(l - 2, 1);
6314
6665
  }
6315
6666
 
6316
- pathDataNew.push(...pathData);
6667
+ // add close path
6668
+ if (autoClose && !isClosed && hasClosingCommand) {
6669
+ pathDataN.push({ type: 'Z', values: [] });
6670
+ }
6671
+
6672
+ return pathDataN
6317
6673
 
6318
- return pathDataNew
6319
6674
  }
6320
6675
 
6321
6676
  /**
@@ -6828,7 +7183,7 @@
6828
7183
 
6829
7184
  switch(type){
6830
7185
  case 'path':
6831
- let pathData = parsePathDataNormalized(atts.d);
7186
+ let pathData = parsePathDataNormalized(el.getAttribute('d'));
6832
7187
  bb=getPolyBBox(getPathDataPoly(pathData));
6833
7188
 
6834
7189
  break;
@@ -6870,8 +7225,8 @@
6870
7225
  autoRoundValues = false,
6871
7226
  minifyRgbColors = false,
6872
7227
  removeInvalid = true,
6873
- allowDataAtts=true,
6874
- allowAriaAtts=true,
7228
+ allowDataAtts = true,
7229
+ allowAriaAtts = true,
6875
7230
  removeDefaults = true,
6876
7231
  cleanUpStrokes = true,
6877
7232
  normalizeTransforms = true,
@@ -6937,7 +7292,7 @@
6937
7292
  */
6938
7293
 
6939
7294
  if (removeInvalid || removeDefaults || removeNameSpaced) {
6940
- let propsFilteredObj = filterSvgElProps(nodeName, props, {allowDataAtts, allowAriaAtts, removeIds, removeClassNames, removeDefaults, removeNameSpaced, exclude, cleanUpStrokes, include: [...transformsStandalone, ...include], cleanUpStrokes: false });
7295
+ let propsFilteredObj = filterSvgElProps(nodeName, props, { allowDataAtts, allowAriaAtts, removeIds, removeClassNames, removeDefaults, removeNameSpaced, exclude, cleanUpStrokes, include: [...transformsStandalone, ...include], cleanUpStrokes: false });
6941
7296
  props = propsFilteredObj.propsFiltered;
6942
7297
  remove.push(...propsFilteredObj.remove);
6943
7298
 
@@ -7030,10 +7385,10 @@
7030
7385
 
7031
7386
  if (prop !== 'transforms') {
7032
7387
 
7033
- if (cleanUpStrokes && (prop === 'stroke-dasharray' || prop === 'stroke-dashoffset')) {
7388
+ if ((prop === 'stroke-dasharray' || prop === 'stroke-dashoffset')) {
7034
7389
  normalizedDiagonal = true;
7035
7390
  for (let i = 0; i < values.length; i++) {
7036
- let val = normalizeUnits(values[i].value, { unit: values[i].unit, width, height, normalizedDiagonal, fontSize });
7391
+ let val = normalizeUnits(values[i].value, { unit: values[i].unit, width, height, normalizedDiagonal, fontSize, autoRoundValues });
7037
7392
  valsNew.push(val);
7038
7393
  }
7039
7394
  }
@@ -7075,14 +7430,12 @@
7075
7430
  if (prop === 'scale' && unit === '%') {
7076
7431
  valAbs = valAbs * 0.01;
7077
7432
  } else {
7078
- if (prop === 'r') normalizedDiagonal = true;
7433
+ if (prop === 'r' && width!==height) normalizedDiagonal = true;
7079
7434
  valAbs = normalizeUnits(val.value, { unit, width, height, isHorizontal, isVertical, normalizedDiagonal, fontSize });
7080
7435
 
7081
7436
  if (autoRoundValues && isNumeric) {
7082
7437
  valAbs = autoRound(valAbs);
7083
-
7084
7438
  }
7085
-
7086
7439
  }
7087
7440
  }
7088
7441
  valsNew.push(valAbs);
@@ -7283,6 +7636,7 @@
7283
7636
  removeIds = false,
7284
7637
  removeClassNames = false,
7285
7638
  exclude = [],
7639
+ inheritedProps = null,
7286
7640
  } = {}) {
7287
7641
  let propsFiltered = {};
7288
7642
  let remove = [];
@@ -7311,7 +7665,7 @@
7311
7665
  let isMeta = prop === 'title';
7312
7666
  let isAria = prop.startsWith('aria-');
7313
7667
 
7314
- if( (allowDataAtts && isDataAtt) || (allowAriaAtts && isAria) || (allowMeta && isMeta ) ) continue
7668
+ if ((allowDataAtts && isDataAtt) || (allowAriaAtts && isAria) || (allowMeta && isMeta)) continue
7315
7669
 
7316
7670
  // filter out defaults
7317
7671
  let isDefault = removeDefaults ?
@@ -7322,6 +7676,7 @@
7322
7676
 
7323
7677
  if (isDefault || isDataAtt || isMeta || isAria || isFutileStroke) isValid = false;
7324
7678
  if (include.includes(prop)) isValid = true;
7679
+ if (exclude.includes(prop)) isValid = false;
7325
7680
 
7326
7681
  if (isValid) {
7327
7682
  propsFiltered[prop] = props[prop];
@@ -7554,6 +7909,358 @@
7554
7909
  return "";
7555
7910
  }
7556
7911
 
7912
+ // Legendre Gauss weight and abscissa values
7913
+ const waArr_global = [];
7914
+
7915
+ function getLength(pts, {
7916
+ t = 1,
7917
+ waArr = []
7918
+ } = {}) {
7919
+
7920
+ const cubicBezierLength = (p0, cp1, cp2, p, t = 0, wa = []) => {
7921
+ if (t === 0) {
7922
+ return 0;
7923
+ }
7924
+
7925
+ t = t > 1 ? 1 : t < 0 ? 0 : t;
7926
+ let t2 = t / 2;
7927
+
7928
+ /**
7929
+ * set higher legendre gauss weight abscissae values
7930
+ * by more accurate weight/abscissae lookups
7931
+ * https://pomax.github.io/bezierinfo/legendre-gauss.html
7932
+ */
7933
+
7934
+ let sum = 0;
7935
+
7936
+ let x0 = p0.x, y0 = p0.y, cp1x = cp1.x, cp1y = cp1.y, cp2x = cp2.x, cp2y = cp2.y, px = p.x, py = p.y;
7937
+
7938
+ for (let i = 0, len = wa.length; i < len; i++) {
7939
+ // weight and abscissae
7940
+ let [w, a] = [wa[i][0], wa[i][1]];
7941
+ let ct1_t = t2 * a;
7942
+ let ct0 = -ct1_t + t2;
7943
+
7944
+ let xbase0 = base3(ct0, x0, cp1x, cp2x, px);
7945
+ let ybase0 = base3(ct0, y0, cp1y, cp2y, py);
7946
+
7947
+ let comb0 = xbase0 * xbase0 + ybase0 * ybase0;
7948
+
7949
+ sum += w * Math.sqrt(comb0);
7950
+
7951
+ }
7952
+ return t2 * sum;
7953
+ };
7954
+
7955
+ const quadraticBezierLength = (p0, cp1, p, t, checkFlat = false) => {
7956
+ if (t === 0) {
7957
+ return 0;
7958
+ }
7959
+ // is flat/linear – treat as line
7960
+ if (checkFlat) {
7961
+ let l1 = getDistance(p0, cp1) + getDistance(cp1, p);
7962
+ let l2 = getDistance(p0, p);
7963
+ if (l1 === l2) {
7964
+ return l2;
7965
+ }
7966
+ }
7967
+
7968
+ let a, b, c, d, e, e1, d1, v1x, v1y;
7969
+ v1x = cp1.x * 2;
7970
+ v1y = cp1.y * 2;
7971
+ d = p0.x - v1x + p.x;
7972
+ d1 = p0.y - v1y + p.y;
7973
+ e = v1x - 2 * p0.x;
7974
+ e1 = v1y - 2 * p0.y;
7975
+ a = 4 * (d * d + d1 * d1);
7976
+ b = 4 * (d * e + d1 * e1);
7977
+ c = e * e + e1 * e1;
7978
+
7979
+ const bt = b / (2 * a),
7980
+ ct = c / a,
7981
+ ut = t + bt,
7982
+
7983
+ k = ct - bt * bt;
7984
+
7985
+ return (
7986
+ (Math.sqrt(a) / 2) *
7987
+ (ut * Math.sqrt(ut * ut + k) -
7988
+ bt * Math.sqrt(bt * bt + k) +
7989
+ k *
7990
+ Math.log((ut + Math.sqrt(ut * ut + k)) / (bt + Math.sqrt(bt * bt + k))))
7991
+ );
7992
+ };
7993
+
7994
+ let length;
7995
+ if (pts.length === 4) {
7996
+ length = cubicBezierLength(pts[0], pts[1], pts[2], pts[3], t, waArr);
7997
+
7998
+ }
7999
+ else if (pts.length === 3) {
8000
+ length = quadraticBezierLength(pts[0], pts[1], pts[2], t);
8001
+ }
8002
+ else {
8003
+ length = getDistance(pts[0], pts[1]);
8004
+ }
8005
+
8006
+ return length;
8007
+ }
8008
+
8009
+ // LG weight/abscissae generator
8010
+ function getLegendreGaussValues(n, x1 = -1, x2 = 1) {
8011
+ console.log('add new LG', n);
8012
+
8013
+ let waArr = [];
8014
+ let z1, z, xm, xl, pp, p3, p2, p1;
8015
+ const m = (n + 1) >> 1;
8016
+ xm = 0.5 * (x2 + x1);
8017
+ xl = 0.5 * (x2 - x1);
8018
+
8019
+ for (let i = m - 1; i >= 0; i--) {
8020
+ z = Math.cos((Math.PI * (i + 0.75)) / (n + 0.5));
8021
+ do {
8022
+ p1 = 1;
8023
+ p2 = 0;
8024
+ for (let j = 0; j < n; j++) {
8025
+
8026
+ p3 = p2;
8027
+ p2 = p1;
8028
+ p1 = ((2 * j + 1) * z * p2 - j * p3) / (j + 1);
8029
+ }
8030
+
8031
+ pp = (n * (z * p1 - p2)) / (z * z - 1);
8032
+ z1 = z;
8033
+ z = z1 - p1 / pp; //Newton’s method
8034
+
8035
+ } while (Math.abs(z - z1) > 1.0e-14);
8036
+
8037
+ let weight = (2 * xl) / ((1 - z * z) * pp * pp);
8038
+ let abscissa = xm + xl * z;
8039
+
8040
+ waArr.push(
8041
+ [weight, -abscissa],
8042
+ [weight, abscissa],
8043
+ );
8044
+ }
8045
+
8046
+ return waArr;
8047
+ }
8048
+
8049
+ function base3(t, p1, p2, p3, p4) {
8050
+ let t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4,
8051
+ t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
8052
+ return t * t2 - 3 * p1 + 3 * p2;
8053
+ }
8054
+
8055
+ function getPolygonLength(pts=[], isPoly=false){
8056
+
8057
+ let len = 0;
8058
+ let l=pts.length;
8059
+
8060
+ for(let i=1; i<l; i++){
8061
+ let p1 = pts[i-1];
8062
+ let p2 = pts[i];
8063
+ len += getDistance(p1, p2);
8064
+ }
8065
+ if(isPoly){
8066
+ len += getDistance(pts[l-1], pts[0]);
8067
+ }
8068
+ return len
8069
+ }
8070
+
8071
+ /**
8072
+ * Ramanujan approximation
8073
+ * based on: https://www.mathsisfun.com/geometry/ellipse-perimeter.html#tool
8074
+ */
8075
+ function getEllipseLength(rx=0, ry=0) {
8076
+ // is circle
8077
+ if (rx === ry) {
8078
+
8079
+ return 2 * Math.PI * rx;
8080
+ }
8081
+
8082
+ let c=rx+ry;
8083
+ let d = (rx - ry) / c;
8084
+ let h = d*d;
8085
+
8086
+ let totalLength = Math.PI * c * (1 + 3 * h / (10 + Math.sqrt(4 - 3 * h) ));
8087
+ return totalLength;
8088
+ }
8089
+
8090
+ /**
8091
+ * ellipse helpers
8092
+ * approximate ellipse length
8093
+ * by Legendre-Gauss
8094
+ */
8095
+
8096
+ function getCircleArcLength(r = 0, deltaAngle = 0) {
8097
+ if(r===0) {
8098
+ console.warn('Radius must be positive');
8099
+ return 0;
8100
+ }
8101
+ let len = 2 * Math.PI * r * (1 / 360 * Math.abs(deltaAngle * 180 / Math.PI));
8102
+ return len
8103
+ }
8104
+
8105
+ function getEllipseLengthLG(rx, ry, startAngle, endAngle, wa = []) {
8106
+
8107
+ // Transform [-1, 1] interval to [startAngle, endAngle]
8108
+ let halfInterval = (endAngle - startAngle) * 0.5;
8109
+ let midpoint = (endAngle + startAngle) * 0.5;
8110
+
8111
+ // Arc length integral approximation
8112
+ let arcLength = 0;
8113
+ for (let i = 0; i < wa.length; i++) {
8114
+ let [weight, abscissae] = wa[i];
8115
+ let theta = midpoint + halfInterval * abscissae;
8116
+
8117
+ let a = rx * Math.sin(theta);
8118
+ let b = ry * Math.cos(theta);
8119
+ let integrand = Math.sqrt(
8120
+ a * a + b * b
8121
+ );
8122
+ arcLength += weight * integrand;
8123
+ }
8124
+
8125
+ return Math.abs(halfInterval * arcLength)
8126
+ }
8127
+
8128
+ function getPathDataLength(pathData = []) {
8129
+ let len = 0;
8130
+ let pathDataArr = splitSubpaths(pathData);
8131
+
8132
+ for (let i = 0; i < pathDataArr.length; i++) {
8133
+ let pathData = pathDataArr[i];
8134
+
8135
+ // add verbose point data if not present
8136
+ if (pathData[0].p === undefined) pathData = getPathDataVerbose(pathData);
8137
+
8138
+ // Calculate Legendre Gauss weight and abscissa values
8139
+ if (!waArr_global.length) {
8140
+
8141
+ let waArr = getLegendreGaussValues(48);
8142
+ waArr.forEach(wa => {
8143
+ waArr_global.push(wa);
8144
+ });
8145
+ }
8146
+
8147
+ let waArr = waArr_global;
8148
+
8149
+ pathData.forEach(com => {
8150
+ let { type, values, p0, p, cp1 = null, cp2 = null } = com;
8151
+ let pts = [p0];
8152
+ if (type === 'C' || type === 'Q') pts.push(cp1);
8153
+ if (type === 'C') pts.push(cp2);
8154
+ pts.push(p);
8155
+ let comLen = 0;
8156
+
8157
+ if (type === 'A') {
8158
+
8159
+ // get parametrized arc properties
8160
+ let [largeArc, sweep] = [com.values[3], com.values[4]];
8161
+ let arcData = svgArcToCenterParam(p0.x, p0.y, com.values[0], com.values[1], com.values[2], largeArc, sweep, p.x, p.y, false);
8162
+ let { cx, cy, rx, ry, startAngle, endAngle, deltaAngle, xAxisRotation } = arcData;
8163
+
8164
+ if (rx === ry) {
8165
+ comLen = getCircleArcLength(rx, Math.abs(deltaAngle));
8166
+ }
8167
+
8168
+ // is ellipse
8169
+ else {
8170
+ xAxisRotation = xAxisRotation * deg2rad;
8171
+ startAngle = toParametricAngle((startAngle - xAxisRotation), rx, ry);
8172
+ endAngle = toParametricAngle((endAngle - xAxisRotation), rx, ry);
8173
+
8174
+ // recalculate parametrized delta
8175
+ let deltaAngle_param = endAngle - startAngle;
8176
+
8177
+ let signChange = deltaAngle > 0 && deltaAngle_param < 0 || deltaAngle < 0 && deltaAngle_param > 0;
8178
+
8179
+ deltaAngle = signChange ? deltaAngle : deltaAngle_param;
8180
+
8181
+ // adjust end angle
8182
+ if (sweep && startAngle > endAngle) {
8183
+ endAngle += Math.PI * 2;
8184
+ }
8185
+
8186
+ if (!sweep && startAngle < endAngle) {
8187
+ endAngle -= Math.PI * 2;
8188
+ }
8189
+ comLen = getEllipseLengthLG(rx, ry, startAngle, endAngle, waArr);
8190
+ }
8191
+ }
8192
+
8193
+ else {
8194
+ comLen = getLength(pts, {
8195
+ t: 1,
8196
+ waArr
8197
+ });
8198
+ }
8199
+ len += comLen;
8200
+ });
8201
+ }
8202
+
8203
+ return len;
8204
+ }
8205
+
8206
+ function getElementLength(el, {
8207
+ props = {},
8208
+ pathLength = 0,
8209
+ } = {}) {
8210
+
8211
+ let nodeName = el.nodeName;
8212
+ let len = 0;
8213
+
8214
+ props = JSON.parse(JSON.stringify(props));
8215
+
8216
+ for (let prop in props) {
8217
+ if (props[prop] && props[prop].length && props[prop].length === 1) {
8218
+ props[prop] = props[prop][0];
8219
+
8220
+ }
8221
+ }
8222
+
8223
+ let { x = 0, y = 0, x1 = 0, y1 = 0, x2 = 0, y2 = 0, width = 0, height = 0, r = 0, rx = 0, ry = 0, cx = 0, cy = 0 } = props;
8224
+
8225
+ let pts = nodeName === 'polygon' || nodeName === 'polyline' ? el.getAttribute('points') : [];
8226
+ let isPolygon = nodeName === 'polygon';
8227
+ if (pts.length) {
8228
+ pts = normalizePoly(pts);
8229
+ }
8230
+
8231
+ // we need to convert rects with corner rounding
8232
+ let pathData = [];
8233
+ if (nodeName === 'rect' && (rx || ry)) {
8234
+ pathData = rectToPathData(x, y, width, height, rx, ry);
8235
+ nodeName = 'path';
8236
+ }
8237
+
8238
+ switch (nodeName) {
8239
+ case 'line':
8240
+ len = getDistance({ x: x1, y: y1 }, { x: x2, y: y2 });
8241
+ break;
8242
+ case 'rect':
8243
+ len = width * 2 + height * 2;
8244
+ break;
8245
+ case 'circle':
8246
+ len = 2 * Math.PI * r;
8247
+ break;
8248
+ case 'ellipse':
8249
+ len = getEllipseLength(rx, ry);
8250
+ break;
8251
+ case 'polygon':
8252
+ case 'polyline':
8253
+ len = getPolygonLength(pts, isPolygon);
8254
+ break;
8255
+ case 'path':
8256
+ pathData = pathData.length ? pathData : parsePathDataNormalized(el.getAttribute('d'));
8257
+ len = getPathDataLength(pathData);
8258
+ break;
8259
+ }
8260
+
8261
+ return len
8262
+ }
8263
+
7557
8264
  function removeHiddenSvgEls(svg) {
7558
8265
  let els = svg.querySelectorAll('*');
7559
8266
  els.forEach(el => {
@@ -7602,8 +8309,12 @@
7602
8309
  */
7603
8310
 
7604
8311
  function removeSvgAtts(svg, remove = []) {
8312
+ removeAtts(svg, remove);
8313
+ }
8314
+
8315
+ function removeAtts(el, remove = []) {
7605
8316
  remove.forEach(att => {
7606
- svg.removeAttribute(att);
8317
+ el.removeAttribute(att);
7607
8318
  });
7608
8319
  }
7609
8320
 
@@ -7701,22 +8412,107 @@
7701
8412
  }
7702
8413
  */
7703
8414
 
8415
+ function setNormalizedTransformsToEl(el, {
8416
+ styleProps = {},
8417
+ } = {}) {
8418
+ let { remove, matrix, transComponents } = styleProps;
8419
+ let name = el.nodeName.toLowerCase();
8420
+
8421
+ if(!matrix) return styleProps;
8422
+
8423
+ let { rotate, scaleX, scaleY, skewX, translateX, translateY } = transComponents;
8424
+
8425
+ // scale attributes instead of transform
8426
+ let hasRot = rotate !== 0 || skewX !== 0;
8427
+ let unProportional = scaleX !== scaleY;
8428
+ let scalableByAtt = ['circle', 'ellipse', 'rect'];
8429
+
8430
+ let needsTrans = (hasRot) || unProportional;
8431
+ needsTrans = true;
8432
+
8433
+ if (!needsTrans && scalableByAtt.includes(name)) {
8434
+
8435
+ if (name === 'circle' || name === 'ellipse') {
8436
+ styleProps.cx[0] = [styleProps.cx[0] * scaleX + translateX];
8437
+ styleProps.cy[0] = [styleProps.cy[0] * scaleX + translateY];
8438
+
8439
+ if (styleProps.r) styleProps.r[0] = [styleProps.r[0] * scaleX];
8440
+ if (styleProps.rx) styleProps.rx[0] = [styleProps.rx[0] * scaleX];
8441
+ if (styleProps.ry) styleProps.ry[0] = [styleProps.ry[0] * scaleX];
8442
+
8443
+ }
8444
+ else if (name === 'rect') {
8445
+ let x = styleProps.x ? styleProps.x[0] + translateX : translateX;
8446
+ let y = styleProps.y ? styleProps.y[0] + translateY : translateY;
8447
+
8448
+ let rx = styleProps.rx ? styleProps.rx[0] * scaleX : 0;
8449
+ let ry = styleProps.ry ? styleProps.ry[0] * scaleY : 0;
8450
+
8451
+ styleProps.x = [x];
8452
+ styleProps.y = [y];
8453
+
8454
+ styleProps.rx = [rx];
8455
+ styleProps.ry = [ry];
8456
+
8457
+ styleProps.width = [styleProps.width[0] * scaleX];
8458
+ styleProps.height = [styleProps.height[0] * scaleX];
8459
+ }
8460
+
8461
+ // remove now obsolete transform properties
8462
+ delete styleProps.matrix;
8463
+ delete styleProps.transformArr;
8464
+ delete styleProps.transComponents;
8465
+
8466
+ // mark transform attribute for removal
8467
+ styleProps.remove.push('transform');
8468
+
8469
+ // scale props like stroke width or dash-array
8470
+ styleProps = scaleProps(styleProps, { props: ['stroke-width', 'stroke-dasharray'], scale: scaleX });
8471
+
8472
+ } else {
8473
+ el.setAttribute('transform', transComponents.matrixAtt);
8474
+
8475
+ }
8476
+
8477
+ return styleProps
8478
+
8479
+ }
8480
+
8481
+ function scaleProps(styleProps = {}, { props = [], scale = 1 } = {}, round = true) {
8482
+ if (scale === 1 || !props.length) return props;
8483
+
8484
+ for (let i = 0; i < props.length; i++) {
8485
+ let prop = props[i];
8486
+
8487
+ if (styleProps[prop] !== undefined) {
8488
+ styleProps[prop] = styleProps[prop].map(val => round ? roundTo(val * scale, 2) : val * scale);
8489
+ }
8490
+ }
8491
+ return styleProps
8492
+ }
8493
+
7704
8494
  function convertPathLengthAtt(el, {
7705
8495
  styleProps = {}
7706
- }={}) {
8496
+ } = {}) {
7707
8497
 
7708
- let pathLength = el.getAttribute('pathLength') ? +el.getAttribute('pathLength') : 0;
8498
+ let pathLength = styleProps['pathLength'];
7709
8499
 
7710
- if (pathLength && (styleProps['stroke-dasharray'] || styleProps['stroke-dashoffset'])) {
7711
- let elLength = getElementLength(el, {
7712
- pathLength,
7713
- props: styleProps
7714
- });
8500
+ if (pathLength) {
8501
+
8502
+ if ((styleProps['stroke-dasharray'] || styleProps['stroke-dashoffset'])) {
8503
+ let elLength = getElementLength(el, {
8504
+ pathLength,
8505
+ props: styleProps
8506
+ });
8507
+
8508
+ let scale = elLength / pathLength;
8509
+ styleProps = scaleProps(styleProps, { props: ['stroke-dasharray', 'stroke-dashoffset'], scale });
7715
8510
 
7716
- let scale = elLength / pathLength;
8511
+ // set absolute
8512
+ if (styleProps['stroke-dasharray']) el.setAttribute('stroke-dasharray', styleProps['stroke-dasharray'].join(' '));
8513
+ if (styleProps['stroke-dashoffset']) el.setAttribute('stroke-dashoffset', styleProps['stroke-dashoffset'][0]);
7717
8514
 
7718
- styleProps = scaleProps(styleProps, { props: ['stroke-dasharray', 'stroke-dashoffset'], scale });
7719
- [styleProps['stroke-dasharray'], styleProps['stroke-dashoffset']];
8515
+ }
7720
8516
 
7721
8517
  // tag for removal
7722
8518
  delete styleProps['pathLength'];
@@ -8051,85 +8847,6 @@
8051
8847
  return props
8052
8848
  }
8053
8849
 
8054
- function setNormalizedTransformsToEl(el, {
8055
- styleProps = {},
8056
- } = {}) {
8057
- let { remove, matrix, transComponents } = styleProps;
8058
- let name = el.nodeName.toLowerCase();
8059
-
8060
- if(!matrix) return styleProps;
8061
-
8062
- let { rotate, scaleX, scaleY, skewX, translateX, translateY } = transComponents;
8063
-
8064
- // scale attributes instead of transform
8065
- let hasRot = rotate !== 0 || skewX !== 0;
8066
- let unProportional = scaleX !== scaleY;
8067
- let scalableByAtt = ['circle', 'ellipse', 'rect'];
8068
-
8069
- let needsTrans = (hasRot) || unProportional;
8070
- needsTrans = true;
8071
-
8072
- if (!needsTrans && scalableByAtt.includes(name)) {
8073
-
8074
- if (name === 'circle' || name === 'ellipse') {
8075
- styleProps.cx[0] = [styleProps.cx[0] * scaleX + translateX];
8076
- styleProps.cy[0] = [styleProps.cy[0] * scaleX + translateY];
8077
-
8078
- if (styleProps.r) styleProps.r[0] = [styleProps.r[0] * scaleX];
8079
- if (styleProps.rx) styleProps.rx[0] = [styleProps.rx[0] * scaleX];
8080
- if (styleProps.ry) styleProps.ry[0] = [styleProps.ry[0] * scaleX];
8081
-
8082
- }
8083
- else if (name === 'rect') {
8084
- let x = styleProps.x ? styleProps.x[0] + translateX : translateX;
8085
- let y = styleProps.y ? styleProps.y[0] + translateY : translateY;
8086
-
8087
- let rx = styleProps.rx ? styleProps.rx[0] * scaleX : 0;
8088
- let ry = styleProps.ry ? styleProps.ry[0] * scaleY : 0;
8089
-
8090
- styleProps.x = [x];
8091
- styleProps.y = [y];
8092
-
8093
- styleProps.rx = [rx];
8094
- styleProps.ry = [ry];
8095
-
8096
- styleProps.width = [styleProps.width[0] * scaleX];
8097
- styleProps.height = [styleProps.height[0] * scaleX];
8098
- }
8099
-
8100
- // remove now obsolete transform properties
8101
- delete styleProps.matrix;
8102
- delete styleProps.transformArr;
8103
- delete styleProps.transComponents;
8104
-
8105
- // mark transform attribute for removal
8106
- styleProps.remove.push('transform');
8107
-
8108
- // scale props like stroke width or dash-array
8109
- styleProps = scaleProps$1(styleProps, { props: ['stroke-width', 'stroke-dasharray'], scale: scaleX });
8110
-
8111
- } else {
8112
- el.setAttribute('transform', transComponents.matrixAtt);
8113
-
8114
- }
8115
-
8116
- return styleProps
8117
-
8118
- }
8119
-
8120
- function scaleProps$1(styleProps = {}, { props = [], scale = 1 } = {}, round = true) {
8121
- if (scale === 1 || !props.length) return props;
8122
-
8123
- for (let i = 0; i < props.length; i++) {
8124
- let prop = props[i];
8125
-
8126
- if (styleProps[prop] !== undefined) {
8127
- styleProps[prop] = styleProps[prop].map(val => round ? roundTo(val * scale, 2) : val * scale);
8128
- }
8129
- }
8130
- return styleProps
8131
- }
8132
-
8133
8850
  function cleanUpSVG(svgMarkup, {
8134
8851
  removeHidden = true,
8135
8852
 
@@ -8159,7 +8876,10 @@
8159
8876
  cleanupSVGAtts = true,
8160
8877
  removeNameSpaced = true,
8161
8878
  removeNameSpacedAtts = true,
8879
+
8880
+ // unit conversions
8162
8881
  convertPathLength = false,
8882
+ toAbsoluteUnits = false,
8163
8883
 
8164
8884
  // meta
8165
8885
  allowMeta = false,
@@ -8184,10 +8904,10 @@
8184
8904
  } = {}) {
8185
8905
 
8186
8906
  // resolve dependencies
8187
- if (unGroup || convertTransforms || minifyRgbColors || attributesToGroup)
8188
- stylesToAttributes = true;
8907
+ if (unGroup || convertTransforms || minifyRgbColors || attributesToGroup)
8908
+ stylesToAttributes = true;
8189
8909
 
8190
- if(stylesToAttributes) cleanUpStrokes = true;
8910
+ if (stylesToAttributes) cleanUpStrokes = true;
8191
8911
 
8192
8912
  // replace namespaced refs
8193
8913
  if (fixHref) svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
@@ -8232,7 +8952,7 @@
8232
8952
  removeClassNames,
8233
8953
  minifyRgbColors,
8234
8954
  stylesheetProps: {},
8235
- exclude:[]
8955
+ exclude: []
8236
8956
  };
8237
8957
 
8238
8958
  // root svg inline style properties
@@ -8349,9 +9069,14 @@
8349
9069
  if (stylePropsSVG['class']) delete stylePropsSVG['class'];
8350
9070
  if (stylePropsSVG['id']) delete stylePropsSVG['id'];
8351
9071
 
9072
+ // add svg props
9073
+ inheritedProps = {
9074
+ ...stylePropsSVG,
9075
+ ...inheritedProps,
9076
+ };
9077
+
8352
9078
  // merge with svg props
8353
9079
  styleProps = {
8354
- ...stylePropsSVG,
8355
9080
  ...inheritedProps,
8356
9081
  ...styleProps
8357
9082
  };
@@ -8393,6 +9118,40 @@
8393
9118
  // general cleanup
8394
9119
  if (cleanupSVGAtts) cleanupSVGAttributes(svg, { removeIds, removeClassNames, removeDimensions, stylesToAttributes, allowMeta, allowAriaAtts, allowDataAtts });
8395
9120
 
9121
+ // all relative units to absolute
9122
+ if (toAbsoluteUnits) {
9123
+ normalizeTransforms = true;
9124
+
9125
+ /**
9126
+ * apply consolidated
9127
+ * element attributes
9128
+ * remove non-supported element props
9129
+ */
9130
+ stylePropsFiltered = filterSvgElProps(name, styleProps,
9131
+ { removeDefaults: true, cleanUpStrokes, allowMeta, allowAriaAtts, allowDataAtts, removeIds, inheritedProps });
9132
+
9133
+ for (let prop in stylePropsFiltered.propsFiltered) {
9134
+ let values = styleProps[prop];
9135
+ let val = values.length ? values.join(' ') : values[0];
9136
+ el.setAttribute(prop, val);
9137
+ }
9138
+
9139
+ let removeAttsEl = [...new Set([...remove, ...stylePropsFiltered.remove])];
9140
+
9141
+ // check if same value is in inherited
9142
+ for (let prop in stylePropsFiltered.propsFiltered) {
9143
+ let valInh = inheritedProps[prop] || [];
9144
+ let val = stylePropsFiltered.propsFiltered[prop] || [];
9145
+ if (valInh.join() === val.join()) {
9146
+ removeAttsEl.push(prop);
9147
+ }
9148
+ }
9149
+
9150
+ // remove obsolete/inherited
9151
+ removeAtts(el, removeAttsEl);
9152
+
9153
+ }
9154
+
8396
9155
  if (stylesToAttributes) {
8397
9156
 
8398
9157
  /**
@@ -8411,7 +9170,7 @@
8411
9170
  * remove non-supported element props
8412
9171
  */
8413
9172
  stylePropsFiltered = filterSvgElProps(name, styleProps,
8414
- { removeDefaults: true, cleanUpStrokes, allowMeta, allowAriaAtts, allowDataAtts, removeIds });
9173
+ { removeDefaults: true, cleanUpStrokes, allowMeta, allowAriaAtts, allowDataAtts, removeIds, inheritedProps });
8415
9174
 
8416
9175
  remove = [...new Set([...remove, ...stylePropsFiltered.remove])];
8417
9176
 
@@ -8425,12 +9184,14 @@
8425
9184
  * remove obsolete
8426
9185
  * attributes
8427
9186
  */
9187
+ removeAtts(el, remove);
8428
9188
 
9189
+ /*
8429
9190
  for (let i = 0; i < remove.length; i++) {
8430
9191
  let att = remove[i];
8431
-
8432
- el.removeAttribute(att);
9192
+ el.removeAttribute(att)
8433
9193
  }
9194
+ */
8434
9195
 
8435
9196
  } // endof style processing
8436
9197
 
@@ -8515,15 +9276,15 @@
8515
9276
  let values = stylePropsFiltered[prop];
8516
9277
  let val = values.length ? values.join(' ') : values[0];
8517
9278
 
8518
- if(prop!=='class' && prop!=='id'){
9279
+ if (prop !== 'class' && prop !== 'id') {
8519
9280
 
8520
9281
  let propShort = toShortStr(prop);
8521
9282
  let valShort = toShortStr(val);
8522
9283
  let propStr = `${propShort}-${valShort}`;
8523
-
9284
+
8524
9285
  // store in node property
8525
9286
  if (!el.styleSet) el.styleSet = new Set();
8526
- if(propStr) el.styleSet.add(propStr);
9287
+ if (propStr) el.styleSet.add(propStr);
8527
9288
  }
8528
9289
  }
8529
9290
 
@@ -8572,7 +9333,7 @@
8572
9333
  }
8573
9334
 
8574
9335
  function removeEmptyClassAtts(svg) {
8575
- let emptyClassEls = svg.querySelectorAll('[class=""');
9336
+ let emptyClassEls = svg.querySelectorAll('[class=""]');
8576
9337
  emptyClassEls.forEach(el => {
8577
9338
  el.removeAttribute('class');
8578
9339
  });
@@ -8585,7 +9346,7 @@
8585
9346
 
8586
9347
  let els = svg.querySelectorAll(renderedEls.join(', '));
8587
9348
  let len = els.length;
8588
- if(len===1) return;
9349
+ if (len === 1) return;
8589
9350
 
8590
9351
  let el0 = els[0] || null;
8591
9352
  let stylePrev = el0.styleSet !== undefined ? [...el0.styleSet].join('_') : '';
@@ -8661,7 +9422,7 @@
8661
9422
  if (children.length === 1) continue
8662
9423
 
8663
9424
  // create new group
8664
- if (!groupEl || groups.length>1) {
9425
+ if (!groupEl || groups.length > 1) {
8665
9426
 
8666
9427
  groupEl = document.createElementNS(svgNs, 'g');
8667
9428
  child0.parentNode.insertBefore(groupEl, child0);
@@ -8743,7 +9504,9 @@
8743
9504
  bb0.bottom = y + height;
8744
9505
 
8745
9506
  els.forEach(el => {
9507
+
8746
9508
  let bb = getElBBox(el);
9509
+
8747
9510
  let outside = bb.right < bb0.x || bb.bottom < bb0.y || bb.x > bb0.right || bb.y > bb.bottom;
8748
9511
  if (outside) el.remove();
8749
9512
  });
@@ -8852,8 +9615,130 @@
8852
9615
  });
8853
9616
  }
8854
9617
 
9618
+ function getArcFromPoly(pts, precise = false) {
9619
+ if (pts.length < 3) return false
9620
+
9621
+ // Pick 3 well-spaced points
9622
+ let len = pts.length;
9623
+ let idx1 = Math.floor(len * 0.333);
9624
+ let idx2 = Math.floor(len * 0.666);
9625
+ let idx3 = Math.floor(len * 0.5);
9626
+
9627
+ let p1 = pts[0];
9628
+ let p2 = pts[idx3];
9629
+ let p3 = pts[len - 1];
9630
+
9631
+ // Radius (use start point)
9632
+ let pts1 = [p1, p2, p3];
9633
+ let centroid = getPolyArcCentroid(pts1);
9634
+
9635
+ let r = 0, deltaAngle = 0, startAngle = 0, endAngle = 0, angleData = {};
9636
+
9637
+ // check if radii are consistent
9638
+ if (precise) {
9639
+
9640
+ /**
9641
+ * check multiple centroids
9642
+ * if the polyline can be expressed as
9643
+ * an arc - all centroids should be close
9644
+ */
9645
+
9646
+ if (len > 3) {
9647
+ let centroid1 = getPolyArcCentroid([p1, pts[idx1], p3]);
9648
+ let centroid2 = getPolyArcCentroid([p1, pts[idx2], p3]);
9649
+
9650
+ if (!centroid1 || !centroid2) return false;
9651
+
9652
+ let dist0 = getDistManhattan(centroid, p2);
9653
+ let dist1 = getDistManhattan(centroid, centroid1);
9654
+ let dist2 = getDistManhattan(centroid, centroid2);
9655
+ let errorCentroid = (dist1 + dist2);
9656
+
9657
+ // centroids diverging too much
9658
+ if (errorCentroid > dist0 * 0.05) {
9659
+
9660
+ return false
9661
+ }
9662
+
9663
+ }
9664
+
9665
+ let rSqMid = getSquareDistance(centroid, p2);
9666
+
9667
+ for (let i = 0; i < len; i++) {
9668
+ let pt = pts[i];
9669
+ let rSq = getSquareDistance(centroid, pt);
9670
+ let error = Math.abs(rSqMid - rSq) / rSqMid;
9671
+
9672
+ if (error > 0.0025) {
9673
+ /*
9674
+ console.log('error', error, len, idx1, idx2, idx3);
9675
+ renderPoint(markers, centroid, 'orange')
9676
+ renderPoint(markers, p1, 'green')
9677
+ renderPoint(markers, p2)
9678
+ renderPoint(markers, p3, 'purple')
9679
+ */
9680
+ return false;
9681
+ }
9682
+ }
9683
+
9684
+ // calculate proper radius
9685
+ r = Math.sqrt(rSqMid);
9686
+ angleData = getDeltaAngle(centroid, p1, p3);
9687
+ ({ deltaAngle, startAngle, endAngle } = angleData);
9688
+
9689
+ } else {
9690
+ r = getDistance(centroid, p1);
9691
+ angleData = getDeltaAngle(centroid, p1, p3);
9692
+ ({ deltaAngle, startAngle, endAngle } = angleData);
9693
+ }
9694
+
9695
+ return {
9696
+ centroid,
9697
+ r,
9698
+ startAngle,
9699
+ endAngle,
9700
+ deltaAngle
9701
+ };
9702
+ }
9703
+
9704
+ function getPolyArcCentroid(pts = []) {
9705
+
9706
+ pts = pts.filter(pt => pt !== undefined);
9707
+ if (pts.length < 3) return false
9708
+
9709
+ let p1 = pts[0];
9710
+ let p2 = pts[Math.floor(pts.length / 2)];
9711
+ let p3 = pts[pts.length - 1];
9712
+
9713
+ let x1 = p1.x, y1 = p1.y;
9714
+ let x2 = p2.x, y2 = p2.y;
9715
+ let x3 = p3.x, y3 = p3.y;
9716
+
9717
+ let a = x1 - x2;
9718
+ let b = y1 - y2;
9719
+ let c = x1 - x3;
9720
+ let d = y1 - y3;
9721
+
9722
+ let e = ((x1 * x1 - x2 * x2) + (y1 * y1 - y2 * y2)) / 2;
9723
+ let f = ((x1 * x1 - x3 * x3) + (y1 * y1 - y3 * y3)) / 2;
9724
+
9725
+ let det = a * d - b * c;
9726
+
9727
+ // colinear points
9728
+ if (Math.abs(det) < 1e-10) {
9729
+ return false;
9730
+ }
9731
+
9732
+ // find center of arc
9733
+ let cx = (d * e - b * f) / det;
9734
+ let cy = (-c * e + a * f) / det;
9735
+ let centroid = { x: cx, y: cy };
9736
+ return centroid
9737
+ }
9738
+
8855
9739
  function refineRoundedCorners(pathData, {
8856
9740
  threshold = 0,
9741
+ simplifyQuadraticCorners = false,
8857
9742
  tolerance = 1
8858
9743
  } = {}) {
8859
9744
 
@@ -8878,6 +9763,9 @@
8878
9763
  let firstIsLine = pathData[1].type === 'L';
8879
9764
  let firstIsBez = pathData[1].type === 'C';
8880
9765
 
9766
+ // in case we have simplified a corner connecting to the start
9767
+ let M_adj = null;
9768
+
8881
9769
  let normalizeClose = isClosed && firstIsBez && (lastIsLine || zIsLineto);
8882
9770
 
8883
9771
  // normalize closepath to lineto
@@ -8917,15 +9805,17 @@
8917
9805
  // closing corner to start
8918
9806
  if (isClosed && lastIsBez && firstIsLine && i === l - lastOff - 1) {
8919
9807
  comL1 = pathData[1];
9808
+
8920
9809
  comBez = [pathData[l - lastOff]];
8921
9810
 
8922
9811
  }
8923
9812
 
9813
+ // collect enclosed bezier segments
8924
9814
  for (let j = i + 1; j < l; j++) {
8925
9815
  let comN = pathData[j] ? pathData[j] : null;
8926
9816
  let comPrev = pathData[j - 1];
8927
9817
 
8928
- if (comPrev.type === 'C') {
9818
+ if (comPrev.type === 'C' && j > 2) {
8929
9819
  comBez.push(comPrev);
8930
9820
  }
8931
9821
 
@@ -8956,39 +9846,67 @@
8956
9846
  let bezThresh = len3 * 0.5 * tolerance;
8957
9847
  let isSmall = bezThresh < len1 && bezThresh < len2;
8958
9848
 
9849
+ /*
9850
+ */
9851
+
8959
9852
  if (comBez.length && !signChange && isSmall) {
8960
9853
 
8961
- let isFlatBezier = Math.abs(area2) < getSquareDistance(comBez[0].p0, comBez[0].p) * 0.005;
9854
+ let isSquare = false;
9855
+
9856
+ if (comBez.length === 1) {
9857
+ let dx = Math.abs(comBez[0].p.x - comBez[0].p0.x);
9858
+ let dy = Math.abs(comBez[0].p.y - comBez[0].p0.y);
9859
+ let diff = (dx - dy);
9860
+ let rat = Math.abs(diff / dx);
9861
+ isSquare = rat < 0.01;
9862
+ }
9863
+
9864
+ let preferArcs = true;
9865
+ preferArcs = false;
9866
+
9867
+ // if rectangular prefer arcs
9868
+ if (preferArcs && isSquare) {
9869
+
9870
+ let pM = pointAtT([comBez[0].p0, comBez[0].cp1, comBez[0].cp2, comBez[0].p], 0.5);
9871
+
9872
+ let arcProps = getArcFromPoly([comBez[0].p0, pM, comBez[0].p]);
9873
+ let { r, centroid, deltaAngle } = arcProps;
9874
+
9875
+ let sweep = deltaAngle > 0 ? 1 : 0;
9876
+
9877
+ let largeArc = 0;
9878
+
9879
+ let comArc = { type: 'A', values: [r, r, 0, largeArc, sweep, comBez[0].p.x, comBez[0].p.y] };
9880
+
9881
+ pathDataN.push(comL0, comArc);
9882
+ i += offset;
9883
+ continue
9884
+
9885
+ }
9886
+
9887
+ let areaThresh = getSquareDistance(comBez[0].p0, comBez[0].p) * 0.005;
9888
+ let isFlatBezier = Math.abs(area2) < areaThresh;
9889
+ let isFlatBezier2 = Math.abs(area2) < areaThresh * 10;
9890
+
8962
9891
  let ptQ = !isFlatBezier ? checkLineIntersection(comL0.p0, comL0.p, comL1.p, comL1.p0, false, true) : null;
8963
9892
 
8964
- if (!ptQ) {
9893
+ // exit: is rather flat or has no intersection
9894
+
9895
+ if (!ptQ || (isFlatBezier2 && comBez.length === 1)) {
8965
9896
  pathDataN.push(com);
8966
9897
  continue
8967
9898
  }
8968
9899
 
8969
- // check sign change
9900
+ // check sign change - exit if present
8970
9901
  if (ptQ) {
8971
9902
  let area0 = getPolygonArea([comL0.p0, comL0.p, comL1.p0, comL1.p], false);
8972
9903
  let area0_abs = Math.abs(area0);
8973
9904
  let area1 = getPolygonArea([comL0.p0, comL0.p, ptQ, comL1.p0, comL1.p], false);
8974
9905
  let area1_abs = Math.abs(area1);
8975
9906
  let areaDiff = Math.abs(area0_abs - area1_abs) / area0_abs;
8976
-
8977
- /*
8978
- renderPoint(markers, comL0.p0, 'green', '0.5%', '0.5')
8979
- renderPoint(markers, comL0.p, 'red', '1.5%', '0.5')
8980
- renderPoint(markers, comL1.p0, 'blue', '0.5%', '0.5')
8981
- renderPoint(markers, comL1.p, 'orange', '0.5%', '0.5')
8982
- if(!area0) {
8983
- pathDataN.push(com);
8984
- continue
8985
- }
8986
- */
8987
-
8988
9907
  let signChange = area0 < 0 && area1 > 0 || area0 > 0 && area1 < 0;
8989
9908
 
8990
9909
  if (!ptQ || signChange || areaDiff > 0.5) {
8991
-
8992
9910
  pathDataN.push(com);
8993
9911
  continue
8994
9912
  }
@@ -9003,24 +9921,67 @@
9003
9921
 
9004
9922
  // not in tolerance – return original command
9005
9923
  if (bezThresh && dist1 > bezThresh && dist1 > len3 * 0.3) {
9006
-
9007
9924
  pathDataN.push(com);
9008
9925
  continue;
9009
9926
 
9010
- } else {
9927
+ }
9011
9928
 
9012
- let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, comL1.p0.x, comL1.p0.y] };
9013
- comQ.p0 = comL0.p;
9014
- comQ.cp1 = ptQ;
9015
- comQ.p = comL1.p0;
9929
+ // return simplified quadratic Bézier command
9930
+ let p_Q = comL1.p0;
9016
9931
 
9017
- // add quadratic command
9018
- pathDataN.push(comL0, comQ);
9019
- i += offset;
9932
+ // adjust previous end point to better fit the cubic curvature
9933
+ let adjustQ = !simplifyQuadraticCorners;
9934
+
9935
+ if (adjustQ) {
9936
+
9937
+ let t = 0.1666;
9938
+ let p0_adj = interpolate(ptQ, comL0.p, (1 + t));
9939
+ p_Q = interpolate(ptQ, comL1.p0, (1 + t));
9940
+
9941
+ // round for large enough segments
9942
+ let isH = ptQ.y===comL0.p.y;
9943
+ let isV = ptQ.x===comL0.p.x;
9944
+ let isH2 = ptQ.y===comL1.p0.y;
9945
+ let isV2 = ptQ.x===comL1.p0.x;
9946
+
9947
+ if(isSquare && com.dimA>3){
9948
+ let dec = 0.5;
9949
+ if(isH) p0_adj.x = roundTo(p0_adj.x, dec);
9950
+ if(isV) p0_adj.y = roundTo(p0_adj.y, dec);
9951
+ if(isH2) p_Q.x = roundTo(p_Q.x, dec);
9952
+ if(isV2) p_Q.y = roundTo(p_Q.y, dec);
9953
+ }
9954
+
9955
+ /*
9956
+ renderPoint(markers, p0_adj, 'orange')
9957
+ renderPoint(markers, p_Q, 'orange')
9958
+ renderPoint(markers, comL0.p, 'green')
9959
+ renderPoint(markers, comL1.p0, 'magenta')
9960
+ */
9961
+
9962
+ // set new M starting point
9963
+ if (i === l - lastOff - 1) {
9964
+
9965
+ M_adj = p_Q;
9966
+ }
9967
+
9968
+ // adjust previous lineto end point
9969
+ comL0.values = [p0_adj.x, p0_adj.y];
9970
+ comL0.p = p0_adj;
9020
9971
 
9021
- continue;
9022
9972
  }
9023
9973
 
9974
+ let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, p_Q.x, p_Q.y] };
9975
+ comQ.cp1 = ptQ;
9976
+ comQ.p0 = comL0.p;
9977
+ comQ.p = p_Q;
9978
+
9979
+ // add quadratic command
9980
+ pathDataN.push(comL0, comQ);
9981
+
9982
+ i += offset;
9983
+ continue;
9984
+
9024
9985
  }
9025
9986
  }
9026
9987
  }
@@ -9034,6 +9995,12 @@
9034
9995
 
9035
9996
  }
9036
9997
 
9998
+ // correct starting point connecting with last corner rounding
9999
+ if (M_adj) {
10000
+ pathDataN[0].values = [M_adj.x, M_adj.y];
10001
+ pathDataN[0].p0 = M_adj;
10002
+ }
10003
+
9037
10004
  // revert close path normalization
9038
10005
  if (normalizeClose || (isClosed && pathDataN[pathDataN.length - 1].type !== 'Z')) {
9039
10006
  pathDataN.push({ type: 'Z', values: [] });
@@ -9043,51 +10010,143 @@
9043
10010
 
9044
10011
  }
9045
10012
 
9046
- function getArcFromPoly(pts) {
9047
- if (pts.length < 3) return false
10013
+ function simplifyAdjacentRound(pathData, {
10014
+ threshold = 0,
10015
+ tolerance = 1,
10016
+ // take arcs or cubic beziers
10017
+ toCubic = false,
10018
+ debug = false
10019
+ } = {}) {
9048
10020
 
9049
- // Pick 3 well-spaced points
9050
- let p1 = pts[0];
9051
- let p2 = pts[Math.floor(pts.length / 2)];
9052
- let p3 = pts[pts.length - 1];
10021
+ // fix small Arcs
10022
+ pathData = convertSmallArcsToLinetos(pathData);
9053
10023
 
9054
- let x1 = p1.x, y1 = p1.y;
9055
- let x2 = p2.x, y2 = p2.y;
9056
- let x3 = p3.x, y3 = p3.y;
10024
+ // min size threshold for corners
10025
+ threshold *= tolerance;
9057
10026
 
9058
- let a = x1 - x2;
9059
- let b = y1 - y2;
9060
- let c = x1 - x3;
9061
- let d = y1 - y3;
10027
+ let l = pathData.length;
9062
10028
 
9063
- let e = ((x1 * x1 - x2 * x2) + (y1 * y1 - y2 * y2)) / 2;
9064
- let f = ((x1 * x1 - x3 * x3) + (y1 * y1 - y3 * y3)) / 2;
10029
+ // add fist command
10030
+ let pathDataN = [pathData[0]];
9065
10031
 
9066
- let det = a * d - b * c;
10032
+ // find adjacent cubics between extremes
9067
10033
 
9068
- if (Math.abs(det) < 1e-10) {
9069
- console.warn("Points are collinear or numerically unstable");
9070
- return false;
9071
- }
10034
+ for (let i = 1; i < l; i++) {
10035
+ pathData[i - 1];
10036
+ let com = pathData[i];
10037
+ let comN = pathData[i + 1] || null;
9072
10038
 
9073
- // find center of arc
9074
- let cx = (d * e - b * f) / det;
9075
- let cy = (-c * e + a * f) / det;
9076
- let centroid = { x: cx, y: cy };
10039
+ if (!comN) {
10040
+ pathDataN.push(com);
10041
+ break
10042
+ }
9077
10043
 
9078
- // Radius (use start point)
9079
- let r = getDistance(centroid, p1);
10044
+ let { type, extreme = false, p0, p, dimA = 0 } = com;
10045
+ // for short segment detection
10046
+ let dimAN = comN.dimA;
10047
+ let dimA0 = dimA + dimAN;
10048
+ let thresh = 0.1;
9080
10049
 
9081
- let angleData = getDeltaAngle(centroid, p1, p3);
9082
- let {deltaAngle, startAngle, endAngle} = angleData;
10050
+ // ignore short linetos
10051
+ let isShortN = dimAN < dimA0 * thresh;
9083
10052
 
9084
- return {
9085
- centroid,
9086
- r,
9087
- startAngle,
9088
- endAngle,
9089
- deltaAngle
9090
- };
10053
+ // adjacent cubic commands - accept short in between linetos
10054
+ if ((type === 'C') && (comN.type === 'C' || isShortN)) {
10055
+
10056
+ let candidates = [];
10057
+
10058
+ for (let j = i + 1; j < l; j++) {
10059
+ let comN = pathData[j];
10060
+ let { type, extreme = false, corner = false, dimA = 0 } = comN;
10061
+ let isShort = dimA < dimA0 * thresh;
10062
+
10063
+ // skip for type change(unless very short), extremes or corners
10064
+ /*
10065
+ if ( (comN.extreme || comN.corner) ) {
10066
+ if(!extreme && !corner) candidates.push(comN)
10067
+ break;
10068
+ }
10069
+ */
10070
+
10071
+ if (extreme || corner) {
10072
+
10073
+ if (isShort && comN.type !== 'C') ;
10074
+
10075
+ if ((extreme && !corner)) {
10076
+
10077
+ candidates.push(comN);
10078
+ }
10079
+
10080
+ break;
10081
+ }
10082
+
10083
+ candidates.push(comN);
10084
+ }
10085
+
10086
+ // try to create arc command
10087
+ if (candidates.length > 1) {
10088
+
10089
+ let clen = candidates.length;
10090
+ let pts = [com.p0, com.p,];
10091
+
10092
+ // add interpolated points to prevent wrong arc replacements
10093
+ candidates.forEach(c => {
10094
+ if (c.type === 'C') {
10095
+ let pt = pointAtT([c.p0, c.cp1, c.cp2, c.p], 0.5);
10096
+ pts.push(pt);
10097
+ }
10098
+ pts.push(c.p);
10099
+ });
10100
+
10101
+ let precise = true;
10102
+ let arcProps = getArcFromPoly(pts, precise);
10103
+
10104
+ // could be combined
10105
+ if (arcProps) {
10106
+
10107
+ let { centroid, r, deltaAngle, startAngle, endAngle } = arcProps;
10108
+ let sweep = deltaAngle > 0 ? 1 : 0;
10109
+
10110
+ let largeArc = Math.abs(deltaAngle) > Math.PI ? 1 : 0;
10111
+ largeArc = 0;
10112
+ let comLast = candidates[clen - 1];
10113
+ let p = comLast.p;
10114
+
10115
+ let comArc = { type: 'A', values: [r, r, 0, largeArc, sweep, p.x, p.y] };
10116
+
10117
+ comArc.dimA = getDistManhattan(p0, p);
10118
+ comArc.p0 = p0;
10119
+ comArc.p = p;
10120
+ comArc.error = 0;
10121
+ comArc.directionChange = comLast.directionChange;
10122
+ comArc.extreme = comLast.extreme;
10123
+ comArc.corner = comLast.corner;
10124
+ pathDataN.push(comArc);
10125
+
10126
+ i += candidates.length;
10127
+ continue
10128
+
10129
+ }
10130
+
10131
+ // arc radius calculation failed - return original
10132
+ else {
10133
+ pathDataN.push(com);
10134
+ }
10135
+ }
10136
+
10137
+ // could not be simplified – return original command
10138
+ else {
10139
+ pathDataN.push(com);
10140
+ }
10141
+
10142
+ }
10143
+ // all other commands
10144
+ else {
10145
+ pathDataN.push(com);
10146
+ }
10147
+ }
10148
+
10149
+ return pathDataN
9091
10150
  }
9092
10151
 
9093
10152
  function refineRoundSegments(pathData, {
@@ -9106,9 +10165,6 @@
9106
10165
  // add fist command
9107
10166
  let pathDataN = [pathData[0]];
9108
10167
 
9109
- // just for debugging
9110
- let pathDataTest = [];
9111
-
9112
10168
  for (let i = 1; i < l; i++) {
9113
10169
  let com = pathData[i];
9114
10170
  let { type } = com;
@@ -9135,11 +10191,12 @@
9135
10191
 
9136
10192
  // 2. line-line-bezier-line-line
9137
10193
  if (
10194
+ comN2 && comN3 &&
9138
10195
  comP.type === 'L' &&
9139
10196
  type === 'L' &&
9140
10197
  comBez &&
9141
10198
  comN2.type === 'L' &&
9142
- comN3 && (comN3.type === 'L' || comN3.type === 'Z')
10199
+ (comN3.type === 'L' || comN3.type === 'Z')
9143
10200
  ) {
9144
10201
 
9145
10202
  L1 = [com.p0, com.p];
@@ -9166,10 +10223,10 @@
9166
10223
  }
9167
10224
 
9168
10225
  // 1. line-bezier-bezier-line
9169
- else if ((type === 'C' || type === 'Q') && comP.type === 'L') {
10226
+ else if (comN && (type === 'C' || type === 'Q') && comP.type === 'L') {
9170
10227
 
9171
10228
  // 1.2 next is cubic next is lineto
9172
- if ((comN.type === 'C' || comN.type === 'Q') && comN2.type === 'L') {
10229
+ if (comN2 && comN2.type === 'L' && (comN.type === 'C' || comN.type === 'Q')) {
9173
10230
 
9174
10231
  combine = true;
9175
10232
 
@@ -9228,16 +10285,19 @@
9228
10285
  }
9229
10286
  );
9230
10287
 
9231
- if(bezierCommands.length === 1){
10288
+ if (bezierCommands.length === 1) {
9232
10289
 
9233
10290
  // prefer more compact quadratic - otherwise arcs
9234
10291
  let comBezier = revertCubicQuadratic(p0_S, bezierCommands[0].cp1, bezierCommands[0].cp2, p_S);
9235
10292
 
9236
10293
  if (comBezier.type === 'Q') {
9237
10294
  toCubic = true;
10295
+ }else {
10296
+ comBezier = bezierCommands[0];
9238
10297
  }
9239
10298
 
9240
10299
  com = comBezier;
10300
+
9241
10301
  }
9242
10302
 
9243
10303
  // prefer arcs if 2 cubics are required
@@ -9257,25 +10317,28 @@
9257
10317
 
9258
10318
  // test rendering
9259
10319
 
10320
+ /*
9260
10321
  if (debug) {
9261
10322
  // arcs
9262
10323
  if (!toCubic) {
9263
10324
  pathDataTest = [
9264
10325
  { type: 'M', values: [p0_S.x, p0_S.y] },
9265
10326
  { type: 'A', values: [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y] },
9266
- ];
10327
+ ]
9267
10328
  }
9268
10329
  // cubics
9269
10330
  else {
9270
10331
  pathDataTest = [
9271
10332
  { type: 'M', values: [p0_S.x, p0_S.y] },
9272
10333
  ...bezierCommands
9273
- ];
10334
+ ]
10335
+
9274
10336
  }
9275
10337
 
9276
10338
  let d = pathDataToD(pathDataTest);
9277
- renderPath(markers, d, 'orange', '0.5%', '0.5');
10339
+ renderPath(markers, d, 'orange', '0.5%', '0.5')
9278
10340
  }
10341
+ */
9279
10342
 
9280
10343
  pathDataN.push(com);
9281
10344
  i++;
@@ -9407,7 +10470,6 @@
9407
10470
  let com = pathData[c];
9408
10471
  let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
9409
10472
  if (type === 'C') {
9410
-
9411
10473
  let comQ = revertCubicQuadratic(p0, cp1, cp2, p, tolerance);
9412
10474
  if (comQ.type === 'Q') {
9413
10475
  comQ.extreme = com.extreme;
@@ -11019,32 +12081,24 @@
11019
12081
  }
11020
12082
 
11021
12083
  // reverse paths
11022
- for (let i = 0; i < l; i++) {
12084
+ for (let i = 0; l && i < l; i++) {
11023
12085
 
11024
12086
  let poly = polys[i];
11025
12087
  let { cw, includedIn, includes } = poly;
11026
12088
 
11027
- // outer path direction to counter clockwise
11028
- if (!includedIn.length && cw && !toClockwise
11029
- || !includedIn.length && !cw && toClockwise
11030
- ) {
11031
-
11032
- pathDataArr[i].pathData = reversePathData(pathDataArr[i].pathData);
11033
- polys[i].cw = polys[i].cw ? false : true;
11034
- cw = polys[i].cw;
11035
-
11036
- }
12089
+ let len = includes.length;
11037
12090
 
11038
12091
  // reverse inner sub paths
11039
- for (let j = 0; j < includes.length; j++) {
12092
+ for (let j = 0; len && j < len; j++) {
11040
12093
  let ind = includes[j];
11041
12094
  let child = polys[ind];
11042
12095
 
11043
- if (child.cw === cw) {
12096
+ // nothing to do
12097
+ if (child.cw !== cw) continue
12098
+
12099
+ pathDataArr[ind].pathData = reversePathData(pathDataArr[ind].pathData);
12100
+ polys[ind].cw = polys[ind].cw ? false : true;
11044
12101
 
11045
- pathDataArr[ind].pathData = reversePathData(pathDataArr[ind].pathData);
11046
- polys[ind].cw = polys[ind].cw ? false : true;
11047
- }
11048
12102
  }
11049
12103
  }
11050
12104
 
@@ -11084,6 +12138,7 @@
11084
12138
  allowAriaAtts: true,
11085
12139
 
11086
12140
  convertPathLength: false,
12141
+ toAbsoluteUnits: false,
11087
12142
 
11088
12143
  // custom removal
11089
12144
  removeElements: [],
@@ -11112,6 +12167,7 @@
11112
12167
  revertToQuadratics: true,
11113
12168
  refineExtremes: false,
11114
12169
  simplifyCorners: false,
12170
+ simplifyQuadraticCorners: false,
11115
12171
  keepExtremes: true,
11116
12172
  keepCorners: true,
11117
12173
  keepInflections: false,
@@ -11168,7 +12224,7 @@
11168
12224
  let isArray = Array.isArray(val);
11169
12225
 
11170
12226
  if (isBoolean) val = false;
11171
- else if (!isArray && isNum) val = val===1 ? 1 : (prop==='decimals'? -1 : 0);
12227
+ else if (!isArray && isNum) val = val === 1 ? 1 : (prop === 'decimals' ? -1 : 0);
11172
12228
  else if (isArray) val = [];
11173
12229
  settingsNull[prop] = val;
11174
12230
  }
@@ -11199,10 +12255,14 @@
11199
12255
  ...settingsDefaults,
11200
12256
  ...{
11201
12257
  keepSmaller: false,
12258
+ convertPathLength:true,
11202
12259
  toRelative: true,
11203
12260
  toMixed: true,
11204
12261
  toShorthands: true,
11205
12262
 
12263
+ allowMeta:true,
12264
+ allowDataAtts:true,
12265
+ allowAriaAtts:true,
11206
12266
  legacyHref: true,
11207
12267
  addViewBox: true,
11208
12268
  addDimensions: true,
@@ -11270,19 +12330,24 @@
11270
12330
  high: {
11271
12331
  ...settingsDefaults,
11272
12332
  ...{
11273
- tolerance: 1.2,
12333
+ tolerance: 1.1,
11274
12334
  toMixed: true,
11275
12335
  refineExtremes: true,
11276
12336
  simplifyCorners: true,
12337
+ simplifyQuadraticCorners: true,
12338
+ removeOrphanSubpaths: true,
11277
12339
  simplifyRound: true,
11278
12340
  removeClassNames: true,
11279
12341
  cubicToArc: true,
12342
+ minifyD: 0,
11280
12343
  removeComments: true,
11281
12344
  removeHidden: true,
11282
- removeOffCanvas: true,
11283
12345
  addViewBox: true,
11284
12346
  removeDimensions: true,
11285
- minifyD: 0
12347
+ removeOffCanvas: true,
12348
+
12349
+ /*
12350
+ */
11286
12351
  }
11287
12352
  }
11288
12353
 
@@ -11440,18 +12505,44 @@
11440
12505
  ...settings
11441
12506
  };
11442
12507
 
11443
- 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;
12508
+ 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, 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, simplifyQuadraticCorners, scale, scaleTo, crop, alignToOrigin, convertTransforms, keepSmaller, splitCompound, convertPathLength, toAbsoluteUnits } = settings;
11444
12509
 
11445
12510
  // clamp tolerance and scale
11446
12511
  tolerance = Math.max(0.1, tolerance);
11447
12512
  scale = Math.max(0.001, scale);
11448
- if(fixDirections) keepSmaller = false;
12513
+ if (fixDirections) keepSmaller = false;
11449
12514
  if (scale !== 1 || scaleTo || crop || alignToOrigin) {
11450
12515
  convertTransforms = true;
11451
12516
  settings.convertTransforms = true;
11452
12517
  }
11453
12518
 
11454
- let inputType = detectInputType(input);
12519
+ /**
12520
+ * intercept
12521
+ * invalid inputs
12522
+ */
12523
+
12524
+ let inputDetection = detectInputType(input);
12525
+ let { inputType, log } = inputDetection;
12526
+
12527
+ // invalid file
12528
+ if (inputType === 'invalid' || input === dummySVG) {
12529
+ // return dummy SVG to continue processing
12530
+
12531
+ let report = {
12532
+ original: 0,
12533
+ new: 0,
12534
+ saved: 0,
12535
+ svgSize:0,
12536
+ svgSizeOpt:0,
12537
+ compression:0,
12538
+ decimals:0,
12539
+ invalid:true
12540
+ };
12541
+
12542
+ return { svg: dummySVG, d: '', polys: [], report, pathDataPlusArr: [], pathDataPlusArr_global: [], inputType: 'invalid', dOriginal: '' };
12543
+
12544
+ }
12545
+
11455
12546
  let svg = '';
11456
12547
  let svgSize = 0;
11457
12548
  let svgSizeOpt = 0;
@@ -11571,10 +12662,7 @@
11571
12662
  // convert all shapes to paths
11572
12663
  if (shapesToPaths) {
11573
12664
  shapeConvert = 'toPaths';
11574
- convert_rects = true;
11575
- convert_ellipses = true;
11576
- convert_poly = true;
11577
- convert_lines = true;
12665
+ convertShapes = ['rect', 'polygon', 'polyline', 'line', 'circle', 'ellipse'];
11578
12666
  }
11579
12667
 
11580
12668
  // sanitize SVG - clone/decouple settings
@@ -11608,6 +12696,9 @@
11608
12696
  decimals,
11609
12697
  };
11610
12698
 
12699
+ let comCount = 0;
12700
+ let comCountS = 0;
12701
+
11611
12702
  for (let i = 0, l = paths.length; l && i < l; i++) {
11612
12703
 
11613
12704
  let pathDataPlusArr = [];
@@ -11652,7 +12743,7 @@
11652
12743
  }
11653
12744
 
11654
12745
  // count commands for evaluation
11655
- let comCount = pathData.length;
12746
+ comCount += pathData.length;
11656
12747
 
11657
12748
  if (!isPoly && removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
11658
12749
 
@@ -11817,11 +12908,14 @@
11817
12908
  if (simplifyCorners) {
11818
12909
 
11819
12910
  let threshold = (bb.width + bb.height) * 0.1;
11820
- pathData = refineRoundedCorners(pathData, { threshold, tolerance });
12911
+ pathData = refineRoundedCorners(pathData, { threshold, tolerance, simplifyQuadraticCorners });
11821
12912
  }
11822
12913
 
11823
12914
  // refine round segment sequences
11824
- if (simplifyRound) pathData = refineRoundSegments(pathData);
12915
+ if (simplifyRound) {
12916
+ pathData = refineRoundSegments(pathData);
12917
+ pathData = simplifyAdjacentRound(pathData);
12918
+ }
11825
12919
 
11826
12920
  // simplify to quadratics
11827
12921
  if (revertToQuadratics) pathData = pathDataRevertCubicToQuadratic(pathData, tolerance);
@@ -11851,7 +12945,7 @@
11851
12945
  let yMax = Math.max(...yArr);
11852
12946
 
11853
12947
  bb_global = { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin };
11854
- let isPortrait = bb_global.height > bb_global.width;
12948
+ bb_global.height > bb_global.width;
11855
12949
 
11856
12950
  // fix path directions - before reordering
11857
12951
  if (fixDirections) {
@@ -11860,7 +12954,23 @@
11860
12954
 
11861
12955
  // prefer top to bottom priority for portrait aspect ratios
11862
12956
  if (optimizeOrder) {
11863
- 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);
12957
+ /*
12958
+ 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)
12959
+ */
12960
+
12961
+ // add missin bbox
12962
+ pathDataPlusArr.forEach(p => {
12963
+ if (p.bb.x === undefined) {
12964
+ p.bb = getPolyBBox(getPathDataVertices(p.pathData));
12965
+ }
12966
+ });
12967
+
12968
+ try {
12969
+ pathDataPlusArr = pathDataPlusArr.sort((a, b) => +a.bb.x.toFixed(2) - (+b.bb.x.toFixed(2)) || a.bb.y - b.bb.y);
12970
+
12971
+ } catch {
12972
+ }
12973
+
11864
12974
  }
11865
12975
 
11866
12976
  // flatten compound paths
@@ -11923,7 +13033,7 @@
11923
13033
  }
11924
13034
 
11925
13035
  // compare command count
11926
- let comCountS = pathData.length;
13036
+ comCountS += pathData.length;
11927
13037
 
11928
13038
  let dOpt = pathDataToD(pathData, minifyD);
11929
13039
 
@@ -11998,6 +13108,9 @@
11998
13108
  svgSizeOpt = +(svgSizeOpt / 1024).toFixed(3);
11999
13109
 
12000
13110
  report = {
13111
+ original: comCount,
13112
+ new: comCountS,
13113
+ saved: comCount - comCountS,
12001
13114
  svgSize,
12002
13115
  svgSizeOpt,
12003
13116
  compression,
@@ -12032,6 +13145,8 @@
12032
13145
  if (typeof window !== 'undefined') {
12033
13146
  window.svgPathSimplify = svgPathSimplify;
12034
13147
  window.getElementTransform = getElementTransform;
13148
+ window.validateSVG = validateSVG;
13149
+ window.detectInputType = detectInputType;
12035
13150
 
12036
13151
  window.getViewBox = getViewBox;
12037
13152
 
@@ -12045,6 +13160,7 @@
12045
13160
  exports.atan2 = atan2$1;
12046
13161
  exports.ceil = ceil$1;
12047
13162
  exports.cos = cos$1;
13163
+ exports.detectInputType = detectInputType;
12048
13164
  exports.exp = exp$1;
12049
13165
  exports.floor = floor$1;
12050
13166
  exports.getElementTransform = getElementTransform;
@@ -12060,5 +13176,6 @@
12060
13176
  exports.sqrt = sqrt$1;
12061
13177
  exports.svgPathSimplify = svgPathSimplify;
12062
13178
  exports.tan = tan$1;
13179
+ exports.validateSVG = validateSVG;
12063
13180
 
12064
13181
  })(this["svg-path-simplify"] = this["svg-path-simplify"] || {});