%PDF- %PDF-
Direktori : /lib/python3/dist-packages/reportlab/platypus/ |
Current File : //lib/python3/dist-packages/reportlab/platypus/flowables.py |
#Copyright ReportLab Europe Ltd. 2000-2017 #see license.txt for license details #history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/platypus/flowables.py __version__='3.3.0' __doc__=""" A flowable is a "floating element" in a document whose exact position is determined by the other elements that precede it, such as a paragraph, a diagram interspersed between paragraphs, a section header, etcetera. Examples of non-flowables include page numbering annotations, headers, footers, fixed diagrams or logos, among others. Flowables are defined here as objects which know how to determine their size and which can draw themselves onto a page with respect to a relative "origin" position determined at a higher level. The object's draw() method should assume that (0,0) corresponds to the bottom left corner of the enclosing rectangle that will contain the object. The attributes vAlign and hAlign may be used by 'packers' as hints as to how the object should be placed. Some Flowables also know how to "split themselves". For example a long paragraph might split itself between one page and the next. Packers should set the canv attribute during wrap, split & draw operations to allow the flowable to work out sizes etc in the proper context. The "text" of a document usually consists mainly of a sequence of flowables which flow into a document from top to bottom (with column and page breaks controlled by higher level components). """ import os from copy import deepcopy, copy from reportlab.lib.colors import gray, lightgrey from reportlab.lib.rl_accel import fp_str from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT from reportlab.lib.styles import _baseFontName from reportlab.lib.utils import strTypes, rl_safe_exec, annotateException from reportlab.lib.abag import ABag from reportlab.pdfbase import pdfutils from reportlab.pdfbase.pdfmetrics import stringWidth from reportlab.rl_config import _FUZZ, overlapAttachedSpace, ignoreContainerActions, listWrapOnFakeWidth __all__ = '''AnchorFlowable BalancedColumns BulletDrawer CallerMacro CondPageBreak DDIndenter DocAssert DocAssign DocExec DocIf DocPara DocWhile FailOnDraw FailOnWrap Flowable FrameBG FrameSplitter HRFlowable Image ImageAndFlowables KeepInFrame KeepTogether LIIndenter ListFlowable ListItem Macro NullDraw PTOContainer PageBreak PageBreakIfNotEmpty ParagraphAndImage Preformatted SetPageTopFlowables SetTopFlowables SlowPageBreak Spacer TopPadder TraceInfo UseUpSpace XBox splitLine splitLines'''.split() class TraceInfo: "Holder for info about where an object originated" def __init__(self): self.srcFile = '(unknown)' self.startLineNo = -1 self.startLinePos = -1 self.endLineNo = -1 self.endLinePos = -1 ############################################################# # Flowable Objects - a base class and a few examples. # One is just a box to get some metrics. We also have # a paragraph, an image and a special 'page break' # object which fills the space. ############################################################# class Flowable: """Abstract base class for things to be drawn. Key concepts: 1. It knows its size 2. It draws in its own coordinate system (this requires the base API to provide a translate() function. """ _fixedWidth = 0 #assume wrap results depend on arguments? _fixedHeight = 0 def __init__(self): self.width = 0 self.height = 0 self.wrapped = 0 #these are hints to packers/frames as to how the floable should be positioned self.hAlign = 'LEFT' #CENTER/CENTRE or RIGHT self.vAlign = 'BOTTOM' #MIDDLE or TOP #optional holder for trace info self._traceInfo = None self._showBoundary = None #many flowables handle text and must be processed in the #absence of a canvas. tagging them with their encoding #helps us to get conversions right. Use Python codec names. self.encoding = None def _drawOn(self,canv): '''ensure canv is set on and then draw''' self.canv = canv self.draw()#this is the bit you overload del self.canv def _hAlignAdjust(self,x,sW=0): if sW and hasattr(self,'hAlign'): a = self.hAlign if a in ('CENTER','CENTRE', TA_CENTER): x += 0.5*sW elif a in ('RIGHT',TA_RIGHT): x += sW elif a not in ('LEFT',TA_LEFT): raise ValueError("Bad hAlign value "+str(a)) return x def drawOn(self, canvas, x, y, _sW=0): "Tell it to draw itself on the canvas. Do not override" x = self._hAlignAdjust(x,_sW) canvas.saveState() canvas.translate(x, y) self._drawOn(canvas) if hasattr(self, '_showBoundary') and self._showBoundary: #diagnostic tool support canvas.setStrokeColor(gray) canvas.rect(0,0,self.width, self.height) canvas.restoreState() def wrapOn(self, canv, aW, aH): '''intended for use by packers allows setting the canvas on during the actual wrap''' self.canv = canv w, h = self.wrap(aW,aH) del self.canv return w, h def wrap(self, availWidth, availHeight): """This will be called by the enclosing frame before objects are asked their size, drawn or whatever. It returns the size actually used.""" return (self.width, self.height) def minWidth(self): """This should return the minimum required width""" return getattr(self,'_minWidth',self.width) def splitOn(self, canv, aW, aH): '''intended for use by packers allows setting the canvas on during the actual split''' self.canv = canv S = self.split(aW,aH) del self.canv return S def split(self, availWidth, availheight): """This will be called by more sophisticated frames when wrap fails. Stupid flowables should return []. Clever flowables should split themselves and return a list of flowables. If they decide that nothing useful can be fitted in the available space (e.g. if you have a table and not enough space for the first row), also return []""" return [] def getKeepWithNext(self): """returns boolean determining whether the next flowable should stay with this one""" if hasattr(self,'keepWithNext'): return self.keepWithNext elif hasattr(self,'style') and hasattr(self.style,'keepWithNext'): return self.style.keepWithNext else: return 0 def getSpaceAfter(self): """returns how much space should follow this item if another item follows on the same page.""" if hasattr(self,'spaceAfter'): return self.spaceAfter elif hasattr(self,'style') and hasattr(self.style,'spaceAfter'): return self.style.spaceAfter else: return 0 def getSpaceBefore(self): """returns how much space should precede this item if another item precedess on the same page.""" if hasattr(self,'spaceBefore'): return self.spaceBefore elif hasattr(self,'style') and hasattr(self.style,'spaceBefore'): return self.style.spaceBefore else: return 0 def isIndexing(self): """Hook for IndexingFlowables - things which have cross references""" return 0 def identity(self, maxLen=None): ''' This method should attempt to return a string that can be used to identify a particular flowable uniquely. The result can then be used for debugging and or error printouts ''' if hasattr(self, 'getPlainText'): r = self.getPlainText(identify=1) elif hasattr(self, 'text'): r = str(self.text) else: r = '...' if r and maxLen: r = r[:maxLen] return "<%s at %s%s>%s" % (self.__class__.__name__, hex(id(self)), self._frameName(), r) @property def _doctemplate(self): return getattr(getattr(self,'canv',None),'_doctemplate',None) def _doctemplateAttr(self,a): return getattr(self._doctemplate,a,None) def _frameName(self): f = getattr(self,'_frame',None) if not f: f = self._doctemplateAttr('frame') if f and f.id: return ' frame=%s' % f.id return '' class XBox(Flowable): """Example flowable - a box with an x through it and a caption. This has a known size, so does not need to respond to wrap().""" def __init__(self, width, height, text = 'A Box'): Flowable.__init__(self) self.width = width self.height = height self.text = text def __repr__(self): return "XBox(w=%s, h=%s, t=%s)" % (self.width, self.height, self.text) def draw(self): self.canv.rect(0, 0, self.width, self.height) self.canv.line(0, 0, self.width, self.height) self.canv.line(0, self.height, self.width, 0) #centre the text self.canv.setFont(_baseFontName,12) self.canv.drawCentredString(0.5*self.width, 0.5*self.height, self.text) def _trimEmptyLines(lines): #don't want the first or last to be empty while len(lines) and lines[0].strip() == '': lines = lines[1:] while len(lines) and lines[-1].strip() == '': lines = lines[:-1] return lines def _dedenter(text,dedent=0): ''' tidy up text - carefully, it is probably code. If people want to indent code within a source script, you can supply an arg to dedent and it will chop off that many character, otherwise it leaves left edge intact. ''' lines = text.split('\n') if dedent>0: templines = _trimEmptyLines(lines) lines = [] for line in templines: line = line[dedent:].rstrip() lines.append(line) else: lines = _trimEmptyLines(lines) return lines SPLIT_CHARS = "[{( ,.;:/\\-" def splitLines(lines, maximum_length, split_characters, new_line_characters): if split_characters is None: split_characters = SPLIT_CHARS if new_line_characters is None: new_line_characters = "" # Return a table of lines lines_splitted = [] for line in lines: if len(line) > maximum_length: splitLine(line, lines_splitted, maximum_length, \ split_characters, new_line_characters) else: lines_splitted.append(line) return lines_splitted def splitLine(line_to_split, lines_splitted, maximum_length, \ split_characters, new_line_characters): # Used to implement the characters added #at the beginning of each new line created first_line = True # Check if the text can be splitted while line_to_split and len(line_to_split)>0: # Index of the character where we can split split_index = 0 # Check if the line length still exceeds the maximum length if len(line_to_split) <= maximum_length: # Return the remaining of the line split_index = len(line_to_split) else: # Iterate for each character of the line for line_index in range(maximum_length): # Check if the character is in the list # of allowed characters to split on if line_to_split[line_index] in split_characters: split_index = line_index + 1 # If the end of the line was reached # with no character to split on if split_index==0: split_index = line_index + 1 if first_line: lines_splitted.append(line_to_split[0:split_index]) first_line = False maximum_length -= len(new_line_characters) else: lines_splitted.append(new_line_characters + \ line_to_split[0:split_index]) # Remaining text to split line_to_split = line_to_split[split_index:] class Preformatted(Flowable): """This is like the HTML <PRE> tag. It attempts to display text exactly as you typed it in a fixed width "typewriter" font. By default the line breaks are exactly where you put them, and it will not be wrapped. You can optionally define a maximum line length and the code will be wrapped; and extra characters to be inserted at the beginning of each wrapped line (e.g. '> '). """ def __init__(self, text, style, bulletText = None, dedent=0, maxLineLength=None, splitChars=None, newLineChars=""): """text is the text to display. If dedent is set then common leading space will be chopped off the front (for example if the entire text is indented 6 spaces or more then each line will have 6 spaces removed from the front). """ self.style = style self.bulletText = bulletText self.lines = _dedenter(text,dedent) if text and maxLineLength: self.lines = splitLines( self.lines, maxLineLength, splitChars, newLineChars ) def __repr__(self): bT = self.bulletText H = "Preformatted(" if bT is not None: H = "Preformatted(bulletText=%s," % repr(bT) return "%s'''\\ \n%s''')" % (H, '\n'.join(self.lines)) def wrap(self, availWidth, availHeight): self.width = availWidth self.height = self.style.leading*len(self.lines) return (self.width, self.height) def minWidth(self): style = self.style fontSize = style.fontSize fontName = style.fontName return max([stringWidth(line,fontName,fontSize) for line in self.lines]) def split(self, availWidth, availHeight): #returns two Preformatted objects #not sure why they can be called with a negative height if availHeight < self.style.leading: return [] linesThatFit = int(availHeight * 1.0 / self.style.leading) text1 = '\n'.join(self.lines[0:linesThatFit]) text2 = '\n'.join(self.lines[linesThatFit:]) style = self.style if style.firstLineIndent != 0: style = deepcopy(style) style.firstLineIndent = 0 return [Preformatted(text1, self.style), Preformatted(text2, style)] def draw(self): #call another method for historical reasons. Besides, I #suspect I will be playing with alternate drawing routines #so not doing it here makes it easier to switch. cur_x = self.style.leftIndent cur_y = self.height - self.style.fontSize self.canv.addLiteral('%PreformattedPara') if self.style.textColor: self.canv.setFillColor(self.style.textColor) tx = self.canv.beginText(cur_x, cur_y) #set up the font etc. tx.setFont( self.style.fontName, self.style.fontSize, self.style.leading) for text in self.lines: tx.textLine(text) self.canv.drawText(tx) class Image(Flowable): """an image (digital picture). Formats supported by PIL/Java 1.4 (the Python/Java Imaging Library are supported. Images as flowables may be aligned horizontally in the frame with the hAlign parameter - accepted values are 'CENTER', 'LEFT' or 'RIGHT' with 'CENTER' being the default. We allow for two kinds of lazyness to allow for many images in a document which could lead to file handle starvation. lazy=1 don't open image until required. lazy=2 open image when required then shut it. """ _fixedWidth = 1 _fixedHeight = 1 def __init__(self, filename, width=None, height=None, kind='direct', mask="auto", lazy=1, hAlign='CENTER', useDPI=False): """If size to draw at not specified, get it from the image.""" self.hAlign = hAlign self._mask = mask fp = hasattr(filename,'read') self._drawing = None if fp: self._file = filename self.filename = repr(filename) elif hasattr(filename,'_renderPy'): self._drawing = filename self.filename=repr(filename) self._file = None self._img = None fp = True else: self._file = self.filename = filename self._dpi = useDPI if not fp and os.path.splitext(filename)[1] in ['.jpg', '.JPG', '.jpeg', '.JPEG']: # if it is a JPEG, will be inlined within the file - # but we still need to know its size now from reportlab.lib.utils import open_for_read f = open_for_read(filename, 'b') try: try: info = pdfutils.readJPEGInfo(f) except: #couldn't read as a JPEG, try like normal self._setup(width,height,kind,lazy) return finally: f.close() self.imageWidth = info[0] self.imageHeight = info[1] if useDPI: self._dpi = info[3] self._img = None self._setup(width,height,kind,0) elif fp: self._setup(width,height,kind,0) else: self._setup(width,height,kind,lazy) def _dpiAdjust(self): dpi = self._dpi if dpi: if dpi[0]!=72: self.imageWidth *= 72.0 / dpi[0] if dpi[1]!=72: self.imageHeight *= 72.0 / dpi[1] def _setup(self,width,height,kind,lazy): self._lazy = lazy self._width = width self._height = height self._kind = kind if lazy<=0: self._setup_inner() def _setup_inner(self): width = self._width height = self._height kind = self._kind img = self._img if img: self.imageWidth, self.imageHeight = img.getSize() if self._dpi and hasattr(img,'_image'): self._dpi = img._image.info.get('dpi',(72,72)) elif self._drawing: self.imageWidth, self.imageHeight = self._drawing.width,self._drawing.height self._dpi = False self._dpiAdjust() if self._lazy>=2: del self._img if kind in ['direct','absolute']: self.drawWidth = width or self.imageWidth self.drawHeight = height or self.imageHeight elif kind in ['percentage','%']: self.drawWidth = self.imageWidth*width*0.01 self.drawHeight = self.imageHeight*height*0.01 elif kind in ['bound','proportional']: factor = min(float(width)/self.imageWidth,float(height)/self.imageHeight) self.drawWidth = self.imageWidth*factor self.drawHeight = self.imageHeight*factor def _restrictSize(self,aW,aH): if self.drawWidth>aW+_FUZZ or self.drawHeight>aH+_FUZZ: self._oldDrawSize = self.drawWidth, self.drawHeight factor = min(float(aW)/self.drawWidth,float(aH)/self.drawHeight) self.drawWidth *= factor self.drawHeight *= factor return self.drawWidth, self.drawHeight def _unRestrictSize(self): dwh = getattr(self,'_oldDrawSize',None) if dwh: self.drawWidth, self.drawHeight = dwh def __getattr__(self,a): if a=='_img': from reportlab.lib.utils import ImageReader #this may raise an error self._img = ImageReader(self._file) if not isinstance(self._file,strTypes): self._file = None if self._lazy>=2: self._lazy = 1 #here we're assuming we cannot read again return self._img elif a in ('drawWidth','drawHeight','imageWidth','imageHeight'): self._setup_inner() return self.__dict__[a] raise AttributeError("<Image @ 0x%x>.%s" % (id(self),a)) def wrap(self, availWidth, availHeight): #the caller may decide it does not fit. return self.drawWidth, self.drawHeight def draw(self): dx = getattr(self,'_offs_x',0) dy = getattr(self,'_offs_y',0) d = self._drawing if d: sx = self.drawWidth / float(self.imageWidth) sy = self.drawHeight / float(self.imageHeight) otrans = d.transform try: d.scale(sx,sy) d.drawOn(self.canv,dx,dy) finally: d.transform = otrans else: lazy = self._lazy if lazy>=2: self._lazy = 1 self.canv.drawImage( self._img or self.filename, dx, dy, self.drawWidth, self.drawHeight, mask=self._mask, ) if lazy>=2: self._img = self._file = None self._lazy = lazy def identity(self,maxLen=None): r = Flowable.identity(self,maxLen) if r[-4:]=='>...' and isinstance(self.filename,str): r = "%s filename=%s>" % (r[:-4],self.filename) return r class NullDraw(Flowable): def draw(self): pass class Spacer(NullDraw): """A spacer just takes up space and doesn't draw anything - it guarantees a gap between objects.""" _fixedWidth = 1 _fixedHeight = 1 def __init__(self, width, height, isGlue=False): self.width = width if isGlue: self.height = 1e-4 self.spacebefore = height self.height = height def __repr__(self): return "%s(%s, %s)" % (self.__class__.__name__,self.width, self.height) class UseUpSpace(NullDraw): def __init__(self): pass def __repr__(self): return "%s()" % self.__class__.__name__ def wrap(self, availWidth, availHeight): self.width = availWidth self.height = availHeight return (availWidth,availHeight-1e-8) #step back a point class PageBreak(UseUpSpace): locChanger=1 """Move on to the next page in the document. This works by consuming all remaining space in the frame!""" def __init__(self,nextTemplate=None): self.nextTemplate = nextTemplate def __repr__(self): return "%s(%s)" % (self.__class__.__name__,repr(self.nextTemplate) if self.nextTemplate else '') class SlowPageBreak(PageBreak): pass class PageBreakIfNotEmpty(PageBreak): pass class CondPageBreak(Spacer): locChanger=1 """use up a frame if not enough vertical space effectively CondFrameBreak""" def __init__(self, height): self.height = height def __repr__(self): return "CondPageBreak(%s)" %(self.height,) def wrap(self, availWidth, availHeight): if availHeight<self.height: f = self._doctemplateAttr('frame') if not f: return availWidth, availHeight from reportlab.platypus.doctemplate import FrameBreak f.add_generated_content(FrameBreak) return 0, 0 def identity(self,maxLen=None): return repr(self).replace(')',',frame=%s)'%self._frameName()) def _listWrapOn(F,availWidth,canv,mergeSpace=1,obj=None,dims=None,fakeWidth=None): '''return max width, required height for a list of flowables F''' doct = getattr(canv,'_doctemplate',None) cframe = getattr(doct,'frame',None) if fakeWidth is None: fakeWidth = listWrapOnFakeWidth if cframe: from reportlab.platypus.doctemplate import _addGeneratedContent, Indenter doct_frame = cframe cframe = doct.frame = deepcopy(doct_frame) cframe._generated_content = None del cframe._generated_content try: W = 0 H = 0 pS = 0 atTop = 1 F = F[:] while F: f = F.pop(0) if hasattr(f,'frameAction'): from reportlab.platypus.doctemplate import Indenter if isinstance(f,Indenter): availWidth -= f.left+f.right continue w,h = f.wrapOn(canv,availWidth,0xfffffff) if dims is not None: dims.append((w,h)) if cframe: _addGeneratedContent(F,cframe) if (w<=_FUZZ and False) or h<=_FUZZ: continue W = max(W,min(w,availWidth) if fakeWidth else w) H += h if not atTop: h = f.getSpaceBefore() if mergeSpace: if getattr(f,'_SPACETRANSFER',False): h = pS h = max(h-pS,0) H += h else: if obj is not None: obj._spaceBefore = f.getSpaceBefore() atTop = 0 s = f.getSpaceAfter() if getattr(f,'_SPACETRANSFER',False): s = pS pS = s H += pS if obj is not None: obj._spaceAfter = pS return W, H-pS finally: if cframe: doct.frame = doct_frame def _flowableSublist(V): "if it isn't a list or tuple, wrap it in a list" if not isinstance(V,(list,tuple)): V = V is not None and [V] or [] from reportlab.platypus.doctemplate import LCActionFlowable assert not [x for x in V if isinstance(x,LCActionFlowable)],'LCActionFlowables not allowed in sublists' return V class _ContainerSpace: #Abstract some common container like behaviour def getSpaceBefore(self,content=None): for c in (self._content if content is None else content): if not hasattr(c,'frameAction'): return c.getSpaceBefore() return 0 def getSpaceAfter(self,content=None): for c in reversed(self._content if content is None else content): if not hasattr(c,'frameAction'): return c.getSpaceAfter() return 0 class KeepTogether(_ContainerSpace,Flowable): splitAtTop = False def __init__(self,flowables,maxHeight=None): if not hasattr(KeepTogether,'NullActionFlowable'): #cache these on the class from reportlab.platypus.doctemplate import NullActionFlowable from reportlab.platypus.doctemplate import FrameBreak KeepTogether.NullActionFlowable = NullActionFlowable KeepTogether.FrameBreak = FrameBreak if not flowables: flowables = [self.NullActionFlowable()] self._content = _flowableSublist(flowables) self._maxHeight = maxHeight def __repr__(self): f = self._content L = list(map(repr,f)) L = "\n"+"\n".join(L) L = L.replace("\n", "\n ") return "%s(%s,maxHeight=%s)" % (self.__class__.__name__,L,self._maxHeight) def wrap(self, aW, aH): dims = [] try: W,H = _listWrapOn(self._content,aW,self.canv,dims=dims) except: annotateException('\nraised by class %s(%s)@0x%8.8x wrap\n' % (self.__class__.__name__,self.__class__.__module__,id(self))) self._H = H self._H0 = dims and dims[0][1] or 0 self._wrapInfo = aW,aH return W, 0xffffff # force a split def split(self, aW, aH): if getattr(self,'_wrapInfo',None)!=(aW,aH): self.wrap(aW,aH) S = self._content[:] cf = atTop = getattr(self,'_frame',None) if cf: atTop = getattr(cf,'_atTop',None) cAW = cf._width cAH = cf._height C0 = self._H>aH and (not self._maxHeight or aH>self._maxHeight) C1 = (self._H0>aH) or C0 and atTop if C0 or C1: fb = False panf = self._doctemplateAttr('_peekNextFrame') if cf and panf: nf = panf() nAW = nf._width nAH = nf._height if C0 and not (self.splitAtTop and atTop): fb = not (atTop and cf and nf and cAW>=nAW and cAH>=nAH) elif nf and nAW>=cf._width and nAH>=self._H: fb = True S.insert(0,(self.FrameBreak if fb else self.NullActionFlowable)()) return S def identity(self, maxLen=None): msg = "<%s at %s%s> containing :%s" % (self.__class__.__name__,hex(id(self)),self._frameName(),"\n".join([f.identity() for f in self._content])) if maxLen: return msg[0:maxLen] else: return msg class KeepTogetherSplitAtTop(KeepTogether): ''' Same as KeepTogether, but it will split content immediately if it cannot fit at the top of a frame. ''' splitAtTop = True class Macro(Flowable): """This is not actually drawn (i.e. it has zero height) but is executed when it would fit in the frame. Allows direct access to the canvas through the object 'canvas'""" def __init__(self, command): self.command = command def __repr__(self): return "Macro(%s)" % repr(self.command) def wrap(self, availWidth, availHeight): return (0,0) def draw(self): rl_safe_exec(self.command, g=None, l={'canvas':self.canv}) def _nullCallable(*args,**kwds): pass class CallerMacro(Flowable): ''' like Macro, but with callable command(s) drawCallable(self) wrapCallable(self,aW,aH) ''' def __init__(self, drawCallable=None, wrapCallable=None): self._drawCallable = drawCallable or _nullCallable self._wrapCallable = wrapCallable or _nullCallable def __repr__(self): return "CallerMacro(%r,%r)" % (self._drawCallable,self._wrapCallable) def wrap(self, aW, aH): self._wrapCallable(self,aW,aH) return (0,0) def draw(self): self._drawCallable(self) class ParagraphAndImage(Flowable): '''combine a Paragraph and an Image''' def __init__(self,P,I,xpad=3,ypad=3,side='right'): self.P = P self.I = I self.xpad = xpad self.ypad = ypad self._side = side def getSpaceBefore(self): return max(self.P.getSpaceBefore(),self.I.getSpaceBefore()) def getSpaceAfter(self): return max(self.P.getSpaceAfter(),self.I.getSpaceAfter()) def wrap(self,availWidth,availHeight): wI, hI = self.I.wrap(availWidth,availHeight) self.wI = wI self.hI = hI # work out widths array for breaking self.width = availWidth P = self.P style = P.style xpad = self.xpad ypad = self.ypad leading = style.leading leftIndent = style.leftIndent later_widths = availWidth - leftIndent - style.rightIndent intermediate_widths = later_widths - xpad - wI first_line_width = intermediate_widths - style.firstLineIndent P.width = 0 nIW = int((hI+ypad)/(leading*1.0)) P.blPara = P.breakLines([first_line_width] + nIW*[intermediate_widths]+[later_widths]) if self._side=='left': self._offsets = [wI+xpad]*(1+nIW)+[0] P.height = len(P.blPara.lines)*leading self.height = max(hI,P.height) return (self.width, self.height) def split(self,availWidth, availHeight): P, wI, hI, ypad = self.P, self.wI, self.hI, self.ypad if hI+ypad>availHeight or len(P.frags)<=0: return [] S = P.split(availWidth,availHeight) if not S: return S P = self.P = S[0] del S[0] style = P.style P.height = len(self.P.blPara.lines)*style.leading self.height = max(hI,P.height) return [self]+S def draw(self): canv = self.canv if self._side=='left': self.I.drawOn(canv,0,self.height-self.hI-self.ypad) self.P._offsets = self._offsets try: self.P.drawOn(canv,0,0) finally: del self.P._offsets else: self.I.drawOn(canv,self.width-self.wI-self.xpad,self.height-self.hI-self.ypad) self.P.drawOn(canv,0,0) class FailOnWrap(NullDraw): def wrap(self, availWidth, availHeight): raise ValueError("FailOnWrap flowable wrapped and failing as ordered!") class FailOnDraw(Flowable): def wrap(self, availWidth, availHeight): return 0,0 def draw(self): raise ValueError("FailOnDraw flowable drawn, and failing as ordered!") class HRFlowable(Flowable): '''Like the hr tag''' def __init__(self, width="80%", thickness=1, lineCap='round', color=lightgrey, spaceBefore=1, spaceAfter=1, hAlign='CENTER', vAlign='BOTTOM', dash=None): Flowable.__init__(self) self.width = width self.lineWidth = thickness self.lineCap=lineCap self.spaceBefore = spaceBefore self.spaceAfter = spaceAfter self.color = color self.hAlign = hAlign self.vAlign = vAlign self.dash = dash def __repr__(self): return "HRFlowable(width=%s, height=%s)" % (self.width, self.height) def wrap(self, availWidth, availHeight): w = self.width if isinstance(w,strTypes): w = w.strip() if w.endswith('%'): w = availWidth*float(w[:-1])*0.01 else: w = float(w) w = min(w,availWidth) self._width = w return w, self.lineWidth def draw(self): canv = self.canv canv.saveState() canv.setLineWidth(self.lineWidth) canv.setLineCap({'butt':0,'round':1, 'square': 2}[self.lineCap.lower()]) canv.setStrokeColor(self.color) if self.dash: canv.setDash(self.dash) canv.line(0, 0, self._width, self.height) canv.restoreState() class _PTOInfo: def __init__(self,trailer,header): self.trailer = _flowableSublist(trailer) self.header = _flowableSublist(header) def cdeepcopy(obj): if hasattr(obj,'deepcopy'): return obj.deepcopy() else: return deepcopy(obj) class _Container(_ContainerSpace): #Abstract some common container like behaviour def drawOn(self, canv, x, y, _sW=0, scale=1.0, content=None, aW=None): '''we simulate being added to a frame''' from reportlab.platypus.doctemplate import ActionFlowable, Indenter x0 = x y0 = y pS = 0 if aW is None: aW = self.width aW *= scale if content is None: content = self._content x = self._hAlignAdjust(x,_sW*scale) y += self.height*scale yt = y frame = getattr(self,'_frame',None) for c in content: if not ignoreContainerActions and isinstance(c,ActionFlowable): c.apply(canv._doctemplate) continue if isinstance(c,Indenter): x += c.left*scale aW -= (c.left+c.right)*scale continue w, h = c.wrapOn(canv,aW,0xfffffff) if h<_FUZZ and not getattr(c,'_ZEROSIZE',None): continue if yt!=y: s = c.getSpaceBefore() if not getattr(c,'_SPACETRANSFER',False): h += max(s-pS,0) y -= h s = c.getSpaceAfter() if getattr(c,'_SPACETRANSFER',False): s = pS pS = s fbg = getattr(frame,'_frameBGs',None) if fbg and fbg[-1].active: bg = fbg[-1] fbgl = bg.left fbgr = bg.right bgm = bg.start fbw = scale*(frame._width-fbgl-fbgr) fbx = x0+scale*(fbgl-frame._leftPadding) fbh = y + h + pS fby = max(y0,y-pS) fbh = max(0,fbh-fby) bg.render(canv,frame,fbx,fby,fbw,fbh) c._frame = frame c.drawOn(canv,x,y,_sW=aW-w) if c is not content[-1] and not getattr(c,'_SPACETRANSFER',None): y -= pS del c._frame def copyContent(self,content=None): C = [].append for c in (content or self._content): C(cdeepcopy(c)) self._content = C.__self__ class PTOContainer(_Container,Flowable): '''PTOContainer(contentList,trailerList,headerList) A container for flowables decorated with trailer & header lists. If the split operation would be called then the trailer and header lists are injected before and after the split. This allows specialist "please turn over" and "continued from previous" like behaviours.''' def __init__(self,content,trailer=None,header=None): I = _PTOInfo(trailer,header) self._content = C = [] for _ in _flowableSublist(content): if isinstance(_,PTOContainer): C.extend(_._content) else: C.append(_) if not hasattr(_,'_ptoinfo'): _._ptoinfo = I def wrap(self,availWidth,availHeight): self.width, self.height = _listWrapOn(self._content,availWidth,self.canv) return self.width,self.height def split(self, availWidth, availHeight): from reportlab.platypus.doctemplate import Indenter if availHeight<0: return [] canv = self.canv C = self._content x = i = H = pS = hx = 0 n = len(C) I2W = {} dLeft = dRight = 0 for x in range(n): c = C[x] I = c._ptoinfo if I not in I2W.keys(): T = I.trailer Hdr = I.header tW, tH = _listWrapOn(T, availWidth, self.canv) if len(T): #trailer may have no content tSB = T[0].getSpaceBefore() else: tSB = 0 I2W[I] = T,tW,tH,tSB else: T,tW,tH,tSB = I2W[I] _, h = c.wrapOn(canv,availWidth,0xfffffff) if isinstance(c,Indenter): dw = c.left+c.right dLeft += c.left dRight += c.right availWidth -= dw pS = 0 hx = 0 else: if x: hx = max(c.getSpaceBefore()-pS,0) h += hx pS = c.getSpaceAfter() H += h+pS tHS = tH+max(tSB,pS) if H+tHS>=availHeight-_FUZZ: break i += 1 #first retract last thing we tried H -= (h+pS) #attempt a sub split on the last one we have aH = (availHeight-H-tHS-hx)*0.99999 if aH>=0.05*availHeight: SS = c.splitOn(canv,availWidth,aH) else: SS = [] if abs(dLeft)+abs(dRight)>1e-8: R1I = [Indenter(-dLeft,-dRight)] R2I = [Indenter(dLeft,dRight)] else: R1I = R2I = [] if not SS: j = i while i>1 and C[i-1].getKeepWithNext(): i -= 1 C[i].keepWithNext = 0 if i==1 and C[0].getKeepWithNext(): #robin's black sheep i = j C[0].keepWithNext = 0 F = [UseUpSpace()] if len(SS)>1: R1 = C[:i]+SS[:1]+R1I+T+F R2 = Hdr+R2I+SS[1:]+C[i+1:] elif not i: return [] else: R1 = C[:i]+R1I+T+F R2 = Hdr+R2I+C[i:] T = R1 + [PTOContainer(R2,[copy(x) for x in I.trailer],[copy(x) for x in I.header])] return T #utility functions used by KeepInFrame def _hmodel(s0,s1,h0,h1): # calculate the parameters in the model # h = a/s**2 + b/s a11 = 1./s0**2 a12 = 1./s0 a21 = 1./s1**2 a22 = 1./s1 det = a11*a22-a12*a21 b11 = a22/det b12 = -a12/det b21 = -a21/det b22 = a11/det a = b11*h0+b12*h1 b = b21*h0+b22*h1 return a,b def _qsolve(h,ab): '''solve the model v = a/s**2 + b/s for an s which gives us v==h''' a,b = ab if abs(a)<=_FUZZ: return b/h t = 0.5*b/a from math import sqrt f = -h/a r = t*t-f if r<0: return None r = sqrt(r) if t>=0: s1 = -t - r else: s1 = -t + r s2 = f/s1 return max(1./s1, 1./s2) class KeepInFrame(_Container,Flowable): def __init__(self, maxWidth, maxHeight, content=[], mergeSpace=1, mode='shrink', name='',hAlign='LEFT',vAlign='BOTTOM', fakeWidth=None): '''mode describes the action to take when overflowing error raise an error in the normal way continue ignore ie just draw it and report maxWidth, maxHeight shrink shrinkToFit truncate fit as much as possible set fakeWidth to False to make _listWrapOn do the 'right' thing ''' self.name = name self.maxWidth = maxWidth self.maxHeight = maxHeight self.mode = mode assert mode in ('error','overflow','shrink','truncate'), '%s invalid mode value %s' % (self.identity(),mode) assert maxHeight>=0, '%s invalid maxHeight value %s' % (self.identity(),maxHeight) if mergeSpace is None: mergeSpace = overlapAttachedSpace self.mergespace = mergeSpace self._content = content or [] self.vAlign = vAlign self.hAlign = hAlign self.fakeWidth = fakeWidth def _getAvailableWidth(self): return self.maxWidth - self._leftExtraIndent - self._rightExtraIndent def identity(self, maxLen=None): return "<%s at %s%s%s> size=%sx%s" % (self.__class__.__name__, hex(id(self)), self._frameName(), getattr(self,'name','') and (' name="%s"'% getattr(self,'name','')) or '', getattr(self,'maxWidth','') and (' maxWidth=%s'%fp_str(getattr(self,'maxWidth',0))) or '', getattr(self,'maxHeight','')and (' maxHeight=%s' % fp_str(getattr(self,'maxHeight')))or '') def wrap(self,availWidth,availHeight): from reportlab.platypus.doctemplate import LayoutError mode = self.mode maxWidth = float(min(self.maxWidth or availWidth,availWidth)) maxHeight = float(min(self.maxHeight or availHeight,availHeight)) fakeWidth = self.fakeWidth W, H = _listWrapOn(self._content,maxWidth,self.canv, fakeWidth=fakeWidth) if (mode=='error' and (W>maxWidth+_FUZZ or H>maxHeight+_FUZZ)): ident = 'content %sx%s too large for %s' % (W,H,self.identity(30)) #leave to keep apart from the raise raise LayoutError(ident) elif W<=maxWidth+_FUZZ and H<=maxHeight+_FUZZ: self.width = W-_FUZZ #we take what we get self.height = H-_FUZZ elif mode in ('overflow','truncate'): #we lie self.width = min(maxWidth,W)-_FUZZ self.height = min(maxHeight,H)-_FUZZ else: def func(x): x = float(x) W, H = _listWrapOn(self._content,x*maxWidth,self.canv, fakeWidth=fakeWidth) W /= x H /= x return W, H W0 = W H0 = H s0 = 1 if W>maxWidth+_FUZZ: #squeeze out the excess width and or Height s1 = W/maxWidth #linear model W, H = func(s1) if H<=maxHeight+_FUZZ: self.width = W-_FUZZ self.height = H-_FUZZ self._scale = s1 return W,H s0 = s1 H0 = H W0 = W s1 = H/maxHeight W, H = func(s1) self.width = W-_FUZZ self.height = H-_FUZZ self._scale = s1 if H<min(0.95*maxHeight,maxHeight-10) or H>=maxHeight+_FUZZ: #the standard case W should be OK, H is short we want #to find the smallest s with H<=maxHeight H1 = H for f in 0, 0.01, 0.05, 0.10, 0.15: #apply the quadratic model s = _qsolve(maxHeight*(1-f),_hmodel(s0,s1,H0,H1)) W, H = func(s) if H<=maxHeight+_FUZZ and W<=maxWidth+_FUZZ: self.width = W-_FUZZ self.height = H-_FUZZ self._scale = s break return self.width, self.height def drawOn(self, canv, x, y, _sW=0): scale = getattr(self,'_scale',1.0) truncate = self.mode=='truncate' ss = scale!=1.0 or truncate if ss: canv.saveState() if truncate: p = canv.beginPath() p.rect(x, y, self.width,self.height) canv.clipPath(p,stroke=0) else: canv.translate(x,y) x=y=0 canv.scale(1.0/scale, 1.0/scale) _Container.drawOn(self, canv, x, y, _sW=_sW, scale=scale) if ss: canv.restoreState() class _FindSplitterMixin: def _findSplit(self,canv,availWidth,availHeight,mergeSpace=1,obj=None,content=None,paraFix=True): '''return max width, required height for a list of flowables F''' W = 0 H = 0 pS = sB = 0 atTop = 1 F = self._getContent(content) for i,f in enumerate(F): if hasattr(f,'frameAction'): from reportlab.platypus.doctemplate import Indenter if isinstance(f,Indenter): availWidth -= f.left+f.right continue w,h = f.wrapOn(canv,availWidth,0xfffffff) if w<=_FUZZ or h<=_FUZZ: continue W = max(W,w) if not atTop: s = f.getSpaceBefore() if mergeSpace: s = max(s-pS,0) H += s else: if obj is not None: obj._spaceBefore = f.getSpaceBefore() atTop = 0 if H>=availHeight or w>availWidth: return W, availHeight, F[:i],F[i:] H += h if H>availHeight: aH = availHeight-(H-h) if paraFix: from reportlab.platypus.paragraph import Paragraph if isinstance(f,(Paragraph,Preformatted)): leading = f.style.leading nH = leading*int(aH/float(leading))+_FUZZ if nH<aH: nH += leading availHeight += nH-aH aH = nH try: S = cdeepcopy(f).splitOn(canv,availWidth,aH) except: S = None #sometimes the deepcopy cannot be done if not S: return W, availHeight, F[:i],F[i:] else: return W,availHeight,F[:i]+S[:1],S[1:]+F[i+1:] pS = f.getSpaceAfter() H += pS if obj is not None: obj._spaceAfter = pS return W, H-pS, F, [] def _getContent(self,content=None): F = [] C = content if content is not None else self._content for f in C: if isinstance(f,ListFlowable): F.extend(f._getContent()) else: F.append(f) return F class ImageAndFlowables(_Container,_FindSplitterMixin,Flowable): '''combine a list of flowables and an Image''' def __init__(self,I,F,imageLeftPadding=0,imageRightPadding=3,imageTopPadding=0,imageBottomPadding=3, imageSide='right', imageHref=None): self._content = _flowableSublist(F) self._I = I self._irpad = imageRightPadding self._ilpad = imageLeftPadding self._ibpad = imageBottomPadding self._itpad = imageTopPadding self._side = imageSide self.imageHref = imageHref def deepcopy(self): c = copy(self) #shallow self._reset() c.copyContent() #partially deep? return c def getSpaceAfter(self): if hasattr(self,'_C1'): C = self._C1 elif hasattr(self,'_C0'): C = self._C0 else: C = self._content return _Container.getSpaceAfter(self,C) def getSpaceBefore(self): return max(self._I.getSpaceBefore(),_Container.getSpaceBefore(self)) def _reset(self): for a in ('_wrapArgs','_C0','_C1'): try: delattr(self,a) except: pass def wrap(self,availWidth,availHeight): canv = self.canv I = self._I if hasattr(self,'_wrapArgs'): if self._wrapArgs==(availWidth,availHeight) and getattr(I,'_oldDrawSize',None) is None: return self.width,self.height self._reset() I._unRestrictSize() self._wrapArgs = availWidth, availHeight I.wrap(availWidth,availHeight) wI, hI = I._restrictSize(availWidth,availHeight) self._wI = wI self._hI = hI ilpad = self._ilpad irpad = self._irpad ibpad = self._ibpad itpad = self._itpad self._iW = iW = availWidth - irpad - wI - ilpad aH = itpad + hI + ibpad if iW>_FUZZ: W,H0,self._C0,self._C1 = self._findSplit(canv,iW,aH) else: W = availWidth H0 = 0 if W>iW+_FUZZ: self._C0 = [] self._C1 = self._content aH = self._aH = max(aH,H0) self.width = availWidth if not self._C1: self.height = aH else: W1,H1 = _listWrapOn(self._C1,availWidth,canv) self.height = aH+H1 return self.width, self.height def split(self,availWidth, availHeight): if hasattr(self,'_wrapArgs'): I = self._I if self._wrapArgs!=(availWidth,availHeight) or getattr(I,'_oldDrawSize',None) is not None: self._reset() I._unRestrictSize() W,H=self.wrap(availWidth,availHeight) if self._aH>availHeight: return [] C1 = self._C1 if C1: S = C1[0].split(availWidth,availHeight-self._aH) if not S: _C1 = [] else: _C1 = [S[0]] C1 = S[1:]+C1[1:] else: _C1 = [] return [ImageAndFlowables( self._I, self._C0+_C1, imageLeftPadding=self._ilpad, imageRightPadding=self._irpad, imageTopPadding=self._itpad, imageBottomPadding=self._ibpad, imageSide=self._side, imageHref=self.imageHref) ]+C1 def drawOn(self, canv, x, y, _sW=0): if self._side=='left': Ix = x + self._ilpad Fx = Ix+ self._irpad + self._wI else: Ix = x + self.width-self._wI-self._irpad Fx = x self._I.drawOn(canv,Ix,y+self.height-self._itpad-self._hI) if self.imageHref: canv.linkURL(self.imageHref, (Ix, y+self.height-self._itpad-self._hI, Ix + self._wI, y+self.height), relative=1) if self._C0: _Container.drawOn(self, canv, Fx, y, content=self._C0, aW=self._iW) if self._C1: aW, aH = self._wrapArgs _Container.drawOn(self, canv, x, y-self._aH,content=self._C1, aW=aW) class _AbsRect(NullDraw): _ZEROSIZE=1 _SPACETRANSFER = True def __init__(self,x,y,width,height,strokeWidth=0,strokeColor=None,fillColor=None,strokeDashArray=None): self._x = x self._y = y self._width = width self._height = height self._strokeColor = strokeColor self._fillColor = fillColor self._strokeWidth = strokeWidth self._strokeDashArray = strokeDashArray def wrap(self, availWidth, availHeight): return 0,0 def drawOn(self, canv, x, y, _sW=0): if self._width>_FUZZ and self._height>_FUZZ: st = self._strokeColor and self._strokeWidth is not None and self._strokeWidth>=0 if st or self._fillColor: canv.saveState() if st: canv.setStrokeColor(self._strokeColor) canv.setLineWidth(self._strokeWidth) if self._fillColor: canv.setFillColor(self._fillColor) canv.rect(self._x,self._y,self._width,self._height,stroke=1 if st else 0, fill=1 if self._fillColor else 0) canv.restoreState() class _ExtendBG(NullDraw): _ZEROSIZE=1 _SPACETRANSFER = True def __init__(self,y,height,bg,frame): self._y = y self._height = height self._bg = bg def wrap(self, availWidth, availHeight): return 0,0 def frameAction(self, frame): bg = self._bg fby = self._y fbh = self._height fbgl = bg.left fbw = frame._width - fbgl - bg.right fbx = frame._x1 - fbgl canv = self.canv pn = canv.getPageNumber() bg.render(canv,frame,fbx,fby,fbw,fbh) class _AbsLine(NullDraw): _ZEROSIZE=1 _SPACETRANSFER = True def __init__(self,x,y,x1,y1,strokeWidth=0,strokeColor=None,strokeDashArray=None): self._x = x self._y = y self._x1 = x1 self._y1 = y1 self._strokeColor = strokeColor self._strokeWidth = strokeWidth self._strokeDashArray = strokeDashArray def wrap(self, availWidth, availHeight): return 0,0 def drawOn(self, canv, x, y, _sW=0): if self._strokeColor and self._strokeWidth is not None and self._strokeWidth>=0: canv.saveState() canv.setStrokeColor(self._strokeColor) canv.setLineWidth(self._strokeWidth) canv.line(self._x,self._y,self._x1,self._y1) canv.restoreState() class BalancedColumns(_FindSplitterMixin,NullDraw): '''combine a list of flowables and an Image''' def __init__(self, F, nCols=2, needed=72, spaceBefore=0, spaceAfter=0, showBoundary=None, leftPadding=None, innerPadding=None, rightPadding=None, topPadding=None, bottomPadding=None, name='', endSlack=0.1, boxStrokeColor=None, boxStrokeWidth=0, boxFillColor=None, boxMargin=None, vLinesStrokeColor=None, vLinesStrokeWidth=None, ): self.name = name or 'BalancedColumns-%d' % id(self) if nCols <2: raise ValueError('nCols should be at least 2 not %r in %s' % (nCols,self.identitity())) self._content = _flowableSublist(F) self._nCols = nCols self.spaceAfter = spaceAfter self._leftPadding = leftPadding self._innerPadding = innerPadding self._rightPadding = rightPadding self._topPadding = topPadding self._bottomPadding = bottomPadding self.spaceBefore = spaceBefore self._needed = needed - _FUZZ self.showBoundary = showBoundary self.endSlack = endSlack #what we might allow as a lastcolumn overrun self._boxStrokeColor = boxStrokeColor self._boxStrokeWidth = boxStrokeWidth self._boxFillColor = boxFillColor self._boxMargin = boxMargin self._vLinesStrokeColor = vLinesStrokeColor self._vLinesStrokeWidth = vLinesStrokeWidth def identity(self, maxLen=None): return "<%s nCols=%r at %s%s%s>" % (self.__class__.__name__, self._nCols, hex(id(self)), self._frameName(), getattr(self,'name','') and (' name="%s"'% getattr(self,'name','')) or '', ) def getSpaceAfter(self): return self.spaceAfter def getSpaceBefore(self): return self.spaceBefore def _generated_content(self,aW,aH): G = [] frame = self._frame from reportlab.platypus.doctemplate import LayoutError, ActionFlowable, Indenter from reportlab.platypus.frames import Frame from reportlab.platypus.doctemplate import FrameBreak lpad = frame._leftPadding if self._leftPadding is None else self._leftPadding rpad = frame._rightPadding if self._rightPadding is None else self._rightPadding tpad = frame._topPadding if self._topPadding is None else self._topPadding bpad = frame._bottomPadding if self._bottomPadding is None else self._bottomPadding leftExtraIndent = frame._leftExtraIndent rightExtraIndent = frame._rightExtraIndent gap = max(lpad,rpad) if self._innerPadding is None else self._innerPadding hgap = gap*0.5 canv = self.canv nCols = self._nCols cw = (aW - gap*(nCols-1) - lpad - rpad)/float(nCols) aH0 = aH aH -= tpad + bpad W,H0,_C0,C2 = self._findSplit(canv,cw,nCols*aH,paraFix=False) if not _C0: raise ValueError( "%s cannot make initial split aW=%r aH=%r ie cw=%r ah=%r\ncontent=%s" % ( self.identity(),aW,aH,cw,nCols*aH, [f.__class__.__name__ for f in self._content], )) _fres = {} def splitFunc(ah,endSlack=0): if ah not in _fres: c = [] w = 0 h = 0 cn = None icheck = nCols-2 if endSlack else -1 for i in range(nCols): wi, hi, c0, c1 = self._findSplit(canv,cw,ah,content=cn,paraFix=False) w = max(w,wi) h = max(h,hi) c.append(c0) if i==icheck: wc, hc, cc0, cc1 = self._findSplit(canv,cw,2*ah,content=c1,paraFix=False) if hc<=(1+endSlack)*ah: c.append(c1) h = ah-1e-6 cn = [] break cn = c1 _fres[ah] = ah+100000*int(cn!=[]),cn==[],(w,h,c,cn) return _fres[ah][2] endSlack = 0 if C2: H = aH else: #we are short so use H0 to figure out what to use import math def func(ah): splitFunc(ah) return _fres[ah][0] def gss(f, a, b, tol=1, gr=(math.sqrt(5) + 1) / 2): c = b - (b - a) / gr d = a + (b - a) / gr while abs(a - b) > tol: if f(c) < f(d): b = d else: a = c # we recompute both c and d here to avoid loss of precision which may lead to incorrect results or infinite loop c = b - (b - a) / gr d = a + (b - a) / gr F = [(x,tf,v) for x,tf,v in _fres.values() if tf] if F: F.sort() return F[0][2] return None H = min(int(H0/float(nCols)+self.spaceAfter*0.4),aH) splitFunc(H) if not _fres[H][1]: H = gss(func,H,aH) if H: W, H0, _C0, C2 = H H = H0 endSlack = False else: H = aH endSlack = self.endSlack else: H1 = H0/float(nCols) splitFunc(H1) if not _fres[H1][1]: H = gss(func,H,aH) if H: W, H0, _C0, C2 = H H = H0 endSlack = False else: H = aH endSlack = self.endSlack assert not C2, "unexpected non-empty C2" W1, H1, C, C1 = splitFunc(H, endSlack) _fres.clear() if C[0]==[] and C[1]==[] and C1: #no split situation C, C1 = [C1,C[1]], C[0] x1 = frame._x1 y1 = frame._y1 fw = frame._width ftop = y1+bpad+tpad+aH fh = H1 + bpad + tpad y2 = ftop - fh dx = aW / float(nCols) if leftExtraIndent or rightExtraIndent: indenter0 = Indenter(-leftExtraIndent,-rightExtraIndent) indenter1 = Indenter(leftExtraIndent,rightExtraIndent) else: indenter0 = indenter1 = None showBoundary=self.showBoundary if self.showBoundary is not None else frame.showBoundary obx = x1+leftExtraIndent+frame._leftPadding F = [Frame(obx+i*dx,y2,dx,fh, leftPadding=lpad if not i else hgap, bottomPadding=bpad, rightPadding=rpad if i==nCols-1 else hgap, topPadding=tpad, id='%s-%d' %(self.name,i), showBoundary=showBoundary, overlapAttachedSpace=frame._oASpace, _debug=frame._debug) for i in range(nCols)] #we are going to modify the current template T=self._doctemplateAttr('pageTemplate') if T is None: raise LayoutError('%s used in non-doctemplate environment' % self.identity()) BGs = getattr(frame,'_frameBGs',None) xbg = bg = BGs[-1] if BGs else None class TAction(ActionFlowable): '''a special Action flowable that sets stuff on the doc template T''' def __init__(self, bgs=[],F=[],f=None): Flowable.__init__(self) self.bgs = bgs self.F = F self.f = f def apply(self,doc,T=T): T.frames = self.F frame._frameBGs = self.bgs doc.handle_currentFrame(self.f.id) frame._frameBGs = self.bgs if bg: #G.append(Spacer(1e-5,1e-5)) #G[-1].__id__ = 'spacer0' xbg = _ExtendBG(y2,fh,bg,frame) G.append(xbg) oldFrames = T.frames G.append(TAction([],F,F[0])) if indenter0: G.append(indenter0) doBox = (self._boxStrokeColor and self._boxStrokeWidth and self._boxStrokeWidth>=0) or self._boxFillColor doVLines = self._vLinesStrokeColor and self._vLinesStrokeWidth and self._vLinesStrokeWidth>=0 if doBox or doVLines: obm = self._boxMargin if not obm: obm = (0,0,0,0) if len(obm)==1: obmt = obml = obmr = obmb = obm[0] elif len(obm)==2: obmt = obmb = obm[0] obml = obmr = obm[1] elif len(obm)==3: obmt = obm[0] obml = obmr = obm[1] obmb = obm[2] elif len(obm)==4: obmt = obm[0] obmr = obm[1] obmb = obm[2] obml = obm[3] else: raise ValueError('Invalid value %s for boxMargin' % repr(obm)) obx1 = obx - obml obx2 = F[-1]._x1+F[-1]._width + obmr oby2 = y2-obmb obh = fh+obmt+obmb oby1 = oby2+obh if doBox: box = _AbsRect(obx1,oby2, obx2-obx1, obh, fillColor=self._boxFillColor, strokeColor=self._boxStrokeColor, strokeWidth=self._boxStrokeWidth, ) if doVLines: vLines = [] for i in range(1,nCols): vlx = 0.5*(F[i]._x1 + F[i-1]._x1+F[i-1]._width) vLines.append(_AbsLine(vlx,oby2,vlx,oby1,strokeWidth=self._vLinesStrokeWidth,strokeColor=self._vLinesStrokeColor)) else: oby1 = ftop oby2 = y2 if doBox: G.append(box) if doVLines: G.extend(vLines) sa = self.getSpaceAfter() for i in range(nCols): Ci = C[i] if Ci: Ci = KeepInFrame(W1,H1,Ci,mode='shrink') sa = max(sa,Ci.getSpaceAfter()) G.append(Ci) if i!=nCols-1: G.append(FrameBreak) G.append(TAction(BGs,oldFrames,frame)) if xbg: if C1: sa = 0 xbg._y = min(y2,oby2) - sa xbg._height = max(ftop,oby1) - xbg._y if indenter1: G.append(indenter1) if C1: G.append( BalancedColumns(C1, nCols=nCols, needed=self._needed, spaceBefore=self.spaceBefore, spaceAfter=self.spaceAfter, showBoundary=self.showBoundary, leftPadding=self._leftPadding, innerPadding=self._innerPadding, rightPadding=self._rightPadding, topPadding=self._topPadding, bottomPadding=self._bottomPadding, name=self.name+'-1', endSlack=self.endSlack, boxStrokeColor=self._boxStrokeColor, boxStrokeWidth=self._boxStrokeWidth, boxFillColor=self._boxFillColor, boxMargin=self._boxMargin, vLinesStrokeColor=self._vLinesStrokeColor, vLinesStrokeWidth=self._vLinesStrokeWidth, ) ) return fh, G def wrap(self,aW,aH): #here's where we mess with everything self_frame = getattr(self,'_frame',None) if aH<self.spaceBefore+self._needed-_FUZZ: #we are going straight to the nextTemplate with no attempt to modify the frames G = [PageBreak(), self] H1 = 0 else: if not self_frame: #probably in some kind of container from reportlab.platypus.frames import Frame self._frame = Frame(0,0,aW,0x7fffffff,leftPadding=0, rightPadding=0, topPadding=0,bottomPadding=0) H1, G = self._generated_content(aW,aH) if not self_frame: del self._frame if self_frame: self_frame.add_generated_content(*G) return 0,min(H1,aH) class AnchorFlowable(Spacer): '''create a bookmark in the pdf''' _ZEROSIZE=1 _SPACETRANSFER = True def __init__(self,name): Spacer.__init__(self,0,0) self._name = name def __repr__(self): return "%s(%s)" % (self.__class__.__name__,self._name) def wrap(self,aW,aH): return 0,0 def draw(self): self.canv.bookmarkHorizontal(self._name,0,0) class _FBGBag(ABag): def matches(self,frame,canv): fid = id(frame) return ((isinstance(self.fid,list) and fid in self.fid or fid==self.fid) and id(canv)==self.cid and self.pn==canv.getPageNumber()) def getDims(self,canv): self._inst = canv._code[self.codePos].split() return map(float,self._inst[1:5]) def setDims(self,canv,x,y,w,h): self._inst[1:5] = [fp_str(x,y,w,h)] canv._code[self.codePos] = ' '.join(self._inst) def render(self,canv,frame,fbx,fby,fbw,fbh): if abs(fbw)>_FUZZ and abs(fbh)>_FUZZ: pn = canv.getPageNumber() if self.fid==id(frame) and self.cid==id(canv) and self.pn==pn: ox,oy,ow,oh = self.getDims(canv) self.setDims(canv,ox,fby,ow,oh+oy-fby) else: canv.saveState() fbgc = self.fillColor if fbgc: canv.setFillColor(fbgc) sw = self.strokeWidth sc = None if sw is None or sw<0 else self.strokeColor if sc: canv.setStrokeColor(sc) canv.setLineWidth(sw) da = self.strokeDashArray if da: canv.setDash(da) self.fid = id(frame) self.cid = id(canv) self.pn = pn self.codePos = len(canv._code) canv.rect(fbx,fby,fbw,fbh,stroke=1 if sc else 0,fill=1 if fbgc else 0) canv.restoreState() class FrameBG(AnchorFlowable): """Start or stop coloring the frame background left & right are distances from the edge of the frame to start stop colouring. if start in ('frame','frame-permanent') then the background is filled from here to the bottom of the frame and immediately discarded for the frame case. """ _ZEROSIZE=1 def __init__(self, color=None, left=0, right=0, start=True, strokeWidth=None, strokeColor=None, strokeDashArray=None): Spacer.__init__(self,0,0) self.start = start if start: from reportlab.platypus.doctemplate import _evalMeasurement self.left = _evalMeasurement(left) self.right = _evalMeasurement(right) self.color = color self.strokeWidth = strokeWidth self.strokeColor = strokeColor self.strokeDashArray = strokeDashArray def __repr__(self): return "%s(%s)" % (self.__class__.__name__,', '.join(['%s=%r' % (i,getattr(self,i,None)) for i in 'start color left right'.split()])) def draw(self): frame = getattr(self,'_frame',None) if frame is None: return if self.start: sc = self.strokeColor sw = self.strokeWidth sw = -1 if sw is None else sw frame._frameBGs.append( _FBGBag(left=self.left, right=self.right, fillColor=self.color, start=self.start if self.start in ('frame','frame-permanent') else None, strokeColor=self.strokeColor, strokeWidth=self.strokeWidth, strokeDashArray=self.strokeDashArray, fid = 0, cid = 0, pn = -1, codePos = None, active=True, )) elif frame._frameBGs: frame._frameBGs.pop() class FrameSplitter(NullDraw): '''When encountered this flowable should either switch directly to nextTemplate if remaining space in the current frame is less than gap+required or it should temporarily modify the current template to have the frames from nextTemplate that are listed in nextFrames and switch to the first of those frames. ''' _ZEROSIZE=1 def __init__(self, nextTemplate, nextFrames=[], gap=10, required=72, adjustHeight=True): self.nextTemplate = nextTemplate self.nextFrames = nextFrames or [] self.gap = gap self.required = required self.adjustHeight = adjustHeight def wrap(self,aW,aH): frame = self._frame from reportlab.platypus.doctemplate import NextPageTemplate,CurrentFrameFlowable,LayoutError G=[NextPageTemplate(self.nextTemplate)] if aH<self.gap+self.required-_FUZZ: #we are going straight to the nextTemplate with no attempt to modify the frames G.append(PageBreak()) else: #we are going to modify the incoming templates templates = self._doctemplateAttr('pageTemplates') if templates is None: raise LayoutError('%s called in non-doctemplate environment'%self.identity()) T=[t for t in templates if t.id==self.nextTemplate] if not T: raise LayoutError('%s.nextTemplate=%s not found' % (self.identity(),self.nextTemplate)) T=T[0] F=[f for f in T.frames if f.id in self.nextFrames] N=[f.id for f in F] N=[f for f in self.nextFrames if f not in N] if N: 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])) T=self._doctemplateAttr('pageTemplate') def unwrap(canv,doc,T=T,onPage=T.onPage,oldFrames=T.frames): T.frames=oldFrames T.onPage=onPage onPage(canv,doc) T.onPage=unwrap h=aH-self.gap for i,f in enumerate(F): f=copy(f) if self.adjustHeight: f.height=h f._reset() F[i]=f T.frames=F G.append(CurrentFrameFlowable(F[0].id)) frame.add_generated_content(*G) return 0,0 from reportlab.lib.sequencer import _type2formatter _bulletNames = dict( bulletchar=u'\u2022', #usually a small circle circle=u'\u25cf', #circle as high as the font square=u'\u25a0', disc=u'\u25cb', diamond=u'\u25c6', diamondwx=u'\u2756', rarrowhead=u'\u27a4', sparkle=u'\u2747', squarelrs=u'\u274f', blackstar=u'\u2605', ) def _bulletFormat(value,type='1',format=None): if type=='bullet': s = _bulletNames.get(value,value) else: s = _type2formatter[type](int(value)) if format: if isinstance(format,strTypes): s = format % s elif callable(format): s = format(s) else: raise ValueError('unexpected BulletDrawer format %r' % format) return s class BulletDrawer: def __init__(self, value='0', bulletAlign='left', bulletType='1', bulletColor='black', bulletFontName='Helvetica', bulletFontSize=12, bulletOffsetY=0, bulletDedent=0, bulletDir='ltr', bulletFormat=None, ): self.value = value self._bulletAlign = bulletAlign self._bulletType = bulletType self._bulletColor = bulletColor self._bulletFontName = bulletFontName self._bulletFontSize = bulletFontSize self._bulletOffsetY = bulletOffsetY self._bulletDedent = bulletDedent self._bulletDir = bulletDir self._bulletFormat = bulletFormat def drawOn(self,indenter,canv,x,y,_sW=0): value = self.value if not value: return canv.saveState() canv.translate(x, y) y = indenter.height-self._bulletFontSize+self._bulletOffsetY if self._bulletDir=='rtl': x = indenter.width - indenter._rightIndent + self._bulletDedent else: x = indenter._leftIndent - self._bulletDedent canv.setFont(self._bulletFontName,self._bulletFontSize) canv.setFillColor(self._bulletColor) bulletAlign = self._bulletAlign value = _bulletFormat(value,self._bulletType,self._bulletFormat) if bulletAlign=='left': canv.drawString(x,y,value) elif bulletAlign=='right': canv.drawRightString(x,y,value) elif bulletAlign in ('center','centre'): canv.drawCentredString(x,y,value) elif bulletAlign.startswith('numeric') or bulletAlign.startswith('decimal'): pc = bulletAlign[7:].strip() or '.' canv.drawAlignedString(x,y,value,pc) else: raise ValueError('Invalid bulletAlign: %r' % bulletAlign) canv.restoreState() def _computeBulletWidth(b,value): value = _bulletFormat(value,b._bulletType,b._bulletFormat) return stringWidth(value,b._bulletFontName,b._bulletFontSize) class DDIndenter(Flowable): _IndenterAttrs = '_flowable _leftIndent _rightIndent width height'.split() def __init__(self,flowable,leftIndent=0,rightIndent=0): self._flowable = flowable self._leftIndent = leftIndent self._rightIndent = rightIndent self.width = None self.height = None def split(self, aW, aH): S = self._flowable.split(aW-self._leftIndent-self._rightIndent, aH) return [ DDIndenter(s, leftIndent=self._leftIndent, rightIndent=self._rightIndent, ) for s in S ] def drawOn(self, canv, x, y, _sW=0): self._flowable.drawOn(canv,x+self._leftIndent,y,max(0,_sW-self._leftIndent-self._rightIndent)) def wrap(self, aW, aH): w,h = self._flowable.wrap(aW-self._leftIndent-self._rightIndent, aH) self.width = w+self._leftIndent+self._rightIndent self.height = h return self.width,h def __getattr__(self,a): if a in self._IndenterAttrs: try: return self.__dict__[a] except KeyError: if a not in ('spaceBefore','spaceAfter'): raise AttributeError(f'{self!r} has no attribute {a} dict={self.__dict__}') return getattr(self._flowable,a) def __setattr__(self,a,v): if a in self._IndenterAttrs: self.__dict__[a] = v else: setattr(self._flowable,a,v) def __delattr__(self,a): if a in self._IndenterAttrs: del self.__dict__[a] else: delattr(self._flowable,a) def identity(self,maxLen=None): return '%s containing %s' % (self.__class__.__name__,self._flowable.identity(maxLen)) class LIIndenter(DDIndenter): _IndenterAttrs = '_flowable _bullet _leftIndent _rightIndent width height spaceBefore spaceAfter'.split() def __init__(self,flowable,leftIndent=0,rightIndent=0,bullet=None, spaceBefore=None, spaceAfter=None): self._flowable = flowable self._bullet = bullet self._leftIndent = leftIndent self._rightIndent = rightIndent self.width = None self.height = None if spaceBefore is not None: self.spaceBefore = spaceBefore if spaceAfter is not None: self.spaceAfter = spaceAfter def split(self, aW, aH): S = self._flowable.split(aW-self._leftIndent-self._rightIndent, aH) return [ LIIndenter(s, leftIndent=self._leftIndent, rightIndent=self._rightIndent, bullet = (s is S[0] and self._bullet or None), ) for s in S ] def drawOn(self, canv, x, y, _sW=0): if self._bullet: self._bullet.drawOn(self,canv,x,y,0) self._flowable.drawOn(canv,x+self._leftIndent,y,max(0,_sW-self._leftIndent-self._rightIndent)) from reportlab.lib.styles import ListStyle class ListItem: def __init__(self, flowables, #the initial flowables style=None, #leftIndent=18, #rightIndent=0, #spaceBefore=None, #spaceAfter=None, #bulletType='1', #bulletColor='black', #bulletFontName='Helvetica', #bulletFontSize=12, #bulletOffsetY=0, #bulletDedent='auto', #bulletDir='ltr', #bulletFormat=None, **kwds ): if not isinstance(flowables,(list,tuple)): flowables = (flowables,) self._flowables = flowables params = self._params = {} if style: if not isinstance(style,ListStyle): raise ValueError('%s style argument (%r) not a ListStyle' % (self.__class__.__name__,style)) self._style = style for k in ListStyle.defaults: if k in kwds: v = kwds.get(k) elif style: v = getattr(style,k) else: continue params[k] = v for k in ('value', 'spaceBefore','spaceAfter'): v = kwds.get(k,getattr(style,k,None)) if v is not None: params[k] = v class _LIParams: def __init__(self,flowable,params,value,first): self.flowable = flowable self.params = params self.value = value self.first= first class ListFlowable(_Container,Flowable): _numberStyles = '1aAiI' def __init__(self, flowables, #the initial flowables start=None, style=None, #leftIndent=18, #rightIndent=0, #spaceBefore=None, #spaceAfter=None, #bulletType='1', #bulletColor='black', #bulletFontName='Helvetica', #bulletFontSize=12, #bulletOffsetY=0, #bulletDedent='auto', #bulletDir='ltr', #bulletFormat=None, **kwds ): self._flowables = flowables if style: if not isinstance(style,ListStyle): raise ValueError('%s style argument not a ListStyle' % self.__class__.__name__) self.style = style for k,v in ListStyle.defaults.items(): setattr(self,'_'+k,kwds.get(k,getattr(style,k,v))) for k in ('spaceBefore','spaceAfter'): v = kwds.get(k,getattr(style,k,None)) if v is not None: setattr(self,k,v) auto = False if start is None: start = getattr(self,'_start',None) if start is None: if self._bulletType=='bullet': start = 'bulletchar' auto = True else: start = self._bulletType auto = True if self._bulletType!='bullet': if auto: for v in start: if v not in self._numberStyles: raise ValueError('invalid start=%r or bullettype=%r' % (start,self._bulletType)) else: for v in self._bulletType: if v not in self._numberStyles: raise ValueError('invalid bullettype=%r' % self._bulletType) self._start = start self._auto = auto or isinstance(start,(list,tuple)) self._list_content = None self._dims = None self._caption = kwds.pop('caption',None) @property def _content(self): if self._list_content is None: self._list_content = self._getContent() del self._flowables return self._list_content def wrap(self,aW,aH): if self._dims!=aW: self.width, self.height = _listWrapOn(self._content,aW,self.canv) self._dims = aW return self.width,self.height def split(self,aW,aH): return self._content def _flowablesIter(self): for f in self._flowables: if isinstance(f,(list,tuple)): if f: for i, z in enumerate(f): yield i==0 and not isinstance(z,LIIndenter), z elif isinstance(f,ListItem): params = f._params if not params: #meerkat simples just a list like object for i, z in enumerate(f._flowables): if isinstance(z,LIIndenter): raise ValueError('LIIndenter not allowed in ListItem') yield i==0, z else: params = params.copy() value = params.pop('value',None) spaceBefore = params.pop('spaceBefore',None) spaceAfter = params.pop('spaceAfter',None) n = len(f._flowables) - 1 for i, z in enumerate(f._flowables): P = params.copy() if not i and spaceBefore is not None: P['spaceBefore'] = spaceBefore if i==n and spaceAfter is not None: P['spaceAfter'] = spaceAfter if i: value=None yield 0, _LIParams(z,P,value,i==0) else: yield not isinstance(f,LIIndenter), f def _makeLIIndenter(self,flowable, bullet, params=None): if params: leftIndent = params.get('leftIndent',self._leftIndent) rightIndent = params.get('rightIndent',self._rightIndent) spaceBefore = params.get('spaceBefore',None) spaceAfter = params.get('spaceAfter',None) return LIIndenter(flowable,leftIndent,rightIndent,bullet,spaceBefore=spaceBefore,spaceAfter=spaceAfter) else: return LIIndenter(flowable,self._leftIndent,self._rightIndent,bullet) def _makeBullet(self,value,params=None): if params is None: def getp(a): return getattr(self,'_'+a) else: style = getattr(params,'style',None) def getp(a): if a in params: return params[a] if style and a in style.__dict__: return getattr(self,a) return getattr(self,'_'+a) return BulletDrawer( value=value, bulletAlign=getp('bulletAlign'), bulletType=getp('bulletType'), bulletColor=getp('bulletColor'), bulletFontName=getp('bulletFontName'), bulletFontSize=getp('bulletFontSize'), bulletOffsetY=getp('bulletOffsetY'), bulletDedent=getp('calcBulletDedent'), bulletDir=getp('bulletDir'), bulletFormat=getp('bulletFormat'), ) def _getContent(self): bt = self._bulletType value = self._start if isinstance(value,(list,tuple)): values = value value = values[0] else: values = [value] autov = values[0] inc = int(bt in '1aAiI') if inc: try: value = int(value) except: value = 1 bd = self._bulletDedent if bd=='auto': align = self._bulletAlign dir = self._bulletDir if dir=='ltr' and align=='left': bd = self._leftIndent elif align=='right': bd = self._rightIndent else: #we need to work out the maximum width of any of the labels tvalue = value maxW = 0 for d,f in self._flowablesIter(): if d: maxW = max(maxW,_computeBulletWidth(self,tvalue)) if inc: tvalue += inc elif isinstance(f,LIIndenter): b = f._bullet if b: if b.bulletType==bt: maxW = max(maxW,_computeBulletWidth(b,b.value)) tvalue = int(b.value) else: maxW = max(maxW,_computeBulletWidth(self,tvalue)) if inc: tvalue += inc if dir=='ltr': if align=='right': bd = self._leftIndent - maxW else: bd = self._leftIndent - maxW*0.5 elif align=='left': bd = self._rightIndent - maxW else: bd = self._rightIndent - maxW*0.5 self._calcBulletDedent = bd S = [] aS = S.append i=0 for d,f in self._flowablesIter(): if isinstance(f,ListFlowable): fstart = f._start if isinstance(fstart,(list,tuple)): fstart = fstart[0] if fstart in values: #my kind of ListFlowable if f._auto: autov = values.index(autov)+1 f._start = values[autov:]+values[:autov] autov = f._start[0] if inc: f._bulletType = autov else: autov = fstart fparams = {} if not i: i += 1 spaceBefore = getattr(self,'spaceBefore',None) if spaceBefore is not None: fparams['spaceBefore'] = spaceBefore if d: aS(self._makeLIIndenter(f,bullet=self._makeBullet(value),params=fparams)) if inc: value += inc elif isinstance(f,LIIndenter): b = f._bullet if b: if b.bulletType!=bt: raise ValueError('Included LIIndenter bulletType=%s != OrderedList bulletType=%s' % (b.bulletType,bt)) value = int(b.value) else: f._bullet = self._makeBullet(value,params=getattr(f,'params',None)) if fparams: f.__dict__['spaceBefore'] = max(f.__dict__.get('spaceBefore',0),spaceBefore) aS(f) if inc: value += inc elif isinstance(f,_LIParams): fparams.update(f.params) z = self._makeLIIndenter(f.flowable,bullet=None,params=fparams) if f.first: if f.value is not None: value = f.value if inc: value = int(value) z._bullet = self._makeBullet(value,f.params) if inc: value += inc aS(z) else: aS(self._makeLIIndenter(f,bullet=None,params=fparams)) spaceAfter = getattr(self,'spaceAfter',None) if spaceAfter is not None: f=S[-1] f.__dict__['spaceAfter'] = max(f.__dict__.get('spaceAfter',0),spaceAfter) if self._caption: S.insert(0,self._caption) return S class TopPadder(Flowable): '''wrap a single flowable so that its first bit will be padded to fill out the space so that it appears at the bottom of its frame''' def __init__(self,f): self.__dict__['_TopPadder__f'] = f def wrap(self,aW,aH): w,h = self.__f.wrap(aW,aH) self.__dict__['_TopPadder__dh'] = aH-h return w,h def split(self,aW,aH): S = self.__f.split(aW,aH) if len(S)>1: S[0] = TopPadder(S[0]) return S def drawOn(self, canvas, x, y, _sW=0): self.__f.drawOn(canvas,x,y-max(0,self.__dh-1e-8),_sW) def __setattr__(self,a,v): setattr(self.__f,a,v) def __getattr__(self,a): return getattr(self.__f,a) def __delattr__(self,a): delattr(self.__f,a) class DocAssign(NullDraw): '''At wrap time this flowable evaluates var=expr in the doctemplate namespace''' _ZEROSIZE=1 def __init__(self,var,expr,life='forever'): Flowable.__init__(self) self.args = var,expr,life def funcWrap(self,aW,aH): NS=self._doctemplateAttr('_nameSpace') NS.update(dict(availableWidth=aW,availableHeight=aH)) try: return self.func() finally: for k in 'availableWidth','availableHeight': try: del NS[k] except: pass def func(self): return self._doctemplateAttr('d'+self.__class__.__name__[1:])(*self.args) def wrap(self,aW,aH): self.funcWrap(aW,aH) return 0,0 class DocExec(DocAssign): '''at wrap time exec stmt in doc._nameSpace''' def __init__(self,stmt,lifetime='forever'): Flowable.__init__(self) self.args=stmt,lifetime class DocPara(DocAssign): '''at wrap time create a paragraph with the value of expr as text if format is specified it should use %(__expr__)s for string interpolation of the expression expr (if any). It may also use %(name)s interpolations for other variables in the namespace. suitable defaults will be used if style and klass are None ''' def __init__(self,expr,format=None,style=None,klass=None,escape=True): Flowable.__init__(self) self.expr=expr self.format=format self.style=style self.klass=klass self.escape=escape def func(self): expr = self.expr if expr: if not isinstance(expr,str): expr = str(expr) return self._doctemplateAttr('docEval')(expr) def add_content(self,*args): self._doctemplateAttr('frame').add_generated_content(*args) def get_value(self,aW,aH): value = self.funcWrap(aW,aH) if self.format: NS=self._doctemplateAttr('_nameSpace').copy() NS.update(dict(availableWidth=aW,availableHeight=aH)) NS['__expr__'] = value value = self.format % NS else: value = str(value) return value def wrap(self,aW,aH): value = self.get_value(aW,aH) P = self.klass if not P: from reportlab.platypus.paragraph import Paragraph as P style = self.style if not style: from reportlab.lib.styles import getSampleStyleSheet style=getSampleStyleSheet()['Code'] if self.escape: from xml.sax.saxutils import escape value=escape(value) self.add_content(P(value,style=style)) return 0,0 class DocAssert(DocPara): def __init__(self,cond,format=None): Flowable.__init__(self) self.expr=cond self.format=format def funcWrap(self,aW,aH): self._cond = DocPara.funcWrap(self,aW,aH) return self._cond def wrap(self,aW,aH): value = self.get_value(aW,aH) if not bool(self._cond): raise AssertionError(value) return 0,0 class DocIf(DocPara): def __init__(self,cond,thenBlock,elseBlock=[]): Flowable.__init__(self) self.expr = cond self.blocks = elseBlock or [],thenBlock def checkBlock(self,block): if not isinstance(block,(list,tuple)): block = (block,) return block def wrap(self,aW,aH): self.add_content(*self.checkBlock(self.blocks[int(bool(self.funcWrap(aW,aH)))])) return 0,0 class DocWhile(DocIf): def __init__(self,cond,whileBlock): Flowable.__init__(self) self.expr = cond self.block = self.checkBlock(whileBlock) def wrap(self,aW,aH): if bool(self.funcWrap(aW,aH)): self.add_content(*(list(self.block)+[self])) return 0,0 class SetTopFlowables(NullDraw): _ZEROZSIZE = 1 def __init__(self,F,show=False): self._F = F self._show = show def wrap(self,aW,aH): doc = getattr(getattr(self,'canv',None),'_doctemplate',None) if doc: doc._topFlowables = self._F if self._show and self._F: doc.frame._generated_content = self._F return 0,0 class SetPageTopFlowables(NullDraw): _ZEROZSIZE = 1 def __init__(self,F,show=False): self._F = F self._show = show def wrap(self,aW,aH): doc = getattr(getattr(self,'canv',None),'_doctemplate',None) if doc: doc._pageTopFlowables = self._F if self._show and self._F: doc.frame._generated_content = self._F return 0,0