Creating Scalable Vector Graphics with Perl
July 11, 2001
Introduction
Scalable Vector Graphics (SVG) is a compact XML language to describe two-dimensional
images. With SVG you can create extremely sophisticated images complete with paths,
layering, masks, opacity control, animation, scriptable interactivity, and a small
host of
other advanced features -- all using nothing more than your favorite text editor or
XML
tools. This month we talk about creating SVG documents quickly and simply using Perl
and
David Megginson's XML::Writer
module.
A complete overview of SVG is beyond the scope of this article; if you are new to SVG, and have not done so already, I highly recommend that you have a look at J. David Eisenberg's excellent Introduction to Scalable Vector Graphics before proceeding.
It's worth noting that SVG requires a special browser plug-in or standalone viewer to view the rendered markup as the intended image. Rather than requiring readers to download one of these tools in order to view the results of the code samples, I have rasterized and exported the generated SVG images into Portable Network Graphics (PNG) using a utility that ships with the Apache Software Foundation's Batik project. Do not be confused: the images you will see below are PNGs, but the SVG source for each example is still available with this month's sample code.
A Simple Example -- Just Another Perl (XML) Hacker
For our first example we will write a simple script that creates an SVG banner memorializing our commitment to Perl and XML.
use strict; use XML::Writer; my $image_height = 60; my $image_width = 200; my $writer = XML::Writer->new(); $writer->xmlDecl('UTF-8'); $writer->doctype('svg', '-//W3C//DTD SVG 20001102//EN', 'http://www.w3.org/TR/2000/CR-SVG-20001102/DTD/svg-20001102.dtd'); $writer->startTag('svg', height => $image_height, width => $image_width); $writer->emptyTag('rect', height => $image_height, width => $image_width, fill => '#005580'); $writer->startTag('g', id => 'mainGroup', transform => 'translate(24,42)', style => 'font-size:42;font-weight:bold;'); $writer->dataElement('desc', 'JAPH with an XML twist. Features a simple drop shadow.'); $writer->startTag('text', transform => 'translate(3, 3)', style => 'fill:#003955'); $writer->characters('<japh/>'); $writer->endTag('text'); $writer->startTag('text', style => 'fill:#FFFFFF'); $writer->characters('<japh/>'); $writer->endTag('text'); $writer->endTag('g'); $writer->endTag('svg'); $writer->end();
Running this script generates the following XML document:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20000303 Stylable//EN" "http://www.w3.org/TR/2000/03/WD-SVG-20000303/DTD/svg-20000303-stylable.dtd"> <svg height="60" width="200"> <rect height="60" width="200" fill="#005580" /> <g id="mainGroup" transform="translate(24,42)" style="font-size:42;font-weight:bold;"> <desc>JAPH with an XML twist. Features a simple drop shadow.</desc> <text transform="translate(3, 3)" style="fill:#003955"> <japh/> </text> <text style="fill:#FFFFFF"> <japh/> </text> </g> </svg>
If you view this document in an SVG-enabled browser or standalone SVG viewer, you'll see something like the following.
This amusing little twist on the traditional JAPH block will certainly not win any design awards; but, aesthetics aside, when you compare this method of scripted image creation to those provided by other modules (that often, themselves, depend on cranky and platform-specific libraries) the true power of combining Perl and SVG becomes obvious.
Let's move on to a more serious example.
Generating Dynamic SVG From Non-XML Data
There are many examples on the Web that illustrate how to generate SVG images from other data sources. Most, however, presume that the data in question is already marked up as XML and, in the real world, this is often not the case. Although it may be trivial (using Perl) to create XML documents based on the data at hand, unless those document are useful elsewhere this would constitute a needless extra step in our applications. As usual, Perl lets us cut right to the chase.
For our second and final example we will parse a Web host's user agent log and, based on the data extracted, generate an SVG pie chart that illustrates the browser preference among visitors to that site. In order to make the chart more meaningful, the generated image will also feature a color-coded legend detailing the names and number of requests made by each type of browser.
Our script begins by passing the sole command line argument, the location of the
user
agent log we wish to analyze, to Akira Hangai's Apache::ParseLog
. We then
extract a hash containing the browser data using the browser()
method, and then
we calculate the total number of browsers reported using Perl's built-in map
function.
use strict; use XML::Writer; use Apache::ParseLog; my $ua_log = $ARGV[0]; my $log = Apache::ParseLog->new(); $log = $log->config(agentlog => $ua_log); my $log_data = $log->getAgentLog; my %browsers = $log_data->browser(); my $total_browsers = 0; map {$total_browsers += $_ } values (%browsers);
We declare a few global variables that will help us create our graphic. We use globals
here
rather than hardcoding the values later in order to make the script a bit more flexible.
For
example, changing the value for $pie_radius
also alters the overall height of
the image and the placement of the chart's legend. Also, since we do not know the
how many
individual types and versions of browsers will be found while parsing the log, we
will also
adjust the image's height based on the number of keys in the %browsers
hash to
ensure that it is neither too long nor too short.
my $pi = '3.14159256'; my $pie_radius = '90'; my $pie_center_x = '150'; my $pie_center_y = '125'; my $wedge_rotation = '0'; my $legend_start = $pie_center_y + $pie_radius + 10; my $image_height = $legend_start + (20 * scalar (keys (%browsers))); # generate random hex colors for the wedges my @colors = map {join "", map { sprintf "%02x", rand(255) } (0..2) } (0..63);
With the initialization out of the way we can get down to the business of generating
our
image. We will start by creating a new XML::Writer
object, setting the XML
encoding pseudo-attribute to UTF-8, and adding the standard SVG Document Type
Definition,
my $writer = XML::Writer->new(); $writer->xmlDecl('UTF-8'); $writer->doctype('svg', '-//W3C//DTD SVG 20001102//EN', 'http://www.w3.org/TR/2000/CR-SVG-20001102/DTD/svg-20001102.dtd');
All SVG images must have an <svg> element as the root element. The SVG specification
provides developers with many attributes for this root element that can be used to
configure
how the image is rendered, but since our needs are modest we only set the height and
width.
While the width attribute is hardcoded, the height attribute is set to the value of
$image_height
, which we calculated earlier.
$writer->startTag('svg', height => $image_height, width => '300');
Next we add a simple heading to our image using SVG's <text> element. SVG shares many of the styling rules of Cascading Style Sheets, which is convenient for those familiar with CSS already.
$writer->startTag('text', x => '20', y => '20', style => 'font-size:14;font-weight:bold;fill:#000000'); $writer->characters('Browser Stats - ' . localtime(time)); $writer->endTag('text');
The last step before creating the wedges for our pie chart is to define a <g>
(group) element to use as a wrapper for pie wedges themselves. While this element
is
optional, it will greatly simplify the task of creating the individual wedges since
all
child elements in a group inherit the properties of that enclosing group (unless explicitly
overridden). That means that each wedge of our chart will begin at the coordinates
defined
by $pie_center_x
and $pie_center_y
through the use of the
transform attribute.
$writer->startTag('g', id => 'pieChart', transform => "translate($pie_center_x,$pie_center_y)");
To create the pie chart for our sample image all we need to do is loop over the elements
in the %browsers
hash, select a random hex color from the @colors
array, and draw the wedge that represents each browser's fractional percentage of
the total
number of hits. In the interest of clarity we will not descend into a detailed description
of how the size of each wedge is calculated, nor will we look at the SVG <path>
element's somewhat esoteric syntax for drawing free-form shapes. For our purposes
it is
enough to understand that each wedge is created in the context of the x and y coordinates
inherited from the parent <g> element and is rotated into it's proper place based
on
the value of the $wedge_rotation
variable. For a more detailed discussion of
the <path> element and its properties, please see the relevant parts of the SVG
specification.
my $i = 0; my %color_lookup = (); foreach my $browser (sortHashByValue(%browsers)) { my $do_arc = '0'; my $wedge_color = '#' . $colors[$i]; my $wedge = $browsers{$browser} / $total_browsers * 360; my $radians = $wedge * $pi / 180; my $ry = 0 - int($pie_radius * sin($radians)); my $rx = int($pie_radius * cos($radians)); $do_arc++ if $wedge > 180; $writer->startTag('g', id => "wedge_$browser", transform => "rotate(-$wedge_rotation)"); $writer->emptyTag('path', style => "fill:$wedge_color;", d => "M $pie_radius,0 A $pie_radius, $pie_radius 0 $do_arc 0 $rx,$ry L 0,0 z"); $writer->endTag('g'); $wedge_rotation += $wedge; $color_lookup{$browser} = $wedge_color; $i++; } $writer->endTag('g');
Next we create the legend for our chart. Once again we create a top-level <g>
context element and then loop through the elements of the %browser
hash. This
time through we create a nested group element containing a small rectangle (the <rect>
element) filled with corresponding color for each browser, and a <text> element
containing the browser's name and number of requests. These legend items are rendered
from
top to bottom in a single column 20 pixels apart by incrementing the
$legend_item_xoffset
variable and passing its value to the item via the
transform attribute's translate function.
my $legend_item_xoffset = 0; $writer->startTag('g', id => 'legendGroup', style => 'font-size:10;fill:#000000', transform => "translate(25, $legend_start)"); foreach my $browser (sortHashByValue(%browsers)) { $writer->startTag('g', id => "legend_item_$browser", transform => "translate(0, $legend_item_xoffset)"); $writer->emptyTag('rect', width => '10', height => '10', style => "fill:$color_lookup{$browser};"); $writer->startTag('text', transform => 'translate(15, 10)'); $writer->characters("$browser ($browsers{$browser})"); $writer->endTag('text'); $writer->endTag('g'); $legend_item_xoffset += 20; }
Finally we close the legend's group element and the root <svg> element, and we call
XML::Writer
's close
method which, by default, prints the
document to STDOUT
.
$writer->endTag('g'); $writer->endTag('svg'); $writer->end();
Passing the location of a sample referrer log to our script yields the following image.
This script is far from perfect. For example, it is mathematically possible for two or more browsers to be assigned the same random color. Also, in real world applications, it is often desirable to exclude Web spiders and other automated user agents from browser reports. The goal here, however, has been to illustrate just how easy it is to generate eye-catching images dynamically using nothing more than Perl and a few of its modules. Other features are left as an exercise for the reader.
Conclusions
While I rarely offer subjective value judgments about the technologies I discuss in this column, I'm going to make an exception in this case. SVG is astonishingly cool. The examples above barely scratch the surface of SVG's flexibility and communicative power. To brush it aside as just another way to make "pretty pictures" is to miss the point completely. The combination of Perl's XML tools and SVG provides a range of creative options that would be difficult to achieve by other means. I hope that you have been inspired to continue to investigate both SVG and Perl's ability to generate it.