improvements to keepTogether & added peek frame/template methods; issue started by Андрија Зарић lisandrija @ bitbucket.org
authorrptlab
Mon, 29 Jan 2018 13:54:43 +0000
changeset 4380 831ee16f2338
parent 4379 01b9c20f5204
child 4381 3540d9f17e20
improvements to keepTogether & added peek frame/template methods; issue started by Андрија Зарић lisandrija @ bitbucket.org
src/reportlab/__init__.py
src/reportlab/platypus/doctemplate.py
src/reportlab/platypus/flowables.py
tests/test_platypus_breaking.py
--- a/src/reportlab/__init__.py	Wed Jan 24 16:31:39 2018 +0000
+++ b/src/reportlab/__init__.py	Mon Jan 29 13:54:43 2018 +0000
@@ -1,9 +1,9 @@
 #Copyright ReportLab Europe Ltd. 2000-2017
 #see license.txt for license details
 __doc__="""The Reportlab PDF generation library."""
-Version = "3.4.23"
+Version = "3.4.24"
 __version__=Version
-__date__='20180109'
+__date__='20180129'
 
 import sys, os
 
--- a/src/reportlab/platypus/doctemplate.py	Wed Jan 24 16:31:39 2018 +0000
+++ b/src/reportlab/platypus/doctemplate.py	Mon Jan 29 13:54:43 2018 +0000
@@ -486,6 +486,7 @@
                     'artBox': None,
                     'trimBox': None,
                     'bleedBox': None,
+                    'keepTogetherClass': KeepTogether,
                     }
     _invalidInitArgs = ()
     _firstPageTemplateIndex = 0
@@ -677,19 +678,20 @@
         self._rightExtraIndent = self.frame._rightExtraIndent
         self._frameBGs = self.frame._frameBGs
 
-        f = self.frame
         if hasattr(self,'_nextFrameIndex'):
             self.frame = self.pageTemplate.frames[self._nextFrameIndex]
             self.frame._debug = self._debug
             del self._nextFrameIndex
             self.handle_frameBegin(resume)
-        elif hasattr(f,'lastFrame') or f is self.pageTemplate.frames[-1]:
-            self.handle_pageEnd()
-            self.frame = None
         else:
-            self.frame = self.pageTemplate.frames[self.pageTemplate.frames.index(f) + 1]
-            self.frame._debug = self._debug
-            self.handle_frameBegin()
+            f = self.frame
+            if hasattr(f,'lastFrame') or f is self.pageTemplate.frames[-1]:
+                self.handle_pageEnd()
+                self.frame = None
+            else:
+                self.frame = self.pageTemplate.frames[self.pageTemplate.frames.index(f) + 1]
+                self.frame._debug = self._debug
+                self.handle_frameBegin()
 
     def handle_nextPageTemplate(self,pt):
         '''On endPage change to the page template with name or index pt'''
@@ -728,6 +730,56 @@
         else:
             raise TypeError("argument pt should be string or integer or list")
 
+    def _peekNextPageTemplate(self,pt):
+        if isinstance(pt,strTypes):
+            for t in self.pageTemplates:
+                if t.id == pt:
+                    return t
+            raise ValueError("can't find template('%s')"%pt)
+        elif isinstance(pt,int):
+            self.pageTemplates[pt]
+        elif isSeq(pt):
+            #used for alternating left/right pages
+            #collect the refs to the template objects, complain if any are bad
+            c = PTCycle()
+            for ptn in pt:
+                found = 0
+                if ptn=='*':    #special case name used to short circuit the iteration
+                    c._restart = len(c)
+                    continue
+                for t in self.pageTemplates:
+                    if t.id == ptn:
+                        c.append(t)
+                        found = 1
+                if not found:
+                    raise ValueError("Cannot find page template called %s" % ptn)
+            if not c:
+                raise ValueError("No valid page templates in cycle")
+            elif c._restart>len(c):
+                raise ValueError("Invalid cycle restart position")
+            return c.peek
+        else:
+            raise TypeError("argument pt should be string or integer or list")
+
+    def _peekNextFrame(self):
+        '''intended to be used by extreme flowables'''
+        if hasattr(self,'_nextFrameIndex'):
+            return self.pageTemplate.frames[self._nextFrameIndex]
+        f = self.frame
+        if hasattr(f,'lastFrame') or f is self.pageTemplate.frames[-1]:
+            if hasattr(self,'_nextPageTemplateCycle'):
+                #they are cycling through pages'; we keep the index
+                pageTemplate = self._nextPageTemplateCycle.peek
+            elif hasattr(self,'_nextPageTemplateIndex'):
+                pageTemplate = self.pageTemplates[self._nextPageTemplateIndex]
+            elif self.pageTemplate.autoNextPageTemplate:
+                pageTemplate = self._peekNextPageTemplate(self.pageTemplate.autoNextPageTemplate)
+            else:
+                pageTemplate = self.pageTemplate
+            return pageTemplate.frames[0]
+        else:
+            return self.pageTemplate.frames[self.pageTemplate.frames.index(f) + 1]
+
     def handle_nextFrame(self,fx,resume=0):
         '''On endFrame change to the frame with name or index fx'''
         if isinstance(fx,strTypes):
@@ -781,7 +833,7 @@
         while i<n and flowables[i].getKeepWithNext() and _ktAllow(flowables[i]): i += 1
         if i:
             if i<n and _ktAllow(flowables[i]): i += 1
-            K = KeepTogether(flowables[:i])
+            K = self.keepTogetherClass(flowables[:i])
             mbe = getattr(self,'_multiBuildEdits',None)
             if mbe:
                 for f in K._content[:-1]:
--- a/src/reportlab/platypus/flowables.py	Wed Jan 24 16:31:39 2018 +0000
+++ b/src/reportlab/platypus/flowables.py	Mon Jan 29 13:54:43 2018 +0000
@@ -669,10 +669,20 @@
         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
+            from reportlab.lib.utils import annotateException
+            KeepTogether.NullActionFlowable = NullActionFlowable
+            KeepTogether.FrameBreak = FrameBreak
+            KeepTogether.annotateException = annotateException
+
         if not flowables:
-            from reportlab.platypus.doctemplate import NullActionFlowable
-            flowables = [NullActionFlowable()]
+            flowables = [self.NullActionFlowable()]
         self._content = _flowableSublist(flowables)
         self._maxHeight = maxHeight
 
@@ -688,8 +698,7 @@
         try:
             W,H = _listWrapOn(self._content,aW,self.canv,dims=dims)
         except:
-            from reportlab.lib.utils import annotateException
-            annotateException('\nraised by class %s(%s)@0x%8.8x wrap\n' % (self.__class__.__name__,self.__class__.__module__,id(self)))
+            self.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
@@ -698,18 +707,23 @@
     def split(self, aW, aH):
         if getattr(self,'_wrapInfo',None)!=(aW,aH): self.wrap(aW,aH)
         S = self._content[:]
-        atTop = getattr(self,'_frame',None)
-        if atTop: atTop = getattr(atTop,'_atTop',None)
+        cf = getattr(self,'_frame',None)
+        if cf: atTop = getattr(cf,'_atTop',None)
         C0 = self._H>aH and (not self._maxHeight or aH>self._maxHeight)
         C1 = (self._H0>aH) or C0 and atTop
         if C0 or C1:
-            if C0:
-                from reportlab.platypus.doctemplate import FrameBreak
-                A = FrameBreak
+            fb = False
+            if C0 and not (self.splitAtTop and atTop):
+                fb = True
             else:
-                from reportlab.platypus.doctemplate import NullActionFlowable
-                A = NullActionFlowable
-            S.insert(0,A())
+                panf = self._doctemplateAttr('_peekNextFrame')
+                if cf and panf:
+                    nf = panf()
+                    nAW = nf._width
+                    nAH = nf._height
+                    if 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):
@@ -719,6 +733,13 @@
         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
--- a/tests/test_platypus_breaking.py	Wed Jan 24 16:31:39 2018 +0000
+++ b/tests/test_platypus_breaking.py	Mon Jan 29 13:54:43 2018 +0000
@@ -8,7 +8,7 @@
 import sys, os, time, re
 from operator import truth
 import unittest
-from reportlab.platypus.flowables import Flowable
+from reportlab.platypus.flowables import Flowable, KeepTogether, KeepTogetherSplitAtTop
 from reportlab.lib import colors
 from reportlab.lib.units import cm
 from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
@@ -62,98 +62,106 @@
 def _test0(self):
     "This makes one long multi-page paragraph."
 
-    # Build story.
-    story = []
-    a = story.append
+    def RT(k,theme='PYTHON',sentences=1,cache={}):
+        if k not in cache:
+            cache[k] = randomText(theme=theme,sentences=sentences)
+        return cache[k]
 
-    styleSheet = getSampleStyleSheet()
-    h1 = styleSheet['Heading1']
-    h1.pageBreakBefore = 1
-    h1.keepWithNext = 1
+    # Build story.
+    def makeStory():
+        story = []
+        a = story.append
 
-    h2 = styleSheet['Heading2']
-    h2.frameBreakBefore = 1
-    h2.keepWithNext = 1
+        styleSheet = getSampleStyleSheet()
+        h1 = styleSheet['Heading1']
+        h1.pageBreakBefore = 1
+        h1.keepWithNext = 1
 
-    h3 = styleSheet['Heading3']
-    h3.backColor = colors.cyan
-    h3.keepWithNext = 1
+        h2 = styleSheet['Heading2']
+        h2.frameBreakBefore = 1
+        h2.keepWithNext = 1
 
-    bt = styleSheet['BodyText']
-    btj = ParagraphStyle('bodyText1j',parent=bt,alignment=TA_JUSTIFY)
-    btr = ParagraphStyle('bodyText1r',parent=bt,alignment=TA_RIGHT)
-    btc = ParagraphStyle('bodyText1c',parent=bt,alignment=TA_CENTER)
-    from reportlab.lib.utils import TimeStamp
-    ts = TimeStamp()
-    a(Paragraph("""
-        <a name='top'/>Subsequent pages test pageBreakBefore, frameBreakBefore and
-        keepTogether attributes.  Generated at %s.  The number in brackets
-        at the end of each paragraph is its position in the story. (%d)""" % (
-            ts.asctime, len(story)), bt))
+        h3 = styleSheet['Heading3']
+        h3.backColor = colors.cyan
+        h3.keepWithNext = 1
 
-    for i in range(10):
-        a(Paragraph('Heading 1 always starts a new page (%d)' % len(story), h1))
-        for j in range(3):
-            a(Paragraph('Heading1 paragraphs should always'
-                            'have a page break before.  Heading 2 on the other hand'
-                            'should always have a FRAME break before (%d)' % len(story), bt))
-            a(Paragraph('Heading 2 always starts a new frame (%d)' % len(story), h2))
-            a(Paragraph('Heading1 paragraphs should always'
-                            'have a page break before.  Heading 2 on the other hand'
-                            'should always have a FRAME break before (%d)' % len(story), bt))
+        bt = styleSheet['BodyText']
+        btj = ParagraphStyle('bodyText1j',parent=bt,alignment=TA_JUSTIFY)
+        btr = ParagraphStyle('bodyText1r',parent=bt,alignment=TA_RIGHT)
+        btc = ParagraphStyle('bodyText1c',parent=bt,alignment=TA_CENTER)
+        from reportlab.lib.utils import TimeStamp
+        ts = TimeStamp()
+        a(Paragraph("""
+            <a name='top'/>Subsequent pages test pageBreakBefore, frameBreakBefore and
+            keepTogether attributes.  Generated at %s.  The number in brackets
+            at the end of each paragraph is its position in the story. (%d)""" % (
+                ts.asctime, len(story)), bt))
+
+        for i in range(10):
+            a(Paragraph('Heading 1 always starts a new page (%d)' % len(story), h1))
             for j in range(3):
-                a(Paragraph(randomText(theme=PYTHON, sentences=2)+' (%d)' % len(story), bt))
-                a(Paragraph('I should never be at the bottom of a frame (%d)' % len(story), h3))
-                a(Paragraph(randomText(theme=PYTHON, sentences=1)+' (%d)' % len(story), bt))
+                a(Paragraph('Heading1 paragraphs should always'
+                                'have a page break before.  Heading 2 on the other hand'
+                                'should always have a FRAME break before (%d)' % len(story), bt))
+                a(Paragraph('Heading 2 always starts a new frame (%d)' % len(story), h2))
+                a(Paragraph('Heading1 paragraphs should always'
+                                'have a page break before.  Heading 2 on the other hand'
+                                'should always have a FRAME break before (%d)' % len(story), bt))
+                for j in range(3):
+                    a(Paragraph(RT((i,j,0),theme=PYTHON, sentences=2)+' (%d)' % len(story), bt))
+                    a(Paragraph('I should never be at the bottom of a frame (%d)' % len(story), h3))
+                    a(Paragraph(RT((i,j,1),theme=PYTHON, sentences=1)+' (%d)' % len(story), bt))
 
-    for align,bts in [('left',bt),('JUSTIFIED',btj),('RIGHT',btr),('CENTER',btc)]:
-        a(Paragraph('Now we do &lt;br/&gt; tests(align=%s)' % align, h1))
-        a(Paragraph('First off no br tags',h3))
-        a(Paragraph(_text1,bts))
-        a(Paragraph("&lt;br/&gt; after 'the' in line 4",h3))
-        a(Paragraph(_text1.replace('forms of the','forms of the<br/>',1),bts))
-        a(Paragraph("2*&lt;br/&gt; after 'the' in line 4",h3))
-        a(Paragraph(_text1.replace('forms of the','forms of the<br/><br/>',1),bts))
-        a(Paragraph("&lt;br/&gt; after 'I suggested ' in line 5",h3))
-        a(Paragraph(_text1.replace('I suggested ','I suggested<br/>',1),bts))
-        a(Paragraph("2*&lt;br/&gt; after 'I suggested ' in line 5",h3))
-        a(Paragraph(_text1.replace('I suggested ','I suggested<br/><br/>',1),bts))
-        a(Paragraph("&lt;br/&gt; at the end of the paragraph!",h3))
-        a(Paragraph("""text one<br/>text two<br/>""",bts))
-        a(Paragraph("Border with &lt;br/&gt; at the end of the paragraph!",h3))
-        bt1 = ParagraphStyle('bodyText1',bts)
-        bt1.borderWidth = 0.5
-        bt1.borderColor = colors.toColor('red')
-        bt1.backColor = colors.pink
-        bt1.borderRadius = 2
-        bt1.borderPadding = 3
-        a(Paragraph("""text one<br/>text two<br/>""",bt1))
-        a(Paragraph("Border no &lt;br/&gt; at the end of the paragraph!",h3))
-        bt1 = ParagraphStyle('bodyText1',bts)
-        bt1.borderWidth = 0.5
-        bt1.borderColor = colors.toColor('red')
-        bt1.backColor = colors.pink
-        bt1.borderRadius = 2
-        bt1.borderPadding = 3
-        a(Paragraph("""text one<br/>text two""",bt1))
-        a(Paragraph("Different border style!",h3))
-        bt2 = ParagraphStyle('bodyText1',bt1)
-        bt2.borderWidth = 1.5
-        bt2.borderColor = colors.toColor('blue')
-        bt2.backColor = colors.gray
-        bt2.borderRadius = 3
-        bt2.borderPadding = 3
-        a(Paragraph("""text one<br/>text two<br/>""",bt2))
-    for i in 0, 1, 2:
-        P = Paragraph("""This is a paragraph with <font color='blue'><a href='#top'>with an incredibly
+        for align,bts in [('left',bt),('JUSTIFIED',btj),('RIGHT',btr),('CENTER',btc)]:
+            a(Paragraph('Now we do &lt;br/&gt; tests(align=%s)' % align, h1))
+            a(Paragraph('First off no br tags',h3))
+            a(Paragraph(_text1,bts))
+            a(Paragraph("&lt;br/&gt; after 'the' in line 4",h3))
+            a(Paragraph(_text1.replace('forms of the','forms of the<br/>',1),bts))
+            a(Paragraph("2*&lt;br/&gt; after 'the' in line 4",h3))
+            a(Paragraph(_text1.replace('forms of the','forms of the<br/><br/>',1),bts))
+            a(Paragraph("&lt;br/&gt; after 'I suggested ' in line 5",h3))
+            a(Paragraph(_text1.replace('I suggested ','I suggested<br/>',1),bts))
+            a(Paragraph("2*&lt;br/&gt; after 'I suggested ' in line 5",h3))
+            a(Paragraph(_text1.replace('I suggested ','I suggested<br/><br/>',1),bts))
+            a(Paragraph("&lt;br/&gt; at the end of the paragraph!",h3))
+            a(Paragraph("""text one<br/>text two<br/>""",bts))
+            a(Paragraph("Border with &lt;br/&gt; at the end of the paragraph!",h3))
+            bt1 = ParagraphStyle('bodyText1',bts)
+            bt1.borderWidth = 0.5
+            bt1.borderColor = colors.toColor('red')
+            bt1.backColor = colors.pink
+            bt1.borderRadius = 2
+            bt1.borderPadding = 3
+            a(Paragraph("""text one<br/>text two<br/>""",bt1))
+            a(Paragraph("Border no &lt;br/&gt; at the end of the paragraph!",h3))
+            bt1 = ParagraphStyle('bodyText1',bts)
+            bt1.borderWidth = 0.5
+            bt1.borderColor = colors.toColor('red')
+            bt1.backColor = colors.pink
+            bt1.borderRadius = 2
+            bt1.borderPadding = 3
+            a(Paragraph("""text one<br/>text two""",bt1))
+            a(Paragraph("Different border style!",h3))
+            bt2 = ParagraphStyle('bodyText1',bt1)
+            bt2.borderWidth = 1.5
+            bt2.borderColor = colors.toColor('blue')
+            bt2.backColor = colors.gray
+            bt2.borderRadius = 3
+            bt2.borderPadding = 3
+            a(Paragraph("""text one<br/>text two<br/>""",bt2))
+        for i in 0, 1, 2:
+            P = Paragraph("""This is a paragraph with <font color='blue'><a href='#top'>with an incredibly
 long and boring link in side of it that
 contains lots and lots of stupidly boring and worthless information.
 So that we can split the link and see if we get problems like Dinu's.
 I hope we don't, but you never do Know.</a></font>""",bt)
-        a(P)
+            a(P)
+        return story
 
-    doc = MyDocTemplate(outputfile('test_platypus_breaking.pdf'))
-    doc.multiBuild(story)
+    for sfx,klass in (('',KeepTogether),('_ktsat',KeepTogetherSplitAtTop)):
+        doc = MyDocTemplate(outputfile('test_platypus_breaking%s.pdf'%sfx),keepTogetherClass=klass)
+        doc.multiBuild(makeStory())
 
 class BreakingTestCase(unittest.TestCase):
     "Test multi-page splitting of paragraphs (eyeball-test)."