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.
- package/LICENSE +339 -0
- package/README.md +33 -1
- package/dist/svg-path-simplify.esm.js +489 -131
- package/dist/svg-path-simplify.esm.min.js +1 -1
- package/dist/svg-path-simplify.js +489 -130
- package/dist/svg-path-simplify.min.js +1 -1
- package/dist/svg-path-simplify.node.js +489 -130
- package/dist/svg-path-simplify.node.min.js +1 -1
- package/index.html +17 -9
- package/package.json +4 -5
- package/src/detect_input.js +42 -0
- package/src/index.js +3 -0
- package/src/pathSimplify-main.js +198 -90
- package/src/svg_getViewbox.js +32 -0
- package/src/svgii/geometry.js +51 -4
- package/src/svgii/pathData_remove_collinear.js +31 -18
- package/src/svgii/pathData_reorder.js +50 -11
- package/src/svgii/pathData_stringify.js +11 -12
- package/src/svgii/rounding.js +14 -6
- package/src/svgii/svg_cleanup.js +7 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
644
|
-
let
|
|
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
|
|
1385
|
+
let separator_command = beautify ? `\n` : (minify ? '' : ' ');
|
|
1386
|
+
let separator_type = !minify ? ' ' : '';
|
|
1295
1387
|
|
|
1296
|
-
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
|
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
|
|
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 === '
|
|
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
|
|
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
|
|
3417
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
3428
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4043
|
+
// clamp tolerance
|
|
4044
|
+
tolerance = Math.max(0.1, tolerance);
|
|
4045
|
+
|
|
4046
|
+
let inputType = detectInputType(input);
|
|
3819
4047
|
|
|
3820
|
-
|
|
3821
|
-
let
|
|
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
|
-
|
|
3824
|
-
let comCount = pathDataO.length;
|
|
4056
|
+
let paths = [];
|
|
3825
4057
|
|
|
3826
4058
|
/**
|
|
3827
|
-
*
|
|
4059
|
+
* normalize input
|
|
4060
|
+
* switch mode
|
|
3828
4061
|
*/
|
|
3829
|
-
let subPathArr = splitSubpaths(pathData);
|
|
3830
4062
|
|
|
3831
|
-
//
|
|
3832
|
-
|
|
4063
|
+
// original size
|
|
4064
|
+
svgSize = new Blob([input]).size;
|
|
3833
4065
|
|
|
3834
|
-
|
|
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
|
|
4078
|
+
let returnDom = true;
|
|
4079
|
+
svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
|
|
4080
|
+
);
|
|
3837
4081
|
|
|
3838
|
-
//
|
|
3839
|
-
|
|
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
|
-
|
|
3842
|
-
|
|
4089
|
+
/**
|
|
4090
|
+
* process all paths
|
|
4091
|
+
*/
|
|
4092
|
+
paths.forEach(path => {
|
|
4093
|
+
let { d, el } = path;
|
|
3843
4094
|
|
|
3844
|
-
|
|
4095
|
+
let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
|
|
3845
4096
|
|
|
3846
|
-
|
|
3847
|
-
|
|
4097
|
+
// create clone for fallback
|
|
4098
|
+
let pathData = JSON.parse(JSON.stringify(pathDataO));
|
|
3848
4099
|
|
|
3849
|
-
//
|
|
3850
|
-
|
|
4100
|
+
// count commands for evaluation
|
|
4101
|
+
let comCount = pathDataO.length;
|
|
3851
4102
|
|
|
3852
|
-
|
|
3853
|
-
|
|
4103
|
+
/**
|
|
4104
|
+
* get sub paths
|
|
4105
|
+
*/
|
|
4106
|
+
let subPathArr = splitSubpaths(pathData);
|
|
3854
4107
|
|
|
3855
|
-
//
|
|
3856
|
-
let
|
|
4108
|
+
// cleaned up pathData
|
|
4109
|
+
let pathDataArrN = [];
|
|
3857
4110
|
|
|
3858
|
-
|
|
3859
|
-
let { pathData, bb, dimA } = pathDataPlus;
|
|
4111
|
+
for (let i = 0, l = subPathArr.length; i < l; i++) {
|
|
3860
4112
|
|
|
3861
|
-
|
|
4113
|
+
let pathDataSub = subPathArr[i];
|
|
3862
4114
|
|
|
3863
|
-
|
|
3864
|
-
|
|
4115
|
+
// try simplification in reversed order
|
|
4116
|
+
if (reverse) pathDataSub = reversePathData(pathDataSub);
|
|
3865
4117
|
|
|
3866
|
-
|
|
3867
|
-
|
|
4118
|
+
// remove zero length linetos
|
|
4119
|
+
if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub);
|
|
3868
4120
|
|
|
3869
|
-
|
|
4121
|
+
// add extremes
|
|
3870
4122
|
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
if (type === 'C') {
|
|
4123
|
+
let tMin = 0, tMax = 1;
|
|
4124
|
+
if (addExtremes) pathDataSub = addExtremePoints(pathDataSub, tMin, tMax);
|
|
3874
4125
|
|
|
3875
|
-
|
|
3876
|
-
|
|
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
|
-
|
|
3882
|
-
|
|
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
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
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
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
4183
|
+
/**
|
|
4184
|
+
* detect accuracy
|
|
4185
|
+
*/
|
|
4186
|
+
if (autoAccuracy) {
|
|
4187
|
+
decimals = detectAccuracy(pathData);
|
|
3896
4188
|
}
|
|
3897
4189
|
|
|
3898
|
-
//
|
|
3899
|
-
|
|
3900
|
-
|
|
4190
|
+
// optimize
|
|
4191
|
+
let pathOptions = {
|
|
4192
|
+
toRelative,
|
|
4193
|
+
toShorthands,
|
|
4194
|
+
decimals,
|
|
4195
|
+
};
|
|
3901
4196
|
|
|
3902
|
-
|
|
3903
|
-
|
|
4197
|
+
// optimize path data
|
|
4198
|
+
pathData = convertPathData(pathData, pathOptions);
|
|
3904
4199
|
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
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
|
-
|
|
3913
|
-
let comCountS = pathDataFlat.length;
|
|
4213
|
+
pathData = pathDataOpt;
|
|
3914
4214
|
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
toRelative,
|
|
3918
|
-
toShorthands,
|
|
3919
|
-
decimals,
|
|
3920
|
-
};
|
|
4215
|
+
// compare command count
|
|
4216
|
+
let comCountS = pathData.length;
|
|
3921
4217
|
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
let dOpt = pathDataToD(pathData, minifyD);
|
|
4218
|
+
let dOpt = pathDataToD(pathData, minifyD);
|
|
4219
|
+
svgSizeOpt = new Blob([dOpt]).size;
|
|
3925
4220
|
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
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;
|