Perl

PDF::Cairo unleashed!


On Github now, and up for review on PrePAN.

I made sure to install it and run the examples at least once on a different OS. 😁

Epicycles: poster-making


So, as I prepare to inflict PDF::Cairo on the world, I’ve been sensibilizing the documentation and adding example code. My latest effort was dusting off my 15-year-old Traveller sector map generator and cleaning up the hex-grid calculations to use my Box library and the regular-polygon method I recently added. Works great.

Then I had an idea for Yet Another Epicycle: tiling a large image across multiple pages. I did this by hand in the original sec2pdf script, splitting sector maps across 4 or 16 pages, but it would be cool to render the entire sector once and then simply ‘cut out’ the pieces I need for each page. It would also get rid of a lot of special-case code for handling things that are partially outside of the current page.

I tried it out by hand, and it worked great. I drew a big box on a recording surface that was 17x22, and replayed it four times onto 8.5x11 pages with different offsets. Not lightning fast, since you end up rendering the entire drawing N+1 times, but it worked.

For that very simple example.

Unfortunately, it looks like there’s internal state that doesn’t get reset completely, so that when I integrated it into the module and told it to make a really big hex grid, only the first replay was 100% successful. The rest were incomplete (one row/column of hexes, with no text labels). It didn’t matter what order I replayed them in.

Cairo apparently has scripting surfaces and proxy surfaces in recent versions, but no one has updated the Perl module to expose those, and I’m not sure they’d help (scripting surfaces are basically a debugging tool that writes to disk and gets replayed with a standalone CLI tool).

I still want to do it, so I may simply add a simple-minded recording of my API calls and do a high-level replay. Even more overhead than using a recording surface (especially for things like loading fonts and images), but then I’ll know it can be replayed more than once.

Meanwhile, the recording()/replay() methods work at least once, so I’m leaving them in for now.

Update!

Wow, that was an easy fix. After walking away for a few hours, I went back and added a single cairo_surface_flush() right before each set_source_surface() call. Renders perfectly, as many times as I want. See?; I just need to tweak the clipping regions a bit, to capture the overlap between pages.

Performance is actually pretty good, now that I have something to measure: it took only 28% longer to generate the drawing and replay it four times on different pages. Output size was 146% larger than generating the same drawing on a single large sheet, but that’s a small price to pay for the convenience.

If I were really going to revive sec2pdf (and I might, but not this weekend!), what I’d do to get the most efficient output is use a recording to draw all of the sector-wide elements (regional borders, X-boat routes, etc) and replay them onto the individual pages, after drawing each page’s grid but before adding text and system data. That would significantly reduce the amount of wasted drawing outside each page’s boundaries, and only repeat the elements that might appear on multiple pages.

PDF::Cairo example output


I’ve been adding example code to PDF::Cairo. Here’s the output from what I’ve got so far:

  • 2019 Progressive Calendar, a much-cleaner version of the script I wrote in March.

  • Box-slicing test, not actually useful for anything.

  • Image-loading test, ditto. (new! improved! fixed translate/rotate/scale order to get rid of skew)

  • Inkle drafting grid, planning tool for Inkle weaving. Reimplemented from here, using my Box library and polygons rather than my hand-tuned, impossible-to-debug component method. About 12x larger output, but Cairo isn’t a PDF library, it’s a library that can output PDF, so you don’t get as close to the metal.

  • Isometric drawing pad, mixes my Box library with a bit of trig to clean up the grid layout.

  • Kanji practice sheet, currently blank; I plan to add a second example that fills it in with selected characters in a pen font, grayed-out so you can draw on top of them. It’ll be much cleaner than the version I wrote ~14 years ago.

  • Sample Pango layout, showing the use of its markup. Now includes glyph outline support.

  • Japanese report paper.

  • Kyokuto-ish notepad sheets.

  • NEW! Glyph outlines, including manually processing the drawing operators.

  • New! Traveller hex grid. Unlike my 15-year-old sec2pdf script, which I threw together over a weekend when I discovered how terrible the existing Traveller mapping tools were, this one actually centers the grid correctly on the page. (seriously, before sec2pdf, the way to create a PDF Traveller map was to run a DOS program that spat out an import script for an obscure 2D free CAD program, then let it run for 10-20 minutes; these days it’s a lot better, although the border definition method I came up with is still in use)

Preliminary release of PDF::Cairo


Here. Needs a full test suite and a bunch of examples before I throw it up on Github/CPAN, but I’m pretty happy with the functionality.

I think if I were exploring another scripting language, I’d look for Cairo and FreeType support, and then I’d port this module over. A bit of work up front, but less hassle than dealing with the quirks and limitations of another implementation.

Stripped of comments and documentation, it’s just about 1,500 lines of code, half of which is in the main PDF::Cairo package. Quite a bit of it is PDF::API2 compatibility, which I could jettison when porting to another language, so it would be pretty quick.

Usage is quite straightforward:

use PDF::Cairo qw(cm);
$pdf = PDF::Cairo->new(
    file => "output.pdf",
    paper => "a4",
    landscape => 1,
);
$font = $pdf->loadfont('Times-Bold');
$pdf->move(cm(2), cm(4));
$pdf->fillcolor('red');
$pdf->setfont($font, 32);
$pdf->print("Hamburgefontsiv");
$image = $pdf->loadimage("logo.png");
$pdf->showimage($image, cm(5), cm(5),
    scale => 0.5, rotate => 45);
$pdf->write;

Honestly, the thing that took the longest was dredging up the schoolboy trig to get it to rotate images from the lower-left corner instead of the upper-left. Cairo’s inverted coordinate system makes sense on-screen, not so much in print.

I was very selective about what I pulled in from Pango. It’s a peculiar library that simultaneously gives you more control over text layout while removing control over font selection. You can precisely position each glyph in a Unicode string, but have no idea what it will look like on the page. Some of this it inherits from Fontconfig, which is a giant hairy mess of XML configuration files, but they’ve layered their own clunky parser on top of that.

For instance, on my Mac, I have four styles of the typeface family Hoefler Text installed: Regular, Italic, Black, and Black Italic. Using the Fontconfig tool fc-match, I can select them with the string ‘Hoefler Text’ optionally followed by ‘:bold’, ‘:italic’, or ‘:bold:italic’. Using Pango’s pango-list tool, I discover that the correct incantation there is to request ‘Hoefler Text Small-Caps’, with the words ‘Bold’ and ‘Italic’ optionally added in that order after ‘Text’.

Leave out the ‘Small-Caps’, and I get Verdana (well, technically I get Hoefler Text Ornaments, which then falls back to Verdana because that font doesn’t actually have any alphanumeric characters in it). Use it, and I get the right font, but without small caps. Try to request real small caps (which it has), using the variant='smallcaps' attribute, and I still don’t get small caps (despite the documentation, I’m not sure that works at all). I have no idea how to fix this, or if it’s even possible.

I made the Pango support optional, because it takes a full two seconds to initialize the first layout on my laptop. By comparison, my calendar generator takes less than a second to create a 10-year, 59-page calendar.

Obnoxious Bug!

Unfortunately, while I was porting one of my other old scripts to use as an example, libfreetype segfaulted in FT_Set_Transform. Didn’t matter what font I loaded, there was just something about that script that triggered it.

After some painstaking debugging, it looks like I have two workarounds. First, render the text as a Pango layout, which avoids calling Freetype directly from Cairo. Second, use the create_for_stream() method to initialize the PDF surface and explicitly save the file at the end, rather than use the standard create() method that takes a file name up front and auto-saves when the script exits. That last bit seems to be where the bug is being triggered.

Ironically, this is how my PDF::API2 compatibility methods work, so a direct port of the old script works fine, and it doesn’t crash until I try to update it to use what seemed to be the recommended method (pun intended).

For now, I’ve commented out the use of create(), and added an explicit write() method to dump the PDF file to disk. I’ll test again when Homebrew catches up to the current Cairo release.

5/17 Update

I’ve refreshed the tarball about a dozen times now, as I add examples and come across bugs and little niggling issues in the APIs. I’ve pretty much rewritten half of the Box module as I’ve started using it more aggressively in simplifying the calculations in my examples.

I’d say that I’m just about ready to stick a fork in it, write the test cases, and release, but then I thought of another epicycle: rendering SVG files as vectors. Currently, the only way to import existing graphical data is as fonts with FreeType or bitmapped images with Imagemagick’s convert tool.

I’ve used the custom font trick before to load illustrations into a program that didn’t support them, but I’d rather import SVG directly. Because ImageMagick supports rasterizing vector formats, I can already place an SVG into a PDF, but only as a bitmap.

But it turns out that librsvg is already designed to do exactly what I want: render an SVG into a Cairo context. There’s just not a maintained Perl module that links to it. I can’t say that I’ve always wanted to figure out how to write XS glue, but since my needs are very simple, I think I can dodge that bullet and create a simple API with SWIG.

Looking at the RSVG API, I think all I really need to do is:

  1. Pass a filename to rsvg_handle_new_from_file(), save the result.

  2. Pass that to rsvg_handle_render_cairo() along with a Cairo handle.

  3. Profit!

I’m not even sure I need rsvg_handle_free(); the memory should be reclaimed when the Perl variables go out of scope, or worst case, when the script exits.

Never mind…

It looks like SWIG is not going to be easier than XS. Oh, well.

PDF::Cairo lives!


After a few more epicycles (and a two-week vacation in Japan), I have about 95% compatibility with PDF::API2::Lite, covering pretty much everything I’ve used in my scripts over the years, so that they work with a two-line change. Along the way, I learned just enough about fontconfig to coax it into finding my real Adobe core fonts based on their legacy PostScript names; I’ll need to figure out the substitution crap for any system that only has free equivalents installed, of course, but that’s for when I clean it up and add a test suite before releasing to the world.

As a bonus, my most recent epicycle was calculating the exact values of ascender, descender, and xheight for non-exotic fonts, which is more useful than the font designer’s opinion when you want to precisely position the top edge of a string (as in, say, a calendar; I was able to completely rip out the unreliable hackwork from the PDF::API2::Lite version).

Currently missing: skew transformations (matrix math), elliptical arcs (matrix math), full Cairo path support (new feature), Pango layouts (major new feature), rendering JPG/PNG/GIF/etc (under-documented API calls…), and a PDF::API2 compatibility method for cjkfont (requires messy fc-match tinkering, and I’ve never used it…).

And I just thought of another epicycle! A lot of my grid-based PDF scripts make use of a little box-manipulation library I wrote years ago, that does things like ISO folds, centering, cropping, etc, and recently I started going over the API to clean it up and improve its functionality. Then I realized: I only use this for PDFs, and I’ve already copied the big paper-size library into PDF::Cairo, so why not just make it PDF::Cairo::Box? One-stop shopping, and it will make my calendar generator even cleaner!

5/10 Update

Basic test/install done for the packages; need a lot more test code before I can release. Revised Box module really cleans up the calendar script. Played with Pango just enough to learn that it’s fragile and quirky and expensive to load, but has potential when you need real text layout; couldn’t get vertical text out of it, though, and the feature request for furigana support has been open since 2005.

If I have any working brain cells tonight, I think I’ll work on tests for the matrix transforms to get skews and arcs working correctly. Maybe take a first crack at image methods.

Update

Well, that wasn’t so bad. Got skew transforms to work, and got PNG images to load, scale, and display correctly. I may punt and use ImageMagick’s convert CLI utility to handle other image formats. There is a Perl ImageMagick library, but I’ve never been able to get it to work correctly with a perlbrew-built Perl on a Mac, and just calling convert is easier.

5/12 Update

The last item missing for full PDF::API2::Lite compatibility is the stripped-down version of the textstart/textend functionality that I’ve never used. Adding the full API2 version of this would be more complicated and less useful than integrating Pango layouts, despite the latter’s significant startup time and entertaining quirks (such as Pango’s two completely different integer units, both of which are referred to as “Pango units” in the documentation: 1/1024pt for general positioning, 1/10000em for vertical positioning of glyphs).

As a bonus, because PDF::API2 is mostly pure-Perl, PDF::Cairo is about five times faster, similar to the speedup I got the time I converted one of my scripts to use the abandoned Haru library.

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.

Fun with PDFs…


While waiting on a fix for my Perl PDF font woes, I started thinking about some of the other long-standing issues I’ve had with PDF::API2. I actually use PDF::API2::Lite most of the time, with a few extra functions (like clip) merged back in, because I don’t need color spaces, bar codes, metadata, outlines, etc, etc. I just need all of the drawing functions and decent font handling, including proper subsetting in the output file.

Which it doesn’t do. Pro tip: the easiest way to clean up a PDF file full of cruft like giant embedded fonts is to run it through the ps2pdf utility supplied with Ghostscript. Despite the name, it works just fine as a pdf-to-pdf converter and optimizer.

Bottom line, PDF::API2 has been getting staler every year, and the font thing encouraged me to look elsewhere. Phil Perry is taking a stab at it with his fork PDF::Builder, but he’s still feeling his way around, and it’s a big job.

But I’ve written a lot of code over the years, and the PDF APIs I’ve looked at in other languages are either convoluted, limited in function (report writers and form fillers, mostly), just plain missing, and in at least one case commercial, and that generally applies to the other modules I rely on, too, like the Swiss Army Knife of date handling, DateTime.

By happy accident, I discovered that the Cairo library will generate PDF files, and integrates nicely with FreeType for robust font support, and they both have decent Perl modules. Being C libraries, they also have some performance advantages, but the most important thing they have is support.

Documentation and examples are pretty limited for the Perl modules, but I just finished successfully converting my calendar generator to use them, and it was only moderately annoying. The two biggest issues were that Cairo thinks (0,0) is the upper left corner, and that it only tracks one color in its graphics state, compared to PDF’s separate stroke and fill colors. Shimming around these problems added about 55 lines of code. Text positioning is improved thanks to Font::FreeType (and can be improved a lot more by exploiting the API fully), and output with embedded fonts is significantly smaller. Also, all my fonts work.

Now that I have a working example, I’ve started work on a wrapper module that works like PDF::API2::Lite, so that I can convert my old scripts by just editing a few lines at the top. Shouldn’t be too much work, and I’m tentatively calling it PDF::Cairo.

There’s also a Pango module for advanced text layout, but integrating that is definitely a “phase 2” item.

Update

As far as I can tell, Pango offers the exact opposite of what I want, turning font lookup into a game where the penalty for guessing wrong is having all your text rendered in Verdana.

OpenType font mapping woes…


It seems the font bug I ran into when making calendars has opened a can of worms. PDF::API2 is only lightly maintained these days, but there’s active development on the fork PDF::Builder, and the dev saw my bug report, cloned it into his project, and started poking around. Original module author Alfred Reibenschuh chimed in, and after some back-and-forth, it looks like the glyph-mapping code pretty much needs to be ripped out and rewritten from scratch.

I have a hunch I’ll be tasked with producing a CJK test case, which means I’ll need to collect some Chinese, Chinese, and Korean samples.

“Need a clue, take a clue,
 got a clue, leave a clue”