Package s3 :: Package codecs :: Module pdf
[frames] | no frames]

Source Code for Module s3.codecs.pdf

   1  # -*- coding: utf-8 -*- 
   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    # Faster, where available 
  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 
82 83 # ----------------------------------------------------------------------------- 84 -def set_fonts(instance):
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 # Requires the font-files at /static/fonts/<font_name>.ttf 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 # Use the default "Helvetica" and "Helvetica-Bold" 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 # Use the default "Helvetica" and "Helvetica-Bold" 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
149 # ============================================================================= 150 -class S3RL_PDF(S3Codec):
151 """ 152 Simple Report Labs PDF format codec 153 """ 154
155 - def __init__(self):
156 """ 157 Constructor 158 """ 159 160 # Error codes 161 self.ERROR = Storage( 162 RL_ERROR = "Python needs the ReportLab module installed for PDF export" 163 ) 164 165 # Fonts 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 # Settings 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 # Get the title & filename 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 # Must be str not unicode 247 title = title.encode("utf-8") 248 filename = "%s_%s.pdf" % (title, now) 249 elif len(filename) < 5 or filename[-4:] != ".pdf": 250 # Add extension 251 filename = "%s.pdf" % filename 252 self.filename = filename 253 254 # Get the Doc Template 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 # HTML styles 265 pdf_html_styles = attr_get("pdf_html_styles") 266 267 # Get the header 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 # Get the footer 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 # Build report template 294 295 # Get data for the body of the text 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 # Get the document body from the callback 304 body_flowable = self.get_html_flowable(callback(r), 305 doc.printable_width, 306 styles = pdf_html_styles, 307 ) 308 309 elif pdf_componentname: # and resource.parent is None: 310 # Enforce a particular component 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 # Use the requested resource 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 # Build the PDF 332 doc.build(header_flowable, 333 body_flowable, 334 footer_flowable, 335 ) 336 337 # Return the generated PDF 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 # -------------------------------------------------------------------------
347 - def get_html_flowable(self, rules, printable_width, styles=None):
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 # Callback to produce the HTML (e.g. rheader) 361 r = self.r 362 # Switch to HTML representation 363 if r is not None: 364 representation = r.representation 365 r.representation = "html" 366 try: 367 html = rules(r) 368 except: 369 # Unspecific except => must raise in debug mode 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 # Static HTML 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 # -------------------------------------------------------------------------
390 - def get_resource_flowable(self, resource, doc):
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 # Now generate the PDF table 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
430 # ============================================================================= 431 -class EdenDocTemplate(BaseDocTemplate):
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, # top 444 0.3 * inch, # left 445 0.5 * inch, # bottom 446 0.3 * inch), # right 447 margin_inside = 0.0 * inch, # used for odd even pages 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 # Start with "Portrait", allow later adjustment 459 self.page_orientation = "Portrait" 460 self.auto_orientation = True 461 else: 462 # Fixed page orientation 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 # -------------------------------------------------------------------------
496 - def get_flowable_size(self, flowable):
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 # -------------------------------------------------------------------------
517 - def _calc(self):
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() # in case we changed margins sizes etc 545 #self.height = self.pagesize[PDF_HEIGHT] 546 #self.width = self.pagesize[PDF_WIDTH] 547 #self.printable_width = self.width - \ 548 # self.leftMargin - \ 549 # self.rightMargin - \ 550 # self.insideMargin 551 #self.printable_height = self.height - \ 552 # self.topMargin - \ 553 # self.bottomMargin 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 # for debugging set to 1, otherwise 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 # @todo set these page templates up 601 #self.evenPage = PageTemplate(id = "even", 602 # frames = frame_list, 603 # onPage = self.onEvenPage, 604 # pagesize = self.pagesize 605 # ) 606 #self.oddPage = PageTemplate(id = "odd", 607 # frames = frame_list, 608 # onPage = self.onOddPage, 609 # pagesize = self.pagesize 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 # -------------------------------------------------------------------------
650 - def addParagraph(self, text, style=None, append=True):
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 # -------------------------------------------------------------------------
679 - def cellStyle(self, style, cell):
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 # -------------------------------------------------------------------------
691 - def addCellStyling(self, table, style):
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
711 # ============================================================================= 712 -class S3PDFTable(object):
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 # The main document 751 self.doc = document 752 753 # Parameters for rendering 754 self.body_height = document.body_height 755 self.autogrow = autogrow 756 757 # Set fonts 758 self.font_name = None 759 self.font_name_bold = None 760 set_fonts(self) 761 762 # Determine list fields and collect the labels 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 # Convert the input data into suitable ReportLab elements 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 # Initialize style parameters (can be changed by caller after init) 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 # Initialize groups 792 self.groupby = groupby 793 self.subheading_rows = [] 794 self.subheading_level = {} 795 796 # Initialize output 797 self.pdf_data = [] 798 self.parts = [] 799 self.col_widths = [] 800 self.row_heights = []
801 802 # ------------------------------------------------------------------------- 803 @classmethod
804 - def convert(cls, rfield, value):
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 # -------------------------------------------------------------------------
841 - def build(self):
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 # Only build the table if we have some data 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 # -------------------------------------------------------------------------
870 - def group_data(self):
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 # Find the location of field in list_fields 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 # All existing subheadings after this point need to 915 # be shuffled down one place. 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 # A temporary document to test and adjust rendering parameters 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 # Make a copy so it can be changed 959 style = style[:] 960 961 # Build the table to determine row heights and column widths 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 # Determine the overall table width and whether it fits into the page 968 table_width = sum(col_widths) 969 fit = table_width <= temp_doc.printable_width 970 971 # Prepare possible adjustments 972 min_width = self.MIN_COL_WIDTH 973 if not fit: 974 # Determine which columns should be wrapped in Paragraphs 975 # for automatic line breaks (=primary means to reduce width) 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 # Adjust margins 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 # This will work => exit with current adjustments 998 fit = True 999 main_doc.leftMargin -= overlap / 2 1000 main_doc.rightMargin -= overlap / 2 1001 break 1002 1003 # Wrap wide columns in Paragraphs (for automatic line breaks) 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 # Rebuild temp_doc to re-calculate col widths and row heights 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 # Check if table fits into page 1038 table_width = sum(col_widths) 1039 if table_width > temp_doc.printable_width: 1040 1041 if fontsize <= min_fontsize: 1042 # Last resort: try changing page orientation 1043 if main_doc.page_orientation == "Portrait" and \ 1044 main_doc.auto_orientation: 1045 1046 # Switch to Landscape 1047 temp_doc.page_orientation = \ 1048 main_doc.page_orientation = "Landscape" 1049 1050 # Re-calculate page size and margins 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 # Reset font-size 1058 fontsize = self.fontsize 1059 else: 1060 break 1061 else: 1062 # Reduce font-size 1063 fontsize -= 1 1064 else: 1065 fit = True 1066 break 1067 1068 # Store adjusted rendering parameters 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 # -------------------------------------------------------------------------
1077 - def presentation(self):
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 # Build the tables 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 # Auto-grow horizontally (=make columns wider to fill page width) 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 # Render each part 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 # Auto-grow vertically (=add empty extra rows) 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 # Add a page break, except for the last part 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 # Don't include the heading 1154 1155 # Return a list of table objects 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 # Split columns 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 # Split before this column... 1181 split_cols.append(i) 1182 new_col_widths.append(part_col_widths) 1183 # ...and start a new part 1184 part_col_widths = [col_width] 1185 total = col_width 1186 else: 1187 # Append this column to the current part 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 # Split rows 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 # default 1203 for i, row_height in enumerate(row_heights): 1204 1205 # Remember the actual header height 1206 if i == 0: 1207 header_height = row_height 1208 1209 if total + row_height > body_height: 1210 # Split before this row 1211 new_row_heights.append(page_row_heights) 1212 # ...and start a new page 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 # Split the data into slices (parts) 1225 pdf_data = self.pdf_data 1226 all_labels = self.labels 1227 subheading_rows = self.subheading_rows 1228 parts = [] 1229 1230 start_row = 1 # skip labels => generating new partial label-rows 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 # Add the labels-row to the part 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 # Add all other rows 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 # If this is the first column of a subheading row then 1263 # repeat the subheading: 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 # used to alternate the colours correctly when we have subheadings 1315 for i in range(row_cnt): 1316 # If subheading 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
1338 # ============================================================================= 1339 -class S3html2pdf():
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 # Fonts 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 # Standard styles 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 # To add more PDF styles define the style above (just like the titlestyle) 1388 # Then add the style and the name to the lookup dict below 1389 # These can then be added to the html in the code as follows: 1390 # TD("Waybill", _class="pdf_title") 1391 self.style_lookup = {"pdf_title": self.titlestyle, 1392 } 1393 1394 # Additional styles from the caller 1395 self.styles = styles
1396 1397 # -------------------------------------------------------------------------
1398 - def parse(self, html):
1399 """ 1400 Entry point for class 1401 """ 1402 1403 result = self.select_tag(html) 1404 return result
1405 1406 # -------------------------------------------------------------------------
1407 - def select_tag(self, html, title=False):
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 # -------------------------------------------------------------------------
1436 - def exclude_tag(self, html):
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 # -------------------------------------------------------------------------
1450 - def parse_div(self, html):
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 # -------------------------------------------------------------------------
1469 - def parse_a(self, html):
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
1489 - def parse_img(html, uploadfolder=None):
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 # Assume that src is a filename directly off the uploadfolder 1507 src = src.rsplit("/", 1) # Don't use os.sep here 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 # Assume that filename is specified as a URL in static 1515 src = src.split(base_url)[-1] 1516 src = src.replace("/", os.sep) 1517 src = os.path.join(request.folder, src) 1518 else: 1519 # Assume that filename is in root of main uploads folder 1520 # @ToDo: Allow use of subfolders! 1521 src = src.rsplit("/", 1) # Don't use os.sep here 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 # Assuming 96dpi original resolution 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 # -------------------------------------------------------------------------
1551 - def parse_p(self, html):
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 # -------------------------------------------------------------------------
1599 - def parse_table(self, html):
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 # Calculate column widths 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 # Table overflows => adjust column widths proportionally 1640 factor = pw / tw 1641 cw = [w * factor for w in cw] 1642 1643 # Re-instantiate with colWidths 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 # -------------------------------------------------------------------------
1655 - def parse_table_components(self, 1656 table, 1657 content=None, 1658 row_count=0, 1659 style=None):
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 # Identify custom styles 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 # @ToDo: Centre the text across the rows 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 # Reset to black 1764 self.normalstyle.textColor = colors.black 1765 1766 # Render cell content 1767 result = select_tag(detail, title=isinstance(component, TH)) 1768 if result is None: 1769 continue 1770 rappend(result) 1771 1772 # Add cell styles 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 # Column span 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 # -------------------------------------------------------------------------
1797 - def _styles(self, element):
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
1827 - def _color(val):
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 # END ========================================================================= 1850