Crear documentos PDF en Python con ReportLab

Crear documentos PDF en Python con ReportLab



ReportLab es un toolkit de código abierto para crear documentos PDF desde Python. Se trata de una librería muy extensa y con muchas funcionalidades, desde pequeños textos y figuras geométricas a grandes gráficos e ilustraciones, todo ello puede ser incluido dentro de un PDF. En este artículo estaremos sopesando sus características generales y sus principales funciones para crear este tipo de documentos.

La librería se instala sencillamente vía pip:

pip install reportlab

El código de fuente está alojado en BitBucket.

Primeros pasos

ReportLab incluye una API de bajo nivel para generar documentos PDF directamente desde Python, y un lenguaje de plantillas de más alto nivel ─similar a HTML y a los sistemas de plantilla que se emplean en el desarrollo web─ llamado RML. Generalmente la segunda opción suele ser más conveniente para aquellos que deban hacer un uso exhaustivo de las capacidades de la librería al momento de generar documentos. Para el resto de los casos, será suficiente con la API de bajo nivel que describiremos en este artículo. Como sea, puedes encontrar la documentación oficial del paquete en su totalidad en este enlace.

El código más básico que podremos encontrar usando ReportLab es aquel que genera un documento PDF vacío, que es el siguiente.

from reportlab.pdfgen import canvas

c = canvas.Canvas("hola-mundo.pdf")
c.save()

Lo primero que hacemos es importar el módulo reportlab.pdfgen.canvas, luego creamos una instancia de la clase canvas.Canvas pasándole como argumento el nombre o la ruta del archivo que queremos generar, y por último invocamos el método Canvas.save() que guarda efectivamente los cambios en el documento.

Si bien nuestro objeto c representa al archivo íntegro sobre el que estamos trabajando, un canvas debe ser pensado simplemente como una hoja en blanco en la que debemos escribir, dibujar o lo que fuere. Estas operaciones de escritura o dibujo ocurrirán siempre entre la creación del documento (línea 3) y el método que guarda los cambios (línea 4).

Empecemos, entonces, por escribir nuestro primer texto en el documento
(recuerda que esta línea se ubica entre las dos anteriores).

c.drawString(50, 50, "¡Hola, mundo!")

Ahora bien, al abrir el archivo hola-mundo.pdf encontrarás nuestro pequeño mensaje en el extremo inferior izquierdo de la hoja.

Como tal vez lo hayas deducido, los primeros dos argumentos pasados a drawString() indican la posición (x, y) en la que aparecerá el texto. A diferencia de las librerías para el desarrollo de aplicaciones de escritorio más populares, en ReportLab el origen de las coordenadas (esto es, la posición (0, 0)) se encuentra en el extremo inferior izquierdo; de modo que la posición en Y aumenta a medida que sube en la pantalla, y la posición en X a medida que se corre hacia la derecha. Esta inversión del eje Y puede resultar un tanto confusa al principio, pero no presenta dificultar adicional alguna, simplemente el recordar estas cuestiones al momento de posicionar los objetos.

Dicho esto, resulta fundamental conocer cuáles son las medidas de cada hoja al generar el documento. El alto y el ancho corresponden a las medidas del estándar A4, el cual es utilizado por defecto al crear un canvas. Las dimensiones de una hoja están expresadas en puntos (points), no en píxeles, equivaliendo un punto a 1/72 pulgadas. Una hoja A4 está constituida por 595.2 puntos de ancho (width) y 841.8 puntos de alto (height).

Al crear una instancia de canvas.Canvas podemos especificar una dimensión alternativa para cada una de las hojas vía el parámetro pagesize, pasando una tupla cuyo primer elemento representa el ancho en puntos y el segundo, el alto. Dijimos que las dimensiones por defecto son las correspondientes al estándar A4; el módulo reportlab.lib.pagesizes provee las dimensiones de otros estándares, como letter, que es el más utilizado en Estados Unidos.

>>> from reportlab.lib.pagesizes import A4, letter
>>> letter
(612.0, 792.0)
>>> A4
(595.275590551181, 841.8897637795275)

Así, para crear un documento con las dimensiones empleadas en Estados Unidos haríamos lo siguiente.

from reportlab.lib.pagesizes import letter

c = canvas.Canvas("hola-mundo.pdf", pagesize=letter)

Y para utilizar las dimensiones del estándar A4:

from reportlab.lib.pagesizes import A4

c = canvas.Canvas("hola-mundo.pdf", pagesize=A4)

Lo cual resulta en un documento igual al primero que creamos, por cuanto ─reitero─ pagesize es A4 por defecto.

Ahora que conocemos el alto y el ancho de nuestra hoja, podemos utilizarlos para calcular distintas posiciones dentro de ella. Por ejemplo, para escribir nuestro mensaje en el extremo superior izquierdo con márgenes de (aproximadamente) 50 puntos:

from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas

w, h = A4
c = canvas.Canvas("hola-mundo.pdf", pagesize=A4)
c.drawString(50, h - 50, "¡Hola, mundo!")
c.showPage()
c.save()

En este caso hemos agregado una llamada a c.showPage() antes de guardar el documento. Este método le indica a ReportLab que ya hemos terminado de trabajar en la hoja actual y queremos pasar a la siguiente. Aunque todavía no hemos trabajado con una segunda hoja (y no aparecerá en el documento en tanto no se haya dibujado nada) es una buena práctica recordar hacerlo antes de invocar c.save().

Volveremos sobre la escritura más adelante, primero veamos cómo dibujar algunas líneas y figuras geométricas básicas.

Líneas y figuras geométricas

ReportLab permite dibujar líneas, rectángulos, círculos y otras figuras de una forma sencilla. Por ejemplo, para dibujar una línea invocamos el método line() indicando la posición de los dos puntos del segmento: x1, y1, x2, y2.

# Dibujar una línea horizontal.
x = 50
y = h - 50
c.line(x, y, x + 200, y)

Para un rectángulo, rect(x, y, width, height).

# Rectángulo.
c.rect(50, h - 300, 300, 200)

roundRect() opera de forma similar, pero un quinto argumento indica el radio según el cual se curvan se curvan los extremos.

# Rectángulo con extremos curvos.
c.roundRect(50, h - 300, 300, 200, 10)

En el caso de los círculos se indica la posición del centro seguido del radio.

# Círculo: posición (x, y) del centro y el radio.
c.circle(100, h - 100, 50)

Por último, para los elipses los argumentos son similares a los de una líneas.

c.ellipse(50, h - 50, x + 150, y - 50)

En una conjunción de todas estas funciones podemos generar un documento PDF como el siguiente.

from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas

w, h = A4
c = canvas.Canvas("figuras.pdf", pagesize=A4)
c.drawString(30, h - 50, "Línea")
x = 120
y = h - 45
c.line(x, y, x + 100, y)
c.drawString(30, h - 100, "Rectángulo")
c.rect(x, h - 120, 100, 50)
c.drawString(30, h - 170, "Círculo")
c.circle(170, h - 165, 20)
c.drawString(30, h - 240, "Elipse")
c.ellipse(x, y - 170, x + 100, y - 220)
c.showPage()
c.save()

Figuras geométricas con ReportLab

Otros métodos para generar figuras incluyen bezier(), arc(), wedge() y grid(). De este último hablaremos al final del artículo.

Estilos

Hasta el momento, tanto el texto como las figuras que hemos dibujado se valieron de los estilos por defecto (básicamente, los colores blanco y negro). Habrás notado que las funciones que venimos empleando no soportan argumentos tales como foreground o background para indicar el color de cada dibujo en particular. En su lugar, los estilos se configuran directamente sobre el canvas (la hoja), y todas las operaciones sobre la hoja que sucedan a esta configuración usarán los estilos indicados. Cuando cambiamos la hoja (showPage()), los estilos se pierden y ─de ser necesario─ deben ser establecidos nuevamente.

Así, por ejemplo, el método setFillColoRGB() establece el color de relleno de cualquier objeto dibujado en la hoja, de modo que el siguiente código tiene como resultado el texto “¡Hola, mundo!” y un cuadrado ambos en color rojo.

# Valores RGB entre 0.0 y 1.0.
c.setFillColorRGB(1, 0, 0)
c.drawString(50, h - 50, "¡Hola, mundo!")
c.rect(50, h - 150, 50, 50, fill=True)

(Nótese que las funciones que dibujan figuras incluyen el argumento fillFalse por defecto─ para indicar si deben ser coloreadas).

Asimismo, el método setStrokeColorRGB() establece el color del borde de las figuras.

c.setStrokeColorRGB(0.7, 0, 0.7)

Y para alterar la fuente y el tamaño del texto dibujado vía drawString(), empleamos setFont().

c.setFont("Helvetica", 10)
c.drawString(50, h - 50, "¡Hola, mundo!")
c.setFont("Times-Roman", 20)
c.drawString(130, h - 50, "¡Hola, mundo!")

Textos

Si bien drawString() es suficiente para algunas palabras, resulta un tanto incómodo al momento de tener que dibujar textos medianos o grandes, por cuanto no es capaz de aceptar saltos de línea. Para tareas como ésta ReportLab incluye text objects, una forma más especializada de dibujar texto.

Como primera instancia debemos crear el objeto correspondiente, indicando en qué lugar queremos posicionar el texto.

text = c.beginText(50, h - 50)

Una vez hecho esto, procedemos a configurar a partir del objeto creado los distintos estilos. Por ejemplo, aquí también tenemos un método setFont(), pero que actúa sobre este objeto en particular y no sobre el resto de la hoja.

text.setFont("Times-Roman", 12)

Vía el método textLine() añadimos líneas de texto a nuestro objeto.

# Las dos frases aparecen en dos líneas diferentes.
text.textLine("¡Hola, mundo!")
text.textLine("¡Desde ReportLab y Python!")

O bien:

# El método textLines() soporta el carácter de salto de línea.
text.textLines("¡Hola, mundo!\n¡Desde ReportLab y Python!")

Una vez escrito el texto, lo dibujamos en la hoja.

c.drawText(text)

Otros métodos para darle formato al texto incluyen setCharSpace(), setWordSpace() y setLeading(), que toman como argumento el tamaño de la distancia (en puntos) entre, respectivamente, dos caracteres, dos palabras y dos líneas.

Imágenes

Para insertar imagenes en un documento PDF ReportLab hace uso de la librería Pillow, que se instala sencillamente vía pip install Pillow.

El método drawImage() toma como argumento la ruta de una imagen (soporta múltiples formatos tales como PNG, JPEG y GIF) y la posición (x, y) en la que se quiere insertar.

c.drawImage("logo.png", 50, h - 200)

Podemos achicar o agrandar la imagen indicando sus dimensiones vía los argumentos width y height.

c.drawImage("logo.png", 50, h - 200, width=50, height=50)

Grillas

Al momento de generar grillas ReportLab nos facilita el trabajo proveyendo el método grid() ─en lugar de tener que hacerlo manualmente vía line() o lines()─, que toma como primer argumento una lista de posiciones en X y, como segundo, una lista de posiciones en Y.

xlist = [10, 60, 110, 160]
ylist = [h - 10, h - 60, h - 110, h - 160]
c.grid(xlist, ylist)

El resultado es el siguiente:

Grilla en PDF

Como habrás notado, xlist indica las posiciones en el eje X del inicio de cada una de las líneas verticales, mientras que ylist indica el inicio ─sobre el eje Y─ de las horizontales. A partir de esa información la libería se encarga de constituir la grilla en su totalidad.

A modo de ilustración, considérese el siguiente código que genera, haciendo uso de este método, una grilla de alumnos con sus respectivas calificaciones.

import itertools
from random import randint
from statistics import mean

from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas


def grouper(iterable, n):
    args = [iter(iterable)] * n
    return itertools.zip_longest(*args)


def export_to_pdf(data):
    c = canvas.Canvas("grilla-alumnos.pdf", pagesize=A4)
    w, h = A4
    max_rows_per_page = 45
    # Margin.
    x_offset = 50
    y_offset = 50
    # Space between rows.
    padding = 15
    
    xlist = [x + x_offset for x in [0, 200, 250, 300, 350, 400, 480]]
    ylist = [h - y_offset - i*padding for i in range(max_rows_per_page + 1)]
    
    for rows in grouper(data, max_rows_per_page):
        rows = tuple(filter(bool, rows))
        c.grid(xlist, ylist[:len(rows) + 1])
        for y, row in zip(ylist[:-1], rows):
            for x, cell in zip(xlist, row):
                c.drawString(x + 2, y - padding + 3, str(cell))
        c.showPage()
    
    c.save()


data = [("NOMBRE", "NOTA 1", "NOTA 2", "NOTA 3", "PROM.", "ESTADO")]
for i in range(1, 101):
    exams = [randint(0, 10) for _ in range(3)]
    avg = round(mean(exams), 2)
    state = "Aprobado" if avg >= 4 else "Desaprobado"
    data.append((f"Alumno {i}", *exams, avg, state))
export_to_pdf(data)

Conclusión

Hemos examinado las principales características de ReportLab, aunque no se trate sino de una pequeña selección de su vasta colección de funcionalidades tal como lo hemos comentado al inicio del artículo. Aquellos que requieran de un uso más exhausto de la librería, ya habrán conocido las bases y los dirijo nuevamente a la documentación oficial para inmiscuirse en las herramientas más complejas.



Deja un comentario