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