1#Copyright ReportLab Europe Ltd. 2000-2017 2#see license.txt for license details 3#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/platypus/flowables.py 4__version__='3.3.0' 5__doc__=""" 6A flowable is a "floating element" in a document whose exact position is determined by the 7other elements that precede it, such as a paragraph, a diagram interspersed between paragraphs, 8a section header, etcetera. Examples of non-flowables include page numbering annotations, 9headers, footers, fixed diagrams or logos, among others. 10 11Flowables are defined here as objects which know how to determine their size and which 12can draw themselves onto a page with respect to a relative "origin" position determined 13at a higher level. The object's draw() method should assume that (0,0) corresponds to the 14bottom left corner of the enclosing rectangle that will contain the object. The attributes 15vAlign and hAlign may be used by 'packers' as hints as to how the object should be placed. 16 17Some Flowables also know how to "split themselves". For example a 18long paragraph might split itself between one page and the next. 19 20Packers should set the canv attribute during wrap, split & draw operations to allow 21the flowable to work out sizes etc in the proper context. 22 23The "text" of a document usually consists mainly of a sequence of flowables which 24flow into a document from top to bottom (with column and page breaks controlled by 25higher level components). 26""" 27import os 28from copy import deepcopy, copy 29from reportlab.lib.colors import red, gray, lightgrey 30from reportlab.lib.rl_accel import fp_str 31from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY 32from reportlab.lib.styles import _baseFontName 33from reportlab.lib.utils import strTypes, rl_safe_exec 34from reportlab.lib.abag import ABag 35from reportlab.pdfbase import pdfutils 36from reportlab.pdfbase.pdfmetrics import stringWidth 37from reportlab.rl_config import _FUZZ, overlapAttachedSpace, ignoreContainerActions, listWrapOnFakeWidth 38from reportlab import xrange 39import collections 40 41__all__ = '''AnchorFlowable BalancedColumns BulletDrawer CallerMacro CondPageBreak DDIndenter DocAssert 42 DocAssign DocExec DocIf DocPara DocWhile FailOnDraw FailOnWrap Flowable FrameBG FrameSplitter 43 HRFlowable Image ImageAndFlowables KeepInFrame KeepTogether LIIndenter ListFlowable ListItem 44 Macro NullDraw PTOContainer PageBreak PageBreakIfNotEmpty ParagraphAndImage Preformatted 45 SetPageTopFlowables SetTopFlowables SlowPageBreak Spacer TopPadder TraceInfo UseUpSpace XBox 46 splitLine splitLines'''.split() 47 48class TraceInfo: 49 "Holder for info about where an object originated" 50 def __init__(self): 51 self.srcFile = '(unknown)' 52 self.startLineNo = -1 53 self.startLinePos = -1 54 self.endLineNo = -1 55 self.endLinePos = -1 56 57############################################################# 58# Flowable Objects - a base class and a few examples. 59# One is just a box to get some metrics. We also have 60# a paragraph, an image and a special 'page break' 61# object which fills the space. 62############################################################# 63class Flowable: 64 """Abstract base class for things to be drawn. Key concepts: 65 66 1. It knows its size 67 2. It draws in its own coordinate system (this requires the 68 base API to provide a translate() function. 69 70 """ 71 _fixedWidth = 0 #assume wrap results depend on arguments? 72 _fixedHeight = 0 73 74 def __init__(self): 75 self.width = 0 76 self.height = 0 77 self.wrapped = 0 78 79 #these are hints to packers/frames as to how the floable should be positioned 80 self.hAlign = 'LEFT' #CENTER/CENTRE or RIGHT 81 self.vAlign = 'BOTTOM' #MIDDLE or TOP 82 83 #optional holder for trace info 84 self._traceInfo = None 85 self._showBoundary = None 86 87 #many flowables handle text and must be processed in the 88 #absence of a canvas. tagging them with their encoding 89 #helps us to get conversions right. Use Python codec names. 90 self.encoding = None 91 92 def _drawOn(self,canv): 93 '''ensure canv is set on and then draw''' 94 self.canv = canv 95 self.draw()#this is the bit you overload 96 del self.canv 97 98 def _hAlignAdjust(self,x,sW=0): 99 if sW and hasattr(self,'hAlign'): 100 a = self.hAlign 101 if a in ('CENTER','CENTRE', TA_CENTER): 102 x += 0.5*sW 103 elif a in ('RIGHT',TA_RIGHT): 104 x += sW 105 elif a not in ('LEFT',TA_LEFT): 106 raise ValueError("Bad hAlign value "+str(a)) 107 return x 108 109 def drawOn(self, canvas, x, y, _sW=0): 110 "Tell it to draw itself on the canvas. Do not override" 111 x = self._hAlignAdjust(x,_sW) 112 canvas.saveState() 113 canvas.translate(x, y) 114 self._drawOn(canvas) 115 if hasattr(self, '_showBoundary') and self._showBoundary: 116 #diagnostic tool support 117 canvas.setStrokeColor(gray) 118 canvas.rect(0,0,self.width, self.height) 119 canvas.restoreState() 120 121 def wrapOn(self, canv, aW, aH): 122 '''intended for use by packers allows setting the canvas on 123 during the actual wrap''' 124 self.canv = canv 125 w, h = self.wrap(aW,aH) 126 del self.canv 127 return w, h 128 129 def wrap(self, availWidth, availHeight): 130 """This will be called by the enclosing frame before objects 131 are asked their size, drawn or whatever. It returns the 132 size actually used.""" 133 return (self.width, self.height) 134 135 def minWidth(self): 136 """This should return the minimum required width""" 137 return getattr(self,'_minWidth',self.width) 138 139 def splitOn(self, canv, aW, aH): 140 '''intended for use by packers allows setting the canvas on 141 during the actual split''' 142 self.canv = canv 143 S = self.split(aW,aH) 144 del self.canv 145 return S 146 147 def split(self, availWidth, availheight): 148 """This will be called by more sophisticated frames when 149 wrap fails. Stupid flowables should return []. Clever flowables 150 should split themselves and return a list of flowables. 151 If they decide that nothing useful can be fitted in the 152 available space (e.g. if you have a table and not enough 153 space for the first row), also return []""" 154 return [] 155 156 def getKeepWithNext(self): 157 """returns boolean determining whether the next flowable should stay with this one""" 158 if hasattr(self,'keepWithNext'): return self.keepWithNext 159 elif hasattr(self,'style') and hasattr(self.style,'keepWithNext'): return self.style.keepWithNext 160 else: return 0 161 162 def getSpaceAfter(self): 163 """returns how much space should follow this item if another item follows on the same page.""" 164 if hasattr(self,'spaceAfter'): return self.spaceAfter 165 elif hasattr(self,'style') and hasattr(self.style,'spaceAfter'): return self.style.spaceAfter 166 else: return 0 167 168 def getSpaceBefore(self): 169 """returns how much space should precede this item if another item precedess on the same page.""" 170 if hasattr(self,'spaceBefore'): return self.spaceBefore 171 elif hasattr(self,'style') and hasattr(self.style,'spaceBefore'): return self.style.spaceBefore 172 else: return 0 173 174 def isIndexing(self): 175 """Hook for IndexingFlowables - things which have cross references""" 176 return 0 177 178 def identity(self, maxLen=None): 179 ''' 180 This method should attempt to return a string that can be used to identify 181 a particular flowable uniquely. The result can then be used for debugging 182 and or error printouts 183 ''' 184 if hasattr(self, 'getPlainText'): 185 r = self.getPlainText(identify=1) 186 elif hasattr(self, 'text'): 187 r = str(self.text) 188 else: 189 r = '...' 190 if r and maxLen: 191 r = r[:maxLen] 192 return "<%s at %s%s>%s" % (self.__class__.__name__, hex(id(self)), self._frameName(), r) 193 194 @property 195 def _doctemplate(self): 196 return getattr(getattr(self,'canv',None),'_doctemplate',None) 197 198 def _doctemplateAttr(self,a): 199 return getattr(self._doctemplate,a,None) 200 201 def _frameName(self): 202 f = getattr(self,'_frame',None) 203 if not f: f = self._doctemplateAttr('frame') 204 if f and f.id: return ' frame=%s' % f.id 205 return '' 206 207class XBox(Flowable): 208 """Example flowable - a box with an x through it and a caption. 209 This has a known size, so does not need to respond to wrap().""" 210 def __init__(self, width, height, text = 'A Box'): 211 Flowable.__init__(self) 212 self.width = width 213 self.height = height 214 self.text = text 215 216 def __repr__(self): 217 return "XBox(w=%s, h=%s, t=%s)" % (self.width, self.height, self.text) 218 219 def draw(self): 220 self.canv.rect(0, 0, self.width, self.height) 221 self.canv.line(0, 0, self.width, self.height) 222 self.canv.line(0, self.height, self.width, 0) 223 224 #centre the text 225 self.canv.setFont(_baseFontName,12) 226 self.canv.drawCentredString(0.5*self.width, 0.5*self.height, self.text) 227 228def _trimEmptyLines(lines): 229 #don't want the first or last to be empty 230 while len(lines) and lines[0].strip() == '': 231 lines = lines[1:] 232 while len(lines) and lines[-1].strip() == '': 233 lines = lines[:-1] 234 return lines 235 236def _dedenter(text,dedent=0): 237 ''' 238 tidy up text - carefully, it is probably code. If people want to 239 indent code within a source script, you can supply an arg to dedent 240 and it will chop off that many character, otherwise it leaves 241 left edge intact. 242 ''' 243 lines = text.split('\n') 244 if dedent>0: 245 templines = _trimEmptyLines(lines) 246 lines = [] 247 for line in templines: 248 line = line[dedent:].rstrip() 249 lines.append(line) 250 else: 251 lines = _trimEmptyLines(lines) 252 253 return lines 254 255 256SPLIT_CHARS = "[{( ,.;:/\\-" 257 258def splitLines(lines, maximum_length, split_characters, new_line_characters): 259 if split_characters is None: 260 split_characters = SPLIT_CHARS 261 if new_line_characters is None: 262 new_line_characters = "" 263 # Return a table of lines 264 lines_splitted = [] 265 for line in lines: 266 if len(line) > maximum_length: 267 splitLine(line, lines_splitted, maximum_length, \ 268 split_characters, new_line_characters) 269 else: 270 lines_splitted.append(line) 271 return lines_splitted 272 273def splitLine(line_to_split, lines_splitted, maximum_length, \ 274split_characters, new_line_characters): 275 # Used to implement the characters added 276 #at the beginning of each new line created 277 first_line = True 278 279 # Check if the text can be splitted 280 while line_to_split and len(line_to_split)>0: 281 282 # Index of the character where we can split 283 split_index = 0 284 285 # Check if the line length still exceeds the maximum length 286 if len(line_to_split) <= maximum_length: 287 # Return the remaining of the line 288 split_index = len(line_to_split) 289 else: 290 # Iterate for each character of the line 291 for line_index in range(maximum_length): 292 # Check if the character is in the list 293 # of allowed characters to split on 294 if line_to_split[line_index] in split_characters: 295 split_index = line_index + 1 296 297 # If the end of the line was reached 298 # with no character to split on 299 if split_index==0: 300 split_index = line_index + 1 301 302 if first_line: 303 lines_splitted.append(line_to_split[0:split_index]) 304 first_line = False 305 maximum_length -= len(new_line_characters) 306 else: 307 lines_splitted.append(new_line_characters + \ 308 line_to_split[0:split_index]) 309 310 # Remaining text to split 311 line_to_split = line_to_split[split_index:] 312 313class Preformatted(Flowable): 314 """This is like the HTML <PRE> tag. 315 It attempts to display text exactly as you typed it in a fixed width "typewriter" font. 316 By default the line breaks are exactly where you put them, and it will not be wrapped. 317 You can optionally define a maximum line length and the code will be wrapped; and 318 extra characters to be inserted at the beginning of each wrapped line (e.g. '> '). 319 """ 320 def __init__(self, text, style, bulletText = None, dedent=0, maxLineLength=None, splitChars=None, newLineChars=""): 321 """text is the text to display. If dedent is set then common leading space 322 will be chopped off the front (for example if the entire text is indented 323 6 spaces or more then each line will have 6 spaces removed from the front). 324 """ 325 self.style = style 326 self.bulletText = bulletText 327 self.lines = _dedenter(text,dedent) 328 if text and maxLineLength: 329 self.lines = splitLines( 330 self.lines, 331 maxLineLength, 332 splitChars, 333 newLineChars 334 ) 335 336 def __repr__(self): 337 bT = self.bulletText 338 H = "Preformatted(" 339 if bT is not None: 340 H = "Preformatted(bulletText=%s," % repr(bT) 341 return "%s'''\\ \n%s''')" % (H, '\n'.join(self.lines)) 342 343 def wrap(self, availWidth, availHeight): 344 self.width = availWidth 345 self.height = self.style.leading*len(self.lines) 346 return (self.width, self.height) 347 348 def minWidth(self): 349 style = self.style 350 fontSize = style.fontSize 351 fontName = style.fontName 352 return max([stringWidth(line,fontName,fontSize) for line in self.lines]) 353 354 def split(self, availWidth, availHeight): 355 #returns two Preformatted objects 356 357 #not sure why they can be called with a negative height 358 if availHeight < self.style.leading: 359 return [] 360 361 linesThatFit = int(availHeight * 1.0 / self.style.leading) 362 363 text1 = '\n'.join(self.lines[0:linesThatFit]) 364 text2 = '\n'.join(self.lines[linesThatFit:]) 365 style = self.style 366 if style.firstLineIndent != 0: 367 style = deepcopy(style) 368 style.firstLineIndent = 0 369 return [Preformatted(text1, self.style), Preformatted(text2, style)] 370 371 def draw(self): 372 #call another method for historical reasons. Besides, I 373 #suspect I will be playing with alternate drawing routines 374 #so not doing it here makes it easier to switch. 375 376 cur_x = self.style.leftIndent 377 cur_y = self.height - self.style.fontSize 378 self.canv.addLiteral('%PreformattedPara') 379 if self.style.textColor: 380 self.canv.setFillColor(self.style.textColor) 381 tx = self.canv.beginText(cur_x, cur_y) 382 #set up the font etc. 383 tx.setFont( self.style.fontName, 384 self.style.fontSize, 385 self.style.leading) 386 387 for text in self.lines: 388 tx.textLine(text) 389 self.canv.drawText(tx) 390 391class Image(Flowable): 392 """an image (digital picture). Formats supported by PIL/Java 1.4 (the Python/Java Imaging Library 393 are supported. Images as flowables may be aligned horizontally in the 394 frame with the hAlign parameter - accepted values are 'CENTER', 395 'LEFT' or 'RIGHT' with 'CENTER' being the default. 396 We allow for two kinds of lazyness to allow for many images in a document 397 which could lead to file handle starvation. 398 lazy=1 don't open image until required. 399 lazy=2 open image when required then shut it. 400 """ 401 _fixedWidth = 1 402 _fixedHeight = 1 403 def __init__(self, filename, width=None, height=None, kind='direct', 404 mask="auto", lazy=1, hAlign='CENTER', useDPI=False): 405 """If size to draw at not specified, get it from the image.""" 406 self.hAlign = hAlign 407 self._mask = mask 408 fp = hasattr(filename,'read') 409 self._drawing = None 410 if fp: 411 self._file = filename 412 self.filename = repr(filename) 413 elif hasattr(filename,'_renderPy'): 414 self._drawing = filename 415 self.filename=repr(filename) 416 self._file = None 417 self._img = None 418 fp = True 419 else: 420 self._file = self.filename = filename 421 self._dpi = useDPI 422 if not fp and os.path.splitext(filename)[1] in ['.jpg', '.JPG', '.jpeg', '.JPEG']: 423 # if it is a JPEG, will be inlined within the file - 424 # but we still need to know its size now 425 from reportlab.lib.utils import open_for_read 426 f = open_for_read(filename, 'b') 427 try: 428 try: 429 info = pdfutils.readJPEGInfo(f) 430 except: 431 #couldn't read as a JPEG, try like normal 432 self._setup(width,height,kind,lazy) 433 return 434 finally: 435 f.close() 436 self.imageWidth = info[0] 437 self.imageHeight = info[1] 438 if useDPI: 439 self._dpi = info[3] 440 self._img = None 441 self._setup(width,height,kind,0) 442 elif fp: 443 self._setup(width,height,kind,0) 444 else: 445 self._setup(width,height,kind,lazy) 446 447 def _dpiAdjust(self): 448 dpi = self._dpi 449 if dpi: 450 if dpi[0]!=72: self.imageWidth *= 72.0 / dpi[0] 451 if dpi[1]!=72: self.imageHeight *= 72.0 / dpi[1] 452 453 def _setup(self,width,height,kind,lazy): 454 self._lazy = lazy 455 self._width = width 456 self._height = height 457 self._kind = kind 458 if lazy<=0: self._setup_inner() 459 460 def _setup_inner(self): 461 width = self._width 462 height = self._height 463 kind = self._kind 464 img = self._img 465 if img: 466 self.imageWidth, self.imageHeight = img.getSize() 467 if self._dpi and hasattr(img,'_image'): 468 self._dpi = img._image.info.get('dpi',(72,72)) 469 elif self._drawing: 470 self.imageWidth, self.imageHeight = self._drawing.width,self._drawing.height 471 self._dpi = False 472 self._dpiAdjust() 473 if self._lazy>=2: del self._img 474 if kind in ['direct','absolute']: 475 self.drawWidth = width or self.imageWidth 476 self.drawHeight = height or self.imageHeight 477 elif kind in ['percentage','%']: 478 self.drawWidth = self.imageWidth*width*0.01 479 self.drawHeight = self.imageHeight*height*0.01 480 elif kind in ['bound','proportional']: 481 factor = min(float(width)/self.imageWidth,float(height)/self.imageHeight) 482 self.drawWidth = self.imageWidth*factor 483 self.drawHeight = self.imageHeight*factor 484 485 def _restrictSize(self,aW,aH): 486 if self.drawWidth>aW+_FUZZ or self.drawHeight>aH+_FUZZ: 487 self._oldDrawSize = self.drawWidth, self.drawHeight 488 factor = min(float(aW)/self.drawWidth,float(aH)/self.drawHeight) 489 self.drawWidth *= factor 490 self.drawHeight *= factor 491 return self.drawWidth, self.drawHeight 492 493 def _unRestrictSize(self): 494 dwh = getattr(self,'_oldDrawSize',None) 495 if dwh: 496 self.drawWidth, self.drawHeight = dwh 497 498 def __getattr__(self,a): 499 if a=='_img': 500 from reportlab.lib.utils import ImageReader #this may raise an error 501 self._img = ImageReader(self._file) 502 if not isinstance(self._file,strTypes): 503 self._file = None 504 if self._lazy>=2: self._lazy = 1 #here we're assuming we cannot read again 505 return self._img 506 elif a in ('drawWidth','drawHeight','imageWidth','imageHeight'): 507 self._setup_inner() 508 return self.__dict__[a] 509 raise AttributeError("<Image @ 0x%x>.%s" % (id(self),a)) 510 511 def wrap(self, availWidth, availHeight): 512 #the caller may decide it does not fit. 513 return self.drawWidth, self.drawHeight 514 515 def draw(self): 516 dx = getattr(self,'_offs_x',0) 517 dy = getattr(self,'_offs_y',0) 518 d = self._drawing 519 if d: 520 sx = self.drawWidth / float(self.imageWidth) 521 sy = self.drawHeight / float(self.imageHeight) 522 otrans = d.transform 523 try: 524 d.scale(sx,sy) 525 d.drawOn(self.canv,dx,dy) 526 finally: 527 d.transform = otrans 528 else: 529 lazy = self._lazy 530 if lazy>=2: self._lazy = 1 531 self.canv.drawImage( self._img or self.filename, 532 dx, 533 dy, 534 self.drawWidth, 535 self.drawHeight, 536 mask=self._mask, 537 ) 538 if lazy>=2: 539 self._img = self._file = None 540 self._lazy = lazy 541 542 def identity(self,maxLen=None): 543 r = Flowable.identity(self,maxLen) 544 if r[-4:]=='>...' and isinstance(self.filename,str): 545 r = "%s filename=%s>" % (r[:-4],self.filename) 546 return r 547 548class NullDraw(Flowable): 549 def draw(self): 550 pass 551 552class Spacer(NullDraw): 553 """A spacer just takes up space and doesn't draw anything - it guarantees 554 a gap between objects.""" 555 _fixedWidth = 1 556 _fixedHeight = 1 557 def __init__(self, width, height, isGlue=False): 558 self.width = width 559 if isGlue: 560 self.height = 1e-4 561 self.spacebefore = height 562 self.height = height 563 564 def __repr__(self): 565 return "%s(%s, %s)" % (self.__class__.__name__,self.width, self.height) 566 567class UseUpSpace(NullDraw): 568 def __init__(self): 569 pass 570 571 def __repr__(self): 572 return "%s()" % self.__class__.__name__ 573 574 def wrap(self, availWidth, availHeight): 575 self.width = availWidth 576 self.height = availHeight 577 return (availWidth,availHeight-1e-8) #step back a point 578 579class PageBreak(UseUpSpace): 580 locChanger=1 581 """Move on to the next page in the document. 582 This works by consuming all remaining space in the frame!""" 583 def __init__(self,nextTemplate=None): 584 self.nextTemplate = nextTemplate 585 586 def __repr__(self): 587 return "%s(%s)" % (self.__class__.__name__,repr(self.nextTemplate) if self.nextTemplate else '') 588 589class SlowPageBreak(PageBreak): 590 pass 591 592class PageBreakIfNotEmpty(PageBreak): 593 pass 594 595class CondPageBreak(Spacer): 596 locChanger=1 597 """use up a frame if not enough vertical space effectively CondFrameBreak""" 598 def __init__(self, height): 599 self.height = height 600 601 def __repr__(self): 602 return "CondPageBreak(%s)" %(self.height,) 603 604 def wrap(self, availWidth, availHeight): 605 if availHeight<self.height: 606 f = self._doctemplateAttr('frame') 607 if not f: return availWidth, availHeight 608 from reportlab.platypus.doctemplate import FrameBreak 609 f.add_generated_content(FrameBreak) 610 return 0, 0 611 612 def identity(self,maxLen=None): 613 return repr(self).replace(')',',frame=%s)'%self._frameName()) 614 615def _listWrapOn(F,availWidth,canv,mergeSpace=1,obj=None,dims=None,fakeWidth=None): 616 '''return max width, required height for a list of flowables F''' 617 doct = getattr(canv,'_doctemplate',None) 618 cframe = getattr(doct,'frame',None) 619 if fakeWidth is None: 620 fakeWidth = listWrapOnFakeWidth 621 if cframe: 622 from reportlab.platypus.doctemplate import _addGeneratedContent, Indenter 623 doct_frame = cframe 624 cframe = doct.frame = deepcopy(doct_frame) 625 cframe._generated_content = None 626 del cframe._generated_content 627 try: 628 W = 0 629 H = 0 630 pS = 0 631 atTop = 1 632 F = F[:] 633 while F: 634 f = F.pop(0) 635 if hasattr(f,'frameAction'): 636 from reportlab.platypus.doctemplate import Indenter 637 if isinstance(f,Indenter): 638 availWidth -= f.left+f.right 639 continue 640 w,h = f.wrapOn(canv,availWidth,0xfffffff) 641 if dims is not None: dims.append((w,h)) 642 if cframe: 643 _addGeneratedContent(F,cframe) 644 if w<=_FUZZ or h<=_FUZZ: continue 645 W = max(W,min(w,availWidth) if fakeWidth else w) 646 H += h 647 if not atTop: 648 h = f.getSpaceBefore() 649 if mergeSpace: 650 if getattr(f,'_SPACETRANSFER',False): 651 h = pS 652 h = max(h-pS,0) 653 H += h 654 else: 655 if obj is not None: obj._spaceBefore = f.getSpaceBefore() 656 atTop = 0 657 s = f.getSpaceAfter() 658 if getattr(f,'_SPACETRANSFER',False): 659 s = pS 660 pS = s 661 H += pS 662 if obj is not None: obj._spaceAfter = pS 663 return W, H-pS 664 finally: 665 if cframe: 666 doct.frame = doct_frame 667 668def _flowableSublist(V): 669 "if it isn't a list or tuple, wrap it in a list" 670 if not isinstance(V,(list,tuple)): V = V is not None and [V] or [] 671 from reportlab.platypus.doctemplate import LCActionFlowable 672 assert not [x for x in V if isinstance(x,LCActionFlowable)],'LCActionFlowables not allowed in sublists' 673 return V 674 675class _ContainerSpace: #Abstract some common container like behaviour 676 def getSpaceBefore(self,content=None): 677 for c in (self._content if content is None else content): 678 if not hasattr(c,'frameAction'): 679 return c.getSpaceBefore() 680 return 0 681 682 def getSpaceAfter(self,content=None): 683 for c in reversed(self._content if content is None else content): 684 if not hasattr(c,'frameAction'): 685 return c.getSpaceAfter() 686 return 0 687 688class KeepTogether(_ContainerSpace,Flowable): 689 splitAtTop = False 690 691 def __init__(self,flowables,maxHeight=None): 692 if not hasattr(KeepTogether,'NullActionFlowable'): 693 #cache these on the class 694 from reportlab.platypus.doctemplate import NullActionFlowable 695 from reportlab.platypus.doctemplate import FrameBreak 696 from reportlab.lib.utils import annotateException 697 KeepTogether.NullActionFlowable = NullActionFlowable 698 KeepTogether.FrameBreak = FrameBreak 699 KeepTogether.annotateException = annotateException 700 701 if not flowables: 702 flowables = [self.NullActionFlowable()] 703 self._content = _flowableSublist(flowables) 704 self._maxHeight = maxHeight 705 706 def __repr__(self): 707 f = self._content 708 L = list(map(repr,f)) 709 L = "\n"+"\n".join(L) 710 L = L.replace("\n", "\n ") 711 return "%s(%s,maxHeight=%s)" % (self.__class__.__name__,L,self._maxHeight) 712 713 def wrap(self, aW, aH): 714 dims = [] 715 try: 716 W,H = _listWrapOn(self._content,aW,self.canv,dims=dims) 717 except: 718 self.annotateException('\nraised by class %s(%s)@0x%8.8x wrap\n' % (self.__class__.__name__,self.__class__.__module__,id(self))) 719 self._H = H 720 self._H0 = dims and dims[0][1] or 0 721 self._wrapInfo = aW,aH 722 return W, 0xffffff # force a split 723 724 def split(self, aW, aH): 725 if getattr(self,'_wrapInfo',None)!=(aW,aH): self.wrap(aW,aH) 726 S = self._content[:] 727 cf = atTop = getattr(self,'_frame',None) 728 if cf: 729 atTop = getattr(cf,'_atTop',None) 730 cAW = cf._width 731 cAH = cf._height 732 C0 = self._H>aH and (not self._maxHeight or aH>self._maxHeight) 733 C1 = (self._H0>aH) or C0 and atTop 734 if C0 or C1: 735 fb = False 736 panf = self._doctemplateAttr('_peekNextFrame') 737 if cf and panf: 738 nf = panf() 739 nAW = nf._width 740 nAH = nf._height 741 if C0 and not (self.splitAtTop and atTop): 742 fb = not (atTop and cf and nf and cAW>=nAW and cAH>=nAH) 743 elif nf and nAW>=cf._width and nAH>=self._H: 744 fb = True 745 746 S.insert(0,(self.FrameBreak if fb else self.NullActionFlowable)()) 747 return S 748 749 def identity(self, maxLen=None): 750 msg = "<%s at %s%s> containing :%s" % (self.__class__.__name__,hex(id(self)),self._frameName(),"\n".join([f.identity() for f in self._content])) 751 if maxLen: 752 return msg[0:maxLen] 753 else: 754 return msg 755 756class KeepTogetherSplitAtTop(KeepTogether): 757 ''' 758 Same as KeepTogether, but it will split content immediately if it cannot 759 fit at the top of a frame. 760 ''' 761 splitAtTop = True 762 763class Macro(Flowable): 764 """This is not actually drawn (i.e. it has zero height) 765 but is executed when it would fit in the frame. Allows direct 766 access to the canvas through the object 'canvas'""" 767 def __init__(self, command): 768 self.command = command 769 def __repr__(self): 770 return "Macro(%s)" % repr(self.command) 771 def wrap(self, availWidth, availHeight): 772 return (0,0) 773 def draw(self): 774 rl_safe_exec(self.command, g=None, l={'canvas':self.canv}) 775 776def _nullCallable(*args,**kwds): 777 pass 778 779class CallerMacro(Flowable): 780 ''' 781 like Macro, but with callable command(s) 782 drawCallable(self) 783 wrapCallable(self,aW,aH) 784 ''' 785 def __init__(self, drawCallable=None, wrapCallable=None): 786 self._drawCallable = drawCallable or _nullCallable 787 self._wrapCallable = wrapCallable or _nullCallable 788 def __repr__(self): 789 return "CallerMacro(%r,%r)" % (self._drawCallable,self._wrapCallable) 790 def wrap(self, aW, aH): 791 self._wrapCallable(self,aW,aH) 792 return (0,0) 793 def draw(self): 794 self._drawCallable(self) 795 796class ParagraphAndImage(Flowable): 797 '''combine a Paragraph and an Image''' 798 def __init__(self,P,I,xpad=3,ypad=3,side='right'): 799 self.P = P 800 self.I = I 801 self.xpad = xpad 802 self.ypad = ypad 803 self._side = side 804 805 def getSpaceBefore(self): 806 return max(self.P.getSpaceBefore(),self.I.getSpaceBefore()) 807 808 def getSpaceAfter(self): 809 return max(self.P.getSpaceAfter(),self.I.getSpaceAfter()) 810 811 def wrap(self,availWidth,availHeight): 812 wI, hI = self.I.wrap(availWidth,availHeight) 813 self.wI = wI 814 self.hI = hI 815 # work out widths array for breaking 816 self.width = availWidth 817 P = self.P 818 style = P.style 819 xpad = self.xpad 820 ypad = self.ypad 821 leading = style.leading 822 leftIndent = style.leftIndent 823 later_widths = availWidth - leftIndent - style.rightIndent 824 intermediate_widths = later_widths - xpad - wI 825 first_line_width = intermediate_widths - style.firstLineIndent 826 P.width = 0 827 nIW = int((hI+ypad)/(leading*1.0)) 828 P.blPara = P.breakLines([first_line_width] + nIW*[intermediate_widths]+[later_widths]) 829 if self._side=='left': 830 self._offsets = [wI+xpad]*(1+nIW)+[0] 831 P.height = len(P.blPara.lines)*leading 832 self.height = max(hI,P.height) 833 return (self.width, self.height) 834 835 def split(self,availWidth, availHeight): 836 P, wI, hI, ypad = self.P, self.wI, self.hI, self.ypad 837 if hI+ypad>availHeight or len(P.frags)<=0: return [] 838 S = P.split(availWidth,availHeight) 839 if not S: return S 840 P = self.P = S[0] 841 del S[0] 842 style = P.style 843 P.height = len(self.P.blPara.lines)*style.leading 844 self.height = max(hI,P.height) 845 return [self]+S 846 847 def draw(self): 848 canv = self.canv 849 if self._side=='left': 850 self.I.drawOn(canv,0,self.height-self.hI-self.ypad) 851 self.P._offsets = self._offsets 852 try: 853 self.P.drawOn(canv,0,0) 854 finally: 855 del self.P._offsets 856 else: 857 self.I.drawOn(canv,self.width-self.wI-self.xpad,self.height-self.hI-self.ypad) 858 self.P.drawOn(canv,0,0) 859 860class FailOnWrap(NullDraw): 861 def wrap(self, availWidth, availHeight): 862 raise ValueError("FailOnWrap flowable wrapped and failing as ordered!") 863 864class FailOnDraw(Flowable): 865 def wrap(self, availWidth, availHeight): 866 return 0,0 867 868 def draw(self): 869 raise ValueError("FailOnDraw flowable drawn, and failing as ordered!") 870 871class HRFlowable(Flowable): 872 '''Like the hr tag''' 873 def __init__(self, 874 width="80%", 875 thickness=1, 876 lineCap='round', 877 color=lightgrey, 878 spaceBefore=1, spaceAfter=1, 879 hAlign='CENTER', vAlign='BOTTOM', 880 dash=None): 881 Flowable.__init__(self) 882 self.width = width 883 self.lineWidth = thickness 884 self.lineCap=lineCap 885 self.spaceBefore = spaceBefore 886 self.spaceAfter = spaceAfter 887 self.color = color 888 self.hAlign = hAlign 889 self.vAlign = vAlign 890 self.dash = dash 891 892 def __repr__(self): 893 return "HRFlowable(width=%s, height=%s)" % (self.width, self.height) 894 895 def wrap(self, availWidth, availHeight): 896 w = self.width 897 if isinstance(w,strTypes): 898 w = w.strip() 899 if w.endswith('%'): w = availWidth*float(w[:-1])*0.01 900 else: w = float(w) 901 w = min(w,availWidth) 902 self._width = w 903 return w, self.lineWidth 904 905 def draw(self): 906 canv = self.canv 907 canv.saveState() 908 canv.setLineWidth(self.lineWidth) 909 canv.setLineCap({'butt':0,'round':1, 'square': 2}[self.lineCap.lower()]) 910 canv.setStrokeColor(self.color) 911 if self.dash: canv.setDash(self.dash) 912 canv.line(0, 0, self._width, self.height) 913 canv.restoreState() 914 915class _PTOInfo: 916 def __init__(self,trailer,header): 917 self.trailer = _flowableSublist(trailer) 918 self.header = _flowableSublist(header) 919 920def cdeepcopy(obj): 921 if hasattr(obj,'deepcopy'): 922 return obj.deepcopy() 923 else: 924 return deepcopy(obj) 925 926class _Container(_ContainerSpace): #Abstract some common container like behaviour 927 def drawOn(self, canv, x, y, _sW=0, scale=1.0, content=None, aW=None): 928 '''we simulate being added to a frame''' 929 from reportlab.platypus.doctemplate import ActionFlowable, Indenter 930 x0 = x 931 y0 = y 932 pS = 0 933 if aW is None: aW = self.width 934 aW *= scale 935 if content is None: 936 content = self._content 937 x = self._hAlignAdjust(x,_sW*scale) 938 y += self.height*scale 939 yt = y 940 frame = getattr(self,'_frame',None) 941 for c in content: 942 if not ignoreContainerActions and isinstance(c,ActionFlowable): 943 c.apply(canv._doctemplate) 944 continue 945 if isinstance(c,Indenter): 946 x += c.left*scale 947 aW -= (c.left+c.right)*scale 948 continue 949 w, h = c.wrapOn(canv,aW,0xfffffff) 950 if (w<_FUZZ or h<_FUZZ) and not getattr(c,'_ZEROSIZE',None): continue 951 if yt!=y: 952 s = c.getSpaceBefore() 953 if not getattr(c,'_SPACETRANSFER',False): 954 h += max(s-pS,0) 955 y -= h 956 s = c.getSpaceAfter() 957 if getattr(c,'_SPACETRANSFER',False): 958 s = pS 959 pS = s 960 fbg = getattr(frame,'_frameBGs',None) 961 if fbg and fbg[-1].active: 962 bg = fbg[-1] 963 fbgl = bg.left 964 fbgr = bg.right 965 bgm = bg.start 966 fbw = scale*(frame._width-fbgl-fbgr) 967 fbx = x0+scale*(fbgl-frame._leftPadding) 968 fbh = y + h + pS 969 fby = max(y0,y-pS) 970 fbh = max(0,fbh-fby) 971 bg.render(canv,frame,fbx,fby,fbw,fbh) 972 c._frame = frame 973 c.drawOn(canv,x,y,_sW=aW-w) 974 if c is not content[-1] and not getattr(c,'_SPACETRANSFER',None): 975 y -= pS 976 del c._frame 977 978 def copyContent(self,content=None): 979 C = [].append 980 for c in (content or self._content): 981 C(cdeepcopy(c)) 982 self._content = C.__self__ 983 984class PTOContainer(_Container,Flowable): 985 '''PTOContainer(contentList,trailerList,headerList) 986 987 A container for flowables decorated with trailer & header lists. 988 If the split operation would be called then the trailer and header 989 lists are injected before and after the split. This allows specialist 990 "please turn over" and "continued from previous" like behaviours.''' 991 def __init__(self,content,trailer=None,header=None): 992 I = _PTOInfo(trailer,header) 993 self._content = C = [] 994 for _ in _flowableSublist(content): 995 if isinstance(_,PTOContainer): 996 C.extend(_._content) 997 else: 998 C.append(_) 999 if not hasattr(_,'_ptoinfo'): _._ptoinfo = I 1000 1001 def wrap(self,availWidth,availHeight): 1002 self.width, self.height = _listWrapOn(self._content,availWidth,self.canv) 1003 return self.width,self.height 1004 1005 def split(self, availWidth, availHeight): 1006 from reportlab.platypus.doctemplate import Indenter 1007 if availHeight<0: return [] 1008 canv = self.canv 1009 C = self._content 1010 x = i = H = pS = hx = 0 1011 n = len(C) 1012 I2W = {} 1013 dLeft = dRight = 0 1014 for x in xrange(n): 1015 c = C[x] 1016 I = c._ptoinfo 1017 if I not in I2W.keys(): 1018 T = I.trailer 1019 Hdr = I.header 1020 tW, tH = _listWrapOn(T, availWidth, self.canv) 1021 if len(T): #trailer may have no content 1022 tSB = T[0].getSpaceBefore() 1023 else: 1024 tSB = 0 1025 I2W[I] = T,tW,tH,tSB 1026 else: 1027 T,tW,tH,tSB = I2W[I] 1028 _, h = c.wrapOn(canv,availWidth,0xfffffff) 1029 if isinstance(c,Indenter): 1030 dw = c.left+c.right 1031 dLeft += c.left 1032 dRight += c.right 1033 availWidth -= dw 1034 pS = 0 1035 hx = 0 1036 else: 1037 if x: 1038 hx = max(c.getSpaceBefore()-pS,0) 1039 h += hx 1040 pS = c.getSpaceAfter() 1041 H += h+pS 1042 tHS = tH+max(tSB,pS) 1043 if H+tHS>=availHeight-_FUZZ: break 1044 i += 1 1045 1046 #first retract last thing we tried 1047 H -= (h+pS) 1048 1049 #attempt a sub split on the last one we have 1050 aH = (availHeight-H-tHS-hx)*0.99999 1051 if aH>=0.05*availHeight: 1052 SS = c.splitOn(canv,availWidth,aH) 1053 else: 1054 SS = [] 1055 1056 if abs(dLeft)+abs(dRight)>1e-8: 1057 R1I = [Indenter(-dLeft,-dRight)] 1058 R2I = [Indenter(dLeft,dRight)] 1059 else: 1060 R1I = R2I = [] 1061 1062 if not SS: 1063 j = i 1064 while i>1 and C[i-1].getKeepWithNext(): 1065 i -= 1 1066 C[i].keepWithNext = 0 1067 1068 if i==1 and C[0].getKeepWithNext(): 1069 #robin's black sheep 1070 i = j 1071 C[0].keepWithNext = 0 1072 1073 F = [UseUpSpace()] 1074 1075 if len(SS)>1: 1076 R1 = C[:i]+SS[:1]+R1I+T+F 1077 R2 = Hdr+R2I+SS[1:]+C[i+1:] 1078 elif not i: 1079 return [] 1080 else: 1081 R1 = C[:i]+R1I+T+F 1082 R2 = Hdr+R2I+C[i:] 1083 T = R1 + [PTOContainer(R2,[copy(x) for x in I.trailer],[copy(x) for x in I.header])] 1084 return T 1085 1086#utility functions used by KeepInFrame 1087def _hmodel(s0,s1,h0,h1): 1088 # calculate the parameters in the model 1089 # h = a/s**2 + b/s 1090 a11 = 1./s0**2 1091 a12 = 1./s0 1092 a21 = 1./s1**2 1093 a22 = 1./s1 1094 det = a11*a22-a12*a21 1095 b11 = a22/det 1096 b12 = -a12/det 1097 b21 = -a21/det 1098 b22 = a11/det 1099 a = b11*h0+b12*h1 1100 b = b21*h0+b22*h1 1101 return a,b 1102 1103def _qsolve(h,ab): 1104 '''solve the model v = a/s**2 + b/s for an s which gives us v==h''' 1105 a,b = ab 1106 if abs(a)<=_FUZZ: 1107 return b/h 1108 t = 0.5*b/a 1109 from math import sqrt 1110 f = -h/a 1111 r = t*t-f 1112 if r<0: return None 1113 r = sqrt(r) 1114 if t>=0: 1115 s1 = -t - r 1116 else: 1117 s1 = -t + r 1118 s2 = f/s1 1119 return max(1./s1, 1./s2) 1120 1121class KeepInFrame(_Container,Flowable): 1122 def __init__(self, maxWidth, maxHeight, content=[], mergeSpace=1, mode='shrink', name='',hAlign='LEFT',vAlign='BOTTOM', fakeWidth=None): 1123 '''mode describes the action to take when overflowing 1124 error raise an error in the normal way 1125 continue ignore ie just draw it and report maxWidth, maxHeight 1126 shrink shrinkToFit 1127 truncate fit as much as possible 1128 set fakeWidth to False to make _listWrapOn do the 'right' thing 1129 ''' 1130 self.name = name 1131 self.maxWidth = maxWidth 1132 self.maxHeight = maxHeight 1133 self.mode = mode 1134 assert mode in ('error','overflow','shrink','truncate'), '%s invalid mode value %s' % (self.identity(),mode) 1135 assert maxHeight>=0, '%s invalid maxHeight value %s' % (self.identity(),maxHeight) 1136 if mergeSpace is None: mergeSpace = overlapAttachedSpace 1137 self.mergespace = mergeSpace 1138 self._content = content or [] 1139 self.vAlign = vAlign 1140 self.hAlign = hAlign 1141 self.fakeWidth = fakeWidth 1142 1143 def _getAvailableWidth(self): 1144 return self.maxWidth - self._leftExtraIndent - self._rightExtraIndent 1145 1146 def identity(self, maxLen=None): 1147 return "<%s at %s%s%s> size=%sx%s" % (self.__class__.__name__, hex(id(self)), self._frameName(), 1148 getattr(self,'name','') and (' name="%s"'% getattr(self,'name','')) or '', 1149 getattr(self,'maxWidth','') and (' maxWidth=%s'%fp_str(getattr(self,'maxWidth',0))) or '', 1150 getattr(self,'maxHeight','')and (' maxHeight=%s' % fp_str(getattr(self,'maxHeight')))or '') 1151 1152 def wrap(self,availWidth,availHeight): 1153 from reportlab.platypus.doctemplate import LayoutError 1154 mode = self.mode 1155 maxWidth = float(min(self.maxWidth or availWidth,availWidth)) 1156 maxHeight = float(min(self.maxHeight or availHeight,availHeight)) 1157 fakeWidth = self.fakeWidth 1158 W, H = _listWrapOn(self._content,maxWidth,self.canv, fakeWidth=fakeWidth) 1159 if (mode=='error' and (W>maxWidth+_FUZZ or H>maxHeight+_FUZZ)): 1160 ident = 'content %sx%s too large for %s' % (W,H,self.identity(30)) 1161 #leave to keep apart from the raise 1162 raise LayoutError(ident) 1163 elif W<=maxWidth+_FUZZ and H<=maxHeight+_FUZZ: 1164 self.width = W-_FUZZ #we take what we get 1165 self.height = H-_FUZZ 1166 elif mode in ('overflow','truncate'): #we lie 1167 self.width = min(maxWidth,W)-_FUZZ 1168 self.height = min(maxHeight,H)-_FUZZ 1169 else: 1170 def func(x): 1171 x = float(x) 1172 W, H = _listWrapOn(self._content,x*maxWidth,self.canv, fakeWidth=fakeWidth) 1173 W /= x 1174 H /= x 1175 return W, H 1176 W0 = W 1177 H0 = H 1178 s0 = 1 1179 if W>maxWidth+_FUZZ: 1180 #squeeze out the excess width and or Height 1181 s1 = W/maxWidth #linear model 1182 W, H = func(s1) 1183 if H<=maxHeight+_FUZZ: 1184 self.width = W-_FUZZ 1185 self.height = H-_FUZZ 1186 self._scale = s1 1187 return W,H 1188 s0 = s1 1189 H0 = H 1190 W0 = W 1191 s1 = H/maxHeight 1192 W, H = func(s1) 1193 self.width = W-_FUZZ 1194 self.height = H-_FUZZ 1195 self._scale = s1 1196 if H<min(0.95*maxHeight,maxHeight-10) or H>=maxHeight+_FUZZ: 1197 #the standard case W should be OK, H is short we want 1198 #to find the smallest s with H<=maxHeight 1199 H1 = H 1200 for f in 0, 0.01, 0.05, 0.10, 0.15: 1201 #apply the quadratic model 1202 s = _qsolve(maxHeight*(1-f),_hmodel(s0,s1,H0,H1)) 1203 W, H = func(s) 1204 if H<=maxHeight+_FUZZ and W<=maxWidth+_FUZZ: 1205 self.width = W-_FUZZ 1206 self.height = H-_FUZZ 1207 self._scale = s 1208 break 1209 1210 return self.width, self.height 1211 1212 def drawOn(self, canv, x, y, _sW=0): 1213 scale = getattr(self,'_scale',1.0) 1214 truncate = self.mode=='truncate' 1215 ss = scale!=1.0 or truncate 1216 if ss: 1217 canv.saveState() 1218 if truncate: 1219 p = canv.beginPath() 1220 p.rect(x, y, self.width,self.height) 1221 canv.clipPath(p,stroke=0) 1222 else: 1223 canv.translate(x,y) 1224 x=y=0 1225 canv.scale(1.0/scale, 1.0/scale) 1226 _Container.drawOn(self, canv, x, y, _sW=_sW, scale=scale) 1227 if ss: canv.restoreState() 1228 1229 1230class _FindSplitterMixin: 1231 def _findSplit(self,canv,availWidth,availHeight,mergeSpace=1,obj=None,content=None,paraFix=True): 1232 '''return max width, required height for a list of flowables F''' 1233 W = 0 1234 H = 0 1235 pS = sB = 0 1236 atTop = 1 1237 F = self._getContent(content) 1238 for i,f in enumerate(F): 1239 if hasattr(f,'frameAction'): 1240 from reportlab.platypus.doctemplate import Indenter 1241 if isinstance(f,Indenter): 1242 availWidth -= f.left+f.right 1243 continue 1244 w,h = f.wrapOn(canv,availWidth,0xfffffff) 1245 if w<=_FUZZ or h<=_FUZZ: continue 1246 W = max(W,w) 1247 if not atTop: 1248 s = f.getSpaceBefore() 1249 if mergeSpace: s = max(s-pS,0) 1250 H += s 1251 else: 1252 if obj is not None: obj._spaceBefore = f.getSpaceBefore() 1253 atTop = 0 1254 if H>=availHeight or w>availWidth: 1255 return W, availHeight, F[:i],F[i:] 1256 H += h 1257 if H>availHeight: 1258 aH = availHeight-(H-h) 1259 if paraFix: 1260 from reportlab.platypus.paragraph import Paragraph 1261 if isinstance(f,(Paragraph,Preformatted)): 1262 leading = f.style.leading 1263 nH = leading*int(aH/float(leading))+_FUZZ 1264 if nH<aH: nH += leading 1265 availHeight += nH-aH 1266 aH = nH 1267 S = cdeepcopy(f).splitOn(canv,availWidth,aH) 1268 if not S: 1269 return W, availHeight, F[:i],F[i:] 1270 else: 1271 return W,availHeight,F[:i]+S[:1],S[1:]+F[i+1:] 1272 pS = f.getSpaceAfter() 1273 H += pS 1274 1275 if obj is not None: obj._spaceAfter = pS 1276 return W, H-pS, F, [] 1277 1278 def _getContent(self,content=None): 1279 F = [] 1280 C = content if content is not None else self._content 1281 for f in C: 1282 if isinstance(f,ListFlowable): 1283 F.extend(self._getContent(f._content)) 1284 else: 1285 F.append(f) 1286 return F 1287 1288class ImageAndFlowables(_Container,_FindSplitterMixin,Flowable): 1289 '''combine a list of flowables and an Image''' 1290 def __init__(self,I,F,imageLeftPadding=0,imageRightPadding=3,imageTopPadding=0,imageBottomPadding=3, 1291 imageSide='right', imageHref=None): 1292 self._content = _flowableSublist(F) 1293 self._I = I 1294 self._irpad = imageRightPadding 1295 self._ilpad = imageLeftPadding 1296 self._ibpad = imageBottomPadding 1297 self._itpad = imageTopPadding 1298 self._side = imageSide 1299 self.imageHref = imageHref 1300 1301 def deepcopy(self): 1302 c = copy(self) #shallow 1303 self._reset() 1304 c.copyContent() #partially deep? 1305 return c 1306 1307 def getSpaceAfter(self): 1308 if hasattr(self,'_C1'): 1309 C = self._C1 1310 elif hasattr(self,'_C0'): 1311 C = self._C0 1312 else: 1313 C = self._content 1314 return _Container.getSpaceAfter(self,C) 1315 1316 def getSpaceBefore(self): 1317 return max(self._I.getSpaceBefore(),_Container.getSpaceBefore(self)) 1318 1319 def _reset(self): 1320 for a in ('_wrapArgs','_C0','_C1'): 1321 try: 1322 delattr(self,a) 1323 except: 1324 pass 1325 1326 def wrap(self,availWidth,availHeight): 1327 canv = self.canv 1328 I = self._I 1329 if hasattr(self,'_wrapArgs'): 1330 if self._wrapArgs==(availWidth,availHeight) and getattr(I,'_oldDrawSize',None) is None: 1331 return self.width,self.height 1332 self._reset() 1333 I._unRestrictSize() 1334 self._wrapArgs = availWidth, availHeight 1335 I.wrap(availWidth,availHeight) 1336 wI, hI = I._restrictSize(availWidth,availHeight) 1337 self._wI = wI 1338 self._hI = hI 1339 ilpad = self._ilpad 1340 irpad = self._irpad 1341 ibpad = self._ibpad 1342 itpad = self._itpad 1343 self._iW = iW = availWidth - irpad - wI - ilpad 1344 aH = itpad + hI + ibpad 1345 if iW>_FUZZ: 1346 W,H0,self._C0,self._C1 = self._findSplit(canv,iW,aH) 1347 else: 1348 W = availWidth 1349 H0 = 0 1350 if W>iW+_FUZZ: 1351 self._C0 = [] 1352 self._C1 = self._content 1353 aH = self._aH = max(aH,H0) 1354 self.width = availWidth 1355 if not self._C1: 1356 self.height = aH 1357 else: 1358 W1,H1 = _listWrapOn(self._C1,availWidth,canv) 1359 self.height = aH+H1 1360 return self.width, self.height 1361 1362 def split(self,availWidth, availHeight): 1363 if hasattr(self,'_wrapArgs'): 1364 I = self._I 1365 if self._wrapArgs!=(availWidth,availHeight) or getattr(I,'_oldDrawSize',None) is not None: 1366 self._reset() 1367 I._unRestrictSize() 1368 W,H=self.wrap(availWidth,availHeight) 1369 if self._aH>availHeight: return [] 1370 C1 = self._C1 1371 if C1: 1372 S = C1[0].split(availWidth,availHeight-self._aH) 1373 if not S: 1374 _C1 = [] 1375 else: 1376 _C1 = [S[0]] 1377 C1 = S[1:]+C1[1:] 1378 else: 1379 _C1 = [] 1380 return [ImageAndFlowables( 1381 self._I, 1382 self._C0+_C1, 1383 imageLeftPadding=self._ilpad, 1384 imageRightPadding=self._irpad, 1385 imageTopPadding=self._itpad, 1386 imageBottomPadding=self._ibpad, 1387 imageSide=self._side, imageHref=self.imageHref) 1388 ]+C1 1389 1390 def drawOn(self, canv, x, y, _sW=0): 1391 if self._side=='left': 1392 Ix = x + self._ilpad 1393 Fx = Ix+ self._irpad + self._wI 1394 else: 1395 Ix = x + self.width-self._wI-self._irpad 1396 Fx = x 1397 self._I.drawOn(canv,Ix,y+self.height-self._itpad-self._hI) 1398 1399 if self.imageHref: 1400 canv.linkURL(self.imageHref, (Ix, y+self.height-self._itpad-self._hI, Ix + self._wI, y+self.height), relative=1) 1401 1402 if self._C0: 1403 _Container.drawOn(self, canv, Fx, y, content=self._C0, aW=self._iW) 1404 if self._C1: 1405 aW, aH = self._wrapArgs 1406 _Container.drawOn(self, canv, x, y-self._aH,content=self._C1, aW=aW) 1407 1408class _AbsRect(NullDraw): 1409 _ZEROSIZE=1 1410 _SPACETRANSFER = True 1411 def __init__(self,x,y,width,height,strokeWidth=0,strokeColor=None,fillColor=None,strokeDashArray=None): 1412 self._x = x 1413 self._y = y 1414 self._width = width 1415 self._height = height 1416 self._strokeColor = strokeColor 1417 self._fillColor = fillColor 1418 self._strokeWidth = strokeWidth 1419 self._strokeDashArray = strokeDashArray 1420 1421 def wrap(self, availWidth, availHeight): 1422 return 0,0 1423 1424 def drawOn(self, canv, x, y, _sW=0): 1425 if self._width>_FUZZ and self._height>_FUZZ: 1426 st = self._strokeColor and self._strokeWidth is not None and self._strokeWidth>=0 1427 if st or self._fillColor: 1428 canv.saveState() 1429 if st: 1430 canv.setStrokeColor(self._strokeColor) 1431 canv.setLineWidth(self._strokeWidth) 1432 if self._fillColor: 1433 canv.setFillColor(self._fillColor) 1434 canv.rect(self._x,self._y,self._width,self._height,stroke=1 if st else 0, fill=1 if self._fillColor else 0) 1435 canv.restoreState() 1436 1437class _ExtendBG(NullDraw): 1438 _ZEROSIZE=1 1439 _SPACETRANSFER = True 1440 def __init__(self,y,height,bg,frame): 1441 self._y = y 1442 self._height = height 1443 self._bg = bg 1444 1445 def wrap(self, availWidth, availHeight): 1446 return 0,0 1447 1448 def frameAction(self, frame): 1449 bg = self._bg 1450 fby = self._y 1451 fbh = self._height 1452 fbgl = bg.left 1453 fbw = frame._width - fbgl - bg.right 1454 fbx = frame._x1 - fbgl 1455 canv = self.canv 1456 pn = canv.getPageNumber() 1457 bg.render(canv,frame,fbx,fby,fbw,fbh) 1458 1459class _AbsLine(NullDraw): 1460 _ZEROSIZE=1 1461 _SPACETRANSFER = True 1462 def __init__(self,x,y,x1,y1,strokeWidth=0,strokeColor=None,strokeDashArray=None): 1463 self._x = x 1464 self._y = y 1465 self._x1 = x1 1466 self._y1 = y1 1467 self._strokeColor = strokeColor 1468 self._strokeWidth = strokeWidth 1469 self._strokeDashArray = strokeDashArray 1470 1471 def wrap(self, availWidth, availHeight): 1472 return 0,0 1473 1474 def drawOn(self, canv, x, y, _sW=0): 1475 if self._strokeColor and self._strokeWidth is not None and self._strokeWidth>=0: 1476 canv.saveState() 1477 canv.setStrokeColor(self._strokeColor) 1478 canv.setLineWidth(self._strokeWidth) 1479 canv.line(self._x,self._y,self._x1,self._y1) 1480 canv.restoreState() 1481 1482class BalancedColumns(_FindSplitterMixin,NullDraw): 1483 '''combine a list of flowables and an Image''' 1484 def __init__(self, F, nCols=2, needed=72, spaceBefore=0, spaceAfter=0, showBoundary=None, 1485 leftPadding=None, innerPadding=None, rightPadding=None, topPadding=None, bottomPadding=None, 1486 name='', endSlack=0.1, 1487 boxStrokeColor=None, 1488 boxStrokeWidth=0, 1489 boxFillColor=None, 1490 boxMargin=None, 1491 vLinesStrokeColor=None, 1492 vLinesStrokeWidth=None, 1493 ): 1494 self.name = name or 'BalancedColumns-%d' % id(self) 1495 if nCols <2: 1496 raise ValueError('nCols should be at least 2 not %r in %s' % (nCols,self.identitity())) 1497 self._content = _flowableSublist(F) 1498 self._nCols = nCols 1499 self.spaceAfter = spaceAfter 1500 self._leftPadding = leftPadding 1501 self._innerPadding = innerPadding 1502 self._rightPadding = rightPadding 1503 self._topPadding = topPadding 1504 self._bottomPadding = bottomPadding 1505 self.spaceBefore = spaceBefore 1506 self._needed = needed - _FUZZ 1507 self.showBoundary = showBoundary 1508 self.endSlack = endSlack #what we might allow as a lastcolumn overrun 1509 self._boxStrokeColor = boxStrokeColor 1510 self._boxStrokeWidth = boxStrokeWidth 1511 self._boxFillColor = boxFillColor 1512 self._boxMargin = boxMargin 1513 self._vLinesStrokeColor = vLinesStrokeColor 1514 self._vLinesStrokeWidth = vLinesStrokeWidth 1515 1516 def identity(self, maxLen=None): 1517 return "<%s nCols=%r at %s%s%s>" % (self.__class__.__name__, self._nCols, hex(id(self)), self._frameName(), 1518 getattr(self,'name','') and (' name="%s"'% getattr(self,'name','')) or '', 1519 ) 1520 1521 def getSpaceAfter(self): 1522 return self.spaceAfter 1523 1524 def getSpaceBefore(self): 1525 return self.spaceBefore 1526 1527 def _generated_content(self,aW,aH): 1528 G = [] 1529 frame = self._frame 1530 from reportlab.platypus.doctemplate import CurrentFrameFlowable,LayoutError, ActionFlowable, Indenter 1531 from reportlab.platypus.frames import Frame 1532 from reportlab.platypus.doctemplate import FrameBreak 1533 lpad = frame._leftPadding if self._leftPadding is None else self._leftPadding 1534 rpad = frame._rightPadding if self._rightPadding is None else self._rightPadding 1535 tpad = frame._topPadding if self._topPadding is None else self._topPadding 1536 bpad = frame._bottomPadding if self._bottomPadding is None else self._bottomPadding 1537 leftExtraIndent = frame._leftExtraIndent 1538 rightExtraIndent = frame._rightExtraIndent 1539 gap = max(lpad,rpad) if self._innerPadding is None else self._innerPadding 1540 hgap = gap*0.5 1541 canv = self.canv 1542 nCols = self._nCols 1543 cw = (aW - gap*(nCols-1) - lpad - rpad)/float(nCols) 1544 aH0 = aH 1545 aH -= tpad + bpad 1546 W,H0,_C0,C2 = self._findSplit(canv,cw,nCols*aH,paraFix=False) 1547 if not _C0: 1548 raise ValueError( 1549 "%s cannot make initial split aW=%r aH=%r ie cw=%r ah=%r\ncontent=%s" % ( 1550 self.identity(),aW,aH,cw,nCols*aH, 1551 [f.__class__.__name__ for f in self._content], 1552 )) 1553 _fres = {} 1554 def splitFunc(ah,endSlack=0): 1555 if ah not in _fres: 1556 c = [] 1557 w = 0 1558 h = 0 1559 cn = None 1560 icheck = nCols-2 if endSlack else -1 1561 for i in xrange(nCols): 1562 wi, hi, c0, c1 = self._findSplit(canv,cw,ah,content=cn,paraFix=False) 1563 w = max(w,wi) 1564 h = max(h,hi) 1565 c.append(c0) 1566 if i==icheck: 1567 wc, hc, cc0, cc1 = self._findSplit(canv,cw,2*ah,content=c1,paraFix=False) 1568 if hc<=(1+endSlack)*ah: 1569 c.append(c1) 1570 h = ah-1e-6 1571 cn = [] 1572 break 1573 cn = c1 1574 _fres[ah] = ah+100000*int(cn!=[]),cn==[],(w,h,c,cn) 1575 return _fres[ah][2] 1576 1577 endSlack = 0 1578 if C2: 1579 H = aH 1580 else: 1581 #we are short so use H0 to figure out what to use 1582 import math 1583 1584 def func(ah): 1585 splitFunc(ah) 1586 return _fres[ah][0] 1587 1588 def gss(f, a, b, tol=1, gr=(math.sqrt(5) + 1) / 2): 1589 c = b - (b - a) / gr 1590 d = a + (b - a) / gr 1591 while abs(a - b) > tol: 1592 if f(c) < f(d): 1593 b = d 1594 else: 1595 a = c 1596 1597 # we recompute both c and d here to avoid loss of precision which may lead to incorrect results or infinite loop 1598 c = b - (b - a) / gr 1599 d = a + (b - a) / gr 1600 1601 F = [(x,tf,v) for x,tf,v in _fres.values() if tf] 1602 if F: 1603 F.sort() 1604 return F[0][2] 1605 return None 1606 1607 H = min(int(H0/float(nCols)+self.spaceAfter*0.4),aH) 1608 splitFunc(H) 1609 if not _fres[H][1]: 1610 H = gss(func,H,aH) 1611 if H: 1612 W, H0, _C0, C2 = H 1613 H = H0 1614 endSlack = False 1615 else: 1616 H = aH 1617 endSlack = self.endSlack 1618 else: 1619 H1 = H0/float(nCols) 1620 splitFunc(H1) 1621 if not _fres[H1][1]: 1622 H = gss(func,H,aH) 1623 if H: 1624 W, H0, _C0, C2 = H 1625 H = H0 1626 endSlack = False 1627 else: 1628 H = aH 1629 endSlack = self.endSlack 1630 assert not C2, "unexpected non-empty C2" 1631 W1, H1, C, C1 = splitFunc(H, endSlack) 1632 _fres.clear() 1633 if C[0]==[] and C[1]==[] and C1: 1634 #no split situation 1635 C, C1 = [C1,C[1]], C[0] 1636 1637 x1 = frame._x1 1638 y1 = frame._y1 1639 fw = frame._width 1640 ftop = y1+bpad+tpad+aH 1641 fh = H1 + bpad + tpad 1642 y2 = ftop - fh 1643 dx = aW / float(nCols) 1644 if leftExtraIndent or rightExtraIndent: 1645 indenter0 = Indenter(-leftExtraIndent,-rightExtraIndent) 1646 indenter1 = Indenter(leftExtraIndent,rightExtraIndent) 1647 else: 1648 indenter0 = indenter1 = None 1649 1650 showBoundary=self.showBoundary if self.showBoundary is not None else frame.showBoundary 1651 obx = x1+leftExtraIndent+frame._leftPadding 1652 F = [Frame(obx+i*dx,y2,dx,fh, 1653 leftPadding=lpad if not i else hgap, bottomPadding=bpad, 1654 rightPadding=rpad if i==nCols-1 else hgap, topPadding=tpad, 1655 id='%s-%d' %(self.name,i), 1656 showBoundary=showBoundary, 1657 overlapAttachedSpace=frame._oASpace, 1658 _debug=frame._debug) for i in xrange(nCols)] 1659 1660 1661 #we are going to modify the current template 1662 T=self._doctemplateAttr('pageTemplate') 1663 if T is None: 1664 raise LayoutError('%s used in non-doctemplate environment' % self.identity()) 1665 1666 BGs = getattr(frame,'_frameBGs',None) 1667 xbg = bg = BGs[-1] if BGs else None 1668 1669 class TAction(ActionFlowable): 1670 '''a special Action flowable that sets stuff on the doc template T''' 1671 def __init__(self, bgs=[],F=[],f=None): 1672 Flowable.__init__(self) 1673 self.bgs = bgs 1674 self.F = F 1675 self.f = f 1676 1677 def apply(self,doc,T=T): 1678 T.frames = self.F 1679 frame._frameBGs = self.bgs 1680 doc.handle_currentFrame(self.f.id) 1681 frame._frameBGs = self.bgs 1682 1683 if bg: 1684 #G.append(Spacer(1e-5,1e-5)) 1685 #G[-1].__id__ = 'spacer0' 1686 xbg = _ExtendBG(y2,fh,bg,frame) 1687 G.append(xbg) 1688 1689 oldFrames = T.frames 1690 G.append(TAction([],F,F[0])) 1691 if indenter0: G.append(indenter0) 1692 doBox = (self._boxStrokeColor and self._boxStrokeWidth and self._boxStrokeWidth>=0) or self._boxFillColor 1693 doVLines = self._vLinesStrokeColor and self._vLinesStrokeWidth and self._vLinesStrokeWidth>=0 1694 if doBox or doVLines: 1695 obm = self._boxMargin 1696 if not obm: obm = (0,0,0,0) 1697 if len(obm)==1: 1698 obmt = obml = obmr = obmb = obm[0] 1699 elif len(obm)==2: 1700 obmt = obmb = obm[0] 1701 obml = obmr = obm[1] 1702 elif len(obm)==3: 1703 obmt = obm[0] 1704 obml = obmr = obm[1] 1705 obmb = obm[2] 1706 elif len(obm)==4: 1707 obmt = obm[0] 1708 obmr = obm[1] 1709 obmb = obm[2] 1710 obml = obm[3] 1711 else: 1712 raise ValueError('Invalid value %s for boxMargin' % repr(obm)) 1713 obx1 = obx - obml 1714 obx2 = F[-1]._x1+F[-1]._width + obmr 1715 oby2 = y2-obmb 1716 obh = fh+obmt+obmb 1717 oby1 = oby2+obh 1718 if doBox: 1719 box = _AbsRect(obx1,oby2, obx2-obx1, obh, 1720 fillColor=self._boxFillColor, 1721 strokeColor=self._boxStrokeColor, 1722 strokeWidth=self._boxStrokeWidth, 1723 ) 1724 if doVLines: 1725 vLines = [] 1726 for i in xrange(1,nCols): 1727 vlx = 0.5*(F[i]._x1 + F[i-1]._x1+F[i-1]._width) 1728 vLines.append(_AbsLine(vlx,oby2,vlx,oby1,strokeWidth=self._vLinesStrokeWidth,strokeColor=self._vLinesStrokeColor)) 1729 else: 1730 oby1 = ftop 1731 oby2 = y2 1732 1733 if doBox: G.append(box) 1734 if doVLines: G.extend(vLines) 1735 sa = self.getSpaceAfter() 1736 for i in xrange(nCols): 1737 Ci = C[i] 1738 if Ci: 1739 Ci = KeepInFrame(W1,H1,Ci,mode='shrink') 1740 sa = max(sa,Ci.getSpaceAfter()) 1741 G.append(Ci) 1742 if i!=nCols-1: 1743 G.append(FrameBreak) 1744 G.append(TAction(BGs,oldFrames,frame)) 1745 if xbg: 1746 if C1: sa = 0 1747 xbg._y = min(y2,oby2) - sa 1748 xbg._height = max(ftop,oby1) - xbg._y 1749 if indenter1: G.append(indenter1) 1750 if C1: 1751 G.append( 1752 BalancedColumns(C1, nCols=nCols, 1753 needed=self._needed, spaceBefore=self.spaceBefore, spaceAfter=self.spaceAfter, 1754 showBoundary=self.showBoundary, 1755 leftPadding=self._leftPadding, 1756 innerPadding=self._innerPadding, 1757 rightPadding=self._rightPadding, 1758 topPadding=self._topPadding, 1759 bottomPadding=self._bottomPadding, 1760 name=self.name+'-1', endSlack=self.endSlack, 1761 boxStrokeColor=self._boxStrokeColor, 1762 boxStrokeWidth=self._boxStrokeWidth, 1763 boxFillColor=self._boxFillColor, 1764 boxMargin=self._boxMargin, 1765 vLinesStrokeColor=self._vLinesStrokeColor, 1766 vLinesStrokeWidth=self._vLinesStrokeWidth, 1767 ) 1768 ) 1769 return fh, G 1770 1771 def wrap(self,aW,aH): 1772 #here's where we mess with everything 1773 if aH<self.spaceBefore+self._needed-_FUZZ: 1774 #we are going straight to the nextTemplate with no attempt to modify the frames 1775 G = [PageBreak(), self] 1776 H1 = 0 1777 else: 1778 H1, G = self._generated_content(aW,aH) 1779 1780 self._frame.add_generated_content(*G) 1781 return 0,min(H1,aH) 1782 1783class AnchorFlowable(Spacer): 1784 '''create a bookmark in the pdf''' 1785 _ZEROSIZE=1 1786 _SPACETRANSFER = True 1787 def __init__(self,name): 1788 Spacer.__init__(self,0,0) 1789 self._name = name 1790 1791 def __repr__(self): 1792 return "%s(%s)" % (self.__class__.__name__,self._name) 1793 1794 def wrap(self,aW,aH): 1795 return 0,0 1796 1797 def draw(self): 1798 self.canv.bookmarkHorizontal(self._name,0,0) 1799 1800class _FBGBag(ABag): 1801 def matches(self,frame,canv): 1802 fid = id(frame) 1803 return ((isinstance(self.fid,list) and fid in self.fid or fid==self.fid) 1804 and id(canv)==self.cid and self.pn==canv.getPageNumber()) 1805 1806 def getDims(self,canv): 1807 self._inst = canv._code[self.codePos].split() 1808 return map(float,self._inst[1:5]) 1809 1810 def setDims(self,canv,x,y,w,h): 1811 self._inst[1:5] = [fp_str(x,y,w,h)] 1812 canv._code[self.codePos] = ' '.join(self._inst) 1813 1814 def render(self,canv,frame,fbx,fby,fbw,fbh): 1815 if abs(fbw)>_FUZZ and abs(fbh)>_FUZZ: 1816 pn = canv.getPageNumber() 1817 if self.fid==id(frame) and self.cid==id(canv) and self.pn==pn: 1818 ox,oy,ow,oh = self.getDims(canv) 1819 self.setDims(canv,ox,fby,ow,oh+oy-fby) 1820 else: 1821 canv.saveState() 1822 fbgc = self.fillColor 1823 if fbgc: 1824 canv.setFillColor(fbgc) 1825 sw = self.strokeWidth 1826 sc = None if sw is None or sw<0 else self.strokeColor 1827 if sc: 1828 canv.setStrokeColor(sc) 1829 canv.setLineWidth(sw) 1830 da = self.strokeDashArray 1831 if da: 1832 canv.setDash(da) 1833 self.fid = id(frame) 1834 self.cid = id(canv) 1835 self.pn = pn 1836 self.codePos = len(canv._code) 1837 canv.rect(fbx,fby,fbw,fbh,stroke=1 if sc else 0,fill=1 if fbgc else 0) 1838 canv.restoreState() 1839 1840class FrameBG(AnchorFlowable): 1841 """Start or stop coloring the frame background 1842 left & right are distances from the edge of the frame to start stop colouring. 1843 if start in ('frame','frame-permanent') then the background is filled from here to the bottom of the frame and immediately discarded 1844 for the frame case. 1845 """ 1846 _ZEROSIZE=1 1847 def __init__(self, color=None, left=0, right=0, start=True, strokeWidth=None, strokeColor=None, strokeDashArray=None): 1848 Spacer.__init__(self,0,0) 1849 self.start = start 1850 if start: 1851 from reportlab.platypus.doctemplate import _evalMeasurement 1852 self.left = _evalMeasurement(left) 1853 self.right = _evalMeasurement(right) 1854 self.color = color 1855 self.strokeWidth = strokeWidth 1856 self.strokeColor = strokeColor 1857 self.strokeDashArray = strokeDashArray 1858 1859 def __repr__(self): 1860 return "%s(%s)" % (self.__class__.__name__,', '.join(['%s=%r' % (i,getattr(self,i,None)) for i in 'start color left right'.split()])) 1861 1862 def draw(self): 1863 frame = getattr(self,'_frame',None) 1864 if frame is None: return 1865 if self.start: 1866 sc = self.strokeColor 1867 sw = self.strokeWidth 1868 sw = -1 if sw is None else sw 1869 frame._frameBGs.append( 1870 _FBGBag(left=self.left, 1871 right=self.right, 1872 fillColor=self.color, 1873 start=self.start if self.start in ('frame','frame-permanent') else None, 1874 strokeColor=self.strokeColor, 1875 strokeWidth=self.strokeWidth, 1876 strokeDashArray=self.strokeDashArray, 1877 fid = 0, 1878 cid = 0, 1879 pn = -1, 1880 codePos = None, 1881 active=True, 1882 )) 1883 elif frame._frameBGs: 1884 frame._frameBGs.pop() 1885 1886class FrameSplitter(NullDraw): 1887 '''When encountered this flowable should either switch directly to nextTemplate 1888 if remaining space in the current frame is less than gap+required or it should 1889 temporarily modify the current template to have the frames from nextTemplate 1890 that are listed in nextFrames and switch to the first of those frames. 1891 ''' 1892 _ZEROSIZE=1 1893 def __init__(self, nextTemplate, nextFrames=[], gap=10, required=72, adjustHeight=True): 1894 self.nextTemplate = nextTemplate 1895 self.nextFrames = nextFrames or [] 1896 self.gap = gap 1897 self.required = required 1898 self.adjustHeight = adjustHeight 1899 1900 def wrap(self,aW,aH): 1901 frame = self._frame 1902 from reportlab.platypus.doctemplate import NextPageTemplate,CurrentFrameFlowable,LayoutError 1903 G=[NextPageTemplate(self.nextTemplate)] 1904 if aH<self.gap+self.required-_FUZZ: 1905 #we are going straight to the nextTemplate with no attempt to modify the frames 1906 G.append(PageBreak()) 1907 else: 1908 #we are going to modify the incoming templates 1909 templates = self._doctemplateAttr('pageTemplates') 1910 if templates is None: 1911 raise LayoutError('%s called in non-doctemplate environment'%self.identity()) 1912 T=[t for t in templates if t.id==self.nextTemplate] 1913 if not T: 1914 raise LayoutError('%s.nextTemplate=%s not found' % (self.identity(),self.nextTemplate)) 1915 T=T[0] 1916 F=[f for f in T.frames if f.id in self.nextFrames] 1917 N=[f.id for f in F] 1918 N=[f for f in self.nextFrames if f not in N] 1919 if N: 1920 raise LayoutError('%s frames=%r not found in pageTemplate(%s)\n%r has frames %r' % (self.identity(),N,T.id,T,[f.id for f in T.frames])) 1921 T=self._doctemplateAttr('pageTemplate') 1922 def unwrap(canv,doc,T=T,onPage=T.onPage,oldFrames=T.frames): 1923 T.frames=oldFrames 1924 T.onPage=onPage 1925 onPage(canv,doc) 1926 T.onPage=unwrap 1927 h=aH-self.gap 1928 for i,f in enumerate(F): 1929 f=copy(f) 1930 if self.adjustHeight: f.height=h 1931 f._reset() 1932 F[i]=f 1933 T.frames=F 1934 G.append(CurrentFrameFlowable(F[0].id)) 1935 frame.add_generated_content(*G) 1936 return 0,0 1937 1938 1939from reportlab.lib.sequencer import _type2formatter 1940_bulletNames = dict( 1941 bulletchar=u'\u2022', #usually a small circle 1942 circle=u'\u25cf', #circle as high as the font 1943 square=u'\u25a0', 1944 disc=u'\u25cb', 1945 diamond=u'\u25c6', 1946 diamondwx=u'\u2756', 1947 rarrowhead=u'\u27a4', 1948 sparkle=u'\u2747', 1949 squarelrs=u'\u274f', 1950 blackstar=u'\u2605', 1951 ) 1952 1953def _bulletFormat(value,type='1',format=None): 1954 if type=='bullet': 1955 s = _bulletNames.get(value,value) 1956 else: 1957 s = _type2formatter[type](int(value)) 1958 1959 if format: 1960 if isinstance(format,strTypes): 1961 s = format % s 1962 elif callable(format): 1963 s = format(s) 1964 else: 1965 raise ValueError('unexpected BulletDrawer format %r' % format) 1966 return s 1967 1968class BulletDrawer: 1969 def __init__(self, 1970 value='0', 1971 bulletAlign='left', 1972 bulletType='1', 1973 bulletColor='black', 1974 bulletFontName='Helvetica', 1975 bulletFontSize=12, 1976 bulletOffsetY=0, 1977 bulletDedent=0, 1978 bulletDir='ltr', 1979 bulletFormat=None, 1980 ): 1981 self.value = value 1982 self._bulletAlign = bulletAlign 1983 self._bulletType = bulletType 1984 self._bulletColor = bulletColor 1985 self._bulletFontName = bulletFontName 1986 self._bulletFontSize = bulletFontSize 1987 self._bulletOffsetY = bulletOffsetY 1988 self._bulletDedent = bulletDedent 1989 self._bulletDir = bulletDir 1990 self._bulletFormat = bulletFormat 1991 1992 def drawOn(self,indenter,canv,x,y,_sW=0): 1993 value = self.value 1994 if not value: return 1995 canv.saveState() 1996 canv.translate(x, y) 1997 1998 y = indenter.height-self._bulletFontSize+self._bulletOffsetY 1999 if self._bulletDir=='rtl': 2000 x = indenter.width - indenter._rightIndent + self._bulletDedent 2001 else: 2002 x = indenter._leftIndent - self._bulletDedent 2003 canv.setFont(self._bulletFontName,self._bulletFontSize) 2004 canv.setFillColor(self._bulletColor) 2005 bulletAlign = self._bulletAlign 2006 value = _bulletFormat(value,self._bulletType,self._bulletFormat) 2007 2008 if bulletAlign=='left': 2009 canv.drawString(x,y,value) 2010 elif bulletAlign=='right': 2011 canv.drawRightString(x,y,value) 2012 elif bulletAlign in ('center','centre'): 2013 canv.drawCentredString(x,y,value) 2014 elif bulletAlign.startswith('numeric') or bulletAlign.startswith('decimal'): 2015 pc = bulletAlign[7:].strip() or '.' 2016 canv.drawAlignedString(x,y,value,pc) 2017 else: 2018 raise ValueError('Invalid bulletAlign: %r' % bulletAlign) 2019 canv.restoreState() 2020 2021def _computeBulletWidth(b,value): 2022 value = _bulletFormat(value,b._bulletType,b._bulletFormat) 2023 return stringWidth(value,b._bulletFontName,b._bulletFontSize) 2024 2025class DDIndenter(Flowable): 2026 _IndenterAttrs = '_flowable _leftIndent _rightIndent width height'.split() 2027 def __init__(self,flowable,leftIndent=0,rightIndent=0): 2028 self._flowable = flowable 2029 self._leftIndent = leftIndent 2030 self._rightIndent = rightIndent 2031 self.width = None 2032 self.height = None 2033 2034 def split(self, aW, aH): 2035 S = self._flowable.split(aW-self._leftIndent-self._rightIndent, aH) 2036 return [ 2037 DDIndenter(s, 2038 leftIndent=self._leftIndent, 2039 rightIndent=self._rightIndent, 2040 ) for s in S 2041 ] 2042 2043 def drawOn(self, canv, x, y, _sW=0): 2044 self._flowable.drawOn(canv,x+self._leftIndent,y,max(0,_sW-self._leftIndent-self._rightIndent)) 2045 2046 def wrap(self, aW, aH): 2047 w,h = self._flowable.wrap(aW-self._leftIndent-self._rightIndent, aH) 2048 self.width = w+self._leftIndent+self._rightIndent 2049 self.height = h 2050 return self.width,h 2051 2052 def __getattr__(self,a): 2053 if a in self._IndenterAttrs: 2054 try: 2055 return self.__dict__[a] 2056 except KeyError: 2057 if a not in ('spaceBefore','spaceAfter'): 2058 raise AttributeError('%r has no attribute %s' % (self,a)) 2059 return getattr(self._flowable,a) 2060 2061 def __setattr__(self,a,v): 2062 if a in self._IndenterAttrs: 2063 self.__dict__[a] = v 2064 else: 2065 setattr(self._flowable,a,v) 2066 2067 def __delattr__(self,a): 2068 if a in self._IndenterAttrs: 2069 del self.__dict__[a] 2070 else: 2071 delattr(self._flowable,a) 2072 2073 def identity(self,maxLen=None): 2074 return '%s containing %s' % (self.__class__.__name__,self._flowable.identity(maxLen)) 2075 2076class LIIndenter(DDIndenter): 2077 _IndenterAttrs = '_flowable _bullet _leftIndent _rightIndent width height spaceBefore spaceAfter'.split() 2078 def __init__(self,flowable,leftIndent=0,rightIndent=0,bullet=None, spaceBefore=None, spaceAfter=None): 2079 self._flowable = flowable 2080 self._bullet = bullet 2081 self._leftIndent = leftIndent 2082 self._rightIndent = rightIndent 2083 self.width = None 2084 self.height = None 2085 if spaceBefore is not None: 2086 self.spaceBefore = spaceBefore 2087 if spaceAfter is not None: 2088 self.spaceAfter = spaceAfter 2089 2090 def split(self, aW, aH): 2091 S = self._flowable.split(aW-self._leftIndent-self._rightIndent, aH) 2092 return [ 2093 LIIndenter(s, 2094 leftIndent=self._leftIndent, 2095 rightIndent=self._rightIndent, 2096 bullet = (s is S[0] and self._bullet or None), 2097 ) for s in S 2098 ] 2099 2100 def drawOn(self, canv, x, y, _sW=0): 2101 if self._bullet: 2102 self._bullet.drawOn(self,canv,x,y,0) 2103 self._flowable.drawOn(canv,x+self._leftIndent,y,max(0,_sW-self._leftIndent-self._rightIndent)) 2104 2105 2106from reportlab.lib.styles import ListStyle 2107class ListItem: 2108 def __init__(self, 2109 flowables, #the initial flowables 2110 style=None, 2111 #leftIndent=18, 2112 #rightIndent=0, 2113 #spaceBefore=None, 2114 #spaceAfter=None, 2115 #bulletType='1', 2116 #bulletColor='black', 2117 #bulletFontName='Helvetica', 2118 #bulletFontSize=12, 2119 #bulletOffsetY=0, 2120 #bulletDedent='auto', 2121 #bulletDir='ltr', 2122 #bulletFormat=None, 2123 **kwds 2124 ): 2125 if not isinstance(flowables,(list,tuple)): 2126 flowables = (flowables,) 2127 self._flowables = flowables 2128 params = self._params = {} 2129 2130 if style: 2131 if not isinstance(style,ListStyle): 2132 raise ValueError('%s style argument (%r) not a ListStyle' % (self.__class__.__name__,style)) 2133 self._style = style 2134 2135 for k in ListStyle.defaults: 2136 if k in kwds: 2137 v = kwds.get(k) 2138 elif style: 2139 v = getattr(style,k) 2140 else: 2141 continue 2142 params[k] = v 2143 2144 for k in ('value', 'spaceBefore','spaceAfter'): 2145 v = kwds.get(k,getattr(style,k,None)) 2146 if v is not None: 2147 params[k] = v 2148 2149class _LIParams: 2150 def __init__(self,flowable,params,value,first): 2151 self.flowable = flowable 2152 self.params = params 2153 self.value = value 2154 self.first= first 2155 2156class ListFlowable(_Container,Flowable): 2157 _numberStyles = '1aAiI' 2158 def __init__(self, 2159 flowables, #the initial flowables 2160 start=None, 2161 style=None, 2162 #leftIndent=18, 2163 #rightIndent=0, 2164 #spaceBefore=None, 2165 #spaceAfter=None, 2166 #bulletType='1', 2167 #bulletColor='black', 2168 #bulletFontName='Helvetica', 2169 #bulletFontSize=12, 2170 #bulletOffsetY=0, 2171 #bulletDedent='auto', 2172 #bulletDir='ltr', 2173 #bulletFormat=None, 2174 **kwds 2175 ): 2176 self._flowables = flowables 2177 2178 if style: 2179 if not isinstance(style,ListStyle): 2180 raise ValueError('%s style argument not a ListStyle' % self.__class__.__name__) 2181 self.style = style 2182 2183 for k,v in ListStyle.defaults.items(): 2184 setattr(self,'_'+k,kwds.get(k,getattr(style,k,v))) 2185 2186 for k in ('spaceBefore','spaceAfter'): 2187 v = kwds.get(k,getattr(style,k,None)) 2188 if v is not None: 2189 setattr(self,k,v) 2190 2191 auto = False 2192 if start is None: 2193 start = getattr(self,'_start',None) 2194 if start is None: 2195 if self._bulletType=='bullet': 2196 start = 'bulletchar' 2197 auto = True 2198 else: 2199 start = self._bulletType 2200 auto = True 2201 if self._bulletType!='bullet': 2202 if auto: 2203 for v in start: 2204 if v not in self._numberStyles: 2205 raise ValueError('invalid start=%r or bullettype=%r' % (start,self._bulletType)) 2206 else: 2207 for v in self._bulletType: 2208 if v not in self._numberStyles: 2209 raise ValueError('invalid bullettype=%r' % self._bulletType) 2210 self._start = start 2211 self._auto = auto or isinstance(start,(list,tuple)) 2212 2213 self._list_content = None 2214 self._dims = None 2215 2216 @property 2217 def _content(self): 2218 if self._list_content is None: 2219 self._list_content = self._getContent() 2220 del self._flowables 2221 return self._list_content 2222 2223 def wrap(self,aW,aH): 2224 if self._dims!=aW: 2225 self.width, self.height = _listWrapOn(self._content,aW,self.canv) 2226 self._dims = aW 2227 return self.width,self.height 2228 2229 def split(self,aW,aH): 2230 return self._content 2231 2232 def _flowablesIter(self): 2233 for f in self._flowables: 2234 if isinstance(f,(list,tuple)): 2235 if f: 2236 for i, z in enumerate(f): 2237 yield i==0 and not isinstance(z,LIIndenter), z 2238 elif isinstance(f,ListItem): 2239 params = f._params 2240 if not params: 2241 #meerkat simples just a list like object 2242 for i, z in enumerate(f._flowables): 2243 if isinstance(z,LIIndenter): 2244 raise ValueError('LIIndenter not allowed in ListItem') 2245 yield i==0, z 2246 else: 2247 params = params.copy() 2248 value = params.pop('value',None) 2249 spaceBefore = params.pop('spaceBefore',None) 2250 spaceAfter = params.pop('spaceAfter',None) 2251 n = len(f._flowables) - 1 2252 for i, z in enumerate(f._flowables): 2253 P = params.copy() 2254 if not i and spaceBefore is not None: 2255 P['spaceBefore'] = spaceBefore 2256 if i==n and spaceAfter is not None: 2257 P['spaceAfter'] = spaceAfter 2258 if i: value=None 2259 yield 0, _LIParams(z,P,value,i==0) 2260 else: 2261 yield not isinstance(f,LIIndenter), f 2262 2263 def _makeLIIndenter(self,flowable, bullet, params=None): 2264 if params: 2265 leftIndent = params.get('leftIndent',self._leftIndent) 2266 rightIndent = params.get('rightIndent',self._rightIndent) 2267 spaceBefore = params.get('spaceBefore',None) 2268 spaceAfter = params.get('spaceAfter',None) 2269 return LIIndenter(flowable,leftIndent,rightIndent,bullet,spaceBefore=spaceBefore,spaceAfter=spaceAfter) 2270 else: 2271 return LIIndenter(flowable,self._leftIndent,self._rightIndent,bullet) 2272 2273 def _makeBullet(self,value,params=None): 2274 if params is None: 2275 def getp(a): 2276 return getattr(self,'_'+a) 2277 else: 2278 style = getattr(params,'style',None) 2279 def getp(a): 2280 if a in params: return params[a] 2281 if style and a in style.__dict__: return getattr(self,a) 2282 return getattr(self,'_'+a) 2283 2284 return BulletDrawer( 2285 value=value, 2286 bulletAlign=getp('bulletAlign'), 2287 bulletType=getp('bulletType'), 2288 bulletColor=getp('bulletColor'), 2289 bulletFontName=getp('bulletFontName'), 2290 bulletFontSize=getp('bulletFontSize'), 2291 bulletOffsetY=getp('bulletOffsetY'), 2292 bulletDedent=getp('calcBulletDedent'), 2293 bulletDir=getp('bulletDir'), 2294 bulletFormat=getp('bulletFormat'), 2295 ) 2296 2297 def _getContent(self): 2298 bt = self._bulletType 2299 value = self._start 2300 if isinstance(value,(list,tuple)): 2301 values = value 2302 value = values[0] 2303 else: 2304 values = [value] 2305 autov = values[0] 2306 inc = int(bt in '1aAiI') 2307 if inc: 2308 try: 2309 value = int(value) 2310 except: 2311 value = 1 2312 2313 bd = self._bulletDedent 2314 if bd=='auto': 2315 align = self._bulletAlign 2316 dir = self._bulletDir 2317 if dir=='ltr' and align=='left': 2318 bd = self._leftIndent 2319 elif align=='right': 2320 bd = self._rightIndent 2321 else: 2322 #we need to work out the maximum width of any of the labels 2323 tvalue = value 2324 maxW = 0 2325 for d,f in self._flowablesIter(): 2326 if d: 2327 maxW = max(maxW,_computeBulletWidth(self,tvalue)) 2328 if inc: tvalue += inc 2329 elif isinstance(f,LIIndenter): 2330 b = f._bullet 2331 if b: 2332 if b.bulletType==bt: 2333 maxW = max(maxW,_computeBulletWidth(b,b.value)) 2334 tvalue = int(b.value) 2335 else: 2336 maxW = max(maxW,_computeBulletWidth(self,tvalue)) 2337 if inc: tvalue += inc 2338 if dir=='ltr': 2339 if align=='right': 2340 bd = self._leftIndent - maxW 2341 else: 2342 bd = self._leftIndent - maxW*0.5 2343 elif align=='left': 2344 bd = self._rightIndent - maxW 2345 else: 2346 bd = self._rightIndent - maxW*0.5 2347 2348 self._calcBulletDedent = bd 2349 2350 S = [] 2351 aS = S.append 2352 i=0 2353 for d,f in self._flowablesIter(): 2354 if isinstance(f,ListFlowable): 2355 fstart = f._start 2356 if isinstance(fstart,(list,tuple)): 2357 fstart = fstart[0] 2358 if fstart in values: 2359 #my kind of ListFlowable 2360 if f._auto: 2361 autov = values.index(autov)+1 2362 f._start = values[autov:]+values[:autov] 2363 autov = f._start[0] 2364 if inc: f._bulletType = autov 2365 else: 2366 autov = fstart 2367 fparams = {} 2368 if not i: 2369 i += 1 2370 spaceBefore = getattr(self,'spaceBefore',None) 2371 if spaceBefore is not None: 2372 fparams['spaceBefore'] = spaceBefore 2373 if d: 2374 aS(self._makeLIIndenter(f,bullet=self._makeBullet(value),params=fparams)) 2375 if inc: value += inc 2376 elif isinstance(f,LIIndenter): 2377 b = f._bullet 2378 if b: 2379 if b.bulletType!=bt: 2380 raise ValueError('Included LIIndenter bulletType=%s != OrderedList bulletType=%s' % (b.bulletType,bt)) 2381 value = int(b.value) 2382 else: 2383 f._bullet = self._makeBullet(value,params=getattr(f,'params',None)) 2384 if fparams: 2385 f.__dict__['spaceBefore'] = max(f.__dict__.get('spaceBefore',0),spaceBefore) 2386 aS(f) 2387 if inc: value += inc 2388 elif isinstance(f,_LIParams): 2389 fparams.update(f.params) 2390 z = self._makeLIIndenter(f.flowable,bullet=None,params=fparams) 2391 if f.first: 2392 if f.value is not None: 2393 value = f.value 2394 if inc: value = int(value) 2395 z._bullet = self._makeBullet(value,f.params) 2396 if inc: value += inc 2397 aS(z) 2398 else: 2399 aS(self._makeLIIndenter(f,bullet=None,params=fparams)) 2400 2401 spaceAfter = getattr(self,'spaceAfter',None) 2402 if spaceAfter is not None: 2403 f=S[-1] 2404 f.__dict__['spaceAfter'] = max(f.__dict__.get('spaceAfter',0),spaceAfter) 2405 return S 2406 2407class TopPadder(Flowable): 2408 '''wrap a single flowable so that its first bit will be 2409 padded to fill out the space so that it appears at the 2410 bottom of its frame''' 2411 def __init__(self,f): 2412 self.__dict__['_TopPadder__f'] = f 2413 2414 def wrap(self,aW,aH): 2415 w,h = self.__f.wrap(aW,aH) 2416 self.__dict__['_TopPadder__dh'] = aH-h 2417 return w,h 2418 2419 def split(self,aW,aH): 2420 S = self.__f.split(aW,aH) 2421 if len(S)>1: 2422 S[0] = TopPadder(S[0]) 2423 return S 2424 2425 def drawOn(self, canvas, x, y, _sW=0): 2426 self.__f.drawOn(canvas,x,y-max(0,self.__dh-1e-8),_sW) 2427 2428 def __setattr__(self,a,v): 2429 setattr(self.__f,a,v) 2430 2431 def __getattr__(self,a): 2432 return getattr(self.__f,a) 2433 2434 def __delattr__(self,a): 2435 delattr(self.__f,a) 2436 2437class DocAssign(NullDraw): 2438 '''At wrap time this flowable evaluates var=expr in the doctemplate namespace''' 2439 _ZEROSIZE=1 2440 def __init__(self,var,expr,life='forever'): 2441 Flowable.__init__(self) 2442 self.args = var,expr,life 2443 2444 def funcWrap(self,aW,aH): 2445 NS=self._doctemplateAttr('_nameSpace') 2446 NS.update(dict(availableWidth=aW,availableHeight=aH)) 2447 try: 2448 return self.func() 2449 finally: 2450 for k in 'availableWidth','availableHeight': 2451 try: 2452 del NS[k] 2453 except: 2454 pass 2455 2456 def func(self): 2457 return self._doctemplateAttr('d'+self.__class__.__name__[1:])(*self.args) 2458 2459 def wrap(self,aW,aH): 2460 self.funcWrap(aW,aH) 2461 return 0,0 2462 2463class DocExec(DocAssign): 2464 '''at wrap time exec stmt in doc._nameSpace''' 2465 def __init__(self,stmt,lifetime='forever'): 2466 Flowable.__init__(self) 2467 self.args=stmt,lifetime 2468 2469class DocPara(DocAssign): 2470 '''at wrap time create a paragraph with the value of expr as text 2471 if format is specified it should use %(__expr__)s for string interpolation 2472 of the expression expr (if any). It may also use %(name)s interpolations 2473 for other variables in the namespace. 2474 suitable defaults will be used if style and klass are None 2475 ''' 2476 def __init__(self,expr,format=None,style=None,klass=None,escape=True): 2477 Flowable.__init__(self) 2478 self.expr=expr 2479 self.format=format 2480 self.style=style 2481 self.klass=klass 2482 self.escape=escape 2483 2484 def func(self): 2485 expr = self.expr 2486 if expr: 2487 if not isinstance(expr,str): expr = str(expr) 2488 return self._doctemplateAttr('docEval')(expr) 2489 2490 def add_content(self,*args): 2491 self._doctemplateAttr('frame').add_generated_content(*args) 2492 2493 def get_value(self,aW,aH): 2494 value = self.funcWrap(aW,aH) 2495 if self.format: 2496 NS=self._doctemplateAttr('_nameSpace').copy() 2497 NS.update(dict(availableWidth=aW,availableHeight=aH)) 2498 NS['__expr__'] = value 2499 value = self.format % NS 2500 else: 2501 value = str(value) 2502 return value 2503 2504 def wrap(self,aW,aH): 2505 value = self.get_value(aW,aH) 2506 P = self.klass 2507 if not P: 2508 from reportlab.platypus.paragraph import Paragraph as P 2509 style = self.style 2510 if not style: 2511 from reportlab.lib.styles import getSampleStyleSheet 2512 style=getSampleStyleSheet()['Code'] 2513 if self.escape: 2514 from xml.sax.saxutils import escape 2515 value=escape(value) 2516 self.add_content(P(value,style=style)) 2517 return 0,0 2518 2519class DocAssert(DocPara): 2520 def __init__(self,cond,format=None): 2521 Flowable.__init__(self) 2522 self.expr=cond 2523 self.format=format 2524 2525 def funcWrap(self,aW,aH): 2526 self._cond = DocPara.funcWrap(self,aW,aH) 2527 return self._cond 2528 2529 def wrap(self,aW,aH): 2530 value = self.get_value(aW,aH) 2531 if not bool(self._cond): 2532 raise AssertionError(value) 2533 return 0,0 2534 2535class DocIf(DocPara): 2536 def __init__(self,cond,thenBlock,elseBlock=[]): 2537 Flowable.__init__(self) 2538 self.expr = cond 2539 self.blocks = elseBlock or [],thenBlock 2540 2541 def checkBlock(self,block): 2542 if not isinstance(block,(list,tuple)): 2543 block = (block,) 2544 return block 2545 2546 def wrap(self,aW,aH): 2547 self.add_content(*self.checkBlock(self.blocks[int(bool(self.funcWrap(aW,aH)))])) 2548 return 0,0 2549 2550class DocWhile(DocIf): 2551 def __init__(self,cond,whileBlock): 2552 Flowable.__init__(self) 2553 self.expr = cond 2554 self.block = self.checkBlock(whileBlock) 2555 2556 def wrap(self,aW,aH): 2557 if bool(self.funcWrap(aW,aH)): 2558 self.add_content(*(list(self.block)+[self])) 2559 return 0,0 2560 2561class SetTopFlowables(NullDraw): 2562 _ZEROZSIZE = 1 2563 def __init__(self,F,show=False): 2564 self._F = F 2565 self._show = show 2566 2567 def wrap(self,aW,aH): 2568 doc = getattr(getattr(self,'canv',None),'_doctemplate',None) 2569 if doc: 2570 doc._topFlowables = self._F 2571 if self._show and self._F: 2572 doc.frame._generated_content = self._F 2573 return 0,0 2574 2575class SetPageTopFlowables(NullDraw): 2576 _ZEROZSIZE = 1 2577 def __init__(self,F,show=False): 2578 self._F = F 2579 self._show = show 2580 2581 def wrap(self,aW,aH): 2582 doc = getattr(getattr(self,'canv',None),'_doctemplate',None) 2583 if doc: 2584 doc._pageTopFlowables = self._F 2585 if self._show and self._F: 2586 doc.frame._generated_content = self._F 2587 return 0,0 2588