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