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