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@31: # default terom@31: default = styles.ParagraphStyle('Text', samplestyles['Normal'], terom@31: terom@31: ) terom@31: terom@4: # normal text terom@31: text = p = default terom@31: terom@31: # headers 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: 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@31: list_p = styles.ParagraphStyle('ListText', samplestyles['Normal'], terom@4: bulletIndent = 0, terom@4: leftIndent = list_indent, terom@4: ) terom@31: list_text = list_p terom@4: terom@31: # lookup styles by rule terom@31: # searched in order, first match against end of node_path wins terom@31: RULES = ( terom@31: ('li p', list_p), terom@31: ('li h1', list_h1), terom@31: ('li h2', list_h2), terom@31: ('li h3', list_h3), terom@31: terom@31: ('p', p), terom@31: ('h1', h1), terom@31: ('h2', h2), terom@31: ('h3', h3), terom@31: terom@31: ) terom@31: terom@4: @classmethod terom@31: def match (cls, name=None) : terom@4: """ terom@31: Return appropriate style. terom@4: """ terom@4: terom@31: if name : terom@31: return getattr(cls, name, cls.default) terom@4: terom@31: else : terom@31: return cls.default terom@4: terom@31: def lookup (self, node_path) : terom@4: """ terom@31: Return a suitable ParagraphStyle to use for a node terom@31: terom@31: node_path - tag-path to node (list of tags as ancestry of node) terom@4: """ terom@4: terom@31: # XXX: find a better hack terom@31: nodepath = ' '.join(node_path) terom@4: terom@31: for rule, style in self.RULES : terom@31: if nodepath.endswith(rule) : terom@31: return style terom@4: terom@31: # default terom@31: return self.default 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@23: def __init__ (self, cols, fields, values={}, fullheight=None) : terom@4: """ terom@23: cols - Column titles terom@23: fields - Fields titles, describing the horizontal fields terom@23: values - Pre-filled values as a {(col, field): value} dict terom@23: fullheight - Consume full vertical height to place at bottom of page 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@23: self.fullheight = fullheight 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@23: self.height = len(self.fields) * self.FIELD_HEIGHT terom@23: terom@23: if self.fullheight : terom@23: # consume all available height, to place us at the bottom terom@23: self.height = max(self.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@17: terom@17: # XXX: the canvas.drawString we use here does support unicode? 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@17: terom@17: return unicode(text % dict( terom@4: today = datetime.date.today().strftime("%d / %m / %Y"), 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@17: terom@17: # XXX: textobject's textLine fails at unicode, but textLine should work... terom@17: for line in lines : terom@17: text.textLine(self.fmt_string(line)) 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: terom@31: class Markup (object) : terom@31: """ terom@31: Generate Paragraphs from Markup docs. terom@31: """ terom@31: terom@31: def __init__ (self, styles) : terom@31: """ terom@31: Initialize render state. terom@31: terom@31: styles - Style instance to use terom@31: terom@31: This class is stateful and single-use. terom@31: terom@31: """ terom@31: terom@31: # elment stack terom@31: self.stack = [] terom@31: terom@31: # styles terom@31: self.styles = styles terom@31: terom@31: def lookup_style (self, elem=None) : terom@31: """ terom@31: Lookup style for given element, which must be at the top of the stack. terom@31: """ terom@31: terom@31: # path to element terom@31: path = self.stack terom@31: terom@31: if elem is not None and path[-1] is not elem : terom@31: # XXX: sub-elem? terom@31: path = path + [elem] terom@31: terom@31: # lookup terom@31: return self.styles.lookup([elem.tag for elem in path]) terom@31: terom@31: ## Basic text terom@31: def render_p (self, elem, bullet=None) : terom@31: """ terom@31: Normal paragraph. terom@31: terom@31: XXX: inline markup? terom@31: """ terom@31: terom@31: style = self.lookup_style(elem) terom@31: terom@31: yield rlpp.Paragraph(elem.text, style, bullet) terom@31: terom@31: def _render_hx (self, elem, bullet=None) : terom@31: """ terom@31: Render given h1/h2/h3 element terom@31: """ terom@31: terom@31: # get style for tag terom@31: style = self.lookup_style(elem) terom@31: terom@31: # styled text terom@31: yield rlpp.Paragraph(elem.text, style, bullet) terom@31: terom@31: render_h1 = render_h2 = render_h3 = _render_hx terom@31: terom@31: ## Lists terom@31: def list_ol_seq (self) : terom@31: """ terom@31: Numeric ordered list numbering. terom@31: """ terom@31: terom@31: for idx in itertools.count(1) : terom@31: yield "%d." % (idx, ) terom@31: terom@31: def list_ul_seq (self) : terom@31: """ terom@31: Bulleted unordered list numbering. terom@31: """ terom@31: terom@31: while True : terom@31: yield "•" terom@31: terom@31: def _render_list (self, elem, seq, bullet=None) : terom@31: """ terom@31: Render