Preprocessing for the Web

, Alexander Goedde

An automated SVG workflow for setting up the SVGs for cross-browser display, optimizing and compressing them, and generating fallback PNG versions.

Part 2 of our series on SVG

This is the second part of our series of blog posts on SVG.

  1. SVG - Super Vector Graphics
  2. Preprocessing SVGs for the Web in 3 automated steps
  3. Falling back from SVG

In this part we'll look at:

  • problems with cross-browser display of SVGs and how to solve them
  • size reduction through optimization and compression
  • generating PNG images for fallback solutions

and show an automated solution which covers the above.

Why SVG?

Scalable Vector Graphics (SVGs) offer several advantages over bitmap images.

The most direct is that they scale to any size. No separate image sets on the server required to target anything from a mobile phone to a retina desktop screen. You get perfectly sharp images on any screen.

Since SVG images are markup and have their own DOM, they can be accessed from JavaScript, and styled using CSS (depending on who you add them).

Finally, there are potential file size savings compared to bitmap images.

For more on the basics of SVG in HTML5 see the first part in this series, Super Vector Graphics.

Different browsers, different sizes

We're using Inkscape to create our SVGs. Inkscape stores explicit dimensions at which the image was layouted. If all you want is to display the image at these dimensions, then embed it with an image tag, and you're done1, across all browsers tested for this post.2

But then, they're Scalable Vector Graphics, so it's likely you'll want them to actually scale. An example of this is the images adjusting to the width of the body text they're used to illustrate, e.g. to have

      <div class="textFlow">
         <p>
            The advantage of fluid designs is
            that they adapt to the width of their containers
         </p>
         <img src="mySVG.svg">
      </div>
   

where you could give the textFlow a percentage width of the viewport, and then set the img to a percentage of the textFlow width.

For a working example of this type of layout, see the How it works page on the Crossbar.io website which is built using the techniques from this post and also features responsive design (which we'll cover in another post).

When starting to work with this, it quickly becomes obvious that across browsers our unprocessed SVGs from Inkscape cause some problems.

We'll take a look at the different ways to add the image, the problems that occur and how to solve them.

img - problems easily solved

Most browsers scale the img proportionally once a width is set on the tag. Internet Explorer doesn't - it respects the explicit width and height settings in the SVG itself.

Icon doesn't scale to fit container
The entire grey area should be covered

We need to set the property which SVG provides to enable scaling of images: viewBox.

This specifies a rectangle whithin the image which is then mapped to the bounds of the viewport. If we set the viewBox to the layout dimensions of the SVG, then this scales the entire image. For an image with a width of 100px and a height of 50px we'd add

viewBox="0 0 100 50"

As a default, the aspect ratio is preserved during scaling, so when adjusting to the width of the textFlow, the scaling adds the appropriate height.

Using this, Internet Explorer now scales our SVG contained in an img tag.

object & embed - one step forward ...

With the unmodified SVG in an object or embed tag, all browsers display the image at the dimensions set in it - provided that the type is set correctly to image/svg+xml. Otherwise at least Internet Explorer doesn't know how to process the file. None of them scale the image to the dimensions of the container.

Adding the viewBox in the SVG leads to this scaling in at least Firefox and Internet Explorer. Both Chrome and the Android browser still use the explicit width and height declarations in the SVG, however.

The solution for Chrome is to remove the width and height declarations from the SVG.

With the Android browser, this alone does not fix this problem - but it creates another one regarding the img tag, which previously worked fine: the Android browser requires an explicit height to be set to either the image or the container. Absent this, the img tag falls back to a most inconvenient 100% viewport height!

So, if you don't count the stock Android browser as unsupported legacy, or include it into a bitmap graphics fallback solution, you'll now need to set explicit height on either the SVG or a container element.3

iframe - just different

IFrames don't adjust to the size of their content. Except for some browsers, some time. Among those tested, Chrome and the Android browser handle IFrame sizes differently: they adjusted the height of the IFrame so that the contained image could be scaled to the full width of the IFrame. They also shrunk it to the size of the image if neither height nor width were given.

Internet Explorer and Firefox, with just the width of the IFrame given, set the height at 150px which is the fallback value given in the spec. Without a declaration for the width, this is set to 300px.4

Well, adding an explicit height makes everybody behave the same way.

File size - the other size

For download sizes, an initial comparison for the Crossbar.io website which we're working on at the moment, across the 27 images presently used, comes out as

  • png 321 KB
  • svg 581 KB

So SVGs, admittedly across a pretty small sample size, seem to incur an additional download size of 269 KB, or a whopping 86%!

While the advantages would probably outweigh this for us for our specific use case (diagrams we need to be sharp at any size, on not especially image-heavy or large pages), it would still be a serious downside.

Optimizing verbose XML

But then these are Inkscape SVGs - and as it turns out, this means that there is significant room for optimization. For one, Inkscape files contain editing history, and with it possibly all kinds of elements, filters and other definitions which are no longer needed. Additionally, the XML that Inkscape writes is generally verbose, and can be shortened considerably.

Scour is an open-source Python script which cleans and optimizes SVG files. For more information, take a look at the original project page.

With this, taking advantage of these optimization possibilities is just a quick shell command away.

scour -i mysvg.svg -o mysvg_optimized.svg

To give you an idea of what this does, you can compare the source Inkscape SVG and the scoured version for the relatively simple Crossbar.io logo:

Crossbar.io icon

Processing our 27 images, the size relationship is now

  • png 321 KB
  • svg 219 KB

A 86% penalty has turned into a 30% gain!

It's text, so gzip it!

We're not finished yet, though. Since the SVG is just XML, i.e. text, it compresses really well.

If we have gzip installed, we can do this on the command line, e.g.

gzip < mySVG.svg > mySVG.svg.gz

Doing so for our test files, we now get

  • png 310 KB
  • svg 74.1 KB

The SVG images in our (admittedly small) sample set are now just a quarter the size of the bitmap images. It turns out that not only is size not a problem when using SVGs for our website, but provides substantial savings. This is always good, but especially welcome when considering uses on mobile connections!

A few remarks regarding the comparison: The PNGs we are using are intended for regular resolution displays, with a maximum content width of 960px (though few are full width). Using higher-resolution images for high-res desktop displays would further skew things in favor of the SVGs. Gzipping the PNGs brings not enough size reduction to shift the size ratio in any meaningful way.

Scour --enable-viewboxing

The default options for scour provide great space savings, though you can of course go further.5

Looking over the scour options led to a welcome discovery: There is an option we can use to apply the changes we want for scalable images, i.e. add the viewBox property and remove the width and height declarations:

--enable-viewboxing

So we get optimized SVGs which scale with their containers - in one processing step.

PNG fallback

As seen earlier, browsers that do support SVG differ in their implementation details - and older browsers (notably IE8 and Android<3.0) don't offer any support.

For these browsers, plus any browsers where getting things to work as intended may be more trouble than it's worth (looking at you, current Android), a fallback solution which switches to bitmaps may be required.

So we need PNG files of our SVG sources.

Creating PNGs via Inkscape

SVGs can, of course, be exported as bitmap images, with Inkscape supporting just PNGs. Instead of having to open every file and then do this manually, Inkscape can be scripted.

"C:\Program Files (x86)\Inkscape\inkscape.exe" -z -e myimage.png myimage.svg

starts Inkscape on a typical Windows install, suppresses the GUI with -z and exports myimage.svg as a PNG file (-e) with the filename myimage.png.6

Switching to PNGs

When looking for a fallback solution which delivered PNGs on problematic browsers, a few things were clear:

  • browsers with SVG support were the normal use case, and these shouldn't incur any extra loads
  • the HTML should stay clean and simple - no nested object / img tags and the like
  • We could live with people whose browser neither supports SVGs nor had JavaScript turned on not displaying any images

We'll cover our implementation of a fallback solution based on these principles in a future blog post.

You can take a look at how this works in practice on the Crossbar.io website:

The automation

It's possible, but tedious do the scouring, compressing and converting to PNG sequentially on the command line, and repeat for each new file. But since every step in this workflow can be done on the command line, it's easy to write a script that combines them.

A simple script works fine, but leads to unnecessary processing. When applied to an entire folder of design files, like we want in our own use case, a change to a single file already leads to processing all files.

Something with proper dependency tracking is a much better option. In our case, this is SCons, a Python build tool.

The process has three steps:

  1. Optimize the source SVG using Scour
  2. Generate a PNG using Inkscape
  3. Compress the generated files

Dependencies

SCons obviously requires an installed Python. Additionally, we're using Taschenmesser, an SCons plugin we wrote and which provides builders for, among other things, gzipping, scouring and PNG generation using Inkscape.

To install Taschenmesser just do

easy_install taschenmesser

For PNG conversion to work, we additionally need not only Inkscape installed, but this also has to have an environment variable set. On Windows, you need to add the Inkscape program directory to the system path.

SConstruct - basics

In the SConstruct file which SCons processes, we first need to do some imports

import os
import pkg_resources
taschenmesser = pkg_resources.resource_filename('taschenmesser', '..')
   

and then set our environment variable


env = Environment(tools = ['default', 'taschenmesser'],
                  toolpath = [taschenmesser],
                  ENV  = os.environ)
   

To create the files we want from a file test.svg, we can now do

test_svg = env.Scour('test_opt.svg', 'test.svg')
test_png = env.Svg2Png('test.png', test_svg)

env.GZip('test_opt.svg.gz', test_svg)
env.GZip('test.png.gz', test_png)
   

SCons keeps track of whether the files have actually been modified, and only runs necessary tasks based on this when called.

We can also pass arguments for the SVG optimization and PNG export. In our case, since we want the viewBox to be added to the SVG, and the default scour options don't do this, we do

test_svg = env.Scour('test_opt.svg', 'test.svg',
                            SCOUR_OPTIONS = {'enable_viewboxing': True})
   

SConstruct - the full monty

We don't want to have to add the above commands for each and every new image we want to process, including, most likely, specifying a target directory.

To ease things, our full script takes a list of files to process, a source directory and target directory relative to the location of the SConstruct file, and then iterates over the list.

SVG_FILES = ['mySVG_01.svg',
             'mySVG_02.svg']

IMG_SOURCE_DIR = "design"
IMG_GEN_DIR    = "website/tavendocom/static/img/gen"

import os
import pkg_resources

taschenmesser = pkg_resources.resource_filename('taschenmesser', '..')

env = Environment(tools = ['default', 'taschenmesser'],
                  toolpath = [taschenmesser],
                  ENV  = os.environ)


## build optimized SVGs, PNGs and gzipped versions of the former
## inside IMG_GEN_DIR
##
for svg in SVG_FILES:
   svg_optim = env.Scour("%s/%s" % (IMG_GEN_DIR, svg),
                         "%s/%s" % (IMG_SOURCE_DIR, svg),
                         SCOUR_OPTIONS = {'enable_viewboxing': True})
   env.GZip("%s.gz" % svg_optim[0], svg_optim)

   png = env.Svg2Png("%s.png" % os.path.splitext(str(svg_optim[0]))[0],
                     svg_optim,
                     SVG2PNG_OPTIONS = {})
   env.GZip("%s.gz" % png[0], png)
   

Calling this produces the optimized, converted and compressed files we want, and only does so for new and changed source files.

Automated and easy

So here we have it: automated pre-processing of SVG files, which enables cross-browser scaling of the images, minimizes bandwidth requirements, and generates the PNGs for a possible fallback solution, all in one go.

Footnotes:

1. Inkscape has more robust SVG processing, and implements more SVG features than browsers do. So it's best no to get too fancy. Use simple tools and then convert everything into paths for use on the web.

2.Tested on Chrome 31, Firefox 25 and Internet Explorer 11 on Windows plus the Samsung Android 4.2.2.

3. Of course this sucks. You can set width dependent on the viewport width, but there's no direct way to set a proportional height.
Fortunately, vertical padding set in percent is calculated based on the width of the parent. So you can do a container with 100% width of the resizable parent, give it a padding-bottom/padding-top of e.g. 74% percent for a 4:3 aspect ratio, and use this to house your image. The image is positioned absolutely in there, and spans the entire width and height of its container. Mission accomplished, with a single extra container element.
More on this method e.g. in this sitepoint blog post.

4. According to the relevant W3C spec, absent other defining conditions, inline replaceable elements fall back to a width of 300px at a 2:1 aspect ratio, i.e. a height of 150px.
This means it's best to avoid test content & styling which falls close to either dimensions. For example: With an element of width: 300px; height: 300px, it's easy to assume that the width has actually been applied, and then go hunting for why the same isn't true for the height, which sends you down an entirely wrong path.

5. For our example above, the following scour options were set:
--enable-viewboxing --enable-comment-stripping --enable-id-stripping
--remove-metadata --shorten-ids --strip-xml-prolog

For the full set of scour options, it's easiest to take a look at the code, since the original project website is a bit outdated.

6. For a full overview of options, have a look at the Inkscape man pages.

</div>

Start experimenting and prototyping for IoT applications with Crossbar.io and Raspberry Pi!

Loaded with all the software you need to make the board part of WAMP applications!



Learn more

Recent posts

Atom Feed

Search this Site

Stay Informed

Sign up for our newsletter to stay informed of new product releases and features: