Index: /main/waeup.kofa/trunk/src/waeup/kofa/applicants/pdf.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/applicants/pdf.py	(revision 8058)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/applicants/pdf.py	(revision 8059)
@@ -20,28 +20,23 @@
 """
 import grok
-from datetime import datetime
-from reportlab.pdfgen import canvas
+from reportlab.lib.styles import getSampleStyleSheet
 from reportlab.lib.units import cm
-from reportlab.lib.pagesizes import A4
-from reportlab.lib.styles import getSampleStyleSheet
-from reportlab.platypus import Frame, Paragraph, Image, Table, Spacer
-from reportlab.platypus.tables import TableStyle
-from zope.i18n import translate
+from reportlab.platypus import Paragraph, Image, Table, Spacer
 from zope.component import getUtility
 from zope.formlib.form import setUpEditWidgets
+from zope.i18n import translate
 from zope.publisher.browser import TestRequest
-from waeup.kofa.widgets.datewidget import FriendlyDateDisplayWidget
 from waeup.kofa.applicants.interfaces import IApplicant, IApplicantsUtils
 from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
+from waeup.kofa.browser.interfaces import IPDFCreator
 from waeup.kofa.interfaces import IExtFileStore, IPDF, IKofaUtils
 from waeup.kofa.interfaces import MessageFactory as _
-
-SLIP_STYLE = TableStyle(
-    [('VALIGN',(0,0),(-1,-1),'TOP')]
-    )
-
+from waeup.kofa.widgets.datewidget import FriendlyDateDisplayWidget
 
 class PDFApplicationSlip(grok.Adapter):
-
+    """Create a PDF application slip for applicants.
+    """
+    # XXX: Many things in here are reusable. We might want to split
+    # things. Possibly move parts to IPDFCreator?
     grok.context(IApplicant)
     grok.implements(IPDF)
@@ -129,49 +124,16 @@
         return data
 
-    def __call__(self, view=None):
-        """Return a PDF representation of the context applicant.
-
-        If no `view` is given, the course admitted field will be an
-        empty string and author will be set as ``'unknown'``.
-
-        If a `view` is given, author will be set as the calling
-        principal.
-        """
-        pdf = canvas.Canvas('application_slip.pdf',pagesize=A4)
-        pdf.setTitle(self.title)
-        pdf.setSubject('Application')
-        if view is None:
-            pdf.setAuthor('unknown')
-        if view is not None:
-            pdf.setAuthor('%s (%s)' % (view.request.principal.title,
-                view.request.principal.id))
-        pdf.setCreator('Kofa')
-        width, height = A4
-        style = getSampleStyleSheet()
-        pdf.line(1*cm,height-(1.8*cm),width-(1*cm),height-(1.8*cm))
-
-        story = []
-        frame_header = Frame(1*cm,1*cm,width-(1.7*cm),height-(1.7*cm))
-        header_title = getattr(grok.getSite(), 'name', u'Sample University')
-        story.append(Paragraph(header_title, style["Heading1"]))
-        frame_header.addFromList(story,pdf)
-
-        story = []
-        frame_body = Frame(1*cm,1*cm,width-(2*cm),height-(3.5*cm))
-        story.append(Paragraph(self.title, style["Heading2"]))
-        story.append(Spacer(1, 18))
-        for msg in self.context.history.messages:
-            f_msg = '<font face="Courier" size=10>%s</font>' % msg
-            story.append(Paragraph(f_msg, style["Normal"]))
-        story.append(Spacer(1, 24))
-
-        # Setup table data
-        data = []
-        # Insert passport photograph
-        data = self._addPassportImage(data)
-        # Render widget fields
+    def _getWidgetsTable(self, view):
+        """Return a reportlab table buildt of widgets.
+        """
+        style = getSampleStyleSheet()
+        table_data = []
+        table_style = [('LEFTPADDING', (0,0), (0,-1), 0),
+                       ('VALIGN', (0,0), (-1,-1), 'TOP'),
+                       ]
+        row_num = 0
         widgets = self._setUpWidgets()
         portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
-        for widget in widgets: # self.widgets:
+        for widget in widgets:
             separators = getUtility(IApplicantsUtils).SEPARATORS_DICT
             if separators and separators.get(widget.name):
@@ -180,5 +142,9 @@
                     target_language=portal_language))
                 f_headline = Paragraph(f_headline, style["Normal"])
-                data.append([f_headline ])
+                table_data.append([f_headline ])
+                table_style.append(('SPAN', (0,row_num), (-1,row_num)),)
+                table_style.append(
+                    ('TOPPADDING', (0,row_num), (-1,row_num), 12),)
+                row_num += 1
             f_label = '<font size=12>%s</font>:' % translate(
                 widget.label.strip(), 'waeup.kofa',
@@ -191,5 +157,6 @@
             f_text = f_text.replace('\n', '')
             f_text = Paragraph(f_text, style["Normal"])
-            data.append([f_label,f_text])
+            table_data.append([f_label,f_text])
+            row_num += 1
         adm_translation = translate(_('Admitted Course of Study:'),
                 'waeup.kofa', target_language=portal_language)
@@ -199,14 +166,17 @@
         f_label = Paragraph(f_label, style["Normal"])
         f_text = Paragraph(f_text, style["Normal"])
-        data.append([f_label,f_text])
+        table_data.append([f_label,f_text])
+        row_num += 1
 
         # Add dept. and faculty if applicable
-        data = self._addDeptAndFaculty(data)
+        table_data = self._addDeptAndFaculty(table_data)
 
         # Create table
-        table = Table(data,style=SLIP_STYLE, colWidths=[10*cm, 8.5*cm])
-        story.append(table)
-
-        # Add some comments
+        table = Table(table_data,style=table_style)
+        table.hAlign = 'LEFT'
+        return table
+
+    def _addComments(self, data):
+        style = getSampleStyleSheet()
         if self.context.state == 'created':
             comment1 = _(
@@ -214,5 +184,6 @@
                 ' and enter your new credentials:' +
                 ' user name= ${a}, password = ${b}.</font>', mapping = {
-                'a':self.context.student_id, 'b':self.context.application_number}
+                    'a':self.context.student_id,
+                    'b':self.context.application_number}
                 )
             comment2 = _(
@@ -222,15 +193,40 @@
             comment1 = Paragraph(comment1, style["Normal"])
             comment2 = Paragraph(comment2, style["Normal"])
-            story.append(Spacer(1, 18))
-            story.append(comment1)
-            story.append(comment2)
-
-        frame_body.addFromList(story,pdf)
-
-        story = []
-        frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
-        timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
-        f_text = '<font size=10>%s</font>' % timestamp
-        story.append(Paragraph(f_text, style["Normal"]))
-        frame_footer.addFromList(story,pdf)
-        return pdf.getpdfdata()
+            data.extend([Spacer(1, 18), comment1, comment2])
+        return data
+
+    def __call__(self, view=None):
+        """Return a PDF representation of the context applicant.
+
+        If no `view` is given, the course admitted field will be an
+        empty string and author will be set as ``'unknown'``.
+
+        If a `view` is given, author will be set as the calling
+        principal.
+        """
+        doc_title = self.title.replace('-', '\n')
+        style = getSampleStyleSheet()
+        data = []
+
+        # append history
+        for msg in self.context.history.messages:
+            f_msg = '<font face="Courier" size=10>%s</font>' % msg
+            data.append(Paragraph(f_msg, style["Normal"]))
+        data.append(Spacer(1, 20))
+
+        # append photograph
+        img_path = getattr(
+            getUtility(IExtFileStore).getFileByContext(self.context),
+            'name', DEFAULT_PASSPORT_IMAGE_PATH)
+        doc_img = Image(img_path, width=4*cm, height=3*cm, kind='bound')
+        doc_img.hAlign='LEFT'
+        data.append(doc_img)
+
+        # append widgets
+        data.append(self._getWidgetsTable(view))
+
+        # append comments
+        data = self._addComments(data)
+
+        creator = getUtility(IPDFCreator)
+        return creator.create_pdf(data, None, doc_title)
Index: /main/waeup.kofa/trunk/src/waeup/kofa/browser/interfaces.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/browser/interfaces.py	(revision 8058)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/browser/interfaces.py	(revision 8059)
@@ -163,2 +163,32 @@
         """Get captcha chosen for a certain site or default.
         """
+
+class IPDFCreator(Interface):
+    """A component that knows where logo graphics for PDFs are stored
+    and can generate PDF documents.
+
+    It is a utility (and not a simple function or class) to make these
+    data customizable in derived packages.
+    """
+    header_logo_path = schema.TextLine(
+        title = u'Path to header logo JPG')
+    watermark_path = schema.TextLine(
+        title = u'Path to watermark logo JPG')
+    def paint_background(canvas, doc):
+        """A callback function to render background of PDFs.
+        """
+    def create_pdf(data, headerline=None, title=None):
+        """Create a PDF.
+
+        `data` is expected to be a list of reportlab flowables
+        (paragraphs, images, tables, etc.), that will be rendered into
+        some background.
+
+        `headerline` will be displayed in page head and `title` under
+        the top bar.
+
+        If no `headerline` is given, a default will be rendered (name
+        of university).
+
+        If no `title` is given, nothing will be rendered.
+        """
Index: /main/waeup.kofa/trunk/src/waeup/kofa/browser/pdf.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/browser/pdf.py	(revision 8059)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/browser/pdf.py	(revision 8059)
@@ -0,0 +1,120 @@
+## $Id$
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Reusable components for pdf generation.
+"""
+import grok
+import os
+from cStringIO import StringIO
+from datetime import datetime
+from reportlab.lib.units import cm, inch
+from reportlab.lib.pagesizes import A4, landscape, portrait
+from reportlab.platypus import SimpleDocTemplate
+from waeup.kofa.browser.interfaces import IPDFCreator
+
+class PDFCreator(grok.GlobalUtility):
+    grok.implements(IPDFCreator)
+
+    header_logo_path = os.path.join(
+        os.path.dirname(__file__), 'static', 'pdf_logo.jpg')
+    watermark_path = os.path.join(
+        os.path.dirname(__file__), 'static', 'pdf_watermark.jpg')
+
+    def paint_background(self, canvas, doc):
+        """Paint background of a PDF, including watermark, title, etc.
+
+        The `doc` is expected to be some reportlab SimpleDocTemplate
+        or similar object.
+
+        Text of headerline is extracted from doc.kofa_headtitle, the
+        document title (under the head) from doc.kofa_title.
+
+        This is a callback method that will be called from reportlab
+        when creating PDFs with :meth:`create_pdf`.
+        """
+        canvas.saveState()
+        width, height = doc.width, doc.height
+        width += doc.leftMargin + doc.rightMargin
+        height += doc.topMargin + doc.bottomMargin
+
+        # Watermark
+        if self.watermark_path is not None:
+            canvas.saveState()
+            canvas.drawImage(self.watermark_path, 26, 100)
+            canvas.restoreState()
+
+        # Header
+        head_title = getattr(
+            doc, 'kofa_headtitle', getattr(grok.getSite(), 'name',
+                                           u'Sample University'))
+        canvas.setFont("Helvetica-Bold", 18)
+        canvas.drawString(1.5*cm, height-1.7*cm, head_title)
+        canvas.line(1.5*cm,height-1.9*cm,width-1.5*cm,height-1.7*cm)
+        if self.header_logo_path is not None:
+            canvas.drawImage(self.header_logo_path, 507, 170, width=40,
+                             preserveAspectRatio=True, anchor='ne')
+
+        # Title
+        canvas.saveState()
+        canvas.setFont("Helvetica-Bold", 14)
+        title = getattr(doc, 'kofa_title', '')
+        if '\n' in title:
+            title_lines = title.split('\n')
+            for num, line in enumerate(title_lines):
+                canvas.drawCentredString(
+                    width/2.0, height-2.8*cm-(num*0.7*cm), line)
+        elif title:
+            canvas.drawCentredString(width/2.0, height-2.8*cm, title)
+        canvas.restoreState()
+
+        # Footer
+        canvas.saveState()
+        canvas.line(2.2*cm, 0.62*inch, width-2.2*cm, 0.62*inch)
+        canvas.setFont("Helvetica", 9)
+        if not getattr(doc, 'kofa_nodate', False):
+            today = datetime.now().strftime('%d/%m/%Y %H:%M:%S')
+            canvas.drawString(2.2*cm, 0.5 * inch, "Date: %s" % today)
+        canvas.drawRightString(
+            width-2.2*cm, 0.5 * inch, "page %d" % (doc.page,))
+        canvas.restoreState()
+        canvas.restoreState()
+        return
+
+    def create_pdf(self, data, headerline=None, title=None):
+        """Returns a binary data stream which is a PDF document.
+        """
+        pdf_stream = StringIO()
+        doc = SimpleDocTemplate(
+            pdf_stream,
+            bottomMargin=1.1*inch,
+            topMargin=1.6*inch,
+            title=title,
+            pagesize=portrait(A4),
+            showBoundary=False,
+            )
+        # XXX: more things to set here, like author, etc.
+
+        # Set some attributes that are needed when rendering the background.
+        if headerline is not None:
+            doc.kofa_headtitle = headerline
+        doc.kofa_title = title
+        doc.build(data, onFirstPage=self.paint_background,
+                  onLaterPages=self.paint_background)
+        result = pdf_stream.getvalue()
+        pdf_stream.close()
+        return result
