Renderizando PDFs en Python con Poppler II

Hace unos días me llegó un mail de alguien preguntándome cómo, a partir de la parte I de este artículo, hacer un sencillo visor de PDFs con wxPython. Me encontré con algunas dificultades, principalmente que el ScrolledWindow de wxPython no permite actualizarse dinámicamente, o automáticamente según el contenido (esto sí es bastante sencillo en GTK); con lo cual se complicaba hacer zoom, modificar el tamaño de la ventana y adaptar los scrollbars, etc.

Sin embargo, con alguna vuelta de más pude armar un ejemplo, que paso a dejar acá:

#!/usr/bin/env python
# coding: utf-8

""" 
    wxPDFViewer - Simple PDF Viewer using Python-Poppler and wxPython 
    Marcelo Fidel Fernandez - MIT License
    http://www.marcelofernandez.info - [email protected]
"""

import wx
import wx.lib.wxcairo as wxcairo
import sys
import poppler

 
class PDFWindow(wx.ScrolledWindow):
    """ This example class implements a PDF Viewer Window, handling Zoom and Scrolling """

    MAX_SCALE = 2
    MIN_SCALE = 1
    SCROLLBAR_UNITS = 20  # pixels per scrollbar unit

    def __init__(self, parent):
        wx.ScrolledWindow.__init__(self, parent, wx.ID_ANY)
        # Wrap a panel inside
        self.panel = wx.Panel(self)
        # Initialize variables
        self.n_page = 0
        self.scale = 1
        self.document = None
        self.n_pages = None
        self.current_page = None
        self.width = None
        self.height = None
        # Connect panel events
        self.panel.Bind(wx.EVT_PAINT, self.OnPaint)
        self.panel.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self.panel.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.panel.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)

    def LoadDocument(self, file):
        self.document = poppler.document_new_from_file("file://" + file, None)
        self.n_pages = self.document.get_n_pages()
        self.current_page = self.document.get_page(self.n_page)
        self.width, self.height = self.current_page.get_size() 
        self._UpdateSize()

    def OnPaint(self, event):
        dc = wx.PaintDC(self.panel)
        cr = wxcairo.ContextFromDC(dc)
        cr.set_source_rgb(1, 1, 1)  # White background
        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)

    def OnLeftDown(self, event):
        self._UpdateScale(self.scale + 0.2)

    def OnRightDown(self, event):
        self._UpdateScale(self.scale - 0.2)

    def _UpdateScale(self, new_scale):
        if new_scale >= PDFWindow.MIN_SCALE and new_scale <= PDFWindow.MAX_SCALE:
            self.scale = new_scale
            # Obtain the current scroll position
            prev_position = self.GetViewStart() 
            # Scroll to the beginning because I'm going to redraw all the panel
            self.Scroll(0, 0) 
            # Redraw (calls OnPaint and such)
            self.Refresh() 
            # Update panel Size and scrollbar config
            self._UpdateSize()
            # Get to the previous scroll position
            self.Scroll(prev_position[0], prev_position[1]) 

    def _UpdateSize(self):
        u = PDFWindow.SCROLLBAR_UNITS
        self.panel.SetSize((self.width*self.scale, self.height*self.scale))
        self.SetScrollbars(u, u, (self.width*self.scale)/u, (self.height*self.scale)/u)

    def OnKeyDown(self, event):
        update = True
        # More keycodes in http://docs.wxwidgets.org/stable/wx_keycodes.html#keycodes
        keycode = event.GetKeyCode() 
        if keycode in (wx.WXK_PAGEDOWN, wx.WXK_SPACE):
            next_page = self.n_page + 1
        elif keycode == wx.WXK_PAGEUP:
            next_page = self.n_page - 1
        else:
            update = False
        if update and (next_page >= 0) and (next_page < self.n_pages):
                self.n_page = next_page
                self.current_page = self.document.get_page(next_page)
                self.Refresh()


class MyFrame(wx.Frame):
 
    def __init__(self):
        wx.Frame.__init__(self, None, -1, "wxPdf Viewer", size=(800,600))
        self.pdfwindow = PDFWindow(self)
        self.pdfwindow.LoadDocument(sys.argv[1])
        self.pdfwindow.SetFocus() # To capture keyboard events

 
if __name__=="__main__":
    app = wx.App()
    f = MyFrame()
    f.Show()
    app.MainLoop()

Si bien tengo entendido que el scroll no es "óptimo", ya que en cada OnPaint() se redibuja todo el PDF, anda bastante bien en mi instalación y no parece ser un problema. Lógicamente sí se puede redibujar un área en particular con Cairo (sería la que queda "invalidada" por el scroll), pero wxPython me explotó al intentar algunas cosas por un lado, y me parece que en realidad no sé hacerlo bien por el otro (si alguien puede tirar una soga en esto en particular, bienvenido sea).

Me entretuve mucho haciendo esto, y espero que a alguien le sea de utilidad; y aunque todavía Python-Poppler no está disponible para Windows en forma binaria (pero puede ser compilado, bug acá), sí se puede usar en cualquier Linux, OSX, etc. Y nuevamente, hay una demo para hacer esto mismo con PyGTK, y la demo de Poppler en C es mucho más completa, además de fácil de leer y traducir a Python.

Saludos


Comentarios

8 respuestas a «Renderizando PDFs en Python con Poppler II»

  1. Exelente el ejemplo, una pregunta. No existe la posibilidad de usar la libreria poppler con windows??

  2. Lo digo en el artículo al final, al parecer no están los binarios, pero se puede compilar y usar en Windows. Una vez que hiciste un binario, puede servir para cualquier windows, claro.

    https://bugs.launchpad.net/poppler-python/+bug/499592

    Saludos

  3. Avatar de Manuel

    De mucha utilidad tu artículo, enhorabuenta.
    Yo estoy peleándome con PyQt y es algo diferente, por ejemplo la carga del documento es con «load» teniendo que llamarlo así: document = popplerqt4.poppler.document.load(‘file.pdf’) Así que ya que estamos por si le sirve a alguien ahí esta dicho.
    Quería preguntarte si te había dado por intentar imprimirlo. Yo quiero, una vez cargado, mandarlo a imprimir con QPrinter y QPrinterDialog, pero no doy con la tecla. Me sale el dialogo perfectamente, ahora la cosa es darle la orden document.print_(printer) o algo por el estilo, pero ese evidentemente no funciona, jeje. Alguna sugerencia?
    Saludos

  4. Avatar de Manuel

    quise decir enhorabuena! se entiende no?, jeje

  5. Hola Manuel,

    Para imprimir ya dependés de la plataforma, no de Poppler. En Linux/Unix es tanto como llamar al comando «lp archivo.pdf», siempre que esté instalado CUPS.

    http://www.cups.org/documentation.php/options.html

    Saludos

  6. Avatar de Manuel

    @Marcelo
    Lo cierto es que lo que pretendo es que imprima igual que cualquier aplicación cuando le damos a imprimir, que sale el dialogo de impresión para elegir la impresora y posteriormente imprimir.
    Y mi código pretendo que sea multiplataforma, al menos comenzar por linux y windows, es por eso que no me interesa enviarlo a la cola de impresión con lp. No sé si me entiendes.

    Entonces lo que necesito es el código para imprimir, una vez configurada la impresora con QPrinter.
    De modo que si fuera un QTextEdit si seria como dije antes, document.print_(printer), pero en este caso es un Poppler.Document el cual no tiene la función print_.
    Busco la correspondiente a este último, al Poppler.Document

    Saludos

  7. Hola Manuel,

    No es tan fácil como lo planteás, normalmente la impresión la implementas por plataforma, no por la librería de parseo/display de PDFs, eso es lo que quería decir. Y no conozco si QT brinda algún acceso fácil y multi-plataforma para eso.

    Te sugiero que te suscribas a la lista de Python Argentina (PyAr), donde hay muchos expertos en QT, que seguro te pueden dar una mano.

    Esta es la URL del grupo: http://www.python.org.ar

    Saludos

  8. Avatar de Manuel

    @Marcelo
    Vale, muchas gracias por tu respuesta, echaré un vistazo por la lista de Python.

    Saludos

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *