1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-05 16:37:47 +03:00
WeasyPrint/weasyprint/layout/preferred.py

525 lines
20 KiB
Python
Raw Normal View History

# coding: utf8
"""
weasyprint.layout.preferred
---------------------------
Preferred and minimum preferred width, aka. the shrink-to-fit algorithm.
2014-01-10 18:27:02 +04:00
:copyright: Copyright 2011-2014 Simon Sapin and contributors, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from __future__ import division, unicode_literals
import weakref
from ..formatting_structure import boxes
from .. import text
from .replaced import default_image_sizing
2012-03-23 20:33:59 +04:00
2012-07-12 19:13:21 +04:00
def shrink_to_fit(context, box, available_width):
2012-03-23 22:31:54 +04:00
"""Return the shrink-to-fit width of ``box``.
*Warning:* both available_outer_width and the return value are
for width of the *content area*, not margin area.
2012-03-23 22:31:54 +04:00
http://www.w3.org/TR/CSS21/visudet.html#float-width
"""
return min(
max(
2012-07-12 19:13:21 +04:00
preferred_minimum_width(context, box, outer=False),
available_width),
2012-07-12 19:13:21 +04:00
preferred_width(context, box, outer=False))
2012-03-22 21:36:56 +04:00
2012-07-12 19:13:21 +04:00
def preferred_minimum_width(context, box, outer=True):
"""Return the preferred minimum width for ``box``.
This is the width by breaking at every line-break opportunity.
"""
if isinstance(box, boxes.BlockContainerBox):
if box.is_table_wrapper:
2012-07-12 19:13:21 +04:00
return table_preferred_minimum_width(context, box, outer)
else:
2012-07-12 19:13:21 +04:00
return block_preferred_minimum_width(context, box, outer)
elif isinstance(box, (boxes.InlineBox, boxes.LineBox)):
2013-04-22 19:26:41 +04:00
return inline_preferred_minimum_width(
context, box, outer, is_line_start=True)
2012-05-25 16:08:35 +04:00
elif isinstance(box, boxes.ReplacedBox):
return replaced_preferred_width(box, outer)
else:
raise TypeError(
'Preferred minimum width for %s not handled yet' %
type(box).__name__)
2012-07-12 19:13:21 +04:00
def preferred_width(context, box, outer=True):
"""Return the preferred width for ``box``.
This is the width by only breaking at forced line breaks.
"""
if isinstance(box, boxes.BlockContainerBox):
if box.is_table_wrapper:
2012-07-12 19:13:21 +04:00
return table_preferred_width(context, box, outer)
else:
2012-07-12 19:13:21 +04:00
return block_preferred_width(context, box, outer)
elif isinstance(box, (boxes.InlineBox, boxes.LineBox)):
return inline_preferred_width(context, box, outer, is_line_start=True)
2012-05-25 16:08:35 +04:00
elif isinstance(box, boxes.ReplacedBox):
return replaced_preferred_width(box, outer)
else:
raise TypeError(
'Preferred width for %s not handled yet' % type(box).__name__)
2012-07-12 19:13:21 +04:00
def _block_preferred_width(context, box, function, outer):
2012-03-23 20:33:59 +04:00
"""Helper to create ``block_preferred_*_width.``"""
2012-04-05 15:16:17 +04:00
width = box.style.width
if width == 'auto' or width.unit == '%':
# "percentages on the following properties are treated instead as
# though they were the following: width: auto"
# http://dbaron.org/css/intrinsic/#outer-intrinsic
2012-05-25 17:57:13 +04:00
children_widths = [
2012-07-12 19:13:21 +04:00
function(context, child, outer=True) for child in box.children
if not child.is_absolutely_positioned()]
2012-05-25 17:57:13 +04:00
width = max(children_widths) if children_widths else 0
2012-03-23 20:33:59 +04:00
else:
2012-04-05 15:16:17 +04:00
assert width.unit == 'px'
width = width.value
return adjust(box, outer, width)
def adjust(box, outer, width, left=True, right=True):
"""Add paddings, borders and margins to ``width`` when ``outer`` is set."""
if not outer:
return width
min_width = box.style.min_width
max_width = box.style.max_width
min_width = min_width.value if min_width.unit != '%' else 0
max_width = max_width.value if max_width.unit != '%' else float('inf')
fixed = max(min_width, min(width, max_width))
percentages = 0
for value in (
(['margin_left', 'padding_left'] if left else []) +
(['margin_right', 'padding_right'] if right else [])
):
2012-04-10 21:00:56 +04:00
# Padding and border are set on the table, not on the wrapper
# http://www.w3.org/TR/CSS21/tables.html#model
# TODO: clean this horrible hack!
if box.is_table_wrapper and value == 'padding_left':
box = box.get_wrapped_table()
style_value = box.style[value]
if style_value != 'auto':
if style_value.unit == 'px':
fixed += style_value.value
else:
assert style_value.unit == '%'
percentages += style_value.value
if left:
fixed += box.style.border_left_width
if right:
fixed += box.style.border_right_width
2012-04-10 21:00:56 +04:00
2012-04-12 17:11:33 +04:00
if percentages < 100:
return fixed / (1 - percentages / 100.)
else:
# Pathological case, ignore
2012-03-23 20:33:59 +04:00
return 0
2012-07-12 19:13:21 +04:00
def block_preferred_minimum_width(context, box, outer=True):
2012-03-23 20:33:59 +04:00
"""Return the preferred minimum width for a ``BlockBox``."""
return _block_preferred_width(
2012-07-12 19:13:21 +04:00
context, box, preferred_minimum_width, outer)
2012-07-12 19:13:21 +04:00
def block_preferred_width(context, box, outer=True):
"""Return the preferred width for a ``BlockBox``."""
2012-07-12 19:13:21 +04:00
return _block_preferred_width(context, box, preferred_width, outer)
def table_cell_preferred_minimum_width(context, box, table,
resolved_table_width, outer=True):
"""Return the preferred minimum width for a ``TableCellBox``."""
# Try to solve the cell's width if it is a percentage
width = box.style.width
2014-04-27 15:29:55 +04:00
if (resolved_table_width and table.width != 'auto' and
width != 'auto' and width.unit == '%'):
return width.value / 100. * table.width
# Else return standard block's preferred minimum width
return _block_preferred_width(
context, box, preferred_minimum_width, outer)
def table_cell_preferred_width(context, box, table, resolved_table_width,
outer=True):
"""Return the preferred width for a ``TableCellBox``."""
# Try to solve the cell's width if it is a percentage
width = box.style.width
2014-04-27 15:29:55 +04:00
if (resolved_table_width and table.width != 'auto' and
width != 'auto' and width.unit == '%'):
return width.value / 100. * table.width
# Else return standard block's preferred width
return _block_preferred_width(context, box, preferred_width, outer)
2012-07-12 19:13:21 +04:00
def inline_preferred_minimum_width(context, box, outer=True, skip_stack=None,
2013-04-22 19:26:41 +04:00
first_line=False, is_line_start=False):
"""Return the preferred minimum width for an ``InlineBox``.
The width is calculated from the lines from ``skip_stack``. If
``first_line`` is ``True``, only the first line minimum width is
calculated.
"""
widths = list(inline_line_widths(
context, box, outer, is_line_start, minimum=True,
skip_stack=skip_stack))
if first_line and len(widths) > 1:
del widths[1:]
else:
widths[-1] -= trailing_whitespace_size(context, box)
return adjust(box, outer, max(widths))
def inline_preferred_width(context, box, outer=True, is_line_start=False):
"""Return the preferred width for an ``InlineBox``."""
widths = list(
inline_line_widths(context, box, outer, is_line_start, minimum=False))
widths[-1] -= trailing_whitespace_size(context, box)
return adjust(box, outer, max(widths))
def inline_line_widths(context, box, outer, is_line_start, minimum,
skip_stack=None):
current_line = 0
if skip_stack is None:
skip = 0
else:
skip, skip_stack = skip_stack
for index, child in box.enumerate_skip(skip):
if child.is_absolutely_positioned():
continue # Skip
2012-06-28 21:07:26 +04:00
if isinstance(child, boxes.InlineBox):
lines = list(inline_line_widths(
context, child, outer, is_line_start, minimum, skip_stack))
if len(lines) == 1:
lines[0] = adjust(child, outer, lines[0])
else:
lines[0] = adjust(child, outer, lines[0], right=False)
lines[-1] = adjust(child, outer, lines[-1], left=False)
2012-06-28 21:07:26 +04:00
elif isinstance(child, boxes.TextBox):
if skip_stack is None:
skip = 0
else:
skip, skip_stack = skip_stack
assert skip_stack is None
child_text = child.text[(skip or 0):]
if is_line_start:
child_text = child_text.lstrip(' ')
if minimum and child_text == ' ':
lines = [0, 0]
else:
lines = list(text.line_widths(
child_text, child.style, context.enable_hinting,
width=0 if minimum else None))
2012-06-28 21:07:26 +04:00
else:
# http://www.w3.org/TR/css3-text/#line-break-details
# "The line breaking behavior of a replaced element
# or other atomic inline is equivalent to that
# of the Object Replacement Character (U+FFFC)."
# http://www.unicode.org/reports/tr14/#DescriptionOfProperties
# "By default, there is a break opportunity
# both before and after any inline object."
if minimum:
lines = [0, preferred_width(context, child), 0]
else:
lines = [preferred_width(context, child)]
# The first text line goes on the current line
current_line += lines[0]
if len(lines) > 1:
# Forced line break
yield current_line
if len(lines) > 2:
for line in lines[1:-1]:
yield line
current_line = lines[-1]
is_line_start = lines[-1] == 0
skip_stack = None
yield current_line
TABLE_CACHE = weakref.WeakKeyDictionary()
2013-04-11 14:08:53 +04:00
2012-07-12 19:13:21 +04:00
def table_and_columns_preferred_widths(context, box, outer=True,
resolved_table_width=False):
"""Return preferred widths for the table and its columns.
If ``resolved_table_width`` is ``True``, the resolved width (instead of the
one given in ``box.style``) is used to get the preferred widths.
The tuple returned is
``(table_preferred_minimum_width, table_preferred_width,
column_preferred_minimum_widths, column_preferred_widths)``
http://www.w3.org/TR/CSS21/tables.html#auto-table-layout
"""
table = box.get_wrapped_table()
result = TABLE_CACHE.get(table)
if result:
return result
if table.style.border_collapse == 'separate':
border_spacing_x, _ = table.style.border_spacing
else:
border_spacing_x = 0
nb_columns = 0
2012-06-01 11:29:28 +04:00
if table.column_groups:
last_column_group = table.column_groups[-1]
# Column groups always have at least one child column.
last_column = last_column_group.children[-1]
# +1 as the grid starts at 0
nb_columns = last_column.grid_x + 1
rows = []
for i, row_group in enumerate(table.children):
for j, row in enumerate(row_group.children):
rows.append(row)
2012-06-01 11:29:28 +04:00
if row.children:
last_cell = row.children[-1]
row_grid_width = last_cell.grid_x + last_cell.colspan
nb_columns = max(nb_columns, row_grid_width)
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:
column_preferred_widths[j][i] = \
table_cell_preferred_width(
context, cell, table, resolved_table_width)
column_preferred_minimum_widths[j][i] = \
table_cell_preferred_minimum_width(
context, cell, table, resolved_table_width)
column_preferred_widths = [
2012-06-01 11:29:28 +04:00
max(widths) if widths else 0
for widths in column_preferred_widths]
column_preferred_minimum_widths = [
2012-06-01 11:29:28 +04:00
max(widths) if widths else 0
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
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':
if column_width.unit == '%' and resolved_table_width:
# TODO: If resolved_table_width is false, we should try
# to use this percentage as a constraint
widths[i] = max(
column_width.value / 100. * table.width, widths[i])
elif 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)
cell_width = (
table_cell_preferred_width(
context, cell, table, resolved_table_width) -
border_spacing_x * (cell.colspan - 1))
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
cell_minimum_width = (
table_cell_preferred_minimum_width(
context, cell, table, resolved_table_width) -
border_spacing_x * (cell.colspan - 1))
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:
if (column_group_width and column_group_width != 'auto' and
2013-08-03 18:35:19 +04:00
(column_group_width.unit != '%' or resolved_table_width)):
column_indexes = [
column.grid_x for column in column_group.children]
columns_width = sum(
column_preferred_minimum_widths[index]
for index in column_indexes)
2013-08-03 18:35:19 +04:00
if column_group_width.unit == '%':
column_group_width = (
column_group_width.value / 100. * table.width)
else:
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
# 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. These lines make the preferred
# width equal or greater than the minimum preferred width.
if (column_preferred_widths[i] <
2013-04-11 14:08:53 +04:00
column_preferred_minimum_widths[i]):
column_preferred_widths[i] = \
column_preferred_minimum_widths[i]
total_border_spacing = (nb_columns + 1) * border_spacing_x
table_preferred_minimum_width = (
sum(column_preferred_minimum_widths) + total_border_spacing)
table_preferred_width = sum(column_preferred_widths) + total_border_spacing
captions = [child for child in box.children
if child is not table and not child.is_absolutely_positioned()]
2012-04-10 19:56:23 +04:00
if captions:
caption_width = max(
2012-07-12 19:13:21 +04:00
preferred_minimum_width(context, caption) for caption in captions)
2012-04-10 19:56:23 +04:00
else:
caption_width = 0
if table.style.width != 'auto':
# Take care of the table width
if resolved_table_width:
if table.width > table_preferred_minimum_width:
table_preferred_minimum_width = table.width
else:
if (table.style.width.unit != '%' and
2013-04-11 14:08:53 +04:00
table.style.width.value > table_preferred_minimum_width):
table_preferred_minimum_width = table.style.width.value
2012-04-10 19:56:23 +04:00
if table_preferred_minimum_width < caption_width:
table_preferred_minimum_width = caption_width
if table_preferred_minimum_width > table_preferred_width:
table_preferred_width = table_preferred_minimum_width
result = (
table_preferred_minimum_width, table_preferred_width,
column_preferred_minimum_widths, column_preferred_widths)
TABLE_CACHE[table] = result
return result
2012-07-12 19:13:21 +04:00
def table_preferred_minimum_width(context, box, outer=True):
"""Return the preferred minimum width for a ``TableBox``. wrapper"""
resolved_table_width = box.style.width != 'auto'
minimum_width, _, _, _ = table_and_columns_preferred_widths(
context, box, resolved_table_width)
return adjust(box, outer, minimum_width)
2012-07-12 19:13:21 +04:00
def table_preferred_width(context, box, outer=True):
"""Return the preferred width for a ``TableBox`` wrapper."""
resolved_table_width = box.style.width != 'auto'
_, width, _, _ = table_and_columns_preferred_widths(
context, box, resolved_table_width)
return adjust(box, outer, width)
def replaced_preferred_width(box, outer=True):
"""Return the preferred minimum width for an ``InlineReplacedBox``."""
2012-04-05 15:16:17 +04:00
width = box.style.width
if width == 'auto' or width.unit == '%':
height = box.style.height
if height == 'auto' or height.unit == '%':
height = 'auto'
else:
assert height.unit == 'px'
height = height.value
image = box.replacement
iwidth, iheight = image.get_intrinsic_size(box.style.image_resolution)
width, _ = default_image_sizing(
iwidth, iheight, image.intrinsic_ratio, 'auto', height,
default_width=300, default_height=150)
else:
assert width.unit == 'px'
width = width.value
return adjust(box, outer, width)
def trailing_whitespace_size(context, box):
"""Return the size of the trailing whitespace of ``box``."""
from .inlines import split_text_box, split_first_line
while isinstance(box, (boxes.InlineBox, boxes.LineBox)):
if not box.children:
return 0
box = box.children[-1]
if not (isinstance(box, boxes.TextBox) and box.text and
box.style.white_space in ('normal', 'nowrap', 'pre-line')):
return 0
stripped_text = box.text.rstrip(' ')
if box.style.font_size == 0 or len(stripped_text) == len(box.text):
return 0
if stripped_text:
old_box, _, _ = split_text_box(context, box, None, None, 0)
assert old_box
stripped_box = box.copy_with_text(stripped_text)
stripped_box, resume, _ = split_text_box(
context, stripped_box, None, None, 0)
assert stripped_box is not None
assert resume is None
return old_box.width - stripped_box.width
else:
_, _, _, width, _, _ = split_first_line(
box.text, box.style, context.enable_hinting,
None, None)
return width