Symfony 2 PDF service using LaTeX
Posted on 19 April 2015 in Web development
Warning
This Symfony 2 article is obsolete. Symfony 2 reached end-of-life in November 2018.
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 the LaTeX typesetting system . LaTeX is ideally 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:
- TCPDF
- HTML layout and rendering engine written in PHP.
- Coordinate-based interface.
- dompdf
- HTML layout and rendering engine written in PHP.
- Style-driven renderer.
- wkhtmltopdf
- Command line tools to render HTML into PDF (and various other image formats) using the QT WebKit rendering engine.
- KnpLabs/KnpSnappyBundle
- Symfony 2 bundle wrapping wkhtmltopdf.
- 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, 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:
The PDF LaTeX service consists of the following three components:
- The Symfony service container configured in services.yml.
- A class Pdflatex of which an instance is available in the service container.
- 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()
);
}
}