Creando reportes con PDFMake

gray and black machine on white table

Seguimos en la creación de algunas soluciones, en esta oportunidad debemos generar un archivo PDF en el backend con bastante contenido estático y algún contenido dinámico. Esto último, extraído desde el resultado de una query en una base de datos. Hasta ahora tenía experiencia con EJS/HBS + HTML-PDF en Node… Todo bien , hasta que un mensaje de vulnerabilidad apareció en mi terminal y luego en Github… Mala cosa!

Luego de un viaje al Universo Alterno, me traje una recomendación: PDFMake, una librería simple, escrita en JavaScript y con un uso relativamente intuitivo… OKNO, intuitivo si conoces de JSON y otras yerbas como LaTeX. PDFMake te permite definir muchos aspectos de tu documento y acá hay una clara diferencia con otras librerías que sólo imprimen un renderizado de HTML, PDFMake genera un PDF desde su configuración más basica, es decir, tamaño de página, definición de bordes, orientación de la página, etc.

Elementos más triviales como Cabeceras y Pie de Página están disponibles y son 100% configurables… OK, luego de googlear un poco te enteras de algunos trucos para hacerlos 100% configurables, porque por defecto son horribles.

Primero, definiremos la(s) fuente(s) que utilizaremos para crear nuestro PDF en una variable para que PDFMake la(s) utilice.

const fonts = {
    Roboto: {
        normal: __dirname + '/fonts/Roboto-Regular.ttf',
        bold: __dirname + '/fonts/Roboto-Medium.ttf',
        italics: __dirname + '/fonts/Roboto-Italic.ttf',
        bolditalics: __dirname + '/fonts/Roboto-MediumItalic.ttf'
    },
}


Ahora veamos cómo se configura un documento, con tamaño de página «carta», con un margen establecido.

pageSize: 'LETTER',
pageOrientation: 'portrait',
pageMargins: [40, 80, 40, 60],

luego, veamos cómo establecer el header con un margen y una logo (imagen) a la izquierda.

header: {
  image: __dirname + '/images/logo.png',
  fit: [150, 75],
  margin: [40,40],
},

A continuación, veamos cómo crear el footer, con una paginación automática. Este es otro punto a favor de esta librería respecto a otras utilizadas. Nótese que el truco acá es crear una tabla de una fila y una columna, luego sobre la tabla aplicamos los margenes.

footer: function (currentPagepageCount) {
  return {
    table: {
      widths: ['*'],
      body: [
        [{ text: 'Página ' + currentPage + "   ", alignment: 'right' }]
      ]
    },
    layout: 'noBorders',
    margin: [39,10]
  };
},

Ahora, podemos definir un estilo para todo el documento, es decir, un estilo por defecto. Este estilo será aplicado a todo el documento, excepto al contenido que indiquemos que tendrá otro estilo particular.

defaultStyle: {
  font: 'Roboto',
  fontSize: 9,
  color: '#525252',
  alignment: 'justify',
  columnGap: 20,
},

Ahora definiremos los estilos para los titulos y subtitulos, nótese que cambio el tamaño de fuente y el color. Ustedes pueden personalizar aún más sus estilos según la documentación oficial.

styles: {
  header: {
    font: 'Roboto',
    fontSize: 16,
    bold: true,
    alignment: 'left',
    color: '#161616',
  },
    subheader: {
    font: 'Roboto',
    fontSize: 14,
    italic: true,
    alignment: 'left',
    color: '#0043ce',
  },
},

Sigamos avanzando, veamos cómo escribir los parrafos de texto. Vemos que el parrafo va entre comillas simples y cada parrafo está separado con una coma… Les dije que esto se parece mucho a un JSON?

content: [
         'First paragraph',
         'Another paragraph, this time a little bit longer to make sure, this line will be divided into at least two lines'
]

Ahora veremos cómo aplicar los estilos que definimos previamente. acá aparece la key llamada text, que identifica un bloque de texto y luego, separado por coma, una key llamada style que… adivinen… Siiii, es uno de los estilos que definimos previamente.

{
  text: 'This is a header, using header style',
  style: 'header'
},

¿Alguien dijo columnas? En el caso de las columnas, son arreglo de objetos text ¿simple, no? Bueno, también podemos aplicar estilos y tal.

{
  alignment: 'justify',
  columns: [
    {
      text: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Malit profecta versatur nomine ocurreret multavit, officiis viveremus aeternum superstitio suspicor alia nostram, quando nostros congressus susceperant concederetur leguntur iam, vigiliae democritea tantopere causae, atilii plerumque ipsas potitur pertineant multis rem quaeri pro, legendum didicisse credere ex maluisset per videtis. Cur discordans praetereat aliae ruinae dirigentur orestem eodem, praetermittenda divinum. Collegisti, deteriora malint loquuntur officii cotidie finitas referri doleamus ambigua acute. Adhaesiones ratione beate arbitraretur detractis perdiscere, constituant hostis polyaeno. Diu concederetur.'
    },
    {
      text: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Malit profecta versatur nomine ocurreret multavit, officiis viveremus aeternum superstitio suspicor alia nostram, quando nostros congressus susceperant concederetur leguntur iam, vigiliae democritea tantopere causae, atilii plerumque ipsas potitur pertineant multis rem quaeri pro, legendum didicisse credere ex maluisset per videtis. Cur discordans praetereat aliae ruinae dirigentur orestem eodem, praetermittenda divinum. Collegisti, deteriora malint loquuntur officii cotidie finitas referri doleamus ambigua acute. Adhaesiones ratione beate arbitraretur detractis perdiscere, constituant hostis polyaeno. Diu concederetur.'
    }
  ]
},

Listas desordenadas y ordenadas.

{text: 'Unordered list', style: 'subheader'},
{
  ul: [
    'item 1',
    'item 2', 
    'item 3'
  ]
},
{text: 'Ordered list', style: 'subheader'},
{
  ol: [
    'item 1',
    'item 2',
    'item 3'
  ]
},

Imágenes, ¿cómo lo hacemos?, acá definimos una imagen con su ancho y alto, además, usamos el comando para hacer salto de página, en este caso, luego de la imagen.

{
  image: __dirname + '/images/portada.png',
  width: 530,
  height: 630,
  pageBreak: 'after'
},

Finalmente, las tablas… En este ejemplo se aprecia claramente la forma en la cual se contruye una tabla en PDFMake.

{
  table: {
    body: [
      ['Fila cabecera - Columna 1', Fila Cabecera - Column 2', 'Fila Cabecera - Columna 1'],
      ['Fila 1 - Valor Columna 1', 'Fila 1 - Valor Columna 2', 'Fila 1 - Valor Columna 3']
      ['Fila 2 - Valor Columna 1', 'Fila 2 - Valor Columna 2', 'Fila 2 - Valor Columna 3']
     ]
   }
},

Esto es el inicio, ahora queda lo más entretenido. Hacer contenidos dinámicos mediante tablas y gráficos… Siguen los desafios… Creo que es tiempo de reescribir un taller que hice el 2009 de LaTex, el parecido no es tanto, pero la idea de documentación por bloque y marcado de etiquetas es similar. LaTeX es el soporte por excelencia de documentos científicos por su calidad, simpleza en manejo de fórmular matemáticas y sobre todo, porque el acabado del documento es sencillamente genial.

Namasté!