ajout fichiers manquant

This commit is contained in:
Ambulance Clerc
2022-03-04 18:47:24 +01:00
parent 2e92cb8cf4
commit aa5b7f9254
170 changed files with 554027 additions and 1 deletions

View File

@@ -0,0 +1,950 @@
"""
weasyprint.layout.block
-----------------------
Page breaking and layout for block-level and block-container boxes.
"""
from ..formatting_structure import boxes
from .absolute import AbsolutePlaceholder, absolute_layout
from .column import columns_layout
from .flex import flex_layout
from .float import avoid_collisions, float_layout, get_clearance
from .inline import iter_line_boxes
from .min_max import handle_min_max_width
from .percent import resolve_percentages, resolve_position_percentages
from .replaced import block_replaced_box_layout
from .table import table_layout, table_wrapper_width
def block_level_layout(context, box, bottom_space, skip_stack,
containing_block, page_is_empty, absolute_boxes,
fixed_boxes, adjoining_margins, discard):
"""Lay out the block-level ``box``."""
if not isinstance(box, boxes.TableBox):
resolve_percentages(box, containing_block)
if box.margin_top == 'auto':
box.margin_top = 0
if box.margin_bottom == 'auto':
box.margin_bottom = 0
if context.current_page > 1 and page_is_empty:
# TODO: this condition is wrong, it only works for blocks whose
# parent breaks collapsing margins. It should work for blocks whose
# one of the ancestors breaks collapsing margins.
# See test_margin_break_clearance.
collapse_with_page = (
containing_block.is_for_root_element or
adjoining_margins)
if collapse_with_page:
if box.style['margin_break'] == 'discard':
box.margin_top = 0
elif box.style['margin_break'] == 'auto':
if not context.forced_break:
box.margin_top = 0
collapsed_margin = collapse_margin(
adjoining_margins + [box.margin_top])
box.clearance = get_clearance(context, box, collapsed_margin)
if box.clearance is not None:
top_border_edge = box.position_y + collapsed_margin + box.clearance
box.position_y = top_border_edge - box.margin_top
adjoining_margins = []
return block_level_layout_switch(
context, box, bottom_space, skip_stack, containing_block,
page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins, discard)
def block_level_layout_switch(context, box, bottom_space, skip_stack,
containing_block, page_is_empty, absolute_boxes,
fixed_boxes, adjoining_margins, discard):
"""Call the layout function corresponding to the ``box`` type."""
if isinstance(box, boxes.TableBox):
return table_layout(
context, box, bottom_space, skip_stack, containing_block,
page_is_empty, absolute_boxes, fixed_boxes)
elif isinstance(box, boxes.BlockBox):
return block_box_layout(
context, box, bottom_space, skip_stack, containing_block,
page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins,
discard)
elif isinstance(box, boxes.BlockReplacedBox):
return block_replaced_box_layout(context, box, containing_block)
elif isinstance(box, boxes.FlexBox):
return flex_layout(
context, box, bottom_space, skip_stack, containing_block,
page_is_empty, absolute_boxes, fixed_boxes)
else: # pragma: no cover
raise TypeError(f'Layout for {type(box).__name__} not handled yet')
def block_box_layout(context, box, bottom_space, skip_stack,
containing_block, page_is_empty, absolute_boxes,
fixed_boxes, adjoining_margins, discard):
"""Lay out the block ``box``."""
if (box.style['column_width'] != 'auto' or
box.style['column_count'] != 'auto'):
result = columns_layout(
context, box, bottom_space, skip_stack, containing_block,
page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins)
resume_at = result[1]
# TODO: this condition and the whole relayout are probably wrong
if resume_at is None:
new_box = result[0]
columns_bottom_space = (
new_box.margin_bottom + new_box.padding_bottom +
new_box.border_bottom_width)
if columns_bottom_space:
bottom_space += columns_bottom_space
result = columns_layout(
context, box, bottom_space, skip_stack,
containing_block, page_is_empty, absolute_boxes,
fixed_boxes, adjoining_margins)
return result
elif box.is_table_wrapper:
table_wrapper_width(
context, box, (containing_block.width, containing_block.height))
block_level_width(box, containing_block)
result = block_container_layout(
context, box, bottom_space, skip_stack, page_is_empty,
absolute_boxes, fixed_boxes, adjoining_margins, discard)
new_box = result[0]
if new_box and new_box.is_table_wrapper:
# Don't collide with floats
# http://www.w3.org/TR/CSS21/visuren.html#floats
position_x, position_y, _ = avoid_collisions(
context, new_box, containing_block, outer=False)
new_box.translate(
position_x - new_box.position_x, position_y - new_box.position_y)
return result
@handle_min_max_width
def block_level_width(box, containing_block):
"""Set the ``box`` width."""
# 'cb' stands for 'containing block'
if isinstance(containing_block, boxes.Box):
cb_width = containing_block.width
direction = containing_block.style['direction']
else:
cb_width = containing_block[0]
# TODO: what is the real text direction?
direction = 'ltr'
# http://www.w3.org/TR/CSS21/visudet.html#blockwidth
# These names are waaay too long
margin_l = box.margin_left
margin_r = box.margin_right
padding_l = box.padding_left
padding_r = box.padding_right
border_l = box.border_left_width
border_r = box.border_right_width
width = box.width
# Only margin-left, margin-right and width can be 'auto'.
# We want: width of containing block ==
# margin-left + border-left-width + padding-left + width
# + padding-right + border-right-width + margin-right
paddings_plus_borders = padding_l + padding_r + border_l + border_r
if box.width != 'auto':
total = paddings_plus_borders + width
if margin_l != 'auto':
total += margin_l
if margin_r != 'auto':
total += margin_r
if total > cb_width:
if margin_l == 'auto':
margin_l = box.margin_left = 0
if margin_r == 'auto':
margin_r = box.margin_right = 0
if width != 'auto' and margin_l != 'auto' and margin_r != 'auto':
# The equation is over-constrained.
if direction == 'rtl' and not box.is_column:
box.position_x += (
cb_width - paddings_plus_borders - width - margin_r - margin_l)
# Do nothing in ltr.
if width == 'auto':
if margin_l == 'auto':
margin_l = box.margin_left = 0
if margin_r == 'auto':
margin_r = box.margin_right = 0
width = box.width = cb_width - (
paddings_plus_borders + margin_l + margin_r)
margin_sum = cb_width - paddings_plus_borders - width
if margin_l == 'auto' and margin_r == 'auto':
box.margin_left = margin_sum / 2.
box.margin_right = margin_sum / 2.
elif margin_l == 'auto' and margin_r != 'auto':
box.margin_left = margin_sum - margin_r
elif margin_l != 'auto' and margin_r == 'auto':
box.margin_right = margin_sum - margin_l
def relative_positioning(box, containing_block):
"""Translate the ``box`` if it is relatively positioned."""
if box.style['position'] == 'relative':
resolve_position_percentages(box, containing_block)
if box.left != 'auto' and box.right != 'auto':
if box.style['direction'] == 'ltr':
translate_x = box.left
else:
translate_x = -box.right
elif box.left != 'auto':
translate_x = box.left
elif box.right != 'auto':
translate_x = -box.right
else:
translate_x = 0
if box.top != 'auto':
translate_y = box.top
elif box.style['bottom'] != 'auto':
translate_y = -box.bottom
else:
translate_y = 0
box.translate(translate_x, translate_y)
if isinstance(box, (boxes.InlineBox, boxes.LineBox)):
for child in box.children:
relative_positioning(child, containing_block)
def _out_of_flow_layout(context, box, index, child, new_children,
page_is_empty, absolute_boxes, fixed_boxes,
adjoining_margins, bottom_space):
stop = False
resume_at = None
out_of_flow_resume_at = None
child.position_y += collapse_margin(adjoining_margins)
if child.is_absolutely_positioned():
placeholder = AbsolutePlaceholder(child)
placeholder.index = index
new_children.append(placeholder)
if child.style['position'] == 'absolute':
absolute_boxes.append(placeholder)
else:
fixed_boxes.append(placeholder)
elif child.is_floated():
new_child, out_of_flow_resume_at = float_layout(
context, child, box, absolute_boxes, fixed_boxes, bottom_space,
skip_stack=None)
# New page if overflow
if (page_is_empty and not new_children) or not (
new_child.position_y + new_child.height >
context.page_bottom - bottom_space):
new_child.index = index
new_children.append(new_child)
else:
last_in_flow_child = find_last_in_flow_child(new_children)
page_break = block_level_page_break(last_in_flow_child, child)
resume_at = {index: None}
if new_children and page_break in ('avoid', 'avoid-page'):
result = find_earlier_page_break(
new_children, absolute_boxes, fixed_boxes)
if result:
new_children, resume_at = result
stop = True
elif child.is_running():
running_name = child.style['position'][1]
page = context.current_page
context.running_elements[running_name][page].append(child)
return stop, resume_at, out_of_flow_resume_at
def _break_line(box, new_children, lines_iterator, page_is_empty, index,
skip_stack, resume_at):
over_orphans = len(new_children) - box.style['orphans']
if over_orphans < 0 and not page_is_empty:
# Reached the bottom of the page before we had
# enough lines for orphans, cancel the whole box.
return True, False, resume_at
# How many lines we need on the next page to satisfy widows
# -1 for the current line.
needed = box.style['widows'] - 1
if needed:
for _ in lines_iterator:
needed -= 1
if needed == 0:
break
if needed > over_orphans and not page_is_empty:
# Total number of lines < orphans + widows
return True, False, resume_at
if needed and needed <= over_orphans:
# Remove lines to keep them for the next page
del new_children[-needed:]
# Page break here, resume before this line
return False, True, {index: skip_stack}
def _linebox_layout(context, box, index, child, new_children, page_is_empty,
absolute_boxes, fixed_boxes, adjoining_margins,
bottom_space, position_y, skip_stack, first_letter_style,
draw_bottom_decoration):
abort = stop = False
resume_at = None
assert len(box.children) == 1, 'line box with siblings before layout'
if adjoining_margins:
position_y += collapse_margin(adjoining_margins)
new_containing_block = box
lines_iterator = iter_line_boxes(
context, child, position_y, bottom_space, skip_stack,
new_containing_block, absolute_boxes, fixed_boxes, first_letter_style)
for i, (line, resume_at) in enumerate(lines_iterator):
line.resume_at = resume_at
new_position_y = line.position_y + line.height
# Add bottom padding and border to the bottom position of the
# box if needed
draw_bottom_decoration |= resume_at is None
if draw_bottom_decoration:
offset_y = box.border_bottom_width + box.padding_bottom
else:
offset_y = 0
# Allow overflow if the first line of the page is higher
# than the page itself so that we put *something* on this
# page and can advance in the context.
overflow = (
(new_children or not page_is_empty) and
(new_position_y + offset_y > context.page_bottom - bottom_space))
if overflow:
abort, stop, resume_at = _break_line(
box, new_children, lines_iterator, page_is_empty, index,
skip_stack, resume_at)
break
# TODO: this is incomplete.
# See http://dev.w3.org/csswg/css3-page/#allowed-pg-brk
# "When an unforced page break occurs here, both the adjoining
# margin-top and margin-bottom are set to zero."
# See https://github.com/Kozea/WeasyPrint/issues/115
elif page_is_empty and (
new_position_y > context.page_bottom - bottom_space):
# Remove the top border when a page is empty and the box is
# too high to be drawn in one page
new_position_y -= box.margin_top
line.translate(0, -box.margin_top)
box.margin_top = 0
if context.footnotes:
break_linebox = False
footnotes = (
descendant.footnote for descendant in line.descendants()
if descendant.footnote in context.footnotes)
for footnote in footnotes:
context.layout_footnote(footnote)
overflow = context.reported_footnotes or (
new_position_y + offset_y >
context.page_bottom - bottom_space)
if overflow:
context.report_footnote(footnote)
if footnote.style['footnote_policy'] == 'line':
abort, stop, resume_at = _break_line(
box, new_children, lines_iterator, page_is_empty,
index, skip_stack, resume_at)
break_linebox = True
elif footnote.style['footnote_policy'] == 'block':
abort = break_linebox = True
break
if break_linebox:
break
new_children.append(line)
position_y = new_position_y
skip_stack = resume_at
# Break box if we reached max-lines
if box.style['max_lines'] != 'none':
if i >= box.style['max_lines'] - 1:
line.block_ellipsis = box.style['block_ellipsis']
break
if new_children:
resume_at = {index: new_children[-1].resume_at}
return abort, stop, resume_at, position_y
def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
absolute_boxes, fixed_boxes, adjoining_margins,
bottom_space, position_y, skip_stack, first_letter_style,
draw_bottom_decoration, collapsing_with_children, discard,
next_page):
abort = stop = False
last_in_flow_child = find_last_in_flow_child(new_children)
if last_in_flow_child is not None:
# Between in-flow siblings
page_break = block_level_page_break(last_in_flow_child, child)
page_name = block_level_page_name(last_in_flow_child, child)
if page_name or page_break in (
'page', 'left', 'right', 'recto', 'verso'):
page_name = child.page_values()[0]
next_page = {'break': page_break, 'page': page_name}
resume_at = {index: None}
stop = True
return (
abort, stop, resume_at, position_y, adjoining_margins,
next_page, new_children)
else:
page_break = 'auto'
new_containing_block = box
if not new_containing_block.is_table_wrapper:
resolve_percentages(child, new_containing_block)
if (child.is_in_normal_flow() and last_in_flow_child is None and
collapsing_with_children):
# TODO: add the adjoining descendants' margin top to
# [child.margin_top]
old_collapsed_margin = collapse_margin(adjoining_margins)
if child.margin_top == 'auto':
child_margin_top = 0
else:
child_margin_top = child.margin_top
new_collapsed_margin = collapse_margin(
adjoining_margins + [child_margin_top])
collapsed_margin_difference = (
new_collapsed_margin - old_collapsed_margin)
for previous_new_child in new_children:
previous_new_child.translate(dy=collapsed_margin_difference)
clearance = get_clearance(context, child, new_collapsed_margin)
if clearance is not None:
for previous_new_child in new_children:
previous_new_child.translate(
dy=-collapsed_margin_difference)
collapsed_margin = collapse_margin(adjoining_margins)
box.position_y += collapsed_margin - box.margin_top
# Count box.margin_top as we emptied adjoining_margins
adjoining_margins = []
position_y = box.content_box_y()
if adjoining_margins and box.is_table_wrapper:
collapsed_margin = collapse_margin(adjoining_margins)
child.position_y += collapsed_margin
position_y += collapsed_margin
adjoining_margins = []
page_is_empty_with_no_children = page_is_empty and not any(
child for child in new_children
if not isinstance(child, AbsolutePlaceholder))
if not getattr(child, 'first_letter_style', None):
child.first_letter_style = first_letter_style
(new_child, resume_at, next_page, next_adjoining_margins,
collapsing_through) = block_level_layout(
context, child, bottom_space, skip_stack,
new_containing_block, page_is_empty_with_no_children, absolute_boxes,
fixed_boxes, adjoining_margins, discard)
if new_child is not None:
# index in its non-laid-out parent, not in future new parent
# May be used in find_earlier_page_break()
new_child.index = index
# We need to do this after the child layout to have the
# used value for margin_top (eg. it might be a percentage.)
if not isinstance(new_child, (boxes.BlockBox, boxes.TableBox)):
adjoining_margins.append(new_child.margin_top)
offset_y = (
collapse_margin(adjoining_margins) - new_child.margin_top)
new_child.translate(0, offset_y)
# else: blocks handle that themselves.
if not collapsing_through:
new_content_position_y = (
new_child.content_box_y() + new_child.height)
new_position_y = (
new_child.border_box_y() + new_child.border_height())
if (new_content_position_y > context.page_bottom - bottom_space and
not page_is_empty_with_no_children):
# The child content overflows the page area, display it on the
# next page.
remove_placeholders([new_child], absolute_boxes, fixed_boxes)
new_child = None
elif (new_position_y > context.page_bottom - bottom_space and
not page_is_empty_with_no_children):
# The child border/padding overflows the page area, do the
# layout again with a higher bottom_space value.
remove_placeholders([new_child], absolute_boxes, fixed_boxes)
bottom_space += (
new_child.padding_bottom + new_child.border_bottom_width)
(new_child, resume_at, next_page, next_adjoining_margins,
collapsing_through) = block_level_layout(
context, child, bottom_space, skip_stack,
new_containing_block, page_is_empty_with_no_children,
absolute_boxes, fixed_boxes, adjoining_margins, discard)
if new_child:
position_y = (
new_child.border_box_y() + new_child.border_height())
else:
position_y = new_position_y
adjoining_margins = next_adjoining_margins
if new_child:
adjoining_margins.append(new_child.margin_bottom)
if new_child and new_child.clearance:
position_y = new_child.border_box_y() + new_child.border_height()
skip_stack = None
if new_child is None:
# Nothing fits in the remaining space of this page: break
if page_break in ('avoid', 'avoid-page'):
# TODO: fill the blank space at the bottom of the page
result = find_earlier_page_break(
new_children, absolute_boxes, fixed_boxes)
if result:
new_children, resume_at = result
stop = True
return (
abort, stop, resume_at, position_y, adjoining_margins,
next_page, new_children)
else:
# We did not find any page break opportunity
if not page_is_empty:
# The page has content *before* this block:
# cancel the block and try to find a break
# in the parent.
abort = True
return (
abort, stop, resume_at, position_y, adjoining_margins,
next_page, new_children)
# else:
# ignore this 'avoid' and break anyway.
if all(child.is_absolutely_positioned() for child in new_children):
# This box has only rendered absolute children, keep them
# for the next page. This is for example useful for list
# markers.
remove_placeholders(new_children, absolute_boxes, fixed_boxes)
new_children = []
if new_children:
resume_at = {index: None}
stop = True
else:
# This was the first child of this box, cancel the box completly
abort = True
return (
abort, stop, resume_at, position_y, adjoining_margins, next_page,
new_children)
new_children.append(new_child)
if resume_at is not None:
resume_at = {index: resume_at}
stop = True
return (
abort, stop, resume_at, position_y, adjoining_margins, next_page,
new_children)
def block_container_layout(context, box, bottom_space, skip_stack,
page_is_empty, absolute_boxes, fixed_boxes,
adjoining_margins, discard):
"""Set the ``box`` height."""
# TODO: boxes.FlexBox is allowed here because flex_layout calls
# block_container_layout, there's probably a better solution.
assert isinstance(box, (boxes.BlockContainerBox, boxes.FlexBox))
if establishes_formatting_context(box):
context.create_block_formatting_context()
is_start = skip_stack is None
box.remove_decoration(start=not is_start, end=False)
discard |= box.style['continue'] == 'discard'
draw_bottom_decoration = (
discard or box.style['box_decoration_break'] == 'clone')
if adjoining_margins is None:
adjoining_margins = []
if draw_bottom_decoration:
bottom_space += (
box.padding_bottom + box.border_bottom_width + box.margin_bottom)
adjoining_margins.append(box.margin_top)
this_box_adjoining_margins = adjoining_margins
collapsing_with_children = not (
box.border_top_width or box.padding_top or box.is_flex_item or
establishes_formatting_context(box) or box.is_for_root_element)
if collapsing_with_children:
# Not counting margins in adjoining_margins, if any
# (there are not padding or borders, see above)
position_y = box.position_y
else:
box.position_y += collapse_margin(adjoining_margins) - box.margin_top
adjoining_margins = []
position_y = box.content_box_y()
position_x = box.content_box_x()
if box.style['position'] == 'relative':
# New containing block, use a new absolute list
absolute_boxes = []
new_children = []
next_page = {'break': 'any', 'page': None}
last_in_flow_child = None
if is_start:
skip = 0
first_letter_style = getattr(box, 'first_letter_style', None)
else:
# TODO: handle multiple skip stacks
(skip, skip_stack), = skip_stack.items()
first_letter_style = None
for index, child in enumerate(box.children[skip:], start=(skip or 0)):
child.position_x = position_x
child.position_y = position_y # doesnt count adjoining_margins
if not child.is_in_normal_flow():
abort = False
stop, resume_at, out_of_flow_resume_at = _out_of_flow_layout(
context, box, index, child, new_children, page_is_empty,
absolute_boxes, fixed_boxes, adjoining_margins,
bottom_space)
if out_of_flow_resume_at:
context.broken_out_of_flow.append(
(child, box, out_of_flow_resume_at))
elif isinstance(child, boxes.LineBox):
abort, stop, resume_at, position_y = _linebox_layout(
context, box, index, child, new_children, page_is_empty,
absolute_boxes, fixed_boxes, adjoining_margins, bottom_space,
position_y, skip_stack, first_letter_style,
draw_bottom_decoration)
draw_bottom_decoration |= resume_at is None
adjoining_margins = []
else:
(abort, stop, resume_at, position_y, adjoining_margins,
next_page, new_children) = _in_flow_layout(
context, box, index, child, new_children, page_is_empty,
absolute_boxes, fixed_boxes, adjoining_margins, bottom_space,
position_y, skip_stack, first_letter_style,
draw_bottom_decoration, collapsing_with_children, discard,
next_page)
skip_stack = None
if abort:
page = child.page_values()[0]
return None, None, {'break': 'any', 'page': page}, [], False
elif stop:
break
else:
resume_at = None
box_is_fragmented = resume_at is not None
if box.style['continue'] == 'discard':
resume_at = None
if (box_is_fragmented and
box.style['break_inside'] in ('avoid', 'avoid-page') and
not page_is_empty):
return (
None, None, {'break': 'any', 'page': None}, [], False)
if collapsing_with_children:
box.position_y += (
collapse_margin(this_box_adjoining_margins) - box.margin_top)
last_in_flow_child = find_last_in_flow_child(new_children)
collapsing_through = False
if last_in_flow_child is None:
collapsed_margin = collapse_margin(adjoining_margins)
# Top and bottom margins of this box
if (box.height in ('auto', 0) and
get_clearance(context, box, collapsed_margin) is None and
all(value == 0 for value in [
box.min_height, box.border_top_width, box.padding_top,
box.border_bottom_width, box.padding_bottom])):
collapsing_through = True
else:
position_y += collapsed_margin
adjoining_margins = []
else:
# Bottom margin of the last child and bottom margin of this box
if box.height != 'auto':
# Not adjoining (position_y is not used afterwards)
adjoining_margins = []
if (box.border_bottom_width or
box.padding_bottom or
establishes_formatting_context(box) or
box.is_for_root_element or
box.is_table_wrapper):
position_y += collapse_margin(adjoining_margins)
adjoining_margins = []
# Add block ellipsis
if box_is_fragmented and new_children:
last_child = new_children[-1]
if isinstance(last_child, boxes.LineBox):
last_child.block_ellipsis = box.style['block_ellipsis']
new_box = box.copy_with_children(new_children)
new_box.remove_decoration(
start=not is_start, end=box_is_fragmented and not discard)
# TODO: See corner cases in
# http://www.w3.org/TR/CSS21/visudet.html#normal-block
# TODO: See float.float_layout
if new_box.height == 'auto':
if context.excluded_shapes and new_box.style['overflow'] != 'visible':
max_float_position_y = max(
float_box.position_y + float_box.margin_height()
for float_box in context.excluded_shapes)
position_y = max(max_float_position_y, position_y)
new_box.height = position_y - new_box.content_box_y()
if new_box.style['position'] == 'relative':
# New containing block, resolve the layout of the absolute descendants
for absolute_box in absolute_boxes:
absolute_layout(
context, absolute_box, new_box, fixed_boxes, bottom_space,
skip_stack=None)
for child in new_box.children:
relative_positioning(child, (new_box.width, new_box.height))
if establishes_formatting_context(new_box):
context.finish_block_formatting_context(new_box)
if discard or not box_is_fragmented:
# After finish_block_formatting_context which may increment
# new_box.height
new_box.height = max(
min(new_box.height, new_box.max_height), new_box.min_height)
elif bottom_space > -float('inf'):
# Make the box fill the blank space at the bottom of the page
# https://www.w3.org/TR/css-break-3/#box-splitting
new_box.height = (
context.page_bottom - bottom_space - new_box.position_y -
(new_box.margin_height() - new_box.height))
if draw_bottom_decoration:
new_box.height += (
box.padding_bottom + box.border_bottom_width +
box.margin_bottom)
if next_page['page'] is None:
next_page['page'] = new_box.page_values()[1]
return new_box, resume_at, next_page, adjoining_margins, collapsing_through
def collapse_margin(adjoining_margins):
"""Get the amount of collapsed margin for a list of adjoining margins."""
margins = [0] # add 0 to make sure that max/min dont get an empty list
margins.extend(adjoining_margins)
positives = (m for m in margins if m >= 0)
negatives = (m for m in margins if m <= 0)
return max(positives) + min(negatives)
def establishes_formatting_context(box):
"""Return whether a box establishes a block formatting context.
See http://www.w3.org/TR/CSS2/visuren.html#block-formatting
"""
return (
box.is_floated()
) or (
box.is_absolutely_positioned()
) or (
# TODO: columns shouldn't be block boxes, this condition would then be
# useless when this is fixed
box.is_column
) or (
isinstance(box, boxes.BlockContainerBox) and
not isinstance(box, boxes.BlockBox)
) or (
isinstance(box, boxes.BlockBox) and box.style['overflow'] != 'visible'
) or (
'flow-root' in box.style['display']
)
def block_level_page_break(sibling_before, sibling_after):
"""Get the correct page break value between siblings.
Return the value of ``page-break-before`` or ``page-break-after`` that
"wins" for boxes that meet at the margin between two sibling boxes.
For boxes before the margin, the 'page-break-after' value is considered;
for boxes after the margin the 'page-break-before' value is considered.
* 'avoid' takes priority over 'auto'
* 'page' takes priority over 'avoid' or 'auto'
* 'left' or 'right' take priority over 'always', 'avoid' or 'auto'
* Among 'left' and 'right', later values in the tree take priority.
See http://dev.w3.org/csswg/css3-page/#allowed-pg-brk
"""
values = []
# https://drafts.csswg.org/css-break-3/#possible-breaks
block_parallel_box_types = (
boxes.BlockLevelBox, boxes.TableRowGroupBox, boxes.TableRowBox)
box = sibling_before
while isinstance(box, block_parallel_box_types):
values.append(box.style['break_after'])
if not (isinstance(box, boxes.ParentBox) and box.children):
break
box = box.children[-1]
values.reverse() # Have them in tree order
box = sibling_after
while isinstance(box, block_parallel_box_types):
values.append(box.style['break_before'])
if not (isinstance(box, boxes.ParentBox) and box.children):
break
box = box.children[0]
result = 'auto'
for value in values:
if value in ('left', 'right', 'recto', 'verso') or (value, result) in (
('page', 'auto'),
('page', 'avoid'),
('avoid', 'auto'),
('page', 'avoid-page'),
('avoid-page', 'auto')):
result = value
return result
def block_level_page_name(sibling_before, sibling_after):
"""Return the next page name when siblings don't have the same names."""
before_page = sibling_before.page_values()[1]
after_page = sibling_after.page_values()[0]
if before_page != after_page:
return after_page
def find_earlier_page_break(children, absolute_boxes, fixed_boxes):
"""Find the last possible page break in ``children``.
Because of a `page-break-before: avoid` or a `page-break-after: avoid` we
need to find an earlier page break opportunity inside `children`.
Absolute or fixed placeholders removed from children should also be
removed from `absolute_boxes` or `fixed_boxes`.
Return (new_children, resume_at).
"""
if children and isinstance(children[0], boxes.LineBox):
# Normally `orphans` and `widows` apply to the block container, but
# line boxes inherit them.
orphans = children[0].style['orphans']
widows = children[0].style['widows']
index = len(children) - widows # how many lines we keep
if index < orphans:
return None
new_children = children[:index]
resume_at = {0: new_children[-1].resume_at}
remove_placeholders(children[index:], absolute_boxes, fixed_boxes)
return new_children, resume_at
previous_in_flow = None
for index, child in reversed_enumerate(children):
if isinstance(child, boxes.TableRowGroupBox) and (
child.is_header or child.is_footer):
# We dont want to break pages before table headers or footers.
continue
elif child.is_column:
# We dont want to break pages between columns.
continue
if child.is_in_normal_flow():
if previous_in_flow is not None and (
block_level_page_break(child, previous_in_flow) not in
('avoid', 'avoid-page')):
index += 1 # break after child
new_children = children[:index]
# Get the index in the original parent
resume_at = {children[index].index: None}
break
previous_in_flow = child
if child.is_in_normal_flow() and (
child.style['break_inside'] not in ('avoid', 'avoid-page')):
breakable_box_types = (
boxes.BlockBox, boxes.TableBox, boxes.TableRowGroupBox)
if isinstance(child, breakable_box_types):
result = find_earlier_page_break(
child.children, absolute_boxes, fixed_boxes)
if result:
new_grand_children, resume_at = result
new_child = child.copy_with_children(new_grand_children)
new_children = list(children[:index]) + [new_child]
# Re-add footer at the end of split table
# TODO: fix table height and footer position
if isinstance(child, boxes.TableRowGroupBox):
for next_child in children[index:]:
if next_child.is_footer:
new_children.append(next_child)
# Index in the original parent
resume_at = {new_child.index: resume_at}
index += 1 # Remove placeholders after child
break
else:
return None
# TODO: dont remove absolute and fixed placeholders found in table footers
remove_placeholders(children[index:], absolute_boxes, fixed_boxes)
return new_children, resume_at
def find_last_in_flow_child(children):
"""Find and return the last in-flow child of given ``children``."""
for child in reversed(children):
if child.is_in_normal_flow():
return child
def reversed_enumerate(seq):
"""Like reversed(list(enumerate(seq))) without copying the whole seq."""
return zip(reversed(range(len(seq))), reversed(seq))
def remove_placeholders(box_list, absolute_boxes, fixed_boxes):
"""Remove placeholders from absolute and fixed lists.
For boxes that have been removed in find_earlier_page_break(), remove the
matching placeholders in absolute_boxes and fixed_boxes.
"""
for box in box_list:
if isinstance(box, boxes.ParentBox):
remove_placeholders(box.children, absolute_boxes, fixed_boxes)
if box.style['position'] == 'absolute' and box in absolute_boxes:
# box is not in absolute_boxes if its parent has position: relative
absolute_boxes.remove(box)
elif box.style['position'] == 'fixed':
fixed_boxes.remove(box)

View File

@@ -0,0 +1,306 @@
"""
weasyprint.layout.columns
-------------------------
Layout for columns.
"""
from math import floor
from .absolute import absolute_layout
from .percent import resolve_percentages
def columns_layout(context, box, bottom_space, skip_stack, containing_block,
page_is_empty, absolute_boxes, fixed_boxes,
adjoining_margins):
"""Lay out a multi-column ``box``."""
from .block import (
block_box_layout, block_level_layout, block_level_width,
collapse_margin)
# Implementation of the multi-column pseudo-algorithm:
# https://www.w3.org/TR/css3-multicol/#pseudo-algorithm
width = None
style = box.style
original_bottom_space = bottom_space
if box.style['position'] == 'relative':
# New containing block, use a new absolute list
absolute_boxes = []
box = box.copy_with_children(box.children)
box.position_y += collapse_margin(adjoining_margins) - box.margin_top
height = box.style['height']
if height != 'auto' and height.unit != '%':
assert height.unit == 'px'
known_height = True
bottom_space = max(
bottom_space,
context.page_bottom - box.content_box_y() - height.value)
else:
known_height = False
# TODO: the available width can be unknown if the containing block needs
# the size of this block to know its own size.
block_level_width(box, containing_block)
available_width = box.width
if style['column_width'] == 'auto' and style['column_count'] != 'auto':
count = style['column_count']
width = max(
0, available_width - (count - 1) * style['column_gap']) / count
elif (style['column_width'] != 'auto' and
style['column_count'] == 'auto'):
count = max(1, int(floor(
(available_width + style['column_gap']) /
(style['column_width'] + style['column_gap']))))
width = (
(available_width + style['column_gap']) / count -
style['column_gap'])
else:
count = min(style['column_count'], int(floor(
(available_width + style['column_gap']) /
(style['column_width'] + style['column_gap']))))
width = (
(available_width + style['column_gap']) / count -
style['column_gap'])
def create_column_box(children):
column_box = box.anonymous_from(box, children=children)
resolve_percentages(column_box, containing_block)
column_box.is_column = True
column_box.width = width
column_box.position_x = box.content_box_x()
column_box.position_y = box.content_box_y()
return column_box
# Handle column-span property.
# We want to get the following structure:
# columns_and_blocks = [
# [column_child_1, column_child_2],
# spanning_block,
# …
# ]
columns_and_blocks = []
column_children = []
for child in box.children:
if child.style['column_span'] == 'all':
if column_children:
columns_and_blocks.append(column_children)
columns_and_blocks.append(child.copy())
column_children = []
continue
column_children.append(child.copy())
if column_children:
columns_and_blocks.append(column_children)
if not box.children:
next_page = {'break': 'any', 'page': None}
skip_stack = None
# Balance.
#
# The current algorithm starts from the ideal height (the total height
# divided by the number of columns). We then iterate until the last column
# is not the highest one. At the end of each loop, we add the minimal
# height needed to make one direct child at the top of one column go to the
# end of the previous column.
#
# We rely on a real rendering for each loop, and with a stupid algorithm
# like this it can last minutes…
adjoining_margins = []
current_position_y = box.content_box_y()
new_children = []
for column_children_or_block in columns_and_blocks:
if not isinstance(column_children_or_block, list):
# We get a spanning block, we display it like other blocks.
block = column_children_or_block
resolve_percentages(block, containing_block)
block.position_x = box.content_box_x()
block.position_y = current_position_y
new_child, _, _, adjoining_margins, _ = block_level_layout(
context, block, original_bottom_space, skip_stack,
containing_block, page_is_empty, absolute_boxes, fixed_boxes,
adjoining_margins, discard=False)
new_children.append(new_child)
current_position_y = (
new_child.border_height() + new_child.border_box_y())
adjoining_margins.append(new_child.margin_bottom)
continue
excluded_shapes = context.excluded_shapes[:]
# We have a list of children that we have to balance between columns.
column_children = column_children_or_block
# Find the total height of the content
current_position_y += collapse_margin(adjoining_margins)
adjoining_margins = []
column_box = create_column_box(column_children)
column_box.position_y = current_position_y
new_child, _, _, _, _ = block_box_layout(
context, column_box, -float('inf'), skip_stack, containing_block,
page_is_empty, [], [], [], discard=False)
height = new_child.margin_height()
if style['column_fill'] == 'balance':
height /= count
# Try to render columns until the content fits, increase the column
# height step by step.
column_skip_stack = skip_stack
lost_space = float('inf')
while True:
# Remove extra excluded shapes introduced during previous loop
new_excluded_shapes = (
len(context.excluded_shapes) - len(excluded_shapes))
for i in range(new_excluded_shapes):
context.excluded_shapes.pop()
for i in range(count):
# Render the column
new_box, resume_at, next_page, _, _ = block_box_layout(
context, column_box,
context.page_bottom - current_position_y - height,
column_skip_stack, containing_block, page_is_empty,
[], [], [], discard=False)
if new_box is None:
# We didn't render anything. Give up and use the max
# content height.
height *= count
continue
column_skip_stack = resume_at
in_flow_children = [
child for child in new_box.children
if child.is_in_normal_flow()]
if in_flow_children:
# Get the empty space at the bottom of the column box
empty_space = height - (
in_flow_children[-1].position_y - box.content_box_y() +
in_flow_children[-1].margin_height())
# Get the minimum size needed to render the next box
next_box, _, _, _, _ = block_box_layout(
context, column_box,
context.page_bottom - box.content_box_y(),
column_skip_stack, containing_block, True, [], [], [],
discard=False)
for child in next_box.children:
if child.is_in_normal_flow():
next_box_size = child.margin_height()
break
else:
empty_space = next_box_size = 0
# Append the size needed to render the next box in this
# column.
#
# The next box size may be smaller than the empty space, for
# example when the next box can't be separated from its own
# next box. In this case we don't try to find the real value
# and let the workaround below fix this for us.
#
# We also want to avoid very small values that may have been
# introduced by rounding errors. As the workaround below at
# least adds 1 pixel for each loop, we can ignore lost spaces
# lower than 1px.
if next_box_size - empty_space > 1:
lost_space = min(lost_space, next_box_size - empty_space)
# Stop if we already rendered the whole content
if resume_at is None:
break
if column_skip_stack is None:
# We rendered the whole content, stop
break
else:
if lost_space == float('inf'):
# We didn't find the extra size needed to render a child in
# the previous column, increase height by the minimal
# value.
height += 1
else:
# Increase the columns heights and render them once again
height += lost_space
column_skip_stack = skip_stack
# TODO: check box.style['max']-height
bottom_space = max(
bottom_space, context.page_bottom - current_position_y - height)
# Replace the current box children with columns
i = 0
max_column_height = 0
columns = []
while True:
if i == count - 1:
bottom_space = original_bottom_space
column_box = create_column_box(column_children)
column_box.position_y = current_position_y
if style['direction'] == 'rtl':
column_box.position_x += (
box.width - (i + 1) * width - i * style['column_gap'])
else:
column_box.position_x += i * (width + style['column_gap'])
new_child, column_skip_stack, column_next_page, _, _ = (
block_box_layout(
context, column_box, bottom_space, skip_stack,
containing_block, page_is_empty, absolute_boxes,
fixed_boxes, None, discard=False))
if new_child is None:
break
next_page = column_next_page
skip_stack = column_skip_stack
columns.append(new_child)
max_column_height = max(
max_column_height, new_child.margin_height())
if skip_stack is None:
break
i += 1
if i == count and not known_height:
# [If] a declaration that constrains the column height
# (e.g., using height or max-height). In this case,
# additional column boxes are created in the inline
# direction.
break
current_position_y += max_column_height
for column in columns:
column.height = max_column_height
new_children.append(column)
if box.children and not new_children:
# The box has children but none can be drawn, let's skip the whole box
return None, (0, None), {'break': 'any', 'page': None}, [], False
# Set the height of box and the columns
box.children = new_children
current_position_y += collapse_margin(adjoining_margins)
height = current_position_y - box.content_box_y()
if box.height == 'auto':
box.height = height
height_difference = 0
else:
height_difference = box.height - height
if box.min_height != 'auto' and box.min_height > box.height:
height_difference += box.min_height - box.height
box.height = box.min_height
for child in new_children[::-1]:
if child.is_column:
child.height += height_difference
else:
break
if box.style['position'] == 'relative':
# New containing block, resolve the layout of the absolute descendants
for absolute_box in absolute_boxes:
absolute_layout(
context, absolute_box, box, fixed_boxes, bottom_space,
skip_stack=None)
return box, skip_stack, next_page, [], False

View File

@@ -0,0 +1,894 @@
"""
weasyprint.layout.flex
------------------------
Layout for flex containers and flex-items.
"""
import sys
from math import log10
from ..css.properties import Dimension
from ..formatting_structure import boxes
from .percent import resolve_one_percentage, resolve_percentages
from .preferred import max_content_width, min_content_width
from .table import find_in_flow_baseline
class FlexLine(list):
pass
def flex_layout(context, box, bottom_space, skip_stack, containing_block,
page_is_empty, absolute_boxes, fixed_boxes):
from . import block, preferred
context.create_block_formatting_context()
resume_at = None
# Step 1 is done in formatting_structure.boxes
# Step 2
if box.style['flex_direction'].startswith('row'):
axis, cross = 'width', 'height'
else:
axis, cross = 'height', 'width'
margin_left = 0 if box.margin_left == 'auto' else box.margin_left
margin_right = 0 if box.margin_right == 'auto' else box.margin_right
margin_top = 0 if box.margin_top == 'auto' else box.margin_top
margin_bottom = 0 if box.margin_bottom == 'auto' else box.margin_bottom
if getattr(box, axis) != 'auto':
available_main_space = getattr(box, axis)
else:
if axis == 'width':
available_main_space = (
containing_block.width -
margin_left - margin_right -
box.padding_left - box.padding_right -
box.border_left_width - box.border_right_width)
else:
main_space = context.page_bottom - bottom_space - box.position_y
if containing_block.height != 'auto':
if isinstance(containing_block.height, Dimension):
assert containing_block.height.unit == 'px'
main_space = min(main_space, containing_block.height.value)
else:
main_space = min(main_space, containing_block.height)
available_main_space = (
main_space -
margin_top - margin_bottom -
box.padding_top - box.padding_bottom -
box.border_top_width - box.border_bottom_width)
if getattr(box, cross) != 'auto':
available_cross_space = getattr(box, cross)
else:
if cross == 'height':
main_space = (
context.page_bottom - bottom_space - box.content_box_y())
if containing_block.height != 'auto':
if isinstance(containing_block.height, Dimension):
assert containing_block.height.unit == 'px'
main_space = min(main_space, containing_block.height.value)
else:
main_space = min(main_space, containing_block.height)
available_cross_space = (
main_space -
margin_top - margin_bottom -
box.padding_top - box.padding_bottom -
box.border_top_width - box.border_bottom_width)
else:
available_cross_space = (
containing_block.width -
margin_left - margin_right -
box.padding_left - box.padding_right -
box.border_left_width - box.border_right_width)
# Step 3
children = box.children
parent_box = box.copy_with_children(children)
resolve_percentages(parent_box, containing_block)
# TODO: removing auto margins is OK for this step, but margins should be
# calculated later.
if parent_box.margin_top == 'auto':
box.margin_top = parent_box.margin_top = 0
if parent_box.margin_bottom == 'auto':
box.margin_bottom = parent_box.margin_bottom = 0
if parent_box.margin_left == 'auto':
box.margin_left = parent_box.margin_left = 0
if parent_box.margin_right == 'auto':
box.margin_right = parent_box.margin_right = 0
if isinstance(parent_box, boxes.FlexBox):
block.block_level_width(parent_box, containing_block)
else:
parent_box.width = preferred.flex_max_content_width(
context, parent_box)
original_skip_stack = skip_stack
if skip_stack is not None:
# TODO: handle multiple skip stacks
(skip, skip_stack), = skip_stack.items()
if box.style['flex_direction'].endswith('-reverse'):
children = children[:skip + 1]
else:
children = children[skip:]
skip_stack = skip_stack
else:
skip_stack = None
child_skip_stack = skip_stack
for child in children:
if not child.is_flex_item:
continue
# See https://www.w3.org/TR/css-flexbox-1/#min-size-auto
if child.style['overflow'] == 'visible':
main_flex_direction = axis
else:
main_flex_direction = None
resolve_percentages(child, containing_block, main_flex_direction)
child.position_x = parent_box.content_box_x()
child.position_y = parent_box.content_box_y()
if child.min_width == 'auto':
specified_size = (
child.width if child.width != 'auto' else float('inf'))
if isinstance(child, boxes.ParentBox):
new_child = child.copy_with_children(child.children)
else:
new_child = child.copy()
new_child.style = child.style.copy()
new_child.style['width'] = 'auto'
new_child.style['min_width'] = Dimension(0, 'px')
new_child.style['max_width'] = Dimension(float('inf'), 'px')
content_size = min_content_width(context, new_child, outer=False)
child.min_width = min(specified_size, content_size)
elif child.min_height == 'auto':
# TODO: find a way to get min-content-height
specified_size = (
child.height if child.height != 'auto' else float('inf'))
if isinstance(child, boxes.ParentBox):
new_child = child.copy_with_children(child.children)
else:
new_child = child.copy()
new_child.style = child.style.copy()
new_child.style['height'] = 'auto'
new_child.style['min_height'] = Dimension(0, 'px')
new_child.style['max_height'] = Dimension(float('inf'), 'px')
new_child = block.block_level_layout(
context, new_child, -float('inf'), child_skip_stack,
parent_box, page_is_empty, [], [], [], False)[0]
content_size = new_child.height
child.min_height = min(specified_size, content_size)
child.style = child.style.copy()
if child.style['flex_basis'] == 'content':
flex_basis = child.flex_basis = 'content'
else:
resolve_one_percentage(child, 'flex_basis', available_main_space)
flex_basis = child.flex_basis
# "If a value would resolve to auto for width, it instead resolves
# to content for flex-basis." Let's do this for height too.
# See https://www.w3.org/TR/css-flexbox-1/#propdef-flex-basis
resolve_one_percentage(child, axis, available_main_space)
if flex_basis == 'auto':
if child.style[axis] == 'auto':
flex_basis = 'content'
else:
if axis == 'width':
flex_basis = child.border_width()
if child.margin_left != 'auto':
flex_basis += child.margin_left
if child.margin_right != 'auto':
flex_basis += child.margin_right
else:
flex_basis = child.border_height()
if child.margin_top != 'auto':
flex_basis += child.margin_top
if child.margin_bottom != 'auto':
flex_basis += child.margin_bottom
# Step 3.A
if flex_basis != 'content':
child.flex_base_size = flex_basis
# TODO: Step 3.B
# TODO: Step 3.C
# Step 3.D is useless, as we never have infinite sizes on paged media
# Step 3.E
else:
child.style[axis] = 'max-content'
# TODO: don't set style value, support *-content values instead
if child.style[axis] == 'max-content':
child.style[axis] = 'auto'
if axis == 'width':
child.flex_base_size = max_content_width(context, child)
else:
if isinstance(child, boxes.ParentBox):
new_child = child.copy_with_children(child.children)
else:
new_child = child.copy()
new_child.width = float('inf')
new_child = block.block_level_layout(
context, new_child, -float('inf'), child_skip_stack,
parent_box, page_is_empty, absolute_boxes, fixed_boxes,
adjoining_margins=[], discard=False)[0]
child.flex_base_size = new_child.margin_height()
elif child.style[axis] == 'min-content':
child.style[axis] = 'auto'
if axis == 'width':
child.flex_base_size = min_content_width(context, child)
else:
if isinstance(child, boxes.ParentBox):
new_child = child.copy_with_children(child.children)
else:
new_child = child.copy()
new_child.width = 0
new_child = block.block_level_layout(
context, new_child, -float('inf'), child_skip_stack,
parent_box, page_is_empty, absolute_boxes, fixed_boxes,
adjoining_margins=[], discard=False)[0]
child.flex_base_size = new_child.margin_height()
else:
assert child.style[axis].unit == 'px'
# TODO: should we add padding, borders and margins?
child.flex_base_size = child.style[axis].value
child.hypothetical_main_size = max(
getattr(child, f'min_{axis}'), min(
child.flex_base_size, getattr(child, f'max_{axis}')))
# Skip stack is only for the first child
child_skip_stack = None
# Step 4
# TODO: the whole step has to be fixed
if axis == 'width':
block.block_level_width(box, containing_block)
else:
if box.style['height'] != 'auto':
box.height = box.style['height'].value
else:
box.height = 0
for i, child in enumerate(children):
if not child.is_flex_item:
continue
child_height = (
child.hypothetical_main_size +
child.border_top_width + child.border_bottom_width +
child.padding_top + child.padding_bottom)
if getattr(box, axis) == 'auto' and (
child_height + box.height > available_main_space):
resume_at = {i: None}
children = children[:i + 1]
break
box.height += child_height
# Step 5
flex_lines = []
line = []
line_size = 0
axis_size = getattr(box, axis)
for i, child in enumerate(
sorted(children, key=lambda item: item.style['order'])):
if not child.is_flex_item:
continue
line_size += child.hypothetical_main_size
if box.style['flex_wrap'] != 'nowrap' and line_size > axis_size:
if line:
flex_lines.append(FlexLine(line))
line = [(i, child)]
line_size = child.hypothetical_main_size
else:
line.append((i, child))
flex_lines.append(FlexLine(line))
line = []
line_size = 0
else:
line.append((i, child))
if line:
flex_lines.append(FlexLine(line))
# TODO: handle *-reverse using the terminology from the specification
if box.style['flex_wrap'] == 'wrap-reverse':
flex_lines.reverse()
if box.style['flex_direction'].endswith('-reverse'):
for line in flex_lines:
line.reverse()
# Step 6
# See https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths
for line in flex_lines:
# Step 6 - 9.7.1
hypothetical_main_size = sum(
child.hypothetical_main_size for i, child in line)
if hypothetical_main_size < available_main_space:
flex_factor_type = 'grow'
else:
flex_factor_type = 'shrink'
# Step 6 - 9.7.2
for i, child in line:
if flex_factor_type == 'grow':
child.flex_factor = child.style['flex_grow']
else:
child.flex_factor = child.style['flex_shrink']
if (child.flex_factor == 0 or
(flex_factor_type == 'grow' and
child.flex_base_size > child.hypothetical_main_size) or
(flex_factor_type == 'shrink' and
child.flex_base_size < child.hypothetical_main_size)):
child.target_main_size = child.hypothetical_main_size
child.frozen = True
else:
child.frozen = False
# Step 6 - 9.7.3
initial_free_space = available_main_space
for i, child in line:
if child.frozen:
initial_free_space -= child.target_main_size
else:
initial_free_space -= child.flex_base_size
# Step 6 - 9.7.4
while not all(child.frozen for i, child in line):
unfrozen_factor_sum = 0
remaining_free_space = available_main_space
# Step 6 - 9.7.4.b
for i, child in line:
if child.frozen:
remaining_free_space -= child.target_main_size
else:
remaining_free_space -= child.flex_base_size
unfrozen_factor_sum += child.flex_factor
if unfrozen_factor_sum < 1:
initial_free_space *= unfrozen_factor_sum
if initial_free_space == float('inf'):
initial_free_space = sys.maxsize
if remaining_free_space == float('inf'):
remaining_free_space = sys.maxsize
initial_magnitude = (
int(log10(initial_free_space)) if initial_free_space > 0
else -float('inf'))
remaining_magnitude = (
int(log10(remaining_free_space)) if remaining_free_space > 0
else -float('inf'))
if initial_magnitude < remaining_magnitude:
remaining_free_space = initial_free_space
# Step 6 - 9.7.4.c
if remaining_free_space == 0:
# "Do nothing", but we at least set the flex_base_size as
# target_main_size for next step.
for i, child in line:
if not child.frozen:
child.target_main_size = child.flex_base_size
else:
scaled_flex_shrink_factors_sum = 0
flex_grow_factors_sum = 0
for i, child in line:
if not child.frozen:
child.scaled_flex_shrink_factor = (
child.flex_base_size * child.style['flex_shrink'])
scaled_flex_shrink_factors_sum += (
child.scaled_flex_shrink_factor)
flex_grow_factors_sum += child.style['flex_grow']
for i, child in line:
if not child.frozen:
if flex_factor_type == 'grow':
ratio = (
child.style['flex_grow'] /
flex_grow_factors_sum)
child.target_main_size = (
child.flex_base_size +
remaining_free_space * ratio)
elif flex_factor_type == 'shrink':
if scaled_flex_shrink_factors_sum == 0:
child.target_main_size = child.flex_base_size
else:
ratio = (
child.scaled_flex_shrink_factor /
scaled_flex_shrink_factors_sum)
child.target_main_size = (
child.flex_base_size +
remaining_free_space * ratio)
# Step 6 - 9.7.4.d
# TODO: First part of this step is useless until 3.E is correct
for i, child in line:
child.adjustment = 0
if not child.frozen and child.target_main_size < 0:
child.adjustment = -child.target_main_size
child.target_main_size = 0
# Step 6 - 9.7.4.e
adjustments = sum(child.adjustment for i, child in line)
for i, child in line:
if adjustments == 0:
child.frozen = True
elif adjustments > 0 and child.adjustment > 0:
child.frozen = True
elif adjustments < 0 and child.adjustment < 0:
child.frozen = True
# Step 6 - 9.7.5
for i, child in line:
if axis == 'width':
child.width = (
child.target_main_size -
child.padding_left - child.padding_right -
child.border_left_width - child.border_right_width)
if child.margin_left != 'auto':
child.width -= child.margin_left
if child.margin_right != 'auto':
child.width -= child.margin_right
else:
child.height = (
child.target_main_size -
child.padding_top - child.padding_bottom -
child.border_top_width - child.border_top_width)
if child.margin_left != 'auto':
child.height -= child.margin_left
if child.margin_right != 'auto':
child.height -= child.margin_right
# Step 7
# TODO: Fix TODO in build.flex_children
# TODO: Handle breaks
new_flex_lines = []
child_skip_stack = skip_stack
for line in flex_lines:
new_flex_line = FlexLine()
for i, child in line:
# TODO: Find another way than calling block_level_layout_switch to
# get baseline and child.height
if child.margin_top == 'auto':
child.margin_top = 0
if child.margin_bottom == 'auto':
child.margin_bottom = 0
if isinstance(child, boxes.ParentBox):
child_copy = child.copy_with_children(child.children)
else:
child_copy = child.copy()
block.block_level_width(child_copy, parent_box)
new_child, _, _, adjoining_margins, _ = (
block.block_level_layout_switch(
context, child_copy, -float('inf'), child_skip_stack,
parent_box, page_is_empty, absolute_boxes, fixed_boxes,
adjoining_margins=[], discard=False))
child._baseline = find_in_flow_baseline(new_child) or 0
if cross == 'height':
child.height = new_child.height
# As flex items margins never collapse (with other flex items
# or with the flex container), we can add the adjoining margins
# to the child bottom margin.
child.margin_bottom += block.collapse_margin(adjoining_margins)
else:
child.width = min_content_width(context, child, outer=False)
new_flex_line.append((i, child))
# Skip stack is only for the first child
child_skip_stack = None
if new_flex_line:
new_flex_lines.append(new_flex_line)
flex_lines = new_flex_lines
# Step 8
cross_size = getattr(box, cross)
if len(flex_lines) == 1 and cross_size != 'auto':
flex_lines[0].cross_size = cross_size
else:
for line in flex_lines:
collected_items = []
not_collected_items = []
for i, child in line:
align_self = child.style['align_self']
if (box.style['flex_direction'].startswith('row') and
align_self == 'baseline' and
child.margin_top != 'auto' and
child.margin_bottom != 'auto'):
collected_items.append(child)
else:
not_collected_items.append(child)
cross_start_distance = 0
cross_end_distance = 0
for child in collected_items:
baseline = child._baseline - child.position_y
cross_start_distance = max(cross_start_distance, baseline)
cross_end_distance = max(
cross_end_distance, child.margin_height() - baseline)
collected_cross_size = cross_start_distance + cross_end_distance
non_collected_cross_size = 0
if not_collected_items:
non_collected_cross_size = float('-inf')
for child in not_collected_items:
if cross == 'height':
child_cross_size = child.border_height()
if child.margin_top != 'auto':
child_cross_size += child.margin_top
if child.margin_bottom != 'auto':
child_cross_size += child.margin_bottom
else:
child_cross_size = child.border_width()
if child.margin_left != 'auto':
child_cross_size += child.margin_left
if child.margin_right != 'auto':
child_cross_size += child.margin_right
non_collected_cross_size = max(
child_cross_size, non_collected_cross_size)
line.cross_size = max(
collected_cross_size, non_collected_cross_size)
if len(flex_lines) == 1:
line, = flex_lines
min_cross_size = getattr(box, f'min_{cross}')
if min_cross_size == 'auto':
min_cross_size = float('-inf')
max_cross_size = getattr(box, f'max_{cross}')
if max_cross_size == 'auto':
max_cross_size = float('inf')
line.cross_size = max(
min_cross_size, min(line.cross_size, max_cross_size))
# Step 9
if box.style['align_content'] == 'stretch':
definite_cross_size = None
if cross == 'height' and box.style['height'] != 'auto':
definite_cross_size = box.style['height'].value
elif cross == 'width':
if isinstance(box, boxes.FlexBox):
if box.style['width'] == 'auto':
definite_cross_size = available_cross_space
else:
definite_cross_size = box.style['width'].value
if definite_cross_size is not None:
extra_cross_size = definite_cross_size - sum(
line.cross_size for line in flex_lines)
if extra_cross_size:
for line in flex_lines:
line.cross_size += extra_cross_size / len(flex_lines)
# TODO: Step 10
# Step 11
for line in flex_lines:
for i, child in line:
align_self = child.style['align_self']
if align_self == 'auto':
align_self = box.style['align_items']
if align_self == 'stretch' and child.style[cross] == 'auto':
cross_margins = (
(child.margin_top, child.margin_bottom)
if cross == 'height'
else (child.margin_left, child.margin_right))
if child.style[cross] == 'auto':
if 'auto' not in cross_margins:
cross_size = line.cross_size
if cross == 'height':
cross_size -= (
child.margin_top + child.margin_bottom +
child.padding_top + child.padding_bottom +
child.border_top_width +
child.border_bottom_width)
else:
cross_size -= (
child.margin_left + child.margin_right +
child.padding_left + child.padding_right +
child.border_left_width +
child.border_right_width)
setattr(child, cross, cross_size)
# TODO: redo layout?
# else: Cross size has been set by step 7
# Step 12
original_position_axis = (
box.content_box_x() if axis == 'width'
else box.content_box_y())
justify_content = box.style['justify_content']
if box.style['flex_direction'].endswith('-reverse'):
if justify_content == 'flex-start':
justify_content = 'flex-end'
elif justify_content == 'flex-end':
justify_content = 'flex-start'
for line in flex_lines:
position_axis = original_position_axis
if axis == 'width':
free_space = box.width
for i, child in line:
free_space -= child.border_width()
if child.margin_left != 'auto':
free_space -= child.margin_left
if child.margin_right != 'auto':
free_space -= child.margin_right
else:
free_space = box.height
for i, child in line:
free_space -= child.border_height()
if child.margin_top != 'auto':
free_space -= child.margin_top
if child.margin_bottom != 'auto':
free_space -= child.margin_bottom
margins = 0
for i, child in line:
if axis == 'width':
if child.margin_left == 'auto':
margins += 1
if child.margin_right == 'auto':
margins += 1
else:
if child.margin_top == 'auto':
margins += 1
if child.margin_bottom == 'auto':
margins += 1
if margins:
free_space /= margins
for i, child in line:
if axis == 'width':
if child.margin_left == 'auto':
child.margin_left = free_space
if child.margin_right == 'auto':
child.margin_right = free_space
else:
if child.margin_top == 'auto':
child.margin_top = free_space
if child.margin_bottom == 'auto':
child.margin_bottom = free_space
free_space = 0
if box.style['direction'] == 'rtl' and axis == 'width':
free_space *= -1
if justify_content == 'flex-end':
position_axis += free_space
elif justify_content == 'center':
position_axis += free_space / 2
elif justify_content == 'space-around':
position_axis += free_space / len(line) / 2
elif justify_content == 'space-evenly':
position_axis += free_space / (len(line) + 1)
for i, child in line:
if axis == 'width':
child.position_x = position_axis
if justify_content == 'stretch':
child.width += free_space / len(line)
else:
child.position_y = position_axis
margin_axis = (
child.margin_width() if axis == 'width'
else child.margin_height())
if box.style['direction'] == 'rtl' and axis == 'width':
margin_axis *= -1
position_axis += margin_axis
if justify_content == 'space-around':
position_axis += free_space / len(line)
elif justify_content == 'space-between':
if len(line) > 1:
position_axis += free_space / (len(line) - 1)
elif justify_content == 'space-evenly':
position_axis += free_space / (len(line) + 1)
# Step 13
position_cross = (
box.content_box_y() if cross == 'height'
else box.content_box_x())
for line in flex_lines:
line.lower_baseline = 0
# TODO: don't duplicate this loop
for i, child in line:
align_self = child.style['align_self']
if align_self == 'auto':
align_self = box.style['align_items']
if align_self == 'baseline' and axis == 'width':
# TODO: handle vertical text
child.baseline = child._baseline - position_cross
line.lower_baseline = max(line.lower_baseline, child.baseline)
for i, child in line:
cross_margins = (
(child.margin_top, child.margin_bottom) if cross == 'height'
else (child.margin_left, child.margin_right))
auto_margins = sum([margin == 'auto' for margin in cross_margins])
if auto_margins:
extra_cross = line.cross_size
if cross == 'height':
extra_cross -= child.border_height()
if child.margin_top != 'auto':
extra_cross -= child.margin_top
if child.margin_bottom != 'auto':
extra_cross -= child.margin_bottom
else:
extra_cross -= child.border_width()
if child.margin_left != 'auto':
extra_cross -= child.margin_left
if child.margin_right != 'auto':
extra_cross -= child.margin_right
if extra_cross > 0:
extra_cross /= auto_margins
if cross == 'height':
if child.margin_top == 'auto':
child.margin_top = extra_cross
if child.margin_bottom == 'auto':
child.margin_bottom = extra_cross
else:
if child.margin_left == 'auto':
child.margin_left = extra_cross
if child.margin_right == 'auto':
child.margin_right = extra_cross
else:
if cross == 'height':
if child.margin_top == 'auto':
child.margin_top = 0
child.margin_bottom = extra_cross
else:
if child.margin_left == 'auto':
child.margin_left = 0
child.margin_right = extra_cross
else:
# Step 14
align_self = child.style['align_self']
if align_self == 'auto':
align_self = box.style['align_items']
position = 'position_y' if cross == 'height' else 'position_x'
setattr(child, position, position_cross)
if align_self == 'flex-end':
if cross == 'height':
child.position_y += (
line.cross_size - child.margin_height())
else:
child.position_x += (
line.cross_size - child.margin_width())
elif align_self == 'center':
if cross == 'height':
child.position_y += (
line.cross_size - child.margin_height()) / 2
else:
child.position_x += (
line.cross_size - child.margin_width()) / 2
elif align_self == 'baseline':
if cross == 'height':
child.position_y += (
line.lower_baseline - child.baseline)
else:
# Handle vertical text
pass
elif align_self == 'stretch':
if child.style[cross] == 'auto':
if cross == 'height':
margins = child.margin_top + child.margin_bottom
else:
margins = child.margin_left + child.margin_right
if child.style['box_sizing'] == 'content-box':
if cross == 'height':
margins += (
child.border_top_width +
child.border_bottom_width +
child.padding_top + child.padding_bottom)
else:
margins += (
child.border_left_width +
child.border_right_width +
child.padding_left + child.padding_right)
# TODO: don't set style width, find a way to avoid
# width re-calculation after Step 16
child.style[cross] = Dimension(
line.cross_size - margins, 'px')
position_cross += line.cross_size
# Step 15
if getattr(box, cross) == 'auto':
# TODO: handle min-max
setattr(box, cross, sum(line.cross_size for line in flex_lines))
# Step 16
elif len(flex_lines) > 1:
extra_cross_size = getattr(box, cross) - sum(
line.cross_size for line in flex_lines)
direction = 'position_y' if cross == 'height' else 'position_x'
if extra_cross_size > 0:
cross_translate = 0
for line in flex_lines:
for i, child in line:
if child.is_flex_item:
current_value = getattr(child, direction)
current_value += cross_translate
setattr(child, direction, current_value)
if box.style['align_content'] == 'flex-end':
setattr(
child, direction,
current_value + extra_cross_size)
elif box.style['align_content'] == 'center':
setattr(
child, direction,
current_value + extra_cross_size / 2)
elif box.style['align_content'] == 'space-around':
setattr(
child, direction,
current_value + extra_cross_size /
len(flex_lines) / 2)
elif box.style['align_content'] == 'space-evenly':
setattr(
child, direction,
current_value + extra_cross_size /
(len(flex_lines) + 1))
if box.style['align_content'] == 'space-between':
cross_translate += extra_cross_size / (len(flex_lines) - 1)
elif box.style['align_content'] == 'space-around':
cross_translate += extra_cross_size / len(flex_lines)
elif box.style['align_content'] == 'space-evenly':
cross_translate += extra_cross_size / (len(flex_lines) + 1)
# TODO: don't use block_box_layout, see TODOs in Step 14 and
# build.flex_children.
box = box.copy()
box.children = []
child_skip_stack = skip_stack
for line in flex_lines:
for i, child in line:
if child.is_flex_item:
new_child, child_resume_at = block.block_level_layout_switch(
context, child, bottom_space, child_skip_stack, box,
page_is_empty, absolute_boxes, fixed_boxes,
adjoining_margins=[], discard=False)[:2]
if new_child is None:
if resume_at:
index, = resume_at
if index:
resume_at = {index + i - 1: None}
else:
box.children.append(new_child)
if child_resume_at is not None:
if original_skip_stack:
# TODO: handle multiple skip stacks
first_level_skip, = original_skip_stack
else:
first_level_skip = 0
if resume_at:
index, = resume_at
first_level_skip += index
resume_at = {first_level_skip + i: child_resume_at}
if resume_at:
break
# Skip stack is only for the first child
child_skip_stack = None
if resume_at:
break
# Set box height
# TODO: this is probably useless because of step #15
if axis == 'width' and box.height == 'auto':
if flex_lines:
box.height = sum(line.cross_size for line in flex_lines)
else:
box.height = 0
# Set baseline
# See https://www.w3.org/TR/css-flexbox-1/#flex-baselines
# TODO: use the real algorithm
if isinstance(box, boxes.InlineFlexBox):
if axis == 'width': # and main text direction is horizontal
box.baseline = flex_lines[0].lower_baseline if flex_lines else 0
else:
box.baseline = ((
find_in_flow_baseline(box.children[0])
if box.children else 0) or 0)
context.finish_block_formatting_context(box)
# TODO: check these returned values
return box, resume_at, {'break': 'any', 'page': None}, [], False

View File

@@ -0,0 +1,229 @@
"""
weasyprint.float
----------------
Layout for floating boxes.
"""
from ..formatting_structure import boxes
from .min_max import handle_min_max_width
from .percent import resolve_percentages, resolve_position_percentages
from .preferred import shrink_to_fit
from .replaced import inline_replaced_box_width_height
from .table import table_wrapper_width
@handle_min_max_width
def float_width(box, context, containing_block):
# Check that box.width is auto even if the caller does it too, because
# the handle_min_max_width decorator can change the value
if box.width == 'auto':
box.width = shrink_to_fit(context, box, containing_block.width)
def float_layout(context, box, containing_block, absolute_boxes, fixed_boxes,
bottom_space, skip_stack):
"""Set the width and position of floating ``box``."""
from .block import block_container_layout
from .flex import flex_layout
cb_width, cb_height = (containing_block.width, containing_block.height)
resolve_percentages(box, (cb_width, cb_height))
# TODO: This is only handled later in blocks.block_container_layout
# http://www.w3.org/TR/CSS21/visudet.html#normal-block
if cb_height == 'auto':
cb_height = (
containing_block.position_y - containing_block.content_box_y())
resolve_position_percentages(box, (cb_width, cb_height))
if box.margin_left == 'auto':
box.margin_left = 0
if box.margin_right == 'auto':
box.margin_right = 0
if box.margin_top == 'auto':
box.margin_top = 0
if box.margin_bottom == 'auto':
box.margin_bottom = 0
clearance = get_clearance(context, box)
if clearance is not None:
box.position_y += clearance
if isinstance(box, boxes.BlockReplacedBox):
inline_replaced_box_width_height(box, containing_block)
elif box.width == 'auto':
float_width(box, context, containing_block)
if box.is_table_wrapper:
table_wrapper_width(context, box, (cb_width, cb_height))
if isinstance(box, boxes.BlockContainerBox):
context.create_block_formatting_context()
box, resume_at, _, _, _ = block_container_layout(
context, box, bottom_space=bottom_space,
skip_stack=skip_stack, page_is_empty=True,
absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes,
adjoining_margins=None, discard=False)
context.finish_block_formatting_context(box)
elif isinstance(box, boxes.FlexContainerBox):
box, resume_at, _, _, _ = flex_layout(
context, box, bottom_space=bottom_space,
skip_stack=skip_stack, containing_block=containing_block,
page_is_empty=True, absolute_boxes=absolute_boxes,
fixed_boxes=fixed_boxes)
else:
assert isinstance(box, boxes.BlockReplacedBox)
resume_at = None
box = find_float_position(context, box, containing_block)
context.excluded_shapes.append(box)
return box, resume_at
def find_float_position(context, box, containing_block):
"""Get the right position of the float ``box``."""
# See http://www.w3.org/TR/CSS2/visuren.html#float-position
# Point 4 is already handled as box.position_y is set according to the
# containing box top position, with collapsing margins handled
# Points 5 and 6, box.position_y is set to the highest position_y possible
if context.excluded_shapes:
highest_y = context.excluded_shapes[-1].position_y
if box.position_y < highest_y:
box.translate(0, highest_y - box.position_y)
# Points 1 and 2
position_x, position_y, available_width = avoid_collisions(
context, box, containing_block)
# Point 9
# position_y is set now, let's define position_x
# for float: left elements, it's already done!
if box.style['float'] == 'right':
position_x += available_width - box.margin_width()
box.translate(position_x - box.position_x, position_y - box.position_y)
return box
def get_clearance(context, box, collapsed_margin=0):
"""Return None if there is no clearance, otherwise the clearance value."""
clearance = None
hypothetical_position = box.position_y + collapsed_margin
# Hypothetical position is the position of the top border edge
for excluded_shape in context.excluded_shapes:
if box.style['clear'] in (excluded_shape.style['float'], 'both'):
y, h = excluded_shape.position_y, excluded_shape.margin_height()
if hypothetical_position < y + h:
clearance = max(
(clearance or 0), y + h - hypothetical_position)
return clearance
def avoid_collisions(context, box, containing_block, outer=True):
excluded_shapes = context.excluded_shapes
position_y = box.position_y if outer else box.border_box_y()
box_width = box.margin_width() if outer else box.border_width()
box_height = box.margin_height() if outer else box.border_height()
if box.border_height() == 0 and box.is_floated():
return 0, 0, containing_block.width
while True:
colliding_shapes = []
for shape in excluded_shapes:
# Assign locals to avoid slow attribute lookups.
shape_position_y = shape.position_y
shape_margin_height = shape.margin_height()
if ((shape_position_y < position_y <
shape_position_y + shape_margin_height) or
(shape_position_y < position_y + box_height <
shape_position_y + shape_margin_height) or
(shape_position_y >= position_y and
shape_position_y + shape_margin_height <=
position_y + box_height)):
colliding_shapes.append(shape)
left_bounds = [
shape.position_x + shape.margin_width()
for shape in colliding_shapes
if shape.style['float'] == 'left']
right_bounds = [
shape.position_x
for shape in colliding_shapes
if shape.style['float'] == 'right']
# Set the default maximum bounds
max_left_bound = containing_block.content_box_x()
max_right_bound = \
containing_block.content_box_x() + containing_block.width
if not outer:
max_left_bound += box.margin_left
max_right_bound -= box.margin_right
# Set the real maximum bounds according to sibling float elements
if left_bounds or right_bounds:
if left_bounds:
max_left_bound = max(max(left_bounds), max_left_bound)
if right_bounds:
max_right_bound = min(min(right_bounds), max_right_bound)
# Points 3, 7 and 8
if box_width > max_right_bound - max_left_bound:
# The box does not fit here
new_positon_y = min(
shape.position_y + shape.margin_height()
for shape in colliding_shapes)
if new_positon_y > position_y:
# We can find a solution with a higher position_y
position_y = new_positon_y
continue
# No solution, we must put the box here
break
# See https://www.w3.org/TR/CSS21/visuren.html#floats
# Boxes that cant collide with floats are:
# - floats
# - line boxes
# - table wrappers
# - block-level replaced box
# - element establishing new formatting contexts (not handled)
assert (
(box.style['float'] in ('right', 'left')) or
isinstance(box, boxes.LineBox) or
box.is_table_wrapper or
isinstance(box, boxes.BlockReplacedBox))
# The x-position of the box depends on its type.
position_x = max_left_bound
if box.style['float'] == 'none':
if containing_block.style['direction'] == 'rtl':
if isinstance(box, boxes.LineBox):
# The position of the line is the position of the cursor, at
# the right bound.
position_x = max_right_bound
elif box.is_table_wrapper:
# The position of the right border of the table is at the right
# bound.
position_x = max_right_bound - box_width
else:
# The position of the right border of the replaced box is at
# the right bound.
assert isinstance(box, boxes.BlockReplacedBox)
position_x = max_right_bound - box_width
available_width = max_right_bound - max_left_bound
if not outer:
position_x -= box.margin_left
position_y -= box.margin_top
return position_x, position_y, available_width

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
"""
weasyprint.layout.leader
------------------------
Leader management.
"""
from ..formatting_structure import boxes
def leader_index(box):
"""Get the index of the first leader box in ``box``."""
for i, child in enumerate(box.children):
if child.is_leader:
return (i, None), child
if isinstance(child, boxes.ParentBox):
child_leader_index, child_leader = leader_index(child)
if child_leader_index is not None:
return (i, child_leader_index), child_leader
return None, None
def handle_leader(context, line, containing_block):
"""Find a leader box in ``line`` and handle its text and its position."""
index, leader_box = leader_index(line)
extra_width = 0
if index is not None and leader_box.children:
text_box, = leader_box.children
# Abort if the leader text has no width
if text_box.width <= 0:
return
# Extra width is the additional width taken by the leader box
extra_width = containing_block.width - sum(
child.width for child in line.children
if child.is_in_normal_flow())
# Take care of excluded shapes
for shape in context.excluded_shapes:
if shape.position_y + shape.height > line.position_y:
extra_width -= shape.width
# Available width is the width available for the leader box
available_width = extra_width + text_box.width
line.width = containing_block.width
# Add text boxes into the leader box
number_of_leaders = int(line.width // text_box.width)
position_x = line.position_x + line.width
children = []
for i in range(number_of_leaders):
position_x -= text_box.width
if position_x < leader_box.position_x:
# Dont add leaders behind the text on the left
continue
elif (position_x + text_box.width >
leader_box.position_x + available_width):
# Dont add leaders behind the text on the right
continue
text_box = text_box.copy()
text_box.position_x = position_x
children.append(text_box)
leader_box.children = tuple(children)
if line.style['direction'] == 'rtl':
leader_box.translate(dx=-extra_width)
# Widen leader parent boxes and translate following boxes
box = line
while index is not None:
for child in box.children[index[0] + 1:]:
if child.is_in_normal_flow():
if line.style['direction'] == 'ltr':
child.translate(dx=extra_width)
else:
child.translate(dx=-extra_width)
box = box.children[index[0]]
box.width += extra_width
index = index[1]

View File

@@ -0,0 +1,45 @@
"""
weasyprint.layout.min_max
-------------------------
"""
import functools
def handle_min_max_width(function):
"""Decorate a function setting used width, handling {min,max}-width."""
@functools.wraps(function)
def wrapper(box, *args):
computed_margins = box.margin_left, box.margin_right
result = function(box, *args)
if box.width > box.max_width:
box.width = box.max_width
box.margin_left, box.margin_right = computed_margins
result = function(box, *args)
if box.width < box.min_width:
box.width = box.min_width
box.margin_left, box.margin_right = computed_margins
result = function(box, *args)
return result
wrapper.without_min_max = function
return wrapper
def handle_min_max_height(function):
"""Decorate a function setting used height, handling {min,max}-height."""
@functools.wraps(function)
def wrapper(box, *args):
computed_margins = box.margin_top, box.margin_bottom
result = function(box, *args)
if box.height > box.max_height:
box.height = box.max_height
box.margin_top, box.margin_bottom = computed_margins
result = function(box, *args)
if box.height < box.min_height:
box.height = box.min_height
box.margin_top, box.margin_bottom = computed_margins
result = function(box, *args)
return result
wrapper.without_min_max = function
return wrapper

View File

@@ -0,0 +1,867 @@
"""
weasyprint.layout.pages
-----------------------
Layout for pages and CSS3 margin boxes.
"""
import copy
from ..css import PageType, computed_from_cascaded
from ..formatting_structure import boxes, build
from ..logger import PROGRESS_LOGGER
from .absolute import absolute_box_layout, absolute_layout
from .block import block_container_layout, block_level_layout
from .float import float_layout
from .min_max import handle_min_max_height, handle_min_max_width
from .percent import resolve_percentages
from .preferred import max_content_width, min_content_width
class OrientedBox:
@property
def sugar(self):
return self.padding_plus_border + self.margin_a + self.margin_b
@property
def outer(self):
return self.sugar + self.inner
@property
def outer_min_content_size(self):
return self.sugar + (
self.min_content_size if self.inner == 'auto' else self.inner)
@property
def outer_max_content_size(self):
return self.sugar + (
self.max_content_size if self.inner == 'auto' else self.inner)
def shrink_to_fit(self, available):
self.inner = min(
max(self.min_content_size, available), self.max_content_size)
class VerticalBox(OrientedBox):
def __init__(self, context, box):
self.context = context
self.box = box
# Inner dimension: that of the content area, as opposed to the
# outer dimension: that of the margin area.
self.inner = box.height
self.margin_a = box.margin_top
self.margin_b = box.margin_bottom
self.padding_plus_border = (
box.padding_top + box.padding_bottom +
box.border_top_width + box.border_bottom_width)
def restore_box_attributes(self):
box = self.box
box.height = self.inner
box.margin_top = self.margin_a
box.margin_bottom = self.margin_b
# TODO: Define what are the min-content and max-content heights
@property
def min_content_size(self):
return 0
@property
def max_content_size(self):
return 1e6
class HorizontalBox(OrientedBox):
def __init__(self, context, box):
self.context = context
self.box = box
self.inner = box.width
self.margin_a = box.margin_left
self.margin_b = box.margin_right
self.padding_plus_border = (
box.padding_left + box.padding_right +
box.border_left_width + box.border_right_width)
self._min_content_size = None
self._max_content_size = None
def restore_box_attributes(self):
box = self.box
box.width = self.inner
box.margin_left = self.margin_a
box.margin_right = self.margin_b
@property
def min_content_size(self):
if self._min_content_size is None:
self._min_content_size = min_content_width(
self.context, self.box, outer=False)
return self._min_content_size
@property
def max_content_size(self):
if self._max_content_size is None:
self._max_content_size = max_content_width(
self.context, self.box, outer=False)
return self._max_content_size
def compute_fixed_dimension(context, box, outer, vertical, top_or_left):
"""
Compute and set a margin box fixed dimension on ``box``, as described in:
http://dev.w3.org/csswg/css3-page/#margin-constraints
:param box:
The margin box to work on
:param outer:
The target outer dimension (value of a page margin)
:param vertical:
True to set height, margin-top and margin-bottom; False for width,
margin-left and margin-right
:param top_or_left:
True if the margin box in if the top half (for vertical==True) or
left half (for vertical==False) of the page.
This determines which margin should be 'auto' if the values are
over-constrained. (Rule 3 of the algorithm.)
"""
box = (VerticalBox if vertical else HorizontalBox)(context, box)
# Rule 2
total = box.padding_plus_border + sum(
value for value in [box.margin_a, box.margin_b, box.inner]
if value != 'auto')
if total > outer:
if box.margin_a == 'auto':
box.margin_a = 0
if box.margin_b == 'auto':
box.margin_b = 0
if box.inner == 'auto':
# XXX this is not in the spec, but without it box.inner
# would end up with a negative value.
# Instead, this will trigger rule 3 below.
# http://lists.w3.org/Archives/Public/www-style/2012Jul/0006.html
box.inner = 0
# Rule 3
if 'auto' not in [box.margin_a, box.margin_b, box.inner]:
# Over-constrained
if top_or_left:
box.margin_a = 'auto'
else:
box.margin_b = 'auto'
# Rule 4
if [box.margin_a, box.margin_b, box.inner].count('auto') == 1:
if box.inner == 'auto':
box.inner = (outer - box.padding_plus_border -
box.margin_a - box.margin_b)
elif box.margin_a == 'auto':
box.margin_a = (outer - box.padding_plus_border -
box.margin_b - box.inner)
elif box.margin_b == 'auto':
box.margin_b = (outer - box.padding_plus_border -
box.margin_a - box.inner)
# Rule 5
if box.inner == 'auto':
if box.margin_a == 'auto':
box.margin_a = 0
if box.margin_b == 'auto':
box.margin_b = 0
box.inner = (outer - box.padding_plus_border -
box.margin_a - box.margin_b)
# Rule 6
if box.margin_a == 'auto' and box.margin_b == 'auto':
box.margin_a = box.margin_b = (
outer - box.padding_plus_border - box.inner) / 2
assert 'auto' not in [box.margin_a, box.margin_b, box.inner]
box.restore_box_attributes()
def compute_variable_dimension(context, side_boxes, vertical, outer_sum):
"""
Compute and set a margin box fixed dimension on ``box``, as described in:
http://dev.w3.org/csswg/css3-page/#margin-dimension
:param side_boxes: Three boxes on a same side (as opposed to a corner.)
A list of:
- A @*-left or @*-top margin box
- A @*-center or @*-middle margin box
- A @*-right or @*-bottom margin box
:param vertical:
True to set height, margin-top and margin-bottom; False for width,
margin-left and margin-right
:param outer_sum:
The target total outer dimension (max box width or height)
"""
box_class = VerticalBox if vertical else HorizontalBox
side_boxes = [box_class(context, box) for box in side_boxes]
box_a, box_b, box_c = side_boxes
for box in side_boxes:
if box.margin_a == 'auto':
box.margin_a = 0
if box.margin_b == 'auto':
box.margin_b = 0
if box_b.box.is_generated:
if box_b.inner == 'auto':
ac_max_content_size = 2 * max(
box_a.outer_max_content_size, box_c.outer_max_content_size)
if outer_sum >= (
box_b.outer_max_content_size + ac_max_content_size):
box_b.inner = box_b.max_content_size
else:
ac_min_content_size = 2 * max(
box_a.outer_min_content_size,
box_c.outer_min_content_size)
box_b.inner = box_b.min_content_size
available = outer_sum - box_b.outer - ac_min_content_size
if available > 0:
weight_ac = ac_max_content_size - ac_min_content_size
weight_b = (
box_b.max_content_size - box_b.min_content_size)
weight_sum = weight_ac + weight_b
# By definition of max_content_size and min_content_size,
# weights can not be negative. weight_sum == 0 implies that
# max_content_size == min_content_size for each box, in
# which case the sum can not be both <= and > outer_sum
# Therefore, one of the last two 'if' statements would not
# have lead us here.
assert weight_sum > 0
box_b.inner += available * weight_b / weight_sum
if box_a.inner == 'auto':
box_a.shrink_to_fit((outer_sum - box_b.outer) / 2 - box_a.sugar)
if box_c.inner == 'auto':
box_c.shrink_to_fit((outer_sum - box_b.outer) / 2 - box_c.sugar)
else:
# Non-generated boxes get zero for every box-model property
assert box_b.inner == 0
if box_a.inner == box_c.inner == 'auto':
if outer_sum >= (
box_a.outer_max_content_size +
box_c.outer_max_content_size):
box_a.inner = box_a.max_content_size
box_c.inner = box_c.max_content_size
else:
box_a.inner = box_a.min_content_size
box_c.inner = box_c.min_content_size
available = outer_sum - box_a.outer - box_c.outer
if available > 0:
weight_a = (
box_a.max_content_size - box_a.min_content_size)
weight_c = (
box_c.max_content_size - box_c.min_content_size)
weight_sum = weight_a + weight_c
# By definition of max_content_size and min_content_size,
# weights can not be negative. weight_sum == 0 implies that
# max_content_size == min_content_size for each box, in
# which case the sum can not be both <= and > outer_sum
# Therefore, one of the last two 'if' statements would not
# have lead us here.
assert weight_sum > 0
box_a.inner += available * weight_a / weight_sum
box_c.inner += available * weight_c / weight_sum
elif box_a.inner == 'auto':
box_a.shrink_to_fit(outer_sum - box_c.outer - box_a.sugar)
elif box_c.inner == 'auto':
box_c.shrink_to_fit(outer_sum - box_a.outer - box_c.sugar)
# And, were done!
assert 'auto' not in [box.inner for box in side_boxes]
# Set the actual attributes back.
for box in side_boxes:
box.restore_box_attributes()
def _standardize_page_based_counters(style, pseudo_type):
"""Drop 'pages' counter from style in @page and @margin context.
Ensure `counter-increment: page` for @page context if not otherwise
manipulated by the style.
"""
page_counter_touched = False
for propname in ('counter_set', 'counter_reset', 'counter_increment'):
if style[propname] == 'auto':
style[propname] = ()
continue
justified_values = []
for name, value in style[propname]:
if name == 'page':
page_counter_touched = True
if name != 'pages':
justified_values.append((name, value))
style[propname] = tuple(justified_values)
if pseudo_type is None and not page_counter_touched:
style['counter_increment'] = (
('page', 1),) + style['counter_increment']
def make_margin_boxes(context, page, state):
"""Yield laid-out margin boxes for this page.
``state`` is the actual, up-to-date page-state from
``context.page_maker[context.current_page]``.
"""
# This is a closure only to make calls shorter
def make_box(at_keyword, containing_block):
"""Return a margin box with resolved percentages.
The margin box may still have 'auto' values.
Return ``None`` if this margin box should not be generated.
:param at_keyword: which margin box to return, eg. '@top-left'
:param containing_block: as expected by :func:`resolve_percentages`.
"""
style = context.style_for(page.page_type, at_keyword)
if style is None:
# doesn't affect counters
style = computed_from_cascaded(
element=None, cascaded={}, parent_style=page.style)
_standardize_page_based_counters(style, at_keyword)
box = boxes.MarginBox(at_keyword, style)
# Empty boxes should not be generated, but they may be needed for
# the layout of their neighbors.
# TODO: should be the computed value.
box.is_generated = style['content'] not in (
'normal', 'inhibit', 'none')
# TODO: get actual counter values at the time of the last page break
if box.is_generated:
# @margins mustn't manipulate page-context counters
margin_state = copy.deepcopy(state)
quote_depth, counter_values, counter_scopes = margin_state
# TODO: check this, probably useless
counter_scopes.append(set())
build.update_counters(margin_state, box.style)
box.children = build.content_to_boxes(
box.style, box, quote_depth, counter_values,
context.get_image_from_uri, context.target_collector,
context.counter_style, context, page)
build.process_whitespace(box)
build.process_text_transform(box)
box = build.create_anonymous_boxes(box)
resolve_percentages(box, containing_block)
if not box.is_generated:
box.width = box.height = 0
for side in ('top', 'right', 'bottom', 'left'):
box._reset_spacing(side)
return box
margin_top = page.margin_top
margin_bottom = page.margin_bottom
margin_left = page.margin_left
margin_right = page.margin_right
max_box_width = page.border_width()
max_box_height = page.border_height()
# bottom right corner of the border box
page_end_x = margin_left + max_box_width
page_end_y = margin_top + max_box_height
# Margin box dimensions, described in
# http://dev.w3.org/csswg/css3-page/#margin-box-dimensions
generated_boxes = []
for prefix, vertical, containing_block, position_x, position_y in [
('top', False, (max_box_width, margin_top),
margin_left, 0),
('bottom', False, (max_box_width, margin_bottom),
margin_left, page_end_y),
('left', True, (margin_left, max_box_height),
0, margin_top),
('right', True, (margin_right, max_box_height),
page_end_x, margin_top),
]:
if vertical:
suffixes = ['top', 'middle', 'bottom']
fixed_outer, variable_outer = containing_block
else:
suffixes = ['left', 'center', 'right']
variable_outer, fixed_outer = containing_block
side_boxes = [
make_box(f'@{prefix}-{suffix}', containing_block)
for suffix in suffixes]
if not any(box.is_generated for box in side_boxes):
continue
# We need the three boxes together for the variable dimension:
compute_variable_dimension(
context, side_boxes, vertical, variable_outer)
for box, offset in zip(side_boxes, [0, 0.5, 1]):
if not box.is_generated:
continue
box.position_x = position_x
box.position_y = position_y
if vertical:
box.position_y += offset * (
variable_outer - box.margin_height())
else:
box.position_x += offset * (
variable_outer - box.margin_width())
compute_fixed_dimension(
context, box, fixed_outer, not vertical,
prefix in ['top', 'left'])
generated_boxes.append(box)
# Corner boxes
for at_keyword, cb_width, cb_height, position_x, position_y in [
('@top-left-corner', margin_left, margin_top, 0, 0),
('@top-right-corner', margin_right, margin_top, page_end_x, 0),
('@bottom-left-corner', margin_left, margin_bottom, 0, page_end_y),
('@bottom-right-corner', margin_right, margin_bottom,
page_end_x, page_end_y),
]:
box = make_box(at_keyword, (cb_width, cb_height))
if not box.is_generated:
continue
box.position_x = position_x
box.position_y = position_y
compute_fixed_dimension(
context, box, cb_height, True, 'top' in at_keyword)
compute_fixed_dimension(
context, box, cb_width, False, 'left' in at_keyword)
generated_boxes.append(box)
for box in generated_boxes:
yield margin_box_content_layout(context, page, box)
def margin_box_content_layout(context, page, box):
"""Layout a margin boxs content once the box has dimensions."""
positioned_boxes = []
box, resume_at, next_page, _, _ = block_container_layout(
context, box, bottom_space=-float('inf'), skip_stack=None,
page_is_empty=True, absolute_boxes=positioned_boxes,
fixed_boxes=positioned_boxes, adjoining_margins=None, discard=False)
assert resume_at is None
for absolute_box in positioned_boxes:
absolute_layout(
context, absolute_box, box, positioned_boxes, bottom_space=0,
skip_stack=None)
vertical_align = box.style['vertical_align']
# Every other value is read as 'top', ie. no change.
if vertical_align in ('middle', 'bottom') and box.children:
first_child = box.children[0]
last_child = box.children[-1]
top = first_child.position_y
# Not always exact because floating point errors
# assert top == box.content_box_y()
bottom = last_child.position_y + last_child.margin_height()
content_height = bottom - top
offset = box.height - content_height
if vertical_align == 'middle':
offset /= 2
for child in box.children:
child.translate(0, offset)
return box
def page_width_or_height(box, containing_block_size):
"""Take a :class:`OrientedBox` object and set either width, margin-left
and margin-right; or height, margin-top and margin-bottom.
"The width and horizontal margins of the page box are then calculated
exactly as for a non-replaced block element in normal flow. The height
and vertical margins of the page box are calculated analogously (instead
of using the block height formulas). In both cases if the values are
over-constrained, instead of ignoring any margins, the containing block
is resized to coincide with the margin edges of the page box."
http://dev.w3.org/csswg/css3-page/#page-box-page-rule
http://www.w3.org/TR/CSS21/visudet.html#blockwidth
"""
remaining = containing_block_size - box.padding_plus_border
if box.inner == 'auto':
if box.margin_a == 'auto':
box.margin_a = 0
if box.margin_b == 'auto':
box.margin_b = 0
box.inner = remaining - box.margin_a - box.margin_b
elif box.margin_a == box.margin_b == 'auto':
box.margin_a = box.margin_b = (remaining - box.inner) / 2
elif box.margin_a == 'auto':
box.margin_a = remaining - box.inner - box.margin_b
elif box.margin_b == 'auto':
box.margin_b = remaining - box.inner - box.margin_a
box.restore_box_attributes()
@handle_min_max_width
def page_width(box, context, containing_block_width):
page_width_or_height(HorizontalBox(context, box), containing_block_width)
@handle_min_max_height
def page_height(box, context, containing_block_height):
page_width_or_height(VerticalBox(context, box), containing_block_height)
def make_page(context, root_box, page_type, resume_at, page_number,
page_state):
"""Take just enough content from the beginning to fill one page.
Return ``(page, finished)``. ``page`` is a laid out PageBox object
and ``resume_at`` indicates where in the document to start the next page,
or is ``None`` if this was the last page.
:param page_number: integer, start at 1 for the first page
:param resume_at: as returned by ``make_page()`` for the previous page,
or ``None`` for the first page.
"""
style = context.style_for(page_type)
# Propagated from the root or <body>.
style['overflow'] = root_box.viewport_overflow
page = boxes.PageBox(page_type, style)
device_size = page.style['size']
resolve_percentages(page, device_size)
page.position_x = 0
page.position_y = 0
cb_width, cb_height = device_size
page_width(page, context, cb_width)
page_height(page, context, cb_height)
root_box.position_x = page.content_box_x()
root_box.position_y = page.content_box_y()
context.page_bottom = root_box.position_y + page.height
initial_containing_block = page
footnote_area_style = context.style_for(page_type, '@footnote')
footnote_area = boxes.FootnoteAreaBox(page, footnote_area_style)
resolve_percentages(footnote_area, page)
footnote_area.position_x = page.content_box_x()
footnote_area.position_y = context.page_bottom
if page_type.blank:
previous_resume_at = resume_at
root_box = root_box.copy_with_children([])
# TODO: handle cases where the root element is something else.
# See http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
assert isinstance(root_box, (boxes.BlockBox, boxes.FlexContainerBox))
context.create_block_formatting_context()
context.current_page = page_number
context.current_page_footnotes = context.reported_footnotes.copy()
context.current_footnote_area = footnote_area
if context.reported_footnotes:
footnote_area.children = tuple(context.reported_footnotes)
context.reported_footnotes = []
reported_footnote_area = build.create_anonymous_boxes(
footnote_area.deepcopy())
reported_footnote_area, _, _, _, _ = block_level_layout(
context, reported_footnote_area, -float('inf'), None,
footnote_area.page, True, [], [], [], False)
footnote_area.height = reported_footnote_area.height
context.page_bottom -= reported_footnote_area.margin_height()
page_is_empty = True
adjoining_margins = []
positioned_boxes = [] # Mixed absolute and fixed
out_of_flow_boxes = []
broken_out_of_flow = []
for box, containing_block, skip_stack in context.broken_out_of_flow:
box.position_y = 0
if box.is_floated():
out_of_flow_box, out_of_flow_resume_at = float_layout(
context, box, containing_block, positioned_boxes,
positioned_boxes, 0, skip_stack)
else:
assert box.is_absolutely_positioned()
out_of_flow_box, out_of_flow_resume_at = absolute_box_layout(
context, box, containing_block, positioned_boxes, 0,
skip_stack)
out_of_flow_boxes.append(out_of_flow_box)
if out_of_flow_resume_at:
broken_out_of_flow.append(
(box, containing_block, out_of_flow_resume_at))
context.broken_out_of_flow = broken_out_of_flow
root_box, resume_at, next_page, _, _ = block_level_layout(
context, root_box, 0, resume_at, initial_containing_block,
page_is_empty, positioned_boxes, positioned_boxes, adjoining_margins,
discard=False)
assert root_box
root_box.children = tuple(out_of_flow_boxes) + root_box.children
page.fixed_boxes = [
placeholder._box for placeholder in positioned_boxes
if placeholder._box.style['position'] == 'fixed']
for absolute_box in positioned_boxes:
absolute_layout(
context, absolute_box, page, positioned_boxes, bottom_space=0,
skip_stack=None)
footnote_area = build.create_anonymous_boxes(footnote_area.deepcopy())
footnote_area, _, _, _, _ = block_level_layout(
context, footnote_area, -float('inf'), None, footnote_area.page,
True, [], [], [], False)
footnote_area.translate(dy=-footnote_area.margin_height())
context.finish_block_formatting_context(root_box)
page.children = [root_box, footnote_area]
descendants = page.descendants()
# Update page counter values
_standardize_page_based_counters(style, None)
build.update_counters(page_state, style)
page_counter_values = page_state[1]
# page_counter_values will be cached in the page_maker
target_collector = context.target_collector
page_maker = context.page_maker
# remake_state tells the make_all_pages-loop in layout_document()
# whether and what to re-make.
remake_state = page_maker[page_number - 1][-1]
# Evaluate and cache page values only once (for the first LineBox)
# otherwise we suffer endless loops when the target/pseudo-element
# spans across multiple pages
cached_anchors = []
cached_lookups = []
for (_, _, _, _, x_remake_state) in page_maker[:page_number - 1]:
cached_anchors.extend(x_remake_state.get('anchors', []))
cached_lookups.extend(x_remake_state.get('content_lookups', []))
for child in descendants:
# Cache target's page counters
anchor = child.style['anchor']
if anchor and anchor not in cached_anchors:
remake_state['anchors'].append(anchor)
cached_anchors.append(anchor)
# Re-make of affected targeting boxes is inclusive
target_collector.cache_target_page_counters(
anchor, page_counter_values, page_number - 1, page_maker)
# string-set and bookmark-labels don't create boxes, only `content`
# requires another call to make_page. There is maximum one 'content'
# item per box.
# TODO: remove attribute or set a default value in Box class
if hasattr(child, 'missing_link'):
# A CounterLookupItem exists for the css-token 'content'
counter_lookup = target_collector.counter_lookup_items.get(
(child.missing_link, 'content'))
else:
counter_lookup = None
# Resolve missing (page based) counters
if counter_lookup is not None:
call_parse_again = False
# Prevent endless loops
counter_lookup_id = id(counter_lookup)
refresh_missing_counters = counter_lookup_id not in cached_lookups
if refresh_missing_counters:
remake_state['content_lookups'].append(counter_lookup_id)
cached_lookups.append(counter_lookup_id)
counter_lookup.page_maker_index = page_number - 1
# Step 1: page based back-references
# Marked as pending by target_collector.cache_target_page_counters
if counter_lookup.pending:
if (page_counter_values !=
counter_lookup.cached_page_counter_values):
counter_lookup.cached_page_counter_values = copy.deepcopy(
page_counter_values)
counter_lookup.pending = False
call_parse_again = True
# Step 2: local counters
# If the box mixed-in page counters changed, update the content
# and cache the new values.
missing_counters = counter_lookup.missing_counters
if missing_counters:
if 'pages' in missing_counters:
remake_state['pages_wanted'] = True
if refresh_missing_counters and page_counter_values != \
counter_lookup.cached_page_counter_values:
counter_lookup.cached_page_counter_values = \
copy.deepcopy(page_counter_values)
for counter_name in missing_counters:
counter_value = page_counter_values.get(
counter_name, None)
if counter_value is not None:
call_parse_again = True
# no need to loop them all
break
# Step 3: targeted counters
target_missing = counter_lookup.missing_target_counters
for anchor_name, missed_counters in target_missing.items():
if 'pages' not in missed_counters:
continue
# Adjust 'pages_wanted'
item = target_collector.target_lookup_items.get(
anchor_name, None)
page_maker_index = item.page_maker_index
if page_maker_index >= 0 and anchor_name in cached_anchors:
page_maker[page_maker_index][-1]['pages_wanted'] = True
# 'content_changed' is triggered in
# targets.cache_target_page_counters()
if call_parse_again:
remake_state['content_changed'] = True
counter_lookup.parse_again(page_counter_values)
if page_type.blank:
resume_at = previous_resume_at
return page, resume_at, next_page
def set_page_type_computed_styles(page_type, html, style_for):
"""Set style for page types and pseudo-types matching ``page_type``."""
style_for.add_page_declarations(page_type)
# Apply style for page
style_for.set_computed_styles(
page_type,
# @page inherits from the root element:
# http://lists.w3.org/Archives/Public/www-style/2012Jan/1164.html
root=html.etree_element, parent=html.etree_element,
base_url=html.base_url)
# Apply style for page pseudo-elements (margin boxes)
for element, pseudo_type in style_for.get_cascaded_styles():
if pseudo_type and element == page_type:
style_for.set_computed_styles(
element, pseudo_type=pseudo_type,
# The pseudo-element inherits from the element.
root=html.etree_element, parent=element,
base_url=html.base_url)
def remake_page(index, context, root_box, html):
"""Return one laid out page without margin boxes.
Start with the initial values from ``context.page_maker[index]``.
The resulting values / initial values for the next page are stored in
the ``page_maker``.
As the function's name suggests: the plan is not to make all pages
repeatedly when a missing counter was resolved, but rather re-make the
single page where the ``content_changed`` happened.
"""
page_maker = context.page_maker
(initial_resume_at, initial_next_page, right_page, initial_page_state,
remake_state) = page_maker[index]
# PageType for current page, values for page_maker[index + 1].
# Don't modify actual page_maker[index] values!
# TODO: should we store (and reuse) page_type in the page_maker?
page_state = copy.deepcopy(initial_page_state)
next_page_name = initial_next_page['page']
first = index == 0
if initial_next_page['break'] in ('left', 'right'):
next_page_side = initial_next_page['break']
elif initial_next_page['break'] in ('recto', 'verso'):
direction_ltr = root_box.style['direction'] == 'ltr'
break_verso = initial_next_page['break'] == 'verso'
next_page_side = 'right' if direction_ltr ^ break_verso else 'left'
else:
next_page_side = None
blank = bool(
(next_page_side == 'left' and right_page) or
(next_page_side == 'right' and not right_page) or
(context.reported_footnotes and initial_resume_at is None))
if blank:
next_page_name = ''
side = 'right' if right_page else 'left'
page_type = PageType(side, blank, first, index, name=next_page_name)
set_page_type_computed_styles(page_type, html, context.style_for)
context.forced_break = (
initial_next_page['break'] != 'any' or initial_next_page['page'])
context.margin_clearance = False
# make_page wants a page_number of index + 1
page_number = index + 1
page, resume_at, next_page = make_page(
context, root_box, page_type, initial_resume_at, page_number,
page_state)
assert next_page
if blank:
next_page['page'] = initial_next_page['page']
right_page = not right_page
# Check whether we need to append or update the next page_maker item
if index + 1 >= len(page_maker):
# New page
page_maker_next_changed = True
else:
# Check whether something changed
# TODO: Find what we need to compare. Is resume_at enough?
(next_resume_at, next_next_page, next_right_page,
next_page_state, _) = page_maker[index + 1]
page_maker_next_changed = (
next_resume_at != resume_at or
next_next_page != next_page or
next_right_page != right_page or
next_page_state != page_state)
if page_maker_next_changed:
# Reset remake_state
remake_state = {
'content_changed': False,
'pages_wanted': False,
'anchors': [],
'content_lookups': [],
}
# Setting content_changed to True ensures remake.
# If resume_at is None (last page) it must be False to prevent endless
# loops and list index out of range (see #794).
remake_state['content_changed'] = resume_at is not None
# page_state is already a deepcopy
item = resume_at, next_page, right_page, page_state, remake_state
if index + 1 >= len(page_maker):
page_maker.append(item)
else:
page_maker[index + 1] = item
return page, resume_at
def make_all_pages(context, root_box, html, pages):
"""Return a list of laid out pages without margin boxes.
Re-make pages only if necessary.
"""
i = 0
while True:
remake_state = context.page_maker[i][-1]
if (len(pages) == 0 or
remake_state['content_changed'] or
remake_state['pages_wanted']):
PROGRESS_LOGGER.info('Step 5 - Creating layout - Page %d', i + 1)
# Reset remake_state
remake_state['content_changed'] = False
remake_state['pages_wanted'] = False
remake_state['anchors'] = []
remake_state['content_lookups'] = []
page, resume_at = remake_page(i, context, root_box, html)
yield page
else:
PROGRESS_LOGGER.info(
'Step 5 - Creating layout - Page %d (up-to-date)', i + 1)
resume_at = context.page_maker[i + 1][0]
yield pages[i]
i += 1
if resume_at is None and not context.reported_footnotes:
# Throw away obsolete pages
context.page_maker = context.page_maker[:i + 1]
return

View File

@@ -0,0 +1,975 @@
"""
weasyprint.layout.tables
------------------------
Layout for tables and internal table boxes.
"""
from ..formatting_structure import boxes
from ..logger import LOGGER
from .percent import resolve_one_percentage, resolve_percentages
from .preferred import max_content_width, table_and_columns_preferred_widths
def table_layout(context, table, bottom_space, skip_stack, containing_block,
page_is_empty, absolute_boxes, fixed_boxes):
"""Layout for a table box."""
from .block import (
block_container_layout, block_level_page_break,
find_earlier_page_break)
table.remove_decoration(start=skip_stack is not None, end=False)
column_widths = table.column_widths
if table.style['border_collapse'] == 'separate':
border_spacing_x, border_spacing_y = table.style['border_spacing']
else:
border_spacing_x = 0
border_spacing_y = 0
column_positions = table.column_positions = []
rows_left_x = table.content_box_x() + border_spacing_x
if table.style['direction'] == 'ltr':
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
else:
position_x = table.content_box_x() + table.width
rows_x = position_x - border_spacing_x
for width in column_widths:
position_x -= border_spacing_x
position_x -= width
column_positions.append(position_x)
rows_width = rows_x - position_x
if table.style['border_collapse'] == 'collapse':
table.skip_cell_border_top = False
table.skip_cell_border_bottom = False
split_cells = False
if skip_stack:
(skipped_groups, group_skip_stack), = skip_stack.items()
if group_skip_stack:
(skipped_rows, cells_skip_stack), = group_skip_stack.items()
if cells_skip_stack:
split_cells = True
else:
skipped_rows = 0
for group in table.children[:skipped_groups]:
skipped_rows += len(group.children)
else:
skipped_rows = 0
if not split_cells:
_, horizontal_borders = table.collapsed_border_grid
if horizontal_borders:
table.border_top_width = max(
width for _, (_, width, _)
in horizontal_borders[skipped_rows]) / 2
# Make this a sub-function so that many local variables like rows_x
# don't need to be passed as parameters.
def group_layout(group, position_y, bottom_space, page_is_empty,
skip_stack):
resume_at = None
next_page = {'break': 'any', 'page': None}
original_page_is_empty = page_is_empty
resolve_percentages(group, containing_block=table)
group.position_x = rows_left_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]
is_group_start = skip_stack is None
if is_group_start:
skip = 0
else:
(skip, skip_stack), = skip_stack.items()
for index_row, row in enumerate(group.children[skip:], start=skip):
row.index = index_row
if new_group_children:
page_break = block_level_page_break(
new_group_children[-1], row)
if page_break in ('page', 'recto', 'verso', 'left', 'right'):
next_page['break'] = page_break
resume_at = {index_row: None}
break
resolve_percentages(row, containing_block=table)
row.position_x = rows_left_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 index_cell, cell in enumerate(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.warning(
'This table row has more columns than the table, '
f'ignored {len(ignored_cells)} cells: {ignored_cells}')
break
resolve_percentages(cell, containing_block=table)
if table.style['direction'] == 'ltr':
cell.position_x = column_positions[cell.grid_x]
else:
cell.position_x = column_positions[
cell.grid_x + cell.colspan - 1]
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
# TODO: we should remove the number of columns with no
# originating cells to cell.colspan, see
# test_layout_table_auto_49
cell.width = (
sum(spanned_widths) +
border_spacing_x * (cell.colspan - 1) -
borders_plus_padding)
# The computed height is a minimum
cell.computed_height = cell.height
cell.height = 'auto'
if skip_stack:
if index_cell in skip_stack:
cell_skip_stack = skip_stack[index_cell]
else:
cell_skip_stack = {len(cell.children): None}
else:
cell_skip_stack = None
if cell_skip_stack:
cell.remove_decoration(start=True, end=False)
if table.style['border_collapse'] == 'collapse':
table.skip_cell_border_top = True
# First try to render content as if there was already something
# on the page to avoid hitting block_level_layouts TODO. Then
# force to render something if the page is actually empty, or
# just draw an empty cell otherwise. See
# test_table_break_children_margin.
new_cell, cell_resume_at, _, _, _ = block_container_layout(
context, cell, bottom_space, cell_skip_stack,
page_is_empty=page_is_empty, absolute_boxes=absolute_boxes,
fixed_boxes=fixed_boxes, adjoining_margins=None,
discard=False)
if new_cell is None:
cell = cell.copy_with_children([])
cell, _, _, _, _ = block_container_layout(
context, cell, bottom_space, cell_skip_stack,
page_is_empty=True, absolute_boxes=[],
fixed_boxes=[], adjoining_margins=None,
discard=False)
cell_resume_at = {0: None}
else:
cell = new_cell
cell.remove_decoration(
start=cell_skip_stack is not None,
end=cell_resume_at is not None)
if cell_resume_at:
if resume_at is None:
resume_at = {index_row: {}}
resume_at[index_row][index_cell] = cell_resume_at
cell.empty = not any(
child.is_floated() or child.is_in_normal_flow()
for child in cell.children)
cell.content_height = cell.height
if cell.computed_height != 'auto':
cell.height = max(cell.height, cell.computed_height)
new_row_children.append(cell)
if resume_at and not page_is_empty:
if row.style['break_inside'] in ('avoid', 'avoid-page'):
resume_at = {index_row: {}}
break
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:
extra = row.baseline - cell.baseline
if cell.baseline != row.baseline and extra:
add_top_padding(cell, extra)
# 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
if row.height == 'auto':
row_bottom_y = max(
cell.position_y + cell.border_height()
for cell in ending_cells)
row.height = max(row_bottom_y - row.position_y, 0)
else:
row.height = max(row.height, max(
row_cell.height for row_cell in ending_cells))
row_bottom_y = row.position_y + row.height
else:
row_bottom_y = row.position_y
row.height = 0
if not baseline_cells:
row.baseline = row_bottom_y
# Add extra padding to make the cells the same height as the row
# and honor vertical-align
for cell in ending_cells:
cell_bottom_y = cell.position_y + cell.border_height()
extra = row_bottom_y - cell_bottom_y
if extra:
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
if cell.computed_height != 'auto':
vertical_align_shift = 0
if cell.vertical_align == 'middle':
vertical_align_shift = (
cell.computed_height - cell.content_height) / 2
elif cell.vertical_align == 'bottom':
vertical_align_shift = (
cell.computed_height - cell.content_height)
if vertical_align_shift > 0:
for child in cell.children:
child.translate(dy=vertical_align_shift)
next_position_y = row.position_y + row.height
if resume_at is None:
next_position_y += border_spacing_y
# Break if one cell was broken
break_cell = False
if resume_at:
values, = list(resume_at.values())
if len(row.children) == len(values):
for cell_resume_at in values.values():
if cell_resume_at != {0: None}:
break_cell = True
break
else:
# No cell was displayed, give up row
next_position_y = float('inf')
page_is_empty = False
resume_at = None
else:
break_cell = True
# Break if this row overflows the page, unless there is no
# other content on the page.
if not page_is_empty and (
next_position_y > context.page_bottom - bottom_space):
if new_group_children:
previous_row = new_group_children[-1]
page_break = block_level_page_break(previous_row, row)
if page_break == 'avoid':
earlier_page_break = find_earlier_page_break(
new_group_children, absolute_boxes, fixed_boxes)
if earlier_page_break:
new_group_children, resume_at = earlier_page_break
break
else:
resume_at = {index_row: None}
break
if original_page_is_empty:
resume_at = {index_row: None}
else:
return None, None, next_page
break
new_group_children.append(row)
position_y = next_position_y
page_is_empty = False
skip_stack = None
if break_cell and table.style['border_collapse'] == 'collapse':
table.skip_cell_border_bottom = True
if break_cell or resume_at:
break
# Do not keep the row group if we made a page break
# before any of its rows or with 'avoid'
if resume_at and not original_page_is_empty and (
group.style['break_inside'] in ('avoid', 'avoid-page') or
not new_group_children):
return None, None, next_page
group = group.copy_with_children(new_group_children)
group.remove_decoration(
start=not is_group_start, end=resume_at is not None)
# 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 = max(
cell.content_box_y() + cell.height
for cell in row.children) - row.position_y
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
return group, resume_at, next_page
def body_groups_layout(skip_stack, position_y, bottom_space,
page_is_empty):
if skip_stack is None:
skip = 0
else:
(skip, skip_stack), = skip_stack.items()
new_table_children = []
resume_at = None
next_page = {'break': 'any', 'page': None}
for i, group in enumerate(table.children[skip:]):
if group.is_header or group.is_footer:
continue
# Index is useless for headers and footers, as we never want to
# break pages after the header or before the footer.
index_group = i + skip
group.index = index_group
if new_table_children:
page_break = block_level_page_break(
new_table_children[-1], group)
if page_break in ('page', 'recto', 'verso', 'left', 'right'):
next_page['break'] = page_break
resume_at = {index_group: None}
break
new_group, resume_at, next_page = group_layout(
group, position_y, bottom_space, page_is_empty, skip_stack)
skip_stack = None
if new_group is None:
if new_table_children:
previous_group = new_table_children[-1]
page_break = block_level_page_break(previous_group, group)
if page_break == 'avoid':
earlier_page_break = find_earlier_page_break(
new_table_children, absolute_boxes, fixed_boxes)
if earlier_page_break is not None:
new_table_children, resume_at = earlier_page_break
break
resume_at = {index_group: None}
else:
return None, None, next_page, position_y
break
new_table_children.append(new_group)
position_y += new_group.height + border_spacing_y
page_is_empty = False
if resume_at:
resume_at = {index_group: resume_at}
break
return new_table_children, resume_at, next_page, position_y
# Layout for row groups, rows and cells
position_y = table.content_box_y()
if skip_stack is None:
position_y += border_spacing_y
initial_position_y = position_y
table_rows = [
child for child in table.children
if not child.is_header and not child.is_footer]
def all_groups_layout():
# If the page is not empty, we try to render the header and the footer
# on it. If the table does not fit on the page, we try to render it on
# the next page.
# If the page is empty and the header and footer are too big, there
# are not rendered. If no row can be rendered because of the header and
# the footer, the header and/or the footer are not rendered.
if page_is_empty:
header_footer_bottom_space = bottom_space
else:
header_footer_bottom_space = -float('inf')
if table.children and table.children[0].is_header:
header = table.children[0]
header, resume_at, next_page = group_layout(
header, position_y, header_footer_bottom_space,
skip_stack=None, page_is_empty=False)
if header and not resume_at:
header_height = header.height + border_spacing_y
else: # Header too big for the page
header = None
else:
header = None
if table.children and table.children[-1].is_footer:
footer = table.children[-1]
footer, resume_at, next_page = group_layout(
footer, position_y, header_footer_bottom_space,
skip_stack=None, page_is_empty=False)
if footer and not resume_at:
footer_height = footer.height + border_spacing_y
else: # Footer too big for the page
footer = None
else:
footer = None
# Don't remove headers and footers if breaks are avoided in line groups
if skip_stack:
skip, = skip_stack
else:
skip = 0
avoid_breaks = False
for group in table.children[skip:]:
if not group.is_header and not group.is_footer:
avoid_breaks = (
group.style['break_inside'] in ('avoid', 'avoid-page'))
break
if header and footer:
# Try with both the header and footer
new_table_children, resume_at, next_page, end_position_y = (
body_groups_layout(
skip_stack, position_y + header_height,
bottom_space + footer_height, page_is_empty=avoid_breaks))
if new_table_children or not table_rows or not page_is_empty:
footer.translate(dy=end_position_y - footer.position_y)
end_position_y += footer_height
return (
header, new_table_children, footer, end_position_y,
resume_at, next_page)
else:
# We could not fit any content, drop the footer
footer = None
if header and not footer:
# Try with just the header
new_table_children, resume_at, next_page, end_position_y = (
body_groups_layout(
skip_stack, position_y + header_height, bottom_space,
page_is_empty=avoid_breaks))
if new_table_children or not table_rows or not page_is_empty:
return (
header, new_table_children, footer, end_position_y,
resume_at, next_page)
else:
# We could not fit any content, drop the header
header = None
if footer and not header:
# Try with just the footer
new_table_children, resume_at, next_page, end_position_y = (
body_groups_layout(
skip_stack, position_y, bottom_space + footer_height,
page_is_empty=avoid_breaks))
if new_table_children or not table_rows or not page_is_empty:
footer.translate(dy=end_position_y - footer.position_y)
end_position_y += footer_height
return (
header, new_table_children, footer, end_position_y,
resume_at, next_page)
else:
# We could not fit any content, drop the footer
footer = None
assert not (header or footer)
new_table_children, resume_at, next_page, end_position_y = (
body_groups_layout(
skip_stack, position_y, bottom_space, page_is_empty))
return (
header, new_table_children, footer, end_position_y, resume_at,
next_page)
def get_column_cells(table, column):
"""Closure getting the column cells."""
return lambda: [
cell
for row_group in table.children
for row in row_group.children
for cell in row.children
if cell.grid_x == column.grid_x]
header, new_table_children, footer, position_y, resume_at, next_page = (
all_groups_layout())
if new_table_children is None:
assert resume_at is None
table = None
adjoining_margins = []
collapsing_through = False
return (
table, resume_at, next_page, adjoining_margins, collapsing_through)
table = table.copy_with_children(
([header] if header is not None else []) +
new_table_children +
([footer] if footer is not None else []))
table.remove_decoration(
start=skip_stack is not None, end=resume_at is not None)
if table.style['border_collapse'] == 'collapse':
table.skipped_rows = skipped_rows
# 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)
if column.grid_x < len(column_positions):
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
else:
# Ignore extra empty columns
column.position_x = 0
column.position_y = 0
column.width = 0
column.height = 0
resolve_percentages(group, containing_block=table)
column.get_cells = get_column_cells(table, column)
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['break_inside'] in ('avoid', 'avoid-page')):
table = None
resume_at = None
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(box):
"""Run the fixed table layout and return a list of column widths.
http://www.w3.org/TR/CSS21/tables.html#fixed-table-layout
"""
table = box.get_wrapped_table()
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
if table.style['border_collapse'] == 'separate':
border_spacing_x, _ = table.style['border_spacing']
else:
border_spacing_x = 0
# `width` on cells of the first row.
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. Subtract these
# known widths and divide among remaining columns.
columns_without_width = [] # and occupied by this cell
for j in range(i, i + cell.colspan):
if column_widths[j] is None:
columns_without_width.append(j)
else:
width -= column_widths[j]
if columns_without_width:
width_per_column = width / len(columns_without_width)
for j in columns_without_width:
column_widths[j] = width_per_column
del width
i += cell.colspan
del i
# 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, w in enumerate(column_widths)
if w 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(context, box, 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
"""
table = box.get_wrapped_table()
(table_min_content_width, table_max_content_width,
column_min_content_widths, column_max_content_widths,
column_intrinsic_percentages, constrainedness,
total_horizontal_border_spacing, grid) = \
table_and_columns_preferred_widths(context, box, outer=False)
margins = 0
if box.margin_left != 'auto':
margins += box.margin_left
if box.margin_right != 'auto':
margins += box.margin_right
paddings = table.padding_left + table.padding_right
borders = table.border_left_width + table.border_right_width
cb_width, _ = containing_block
available_width = cb_width - margins - paddings - borders
if table.width == 'auto':
if available_width <= table_min_content_width:
table.width = table_min_content_width
elif available_width < table_max_content_width:
table.width = available_width
else:
table.width = table_max_content_width
else:
if table.width < table_min_content_width:
table.width = table_min_content_width
if not grid:
table.column_widths = []
return
assignable_width = table.width - total_horizontal_border_spacing
min_content_guess = column_min_content_widths[:]
min_content_percentage_guess = column_min_content_widths[:]
min_content_specified_guess = column_min_content_widths[:]
max_content_guess = column_max_content_widths[:]
guesses = (
min_content_guess, min_content_percentage_guess,
min_content_specified_guess, max_content_guess)
for i in range(len(grid)):
if column_intrinsic_percentages[i]:
min_content_percentage_guess[i] = max(
column_intrinsic_percentages[i] / 100 * assignable_width,
column_min_content_widths[i])
min_content_specified_guess[i] = min_content_percentage_guess[i]
max_content_guess[i] = min_content_percentage_guess[i]
elif constrainedness[i]:
min_content_specified_guess[i] = column_min_content_widths[i]
if assignable_width <= sum(max_content_guess):
# Default values shouldn't be used, but we never know.
# See https://github.com/Kozea/WeasyPrint/issues/770
lower_guess = guesses[0]
upper_guess = guesses[-1]
# We have to work around floating point rounding errors here.
# The 1e-9 value comes from PEP 485.
for guess in guesses:
if sum(guess) <= assignable_width * (1 + 1e-9):
lower_guess = guess
else:
break
for guess in guesses[::-1]:
if sum(guess) >= assignable_width * (1 - 1e-9):
upper_guess = guess
else:
break
if upper_guess == lower_guess:
# TODO: Uncomment the assert when bugs #770 and #628 are closed
# Equivalent to "assert assignable_width == sum(upper_guess)"
# assert abs(assignable_width - sum(upper_guess)) <= (
# assignable_width * 1e-9)
table.column_widths = upper_guess
else:
added_widths = [
upper_guess[i] - lower_guess[i] for i in range(len(grid))]
available_ratio = (
(assignable_width - sum(lower_guess)) / sum(added_widths))
table.column_widths = [
lower_guess[i] + added_widths[i] * available_ratio
for i in range(len(grid))]
else:
table.column_widths = max_content_guess
excess_width = assignable_width - sum(max_content_guess)
excess_width = distribute_excess_width(
context, grid, excess_width, table.column_widths, constrainedness,
column_intrinsic_percentages, column_max_content_widths)
if excess_width:
if table_min_content_width < table.width - excess_width:
# Reduce the width of the size from the excess width that has
# not been distributed.
table.width -= excess_width
else:
# Break rules
columns = [i for i, column in enumerate(grid) if any(column)]
for i in columns:
table.column_widths[i] += excess_width / len(columns)
def table_wrapper_width(context, wrapper, containing_block):
"""Find the width of each column and derive the wrapper width."""
table = wrapper.get_wrapped_table()
resolve_percentages(table, containing_block)
if table.style['table_layout'] == 'fixed' and table.width != 'auto':
fixed_table_layout(wrapper)
else:
auto_table_layout(context, wrapper, containing_block)
wrapper.width = table.border_width()
def cell_baseline(cell):
"""Return the y position of a cell baseline from the top of its border box.
See http://www.w3.org/TR/CSS21/tables.html#height-layout
"""
result = find_in_flow_baseline(
cell, baseline_types=(boxes.LineBox, boxes.TableRowBox))
if result is not None:
return result - cell.position_y
else:
# Default to the bottom of the content area.
return cell.border_top_width + cell.padding_top + cell.height
def find_in_flow_baseline(box, last=False, baseline_types=(boxes.LineBox,)):
"""Return the absolute y position for the first (or last) in-flow baseline.
If theres no in-flow baseline, return None.
"""
# TODO: synthetize baseline when needed
# See https://www.w3.org/TR/css-align-3/#synthesize-baseline
if isinstance(box, baseline_types):
return box.position_y + box.baseline
if isinstance(box, boxes.ParentBox) and not isinstance(
box, boxes.TableCaptionBox):
children = reversed(box.children) if last else box.children
for child in children:
if child.is_in_normal_flow():
result = find_in_flow_baseline(child, last, baseline_types)
if result is not None:
return result
def distribute_excess_width(context, grid, excess_width, column_widths,
constrainedness, column_intrinsic_percentages,
column_max_content_widths,
column_slice=slice(0, None)):
"""Distribute available width to columns.
Return excess width left when it's impossible without breaking rules.
See http://dbaron.org/css/intrinsic/#distributetocols
"""
# First group
columns = [
(i + column_slice.start, column)
for i, column in enumerate(grid[column_slice])
if not constrainedness[i + column_slice.start] and
column_intrinsic_percentages[i + column_slice.start] == 0 and
column_max_content_widths[i + column_slice.start] > 0]
if columns:
current_widths = [column_widths[i] for i, column in columns]
differences = [
max(0, width[0] - width[1])
for width in zip(column_max_content_widths, current_widths)]
if sum(differences) > excess_width:
differences = [
difference / sum(differences) * excess_width
for difference in differences]
excess_width -= sum(differences)
for i, difference in enumerate(differences):
column_widths[columns[i][0]] += difference
if excess_width <= 0:
return
# Second group
columns = [
i + column_slice.start for i, column in enumerate(grid[column_slice])
if not constrainedness[i + column_slice.start] and
column_intrinsic_percentages[i + column_slice.start] == 0]
if columns:
for i in columns:
column_widths[i] += excess_width / len(columns)
return
# Third group
columns = [
(i + column_slice.start, column)
for i, column in enumerate(grid[column_slice])
if constrainedness[i + column_slice.start] and
column_intrinsic_percentages[i + column_slice.start] == 0 and
column_max_content_widths[i + column_slice.start] > 0]
if columns:
current_widths = [column_widths[i] for i, column in columns]
differences = [
max(0, width[0] - width[1])
for width in zip(column_max_content_widths, current_widths)]
if sum(differences) > excess_width:
differences = [
difference / sum(differences) * excess_width
for difference in differences]
excess_width -= sum(differences)
for i, difference in enumerate(differences):
column_widths[columns[i][0]] += difference
if excess_width <= 0:
return
# Fourth group
columns = [
(i + column_slice.start, column)
for i, column in enumerate(grid[column_slice])
if column_intrinsic_percentages[i + column_slice.start] > 0]
if columns:
fixed_width = sum(
column_widths[j] for j in range(len(grid))
if j not in [i for i, column in columns])
percentage_width = sum(
column_intrinsic_percentages[i]
for i, column in columns)
if fixed_width and percentage_width >= 100:
# Sum of the percentages are greater than 100%
ratio = excess_width
elif fixed_width == 0:
# No fixed width, let's take the whole excess width
ratio = excess_width
else:
ratio = fixed_width / (100 - percentage_width)
widths = [
column_intrinsic_percentages[i] * ratio for i, column in columns]
current_widths = [column_widths[i] for i, column in columns]
# Allow to reduce the size of the columns to respect the percentage
differences = [
width[0] - width[1]
for width in zip(widths, current_widths)]
if sum(differences) > excess_width:
differences = [
difference / sum(differences) * excess_width
for difference in differences]
excess_width -= sum(differences)
for i, difference in enumerate(differences):
column_widths[columns[i][0]] += difference
if excess_width <= 0:
return
# Bonus: we've tried our best to distribute the extra size, but we
# failed. Instead of blindly distributing the size among all the colums
# and breaking all the rules (as said in the draft), let's try to
# change the columns with no constraint at all, then resize the table,
# and at least break the rules to make the columns fill the table.
# Fifth group, part 1
columns = [
i + column_slice.start for i, column in enumerate(grid[column_slice])
if any(column) and
column_intrinsic_percentages[i + column_slice.start] == 0 and
not any(
max_content_width(context, cell)
for cell in column if cell)]
if columns:
for i in columns:
column_widths[i] += excess_width / len(columns)
return
# Fifth group, part 2, aka abort
return excess_width