Index: main/waeup.kofa/trunk/src/waeup/kofa/browser/pdf.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/browser/pdf.py	(revision 9962)
+++ main/waeup.kofa/trunk/src/waeup/kofa/browser/pdf.py	(revision 9963)
@@ -21,6 +21,8 @@
 import grok
 import os
+import pytz
 from cStringIO import StringIO
 from datetime import datetime
+from reportlab.lib import colors
 from reportlab.lib.units import cm, inch, mm
 from reportlab.lib.pagesizes import A4, landscape, portrait
@@ -32,5 +34,5 @@
 from zope.i18n import translate
 from zope.publisher.browser import TestRequest
-from zope.component import getUtility
+from zope.component import getUtility, queryUtility
 from waeup.kofa.browser.interfaces import IPDFCreator
 from waeup.kofa.utils.helpers import now
@@ -90,4 +92,13 @@
     fontSize=10,
     )
+
+#: Base style for signature tables
+SIGNATURE_TABLE_STYLE = [
+    ('VALIGN',(0,-1),(-1,-1),'TOP'),
+    #('FONT', (0,0), (-1,-1), 'Helvetica-BoldOblique', 12),
+    ('BOTTOMPADDING', (0,0), (-1,0), 36),
+    ('TOPPADDING', (0,-1), (-1,-1), 0),
+    ]
+
 
 def format_html(html):
@@ -109,4 +120,306 @@
         html = html.replace('\n', '<br />')
     return html
+
+def normalize_signature(signature_tuple):
+    """Normalize a signature tuple.
+
+    Returns a tuple ``(<PRE-TEXT>, <SIGNATURE>, <POST-TEXT>)`` from
+    input tuple. The following rules apply::
+
+      (pre, sig, post)  --> (pre, sig, post)
+      (pre, sig)        --> (pre, sig, None)
+      (sig)             --> (None, sig, None)
+
+    Also simple strings are accepted as input::
+
+      sig               --> (None, sig, None)
+
+    If input is not a tuple nor a basestring or if the tuple contains
+    an invalid number of elements, ``ValueError`` is raised.
+    """
+    if isinstance(signature_tuple, basestring):
+        return (None, signature_tuple, None)
+    if not isinstance(signature_tuple, tuple):
+        raise ValueError("signature_tuple must be a string or tuple")
+    if len(signature_tuple) < 1 or len(signature_tuple) > 3:
+        raise ValueError("signature_tuple must have 1, 2, or 3 elements")
+    elif len(signature_tuple) == 1:
+        signature_tuple = (None, signature_tuple[0], None)
+    elif len(signature_tuple) == 2:
+        signature_tuple = (signature_tuple[0], signature_tuple[1], None)
+    return signature_tuple
+
+def vert_signature_cell(signature, date_field=True, date_text=_('Date:'),
+                        start_row=0, start_col=0, underline=True):
+    """Generate a table part containing a vertical signature cell.
+
+    Returns the table data as list of lists and an according style.
+
+    `signature`:
+       a signature tuple containing (<PRE-TEXT, SIGNATURE-TEXT, POST-TEXT>)
+
+    `date_field`:
+       boolean indicating that a 'Date:' text should be rendered into this
+       signature cell (or not).
+
+    `date_text`:
+       the text to be rendered into the signature field as 'Date:' text.
+
+    `start_row`:
+       starting row of the signature cell inside a broader table.
+
+    `start_col`:
+       starting column of the signature cell inside a broader table.
+
+    `underline`:
+       boolean indicating that the signature cell should provide a line on
+       top (`True` by default).
+
+    Vertical signature cells look like this::
+
+      +------------+
+      |Pre         |
+      +------------+
+      |Date:       |
+      |            |
+      +------------+
+      | ---------- |
+      | Signature  |
+      +------------+
+      |Post        |
+      +------------+
+    """
+    # split signature parts, replacing None with empty string
+    pre, sig, post = [x or '' for x in signature]
+    style = ()
+    x, y = start_col, start_row+2
+    if underline:
+        style += (('LINEABOVE', (x, y), (x, y), 1, colors.black),)
+    d_text = date_field and date_text or ''
+    data = [[pre], [d_text], [sig], [post]]
+    col_widths = [1.0]
+    return data, style, col_widths
+
+def horiz_signature_cell(signature, date_field=True, date_text=_('Date'),
+                         start_row=0, start_col=0):
+    """Generate a table part containing an horizontal signature cell
+
+    Returns the table data as list of lists and an according style.
+
+    `signature`:
+       a signature tuple containing (<PRE-TEXT, SIGNATURE-TEXT, POST-TEXT>)
+
+    `date_field`:
+       boolean indicating that a 'Date:' text should be rendered into this
+       signature cell (or not).
+
+    `date_text`:
+       the text to be rendered into the signature field as 'Date:' text.
+
+    `start_row`:
+       starting row of the signature cell inside a broader table.
+
+    `start_col`:
+       starting column of the signature cell inside a broader table.
+
+    Horizontal signature cells look like this::
+
+      +------------+---+-----------+
+      |Pre text possibly filling   |
+      |the whole box               |
+      +------------+---+-----------+
+      |            |   |           |
+      |            |   |           |
+      +------------+---+-----------+
+      | ---------- |   | --------- |
+      | Date       |   | Signature |
+      +------------+---+-----------+
+      |Post                        |
+      +------------+---+-----------+
+
+    """
+    pre, sig, post = signature
+    if not date_field:
+        data, style, cols = vert_signature_cell(signature, date_field=False)
+        return data, style, cols
+    style = (
+        # let pre and post text span the whole signature cell
+        ('SPAN', (start_col, start_row), (start_col+2, start_row)),
+        ('SPAN', (start_col, start_row+3), (start_col+2, start_row+3)),
+        )
+    # horizontal cells are buildt from vertical ones chained together
+    cell1 = vert_signature_cell(  # leftmost date col
+        (pre, date_text, post), date_field=False,
+        start_row=start_row, start_col=start_col)
+    cell2 = vert_signature_cell(  # spacer col (between date and sig)
+        ('', '', ''), date_field=False, underline=False,
+        start_row=start_row, start_col=start_col+1)
+    cell3 = vert_signature_cell(  # rightmost signature column
+        ('', sig, ''), date_field=False,
+        start_row=start_row, start_col=start_col+2)
+    data = map(lambda x, y, z: x+y+z, cell1[0], cell2[0], cell3[0])
+    style = style + cell1[1] + cell2[1] + cell3[1]
+    col_widths  = [0.3, 0.03, 0.67] # sums up to 1.0
+    return data, style, col_widths
+
+def signature_row(signatures, start_row=0, horizontal=None, max_per_row=3):
+    data = [[], [], [], []]
+    style = ()
+    signatures = [normalize_signature(sig) for sig in signatures]
+    start_col = 0
+    col_widths = []
+
+    if horizontal is None:
+        horizontal = len(signatures) == 1
+    cell_maker = vert_signature_cell
+    if horizontal:
+        cell_maker = horiz_signature_cell
+    main_cell_height = not horizontal and 36 or 18
+
+    for sig in signatures:
+        sig_data, sig_style, sig_cols = cell_maker(
+            sig, start_row=start_row, start_col=start_col)
+        data = map(lambda x, y: x+y, data, sig_data)
+        style += sig_style
+        col_widths += sig_cols + [None,]
+
+        start_col += 1
+        # add spacer
+        spacer, spacer_style, cols = vert_signature_cell(
+            ('', '', ''), date_field=False, underline=False,
+            start_row=start_row, start_col=start_col)
+        data = map(lambda x, y: x+y, data, spacer)
+        style += spacer_style
+        start_col += 1
+
+    y = start_row
+    sig_row = start_row + 2
+    style = style + (
+        ('TOPPADDING', (0, y+2), (-1, y+2), 0), # signature row
+        ('BOTTOMPADDING', (0, y+1), (-1, y+1), main_cell_height),
+        ('LEFTPADDING', (0, y), (-1, y), 1), # pre row
+        ('LEFTPADDING', (0, y+3), (-1, y+3), 1), # post row
+        )
+
+    if len(signatures) == 1:
+        # pre and post text should span whole table
+        style += (('SPAN', (0, y), (-1, y)),
+                  ('SPAN', (0, y+3), (-1, y+3)),
+                  )
+
+    if data[0] == [''] * len(data[0]):
+        # no pre text: hide pre row by minimizing padding
+        style += (('TOPPADDING', (0,y), (-1, y), -6),
+                  ('BOTTOMPADDING', (0,y), (-1, y), -6),
+                  )
+    if data[-1] == [''] * len(data[0]):
+        # no post text: hide post row by minimizing padding
+        style += (('TOPPADDING', (0,y+3), (-1, y+3), -6),
+                  ('BOTTOMPADDING', (0,y+3), (-1, y+3), -6),
+                  )
+
+    if len(signatures) > 1:
+        data = [x[:-1] for x in data] # strip last spacer
+        col_widths = col_widths[:-1]
+    return data, style, col_widths
+
+def sig_table(signatures, lang='en', max_per_row=3, horizontal=None,
+              single_table=False, start_row=0):
+    space_width = 0.4  # width in cm of space between signatures
+    table_width = 16.0 # supposed width of signature table in cms
+    # width of signature cells in cm...
+    sig_num = len(signatures)
+    sig_col_width = (table_width - ((sig_num - 1) * space_width)) / sig_num
+    if sig_num == 1:
+        sig_col_width = 0.6 * table_width         # signature cell part
+        space_width = table_width - sig_col_width # spacer part on the right
+
+    if sig_num > max_per_row:
+        sigs_by_row = [signatures[x:x+max_per_row] for x in range(
+            0, sig_num, max_per_row)]
+        result = []
+        curr_row = 0
+        for num, row_sigs in enumerate(sigs_by_row):
+            curr_row = 0
+            if single_table:
+                curr_row = num * 4
+            result.append(
+                sig_table(row_sigs, lang=lang, max_per_row=max_per_row,
+                          horizontal=False, start_row=curr_row)[0])
+        missing_num = len(result[-2][0][0]) - len(result[-1][0][0])
+        if missing_num:
+            # last row contained less cells, fix it...
+            result[-1] = ([x + [''] * missing_num for x in result[-1][0]],
+                          result[-1][1], result[-2][2])
+        return result
+
+    data, style, cols = signature_row(signatures, horizontal=horizontal,
+                                      start_row=start_row)
+    style += (('VALIGN', (0,0), (-1,-1), 'TOP'),)
+
+    # compute col widths...
+    col_widths = []
+    for col in cols:
+        if col is not None:
+            col = col * sig_col_width * cm
+        else:
+            col = space_width * cm
+        col_widths.append(col)
+
+    # replace strings by paragraphs and translate all contents
+    for rnum, row in enumerate(data):
+        for cnum, cell in enumerate(row):
+            if cell:
+                content = translate(cell, lang)
+                data[rnum][cnum] = Paragraph(content, NORMAL_STYLE)
+    return [(data, style, col_widths),]
+
+def get_sig_tables(signatures, lang='en', max_per_row=3, horizontal=None,
+                   single_table=False):
+    rows = sig_table(signatures, lang=lang, max_per_row=max_per_row,
+                     horizontal=horizontal, single_table=single_table)
+    if single_table:
+        result_data = []
+        result_style = ()
+        for row in rows:
+            data, style, col_widths = row
+            result_data += data
+            result_style += style
+        return [(result_data, result_style, col_widths),]
+    return rows
+
+def get_signature_tables(signatures, lang='en', max_per_row=3,
+                         horizontal=None, single_table=False):
+    """Get a list of reportlab flowables representing signature fields.
+    """
+    data_list = get_sig_tables(
+        signatures, lang=lang, max_per_row=max_per_row,
+        horizontal=horizontal, single_table=single_table)
+    return [Table(row_data, style=row_style, colWidths=row_col_widths,
+                  repeatRows=2)
+            for row_data, row_style, row_col_widths in data_list]
+
+def format_signatures(signatures, max_per_row=3, lang='en',
+                      single_table=False,
+                      date_field=True, date_text=_('Date'),
+                      base_style=SIGNATURE_TABLE_STYLE):
+    result = []
+    signature_tuples = [normalize_signature(sig) for sig in signatures]
+    for pre, sig, post in signature_tuples:
+        row = []
+        if pre is not None:
+            row.append([
+                translate(pre, lang), '', '', ''])
+        row.append([
+            translate(_('Date'), lang), '',
+            translate(sig, lang), ''
+            ])
+        if post is not None:
+            row.append([
+                translate(post, lang), '', '', ''])
+        result.append((row, base_style))
+    return result
+
+
 
 class NumberedCanvas(Canvas):
@@ -346,7 +659,11 @@
 
         # Header
+        site_config = None
+        site = grok.getSite()
+        if site is not None:
+            site_config = site.get('configuration', None)
         head_title = getattr(
             doc, 'kofa_headtitle', getattr(
-                grok.getSite()['configuration'], 'name',
+                site_config, 'name',
                 u'Sample University'))
         canvas.setFont("Helvetica-Bold", 18)
@@ -379,5 +696,6 @@
         canvas.setFont("Helvetica", 9)
         if not getattr(doc, 'kofa_nodate', False):
-            tz = getUtility(IKofaUtils).tzinfo
+            tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
+            #tz = getUtility(IKofaUtils).tzinfo
             today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
             canvas.drawString(2.2*cm, 0.5 * inch,
Index: main/waeup.kofa/trunk/src/waeup/kofa/browser/tests/test_pdf.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/browser/tests/test_pdf.py	(revision 9963)
+++ main/waeup.kofa/trunk/src/waeup/kofa/browser/tests/test_pdf.py	(revision 9963)
@@ -0,0 +1,403 @@
+import os
+import tempfile
+import unittest
+from reportlab.lib import colors
+from waeup.kofa.browser.pdf import (
+    format_html, normalize_signature, format_signatures,
+    vert_signature_cell, horiz_signature_cell, signature_row, get_sig_tables,
+    PDFCreator, Paragraph, Table, NORMAL_STYLE, sig_table,
+    get_signature_tables)
+
+DRAW_SAMPLE_GRID = True
+
+class HelperTest(unittest.TestCase):
+
+    def test_format_html(self):
+        # test format_html()
+        intext = 'Two\nLines'
+        self.assertEqual(format_html(intext), 'Two<br />Lines')
+        intext = '<div>Two</div>\nLines'
+        self.assertEqual(format_html(intext), '<div>Two</div><br />Lines')
+        intext = '<div>Two</div>Lines'
+        self.assertEqual(format_html(intext), '<div>Two</div><br />Lines')
+        return
+
+class SignatureNormalizerTests(unittest.TestCase):
+    # Tests for normalize_signature()
+
+    def test_invalid_input_type(self):
+        # we accept only strings and tuples
+        self.assertRaises(
+            # we don't accept number, for instance
+            ValueError, normalize_signature, 12)
+        return
+
+    def test_invalid_number_of_items(self):
+        # a signature tuple must contain 1, 2, or 3 elements
+        self.assertRaises(
+            # empty tuple
+            ValueError, normalize_signature, ())
+        self.assertRaises(
+            # overcrowded tuple
+            ValueError, normalize_signature, ('a', 'b', 'c', 'd'))
+        return
+
+    def test_simple_string(self):
+        # we accept a simple string
+        self.assertEqual(
+            normalize_signature('sig1'),
+            ((None, 'sig1', None)))
+        # also unicode is allowed
+        self.assertEqual(
+            normalize_signature(u'sig1'),
+            ((None, u'sig1', None)))
+
+    def test_one_item_tuple(self):
+        # we accept a tuple with a single signature string
+        self.assertEqual(
+            normalize_signature(('sig1',)),
+            ((None, 'sig1', None)))
+        return
+
+    def test_two_items_tuple(self):
+        # we accept a tuple with two items
+        self.assertEqual(
+            normalize_signature(('pre-text', 'sig1')),
+            (('pre-text', 'sig1', None)))
+        return
+
+    def test_three_items_tuple(self):
+        # we accept a tuple with three items, of course
+        self.assertEqual(
+            normalize_signature(('pre-text', 'sig1', 'post-text')),
+            (('pre-text', 'sig1', 'post-text')))
+        return
+
+class SignaturesFormatterTests(unittest.TestCase):
+
+    def test_single_signature(self):
+        # we can format a single signature (w/o pre or post blocks)
+        data, style = format_signatures(['My Signature'])[0]
+        self.assertEqual(
+            data, [['Date', '', 'My Signature', '']])
+        #self.assertEqual(style, )
+        return
+
+    def test_single_sig_with_pre_text(self):
+        # we can format a single signature (with pre-text)
+        data, style = format_signatures([('Pre-Text', 'My Sig', None)])[0]
+        self.assertEqual(
+            data,
+            [['Pre-Text', '', '', ''],
+             ['Date', '', 'My Sig', '']])
+        return
+
+    def test_single_sig_with_post_text(self):
+        # we can format a single signature (with post-text)
+        data, style = format_signatures([(None, 'My Sig', 'Post-Text')])[0]
+        self.assertEqual(
+            data,
+            [['Date', '', 'My Sig', ''],
+             ['Post-Text', '', '', '']])
+        return
+
+    def test_single_sig_with_pre_and_post_text(self):
+        # we can format a single signature (with pre- and post-text)
+        data, style = format_signatures([('Pre', 'My Sig', 'Post')])[0]
+        self.assertEqual(
+            data,
+            [['Pre', '', '', ''],
+             ['Date', '', 'My Sig', ''],
+             ['Post', '', '', '']])
+        return
+
+class SignatureCellFormatterTests(unittest.TestCase):
+
+    def test_vert_signature(self):
+        # we can create vertical signatures
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'))
+        self.assertEqual(
+            data,
+            [['Pre'], ['Date:'], ['Sig'], ['Post']])
+        self.assertEqual(
+            style,
+            (('LINEABOVE', (0, 2), (0, 2), 1, colors.black),)
+            )
+        return
+
+    def test_vert_signature_custom_row_and_col(self):
+        # custom row and col nums are respected
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'), start_row=5, start_col=3)
+        self.assertEqual(
+            style,
+            (('LINEABOVE', (3, 7), (3, 7), 1, colors.black),)
+            )
+        return
+
+    def test_vert_signature_no_underline(self):
+        # we can suppress the underlining of signature cell
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'), underline=False)
+        self.assertEqual(
+            style, ())
+        return
+
+    def test_vert_signature_custom_date(self):
+        # we can create vertical signatures with customized date text
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'), date_text='My Date')
+        self.assertEqual(
+            data,
+            [['Pre'], ['My Date'], ['Sig'], ['Post']])
+        return
+
+    def test_vert_signature_no_date(self):
+        # we can create vertical signatures without date
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'), date_field=False)
+        self.assertEqual(
+            data,
+            [['Pre'], [''], ['Sig'], ['Post']])
+        return
+
+    def test_vert_signature_no_pre(self):
+        # we can create vertical signatures w/o pre-text
+        data, style, cols = vert_signature_cell(
+            (None, 'Sig', 'Post'))
+        self.assertEqual(
+            data,
+            [[''], ['Date:'], ['Sig'], ['Post']])
+        return
+
+    def test_vert_signature_no_post(self):
+        # we can create vertical signatures w/o post-text
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', None))
+        self.assertEqual(
+            data,
+            [['Pre'], ['Date:'], ['Sig'], ['']])
+        return
+
+    def test_horiz_signature_cell(self):
+        # we can create horizontal signatures
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', 'Post'))
+        self.assertEqual(
+            data, [['Pre', '', ''],
+                   ['', '', ''],
+                   ['Date', '', 'Sig'],
+                   ['Post', '', '']])
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (0, 0), (2, 0)),
+                ('SPAN', (0, 3), (2, 3)),
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+             ))
+        return
+
+    def test_horiz_signature_cell_no_date(self):
+        # we can create horizontal signatures without date
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', 'Post'), date_field=False)
+        self.assertEqual(
+            data, [['Pre'], [''], ['Sig'], ['Post']])
+        self.assertEqual(
+            style, (('LINEABOVE', (0, 2), (0, 2), 1, colors.black),))
+        return
+
+    def test_horiz_signature_cell_custom_date(self):
+        # we can create horizontal signatures with custom date text
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', 'Post'), date_text='My Date')
+        self.assertEqual(
+            data, [['Pre', '', ''],
+                   ['', '', ''],
+                   ['My Date', '', 'Sig'],
+                   ['Post', '', '']])
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (0, 0), (2, 0)),
+                ('SPAN', (0, 3), (2, 3)),
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+                ))
+        return
+
+    def test_horiz_signature_cell_no_pre(self):
+        # we can create horizontal signatures without pre-text
+        data, style, cols = horiz_signature_cell(
+            (None, 'Sig', 'Post'))
+        self.assertEqual(
+            data, [['', '', ''],
+                   ['', '', ''],
+                   ['Date', '', 'Sig'],
+                   ['Post', '', '']])
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (0, 0), (2, 0)),
+                ('SPAN', (0, 3), (2, 3)),
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+                ))
+        return
+
+    def test_horiz_signature_cell_no_post(self):
+        # we can create horizontal signatures without post-text
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', None))
+        self.assertEqual(
+            data, [['Pre', '', ''],
+                   ['', '', ''],
+                   ['Date', '', 'Sig'],
+                   ['', '', '']])
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (0, 0), (2, 0)),
+                ('SPAN', (0, 3), (2, 3)),
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+                ))
+        return
+
+    def test_horiz_signature_cell_custom_row_and_col(self):
+        # custom col and row numbers are respected in styles
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', 'Post'), start_row=3, start_col=5)
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (5, 3), (7, 3)),
+                ('SPAN', (5, 6), (7, 6)),
+                ('LINEABOVE', (5, 5), (5, 5), 1, colors.black),
+                ('LINEABOVE', (7, 5), (7, 5), 1, colors.black),
+                ))
+        return
+
+class SignatureRowFormatterTests(unittest.TestCase):
+
+    def test_vertical_row(self):
+        data, style, cols = signature_row(
+            ['Sig 1', 'Sig 2',])
+        self.assertEqual(
+            data, [['', '', ''], ['Date:', '', 'Date:'],
+                   ['Sig 1', '', 'Sig 2'], ['', '', '']]
+            )
+        self.assertEqual(
+            style, (
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+                )
+            )
+        return
+
+def samples_dir():
+    # create a directory for sample sheets, if it doesn't exist,
+    # and return its path.
+    tmp_dir = os.path.join(
+        tempfile.gettempdir(), 'waeup.kofa.pdf-samples')
+    if not os.path.exists(tmp_dir):
+        os.mkdir(tmp_dir)
+    return tmp_dir
+
+def draw_sample_pdf(data, name='sample1.pdf',
+                    title='Sample Title', author='Test Author'):
+    # create a pdf document with data as content
+    #data = [Paragraph('<b>Sample</b>', NORMAL_STYLE)]
+    creator = PDFCreator()
+    pdf = creator.create_pdf(
+        data, title=title, author=author)
+    path = os.path.join(samples_dir(), name)
+    open(path, 'wb').write(pdf)
+    return path
+
+
+class SampleSheet(unittest.TestCase):
+
+    def add_tables(self, data, title, tables, grid=DRAW_SAMPLE_GRID):
+        # add tables to `data`
+        mod_tables = []
+        data += [Paragraph(title, NORMAL_STYLE),]
+        for table in tables:
+            if grid:
+                table.setStyle((
+                    ('INNERGRID',(0,0),(-1,-1), 0.25, colors.blue),
+                    ('BOX', (0,0), (-1, -1), 0.25, colors.blue)),)
+            data += [table,]
+        return data
+
+
+    def test_draw_sample(self):
+        # create a sample sheet as PDF
+        # single signature
+        data = [
+            Paragraph('Signature Samples in here are created by '
+                      'calling the '
+                      '<font face="Times">get_signature_tables()</font> '
+                      'function.<br />'
+                      'Grids are drawn for better visibility of '
+                      'internal table structure only. You can set '
+                      'DRAW_SAMPLE_GRID value in browser/tests/test_pdf.py.'
+                      '<br /><br />',
+                      NORMAL_STYLE),
+            ]
+
+        tables = get_signature_tables(['Signature 1'])
+        data = self.add_tables(
+            data, '<b>Single Signature:</b> '
+            '<font face="Times">get_signature_tables(["Signature 1"])</font>',
+            tables)
+
+        # two signatures in a row
+        tables = get_signature_tables(['Signature 1', 'Signature 2'])
+        data = self.add_tables(
+            data, '<br /><b>Two Signatures:</b> '
+            '<font face="Times">get_signature_tables(["Signature 1",'
+            ' "Signature 2"])</font></b>',
+            tables)
+
+        # single signature with pre/post text
+        tables = get_signature_tables(
+            [('Pre-Text', 'Signature 1', 'Post-Text')])
+        data = self.add_tables(
+            data,
+            '<br /><b>Single Signature with Pre- and Post Text:</b> '
+            '<font face="Times">get_signature_tables('
+            '[("Pre-Text", "Signature 1", "Post-Text")])</font>',
+            tables)
+
+        # three signatures
+        tables = get_signature_tables(['Sig 1', 'Sig 2', 'Sig 3'])
+        data = self.add_tables(
+            data,
+            '<br /><b>Three Signatures:</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2", "Sig 3"])</font>',
+            tables)
+
+        # four signatures
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'])
+        data = self.add_tables(
+            data,
+            '<br /><b>Sample: Four Signatures (each row a table)</b>',
+            tables)
+
+        # four sigs in one table
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'], single_table=True)
+        data = self.add_tables(
+            data,
+            '<br /><b>Sample: Four Signatures (as one table)</b>',
+            tables)
+
+        # create PDF from data
+        path = draw_sample_pdf(data, title=u"Signatures Sample Sheet")
+        print "Written Sample PDF to %s" % path
+        return
