Printing Magento currencies and prices on a PDF

This is a short little post, primarily just to remind me. We need to convert to html entities or symbols like $ and £ won’t print. There are lots of ways of getting the price from the product object, but getFinalPrice seems to be the simplest.

$raw_price = $this->getProduct()->getFinalPrice(); // string "£3,295.00"
$price =  htmlspecialchars(Mage::helper('core')->currency($raw_price,true,false));

HTML to PDF using wkhtmltopdf

Converting web pages to PDF? Sick of various libs and their various quirks and complexity? Wkhtmltopdf is a linux program that can reliably generate pdfs from html content. It’s as easy as running:

wkhtmltopdf http://google.com google.pdf

from the command line. However, we want it in our web site! First off though, you’ll need it installed. If you are using puPHPet and vagrant, add the following in your config.yaml under server: packages:

server:
    install: '1'
    packages:
        - wkhtmltopdf

Then run vagrant provision. Standard linux servers will install by using yum or apt-get:

sudo apt-get install wkhtmltopdf

You can get the PHP wrapper class here:

https://github.com/mikehaertl/phpwkhtmltopdf

From within your PHP, you can say:

        use mikehaert\wkhtmlto\Pdf;

        $pdf = new Pdf(array('tmpDir' => '/optional/tmp/folder/here'));
        $pdf->addPage('http://google.com');

        // Save the PDF
        // $pdf->saveAs('/tmp/new.pdf');

        // ... or send to client for inline display
        if (!$pdf->send()) {echo $pdf->getError();}
        
        // ... or send to client as file download
        // $pdf->send('test.pdf');

You may get an error saying it cannot connect to the X server. If you get this, you probably don’t have X installed! I didn’t, it’s a web server, not a desktop machine!

In the event that you have no X server, go into the Command.php class, you will see the following option:

    /**
     * @var bool whether to enable the built in Xvfb support (uses xvfb-run)
     */
    public $enableXvfb = false;

Change this to true, and try again. This time it should work!

If it still doesn’t, then possibly you also need to install xvfb, again with apt-get or yum, then add a startup script for it. I didn’t have to do any of this, but for those of you who may need to, complete the following steps:

sudo nano /etc/init.d/xvfb

XVFB=/usr/bin/Xvfb
XVFBARGS=":0 -screen 0 1024x768x24 -ac +extension GLX +render -noreset"
PIDFILE=/var/run/xvfb.pid
case "$1" in
  start)
    echo -n "Starting virtual X frame buffer: Xvfb"
    start-stop-daemon --chuid www-data --start --quiet --pidfile $PIDFILE --make-pidfile --background --exec $XVFB -- $XVFBARGS
    echo "."
    ;;
  stop)
    echo -n "Stopping virtual X frame buffer: Xvfb"
    start-stop-daemon --chuid www-data --stop --quiet --pidfile $PIDFILE
    echo "."
    ;;
  restart)
    $0 stop
    $0 start
    ;;
  *)
        echo "Usage: /etc/init.d/xvfb {start|stop|restart}"
        exit 1
esac

exit 0

Enable the init script:

update-rc.d xvfb defaults 10

Start it up:

/etc/init.d/xvfb start

Check its running:

ps auxU www-data | grep [X]vfb

Now try reloading your page, and with a bit of luck you should see a PDF version of the google home page! Play around with it and have fun!

Google QR Codes and Zend_PDF

Now that we can generate QR codes using the Google Chart API, and we have a controller action that serves it up for us with the correct .PNG header (see my previous post), we need to get it where it’s of any use – on paper!

So following on from my last post about Zend_Pdf, we can now do the following to get it in our PDF:

//draw qr code
 $target = 'http://example.co.uk/download/qr?target=http://www.example.co.uk/properties/view/id/'.$prop->getID();
 $img = file_get_contents($target); 
 file_put_contents(APPLICATION_PATH.'/uploads/tmp/'.$prop->getID().'-qr.png',$img);
 $image = Zend_Pdf_Image::imageWithPath(APPLICATION_PATH.'/uploads/tmp/'.$prop->getID().'-qr.png');
 $page->drawImage($image, 13, 13, 94,94);
 unlink(APPLICATION_PATH.'/uploads/tmp/'.$prop->getPid().'-qr.png');

The first line of code is the url of our download controllers QR action.  file_get_contents stores the image in memory, which we then save into a temp folder using file_put_contents. These commands are the equivalent of a lot of fopen etc, so use them if you don’t already!  This example uses my property object to make a dynamic filename, but this isn’t necessary, but I don’t need to tell you that!

We then use Zend_Pdf to load our png, and when drawing an image, the numbers in the argument are x1, y1, x2, y2 being bottom left and top right corners respectively. Finally, ditch the temp file with the unlink command. Have Fun!

Zend_PDF with templates

Zend_PDF rocks! It makes drawing a PDF almost child’s play. The main time eater though is the trial and error with the co-ordinates. So I stopped all this nonsense, and used Illustrator.

In adobe Illustrator, set the ruler origin to the bottom left of the page. Now in the transform pallette you can get the XY co-ordinates! Once you have created a mock up of the PDF, save a copy minus all the dynamic stuff, which we’ll drop in from our controller action.

First up, ditch the view and layout:

//disable the layout
 Zend_Layout::getMvcInstance()->disableLayout(); 
 $this->_helper->viewRenderer->setNoRender();

Next we create the Zend_PDF and templates, and set default fonts and colours:

//create a zend pdf
 $template = Zend_Pdf::load(APPLICATION_PATH.'/layouts/pdfs/proplist.pdf');
 $this->_pdf = new Zend_Pdf();
 $extractor = new Zend_Pdf_Resource_Extractor();
//set font
 $font = Zend_Pdf_Font::fontWithName(Zend_Pdf_Font::FONT_HELVETICA);
 $bold = Zend_Pdf_Font::fontWithName(Zend_Pdf_Font::FONT_HELVETICA_BOLD);
//set colors
 $black = new Zend_Pdf_Color_Html('#000000');
 $red = new Zend_Pdf_Color_Html('#ED1F24');
 $green = new Zend_Pdf_Color_Html('#009444');
 $blue = new Zend_Pdf_Color_Html('#0069B5');

We will have so many records per page, so the next bit goes in a for loop:

//add page
 $page = $extractor->clonePage($template->pages[0]);

 //get properties for this page
 $props = $this->_helper->paginator($rows,6);

We can set colours and fonts like this:

$page->setFillColor($black);
$page->setFont($bold, 11);

Add the page to the pdf like this

$pdf->pages[] = $page;

I created a function for getting the coordinates of each element, which adjusts depending which record on the page we are dealing with. This could be tweaked to suit your own project.

private function getCoordinates($row, $elem)
 {
 $xCoords = array(
 'header' => 21,
 'avail_date' => 419,
 'price' => 541,
 'summary' => 21,
 'hol_dep' => 70,
 'dep_date' => 128,
 'board_up' => 217,
 'keys' => 267,
 'tax_band' => 333,
 'on_web' => 388,
 'ref' => 437,
 'images' => 560,
 'condition' => 59,
 'move_date' => 130,
 'gas_safety' => 221 ,
 'hwsa' => 271 ,
 'let_fee' => 327,
 'man_fee' => 391,
 'landlord' => 452,
 'lha' => 551,
 'comment' => 64
 );

 $yCoords = array(
 'header' => 759,
 'avail_date' => 759,
 'price' => 759,
 'summary' => 739,
 'hol_dep' => 692,
 'dep_date' => 692,
 'board_up' => 692,
 'keys' => 692,
 'tax_band' => 692,
 'on_web' => 692,
 'ref' => 692,
 'images' => 692,
 'condition' => 680,
 'move_date' => 680,
 'gas_safety' => 680,
 'hwsa' => 680,
 'let_fee' => 680,
 'man_fee' => 680,
 'landlord' => 680,
 'lha' => 680,
 'comment' => 667
 );

 $z = array();
 $z['x'] = $xCoords[$elem];

 switch($row)
 {
 case 1:
 default:
 $z['y'] = $yCoords[$elem];
 break;
 case 2:
 $z['y'] = $yCoords[$elem] - 127.5;
 break;
 case 3:
 $z['y'] = $yCoords[$elem] - (127.5 * 2);
 break;
 case 4:
 $z['y'] = $yCoords[$elem] - (127.5 * 3);
 break;
 case 5:
 $z['y'] = $yCoords[$elem] - (127.5 * 4);
 break;
 case 6:
 $z['y'] = $yCoords[$elem] - (127.5 * 5);
 break;
 }
 return $z;
 }

Finally we can draw the text like this:

$text = '?';
 $page->setFillColor($blue);
 $z = $this->getCoordinates($row, 'condition');
 $page->drawText($text, $z['x'], $z['y'],'UTF-8');

Other things include fitting text to a specific width, or making a paragraph of multi lines:

private function widthForStringUsingFontSize($string, $font, $fontSize)
 {
 $drawingString = iconv('UTF-8', 'UTF-16BE//IGNORE', $string);
 //$drawingString = $string;
 $characters = array();
 for ($i = 0; $i < strlen($drawingString); $i++) {
 $characters[] = (ord($drawingString[$i++]) << 8 ) | ord($drawingString[$i]);
 }
 $glyphs = $font->glyphNumbersForCharacters($characters);
 $widths = $font->widthsForGlyphs($glyphs);
 $stringWidth = (array_sum($widths) / $font->getUnitsPerEm()) * $fontSize;
 return $stringWidth;
 }
private function getFontSizeForWidth($text,$font,$desiredSize,$maxWidth)
 {
 do 
 {
 $stringWidth = $this->widthForStringUsingFontSize($text, $font, $desiredSize);
 $desiredSize = $desiredSize - 0.5; 
 }
 while($stringWidth > $maxWidth);
 return $desiredSize + 0.5;
 }
private function getWrappedText($string, $font, $fontsize,$max_width)
 {
 $wrappedText = '' ;
 $lines = explode("\n",$string) ;
 foreach($lines as $line) {
 $words = explode(' ',$line) ;
 $word_count = count($words) ;
 $i = 0 ;
 $wrappedLine = '' ;
 while($i < $word_count)
 {
 /* if adding a new word isn't wider than $max_width,
 we add the word */
 if($this->widthForStringUsingFontSize($wrappedLine.' '.$words[$i]
 ,$font
 , $fontsize) < $max_width) {
 if(!empty($wrappedLine)) {
 $wrappedLine .= ' ' ;
 }
 $wrappedLine .= $words[$i] ;
 } else {
 $wrappedText .= $wrappedLine."\n" ;
 $wrappedLine = $words[$i] ;
 }
 $i++ ;
 }
 $wrappedText .= $wrappedLine."\n" ;
 }
 return $wrappedText ;
 }

To align text to the right, we subtract the width:

$width = $this->widthForStringUsingFontSize($text, $bold, 12);
 $z = $this->getCoordinates($row, 'price');
 $z['x'] = $z['x'] - $width;

And if we need wrapped text:

$text = $this->getWrappedText($text, $font,10, 550);

Awesome! Another what used to be pain in the arse task is now a piece of cake (or zf rather lol)!