nex_docus/backend/venv312/lib/python3.12/site-packages/weasyprint/layout/column.py

411 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""Layout for columns."""
from math import floor, inf
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, remove_placeholders)
style = box.style
width = style['column_width']
count = style['column_count']
gap = style['column_gap']
height = style['height']
original_bottom_space = bottom_space
context.in_column = True
if 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
# Set height if defined
if height != 'auto' and height.unit != '%':
assert height.unit == 'px'
height_defined = True
empty_space = context.page_bottom - box.content_box_y() - height.value
bottom_space = max(bottom_space, empty_space)
else:
height_defined = False
# TODO: the columns container 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)
# Define the number of columns and their widths
if width == 'auto' and count != 'auto':
width = max(0, box.width - (count - 1) * gap) / count
elif width != 'auto' and count == 'auto':
count = max(1, int(floor((box.width + gap) / (width + gap))))
width = (box.width + gap) / count - gap
else: # overconstrained, with width != 'auto' and count != 'auto'
count = min(count, int(floor((box.width + gap) / (width + gap))))
width = (box.width + gap) / count - gap
# Handle column-span property with the following structure:
# columns_and_blocks = [
# [column_child_1, column_child_2],
# spanning_block,
# …
# ]
columns_and_blocks = []
column_children = []
skip, = skip_stack.keys() if skip_stack else (0,)
for i, child in enumerate(box.children[skip:], start=skip):
if child.style['column_span'] == 'all':
if column_children:
columns_and_blocks.append(
(i - len(column_children), column_children))
columns_and_blocks.append((i, child.copy()))
column_children = []
continue
column_children.append(child.copy())
if column_children:
columns_and_blocks.append(
(i + 1 - len(column_children), column_children))
if skip_stack:
skip_stack = {0: skip_stack[skip]}
if not box.children:
next_page = {'break': 'any', 'page': None}
skip_stack = None
# Find height and balance.
#
# The current algorithm starts from the total available height, to check
# whether the whole content can fit. If it doesnt fit, we keep the partial
# rendering. If it fits, we try to balance the columns starting 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 = []
column_skip_stack = None
last_loop = False
break_page = False
footnote_area_heights = [
0 if context.current_footnote_area.height == 'auto'
else context.current_footnote_area.margin_height()]
last_footnotes_height = 0
for index, column_children_or_block in columns_and_blocks:
if not isinstance(column_children_or_block, list):
# We have 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, resume_at, next_page, adjoining_margins, _, _ = (
block_level_layout(
context, block, original_bottom_space, skip_stack,
containing_block, page_is_empty, absolute_boxes,
fixed_boxes, adjoining_margins))
skip_stack = None
if new_child is None:
last_loop = True
break_page = True
break
new_children.append(new_child)
current_position_y = (
new_child.border_height() + new_child.border_box_y())
adjoining_margins.append(new_child.margin_bottom)
if resume_at:
last_loop = True
break_page = True
column_skip_stack = resume_at
break
page_is_empty = False
continue
# We have a list of children that we have to balance between columns
column_children = column_children_or_block
# Find the total height available for the first run
current_position_y += collapse_margin(adjoining_margins)
adjoining_margins = []
column_box = _create_column_box(
box, containing_block, column_children, width, current_position_y)
height = max_height = (
context.page_bottom - current_position_y - original_bottom_space)
# Try to render columns until the content fits, increase the column
# height step by step
column_skip_stack = skip_stack
lost_space = inf
original_excluded_shapes = context.excluded_shapes[:]
original_page_is_empty = page_is_empty
page_is_empty = stop_rendering = balancing = False
while True:
# Remove extra excluded shapes introduced during the previous loop
while len(context.excluded_shapes) > len(original_excluded_shapes):
context.excluded_shapes.pop()
# Render the columns
column_skip_stack = skip_stack
consumed_heights = []
new_boxes = []
for i in range(count):
# Render one 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 or not balancing, [], [], [],
discard=False, max_lines=None)
if new_box is None:
# We didn't render anything, retry
column_skip_stack = {0: None}
break
new_boxes.append(new_box)
column_skip_stack = resume_at
# Calculate consumed height, empty space and next box height
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
consumed_height = (
in_flow_children[-1].margin_height() +
in_flow_children[-1].position_y - current_position_y)
empty_space = height - consumed_height
# Get the minimum size needed to render the next box
if column_skip_stack:
next_box = block_box_layout(
context, column_box, inf, column_skip_stack,
containing_block, True, [], [], [],
discard=False, max_lines=None)[0]
for child in next_box.children:
if child.is_in_normal_flow():
next_box_height = child.margin_height()
break
remove_placeholders(context, [next_box], [], [])
else:
next_box_height = 0
else:
consumed_height = empty_space = next_box_height = 0
consumed_heights.append(consumed_height)
# 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_height - empty_space > 1:
lost_space = min(lost_space, next_box_height - empty_space)
# Stop if we already rendered the whole content
if resume_at is None:
break
# Remove placeholders but keep the current footnote area height
last_footnotes_height = (
0 if context.current_footnote_area.height == 'auto'
else context.current_footnote_area.margin_height())
remove_placeholders(context, new_boxes, [], [])
if last_loop:
break
if balancing:
if column_skip_stack is None:
# We rendered the whole content, stop
break
# Increase the column heights and render them again
add_height = 1 if lost_space == inf else lost_space
height += add_height
if height > max_height:
# We reached max height, stop rendering
height = max_height
stop_rendering = True
break
else:
if last_footnotes_height not in footnote_area_heights:
# Footnotes have been rendered, try to re-render with the
# new footnote area height
height -= last_footnotes_height - footnote_area_heights[-1]
footnote_area_heights.append(last_footnotes_height)
continue
everything_fits = (
not column_skip_stack and
max(consumed_heights) <= max_height)
if everything_fits:
# Everything fits, start expanding columns at the average
# of the column heights
max_height -= last_footnotes_height
if (style['column_fill'] == 'balance' or
index < columns_and_blocks[-1][0]):
balancing = True
height = sum(consumed_heights) / count
else:
break
else:
# Content overflows even at maximum height, stop now and
# let the columns continue on the next page
height += footnote_area_heights[-1]
if len(footnote_area_heights) > 2:
last_footnotes_height = min(
last_footnotes_height, footnote_area_heights[-1])
height -= last_footnotes_height
stop_rendering = True
break
# TODO: check style['max']-height
bottom_space = max(
bottom_space, context.page_bottom - current_position_y - height)
# Replace the current box children with real columns
i = 0
max_column_height = 0
columns = []
while True:
column_box = _create_column_box(
box, containing_block, column_children, width,
current_position_y)
if style['direction'] == 'rtl':
column_box.position_x += box.width - (i + 1) * width - i * gap
else:
column_box.position_x += i * (width + gap)
new_child, column_skip_stack, column_next_page, _, _, _ = (
block_box_layout(
context, column_box, bottom_space, skip_stack,
containing_block, original_page_is_empty, absolute_boxes,
fixed_boxes, None, discard=False, max_lines=None))
if new_child is None:
break_page = True
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:
bottom_space = original_bottom_space
break
i += 1
if i == count and not height_defined:
# [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
# Update the current y position and set the columns height
current_position_y += min(max_height, max_column_height)
for column in columns:
column.height = max_column_height
new_children.append(column)
skip_stack = None
page_is_empty = False
if stop_rendering:
break
# Report footnotes above the defined footnotes height
_report_footnotes(context, last_footnotes_height)
if box.children and not new_children:
# The box has children but none can be drawn, let's skip the whole box
context.in_column = False
return None, (0, None), {'break': 'any', 'page': None}, [], False
# Set the height of the containing box
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
# Update the latest columns height to respect min-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 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)
# Calculate skip stack
if column_skip_stack:
skip, = column_skip_stack.keys()
skip_stack = {index + skip: column_skip_stack[skip]}
elif break_page:
skip_stack = {index: None}
# Update page bottom according to the new footnotes
if context.current_footnote_area.height != 'auto':
context.page_bottom += footnote_area_heights[0]
context.page_bottom -= context.current_footnote_area.margin_height()
context.in_column = False
return box, skip_stack, next_page, [], False
def _report_footnotes(context, footnotes_height):
"""Report footnotes above the defined footnotes height."""
if not context.current_page_footnotes:
return
# Report and count footnotes
reported_footnotes = 0
while context.current_footnote_area.margin_height() > footnotes_height:
context.report_footnote(context.current_page_footnotes[-1])
reported_footnotes += 1
# Revert reported footnotes, as theyve been reported starting from the
# last one
if reported_footnotes >= 2:
extra = context.reported_footnotes[-1:-reported_footnotes-1:-1]
context.reported_footnotes[-reported_footnotes:] = extra
def _create_column_box(box, containing_block, children, width, position_y):
"""Create a column box including given 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 = position_y
return column_box