Skip to main content

Ralsina.Me — Roberto Alsina's website

Posts about open source (old posts, page 9)

Extending rst2pdf: easy and powerful

I do al­most all my busi­ness writ­ing (and my book) us­ing re­struc­tured tex­t. And when I want to pro­duce print­-qual­i­ty out­put, I tend to use my own tool, rst2pdf.

It's pop­u­lar, sure­ly my most pop­u­lar pro­gram, but very few know it's al­so ex­treme­ly easy to ex­tend (hat tip to Patrick Maupin, who wrote this part!). And not on­ly that, but you can make it do some amaz­ing stuff with a lit­tle ef­fort.

To show that, let's cre­ate the most daz­zling sec­tion head­ings known to man ( Let's see you do what this ba­by can do in La­TeX ;-).

First: de­fine the prob­lem.

The ti­tles rst2pdf can pro­duce are bor­ing. If you pull ev­ery lever and push ev­ery but­ton, you may end with the ti­tle tex­t, in a Com­ic San­s, right-aligned, in pink let­ter­ing, with av­o­cado-­green back­ground and a red bor­der.

And that's as far as the cus­tomiza­tion ca­pa­bil­i­ties go us­ing stylesheet­s. That's usu­al­ly enough, be­cause rst2pdf is not meant for brochures or some­thing like that (but I have done it).

The re­al prob­lem is that when you get all graph­ic-de­sign­er on rst2pdf, you lose doc­u­ment struc­ture, be­cause you are not be­ing se­man­tic.

Sec­ond: de­fine the goal.

So, imag­ine you want to make a head­ing that looks like this:

fancytitles1

The im­age is tak­en from the li­brary of con­gress with some light (and bad) gimp­ing by me to leave that emp­ty space at the left, and the ti­tle was added us­ing Inkscape.

Can you do that with rst2pdf? Hell no you can't. Not with­out cod­ing. So let's code an ex­ten­sion that lets you cre­ate any head­ing you like with­in the lim­its of Inkscape!

First, we cre­ate a SVG tem­plate for the head­ings (it's a bit big be­cause it has the bit­map em­bed­ded).

Three: the im­age-­head­ing flow­able

Suppose you have an image of the heading just like the one above. How would you draw that in a PDF? In reportlab, you do that using flowables which are elements that compose the story that is your document. These flowables are arranged in pages, and that's your PDF.

If you are do­ing a head­ing, there's a bit more, in that you need to add a book­mark, so it ap­pears on the PDF ta­ble of con­tents.

So, here's a flowable that does just that. It's cobbled from pieces inside rst2pdf, and is basically an unholy mix of Heading and 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)

And how do we tell rst2df to use that instead of a regular Heading? by overriding the TitleHandler class. Here's where the extension magic kicks in.

If you de­fine, in an ex­ten­sion, a class like this:

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

Then that class will handle all docutils nodes of class docutils.nodes.title. Here, I just took rst2pdf.genelements.HandleTitle and changed how it works for level-1 headings, making it generate a FancyHeading instead of a Heading... and that's all there is to it.

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

The full ex­ten­sion is in SVN and you can try it this way:

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

You need to en­able the Inkscape ex­ten­sion so the SVG will look nice. And here's the out­put:

fancytitles2

You can over­ride how any el­e­ment is han­dled. That's be­ing ex­ten­si­ble :-)

This is why Qt (and PyQt) are cool

Ale­jan­dro Dolina once wrote (and this is from mem­o­ry that's prob­a­bly 25 years old) of a round ta­ble dis­cussing "What's Tan­go?", and how af­ter two hours of dis­cussing the na­ture, char­ac­ter­is­tics and his­to­ry of tan­go, one of the mem­bers of the pan­el picked up a ban­doneón, played "El apache ar­genti­no" stood up and left with­out say­ing a word.

So, why are Qt and PyQt cool?

Au­dio play­er wid­get:

# -*- 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()

Video play­er wid­get:

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()

...

Desktop apps and clouds (with video)

I en­joy cre­at­ing desk­top ap­pli­ca­tion­s. That means I may be a mem­ber of a dy­ing breed, since web apps are go­ing to make us all ob­so­lete next week, but I do en­joy do­ing it.

The bad side of it is, of course that some­times it's much more con­ve­nient to use a web ap­pli­ca­tion. For ex­am­ple, I have aban­doned my own ba­by (uRSSus) be­cause google read­er is just eas­i­er and more con­ve­nient to use.

But then I thought... what both­ers me of uRSSus? And there are quite a few things!

  1. It's not in all com­put­ers I may use

    That means I will not ev­er be able to use it ex­­clu­­sive­­ly.

  2. It's pret­­ty use­­less with­­out an In­­ter­net con­nec­­tion (but so is google read­­er most­­ly)

  3. Since I can't use it ex­­clu­­sive­­ly, I end with feeds on uRSSus that are not on google read­­er and vicev­er­sa.

  4. It's freak­ing slow

So, I de­cid­ed to see what I could do about that with­out giv­ing up the good side of uRSSus:

  1. It looks much nicer than a web ap­p, be­­cause it looks like a desk­­top app

  2. It does things like open­ing the site in­­stead of show­ing the feed item (great for par­­tial con­­tent feed­s)

  3. I wrote it (yes, that's a fea­­ture for me. I like self­­-­­made pro­­gram­s)

So, this at­tempt at rewrit­ing the desk­top RSS read­er pro­duced this:

As you can see in the above video, this read­er syncs the sub­scrip­tion list to google read­er. It will al­so even­tu­al­ly sync your read­/un­read post­s.

It still can open full sites in­stead of feed item­s, it has/will have a heck of an off­line mode (full pages cap­tured as im­ages, for ex­am­ple), and... it's very very fast.

It's much faster than google read­er in Chromi­um, and hel­la faster than uRSSus. That was done via smarter cod­ing, so it prob­a­bly means I was brain­dead be­fore and ex­pe­ri­enced a mi­nor re­cov­ery.

The code is not fit for re­lease (for ex­am­ple, the data­base schema will change) but you can try it: http://­code.­google.­com/p/kakawana/­source/check­out

Learn python! For free! With me! (in part)

I'm one of the speak­ers for the free python cour­ses in FM La Tribu, in Buenos Aires.

Ev­ery sat­ur­day you can learn some­thing from one of the best python pro­gram­mers in a thou­sand miles, or from me.

I'll be teach­ing about vir­tualen­v, build­out, nose and oth­er things on Au­gust 21, about GUI stuff on Sep­tem­ber 25 and Oc­to­ber 2, and about PyQt in Oc­to­ber 30.

This is all for free, and I hope lots of peo­ple show up!

The full sched­ule is here.


Contents © 2000-2023 Roberto Alsina