# -*- coding: utf-8 -*-
## $Id: pdf.py 17816 2024-06-14 10:11:23Z henrik $
##
## 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
import pytz
from string import Template
from cStringIO import StringIO
from datetime import datetime
from reportlab.graphics.barcode.qr import QrCodeWidget
from reportlab.graphics.shapes import Drawing
from reportlab.lib import colors
from reportlab.lib.units import cm, inch, mm
from reportlab.lib.pagesizes import A4, A3, landscape, portrait
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import (
SimpleDocTemplate, Spacer, Paragraph, Image, Table)
from zope.formlib.form import setUpEditWidgets
from zope.i18n import translate
from zope.publisher.browser import TestRequest
from zope.component import getUtility, queryUtility
from waeup.kofa.browser.interfaces import IPDFCreator
from waeup.kofa.utils.helpers import now
from waeup.kofa.interfaces import IKofaUtils
from waeup.kofa.interfaces import MessageFactory as _
#: A reportlab paragraph style for 'normal' output.
NORMAL_STYLE = getSampleStyleSheet()['Normal']
#: A reportlab paragraph style for 'normal' output.
HEADING3_STYLE = getSampleStyleSheet()['Heading3']
#: A reportlab paragraph style for headings.
HEADING_STYLE = ParagraphStyle(
name='Heading3',
parent=HEADING3_STYLE,
fontSize=11,
)
#: A reportlab paragraph style for output of 'code'.
CODE_STYLE = ParagraphStyle(
name='Code',
parent=NORMAL_STYLE,
fontName='Courier',
fontSize=10,
leading=10,
)
#: A reportlab paragraph style for regular form output.
ENTRY1_STYLE = ParagraphStyle(
name='Entry1',
parent=NORMAL_STYLE,
fontSize=11,
leading=10,
)
#: A reportlab paragraph style for smaller form output.
SMALL_PARA_STYLE = ParagraphStyle(
name='Small1',
parent=NORMAL_STYLE,
fontSize=8,
)
#: A reportlab paragraph style for headlines or bold text in form output.
HEADLINE1_STYLE = ParagraphStyle(
name='Header1',
parent=NORMAL_STYLE,
fontName='Helvetica-Bold',
fontSize=11,
)
#: A reportlab paragraph style for notes output at end of documents.
NOTE_STYLE = ParagraphStyle(
name='Note',
parent=NORMAL_STYLE,
fontName='Helvetica',
fontSize=11,
#leading=9,
)
#: Base style for signature tables
SIGNATURE_TABLE_STYLE = [
('VALIGN',(0,-1),(-1,-1),'TOP'),
#('FONT', (0,0), (-1,-1), 'Helvetica-BoldOblique', 12),
('BOTTOMPADDING', (0,0), (-1,0), 36),
('TOPPADDING', (0,-1), (-1,-1), 0),
]
[docs]def normalize_signature(signature_tuple):
"""Normalize a signature tuple.
Returns a tuple ``(<PRE-TEXT>, <SIGNATURE>, <POST-TEXT>)`` from
input tuple. The following rules apply::
(pre, sig, post) --> (pre, sig, post)
(pre, sig) --> (pre, sig, None)
(sig) --> (None, sig, None)
Also simple strings are accepted as input::
sig --> (None, sig, None)
If input is not a tuple nor a basestring or if the tuple contains
an invalid number of elements, ``ValueError`` is raised.
"""
if isinstance(signature_tuple, basestring):
return (None, signature_tuple, None)
if not isinstance(signature_tuple, tuple):
raise ValueError("signature_tuple must be a string or tuple")
if len(signature_tuple) < 1 or len(signature_tuple) > 3:
raise ValueError("signature_tuple must have 1, 2, or 3 elements")
elif len(signature_tuple) == 1:
signature_tuple = (None, signature_tuple[0], None)
elif len(signature_tuple) == 2:
signature_tuple = (signature_tuple[0], signature_tuple[1], None)
return signature_tuple
[docs]def vert_signature_cell(signature, date_field=True, date_text=_('Date:'),
start_row=0, start_col=0, underline=True):
"""Generate a table part containing a vertical signature cell.
Returns the table data as list of lists and an according style.
`signature`:
a signature tuple containing (<PRE-TEXT, SIGNATURE-TEXT, POST-TEXT>)
`date_field`:
boolean indicating that a 'Date:' text should be rendered into this
signature cell (or not).
`date_text`:
the text to be rendered into the signature field as 'Date:' text.
`start_row`:
starting row of the signature cell inside a broader table.
`start_col`:
starting column of the signature cell inside a broader table.
`underline`:
boolean indicating that the signature cell should provide a line on
top (`True` by default).
Vertical signature cells look like this::
+------------+
|Pre |
+------------+
|Date: |
| |
+------------+
| ---------- |
| Signature |
+------------+
|Post |
+------------+
"""
# split signature parts, replacing None with empty string
pre, sig, post = [x or '' for x in signature]
style = ()
x, y = start_col, start_row+2
if underline:
style += (('LINEABOVE', (x, y), (x, y), 1, colors.black),)
d_text = date_field and date_text or ''
data = [[pre], [d_text], [sig], [post]]
col_widths = [1.0]
return data, style, col_widths
[docs]def horiz_signature_cell(signature, date_field=True, date_text=_('Date'),
start_row=0, start_col=0):
"""Generate a table part containing an horizontal signature cell
Returns the table data as list of lists and an according style.
`signature`:
a signature tuple containing (<PRE-TEXT, SIGNATURE-TEXT, POST-TEXT>)
`date_field`:
boolean indicating that a 'Date:' text should be rendered into this
signature cell (or not).
`date_text`:
the text to be rendered into the signature field as 'Date:' text.
`start_row`:
starting row of the signature cell inside a broader table.
`start_col`:
starting column of the signature cell inside a broader table.
Horizontal signature cells look like this::
+------------+---+-----------+
|Pre text possibly filling |
|the whole box |
+------------+---+-----------+
| | | |
| | | |
+------------+---+-----------+
| ---------- | | --------- |
| Date | | Signature |
+------------+---+-----------+
|Post |
+------------+---+-----------+
"""
pre, sig, post = signature
if not date_field:
data, style, cols = vert_signature_cell(signature, date_field=False)
return data, style, cols
style = (
# let pre and post text span the whole signature cell
('SPAN', (start_col, start_row), (start_col+2, start_row)),
('SPAN', (start_col, start_row+3), (start_col+2, start_row+3)),
)
# horizontal cells are buildt from vertical ones chained together
cell1 = vert_signature_cell( # leftmost date col
(pre, date_text, post), date_field=False,
start_row=start_row, start_col=start_col)
cell2 = vert_signature_cell( # spacer col (between date and sig)
('', '', ''), date_field=False, underline=False,
start_row=start_row, start_col=start_col+1)
cell3 = vert_signature_cell( # rightmost signature column
('', sig, ''), date_field=False,
start_row=start_row, start_col=start_col+2)
data = map(lambda x, y, z: x+y+z, cell1[0], cell2[0], cell3[0])
style = style + cell1[1] + cell2[1] + cell3[1]
col_widths = [0.3, 0.03, 0.67] # sums up to 1.0
return data, style, col_widths
[docs]def signature_row(signatures, start_row=0, horizontal=None, max_per_row=3):
data = [[], [], [], []]
style = ()
signatures = [normalize_signature(sig) for sig in signatures]
start_col = 0
col_widths = []
if horizontal is None:
horizontal = len(signatures) == 1
cell_maker = vert_signature_cell
if horizontal:
cell_maker = horiz_signature_cell
main_cell_height = not horizontal and 36 or 18
for sig in signatures:
sig_data, sig_style, sig_cols = cell_maker(
sig, start_row=start_row, start_col=start_col)
data = map(lambda x, y: x+y, data, sig_data)
style += sig_style
col_widths += sig_cols + [None,]
start_col += 1
# add spacer
spacer, spacer_style, cols = vert_signature_cell(
('', '', ''), date_field=False, underline=False,
start_row=start_row, start_col=start_col)
data = map(lambda x, y: x+y, data, spacer)
style += spacer_style
start_col += 1
y = start_row
sig_row = start_row + 2
style = style + (
('TOPPADDING', (0, y+2), (-1, y+2), 0), # signature row
('BOTTOMPADDING', (0, y+1), (-1, y+1), main_cell_height),
('LEFTPADDING', (0, y), (-1, y), 1), # pre row
('LEFTPADDING', (0, y+3), (-1, y+3), 1), # post row
)
if len(signatures) == 1:
# pre and post text should span whole table
style += (('SPAN', (0, y), (-1, y)),
('SPAN', (0, y+3), (-1, y+3)),
)
if data[0] == [''] * len(data[0]):
# no pre text: hide pre row by minimizing padding
style += (('TOPPADDING', (0,y), (-1, y), -6),
('BOTTOMPADDING', (0,y), (-1, y), -6),
)
if data[-1] == [''] * len(data[0]):
# no post text: hide post row by minimizing padding
style += (('TOPPADDING', (0,y+3), (-1, y+3), -6),
('BOTTOMPADDING', (0,y+3), (-1, y+3), -6),
)
if len(signatures) > 1:
data = [x[:-1] for x in data] # strip last spacer
col_widths = col_widths[:-1]
return data, style, col_widths
[docs]def sig_table(signatures, lang='en', max_per_row=3, horizontal=None,
single_table=False, start_row=0, landscape=False):
if landscape:
space_width = 2.4 # width in cm of space between signatures
table_width = 24.0 # supposed width of signature table in cms
else:
space_width = 0.4 # width in cm of space between signatures
table_width = 16.0 # supposed width of signature table in cms
# width of signature cells in cm...
sig_num = len(signatures)
sig_col_width = (table_width - ((sig_num - 1) * space_width)) / sig_num
if sig_num == 1:
sig_col_width = 0.8 * table_width # signature cell part
space_width = table_width - sig_col_width # spacer part on the right
if sig_num > max_per_row:
if horizontal is None:
horizontal = max_per_row == 1
sigs_by_row = [signatures[x:x+max_per_row] for x in range(
0, sig_num, max_per_row)]
result = []
curr_row = 0
for num, row_sigs in enumerate(sigs_by_row):
curr_row = 0
if single_table:
curr_row = num * 4
result.append(
sig_table(row_sigs, lang=lang, max_per_row=max_per_row,
horizontal=horizontal, start_row=curr_row,
landscape=landscape)[0],
)
missing_num = len(result[-2][0][0]) - len(result[-1][0][0])
if missing_num:
# last row contained less cells, fix it...
result[-1] = ([x + [''] * missing_num for x in result[-1][0]],
result[-1][1], result[-2][2])
return result
data, style, cols = signature_row(signatures, horizontal=horizontal,
start_row=start_row)
style += (('VALIGN', (0,0), (-1,-1), 'TOP'),)
# compute col widths...
col_widths = []
for col in cols:
if col is not None:
col = col * sig_col_width * cm
else:
col = space_width * cm
col_widths.append(col)
# replace strings by paragraphs and translate all contents
for rnum, row in enumerate(data):
for cnum, cell in enumerate(row):
if cell:
content = translate(cell, lang)
data[rnum][cnum] = Paragraph(content, NORMAL_STYLE)
return [(data, style, col_widths),]
[docs]def get_sig_tables(signatures, lang='en', max_per_row=3, horizontal=None,
single_table=False, landscape=False):
rows = sig_table(signatures, lang=lang, max_per_row=max_per_row,
horizontal=horizontal, single_table=single_table,
landscape=landscape)
if single_table:
result_data = []
result_style = ()
for row in rows:
data, style, col_widths = row
result_data += data
result_style += style
return [(result_data, result_style, col_widths),]
return rows
[docs]def get_signature_tables(signatures, lang='en', max_per_row=3,
horizontal=None, single_table=False,
landscape=False):
"""Get a list of reportlab flowables representing signature fields.
`signatures` is a list of signatures. Each signature can be a
simple string or a tuple of format::
(<PRE-TEXT>, <SIGNATURE>, <POST-TEXT>)
where ``<PRE-TEXT>`` and ``<POST-TEXT>`` are texts that should
appear on top (PRE) or below (POST) the signature cell. Both
formats, string and tuple, can be mixed. A single signature would
be given as ``[('Pre-Text', 'Signature', 'Post-Text'),]`` or
simply as ``['Signature']`` if not pre or post-text is wanted.
All texts (pre, sig, post) are rendered as paragraphs, so you can
pass in also longer texts with basic HTML formatting like ``<b>``,
``<i>``, ``<br />``, etc.
``lang`` sets the language to use in I18n context. All texts are
translated to the given language (``en`` by default) if a
translation is available.
``max_per_row`` gives the maximum number of signatures to put into
a single row. The default is 3. If more signatures are passed in,
these signatures are put into a new row. So, for example by
default 8 signatures would be spread over 3 rows.
``horizontal`` tells how the single signature cells should be
rendered: horizontal or vertical. While horizontal cells render
date and signature fields side by side, in vertical cells date is
rendered on top of the signature.
This parameter accepts *three* different values: ``True``,
``False``, or ``None``. While with ``True`` each cell is rendered
in horizontal mode, ``False`` will create only vertical cells.
The ``None`` value (set by default) is different: if set, the mode
will be dependent on the number of signatures per row. If a row
contains exactly one signature (because only one sig was passed
in, or because ``max_per_row`` was set to ``1``), then this
signature is rendered in horizontal mode. Otherwise (with more
than one sig per row) each cell is rendered in vertical mode. This
pseudo-smart behaviour can be switched off by setting
``horizontal`` explicitly to ``True`` or ``False``.
``single_table`` is a boolean defaulting to ``False``. By default
we return the rows of a signature table in several tables, one of
each row. This makes it easier for reportlab to perform pagebreaks
in case the page is already full, without wasting space. If the
parameter is set to ``True``, then always a list with exactly one
table is returned, which will contain all rows in one table.
Generally, if a row contains only one signature, only a part of
the page width is used to render this signature. If two or more
signatures are passed in, the complete page width will be filled
and the single signature cells will be shrinked to fit.
"""
data_list = get_sig_tables(
signatures, lang=lang, max_per_row=max_per_row,
horizontal=horizontal, single_table=single_table, landscape=landscape)
return [Table(row_data, style=row_style, colWidths=row_col_widths,
repeatRows=4)
for row_data, row_style, row_col_widths in data_list]
[docs]class NumberedCanvas(Canvas):
"""A reportlab canvas for numbering pages after all docs are processed.
Taken from
http://code.activestate.com/recipes/546511-page-x-of-y-with-reportlab/
http://code.activestate.com/recipes/576832/
"""
[docs] def __init__(self, *args, **kw):
Canvas.__init__(self, *args, **kw)
self._saved_page_states = []
return
[docs] def showPage(self):
self._saved_page_states.append(dict(self.__dict__))
self._startPage()
return
[docs] def save(self):
"""add page info to each page (page x of y)"""
num_pages = len(self._saved_page_states)
for state in self._saved_page_states:
self.__dict__.update(state)
self.draw_page_number(num_pages)
Canvas.showPage(self)
Canvas.save(self)
return
[docs] def draw_page_number(self, page_count):
"""draw string at bottom right with 'page x of y'.
Location of the string is determined by canvas attributes
`kofa_footer_x_pos` and `kofa_footer_y_pos` that have to be
set manually.
If this canvas also provides an attribute `kofa_footer_text`,
the contained text is rendered left of the ``page x of y``
string.
"""
self.setFont("Helvetica", 9)
right_footer_text = _(
'${footer_text} Page ${num1} of ${num2}',
mapping = {'footer_text': self.kofa_footer_text,
'num1':self._pageNumber, 'num2':page_count})
self.drawRightString(
self.kofa_footer_x_pos, self.kofa_footer_y_pos,
translate(right_footer_text))
return
[docs]class PDFCreator(grok.GlobalUtility):
"""A utility to help with generating PDF docs.
"""
grok.implements(IPDFCreator)
watermark_path = None
header_logo_path = None
header_logo_left_path = None
watermark_pos = [0, 0]
logo_pos = [0, 0, 0]
logo_left_pos = [0, 0, 0]
pagesize = portrait(A4)
test_signature_img_path = os.path.join(
os.path.dirname(__file__), 'static', 'test_signature.png')
@classmethod
@classmethod
[docs] def _addCourse(cls, table_data, row_num, course_label, course_link,
lang, domain):
"""Add course data to `table_data`.
"""
if not course_label or not course_link:
return table_data, row_num
f_label= translate(
_(course_label), domain, target_language=lang)
f_label = Paragraph(f_label, ENTRY1_STYLE)
f_text = Paragraph(course_link, ENTRY1_STYLE)
table_data.append([f_label, f_text])
row_num += 1
return table_data, row_num
@classmethod
[docs] def _addDeptAndFaculty(cls, table_data, row_num, dept, faculty,
lang, domain):
"""Add `dept` and `faculty` as table rows to `table_data`.
`dept` and `faculty` are expected to be strings or None. In
latter case they are not put into the table.
"""
for label, text in (('Department:', dept), ('Faculty:', faculty)):
if text is None:
continue
label = translate(_(label), domain, target_language=lang)
table_data.append([
Paragraph(label, ENTRY1_STYLE),
Paragraph(text, ENTRY1_STYLE)])
row_num += 1
return table_data, row_num
@classmethod
[docs] def _drawSignatureBoxes(cls, canvas, width, height, signatures=[]):
"""Draw signature boxes into canvas.
"""
canvas.saveState()
canvas.setFont("Helvetica", 10)
mystring = "%r" % ([translate(sig) for sig in signatures])
for num, sig in enumerate(signatures):
box_width = (width - 4.2*cm) / len(signatures)
x_box = 2.1*cm + (box_width) * num
y_box = 0.75*inch
canvas.rect(x_box+0.1*cm, y_box, box_width-0.2*cm, 0.75*inch)
canvas.drawString(
x_box+0.2*cm, 1.35*inch, translate(sig))
canvas.restoreState()
return canvas
@classmethod
[docs] def fromStringList(cls, string_list):
"""Generate a list of reportlab paragraphs out of a list of strings.
Strings are formatted with :data:`CODE_STYLE` and a spacer is
appended at end.
"""
result = []
for msg in string_list:
result.append(Paragraph(msg, CODE_STYLE))
return result
@classmethod
[docs] def getImage(cls, image_path, orientation='LEFT'):
"""Get an image located at `image_path` as reportlab flowable.
"""
img = Image(image_path, width=4*cm, height=3*cm, kind='bound')
img.hAlign=orientation
return img
[docs] 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,
self.watermark_pos[0], self.watermark_pos[1])
canvas.restoreState()
# Header
site_config = None
site = grok.getSite()
if site is not None:
site_config = site.get('configuration', None)
head_title = getattr(
doc, 'kofa_headtitle', getattr(
site_config, 'name',
u'Sample University'))
canvas.setFont("Helvetica-Bold", 18)
if self.header_logo_left_path is not None:
canvas.drawCentredString(width/2.0, height-1.7*cm, head_title)
else:
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.9*cm)
if self.header_logo_path is not None:
canvas.drawImage(self.header_logo_path,
self.logo_pos[0], self.logo_pos[1], width=self.logo_pos[2],
preserveAspectRatio=True, anchor='ne')
if self.header_logo_left_path is not None:
canvas.drawImage(self.header_logo_left_path,
self.logo_left_pos[0], self.logo_left_pos[1],
width=self.logo_left_pos[2],
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()
if getattr(doc, 'sigs_in_footer', False):
self._drawSignatureBoxes(
canvas, width, height, doc.sigs_in_footer)
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):
tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
canvas.drawString(2.2*cm, 0.5 * inch,
translate(_(u'Date: ${a}', mapping = {'a': today})))
# set canves attributes needed to render `page x of y`
canvas.kofa_footer_x_pos = width-2.2*cm
canvas.kofa_footer_y_pos = 0.5 * inch
canvas.kofa_footer_text = doc.kofa_footer
canvas.restoreState()
# Metadata
canvas.setAuthor(getattr(doc, 'kofa_author', 'Unknown'))
canvas.setSubject(title)
canvas.setCreator(u'WAeUP Kofa')
return
[docs] def _substitute_markers(self, text):
"""If `text` contains some marker, substitute it.
Markers are standard library string template placeholders. See
https://docs.python.org/2/library/string.html and
`string.Template` for details. Roughly, placeholders look like
``${some_marker}``.
Valid markers:
``test_signature_img_path`` -- path to signature image stored in
`test_signature_img_path`` attribute.
"""
text = Template(text)
PDF_SIGNATURES = {
'test_signature_img_path': self.test_signature_img_path,
}
return text.safe_substitute(PDF_SIGNATURES)
[docs] def create_pdf(self, data, headerline=None, title=None, author=None,
footer='', note=None, sigs_in_footer=[], topMargin=1.5,
letterhead_path=None, view=None):
"""Returns a binary data stream which is a PDF document.
"""
pdf_stream = StringIO()
bottomMargin = len(sigs_in_footer) and 1.9*inch or 1.2*inch
if not letterhead_path:
topMargin += title.count('\n') * 0.2
doc = SimpleDocTemplate(
pdf_stream,
bottomMargin=bottomMargin,
topMargin=topMargin*inch,
title=title,
pagesize=self.pagesize,
showBoundary=False,
)
# Set some attributes that are needed when rendering the background.
if headerline is not None:
doc.kofa_headtitle = headerline
doc.kofa_title = title
doc.kofa_author = author
doc.kofa_footer = footer
doc.sigs_in_footer = sigs_in_footer
doc.letterhead_path = letterhead_path
# Forward view in case we need customized watermarks
self.view = view
if note is not None:
# Inject signature images
note = self._substitute_markers(note)
# Format html
html = format_html(note)
data.append(Paragraph(html, NOTE_STYLE))
doc.build(data, onFirstPage=self.paint_background,
onLaterPages=self.paint_background,
canvasmaker=NumberedCanvas
)
result = pdf_stream.getvalue()
pdf_stream.close()
return result
[docs]class LandscapePDFCreator(PDFCreator):
"""A utility to help with generating PDF docs in
landscape format.
"""
grok.name('landscape')
pagesize = landscape(A4)
[docs]class LetterPDFCreator(PDFCreator):
"""A utility to help with generating PDF docs with an original letterhead.
"""
grok.name('letter')
letterhead_pos = [0, 0]
[docs] def paint_background(self, canvas, doc):
"""Paint letterhead background of a PDF.
The `doc` is expected to be some reportlab SimpleDocTemplate
or similar object.
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
# Letterhead
if doc.letterhead_path is not None:
canvas.saveState()
canvas.drawImage(doc.letterhead_path,
self.letterhead_pos[0], self.letterhead_pos[1],
width=width, height=height)
canvas.restoreState()
# Footer
if getattr(doc, 'sigs_in_footer', False):
self._drawSignatureBoxes(
canvas, width, height, doc.sigs_in_footer)
#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):
tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
#canvas.drawString(2.2*cm, 0.5 * inch,
# translate(_(u'Date: ${a}', mapping = {'a': today})))
canvas.drawString(2.2*cm, 0.3 * inch,
translate(_(u'Date: ${a}', mapping = {'a': today})))
# set canves attributes needed to render `page x of y`
canvas.kofa_footer_x_pos = width-2.2*cm
#canvas.kofa_footer_y_pos = 0.5 * inch
canvas.kofa_footer_y_pos = 0.3 * inch
canvas.kofa_footer_text = doc.kofa_footer
canvas.restoreState()
# Metadata
title = getattr(doc, 'kofa_title', '')
canvas.setAuthor(getattr(doc, 'kofa_author', 'Unknown'))
canvas.setSubject(title)
canvas.setCreator(u'WAeUP Kofa')
return
[docs]class A3LandscapePDFCreator(LandscapePDFCreator):
"""A utility to help with generating PDF docs in
A3 landscape format. No watermark is shown.
"""
grok.name('A3landscape')
pagesize = landscape(A3)
[docs]def get_qrcode(text, width=60.0):
"""Get a QR Code as Reportlab Flowable (actually a `Drawing`).
`width` gives box width in pixels (I think)
"""
widget = QrCodeWidget(text)
bounds = widget.getBounds()
w_width = bounds[2] - bounds[0]
w_height = bounds[3] - bounds[1]
drawing = Drawing(
width, width,
transform=[width/w_width, 0, 0, width/w_height, 0, 0])
drawing.add(widget)
return drawing