Generando PDF’s con Symfony

Hoy me gustaría hablaros de KnpSnappy, os bundle que os permitirá generar PDF’s desde Symfony sin muchos quebraderos de cabeza, por lo que, en mi opinión, es una de las mejores soluciones si tenéis que enfrentaros a problemas de este tipo.

Emplearé para ello el bundle KnpSnapppy el cual integra la librería wkhtmltopdf que, como su propio nombre indica, transforma el HTML que le pasemos en PDF.

Para empezar, hay que instalar el bundle de la forma habitual mediante composer:

composer require knplabs/knp-snappy-bundle

y, a continuación, crear un archivo de configuración en el que especificaremos dónde se encuentra la librería wkhtmltopdf:

# /config/packages/knp_snappy.yml
knp_snappy:
  process_timeout: 60
  pdf:
  enabled: true
  binary: ‘%env(WKHTMLTOPDF_LIBRARY)%’
  options:
    - { name: ‘viewport-size’, value: ‘1024x768’ }
    - { name: ‘page-size’, value: ‘A4’ }

Como veis, la ruta la he configurado con una variable de entorno para poder modificar su valor dependiendo de la máquina en donde despleguemos el proyecto. Además, se pueden aprovechar el resto de opciones de knp_snappy para establecer tanto el tamaño del viewport como el de la página.

Finalmente, hay que añadir la siguiente línea al archivo config/bundles.php:

Knp\Bundle\SnappyBundle\KnpSnappyBundle::class => [‘all’ => true]

El siguiente paso será crear el controlador encargado de pintar el pdf, por ejemplo:

Y generar los archivos twig correspondientes. Al tratarse de un ejemplo básico, el archivo tendrá el siguiente aspecto:

Aunque podremos trabajar normalmente heredando plantillas e incluyéndolas hasta lograr el aspecto que queramos. Finalmente, en el proyecto he estado empleando webpack para gestionar los assets (de ahí el archivo manifest.js que aparece al final del gist) y no he encontrado ningún problema de compatibilidad a la hora de dar estilos a la página.

Finalmente, accediendo a la ruta download el PDF comenzará a descargarse con el texto “Esto es una prueba” escrito en él. Fácil y sencillo.

Repositorio de wkhtmltopdf

Buscando por Internet he encontrado el siguiente repositorio donde están las imágenes de wkhtmltopdf disponibles desde composer:

https://github.com/h4cc/wkhtmltoimage-i386

lo cual ahorraría tiempo en tener que instalar la librería en el servidor donde vayamos a desplegar la aplicación. Sin embargo, no tienen versión para Mac OS, por lo que en nuestro caso concreto no nos ha servido.

Importante. Os recomiendo instalar la librería parcheada con qt para que el rendereado del PDF os funcione correctamente.

Consejos para emplear este bundle

Tener un archivo html base

En base a mi experiencia usando este bundle, recomiendo tener un archivo base que contenga un código similar al siguiente:

Ya que este archivo será el que posteriormente extenderé en el momento de crear las páginas del PDF así como el header y el footer, por lo que es una buena manera de centralizar los estilos y javascripts que vayamos a emplear.

Cabecera y footer

KnpSnappy permite configurar la cabecera y el footer de todas las páginas del PDF que estamos generando (aunque por el momento no existe la opción de deshabilitarlas para ciertas páginas como la portada o la página final). Para ello lo primero será escribir el template de la cabecera (por ejemplo) con el contenido que queramos, por ejemplo:

Si os fijáis, estoy extendiendo el archivo base que os comentaba en el punto anterior. Esto es porque, a continuación, rendearemos este template con twig en nuestro controlador y, si no tiene la estructura completa de un documento HTML, no seremos capaces de añadir estilos o comportamiento Javascript (además de que se la librería wkhtmltopdf se lía al intentar convertir a PDF documentos que no tienen la estructura típica de un HTML).

Como os decía, el siguiente paso será renderear este archivo en el controlador:

// DownloadController.php
$header = $this->renderView(‘pdf/header.html.twig’, $vars);

Y, finalmente, invocar al servicio KnpSnnapy del siguiente modo:

$knpSnappy->getOutputFromHtml($html, [
  'images' => true,
  'enable-javascript' => true,
  'page-size' => 'A4',
  'viewport-size' => '1280x1024',
  'header-html' => $header,
  'footer-html' => $footer,
  'margin-left' => '10mm',
  'margin-right' => '10mm',
  'margin-top' => '30mm',
  'margin-bottom' => '25mm',
]),

De toda la serie de propiedades que he asignando en la llamada, las más importantes son las siguientes:

  • header-html, a la cual le estamos asignando el html del header ya rendereado
  • margin-top, el cual tiene que valer como poco la altura del header que hayamos preparado para que así no se solape (y preferiblemente en centímetros por lo que probablemente os toquen 5 minutos de prueba y error hasta cuadrar este valor).

Con esto los PDFs que se generen ya tendrán la cabecera (o footer o ambos) que hayamos diseñado.

Números de página

Otra de las cosas que habitualmente podemos necesitar es imprimir el número de la página en cada hoja generada. Si por ejemplo queremos imprimir el paginador en el footer, emplearemos el siguiente código Javascript que, grosso modo, lee los parámetro que la librería wkhtmltopdf pasa por URL y que contienen el número de página o el total de ellas para imprimirlos donde le digamos.

https://gist.github.com/ger86/3abe4410b3d57dc7bf7988ccb3993817

Ojo, para que ésto funcione es necesario que en las opciones de llamada a KnpSnappy activéis javascript:

$knpSnappy->getOutputFromHtml($html, [
...
‘enable-javascript’ => true,
...
]);

Nombre de los archivos

Otra de las cosas que podemos necesitar es generar los archivos con un nombre en concreto, algo que con KnpSnappy es bastante sencillo, basta con importar el archivo PDFResponse de la propia librería para que el archivo PDF que generemos lleve el nombre que necesitemos:

use Knp\Bundle\SnappyBundle\Snappy\Response\PdfResponse;
...
return new PdfResponse(
  $this->get(‘knp_snappy.pdf’)->getOutputFromHtml($html, $vars),
  $filename
);

Imágenes en nuestros PDF’s

En el caso de que necesitemos incorporar imágenes a nuestros PDF’s el proceso es bastante sencillo salvo por la peculiaridad de que el source de las mismas tienen que ir con url absoluta ya que si no se producirán errores 404 que impedirán la generación del PDF.

Por tanto, el código escribiremos para nuestra etiqueta img será similar al siguiente:

<img class=”logo” src=”{{ absolute_url(asset(‘build/images/logo.png’)) }}”>

Fuentes mediante font face

Este es uno de los casos más problemáticos con los que nos hemos encontrado. Básicamente se debe a que wkhtmltopdf no se lleva especialmente bien con la directiva font-face de css, lo cual provoca que en ocasiones los textos con fuentes declaradas de esta forma (especialmente si están basadas en caracteres unicode) no aparezcan en el PDF.

Sin embargo, os dejamos alguna serie de consejos que os ayudarán a trabajar con font-face.

  • Emplear nombres de archivo con longitud no superior a los 8 caracteres (leído en el propio repositorio de la librería por sorprendente que parezca).
  • Emplear fuentes .otf
  • Declarar la fuente sin olvidar el estilo de la misma y el peso:
// fonts.sass
@font-face
  font-family: ‘picon’
  src: url(‘../../fonts/pemsaicon/picon.otf’)
  font-style: normal
  font-weight: normal

Bootstrap 4

Por desgracia no es posibleusar Bootstrap 4 para maquetar nuestros PDF’s ya que éste usa flex, tecnología que no está soportada todavía por wkhtmltopdf, así que os tocará o bien usar una versión anterior o bien escribir vuestros propios css.

Forzar nueva página

Si por lo que sea necesitamos forzar saltos de página para separar determinados elementos del PDF, el siguiente snippet nos será de gran ayuda:

// structure.sass
.page
  page-break-after: always
  page-break-inside: avoid
&:last-child
  page-break-after: avoid
  page-break-inside: avoid

Bastará con emplear un div que lleve la clase .page para que se produzca un salto de página cuando termine el contenido que encierra.

Evitar que las cabeceras sticky de las tablas se solapen con las filas cuando se cambia de página

Finalmente, otro de los problemas con los que os podéis topar es a la hora de usar cabeceras sticky en tablas, ya que éstas se solapan con las filas cuando se cambia de página. Existen numerosas soluciones para este problema en el repositorio oficial pero la que a nosotros nos ha funcionado ha sido la siguiente:

// tables.sass
thead
  display: table-header-group
tfoot
  display: table-row-group
tr
  page-break-inside: avoid!important

Ejemplo

Para que os hagáis una idea de las cosas que se pueden conseguir con esta librería os dejo el enlace a uno de los últimos proyectos en los que he trabajado. Pulsando en el botón ficha técnica podréis descargar un PDF generado mediante el bundle KnpSnappy para que podáis comprobar de primera mano cómo quedan los PDF’s generados de esta forma:

Y con esto espero haberos ahorrado algo de tiempo buscando soluciones a las situaciones más habituales que os pueden surgir cuando usáis KnpSnappy. No obstante, si os habéis encontrado con algún otro problema dejadlo en los comentarios e intentaré responderlo.