Vector graphics: Cairo and SVG#
Raster tools commit to a resolution; vector tools describe shapes and decide the pixels later, or never. This chapter covers the two vector routes in Perl. Cairo is a full anti-aliased 2D engine that rasterizes paths to a pixel surface (or to PDF and SVG): you draw with real-number coordinates and it works out the coverage. SVG generates SVG markup, a text description of shapes that a browser or renderer draws later at whatever size it likes. Use Cairo when you need pixels now with the quality of a proper rasterizer; use SVG when you want resolution-independent output as a file that other tools consume.
Cairo also returns in chapter 9: pperl ships a native reimplementation of it, so the same code runs with no CPAN Cairo installed, and that chapter shows how the native binding works. Here it is the ordinary CPAN module.
Cairo: paths and the drawing model#
Cairo’s model is a path built up from segments, then either stroked (the outline drawn) or filled (the interior painted). You set a source color, build a path with move_to and line_to and curves, and commit it. Colors are floats in 0 .. 1, not 0 .. 255.
use Cairo;
# an ARGB surface is a pixel buffer with alpha
my $surface = Cairo::ImageSurface->create('argb32', 200, 200);
my $cr = Cairo::Context->create($surface);
# paint a white background: set source, then paint fills everything
$cr->set_source_rgb(1, 1, 1);
$cr->paint;
# a stroked path
$cr->set_source_rgb(0, 0, 0);
$cr->set_line_width(3);
$cr->move_to(20, 20);
$cr->line_to(180, 20);
$cr->curve_to(180, 100, 20, 100, 20, 180); # a bezier curve
$cr->stroke;
# a filled, semi-transparent circle (arc angles are in radians)
$cr->arc(100, 100, 40, 0, 6.28318); # full circle = 2*pi
$cr->set_source_rgba(1, 0, 0, 0.5); # red at 50% alpha
$cr->fill;
$surface->write_to_png('output.png');
Three things to internalize. Coordinates are real numbers, so move_to(100.5, 47.25) is meaningful and the half-pixel offset from the foundations chapter is how you get crisp thin lines. Arc angles are in radians, not degrees, so a full circle is 2 * pi. And set_source_rgba’s alpha gives you the compositing for free: fill or stroke with alpha below 1 and Cairo blends over what is already there.
stroke and fill consume the current path. If you want to both fill and stroke the same shape, use fill_preserve (or stroke_preserve), which keeps the path for a second operation:
$cr->arc(100, 100, 40, 0, 6.28318);
$cr->set_source_rgb(0.4, 0.8, 1);
$cr->fill_preserve; # fill, but keep the path
$cr->set_source_rgb(0, 0, 0);
$cr->set_line_width(2);
$cr->stroke; # now outline the same circle
Premultiplied alpha#
The argb32 surface stores pixels with premultiplied alpha: each color channel is already multiplied by its alpha. This is invisible when you draw through Cairo’s API, but it matters the moment you read raw pixel bytes off the surface or feed Cairo’s buffer to another library, because the bytes are not the straight RGBA you might assume. When you cross that boundary, un-multiply first. Within Cairo’s own drawing it never comes up.
SVG: markup, not pixels#
SVG is pure Perl and does something different: it builds an XML document describing shapes, and produces text. Nothing is rasterized; the consumer (a browser, Inkscape, a PDF converter) draws it. That makes it perfect for output that must stay sharp at any zoom, or that other tools will process further.
use SVG;
my $svg = SVG->new(width => 200, height => 200);
# a group carries shared style; children inherit it
my $g = $svg->group(id => 'shapes',
style => { stroke => 'black', fill => 'none' });
$g->line(x1 => 0, y1 => 0, x2 => 200, y2 => 200);
$g->circle(cx => 100, cy => 100, r => 50, style => { fill => 'skyblue' });
$g->rect(x => 10, y => 10, width => 40, height => 40);
$g->path(d => 'M10 190 L100 100 L190 190 Z'); # a triangle via path data
$svg->text(x => 60, y => 110)->cdata('Hello');
print $svg->xmlify;
The style hash maps to SVG’s presentation attributes: stroke, fill, stroke-width, opacity, and the rest. A group (aliased g) with a style is how you avoid repeating attributes on every element; children inherit and can override. Text is attached with a chained cdata because the character content is a child node, not an attribute.
For a polygon from computed points, get_path assembles the points string for you:
my @x = (50, 150, 100);
my @y = (10, 10, 90);
my $points = $svg->get_path(x => \@x, y => \@y, -type => 'polygon');
$g->polygon(%$points, style => { fill => 'coral' });
xmlify (also spelled to_xml or render) returns the finished document as a string. Write it to a .svg file and it opens in any browser; hand it to a converter for PDF or PNG.
Cairo or SVG?#
You want … | Reach for |
|---|---|
Pixels now, anti-aliased, from real-number coordinates |
|
Vector output as a file for browsers or other tools |
|
To render the same drawing to PNG, PDF, and SVG from one API |
|
No compiled dependency at all (pure Perl) |
|
The two also compose: generate structure as SVG markup, then render it to pixels with a rasterizer when you need a bitmap. For applied vector output in the specific shape of charts and graphs, and for reading the metadata already inside image files, the next chapter covers the pragmatic bookends of raster and vector work.