11

community.

I know that there are many answers here, manuals, tutorials and references over the internets and amny more about this question. Also I know that knowledge of linear algebra is required. But when I think about time to figuring out all the theory and solving exercises in practice - my head is blowing off and I can't do the simplest things :(

Please, if you know a little fast solution how to make rotation of text over its center before rendering it - tell me, pleeease.

For now I have:

#...
cr.move_to(*text_center)
myX, myY = text_center[0] - (width / 2), text_center[1] + (height / 2)

cr.save()
cr.translate(myX, myY)
cr.rotate(radians(text_angle))
cr.show_text(letter)
cr.restore()
#...

But my letter isn't rotating around itself. It's just like falling down to the right side :( I know that my code isn't right. Maybe I miss transformation but I don't know how to make it right.

UPDATE: Unfortunately, text are not affected by translations, so

cr.translate(10000, 10000)
cr.rotate(radians(15))
cr.show_text("hello")

will be exactly the same as

cr.rotate(radians(15))
cr.show_text("hello")

And I don't know how to make text rotation over its center without making new surface or something (like new layer in graphic processor) :(

Крайст
  • 776
  • 1
  • 9
  • 22

4 Answers4

14

At least on the version of cairo available on my machine (1.8.8), the following approach works for me:

def text(ctx, string, pos, theta = 0.0, face = 'Georgia', font_size = 18):
    ctx.save()

    # build up an appropriate font
    ctx.select_font_face(face , cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
    ctx.set_font_size(font_size)
    fascent, fdescent, fheight, fxadvance, fyadvance = ctx.font_extents()
    x_off, y_off, tw, th = ctx.text_extents(string)[:4]
    nx = -tw/2.0
    ny = fheight/2

    ctx.translate(pos[0], pos[1])
    ctx.rotate(theta)
    ctx.translate(nx, ny)
    ctx.move_to(0,0)
    ctx.show_text(string)
    ctx.restore()

Which can be used in the following way:

width = 500
height = 500
surface = cairo.ImageSurface(cairo.FORMAT_RGB24, width, height)
ctx = cairo.Context(surface)
ctx.set_source_rgb(1,1,1)
rect(ctx, (0,0), (width, height), stroke=False)
ctx.set_source_rgb(0,0,0)
for i in xrange(5):
    for j in xrange(5):
        x = 100 * i + 20
        y = 100 * j + 20
        theta = math.pi*0.25*(5*i+j)
        text(ctx, 'hello world', (x, y), theta, font_size=15)
surface.write_to_png('text-demo.png')

text-demo.png

Andrew Walker
  • 40,984
  • 8
  • 62
  • 84
  • Genius and perfect! Oh, since I asked that question I freezed my project and opened math books till now. I know that your solution is what I wanted and I'll simply paste it my code if you don't mind and will write your name in comments. But math is great and interesting, so I'll continue learning something new. Can you tell me where to find your solution in math books: how it names and what is it? Thank you for your help. – Крайст Jun 27 '13 at 23:31
  • 1
    @Schollii was correct, but it turns out that there are some intricacies of the Cairo API that make this a bit interesting. The key point is that rotations always happen about the origin. This means you need to translate the origin to where you want to draw the text. Then you can do your rotation. Finally, to make sure that the rendered text passes through your point, you need to do one final translation (in the rotated co-ordinate frame). The move_to command ensures the text will then be rendered from the origin. – Andrew Walker Jun 28 '13 at 02:45
  • 1
    This kind of mathematics can probably be best described using the term [Transformation Matrices](https://en.wikipedia.org/wiki/Transformation_matrix), which are really just a way to express co-ordinate transforms. In a very broad sense this is a linear algebra problem. This task is particularly important in mechanics (which in turn is important for software developers involved in physics, robotics and game development). – Andrew Walker Jun 28 '13 at 02:51
  • Sure, Schollii was right but trigonometry functions in python lowers precision a lot. On the first sight your method much precise and maybe faster. I will use this code in web app and it's important for dynamic image rendering and floating point operations. Thank you one more time. Linear algebra is on my way :) – Крайст Jun 28 '13 at 21:27
  • Doesn't seem to work for me. My special case maybe that I'm using single characters only. Any ideas ? – blissweb May 15 '19 at 02:59
6

OK so cairo allows for text move_to and rotate. This means that what you want is to figure out (x,y) for move_to (T), such that when you rotate (R), the center point of your text is at your desired location, c=(cx,cy):

enter image description here

So you have to solve the equation Mv = c, where v is the text center relative to the text origin:

M = T*R

T = (1 0 x)
    (0 1 y)
    (0 0 1)

R =  (cos r    -sin r   0)
     (sin r     cos r   0)
     (0            0    1)

v = (w/2, h', 1)

c = (cx, cy, 1)

h' = h/2 - (h - y_bearing)

Sanity checks:

  • when r is 0 (no rotation), you get x=cx-w/2, y=cy-h', which you know is the correct answer
  • when r=-90 (text sideways, with "up" towards the right), you get what you expect, ie x = cx - h' and y = cy + w/2

For python code, you will have to rewrite the above equation so you end up with A*t=b, where t=(x,y), and you will compute t = inv(A)*b. Then, you will simply do

cr.move_to(x, y)
cr.rotate(r)
cr.show_text(yourtext)

Note that the coordinate system in cairo has +y going down so there will be a couple signs to fix, and maybe y_bearing is not correct, but you get the idea.

Oliver
  • 27,510
  • 9
  • 72
  • 103
  • "place text at text_center" - how can I make this? I only have separated function cr.move_to(x, y) and separated cr.show_text(text). When must I make move_to() - before translation and rotation or after and before placing text? – Крайст Dec 12 '11 at 10:34
  • my `text_center` means coordinates of origin's x, y to place text exactly to overlap preffered point by its (text) center (w/2, h/2). – Крайст Dec 12 '11 at 11:52
  • these codes isn't and will not working because rotation happens always around **current_point**. If I write: `cr.move_to(a, b); cr.rotate(rad); cr.show_text();` - it'll be rotated only around (a, b) and translation will not affect rotation. But I know that there is some formulas like [here][1] to calculate new coords where to move to. [1]: http://www.codeproject.com/KB/GDI/textrotation.aspx?display=PrintAll – Крайст Dec 13 '11 at 01:29
  • thank you very much for your expanded answer. I can't understand a few things in that but I'll try to figure it out cause it's very important to me to understand all of this in my own head =) Thank you so much! – Крайст Jan 07 '12 at 06:49
  • @Крайст it would be nice if you could let everyone know if this is the answer (mark as answer), or at least useful (vote up, with comment about what you find). – Oliver Jan 08 '12 at 01:59
  • please, can you explain your answer for newbie? I can only feel that it's the right answer or very helpful but my knowledge isn't enough to figure it out :( – Крайст Jan 08 '12 at 10:37
2

Class function based on above input with multi-line text support.

def text(self, text, x, y, rotation=0, fontName="Arial", fontSize=10, verticalPadding=0):

    rotation = rotation * math.pi / 180

    self.ctx.select_font_face(fontName, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
    self.ctx.set_font_size(fontSize)

    fascent, fdescent, fheight, fxadvance, fyadvance = self.ctx.font_extents()

    self.ctx.save()
    self.ctx.translate(x, y)
    self.ctx.rotate(rotation)

    lines = text.split("\n")

    for i in xrange(len(lines)):
        line = lines[i]
        xoff, yoff, textWidth, textHeight = self.ctx.text_extents(line)[:4]

        offx = -textWidth / 2.0
        offy = (fheight / 2.0) + (fheight + verticalPadding) * i

        self.ctx.move_to(offx, offy)
        self.ctx.show_text(line)

    self.ctx.restore()
nvd
  • 2,995
  • 28
  • 16
1

Should

myX, myY = text_center[0] + (height / 2), text_center[1] - (width / 2)

be

myX, myY = text_center[0] - (width / 2), text_center[1] + (height / 2)

?

That might explain why it's falling down to the right side.

Matthew Strawbridge
  • 19,940
  • 10
  • 72
  • 93
  • oh, please, it's my fault. I've wrote this code from my head on my phone. on real code it's quiet like in your example. but when I go home I'll check. you think that translating - is the only transformation that I need? – Крайст Dec 11 '11 at 19:59
  • this isn't error. result is the same. maybe something complex will fix that situation? maybe new transformation matrix or different code order? – Крайст Dec 11 '11 at 20:11