Menu

Creating Scalable Vector Graphics with Perl

July 11, 2001

Kip Hampton

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">

      &lt;japh/&gt;

    </text>

    <text style="fill:#FFFFFF">

      &lt;japh/&gt;

    </text>

  </g>

</svg>

If you view this document in an SVG-enabled browser or standalone SVG viewer, you'll see something like the following.

japh.png

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)");

Also in Perl and XML

Perl XML Quickstart: Convenience Modules

Perl XML Quickstart: The Standard XML Interfaces

Perl XML Quickstart: The Perl XML Interfaces

Using XML::Twig

High-Performance XML Parsing With SAX


Resources

Download the sample code

J. David Eisenberg's Introduction to Scalable Vector Graphics

W3C SVG Specification

The Batik SVG Toolkit

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.

browsers.png

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.