Generando PDF’s con KnpSnappy y Symfony
Cómo generar PDF’s de forma sencilla con Symfony
Hoy me gustaría hablaros de KnpSnappy, un bundle que os permitirá generar PDF’s desde Symfony sin apenas quebraderos de cabeza, por lo que, en mi opinión, es una de las mejores soluciones si tenéis que generar este tipo de documentos.
Emplearé para ello el bundle KnpSnappy 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.ymlknp_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 pdf.html.twig
tendrá el siguiente aspecto:

Aunque podremos trabajar normalmente heredando plantillas e incluyéndolas hasta lograr el aspecto que queramos.
KnpSnappy y Webpack
Como habréis visto en la plantilla anterior estoy empleando Webpack Encore para gestionar los archivos CSS y Javascript del proyecto e integrarlos en las plantillas Twig.
Sin embargo, cuando queramos añadir este tipo de archivos en las plantillas que emplearemos para generar los PDF’s, no podremos recurrir a las funciones encore_entry_link_tags
y encore_entry_script_tags
, ya que estas lo que hacen es imprimir la ruta relativa del archivo generado. Sin embargo la librería wkhtmltopdf necesita que estas rutas sean absolutas, por lo que tendremos que hacerlo manualmente iterando sobre la función encore_entry_css_files
:
{% for file in encore_entry_css_files('pdf') %}
<link href="{{ absolute_url(asset(file)) }}" rel="stylesheet" />
{% endfor %}
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
En la página de wkhtmltopdf disponemos de una sección “Downloads” para descargar la versión que necesitemos de esta librería:
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).
Hecho esto, nuestro controlador quedaría del siguiente modo:

En la línea 21 y en la línea 23 estoy rendereando el “header” y el “footer” de la página web como si se tratase de cualquier otra plantilla.
❗️ Importante. Puesto que estas plantillas extienden del archivo base base_pdf.html.twig
es necesario “reiniciar” el servicio de Webpack Encore que busca los “entrypoints” definidos en la plantilla ( EntrypointLookup
) y sirve los archivos asociados a las plantillas twig. Esto se debe a que tras usar cualquiera de las funciones encore_entry_link_tags
, encore_entry_script_tags
o encore_entry_css_files
su siguiente invocación no devolverá nada si no la reiniciamos antes. Por eso veréis que tanto en la línea 22 como en la 24 reinicio este servicio tras la invocación al método renderView
.
Por otra parte veréis que ahora la llamada getOutputFromHtml
recibe un array de opciones, de las cuales las más importantes son:
header-html
, a la cual le estamos asignando el html del header ya rendereado.footer-html
, a la cual le estamos asignando el html del footer 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).margin-bottom
, el cual tiene que valer como poco la altura del footer que hayamos preparado para que así no se solape.
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 posible usar 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.sassthead display: table-header-grouptfoot display: table-row-grouptr 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.
¿Quieres recibir más artículos como este?
Suscríbete a nuestra newsletter: