Pango and Cairo samples
Adapted from the archived GNOME Wiki page Projects/Vala/PangoCairoSample. The first program (pags) renders a text file to PNG pages. The second draws glyph metrics with GTK 4 instead of GTK 3.
Render text to PNG pages (pags)
This sample matches the wiki’s command-line tool: it reads a UTF-8 text file and lays it out with Pango, rasterizes with Cairo, and writes page-001.png, and so on.
vala
using Cairo;
using Pango;
public class Pags {
static void clear_page (Cairo.Context cr, int width, int height,
double margin, double top_margin, bool do_rotate) {
cr.identity_matrix ();
cr.set_source_rgb (1.0, 1.0, 1.0);
cr.rectangle (0, 0, width, height);
cr.fill ();
cr.set_source_rgb (0.0, 0.0, 0.0);
if (do_rotate) {
cr.translate (width, 0);
cr.rotate (Math.PI / 2);
}
cr.move_to (margin, top_margin);
}
const OptionEntry[] option_entries = {
{ "width", 'w', 0, OptionArg.INT, ref width, "Image width", "PIXELS" },
{ "height", 'h', 0, OptionArg.INT, ref height, "Image height", "PIXELS" },
{ "family", 'f', 0, OptionArg.STRING, ref font_family, "Font family", "NAME" },
{ "font-size", 's', 0, OptionArg.DOUBLE, ref font_size, "Font size", "VALUE" },
{ "margin", 'm', 0, OptionArg.DOUBLE, ref margin, "Margin", "VALUE" },
{ "justify", 'j', 0, OptionArg.NONE, ref do_justify, "Justify", null },
{ "rotate", 'r', 0, OptionArg.NONE, ref do_rotate, "Rotate", null },
{ "", 0, 0, OptionArg.FILENAME_ARRAY, ref filenames, null, "FILE" },
{ null }
};
static int width = 320;
static int height = 240;
static string font_family;
static double font_size;
static double margin;
static bool do_justify = false;
static bool do_rotate = false;
static string[] filenames;
static int main (string[] args) {
font_family = "Sans";
font_size = 14;
margin = font_size;
try {
var opt_context = new OptionContext ("- Render text files to image files");
opt_context.set_help_enabled (true);
opt_context.add_main_entries (option_entries, "pags");
opt_context.parse (ref args);
} catch (OptionError e) {
stderr.printf ("Option parsing failed: %s\n", e.message);
return -1;
}
double top_margin = margin * 2;
double bottom_margin = margin;
if (filenames == null) {
stdout.printf ("Need name of text file!\n");
return -1;
}
Pango.Rectangle ink_rect, logical_rect;
string text;
try {
FileUtils.get_contents (filenames[0], out text);
} catch (FileError e) {
stderr.printf ("Failed reading file %s!\n", filenames[0]);
return -1;
}
var surface = new Cairo.ImageSurface (Cairo.Format.RGB24, width, height);
var cr = new Cairo.Context (surface);
var font_description = new Pango.FontDescription ();
font_description.set_family (font_family);
font_description.set_size ((int) (font_size * Pango.SCALE));
var rwidth = width;
var rheight = height;
if (do_rotate) {
rwidth = height;
rheight = width;
}
var layout = Pango.cairo_create_layout (cr);
layout.set_font_description (font_description);
layout.set_justify (do_justify);
layout.set_width ((int) ((rwidth - 2 * margin) * Pango.SCALE));
layout.set_text (text, -1);
var pagenum_font_description = new Pango.FontDescription ();
pagenum_font_description.set_family ("Sans");
pagenum_font_description.set_size ((int) (9 * Pango.SCALE));
var pagenum_layout = Pango.cairo_create_layout (cr);
pagenum_layout.set_font_description (pagenum_font_description);
int page_num = 1;
double ybottom = rheight - top_margin - bottom_margin;
var iter = layout.get_iter ();
clear_page (cr, width, height, margin, top_margin, do_rotate);
/* Single-line layouts start on the last line; at_last_line() is true
* immediately, so a while (!at_last_line()) loop never ran. */
while (true) {
double y_pos = 0;
bool first_line = true;
bool page_overflow = false;
while (true) {
iter.get_line_extents (out ink_rect, out logical_rect);
var line = iter.get_line_readonly ();
if (ink_rect.width == 0) {
double dy = font_size / 2;
y_pos += dy;
if (!first_line) {
cr.rel_move_to (0, dy);
}
} else {
double xstart = 1.0 * logical_rect.x / Pango.SCALE;
cr.rel_move_to (xstart, 0);
Pango.cairo_show_layout_line (cr, line);
cr.rel_move_to (-xstart, (int) (logical_rect.height / Pango.SCALE));
y_pos += logical_rect.height / Pango.SCALE;
}
if (y_pos > ybottom) {
page_overflow = iter.next_line ();
break;
}
first_line = false;
if (!iter.next_line ()) {
break;
}
}
pagenum_layout.set_text (page_num.to_string (), -1);
pagenum_layout.get_extents (out ink_rect, out logical_rect);
cr.move_to (rwidth - logical_rect.width / Pango.SCALE, rheight - margin);
Pango.cairo_show_layout (cr, pagenum_layout);
surface.write_to_png ("page-%03d.png".printf (page_num));
page_num++;
if (!page_overflow) {
break;
}
cr.show_page ();
clear_page (cr, width, height, margin, top_margin, do_rotate);
}
return 0;
}
}Compile and run
Compile:
shell
valac --pkg cairo --pkg pango --pkg pangocairo pags.valaRun:
shell
./pags mychapter.txtExtent demo (GTK 4)
Draws Pango layout extents (line, run, cluster, or character) for debugging text layout. Edit r_name in main to switch between "line", "run", "cluster", and "char".
vala
using Gtk;
using Cairo;
using GLib;
using Pango;
public class ExtentDemoWindow : Gtk.ApplicationWindow {
public string font_family { get; set; default = "Sans"; }
public double font_size { get; set; default = 24; }
public string display_text { get; set; default = ""; }
public string r_name { get; set; default = "cluster"; }
public Pango.Alignment text_align { get; set; default = Pango.Alignment.LEFT; }
public bool justify_text { get; set; default = false; }
public int draw_width { get; set; default = 800; }
public int draw_height { get; set; default = 600; }
int x_margin = 5;
int y_margin = 5;
int x_offset = 0;
int y_offset = 25;
public ExtentDemoWindow (Gtk.Application app) {
Object (application: app, title: "Pango extents");
var da = new DrawingArea ();
da.set_size_request (draw_width, draw_height);
da.set_draw_func (on_draw);
child = da;
}
void on_draw (DrawingArea area, Cairo.Context ctx, int w, int h) {
var font_description = new Pango.FontDescription ();
font_description.set_family (font_family);
font_description.set_size ((int) (font_size * Pango.SCALE));
var playout = Pango.cairo_create_layout (ctx);
playout.set_font_description (font_description);
playout.set_markup (display_text, -1);
ctx.set_source_rgb (1, 1, 1);
ctx.paint ();
ctx.set_source_rgb (0, 0, 0);
ctx.translate (x_margin, y_margin);
ctx.translate (x_offset, y_offset);
playout.set_width ((int) (draw_width * Pango.SCALE));
playout.set_height ((int) (draw_height * Pango.SCALE));
playout.set_ellipsize (Pango.EllipsizeMode.END);
playout.set_alignment (text_align);
playout.set_justify (justify_text);
ctx.move_to (0, 0);
Pango.cairo_show_layout (ctx, playout);
ctx.set_source_rgba (0, 1, 0, 0.7);
ctx.set_line_width (1);
var li = playout.get_iter ();
for (bool more = true; more;) {
Pango.Rectangle ink_rect = Pango.Rectangle ();
Pango.Rectangle log_rect = Pango.Rectangle ();
if (r_name == "line") {
li.get_line_extents (out ink_rect, out log_rect);
} else if (r_name == "run") {
li.get_run_extents (out ink_rect, out log_rect);
} else if (r_name == "cluster") {
li.get_cluster_extents (out ink_rect, out log_rect);
} else if (r_name == "char") {
ink_rect = li.get_char_extents ();
log_rect = ink_rect;
}
var x = descale (ink_rect.x);
var y = descale (ink_rect.y);
var rw = descale (ink_rect.width);
var rh = descale (ink_rect.height);
ctx.rectangle (x + 0.5, y + 0.5, rw - 1.0, rh - 1.0);
ctx.stroke ();
if (r_name == "line") {
more = li.next_line ();
} else if (r_name == "run") {
more = li.next_run ();
} else if (r_name == "cluster") {
more = li.next_cluster ();
} else if (r_name == "char") {
more = li.next_char ();
} else {
more = false;
}
}
}
int descale (int i) {
return i / Pango.SCALE;
}
}
void present_extent_window (Gtk.Application app, string display_text) {
var win = new ExtentDemoWindow (app);
win.display_text = display_text;
win.r_name = "cluster";
win.text_align = Pango.Alignment.LEFT;
win.justify_text = false;
win.present ();
}
int main (string[] args) {
var app = new Gtk.Application ("org.example.ExtentDemo",
ApplicationFlags.HANDLES_OPEN | ApplicationFlags.DEFAULT_FLAGS);
string demo = "᾿Απ᾿ τὰ κόκκαλα βγαλμένη\nτῶν ῾Ελλήνων τὰ ἱερά\n" +
"καὶ σὰν πρῶτα ἀνδρειωμένη\n" +
"χαῖρε, ὦ χαῖρε, ᾿Ελευθεριά! \n" +
"well let's do some fishy things too.";
app.activate.connect (() => {
present_extent_window (app, demo);
});
app.open.connect ((files, hint) => {
string text = demo;
if (files.length > 0) {
try {
uint8[] raw;
string etag;
files[0].load_contents (null, out raw, out etag);
text = (string) raw;
} catch (Error e) {
stderr.printf ("Failed reading file: %s\n", e.message);
return;
}
if (!text.validate ()) {
stderr.printf ("invalid utf8 string\n");
return;
}
}
present_extent_window (app, text);
});
return app.run (args);
}Compile and run
Compile:
shell
valac --pkg gtk4 --pkg pangocairo extentdemo.valaRun:
shell
./extentdemoRun with file:
shell
./extentdemo myfile.txt