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