Going fractal


Also known as “adding epicycles”, referring to a specific variety of scope creep where each decision in the project design opens an entirely new layer of crap to deal with.

It started with the idea of using Cairo to create PDF files using modern fonts that don’t work well with PDF::API2’s aging codebase. That pulled in Font::FreeType, and together they were enough to convert one of my simple-but-useful scripts, and seemed like 90% of what I needed to create a drop-in replacement for PDF::API2::Lite.

Except that I had to create an extra save/restore state stack to handle the fact that PDF has both a stroke color and a fill color, and Cairo only has one.

Also, Cairo doesn’t support the standard 35 PDF fonts, which are the standard PostScript printer fonts, which are Times, Helvetica, Symbol, etc. In fact, unless you load font files from disk, you’re limited to specifying the CSS2 generic fonts (“serif”, “sans-serif”, “monospace”), and wondering what it will decide that means each time you run your scripts.

So I need to support and/or supply free alternatives, such as the ones from URW that are used with Ghostscript, and Adobe’s Source Han Serif and Sans. And, yes, even if you do have the real fonts Adobe supplies with Acrobat, they’ll end up embedded in the output; PDF output is really kind of a sideline for Cairo, and honestly, I don’t think the authors expect anyone to own commercial fonts.

That should be enough, right? Except that in all of the Cairo documentation, the text-display methods are referred to as the “toy” API, and if you want to really use fonts for more than basic “this un-kerned string goes on the page at x,y”, you need to use a real layout engine like Pango.

Which doesn’t let you load arbitrary font files from disk, blowing up all the scaffolding I’ve just built around the assumption that Cairo’s FreeType support is supposed to actually be used.

Pango doesn’t just layout text, it uses span tags to mark up a string with font family, weight, etc, and uses Fontconfig to figure out what font you really wanted, quickly falling back all the way to the most generic font it can find on your computer, which on my Mac is Verdana.

So now it’s not enough to download a set of metrics-compatible OpenType fonts, I also need to create custom XML config files that override whatever else may be installed on a machine, and install them in a location that fontconfig is guaranteed to process first. XML, because there’s nothing like a text-based config parser that, by definition, must abort when it detects even the tiniest error.

There is a way to tell Pango to tell fontconfig to load a single font file from disk, but those methods aren’t exposed in the Perl Pango module. Which means I’d have to dig into the guts of how you link C libraries into Perl modules, add all the necessary functions and a set of tests, submit a patch to that project, wait for a release, and then at last C-ko would be mine!

Spoiler: yeah, not gonna happen.

By the way, it looks like no one has ever published a non-trivial example of how to use the Cairo, Pango, or Font::FreeType Perl modules, separately or together. Even a lot of the test code is little more than “yup, that called the C API and returned something”. Saying “see the C library web site” isn’t terribly Perl-y, given that everyone in the open source world has basically decided that automatically-generated API dumps are all the documentation anyone should ever need.

Which created Yet Another Problem, because it seems no one has ever even used the create_for_stream() method to render a PDF into memory and save it at the end. Which I need in order to transparently emulate PDF::API2::Lite’s saveas() method.

Therefore I present what is apparently the world’s first working Perl script that uses a Cairo PDF stream:

#!/usr/bin/env perl

use Cairo;

my $DATA;
my $surface = Cairo::PdfSurface->create_for_stream(
	sub {
		my ($whatever, $data) = @_;
		$DATA .= $data;
	},
	"whatever", 320, 240,
);
my $pdf = Cairo::Context->create($surface);

$pdf->move_to(10, 48);
$pdf->set_font_size(32);
$pdf->show_text("Hamburgefontsiv");

$surface->flush;
$surface->finish;
open(Out,'>:raw', "test.pdf");
print Out $DATA;
close(Out);
exit 0;

(if you just use the create() method that requires a file name, then you don’t have to flush, finish, or save your output at all; it just happens when the script exits)

But the fun never stops! Would you believe that there is no user documentation for the lookup strings used by Fontconfig? Everyone just copies the examples from the ‘user’ documentation that tells you all about the XML config format and very little about exactly what properties and constants you can specify:

Fontconfig provides a textual representation for patterns that the
library can both accept and generate. The representation is in three
parts, first a list of family names, second a list of point sizes and
finally a list of additional properties:

  <families>-<point sizes>:<name1>=<values1>:<name2>=<values2>...

Values in a list are separated with commas. The name needn't include
either families or point sizes; they can be elided. In addition, there
are symbolic constants that simultaneously indicate both a name and a
value. Here are some examples:

  Name                            Meaning
  ----------------------------------------------------------
  Times-12                        12 point Times Roman
  Times-12:bold                   12 point Times Bold
  Courier:italic                  Courier Italic in the default size
  Monospace:matrix=1 .1 0 1       The users preferred monospace font
                                  with artificial obliquing

That’s all you get. Everywhere. The rest of the “user” documentation is about the zillions of XML config files that get parsed to decide what to do with these strings, which adds another half-dozen epicycles. The only complete documentation I can find on how these strings are parsed is the commentless source code in src/fcname.c. Maybe.

This is the kind of stuff that’s under the hood of desktop Linux, which is why everyone uses Windows and Mac.

Current Project Status

First script successfully converted using the PDF::API2::Lite compatibility methods. Well, except for the font-specific methods that I need to create a wrapper class for; I can get all the data I need from Cairo’s font_extents() and text_extents() methods, but those only return data for the current font at the current size, where the ones from PDF::API2 calculate them at load time for a size of 1 point. Not a huge issue, just another epicycle…

Update

New epicycle: fonts lie. The text_extents() methods do the right thing, but the Cairo documentation confesses that many of the elements returned by font_extents() are the font designers’ opinion on the correct vertical sizes of the font. This has always annoyed me, so as part of building my wrapper class, I’m going to actually render representative characters into a recording surface in order to determine the precise values of ascender(), descender(), capheight(), xheight(), and fontbbox(). Also latin_baseline(), which can be pretty goofy in some kanji fonts.


Comments via Isso

Markdown formatting and simple HTML accepted.

Sometimes you have to double-click to enter text in the form (interaction between Isso and Bootstrap?). Tab is more reliable.