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.
- package/LICENSE +339 -0
- package/README.md +33 -1
- package/dist/svg-path-simplify.esm.js +455 -187
- package/dist/svg-path-simplify.esm.min.js +1 -1
- package/dist/svg-path-simplify.js +455 -186
- package/dist/svg-path-simplify.min.js +1 -1
- package/dist/svg-path-simplify.node.js +455 -186
- 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 +195 -89
- 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 +43 -9
- 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,87 +3585,35 @@
|
|
|
3488
3585
|
|
|
3489
3586
|
}
|
|
3490
3587
|
|
|
3491
|
-
function pathDataToTopLeft(pathData
|
|
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
|
-
|
|
3520
|
-
|
|
3521
|
-
let
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
3957
|
+
// clamp tolerance
|
|
3958
|
+
tolerance = Math.max(0.1, tolerance);
|
|
3959
|
+
|
|
3960
|
+
let inputType = detectInputType(input);
|
|
3819
3961
|
|
|
3820
|
-
|
|
3821
|
-
let
|
|
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
|
-
|
|
3824
|
-
let comCount = pathDataO.length;
|
|
3970
|
+
let paths = [];
|
|
3825
3971
|
|
|
3826
3972
|
/**
|
|
3827
|
-
*
|
|
3973
|
+
* normalize input
|
|
3974
|
+
* switch mode
|
|
3828
3975
|
*/
|
|
3829
|
-
let subPathArr = splitSubpaths(pathData);
|
|
3830
3976
|
|
|
3831
|
-
//
|
|
3832
|
-
|
|
3977
|
+
// original size
|
|
3978
|
+
svgSize = new Blob([input]).size;
|
|
3833
3979
|
|
|
3834
|
-
|
|
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
|
|
3992
|
+
let returnDom = true;
|
|
3993
|
+
svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
|
|
3994
|
+
);
|
|
3837
3995
|
|
|
3838
|
-
//
|
|
3839
|
-
|
|
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
|
-
|
|
3842
|
-
|
|
4003
|
+
/**
|
|
4004
|
+
* process all paths
|
|
4005
|
+
*/
|
|
4006
|
+
paths.forEach(path => {
|
|
4007
|
+
let { d, el } = path;
|
|
3843
4008
|
|
|
3844
|
-
|
|
4009
|
+
let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
|
|
3845
4010
|
|
|
3846
|
-
|
|
3847
|
-
|
|
4011
|
+
// create clone for fallback
|
|
4012
|
+
let pathData = JSON.parse(JSON.stringify(pathDataO));
|
|
3848
4013
|
|
|
3849
|
-
//
|
|
3850
|
-
|
|
4014
|
+
// count commands for evaluation
|
|
4015
|
+
let comCount = pathDataO.length;
|
|
3851
4016
|
|
|
3852
|
-
|
|
3853
|
-
|
|
4017
|
+
/**
|
|
4018
|
+
* get sub paths
|
|
4019
|
+
*/
|
|
4020
|
+
let subPathArr = splitSubpaths(pathData);
|
|
3854
4021
|
|
|
3855
|
-
//
|
|
3856
|
-
let
|
|
4022
|
+
// cleaned up pathData
|
|
4023
|
+
let pathDataArrN = [];
|
|
3857
4024
|
|
|
3858
|
-
|
|
3859
|
-
let { pathData, bb, dimA } = pathDataPlus;
|
|
4025
|
+
for (let i = 0, l = subPathArr.length; i < l; i++) {
|
|
3860
4026
|
|
|
3861
|
-
|
|
4027
|
+
let pathDataSub = subPathArr[i];
|
|
3862
4028
|
|
|
3863
|
-
|
|
3864
|
-
|
|
4029
|
+
// try simplification in reversed order
|
|
4030
|
+
if (reverse) pathDataSub = reversePathData(pathDataSub);
|
|
3865
4031
|
|
|
3866
|
-
|
|
3867
|
-
|
|
4032
|
+
// remove zero length linetos
|
|
4033
|
+
if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub);
|
|
3868
4034
|
|
|
3869
|
-
|
|
4035
|
+
// add extremes
|
|
3870
4036
|
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
if (type === 'C') {
|
|
4037
|
+
let tMin = 0, tMax = 1;
|
|
4038
|
+
if (addExtremes) pathDataSub = addExtremePoints(pathDataSub, tMin, tMax);
|
|
3874
4039
|
|
|
3875
|
-
|
|
3876
|
-
|
|
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
|
-
|
|
3882
|
-
|
|
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
|
-
//
|
|
3887
|
-
|
|
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
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
4093
|
+
/**
|
|
4094
|
+
* detect accuracy
|
|
4095
|
+
*/
|
|
4096
|
+
if (autoAccuracy) {
|
|
4097
|
+
decimals = detectAccuracy(pathData);
|
|
3896
4098
|
}
|
|
3897
4099
|
|
|
3898
|
-
//
|
|
3899
|
-
|
|
3900
|
-
|
|
4100
|
+
// optimize
|
|
4101
|
+
let pathOptions = {
|
|
4102
|
+
toRelative,
|
|
4103
|
+
toShorthands,
|
|
4104
|
+
decimals,
|
|
4105
|
+
};
|
|
3901
4106
|
|
|
3902
|
-
|
|
3903
|
-
|
|
4107
|
+
// optimize path data
|
|
4108
|
+
pathData = convertPathData(pathData, pathOptions);
|
|
3904
4109
|
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
*/
|
|
3908
|
-
if (autoAccuracy) {
|
|
3909
|
-
decimals = detectAccuracy(pathDataFlat);
|
|
3910
|
-
}
|
|
4110
|
+
// remove zero-length segments introduced by rounding
|
|
4111
|
+
let pathDataOpt = [];
|
|
3911
4112
|
|
|
3912
|
-
|
|
3913
|
-
|
|
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
|
-
|
|
3916
|
-
let pathOptions = {
|
|
3917
|
-
toRelative,
|
|
3918
|
-
toShorthands,
|
|
3919
|
-
decimals,
|
|
3920
|
-
};
|
|
4123
|
+
pathData = pathDataOpt;
|
|
3921
4124
|
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
let dOpt = pathDataToD(pathData, minifyD);
|
|
4125
|
+
// compare command count
|
|
4126
|
+
let comCountS = pathData.length;
|
|
3925
4127
|
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
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;
|