Image creation with PHP
First published at Saturday 29 September 2007
Warning: This blog post is more then 17 years old – read and use with care.
Image creation with PHP
Table of Contents
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 first topic is part of this blog post, the next ones will follow later.
Describes the advantages and drawbacks of the different extensions and the formats they can generate.
Describes the basic required data structure and the generation of a first simple shape.
Describes how you can add radial and linear gradients to your generated graphics with each of the backends.
Integrating bitmaps with the backends.
The basics of text rendering with all extensions.
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 formats
First there are two classes of graphics you generate, and each of those classes again has several different formats. I will classify them by the pros and cons and give a common use case for each of them. This will give you a general overview - the technical details for the relevant formats will be part of a later section.
Vector formats
On the one hand we have vector based graphics formats, which means, that the format describes shapes, their fills, line styles etc. With those formats it is up to some client (image viewer) to actually render the graphics, as the current normal display expects pixels, the clients needs to transform the shapes into pixels. This may result in differently rendered results, or quality differences of the output depending on the renderer.
SVG
SVG is a W3C standard for XML based "Scalable Vector Graphics". It is a open standard, and due its nature being XML, easy to generate and sometimes even user readable. XML always has the drawback of quite large files, compared with the contained information. But, as vector graphics formats don't need to describe each pixel, but shapes which may be far bigger, and you may compress XML using gzip, which nearly each client may read, this is negotiable. Common SVG graphics are mostly smaller then similar images which use a bitmap format.
As SVG is XML you may include stuff from other namespaces directly in your SVG document, or even define your own extensions. You may, for example, use data URLs to directly include other documents, like bitmap images. When displayed in browser you may also include ECMAScript to interactively modify the DOM tree, creating animations or user interfaces. Some other displaying clients may also be capable of interpreting ECMAScript, but you shouldn't expect that from the average image viewer.
There are two real drawbacks when it comes to SVG rendering in image viewers. The subset of supported SVG elements may differ. The W3C defines a standard called SVG Tiny which contains a small subset which is common to nearly all clients, and will be a good base for estimations, what some client may support, and which may even be displayed on mobile clients.
The largest issue with those different clients is the rendering of text. You just can't know which fonts may be available on the client, and because of this you can't know how much space some text will consume, or how it will look like. To bypass this issue you may convert text to paths, which is supported by some editors, but this increases the complexity of rendering for the client and the size of your SVG document.
Pros:
Open standard
Common syntax (XML)
Easy to debug, because it is user readable
A lot of clients, which may display SVG (Gecko engine, Opera, webkit (khtml), Adobe SVG Viewer (IE plugin), a LOT of image creation tools.
Cons:
Different client side rendering
Font issues
Common usecases:
Graphics in web
Scalable graphics
Vector graphics which should still be usable in several years...
Flash
Flash is a closed standard defined by Adobe for animated and interactive web graphics. When it comes to image generation you can consider this as vector graphics format, by omitting all possibility to add frames, or user interaction.
There is only one real client so that you can be pretty sure how some stuff will be rendered, until you use some of evolving open source clients, which grew because the official client does not run on several platforms, can not be compiled and the chance for violating your privacy by security issues or other unknown code is quite high...
Pros:
Rich vector graphics format with installed clients on nearly all systems (~98%)
Cons:
Closed proprietary format.
Clients are not available or easy to develop for all platforms.
Not accessible to users with limited access
Common usecase:
Online marketing presentations with mainstream centric target group.
PDF / PS
PDF and PS also have support for basic vector graphics, which may be generated by one of the extensions, I will mention later. Due to their limited support of graphics features and completely different common usecase I won't go into detail here.
Bitmaps
On the other hand there are bitmap formats, like the well known PNG, GIF, JPEG or BMP. Those formats already describe the pixels, so that there is no more required transformation by the client, it just needs to output the stuff. With these formats the quality and overall result complete depends on the image generation tool.
GIF
THE GIF format had patent issues for some time, which made it unavailable on some platforms or some tools. Even the format is now available everywhere it has some severe limitations, like a limited colorspace - maximum are 256 colors - and bad support for transparency - you may define only one color as fully transparent, which makes alpha blending impossible.
The size of GIF files mainly depends on the count of used colors which results in quite big files for complex images.
Pros:
Well known established image format
Cons:
Bad transparency support
Limited colorspace
Common usecase:
Web applications, where the author did not know about PNG.
Animated images
JPEG
Also affected by several patent issues, it is also a very widely supported format, but the compression algorithm makes it useless for graphics, but more appropriate for common pictures. The algorithm performs a fourier transformation, so that you will have a three dimensional cosine function for parts of the images, which are not able to describe sharp edges. With a lower quality factor for JPEG images, the amount of cosine terms in the resulting function will be reduced, which makes it harder and harder to describe sharp edges. The resulting artifacts are commonly known and shown on the image below, in which I saved a 10 * 10 pixel image with sharp edges with as a JPEG image with a quality of 95.
Another drawback of the JPEG format is, that it does not have any support for transparency - which is OK for its normal usecase, pictures, but makes it very useless for common generated graphics.
Pros:
Well known and established image format
Good compression of pictures
Cons:
Bad compression and artifacts for graphics.
No transparency
Common usecase:
Pictures
PNG
PNG has been developed and standardized by the W3C because of the GIF patent issues and is the ultimate format for graphics, especially on the web. It has a lossless compression, supports the full RGB colorspace, 128 alpha channels for each color and results in quite small files, especially, when the quality is compared with GIF or JPEG graphics of the same size. Currently the only drawback is, that the outdated browser Internet Explorer 6 still does not support the alpha channels in PNG, which can be fixed using a hack in most cases, though...
Pros:
Well known and established image format
Good compression of graphics
Full RGB with 128 alpha channels
Cons:
Limited support in one browser
Common usecase:
Graphics in the web.
BMP, TIF, ...
There are far more (generic) bitmap formats, which do no compression or support other colorspaces like CMYK or HSL. They do not matter for web related images, because they normally result in far too big files, to use them in the web.
Conclusion
If you may want to use a vector graphic format you should use SVG, even some windows people may need to download a plugin to display those graphics. This is the right choice for a future proof format, with an independent consortium behind it.
On the other hand you might want to use a bitmap format which will make it much harder to integrate user interaction or animations. In this case you should use PNG for graphics and JPEG for pictures, because they offer the best compression for their designated use case and both have very wide support in all kinds of possible image viewers.
The libraries
Talking about several formats, there is a list of specialized libraries to generate each of them. Let's start with the bitmap libraries.
ext/GD
The GD extension is the best known PHP library for image generation, and in use for years now. It is very well documented, and you can find examples for nearly everything on the web, you may want to do using the GD library - but there are several major drawbacks. As said, when it comes to bitmaps, the generating library has to take care of the rendering, so that the decision about the used library will have a major effect on the quality of the resulting image.
ext/GD does not support anti aliasing in most of the cases - each line you draw has bad aliasing steps, each circle and each polygon. The aliasing of fonts depends on the backend library you use, for PS type 1 fonts you get no anti aliasing, while TTF fonts, rendered with FreeType 2, have. There is no native support for gradients, and some shapes, like circle sectors may look very bad when used with transparency. Trying to emulate gradients will fail, because setting lots of single pixels in a image is just far to slow.
The only real benefit from using GD is, that it is available nearly everywhere.
ext/cairo_wrapper
Cairo is a fantastic 2D graphic library, for example used by Gnome since 2.8 to draw the GUI, or by Firefox 2 to render SVGs and Firefox 3 to render the complete documents. It has native support for paths, gradients, anti aliasing and nearly everything you could demand from a 2D graphics library. Besides this, it is really fast and uses established libraries like Pango for text rendering.
The original Cairo library may not only output bitmap formats, but also SVG, PDF or PS. Additionally it may also render directly to X windows, or use OpenGL through Glitz for hardware acceleration.
Hartmut Holzgraefe wrote a wrapper for the cairo library using his pear/CodeGen_PECL package for extension generation. The package is available from cvs or from the pear channel, both documented on the packages homepage. To install the package just type:
# pear channel-discover pear.php-baustelle.de
# pear install php-baustelle/cairo_wrapper-beta
If you are looking for documentation of the cairo API the best source currently is the C API documentation and the samples at cairo-graphics.org. As Hartmut really implemented a pure API wrapper for PHP you can use nearly all methods equivalently to their C examples.
ext/ming
The extension is still in an alpha state, but it is designed to create flash's SWF files. It currently supports a subset of functions, which does not match any defined flash version, but you can expect support for texts, gradients, basic shapes a very limited set of bitmaps and action script - even the documentation is often missing or may contain broken examples. Some of the documented functions and methods do not work as expected, or do not work at all - but that is something you have to live with, when using an alpha extension.
The only reason to use this extension is, that it is the only free possibility to create flash graphics or animations.
ext/DOM
For SVG generation we will use ext/DOM and I think every user of PHP 5 knows about it, as it is THE way to work with XML, like it is in several other languages, like ECMAScript and many others. Even XMLWriter is faster for document generation, we will have to insert nodes without the knowledge about the complete document prior to the final generation, so that we need something which holds the complete structure of the XML document. DOM perfectly solves this with a known and handy API.
Conclusion
I will introduce a wrapper which wraps all the mentioned libraries to make the differences transparent to the user - with some limitation in one or another wrapper, which will be mentioned in the implementation description.
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' );
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' );
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' );
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' );
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;
}
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;
}
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.
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 );
}
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 );
}
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.
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
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:
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.
Gradients
The next big thing, to bring some color into the game are gradients. I will only show radial gradients in this example, but linear gradients should also be really easy to implement on this basis, and they show the general problems of this implementation.
The data structure
As described before the gradient definition will extend the basic color definition defined some chapters earlier. This makes it possible to use the gradient as a plain color for backends which do not support gradients, like the GD backend.
namespace kn::Graphic;
/**
* Class extending the basic colors by defining a radial gradient.
*
* @version //autogen//
* @author Kore Nordmann <kore@php.net>
* @license GPLv3
*/
class RadialGradient extends Color
{
/**
* Gradient starting color
*
* @var Color
*/
public $startColor;
/**
* Gradient end color
*
* @var Color
*/
public $endColor;
/**
* Coordinate of the radial gradient center.
*
* @var Coordinate
*/
public $center;
/**
* Width of ellipse filled by the gradient.
*
* @var float
*/
public $width;
/**
* Height of ellipse filled by the gradient.
*
* @var float
*/
public $height;
/**
* Construct radial gradient
*
* Construct radial gradient from its start color, end color, center point
* and width and height of the ellipse which should be filled by the
* gradient.
*
* @param Color $startColor
* @param Color $endColor
* @param Coordinate $center
* @param float $width
* @param float $height
* @return void
*/
public function __construct( Color $startColor, Color $endColor, Coordinate $center, $width, $height )
{
// Just set the properties
$this->startColor = $startColor;
$this->endColor = $endColor;
$this->center = $center;
$this->width = $width;
$this->height = $height;
// Fallback to average color of start end end color for incopatible
// backends
foreach ( $startColor as $key => $value )
{
$this->$key = ( $value + $endColor->$key ) / 2;
}
}
/**
* Return common hex string definition for color.
*
* @return string
*/
public function __toString()
{
return sprintf( '%s_%s_%d_%d_%d_%d',
substr( $this->startColor, 1),
substr( $this->endColor, 1),
$this->center->x, $this->center->y,
$this->width, $this->height
);
}
}
The definition is quite simple. For a radial gradient we need five values to define it:
Start color
End color
Center of gradient ellipse
Width and height
To keep compatibility with backends which are not capable of gradients the single colors values (red, green, blue, alpha), which may be requested from some backends are calculate from the average of the start and end color of the gradient.
The __toString() method will be required by some backends to get a somehow unique identifier of a gradient.
SVG
We start first with the implementation in the SVG backend. As you expected, when you read through the complete text you know that we have to modify the setStyle() method, which defines the currently used style for each drawn shape.
In this method, now the method getGradientUrl() will be called in the first line, which should return the URL of a gradient definition. The gradient URL is used in SVG to reference a fill gradient for a shape. The method returns false, if no gradient has been found, or could be created, so that we work with the provided color structure like it was a normal simple color as a fallback. The creation of gradients results in a bit more complex XML:
/**
* Return gradient URL
*
* Creates the definitions needed for a gradient, if a proper gradient does
* not yet exists. In case a gradient has already been used its URL will be
* returned and no new gradient will be created.
*
* If a gradient type is not yet supported, or a plain Color object has
* been given, the method will return false.
*
* @param Color $color
* @return string
*/
protected function getGradientUrl( Color $color )
{
switch ( true )
{
case ( $color instanceof LinearGradient ):
// Handle other gradient types..
break;
case ( $color instanceof RadialGradient ):
if ( !in_array( $color->__toString(), $this->drawnGradients, true ) )
{
$gradient = $this->dom->createElement( 'linearGradient' );
$gradient->setAttribute( 'id', 'Definition_' . $color->__toString() );
$this->defs->appendChild( $gradient );
// Start of linear gradient
$stop = $this->dom->createElement( 'stop' );
$stop->setAttribute( 'offset', 0 );
$stop->setAttribute( 'style', sprintf( 'stop-color: #%02x%02x%02x; stop-opacity: %.2F;',
$color->startColor->red,
$color->startColor->green,
$color->startColor->blue,
1 - ( $color->startColor->alpha / 255 )
)
);
$gradient->appendChild( $stop );
// End of linear gradient
$stop = $this->dom->createElement( 'stop' );
$stop->setAttribute( 'offset', 1 );
$stop->setAttribute( 'style', sprintf( 'stop-color: #%02x%02x%02x; stop-opacity: %.2F;',
$color->endColor->red,
$color->endColor->green,
$color->endColor->blue,
1 - ( $color->endColor->alpha / 255 )
)
);
$gradient->appendChild( $stop );
// Define gradient dimensions
$gradient = $this->dom->createElement( 'radialGradient' );
$gradient->setAttribute( 'id', $color->__toString() );
$gradient->setAttribute( 'cx', 0 );
$gradient->setAttribute( 'cy', 0 );
$gradient->setAttribute( 'fx', 0 );
$gradient->setAttribute( 'fy', 0 );
$gradient->setAttribute( 'r', $color->width );
$gradient->setAttribute( 'gradientUnits', 'userSpaceOnUse' );
$gradient->setAttribute( 'gradientTransform',
sprintf(
'matrix( 1, 0, 0, %.2F, %.4F, %.4F )',
$color->height / $color->width,
$color->center->x,
$color->center->y
)
);
$gradient->setAttributeNS(
'http://www.w3.org/1999/xlink',
'xlink:href',
'#Definition_' . $color->__toString()
);
$this->defs->appendChild( $gradient );
$this->drawnGradients[] = $color->__toString();
}
return sprintf( 'url(#%s)',
$color->__toString()
);
default:
return false;
}
}
We use a switch statement in this method to handle the different gradient types. The method currently can only return URLs for radial gradients, but you see, how it could be extended.
In SVG each gradient consists of two parts. The first part is a linear gradient element, which just defines the colors of a gradient and where which color is placed on a scale from 0 to 1. Beacuse we only support gradients consisting of two colors we create two stops, one at position (offset) 0 and one at position 1, with the colors given in the RadialGradient object. This linear gradient is added to the defs (now, we finally use this section) section of the SVG document and can later be referenced by its ID.
Once you defined the gradient, the second step is to define the gradients scale, use and position in the image. For this a radialGradient element is created, which is originally placed at position 0, 0 in the image. The cx and cy attributes define the center of the gradient by its x and y coordinate. The r attribute defines the radius of the gradient, where the declared width is used.
To move the radial gradient to the right position in the image and modify its height, so we get an ellipse instead of a circle, a transformation matrix is used to modify the gradient. I will come back to transformation matrices in the last chapter, for now you can just accept that this works, or take a look at the corresponding Wikipedia article. Finally a href attribute out of the xlink namespace is used to reference the earlier defined linearGradient element, which defines the colors of the gradient. At the end we store the gradient in out cache and return its URL - this can be used by the setStyle() method, which now looks like:
/**
* Get Style for SVG Element
*
* @param Color $color
* @param boolean $filled
*/
protected function getStyle( Color $color, $filled )
{
$url = $this->getGradientUrl( $color );
switch ( true )
{
case $filled && $url:
return sprintf( 'fill: %s; stroke: none;',
$url
);
case $filled && !$url:
return sprintf( 'fill: #%02x%02x%02x; fill-opacity: %.2f; stroke: none;',
$color->red,
$color->green,
$color->blue,
1 - ( $color->alpha / 255 )
);
case !$filled && $url:
return sprintf( 'fill: none; stroke: %s;',
$url
);
case !$filled && !$url:
return sprintf( 'fill: none; stroke: #%02x%02x%02x; stroke-width: 1; stroke-opacity: %.2f;',
$color->red,
$color->green,
$color->blue,
1 - ( $color->alpha / 255 )
);
}
}
Remembering the last explanations of this method, everything should be quite self explanatory, and you can see how the URL of the gradient is simply used to define the fill or stroke of the shape. As a results you now can create such nice images.
This cube was generated by this image construction code, you find below all of the examples:
$graphic = new Svg( 150, 150 );
$graphic->drawPolygon(
array(
new Coordinate( 25, 60 ), new Coordinate( 95, 60 ), new Coordinate( 95, 130 ), new Coordinate( 25, 130 ),
),
new RadialGradient(
new Color( '#407cd2' ),
new Color( '#245398' ),
new Coordinate( 95, 60 ), 70, 70
)
);
$graphic->drawPolygon(
array(
new Coordinate( 95, 60 ), new Coordinate( 123.3, 30.3 ), new Coordinate( 123.3, 100.3 ), new Coordinate( 95, 130 ),
),
new RadialGradient(
new Color( '#407cd2' ),
new Color( '#204a87' ),
new Coordinate( 95, 60 ), 40, 70
)
);
$graphic->drawPolygon(
array(
new Coordinate( 95, 60 ), new Coordinate( 123.3, 30.3 ), new Coordinate( 53.3, 30.3 ), new Coordinate( 25, 60 ),
),
new RadialGradient(
new Color( '#407cd2' ),
new Color( '#193b6c' ),
new Coordinate( 95, 60 ), 95, 45
)
);
$graphic->drawPolygon(
array(
new Coordinate( 94.4, 75 ), new Coordinate( 90.8, 66 ), new Coordinate( 81, 66 ), new Coordinate( 88.5, 60 ), new Coordinate( 85.3, 50.8 ),
new Coordinate( 93.5, 56 ), new Coordinate( 101.3, 50.1 ), new Coordinate( 98.9, 59.5 ), new Coordinate( 106.9, 65 ), new Coordinate( 97.2, 65.7 ),
),
new RadialGradient(
new Color( '#ffffffA0' ),
new Color( '#ffffffff' ),
new Coordinate( 95, 60 ), 12, 12
)
);
$graphic->save( 'images/example_svg_04.svg' );
This first defines the 3 four edged polygons required for the cube itself with blue radial gradients on each. Finally a star is defined by its edge points with a transparent white gradient.
GD & PNG
As said earlier GD itself is not capable of rendering gradients. You could of course draw a gradient in a shape pixel by pixel, but this is a quite complex task, because you would need calculate the pixels, which are inside of the given shape and fill the pixel by pixel, where, for radial gradients, you need the distance to the center point, which would mean to calculate the square root of some number (pythagoras). Beside this setting single pixels with GD is damn slow - so, at all, this is not really a solution.
But as we implemented the fallback to the average color of the gradient, the results does not look that bad, even with a backend which does not support gradients, as you can see at the image on the right.
Cairo & PNG
As you know from the prior parts, the next step is to get it working with Cairo. Cairo has full support for this kind of stuff, so it is a small extension of the setStyle() method here.
/**
* Set the style for current cairo context
*
* @param Color $color
* @param boolean $filled
*/
protected function setStyle( Color $color, $filled )
{
switch ( true )
{
case ( $color instanceof LinearGradient ):
// Handle other gradient types..
break;
case $color instanceof RadialGradient:
$pattern = ::cairo_pattern_create_radial(
0, 0, 0,
0, 0, 1
);
::cairo_pattern_add_color_stop_rgba (
$pattern,
0,
$color->startColor->red / 255,
$color->startColor->green / 255,
$color->startColor->blue / 255,
1 - $color->startColor->alpha / 255
);
::cairo_pattern_add_color_stop_rgba (
$pattern,
1,
$color->endColor->red / 255,
$color->endColor->green / 255,
$color->endColor->blue / 255,
1 - $color->endColor->alpha / 255
);
// Scale pattern, and move it to the correct position
$matrix = cairo_matrix_multiply(
$move = ::cairo_matrix_create_translate( -$color->center->x, -$color->center->y ),
$scale = ::cairo_matrix_create_scale( 1 / $color->width, 1 / $color->height )
);
::cairo_pattern_set_matrix( $pattern, $matrix );
::cairo_set_source( $this->context, $pattern );
::cairo_fill( $this->context );
break;
default:
::cairo_set_source_rgba(
$this->context,
$color->red / 255,
$color->green / 255,
$color->blue / 255,
1 - $color->alpha / 255
);
break;
}
if ( $filled )
{
::cairo_fill_preserve( $this->context );
}
}
Again we replace the plain setting of the color by a switch statement, which enables us to add support for more different types of colors.
The radial gradient is a fill pattern in terms of cairo. A fill pattern is used for all complex fill types. The radial gradient fill pattern is constructed from 6 values, which are:
The center coordinate of gradient start (X and Y coordinate)
The radius of the gradient start
The center coordinate of the gradient end (X and Y coordinate)
The radius of the gradient end
As you can see we create the complete gradient at the position (0, 0) of the coordinate system with an outer radius of 1. This gradient can later be modified using transformations matrices, which are used to move and scale the gradient to the provided position / dimensions. This is necessary either way, because you can't provide a different height and width (radial gradient ellipse) in the constructor of the pattern.
After that, very similar to SVG, stops are added to the pattern, which define the colors at some offset of the gradient. We only allow to set two colors for each radial gradient with the API of kn::Graphics::RadialGradient, so that stops are again added at position 0 and 1, with the colors from the struct.
After creating the basic pattern the transformation matrices are created and applied to the pattern, which place at the requested position and scale it. When creating these matrices you may notice that all values are inverted, the cairo manual says here:
Important: Please note that the direction of this transformation matrix is from user space to pattern space. This means that if you imagine the flow from a pattern to user space (and on to device space), then coordinates in that flow will be transformed by the inverse of the pattern matrix.
This is why the translations (movement) and scaling are inverted. And normally you would apply a translation matrix after a scaling matrix, so that the scaling does not affect the translation and multiply the moved distance. But even here you need to respect, that the transformations are inverted, so that the translation matrix is multiplied with the scale matrix, and not the other way round. After creating this matrix, it is applied to the pattern, and set as the current fill for the context.
The results looks very similar to the SVG output - perfectly rendered gradients.
Ming & Flash
Last and least we come to the flash stuff again. First, we extend the style method again, where we come to some strange cases with ming / flash again.
protected function setShapeStyle( SWFShape $shape, Color $color, $filled )
{
if ( $filled )
{
switch ( true )
{
case ( $color instanceof LinearGradient ):
// Handle other gradient types..
break;
case ( $color instanceof RadialGradient ):
$gradient = new SWFGradient();
// Add gradient color stops
$gradient->addEntry(
0,
$color->startColor->red,
$color->startColor->green,
$color->startColor->blue,
255 - $color->startColor->alpha
);
$gradient->addEntry(
1,
$color->endColor->red,
$color->endColor->green,
$color->endColor->blue,
255 - $color->endColor->alpha
);
// Set gradient type
$fill = $shape->addFill( $gradient, SWFFILL_RADIAL_GRADIENT );
// Scale gradient
$fill->scaleTo(
$this->modifyCoordinate( $color->width ) / 32768,
$this->modifyCoordinate( $color->height ) / 32768
);
$fill->moveTo(
$this->modifyCoordinate( $color->center->x ),
$this->modifyCoordinate( $color->center->y )
);
// Use gradient to fill stuff
$shape->setLeftFill( $fill );
break;
default:
$fill = $shape->addFill( $color->red, $color->green, $color->blue, 255 - $color->alpha );
$shape->setLeftFill( $fill );
break;
}
}
else
{
$shape->setLine( $this->modifyCoordinate( 1 ), $color->red, $color->green, $color->blue, 255 - $color->alpha );
}
}
Again we switch to a big switch statement, to differ between the color types. And, again, only the radial gradient is implemented, while the linear gradients is left as an exercise for the reader.
The basic setup of the gradient is very similar to the setup with the cairo library - we create a new gradient and add two stops, at the radius 0 and 1. This could also be reused as linear gradient, because the real type of the gradient is defined, when we create the actual fill object.
In the cairo example we modified the fill using transformation matrices, in this example we just call the common flash modification methods. In the scaleTo() call you may wonder, why the dimensions are divided by 32768. The scaling of those fills are handled different then everything else in ming. You might remember, that the maximum size of a flash graphic is defined in ticks, and that you have a maximum resolution of 32768 ticks. So the initial fill spans over the complete flash canvas and we actually have to reduce its size to the requested boundings. After this we move the fill to the requested center position, using the normal coordinate definitions again. After that we set the fill state, as done before.
As you can see in the example, it looks quite similar to the SVG and cairo examples, even the gradient rendering of the flash client is a bit different from the two other backends.
Bitmaps
When dealing with vector graphics you may also want to include some arbitrary bitmaps in your graphics, for stuff, which can't be easily drawn using common graphics primitives. For this we want to integrate the support for bitmaps in all the backends, which will again cause some funny issues.
For this we again start adding a abstract method to the base class defining the signature for the new method.
/**
* Add a bitmap
*
* Add a bitmap at the specified position, which can be found at the
* specified location in the filesystem. You may optionally specify a
* destination width and height for the integrated bitmap - else the bitmap
* would be integrated using its original dimensions.
*
* @param Coordinate $position
* @param string $file
* @param int $width
* @param int $height
* @return void
*/
abstract public function addBitmap( Coordinate $position, $file, $width = null, $height = null );
The comment block should tell you everything you need to know about this method. ;)
DOM & SVG
When you want to add bitmaps to your SVG image, you normally use XLink, another XML based standard for linking documents, to reference the image by some URI. The URI normally references an image in your local file system, or on some (web)server.
This obviously doesn't work well in our case, because the location of the SVG might change, we might want to display it on the website, where the original bitmap is not available, or is available at a different location. This is, where data URLs get really useful. Using data URLs you may include binary files base64 encoded in your document, and they are used, like the image would have been found at some other available location. You will see in the code how they are build exactly - and all clients I got in contact with, can handle those data URLs. So let's take a look at the actual implementation.
public function addBitmap( Coordinate $position, $file, $width = null, $height = null )
{
// Get mime type and dimensions from image
$data = getimagesize( $file );
if ( ( $width === null ) || ( $height === null ) )
{
// Use original image dimensions if they haven't been specified.
$width = $data[0];
$height = $data[1];
}
// Create new image node
$image = $this->dom->createElement( 'image' );
$image->setAttribute( 'x', sprintf( '%.4F', $position->x ) );
$image->setAttribute( 'y', sprintf( '%.4F', $position->y ) );
$image->setAttribute( 'width', sprintf( '%.4Fpx', $width ) );
$image->setAttribute( 'height', sprintf( '%.4Fpx', $height ) );
$image->setAttributeNS(
'http://www.w3.org/1999/xlink',
'xlink:href',
sprintf( 'data:%s;base64,%s',
$data['mime'],
base64_encode( file_get_contents( $file ) )
)
);
$this->elements->appendChild( $image );
$image->setAttribute(
'id',
$id = ( $this->elementPrefix . 'Image_' . ++$this->elementID )
);
return $id;
}
As documented in the doc block, we get the original dimension from the image, if they were not specified in the method call - this happens, when either the $width or $height parameter has not been set.
You probably want to implement some functionality here, to resize the image keeping the ration intact, when only one of those two parameters has been provided, but this is not required for our example implementation.
Once we got the dimensions of the bitmap for our graphic, we create the new SVG element, intuitively called 'image'. For this new element we set its position and dimensions using the common attributes on the element.
The actual image is reference in the href attribute from the XLink namespace. As mentioned before, we do not just link the image by the given path, but we include the complete image using a data URL. The data URL starts with 'data:', followed by the mime type of the provided file - image/png in our case - and then containing the base64 encoded binary file content.
This image is appended to the elements node of our SVG document, and can now be seen in the resulting SVG. To use this new method, we extend the last example, by adding a background image to the graphic.
$graphic = new Svg( 150, 150 );
$graphic->addBitmap(
new Coordinate( 50, 129 ),
'bitmap.png'
);
$graphic->drawPolygon(
// ...
);
$graphic->save( 'images/example_svg_05.svg' );
As a result we get a nice SVG image again.
Cairo & PNG
In cairo we can create surfaces from existing images. You might remember, surfaces are the cairo equivalent to a canvas. And this new surface can be used as a fill pattern, so that we can add those images, scaled and modified, to our created graphics.
public function addBitmap( Coordinate $position, $file, $width = null, $height = null )
{
// Ensure given bitmap is a PNG image
$data = getimagesize( $file );
if ( $data[2] !== IMAGETYPE_PNG )
{
throw new Exception( 'Cairo only has support for PNGs.' );
}
// Set destination dimennsions from image dimensions, if not specified
if ( ( $width === null ) || ( $height === null ) )
{
$width = $data[0];
$height = $data[1];
}
// Create new surface from given bitmap
$imageSurface = cairo_image_surface_create_from_png( $file );
// Create pattern from source image to be able to transform it
$pattern = cairo_pattern_create_for_surface( $imageSurface );
// Scale pattern to defined dimensions and move it to its destination position
$matrix = cairo_matrix_multiply(
$move = ::cairo_matrix_create_translate( -$position->x, -$position->y ),
$scale = ::cairo_matrix_create_scale( $data[0] / $width, $data[1] / $height )
);
::cairo_pattern_set_matrix( $pattern, $matrix );
// Merge surfaces
::cairo_set_source( $this->context, $pattern );
::cairo_rectangle( $this->context, $position->x, $position->y, $width, $height );
::cairo_fill( $this->context );
}
The only image format currently supported by cairo are PNGs, so that we need to bail out with an exception for all other formats. You may convert images you want to include use some libraries for that, like ImageTransform from the eZ Components project.
Like in the SVG example we start with filling the $width and $height variables, if they were not defined in the method call. After that we create a new surface, called $imageSurface, from the given PNG image. From this surface we create a fill pattern, which now can be transformed.
The transformation, which means moving the pattern to the right position and scale it to the given size, again happens using transformation matrices, which are applied to the pattern. We now got a pattern at the right position, but nothing we can fill with this new pattern.
As bitmaps are not rotated, and are always rectangles, we just create a new rectangle, for which we use the pattern as fill, so that we get our bitmap integrated in the created graphic, like in the SVG example. If you specify, that the image should be scaled somehow - cairo has the best and smoothest algorithms for this, from all the here mentioned libraries. To create this image, we use the same code as for the SVG, again.
GD & PNG
PNG is all about bitmap handling, so we should not expect any problems integrating any other bitmaps. But we introduced some complexity in this abstraction layer, when it comes to GD - the supersampling. We do not want the integrated bitmaps, to be scaled up and down again, because this would make them look very blurry. To bypass this problem we need to restructure the code a bit.
Since now we copied the supersampled image once to the destination image ind the real, not supersampled, size in the final save() method. Now we add another property to the GD class, which contains the destination image during the whole process - you will soon notice, why we need to do this.
The initialize method now needs to initialize both images, the canvas, still stored in the property $image, and the $destination image. The initialisation of the canvas now has been moved to its own method, resetActiveCanvas().
protected function resetActiveCanvas()
{
// 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
);
}
This method basically does, what the initialize method does before - but the initialize() method now only calls this method and also initializes the destination image.
With this structure we can now shorten the save() method.
public function save( $file )
{
// Merge canvas before saving to a file
$this->mergeCanvas();
// Render depending on the chosen output type
switch ( $this->type )
{
case IMG_PNG:
::imagepng( $this->destination, $file );
break;
case IMG_JPEG:
::imagejpeg( $this->destination, $file, 100 );
break;
default:
throw new Exception( "Unknown output type '{$this->type}'." );
}
}
Where the method mergeCanvas() now does the merging of the active canvas in $image to the $destination image, which will be stored in the save() method. The merge canvas now only does the call to imagecopyresampled() and resets the drawing canvas. So what is this restructuring effort worth? Let's take a look at the bitmap rendering method, to understand this.
public function addBitmap( Coordinate $position, $file, $width = null, $height = null )
{
if ( ( $width === null ) || ( $height === null ) )
{
// Use original image dimensions if they haven't been specified.
$data = getimagesize( $file );
$width = $data[0];
$height = $data[1];
}
// First merge everything, which has yet been drawn
$this->mergeCanvas();
// Get new image ressource from provided file
$bitmapData = $this->getRessourceForImage( $file );
// Copy bitmap to destination image
::imagecopyresampled(
$this->destination,
$bitmapData['image'],
$position->x, $position->y,
0, 0,
$width, $height,
$bitmapData['width'], $bitmapData['height']
);
}
At the begin of the method, we again fetch proper values for $width and $height - you now this from the prior implementations. After this the important call to mergeCanvas() happens. This call ensures all yet drawn contents are transferred to the destination image, so that we can render our bitmap on top, of already existing shapes.
This ensures, that the supersampling is applied for all those shapes, but not for the bitmap, which is directly drawn on the destination image. The active canvas is empty again after this, so that only new shapes, applied after adding the bitmaps, are drawn on top of the bitmap.
You may only want to merge, if it is actually necessary. to skip the CPU expensive process of merging big images. I again skip this, because this is still an example implementation only.
I did not explain one method call in the implementation above - the call to getRessourceForImage(). In GD we only work with resources, created by some GD functions. The GD function to create such a resource out of a file, depends on the file type, so that this method just checks for the type of the file, selects the right function and returns the resource and the dimensions of the image:
/**
* Get struct with gd image ressource from file
*
* If the image type of the given file can behandled the method returns an
* array struct with the image dimensions and a gd ressource for the given
* file. The struct looks like:
* array(
* 'width' => (int),
* 'height' => (int),
* 'image' => (ressource),
* )
*
* @param string $file
* @return array
*/
protected function getRessourceForImage( $file )
{
// Extract image size and type
$data = ::getimagesize( $file );
// Mapping of file types to according gd image creation functions
$gdCreateFunction = array(
IMAGETYPE_GIF => 'imagecreatefromgif',
IMAGETYPE_JPEG => 'imagecreatefromjpeg',
IMAGETYPE_PNG => 'imagecreatefrompng',
);
// Check if can handle this file type at all
if ( !isset( $gdCreateFunction[$data[2]] ) )
{
throw new Exception( "Unhandled image type '{$data[2]}'." );
}
// Return array with basic image data and image ressource
return array(
'width' => $data[0],
'height' => $data[1],
'image' => call_user_func(
$gdCreateFunction[$data[2]],
$file
),
);
}
You may want to add support for more image types here, and also check if the required function is available at all in your installation, because the support for GIF / JPEG / PNG depends on the configuration parameters of your GD installation. There may also be support for some other file types available.
The result with GD now is also a graphic with a nice background image - of course still missing the gradients, which can be seen in all the other implementations.
Ming & Flash
The documentation of Ming in the PHP manual for the SWFBitmap class says, that it only accepts a special variant of JPEGs and so called DBL, which can be created out of PNGs. This is not true any more, even the API is still somehow strange, how you really can use images - and, of course, not really documented. You should remember that it may change again, as this extension is still alpha, so that this notes may not be valid any more, but let's take a look at the code.
public function addBitmap( Coordinate $position, $file, $width = null, $height = null )
{
// Try to create a new SWFBitmap object from provided file
$bitmap = new SWFBitmap( fopen( $file, 'rb' ) );
// Add the image to the movie
$object = $this->movie->add( $bitmap );
// Image size is calculated on the base of a tick size of 20, so
// that we need to transform this, to our tick size.
$factor = $this->modifyCoordinate( 1 ) / 20;
$object->scale( $factor, $factor );
// Scale added object, if dimensions wer provided
if ( ( $width !== null ) && ( $height !== null ) )
{
// We need the original image size, to calculate the additonal
// scale factor
$data = getimagesize( $file );
// Scale by ratio of requested and original image size
$object->scale(
$width / $data[0],
$height / $data[1]
);
}
// Move object to the right position
$object->moveTo(
$this->modifyCoordinate( $position->x ),
$this->modifyCoordinate( $position->y )
);
}
The "funny" thing about SWFBitmap currently is, that you need to pass a file resource of the input file to its constructor. Neither the file name, nor the complete binary contents of the file work. Using this you can definitely pass PNGs, may be some other image types, too.
After we added the bitmap to the movie, we receive a SWFDisplayItem in the $object variable again, which may be used to move and scale the just added stuff. When the bitmap has been added to the movie it remains in its original size, but only, when you use a twips pixel transition if 20:1. Depending on our twips factor in the method modifyCoordinate() we start scaling the image to its original size.
If the user specified width and height as method parameters, we additionally scale the image to fit these given boundings. At the end of the method, the bitmap is finally moved to its destination position in the graphic. The result looks like the one known from the cairo and SVG implementation.
Text rendering
Text rendering is a quite complex task in a graphic library, because normally the user passes a position, where the text should be rendered at, and some boundings defining the maximum space the text should use. While rendering you need to respect the requested size, which may be reduce to fit the text in the boundings, and a specified alignment, so that the text may be centered in the box.
This text fitting algorithms are common to all backends, so that this is the first time, the base class really proofs useful, besides defining the API, because we can implement the algorithms here, and reuse them in the backends. The most work in this chapter will actually be done in in the Base class.
We do not implement support for rotated texts, but this would only be one more parameter to the methods and each of the here mentioned backends supports this.
The base class
We create the base class in several steps, starting with the public API, then implementing the algorithm to fit strings into a box, and then, finally rendering the text with the specified text options.
Public API
As a first step, we define the public API we want to use to render texts.
/**
* Draw a single string.
*
* Draw a single string, with the exact given parameters. Do not asjust
* font size any more.
*
* @param string $string
* @param Coordinate $position
* @param int $size
* @param Color $color
* @return void
*/
abstract public function drawString( $string, Coordinate $position, $size, Color $color );
/**
* Draws a string
*
* Render a string or text at the specified position, using a box of the maximum specified size.
*
* The text will be drawn using the specified font size, or rendered
* smaller, if it does not fit into the box. The method will use at minimum
* use the font size specified in the class property $minFontSize. If the
* string still does not fit the box, the method will throw an exception.
*
* Using the last parameter you may specify the alignment of the text in
* the box. This may be a bitmask of the alignment constants in the base
* class. It defaults to a top left alignment.
*
* @param string $text
* @param Coordinate $position
* @param int $width
* @param int $height
* @param int $size
* @param Color $color
* @param int $align
* @return void
*/
public function drawText( $text, Coordinate $position, $width, $height, $size, Color $color, $align = 18 )
{
}
The first method just should render the given string at the given position, without performing any resizing or wrapping of texts. This method needs to be implemented by the backends, because it only does the necessary rendering.
The second method will be used to render longer texts, which should be wrapped, so that they fit into the given boundings, and the size of those texts may be reduced, until it fits into these boundings. Because we have boundings, we can also specify an alignment here, which consists of a bitmask of some class constants.
/**
* Alignment: Horizontally centered
*/
const ALIGN_CENTER = 1;
/**
* Alignment: Horizontally left
*/
const ALIGN_LEFT = 2;
/**
* Alignment: Horizontally right
*/
const ALIGN_RIGHT = 4;
/**
* Alignment: Vertically centered
*/
const ALIGN_MIDDLE = 8;
/**
* Alignment: Vertically top
*/
const ALIGN_TOP = 16;
/**
* Alignment: Vertically bottom
*/
const ALIGN_BOTTOM = 32;
Using this specification you may put your text at all 9 possible positions in the given box. The alignment defaults to Base::ALIGN_LEFT | Base::ALIGN_TOP, which equals 18. As a last publicly visible change we add two public properties to the Base class.
$minFontSize
This option limits the automatic reducing of the font size to some extend. The value of this property defaults to 6, which still should be somehow readable.
$lineSpacing
This options defines how much of the current font size should be used for the space between lines. You may also provide negative values here, and it defaults to .1.
Fit string into box
Obviuosly the first call in the drawText() method will go to a method, which will split up the text into lines, and return a font size, which will work with the given boundings. This method will be called getLineArrayFromString() and is also implemented in the Base class.
/**
* Adjust font size, until we find a way to fit string in the box
*
* The method will try fit the string in the box with the given font size,
* and reduce it further, if it does not fit, until the string fits.
*
* If not fitting font size could be found, the method will throw an
* exception.
*
* @param string $string
* @param Coordinate $position
* @param float $width
* @param float $height
* @param int $size
* @return array
*/
protected function getLineArrayFromString( $string, Coordinate $position, $width, $height, $size )
{
// Text cannot be rendered larger then the text box
$maxSize = min( $height, $size );
$result = false;
// Try to fit the stuff in the box, and reduce the font size until we
// are smaller then the minimal fornt size, or we found a way to fit
// the string in the box.
for ( $size = $maxSize; $size >= $this->minFontSize; )
{
$result = $this->testFitStringInTextBox( $string, $position, $width, $height, $size );
if ( is_array( $result ) )
{
// We found a match
return array(
$result,
$size,
);
}
// Reduce the font size otherwise
$size = ( ( $newsize = $size * ( $result ) ) >= $size ? $size - 1 : floor( $newsize ) );
}
// We could not find a working font size, so that we throw an exception
throw new Exception( "Could not fit string '$string' in box $width * $height with minimum font size {$this->minFontSize}." );
}
First, we reduce the maximum font size to the height of the given boundings - obviously a string with a larger font size would never fit. This is one of the first optimizations to save us some iterations.
After this we start a for loop, which starts with the maximum size, and reduces this size, until we found a fitting text size, or reached the minimum font size. To test, if a string of some size fits into the box, we call the helper method testFitStringInTextBox(), which will either return an array, which contains the text split up into lines, or some numeric factor. If we get an array, we found our matching font size and can just return it.
If we received a numeric return value it is either a float value lower then 1, which gives us a factor to reduce the font size with, or an integer. The float values are used, when the testFitStringInTextBox() detected, that the font size needs a drastically reduction, so that we save some iterations again. The other possible value is the int value 1, which means, that we reduce the font size by only 1.
If we did not find a matching font size larger or equal the minimum font size, we just throw an exception at the very end of the method, so that the user knows, that he is required to change something here.
Interested in, what the testFitStringInTextBox() method does? Here it is:
/**
* Test if string fits in a box with given font size
*
* This method splits the text up into tokens and tries to wrap the text
* in an optimal way to fit in the Box defined by width and height.
*
* If the text fits into the box an array with lines is returned, which
* can be used to render the text later:
* array(
* // Lines
* array( 'word', 'word', .. ),
* )
* Otherwise the function will return a factor to reduce the font size.
*
* @param string $string
* @param Coordinate $position
* @param float $width
* @param float $height
* @param int $size
* @return mixed
*/
protected function testFitStringInTextBox( $string, Coordinate $position, $width, $height, $size )
{
// Tokenize String
$tokens = preg_split( '/\s+/', $string );
$initialHeight = $height;
$lines = array( array() );
$line = 0;
foreach ( $tokens as $nr => $token )
{
// Add token to tested line
$selectedLine = $lines[$line];
$selectedLine[] = $token;
$boundings = $this->getTextBoundings( $size, implode( ' ', $selectedLine ) );
$boundingsWidth = $boundings[0];
// Check if line is too long
if ( $boundingsWidth > $width )
{
if ( count( $selectedLine ) == 1 )
{
// Return false if one single word does not fit into one line
// Scale down font size to fit this word in one line
return $width / $boundingsWidth;
}
else
{
// Put word in next line instead and reduce available height by used space
$lines[++$line][] = $token;
$height -= $size * ( 1 + $this->lineSpacing );
}
}
else
{
// Everything is ok - put token in this line
$lines[$line][] = $token;
}
// Return false if text exceeds vertical limit
if ( $size > $height )
{
return 1;
}
}
// Check width of last line
$boundings = $this->getTextBoundings( $size, implode( ' ', $lines[$line] ) );
$boundingsWidth = $boundings[0];
if ( $boundingsWidth > $width )
{
return 1;
}
// It seems to fit - return line array
return $lines;
}
This method basically splits up a string in so called tokens, which are just the words in the given string, separated by whitespace, and tries to put as many words, as possible, in one line, continuing with the next line, until the vertical limit of the box is reached. If the limit is not reached at the end, the string obviously fits the box.
During this process the array with the lines, and for each line an array with the words fitting in this line, is stored, so that we can return this line array at the end - to store the word to line mapping and reuse it during the rendering process.
To know how much space a single line, with some amount of words, currently consumes the backends need to implement the here called method getTextBoundings(). This method returns an array with the width and height this string would consume, when rendered. This is, of course, highly dependant on the backend and its string rendering methods.
Render the text
Now we just need to implement the drawText() method, which of course makes use of the just introduced methods.
public function drawText( $text, Coordinate $position, $width, $height, $size, Color $color, $align = 18 )
{
// Get text split into lines, and a fitting text size
list( $lines, $size ) = $this->getLineArrayFromString(
$text, $position, $width, $height, $size
);
// Calculate height of text lines
$completeHeight = count( $lines ) * $size + ( count( $lines ) - 1 ) * $this->lineSpacing;
// We will modify the position during the text rendering - to not
// affect outer reuse of the position, we clone it first
$position = clone $position;
// Calculate vertical offset depending on text alignment
switch ( true )
{
case $align & Base::ALIGN_MIDDLE:
$position->y += ( $height - $completeHeight ) / 2;
break;
case $align & Base::ALIGN_BOTTOM:
$position->y += $height - $completeHeight;
break;
}
foreach ( $lines as $lineTokens )
{
// Merge line tokens again
$line = implode( ' ', $lineTokens );
// Get length of current line, to be able to align it correctly
if ( $align & ( Base::ALIGN_CENTER | Base::ALIGN_RIGHT ) )
{
$lineWidth = $this->getTextBoundings( $size, $line );
$lineWidth = $lineWidth[0];
}
// Calculate string root position depending on the horizontal text alignment
switch ( true )
{
case $align & Base::ALIGN_CENTER:
$linePosition = new Coordinate(
$position->x + ( $width - $lineWidth ) / 2,
$position->y
);
break;
case $align & Base::ALIGN_RIGHT:
$linePosition = new Coordinate(
$position->x + $width - $lineWidth,
$position->y
);
break;
default:
$linePosition = $position;
break;
}
// Draw string at the calculated position with the calculated size
$this->drawString( $line, $linePosition, $size, $color );
// Move current position to next line
$position->y += $size + $size * $this->lineSpacing;
}
}
This method is easier, than it may look like at the first glance. Of course, we use the prior defined method getLineArrayFromString() to get a working font size and the separation of the text into lines. Once we know the number of lines required to render the text, we can calculate the height the text box will actually consume, and store it in the variable $completeHeight.
With the value in $completeHeight we can calculate the offset required for the vertical alignment and modify the text drawing position accordingly. The first switch statement in this method checks for the vertical spare value and modifies the y root position of the text depending on the alignment.
After this we iterate over the lines to actually draw them. For this we first combine the word tokens back to a string. The horizontal alignment, of course, needs to be recalculated for each line, depending on the length of the current line. To calculate the space such a line will consume, we again use the backend specific method getTextBoundings() and then modify the $linePosition depending on the horizontal alignment and the width of the current line.
Now we got the real position where the line string should be rendered at, so that we can call the method drawString(), which will be implemented by the backends. Finally, in the iteration, we need to move the rendering position to the next line, taking the $lineSpacing into account.
With the implementation in the base class we can now start with the backends, which now only need to implement two methods, drawString() and getTextBoundings().
DOM & SVG
I already mentioned in the overview of SVG, that we will get some rendering problems here, so let's again take a look at the implementation first.
/**
* Return boundings for the given string
*
* Return the boundings of the given string, depending on the selected
* font, the given string and the given text size.
*
* The boundings are returned as an array like:
* array(
* 0 => (float) width,
* 1 => (float) height,
* )
*
* @param int $size
* @param string $string
* @return array
*/
protected function getTextBoundings( $size, $string )
{
return array(
// We guess, that a common char width is roughly .55 * $size
$size * strlen( $string ) * .55,
$size
);
}
/**
* Draw a single string.
*
* Draw a single string, with the exact given parameters. Do not asjust
* font size any more.
*
* @param string $string
* @param Coordinate $position
* @param int $size
* @param Color $color
* @return void
*/
public function drawString( $string, Coordinate $position, $size, Color $color )
{
// Finally add text nodes to SVG document
$textNode = $this->dom->createElement( 'text', $string );
$textNode->setAttribute( 'x', sprintf( '%.4F', $position->x ) );
$textNode->setAttribute( 'y', sprintf( '%.4F', $position->y + $size * .85 ) );
$textNode->setAttribute(
'style',
sprintf(
'font-size: %dpx; font-family: %s; fill: #%02x%02x%02x; fill-opacity: %.2F; stroke: none;',
$size,
$this->font,
$color->red,
$color->green,
$color->blue,
1 - ( $color->alpha / 255 )
)
);
if ( $this->textGroup !== null )
{
// This method has been called out of the context of text
// rendering, so that we add the string to the text group.
$this->textGroup->appendChild( $textNode );
}
else
{
// This method has been called out of the public API, the string is
// added directly in the element node.
$this->elements->appendChild( $textNode );
}
}
/**
* Draws a string
*
* Render a string or text at the specified position, using a box of the maximum specified size.
*
* The text will be drawn using the specified font size, or rendered
* smaller, if it does not fit into the box. The method will use at minimum
* use the font size specified in the class property $minFontSize. If the
* string still does not fit the box, the method will throw an exception.
*
* Using the last parameter you may specify the alignment of the text in
* the box. This may be a bitmask of the alignment constants in the base
* class. It defaults to a top left alignment.
*
* @param string $text
* @param Coordinate $position
* @param int $width
* @param int $height
* @param int $size
* @param Color $color
* @param int $align
* @return void
*/
public function drawText( $text, Coordinate $position, $width, $height, $size, Color $color, $align = 18 )
{
// Create group for the text lines
$this->textGroup = $this->dom->createElement( 'g' );
$this->elements->appendChild( $this->textGroup );
// Let the method in the base class do the actual rendering
parent::drawText( $text, $position, $width, $height, $size, $color, $align );
// Reset text group
$this->textGroup = null;
}
I am sure you already notice the code flaw, by taking a first look. The getTextBoundings() implementation looks plain wrong. The problem here is, that by default the rendering client actually selects the used font and performs the text rendering. Some clients like Firefox may additionally limit, or increase, the text size beyond the specified values, so that can't be sure, how large the text will be rendered. So that we just guess here, that - with a common font - an average character has the ratio .55 : 1 for width to height.
There are tools available, which can create SVG paths out of a available font and some string. This would allow you to calculate the real size of some string, and specify exactly how this text will be rendered, because the rendering client does not know any more, that this is text, but only sees some path.
This can be considered as a benefit, but this also means, that somebody who wants to modify the SVG at a later stage again, cannot easily modify the text, to fix some typos, or similar.
Besides this, the implementation is quite simple - let's take a first look at the overwritten drawText() method. If we draw multiple lines of text, we want them included in one SVG group element, so that, if one further modifies the document in some editor, the elements are grouped, and can be modified together.
To make this possible, we add some implicit state to the SVG object, by creating a new group before the parent method from the Base class renders the strings. After this we reset the group to null. This group, if existing, can now be used in the drawString method, when called by the Base implementation of drawText().
The drawString() method itself is really simple, we just create a new text node, assign style and position, containing the string, which should be drawn.
You may want to escape XML special chars here, because they may brake the XML document otherwise. Since we do not handle the input charset anywhere in this code, and do not have any knowledge about this here, I skip this in this example implementation.
At the end of the method the created text node is added either directly to the element node, if the drawString() has been called directly, or to the created group, when it is called out of the context of the drawText() method. To use this new created feature, we add one more call to the backend to the used example.
$graphic = new Svg( 150, 150 );
$graphic->addBitmap(
new Coordinate( 50, 129 ),
'bitmap.png'
);
$graphic->drawPolygon(
// ...
);
$graphic->drawText(
'SVG rocks!',
new Coordinate( 25, 60 ),
70, 68,
32,
new Color( '#2e343655' ),
Base::ALIGN_CENTER | Base::ALIGN_MIDDLE
);
$graphic->save( 'images/example_svg_06.svg' );
As you can see from the example, we want to render the string "SVG rocks!" in the bounding matching the cubes front side. We use a gray half transparent color for this, and define, that the text should horizontally and vertically centered in these boundings. The result may not really look like you expected, though.
The vertical alignment worked perfectly, but the horizontal alignment failed because of the inaccurate estimation in the getTextBoundings() method. Even you may enhance the guessing by providing better values for some character groups, this will never work really well, without converting the texts to paths, as mentioned above.
GD & PNG
This is the first time, the GD library works really well. We only use the ttf-functions which base on the FreeType2 library. These functions have a nice anti aliasing and you can calculate the text boundings exactly.
/**
* Return boundings for the given string
*
* Return the boundings of the given string, depending on the selected
* font, the given string and the given text size.
*
* The boundings are returned as an array like:
* array(
* 0 => (float) width,
* 1 => (float) height,
* )
*
* @param int $size
* @param string $string
* @return array
*/
protected function getTextBoundings( $size, $string )
{
// We only use the imagettf* functions here...
$boundings = imagettfbbox( $size, 0, $this->font, $string );
return array(
// For the returned boundings array by imagettfbbox check the
// documentation
$boundings[4] - $boundings[0],
$boundings[5] - $boundings[1],
);
}
/**
* Draw a single string.
*
* Draw a single string, with the exact given parameters. Do not asjust
* font size any more.
*
* @param string $string
* @param Coordinate $position
* @param int $size
* @param Color $color
* @return void
*/
public function drawString( $string, Coordinate $position, $size, Color $color )
{
// Merge images
$this->mergeCanvas();
// Finally we draw the text
imagettftext(
$this->destination,
$size,
0,
$position->x,
$position->y + $size,
$this->allocate( $color ),
$this->font,
$string
);
}
The implementation of getTextBoundings() using the GD function imagettfbbox() returns exact results, even the return values may look strange at the first glance, but we can easily convert it to the array structure we expect.
As the imagettf* functions already do nice anti aliasing, so we do not want supersampling for the texts, or the texts would look quite blurry again. Because of this, we merge the active canvas again, like described in the previous section on bitmaps, and render the text directly on the destination image. The result now looks really nice, except it is still missing the gradients.
The GD libarary also supports some other font libraries, so that you may want to extend both methods with some more magic. Depending on the features of the current GD installation and the provided font you should be able to switch between the shown FreeType 2 library, the native TTF rendering and the rendering of PostScript Type 1 fonts, which do not support anti aliasing.
Cairo & PNG
As always there is no real problem implementing this with cairo, so just let's take a look at the implementation of the two required methods.
/**
* Return boundings for the given string
*
* Return the boundings of the given string, depending on the selected
* font, the given string and the given text size.
*
* The boundings are returned as an array like:
* array(
* 0 => (float) width,
* 1 => (float) height,
* )
*
* @param int $size
* @param string $string
* @return array
*/
protected function getTextBoundings( $size, $string )
{
::cairo_select_font_face( $this->context, $this->font, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL );
::cairo_set_font_size( $this->context, $size );
$extents = ::cairo_text_extents( $this->context, $string );
return array(
$extents['width'],
$extents['height'],
);
}
/**
* Draw a single string.
*
* Draw a single string, with the exact given parameters. Do not asjust
* font size any more.
*
* @param string $string
* @param Coordinate $position
* @param int $size
* @param Color $color
* @return void
*/
public function drawString( $string, Coordinate $position, $size, Color $color )
{
::cairo_select_font_face( $this->context, $this->font, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL );
::cairo_set_font_size( $this->context, $size );
$this->setStyle( $color, true );
::cairo_move_to( $this->context, $position->x, $position->y + $size * .85 );
::cairo_show_text( $this->context, $string );
}
In the implementation of getTextBoundings() the cairo function cairo_text_extents() returns the space consumed by some string together with some other information about the drawn text. This information can easily be used to return the required information about width and height of the string.
Cairo may also load common fonts, like FreeType fonts and Win32 fonts. In this example we use the integrated fonts, like "Sans" in this example. You may of course handle those other fonts, too - but this is not required for this example implementation.
To actually draw the string in the method drawString(), we again select the same font, set the style reusing the known custom method, move our cursor position, to where the text should be drawn and finally render the text. The Cairo API again is quite intuitive, and renders a perfect result.
Ming & Flash
Flash is also a vector graphic format, so that we basically should have the same problems here, we also had in SVG, but there is a solution for flash. You can embed the shapes of the used characters in the delivered file, so that the client knowns how to render those characters, and the file is displayed the same everywhere. The other "benefit" is, that there is only one real flash client out there, so you can easily test how the graphic will look like.
/**
* Return boundings for the given string
*
* Return the boundings of the given string, depending on the selected
* font, the given string and the given text size.
*
* The boundings are returned as an array like:
* array(
* 0 => (float) width,
* 1 => (float) height,
* )
*
* @param int $size
* @param string $string
* @return array
*/
protected function getTextBoundings( $size, $string )
{
$text = new SWFText();
$text->setFont( new SWFFont( $this->font ) );
$text->setHeight( $size );
return array(
$text->getWidth( $string ),
$size
);
}
/**
* Draw a single string.
*
* Draw a single string, with the exact given parameters. Do not asjust
* font size any more.
*
* @param string $string
* @param Coordinate $position
* @param int $size
* @param Color $color
* @return void
*/
public function drawString( $string, Coordinate $position, $size, Color $color )
{
// Finally add textbox to SWF movie
$textBox = new SWFTextField( SWFTEXTFIELD_NOEDIT );
$textBox->setFont( new SWFFont( $this->font ) );
$textBox->setHeight( $this->modifyCoordinate( $size ) );
$textBox->setColor( $color->red, $color->green, $color->blue, 255 - $color->alpha );
$textBox->addString( $string );
$textBox->addChars( $this->usedChars );
$object = $this->movie->add( $textBox );
$object->moveTo(
$this->modifyCoordinate( $position->x ),
$this->modifyCoordinate( $position->y - .15 * $size )
);
}
There are two ways to add text to a flash image, on the one hand there is the SWFText class, and on the other the SWFTextField class. As you can see, I am using both here, so where are the differences?
SWFText
This class has the method getWidth(), so you are able to receive the space consumed by the string. Both classes allow to set the font using a SWFFont object, constructed of the the special font type - the fdb-fonts. But the SWFText class does not allow the above mentioned embedding of the character shapes, so that the actual rendering depends on the fonts available at the viewers environment.
SWFTextField
This class does not allow the calculation of consumed space by some string, but has other features we require for the actual text rendering, like the embedding of the character shapes. It also has some other features, like the optional possibility for the viewer to change the displayed texts. Of course we disable this for out graphics, as you can see in the constructor. Sadly the SWFTextField currently does not support UTF-8 characters, but this may change some time in the future.
After the explanations of the two used classes, there is one more thing to mention. The characters, which are actually embedded into the flash file do not depend on the used characters, but on the characters you set on the first call to addChars() on the SWFTextField object.
To work with this limitation, we have two choices:
Wait with actual text rendering until the file is saved.
This is the way I go in ezcGraph, because the texts should always be on top, so I can just wait with the text rendering, collect all chars, and render the texts into the movie just before the movie is saved.
Add all chars, which will be used on the first call.
In a more general purpose library we may want text in the graphic below some other shapes, so that I added a class property $usedChars, which contains several characters, which are added on the first call. If you intend to use some strange characters, you should modify its contents before the first class to drawString(), or drawText().
If you miss some characters, they will just not appear in the final image.
The reuslt now looks the same, as in the other libraries.
Tools for image creation
At this stage we now got a quite complete and nice abstraction layer, but the very simple API, limited to polygons, bitmaps and texts makes this still not really usable. In this last chapter I want to show some simple tools, to enhance this, using the existing functionality in the backends.
We will create a Tools class, which will provide static methods to create various shapes, which can then be rendered by the backends. Additionally we will provide some methods to modify existing shapes.
The polygons
For now the polygons were just defined by an array of points. This is always sufficient, but starting with this chapter we want to be able to call methods on those polygons, to modify it. For this we create a new class called Polygon, which just contains one array with the Coordinate objects spanning the polygon, and extending the ArrayObject class from SPL to offer easy access to this structure.
/**
* Simple class extending ArrayObject, containing Coordinate objects, defining
* a polygon.
*
* @version //autogen//
* @author Kore Nordmann <kore@php.net>
* @license Copyright by Kore Nordmann
*/
class Polygon extends ArrayObject
{
/**
* Array of points defining the polygon
*
* @var array(Coordinate)
*/
protected $points;
/**
* Construct Polygon from Coordniates
*
* The Polygon constructor accepts any amount of Coordinate objects, which
* define the polygon.
*
* @return void
*/
public function __construct()
{
$this->points = array();
// Add all given Coordinate objects as points to point array
$points = func_get_args();
foreach ( $points as $point )
{
// Only add Coordinate objects
if ( $point instanceof Coordinate )
{
$this->points[] = $point;
}
}
// Construct the parent constructor of the ArrayObject from the points
// array.
parent::__construct( $this->points );
}
/**
* Overloaded append method to ensure only Coordinate objects are added
*
* Overloaded append method to ensure only Coordinate objects are added
*
* @param Coordinate $value
* @return void
*/
public function append( $value )
{
if ( ! $value instanceof Coordinate )
{
throw new Exception( 'Only Coordinate objects may be addded to a polygon.' );
}
parent::append( $value );
}
/**
* Overloaded offsetSet method to ensure only Coordinate objects are added
*
* Overloaded offsetSet method to ensure only Coordinate objects are added
*
* @param Coordinate $value
* @return void
*/
public function offsetSet( $index, $newval )
{
if ( ! $newval instanceof Coordinate )
{
throw new Exception( 'Only Coordinate objects may be addded to a polygon.' );
}
parent::offsetSet( $index, $newval );
}
}
As you can see, we only overload three methods here, to fit our purpose. The constructor should accept any amount of Coordinates as parameters, so it is usable in the same way as before. Both methods, which allow to add or modify values to the ArrayObject now implement additional type checks, so that you may only add Coordinate objects, and nothing else. This is another nice side effect, we now have the possibility to ensure the type of the contents, and it is not possible any more, that we end up with some wrong values in the polygon.
Once we got this structure, we can now start implementing modification on those.
Transformation matrices
We already came in touch with transformation matrices in some of the backends, but now it is time to explain what they do, because we will implement them ourselves, to make it possible to move shapes around, rotate them, and so on.
The coordinates we got spread all over the code define points in the two dimensional coordinate vector. As described above, a coordinate is nearly equivalent to its location vector, also consisting of the same two elements. But a vector can now be multiplied with a matrix, and we get a vector as a result. For us this means, we have a coordinate (vector), multiply it with something (matrix), and get back a modified coordinate (vector). And there are several nice things about matrices, which makes this really useful for us.
You can easily specify a matrix, that will just add something to both coordinate values, which semantically means, that we just move the coordinate (translation). You may also easily specify a matrix, that rotates the coordinate by some angle around the center point of your coordinate system - same for all other imaginable transformations, like scaling, sheering, etc.
Having those single matrices, you may multiply them with each other, and you still have a matrix, which now contains all the multiplied transformations in the given order, and you can multiply a coordinate with it which now will be transformed by all these single transformations at once.
$transformation = $translation * $scaling * $rotation * $translation;
$coordinate *= $transformation;
Take a look at this short example. We create one transformation matrix out of four existing matrices, and when multiplied with the coordinate this has exactly the same effect, as the following code would have.
$coordinate *= $translation;
$coordinate *= $scaling;
$coordinate *= $rotation;
$coordinate *= $translation;
This feature of matrices does not only make your code more readable, but you can easily stack sets of transformations this way and optimize the transformations, by reducing the number of required multiplications. As you may notice, matrix multiplication is not commutative.
This is everything you need to know about matrices to use them, if you want to get some more in depth knowledge, you may want to read the wikipedia articles on this topic.
Matrix Implementation
To reduce the required knowledge about creating matrices, we will provide some useful helper methods, so you will be able to just create a rotation matrix by specifying the angle and do not need to worry about its internal structure.
You may check out the actual code from the provided code archive, but as I do not want to tell you how to create and multiply matrices here, I will just skip those code examples. For matrix and vector calculation an extension like pecl/operator, which allows operator overloading would be really useful. But as it wont ever go into the PHP core the code for creating a matrix would look like:
$matrix = Matrix::createTranslationMatrix( 10, 12 );
$matrix = $matrix->multiply(
Matrix::createScaleMatrix( 2, .5 )
);
And after you created such a stacked matrix you can modify coordinates or polygons using:
$polygon->transform( $matrix );
Since there are multiple objects which should be transformed, let's start implementing this.
Transformable
We define, that all objects in our scene, which can be transformed in some way using the given transformation matrices, should implement the interface Transformable, which is defined like:
namespace kn::Graphic;
/**
* Define interfaces
*/
interface Transformable
{
/**
* Transform using the given transformation matrix.
*
* Transform using the given transformation matrix.
*
* @param Matrix $matrix
* @return Transformable
*/
public function transform( Matrix $matrix );
}
We just require the implementation of one method, transform(), which takes a Matrix as a parameter. This method should again return a Transformable, to be able to stack the calls.
Polygon implementation
As mentioned above, the polygon class is the primary class, which should implement this interface, and the implementation looks like:
/**
* Transform polygon using the given transformation matrix.
*
* Transform polygon using the given transformation matrix.
*
* @param Matrix $matrix
* @return Polygon
*/
public function transform( Matrix $matrix )
{
// A transformation of a polygon just means, that all its coordinates
// are transformed.
foreach( $this->points as $point )
{
$point->transform( $matrix );
}
return $this;
}
If you want to transform a polygon, you just need to transform all its points, spanning the polygon. So there is nothing special here, but we got one more class, which is required to implement this interface.
Coordinate implementation
Coordinates are the atom of graphics, so here we go with some actual implementation.
/**
* Transform using the given transformation matrix.
*
* Transform using the given transformation matrix.
*
* @param Matrix $matrix
* @return Coordinate
*/
public function transform( Matrix $matrix )
{
// Vector matrix multiplication
$this->x = $this->x * $matrix->getValue( 0, 0 ) +
$this->y * $matrix->getValue( 1, 0 ) +
$matrix->getValue( 2, 0 );
$this->y = $this->x * $matrix->getValue( 0, 1 ) +
$this->y * $matrix->getValue( 1, 1 ) +
$matrix->getValue( 2, 1 );
return $this;
}
To multiply a two dimensional coordinate with a 3x3 matrix, we use to store the transformations, we need to assume a third value in the vector, which defaults to 1. You may remember from matrix multiplication, the number of columns of the first matrix must equal the number of rows in the second matrix, so that is not possible to multiply a coordinate, even with three values, with a 3x3 matrix. But as said before, we can consider a coordinate as its location vector, so that we can actually multiply them.
The implementation above just does, what is defined by the matrix definition, but reduced to the values we actually require. Consider this as a slight optimization. For a complete implementation of matrix multiplication take a look at the Matrix class.
Other implementations
I skip this here, but you may also want to let other structures we use implement Transformable, which are defined by some coordinates, like the radial gradients. This way you could also transform them, which may make sense in some applications.
Usage
So, after we made all those structures transformable, we should use this new feature in our code. The following example is only implemented using the cairo backend, but it would, of course, also work with the other backends.
$graphic = new Cairo( 150, 150 );
$graphic->addBitmap(
new Coordinate( 50, 129 ),
'bitmap.png'
);
$polygon = new Polygon(
new Coordinate( 75, 75 ), new Coordinate( 75, 10 ), new Coordinate( 85, 30 )
);
$transformation = Matrix::createRotationMatrixAroundPoint( -10, new Coordinate( 75, 75 ) );
for ( $i = 0; $i < 360; $i += 10 )
{
$graphic->drawPolygon(
$polygon,
new Color( '#2e35367f' ),
true
);
$graphic->drawPolygon(
$polygon,
new Color( '#eeeeef7f' ),
false
);
$polygon->transform( $transformation );
}
$graphic->save( 'images/example_cairo_07.png' );
We again create the canvas in the first line, and add a small background image. To create a polygon we now have to use 'new Polygon' instead of 'array', but else the API stays the same.
Beside the above mentioned convenience factories for transformation matrices, I also created a factory for a more complex matrix, which rotates a shape around any point in the graphic, besides the center point, which I use here.
With this polygon and the transformation matrix, I start a for loop, which once draws a filled black polygon, then a white border around it. After this, the polygon is transformed using the above defined matrix. With such code we can easily create beautiful fractals.
Tool class
As promised, we also want to create a tool class, which makes creating shapes easier. The static methods only need to return a Polygon object, which then can be rendered transformed etc.
namespace kn::Graphic;
/**
* Class providing static methods to create some shapes
*
* @version //autogen//
* @author Kore Nordmann <kore@php.net>
* @license Copyright by Kore Nordmann
*/
class Tools
{
// Static methods...
}
So let's start with a rectangle
Rectangle
A rectangle is somehow the simplest possible and easiest shape, but there are several possible ways to construct it. We want to construct from one of its edge points and its width and height.
/**
* Create a rectangle
*
* Create a rectangle from a position, in combination with its width and
* height.
*
* @param Coordinate $position
* @param float $width
* @param float $height
* @return Polygon
*/
public static function rectangle( Coordinate $position, $width, $height )
{
return new Polygon(
new Coordinate( $position->x, $position->y ),
new Coordinate( $position->x + $width, $position->y ),
new Coordinate( $position->x + $width, $position->y + $height ),
new Coordinate( $position->x, $position->y + $height )
);
}
With those parameters available, we can simply create the Polygon, with four edges for the four edges of the rectangle. Simple, isn't it?
Ellipse sector
Ellipses and especially ellipse sectors are an example for a far more complex shape. As said in the section about polygon size reduction, it is quite hard to reduce the size of a real ellipse sector, so that we just use a polygon to roughly interpolate the ellipse sector and our already implemented algorithm works just fine. It also may look bad if you zoom in the vector formats and detect that the outer border is not round, but consists of a lot short lines.
/**
* Create a ellipse sector
*
* Create a ellipse sector from a center point, the width and height of the
* ellipse, a start angle and end angle. You may optionally specify a
* resolion for the circle to polygon conversion.
*
* @param Coordinate $center
* @param float $width
* @param float $height
* @param float $startAngle
* @param float $endAngle
* @param int $resolution
* @return Polygon
*/
public static function circleSector( Coordinate $center, $width, $height, $startAngle, $endAngle, $resolution = 1 )
{
$polygon = new Polygon( $center );
// Convert all angles to radian values
$startAngle = deg2rad( $startAngle );
$endAngle = deg2rad( $endAngle );
$resolution = deg2rad( $resolution );
// We just need the horizontal and vertical radius
$width /= 2;
$height /= 2;
// Add points defining the ellipse
for ( $angle = $startAngle; $angle < $endAngle; $angle += $resolution )
{
$polygon->append( new Coordinate(
$center->x + sin( $angle ) * $width,
$center->x + cos( $angle ) * $height
) );
}
// Draw a last point at the end angle
$polygon->append( new Coordinate(
$center->x + sin( $endAngle ) * $width,
$center->x + cos( $endAngle ) * $height
) );
return $polygon;
}
A more complex shape, of course, also requires more complex creation code. We start the polygon with the center point of the ellipse, and then iterate over the outer border in small steps, defined by the $resolution parameter. After we added the coordinate for the last point on the outer border, we can return the completed polygon.
Usage
With this basic tool class, we now got static constructors for easy construction of more complex shapes, which can of course be used again in the code, and also be transformed, as they are only polygons.
$graphic = new Cairo( 150, 150 );
$graphic->drawPolygon( Tools::rectangle( new Coordinate( 10, 10 ), 130, 130 ), new Color( '#2e3436' ) );
$graphic->drawPolygon( Tools::circleSector( new Coordinate( 90, 90 ), 80, 30, 25, 288 ), new Color( '#f57900' ) );
$rectangle = Tools::rectangle( new Coordinate( 20, 20 ), 110, 10 );
$transformation = Matrix::createRotationMatrix( 10 );
for ( $i = 0; $i < 90; $i += 10 )
{
$graphic->drawPolygon(
$rectangle,
new Color( '#fcaf3cb0' ),
true
);
$rectangle->transform( $transformation );
}
$graphic->save( 'images/example_cairo_08.png' );
The construction of the shapes using the Tools class is quite well readable, even for the unexperienced reader. For one of the rectangles we again create a rotation matrix, so you can see that the transformations still work.
Extensions
The possible extensions of the tool class are endless, because of the unlimited amounts of available shapes. So, just add there, whatever you need.
Shape arithmetics
A very useful, but quite complicated extensions would be basic shape arithmetics, but we limited the classes to polyogns, for now, so this simplifies this a bit.
Bsaic shape arithmetics would mean, that you are able to combine or intersect polygones, which could be really useful in several cases. Another problem you'll get here is, that the diff of one polygon with another may result in "holes" in polygones, which are not possible with out current polygon structure.
But I would welcome such an extension, and implementing this could refresh your mathematical basics and your knowledge of linear algebra.
Subscribe to updates
There are multiple ways to stay updated with new posts on my blog: