After playing with creating PDFs with PHP using fPDF for a while, and trying to get everything to work consistently, I discovered tcpdf, which is a fork of fpdf that includes everything that anyone has ever added to the original. And I mean everything; this thing is huge! I printed out the source to see how it differed from the original, and it ran more than 500 pages. Good thing they're so generous to me at work.

Most of the size is due to the SVG and HTML formatting, which I don't need, but the biggest advantage is that Unicode font subsetting works. Mostly.

tfpdf, the Unicode-enabled version that comes with fpdf, supports Unicode fonts but they don't show up on the iPhone. Apple's PDF viewer is somehow different from Adobe's and reads the fonts differently. tcpdf does a better job (displays in Adobe Reader but generates an error for the HumaneJenson font): the Droid fonts work on the iPhone, though the DejaVu fonts do not. Try those last links on the iPhone; the built-in Helvetica fonts show up but DejaVu does not. Try refreshing the test page multiple times; it randomly selects fonts to display each time. Some fonts generate errors in Adobe Reader but display, some don't display at all and some don't display on the iPhone. It all seems very random, but at least I have a set of open-source true type fonts that I can include.

It also does most of the things I need: PNG graphics with transparency, form fields like text boxes (I played with that one for weeks with tfpdf, but it never worked the way I wanted it to), rotating text. The API is clunky and poorly documented and I definitely like my routines better, but this is done and someone else maintains it. A huge advantage. I can write my own interface routines to be more elegant if I want.

Using tcpdf

Unlike tfpdf, you have to parse the true type fonts first to create PHP files. In the fonts folder there's a utils folder; copy the contents of that into your font folder then run the script makeallttffonts.php and it will create the necessary files (as above, they're not perfect).

The basic page is similar to fpdf:

define ('K_PATH_FONTS', '/path/to/fonts/'); // some constants are named differently
$pdf = new TCPDF('P', 'in', 'letter');
$pdf->setPrintHeader(false); // it automatically includes a header and footer (just a line by default) unless told otherwise
$pdf->setPrintFooter(false);
$pdf->AddPage();
$pdf->AddFont('DroidSerif-Regular'); // using true type fonts
$pdf->SetFont('DroidSerif-Regular', '', 12);
$pdf->Write(0, 'Hello, world ');
$pdf->Output();

Graphics can use URL's directly, which is nice (it uses CURL internally). It's inefficient, in that it reloads the image from the internet each time it's used rather than caching it. If that matters, download the image yourself and pass the file. Images are placed based on their upper left corner, and there's no way to get the size of the image to be able to calculate the center (that's another argument for downloading an image yourself: you can use getimagesize on the resulting file). I wrote a minor hack to use the class's protected methods to center an image (see the result):

class pdf_imageinfo extends TCPDF{
  // adds a function to return the image information
  function lastimageinfo(){
    return $this->images[$this->imagekeys[count($this->imagekeys)-1]];
  }
}

$pdf = new pdf_imageinfo('P', 'in', 'letter');
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
$pdf->AddPage();

markImage($pdf, 'http://bililite.com/images/silk/bug.png', 2, 2);
markImage($pdf, 'http://bililite.nfshost.com/blog/blogfiles/pdf/dice.png', 4, 6);
$pdf->Output();

function markImage($pdf, $url, $x, $y){
  // place an image file $url centered at ($x, $y) with crosshairs
  $pdf->Image($url, -1000); // hide the image. Not too inefficient since we're going to use it anyway, though tcpdf will call cURL each time
  $info = $pdf->lastimageinfo();
  $w = $info['w']/$pdf->GetScaleFactor(); // sizes are in pixels; we want user units
  $h = $info['h']/$pdf->GetScaleFactor();
  $pdf->SetAlpha(1);
  $pdf->Line ($x,$y-$h,$x,$y+$h); // draw crosshairs to show center
  $pdf->Line ($x-$w,$y,$x+$w,$y);
  $pdf->SetAlpha(0.5); // demonstrates using transparency in images
  $pdf->Image($url, $x-$w/2, $y-$h/2);
}

Text is a little different from fpdf. The Text function in fpdf places the baseline of the text, but in tcpdf it's aligned with the top left, unless the calign parameter is set, which is 14 parameters down the list. Since there's no way to specify named parameters in the call that means a long, inconvenient call: $pdf->Text ($x, $y, $s, FALSE,FALSE,TRUE,0,0,'',FALSE,'',0,FALSE,'L');. In addition, there's a cell margin and cell padding that's added, so if you want to precisely place the text you have to set those to zero (margin is defaulted to 0 but padding is not) (these are the same as in CSS: margin surrounds border which surrounds padding).

Rotating is straightforward: call StartTransform, Rotate (in degrees!), then StopTransfom. So to rotate text around the left end of the baseline (see the result):

$pdf = new TCPDF('P', 'in', 'letter');
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
$pdf->AddPage();
$num = 6;
$pdf->SetCellPaddings(0);
$s = 'Hello, world';
// draw crosshairs
$pdf->Line(4,3,4,5);
$pdf->Line(3,4,5,4);
$pdf->Line(4,5,4,7);
$pdf->Line(3,6,5,6);
// Draw rotated text
for ($i = 0; $i < $num; ++$i){
  $angle = 360*$i/$num;
  $color = array (0, 0, 0, 127*$i/$num);
  text ($pdf, $s, 4, 4, $angle, 'DroidSans', 22, $color);
  text ($pdf, 'Hello', 4, 6, $angle, 'Lilly', 7, $color);
}
$pdf->Output();

function text ($pdf, $s, $x, $y, $angle, $font, $size, $color){
  $pdf->StartTransform();
  $pdf->SetFont($font, '', $size);
  $pdf->SetTextColor ($color[0], $color[1], $color[2]);
  $pdf->SetAlpha ( (127-$color[3])/127 ); // change from PHP to PDF units for transparency
  $pdf->Rotate ($angle, $x, $y); // rotate origin
  $pdf->Text ($x, $y, $s, FALSE,FALSE,TRUE,0,0,'',FALSE,'',0,FALSE,'L'); 
  $pdf->StopTransform();	
}

Text fields are straightforward; set the default properties (borders, colors) with SetFormDefaultProp; the documentation for the properties are in the Adobe Javascript manual. Create a text field with TextField, though setting the properties in that function doesn't work for me. One trick is that Reader uses a font size of 0 to indicate that the size should adjust to fit. Unfortunately, tcpdf uses a font size of 0 to indicate no change, but there's a hack: on output it rounds numbers to 2 decimal places. So setting the font size to 0.001 works as well.

The PDF's I'm creating are generally used as printable forms, with underlines for the fillable fields, but with text boxes that can be filled in online. So I want a text field with no border but with the underline overprinted. This does that (see the result):

$s = $_GET['s'];
preg_match_all('%_+|[^_]+%', $s, $matches);
// $matches[0] is the array of text, split into underlines and other
define ('K_PATH_FONTS', '/path/to/fonts/');
$pdf = new TCPDF('P', 'in', 'letter');
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
$pdf->SetCellPaddings(0); // so text aligns precisely
$pdf->SetFont('Helvetica', '', 12); // always use a built-in font for text fields, since there's no way to know what characters will be used
$height = 14/$pdf->GetScaleFactor(); // 14-point line height
$pdf->AddPage();
$pdf->setFormDefaultProp(array('lineWidth'=>0)); // no border
$fieldcount=0;
foreach ($matches[0] as $text){
  if (strpos($text, '_') !== FALSE){
    // It's an underline. Create a text field of the same size
    $x = $pdf->GetX();
    $width = $pdf->GetStringWidth($text);
    $pdf->SetFontSize(0.001); // automagic font size hack
    $pdf->TextField ('text'.($fieldcount++), $width, $height);
    $pdf->SetX($x); // reset the location and font
    $pdf->SetFontSize(12);
  }
  $pdf->Write($height, $text);

}
$pdf->Output();

Now I can go and get the webservices PDF's working correctly!

2 Comments

  1. Wade says:

    What is ‘s’ in $s = $_GET[‘s’]; ? Can you post the entire code used in “the result”?

    Thanks

  2. Danny says:

    @Wade:
    $s is the text to print, passed to the page as http://bililite.com/blog/blogfiles/pdf/pdftest.php?which=tcpdf-x3&s=Some%20Text.
    The code is exactly as written in the post; just include ('tcpdf.php');.
    –Danny

Leave a Reply


Warning: Undefined variable $user_ID in /home/public/blog/wp-content/themes/evanescence/comments.php on line 75