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/frames.py 4 5__version__='3.5.14' 6 7__doc__="""A frame is a container for content on a page. 8""" 9 10__all__ = ( 11 'ShowBoundaryValue', 12 'Frame', 13 ) 14 15import logging 16logger = logging.getLogger('reportlab.platypus') 17 18_geomAttr=('x1', 'y1', 'width', 'height', 'leftPadding', 'bottomPadding', 'rightPadding', 'topPadding') 19from reportlab import rl_config, isPy3 20from reportlab.lib.rl_accel import fp_str 21_FUZZ=rl_config._FUZZ 22 23class ShowBoundaryValue: 24 def __init__(self,color=(0,0,0),width=0.1,dashArray=None): 25 self.color = color 26 self.width = width 27 self.dashArray = dashArray 28 29 if isPy3: 30 def __bool__(self): 31 return self.color is not None and self.width>=0 32 else: 33 def __nonzero__(self): 34 return self.color is not None and self.width>=0 35 36 37class Frame: 38 ''' 39 A Frame is a piece of space in a document that is filled by the 40 "flowables" in the story. For example in a book like document most 41 pages have the text paragraphs in one or two frames. For generality 42 a page might have several frames (for example for 3 column text or 43 for text that wraps around a graphic). 44 45 After creation a Frame is not usually manipulated directly by the 46 applications program -- it is used internally by the platypus modules. 47 48 Here is a diagramatid abstraction for the definitional part of a Frame:: 49 50 width x2,y2 51 +---------------------------------+ 52 | l top padding r | h 53 | e +-------------------------+ i | e 54 | f | | g | i 55 | t | | h | g 56 | | | t | h 57 | p | | | t 58 | a | | p | 59 | d | | a | 60 | | | d | 61 | +-------------------------+ | 62 | bottom padding | 63 +---------------------------------+ 64 (x1,y1) <-- lower left corner 65 66 NOTE!! Frames are stateful objects. No single frame should be used in 67 two documents at the same time (especially in the presence of multithreading. 68 ''' 69 def __init__(self, x1, y1, width,height, leftPadding=6, bottomPadding=6, 70 rightPadding=6, topPadding=6, id=None, showBoundary=0, 71 overlapAttachedSpace=None,_debug=None): 72 self.id = id 73 self._debug = _debug 74 75 #these say where it goes on the page 76 self.__dict__['_x1'] = x1 77 self.__dict__['_y1'] = y1 78 self.__dict__['_width'] = width 79 self.__dict__['_height'] = height 80 81 #these create some padding. 82 self.__dict__['_leftPadding'] = leftPadding 83 self.__dict__['_bottomPadding'] = bottomPadding 84 self.__dict__['_rightPadding'] = rightPadding 85 self.__dict__['_topPadding'] = topPadding 86 87 # if we want a boundary to be shown 88 self.showBoundary = showBoundary 89 90 if overlapAttachedSpace is None: overlapAttachedSpace = rl_config.overlapAttachedSpace 91 self._oASpace = overlapAttachedSpace 92 self._geom() 93 self._reset() 94 95 def __getattr__(self,a): 96 if a in _geomAttr: return self.__dict__['_'+a] 97 raise AttributeError(a) 98 99 def __setattr__(self,a,v): 100 if a in _geomAttr: 101 self.__dict__['_'+a] = v 102 self._geom() 103 else: 104 self.__dict__[a] = v 105 106 def _saveGeom(self, **kwds): 107 if not self.__dict__.setdefault('_savedGeom',{}): 108 for ga in _geomAttr: 109 ga = '_'+ga 110 self.__dict__['_savedGeom'][ga] = self.__dict__[ga] 111 for k,v in kwds.items(): 112 setattr(self,k,v) 113 114 def _restoreGeom(self): 115 if self.__dict__.get('_savedGeom',None): 116 for ga in _geomAttr: 117 ga = '_'+ga 118 self.__dict__[ga] = self.__dict__[ga]['_savedGeom'] 119 del self.__dict__['_savedGeom'] 120 self._geom() 121 122 def _geom(self): 123 self._x2 = self._x1 + self._width 124 self._y2 = self._y1 + self._height 125 #efficiency 126 self._y1p = self._y1 + self._bottomPadding 127 #work out the available space 128 self._aW = self._x2 - self._x1 - self._leftPadding - self._rightPadding 129 self._aH = self._y2 - self._y1p - self._topPadding 130 131 def _reset(self): 132 self._restoreGeom() 133 #drawing starts at top left 134 self._x = self._x1 + self._leftPadding 135 self._y = self._y2 - self._topPadding 136 self._atTop = 1 137 self._prevASpace = 0 138 139 # these two should NOT be set on a frame. 140 # they are used when Indenter flowables want 141 # to adjust edges e.g. to do nested lists 142 self._leftExtraIndent = 0.0 143 self._rightExtraIndent = 0.0 144 145 def _getAvailableWidth(self): 146 return self._aW - self._leftExtraIndent - self._rightExtraIndent 147 148 def _add(self, flowable, canv, trySplit=0): 149 """ Draws the flowable at the current position. 150 Returns 1 if successful, 0 if it would not fit. 151 Raises a LayoutError if the object is too wide, 152 or if it is too high for a totally empty frame, 153 to avoid infinite loops""" 154 flowable._frame = self 155 flowable.canv = canv #so they can use stringWidth etc 156 try: 157 if getattr(flowable,'frameAction',None): 158 flowable.frameAction(self) 159 return 1 160 161 y = self._y 162 p = self._y1p 163 s = 0 164 aW = self._getAvailableWidth() 165 zeroSize = getattr(flowable,'_ZEROSIZE',False) 166 if not self._atTop: 167 s =flowable.getSpaceBefore() 168 if self._oASpace: 169 if getattr(flowable,'_SPACETRANSFER',False) or zeroSize: 170 s = self._prevASpace 171 s = max(s-self._prevASpace,0) 172 h = y - p - s 173 if h>0 or zeroSize: 174 w, h = flowable.wrap(aW, h) 175 else: 176 return 0 177 178 h += s 179 y -= h 180 181 if y < p-_FUZZ: 182 if not rl_config.allowTableBoundsErrors and ((h>self._aH or w>aW) and not trySplit): 183 from reportlab.platypus.doctemplate import LayoutError 184 raise LayoutError("Flowable %s (%sx%s points) too large for frame (%sx%s points)." % ( 185 flowable.__class__, w,h, aW,self._aH)) 186 return 0 187 else: 188 #now we can draw it, and update the current point. 189 sa = flowable.getSpaceAfter() 190 fbg = getattr(self,'_frameBGs',None) 191 if fbg and fbg[-1].active: 192 bg = fbg[-1] 193 fbgl = bg.left 194 fbgr = bg.right 195 bgm = bg.start 196 fbw = self._width-fbgl-fbgr 197 fbx = self._x1+fbgl 198 if not bgm: 199 fbh = y + h + sa 200 fby = max(p,y-sa) 201 fbh = max(0,fbh-fby) 202 else: 203 fbh = y + h - s 204 att = fbh>=self._y2 - self._topPadding 205 if bgm=='frame' or bgm=='frame-permanent' or (att and bgm=='frame-permanent-1'): 206 #first time or att top use 207 fbh = max(0,(self._y2 if att else fbh)-self._y1) 208 fby = self._y1 209 if bgm=='frame-permanent': 210 fbg[-1].start = 'frame-permanent-1' 211 else: 212 fby = fbw = fbh = 0 213 bg.render(canv,self,fbx,fby,fbw,fbh) 214 if bgm=='frame': 215 fbg.pop() 216 217 flowable.drawOn(canv, self._x + self._leftExtraIndent, y, _sW=aW-w) 218 flowable.canv=canv 219 if self._debug: logger.debug('drew %s' % flowable.identity()) 220 y -= sa 221 if self._oASpace: 222 if getattr(flowable,'_SPACETRANSFER',False): 223 sa = self._prevASpace 224 self._prevASpace = sa 225 if y!=self._y: self._atTop = 0 226 self._y = y 227 return 1 228 finally: 229 #sometimes canv/_frame aren't still on the flowable 230 for a in ('canv', '_frame'): 231 if hasattr(flowable,a): 232 delattr(flowable,a) 233 234 add = _add 235 236 def split(self,flowable,canv): 237 '''Ask the flowable to split using up the available space.''' 238 y = self._y 239 p = self._y1p 240 s = 0 241 if not self._atTop: 242 s = flowable.getSpaceBefore() 243 if self._oASpace: 244 s = max(s-self._prevASpace,0) 245 h = y-p-s 246 if h<=0 and not getattr(flowable,'_ZEROSIZE',False): 247 return [] 248 flowable._frame = self #some flowables might need these 249 flowable.canv = canv 250 try: 251 r = flowable.split(self._aW, h) 252 finally: 253 #sometimes canv/_frame aren't still on the flowable 254 for a in ('canv', '_frame'): 255 if hasattr(flowable,a): 256 delattr(flowable,a) 257 return r 258 259 260 @staticmethod 261 def _drawBoundary(canv,sb,x1,y1,width,height): 262 "draw the frame boundary as a rectangle (primarily for debugging)." 263 from reportlab.lib.colors import Color, toColor 264 ss = isinstance(sb,(str,tuple,list)) or isinstance(sb,Color) 265 w = -1 266 da = None 267 if ss: 268 c = toColor(sb,-1) 269 ss = c != -1 270 elif isinstance(sb,ShowBoundaryValue) and sb: 271 c = toColor(sb.color,-1) 272 ss = c != -1 273 if ss: 274 w = sb.width 275 da = sb.dashArray 276 if ss: 277 canv.saveState() 278 canv.setStrokeColor(c) 279 if w>=0: canv.setLineWidth(w) 280 if da: canv.setDash(da) 281 canv.rect(x1,y1,width,height) 282 if ss: canv.restoreState() 283 284 def drawBoundary(self,canv, __boundary__=None): 285 self._drawBoundary(canv,__boundary__ or self.showBoundary, self._x1, self._y1, 286 self._x2 - self._x1, self._y2 - self._y1) 287 288 def addFromList(self, drawlist, canv): 289 """Consumes objects from the front of the list until the 290 frame is full. If it cannot fit one object, raises 291 an exception.""" 292 293 if self._debug: logger.debug("enter Frame.addFromlist() for frame %s" % self.id) 294 if self.showBoundary: 295 self.drawBoundary(canv) 296 297 while len(drawlist) > 0: 298 head = drawlist[0] 299 if self.add(head,canv,trySplit=0): 300 del drawlist[0] 301 else: 302 #leave it in the list for later 303 break 304 305 def add_generated_content(self,*C): 306 self.__dict__.setdefault('_generated_content',[]).extend(C) 307 308 def _aSpaceString(self): 309 return '(%s x %s%s)' % (self._getAvailableWidth(),self._aH,self._atTop and '*' or '') 310