Skip to content

Commit a41a7c1

Browse files
authored
fix(convertPathData): introducing isSafeToRemove (#2164)
Removing useless commands: a tricky and inconsistent business. SVGO currently uses two approaches: - **!maybeHasStrokeAndLinecap** for the purpose of removing useless line commands This works decently. It allows for removing useless lines, while not removing dots. Flaw: Even in a path with linecaps, if there are "dot" commands after the first move, they wouldn't render at all and should be removed. They currently aren't. - **isSafeToUseZ** for the purpose of removing useless `z` commands isSafeToUseZ is defined as either having no stroke or having a all-round stroke. When that's true, a line home and a z command render the same way, and can be exchanged or removed. Flaw: This works great when removing a redundant `z` that comes right after another command, but what if the `z` is the first move (for the purpose of creating a dot)? Then it's incorrectly removed. This is shown in #2158 and #2163. This PR introduces isSafeToRemove to help. It's worth reading through the code, but the TLDR is that it uses a more sane method, categorizing the path into non-stroked, stroked + first draw command, and stroked but non-first command, and using fitting behavior in each case. Fixes #2158 and fixes #2163.
1 parent 38625dd commit a41a7c1

4 files changed

Lines changed: 52 additions & 11 deletions

File tree

plugins/convertPathData.js

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,25 @@ export const fn = (root, params) => {
155155
computedStyle['stroke-linecap'] &&
156156
(computedStyle['stroke-linecap'].type === 'dynamic' ||
157157
computedStyle['stroke-linecap'].value !== 'butt');
158-
const maybeHasStrokeAndLinecap = maybeHasStroke && maybeHasLinecap;
159158
const isSafeToUseZ = maybeHasStroke
160159
? computedStyle['stroke-linecap']?.type === 'static' &&
161160
computedStyle['stroke-linecap'].value === 'round' &&
162161
computedStyle['stroke-linejoin']?.type === 'static' &&
163162
computedStyle['stroke-linejoin'].value === 'round'
164163
: true;
164+
const isSafeToRemove = (
165+
/** @type {boolean} */ isFirstDraw,
166+
/** @type {boolean} */ safeIfNotFirstDraw,
167+
) => {
168+
if (!maybeHasStroke) {
169+
return true;
170+
}
171+
if (isFirstDraw) {
172+
return !maybeHasLinecap;
173+
} else {
174+
return safeIfNotFirstDraw;
175+
}
176+
};
165177

166178
let data = path2js(node);
167179

@@ -174,7 +186,7 @@ export const fn = (root, params) => {
174186

175187
data = filters(data, newParams, {
176188
isSafeToUseZ,
177-
maybeHasStrokeAndLinecap,
189+
isSafeToRemove,
178190
hasMarkerMid,
179191
});
180192

@@ -378,14 +390,10 @@ const convertToRelative = (pathData) => {
378390
*
379391
* @param {import('../lib/types.js').PathDataItem[]} path
380392
* @param {InternalParams} params
381-
* @param {{ isSafeToUseZ: boolean, maybeHasStrokeAndLinecap: boolean, hasMarkerMid: boolean }} param2
393+
* @param {{ isSafeToUseZ: boolean, isSafeToRemove: (isFirstDraw: boolean, safeIfNotFirstDraw: boolean) => boolean, hasMarkerMid: boolean }} param2
382394
* @returns {import('../lib/types.js').PathDataItem[]}
383395
*/
384-
function filters(
385-
path,
386-
params,
387-
{ isSafeToUseZ, maybeHasStrokeAndLinecap, hasMarkerMid },
388-
) {
396+
function filters(path, params, { isSafeToUseZ, isSafeToRemove, hasMarkerMid }) {
389397
const stringify = data2Path.bind(null, params);
390398
const relSubpoint = [0, 0];
391399
const pathBase = [0, 0];
@@ -835,7 +843,10 @@ function filters(
835843
}
836844

837845
// remove useless non-first path segments
838-
if (params.removeUseless && !maybeHasStrokeAndLinecap) {
846+
if (
847+
params.removeUseless &&
848+
isSafeToRemove(prev.command == 'm' || prev.command == 'M', true)
849+
) {
839850
// l 0,0 / h 0 / v 0 / q 0,0 0,0 / t 0,0 / c 0,0 0,0 0,0 / s 0,0 0,0
840851
if (
841852
(command === 'l' ||
@@ -891,7 +902,10 @@ function filters(
891902
if (
892903
(command === 'Z' || command === 'z') &&
893904
params.removeUseless &&
894-
isSafeToUseZ &&
905+
isSafeToRemove(
906+
prev.command == 'm' || prev.command == 'M',
907+
isSafeToUseZ,
908+
) &&
895909
// @ts-expect-error
896910
Math.abs(item.base[0] - item.coords[0]) < error / 10 &&
897911
// @ts-expect-error
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Should not break dots.
2+
3+
See: https://github.com/svg/svgo/issues/2163
4+
5+
===
6+
7+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
8+
<path d="M10 10z" stroke="#00f" stroke-linecap="round" stroke-linejoin="round" />
9+
</svg>
10+
11+
@@@
12+
13+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
14+
<path d="M10 10z" stroke="#00f" stroke-linecap="round" stroke-linejoin="round"/>
15+
</svg>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Should drop truely useless commands even when a linecap is present.
2+
3+
===
4+
5+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
6+
<path d="M 10 50 h 10 v 0" stroke="#00f" stroke-linecap="round" stroke-linejoin="round" />
7+
</svg>
8+
9+
@@@
10+
11+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
12+
<path d="M10 50h10" stroke="#00f" stroke-linecap="round" stroke-linejoin="round"/>
13+
</svg>

test/regression/lists/expect-mismatch.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ W3C_SVG_11_TestSuite/svg/styling-css-04-f.svg
1212

1313
# broken and pending fix
1414
charm-icons-0.18.0/docs/img/splash.svg
15-
charm-icons-0.18.0/icons/atom.svg
1615
oxygen-icons-5.116.0/scalable/actions/hidef/tools-rip-audio-cd.svg
1716
oxygen-icons-5.116.0/scalable/actions/im-ban-kick-user.svg
1817
oxygen-icons-5.116.0/scalable/actions/im-ban-user.svg

0 commit comments

Comments
 (0)