SVG text transform (translate, scale, rotate)

I was playing with the idea of saving data from canvas element into an SVG file, and experienced first-hand just how much of a pain the SVG standard is. In this post I’ll concentrate on the most annoying part I had to deal with so far, text transformation. I wanted to share it with other interested readers since none of the online tutorials I found explain the order of operations well enough and also to document it for myself in case I need to deal with it in the future, since I spent a good 5 hours playing with it until I figured all the math out. First of all, the text tag is saved as follows:

<text x="left_x_coord" y="btm_y_coord" fill="color" font-size="10" 
font-family="sans-serif" transform="translate(...) scale(...) rotate(...)">

Items in red are to be specified by the user. Items in yellow, like font size and family are configurable in HTML5Canvas wrapper I submitted to Pyjamas, if you’re using GWTCanvas, assume font-size of 10 and sans-serif font-family since those are the values it defaults to. Be careful not to define the font as 10pt, which is not the same as 10 pixels. Items in pink identify the transformation matrix, they can be specified in any order but order will affect how the coordinates are applied, this blog post is about them. While each tag by itself is pretty simple, it’s their interaction that can be counter-intuitive. The following is an explanation of how to use them based on order:

translate(X Y)

Translate translates the frame of reference by X and Y pixels, and affects the x, y positions of text. So the following two statements will produce text in the same position:

<text x="x_coord0" y="y_coord0" transform="translate(x_offset, y_offset)">text

<text x="x_coord0+x_offset" y="y_coord0+y_offset">text</text>

scale(X [Y])

Scale takes in the ratio to scale X and Y coordinates by, if Y coordinate is not provided then the same scale ratio is applied to both. Scale occurs relative to 0, 0 coordinate (top-left corner) of the drawing, which means that scale(2) will not only double the size of text but also double its distance from top-left corner. So if you want to scale the text in-place, you have to either apply translate() beforehand to offset it by -coord0(scale-1) or apply it afterwards with offset of -coord0(scale-1)/scale. Essentially, either of the following two statements would scale the text without moving its position:

<text x="x_coord0" y="y_coord0" transform="translate(-x_coord0*(x_scale-1), 
-y_coord0*(y_scale-1)) scale(x_scale, y_scale)">

<text x="x_coord0" y="y_coord0" transform="scale(x_scale, y_scale) 
translate(-x_coord0*(x_scale-1)/x_scale, -y_coord0*(y_scale-1)/y_scale)">

rotate(degs [X Y])

Rotate applies rotation around the X, Y coordinate specified by the user. If no coordinate is specified, the rotation occurs around 0, 0 coordinate of the drawing. Therefore if we want the text rotated around its own bottom-left corner, we have to call rotate(degs x_coord0 y_coord0). The ability to add these two coordinates makes dealing with rotate much more intuitive than scale, and we can place rotate() in any order relative to the other 2 transformations without having to change these parameters (assuming we’re rotating around bottom-left corner).

Putting it all together (rotation/scale around text center)

Rotation around the origin is pretty easy. But if we need to rotate the text around the center, we’ll still need some magic discovered in the scale section. First, we’ll need to know the text dimensions. The height is font-size*y_scale/2 (the actual font size accounts for whitespace below and above the letters used for letters like “f” and “g”, other letters like “o” only use 1/2 font height). The width can be obtained via HTML5Canvas measureText() method and will also need to be multiplied by x_scale. Since translate() centered the drawing at bottom-left corner of the text, the midpoint will be subject to the same position distortion due to scale() as everything else. Which means that if we just apply width/2 and -height/2 offsets, we’re essentially applying width/2*current_x_scale and -height/2*current_y_scale (where the scale parameters are either 1 if rotation occurs before scale() transformation or the same as specified by scale if it occurs after). So we’d need to divide by that scale parameter. The following two statements would produce equivalent result (remember that text_width and text_height correspond to the text AFTER it has been scaled, even if rotate transform is before scale()):

<text x="x_coord0" y="y_coord0" transform="rotate(degs, x_coord0+text_width/2, 
y_coord0-text_height/2) translate(-x_coord0*(x_scale-1), -y_coord0*(y_scale-1))
scale(x_scale, y_scale)">

<text x="x_coord0" y="y_coord0" transform="translate(-x_coord0*(x_scale-1), 
-y_coord0*(y_scale-1)) scale(x_scale, y_scale) rotate(degs,
x_coord0+text_width/(2*x_scale), y_coord0-text_height/(2*y_scale))">

Now what if in addition to applying rotation around the center, we also wanted the scale to apply around the center? The logic is the same, problem is that logic messes up rotation() since we’d need to apply it to translate(). Let’s ignore rotation for now, we apply same offsets as follows into each of the statements from the scale() section:

<text x="x_coord0" y="y_coord0" transform="translate(
-(x_coord0*(x_scale-1)+text_width/2), -(y_coord0*(y_scale-1)-text_height/2))
scale(x_scale, y_scale)">

<text x="x_coord0" y="y_coord0" transform="scale(x_scale, y_scale) translate(

Now the text is scaled around the center. Problem is that translate moved our drawing origin, so the rotate() offset we calculated for the midpoint is no longer valid. The good news is that only the first of the 2 rotate statements above would be affected, since in the second statement rotate() gets applied after scale() it already factors in the transformation from scale. To fix the first statement all we need to do is divide the text_width and text_height used in rotation by their scale factors. If we apply this logic to the two earlier rotation statements we get the following (notice that the 2nd statement hasn’t changed, also notice that rotate() statement is now identical between the two):

<text x="x_coord0" y="y_coord0" transform="rotate(degs, 
x_coord0+text_width/(2*x_scale), y_coord0-text_height/(2*y_scale)) translate(
-(x_coord0*(x_scale-1)+text_width/2), -(y_coord0*(y_scale-1)-text_height/2))
scale(x_scale, y_scale)">

<text x="x_coord0" y="y_coord0" transform="translate(
-(x_coord0*(x_scale-1)+text_width/2), -(y_coord0*(y_scale-1)-text_height/2))
scale(x_scale, y_scale) rotate(degs, x_coord0+text_width/(2*x_scale),

If you do the math (or just play with some numbers), you’ll notice that the two statements in rotate also can be represented as -translate_input/(scale-1). So, if we wanted to create an SVG tag for text we drew using HTML5Canvas in Pyjamas, we’d write something as follows:

def textToSVG(x, y, rotation, scale_x, scale_y, textString, canvas):
    """Creates an SVG tag for textString located at x, y on the canvas scaled
    horizontally by a factor of scale_x, vertically by a factor of scale_y
    around the center and rotated by rotation degrees around the center"""

    transform_tag = ""
    scaled = False
    width = canvas.measureText(textString)
    font = canvas.getFont().split('px ') #assumes font-size in pixels
    height = float(font[0])
    if scale_x != 1:
        scale_diff_x = x_scale-1
        offset_x = x*scale_diff_x+width/2
        rotate_offset_x = offset_x/scale_diff_x
        scaled = True
    if scale_y != 1:
        scale_diff_y = y_scale-1
        offset_y = y*scale_diff_y-height/4   #accounts for whitespace
        rotate_offset_y = offset_y/scale_diff_y
        scaled = True
    if scaled or rotation:
        if scaled:
            tranform_tag = strcat("translate(-", str(offset_x), " -",
                                  str(offset_y), ") scale(", str(scale_x),
                                  " ", str(scale_y), ") ")
        if rotation:
            tranform_tag = strcat(svg, "rotate(", str(rotation), " ",
                                  str(rotate_offset_x), " ",
                                  str(rotate_offset_y), ")")
        transform_tag = strcat(" transform=\"", transform_tag, "\"")
    return strcat("<text x=\"", str(x), "\" y=\"", str(y),
                  "\" fill=\"black\" font-size=\"", font[0],
                  "\" font-family=\"", font[1], "\"", transform_tag,
                  ">", textString, "</text>")

strcat() is my implementation of string concatenation for Pyjamas, which I explained in an earlier blog post. The above function can quickly generate an SVG text tag with any desired transformation, try it out. There is also a matrix() transformation, which can combine the 3 transformations described above into a single operation. I will not cover it in this post since it’s already getting too big, but I might describe it at a later date.

This entry was posted in How To and tagged , , by Alexander Tsepkov. Bookmark the permalink.

About Alexander Tsepkov

Founder and CEO of Pyjeon. He started out with C++, but switched to Python as his main programming language due to its clean syntax and productivity. He often uses other languages for his work as well, such as JavaScript, Perl, and RapydScript. His posts tend to cover user experience, design considerations, languages, web development, Linux environment, as well as challenges of running a start-up.

3 thoughts on “SVG text transform (translate, scale, rotate)

  1. David, that’s exactly how I’d do it when rendering existing SVG on a page. The problem here is that the data is stored in A JavaScript container for being represented on canvas, and I need to convert it to svg stored offline. I can’t rely on CSS until it’s already ported to SVG, and by that time I already know the new coordinates.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>