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 este repositorio.

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)

Cuando necesitamos hacer cálculos a partir de las dimensiones de una imagen, es conveniente primero abrirla vía ImageReader(). Por ejemplo, si queremos ubicar una imagen en el extremo superior izquierdo de la hoja, será necesario conocer a priori el alto de la imagen para calcular la posición en el eje Y:

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

w, h = A4
c = canvas.Canvas("imagen.pdf", pagesize=A4)
# Ubicar el logo en el extremo superior izquierdo.
img = ImageReader("logo.png")
# Obtener el ancho y alto de la imagen.
img_w, img_h = img.getSize()
# h - img_h es el alto de la hoja menos el alto
# de la imagen.
c.drawImage(img, 0, h - img_h)
c.save()

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.

Curso online 👨‍💻

¡Ya lanzamos el curso oficial de Recursos Python en Udemy! Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter en 2024.

Consultoría 💡

Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.

25 comentarios.

  1. Buenos días! Existe una función en donde se pueda pegar una imagen en el pdf? Por ejemplo si yo tengo un logo en png o svg, podría pegarlo con alguna función?
    O quizas se podria unir con otro pdf que tenga esas imagenes?

    Desde ya muchas gracias!

  2. Hola, estoy intentando poner una firma entre el grib pero el fondo me tapa las lineas, elimine el fondo y me sale igual pero en negro, quisiera saber si hay una forma de poner la imagen detras del grib

    • Recursos Python says:

      Hola, María. Usando c.showPage() (siendo c un Canvas) generás una nueva hoja, como se ilustra en la función export_to_pdf() del último código.

      Saludos

      • Buenas tardes! Gracias por responder! Si me puede orientar se lo super agradezco. Quiero almacenar los caracteres que se pulsan y cuando se apriete un botón, se genere un pdf con los mismos. Probé de almacenarlo en un string el tema es que cuelga el programa cuando supera determinada cantidad de caracteres. Tiene que tener la capacidad de almacenar 300 hojas mínimo. ¿Se pueden hacer guardados parciales en el documento? Cada vez que llamo a text.textLine() se agrega un salto de línea ¿Hay posibilidad no suceda eso? Es decir ¿se escriba todo seguido? Desde ya gracias nuevamente, saludos.

        • Recursos Python says:

          ¡Hola! 300 hojas de texto plano no es demasiado para cargar en memoria (es menos de 1 MB). Capaz haya un problema de rendimiento en el código en otro lado. Como sea, te conviene hacer guardados parciales en un documento de texto (véase Entrada y salida de archivos), en lugar de hacerlo directamente en el PDF. Luego, cuando el usuario presiona el botón, leés el archivo de texto y generás el PDF.

          Si necesitás más ayuda, te invito a que pases por el foro.

          Saludos

  3. y de que manera abro ese pdf con un boton que no sera submit y pasare un parametro que sera con el que jalare datos y llenare en el pdf?

    • Recursos Python says:

      Hola, eso es independiente de ReportLab, así que depende del tipo de aplicación que estés haciendo. Te invito a pasar por el foro y verlo con mayor detalle.

      Saludos

  4. Sergio Rodriguez says:

    Hola, tengo un proyecto para desplegarlo en Heroku, mi problema es que al ejecutarlo de forma local, puedo crear el pdf sin problemas, sin embargo al subirlo en Heroku me da error, yo asumo que es problema de ruta, pero como puedo hacer que al crear el c = canvas.Canvas(«hola-mundo.pdf», pagesize=A4) se descargue con la información desde el navegador o que pregunte una ruta donde guardarlo
    Gracias

    • Recursos Python says:

      Hola Sergio. Eso depende del web framework que estés usando para desarrollar tu sitio. Si querés pasate por el foro y lo vemos mejor.

      Saludos

  5. gabriel araya garcia says:

    Si alguien consulta esta pagina es porque necesita conocimiento y esta empezando con esto de los reportes, en consecuencia, probará que dichas rutinas de código funcionen, pero se llevará la desagradable sorpresa de que al subirlo a producción esto no funcionará, ni siquiera el ejemplo básico que mencionas. Es por eso que te hacia las consultas anteriores, y pensé que con tu posible respuesta el tema quedaba concluido. Y por lo tanto te reitero mi pregunta,.. cual seria el código adicional para ese ejemplo básico que pusiste pueda generar un PDF estando en el hosting, y como resultado se pueda alojar en una carpeta local. Honestamente yo no lo he logrado.

    • Recursos Python says:

      Hola Gabriel. Yo te reitero que los artículos de este sitio suponen que el lector tiene un conocimiento básico de Python (pues es justamente un sitio de recursos de este lenguaje). Las preguntas que estás haciendo denotan que carecés del mismo, de ahí que te haya recomendado que empieces por un tutorial. Los códigos de este artículo son independientes del entorno donde se ejecuten, siempre y cuando estén instalados Python y el módulo en cuestión, como se explica al principio.

      Saludos

  6. gabriel araya garcia says:

    No veo en que archivo se deben digitar estas lineas de código. Y si tengo varios reportes que listan información de diferentes tablas de datos como los llamo para que se ejecuten?,.. y donde los pongo?
    ¿Puedo definir una carpeta donde se guardaran dichos archivos cuando c.save() se ejecute?
    ¿Como puedo hacer para que se habra de inmediato el reporte sin necesidad de ir a la carpeta y darle doble clic?
    Creo que falto esa parte para que quede totalmente clarito.

  7. Roberto Honori says:

    Saludos, estamos colaborando con algunas organizaciones sociales en la implementacion de Software Libre, basados en el kernel Linux (Canaima), Reportlab, Geani editor y tenemos algunos inconvenientes, que agradeceriamos nos coloboraran. Muchisimas Gracias.

    1.- En Reportlab: Como enviar un reporte directamente a la pantalla.
    Actualmente, se guarda y despues hay que buscar enviarlo a la impresion.

    • Recursos Python says:

      Hola Roberto. Seguramente esa funcionalidad exceda a ReportLab. Mi recomendación es que, una vez guardado el archivo, simplemente ejecutes el comando requerido en Linux para abrirlo con un visor de PDF o bien enviarlo a la impresora vía el módulo subprocess.

      Saludos

Deja una respuesta