Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions lib/svgo/tools.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { attrsGroups, referencesProps } from '../../plugins/_collections.js';
import * as csstree from 'css-tree';

/**
* @typedef CleanupOutDataParams
Expand Down Expand Up @@ -239,3 +240,39 @@ export const toFixed = (num, precision) => {
const pow = 10 ** precision;
return Math.round(num * pow) / pow;
};

/**
* takes in a attribute value and returns true if the color is transparent OR alpha is 0
* these values are to fill and stroke attributes
* examples:
* - 'transparent'
* - 'rgba(10, 10, 0, 0)'
* - 'hsla(120, 100%, 50%, 0)'
*/
export const isTransparent = (/** @type {string} */ attribute) => {
if (typeof attribute !== 'string') {
return false;
}
try {
const ast = csstree.parse(attribute, { context: 'value' });
if (ast.type === 'Value') {
const firstNode = ast.children.first;
if (firstNode && firstNode.type === 'Function' && (firstNode.name === 'rgba' || firstNode.name === 'hsla')) {
const args = firstNode.children.toArray().filter(node =>
node.type === 'Number' || node.type === 'Percentage'
);
const alphaNode = args[3];
if ((alphaNode && alphaNode.type === 'Number' || alphaNode && alphaNode.type === 'Percentage') && parseFloat(alphaNode.value) === 0) {
return true;
}
}
if (firstNode && firstNode.type === 'Identifier' && (firstNode.name === 'transparent')) {
return true;
}
}
} catch {
return false;
}
return false;
}

34 changes: 24 additions & 10 deletions plugins/removeUselessStrokeAndFill.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { visit, visitSkip } from '../lib/util/visit.js';
import { collectStylesheet, computeStyle } from '../lib/style.js';
import { hasScripts } from '../lib/svgo/tools.js';
import { elemsGroups } from './_collections.js';
import { isTransparent } from '../lib/svgo/tools.js';

/**
* @typedef RemoveUselessStrokeAndFillParams
Expand Down Expand Up @@ -59,6 +60,8 @@ export const fn = (root, params) => {
const stroke = computedStyle.stroke;
const strokeOpacity = computedStyle['stroke-opacity'];
const strokeWidth = computedStyle['stroke-width'];
const markerStart = computedStyle['marker-start'];
const markerMid = computedStyle['marker-mid'];
const markerEnd = computedStyle['marker-end'];
const fill = computedStyle.fill;
const fillOpacity = computedStyle['fill-opacity'];
Expand All @@ -79,15 +82,17 @@ export const fn = (root, params) => {
strokeOpacity.value === '0') ||
(strokeWidth != null &&
strokeWidth.type === 'static' &&
strokeWidth.value === '0')
strokeWidth.value === '0') ||
(stroke != null && stroke.type === 'static' &&
isTransparent(stroke.value))
) {
// stroke-width may affect the size of marker-end
// marker is not visible when stroke-width is 0
if (
(strokeWidth != null &&
strokeWidth.type === 'static' &&
strokeWidth.value === '0') ||
markerEnd == null
(markerEnd == null && markerStart == null && markerMid == null)
) {
for (const name of Object.keys(node.attributes)) {
if (name.startsWith('stroke')) {
Expand All @@ -112,7 +117,9 @@ export const fn = (root, params) => {
(fill != null && fill.type === 'static' && fill.value === 'none') ||
(fillOpacity != null &&
fillOpacity.type === 'static' &&
fillOpacity.value === '0')
fillOpacity.value === '0') ||
(fill != null && fill.type === 'static' &&
isTransparent(fill.value))
) {
for (const name of Object.keys(node.attributes)) {
if (name.startsWith('fill-')) {
Expand All @@ -129,13 +136,20 @@ export const fn = (root, params) => {
}

if (removeNone) {
if (
(stroke == null || node.attributes.stroke === 'none') &&
((fill != null &&
fill.type === 'static' &&
fill.value === 'none') ||
node.attributes.fill === 'none')
) {
const isStrokeNone = stroke == null ||
(stroke.type === 'static' && stroke.value === 'none') ||
(strokeOpacity != null && strokeOpacity.type === 'static' && strokeOpacity.value === '0') ||
(strokeWidth != null && strokeWidth.type === 'static' && strokeWidth.value === '0') ||
(stroke.type === 'static' && isTransparent(stroke.value));

const isFillNone = fill == null ||
(fill.type === 'static' && fill.value === 'none') ||
(fillOpacity != null && fillOpacity.type === 'static' && fillOpacity.value === '0') ||
(fill.type === 'static' && isTransparent(fill.value));

const hasMarkers = markerStart || markerMid || markerEnd;

if (isStrokeNone && isFillNone && !hasMarkers) {
detachNodeFromParent(node, parentNode);
}
}
Expand Down
54 changes: 54 additions & 0 deletions test/plugins/removeUselessStrokeAndFill.06.svg.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<g id="test">
<rect fill-opacity=".5" width="100" height="100"/>
</g>
</defs>
<rect width="10" height="10" fill="transparent" stroke="none"/>
<rect width="20" height="20" fill="rgba(255, 0, 0, 0)" stroke="transparent"/>
<rect width="30" height="30" fill="hsla(120, 100%, 50%, 0)" stroke="hsla(240, 100%, 50%, 0)"/>
<rect width="40" height="40" fill="rgba(0, 0, 0, 0)" stroke="rgba(255, 255, 255, 0%)"/>

<rect width="50" height="50" fill="transparent" stroke="red"/>
<rect width="60" height="60" fill="blue" stroke="transparent"/>
<rect width="70" height="70" fill="rgba(255, 0, 0, 0.5)" stroke="none"/>

<circle r="15" cx="60" cy="50" fill="green" stroke="transparent" stroke-width="5"/>
<circle r="25" cx="60" cy="50" fill="blue" stroke="rgba(0, 0, 0, 0)" stroke-width="5"/>
<circle r="35" cx="60" cy="50" fill="red" stroke="hsla(120, 100%, 50%, 0)" stroke-dasharray="5,5"/>
<circle fill="none" fill-rule="evenodd" cx="60" cy="60" r="50"/>


<ellipse rx="17" ry="20" fill="transparent" stroke="blue" stroke-width="2"/>
<ellipse rx="27" ry="20" fill="rgba(255, 255, 255, 0)" stroke="blue" stroke-width="2"/>
<ellipse rx="37" ry="20" fill="hsla(0, 0%, 0%, 0%)" stroke="blue" stroke-width="2"/>

<path d="M10,10 L50,50" fill="rgba(0, 0, 0, 0)" stroke="transparent"/>
<path d="M20,20 L60,60" fill="hsla(120, 100%, 50%, 0)" stroke="hsla(240, 100%, 50%, 0%)"/>
</svg>

@@@

<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<g id="test">
<rect fill-opacity=".5" width="100" height="100"/>
</g>
</defs>
<rect width="10" height="10" fill="none"/>
<rect width="20" height="20" fill="none"/>
<rect width="30" height="30" fill="none"/>
<rect width="40" height="40" fill="none"/>
<rect width="50" height="50" fill="none" stroke="red"/>
<rect width="60" height="60" fill="blue"/>
<rect width="70" height="70" fill="rgba(255, 0, 0, 0.5)"/>
<circle r="15" cx="60" cy="50" fill="green"/>
<circle r="25" cx="60" cy="50" fill="blue"/>
<circle r="35" cx="60" cy="50" fill="red"/>
<circle fill="none" cx="60" cy="60" r="50"/>
<ellipse rx="17" ry="20" fill="none" stroke="blue" stroke-width="2"/>
<ellipse rx="27" ry="20" fill="none" stroke="blue" stroke-width="2"/>
<ellipse rx="37" ry="20" fill="none" stroke="blue" stroke-width="2"/>
<path d="M10,10 L50,50" fill="none"/>
<path d="M20,20 L60,60" fill="none"/>
</svg>
44 changes: 44 additions & 0 deletions test/plugins/removeUselessStrokeAndFill.07.svg.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
should remove elements if they are transparent or have zero fill and stroke values

===

<svg xmlns="http://www.w3.org/2000/svg">
<rect width="20" height="20" fill="transparent" stroke="rgba(0, 0, 0, 0)"/>
<circle r="25" fill="hsla(120, 100%, 50%, 0)" stroke="transparent"/>
<ellipse rx="40" ry="20" fill="rgba(255, 255, 255, 0)" stroke="hsla(240, 100%, 50%, 0%)"/>

<rect fill="red" fill-opacity="0" stroke="blue" stroke-opacity="0" width="30" height="30"/>
<circle fill-opacity="0" stroke-width="0" r="25"/>

<rect width="50" height="50" fill="transparent" stroke="red"/>
<circle r="25" fill="blue" stroke="rgba(0, 0, 0, 0)"/>

<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5">
<path d="M 0 0 L 10 5 L 0 10 z"/>
</marker>
</defs>
<path d="M10,10 L50,50" fill="transparent" stroke="rgba(0, 0, 0, 0)" marker-start="url(#arrow)"/>
<path d="M10,10 L50,50" fill="none" stroke="rgba(0, 0, 0, 0)" marker-mid="url(#arrow)"/>
<path d="M10,10 L50,50" fill="none" stroke="rgba(0, 0, 0, 0)" marker-end="url(#arrow)"/>

</svg>

@@@

<svg xmlns="http://www.w3.org/2000/svg">
<rect width="50" height="50" fill="none" stroke="red"/>
<circle r="25" fill="blue"/>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5">
<path d="M 0 0 L 10 5 L 0 10 z"/>
</marker>
</defs>
<path d="M10,10 L50,50" fill="none" stroke="rgba(0, 0, 0, 0)" marker-start="url(#arrow)"/>
<path d="M10,10 L50,50" fill="none" stroke="rgba(0, 0, 0, 0)" marker-mid="url(#arrow)"/>
<path d="M10,10 L50,50" fill="none" stroke="rgba(0, 0, 0, 0)" marker-end="url(#arrow)"/>
</svg>

@@@

{ "removeNone": true }