domingo, 4 de abril de 2010

Crear documentos PDF en Python, y 3


En este post vamos a ver el último capítulo de ReportLab, una herramienta de generación documentos en formato PDF para Python. Ni que decir tiene que hay muchas más características de ReportLab de las que yo nombro aquí, así que animo al lector a leer el manual de ReportLab, que si bien está en inglés, es totalmente comprensible con pocos conocimientos del idioma anglosajón.

Aquí vamos a ver como crear documentos completos, con imágenes y texto, de varias páginas.

PLATYPUS

En primer lugar vamos a ver lo que es PLATYPUS (Page Layout and Typography Using Scripts).

Con PLATYPUS se pueden diseñar páginas y tipografías utilizando scripts. Con esta herramienta se busca separar las decisiones de diseño del contenido del documento, tanto como sea posible. Es decir, es una especie de formateador de texto, en donde se formatea el diseño del texto, de manera que si queremos cambiar la apariencia del texto solo cambiaremos el diseño, sin tener que cambiar nada en el texto.

Los párrafos de texto son construidos utilizando estilos de párrafos y las páginas se construyen usando plantillas (templates) de páginas, con la intención de que cientos de documentos de miles de páginas puedan ser reformateadas de diferentes estilos con unas pocas líneas en un único fichero el cual contiene los estilos de párrafos y las especificaciones del diseño de la página. Un símil podría ser una página HTML y el diseño que se le da mediante una hola CSS.

PLATYPUS está pensado para hacer un diseño por capas, de arriba hacia abajo. Dichas capas podemos verlas a continuación:

  1. DocTemplates - La plantilla del documento. El contenedor último de un documento.
  2. PageTemplates - Las especificaciones para diseñar páginas de varias clases.
  3. Frames - Las especificaciones de regiones en la páginas que pueden contener texto ó gráficos.
  4. Flowables - Elementos de texto ó gráficos que están incrustados en el documento (párrafos, tablas, imágenes, pero no pies de página, por ejemplo).
  5. pdfgen.Canvas - El nivel más bajo que revice las órdenes de pintar en el documento desde las otras capas. Este nivel es el que hemos estado utilizando en los post anteriores.
El DocTemplates contiene uno ó más PageTemplates y cada uno de ellos a su vez puede contener uno ó más Frames. Los Flowables son elementos que pueden estar incluidos en un Frame (por ejemplo, un párrafo ó una tabla).

En el siguiente ejemplo podemos observar un ejemplo de lo expuesto.

Aquí hay un DocTemplate compuesto por 3 PageTemplates. El primer PageTemplate especifica la portada del documento. Vemos que incluye un Frame (amarillo), que a su vez incluye 2 Flowable (en cyan. El flowable1 podría ser una imagen y flowable2 un texto indicando el nombre del documento y autor del mismo). El segundo PageTemplate especifica el diseño de los capítulos del documento. Aquí hay un único Frame que contiene un Flowable. Dicho Flowable sería texto. Y por último el tercer PageTemplate es el fin del documento, el cual lo conforma 4 Frames, y en cada uno de ellos un Flowable.

Darse cuenta que cada uno de los PageTemplates especifican el formato de un número de
páginas.

¿Cómo se utiliza PLATYPUS?

Se crea un documento a partir de la clase DocTemplate y se le pasa una lista de Flowables mediante el método build. Para ello se construye un Platypus story, que consiste en una secuencia de elementos básicos Flowables, que son formateados.

Veámoslo con el siguiente ejemplo:

Ejemplo de uso de PLATYPUS. El Viaje del Navegante. 04/04/2010.

import os

Obtenemos de platypus las clases Paragraph, para escribir párrafos Image, para insertar imágenes y SimpleDocTemplate para definir el DocTemplate. Además importamos Spacer, para incluir espacios .

from reportlab.platypus import Paragraph
from reportlab.platypus import Image
from reportlab.platypus import SimpleDocTemplate
from reportlab.platypus import Spacer

Importamos clase de hoja de estilo de ejemplo.

from reportlab.lib.styles import getSampleStyleSheet

Se importa el tamaño de la hoja.

from reportlab.lib.pagesizes import A4

Y los colores.

from reportlab.lib import colors

Creamos un PageTemplate de ejemplo.

estiloHoja = getSampleStyleSheet()

Inicializamos la lista Platypus Story.

story = []

Definimos cómo queremos que sea el estilo de la PageTemplate.

cabecera = estiloHoja['Heading4']

No se hará un salto de página después de escribir la cabecera (valor 1 en caso contrario).

cabecera.pageBreakBefore=0

Se quiere que se empiece en la primera página a escribir. Si es distinto de 0 deja la primera hoja en blanco.

cabecera.keepWithNext=0

Color de la cabecera.

cabecera.backColor=colors.cyan

Incluimos un Flowable, que en este caso es un párrafo.

parrafo = Paragraph("CABECERA DEL DOCUMENTO ",cabecera)

Lo incluimos en el Platypus story.

story.append(parrafo)

Definimos un párrafo. Vamos a crear un texto largo para demostrar cómo se genera más de una hoja.

cadena = " El Viaje del Navegante " * 600

Damos un estilo BodyText al segundo párrafo, que será el texto a escribir.

estilo = estiloHoja['BodyText']
parrafo2 = Paragraph(cadena, estilo)

Y lo incluimos en el story.

story.append(parrafo2)

Dejamos espacio.

story.append(Spacer(0,20))

Ahora incluimos una imagen.

fichero_imagen = "elviajedelnavegante_png.png"
imagen_logo = Image(os.path.realpath(fichero_imagen),width=400,height=100)
story.append(imagen_logo)

Creamos un DocTemplate en una hoja DIN A4, en la que se muestra el texto enmarcado (showBoundary=1) por un recuadro.

doc=SimpleDocTemplate("ejemplo1.pdf",pagesize=A4,showBoundary=1)

Construimos el Platypus story.

doc.build(story)

El resultado es el siguiente:



Tablas en Platypus

Una tabla se define como una lista de listas, donde cada componente de la lista es una fila de la tabla. Vamos a agregar ahora una tabla al ejemplo anterior, del siguiente modo:

Lo primero es cargar el módulo apropiado:

from reportlab.platypus import Table

Y a continuación tendríamos:

Dejamos espacio.

story.append(Spacer(0,20))

Definimos las filas de una tabla.

fila1 = ['','Lunes','Martes','Miércoles','Jueves','Viernes','Sábado','Domingo']
fila2 = ['Mañana','Estudiar','Gimnasio','-','-','-','Estudiar','Ir a la iglesia']
fila3 = ['Tarde','Trabajar','Trabajar','Trabajar','Trabajar','Trabajar','-','-']
fila4 = ['Noche','Trabajar','Trabajar','Trabajar','Trabajar','-','-','-']

Definimos la tabla.

tabla = Table([fila1,fila2,fila3,fila4])

Podemos dar estilo a los elementos de una tabla. En esta ocasión vamos a poner de color azul Mañana,Tarde y Noche y en color rojo los días de la semana.

tabla.setStyle([('TEXTCOLOR',(1,-4),(7,-4),colors.red),('TEXTCOLOR',(0,0),(0,3),colors.blue)])

Y la asignamos al platypus story.

story.append(tabla)

Darse cuenta de las coordenadas en SetStyle a la hora de cambiar el color. El resultado, si se incluye este código en el ejemplo anterior, justo después de la imagen, es el siguiente:

Sucesivos usos del método setStyle aplica estilos de forma aditiva. Así por ejemplo tendremos que:

Damos color de fondo a las celdas.

tabla.setStyle([('BACKGROUND',(1,1),(-1,-1),colors.cyan)])

Creamos una caja alrededor de las celdas.

tabla.setStyle([('BOX',(1,1),(-1,-1),0.25,colors.black)])

Y ponemos una malla (rejilla) a la tabla.

tabla.setStyle([('INNERGRID',(1,1),(-1,-1),0.25,colors.black)])

Dando como resultado:

Se pueden hacer muchísimas cosas más con tablas. Animo al lector a investigar más sobre el tema en el manual de ReportLab.

Para terminar solo comentar que si se quiere incluir un nuevo elemento en el documento solo es necesario ir añadiendo a la lista de platypus story los flowables que se quieran.

Utilizando Flowables

Un Flowable es una clase abstracta para cosas que pueden ser dibujadas. Una instancia tiene que conocer previamente su tamaño, con el fin de poder ser dibujado en su propio sistema de coordenadas. Para instanciar se utiliza objeto = Flowable(). Darse cuenta que Flowable es una clase abstracta y se usa normalmente como clase base.

Los métodos que nos interesan para trabajar con esta clase son los siguientes:

wrap(): Este método es llamado por el Frame contenedor antes de que se le asigne al objeto su
tamaño, sea pintado o cualquier otra cosa. Calcula el espacio que requiere el Flowable, y devuelve
el tamaño actualmente usado.

drawOn(canvas, x, y): Este método es el que envía el flowable a un canvas en particular. Esto es, maneja el traslado a las coordenadas del canvas (x,y) , de manera que dicho canvas pueda dibujar el flowable en un frame de coordenadas absolutas.

La manera más sencilla de cómo se usan los Flowables es verlo a través del uso de una instancia
de la clase Paragraph, que será dibujada en un canvas. La clase Paragraph es muy importante,
como veremos más adelante.


Ejemplo de uso de Flowables.
El Viaje del Navegante.
04/04/2010.

import os

Importamos lo que nesitamos de ReportLab.

from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import Paragraph
from reportlab.pdfgen.canvas import Canvas

Creamos estilo de hoja.

estiloHoja = getSampleStyleSheet()
estilo = estiloHoja['BodyText']

Párrafo.

parrafo = Paragraph('El viaje del navegante',estilo)

Creamos objeto canvas.

fichero = 'ejemplo3.pdf'
objeto_canvas = Canvas(os.path.realpath(fichero))

Valores de ancho y alto.

ancho, alto = 400, 400

Calculamos el espacio que se requiere realmente, para ajustar. wrap
se utiliza aquí para calcular el tamaño real, y es llamado por el Frame

que encierra al Flowable.

ancho_aux, alto_aux = parrafo.wrap(ancho, alto)

Reducimos la altura, si es necesario (si es así mandamos el flowable al canvas).

if ancho_aux <= ancho and alto_aux <= alto:
# Reducimos la altura.
ancho = ancho - ancho_aux
# Mandamos el Flowable al canvas.
parrafo.drawOn(objeto_canvas,0,alto_aux)
# Salvamos el canvas.
objeto_canvas.save()
else:
raise ValueError, "No hay suficiente espacio"

Utilizando Frames

Como antes he comentado se utilizan los Frames para contener Flowables, esto es, párrafos e imágenes. Los Frames se encuentran incluidos dentro de los PageTemplates. Así un Frame es un espacio de dibujo. La definición de un Frame se realiza como:

Frame(x1,y1,witdh,height,leftPadding=6,bottomPadding=6,rightPadding=6, topPadding=6, id=None,showBoundary=0)

Esto crea una instancia de un Frame en las coordenadas (x1,y1) a partir del eje que se encuentra en la esquina inferior izquierda, con las dimensiones width x height. El showBoundary es para mostrar el rectángulo del área del Frame (si es distinto de cero lo muestra).

Los métodos para utilizar un Frame son los siguientes:

addFromList(ListaDraw,canvas): Añade la lista de Flowables 'ListaDraw' al Frame. Si la lista está vacía, se quejará lanzando una excepción.

split(flowable,canv): Devuelve una lista de flowables.

drawBoundary(canvas): Dibuja el marco del frame como un rectángulo.

Veámoslo con el siguiente ejemplo:

Ejemplo de utilización de Frames de ReportLab.
04/04/2010


import os

Cargamos los módulos que necesitamos.

from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.platypus import Paragraph, Frame
from reportlab.platypus import Image
from reportlab.platypus import Spacer

Creamos objeto Canvas.

fichero_pdf = 'ejemplo2.pdf'
objeto_canvas = Canvas(os.path.realpath(fichero_pdf))

Estilo de la hoja.

estiloHoja = getSampleStyleSheet()

Demás estilos.

estiloN = estiloHoja['Normal']
estiloH = estiloHoja['BodyText']

Inicializamos platypus story.

story = []

Añadimos algunos flowables.

fichero_imagen = "elviajedelnavegante_png.png"
imagen_logo = Image(os.path.realpath(fichero_imagen),width=100,height=50)
story.append(imagen_logo)

Dejamos espacio.

story.append(Spacer(0,20))

Añadimos un párrafo.

story.append(Paragraph("El viaje del navegante. Blog de Python",estiloN))

Definimos un frame.

frame = Frame(inch,inch,2*inch,2*inch,showBoundary=1)
frame.addFromList(story,objeto_canvas)

Inicializamos platypus story.

story2 = []

Añadimos algunos flowables.

story2.append(Paragraph("Segundo Frame. PYTHON!!!!",estiloH))

Dejamos espacio.

story2.append(Spacer(0,20))

Una imagen.

fichero_imagen = "escudo_umu.png"
imagen_logo = Image(os.path.realpath(fichero_imagen),width=100,height=100)
story2.append(imagen_logo)

Definimos otro frame.

frame2 = Frame(250,inch,250,250,2*inch,showBoundary=1)
frame2.addFromList(story2,objeto_canvas)

Salvamos el PDF.

objeto_canvas.save()

Y el resultado es:


SimpleDocTemplate
es una clase derivada de BaseDocTemplate, la cual tiene su propio PageTemplate y una configuración de Frame.

Como se ha podido observar en realidad lo que se está haciendo es encapsular texto e imágenes en un marco de dibujo. Y por ende, si el Frame está dentro de una PageTemplate, dicho PageTemplate es un conjunto de Frames, y así sucesivamente.

Documentos y Plantillas

La clase BaseDocTemplate implementa la maquinaria básica para el formateado de un documento. Una instancia de esta clase contiene una lista de uno ó más PageTemplates, que se utilizan para describir el diseño de información de una página. El método build se usa para procesar una lista de Flowables con el propósito de producir un documento PDF. La definición de esta clase es la siguiente:

BaseDocTemplate(self, filename, pagesize=defaultPageSize, pageTemplates=[], showBoundary=0, leftMargin=inch, rightMargin=inch, topMargin=inch, bottomMargin=inch, allowSplitting=1, title=None, author=None, _pageBreakQuick=1, encrypt=None)

Esta clase crea una plantilla de documento adecuada para crear un documento básico. Darse cuenta que esta clase no lleva PageTemplates por defecto, esto es, habría que crearlas y a continuación pasarlas al DocTemplate. Cabe destacar que filename es el nombre del fichero PDF a crear.

Los métodos más usuales de BaseDocTemplate son:

addPageTemplates(self, pageTemplates): Añade una lista de PageTemplates a un documento existente.

build(self, flowables, filename=None, canvasmaker=canvas.Canvas): Este es el método principal que nos interesa, ya que toma el story de la lista de flowables y va procesando uno a uno, dándole formato.


SimpleDocTemplate es una clase derivada de BaseDocTemplate, la cual tiene su propio PageTemplate y una configuración de Frame.

A continuación un ejemplo de Doctemplate:

Ejemplo de uso de DocTemplate.
El Viaje del Navegante.
04/04/2010.

import os

Importamos lo que nesitamos de ReportLab.

from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.pagesizes import A4
from reportlab.platypus import Paragraph
from reportlab.platypus import SimpleDocTemplate

Creamos estilo de hoja.

estiloHoja = getSampleStyleSheet()
estilo1 = estiloHoja['BodyText']
estilo2 = estiloHoja['Normal']

Inicializamos story.

story = []

Añadimos algunos flowables.

parrafo1 = Paragraph('El viaje del navegante',estilo1)
parrafo2 = Paragraph('España, Murcia, Lorca',estilo2)

Añadimos los flowables a la lista story.

story.append(parrafo1)
story.append(parrafo2)

Creamos documento.

documento = SimpleDocTemplate('ejemplo4.pdf',pagesize = A4)

Y construimos el documento.

documento.build(story)

Y obtenemos:


La clase PageTemplate es una clase contenedora. Cada instancia contiene una lista de Frames y métodos los cuales deberían llamarse al principio y al final de cada página. Su definición es:

PageTemplate(id=None,frames=[],onPage=_doNothing,onPageEnd=_doNothing)

donde frames es la lista de frames.

Paragraphs

Y para terminar, vamos a ver la clase reportlab.platypus.Paragraph. Esta clase es una de las más usadas en los Flowables de Platypus, ya que es la que se utiliza para incluir texto, pudiéndole dar formato a la fuente de la letra y cambio de color utilizando marcas XML (animo al lector a ingadar en este tema). Además el texto se puede justificar, a la derecha, izquierda o centrado.

Para crear una instancia de Paragraph utilizamos:

Paragraph(text, style, bulletText=None)

donde evidentemente text es el texto a escribir. Algo importante y a destacar es que permite el uso de texto Python entre las triples comillas simples (''' y '''). bulletText provee al texto un punto y aparte por defecto para el párrafo. El tipo de letra y otras propiedades del texto se configuran en el argumento style, que es una instancia de la clase ParagraphStyle, y que pueden obtenerse de:

from reportlab.lib.styles import ParagraphStyle

La definición de la clase ParagraphStyle es la siguiente:

class ParagraphStyle(PropertySet):
defaults = {
'fontName':'Times-Roman',
'fontSize':10,
'leading':12,
'leftIndent':0,
'rightIndent':0,
'firstLineIndent':0,
'alignment':TA_LEFT,
'spaceBefore':0,
'spaceAfter':0,
'bulletFontName':'Times-Roman',
'bulletFontSize':10,
'bulletIndent':0,
'textColor': black,
'backColor':None,
'wordWrap':None,
'borderWidth': 0,
'borderPadding': 0,
'borderColor': None,
'borderRadius': None,
'allowWidows': 1,
'allowOrphans': 0,
}

Los estilos se organizan en un objeto diccionario de estilos. La manera de acceder es la que hemos estado viendo en este artículo, por ejemplo, estiloHoja['Normal']. Un ejemplo sería este:

from reportlab.lib.styles import getSampleStyleSheet
estiloHoja=getSampleStyleSheet()
estiloBodyText = estiloHoja['BodyText']

Con estas últimas pinceladas doy por terminado el análisis de ReportLab. Ni que decir tiene que este módulo tiene muchas más características de las que no he hablado. Mi intención es que el lector se anime y comience el trabajo de crear documentos PDF con esta herramienta.

Como se ha podido observar no es muy difícil crear documentos PDF de calidad profesional. Para más información, consultar en manual de ReportLab (en inglés).

Saludos.

8 comentarios:

  1. Como puedo crear el pie de pagina por reportlab.
    Hay alguna funcion??

    ResponderEliminar
  2. Como puedo crear el pie de pagina por reportlab.
    Hay alguna funcion??
    Quiero dejarlo fijo para todas las paginas

    ResponderEliminar
  3. Hos dejo mi correo si sabeis algo me contestais (juancka_1189@hotmail.es)
    Gracias¡¡¡¡

    ResponderEliminar
  4. Disculpen mi ignorancia, pero como puedo vizualizarlo desde mi programa, o mejor aun ponerlo dentro de un formulario, uso glade por sierto como ago para mostrarlo

    ResponderEliminar
  5. Muy bueno, me ha ayudado mucho

    gracias

    ResponderEliminar
  6. Muy buen tuto, me gustaria saber como se maneja la paginación con reportlab?

    ResponderEliminar
  7. Les agradezco mucho si me colaboran con lo de la paginación, mi correo es direyes71@hotmail.com

    ResponderEliminar
  8. como dar un logo y un pie de pagina fijo a todas las hojas de un reporte

    ResponderEliminar