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/tables.py 4__all__= ( 5 'Table', 6 'TableStyle', 7 'CellStyle', 8 'LongTable', 9 ) 10__version__='3.5.21' 11 12__doc__=""" 13Tables are created by passing the constructor a tuple of column widths, a tuple of row heights and the data in 14row order. Drawing of the table can be controlled by using a TableStyle instance. This allows control of the 15color and weight of the lines (if any), and the font, alignment and padding of the text. 16 17None values in the sequence of row heights or column widths, mean that the corresponding rows 18or columns should be automatically sized. 19 20All the cell values should be convertible to strings; embedded newline '\\n' characters 21cause the value to wrap (ie are like a traditional linefeed). 22 23See the test output from running this module as a script for a discussion of the method for constructing 24tables and table styles. 25""" 26from reportlab.platypus.flowables import Flowable, Preformatted 27from reportlab import rl_config, xrange, ascii 28from reportlab.lib.styles import PropertySet, ParagraphStyle, _baseFontName 29from reportlab.lib import colors 30from reportlab.lib.utils import annotateException, IdentStr, flatten, isStr, asNative, strTypes, __UNSET__ 31from reportlab.lib.validators import isListOfNumbersOrNone 32from reportlab.lib.rl_accel import fp_str 33from reportlab.lib.abag import ABag as CellFrame 34from reportlab.pdfbase.pdfmetrics import stringWidth 35from reportlab.platypus.doctemplate import Indenter, NullActionFlowable 36from reportlab.platypus.flowables import LIIndenter 37from collections import namedtuple 38 39LINECAPS={None: None, 'butt':0,'round':1,'projecting':2,'squared':2} 40LINEJOINS={None: None, 'miter':0, 'mitre':0, 'round':1,'bevel':2} 41 42class CellStyle(PropertySet): 43 fontname = _baseFontName 44 fontsize = 10 45 leading = 12 46 leftPadding = 6 47 rightPadding = 6 48 topPadding = 3 49 bottomPadding = 3 50 firstLineIndent = 0 51 color = 'black' 52 alignment = 'LEFT' 53 background = 'white' 54 valign = "BOTTOM" 55 href = None 56 destination = None 57 def __init__(self, name, parent=None): 58 self.name = name 59 if parent is not None: 60 parent.copy(self) 61 def copy(self, result=None): 62 if result is None: 63 result = CellStyle() 64 for name in dir(self): 65 setattr(result, name, getattr(self, name)) 66 return result 67 68class TableStyle: 69 def __init__(self, cmds=None, parent=None, **kw): 70 #handle inheritance from parent first. 71 if parent: 72 # copy the parents list at construction time 73 pcmds = parent.getCommands()[:] 74 self._opts = parent._opts 75 for a in ('spaceBefore','spaceAfter'): 76 if hasattr(parent,a): 77 setattr(self,a,getattr(parent,a)) 78 else: 79 pcmds = [] 80 81 self._cmds = pcmds + list(cmds or []) 82 self._opts={} 83 self._opts.update(kw) 84 85 def add(self, *cmd): 86 self._cmds.append(cmd) 87 def __repr__(self): 88 return "TableStyle(\n%s\n) # end TableStyle" % " \n".join(map(repr, self._cmds)) 89 def getCommands(self): 90 return self._cmds 91 92def _rowLen(x): 93 return not isinstance(x,(tuple,list)) and 1 or len(x) 94 95def _calc_pc(V,avail): 96 '''check list V for percentage or * values 97 1) absolute values go through unchanged 98 2) percentages are used as weights for unconsumed space 99 3) if no None values were seen '*' weights are 100 set equally with unclaimed space 101 otherwise * weights are assigned as None''' 102 R = [] 103 r = R.append 104 I = [] 105 i = I.append 106 J = [] 107 j = J.append 108 s = avail 109 w = n = 0. 110 for v in V: 111 if isinstance(v,strTypes): 112 v = str(v).strip() 113 if not v: 114 v = None 115 n += 1 116 elif v.endswith('%'): 117 v = float(v[:-1]) 118 w += v 119 i(len(R)) 120 elif v=='*': 121 j(len(R)) 122 else: 123 v = float(v) 124 s -= v 125 elif v is None: 126 n += 1 127 else: 128 s -= v 129 r(v) 130 s = max(0.,s) 131 f = s/max(100.,w) 132 for i in I: 133 R[i] *= f 134 s -= R[i] 135 s = max(0.,s) 136 m = len(J) 137 if m: 138 v = n==0 and s/m or None 139 for j in J: 140 R[j] = v 141 return R 142 143def _calcBezierPoints(P, kind): 144 '''calculate all or half of a bezier curve 145 kind==0 all, 1=first half else second half''' 146 if kind==0: 147 return P 148 else: 149 Q0 = (0.5*(P[0][0]+P[1][0]),0.5*(P[0][1]+P[1][1])) 150 Q1 = (0.5*(P[1][0]+P[2][0]),0.5*(P[1][1]+P[2][1])) 151 Q2 = (0.5*(P[2][0]+P[3][0]),0.5*(P[2][1]+P[3][1])) 152 R0 = (0.5*(Q0[0]+Q1[0]),0.5*(Q0[1]+Q1[1])) 153 R1 = (0.5*(Q1[0]+Q2[0]),0.5*(Q1[1]+Q2[1])) 154 S0 = (0.5*(R0[0]+R1[0]),0.5*(R0[1]+R1[1])) 155 return [P[0],Q0,R0,S0] if kind==1 else [S0,R1,Q2,P[3]] 156 157def _quadrantDef(xpos, ypos, corner, r, kind=0, direction='left-right', m=0.4472): 158 t = m*r 159 if xpos=='right' and ypos=='bottom': #bottom right 160 xhi,ylo = corner 161 P = [(xhi - r, ylo),(xhi-t, ylo), (xhi, ylo + t), (xhi, ylo + r)] 162 elif xpos=='right' and ypos=='top': #top right 163 xhi,yhi = corner 164 P = [(xhi, yhi - r),(xhi, yhi - t), (xhi - t, yhi), (xhi - r, yhi)] 165 elif xpos=='left' and ypos=='top': #top left 166 xlo,yhi = corner 167 P = [(xlo + r, yhi),(xlo + t, yhi), (xlo, yhi - t), (xlo, yhi - r)] 168 elif xpos=='left' and ypos=='bottom': #bottom left 169 xlo,ylo = corner 170 P = [(xlo, ylo + r),(xlo, ylo + t), (xlo + t, ylo), (xlo + r, ylo)] 171 else: 172 raise ValueError('Unknown quadrant position %s' % repr((xpos,ypos))) 173 if direction=='left-right' and P[0][0]>P[-1][0] or direction=='bottom-top' and P[0][1]>P[-1][1]: 174 P.reverse() 175 P = _calcBezierPoints(P, kind) 176 return P 177 178def _hLine(canvLine, scp, ecp, y, hBlocks, FUZZ=rl_config._FUZZ): 179 ''' 180 Draw horizontal lines; do not draw through regions specified in hBlocks 181 This also serves for vertical lines with a suitable canvLine 182 ''' 183 if hBlocks: hBlocks = hBlocks.get(y,None) 184 if not hBlocks or scp>=hBlocks[-1][1]-FUZZ or ecp<=hBlocks[0][0]+FUZZ: 185 canvLine(scp,y,ecp,y) 186 else: 187 i = 0 188 n = len(hBlocks) 189 while scp<ecp-FUZZ and i<n: 190 x0, x1 = hBlocks[i] 191 if x1<=scp+FUZZ or x0>=ecp-FUZZ: 192 i += 1 193 continue 194 i0 = max(scp,x0) 195 i1 = min(ecp,x1) 196 if i0>scp: canvLine(scp,y,i0,y) 197 scp = i1 198 if scp<ecp-FUZZ: canvLine(scp,y,ecp,y) 199 200def _multiLine(scp,ecp,y,canvLine,ws,count): 201 offset = 0.5*(count-1)*ws 202 y += offset 203 for idx in xrange(count): 204 canvLine(scp, y, ecp, y) 205 y -= ws 206 207def _convert2int(value, map, low, high, name, cmd): 208 '''private converter tries map(value) low<=int(value)<=high or finally an error''' 209 try: 210 return map[value] 211 except KeyError: 212 try: 213 ivalue = int(value) 214 if low<=ivalue<=high: return ivalue 215 except: 216 pass 217 raise ValueError('Bad %s value %s in %s'%(name,value,ascii(cmd))) 218 219def _endswith(obj,s): 220 try: 221 return obj.endswith(s) 222 except: 223 return 0 224 225def spanFixDim(V0,V,spanCons,lim=None,FUZZ=rl_config._FUZZ): 226 #assign required space to variable rows equally to existing calculated values 227 M = {} 228 if not lim: lim = len(V0) #in longtables the row calcs may be truncated 229 230 #we assign the largest spaces first hoping to get a smaller result 231 for v,(x0,x1) in reversed(sorted(((iv,ik) for ik,iv in spanCons.items()))): 232 if x0>=lim: continue 233 x1 += 1 234 t = sum([V[x]+M.get(x,0) for x in xrange(x0,x1)]) 235 if t>=v-FUZZ: continue #already good enough 236 X = [x for x in xrange(x0,x1) if V0[x] is None] #variable candidates 237 if not X: continue #something wrong here mate 238 v -= t 239 v /= float(len(X)) 240 for x in X: 241 M[x] = M.get(x,0)+v 242 for x,v in M.items(): 243 V[x] += v 244 245class _ExpandedCellTuple(tuple): 246 pass 247 248 249RoundingRectDef = namedtuple('RoundingRectDefs','x0 y0 w h x1 y1 ar SL') 250RoundingRectLine = namedtuple('RoundingRectLine','xs ys xe ye weight color cap dash join') 251 252class Table(Flowable): 253 def __init__(self, data, colWidths=None, rowHeights=None, style=None, 254 repeatRows=0, repeatCols=0, splitByRow=1, emptyTableAction=None, ident=None, 255 hAlign=None,vAlign=None, normalizedData=0, cellStyles=None, rowSplitRange=None, 256 spaceBefore=None,spaceAfter=None, longTableOptimize=None, minRowHeights=None, 257 cornerRadii=__UNSET__, #or [topLeft, topRight, bottomLeft bottomRight] 258 ): 259 self.ident = ident 260 self.hAlign = hAlign or 'CENTER' 261 self.vAlign = vAlign or 'MIDDLE' 262 if not isinstance(data,(tuple,list)): 263 raise ValueError("%s invalid data type" % self.identity()) 264 self._nrows = nrows = len(data) 265 self._cellvalues = [] 266 _seqCW = isinstance(colWidths,(tuple,list)) 267 _seqRH = isinstance(rowHeights,(tuple,list)) 268 if nrows: self._ncols = ncols = max(list(map(_rowLen,data))) 269 elif colWidths and _seqCW: ncols = len(colWidths) 270 else: ncols = 0 271 if not emptyTableAction: emptyTableAction = rl_config.emptyTableAction 272 self._longTableOptimize = (getattr(self,'_longTableOptimize',rl_config.longTableOptimize) 273 if longTableOptimize is None else longTableOptimize) 274 if not (nrows and ncols): 275 if emptyTableAction=='error': 276 raise ValueError("%s must have at least a row and column" % self.identity()) 277 elif emptyTableAction=='indicate': 278 self.__class__ = Preformatted 279 global _emptyTableStyle 280 if '_emptyTableStyle' not in list(globals().keys()): 281 _emptyTableStyle = ParagraphStyle('_emptyTableStyle') 282 _emptyTableStyle.textColor = colors.red 283 _emptyTableStyle.backColor = colors.yellow 284 Preformatted.__init__(self,'%s(%d,%d)' % (self.__class__.__name__,nrows,ncols), _emptyTableStyle) 285 elif emptyTableAction=='ignore': 286 self.__class__ = NullActionFlowable 287 else: 288 raise ValueError('%s bad emptyTableAction: "%s"' % (self.identity(),emptyTableAction)) 289 return 290 291 # we need a cleanup pass to ensure data is strings - non-unicode and non-null 292 if normalizedData: 293 self._cellvalues = data 294 else: 295 self._cellvalues = data = self.normalizeData(data) 296 if not _seqCW: colWidths = ncols*[colWidths] 297 elif len(colWidths)!=ncols: 298 if rl_config.allowShortTableRows and isinstance(colWidths,list): 299 n = len(colWidths) 300 if n<ncols: 301 colWidths[n:] = (ncols-n)*[colWidths[-1]] 302 else: 303 colWidths = colWidths[:ncols] 304 else: 305 raise ValueError("%s data error - %d columns in data but %d in column widths" % (self.identity(),ncols, len(colWidths))) 306 if not _seqRH: rowHeights = nrows*[rowHeights] 307 elif len(rowHeights) != nrows: 308 raise ValueError("%s data error - %d rows in data but %d in row heights" % (self.identity(),nrows, len(rowHeights))) 309 for i,d in enumerate(data): 310 n = len(d) 311 if n!=ncols: 312 if rl_config.allowShortTableRows and isinstance(d,list): 313 d[n:] = (ncols-n)*[''] 314 else: 315 raise ValueError("%s expected %d not %d columns in row %d!" % (self.identity(),ncols,n,i)) 316 self._rowHeights = self._argH = rowHeights 317 self._colWidths = self._argW = colWidths 318 if cellStyles is None: 319 cellrows = [] 320 for i in xrange(nrows): 321 cellcols = [] 322 for j in xrange(ncols): 323 cellcols.append(CellStyle(repr((i,j)))) 324 cellrows.append(cellcols) 325 self._cellStyles = cellrows 326 else: 327 self._cellStyles = cellStyles 328 329 self._bkgrndcmds = [] 330 self._linecmds = [] 331 self._spanCmds = [] 332 self._nosplitCmds = [] 333 self._srflcmds = [] 334 # NB repeatRows can be a list or tuple eg (1,) repeats only the second row of a table 335 # or an integer eg 2 to repeat both rows 0 & 1 336 self.repeatRows = repeatRows 337 self.repeatCols = repeatCols 338 self.splitByRow = splitByRow 339 340 if style: 341 self.setStyle(style) 342 343 if cornerRadii is not __UNSET__: #instance argument overrides 344 self._setCornerRadii(cornerRadii) 345 346 self._rowSplitRange = rowSplitRange 347 if spaceBefore is not None: 348 self.spaceBefore = spaceBefore 349 if spaceAfter is not None: 350 self.spaceAfter = spaceAfter 351 352 if minRowHeights != None: 353 lmrh = len(minRowHeights) 354 if not lmrh: 355 raise ValueError("%s Supplied mismatching minimum row heights of length %d" % (self.identity(),lmrh)) 356 elif lmrh<nrows: 357 minRowHeights = minRowHeights+(nrows-lmrh)*minRowHeights.__class__((0,)) 358 self._minRowHeights = minRowHeights 359 360 361 def __repr__(self): 362 "incomplete, but better than nothing" 363 r = getattr(self,'_rowHeights','[unknown]') 364 c = getattr(self,'_colWidths','[unknown]') 365 cv = getattr(self,'_cellvalues','[unknown]') 366 import pprint 367 cv = pprint.pformat(cv) 368 cv = cv.replace("\n", "\n ") 369 return "%s(\n rowHeights=%s,\n colWidths=%s,\n%s\n) # end table" % (self.__class__.__name__,r,c,cv) 370 371 def normalizeData(self, data): 372 """Takes a block of input data (list of lists etc.) and 373 - coerces unicode strings to non-unicode UTF8 374 - coerces nulls to '' 375 - 376 377 """ 378 def normCell(stuff): 379 if stuff is None: 380 return '' 381 elif isStr(stuff): 382 return asNative(stuff) 383 else: 384 return stuff 385 outData = [] 386 for row in data: 387 outRow = [normCell(cell) for cell in row] 388 outData.append(outRow) 389 return outData 390 391 def identity(self, maxLen=30): 392 '''Identify our selves as well as possible''' 393 if self.ident: return self.ident 394 vx = None 395 nr = getattr(self,'_nrows','unknown') 396 nc = getattr(self,'_ncols','unknown') 397 cv = getattr(self,'_cellvalues',None) 398 rh = getattr(self, '_rowHeights', None) 399 if cv and 'unknown' not in (nr,nc): 400 b = 0 401 for i in xrange(nr): 402 for j in xrange(nc): 403 v = cv[i][j] 404 if isinstance(v,(list,tuple,Flowable)): 405 if not isinstance(v,(tuple,list)): v = (v,) 406 r = '' 407 for vij in v: 408 r = vij.identity(maxLen) 409 if r and r[-4:]!='>...': 410 break 411 if r and r[-4:]!='>...': 412 ix, jx, vx, b = i, j, r, 1 413 else: 414 v = v is None and '' or str(v) 415 ix, jx, vx = i, j, v 416 b = (vx and isinstance(v,strTypes)) and 1 or 0 417 if maxLen: vx = vx[:maxLen] 418 if b: break 419 if b: break 420 if rh: #find tallest row, it's of great interest' 421 tallest = '(tallest row %d)' % int(max(rh)) 422 else: 423 tallest = '' 424 if vx: 425 vx = ' with cell(%d,%d) containing\n%s' % (ix,jx,repr(vx)) 426 else: 427 vx = '...' 428 429 return "<%s@0x%8.8X %s rows x %s cols%s>%s" % (self.__class__.__name__, id(self), nr, nc, tallest, vx) 430 431 def _cellListIter(self,C,aW,aH): 432 canv = getattr(self,'canv',None) 433 for c in C: 434 if getattr(c,'__split_only__',None): 435 for d in c.splitOn(canv,aW,aH): 436 yield d 437 else: 438 yield c 439 440 def _cellListProcess(self,C,aW,aH): 441 if not isinstance(C,_ExpandedCellTuple): 442 frame = None 443 R = [].append 444 for c in self._cellListIter(C,aW,aH): 445 if isinstance(c,Indenter): 446 if not frame: 447 frame = CellFrame(_leftExtraIndent=0,_rightExtraIndent=0) 448 c.frameAction(frame) 449 if frame._leftExtraIndent<1e-8 and frame._rightExtraIndent<1e-8: 450 frame = None 451 continue 452 if frame: 453 R(LIIndenter(c,leftIndent=frame._leftExtraIndent,rightIndent=frame._rightExtraIndent)) 454 else: 455 R(c) 456 C = _ExpandedCellTuple(R.__self__) 457 return C 458 459 def _listCellGeom(self, V,w,s,W=None,H=None,aH=72000): 460 if not V: return 0,0 461 aW = w - s.leftPadding - s.rightPadding 462 aH = aH - s.topPadding - s.bottomPadding 463 t = 0 464 w = 0 465 canv = getattr(self,'canv',None) 466 sb0 = None 467 for v in V: 468 vw, vh = v.wrapOn(canv, aW, aH) 469 sb = v.getSpaceBefore() 470 sa = v.getSpaceAfter() 471 if W is not None: W.append(vw) 472 if H is not None: H.append(vh) 473 w = max(w,vw) 474 t += vh + sa + sb 475 if sb0 is None: 476 sb0 = sb 477 return w, t - sb0 - sa 478 479 def _listValueWidth(self,V,aH=72000,aW=72000): 480 if not V: return 0,0 481 t = 0 482 w = 0 483 canv = getattr(self,'canv',None) 484 return max([v.wrapOn(canv,aW,aH)[0] for v in V]) 485 486 def _calc_width(self,availWidth,W=None): 487 if getattr(self,'_width_calculated_once',None): return 488 #comments added by Andy to Robin's slightly terse variable names 489 if not W: W = _calc_pc(self._argW,availWidth) #widths array 490 if None in W: #some column widths are not given 491 canv = getattr(self,'canv',None) 492 saved = None 493 if self._spanCmds: 494 colSpanCells = self._colSpanCells 495 spanRanges = self._spanRanges 496 else: 497 colSpanCells = () 498 spanRanges = {} 499 spanCons = {} 500 if W is self._argW: 501 W0 = W 502 W = W[:] 503 else: 504 W0 = W[:] 505 V = self._cellvalues 506 S = self._cellStyles 507 while None in W: 508 j = W.index(None) #find first unspecified column 509 w = 0 510 for i,Vi in enumerate(V): 511 v = Vi[j] 512 s = S[i][j] 513 ji = j,i 514 span = spanRanges.get(ji,None) 515 if ji in colSpanCells and not span: #if the current cell is part of a spanned region, 516 t = 0.0 #assume a zero size. 517 else:#work out size 518 t = self._elementWidth(v,s) 519 if t is None: 520 raise ValueError("Flowable %s in cell(%d,%d) can't have auto width\n%s" % (v.identity(30),i,j,self.identity(30))) 521 t += s.leftPadding+s.rightPadding 522 if span: 523 c0 = span[0] 524 c1 = span[2] 525 if c0!=c1: 526 x = c0,c1 527 spanCons[x] = max(spanCons.get(x,t),t) 528 t = 0 529 if t>w: w = t #record a new maximum 530 531 W[j] = w 532 533 if spanCons: 534 try: 535 spanFixDim(W0,W,spanCons) 536 except: 537 annotateException('\nspanning problem in %s\nW0=%r W=%r\nspanCons=%r' % (self.identity(),W0,W,spanCons)) 538 539 self._colWidths = W 540 width = 0 541 self._colpositions = [0] #index -1 is right side boundary; we skip when processing cells 542 for w in W: 543 width = width + w 544 self._colpositions.append(width) 545 546 self._width = width 547 self._width_calculated_once = 1 548 549 def _elementWidth(self,v,s): 550 if isinstance(v,(list,tuple)): 551 w = 0 552 for e in v: 553 ew = self._elementWidth(e,s) 554 if ew is None: return None 555 w = max(w,ew) 556 return w 557 elif isinstance(v,Flowable): 558 if v._fixedWidth: 559 if hasattr(v, 'width') and isinstance(v.width,(int,float)): return v.width 560 if hasattr(v, 'drawWidth') and isinstance(v.drawWidth,(int,float)): return v.drawWidth 561 if hasattr(v,'__styledWrap__'): #very experimental 562 try: 563 return getattr(v,'__styledWrap__')(s)[0] 564 except: 565 pass 566 # Even if something is fixedWidth, the attribute to check is not 567 # necessarily consistent (cf. Image.drawWidth). Therefore, we'll 568 # be extra-careful and fall through to this code if necessary. 569 if hasattr(v, 'minWidth'): 570 try: 571 w = v.minWidth() # should be all flowables 572 if isinstance(w,(float,int)): return w 573 except AttributeError: 574 pass 575 if v is None: 576 return 0 577 else: 578 try: 579 v = str(v).split("\n") 580 except: 581 return 0 582 fontName = s.fontname 583 fontSize = s.fontsize 584 return max([stringWidth(x,fontName,fontSize) for x in v]) 585 586 def _calc_height(self, availHeight, availWidth, H=None, W=None): 587 H = self._argH 588 if not W: W = _calc_pc(self._argW,availWidth) #widths array 589 590 hmax = lim = len(H) 591 longTable = self._longTableOptimize 592 593 if None in H: 594 minRowHeights = self._minRowHeights 595 canv = getattr(self,'canv',None) 596 saved = None 597 #get a handy list of any cells which span rows. should be ignored for sizing 598 if self._spanCmds: 599 rowSpanCells = self._rowSpanCells 600 colSpanCells = self._colSpanCells 601 spanRanges = self._spanRanges 602 colpositions = self._colpositions 603 else: 604 rowSpanCells = colSpanCells = () 605 spanRanges = {} 606 if canv: saved = canv._fontname, canv._fontsize, canv._leading 607 H0 = H 608 H = H[:] #make a copy as we'll change it 609 self._rowHeights = H 610 spanCons = {} 611 FUZZ = rl_config._FUZZ 612 while None in H: 613 i = H.index(None) 614 V = self._cellvalues[i] # values for row i 615 S = self._cellStyles[i] # styles for row i 616 h = 0 617 j = 0 618 for j,(v, s, w) in enumerate(list(zip(V, S, W))): # value, style, width (lengths must match) 619 ji = j,i 620 span = spanRanges.get(ji,None) 621 if ji in rowSpanCells and not span: 622 continue # don't count it, it's either occluded or unreliable 623 else: 624 if isinstance(v,(tuple,list,Flowable)): 625 if isinstance(v,Flowable): v = (v,) 626 else: v = flatten(v) 627 v = V[j] = self._cellListProcess(v,w,None) 628 if w is None and not self._canGetWidth(v): 629 raise ValueError("Flowable %s in cell(%d,%d) can't have auto width in\n%s" % (v[0].identity(30),i,j,self.identity(30))) 630 if canv: canv._fontname, canv._fontsize, canv._leading = s.fontname, s.fontsize, s.leading or 1.2*s.fontsize 631 if ji in colSpanCells: 632 if not span: continue 633 w = max(colpositions[span[2]+1]-colpositions[span[0]],w or 0) 634 dW,t = self._listCellGeom(v,w or self._listValueWidth(v),s) 635 if canv: canv._fontname, canv._fontsize, canv._leading = saved 636 dW = dW + s.leftPadding + s.rightPadding 637 if not rl_config.allowTableBoundsErrors and dW>w: 638 from reportlab.platypus.doctemplate import LayoutError 639 raise LayoutError("Flowable %s (%sx%s points) too wide for cell(%d,%d) (%sx* points) in\n%s" % (v[0].identity(30),fp_str(dW),fp_str(t),i,j, fp_str(w), self.identity(30))) 640 else: 641 v = (v is not None and str(v) or '').split("\n") 642 t = (s.leading or 1.2*s.fontsize)*len(v) 643 t += s.bottomPadding+s.topPadding 644 if span: 645 r0 = span[1] 646 r1 = span[3] 647 if r0!=r1: 648 x = r0,r1 649 spanCons[x] = max(spanCons.get(x,t),t) 650 t = 0 651 if t>h: h = t #record a new maximum 652 # If a minimum height has been specified use that, otherwise allow the cell to grow 653 H[i] = max(minRowHeights[i],h) if minRowHeights else h 654 # we can stop if we have filled up all available room 655 if longTable: 656 hmax = i+1 #we computed H[i] so known len == i+1 657 height = sum(H[:hmax]) 658 if height > availHeight: 659 #we can terminate if all spans are complete in H[:hmax] 660 if spanCons: 661 msr = max(x[1] for x in spanCons.keys()) #RS=[endrowspan,.....] 662 if hmax>msr: 663 break 664 if None not in H: hmax = lim 665 666 if spanCons: 667 try: 668 spanFixDim(H0,H,spanCons) 669 except: 670 annotateException('\nspanning problem in %s hmax=%s lim=%s avail=%s x %s\nH0=%r H=%r\nspanCons=%r' % (self.identity(),hmax,lim,availWidth,availHeight,H0,H,spanCons)) 671 672 #iterate backwards through the heights to get rowpositions in reversed order 673 self._rowpositions = j = [] 674 height = c = 0 675 for i in xrange(hmax-1,-1,-1): 676 j.append(height) 677 y = H[i] - c 678 t = height + y 679 c = (t - height) - y 680 height = t 681 j.append(height) 682 self._height = height 683 j.reverse() #reverse the reversed list of row positions 684 self._hmax = hmax 685 686 def _calc(self, availWidth, availHeight): 687 #if hasattr(self,'_width'): return 688 689 #in some cases there are unsizable things in 690 #cells. If so, apply a different algorithm 691 #and assign some withs in a less (thanks to Gary Poster) dumb way. 692 #this CHANGES the widths array. 693 if (None in self._colWidths or '*' in self._colWidths) and self._hasVariWidthElements(): 694 W = self._calcPreliminaryWidths(availWidth) #widths 695 else: 696 W = None 697 698 # need to know which cells are part of spanned 699 # ranges, so _calc_height and _calc_width can ignore them 700 # in sizing 701 if self._spanCmds: 702 self._calcSpanRanges() 703 if None in self._argH: 704 self._calc_width(availWidth,W=W) 705 706 if self._nosplitCmds: 707 self._calcNoSplitRanges() 708 709 # calculate the full table height 710 self._calc_height(availHeight,availWidth,W=W) 711 712 # calculate the full table width 713 self._calc_width(availWidth,W=W) 714 715 if self._spanCmds: 716 #now work out the actual rect for each spanned cell from the underlying grid 717 self._calcSpanRects() 718 719 def _culprit(self): 720 """Return a string describing the tallest element. 721 722 Usually this is what causes tables to fail to split. Currently 723 tables are the only items to have a '_culprit' method. Doctemplate 724 checks for it. 725 """ 726 rh = self._rowHeights 727 tallest = max(rh) 728 rowNum = rh.index(tallest) 729 #rowNum of limited interest as usually it's a split one 730 #and we see row #1. Text might be a nice addition. 731 732 return 'tallest cell %0.1f points' % tallest 733 734 735 736 def _hasVariWidthElements(self, upToRow=None): 737 """Check for flowables in table cells and warn up front. 738 739 Allow a couple which we know are fixed size such as 740 images and graphics.""" 741 if upToRow is None: upToRow = self._nrows 742 for row in xrange(min(self._nrows, upToRow)): 743 for col in xrange(self._ncols): 744 value = self._cellvalues[row][col] 745 if not self._canGetWidth(value): 746 return 1 747 return 0 748 749 def _canGetWidth(self, thing): 750 "Can we work out the width quickly?" 751 if isinstance(thing,(list, tuple)): 752 for elem in thing: 753 if not self._canGetWidth(elem): 754 return 0 755 return 1 756 elif isinstance(thing, Flowable): 757 return thing._fixedWidth # must loosen this up 758 else: #str, number, None etc. 759 #anything else gets passed to str(...) 760 # so should be sizable 761 return 1 762 763 def _calcPreliminaryWidths(self, availWidth): 764 """Fallback algorithm for when main one fails. 765 766 Where exact width info not given but things like 767 paragraphs might be present, do a preliminary scan 768 and assign some best-guess values.""" 769 770 W = list(self._argW) # _calc_pc(self._argW,availWidth) 771 #verbose = 1 772 totalDefined = 0.0 773 percentDefined = 0 774 percentTotal = 0 775 numberUndefined = 0 776 numberGreedyUndefined = 0 777 for w in W: 778 if w is None: 779 numberUndefined += 1 780 elif w == '*': 781 numberUndefined += 1 782 numberGreedyUndefined += 1 783 elif _endswith(w,'%'): 784 percentDefined += 1 785 percentTotal += float(w[:-1]) 786 else: 787 assert isinstance(w,(int,float)) 788 totalDefined = totalDefined + w 789 #if verbose: print('prelim width calculation. %d columns, %d undefined width, %0.2f units remain' % (self._ncols, numberUndefined, availWidth - totalDefined)) 790 791 #check columnwise in each None column to see if they are sizable. 792 given = [] 793 sizeable = [] 794 unsizeable = [] 795 minimums = {} 796 totalMinimum = 0 797 elementWidth = self._elementWidth 798 for colNo in xrange(self._ncols): 799 w = W[colNo] 800 if w is None or w=='*' or _endswith(w,'%'): 801 siz = 1 802 final = 0 803 for rowNo in xrange(self._nrows): 804 value = self._cellvalues[rowNo][colNo] 805 style = self._cellStyles[rowNo][colNo] 806 new = elementWidth(value,style) or 0 807 new += style.leftPadding+style.rightPadding 808 #if verbose: print('[%d,%d] new=%r-->%r' % (rowNo,colNo,new - style.leftPadding+style.rightPadding, new)) 809 final = max(final, new) 810 siz = siz and self._canGetWidth(value) # irrelevant now? 811 if siz: 812 sizeable.append(colNo) 813 else: 814 unsizeable.append(colNo) 815 minimums[colNo] = final 816 totalMinimum += final 817 else: 818 given.append(colNo) 819 if len(given) == self._ncols: 820 return 821 #if verbose: print('predefined width: ',given) 822 #if verbose: print('uncomputable width: ',unsizeable) 823 #if verbose: print('computable width: ',sizeable) 824 #if verbose: print('minimums=%r' % (list(sorted(list(minimums.items()))),)) 825 826 # how much width is left: 827 remaining = availWidth - (totalMinimum + totalDefined) 828 if remaining > 0: 829 # we have some room left; fill it. 830 definedPercentage = (totalDefined/float(availWidth))*100 831 percentTotal += definedPercentage 832 if numberUndefined and percentTotal < 100: 833 undefined = numberGreedyUndefined or numberUndefined 834 defaultWeight = (100-percentTotal)/float(undefined) 835 percentTotal = 100 836 defaultDesired = (defaultWeight/float(percentTotal))*availWidth 837 else: 838 defaultWeight = defaultDesired = 1 839 # we now calculate how wide each column wanted to be, and then 840 # proportionately shrink that down to fit the remaining available 841 # space. A column may not shrink less than its minimum width, 842 # however, which makes this a bit more complicated. 843 desiredWidths = [] 844 totalDesired = 0 845 effectiveRemaining = remaining 846 for colNo, minimum in minimums.items(): 847 w = W[colNo] 848 if _endswith(w,'%'): 849 desired = (float(w[:-1])/percentTotal)*availWidth 850 elif w == '*': 851 desired = defaultDesired 852 else: 853 desired = not numberGreedyUndefined and defaultDesired or 1 854 if desired <= minimum: 855 W[colNo] = minimum 856 else: 857 desiredWidths.append( 858 (desired-minimum, minimum, desired, colNo)) 859 totalDesired += desired 860 effectiveRemaining += minimum 861 if desiredWidths: # else we're done 862 # let's say we have two variable columns. One wanted 863 # 88 points, and one wanted 264 points. The first has a 864 # minWidth of 66, and the second of 55. We have 71 points 865 # to divide up in addition to the totalMinimum (i.e., 866 # remaining==71). Our algorithm tries to keep the proportion 867 # of these variable columns. 868 # 869 # To do this, we add up the minimum widths of the variable 870 # columns and the remaining width. That's 192. We add up the 871 # totalDesired width. That's 352. That means we'll try to 872 # shrink the widths by a proportion of 192/352--.545454. 873 # That would make the first column 48 points, and the second 874 # 144 points--adding up to the desired 192. 875 # 876 # Unfortunately, that's too small for the first column. It 877 # must be 66 points. Therefore, we go ahead and save that 878 # column width as 88 points. That leaves (192-88==) 104 879 # points remaining. The proportion to shrink the remaining 880 # column is (104/264), which, multiplied by the desired 881 # width of 264, is 104: the amount assigned to the remaining 882 # column. 883 proportion = effectiveRemaining/float(totalDesired) 884 # we sort the desired widths by difference between desired and 885 # and minimum values, a value called "disappointment" in the 886 # code. This means that the columns with a bigger 887 # disappointment will have a better chance of getting more of 888 # the available space. 889 desiredWidths.sort() 890 finalSet = [] 891 for disappointment, minimum, desired, colNo in desiredWidths: 892 adjusted = proportion * desired 893 if adjusted < minimum: 894 W[colNo] = minimum 895 totalDesired -= desired 896 effectiveRemaining -= minimum 897 if totalDesired: 898 proportion = effectiveRemaining/float(totalDesired) 899 else: 900 finalSet.append((minimum, desired, colNo)) 901 for minimum, desired, colNo in finalSet: 902 adjusted = proportion * desired 903 assert adjusted >= minimum 904 W[colNo] = adjusted 905 else: 906 for colNo, minimum in minimums.items(): 907 W[colNo] = minimum 908 #if verbose: print('new widths are:', W) 909 self._argW = self._colWidths = W 910 return W 911 912 def minWidth(self): 913 W = list(self._argW) 914 width = 0 915 elementWidth = self._elementWidth 916 rowNos = xrange(self._nrows) 917 values = self._cellvalues 918 styles = self._cellStyles 919 for colNo in xrange(len(W)): 920 w = W[colNo] 921 if w is None or w=='*' or _endswith(w,'%'): 922 final = 0 923 for rowNo in rowNos: 924 value = values[rowNo][colNo] 925 style = styles[rowNo][colNo] 926 new = (elementWidth(value,style)+ 927 style.leftPadding+style.rightPadding) 928 final = max(final, new) 929 width += final 930 else: 931 width += float(w) 932 return width # XXX + 1/2*(left and right border widths) 933 934 def _calcSpanRanges(self): 935 """Work out rects for tables which do row and column spanning. 936 937 This creates some mappings to let the later code determine 938 if a cell is part of a "spanned" range. 939 self._spanRanges shows the 'coords' in integers of each 940 'cell range', or None if it was clobbered: 941 (col, row) -> (col0, row0, col1, row1) 942 943 Any cell not in the key is not part of a spanned region 944 """ 945 self._spanRanges = spanRanges = {} 946 for x in xrange(self._ncols): 947 for y in xrange(self._nrows): 948 spanRanges[x,y] = (x, y, x, y) 949 self._colSpanCells = [] 950 self._rowSpanCells = [] 951 csa = self._colSpanCells.append 952 rsa = self._rowSpanCells.append 953 for (cmd, start, stop) in self._spanCmds: 954 x0, y0 = start 955 x1, y1 = stop 956 957 #normalize 958 if x0 < 0: x0 = x0 + self._ncols 959 if x1 < 0: x1 = x1 + self._ncols 960 if y0 < 0: y0 = y0 + self._nrows 961 if y1 < 0: y1 = y1 + self._nrows 962 if x0 > x1: x0, x1 = x1, x0 963 if y0 > y1: y0, y1 = y1, y0 964 965 if x0!=x1 or y0!=y1: 966 if x0!=x1: #column span 967 for y in xrange(y0, y1+1): 968 for x in xrange(x0,x1+1): 969 csa((x,y)) 970 if y0!=y1: #row span 971 for y in xrange(y0, y1+1): 972 for x in xrange(x0,x1+1): 973 rsa((x,y)) 974 975 for y in xrange(y0, y1+1): 976 for x in xrange(x0,x1+1): 977 spanRanges[x,y] = None 978 # set the main entry 979 spanRanges[x0,y0] = (x0, y0, x1, y1) 980 981 def _calcNoSplitRanges(self): 982 """ 983 This creates some mappings to let the later code determine 984 if a cell is part of a "nosplit" range. 985 self._nosplitRanges shows the 'coords' in integers of each 986 'cell range', or None if it was clobbered: 987 (col, row) -> (col0, row0, col1, row1) 988 989 Any cell not in the key is not part of a spanned region 990 """ 991 self._nosplitRanges = nosplitRanges = {} 992 for x in xrange(self._ncols): 993 for y in xrange(self._nrows): 994 nosplitRanges[x,y] = (x, y, x, y) 995 self._colNoSplitCells = [] 996 self._rowNoSplitCells = [] 997 csa = self._colNoSplitCells.append 998 rsa = self._rowNoSplitCells.append 999 for (cmd, start, stop) in self._nosplitCmds: 1000 x0, y0 = start 1001 x1, y1 = stop 1002 1003 #normalize 1004 if x0 < 0: x0 = x0 + self._ncols 1005 if x1 < 0: x1 = x1 + self._ncols 1006 if y0 < 0: y0 = y0 + self._nrows 1007 if y1 < 0: y1 = y1 + self._nrows 1008 if x0 > x1: x0, x1 = x1, x0 1009 if y0 > y1: y0, y1 = y1, y0 1010 1011 if x0!=x1 or y0!=y1: 1012 #column span 1013 if x0!=x1: 1014 for y in xrange(y0, y1+1): 1015 for x in xrange(x0,x1+1): 1016 csa((x,y)) 1017 #row span 1018 if y0!=y1: 1019 for y in xrange(y0, y1+1): 1020 for x in xrange(x0,x1+1): 1021 rsa((x,y)) 1022 1023 for y in xrange(y0, y1+1): 1024 for x in xrange(x0,x1+1): 1025 nosplitRanges[x,y] = None 1026 # set the main entry 1027 nosplitRanges[x0,y0] = (x0, y0, x1, y1) 1028 1029 def _calcSpanRects(self): 1030 """Work out rects for tables which do row and column spanning. 1031 1032 Based on self._spanRanges, which is already known, 1033 and the widths which were given or previously calculated, 1034 self._spanRects shows the real coords for drawing: 1035 1036 (col, row) -> (x, y, width, height) 1037 1038 for each cell. Any cell which 'does not exist' as another 1039 has spanned over it will get a None entry on the right 1040 """ 1041 spanRects = getattr(self,'_spanRects',{}) 1042 hmax = getattr(self,'_hmax',None) 1043 longTable = self._longTableOptimize 1044 if spanRects and (longTable and hmax==self._hmax_spanRects or not longTable): 1045 return 1046 colpositions = self._colpositions 1047 rowpositions = self._rowpositions 1048 vBlocks = {} 1049 hBlocks = {} 1050 rlim = len(rowpositions)-1 1051 for (coord, value) in self._spanRanges.items(): 1052 if value is None: 1053 spanRects[coord] = None 1054 else: 1055 try: 1056 col0, row0, col1, row1 = value 1057 if row1>=rlim: continue 1058 col,row = coord 1059 if col1-col0>0: 1060 for _ in xrange(col0+1,col1+1): 1061 vBlocks.setdefault(colpositions[_],[]).append((rowpositions[row1+1],rowpositions[row0])) 1062 if row1-row0>0: 1063 for _ in xrange(row0+1,row1+1): 1064 hBlocks.setdefault(rowpositions[_],[]).append((colpositions[col0],colpositions[col1+1])) 1065 x = colpositions[col0] 1066 y = rowpositions[row1+1] 1067 width = colpositions[col1+1] - x 1068 height = rowpositions[row0] - y 1069 spanRects[coord] = (x, y, width, height) 1070 except: 1071 annotateException('\nspanning problem in %s' % (self.identity(),)) 1072 1073 for _ in hBlocks, vBlocks: 1074 for value in _.values(): 1075 value.sort() 1076 self._spanRects = spanRects 1077 self._vBlocks = vBlocks 1078 self._hBlocks = hBlocks 1079 self._hmax_spanRects = hmax 1080 1081 def setStyle(self, tblstyle): 1082 if not isinstance(tblstyle,TableStyle): 1083 tblstyle = TableStyle(tblstyle) 1084 for cmd in tblstyle.getCommands(): 1085 self._addCommand(cmd) 1086 for k,v in tblstyle._opts.items(): 1087 setattr(self,k,v) 1088 for a in ('spaceBefore','spaceAfter'): 1089 if not hasattr(self,a) and hasattr(tblstyle,a): 1090 setattr(self,a,getattr(tblstyle,a)) 1091 1092 def normCellRange(self, sc, ec, sr, er): 1093 '''ensure cell range ends are with the table bounds''' 1094 if sc < 0: sc = sc + self._ncols 1095 if ec < 0: ec = ec + self._ncols 1096 if sr < 0: sr = sr + self._nrows 1097 if er < 0: er = er + self._nrows 1098 return max(0,sc), min(self._ncols-1,ec), max(0,sr), min(self._nrows-1,er) 1099 1100 def _addCommand(self,cmd): 1101 if cmd[0] in ('BACKGROUND','ROWBACKGROUNDS','COLBACKGROUNDS'): 1102 self._bkgrndcmds.append(cmd) 1103 elif cmd[0] == 'SPAN': 1104 self._spanCmds.append(cmd) 1105 elif cmd[0] == 'NOSPLIT': 1106 # we expect op, start, stop 1107 self._nosplitCmds.append(cmd) 1108 elif _isLineCommand(cmd): 1109 # we expect op, start, stop, weight, colour, cap, dashes, join 1110 cmd = list(cmd) 1111 if len(cmd)<5: raise ValueError('bad line command '+ascii(cmd)) 1112 1113 #determine line cap value at position 5. This can be str or numeric. 1114 if len(cmd)<6: 1115 cmd.append(1) 1116 else: 1117 cap = _convert2int(cmd[5], LINECAPS, 0, 2, 'cap', cmd) 1118 cmd[5] = cap 1119 1120 #dashes at index 6 - this is a dash array: 1121 if len(cmd)<7: cmd.append(None) 1122 1123 #join mode at index 7 - can be str or numeric, look up as for caps 1124 if len(cmd)<8: cmd.append(1) 1125 else: 1126 join = _convert2int(cmd[7], LINEJOINS, 0, 2, 'join', cmd) 1127 cmd[7] = join 1128 1129 #linecount at index 8. Default is 1, set to 2 for double line. 1130 if len(cmd)<9: cmd.append(1) 1131 else: 1132 lineCount = cmd[8] 1133 if lineCount is None: 1134 lineCount = 1 1135 cmd[8] = lineCount 1136 assert lineCount >= 1 1137 #linespacing at index 9. Not applicable unless 2+ lines, defaults to line 1138 #width so you get a visible gap between centres 1139 if len(cmd)<10: cmd.append(cmd[3]) 1140 else: 1141 space = cmd[9] 1142 if space is None: 1143 space = cmd[3] 1144 cmd[9] = space 1145 assert len(cmd) == 10 1146 1147 self._linecmds.append(tuple(cmd)) 1148 elif cmd[0]=="ROUNDEDCORNERS": 1149 self._setCornerRadii(cmd[1]) 1150 else: 1151 (op, (sc, sr), (ec, er)), values = cmd[:3] , cmd[3:] 1152 if sr in ('splitfirst','splitlast'): 1153 self._srflcmds.append(cmd) 1154 else: 1155 sc, ec, sr, er = self.normCellRange(sc,ec,sr,er) 1156 ec += 1 1157 for i in xrange(sr, er+1): 1158 for j in xrange(sc, ec): 1159 _setCellStyle(self._cellStyles, i, j, op, values) 1160 1161 def _drawLines(self): 1162 ccap, cdash, cjoin = None, None, None 1163 canv = self.canv 1164 canv.saveState() 1165 1166 rrd = self._roundingRectDef 1167 if rrd: #we are collection some lines 1168 SL = rrd.SL 1169 SL[:] = [] #empty saved lines list 1170 ocanvline = canv.line 1171 aSL = SL.append 1172 def rcCanvLine(xs, ys, xe, ye): 1173 if ( 1174 (xs==xe and (xs>=rrd.x1 or xs<=rrd.x0)) #vertical line that needs to be saved 1175 or 1176 (ys==ye and (ys>=rrd.y1 or ys<=rrd.y0)) #horizontal line that needs to be saved 1177 ): 1178 aSL(RoundingRectLine(xs,ys,xe,ye,weight,color,cap,dash,join)) 1179 else: 1180 ocanvline(xs,ys,xe,ye) 1181 canv.line = rcCanvLine 1182 1183 try: 1184 for op, (sc,sr), (ec,er), weight, color, cap, dash, join, count, space in self._linecmds: 1185 if isinstance(sr,strTypes) and sr.startswith('split'): continue 1186 if cap!=None and ccap!=cap: 1187 canv.setLineCap(cap) 1188 ccap = cap 1189 if dash is None or dash == []: 1190 if cdash is not None: 1191 canv.setDash() 1192 cdash = None 1193 elif dash != cdash: 1194 canv.setDash(dash) 1195 cdash = dash 1196 if join is not None and cjoin!=join: 1197 canv.setLineJoin(join) 1198 cjoin = join 1199 sc, ec, sr, er = self.normCellRange(sc,ec,sr,er) 1200 getattr(self,_LineOpMap.get(op, '_drawUnknown' ))( (sc, sr), (ec, er), weight, color, count, space) 1201 finally: 1202 if rrd: 1203 canv.line = ocanvline 1204 canv.restoreState() 1205 self._curcolor = None 1206 1207 def _drawUnknown(self, start, end, weight, color, count, space): 1208 #we are only called from _drawLines which is one level up 1209 import sys 1210 op = sys._getframe(1).f_locals['op'] 1211 raise ValueError("Unknown line command '%s'" % op) 1212 1213 def _drawGrid(self, start, end, weight, color, count, space): 1214 self._drawBox( start, end, weight, color, count, space) 1215 self._drawInnerGrid( start, end, weight, color, count, space) 1216 1217 def _drawBox(self, start, end, weight, color, count, space): 1218 sc,sr = start 1219 ec,er = end 1220 self._drawHLines((sc, sr), (ec, sr), weight, color, count, space) 1221 self._drawHLines((sc, er+1), (ec, er+1), weight, color, count, space) 1222 self._drawVLines((sc, sr), (sc, er), weight, color, count, space) 1223 self._drawVLines((ec+1, sr), (ec+1, er), weight, color, count, space) 1224 1225 def _drawInnerGrid(self, start, end, weight, color, count, space): 1226 sc,sr = start 1227 ec,er = end 1228 self._drawHLines((sc, sr+1), (ec, er), weight, color, count, space) 1229 self._drawVLines((sc+1, sr), (ec, er), weight, color, count, space) 1230 1231 def _prepLine(self, weight, color): 1232 if color and color!=self._curcolor: 1233 self.canv.setStrokeColor(color) 1234 self._curcolor = color 1235 if weight and weight!=self._curweight: 1236 self.canv.setLineWidth(weight) 1237 self._curweight = weight 1238 1239 def _drawHLines(self, start, end, weight, color, count, space): 1240 sc,sr = start 1241 ec,er = end 1242 ecp = self._colpositions[sc:ec+2] 1243 rp = self._rowpositions[sr:er+1] 1244 if len(ecp)<=1 or len(rp)<1: return 1245 self._prepLine(weight, color) 1246 scp = ecp[0] 1247 ecp = ecp[-1] 1248 hBlocks = getattr(self,'_hBlocks',{}) 1249 canvLine = self.canv.line 1250 if count == 1: 1251 for y in rp: 1252 _hLine(canvLine, scp, ecp, y, hBlocks) 1253 else: 1254 lf = lambda x0,y0,x1,y1,canvLine=canvLine, ws=weight+space, count=count: _multiLine(x0,x1,y0,canvLine,ws,count) 1255 for y in rp: 1256 _hLine(lf, scp, ecp, y, hBlocks) 1257 1258 def _drawHLinesB(self, start, end, weight, color, count, space): 1259 sc,sr = start 1260 ec,er = end 1261 self._drawHLines((sc, sr+1), (ec, er+1), weight, color, count, space) 1262 1263 def _drawVLines(self, start, end, weight, color, count, space): 1264 sc,sr = start 1265 ec,er = end 1266 erp = self._rowpositions[sr:er+2] 1267 cp = self._colpositions[sc:ec+1] 1268 if len(erp)<=1 or len(cp)<1: return 1269 self._prepLine(weight, color) 1270 srp = erp[0] 1271 erp = erp[-1] 1272 vBlocks = getattr(self,'_vBlocks',{}) 1273 canvLine = lambda y0, x0, y1, x1, _line=self.canv.line: _line(x0,y0,x1,y1) 1274 if count == 1: 1275 for x in cp: 1276 _hLine(canvLine, erp, srp, x, vBlocks) 1277 else: 1278 lf = lambda x0,y0,x1,y1,canvLine=canvLine, ws=weight+space, count=count: _multiLine(x0,x1,y0,canvLine,ws,count) 1279 for x in cp: 1280 _hLine(lf, erp, srp, x, vBlocks) 1281 1282 def _drawVLinesA(self, start, end, weight, color, count, space): 1283 sc,sr = start 1284 ec,er = end 1285 self._drawVLines((sc+1, sr), (ec+1, er), weight, color, count, space) 1286 1287 def wrap(self, availWidth, availHeight): 1288 self._calc(availWidth, availHeight) 1289 self.availWidth = availWidth 1290 return (self._width, self._height) 1291 1292 def onSplit(self,T,byRow=1): 1293 ''' 1294 This method will be called when the Table is split. 1295 Special purpose tables can override to do special stuff. 1296 ''' 1297 pass 1298 1299 def _cr_0(self,n,cmds,nr0,_srflMode=False): 1300 for c in cmds: 1301 (sc,sr), (ec,er) = c[1:3] 1302 if sr in ('splitfirst','splitlast'): 1303 if not _srflMode: continue 1304 self._addCommand(c) #re-append the command 1305 if sr=='splitfirst': continue 1306 sr = er = n-1 1307 if sr<0: sr += nr0 1308 if sr>=n: continue 1309 if er>=n: er = n-1 1310 self._addCommand((c[0],)+((sc, sr), (ec, er))+tuple(c[3:])) 1311 1312 def _cr_1_1(self, n, nRows, repeatRows, cmds, _srflMode=False): 1313 nrr = len(repeatRows) 1314 rrS = set(repeatRows) 1315 for c in cmds: 1316 (sc,sr), (ec,er) = c[1:3] 1317 if sr in ('splitfirst','splitlast'): 1318 if not _srflMode: continue 1319 self._addCommand(c) 1320 if sr=='splitlast': continue 1321 sr = er = n 1322 if sr<0: sr += nRows 1323 if er<0: er += nRows 1324 cS = set(xrange(sr,er+1)) & rrS 1325 if cS: 1326 #it's a repeat row 1327 cS = list(cS) 1328 self._addCommand((c[0],)+((sc, repeatRows.index(min(cS))), (ec, repeatRows.index(max(cS))))+tuple(c[3:])) 1329 if er<n: continue 1330 sr = max(sr-n,0)+nrr 1331 er = max(er-n,0)+nrr 1332 self._addCommand((c[0],)+((sc, sr), (ec, er))+tuple(c[3:])) 1333 sr = self._rowSplitRange 1334 if sr: 1335 sr, er = sr 1336 if sr<0: sr += nRows 1337 if er<0: er += nRows 1338 if er<n: 1339 self._rowSplitRange = None 1340 else: 1341 sr = max(sr-n,0)+nrr 1342 er = max(er-n,0)+nrr 1343 self._rowSplitRange = sr,er 1344 1345 def _cr_1_0(self,n,cmds,_srflMode=False): 1346 for c in cmds: 1347 (sc,sr), (ec,er) = c[1:3] 1348 if sr in ('splitfirst','splitlast'): 1349 if not _srflMode: continue 1350 self._addCommand(c) 1351 if sr=='splitlast': continue 1352 sr = er = n 1353 if er>=0 and er<n: continue 1354 if sr>=0 and sr<n: sr=0 1355 if sr>=n: sr -= n 1356 if er>=n: er -= n 1357 self._addCommand((c[0],)+((sc, sr), (ec, er))+tuple(c[3:])) 1358 1359 def _splitRows(self,availHeight): 1360 n=self._getFirstPossibleSplitRowPosition(availHeight) 1361 repeatRows = self.repeatRows 1362 if n<= (repeatRows if isinstance(repeatRows,int) else (max(repeatRows)+1)): return [] 1363 lim = len(self._rowHeights) 1364 if n==lim: return [self] 1365 1366 lo = self._rowSplitRange 1367 if lo: 1368 lo, hi = lo 1369 if lo<0: lo += lim 1370 if hi<0: hi += lim 1371 if n>hi: 1372 return self._splitRows(availHeight - sum(self._rowHeights[hi:n])) 1373 elif n<lo: 1374 return [] 1375 1376 repeatCols = self.repeatCols 1377 splitByRow = self.splitByRow 1378 data = self._cellvalues 1379 1380 #we're going to split into two superRows 1381 ident = self.ident 1382 if ident: ident = IdentStr(ident) 1383 lto = self._longTableOptimize 1384 if lto: 1385 splitH = self._rowHeights 1386 else: 1387 splitH = self._argH 1388 cornerRadii = getattr(self,'_cornerRadii',None) 1389 R0 = self.__class__( data[:n], colWidths=self._colWidths, rowHeights=splitH[:n], 1390 repeatRows=repeatRows, repeatCols=repeatCols, 1391 splitByRow=splitByRow, normalizedData=1, cellStyles=self._cellStyles[:n], 1392 ident=ident, 1393 spaceBefore=getattr(self,'spaceBefore',None), 1394 longTableOptimize=lto, 1395 cornerRadii=cornerRadii[:2] if cornerRadii else None) 1396 1397 nrows = self._nrows 1398 ncols = self._ncols 1399 #copy the commands 1400 A = [] 1401 # hack up the line commands 1402 for op, (sc,sr), (ec,er), weight, color, cap, dash, join, count, space in self._linecmds: 1403 if isinstance(sr,strTypes) and sr.startswith('split'): 1404 A.append((op,(sc,sr), (ec,sr), weight, color, cap, dash, join, count, space)) 1405 if sr=='splitlast': 1406 sr = er = n-1 1407 elif sr=='splitfirst': 1408 sr = n 1409 er = n 1410 1411 if sc < 0: sc += ncols 1412 if ec < 0: ec += ncols 1413 if sr < 0: sr += nrows 1414 if er < 0: er += nrows 1415 1416 if op in ('BOX','OUTLINE','GRID'): 1417 if sr<n and er>=n: 1418 # we have to split the BOX 1419 A.append(('LINEABOVE',(sc,sr), (ec,sr), weight, color, cap, dash, join, count, space)) 1420 A.append(('LINEBEFORE',(sc,sr), (sc,er), weight, color, cap, dash, join, count, space)) 1421 A.append(('LINEAFTER',(ec,sr), (ec,er), weight, color, cap, dash, join, count, space)) 1422 A.append(('LINEBELOW',(sc,er), (ec,er), weight, color, cap, dash, join, count, space)) 1423 if op=='GRID': 1424 A.append(('LINEBELOW',(sc,n-1), (ec,n-1), weight, color, cap, dash, join, count, space)) 1425 A.append(('LINEABOVE',(sc,n), (ec,n), weight, color, cap, dash, join, count, space)) 1426 A.append(('INNERGRID',(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) 1427 else: 1428 A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) 1429 elif op == 'INNERGRID': 1430 if sr<n and er>=n: 1431 A.append(('LINEBELOW',(sc,n-1), (ec,n-1), weight, color, cap, dash, join, count, space)) 1432 A.append(('LINEABOVE',(sc,n), (ec,n), weight, color, cap, dash, join, count, space)) 1433 A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) 1434 elif op == 'LINEBELOW': 1435 if sr<n and er>=(n-1): 1436 A.append(('LINEABOVE',(sc,n), (ec,n), weight, color, cap, dash, join, count, space)) 1437 A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) 1438 elif op == 'LINEABOVE': 1439 if sr<=n and er>=n: 1440 A.append(('LINEBELOW',(sc,n-1), (ec,n-1), weight, color, cap, dash, join, count, space)) 1441 A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) 1442 else: 1443 A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) 1444 1445 R0._cr_0(n,A,nrows) 1446 R0._cr_0(n,self._bkgrndcmds,nrows,_srflMode=True) 1447 R0._cr_0(n,self._spanCmds,nrows) 1448 R0._cr_0(n,self._nosplitCmds,nrows) 1449 for c in self._srflcmds: 1450 R0._addCommand(c) 1451 if c[1][1]!='splitlast': continue 1452 (sc,sr), (ec,er) = c[1:3] 1453 R0._addCommand((c[0],)+((sc, n-1), (ec, n-1))+tuple(c[3:])) 1454 1455 if ident: ident = IdentStr(ident) 1456 if repeatRows: 1457 if isinstance(repeatRows,int): 1458 iRows = data[:repeatRows] 1459 iRowH = splitH[:repeatRows] 1460 iCS = self._cellStyles[:repeatRows] 1461 repeatRows = list(xrange(repeatRows)) 1462 else: 1463 #we have a list of repeated rows eg (1,3) 1464 repeatRows = list(sorted(repeatRows)) 1465 iRows = [data[i] for i in repeatRows] 1466 iRowH = [splitH[i] for i in repeatRows] 1467 iCS = [self._cellStyles[i] for i in repeatRows] 1468 R1 = self.__class__(iRows+data[n:],colWidths=self._colWidths, 1469 rowHeights=iRowH+splitH[n:], 1470 repeatRows=len(repeatRows), repeatCols=repeatCols, 1471 splitByRow=splitByRow, normalizedData=1, 1472 cellStyles=iCS+self._cellStyles[n:], 1473 ident=ident, 1474 spaceAfter=getattr(self,'spaceAfter',None), 1475 longTableOptimize=lto, 1476 cornerRadii = cornerRadii, 1477 ) 1478 R1._cr_1_1(n,nrows,repeatRows,A) #linecommands 1479 R1._cr_1_1(n,nrows,repeatRows,self._bkgrndcmds,_srflMode=True) 1480 R1._cr_1_1(n,nrows,repeatRows,self._spanCmds) 1481 R1._cr_1_1(n,nrows,repeatRows,self._nosplitCmds) 1482 else: 1483 #R1 = slelf.__class__(data[n:], self._argW, self._argH[n:], 1484 R1 = self.__class__(data[n:], colWidths=self._colWidths, rowHeights=splitH[n:], 1485 repeatRows=repeatRows, repeatCols=repeatCols, 1486 splitByRow=splitByRow, normalizedData=1, cellStyles=self._cellStyles[n:], 1487 ident=ident, 1488 spaceAfter=getattr(self,'spaceAfter',None), 1489 longTableOptimize=lto, 1490 cornerRadii = ([0,0] + cornerRadii[2:]) if cornerRadii else None, 1491 ) 1492 R1._cr_1_0(n,A) 1493 R1._cr_1_0(n,self._bkgrndcmds,_srflMode=True) 1494 R1._cr_1_0(n,self._spanCmds) 1495 R1._cr_1_0(n,self._nosplitCmds) 1496 for c in self._srflcmds: 1497 R1._addCommand(c) 1498 if c[1][1]!='splitfirst': continue 1499 (sc,sr), (ec,er) = c[1:3] 1500 R1._addCommand((c[0],)+((sc, 0), (ec, 0))+tuple(c[3:])) 1501 1502 R0.hAlign = R1.hAlign = self.hAlign 1503 R0.vAlign = R1.vAlign = self.vAlign 1504 self.onSplit(R0) 1505 self.onSplit(R1) 1506 return [R0,R1] 1507 1508 def _getRowImpossible(impossible,cells,ranges): 1509 for xy in cells: 1510 r=ranges[xy] 1511 if r!=None: 1512 y1,y2=r[1],r[3] 1513 if y1!=y2: 1514 ymin=min(y1,y2) #normalize 1515 ymax=max(y1,y2) #normalize 1516 y=ymin+1 1517 while 1: 1518 if y>ymax: break 1519 impossible[y]=None #split at position y is impossible because of overlapping rowspan 1520 y+=1 1521 _getRowImpossible=staticmethod(_getRowImpossible) 1522 1523 def _getFirstPossibleSplitRowPosition(self,availHeight): 1524 impossible={} 1525 if self._spanCmds: 1526 self._getRowImpossible(impossible,self._rowSpanCells,self._spanRanges) 1527 if self._nosplitCmds: 1528 self._getRowImpossible(impossible,self._rowNoSplitCells,self._nosplitRanges) 1529 h = 0 1530 n = 1 1531 split_at = 0 # from this point of view 0 is the first position where the table may *always* be splitted 1532 for rh in self._rowHeights: 1533 if h+rh>availHeight: 1534 break 1535 if n not in impossible: 1536 split_at=n 1537 h=h+rh 1538 n=n+1 1539 return split_at 1540 1541 def split(self, availWidth, availHeight): 1542 self._calc(availWidth, availHeight) 1543 if self.splitByRow: 1544 if not rl_config.allowTableBoundsErrors and self._width>availWidth: return [] 1545 return self._splitRows(availHeight) 1546 else: 1547 raise NotImplementedError 1548 1549 def _makeRoundedCornersClip(self, FUZZ=rl_config._FUZZ): 1550 self._roundingRectDef = None 1551 cornerRadii = getattr(self,'_cornerRadii',None) 1552 if not cornerRadii or max(cornerRadii)<=FUZZ: return 1553 nrows = self._nrows 1554 ncols = self._ncols 1555 ar = [min(self._rowHeights[i],self._colWidths[j],cornerRadii[k]) for 1556 k,(i,j) in enumerate(( 1557 (0,0), 1558 (0,ncols-1), 1559 (nrows-1,0), 1560 (nrows-1, ncols-1), 1561 ))] 1562 rp = self._rowpositions 1563 cp = self._colpositions 1564 1565 x0 = cp[0] 1566 y0 = rp[nrows] 1567 x1 = cp[ncols] 1568 y1 = rp[0] 1569 w = x1 - x0 1570 h = y1 - y0 1571 self._roundingRectDef = RoundingRectDef(x0, y0, w, h, x1, y1, ar, []) 1572 P = self.canv.beginPath() 1573 P.roundRect(x0, y0, w, h, ar) 1574 c = self.canv 1575 c.addLiteral('%begin table rect clip') 1576 c.clipPath(P,stroke=0) 1577 c.addLiteral('%end table rect clip') 1578 1579 def _restoreRoundingObscuredLines(self): 1580 x0, y0, w, h, x1, y1, ar, SL = self._roundingRectDef 1581 if not SL: return 1582 canv = self.canv 1583 canv.saveState() 1584 ccap = cdash = cjoin = self._curweight = self._curcolor = None 1585 line = canv.line 1586 cornerRadii = self._cornerRadii 1587 for (xs,ys,xe,ye,weight,color,cap,dash,join) in SL: 1588 if cap!=None and ccap!=cap: 1589 canv.setLineCap(cap) 1590 ccap = cap 1591 if dash is None or dash == []: 1592 if cdash is not None: 1593 canv.setDash() 1594 cdash = None 1595 elif dash != cdash: 1596 canv.setDash(dash) 1597 cdash = dash 1598 if join is not None and cjoin!=join: 1599 canv.setLineJoin(join) 1600 cjoin = join 1601 self._prepLine(weight, color) 1602 if ys==ye: 1603 #horizontal line 1604 if ys>y1 or ys<y0: 1605 line(xs,ys,xe,ye) #simple line that's outside the clip 1606 continue 1607 #which corners are involved 1608 if ys==y0: 1609 ypos = 'bottom' 1610 r0 = ar[2] 1611 r1 = ar[3] 1612 else: #ys==y1 1613 ypos = 'top' 1614 r0 = ar[0] 1615 r1 = ar[1] 1616 if xs>=x0+r0 and xe<=x1-r1: 1617 line(xs,ys,xe,ye) #simple line with no rounding 1618 continue 1619 #we have some rounding so we must use a path 1620 c0 = _quadrantDef('left',ypos,(xs,ys), r0, kind=2, direction='left-right') if xs<x0+r0 else None 1621 c1 = _quadrantDef('right',ypos,(xe,ye), r1, kind=1, direction='left-right') if xe>x1-r1 else None 1622 else: 1623 #vertical line 1624 if xs>x1 or xs<x0: 1625 line(xs,ys,xe,ye) #simple line that's outside the clip 1626 continue 1627 #which corners are involved 1628 if xs==x0: 1629 xpos = 'left' 1630 r0 = ar[2] 1631 r1 = ar[0] 1632 else: #xs==x1 1633 xpos = 'right' 1634 r0 = ar[3] 1635 r1 = ar[1] 1636 if ys>=y0+r0 and ye<=y1-r1: 1637 line(xs,ys,xe,ye) #simple line with no rounding 1638 continue 1639 #we have some rounding so we must use a path 1640 c0 = _quadrantDef(xpos,'bottom',(xs,ys), r0, kind=2, direction='bottom-top') if ys<y0+r0 else None 1641 c1 = _quadrantDef(xpos,'top',(xe,ye), r1, kind=1, direction='bottom-top') if ye>y1-r1 else None 1642 P = canv.beginPath() 1643 if c0: 1644 P.moveTo(*c0[0]) 1645 P.curveTo(c0[1][0],c0[1][1],c0[2][0],c0[2][1], c0[3][0],c0[3][1]) 1646 else: 1647 P.moveTo(xs,ys) 1648 if not c1: 1649 P.lineTo(xe,ye) 1650 else: 1651 P.lineTo(*c1[0]) 1652 P.curveTo(c1[1][0],c1[1][1],c1[2][0],c1[2][1], c1[3][0],c1[3][1]) 1653 canv.drawPath(P, stroke=1, fill=0) 1654 canv.restoreState() 1655 1656 def draw(self): 1657 c = self.canv 1658 c.saveState() 1659 self._makeRoundedCornersClip() 1660 self._curweight = self._curcolor = self._curcellstyle = None 1661 self._drawBkgrnd() 1662 if not self._spanCmds: 1663 # old fashioned case, no spanning, steam on and do each cell 1664 for row, rowstyle, rowpos, rowheight in zip(self._cellvalues, self._cellStyles, self._rowpositions[1:], self._rowHeights): 1665 for cellval, cellstyle, colpos, colwidth in zip(row, rowstyle, self._colpositions[:-1], self._colWidths): 1666 self._drawCell(cellval, cellstyle, (colpos, rowpos), (colwidth, rowheight)) 1667 else: 1668 # we have some row or col spans, need a more complex algorithm 1669 # to find the rect for each 1670 for rowNo in xrange(self._nrows): 1671 for colNo in xrange(self._ncols): 1672 cellRect = self._spanRects[colNo, rowNo] 1673 if cellRect is not None: 1674 (x, y, width, height) = cellRect 1675 cellval = self._cellvalues[rowNo][colNo] 1676 cellstyle = self._cellStyles[rowNo][colNo] 1677 self._drawCell(cellval, cellstyle, (x, y), (width, height)) 1678 self._drawLines() 1679 c.restoreState() 1680 if self._roundingRectDef: 1681 self._restoreRoundingObscuredLines() 1682 1683 def _drawBkgrnd(self): 1684 nrows = self._nrows 1685 ncols = self._ncols 1686 canv = self.canv 1687 colpositions = self._colpositions 1688 rowpositions = self._rowpositions 1689 rowHeights = self._rowHeights 1690 colWidths = self._colWidths 1691 spanRects = getattr(self,'_spanRects',None) 1692 for cmd, (sc, sr), (ec, er), arg in self._bkgrndcmds: 1693 if sr in ('splitfirst','splitlast'): continue 1694 if sc < 0: sc = sc + ncols 1695 if ec < 0: ec = ec + ncols 1696 if sr < 0: sr = sr + nrows 1697 if er < 0: er = er + nrows 1698 x0 = colpositions[sc] 1699 y0 = rowpositions[sr] 1700 x1 = colpositions[min(ec+1,ncols)] 1701 y1 = rowpositions[min(er+1,nrows)] 1702 w, h = x1-x0, y1-y0 1703 if hasattr(arg,'__call__'): 1704 arg(self,canv, x0, y0, w, h) 1705 elif cmd == 'ROWBACKGROUNDS': 1706 #Need a list of colors to cycle through. The arguments 1707 #might be already colours, or convertible to colors, or 1708 # None, or the str 'None'. 1709 #It's very common to alternate a pale shade with None. 1710 colorCycle = list(map(colors.toColorOrNone, arg)) 1711 count = len(colorCycle) 1712 rowCount = er - sr + 1 1713 for i in xrange(rowCount): 1714 color = colorCycle[i%count] 1715 h = rowHeights[sr + i] 1716 if color: 1717 canv.setFillColor(color) 1718 canv.rect(x0, y0, w, -h, stroke=0,fill=1) 1719 y0 = y0 - h 1720 elif cmd == 'COLBACKGROUNDS': 1721 #cycle through colours columnwise 1722 colorCycle = list(map(colors.toColorOrNone, arg)) 1723 count = len(colorCycle) 1724 colCount = ec - sc + 1 1725 for i in xrange(colCount): 1726 color = colorCycle[i%count] 1727 w = colWidths[sc + i] 1728 if color: 1729 canv.setFillColor(color) 1730 canv.rect(x0, y0, w, h, stroke=0,fill=1) 1731 x0 = x0 +w 1732 else: #cmd=='BACKGROUND' 1733 if arg and isinstance(arg,(list,tuple)) and arg[0] in ('VERTICAL','HORIZONTAL'): 1734 # 1735 # Arg is a list, assume we are going for a gradient fill 1736 # where we expect a containing a direction for the gradient 1737 # and the starting an final gradient colors. For example: 1738 # ['HORIZONTAL', colors.white, colors.grey] or 1739 # ['VERTICAL', colors.red, colors.blue] 1740 # 1741 canv.saveState() 1742 1743 if ec==sc and er==sr and spanRects: 1744 xywh = spanRects.get((sc,sr)) 1745 if xywh: 1746 #it's a single cell 1747 x0, y0, w, h = xywh 1748 p = canv.beginPath() 1749 p.rect(x0, y0, w, h) 1750 canv.clipPath(p, stroke=0) 1751 direction=arg.pop(0) 1752 if direction=="HORIZONTAL": 1753 canv.linearGradient(x0,y0,x0+w,y0,arg,extend=False) 1754 else: #VERTICAL 1755 canv.linearGradient(x0,y0,x0,y0+h,arg,extend=False) 1756 canv.restoreState() 1757 else: 1758 color = colors.toColorOrNone(arg) 1759 if color: 1760 if ec==sc and er==sr and spanRects: 1761 xywh = spanRects.get((sc,sr)) 1762 if xywh: 1763 #it's a single cell 1764 x0, y0, w, h = xywh 1765 canv.setFillColor(color) 1766 canv.rect(x0, y0, w, h, stroke=0,fill=1) 1767 1768 def _drawCell(self, cellval, cellstyle, pos, size): 1769 colpos, rowpos = pos 1770 colwidth, rowheight = size 1771 if self._curcellstyle is not cellstyle: 1772 cur = self._curcellstyle 1773 if cur is None or cellstyle.color != cur.color: 1774 self.canv.setFillColor(cellstyle.color) 1775 if cur is None or cellstyle.leading != cur.leading or cellstyle.fontname != cur.fontname or cellstyle.fontsize != cur.fontsize: 1776 self.canv.setFont(cellstyle.fontname, cellstyle.fontsize, cellstyle.leading) 1777 self._curcellstyle = cellstyle 1778 1779 just = cellstyle.alignment 1780 valign = cellstyle.valign 1781 if isinstance(cellval,(tuple,list,Flowable)): 1782 if not isinstance(cellval,(tuple,list)): cellval = (cellval,) 1783 # we assume it's a list of Flowables 1784 W = [] 1785 H = [] 1786 w, h = self._listCellGeom(cellval,colwidth,cellstyle,W=W, H=H,aH=rowheight) 1787 if valign=='TOP': 1788 y = rowpos + rowheight - cellstyle.topPadding 1789 elif valign=='BOTTOM': 1790 y = rowpos+cellstyle.bottomPadding + h 1791 else: 1792 y = rowpos+(rowheight+cellstyle.bottomPadding-cellstyle.topPadding+h)/2.0 1793 if cellval: y += cellval[0].getSpaceBefore() 1794 for v, w, h in zip(cellval,W,H): 1795 if just=='LEFT': x = colpos+cellstyle.leftPadding 1796 elif just=='RIGHT': x = colpos+colwidth-cellstyle.rightPadding - w 1797 elif just in ('CENTRE', 'CENTER'): 1798 x = colpos+(colwidth+cellstyle.leftPadding-cellstyle.rightPadding-w)/2.0 1799 else: 1800 raise ValueError('Invalid justification %s' % just) 1801 y -= v.getSpaceBefore() 1802 y -= h 1803 v.drawOn(self.canv,x,y) 1804 y -= v.getSpaceAfter() 1805 else: 1806 if just == 'LEFT': 1807 draw = self.canv.drawString 1808 x = colpos + cellstyle.leftPadding 1809 elif just in ('CENTRE', 'CENTER'): 1810 draw = self.canv.drawCentredString 1811 x = colpos+(colwidth+cellstyle.leftPadding-cellstyle.rightPadding)*0.5 1812 elif just == 'RIGHT': 1813 draw = self.canv.drawRightString 1814 x = colpos + colwidth - cellstyle.rightPadding 1815 elif just == 'DECIMAL': 1816 draw = self.canv.drawAlignedString 1817 x = colpos + colwidth - cellstyle.rightPadding 1818 else: 1819 raise ValueError('Invalid justification %s' % just) 1820 vals = str(cellval).split("\n") 1821 n = len(vals) 1822 leading = cellstyle.leading 1823 fontsize = cellstyle.fontsize 1824 if valign=='BOTTOM': 1825 y = rowpos + cellstyle.bottomPadding+n*leading-fontsize 1826 elif valign=='TOP': 1827 y = rowpos + rowheight - cellstyle.topPadding - fontsize 1828 elif valign=='MIDDLE': 1829 #tim roberts pointed out missing fontsize correction 2004-10-04 1830 y = rowpos + (cellstyle.bottomPadding + rowheight-cellstyle.topPadding+n*leading)/2.0 - fontsize 1831 else: 1832 raise ValueError("Bad valign: '%s'" % str(valign)) 1833 1834 for v in vals: 1835 draw(x, y, v) 1836 y -= leading 1837 onDraw = getattr(cellval,'onDraw',None) 1838 if onDraw: 1839 onDraw(self.canv,cellval.kind,cellval.label) 1840 1841 if cellstyle.href: 1842 #external hyperlink 1843 self.canv.linkURL(cellstyle.href, (colpos, rowpos, colpos + colwidth, rowpos + rowheight), relative=1) 1844 if cellstyle.destination: 1845 #external hyperlink 1846 self.canv.linkRect("", cellstyle.destination, Rect=(colpos, rowpos, colpos + colwidth, rowpos + rowheight), relative=1) 1847 1848 def _setCornerRadii(self, cornerRadii): 1849 if isListOfNumbersOrNone(cornerRadii): 1850 self._cornerRadii = None if not cornerRadii else list(cornerRadii) + (max(4-len(cornerRadii),0)*[0]) 1851 else: 1852 raise ValueError('cornerRadii should be None or a list/tuple of numeric radii') 1853 1854_LineOpMap = { 'GRID':'_drawGrid', 1855 'BOX':'_drawBox', 1856 'OUTLINE':'_drawBox', 1857 'INNERGRID':'_drawInnerGrid', 1858 'LINEBELOW':'_drawHLinesB', 1859 'LINEABOVE':'_drawHLines', 1860 'LINEBEFORE':'_drawVLines', 1861 'LINEAFTER':'_drawVLinesA', } 1862 1863class LongTable(Table): 1864 '''Henning von Bargen's changes will be active''' 1865 _longTableOptimize = 1 1866 1867LINECOMMANDS = list(_LineOpMap.keys()) 1868 1869def _isLineCommand(cmd): 1870 return cmd[0] in LINECOMMANDS 1871 1872def _setCellStyle(cellStyles, i, j, op, values): 1873 #new = CellStyle('<%d, %d>' % (i,j), cellStyles[i][j]) 1874 #cellStyles[i][j] = new 1875 ## modify in place!!! 1876 new = cellStyles[i][j] 1877 if op == 'FONT': 1878 n = len(values) 1879 new.fontname = values[0] 1880 if n>1: 1881 new.fontsize = values[1] 1882 if n>2: 1883 new.leading = values[2] 1884 else: 1885 new.leading = new.fontsize*1.2 1886 elif op in ('FONTNAME', 'FACE'): 1887 new.fontname = values[0] 1888 elif op in ('SIZE', 'FONTSIZE'): 1889 new.fontsize = values[0] 1890 elif op == 'LEADING': 1891 new.leading = values[0] 1892 elif op == 'TEXTCOLOR': 1893 new.color = colors.toColor(values[0], colors.Color(0,0,0)) 1894 elif op in ('ALIGN', 'ALIGNMENT'): 1895 new.alignment = values[0] 1896 elif op == 'VALIGN': 1897 new.valign = values[0] 1898 elif op == 'LEFTPADDING': 1899 new.leftPadding = values[0] 1900 elif op == 'RIGHTPADDING': 1901 new.rightPadding = values[0] 1902 elif op == 'TOPPADDING': 1903 new.topPadding = values[0] 1904 elif op == 'BOTTOMPADDING': 1905 new.bottomPadding = values[0] 1906 elif op == 'HREF': 1907 new.href = values[0] 1908 elif op == 'DESTINATION': 1909 new.destination = values[0] 1910 1911GRID_STYLE = TableStyle( 1912 [('GRID', (0,0), (-1,-1), 0.25, colors.black), 1913 ('ALIGN', (1,1), (-1,-1), 'RIGHT')] 1914 ) 1915BOX_STYLE = TableStyle( 1916 [('BOX', (0,0), (-1,-1), 0.50, colors.black), 1917 ('ALIGN', (1,1), (-1,-1), 'RIGHT')] 1918 ) 1919LABELED_GRID_STYLE = TableStyle( 1920 [('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), 1921 ('BOX', (0,0), (-1,-1), 2, colors.black), 1922 ('LINEBELOW', (0,0), (-1,0), 2, colors.black), 1923 ('LINEAFTER', (0,0), (0,-1), 2, colors.black), 1924 ('ALIGN', (1,1), (-1,-1), 'RIGHT')] 1925 ) 1926COLORED_GRID_STYLE = TableStyle( 1927 [('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), 1928 ('BOX', (0,0), (-1,-1), 2, colors.red), 1929 ('LINEBELOW', (0,0), (-1,0), 2, colors.black), 1930 ('LINEAFTER', (0,0), (0,-1), 2, colors.black), 1931 ('ALIGN', (1,1), (-1,-1), 'RIGHT')] 1932 ) 1933LIST_STYLE = TableStyle( 1934 [('LINEABOVE', (0,0), (-1,0), 2, colors.green), 1935 ('LINEABOVE', (0,1), (-1,-1), 0.25, colors.black), 1936 ('LINEBELOW', (0,-1), (-1,-1), 2, colors.green), 1937 ('ALIGN', (1,1), (-1,-1), 'RIGHT')] 1938 ) 1939 1940# experimental iterator which can apply a sequence 1941# of colors e.g. Blue, None, Blue, None as you move 1942# down. 1943if __name__ == '__main__': 1944 from tests.test_platypus_tables import old_tables_test 1945 old_tables_test() 1946