Separate svv.pdf module
authorTero Marttila <terom@fixme.fi>
Mon, 20 Dec 2010 23:15:08 +0200
changeset 4 b3a1ab44f517
parent 3 44122295656a
child 5 c72e0314b930
Separate svv.pdf module
svv/pdf.py
svv/wsgi.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svv/pdf.py	Mon Dec 20 23:15:08 2010 +0200
@@ -0,0 +1,435 @@
+# coding: utf-8
+
+"""
+    PDF output
+"""
+
+from reportlab import platypus as rlpp
+from reportlab.lib.units import inch
+from reportlab.lib import pagesizes, styles
+
+from cStringIO import StringIO
+import itertools
+import logging
+import datetime
+
+
+log = logging.getLogger('svv.pdf')
+
+class Styles (object) :
+    """
+        Simple stylesheet
+    """
+
+    samplestyles = styles.getSampleStyleSheet()
+
+    # normal text
+    h1 = styles.ParagraphStyle('Heading1', samplestyles['h1'],
+            fontName        = 'Times-Bold',
+            fontSize        = 22,
+            spaceBefore     = 0,
+            spaceAfter      = 0,
+
+    )
+
+    h2 = styles.ParagraphStyle('Heading2', samplestyles['h2'],
+            fontName        = 'Times-Bold',
+            fontSize        = 14,
+            spaceBefore     = 6,
+            spaceAfter      = 0,
+    )
+    
+    h3 = styles.ParagraphStyle('Heading3', samplestyles['h3'],
+            fontName        = 'Times-Italic',
+            fontSize        = 12,
+            spaceBefore     = 0,
+            spaceAfter      = 0,
+    )
+
+    text = styles.ParagraphStyle('Text', samplestyles['Normal'],
+
+    )
+
+    # list indent level
+    list_indent = inch / 4
+
+    # root title
+    list_h1 = styles.ParagraphStyle('ListHeading1', samplestyles['h1'],
+            bulletIndent    = 0,
+            leftIndent      = 0,
+    )
+
+    # section
+    list_h2 = styles.ParagraphStyle('ListHeading2', samplestyles['h2'],
+            bulletIndent    = 0,
+            leftIndent      = list_indent,
+            fontName        = 'Times-Bold',
+            fontSize        = 10,
+            leading         = 12,
+            spaceBefore     = 6,
+            spaceAfter      = 0,
+    )
+    
+    # segment
+    list_h3 = styles.ParagraphStyle('ListHeading3', samplestyles['h3'],
+            bulletIndent    = 0,
+            leftIndent      = list_indent,
+            fontName        = 'Times-Italic',
+            fontSize        = 10,
+            leading         = 12,
+            spaceBefore     = 0,
+            spaceAfter      = 0,
+    )
+    
+    # infot
+    list_text = styles.ParagraphStyle('ListText', samplestyles['Normal'],
+            bulletIndent    = 0,
+            leftIndent      = list_indent,
+    )
+
+class ListItem (object) :
+    """
+        Indented/nested list
+    """
+
+    @classmethod
+    def seq (cls) :
+        """
+            List numbering.
+
+            Fixed as numeric only for now
+        """
+        
+        for idx in itertools.count(1) :
+            yield "%d." % (idx, )
+
+
+    def __init__ (self, title, title_style, text, subseq=None, sublist=None, 
+            text_style=Styles.list_text, indent=Styles.list_indent) :
+        """
+                title       - title to display as first line
+                title_style - paragraph style for title line
+                text        - multi-line texto to display on first or previous lines
+                subseq      - sequence of bullet-texts to use for items in sublist
+                sublist     - sequence of sub-nodes
+
+                text_style  - paragraph style for text lines
+                indent      - indentation for text and sub-lists
+        """
+
+        self.title = title
+        self.title_style = title_style
+        self.text = text
+
+        self.subseq = subseq
+        self.sublist = sublist
+
+        self.text_style = text_style
+        self.indent = indent
+
+    def render_pdf (self, bullet=None) :
+        """
+            Yield a series of PDF flowables for this list node and sub-nodes, useable for pdf.DocTemplateBase.build()
+
+                bullet      - bullet text to use for this item's paragraph
+        """
+
+        # first line, with possible bullet
+        if self.title :
+            yield rlpp.Paragraph(self.title, self.title_style, bullet)
+
+        elif self.text :
+            yield rlpp.Paragraph(self.text, self.text_style, bullet)
+
+        # indented text after title
+        if self.title and self.text :
+            yield rlpp.Paragraph(self.text, self.text_style)
+
+        if self.sublist :
+            # following lines, indented
+            yield rlpp.Indenter(self.indent)
+
+            # sub-items
+            for item in self.sublist :
+                # get (optional) bullet for item
+                bullet = next(self.subseq, None) if self.subseq else None
+
+                # render item as series of elements
+                for element in item.render_pdf(bullet) :
+                    yield element
+
+            # de-dent
+            yield rlpp.Indenter(-self.indent)
+
+    
+
+class SignatureBlock (rlpp.Flowable) :
+    """
+        A signature block, with multiple sets of multiple pre-fillied fields.
+    """
+
+    # vertical space per field
+    FIELD_HEIGHT = 2 * inch / 4
+
+    # horizontal offset from top of field to field line
+    FIELD_OFFSET = FIELD_HEIGHT / 2
+    
+    # maximum width to scale columns to
+    COL_WIDTH_MAX = 4 * inch
+    
+    # empty space to leave below the fields
+    PADDING_BOTTOM = inch / 2
+
+    def __init__ (self, cols, fields, values) :
+        """
+            cols    - Column titles
+            fields  - Fields titles, describing the horizontal fields
+            values  - Pre-filled values as a {(col, field): value} dict
+
+            desc/value strings can contain formatting codes:
+
+                column      - title of the current column
+                today       - today's date in %d/%m/%Y format
+        """
+
+        self.cols = cols
+        self.fields = fields
+        self.values = values
+
+    def wrap (self, width, height) :
+        """
+            Calculate how much space we use up, returning (width, height)
+        """
+
+        self.width = width
+
+        # consume all available height, to place us at the bottom
+        self.height = max(len(self.fields) * self.FIELD_HEIGHT, height)
+
+        return self.width, self.height
+    
+    def formatString (self, text, col_title) :
+        """
+            Format display string using context parameters
+        """
+
+        return text % dict(
+                column      = col_title,
+                today       = datetime.date.today().strftime("%d/%m/%Y"),
+        )
+    
+    # text above field line
+    VALUE_FONT = "Courier-Bold"
+    VALUE_FONT_SIZE = 14
+    VALUE_OFFSET = inch / 12
+
+    # text below field line
+    TITLE_FONT = "Times-Italic"
+    TITLE_FONT_SIZE = 10
+    TITLE_OFFSET = inch / 8
+
+    def draw (self) :
+        """
+            Render full block onto our canvas
+        """
+
+        # target canvas
+        canvas = self.canv
+
+        col_width = min(self.width / len(self.cols), self.COL_WIDTH_MAX)
+        col_margin = col_width * 0.1
+        col_height = len(self.fields) * self.FIELD_HEIGHT + self.PADDING_BOTTOM
+
+        for field_idx, (field_title) in enumerate(self.fields) :
+            h = self.FIELD_HEIGHT
+            y = col_height - h * (field_idx + 1)
+
+            for col_idx, (col_title) in enumerate(self.cols) :
+                w = col_width
+                x = w * col_idx
+                value = self.values.get((col_title, field_title))
+                title = field_title
+
+                value = self.formatString(value, col_title) if value else None
+                title = self.formatString(title, col_title) if title else None
+
+                if value :
+                    canvas.setFont(self.VALUE_FONT, self.VALUE_FONT_SIZE)
+                    canvas.drawString(x + col_margin + self.VALUE_OFFSET, y - self.FIELD_OFFSET + 2, value)
+                
+                # field line
+                canvas.line(x + col_margin, y - self.FIELD_OFFSET, x + w - col_margin, y - h / 2)
+
+                # desc text
+                canvas.setFont(self.TITLE_FONT, self.TITLE_FONT_SIZE)
+                canvas.drawString(x + col_margin + self.TITLE_OFFSET, y - self.FIELD_OFFSET - self.TITLE_FONT_SIZE, title)
+
+class PageTemplate (rlpp.PageTemplate) :
+    """
+        A single-frame page with header and footer.
+    """
+
+    # vertical space available for footer/header, fixed because we can't really flow text vertically
+    HEADER_HEIGHT = 1 * inch
+    FOOTER_HEIGHT = 1 * inch
+
+    COL_FONT_SIZE = 8
+    COL_TITLE_FONT_NAME, COL_TITLE_FONT_SIZE = COL_TITLE_FONT = ("Times-Bold", COL_FONT_SIZE)
+    COL_TEXT_FONT_NAME, COL_TEXT_FONT_SIZE = COL_TEXT_FONT = ("Times-Roman", COL_FONT_SIZE)
+
+
+    def __init__ (self, id='page', page_size=pagesizes.A4, margin=inch, header_columns=None, footer_columns=None) :
+        """
+                id          - identifier for this page template
+                page_size   - the (width, height) of this page
+                margin      - the base margin to use between elements
+
+                header_columns  - (title, text) list of header columns
+                footer_columnss - (title, text) list of footer columns
+        """
+        
+        self.page_width, self.page_height = self.page_size = page_size
+        self.margin = margin
+
+        self.header_height = self.HEADER_HEIGHT
+        self.footer_height = self.FOOTER_HEIGHT
+
+        # calculate frame
+        self.frame_left = self.margin
+        self.frame_bottom = self.footer_height + self.margin / 2
+        self.frame_top = self.page_height - self.header_height - self.margin / 2
+        self.frame_right = self.page_width - self.margin
+
+        self.frame = rlpp.Frame(self.frame_left, self.frame_bottom, self.frame_right - self.frame_left, self.frame_top - self.frame_bottom)
+        
+        # init base template
+        rlpp.PageTemplate.__init__(self, id, frames=[self.frame])
+
+        self.header_columns = header_columns
+        self.footer_columns = footer_columns
+
+
+    def fmt_string (self, text) :
+        """
+            Prepare a string for display by handling format codes
+        """
+
+        # XXX: unicode?
+        return str(text % dict(
+            today = datetime.date.today().strftime("%d / %m / %Y"),
+        ))
+
+
+    def draw_column (self, canvas, x, y, width, title, lines, gray=None) :
+        """
+            Draw a column in the specified position, with the specified lines of text
+        """
+
+        text = canvas.beginText(x, y)
+
+        # grayscale text?
+        if gray :
+            text.setFillGray(gray)
+
+        # title
+        text.setFont(*self.COL_TITLE_FONT)
+        text.textLine(self.fmt_string(title))
+
+        # lines
+        text.setFont(*self.COL_TEXT_FONT)
+        text.textLines(self.fmt_string(lines))
+        
+        # draw out
+        canvas.drawText(text)
+
+
+    def draw_columns (self, canvas, x, y, width, columns, **opts) :
+        """
+            Draw a series of columns in the specified position
+        """
+
+        col_count = len(columns)
+        col_width = width / col_count
+
+        x_base = x
+
+        for col_idx, (col_data) in enumerate(columns) :
+            x = x_base + (col_idx * col_width)
+
+            # draw column data at correct offset inside our space
+            self.draw_column(canvas, x, y - self.COL_FONT_SIZE, col_width, *col_data, **opts)
+
+
+    def draw_header (self, canvas) :
+        """
+            Draw page header
+        """
+        
+        # offsets
+        x = self.margin
+        h = self.footer_height - self.margin / 4
+        w = self.page_width - self.margin * 2
+
+        # spacer
+        y = self.page_height - self.footer_height
+
+        canvas.setLineWidth(0.5)
+        canvas.line(x - self.margin / 2, y, self.page_width - self.margin / 2, y)
+
+        # columns
+        y = self.page_height - self.margin / 4
+
+        self.draw_columns(canvas, x, y, w, self.header_columns)
+
+
+    def draw_footer (self, canvas) :
+        """
+            Draw page footer
+        """
+        
+        # offsets
+        x = self.margin
+        y = self.footer_height
+        w = self.page_width - self.margin * 2
+
+        # spacer
+        canvas.setLineWidth(0.5)
+        canvas.line(x - self.margin / 2, y, self.page_width - self.margin / 2, y)
+
+        # columns
+        self.draw_columns(canvas, x, y - inch / 8, w, self.footer_columns, gray=0.4)
+
+
+    def beforeDrawPage (self, canvas, document) :
+        """
+            Draw page headers/footers
+        """
+
+        self.draw_header(canvas)
+        self.draw_footer(canvas)
+
+class DocumentTemplate (rlpp.BaseDocTemplate) :
+    def __init__ (self, page_templates, title, author, page_size=pagesizes.A4) :
+        """
+            Initialize with fixed list of needed PageTemplates.
+        """
+
+        # we supply the file later
+        rlpp.BaseDocTemplate.__init__(self, filename=None, 
+                pageTemplates=page_templates, title=title, author=author,
+                pageSize=page_size
+        )
+
+    def render_string (self, elements) :
+        """
+            Build the document using the given list of Flowables, returning the PDF as a single str.
+        """
+
+        buf = StringIO()
+
+        # build
+        self.build(elements, buf)
+
+        # binary data out
+        return buf.getvalue()
+
--- a/svv/wsgi.py	Mon Dec 20 20:51:45 2010 +0200
+++ b/svv/wsgi.py	Mon Dec 20 23:15:08 2010 +0200
@@ -4,6 +4,8 @@
     WSGI frontend/entry point
 """
 
+from svv import pdf
+
 import werkzeug
 from werkzeug import exceptions
 from werkzeug import Request, Response
@@ -106,304 +108,62 @@
     """
 
     def respond (self, url_values) :
-        from reportlab import platypus as rlpp
-        from reportlab.lib.units import inch
-        from reportlab.lib import pagesizes, styles
-
-        from cStringIO import StringIO
-        import itertools
-
-        buf = StringIO()
-
-        # dimensions
-        page_width, page_height = page_size = pagesizes.A4
-        header_height = 1 * inch
-        footer_height = 1 * inch
-
-        margin = inch
-        frame_width = page_width - 2 * margin
-        frame_height = page_height - footer_height - margin / 2 - margin / 2 - header_height
-        frame_bottom = footer_height + margin / 2
-        frame_left = margin
-        frame = rlpp.Frame(frame_left, frame_bottom, frame_width, frame_height)
 
         title = url_values.get('name', "Hello World")
 
-        def draw_page_tpl (canvas, document) :
-            cols = 4
-            col_width = (page_width - 2 * margin) / 4 
-            
-            def draw_column (x, y, title, lines, gray=None) :
-                text = canvas.beginText(x, y)
-                
-                if gray:
-                    text.setFillGray(gray)
-                
-                # title + rest
-                text.setFont("Times-Bold", 8)
-                text.textLine(title)
-                text.setFont("Times-Roman", 8)
-                text.textLines(lines)
-
-                canvas.drawText(text)
-
-
-            # 
-            # header
-            #
-            x = margin
-            y = page_height - margin / 4
-
-            # spacer
-            y = page_height - footer_height
-
-            canvas.setLineWidth(0.5)
-            canvas.line(x - margin / 2, y, page_width - margin / 2, y)
- 
-            # columns
-            x = margin + 3 * col_width
-            y = page_height - margin / 4
-
-            today = datetime.date.today()
-
-            draw_column(x, y - inch / 6, "Vuokrasopimus", str("%(title)s\n%(today)s" % dict(
-                today = today.strftime("%d / %m / %Y"),
-                title = title
-            ))), 
-           
-           
-            #
-            # footer
-            #
-            x = margin
-            y = footer_height
-
-            # spacer
-            canvas.setLineWidth(0.5)
-            canvas.line(x - margin / 2, y, page_width - margin / 2, y)
-
-            # column
-            draw_column(x, y - inch / 6, "Teekkarispeksi Ry", "www.teekkarispeksi.fi", 0.4)
-            x += col_width
-
-            draw_column(x, y - inch / 6, "Tekniikkavastaava", "Juha Kallas\n045 xxx yyzz\njskallas@cc.hut.fi", 0.4)
-            x += col_width
-
-            draw_column(x, y - inch / 6, "Varastovastaava", "Joel Pirttimaa\n045 xxx yyzz\njhpirtti@cc.hut.fi", 0.4)
-            x += col_width
-
-        tpl = rlpp.PageTemplate('page', [frame], onPage=draw_page_tpl, pagesize=page_size)
-        doc = rlpp.BaseDocTemplate(buf, pageTemplates=[tpl], title=title, author="Test Author", showBoundary=False, pageSize=page_size)
-
-        samplestyles = styles.getSampleStyleSheet()
- 
-        # normal text
-        h1 = styles.ParagraphStyle('Heading1', samplestyles['h1'],
-                fontName        = 'Times-Bold',
-                fontSize        = 22,
-                spaceBefore     = 0,
-                spaceAfter      = 0,
-
+        tpl = pdf.PageTemplate('id', 
+            header_columns  = (
+                ("", ""),
+                ("", ""),
+                ("", ""),
+                ("Vuokrasopimus", "%(today)s\n" + title + "\n"),
+            ),
+            footer_columns  = (
+                ("Teekkarispeksi Ry", "www.teekkarispeksi.fi"),
+                ("Tekniikkavastaava", "Juha Kallas\n045 xxx yyzz\njskallas@cc.hut.fi"),
+                ("Varastovastaava", "Joel Pirttimaa\n045 xxx yyzz\njhpirtti@cc.hut.fi"),
+                ("", ""),
+            ),
         )
 
-        h2 = styles.ParagraphStyle('Heading2', samplestyles['h2'],
-                fontName        = 'Times-Bold',
-                fontSize        = 14,
-                spaceBefore     = 6,
-                spaceAfter      = 0,
-        )
-        
-        h3 = styles.ParagraphStyle('Heading3', samplestyles['h3'],
-                fontName        = 'Times-Italic',
-                fontSize        = 12,
-                spaceBefore     = 0,
-                spaceAfter      = 0,
-        )
-
-        text = styles.ParagraphStyle('Text', samplestyles['Normal'],
-
-        )
-        
-        # standard indent
-        list_indent = inch / 4
-
-        # root title
-        list_h1 = styles.ParagraphStyle('ListHeading1', samplestyles['h1'],
-                bulletIndent    = 0,
-                leftIndent      = 0,
+        doc = pdf.DocumentTemplate([tpl],
+            title = title, author = "A. N. Onous"
         )
 
-        # section
-        list_h2 = styles.ParagraphStyle('ListHeading2', samplestyles['h2'],
-                bulletIndent    = 0,
-                leftIndent      = list_indent,
-                fontName        = 'Times-Bold',
-                fontSize        = 10,
-                leading         = 12,
-                spaceBefore     = 6,
-                spaceAfter      = 0,
-        )
-        
-        # segment
-        list_h3 = styles.ParagraphStyle('ListHeading3', samplestyles['h3'],
-                bulletIndent    = 0,
-                leftIndent      = list_indent,
-                fontName        = 'Times-Italic',
-                fontSize        = 10,
-                leading         = 12,
-                spaceBefore     = 0,
-                spaceAfter      = 0,
-        )
-
-        list_text = styles.ParagraphStyle('ListText', samplestyles['Normal'],
-                bulletIndent    = 0,
-                leftIndent      = list_indent,
-        )
-
-        def tree_seq () :
-            """
-                Tree list numbering
-            """
-            
-            for idx in itertools.count(1) :
-                yield "%d." % (idx, )
+        # stylesheet
+        styles = pdf.Styles
 
         # tree root
-        tree = ("Sopimusehdot", h2, None, tree_seq(), [
-            ("Osapuolet", list_h2, None, tree_seq(), [
-                (None, None, "Teekkarispeksi ry (Y-tunnus 1888541-7), jäljempänä “Vuokranantaja”."),
-                (None, None, title + u", jäljempänä “Vuokraaja”. 1.1 ja 1.2 jäljempänä yhdessä “osapuolet”.")
+        list_seq = pdf.ListItem.seq
+        tree = pdf.ListItem("Sopimusehdot", styles.h2, None, list_seq(), [
+            pdf.ListItem("Osapuolet", styles.list_h2, None, list_seq(), [
+                pdf.ListItem(None, None, "Teekkarispeksi ry (Y-tunnus 1888541-7), jäljempänä “Vuokranantaja”."),
+                pdf.ListItem(None, None, title + u", jäljempänä “Vuokraaja”. 1.1 ja 1.2 jäljempänä yhdessä “osapuolet”.")
             ]),
-            ("Kaluston lainaaminen", list_h2, None, tree_seq(), [
-                ("Yleistä", list_h3, "Tässä sopimuksessa sovitaan toiminnasta Vuokranantajan lainatessa tanssimattoja Vuokraajalle"),
-                ("Vuokranantajan velvollisuudet", list_h3, "Vuokranantaja sitoutuu toimittamaan sovittuna ajankohtana Vuokraajalle erikseen sovittava (liite) määrä tanssimattoja."),
-                ("Blaa Blaa", list_h3, "Etc."),
+            pdf.ListItem("Kaluston lainaaminen", styles.list_h2, None, list_seq(), [
+                pdf.ListItem("Yleistä", styles.list_h3, "Tässä sopimuksessa sovitaan toiminnasta Vuokranantajan lainatessa tanssimattoja Vuokraajalle"),
+                pdf.ListItem("Vuokranantajan velvollisuudet", styles.list_h3, "Vuokranantaja sitoutuu toimittamaan sovittuna ajankohtana Vuokraajalle erikseen sovittava (liite) määrä tanssimattoja."),
+                pdf.ListItem("Blaa Blaa", styles.list_h3, "Etc."),
             ]),
-            ("Tätä sopimusta ja sitä koskevia erimielisyyksiä koskevat määräykset", list_h2, None, tree_seq(), [
-                ("Sopimuksen voimassaolo", list_h3, "Sopimus on osapuolia sitova sen jälkeen, kun osapuolet ovat sen allekirjoittaneet."),
-                ("Muutosten tekeminen", list_h3, "Muutokset sopimukseen on tehtävä kirjallisesti molempien osapuolten kesken."),
-                ("Blaa Blaa", list_h3, "Etc."),
+            pdf.ListItem("Tätä sopimusta ja sitä koskevia erimielisyyksiä koskevat määräykset", styles.list_h2, None, list_seq(), [
+                pdf.ListItem("Sopimuksen voimassaolo", styles.list_h3, "Sopimus on osapuolia sitova sen jälkeen, kun osapuolet ovat sen allekirjoittaneet."),
+                pdf.ListItem("Muutosten tekeminen", styles.list_h3, "Muutokset sopimukseen on tehtävä kirjallisesti molempien osapuolten kesken."),
+                pdf.ListItem("Blaa Blaa", styles.list_h3, "Etc."),
             ]),
         ])
 
-        def fmt_tree (bullet, title, title_style, text, seq=None, sublist=[]) :
-            log.debug("bullet(%r) = %r", title, bullet)
-            
-            # fixed
-            text_style = list_text
-
-            # first line, with possible bullet
-            if title :
-                yield rlpp.Paragraph(title, title_style, bullet)
-
-            elif text :
-                yield rlpp.Paragraph(text, text_style, bullet)
-
-            if title and text :
-                # indented text after title
-                yield rlpp.Paragraph(text, text_style)
-
-            if sublist :
-                # following lines, indented
-                yield rlpp.Indenter(list_indent)
-
-                # sub-items
-                for item in sublist :
-                    # get bullet for item
-                    bullet = next(seq, None) if seq else None
-
-                    for element in fmt_tree(bullet, *item) :
-                        yield element
-
-                # de-dent
-                yield rlpp.Indenter(-list_indent)
-
-        class SignatureBlock (rlpp.Flowable) :
-
-            # vertical space per field
-            FIELD_HEIGHT = 2 * inch / 4
-
-            COL_WIDTH_MAX = 4 * inch
-
-            PADDING_BOTTOM = inch / 2
-
-            def __init__ (self, cols, fields, values) :
-                """
-                    cols    - Column titles
-                    fields  - Fields titles, describing the horizontal fields
-                    values  - Pre-filled values as a {(col, field): value} dict
-
-                    desc/value strings can contain formatting codes:
-
-                        column      - title of the current column
-                        today       - today's date in %d/%m/%Y format
-                """
-
-                self.cols = cols
-                self.fields = fields
-                self.values = values
-
-            def wrap (self, width, height) :
-                """
-                    Calculate how much space we use up, returning (width, height)
-                """
-
-                self.width = width
-
-                # consume all available height, to place us at the bottom
-                self.height = max(len(self.fields) * self.FIELD_HEIGHT, height)
-
-                return self.width, self.height
-            
-            def formatString (self, text, col_title) :
-                return text % dict(
-                        column      = col_title,
-                        today       = datetime.date.today().strftime("%d/%m/%Y"),
-                )
-
-            def draw (self) :
-                # target canvas
-                canvas = self.canv
-
-                col_width = min(self.width / len(self.cols), self.COL_WIDTH_MAX)
-                col_margin = col_width * 0.1
-                col_height = len(self.fields) * self.FIELD_HEIGHT + self.PADDING_BOTTOM
-
-                for field_idx, (field_title) in enumerate(self.fields) :
-                    h = self.FIELD_HEIGHT
-                    y = col_height - h * (field_idx + 1)
-
-                    for col_idx, (col_title) in enumerate(self.cols) :
-                        w = col_width
-                        x = w * col_idx
-                        value = self.values.get((col_title, field_title))
-                        title = field_title
-
-                        value = self.formatString(value, col_title) if value else None
-                        title = self.formatString(title, col_title) if title else None
-
-                        if value :
-                            canvas.setFont("Courier-Bold", 14)
-                            canvas.drawString(x + col_margin + inch / 12, y - h / 2 + 2, value)
-                        
-                        # field line
-                        canvas.line(x + col_margin, y - h / 2, x + w - col_margin, y - h / 2)
-
-                        # desc text
-                        canvas.setFont("Times-Italic", 10)
-                        canvas.drawString(x + col_margin + inch / 8, y - h / 2 - 10, title)
-
+        from reportlab.platypus import Paragraph as p
 
         elements = [
-                rlpp.Paragraph("Vuokrasopimus", h1),
-                rlpp.Paragraph("Teekkarispeksi ry AV-tekniikka", h3),
-        ] + list(fmt_tree(None, *tree)) + [
-                rlpp.Paragraph("Nouto", h2),
-                rlpp.Paragraph("\t\tAika: _______________\tPaikka: _______________", text),
-                rlpp.Paragraph("Palautus", h2),
-                rlpp.Paragraph("\t\tAika: _______________\tPaikka: _______________", text),
+                p("Vuokrasopimus", styles.h1),
+                p("Teekkarispeksi ry AV-tekniikka", styles.h3),
+        ] + list(tree.render_pdf()) + [
+                p("Nouto", styles.h2),
+                p("\t\tAika: _______________\tPaikka: _______________", styles.text),
+                p("Palautus", styles.h2),
+                p("\t\tAika: _______________\tPaikka: _______________", styles.text),
                 
-                SignatureBlock(("Vuokranantaja", "Vuokraaja"), ("%(column)s", "Nimen selvennys", "Aika ja paikka"), {
+                pdf.SignatureBlock(("Vuokranantaja", "Vuokraaja"), ("%(column)s", "Nimen selvennys", "Aika ja paikka"), {
                     ('Vuokranantaja', 'Nimen selvennys'): "Joel Pirttimaa",
                     ('Vuokranantaja', 'Aika ja paikka'): 'Otaniemi, %(today)s',
                     ('Vuokraaja', 'Aika ja paikka'): 'Otaniemi, %(today)s',
@@ -411,9 +171,9 @@
         ]
        
         # render elements to buf as PDF code
-        doc.build(elements)
+        pdf_code = doc.render_string(elements)
 
-        return Response(buf.getvalue(), mimetype='application/pdf')
+        return Response(pdf_code, mimetype='application/pdf')
 
 class Index (PageHandler) :
     DATA = (