1
2
3 """
4 S3 Adobe PDF codec
5
6 @copyright: 2011-2019 (c) Sahana Software Foundation
7 @license: MIT
8
9 Permission is hereby granted, free of charge, to any person
10 obtaining a copy of this software and associated documentation
11 files (the "Software"), to deal in the Software without
12 restriction, including without limitation the rights to use,
13 copy, modify, merge, publish, distribute, sublicense, and/or sell
14 copies of the Software, and to permit persons to whom the
15 Software is furnished to do so, subject to the following
16 conditions:
17
18 The above copyright notice and this permission notice shall be
19 included in all copies or substantial portions of the Software.
20
21 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
23 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
25 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
26 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
27 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
28 OTHER DEALINGS IN THE SOFTWARE.
29 """
30
31 __all__ = ("S3RL_PDF",)
32
33 try:
34 from cStringIO import StringIO
35 except:
36 from StringIO import StringIO
37
38 from copy import deepcopy
39 import os
40
41 from gluon import current, redirect, URL, \
42 A, DIV, H1, H2, H3, H4, H5, H6, IMG, P, \
43 TABLE, TBODY, TD, TFOOT, TH, THEAD, TR
44 from gluon.storage import Storage
45 from gluon.contenttype import contenttype
46 from gluon.languages import lazyT
47
48 from ..s3codec import S3Codec
49 from ..s3utils import s3_strip_markup, s3_unicode, s3_str
50
51 try:
52 from reportlab.lib import colors
53 from reportlab.lib.colors import Color, HexColor
54 from reportlab.lib.pagesizes import A4, LETTER, landscape, portrait
55 from reportlab.lib.styles import getSampleStyleSheet
56 from reportlab.lib.units import inch
57 from reportlab.pdfbase import pdfmetrics
58 from reportlab.pdfbase.ttfonts import TTFont
59 from reportlab.pdfgen import canvas
60 from reportlab.platypus import BaseDocTemplate, PageBreak, PageTemplate, \
61 Paragraph, Spacer, Table
62 from reportlab.platypus.frames import Frame
63 reportLabImported = True
64 except ImportError:
65 reportLabImported = False
66 BaseDocTemplate = object
67 inch = 72.0
68 canvas = Storage()
69 canvas.Canvas = None
70
71 try:
72 from bidi.algorithm import get_display
73 import arabic_reshaper
74 import unicodedata
75 biDiImported = True
76 except ImportError:
77 biDiImported = False
78 current.log.warning("PDF Codec", "BiDirectional Support not available: Install Python-BiDi")
79
80 PDF_WIDTH = 0
81 PDF_HEIGHT = 1
85 """
86 DRY Helper function for all classes which use PDF to set the appropriate Fonts
87 """
88
89 font_set = current.deployment_settings.get_pdf_export_font()
90 if font_set:
91 try:
92 font_name = font_set[0]
93 font_name_bold = font_set[1]
94 folder = current.request.folder
95
96 pdfmetrics.registerFont(TTFont(font_name, os.path.join(folder,
97 "static",
98 "fonts",
99 "%s.ttf" % font_name)))
100 pdfmetrics.registerFont(TTFont(font_name_bold, os.path.join(folder,
101 "static",
102 "fonts",
103 "%s.ttf" % font_name_bold)))
104 except:
105 current.log.error("%s Font not found: Please install it to see the correct fonts in PDF exports" % font_set[0])
106
107 instance.font_name = "Helvetica"
108 instance.font_name_bold = "Helvetica-Bold"
109 else:
110 instance.font_name = font_name
111 instance.font_name_bold = font_name_bold
112 else:
113
114 instance.font_name = "Helvetica"
115 instance.font_name_bold = "Helvetica-Bold"
116
117
118 -def biDiText(text):
119 """
120 Ensure that RTL text is rendered RTL & also that Arabic text is
121 rewritten to use the joined format.
122 """
123
124 text = s3_unicode(text)
125
126 if biDiImported and current.deployment_settings.get_pdf_bidi():
127
128 isArabic = False
129 isBidi = False
130
131 for c in text:
132 cat = unicodedata.bidirectional(c)
133
134 if cat in ("AL", "AN"):
135 isArabic = True
136 isBidi = True
137 break
138 elif cat in ("R", "RLE", "RLO"):
139 isBidi = True
140
141 if isArabic:
142 text = arabic_reshaper.reshape(text)
143
144 if isBidi:
145 text = get_display(text)
146
147 return text
148
151 """
152 Simple Report Labs PDF format codec
153 """
154
156 """
157 Constructor
158 """
159
160
161 self.ERROR = Storage(
162 RL_ERROR = "Python needs the ReportLab module installed for PDF export"
163 )
164
165
166 self.font_name = None
167 self.font_name_bold = None
168 set_fonts(self)
169
170
171 - def encode(self, resource, **attr):
172 """
173 Export data as a PDF document
174
175 @param resource: the resource
176 @param attr: dictionary of keyword arguments, in s3_rest_controller
177 passed through from the calling controller
178
179 @keyword request: the S3Request
180 @keyword method: "read" to not include a list view when no
181 component is specified
182 @keyword list_fields: fields to include in lists
183
184 @keyword pdf_componentname: enforce this component
185
186 @keyword pdf_groupby: how to group the results
187 @keyword pdf_orderby: how to sort rows (within any level of grouping)
188
189 @keyword pdf_callback: callback to be used rather than request
190
191 @keyword pdf_title: the title of the report
192 @keyword pdf_filename: the filename for the report
193
194 @keyword rheader: HTML page header (override by pdf_header)
195 @keyword rfooter: HTML page footer (override by pdf_footer)
196 @keyword pdf_header: callback to generate the HTML header
197 (overrides rheader)
198 @keyword pdf_footer: callback to generate the HTML footer,
199 or static HTML (overrides rfooter)
200
201 @keyword pdf_header_padding: add this amount of space between
202 the header and the body
203 @keyword pdf_footer_padding: add this amount of space between
204 the body and the footer
205
206 @keyword pdf_hide_comments: don't show the comments in a table
207
208 @keyword pdf_table_autogrow: Indicates that a table should grow to
209 fill the available space. Valid values:
210 H - Horizontal
211 V - Vertical
212 B - Both
213 @keyword pdf_orientation: Portrait (default) or Landscape
214 @keyword use_colour: True to add colour to the cells. default False
215
216 @keyword pdf_html_styles: styles for S3html2pdf (dict)
217
218 @ToDo: Add Page Numbers in Footer:
219 http://www.blog.pythonlibrary.org/2013/08/12/reportlab-how-to-add-page-numbers/
220 """
221
222 if not reportLabImported:
223 current.session.error = self.ERROR.RL_ERROR
224 redirect(URL(extension=""))
225
226
227 attr_get = attr.get
228 r = self.r = attr_get("request", None)
229 self.list_fields = attr_get("list_fields")
230 self.pdf_groupby = attr_get("pdf_groupby")
231 self.pdf_orderby = attr_get("pdf_orderby")
232 self.pdf_hide_comments = attr_get("pdf_hide_comments")
233 self.table_autogrow = attr_get("pdf_table_autogrow")
234 self.pdf_header_padding = attr_get("pdf_header_padding", 0)
235 self.pdf_footer_padding = attr_get("pdf_footer_padding", 0)
236
237
238 now = current.request.now.isoformat()[:19].replace("T", " ")
239 title = attr_get("pdf_title")
240 if title is None:
241 title = "Report"
242 docTitle = "%s %s" % (title, now)
243 filename = attr_get("pdf_filename")
244 if filename is None:
245 if not isinstance(title, str):
246
247 title = title.encode("utf-8")
248 filename = "%s_%s.pdf" % (title, now)
249 elif len(filename) < 5 or filename[-4:] != ".pdf":
250
251 filename = "%s.pdf" % filename
252 self.filename = filename
253
254
255 size = attr_get("pdf_size")
256 orientation = attr_get("pdf_orientation")
257 if not orientation:
258 orientation = current.deployment_settings.get_pdf_orientation()
259 doc = EdenDocTemplate(title = docTitle,
260 size = size,
261 orientation = orientation,
262 )
263
264
265 pdf_html_styles = attr_get("pdf_html_styles")
266
267
268 header_flowable = None
269 header = attr_get("pdf_header")
270 if not header:
271 header = attr_get("rheader")
272 if header:
273 header_flowable = self.get_html_flowable(header,
274 doc.printable_width,
275 styles = pdf_html_styles,
276 )
277 if self.pdf_header_padding:
278 header_flowable.append(Spacer(1, self.pdf_header_padding))
279
280
281 footer_flowable = None
282 footer = attr_get("pdf_footer")
283 if not footer:
284 footer = attr_get("rfooter")
285 if footer:
286 footer_flowable = self.get_html_flowable(footer,
287 doc.printable_width,
288 styles = pdf_html_styles,
289 )
290 if self.pdf_footer_padding:
291 footer_flowable.append(Spacer(1, self.pdf_footer_padding))
292
293
294
295
296 body_flowable = None
297
298 doc.calc_body_size(header_flowable, footer_flowable)
299
300 callback = attr_get("pdf_callback")
301 pdf_componentname = attr_get("pdf_componentname", None)
302 if callback:
303
304 body_flowable = self.get_html_flowable(callback(r),
305 doc.printable_width,
306 styles = pdf_html_styles,
307 )
308
309 elif pdf_componentname:
310
311 resource = current.s3db.resource(r.tablename,
312 components = [pdf_componentname],
313 id = r.id,
314 )
315 component = resource.components.get(pdf_componentname)
316 if component:
317 body_flowable = self.get_resource_flowable(component, doc)
318
319 elif r.component or attr_get("method", "list") != "read":
320
321 body_flowable = self.get_resource_flowable(resource, doc)
322
323 styleSheet = getSampleStyleSheet()
324 style = styleSheet["Normal"]
325 style.fontName = self.font_name
326 style.fontSize = 9
327 if not body_flowable:
328 body_flowable = [Paragraph("", style)]
329 self.normalstyle = style
330
331
332 doc.build(header_flowable,
333 body_flowable,
334 footer_flowable,
335 )
336
337
338 response = current.response
339 if response:
340 disposition = "attachment; filename=\"%s\"" % self.filename
341 response.headers["Content-Type"] = contenttype(".pdf")
342 response.headers["Content-disposition"] = disposition
343
344 return doc.output.getvalue()
345
346
348 """
349 Function to convert the rules passed in to a flowable.
350 The rules (for example) could be an rHeader callback
351
352 @param rules: the HTML (web2py helper class) or a callback
353 to produce it. The callback receives the
354 S3Request as parameter.
355 @param printable_width: the printable width
356 @param styles: styles for HTML=>PDF conversion
357 """
358
359 if callable(rules):
360
361 r = self.r
362
363 if r is not None:
364 representation = r.representation
365 r.representation = "html"
366 try:
367 html = rules(r)
368 except:
369
370 if current.response.s3.debug:
371 raise
372 else:
373 import sys
374 current.log.error(sys.exc_info()[1])
375 html = ""
376 if r is not None:
377 r.representation = representation
378 else:
379
380 html = rules
381
382 parser = S3html2pdf(pageWidth=printable_width,
383 exclude_class_list=["tabs"],
384 styles = styles,
385 )
386 result = parser.parse(html)
387 return result
388
389
391 """
392 Get a list of fields, if the list_fields attribute is provided
393 then use that to extract the fields that are required, otherwise
394 use the list of readable fields.
395 """
396
397 fields = self.list_fields
398 if fields:
399 list_fields = [f for f in fields if f != "id"]
400 else:
401 list_fields = [f.name for f in resource.readable_fields()
402 if f.type != "id" and
403 f.name != "comments" or
404 not self.pdf_hide_comments]
405
406 get_vars = Storage(current.request.get_vars)
407 get_vars["iColumns"] = len(list_fields)
408 dtfilter, orderby, left = resource.datatable_filter(list_fields, get_vars)
409 resource.add_filter(dtfilter)
410
411 result = resource.select(list_fields,
412 left=left,
413 limit=None,
414 count=True,
415 getids=True,
416 orderby=orderby,
417 represent=True,
418 show_links=False)
419
420
421 pdf_table = S3PDFTable(doc,
422 result.rfields,
423 result.rows,
424 groupby = self.pdf_groupby,
425 autogrow = self.table_autogrow,
426 ).build()
427
428 return pdf_table
429
432 """
433 The standard document template for eden reports
434 It allows for the following page templates:
435 1) First Page
436 2) Even Page
437 3) Odd Page
438 4) Landscape Page
439 """
440
441 - def __init__(self,
442 title = "Sahana Eden",
443 margin = (0.5 * inch,
444 0.3 * inch,
445 0.5 * inch,
446 0.3 * inch),
447 margin_inside = 0.0 * inch,
448 size = None,
449 orientation = "Auto",
450 ):
451 """
452 Set up the standard page templates
453 """
454
455 self.output = StringIO()
456
457 if orientation == "Auto":
458
459 self.page_orientation = "Portrait"
460 self.auto_orientation = True
461 else:
462
463 self.page_orientation = orientation
464 self.auto_orientation = False
465
466 if not size:
467 size = current.deployment_settings.get_pdf_size()
468 if size == "Letter":
469 self.paper_size = LETTER
470 elif size == "A4" or not isinstance(size, tuple):
471 self.paper_size = A4
472 else:
473 self.paper_size = size
474
475 self.topMargin = margin[0]
476 self.leftMargin = margin[1]
477 self.bottomMargin = margin[2]
478 self.rightMargin = margin[3]
479 self.insideMargin = margin_inside
480
481 BaseDocTemplate.__init__(self,
482 self.output,
483 title = s3_str(title),
484 leftMargin = self.leftMargin,
485 rightMargin = self.rightMargin,
486 topMargin = self.topMargin,
487 bottomMargin = self.bottomMargin,
488 )
489
490 self.MINIMUM_MARGIN_SIZE = 0.2 * inch
491 self.body_flowable = None
492
493 self._calc()
494
495
497 """
498 Function to return the size a flowable will require
499 """
500
501 if not flowable:
502 return (0, 0)
503 if not isinstance(flowable, list):
504 flowable = [flowable]
505 w = 0
506 h = 0
507 for f in flowable:
508 if f:
509 size = f.wrap(self.printable_width,
510 self.printable_height)
511 if size[0] > w:
512 w = size[PDF_WIDTH]
513 h += size[PDF_HEIGHT]
514 return (w, h)
515
516
518
519 if self.page_orientation == "Landscape":
520 self.pagesize = landscape(self.paper_size)
521 else:
522 self.pagesize = portrait(self.paper_size)
523
524 BaseDocTemplate._calc(self)
525 self.height = self.pagesize[PDF_HEIGHT]
526 self.width = self.pagesize[PDF_WIDTH]
527 self.printable_width = self.width - \
528 self.leftMargin - \
529 self.rightMargin - \
530 self.insideMargin
531 self.printable_height = self.height - \
532 self.topMargin - \
533 self.bottomMargin
534
535
536 - def calc_body_size(self,
537 header_flowable,
538 footer_flowable,
539 ):
540 """
541 Helper function to calculate the various sizes of the page
542 """
543
544 self._calc()
545
546
547
548
549
550
551
552
553
554 header_size = self.get_flowable_size(header_flowable)
555 footer_size = self.get_flowable_size(footer_flowable)
556 self.header_height = header_size[PDF_HEIGHT]
557 self.footer_height = footer_size[PDF_HEIGHT]
558 self.body_height = self.printable_height - \
559 self.header_height - \
560 self.footer_height
561
562
563 - def build(self,
564 header_flowable,
565 body_flowable,
566 footer_flowable,
567 canvasmaker=canvas.Canvas):
568 """
569 Build the document using the flowables.
570
571 Set up the page templates that the document can use
572 """
573
574 self.header_flowable = header_flowable
575 self.body_flowable = body_flowable
576 self.footer_flowable = footer_flowable
577 self.calc_body_size(header_flowable,
578 footer_flowable,
579 )
580 showBoundary = 0
581
582 body_frame = Frame(self.leftMargin,
583 self.bottomMargin + self.footer_height,
584 self.printable_width,
585 self.body_height,
586 leftPadding = 0,
587 bottomPadding = 0,
588 rightPadding = 0,
589 topPadding = 0,
590 id = "body",
591 showBoundary = showBoundary
592 )
593
594 self.body_frame = body_frame
595 self.normalPage = PageTemplate(id = "Normal",
596 frames = [body_frame,],
597 onPage = self.add_page_decorators,
598 pagesize = self.pagesize
599 )
600
601
602
603
604
605
606
607
608
609
610
611 self.landscapePage = PageTemplate(id = "Landscape",
612 frames = [body_frame,],
613 onPage = self.add_page_decorators,
614 pagesize = landscape(self.pagesize)
615 )
616 if self.page_orientation == "Landscape":
617 self.addPageTemplates(self.landscapePage)
618 else:
619 self.addPageTemplates(self.normalPage)
620
621 BaseDocTemplate.build(self, self.body_flowable, canvasmaker=canvasmaker)
622
623
624 - def add_page_decorators(self, canvas, doc):
625 """
626 """
627
628 if self.header_flowable:
629 top = self.bottomMargin + self.printable_height
630 for flow in self.header_flowable:
631 height = self.get_flowable_size(flow)[PDF_HEIGHT]
632 bottom = top - height
633 flow.drawOn(canvas,
634 self.leftMargin,
635 bottom
636 )
637 top = bottom
638 if self.footer_flowable:
639 top = self.bottomMargin + self.footer_height
640 for flow in self.footer_flowable:
641 height = self.get_flowable_size(flow)[PDF_HEIGHT]
642 bottom = top - height
643 flow.drawOn(canvas,
644 self.leftMargin,
645 bottom
646 )
647 top = bottom
648
649
651 """
652 Method to create a paragraph that may be inserted into the document
653
654 @param text: The text for the paragraph
655 @param append: If True then the paragraph will be stored in the
656 document flow ready for generating the pdf.
657
658 @return The paragraph
659
660 This method can return the paragraph rather than inserting into the
661 document. This is useful if the paragraph needs to be first
662 inserted in another flowable, before being added to the document.
663 An example of when this is useful is when large amounts of text
664 (such as a comment) are added to a cell of a table.
665 """
666
667 if text != "":
668 if style == None:
669 styleSheet = getSampleStyleSheet()
670 style = styleSheet["Normal"]
671 text = biDiText(text)
672 para = Paragraph(text, style)
673 if append and self.body_flowable:
674 self.body_flowable.append(para)
675 return para
676 return ""
677
678
680 """
681 Add special styles to the text in a cell
682 """
683
684 if style == "*GREY":
685 return [("TEXTCOLOR", cell, cell, colors.lightgrey)]
686 elif style == "*RED":
687 return [("TEXTCOLOR", cell, cell, colors.red)]
688 return []
689
690
692 """
693 Add special styles to the text in a table
694 """
695
696 row = 0
697 for line in table:
698 col = 0
699 for cell in line:
700 try:
701 if cell.startswith("*"):
702 (instruction, sep, text) = cell.partition(" ")
703 style += self.cellStyle(instruction, (col, row))
704 table[row][col] = text
705 except:
706 pass
707 col += 1
708 row += 1
709 return (table, style)
710
713 """
714 Class to build a table that can then be placed in a pdf document
715
716 The table will be formatted so that is fits on the page. This class
717 doesn't need to be called directly. Rather see S3PDF.addTable()
718 """
719
720 MIN_COL_WIDTH = 200
721 MIN_ROW_HEIGHT = 20
722
723 - def __init__(self,
724 document,
725 rfields,
726 rows,
727 groupby = None,
728 autogrow = False,
729 ):
730 """
731 Constructor
732
733 @param document: the EdenDocTemplate instance in which the table
734 shall be rendered
735 @param rfields: list of resolved field selectors for
736 the columns (S3ResourceData.rfields)
737 @param rows: the represented rows (S3ResourceData.rows)
738 @param groupby: a field name that is to be used as a sub-group
739 - all records with the same value in that the
740 groupby column will be clustered together
741 @param autogrow: what to do about empty space on the page:
742 "H" - make columns wider to fill horizontally
743 "V" - add extra (empty) rows to fill vertically
744 "B" - do both
745 False - do nothing
746 """
747
748 rtl = current.response.s3.rtl
749
750
751 self.doc = document
752
753
754 self.body_height = document.body_height
755 self.autogrow = autogrow
756
757
758 self.font_name = None
759 self.font_name_bold = None
760 set_fonts(self)
761
762
763 list_fields = []
764 labels = []
765 for rfield in rfields:
766 list_fields.append(rfield.fname)
767 labels.append(biDiText(rfield.label))
768 if rtl:
769 list_fields.reverse()
770 labels.reverse()
771 self.list_fields = list_fields
772 self.labels = labels
773
774
775 convert = self.convert
776 data = []
777 append = data.append
778 for row in rows:
779 row_data = [convert(rfield, row[rfield.colname]) for rfield in rfields]
780 if rtl:
781 row_data.reverse()
782 append(row_data)
783 self.data = data
784
785
786 self.header_color = Color(0.73, 0.76, 1)
787 self.odd_color = Color(0.92, 0.92, 1)
788 self.even_color = Color(0.83, 0.84, 1)
789 self.fontsize = 12
790
791
792 self.groupby = groupby
793 self.subheading_rows = []
794 self.subheading_level = {}
795
796
797 self.pdf_data = []
798 self.parts = []
799 self.col_widths = []
800 self.row_heights = []
801
802
803 @classmethod
805 """
806 Convert represented field value into a suitable
807 ReportLab element
808
809 @param rfield: the S3ResourceField
810 @param value: the field value
811 """
812
813 if isinstance(value, (basestring, lazyT)):
814
815 pdf_value = biDiText(value)
816
817 elif isinstance(value, IMG):
818
819 field = rfield.field
820 if field:
821 pdf_value = S3html2pdf.parse_img(value, field.uploadfolder)
822 if pdf_value:
823 pdf_value = pdf_value[0]
824 else:
825 pdf_value = ""
826
827 elif isinstance(value, DIV):
828
829 if len(value.components) > 0:
830 pdf_value = cls.convert(rfield, value.components[0])
831 else:
832 pdf_value = biDiText(value)
833
834 else:
835
836 pdf_value = biDiText(value)
837
838 return pdf_value
839
840
842 """
843 Method to build the table.
844
845 @returns: a list of ReportLab Table instances
846 - if the table fits into the page width, this list will
847 contain a single Table, otherwise it will contain the
848 parts of the split table in the order in which they
849 shall be inserted into the main document
850 """
851
852 if self.groupby:
853 data = self.group_data()
854 data = [self.labels] + data
855 elif self.data != None:
856 data = [self.labels] + self.data
857 self.pdf_data = data
858
859
860 if not data or not (data[0]):
861 return None
862
863 style = self.table_style(0, len(data), len(self.labels) - 1)
864
865 self.parts = self.calc(data, style)
866
867 return self.presentation()
868
869
871 """
872 Group the rows
873
874 @returns: the PDF-formatted data with grouping headers
875
876 FIXME: will not work with RTL-biDiText or any non-text
877 representation, rewrite to use raw resource data
878 """
879
880 groups = self.groupby.split(",")
881 new_data = []
882 data = self.data
883 level = 0
884 list_fields = self.list_fields
885 for field in groups:
886 level += 1
887 field = field.strip()
888
889 i = 0
890 rowlength = len(list_fields)
891 while i < rowlength:
892 if list_fields[i] == field:
893 break
894 i += 1
895 list_fields = list_fields[0:i] + list_fields[i + 1:]
896 labels = self.labels[0:i] + self.labels[i + 1:]
897 self.labels = labels
898 current_group = None
899 r = 0
900 for row in data:
901 if r + 1 in self.subheading_rows:
902 new_data.append(row)
903 r += 1
904 else:
905 try:
906 group = row[i]
907 if group != current_group:
908 line = [group]
909 new_data.append(line)
910 r += 1
911 current_group = group
912 self.subheading_rows.append(r)
913 self.subheading_level[r] = level
914
915
916 for x in range (len(self.subheading_rows)):
917 if self.subheading_rows[x] > r:
918 posn = self.subheading_rows[x]
919 self.subheading_rows[x] = posn + 1
920 oldlevel = self.subheading_level[posn]
921 del self.subheading_level[posn]
922 self.subheading_level[posn + 1] = oldlevel
923 line = row[0:i] + row[i + 1:]
924 new_data.append(line)
925 r += 1
926 except:
927 new_data.append(row)
928 r += 1
929 data = new_data
930 new_data = []
931
932 self.list_fields = list_fields
933 return data
934
935
936 - def calc(self, data, style):
937 """
938 Compute rendering parameters:
939 - formatted output data
940 - row heights and column widths
941 - font-size
942
943 @returns: the table parts, a list of row data arrays:
944 [ [[value1, value2, value3, ...], ...], ...]
945 """
946
947 main_doc = self.doc
948
949
950 if main_doc.auto_orientation:
951 orientation = "Auto"
952 else:
953 orientation = main_doc.page_orientation
954 temp_doc = EdenDocTemplate(orientation = orientation,
955 size = main_doc.paper_size,
956 )
957
958
959 style = style[:]
960
961
962 table = Table(data, repeatRows=1, style=style, hAlign="LEFT")
963 temp_doc.build(None, [table], None)
964 col_widths = table._colWidths
965 row_heights = table._rowHeights
966
967
968 table_width = sum(col_widths)
969 fit = table_width <= temp_doc.printable_width
970
971
972 min_width = self.MIN_COL_WIDTH
973 if not fit:
974
975
976 para_cols = set(i for i, _ in enumerate(self.labels)
977 if col_widths[i] > min_width)
978 else:
979 para_cols = ()
980
981 fontsize = self.fontsize
982 min_fontsize = fontsize - 3
983
984 stylesheet = getSampleStyleSheet()
985 para_style = stylesheet["Normal"]
986
987 adj_data = data = self.pdf_data
988 while not fit:
989
990
991 available_margin_space = main_doc.leftMargin + \
992 main_doc.rightMargin - \
993 2 * main_doc.MINIMUM_MARGIN_SIZE
994
995 overlap = table_width - temp_doc.printable_width
996 if overlap < available_margin_space:
997
998 fit = True
999 main_doc.leftMargin -= overlap / 2
1000 main_doc.rightMargin -= overlap / 2
1001 break
1002
1003
1004 adj_data = []
1005 para_style.fontSize = fontsize
1006
1007 for row_index, row in enumerate(data):
1008 if row_index == 0:
1009 adj_data.append(row)
1010 else:
1011 temp_row = []
1012 for col_index, item in enumerate(row):
1013 if col_index in para_cols:
1014 col_widths[col_index] = min_width
1015 para = main_doc.addParagraph(item,
1016 style=para_style,
1017 append=False,
1018 )
1019 temp_row.append(para)
1020 else:
1021 temp_row.append(item)
1022 adj_data.append(temp_row)
1023
1024
1025 style[1] = ("FONTSIZE", (0, 0), (-1, -1), fontsize)
1026 table = Table(adj_data,
1027 repeatRows = 1,
1028 style = style,
1029 colWidths = col_widths,
1030 hAlign = "LEFT",
1031 )
1032 temp_doc.build(None, [table], None)
1033
1034 col_widths = table._colWidths
1035 row_heights = table._rowHeights
1036
1037
1038 table_width = sum(col_widths)
1039 if table_width > temp_doc.printable_width:
1040
1041 if fontsize <= min_fontsize:
1042
1043 if main_doc.page_orientation == "Portrait" and \
1044 main_doc.auto_orientation:
1045
1046
1047 temp_doc.page_orientation = \
1048 main_doc.page_orientation = "Landscape"
1049
1050
1051 temp_doc._calc()
1052 main_doc._calc()
1053 self.body_height = main_doc.printable_height - \
1054 main_doc.header_height - \
1055 main_doc.footer_height
1056
1057
1058 fontsize = self.fontsize
1059 else:
1060 break
1061 else:
1062
1063 fontsize -= 1
1064 else:
1065 fit = True
1066 break
1067
1068
1069 self.pdf_data = adj_data
1070 self.fontsize = fontsize
1071 self.col_widths = [col_widths]
1072 self.row_heights = [row_heights]
1073
1074 return [adj_data] if fit else self.split(temp_doc)
1075
1076
1078 """
1079 Render all data parts (self.parts) as a list of ReportLab Tables.
1080
1081 @returns: a list of ReportLab Table instances
1082 """
1083
1084
1085 tables = []
1086
1087 main_doc = self.doc
1088 autogrow = self.autogrow
1089 body_height = self.body_height
1090 default_row_height = self.MIN_ROW_HEIGHT
1091
1092 num_parts = len(self.parts)
1093 num_horz_parts = len(self.col_widths)
1094
1095
1096 if autogrow == "H" or autogrow == "B":
1097
1098 printable_width = self.doc.printable_width
1099 new_col_widths = []
1100
1101 for widths in self.col_widths:
1102 total_width = sum(widths)
1103 if total_width and total_width < printable_width:
1104 factor = 1 + (printable_width - total_width) / total_width
1105 new_col_widths.append([width * factor for width in widths])
1106 else:
1107 new_col_widths.append(widths)
1108
1109 self.col_widths = new_col_widths
1110
1111
1112 start_row = 0
1113 for current_part, part in enumerate(self.parts):
1114
1115 if part == []:
1116 continue
1117 num_rows = len(part)
1118
1119 col_widths = self.col_widths[current_part % num_horz_parts]
1120 row_heights = self.row_heights[int(current_part / num_horz_parts)]
1121
1122
1123 if autogrow == "V" or autogrow == "B":
1124
1125 total_height = sum(row_heights)
1126 available_height = body_height - total_height
1127
1128 if available_height > default_row_height:
1129 num_extra_rows = int(available_height / default_row_height)
1130 if num_extra_rows:
1131 part += [[""] * len(col_widths)] * num_extra_rows
1132 row_heights = list(row_heights) + \
1133 [default_row_height] * num_extra_rows
1134
1135 style = self.table_style(start_row, num_rows, len(col_widths) - 1)
1136 (part, style) = main_doc.addCellStyling(part, style)
1137
1138 p = Table(part,
1139 repeatRows = 1,
1140 style = style,
1141 hAlign = "LEFT",
1142 colWidths = col_widths,
1143 rowHeights = row_heights,
1144 emptyTableAction = "indicate",
1145 )
1146 tables.append(p)
1147
1148
1149 next_part = current_part + 1
1150 if next_part < num_parts:
1151 tables.append(PageBreak())
1152 if next_part % num_horz_parts == 0:
1153 start_row += num_rows - 1
1154
1155
1156 return tables
1157
1158
1159 - def split(self, temp_doc):
1160 """
1161 Helper for calc(): split the table horizontally so that each
1162 part fits into the page width
1163
1164 @param temp_doc: the temporary doc
1165
1166 @returns: the data slices for each part
1167 """
1168
1169 col_widths = self.col_widths[0]
1170 row_heights = self.row_heights[0]
1171
1172
1173 total = 0
1174 split_cols = []
1175 new_col_widths = []
1176 part_col_widths = []
1177
1178 for i, col_width in enumerate(col_widths):
1179 if i > 0 and total + col_width > temp_doc.printable_width:
1180
1181 split_cols.append(i)
1182 new_col_widths.append(part_col_widths)
1183
1184 part_col_widths = [col_width]
1185 total = col_width
1186 else:
1187
1188 part_col_widths.append(col_width)
1189 total += col_width
1190
1191 split_cols.append(len(col_widths))
1192 new_col_widths.append(part_col_widths)
1193 self.col_widths = new_col_widths
1194
1195
1196 total = 0
1197 split_rows = []
1198 new_row_heights = []
1199 page_row_heights = []
1200
1201 body_height = self.body_height
1202 header_height = 20
1203 for i, row_height in enumerate(row_heights):
1204
1205
1206 if i == 0:
1207 header_height = row_height
1208
1209 if total + row_height > body_height:
1210
1211 new_row_heights.append(page_row_heights)
1212
1213 page_row_heights = [header_height, row_height] if i > 0 else [row_height]
1214 total = sum(page_row_heights)
1215 split_rows.append(i)
1216 else:
1217 page_row_heights.append(row_height)
1218 total += row_height
1219
1220 split_rows.append(len(row_heights))
1221 new_row_heights.append(page_row_heights)
1222 self.row_heights = new_row_heights
1223
1224
1225 pdf_data = self.pdf_data
1226 all_labels = self.labels
1227 subheading_rows = self.subheading_rows
1228 parts = []
1229
1230 start_row = 1
1231 for end_row in split_rows:
1232
1233 start_col = 0
1234
1235 for end_col in split_cols:
1236
1237 part = []
1238 pappend = part.append
1239
1240
1241 labels = []
1242 lappend = labels.append
1243 for col_index in range(start_col, end_col):
1244 try:
1245 lappend(all_labels[col_index])
1246 except IndexError:
1247 lappend("")
1248 pappend(labels)
1249
1250
1251 for row_index in range(start_row, end_row):
1252
1253 row_data = pdf_data[row_index]
1254
1255 out_row = []
1256 oappend = out_row.append
1257
1258 for col_index in range(start_col, end_col):
1259 try:
1260 oappend(row_data[col_index])
1261 except IndexError:
1262
1263
1264 if len(out_row) == 0 and row_index in subheading_rows:
1265 try:
1266 oappend(row_data[0])
1267 except IndexError:
1268 oappend("")
1269 else:
1270 oappend("")
1271
1272 pappend(out_row)
1273
1274 parts.append(part)
1275 start_col = end_col
1276 start_row = end_row
1277
1278 return parts
1279
1280
1281 - def table_style(self, start_row, row_cnt, end_col, colour_required=False):
1282 """
1283 Internally used method to assign a style to the table
1284
1285 @param start_row: The row from the data that the first data row in
1286 the table refers to. When a table is split the first row in the
1287 table (ignoring the label header row) will not always be the first row
1288 in the data. This is needed to align the two. Currently this parameter
1289 is used to identify sub headings and give them an emphasised styling
1290 @param row_cnt: The number of rows in the table
1291 @param end_col: The last column in the table
1292
1293 FIXME: replace end_col with -1
1294 (should work but need to test with a split table)
1295 """
1296
1297 font_name_bold = self.font_name_bold
1298
1299 style = [("FONTNAME", (0, 0), (-1, -1), self.font_name),
1300 ("FONTSIZE", (0, 0), (-1, -1), self.fontsize),
1301 ("VALIGN", (0, 0), (-1, -1), "TOP"),
1302 ("LINEBELOW", (0, 0), (end_col, 0), 1, Color(0, 0, 0)),
1303 ("FONTNAME", (0, 0), (end_col, 0), font_name_bold),
1304 ]
1305 sappend = style.append
1306 if colour_required:
1307 sappend(("BACKGROUND", (0, 0), (end_col, 0), self.header_color))
1308 else:
1309 sappend(("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey))
1310 sappend(("INNERGRID", (0, 0), (-1, -1), 0.2, colors.lightgrey))
1311 if self.groupby != None:
1312 sappend(("LEFTPADDING", (0, 0), (-1, -1), 20))
1313
1314 row_color_cnt = 0
1315 for i in range(row_cnt):
1316
1317 if start_row + i in self.subheading_rows:
1318 level = self.subheading_level[start_row + i]
1319 if colour_required:
1320 sappend(("BACKGROUND", (0, i), (end_col, i),
1321 self.header_color))
1322 sappend(("FONTNAME", (0, i), (end_col, i), font_name_bold))
1323 sappend(("SPAN", (0, i), (end_col, i)))
1324 sappend(("LEFTPADDING", (0, i), (end_col, i), 6 * level))
1325 elif i > 0:
1326 if colour_required:
1327 if row_color_cnt % 2 == 0:
1328 sappend(("BACKGROUND", (0, i), (end_col, i),
1329 self.even_color))
1330 row_color_cnt += 1
1331 else:
1332 sappend(("BACKGROUND", (0, i), (end_col, i),
1333 self.odd_color))
1334 row_color_cnt += 1
1335 sappend(("BOX", (0, 0), (-1, -1), 1, Color(0, 0, 0)))
1336 return style
1337
1340 """
1341 Class that takes HTML in the form of web2py helper objects
1342 and converts it to PDF
1343 """
1344
1345 - def __init__(self,
1346 pageWidth,
1347 exclude_class_list=None,
1348 styles=None):
1349 """
1350 Constructor
1351
1352 @param pageWidth: the printable width
1353 @param exclude_class_list: list of classes for elements to skip
1354 @param styles: the styles dict from the caller
1355 """
1356
1357
1358 self.font_name = None
1359 self.font_name_bold = None
1360 set_fonts(self)
1361
1362 if exclude_class_list is None:
1363 self.exclude_class_list = []
1364 else:
1365 self.exclude_class_list = exclude_class_list
1366
1367 self.pageWidth = pageWidth
1368 self.fontsize = 10
1369
1370
1371 styleSheet = getSampleStyleSheet()
1372
1373 self.plainstyle = styleSheet["Normal"]
1374 self.plainstyle.fontName = self.font_name
1375 self.plainstyle.fontSize = 9
1376
1377 self.boldstyle = deepcopy(styleSheet["Normal"])
1378 self.boldstyle.fontName = self.font_name_bold
1379 self.boldstyle.fontSize = 10
1380
1381 self.titlestyle = deepcopy(styleSheet["Normal"])
1382 self.titlestyle.fontName = self.font_name_bold
1383 self.titlestyle.fontSize = 16
1384
1385 self.normalstyle = self.plainstyle
1386
1387
1388
1389
1390
1391 self.style_lookup = {"pdf_title": self.titlestyle,
1392 }
1393
1394
1395 self.styles = styles
1396
1397
1399 """
1400 Entry point for class
1401 """
1402
1403 result = self.select_tag(html)
1404 return result
1405
1406
1408 """
1409 """
1410
1411 if self.exclude_tag(html):
1412 return None
1413 if isinstance(html, TABLE):
1414 return self.parse_table(html)
1415 elif isinstance(html, A):
1416 return self.parse_a(html)
1417 elif isinstance(html, (P, H1, H2, H3, H4, H5, H6)):
1418 return self.parse_p(html)
1419 elif isinstance(html, IMG):
1420 return S3html2pdf.parse_img(html)
1421 elif isinstance(html, DIV):
1422 return self.parse_div(html)
1423 elif (isinstance(html, basestring) or isinstance(html, lazyT)):
1424 html = s3_str(html)
1425 if "<" in html:
1426 html = s3_strip_markup(html)
1427 if title:
1428 para = [Paragraph(biDiText(html), self.boldstyle)]
1429 else:
1430 para = [Paragraph(biDiText(html), self.normalstyle)]
1431 self.normalstyle = self.plainstyle
1432 return para
1433 return None
1434
1435
1437 """
1438 """
1439
1440 try:
1441 if html.attributes["_class"] in self.exclude_class_list:
1442 return True
1443 if html.attributes["_class"] in self.style_lookup:
1444 self.normalstyle = self.style_lookup[html.attributes["_class"]]
1445 except:
1446 pass
1447 return False
1448
1449
1451 """
1452 Parses a DIV element and converts it into a format for ReportLab
1453
1454 @param html: the DIV element to convert
1455 @return: a list containing text that ReportLab can use
1456 """
1457
1458 content = []
1459 select_tag = self.select_tag
1460 for component in html.components:
1461 result = select_tag(component)
1462 if result != None:
1463 content += result
1464 if content == []:
1465 return None
1466 return content
1467
1468
1470 """
1471 Parses an A element and converts it into a format for ReportLab
1472
1473 @param html: the A element to convert
1474 @return: a list containing text that ReportLab can use
1475 """
1476
1477 content = []
1478 select_tag = self.select_tag
1479 for component in html.components:
1480 result = select_tag(component)
1481 if result != None:
1482 content += result
1483 if content == []:
1484 return None
1485 return content
1486
1487
1488 @staticmethod
1490 """
1491 Parses an IMG element and converts it into an Image for ReportLab
1492
1493 @param html: the IMG element to convert
1494 @param uploadfolder: an optional uploadfolder in which to find the file
1495 @return: a list containing an Image that ReportLab can use
1496
1497
1498 @note: The `src` attribute of the image must either
1499 point to a static resource, directly to a file, or to an upload.
1500 """
1501
1502 I = None
1503 src = html.attributes.get("_src")
1504 if src:
1505 if uploadfolder:
1506
1507 src = src.rsplit("/", 1)
1508 src = os.path.join(uploadfolder, src[1])
1509 else:
1510 request = current.request
1511 base_url = "/%s/" % request.application
1512 STATIC = "%sstatic" % base_url
1513 if src.startswith(STATIC):
1514
1515 src = src.split(base_url)[-1]
1516 src = src.replace("/", os.sep)
1517 src = os.path.join(request.folder, src)
1518 else:
1519
1520
1521 src = src.rsplit("/", 1)
1522 src = os.path.join(request.folder,
1523 "uploads", src[1])
1524 if os.path.exists(src):
1525 from reportlab.platypus import Image
1526 I = Image(src)
1527
1528 if not I:
1529 return None
1530
1531 iwidth = I.drawWidth
1532 iheight = I.drawHeight
1533
1534
1535 resolution = 96
1536 if "_height" in html.attributes:
1537 height = int(html.attributes["_height"]) * inch / resolution
1538 width = iwidth * (height / iheight)
1539 elif "_width" in html.attributes:
1540 width = int(html.attributes["_width"]) * inch / resolution
1541 height = iheight * (width / iwidth)
1542 else:
1543 height = 1.0 * inch
1544 width = iwidth * (height / iheight)
1545
1546 I.drawHeight = height
1547 I.drawWidth = width
1548 return [I]
1549
1550
1552 """
1553 Parses a P element and converts it into a format for ReportLab
1554
1555 @param html: the P element to convert
1556 @return: a list containing text that ReportLab can use
1557 """
1558
1559 font_sizes = {"p": 9,
1560 "h1": 18,
1561 "h2": 16,
1562 "h3": 14,
1563 "h4": 12,
1564 "h5": 10,
1565 "h6": 9,
1566 }
1567
1568 font_size = None
1569 title = False
1570 try:
1571 tag = html.tag
1572 except AttributeError:
1573 pass
1574 else:
1575 font_size = font_sizes.get(tag)
1576 title = tag != "p"
1577 style = self.boldstyle if title else self.normalstyle
1578
1579 if font_size:
1580 default_font_size = style.fontSize
1581 style.fontSize = font_size
1582 style.spaceAfter = 8
1583
1584 content = []
1585 select_tag = self.select_tag
1586 for component in html.components:
1587 result = select_tag(component, title=title)
1588 if result != None:
1589 content += result
1590
1591 if font_size:
1592 style.fontSize = default_font_size
1593
1594 if content == []:
1595 return None
1596 return content
1597
1598
1600 """
1601 Parses a TABLE element and converts it into a format for ReportLab
1602
1603 @param html: the TABLE element to convert
1604 @return: a list containing text that ReportLab can use
1605 """
1606
1607 table_classes = (html["_class"] or "").split()
1608
1609 style = [("FONTSIZE", (0, 0), (-1, -1), self.fontsize),
1610 ("VALIGN", (0, 0), (-1, -1), "TOP"),
1611 ("FONTNAME", (0, 0), (-1, -1), self.font_name),
1612 ]
1613 if "no-grid" not in table_classes:
1614 style.append(("GRID", (0, 0), (-1, -1), 0.5, colors.grey))
1615
1616 content = self.parse_table_components(html,
1617 style = style,
1618 )[0]
1619
1620 if content == []:
1621 return None
1622
1623 table = Table(content,
1624 style = style,
1625 hAlign = "LEFT",
1626 vAlign = "Top",
1627 repeatRows = 1 if "repeat-header" in table_classes else 0,
1628 )
1629
1630 if "shrink-to-fit" in table_classes:
1631
1632 table.wrap(self.pageWidth, 0)
1633
1634 cw = table._colWidths
1635 tw = sum(cw)
1636 pw = self.pageWidth
1637
1638 if tw and tw > pw:
1639
1640 factor = pw / tw
1641 cw = [w * factor for w in cw]
1642
1643
1644 table = Table(content,
1645 style = style,
1646 hAlign = "LEFT",
1647 vAlign = "Top",
1648 repeatRows = 1 if "repeat-header" in table_classes else 0,
1649 colWidths = cw
1650 )
1651
1652 return [table]
1653
1654
1660 """
1661 Parses TABLE components
1662
1663 @param table: the TABLE instance or a subcomponent of it
1664 @param content: the current content array
1665 @param row_count: the current number of rows in the content array
1666 @param style: the style list
1667 """
1668
1669 if content is None:
1670 content = []
1671 cappend = content.append
1672
1673 rowspans = []
1674
1675 exclude_tag = self.exclude_tag
1676 parse_tr = self.parse_tr
1677 parse = self.parse_table_components
1678
1679 for component in table.components:
1680 result = None
1681
1682 if exclude_tag(component):
1683 continue
1684
1685 if isinstance(component, (THEAD, TBODY, TFOOT)):
1686 content, row_count = parse(component,
1687 content = content,
1688 row_count = row_count,
1689 style = style,
1690 )
1691
1692 elif isinstance(component, TR):
1693 result = parse_tr(component, style, row_count, rowspans)
1694 row_count += 1
1695
1696 if result != None:
1697 cappend(result)
1698
1699 return content, row_count
1700
1701
1702 - def parse_tr(self, html, style, rowCnt, rowspans):
1703 """
1704 Parses a TR element and converts it into a format for ReportLab
1705
1706 @param html: the TR element to convert
1707 @param style: the default style
1708 @param rowCnt: the row counter
1709 @param rowspans: the remaining rowspans (if any)
1710
1711 @return: a list containing text that ReportLab can use
1712 """
1713
1714
1715 row_styles = self._styles(html)
1716
1717 background = self._color(row_styles.get("background-color"))
1718 color = self._color(row_styles.get("color"))
1719
1720 row = []
1721 rappend = row.append
1722 sappend = style.append
1723
1724 select_tag = self.select_tag
1725 font_name_bold = self.font_name_bold
1726
1727 exclude_tag = self.exclude_tag
1728
1729 colCnt = 0
1730 rspan_index = -1
1731 for component in html.components:
1732
1733 if not isinstance(component, (TH, TD)) or \
1734 exclude_tag(component):
1735 continue
1736
1737 rspan_index += 1
1738
1739 if len(rowspans) < (rspan_index + 1):
1740 rowspans.append(0)
1741
1742 if rowspans[rspan_index]:
1743 rappend("")
1744 rowspans[rspan_index] -= 1
1745 cell = (colCnt, rowCnt)
1746 sappend(("LINEABOVE", cell, cell, 0, colors.white))
1747 colCnt += 1
1748
1749 if component.components == []:
1750 rappend("")
1751 continue
1752
1753 rowspan = component.attributes.get("_rowspan")
1754 if rowspan:
1755
1756 rowspans[rspan_index] = rowspan - 1
1757
1758 colspan = component.attributes.get("_colspan", 1)
1759 for detail in component.components:
1760 if color:
1761 self.normalstyle.textColor = color
1762 else:
1763
1764 self.normalstyle.textColor = colors.black
1765
1766
1767 result = select_tag(detail, title=isinstance(component, TH))
1768 if result is None:
1769 continue
1770 rappend(result)
1771
1772
1773 cell = (colCnt, rowCnt)
1774 if color:
1775 sappend(("TEXTCOLOR", cell, cell, color))
1776 if background:
1777 sappend(("BACKGROUND", cell, cell, background))
1778 elif isinstance(component, TH):
1779 sappend(("BACKGROUND", cell, cell, colors.lightgrey))
1780 sappend(("FONTNAME", cell, cell, font_name_bold))
1781
1782
1783 if colspan > 1:
1784 for i in xrange(1, colspan):
1785 rappend("")
1786 sappend(("SPAN", cell, (colCnt + colspan - 1, rowCnt)))
1787 colCnt += colspan
1788 else:
1789 colCnt += 1
1790
1791 if row == []:
1792 return None
1793 else:
1794 return row
1795
1796
1798 """
1799 Get the custom styles for the given element (match by tag and
1800 classes)
1801
1802 @param element: the HTML element (web2py helper)
1803 @param styles: the pdf_html_styles dict
1804 """
1805
1806 element_styles = {}
1807
1808 styles = self.styles
1809 if styles:
1810
1811 classes = element["_class"]
1812 if classes:
1813 tag = element.tag
1814 classes = set(classes.split(" "))
1815 for k, v in styles.items():
1816 t, c = k.split(".", 1)
1817 if t != tag:
1818 continue
1819 keys = set(c.split("."))
1820 if keys <= classes:
1821 element_styles.update(v)
1822
1823 return element_styles
1824
1825
1826 @staticmethod
1828 """
1829 Get the Color instance from colors for:
1830 a given name (e.g. 'white')
1831 or
1832 Hex string (e.g. '#FFFFFF')
1833
1834 @param val: the name or hex string
1835 """
1836
1837 if not val:
1838 color = None
1839 else:
1840 if val[:1] == '#':
1841 color = HexColor(val)
1842 else:
1843 try:
1844 color = object.__getattribute__(colors, val)
1845 except AttributeError:
1846 color = None
1847 return color
1848
1849
1850