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

Source Code for Module s3.codecs.card

  1  # -*- coding: utf-8 -*- 
  2   
  3  """ 
  4      S3Codec to produce printable data cards (e.g. ID cards) 
  5   
  6      @copyright: 2018-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__ = ("S3PDFCard", 
 32             ) 
 33   
 34  try: 
 35      from cStringIO import StringIO    # Faster, where available 
 36  except ImportError: 
 37      from StringIO import StringIO 
 38   
 39  try: 
 40      from reportlab.lib.pagesizes import A4, LETTER, landscape, portrait 
 41      from reportlab.platypus import BaseDocTemplate, PageTemplate, Flowable, \ 
 42                                     Frame, NextPageTemplate, PageBreak 
 43      from reportlab.lib.utils import ImageReader 
 44      from reportlab.graphics import renderPDF 
 45      from reportlab.graphics.barcode import code39, code128, qr 
 46      from reportlab.graphics.shapes import Drawing 
 47      REPORTLAB = True 
 48  except ImportError: 
 49      BaseDocTemplate = object 
 50      Flowable = object 
 51      REPORTLAB = False 
 52   
 53  from gluon import current, HTTP 
 54   
 55  from ..s3codec import S3Codec 
 56  from ..s3resource import S3Resource 
 57  from ..s3utils import s3_str 
 58   
 59  CREDITCARD = (153, 243) # Default format for cards (in points) 
60 # ============================================================================= 61 -class S3PDFCard(S3Codec):
62 """ 63 Codec to produce printable data cards (e.g. ID cards) 64 """ 65
66 - def encode(self, resource, **attr):
67 """ 68 API Method to encode a resource as cards 69 70 @param resource: the S3Resource, or 71 - the data items as list [{fieldname: representation, ...}, ...], or 72 - a callable that produces such a list of items 73 @param attr: additional encoding parameters (see below) 74 75 @keyword layout: the layout (a S3PDFCardLayout subclass, overrides 76 the resource's pdf_card_layout setting 77 @keyword orderby: orderby-expression for data extraction, overrides 78 the resource's orderby setting 79 @keyword labels: the labels for the fields, 80 - a dict {colname: label}, or 81 - a callable that produces it, 82 - defaults to the labels of the extracted fields 83 @keyword pagesize: the PDF page size, 84 - a string "A4" or "Letter", or 85 - a tuple (width, height), in points 86 - defaults to the layout's card size 87 @keyword margins: the page margins, 88 - a tuple (N, E, S, W), in points, or 89 - a single number, in points 90 - will be computed if omitted 91 @keyword spacing: the spacing between cards, 92 - a tuple (H, V), in points, or 93 - a single number, in points 94 - defaults to 18 points in both directions 95 @keyword title: the document title, 96 - defaults to title_list crud string of the resource 97 98 @return: a handle to the output 99 """ 100 101 if not REPORTLAB: 102 # FIXME is this the correct handling of a dependency failure? 103 raise HTTP(503, "Python ReportLab library not installed") 104 105 # Do we operate on a S3Resource? 106 is_resource = isinstance(resource, S3Resource) 107 108 # The card layout 109 layout = attr.get("layout") 110 if layout is None and is_resource: 111 layout = resource.get_config("pdf_card_layout") 112 if layout is None: 113 layout = S3PDFCardLayout 114 115 # Card (and hence page) orientation 116 orientation = layout.orientation 117 if orientation == "Landscape": 118 orientation = landscape 119 else: 120 orientation = portrait 121 122 # Card and page size 123 cardsize = orientation(layout.cardsize) 124 pagesize = attr.get("pagesize") 125 if pagesize == "A4": 126 pagesize = A4 127 elif pagesize == "Letter": 128 pagesize = LETTER 129 elif not isinstance(pagesize, (tuple, list)): 130 pagesize = cardsize 131 pagesize = orientation(pagesize) 132 133 # Extract the data 134 if is_resource: 135 # Extract the data items from the resource 136 fields = layout.fields(resource) 137 data = self.extract(resource, fields, orderby=attr.get("orderby")) 138 items = data.rows 139 elif callable(resource): 140 # External getter => call with resource, returns the data items 141 data = None 142 items = resource() 143 else: 144 # The data items have been passed-in in place of the resource 145 data = None 146 items = resource 147 148 # Get the labels 149 labels = attr.get("labels") 150 if callable(labels): 151 labels = labels(resource) 152 elif not isinstance(labels, dict): 153 if data and hasattr(data, "rfields"): 154 # Collect the labels from rfields 155 rfields = data.rfields 156 labels = {rfield.colname: rfield.label for rfield in rfields} 157 else: 158 labels = {} 159 160 161 # Document title 162 title = attr.get("title") 163 if not title and is_resource: 164 crud_strings = current.response.s3.crud_strings[resource.tablename] 165 if crud_strings: 166 title = crud_strings["title_list"] 167 168 # Instantiate the doc template 169 doc = S3PDFCardTemplate(pagesize, 170 cardsize, 171 margins = attr.get("margins"), 172 spacing = attr.get("spacing"), 173 title = title, 174 ) 175 176 # Produce the flowables 177 flowables = self.get_flowables(layout, 178 resource, 179 items, 180 labels = labels, 181 cards_per_page = doc.cards_per_page, 182 ) 183 184 # Build the doc 185 output_stream = StringIO() 186 doc.build(flowables, 187 output_stream, 188 #canvasmaker=canvas.Canvas, # is default 189 ) 190 191 output_stream.seek(0) 192 return output_stream
193 194 # -------------------------------------------------------------------------
195 - def extract(self, resource, fields, orderby=None):
196 """ 197 Extract the data items from the given resource 198 199 @param resource: the resource (a filtered S3Resource) 200 @param fields: the fields to extract (array of field selectors) 201 @param orderby: the orderby-expression 202 203 @returns: an S3ResourceData instance 204 """ 205 206 if orderby is None: 207 orderby = resource.get_config("orderby") 208 if orderby is None: 209 orderby = resource._id 210 211 return resource.select(fields, 212 represent = True, 213 show_links = False, 214 raw_data = True, 215 orderby = orderby, 216 )
217 218 # -------------------------------------------------------------------------
219 - def get_flowables(self, layout, resource, items, labels=None, cards_per_page=1):
220 """ 221 Get the Flowable-instances for the data items 222 223 @param layout: the S3PDFCardLayout subclass implementing the 224 card layout 225 @param resource: the resource 226 @param items: the data items 227 @param labels: the field labels 228 @param cards_per_page: the number of cards per page 229 """ 230 231 if not len(items): 232 # Need at least one flowable even to produce an empty doc 233 return [PageBreak()] 234 235 # Determine the number of pages 236 number_of_pages = int(len(items) / cards_per_page) + 1 237 multiple = cards_per_page > 1 238 239 # Look up common data 240 common = layout.lookup(resource, items) 241 242 # Generate the pages 243 flowables = [] 244 append = flowables.append 245 for i in range(number_of_pages): 246 247 # Get the items for the current page 248 batch = items[i * cards_per_page:(i+1) * cards_per_page] 249 if not batch: 250 continue 251 252 # Add the flowables for the card fronts to the page 253 append(NextPageTemplate("Front")) 254 if i > 0: 255 append(PageBreak()) 256 for item in batch: 257 append(layout(resource, 258 item, 259 labels = labels, 260 common = common, 261 multiple = multiple, 262 )) 263 264 if layout.doublesided: 265 # Add the flowables for the backsides on a new page 266 append(NextPageTemplate("Back")) 267 append(PageBreak()) 268 for item in batch: 269 append(layout(resource, 270 item, 271 labels = labels, 272 common = common, 273 multiple = multiple, 274 backside = True, 275 )) 276 277 return flowables
278
279 # ============================================================================= 280 -class S3PDFCardTemplate(BaseDocTemplate):
281 """ 282 Document Template for data cards 283 """ 284
285 - def __init__(self, 286 pagesize, 287 cardsize, 288 margins = None, 289 spacing = None, 290 title = None, 291 ):
292 """ 293 Constructor 294 295 @param pagesize: the page size, tuple (w, h) 296 @param cardsize: the card size, tuple (w, h) 297 @param margins: the page margins, tuple (N, E, S, W) 298 @param spacing: the spacing between cards, tuple (H, V) 299 @param title: the document title 300 301 - all sizes in points (72 points per inch) 302 """ 303 304 # Spacing between cards 305 if spacing is None: 306 spacing = (18, 18) 307 elif not isinstance(spacing, (tuple, list)): 308 spacing = (spacing, spacing) 309 310 # Page margins 311 if margins is None: 312 margins = self.compute_margins(pagesize, cardsize, spacing) 313 elif not isinstance(margins, (tuple, list)): 314 margins = (margins, margins, margins, margins) 315 316 # Cards per row, rows per page and cards per page 317 pagewidth, pageheight = pagesize 318 cardwidth, cardheight = cardsize 319 320 number_of_cards = self.number_of_cards 321 322 cards_per_row = number_of_cards(pagewidth, 323 cardwidth, 324 (margins[1], margins[3]), 325 spacing[0], 326 ) 327 328 rows_per_page = number_of_cards(pageheight, 329 cardheight, 330 (margins[0], margins[2]), 331 spacing[1], 332 ) 333 334 self.cards_per_row = cards_per_row 335 self.rows_per_page = rows_per_page 336 self.cards_per_page = rows_per_page * cards_per_row 337 338 # Generate page templates 339 pages = self.page_layouts(pagesize, cardsize, margins, spacing) 340 341 if title is None: 342 title = current.T("Items") 343 344 # Call super-constructor 345 BaseDocTemplate.__init__(self, 346 None, 347 pagesize = pagesize, 348 pageTemplates = pages, 349 topMargin = margins[0], 350 rightMargin = margins[1], 351 bottomMargin = margins[2], 352 leftMargin = margins[3], 353 title = s3_str(title), 354 )
355 356 # -------------------------------------------------------------------------
357 - def number_of_cards(self, pagesize, cardsize, margins, spacing):
358 """ 359 Compute the number of cards for one page dimension 360 361 @param pagesize: the page size 362 @param cardsize: the card size 363 @param margins: tuple of margins 364 @param spacing: the spacing between cards 365 """ 366 367 available = pagesize - sum(margins) 368 if available < cardsize: 369 available = pagesize 370 if available < cardsize: 371 return 0 372 return int((available + spacing) / (cardsize + spacing))
373 374 # -------------------------------------------------------------------------
375 - def compute_margins(self, pagesize, cardsize, spacing):
376 """ 377 Calculate default margins 378 379 @param pagesize: the page size, tuple (w, h) 380 @param cardsize: the card size, tuple (w, h) 381 @param spacing: spacing between cards, tuple (h, v) 382 """ 383 384 cardwidth, cardheight = cardsize 385 pagewidth, pageheight = pagesize 386 spacing_h, spacing_v = spacing 387 388 # Calculate number of cards with minimal margins 389 number_of_cards = self.number_of_cards 390 numh = number_of_cards(pagewidth, cardwidth, (18, 18), spacing_h) 391 numv = number_of_cards(pageheight, cardheight, (12, 12), spacing_v) 392 393 # Compute total width/height of as many cards 394 width = (numh - 1) * (cardwidth + spacing_h) + cardwidth 395 height = (numv - 1) * (cardheight + spacing_v) + cardheight 396 397 # Compute the actual margins, centering the cards on the page 398 margin_h = (pagewidth - width) / 2 399 margin_v = (pageheight - height) / 2 400 401 return (margin_v, margin_h, margin_v, margin_h)
402 403 # -------------------------------------------------------------------------
404 - def page_layouts(self, pagesize, cardsize, margins, spacing):
405 """ 406 Generate page templates for front/back sides of cards 407 408 @param pagesize: the page size, tuple (w, h) 409 @param cardsize: the card size, tuple (w, h) 410 @param margins: the page margins, tuple (N, E, S, W) 411 @param spacing: the spacing between cards, tuple (H, V) 412 413 - all sizes in points (72 points per inch) 414 """ 415 416 pagewidth, pageheight = pagesize 417 cardwidth, cardheight = cardsize 418 419 topmargin, leftmargin = margins[0], margins[3] 420 421 hspacing, vspacing = spacing 422 423 # Y-position (from page bottom) of first card row 424 y0 = pageheight - topmargin - cardheight 425 426 # X-position of first card in a row 427 # - front from the left, back from the right 428 # - actual page width differs between printers, so we may need 429 # to add a parameter to account for this horizontal shift (TODO) 430 x0_front = leftmargin 431 x0_back = pagewidth - leftmargin - cardwidth # + hshift 432 433 fframes, bframes = [], [] 434 for i in range(self.rows_per_page): 435 436 # Y-position of current row 437 y = y0 - i * (vspacing + cardheight) 438 439 # Add frames for cards in this row 440 for j in range(self.cards_per_row): 441 442 # Front 443 fframes.append(Frame(x0_front + j * (cardwidth + hspacing), 444 y, 445 cardwidth, 446 cardheight, 447 topPadding = 0, 448 rightPadding = 0, 449 bottomPadding = 0, 450 leftPadding = 0, 451 )) 452 453 # Back 454 bframes.append(Frame(x0_back - j * (cardwidth + hspacing), 455 y, 456 cardwidth, 457 cardheight, 458 topPadding = 0, 459 rightPadding = 0, 460 bottomPadding = 0, 461 leftPadding = 0, 462 )) 463 464 # Generate and return the page templates 465 return [PageTemplate(id="Front", frames = fframes), 466 PageTemplate(id="Back", frames = bframes), 467 ]
468
469 # ============================================================================= 470 -class S3PDFCardLayout(Flowable):
471 """ 472 Flowable base class for data cards, to be subclassed per use-case; 473 subclasses should implement the draw()-method to render a data item. 474 """ 475 476 # Card layout parameters (subclasses can override this) 477 cardsize = CREDITCARD 478 orientation = "Portrait" 479 doublesided = True 480
481 - def __init__(self, 482 resource, 483 item, 484 labels=None, 485 common=None, 486 backside=False, 487 multiple=False, 488 ):
489 """ 490 Constructor 491 492 @param resource: the resource 493 @param item: the data item 494 @param labels: the field labels 495 @param common: common data for all cards 496 @param backside: this instance should render a card backside 497 @param multiple: there are multiple cards per page 498 """ 499 500 Flowable.__init__(self) 501 502 self.width, self.height = self.cardsize 503 504 self.resource = resource 505 self.item = item 506 507 self.labels = labels if labels is not None else {} 508 self.common = common if common is not None else {} 509 510 self.backside = backside 511 self.multiple = multiple
512 513 # -------------------------------------------------------------------------
514 - def draw(self):
515 """ 516 Draw the card (one side), to be implemented by subclass 517 518 Instance attributes (NB draw-function should not modify them): 519 - self.canv...............the canvas (provides the drawing methods) 520 - self.resource...........the resource 521 - self.item...............the data item (dict) 522 - self.labels.............the field labels (dict) 523 - self.backside...........this instance should render the backside 524 of a card 525 - self.multiple...........there are multiple cards per page 526 - self.width..............the width of the card (in points) 527 - self.height.............the height of the card (in points) 528 529 NB Canvas coordinates are relative to the lower left corner of the 530 card's frame, drawing must not overshoot self.width/self.height 531 """ 532 533 c = self.canv 534 535 w = self.width 536 h = self.height 537 538 c.setDash(1, 2) 539 self.draw_outline() 540 541 x = w / 2 542 y = h / 2 543 544 c.setFont("Helvetica", 12) 545 c.drawCentredString(x, y, "Back" if self.backside else "Front") 546 547 resource = self.resource 548 if isinstance(resource, S3Resource): 549 # Render the record ID if available 550 item = self.item 551 pid = str(resource._id) 552 if pid in item: 553 c.setFont("Helvetica", 8) 554 c.drawCentredString(x, y - 10, "Record #%s" % item[pid])
555 556 # ------------------------------------------------------------------------- 557 @classmethod
558 - def fields(cls, resource):
559 """ 560 Get the fields to look up from the resource, can be overridden 561 in subclasses (as the field list is usually layout-specific) 562 563 @param resource: the resource 564 565 @returns: list of field selectors 566 """ 567 568 return resource.list_fields()
569 570 # ------------------------------------------------------------------------- 571 @classmethod
572 - def lookup(cls, resource, items):
573 """ 574 Look up common data for all cards 575 576 @param resource: the resource 577 @param items: the items 578 579 @returns: a dict with common data for all cards, will be 580 passed to the individual flowables 581 """ 582 583 return {}
584 585 # -------------------------------------------------------------------------
586 - def draw_barcode(self, 587 value, 588 x, 589 y, 590 bctype="code128", 591 height=12, 592 barwidth=0.85, 593 halign=None, 594 valign=None, 595 maxwidth=None, 596 ):
597 """ 598 Helper function to render a barcode 599 600 @param value: the string to encode 601 @param x: drawing position 602 @param y: drawing position 603 @param bctype: the barcode type 604 @param height: the height of the barcode (in points) 605 @param barwidth: the default width of the smallest bar 606 @param halign: horizontal alignment ("left"|"center"|"right"), default left 607 @param valign: vertical alignment ("top"|"middle"|"bottom"), default bottom 608 @param maxwidth: the maximum total width, if specified, the barcode will 609 not be rendered if it would exceed this width even with 610 the minimum possible bar width 611 612 @return: True if successful, otherwise False 613 """ 614 615 # For arbitrary alphanumeric values, these would be the most 616 # commonly used symbologies: 617 types = {"code39": code39.Standard39, 618 "code128": code128.Code128, 619 } 620 621 encode = types.get(bctype) 622 if not encode: 623 raise RuntimeError("Barcode type %s not supported" % bctype) 624 else: 625 qz = 12 * barwidth 626 barcode = encode(value, 627 barHeight = height, 628 barWidth = barwidth, 629 lquiet = qz, 630 rquiet = qz, 631 ) 632 633 width, height = barcode.width, barcode.height 634 if maxwidth and width > maxwidth: 635 # Try to adjust the bar width 636 bw = max(float(maxwidth) / width * barwidth, encode.barWidth) 637 qz = 12 * bw 638 barcode = encode(value, 639 barHeight = height, 640 barWidth = bw, 641 lquiet = qz, 642 rquiet = qz, 643 ) 644 width = barcode.width 645 if width > maxwidth: 646 return False 647 648 hshift = vshift = 0 649 if halign == "right": 650 hshift = width 651 elif halign == "center": 652 hshift = width / 2.0 653 654 if valign == "top": 655 vshift = height 656 elif valign == "middle": 657 vshift = height / 2.0 658 659 barcode.drawOn(self.canv, x - hshift, y - vshift) 660 return True
661 662 # -------------------------------------------------------------------------
663 - def draw_qrcode(self, value, x, y, size=40, halign=None, valign=None):
664 """ 665 Helper function to draw a QR code 666 667 @param value: the string to encode 668 @param x: drawing position 669 @param y: drawing position 670 @param size: the size (edge length) of the QR code 671 @param halign: horizontal alignment ("left"|"center"|"right"), default left 672 @param valign: vertical alignment ("top"|"middle"|"bottom"), default bottom 673 """ 674 675 qr_code = qr.QrCodeWidget(value) 676 677 bounds = qr_code.getBounds() 678 w = bounds[2] - bounds[0] 679 h = bounds[3] - bounds[1] 680 681 transform = [float(size) / w, 0, 0, float(size) / h, 0, 0] 682 d = Drawing(size, size, transform=transform) 683 d.add(qr_code) 684 685 hshift = vshift = 0 686 if halign == "right": 687 hshift = size 688 elif halign == "center": 689 hshift = float(size) / 2.0 690 691 if valign == "top": 692 vshift = size 693 elif valign == "middle": 694 vshift = float(size) / 2.0 695 696 renderPDF.draw(d, self.canv, x - hshift, y - vshift)
697 698 # -------------------------------------------------------------------------
699 - def draw_image(self, 700 img, 701 x, 702 y, 703 width=None, 704 height=None, 705 proportional=True, 706 scale=None, 707 halign=None, 708 valign=None, 709 ):
710 """ 711 Helper function to draw an image 712 - requires PIL (required for ReportLab image handling anyway) 713 714 @param img: the image (filename or StringIO buffer) 715 @param x: drawing position 716 @param y: drawing position 717 @param width: the target width of the image (in points) 718 @param height: the target height of the image (in points) 719 @param proportional: keep image proportions when scaling to width/height 720 @param scale: scale the image by this factor (overrides width/height) 721 @param halign: horizontal alignment ("left"|"center"|"right"), default left 722 @param valign: vertical alignment ("top"|"middle"|"bottom"), default bottom 723 """ 724 725 if hasattr(img, "seek"): 726 is_buffer = True 727 img.seek(0) 728 else: 729 is_buffer = False 730 731 try: 732 from PIL import Image as pImage 733 except ImportError: 734 current.log.error("Image rendering failed: PIL not installed") 735 return 736 737 pimg = pImage.open(img) 738 img_size = pimg.size 739 740 if not img_size[0] or not img_size[1]: 741 # This image has at least one dimension of zero 742 return 743 744 # Compute drawing width/height 745 if scale: 746 width = img_size[0] * scale 747 height = img_size[1] * scale 748 elif width and height: 749 if proportional: 750 scale = min(float(width) / img_size[0], float(height) / img_size[1]) 751 width = img_size[0] * scale 752 height = img_size[1] * scale 753 elif width: 754 height = img_size[1] * (float(width) / img_size[0]) 755 elif height: 756 width = img_size[0] * (float(height) / img_size[1]) 757 else: 758 width = img_size[0] 759 height = img_size[1] 760 761 # Compute drawing position from alignment options 762 hshift = vshift = 0 763 if halign == "right": 764 hshift = width 765 elif halign == "center": 766 hshift = width / 2.0 767 768 if valign == "top": 769 vshift = height 770 elif valign == "middle": 771 vshift = height / 2.0 772 773 # Draw the image 774 if is_buffer: 775 img.seek(0) 776 ir = ImageReader(img) 777 778 c = self.canv 779 c.drawImage(ir, 780 x - hshift, 781 y - vshift, 782 width = width, 783 height = height, 784 preserveAspectRatio = proportional, 785 mask = "auto", 786 )
787 788 # -------------------------------------------------------------------------
789 - def draw_outline(self):
790 """ 791 Helper function to draw the outline of the card, useful as cutting 792 line when there are multiple cards per page 793 """ 794 795 c = self.canv 796 797 c.setLineWidth(1) 798 c.rect(-1, -1, self.width + 2, self.height + 2)
799 800 # END ========================================================================= 801