13.2 Producing PDF

In this section, we show you how to use R&OS Ltd's free PHP PDF creation library (we refer to this as pdf-php throughout this section). The library has two advantages over other approaches: it's free and it doesn't require any additional PHP configuration. What's more, it's powerful and you can do most things you need, including producing tables containing results from database queries and inserting images into a document.

You can find out more about pdf-php from http://www.ros.co.nz/pdf and you can download the source, documentation, and get involved in the project at http://sourceforge.net/projects/pdf-php/. Instructions for installing pdf-php are included in Appendix A through Appendix C.

13.2.1 Hello, world

Example 13-1 shows a simple PHP example.

Example 13-1. A simple example that produces Hello, world on an A4 page
<?php

  require "class.ezpdf.php";



  // Create a new PDF document

  $doc =& new Cezpdf( );



  // Add text to the document

  $doc->ezText("Hello, world");



  // Output the document

  $doc->ezStream( );

?>

This PHP example produces the PDF document shown in Figure 13-1.

Figure 13-1. The PDF document produced by Example 13-1
figs/wda2_1301.gif


The base class for producing PDF files is class.pdf.php , but we've used its extension class.ezpdf.php in our simple example. The extension has several useful utilities for document creation and it still includes all the methods of the base class, and so we recommend always using it instead.

In Example 13-1, it's assumed both class files are in the same directory as the example code (or in a directory set by the include_path directive in your php.ini file). In addition, it's assumed that the fonts are in a subdirectory of the example directory named fonts; the fonts are part of the pdf-php install package.

The constructor Cezpdf( ) creates a new PDF document with the default A4 paper size and the default portrait orientation. The =& operator creates an instance of a class, and returns a reference to the instance (and not the whole object itself). The =& operator is a faster, more memory efficient alternative to the = operator. It's discussed in Chapter 4.

The ezText( ) method adds text to the document at the current cursor position (which defaults to the top-left corner; there are, however, top and left margins of 30 points, where a point is 1/72 of an inch), with the current font (which defaults to Helvetica), and the current font size (which defaults to 12). The ezStream( ) method cleans up and then creates the PDF output and sends it to the browser; this is a similar approach to templates (as described in Chapter 7), where a document is first prepared and later output using a method.

In many browsers, when a PDF document arrives as part of an HTTP response, a window containing the PDF document will automatically appear. However, in some browsers, the arrival of a PDF document will cause a dialog box to pop up that asks whether to save or open the PDF file. If you choose open and have a PDF viewer correctly installed, you'll see the output. Now is a good time to get yourself a PDF viewer, as you'll need it throughout this chapter.

You might be wondering what the content of a PDF file looks like. The answer is that it's an ASCII text file that contains instructions on how to render and present the document content; however, it can be compressed, and so you might find it isn't always readable with a text editor. For a document containing graphics, the text can be a complicated list of instructions about lines and points. However, for simple text that's rendered using a font, it's basically human-readable text that you could conceivably edit. For example, here's the part of the file output from Example 13-1 that creates the Hello, world message:

7 0 obj

<<

/Length 55 >>

stream



BT 30.000 800.330 Td /F1 10.0 Tf  (Hello, world) Tj ET

endstream

endobj

13.2.2 A Full-Featured Document

Example 13-2 shows a more complex example that makes use of many of the features of the pdf-php library. The example prints the first two pages of Lewis Carroll's Alice's Adventures in Wonderland in a two-column format, with a title on the first page and an image from the book.

Example 13-2. Formatting Alice's Adventures in Wonderland for printing
<?php

  require "class.ezpdf.php";

  require "alice.inc";



  // Create a new PDF document

  $doc =& new Cezpdf( );



  // Use the Helvetica font for the headings

  $doc->selectFont("./fonts/Helvetica.afm");



  // Output the book heading and author

  $doc->ezText("<u>Alice's Adventures in Wonderland</u>", 24,

                array("justification"=>"center"));

  $doc->ezText("by Lewis Carroll", 20, array("justification"=>"center"));



  // Create a little bit of space

  $doc->ezSetDy(-10);



  // Output the chapter title

  $doc->ezText(Chapter 1: Down the Rabbit-Hole", 18,

                array("justification"=>"center"));



  // Number the pages

  $doc->ezStartPageNumbers(320, 15, 8,"",

     "{PAGENUM} of {TOTALPAGENUM} pages");



  // Create a little bit of space

  $doc->ezSetDy(-30);



  // Switch to two-column mode

  $doc->ezColumnsStart(array("num"=>2, "gap"=>15));



  // Use the Times-Roman font for the text

  $doc->selectFont("./fonts/Times-Roman.afm");



  // Include an image with a caption

  $doc->ezImage("rabbit.jpg", "", "", "none");

  $doc->ezText("<b>White Rabbit checking watch</b>",

               12,array("justification"=>"center"));



  // Create a little bit of space

  $doc->ezSetDy(-10);



  // Add chapter text to the document

  $doc->ezText($text,10,array("justification"=>"full"));



  // Output the document

  $doc->ezStream( );

?>

The first page is shown rendered by the xpdf viewer in Figure 13-2.

Figure 13-2. The output of Example 13-2 shown in the xpdf viewer
figs/wda2_1302.gif


The following code fragment from Example 13-2 sets the font:

// Use the Helvetica font for the headings

$doc->selectFont("./fonts/Helvetica.afm");

We use Helvetica for the headings, and Times-Roman for the body of the document. The available fonts are in the subdirectory fonts in the pdf-php install package, and are passed to the selectFont( ) method using their path and the full file name. The font name must include the .afm extension, and only .afm format files are supported; however, there are free utilities, such as t1utils and ttf2pt1, that convert other font formats (such as .ttf) into .afm files.

This next fragment outputs the headings:

// Output the book heading and author

$doc->ezText("<u>Alice's Adventures in Wonderland</u>", 24,

              array("justification"=>"center"));

$doc->ezText("by Lewis Carroll", 20, array("justification"=>"center"));

The ezText( ) method has three parameters, but only the first is mandatory. The first parameter is the text to add to the document, and it can include simple HTML-like markup elements such as <u> for underline and <b> for bold. Text is output by the method, and followed with a carriage return in the same way as echo or print in PHP. The second parameter is the font size to use (the default is 12), and the third parameter is an array of options. In this example, we've set the justification parameter to center, but it can also be set to left, right, or full; we use full for the text. The complete list of options is described later in Section 13.3

The ezSetDy( ) method is used to create space between text and images. For example, the following fragment moves the cursor down the page by 10 points:

// Create a little bit of space

$doc->ezSetDy(-10);

A negative value is downwards, and a positive value is upwards. In PDF, the bottom-left-hand corner of a page is coordinate X=0, Y=0, and the top-right has the maximum X and Y values. For an A4 page, the top-right corner has a point value of X=595.28 and Y=841.89, and for US letter of X=612.00 and Y=792.00.

The class includes the useful ezStartPageNumbers( ) method for numbering pages. We use it as follows:

// Number the pages

$doc->ezStartPageNumbers(320, 15, 8, "",

  "{PAGENUM} of {TOTALPAGENUM} pages");

The first two parameters are the X and Y coordinates of where to put the page number text, and the third parameter is the font size to use; the first three parameters are mandatory. The optional fourth parameter can be set to left or right, and indicates whether to put the text to the left or right of the X coordinate; by default, the text is written to the left of the X coordinate. The optional fifth parameter specifies how to present the page numbering; by default, it is {PAGENUM} of {TOTALPAGENUM} but we've set it to {PAGENUM} of {TOTALPAGENUM} pages to get strings such as 1 of 2 pages. The optional sixth parameter is a page number and, if it is supplied, the current page is numbered beginning with the number.

We've presented the text of the book in a two column newspaper-like format. This is achieved by calling the ezColumnsStart( ) method as follows:

// Switch to two-column mode

$doc->ezColumnsStart(array("num"=>2, "gap"=>15));

The method takes one optional parameter. The parameter is an array that specifies the number of columns and the gap in points between the columns. If the parameter is omitted, the number of columns defaults to 2 and the gap to 10. The method ezColumnsStop( ) stops multi-column mode, but we don't use it here because we're working with only one chapter.

To include an image in the text, you can use the ezImage( ) method. We use it to include a picture of the white rabbit after the headings:

$doc->ezImage("rabbit.jpg", "", "", "none");

A great feature of this method is that it doesn't require any additional configuration: you don't need to install any graphics libraries (such as GD) and it'll work on all platforms without modification. The method takes six parameters. The first parameter is a mandatory image file path and name, and only JPEG and PNG format images are supported. All of the remaining parameters are optional. The second parameter is the amount of padding in points to place around the image, and it defaults to 5. The third parameter is the width of the image, and the default is the image's actual width. The fourth parameter is a resize value that controls how the image fits in a column, and we've used none so that the image isn't resized at all. The fifth parameter specifies justification, and can be set to left, right, or center with a default of center. The sixth parameter is a border to place around the image, and defaults to none. More details on all parameters are provided in Section 13.3.

After we've finished with headings and images, the following fragment includes the text of the book into the PDF document:

$doc->ezText($text,10,array("justification"=>"full"));

The variable $text contains the text of the book. It is set in the alice.inc include file. Here are the first few lines of alice.inc:

<?php

$text = "Alice was beginning to get very tired of sitting by her sister on

the bank, and of having nothing to do:  once or twice she had peeped into

the book her sister was reading, but it had no pictures or conversations

in it, ... ";

Carriage returns and whitespace characters are preserved in the output. So, for example, a carriage return creates a new line in the PDF file; this is unlike HTML, where whitespace is ignored. The book text itself is sourced from the Project Gutenberg homepage at http://gutenberg.net.

13.2.3 A Database Example

Example 13-3 shows a script that produces a page containing one customer's details from a customer table. The customer table is discussed in Chapter 5 and created with the following CREATE TABLE statement:

CREATE TABLE customer (

  cust_id int(5) NOT NULL,

  surname varchar(50),

  firstname varchar(50),

  initial char(1),

  title_id int(3),

  address varchar(50),

  city varchar(50),

  state varchar(20),

  zipcode varchar(10),

  country_id int(4),

  phone varchar(15),

  birth_date char(10),

  PRIMARY KEY (cust_id)

) type=MyISAM;

The example also uses the titles lookup table that contains title_id values and titles (such as Mr. and Miss), and the countries lookup table that contains country_id values and country names. The output of Example 13-3 is shown in Figure 13-3.

Example 13-3. Producing customer information from the customer table
<?php

  require "class.ezpdf.php";

  require "db.inc";



  $query = "SELECT * FROM customer, titles, countries

            WHERE customer.title_id = titles.title_id

            AND customer.country_id = countries.country_id

            AND cust_id = 1";



  if (!($connection = @ mysql_connect($hostName, $username, $password)))

    die("Could not connect to database");



  if (!(mysql_selectdb($databaseName, $connection)))

    showerror( );



  if (!($result = @ mysql_query($query, $connection)))

    showerror( );



  $row = mysql_fetch_array($result);



  // Construct the title and name

  $name = "{$row["title"]} {$row["firstname"]}";

  if (!empty($row["initial"]))

    $name .= " {$row["initial"]} ";

  $name .= "{$row["surname"]}";



  // Create a new PDF document

  $doc =& new Cezpdf( );



  // Use the Helvetica font

  $doc->selectFont("./fonts/Helvetica.afm");



  // Create a heading

  $doc->ezText("<u>Customer Details for {$name}</u>",

                14, array("justification"=>"center"));



  // Create a little bit of space

  $doc->ezSetDy(-15);



  // Set up an array of customer information

  $table = array(

    array("Details"=>"Title and name",

          "Value"=>$name),

    array("Details"=>"Address",

          "Value"=>"{$row["address"]} {$row["city"]} {$row["zipcode"]}"),

    array("Details"=>"State and country",

          "Value"=>"{$row["state"]} {$row["country"]}"),

    array("Details"=>"Telephone",

          "Value"=>$row["phone"]),

    array("Details"=>"Date of birth",

          "Value"=>$row["birth_date"]));



  $doc->ezTable($table);



  // Output the document

  $doc->ezStream( );

?>

The database processing in Example 13-3 is similar to that of most examples in previous chapters. The script queries and retrieves the customer details for customer #1, including the customer's title and country. The results are stored in the array $row, and the array is then used as the source of data for the PDF document.

Figure 13-3. The customer details page output by Example 13-3
figs/wda2_1303.gif


The use of the pdf-php library is also similar to that in our previous examples, with the exception that the customer details are shown in a table using the ezTable( ) method. The ezTable( ) method is a flexible tool that allows you to present data in different table styles and to configure the column headings, column widths, shading, borders, and alignment.

In this example, we only use the basic features of the ezTable( ) method. First, we've created an array that contains the data we want to display in the table:

  // Set up an array of customer information

  $table = array(

    array("Details"=>"Title and name",

          "Value"=>$name),

    array("Details"=>"Address",

          "Value"=>"{$row["address"]} {$row["city"]} {$row["zipcode"]}"),

    array("Details"=>"State and country",

          "Value"=>"{$row["state"]} {$row["country"]}"),

    array("Details"=>"Telephone",

          "Value"=>$row["phone"]),

    array("Details"=>"Date of birth",

          "Value"=>$row["birth_date"]));

The array contains five elements, each of which is itself an array. Each of these five inner arrays has two associatively-labeled elements: Details and Value. The Details element holds as a row label value such as Title and name, and the Value element holds the data that matches the label.

The PDF table itself is created with the fragment:

$doc->ezTable($table);

The method creates a table with the number of rows equal to the number of elements in the array (in our example, five rows). The number of columns in the table is equal to the number of elements in the inner arrays and, in our example, they each have two elements. By default, column headings are taken from the associative-access keys, and the data in the tables comes from the values. The default mode is to create a bordered table with shading in every second row, and to center the table in the output.

13.2.4 Creating a Report

Example 13-4 shows a script that produces a more complex purchase report. The report is a table that lists the customers in the winestore database, the number of orders they've placed, the number of bottles of wine they've bought, and the total dollar value of their purchases. We also show totals at the end of each page, and an overall total on the final page.

Example 13-4. A script to produce a customer purchasing report
<?php

  require "class.ezpdf.php";

  require "db.inc";



  // Do the querying to produce the customer report

  $query = "SELECT customer.cust_id, surname, firstname,

                   SUM(qty), SUM(price), MAX(order_id)

            FROM customer, items

            WHERE customer.cust_id = items.cust_id

            GROUP BY customer.cust_id";



  if (!($connection = @ mysql_connect($hostName, $username, $password)))

    die("Could not connect to database");



  if (!(mysql_selectdb($databaseName, $connection)))

    showerror( );



  if (!($result = @ mysql_query($query, $connection)))

    showerror( );



  // Now, create a new PDF document

  $doc =& new Cezpdf( );



  // Use the Helvetica font

  $doc->selectFont("./fonts/Helvetica.afm");



  // Number the pages

  $doc->ezStartPageNumbers(320, 15, 8);



  // Set up running totals and an empty array for the output

  $counter = 0;

  $table = array( );

  $totalOrders = 0;

  $totalBottles = 0;

  $totalAmount = 0;



  // Get the query rows, and put them in the table

  while ($row = mysql_fetch_array($result))

  {

    // Counts the total number of rows output

    $counter++;



    // Add current query row to the array of customer information

    $table[] = array(

          "Customer #"=>$row["cust_id"],

          "Name"=> "{$row["surname"]}, {$row["firstname"]}",

          "Orders Placed"=>$row["MAX(order_id)"],

          "Total Bottles"=>$row["SUM(qty)"],

          "Total Amount"=>"\${$row["SUM(price)"]}");



    // Update running totals

    $totalOrders += $row["MAX(order_id)"];

    $totalBottles += $row["SUM(qty)"];

    $totalAmount += $row["SUM(price)"];

  }



  // Today's date is used in the table heading

  $date = date("d M Y");



  // Right-justify the numeric columns

  $options = array("cols" =>

               array("Total Amount" =>

                 array("justification" => "right"),

                     "Total Bottles" =>

                 array("justification" => "right"),

                     "Orders Placed" =>

                 array("justification" => "right")));



  // Output the table with a heading

  $doc->ezTable($table, "", "Customer Order Report for {$date}",

                $options);



  $doc->ezSetDy(-10);



  // Show totals

  $doc->ezText("Total customers: {$counter}");

  $doc->ezText("Total orders: {$totalOrders}");

  $doc->ezText("Total bottles: {$totalBottles}");

  $doc->ezText("Total amount: \${$totalAmount}");



  // Output the document

  $doc->ezStream( );

?>

The first page of the output of Example 13-4 is shown in Figure 13-4 and the final page of the output in Figure 13-5.

Figure 13-4. The first page of output from Example 13-4
figs/wda2_1304.gif


Figure 13-5. The final page of output from Example 13-4
figs/wda2_1305.gif


The report makes use of two tables, the customer table shown in the previous section and an items table that's described in more detail in Chapter 5. The items table is created with the following statement:

CREATE TABLE items (

  cust_id int(5) NOT NULL,

  order_id int(5) NOT NULL,

  item_id int(3) NOT NULL,

  wine_id int(4) NOT NULL,

  qty int(3),

  price decimal(5,2),

  PRIMARY KEY (cust_id,order_id,item_id)

) type=MyISAM;

We only use the order_id (which is used to count how many orders each customer has placed), qty (quantity of wine ordered in bottles), and price (per bottle price) attributes from the items table in this section.

We use the following query in our report:

$query = "SELECT customer.cust_id, surname, firstname, 

                 SUM(qty), SUM(price), MAX(order_id) 

          FROM customer, items 

          WHERE customer.cust_id = items.cust_id

          GROUP BY customer.cust_id";

The query groups each customer's items together by his unique cust_id, and we then discover the customer's name, cust_id, the sum of bottles sold using SUM(qty), the total value of the sales using SUM(price), and the number of orders placed using MAX(order_id). We run the query using the usual MySQL functions, and then retrieve each row of the results and add it to the PDF document as a table row.

We use the class constructor and the SelectFont( ), ezStartPageNumbers( ), ezText( ), ezSetDy( ), and ezStream( ) methods in the same way as in the previous three sections.

Data comes from multiple rows in our database query results and is displayed using the ezTable( ) method in this example. To create the table, we first initialize an empty array using:

$table = array( );

Then, for each row in the table, we add an element to that array that is itself an array that contains five elements:

// Add to the array of customer information

$table[] = array(

      "Customer #"=>$row["cust_id"],

      "Name"=> "{$row["surname"]}, {$row["firstname"]}",

      "Orders Placed"=>$row["MAX(order_id)"],

      "Total Bottles"=>$row["SUM(qty)"],

      "Total Amount"=>"\${$row["SUM(price)"]}");

The associative-access labels (Customer #, Name, Orders Placed, Total Bottles, and Total Amount) are used as the column headings for the table, and the customer data from the query is used to populate the rows.

The table itself is then output with the following fragment:

// Output the table

$doc->ezTable($table, "", "Customer Order Report for {$date}", $options);

We use the optional third parameter that adds a title to the table. This is output on the first page of output. The fourth optional parameter is also used in this example to right-justify the numeric columns (so that the differences in magnitude are obvious and so the decimal points line up). To do this, we create a nested array:

// Right-justify the numeric columns

$options = array("cols" =>

             array("Total Amount" =>

               array("justification" => "right"),

                   "Total Bottles" =>

               array("justification" => "right"),

                   "Orders Placed" =>

               array("justification" => "right")));

The outer array contains one element, with the associative key 'cols', and this indicates the option we're setting (you can set more than 15 different options for a table). It contains as a value another array that contains as keys the names of the three columns we want to configure ('Total Amount', 'Total Bottles', and 'Orders Placed'). Each of these three elements has as its value yet another array, this time with the column setting we want to change as the key ('justification') and what we want to set it to ('right'). This complex options parameter is discussed in more detail in Section 13.3.

Finally, with the pages of tables complete, overall totals of customers, orders, bottles, and sales are added using ezText( ) and the whole document is output using ezStream( ).