--- a/reportlab/platypus/tables.py Wed Mar 23 13:35:58 2005 +0000
+++ b/reportlab/platypus/tables.py Tue Mar 29 13:54:51 2005 +0000
@@ -190,6 +190,12 @@
pass
raise ValueError('Bad %s value %s in %s'%(name,value,str(cmd)))
+def _endswith(obj,s):
+ try:
+ return obj.endswith(s)
+ except:
+ return 0
+
class Table(Flowable):
def __init__(self, data, colWidths=None, rowHeights=None, style=None,
repeatRows=0, repeatCols=0, splitByRow=1, emptyTableAction=None, ident=None):
@@ -378,8 +384,7 @@
if type(w) in (FloatType,IntType): return w
except AttributeError:
pass
- if t is not StringType: v = v is not None and str(v) or ''
- v = string.split(v, "\n")
+ v = string.split(v is not None and str(v) or '', "\n")
return max(map(lambda a, b=s.fontname, c=s.fontsize,d=pdfmetrics.stringWidth: d(a,b,c), v))
def _calc_height(self, availHeight, availWidth, H=None, W=None):
@@ -435,9 +440,7 @@
if not rl_config.allowTableBoundsErrors and dW>w:
raise "LayoutError", "Flowable %s (%sx%s points) too wide for cell(%d,%d) (%sx* points) in\n%s" % (v[0].identity(30),fp_str(dW),fp_str(t),i,j, fp_str(w), self.identity(30))
else:
- if t is not StringType:
- v = v is None and '' or str(v)
- v = string.split(v, "\n")
+ v = string.split(v is not None and str(v) or '', "\n")
t = s.leading*len(v)
t = t+s.bottomPadding+s.topPadding
if t>h: h = t #record a new maximum
@@ -532,7 +535,7 @@
elif w == '*':
numberUndefined += 1
numberGreedyUndefined += 1
- elif type(w) is StringType and w.endswith('%'):
+ elif _endswith(w,'%'):
percentDefined += 1
percentTotal += float(w[:-1])
else:
@@ -550,7 +553,7 @@
elementWidth = self._elementWidth
for colNo in range(self._ncols):
w = W[colNo]
- if w is None or w=='*' or (type(w) is StringType and w.endswith('%')):
+ if w is None or w=='*' or _endswith(w,'%'):
siz = 1
current = final = None
for rowNo in range(self._nrows):
@@ -586,37 +589,73 @@
percentTotal = 100
defaultDesired = (defaultWeight/percentTotal)*availWidth
else:
- defaultWeight = defaultDesired = 0
-
- desiredWidths = {}
- difference = 0
+ defaultWeight = defaultDesired = 1
+ # we now calculate how wide each column wanted to be, and then
+ # proportionately shrink that down to fit the remaining available
+ # space. A column may not shrink less than its minimum width,
+ # however, which makes this a bit more complicated.
+ desiredWidths = []
+ totalDesired = 0
+ effectiveRemaining = remaining
for colNo, minimum in minimums.items():
w = W[colNo]
- if w is not None and w.endswith('%'):
+ if _endswith(w,'%'):
desired = (float(w[:-1])/percentTotal)*availWidth
elif w == '*':
desired = defaultDesired
else:
- desired = not numberGreedyUndefined and defaultDesired or 0
+ desired = not numberGreedyUndefined and defaultDesired or 1
if desired <= minimum:
W[colNo] = minimum
else:
- desiredWidths[colNo] = desired
- difference += desired-minimum
- disappointment = (difference-remaining)/len(desiredWidths)
- for colNo, desired in desiredWidths.items():
- adjusted = desired - disappointment
- minimum = minimums[colNo]
- if minimum > adjusted:
- W[colNo] = minimum
- del desiredWidths[colNo]
- difference += minimum-adjusted
- disappointment = (difference-remaining)/len(desiredWidths)
- for colNo, desired in desiredWidths.items():
- adjusted = desired - disappointment
- minimum = minimums[colNo]
- assert adjusted >= minimum
- W[colNo] = adjusted
+ desiredWidths.append(
+ (desired-minimum, minimum, desired, colNo))
+ totalDesired += desired
+ effectiveRemaining += minimum
+ if desiredWidths: # else we're done
+ # let's say we have two variable columns. One wanted
+ # 88 points, and one wanted 264 points. The first has a
+ # minWidth of 66, and the second of 55. We have 71 points
+ # to divide up in addition to the totalMinimum (i.e.,
+ # remaining==71). Our algorithm tries to keep the proportion
+ # of these variable columns.
+ #
+ # To do this, we add up the minimum widths of the variable
+ # columns and the remaining width. That's 192. We add up the
+ # totalDesired width. That's 352. That means we'll try to
+ # shrink the widths by a proportion of 192/352--.545454.
+ # That would make the first column 48 points, and the second
+ # 144 points--adding up to the desired 192.
+ #
+ # Unfortunately, that's too small for the first column. It
+ # must be 66 points. Therefore, we go ahead and save that
+ # column width as 88 points. That leaves (192-88==) 104
+ # points remaining. The proportion to shrink the remaining
+ # column is (104/264), which, multiplied by the desired
+ # width of 264, is 104: the amount assigned to the remaining
+ # column.
+ proportion = effectiveRemaining/totalDesired
+ # we sort the desired widths by difference between desired and
+ # and minimum values, a value called "disappointment" in the
+ # code. This means that the columns with a bigger
+ # disappointment will have a better chance of getting more of
+ # the available space.
+ desiredWidths.sort()
+ finalSet = []
+ for disappointment, minimum, desired, colNo in desiredWidths:
+ adjusted = proportion * desired
+ if adjusted < minimum:
+ W[colNo] = minimum
+ totalDesired -= desired
+ effectiveRemaining -= minimum
+ if totalDesired:
+ proportion = effectiveRemaining/totalDesired
+ else:
+ finalSet.append((minimum, desired, colNo))
+ for minimum, desired, colNo in finalSet:
+ adjusted = proportion * desired
+ assert adjusted >= minimum
+ W[colNo] = adjusted
else:
for colNo, minimum in minimums.items():
W[colNo] = minimum
@@ -624,6 +663,24 @@
self._argW = self._colWidths = W
return W
+ def minWidth(self):
+ W = list(self._argW)
+ width = 0
+ elementWidth = self._elementWidth
+ for colNo, w in enumerate(W):
+ if w is None or w=='*' or _endswith(w,'%'):
+ final = 0
+ for rowNo in range(self._nrows):
+ value = self._cellvalues[rowNo][colNo]
+ style = self._cellStyles[rowNo][colNo]
+ new = (elementWidth(value,style)+
+ style.leftPadding+style.rightPadding)
+ final = max(final, new)
+ width += final
+ else:
+ width += float(w)
+ return width # XXX + 1/2*(left and right border widths)
+
def _calcSpanRanges(self):
"""Work out rects for tables which do row and column spanning.
@@ -1174,9 +1231,7 @@
x = colpos + colwidth - cellstyle.rightPadding
else:
raise ValueError, 'Invalid justification %s' % just
- if n is StringType: val = cellval
- else: val = str(cellval)
- vals = string.split(val, "\n")
+ vals = string.split(str(cellval), "\n")
n = len(vals)
leading = cellstyle.leading
fontsize = cellstyle.fontsize
--- a/reportlab/test/test_table_layout.py Wed Mar 23 13:35:58 2005 +0000
+++ b/reportlab/test/test_table_layout.py Tue Mar 29 13:54:51 2005 +0000
@@ -338,6 +338,79 @@
of ReportLab tables: (1) fix the widths if you can, (2) don't use
a paragraph when a string will do.
""", styNormal))
+
+ lst.append(Paragraph("""Unsized columns that contain flowables without
+ precise widths, such as paragraphs and nested tables,
+ still need to try and keep their content within borders and ideally
+ even honor percentage requests. This can be tricky--and expensive.
+ But sometimes you can't follow the golden rules.
+ """, styNormal))
+
+ lst.append(Paragraph("""The code first calculates the minimum width
+ for each unsized column by iterating over every flowable in each column
+ and remembering the largest minimum width. It then allocates
+ available space to accomodate the minimum widths. Any remaining space
+ is divided up, treating a width of '*' as greedy, a width of None as
+ non-greedy, and a percentage as a weight. If a column is already
+ wider than its percentage warrants, it is not further expanded, and
+ the other widths accomodate it.
+ """, styNormal))
+
+ lst.append(Paragraph("""For instance, consider this tortured table.
+ It contains four columns, with widths of None, None, 60%, and 20%,
+ respectively, and a single row. The first cell contains a paragraph.
+ The second cell contains a table with fixed column widths that total
+ about 50% of the total available table width. The third cell contains
+ a string. The last cell contains a table with no set widths but a
+ single cell containing a paragraph.
+ """, styNormal))
+ ministy = TableStyle([
+ ('GRID', (0,0), (-1,-1), 1.0, colors.black),
+ ])
+ nested1 = [Paragraph(
+ 'This is a paragraph. The column has a width of None.',
+ styNormal)]
+ nested2 = [Table(
+ [[Paragraph(
+ 'This table is set to take up two and a half inches. The '
+ 'column that holds it has a width of None.', styNormal)]],
+ colWidths=(180,),
+ rowHeights=None,
+ style=ministy)]
+ nested3 = '60% width'
+ nested4 = [Table(
+ [[[Paragraph(
+ "This is a table with a paragraph in it but no width set. "
+ "The column width in the containing table is 20%.",
+ styNormal)]]],
+ colWidths=(None,),
+ rowHeights=None,
+ style=ministy)]
+ t = Table([[nested1, nested2, nested3, nested4]],
+ colWidths=(None, None, '60%', '20%'),
+ rowHeights=None,
+ style=ministy)
+ lst.append(t)
+
+ lst.append(Paragraph("""Notice that the second column does expand to
+ account for the minimum size of its contents; and that the remaining
+ space goes to the third column, in an attempt to honor the '60%'
+ request as much as possible. This is reminiscent of the typical HTML
+ browser approach to tables.""", styNormal))
+
+ lst.append(Paragraph("""To get an idea of how potentially expensive
+ this is, consider the case of the last column: the table gets the
+ minimum width of every flowable of every cell in the column. In this
+ case one of the flowables is a table with a column without a set
+ width, so the nested table must itself iterate over its flowables.
+ The contained paragraph then calculates the width of every word in it
+ to see what the biggest word is, given the set font face and size. It
+ is easy to imagine creating a structure of this sort that took an
+ unacceptably large amount of time to calculate. Remember the golden
+ rule, if you can. """, styNormal))
+
+ lst.append(Paragraph("""This code does not yet handle spans well.""",
+ styNormal))
SimpleDocTemplate(outputfile('test_table_layout.pdf'), showBoundary=1).build(lst)