# Tips for golfing in SVG

SVG is an XML vector graphics markup language embeddable in web content.

Your tips should be at least somewhat specific to SVG.

• So apparently SVG inside HTML5 doesn't need to be well-formed XML? I'd love an answer that explains the limits of that more Commented Mar 4, 2023 at 0:15

Note: this answer talks about the implementation of transforms described in the SVG 1.1 standard, not the CSS standard.

In SVG there are five main types of affine transforms that can be used in the transform attribute of an object.

• matrix(a,b,c,d,e,f) is the most general one. It transforms using the affine transformation matrix $$\begin{bmatrix}x'\\y'\end{bmatrix}\leftarrow\begin{bmatrix}a&c\\b&d\end{bmatrix}\begin{bmatrix}x\\y\end{bmatrix}+\begin{bmatrix}e\\f\end{bmatrix}$$ where the coordinate system is the usual "x right, y down" one in the frame of the deepest enclosing group of the object in question, which may be the document root. Thus (for example) groups of groups of clones can be arranged in a tree with one simple transformation per group to achieve fractal-like effects.
• translate(x,y) does as you would expect. Note that translate(x) works and is equivalent to translate(x,0).
• Ditto for scale(x,y), but here scale(x) = scale(x,x). Negative values can be provided, and indeed scale(-1) is shorter than rotate(180) (see below).
• rotate(d) means rotation by d degrees clockwise about the origin (of the current coordinate frame). rotate(d,x,y) means rotation by d degrees clockwise about (x,y).
• Define skew(a,b) = matrix(1,tan(b),tan(a),1,0,0) where a,b are again in degrees. This is not in SVG, but skewX(a) = skew(a,0) and skewY(b) = skew(0,b) are.

My experience with compressing My Little Pony: Friendship Is Magic cutie marks has taught me that in the most general case using matrix is the best. When the transform is only a similarity (angle-preserving) using rotate(a,b,c)scale(s) (transforms are applied right-to-left) is usually better, and for yet more special cases composing two of the non-matrix primitives above is usually better, but never three.

A particularly egregious example of transform compression can be seen in Fluttershy's cutie mark from the relevant section of my Selwyn repository. (The actual file over there duplicates groups because clones don't play well with the path effects of Inkscape; I often use the Envelope Deformation path effect to slightly bend cutie marks to fit the curving flanks of ponies, as can be seen e.g. here.)

<svg viewBox=-50,-50,100,100><g id=a><path d=m15-33.8c-3.8-4,10.2-9.1-3,16.3,14.4-25.5,18.3-11.7,12.6-12.2 fill=none stroke=#69c8c3 stroke-width=.9 /><path d=m17.1-14.3c12.5,13.7-8.1,27-9.7,4-21.6,12.5-21.9-12.6-3-11.6-11.9-12.7,10-31.6,8.4,2.7,28.5-19.9,19.3,2.4,4.3,4.9z fill=#f3b5cf /><path d=m5.9-7.6c4.4-11.9,8.7-17.7,10-17.1,1.3.6-1,7.8-10,17.1z fill=#69c8c3 /></g><use href=#a transform=matrix(.83,0,0,.83,11.9,37.3) /><use href=#a transform=rotate(-65,22.3,22)scale(.94) />

Here one of the cloned butterflies is transformed using the rotate()scale() pattern, but the other clone is not because matrix(.83,0,0,.83,11.9,37.3) is shorter than translate(11.9,37.3)scale(.83).

• You should at least mention that you are talking about the SVG variant and not CSS. Whether you link to SVG1.1 or to w3.org/TR/css-transforms-1/#svg-transform might be a matter of taste. Can you confirm that transform="skew(a,b)" works? I've never even tried. – Another matter of interest is the difference in the transform origin. scale transforms around the center of the bounding box might be gainfully written in CSS, because the default origin is center, while the SVG is at (0,0) in userspace. Commented Mar 3, 2023 at 15:02
• @ccprog skew(a,b) doesn't work. And a note about this being SVG's implementation has been added. Commented Mar 3, 2023 at 16:03
• I didn't get the meaning of the relevant sentence on first read. Rephrase? Commented Mar 3, 2023 at 16:46

# Path data: The art of spaces

The most curious term in the BNF grammar is the production comma_wsp?. What it means is: separate numbers with commas and/or whitespace as you wish, or leave them off completely. I'll quote from the spec directly:

The processing of the EBNF must consume as much of a given EBNF production as possible, stopping at the point when a character is encountered which no longer satisfies the production. Thus, in the string "M 100-200", the first coordinate for the "moveto" consumes the characters "100" and stops upon encountering the minus sign because the minus sign cannot follow a digit in the production of a "coordinate". The result is that the first coordinate will be "100" and the second coordinate will be "-200".

Similarly, for the string "M 0.6.5", the first coordinate of the "moveto" consumes the characters "0.6" and stops upon encountering the second decimal point because the production of a "coordinate" only allows one decimal point. The result is that the first coordinate will be "0.6" and the second coordinate will be ".5".

Implied here is that adjecent to command letters no space is needed to separate a number (and commas are forbidden).

The same rules apply to transform functions, but not to the viewBox attribute.

Another small note is about writing SVG content inline in a HTML page. It is legal to leave off the surrounding quotes for an attribute, as long as the attribute contains no whitespace. This applies also to path data, but you might be forced to replace spaces with commas

<path d="M0,0 1,1" />  == <path d=M0,0,1,1 />


# Coordinates: use default (0,0) positioning

Transforming the origin is often cheaper than giving explicit coordinates.

<svg viewBox=-4,-4,8,8><circle r=3 fill=red

# Path data: Path fills close automatically

If a path has a fill, but no stroke, you can leave out closing the path

M0,0H5V5H0V0Z  ==  M0,0H5V5H0V0  ==  M0,0H5V5H0    for stroke="none" or in a clip-path
==  M0,0H5V5H0Z   for stroke="<paint>"


But the automatic closing last segment is alway straight, so if you have a curve as the last command, the repetition of the last coordinate might be inevitable

M0,0H5V5Q0,5 0,0Z  ==  M0,0H5V5Q0,5 0,0


...unless you are able to use another point as the start

M0,0H5V5Q0,5 0,0Z  ==  M5,0V5Q0,5 0,0


# <rect> is never the answer

The equivalent path for a rectangle is always shorter than <rect>

<rect x=5 y=5 width=2 height=1 fill=red />
<path d=M5,5h2v1h-2 fill=red />


If either the width or height is equal to 1 (or otherwise the local line width), <line> is shorter when either the x or y coordinate is 0

One coordinate is 0:

<rect x=5 width=2 height=1 fill=red />
<path d=M5,0h2v1h-2 fill=red />
<line x1=5 x2=7 stroke=red />

Both coordinates are 0:

<rect width=2 height=1 fill=red />
<path d=h2v1h-2 fill=red />
<line x2=2 stroke=red />


# Path data: Try out the A command

Grafical editors tend to approximate elliptical arcs with (sequences of) cubic Bezier curves. For example, Inkscape perfectly understands

M0,0A10,15-20 0 1 10,12


But move the handle of the end point, even if you immediately move it back to its original position, and suddenly you will have a cubic Bezier.

M 0,0 C 4.3412,1.4378 8.2913,6.1779 10,12


This also happens with quadratic Beziers, with the small difference that the result of the conversion is mathematically equal. Conversions from C to A or C to Q on the other hand are impossible most of the time.

With reasonable rounding, you might (almost) regain the original length, but you will have to test this manually

M0,0C4.3,1.4 8.3,6.2 10,12


But take note: the above arc command has extra opportunities for minification. (This is something that the available tools all miss.) The large_arc and sweep parameters are flags. As they can only be 0 or 1, no space needs to follow them

M0,0A10,15-20 0 1 10,12  ==  M0,0A10,15-20 0110,12


I know this looks strange, but it is really part of the grammar. Note the comma_wsp?

elliptical_arc_argument::=
number comma_wsp? number comma_wsp? number comma_wsp
flag comma_wsp? flag comma_wsp? coordinate_pair


# Path data: Use automatic tools

The grammar for the path data microsyntax is especially aimed at a minimal representation, but leaves room for a lot of variants.

Because of that, there are tools available that try to automatically minimize the path data. Notably Adobe Illustrator will automatically try to apply minification on SVG export, and the SVGOMG optimization tool contains a module for the same purpose. The latter one is included in a number of online tools, including for example Figma.

These tools are a good start, but still fall short in some aspects.

# Path data: Use H and V instead of L

If your path contains straight lines that are horizontal or vertical, the following sequences are equal:

M0,0L1,0  ==  M0,0H1
M0,0L0,1  ==  M0,0V1


# Path data: Concatenate paths

Path strings can contain multiple substrings. If they

• have the same styles,
• are defined in the same userspace coordinate system and
• do not overlap,

instead of writing two elements, you can just concatenate the path data strings.

<path d="M0,0 5,5" /><path d="M10,10 15,15" />  ==  <path d="M0,0 5,5M10,10 15,15" />


In this case, you could even take advantage of relative commands

<path d="M0,0 5,5m5,5 5,5" />


# Path data: Don't overestimate relative commands

Adobe Illustrator writes all path commands as relative commands, obviously assuming they will lead to shorter strings. While this is true in a lot of cases, most of the time they will lead to nothing, and sometimes even take up extra space

M10,10 15,16  ==  M10,10l5,6
M10,10 25,26  ==  M10,10l15,16
M25,26 10,10  ==  M25,26l-15-16


In an extra note, relative commands are relative not to the previous (control) point, but to the previous stop point, which might be a further distance away

M0,0C0,2 8,10 15,15  != M0,0c0,2 8,8 7,7     WRONG!
== M0,0c0,2 8,10 15,15


# You have all of unicode

There's a built-in library of thousands upon thousands of graphic primitives, which cost you just a couple of bytes each, plus an up-front cost of <text></text>

In production SVGs, this is horrible practice since you will of course be at the mercy of font rendering and support, and are mixing up graphics and semantics. But good practice has never stopped code golfing!

Interesting unicode blocks:

# Path data: Do not repeat command letters

If there are multiple path segments with the same command, you can leave out the letters on the repetitions.

 M0,0Q0,1 1,1Q1,2 2,2  == M0,0Q0,1 1,1 1,2 2,2


# Path data: Mirror symetrical Bezier curves

There are special commands that mirror the Bezier control points around a stop coordinate.

M0,0Q0,1 1,1 1,2 2,2 2,3 3,3  == M0,0Q0,1 1,1T2,2 3,3
M0,0C0,1 1,2 2,2 3,2 4,3 4,4  == M0,0C0,1 1,2 2,2S4,3 4,4


This even works when mixing quadratic and cubic Beziers:

M0,0C0,1 1,2 2,2Q3,2 3,3  == M0,0C0,1 1,2 2,2T3,3