source: main/waeup.kofa/trunk/src/waeup/kofa/browser/pdf.py @ 12774

Last change on this file since 12774 was 10595, checked in by uli, 11 years ago

Add helper function to generate a flowable QR code for reportlab.

  • Property svn:keywords set to Id
File size: 31.9 KB
Line 
1## $Id: pdf.py 10595 2013-09-06 13:12:27Z 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
23import pytz
24from cStringIO import StringIO
25from datetime import datetime
26from reportlab.graphics.barcode.qr import QrCodeWidget
27from reportlab.graphics.shapes import Drawing
28from reportlab.lib import colors
29from reportlab.lib.units import cm, inch, mm
30from reportlab.lib.pagesizes import A4, landscape, portrait
31from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
32from reportlab.pdfgen.canvas import Canvas
33from reportlab.platypus import (
34    SimpleDocTemplate, Spacer, Paragraph, Image, Table)
35from zope.formlib.form import setUpEditWidgets
36from zope.i18n import translate
37from zope.publisher.browser import TestRequest
38from zope.component import getUtility, queryUtility
39from waeup.kofa.browser.interfaces import IPDFCreator
40from waeup.kofa.utils.helpers import now
41from waeup.kofa.interfaces import IKofaUtils
42from waeup.kofa.interfaces import MessageFactory as _
43
44
45#: A reportlab paragraph style for 'normal' output.
46NORMAL_STYLE = getSampleStyleSheet()['Normal']
47
48#: A reportlab paragraph style for 'normal' output.
49HEADING3_STYLE = getSampleStyleSheet()['Heading3']
50
51#: A reportlab paragraph style for headings.
52HEADING_STYLE = ParagraphStyle(
53    name='Heading3',
54    parent=HEADING3_STYLE,
55    fontSize=11,
56    )
57
58#: A reportlab paragraph style for output of 'code'.
59CODE_STYLE = ParagraphStyle(
60    name='Code',
61    parent=NORMAL_STYLE,
62    fontName='Courier',
63    fontSize=10,
64    leading=10,
65    )
66
67#: A reportlab paragraph style for regular form output.
68ENTRY1_STYLE = ParagraphStyle(
69    name='Entry1',
70    parent=NORMAL_STYLE,
71    fontSize=11,
72    leading=10,
73    )
74
75#: A reportlab paragraph style for smaller form output.
76SMALL_PARA_STYLE = ParagraphStyle(
77    name='Small1',
78    parent=NORMAL_STYLE,
79    fontSize=8,
80    )
81
82#: A reportlab paragraph style for headlines or bold text in form output.
83HEADLINE1_STYLE = ParagraphStyle(
84    name='Header1',
85    parent=NORMAL_STYLE,
86    fontName='Helvetica-Bold',
87    fontSize=10,
88    )
89
90#: A reportlab paragraph style for notes output at end of documents.
91NOTE_STYLE = ParagraphStyle(
92    name='Note',
93    parent=NORMAL_STYLE,
94    fontName='Helvetica',
95    fontSize=10,
96    leading=9,
97    )
98
99#: Base style for signature tables
100SIGNATURE_TABLE_STYLE = [
101    ('VALIGN',(0,-1),(-1,-1),'TOP'),
102    #('FONT', (0,0), (-1,-1), 'Helvetica-BoldOblique', 12),
103    ('BOTTOMPADDING', (0,0), (-1,0), 36),
104    ('TOPPADDING', (0,-1), (-1,-1), 0),
105    ]
106
107
108def format_html(html):
109    """Make HTML code usable for use in reportlab paragraphs.
110
111    Main things fixed here:
112    If html code:
113    - remove newlines (not visible in HTML but visible in PDF)
114    - add <br> tags after <div> (as divs break lines in HTML but not in PDF)
115    If not html code:
116    - just replace newlines by <br> tags
117    """
118    if '</' in html:
119        # Add br tag if widgets contain div tags
120        # which are not supported by reportlab
121        html = html.replace('</div>', '</div><br />')
122        html = html.replace('\n', '')
123    else:
124        html = html.replace('\n', '<br />')
125    return html
126
127def normalize_signature(signature_tuple):
128    """Normalize a signature tuple.
129
130    Returns a tuple ``(<PRE-TEXT>, <SIGNATURE>, <POST-TEXT>)`` from
131    input tuple. The following rules apply::
132
133      (pre, sig, post)  --> (pre, sig, post)
134      (pre, sig)        --> (pre, sig, None)
135      (sig)             --> (None, sig, None)
136
137    Also simple strings are accepted as input::
138
139      sig               --> (None, sig, None)
140
141    If input is not a tuple nor a basestring or if the tuple contains
142    an invalid number of elements, ``ValueError`` is raised.
143    """
144    if isinstance(signature_tuple, basestring):
145        return (None, signature_tuple, None)
146    if not isinstance(signature_tuple, tuple):
147        raise ValueError("signature_tuple must be a string or tuple")
148    if len(signature_tuple) < 1 or len(signature_tuple) > 3:
149        raise ValueError("signature_tuple must have 1, 2, or 3 elements")
150    elif len(signature_tuple) == 1:
151        signature_tuple = (None, signature_tuple[0], None)
152    elif len(signature_tuple) == 2:
153        signature_tuple = (signature_tuple[0], signature_tuple[1], None)
154    return signature_tuple
155
156def vert_signature_cell(signature, date_field=True, date_text=_('Date:'),
157                        start_row=0, start_col=0, underline=True):
158    """Generate a table part containing a vertical signature cell.
159
160    Returns the table data as list of lists and an according style.
161
162    `signature`:
163       a signature tuple containing (<PRE-TEXT, SIGNATURE-TEXT, POST-TEXT>)
164
165    `date_field`:
166       boolean indicating that a 'Date:' text should be rendered into this
167       signature cell (or not).
168
169    `date_text`:
170       the text to be rendered into the signature field as 'Date:' text.
171
172    `start_row`:
173       starting row of the signature cell inside a broader table.
174
175    `start_col`:
176       starting column of the signature cell inside a broader table.
177
178    `underline`:
179       boolean indicating that the signature cell should provide a line on
180       top (`True` by default).
181
182    Vertical signature cells look like this::
183
184      +------------+
185      |Pre         |
186      +------------+
187      |Date:       |
188      |            |
189      +------------+
190      | ---------- |
191      | Signature  |
192      +------------+
193      |Post        |
194      +------------+
195    """
196    # split signature parts, replacing None with empty string
197    pre, sig, post = [x or '' for x in signature]
198    style = ()
199    x, y = start_col, start_row+2
200    if underline:
201        style += (('LINEABOVE', (x, y), (x, y), 1, colors.black),)
202    d_text = date_field and date_text or ''
203    data = [[pre], [d_text], [sig], [post]]
204    col_widths = [1.0]
205    return data, style, col_widths
206
207def horiz_signature_cell(signature, date_field=True, date_text=_('Date'),
208                         start_row=0, start_col=0):
209    """Generate a table part containing an horizontal signature cell
210
211    Returns the table data as list of lists and an according style.
212
213    `signature`:
214       a signature tuple containing (<PRE-TEXT, SIGNATURE-TEXT, POST-TEXT>)
215
216    `date_field`:
217       boolean indicating that a 'Date:' text should be rendered into this
218       signature cell (or not).
219
220    `date_text`:
221       the text to be rendered into the signature field as 'Date:' text.
222
223    `start_row`:
224       starting row of the signature cell inside a broader table.
225
226    `start_col`:
227       starting column of the signature cell inside a broader table.
228
229    Horizontal signature cells look like this::
230
231      +------------+---+-----------+
232      |Pre text possibly filling   |
233      |the whole box               |
234      +------------+---+-----------+
235      |            |   |           |
236      |            |   |           |
237      +------------+---+-----------+
238      | ---------- |   | --------- |
239      | Date       |   | Signature |
240      +------------+---+-----------+
241      |Post                        |
242      +------------+---+-----------+
243
244    """
245    pre, sig, post = signature
246    if not date_field:
247        data, style, cols = vert_signature_cell(signature, date_field=False)
248        return data, style, cols
249    style = (
250        # let pre and post text span the whole signature cell
251        ('SPAN', (start_col, start_row), (start_col+2, start_row)),
252        ('SPAN', (start_col, start_row+3), (start_col+2, start_row+3)),
253        )
254    # horizontal cells are buildt from vertical ones chained together
255    cell1 = vert_signature_cell(  # leftmost date col
256        (pre, date_text, post), date_field=False,
257        start_row=start_row, start_col=start_col)
258    cell2 = vert_signature_cell(  # spacer col (between date and sig)
259        ('', '', ''), date_field=False, underline=False,
260        start_row=start_row, start_col=start_col+1)
261    cell3 = vert_signature_cell(  # rightmost signature column
262        ('', sig, ''), date_field=False,
263        start_row=start_row, start_col=start_col+2)
264    data = map(lambda x, y, z: x+y+z, cell1[0], cell2[0], cell3[0])
265    style = style + cell1[1] + cell2[1] + cell3[1]
266    col_widths  = [0.3, 0.03, 0.67] # sums up to 1.0
267    return data, style, col_widths
268
269def signature_row(signatures, start_row=0, horizontal=None, max_per_row=3):
270    data = [[], [], [], []]
271    style = ()
272    signatures = [normalize_signature(sig) for sig in signatures]
273    start_col = 0
274    col_widths = []
275
276    if horizontal is None:
277        horizontal = len(signatures) == 1
278    cell_maker = vert_signature_cell
279    if horizontal:
280        cell_maker = horiz_signature_cell
281    main_cell_height = not horizontal and 36 or 18
282
283    for sig in signatures:
284        sig_data, sig_style, sig_cols = cell_maker(
285            sig, start_row=start_row, start_col=start_col)
286        data = map(lambda x, y: x+y, data, sig_data)
287        style += sig_style
288        col_widths += sig_cols + [None,]
289
290        start_col += 1
291        # add spacer
292        spacer, spacer_style, cols = vert_signature_cell(
293            ('', '', ''), date_field=False, underline=False,
294            start_row=start_row, start_col=start_col)
295        data = map(lambda x, y: x+y, data, spacer)
296        style += spacer_style
297        start_col += 1
298
299    y = start_row
300    sig_row = start_row + 2
301    style = style + (
302        ('TOPPADDING', (0, y+2), (-1, y+2), 0), # signature row
303        ('BOTTOMPADDING', (0, y+1), (-1, y+1), main_cell_height),
304        ('LEFTPADDING', (0, y), (-1, y), 1), # pre row
305        ('LEFTPADDING', (0, y+3), (-1, y+3), 1), # post row
306        )
307
308    if len(signatures) == 1:
309        # pre and post text should span whole table
310        style += (('SPAN', (0, y), (-1, y)),
311                  ('SPAN', (0, y+3), (-1, y+3)),
312                  )
313
314    if data[0] == [''] * len(data[0]):
315        # no pre text: hide pre row by minimizing padding
316        style += (('TOPPADDING', (0,y), (-1, y), -6),
317                  ('BOTTOMPADDING', (0,y), (-1, y), -6),
318                  )
319    if data[-1] == [''] * len(data[0]):
320        # no post text: hide post row by minimizing padding
321        style += (('TOPPADDING', (0,y+3), (-1, y+3), -6),
322                  ('BOTTOMPADDING', (0,y+3), (-1, y+3), -6),
323                  )
324
325    if len(signatures) > 1:
326        data = [x[:-1] for x in data] # strip last spacer
327        col_widths = col_widths[:-1]
328    return data, style, col_widths
329
330def sig_table(signatures, lang='en', max_per_row=3, horizontal=None,
331              single_table=False, start_row=0, landscape=False):
332    if landscape:
333        space_width = 2.4  # width in cm of space between signatures
334        table_width = 24.0 # supposed width of signature table in cms
335    else:
336        space_width = 0.4  # width in cm of space between signatures
337        table_width = 16.0 # supposed width of signature table in cms
338    # width of signature cells in cm...
339    sig_num = len(signatures)
340    sig_col_width = (table_width - ((sig_num - 1) * space_width)) / sig_num
341    if sig_num == 1:
342        sig_col_width = 0.6 * table_width         # signature cell part
343        space_width = table_width - sig_col_width # spacer part on the right
344
345    if sig_num > max_per_row:
346        if horizontal is None:
347            horizontal = max_per_row == 1
348        sigs_by_row = [signatures[x:x+max_per_row] for x in range(
349            0, sig_num, max_per_row)]
350        result = []
351        curr_row = 0
352        for num, row_sigs in enumerate(sigs_by_row):
353            curr_row = 0
354            if single_table:
355                curr_row = num * 4
356            result.append(
357                sig_table(row_sigs, lang=lang, max_per_row=max_per_row,
358                          horizontal=horizontal, start_row=curr_row,
359                          landscape=landscape)[0],
360                          )
361        missing_num = len(result[-2][0][0]) - len(result[-1][0][0])
362        if missing_num:
363            # last row contained less cells, fix it...
364            result[-1] = ([x + [''] * missing_num for x in result[-1][0]],
365                          result[-1][1], result[-2][2])
366        return result
367
368    data, style, cols = signature_row(signatures, horizontal=horizontal,
369                                      start_row=start_row)
370    style += (('VALIGN', (0,0), (-1,-1), 'TOP'),)
371
372    # compute col widths...
373    col_widths = []
374    for col in cols:
375        if col is not None:
376            col = col * sig_col_width * cm
377        else:
378            col = space_width * cm
379        col_widths.append(col)
380
381    # replace strings by paragraphs and translate all contents
382    for rnum, row in enumerate(data):
383        for cnum, cell in enumerate(row):
384            if cell:
385                content = translate(cell, lang)
386                data[rnum][cnum] = Paragraph(content, NORMAL_STYLE)
387    return [(data, style, col_widths),]
388
389def get_sig_tables(signatures, lang='en', max_per_row=3, horizontal=None,
390                   single_table=False, landscape=False):
391    rows = sig_table(signatures, lang=lang, max_per_row=max_per_row,
392                     horizontal=horizontal, single_table=single_table,
393                     landscape=landscape)
394    if single_table:
395        result_data = []
396        result_style = ()
397        for row in rows:
398            data, style, col_widths = row
399            result_data += data
400            result_style += style
401        return [(result_data, result_style, col_widths),]
402    return rows
403
404def get_signature_tables(signatures, lang='en', max_per_row=3,
405                         horizontal=None, single_table=False,
406                         landscape=False):
407    """Get a list of reportlab flowables representing signature fields.
408
409    `signatures` is a list of signatures. Each signature can be a
410    simple string or a tuple of format::
411
412      (<PRE-TEXT>, <SIGNATURE>, <POST-TEXT>)
413
414    where ``<PRE-TEXT>`` and ``<POST-TEXT>`` are texts that should
415    appear on top (PRE) or below (POST) the signature cell. Both
416    formats, string and tuple, can be mixed. A single signature would
417    be given as ``[('Pre-Text', 'Signature', 'Post-Text'),]`` or
418    simply as ``['Signature']`` if not pre or post-text is wanted.
419
420    All texts (pre, sig, post) are rendered as paragraphs, so you can
421    pass in also longer texts with basic HTML formatting like ``<b>``,
422    ``<i>``, ``<br />``, etc.
423
424    ``lang`` sets the language to use in I18n context. All texts are
425    translated to the given language (``en`` by default) if a
426    translation is available.
427
428    ``max_per_row`` gives the maximum number of signatures to put into
429    a single row. The default is 3. If more signatures are passed in,
430    these signatures are put into a new row. So, for example by
431    default 8 signatures would be spread over 3 rows.
432
433    ``horizontal`` tells how the single signature cells should be
434    rendered: horizontal or vertical. While horizontal cells render
435    date and signature fields side by side, in vertical cells date is
436    rendered on top of the signature.
437
438    This parameter accepts *three* different values: ``True``,
439    ``False``, or ``None``. While with ``True`` each cell is rendered
440    in horizontal mode, ``False`` will create only vertical cells.
441
442    The ``None`` value (set by default) is different: if set, the mode
443    will be dependent on the number of signatures per row. If a row
444    contains exactly one signature (because only one sig was passed
445    in, or because ``max_per_row`` was set to ``1``), then this
446    signature is rendered in horizontal mode. Otherwise (with more
447    than one sig per row) each cell is rendered in vertical mode. This
448    pseudo-smart behaviour can be switched off by setting
449    ``horizontal`` explicitly to ``True`` or ``False``.
450
451    ``single_table`` is a boolean defaulting to ``False``. By default
452    we return the rows of a signature table in several tables, one of
453    each row. This makes it easier for reportlab to perform pagebreaks
454    in case the page is already full, without wasting space. If the
455    parameter is set to ``True``, then always a list with exactly one
456    table is returned, which will contain all rows in one table.
457
458    Generally, if a row contains only one signature, only a part of
459    the page width is used to render this signature. If two or more
460    signatures are passed in, the complete page width will be filled
461    and the single signature cells will be shrinked to fit.
462    """
463    data_list = get_sig_tables(
464        signatures, lang=lang, max_per_row=max_per_row,
465        horizontal=horizontal, single_table=single_table, landscape=landscape)
466    return [Table(row_data, style=row_style, colWidths=row_col_widths,
467                  repeatRows=4)
468            for row_data, row_style, row_col_widths in data_list]
469
470def format_signatures(signatures, max_per_row=3, lang='en',
471                      single_table=False,
472                      date_field=True, date_text=_('Date'),
473                      base_style=SIGNATURE_TABLE_STYLE):
474    result = []
475    signature_tuples = [normalize_signature(sig) for sig in signatures]
476    for pre, sig, post in signature_tuples:
477        row = []
478        if pre is not None:
479            row.append([
480                translate(pre, lang), '', '', ''])
481        row.append([
482            translate(_('Date'), lang), '',
483            translate(sig, lang), ''
484            ])
485        if post is not None:
486            row.append([
487                translate(post, lang), '', '', ''])
488        result.append((row, base_style))
489    return result
490
491
492
493class NumberedCanvas(Canvas):
494    """A reportlab canvas for numbering pages after all docs are processed.
495
496    Taken from
497    http://code.activestate.com/recipes/546511-page-x-of-y-with-reportlab/
498    http://code.activestate.com/recipes/576832/
499    """
500
501    def __init__(self, *args, **kw):
502        Canvas.__init__(self, *args, **kw)
503        self._saved_page_states = []
504        return
505
506    def showPage(self):
507        self._saved_page_states.append(dict(self.__dict__))
508        self._startPage()
509        return
510
511    def save(self):
512        """add page info to each page (page x of y)"""
513        num_pages = len(self._saved_page_states)
514        for state in self._saved_page_states:
515            self.__dict__.update(state)
516            self.draw_page_number(num_pages)
517            Canvas.showPage(self)
518        Canvas.save(self)
519        return
520
521    def draw_page_number(self, page_count):
522        """draw string at bottom right with 'page x of y'.
523
524        Location of the string is determined by canvas attributes
525        `kofa_footer_x_pos` and `kofa_footer_y_pos` that have to be
526        set manually.
527
528        If this canvas also provides an attribute `kofa_footer_text`,
529        the contained text is rendered left of the ``page x of y``
530        string.
531        """
532        self.setFont("Helvetica", 9)
533        right_footer_text = _(
534            '${footer_text} Page ${num1} of ${num2}',
535            mapping = {'footer_text': self.kofa_footer_text,
536                       'num1':self._pageNumber, 'num2':page_count})
537        self.drawRightString(
538            self.kofa_footer_x_pos, self.kofa_footer_y_pos,
539             translate(right_footer_text))
540        return
541
542class PDFCreator(grok.GlobalUtility):
543    """A utility to help with generating PDF docs.
544    """
545    grok.implements(IPDFCreator)
546
547    watermark_path = None
548    header_logo_path = None
549    header_logo_left_path = None
550    watermark_pos = [0, 0]
551    logo_pos = [0, 0, 0]
552    logo_left_pos = [0, 0, 0]
553    pagesize = portrait(A4)
554
555    @classmethod
556    def _setUpWidgets(cls, form_fields, context):
557        """Setup simple display widgets.
558
559        Returns the list of widgets.
560        """
561        request = TestRequest()
562        return setUpEditWidgets(
563            form_fields, 'form', context, request, {},
564            for_display=True, ignore_request=True
565            )
566
567    @classmethod
568    def _addCourse(cls, table_data, row_num, course_label, course_link,
569                   lang, domain):
570        """Add course data to `table_data`.
571        """
572        if not course_label or not course_link:
573            return table_data, row_num
574        f_label= translate(
575            _(course_label), domain, target_language=lang)
576        f_label = Paragraph(f_label, ENTRY1_STYLE)
577        f_text = Paragraph(course_link, ENTRY1_STYLE)
578        table_data.append([f_label, f_text])
579        row_num += 1
580        return table_data, row_num
581
582    @classmethod
583    def _addDeptAndFaculty(cls, table_data, row_num, dept, faculty,
584                           lang, domain):
585        """Add `dept` and `faculty` as table rows to `table_data`.
586
587        `dept` and `faculty` are expected to be strings or None. In
588        latter case they are not put into the table.
589        """
590        for label, text in (('Department:', dept), ('Faculty:', faculty)):
591            if text is None:
592                continue
593            label = translate(_(label), domain, target_language=lang)
594            table_data.append([
595                Paragraph(label, ENTRY1_STYLE),
596                Paragraph(text, ENTRY1_STYLE)])
597            row_num += 1
598        return table_data, row_num
599
600    @classmethod
601    def _drawSignatureBoxes(cls, canvas, width, height, signatures=[]):
602        """Draw signature boxes into canvas.
603        """
604        canvas.saveState()
605        canvas.setFont("Helvetica", 10)
606        mystring = "%r" % ([translate(sig) for sig in signatures])
607        for num, sig in enumerate(signatures):
608            box_width = (width - 4.2*cm) / len(signatures)
609            x_box = 2.1*cm + (box_width) * num
610            y_box = 0.75*inch
611            canvas.rect(x_box+0.1*cm, y_box, box_width-0.2*cm, 0.75*inch)
612            canvas.drawString(
613                x_box+0.2*cm, 1.35*inch, translate(sig))
614        canvas.restoreState()
615        return canvas
616
617    @classmethod
618    def fromStringList(cls, string_list):
619        """Generate a list of reportlab paragraphs out of a list of strings.
620
621        Strings are formatted with :data:`CODE_STYLE` and a spacer is
622        appended at end.
623        """
624        result = []
625        for msg in string_list:
626            result.append(Paragraph(msg, CODE_STYLE))
627        return result
628
629    @classmethod
630    def getImage(cls, image_path, orientation='LEFT'):
631        """Get an image located at `image_path` as reportlab flowable.
632        """
633        img = Image(image_path, width=4*cm, height=3*cm, kind='bound')
634        img.hAlign=orientation
635        return img
636
637    def _getWidgetsTableData(self, widgets, separators, domain,
638                             lang, twoDataCols):
639        row_num = 0
640        table_data = []
641        for widget in widgets:
642            if separators and separators.get(widget.name):
643                f_headline = translate(
644                    separators[widget.name], domain, target_language=lang)
645                f_headline = Paragraph(f_headline, HEADLINE1_STYLE)
646                table_data.append([f_headline])
647                row_num += 1
648            f_label = translate(widget.label.strip(), domain,
649                                target_language=lang)
650            f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
651            f_text = translate(widget(), domain, target_language=lang)
652            f_text = format_html(f_text)
653            if f_text:
654                hint = ' <font size=9>' + widget.hint + '</font>'
655                f_text = f_text + hint
656            f_text = Paragraph(f_text, ENTRY1_STYLE)
657            table_data.append([f_label,f_text])
658            row_num += 1
659        return table_data, row_num
660
661    def getWidgetsTable(self, form_fields, context, view, lang='en',
662                        domain='waeup.kofa', separators=None,
663                        course_label=None, course_link=None, dept=None,
664                        faculty=None, colWidths=None, twoDataCols=False):
665        """Return a reportlab `Table` instance, created from widgets
666        determined by `form_fields` and `context`.
667
668        - `form_fields`
669           is a list of schema fields as created by grok.AutoFields.
670        - `context`
671           is some object whose content is rendered here.
672        - `view`
673           is currently not used but supposed to be a view which is
674           actually rendering a PDF document.
675        - `lang`
676           the portal language. Used for translations of strings.
677        - `domain`
678           the translation domain used for translations of strings.
679        - `separators`
680           a list of separators.
681        - `course_label` and `course_link`
682           if a course should be added to the table, `course_label`
683           and `course_link` can be given, both being strings. They
684           will be rendered in an extra-row.
685        - `dept` and `faculty`
686           if these are given, we render extra rows with faculty and
687           department.
688        - `colWidths`
689           defines the the column widths of the data in the right column
690           of base data (right to the passport image).
691        - `twoDataCols`
692           renders data widgets in a parent table with two columns.
693        """
694        table_style = [('VALIGN', (0,0), (-1,-1), 'TOP'),
695                       ]
696        widgets = self._setUpWidgets(form_fields, context)
697
698        # Determine table data
699        table_data, row_num = self._getWidgetsTableData(
700            widgets, separators, domain, lang, twoDataCols)
701
702        # Add course (admitted, etc.) if applicable
703        table_data, row_num = self._addCourse(
704            table_data, row_num, course_label, course_link, lang, domain,)
705
706        ## Add dept. and faculty if applicable
707        table_data, row_num = self._addDeptAndFaculty(
708            table_data, row_num, dept, faculty, lang, domain)
709
710        # render two-column table of tables if applicable
711        lines = len(table_data)
712        middle = lines/2
713        if twoDataCols is True and lines > 2:
714            table_left = Table(table_data[:middle],
715                               style=table_style, colWidths=[3*cm, 6.3*cm])
716            table_right = Table(table_data[middle:],
717                                style=table_style, colWidths=[3*cm, 6.3*cm])
718            table_style.append(('LEFTPADDING', (0,0), (0,-1), 1.2*cm),)
719            table = Table([[table_left, table_right],],style=table_style)
720            return table
721
722        # render single table
723        table = Table(
724            table_data,style=table_style, colWidths=colWidths) #, rowHeights=14)
725        table.hAlign = 'LEFT'
726        return table
727
728
729    def paint_background(self, canvas, doc):
730        """Paint background of a PDF, including watermark, title, etc.
731
732        The `doc` is expected to be some reportlab SimpleDocTemplate
733        or similar object.
734
735        Text of headerline is extracted from doc.kofa_headtitle, the
736        document title (under the head) from doc.kofa_title.
737
738        This is a callback method that will be called from reportlab
739        when creating PDFs with :meth:`create_pdf`.
740        """
741        canvas.saveState()
742        width, height = doc.width, doc.height
743        width += doc.leftMargin + doc.rightMargin
744        height += doc.topMargin + doc.bottomMargin
745
746        # Watermark
747        if self.watermark_path is not None:
748            canvas.saveState()
749            canvas.drawImage(self.watermark_path,
750                self.watermark_pos[0], self.watermark_pos[1])
751            canvas.restoreState()
752
753        # Header
754        site_config = None
755        site = grok.getSite()
756        if site is not None:
757            site_config = site.get('configuration', None)
758        head_title = getattr(
759            doc, 'kofa_headtitle', getattr(
760                site_config, 'name',
761                u'Sample University'))
762        canvas.setFont("Helvetica-Bold", 18)
763        if self.header_logo_left_path is not None:
764            canvas.drawCentredString(width/2.0, height-1.7*cm, head_title)
765        else:
766            canvas.drawString(1.5*cm, height-1.7*cm, head_title)
767        canvas.line(1.5*cm,height-1.9*cm,width-1.5*cm,height-1.9*cm)
768        if self.header_logo_path is not None:
769            canvas.drawImage(self.header_logo_path,
770                self.logo_pos[0], self.logo_pos[1], width=self.logo_pos[2],
771                preserveAspectRatio=True, anchor='ne')
772        if self.header_logo_left_path is not None:
773            canvas.drawImage(self.header_logo_left_path,
774                self.logo_left_pos[0], self.logo_left_pos[1],
775                width=self.logo_left_pos[2],
776                preserveAspectRatio=True, anchor='ne')
777
778        # Title
779        canvas.saveState()
780        canvas.setFont("Helvetica-Bold", 14)
781        title = getattr(doc, 'kofa_title', '')
782        if '\n' in title:
783            title_lines = title.split('\n')
784            for num, line in enumerate(title_lines):
785                canvas.drawCentredString(
786                    width/2.0, height-2.8*cm-(num*0.7*cm), line)
787        elif title:
788            canvas.drawCentredString(width/2.0, height-2.8*cm, title)
789        canvas.restoreState()
790
791        # Footer
792        canvas.saveState()
793        if getattr(doc, 'sigs_in_footer', False):
794            self._drawSignatureBoxes(
795                canvas, width, height, doc.sigs_in_footer)
796        canvas.line(2.2*cm, 0.62*inch, width-2.2*cm, 0.62*inch)
797        canvas.setFont("Helvetica", 9)
798        if not getattr(doc, 'kofa_nodate', False):
799            tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
800            #tz = getUtility(IKofaUtils).tzinfo
801            today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
802            canvas.drawString(2.2*cm, 0.5 * inch,
803                translate(_(u'Date: ${a}', mapping = {'a': today})))
804        # set canves attributes needed to render `page x of y`
805        canvas.kofa_footer_x_pos = width-2.2*cm
806        canvas.kofa_footer_y_pos = 0.5 * inch
807        canvas.kofa_footer_text =  doc.kofa_footer
808        canvas.restoreState()
809        canvas.restoreState()
810
811        # Metadata
812        canvas.setAuthor(getattr(doc, 'kofa_author', 'Unknown'))
813        canvas.setSubject(title)
814        canvas.setCreator(u'WAeUP Kofa')
815        return
816
817    def create_pdf(self, data, headerline=None, title=None, author=None,
818                   footer='', note=None, sigs_in_footer=[], topMargin=1.5):
819        """Returns a binary data stream which is a PDF document.
820        """
821        pdf_stream = StringIO()
822        bottomMargin = len(sigs_in_footer) and 1.9*inch or 1.2*inch
823        doc = SimpleDocTemplate(
824            pdf_stream,
825            bottomMargin=bottomMargin,
826            topMargin=topMargin*inch,
827            title=title,
828            pagesize=self.pagesize,
829            showBoundary=False,
830            )
831        # Set some attributes that are needed when rendering the background.
832        if headerline is not None:
833            doc.kofa_headtitle = headerline
834        doc.kofa_title = title
835        doc.kofa_author = author
836        doc.kofa_footer = footer
837        doc.sigs_in_footer = sigs_in_footer
838        if note is not None:
839            html = format_html(note)
840            data.append(Paragraph(html, NOTE_STYLE))
841        doc.build(data, onFirstPage=self.paint_background,
842                  onLaterPages=self.paint_background,
843                  canvasmaker=NumberedCanvas
844                  )
845        result = pdf_stream.getvalue()
846        pdf_stream.close()
847        return result
848
849class LandscapePDFCreator(PDFCreator):
850    """A utility to help with generating PDF docs in
851    landscape format.
852    """
853    grok.name('landscape')
854    pagesize = landscape(A4)
855
856def get_qrcode(text, width=60.0):
857    """Get a QR Code as Reportlab Flowable (actually a `Drawing`).
858
859    `width` gives box width in pixels (I think)
860    """
861    widget = QrCodeWidget(text)
862    bounds = widget.getBounds()
863    w_width = bounds[2] - bounds[0]
864    w_height = bounds[3] - bounds[1]
865    drawing = Drawing(
866        width, width,
867        transform=[width/w_width, 0, 0, width/w_height, 0, 0])
868    drawing.add(widget)
869    return drawing
Note: See TracBrowser for help on using the repository browser.