SVG is an XML vector graphics markup language embeddable in web content.
Your tips should be at least somewhat specific to SVG.
Please post one tip per answer.
Code Golf Stack Exchange is a question and answer site for programming puzzle enthusiasts and code golfers. It only takes a minute to sign up.
Sign up to join this communitySVG is an XML vector graphics markup language embeddable in web content.
Your tips should be at least somewhat specific to SVG.
Please post one tip per answer.
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)
.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)
.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)
.
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.
\$\endgroup\$
skew(a,b)
doesn't work. And a note about this being SVG's implementation has been added.
\$\endgroup\$
Mar 3 at 16:03
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 />
Transforming the origin is often cheaper than giving explicit coordinates.
<svg viewBox=-4,-4,8,8><circle r=3 fill=red
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 answerThe 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 />
A
commandGrafical 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
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.
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 strings can contain multiple substrings. If they
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" />
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
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:
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
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