Re: Patch to support tables with oversize cells
Lennart Regebro via reportlab-users <[email protected]> Mon, 7 Mar 2022 17:13:29 +0100
| Newsgroups | gmane.comp.python.reportlab.user |
|---|---|
| Message-ID | <CAHT-kBdTjgHmpcqtVMcqSLTS=uaK74x5j5bM69RiqtL4h__iUw@mail.gmail.com> |
On Wed, Feb 23, 2022 at 11:51 AM Robin Becker <[email protected]> wrote: > Bad > 1) the split return wrongly adjusts the styling of the second part of > the split. Yes, I had some insights in the handling of commands when looking at this, and rewrote that bit to be much nicer, and also now handle the _srflcmds, which I had completely missed before. I added tests to exercise this (and the earlier split tests) into the new test_table_inrowsplit.py, where I have also added your edge cases. I added the current output of that file. > 2) The cell split seems to have no lower limit. In most cases we would > not want to leave a very small part of a table > > at the end of a page so presumable we would only want to split a row > if it exceeds a specific height. That would > normally be the height of a frame or page. My guess is we could > adjust this patch to use the value of splitInRow as > a suitable lower limit on row height for splitting. So, the minimal "split height" will be one row of text for text cells, and whatever the flowable wants for flowables. We can use splitInRow for that as well. I now implemented it slightly differently, the splitInRow value will be used for a minimum "end of table" height, so that we don't get just one row of a table on a separate page. I think that's called an "orphan" when it comes to paragraphs, right? I don't know if you think that makes sense, I can change it to just be a general minimum of a cell. > Untested > 1) I guess this has not been tested at all with spanned rows/columns, > but I haven't tried as yet. > I added a test for this. 2) I haven't tried very long cells that would require two or more splits. > Added tests for this. 3) I haven't checked to see if vertical alignment makes any real > difference in split cells, but I think your > added test does show that it seems to do the right thing. > Yeah, it won't be pixel-perfect, I think, but at least something in the vicinity. // Lennart
splitInRow-v4.diff
(text/x-patch, 37.5 KB)
diff --git a/src/reportlab/platypus/tables.py b/src/reportlab/platypus/tables.py
index 2b1003c..94231bb 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,233 @@ 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)
+ 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 +1742,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 +1809,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 +1829,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 +1853,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..048bf8a
--- /dev/null
+++ b/tests/test_table_inrowsplit.py
@@ -0,0 +1,325 @@
+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
+
+ 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