SVG and the off by ½ error

SVG receives a lot of praise for being an easy to use, well-supported, format for vector graphics. When you need high quality resolution independent images there is no substitute. But below the surface of this celebrated format lies a dirty secret.

Pixels and Images

For the types of images we are most used to (JPG, PNG, etc.) an image is constructed from a grid of pixels. A pair of coordinates refer to the whole pixel with no ability to reference a partial pixel.

A pixel in a raster image.
A pixel in a raster image.

An SVG image, however, is not made of pixels. SVG images are made of shapes that are made of points. The coordinates for these points can refer to finer and finer increments with no realistic limit on how small they can be.

This is one of the main reasons why SVGs, and vector graphics in general, provide better quality images than the other formats. Leaving one important question, how do these coordinates translate to the pixel grid.

Pixels, where?
Pixels, where?

Stuck In the Middle

To eliminate this ambiguity, the SVG standard defines the whole number part of the coordinates to be on the boundary between pixels.

A pixel in a vector image.
A pixel in a vector image.

At a surface level this makes a lot of sense, but once we get into the details of rendering the image it presents a significant problem. Lets take a closer look at some rendered SVGs to see what happens.

Rectangle with a 1px black stroke
Rectangle with a 1px black stroke
Rectangle with a 2px black stroke
Rectangle with a 2px black stroke
Rectangle with a 3px black stroke
Rectangle with a 3px black stroke

In this example we've zoomed in on a rectangle with varying thicknesses of strokes, but they are all the same color-- black. How can we be getting such inconsistent results? Lets break it down starting with the two pixel stroke.

The coordinates, and therefore the edge, of the rectangle lie on the boundary between two pixels. So if we were to draw a two pixel stroke one full pixel will lie to the left of the edge and another full pixel will be to the right.

Two half pixels
Two half pixels

Applying this rendering logic to the one pixel stroke gives us our first critical insight in to this problem. We cannot have a half pixel, so we render a full pixel at half intensity both to the left and the right.

You may know this as anti-aliasing. In most cases this is good because it smooths the transition between two hard-edged shapes, but sometimes it can result in blurry edges or illegible text.

What can we do?

To find a solution to this problem, lets take a look at our third example. With a three pixel stroke, it has qualities of both the 2 pixel stroke and the one pixel stroke. This is because every odd number is just an even number plus 1.

Our three pixel stroke is rendered as a two pixel stroke plus the half pixel to the left and right. This insight tracks with all even width strokes rendering crisply, while odd numbered strokes always have a blurry edge. So our solution comes in two parts:

  • If the shape has a stroke with an even pixel width, leave it alone.
  • If the shape has a stroke with an odd pixel width, shift the shape right and down by a half pixel.
The 3px rectangle coming into focus when we apply our logic
The 3px rectangle coming into focus when we apply our logic

Final thoughts

With this simple technique, you can always have crisply rendered SVG images.