Skip to content

Creating PDFs with PHP, part 4: Images

Now that we can draw in our PDF, we want to add images. There are two kinds of images, bitmapped and vector. In PDF, images are called XObjects (The X stands for external, meaning defined outside the page). Vector images are easier, since they are just packages of PDF drawing commands, a sort of macro. PDF calls them Forms, since they were originally used to draw the boxes on a printed form.

See the code.

Vector graphics

Using an XObject Form involves three parts:

Define the image
Create a content stream with the drawing commands, like:
6 0 obj
<<
/Subtype /Form % defines the type of image
/Resources 2 0 R % just like the resource dictionary for a page; no reason not to use the same one
/BBox [0 0 40 40] % the bounding box of the image, as [left bottom right top]. No reason not to use [0 0 width height]
/Length 47
>>
stream
5 w
0 0 40 40 re % draw a black box
0 0 0 RG S
endstream
endobj
Define a name for the image
In the resource dictionary, determine a name for the image (this is arbitrary) in the /XObject subdictionary:
2 0 obj
<<
  % ... other resource definitions
  /XObject << 
    /Image1 6 0 R % the name of object 6, defined above
    % ... other images
  >>
>>
Use the image
In the content stream for the page, include the command
/Image1 Do

The question is obvious: how do you tell PDF where on the page to draw the image? The answer is, you don't. The drawing commands draw a box from (0,0) to (40,40), and that's where it's drawn. But you object: that makes them worthless!

The trick is to redefine the coordinate system so that (0,0) ends up where you want. PDF uses affine transformations:

x? = ax+cy+e
y? = bx+dy+f

where x is the x-coordinate in the new coordinate system (where the image "thinks" its drawing) and x? is the x-coordinate in the original coordinate system. So to put the image with the lower left corner (its (0,0) point) at (50,100), you would want to have x? = x+50 and y? = y+100, or a=1 b=0 c=0 d=1 e=50 f=100. The PDF command for that is cm (for coordinate matrix), so the drawing command would be:

1 0 0 1 50 100 cm
/Image1 Do

Coordinate transformations are cumulative, so repeating the translation for another image would push it too far. You want to reset the transformation matrix before each drawing. You could calculate the inverse matrix (for the translation above, it's easy: 1 0 0 1 -50 -100 cm, but other transformations are more complicated) but PDF has an easier way: a graphics state stack. q pushes the current graphics state, including the transformation matrix, and Q pops it, so the final drawing command would be:

q
1 0 0 1 50 100 cm
/Image1 Do
Q

Other transformations are possible, and the work of getting the math right has already been done:

Translation of the origin to (x,y)
As above, 1 0 0 1 x y cm.
Rotation around the origin by angle theta
cos(theta) sin(theta) –sin(theta) cos(theta) 0 0 cm.
Scale (change the size of) the x-direction by a factor xscale and the y-direction by yscale
xscale 0 0 yscale 0 0 cm

These do the transformation around the origin (and doing them in the above order has the expected results; doing the transforms in some other order means translating in scaled units or along rotated axes, which is rarely what you want). More often, we want to place and rotate around the center of the image, so after the transformation, translate backwards half the height and width (in the new coordinate system): 1 0 0 1 -width/2 -height/2 cm.

Bitmapped graphics

Bitmapped images are simply streams of bits, with a given color space (grayscale, CMYK or RGB) and a given number of bits per color component (grayscale has one component, CMYK has four and RGB has three). The easiest to use is RGB, with 8 bits per component, since that translates directly from the PHP image routines.

Using a bitmapped image is exactly the same as for a vector image, except the content stream is different:

7 0 obj
<<
/Subtype /Image % defines the type of image
/Width 50 % no bounding box, just a width and height
/Height 50
/ColorSpace /DeviceRGB
/BitsPerComponent 8
/Length 2500
>>
stream
...generate the bytes
endstream
endobj

And "generate the bytes" for a PHP image is straightforward:

for ($row = 0; $row < $height; ++$row) for ($col = 0; $col < $width; ++$col){
  $colorindex = imagecolorat($im, $col, $row);
  $colors = imagecolorsforindex($im, $colorindex);
  $image->contents .= sprintf('%c%c%c', $colors['red'], $colors['green'], $colors['blue']);
}

There is a huge gotcha here: the image is always scaled to be 1 pixel square. I don't know why; the width and height are there in the image definition, but that's the way it is. So you have to scale the image with width 0 0 height 0 0 cm (or multiply width and height by some scale factor) before displaying with /bitmappedImage1 Do. Then, since the units are now scaled, to place and rotate around the center you have to use 1 0 0 1 -0.5 -0.5 cm (half the original (1x1) sizes). Trust me, this works.

The final code is pretty simple.

Example

See the example.

Create the apple vector image:

$pdf->newForm();
// http://en.wikipedia.org/wiki/File:Apple_logo_black.svg, same as last example but now going into an Xform
$pdf->moveto(28.70919,92.37034);
$pdf->curveto(32.22477,92.37025,36.46696,91.64368,41.43575,90.19065);
$pdf->curveto(46.45132,88.73743999999999,49.77944,88.01088,51.42013,88.01096);
$pdf->curveto(53.52944,88.01088,56.97475,88.83118999999999,61.756057,90.4719);
$pdf->curveto(66.537237,92.11243,70.685667,92.93275,74.201377,92.93284);
$pdf->curveto(79.966907,92.93275,85.099717,91.38587,89.599807,88.29221);
$pdf->curveto(92.130957,86.51088,94.638767,84.09682000000001,97.123244,81.05002);
$pdf->curveto(93.373147,77.862449,90.630967,75.02649,88.896687,72.54219);
$pdf->curveto(85.755967,68.04213,84.185657,63.07338,84.185747,57.63594);
$pdf->curveto(84.185657,51.682770000000005,85.849717,46.31558,89.177937,41.53438);
$pdf->curveto(92.505957,36.75309,96.302824,33.729659999999996,100.56855,32.46407000000001);
$pdf->curveto(98.787204,26.69842,95.834084,20.674989999999994,91.709187,14.393749999999997);
$pdf->curveto(85.474717,4.971879999999999,79.287227,0.26094000000000506,73.146687,0.26094000000000506);
$pdf->curveto(70.709107,0.26094000000000506,67.334107,1.0343799999999987,63.021687,2.581249999999997);
$pdf->curveto(58.755997,4.128129999999999,55.14663,4.9015600000000035,52.19356,4.901570000000007);
$pdf->curveto(49.24038,4.9015600000000035,45.79507,4.104690000000005,41.85763,2.510940000000005);
$pdf->curveto(37.96696,0.8703200000000066,34.8029,0.05001000000000033,32.36544,0.04999999999999716);
$pdf->curveto(25.00603,0.05001000000000033,17.78729,6.284369999999996,10.70919,18.75313);
$pdf->curveto(3.63105,31.081220000000002,0.09199,43.17496,0.092,55.03438);
$pdf->curveto(0.09199,66.04993999999999,2.7873,75.02649,8.1779402,81.96409);
$pdf->curveto(13.61542,88.9015,20.45916,92.37025,28.70919,92.37034);
$pdf->moveto(73.006057,120.07346);
$pdf->curveto(73.193477,119.46397,73.310667,118.92491,73.357627,118.45627);
$pdf->curveto(73.404417,117.98741,73.427857,117.51866,73.427937,117.05002);
$pdf->curveto(73.427857,114.04991,72.724727,110.76867,71.318557,107.20627);
$pdf->curveto(69.912237,103.64367,67.685677,100.33899,64.638877,97.29221);
$pdf->curveto(62.013807,94.71399,59.412247,92.97962,56.83419,92.08909);
$pdf->curveto(55.1935,91.57337,52.70913,91.17493,49.38106,90.89377);
$pdf->curveto(49.47476,98.01868,51.32632,104.18272999999999,54.93575,109.38596);
$pdf->curveto(58.591927,114.58897,64.615367,118.15147,73.006057,120.07346);
$pdf->fill (0,149,182); // biondi blue
$pdf->appendForm('apple', 101, 121);

And if we have these images: smiley.png smileyand smiley2.png long smiley we can create bitmapped images:

$im = imagecreatefrompng('smiley.png');
$pdf->appendImage($im);
$im2 = imagecreatefrompng('smiley2.png');
$pdf->appendImage($im2);

And use the images:

$pdf->placeimage('apple', 0, 0, 0, 1, 3); // centered at origin, stretched vertically
$pdf->placeimage('apple', 300, 300); // image placed straight
$pdf->placeimage('apple', 300, 500, M_PI/2); // rotated up 90 degrees
$pdf->placeimage($im, 200, 200); // image placed straight
$pdf->placeimage($im2, 200, 400, deg2rad(60)); // rotated 60 degrees

Transparency

You will notice there is no mention of the alpha channel in the images or other forms of transparency. This is a format from 1993, after all. We were lucky to have transparent colors in our GIFs. But it is possible; see the transparency and alpha channel extensions to FPDF. I just am not going to go into it here.

Compression

The classes presented here allow for gzip compression, but there are better ways to compress images. PDF supports png and jpeg compressed images, as well as color palettes to reduce the bits per pixel. See the FPDF functions Image, _parsejpeg and _parsepng for details.

Continued…

{ 1 } Trackback

  1. […] images are stored as individual objects in the PDF file, so if there is an image I can identify, I can easily replace it with one of my own choosing. The […]

Post a Comment

Your email is never published nor shared. Required fields are marked *