Displaying text in Pycairo

By Martin McBride, 2018-04-14
Tags: text font font style text metrics alignment
Categories: pycairo


This article describes how to create text in Pycairo. You can find other topics in the main Pycairo article list.

Drawing a simple text string

Here is the code to create a simple text string in our user coordinates (see the previous article). The full code is here:

import cairo

WIDTH = 3
HEIGHT = 2
PIXEL_SCALE = 200

surface = cairo.ImageSurface(cairo.FORMAT_RGB24,
                             WIDTH*PIXEL_SCALE,
                             HEIGHT*PIXEL_SCALE)
ctx = cairo.Context(surface)
ctx.scale(PIXEL_SCALE, PIXEL_SCALE)

ctx.rectangle(0, 0, WIDTH, HEIGHT)
ctx.set_source_rgb(0.8, 0.8, 1)
ctx.fill()

# Drawing code
ctx.set_source_rgb(1, 0, 0)
ctx.set_font_size(0.25)
ctx.select_font_face("Arial",
                     cairo.FONT_SLANT_NORMAL,
                     cairo.FONT_WEIGHT_NORMAL)
ctx.move_to(0.5, 0.5)
ctx.show_text("Drawing text")
# End of drawing code

surface.write_to_png('text.png')    

Within the drawing code:

  • set_source_rgb sets the colour to red.
  • set_font_size sets the text size to 0.25 units. This gives the approximate text height (the overall page size is 2 by 3 units).
  • select_font_face selects an Arial font with default slant and weight (see below).
  • move_to sets the position for the text
  • show_text displays the text using the style, colour and position specified.

Here is the result:

line

The font size is specified in units (depending on the current scaling). If we assume that our units represent inches, the font height would be 0.25 inches. To convert this to points, you multiply it by 72 (a point is 1/72 of an inch), so our font would be 18 pt if we printed the image above at 3 by 2 inches.

Font styles

We will now change the drawing code to try some different font styles:

ctx.set_source_rgb(1, 0, 0)
ctx.set_font_size(0.25)

ctx.select_font_face("Arial",
                     cairo.FONT_SLANT_NORMAL,
                     cairo.FONT_WEIGHT_NORMAL)
ctx.move_to(0.5, 0.4)
ctx.show_text("Arial")

ctx.select_font_face("Arial",
                     cairo.FONT_SLANT_ITALIC,
                     cairo.FONT_WEIGHT_NORMAL)
ctx.move_to(0.5, 0.7)
ctx.show_text("Arial italic")

ctx.select_font_face("Arial",
                     cairo.FONT_SLANT_NORMAL,
                     cairo.FONT_WEIGHT_BOLD)
ctx.move_to(0.5, 1.0)
ctx.show_text("Arial bold")

ctx.select_font_face("Times",
                     cairo.FONT_SLANT_NORMAL,
                     cairo.FONT_WEIGHT_NORMAL)
ctx.move_to(0.5, 1.3)
ctx.show_text("Times")

ctx.select_font_face("Courier",
                     cairo.FONT_SLANT_NORMAL,
                     cairo.FONT_WEIGHT_NORMAL)
ctx.move_to(0.5, 1.6)
ctx.show_text("Courier")

We keep the size and colour the same, and display 5 lines of text with different settings:

  1. Normal Arial text.
  2. Arial italic, by setting the font face to use cairo.FONT_SLANT_ITALIC.
  3. Arial bold, by setting the font face to use cairo.FONT_WEIGHT_BOLD.
  4. Times font, by changing the font face.
  5. Courier font, by changing the font face.

In each case we add 0.3 to the y position in the move_to call, to move each line below the previous. Here is the result:

line

Text metrics

It is often useful to know how large the text will be when it is displayed on the page. This is easy to find using the text_extents function:

xbearing, ybearing, width, height, dx, dy = ctx.text_extents(s)

text_extents returns a tuple containing 6 values that describe the dimensions of the text. The dimensions are calculated using the selected font and current scaling factor.

In the code above the tuple is unpacked into 6 variables. We are only interested in the width and height for now. These specify the width and height of a rectangle than tightly contains all the pixels that will be marked when the text is displayed.

We will look at the other text metrics later in a later article.

Example - text alignment

Let's start by printing 3 lines of text:

xpos = 0.5
ctx.set_source_rgb(0, 0, 0)
ctx.set_font_size(0.20)
ctx.select_font_face("Arial",
                     cairo.FONT_SLANT_NORMAL,
                     cairo.FONT_WEIGHT_NORMAL)
ctx.move_to(xpos, 0.7)
ctx.show_text("Text string")
ctx.move_to(xpos, 1.0)
ctx.show_text("Longer text string")
ctx.move_to(xpos, 1.3)
ctx.show_text("Even longer text string")

This will display the text, left aligned (because each move_to has the same x coordinate, 0.5)

line

Now we are going to right align the text.

xpos = 2.5
ctx.set_source_rgb(0, 0, 0)
ctx.set_font_size(0.20)
ctx.select_font_face("Arial",
                     cairo.FONT_SLANT_NORMAL,
                     cairo.FONT_WEIGHT_NORMAL)
s = "Text string"
xbearing, ybearing, width, height, dx, dy = ctx.text_extents(s)
ctx.move_to(xpos - width, 0.7)
ctx.show_text(s)

s = "Longer text string"
xbearing, ybearing, width, height, dx, dy = ctx.text_extents(s)
ctx.move_to(xpos - width, 1.0)
ctx.show_text(s)

s = "Even longer text string"
xbearing, ybearing, width, height, dx, dy = ctx.text_extents(s)
ctx.move_to(xpos - width, 1.3)
ctx.show_text(s)

Notice that we have set xpos to 2.5. We will align the right edge of each string with that position.

For each of the three strings, we use text_extents to calculate the metrics, including the width of the string. We then set the x position to xpos - width in the move_to call. This means that each string has a different x position, that has been calculated to that its right hand edge is at x = 2.5. Here is the result:

line

If you wanted to centre align the text, you should use xpos - width/2 as the x position (setting xpos to something suitable, for instance 1.5)

Text paths

You can convert a text string to a path. Once you have the path you can fill it (that will look similar to just displaying text the normal way), or outline it, or both.

Here is how to outline some text:

ctx.set_source_rgb(1, 0, 0)
ctx.set_font_size(0.75)
ctx.select_font_face("Arial",
                     cairo.FONT_SLANT_NORMAL,
                     cairo.FONT_WEIGHT_NORMAL)
ctx.move_to(0.3, 1.5)
ctx.text_path("Outline")
ctx.set_line_width(0.02)
ctx.stroke()

All we have done here is call text_path instead of show_text. This adds the text letter shapes to the path. We then set the line width and stroke the path. We have made the text quite large so you can see the effect.

line

You can combine the text path with other shapes:

ctx.set_source_rgb(1, 0, 0)
ctx.set_font_size(0.75)
ctx.select_font_face("Arial",
                     cairo.FONT_SLANT_NORMAL,
                     cairo.FONT_WEIGHT_NORMAL)
ctx.rectangle(0.2, 0.8, 2.6, 1)
ctx.move_to(0.3, 1.5)
ctx.text_path("Outline")
ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
ctx.fill()

In this case, we have create a rectangle, with text inside it. The rectangle and text form part of the same path. When we fill it with the even-odd fill rule, we get text shaped holes in the rectangle!

line

If you found this article useful, you might be interested in the book NumPy Recipes or other books by the same author.

Join the PythonInformer Newsletter

Sign up using this form to receive an email when new content is added:

Popular tags

2d arrays abstract data type alignment and angle animation arc array arrays bar chart bar style behavioural pattern bezier curve built-in function callable object chain circle classes clipping close closure cmyk colour combinations comparison operator comprehension context context manager conversion count creational pattern data science data types decorator design pattern device space dictionary drawing duck typing efficiency ellipse else encryption enumerate fill filter font font style for loop formula function function composition function plot functools game development generativepy tutorial generator geometry gif global variable gradient greyscale higher order function hsl html image image processing imagesurface immutable object in operator index inner function input installing iter iterable iterator itertools join l system lambda function latex len lerp line line plot line style linear gradient linspace list list comprehension logical operator lru_cache magic method mandelbrot mandelbrot set map marker style matplotlib monad mutability named parameter numeric python numpy object open operator optimisation optional parameter or pandas partial application path pattern permutations pie chart pil pillow polygon pong positional parameter print product programming paradigms programming techniques pure function python standard library radial gradient range recipes rectangle recursion reduce regular polygon repeat rgb rotation roundrect scaling scatter plot scipy sector segment sequence setup shape singleton slice slicing sound spirograph sprite square str stream string stroke structural pattern subpath symmetric encryption template tex text text metrics tinkerbell fractal transform translation transparency triangle truthy value tuple turtle unpacking user space vectorisation webserver website while loop zip zip_longest