Patch to support tables with oversize cells
Lennart Regebro via reportlab-users <[email protected]> Tue, 11 Jan 2022 20:55:03 +0100
| Newsgroups | gmane.comp.python.reportlab.user |
|---|---|
| Message-ID | <CAHT-kBfxLJeSuHQ4vTeBiqBNREjwx=y5-2=xQ7u28dn32pir-Q@mail.gmail.com> |
I have implemented a new feature of tables, that enables tables which have cells that are too tall to fit on one page to still be rendered to PDF, which is a behavior that would make things a lot easier for us, as we sometimes have tables that have long texts in them. See description below. //Lennart Regebro, Shoobx == splitInRow patch == Currently, if a table is too tall to fit on one page and has the attribute splitByRow set to 1 (which is the default), the table will be split into two tables and the rows that do fit on the current page will be rendered on the current page. The remaining rows will be printed on the second page. If the remaining rows do not fit on that page, the table will be split again in the same manner until the end of the table. However, should one cell be too large for one page, the table can not be split, and the rendering to PDF will fail. This will also happen if the splitting is disabled by setting the splitByRow attribute to 0, and the table as a whole is too large for a single page. This patch implements functionality that will split a row that is taller than a page into two rows, enabling the rendering of tables with very tall rows. It does this by adding a new attribute, splitInRow, which also defaults to 1. This will if, and only if, a table fails to render in the allocated space, split a row into two rows, also splitting all the content inside those rows at the appropriate point. === How to use it === The _splitRows() method of tables now takes a new argument, doInRowSplit, which defaults to 0. The split() method of the table will now do the following: * If both .splitByRow and .splitInRow is 0, it will return [], ie fail. * If .splitByRow is 1, it will call split() with doInRowSplit=False, to attempt a split by whole rows. But if .splitByRow is 0 is will call it with doInRowSplit=True to attempt to split it inside a row. * If that fails, it will fall back to doing the opposite. * If that also fails, the method will fail (ie return []). This results in the following behavior: * Setting both attributes to 0 will never split the table * Setting .splitByRow to 1 and .splitInRow to 0 will, when a split is necessary, only split between rows (this is the current behavior). * Setting .splitByRow to 0 and .splitInRow to 1 will always split inside rows, unless the split is too close to the start or end of a row that it can't split those cells, in which case it splits between those rows. * Setting both .splitByRow and .splitInRow to 1 will first attempt to split the table between rows, and only split a row if it is necessary to fit the table on a page. This is the new default behavior. === Implementation details === Table._splitRow() will as before find the place to split the table as variable n. But when doInRowSplit is set to one, instead of n being the first row after the split, n will be the row that is split. This necessitated a small change in the logic dealing with repeatRows, as we otherwise might split in a repeat row. If a row should be split, each cell is checked for the minimum and maximum split points, which typically is one line of text, as we can't split inside a line of text. The split point is then selected to be as high as possible inside that. After that, each cell is split into two at the height given by a new method, Table._splitCell(). This split looks at the valign set in the cell style and calculates where inside the cell value the split will be, and splits at a row or flowable value before that. When the valign of the cell is "MIDDLE", the margins of the new split cells are adjusted to try to keep the contents in the "middle" of a merged cell. If the cell contents were actually split, that's easy, the first one is simply set to valign to the bottom, and the second to the top. But if the value was not split, the margins of that cell is adjusted to try to get the value to remain close to the original position. After this, a new table T is created from the new data, and the adjusted styles, line commands, etc. This is then used in the rest of the method, which will split the new table into two tables, in the normal fashion. ==== Other changes ==== To get CellStyle.copy() to work, self.name is passed in to the copy, and attributes that starts with underscore are skipped. In Table._splitRows there was a section that would adjust the line commands list to adjust for the new split. This section did need a few extra lines for handling the splitting of a row, and it also needed to be called in two different places, so it was moved to its own method, Table._splitLineCmds(). Table._listCellGeom() now supports cells that have plain text content. This could be used to simplify code in some places where it's used. Tests were added in the test_table_layout.py module.
splitInRow.diff
(application/x-patch, 26.5 KB)
diff --git a/src/reportlab/platypus/tables.py b/src/reportlab/platypus/tables.py
index 070c3fa..04717e8 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=1, 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()
@@ -1356,46 +1362,100 @@ 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
A = []
# hack up the line commands
@@ -1414,16 +1474,21 @@ 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), weight, color, cap, dash, join, count, space))
+ A.append(('INNERGRID',(sc,n+1), (ec,er), weight, color, cap, dash, join, count, space))
+ #A.append(('LINEBELOW',(sc,n), (ec,n), 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':
@@ -1442,11 +1507,180 @@ class Table(Flowable):
else:
A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space))
+ return A
+
+ 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)
+
+ 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._linecmds
+ T._linecmds = T._splitLineCmds(n, doInRowSplit=1)
+ 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
+
+ if doInRowSplit:
+ spaceAfter = None
+ else:
+ spaceAfter = availHeight - sum(splitH[:n])
+
+
+ cornerRadii = getattr(self,'_cornerRadii',None)
+ R0 = self.__class__( data[:n], colWidths=T._colWidths, rowHeights=splitH[:n],
+ repeatRows=repeatRows, repeatCols=repeatCols, splitByRow=splitByRow,
+ splitInRow=splitInRow, normalizedData=1, cellStyles=T._cellStyles[:n],
+ ident=ident,
+ spaceBefore=getattr(self,'spaceBefore',None),
+ spaceAfter=spaceAfter,
+ longTableOptimize=lto,
+ cornerRadii=cornerRadii[:2] if cornerRadii else None)
+
+ nrows = T._nrows
+ ncols = T._ncols
+
+ A = T._splitLineCmds(n, doInRowSplit=doInRowSplit)
+
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:
+ 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 +1691,50 @@ 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:],
+ 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._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=splitByRow, 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._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 +1755,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 +1775,21 @@ 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.
+ 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
diff --git a/tests/test_table_layout.py b/tests/test_table_layout.py
index edfe436..b019f1f 100644
--- a/tests/test_table_layout.py
+++ b/tests/test_table_layout.py
@@ -403,6 +403,120 @@ class TableTestCase(unittest.TestCase):
lst.append(Paragraph("""This code does not yet handle spans well.""",
styNormal))
+ lst.append(PageBreak())
+
+ lst.append(Paragraph("""Oversized cells""", styleSheet['Heading1']))
+
+ lst.append(Paragraph("""In some cases cells with flowables can end up
+ being larger than a page. In that case, we need to split the page.
+ The splitInRow attribute allows that, it's by default 0.""",
+ styNormal))
+
+ lst.append(Paragraph("""Here is a table, with splitByRow=1 and
+ splitInRow=0. It splits between the 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 for splitting.""",
+ styNormal)]
+ cell2 = [Paragraph("This cell has 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']
+ ]
+
+ # This is the table with splitByRow, which splits between row 1 & 2:
+ t = Table(tableData,
+ colWidths=(50, 70, 70, 90, 90, 70),
+ rowHeights=None,
+ style=ministy,
+ splitByRow=1,
+ splitInRow=0)
+ parts = t.split(451, 55)
+ lst.append(parts[0])
+ lst.append(Paragraph("", styNormal))
+ 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=(50, 70, 70, 90, 90, 70),
+ rowHeights=None,
+ style=ministy,
+ splitByRow=0,
+ splitInRow=1)
+
+ parts = t.split(451, 57)
+ lst.append(parts[0])
+ lst.append(Paragraph("", styNormal))
+ 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=(50, 70, 70, 90, 90, 70),
+ rowHeights=None,
+ style=ministy,
+ splitByRow=1,
+ splitInRow=1)
+
+ parts = t.split(451, 57)
+ lst.append(parts[0])
+ #lst.append(Paragraph("", styNormal))
+ 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=(50, 70, 70, 90, 90, 70),
+ rowHeights=None,
+ style=ministy,
+ splitByRow=1,
+ splitInRow=1)
+
+ parts = t.split(451, 15)
+ lst.append(parts[0])
+ lst.append(Paragraph("", styNormal))
+ 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=(50, 70, 70, 90, 90, 70),
+ rowHeights=None,
+ style=ministy,
+ splitByRow=0,
+ splitInRow=1)
+ parts = t.split(451, 55)
+ lst.append(parts[0])
+ lst.append(Paragraph("", styNormal))
+ lst.append(parts[1])
+
+
SimpleDocTemplate(outputfile('test_table_layout.pdf'), showBoundary=1).build(lst)
def makeSuite():