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

Last change on this file since 12774 was 12492, checked in by Henrik Bettermann, 10 years ago

Do not print hints on slips.

  • Property svn:keywords set to Id
File size: 30.0 KB
Line 
1## $Id: pdf.py 12492 2015-01-19 08:15:05Z henrik $
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.ikoba.browser.interfaces import IPDFCreator
40from waeup.ikoba.utils.helpers import now
41from waeup.ikoba.interfaces import IIkobaUtils
42from waeup.ikoba.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        `ikoba_footer_x_pos` and `ikoba_footer_y_pos` that have to be
526        set manually.
527
528        If this canvas also provides an attribute `ikoba_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.ikoba_footer_text,
536                       'num1':self._pageNumber, 'num2':page_count})
537        self.drawRightString(
538            self.ikoba_footer_x_pos, self.ikoba_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 _drawSignatureBoxes(cls, canvas, width, height, signatures=[]):
569        """Draw signature boxes into canvas.
570        """
571        canvas.saveState()
572        canvas.setFont("Helvetica", 10)
573        mystring = "%r" % ([translate(sig) for sig in signatures])
574        for num, sig in enumerate(signatures):
575            box_width = (width - 4.2*cm) / len(signatures)
576            x_box = 2.1*cm + (box_width) * num
577            y_box = 0.75*inch
578            canvas.rect(x_box+0.1*cm, y_box, box_width-0.2*cm, 0.75*inch)
579            canvas.drawString(
580                x_box+0.2*cm, 1.35*inch, translate(sig))
581        canvas.restoreState()
582        return canvas
583
584    @classmethod
585    def fromStringList(cls, string_list):
586        """Generate a list of reportlab paragraphs out of a list of strings.
587
588        Strings are formatted with :data:`CODE_STYLE` and a spacer is
589        appended at end.
590        """
591        result = []
592        for msg in string_list:
593            result.append(Paragraph(msg, CODE_STYLE))
594        return result
595
596    @classmethod
597    def getImage(cls, image_path, orientation='LEFT'):
598        """Get an image located at `image_path` as reportlab flowable.
599        """
600        img = Image(image_path, width=4*cm, height=3*cm, kind='bound')
601        img.hAlign=orientation
602        return img
603
604    def _getWidgetsTableData(self, widgets, separators, domain,
605                             lang, twoDataCols):
606        row_num = 0
607        table_data = []
608        for widget in widgets:
609            if separators and separators.get(widget.name):
610                f_headline = translate(
611                    separators[widget.name], domain, target_language=lang)
612                f_headline = Paragraph(f_headline, HEADLINE1_STYLE)
613                table_data.append([f_headline])
614                row_num += 1
615            f_label = translate(widget.label.strip(), domain,
616                                target_language=lang)
617            f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
618            f_text = translate(widget(), domain, target_language=lang)
619            f_text = format_html(f_text)
620            #if f_text:
621            #    hint = ' <font size=9>' + widget.hint + '</font>'
622            #    f_text = f_text + hint
623            f_text = Paragraph(f_text, ENTRY1_STYLE)
624            table_data.append([f_label,f_text])
625            row_num += 1
626        return table_data, row_num
627
628    def getWidgetsTable(self, form_fields, context, view, lang='en',
629                        domain='waeup.ikoba', separators=None,
630                        colWidths=None, twoDataCols=False):
631        """Return a reportlab `Table` instance, created from widgets
632        determined by `form_fields` and `context`.
633
634        - `form_fields`
635           is a list of schema fields as created by grok.AutoFields.
636        - `context`
637           is some object whose content is rendered here.
638        - `view`
639           is currently not used but supposed to be a view which is
640           actually rendering a PDF document.
641        - `lang`
642           the portal language. Used for translations of strings.
643        - `domain`
644           the translation domain used for translations of strings.
645        - `separators`
646           a list of separators.
647        - `colWidths`
648           defines the the column widths of the data in the right column
649           of base data (right to the passport image).
650        - `twoDataCols`
651           renders data widgets in a parent table with two columns.
652        """
653        table_style = [('VALIGN', (0,0), (-1,-1), 'TOP'),
654                       ]
655        widgets = self._setUpWidgets(form_fields, context)
656
657        # Determine table data
658        table_data, row_num = self._getWidgetsTableData(
659            widgets, separators, domain, lang, twoDataCols)
660
661        # render two-column table of tables if applicable
662        lines = len(table_data)
663        middle = lines/2
664        if twoDataCols is True and lines > 2:
665            table_left = Table(table_data[:middle],
666                               style=table_style, colWidths=[3*cm, 6.3*cm])
667            table_right = Table(table_data[middle:],
668                                style=table_style, colWidths=[3*cm, 6.3*cm])
669            table_style.append(('LEFTPADDING', (0,0), (0,-1), 1.2*cm),)
670            table = Table([[table_left, table_right],],style=table_style)
671            return table
672
673        # render single table
674        table = Table(
675            table_data,style=table_style, colWidths=colWidths) #, rowHeights=14)
676        table.hAlign = 'LEFT'
677        return table
678
679
680    def paint_background(self, canvas, doc):
681        """Paint background of a PDF, including watermark, title, etc.
682
683        The `doc` is expected to be some reportlab SimpleDocTemplate
684        or similar object.
685
686        Text of headerline is extracted from doc.ikoba_headtitle, the
687        document title (under the head) from doc.ikoba_title.
688
689        This is a callback method that will be called from reportlab
690        when creating PDFs with :meth:`create_pdf`.
691        """
692        canvas.saveState()
693        width, height = doc.width, doc.height
694        width += doc.leftMargin + doc.rightMargin
695        height += doc.topMargin + doc.bottomMargin
696
697        # Watermark
698        if self.watermark_path is not None:
699            canvas.saveState()
700            canvas.drawImage(self.watermark_path,
701                self.watermark_pos[0], self.watermark_pos[1])
702            canvas.restoreState()
703
704        # Header
705        site_config = None
706        site = grok.getSite()
707        if site is not None:
708            site_config = site.get('configuration', None)
709        head_title = getattr(
710            doc, 'ikoba_headtitle', getattr(
711                site_config, 'name',
712                u'Sample Company'))
713        canvas.setFont("Helvetica-Bold", 18)
714        if self.header_logo_left_path is not None:
715            canvas.drawCentredString(width/2.0, height-1.7*cm, head_title)
716        else:
717            canvas.drawString(1.5*cm, height-1.7*cm, head_title)
718        canvas.line(1.5*cm,height-1.9*cm,width-1.5*cm,height-1.9*cm)
719        if self.header_logo_path is not None:
720            canvas.drawImage(self.header_logo_path,
721                self.logo_pos[0], self.logo_pos[1], width=self.logo_pos[2],
722                preserveAspectRatio=True, anchor='ne')
723        if self.header_logo_left_path is not None:
724            canvas.drawImage(self.header_logo_left_path,
725                self.logo_left_pos[0], self.logo_left_pos[1],
726                width=self.logo_left_pos[2],
727                preserveAspectRatio=True, anchor='ne')
728
729        # Title
730        canvas.saveState()
731        canvas.setFont("Helvetica-Bold", 14)
732        title = getattr(doc, 'ikoba_title', '')
733        if '\n' in title:
734            title_lines = title.split('\n')
735            for num, line in enumerate(title_lines):
736                canvas.drawCentredString(
737                    width/2.0, height-2.8*cm-(num*0.7*cm), line)
738        elif title:
739            canvas.drawCentredString(width/2.0, height-2.8*cm, title)
740        canvas.restoreState()
741
742        # Footer
743        canvas.saveState()
744        if getattr(doc, 'sigs_in_footer', False):
745            self._drawSignatureBoxes(
746                canvas, width, height, doc.sigs_in_footer)
747        canvas.line(2.2*cm, 0.62*inch, width-2.2*cm, 0.62*inch)
748        canvas.setFont("Helvetica", 9)
749        if not getattr(doc, 'ikoba_nodate', False):
750            tz = getattr(queryUtility(IIkobaUtils), 'tzinfo', pytz.utc)
751            #tz = getUtility(IIkobaUtils).tzinfo
752            today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
753            canvas.drawString(2.2*cm, 0.5 * inch,
754                translate(_(u'Date: ${a}', mapping = {'a': today})))
755        # set canves attributes needed to render `page x of y`
756        canvas.ikoba_footer_x_pos = width-2.2*cm
757        canvas.ikoba_footer_y_pos = 0.5 * inch
758        canvas.ikoba_footer_text =  doc.ikoba_footer
759        canvas.restoreState()
760        canvas.restoreState()
761
762        # Metadata
763        canvas.setAuthor(getattr(doc, 'ikoba_author', 'Unknown'))
764        canvas.setSubject(title)
765        canvas.setCreator(u'WAeUP Ikoba')
766        return
767
768    def create_pdf(self, data, headerline=None, title=None, author=None,
769                   footer='', note=None, sigs_in_footer=[], topMargin=1.5):
770        """Returns a binary data stream which is a PDF document.
771        """
772        pdf_stream = StringIO()
773        bottomMargin = len(sigs_in_footer) and 1.9*inch or 1.2*inch
774        doc = SimpleDocTemplate(
775            pdf_stream,
776            bottomMargin=bottomMargin,
777            topMargin=topMargin*inch,
778            title=title,
779            pagesize=self.pagesize,
780            showBoundary=False,
781            )
782        # Set some attributes that are needed when rendering the background.
783        if headerline is not None:
784            doc.ikoba_headtitle = headerline
785        doc.ikoba_title = title
786        doc.ikoba_author = author
787        doc.ikoba_footer = footer
788        doc.sigs_in_footer = sigs_in_footer
789        if note is not None:
790            html = format_html(note)
791            data.append(Paragraph(html, NOTE_STYLE))
792        doc.build(data, onFirstPage=self.paint_background,
793                  onLaterPages=self.paint_background,
794                  canvasmaker=NumberedCanvas
795                  )
796        result = pdf_stream.getvalue()
797        pdf_stream.close()
798        return result
799
800class LandscapePDFCreator(PDFCreator):
801    """A utility to help with generating PDF docs in
802    landscape format.
803    """
804    grok.name('landscape')
805    pagesize = landscape(A4)
806
807def get_qrcode(text, width=60.0):
808    """Get a QR Code as Reportlab Flowable (actually a `Drawing`).
809
810    `width` gives box width in pixels (I think)
811    """
812    widget = QrCodeWidget(text)
813    bounds = widget.getBounds()
814    w_width = bounds[2] - bounds[0]
815    w_height = bounds[3] - bounds[1]
816    drawing = Drawing(
817        width, width,
818        transform=[width/w_width, 0, 0, width/w_height, 0, 0])
819    drawing.add(widget)
820    return drawing
Note: See TracBrowser for help on using the repository browser.