svg-path-simplify 0.4.3 → 0.4.5

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 (41) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +2 -1
  3. package/dist/svg-path-simplify.esm.js +1670 -509
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +1671 -508
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +936 -463
  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 +60 -20
  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 +46 -18
  16. package/src/pathData_simplify_revertToquadratics.js +0 -1
  17. package/src/pathSimplify-main.js +81 -20
  18. package/src/pathSimplify-only-pathdata.js +7 -2
  19. package/src/pathSimplify-presets.js +14 -4
  20. package/src/svg-getAttributes.js +5 -3
  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 +18 -2
  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 +123 -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/poly_normalize.js +9 -8
  33. package/src/svgii/rounding.js +112 -80
  34. package/src/svgii/svg_cleanup.js +75 -22
  35. package/src/svgii/svg_cleanup_convertPathLength.js +27 -15
  36. package/src/svgii/svg_cleanup_normalize_transforms.js +1 -1
  37. package/src/svgii/svg_cleanup_remove_els_and_atts.js +6 -1
  38. package/src/svgii/svg_el_parse_style_props.js +13 -10
  39. package/src/svgii/svg_validate.js +220 -0
  40. package/tests/testSVG.js +14 -1
  41. package/src/svgii/pathData_refine_round.js +0 -222
@@ -28,62 +28,276 @@
28
28
  }
29
29
  }
30
30
 
31
- function renderPath(svg, d = '', stroke = 'green', strokeWidth = '1%', opacity="1", render = true) {
31
+ const {
32
+ abs: abs$1, acos: acos$1, asin: asin$1, atan: atan$1, atan2: atan2$1, ceil: ceil$1, cos: cos$1, exp: exp$1, floor: floor$1,
33
+ log: log$1, hypot, max: max$1, min: min$1, pow: pow$1, random: random$1, round: round$1, sin: sin$1, sqrt: sqrt$1, tan: tan$1, PI: PI$1
34
+ } = Math;
35
+
36
+ const rad2Deg = 180/Math.PI;
37
+ const deg2rad = Math.PI/180;
38
+ const root2 = 1.4142135623730951;
39
+ const svgNs = 'http://www.w3.org/2000/svg';
40
+ const dummySVG = `<svg id="svgInvalid" xmlns="${svgNs}" viewBox="0 0 1 1"><path d="M0 0 h0" /></svg>`;
32
41
 
33
- let path = `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${opacity}" /> `;
42
+ // 1/2.54
43
+ const inch2cm = 0.39370078;
34
44
 
35
- if (render) {
36
- svg.insertAdjacentHTML("beforeend", path);
37
- } else {
38
- return path;
45
+ // 1/72
46
+ const inch2pt = 0.01388889;
47
+
48
+ function validateSVG(markup, allowed = {}) {
49
+ allowed = {
50
+ ...{
51
+
52
+ useElsNested: 5000,
53
+ hasScripts: false,
54
+ hasEntity: false,
55
+ fileSizeKB: 10000,
56
+ isSymbolSprite: false,
57
+ isSvgFont: false
58
+ },
59
+ ...allowed
60
+ };
61
+
62
+ let fileReport = analyzeSVG(markup, allowed);
63
+ let isValid = true;
64
+ let log = [];
65
+
66
+ if (!fileReport.hasEls) {
67
+ log.push("no elements");
68
+ isValid = false;
69
+ }
70
+
71
+ if (Object.keys(fileReport).length) {
72
+ if (fileReport.isBillionLaugh === true) {
73
+ log.push(`suspicious: might contain billion laugh attack`);
74
+ isValid = false;
75
+ }
76
+
77
+ for (let key in allowed) {
78
+ let val = allowed[key];
79
+ let valRep = fileReport[key];
80
+ if (typeof val === "number" && valRep > val) {
81
+ log.push(`allowed "${key}" exceeded: ${valRep} / ${val} `);
82
+ isValid = false;
83
+ }
84
+ if (valRep === true && val === false) {
85
+ log.push(`not allowed: "${key}" `);
86
+ isValid = false;
87
+ }
88
+ }
89
+ } else {
90
+ isValid = false;
91
+ }
92
+
93
+ /*
94
+ if (!isValid) {
95
+ log = ["SVG not valid"].concat(log);
96
+
97
+ if (Object.keys(fileReport).length) {
98
+ console.warn(fileReport);
99
+ }
100
+ }
101
+ */
102
+
103
+ return { isValid, log, fileReport };
104
+ }
105
+
106
+ function analyzeSVG(markup, allowed = {}) {
107
+ markup = markup.trim();
108
+ let doc, svg;
109
+ let fileSizeKB = +(markup.length / 1024).toFixed(3);
110
+
111
+ let fileReport = {
112
+ totalEls: 1,
113
+ hasEls: true,
114
+ hasDefs: false,
115
+ geometryEls: [],
116
+ useEls: 0,
117
+ useElsNested: 0,
118
+ nonsensePaths: 0,
119
+ isSuspicious: false,
120
+ isBillionLaugh: false,
121
+ hasScripts: false,
122
+ hasPrologue: false,
123
+ hasEntity: false,
124
+ isPathData:false,
125
+ fileSizeKB,
126
+ hasXmlns: markup.includes("http://www.w3.org/2000/svg"),
127
+ isSymbolSprite: false,
128
+ isSvgFont: markup.includes("<glyph>")
129
+ };
130
+
131
+ let maxNested = allowed.useElsNested ? allowed.useElsNested : 2000;
132
+
133
+ /**
134
+ * analyze nestes use references
135
+ */
136
+ const countUseRefs = (useEls, maxNested = 2000) => {
137
+ let nestedCount = 0;
138
+
139
+ for (let i = 0; i < useEls.length && nestedCount < maxNested; i++) {
140
+ let use = useEls[i];
141
+ let refId = use.getAttribute("xlink:href")
142
+ ? use.getAttribute("xlink:href")
143
+ : use.getAttribute("href");
144
+ refId = refId ? refId.replace("#", "") : "";
145
+
146
+ use.setAttribute("href", "#" + refId);
147
+
148
+ let refEl = svg.getElementById(refId);
149
+ let nestedUse = refEl.querySelectorAll("use");
150
+ let nestedUseLength = nestedUse.length;
151
+ nestedCount += nestedUseLength;
152
+
153
+ // query nested use references
154
+ for (let n = 0; n < nestedUse.length && nestedCount < maxNested; n++) {
155
+ let nested = nestedUse[n];
156
+ let id1 = nested.getAttribute("href").replace("#", "");
157
+ let refEl1 = svg.getElementById(id1);
158
+ let nestedUse1 = refEl1.querySelectorAll("use");
159
+ nestedCount += nestedUse1.length;
160
+ }
161
+ }
162
+ fileReport.useElsNested = nestedCount;
163
+ return nestedCount;
164
+ };
165
+ let hasEntity = /\<\!ENTITY/gi.test(markup);
166
+ let hasScripts = /\<script/gi.test(markup) ? true : false;
167
+ let hasUse = /\<use/gi.test(markup) ? true : false;
168
+ let hasEls = /[\<path|\<polygon|\<polyline|\<rect|\<circle|\<ellipse|\<line|\<text|\<foreignObject]/gi.test(markup);
169
+ let hasDefs = /[\<filter|\<linearGradient|\<radialGradient|\<pattern|\<animate|\<animateMotion|\<animateTransform|\<clipPath|\<mask|\<symbol|\<marker]/gi.test(markup);
170
+
171
+ let isPathData = (markup.startsWith('M') || markup.startsWith('m')) && !/[\<svg|\<\/svg]/gi.test(markup);
172
+ fileReport.isPathData = isPathData;
173
+
174
+ // seems OK
175
+ if (!hasEntity && !hasUse && !hasScripts && (hasEls || hasDefs) && fileSizeKB < allowed.fileSizeKB) {
176
+ fileReport.hasEls = hasEls;
177
+ fileReport.hasDefs = hasDefs;
178
+
179
+ return fileReport
180
+ }
181
+
182
+ // Contains xml entity definition: highly suspicious - stop parsing!
183
+ if (allowed.hasEntity === false && hasEntity) {
184
+ fileReport.hasEntity = true;
185
+
186
+ }
187
+
188
+ /**
189
+ * sanitizing for parsing:
190
+ * remove xml prologue and comments
191
+ */
192
+ markup = markup
193
+ .replace(/\<\?xml.+\?\>|\<\!DOCTYPE.+]\>/g, "")
194
+ .replace(/(<!--.*?-->)|(<!--[\S\s]+?-->)|(<!--[\S\s]*?$)/g, "");
195
+
196
+ /**
197
+ * Try to parse svg:
198
+ * invalid svg will return false via "catch"
199
+ */
200
+ try {
201
+
202
+ doc = new DOMParser().parseFromString(markup, "text/html");
203
+ svg = doc.querySelector("svg");
204
+
205
+ // paths containing only a M command
206
+ let nonsensePaths = svg.querySelectorAll('path[d="M0,0"], path[d="M0 0"]').length;
207
+ let useEls = svg.querySelectorAll("use").length;
208
+
209
+ // create analyzing object
210
+ fileReport.totalEls = svg.querySelectorAll("*").length;
211
+ fileReport.geometryEls = svg.querySelectorAll(
212
+ "path, rect, circle, ellipse, polygon, polyline, line"
213
+ ).length;
214
+
215
+ fileReport.hasScripts = hasScripts;
216
+ fileReport.useEls = useEls;
217
+ fileReport.nonsensePaths = nonsensePaths;
218
+ fileReport.isSuspicious = false;
219
+ fileReport.isBillionLaugh = false;
220
+ fileReport.hasXmlns = svg.getAttribute("xmlns")
221
+ ? svg.getAttribute("xmlns") === "http://www.w3.org/2000/svg"
222
+ ? true
223
+ : false
224
+ : false;
225
+ fileReport.isSymbolSprite =
226
+ svg.querySelectorAll("symbol").length &&
227
+ svg.querySelectorAll("use").length === 0
228
+ ? true
229
+ : false;
230
+ fileReport.isSvgFont = svg.querySelectorAll("glyph").length ? true : false;
231
+
232
+ let totalEls = fileReport.totalEls;
233
+ let totalUseEls = fileReport.useEls;
234
+ let usePercentage = (100 / totalEls) * totalUseEls;
235
+
236
+ // if percentage of use elements is higher than 75% - suspicious
237
+ if (usePercentage > 75) {
238
+ fileReport.isSuspicious = true;
239
+
240
+ // check nested use references
241
+ let nestedCount = countUseRefs(svg.querySelectorAll("use"), maxNested);
242
+ if (nestedCount >= maxNested) {
243
+ fileReport.isBillionLaugh = true;
244
+ }
39
245
  }
40
246
 
247
+ return fileReport;
248
+ } catch {
249
+ // svg file has malformed markup
250
+ console.warn("svg could not be parsed");
251
+ return false;
252
+ }
41
253
  }
42
254
 
43
255
  function detectInputType(input) {
44
- let type = 'string';
45
- /*
46
- if (input instanceof HTMLImageElement) return "img";
47
- if (input instanceof SVGElement) return "svg";
48
- if (input instanceof HTMLCanvasElement) return "canvas";
49
- if (input instanceof File) return "file";
50
- if (input instanceof ArrayBuffer) return "buffer";
51
- if (input instanceof Blob) return "blob";
52
- */
256
+ let log = '';
257
+ let isValid = true;
258
+
259
+ let result = {
260
+ inputType:'',
261
+ isValid:true,
262
+ fileReport:{},
263
+ };
264
+
53
265
  if (Array.isArray(input)) {
54
266
 
267
+ result.inputType = "array";
268
+
55
269
  // nested array
56
270
  if (Array.isArray(input[0])) {
57
271
 
58
272
  if (input[0].length === 2) {
59
273
 
60
- return 'polyArray'
274
+ result.inputType = 'polyArray';
61
275
  }
62
276
 
63
277
  else if (Array.isArray(input[0][0]) && input[0][0].length === 2) {
64
278
 
65
- return 'polyComplexArray'
279
+ result.inputType = 'polyComplexArray';
66
280
  }
67
281
  else if (input[0][0].x !== undefined && input[0][0].y !== undefined) {
68
282
 
69
- return 'polyComplexObjectArray'
283
+ result.inputType = 'polyComplexObjectArray';
70
284
  }
285
+
71
286
  }
72
287
 
73
288
  // is point array
74
289
  else if (input[0].x !== undefined && input[0].y !== undefined) {
75
290
 
76
- return 'polyObjectArray'
291
+ result.inputType = 'polyObjectArray';
77
292
  }
78
293
 
79
294
  // path data array
80
295
  else if (input[0]?.type && input[0]?.values
81
296
  ) {
82
- return "pathData"
83
-
297
+ result.inputType = "pathData";
84
298
  }
85
299
 
86
- return "array";
300
+ return result;
87
301
  }
88
302
 
89
303
  if (typeof input === "string") {
@@ -95,36 +309,48 @@
95
309
  let isJson = isNumberJson(input);
96
310
 
97
311
  if (isSVG) {
98
- type = 'svgMarkup';
312
+ let validate = validateSVG(input);
313
+ ({isValid, log} = validate) ;
314
+ if(!isValid){
315
+
316
+ result.inputType = 'invalid';
317
+ result.isValid=false,
318
+
319
+ result.log = log;
320
+ }else {
321
+ result.inputType = 'svgMarkup';
322
+ }
323
+
324
+ result.fileReport = validate.fileReport;
325
+
99
326
  }
100
327
 
101
328
  else if (isJson) {
102
- type = 'json';
329
+ result.inputType = 'json';
103
330
  }
104
331
 
105
332
  else if (isSymbol) {
106
- type = 'symbol';
333
+ result.inputType = 'symbol';
107
334
  }
108
335
  else if (isPathData) {
109
- type = 'pathDataString';
336
+ result.inputType = 'pathDataString';
110
337
  }
111
338
  else if (isPolyString) {
112
- type = 'polyString';
339
+ result.inputType = 'polyString';
113
340
  }
114
341
 
115
342
  else {
116
343
  let url = /^(file:|https?:\/\/|\/|\.\/|\.\.\/)/.test(input);
117
344
  let dataUrl = input.startsWith('data:image');
118
- type = url || dataUrl ? "url" : "string";
345
+ result.inputType = url || dataUrl ? "url" : "string";
119
346
  }
120
347
 
121
- return type
348
+ return result
122
349
  }
123
350
 
124
- type = typeof input;
125
- let constructor = input.constructor.name;
351
+ result.inputType = (input.constructor.name || typeof input ).toLowerCase();
126
352
 
127
- return (constructor || type).toLowerCase();
353
+ return result;
128
354
  }
129
355
 
130
356
  function isNumberJson(str) {
@@ -142,22 +368,6 @@
142
368
 
143
369
  }
144
370
 
145
- const {
146
- abs: abs$1, acos: acos$1, asin: asin$1, atan: atan$1, atan2: atan2$1, ceil: ceil$1, cos: cos$1, exp: exp$1, floor: floor$1,
147
- log: log$1, hypot, max: max$1, min: min$1, pow: pow$1, random: random$1, round: round$1, sin: sin$1, sqrt: sqrt$1, tan: tan$1, PI: PI$1
148
- } = Math;
149
-
150
- const rad2Deg = 180/Math.PI;
151
- const deg2rad = Math.PI/180;
152
- const root2 = 1.4142135623730951;
153
- const svgNs = 'http://www.w3.org/2000/svg';
154
-
155
- // 1/2.54
156
- const inch2cm = 0.39370078;
157
-
158
- // 1/72
159
- const inch2pt = 0.01388889;
160
-
161
371
  /*
162
372
  import {abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
163
373
  log, max, min, pow, random, round, sin, sqrt, tan, PI} from '/.constants.js';
@@ -412,6 +622,7 @@
412
622
  let t1 = 1 - t;
413
623
 
414
624
  // cubic beziers
625
+ /*
415
626
  if (isCubic) {
416
627
  pt = {
417
628
  x:
@@ -427,11 +638,29 @@
427
638
  };
428
639
 
429
640
  }
641
+ */
642
+
643
+ if (isCubic) {
644
+ pt = {
645
+ x:
646
+ t1 * t1 * t1 * p0.x +
647
+ 3 * t1 * t1 * t * cp1.x +
648
+ 3 * t1 * t * t * cp2.x +
649
+ t * t * t * p.x,
650
+ y:
651
+ t1 * t1 * t1 * p0.y +
652
+ 3 * t1 * t1 * t * cp1.y +
653
+ 3 * t1 * t * t * cp2.y +
654
+ t * t * t * p.y,
655
+ };
656
+
657
+ }
658
+
430
659
  // quadratic beziers
431
660
  else {
432
661
  pt = {
433
- x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
434
- y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t ** 2 * p.y,
662
+ x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t * t * p.x,
663
+ y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t * t * p.y,
435
664
  };
436
665
  }
437
666
 
@@ -747,12 +976,6 @@
747
976
  return tArr;
748
977
  }
749
978
 
750
- /**
751
- * based on Nikos M.'s answer
752
- * how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
753
- * https://stackoverflow.com/questions/87734/#75031511
754
- * See also: https://github.com/foo123/Geometrize
755
- */
756
979
  function getArcExtemes(p0, values) {
757
980
  // compute point on ellipse from angle around ellipse (theta)
758
981
  const arc = (theta, cx, cy, rx, ry, alpha) => {
@@ -1621,6 +1844,36 @@
1621
1844
  return `#${rHex}${gHex}${bHex}${aHex}`;
1622
1845
  }
1623
1846
 
1847
+ /**
1848
+ * round path data
1849
+ * either by explicit decimal value or
1850
+ * based on suggested accuracy in path data
1851
+ */
1852
+ function roundPathData(pathData, decimalsGlobal = -1) {
1853
+
1854
+ if (decimalsGlobal < 0) return pathData;
1855
+
1856
+ let len = pathData.length;
1857
+ let decimals = decimalsGlobal;
1858
+ let decimalsArc = decimals < 3 ? decimals + 2 : decimals;
1859
+
1860
+ for (let c = 0; c < len; c++) {
1861
+ let com = pathData[c];
1862
+ let { type, values } = com;
1863
+ let valLen = values.length;
1864
+ if (!valLen) continue
1865
+
1866
+ let isArc = type.toLowerCase() === 'a';
1867
+
1868
+ for (let v = 0; v < valLen; v++) {
1869
+ // allow higher accuracy for arc radii (... it's always arcs)
1870
+ pathData[c].values[v] = isArc && v < 2 ? roundTo(values[v], decimalsArc) : roundTo(values[v], decimals);
1871
+ }
1872
+ }
1873
+
1874
+ return pathData;
1875
+ }
1876
+
1624
1877
  function detectAccuracyPoly(pts) {
1625
1878
  let dims = [];
1626
1879
 
@@ -1630,7 +1883,7 @@
1630
1883
  let { p0 = null, p = null, dimA = 0 } = pt;
1631
1884
 
1632
1885
  // use existing averave dimension value or calculate
1633
- if ( p && p0) {
1886
+ if (p && p0) {
1634
1887
  dimA = dimA ? dimA : getDistManhattan(p0, p);
1635
1888
 
1636
1889
  if (dimA) dims.push(dimA);
@@ -1662,29 +1915,71 @@
1662
1915
 
1663
1916
  dimA = dimA ? dimA : getDistManhattan(p0, p);
1664
1917
 
1665
- if (dimA) dims.push(dimA);
1918
+ if (dimA) dims.push(+dimA.toFixed(8));
1666
1919
 
1667
1920
  }
1668
1921
 
1669
1922
  }
1670
1923
 
1671
- let dim_min = dims.sort();
1924
+ dims = dims.sort();
1925
+ let len = dims.length;
1926
+ let dim_mid = dims[Math.floor(len*0.5)];
1927
+
1928
+ // smallest 25% of values
1929
+ let idx_q = Math.ceil(len*0.25);
1930
+ let dims_min = dims.slice(0, idx_q);
1931
+
1932
+ // average smallest values with mid value
1933
+ let dim_min = ((dims_min.reduce((a, b) => a + b, 0) / idx_q) + dim_mid) * 0.5;
1934
+
1935
+ let threshold = 75;
1936
+ let decimalsAuto = dim_min > threshold * 1.5 ? 0 : Math.floor(threshold / dim_min).toString().length;
1672
1937
 
1673
- let sliceIdx = Math.ceil(dim_min.length / 6);
1938
+ // clamp
1939
+ return Math.min(Math.max(0, decimalsAuto), 8)
1940
+
1941
+ /*
1942
+ let dim_min = dims.sort()
1943
+
1944
+ let dim_mid = dim_min[Math.floor(dim_min.length*0.5)]
1945
+
1946
+ let sliceIdx = Math.ceil(dim_min.length / 4);
1674
1947
  dim_min = dim_min.slice(0, sliceIdx);
1675
1948
  let minVal = dim_min.reduce((a, b) => a + b, 0) / sliceIdx;
1676
1949
 
1677
- let threshold = 75;
1678
- let decimalsAuto = minVal > threshold * 1.5 ? 0 : Math.floor(threshold / minVal).toString().length;
1950
+ // average with mid value
1951
+ minVal = (minVal+dim_mid)*0.5
1952
+
1953
+ let threshold = 75
1954
+ let decimalsAuto = minVal > threshold * 1.5 ? 0 : Math.floor(threshold / minVal).toString().length
1679
1955
 
1680
1956
  // clamp
1681
1957
  return Math.min(Math.max(0, decimalsAuto), 8)
1958
+ */
1682
1959
 
1683
1960
  }
1684
1961
 
1962
+ /**
1963
+ * rounding helper
1964
+ * allows for quantized rounding
1965
+ * e.g 0.5 decimals s
1966
+ */
1685
1967
  function roundTo(num = 0, decimals = 3) {
1686
- if(decimals<=-1) return num;
1968
+ if (decimals < 0) return num;
1969
+ // Normal integer rounding
1687
1970
  if (!decimals) return Math.round(num);
1971
+
1972
+ // stepped rounding
1973
+ let intPart = Math.floor(decimals);
1974
+
1975
+ if (intPart !== decimals) {
1976
+ let f = +(decimals - intPart).toFixed(2);
1977
+ f = f > 0.5 ? (Math.floor((f) / 0.5) * 0.5) : f;
1978
+
1979
+ let step = 10 ** -intPart * f;
1980
+ return +(Math.round(num / step) * step).toFixed(8);
1981
+ }
1982
+
1688
1983
  let factor = 10 ** decimals;
1689
1984
  return Math.round(num * factor) / factor;
1690
1985
  }
@@ -1694,50 +1989,21 @@
1694
1989
  * floating point accuracy
1695
1990
  * based on numeric value
1696
1991
  */
1697
- function autoRound(val, integerThresh = 50){
1698
- let decimals=8;
1699
-
1700
- if(val>integerThresh*2){
1701
- decimals=0;
1702
- }
1703
- else if(val>integerThresh){
1704
- decimals=1;
1705
- }else {
1706
- decimals=Math.ceil(500/val).toString().length;
1707
-
1708
- }
1709
-
1710
-
1711
- let factor = 10 ** decimals;
1712
- return Math.round(val * factor) / factor;
1713
- }
1714
-
1715
- /**
1716
- * round path data
1717
- * either by explicit decimal value or
1718
- * based on suggested accuracy in path data
1719
- */
1720
- function roundPathData(pathData, decimalsGlobal = -1) {
1721
-
1722
- if (decimalsGlobal < 0) return pathData;
1723
-
1724
- let len = pathData.length;
1992
+ function autoRound(val, integerThresh = 50) {
1993
+ let decimals = 8;
1725
1994
 
1726
- let decimals = decimalsGlobal;
1727
-
1728
- for (let c = 0; c < len; c++) {
1729
- let com = pathData[c];
1730
- let {values} = com;
1731
-
1732
- let valLen = values.length;
1733
- if (!valLen) continue
1995
+ if (val > integerThresh * 2) {
1996
+ decimals = 0;
1997
+ }
1998
+ else if (val > integerThresh) {
1999
+ decimals = 1;
2000
+ } else {
2001
+ decimals = Math.ceil(500 / val).toString().length;
1734
2002
 
1735
- for (let v = 0; v < valLen; v++) {
1736
- pathData[c].values[v] = roundTo(values[v], decimals);
1737
- }
1738
2003
  }
1739
2004
 
1740
- return pathData;
2005
+ let factor = 10 ** decimals;
2006
+ return Math.round(val * factor) / factor;
1741
2007
  }
1742
2008
 
1743
2009
  /**
@@ -2179,7 +2445,7 @@
2179
2445
  scale = height / 100;
2180
2446
  }
2181
2447
  else {
2182
- scale = normalizedDiagonal ? scaleRoot / 100 : 1;
2448
+ scale = normalizedDiagonal ? scaleRoot / 100 : width / 100;
2183
2449
  }
2184
2450
  break;
2185
2451
 
@@ -2248,12 +2514,14 @@
2248
2514
  }
2249
2515
 
2250
2516
  function getElementAtts(el, {x=0, y=0, width=0, height=0}={}){
2251
- let attributes = [...el.attributes];
2517
+
2518
+ let attributes = [...el.attributes].map(att=>att.name);
2252
2519
 
2253
2520
  let atts={};
2254
2521
  attributes.forEach(att=>{
2255
- let value = normalizeUnits(att.nodeValue, {x, y, width, height});
2256
- atts[att.name] = value;
2522
+
2523
+ let value = normalizeUnits(el.getAttribute(att), {x, y, width, height});
2524
+ atts[att] = value;
2257
2525
  });
2258
2526
 
2259
2527
  return atts
@@ -2998,7 +3266,7 @@
2998
3266
  error += com.error;
2999
3267
 
3000
3268
  // find next candidates
3001
- for (let n = i + 1; error < tolerance && n < l; n++) {
3269
+ for (let n = i + offset; error < tolerance && n < l; n++) {
3002
3270
  let comN = pathData[n];
3003
3271
 
3004
3272
  if (comN.type !== 'C' ||
@@ -3008,6 +3276,7 @@
3008
3276
  (keepExtremes && com.extreme)
3009
3277
  )
3010
3278
  ) {
3279
+
3011
3280
  break
3012
3281
  }
3013
3282
 
@@ -3015,6 +3284,7 @@
3015
3284
 
3016
3285
  // failure - could not be combined - exit loop
3017
3286
  if (combined.length > 1) {
3287
+
3018
3288
  break
3019
3289
  }
3020
3290
 
@@ -3028,6 +3298,7 @@
3028
3298
 
3029
3299
  // return combined
3030
3300
  com = combined[0];
3301
+
3031
3302
  }
3032
3303
 
3033
3304
  pathDataN.push(com);
@@ -3077,9 +3348,9 @@
3077
3348
  let comS = getExtrapolatedCommand(com1, com2, t);
3078
3349
 
3079
3350
  // test new point-at-t against original mid segment starting point
3080
- let pt = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
3351
+ let ptI = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
3081
3352
 
3082
- let dist0 = getDistManhattan(com1.p, pt);
3353
+ let dist0 = getDistManhattan(com1.p, ptI);
3083
3354
  let dist1 = 0, dist2 = 0;
3084
3355
  let close = dist0 < maxDist;
3085
3356
  let success = false;
@@ -3094,29 +3365,40 @@
3094
3365
  * to prevent distortions
3095
3366
  */
3096
3367
 
3097
- // 2nd segment mid
3098
- let pt_2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
3368
+ // 1st segment mid
3369
+ let ptM_seg1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
3099
3370
 
3100
- // simplified path
3101
- let t3 = (1 + t) * 0.5;
3102
- let ptS_2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
3103
- dist1 = getDistManhattan(pt_2, ptS_2);
3371
+ let t2 = t * 0.5;
3372
+ // combined interpolated mid point
3373
+ let ptI_seg1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
3374
+ dist1 = getDistManhattan(ptM_seg1, ptI_seg1);
3104
3375
 
3105
3376
  error += dist1;
3106
3377
 
3107
3378
  if (dist1 < maxDist) {
3108
3379
 
3109
- // 1st segment mid
3110
- let pt_1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
3380
+ // 2nd segment mid
3381
+ let ptM_seg2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
3111
3382
 
3112
- let t2 = t * 0.5;
3113
- let ptS_1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
3114
- dist2 = getDistManhattan(pt_1, ptS_1);
3383
+ // simplified path
3384
+ let t3 = (1 + t) * 0.5;
3385
+ let ptI_seg2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
3386
+ dist2 = getDistManhattan(ptM_seg2, ptI_seg2);
3115
3387
 
3116
3388
  error += dist2;
3117
3389
 
3118
3390
  if (error < maxDist) success = true;
3119
3391
 
3392
+ /*
3393
+ renderPoint(markers, ptM_seg1, 'cyan')
3394
+ renderPoint(markers, pt, 'orange', '1.5%', '1')
3395
+ renderPoint(markers, ptM_seg2, 'orange')
3396
+
3397
+ renderPoint(markers, com1.p, 'green')
3398
+
3399
+ renderPoint(markers, ptI_seg1, 'purple')
3400
+ */
3401
+
3120
3402
  }
3121
3403
 
3122
3404
  } // end 1st try
@@ -3130,11 +3412,19 @@
3130
3412
 
3131
3413
  comS.dimA = getDistManhattan(comS.p0, comS.p);
3132
3414
  comS.type = 'C';
3415
+
3133
3416
  comS.extreme = com2.extreme;
3134
3417
  comS.directionChange = com2.directionChange;
3135
-
3136
3418
  comS.corner = com2.corner;
3137
3419
 
3420
+ if (comS.extreme || comS.corner) ;
3421
+
3422
+ /*
3423
+ comS.extreme = com1.extreme;
3424
+ comS.directionChange = com1.directionChange;
3425
+ comS.corner = com1.corner;
3426
+ */
3427
+
3138
3428
  comS.values = [comS.cp1.x, comS.cp1.y, comS.cp2.x, comS.cp2.y, comS.p.x, comS.p.y];
3139
3429
 
3140
3430
  // relative error
@@ -3281,6 +3571,7 @@
3281
3571
  let com = pathData[c - 1];
3282
3572
  let { type, values, p0, p, cp1 = null, cp2 = null, squareDist = 0, cptArea = 0, dimA = 0 } = com;
3283
3573
 
3574
+ let comPrev = pathData[c-2];
3284
3575
  let comN = pathData[c] || null;
3285
3576
 
3286
3577
  // init properties
@@ -3299,6 +3590,7 @@
3299
3590
 
3300
3591
  // bezier types
3301
3592
  let isBezier = type === 'Q' || type === 'C';
3593
+ let isArc = type === 'A';
3302
3594
  let isBezierN = comN && (comN.type === 'Q' || comN.type === 'C');
3303
3595
 
3304
3596
  /**
@@ -3345,6 +3637,22 @@
3345
3637
  }
3346
3638
  }
3347
3639
 
3640
+ // check extremes introduce by small arcs
3641
+ else if(isArc && comN && ((comPrev.type==='C' || comPrev.type==='Q') || (comN.type==='C' || comN.type==='Q')) ){
3642
+ let distN = comN ? comN.dimA : 0;
3643
+ let isShort = com.dimA < (comPrev.dimA + distN) * 0.1;
3644
+ let smallRadius = com.values[0] === com.values[1] && (com.values[0] < 1);
3645
+
3646
+ if(isShort && smallRadius){
3647
+ let bb = getPolyBBox([comPrev.p0, comN.p]);
3648
+ if(p.x>bb.right || p.x<bb.x || p.y<bb.y || p.y>bb.bottom){
3649
+ hasExtremes = true;
3650
+
3651
+ }
3652
+ }
3653
+
3654
+ }
3655
+
3348
3656
  if (hasExtremes) com.extreme = true;
3349
3657
 
3350
3658
  // Corners and semi extremes
@@ -3902,50 +4210,10 @@
3902
4210
  return pathData.map(com => { return `${com.type} ${com.values.join(' ')}` }).join(' ');
3903
4211
  }
3904
4212
 
3905
- function parsePathDataNormalized(d,
3906
- {
3907
- // necessary for most calculations
3908
- toAbsolute = true,
3909
- toLonghands = true,
3910
-
3911
- // not necessary unless you need cubics only
3912
- quadraticToCubic = false,
3913
-
3914
- // mostly a fallback if arc calculations fail
3915
- arcToCubic = false,
3916
- // arc to cubic precision - adds more segments for better precision
3917
- arcAccuracy = 4,
3918
- } = {}
3919
- ) {
3920
-
3921
- // is already array
3922
- let isArray = Array.isArray(d);
3923
-
3924
- // normalize native pathData to regular array
3925
- let hasConstructor = isArray && typeof d[0] === 'object' && typeof d[0].constructor === 'function';
3926
- /*
3927
- if (hasConstructor) {
3928
- d = d.map(com => { return { type: com.type, values: com.values } })
3929
- console.log('hasConstructor', hasConstructor, (typeof d[0].constructor), d);
3930
- }
3931
- */
3932
-
3933
- let pathDataObj = isArray ? d : parsePathDataString(d);
3934
-
3935
- let { hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true } = pathDataObj;
3936
- let pathData = hasConstructor ? pathDataObj : pathDataObj.pathData;
3937
-
3938
- // normalize
3939
- pathData = normalizePathData(pathData,
3940
- {
3941
- toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy,
3942
- hasRelatives, hasShorthands, hasQuadratics, hasArcs
3943
- },
3944
- );
3945
-
3946
- return pathData;
3947
- }
3948
-
4213
+ /**
4214
+ * wrapper function for
4215
+ * all path data conversion
4216
+ */
3949
4217
  function convertPathData(pathData, {
3950
4218
  toShorthands = true,
3951
4219
  toLonghands = false,
@@ -3997,22 +4265,24 @@
3997
4265
 
3998
4266
  if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
3999
4267
 
4000
- if(toMixed) toRelative = true;
4268
+ if (toMixed) toRelative = true;
4001
4269
 
4002
4270
  // pre round - before relative conversion to minimize distortions
4003
4271
  if (decimals > -1 && toRelative) pathData = roundPathData(pathData, decimals);
4004
4272
 
4005
4273
  // clone absolute pathdata
4006
- if(toMixed){
4274
+ if (toMixed) {
4007
4275
  pathDataAbs = JSON.parse(JSON.stringify(pathData));
4008
4276
  }
4009
4277
 
4010
4278
  if (toRelative) pathData = pathDataToRelative(pathData);
4279
+
4280
+ // final rounding
4011
4281
  if (decimals > -1) pathData = roundPathData(pathData, decimals);
4012
4282
 
4013
4283
  // choose most compact commands: relative or absolute
4014
- if(toMixed){
4015
- for(let i=0; i<pathData.length; i++){
4284
+ if (toMixed) {
4285
+ for (let i = 0; i < pathData.length; i++) {
4016
4286
  let com = pathData[i];
4017
4287
  let comA = pathDataAbs[i];
4018
4288
  // compare Lengths
@@ -4022,7 +4292,7 @@
4022
4292
  let lenR = comStr.length;
4023
4293
  let lenA = comStrA.length;
4024
4294
 
4025
- if(lenA<lenR){
4295
+ if (lenA < lenR) {
4026
4296
 
4027
4297
  pathData[i] = pathDataAbs[i];
4028
4298
  }
@@ -4032,56 +4302,140 @@
4032
4302
  return pathData
4033
4303
  }
4034
4304
 
4035
- /**
4036
- *
4037
- * @param {*} pathData
4038
- * @returns
4305
+ function parsePathDataNormalized(d,
4306
+ {
4307
+ // necessary for most calculations
4308
+ toAbsolute = true,
4309
+ toLonghands = true,
4310
+
4311
+ // not necessary unless you need cubics only
4312
+ quadraticToCubic = false,
4313
+
4314
+ // mostly a fallback if arc calculations fail
4315
+ arcToCubic = false,
4316
+ // arc to cubic precision - adds more segments for better precision
4317
+ arcAccuracy = 4,
4318
+ } = {}
4319
+ ) {
4320
+
4321
+ // is already array
4322
+ let isArray = Array.isArray(d);
4323
+
4324
+ // normalize native pathData to regular array
4325
+ let hasConstructor = isArray && typeof d[0] === 'object' && typeof d[0].constructor === 'function';
4326
+ /*
4327
+ if (hasConstructor) {
4328
+ d = d.map(com => { return { type: com.type, values: com.values } })
4329
+ console.log('hasConstructor', hasConstructor, (typeof d[0].constructor), d);
4330
+ }
4331
+ */
4332
+
4333
+ let pathDataObj = isArray ? d : parsePathDataString(d);
4334
+
4335
+ let { hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true } = pathDataObj;
4336
+ let pathData = hasConstructor ? pathDataObj : pathDataObj.pathData;
4337
+
4338
+ // normalize
4339
+ pathData = normalizePathData(pathData,
4340
+ {
4341
+ toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy,
4342
+ hasRelatives, hasShorthands, hasQuadratics, hasArcs
4343
+ },
4344
+ );
4345
+
4346
+ return pathData;
4347
+ }
4348
+
4349
+ /**
4350
+ *
4351
+ * @param {*} pathData
4352
+ * @returns
4039
4353
  */
4040
4354
 
4041
4355
  function optimizeArcPathData(pathData = []) {
4356
+ let l = pathData.length;
4357
+ let pathDataN = [];
4042
4358
 
4043
- let remove =[];
4044
-
4045
- pathData.forEach((com, i) => {
4359
+ for (let i = 0; i < l; i++) {
4360
+ let com = pathData[i];
4046
4361
  let { type, values } = com;
4047
- if (type === 'A') {
4048
- let [rx, ry, largeArc, x, y] = [values[0], values[1], values[3], values[5], values[6]];
4049
- let comPrev = pathData[i - 1];
4050
- let [x0, y0] = [comPrev.values[comPrev.values.length - 2], comPrev.values[comPrev.values.length - 1]];
4051
- let M = { x: x0, y: y0 };
4052
- let p = { x, y };
4053
4362
 
4054
- if(rx===0 || ry===0){
4055
- pathData[i]= null;
4056
- remove.push(i);
4363
+ if (type !== 'A') {
4364
+ pathDataN.push(com);
4365
+ continue
4366
+ }
4367
+
4368
+ let [rx, ry, largeArc, x, y] = [values[0], values[1], values[3], values[5], values[6]];
4369
+ let comPrev = pathData[i - 1];
4370
+ let [x0, y0] = [comPrev.values[comPrev.values.length - 2], comPrev.values[comPrev.values.length - 1]];
4371
+ let M = { x: x0, y: y0 };
4372
+ let p = { x, y };
4057
4373
 
4058
- }
4374
+ if (rx === 0 || ry === 0) {
4375
+ pathData[i] = null;
4376
+ }
4059
4377
 
4060
- // rx and ry are large enough
4061
- if (rx >= 1 && (x === x0 || y === y0)) {
4062
- let diff = Math.abs(rx - ry) / rx;
4378
+ // test for elliptic
4379
+ let rat = rx / ry;
4380
+ let error = rx !== ry ? Math.abs(1 - rat) : 0;
4063
4381
 
4064
- // rx~==ry
4065
- if (diff < 0.01) {
4382
+ if (error > 0.01) {
4066
4383
 
4067
- // test radius against mid point
4068
- let pMid = interpolate(M, p, 0.5);
4069
- let distM = getDistance(pMid, M);
4070
- let rDiff = Math.abs(distM - rx) / rx;
4384
+ pathDataN.push(com);
4385
+ continue
4071
4386
 
4072
- // half distance between mid and start point should be ~ equal
4073
- if(rDiff<0.01){
4074
- pathData[i].values[0] = 1;
4075
- pathData[i].values[1] = 1;
4076
- pathData[i].values[2] = 0;
4077
- }
4078
- }
4387
+ }
4388
+
4389
+ // xAxis rotation is futile for circular arcs - reset
4390
+ com.values[2] = 0;
4391
+
4392
+ /**
4393
+ * test semi circles
4394
+ * rx and ry are large enough
4395
+ */
4396
+
4397
+ // 1. horizontal or vertical
4398
+ let thresh = getDistManhattan(M, p) * 0.001;
4399
+ let diffX = Math.abs(x - x0);
4400
+ let diffY = Math.abs(y - y0);
4401
+
4402
+ let isHorizontal = diffY < thresh;
4403
+ let isVertical = diffX < thresh;
4404
+
4405
+ // minify rx and ry
4406
+ if (isHorizontal || isVertical) {
4407
+
4408
+ // check if semi circle
4409
+ let needsTrueR = isHorizontal ? rx*1.9 > diffX : ry*1.9 > diffY;
4410
+
4411
+ // is semicircle we can simplify rx
4412
+ if (!needsTrueR) {
4413
+
4414
+ rx = rx >= 1 ? 1 : (rx > 0.5 ? 0.5 : rx);
4079
4415
  }
4416
+
4417
+ com.values[0] = rx;
4418
+ com.values[1] = rx;
4419
+ pathDataN.push(com);
4420
+ continue
4421
+
4080
4422
  }
4081
- });
4082
4423
 
4083
- if(remove.length) pathData = pathData.filter(Boolean);
4084
- return pathData;
4424
+ // 2. get true radius - if rx ~= diameter/distance we have a semicircle
4425
+ let r = getDistance(M, p) * 0.5;
4426
+ error = rx / r;
4427
+
4428
+ if (error < 0.5) {
4429
+ rx = r >= 1 ? 1 : (r > 0.5 ? 0.5 : r);
4430
+ }
4431
+
4432
+ com.values[0] = rx;
4433
+ com.values[1] = rx;
4434
+ pathDataN.push(com);
4435
+
4436
+ }
4437
+
4438
+ return pathDataN;
4085
4439
  }
4086
4440
 
4087
4441
  /**
@@ -4146,6 +4500,44 @@
4146
4500
  }
4147
4501
  */
4148
4502
 
4503
+ function convertSmallArcsToLinetos(pathData) {
4504
+
4505
+ let l = pathData.length;
4506
+
4507
+ // add fist command
4508
+ let pathDataN = [pathData[0]];
4509
+
4510
+ for (let i = 1; i < l; i++) {
4511
+ let com = pathData[i];
4512
+ let comPrev = pathData[i - 1];
4513
+ let comN = pathData[i + 1] || null;
4514
+
4515
+ if (!comN) {
4516
+ pathDataN.push(com);
4517
+ break
4518
+ }
4519
+
4520
+ let { type, values, extreme = false, p0, p, dimA = 0 } = com;
4521
+ // for short segment detection
4522
+ let dimAN = comN.dimA;
4523
+ let dimA0 = comPrev.dimA + dimA + dimAN;
4524
+ let thresh = 0.05;
4525
+ let isShort = dimA < dimA0 * thresh;
4526
+
4527
+ if (type === 'A' && isShort && values[0] < 1 && values[1] < 1) {
4528
+
4529
+ com.type = 'L';
4530
+ com.values = [p.x, p.y];
4531
+ }
4532
+
4533
+ pathDataN.push(com);
4534
+
4535
+ }
4536
+
4537
+ return pathDataN;
4538
+
4539
+ }
4540
+
4149
4541
  function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}, tolerance = 1) {
4150
4542
 
4151
4543
  // test if cubic can be simplified to quadratic
@@ -6201,7 +6593,8 @@
6201
6593
  let { type, values } = com;
6202
6594
  let valsLen = values.length;
6203
6595
  if (valsLen) {
6204
- let p = { type: type, x: values[valsLen-2], y: values[valsLen-1], index: 0};
6596
+ // we need rounding otherwise sorting may crash due to e notation
6597
+ let p = { type: type, x: +values[valsLen - 2].toFixed(8), y: +values[valsLen - 1].toFixed(8), index: 0 };
6205
6598
  p.index = i;
6206
6599
  indices.push(p);
6207
6600
  }
@@ -6209,113 +6602,111 @@
6209
6602
 
6210
6603
  // reorder to top left most
6211
6604
 
6212
- indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x-b.x );
6605
+ indices = indices.sort((a, b) => a.y - b.y || a.x - b.x);
6213
6606
  newIndex = indices[0].index;
6214
6607
 
6215
- return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
6608
+ return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
6216
6609
  }
6217
6610
 
6218
- function optimizeClosePath(pathData, {removeFinalLineto = true, autoClose = true}={}) {
6611
+ function optimizeClosePath(pathData, { removeFinalLineto = true, autoClose = true } = {}) {
6219
6612
 
6220
- let pathDataNew = [];
6613
+ let pathDataN = pathData;
6221
6614
  let l = pathData.length;
6222
6615
  let M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
6223
6616
  let isClosed = pathData[l - 1].type.toLowerCase() === 'z';
6224
6617
 
6225
- let linetos = pathData.filter(com => com.type === 'L');
6226
-
6227
- // check if order is ideal
6228
- let idxPenultimate = isClosed ? l-2 : l-1;
6618
+ let hasLinetos = false;
6229
6619
 
6620
+ // check if path is closed by explicit lineto
6621
+ let idxPenultimate = isClosed ? l - 2 : l - 1;
6230
6622
  let penultimateCom = pathData[idxPenultimate];
6231
6623
  let penultimateType = penultimateCom.type;
6232
6624
  let penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
6233
6625
 
6234
6626
  // last L command ends at M
6235
- let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
6627
+ let hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
6628
+ let lastIsLine = penultimateType === 'L';
6236
6629
 
6237
- // add closepath Z to enable order optimizations
6238
- if(!isClosed && autoClose && isClosingCommand){
6239
-
6240
- /*
6241
- // adjust final coords
6242
- let valsLast = pathData[idxPenultimate].values
6243
- let valsLastLen = valsLast.length;
6244
- pathData[idxPenultimate].values[valsLastLen-2] = M.x
6245
- pathData[idxPenultimate].values[valsLastLen-1] = M.y
6246
- */
6247
-
6248
- pathData.push({type:'Z', values:[]});
6249
- isClosed = true;
6250
- l++;
6251
- }
6630
+ // create index
6631
+ let indices = [];
6632
+ for (let i = 0; i < l; i++) {
6633
+ let com = pathData[i];
6634
+ let { type, values, p0, p } = com;
6252
6635
 
6253
- // if last segment is not closing or a lineto
6254
- let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand || penultimateCom.type === 'L');
6255
- skipReorder = false;
6636
+ if(type==='L') hasLinetos = true;
6256
6637
 
6257
- // we can't change starting point for non closed paths
6258
- if (!isClosed) {
6259
- return pathData
6260
- }
6638
+ // exclude Z
6639
+ if (values.length) {
6640
+ values.slice(-2);
6261
6641
 
6262
- let newIndex = 0;
6642
+ let x = Math.min(p0.x, p.x);
6643
+ let y = Math.min(p0.y, p.y);
6263
6644
 
6264
- if (!skipReorder) {
6645
+ let prevCom = pathData[i - 1] ? pathData[i - 1] : pathData[idxPenultimate];
6646
+ let prevType = prevCom.type;
6265
6647
 
6266
- let indices = [];
6267
- for (let i = 0; i < l; i++) {
6268
- let com = pathData[i];
6269
- let { type, values } = com;
6270
- if (values.length) {
6271
- let valsL = values.slice(-2);
6272
- let prevL = pathData[i - 1] && pathData[i - 1].type === 'L';
6273
- let nextL = pathData[i + 1] && pathData[i + 1].type === 'L';
6274
- let prevCom = pathData[i - 1] ? pathData[i - 1].type.toUpperCase() : null;
6275
- let nextCom = pathData[i + 1] ? pathData[i + 1].type.toUpperCase() : null;
6276
- let p = { type: type, x: valsL[0], y: valsL[1], dist: 0, index: 0, prevL, nextL, prevCom, nextCom };
6277
- p.index = i;
6278
- indices.push(p);
6279
- }
6648
+ let item = { type: type, x, y, index: 0, prevType };
6649
+ item.index = i;
6650
+ indices.push(item);
6280
6651
  }
6281
6652
 
6282
- // find top most lineto
6653
+ }
6283
6654
 
6284
- if (linetos.length) {
6285
- let curveAfterLine = indices.filter(com => (com.type !== 'L' && com.type !== 'M') && com.prevCom &&
6286
- com.prevCom === 'L' || com.prevCom === 'M' && penultimateType === 'L').sort((a, b) => a.y - b.y || a.x - b.x)[0];
6655
+ let xMin = Infinity;
6656
+ let yMin = Infinity;
6657
+ let idx_top = null;
6658
+ let len = indices.length;
6287
6659
 
6288
- newIndex = curveAfterLine ? curveAfterLine.index - 1 : 0;
6660
+ for (let i = 0; i < len; i++) {
6661
+ let com = indices[i];
6662
+ let { type, index, x, y, prevType } = com;
6289
6663
 
6290
- }
6291
- // use top most command
6292
- else {
6293
- indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x - b.x);
6294
- newIndex = indices[0].index;
6295
- }
6664
+ if (hasLinetos && prevType === 'L') {
6665
+ if (x < xMin && y < yMin) {
6666
+ idx_top = index-1;
6667
+ }
6668
+
6669
+ if (y < yMin) {
6670
+ yMin = y;
6671
+ }
6296
6672
 
6297
- // reorder
6298
- pathData = newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
6673
+ if (x < xMin) {
6674
+ xMin = x;
6675
+ }
6676
+ }
6299
6677
  }
6300
6678
 
6301
- M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
6679
+ // shift to better starting point
6680
+ if (idx_top) {
6681
+ pathDataN = shiftSvgStartingPoint(pathDataN, idx_top);
6302
6682
 
6303
- l = pathData.length;
6683
+ // update penultimate - reorder might have added new close paths
6684
+ l = pathDataN.length;
6685
+ M = { x: +pathDataN[0].values[0].toFixed(8), y: +pathDataN[0].values[1].toFixed(8) };
6686
+
6687
+ idxPenultimate = isClosed ? l - 2 : l - 1;
6688
+ penultimateCom = pathDataN[idxPenultimate];
6689
+ penultimateType = penultimateCom.type;
6690
+ penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
6691
+ lastIsLine = penultimateType ==='L';
6304
6692
 
6305
- // remove last lineto
6306
- penultimateCom = pathData[l - 2];
6307
- penultimateType = penultimateCom.type;
6308
- penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8));
6693
+ // last L command ends at M
6694
+ hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
6309
6695
 
6310
- isClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
6696
+ }
6311
6697
 
6312
- if (removeFinalLineto && isClosingCommand) {
6313
- pathData.splice(l - 2, 1);
6698
+ // remove unnecessary closing lineto
6699
+ if (removeFinalLineto && hasClosingCommand && lastIsLine) {
6700
+ pathDataN.splice(l - 2, 1);
6314
6701
  }
6315
6702
 
6316
- pathDataNew.push(...pathData);
6703
+ // add close path
6704
+ if (autoClose && !isClosed && hasClosingCommand) {
6705
+ pathDataN.push({ type: 'Z', values: [] });
6706
+ }
6707
+
6708
+ return pathDataN
6317
6709
 
6318
- return pathDataNew
6319
6710
  }
6320
6711
 
6321
6712
  /**
@@ -6747,7 +7138,7 @@
6747
7138
  } = {}) {
6748
7139
 
6749
7140
  // is stringified flat point attribute
6750
- if(typeof pts === 'string' && !isNaN(pts[0])){
7141
+ if (typeof pts === 'string' && !isNaN(pts[0])) {
6751
7142
  pts = toPointArray(pts.split(/,| /).filter(Boolean).map(Number));
6752
7143
  return pts
6753
7144
  }
@@ -6757,8 +7148,9 @@
6757
7148
  return poly
6758
7149
  }
6759
7150
 
6760
- function polyArrayToObject(pts) {
7151
+ function polyArrayToObject(pts = []) {
6761
7152
 
7153
+ if (!pts.length) return [];
6762
7154
  // is point object array
6763
7155
  if (pts[0].x !== undefined && pts[0].y !== undefined) return pts
6764
7156
 
@@ -6776,7 +7168,7 @@
6776
7168
  return poly
6777
7169
  }
6778
7170
 
6779
- else if(pts.length>3){
7171
+ else if (pts.length > 3) {
6780
7172
  pts = toPointArray(pts);
6781
7173
  return pts
6782
7174
  }
@@ -6805,13 +7197,13 @@
6805
7197
  function toPointArray(pts) {
6806
7198
  let ptArr = [];
6807
7199
 
6808
- if(pts[0].length===2){
6809
- for (let i = 0, l = pts.length; i < l; i ++) {
7200
+ if (pts[0].length === 2) {
7201
+ for (let i = 0, l = pts.length; i < l; i++) {
6810
7202
  let pt = pts[i];
6811
- ptArr.push({ x: pt[0], y:pt[1] });
7203
+ ptArr.push({ x: pt[0], y: pt[1] });
6812
7204
  }
6813
7205
 
6814
- }else {
7206
+ } else {
6815
7207
  for (let i = 1, l = pts.length; i < l; i += 2) {
6816
7208
  ptArr.push({ x: pts[i - 1], y: pts[i] });
6817
7209
  }
@@ -6828,7 +7220,7 @@
6828
7220
 
6829
7221
  switch(type){
6830
7222
  case 'path':
6831
- let pathData = parsePathDataNormalized(atts.d);
7223
+ let pathData = parsePathDataNormalized(el.getAttribute('d'));
6832
7224
  bb=getPolyBBox(getPathDataPoly(pathData));
6833
7225
 
6834
7226
  break;
@@ -6870,8 +7262,8 @@
6870
7262
  autoRoundValues = false,
6871
7263
  minifyRgbColors = false,
6872
7264
  removeInvalid = true,
6873
- allowDataAtts=true,
6874
- allowAriaAtts=true,
7265
+ allowDataAtts = true,
7266
+ allowAriaAtts = true,
6875
7267
  removeDefaults = true,
6876
7268
  cleanUpStrokes = true,
6877
7269
  normalizeTransforms = true,
@@ -6937,7 +7329,7 @@
6937
7329
  */
6938
7330
 
6939
7331
  if (removeInvalid || removeDefaults || removeNameSpaced) {
6940
- let propsFilteredObj = filterSvgElProps(nodeName, props, {allowDataAtts, allowAriaAtts, removeIds, removeClassNames, removeDefaults, removeNameSpaced, exclude, cleanUpStrokes, include: [...transformsStandalone, ...include], cleanUpStrokes: false });
7332
+ let propsFilteredObj = filterSvgElProps(nodeName, props, { allowDataAtts, allowAriaAtts, removeIds, removeClassNames, removeDefaults, removeNameSpaced, exclude, cleanUpStrokes, include: [...transformsStandalone, ...include], cleanUpStrokes: false });
6941
7333
  props = propsFilteredObj.propsFiltered;
6942
7334
  remove.push(...propsFilteredObj.remove);
6943
7335
 
@@ -7030,10 +7422,10 @@
7030
7422
 
7031
7423
  if (prop !== 'transforms') {
7032
7424
 
7033
- if (cleanUpStrokes && (prop === 'stroke-dasharray' || prop === 'stroke-dashoffset')) {
7425
+ if ((prop === 'stroke-dasharray' || prop === 'stroke-dashoffset')) {
7034
7426
  normalizedDiagonal = true;
7035
7427
  for (let i = 0; i < values.length; i++) {
7036
- let val = normalizeUnits(values[i].value, { unit: values[i].unit, width, height, normalizedDiagonal, fontSize });
7428
+ let val = normalizeUnits(values[i].value, { unit: values[i].unit, width, height, normalizedDiagonal, fontSize, autoRoundValues });
7037
7429
  valsNew.push(val);
7038
7430
  }
7039
7431
  }
@@ -7075,14 +7467,12 @@
7075
7467
  if (prop === 'scale' && unit === '%') {
7076
7468
  valAbs = valAbs * 0.01;
7077
7469
  } else {
7078
- if (prop === 'r') normalizedDiagonal = true;
7470
+ if (prop === 'r' && width!==height) normalizedDiagonal = true;
7079
7471
  valAbs = normalizeUnits(val.value, { unit, width, height, isHorizontal, isVertical, normalizedDiagonal, fontSize });
7080
7472
 
7081
7473
  if (autoRoundValues && isNumeric) {
7082
7474
  valAbs = autoRound(valAbs);
7083
-
7084
7475
  }
7085
-
7086
7476
  }
7087
7477
  }
7088
7478
  valsNew.push(valAbs);
@@ -7283,6 +7673,7 @@
7283
7673
  removeIds = false,
7284
7674
  removeClassNames = false,
7285
7675
  exclude = [],
7676
+ inheritedProps = null,
7286
7677
  } = {}) {
7287
7678
  let propsFiltered = {};
7288
7679
  let remove = [];
@@ -7311,7 +7702,7 @@
7311
7702
  let isMeta = prop === 'title';
7312
7703
  let isAria = prop.startsWith('aria-');
7313
7704
 
7314
- if( (allowDataAtts && isDataAtt) || (allowAriaAtts && isAria) || (allowMeta && isMeta ) ) continue
7705
+ if ((allowDataAtts && isDataAtt) || (allowAriaAtts && isAria) || (allowMeta && isMeta)) continue
7315
7706
 
7316
7707
  // filter out defaults
7317
7708
  let isDefault = removeDefaults ?
@@ -7322,6 +7713,7 @@
7322
7713
 
7323
7714
  if (isDefault || isDataAtt || isMeta || isAria || isFutileStroke) isValid = false;
7324
7715
  if (include.includes(prop)) isValid = true;
7716
+ if (exclude.includes(prop)) isValid = false;
7325
7717
 
7326
7718
  if (isValid) {
7327
7719
  propsFiltered[prop] = props[prop];
@@ -7554,6 +7946,357 @@
7554
7946
  return "";
7555
7947
  }
7556
7948
 
7949
+ // Legendre Gauss weight and abscissa values
7950
+ const waArr_global = [];
7951
+
7952
+ function getLength(pts, {
7953
+ t = 1,
7954
+ waArr = []
7955
+ } = {}) {
7956
+
7957
+ const cubicBezierLength = (p0, cp1, cp2, p, t = 0, wa = []) => {
7958
+ if (t === 0) {
7959
+ return 0;
7960
+ }
7961
+
7962
+ t = t > 1 ? 1 : t < 0 ? 0 : t;
7963
+ let t2 = t / 2;
7964
+
7965
+ /**
7966
+ * set higher legendre gauss weight abscissae values
7967
+ * by more accurate weight/abscissae lookups
7968
+ * https://pomax.github.io/bezierinfo/legendre-gauss.html
7969
+ */
7970
+
7971
+ let sum = 0;
7972
+
7973
+ 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;
7974
+
7975
+ for (let i = 0, len = wa.length; i < len; i++) {
7976
+ // weight and abscissae
7977
+ let [w, a] = [wa[i][0], wa[i][1]];
7978
+ let ct1_t = t2 * a;
7979
+ let ct0 = -ct1_t + t2;
7980
+
7981
+ let xbase0 = base3(ct0, x0, cp1x, cp2x, px);
7982
+ let ybase0 = base3(ct0, y0, cp1y, cp2y, py);
7983
+
7984
+ let comb0 = xbase0 * xbase0 + ybase0 * ybase0;
7985
+
7986
+ sum += w * Math.sqrt(comb0);
7987
+
7988
+ }
7989
+ return t2 * sum;
7990
+ };
7991
+
7992
+ const quadraticBezierLength = (p0, cp1, p, t, checkFlat = false) => {
7993
+ if (t === 0) {
7994
+ return 0;
7995
+ }
7996
+ // is flat/linear – treat as line
7997
+ if (checkFlat) {
7998
+ let l1 = getDistance(p0, cp1) + getDistance(cp1, p);
7999
+ let l2 = getDistance(p0, p);
8000
+ if (l1 === l2) {
8001
+ return l2;
8002
+ }
8003
+ }
8004
+
8005
+ let a, b, c, d, e, e1, d1, v1x, v1y;
8006
+ v1x = cp1.x * 2;
8007
+ v1y = cp1.y * 2;
8008
+ d = p0.x - v1x + p.x;
8009
+ d1 = p0.y - v1y + p.y;
8010
+ e = v1x - 2 * p0.x;
8011
+ e1 = v1y - 2 * p0.y;
8012
+ a = 4 * (d * d + d1 * d1);
8013
+ b = 4 * (d * e + d1 * e1);
8014
+ c = e * e + e1 * e1;
8015
+
8016
+ const bt = b / (2 * a),
8017
+ ct = c / a,
8018
+ ut = t + bt,
8019
+
8020
+ k = ct - bt * bt;
8021
+
8022
+ return (
8023
+ (Math.sqrt(a) / 2) *
8024
+ (ut * Math.sqrt(ut * ut + k) -
8025
+ bt * Math.sqrt(bt * bt + k) +
8026
+ k *
8027
+ Math.log((ut + Math.sqrt(ut * ut + k)) / (bt + Math.sqrt(bt * bt + k))))
8028
+ );
8029
+ };
8030
+
8031
+ let length;
8032
+ if (pts.length === 4) {
8033
+ length = cubicBezierLength(pts[0], pts[1], pts[2], pts[3], t, waArr);
8034
+
8035
+ }
8036
+ else if (pts.length === 3) {
8037
+ length = quadraticBezierLength(pts[0], pts[1], pts[2], t);
8038
+ }
8039
+ else {
8040
+ length = getDistance(pts[0], pts[1]);
8041
+ }
8042
+
8043
+ return length;
8044
+ }
8045
+
8046
+ // LG weight/abscissae generator
8047
+ function getLegendreGaussValues(n, x1 = -1, x2 = 1) {
8048
+
8049
+ let waArr = [];
8050
+ let z1, z, xm, xl, pp, p3, p2, p1;
8051
+ const m = (n + 1) >> 1;
8052
+ xm = 0.5 * (x2 + x1);
8053
+ xl = 0.5 * (x2 - x1);
8054
+
8055
+ for (let i = m - 1; i >= 0; i--) {
8056
+ z = Math.cos((Math.PI * (i + 0.75)) / (n + 0.5));
8057
+ do {
8058
+ p1 = 1;
8059
+ p2 = 0;
8060
+ for (let j = 0; j < n; j++) {
8061
+
8062
+ p3 = p2;
8063
+ p2 = p1;
8064
+ p1 = ((2 * j + 1) * z * p2 - j * p3) / (j + 1);
8065
+ }
8066
+
8067
+ pp = (n * (z * p1 - p2)) / (z * z - 1);
8068
+ z1 = z;
8069
+ z = z1 - p1 / pp; //Newton’s method
8070
+
8071
+ } while (Math.abs(z - z1) > 1.0e-14);
8072
+
8073
+ let weight = (2 * xl) / ((1 - z * z) * pp * pp);
8074
+ let abscissa = xm + xl * z;
8075
+
8076
+ waArr.push(
8077
+ [weight, -abscissa],
8078
+ [weight, abscissa],
8079
+ );
8080
+ }
8081
+
8082
+ return waArr;
8083
+ }
8084
+
8085
+ function base3(t, p1, p2, p3, p4) {
8086
+ let t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4,
8087
+ t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
8088
+ return t * t2 - 3 * p1 + 3 * p2;
8089
+ }
8090
+
8091
+ function getPolygonLength(pts=[], isPoly=false){
8092
+
8093
+ let len = 0;
8094
+ let l=pts.length;
8095
+
8096
+ for(let i=1; i<l; i++){
8097
+ let p1 = pts[i-1];
8098
+ let p2 = pts[i];
8099
+ len += getDistance(p1, p2);
8100
+ }
8101
+ if(isPoly){
8102
+ len += getDistance(pts[l-1], pts[0]);
8103
+ }
8104
+ return len
8105
+ }
8106
+
8107
+ /**
8108
+ * Ramanujan approximation
8109
+ * based on: https://www.mathsisfun.com/geometry/ellipse-perimeter.html#tool
8110
+ */
8111
+ function getEllipseLength(rx=0, ry=0) {
8112
+ // is circle
8113
+ if (rx === ry) {
8114
+
8115
+ return 2 * Math.PI * rx;
8116
+ }
8117
+
8118
+ let c=rx+ry;
8119
+ let d = (rx - ry) / c;
8120
+ let h = d*d;
8121
+
8122
+ let totalLength = Math.PI * c * (1 + 3 * h / (10 + Math.sqrt(4 - 3 * h) ));
8123
+ return totalLength;
8124
+ }
8125
+
8126
+ /**
8127
+ * ellipse helpers
8128
+ * approximate ellipse length
8129
+ * by Legendre-Gauss
8130
+ */
8131
+
8132
+ function getCircleArcLength(r = 0, deltaAngle = 0) {
8133
+ if(r===0) {
8134
+ console.warn('Radius must be positive');
8135
+ return 0;
8136
+ }
8137
+ let len = 2 * Math.PI * r * (1 / 360 * Math.abs(deltaAngle * 180 / Math.PI));
8138
+ return len
8139
+ }
8140
+
8141
+ function getEllipseLengthLG(rx, ry, startAngle, endAngle, wa = []) {
8142
+
8143
+ // Transform [-1, 1] interval to [startAngle, endAngle]
8144
+ let halfInterval = (endAngle - startAngle) * 0.5;
8145
+ let midpoint = (endAngle + startAngle) * 0.5;
8146
+
8147
+ // Arc length integral approximation
8148
+ let arcLength = 0;
8149
+ for (let i = 0; i < wa.length; i++) {
8150
+ let [weight, abscissae] = wa[i];
8151
+ let theta = midpoint + halfInterval * abscissae;
8152
+
8153
+ let a = rx * Math.sin(theta);
8154
+ let b = ry * Math.cos(theta);
8155
+ let integrand = Math.sqrt(
8156
+ a * a + b * b
8157
+ );
8158
+ arcLength += weight * integrand;
8159
+ }
8160
+
8161
+ return Math.abs(halfInterval * arcLength)
8162
+ }
8163
+
8164
+ function getPathDataLength(pathData = []) {
8165
+ let len = 0;
8166
+ let pathDataArr = splitSubpaths(pathData);
8167
+
8168
+ for (let i = 0; i < pathDataArr.length; i++) {
8169
+ let pathData = pathDataArr[i];
8170
+
8171
+ // add verbose point data if not present
8172
+ if (pathData[0].p === undefined) pathData = getPathDataVerbose(pathData);
8173
+
8174
+ // Calculate Legendre Gauss weight and abscissa values
8175
+ if (!waArr_global.length) {
8176
+
8177
+ let waArr = getLegendreGaussValues(48);
8178
+ waArr.forEach(wa => {
8179
+ waArr_global.push(wa);
8180
+ });
8181
+ }
8182
+
8183
+ let waArr = waArr_global;
8184
+
8185
+ pathData.forEach(com => {
8186
+ let { type, values, p0, p, cp1 = null, cp2 = null } = com;
8187
+ let pts = [p0];
8188
+ if (type === 'C' || type === 'Q') pts.push(cp1);
8189
+ if (type === 'C') pts.push(cp2);
8190
+ pts.push(p);
8191
+ let comLen = 0;
8192
+
8193
+ if (type === 'A') {
8194
+
8195
+ // get parametrized arc properties
8196
+ let [largeArc, sweep] = [com.values[3], com.values[4]];
8197
+ let arcData = svgArcToCenterParam(p0.x, p0.y, com.values[0], com.values[1], com.values[2], largeArc, sweep, p.x, p.y, false);
8198
+ let { cx, cy, rx, ry, startAngle, endAngle, deltaAngle, xAxisRotation } = arcData;
8199
+
8200
+ if (rx === ry) {
8201
+ comLen = getCircleArcLength(rx, Math.abs(deltaAngle));
8202
+ }
8203
+
8204
+ // is ellipse
8205
+ else {
8206
+ xAxisRotation = xAxisRotation * deg2rad;
8207
+ startAngle = toParametricAngle((startAngle - xAxisRotation), rx, ry);
8208
+ endAngle = toParametricAngle((endAngle - xAxisRotation), rx, ry);
8209
+
8210
+ // recalculate parametrized delta
8211
+ let deltaAngle_param = endAngle - startAngle;
8212
+
8213
+ let signChange = deltaAngle > 0 && deltaAngle_param < 0 || deltaAngle < 0 && deltaAngle_param > 0;
8214
+
8215
+ deltaAngle = signChange ? deltaAngle : deltaAngle_param;
8216
+
8217
+ // adjust end angle
8218
+ if (sweep && startAngle > endAngle) {
8219
+ endAngle += Math.PI * 2;
8220
+ }
8221
+
8222
+ if (!sweep && startAngle < endAngle) {
8223
+ endAngle -= Math.PI * 2;
8224
+ }
8225
+ comLen = getEllipseLengthLG(rx, ry, startAngle, endAngle, waArr);
8226
+ }
8227
+ }
8228
+
8229
+ else {
8230
+ comLen = getLength(pts, {
8231
+ t: 1,
8232
+ waArr
8233
+ });
8234
+ }
8235
+ len += comLen;
8236
+ });
8237
+ }
8238
+
8239
+ return len;
8240
+ }
8241
+
8242
+ function getElementLength(el, {
8243
+ props = {},
8244
+ pathLength = 0,
8245
+ } = {}) {
8246
+
8247
+ let nodeName = el.nodeName;
8248
+ let len = 0;
8249
+
8250
+ props = JSON.parse(JSON.stringify(props));
8251
+
8252
+ for (let prop in props) {
8253
+ if (props[prop] && props[prop].length && props[prop].length === 1) {
8254
+ props[prop] = props[prop][0];
8255
+
8256
+ }
8257
+ }
8258
+
8259
+ 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;
8260
+
8261
+ let pts = nodeName === 'polygon' || nodeName === 'polyline' ? el.getAttribute('points') : [];
8262
+ let isPolygon = nodeName === 'polygon';
8263
+ if (pts.length) {
8264
+ pts = normalizePoly(pts);
8265
+ }
8266
+
8267
+ // we need to convert rects with corner rounding
8268
+ let pathData = [];
8269
+ if (nodeName === 'rect' && (rx || ry)) {
8270
+ pathData = rectToPathData(x, y, width, height, rx, ry);
8271
+ nodeName = 'path';
8272
+ }
8273
+
8274
+ switch (nodeName) {
8275
+ case 'line':
8276
+ len = getDistance({ x: x1, y: y1 }, { x: x2, y: y2 });
8277
+ break;
8278
+ case 'rect':
8279
+ len = width * 2 + height * 2;
8280
+ break;
8281
+ case 'circle':
8282
+ len = 2 * Math.PI * r;
8283
+ break;
8284
+ case 'ellipse':
8285
+ len = getEllipseLength(rx, ry);
8286
+ break;
8287
+ case 'polygon':
8288
+ case 'polyline':
8289
+ len = getPolygonLength(pts, isPolygon);
8290
+ break;
8291
+ case 'path':
8292
+ pathData = pathData.length ? pathData : parsePathDataNormalized(el.getAttribute('d'));
8293
+ len = getPathDataLength(pathData);
8294
+ break;
8295
+ }
8296
+
8297
+ return len
8298
+ }
8299
+
7557
8300
  function removeHiddenSvgEls(svg) {
7558
8301
  let els = svg.querySelectorAll('*');
7559
8302
  els.forEach(el => {
@@ -7602,8 +8345,12 @@
7602
8345
  */
7603
8346
 
7604
8347
  function removeSvgAtts(svg, remove = []) {
8348
+ removeAtts(svg, remove);
8349
+ }
8350
+
8351
+ function removeAtts(el, remove = []) {
7605
8352
  remove.forEach(att => {
7606
- svg.removeAttribute(att);
8353
+ el.removeAttribute(att);
7607
8354
  });
7608
8355
  }
7609
8356
 
@@ -7701,22 +8448,108 @@
7701
8448
  }
7702
8449
  */
7703
8450
 
8451
+ function setNormalizedTransformsToEl(el, {
8452
+ styleProps = {},
8453
+ } = {}) {
8454
+ let { remove, matrix, transComponents } = styleProps;
8455
+ let name = el.nodeName.toLowerCase();
8456
+
8457
+ if(!matrix) return styleProps;
8458
+
8459
+ let { rotate, scaleX, scaleY, skewX, translateX, translateY } = transComponents;
8460
+
8461
+ // scale attributes instead of transform
8462
+ let hasRot = rotate !== 0 || skewX !== 0;
8463
+ let unProportional = scaleX !== scaleY;
8464
+ let scalableByAtt = ['circle', 'ellipse', 'rect'];
8465
+
8466
+ let needsTrans = (hasRot) || unProportional;
8467
+ needsTrans = true;
8468
+
8469
+ if (!needsTrans && scalableByAtt.includes(name)) {
8470
+
8471
+ if (name === 'circle' || name === 'ellipse') {
8472
+ styleProps.cx[0] = [styleProps.cx[0] * scaleX + translateX];
8473
+ styleProps.cy[0] = [styleProps.cy[0] * scaleX + translateY];
8474
+
8475
+ if (styleProps.r) styleProps.r[0] = [styleProps.r[0] * scaleX];
8476
+ if (styleProps.rx) styleProps.rx[0] = [styleProps.rx[0] * scaleX];
8477
+ if (styleProps.ry) styleProps.ry[0] = [styleProps.ry[0] * scaleX];
8478
+
8479
+ }
8480
+ else if (name === 'rect') {
8481
+ let x = styleProps.x ? styleProps.x[0] + translateX : translateX;
8482
+ let y = styleProps.y ? styleProps.y[0] + translateY : translateY;
8483
+
8484
+ let rx = styleProps.rx ? styleProps.rx[0] * scaleX : 0;
8485
+ let ry = styleProps.ry ? styleProps.ry[0] * scaleY : 0;
8486
+
8487
+ styleProps.x = [x];
8488
+ styleProps.y = [y];
8489
+
8490
+ styleProps.rx = [rx];
8491
+ styleProps.ry = [ry];
8492
+
8493
+ styleProps.width = [styleProps.width[0] * scaleX];
8494
+ styleProps.height = [styleProps.height[0] * scaleX];
8495
+ }
8496
+
8497
+ // remove now obsolete transform properties
8498
+ delete styleProps.matrix;
8499
+ delete styleProps.transformArr;
8500
+ delete styleProps.transComponents;
8501
+
8502
+ // mark transform attribute for removal
8503
+ styleProps.remove.push('transform');
8504
+
8505
+ // scale props like stroke width or dash-array
8506
+ styleProps = scaleProps(styleProps, { props: ['stroke-width', 'stroke-dasharray'], scale: scaleX });
8507
+
8508
+ } else {
8509
+ el.setAttribute('transform', transComponents.matrixAtt);
8510
+
8511
+ }
8512
+
8513
+ return styleProps
8514
+
8515
+ }
8516
+
8517
+ function scaleProps(styleProps = {}, { props = [], scale = 1 } = {}, round = true) {
8518
+ if (scale === 1 || !props.length) return props;
8519
+
8520
+ for (let i = 0; i < props.length; i++) {
8521
+ let prop = props[i];
8522
+
8523
+ if (styleProps[prop] !== undefined) {
8524
+ styleProps[prop] = styleProps[prop].map(val => round ? roundTo(val * scale, 3) : val * scale);
8525
+ }
8526
+ }
8527
+ return styleProps
8528
+ }
8529
+
7704
8530
  function convertPathLengthAtt(el, {
7705
8531
  styleProps = {}
7706
- }={}) {
8532
+ } = {}) {
7707
8533
 
7708
- let pathLength = el.getAttribute('pathLength') ? +el.getAttribute('pathLength') : 0;
8534
+ let pathLength = styleProps['pathLength'];
7709
8535
 
7710
- if (pathLength && (styleProps['stroke-dasharray'] || styleProps['stroke-dashoffset'])) {
7711
- let elLength = getElementLength(el, {
7712
- pathLength,
7713
- props: styleProps
7714
- });
8536
+ if (pathLength) {
8537
+
8538
+ if ((styleProps['stroke-dasharray'] || styleProps['stroke-dashoffset'])) {
8539
+ let elLength = getElementLength(el, {
8540
+ pathLength,
8541
+ props: styleProps
8542
+ });
8543
+
8544
+ let scale = elLength / pathLength;
7715
8545
 
7716
- let scale = elLength / pathLength;
8546
+ styleProps = scaleProps(styleProps, { props: ['stroke-dasharray', 'stroke-dashoffset'], scale });
7717
8547
 
7718
- styleProps = scaleProps(styleProps, { props: ['stroke-dasharray', 'stroke-dashoffset'], scale });
7719
- [styleProps['stroke-dasharray'], styleProps['stroke-dashoffset']];
8548
+ // set absolute
8549
+ if (styleProps['stroke-dasharray']) el.setAttribute('stroke-dasharray', styleProps['stroke-dasharray'].join(' '));
8550
+ if (styleProps['stroke-dashoffset']) el.setAttribute('stroke-dashoffset', styleProps['stroke-dashoffset'][0]);
8551
+
8552
+ }
7720
8553
 
7721
8554
  // tag for removal
7722
8555
  delete styleProps['pathLength'];
@@ -8051,85 +8884,6 @@
8051
8884
  return props
8052
8885
  }
8053
8886
 
8054
- function setNormalizedTransformsToEl(el, {
8055
- styleProps = {},
8056
- } = {}) {
8057
- let { remove, matrix, transComponents } = styleProps;
8058
- let name = el.nodeName.toLowerCase();
8059
-
8060
- if(!matrix) return styleProps;
8061
-
8062
- let { rotate, scaleX, scaleY, skewX, translateX, translateY } = transComponents;
8063
-
8064
- // scale attributes instead of transform
8065
- let hasRot = rotate !== 0 || skewX !== 0;
8066
- let unProportional = scaleX !== scaleY;
8067
- let scalableByAtt = ['circle', 'ellipse', 'rect'];
8068
-
8069
- let needsTrans = (hasRot) || unProportional;
8070
- needsTrans = true;
8071
-
8072
- if (!needsTrans && scalableByAtt.includes(name)) {
8073
-
8074
- if (name === 'circle' || name === 'ellipse') {
8075
- styleProps.cx[0] = [styleProps.cx[0] * scaleX + translateX];
8076
- styleProps.cy[0] = [styleProps.cy[0] * scaleX + translateY];
8077
-
8078
- if (styleProps.r) styleProps.r[0] = [styleProps.r[0] * scaleX];
8079
- if (styleProps.rx) styleProps.rx[0] = [styleProps.rx[0] * scaleX];
8080
- if (styleProps.ry) styleProps.ry[0] = [styleProps.ry[0] * scaleX];
8081
-
8082
- }
8083
- else if (name === 'rect') {
8084
- let x = styleProps.x ? styleProps.x[0] + translateX : translateX;
8085
- let y = styleProps.y ? styleProps.y[0] + translateY : translateY;
8086
-
8087
- let rx = styleProps.rx ? styleProps.rx[0] * scaleX : 0;
8088
- let ry = styleProps.ry ? styleProps.ry[0] * scaleY : 0;
8089
-
8090
- styleProps.x = [x];
8091
- styleProps.y = [y];
8092
-
8093
- styleProps.rx = [rx];
8094
- styleProps.ry = [ry];
8095
-
8096
- styleProps.width = [styleProps.width[0] * scaleX];
8097
- styleProps.height = [styleProps.height[0] * scaleX];
8098
- }
8099
-
8100
- // remove now obsolete transform properties
8101
- delete styleProps.matrix;
8102
- delete styleProps.transformArr;
8103
- delete styleProps.transComponents;
8104
-
8105
- // mark transform attribute for removal
8106
- styleProps.remove.push('transform');
8107
-
8108
- // scale props like stroke width or dash-array
8109
- styleProps = scaleProps$1(styleProps, { props: ['stroke-width', 'stroke-dasharray'], scale: scaleX });
8110
-
8111
- } else {
8112
- el.setAttribute('transform', transComponents.matrixAtt);
8113
-
8114
- }
8115
-
8116
- return styleProps
8117
-
8118
- }
8119
-
8120
- function scaleProps$1(styleProps = {}, { props = [], scale = 1 } = {}, round = true) {
8121
- if (scale === 1 || !props.length) return props;
8122
-
8123
- for (let i = 0; i < props.length; i++) {
8124
- let prop = props[i];
8125
-
8126
- if (styleProps[prop] !== undefined) {
8127
- styleProps[prop] = styleProps[prop].map(val => round ? roundTo(val * scale, 2) : val * scale);
8128
- }
8129
- }
8130
- return styleProps
8131
- }
8132
-
8133
8887
  function cleanUpSVG(svgMarkup, {
8134
8888
  removeHidden = true,
8135
8889
 
@@ -8159,7 +8913,10 @@
8159
8913
  cleanupSVGAtts = true,
8160
8914
  removeNameSpaced = true,
8161
8915
  removeNameSpacedAtts = true,
8916
+
8917
+ // unit conversions
8162
8918
  convertPathLength = false,
8919
+ toAbsoluteUnits = false,
8163
8920
 
8164
8921
  // meta
8165
8922
  allowMeta = false,
@@ -8184,10 +8941,10 @@
8184
8941
  } = {}) {
8185
8942
 
8186
8943
  // resolve dependencies
8187
- if (unGroup || convertTransforms || minifyRgbColors || attributesToGroup)
8188
- stylesToAttributes = true;
8944
+ if (unGroup || convertTransforms || minifyRgbColors || attributesToGroup)
8945
+ stylesToAttributes = true;
8189
8946
 
8190
- if(stylesToAttributes) cleanUpStrokes = true;
8947
+ if (stylesToAttributes) cleanUpStrokes = true;
8191
8948
 
8192
8949
  // replace namespaced refs
8193
8950
  if (fixHref) svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
@@ -8232,7 +8989,7 @@
8232
8989
  removeClassNames,
8233
8990
  minifyRgbColors,
8234
8991
  stylesheetProps: {},
8235
- exclude:[]
8992
+ exclude: []
8236
8993
  };
8237
8994
 
8238
8995
  // root svg inline style properties
@@ -8314,9 +9071,13 @@
8314
9071
  let stylePropsFiltered = {};
8315
9072
 
8316
9073
  // convert pathLength before transforming
8317
- if (convertPathLength) {
9074
+ if(convertTransforms || attributesToGroup) convertPathLength=true;
9075
+
9076
+ if (convertPathLength ) {
9077
+
8318
9078
  styleProps = convertPathLengthAtt(el, { styleProps });
8319
9079
  remove = [...new Set([...remove, ...styleProps.remove])];
9080
+
8320
9081
  }
8321
9082
 
8322
9083
  // get parent styles
@@ -8349,9 +9110,14 @@
8349
9110
  if (stylePropsSVG['class']) delete stylePropsSVG['class'];
8350
9111
  if (stylePropsSVG['id']) delete stylePropsSVG['id'];
8351
9112
 
9113
+ // add svg props
9114
+ inheritedProps = {
9115
+ ...stylePropsSVG,
9116
+ ...inheritedProps,
9117
+ };
9118
+
8352
9119
  // merge with svg props
8353
9120
  styleProps = {
8354
- ...stylePropsSVG,
8355
9121
  ...inheritedProps,
8356
9122
  ...styleProps
8357
9123
  };
@@ -8393,6 +9159,40 @@
8393
9159
  // general cleanup
8394
9160
  if (cleanupSVGAtts) cleanupSVGAttributes(svg, { removeIds, removeClassNames, removeDimensions, stylesToAttributes, allowMeta, allowAriaAtts, allowDataAtts });
8395
9161
 
9162
+ // all relative units to absolute
9163
+ if (toAbsoluteUnits) {
9164
+ normalizeTransforms = true;
9165
+
9166
+ /**
9167
+ * apply consolidated
9168
+ * element attributes
9169
+ * remove non-supported element props
9170
+ */
9171
+ stylePropsFiltered = filterSvgElProps(name, styleProps,
9172
+ { removeDefaults: true, cleanUpStrokes, allowMeta, allowAriaAtts, allowDataAtts, removeIds, inheritedProps });
9173
+
9174
+ for (let prop in stylePropsFiltered.propsFiltered) {
9175
+ let values = styleProps[prop];
9176
+ let val = values.length ? values.join(' ') : values[0];
9177
+ el.setAttribute(prop, val);
9178
+ }
9179
+
9180
+ let removeAttsEl = [...new Set([...remove, ...stylePropsFiltered.remove])];
9181
+
9182
+ // check if same value is in inherited
9183
+ for (let prop in stylePropsFiltered.propsFiltered) {
9184
+ let valInh = inheritedProps[prop] || [];
9185
+ let val = stylePropsFiltered.propsFiltered[prop] || [];
9186
+ if (valInh.join() === val.join()) {
9187
+ removeAttsEl.push(prop);
9188
+ }
9189
+ }
9190
+
9191
+ // remove obsolete/inherited
9192
+ removeAtts(el, removeAttsEl);
9193
+
9194
+ }
9195
+
8396
9196
  if (stylesToAttributes) {
8397
9197
 
8398
9198
  /**
@@ -8411,7 +9211,7 @@
8411
9211
  * remove non-supported element props
8412
9212
  */
8413
9213
  stylePropsFiltered = filterSvgElProps(name, styleProps,
8414
- { removeDefaults: true, cleanUpStrokes, allowMeta, allowAriaAtts, allowDataAtts, removeIds });
9214
+ { removeDefaults: true, cleanUpStrokes, allowMeta, allowAriaAtts, allowDataAtts, removeIds, inheritedProps });
8415
9215
 
8416
9216
  remove = [...new Set([...remove, ...stylePropsFiltered.remove])];
8417
9217
 
@@ -8425,12 +9225,14 @@
8425
9225
  * remove obsolete
8426
9226
  * attributes
8427
9227
  */
9228
+ removeAtts(el, remove);
8428
9229
 
9230
+ /*
8429
9231
  for (let i = 0; i < remove.length; i++) {
8430
9232
  let att = remove[i];
8431
-
8432
- el.removeAttribute(att);
9233
+ el.removeAttribute(att)
8433
9234
  }
9235
+ */
8434
9236
 
8435
9237
  } // endof style processing
8436
9238
 
@@ -8453,7 +9255,7 @@
8453
9255
 
8454
9256
  // scale props like stroke width or dash-array before conversion
8455
9257
  if (matrix && transComponents) {
8456
- ['stroke-width', 'stroke-dasharray'].forEach(att => {
9258
+ ['stroke-width', 'stroke-dasharray', 'stroke-dashoffset'].forEach(att => {
8457
9259
  let attVal = el.getAttribute(att);
8458
9260
  let vals = attVal ? attVal.split(' ').filter(Boolean).map(Number).map(val => val * transComponents.scaleX) : [];
8459
9261
  if (vals.length) el.setAttribute(att, vals.join(' '));
@@ -8515,15 +9317,15 @@
8515
9317
  let values = stylePropsFiltered[prop];
8516
9318
  let val = values.length ? values.join(' ') : values[0];
8517
9319
 
8518
- if(prop!=='class' && prop!=='id'){
9320
+ if (prop !== 'class' && prop !== 'id') {
8519
9321
 
8520
9322
  let propShort = toShortStr(prop);
8521
9323
  let valShort = toShortStr(val);
8522
9324
  let propStr = `${propShort}-${valShort}`;
8523
-
9325
+
8524
9326
  // store in node property
8525
9327
  if (!el.styleSet) el.styleSet = new Set();
8526
- if(propStr) el.styleSet.add(propStr);
9328
+ if (propStr) el.styleSet.add(propStr);
8527
9329
  }
8528
9330
  }
8529
9331
 
@@ -8572,7 +9374,7 @@
8572
9374
  }
8573
9375
 
8574
9376
  function removeEmptyClassAtts(svg) {
8575
- let emptyClassEls = svg.querySelectorAll('[class=""');
9377
+ let emptyClassEls = svg.querySelectorAll('[class=""]');
8576
9378
  emptyClassEls.forEach(el => {
8577
9379
  el.removeAttribute('class');
8578
9380
  });
@@ -8585,7 +9387,7 @@
8585
9387
 
8586
9388
  let els = svg.querySelectorAll(renderedEls.join(', '));
8587
9389
  let len = els.length;
8588
- if(len===1) return;
9390
+ if (len === 1) return;
8589
9391
 
8590
9392
  let el0 = els[0] || null;
8591
9393
  let stylePrev = el0.styleSet !== undefined ? [...el0.styleSet].join('_') : '';
@@ -8661,7 +9463,7 @@
8661
9463
  if (children.length === 1) continue
8662
9464
 
8663
9465
  // create new group
8664
- if (!groupEl || groups.length>1) {
9466
+ if (!groupEl || groups.length > 1) {
8665
9467
 
8666
9468
  groupEl = document.createElementNS(svgNs, 'g');
8667
9469
  child0.parentNode.insertBefore(groupEl, child0);
@@ -8743,7 +9545,9 @@
8743
9545
  bb0.bottom = y + height;
8744
9546
 
8745
9547
  els.forEach(el => {
9548
+
8746
9549
  let bb = getElBBox(el);
9550
+
8747
9551
  let outside = bb.right < bb0.x || bb.bottom < bb0.y || bb.x > bb0.right || bb.y > bb.bottom;
8748
9552
  if (outside) el.remove();
8749
9553
  });
@@ -8852,8 +9656,130 @@
8852
9656
  });
8853
9657
  }
8854
9658
 
9659
+ function getArcFromPoly(pts, precise = false) {
9660
+ if (pts.length < 3) return false
9661
+
9662
+ // Pick 3 well-spaced points
9663
+ let len = pts.length;
9664
+ let idx1 = Math.floor(len * 0.333);
9665
+ let idx2 = Math.floor(len * 0.666);
9666
+ let idx3 = Math.floor(len * 0.5);
9667
+
9668
+ let p1 = pts[0];
9669
+ let p2 = pts[idx3];
9670
+ let p3 = pts[len - 1];
9671
+
9672
+ // Radius (use start point)
9673
+ let pts1 = [p1, p2, p3];
9674
+ let centroid = getPolyArcCentroid(pts1);
9675
+
9676
+ let r = 0, deltaAngle = 0, startAngle = 0, endAngle = 0, angleData = {};
9677
+
9678
+ // check if radii are consistent
9679
+ if (precise) {
9680
+
9681
+ /**
9682
+ * check multiple centroids
9683
+ * if the polyline can be expressed as
9684
+ * an arc - all centroids should be close
9685
+ */
9686
+
9687
+ if (len > 3) {
9688
+ let centroid1 = getPolyArcCentroid([p1, pts[idx1], p3]);
9689
+ let centroid2 = getPolyArcCentroid([p1, pts[idx2], p3]);
9690
+
9691
+ if (!centroid1 || !centroid2) return false;
9692
+
9693
+ let dist0 = getDistManhattan(centroid, p2);
9694
+ let dist1 = getDistManhattan(centroid, centroid1);
9695
+ let dist2 = getDistManhattan(centroid, centroid2);
9696
+ let errorCentroid = (dist1 + dist2);
9697
+
9698
+ // centroids diverging too much
9699
+ if (errorCentroid > dist0 * 0.05) {
9700
+
9701
+ return false
9702
+ }
9703
+
9704
+ }
9705
+
9706
+ let rSqMid = getSquareDistance(centroid, p2);
9707
+
9708
+ for (let i = 0; i < len; i++) {
9709
+ let pt = pts[i];
9710
+ let rSq = getSquareDistance(centroid, pt);
9711
+ let error = Math.abs(rSqMid - rSq) / rSqMid;
9712
+
9713
+ if (error > 0.0025) {
9714
+ /*
9715
+ console.log('error', error, len, idx1, idx2, idx3);
9716
+ renderPoint(markers, centroid, 'orange')
9717
+ renderPoint(markers, p1, 'green')
9718
+ renderPoint(markers, p2)
9719
+ renderPoint(markers, p3, 'purple')
9720
+ */
9721
+ return false;
9722
+ }
9723
+ }
9724
+
9725
+ // calculate proper radius
9726
+ r = Math.sqrt(rSqMid);
9727
+ angleData = getDeltaAngle(centroid, p1, p3);
9728
+ ({ deltaAngle, startAngle, endAngle } = angleData);
9729
+
9730
+ } else {
9731
+ r = getDistance(centroid, p1);
9732
+ angleData = getDeltaAngle(centroid, p1, p3);
9733
+ ({ deltaAngle, startAngle, endAngle } = angleData);
9734
+ }
9735
+
9736
+ return {
9737
+ centroid,
9738
+ r,
9739
+ startAngle,
9740
+ endAngle,
9741
+ deltaAngle
9742
+ };
9743
+ }
9744
+
9745
+ function getPolyArcCentroid(pts = []) {
9746
+
9747
+ pts = pts.filter(pt => pt !== undefined);
9748
+ if (pts.length < 3) return false
9749
+
9750
+ let p1 = pts[0];
9751
+ let p2 = pts[Math.floor(pts.length / 2)];
9752
+ let p3 = pts[pts.length - 1];
9753
+
9754
+ let x1 = p1.x, y1 = p1.y;
9755
+ let x2 = p2.x, y2 = p2.y;
9756
+ let x3 = p3.x, y3 = p3.y;
9757
+
9758
+ let a = x1 - x2;
9759
+ let b = y1 - y2;
9760
+ let c = x1 - x3;
9761
+ let d = y1 - y3;
9762
+
9763
+ let e = ((x1 * x1 - x2 * x2) + (y1 * y1 - y2 * y2)) / 2;
9764
+ let f = ((x1 * x1 - x3 * x3) + (y1 * y1 - y3 * y3)) / 2;
9765
+
9766
+ let det = a * d - b * c;
9767
+
9768
+ // colinear points
9769
+ if (Math.abs(det) < 1e-10) {
9770
+ return false;
9771
+ }
9772
+
9773
+ // find center of arc
9774
+ let cx = (d * e - b * f) / det;
9775
+ let cy = (-c * e + a * f) / det;
9776
+ let centroid = { x: cx, y: cy };
9777
+ return centroid
9778
+ }
9779
+
8855
9780
  function refineRoundedCorners(pathData, {
8856
9781
  threshold = 0,
9782
+ simplifyQuadraticCorners = false,
8857
9783
  tolerance = 1
8858
9784
  } = {}) {
8859
9785
 
@@ -8878,6 +9804,9 @@
8878
9804
  let firstIsLine = pathData[1].type === 'L';
8879
9805
  let firstIsBez = pathData[1].type === 'C';
8880
9806
 
9807
+ // in case we have simplified a corner connecting to the start
9808
+ let M_adj = null;
9809
+
8881
9810
  let normalizeClose = isClosed && firstIsBez && (lastIsLine || zIsLineto);
8882
9811
 
8883
9812
  // normalize closepath to lineto
@@ -8917,15 +9846,17 @@
8917
9846
  // closing corner to start
8918
9847
  if (isClosed && lastIsBez && firstIsLine && i === l - lastOff - 1) {
8919
9848
  comL1 = pathData[1];
9849
+
8920
9850
  comBez = [pathData[l - lastOff]];
8921
9851
 
8922
9852
  }
8923
9853
 
9854
+ // collect enclosed bezier segments
8924
9855
  for (let j = i + 1; j < l; j++) {
8925
9856
  let comN = pathData[j] ? pathData[j] : null;
8926
9857
  let comPrev = pathData[j - 1];
8927
9858
 
8928
- if (comPrev.type === 'C') {
9859
+ if (comPrev.type === 'C' && j > 2) {
8929
9860
  comBez.push(comPrev);
8930
9861
  }
8931
9862
 
@@ -8956,39 +9887,67 @@
8956
9887
  let bezThresh = len3 * 0.5 * tolerance;
8957
9888
  let isSmall = bezThresh < len1 && bezThresh < len2;
8958
9889
 
9890
+ /*
9891
+ */
9892
+
8959
9893
  if (comBez.length && !signChange && isSmall) {
8960
9894
 
8961
- let isFlatBezier = Math.abs(area2) < getSquareDistance(comBez[0].p0, comBez[0].p) * 0.005;
9895
+ let isSquare = false;
9896
+
9897
+ if (comBez.length === 1) {
9898
+ let dx = Math.abs(comBez[0].p.x - comBez[0].p0.x);
9899
+ let dy = Math.abs(comBez[0].p.y - comBez[0].p0.y);
9900
+ let diff = (dx - dy);
9901
+ let rat = Math.abs(diff / dx);
9902
+ isSquare = rat < 0.01;
9903
+ }
9904
+
9905
+ let preferArcs = true;
9906
+ preferArcs = false;
9907
+
9908
+ // if rectangular prefer arcs
9909
+ if (preferArcs && isSquare) {
9910
+
9911
+ let pM = pointAtT([comBez[0].p0, comBez[0].cp1, comBez[0].cp2, comBez[0].p], 0.5);
9912
+
9913
+ let arcProps = getArcFromPoly([comBez[0].p0, pM, comBez[0].p]);
9914
+ let { r, centroid, deltaAngle } = arcProps;
9915
+
9916
+ let sweep = deltaAngle > 0 ? 1 : 0;
9917
+
9918
+ let largeArc = 0;
9919
+
9920
+ let comArc = { type: 'A', values: [r, r, 0, largeArc, sweep, comBez[0].p.x, comBez[0].p.y] };
9921
+
9922
+ pathDataN.push(comL0, comArc);
9923
+ i += offset;
9924
+ continue
9925
+
9926
+ }
9927
+
9928
+ let areaThresh = getSquareDistance(comBez[0].p0, comBez[0].p) * 0.005;
9929
+ let isFlatBezier = Math.abs(area2) < areaThresh;
9930
+ let isFlatBezier2 = Math.abs(area2) < areaThresh * 10;
9931
+
8962
9932
  let ptQ = !isFlatBezier ? checkLineIntersection(comL0.p0, comL0.p, comL1.p, comL1.p0, false, true) : null;
8963
9933
 
8964
- if (!ptQ) {
9934
+ // exit: is rather flat or has no intersection
9935
+
9936
+ if (!ptQ || (isFlatBezier2 && comBez.length === 1)) {
8965
9937
  pathDataN.push(com);
8966
9938
  continue
8967
9939
  }
8968
9940
 
8969
- // check sign change
9941
+ // check sign change - exit if present
8970
9942
  if (ptQ) {
8971
9943
  let area0 = getPolygonArea([comL0.p0, comL0.p, comL1.p0, comL1.p], false);
8972
9944
  let area0_abs = Math.abs(area0);
8973
9945
  let area1 = getPolygonArea([comL0.p0, comL0.p, ptQ, comL1.p0, comL1.p], false);
8974
9946
  let area1_abs = Math.abs(area1);
8975
9947
  let areaDiff = Math.abs(area0_abs - area1_abs) / area0_abs;
8976
-
8977
- /*
8978
- renderPoint(markers, comL0.p0, 'green', '0.5%', '0.5')
8979
- renderPoint(markers, comL0.p, 'red', '1.5%', '0.5')
8980
- renderPoint(markers, comL1.p0, 'blue', '0.5%', '0.5')
8981
- renderPoint(markers, comL1.p, 'orange', '0.5%', '0.5')
8982
- if(!area0) {
8983
- pathDataN.push(com);
8984
- continue
8985
- }
8986
- */
8987
-
8988
9948
  let signChange = area0 < 0 && area1 > 0 || area0 > 0 && area1 < 0;
8989
9949
 
8990
9950
  if (!ptQ || signChange || areaDiff > 0.5) {
8991
-
8992
9951
  pathDataN.push(com);
8993
9952
  continue
8994
9953
  }
@@ -9003,24 +9962,67 @@
9003
9962
 
9004
9963
  // not in tolerance – return original command
9005
9964
  if (bezThresh && dist1 > bezThresh && dist1 > len3 * 0.3) {
9006
-
9007
9965
  pathDataN.push(com);
9008
9966
  continue;
9009
9967
 
9010
- } else {
9968
+ }
9011
9969
 
9012
- let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, comL1.p0.x, comL1.p0.y] };
9013
- comQ.p0 = comL0.p;
9014
- comQ.cp1 = ptQ;
9015
- comQ.p = comL1.p0;
9970
+ // return simplified quadratic Bézier command
9971
+ let p_Q = comL1.p0;
9016
9972
 
9017
- // add quadratic command
9018
- pathDataN.push(comL0, comQ);
9019
- i += offset;
9973
+ // adjust previous end point to better fit the cubic curvature
9974
+ let adjustQ = !simplifyQuadraticCorners;
9975
+
9976
+ if (adjustQ) {
9977
+
9978
+ let t = 0.1666;
9979
+ let p0_adj = interpolate(ptQ, comL0.p, (1 + t));
9980
+ p_Q = interpolate(ptQ, comL1.p0, (1 + t));
9981
+
9982
+ // round for large enough segments
9983
+ let isH = ptQ.y===comL0.p.y;
9984
+ let isV = ptQ.x===comL0.p.x;
9985
+ let isH2 = ptQ.y===comL1.p0.y;
9986
+ let isV2 = ptQ.x===comL1.p0.x;
9987
+
9988
+ if(isSquare && com.dimA>3){
9989
+ let dec = 0.5;
9990
+ if(isH) p0_adj.x = roundTo(p0_adj.x, dec);
9991
+ if(isV) p0_adj.y = roundTo(p0_adj.y, dec);
9992
+ if(isH2) p_Q.x = roundTo(p_Q.x, dec);
9993
+ if(isV2) p_Q.y = roundTo(p_Q.y, dec);
9994
+ }
9995
+
9996
+ /*
9997
+ renderPoint(markers, p0_adj, 'orange')
9998
+ renderPoint(markers, p_Q, 'orange')
9999
+ renderPoint(markers, comL0.p, 'green')
10000
+ renderPoint(markers, comL1.p0, 'magenta')
10001
+ */
10002
+
10003
+ // set new M starting point
10004
+ if (i === l - lastOff - 1) {
10005
+
10006
+ M_adj = p_Q;
10007
+ }
10008
+
10009
+ // adjust previous lineto end point
10010
+ comL0.values = [p0_adj.x, p0_adj.y];
10011
+ comL0.p = p0_adj;
9020
10012
 
9021
- continue;
9022
10013
  }
9023
10014
 
10015
+ let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, p_Q.x, p_Q.y] };
10016
+ comQ.cp1 = ptQ;
10017
+ comQ.p0 = comL0.p;
10018
+ comQ.p = p_Q;
10019
+
10020
+ // add quadratic command
10021
+ pathDataN.push(comL0, comQ);
10022
+
10023
+ i += offset;
10024
+ continue;
10025
+
9024
10026
  }
9025
10027
  }
9026
10028
  }
@@ -9034,6 +10036,12 @@
9034
10036
 
9035
10037
  }
9036
10038
 
10039
+ // correct starting point connecting with last corner rounding
10040
+ if (M_adj) {
10041
+ pathDataN[0].values = [M_adj.x, M_adj.y];
10042
+ pathDataN[0].p0 = M_adj;
10043
+ }
10044
+
9037
10045
  // revert close path normalization
9038
10046
  if (normalizeClose || (isClosed && pathDataN[pathDataN.length - 1].type !== 'Z')) {
9039
10047
  pathDataN.push({ type: 'Z', values: [] });
@@ -9043,51 +10051,143 @@
9043
10051
 
9044
10052
  }
9045
10053
 
9046
- function getArcFromPoly(pts) {
9047
- if (pts.length < 3) return false
10054
+ function simplifyAdjacentRound(pathData, {
10055
+ threshold = 0,
10056
+ tolerance = 1,
10057
+ // take arcs or cubic beziers
10058
+ toCubic = false,
10059
+ debug = false
10060
+ } = {}) {
9048
10061
 
9049
- // Pick 3 well-spaced points
9050
- let p1 = pts[0];
9051
- let p2 = pts[Math.floor(pts.length / 2)];
9052
- let p3 = pts[pts.length - 1];
10062
+ // fix small Arcs
10063
+ pathData = convertSmallArcsToLinetos(pathData);
9053
10064
 
9054
- let x1 = p1.x, y1 = p1.y;
9055
- let x2 = p2.x, y2 = p2.y;
9056
- let x3 = p3.x, y3 = p3.y;
10065
+ // min size threshold for corners
10066
+ threshold *= tolerance;
9057
10067
 
9058
- let a = x1 - x2;
9059
- let b = y1 - y2;
9060
- let c = x1 - x3;
9061
- let d = y1 - y3;
10068
+ let l = pathData.length;
9062
10069
 
9063
- let e = ((x1 * x1 - x2 * x2) + (y1 * y1 - y2 * y2)) / 2;
9064
- let f = ((x1 * x1 - x3 * x3) + (y1 * y1 - y3 * y3)) / 2;
10070
+ // add fist command
10071
+ let pathDataN = [pathData[0]];
9065
10072
 
9066
- let det = a * d - b * c;
10073
+ // find adjacent cubics between extremes
9067
10074
 
9068
- if (Math.abs(det) < 1e-10) {
9069
- console.warn("Points are collinear or numerically unstable");
9070
- return false;
9071
- }
10075
+ for (let i = 1; i < l; i++) {
10076
+ pathData[i - 1];
10077
+ let com = pathData[i];
10078
+ let comN = pathData[i + 1] || null;
9072
10079
 
9073
- // find center of arc
9074
- let cx = (d * e - b * f) / det;
9075
- let cy = (-c * e + a * f) / det;
9076
- let centroid = { x: cx, y: cy };
10080
+ if (!comN) {
10081
+ pathDataN.push(com);
10082
+ break
10083
+ }
9077
10084
 
9078
- // Radius (use start point)
9079
- let r = getDistance(centroid, p1);
10085
+ let { type, extreme = false, p0, p, dimA = 0 } = com;
10086
+ // for short segment detection
10087
+ let dimAN = comN.dimA;
10088
+ let dimA0 = dimA + dimAN;
10089
+ let thresh = 0.1;
9080
10090
 
9081
- let angleData = getDeltaAngle(centroid, p1, p3);
9082
- let {deltaAngle, startAngle, endAngle} = angleData;
10091
+ // ignore short linetos
10092
+ let isShortN = dimAN < dimA0 * thresh;
9083
10093
 
9084
- return {
9085
- centroid,
9086
- r,
9087
- startAngle,
9088
- endAngle,
9089
- deltaAngle
9090
- };
10094
+ // adjacent cubic commands - accept short in between linetos
10095
+ if ((type === 'C') && (comN.type === 'C' || isShortN)) {
10096
+
10097
+ let candidates = [];
10098
+
10099
+ for (let j = i + 1; j < l; j++) {
10100
+ let comN = pathData[j];
10101
+ let { type, extreme = false, corner = false, dimA = 0 } = comN;
10102
+ let isShort = dimA < dimA0 * thresh;
10103
+
10104
+ // skip for type change(unless very short), extremes or corners
10105
+ /*
10106
+ if ( (comN.extreme || comN.corner) ) {
10107
+ if(!extreme && !corner) candidates.push(comN)
10108
+ break;
10109
+ }
10110
+ */
10111
+
10112
+ if (extreme || corner) {
10113
+
10114
+ if (isShort && comN.type !== 'C') ;
10115
+
10116
+ if ((extreme && !corner)) {
10117
+
10118
+ candidates.push(comN);
10119
+ }
10120
+
10121
+ break;
10122
+ }
10123
+
10124
+ candidates.push(comN);
10125
+ }
10126
+
10127
+ // try to create arc command
10128
+ if (candidates.length > 1) {
10129
+
10130
+ let clen = candidates.length;
10131
+ let pts = [com.p0, com.p,];
10132
+
10133
+ // add interpolated points to prevent wrong arc replacements
10134
+ candidates.forEach(c => {
10135
+ if (c.type === 'C') {
10136
+ let pt = pointAtT([c.p0, c.cp1, c.cp2, c.p], 0.5);
10137
+ pts.push(pt);
10138
+ }
10139
+ pts.push(c.p);
10140
+ });
10141
+
10142
+ let precise = true;
10143
+ let arcProps = getArcFromPoly(pts, precise);
10144
+
10145
+ // could be combined
10146
+ if (arcProps) {
10147
+
10148
+ let { centroid, r, deltaAngle, startAngle, endAngle } = arcProps;
10149
+ let sweep = deltaAngle > 0 ? 1 : 0;
10150
+
10151
+ let largeArc = Math.abs(deltaAngle) > Math.PI ? 1 : 0;
10152
+ largeArc = 0;
10153
+ let comLast = candidates[clen - 1];
10154
+ let p = comLast.p;
10155
+
10156
+ let comArc = { type: 'A', values: [r, r, 0, largeArc, sweep, p.x, p.y] };
10157
+
10158
+ comArc.dimA = getDistManhattan(p0, p);
10159
+ comArc.p0 = p0;
10160
+ comArc.p = p;
10161
+ comArc.error = 0;
10162
+ comArc.directionChange = comLast.directionChange;
10163
+ comArc.extreme = comLast.extreme;
10164
+ comArc.corner = comLast.corner;
10165
+ pathDataN.push(comArc);
10166
+
10167
+ i += candidates.length;
10168
+ continue
10169
+
10170
+ }
10171
+
10172
+ // arc radius calculation failed - return original
10173
+ else {
10174
+ pathDataN.push(com);
10175
+ }
10176
+ }
10177
+
10178
+ // could not be simplified – return original command
10179
+ else {
10180
+ pathDataN.push(com);
10181
+ }
10182
+
10183
+ }
10184
+ // all other commands
10185
+ else {
10186
+ pathDataN.push(com);
10187
+ }
10188
+ }
10189
+
10190
+ return pathDataN
9091
10191
  }
9092
10192
 
9093
10193
  function refineRoundSegments(pathData, {
@@ -9106,9 +10206,6 @@
9106
10206
  // add fist command
9107
10207
  let pathDataN = [pathData[0]];
9108
10208
 
9109
- // just for debugging
9110
- let pathDataTest = [];
9111
-
9112
10209
  for (let i = 1; i < l; i++) {
9113
10210
  let com = pathData[i];
9114
10211
  let { type } = com;
@@ -9135,11 +10232,12 @@
9135
10232
 
9136
10233
  // 2. line-line-bezier-line-line
9137
10234
  if (
10235
+ comN2 && comN3 &&
9138
10236
  comP.type === 'L' &&
9139
10237
  type === 'L' &&
9140
10238
  comBez &&
9141
10239
  comN2.type === 'L' &&
9142
- comN3 && (comN3.type === 'L' || comN3.type === 'Z')
10240
+ (comN3.type === 'L' || comN3.type === 'Z')
9143
10241
  ) {
9144
10242
 
9145
10243
  L1 = [com.p0, com.p];
@@ -9166,10 +10264,10 @@
9166
10264
  }
9167
10265
 
9168
10266
  // 1. line-bezier-bezier-line
9169
- else if ((type === 'C' || type === 'Q') && comP.type === 'L') {
10267
+ else if (comN && (type === 'C' || type === 'Q') && comP.type === 'L') {
9170
10268
 
9171
10269
  // 1.2 next is cubic next is lineto
9172
- if ((comN.type === 'C' || comN.type === 'Q') && comN2.type === 'L') {
10270
+ if (comN2 && comN2.type === 'L' && (comN.type === 'C' || comN.type === 'Q')) {
9173
10271
 
9174
10272
  combine = true;
9175
10273
 
@@ -9228,16 +10326,19 @@
9228
10326
  }
9229
10327
  );
9230
10328
 
9231
- if(bezierCommands.length === 1){
10329
+ if (bezierCommands.length === 1) {
9232
10330
 
9233
10331
  // prefer more compact quadratic - otherwise arcs
9234
10332
  let comBezier = revertCubicQuadratic(p0_S, bezierCommands[0].cp1, bezierCommands[0].cp2, p_S);
9235
10333
 
9236
10334
  if (comBezier.type === 'Q') {
9237
10335
  toCubic = true;
10336
+ }else {
10337
+ comBezier = bezierCommands[0];
9238
10338
  }
9239
10339
 
9240
10340
  com = comBezier;
10341
+
9241
10342
  }
9242
10343
 
9243
10344
  // prefer arcs if 2 cubics are required
@@ -9257,25 +10358,28 @@
9257
10358
 
9258
10359
  // test rendering
9259
10360
 
10361
+ /*
9260
10362
  if (debug) {
9261
10363
  // arcs
9262
10364
  if (!toCubic) {
9263
10365
  pathDataTest = [
9264
10366
  { type: 'M', values: [p0_S.x, p0_S.y] },
9265
10367
  { type: 'A', values: [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y] },
9266
- ];
10368
+ ]
9267
10369
  }
9268
10370
  // cubics
9269
10371
  else {
9270
10372
  pathDataTest = [
9271
10373
  { type: 'M', values: [p0_S.x, p0_S.y] },
9272
10374
  ...bezierCommands
9273
- ];
10375
+ ]
10376
+
9274
10377
  }
9275
10378
 
9276
10379
  let d = pathDataToD(pathDataTest);
9277
- renderPath(markers, d, 'orange', '0.5%', '0.5');
10380
+ renderPath(markers, d, 'orange', '0.5%', '0.5')
9278
10381
  }
10382
+ */
9279
10383
 
9280
10384
  pathDataN.push(com);
9281
10385
  i++;
@@ -9407,7 +10511,6 @@
9407
10511
  let com = pathData[c];
9408
10512
  let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
9409
10513
  if (type === 'C') {
9410
-
9411
10514
  let comQ = revertCubicQuadratic(p0, cp1, cp2, p, tolerance);
9412
10515
  if (comQ.type === 'Q') {
9413
10516
  comQ.extreme = com.extreme;
@@ -11019,32 +12122,24 @@
11019
12122
  }
11020
12123
 
11021
12124
  // reverse paths
11022
- for (let i = 0; i < l; i++) {
12125
+ for (let i = 0; l && i < l; i++) {
11023
12126
 
11024
12127
  let poly = polys[i];
11025
12128
  let { cw, includedIn, includes } = poly;
11026
12129
 
11027
- // outer path direction to counter clockwise
11028
- if (!includedIn.length && cw && !toClockwise
11029
- || !includedIn.length && !cw && toClockwise
11030
- ) {
11031
-
11032
- pathDataArr[i].pathData = reversePathData(pathDataArr[i].pathData);
11033
- polys[i].cw = polys[i].cw ? false : true;
11034
- cw = polys[i].cw;
11035
-
11036
- }
12130
+ let len = includes.length;
11037
12131
 
11038
12132
  // reverse inner sub paths
11039
- for (let j = 0; j < includes.length; j++) {
12133
+ for (let j = 0; len && j < len; j++) {
11040
12134
  let ind = includes[j];
11041
12135
  let child = polys[ind];
11042
12136
 
11043
- if (child.cw === cw) {
12137
+ // nothing to do
12138
+ if (child.cw !== cw) continue
12139
+
12140
+ pathDataArr[ind].pathData = reversePathData(pathDataArr[ind].pathData);
12141
+ polys[ind].cw = polys[ind].cw ? false : true;
11044
12142
 
11045
- pathDataArr[ind].pathData = reversePathData(pathDataArr[ind].pathData);
11046
- polys[ind].cw = polys[ind].cw ? false : true;
11047
- }
11048
12143
  }
11049
12144
  }
11050
12145
 
@@ -11084,6 +12179,7 @@
11084
12179
  allowAriaAtts: true,
11085
12180
 
11086
12181
  convertPathLength: false,
12182
+ toAbsoluteUnits: false,
11087
12183
 
11088
12184
  // custom removal
11089
12185
  removeElements: [],
@@ -11112,6 +12208,7 @@
11112
12208
  revertToQuadratics: true,
11113
12209
  refineExtremes: false,
11114
12210
  simplifyCorners: false,
12211
+ simplifyQuadraticCorners: false,
11115
12212
  keepExtremes: true,
11116
12213
  keepCorners: true,
11117
12214
  keepInflections: false,
@@ -11168,7 +12265,7 @@
11168
12265
  let isArray = Array.isArray(val);
11169
12266
 
11170
12267
  if (isBoolean) val = false;
11171
- else if (!isArray && isNum) val = val===1 ? 1 : (prop==='decimals'? -1 : 0);
12268
+ else if (!isArray && isNum) val = val === 1 ? 1 : (prop === 'decimals' ? -1 : 0);
11172
12269
  else if (isArray) val = [];
11173
12270
  settingsNull[prop] = val;
11174
12271
  }
@@ -11199,10 +12296,14 @@
11199
12296
  ...settingsDefaults,
11200
12297
  ...{
11201
12298
  keepSmaller: false,
12299
+ convertPathLength:true,
11202
12300
  toRelative: true,
11203
12301
  toMixed: true,
11204
12302
  toShorthands: true,
11205
12303
 
12304
+ allowMeta:true,
12305
+ allowDataAtts:true,
12306
+ allowAriaAtts:true,
11206
12307
  legacyHref: true,
11207
12308
  addViewBox: true,
11208
12309
  addDimensions: true,
@@ -11270,19 +12371,23 @@
11270
12371
  high: {
11271
12372
  ...settingsDefaults,
11272
12373
  ...{
11273
- tolerance: 1.2,
12374
+ tolerance: 1.1,
11274
12375
  toMixed: true,
11275
12376
  refineExtremes: true,
11276
12377
  simplifyCorners: true,
12378
+ simplifyQuadraticCorners: true,
12379
+ removeOrphanSubpaths: true,
11277
12380
  simplifyRound: true,
11278
12381
  removeClassNames: true,
11279
12382
  cubicToArc: true,
12383
+ minifyD: 0,
11280
12384
  removeComments: true,
11281
12385
  removeHidden: true,
11282
- removeOffCanvas: true,
11283
12386
  addViewBox: true,
11284
12387
  removeDimensions: true,
11285
- minifyD: 0
12388
+ removeOffCanvas: true,
12389
+ /*
12390
+ */
11286
12391
  }
11287
12392
  }
11288
12393
 
@@ -11440,18 +12545,44 @@
11440
12545
  ...settings
11441
12546
  };
11442
12547
 
11443
- let { getObject = false, removeComments, removeOffCanvas, unGroup, mergePaths, removeElements, removeDimensions, removeIds, removeClassNames, omitNamespace, cleanUpStrokes, addViewBox, addDimensions, removePrologue, removeHidden, removeUnused, cleanupDefs, cleanupClip, cleanupSVGAtts, removeNameSpaced, removeNameSpacedAtts, attributesToGroup, minifyRgbColors, stylesToAttributes, fixHref, legacyHref, allowMeta, allowDataAtts, allowAriaAtts, convertPathLength, removeSVGAttributes, removeElAttributes, shapesToPaths, shapeConvert, convertShapes, simplifyBezier, optimizeOrder, autoClose, removeZeroLength, refineClosing, removeColinear, flatBezierToLinetos, revertToQuadratics, refineExtremes, simplifyCorners, fixDirections, keepExtremes, keepCorners, keepInflections, addExtremes, reversePath, toAbsolute, toRelative, toMixed, toShorthands, toLonghands, quadraticToCubic, arcToCubic, cubicToArc, lineToCubic, decimals, autoAccuracy, minifyD, tolerance, toPolygon, smoothPoly, polyFormat, precisionPoly, simplifyRD, simplifyRDP, harmonizeCpts, removeOrphanSubpaths, simplifyRound, scale, scaleTo, crop, alignToOrigin, convertTransforms, keepSmaller, splitCompound } = settings;
12548
+ let { getObject = false, removeComments, removeOffCanvas, unGroup, mergePaths, removeElements, removeDimensions, removeIds, removeClassNames, omitNamespace, cleanUpStrokes, addViewBox, addDimensions, removePrologue, removeHidden, removeUnused, cleanupDefs, cleanupClip, cleanupSVGAtts, removeNameSpaced, removeNameSpacedAtts, attributesToGroup, minifyRgbColors, stylesToAttributes, fixHref, legacyHref, allowMeta, allowDataAtts, allowAriaAtts, removeSVGAttributes, removeElAttributes, shapesToPaths, shapeConvert, convertShapes, simplifyBezier, optimizeOrder, autoClose, removeZeroLength, refineClosing, removeColinear, flatBezierToLinetos, revertToQuadratics, refineExtremes, simplifyCorners, fixDirections, keepExtremes, keepCorners, keepInflections, addExtremes, reversePath, toAbsolute, toRelative, toMixed, toShorthands, toLonghands, quadraticToCubic, arcToCubic, cubicToArc, lineToCubic, decimals, autoAccuracy, minifyD, tolerance, toPolygon, smoothPoly, polyFormat, precisionPoly, simplifyRD, simplifyRDP, harmonizeCpts, removeOrphanSubpaths, simplifyRound, simplifyQuadraticCorners, scale, scaleTo, crop, alignToOrigin, convertTransforms, keepSmaller, splitCompound, convertPathLength, toAbsoluteUnits } = settings;
11444
12549
 
11445
12550
  // clamp tolerance and scale
11446
12551
  tolerance = Math.max(0.1, tolerance);
11447
12552
  scale = Math.max(0.001, scale);
11448
- if(fixDirections) keepSmaller = false;
12553
+ if (fixDirections) keepSmaller = false;
11449
12554
  if (scale !== 1 || scaleTo || crop || alignToOrigin) {
11450
12555
  convertTransforms = true;
11451
12556
  settings.convertTransforms = true;
11452
12557
  }
11453
12558
 
11454
- let inputType = detectInputType(input);
12559
+ /**
12560
+ * intercept
12561
+ * invalid inputs
12562
+ */
12563
+
12564
+ let inputDetection = detectInputType(input);
12565
+ let { inputType, log } = inputDetection;
12566
+
12567
+ // invalid file
12568
+ if (inputType === 'invalid' || input === dummySVG) {
12569
+ // return dummy SVG to continue processing
12570
+
12571
+ let report = {
12572
+ original: 0,
12573
+ new: 0,
12574
+ saved: 0,
12575
+ svgSize: 0,
12576
+ svgSizeOpt: 0,
12577
+ compression: 0,
12578
+ decimals: 0,
12579
+ invalid: true
12580
+ };
12581
+
12582
+ return { svg: dummySVG, d: '', polys: [], report, pathDataPlusArr: [], pathDataPlusArr_global: [], inputType: 'invalid', dOriginal: '' };
12583
+
12584
+ }
12585
+
11455
12586
  let svg = '';
11456
12587
  let svgSize = 0;
11457
12588
  let svgSizeOpt = 0;
@@ -11571,10 +12702,7 @@
11571
12702
  // convert all shapes to paths
11572
12703
  if (shapesToPaths) {
11573
12704
  shapeConvert = 'toPaths';
11574
- convert_rects = true;
11575
- convert_ellipses = true;
11576
- convert_poly = true;
11577
- convert_lines = true;
12705
+ convertShapes = ['rect', 'polygon', 'polyline', 'line', 'circle', 'ellipse'];
11578
12706
  }
11579
12707
 
11580
12708
  // sanitize SVG - clone/decouple settings
@@ -11608,6 +12736,9 @@
11608
12736
  decimals,
11609
12737
  };
11610
12738
 
12739
+ let comCount = 0;
12740
+ let comCountS = 0;
12741
+
11611
12742
  for (let i = 0, l = paths.length; l && i < l; i++) {
11612
12743
 
11613
12744
  let pathDataPlusArr = [];
@@ -11615,6 +12746,12 @@
11615
12746
  let { d, el } = path;
11616
12747
  let isPoly = false;
11617
12748
 
12749
+ // disable reordering for elements with stroke dash-array
12750
+ if (el && (el.hasAttribute('stroke-dasharray') || el.hasAttribute('stroke-dashoffset'))) {
12751
+ optimizeOrder = false;
12752
+
12753
+ }
12754
+
11618
12755
  // if polygon we already heave absolute coordinates
11619
12756
 
11620
12757
  let pathData = parsePathDataNormalized(d, { quadraticToCubic, arcToCubic });
@@ -11652,7 +12789,7 @@
11652
12789
  }
11653
12790
 
11654
12791
  // count commands for evaluation
11655
- let comCount = pathData.length;
12792
+ comCount += pathData.length;
11656
12793
 
11657
12794
  if (!isPoly && removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
11658
12795
 
@@ -11817,11 +12954,14 @@
11817
12954
  if (simplifyCorners) {
11818
12955
 
11819
12956
  let threshold = (bb.width + bb.height) * 0.1;
11820
- pathData = refineRoundedCorners(pathData, { threshold, tolerance });
12957
+ pathData = refineRoundedCorners(pathData, { threshold, tolerance, simplifyQuadraticCorners });
11821
12958
  }
11822
12959
 
11823
12960
  // refine round segment sequences
11824
- if (simplifyRound) pathData = refineRoundSegments(pathData);
12961
+ if (simplifyRound) {
12962
+ pathData = refineRoundSegments(pathData);
12963
+ pathData = simplifyAdjacentRound(pathData);
12964
+ }
11825
12965
 
11826
12966
  // simplify to quadratics
11827
12967
  if (revertToQuadratics) pathData = pathDataRevertCubicToQuadratic(pathData, tolerance);
@@ -11851,7 +12991,7 @@
11851
12991
  let yMax = Math.max(...yArr);
11852
12992
 
11853
12993
  bb_global = { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin };
11854
- let isPortrait = bb_global.height > bb_global.width;
12994
+ bb_global.height > bb_global.width;
11855
12995
 
11856
12996
  // fix path directions - before reordering
11857
12997
  if (fixDirections) {
@@ -11860,7 +13000,23 @@
11860
13000
 
11861
13001
  // prefer top to bottom priority for portrait aspect ratios
11862
13002
  if (optimizeOrder) {
11863
- pathDataPlusArr = isPortrait ? pathDataPlusArr.sort((a, b) => a.bb.y - b.bb.y || a.bb.x - b.bb.x) : pathDataPlusArr.sort((a, b) => a.bb.x - b.bb.x || a.bb.y - b.bb.y);
13003
+ /*
13004
+ 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)
13005
+ */
13006
+
13007
+ // add missin bbox
13008
+ pathDataPlusArr.forEach(p => {
13009
+ if (p.bb.x === undefined) {
13010
+ p.bb = getPolyBBox(getPathDataVertices(p.pathData));
13011
+ }
13012
+ });
13013
+
13014
+ try {
13015
+ pathDataPlusArr = pathDataPlusArr.sort((a, b) => +a.bb.x.toFixed(2) - (+b.bb.x.toFixed(2)) || a.bb.y - b.bb.y);
13016
+
13017
+ } catch {
13018
+ }
13019
+
11864
13020
  }
11865
13021
 
11866
13022
  // flatten compound paths
@@ -11923,7 +13079,7 @@
11923
13079
  }
11924
13080
 
11925
13081
  // compare command count
11926
- let comCountS = pathData.length;
13082
+ comCountS += pathData.length;
11927
13083
 
11928
13084
  let dOpt = pathDataToD(pathData, minifyD);
11929
13085
 
@@ -11998,6 +13154,9 @@
11998
13154
  svgSizeOpt = +(svgSizeOpt / 1024).toFixed(3);
11999
13155
 
12000
13156
  report = {
13157
+ original: comCount,
13158
+ new: comCountS,
13159
+ saved: comCount - comCountS,
12001
13160
  svgSize,
12002
13161
  svgSizeOpt,
12003
13162
  compression,
@@ -12032,6 +13191,8 @@
12032
13191
  if (typeof window !== 'undefined') {
12033
13192
  window.svgPathSimplify = svgPathSimplify;
12034
13193
  window.getElementTransform = getElementTransform;
13194
+ window.validateSVG = validateSVG;
13195
+ window.detectInputType = detectInputType;
12035
13196
 
12036
13197
  window.getViewBox = getViewBox;
12037
13198
 
@@ -12045,6 +13206,7 @@
12045
13206
  exports.atan2 = atan2$1;
12046
13207
  exports.ceil = ceil$1;
12047
13208
  exports.cos = cos$1;
13209
+ exports.detectInputType = detectInputType;
12048
13210
  exports.exp = exp$1;
12049
13211
  exports.floor = floor$1;
12050
13212
  exports.getElementTransform = getElementTransform;
@@ -12060,5 +13222,6 @@
12060
13222
  exports.sqrt = sqrt$1;
12061
13223
  exports.svgPathSimplify = svgPathSimplify;
12062
13224
  exports.tan = tan$1;
13225
+ exports.validateSVG = validateSVG;
12063
13226
 
12064
13227
  })(this["svg-path-simplify"] = this["svg-path-simplify"] || {});