_ (_) ____ ____ _ ____ ___ / ___) _ | |/ ___) _ \ ( (__( ( | | | | | |_| | \____)_||_|_|_| \___/ Infuriating isn't it? You get all your tables columns lined up almost perfectly. Then you realize one needs adjusting. And now you're fighting with the GUI... You try to find the one-pixel-wide place to put the mouse to grab the line and move it. And then you move it and everything else is disturbed. So you have to carefully place each one as though you were building a house of cards. If only you could just tell the word processor the column proportions you want. Or you have to make a table of regular dates. Every Wednesday, say. So you sit there with the calendar open in the corner, looking down the Wednesday column and typing in each date. Next year you come to print a new copy, only to realize the dates are all different and you have to start again. But hold on, isn't this what computers are for? To do the work for us, not make us work. So why not tell the computer exactly what we want, instead of poking at it with the mouse and hoping it will work out what we meant? That's where Cairo comes in. It's a library for drawing vector graphics, and can create PDFs, SVGs, PS files, among others. ========================================================= 1. Setup ========================================================= We will be using C89, and our progam should compile with either clang or gcc. Make sure cairo is installed, obviously. Our example program will create a cleaning rota to be signed whenever the cleaning is completed. Let's create a makefile first to make things easier: --- makefile -------------------------------------------- PROG=rota SRCS=main.c MAN= PKGS=cairo CFLAGS += -Wall -Wextra -pedantic -std=c89 -O2 COPTS != pkg-config --cflags ${PKGS} LDADD != pkg-config --libs ${PKGS} .include <bsd.prog.mk> --------------------------------------------------------- This is using BSD make, as you can probably tell. If you're on linux the install 'bmake' to be able to use it, and run bmake instead of make. First of all let's create a blank A4 page; these are 210x297mm. cairo uses points as its main unit, so this needs converting, it turns out to be 595.28x841.89 points. The process is, a surface has to be created, and then a cairo handle is created against the surface. So in our example, the surface is a PDF, and then the handle is used to draw onto it. Unfortunately the cairo headers use some C99 but we can use pragmas to ignore it. --- main.c ---------------------------------------------- #pragma clang diagnostic push #pragma diagnostic ignored "-Wc99-extensions" #include <cairo.h> #include <cairo-pdf.h> #pragma clang diagnostic pop #define PAGE_WIDTH 595.28 #define PAGE_HEIGHT 841.89 int main(void) { cairo_surface_t *surface; cairo_t *cr; surface = cairo_pdf_surface_create("/tmp/test.pdf", PAGE_WIDTH, PAGE_HEIGHT); cr = cairo_create(surface); /* TODO: draw some stuff */ cairo_show_page(cr); cairo_destroy(cr); cairo_surface_destroy(surface); return 0; } --------------------------------------------------------- Compile and run this, and you've got a blank A4 PDF. Well it's a start... ========================================================= 2. The Project ========================================================= As mentioned before, the plan is to create a PDF of a cleaning rota, with a space to sign for each Wednesday of the month. From now on, only the drawing parts of the code will be mentioned, but these should go where the TODO comment is in the code. Let's start with a title. --------------------------------------------------------- /* set things up */ cairo_set_source_rgb(cr, 0, 0, 0); cairo_select_font_face(cr, "Linux Libertine", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); cairo_set_font_size(cr, 28.0); /* draw the title */ cairo_move_to(cr, 64.0, 64.0); cairo_show_text(cr, "Cleaning Rota"); --------------------------------------------------------- Excellent! But it would be nice if the title was centred. Fortunately cairo let's you find out how big some text would be before you draw it, and you can use that to position it. So instead of moving to 64.0, 64.0: --------------------------------------------------------- cairo_text_extents_t textents; cairo_text_extents(cr, "Cleaning Rota", &textents); cairo_move_to(cr, PAGE_WIDTH/2 - textents.width/2, 64 - textents.height/2); cairo_show_text(cr, "Cleaning Rota"); --------------------------------------------------------- It would, of course, be better to keep the title in a #define or variable to make the code less redundant. Anyway this centres the text horizontally, and places it such that the centre line of the text is at y-coordinate 64.0. The next thing is to start drawing our actual rota. We'll start with a border to spice things up (this is a joke). The stroke colour was already set to black. --------------------------------------------------------- cairo_rectangle(cr, 16, 64 + textents.height + 8, PAGE_WIDTH - 16, PAGE_HEIGHT - 64 - 8 - textents.height - 16); cairo_stroke(cr); --------------------------------------------------------- Now our PDF has a title and a rectangle. Exciting. =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 2.1 Date arithmetic =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= This section is not related to cairo and PDF generation as such, but as part of the project we need to do some date arithmetic. The cleaning rota is supposed to show all the Wednesdays, so the first thing is to find the first Wednesday of the month and year. We can use mktime to find out the weekday of a given date by filling in the tm_year, tm_mon and tm_mday values. The values are a little bit special, in that months are counted from 0, that is January is month 0. The year is based on 1900, so that year 2001 is 101. Similarly the weekday, tm_wday starts from 0, which represents Sunday. | Member | Interpretation | |---------|-------------------| | tm_year | Year - 1900 | | tm_mon | Month [0-11] | | tm_mday | Day [1-31] | | tm_wday | Day of week [0-6] | If we fill in the year and month, mktime will fill in the rest for us. The offset of the weekday from our desired day gives us the nearest matching weekday. If this ends up negative, the nearest day was in the previous month, so add 7 to the day of the month. --------------------------------------------------------- struct tm tm; memset(&tm, 0, sizeof(tm)); tm.tm_year = START_YEAR; tm.tm_mon = START_MONTH; tm.tm_mday = 1; mktime(&tm); /* find nearest Wednesday */ tm.tm_mday += (3 - tm.tm_wday); /* add 7 if we've underflowed into the previous month */ if (tm.tm_mday <= 0) tm.tm_mday += 7; ---------------------------------------------------------