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