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