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
|
@@ -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
|
-
|
|
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
|
-
|
|
643
|
-
let
|
|
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
|
|
1384
|
+
let separator_command = beautify ? `\n` : (minify ? '' : ' ');
|
|
1385
|
+
let separator_type = !minify ? ' ' : '';
|
|
1294
1386
|
|
|
1295
|
-
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
|
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
|
|
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 === '
|
|
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
|
|
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
|
|
3416
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
3427
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
3519
|
-
|
|
3520
|
-
let
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
3956
|
+
// clamp tolerance
|
|
3957
|
+
tolerance = Math.max(0.1, tolerance);
|
|
3958
|
+
|
|
3959
|
+
let inputType = detectInputType(input);
|
|
3818
3960
|
|
|
3819
|
-
|
|
3820
|
-
let
|
|
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
|
-
|
|
3823
|
-
let comCount = pathDataO.length;
|
|
3969
|
+
let paths = [];
|
|
3824
3970
|
|
|
3825
3971
|
/**
|
|
3826
|
-
*
|
|
3972
|
+
* normalize input
|
|
3973
|
+
* switch mode
|
|
3827
3974
|
*/
|
|
3828
|
-
let subPathArr = splitSubpaths(pathData);
|
|
3829
3975
|
|
|
3830
|
-
//
|
|
3831
|
-
|
|
3976
|
+
// original size
|
|
3977
|
+
svgSize = new Blob([input]).size;
|
|
3832
3978
|
|
|
3833
|
-
|
|
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
|
|
3991
|
+
let returnDom = true;
|
|
3992
|
+
svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
|
|
3993
|
+
);
|
|
3836
3994
|
|
|
3837
|
-
//
|
|
3838
|
-
|
|
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
|
-
|
|
3841
|
-
|
|
4002
|
+
/**
|
|
4003
|
+
* process all paths
|
|
4004
|
+
*/
|
|
4005
|
+
paths.forEach(path => {
|
|
4006
|
+
let { d, el } = path;
|
|
3842
4007
|
|
|
3843
|
-
|
|
4008
|
+
let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
|
|
3844
4009
|
|
|
3845
|
-
|
|
3846
|
-
|
|
4010
|
+
// create clone for fallback
|
|
4011
|
+
let pathData = JSON.parse(JSON.stringify(pathDataO));
|
|
3847
4012
|
|
|
3848
|
-
//
|
|
3849
|
-
|
|
4013
|
+
// count commands for evaluation
|
|
4014
|
+
let comCount = pathDataO.length;
|
|
3850
4015
|
|
|
3851
|
-
|
|
3852
|
-
|
|
4016
|
+
/**
|
|
4017
|
+
* get sub paths
|
|
4018
|
+
*/
|
|
4019
|
+
let subPathArr = splitSubpaths(pathData);
|
|
3853
4020
|
|
|
3854
|
-
//
|
|
3855
|
-
let
|
|
4021
|
+
// cleaned up pathData
|
|
4022
|
+
let pathDataArrN = [];
|
|
3856
4023
|
|
|
3857
|
-
|
|
3858
|
-
let { pathData, bb, dimA } = pathDataPlus;
|
|
4024
|
+
for (let i = 0, l = subPathArr.length; i < l; i++) {
|
|
3859
4025
|
|
|
3860
|
-
|
|
4026
|
+
let pathDataSub = subPathArr[i];
|
|
3861
4027
|
|
|
3862
|
-
|
|
3863
|
-
|
|
4028
|
+
// try simplification in reversed order
|
|
4029
|
+
if (reverse) pathDataSub = reversePathData(pathDataSub);
|
|
3864
4030
|
|
|
3865
|
-
|
|
3866
|
-
|
|
4031
|
+
// remove zero length linetos
|
|
4032
|
+
if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub);
|
|
3867
4033
|
|
|
3868
|
-
|
|
4034
|
+
// add extremes
|
|
3869
4035
|
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
if (type === 'C') {
|
|
4036
|
+
let tMin = 0, tMax = 1;
|
|
4037
|
+
if (addExtremes) pathDataSub = addExtremePoints(pathDataSub, tMin, tMax);
|
|
3873
4038
|
|
|
3874
|
-
|
|
3875
|
-
|
|
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
|
-
|
|
3881
|
-
|
|
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
|
-
//
|
|
3886
|
-
|
|
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
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
4092
|
+
/**
|
|
4093
|
+
* detect accuracy
|
|
4094
|
+
*/
|
|
4095
|
+
if (autoAccuracy) {
|
|
4096
|
+
decimals = detectAccuracy(pathData);
|
|
3895
4097
|
}
|
|
3896
4098
|
|
|
3897
|
-
//
|
|
3898
|
-
|
|
3899
|
-
|
|
4099
|
+
// optimize
|
|
4100
|
+
let pathOptions = {
|
|
4101
|
+
toRelative,
|
|
4102
|
+
toShorthands,
|
|
4103
|
+
decimals,
|
|
4104
|
+
};
|
|
3900
4105
|
|
|
3901
|
-
|
|
3902
|
-
|
|
4106
|
+
// optimize path data
|
|
4107
|
+
pathData = convertPathData(pathData, pathOptions);
|
|
3903
4108
|
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
*/
|
|
3907
|
-
if (autoAccuracy) {
|
|
3908
|
-
decimals = detectAccuracy(pathDataFlat);
|
|
3909
|
-
}
|
|
4109
|
+
// remove zero-length segments introduced by rounding
|
|
4110
|
+
let pathDataOpt = [];
|
|
3910
4111
|
|
|
3911
|
-
|
|
3912
|
-
|
|
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
|
-
|
|
3915
|
-
let pathOptions = {
|
|
3916
|
-
toRelative,
|
|
3917
|
-
toShorthands,
|
|
3918
|
-
decimals,
|
|
3919
|
-
};
|
|
4122
|
+
pathData = pathDataOpt;
|
|
3920
4123
|
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
let dOpt = pathDataToD(pathData, minifyD);
|
|
4124
|
+
// compare command count
|
|
4125
|
+
let comCountS = pathData.length;
|
|
3924
4126
|
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
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;
|