1 | ## $Id: pdf.py 9169 2012-09-10 11:05:07Z uli $ |
---|
2 | ## |
---|
3 | ## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann |
---|
4 | ## This program is free software; you can redistribute it and/or modify |
---|
5 | ## it under the terms of the GNU General Public License as published by |
---|
6 | ## the Free Software Foundation; either version 2 of the License, or |
---|
7 | ## (at your option) any later version. |
---|
8 | ## |
---|
9 | ## This program is distributed in the hope that it will be useful, |
---|
10 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of |
---|
11 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
---|
12 | ## GNU General Public License for more details. |
---|
13 | ## |
---|
14 | ## You should have received a copy of the GNU General Public License |
---|
15 | ## along with this program; if not, write to the Free Software |
---|
16 | ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
---|
17 | ## |
---|
18 | """ |
---|
19 | Reusable components for pdf generation. |
---|
20 | """ |
---|
21 | import grok |
---|
22 | import os |
---|
23 | from cStringIO import StringIO |
---|
24 | from datetime import datetime |
---|
25 | from reportlab.lib.units import cm, inch, mm |
---|
26 | from reportlab.lib.pagesizes import A4, landscape, portrait |
---|
27 | from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle |
---|
28 | from reportlab.pdfgen.canvas import Canvas |
---|
29 | from reportlab.platypus import ( |
---|
30 | SimpleDocTemplate, Spacer, Paragraph, Image, Table) |
---|
31 | from zope.formlib.form import setUpEditWidgets |
---|
32 | from zope.i18n import translate |
---|
33 | from zope.publisher.browser import TestRequest |
---|
34 | from zope.component import getUtility |
---|
35 | from waeup.kofa.browser.interfaces import IPDFCreator |
---|
36 | from waeup.kofa.utils.helpers import now |
---|
37 | from waeup.kofa.interfaces import IKofaUtils |
---|
38 | from waeup.kofa.interfaces import MessageFactory as _ |
---|
39 | |
---|
40 | #: A reportlab paragraph style for 'normal' output. |
---|
41 | NORMAL_STYLE = getSampleStyleSheet()['Normal'] |
---|
42 | |
---|
43 | #: A reportlab paragraph style for output of 'code'. |
---|
44 | CODE_STYLE = ParagraphStyle( |
---|
45 | name='Code', |
---|
46 | parent=NORMAL_STYLE, |
---|
47 | fontName='Courier', |
---|
48 | fontSize=10, |
---|
49 | ) |
---|
50 | |
---|
51 | #: A reportlab paragraph style for regular form output. |
---|
52 | ENTRY1_STYLE = ParagraphStyle( |
---|
53 | name='Entry1', |
---|
54 | parent=NORMAL_STYLE, |
---|
55 | fontSize=12, |
---|
56 | ) |
---|
57 | |
---|
58 | #: A reportlab paragraph style for smaller form output. |
---|
59 | SMALL_PARA_STYLE = ParagraphStyle( |
---|
60 | name='Small1', |
---|
61 | parent=NORMAL_STYLE, |
---|
62 | fontSize=10, |
---|
63 | ) |
---|
64 | |
---|
65 | #: A reportlab paragraph style for headlines or bold text in form output. |
---|
66 | HEADLINE1_STYLE = ParagraphStyle( |
---|
67 | name='Header1', |
---|
68 | parent=NORMAL_STYLE, |
---|
69 | fontName='Helvetica-Bold', |
---|
70 | fontSize=12, |
---|
71 | ) |
---|
72 | |
---|
73 | #: A reportlab paragraph style for notes output at end of documents. |
---|
74 | NOTE_STYLE = ParagraphStyle( |
---|
75 | name='Note', |
---|
76 | parent=NORMAL_STYLE, |
---|
77 | fontName='Helvetica', |
---|
78 | fontSize=12, |
---|
79 | ) |
---|
80 | |
---|
81 | def format_html(html): |
---|
82 | """Make HTML code usable for use in reportlab paragraphs. |
---|
83 | |
---|
84 | Main things fixed here: |
---|
85 | If html code: |
---|
86 | - remove newlines (not visible in HTML but visible in PDF) |
---|
87 | - add <br> tags after <div> (as divs break lines in HTML but not in PDF) |
---|
88 | If not html code: |
---|
89 | - just replace newlines by <br> tags |
---|
90 | """ |
---|
91 | if '</' in html: |
---|
92 | # Add br tag if widgets contain div tags |
---|
93 | # which are not supported by reportlab |
---|
94 | html = html.replace('</div>', '</div><br />') |
---|
95 | html = html.replace('\n', '') |
---|
96 | else: |
---|
97 | html = html.replace('\n', '<br />') |
---|
98 | return html |
---|
99 | |
---|
100 | class NumberedCanvas(Canvas): |
---|
101 | """A reportlab canvas for numbering pages after all docs are processed. |
---|
102 | |
---|
103 | Taken from |
---|
104 | http://code.activestate.com/recipes/546511-page-x-of-y-with-reportlab/ |
---|
105 | http://code.activestate.com/recipes/576832/ |
---|
106 | """ |
---|
107 | |
---|
108 | def __init__(self, *args, **kw): |
---|
109 | Canvas.__init__(self, *args, **kw) |
---|
110 | self._saved_page_states = [] |
---|
111 | return |
---|
112 | |
---|
113 | def showPage(self): |
---|
114 | self._saved_page_states.append(dict(self.__dict__)) |
---|
115 | self._startPage() |
---|
116 | return |
---|
117 | |
---|
118 | def save(self): |
---|
119 | """add page info to each page (page x of y)""" |
---|
120 | num_pages = len(self._saved_page_states) |
---|
121 | for state in self._saved_page_states: |
---|
122 | self.__dict__.update(state) |
---|
123 | self.draw_page_number(num_pages) |
---|
124 | Canvas.showPage(self) |
---|
125 | Canvas.save(self) |
---|
126 | return |
---|
127 | |
---|
128 | def draw_page_number(self, page_count): |
---|
129 | """draw string at bottom right with 'page x of y'. |
---|
130 | |
---|
131 | Location of the string is determined by canvas attributes |
---|
132 | `kofa_footer_x_pos` and `kofa_footer_y_pos` that have to be |
---|
133 | set manually. |
---|
134 | |
---|
135 | If this canvas also provides an attribute `kofa_footer_text`, |
---|
136 | the contained text is rendered left of the ``page x of y`` |
---|
137 | string. |
---|
138 | """ |
---|
139 | self.setFont("Helvetica", 9) |
---|
140 | right_footer_text = _( |
---|
141 | '${footer_text} Page ${num1} of ${num2}', |
---|
142 | mapping = {'footer_text': self.kofa_footer_text, |
---|
143 | 'num1':self._pageNumber, 'num2':page_count}) |
---|
144 | self.drawRightString( |
---|
145 | self.kofa_footer_x_pos, self.kofa_footer_y_pos, |
---|
146 | translate(right_footer_text)) |
---|
147 | return |
---|
148 | |
---|
149 | class PDFCreator(grok.GlobalUtility): |
---|
150 | """A utility to help with generating PDF docs. |
---|
151 | """ |
---|
152 | grok.implements(IPDFCreator) |
---|
153 | |
---|
154 | watermark_path = None |
---|
155 | header_logo_path = None |
---|
156 | watermark_pos = [0, 0] |
---|
157 | logo_pos = [0, 0, 0] |
---|
158 | |
---|
159 | @classmethod |
---|
160 | def _setUpWidgets(cls, form_fields, context): |
---|
161 | """Setup simple display widgets. |
---|
162 | |
---|
163 | Returns the list of widgets. |
---|
164 | """ |
---|
165 | request = TestRequest() |
---|
166 | return setUpEditWidgets( |
---|
167 | form_fields, 'form', context, request, {}, |
---|
168 | for_display=True, ignore_request=True |
---|
169 | ) |
---|
170 | |
---|
171 | @classmethod |
---|
172 | def _addCourse(cls, table_data, row_num, course_label, course_link, |
---|
173 | lang, domain): |
---|
174 | """Add course data to `table_data`. |
---|
175 | """ |
---|
176 | if not course_label or not course_link: |
---|
177 | return table_data, row_num |
---|
178 | f_label= translate( |
---|
179 | _(course_label), domain, target_language=lang) |
---|
180 | f_label = Paragraph(f_label, ENTRY1_STYLE) |
---|
181 | f_text = Paragraph(course_link, ENTRY1_STYLE) |
---|
182 | table_data.append([f_label, f_text]) |
---|
183 | row_num += 1 |
---|
184 | return table_data, row_num |
---|
185 | |
---|
186 | @classmethod |
---|
187 | def _addDeptAndFaculty(cls, table_data, row_num, dept, faculty, |
---|
188 | lang, domain): |
---|
189 | """Add `dept` and `faculty` as table rows to `table_data`. |
---|
190 | |
---|
191 | `dept` and `faculty` are expected to be strings or None. In |
---|
192 | latter case they are not put into the table. |
---|
193 | """ |
---|
194 | for label, text in (('Department:', dept), ('Faculty:', faculty)): |
---|
195 | if text is None: |
---|
196 | continue |
---|
197 | label = translate(_(label), domain, target_language=lang) |
---|
198 | table_data.append([ |
---|
199 | Paragraph(label, ENTRY1_STYLE), |
---|
200 | Paragraph(text, ENTRY1_STYLE)]) |
---|
201 | row_num += 1 |
---|
202 | return table_data, row_num |
---|
203 | |
---|
204 | @classmethod |
---|
205 | def fromStringList(cls, string_list): |
---|
206 | """Generate a list of reportlab paragraphs out of a list of strings. |
---|
207 | |
---|
208 | Strings are formatted with :data:`CODE_STYLE` and a spacer is |
---|
209 | appended at end. |
---|
210 | """ |
---|
211 | result = [] |
---|
212 | for msg in string_list: |
---|
213 | result.append(Paragraph(msg, CODE_STYLE)) |
---|
214 | result.append(Spacer(1, 20)) |
---|
215 | return result |
---|
216 | |
---|
217 | @classmethod |
---|
218 | def getImage(cls, image_path, orientation='LEFT'): |
---|
219 | """Get an image located at `image_path` as reportlab flowable. |
---|
220 | """ |
---|
221 | img = Image(image_path, width=4*cm, height=3*cm, kind='bound') |
---|
222 | img.hAlign=orientation |
---|
223 | return img |
---|
224 | |
---|
225 | def getWidgetsTable(self, form_fields, context, view, lang='en', |
---|
226 | domain='waeup.kofa', separators=None, |
---|
227 | course_label=None, course_link=None, dept=None, |
---|
228 | faculty=None): |
---|
229 | """Return a reportlab `Table` instance, created from widgets |
---|
230 | determined by `form_fields` and `context`. |
---|
231 | |
---|
232 | - `form_fields` |
---|
233 | is a list of schema fields as created by grok.AutoFields. |
---|
234 | - `context` |
---|
235 | is some object whose content is rendered here. |
---|
236 | - `view` |
---|
237 | is currently not used but supposed to be a view which is |
---|
238 | actually rendering a PDF document. |
---|
239 | - `lang` |
---|
240 | the portal language. Used for translations of strings. |
---|
241 | - `domain` |
---|
242 | the translation domain used for translations of strings. |
---|
243 | - `separators` |
---|
244 | a list of separators. |
---|
245 | - `course_label` and `course_link` |
---|
246 | if a course should be added to the table, `course_label` |
---|
247 | and `course_link` can be given, both being strings. They |
---|
248 | will be rendered in an extra-row. |
---|
249 | - `dept` and `faculty` |
---|
250 | if these are given, we render extra rows with faculty and |
---|
251 | department. |
---|
252 | """ |
---|
253 | table_data = [] |
---|
254 | table_style = [#('LEFTPADDING', (0,0), (0,-1), 0), # indentation |
---|
255 | ('VALIGN', (0,0), (-1,-1), 'TOP'), |
---|
256 | ] |
---|
257 | row_num = 0 |
---|
258 | widgets = self._setUpWidgets(form_fields, context) |
---|
259 | for widget in widgets: |
---|
260 | if separators and separators.get(widget.name): |
---|
261 | f_headline = translate( |
---|
262 | separators[widget.name], domain, target_language=lang) |
---|
263 | f_headline = Paragraph(f_headline, HEADLINE1_STYLE) |
---|
264 | table_data.append([f_headline ]) |
---|
265 | table_style.append(('SPAN', (0,row_num), (-1,row_num)),) |
---|
266 | table_style.append( |
---|
267 | ('TOPPADDING', (0,row_num), (-1,row_num), 12),) |
---|
268 | row_num += 1 |
---|
269 | f_label = translate(widget.label.strip(), domain, |
---|
270 | target_language=lang) |
---|
271 | f_label = Paragraph(f_label, ENTRY1_STYLE) |
---|
272 | f_text = translate(widget(), domain, target_language=lang) |
---|
273 | f_text = format_html(f_text) |
---|
274 | if f_text: |
---|
275 | hint = ' <font size=9>' + widget.hint + '</font>' |
---|
276 | f_text = f_text + hint |
---|
277 | f_text = Paragraph(f_text, ENTRY1_STYLE) |
---|
278 | table_data.append([f_label,f_text]) |
---|
279 | row_num += 1 |
---|
280 | |
---|
281 | # Add course (admitted, etc.) if applicable |
---|
282 | table_data, row_num = self._addCourse( |
---|
283 | table_data, row_num, course_label, course_link, lang, domain,) |
---|
284 | |
---|
285 | ## Add dept. and faculty if applicable |
---|
286 | table_data, row_num = self._addDeptAndFaculty( |
---|
287 | table_data, row_num, dept, faculty, lang, domain) |
---|
288 | |
---|
289 | # Create table |
---|
290 | table = Table(table_data,style=table_style) |
---|
291 | table.hAlign = 'LEFT' |
---|
292 | return table |
---|
293 | |
---|
294 | |
---|
295 | def paint_background(self, canvas, doc): |
---|
296 | """Paint background of a PDF, including watermark, title, etc. |
---|
297 | |
---|
298 | The `doc` is expected to be some reportlab SimpleDocTemplate |
---|
299 | or similar object. |
---|
300 | |
---|
301 | Text of headerline is extracted from doc.kofa_headtitle, the |
---|
302 | document title (under the head) from doc.kofa_title. |
---|
303 | |
---|
304 | This is a callback method that will be called from reportlab |
---|
305 | when creating PDFs with :meth:`create_pdf`. |
---|
306 | """ |
---|
307 | canvas.saveState() |
---|
308 | width, height = doc.width, doc.height |
---|
309 | width += doc.leftMargin + doc.rightMargin |
---|
310 | height += doc.topMargin + doc.bottomMargin |
---|
311 | |
---|
312 | # Watermark |
---|
313 | if self.watermark_path is not None: |
---|
314 | canvas.saveState() |
---|
315 | canvas.drawImage(self.watermark_path, |
---|
316 | self.watermark_pos[0], self.watermark_pos[1]) |
---|
317 | canvas.restoreState() |
---|
318 | |
---|
319 | # Header |
---|
320 | head_title = getattr( |
---|
321 | doc, 'kofa_headtitle', getattr( |
---|
322 | grok.getSite()['configuration'], 'name', |
---|
323 | u'Sample University')) |
---|
324 | canvas.setFont("Helvetica-Bold", 18) |
---|
325 | canvas.drawString(1.5*cm, height-1.7*cm, head_title) |
---|
326 | canvas.line(1.5*cm,height-1.9*cm,width-1.5*cm,height-1.9*cm) |
---|
327 | if self.header_logo_path is not None: |
---|
328 | canvas.drawImage(self.header_logo_path, |
---|
329 | self.logo_pos[0], self.logo_pos[1], width=self.logo_pos[2], |
---|
330 | preserveAspectRatio=True, anchor='ne') |
---|
331 | |
---|
332 | # Title |
---|
333 | canvas.saveState() |
---|
334 | canvas.setFont("Helvetica-Bold", 14) |
---|
335 | title = getattr(doc, 'kofa_title', '') |
---|
336 | if '\n' in title: |
---|
337 | title_lines = title.split('\n') |
---|
338 | for num, line in enumerate(title_lines): |
---|
339 | canvas.drawCentredString( |
---|
340 | width/2.0, height-2.8*cm-(num*0.7*cm), line) |
---|
341 | elif title: |
---|
342 | canvas.drawCentredString(width/2.0, height-2.8*cm, title) |
---|
343 | canvas.restoreState() |
---|
344 | |
---|
345 | # Footer |
---|
346 | canvas.saveState() |
---|
347 | canvas.line(2.2*cm, 0.62*inch, width-2.2*cm, 0.62*inch) |
---|
348 | canvas.setFont("Helvetica", 9) |
---|
349 | if not getattr(doc, 'kofa_nodate', False): |
---|
350 | tz = getUtility(IKofaUtils).tzinfo |
---|
351 | today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z') |
---|
352 | canvas.drawString(2.2*cm, 0.5 * inch, |
---|
353 | translate(_(u'Date: ${a}', mapping = {'a': today}))) |
---|
354 | # set canves attributes needed to render `page x of y` |
---|
355 | canvas.kofa_footer_x_pos = width-2.2*cm |
---|
356 | canvas.kofa_footer_y_pos = 0.5 * inch |
---|
357 | canvas.kofa_footer_text = doc.kofa_footer |
---|
358 | canvas.restoreState() |
---|
359 | canvas.restoreState() |
---|
360 | |
---|
361 | # Metadata |
---|
362 | canvas.setAuthor(getattr(doc, 'kofa_author', 'Unknown')) |
---|
363 | canvas.setSubject(title) |
---|
364 | canvas.setCreator(u'WAeUP Kofa') |
---|
365 | return |
---|
366 | |
---|
367 | def create_pdf(self, data, headerline=None, title=None, author=None, |
---|
368 | footer='', note=None): |
---|
369 | """Returns a binary data stream which is a PDF document. |
---|
370 | """ |
---|
371 | pdf_stream = StringIO() |
---|
372 | doc = SimpleDocTemplate( |
---|
373 | pdf_stream, |
---|
374 | bottomMargin=1.1*inch, |
---|
375 | topMargin=1.6*inch, |
---|
376 | title=title, |
---|
377 | pagesize=portrait(A4), |
---|
378 | showBoundary=False, |
---|
379 | ) |
---|
380 | # Set some attributes that are needed when rendering the background. |
---|
381 | if headerline is not None: |
---|
382 | doc.kofa_headtitle = headerline |
---|
383 | doc.kofa_title = title |
---|
384 | doc.kofa_author = author |
---|
385 | doc.kofa_footer = footer |
---|
386 | if note is not None: |
---|
387 | html = format_html(note) |
---|
388 | data.append(Paragraph(html, NOTE_STYLE)) |
---|
389 | doc.build(data, onFirstPage=self.paint_background, |
---|
390 | onLaterPages=self.paint_background, |
---|
391 | canvasmaker=NumberedCanvas |
---|
392 | ) |
---|
393 | result = pdf_stream.getvalue() |
---|
394 | pdf_stream.close() |
---|
395 | return result |
---|