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
|
@@ -25,6 +25,48 @@ function renderPoint(
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function detectInputType(input) {
|
|
29
|
+
let type = 'string';
|
|
30
|
+
if (input instanceof HTMLImageElement) return "img";
|
|
31
|
+
if (input instanceof SVGElement) return "svg";
|
|
32
|
+
if (input instanceof HTMLCanvasElement) return "canvas";
|
|
33
|
+
if (input instanceof File) return "file";
|
|
34
|
+
if (input instanceof ArrayBuffer) return "buffer";
|
|
35
|
+
if (input instanceof Blob) return "blob";
|
|
36
|
+
if (Array.isArray(input)) return "array";
|
|
37
|
+
|
|
38
|
+
if (typeof input === "string") {
|
|
39
|
+
input = input.trim();
|
|
40
|
+
let isSVG = input.includes('<svg') && input.includes('</svg');
|
|
41
|
+
let isPathData = input.startsWith('M') || input.startsWith('m');
|
|
42
|
+
let isPolyString = !isNaN(input.substring(0, 1)) && !isNaN(input.substring(input.length-1, input.length));
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if(isSVG) {
|
|
46
|
+
type='svgMarkup';
|
|
47
|
+
}
|
|
48
|
+
else if(isPathData) {
|
|
49
|
+
type='pathDataString';
|
|
50
|
+
}
|
|
51
|
+
else if(isPolyString) {
|
|
52
|
+
type='polyString';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
else {
|
|
56
|
+
let url = /^(file:|https?:\/\/|\/|\.\/|\.\.\/)/.test(input);
|
|
57
|
+
let dataUrl = input.startsWith('data:image');
|
|
58
|
+
type = url || dataUrl ? "url" : "string";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return type
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type = typeof input;
|
|
65
|
+
let constructor = input.constructor.name;
|
|
66
|
+
|
|
67
|
+
return (constructor || type).toLowerCase();
|
|
68
|
+
}
|
|
69
|
+
|
|
28
70
|
/*
|
|
29
71
|
import {abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
|
|
30
72
|
log, max, min, pow, random, round, sin, sqrt, tan, PI} from '/.constants.js';
|
|
@@ -77,7 +119,7 @@ function checkLineIntersection(p1, p2, p3, p4, exact = true) {
|
|
|
77
119
|
y: p1.y + (a * (p2.y - p1.y))
|
|
78
120
|
};
|
|
79
121
|
|
|
80
|
-
|
|
122
|
+
// console.log('intersectionPoint', intersectionPoint, p1, p2);
|
|
81
123
|
|
|
82
124
|
let intersection = false;
|
|
83
125
|
// if line1 is a segment and line2 is infinite, they intersect if:
|
|
@@ -632,14 +674,60 @@ function commandIsFlat(points, tolerance = 0.025) {
|
|
|
632
674
|
return { area: area, flat: isFlat, thresh: thresh, ratio: ratio, squareDist: squareDist, areaThresh: squareDist / areaThresh };
|
|
633
675
|
}
|
|
634
676
|
|
|
677
|
+
function checkBezierFlatness(p0, cpts, p) {
|
|
678
|
+
|
|
679
|
+
let isFlat = false;
|
|
680
|
+
|
|
681
|
+
let isCubic = cpts.length===2;
|
|
682
|
+
|
|
683
|
+
let cp1 = cpts[0];
|
|
684
|
+
let cp2 = isCubic ? cpts[1] : cp1;
|
|
685
|
+
|
|
686
|
+
if(p0.x===cp1.x && p0.y===cp1.y && p.x===cp2.x && p.y===cp2.y) return true;
|
|
687
|
+
|
|
688
|
+
let dx1 = cp1.x - p0.x;
|
|
689
|
+
let dy1 = cp1.y - p0.y;
|
|
690
|
+
|
|
691
|
+
let dx2 = p.x - cp2.x;
|
|
692
|
+
let dy2 = p.y - cp2.y;
|
|
693
|
+
|
|
694
|
+
let cross1 = Math.abs(dx1 * dy2 - dy1 * dx2);
|
|
695
|
+
|
|
696
|
+
if(!cross1) return true
|
|
697
|
+
|
|
698
|
+
let dx0 = p.x - p0.x;
|
|
699
|
+
let dy0 = p.y - p0.y;
|
|
700
|
+
let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
|
|
701
|
+
|
|
702
|
+
if(!cross0) return true
|
|
703
|
+
|
|
704
|
+
let rat = (cross0/cross1);
|
|
705
|
+
|
|
706
|
+
if (rat<1.1 ) {
|
|
707
|
+
|
|
708
|
+
isFlat = true;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return isFlat;
|
|
712
|
+
|
|
713
|
+
}
|
|
714
|
+
|
|
635
715
|
/**
|
|
636
716
|
* sloppy distance calculation
|
|
637
717
|
* based on x/y differences
|
|
638
718
|
*/
|
|
639
719
|
function getDistAv(pt1, pt2) {
|
|
640
|
-
|
|
641
|
-
let
|
|
720
|
+
|
|
721
|
+
let diffX = Math.abs(pt2.x - pt1.x);
|
|
722
|
+
let diffY = Math.abs(pt2.y - pt1.y);
|
|
642
723
|
let diff = (diffX + diffY) / 2;
|
|
724
|
+
|
|
725
|
+
/*
|
|
726
|
+
let diffX = pt2.x - pt1.x;
|
|
727
|
+
let diffY = pt2.y - pt1.y;
|
|
728
|
+
let diff = Math.abs(diffX + diffY) / 2;
|
|
729
|
+
*/
|
|
730
|
+
|
|
643
731
|
return diff;
|
|
644
732
|
}
|
|
645
733
|
|
|
@@ -1279,24 +1367,24 @@ function pathDataToD(pathData, optimize = 0) {
|
|
|
1279
1367
|
|
|
1280
1368
|
optimize = parseFloat(optimize);
|
|
1281
1369
|
|
|
1370
|
+
let len = pathData.length;
|
|
1282
1371
|
let beautify = optimize > 1;
|
|
1283
1372
|
let minify = beautify || optimize ? false : true;
|
|
1284
1373
|
|
|
1285
1374
|
// Convert first "M" to "m" if followed by "l" (when minified)
|
|
1375
|
+
/*
|
|
1286
1376
|
if (pathData[1].type === "l" && minify) {
|
|
1287
1377
|
pathData[0].type = "m";
|
|
1288
1378
|
}
|
|
1379
|
+
*/
|
|
1289
1380
|
|
|
1290
1381
|
let d = '';
|
|
1291
|
-
let
|
|
1382
|
+
let separator_command = beautify ? `\n` : (minify ? '' : ' ');
|
|
1383
|
+
let separator_type = !minify ? ' ' : '';
|
|
1292
1384
|
|
|
1293
|
-
|
|
1294
|
-
d = `${pathData[0].type} ${pathData[0].values.join(" ")}`;
|
|
1295
|
-
} else {
|
|
1296
|
-
d = `${pathData[0].type} ${pathData[0].values.join(" ")}${suff}`;
|
|
1297
|
-
}
|
|
1385
|
+
d = `${pathData[0].type}${separator_type}${pathData[0].values.join(" ")}${separator_command}`;
|
|
1298
1386
|
|
|
1299
|
-
for (let i = 1
|
|
1387
|
+
for (let i = 1; i < len; i++) {
|
|
1300
1388
|
let com0 = pathData[i - 1];
|
|
1301
1389
|
let com = pathData[i];
|
|
1302
1390
|
let { type, values } = com;
|
|
@@ -1314,8 +1402,6 @@ function pathDataToD(pathData, optimize = 0) {
|
|
|
1314
1402
|
type = (com0.type === com.type && com.type.toLowerCase() !== 'm' && minify)
|
|
1315
1403
|
? " "
|
|
1316
1404
|
: (
|
|
1317
|
-
(com0.type === "m" && com.type === "l") ||
|
|
1318
|
-
(com0.type === "M" && com.type === "l") ||
|
|
1319
1405
|
(com0.type === "M" && com.type === "L")
|
|
1320
1406
|
) && minify
|
|
1321
1407
|
? " "
|
|
@@ -1344,16 +1430,15 @@ function pathDataToD(pathData, optimize = 0) {
|
|
|
1344
1430
|
}
|
|
1345
1431
|
|
|
1346
1432
|
valsString += valStr;
|
|
1347
|
-
|
|
1348
1433
|
prevWasFloat = isSmallFloat;
|
|
1349
1434
|
}
|
|
1350
1435
|
|
|
1351
|
-
d += `${type}${valsString}`;
|
|
1436
|
+
d += `${type}${separator_type}${valsString}${separator_command}`;
|
|
1352
1437
|
|
|
1353
1438
|
}
|
|
1354
1439
|
// regular non-minified output
|
|
1355
1440
|
else {
|
|
1356
|
-
d += `${type}
|
|
1441
|
+
d += `${type}${separator_type}${values.join(' ')}${separator_command}`;
|
|
1357
1442
|
}
|
|
1358
1443
|
}
|
|
1359
1444
|
|
|
@@ -1957,7 +2042,8 @@ function detectAccuracy(pathData) {
|
|
|
1957
2042
|
let p0 = M;
|
|
1958
2043
|
let p = M;
|
|
1959
2044
|
pathData[0].decimals = 0;
|
|
1960
|
-
|
|
2045
|
+
|
|
2046
|
+
let dims = new Set();
|
|
1961
2047
|
|
|
1962
2048
|
// add average distances
|
|
1963
2049
|
for (let i = 0, len = pathData.length; i < len; i++) {
|
|
@@ -1970,8 +2056,7 @@ function detectAccuracy(pathData) {
|
|
|
1970
2056
|
// use existing averave dimension value or calculate
|
|
1971
2057
|
let dimA = com.dimA ? +com.dimA.toFixed(8) : type!=='M' ? +getDistAv(p0, p).toFixed(8) : 0;
|
|
1972
2058
|
|
|
1973
|
-
if(dimA
|
|
1974
|
-
|
|
2059
|
+
if(dimA) dims.add(dimA);
|
|
1975
2060
|
|
|
1976
2061
|
|
|
1977
2062
|
if(type==='M'){
|
|
@@ -1980,7 +2065,14 @@ function detectAccuracy(pathData) {
|
|
|
1980
2065
|
p0 = p;
|
|
1981
2066
|
}
|
|
1982
2067
|
|
|
1983
|
-
let
|
|
2068
|
+
let dim_min = Array.from(dims).sort();
|
|
2069
|
+
let sliceIdx = Math.ceil(dim_min.length/8);
|
|
2070
|
+
dim_min = dim_min.slice(0, sliceIdx );
|
|
2071
|
+
|
|
2072
|
+
let dimVal = dim_min.reduce((a,b)=>a+b, 0) / sliceIdx;
|
|
2073
|
+
|
|
2074
|
+
let threshold = 50;
|
|
2075
|
+
let decimalsAuto = dimVal > threshold ? 0 : Math.floor(threshold / dimVal).toString().length;
|
|
1984
2076
|
|
|
1985
2077
|
// clamp
|
|
1986
2078
|
return Math.min(Math.max(0, decimalsAuto), 8)
|
|
@@ -3400,33 +3492,37 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
|
|
|
3400
3492
|
let comPrev = pathData[c - 1];
|
|
3401
3493
|
let com = pathData[c];
|
|
3402
3494
|
let comN = pathData[c + 1] || pathData[l - 1];
|
|
3403
|
-
let p1 = comN.type === '
|
|
3495
|
+
let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
|
|
3404
3496
|
|
|
3405
3497
|
let { type, values } = com;
|
|
3406
3498
|
let valsL = values.slice(-2);
|
|
3407
3499
|
p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
|
|
3408
3500
|
|
|
3409
|
-
let
|
|
3410
|
-
[{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
|
|
3411
|
-
(type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
|
|
3501
|
+
let area = getPolygonArea([p0, p, p1], true);
|
|
3412
3502
|
|
|
3413
|
-
let
|
|
3414
|
-
let
|
|
3415
|
-
let distMax = distSquare / 500 * tolerance;
|
|
3503
|
+
let distSquare = getSquareDistance(p0, p1);
|
|
3504
|
+
let distMax = distSquare / 100 * tolerance;
|
|
3416
3505
|
|
|
3417
|
-
let isFlat = area < distMax;
|
|
3418
|
-
|
|
3419
|
-
|
|
3506
|
+
let isFlat = area < distMax;
|
|
3507
|
+
let isFlatBez = false;
|
|
3508
|
+
|
|
3509
|
+
if (!flatBezierToLinetos && type === 'C') isFlat = false;
|
|
3420
3510
|
|
|
3421
3511
|
// convert flat beziers to linetos
|
|
3422
|
-
if (flatBezierToLinetos && type === 'C') {
|
|
3512
|
+
if (flatBezierToLinetos && (type === 'C' || type === 'Q')) {
|
|
3513
|
+
|
|
3514
|
+
let cpts = type === 'C' ?
|
|
3515
|
+
[{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
|
|
3516
|
+
(type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
|
|
3423
3517
|
|
|
3424
|
-
|
|
3425
|
-
|
|
3518
|
+
isFlatBez = checkBezierFlatness(p0, cpts, p);
|
|
3519
|
+
// console.log();
|
|
3426
3520
|
|
|
3427
|
-
if (isFlatBez && comPrev.type !== 'C') {
|
|
3521
|
+
if (isFlatBez && c < l - 1 && comPrev.type !== 'C') {
|
|
3522
|
+
type = "L";
|
|
3428
3523
|
com.type = "L";
|
|
3429
3524
|
com.values = valsL;
|
|
3525
|
+
|
|
3430
3526
|
}
|
|
3431
3527
|
|
|
3432
3528
|
}
|
|
@@ -3435,7 +3531,8 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
|
|
|
3435
3531
|
p0 = p;
|
|
3436
3532
|
|
|
3437
3533
|
// colinear – exclude arcs (as always =) as semicircles won't have an area
|
|
3438
|
-
if (
|
|
3534
|
+
if ( isFlat && c < l - 1 && (type === 'L' || (flatBezierToLinetos && isFlatBez))) {
|
|
3535
|
+
|
|
3439
3536
|
continue;
|
|
3440
3537
|
}
|
|
3441
3538
|
|
|
@@ -3485,87 +3582,35 @@ function removeZeroLengthLinetos(pathData) {
|
|
|
3485
3582
|
|
|
3486
3583
|
}
|
|
3487
3584
|
|
|
3488
|
-
function pathDataToTopLeft(pathData
|
|
3585
|
+
function pathDataToTopLeft(pathData) {
|
|
3489
3586
|
|
|
3490
|
-
let pathDataNew = [];
|
|
3491
3587
|
let len = pathData.length;
|
|
3492
|
-
let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
3493
3588
|
let isClosed = pathData[len - 1].type.toLowerCase() === 'z';
|
|
3494
3589
|
|
|
3495
|
-
let linetos = pathData.filter(com => com.type === 'L');
|
|
3496
|
-
|
|
3497
|
-
// check if order is ideal
|
|
3498
|
-
let penultimateCom = pathData[len - 2];
|
|
3499
|
-
let penultimateType = penultimateCom.type;
|
|
3500
|
-
let penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8));
|
|
3501
|
-
|
|
3502
|
-
// last L command ends at M
|
|
3503
|
-
let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
|
|
3504
|
-
|
|
3505
|
-
// if last segment is not closing or a lineto
|
|
3506
|
-
let skipReorder = pathData[1].type!=='L' && (!isClosingCommand || penultimateType==='L' );
|
|
3507
|
-
skipReorder=false;
|
|
3508
|
-
|
|
3509
3590
|
// we can't change starting point for non closed paths
|
|
3510
|
-
if (!isClosed
|
|
3591
|
+
if (!isClosed) {
|
|
3511
3592
|
return pathData
|
|
3512
3593
|
}
|
|
3513
3594
|
|
|
3514
3595
|
let newIndex = 0;
|
|
3515
3596
|
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
let
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
let nextL = pathData[i + 1] && pathData[i + 1].type === 'L';
|
|
3526
|
-
let prevCom = pathData[i - 1] ? pathData[i - 1].type.toUpperCase() : null;
|
|
3527
|
-
let nextCom = pathData[i + 1] ? pathData[i + 1].type.toUpperCase() : null;
|
|
3528
|
-
let p = { type: type, x: valsL[0], y: valsL[1], dist: 0, index: 0, prevL, nextL, prevCom, nextCom };
|
|
3529
|
-
p.index = i;
|
|
3530
|
-
indices.push(p);
|
|
3531
|
-
}
|
|
3532
|
-
}
|
|
3533
|
-
|
|
3534
|
-
// find top most lineto
|
|
3535
|
-
|
|
3536
|
-
if (linetos.length) {
|
|
3537
|
-
let curveAfterLine = indices.filter(com => (com.type !== 'L' && com.type !== 'M') && com.prevCom &&
|
|
3538
|
-
com.prevCom === 'L' || com.prevCom==='M' && penultimateType==='L' ).sort((a, b) => a.y - b.y || a.x-b.x)[0];
|
|
3539
|
-
|
|
3540
|
-
newIndex = curveAfterLine ? curveAfterLine.index - 1 : 0;
|
|
3541
|
-
|
|
3542
|
-
}
|
|
3543
|
-
// use top most command
|
|
3544
|
-
else {
|
|
3545
|
-
indices = indices.sort((a, b) => +a.y.toFixed(1) - +b.y.toFixed(1) || a.x - b.x );
|
|
3546
|
-
newIndex = indices[0].index;
|
|
3597
|
+
let indices = [];
|
|
3598
|
+
for (let i = 0; i < len; i++) {
|
|
3599
|
+
let com = pathData[i];
|
|
3600
|
+
let { type, values } = com;
|
|
3601
|
+
let valsLen = values.length;
|
|
3602
|
+
if (valsLen) {
|
|
3603
|
+
let p = { type: type, x: values[valsLen-2], y: values[valsLen-1], index: 0};
|
|
3604
|
+
p.index = i;
|
|
3605
|
+
indices.push(p);
|
|
3547
3606
|
}
|
|
3548
|
-
|
|
3549
|
-
// reorder
|
|
3550
|
-
pathData = newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
|
|
3551
|
-
}
|
|
3552
|
-
|
|
3553
|
-
len = pathData.length;
|
|
3554
|
-
|
|
3555
|
-
// remove last lineto
|
|
3556
|
-
penultimateCom = pathData[len - 2];
|
|
3557
|
-
penultimateType = penultimateCom.type;
|
|
3558
|
-
penultimateComCoords = penultimateCom.values.slice(-2);
|
|
3559
|
-
|
|
3560
|
-
isClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
|
|
3561
|
-
|
|
3562
|
-
if (removeFinalLineto && isClosingCommand) {
|
|
3563
|
-
pathData.splice(len - 2, 1);
|
|
3564
3607
|
}
|
|
3565
3608
|
|
|
3566
|
-
|
|
3609
|
+
// reorder to top left most
|
|
3610
|
+
indices = indices.sort((a, b) => +a.y.toFixed(3) - +b.y.toFixed(3) || a.x - b.x);
|
|
3611
|
+
newIndex = indices[0].index;
|
|
3567
3612
|
|
|
3568
|
-
return
|
|
3613
|
+
return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
|
|
3569
3614
|
}
|
|
3570
3615
|
|
|
3571
3616
|
/**
|
|
@@ -3781,7 +3826,93 @@ function reversePathData(pathData, {
|
|
|
3781
3826
|
return pathDataNew;
|
|
3782
3827
|
}
|
|
3783
3828
|
|
|
3784
|
-
function
|
|
3829
|
+
function cleanUpSVG(svgMarkup, {
|
|
3830
|
+
returnDom=false,
|
|
3831
|
+
removeHidden=true,
|
|
3832
|
+
removeUnused=true,
|
|
3833
|
+
}={}) {
|
|
3834
|
+
svgMarkup = cleanSvgPrologue(svgMarkup);
|
|
3835
|
+
|
|
3836
|
+
// replace namespaced refs
|
|
3837
|
+
svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
|
|
3838
|
+
|
|
3839
|
+
let svg = new DOMParser()
|
|
3840
|
+
.parseFromString(svgMarkup, "text/html")
|
|
3841
|
+
.querySelector("svg");
|
|
3842
|
+
|
|
3843
|
+
|
|
3844
|
+
let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class'];
|
|
3845
|
+
removeExcludedAttribues(svg, allowed);
|
|
3846
|
+
|
|
3847
|
+
let removeEls = ['metadata', 'script'];
|
|
3848
|
+
|
|
3849
|
+
let els = svg.querySelectorAll('*');
|
|
3850
|
+
els.forEach(el=>{
|
|
3851
|
+
let name = el.nodeName;
|
|
3852
|
+
// remove hidden elements
|
|
3853
|
+
let style = el.getAttribute('style') || '';
|
|
3854
|
+
let isHiddenByStyle = style ? style.trim().includes('display:none') : false;
|
|
3855
|
+
let isHidden = (el.getAttribute('display') && el.getAttribute('display') === 'none') || isHiddenByStyle;
|
|
3856
|
+
if(name.includes(':') || removeEls.includes(name) || (removeHidden && isHidden )) {
|
|
3857
|
+
el.remove();
|
|
3858
|
+
}else {
|
|
3859
|
+
// remove BS elements
|
|
3860
|
+
removeNameSpaceAtts(el);
|
|
3861
|
+
}
|
|
3862
|
+
});
|
|
3863
|
+
|
|
3864
|
+
if(returnDom) return svg
|
|
3865
|
+
|
|
3866
|
+
let markup = stringifySVG(svg);
|
|
3867
|
+
console.log(markup);
|
|
3868
|
+
|
|
3869
|
+
return markup;
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
function cleanSvgPrologue(svgString) {
|
|
3873
|
+
return (
|
|
3874
|
+
svgString
|
|
3875
|
+
// Remove XML prologues like <?xml ... ?>
|
|
3876
|
+
.replace(/<\?xml[\s\S]*?\?>/gi, "")
|
|
3877
|
+
// Remove DOCTYPE declarations
|
|
3878
|
+
.replace(/<!DOCTYPE[\s\S]*?>/gi, "")
|
|
3879
|
+
// Remove comments <!-- ... -->
|
|
3880
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
3881
|
+
// Trim extra whitespace
|
|
3882
|
+
.trim()
|
|
3883
|
+
);
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
function removeExcludedAttribues(el, allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class']){
|
|
3887
|
+
let atts = [...el.attributes].map((att) => att.name);
|
|
3888
|
+
atts.forEach((att) => {
|
|
3889
|
+
if (!allowed.includes(att)) {
|
|
3890
|
+
el.removeAttribute(att);
|
|
3891
|
+
}
|
|
3892
|
+
});
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
function removeNameSpaceAtts(el) {
|
|
3896
|
+
let atts = [...el.attributes].map((att) => att.name);
|
|
3897
|
+
atts.forEach((att) => {
|
|
3898
|
+
if (att.includes(":")) {
|
|
3899
|
+
el.removeAttribute(att);
|
|
3900
|
+
}
|
|
3901
|
+
});
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3904
|
+
function stringifySVG(svg){
|
|
3905
|
+
let markup = new XMLSerializer().serializeToString(svg);
|
|
3906
|
+
markup = markup
|
|
3907
|
+
.replace(/\t/g, "")
|
|
3908
|
+
.replace(/[\n\r|]/g, "\n")
|
|
3909
|
+
.replace(/\n\s*\n/g, '\n')
|
|
3910
|
+
.replace(/ +/g, ' ');
|
|
3911
|
+
|
|
3912
|
+
return markup
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
function svgPathSimplify(input = '', {
|
|
3785
3916
|
toAbsolute = true,
|
|
3786
3917
|
toRelative = true,
|
|
3787
3918
|
toShorthands = true,
|
|
@@ -3809,126 +3940,229 @@ function svgPathSimplify(d = '', {
|
|
|
3809
3940
|
revertToQuadratics = true,
|
|
3810
3941
|
minifyD = 0,
|
|
3811
3942
|
tolerance = 1,
|
|
3812
|
-
reverse = false
|
|
3943
|
+
reverse = false,
|
|
3944
|
+
|
|
3945
|
+
// svg cleanup options
|
|
3946
|
+
removeHidden = true,
|
|
3947
|
+
removeUnused = true,
|
|
3948
|
+
|
|
3949
|
+
// return svg markup or object
|
|
3950
|
+
getObject = false
|
|
3951
|
+
|
|
3813
3952
|
} = {}) {
|
|
3814
3953
|
|
|
3815
|
-
|
|
3954
|
+
// clamp tolerance
|
|
3955
|
+
tolerance = Math.max(0.1, tolerance);
|
|
3956
|
+
|
|
3957
|
+
let inputType = detectInputType(input);
|
|
3816
3958
|
|
|
3817
|
-
|
|
3818
|
-
let
|
|
3959
|
+
let svg = '';
|
|
3960
|
+
let svgSize = 0;
|
|
3961
|
+
let svgSizeOpt = 0;
|
|
3962
|
+
let compression = 0;
|
|
3963
|
+
let report = {};
|
|
3964
|
+
let d = '';
|
|
3965
|
+
let mode = inputType === 'svgMarkup' ? 1 : 0;
|
|
3819
3966
|
|
|
3820
|
-
|
|
3821
|
-
let comCount = pathDataO.length;
|
|
3967
|
+
let paths = [];
|
|
3822
3968
|
|
|
3823
3969
|
/**
|
|
3824
|
-
*
|
|
3970
|
+
* normalize input
|
|
3971
|
+
* switch mode
|
|
3825
3972
|
*/
|
|
3826
|
-
let subPathArr = splitSubpaths(pathData);
|
|
3827
3973
|
|
|
3828
|
-
//
|
|
3829
|
-
|
|
3974
|
+
// original size
|
|
3975
|
+
svgSize = new Blob([input]).size;
|
|
3830
3976
|
|
|
3831
|
-
|
|
3977
|
+
// single path
|
|
3978
|
+
if (!mode) {
|
|
3979
|
+
if (inputType === 'pathDataString') {
|
|
3980
|
+
d = input;
|
|
3981
|
+
} else if (inputType === 'polyString') {
|
|
3982
|
+
d = 'M' + input;
|
|
3983
|
+
}
|
|
3984
|
+
paths.push({ d, el: null });
|
|
3985
|
+
}
|
|
3986
|
+
// process svg
|
|
3987
|
+
else {
|
|
3832
3988
|
|
|
3833
|
-
let
|
|
3989
|
+
let returnDom = true;
|
|
3990
|
+
svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
|
|
3991
|
+
);
|
|
3834
3992
|
|
|
3835
|
-
//
|
|
3836
|
-
|
|
3993
|
+
// collect paths
|
|
3994
|
+
let pathEls = svg.querySelectorAll('path');
|
|
3995
|
+
pathEls.forEach(path => {
|
|
3996
|
+
paths.push({ d: path.getAttribute('d'), el: path });
|
|
3997
|
+
});
|
|
3998
|
+
}
|
|
3837
3999
|
|
|
3838
|
-
|
|
3839
|
-
|
|
4000
|
+
/**
|
|
4001
|
+
* process all paths
|
|
4002
|
+
*/
|
|
4003
|
+
paths.forEach(path => {
|
|
4004
|
+
let { d, el } = path;
|
|
3840
4005
|
|
|
3841
|
-
|
|
4006
|
+
let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
|
|
3842
4007
|
|
|
3843
|
-
|
|
3844
|
-
|
|
4008
|
+
// create clone for fallback
|
|
4009
|
+
let pathData = JSON.parse(JSON.stringify(pathDataO));
|
|
3845
4010
|
|
|
3846
|
-
//
|
|
3847
|
-
|
|
4011
|
+
// count commands for evaluation
|
|
4012
|
+
let comCount = pathDataO.length;
|
|
3848
4013
|
|
|
3849
|
-
|
|
3850
|
-
|
|
4014
|
+
/**
|
|
4015
|
+
* get sub paths
|
|
4016
|
+
*/
|
|
4017
|
+
let subPathArr = splitSubpaths(pathData);
|
|
3851
4018
|
|
|
3852
|
-
//
|
|
3853
|
-
let
|
|
4019
|
+
// cleaned up pathData
|
|
4020
|
+
let pathDataArrN = [];
|
|
3854
4021
|
|
|
3855
|
-
|
|
3856
|
-
let { pathData, bb, dimA } = pathDataPlus;
|
|
4022
|
+
for (let i = 0, l = subPathArr.length; i < l; i++) {
|
|
3857
4023
|
|
|
3858
|
-
|
|
4024
|
+
let pathDataSub = subPathArr[i];
|
|
3859
4025
|
|
|
3860
|
-
|
|
3861
|
-
|
|
4026
|
+
// try simplification in reversed order
|
|
4027
|
+
if (reverse) pathDataSub = reversePathData(pathDataSub);
|
|
3862
4028
|
|
|
3863
|
-
|
|
3864
|
-
|
|
4029
|
+
// remove zero length linetos
|
|
4030
|
+
if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub);
|
|
3865
4031
|
|
|
3866
|
-
|
|
4032
|
+
// add extremes
|
|
3867
4033
|
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
if (type === 'C') {
|
|
4034
|
+
let tMin = 0, tMax = 1;
|
|
4035
|
+
if (addExtremes) pathDataSub = addExtremePoints(pathDataSub, tMin, tMax);
|
|
3871
4036
|
|
|
3872
|
-
|
|
3873
|
-
|
|
4037
|
+
// sort to top left
|
|
4038
|
+
if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
|
|
3874
4039
|
|
|
3875
|
-
|
|
3876
|
-
|
|
4040
|
+
// remove colinear/flat
|
|
4041
|
+
if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
|
|
4042
|
+
|
|
4043
|
+
// analyze pathdata to add info about signicant properties such as extremes, corners
|
|
4044
|
+
let pathDataPlus = analyzePathData(pathDataSub);
|
|
4045
|
+
|
|
4046
|
+
// simplify beziers
|
|
4047
|
+
let { pathData, bb, dimA } = pathDataPlus;
|
|
4048
|
+
|
|
4049
|
+
pathData = simplifyBezier ? simplifyPathData(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
|
|
4050
|
+
|
|
4051
|
+
// cubic to arcs
|
|
4052
|
+
if (cubicToArc) {
|
|
4053
|
+
|
|
4054
|
+
let thresh = 3;
|
|
3877
4055
|
|
|
3878
|
-
|
|
3879
|
-
|
|
4056
|
+
pathData.forEach((com, c) => {
|
|
4057
|
+
let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
|
|
4058
|
+
if (type === 'C') {
|
|
4059
|
+
|
|
4060
|
+
let comA = cubicCommandToArc(p0, cp1, cp2, p, thresh);
|
|
4061
|
+
if (comA.isArc) pathData[c] = comA.com;
|
|
4062
|
+
|
|
4063
|
+
}
|
|
4064
|
+
});
|
|
4065
|
+
|
|
4066
|
+
// combine adjacent cubics
|
|
4067
|
+
pathData = combineArcs(pathData);
|
|
4068
|
+
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
// simplify to quadratics
|
|
4072
|
+
if (revertToQuadratics) {
|
|
4073
|
+
pathData.forEach((com, c) => {
|
|
4074
|
+
let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
|
|
4075
|
+
if (type === 'C') {
|
|
4076
|
+
|
|
4077
|
+
let comQ = revertCubicQuadratic(p0, cp1, cp2, p);
|
|
4078
|
+
if (comQ.type === 'Q') pathData[c] = comQ;
|
|
4079
|
+
}
|
|
4080
|
+
});
|
|
4081
|
+
}
|
|
3880
4082
|
|
|
4083
|
+
// update
|
|
4084
|
+
pathDataArrN.push(pathData);
|
|
3881
4085
|
}
|
|
3882
4086
|
|
|
3883
|
-
//
|
|
3884
|
-
|
|
3885
|
-
pathDataN.forEach((com, c) => {
|
|
3886
|
-
let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
|
|
3887
|
-
if (type === 'C') {
|
|
4087
|
+
// flatten compound paths
|
|
4088
|
+
pathData = pathDataArrN.flat();
|
|
3888
4089
|
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
4090
|
+
/**
|
|
4091
|
+
* detect accuracy
|
|
4092
|
+
*/
|
|
4093
|
+
if (autoAccuracy) {
|
|
4094
|
+
decimals = detectAccuracy(pathData);
|
|
3893
4095
|
}
|
|
3894
4096
|
|
|
3895
|
-
//
|
|
3896
|
-
|
|
3897
|
-
|
|
4097
|
+
// optimize
|
|
4098
|
+
let pathOptions = {
|
|
4099
|
+
toRelative,
|
|
4100
|
+
toShorthands,
|
|
4101
|
+
decimals,
|
|
4102
|
+
};
|
|
3898
4103
|
|
|
3899
|
-
|
|
3900
|
-
|
|
4104
|
+
// optimize path data
|
|
4105
|
+
pathData = convertPathData(pathData, pathOptions);
|
|
3901
4106
|
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
*/
|
|
3905
|
-
if (autoAccuracy) {
|
|
3906
|
-
decimals = detectAccuracy(pathDataFlat);
|
|
3907
|
-
}
|
|
4107
|
+
// remove zero-length segments introduced by rounding
|
|
4108
|
+
let pathDataOpt = [];
|
|
3908
4109
|
|
|
3909
|
-
|
|
3910
|
-
|
|
4110
|
+
pathData.forEach((com, i) => {
|
|
4111
|
+
let { type, values } = com;
|
|
4112
|
+
if (type === 'l' || type === 'v' || type === 'h') {
|
|
4113
|
+
let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0;
|
|
4114
|
+
if (hasLength) pathDataOpt.push(com);
|
|
4115
|
+
} else {
|
|
4116
|
+
pathDataOpt.push(com);
|
|
4117
|
+
}
|
|
4118
|
+
});
|
|
3911
4119
|
|
|
3912
|
-
|
|
3913
|
-
let pathOptions = {
|
|
3914
|
-
toRelative,
|
|
3915
|
-
toShorthands,
|
|
3916
|
-
decimals,
|
|
3917
|
-
};
|
|
4120
|
+
pathData = pathDataOpt;
|
|
3918
4121
|
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
let dOpt = pathDataToD(pathData, minifyD);
|
|
4122
|
+
// compare command count
|
|
4123
|
+
let comCountS = pathData.length;
|
|
3922
4124
|
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
4125
|
+
let dOpt = pathDataToD(pathData, minifyD);
|
|
4126
|
+
svgSizeOpt = new Blob([dOpt]).size;
|
|
4127
|
+
|
|
4128
|
+
compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
|
|
4129
|
+
|
|
4130
|
+
path.d = dOpt;
|
|
4131
|
+
path.report = {
|
|
4132
|
+
original: comCount,
|
|
4133
|
+
new: comCountS,
|
|
4134
|
+
saved: comCount - comCountS,
|
|
4135
|
+
compression,
|
|
4136
|
+
decimals,
|
|
4137
|
+
|
|
4138
|
+
};
|
|
4139
|
+
|
|
4140
|
+
// apply new path for svgs
|
|
4141
|
+
if (el) el.setAttribute('d', dOpt);
|
|
4142
|
+
|
|
4143
|
+
});
|
|
3930
4144
|
|
|
3931
|
-
|
|
4145
|
+
// stringify new SVG
|
|
4146
|
+
if (mode) {
|
|
4147
|
+
svg = new XMLSerializer().serializeToString(svg);
|
|
4148
|
+
svgSizeOpt = new Blob([svg]).size;
|
|
4149
|
+
|
|
4150
|
+
compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
|
|
4151
|
+
|
|
4152
|
+
svgSize = +(svgSize / 1024).toFixed(3);
|
|
4153
|
+
svgSizeOpt = +(svgSizeOpt / 1024).toFixed(3);
|
|
4154
|
+
|
|
4155
|
+
report = {
|
|
4156
|
+
svgSize,
|
|
4157
|
+
svgSizeOpt,
|
|
4158
|
+
compression
|
|
4159
|
+
};
|
|
4160
|
+
|
|
4161
|
+
} else {
|
|
4162
|
+
({ d, report } = paths[0]);
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
return !getObject ? (d ? d : svg) : { svg, d, report, inputType, mode };
|
|
3932
4166
|
|
|
3933
4167
|
}
|
|
3934
4168
|
|
|
@@ -3958,9 +4192,9 @@ function simplifyPathData(pathData, {
|
|
|
3958
4192
|
|
|
3959
4193
|
// cannot be combined as crossing extremes or corners
|
|
3960
4194
|
if (
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
4195
|
+
(keepInflections && isDirChangeN) ||
|
|
4196
|
+
(keepCorners && corner) ||
|
|
4197
|
+
(!isDirChange && keepExtremes && extreme)
|
|
3964
4198
|
) {
|
|
3965
4199
|
|
|
3966
4200
|
pathDataN.push(com);
|
|
@@ -4024,6 +4258,39 @@ function simplifyPathData(pathData, {
|
|
|
4024
4258
|
return pathDataN
|
|
4025
4259
|
}
|
|
4026
4260
|
|
|
4261
|
+
/**
|
|
4262
|
+
* get viewBox
|
|
4263
|
+
* either from explicit attribute or
|
|
4264
|
+
* width and height attributes
|
|
4265
|
+
*/
|
|
4266
|
+
|
|
4267
|
+
function getViewBox(svg = null, round = false) {
|
|
4268
|
+
|
|
4269
|
+
// browser default
|
|
4270
|
+
if (!svg) return { x: 0, y: 0, width: 300, height: 150 }
|
|
4271
|
+
|
|
4272
|
+
let style = window.getComputedStyle(svg);
|
|
4273
|
+
|
|
4274
|
+
// the baseVal API method also converts physical units to pixels/user-units
|
|
4275
|
+
let w = svg.hasAttribute('width') ? svg.width.baseVal.value : parseFloat(style.width) || 300;
|
|
4276
|
+
let h = svg.hasAttribute('height') ? svg.height.baseVal.value : parseFloat(style.height) || 150;
|
|
4277
|
+
|
|
4278
|
+
let viewBox = svg.getAttribute('viewBox') ? svg.viewBox.baseVal : { x: 0, y: 0, width: w, height: h };
|
|
4279
|
+
|
|
4280
|
+
// remove SVG constructor
|
|
4281
|
+
let { x, y, width, height } = viewBox;
|
|
4282
|
+
viewBox = { x, y, width, height };
|
|
4283
|
+
|
|
4284
|
+
// round to integers
|
|
4285
|
+
if (round) {
|
|
4286
|
+
for (let prop in viewBox) {
|
|
4287
|
+
viewBox[prop] = Math.ceil(viewBox[prop]);
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
return viewBox
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4027
4294
|
const {
|
|
4028
4295
|
abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
|
|
4029
4296
|
log, hypot, max, min, pow, random, round, sin, sqrt, tan, PI
|
|
@@ -4034,7 +4301,8 @@ const {
|
|
|
4034
4301
|
// IIFE
|
|
4035
4302
|
if (typeof window !== 'undefined') {
|
|
4036
4303
|
window.svgPathSimplify = svgPathSimplify;
|
|
4304
|
+
window.getViewBox = getViewBox;
|
|
4037
4305
|
window.renderPoint = renderPoint;
|
|
4038
4306
|
}
|
|
4039
4307
|
|
|
4040
|
-
export { PI, abs, acos, asin, atan, atan2, ceil, cos, exp, floor, hypot, log, max, min, pow, random, round, sin, sqrt, svgPathSimplify, tan };
|
|
4308
|
+
export { PI, abs, acos, asin, atan, atan2, ceil, cos, exp, floor, getViewBox, hypot, log, max, min, pow, random, round, sin, sqrt, svgPathSimplify, tan };
|