|
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 |