Portable Document Format (PDF) has become a universally accepted format for sharing documentation. As a result, the dynamic generation of PDF documents is an expected feature of many web applications. After reviewing a number of libraries for generating PDF documents it was decided to write a service wrapping LaTeX; a typesetting system with features suited to the production of scientific and technical documentation.

The code examples used in this article are from the certificate generation component of IB2020, a Symfony 2 web application for the management of welder qualifications.

PDF generating libraries

Before implementing the LaTeX service the following PDF generating libraries were considered:

  1. TCPDF

    • HTML layout and rendering engine written in PHP.
    • Coordinate-based interface.
  2. dompdf

    • HTML layout and rendering engine written in PHP.
    • Style-driven renderer.
  3. wkhtmltopdf

    • Command line tools to render HTML into PDF (and various other image formats) using the QT Webkit rendering engine.
  4. KnpLabs/KnpSnappyBundle

    • Symfony 2 bundle wrapping wkhtmltopdf.
  5. zendframework/ZendPdf

    • HTML layout and rendering engine written in PHP.
    • Coordinate-based interface.

LaTeX

The LaTeX system is a markup language and high-quality typesetting system. It is written in the TeX macro language.

A PDF document may be generated directly from a LaTeX source document with the pdflatex binary which is distributed as part of the Debian texlive-full meta-package. Some LaTeX documents require multiple passes with pdflatex, a process which is automated with the script rubber-pipe.

LaTeX and rubber-pipe may be installed on Debian-based distributions with the following commands:

$ sudo apt-get install texlive-full
$ sudo apt-get install rubber

The PDF LaTeX service is essentially a wrapper around rubber-pipe which in turn invokes pdflatex.

Symfony 2 service

Services are re-usable, decoupled components, that may be accessed from any part of the application. The definition of a service from the Symfony documentation:

A service is usually used "globally", such as a database connection object or an object that delivers email messages. In Symfony, services are often configured and retrieved from the service container.

The PDF LaTeX service consists of the following three components:

  1. The Symfony service container configured in services.yml.
  2. A class Pdflatex of which an instance will be available in the service container.
  3. A Twig template show.tex.twig used to output the LaTeX source document.

Configure the service container

The service container is configured with services.yml. Two services are defined in the YAML below: 1. electrotech.twig.ib2020_extension, which provides a Twig filer to escape LaTeX special characters; and 2. electrotech.pdf.pdflatex, the PDF LaTeX service itself.

For convenience the rubber-pipe binary is defined as a parameter.

# src/Electrotech/WeldqualBundle/Resources/config/services.yml

parameters:
    electrotech.twig.ib2020_extension.class: Electrotech\WeldqualBundle\Twig\Ib2020Extension
    electrotech.pdf.pdflatex.rubber-pipe: /usr/bin/rubber-pipe

services:
    electrotech.twig.ib2020_extension:
        class: %electrotech.twig.ib2020_extension.class%
        arguments: [%kernel.bundles%]
        tags:
            - { name: twig.extension }

    electrotech.pdf.pdflatex:
        class:        Electrotech\WeldqualBundle\Pdf\Pdflatex
        arguments:    [%electrotech.pdf.pdflatex.rubber-pipe%]

Service object

An instance of the class Pdflatex provides the service. Pdflatex takes a LaTeX source document and returns a PDF document.

<?php
// src/Electrotech/WeldqualBundle/Pdf/Pdflatex.php

namespace Electrotech\WeldqualBundle\Pdf;

class Pdflatex
{

    /**
     * Full system path to rubber-pipe binary
     * @var string
     */
    private $binary;

    /**
     * Options for rubber-pipe
     * @var array
     */
    private $options = array(
        '--pdf' => null,
        '--into' => '/tmp/'
    );

    /**
     * Tex source document
     * @var string
     */
    private $texSource;

    /**
     * Generated PDF document
     */
    private $pdf;

    /**
     * Initial working dir
     * @var string
     */
    private $cwd = '/tmp/';

    /**
     * Environment variables
     * @var array|null
     */
    private $env = null;

    /**
     * Error output
     * @var string
     */
    private $stderr = null;

    /**
     * Return value
     * @var integer
     */
    private $returnValue;


    public function __construct($binary)
    {
        $this->binary = $binary;
    }

    /**
     * Create rubber-pipe command
     */
    public function getCommand()
    {
        $args = '';
        foreach ($this->options as $option => $value)
        {
            $args .= ' '.$option;
            if ($value !== null)
            {
                $args .= ' '.$value;
            }
        }
        return $this->binary.$args;
    }

    /**
     * Execute rubber-pipe command
     */
    public function execute()
    {
        $descriptorSpec = array(
            0 => array("pipe", "r"),
            1 => array("pipe", "w"),
            2 => array("pipe", "w"),
        );

        $process = proc_open(
            $this->getCommand(),
            $descriptorSpec,
            $pipes,
            $this->cwd,
            $this->env
        );

        if (is_resource($process)) {
            fwrite($pipes[0], $this->getTexSource());
            fclose($pipes[0]);

            $this->pdf = stream_get_contents($pipes[1]);
            $this->stderr = stream_get_contents($pipes[2]);
            $this->returnValue = proc_close($process);
        }

        if ($this->returnValue == 0)
        {
            return true;
        }
        return false;
    }

    /**
     * Set path to rubber-pipe binary
     * @param string $binary Full system path to rubber-pipe binary
     */
    public function setBinary($binary)
    {
        $this->binary = $binary;
    }

    /**
     * Get path to rubber-pipe binary
     * @return string Full system path to rubber-pipe binary
     */
    public function getBinary()
    {
        return $this->binary;
    }

    /**
     * Set LaTeX source
     * @param string $texSource LaTeX source document
     */
    public function setTexSource($texSource)
    {
        $this->texSource = $texSource;
    }

    /**
     * Get LaTeX source
     * @return string LaTeX source document
     */
    public function getTexSource()
    {
        return $this->texSource;
    }

    /**
     * Get PDF file contents
     * @return mixed Generated PDF file contents
     */
    public function getPdf()
    {
        return $this->pdf;
    }

    /**
     * Get errors
     * @return string Error output
     */
    public function getStderr()
    {
        return $this->stderr;
    }

    /**
     * Get return value
     * @return integer Return value from rubber-pipe command
     */
    public function getReturnValue()
    {
        return $this->returnValue;
    }

}

Twig template

A Twig template show.tex.twig is used to generate the LaTeX source document.

% src/Electrotech/WeldqualBundle/Resources/views/Testweld/show.tex.twig

% This template has been simplified for the sake of brevity.

\documentclass[10pt,a4paper]{article}

\usepackage{array}
\usepackage{calc}
\usepackage{color}
\usepackage{colortbl}
\usepackage{graphicx}
\usepackage[margin=1cm]{geometry}
\usepackage{multirow}
\usepackage{tabularx}
\usepackage{wasysym}

% width of table columns
\newlength{\colOneWidth}
\setlength{\colOneWidth}{0.13\textwidth}
\newlength{\colThreeWidth}
\setlength{\colThreeWidth}{0.13\textwidth}
\newlength{\colFourWidth}
\setlength{\colFourWidth}{0.25\textwidth}
\newlength{\colFiveWidth}
\setlength{\colFiveWidth}{0.13\textwidth}
\newlength{\colThreeToFiveWidth}
\setlength{\colThreeToFiveWidth}{\colThreeWidth + \colFourWidth + \colFiveWidth}
\newlength{\colFourToFiveWidth}
\setlength{\colFourToFiveWidth}{\colFourWidth + \colFiveWidth}


% colours
\definecolor{IB2020Blue}{RGB}{172,206,230} % #ACCEE6
\definecolor{invalidBg}{RGB}{242,222,222}  % #F2DEDE
\definecolor{invalidFg}{RGB}{185,74,72}    % #B94A48

% page style
\pagestyle{empty} % remove page numbering

% PDF meta data
\pdfinfo{
    /Title (Welder Qualification Certificate)
    /Creator (IB2020 {{ electrotech_system_owner | e_latex }})
    /Producer (IB2020 {{ electrotech_system_owner | e_latex }})
    /Author (IB2020 A Management Information System for Inspection Bodies)
    /CreationDate (D:{{ "now"|date("YmdGisO") | e_latex }})
    /ModDate (D:{{ "now"|date("YmdGisO") | e_latex }})
    /Subject (Welder Qualification Certificate)
    /Keywords (IB2020)
}


\begin{document}

% remove left indent from table
\noindent%
\begin{tabularx}{\textwidth}{@{}|p{\colOneWidth}|X|p{\colThreeWidth}|p{\colFourWidth}|p{\colFiveWidth}| }
    \hline
        \centering \scriptsize{}Certificate Number\newline \normalsize {{ entity.certificateNumber | e_latex }} &
        \multicolumn{3}{c|}{ \cellcolor{IB2020Blue} \textbf{Welder Qualification Certificate} } &
        \raisebox{-0.5\height}{
            \includegraphics[width=0.13\textwidth]{{ '{' }}{{ logoFile | e_latex }}{{ '}' }}
        } \\
    \hline
        \multicolumn{5}{|c|}{
            {{ electrotech_system_owner | e_latex }}
            \enspace
            IANZ Accredited Inspection Body No. {{ electrotech_ianz_number | e_latex }}
        } \\
    \hline
\end{tabularx}

\end{document}

A custom Twig filter e_latex is used to escape LaTeX special characters. Custom Twig filters are created by extending Twig_Extension.

<?php
// src/Electrotech/WeldqualBundle/Twig/Ib2020Extension.php

namespace Electrotech\WeldqualBundle\Twig;

use Twig_Extension;
use Twig_Filter_Method;
use Twig_Test_Method;

class Ib2020Extension extends Twig_Extension
{
    // Unrelated methods have been omitted from this code sample for the sake
    // of brevity.

    private $kernelBundles;

    public function __construct($kernelBundles)
    {
        $this->kernelBundles = $kernelBundles;
    }

    /**
     * Returns a list of filters to add to the existing list.
     *
     * @return array An array of filters
     */
    public function getFilters()
    {
        return array(
            'e_latex'     => new Twig_Filter_Method($this, 'escapeLatexFilter'),
        );
    }

    /**
     * Escape LaTeX special characters
     *
     * @return string
     */
    public function escapeLatexFilter($str = null)
    {
        $search = array('\\', '#', '$', '%', '&', '_', '{', '}', '~', '^',
            '>', '<');

        $replace = array('\textbackslash ', '\#', '\$', '\%', '\&', '\_',
            '\{', '\}', '\textasciitilde ', '\textasciicircum ',
            '\textgreater', '\textless');

        return str_replace($search ,$replace ,$str);
    }

    /**
     * Returns the name of the extension
     *
     * @return string The extension name
     */
    public function getName()
    {
        return 'electrotech_twig_ib2020_extension';
    }

}

Utilising the service

The service is used in the controller by passing a certificate ID to the method pdfAction(). The LaTeX source document is then generated by the method latexSource().

<?php
// Utilising the PDF LaTeX service
$pdflatex = $this->get('electrotech.pdf.pdflatex');
$pdflatex->setTexSource($latexSource);
$pdf = $pdflatex->getPdf()

Below is an example of how this service is used in a controller.

<?php
// src/Electrotech/WeldqualBundle/Controller/TestweldController.php

namespace Electrotech\WeldqualBundle\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use Electrotech\WeldqualBundle\Entity\Testweld;
use Electrotech\WeldqualBundle\Entity\Testweldassessment;
use Electrotech\WeldqualBundle\Form\TestweldType;
use Electrotech\WeldqualBundle\Helper\QualificationRangeHelper;


/**
 * Testweld controller
 */
class TestweldController extends Controller
{
    // Unrelated methods have been omitted from this code sample for the sake
    // of brevity.

    /**
     * Creates a PDF certificate
     */
    public function pdfAction($id)
    {

        $latexSource = $this->latexSource($id, 'show.tex.twig');

        $pdflatex = $this->get('electrotech.pdf.pdflatex');
        $pdflatex->setTexSource($latexSource['latex']);

        if (!$pdflatex->execute())
        {
            throw new HttpException(500, 'Error creating PDF: '.$pdflatex->getStderr());
        }

        $response = new Response();
        $response->setContent($pdflatex->getPdf());
        $response->headers->set('Content-Type', 'application/pdf');
        $response->headers->set('Content-Disposition', 'inline; filename="'.$latexSource['filename'].'.pdf"');

        return $response;
    }

    /**
     * Creates LaTeX source
     */
    private function latexSource($id, $template)
    {
        $em = $this->getDoctrine()->getManager();

        $entity = $em->getRepository('ElectrotechWeldqualBundle:Testweld')->find($id);

        if (!$entity) {
            throw $this->createNotFoundException('Unable to find Testweld entity.');
        }

        $em = $this->getDoctrine()->getManager();

        $weldVariables = $em->getRepository('ElectrotechWeldqualBundle:Weldvariables')
            ->fetchWeldVariables(
                $entity->getQualificationstandard()->getEdition()->getTechdoc(),
                $entity->getProducttype(),
                $entity->getWeldtype(),
                $entity->getWeldposition(),
                $entity->getWelddirection()
            );

        $qualifiedRange = null;

        if ($weldVariables !== null)
        {
            $qualifiedRange = new QualificationRangeHelper(
                $entity->getProducttype(),
                $entity->getPipeod(),
                $weldVariables->getQualifiedweldvariablesid()
            );
        }

        $logoFile = $this->get('kernel')->getRootDir().DIRECTORY_SEPARATOR.
            $this->container->getParameter('electrotech_upload_dir').DIRECTORY_SEPARATOR.
            'sysowner'.DIRECTORY_SEPARATOR.'logo.pdf';

        $templating = $this->get('templating');

        $latexSource = $templating->render(
            'ElectrotechWeldqualBundle:Testweld:'.$template,
            array(
                'entity'         => $entity,
                'logoFile'       => $logoFile,
                'qualifiedRange' => $qualifiedRange,
            )
        );

        return array(
            'latex'    => $latexSource,
            'filename' => $entity->getFilename()
        );
    }
}

Further reading