1"""Table-related formatting functions. 2 3This module contains functions called from measurers.py to format tables.""" 4import sys, mathnode 5 6def getByIndexOrLast(lst, idx): 7 if idx < len(lst): return lst[idx] 8 else: return lst[-1] 9 10class CellDescriptor: 11 """Descriptor of a single cell in a table""" 12 def __init__(self, content, halign, valign, colspan, rowspan): 13 self.content = content 14 self.halign = halign 15 self.valign = valign 16 self.colspan = colspan 17 self.rowspan = rowspan 18 19class ColumnDescriptor: 20 """Descriptor of a single column in a table""" 21 def __init__(self): 22 self.auto = True 23 self.fit = False 24 self.width = 0 25 self.spaceAfter = 0 26 self.lineAfter = None 27 28class RowDescriptor: 29 """Descriptor of a single row in a table; contains cells""" 30 def __init__(self, node, cells, rowalign, columnaligns, busycells): 31 self.alignToAxis = (rowalign == u"axis") 32 self.height = 0 33 self.depth = 0 34 self.spaceAfter = 0 35 self.lineAfter = None 36 self.cells = [] 37 for c in cells: 38 # Find the first free cell 39 while len(busycells) > len(self.cells) and busycells[len(self.cells)] > 0: 40 self.cells.append(None) 41 42 halign = getByIndexOrLast(columnaligns, len(self.cells)) 43 valign = rowalign 44 colspan = 1 45 rowspan = 1 46 47 if c.elementName == u"mtd": 48 halign = c.attributes.get(u"columnalign", halign) 49 valign = c.attributes.get(u"rowalign", valign) 50 colspan = node.parseInt(c.attributes.get(u"colspan", u"1")) 51 rowspan = node.parseInt(c.attributes.get(u"rowspan", u"1")) 52 53 while len(self.cells) >= len(node.columns): 54 node.columns.append(ColumnDescriptor()) 55 self.cells.append(CellDescriptor(c, halign, valign, colspan, rowspan)) 56 57 for i in range (1, colspan): self.cells.append(None) 58 while len(self.cells) > len(node.columns): 59 node.columns.append(ColumnDescriptor()) 60 61def arrangeCells(node): 62 node.rows = [] 63 node.columns = [] 64 busycells = [] 65 66 # Read table-level alignment properties 67 table_rowaligns = node.getListProperty(u"rowalign") 68 table_columnaligns = node.getListProperty(u"columnalign") 69 70 for ch in node.children: 71 rowalign = getByIndexOrLast(table_rowaligns, len(node.rows)) 72 row_columnaligns = table_columnaligns 73 if ch.elementName == u"mtr" or ch.elementName == "mlabeledtr": 74 cells = ch.children 75 rowalign = ch.attributes.get(u"rowalign", rowalign) 76 if u"columnalign" in ch.attributes.keys(): 77 columnaligns = node.getListProperty(u"columnalign", ch.attributes.get(u"columnalign")) 78 else: 79 cells = [ch] 80 81 row = RowDescriptor(node, cells, rowalign, row_columnaligns, busycells) 82 node.rows.append(row) 83 # busycells passes information about cells spanning multiple rows 84 busycells = [max (0, n - 1) for n in busycells] 85 while len(busycells) < len(row.cells): busycells.append(0) 86 for i in range (len(row.cells)): 87 cell = row.cells[i] 88 if cell is None: continue 89 if cell.rowspan > 1: 90 for j in range(i, i + cell.colspan): 91 busycells[j] = cell.rowspan - 1 92 93 # Pad the table with empty rows until no spanning cell protrudes 94 while max(busycells) > 0: 95 rowalign = getByIndexOrLast(table_rowaligns, len(node.rows)) 96 node.rows.append(RowDescriptor(node, [], rowalign, table_columnaligns, busycells)) 97 busycells = [max (0, n - 1) for n in busycells] 98 99def arrangeLines(node): 100 # Get spacings and line styles; expand to cover the table fully 101 spacings = map(node.parseLength, node.getListProperty(u"rowspacing")) 102 lines = node.getListProperty(u"rowlines") 103 104 for i in range(len(node.rows) - 1): 105 node.rows[i].spaceAfter = getByIndexOrLast(spacings, i) 106 line = getByIndexOrLast(lines, i) 107 if line != u"none": 108 node.rows[i].lineAfter = line 109 node.rows[i].spaceAfter += node.lineWidth 110 111 spacings = map(node.parseSpace, node.getListProperty(u"columnspacing")) 112 lines = node.getListProperty(u"columnlines") 113 114 for i in range(len(node.columns) - 1): 115 node.columns[i].spaceAfter = getByIndexOrLast(spacings, i) 116 line = getByIndexOrLast(lines, i) 117 if line != u"none": 118 node.columns[i].lineAfter = line 119 node.columns[i].spaceAfter += node.lineWidth 120 121 node.framespacings = [0, 0] 122 node.framelines = [None, None] 123 spacings = map(node.parseSpace, node.getListProperty(u"framespacing")) 124 lines = node.getListProperty(u"frame") 125 for i in range(2): 126 line = getByIndexOrLast(lines, i) 127 if line != u"none": 128 node.framespacings[i] = getByIndexOrLast(spacings, i) 129 node.framelines[i] = line 130 131def calculateColumnWidths(node): 132 # Get total width 133 fullwidthattr = node.attributes.get(u"width", u"auto") 134 if fullwidthattr == u"auto": 135 fullwidth = None 136 else: 137 fullwidth = node.parseLength(fullwidthattr) 138 if fullwidth <= 0: fullwidth = None 139 140 # Fill fixed column widths 141 columnwidths = node.getListProperty(u"columnwidth") 142 for i in range(len(node.columns)): 143 column = node.columns[i] 144 attr = getByIndexOrLast(columnwidths, i) 145 if attr in [u"auto", u"fit"]: 146 column.fit = (attr == u"fit") 147 elif attr.endswith(u'%'): 148 if fullwidth is None: 149 node.error("Percents in column widths supported only in tables with explicit width; width of column %d treated as 'auto'" % (i+1)) 150 else: 151 value = node.parseFloat(attr[:-1]) 152 if value and value > 0: 153 column.width = fullwidth * value / 100 154 column.auto = False 155 else: 156 column.width = node.parseSpace(attr) 157 column.auto = False 158 159 # Set initial auto widths for cells with colspan == 1 160 for r in node.rows: 161 for i in range(len(r.cells)): 162 c = r.cells[i] 163 if c is None or c.content is None or c.colspan > 1: continue 164 column = node.columns[i] 165 if column.auto: column.width = max(column.width, c.content.width) 166 167 # Calculate auto widths for cells with colspan > 1 168 while True: 169 adjustedColumns = [] 170 adjustedWidth = 0 171 172 for r in node.rows: 173 for i in range(len(r.cells)): 174 c = r.cells[i] 175 if c is None or c.content is None or c.colspan == 1: continue 176 177 columns = node.columns[i : i + c.colspan] 178 autoColumns = [x for x in columns if x.auto] 179 if len(autoColumns) == 0: continue # nothing to adjust 180 fixedColumns = [x for x in columns if not x.auto] 181 182 fixedWidth = sum([x.spaceAfter for x in columns[:-1]]) 183 if len(fixedColumns) > 0: 184 fixedWidth += sum ([x.width for x in fixedColumns]) 185 autoWidth = sum ([x.width for x in autoColumns]) 186 if c.content.width <= fixedWidth + autoWidth: continue # already fits 187 188 requiredWidth = c.content.width - fixedWidth 189 unitWidth = requiredWidth / len(autoColumns) 190 191 while True: 192 oversizedColumns = [x for x in autoColumns if x.width >= unitWidth] 193 if len(oversizedColumns) == 0: break 194 195 autoColumns = [x for x in autoColumns if x.width < unitWidth] 196 if len(autoColumns) == 0: break # weird rounding effects 197 requiredWidth -= sum ([x.width for x in oversizedColumns]) 198 unitWidth = requiredWidth / len(autoColumns) 199 if len(autoColumns) == 0: continue; # protection against arithmetic overflow 200 201 # Store the maximum unit width 202 if unitWidth > adjustedWidth: 203 adjustedWidth = unitWidth 204 adjustedColumns = autoColumns 205 206 if len(adjustedColumns) == 0: break; 207 for col in adjustedColumns: col.width = adjustedWidth 208 209 if node.getProperty(u"equalcolumns") == u"true": 210 globalWidth = max([col.width for col in node.columns if col.auto]) 211 for col in node.columns: 212 if col.auto: col.width = globalWidth 213 214 if fullwidth is not None: 215 delta = fullwidth 216 delta -= sum ([x.width for x in node.columns]) 217 delta -= sum ([x.spaceAfter for x in node.columns[:-1]]) 218 delta -= 2 * node.framespacings[0] 219 if delta != 0: 220 sizableColumns = [x for x in node.columns if x.fit] 221 if len(sizableColumns) == 0: 222 sizableColumns = [x for x in node.columns if x.auto] 223 if len(sizableColumns) == 0: 224 node.error("Overconstrained table layout: explicit table width specified, but no column has automatic width; table width attribute ignored") 225 else: 226 delta /= len(sizableColumns) 227 for col in sizableColumns: col.width += delta 228 229def calculateRowHeights(node): 230 # Set initial row heights for cells with rowspan == 1 231 commonAxis = node.axis() 232 for r in node.rows: 233 r.height = 0 234 r.depth = 0 235 for c in r.cells: 236 if c is None or c.content is None or c.rowspan != 1: continue 237 cellAxis = c.content.axis() 238 c.vshift = 0 239 240 if c.valign == u"baseline": 241 if r.alignToAxis: cell.vshift -= commonAxis 242 if c.content.alignToAxis: c.vshift += cellAxis 243 244 elif c.valign == u"axis": 245 if not r.alignToAxis: c.vshift += commonAxis 246 if not c.content.alignToAxis: c.vshift -= cellAxis 247 248 else: 249 c.vshift = (r.height - r.depth - c.content.height + c.content.depth) / 2 250 251 r.height = max(r.height, c.content.height + c.vshift) 252 r.depth = max(r.depth, c.content.depth - c.vshift) 253 254 # Calculate heights for cells with rowspan > 1 255 while True: 256 adjustedRows = [] 257 adjustedSize = 0 258 for i in range(len(node.rows)): 259 r = node.rows[i] 260 for c in r.cells: 261 if c is None or c.content is None or c.rowspan == 1: continue 262 rows = node.rows[i : i + c.rowspan] 263 264 requiredSize = c.content.height + c.content.depth 265 requiredSize -= sum([x.spaceAfter for x in rows[:-1]]) 266 fullSize = sum ([x.height + x.depth for x in rows]) 267 if fullSize >= requiredSize: continue 268 269 unitSize = requiredSize / len(rows) 270 while True: 271 oversizedRows = [x for x in rows if x.height + x.depth >= unitSize] 272 if len(oversizedRows) == 0: break 273 274 rows = [x for x in rows if x.height + x.depth < unitSize] 275 if len(rows) == 0: break # weird rounding effects 276 requiredSize -= sum ([x.height + x.depth for x in oversizedRows]) 277 unitSize = requiredSize / len(rows) 278 if len(rows) == 0: continue; # protection against arithmetic overflow 279 280 if unitSize > adjustedSize: 281 adjustedSize = unitSize 282 adjustedRows = rows 283 284 if len(adjustedRows) == 0: break; 285 for r in adjustedRows: 286 delta = (adjustedSize - r.height - r.depth) / 2 287 r.height += delta; r.depth += delta 288 289 if node.getProperty(u"equalrows") == u"true": 290 maxvsize = max([r.height + r.depth for r in node.rows]) 291 for r in node.rows: 292 delta = (maxvsize - r.height - r.depth) / 2 293 r.height += delta; r.depth += delta 294 295 296def getAlign(node): 297 alignattr = node.getProperty(u"align").strip() 298 if len(alignattr) == 0: alignattr = mathnode.globalDefaults[u"align"] 299 300 splitalign = alignattr.split() 301 alignType = splitalign[0] 302 303 if len(splitalign) == 1: 304 alignRow = None 305 else: 306 alignRow = node.parseInt(splitalign[1]) 307 if alignrownumber == 0: 308 node.error("Alignment row number cannot be zero") 309 alignrownumber = None 310 elif alignrownumber > len(node.rows): 311 node.error("Alignment row number cannot exceed row count") 312 alignrownumber = len(node.rows) 313 elif alignrownumber < - len(node.rows): 314 node.error("Negative alignment row number cannot exceed row count") 315 alignrownumber = 1 316 elif alignrownumber < 0: 317 alignrownumber = len(node.rows) - alignrownumber + 1 318 319 return (alignType, alignRow)