Extensiones para rst2pdf: Fácil y poderoso
Es popular, por lejos mi programa más usado, pero pocos saben que es también fácil de extender (¡saludos a Patrick Maupin, que escribió esa parte!). Y no sólo eso, sino que se puede hacer que haga cosas bastante asombrosas con un poco de esfuerzo.
Para demostrarlo, creemos los títulos de sección más impresionantes del mundo y sus alrededores (a ver como hacen esto con LaTeX ;-)
Primero: definamos el problema
Los títulos que puede producir rst2pdf son aburridos. Si apretás todos los botones y tirás de todas las palancas, podés llegar a producir un título en Comic Sans, alineado derecha en letras rosas con fondo palta y borde rojo.
Y hasta ahí llega la personalización que podés hacer usando stylesheets. Normalmente eso es suficiente porque rst2pdf no está pensado para folletería (aunque alguna se ha hecho).
El problema real es que si te ponés en diseñador gráfico con rst2pdf, perdés la estructura del documento porque no estás siendo semántico.
Segundo: definir la meta
Uiero hacer encabezados como este:
La imagen está sacada de la biblioteca del congreso de EEUU con un poco de (mal) trabajo de gimp para dejar el espacio libre a la izquierda, y el título se agregó con Inkscape.
¿Se puede hacer eso con rst2pdf? No, ni cerca. No sin programar. ¡Así que programemos una extensión que te permita crear cualquier encabezado que vos quieras dentro de los límites de Inkscape!
Primero creamos un template SVG para los encabezados (Es un poco grande porque tiene la imagen adentro).
Tercero: el flowable encabezado-imagen
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 estás haciendo un título, hay una cosa más, necesitás crear un bookmark para que aparezca en la tabla de contenidos 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 define, en una extensión, una clase como esta:
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 extensión está en SVN y se puede probar así:
[fancytitles]$ rst2pdf -e fancytitles -e inkscape demo.txt -b1
Hay que habilitar la extensión Inkscape para que los SVG se vean lo mejor posible. Y esta es la salida:
Se puede cambiar cualquier elemento de la salida. Eso es ser extensible :-)