# coding: utf-8
"""
PDF output
"""
from reportlab import platypus as rlpp
from reportlab.lib.units import inch
from reportlab.lib import pagesizes, styles
from cStringIO import StringIO
import itertools
import logging
import datetime
log = logging.getLogger('svv.pdf')
class Styles (object) :
"""
Simple stylesheet
"""
samplestyles = styles.getSampleStyleSheet()
# normal text
h1 = styles.ParagraphStyle('Heading1', samplestyles['h1'],
fontName = 'Times-Bold',
fontSize = 22,
spaceBefore = 0,
spaceAfter = 0,
)
h2 = styles.ParagraphStyle('Heading2', samplestyles['h2'],
fontName = 'Times-Bold',
fontSize = 14,
spaceBefore = 6,
spaceAfter = 0,
)
h3 = styles.ParagraphStyle('Heading3', samplestyles['h3'],
fontName = 'Times-Italic',
fontSize = 12,
spaceBefore = 0,
spaceAfter = 0,
)
text = styles.ParagraphStyle('Text', samplestyles['Normal'],
)
# list indent level
list_indent = inch / 4
# root title
list_h1 = styles.ParagraphStyle('ListHeading1', samplestyles['h1'],
bulletIndent = 0,
leftIndent = 0,
)
# section
list_h2 = styles.ParagraphStyle('ListHeading2', samplestyles['h2'],
bulletIndent = 0,
leftIndent = list_indent,
fontName = 'Times-Bold',
fontSize = 10,
leading = 12,
spaceBefore = 6,
spaceAfter = 0,
)
# segment
list_h3 = styles.ParagraphStyle('ListHeading3', samplestyles['h3'],
bulletIndent = 0,
leftIndent = list_indent,
fontName = 'Times-Italic',
fontSize = 10,
leading = 12,
spaceBefore = 0,
spaceAfter = 0,
)
# infot
list_text = styles.ParagraphStyle('ListText', samplestyles['Normal'],
bulletIndent = 0,
leftIndent = list_indent,
)
class ListItem (object) :
"""
Indented/nested list
"""
@classmethod
def seq (cls) :
"""
List numbering.
Fixed as numeric only for now
"""
for idx in itertools.count(1) :
yield "%d." % (idx, )
def __init__ (self, title, title_style, text, subseq=None, sublist=None,
text_style=Styles.list_text, indent=Styles.list_indent) :
"""
title - title to display as first line
title_style - paragraph style for title line
text - multi-line texto to display on first or previous lines
subseq - sequence of bullet-texts to use for items in sublist
sublist - sequence of sub-nodes
text_style - paragraph style for text lines
indent - indentation for text and sub-lists
"""
self.title = title
self.title_style = title_style
self.text = text
self.subseq = subseq
self.sublist = sublist
self.text_style = text_style
self.indent = indent
def render_pdf (self, bullet=None) :
"""
Yield a series of PDF flowables for this list node and sub-nodes, useable for pdf.DocTemplateBase.build()
bullet - bullet text to use for this item's paragraph
"""
# first line, with possible bullet
if self.title :
yield rlpp.Paragraph(self.title, self.title_style, bullet)
elif self.text :
yield rlpp.Paragraph(self.text, self.text_style, bullet)
# indented text after title
if self.title and self.text :
yield rlpp.Paragraph(self.text, self.text_style)
if self.sublist :
# following lines, indented
yield rlpp.Indenter(self.indent)
# sub-items
for item in self.sublist :
# get (optional) bullet for item
bullet = next(self.subseq, None) if self.subseq else None
# render item as series of elements
for element in item.render_pdf(bullet) :
yield element
# de-dent
yield rlpp.Indenter(-self.indent)
class SignatureBlock (rlpp.Flowable) :
"""
A signature block, with multiple sets of multiple pre-fillied fields.
"""
# vertical space per field
FIELD_HEIGHT = 2 * inch / 4
# horizontal offset from top of field to field line
FIELD_OFFSET = FIELD_HEIGHT / 2
# maximum width to scale columns to
COL_WIDTH_MAX = 4 * inch
# empty space to leave below the fields
PADDING_BOTTOM = inch / 2
def __init__ (self, cols, fields, values={}, fullheight=None) :
"""
cols - Column titles
fields - Fields titles, describing the horizontal fields
values - Pre-filled values as a {(col, field): value} dict
fullheight - Consume full vertical height to place at bottom of page
desc/value strings can contain formatting codes:
column - title of the current column
today - today's date in %d/%m/%Y format
"""
self.cols = cols
self.fields = fields
self.values = values
self.fullheight = fullheight
def wrap (self, width, height) :
"""
Calculate how much space we use up, returning (width, height)
"""
self.width = width
self.height = len(self.fields) * self.FIELD_HEIGHT
if self.fullheight :
# consume all available height, to place us at the bottom
self.height = max(self.height, height)
return self.width, self.height
def formatString (self, text, col_title) :
"""
Format display string using context parameters
"""
# XXX: the canvas.drawString we use here does support unicode?
return text % dict(
column = col_title,
today = datetime.date.today().strftime("%d/%m/%Y"),
)
# text above field line
VALUE_FONT = "Courier-Bold"
VALUE_FONT_SIZE = 14
VALUE_OFFSET = inch / 12
# text below field line
TITLE_FONT = "Times-Italic"
TITLE_FONT_SIZE = 10
TITLE_OFFSET = inch / 8
def draw (self) :
"""
Render full block onto our canvas
"""
# target canvas
canvas = self.canv
col_width = min(self.width / len(self.cols), self.COL_WIDTH_MAX)
col_margin = col_width * 0.1
col_height = len(self.fields) * self.FIELD_HEIGHT + self.PADDING_BOTTOM
for field_idx, (field_title) in enumerate(self.fields) :
h = self.FIELD_HEIGHT
y = col_height - h * (field_idx + 1)
for col_idx, (col_title) in enumerate(self.cols) :
w = col_width
x = w * col_idx
value = self.values.get((col_title, field_title))
title = field_title
value = self.formatString(value, col_title) if value else None
title = self.formatString(title, col_title) if title else None
if value :
canvas.setFont(self.VALUE_FONT, self.VALUE_FONT_SIZE)
canvas.drawString(x + col_margin + self.VALUE_OFFSET, y - self.FIELD_OFFSET + 2, value)
# field line
canvas.line(x + col_margin, y - self.FIELD_OFFSET, x + w - col_margin, y - h / 2)
# desc text
canvas.setFont(self.TITLE_FONT, self.TITLE_FONT_SIZE)
canvas.drawString(x + col_margin + self.TITLE_OFFSET, y - self.FIELD_OFFSET - self.TITLE_FONT_SIZE, title)
class PageTemplate (rlpp.PageTemplate) :
"""
A single-frame page with header and footer.
"""
# vertical space available for footer/header, fixed because we can't really flow text vertically
HEADER_HEIGHT = 1 * inch
FOOTER_HEIGHT = 1 * inch
COL_FONT_SIZE = 8
COL_TITLE_FONT_NAME, COL_TITLE_FONT_SIZE = COL_TITLE_FONT = ("Times-Bold", COL_FONT_SIZE)
COL_TEXT_FONT_NAME, COL_TEXT_FONT_SIZE = COL_TEXT_FONT = ("Times-Roman", COL_FONT_SIZE)
def __init__ (self, id='page', page_size=pagesizes.A4, margin=inch, header_columns=None, footer_columns=None) :
"""
id - identifier for this page template
page_size - the (width, height) of this page
margin - the base margin to use between elements
header_columns - (title, text) list of header columns
footer_columnss - (title, text) list of footer columns
"""
self.page_width, self.page_height = self.page_size = page_size
self.margin = margin
self.header_height = self.HEADER_HEIGHT
self.footer_height = self.FOOTER_HEIGHT
# calculate frame
self.frame_left = self.margin
self.frame_bottom = self.footer_height + self.margin / 2
self.frame_top = self.page_height - self.header_height - self.margin / 2
self.frame_right = self.page_width - self.margin
self.frame = rlpp.Frame(self.frame_left, self.frame_bottom, self.frame_right - self.frame_left, self.frame_top - self.frame_bottom)
# init base template
rlpp.PageTemplate.__init__(self, id, frames=[self.frame])
self.header_columns = header_columns
self.footer_columns = footer_columns
def fmt_string (self, text) :
"""
Prepare a string for display by handling format codes
"""
return unicode(text % dict(
today = datetime.date.today().strftime("%d / %m / %Y"),
))
def draw_column (self, canvas, x, y, width, title, lines, gray=None) :
"""
Draw a column in the specified position, with the specified lines of text
"""
text = canvas.beginText(x, y)
# grayscale text?
if gray :
text.setFillGray(gray)
# title
text.setFont(*self.COL_TITLE_FONT)
text.textLine(self.fmt_string(title))
# lines
text.setFont(*self.COL_TEXT_FONT)
# XXX: textobject's textLine fails at unicode, but textLine should work...
for line in lines :
text.textLine(self.fmt_string(line))
# draw out
canvas.drawText(text)
def draw_columns (self, canvas, x, y, width, columns, **opts) :
"""
Draw a series of columns in the specified position
"""
col_count = len(columns)
col_width = width / col_count
x_base = x
for col_idx, (col_data) in enumerate(columns) :
x = x_base + (col_idx * col_width)
# draw column data at correct offset inside our space
self.draw_column(canvas, x, y - self.COL_FONT_SIZE, col_width, *col_data, **opts)
def draw_header (self, canvas) :
"""
Draw page header
"""
# offsets
x = self.margin
h = self.footer_height - self.margin / 4
w = self.page_width - self.margin * 2
# spacer
y = self.page_height - self.footer_height
canvas.setLineWidth(0.5)
canvas.line(x - self.margin / 2, y, self.page_width - self.margin / 2, y)
# columns
y = self.page_height - self.margin / 4
self.draw_columns(canvas, x, y, w, self.header_columns)
def draw_footer (self, canvas) :
"""
Draw page footer
"""
# offsets
x = self.margin
y = self.footer_height
w = self.page_width - self.margin * 2
# spacer
canvas.setLineWidth(0.5)
canvas.line(x - self.margin / 2, y, self.page_width - self.margin / 2, y)
# columns
self.draw_columns(canvas, x, y - inch / 8, w, self.footer_columns, gray=0.4)
def beforeDrawPage (self, canvas, document) :
"""
Draw page headers/footers
"""
self.draw_header(canvas)
self.draw_footer(canvas)
class DocumentTemplate (rlpp.BaseDocTemplate) :
def __init__ (self, page_templates, title, author, page_size=pagesizes.A4) :
"""
Initialize with fixed list of needed PageTemplates.
"""
# we supply the file later
rlpp.BaseDocTemplate.__init__(self, filename=None,
pageTemplates=page_templates, title=title, author=author,
pageSize=page_size
)
def render_buf (self, elements) :
"""
Build the document using the given list of Flowables, returning a StringIO containing the PDF.
"""
buf = StringIO()
# build
self.build(elements, buf)
# prepare for read
buf.seek(0)
return buf
def render_string (self, elements) :
"""
Build the document using the given list of Flowables, returning the PDF as a single str.
"""
# render
buf = self.render_buf(elements)
# binary data out
return buf.getvalue()