svv/pdf.py
changeset 4 b3a1ab44f517
child 15 e098ee83b363
equal deleted inserted replaced
3:44122295656a 4:b3a1ab44f517
       
     1 # coding: utf-8
       
     2 
       
     3 """
       
     4     PDF output
       
     5 """
       
     6 
       
     7 from reportlab import platypus as rlpp
       
     8 from reportlab.lib.units import inch
       
     9 from reportlab.lib import pagesizes, styles
       
    10 
       
    11 from cStringIO import StringIO
       
    12 import itertools
       
    13 import logging
       
    14 import datetime
       
    15 
       
    16 
       
    17 log = logging.getLogger('svv.pdf')
       
    18 
       
    19 class Styles (object) :
       
    20     """
       
    21         Simple stylesheet
       
    22     """
       
    23 
       
    24     samplestyles = styles.getSampleStyleSheet()
       
    25 
       
    26     # normal text
       
    27     h1 = styles.ParagraphStyle('Heading1', samplestyles['h1'],
       
    28             fontName        = 'Times-Bold',
       
    29             fontSize        = 22,
       
    30             spaceBefore     = 0,
       
    31             spaceAfter      = 0,
       
    32 
       
    33     )
       
    34 
       
    35     h2 = styles.ParagraphStyle('Heading2', samplestyles['h2'],
       
    36             fontName        = 'Times-Bold',
       
    37             fontSize        = 14,
       
    38             spaceBefore     = 6,
       
    39             spaceAfter      = 0,
       
    40     )
       
    41     
       
    42     h3 = styles.ParagraphStyle('Heading3', samplestyles['h3'],
       
    43             fontName        = 'Times-Italic',
       
    44             fontSize        = 12,
       
    45             spaceBefore     = 0,
       
    46             spaceAfter      = 0,
       
    47     )
       
    48 
       
    49     text = styles.ParagraphStyle('Text', samplestyles['Normal'],
       
    50 
       
    51     )
       
    52 
       
    53     # list indent level
       
    54     list_indent = inch / 4
       
    55 
       
    56     # root title
       
    57     list_h1 = styles.ParagraphStyle('ListHeading1', samplestyles['h1'],
       
    58             bulletIndent    = 0,
       
    59             leftIndent      = 0,
       
    60     )
       
    61 
       
    62     # section
       
    63     list_h2 = styles.ParagraphStyle('ListHeading2', samplestyles['h2'],
       
    64             bulletIndent    = 0,
       
    65             leftIndent      = list_indent,
       
    66             fontName        = 'Times-Bold',
       
    67             fontSize        = 10,
       
    68             leading         = 12,
       
    69             spaceBefore     = 6,
       
    70             spaceAfter      = 0,
       
    71     )
       
    72     
       
    73     # segment
       
    74     list_h3 = styles.ParagraphStyle('ListHeading3', samplestyles['h3'],
       
    75             bulletIndent    = 0,
       
    76             leftIndent      = list_indent,
       
    77             fontName        = 'Times-Italic',
       
    78             fontSize        = 10,
       
    79             leading         = 12,
       
    80             spaceBefore     = 0,
       
    81             spaceAfter      = 0,
       
    82     )
       
    83     
       
    84     # infot
       
    85     list_text = styles.ParagraphStyle('ListText', samplestyles['Normal'],
       
    86             bulletIndent    = 0,
       
    87             leftIndent      = list_indent,
       
    88     )
       
    89 
       
    90 class ListItem (object) :
       
    91     """
       
    92         Indented/nested list
       
    93     """
       
    94 
       
    95     @classmethod
       
    96     def seq (cls) :
       
    97         """
       
    98             List numbering.
       
    99 
       
   100             Fixed as numeric only for now
       
   101         """
       
   102         
       
   103         for idx in itertools.count(1) :
       
   104             yield "%d." % (idx, )
       
   105 
       
   106 
       
   107     def __init__ (self, title, title_style, text, subseq=None, sublist=None, 
       
   108             text_style=Styles.list_text, indent=Styles.list_indent) :
       
   109         """
       
   110                 title       - title to display as first line
       
   111                 title_style - paragraph style for title line
       
   112                 text        - multi-line texto to display on first or previous lines
       
   113                 subseq      - sequence of bullet-texts to use for items in sublist
       
   114                 sublist     - sequence of sub-nodes
       
   115 
       
   116                 text_style  - paragraph style for text lines
       
   117                 indent      - indentation for text and sub-lists
       
   118         """
       
   119 
       
   120         self.title = title
       
   121         self.title_style = title_style
       
   122         self.text = text
       
   123 
       
   124         self.subseq = subseq
       
   125         self.sublist = sublist
       
   126 
       
   127         self.text_style = text_style
       
   128         self.indent = indent
       
   129 
       
   130     def render_pdf (self, bullet=None) :
       
   131         """
       
   132             Yield a series of PDF flowables for this list node and sub-nodes, useable for pdf.DocTemplateBase.build()
       
   133 
       
   134                 bullet      - bullet text to use for this item's paragraph
       
   135         """
       
   136 
       
   137         # first line, with possible bullet
       
   138         if self.title :
       
   139             yield rlpp.Paragraph(self.title, self.title_style, bullet)
       
   140 
       
   141         elif self.text :
       
   142             yield rlpp.Paragraph(self.text, self.text_style, bullet)
       
   143 
       
   144         # indented text after title
       
   145         if self.title and self.text :
       
   146             yield rlpp.Paragraph(self.text, self.text_style)
       
   147 
       
   148         if self.sublist :
       
   149             # following lines, indented
       
   150             yield rlpp.Indenter(self.indent)
       
   151 
       
   152             # sub-items
       
   153             for item in self.sublist :
       
   154                 # get (optional) bullet for item
       
   155                 bullet = next(self.subseq, None) if self.subseq else None
       
   156 
       
   157                 # render item as series of elements
       
   158                 for element in item.render_pdf(bullet) :
       
   159                     yield element
       
   160 
       
   161             # de-dent
       
   162             yield rlpp.Indenter(-self.indent)
       
   163 
       
   164     
       
   165 
       
   166 class SignatureBlock (rlpp.Flowable) :
       
   167     """
       
   168         A signature block, with multiple sets of multiple pre-fillied fields.
       
   169     """
       
   170 
       
   171     # vertical space per field
       
   172     FIELD_HEIGHT = 2 * inch / 4
       
   173 
       
   174     # horizontal offset from top of field to field line
       
   175     FIELD_OFFSET = FIELD_HEIGHT / 2
       
   176     
       
   177     # maximum width to scale columns to
       
   178     COL_WIDTH_MAX = 4 * inch
       
   179     
       
   180     # empty space to leave below the fields
       
   181     PADDING_BOTTOM = inch / 2
       
   182 
       
   183     def __init__ (self, cols, fields, values) :
       
   184         """
       
   185             cols    - Column titles
       
   186             fields  - Fields titles, describing the horizontal fields
       
   187             values  - Pre-filled values as a {(col, field): value} dict
       
   188 
       
   189             desc/value strings can contain formatting codes:
       
   190 
       
   191                 column      - title of the current column
       
   192                 today       - today's date in %d/%m/%Y format
       
   193         """
       
   194 
       
   195         self.cols = cols
       
   196         self.fields = fields
       
   197         self.values = values
       
   198 
       
   199     def wrap (self, width, height) :
       
   200         """
       
   201             Calculate how much space we use up, returning (width, height)
       
   202         """
       
   203 
       
   204         self.width = width
       
   205 
       
   206         # consume all available height, to place us at the bottom
       
   207         self.height = max(len(self.fields) * self.FIELD_HEIGHT, height)
       
   208 
       
   209         return self.width, self.height
       
   210     
       
   211     def formatString (self, text, col_title) :
       
   212         """
       
   213             Format display string using context parameters
       
   214         """
       
   215 
       
   216         return text % dict(
       
   217                 column      = col_title,
       
   218                 today       = datetime.date.today().strftime("%d/%m/%Y"),
       
   219         )
       
   220     
       
   221     # text above field line
       
   222     VALUE_FONT = "Courier-Bold"
       
   223     VALUE_FONT_SIZE = 14
       
   224     VALUE_OFFSET = inch / 12
       
   225 
       
   226     # text below field line
       
   227     TITLE_FONT = "Times-Italic"
       
   228     TITLE_FONT_SIZE = 10
       
   229     TITLE_OFFSET = inch / 8
       
   230 
       
   231     def draw (self) :
       
   232         """
       
   233             Render full block onto our canvas
       
   234         """
       
   235 
       
   236         # target canvas
       
   237         canvas = self.canv
       
   238 
       
   239         col_width = min(self.width / len(self.cols), self.COL_WIDTH_MAX)
       
   240         col_margin = col_width * 0.1
       
   241         col_height = len(self.fields) * self.FIELD_HEIGHT + self.PADDING_BOTTOM
       
   242 
       
   243         for field_idx, (field_title) in enumerate(self.fields) :
       
   244             h = self.FIELD_HEIGHT
       
   245             y = col_height - h * (field_idx + 1)
       
   246 
       
   247             for col_idx, (col_title) in enumerate(self.cols) :
       
   248                 w = col_width
       
   249                 x = w * col_idx
       
   250                 value = self.values.get((col_title, field_title))
       
   251                 title = field_title
       
   252 
       
   253                 value = self.formatString(value, col_title) if value else None
       
   254                 title = self.formatString(title, col_title) if title else None
       
   255 
       
   256                 if value :
       
   257                     canvas.setFont(self.VALUE_FONT, self.VALUE_FONT_SIZE)
       
   258                     canvas.drawString(x + col_margin + self.VALUE_OFFSET, y - self.FIELD_OFFSET + 2, value)
       
   259                 
       
   260                 # field line
       
   261                 canvas.line(x + col_margin, y - self.FIELD_OFFSET, x + w - col_margin, y - h / 2)
       
   262 
       
   263                 # desc text
       
   264                 canvas.setFont(self.TITLE_FONT, self.TITLE_FONT_SIZE)
       
   265                 canvas.drawString(x + col_margin + self.TITLE_OFFSET, y - self.FIELD_OFFSET - self.TITLE_FONT_SIZE, title)
       
   266 
       
   267 class PageTemplate (rlpp.PageTemplate) :
       
   268     """
       
   269         A single-frame page with header and footer.
       
   270     """
       
   271 
       
   272     # vertical space available for footer/header, fixed because we can't really flow text vertically
       
   273     HEADER_HEIGHT = 1 * inch
       
   274     FOOTER_HEIGHT = 1 * inch
       
   275 
       
   276     COL_FONT_SIZE = 8
       
   277     COL_TITLE_FONT_NAME, COL_TITLE_FONT_SIZE = COL_TITLE_FONT = ("Times-Bold", COL_FONT_SIZE)
       
   278     COL_TEXT_FONT_NAME, COL_TEXT_FONT_SIZE = COL_TEXT_FONT = ("Times-Roman", COL_FONT_SIZE)
       
   279 
       
   280 
       
   281     def __init__ (self, id='page', page_size=pagesizes.A4, margin=inch, header_columns=None, footer_columns=None) :
       
   282         """
       
   283                 id          - identifier for this page template
       
   284                 page_size   - the (width, height) of this page
       
   285                 margin      - the base margin to use between elements
       
   286 
       
   287                 header_columns  - (title, text) list of header columns
       
   288                 footer_columnss - (title, text) list of footer columns
       
   289         """
       
   290         
       
   291         self.page_width, self.page_height = self.page_size = page_size
       
   292         self.margin = margin
       
   293 
       
   294         self.header_height = self.HEADER_HEIGHT
       
   295         self.footer_height = self.FOOTER_HEIGHT
       
   296 
       
   297         # calculate frame
       
   298         self.frame_left = self.margin
       
   299         self.frame_bottom = self.footer_height + self.margin / 2
       
   300         self.frame_top = self.page_height - self.header_height - self.margin / 2
       
   301         self.frame_right = self.page_width - self.margin
       
   302 
       
   303         self.frame = rlpp.Frame(self.frame_left, self.frame_bottom, self.frame_right - self.frame_left, self.frame_top - self.frame_bottom)
       
   304         
       
   305         # init base template
       
   306         rlpp.PageTemplate.__init__(self, id, frames=[self.frame])
       
   307 
       
   308         self.header_columns = header_columns
       
   309         self.footer_columns = footer_columns
       
   310 
       
   311 
       
   312     def fmt_string (self, text) :
       
   313         """
       
   314             Prepare a string for display by handling format codes
       
   315         """
       
   316 
       
   317         # XXX: unicode?
       
   318         return str(text % dict(
       
   319             today = datetime.date.today().strftime("%d / %m / %Y"),
       
   320         ))
       
   321 
       
   322 
       
   323     def draw_column (self, canvas, x, y, width, title, lines, gray=None) :
       
   324         """
       
   325             Draw a column in the specified position, with the specified lines of text
       
   326         """
       
   327 
       
   328         text = canvas.beginText(x, y)
       
   329 
       
   330         # grayscale text?
       
   331         if gray :
       
   332             text.setFillGray(gray)
       
   333 
       
   334         # title
       
   335         text.setFont(*self.COL_TITLE_FONT)
       
   336         text.textLine(self.fmt_string(title))
       
   337 
       
   338         # lines
       
   339         text.setFont(*self.COL_TEXT_FONT)
       
   340         text.textLines(self.fmt_string(lines))
       
   341         
       
   342         # draw out
       
   343         canvas.drawText(text)
       
   344 
       
   345 
       
   346     def draw_columns (self, canvas, x, y, width, columns, **opts) :
       
   347         """
       
   348             Draw a series of columns in the specified position
       
   349         """
       
   350 
       
   351         col_count = len(columns)
       
   352         col_width = width / col_count
       
   353 
       
   354         x_base = x
       
   355 
       
   356         for col_idx, (col_data) in enumerate(columns) :
       
   357             x = x_base + (col_idx * col_width)
       
   358 
       
   359             # draw column data at correct offset inside our space
       
   360             self.draw_column(canvas, x, y - self.COL_FONT_SIZE, col_width, *col_data, **opts)
       
   361 
       
   362 
       
   363     def draw_header (self, canvas) :
       
   364         """
       
   365             Draw page header
       
   366         """
       
   367         
       
   368         # offsets
       
   369         x = self.margin
       
   370         h = self.footer_height - self.margin / 4
       
   371         w = self.page_width - self.margin * 2
       
   372 
       
   373         # spacer
       
   374         y = self.page_height - self.footer_height
       
   375 
       
   376         canvas.setLineWidth(0.5)
       
   377         canvas.line(x - self.margin / 2, y, self.page_width - self.margin / 2, y)
       
   378 
       
   379         # columns
       
   380         y = self.page_height - self.margin / 4
       
   381 
       
   382         self.draw_columns(canvas, x, y, w, self.header_columns)
       
   383 
       
   384 
       
   385     def draw_footer (self, canvas) :
       
   386         """
       
   387             Draw page footer
       
   388         """
       
   389         
       
   390         # offsets
       
   391         x = self.margin
       
   392         y = self.footer_height
       
   393         w = self.page_width - self.margin * 2
       
   394 
       
   395         # spacer
       
   396         canvas.setLineWidth(0.5)
       
   397         canvas.line(x - self.margin / 2, y, self.page_width - self.margin / 2, y)
       
   398 
       
   399         # columns
       
   400         self.draw_columns(canvas, x, y - inch / 8, w, self.footer_columns, gray=0.4)
       
   401 
       
   402 
       
   403     def beforeDrawPage (self, canvas, document) :
       
   404         """
       
   405             Draw page headers/footers
       
   406         """
       
   407 
       
   408         self.draw_header(canvas)
       
   409         self.draw_footer(canvas)
       
   410 
       
   411 class DocumentTemplate (rlpp.BaseDocTemplate) :
       
   412     def __init__ (self, page_templates, title, author, page_size=pagesizes.A4) :
       
   413         """
       
   414             Initialize with fixed list of needed PageTemplates.
       
   415         """
       
   416 
       
   417         # we supply the file later
       
   418         rlpp.BaseDocTemplate.__init__(self, filename=None, 
       
   419                 pageTemplates=page_templates, title=title, author=author,
       
   420                 pageSize=page_size
       
   421         )
       
   422 
       
   423     def render_string (self, elements) :
       
   424         """
       
   425             Build the document using the given list of Flowables, returning the PDF as a single str.
       
   426         """
       
   427 
       
   428         buf = StringIO()
       
   429 
       
   430         # build
       
   431         self.build(elements, buf)
       
   432 
       
   433         # binary data out
       
   434         return buf.getvalue()
       
   435