Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

Publicaciones sobre open source (publicaciones antiguas, página 9)

Extensiones para rst2pdf: Fácil y poderoso

Es po­pu­la­r, por le­jos mi pro­gra­ma más usa­do, pe­ro po­cos sa­ben que es tam­bién fá­cil de ex­ten­der (¡s­alu­dos a Pa­tri­ck Mau­pi­n, que es­cri­bió esa par­te!). Y no só­lo eso, sino que se pue­de ha­cer que ha­ga co­sas bas­tan­te asom­bro­sas con un po­co de es­fuer­zo.

Pa­ra de­mos­trar­lo, cree­mos los tí­tu­los de sec­ción más im­pre­sio­nan­tes del mun­do y sus al­re­de­do­res (a ver co­mo ha­cen es­to con La­TeX ;-)

Pri­me­ro: de­fi­na­mos el pro­ble­ma

Los tí­tu­los que pue­de pro­du­cir rs­t2­pdf son abu­rri­do­s. Si apre­tás to­dos los bo­to­nes y ti­rás de to­das las pa­lan­ca­s, po­dés lle­gar a pro­du­cir un tí­tu­lo en Co­mic San­s, ali­nea­do de­re­cha en le­tras ro­sas con fon­do pal­ta y bor­de ro­jo.

Y has­ta ahí lle­ga la per­so­na­li­za­ción que po­dés ha­cer usan­do sty­les­hee­ts. Nor­mal­men­te eso es su­fi­cien­te por­que rs­t2­pdf no es­tá pen­sa­do pa­ra fo­lle­te­ría (aun­que al­gu­na se ha he­cho).

El pro­ble­ma real es que si te po­nés en di­se­ña­dor grá­fi­co con rs­t2­pdf, per­dés la es­truc­tu­ra del do­cu­men­to por­que no es­tás sien­do se­mánti­co.

Se­gun­do: de­fi­nir la me­ta

Uie­ro ha­cer en­ca­be­za­dos co­mo es­te:

fancytitles1

La ima­gen es­tá saca­da de la bi­blio­te­ca del con­gre­so de EEUU con un po­co de (ma­l) tra­ba­jo de gimp pa­ra de­jar el es­pa­cio li­bre a la iz­quier­da, y el tí­tu­lo se agre­gó con Inks­ca­pe.

¿Se pue­de ha­cer eso con rs­t2­pdf? No, ni cer­ca. No sin pro­gra­ma­r. ¡A­sí que pro­gra­me­mos una ex­ten­sión que te per­mi­ta crear cual­quier en­ca­be­za­do que vos quie­ras den­tro de los lí­mi­tes de Inks­ca­pe!

Pri­me­ro crea­mos un tem­pla­te SVG pa­ra los en­ca­be­za­dos (Es un po­co gran­de por­que tie­ne la ima­gen aden­tro­).

Ter­ce­ro: el flo­wa­ble en­ca­be­za­do­-i­ma­gen

Suponete que tenés una imagen del encabezado exactamente como esa de arriba. ¿Como lo dibujás en un PDF? En reportlab se hace usando flowables que son elementos que componen la historia que es tu documento. Esos flowables se acomodan en páginas, y eso es tu PDF.

Si es­tás ha­cien­do un tí­tu­lo, hay una co­sa má­s, ne­ce­si­tás crear un book­ma­rk pa­ra que apa­rez­ca en la ta­bla de con­te­ni­dos del PDF.

Este es un flowable que hace eso. Está hecho pegando pedazos de cosas de rst2pdf y es una cruza maligna entre Heading y MyImage:

class FancyHeading(MyImage):
  '''This is a cross between the Heading flowable, that adds outline
  entries so you have a PDF TOC, and MyImage, that draws images'''

  def __init__(self, *args, **kwargs):
      # The inicialization is taken from rst2pdf.flowables.Heading
      self.stext = kwargs.pop('text')
      # Cleanup title text
      self.stext = re.sub(r'<[^>]*?>', '', unescape(self.stext))
      self.stext = self.stext.strip()

      # Stuff needed for the outline entry
      self.snum = kwargs.pop('snum')
      self.level = kwargs.pop('level')
      self.parent_id= kwargs.pop('parent_id')


      MyImage.__init__(self, *args, **kwargs)

  def drawOn(self,canv,x,y,_sW):

      ## These two lines are magic.
      #if isinstance(self.parent_id, tuple):
          #self.parent_id=self.parent_id[0]

      # Add outline entry. This is copied from rst2pdf.flowables.heading
      canv.bookmarkHorizontal(self.parent_id,0,0+self.image.height)

      if canv.firstSect:
          canv.sectName = self.stext
          canv.firstSect=False
          if self.snum is not None:
              canv.sectNum = self.snum
          else:
              canv.sectNum = ""

      canv.addOutlineEntry(self.stext.encode('utf-8','replace'),
                                self.parent_id.encode('utf-8','replace'),
                                int(self.level), False)

      # And let MyImage do all the drawing
      MyImage.drawOn(self,canv,x,y,_sW)

¿Y cómo le decimos a rst2pdf que use eso en vez de un Heading común? Pisando la clase TitleHandler. Acá es donde entra la magia de las extensiones.

Si se de­fi­ne, en una ex­ten­sió­n, una cla­se co­mo es­ta:

class FancyTitleHandler(genelements.HandleParagraph, docutils.nodes.title):

Entonces esa clase va a ser responsable de todos los nodos de clase docutils.nodes.title. En este caso, tan solo copié rst2pdf.genelements.HandleTitle y cambié el resultado de los encabezados nivel 1 para que use un FancyHeading en vez de un Heading... y eso es todo.

class FancyTitleHandler(genelements.HandleParagraph, docutils.nodes.title):
  '''
  This class will handle title nodes.

  It takes a "titletemplate.svg", replaces TITLEGOESHERE with
  the actual title text, and draws that using the FancyHeading flowable
  (see below).

  Since this class is defined in an extension, it
  effectively replaces rst2pdf.genelements.HandleTitle.
  '''

  def gather_elements(self, client, node, style):
      # This method is copied from the HandleTitle class
      # in rst2pdf.genelements.

      # Special cases: (Not sure this is right ;-)
      if isinstance(node.parent, docutils.nodes.document):
          #node.elements = [Paragraph(client.gen_pdftext(node),
                                      #client.styles['title'])]
          # The visible output is now done by the cover template
          node.elements = []
          client.doc_title = node.rawsource
          client.doc_title_clean = node.astext().strip()
      elif isinstance(node.parent, docutils.nodes.topic):
          node.elements = [Paragraph(client.gen_pdftext(node),
                                      client.styles['topic-title'])]
      elif isinstance(node.parent, docutils.nodes.Admonition):
          node.elements = [Paragraph(client.gen_pdftext(node),
                                      client.styles['admonition-title'])]
      elif isinstance(node.parent, docutils.nodes.table):
          node.elements = [Paragraph(client.gen_pdftext(node),
                                      client.styles['table-title'])]
      elif isinstance(node.parent, docutils.nodes.sidebar):
          node.elements = [Paragraph(client.gen_pdftext(node),
                                      client.styles['sidebar-title'])]
      else:
          # Section/Subsection/etc.
          text = client.gen_pdftext(node)
          fch = node.children[0]
          if isinstance(fch, docutils.nodes.generated) and \
              fch['classes'] == ['sectnum']:
              snum = fch.astext()
          else:
              snum = None
          key = node.get('refid')
          maxdepth=4
          if reportlab.Version > '2.1':
              maxdepth=6

          # The parent ID is the refid + an ID to make it unique for Sphinx
          parent_id=(node.parent.get('ids', [None]) or [None])[0]+u'-'+unicode(id(node))
          if client.depth > 1:
              node.elements = [ Heading(text,
                      client.styles['heading%d'%min(client.depth, maxdepth)],
                      level=client.depth-1,
                      parent_id=parent_id,
                      node=node,
                      )]
          else: # This is an important title, do our magic ;-)
              # Hack the title template SVG
              tfile = open('titletemplate.svg')
              tdata = tfile.read()
              tfile.close()
              tfile = tempfile.NamedTemporaryFile(dir='.', delete=False, suffix='.svg')
              tfname = tfile.name
              tfile.write(tdata.replace('TITLEGOESHERE', text))
              tfile.close()

              # Now tfname contains a SVG with the right title.
              # Make rst2pdf delete it later.
              client.to_unlink.append(tfname)

              e = FancyHeading(tfname, width=700, height=100,
                  client=client, snum=snum, parent_id=parent_id,
                  text=text, level=client.depth-1)

              node.elements = [e]

          if client.depth <= client.breaklevel:
              node.elements.insert(0, MyPageBreak(breakTo=client.breakside))
      return node.elements

La ex­ten­sión es­tá en SVN y se pue­de pro­bar así:

[fancytitles]$ rst2pdf -e fancytitles -e inkscape demo.txt -b1

Hay que ha­bi­li­tar la ex­ten­sión Inks­ca­pe pa­ra que los SVG se vean lo me­jor po­si­ble. Y es­ta es la sali­da:

fancytitles2

Se pue­de cam­biar cual­quier ele­men­to de la sali­da. Eso es ser ex­ten­si­ble :-)

Por esto es que Qt y PyQt valen la pena

¿Por qué es­tán bue­nos Qt y Py­Q­t?

Wi­dget re­pro­duc­tor de au­dio:

# -*- coding: utf-8 -*-

import sys, os
from PyQt4 import QtCore, QtGui, uic
from PyQt4.phonon import Phonon
import icons_rc

class AudioPlayer(QtGui.QWidget):
    def __init__(self, url, parent = None):

        self.url = url

        QtGui.QWidget.__init__(self, parent)
        self.setSizePolicy(QtGui.QSizePolicy.Expanding,
            QtGui.QSizePolicy.Preferred)


        self.player = Phonon.createPlayer(Phonon.MusicCategory,
            Phonon.MediaSource(url))
        self.player.setTickInterval(100)
        self.player.tick.connect(self.tock)

        self.play_pause = QtGui.QPushButton(self)
        self.play_pause.setIcon(QtGui.QIcon(':/icons/player_play.svg'))
        self.play_pause.clicked.connect(self.playClicked)
        self.player.stateChanged.connect(self.stateChanged)

        self.slider = Phonon.SeekSlider(self.player , self)

        self.status = QtGui.QLabel(self)
        self.status.setAlignment(QtCore.Qt.AlignRight |
            QtCore.Qt.AlignVCenter)

        self.download = QtGui.QPushButton("Download", self)
        self.download.clicked.connect(self.fetch)

        layout = QtGui.QHBoxLayout(self)
        layout.addWidget(self.play_pause)
        layout.addWidget(self.slider)
        layout.addWidget(self.status)
        layout.addWidget(self.download)

    def playClicked(self):
        if self.player.state() == Phonon.PlayingState:
            self.player.pause()
        else:
            self.player.play()

    def stateChanged(self, new, old):
        if new == Phonon.PlayingState:
            self.play_pause.setIcon(QtGui.QIcon(':/icons/player_pause.svg'))
        else:
            self.play_pause.setIcon(QtGui.QIcon(':/icons/player_play.svg'))

    def tock(self, time):
        time = time/1000
        h = time/3600
        m = (time-3600*h) / 60
        s = (time-3600*h-m*60)
        self.status.setText('%02d:%02d:%02d'%(h,m,s))

    def fetch(self):
        print 'Should download %s'%self.url

def main():
    app = QtGui.QApplication(sys.argv)
    window=AudioPlayer(sys.argv[1])
    window.show()
    # It's exec_ because exec is a reserved word in Python
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

Wi­dget re­pro­duc­tor de vi­deo:

import sys, os
from PyQt4 import QtCore, QtGui, uic
from PyQt4.phonon import Phonon
import icons_rc

class VideoPlayer(QtGui.QWidget):
    def __init__(self, url, parent = None):

        self.url = url

        QtGui.QWidget.__init__(self, parent)
        self.setSizePolicy(QtGui.QSizePolicy.Expanding,
            QtGui.QSizePolicy.Preferred)


        self.player = Phonon.VideoPlayer(Phonon.VideoCategory,self)
        self.player.load(Phonon.MediaSource(self.url))
        self.player.mediaObject().setTickInterval(100)
        self.player.mediaObject().tick.connect(self.tock)

        self.play_pause = QtGui.QPushButton(self)
        self.play_pause.setIcon(QtGui.QIcon(':/icons/player_play.svg'))
        self.play_pause.clicked.connect(self.playClicked)
        self.player.mediaObject().stateChanged.connect(self.stateChanged)

        self.slider = Phonon.SeekSlider(self.player.mediaObject() , self)

        self.status = QtGui.QLabel(self)
        self.status.setAlignment(QtCore.Qt.AlignRight |
            QtCore.Qt.AlignVCenter)

        self.download = QtGui.QPushButton("Download", self)
        self.download.clicked.connect(self.fetch)
        topLayout = QtGui.QVBoxLayout(self)
        topLayout.addWidget(self.player)
        layout = QtGui.QHBoxLayout(self)
        layout.addWidget(self.play_pause)
        layout.addWidget(self.slider)
        layout.addWidget(self.status)
        layout.addWidget(self.download)
        topLayout.addLayout(layout)
        self.setLayout(topLayout)

    def playClicked(self):
        if self.player.mediaObject().state() == Phonon.PlayingState:
            self.player.pause()
        else:
            self.player.play()

    def stateChanged(self, new, old):
        if new == Phonon.PlayingState:
            self.play_pause.setIcon(QtGui.QIcon(':/icons/player_pause.svg'))
        else:
            self.play_pause.setIcon(QtGui.QIcon(':/icons/player_play.svg'))

    def tock(self, time):
        time = time/1000
        h = time/3600
        m = (time-3600*h) / 60
        s = (time-3600*h-m*60)
        self.status.setText('%02d:%02d:%02d'%(h,m,s))

    def fetch(self):
        print 'Should download %s'%self.url

def main():
    app = QtGui.QApplication(sys.argv)
    window=VideoPlayer(sys.argv[1])
    window.show()
    # It's exec_ because exec is a reserved word in Python
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

...

Aplicaciones de escritorio y nubes (con video)

Lo ma­lo es, por su­pues­to, que a ve­ces es mu­cho más con­ve­nien­te usar una apli­ca­ción we­b. Por ejem­plo, he aban­do­na­do a mi pro­pio be­bé (uR­S­Sus) por­que google rea­der es más fá­cil y con­ve­nien­te.

Pe­ro en­ton­ces pen­sé... ¿qué me mo­les­ta de uR­S­Sus? ¡Y son bas­tan­tes co­sas!

  1. No es­­tá en to­­­das las co­m­­pu­­ta­­do­­­ras que uso. Eso quie­­re de­­cir que ja­­más po­­­dré usar­­la de fo­r­­ma ex­­clu­­si­­va.

  2. Es ba­s­­tan­­te inú­­til sin una co­­­ne­­xión a In­­te­r­­net (pe­­ro ta­m­­bién lo es google rea­­de­­r).

  3. Co­­­mo no la pue­­do usar ex­­clu­­si­­va­­men­­te, te­r­­mino con fee­­ds en uR­S­­Sus que no es­­tán en google rea­­der y vi­­ce­­ve­r­­s­a.

  4. Es len­­tí­­si­­ma.

En­ton­ces de­ci­dí ver que pue­do ha­cer al res­pec­to sin aban­do­nar el la­do bue­no de uR­S­Sus:

  1. Me gus­­ta más que una apli­­ca­­ción we­­b, po­r­­que es de es­­cri­­to­­­rio.

  2. Ha­­ce co­­sas co­­­mo abrir el si­­tio en vez de mo­s­­trar el post del feed (bue­­no pa­­ra fee­­ds de co­n­­te­­ni­­do pa­r­­cia­­l)

  3. La hi­­ce yo (sí, eso es un fea­­tu­­re pa­­ra mí. Me gus­­ta te­­ner pro­­­gra­­mas que yo hi­­ce)

En­ton­ce­s, es­te in­ten­to de rees­cri­bir el lec­tor RSS de es­cri­to­rio pro­du­jo es­to:

Co­mo se pue­de ver en el vi­deo, es­te lec­tor sin­cro­ni­za la lis­ta de sus­crip­cio­nes con google. Tam­bién even­tual­men­te sin­cro­ni­za­rá pos­ts leí­do­s/no leí­do­s.

Si­gue pu­dien­do abrir si­tios com­ple­tos en vez de pos­ts, tie­ne/­ten­drá un muy buen mo­do offli­ne (pá­gi­nas com­ple­tas cap­tu­ra­das co­mo imá­ge­nes, por ejem­plo­), y... es muy muy rá­pi­do.

Es mu­cho más rá­pi­do que google rea­der en ch­ro­miu­m, y mu­chí­si­mo más rá­pi­do que uR­S­Sus. Eso es por­que es­tá me­jor el có­di­go, así que pro­ba­ble­men­te sig­ni­fi­ca que an­tes te­nía muer­te ce­re­bral y he ex­pe­ri­men­ta­do una le­ve me­jo­ría.

El có­di­go no es ap­to pa­ra pu­bli­ca­ción (por ejem­plo, el sche­ma de la ba­se de da­tos va a cam­bia­r) pe­ro se pue­de pro­ba­r: http://­co­de.­google.­co­m/­p/kakawa­na/­sour­ce/­che­ckout

Aprendé python! Gratis! Conmigo! (una parte)

Ca­da sá­ba­do po­dés apren­der al­go de al­gu­nos de los me­jo­res pro­gra­ma­do­res Py­thon de la co­mar­ca (o de mí).

Voy a en­se­ñar vir­tua­len­v, buil­dou­t, no­se y otras co­sas el 21/8, co­sas de GUI el 25/9 y 2/10 y Py­Qt el 30/10.

¡To­do es gra­tis, y oja­lá ven­ga mu­cha gen­te!

Cro­no­gra­ma com­ple­to acá.


Contents © 2000-2023 Roberto Alsina