Re: Patch to support tables with oversize cells

Lennart Regebro via reportlab-users <[email protected]> Tue, 12 Apr 2022 12:12:59 +0200
Newsgroups gmane.comp.python.reportlab.user
Message-ID <CAHT-kBdnH6WFwDiat-=vfa9f7MSgRwOKFdJpYkZXMWqAFyx+xg@mail.gmail.com>
Hi! Have you been able to look more at this?

I'm feeling pretty good about the patch now, with added tests and better
command handling.


On Fri, Mar 11, 2022 at 1:33 PM Robin Becker <[email protected]> wrote:

> Hi Lennart,
>
> tried the latest patch and it seems better for backgrounds etc etc. See
> the attached test script which I used to see
> what was happening visually some examples are seen with
>
> python lennart-example.py --split=36 --splitByRow=1 --splitInRow=1
> --pdfn=sss.pdf
> python lennart-example.py --split=58 --splitByRow=1 --splitInRow=1
> --pdfn=sss.pdf
> python lennart-example.py --split=58 --splitByRow=0 --splitInRow=1
> --pdfn=sss.pdf
>
> I haven't run this version entirely though the tests yet, but I will try
> over the weekend.
>
> As for small tables:
>    widow is end of something alone at the start of a page. A widow has a
> past, but no future.
>    orphan is start of something alone at the end of a page. An orphan has
> no past, but has a future.
> ....
> thanks again for the effort
> --
> Robin Becker
splitInRow-v5.diff (text/x-patch, 37.8 KB)
diff --git a/src/reportlab/platypus/tables.py b/src/reportlab/platypus/tables.py
index 2b1003c..007a6cd 100755
--- a/src/reportlab/platypus/tables.py
+++ b/src/reportlab/platypus/tables.py
@@ -60,8 +60,9 @@ class CellStyle(PropertySet):
             parent.copy(self)
     def copy(self, result=None):
         if result is None:
-            result = CellStyle()
+            result = CellStyle(self.name)
         for name in dir(self):
+            if name.startswith('_'): continue
             setattr(result, name, getattr(self, name))
         return result
 
@@ -251,7 +252,7 @@ RoundingRectLine = namedtuple('RoundingRectLine','xs ys xe ye weight color cap d
 
 class Table(Flowable):
     def __init__(self, data, colWidths=None, rowHeights=None, style=None,
-                repeatRows=0, repeatCols=0, splitByRow=1, emptyTableAction=None, ident=None,
+                repeatRows=0, repeatCols=0, splitByRow=1, splitInRow=0, emptyTableAction=None, ident=None,
                 hAlign=None,vAlign=None, normalizedData=0, cellStyles=None, rowSplitRange=None,
                 spaceBefore=None,spaceAfter=None, longTableOptimize=None, minRowHeights=None,
                 cornerRadii=__UNSET__, #or [topLeft, topRight, bottomLeft bottomRight]
@@ -336,6 +337,7 @@ class Table(Flowable):
         self.repeatRows = repeatRows
         self.repeatCols = repeatCols
         self.splitByRow = splitByRow
+        self.splitInRow = splitInRow
 
         if style:
             self.setStyle(style)
@@ -464,6 +466,10 @@ class Table(Flowable):
         w = 0
         canv = getattr(self,'canv',None)
         sb0 = None
+        if isinstance(V, str):
+            vw = self._elementWidth(V, s)
+            vh = len(V.split('\n'))*s.fontsize*1.2
+            return max(w, vw), vh
         for v in V:
             vw, vh = v.wrapOn(canv, aW, aH)
             sb = v.getSpaceBefore()
@@ -1199,7 +1205,7 @@ class Table(Flowable):
                 sc, ec, sr, er = self.normCellRange(sc,ec,sr,er)
                 getattr(self,_LineOpMap.get(op, '_drawUnknown' ))( (sc, sr), (ec, er), weight, color, count, space)
         finally:
-            if rrd: 
+            if rrd:
                 canv.line = ocanvline
         canv.restoreState()
         self._curcolor = None
@@ -1356,44 +1362,97 @@ class Table(Flowable):
             if er>=n: er -= n
             self._addCommand((c[0],)+((sc, sr), (ec, er))+tuple(c[3:]))
 
-    def _splitRows(self,availHeight):
-        n=self._getFirstPossibleSplitRowPosition(availHeight)
-        repeatRows = self.repeatRows
-        if n<= (repeatRows if isinstance(repeatRows,int) else (max(repeatRows)+1)): return []
-        lim = len(self._rowHeights)
-        if n==lim: return [self]
+    def _splitCell(self, value, style, oldHeight, newHeight, width):
+        # Content height of the new top row
+        height0 = newHeight - style.topPadding
+        # Content height of the new bottom row
+        height1 = oldHeight - (style.topPadding + newHeight)
 
-        lo = self._rowSplitRange
-        if lo:
-            lo, hi = lo
-            if lo<0: lo += lim
-            if hi<0: hi += lim
-            if n>hi:
-                return self._splitRows(availHeight - sum(self._rowHeights[hi:n]))
-            elif n<lo:
+        if isinstance(value, (tuple, list)):
+            newCellContent = []
+            postponedContent = []
+            split = False
+            cellHeight = self._listCellGeom(value, width, style)[1]
+
+            if style.valign == "MIDDLE":
+                usedHeight = (oldHeight - cellHeight) / 2
+            else:
+                usedHeight = 0
+
+            for flowable in value:
+                if split:
+                    if flowable.height <= height1:
+                        postponedContent.append(flowable)
+                        # Shrink the available height:
+                        height1 -= flowable.height
+                    else:
+                        # The content doesn't fit after the split:
+                        return []
+                elif usedHeight + flowable.height <= height0:
+                    newCellContent.append(flowable)
+                    usedHeight += flowable.height
+                else:
+                    # This is where we need to split
+                    splits = flowable.split(width, height0-usedHeight)
+                    if splits:
+                        newCellContent.append(splits[0])
+                        postponedContent.append(splits[1])
+                    else:
+                        # We couldn't split this flowable at the desired
+                        # point. If we already has added previous paragraphs
+                        # to the content, just add everything after the split.
+                        # Also try adding it after the split if valign isn't TOP
+                        if newCellContent or style.valign != "TOP":
+                            if flowable.height <= height1:
+                                postponedContent.append(flowable)
+                                # Shrink the available height:
+                                height1 -= flowable.height
+                            else:
+                                # The content doesn't fit after the split:
+                                return []
+                        else:
+                            # We could not split this, so we fail:
+                            return []
+
+                    split = True
+
+            return (tuple(newCellContent), tuple(postponedContent))
+
+        elif isinstance(value, str):
+            rows = value.split("\n")
+            lineHeight = 1.2 * style.fontsize
+            contentHeight = (style.leading or lineHeight) * len(rows)
+            if style.valign == "TOP" and contentHeight <= height0:
+                # This fits in the first cell, all is good
+                return (value, '')
+            elif style.valign == "BOTTOM" and contentHeight <= height1:
+                # This fits in the second cell, all is good
+                return ('', value)
+            elif style.valign == "MIDDLE":
+                # Put it in the largest cell:
+                if height1 > height0:
+                    return ('', value)
+                else:
+                    return (value, '')
+
+            elif len(rows) < 2:
+                # It doesn't fit, and there's nothing to split: Fail
                 return []
+            # We need to split this, and there are multiple lines, so we can
+            if style.valign == "TOP":
+                splitPoint = height0 // lineHeight
+            elif style.valign == "BOTTOM":
+                splitPoint = len(rows) - (height1 // lineHeight)
+            else:  # MID
+                splitPoint = (height0 - height1 + contentHeight) // (2 * lineHeight)
 
-        repeatCols = self.repeatCols
-        splitByRow = self.splitByRow
-        data = self._cellvalues
+            splitPoint = int(splitPoint)
+            return ('\n'.join(rows[:splitPoint]), '\n'.join(rows[splitPoint:]))
 
-        #we're going to split into two superRows
-        ident = self.ident
-        if ident: ident = IdentStr(ident)
-        lto = self._longTableOptimize
-        if lto:
-            splitH = self._rowHeights
-        else:
-            splitH = self._argH
-        cornerRadii = getattr(self,'_cornerRadii',None)
-        R0 = self.__class__( data[:n], colWidths=self._colWidths, rowHeights=splitH[:n],
-                repeatRows=repeatRows, repeatCols=repeatCols,
-                splitByRow=splitByRow, normalizedData=1, cellStyles=self._cellStyles[:n],
-                ident=ident,
-                spaceBefore=getattr(self,'spaceBefore',None),
-                longTableOptimize=lto,
-                cornerRadii=cornerRadii[:2] if cornerRadii else None)
+        # No content
+        return ('', '')
 
+    def _splitLineCmds(self, n, doInRowSplit=0):
         nrows = self._nrows
         ncols = self._ncols
         #copy the commands
@@ -1414,20 +1473,24 @@ class Table(Flowable):
             if er < 0: er += nrows
 
             if op in ('BOX','OUTLINE','GRID'):
-                if sr<n and er>=n:
+                if (sr<n and er>=n) or (doInRowSplit and sr==n):
                     # we have to split the BOX
                     A.append(('LINEABOVE',(sc,sr), (ec,sr), weight, color, cap, dash, join, count, space))
                     A.append(('LINEBEFORE',(sc,sr), (sc,er), weight, color, cap, dash, join, count, space))
                     A.append(('LINEAFTER',(ec,sr), (ec,er), weight, color, cap, dash, join, count, space))
                     A.append(('LINEBELOW',(sc,er), (ec,er), weight, color, cap, dash, join, count, space))
                     if op=='GRID':
-                        A.append(('LINEBELOW',(sc,n-1), (ec,n-1), weight, color, cap, dash, join, count, space))
-                        A.append(('LINEABOVE',(sc,n), (ec,n), weight, color, cap, dash, join, count, space))
-                        A.append(('INNERGRID',(sc,sr), (ec,er), weight, color, cap, dash, join, count, space))
+                        if doInRowSplit:
+                            A.append(('INNERGRID',(sc,sr), (ec,n-1), weight, color, cap, dash, join, count, space))
+                            A.append(('INNERGRID',(sc,n), (ec,er), weight, color, cap, dash, join, count, space))
+                        else:
+                            A.append(('LINEBELOW',(sc,n-1), (ec,n-1), weight, color, cap, dash, join, count, space))
+                            A.append(('LINEABOVE',(sc,n), (ec,n), weight, color, cap, dash, join, count, space))
+                            A.append(('INNERGRID',(sc,sr), (ec,er), weight, color, cap, dash, join, count, space))
                 else:
                     A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space))
             elif op == 'INNERGRID':
-                if sr<n and er>=n:
+                if sr<n and er>=n and not doInRowSplit:
                     A.append(('LINEBELOW',(sc,n-1), (ec,n-1), weight, color, cap, dash, join, count, space))
                     A.append(('LINEABOVE',(sc,n), (ec,n), weight, color, cap, dash, join, count, space))
                 A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space))
@@ -1442,11 +1505,238 @@ class Table(Flowable):
             else:
                 A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space))
 
-        R0._cr_0(n,A,nrows)
-        R0._cr_0(n,self._bkgrndcmds,nrows,_srflMode=True)
-        R0._cr_0(n,self._spanCmds,nrows)
-        R0._cr_0(n,self._nosplitCmds,nrows)
-        for c in self._srflcmds:
+        return A
+
+    def _stretchCommands(self, n, cmds, oldrowcount):
+        """Stretches the commands when a row is split
+
+        The row start is sr, the row end is er.
+
+         sr   | er  | result
+        ---------------------------------------------------------------------
+          <n  |  <n | Do nothing.
+              | >=n | A command that spans the break, extend end.
+        ---------------------------------------------------------------------
+         ==n  | ==n | Zero height. Extend the end, unless it's a LINEABOVE
+              |     | commands, it's between rows so do nothing.
+              |     | For LINEBELOW increase both.
+              |  >n | A command that spans the break, extend end.
+        ---------------------------------------------------------------------
+          >n  |  >n | This command comes after the break, increase both.
+        ---------------------------------------------------------------------
+
+        Summary:
+        1. If er > n then increase er
+        2. If sr > n then increase sr
+        3. If er == n and sr < n, increase er
+        4. If er == sr == n and cmd is not line, increase er
+
+        """
+        stretched = []
+        for c in cmds:
+            cmd, (sc,sr), (ec,er) = c[0:3]
+
+            if sr in ("splitlast", "splitfirst") or er in ("splitlast", "splitfirst"):
+                stretched.append(c)
+                continue
+
+            if er < 0:
+                er += oldrowcount
+            if sr < 0:
+                sr += oldrowcount
+
+            if er > n:
+                er += 1
+            elif  er == n:
+                if sr < n or (sr == n and cmd != "LINEABOVE"):
+                    er += 1
+
+            if sr > n or (sr == n and cmd == "LINEBELOW"):
+                sr += 1
+
+            stretched.append((c[0], (sc,sr), (ec,er)) + c[3:])
+
+        return stretched
+
+    def _splitRows(self,availHeight,doInRowSplit=0):
+        # Get the split position. if we split between rows (doInRowSplit=0),
+        # then n will be the first row after the split. If we split a row,
+        # then n is the row we split in two.
+        n=self._getFirstPossibleSplitRowPosition(availHeight)
+
+        # We can't split before or in the repeatRows/headers
+        repeatRows = self.repeatRows
+        maxrepeat = repeatRows if isinstance(repeatRows,int) else max(repeatRows)+1
+        if doInRowSplit and n<maxrepeat or not doInRowSplit and n<=maxrepeat:
+            return []
+
+        # If the whole table fits, return it
+        lim = len(self._rowHeights)
+        if n==lim: return [self]
+
+        lo = self._rowSplitRange
+        if lo:
+            lo, hi = lo
+            if lo<0: lo += lim
+            if hi<0: hi += lim
+            if n>hi:
+                return self._splitRows(availHeight - sum(self._rowHeights[hi:n]), doInRowSplit=doInRowSplit)
+            elif n<lo:
+                return []
+
+        repeatCols = self.repeatCols
+        splitByRow = self.splitByRow
+        splitInRow = self.splitInRow
+        data = self._cellvalues
+
+        if not doInRowSplit:
+            T = self
+        else:
+            # We are splitting the n row into two, if possible.
+            # We can't split if the available height is less than the minimum set:
+            if self._minRowHeights and availHeight < self._minRowHeights[n]:
+                return []
+
+            usedHeights = sum(self._rowHeights[:n])
+
+            cellvalues = self._cellvalues[n]
+            cellStyles = self._cellStyles[n]
+            cellWidths = self._colWidths
+            curRowHeight = self._rowHeights[n]
+
+            # First find the min/max split point
+            minSplit = 0  # Counted from top
+            maxSplit = 0  # Counted from bottom
+            maxHeight = 0
+
+            for (value, style, width) in zip(cellvalues, cellStyles, cellWidths):
+
+                if isinstance(value, (tuple, list)):
+                    # A sequence of flowables:
+                    w, height = self._listCellGeom(value, width, style)
+                    height += style.topPadding + style.bottomPadding
+                    if height > maxHeight:
+                        maxHeight = height
+                elif isinstance(value, str):
+                    rows = value.split("\n")
+                    lineHeight = 1.2 * style.fontsize
+                    height = lineHeight * len(rows) + style.topPadding + style.bottomPadding
+
+                    # Make sure we don't try to split in the middle of the first or last line
+                    minSplit = max(minSplit, lineHeight + style.topPadding)
+                    maxSplit = max(maxSplit, lineHeight + style.bottomPadding)
+
+                    if height > maxHeight:
+                        maxHeight = height
+
+            if minSplit + maxSplit > curRowHeight:
+                return []
+            if minSplit > (availHeight - usedHeights):  # Fail
+                return []
+
+            # This is where we split the row:
+            splitPoint = min(availHeight - usedHeights, maxHeight - maxSplit)
+            if splitPoint < self.splitInRow:
+                # The height of the split is smaller than the minimum.
+                # Fail, and the whole table will be moved to the next page.
+                return []
+
+            remaining = self._height - splitPoint
+            if remaining < self.splitInRow:
+                # The remaining height of the table is smaller than the minimum.
+                # Fail, and the whole table will be moved to the next page.
+                return []
+
+            R0 = []  # Top half of the row
+            R0Height = 0  # Minimum height
+            R1 = []  # Bottom half of the row
+            R1Height = 0  # Minimum height
+            R1Styles = []
+            for (value, style, width) in zip(cellvalues, cellStyles, cellWidths):
+                v = self._splitCell(value, style, curRowHeight, splitPoint, width)
+                if not v:
+                    # Splitting the table failed
+                    return []
+
+                newStyle = style.copy()
+                if style.valign == "MIDDLE":
+                    # Adjust margins
+                    if v[0] and v[1]:
+                        # We split the content, so fix up the valign:
+                        style.valign = "BOTTOM"
+                        newStyle.valign = "TOP"
+                    else:
+                        # Adjust the margins to push it towards the true middle
+                        h = self._listCellGeom(v[0] or v[1], width, style)[1]
+                        margin = (curRowHeight - h) / 2
+                        if v[0]:
+                            style.topPadding += margin
+                        elif v[1]:
+                            newStyle.bottomPadding += margin
+                R0.append(v[0])
+                R1.append(v[1])
+                h0 = self._listCellGeom(v[0], width, style)[1] + style.topPadding + style.bottomPadding
+                R0Height = max(R0Height, h0)
+                h1 = self._listCellGeom(v[1], width, style)[1] + style.topPadding + style.bottomPadding
+                R1Height = max(R1Height, h1)
+                R1Styles.append(newStyle)
+
+            # Make a new table with the row split into two:
+            usedHeight = min(splitPoint, R0Height)
+            newRowHeight = max(R1Height, self._rowHeights[n] - usedHeight)
+            newRowHeights = self._rowHeights[:]
+            newRowHeights.insert(n + 1, newRowHeight)
+            newRowHeights[n] = usedHeight
+            newCellStyles = self._cellStyles[:]
+            newCellStyles.insert(n + 1, R1Styles)
+
+            data = data[:n] + [R0] + [R1] + data[n+1:]
+
+            T = self.__class__( data, colWidths=self._colWidths,
+                rowHeights=newRowHeights, repeatRows=self.repeatRows,
+                repeatCols=self.repeatCols, splitByRow=self.splitByRow,
+                splitInRow=self.splitInRow, normalizedData=1,
+                cellStyles=newCellStyles, ident=self.ident,
+                spaceBefore=getattr(self,'spaceBefore',None),
+                longTableOptimize=self._longTableOptimize,
+                cornerRadii=getattr(self,'_cornerRadii',None))
+
+            T._linecmds = self._stretchCommands(n, self._linecmds, lim)
+            T._bkgrndcmds = self._stretchCommands(n, self._bkgrndcmds, lim)
+            T._spanCmds = self._stretchCommands(n, self._spanCmds, lim)
+            T._nosplitCmds = self._stretchCommands(n, self._nosplitCmds, lim)
+            T._srflcmds = self._stretchCommands(n, self._srflcmds, lim)
+            n = n + 1
+
+        #we're going to split into two superRows
+        ident = self.ident
+        if ident: ident = IdentStr(ident)
+        lto = T._longTableOptimize
+        if lto:
+            splitH = T._rowHeights
+        else:
+            splitH = T._argH
+
+        cornerRadii = getattr(self,'_cornerRadii',None)
+        R0 = self.__class__( data[:n], colWidths=T._colWidths, rowHeights=splitH[:n],
+                repeatRows=repeatRows, repeatCols=repeatCols, splitByRow=self.splitByRow,
+                splitInRow=self.splitInRow, normalizedData=1, cellStyles=T._cellStyles[:n],
+                ident=ident,
+                spaceBefore=getattr(self,'spaceBefore',None),
+                longTableOptimize=lto,
+                cornerRadii=cornerRadii[:2] if cornerRadii else None)
+
+        nrows = T._nrows
+        ncols = T._ncols
+
+        T._linecmds = T._splitLineCmds(n, doInRowSplit=doInRowSplit)
+
+        R0._cr_0(n,T._linecmds,nrows)
+        R0._cr_0(n,T._bkgrndcmds,nrows,_srflMode=True)
+        R0._cr_0(n,T._spanCmds,nrows)
+        R0._cr_0(n,T._nosplitCmds,nrows)
+
+        for c in T._srflcmds:
             R0._addCommand(c)
             if c[1][1]!='splitlast': continue
             (sc,sr), (ec,er) = c[1:3]
@@ -1457,50 +1747,53 @@ class Table(Flowable):
             if isinstance(repeatRows,int):
                 iRows = data[:repeatRows]
                 iRowH = splitH[:repeatRows]
-                iCS = self._cellStyles[:repeatRows]
+                iCS = T._cellStyles[:repeatRows]
                 repeatRows = list(range(repeatRows))
             else:
                 #we have a list of repeated rows eg (1,3)
                 repeatRows = list(sorted(repeatRows))
                 iRows = [data[i] for i in repeatRows]
                 iRowH = [splitH[i] for i in repeatRows]
-                iCS = [self._cellStyles[i] for i in repeatRows]
-            R1 = self.__class__(iRows+data[n:],colWidths=self._colWidths,
+                iCS = [T._cellStyles[i] for i in repeatRows]
+            R1 = self.__class__(iRows+data[n:],colWidths=T._colWidths,
                     rowHeights=iRowH+splitH[n:],
                     repeatRows=len(repeatRows), repeatCols=repeatCols,
-                    splitByRow=splitByRow, normalizedData=1,
-                    cellStyles=iCS+self._cellStyles[n:],
+                    splitByRow=self.splitByRow, splitInRow=self.splitInRow,
+                    normalizedData=1,
+                    cellStyles=iCS+T._cellStyles[n:],
                     ident=ident,
                     spaceAfter=getattr(self,'spaceAfter',None),
                     longTableOptimize=lto,
                     cornerRadii = cornerRadii,
                     )
-            R1._cr_1_1(n,nrows,repeatRows,A) #linecommands
-            R1._cr_1_1(n,nrows,repeatRows,self._bkgrndcmds,_srflMode=True)
-            R1._cr_1_1(n,nrows,repeatRows,self._spanCmds)
-            R1._cr_1_1(n,nrows,repeatRows,self._nosplitCmds)
+            R1._cr_1_1(n,nrows,repeatRows,T._linecmds)
+            R1._cr_1_1(n,nrows,repeatRows,T._bkgrndcmds,_srflMode=True)
+            R1._cr_1_1(n,nrows,repeatRows,T._spanCmds)
+            R1._cr_1_1(n,nrows,repeatRows,T._nosplitCmds)
         else:
             #R1 = slelf.__class__(data[n:], self._argW, self._argH[n:],
-            R1 = self.__class__(data[n:], colWidths=self._colWidths, rowHeights=splitH[n:],
+            R1 = self.__class__(data[n:], colWidths=T._colWidths, rowHeights=splitH[n:],
                     repeatRows=repeatRows, repeatCols=repeatCols,
-                    splitByRow=splitByRow, normalizedData=1, cellStyles=self._cellStyles[n:],
+                    splitByRow=self.splitByRow, splitInRow=self.splitInRow,
+                    normalizedData=1, cellStyles=T._cellStyles[n:],
                     ident=ident,
                     spaceAfter=getattr(self,'spaceAfter',None),
                     longTableOptimize=lto,
                     cornerRadii = ([0,0] + cornerRadii[2:]) if cornerRadii else None,
                     )
-            R1._cr_1_0(n,A)
-            R1._cr_1_0(n,self._bkgrndcmds,_srflMode=True)
-            R1._cr_1_0(n,self._spanCmds)
-            R1._cr_1_0(n,self._nosplitCmds)
-        for c in self._srflcmds:
+
+            R1._cr_1_0(n,T._linecmds)
+            R1._cr_1_0(n,T._bkgrndcmds,_srflMode=True)
+            R1._cr_1_0(n,T._spanCmds)
+            R1._cr_1_0(n,T._nosplitCmds)
+        for c in T._srflcmds:
             R1._addCommand(c)
             if c[1][1]!='splitfirst': continue
             (sc,sr), (ec,er) = c[1:3]
             R1._addCommand((c[0],)+((sc, 0), (ec, 0))+tuple(c[3:]))
 
-        R0.hAlign = R1.hAlign = self.hAlign
-        R0.vAlign = R1.vAlign = self.vAlign
+        R0.hAlign = R1.hAlign = T.hAlign
+        R0.vAlign = R1.vAlign = T.vAlign
         self.onSplit(R0)
         self.onSplit(R1)
         return [R0,R1]
@@ -1521,6 +1814,7 @@ class Table(Flowable):
     _getRowImpossible=staticmethod(_getRowImpossible)
 
     def _getFirstPossibleSplitRowPosition(self,availHeight):
+        # Returns the LAST possible split row position
         impossible={}
         if self._spanCmds:
             self._getRowImpossible(impossible,self._rowSpanCells,self._spanRanges)
@@ -1540,11 +1834,23 @@ class Table(Flowable):
 
     def split(self, availWidth, availHeight):
         self._calc(availWidth, availHeight)
-        if self.splitByRow:
+        if self.splitByRow or self.splitInRow:
             if not rl_config.allowTableBoundsErrors and self._width>availWidth: return []
-            return self._splitRows(availHeight)
-        else:
-            raise NotImplementedError
+
+            # If self.splitByRow is true, first try with doInRowSplit as false.
+            # Otherwise, first try with doInRowSplit as true
+            result = self._splitRows(availHeight, doInRowSplit=not self.splitByRow)
+            if result:
+                # That worked, return that:
+                return result
+
+            # The first attempt did NOT succeed, now try with the flag flipped
+            # (unless self.splitInRow is false)
+            if self.splitInRow:
+                return self._splitRows(availHeight, doInRowSplit=self.splitByRow)
+
+        # We can't split this table in any way, raise an error:
+        return []
 
     def _makeRoundedCornersClip(self, FUZZ=rl_config._FUZZ):
         self._roundingRectDef = None
@@ -1552,7 +1858,7 @@ class Table(Flowable):
         if not cornerRadii or max(cornerRadii)<=FUZZ: return
         nrows = self._nrows
         ncols = self._ncols
-        ar = [min(self._rowHeights[i],self._colWidths[j],cornerRadii[k]) for 
+        ar = [min(self._rowHeights[i],self._colWidths[j],cornerRadii[k]) for
                 k,(i,j) in enumerate((
                     (0,0),
                     (0,ncols-1),
diff --git a/tests/test_table_inrowsplit.py b/tests/test_table_inrowsplit.py
new file mode 100644
index 0000000..0c7fae3
--- /dev/null
+++ b/tests/test_table_inrowsplit.py
@@ -0,0 +1,328 @@
+from reportlab.lib.testutils import setOutDir,makeSuiteForClasses, outputfile, printLocation
+setOutDir(__name__)
+import operator, string
+from reportlab.platypus import *
+#from reportlab import rl_config
+from reportlab.lib.styles import PropertySet, getSampleStyleSheet, ParagraphStyle
+from reportlab.lib import colors
+from reportlab.lib.units import inch
+from reportlab.platypus.paragraph import Paragraph
+#from reportlab.lib.utils import fp_str
+#from reportlab.pdfbase import pdfmetrics
+from reportlab.platypus.flowables import PageBreak
+import os
+import unittest
+
+class TableTestCase(unittest.TestCase):
+
+
+    def getDataBlock(self):
+        "Helper - data for our spanned table"
+        return [
+            # two rows are for headers
+            ['Region','Product','Period',None,None,None,'Total'],
+            [None,None,'Q1','Q2','Q3','Q4',None],
+
+            # now for data
+            ['North','Spam',100,110,120,130,460],
+            ['North','Eggs',101,111,121,131,464],
+            ['North','Guinness',102,112,122,132,468],
+
+            ['South','Spam',100,110,120,130,460],
+            ['South','Eggs',101,111,121,131,464],
+            ['South','Guinness',102,112,122,132,468],
+            ]
+
+    def test_document(self):
+
+        rowheights = (24, 16, 16, 16, 16)
+        rowheights2 = (24, 16, 16, 16, 30)
+        colwidths = (50, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32)
+        GRID_STYLE = TableStyle(
+            [('GRID', (0,0), (-1,-1), 0.25, colors.black),
+             ('ALIGN', (1,1), (-1,-1), 'RIGHT')]
+            )
+
+        styleSheet = getSampleStyleSheet()
+        styNormal = styleSheet['Normal']
+        styNormal.spaceBefore = 6
+        styNormal.spaceAfter = 6
+
+        lst = []
+
+        lst.append(Paragraph("""Oversized cells""", styleSheet['Heading1']))
+
+        lst.append(Paragraph("""Cells can end up being larger than a page.
+        In that case, we need to split the cell. splitByRow and splitInRow
+        controls that. By default splitByRow is 1 and splitInRow is 0.
+        It splits between two rows. """,
+        styNormal))
+
+        ministy = TableStyle([
+            ('GRID', (0,0), (-1,-1), 1.0, colors.black),
+            ('VALIGN', (0,1), (1,1), 'BOTTOM'),
+            ('VALIGN', (1,1), (2,1), 'MIDDLE'),
+            ('VALIGN', (2,1), (3,1), 'TOP'),
+            ('VALIGN', (3,1), (4,1), 'BOTTOM'),
+            ('VALIGN', (4,1), (5,1), 'MIDDLE'),
+            ('VALIGN', (5,1), (6,1), 'TOP'),
+            ])
+        cell1 = [Paragraph(
+            """This is a very tall cell to make a tall row.""",
+            styNormal)]
+        cell2 = [Paragraph("A cell with two flowables.", styNormal),
+            Paragraph("And valign= MIDDLE.", styNormal)]
+        cell3 = [Paragraph("Paragraph with valign=TOP", styNormal)]
+
+        tableData = [
+            ['Row 1', 'Two rows:\nSo there', 'is a', 'place', 'to split', 'the table'],
+            [cell1, cell2, cell3, 'valign=BOTTOM', 'valign=MIDDLE', 'valign=TOP']
+        ]
+        colWidths = (50, 75, 70, 90, 90, 70)
+
+        # This is the table with splitByRow, which splits between row 1 & 2:
+        t = Table(tableData,
+                  colWidths=colWidths,
+                  rowHeights=None,
+                  style=ministy,
+                  splitByRow=1,
+                  splitInRow=0)
+        parts = t.split(451, 60)
+        lst.append(parts[0])
+        lst.append(Spacer(0,6))
+        lst.append(parts[1])
+
+        lst.append(Paragraph("""Here is the same table, with splitByRow=0 and
+        splitInRow=1. It splits inside a row.""",
+        styNormal))
+
+        # This is the table with splitInRow, which splits in row 2:
+        t = Table(tableData,
+                  colWidths=colWidths,
+                  rowHeights=None,
+                  style=ministy,
+                  splitByRow=0,
+                  splitInRow=1)
+
+        parts = t.split(451, 60)
+        lst.append(parts[0])
+        lst.append(Spacer(0,6))
+        lst.append(parts[1])
+
+        lst.append(Paragraph("""Here is the same table, with splitByRow=1 and
+        splitInRow=1. It splits between the rows, if possible.""",
+        styNormal))
+
+        # This is the table with both splits, which splits in row 2:
+        t = Table(tableData,
+                  colWidths=colWidths,
+                  rowHeights=None,
+                  style=ministy,
+                  splitByRow=1,
+                  splitInRow=1)
+
+        parts = t.split(451, 60)
+        lst.append(parts[0])
+        lst.append(Spacer(0,6))
+        lst.append(parts[1])
+
+        lst.append(Paragraph("""But if we constrict the space to less than the first row,
+        it splits that row.""",
+        styNormal))
+
+        # This is the table with both splits and no space, which splits in row 1:
+        t = Table(tableData,
+                  colWidths=colWidths,
+                  rowHeights=None,
+                  style=ministy,
+                  splitByRow=1,
+                  splitInRow=1)
+
+        parts = t.split(451, 15)
+        lst.append(parts[0])
+        lst.append(Spacer(0,6))
+        lst.append(parts[1])
+
+        # Split it at a point in row 2, where the split fails
+        lst.append(Paragraph("""When splitByRow is 0 and splitInRow is 1, we should
+        still allow fallback to splitting between rows""",
+        styNormal))
+
+        t = Table(tableData,
+                  colWidths=colWidths,
+                  rowHeights=None,
+                  style=ministy,
+                  splitByRow=0,
+                  splitInRow=1)
+        parts = t.split(451, 50)
+        lst.append(parts[0])
+        lst.append(Spacer(0,6))
+        lst.append(parts[1])
+
+        lst.append(PageBreak())
+        lst.append(Paragraph("""Oversized cells, style handling splitInRow""", styleSheet['Heading2']))
+
+        lst.append(Paragraph("""Test styles and spans when splitting in various rows:""", styNormal))
+
+        tableData = [
+            ['00\n\naa', '01', '02', '03', '04'],
+            ['10', '11\nbb', '12', '13', '14'],
+            ['20', '21', '22\ncc', '23', '24'],
+            ['30', '31', '32', '33\ndd', '34']
+        ]
+        styles = [
+            ('GRID',(0,0),(-1,-1),0.5,colors.grey),
+            ('GRID',(1,1),(-2,-2),1,colors.green),
+            ('BOX',(0,0),(1,-1),2,colors.red),
+            ('BOX',(0,0),(-1,-1),2,colors.black),
+            ('LINEABOVE',(1,2),(-2,2),1,colors.blue),
+            ('LINEBEFORE',(2,1),(2,-2),1,colors.pink),
+            ('BACKGROUND', (0, 0), (0, 1), colors.pink),
+            ('BACKGROUND', (1, 1), (1, 2), colors.lavender),
+            ('BACKGROUND', (2, 2), (2, 3), colors.orange),
+            ('TEXTCOLOR',(0,-1),(-2,-1),colors.green),
+        ]
+
+        t = Table(tableData,style=styles, splitInRow=1, splitByRow=0)
+
+        parts = t.split(4*inch, 36)
+        lst.append(parts[0])
+        lst.append(Spacer(0,6))
+        lst.append(parts[1])
+        lst.append(Spacer(0,12))
+
+        parts = t.split(4*inch, 60)
+        lst.append(parts[0])
+        lst.append(Spacer(0,6))
+        lst.append(parts[1])
+        lst.append(Spacer(0,12))
+
+        parts = t.split(4*inch, 100)
+        lst.append(parts[0])
+        lst.append(Spacer(0,6))
+        lst.append(parts[1])
+        lst.append(Spacer(0,12))
+
+        parts = t.split(4*inch, 130)
+        lst.append(parts[0])
+        lst.append(Spacer(0,6))
+        lst.append(parts[1])
+
+        lst.append(PageBreak())
+        lst.append(Paragraph("""Splitfirst/splitlast behavior with split rows and spans""",
+                             styleSheet['Heading2']))
+
+        data=  [['A', 'BBBBB', 'C', 'D', 'E'],
+                ['00', '01', '02', '03', '04'],
+                ['10\n11', ],
+                ['20', '21', '22', '23', '24'],
+                ['30', '31', '32', '33', '34']]
+        sty = [
+                ('ALIGN',(0,0),(-1,-1),'CENTER'),
+                ('VALIGN',(0,0),(-1,-1),'TOP'),
+                ('GRID',(0,0),(-1,-1),1,colors.green),
+                ('BOX',(0,0),(-1,-1),2,colors.red),
+
+                #span 'BBBB' across middle 3 cells in top row
+                ('SPAN',(1,0),(3,0)),
+                #now color the first cell in this range only,
+                #i.e. the one we want to have spanned.  Hopefuly
+                #the range of 3 will come out khaki.
+                ('BACKGROUND',(1,0),(1,0),colors.khaki),
+
+                ('SPAN',(0,2),(-1,2)),
+
+                #span 'AAA'down entire left column
+                ('SPAN',(0,0), (0, 1)),
+                ('BACKGROUND',(0,0),(0,0),colors.cyan),
+                ('TEXTCOLOR', (0,'splitfirst'), (-1,'splitfirst'), colors.cyan),
+                ('TEXTCOLOR', (0,'splitlast'), (-1,'splitlast'), colors.red),
+                ('BACKGROUND', (0,'splitlast'), (-1,'splitlast'), colors.pink),
+                ('LINEBELOW', (0,'splitlast'), (-1,'splitlast'), 1, colors.grey,'butt'),
+               ]
+        t=Table(data,style=sty, colWidths = [20] * 5, splitInRow=1, splitByRow=0)
+        lst.append(t)
+        lst.append(Spacer(18,18))
+
+        t=Table(data,style=sty, colWidths = [20] * 5, splitInRow=1, splitByRow=0)
+        for s in t.split(4*inch,40):
+            lst.append(s)
+            lst.append(Spacer(0,6))
+        lst.append(Spacer(18,12))
+
+        t=Table(data,style=sty, colWidths = [20] * 5, splitInRow=1, splitByRow=0)
+        for s in t.split(4*inch,60):
+            lst.append(s)
+            lst.append(Spacer(0,6))
+
+        lst.append(PageBreak())
+        lst.append(Paragraph("""Long cell with multiple splits, and minimum split size""",
+                             styleSheet['Heading2']))
+
+        lst.append(Paragraph("With a height of 80 amd splitInRow=1 (no minimum rest) "
+                             "we get a small last split.",
+                             styleSheet['Normal']))
+
+        ministy = TableStyle([('GRID', (0,0), (-1,-1), 1.0, colors.black),])
+        cell1 = [Paragraph(
+            "This is a very very tall cell to make a very very tall row so we can split it "
+            "many many times, and also test for minimum splits.""",
+            styNormal)]
+
+        tableData = [['Row 1', 'Row 1'],[cell1, 'Cell2']]
+        colWidths = (50,50)
+
+        # This is the table with splitByRow, which splits between row 1 & 2:
+        t = Table(tableData,
+                  colWidths=colWidths,
+                  rowHeights=None,
+                  style=ministy,
+                  splitByRow=0,
+                  splitInRow=1)
+
+        while True:
+            s = t.split(4*inch, 80)
+            lst.append(s[0])
+            lst.append(Spacer(6,6))
+            if len(s) > 1:
+                t = s[1]
+            else:
+                break
+
+        # Now the same, but with a minimum size of 40.
+        lst.append(Paragraph("With minimum split size to 40 (splitInRow=40) the last split "
+                             "isn't done, and it should instead flow over to the next page.",
+                             styleSheet['Normal']))
+        t = Table(tableData,
+                  colWidths=colWidths,
+                  rowHeights=None,
+                  style=ministy,
+                  splitByRow=0,
+                  splitInRow=40)
+
+        while True:
+            s = t.split(4*inch, 80)
+            if s:
+                lst.append(s[0])
+                lst.append(Spacer(6,6))
+            else:
+                lst.append(t)
+            if len(s) > 1:
+                t = s[1]
+            else:
+                break
+
+        # It should also not split at less than 40 in the beginning.
+        assert not t.split(4*inch, 30)
+
+        SimpleDocTemplate(outputfile('test_table_inrowsplit.pdf'), showBoundary=1).build(lst)
+
+def makeSuite():
+    return makeSuiteForClasses(TableTestCase)
+
+
+#noruntests
+if __name__ == "__main__":
+    unittest.TextTestRunner().run(makeSuite())
+    print('saved '+outputfile('test_table_inrowsplit.pdf'))
+    printLocation()
test_table_inrowsplit.pdf (application/pdf, 7.4 KB) - not displayed