svg-path-simplify 0.0.2 → 0.0.4

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,87 +3585,35 @@
3488
3585
 
3489
3586
  }
3490
3587
 
3491
- function pathDataToTopLeft(pathData, removeFinalLineto = false, reorder = true) {
3588
+ function pathDataToTopLeft(pathData) {
3492
3589
 
3493
- let pathDataNew = [];
3494
3590
  let len = pathData.length;
3495
- let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
3496
3591
  let isClosed = pathData[len - 1].type.toLowerCase() === 'z';
3497
3592
 
3498
- let linetos = pathData.filter(com => com.type === 'L');
3499
-
3500
- // check if order is ideal
3501
- let penultimateCom = pathData[len - 2];
3502
- let penultimateType = penultimateCom.type;
3503
- let penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8));
3504
-
3505
- // last L command ends at M
3506
- let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
3507
-
3508
- // if last segment is not closing or a lineto
3509
- let skipReorder = pathData[1].type!=='L' && (!isClosingCommand || penultimateType==='L' );
3510
- skipReorder=false;
3511
-
3512
3593
  // we can't change starting point for non closed paths
3513
- if (!isClosed ) {
3594
+ if (!isClosed) {
3514
3595
  return pathData
3515
3596
  }
3516
3597
 
3517
3598
  let newIndex = 0;
3518
3599
 
3519
- if (!skipReorder) {
3520
-
3521
- let indices = [];
3522
- for (let i = 0, len = pathData.length; i < len; i++) {
3523
- let com = pathData[i];
3524
- let { type, values } = com;
3525
- if (values.length) {
3526
- let valsL = values.slice(-2);
3527
- let prevL = pathData[i - 1] && pathData[i - 1].type === 'L';
3528
- let nextL = pathData[i + 1] && pathData[i + 1].type === 'L';
3529
- let prevCom = pathData[i - 1] ? pathData[i - 1].type.toUpperCase() : null;
3530
- let nextCom = pathData[i + 1] ? pathData[i + 1].type.toUpperCase() : null;
3531
- let p = { type: type, x: valsL[0], y: valsL[1], dist: 0, index: 0, prevL, nextL, prevCom, nextCom };
3532
- p.index = i;
3533
- indices.push(p);
3534
- }
3535
- }
3536
-
3537
- // find top most lineto
3538
-
3539
- 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];
3542
-
3543
- newIndex = curveAfterLine ? curveAfterLine.index - 1 : 0;
3544
-
3545
- }
3546
- // use top most command
3547
- else {
3548
- indices = indices.sort((a, b) => +a.y.toFixed(1) - +b.y.toFixed(1) || a.x - b.x );
3549
- newIndex = indices[0].index;
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);
3550
3609
  }
3551
-
3552
- // reorder
3553
- pathData = newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
3554
- }
3555
-
3556
- len = pathData.length;
3557
-
3558
- // remove last lineto
3559
- penultimateCom = pathData[len - 2];
3560
- penultimateType = penultimateCom.type;
3561
- penultimateComCoords = penultimateCom.values.slice(-2);
3562
-
3563
- isClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
3564
-
3565
- if (removeFinalLineto && isClosingCommand) {
3566
- pathData.splice(len - 2, 1);
3567
3610
  }
3568
3611
 
3569
- pathDataNew.push(...pathData);
3612
+ // reorder to top left most
3613
+ indices = indices.sort((a, b) => +a.y.toFixed(3) - +b.y.toFixed(3) || a.x - b.x);
3614
+ newIndex = indices[0].index;
3570
3615
 
3571
- return pathDataNew
3616
+ return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
3572
3617
  }
3573
3618
 
3574
3619
  /**
@@ -3784,7 +3829,93 @@
3784
3829
  return pathDataNew;
3785
3830
  }
3786
3831
 
3787
- function svgPathSimplify(d = '', {
3832
+ function cleanUpSVG(svgMarkup, {
3833
+ returnDom=false,
3834
+ removeHidden=true,
3835
+ removeUnused=true,
3836
+ }={}) {
3837
+ svgMarkup = cleanSvgPrologue(svgMarkup);
3838
+
3839
+ // replace namespaced refs
3840
+ svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
3841
+
3842
+ let svg = new DOMParser()
3843
+ .parseFromString(svgMarkup, "text/html")
3844
+ .querySelector("svg");
3845
+
3846
+
3847
+ let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class'];
3848
+ removeExcludedAttribues(svg, allowed);
3849
+
3850
+ let removeEls = ['metadata', 'script'];
3851
+
3852
+ let els = svg.querySelectorAll('*');
3853
+ els.forEach(el=>{
3854
+ let name = el.nodeName;
3855
+ // remove hidden elements
3856
+ let style = el.getAttribute('style') || '';
3857
+ let isHiddenByStyle = style ? style.trim().includes('display:none') : false;
3858
+ let isHidden = (el.getAttribute('display') && el.getAttribute('display') === 'none') || isHiddenByStyle;
3859
+ if(name.includes(':') || removeEls.includes(name) || (removeHidden && isHidden )) {
3860
+ el.remove();
3861
+ }else {
3862
+ // remove BS elements
3863
+ removeNameSpaceAtts(el);
3864
+ }
3865
+ });
3866
+
3867
+ if(returnDom) return svg
3868
+
3869
+ let markup = stringifySVG(svg);
3870
+ console.log(markup);
3871
+
3872
+ return markup;
3873
+ }
3874
+
3875
+ function cleanSvgPrologue(svgString) {
3876
+ return (
3877
+ svgString
3878
+ // Remove XML prologues like <?xml ... ?>
3879
+ .replace(/<\?xml[\s\S]*?\?>/gi, "")
3880
+ // Remove DOCTYPE declarations
3881
+ .replace(/<!DOCTYPE[\s\S]*?>/gi, "")
3882
+ // Remove comments <!-- ... -->
3883
+ .replace(/<!--[\s\S]*?-->/g, "")
3884
+ // Trim extra whitespace
3885
+ .trim()
3886
+ );
3887
+ }
3888
+
3889
+ function removeExcludedAttribues(el, allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class']){
3890
+ let atts = [...el.attributes].map((att) => att.name);
3891
+ atts.forEach((att) => {
3892
+ if (!allowed.includes(att)) {
3893
+ el.removeAttribute(att);
3894
+ }
3895
+ });
3896
+ }
3897
+
3898
+ function removeNameSpaceAtts(el) {
3899
+ let atts = [...el.attributes].map((att) => att.name);
3900
+ atts.forEach((att) => {
3901
+ if (att.includes(":")) {
3902
+ el.removeAttribute(att);
3903
+ }
3904
+ });
3905
+ }
3906
+
3907
+ function stringifySVG(svg){
3908
+ let markup = new XMLSerializer().serializeToString(svg);
3909
+ markup = markup
3910
+ .replace(/\t/g, "")
3911
+ .replace(/[\n\r|]/g, "\n")
3912
+ .replace(/\n\s*\n/g, '\n')
3913
+ .replace(/ +/g, ' ');
3914
+
3915
+ return markup
3916
+ }
3917
+
3918
+ function svgPathSimplify(input = '', {
3788
3919
  toAbsolute = true,
3789
3920
  toRelative = true,
3790
3921
  toShorthands = true,
@@ -3812,126 +3943,229 @@
3812
3943
  revertToQuadratics = true,
3813
3944
  minifyD = 0,
3814
3945
  tolerance = 1,
3815
- reverse = false
3946
+ reverse = false,
3947
+
3948
+ // svg cleanup options
3949
+ removeHidden = true,
3950
+ removeUnused = true,
3951
+
3952
+ // return svg markup or object
3953
+ getObject = false
3954
+
3816
3955
  } = {}) {
3817
3956
 
3818
- let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
3957
+ // clamp tolerance
3958
+ tolerance = Math.max(0.1, tolerance);
3959
+
3960
+ let inputType = detectInputType(input);
3819
3961
 
3820
- // create clone for fallback
3821
- let pathData = JSON.parse(JSON.stringify(pathDataO));
3962
+ let svg = '';
3963
+ let svgSize = 0;
3964
+ let svgSizeOpt = 0;
3965
+ let compression = 0;
3966
+ let report = {};
3967
+ let d = '';
3968
+ let mode = inputType === 'svgMarkup' ? 1 : 0;
3822
3969
 
3823
- // count commands for evaluation
3824
- let comCount = pathDataO.length;
3970
+ let paths = [];
3825
3971
 
3826
3972
  /**
3827
- * get sub paths
3973
+ * normalize input
3974
+ * switch mode
3828
3975
  */
3829
- let subPathArr = splitSubpaths(pathData);
3830
3976
 
3831
- // cleaned up pathData
3832
- let pathDataArrN = [];
3977
+ // original size
3978
+ svgSize = new Blob([input]).size;
3833
3979
 
3834
- for (let i = 0, l = subPathArr.length; i < l; i++) {
3980
+ // single path
3981
+ if (!mode) {
3982
+ if (inputType === 'pathDataString') {
3983
+ d = input;
3984
+ } else if (inputType === 'polyString') {
3985
+ d = 'M' + input;
3986
+ }
3987
+ paths.push({ d, el: null });
3988
+ }
3989
+ // process svg
3990
+ else {
3835
3991
 
3836
- let pathDataSub = subPathArr[i];
3992
+ let returnDom = true;
3993
+ svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
3994
+ );
3837
3995
 
3838
- // try simplification in reversed order
3839
- if (reverse) pathDataSub = reversePathData(pathDataSub);
3996
+ // collect paths
3997
+ let pathEls = svg.querySelectorAll('path');
3998
+ pathEls.forEach(path => {
3999
+ paths.push({ d: path.getAttribute('d'), el: path });
4000
+ });
4001
+ }
3840
4002
 
3841
- // remove zero length linetos
3842
- if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub);
4003
+ /**
4004
+ * process all paths
4005
+ */
4006
+ paths.forEach(path => {
4007
+ let { d, el } = path;
3843
4008
 
3844
- // add extremes
4009
+ let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
3845
4010
 
3846
- let tMin = 0, tMax = 1;
3847
- if (addExtremes) pathDataSub = addExtremePoints(pathDataSub, tMin, tMax);
4011
+ // create clone for fallback
4012
+ let pathData = JSON.parse(JSON.stringify(pathDataO));
3848
4013
 
3849
- // sort to top left
3850
- if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
4014
+ // count commands for evaluation
4015
+ let comCount = pathDataO.length;
3851
4016
 
3852
- // remove colinear/flat
3853
- if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
4017
+ /**
4018
+ * get sub paths
4019
+ */
4020
+ let subPathArr = splitSubpaths(pathData);
3854
4021
 
3855
- // analyze pathdata to add info about signicant properties such as extremes, corners
3856
- let pathDataPlus = analyzePathData(pathDataSub);
4022
+ // cleaned up pathData
4023
+ let pathDataArrN = [];
3857
4024
 
3858
- // simplify beziers
3859
- let { pathData, bb, dimA } = pathDataPlus;
4025
+ for (let i = 0, l = subPathArr.length; i < l; i++) {
3860
4026
 
3861
- let pathDataN = pathData;
4027
+ let pathDataSub = subPathArr[i];
3862
4028
 
3863
-
3864
- pathDataN = simplifyBezier ? simplifyPathData(pathDataN, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathDataN;
4029
+ // try simplification in reversed order
4030
+ if (reverse) pathDataSub = reversePathData(pathDataSub);
3865
4031
 
3866
- // cubic to arcs
3867
- if(cubicToArc){
4032
+ // remove zero length linetos
4033
+ if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub);
3868
4034
 
3869
- let thresh = 3;
4035
+ // add extremes
3870
4036
 
3871
- pathDataN.forEach((com, c) => {
3872
- let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
3873
- if (type === 'C') {
4037
+ let tMin = 0, tMax = 1;
4038
+ if (addExtremes) pathDataSub = addExtremePoints(pathDataSub, tMin, tMax);
3874
4039
 
3875
- let comA = cubicCommandToArc(p0, cp1, cp2, p, thresh);
3876
- if(comA.isArc) pathDataN[c] = comA.com;
4040
+ // sort to top left
4041
+ if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
3877
4042
 
3878
- }
3879
- });
4043
+ // remove colinear/flat
4044
+ if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
4045
+
4046
+ // analyze pathdata to add info about signicant properties such as extremes, corners
4047
+ let pathDataPlus = analyzePathData(pathDataSub);
4048
+
4049
+ // simplify beziers
4050
+ let { pathData, bb, dimA } = pathDataPlus;
4051
+
4052
+ pathData = simplifyBezier ? simplifyPathData(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
4053
+
4054
+ // cubic to arcs
4055
+ if (cubicToArc) {
4056
+
4057
+ let thresh = 3;
3880
4058
 
3881
- // combine adjacent cubics
3882
- pathDataN = combineArcs(pathDataN);
4059
+ pathData.forEach((com, c) => {
4060
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4061
+ if (type === 'C') {
4062
+
4063
+ let comA = cubicCommandToArc(p0, cp1, cp2, p, thresh);
4064
+ if (comA.isArc) pathData[c] = comA.com;
4065
+
4066
+ }
4067
+ });
4068
+
4069
+ // combine adjacent cubics
4070
+ pathData = combineArcs(pathData);
4071
+
4072
+ }
4073
+
4074
+ // simplify to quadratics
4075
+ if (revertToQuadratics) {
4076
+ pathData.forEach((com, c) => {
4077
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4078
+ if (type === 'C') {
4079
+
4080
+ let comQ = revertCubicQuadratic(p0, cp1, cp2, p);
4081
+ if (comQ.type === 'Q') pathData[c] = comQ;
4082
+ }
4083
+ });
4084
+ }
3883
4085
 
4086
+ // update
4087
+ pathDataArrN.push(pathData);
3884
4088
  }
3885
4089
 
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') {
4090
+ // flatten compound paths
4091
+ pathData = pathDataArrN.flat();
3891
4092
 
3892
- let comQ = revertCubicQuadratic(p0, cp1, cp2, p);
3893
- if (comQ.type === 'Q') pathDataN[c] = comQ;
3894
- }
3895
- });
4093
+ /**
4094
+ * detect accuracy
4095
+ */
4096
+ if (autoAccuracy) {
4097
+ decimals = detectAccuracy(pathData);
3896
4098
  }
3897
4099
 
3898
- // update
3899
- pathDataArrN.push(pathDataN);
3900
- }
4100
+ // optimize
4101
+ let pathOptions = {
4102
+ toRelative,
4103
+ toShorthands,
4104
+ decimals,
4105
+ };
3901
4106
 
3902
- // merge pathdata
3903
- let pathDataFlat = pathDataArrN.flat();
4107
+ // optimize path data
4108
+ pathData = convertPathData(pathData, pathOptions);
3904
4109
 
3905
- /**
3906
- * detect accuracy
3907
- */
3908
- if (autoAccuracy) {
3909
- decimals = detectAccuracy(pathDataFlat);
3910
- }
4110
+ // remove zero-length segments introduced by rounding
4111
+ let pathDataOpt = [];
3911
4112
 
3912
- // compare command count
3913
- let comCountS = pathDataFlat.length;
4113
+ pathData.forEach((com, i) => {
4114
+ let { type, values } = com;
4115
+ if (type === 'l' || type === 'v' || type === 'h') {
4116
+ let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0;
4117
+ if (hasLength) pathDataOpt.push(com);
4118
+ } else {
4119
+ pathDataOpt.push(com);
4120
+ }
4121
+ });
3914
4122
 
3915
- // optimize
3916
- let pathOptions = {
3917
- toRelative,
3918
- toShorthands,
3919
- decimals,
3920
- };
4123
+ pathData = pathDataOpt;
3921
4124
 
3922
- // optimize path data
3923
- pathData = convertPathData(pathDataFlat, pathOptions);
3924
- let dOpt = pathDataToD(pathData, minifyD);
4125
+ // compare command count
4126
+ let comCountS = pathData.length;
3925
4127
 
3926
- let report = {
3927
- original: comCount,
3928
- new: comCountS,
3929
- saved: comCount - comCountS,
3930
- decimals,
3931
- success: comCountS < comCount
3932
- };
4128
+ let dOpt = pathDataToD(pathData, minifyD);
4129
+ svgSizeOpt = new Blob([dOpt]).size;
4130
+
4131
+ compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4132
+
4133
+ path.d = dOpt;
4134
+ path.report = {
4135
+ original: comCount,
4136
+ new: comCountS,
4137
+ saved: comCount - comCountS,
4138
+ compression,
4139
+ decimals,
4140
+
4141
+ };
4142
+
4143
+ // apply new path for svgs
4144
+ if (el) el.setAttribute('d', dOpt);
4145
+
4146
+ });
3933
4147
 
3934
- return { pathData, d: dOpt, report };
4148
+ // stringify new SVG
4149
+ if (mode) {
4150
+ svg = new XMLSerializer().serializeToString(svg);
4151
+ svgSizeOpt = new Blob([svg]).size;
4152
+
4153
+ compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4154
+
4155
+ svgSize = +(svgSize / 1024).toFixed(3);
4156
+ svgSizeOpt = +(svgSizeOpt / 1024).toFixed(3);
4157
+
4158
+ report = {
4159
+ svgSize,
4160
+ svgSizeOpt,
4161
+ compression
4162
+ };
4163
+
4164
+ } else {
4165
+ ({ d, report } = paths[0]);
4166
+ }
4167
+
4168
+ return !getObject ? (d ? d : svg) : { svg, d, report, inputType, mode };
3935
4169
 
3936
4170
  }
3937
4171
 
@@ -3961,9 +4195,9 @@
3961
4195
 
3962
4196
  // cannot be combined as crossing extremes or corners
3963
4197
  if (
3964
- (keepInflections && isDirChangeN) ||
3965
- (keepCorners && corner) ||
3966
- (!isDirChange && keepExtremes && extreme)
4198
+ (keepInflections && isDirChangeN) ||
4199
+ (keepCorners && corner) ||
4200
+ (!isDirChange && keepExtremes && extreme)
3967
4201
  ) {
3968
4202
 
3969
4203
  pathDataN.push(com);
@@ -4027,6 +4261,39 @@
4027
4261
  return pathDataN
4028
4262
  }
4029
4263
 
4264
+ /**
4265
+ * get viewBox
4266
+ * either from explicit attribute or
4267
+ * width and height attributes
4268
+ */
4269
+
4270
+ function getViewBox(svg = null, round = false) {
4271
+
4272
+ // browser default
4273
+ if (!svg) return { x: 0, y: 0, width: 300, height: 150 }
4274
+
4275
+ let style = window.getComputedStyle(svg);
4276
+
4277
+ // the baseVal API method also converts physical units to pixels/user-units
4278
+ let w = svg.hasAttribute('width') ? svg.width.baseVal.value : parseFloat(style.width) || 300;
4279
+ let h = svg.hasAttribute('height') ? svg.height.baseVal.value : parseFloat(style.height) || 150;
4280
+
4281
+ let viewBox = svg.getAttribute('viewBox') ? svg.viewBox.baseVal : { x: 0, y: 0, width: w, height: h };
4282
+
4283
+ // remove SVG constructor
4284
+ let { x, y, width, height } = viewBox;
4285
+ viewBox = { x, y, width, height };
4286
+
4287
+ // round to integers
4288
+ if (round) {
4289
+ for (let prop in viewBox) {
4290
+ viewBox[prop] = Math.ceil(viewBox[prop]);
4291
+ }
4292
+ }
4293
+
4294
+ return viewBox
4295
+ }
4296
+
4030
4297
  const {
4031
4298
  abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
4032
4299
  log, hypot, max, min, pow, random, round, sin, sqrt, tan, PI
@@ -4037,6 +4304,7 @@
4037
4304
  // IIFE
4038
4305
  if (typeof window !== 'undefined') {
4039
4306
  window.svgPathSimplify = svgPathSimplify;
4307
+ window.getViewBox = getViewBox;
4040
4308
  window.renderPoint = renderPoint;
4041
4309
  }
4042
4310
 
@@ -4050,6 +4318,7 @@
4050
4318
  exports.cos = cos;
4051
4319
  exports.exp = exp;
4052
4320
  exports.floor = floor;
4321
+ exports.getViewBox = getViewBox;
4053
4322
  exports.hypot = hypot;
4054
4323
  exports.log = log;
4055
4324
  exports.max = max;