Extending rst2pdf: easy and powerful
I do almost all my business writing (and my book) using restructured text. And when I want to produce print-quality output, I tend to use my own tool, rst2pdf.
It's popular, surely my most popular program, but very few know it's also extremely easy to extend (hat tip to Patrick Maupin, who wrote this part!). And not only that, but you can make it do some amazing stuff with a little effort.
To show that, let's create the most dazzling section headings known to man ( Let's see you do what this baby can do in LaTeX ;-).
First: define the problem.
The titles rst2pdf can produce are boring. If you pull every lever and push every button, you may end with the title text, in a Comic Sans, right-aligned, in pink lettering, with avocado-green background and a red border.
And that's as far as the customization capabilities go using stylesheets. That's usually enough, because rst2pdf is not meant for brochures or something like that (but I have done it).
The real problem is that when you get all graphic-designer on rst2pdf, you lose document structure, because you are not being semantic.
Second: define the goal.
So, imagine you want to make a heading that looks like this:
The image is taken from the library of congress with some light (and bad) gimping by me to leave that empty space at the left, and the title was added using Inkscape.
Can you do that with rst2pdf? Hell no you can't. Not without coding. So let's code an extension that lets you create any heading you like within the limits of Inkscape!
First, we create a SVG template for the headings (it's a bit big because it has the bitmap embedded).
Three: the image-heading flowable
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 doing a heading, there's a bit more, in that you need to add a bookmark, so it appears on the PDF table of contents.
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 define, in an extension, a class like this:
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 extension is in SVN and you can try it this way:
[fancytitles]$ rst2pdf -e fancytitles -e inkscape demo.txt -b1
You need to enable the Inkscape extension so the SVG will look nice. And here's the output:
You can override how any element is handled. That's being extensible :-)