1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-05 08:27:22 +03:00
WeasyPrint/weasyprint/layout/tables.py

510 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# coding: utf8
"""
weasyprint.layout.tables
------------------------
Layout for tables and internal table boxes.
:copyright: Copyright 2011-2012 Simon Sapin and contributors, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from __future__ import division, unicode_literals
from ..compat import xrange
from ..logger import LOGGER
from ..formatting_structure import boxes
from .percentages import resolve_percentages, resolve_one_percentage
from .preferred import preferred_minimum_width, preferred_width
def table_layout(document, table, max_position_y, skip_stack,
containing_block, device_size, page_is_empty):
"""Layout for a table box.
For now only the fixed layout and separate border model are supported.
"""
# Avoid a circular import
from .blocks import block_level_height
column_widths = table.column_widths
border_spacing_x, border_spacing_y = table.style.border_spacing
# TODO: reverse this for direction: rtl
column_positions = []
position_x = table.content_box_x()
rows_x = position_x + border_spacing_x
for width in column_widths:
position_x += border_spacing_x
column_positions.append(position_x)
position_x += width
rows_width = position_x - rows_x
if skip_stack is None:
skip = 0
else:
skip, skip_stack = skip_stack
resume_at = None
# Layout for row groups, rows and cells
position_y = table.content_box_y() + border_spacing_y
initial_position_y = position_y
new_table_children = []
for index_group, group in table.enumerate_skip(skip):
resolve_percentages(group, containing_block=table)
group.position_x = rows_x
group.position_y = position_y
group.width = rows_width
new_group_children = []
# For each rows, cells for which this is the last row (with rowspan)
ending_cells_by_row = [[] for row in group.children]
if skip_stack is None:
skip = 0
else:
skip, skip_stack = skip_stack
for index_row, row in group.enumerate_skip(skip):
resolve_percentages(row, containing_block=table)
row.position_x = rows_x
row.position_y = position_y
row.width = rows_width
# Place cells at the top of the row and layout their content
new_row_children = []
for cell in row.children:
spanned_widths = column_widths[cell.grid_x:][:cell.colspan]
# In the fixed layout the grid width is set by cells in
# the first row and column elements.
# This may be less than the previous value of cell.colspan
# if that would bring the cell beyond the grid width.
cell.colspan = len(spanned_widths)
if cell.colspan == 0:
# The cell is entierly beyond the grid width, remove it
# entierly. Subsequent cells in the same row have greater
# grid_x, so they are beyond too.
cell_index = row.children.index(cell)
ignored_cells = row.children[cell_index:]
LOGGER.warn('This table row has more columns than '
'the table, ignored %i cells: %r',
len(ignored_cells), ignored_cells)
break
resolve_percentages(cell, containing_block=table)
cell.position_x = column_positions[cell.grid_x]
cell.position_y = row.position_y
cell.margin_top = 0
cell.margin_left = 0
cell.width = 0
borders_plus_padding = cell.border_width() # with width==0
cell.width = (
sum(spanned_widths)
+ border_spacing_x * (cell.colspan - 1)
- borders_plus_padding)
# The computed height is a minimum
computed_cell_height = cell.height
cell.height = 'auto'
cell, _, _, _, _ = block_level_height(
document, cell,
max_position_y=float('inf'),
skip_stack=None,
device_size=device_size,
page_is_empty=True)
if computed_cell_height != 'auto':
cell.height = max(cell.height, computed_cell_height)
new_row_children.append(cell)
row = row.copy_with_children(new_row_children)
# Table height algorithm
# http://www.w3.org/TR/CSS21/tables.html#height-layout
# cells with vertical-align: baseline
baseline_cells = []
for cell in row.children:
vertical_align = cell.style.vertical_align
if vertical_align in ('top', 'middle', 'bottom'):
cell.vertical_align = vertical_align
else:
# Assume 'baseline' for any other value
cell.vertical_align = 'baseline'
cell.baseline = cell_baseline(cell)
baseline_cells.append(cell)
if baseline_cells:
row.baseline = max(cell.baseline for cell in baseline_cells)
for cell in baseline_cells:
if cell.baseline != row.baseline:
add_top_padding(cell, row.baseline - cell.baseline)
else:
row.baseline = None
# row height
for cell in row.children:
ending_cells_by_row[cell.rowspan - 1].append(cell)
ending_cells = ending_cells_by_row.pop(0)
if ending_cells: # in this row
row_bottom_y = max(
cell.position_y + cell.border_height()
for cell in ending_cells)
row.height = row_bottom_y - row.position_y
else:
row_bottom_y = row.position_y
row.height = 0
# Add extra padding to make the cells the same height as the row
for cell in ending_cells:
cell_bottom_y = cell.position_y + cell.border_height()
extra = row_bottom_y - cell_bottom_y
if cell.vertical_align == 'bottom':
add_top_padding(cell, extra)
elif cell.vertical_align == 'middle':
extra /= 2.
add_top_padding(cell, extra)
cell.padding_bottom += extra
else:
cell.padding_bottom += extra
next_position_y = position_y + row.height + border_spacing_y
# Break if this row overflows the page, unless there is no
# other content on the page.
if next_position_y > max_position_y and (
new_table_children or new_group_children
or not page_is_empty):
resume_at = (index_row, None)
break
position_y = next_position_y
new_group_children.append(row)
skip_stack = None
if resume_at and group.style.page_break_inside == 'avoid':
resume_at = (index_group, None)
break
# Do not keep the row group if we made a page break
# before any of its rows.
if not (group.children and not new_group_children):
group = group.copy_with_children(new_group_children)
new_table_children.append(group)
# Set missing baselines in a second loop because of rowspan
for row in group.children:
if row.baseline is None:
if row.children:
# lowest bottom content edge
row.baseline = row.position_y - max(
cell.content_box_y() + cell.height
for cell in row.children)
else:
row.baseline = 0
group.height = position_y - group.position_y
if group.children:
# The last border spacing is outside of the group.
group.height -= border_spacing_y
if resume_at:
resume_at = (index_group, resume_at)
break
table = table.copy_with_children(new_table_children)
# If the height property has a bigger value, just add blank space
# below the last row group.
table.height = max(
table.height if table.height != 'auto' else 0,
position_y - table.content_box_y())
# Layout for column groups and columns
columns_height = position_y - initial_position_y
if table.children:
# The last border spacing is below the columns.
columns_height -= border_spacing_y
for group in table.column_groups:
for column in group.children:
resolve_percentages(column, containing_block=table)
column.position_x = column_positions[column.grid_x]
column.position_y = initial_position_y
column.width = column_widths[column.grid_x]
column.height = columns_height
resolve_percentages(group, containing_block=table)
first = group.children[0]
last = group.children[-1]
group.position_x = first.position_x
group.position_y = initial_position_y
group.width = last.position_x + last.width - first.position_x
group.height = columns_height
if resume_at and not page_is_empty and (
table.style.page_break_inside == 'avoid'):
table = None
resume_at = None
next_page = 'any'
adjoining_margins = []
collapsing_through = False
return table, resume_at, next_page, adjoining_margins, collapsing_through
def add_top_padding(box, extra_padding):
"""Increase the top padding of a box. This also translates the children.
"""
box.padding_top += extra_padding
for child in box.children:
child.translate(dy=extra_padding)
def fixed_table_layout(table):
"""Run the fixed table layout and return a list of column widths
http://www.w3.org/TR/CSS21/tables.html#fixed-table-layout
"""
assert table.width != 'auto'
all_columns = [column for column_group in table.column_groups
for column in column_group.children]
if table.children and table.children[0].children:
first_rowgroup = table.children[0]
first_row_cells = first_rowgroup.children[0].children
else:
first_row_cells = []
num_columns = max(
len(all_columns),
sum(cell.colspan for cell in first_row_cells)
)
# ``None`` means not know yet.
column_widths = [None] * num_columns
# `width` on column boxes
for i, column in enumerate(all_columns):
resolve_one_percentage(column, 'width', table.width)
if column.width != 'auto':
column_widths[i] = column.width
# `width` on cells of the first row.
border_spacing_x, border_spacing_y = table.style.border_spacing
i = 0
for cell in first_row_cells:
resolve_percentages(cell, table)
if cell.width != 'auto':
width = cell.border_width()
width -= border_spacing_x * (cell.colspan - 1)
# In the general case, this width affects several columns (through
# colspan) some of which already have a width. Subscract these
# known widths and divide among remaining columns.
columns_without_width = [] # and occupied by this cell
for j in xrange(i, i + cell.colspan):
if column_widths[j] is None:
columns_without_width.append(j)
else:
width -= column_widths[j]
width_per_column = width / len(columns_without_width)
for j in columns_without_width:
column_widths[j] = width_per_column
i += cell.colspan
# Distribute the remaining space equally on columns that do not have
# a width yet.
all_border_spacing = border_spacing_x * (num_columns + 1)
min_table_width = (sum(w for w in column_widths if w is not None)
+ all_border_spacing)
columns_without_width = [i for i, width in enumerate(column_widths)
if width is None]
if columns_without_width and table.width >= min_table_width:
remaining_width = table.width - min_table_width
width_per_column = remaining_width / len(columns_without_width)
for i in columns_without_width:
column_widths[i] = width_per_column
else:
# XXX this is bad, but we were given a broken table to work with...
for i in columns_without_width:
column_widths[i] = 0
# If the sum is less than the table width,
# distribute the remaining space equally
extra_width = table.width - sum(column_widths) - all_border_spacing
if extra_width <= 0:
# substract a negative: widen the table
table.width -= extra_width
elif num_columns:
extra_per_column = extra_width / num_columns
column_widths = [w + extra_per_column for w in column_widths]
# Now we have table.width == sum(column_widths) + all_border_spacing
# with possible floating point rounding errors.
# (unless there is zero column)
table.column_widths = column_widths
def auto_table_layout(table, wrapper, containing_block):
"""Run the auto table layout and return a list of column widths.
http://www.w3.org/TR/CSS21/tables.html#auto-table-layout
"""
# TODO: change this naive code, don't go through the whole table so many
# times
# First of all, we have to get a grid with cells inside
nb_columns = 0
rows = []
for i, row_group in enumerate(table.children):
assert isinstance(row_group, boxes.TableRowGroupBox)
for j, row in enumerate(row_group.children):
assert isinstance(row, boxes.TableRowBox)
rows.append(row)
for k, cell in enumerate(row.children):
assert isinstance(cell, boxes.TableCellBox)
nb_columns = max(nb_columns, k + cell.colspan)
nb_rows = len(rows)
colspan_cells = []
grid = [[None] * nb_columns for i in range(nb_rows)]
for i, row in enumerate(rows):
for cell in row.children:
if cell.colspan == 1:
grid[i][cell.grid_x] = cell
else:
cell.grid_y = i
colspan_cells.append(cell)
# Point #1
column_preferred_widths = [[0] * nb_rows for i in range(nb_columns)]
column_preferred_minimum_widths = [
[0] * nb_rows for i in range(nb_columns)]
for i, row in enumerate(grid):
for j, cell in enumerate(row):
if cell:
# TODO: when border-collapse: collapse; set outer=False
column_preferred_widths[j][i] = \
preferred_width(cell)
column_preferred_minimum_widths[j][i] = \
preferred_minimum_width(cell)
column_preferred_widths = [
max(widths) for widths in column_preferred_widths]
column_preferred_minimum_widths = [
max(widths) for widths in column_preferred_minimum_widths]
# Point #2
column_groups_widths = []
column_widths = [None] * nb_columns
for column_group in table.column_groups:
assert isinstance(column_group, boxes.TableColumnGroupBox)
column_groups_widths.append((column_group, column_group.style.width))
for column in column_group.children:
assert isinstance(column, boxes.TableColumnBox)
column_widths[column.grid_x] = column.style.width
# TODO: handle percentages for column widths
if column_widths:
for widths in (column_preferred_widths,
column_preferred_minimum_widths):
for i, width in enumerate(widths):
column_width = column_widths[i]
if (column_width and column_width != 'auto' and
column_width.unit != '%'):
widths[i] = max(column_width.value, widths[i])
# Point #3
for cell in colspan_cells:
column_slice = slice(cell.grid_x, cell.grid_x + cell.colspan)
# TODO: when border-collapse: collapse; set outer=False
cell_width = preferred_width(cell)
columns_width = sum(column_preferred_widths[column_slice])
if cell_width > columns_width:
added_space = (cell_width - columns_width) / cell.colspan
for i in range(cell.grid_x, cell.grid_x + cell.colspan):
column_preferred_widths[i] += added_space
# TODO: when border-collapse: collapse; set outer=False
cell_minimum_width = preferred_minimum_width(cell)
columns_minimum_width = sum(
column_preferred_minimum_widths[column_slice])
if cell_minimum_width > columns_minimum_width:
added_space = (
(cell_minimum_width - columns_minimum_width) / cell.colspan)
for i in range(cell.grid_x, cell.grid_x + cell.colspan):
column_preferred_minimum_widths[i] += added_space
# Point #4
for column_group, column_group_width in column_groups_widths:
# TODO: handle percentages for column group widths
if (column_group_width and column_group_width != 'auto' and
column_group_width.unit != '%'):
column_indexes = [
column.grid_x for column in column_group.children]
columns_width = sum(
column_preferred_minimum_widths[index]
for index in column_indexes)
column_group_width = column_group_width.value
if column_group_width > columns_width:
added_space = (
(column_group_width - columns_width) / len(column_indexes))
for i in column_indexes:
column_preferred_minimum_widths[i] += added_space
# TODO: Do we need to do this? The spec seems to say that
# the colgroup's width is just a hint for column group's
# columns minimum width, but if the sum of the preferred
# maximum width of the colums is lower or greater than the
# colgroup's one, then the columns don't follow the hint.
if (column_preferred_widths[i] <
column_preferred_minimum_widths[i]):
column_preferred_widths[i] = \
column_preferred_minimum_widths[i]
total_border_spacing = (nb_columns + 1) * table.style.border_spacing[0]
table_preferred_minimum_width = (
sum(column_preferred_minimum_widths) + total_border_spacing)
table_preferred_width = sum(column_preferred_widths) + total_border_spacing
table_maximum_width = max(
containing_block.width, table_preferred_minimum_width)
# TODO: handle the border spacings
table.width = min(table_preferred_width, table_maximum_width)
# TODO: find a better algorithm
if table_preferred_width == table.width:
# Preferred width fits in the containing block
table.column_widths = column_preferred_widths
else:
# Preferred width is too large for the containing block
# Use the minimum widths and add the lost space to the columns
# according to their preferred widths
table.column_widths = column_preferred_minimum_widths
lost_width = (
min(containing_block.width, table_preferred_width) -
table_preferred_minimum_width)
if lost_width > 0:
table.column_widths = [
(column_width + lost_width *
preferred_column_width / table_preferred_width)
for (preferred_column_width, column_width)
in zip(column_preferred_widths,
column_preferred_minimum_widths)]
def cell_baseline(cell):
"""
Return the y position of a cells baseline from the top of its border box.
See http://www.w3.org/TR/CSS21/tables.html#height-layout
"""
# Do not use cell.descendants() as we do not want to recurse into
# out-of-flow children.
stack = [iter(cell.children)] # DIY recursion
while stack:
child = next(stack[-1], None)
if child is None:
stack.pop()
continue
if child.is_in_normal_flow():
if isinstance(child, (boxes.LineBox, boxes.TableRowBox)):
# First in-flow line or row.
return child.baseline + child.position_y - cell.position_y
if isinstance(child, boxes.ParentBox):
stack.append(iter(child.children))
# Default to the bottom of the content area.
return cell.border_top_width + cell.padding_top + cell.height