Wednesday, April 17, 2013

Build PDF files dynamically with PHP

Build PDF files dynamically with PHP

Sometimes you need control over exactly how pages are rendered for printing. At times like those, HTML is not the best choice. PDF files give you complete control over how pages are rendered and how text, graphics and images are rendered on the page. Sadly, APIs for building PDF files are not standard parts of the PHP toolkit. Now is the time to bring in a little help.

When you search the web for PDF support for PHP, the first thing you are likely to find is the commercial PDFLib library and its open source version, PDFLib-Lite. These are good libraries, but the commercial version is fairly expensive. The light version of the library is distributed only as source, and that restriction might be an issue if you try to install it in a hosted environment.

Another choice is the Free PDF library (FPDF), which is native PHP. It doesn't require any compilation, and it is completely free so you don't see watermarks as you do with an unlicensed version of PDFLib. This Free PDF library is what I use in this article.

To demonstrate building PDF files dynamically, you'll use scores from women's roller derby tournaments. These scores were mined from the web and converted into XML. Listing 1 shows an example of the XML data file.

Listing 1. The XML data

<events>  
  <event name='Beast of the East 2011'>
    <game score1='88' team1='Toronto Gore-Gore Rollergirls' 
          team2='Montreal La Racaille' score2='11'/>
    <game score1='58' team1='Toronto Death Track Dolls' 
          team2='Montreal Les Contrabanditas' score2='49'/>
     ...
  </event>
  <event name='Dustbowl Invitational 2011'>
     ...
  </event>
  <event name='The Great Yorkshire Showdown 2011'>
     ...
  </event>
</events>

The root element for the XML is an events tag. The data is grouped into events, where each event holds a number of games. Within the events tag is a series of event tags, within which are multiple game tags. These game tags include the names of each of the two teams playing and their scores during the game.
Listing 2 shows the PHP code that you use to read the XML.

Listing 2. getresults.php

<?php
function getResults() {
  $xml = new DOMDocument(); 
  $xml->load('events.xml'); 
  $events = array();
  foreach($xml->getElementsByTagName('event') as $event) { 
    $games = array();
    foreach($event->getElementsByTagName('game') as $game) {
      $games []= array( 'team1' => $game->getAttribute('team1'),
        'score1' => $game->getAttribute('score1'),
        'team2' => $game->getAttribute('team2'),
        'score2' => $game->getAttribute('score2') );
    }
    $events []= array( 'name' => $event->getAttribute('name'),
      'games' => $games );
  }
  return $events;
}
?>

This script implements a getResults function that reads the XML file into a DOMDocument. DOM calls are then used to traverse all of the event and game tags to build an array of events. Within each element of the array is a hash table that includes the name of the event and an array of the games played. The structure is basically an in-memory version of the structure of the XML.

To test that this script works, you will build an HTML export page that uses the getResults function to read the file, then outputs the data as a series of HTML tables. Listing 3 shows the PHP code for this test.

Listing 3. The results HTML page

<html>
    <head>
        <title>Event Results</title>
    </head>
    <body>
        <?php
            include_once('getresults.php');
            $results = getResults();
            foreach( $results as $event ) {
        ?>
        <h1><?php echo( $event['name'] ) ?></h1>
        <table>
        <?php
            foreach( $event['games'] as $game ) {
              $s1 = (int)$game['score1'];
              $s2 = (int)$game['score2'];
        ?>
            <tr>
                <td style="font-weight:<?php echo( ( $s1 > $s2 ) ? 'bold' : 'normal') ?>">
                <?php echo( $game['team1'] ) ?></td>
                <td><?php echo( $s1 ) ?></td>
                <td style="font-weight:<?php echo( ( $s2 > $s1 ) ? 'bold' : 'normal') ?>">
                <?php echo( $game['team2'] ) ?></td>
                <td><?php echo( $s2 ) ?></td>
            </tr>
            <?php
            }
            ?>
        </table>
        <?php
        }
        ?>
    </body>
</html>

Building the PDF

With the data in hand, it's time to focus on building PDF files. The first step is to download the FPDF library and install it in the same directory as the existing set of application files. Actually, you can install it wherever you like as long it's in the PHP library path. Keep track of where you put the fonts directory, as you will need to set the FPDF_FONTPATH as in Listing 4.

Listing 4. A PDF Hello World

<?php
define('FPDF_FONTPATH','/Library/WebServer/Documents/derby/font/');

require( 'fpdf.php' );

$pdf = new FPDF();
$pdf->SetFont('Arial','',72);
$pdf->AddPage();
$pdf->Cell(40,10,"Hello World!",15);
$pdf->Output();
?>

This script is literally a "Hello World" but as a PDF instead of HTML. The first thing the script does is set the location of the FPDF fonts directory using the define statement. It then brings in the FPDF library using the require statement. From there, the script creates an FPDF object, sets the font, adds a page, puts some text on the page using the Cell method, and outputs the PDF.

If you don't see a PDF, you will probably want to run the script on the command line to see if you are missing the fpdf.php file or if there is another issue.

Now that the PDF rendering works, it's time to merge it with the roller derby results file and see what you can generate dynamically. Listing 5 shows the first version of this merging.

Listing 5. The first version of the PDF displaying the results

<?php
define('FPDF_FONTPATH','/Library/WebServer/Documents/derby/font/');

require( 'fpdf.php' );
require( 'getresults.php' );

class PDF extends FPDF {
    function EventTable($event) {
        $this->Cell(40,10,$event['name'],15);
        $this->Ln();
    }
}

$pdf = new PDF();
$pdf->SetFont('Arial','',48);
foreach( getResults() as $event ) {
  $pdf->AddPage();
  $pdf->EventTable($event);  
}
$pdf->Output();
?>

Instead of driving the FPDF class from the outside we extend the FPDF class with our own PDF subclass. Within that subclass, we create a new method called EventTable that builds a table of results for a given event. In this case, we start small and just put out the name of the event. That name is wrapped in a foreach loop at the bottom of the script that adds a page for each event, then invokes the EventTable method.

Building a results table

No table structure is as easy as HTML when you are building PDF files. The way to build tables is to build a bunch of cells that have various widths, fonts, fill color, line color and so on.

Listing 6 shows the addition code that sets up the header bar for the table.

Listing 6. Adding the results table header

<?php
define('FPDF_FONTPATH','/Library/WebServer/Documents/derby/font/');

require( 'fpdf.php' );
require( 'getresults.php' );

class PDF extends FPDF {
    function EventTable($event) {
        $this->SetFont('','B','24');
        $this->Cell(40,10,$event['name'],15);
        $this->Ln();

        $this->SetXY( 10, 45 );

        $this->SetFont('','B','10');
        $this->SetFillColor(128,128,128);
        $this->SetTextColor(255);
        $this->SetDrawColor(92,92,92);
        $this->SetLineWidth(.3);

        $this->Cell(70,7,"Team 1",1,0,'C',true);
        $this->Cell(20,7,"Score 1",1,0,'C',true);
        $this->Cell(70,7,"Team 2",1,0,'C',true);
        $this->Cell(20,7,"Score 2",1,0,'C',true);
        $this->Ln();
    }
}

$pdf = new PDF();
$pdf->SetFont('Arial','',10);
foreach( getResults() as $event ) {
  $pdf->AddPage();
  $pdf->EventTable($event);  
}
$pdf->Output();
?>

The additional code here sets up the font, colors, and line width. Then it renders a few cells with the four header columns. It then calls the Ln method, which is the equivalent of a carriage return to start a new line.

To render the game results, add the code in Listing 7.

Listing 7. Adding the full results table

<?php
define('FPDF_FONTPATH','/Library/WebServer/Documents/derby/font/');

require( 'fpdf.php' );
require( 'getresults.php' );

class PDF extends FPDF {
    function EventTable($event) {
        $this->SetFont('','B','24');
        $this->Cell(40,10,$event['name'],15);
        $this->Ln();

        $this->SetFont('','B','10');
        $this->SetFillColor(128,128,128);
        $this->SetTextColor(255);
        $this->SetDrawColor(92,92,92);
        $this->SetLineWidth(.3);

        $this->Cell(70,7,"Team 1",1,0,'C',true);
        $this->Cell(20,7,"Score 1",1,0,'C',true);
        $this->Cell(70,7,"Team 2",1,0,'C',true);
        $this->Cell(20,7,"Score 2",1,0,'C',true);
        $this->Ln();

        $this->SetFillColor(224,235,255);
        $this->SetTextColor(0);
        $this->SetFont('');

        $fill = false;

        foreach($event['games'] as $game) {
            $this->SetFont('Times',((int)$game['score1']>(int)$game['score2'])?'BI':'');
            $this->Cell(70,6,$game['team1'],'LR',0,'L',$fill);
            $this->Cell(20,6,$game['score1'],'LR',0,'R',$fill);
            $this->SetFont('Times',((int)$game['score1']<(int)$game['score2'])?'BI':'');
            $this->Cell(70,6,$game['team2'],'LR',0,'L',$fill);
            $this->Cell(20,6,$game['score2'],'LR',0,'R',$fill);
            $this->Ln();
            $fill =! $fill;
        }
        $this->Cell(180,0,'','T');
    }
}

$pdf = new PDF();
$pdf->SetFont('Arial','',10);
foreach( getResults() as $event ) {
  $pdf->AddPage();
  $pdf->EventTable($event);  
}
$pdf->Output();
?>

In addition to the header line, you have a foreach loop in the EventTable method that iterates through each of the games.

The $fill variable toggles to alternate the color of each row in the table. The names and scores of the winning teams are in a bold, italic font, which makes them really stand out. Also note that the font changes from Arial for the headers to Times for the game content.
To finish the example code, you'll want to add some graphics.

Dressing it up with graphics

Adding images to a PDF is remarkably easy. To make it happen, first grab an image off the web. I grabbed the logo of one of the roller derby teams and stored it as a PNG. From there, I used the new code in Listing 8.

Listing 8. Adding a logo image

<?php
define('FPDF_FONTPATH','/Library/WebServer/Documents/derby/font/');

require( 'fpdf.php' );
require( 'getresults.php' );

class PDF extends FPDF {
    function EventTable($event) {
        $this->Image('logo.png',5,5,33);

        $this->SetXY( 40, 15 );

        $this->SetFont('','B','24');
        $this->Cell(40,10,$event['name'],15);
        $this->Ln();

        $this->SetXY( 10, 45 );

        $this->SetFont('','B','10');
        $this->SetFillColor(128,128,128);
        $this->SetTextColor(255);
        $this->SetDrawColor(92,92,92);
        $this->SetLineWidth(.3);

        $this->Cell(70,7,"Team 1",1,0,'C',true);
        $this->Cell(20,7,"Score 1",1,0,'C',true);
        $this->Cell(70,7,"Team 2",1,0,'C',true);
        $this->Cell(20,7,"Score 2",1,0,'C',true);
        $this->Ln();

        $this->SetFillColor(224,235,255);
        $this->SetTextColor(0);
        $this->SetFont('');

        $fill = false;

        foreach($event['games'] as $game) {
            $this->SetFont('Times',((int)$game['score1']>(int)$game['score2'])?'BI':'');
            $this->Cell(70,6,$game['team1'],'LR',0,'L',$fill);
            $this->Cell(20,6,$game['score1'],'LR',0,'R',$fill);
            $this->SetFont('Times',((int)$game['score1']<(int)$game['score2'])?'BI':'');
            $this->Cell(70,6,$game['team2'],'LR',0,'L',$fill);
            $this->Cell(20,6,$game['score2'],'LR',0,'R',$fill);
            $this->Ln();
            $fill =! $fill;
        }
        $this->Cell(180,0,'','T');
    }
}

$pdf = new PDF();
$pdf->SetFont('Arial','',10);
foreach( getResults() as $event ) {
  $pdf->AddPage();
  $pdf->EventTable($event); 
}
$pdf->Output();
?>

The key method in Listing 8 is the Image method, which takes a file name for the image, a location and a width. All of the additional parameters are optional so you specify only as much information as you want.
Some new calls to SetXY move the text and table around to appropriate positions to keep them from overwriting the image.

With all the other methods provided by the library to render graphics, add flowing text, add hyperlinks and manage page mechanics such as margins and orientation, you have complete control over your PDF files.