diff --git a/lib/svgo/tools.js b/lib/svgo/tools.js
index 439157a4f..b66a1bb07 100644
--- a/lib/svgo/tools.js
+++ b/lib/svgo/tools.js
@@ -1,4 +1,5 @@
import { attrsGroups, referencesProps } from '../../plugins/_collections.js';
+import * as csstree from 'css-tree';
/**
* @typedef CleanupOutDataParams
@@ -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;
+}
+
diff --git a/plugins/removeUselessStrokeAndFill.js b/plugins/removeUselessStrokeAndFill.js
index 5b055890a..b162974e4 100644
--- a/plugins/removeUselessStrokeAndFill.js
+++ b/plugins/removeUselessStrokeAndFill.js
@@ -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
@@ -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'];
@@ -79,7 +82,9 @@ 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
@@ -87,7 +92,7 @@ export const fn = (root, params) => {
(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')) {
@@ -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-')) {
@@ -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);
}
}
diff --git a/test/plugins/removeUselessStrokeAndFill.06.svg.txt b/test/plugins/removeUselessStrokeAndFill.06.svg.txt
new file mode 100644
index 000000000..0ab1c78c5
--- /dev/null
+++ b/test/plugins/removeUselessStrokeAndFill.06.svg.txt
@@ -0,0 +1,54 @@
+
+
+@@@
+
+
\ No newline at end of file
diff --git a/test/plugins/removeUselessStrokeAndFill.07.svg.txt b/test/plugins/removeUselessStrokeAndFill.07.svg.txt
new file mode 100644
index 000000000..cf84653f6
--- /dev/null
+++ b/test/plugins/removeUselessStrokeAndFill.07.svg.txt
@@ -0,0 +1,44 @@
+should remove elements if they are transparent or have zero fill and stroke values
+
+===
+
+
+
+@@@
+
+
+
+@@@
+
+{ "removeNone": true }
\ No newline at end of file