Image creation with PHP

First published at Tuesday, 11 December 2007

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

Image creation with PHP

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

Introduction

General notes, relevant for this article.

Terms

Some terms I use in a special way.

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

Agenda

The third part of this article makes the images a bit more colorful, by providing support for gradients in the graphics.

  • 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

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.

Your browser needs to support SVG to display this image. SVG cube

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.

GD cubeGD cube

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.

Cairo cubeCairo cube

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.

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

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.

Subscribe to updates

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