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