sábado, 2 de mayo de 2009

Renderizando PDFs en Python con Poppler

Buenas... hoy voy a mostrar una forma de renderizar archivos PDF dentro de nuestras aplicaciones y desde Python, para casi cualquier plataforma con la estemos trabajando.

Introducción
Si bien cuando se arma una aplicación de escritorio se suele "llamar" a un software externo (Acrobat Reader, Fox-It, Evince, etc.) para que muestre el contenido de un archivo PDF, generalmente queda "feo"; nuestro programa pierde la interacción y el control de lo que el usuario hace con el mismo, implica superponer un programa arriba del otro, etc. Esto suele producir una experiencia dificultosa para el usuario de nuestro querido programa, y como corresponde, los programadores nos ponemos tristes. :-(

Como una alternativa tenemos, en plataformas Windows, y estando el Acrobat Reader instalado previamente, la posibilidad de embeber en nuestro programa el mismo como un control ActiveX (hay otras alternativas) pero en el resto de las plataformas este problema no tenía solución (que yo conociera, al menos), ni siquiera algo más "liviano". El resultado de embeber todo el Acrobat Reader es más o menos lo que hacen los navegadores web cuando tienen el plugin de Adobe instalado y hacemos click en un link a un archivo PDF de la web:

(Después de 1 minuto, y si el usuario no cerró la ventana, aparece esto)

Hace unos días, en la lista de usuarios de las bibliotecas wxPython preguntaron si se podía renderizar un PDF dentro de una ventana del mismo programa, pero sobre Linux, y buscando la solución me encontré con un binding Python para Poppler.

Solución: Poppler-Python sobre Cairo
Veamos, Poppler es una biblioteca de código abierto desarrollada en C con el objetivo de renderizar (es decir, interpretar y dibujar) archivos PDF, siendo la evolución del viejo Xpdf. Es una pieza de software imprescindible en el escritorio de cualquier Sistema Operativo Libre, ya que es un trabajo bárbaro el que hace, y además es la piedra fundamental de programas como Evince y Okular (más allá de que el PDF es un formato de archivo universal). Y lo mejor es que dibuja sobre cualquier contexto de dibujo de Cairo, así que donde haya Cairo y compilador de C, puede estar Poppler*.

Hasta acá, bien, Poppler se puede utilizar desde C (como lo hace Evince, el visor de PDFs de Gnome). Pero 'ete aquí que me encontré con estos bindings de Poppler para Python, y si bien el ejemplo que éstos incluyen utiliza PyGTK, en realidad, sólo necesita de un Canvas Cairo (GTK dibuja mucho sobre Cairo y provee mecanismos para que el desarrollador lo utilice también); es el mismo Cairo que incluye wxPython a partir de la versión 2.8.9.0 (aunque se puede utilizar con ctypes desde versiones anteriores a la 2.8.9).

Un poco de Código
En fin, voy a tratar de explicarlo mejor, esta es la manera de renderizar un PDF dentro de una aplicación PyGTK. Voy a pegar acá abajo lo relevante:
def __init__(self):
[...]
self.document = poppler.document_new_from_file (uri, None)
self.n_pages = self.document.get_n_pages()
self.current_page = self.document.get_page(0)
self.scale = 1
self.width, self.height = self.current_page.get_size()
[...]
self.dwg = gtk.DrawingArea()
self.dwg.set_size_request(int(self.width), int(self.height))
self.dwg.connect("expose-event", self.on_expose)

[...]

def on_expose(self, widget, event):
cr = widget.window.cairo_create()
cr.set_source_rgb(1, 1, 1)

if self.scale != 1:
cr.scale(self.scale, self.scale)

cr.rectangle(0, 0, self.width, self.height)
cr.fill()
self.current_page.render(cr)
Como se puede ver, Poppler-Python se encarga de cargar el archivo, devolver la cantidad de páginas, etc., mientras que con GTK creamos el contexto de dibujo Cairo, y todo lo que hacemos es decirle a Poppler "dibujá acá, en este contexto Cairo".

Entonces me puse a jugar con wxPython para hacer lo mismo, !y funciona!

Acá está el ejemplo:
#!/usr/bin/env python
import wx
import wx.lib.wxcairo
import sys
import poppler

class MyFrame(wx.Frame):

def __init__(self):
wx.Frame.__init__(self, None, -1, "Cairo Test", size=(500,400))
self.Bind(wx.EVT_PAINT, self.OnPaint)
uri = "file://" + sys.argv[1]
self.document = poppler.document_new_from_file (uri, None)
self.n_pages = self.document.get_n_pages()

self.current_page = self.document.get_page(0)
self.scale = 1
self.width, self.height = self.current_page.get_size()
self.SetSize((self.width, self.height))

def OnPaint(self, event):
dc = wx.PaintDC(self)
cr = wx.lib.wxcairo.ContextFromDC(dc)
cr.set_source_rgb(1, 1, 1)

if self.scale != 1:
cr.scale(self.scale, self.scale)

cr.rectangle(0, 0, self.width, self.height)
cr.fill()
self.current_page.render(cr)

if __name__=="__main__":
app = wx.App()
f = MyFrame()
f.Show()
app.MainLoop()
Y acá está la captura de pantalla obligada, luego de ejecutar "python ejemplo.py /path/al/archivo.pdf" (click para agrandar):

De esta manera, es posible utilizar Python, Poppler y wxPython (o PyGTK) para mostrar y manejar el contenido de un archivo PDF dentro de nuestra aplicación y en casi cualquier plataforma, sin recurrir a componentes de terceros. :-)

Consideraciones:
Bueno, quisiera comentar que poppler-python parece ser un proyecto "joven", ya que todavía sólo está incluido en los repositorios de Ubuntu 9.04 en adelante. Con lo cual, en caso de utilizarlo en otras distribuciones y/o plataformas, hay que hacer un checkout desde sus repositorios y compilarlo; sus únicas dependencias son pycairo y poppler (0.10.x o posterior). Es muy sencillo hacerlo (el típico ./configure, make y make install), y no tuve ningún problema, utilizando Ubuntu 8.10.

En caso de querer utilizar Poppler-Python sobre el Cairo que incluye wxPython, lo mejor es utilizar una versión 2.8.9.x o superior de wxPython (ya con eso es suficiente, porque incluye Cairo en el paquete). Ubuntu 9.04 trae wxPython 2.8.9.1, Poppler 0.10.x y Poppler-Python en los repositorios, así que con un único apt-get ya tenemos todo lo necesario para hacer andar el ejemplo de arriba.

En mi caso, para probarlo en Ubuntu 8.10, tuve que compilar poppler desde los fuentes (traía la versión 0.8.x) y después compilar python-poppler, por un lado. Por el otro, para utilizar Cairo sobre wxPython 2.8.8.0 (el que viene en Ubuntu 8.10), llamé a Cairo con ctypes, como dice acá, y me quedó este ejemplo. Este procedimiento sería similar para cualquier otra distribución con versiones viejas de Poppler y/o de wxPython. Para la distribución de nuestro software todo se simplifica, ya que se puede usar tranquilamente Py2Exe, cx_Freeze o PyInstaller, por nombrar algunos. Pero eso es tema de otro post ;-)

* Poppler denomina Frontend a las diferentes APIs que tiene para ser utilizado, entre ellas QT4 (base de KDE) y Glib (GTK principalmente, base de Gnome). Y tiene como backend (es decir, necesita para dibujar) no sólo a Cairo, sino que también puede dibujar sobre Splash (ignoro qué es, y Google no me devuelve nada relevante).

Bueno, espero que al menos le sirva a alguien, que lo aprovechen y lo mejoren!

Saludos
Marcelo

6 comentarios:

Marpe dijo...

Sabrias decirme donde hay documentacion de esta libreria?

Estoy interesado en leer/escribir metadatos esobre PDF en Python.

Agradezco tu ayuda.

Marcelo Fernández dijo...

No, parece no haber mucha documentación, salvo las demos de poppler como de python-poppler. Habrá que ver el código fuente de otros programas que usen poppler...

¿Qué es lo que precisás hacer en particular? Quizás pdftk te sirva...

http://www.accesspdf.com/pdftk/

Saludos

Marpe dijo...
Este comentario ha sido eliminado por el autor.
Marpe dijo...

hola marcelo.

particularmente necesito,desde scripts python, leer datos PDF (lo cual he logrado con otras librerias), asi como escribir/editar metadatos hacia .PDF . Por ejemplo, obtener esta informacion (diccionario python por print):

Keywords from AndromedaHalo.pdf:
creation date - 20090802112530+09'00'
producer - dvipdfm 0.13.2c, Copyright \251 1998, by Mark A. Wicks
creator - TeX output 2009.08.02:1124
format - PDF 1.2
mimetype - application/pdf

He usado las librerias pypdf y pthon-extractor, y leen los datos, pero (creo) no escriben metadatos.
Tambien me gustaria hacer lo mismo con video, sonido... cualquier tipo de archivo.

pdftk parece ser un programa de linea de comandos, no usable como libreria, corrigeme si me equivoco.

Muchas gracias x la atencion ;)

Unknown dijo...

es posible usar esto con pyqt?

Marcelo Fernández dijo...

Uhm... estuve buscando un rato y parece que no hay muchas pruebas y ejemplos al mezclar Cairo y QT.

Lamentablemente no conozco mucho de QT, seguramente vas a tener más suerte preguntando justo esto en PyAr, que hay unos cuantos especialistas ;-)

http://www.python.com.ar

¡Saludos y Gracias!

PD: Ojo que el blog nuevo está en http://blog.marcelofernandez.info eh! :-)