svg-path-simplify 0.0.2 → 0.0.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.
@@ -25,6 +25,48 @@ function renderPoint(
25
25
  }
26
26
  }
27
27
 
28
+ function detectInputType(input) {
29
+ let type = 'string';
30
+ if (input instanceof HTMLImageElement) return "img";
31
+ if (input instanceof SVGElement) return "svg";
32
+ if (input instanceof HTMLCanvasElement) return "canvas";
33
+ if (input instanceof File) return "file";
34
+ if (input instanceof ArrayBuffer) return "buffer";
35
+ if (input instanceof Blob) return "blob";
36
+ if (Array.isArray(input)) return "array";
37
+
38
+ if (typeof input === "string") {
39
+ input = input.trim();
40
+ let isSVG = input.includes('<svg') && input.includes('</svg');
41
+ let isPathData = input.startsWith('M') || input.startsWith('m');
42
+ let isPolyString = !isNaN(input.substring(0, 1)) && !isNaN(input.substring(input.length-1, input.length));
43
+
44
+
45
+ if(isSVG) {
46
+ type='svgMarkup';
47
+ }
48
+ else if(isPathData) {
49
+ type='pathDataString';
50
+ }
51
+ else if(isPolyString) {
52
+ type='polyString';
53
+ }
54
+
55
+ else {
56
+ let url = /^(file:|https?:\/\/|\/|\.\/|\.\.\/)/.test(input);
57
+ let dataUrl = input.startsWith('data:image');
58
+ type = url || dataUrl ? "url" : "string";
59
+ }
60
+
61
+ return type
62
+ }
63
+
64
+ type = typeof input;
65
+ let constructor = input.constructor.name;
66
+
67
+ return (constructor || type).toLowerCase();
68
+ }
69
+
28
70
  /*
29
71
  import {abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
30
72
  log, max, min, pow, random, round, sin, sqrt, tan, PI} from '/.constants.js';
@@ -77,7 +119,7 @@ function checkLineIntersection(p1, p2, p3, p4, exact = true) {
77
119
  y: p1.y + (a * (p2.y - p1.y))
78
120
  };
79
121
 
80
- // console.log('intersectionPoint', intersectionPoint, p1, p2);
122
+ // console.log('intersectionPoint', intersectionPoint, p1, p2);
81
123
 
82
124
  let intersection = false;
83
125
  // if line1 is a segment and line2 is infinite, they intersect if:
@@ -632,14 +674,60 @@ function commandIsFlat(points, tolerance = 0.025) {
632
674
  return { area: area, flat: isFlat, thresh: thresh, ratio: ratio, squareDist: squareDist, areaThresh: squareDist / areaThresh };
633
675
  }
634
676
 
677
+ function checkBezierFlatness(p0, cpts, p) {
678
+
679
+ let isFlat = false;
680
+
681
+ let isCubic = cpts.length===2;
682
+
683
+ let cp1 = cpts[0];
684
+ let cp2 = isCubic ? cpts[1] : cp1;
685
+
686
+ if(p0.x===cp1.x && p0.y===cp1.y && p.x===cp2.x && p.y===cp2.y) return true;
687
+
688
+ let dx1 = cp1.x - p0.x;
689
+ let dy1 = cp1.y - p0.y;
690
+
691
+ let dx2 = p.x - cp2.x;
692
+ let dy2 = p.y - cp2.y;
693
+
694
+ let cross1 = Math.abs(dx1 * dy2 - dy1 * dx2);
695
+
696
+ if(!cross1) return true
697
+
698
+ let dx0 = p.x - p0.x;
699
+ let dy0 = p.y - p0.y;
700
+ let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
701
+
702
+ if(!cross0) return true
703
+
704
+ let rat = (cross0/cross1);
705
+
706
+ if (rat<1.1 ) {
707
+
708
+ isFlat = true;
709
+ }
710
+
711
+ return isFlat;
712
+
713
+ }
714
+
635
715
  /**
636
716
  * sloppy distance calculation
637
717
  * based on x/y differences
638
718
  */
639
719
  function getDistAv(pt1, pt2) {
640
- let diffX = Math.abs(pt1.x - pt2.x);
641
- let diffY = Math.abs(pt1.y - pt2.y);
720
+
721
+ let diffX = Math.abs(pt2.x - pt1.x);
722
+ let diffY = Math.abs(pt2.y - pt1.y);
642
723
  let diff = (diffX + diffY) / 2;
724
+
725
+ /*
726
+ let diffX = pt2.x - pt1.x;
727
+ let diffY = pt2.y - pt1.y;
728
+ let diff = Math.abs(diffX + diffY) / 2;
729
+ */
730
+
643
731
  return diff;
644
732
  }
645
733
 
@@ -1279,24 +1367,24 @@ function pathDataToD(pathData, optimize = 0) {
1279
1367
 
1280
1368
  optimize = parseFloat(optimize);
1281
1369
 
1370
+ let len = pathData.length;
1282
1371
  let beautify = optimize > 1;
1283
1372
  let minify = beautify || optimize ? false : true;
1284
1373
 
1285
1374
  // Convert first "M" to "m" if followed by "l" (when minified)
1375
+ /*
1286
1376
  if (pathData[1].type === "l" && minify) {
1287
1377
  pathData[0].type = "m";
1288
1378
  }
1379
+ */
1289
1380
 
1290
1381
  let d = '';
1291
- let suff = beautify ? `\n` : ' ';
1382
+ let separator_command = beautify ? `\n` : (minify ? '' : ' ');
1383
+ let separator_type = !minify ? ' ' : '';
1292
1384
 
1293
- if (minify) {
1294
- d = `${pathData[0].type} ${pathData[0].values.join(" ")}`;
1295
- } else {
1296
- d = `${pathData[0].type} ${pathData[0].values.join(" ")}${suff}`;
1297
- }
1385
+ d = `${pathData[0].type}${separator_type}${pathData[0].values.join(" ")}${separator_command}`;
1298
1386
 
1299
- for (let i = 1, len = pathData.length; i < len; i++) {
1387
+ for (let i = 1; i < len; i++) {
1300
1388
  let com0 = pathData[i - 1];
1301
1389
  let com = pathData[i];
1302
1390
  let { type, values } = com;
@@ -1314,8 +1402,6 @@ function pathDataToD(pathData, optimize = 0) {
1314
1402
  type = (com0.type === com.type && com.type.toLowerCase() !== 'm' && minify)
1315
1403
  ? " "
1316
1404
  : (
1317
- (com0.type === "m" && com.type === "l") ||
1318
- (com0.type === "M" && com.type === "l") ||
1319
1405
  (com0.type === "M" && com.type === "L")
1320
1406
  ) && minify
1321
1407
  ? " "
@@ -1344,16 +1430,15 @@ function pathDataToD(pathData, optimize = 0) {
1344
1430
  }
1345
1431
 
1346
1432
  valsString += valStr;
1347
-
1348
1433
  prevWasFloat = isSmallFloat;
1349
1434
  }
1350
1435
 
1351
- d += `${type}${valsString}`;
1436
+ d += `${type}${separator_type}${valsString}${separator_command}`;
1352
1437
 
1353
1438
  }
1354
1439
  // regular non-minified output
1355
1440
  else {
1356
- d += `${type} ${values.join(' ')}${suff}`;
1441
+ d += `${type}${separator_type}${values.join(' ')}${separator_command}`;
1357
1442
  }
1358
1443
  }
1359
1444
 
@@ -1957,7 +2042,8 @@ function detectAccuracy(pathData) {
1957
2042
  let p0 = M;
1958
2043
  let p = M;
1959
2044
  pathData[0].decimals = 0;
1960
- let minDim = Infinity;
2045
+
2046
+ let dims = new Set();
1961
2047
 
1962
2048
  // add average distances
1963
2049
  for (let i = 0, len = pathData.length; i < len; i++) {
@@ -1970,8 +2056,7 @@ function detectAccuracy(pathData) {
1970
2056
  // use existing averave dimension value or calculate
1971
2057
  let dimA = com.dimA ? +com.dimA.toFixed(8) : type!=='M' ? +getDistAv(p0, p).toFixed(8) : 0;
1972
2058
 
1973
- if(dimA && dimA<minDim) minDim = dimA;
1974
-
2059
+ if(dimA) dims.add(dimA);
1975
2060
 
1976
2061
 
1977
2062
  if(type==='M'){
@@ -1980,7 +2065,14 @@ function detectAccuracy(pathData) {
1980
2065
  p0 = p;
1981
2066
  }
1982
2067
 
1983
- let decimalsAuto = Math.floor(50 / minDim).toString().length;
2068
+ let dim_min = Array.from(dims).sort();
2069
+ let sliceIdx = Math.ceil(dim_min.length/8);
2070
+ dim_min = dim_min.slice(0, sliceIdx );
2071
+
2072
+ let dimVal = dim_min.reduce((a,b)=>a+b, 0) / sliceIdx;
2073
+
2074
+ let threshold = 50;
2075
+ let decimalsAuto = dimVal > threshold ? 0 : Math.floor(threshold / dimVal).toString().length;
1984
2076
 
1985
2077
  // clamp
1986
2078
  return Math.min(Math.max(0, decimalsAuto), 8)
@@ -3400,33 +3492,37 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3400
3492
  let comPrev = pathData[c - 1];
3401
3493
  let com = pathData[c];
3402
3494
  let comN = pathData[c + 1] || pathData[l - 1];
3403
- let p1 = comN.type === 'Z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
3495
+ let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
3404
3496
 
3405
3497
  let { type, values } = com;
3406
3498
  let valsL = values.slice(-2);
3407
3499
  p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
3408
3500
 
3409
- let cpts = type === 'C' ?
3410
- [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
3411
- (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
3501
+ let area = getPolygonArea([p0, p, p1], true);
3412
3502
 
3413
- let area = getPolygonArea([p0, ...cpts, p, p1], true);
3414
- let distSquare = getSquareDistance(p0, p);
3415
- let distMax = distSquare / 500 * tolerance;
3503
+ let distSquare = getSquareDistance(p0, p1);
3504
+ let distMax = distSquare / 100 * tolerance;
3416
3505
 
3417
- let isFlat = area < distMax;
3418
-
3419
- if(!flatBezierToLinetos && type==='C') isFlat = false;
3506
+ let isFlat = area < distMax;
3507
+ let isFlatBez = false;
3508
+
3509
+ if (!flatBezierToLinetos && type === 'C') isFlat = false;
3420
3510
 
3421
3511
  // convert flat beziers to linetos
3422
- if (flatBezierToLinetos && type === 'C') {
3512
+ if (flatBezierToLinetos && (type === 'C' || type === 'Q')) {
3513
+
3514
+ let cpts = type === 'C' ?
3515
+ [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
3516
+ (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
3423
3517
 
3424
- let areaBez = getPolygonArea([p0, ...cpts, p], true);
3425
- let isFlatBez = areaBez < distSquare / 1000;
3518
+ isFlatBez = checkBezierFlatness(p0, cpts, p);
3519
+ // console.log();
3426
3520
 
3427
- if (isFlatBez && comPrev.type !== 'C') {
3521
+ if (isFlatBez && c < l - 1 && comPrev.type !== 'C') {
3522
+ type = "L";
3428
3523
  com.type = "L";
3429
3524
  com.values = valsL;
3525
+
3430
3526
  }
3431
3527
 
3432
3528
  }
@@ -3435,7 +3531,8 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3435
3531
  p0 = p;
3436
3532
 
3437
3533
  // colinear – exclude arcs (as always =) as semicircles won't have an area
3438
- if (type !== 'A' && isFlat && c < l - 1) {
3534
+ if ( isFlat && c < l - 1 && (type === 'L' || (flatBezierToLinetos && isFlatBez))) {
3535
+
3439
3536
  continue;
3440
3537
  }
3441
3538
 
@@ -3485,11 +3582,43 @@ function removeZeroLengthLinetos(pathData) {
3485
3582
 
3486
3583
  }
3487
3584
 
3488
- function pathDataToTopLeft(pathData, removeFinalLineto = false, reorder = true) {
3585
+ function pathDataToTopLeft(pathData) {
3586
+
3587
+ let len = pathData.length;
3588
+ let isClosed = pathData[len - 1].type.toLowerCase() === 'z';
3589
+
3590
+ // we can't change starting point for non closed paths
3591
+ if (!isClosed) {
3592
+ return pathData
3593
+ }
3594
+
3595
+ let newIndex = 0;
3596
+
3597
+ let indices = [];
3598
+ for (let i = 0; i < len; i++) {
3599
+ let com = pathData[i];
3600
+ let { type, values } = com;
3601
+ let valsLen = values.length;
3602
+ if (valsLen) {
3603
+ let p = { type: type, x: values[valsLen-2], y: values[valsLen-1], index: 0};
3604
+ p.index = i;
3605
+ indices.push(p);
3606
+ }
3607
+ }
3608
+
3609
+ // reorder to top left most
3610
+
3611
+ indices = indices.sort((a, b) => +a.y.toFixed(3) - +b.y.toFixed(3) );
3612
+ newIndex = indices[0].index;
3613
+
3614
+ return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
3615
+ }
3616
+
3617
+ function optimizeClosePath(pathData, removeFinalLineto = true, reorder = true) {
3489
3618
 
3490
3619
  let pathDataNew = [];
3491
3620
  let len = pathData.length;
3492
- let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
3621
+ let M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
3493
3622
  let isClosed = pathData[len - 1].type.toLowerCase() === 'z';
3494
3623
 
3495
3624
  let linetos = pathData.filter(com => com.type === 'L');
@@ -3497,17 +3626,17 @@ function pathDataToTopLeft(pathData, removeFinalLineto = false, reorder = true)
3497
3626
  // check if order is ideal
3498
3627
  let penultimateCom = pathData[len - 2];
3499
3628
  let penultimateType = penultimateCom.type;
3500
- let penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8));
3629
+ let penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
3501
3630
 
3502
3631
  // last L command ends at M
3503
- let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
3632
+ let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
3504
3633
 
3505
3634
  // if last segment is not closing or a lineto
3506
- let skipReorder = pathData[1].type!=='L' && (!isClosingCommand || penultimateType==='L' );
3507
- skipReorder=false;
3635
+ let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand || penultimateType === 'L');
3636
+ skipReorder = false;
3508
3637
 
3509
3638
  // we can't change starting point for non closed paths
3510
- if (!isClosed ) {
3639
+ if (!isClosed) {
3511
3640
  return pathData
3512
3641
  }
3513
3642
 
@@ -3534,15 +3663,15 @@ function pathDataToTopLeft(pathData, removeFinalLineto = false, reorder = true)
3534
3663
  // find top most lineto
3535
3664
 
3536
3665
  if (linetos.length) {
3537
- let curveAfterLine = indices.filter(com => (com.type !== 'L' && com.type !== 'M') && com.prevCom &&
3538
- com.prevCom === 'L' || com.prevCom==='M' && penultimateType==='L' ).sort((a, b) => a.y - b.y || a.x-b.x)[0];
3666
+ let curveAfterLine = indices.filter(com => (com.type !== 'L' && com.type !== 'M') && com.prevCom &&
3667
+ com.prevCom === 'L' || com.prevCom === 'M' && penultimateType === 'L').sort((a, b) => a.y - b.y || a.x - b.x)[0];
3539
3668
 
3540
3669
  newIndex = curveAfterLine ? curveAfterLine.index - 1 : 0;
3541
3670
 
3542
3671
  }
3543
3672
  // use top most command
3544
3673
  else {
3545
- indices = indices.sort((a, b) => +a.y.toFixed(1) - +b.y.toFixed(1) || a.x - b.x );
3674
+ indices = indices.sort((a, b) => +a.y.toFixed(1) - +b.y.toFixed(1) || a.x - b.x);
3546
3675
  newIndex = indices[0].index;
3547
3676
  }
3548
3677
 
@@ -3550,12 +3679,14 @@ function pathDataToTopLeft(pathData, removeFinalLineto = false, reorder = true)
3550
3679
  pathData = newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
3551
3680
  }
3552
3681
 
3682
+ M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(7) };
3683
+
3553
3684
  len = pathData.length;
3554
3685
 
3555
3686
  // remove last lineto
3556
3687
  penultimateCom = pathData[len - 2];
3557
3688
  penultimateType = penultimateCom.type;
3558
- penultimateComCoords = penultimateCom.values.slice(-2);
3689
+ penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8));
3559
3690
 
3560
3691
  isClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
3561
3692
 
@@ -3781,7 +3912,93 @@ function reversePathData(pathData, {
3781
3912
  return pathDataNew;
3782
3913
  }
3783
3914
 
3784
- function svgPathSimplify(d = '', {
3915
+ function cleanUpSVG(svgMarkup, {
3916
+ returnDom=false,
3917
+ removeHidden=true,
3918
+ removeUnused=true,
3919
+ }={}) {
3920
+ svgMarkup = cleanSvgPrologue(svgMarkup);
3921
+
3922
+ // replace namespaced refs
3923
+ svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
3924
+
3925
+ let svg = new DOMParser()
3926
+ .parseFromString(svgMarkup, "text/html")
3927
+ .querySelector("svg");
3928
+
3929
+
3930
+ let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class'];
3931
+ removeExcludedAttribues(svg, allowed);
3932
+
3933
+ let removeEls = ['metadata', 'script'];
3934
+
3935
+ let els = svg.querySelectorAll('*');
3936
+ els.forEach(el=>{
3937
+ let name = el.nodeName;
3938
+ // remove hidden elements
3939
+ let style = el.getAttribute('style') || '';
3940
+ let isHiddenByStyle = style ? style.trim().includes('display:none') : false;
3941
+ let isHidden = (el.getAttribute('display') && el.getAttribute('display') === 'none') || isHiddenByStyle;
3942
+ if(name.includes(':') || removeEls.includes(name) || (removeHidden && isHidden )) {
3943
+ el.remove();
3944
+ }else {
3945
+ // remove BS elements
3946
+ removeNameSpaceAtts(el);
3947
+ }
3948
+ });
3949
+
3950
+ if(returnDom) return svg
3951
+
3952
+ let markup = stringifySVG(svg);
3953
+ console.log(markup);
3954
+
3955
+ return markup;
3956
+ }
3957
+
3958
+ function cleanSvgPrologue(svgString) {
3959
+ return (
3960
+ svgString
3961
+ // Remove XML prologues like <?xml ... ?>
3962
+ .replace(/<\?xml[\s\S]*?\?>/gi, "")
3963
+ // Remove DOCTYPE declarations
3964
+ .replace(/<!DOCTYPE[\s\S]*?>/gi, "")
3965
+ // Remove comments <!-- ... -->
3966
+ .replace(/<!--[\s\S]*?-->/g, "")
3967
+ // Trim extra whitespace
3968
+ .trim()
3969
+ );
3970
+ }
3971
+
3972
+ function removeExcludedAttribues(el, allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class']){
3973
+ let atts = [...el.attributes].map((att) => att.name);
3974
+ atts.forEach((att) => {
3975
+ if (!allowed.includes(att)) {
3976
+ el.removeAttribute(att);
3977
+ }
3978
+ });
3979
+ }
3980
+
3981
+ function removeNameSpaceAtts(el) {
3982
+ let atts = [...el.attributes].map((att) => att.name);
3983
+ atts.forEach((att) => {
3984
+ if (att.includes(":")) {
3985
+ el.removeAttribute(att);
3986
+ }
3987
+ });
3988
+ }
3989
+
3990
+ function stringifySVG(svg){
3991
+ let markup = new XMLSerializer().serializeToString(svg);
3992
+ markup = markup
3993
+ .replace(/\t/g, "")
3994
+ .replace(/[\n\r|]/g, "\n")
3995
+ .replace(/\n\s*\n/g, '\n')
3996
+ .replace(/ +/g, ' ');
3997
+
3998
+ return markup
3999
+ }
4000
+
4001
+ function svgPathSimplify(input = '', {
3785
4002
  toAbsolute = true,
3786
4003
  toRelative = true,
3787
4004
  toShorthands = true,
@@ -3809,126 +4026,233 @@ function svgPathSimplify(d = '', {
3809
4026
  revertToQuadratics = true,
3810
4027
  minifyD = 0,
3811
4028
  tolerance = 1,
3812
- reverse = false
4029
+ reverse = false,
4030
+
4031
+ // svg cleanup options
4032
+ removeHidden = true,
4033
+ removeUnused = true,
4034
+
4035
+ // return svg markup or object
4036
+ getObject = false
4037
+
3813
4038
  } = {}) {
3814
4039
 
3815
- let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
4040
+ // clamp tolerance
4041
+ tolerance = Math.max(0.1, tolerance);
4042
+
4043
+ let inputType = detectInputType(input);
3816
4044
 
3817
- // create clone for fallback
3818
- let pathData = JSON.parse(JSON.stringify(pathDataO));
4045
+ let svg = '';
4046
+ let svgSize = 0;
4047
+ let svgSizeOpt = 0;
4048
+ let compression = 0;
4049
+ let report = {};
4050
+ let d = '';
4051
+ let mode = inputType === 'svgMarkup' ? 1 : 0;
3819
4052
 
3820
- // count commands for evaluation
3821
- let comCount = pathDataO.length;
4053
+ let paths = [];
3822
4054
 
3823
4055
  /**
3824
- * get sub paths
4056
+ * normalize input
4057
+ * switch mode
3825
4058
  */
3826
- let subPathArr = splitSubpaths(pathData);
3827
4059
 
3828
- // cleaned up pathData
3829
- let pathDataArrN = [];
4060
+ // original size
4061
+ svgSize = new Blob([input]).size;
3830
4062
 
3831
- for (let i = 0, l = subPathArr.length; i < l; i++) {
4063
+ // single path
4064
+ if (!mode) {
4065
+ if (inputType === 'pathDataString') {
4066
+ d = input;
4067
+ } else if (inputType === 'polyString') {
4068
+ d = 'M' + input;
4069
+ }
4070
+ paths.push({ d, el: null });
4071
+ }
4072
+ // process svg
4073
+ else {
3832
4074
 
3833
- let pathDataSub = subPathArr[i];
4075
+ let returnDom = true;
4076
+ svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
4077
+ );
3834
4078
 
3835
- // try simplification in reversed order
3836
- if (reverse) pathDataSub = reversePathData(pathDataSub);
4079
+ // collect paths
4080
+ let pathEls = svg.querySelectorAll('path');
4081
+ pathEls.forEach(path => {
4082
+ paths.push({ d: path.getAttribute('d'), el: path });
4083
+ });
4084
+ }
3837
4085
 
3838
- // remove zero length linetos
3839
- if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub);
4086
+ /**
4087
+ * process all paths
4088
+ */
4089
+ paths.forEach(path => {
4090
+ let { d, el } = path;
3840
4091
 
3841
- // add extremes
4092
+ let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
3842
4093
 
3843
- let tMin = 0, tMax = 1;
3844
- if (addExtremes) pathDataSub = addExtremePoints(pathDataSub, tMin, tMax);
4094
+ // create clone for fallback
4095
+ let pathData = JSON.parse(JSON.stringify(pathDataO));
3845
4096
 
3846
- // sort to top left
3847
- if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
4097
+ // count commands for evaluation
4098
+ let comCount = pathDataO.length;
3848
4099
 
3849
- // remove colinear/flat
3850
- if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
4100
+ /**
4101
+ * get sub paths
4102
+ */
4103
+ let subPathArr = splitSubpaths(pathData);
3851
4104
 
3852
- // analyze pathdata to add info about signicant properties such as extremes, corners
3853
- let pathDataPlus = analyzePathData(pathDataSub);
4105
+ // cleaned up pathData
4106
+ let pathDataArrN = [];
3854
4107
 
3855
- // simplify beziers
3856
- let { pathData, bb, dimA } = pathDataPlus;
4108
+ for (let i = 0, l = subPathArr.length; i < l; i++) {
3857
4109
 
3858
- let pathDataN = pathData;
4110
+ let pathDataSub = subPathArr[i];
3859
4111
 
3860
-
3861
- pathDataN = simplifyBezier ? simplifyPathData(pathDataN, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathDataN;
4112
+ // try simplification in reversed order
4113
+ if (reverse) pathDataSub = reversePathData(pathDataSub);
3862
4114
 
3863
- // cubic to arcs
3864
- if(cubicToArc){
4115
+ // remove zero length linetos
4116
+ if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub);
3865
4117
 
3866
- let thresh = 3;
4118
+ // add extremes
3867
4119
 
3868
- pathDataN.forEach((com, c) => {
3869
- let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
3870
- if (type === 'C') {
4120
+ let tMin = 0, tMax = 1;
4121
+ if (addExtremes) pathDataSub = addExtremePoints(pathDataSub, tMin, tMax);
3871
4122
 
3872
- let comA = cubicCommandToArc(p0, cp1, cp2, p, thresh);
3873
- if(comA.isArc) pathDataN[c] = comA.com;
4123
+ // sort to top left
4124
+ if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
3874
4125
 
3875
- }
3876
- });
4126
+ // remove colinear/flat
4127
+ if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
4128
+
4129
+ // analyze pathdata to add info about signicant properties such as extremes, corners
4130
+ let pathDataPlus = analyzePathData(pathDataSub);
4131
+
4132
+ // simplify beziers
4133
+ let { pathData, bb, dimA } = pathDataPlus;
4134
+
4135
+ pathData = simplifyBezier ? simplifyPathData(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
4136
+
4137
+ // cubic to arcs
4138
+ if (cubicToArc) {
4139
+
4140
+ let thresh = 3;
4141
+
4142
+ pathData.forEach((com, c) => {
4143
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4144
+ if (type === 'C') {
4145
+
4146
+ let comA = cubicCommandToArc(p0, cp1, cp2, p, thresh);
4147
+ if (comA.isArc) pathData[c] = comA.com;
4148
+
4149
+ }
4150
+ });
4151
+
4152
+ // combine adjacent cubics
4153
+ pathData = combineArcs(pathData);
3877
4154
 
3878
- // combine adjacent cubics
3879
- pathDataN = combineArcs(pathDataN);
4155
+ }
4156
+
4157
+ // simplify to quadratics
4158
+ if (revertToQuadratics) {
4159
+ pathData.forEach((com, c) => {
4160
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4161
+ if (type === 'C') {
3880
4162
 
4163
+ let comQ = revertCubicQuadratic(p0, cp1, cp2, p);
4164
+ if (comQ.type === 'Q') pathData[c] = comQ;
4165
+ }
4166
+ });
4167
+ }
4168
+
4169
+ // optimize close path
4170
+ if(optimizeOrder) pathData=optimizeClosePath(pathData);
4171
+
4172
+ // update
4173
+ pathDataArrN.push(pathData);
3881
4174
  }
3882
4175
 
3883
- // simplify to quadratics
3884
- if (revertToQuadratics) {
3885
- pathDataN.forEach((com, c) => {
3886
- let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
3887
- if (type === 'C') {
4176
+
4177
+ // flatten compound paths
4178
+ pathData = pathDataArrN.flat();
3888
4179
 
3889
- let comQ = revertCubicQuadratic(p0, cp1, cp2, p);
3890
- if (comQ.type === 'Q') pathDataN[c] = comQ;
3891
- }
3892
- });
4180
+ /**
4181
+ * detect accuracy
4182
+ */
4183
+ if (autoAccuracy) {
4184
+ decimals = detectAccuracy(pathData);
3893
4185
  }
3894
4186
 
3895
- // update
3896
- pathDataArrN.push(pathDataN);
3897
- }
4187
+ // optimize
4188
+ let pathOptions = {
4189
+ toRelative,
4190
+ toShorthands,
4191
+ decimals,
4192
+ };
3898
4193
 
3899
- // merge pathdata
3900
- let pathDataFlat = pathDataArrN.flat();
4194
+ // optimize path data
4195
+ pathData = convertPathData(pathData, pathOptions);
3901
4196
 
3902
- /**
3903
- * detect accuracy
3904
- */
3905
- if (autoAccuracy) {
3906
- decimals = detectAccuracy(pathDataFlat);
3907
- }
4197
+ // remove zero-length segments introduced by rounding
4198
+ let pathDataOpt = [];
4199
+
4200
+ pathData.forEach((com, i) => {
4201
+ let { type, values } = com;
4202
+ if (type === 'l' || type === 'v' || type === 'h') {
4203
+ let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0;
4204
+ if (hasLength) pathDataOpt.push(com);
4205
+ } else {
4206
+ pathDataOpt.push(com);
4207
+ }
4208
+ });
3908
4209
 
3909
- // compare command count
3910
- let comCountS = pathDataFlat.length;
4210
+ pathData = pathDataOpt;
3911
4211
 
3912
- // optimize
3913
- let pathOptions = {
3914
- toRelative,
3915
- toShorthands,
3916
- decimals,
3917
- };
4212
+ // compare command count
4213
+ let comCountS = pathData.length;
3918
4214
 
3919
- // optimize path data
3920
- pathData = convertPathData(pathDataFlat, pathOptions);
3921
- let dOpt = pathDataToD(pathData, minifyD);
4215
+ let dOpt = pathDataToD(pathData, minifyD);
4216
+ svgSizeOpt = new Blob([dOpt]).size;
3922
4217
 
3923
- let report = {
3924
- original: comCount,
3925
- new: comCountS,
3926
- saved: comCount - comCountS,
3927
- decimals,
3928
- success: comCountS < comCount
3929
- };
4218
+ compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4219
+
4220
+ path.d = dOpt;
4221
+ path.report = {
4222
+ original: comCount,
4223
+ new: comCountS,
4224
+ saved: comCount - comCountS,
4225
+ compression,
4226
+ decimals,
4227
+
4228
+ };
4229
+
4230
+ // apply new path for svgs
4231
+ if (el) el.setAttribute('d', dOpt);
4232
+
4233
+ });
3930
4234
 
3931
- return { pathData, d: dOpt, report };
4235
+ // stringify new SVG
4236
+ if (mode) {
4237
+ svg = new XMLSerializer().serializeToString(svg);
4238
+ svgSizeOpt = new Blob([svg]).size;
4239
+
4240
+ compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4241
+
4242
+ svgSize = +(svgSize / 1024).toFixed(3);
4243
+ svgSizeOpt = +(svgSizeOpt / 1024).toFixed(3);
4244
+
4245
+ report = {
4246
+ svgSize,
4247
+ svgSizeOpt,
4248
+ compression
4249
+ };
4250
+
4251
+ } else {
4252
+ ({ d, report } = paths[0]);
4253
+ }
4254
+
4255
+ return !getObject ? (d ? d : svg) : { svg, d, report, inputType, mode };
3932
4256
 
3933
4257
  }
3934
4258
 
@@ -3958,9 +4282,9 @@ function simplifyPathData(pathData, {
3958
4282
 
3959
4283
  // cannot be combined as crossing extremes or corners
3960
4284
  if (
3961
- (keepInflections && isDirChangeN) ||
3962
- (keepCorners && corner) ||
3963
- (!isDirChange && keepExtremes && extreme)
4285
+ (keepInflections && isDirChangeN) ||
4286
+ (keepCorners && corner) ||
4287
+ (!isDirChange && keepExtremes && extreme)
3964
4288
  ) {
3965
4289
 
3966
4290
  pathDataN.push(com);
@@ -4024,6 +4348,39 @@ function simplifyPathData(pathData, {
4024
4348
  return pathDataN
4025
4349
  }
4026
4350
 
4351
+ /**
4352
+ * get viewBox
4353
+ * either from explicit attribute or
4354
+ * width and height attributes
4355
+ */
4356
+
4357
+ function getViewBox(svg = null, round = false) {
4358
+
4359
+ // browser default
4360
+ if (!svg) return { x: 0, y: 0, width: 300, height: 150 }
4361
+
4362
+ let style = window.getComputedStyle(svg);
4363
+
4364
+ // the baseVal API method also converts physical units to pixels/user-units
4365
+ let w = svg.hasAttribute('width') ? svg.width.baseVal.value : parseFloat(style.width) || 300;
4366
+ let h = svg.hasAttribute('height') ? svg.height.baseVal.value : parseFloat(style.height) || 150;
4367
+
4368
+ let viewBox = svg.getAttribute('viewBox') ? svg.viewBox.baseVal : { x: 0, y: 0, width: w, height: h };
4369
+
4370
+ // remove SVG constructor
4371
+ let { x, y, width, height } = viewBox;
4372
+ viewBox = { x, y, width, height };
4373
+
4374
+ // round to integers
4375
+ if (round) {
4376
+ for (let prop in viewBox) {
4377
+ viewBox[prop] = Math.ceil(viewBox[prop]);
4378
+ }
4379
+ }
4380
+
4381
+ return viewBox
4382
+ }
4383
+
4027
4384
  const {
4028
4385
  abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
4029
4386
  log, hypot, max, min, pow, random, round, sin, sqrt, tan, PI
@@ -4034,7 +4391,8 @@ const {
4034
4391
  // IIFE
4035
4392
  if (typeof window !== 'undefined') {
4036
4393
  window.svgPathSimplify = svgPathSimplify;
4394
+ window.getViewBox = getViewBox;
4037
4395
  window.renderPoint = renderPoint;
4038
4396
  }
4039
4397
 
4040
- export { PI, abs, acos, asin, atan, atan2, ceil, cos, exp, floor, hypot, log, max, min, pow, random, round, sin, sqrt, svgPathSimplify, tan };
4398
+ export { PI, abs, acos, asin, atan, atan2, ceil, cos, exp, floor, getViewBox, hypot, log, max, min, pow, random, round, sin, sqrt, svgPathSimplify, tan };