PDF Generation Using Only PHP - Part 2
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.
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);
}
}
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'.
'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);
}
}
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.
$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));
}
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);
}
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 theaddPage() 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']));
}
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);
}
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');
}
_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

Comments
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.