platypus: initial FrameFlowable implementation
authorrgbecker
Tue, 27 Sep 2005 13:26:12 +0000
changeset 2525 634ec1f48514
parent 2524 7e6da7c7fe2c
child 2526 eaaa012dffd9
platypus: initial FrameFlowable implementation
reportlab/platypus/doctemplate.py
reportlab/platypus/flowables.py
reportlab/platypus/frames.py
reportlab/test/test_platypus_pto.py
--- a/reportlab/platypus/doctemplate.py	Thu Sep 22 16:53:18 2005 +0000
+++ b/reportlab/platypus/doctemplate.py	Tue Sep 27 13:26:12 2005 +0000
@@ -29,10 +29,12 @@
 """
 
 from reportlab.platypus.flowables import *
+from reportlab.lib.units import inch
 from reportlab.platypus.paragraph import Paragraph
 from reportlab.platypus.frames import Frame
 from reportlab.rl_config import defaultPageSize, verbose
 import reportlab.lib.sequencer
+from reportlab.pdfgen import canvas
 
 from types import *
 import sys
@@ -153,21 +155,26 @@
         if type(n) is type(()): n = n[1]
     return n
 
-class Indenter(ActionFlowable):
+class FrameActionFlowable(Flowable):
+    def __init__(self,*arg,**kw):
+        raise NotImplementedError('Abstract Class')
+
+    def frameAction(self,frame):
+        raise NotImplementedError('Abstract Class')
+
+class Indenter(FrameActionFlowable):
     """Increases or decreases left and right margins of frame.
 
     This allows one to have a 'context-sensitive' indentation
     and makes nested lists way easier.
     """
-
     def __init__(self, left=0, right=0):
         self.left = _evalMeasurement(left)
         self.right = _evalMeasurement(right)
 
-    def apply(self, doc):
-        doc.frame._leftExtraIndent = doc.frame._leftExtraIndent + self.left
-        doc.frame._rightExtraIndent = doc.frame._rightExtraIndent + self.right
-
+    def frameAction(self, frame):
+        frame._leftExtraIndent += self.left
+        frame._rightExtraIndent += self.right
 
 class NextPageTemplate(ActionFlowable):
     """When you get to the next page, use the template specified (change to two column, for example)  """
--- a/reportlab/platypus/flowables.py	Thu Sep 22 16:53:18 2005 +0000
+++ b/reportlab/platypus/flowables.py	Tue Sep 27 13:26:12 2005 +0000
@@ -29,13 +29,14 @@
 from copy import deepcopy
 from types import ListType, TupleType, StringType
 
-from reportlab.pdfgen import canvas
-from reportlab.lib.units import inch
 from reportlab.lib.colors import red, gray, lightgrey
+from reportlab.lib.utils import fp_str
 from reportlab.pdfbase import pdfutils
 
-from reportlab.rl_config import defaultPageSize, _FUZZ
-PAGE_HEIGHT = defaultPageSize[1]
+from reportlab.rl_config import _FUZZ, overlapAttachedSpace
+__all__=('TraceInfo','Flowable','XBox','Preformatted','Image','Spacer','PageBreak','SlowPageBreak',
+        'CondPageBreak','KeepTogether','Macro','CallerMacro','ParagraphAndImage',
+        'FailOnWrap','HRFlowable','PTOContainer','FrameFlowable')
 
 
 class TraceInfo:
@@ -424,26 +425,28 @@
             return (availWidth, availHeight)
         return (0, 0)
 
-def _listWrapOn(F,availWidth,canv,mergeSpace=1):
+def _listWrapOn(F,availWidth,canv,mergeSpace=1,obj=None):
     '''return max width, required height for a list of flowables F'''
     W = 0
     H = 0
     pS = 0
-    n = len(F)
-    nm1 = n - 1
-    for i in xrange(n):
-        f = F[i]
+    atTop = 1
+    for f in F:
         w,h = f.wrapOn(canv,availWidth,0xfffffff)
+        if w<=_FUZZ or h<=_FUZZ: continue
         W = max(W,w)
-        H = H+h
-        if i:
+        H += h
+        if not atTop:
             h = f.getSpaceBefore()
             if mergeSpace: H += max(h-pS,0) 
             else: H += h
-        if i!=nm1:
-            pS = f.getSpaceAfter()
-            H += pS
-    return W, H
+        else:
+            if obj is not None: obj._spaceBefore = f.getSpaceBefore()
+            atTop = 0
+        pS = f.getSpaceAfter()
+        H += pS
+    if obj is not None: obj._spaceAfter = pS
+    return W, H-pS
 
 def _flowableSublist(V):
     "if it isn't a list or tuple, wrap it in a list"
@@ -611,7 +614,36 @@
         self.trailer = _flowableSublist(trailer)
         self.header = _flowableSublist(header)
 
-class PTOContainer(Flowable):
+class _Container:   #Abstract some common container like behaviour
+    def getSpaceBefore(self):
+        for c in self._content:
+            if not hasattr(c,'frameAction'):
+                return c.getSpaceBefore()
+        return 0
+
+    def getSpaceAfter(self):
+        for c in reversed(self._content):
+            if not hasattr(c,'frameAction'):
+                return c.getSpaceAfter()
+        return 0
+
+    def drawOn(self, canv, x, y, _sW=0,scale=1.0):
+        '''we simulate being added to a frame'''
+        pS = 0
+        aW = scale*(self.width+_sW)
+        C = self._content
+        y += self.height*scale
+        for c in C:
+            w, h = c.wrapOn(canv,aW,0xfffffff)
+            if w<_FUZZ or h<_FUZZ: continue
+            if c is not C[0]: h += max(c.getSpaceBefore()-pS,0)
+            y -= h
+            c.drawOn(canv,x,y,_sW=aW-w)
+            if c is not C[-1]:
+                pS = c.getSpaceAfter()
+                y -= pS
+
+class PTOContainer(_Container,Flowable):
     '''PTOContainer(contentList,trailerList,headerList)
     
     A container for flowables decorated with trailer & header lists.
@@ -632,12 +664,6 @@
         self.width, self.height = _listWrapOn(self._content,availWidth,self.canv)
         return self.width,self.height
 
-    def getSpaceBefore(self):
-        return self._content[0].getSpaceBefore()
-
-    def getSpaceAfter(self):
-        return self._content[-1].getSpaceAfter()
-
     def split(self, availWidth, availHeight):
         canv = self.canv
         C = self._content
@@ -685,17 +711,120 @@
             R2 = Hdr + C[i:]
         return R1 + [PTOContainer(R2,deepcopy(I.trailer),deepcopy(I.header))]
 
+#utility functions used by FrameFlowable
+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,(a,b)):
+    '''solve the model v = a/s**2 + b/s for an s which gives us v==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 FrameFlowable(_Container,Flowable):
+    def __init__(self, maxWidth, maxHeight, content=[], mergeSpace=None, mode=0, id=None):
+        '''mode describes the action to take when overflowing
+            0   raise an error in the normal way
+            1   ignore ie just draw it and report maxWidth, maxHeight
+            2   shrinkToFit
+        '''
+        self.id = id
+        self.maxWidth = maxWidth
+        self.maxHeight = maxHeight
+        self.mode = mode
+        assert mode in (0,1,2), '%s invalid mode value %s' % (self.identity(),mode)
+        if mergeSpace is None: mergeSpace = overlapAttachedSpace
+        self.mergespace = mergeSpace
+        self._content = content
+
+    def _getAvailableWidth(self):
+        return self.maxWidth - self._leftExtraIndent - self._rightExtraIndent
+
+    def identity(self, maxLen=None):
+        return "<%s at %d%s> size=%sx%s" % (self.__class__.__name__, id(self), self.id and ' id="%s"'%self.id, fp_str(self.maxWidth),fp_str(self.maxHeight))
+
+    def wrap(self,availWidth,availHeight):
+        mode = self.mode
+        maxWidth = float(self.maxWidth)
+        maxHeight = float(self.maxHeight)
+        W, H = _listWrapOn(self._content,availWidth,self.canv)
+        if mode==0 or (W<=maxWidth and H<=maxHeight):
+            self.width = W  #we take what we get
+            self.height = H
+        elif mode==1:   #we lie
+            self.width = min(maxWidth,W)-_FUZZ
+            self.height = min(maxHeight,H)-_FUZZ
+        else:
+            def func(x):
+                W, H = _listWrapOn(self._content,x*availWidth,self.canv)
+                W /= x
+                H /= x
+                return W, H
+            W0 = W
+            H0 = H
+            s0 = 1
+            if W>maxWidth:
+                #squeeze out the excess width
+                s1 = W/maxWidth
+                W, H = func(s1)
+                if H<=maxHeight:
+                    self.width = W
+                    self.height = H
+                    self._scale = s1
+                    return W,H
+                s0 = s1
+                H0 = H
+                W0 = W
+            s1 = H/maxHeight
+            W, H = func(s1)
+            self.width = W
+            self.height = H
+            self._scale = s1
+            if H<min(0.95*maxHeight,maxHeight-10):
+                #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:
+                        self.width = W
+                        self.height = H
+                        self._scale = s
+                        break
+
+        return self.width, self.height
+
     def drawOn(self, canv, x, y, _sW=0):
-        '''we simulate being added to a frame'''
-        pS = 0
-        aW = self.width+_sW
-        C = self._content
-        y += self.height
-        for c in C:
-            w, h = c.wrapOn(canv,aW,0xfffffff)
-            if c is not C[0]: h += max(c.getSpaceBefore()-pS,0)
-            y -= h
-            c.drawOn(canv,x,y,_sW=aW-w)
-            if c is not C[-1]:
-                pS = c.getSpaceAfter()
-                y -= pS
+        scale = getattr(self,'_scale',1.0)
+        if scale!=1.0:
+            canv.saveState()
+            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 scale!=1.0:
+            canv.restoreState()
--- a/reportlab/platypus/frames.py	Thu Sep 22 16:53:18 2005 +0000
+++ b/reportlab/platypus/frames.py	Tue Sep 27 13:26:12 2005 +0000
@@ -63,12 +63,6 @@
         self.__dict__['_rightPadding'] = rightPadding
         self.__dict__['_topPadding'] = topPadding
 
-        # these two should NOT be set on a frame.
-        # they are used when Indenter flowables want
-        # to adjust edges e.g. to do nested lists
-        self._leftExtraIndent = 0.0
-        self._rightExtraIndent = 0.0
-
         # if we want a boundary to be shown
         self.showBoundary = showBoundary
 
@@ -104,6 +98,12 @@
         self._atTop = 1
         self._prevASpace = 0
 
+        # these two should NOT be set on a frame.
+        # they are used when Indenter flowables want
+        # to adjust edges e.g. to do nested lists
+        self._leftExtraIndent = 0.0
+        self._rightExtraIndent = 0.0
+
     def _getAvailableWidth(self):
         return self._aW - self._leftExtraIndent - self._rightExtraIndent
 
@@ -113,6 +113,10 @@
         Raises a LayoutError if the object is too wide,
         or if it is too high for a totally empty frame,
         to avoid infinite loops"""
+        if getattr(flowable,'frameAction',None):
+            flowable.frameAction(self)
+            return 1
+
         y = self._y
         p = self._y1p
         s = 0
--- a/reportlab/test/test_platypus_pto.py	Thu Sep 22 16:53:18 2005 +0000
+++ b/reportlab/test/test_platypus_pto.py	Tue Sep 27 13:26:12 2005 +0000
@@ -8,7 +8,7 @@
 
 from reportlab.test import unittest
 from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation
-from reportlab.platypus.flowables import Flowable, PTOContainer
+from reportlab.platypus.flowables import Flowable, PTOContainer, FrameFlowable
 from reportlab.lib.units import cm
 from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
 from reportlab.lib.colors import toColor, black
@@ -28,7 +28,72 @@
     canvas.drawString(10*cm, cm, str(pageNumber))
     canvas.restoreState()
 
-def _breakingTestCase(self):
+def _showDoc(fn,story):
+    pageTemplate = PageTemplate('normal', [Frame(72, 440, 170, 284, id='F1'),
+                            Frame(326, 440, 170, 284, id='F2'),
+                            Frame(72, 72, 170, 284, id='F3'),
+                            Frame(326, 72, 170, 284, id='F4'),
+                            ], myMainPageFrame)
+    doc = BaseDocTemplate(outputfile(fn),
+            pageTemplates = pageTemplate,
+            showBoundary = 1,
+            )
+    doc.multiBuild(story)
+
+text2 ='''We have already seen that the natural general principle that will
+subsume this case cannot be arbitrary in the requirement that branching
+is not tolerated within the dominance scope of a complex symbol.
+Notice, incidentally, that the speaker-hearer's linguistic intuition is
+to be regarded as the strong generative capacity of the theory.  A
+consequence of the approach just outlined is that the descriptive power
+of the base component does not affect the structure of the levels of
+acceptability from fairly high (e.g. (99a)) to virtual gibberish (e.g.
+(98d)).  By combining adjunctions and certain deformations, a
+descriptively adequate grammar cannot be arbitrary in the strong
+generative capacity of the theory.'''
+
+text1='''
+On our assumptions, a descriptively adequate grammar delimits the strong
+generative capacity of the theory.  For one thing, the fundamental error
+of regarding functional notions as categorial is to be regarded as a
+corpus of utterance tokens upon which conformity has been defined by the
+paired utterance test.  A majority  of informed linguistic specialists
+agree that the appearance of parasitic gaps in domains relatively
+inaccessible to ordinary extraction is necessary to impose an
+interpretation on the requirement that branching is not tolerated within
+the dominance scope of a complex symbol.  It may be, then, that the
+speaker-hearer's linguistic intuition appears to correlate rather
+closely with the ultimate standard that determines the accuracy of any
+proposed grammar.  Analogously, the notion of level of grammaticalness
+may remedy and, at the same time, eliminate a general convention
+regarding the forms of the grammar.'''
+    
+text0 = '''To characterize a linguistic level L,
+this selectionally introduced contextual
+feature delimits the requirement that
+branching is not tolerated within the
+dominance scope of a complex
+symbol. Notice, incidentally, that the
+notion of level of grammaticalness
+does not affect the structure of the
+levels of acceptability from fairly high
+(e.g. (99a)) to virtual gibberish (e.g.
+(98d)). Suppose, for instance, that a
+subset of English sentences interesting
+on quite independent grounds appears
+to correlate rather closely with an
+important distinction in language use.
+Presumably, this analysis of a
+formative as a pair of sets of features is
+not quite equivalent to the system of
+base rules exclusive of the lexicon. We
+have already seen that the appearance
+of parasitic gaps in domains relatively
+inaccessible to ordinary extraction
+does not readily tolerate the strong
+generative capacity of the theory.'''
+
+def _ptoTestCase(self):
     "This makes one long multi-page paragraph."
 
     # Build story.
@@ -52,57 +117,6 @@
         if type(content) not in (type([]),type(())): content = [content]
         story.append(PTOContainer([Paragraph(blurb,H1)]+list(content),trailer,header))
 
-    text2 ='''We have already seen that the natural general principle that will
-subsume this case cannot be arbitrary in the requirement that branching
-is not tolerated within the dominance scope of a complex symbol.
-Notice, incidentally, that the speaker-hearer's linguistic intuition is
-to be regarded as the strong generative capacity of the theory.  A
-consequence of the approach just outlined is that the descriptive power
-of the base component does not affect the structure of the levels of
-acceptability from fairly high (e.g. (99a)) to virtual gibberish (e.g.
-(98d)).  By combining adjunctions and certain deformations, a
-descriptively adequate grammar cannot be arbitrary in the strong
-generative capacity of the theory.'''
-    text1='''
-On our assumptions, a descriptively adequate grammar delimits the strong
-generative capacity of the theory.  For one thing, the fundamental error
-of regarding functional notions as categorial is to be regarded as a
-corpus of utterance tokens upon which conformity has been defined by the
-paired utterance test.  A majority  of informed linguistic specialists
-agree that the appearance of parasitic gaps in domains relatively
-inaccessible to ordinary extraction is necessary to impose an
-interpretation on the requirement that branching is not tolerated within
-the dominance scope of a complex symbol.  It may be, then, that the
-speaker-hearer's linguistic intuition appears to correlate rather
-closely with the ultimate standard that determines the accuracy of any
-proposed grammar.  Analogously, the notion of level of grammaticalness
-may remedy and, at the same time, eliminate a general convention
-regarding the forms of the grammar.'''
-    
-    text0 = '''To characterize a linguistic level L,
-this selectionally introduced contextual
-feature delimits the requirement that
-branching is not tolerated within the
-dominance scope of a complex
-symbol. Notice, incidentally, that the
-notion of level of grammaticalness
-does not affect the structure of the
-levels of acceptability from fairly high
-(e.g. (99a)) to virtual gibberish (e.g.
-(98d)). Suppose, for instance, that a
-subset of English sentences interesting
-on quite independent grounds appears
-to correlate rather closely with an
-important distinction in language use.
-Presumably, this analysis of a
-formative as a pair of sets of features is
-not quite equivalent to the system of
-base rules exclusive of the lexicon. We
-have already seen that the appearance
-of parasitic gaps in domains relatively
-inaccessible to ordinary extraction
-does not readily tolerate the strong
-generative capacity of the theory.'''
     t0 = [ColorParagraph('blue','Please turn over', pto )]
     h0 = [ColorParagraph('blue','continued from previous page', pto )]
     t1 = [ColorParagraph('red','Please turn over(inner)', pto )]
@@ -134,30 +148,38 @@
     ptoblob('A long PTO',[Paragraph(text0+' '+text1,bt)],t0,h0)
     fbreak()
     ptoblob('2 PTO (inner split)',[ColorParagraph('pink',text0,bt),PTOContainer([ColorParagraph(black,'Inner Starts',H1),ColorParagraph('yellow',text2,bt),ColorParagraph('black','Inner Ends',H1)],t1,h1),ColorParagraph('magenta',text1,bt)],t0,h0)
+    _showDoc('test_platypus_pto.pdf',story)
 
-    pageTemplate = PageTemplate('normal', [Frame(2.5*cm, 15.5*cm, 6*cm, 10*cm, id='F1'),
-                            Frame(11.5*cm, 15.5*cm, 6*cm, 10*cm, id='F2'),
-                            Frame(2.5*cm, 2.5*cm, 6*cm, 10*cm, id='F3'),
-                            Frame(11.5*cm, 2.5*cm, 6*cm, 10*cm, id='F4'),
-                            ], myMainPageFrame)
-    doc = BaseDocTemplate(outputfile('test_platypus_pto.pdf'),
-            pageTemplates = pageTemplate,
-            showBoundary = 1,
-            )
-    doc.multiBuild(story)
+def _frameFlowableTestCase(self):
+    story = []
+    def fbreak(story=story):
+        story.append(FrameBreak())
+    styleSheet = getSampleStyleSheet()
+    H1 = styleSheet['Heading1']
+    H1.pageBreakBefore = 0
+    H1.keepWithNext = 0
+    bt = styleSheet['BodyText']
+    story.append(FrameFlowable(170-12,284-12,[Paragraph(text0,bt)],mode=2))
+    fbreak()
+    story.append(FrameFlowable(170-12,284-12,[Paragraph(text0,bt),Paragraph(text1,bt)],mode=2))
+    fbreak()
+    story.append(FrameFlowable(170-12,284-12,[Paragraph(text0,bt),Paragraph(text1,bt),Paragraph(text2,bt)],mode=2))
+    _showDoc('test_platypus_frameflowable.pdf',story)
 
-class BreakingTestCase(unittest.TestCase):
+class TestCases(unittest.TestCase):
     "Test multi-page splitting of paragraphs (eyeball-test)."
     def test0(self):
-        _breakingTestCase(self)
+        _ptoTestCase(self)
+    def test1(self):
+        _frameFlowableTestCase(self)
 
 def makeSuite():
-    return makeSuiteForClasses(BreakingTestCase)
+    return makeSuiteForClasses(TestCases)
 
 #noruntests
 if __name__ == "__main__": #NORUNTESTS
     if 'debug' in sys.argv:
-        _test0(None)
+        _frameFlowableTestCase(None)
     else:
         unittest.TextTestRunner().run(makeSuite())
         printLocation()