Image creation with PHP

First published at Tuesday, 11 December 2007

Warning: This blog post is more then 17 years old – read and use with care.

Image creation with PHP

There are several ways to create images or graphics with PHP. First there are some well known extensions, like ext/GD, or perhaps ext/ming everybody immediately remembers, when it comes to graphics generation. But there are several structural differences not in between the avaliable libraries, but also between the image formats you can create. This article series will give you some insight on the formats and libraries, and then shows how an abstraction layer could be build.

Introduction

General notes, relevant for this article.

Terms

Some terms I use in a special way.

Image
I use it as a generalization of pictures and graphics.
Picture
Images with natural contents, like photos or drawings. Usually there are no or only few clear borders in those images.
Graphic
Computer generated graphics with technical illustrations or charts. They often contain clear borders.

Agenda

The second part describes the generation of shapes and the basic drawing context.

  • Formats

    Describes the advantages and drawbacks of the different extensions and the formats they can generate.

  • Simple shapes

    Describes the basic required data structure and the generation of a first simple shape.

  • Gradients

    Describes how you can add radial and linear gradients to your generated graphics with each of the backends.

  • Integrated bitmaps

    Integrating bitmaps with the backends.

  • Text rendering

    The basics of text rendering with all extensions.

  • Additional image tools

    With a small set of tools the automatic generation of images gets a lot simpler and allows you the construction of nice images using the existing APIs.

The code

I provide completely working code within this article, which will not be developed any further, because there are already existing packages, which try to provide such an abstraction layer, like Image_Canvas in PEAR. In the graph component from the eZ Components I personally develop very similar backends under the New BSD license, which will stay limited to the drawing methods we need for the graph component for now.

The complete source code can be downloaded here, or partially copied from the code examples. The source code is not provided under some OpenSource license, but stays under my personal copyright, like the article does. If you want to take the code and continue developing it, please send me a mail and we can discuss this.

The code is written for PHP 5.3, which can currently be compiled from the CVS and makes use of the namespaces features in 5.3.

To run the provided code, you need at least the following extensions installed:

  • GD (with PNG and FreeType 2 support)

  • cairo_wrapper 0.2.3-beta

  • Ming 0.3.0

  • The default extensions: DOM, SPL

The basic datastructures

First we need two very basic structs to wrap often used structures in a simple graphic abstraction layer. I use the term 'structure' here - even though PHP does not have a construct like this - for classes with only public properties, which will be set by the constructor and a very limited set of methods.

Colors

It is intuitive, that colors are required all over a graphic abstraction. In this introduction we only support the creation of colors from the known hex strings, for example often used in HTML and CSS. You may of course create more different static functions to construct the structure from other values. I choosed RGB(A) colors, because they are common between all of the above mentioned extensions and output formats.

namespace kn::Graphic; /** * Struct to wrap and unify colors for different backends. * * @version //autogen// * @author Kore Nordmann <kore@php.net> * @license GPLv3 */ class Color { /** * Red color value between 0 and 255. * * @var int */ public $red; /** * Green color value between 0 and 255. * * @var int */ public $green; /** * Blue color value between 0 and 255. * * @var int */ public $blue; /** * Alpha value. * * An integer value between 0 and 255, where 255 defines full transparency. * * @var int */ public $alpha; /** * Construct color from HEX string. * * @param string $hexString * @return void */ public function __construct( $hexString ) { $this->fromHex( $hexString ); } /** * Read color values from hex string. * * Read RGBA color values from a hex string commonly known from CSS and * similar definition languages. * * Allowed values are #FF0000 or #2e343612, where the first two digits * define the red value, the second two digits the green value, the third * two digits the blue value and the last two optional gigits the alpha * value - also known as transparency. * * @param string $hexString * @return void */ public function fromHex( $hexString ) { // Remove trailing # if ( $hexString[0] === '#' ) { $hexString = substr( $hexString, 1 ); } // Iterate over chunks and convert to integer $keys = array( 'red', 'green', 'blue', 'alpha' ); foreach ( str_split( $hexString, 2) as $nr => $hexValue ) { if ( isset( $keys[$nr] ) ) { $key = $keys[$nr]; $this->$key = hexdec( $hexValue ); } } // Set missing values to zero for ( ++$nr; $nr < count( $keys ); ++$nr ) { $key = $keys[$nr]; $this->$key = 0; } } }

In this implementation I define an alpha value of FF, or 255, as full transparency. The interpretation of alpha values as transparency or opacity varies from backend to backend. As described in the property comment I consider a value of 255 as full transparency, and the value will be mapped in the backend. This also shows how essential a color wrapper is, to be able to use all backends in the same way.

The method fromHex() is simple and unimportant enough to not be described in any further detail.

The way you may want to extend the class are methods to read other possible color descriptions, like converting color describing words or reading arrays of integer or float values. Another nice thing could be to wrap the access to the public variables and enforce type and value range checking. perhaps you also want to add conversion to other color formats like HSL and CMYK.

Coordinates

We will need the coordinates even more often than the colors. Each shape and transformation needs some base point and or sizes, which are defined as position in some coordinate system. We choose a cartesian coordinate system, because all the above mentioned formats and extensions uses such one. If you don't know any difference between coordinate systems, the one you know is the cartesian coordinate system.

namespace kn::Graphic; /** * Struct to wrap and unify coordinates in a two dimensional cartesian * coordinate system. * * @version //autogen// * @author Kore Nordmann <kore@php.net> * @license GPLv3 */ class Coordinate { /** * X coordinate in a cartesian coordinate system. * * @var float */ public $x; /** * Y coordinate in a cartesian coordinate system. * * @var float */ public $y; /** * Construct coordinate struct from coordinates. * * @param mixed $x * @param mixed $y * @return void */ public function __construct( $x, $y ) { $this->x = $x; $this->y = $y; } }

The struct only offers access to two public variables, $x and $y. This is very simple but will add some convenience during further development.

Creating the surface

First we start defining a base class for all of the graphic backends. This base class defines the methods signatures we want to use to interact with the backend.

namespace kn::Graphic; /** * Require basic structs */ require_once 'color.php'; require_once 'coordinate.php'; /** * Base class for all graphic backends. * * @version //autogen// * @author Kore Nordmann <kore@php.net> * @license GPLv3 */ abstract class Base { /** * Construct graphic from width and height * * @param int $width * @param int $height * @return void */ public function __construct( $width, $height ) { $this->width = (int) $width; $this->height = (int) $height; // Graphic format dependant intialisation $this->initialize(); } /** * Driver dependent backend initialisation. * * @return void */ abstract protected function initialize(); /** * Store generated file to disk. * * Stores the genrated file into the file of the specified filename. * * @param string $file */ abstract public function save( $file ); }

In the first step the base class only defines the constructor, which will call the abstract method initialize() for backend specific initializations. Additionally each backend should implement the method save(), to write the resulting graphic into a file.

We do not add a method display(), because this would encourage users not to cache generated graphics, but display them directly to the user, which may have a severe impact on load and speed of the application. If somebody really needs this, he still could add this.

So, let's start with the real implementations in the backends, besides the abstract stuff...

DOM & SVG

The backend of course extends from the base graphic backends class and - until now - only needs to implement the mentioned two methods.

namespace kn::Graphic; /** * Require base */ require_once 'base_1.php'; /** * SVG graphic backend * * @version //autogen// * @author Kore Nordmann <kore@php.net> * @license GPLv3 */ class Svg extends Base { /** * Complete dom tree * * @var DOMDocument */ protected $dom; /** * Root node for SVG graphic primitives. * * @var DOMElement */ protected $elements; /** * Root node for SVG definitions. * * @var DOMElement */ protected $defs; /** * Prefix for generated SVG elements * * @var string */ public $elementPrefix = ''; /** * Driver dependent backend initialisation. * * Ensures the existance of a valid DOM document * * @return void */ protected function initialize() { $this->dom = new ::DOMDocument( '1.0' ); $svg = $this->dom->createElementNS( 'http://www.w3.org/2000/svg', 'svg' ); $svg->setAttribute( 'width', '100%' ); $svg->setAttribute( 'height', '100%' ); $svg->setAttribute( 'viewbox', "0 0 {$this->width} {$this->height}" ); $svg->setAttribute( 'version', '1.0' ); $this->dom->appendChild( $svg ); $this->defs = $this->dom->createElement( 'defs' ); $this->defs = $svg->appendChild( $this->defs ); $this->elements = $this->dom->createElement( 'g' ); $this->elements->setAttribute( 'id', $this->elementPrefix . 'main' ); $this->elements = $svg->appendChild( $this->elements ); } /** * Store generated file to disk. * * Stores the genrated file into the file of the specified filename. * * @param string $file */ public function save( $file ) { $this->dom->save( $file ); } } $graphic = new Svg( 100, 100 ); $graphic->save( 'images/example_svg_01.svg' );
Your browser needs to support SVG to display this image. Empty SVG image

We introduce four new class properties, which will contain the complete DOM document and the root nodes, where the created elements will be added later. The graphic primitives, like paths, polygons or circles will be added to the $elements node, while gradients and similar definitions will be added to the $defs node. The custom element prefix ensures, that all nodes get a somehow unique ID. If you try to merge several document you should modify the used prefix.

The method initialize() creates the basic document in the correct namespace, of the version 1.0. You may define the size of the SVG in pixel in the attributes width="" and height="", but we set width and height to 100% and define the used size in the viewbox attribute, which will result in a SVG document, which will scaled by the client, if you use a different size definition there, for example in the XHTML <object> element.

After this the defs and elements root nodes are added to the SVG document.

A possible extension to this method would be to reuse and extend existing SVG documents. You will notice some (solveable) problems, during the later sections.

The method save just calls the DOM method saveXml() to store the generated XML into a file. Now you may create empty SVG graphics - isn't this amazing?

GD & PNG

For PNG images created with the GD extension we do the same as for SVG, only with some minor differences...

namespace kn::Graphic; /** * Require base */ require_once 'base_1.php'; /** * SVG graphic backend * * @version //autogen// * @author Kore Nordmann <kore@php.net> * @license GPLv3 */ class Gd extends Base { /** * GD image resource * * @var resource */ protected $image; /** * Supersampling factor. * * Describes the factor the size of the image is multiplied with, when * rendering. This factor enhances the output quality by antialiasing but * consumes lots of memory. Factors bigger then 2 usally do not make sense. * * @var int */ protected $supersampling; /** * Type of image to render. * * Supported image types are: * - IMG_PNG for PNG images * - IMG_JPEG for JPEG images. * * @var int */ public $type = IMG_PNG; /** * Construct graphic. * * Like the base class the method takes two parameters, width and height * defining the boundings of the generated graphic, and additionally and * optionally the supersampling factor. This factor enhances the output * quality by antialiasing but consumes lots of memory. Factors bigger then * 2 usally do not make sense. * * @param int $width * @param int $height * @return void */ public function __construct( $width, $height, $supersampling = 2 ) { $this->supersampling = $supersampling; parent::__construct( $width, $height ); } /** * Ensures the existance of a valid image ressource */ protected function initialize() { // Create bigger image respecting the supersampling factor $this->image = ::imagecreatetruecolor( $this->supersample( $this->width ), $this->supersample( $this->height ) ); // Default to a transparent white background $bgColor = ::imagecolorallocatealpha( $this->image, 255, 255, 255, 127 ); ::imagealphablending( $this->image, true ); ::imagesavealpha( $this->image, true ); ::imagefill( $this->image, 1, 1, $bgColor ); // Set line thickness to supersampling factor ::imagesetthickness( $this->image, $this->supersampling ); } /** * Supersamples a single coordinate value. * * @param float $value * @return float */ protected function supersample( $value ) { $mod = (int) floor( $this->supersampling / 2 ); return $value * $this->supersampling - $mod; } /** * Store generated image to disk * * @param string $file */ public function save( $file ) { if ( $this->supersampling === 1 ) { $destination = $this->image; } else { $destination = ::imagecreatetruecolor( $this->width, $this->height ); // Default to a transparent white background for destination image $bgColor = ::imagecolorallocatealpha( $destination, 255, 255, 255, 127 ); ::imagealphablending( $destination, true ); ::imagesavealpha( $destination, true ); ::imagefill( $destination, 1, 1, $bgColor ); // Merge with created image ::imagecopyresampled( $destination, $this->image, 0, 0, 0, 0, $this->width, $this->height, $this->supersample( $this->width ), $this->supersample( $this->height ) ); } // Render depending on the chosen output type switch ( $this->type ) { case IMG_PNG: ::imagepng( $destination, $file ); break; case IMG_JPEG: ::imagejpeg( $destination, $file, 100 ); break; default: throw new Exception( "Unknown output type '{$this->type}'." ); } } } $graphic = new Gd( 100, 100 ); $graphic->save( 'images/example_gd_01.png' );
Empty GD PNG imageEmpty GD PNG image

You can see three class properties. The first property contains the resource used by GD to perform all operations on the image, which will be created in the initialize method. The second contains the supersampling factor, while the third option defines the type of the image, which should be rendered. The supersampling factor describes the factor the size of the image is multiplied with, when rendering. This factor enhances the output quality by anti aliasing but consumes lots of memory. Factors bigger then 2 usually do not make sense.

In this class the constructor has been overloaded, to make it possible to define a custom supersampling factor. The initialize() method creates an image resource of the size defined by width and height, multiplied by the supersampling factor. The color stuff is necessary to ensure we really get an image with transparent background. The alpha blending defines, that additional layers of graphic primitives do not overwrite the stuff behind the new primitive, but smoothly blend over them - as far as you may talk about "smoothly" when talking about GD. The line thickness should also match the supersampling factor, otherwise they won't be recognizeable in the resulting image at the end. As you can see the alpha value in GD is also interpreted as transparency, but for PNG it can only accept values with a resolution of 0 and 127, so that we divide the provided value by 2.

The supersampling method just wraps the multiplication with the factor. The small modification places lines and edges at the right position of the image...

Finally the save method does a lot more, than in the SVG backend. It needs to resize the rendered image to the correct output size, if the supersampling factor does not equal (int) 1. In this case a new image with the correct dimension is created and the source is resampled to the correct size. We do not use imagecopyresize() here, because the result would look even more crappy, then an image without supersampling at all, but imagecopyresampled() which produces nice results, as you will see later.

As I wrote earlier in the section JPEG, it does not make any sense to use the JPEG format for graphics because of its compression algorithm. But there are still some people, who still want to use it.

Cairo & PNG

Let's do the same with etx/cairo. The installation of the required package cairo_wrapper is described in the above section about ext/cairo_wrapper.

namespace kn::Graphic; /** * Require base */ require_once 'base_1.php'; /** * Cairo graphic backend * * @version //autogen// * @author Kore Nordmann <kore@php.net> * @license GPLv3 */ class Cairo extends Base { /** * Surface for cairo * * @var resource */ protected $surface; /** * Current cairo context. * * @var resource */ protected $context; /** * Driver dependent backend initialisation. * * Creates the cairo surface and drawing context * * @return void */ protected function initialize() { $this->surface = ::cairo_image_surface_create( ::CAIRO_FORMAT_ARGB32, $this->width, $this->height ); $this->context = ::cairo_create( $this->surface ); ::cairo_set_line_width( $this->context, 1 ); } /** * Store generated file to disk. * * Stores the genrated file into the file of the specified filename. * * @param string $file */ public function save( $file ) { ::cairo_surface_write_to_png( $this->surface, $file ); } } $graphic = new Cairo( 100, 100 ); $graphic->save( 'images/example_cairo_01.png' );
Empty Cairo PNG imageEmpty Cairo PNG image

The code is obviously cleaner and shorter then the GD example, while it does exactly the same - it creates an empty image. In the method initialize() we create the surface, we draw on, and the context, which has the current drawing state, like foreground and background color. In the context we for now only explicitly set the line width.

In the save() method we just save the context to a png file .. there is nothing more you need. The supersampling stuff we did in the GD wrapper you get for free in cairo - even in a far better quality.

Ming & Flash

Creating the empty SWF file nearly takes the same effort. Creating the surface and store it to a file...

namespace kn::Graphic; /** * Require base */ require_once 'base_1.php'; /** * Flash graphic backend * * @version //autogen// * @author Kore Nordmann <kore@php.net> * @license GPLv3 */ class Flash extends Base { /** * Movie to draw on. * * @var SWFMovie */ protected $movie; /** * Driver dependent backend initialisation. * * Creates a new flash movie * * @return void */ protected function initialize() { ::ming_setscale( 1.0 ); $this->movie = new ::SWFMovie(); $this->movie->setDimension( $this->modifyCoordinate( $this->width ), $this->modifyCoordinate( $this->height ) ); $this->movie->setRate( 1 ); $this->movie->setBackground( 255, 255, 255 ); } /** * Modifies a coordinate value, as flash usally uses twips instead of * pixels for a higher solution, as it only accepts integer values. * * @param float $pointValue * @return float */ protected function modifyCoordinate( $coord ) { return $coord * 10; } /** * Store generated file to disk. * * Stores the genrated file into the file of the specified filename. You * may optionally provide a compression factor for the SWF file - it * defaults to 9, the highest possible compression. * * @param string $file * @param int $compression */ public function save( $file, $compression = 9 ) { $this->movie->save( $file, $compression ); } } $graphic = new Flash( 100, 100 ); $graphic->save( 'images/example_flash_01.swf' );
Your browser needs to support Flash contents to display this image. Empty Flash image

The surface we draw on is a object of the class SWFMovie - as mentioned above, it may contain more frames, then we are actually using, which does not matter here for now. Transparent backgrounds are not possible with ext/ming, so that we have to live with a plain white background. The method min_setscale() shows a "nice" behaviour of flash - it defines the numbers of "twips" used for each pixel. As all sizes in ext/ming are integers this also defines the maximum resolution - with 10 twips per pixel this should be enough for us. Flash internally uses a coordinate system based on twips. The transition between twips and pixels is encapsulated in the method modifyCoordinate(), which just multiplies the value by 10. Even the ming documentation says, that you can live without ming_setscale() and just pass pixel values, this is still not true for all functions. Text sizes and gradients still expect twips, so that the consequent usage of this method will make our lives easier.

The default transistion n flash is 1 pixel = 20 twips, but as the flash document has a maximum size of 32768 * 32768 twips, and a resolution of 10 steps per pixel is enough for us, we chose this as our default transistion.

The save() method gets one optional parameter compared to the already known save() methods - a compression setting. The SWF files may be compressed, and we default to the maximum possible compression here. Like with the examples before, we now can create an empty flash file.

A first shape: polygons

It is time to extend the base class, because now its only creating the surface to draw a first shape. We will draw polygons, as this seems the most generic shape. So first, another abstract method will be added to the base class:

/** * Draws a single, optionally filled, polygon. * * As the first parameter the polygon expects an array with knCoordinate * objects, a color for the polygon and the fill status, which defaults to * filled. * * @param array(Coordinate) $points * @param Color $color * @param boolean $filled */ abstract public function drawPolygon( array $points, Color $color, $filled = true );

Polygons are defined by an array of points (coordinates) and the inherent order defined by the array. The drawing method now should draw a line from each point to the next one defined in the array. The second parameter defines the color for the polygon, respectively its border. The third parameter is a boolean value and defines, weather the polygon should be filled or not. So let's implement this in the backend, starting with SVG again.

SVG

Now the implementation in the SVG backend:

/** * Get Style for SVG Element * * @param Color $color * @param boolean $filled */ protected function getStyle( Color $color, $filled ) { if ( $filled ) { return sprintf( 'fill: #%02x%02x%02x; fill-opacity: %.2f; stroke: none;', $color->red, $color->green, $color->blue, 1 - ( $color->alpha / 255 ) ); } else { return sprintf( 'fill: none; stroke: #%02x%02x%02x; stroke-width: 1; stroke-opacity: %.2f;', $color->red, $color->green, $color->blue, 1 - ( $color->alpha / 255 ) ); } } /** * Draws a single polygon * * @param array(knCoordinate) $points * @param Color $color * @param boolean $filled */ public function drawPolygon( array $points, Color $color, $filled = true ) { $lastPoint = end( $points ); $pointString = sprintf( ' M %.4F,%.4F', $lastPoint->x, $lastPoint->y ); foreach ( $points as $point ) { $pointString .= sprintf( ' L %.4F,%.4F', $point->x, $point->y ); } $pointString .= ' z '; $path = $this->dom->createElement( 'path' ); $path->setAttribute( 'd', $pointString ); $path->setAttribute( 'style', $this->getStyle( $color, $filled ) ); $path->setAttribute( 'id', $id = ( $this->elementPrefix . 'Polygon_' . ++$this->elementID ) ); $this->elements->appendChild( $path ); return $id; }
Your browser needs to support SVG to display this image. SVG polygons

You can see two new methods here, the actual drawPolygon() implementation and the helper function getStyle(). The latter function will be reused and extended several times during this article, so that we take a first look here.

The getStyle() method generates the style definitions from the given color and fill state. In SVG that means we need to return a string for the style attribute, which is quite similar to inline CSS definition in HTML, only the used values differ. Depending on the fill state we either return a string with the style definitions for the border or the fill color. Color definitions are again in the commonly known hex definition, starting with a #. The alpha value defining the transparency is used to define the opacity for the graphic primitive. Using this kind of definition, it is not possible to define a bordercolor and a fill color in one shape. As it is not supported by some of the other backends, you still could just add two identical shapes this does not really matter.

In the method drawPolygon() a path definition string is created. The coordinate value following the 'M' is to move the cursor to an absolute position - lowercase characters in SVG paths are for coordinate definitions relatively to the prior coordinate, while uppercase characters are absolute coordinates. The 'L' for all following coordinates means "line to", which means that a line is drawn between the previous and current coordinate. Finally the path is closed by a 'z'. The created string is assigned to the attribute d="" of a <path> element. After this the style for the polygon is assigned using the described method getStyle(). The created elements will be added to the group node, we created earlier in the initialize() method. At the end we also create a unique ID for the element and return this for further reference.

The order of graphic primitives in the XML document defines the drawing order in SVG, so that each added polygons will be drawn above the already existing polygons, as you can see from the image, which has been generated by the example code below.

$graphic = new Svg( 100, 100 ); $graphic->drawPolygon( array( new Coordinate( 10, 14 ), new Coordinate( 70, 23 ), new Coordinate( 40, 87 ), ), new Color( '#3465A4' ) ); $graphic->drawPolygon( array( new Coordinate( 90, 14 ), new Coordinate( 30, 23 ), new Coordinate( 60, 87 ), ), new Color( '#A00000' ), false ); $graphic->drawPolygon( array( new Coordinate( 20, 80 ), new Coordinate( 70, 45 ), new Coordinate( 90, 90 ), ), new Color( '#4E9A067F' ) ); $graphic->save( 'images/example_svg_02.svg' );

GD & PNG

And for the GD backend the polygon drawing is simpler then the initial creation of the surface.

/** * Allocates a color * * @param Color $color * @return int Color index */ protected function allocate( Color $color ) { if ( $color->alpha > 0 ) { return ::imagecolorallocatealpha( $this->image, $color->red, $color->green, $color->blue, $color->alpha / 2 ); } else { return ::imagecolorallocate( $this->image, $color->red, $color->green, $color->blue ); } } /** * Draws a single polygon * * @param array(knCoordinate) $points * @param Color $color * @param boolean $filled */ public function drawPolygon( array $points, Color $color, $filled = true ) { $drawColor = $this->allocate( $color ); // Create point array $pointArray = array(); foreach( $points as $point ) { $pointArray[] = $this->supersample( $point->x ); $pointArray[] = $this->supersample( $point->y ); } // Draw polygon if ( $filled ) { ::imagefilledpolygon( $this->image, $pointArray, count( $points ), $drawColor ); } else { ::imagepolygon( $this->image, $pointArray, count( $points ), $drawColor ); } return $points; }
GD polygonsGD polygons

Again, there are two new methods in this class, allocate() to get a pointer to a GD color, which actually is only the color index, and the drawPolygon() method we need to implement.

In the allocate() method two different GD methods are required to be called depending on the alpha value of the color. If the alpha value is bigger then 0, which means a transparent color, imagecolorallocatealpha() is used, which accepts a fourth parameter, the transparency. As mentioned earlier only 128 different alpha values are supported, so that we need to divide the value by 2.

The actual drawPolygon() implementation is really simple. First the point array for the polygon needs to be transformed, because GD expects an one dimensional array with the coordinate values directly following each others in the schema:

array( x1, y1, x2, y2, x3, y3, ... )

With this point array a GD method is called, depending on the fill state with the drawing color returned by the allocate() method. Finally we return the point array for further reference - why this may be relevant will reveal later.

GD polygons without supersamplingGD polygons without supersampling

I mentioned the supersampling earlier, we use to add at least some kind of anti aliasing to the images generated by GD. You can see the difference in the image on the right, when supersampling is disabled. For polygons or simple lines this is not that important, but as the generated image gets more complex the anti aliasing enhances the result more and more.

Cairo & PNG

The cairo example also looks quite similar to the two earlier ones - creating polygons is always easy.

/** * Set the style for current cairo context * * @param Color $color * @param boolean $filled */ protected function setStyle( Color $color, $filled ) { ::cairo_set_source_rgba( $this->context, $color->red / 255, $color->green / 255, $color->blue / 255, 1 - $color->alpha / 255 ); if ( $filled ) { ::cairo_fill_preserve( $this->context ); } } /** * Draws a single polygon * * @param array(knCoordinate) $points * @param Color $color * @param boolean $filled */ public function drawPolygon( array $points, Color $color, $filled = true ) { $path = ::cairo_new_path( $this->context ); $lastPoint = end( $points ); ::cairo_move_to( $this->context, $lastPoint->x, $lastPoint->y ); foreach ( $points as $point ) { ::cairo_line_to( $this->context, $point->x, $point->y ); } ::cairo_close_path( $this->context ); $this->setStyle( $color, $filled ); ::cairo_stroke( $this->context ); }
Cairo polygonsCairo polygons

And again there are two new methods, one to set the current style using the method setStyle(), and the actual polygon rendering method, we need to implement drawPolygon().

As described earlier, the drawing context in $context defines the current colors and similar values we use to draw any stuff in cairo. Depending on the passed values the color is set using the function cairo_set_source_rgba(), which accepts float values between 0 and 1, so that we divide all values by 255. And cairo defines the alpha value as opacity instead of transparency, so that we need to subtract the actual value from 1. The method cairo_fill_preserve() finally defines whether the shape should be filled or not.

Cairo is all about paths, and a polygon could be considered as a closed paths with sharp edges. So, we start with creating a new path and move the cursor around, which reminds of the path string in SVG. We start by moving the cursor to the last point in the path, then iterate over the point array and draw lines to the respective next point. Finally we close the path, set the drawing style using the setStyle() method and draw the polygon using the method cairo_stroke().

When you compare the rendered image with the supersampled GD image you may notice, that the quality of cairos native anti aliasing is of course far better then the supersampled with GD.

Ming & Flash

And the same for flash.

/** * Sets a shapes fill style * * @param SWFShape $shape * @param Color $color * @param boolean $filled */ protected function setShapeStyle( SWFShape $shape, Color $color, $filled ) { if ( $filled ) { $fill = $shape->addFill( $color->red, $color->green, $color->blue, 255 - $color->alpha ); $shape->setLeftFill( $fill ); } else { $shape->setLine( $this->modifyCoordinate( 1 ), $color->red, $color->green, $color->blue, 255 - $color->alpha ); } } /** * Draws a single polygon * * @param array(Coordinate) $points * @param Color $color * @param boolean $filled */ public function drawPolygon( array $points, Color $color, $filled = true ) { $shape = new SWFShape(); $this->setShapeStyle( $shape, $color, $filled ); $lastPoint = end( $points ); $shape->movePenTo( $this->modifyCoordinate( $lastPoint->x ), $this->modifyCoordinate( $lastPoint->y ) ); foreach ( $points as $point ) { $shape->drawLineTo( $this->modifyCoordinate( $point->x ), $this->modifyCoordinate( $point->y ) ); } $object = $this->movie->add( $shape ); }
Your browser needs to support Flash contents to display this image. Flash polygons

The two methods setStyle(), to set the drawing style for a flash shape and the required method drawPolygon() also need to be defined in the flash backend.

The method setStyle() sets the style for a SWFShape, which is the generic class for all graphic primitives. The set style again depends on the desired fill state. In the case of a filled shape first a SWFFill object is created from the shape, with the colors from the Color object. Again, the alpha value defines the opacity and not the transparency, so that we need to subtract the alpha value from 255. The call to setLeftFill() with the SWFFill object let the shape use the fill color. If only a line should be drawn, the line style needs to be set with the same parameters.

The drawing of the polygon is also very similar to cairo and SVG. After creating the raw shape and assigning the style to it, the "pen" is moved to the last point of the polygon. After that the foreach loop iterates over all points and draws the line to the next point. Of course all coordinates needs to be passed to the modifyCoordinate() method, the same for the line thickness in the last method, to transform all size and coordinate values to those twips. Finally the shape can be added to the movie. As a result of this operation you get a SWFDisplayItem which may be moved, transformed or animated by further actions, which will be used later.

Different border placements

You may not have noticed this in the images with the polygons, because it is a negligible effect for borders with a width of one, but the borders are placed differently in the different extensions.

Different border placementDifferent border placement

With increasing border width this gets more and more visible, but also if you have multiple shapes with very few space in between, so that they may overlap in ming and SVG. So what can we do about this? The black border in the image above shows the virtual border specified by the given coordinates. The blue border shows the actual drawn border. Seldom, but for me the behaviour of GD seems the one a user of a wrapper would expect, so that we reduce the size of the polygons in the other drivers - but this is more complicated then it seems in the first place.

In theory
Polygon border size reductionPolygon border size reduction

Remember the structure of the polygon defining point array, it is an array with points. We now can iterate over this array and take three points into consideration on each point, the last, the current and the next one. From the three points we can calculate the vectors $last and $next describing the two edges next to the current point. When we now know on which side of the point the inner face of the polygon is, we can calculate the angle a.

As we don't need the length of the edges next to the current point, we can unify the length of the vectors, which is indicated by |$next| = 1 in the graphic. The rotated vector $next to the inner face of the polygon multiplied with the $size of the border reduction results in a point parallel to the original vector $next. The point should be moved to the middle of the angle between the two vectors which can be done by adding the vector $next * tan( a ). The resulting point P' is the new point of the polygon. If this is done for all the points in a polygon we get a smaller polygon, reduced exactly by the given value. In the implementation you will notice some handled edge cases, mentioned in the next paragraph.

The implementation

You may have noticed, that I talked about vectors in the last paragraph. To be able to use them in the backend I extend the coordinate class by common vector functions, like adding, subtracting and multiplicating with a scalar. The class is not interesting per se, so I skip explaining the details. But it will just work in the downloadable package and extends the already known coordinate class.

/** * Reduces the size of a polygon * * The method takes a polygon defined by a list of points and reduces its * size by moving all lines to the center by the given $size value. * * This code bases on the the corresponding method in the graph cmponent * from the eZ Components project, but has been extented to fit a more * general purpose. The original code is licensed under the "New BSD * License" and the copyright owner is eZ Systems as. * * @param array(Coordinate) $points * @param float $size * @throws ReducementFailedException * @return array( Coordinate ) */ public function reducePolygonSize( array $points, $size ) { $pointCount = count( $points ); // Build normalized vectors between polygon edge points $vectors = array(); for ( $i = 0; $i < $pointCount; ++$i ) { $nextPoint = ( $i + 1 ) % $pointCount; $vectors[$i] = Vector::fromCoordinate( $points[$nextPoint] ) ->sub( $points[$i] ); // Throw exception if polygon is too small to reduce if ( $vectors[$i]->length() < $size ) { throw new ::Exception( 'Reducement of polygon failed.' ); } $vectors[$i]->unify(); // Remove point from list if it is the same as the next point if ( ( $vectors[$i]->x == $vectors[$i]->y ) && ( $vectors[$i]->x == 0 ) ) { $pointCount--; if ( $i === 0 ) { $points = array_slice( $points, $i + 1 ); } else { $points = array_merge( array_slice( $points, 0, $i ), array_slice( $points, $i + 1 ) ); } $i--; } } // Remove vectors and appendant point, if local angle equals zero // degrees. for ( $i = 0; $i < $pointCount; ++$i ) { $nextPoint = ( $i + 1 ) % $pointCount; if ( ( abs( $vectors[$i]->x - $vectors[$nextPoint]->x ) < .0001 ) && ( abs( $vectors[$i]->y - $vectors[$nextPoint]->y ) < .0001 ) ) { $pointCount--; $points = array_merge( array_slice( $points, 0, $i + 1 ), array_slice( $points, $i + 2 ) ); $vectors = array_merge( array_slice( $vectors, 0, $i + 1 ), array_slice( $vectors, $i + 2 ) ); $i--; } } // No reducements for lines if ( $pointCount <= 2 ) { return $points; } // The sign of the scalar products results indicates on which site // the smaller angle is, when comparing the orthogonale vector of // one of the vectors with the other. Why? .. use pen and paper .. // // We just build the sum of the sign for the angles at each point of // the polygon and check which kind of angles occured more. Using this // to determine the inner side of a polygon is a valid simplication for // polygones without crossing edges. $signSum = 0; for ( $i = 0; $i < $pointCount; ++$i ) { $last = $i; $next = ( $i + 1 ) % $pointCount; // Calculate not normalized scalar product. This is a bit fatser // then the real calculation, and we just need the sign. $signSum += ( -$vectors[$last]->y * $vectors[$next]->x + $vectors[$last]->x * $vectors[$next]->y ) < 0 ? 1 : -1; } if ( $signSum === 0 ) { // We got a polygon with cutting edges. This method is not able to // reduce the size of those. Just return the original point array. return $points; } // Normalize the value, we just need the sign in the further steps $sign = ( $signSum < 0 ? -1 : 1 ); // Move points to center $newPoints = array(); for ( $i = 0; $i < $pointCount; ++$i ) { $last = $i; $next = ( $i + 1 ) % $pointCount; // Orthogonal vector with direction based on the side of the inner // angle $v = clone $vectors[$next]; if ( $sign > 0 ) { $v->rotateCounterClockwise()->scalar( $size ); } else { $v->rotateClockwise()->scalar( $size ); } // get last vector not pointing in reverse direction $lastVector = clone $vectors[$last]; $lastVector->scalar( -1 ); // Calculate new point: Move point to the center site of the // polygon using the normalized orthogonal vectors next to the // point and the size as distance to move. // point + v + size / tan( angle / 2 ) * startVector $newPoint = clone $vectors[$next]; $newPoints[$next] = $v ->add( $newPoint ->scalar( $size / tan( $lastVector->angle( $vectors[$next] ) / 2 ) ) ) ->add( $points[$next] ); } return $newPoints; }

As you remember from the previous paragraph we need the normalized vectors between the points instead of the points themselves. To build the $vectors array we iterate over the point array as a first step in the method and use the common vector functions to calculate this vector. We do not normalize the vector immediately, because we check first, if the length of a edge is lower then requested size reduction. In this case we bail out with an exception, because we obviously would end up with some unwanted polygon, if we would just proceed. After this step we can unify the vector and add it to the array. After this another sanity check is implemented - we check if the calculated vector is zero, which means, that we got two points at the exactly same position, which does not make any sense. Such points are just removed from the list, because of redundancy.

Once we calculated all vectors between the points, another required sanity check can be performed. We check if there are any points in the polygon which are just on a line with an angle of 180 degrees on both sides. Those points are also redundant and will be removed.

After all redundant points are removed a quick check is performed, if we now end up with a line, which obviously makes no sense to reduce in length. In this case the (perhaps reduced) amount of points is returned, which can now be rendered.

The next block performs the most problematic algorithm in this context - it tries to detect where the inner side of the polygon is, to move the points to right direction on later size reduction. As perhaps know, the scalar product of two vectors indicates on which side the smaller angle is - plus some other stuff. For trivial polygons with three points the smaller angle is on the same side at each point of the polygon - this is why we use a simplification of this algorithm in eZ Components, where we only determine the sign at one point, because the component only uses polygons with three points.

When the image creation library should fulfill a more general purpose, we need a bit more complex algorithm. As you can see from the code we check the sign at each point, build the sum of all determined scalar products. The sum of all signs should always return us a valid indicator where the inner side of the polygon is located, which can be used during the actual size reduction.

The last loop now performs the reduction described in the theoretical part above, and visualized in the small graphic. Even the notation of the calculation is not that readable using fluent interfaces you should get the point.

Usage

I will only show the usage of the border reduction algorithm in the SVG backend, because it should be intuitive how it will be used in the other drivers, and the examples for this are included in the source archive.

/** * Draws a single polygon * * @param array(knCoordinate) $points * @param Color $color * @param boolean $filled */ public function drawPolygon( array $points, Color $color, $filled = true ) { // Fix polygone size for non filled polygons if ( !$filled ) { $points = $this->reducePolygonSize( $points, .5 ); } // The known code... }

At the beginning the point array will be modified for non filled polygons with the method we just wrote. As you may remember from the initial image describing the border placement, SVG renderers by default draw the border in the middle of the real edge of the polygon. With a stroke width of 1, we reduce the size of the polygon by the half: 0.5. This results in equivalent sized polygons, no matter weather they are filled or not, like this images shows:

Usage of polygon size reductionUsage of polygon size reduction

The usage of this function happens completely transparent to the user of the library, so you really don't need to think about it.

Conclusion

In the last chapter I showed the generation of a very simple shape - polygons - with its problems, like the different border placement in the backends. For other shapes the progress is nearly the same, most shapes you could request can implement very much like the polygons, or even as a wrapper of the polygon creation, like stars, rectangles or similar. We will create a tool class, which does this in the last chapter.

A bit more complex are ellipses, or ellipse sectors, because it is much harder to recalculate the border placement for them. Ellipse segments, or sectors, are implemented in ezcGraph for example, so you may take the code from there.


Comments

azahari at Wednesday, 13.2. 2008

i don't know if this code really work with what i'm doing now because i don't go through the code yet.here ill give u some idea on what im doing.right now im trying to develop dfd editor on my web based system.so user don't have to create dfd using visio or 3rd party software..so, from ur code..i hope could create a shape to use in that dfd editor..so, maybe u could give some idea on how to do it.thanking you in advance..sorry because my english is bad..for ur info i'm asian..or more specific malay..thanx

Subscribe to updates

There are multiple ways to stay updated with new posts on my blog: