## $Id: pdf.py 11954 2014-11-13 16:54:17Z 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 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, 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.ikoba.browser.interfaces import IPDFCreator
from waeup.ikoba.utils.helpers import now
from waeup.ikoba.interfaces import IIkobaUtils
from waeup.ikoba.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=10,
)
#: A reportlab paragraph style for notes output at end of documents.
NOTE_STYLE = ParagraphStyle(
name='Note',
parent=NORMAL_STYLE,
fontName='Helvetica',
fontSize=10,
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),
]
def format_html(html):
"""Make HTML code usable for use in reportlab paragraphs.
Main things fixed here:
If html code:
- remove newlines (not visible in HTML but visible in PDF)
- add
tags after
(as divs break lines in HTML but not in PDF)
If not html code:
- just replace newlines by
tags
"""
if '' in html:
# Add br tag if widgets contain div tags
# which are not supported by reportlab
html = html.replace('
', '
')
html = html.replace('\n', '')
else:
html = html.replace('\n', '
')
return html
def normalize_signature(signature_tuple):
"""Normalize a signature tuple.
Returns a tuple ``(, , )`` 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
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 ()
`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
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 ()
`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
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
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.6 * 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),]
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
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::
(, , )
where ```` and ```` 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 ````,
````, ``
``, 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]
def format_signatures(signatures, max_per_row=3, lang='en',
single_table=False,
date_field=True, date_text=_('Date'),
base_style=SIGNATURE_TABLE_STYLE):
result = []
signature_tuples = [normalize_signature(sig) for sig in signatures]
for pre, sig, post in signature_tuples:
row = []
if pre is not None:
row.append([
translate(pre, lang), '', '', ''])
row.append([
translate(_('Date'), lang), '',
translate(sig, lang), ''
])
if post is not None:
row.append([
translate(post, lang), '', '', ''])
result.append((row, base_style))
return result
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/
"""
def __init__(self, *args, **kw):
Canvas.__init__(self, *args, **kw)
self._saved_page_states = []
return
def showPage(self):
self._saved_page_states.append(dict(self.__dict__))
self._startPage()
return
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
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
`ikoba_footer_x_pos` and `ikoba_footer_y_pos` that have to be
set manually.
If this canvas also provides an attribute `ikoba_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.ikoba_footer_text,
'num1':self._pageNumber, 'num2':page_count})
self.drawRightString(
self.ikoba_footer_x_pos, self.ikoba_footer_y_pos,
translate(right_footer_text))
return
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)
@classmethod
def _setUpWidgets(cls, form_fields, context):
"""Setup simple display widgets.
Returns the list of widgets.
"""
request = TestRequest()
return setUpEditWidgets(
form_fields, 'form', context, request, {},
for_display=True, ignore_request=True
)
@classmethod
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
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
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
def _getWidgetsTableData(self, widgets, separators, domain,
lang, twoDataCols):
row_num = 0
table_data = []
for widget in widgets:
if separators and separators.get(widget.name):
f_headline = translate(
separators[widget.name], domain, target_language=lang)
f_headline = Paragraph(f_headline, HEADLINE1_STYLE)
table_data.append([f_headline])
row_num += 1
f_label = translate(widget.label.strip(), domain,
target_language=lang)
f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
f_text = translate(widget(), domain, target_language=lang)
f_text = format_html(f_text)
if f_text:
hint = ' ' + widget.hint + ''
f_text = f_text + hint
f_text = Paragraph(f_text, ENTRY1_STYLE)
table_data.append([f_label,f_text])
row_num += 1
return table_data, row_num
def getWidgetsTable(self, form_fields, context, view, lang='en',
domain='waeup.ikoba', separators=None,
colWidths=None, twoDataCols=False):
"""Return a reportlab `Table` instance, created from widgets
determined by `form_fields` and `context`.
- `form_fields`
is a list of schema fields as created by grok.AutoFields.
- `context`
is some object whose content is rendered here.
- `view`
is currently not used but supposed to be a view which is
actually rendering a PDF document.
- `lang`
the portal language. Used for translations of strings.
- `domain`
the translation domain used for translations of strings.
- `separators`
a list of separators.
- `colWidths`
defines the the column widths of the data in the right column
of base data (right to the passport image).
- `twoDataCols`
renders data widgets in a parent table with two columns.
"""
table_style = [('VALIGN', (0,0), (-1,-1), 'TOP'),
]
widgets = self._setUpWidgets(form_fields, context)
# Determine table data
table_data, row_num = self._getWidgetsTableData(
widgets, separators, domain, lang, twoDataCols)
# render two-column table of tables if applicable
lines = len(table_data)
middle = lines/2
if twoDataCols is True and lines > 2:
table_left = Table(table_data[:middle],
style=table_style, colWidths=[3*cm, 6.3*cm])
table_right = Table(table_data[middle:],
style=table_style, colWidths=[3*cm, 6.3*cm])
table_style.append(('LEFTPADDING', (0,0), (0,-1), 1.2*cm),)
table = Table([[table_left, table_right],],style=table_style)
return table
# render single table
table = Table(
table_data,style=table_style, colWidths=colWidths) #, rowHeights=14)
table.hAlign = 'LEFT'
return table
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.ikoba_headtitle, the
document title (under the head) from doc.ikoba_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, 'ikoba_headtitle', getattr(
site_config, 'name',
u'Sample Company'))
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, 'ikoba_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, 'ikoba_nodate', False):
tz = getattr(queryUtility(IIkobaUtils), 'tzinfo', pytz.utc)
#tz = getUtility(IIkobaUtils).tzinfo
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.ikoba_footer_x_pos = width-2.2*cm
canvas.ikoba_footer_y_pos = 0.5 * inch
canvas.ikoba_footer_text = doc.ikoba_footer
canvas.restoreState()
canvas.restoreState()
# Metadata
canvas.setAuthor(getattr(doc, 'ikoba_author', 'Unknown'))
canvas.setSubject(title)
canvas.setCreator(u'WAeUP Ikoba')
return
def create_pdf(self, data, headerline=None, title=None, author=None,
footer='', note=None, sigs_in_footer=[], topMargin=1.5):
"""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
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.ikoba_headtitle = headerline
doc.ikoba_title = title
doc.ikoba_author = author
doc.ikoba_footer = footer
doc.sigs_in_footer = sigs_in_footer
if note is not None:
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
class LandscapePDFCreator(PDFCreator):
"""A utility to help with generating PDF docs in
landscape format.
"""
grok.name('landscape')
pagesize = landscape(A4)
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