Recreating raster images as SVGs: part 2

Wherein our hero manually optimizes our image to reduce its size

Published Thu May 23 2019

When last we spoke we had just finished discussing the merits of delivering assets as SVGs instead of PNGs, or other raster formats. We found that SVGs offered greater file size savings and compressibility while simultaneously offering superior image quality. We also saw that we could optimize SVGs to save on file size even more.

So how does one optimize an SVG?

Great question! I’m glad you asked. You see drawing apps tend to use the general purpose and quite versatile <path> tag to create objects in an SVG image. By using more specific tags we can cut down on the amount of text used to describe an object thereby saving on file size. We can also define objects in the <defs> tag, and recall them for use later via the <use> tag. Finally, for objects which repeat, we can use <pattern> to create a repeating pattern. Patterns are saved in and used in an object’s fill attribute, as we’ll see below.

Background

Instead of explaining every step in minute detail I’ll show you the general steps used to optimize our owl image. For each type of optimization performed I’ll take an example from the image and walk through it. Hopefully there’ll be enough info here to help you:

  1. understand what happened with the parts of the image left unexplained
  2. play around with the original image, working along with the sections to aid in your understanding of what we’re doing.

We’ll start with the chevron background. It is actually the <g id="chevron"> group repeated 12 times. The first three are shown below.

And here’s the original markup that gave us the image above.

<g id="chevron">
  <path d="M-30,81.704 L102.37,-50.667 L132.2,-20.837 L-0.171,111.533 L-30,81.704 z" fill="#959595"/>
  <path d="M102.37,-50.667 L234.741,81.704 L204.911,111.533 L72.541,-20.837 L102.37,-50.667 z" fill="#959595"/>
  <path d="M102.366,68.675 L234.736,201.046 L204.907,230.875 L72.536,98.505 L102.366,68.675 z" fill="#959595"/>
  <path d="M-30,201.041 L102.37,68.67 L132.2,98.5 L-0.171,230.87 L-30,201.041 z" fill="#959595"/>
</g>
<g id="chevron">
  <path d="M175.283,81.704 L307.653,-50.667 L337.482,-20.837 L205.112,111.533 L175.283,81.704 z" fill="#959595"/>
  <path d="M307.653,-50.667 L440.023,81.704 L410.194,111.533 L277.824,-20.837 L307.653,-50.667 z" fill="#959595"/>
  <path d="M307.648,68.675 L440.018,201.046 L410.189,230.875 L277.819,98.505 L307.648,68.675 z" fill="#959595"/>
  <path d="M175.283,201.041 L307.653,68.67 L337.482,98.5 L205.112,230.87 L175.283,201.041 z" fill="#959595"/>
</g>
<g id="chevron">
  <path d="M380.432,81.704 L512.802,-50.667 L542.631,-20.837 L410.261,111.533 L380.432,81.704 z" fill="#959595"/>
  <path d="M512.802,-50.667 L645.172,81.704 L615.343,111.533 L482.973,-20.837 L512.802,-50.667 z" fill="#959595"/>
  <path d="M512.797,68.675 L645.168,201.046 L615.338,230.875 L482.968,98.505 L512.797,68.675 z" fill="#959595"/>
  <path d="M380.432,201.041 L512.802,68.67 L542.631,98.5 L410.261,230.87 L380.432,201.041 z" fill="#959595"/>
</g>

To create this background:

  1. I define a group of rectangles at a 90 degree angles to each other.
  2. Next I rotate the group by 45 degrees around the center point of the group.
  3. Finally, I shift the whole thing up a bit because I want to.

Above you see the results of steps 1, 2, and 3. Below is the code which does what was explained above. Our right-angle s are defined in <g id="chevron">.

<defs>
    <g id="chevron" fill="#fff" transform="rotate(-45 101.3705 90.104) translate(17 0)">
        <rect id="chevron-piece" x="0" y="0" width="187.2" height="42.185" />
        <rect id="chevron-piece2" x="145.015" y="0" height="187.2" width="42.185" />
        <rect id="chevron-piece3" x="60.624" y="84.384" height="187.2" width="42.185" />
        <rect id="chevron-piece4" x="-84.384" y="84.384" width="187.2" height="42.185" />
    </g>
    <pattern id="chevron-fill" width="205" height="119" x="0" y="29" patternUnits="userSpaceOnUse">
      <rect  width="205" height="119" x="0" y="0" fill="#959595"/>
      <use xlink:href="#chevron" x="-8.186" y="0"/>
    </pattern>
</defs>
<rect id="chevron-pattern" x="0" y="0" width="612" height="792" fill="url(#chevron-fill)"/>

Once the shape is defined I take the entire group and rotate it by 45 degrees. The rotate() function rotates an object about the x and y coordinates given. Otherwise it rotates an object about (0 0).[^1]

What I wanted to do after rotation was to shift the group to the right by 17px. If you try to edit the value in the browser’s inspector you will quickly realize that this is not actually happening. The code seems to result in movement of the x and y coordinates. That’s because transform parameters are applied from left to right, in the order that they are written. This means that transform="rotate(-45 101.3705 90.104) translate(17 0)" will rotate our group around (101.3705 90.104) and then move this rotated object 17px along the x axis. But since our x-axis is rotated by 45 degrees, it seems to be moving in the x and y direction.

Hopefully it’s clear why I should have done transform=" translate(17 0) rotate(-45 101.3705 90.104)" instead (move, then rotate) to achieve horizontal movement along the original x-axis. Why keep the mistake then? Makes for a great teachable moment I think. Once I had the chevron group in place I created a pattern for it as shown above.

The SVG specification document actually does a good job of explaining the different pattern parameters. The pattern contains a reference to our rotated chevron via the <use> tag. Meanwhile a <rect> the size of the canvas was created and its ``fill`` value — instead of being a color, or a gradient[^2] — is the id of our pattern.

Eyes

Let’s move on to the eyes. They are simple circles. Height = width with a radius that’s half of the diameter (which is the width).

Here’s the original svg code for that

<g id="Layer_2">
  <g id="eye">
    <path d="M386,365 C335.742,365 295,323.81 295,273 C295,222.19 335.742,181 386,181 C436.258,181 477,222.19 477,273 C477,323.81 436.258,365 386,365 z" fill="#959595"/>
    <path d="M389.5,336 C351.116,336 320,304.884 320,266.5 C320,228.116 351.116,197 389.5,197 C427.884,197 459,228.116 459,266.5 C459,304.884 427.884,336 389.5,336 z" fill="#FFFFFF"/>
    <path d="M389.5,308.6 C363.653,308.6 342.7,287.647 342.7,261.8 C342.7,235.953 363.653,215 389.5,215 C415.347,215 436.3,235.953 436.3,261.8 C436.3,287.647 415.347,308.6 389.5,308.6 z" fill="#333333"/>
    <path d="M374.2,285 C363.541,285 354.9,276.359 354.9,265.7 C354.9,255.041 363.541,246.4 374.2,246.4 C384.859,246.4 393.5,255.041 393.5,265.7 C393.5,276.359 384.859,285 374.2,285 z" fill="#FFFFFF"/>
  </g>
  <g id="eye">
    <path d="M222,365 C171.742,365 131,323.81 131,273 C131,222.19 171.742,181 222,181 C272.258,181 313,222.19 313,273 C313,323.81 272.258,365 222,365 z" fill="#959595"/>
    <path d="M225.5,336 C187.116,336 156,304.884 156,266.5 C156,228.116 187.116,197 225.5,197 C263.884,197 295,228.116 295,266.5 C295,304.884 263.884,336 225.5,336 z" fill="#FFFFFF"/>
    <path d="M225.5,308.6 C199.653,308.6 178.7,287.647 178.7,261.8 C178.7,235.953 199.653,215 225.5,215 C251.347,215 272.3,235.953 272.3,261.8 C272.3,287.647 251.347,308.6 225.5,308.6 z" fill="#333333"/>
    <path d="M210.2,285 C199.541,285 190.9,276.359 190.9,265.7 C190.9,255.041 199.541,246.4 210.2,246.4 C220.859,246.4 229.5,255.041 229.5,265.7 C229.5,276.359 220.859,285 210.2,285 z" fill="#FFFFFF"/>
  </g>
</g>

What we have above is a group of circles which make up the eye grouped with a <g> tag. What’s the <g>’s purpose? To group things. We also have an id on the to help identify it. So when this was originally made in iDraw I made an eye, grouped it, named the group, and then copied it to create the other eye. Then I drug the copied eye over to its new position. Copying the original eye duplicated the id attribute sadly; a big no-no for HTML/XML documents, where all ids must be unique. In the case of an SVG (which is an xml document) if we target that id with javascript weird things will happen. Part of the optimization process then, will include removing these duplicate ids.

Here is the optimized code:

<defs>
  <g id="eye">
    <circle cx="222" cy="273" r="92" fill="#959595"/>
    <circle cx="225.5" cy="266.5" r="69.5" fill="#fff"/>
    <circle cx="226" cy="262"  r="47" fill="#333"/>
    <circle cx="210.5" cy="265.5" r="19.5" fill="#fff"/>
  </g>
</defs>
<g id="zowl">
  <use id="left-eye" xlink:href="#eye" x="1" y="0"/>
  <use id="right-eye" xlink:href="#eye" x="167" y="0"/>
</g>

Quite a difference isn’t it. You’ll notice it’s much more efficient to define a circle using <circle> than to use a path.

Ears and Nose

Next came the ears and the nose. These three objects are all the same since the ears are just rotated and scaled versions of the nose.

Below you’ll notice there are three separate path objects.

<path id="ear-l" d="M154.139,199.144 L153.31,196.391 C147.811,188.505 143.635,179.795 140.422,170.756 C135.002,154.642 136.416,140.5 136.416,140.5 L136.481,140.519 L136.462,140.454 C136.462,140.454 150.604,139.04 166.718,144.46 C175.79,147.573 184.45,151.911 192.353,157.348 L195.106,158.177 L154.139,199.144 z" fill="#F23897"/>
<path id="ear-r" d="M459.467,199.144 L460.296,196.391 C465.795,188.505 469.971,179.795 473.184,170.756 C478.604,154.642 477.19,140.5 477.19,140.5 L477.125,140.519 L477.145,140.454 C477.145,140.454 463.002,139.04 446.888,144.46 C437.817,147.573 429.156,151.911 421.253,157.348 L418.5,158.177 L459.467,199.144 z" fill="#F23897"/>
<path id="nose" d="M334.416,288 L333.055,290.534 C331.368,299.997 328.162,309.109 324.042,317.773 C316.48,333 305.48,342 305.48,342 L305.448,341.94 L305.416,342 C305.416,342 294.416,333 286.854,317.773 C282.641,309.157 279.584,299.966 277.841,290.533 L276.48,288 L334.416,288 z" fill="#F6B7D6"/>

Instead of keeping these three, we define one in <defs> and reference it via <use> and the xlink:href attribute. One of the ears requires a rotation of the . The second ear references the first ear and translates it, then flips it horizontally.

<defs>
    <path id="appendage" d="M57.936,-0 L56.575,2.533 C54.888,11.997 51.682,21.109 47.562,29.773 C40,45 29,54 29,54 L28.968,53.94 L28.936,54 C28.936,54 17.936,45 10.374,29.773 C6.161,21.157 3.104,11.966 1.361,2.533 L0,-0 L57.936,-0 z"/>
</defs>
<use id="ear-l" xlink:href="#appendage" transform="rotate(125 161.94 174.78)" x="134.286" y="166.274" fill="#F23897"/>
<use id="ear-r" xlink:href="#ear-l" transform="translate(610 0) scale(-1 1) "/>
<use id="nose" xlink:href="#appendage" x="279" y="288" fill="#F6B7D6"/>

Wings

Here are the wings we’ll be flying high with today.

Now this one’s interesting. Since we only have two hands (wings) we don’t save a whole lot by using <defs> and <use> tags here. As a matter of fact, use of the <defs> tag isn’t really necessary[^3]. Why do it then? Just to keep the markup clean. And speaking of the markup, here it is:

<path id="hand-r" d="M534.517,527.46 C534.517,527.46 534.517,527.46 534.517,527.46 C534.517,527.46 534.517,527.46 534.517,527.46 C534.517,527.46 534.517,527.46 534.517,527.46 C534.517,527.46 534.517,527.46 534.517,527.46 C530.322,537.049 462.001,406.401 466.795,350.665 C468.206,334.269 477.839,327.491 491.244,327.491 C504.649,327.491 514.903,342.109 518.335,350.066 C531.52,380.631 550.666,490.547 534.517,527.46 z" fill="#F6B7D6"/>
<path id="hand-l" d="M81.419,527.46 C81.419,527.46 81.419,527.46 81.419,527.46 C81.419,527.46 81.419,527.46 81.419,527.46 C81.419,527.46 81.419,527.46 81.419,527.46 C81.419,527.46 81.419,527.46 81.419,527.46 C85.614,537.049 153.935,406.401 149.141,350.665 C147.73,334.269 138.097,327.491 124.692,327.491 C111.287,327.491 101.033,342.109 97.6,350.066 C84.416,380.631 65.269,490.547 81.419,527.46 z" fill="#F6B7D6"/>

Our optimized code takes the second hand — id="hand-l" above — and sticks it in a <defs> as id="hand". Then we use it later to make our left and (horizontally flipped) right hands. If you look below you’ll see transform="scale(-1 1) translate(-616 0)". We use scale to mirror objects by giving it negative values. The scale(-1 1) changes the x-axis values, flipping our wings horizontally.

Using this function changes the x coordinate values and makes them negative. Since the x values become negative, our wing will be outside the boundary of our canvas. We compensate for this by using translate to move our wing to its new location within the bounds of our canvas.

<defs>
    <path id="hand" d="M81.419,527.46 C81.419,527.46 81.419,527.46 81.419,527.46 C81.419,527.46 81.419,527.46 81.419,527.46 C81.419,527.46 81.419,527.46 81.419,527.46 C81.419,527.46 81.419,527.46 81.419,527.46 C85.614,537.049 153.935,406.401 149.141,350.665 C147.73,334.269 138.097,327.491 124.692,327.491 C111.287,327.491 101.033,342.109 97.6,350.066 C84.416,380.631 65.269,490.547 81.419,527.46 z" fill="#F6B7D6"/>
</defs>
<use id="hand-r" xlink:href="#hand" transform="scale(-1 1) translate(-616 0)" x="0" y="0"></use>
<use id="hand-l" xlink:href="#hand" x="0" y="0"/>

Eventually we end up with a file that, as you’ll see below, brings quite some savings from the original while looking just as good.

Original Optimized
original owl SVG illustration optimized owl SVG illustration

Conclusion

After going through all this trouble a very good question to ask is, Was it worth it? My answer is: it depends.

  • If you’re goal is to animate SVGs later, your best bet is actually to leave your <path>s intact[^4].
  • If you’re just using them as static images I’d recommend getting those file sizes down as much as possible. You can see from the table below that optimization of our owl image gives us ~67% savings in both our normal and compressed SVG.

Table 1. File size comparison between SVG owl image

SVG Compressed SVG
Original file size 16K 5.6K
Optimized file size 5.3K 1.8K
% savings of optimization 66.9% 67.9%

Addendum

Optimization Tools

Optimizing by hand, while great for learning, does become tedious after a while. It sure would be great if there were some thing, some tool we could use to help us. And lo’, there is just such a tool by the name of SVGOMG.[^5] This is actually based on the node.js app SVGO, but what makes SVGOMG much nicer is that we get a live preview of the changes it proposes.

While SVGO won’t replace redundant items with <pattern> or use <circle> and <rect> where possible, it still achieves considerable file size savings. Given that, SVGO and SVGOMG are promising tools for developers in their constant battle against slow-loading sites and heavy apps.

Combined, manual and programmatic optimization of SVGs promise considerable file size savings. Moreso than is attainable by either technique individually. At least one of these techniques should be used before declaring a project production-ready.

[^1]: For objects that aren’t actually located at (0 0), the result is likely not what you expect. To avoid this you can translate to (0 0), rotate, then translate back to the desired position.

[^2]: Yep. It’s possible to create gradients via the <linearGradient> and <radialGradient> tags, stick them in <defs>, then use them as fill values.

[^3]: Anything with a unique id can be referenced from a <use> tag via the xlink:href attribute. Also, both of our hands are already at the positions I need them to be in, so I will not need to move it to a new position using the x and y attributes). I don’t really gain anything by first hiding it in a <defs> tag, and then showing it later in the <use>.

[^4]: Some SVG libraries, like Raphael.js, only work with <path> so converting to <circle> or other shapes wouldn’t be helpful if you’re planning on working with these libraries. You should still be able to do some optimization, e.g. using <pattern> to minimize duplication.

[^5]: It is very important to look at the output SVGO produces carefully because it can distort the final image when certain options are checked. I’ve found this to be particularly true when SMIL animations and transforms are in play.