Image creation with PHP

First published at Tuesday 11 December 2007

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

Image creation with PHP

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

Introduction

General notes, relevant for this article.

Terms

Some terms I use in a special way.

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

Agenda

This is the fourth part of this article, which describes the integration of bitmaps with the various backends.

  • Formats

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

  • Simple shapes

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

  • Gradients

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

  • Integrated bitmaps

    Integrating bitmaps with the backends.

  • Text rendering

    The basics of text rendering with all extensions.

  • Additional image tools

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

The code

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

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

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

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

  • GD (with PNG and FreeType 2 support)

  • cairo_wrapper 0.2.3-beta

  • Ming 0.3.0

  • The default extensions: DOM, SPL

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' );
Your browser needs to support SVG to display this image. SVG cube with image

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.

Cairo cube with imageCairo cube with image

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.

GD cube with imageGD cube with image

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.

Your browser needs to support Flash contents to display this image. Flash cube with image

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.

Subscribe to updates

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