Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inline things in defs referenced only once #563

Open
kirilloid opened this issue Jul 1, 2016 · 21 comments
Open

Inline things in defs referenced only once #563

kirilloid opened this issue Jul 1, 2016 · 21 comments

Comments

@kirilloid
Copy link

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.

@GreLI
Copy link
Member

GreLI commented Jul 1, 2016

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

@kirilloid
Copy link
Author

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

@themadcreator
Copy link

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>

@steadicat
Copy link

steadicat commented Dec 6, 2016

@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

@ProdigySim
Copy link

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
Copy link

ProdigySim commented Apr 14, 2017

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

@strarsis
Copy link
Contributor

@ProdigySim: Is this related?: #733

@lifeiscontent
Copy link

@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
Copy link

lifeiscontent commented Mar 18, 2018

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.

@strarsis
Copy link
Contributor

strarsis commented Mar 18, 2018

@lifeiscontent: What about using the svgo prefixIds plugin?

@lifeiscontent
Copy link

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

@strarsis
Copy link
Contributor

strarsis commented Apr 6, 2018

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

@lifeiscontent
Copy link

@strarsis how do you use a callback in SVGO?

@strarsis
Copy link
Contributor

strarsis commented Apr 6, 2018

@zomars
Copy link

zomars commented May 17, 2018

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

@0gust1
Copy link

0gust1 commented Nov 20, 2018

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>

@mixtur
Copy link

mixtur commented Jun 24, 2019

@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.

@ghost
Copy link

ghost commented Aug 17, 2019

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).

@strarsis
Copy link
Contributor

strarsis commented Sep 2, 2020

PR ready: #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 [...]

@eLarocque
Copy link

eLarocque commented Mar 14, 2022

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
Copy link

Hi,

Just wanted to ping this PR again.
Next comment might be in another 2 years ;)

Thanks in advance!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests