source: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/browser/pdf.py @ 9169

Last change on this file since 9169 was 9169, checked in by uli, 12 years ago

Merge changes from trunk, r8786-HEAD

  • Property svn:keywords set to Id
File size: 14.0 KB
Line 
1## $Id: pdf.py 9169 2012-09-10 11:05:07Z uli $
2##
3## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""
19Reusable components for pdf generation.
20"""
21import grok
22import os
23from cStringIO import StringIO
24from datetime import datetime
25from reportlab.lib.units import cm, inch, mm
26from reportlab.lib.pagesizes import A4, landscape, portrait
27from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
28from reportlab.pdfgen.canvas import Canvas
29from reportlab.platypus import (
30    SimpleDocTemplate, Spacer, Paragraph, Image, Table)
31from zope.formlib.form import setUpEditWidgets
32from zope.i18n import translate
33from zope.publisher.browser import TestRequest
34from zope.component import getUtility
35from waeup.kofa.browser.interfaces import IPDFCreator
36from waeup.kofa.utils.helpers import now
37from waeup.kofa.interfaces import IKofaUtils
38from waeup.kofa.interfaces import MessageFactory as _
39
40#: A reportlab paragraph style for 'normal' output.
41NORMAL_STYLE = getSampleStyleSheet()['Normal']
42
43#: A reportlab paragraph style for output of 'code'.
44CODE_STYLE = ParagraphStyle(
45    name='Code',
46    parent=NORMAL_STYLE,
47    fontName='Courier',
48    fontSize=10,
49    )
50
51#: A reportlab paragraph style for regular form output.
52ENTRY1_STYLE = ParagraphStyle(
53    name='Entry1',
54    parent=NORMAL_STYLE,
55    fontSize=12,
56    )
57
58#: A reportlab paragraph style for smaller form output.
59SMALL_PARA_STYLE = ParagraphStyle(
60    name='Small1',
61    parent=NORMAL_STYLE,
62    fontSize=10,
63    )
64
65#: A reportlab paragraph style for headlines or bold text in form output.
66HEADLINE1_STYLE = ParagraphStyle(
67    name='Header1',
68    parent=NORMAL_STYLE,
69    fontName='Helvetica-Bold',
70    fontSize=12,
71    )
72
73#: A reportlab paragraph style for notes output at end of documents.
74NOTE_STYLE = ParagraphStyle(
75    name='Note',
76    parent=NORMAL_STYLE,
77    fontName='Helvetica',
78    fontSize=12,
79    )
80
81def format_html(html):
82    """Make HTML code usable for use in reportlab paragraphs.
83
84    Main things fixed here:
85    If html code:
86    - remove newlines (not visible in HTML but visible in PDF)
87    - add <br> tags after <div> (as divs break lines in HTML but not in PDF)
88    If not html code:
89    - just replace newlines by <br> tags
90    """
91    if '</' in html:
92        # Add br tag if widgets contain div tags
93        # which are not supported by reportlab
94        html = html.replace('</div>', '</div><br />')
95        html = html.replace('\n', '')
96    else:
97        html = html.replace('\n', '<br />')
98    return html
99
100class NumberedCanvas(Canvas):
101    """A reportlab canvas for numbering pages after all docs are processed.
102
103    Taken from
104    http://code.activestate.com/recipes/546511-page-x-of-y-with-reportlab/
105    http://code.activestate.com/recipes/576832/
106    """
107
108    def __init__(self, *args, **kw):
109        Canvas.__init__(self, *args, **kw)
110        self._saved_page_states = []
111        return
112
113    def showPage(self):
114        self._saved_page_states.append(dict(self.__dict__))
115        self._startPage()
116        return
117
118    def save(self):
119        """add page info to each page (page x of y)"""
120        num_pages = len(self._saved_page_states)
121        for state in self._saved_page_states:
122            self.__dict__.update(state)
123            self.draw_page_number(num_pages)
124            Canvas.showPage(self)
125        Canvas.save(self)
126        return
127
128    def draw_page_number(self, page_count):
129        """draw string at bottom right with 'page x of y'.
130
131        Location of the string is determined by canvas attributes
132        `kofa_footer_x_pos` and `kofa_footer_y_pos` that have to be
133        set manually.
134
135        If this canvas also provides an attribute `kofa_footer_text`,
136        the contained text is rendered left of the ``page x of y``
137        string.
138        """
139        self.setFont("Helvetica", 9)
140        right_footer_text = _(
141            '${footer_text} Page ${num1} of ${num2}',
142            mapping = {'footer_text': self.kofa_footer_text,
143                       'num1':self._pageNumber, 'num2':page_count})
144        self.drawRightString(
145            self.kofa_footer_x_pos, self.kofa_footer_y_pos,
146             translate(right_footer_text))
147        return
148
149class PDFCreator(grok.GlobalUtility):
150    """A utility to help with generating PDF docs.
151    """
152    grok.implements(IPDFCreator)
153
154    watermark_path = None
155    header_logo_path = None
156    watermark_pos = [0, 0]
157    logo_pos = [0, 0, 0]
158
159    @classmethod
160    def _setUpWidgets(cls, form_fields, context):
161        """Setup simple display widgets.
162
163        Returns the list of widgets.
164        """
165        request = TestRequest()
166        return setUpEditWidgets(
167            form_fields, 'form', context, request, {},
168            for_display=True, ignore_request=True
169            )
170
171    @classmethod
172    def _addCourse(cls, table_data, row_num, course_label, course_link,
173                   lang, domain):
174        """Add course data to `table_data`.
175        """
176        if not course_label or not course_link:
177            return table_data, row_num
178        f_label= translate(
179            _(course_label), domain, target_language=lang)
180        f_label = Paragraph(f_label, ENTRY1_STYLE)
181        f_text = Paragraph(course_link, ENTRY1_STYLE)
182        table_data.append([f_label, f_text])
183        row_num += 1
184        return table_data, row_num
185
186    @classmethod
187    def _addDeptAndFaculty(cls, table_data, row_num, dept, faculty,
188                           lang, domain):
189        """Add `dept` and `faculty` as table rows to `table_data`.
190
191        `dept` and `faculty` are expected to be strings or None. In
192        latter case they are not put into the table.
193        """
194        for label, text in (('Department:', dept), ('Faculty:', faculty)):
195            if text is None:
196                continue
197            label = translate(_(label), domain, target_language=lang)
198            table_data.append([
199                Paragraph(label, ENTRY1_STYLE),
200                Paragraph(text, ENTRY1_STYLE)])
201            row_num += 1
202        return table_data, row_num
203
204    @classmethod
205    def fromStringList(cls, string_list):
206        """Generate a list of reportlab paragraphs out of a list of strings.
207
208        Strings are formatted with :data:`CODE_STYLE` and a spacer is
209        appended at end.
210        """
211        result = []
212        for msg in string_list:
213            result.append(Paragraph(msg, CODE_STYLE))
214        result.append(Spacer(1, 20))
215        return result
216
217    @classmethod
218    def getImage(cls, image_path, orientation='LEFT'):
219        """Get an image located at `image_path` as reportlab flowable.
220        """
221        img = Image(image_path, width=4*cm, height=3*cm, kind='bound')
222        img.hAlign=orientation
223        return img
224
225    def getWidgetsTable(self, form_fields, context, view, lang='en',
226                        domain='waeup.kofa', separators=None,
227                        course_label=None, course_link=None, dept=None,
228                        faculty=None):
229        """Return a reportlab `Table` instance, created from widgets
230        determined by `form_fields` and `context`.
231
232        - `form_fields`
233           is a list of schema fields as created by grok.AutoFields.
234        - `context`
235           is some object whose content is rendered here.
236        - `view`
237           is currently not used but supposed to be a view which is
238           actually rendering a PDF document.
239        - `lang`
240           the portal language. Used for translations of strings.
241        - `domain`
242           the translation domain used for translations of strings.
243        - `separators`
244           a list of separators.
245        - `course_label` and `course_link`
246           if a course should be added to the table, `course_label`
247           and `course_link` can be given, both being strings. They
248           will be rendered in an extra-row.
249        - `dept` and `faculty`
250           if these are given, we render extra rows with faculty and
251           department.
252        """
253        table_data = []
254        table_style = [#('LEFTPADDING', (0,0), (0,-1), 0), # indentation
255                       ('VALIGN', (0,0), (-1,-1), 'TOP'),
256                       ]
257        row_num = 0
258        widgets = self._setUpWidgets(form_fields, context)
259        for widget in widgets:
260            if separators and separators.get(widget.name):
261                f_headline = translate(
262                    separators[widget.name], domain, target_language=lang)
263                f_headline = Paragraph(f_headline, HEADLINE1_STYLE)
264                table_data.append([f_headline ])
265                table_style.append(('SPAN', (0,row_num), (-1,row_num)),)
266                table_style.append(
267                    ('TOPPADDING', (0,row_num), (-1,row_num), 12),)
268                row_num += 1
269            f_label = translate(widget.label.strip(), domain,
270                                target_language=lang)
271            f_label = Paragraph(f_label, ENTRY1_STYLE)
272            f_text = translate(widget(), domain, target_language=lang)
273            f_text = format_html(f_text)
274            if f_text:
275                hint = ' <font size=9>' + widget.hint + '</font>'
276                f_text = f_text + hint
277            f_text = Paragraph(f_text, ENTRY1_STYLE)
278            table_data.append([f_label,f_text])
279            row_num += 1
280
281        # Add course (admitted, etc.) if applicable
282        table_data, row_num = self._addCourse(
283            table_data, row_num, course_label, course_link, lang, domain,)
284
285        ## Add dept. and faculty if applicable
286        table_data, row_num = self._addDeptAndFaculty(
287            table_data, row_num, dept, faculty, lang, domain)
288
289        # Create table
290        table = Table(table_data,style=table_style)
291        table.hAlign = 'LEFT'
292        return table
293
294
295    def paint_background(self, canvas, doc):
296        """Paint background of a PDF, including watermark, title, etc.
297
298        The `doc` is expected to be some reportlab SimpleDocTemplate
299        or similar object.
300
301        Text of headerline is extracted from doc.kofa_headtitle, the
302        document title (under the head) from doc.kofa_title.
303
304        This is a callback method that will be called from reportlab
305        when creating PDFs with :meth:`create_pdf`.
306        """
307        canvas.saveState()
308        width, height = doc.width, doc.height
309        width += doc.leftMargin + doc.rightMargin
310        height += doc.topMargin + doc.bottomMargin
311
312        # Watermark
313        if self.watermark_path is not None:
314            canvas.saveState()
315            canvas.drawImage(self.watermark_path,
316                self.watermark_pos[0], self.watermark_pos[1])
317            canvas.restoreState()
318
319        # Header
320        head_title = getattr(
321            doc, 'kofa_headtitle', getattr(
322                grok.getSite()['configuration'], 'name',
323                u'Sample University'))
324        canvas.setFont("Helvetica-Bold", 18)
325        canvas.drawString(1.5*cm, height-1.7*cm, head_title)
326        canvas.line(1.5*cm,height-1.9*cm,width-1.5*cm,height-1.9*cm)
327        if self.header_logo_path is not None:
328            canvas.drawImage(self.header_logo_path,
329                self.logo_pos[0], self.logo_pos[1], width=self.logo_pos[2],
330                preserveAspectRatio=True, anchor='ne')
331
332        # Title
333        canvas.saveState()
334        canvas.setFont("Helvetica-Bold", 14)
335        title = getattr(doc, 'kofa_title', '')
336        if '\n' in title:
337            title_lines = title.split('\n')
338            for num, line in enumerate(title_lines):
339                canvas.drawCentredString(
340                    width/2.0, height-2.8*cm-(num*0.7*cm), line)
341        elif title:
342            canvas.drawCentredString(width/2.0, height-2.8*cm, title)
343        canvas.restoreState()
344
345        # Footer
346        canvas.saveState()
347        canvas.line(2.2*cm, 0.62*inch, width-2.2*cm, 0.62*inch)
348        canvas.setFont("Helvetica", 9)
349        if not getattr(doc, 'kofa_nodate', False):
350            tz = getUtility(IKofaUtils).tzinfo
351            today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
352            canvas.drawString(2.2*cm, 0.5 * inch,
353                translate(_(u'Date: ${a}', mapping = {'a': today})))
354        # set canves attributes needed to render `page x of y`
355        canvas.kofa_footer_x_pos = width-2.2*cm
356        canvas.kofa_footer_y_pos = 0.5 * inch
357        canvas.kofa_footer_text =  doc.kofa_footer
358        canvas.restoreState()
359        canvas.restoreState()
360
361        # Metadata
362        canvas.setAuthor(getattr(doc, 'kofa_author', 'Unknown'))
363        canvas.setSubject(title)
364        canvas.setCreator(u'WAeUP Kofa')
365        return
366
367    def create_pdf(self, data, headerline=None, title=None, author=None,
368                   footer='', note=None):
369        """Returns a binary data stream which is a PDF document.
370        """
371        pdf_stream = StringIO()
372        doc = SimpleDocTemplate(
373            pdf_stream,
374            bottomMargin=1.1*inch,
375            topMargin=1.6*inch,
376            title=title,
377            pagesize=portrait(A4),
378            showBoundary=False,
379            )
380        # Set some attributes that are needed when rendering the background.
381        if headerline is not None:
382            doc.kofa_headtitle = headerline
383        doc.kofa_title = title
384        doc.kofa_author = author
385        doc.kofa_footer = footer
386        if note is not None:
387            html = format_html(note)
388            data.append(Paragraph(html, NOTE_STYLE))
389        doc.build(data, onFirstPage=self.paint_background,
390                  onLaterPages=self.paint_background,
391                  canvasmaker=NumberedCanvas
392                  )
393        result = pdf_stream.getvalue()
394        pdf_stream.close()
395        return result
Note: See TracBrowser for help on using the repository browser.