PDF Generation Using Only PHP – Part 2

January 6, 2004

Uncategorized

Intended Audience
Overview
Learning Objectives
Prerequisites
How It Works
•  New Class Variables
•  Color

•  Line Drawing
•  Rectangles
•  Circles
•  Line Width
•  Page Add Modifications
•  Images
The Script
•  Example Use
About the Author

Intended Audience


This tutorial is intended for the PHP programmer who needs to incorporate PDF generation
in a script without using external libraries such as PDFlib (often unavailable due to licensing
restrictions or lack of funds).

This tutorial is the second of two parts, and builds on what
was covered in the first part. Therefore, if you have not yet gone through
Part 1,
you are advised to do so (or at least read through it), before going through
this tutorial (Part 2).

Apart from what was dealt with in Part 1, no knowledge of PDF
file structure is required to understand this tutorial, as all references are
explained.

Overview


We have seen in Part 1 how PDF files are, after all, just
plain text files, with specific markup syntax that describes what should happen
to objects within the document, such as text and images. We shall now further
examine this syntax, to allow us to create a more complete PDF document (i.e
more than simple text).

Learning Objectives


With the combined knowledge from Part 1, and from the current
tutorial (Part 2), you should be able to put together a simple PDF class that
can:

  • Add colors to your PDF documents;
  • Add lines, rectangles and circles;
  • Insert JPEG images.

Prerequisites


You need to have a fully functional PHP install (either PHP 4
or PHP 5), and a running web server to output the PDF file from your
script.

Acrobat Reader, XPDF, or equivalent is required, to display
the results of your work.

You do not need any external library, either separate or
compiled into PHP, to generate your PDF files.

How It Works


We already have the class from Part 1 of this tutorial. In
Part 2 we will just add a few more class variables and methods to it.

We shall review the various methods and features of the PDF
language, and then finally put it all together as one class.

New Class Variables


In addition to the class variables we introduced in Part 1, we
will also need the following.



var $_fill_color = '0 g';   // Color used on text and fills.

var $_draw_color = '0 G';   // Line draw color.

var $_line_width = 1;       // Drawing line width.

var $_images = array();     // An array of used images.




In
the PDF specifications, black is the default value for fill and draw colors, and
the default line width is 1 point. Therefore we will use these values as our
class defaults for the above variables.

Color


We have two different color settings to consider. The
$_fill_color variable refers to the
“non-stroke” color which is applied to fills and text. The
$_draw_color variable is the

“stroke” color applied to lines.

The following function will set the fill color:


function setFillColor($cs = 'rgb', $c1, $c2 = 0, $c3 = 0, $c4 = 0)

{

    
$cs = strtolower($cs);

    if (
$cs = 'rgb') {

        
/* Using a three component RGB color. */

        
$this->_fill_color = sprintf('%.3f %.3f %.3f rg', $c1, $c2, $c3);

    } elseif ($cs = 'cmyk') {

        
/* Using a four component CMYK color. */

        
$this->_fill_color = sprintf('%.3f %.3f %.3f %.3f k', $c1, $c2, $c3, $c4);

    } else {

        
/* Grayscale one component color. */

        
$this->_fill_color = sprintf('%.3f g', $c1);

    }

    
/* If document started output to buffer. */

    
if ($this->_page > 0) {

        $this->_out($this->_fill_color);

    }

}


Notice that we need to pass varying number of color
components depending on the colorspace selected: one for grayscale, or several
for full color.

Grayscale colors are represented by a single 3 decimal place
number followed by the letter ‘g’. 0 is black, 1 is white, with a whole lot of
shades of gray in between.

For example:

’0.000 g’ = black, eg. setFillColor(‘gray’, 0)

’0.200 g’ = 20% gray, eg. setFillColor(‘gray’, 0.2)

’0.800 g’ = 80% gray, eg. setFillColor(‘gray’, 0.8)

Selecting ‘rgb’ and passing three color components gives us
full color, represented by 3 separate 3-decimal-place numbers, followed by the
letters ‘rg’.

So, for example:

’0.800 0.400 0.200 rg’ = purple-ish color. eg. setFillColor(‘rgb’, 0.8, 0.4, 0.2)

A second, very similar function sets the drawing
color:


function setDrawColor($cs = 'rgb', $c1, $c2 = 0, $c3 = 0,

                      $c4 = 0)

{   

    
$cs = strtolower($cs);

    if (
$cs = 'rgb') {

        $this->_draw_color = sprintf('%.3f %.3f %.3f RG',

                                     
$c1, $c2, $c3);

    } elseif (
$cs = 'cmyk') {

        $this->_draw_color = sprintf('%.3f %.3f %.3f %.3f K',

                                     
$c1, $c2, $c3, $c4);

    } else {

        $this->_draw_color = sprintf('%.3f G', $c1);

    }   

    
/* If document started output to buffer. */

    
if ($this->_page > 0) {

        $this->_out($this->_draw_color);

    }

}


Note that the only difference between these two functions is
the lowercase/uppercase syntax. ‘g’, ‘rg’, and ‘k’ specify fill color, while
‘G’, ‘RG’, and ‘K’, specify draw color.

That’s it! Calling either of these
two functions will set the colors for any subsequent text or drawing output to
your PDF document, and preserve that setting between pages.


Line Drawing


All that is required for drawing a line in PDF is a starting
point and an end point. The below function takes an x1/y1 start, and an x2/y2
end point.

The actual PDF syntax to achieve this is:

  • x1 y1 m‘ – move the current x/y position to x1/y1;
  • x2 y2 l‘ – continue in a straight line to x2/y2;
  • S‘ – set a “stroke” on the resulting path.


As in Part 1, we compensate for PDF’s inverted y scale by subtracting the user
supplied $y value from the $this->_h value for page height, to
obtain the vertical distance from the bottom of the page.


function line($x1, $y1, $x2, $y2)

{   

    
$this->_out(sprintf('%.2f %.2f m %.2f %.2f l S', $x1, $this->_h - $y1, $x2, $this->_h - $y2));

}


Rectangles


A rectangle function requires a starting x/y point, a width,
and a height. A fifth parameter defines the rectangle style, which can be ‘f’
for filled, ‘d’ for drawn, or ‘fd’ (or ‘df’, the order doesn’t matter) for
both filled and drawn.

Similar to the line drawing function, the PDF output for the
rectangle involves creating a rectangular path and applying a path-painting
operator to it:

  • x y w h re‘ – the rectangular path;
  • f‘, ‘S‘ or ‘B‘ – the path-painting operators.


function rect($x, $y, $width, $height, $style = '')

{

    
$style = strtolower($style);

    if (
$style == 'f') {

        
$op = 'f';      // Style is fill only.

    } elseif ($style == 'fd' || $style == 'df') {

        
$op = 'B';      // Style is fill and stroke.

    
} else {

        
$op = 'S';      // Style is stroke only.

    }

    $this->_out(sprintf('%.2f %.2f %.2f %.2f re %s', $x, $this->_h - $y, $width, -$height, $op));

}

Note that the end result of calling a drawn and filled
rectangle (‘fd’) is exactly the same as first creating a filled rectangle (‘f’)
and then creating a drawn one (‘d’).


Circles


Drawing a circle is slightly more complex. There is no native
PDF syntax to draw a circle. However, luckily, PDF can draw not only straight
lines, but also complex cubic Bézier curves. Using a combination of these
we can create our own function to draw a circle.

The following is a fairly good general introduction to
Bézier curves in PDF, which you can use to create complex patterns or
drawings, not only circles.


function circle($x, $y, $r, $style = '')

{

    
$style = strtolower($style);

    if ($style == 'f') {

        
$op = 'f';      // Style is fill only.

    
} elseif ($style == 'fd' || $style == 'df') {

        $op = 'B';      // Style is fill and stroke.

    
} else {

        
$op = 'S';      // Style is stroke only.

    
}

    $y = $this->_h - $y;                 // Adjust y value.

    $b = $r * 0.552;                     // Length of the Bezier

                                         // controls.

    /* Move from the given origin and set the current point

     * to the start of the first Bezier curve. */

    
$c = sprintf('%.2f %.2f m', $x - $r, $y);

    $x = $x - $r;

    
/* First circle quarter. */

    
$c .= sprintf(' %.2f %.2f %.2f %.2f %.2f %.2f c',

                  
$x, $y + $b,           // First control point.

                  $x + $r - $b, $y + $r, // Second control point.

                  
$x + $r, $y + $r);     // Final point.

    /* Set x/y to the final point. */

    
$x = $x + $r;

    
$y = $y + $r;

    
/* Second circle quarter. */

    
$c .= sprintf(' %.2f %.2f %.2f %.2f %.2f %.2f c',

                  $x + $b, $y,

                  
$x + $r, $y - $r + $b,

                  
$x + $r, $y - $r);

    /* Set x/y to the final point. */

    
$x = $x + $r;

    
$y = $y - $r;

    
/* Third circle quarter. */

    
$c .= sprintf(' %.2f %.2f %.2f %.2f %.2f %.2f c',

                  $x, $y - $b,

                  
$x - $r + $b, $y - $r,

                  
$x - $r, $y - $r);

    /* Set x/y to the final point. */

    
$x = $x - $r;

    
$y = $y - $r;

    
/* Fourth circle quarter. */

    
$c .= sprintf(' %.2f %.2f %.2f %.2f %.2f %.2f c %s',

                  $x - $b, $y,

                  
$x - $r, $y + $r - $b,

                  
$x - $r, $y + $r,

                  $op);

    
/* Output the whole string. */

    
$this->_out($c);

}


Note that the process is essentially one of creating
four joined arcs, each one starting from where the previous one
finished.

PDF is “aware” of the current x/y position after
having drawn an object. This means that you can concatenate several draw
objects, following each new object on from where the last one left over, without
having to explicitly move to the new x/y start position.


Line Width


Since we have introduced lines and drawings, it will be good
to have a function to control the line width of our drawings. This function sets
our class variable $this->_line_width and outputs it to
the buffer, if there is an open document.


function setLineWidth($width)

{

    
$this->_line_width = $width;

    if (
$this->_page > 0) {

        
$this->_out(sprintf('%.2f w', $width));

    }

}



Page Add Modifications


The only code we still need to add for colors and drawings are some checks in the addPage() function.
These make sure that the colors and line width, set before any page is added,
are actually inserted into the buffer, and that they are remembered from page to page.

For the addPage() function, introduced in Part 1, the three

if() checks appear below, at the bottom of the function:


function addPage()

{

    
$this->_page++;                    // Increment page count.

    
$this->_pages[$this->_page] = '';  // Start the page buffer.

    $this->_state = 2;                 // Set state to page

                                       // opened.

    /* Check if font has been set before this page. */

    
if ($this->_font_family) {

        
$this->setFont($this->_font_family, $this->_font_style, $this->_font_size);

    }

    
/* Check if fill color has been set before this page. */

    
if ($this->_fill_color != '0 g') {

        
$this->_out($this->_fill_color);

    }   

    
/* Check if draw color has been set before this page. */

    
if ($this->_draw_color != '0 G') {

        
$this->_out($this->_draw_color);

    }

    
/* Check if line width has been set before this page. */

    
if ($this->_line_width != 1) {

        
$this->_out($this->_line_width);

    }

}



Images


The last step in this tutorial will be a brief look at images:
specifically the inserting of the JPEG image type (since it is the simplest one to explain).


function image($file, $x, $y, $width = 0, $height = 0)

{

    if (!isset(
$this->_images[$file])) {

        
/* First use of requested image, get the extension. */

        
if (($pos = strrpos($file, '.')) === false) {

            die(sprintf('Image file %s has no extension and no type was specified', $file));

        }

        
$type = strtolower(substr($file, $pos + 1));

        /* Check the image type and parse. */

        
if ($type == 'jpg' || $type == 'jpeg') {

            
$info = $this->_parseJPG($file);

        } else {

            die(
sprintf('Unsupported image file type: %s', $type));

        }

        
/* Set the image object id. */

        
$info['i'] = count($this->_images) + 1;

        /* Set image to array. */

        
$this->_images[$file] = $info;

    } else {

        
$info = $this->_images[$file];          // Known image, retrieve

                                                // from array.

    
}

    /* If not specified, do automatic width and height

     * calculations, either setting to original or

     * proportionally scaling to one or the other given

     * dimension. */

    
if (empty($width) && empty($height)) {

        
$width = $info['w'];

        $height = $info['h'];

    } elseif (empty(
$width)) {

        
$width = $height * $info['w'] / $info['h'];

    } elseif (empty($height)) {

        
$height = $width * $info['h'] / $info['w'];

    }

    $this->_out(sprintf('q %.2f 0 0 %.2f %.2f %.2f cm /I%d Do Q', $width, $height, $x, $this->_h - ($y + $height), $info['i']));

}

To keep this example simple the file checking is rather crude. We guess the image type according to what its extension is.
Ideally we should be using a better way to check the image type, regardless of extension.

The _parseJPG() method does a further check to make sure we are dealing with a JPEG file.

We will need to add a few other functions to handle the inserting of images. First of all there is the
_parseJPG() method we saw in the function above:


function _parseJPG($file)

{   

    
/* Extract info from the JPEG file. */

    
$img = @getimagesize($file);

    if (!
$img) {

        die(sprintf('Missing or incorrect image file: %s', $file));

    }

    
/* Check if dealing with an actual JPEG. */

    
if ($img[2] != 2) {

        die(
sprintf('Not a JPEG file: %s', $file));

    }

    
/* Get the image colorspace. */

    
if (!isset($img['channels']) || $img['channels'] == 3) {

        
$colspace = 'DeviceRGB';

    } elseif ($img['channels'] == 4) {

        
$colspace = 'DeviceCMYK';

    } else {

        
$colspace = 'DeviceGray';

    }

    $bpc = isset($img['bits']) ? $img['bits'] : 8;

    /* Read the whole file. */

    
$f = fopen($file, 'rb');

    $data = fread($f, filesize($file));

    
fclose($f);

    return array('w' => $img[0], 'h' => $img[1], 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'DCTDecode', 'data' => $data);

}

All that this function does is to check that the file format is JPEG, get the colorspace, and read the file data.
It returns an array containing width, height, colorspace, bits, filter (always ‘DCTDecode’ for JPEG), and the actual data.

Our _putResources() function that we set up in Part 1 of this tutorial, now needs to add images as
well. This is how the modified function should look. Note the added call to _putImages(), the extra parameters in
the ‘ProcSet’ line, and the loop to output the image objects.


function _putResources()

{

    
$this->_putFonts();              // Output any fonts.

    
$this->_putImages();             // Output any images.

    /* Resources are always object number 2. */

    
$this->_offsets[2] = strlen($this->_buffer);

    $this->_out('2 0 obj');

    
$this->_out('<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');

    
$this->_out('/Font <<');

    foreach ($this->_fonts as $font) {

        
$this->_out('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R');

    }

    
$this->_out('>>');

    if (
count($this->_images)) {     // Loop through any images

        
$this->_out('/XObject <<');  // and output the objects.

        foreach ($this->_images as $image) {

            
$this->_out('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R');

        }

        
$this->_out('>>');

    }

    
$this->_out('>>');

    
$this->_out('endobj');

}

The above function calls the _putImages() to output the actual image data:


function _putImages()

{

    
/* Output any images. */

    
$filter = ($this->_compress) ? '/Filter /FlateDe ' : '';

    foreach ($this->_images as $file => $info) {

        
$this->_newobj();

        
$this->_images[$file]['n'] = $this->_n;

        $this->_out('<</Type /XObject');

        
$this->_out('/Subtype /Image');

        
$this->_out('/Width ' . $info['w']);    // Image width.

        $this->_out('/Height ' . $info['h']);   // Image height.

        
$this->_out('/ColorSpace /' . $info['cs']); //Colorspace

        if ($info['cs'] == 'DeviceCMYK') {

            
$this->_out('/De [1 0 1 0 1 0 1 0]');

        }

        
$this->_out('/BitsPerComponent ' . $info['bpc']); // Bits

        $this->_out('/Filter /' . $info['f']);  // Filter used.

        
$this->_out('/Length ' . strlen($info['data']) . '>>');

        $this->_putStream($info['data']);       // Image data.

        
$this->_out('endobj');

    }

}


The Script


You can download the entire class for use with Part 2 of this tutorial.


Example Use

The new example script below includes the added functions. It
sets different colors, draws some lines, a rectangle and a circle, and uploads
an image. Note how we can use line drawing to emulate underline, which is not
natively available in the PDF syntax.



<?php

require 'PDF.php';                    // Require the class.

$pdf = &PDF::factory('p', 'a4');      // Set up the pdf object.

$pdf->open();                         // Start the document.

$pdf->setCompression(true);           // Activate compression.

$pdf->addPage();                      // Start a page.

$pdf->setFont('Courier', '', 8);      // Set font to courier 8 pt.

$pdf->text(100, 100, 'First page');   // Text at x=100 and y=100.

$pdf->setFontSize(20);                // Set font size to 20 pt.

$pdf->setFillColor('rgb', 1, 0, 0);   // Set text color to red.

$pdf->text(100, 200, 'HELLO WORLD!'); // Text at x=100 and y=200.

$pdf->setDrawColor('rgb', 0, 0, 1);   // Set draw color to blue.

$pdf->line(100, 202, 240, 202);       // Draw a line.

$pdf->setFillColor('rgb', 1, 1, 0);   // Set fill/text to yellow.

$pdf->rect(200, 300, 100, 100, 'fd'); // Draw a filled rectangle.

$pdf->addPage();                      // Add a new page.

$pdf->setFont('Arial', 'BI', 12);     // Set font to arial bold

                                      // italic 12 pt.

$pdf->text(100, 100, 'Second page');  // Text at x=100 and y=100.

$pdf->image('sample.jpg', 50, 200);   // Image at x=50 and y=200.

$pdf->setLineWidth(4);                // Set line width to 4 pt.

$pdf->circle(200, 300, 150, 'd');     // Draw a non-filled

                                      // circle.

$pdf->output('foo.pdf');              // Output the file named foo.pdf

?>

About the Author


Marko Djukic works and lives in Florence, Italy running his
own company http://oblo.com with the goal of
bringing innovative Open Source solutions to local government and SMEs. He is
also a core developer for the Horde Project (http://horde.org).
Marko can be reached directly at
marko@oblo.com

4 Responses to “PDF Generation Using Only PHP – Part 2”

  1. philsown Says:

    There’s a couple of "assignment in comparison" errors in the setFillColor and setDrawColor functions. Make sure you use two equals ==.

    Also, the $filter variable isn’t used in the final _putImages() function. Where is this supposed to go? In this line later:
    $this->_out(‘/Filter /’ . $info['f']);

    Or should it be used as above in the _putPages() function?
    $this->_out(‘<<’ . $filter . ‘/Length ‘ . strlen($p) . ‘>>’);

    Thanks for the pointers.

  2. _____anonymous_____ Says:

    Just letting you know that the links are still broken…

  3. _____anonymous_____ Says:

    How can I modify this to make the file available to be emailed as an attachment?

  4. _____anonymous_____ Says:

    It appears that on both part one and part two, the links are broken. Perhaps some major maintenance happening here.