svgo icon indicating copy to clipboard operation
svgo copied to clipboard

Inline things in defs referenced only once

Open kirilloid opened this issue 8 years ago • 21 comments

Since ids are mangled by default, they hardly can be referenced reliably outside. So it's worth inlining if we detect they're used only once (after removing invisible and other parts). Such references might happen pretty often even in hand-written code.

kirilloid avatar Jul 01 '16 16:07 kirilloid

Yep, but I don't understand your point. What's next?

GreLI avatar Jul 01 '16 18:07 GreLI

Sorry, I don't understand what you don't understand. I'm suggesting a feature for the tool.

kirilloid avatar Jul 01 '16 18:07 kirilloid

Consider this example @GreLI.

This defs/mask/use structure is typical of some graphics programs output like Illustrator or Sketch.

With IDs:

<svg width="315" height="92" viewBox="0 0 315 92" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <defs>
        <rect id="a" x="7" y="5" width="301" height="31" rx="3"/>
        <mask id="m" x="0" y="0" width="301" height="31" fill="#fff">
            <use xlink:href="#a"/>
        </mask>
    </defs>
    <g fill="none" fill-rule="evenodd" opacity=".1">
        <g stroke="#FFF">
            <g>
                <g opacity=".8">
                    <g>
                        <use mask="url(#m)" stroke-width="4" opacity=".4" xlink:href="#a"/>
                    </g>
                </g>
            </g>
        </g>
    </g>
</svg>

Optimized by inlining referenced element:

<svg width="315" height="92" viewBox="0 0 315 92" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <g fill="none" fill-rule="evenodd" opacity=".1">
        <g stroke="#FFF" opacity=".8">
            <rect x="7" y="5" width="301" height="31" rx="3" stroke-width="4" opacity=".4"/>
        </g>
    </g>
</svg>

themadcreator avatar Nov 11 '16 19:11 themadcreator

@themadcreator I like this idea a lot, but are you sure your example is correct? I believe Sketch uses the shape-with-mask pattern to recreate the stroke-position: inside setting which is not available in SVG. The mask is supposed to hide the part of the stroke that falls outside the shape. By removing the mask you are breaking this and effectively doubling the width stroke.

Here's a screenshot of the before and after:

image

steadicat avatar Dec 06 '16 19:12 steadicat

Though that example was not great, this feature would still be neat for cases where the referenced element is truly only used once within the SVG.

Here's a snippet from one of mine:

<defs>
    <path id="SVGID_1_" d="M8.9,12.3c0,0,2,0,2.8,0c1.8,0,3.6-3.4,3.6-5.8c0-3.2-2.5-5.8-5.6-5.8C7.5,0.7,5.5,2,4.6,4
					C2.4,4.1,0.7,6,0.7,8.2c0,2.3,2.1,4.1,4,4.1C6.1,12.3,8.9,12.3,8.9,12.3z"/>
</defs>
<clipPath id="SVGID_2_">
    <use xlink:href="#SVGID_1_" style="overflow:visible" />
</clipPath>

Can easily become

<defs>
    <clipPath id="a">
        <path d="M8.9 12.3h2.8c1.8 0 3.6-3.4 3.6-5.8 0-3.2-2.5-5.8-5.6-5.8C7.5.7 5.5 2 4.6 4 2.4 4.1.7 6 .7 8.2c0 2.3 2.1 4.1 4 4.1h4.2z"/>
    </clipPath>
</defs>

I'm just starting to use SVGO and SVGs in my workflow, but I'm seeing a number of examples like this exported from Sketch with <mask>, <clipPath> and other elements.

ProdigySim avatar Jan 25 '17 21:01 ProdigySim

I just found out firefox has some issues with rendering SVGs that use xlink:href on pages with a <base> tag. This kind of automatic refactor would be really killer for helping with those bugs.

See this codepen for example: http://codepen.io/anon/pen/oWgMov (renders on chrome, but not ff) And this firefox bug (marked fixed, but I think there is still an issue): https://bugzilla.mozilla.org/show_bug.cgi?id=652991

ProdigySim avatar Apr 14 '17 17:04 ProdigySim

@ProdigySim: Is this related?: https://github.com/svg/svgo/pull/733

strarsis avatar Oct 23 '17 12:10 strarsis

@GreLI I'd really love a feature like this, currently, I'm working with a designer who uses masks to apply colors to icons in sketch and during the build, I'd like to remove them because they're essentially useless to me and causing display issues in production.

lifeiscontent avatar Mar 18 '18 15:03 lifeiscontent

specifically in this case:

  <svg
    width="60"
    height="60"
    viewBox="0 0 60 60"
    xmlns:link="http://www.w3.org/1999/xlink"
  >
    <defs>
      <path
        d="M20.588 26L19.1 22.424h-7.2L10.412 26h-2.52l7.416-17.04h.336L23.084 26h-2.496zm-7.776-5.784l2.688-6.48 2.688 6.48h-5.376zM47.744 34.2v.288L40.64 48.792h6.864V51H37.256v-.336l7.152-14.256h-7.056V34.2h10.392zM27 18.253c0-.14.114-.253.256-.253h2.488a.25.25 0 0 1 .256.253v23.494c0 .14-.114.253-.256.253h-2.488a.25.25 0 0 1-.256-.253V18.253z"
        id="a"
      />
    </defs>
    <use xlink:href="#a" fillRule="evenodd" />
  </svg>

the xlink:href is causing issues due to all references being named a in sketch.

lifeiscontent avatar Mar 18 '18 15:03 lifeiscontent

@lifeiscontent: What about using the svgo prefixIds plugin?

strarsis avatar Mar 18 '18 15:03 strarsis

@strarsis that works, but it'd be ideal to simplify the SVG.

lifeiscontent avatar Apr 06 '18 12:04 lifeiscontent

@lifeiscontent: You can use a custom hash/prefix callback, so you can use some custom unique string minifier.

strarsis avatar Apr 06 '18 12:04 strarsis

@strarsis how do you use a callback in SVGO?

lifeiscontent avatar Apr 06 '18 13:04 lifeiscontent

@lifeiscontent: https://github.com/svg/svgo/issues/913#issuecomment-373416266

strarsis avatar Apr 06 '18 14:04 strarsis

This would be very useful for exported svg from sketch, most <defs> are only being used once.

zomars avatar May 17 '18 18:05 zomars

I dare adding another example : With svgo only :

<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <defs>
    <path d="M8.18 8H6v13h4.78L8.18 8zm2.04 0l2.6 13H26V8H10.22zM2 24h28l-2 3H4l-2-3zM5 6h22a1 1 0 0 1 1 1v15a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z" id="a"/>
  </defs>
  <use fill="#0082C3" xlink:href="#a" fill-rule="evenodd"/>
</svg>

With svgood (https://github.com/BrentonWheeler/svgood) + svgo

<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
  <path d="M8.18 8H6v13h4.78L8.18 8zm2.04 0l2.6 13H26V8H10.22zM2 24h28l-2 3H4l-2-3zM5 6h22a1 1 0 0 1 1 1v15a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/>
</svg>

0gust1 avatar Nov 20 '18 15:11 0gust1

@lifeiscontent the same thing here. Our designer also uses masks to apply colors, and doing so produces a lot of useless and sometimes broken code. I currently clean this up manually, but it takes a lot of time. I think this is not an uncommon problem.

mixtur avatar Jun 24 '19 15:06 mixtur

Hello, everyone! I am writing an SVG image spread across various files, and combining it on Node during build by using a self‐made script (to make ids unique based on file path) that uses jsdom, and then passing the result to SVGO.

As such, I have a lot of elements in defs that are used by a single use element from a different file in the source, and I would really appreciate this kind of feature!

So, I figured I’d implement it myself! (And that’s what I did.)

If anyone thinks it would be useful, feel free to copy and paste:

{
	type: "full",
	fn: item =>
	{
		let referenced = {}
		let referencedOnce = {}
		let references = {}
		let parents = new Map()
		
		let action = item =>
		{
			if(item.hasAttr("id"))
				referenced[item.attr("id").value] = item
			
			if(item.hasAttr("href") && item.attr("href").value.startsWith("#"))
			{
				let id = item.attr("href").value.slice(1)
				referencedOnce[id] = referencedOnce[id] === undefined
				if(referencedOnce[id]) references[id] = item
			}
			
			for(let name in item.attrs)
			{
				let value = item.attrs[name].value
				
				let match = value.match(/\burl\(("|')?#(.+?)\1\)/)
				if(match)
					referencedOnce[match[2]] = false
			}
		}
		
		let walk = item =>
		{
			if(!item.isElem()) return
			
			action(item)
			
			if(!item.content) return
			
			for(let child of item.content)
			{
				parents.set(child, item)
				walk(child)
			}
		}
		
		walk(item)
		
		for(let id in referenced)
		{
			let item = referenced[id]
			switch(referencedOnce[id])
			{
				case undefined:
					item.removeAttr("id")
					break
				case true:
					let parent = parents.get(item)
					if(!parent.isElem("defs")) break
					let replaced = references[id]
					let replacing = parents.get(replaced)
					parents.set(item, replaced)
					parent.content = parent.content.filter(child => child !== item)
					for(let child of replacing.content)
					{
						if(child === replaced)
						{
							item.removeAttr("id")
							child.removeAttr("href")
							child.renameElem("g")
							child.content = [item]
						}
					}
					break
			}
		}
		
		return item
	},
}

With this, I was successfully able to reduce my (current) SVG by 1.4kB (from 18.1kB to 16.7kB).

Unfortunately, it only works with use elements (and not mask, etc.), so @lifeiscontent’s and @mixtur’s use cases are not covered.

Note: This replaces the use elements with g. I experienced best results when rerunning SVGO several times on the same input to allow it to collapse all those g elements. I used the following loop:

let data = "<svg />" // Your SVG.
for(let i = 0; i < 12; i++)
{
	let newdata = (await svgo.optimize(data)).data
	if(data === newdata) break
	data = newdata
}

I set an upper bound of twelve iterations, but in reality, it stops after about five on the break.

I hope someone is able to appreciate this! Cheers〜

Edit: Note that this doesn’t work well when the use element has either an x or a y attribute (or both).

zamfofex avatar Aug 17 '19 02:08 zamfofex

PR ready: https://github.com/svg/svgo/pull/1279 No recursion yet, but svgo multipass feature should handle this.

You can already try this feature branch by installing it using this npm command: npm install github:strarsis/svgo#dereferenceUses-plugin Note that this plugin currently doesn't run by default and has to explicitly enabled: svgo --enable=dereferenceUses [...]

strarsis avatar Sep 02 '20 18:09 strarsis

Hi there! I am also curious as to why the PR hasn't been merged. Is there a reason?

Thanks in advance!

Edit: Just realized there is an unresolved comment in the PR.

eLarocque avatar Mar 14 '22 18:03 eLarocque