Skip to main content

Ralsina.Me — Roberto Alsina's website

Come see me in Bahía Blanca next weekend!

I will be speak­ing at the Jor­nadas del Sur in Bahía Blan­ca this week­end (Au­gust 14/15 and 16).

For a change, I will not be giv­ing my old tired talk­s, but a brand new one, called "The Am­a­teur", and prob­a­bly a light­ning talk or some­thing else.

The usu­al of­fer of free beer will not be pos­si­ble this year, so: If you men­tion this blog in the QA ses­sion, you get... free can­dy!

Af­ter that, I will be speak­ing at FM La Tribu on sat­ur­day Au­gust 21st in the Char­las Abier­tas 2010 where I will be speak­ing about a lot of things be­tween vir­tualenv and nose and tox and oth­er test­ing-re­lat­ed top­ic­s.

It's coming: PET - Python Entre Todos

I have been dragged in­to YAP (Yet An­oth­er Pro­jec­t) and in this case it's the first-is­sue-ev­er of PET - Python En­tre To­dos a python on­line mag­a­zine in span­ish.

This is an emer­gent out­put of PyAr the on­line Python com­mu­ni­ty of Ar­genti­na, and it may nev­er have a sec­ond is­sue un­less we get a lot of help, so pay at­ten­tion to the first one :-)

I have been do­ing web­mas­ter­ing, edit­ing and PDF pro­duc­tion, Emil­iano Dal­la Verde Mar­cozzi has been help­ing ev­ery­where and get­ting ar­ti­cles.

The first is­sue is some 45 A5 pages (or 23 A4 ones) in small type, so it's not ex­act­ly huge, but it has half a dozen ar­ti­cles aimed at dif­fer­ent python lev­el­s.

It has been a lot of fun so far, I hope it's well re­ceived!

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


Contents © 2000-2023 Roberto Alsina