Drawing a clock with Python and the Cairo graphics library
The Hour Numerals
In the last section, I warned that drawing the numerals on the clock would be complex since it involves text handling. On the one hand, drawing text can get very complicated: Some languages draw their characters differently depending on the order in which the characters appear in the text (Arabic letters often connect together much like cursive in Western languages); text might consist of words from a language written from left-to-right mixed with words from a language written from right-to-left; and some Asian languages are written vertically. For such serious layout of text, it is a good idea to use a library such as Pango [6] to ensure all such complications are dealt with properly.
However, the numerals on a clock face are much simpler to draw: They are language-neutral (all languages print Indo-Arabic numerals 1 through 12 the same way). Therefore, I can get away with using Cairo's built-in text handling capabilities [7], which are simplistic enough that they are marked as a "toy API."
The first step is to define a list (actually a Python tuple) of strings, each corresponding to one of the 12 hour markings on the clock face. The 12-hour mark is conventionally placed at the top of the clock, so that mark will appear first on the list; the rest of the list is in increasing numerical order starting with 1:
NUMERAL_STRINGS = ('12', '1', '2', '3', '4', '5', '6', '7', '8', '9','10', '11')
Of course, feel free to use any text strings you'd like, as long as there are 12 of them in the list and they are not too long; you might, for example, prefer Roman numerals over the Indo-Arabic ones I've chosen to use here. Figure 7 shows the clock with Indo-Arabic numerals.

Again, some distances need to be calculated relative to the size of the entire clock, so that all parts of the clock will scale properly. For a 300 pixel clock, I find a 10-pixel gap between the indicator mark lines and the numerals to be reasonable – the same distance I chose for the gap between the indicator marks and the clock bezel in the previous section. The numerals should be fairly large – they need to be easily readable – so I chose 30 pixels, or a fifth of the clock's radius, for the numerals:
marks_to_numeral_gap = clock_radius/15 numeral_size = clock_radius/5
The distance from the center of the clock to each numeral can be roughly calculated at this point, but the final position of each numeral has to be determined on a numeral-by-numeral basis, as the text for each hour marking may vary in height (some fonts display digits and many letters with descenders, among other inconsistencies):
numeral_distance = mark_end_distance - marks_to_numeral_gap
Next, Cairo needs to know the font and font size in which to subsequently draw text. Many clocks I have seen have the numerals drawn in a serif font; most serif fonts I've seen are quite thin, so I chose bold text to make the display a bit clearer:
context.select_font_face ('serif', cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD) context.set_font_size (numeral_size)
You may replace serif
with sans-serif
if you prefer, or even another generic font name such as monospace
or cursive
. Specific font names, such as Times New Roman
, also work here, though a fallback font will be used instead if the named font you choose is not installed. Also, you may change cairo.FontSlant.NORMAL
to a different setting, such as cairo.FontSlant.ITALIC
, but in practice, I've never seen a clock with such styled hour markings.
The next part of the program operates in a loop to encircle the clock, much like in the last section when I drew the indicator mark lines. The main difference in this part is that this loop only cycles 12 times, since there are only 12 hour marks, instead of 60 second marks. But like in the previous section, the angle at which each hour mark is to be placed is calculated for each iteration of the loop:
angle = 2*math.pi/12 * i x_multiplier = math.sin (angle) y_multiplier = -math.cos (angle)
For efficiency, I have precalculated the sine and cosine of the angle and stored them in temporary variables; they will be used multiple times in the following calculations.
Now I will use Cairo to find out how wide and tall the text comprising the hour marking will be when it is drawn on the image – without actually drawing the text anywhere yet. This is because I need to know exactly where to position the text, and that depends on the final dimensions of the text:
extents = context.text_extents (NUMERAL_STRINGS[i])
Calculating where to position the text relative to the center of the clock certainly looks complicated, but it really isn't; it just involves a fair amount of arithmetic:
text_x = \ numeral_distance * x_multiplier + \ extents.x_bearing * x_multiplier - \ extents.width / 2 * x_multiplier - \ extents.width / 2 text_y = \ numeral_distance * y_multiplier + \ extents.y_bearing * y_multiplier + \ extents.height / 2 * y_multiplier + \ extents.height / 2
numeral_distance
positions the text in the ballpark of where it needs to go. However, the text will vary in size from one numeral to another – 12 is definitely at least wider than a 1, and some fonts draw a 6 with a rounded part on the bottom, which they don't draw on a 2, for instance, so even the heights of the text strings are likely to vary. To properly center the text, the coordinates at which to place the text are adjusted by half the text's width (extents.width / 2
) and half the text's height ( extents.height / 2
). Sines and cosines range between negative 1 and positive 1, and Cairo needs to know where to position the top-left corner of the text, so I have to further subtract half the width (and height) from the coordinates. Finally, when Cairo draws the "extents" (dimensions) of the text, it tends to expect the coordinates to be relative to the bottom left corner of the text; the x_bearing
and y_bearing
contain adjustments to be made to the coordinates in order to obtain coordinates relative to where Cairo expects them to be.
Finally, draw the text at the proper position in the image:
context.move_to (text_x, text_y) context.show_text (NUMERAL_STRINGS[i]) context.fill ()
Using context.stroke ()
in lieu of context.fill ()
would produce an outline or "silhouette" of the characters in the text, instead of normal "solid" characters.
The Hour, Minute, and Second Hands
I have now walked you through the steps and code required to draw all of the components of a clock face – from the bezel and white backdrop to the markings for each second and the numerals denoting each hour. However, a clock is not complete without a set of hands to indicate the current time. The hands consist of nothing more than a few lines and an arc (a semi-circle to be exact). In this section, I will at last complete the clock program by adding the code to draw the hour, minute, and second hands according to the current time.
To find the current time in Python, I use the localtime ()
function, which requires importing the time
module built-in to Python:
import time current_time = time.localtime ()
localtime ()
returns a Python object which, among other members, contains the current hour (tm_hour
, ranging from 0 to 23, where 0 is midnight); the current minute (tm_min
, ranging from 0 to 59); and the current second (tm_sec
, usually ranging from 0 to 59, though very rarely it can reach 60 due to leap seconds). With these three values and some mathematics, I can calculate the coordinates of each component – line and arc – of each hand on the clock.
As with the other parts of the clock, I want the dimensions of the hands to scale along with the rest of the clock. For a 300 pixel clock, I find that hands with a width of about 8 pixels look good. I also like hour, minute, and second hands which, respectively, radiate 50, 75, and about 95 pixels out from the center of the clock. These dimensions are 1/20, 1/3, 1/2, and 5/8 of the clock's radius:
hand_width = clock_radius/20 hour_hand_length = clock_radius/3 minute_hand_length = clock_radius/2 second_hand_length = clock_radius*5/8
Once I know the current hour, minute and second, I need to calculate the angle at which to draw each of the hands. I convert the 24-hour time returned by localtime ()
to 12-hour time using a modulus of 12 (divide by 12 and find the remainder). From there, in theory, it should be simply a matter of converting the ranges of each of the values – 0 to 60 seconds, 0 to 60 minutes, and 0 to 12 hours – to fractions of 2*pi
:
second_hand_angle = 2*math.pi / 60 * current_time.tm_sec minute_hand_angle = 2*math.pi / 60 * current_time.tm_min hour_hand_angle = 2*math.pi / 12 * (current_time.tm_hour % 12)
However, a real analog clock usually connects all three hands to the same rotor, gearing down each hand in such a way that the hands move at different rates. This means that the hour hand only moves 1/12 of the circumference of the clock when the minute hand makes a full circle; but the hour hand moves continuously, albeit barely noticeably, every time the minute hand – and, for that matter, the second hand – moves. I'll do the same with this clock:
second_hand_angle = 2*math.pi / 60 * current_time.tm_sec minute_hand_angle = 2*math.pi / 60 * current_time.tm_min +second_hand_angle / 60 hour_hand_angle = 2*math.pi / 12 * (current_time.tm_hour % 12) +minute_hand_angle / 60
The code for drawing the hands is the same for each of the three hands – the only part that varies is the length of each hand (the hour hand is conventionally shorter than the other two hands at least). For this reason, I implemented the code for drawing a hand as a function, whose two parameters are the angle at which the hand should be drawn and the desired length of the hand:
def draw_hand (hand_angle, hand_length):
Inside the function, I calculate the coordinates for the points making up the hand. Each hand starts with a line as long as the desired hand length; the line is drawn half the hand's width from the center of the clock. The tip of the hand is drawn as an arc – a semi-circle to be exact – centered between the top of the first line of the hand and a second, mirror-image line to finish the hand:
x1 = hand_width/2 * -math.cos (hand_angle) y1 = hand_width/2 * -math.sin (hand_angle) x2 = x1 + hand_length * math.sin (hand_angle) y2 = y1 - hand_length * math.cos (hand_angle) xc = hand_length * math.sin (hand_angle) yc = hand_length * -math.cos (hand_angle)
Now it's time to actually draw the hand:
context.move_to (x1, y1) context.line_to (x2, y2) context.arc (xc, yc, hand_width/2, hand_angle - math.pi, hand_angle) context.line_to (-x1, -y1) context.close_path () context.fill ()
Returning to the main part of the program, I call the function for each hand to draw – first the hour, then the minute, and finally the second hand:
draw_hand (hour_hand_angle, hour_hand_length) draw_hand (minute_hand_angle, minute_hand_length) draw_hand (second_hand_angle, second_hand_length)
Finally, I draw a circle over the center of the clock, with a diameter equal to the width of each hand:
context.arc (0, 0, hand_width/2, 0, 2*math.pi) context.fill ()
Without the extra circle, the base of each hand would be drawn with flat lines, giving a less-than-eye-appealing "sharp" look to the hands (Figure 8). Compare Figure 8 to Figure 1, which does include the circle capping the rotor.

Listing 3 contains the complete code for the clock program. As already shown, Figure 1 illustrates the final result of the completed program. Obviously, the exact appearance of the clock will vary depending on the current time and your choice of fonts (for the numerals).
Listing 3
clock.py
001 #!/usr/bin/env python3 002 003 import math 004 import time 005 import cairo 006 007 def draw_hand (hand_angle, hand_length): 008 x1 = hand_width/2 * \ 009 -math.cos (hand_angle) 010 y1 = hand_width/2 * \ 011 -math.sin (hand_angle) 012 x2 = x1 + hand_length * \ 013 math.sin (hand_angle) 014 y2 = y1 - hand_length * \ 015 math.cos (hand_angle) 016 xc = hand_length * \ 017 math.sin (hand_angle) 018 yc = hand_length * \ 019 -math.cos (hand_angle) 020 021 context.move_to (x1, y1) 022 context.line_to (x2, y2) 023 context.arc (xc, yc, 024 hand_width/2, 025 hand_angle-math.pi, 026 hand_angle) 027 context.line_to (-x1, -y1) 028 context.close_path () 029 context.fill () 030 031 # Replace the following with values of 032 # your choosing: 033 IMAGE_WIDTH = 300 034 IMAGE_HEIGHT = 300 035 036 MARK_WIDTH = 1 037 MAJOR_MARK_WIDTH = 3 038 039 # You may change this to the 040 # corresponding Roman numerals ('XII', 041 # 'I', 'II', etc.) if you wish 042 NUMERAL_STRINGS = ('12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11') 043 044 clock_diameter = min (IMAGE_WIDTH, 045 IMAGE_HEIGHT) 046 clock_radius = clock_diameter/2 047 048 surface = cairo.ImageSurface (cairo.Format.ARGB32, IMAGE_WIDTH, IMAGE_HEIGHT) 049 context = cairo.Context (surface) 050 051 context.translate ((IMAGE_WIDTH-clock_diameter)/2, (IMAGE_HEIGHT-clock_diameter)/2) 052 053 # Draw the background of the clock face 054 context.set_source_rgb (1., 1., 1.) 055 context.arc (clock_radius, clock_radius, 056 clock_radius, 0, 2*math.pi) 057 context.fill () 058 059 # Draw the clock bezel 060 # (300 / 12 = 25 pixels) 061 bezel_width = clock_diameter/12 062 bezel_radius = clock_radius - bezel_width/2 063 064 context.set_source_rgb (0., 0., 0.) 065 context.set_line_width (bezel_width) 066 context.arc (clock_radius, clock_radius, 067 bezel_radius, 0, 2*math.pi) 068 context.stroke () 069 070 # Draw the circles enclosing the 071 # indicator marking lines 072 # (150 / 15 = 10 pixels) 073 bezel_to_marks_gap = clock_radius/15 074 # (150 / 10 = 15 pixels) 075 mark_length = clock_radius/10 076 mark_start_distance = clock_radius - bezel_width - bezel_to_marks_gap 077 mark_end_distance = mark_start_distance - mark_length 078 079 # From now on, make Cairo interpret all 080 # coordinates we pass to it as relative 081 # to the center of the clock 082 context.translate (clock_radius, 083 clock_radius) 084 085 context.set_line_width (MARK_WIDTH) 086 context.arc (0, 0, mark_start_distance, 087 0, 2*math.pi) 088 context.stroke () 089 context.arc (0, 0, mark_end_distance, 090 0, 2*math.pi) 091 context.stroke () 092 093 # Draw the indicator marks 094 for i in range (60): 095 angle = 2*math.pi/60 * i 096 097 if i % 5 == 0: 098 context.set_line_width (MAJOR_MARK_WIDTH) 099 else: 100 context.set_line_width (MARK_WIDTH) 101 102 context.move_to (mark_start_distance * math.sin (angle), mark_start_distance * -math.cos (angle)) 103 context.line_to (mark_end_distance * math.sin (angle), mark_end_distance * -math.cos (angle)) 104 context.stroke () 105 106 # Draw the numerals denoting each hour 107 # (150 / 15 = 10 pixels) 108 marks_to_numeral_gap = clock_radius/15 109 # (150 / 5 = 30 pixels) 110 numeral_size = clock_radius/5 111 numeral_distance = mark_end_distance - marks_to_numeral_gap 112 113 context.select_font_face ('serif', cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD) 114 context.set_font_size (numeral_size) 115 116 for i in range (12): 117 angle = 2*math.pi/12 * i 118 x_multiplier = math.sin (angle) 119 y_multiplier = -math.cos (angle) 120 121 extents = context.text_extents (NUMERAL_STRINGS[i]) 122 123 text_x = numeral_distance * x_multiplier + extents.x_bearing * x_multiplier - \ 124 extents.width / 2 * x_multiplier - extents.width / 2 125 126 text_y = numeral_distance * y_multiplier + extents.y_bearing * y_multiplier + \ 127 extents.height / 2 * y_multiplier + extents.height / 2 128 129 context.move_to (text_x, text_y) 130 context.show_text (NUMERAL_STRINGS[i]) 131 context.fill () 132 133 # Draw the hour, minute and second hands 134 # (150 / 20 = 7.5 pixels) 135 hand_width = clock_radius/20 136 137 # (150 / 3 = 50 pixels) 138 hour_hand_length = clock_radius/3 139 # (150 / 2 = 75 pixels) 140 minute_hand_length = clock_radius/2 141 # (150 * 5/8 = 93.75 pixels) 142 second_hand_length = clock_radius*5/8 143 144 current_time = time.localtime () 145 146 second_hand_angle = 2*math.pi / 60 * \ 147 current_time.tm_sec 148 minute_hand_angle = 2*math.pi / 60 * \ 149 current_time.tm_min + \ 150 second_hand_angle / 60 151 hour_hand_angle = 2*math.pi / 12 * \ 152 (current_time.tm_hour % 12) + \ 153 minute_hand_angle / 60 154 155 draw_hand (hour_hand_angle, 156 hour_hand_length) 157 draw_hand (minute_hand_angle, 158 minute_hand_length) 159 draw_hand (second_hand_angle, 160 second_hand_length) 161 162 # Draw the rotor cap in front of the 163 # base of the hands, to conceal the 164 # sharp edges of the lines which 165 # comprise the clock hands 166 context.arc (0, 0, hand_width/2, 167 0, 2*math.pi) 168 context.fill () 169 170 # Write the result to a PNG file 171 surface.flush () 172 surface.write_to_png ('clock.png')
Conclusion
In this article, I have shown you how to use the Cairo graphics library to draw computer graphics. I have demonstrated its use by walking you through all the steps required to draw an analog clock displaying the current time. This article only scratches the surface of what Cairo can do. For instance, you can draw Bezier curves, which are far more flexible in scope than arcs. Cairo can also draw shapes in colored gradients and other patterns more complex than solid colors. You also can save to other file formats in addition to PNG. I could have written the program to draw to SVG files instead. If you want to send output to a printer, look into Cairo's capability for producing output as PDF or PostScript. In a subsequent article, I will integrate the clock into an interactive graphical user interface program using the GTK [2] user interface library.
Infos
- The Cairo graphics library: https://cairographics.org/
- The GTK graphical user interface toolkit: https://www.gtk.org/
- A list of official Cairo documentation: https://cairographics.org/documentation/
- The official Cairo programmer's reference documentation: https://cairographics.org/manual/
- The X Window System: https://www.x.org/
- The Pango text layout and rendering library: https://www.pango.org/
- Documentation for Cairo's basic text rendering capabilities: https://cairographics.org/manual/cairo-text.html
« Previous 1 2
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

News
-
AerynOS Alpha Release Available
With a choice of several desktop environments, AerynOS 2025.08 is almost ready to be your next operating system.
-
AUR Repository Still Under DDoS Attack
Arch User Repository continues to be under a DDoS attack that has been going on for more than two weeks.
-
RingReaper Malware Poses Danger to Linux Systems
A new kind of malware exploits modern Linux kernels for I/O operations.
-
Happy Birthday, Linux
On August 25, Linux officially turns 34.
-
VirtualBox 7.2 Has Arrived
With early support for Linux kernel 6.17 and other new additions, VirtualBox 7.2 is a must-update for users.
-
Linux Mint 22.2 Beta Available for Testing
Some interesting new additions and improvements are coming to Linux Mint. Check out the Linux Mint 22.2 Beta to give it a test run.
-
Debian 13.0 Officially Released
After two years of development, the latest iteration of Debian is now available with plenty of under-the-hood improvements.
-
Upcoming Changes for MXLinux
MXLinux 25 has plenty in store to please all types of users.
-
A New Linux AI Assistant in Town
Newelle, a Linux AI assistant, works with different LLMs and includes document parsing and profiles.
-
Linux Kernel 6.16 Released with Minor Fixes
The latest Linux kernel doesn't really include any big-ticket features, just a lot of lines of code.