feat: add comprehensive GitHub workflow and development tools
This commit is contained in:
384
app/.venv/Lib/site-packages/weasyprint/layout/__init__.py
Normal file
384
app/.venv/Lib/site-packages/weasyprint/layout/__init__.py
Normal file
@@ -0,0 +1,384 @@
|
||||
"""Transform a "before layout" box tree into an "after layout" tree.
|
||||
|
||||
Break boxes across lines and pages; determine the size and dimension of each
|
||||
box fragement.
|
||||
|
||||
Boxes in the new tree have *used values* in their ``position_x``,
|
||||
``position_y``, ``width`` and ``height`` attributes, amongst others.
|
||||
|
||||
See https://www.w3.org/TR/CSS21/cascade.html#used-value
|
||||
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from functools import partial
|
||||
from math import inf
|
||||
|
||||
from ..formatting_structure import boxes, build
|
||||
from ..logger import PROGRESS_LOGGER
|
||||
from .absolute import absolute_box_layout, absolute_layout
|
||||
from .background import layout_backgrounds
|
||||
from .block import block_level_layout
|
||||
from .page import make_all_pages, make_margin_boxes
|
||||
|
||||
|
||||
def initialize_page_maker(context, root_box):
|
||||
"""Initialize ``context.page_maker``.
|
||||
|
||||
Collect the pagination's states required for page based counters.
|
||||
|
||||
"""
|
||||
context.page_maker = []
|
||||
|
||||
# Special case the root box
|
||||
page_break = root_box.style['break_before']
|
||||
|
||||
# TODO: take care of text direction and writing mode
|
||||
# https://www.w3.org/TR/css-page-3/#progression
|
||||
if page_break == 'right':
|
||||
right_page = True
|
||||
elif page_break == 'left':
|
||||
right_page = False
|
||||
elif page_break == 'recto':
|
||||
right_page = root_box.style['direction'] == 'ltr'
|
||||
elif page_break == 'verso':
|
||||
right_page = root_box.style['direction'] == 'rtl'
|
||||
else:
|
||||
right_page = root_box.style['direction'] == 'ltr'
|
||||
resume_at = None
|
||||
next_page = {'break': 'any', 'page': root_box.page_values()[0]}
|
||||
|
||||
# page_state is prerequisite for filling in missing page based counters
|
||||
# although neither a variable quote_depth nor counter_scopes are needed
|
||||
# in page-boxes -- reusing
|
||||
# `formatting_structure.build.update_counters()` to avoid redundant
|
||||
# code requires a full `state`.
|
||||
# The value of **pages**, of course, is unknown until we return and
|
||||
# might change when 'content_changed' triggers re-pagination...
|
||||
# So we start with an empty state
|
||||
page_state = (
|
||||
# Shared mutable objects:
|
||||
[0], # quote_depth: single integer
|
||||
{'pages': [0]},
|
||||
[{'pages'}] # counter_scopes
|
||||
)
|
||||
|
||||
# Initial values
|
||||
remake_state = {
|
||||
'content_changed': False,
|
||||
'pages_wanted': False,
|
||||
'anchors': [], # first occurrence of anchor
|
||||
'content_lookups': [] # first occurr. of content-CounterLookupItem
|
||||
}
|
||||
context.page_maker.append((
|
||||
resume_at, next_page, right_page, page_state, remake_state))
|
||||
|
||||
|
||||
def layout_fixed_boxes(context, pages, containing_page):
|
||||
"""Lay out and yield fixed boxes of ``pages`` on ``containing_page``."""
|
||||
for page in pages:
|
||||
for box in page.fixed_boxes:
|
||||
# As replaced boxes are never copied during layout, ensure that we
|
||||
# have different boxes (with a possibly different layout) for
|
||||
# each pages.
|
||||
if isinstance(box, boxes.ReplacedBox):
|
||||
box = box.copy()
|
||||
# Absolute boxes in fixed boxes are rendered as fixed boxes'
|
||||
# children, even when they are fixed themselves.
|
||||
absolute_boxes = []
|
||||
absolute_box, _ = absolute_box_layout(
|
||||
context, box, containing_page, absolute_boxes,
|
||||
bottom_space=-inf, skip_stack=None)
|
||||
yield absolute_box
|
||||
while absolute_boxes:
|
||||
new_absolute_boxes = []
|
||||
for box in absolute_boxes:
|
||||
absolute_layout(
|
||||
context, box, containing_page, new_absolute_boxes,
|
||||
bottom_space=-inf, skip_stack=None)
|
||||
absolute_boxes = new_absolute_boxes
|
||||
|
||||
|
||||
def layout_document(html, root_box, context, max_loops=8):
|
||||
"""Lay out the whole document.
|
||||
|
||||
This includes line breaks, page breaks, absolute size and position for all
|
||||
boxes. Page based counters might require multiple passes.
|
||||
|
||||
:param root_box:
|
||||
Root of the box tree (formatting structure of the HTML). The page boxes
|
||||
are created from that tree, this structure is not lost during
|
||||
pagination.
|
||||
:returns:
|
||||
A list of laid out Page objects.
|
||||
|
||||
"""
|
||||
initialize_page_maker(context, root_box)
|
||||
pages = []
|
||||
original_footnotes = []
|
||||
actual_total_pages = 0
|
||||
|
||||
for loop in range(max_loops):
|
||||
if loop > 0:
|
||||
PROGRESS_LOGGER.info(
|
||||
'Step 5 - Creating layout - Repagination #%d', loop)
|
||||
context.footnotes = original_footnotes.copy()
|
||||
|
||||
initial_total_pages = actual_total_pages
|
||||
if loop == 0:
|
||||
original_footnotes = context.footnotes.copy()
|
||||
pages = list(make_all_pages(context, root_box, html, pages))
|
||||
actual_total_pages = len(pages)
|
||||
|
||||
# Check whether another round is required
|
||||
reloop_content = False
|
||||
reloop_pages = False
|
||||
for page_data in context.page_maker:
|
||||
# Update pages
|
||||
_, _, _, page_state, remake_state = page_data
|
||||
page_counter_values = page_state[1]
|
||||
page_counter_values['pages'] = [actual_total_pages]
|
||||
if remake_state['content_changed']:
|
||||
reloop_content = True
|
||||
if remake_state['pages_wanted']:
|
||||
reloop_pages = initial_total_pages != actual_total_pages
|
||||
|
||||
# No need for another loop, stop here
|
||||
if not reloop_content and not reloop_pages:
|
||||
break
|
||||
|
||||
# Calculate string-sets and bookmark-labels containing page based counters
|
||||
# when pagination is finished. No need to do that (maybe multiple times) in
|
||||
# make_page because they dont create boxes, only appear in MarginBoxes and
|
||||
# in the final PDF.
|
||||
# Prevent repetition of bookmarks (see #1145).
|
||||
|
||||
watch_elements = []
|
||||
watch_elements_before = []
|
||||
watch_elements_after = []
|
||||
for i, page in enumerate(pages):
|
||||
# We need the updated page_counter_values
|
||||
_, _, _, page_state, _ = context.page_maker[i + 1]
|
||||
page_counter_values = page_state[1]
|
||||
|
||||
for child in page.descendants():
|
||||
# Only one bookmark per original box
|
||||
if child.bookmark_label:
|
||||
if child.element_tag.endswith('::before'):
|
||||
checklist = watch_elements_before
|
||||
elif child.element_tag.endswith('::after'):
|
||||
checklist = watch_elements_after
|
||||
else:
|
||||
checklist = watch_elements
|
||||
if child.element in checklist:
|
||||
child.bookmark_label = ''
|
||||
else:
|
||||
checklist.append(child.element)
|
||||
|
||||
if child.missing_link:
|
||||
for (box, css_token), item in (
|
||||
context.target_collector.counter_lookup_items.items()):
|
||||
if child.missing_link == box and css_token != 'content':
|
||||
if (css_token == 'bookmark-label' and
|
||||
not child.bookmark_label):
|
||||
# don't refill it!
|
||||
continue
|
||||
item.parse_again(page_counter_values)
|
||||
# string_set is a pointer, but the bookmark_label is
|
||||
# just a string: copy it
|
||||
if css_token == 'bookmark-label':
|
||||
child.bookmark_label = box.bookmark_label
|
||||
# Collect the string_sets in the LayoutContext
|
||||
string_sets = child.string_set
|
||||
if string_sets and string_sets != 'none':
|
||||
for string_set in string_sets:
|
||||
string_name, text = string_set
|
||||
context.string_set[string_name][i+1].append(text)
|
||||
|
||||
# Add margin boxes
|
||||
for i, page in enumerate(pages):
|
||||
root_children = []
|
||||
root, footnote_area = page.children
|
||||
root_children.extend(layout_fixed_boxes(context, pages[:i], page))
|
||||
root_children.extend(root.children)
|
||||
root_children.extend(layout_fixed_boxes(context, pages[i + 1:], page))
|
||||
root.children = root_children
|
||||
context.current_page = i + 1 # page_number starts at 1
|
||||
|
||||
# page_maker's page_state is ready for the MarginBoxes
|
||||
state = context.page_maker[context.current_page][3]
|
||||
page.children = (root,)
|
||||
if footnote_area.children:
|
||||
page.children += (footnote_area,)
|
||||
page.children += tuple(make_margin_boxes(context, page, state))
|
||||
layout_backgrounds(page, context.get_image_from_uri)
|
||||
yield page
|
||||
|
||||
|
||||
class LayoutContext:
|
||||
def __init__(self, style_for, get_image_from_uri, font_config,
|
||||
counter_style, target_collector):
|
||||
self.style_for = style_for
|
||||
self.get_image_from_uri = partial(get_image_from_uri, context=self)
|
||||
self.font_config = font_config
|
||||
self.counter_style = counter_style
|
||||
self.target_collector = target_collector
|
||||
self._excluded_shapes_lists = []
|
||||
self.footnotes = []
|
||||
self.page_footnotes = {}
|
||||
self.current_page_footnotes = []
|
||||
self.reported_footnotes = []
|
||||
self.current_footnote_area = None # Not initialized yet
|
||||
self.excluded_shapes = None # Not initialized yet
|
||||
self.page_bottom = None
|
||||
self.string_set = defaultdict(lambda: defaultdict(lambda: []))
|
||||
self.running_elements = defaultdict(lambda: defaultdict(lambda: []))
|
||||
self.current_page = None
|
||||
self.forced_break = False
|
||||
self.broken_out_of_flow = {}
|
||||
self.in_column = False
|
||||
|
||||
# Cache
|
||||
self.strut_layouts = {}
|
||||
self.font_features = {}
|
||||
self.tables = {}
|
||||
self.dictionaries = {}
|
||||
|
||||
def overflows_page(self, bottom_space, position_y):
|
||||
return self.overflows(self.page_bottom - bottom_space, position_y)
|
||||
|
||||
@staticmethod
|
||||
def overflows(bottom, position_y):
|
||||
# Use a small fudge factor to avoid floating numbers errors.
|
||||
# The 1e-9 value comes from PEP 485.
|
||||
return position_y > bottom * (1 + 1e-9)
|
||||
|
||||
def create_block_formatting_context(self):
|
||||
self.excluded_shapes = []
|
||||
self._excluded_shapes_lists.append(self.excluded_shapes)
|
||||
|
||||
def finish_block_formatting_context(self, root_box):
|
||||
# See https://www.w3.org/TR/CSS2/visudet.html#root-height
|
||||
if root_box.style['height'] == 'auto' and self.excluded_shapes:
|
||||
box_bottom = root_box.content_box_y() + root_box.height
|
||||
max_shape_bottom = max([
|
||||
shape.position_y + shape.margin_height()
|
||||
for shape in self.excluded_shapes] + [box_bottom])
|
||||
root_box.height += max_shape_bottom - box_bottom
|
||||
self._excluded_shapes_lists.pop()
|
||||
if self._excluded_shapes_lists:
|
||||
self.excluded_shapes = self._excluded_shapes_lists[-1]
|
||||
else:
|
||||
self.excluded_shapes = None
|
||||
|
||||
def get_string_set_for(self, page, name, keyword='first'):
|
||||
"""Resolve value of string function."""
|
||||
return self.get_string_or_element_for(
|
||||
self.string_set, page, name, keyword)
|
||||
|
||||
def get_running_element_for(self, page, name, keyword='first'):
|
||||
"""Resolve value of element function."""
|
||||
return self.get_string_or_element_for(
|
||||
self.running_elements, page, name, keyword)
|
||||
|
||||
def get_string_or_element_for(self, store, page, name, keyword):
|
||||
"""Resolve value of string or element function.
|
||||
|
||||
We'll have something like this that represents all assignments on a
|
||||
given page:
|
||||
|
||||
{1: ['First Header'], 3: ['Second Header'],
|
||||
4: ['Third Header', '3.5th Header']}
|
||||
|
||||
Value depends on current page.
|
||||
https://drafts.csswg.org/css-gcpm/#funcdef-string
|
||||
|
||||
:param dict store:
|
||||
Dictionary where the resolved value is stored.
|
||||
:param page:
|
||||
Current page.
|
||||
:param str name:
|
||||
Name of the named string or running element.
|
||||
:param str keyword:
|
||||
Indicates which value of the named string or running element to
|
||||
use. Default is the first assignment on the current page else the
|
||||
most recent assignment.
|
||||
:returns:
|
||||
Text for string set, box for running element.
|
||||
|
||||
"""
|
||||
if self.current_page in store[name]:
|
||||
# A value was assigned on this page
|
||||
first_string = store[name][self.current_page][0]
|
||||
last_string = store[name][self.current_page][-1]
|
||||
if keyword == 'first':
|
||||
return first_string
|
||||
elif keyword == 'start':
|
||||
element = page
|
||||
while element:
|
||||
if element.style['string_set'] != 'none':
|
||||
for (string_name, _) in element.style['string_set']:
|
||||
if string_name == name:
|
||||
return first_string
|
||||
if element.children:
|
||||
element = element.children[0]
|
||||
continue
|
||||
break
|
||||
elif keyword == 'last':
|
||||
return last_string
|
||||
elif keyword == 'first-except':
|
||||
return
|
||||
# Search backwards through previous pages
|
||||
for previous_page in range(self.current_page - 1, 0, -1):
|
||||
if previous_page in store[name]:
|
||||
return store[name][previous_page][-1]
|
||||
|
||||
def layout_footnote(self, footnote):
|
||||
"""Add a footnote to the layout for this page."""
|
||||
self.footnotes.remove(footnote)
|
||||
self.current_page_footnotes.append(footnote)
|
||||
return self._update_footnote_area()
|
||||
|
||||
def unlayout_footnote(self, footnote):
|
||||
"""Remove a footnote from the layout and return it to the waitlist."""
|
||||
# TODO: Handle unlayouting a footnote that hasn't been laid out yet or
|
||||
# has already been unlayouted
|
||||
if footnote not in self.footnotes:
|
||||
self.footnotes.append(footnote)
|
||||
if footnote in self.current_page_footnotes:
|
||||
self.current_page_footnotes.remove(footnote)
|
||||
elif footnote in self.reported_footnotes:
|
||||
self.reported_footnotes.remove(footnote)
|
||||
self._update_footnote_area()
|
||||
|
||||
def report_footnote(self, footnote):
|
||||
"""Mark a footnote as being moved to the next page."""
|
||||
self.current_page_footnotes.remove(footnote)
|
||||
self.reported_footnotes.append(footnote)
|
||||
self._update_footnote_area()
|
||||
|
||||
def _update_footnote_area(self):
|
||||
"""Update the page bottom size and our footnote area height."""
|
||||
if self.current_footnote_area.height != 'auto' and not self.in_column:
|
||||
self.page_bottom += self.current_footnote_area.margin_height()
|
||||
self.current_footnote_area.children = self.current_page_footnotes
|
||||
if self.current_footnote_area.children:
|
||||
footnote_area = build.create_anonymous_boxes(
|
||||
self.current_footnote_area.deepcopy())
|
||||
footnote_area = block_level_layout(
|
||||
self, footnote_area, -inf, None,
|
||||
self.current_footnote_area.page)[0]
|
||||
self.current_footnote_area.height = footnote_area.height
|
||||
if not self.in_column:
|
||||
self.page_bottom -= footnote_area.margin_height()
|
||||
last_child = footnote_area.children[-1]
|
||||
overflow = (
|
||||
last_child.position_y + last_child.margin_height() >
|
||||
footnote_area.position_y + footnote_area.margin_height() -
|
||||
footnote_area.margin_bottom)
|
||||
return overflow
|
||||
else:
|
||||
self.current_footnote_area.height = 0
|
||||
if not self.in_column:
|
||||
self.page_bottom -= self.current_footnote_area.margin_height()
|
||||
return False
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
344
app/.venv/Lib/site-packages/weasyprint/layout/absolute.py
Normal file
344
app/.venv/Lib/site-packages/weasyprint/layout/absolute.py
Normal file
@@ -0,0 +1,344 @@
|
||||
"""Absolutely positioned boxes management."""
|
||||
|
||||
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
|
||||
|
||||
|
||||
class AbsolutePlaceholder:
|
||||
"""Left where an absolutely-positioned box was taken out of the flow."""
|
||||
def __init__(self, box):
|
||||
assert not isinstance(box, AbsolutePlaceholder)
|
||||
# Work around the overloaded __setattr__
|
||||
object.__setattr__(self, '_box', box)
|
||||
object.__setattr__(self, '_layout_done', False)
|
||||
|
||||
def set_laid_out_box(self, new_box):
|
||||
object.__setattr__(self, '_box', new_box)
|
||||
object.__setattr__(self, '_layout_done', True)
|
||||
|
||||
def translate(self, dx=0, dy=0, ignore_floats=False):
|
||||
if dx == dy == 0:
|
||||
return
|
||||
if self._layout_done:
|
||||
self._box.translate(dx, dy, ignore_floats)
|
||||
else:
|
||||
# Descendants do not have a position yet.
|
||||
self._box.position_x += dx
|
||||
self._box.position_y += dy
|
||||
|
||||
def copy(self):
|
||||
new_placeholder = AbsolutePlaceholder(self._box.copy())
|
||||
object.__setattr__(new_placeholder, '_layout_done', self._layout_done)
|
||||
return new_placeholder
|
||||
|
||||
# Pretend to be the box itself
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._box, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
setattr(self._box, name, value)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Placeholder %r>' % self._box
|
||||
|
||||
|
||||
@handle_min_max_width
|
||||
def absolute_width(box, context, cb_x, cb_y, cb_width, cb_height):
|
||||
# https://www.w3.org/TR/CSS2/visudet.html#abs-replaced-width
|
||||
ltr = (
|
||||
box.style.parent_style is None or
|
||||
box.style.parent_style['direction'] == 'ltr')
|
||||
paddings_borders = (
|
||||
box.padding_left + box.padding_right +
|
||||
box.border_left_width + box.border_right_width)
|
||||
translate_x = 0
|
||||
translate_box_width = False
|
||||
default_translate_x = cb_x - box.position_x
|
||||
|
||||
if box.left == box.right == box.width == 'auto':
|
||||
if box.margin_left == 'auto':
|
||||
box.margin_left = 0
|
||||
if box.margin_right == 'auto':
|
||||
box.margin_right = 0
|
||||
available_width = cb_width - (
|
||||
paddings_borders + box.margin_left + box.margin_right)
|
||||
box.width = shrink_to_fit(context, box, available_width)
|
||||
if not ltr:
|
||||
translate_box_width = True
|
||||
translate_x = default_translate_x + available_width
|
||||
elif box.left != 'auto' and box.right != 'auto' and box.width != 'auto':
|
||||
width_for_margins = cb_width - (
|
||||
box.right + box.left + box.width + paddings_borders)
|
||||
if box.margin_left == box.margin_right == 'auto':
|
||||
if box.width + paddings_borders + box.right + box.left <= cb_width:
|
||||
box.margin_left = box.margin_right = width_for_margins / 2
|
||||
else:
|
||||
box.margin_left = 0 if ltr else width_for_margins
|
||||
box.margin_right = width_for_margins if ltr else 0
|
||||
elif box.margin_left == 'auto':
|
||||
box.margin_left = width_for_margins
|
||||
elif box.margin_right == 'auto':
|
||||
box.margin_right = width_for_margins
|
||||
elif ltr:
|
||||
box.margin_right = width_for_margins
|
||||
else:
|
||||
box.margin_left = width_for_margins
|
||||
translate_x = box.left + default_translate_x
|
||||
else:
|
||||
if box.margin_left == 'auto':
|
||||
box.margin_left = 0
|
||||
if box.margin_right == 'auto':
|
||||
box.margin_right = 0
|
||||
spacing = paddings_borders + box.margin_left + box.margin_right
|
||||
if box.left == box.width == 'auto':
|
||||
box.width = shrink_to_fit(
|
||||
context, box, cb_width - spacing - box.right)
|
||||
translate_x = cb_width - box.right - spacing + default_translate_x
|
||||
translate_box_width = True
|
||||
elif box.left == box.right == 'auto':
|
||||
if not ltr:
|
||||
available_width = cb_width - (
|
||||
paddings_borders + box.margin_left + box.margin_right)
|
||||
translate_box_width = True
|
||||
translate_x = default_translate_x + available_width
|
||||
elif box.width == box.right == 'auto':
|
||||
box.width = shrink_to_fit(
|
||||
context, box, cb_width - spacing - box.left)
|
||||
translate_x = box.left + default_translate_x
|
||||
elif box.left == 'auto':
|
||||
translate_x = cb_width + default_translate_x - (
|
||||
box.right + spacing + box.width)
|
||||
elif box.width == 'auto':
|
||||
box.width = cb_width - box.right - box.left - spacing
|
||||
translate_x = box.left + default_translate_x
|
||||
elif box.right == 'auto':
|
||||
translate_x = box.left + default_translate_x
|
||||
|
||||
return translate_box_width, translate_x
|
||||
|
||||
|
||||
def absolute_height(box, context, cb_x, cb_y, cb_width, cb_height):
|
||||
# https://www.w3.org/TR/CSS2/visudet.html#abs-non-replaced-height
|
||||
paddings_borders = (
|
||||
box.padding_top + box.padding_bottom +
|
||||
box.border_top_width + box.border_bottom_width)
|
||||
translate_y = 0
|
||||
translate_box_height = False
|
||||
default_translate_y = cb_y - box.position_y
|
||||
|
||||
if box.top == box.bottom == box.height == 'auto':
|
||||
# Keep the static position
|
||||
if box.margin_top == 'auto':
|
||||
box.margin_top = 0
|
||||
if box.margin_bottom == 'auto':
|
||||
box.margin_bottom = 0
|
||||
elif 'auto' not in (box.top, box.bottom, box.height):
|
||||
height_for_margins = cb_height - (
|
||||
box.top + box.bottom + box.height + paddings_borders)
|
||||
if box.margin_top == box.margin_bottom == 'auto':
|
||||
box.margin_top = box.margin_bottom = height_for_margins / 2
|
||||
elif box.margin_top == 'auto':
|
||||
box.margin_top = height_for_margins
|
||||
elif box.margin_bottom == 'auto':
|
||||
box.margin_bottom = height_for_margins
|
||||
else:
|
||||
box.margin_bottom = height_for_margins
|
||||
translate_y = box.top + default_translate_y
|
||||
else:
|
||||
if box.margin_top == 'auto':
|
||||
box.margin_top = 0
|
||||
if box.margin_bottom == 'auto':
|
||||
box.margin_bottom = 0
|
||||
spacing = paddings_borders + box.margin_top + box.margin_bottom
|
||||
if box.top == box.height == 'auto':
|
||||
translate_y = (
|
||||
cb_height - box.bottom - spacing + default_translate_y)
|
||||
translate_box_height = True
|
||||
elif box.top == box.bottom == 'auto':
|
||||
pass # Keep the static position
|
||||
elif box.height == box.bottom == 'auto':
|
||||
translate_y = box.top + default_translate_y
|
||||
elif box.top == 'auto':
|
||||
translate_y = cb_height + default_translate_y - (
|
||||
box.bottom + spacing + box.height)
|
||||
elif box.height == 'auto':
|
||||
box.height = cb_height - box.bottom - box.top - spacing
|
||||
translate_y = box.top + default_translate_y
|
||||
elif box.bottom == 'auto':
|
||||
translate_y = box.top + default_translate_y
|
||||
|
||||
return translate_box_height, translate_y
|
||||
|
||||
|
||||
def absolute_block(context, box, containing_block, fixed_boxes, bottom_space,
|
||||
skip_stack, cb_x, cb_y, cb_width, cb_height):
|
||||
from .block import block_container_layout
|
||||
from .flex import flex_layout
|
||||
from .grid import grid_layout
|
||||
|
||||
translate_box_width, translate_x = absolute_width(
|
||||
box, context, cb_x, cb_y, cb_width, cb_height)
|
||||
if skip_stack:
|
||||
translate_box_height, translate_y = False, 0
|
||||
else:
|
||||
translate_box_height, translate_y = absolute_height(
|
||||
box, context, cb_x, cb_y, cb_width, cb_height)
|
||||
|
||||
bottom_space += -box.position_y if translate_box_height else translate_y
|
||||
|
||||
# This box is the containing block for absolute descendants.
|
||||
absolute_boxes = []
|
||||
|
||||
if box.is_table_wrapper:
|
||||
table_wrapper_width(context, box, (cb_width, cb_height))
|
||||
|
||||
if isinstance(box, (boxes.BlockBox)):
|
||||
new_box, resume_at, _, _, _, _ = block_container_layout(
|
||||
context, box, bottom_space, skip_stack, page_is_empty=True,
|
||||
absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes,
|
||||
adjoining_margins=None, discard=False, max_lines=None)
|
||||
elif isinstance(box, (boxes.FlexContainerBox)):
|
||||
new_box, resume_at, _, _, _ = flex_layout(
|
||||
context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty=True, absolute_boxes=absolute_boxes,
|
||||
fixed_boxes=fixed_boxes)
|
||||
elif isinstance(box, (boxes.GridContainerBox)):
|
||||
new_box, resume_at, _, _, _ = grid_layout(
|
||||
context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty=True, absolute_boxes=absolute_boxes,
|
||||
fixed_boxes=fixed_boxes)
|
||||
|
||||
for child_placeholder in absolute_boxes:
|
||||
absolute_layout(
|
||||
context, child_placeholder, new_box, fixed_boxes, bottom_space,
|
||||
skip_stack=None)
|
||||
|
||||
if translate_box_width:
|
||||
translate_x -= new_box.width
|
||||
if translate_box_height:
|
||||
translate_y -= new_box.height
|
||||
new_box.translate(translate_x, translate_y)
|
||||
|
||||
return new_box, resume_at
|
||||
|
||||
|
||||
def absolute_layout(context, placeholder, containing_block, fixed_boxes,
|
||||
bottom_space, skip_stack):
|
||||
"""Set the width of absolute positioned ``box``."""
|
||||
assert not placeholder._layout_done
|
||||
box = placeholder._box
|
||||
new_box, resume_at = absolute_box_layout(
|
||||
context, box, containing_block, fixed_boxes, bottom_space, skip_stack)
|
||||
placeholder.set_laid_out_box(new_box)
|
||||
if resume_at:
|
||||
context.broken_out_of_flow[placeholder] = (
|
||||
box, containing_block, resume_at)
|
||||
|
||||
|
||||
def absolute_box_layout(context, box, containing_block, fixed_boxes,
|
||||
bottom_space, skip_stack):
|
||||
# TODO: handle inline boxes (point 10.1.4.1)
|
||||
# https://www.w3.org/TR/CSS2/visudet.html#containing-block-details
|
||||
if isinstance(containing_block, boxes.PageBox):
|
||||
cb_x = containing_block.content_box_x()
|
||||
cb_y = containing_block.content_box_y()
|
||||
cb_width = containing_block.width
|
||||
cb_height = containing_block.height
|
||||
else:
|
||||
cb_x = containing_block.padding_box_x()
|
||||
cb_y = containing_block.padding_box_y()
|
||||
cb_width = containing_block.padding_width()
|
||||
cb_height = containing_block.padding_height()
|
||||
|
||||
resolve_percentages(box, (cb_width, cb_height))
|
||||
resolve_position_percentages(box, (cb_width, cb_height))
|
||||
|
||||
context.create_block_formatting_context()
|
||||
if isinstance(box, boxes.BlockReplacedBox):
|
||||
new_box = absolute_replaced(
|
||||
context, box, cb_x, cb_y, cb_width, cb_height)
|
||||
resume_at = None
|
||||
else:
|
||||
# Absolute tables are wrapped into block boxes
|
||||
new_box, resume_at = absolute_block(
|
||||
context, box, containing_block, fixed_boxes, bottom_space,
|
||||
skip_stack, cb_x, cb_y, cb_width, cb_height)
|
||||
context.finish_block_formatting_context(new_box)
|
||||
return new_box, resume_at
|
||||
|
||||
|
||||
def absolute_replaced(context, box, cb_x, cb_y, cb_width, cb_height):
|
||||
inline_replaced_box_width_height(box, (cb_x, cb_y, cb_width, cb_height))
|
||||
ltr = (
|
||||
box.style.parent_style is None or
|
||||
box.style.parent_style['direction'] == 'ltr')
|
||||
|
||||
# https://www.w3.org/TR/CSS21/visudet.html#abs-replaced-width
|
||||
if box.left == box.right == 'auto':
|
||||
# static position:
|
||||
if ltr:
|
||||
box.left = box.position_x - cb_x
|
||||
else:
|
||||
box.right = cb_x + cb_width - box.position_x
|
||||
if 'auto' in (box.left, box.right):
|
||||
if box.margin_left == 'auto':
|
||||
box.margin_left = 0
|
||||
if box.margin_right == 'auto':
|
||||
box.margin_right = 0
|
||||
remaining = cb_width - box.margin_width()
|
||||
if box.left == 'auto':
|
||||
box.left = remaining - box.right
|
||||
if box.right == 'auto':
|
||||
box.right = remaining - box.left
|
||||
elif 'auto' in (box.margin_left, box.margin_right):
|
||||
remaining = cb_width - (box.border_width() + box.left + box.right)
|
||||
if box.margin_left == box.margin_right == 'auto':
|
||||
if remaining >= 0:
|
||||
box.margin_left = box.margin_right = remaining // 2
|
||||
else:
|
||||
box.margin_left = 0 if ltr else remaining
|
||||
box.margin_right = remaining if ltr else 0
|
||||
elif box.margin_left == 'auto':
|
||||
box.margin_left = remaining
|
||||
else:
|
||||
box.margin_right = remaining
|
||||
else:
|
||||
# Over-constrained
|
||||
if ltr:
|
||||
box.right = cb_width - (box.margin_width() + box.left)
|
||||
else:
|
||||
box.left = cb_width - (box.margin_width() + box.right)
|
||||
|
||||
# https://www.w3.org/TR/CSS21/visudet.html#abs-replaced-height
|
||||
if box.top == box.bottom == 'auto':
|
||||
box.top = box.position_y - cb_y
|
||||
if 'auto' in (box.top, box.bottom):
|
||||
if box.margin_top == 'auto':
|
||||
box.margin_top = 0
|
||||
if box.margin_bottom == 'auto':
|
||||
box.margin_bottom = 0
|
||||
remaining = cb_height - box.margin_height()
|
||||
if box.top == 'auto':
|
||||
box.top = remaining - box.bottom
|
||||
if box.bottom == 'auto':
|
||||
box.bottom = remaining - box.top
|
||||
elif 'auto' in (box.margin_top, box.margin_bottom):
|
||||
remaining = cb_height - (box.border_height() + box.top + box.bottom)
|
||||
if box.margin_top == box.margin_bottom == 'auto':
|
||||
box.margin_top = box.margin_bottom = remaining // 2
|
||||
elif box.margin_top == 'auto':
|
||||
box.margin_top = remaining
|
||||
else:
|
||||
box.margin_bottom = remaining
|
||||
else:
|
||||
# Over-constrained
|
||||
box.bottom = cb_height - (box.margin_height() + box.top)
|
||||
|
||||
# No children for replaced boxes, no need to .translate()
|
||||
box.position_x = cb_x + box.left
|
||||
box.position_y = cb_y + box.top
|
||||
return box
|
||||
248
app/.venv/Lib/site-packages/weasyprint/layout/background.py
Normal file
248
app/.venv/Lib/site-packages/weasyprint/layout/background.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""Manage background position and size."""
|
||||
|
||||
from collections import namedtuple
|
||||
from itertools import cycle
|
||||
|
||||
from tinycss2.color3 import parse_color
|
||||
|
||||
from ..formatting_structure import boxes
|
||||
from . import replaced
|
||||
from .percent import percentage, resolve_radii_percentages
|
||||
|
||||
Background = namedtuple('Background', 'color, layers, image_rendering')
|
||||
BackgroundLayer = namedtuple(
|
||||
'BackgroundLayer',
|
||||
'image, size, position, repeat, unbounded, '
|
||||
'painting_area, positioning_area, clipped_boxes')
|
||||
|
||||
|
||||
def box_rectangle(box, which_rectangle):
|
||||
if which_rectangle == 'border-box':
|
||||
return (
|
||||
box.border_box_x(), box.border_box_y(),
|
||||
box.border_width(), box.border_height())
|
||||
elif which_rectangle == 'padding-box':
|
||||
return (
|
||||
box.padding_box_x(), box.padding_box_y(),
|
||||
box.padding_width(), box.padding_height())
|
||||
else:
|
||||
assert which_rectangle == 'content-box', which_rectangle
|
||||
return (
|
||||
box.content_box_x(), box.content_box_y(),
|
||||
box.width, box.height)
|
||||
|
||||
|
||||
def layout_box_backgrounds(page, box, get_image_from_uri, layout_children=True,
|
||||
style=None):
|
||||
"""Fetch and position background images."""
|
||||
from ..draw import get_color
|
||||
|
||||
# Resolve percentages in border-radius properties
|
||||
resolve_radii_percentages(box)
|
||||
|
||||
if layout_children:
|
||||
for child in box.all_children():
|
||||
layout_box_backgrounds(page, child, get_image_from_uri)
|
||||
|
||||
if style is None:
|
||||
style = box.style
|
||||
|
||||
# This is for the border image, not the background, but this is a
|
||||
# convenient place to get the image.
|
||||
if style['border_image_source'][0] != 'none':
|
||||
type_, value = style['border_image_source']
|
||||
if type_ == 'url':
|
||||
box.border_image = get_image_from_uri(url=value)
|
||||
else:
|
||||
box.border_image = value
|
||||
|
||||
if style['visibility'] == 'hidden':
|
||||
images = []
|
||||
color = parse_color('transparent')
|
||||
else:
|
||||
orientation = style['image_orientation']
|
||||
images = [
|
||||
get_image_from_uri(url=value, orientation=orientation)
|
||||
if type_ == 'url' else value
|
||||
for type_, value in style['background_image']]
|
||||
color = get_color(style, 'background_color')
|
||||
|
||||
if color.alpha == 0 and not any(images):
|
||||
if box != page: # Pages need a background for bleed box
|
||||
box.background = None
|
||||
return
|
||||
|
||||
layers = [
|
||||
layout_background_layer(box, page, style['image_resolution'], *layer)
|
||||
for layer in zip(images, *map(cycle, [
|
||||
style['background_size'],
|
||||
style['background_clip'],
|
||||
style['background_repeat'],
|
||||
style['background_origin'],
|
||||
style['background_position'],
|
||||
style['background_attachment']]))]
|
||||
box.background = Background(color, layers, style['image_rendering'])
|
||||
|
||||
|
||||
def layout_background_layer(box, page, resolution, image, size, clip, repeat,
|
||||
origin, position, attachment):
|
||||
|
||||
# TODO: respect box-sizing for table cells?
|
||||
clipped_boxes = []
|
||||
painting_area = 0, 0, 0, 0
|
||||
if box is page:
|
||||
# [The page’s] background painting area is the bleed area […]
|
||||
# regardless of background-clip.
|
||||
# https://drafts.csswg.org/css-page-3/#painting
|
||||
painting_area = page.bleed_area
|
||||
clipped_boxes = []
|
||||
elif isinstance(box, boxes.TableRowGroupBox):
|
||||
clipped_boxes = []
|
||||
total_height = 0
|
||||
for row in box.children:
|
||||
if row.children:
|
||||
clipped_boxes += [
|
||||
cell.rounded_border_box() for cell in row.children]
|
||||
total_height = max(total_height, max(
|
||||
cell.border_height() for cell in row.children))
|
||||
painting_area = [
|
||||
box.border_box_x(), box.border_box_y(),
|
||||
box.border_width(), total_height]
|
||||
elif isinstance(box, boxes.TableRowBox):
|
||||
if box.children:
|
||||
clipped_boxes = [
|
||||
cell.rounded_border_box() for cell in box.children]
|
||||
height = max(cell.border_height() for cell in box.children)
|
||||
painting_area = [
|
||||
box.border_box_x(), box.border_box_y(),
|
||||
box.border_width(), height]
|
||||
elif isinstance(box, (boxes.TableColumnGroupBox, boxes.TableColumnBox)):
|
||||
cells = box.get_cells()
|
||||
if cells:
|
||||
clipped_boxes = [cell.rounded_border_box() for cell in cells]
|
||||
min_x = min(cell.border_box_x() for cell in cells)
|
||||
max_x = max(
|
||||
cell.border_box_x() + cell.border_width() for cell in cells)
|
||||
painting_area = [
|
||||
min_x, box.border_box_y(), max_x - min_x, box.border_height()]
|
||||
else:
|
||||
painting_area = box_rectangle(box, clip)
|
||||
if clip == 'border-box':
|
||||
clipped_boxes = [box.rounded_border_box()]
|
||||
elif clip == 'padding-box':
|
||||
clipped_boxes = [box.rounded_padding_box()]
|
||||
else:
|
||||
assert clip == 'content-box', clip
|
||||
clipped_boxes = [box.rounded_content_box()]
|
||||
|
||||
if image is not None:
|
||||
intrinsic_width, intrinsic_height, ratio = image.get_intrinsic_size(
|
||||
resolution, box.style['font_size'])
|
||||
if image is None or 0 in (intrinsic_width, intrinsic_height):
|
||||
return BackgroundLayer(
|
||||
image=None, unbounded=False, painting_area=painting_area,
|
||||
size='unused', position='unused', repeat='unused',
|
||||
positioning_area='unused', clipped_boxes=clipped_boxes)
|
||||
|
||||
if attachment == 'fixed':
|
||||
# Initial containing block
|
||||
if isinstance(box, boxes.PageBox):
|
||||
# […] if background-attachment is fixed then the image is
|
||||
# positioned relative to the page box including its margins […].
|
||||
# https://drafts.csswg.org/css-page/#painting
|
||||
positioning_area = (0, 0, box.margin_width(), box.margin_height())
|
||||
else:
|
||||
positioning_area = box_rectangle(page, 'content-box')
|
||||
else:
|
||||
positioning_area = box_rectangle(box, origin)
|
||||
|
||||
positioning_x, positioning_y, positioning_width, positioning_height = (
|
||||
positioning_area)
|
||||
painting_x, painting_y, painting_width, painting_height = painting_area
|
||||
|
||||
if size == 'cover':
|
||||
image_width, image_height = replaced.cover_constraint_image_sizing(
|
||||
positioning_width, positioning_height, ratio)
|
||||
elif size == 'contain':
|
||||
image_width, image_height = replaced.contain_constraint_image_sizing(
|
||||
positioning_width, positioning_height, ratio)
|
||||
else:
|
||||
size_width, size_height = size
|
||||
image_width, image_height = replaced.default_image_sizing(
|
||||
intrinsic_width, intrinsic_height, ratio,
|
||||
percentage(size_width, positioning_width),
|
||||
percentage(size_height, positioning_height),
|
||||
positioning_width, positioning_height)
|
||||
|
||||
origin_x, position_x, origin_y, position_y = position
|
||||
ref_x = positioning_width - image_width
|
||||
ref_y = positioning_height - image_height
|
||||
position_x = percentage(position_x, ref_x)
|
||||
position_y = percentage(position_y, ref_y)
|
||||
if origin_x == 'right':
|
||||
position_x = ref_x - position_x
|
||||
if origin_y == 'bottom':
|
||||
position_y = ref_y - position_y
|
||||
|
||||
repeat_x, repeat_y = repeat
|
||||
|
||||
if repeat_x == 'round':
|
||||
n_repeats = max(1, round(positioning_width / image_width))
|
||||
new_width = positioning_width / n_repeats
|
||||
position_x = 0 # Ignore background-position for this dimension
|
||||
if repeat_y != 'round' and size[1] == 'auto':
|
||||
image_height *= new_width / image_width
|
||||
image_width = new_width
|
||||
if repeat_y == 'round':
|
||||
n_repeats = max(1, round(positioning_height / image_height))
|
||||
new_height = positioning_height / n_repeats
|
||||
position_y = 0 # Ignore background-position for this dimension
|
||||
if repeat_x != 'round' and size[0] == 'auto':
|
||||
image_width *= new_height / image_height
|
||||
image_height = new_height
|
||||
|
||||
return BackgroundLayer(
|
||||
image=image,
|
||||
size=(image_width, image_height),
|
||||
position=(position_x, position_y),
|
||||
repeat=repeat,
|
||||
unbounded=False,
|
||||
painting_area=painting_area,
|
||||
positioning_area=positioning_area,
|
||||
clipped_boxes=clipped_boxes)
|
||||
|
||||
|
||||
def layout_backgrounds(page, get_image_from_uri):
|
||||
"""Layout backgrounds on the page box and on its children.
|
||||
|
||||
This function takes care of the canvas background, taken from the root
|
||||
elememt or a <body> child of the root element.
|
||||
|
||||
See https://www.w3.org/TR/CSS21/colors.html#background
|
||||
|
||||
"""
|
||||
layout_box_backgrounds(page, page, get_image_from_uri)
|
||||
assert not isinstance(page.children[0], boxes.MarginBox)
|
||||
root_box = page.children[0]
|
||||
chosen_box = root_box
|
||||
if root_box.element_tag.lower() == 'html' and root_box.background is None:
|
||||
for child in root_box.children:
|
||||
if child.element_tag.lower() == 'body':
|
||||
chosen_box = child
|
||||
break
|
||||
|
||||
if chosen_box.background:
|
||||
painting_area = box_rectangle(page, 'border-box')
|
||||
original_background = page.background
|
||||
layout_box_backgrounds(
|
||||
page, page, get_image_from_uri, layout_children=False,
|
||||
style=chosen_box.style)
|
||||
page.canvas_background = page.background._replace(
|
||||
# TODO: background-clip should be updated
|
||||
layers=[
|
||||
layer._replace(painting_area=painting_area)
|
||||
for layer in page.background.layers])
|
||||
page.background = original_background
|
||||
chosen_box.background = None
|
||||
else:
|
||||
page.canvas_background = None
|
||||
1091
app/.venv/Lib/site-packages/weasyprint/layout/block.py
Normal file
1091
app/.venv/Lib/site-packages/weasyprint/layout/block.py
Normal file
File diff suppressed because it is too large
Load Diff
417
app/.venv/Lib/site-packages/weasyprint/layout/column.py
Normal file
417
app/.venv/Lib/site-packages/weasyprint/layout/column.py
Normal file
@@ -0,0 +1,417 @@
|
||||
"""Layout for columns."""
|
||||
|
||||
from math import floor, inf
|
||||
|
||||
from .absolute import absolute_layout
|
||||
from .percent import percentage, 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 ( # isort:skip
|
||||
block_box_layout, block_level_layout, block_level_width,
|
||||
collapse_margin, remove_placeholders)
|
||||
|
||||
style = box.style
|
||||
width = style['column_width']
|
||||
count = style['column_count']
|
||||
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)
|
||||
|
||||
if style['column_gap'] == 'normal':
|
||||
# 1em because in column context
|
||||
gap = style['font_size']
|
||||
else:
|
||||
gap = percentage(style['column_gap'], box.width)
|
||||
|
||||
# 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 = max(
|
||||
1, 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 doesn’t 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:
|
||||
columns = []
|
||||
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 they’ve 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
|
||||
898
app/.venv/Lib/site-packages/weasyprint/layout/flex.py
Normal file
898
app/.venv/Lib/site-packages/weasyprint/layout/flex.py
Normal file
@@ -0,0 +1,898 @@
|
||||
"""Layout for flex containers and flex-items."""
|
||||
|
||||
import sys
|
||||
from math import inf, 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
|
||||
children = sorted(children, key=lambda item: item.style['order'])
|
||||
if skip_stack is not None:
|
||||
(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 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(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 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(inf, 'px')
|
||||
new_child = block.block_level_layout(
|
||||
context, new_child, -inf, child_skip_stack, parent_box,
|
||||
page_is_empty)[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 = inf
|
||||
new_child = block.block_level_layout(
|
||||
context, new_child, -inf, child_skip_stack, parent_box,
|
||||
page_is_empty, absolute_boxes, fixed_boxes)[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, -inf, child_skip_stack, parent_box,
|
||||
page_is_empty, absolute_boxes, fixed_boxes)[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' and box.style['height'].unit != '%':
|
||||
box.height = box.style['height'].value
|
||||
elif box.style['height'] != 'auto' and containing_block.height != 'auto':
|
||||
box.height = box.style['height'].value / 100 * containing_block.height
|
||||
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(children):
|
||||
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 == inf:
|
||||
initial_free_space = sys.maxsize
|
||||
if remaining_free_space == inf:
|
||||
remaining_free_space = sys.maxsize
|
||||
|
||||
initial_magnitude = (
|
||||
int(log10(initial_free_space)) if initial_free_space > 0
|
||||
else -inf)
|
||||
remaining_magnitude = (
|
||||
int(log10(remaining_free_space)) if remaining_free_space > 0
|
||||
else -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, -inf, child_skip_stack, parent_box,
|
||||
page_is_empty, absolute_boxes, fixed_boxes,
|
||||
[], False, None))
|
||||
|
||||
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
|
||||
'baseline' in align_self 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 = -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 = -inf
|
||||
max_cross_size = getattr(box, f'max_{cross}')
|
||||
if max_cross_size == 'auto':
|
||||
max_cross_size = inf
|
||||
line.cross_size = max(
|
||||
min_cross_size, min(line.cross_size, max_cross_size))
|
||||
|
||||
# Step 9
|
||||
align_content = box.style['align_content']
|
||||
if 'normal' in align_content:
|
||||
align_content = ('stretch',)
|
||||
if 'stretch' in align_content:
|
||||
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
|
||||
align_items = box.style['align_items']
|
||||
if 'normal' in align_items:
|
||||
align_items = ('stretch',)
|
||||
for line in flex_lines:
|
||||
for i, child in line:
|
||||
align_self = child.style['align_self']
|
||||
if 'normal' in align_self:
|
||||
align_self = ('stretch',)
|
||||
elif 'auto' in align_self:
|
||||
align_self = align_items
|
||||
if 'stretch' in align_self 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 'normal' in justify_content:
|
||||
justify_content = ('flex-start',)
|
||||
if box.style['flex_direction'].endswith('-reverse'):
|
||||
if 'flex-start' in justify_content:
|
||||
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 {'end', 'flex-end', 'right'} & set(justify_content):
|
||||
position_axis += free_space
|
||||
elif 'center' in justify_content:
|
||||
position_axis += free_space / 2
|
||||
elif 'space-around' in justify_content:
|
||||
position_axis += free_space / len(line) / 2
|
||||
elif 'space-evenly' in justify_content:
|
||||
position_axis += free_space / (len(line) + 1)
|
||||
|
||||
for i, child in line:
|
||||
if axis == 'width':
|
||||
child.position_x = position_axis
|
||||
if 'stretch' in justify_content:
|
||||
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 'space-around' in justify_content:
|
||||
position_axis += free_space / len(line)
|
||||
elif 'space-between' in justify_content:
|
||||
if len(line) > 1:
|
||||
position_axis += free_space / (len(line) - 1)
|
||||
elif 'space-evenly' in justify_content:
|
||||
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 = -inf
|
||||
# TODO: don't duplicate this loop
|
||||
for i, child in line:
|
||||
align_self = child.style['align_self']
|
||||
if 'auto' in align_self:
|
||||
align_self = align_items
|
||||
if 'baseline' in align_self and axis == 'width':
|
||||
# TODO: handle vertical text
|
||||
child.baseline = child._baseline - position_cross
|
||||
line.lower_baseline = max(line.lower_baseline, child.baseline)
|
||||
if line.lower_baseline == -inf:
|
||||
line.lower_baseline = line[0][1]._baseline if line else 0
|
||||
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 'normal' in align_self:
|
||||
align_self = ('stretch',)
|
||||
elif 'auto' in align_self:
|
||||
align_self = align_items
|
||||
position = 'position_y' if cross == 'height' else 'position_x'
|
||||
setattr(child, position, position_cross)
|
||||
if {'end', 'self-end', 'flex-end'} & set(align_self):
|
||||
if cross == 'height':
|
||||
child.position_y += (
|
||||
line.cross_size - child.margin_height())
|
||||
else:
|
||||
child.position_x += (
|
||||
line.cross_size - child.margin_width())
|
||||
elif 'center' in align_self:
|
||||
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 'baseline' in align_self:
|
||||
if cross == 'height':
|
||||
child.position_y += (
|
||||
line.lower_baseline - child.baseline)
|
||||
else:
|
||||
# Handle vertical text
|
||||
pass
|
||||
elif 'stretch' in align_self:
|
||||
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 {'flex-end', 'end'} & set(align_content):
|
||||
setattr(
|
||||
child, direction,
|
||||
current_value + extra_cross_size)
|
||||
elif 'center' in align_content:
|
||||
setattr(
|
||||
child, direction,
|
||||
current_value + extra_cross_size / 2)
|
||||
elif 'space-around' in align_content:
|
||||
setattr(
|
||||
child, direction,
|
||||
current_value + extra_cross_size /
|
||||
len(flex_lines) / 2)
|
||||
elif 'space-evenly' in align_content:
|
||||
setattr(
|
||||
child, direction,
|
||||
current_value + extra_cross_size /
|
||||
(len(flex_lines) + 1))
|
||||
if 'space-between' in align_content:
|
||||
cross_translate += extra_cross_size / (len(flex_lines) - 1)
|
||||
elif 'space-around' in align_content:
|
||||
cross_translate += extra_cross_size / len(flex_lines)
|
||||
elif 'space-evenly' in align_content:
|
||||
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, max_lines=None)[: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:
|
||||
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
|
||||
229
app/.venv/Lib/site-packages/weasyprint/layout/float.py
Normal file
229
app/.venv/Lib/site-packages/weasyprint/layout/float.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""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
|
||||
from .grid import grid_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
|
||||
# https://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, max_lines=None)
|
||||
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)
|
||||
elif isinstance(box, boxes.GridContainerBox):
|
||||
box, resume_at, _, _, _ = grid_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 https://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 can’t 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
|
||||
1246
app/.venv/Lib/site-packages/weasyprint/layout/grid.py
Normal file
1246
app/.venv/Lib/site-packages/weasyprint/layout/grid.py
Normal file
File diff suppressed because it is too large
Load Diff
1191
app/.venv/Lib/site-packages/weasyprint/layout/inline.py
Normal file
1191
app/.venv/Lib/site-packages/weasyprint/layout/inline.py
Normal file
File diff suppressed because it is too large
Load Diff
75
app/.venv/Lib/site-packages/weasyprint/layout/leader.py
Normal file
75
app/.venv/Lib/site-packages/weasyprint/layout/leader.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Leaders 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.margin_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:
|
||||
# Don’t add leaders behind the text on the left
|
||||
continue
|
||||
elif (position_x + text_box.width >
|
||||
leader_box.position_x + available_width):
|
||||
# Don’t 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]
|
||||
41
app/.venv/Lib/site-packages/weasyprint/layout/min_max.py
Normal file
41
app/.venv/Lib/site-packages/weasyprint/layout/min_max.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Decorators handling min- and max- widths and heights."""
|
||||
|
||||
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
|
||||
922
app/.venv/Lib/site-packages/weasyprint/layout/page.py
Normal file
922
app/.venv/Lib/site-packages/weasyprint/layout/page.py
Normal file
@@ -0,0 +1,922 @@
|
||||
"""Layout for pages and CSS3 margin boxes."""
|
||||
|
||||
import copy
|
||||
from math import inf
|
||||
|
||||
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
|
||||
|
||||
@outer.setter
|
||||
def outer(self, new_outer_width):
|
||||
self.inner = min(
|
||||
max(self.min_content_size, new_outer_width - self.sugar),
|
||||
self.max_content_size)
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
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``.
|
||||
|
||||
Described in: https://drafts.csswg.org/css-page-3/#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.
|
||||
# https://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 == 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, available_size):
|
||||
"""Compute and set a margin box fixed dimension on ``box``
|
||||
|
||||
Described in: https://drafts.csswg.org/css-page-3/#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 available_size:
|
||||
The distance between the page box’s left right border edges
|
||||
|
||||
"""
|
||||
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 not box_b.box.is_generated:
|
||||
# Non-generated boxes get zero for every box-model property
|
||||
assert box_b.inner == 0
|
||||
if box_a.inner == box_c.inner == 'auto':
|
||||
# A and C both have 'width: auto'
|
||||
if available_size > (
|
||||
box_a.outer_max_content_size +
|
||||
box_c.outer_max_content_size):
|
||||
# sum of the outer max-content widths
|
||||
# is less than the available width
|
||||
flex_space = (
|
||||
available_size -
|
||||
box_a.outer_max_content_size -
|
||||
box_c.outer_max_content_size)
|
||||
flex_factor_a = box_a.outer_max_content_size
|
||||
flex_factor_c = box_c.outer_max_content_size
|
||||
flex_factor_sum = flex_factor_a + flex_factor_c
|
||||
if flex_factor_sum == 0:
|
||||
flex_factor_sum = 1
|
||||
box_a.outer = box_a.max_content_size + (
|
||||
flex_space * flex_factor_a / flex_factor_sum)
|
||||
box_c.outer = box_c.max_content_size + (
|
||||
flex_space * flex_factor_c / flex_factor_sum)
|
||||
elif available_size > (
|
||||
box_a.outer_min_content_size +
|
||||
box_c.outer_min_content_size):
|
||||
# sum of the outer min-content widths
|
||||
# is less than the available width
|
||||
flex_space = (
|
||||
available_size -
|
||||
box_a.outer_min_content_size -
|
||||
box_c.outer_min_content_size)
|
||||
flex_factor_a = (
|
||||
box_a.max_content_size - box_a.min_content_size)
|
||||
flex_factor_c = (
|
||||
box_c.max_content_size - box_c.min_content_size)
|
||||
flex_factor_sum = flex_factor_a + flex_factor_c
|
||||
if flex_factor_sum == 0:
|
||||
flex_factor_sum = 1
|
||||
box_a.outer = box_a.min_content_size + (
|
||||
flex_space * flex_factor_a / flex_factor_sum)
|
||||
box_c.outer = box_c.min_content_size + (
|
||||
flex_space * flex_factor_c / flex_factor_sum)
|
||||
else:
|
||||
# otherwise
|
||||
flex_space = (
|
||||
available_size -
|
||||
box_a.outer_min_content_size -
|
||||
box_c.outer_min_content_size)
|
||||
flex_factor_a = box_a.min_content_size
|
||||
flex_factor_c = box_c.min_content_size
|
||||
flex_factor_sum = flex_factor_a + flex_factor_c
|
||||
if flex_factor_sum == 0:
|
||||
flex_factor_sum = 1
|
||||
box_a.outer = box_a.min_content_size + (
|
||||
flex_space * flex_factor_a / flex_factor_sum)
|
||||
box_c.outer = box_c.min_content_size + (
|
||||
flex_space * flex_factor_c / flex_factor_sum)
|
||||
else:
|
||||
# only one box has 'width: auto'
|
||||
if box_a.inner == 'auto':
|
||||
box_a.outer = available_size - box_c.outer
|
||||
elif box_c.inner == 'auto':
|
||||
box_c.outer = available_size - box_a.outer
|
||||
else:
|
||||
if box_b.inner == 'auto':
|
||||
# resolve any auto width of the middle box (B)
|
||||
ac_max_content_size = 2 * max(
|
||||
box_a.outer_max_content_size, box_c.outer_max_content_size)
|
||||
if available_size > (
|
||||
box_b.outer_max_content_size + ac_max_content_size):
|
||||
flex_space = (
|
||||
available_size -
|
||||
box_b.outer_max_content_size -
|
||||
ac_max_content_size)
|
||||
flex_factor_b = box_b.outer_max_content_size
|
||||
flex_factor_ac = ac_max_content_size
|
||||
flex_factor_sum = flex_factor_b + flex_factor_ac
|
||||
if flex_factor_sum == 0:
|
||||
flex_factor_sum = 1
|
||||
box_b.outer = box_b.max_content_size + (
|
||||
flex_space * flex_factor_b / flex_factor_sum)
|
||||
else:
|
||||
ac_min_content_size = 2 * max(
|
||||
box_a.outer_min_content_size, box_c.outer_min_content_size)
|
||||
if available_size > (
|
||||
box_b.outer_min_content_size + ac_min_content_size):
|
||||
flex_space = (
|
||||
available_size -
|
||||
box_b.outer_min_content_size -
|
||||
ac_min_content_size)
|
||||
flex_factor_b = (
|
||||
box_b.max_content_size - box_b.min_content_size)
|
||||
flex_factor_ac = ac_max_content_size - ac_min_content_size
|
||||
flex_factor_sum = flex_factor_b + flex_factor_ac
|
||||
if flex_factor_sum == 0:
|
||||
flex_factor_sum = 1
|
||||
box_b.outer = box_b.min_content_size + (
|
||||
flex_space * flex_factor_b / flex_factor_sum)
|
||||
else:
|
||||
flex_space = (
|
||||
available_size -
|
||||
box_b.outer_min_content_size -
|
||||
ac_min_content_size)
|
||||
flex_factor_b = box_b.min_content_size
|
||||
flex_factor_ac = ac_min_content_size
|
||||
flex_factor_sum = flex_factor_b + flex_factor_ac
|
||||
if flex_factor_sum == 0:
|
||||
flex_factor_sum = 1
|
||||
box_b.outer = box_b.min_content_size + (
|
||||
flex_space * flex_factor_b / flex_factor_sum)
|
||||
if box_a.inner == 'auto':
|
||||
box_a.outer = (available_size - box_b.outer) / 2
|
||||
if box_c.inner == 'auto':
|
||||
box_c.outer = (available_size - box_b.outer) / 2
|
||||
|
||||
# And, we’re 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, e.g. '@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
|
||||
# https://drafts.csswg.org/css-page-3/#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 box’s content once the box has dimensions."""
|
||||
positioned_boxes = []
|
||||
box, resume_at, next_page, _, _, _ = block_container_layout(
|
||||
context, box, bottom_space=-inf, skip_stack=None, page_is_empty=True,
|
||||
absolute_boxes=positioned_boxes, fixed_boxes=positioned_boxes,
|
||||
adjoining_margins=None, discard=False, max_lines=None)
|
||||
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."
|
||||
|
||||
https://drafts.csswg.org/css-page-3/#page-box-page-rule
|
||||
https://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 int page_number:
|
||||
Page number, starts 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 https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
|
||||
assert isinstance(root_box, (
|
||||
boxes.BlockBox, boxes.FlexContainerBox, boxes.GridContainerBox))
|
||||
context.create_block_formatting_context()
|
||||
context.current_page = page_number
|
||||
context.current_page_footnotes = []
|
||||
context.current_footnote_area = footnote_area
|
||||
|
||||
reported_footnotes = context.reported_footnotes
|
||||
context.reported_footnotes = []
|
||||
for i, reported_footnote in enumerate(reported_footnotes):
|
||||
context.footnotes.append(reported_footnote)
|
||||
overflow = context.layout_footnote(reported_footnote)
|
||||
if overflow and i != 0:
|
||||
context.report_footnote(reported_footnote)
|
||||
context.reported_footnotes = reported_footnotes[i:]
|
||||
break
|
||||
|
||||
page_is_empty = True
|
||||
adjoining_margins = []
|
||||
positioned_boxes = [] # Mixed absolute and fixed
|
||||
out_of_flow_boxes = []
|
||||
broken_out_of_flow = {}
|
||||
context_out_of_flow = context.broken_out_of_flow.values()
|
||||
context.broken_out_of_flow = broken_out_of_flow
|
||||
for box, containing_block, skip_stack in context_out_of_flow:
|
||||
box.position_y = root_box.content_box_y()
|
||||
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[out_of_flow_box] = (
|
||||
box, containing_block, out_of_flow_resume_at)
|
||||
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)
|
||||
assert root_box
|
||||
root_box.children = out_of_flow_boxes + root_box.children
|
||||
|
||||
footnote_area = build.create_anonymous_boxes(footnote_area.deepcopy())
|
||||
footnote_area = block_level_layout(
|
||||
context, footnote_area, -inf, None, footnote_area.page, True,
|
||||
positioned_boxes, positioned_boxes)[0]
|
||||
footnote_area.translate(dy=-footnote_area.margin_height())
|
||||
|
||||
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)
|
||||
|
||||
context.finish_block_formatting_context(root_box)
|
||||
|
||||
page.children = [root_box, footnote_area]
|
||||
|
||||
# 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 page.descendants(placeholders=True):
|
||||
# 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.
|
||||
if 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
|
||||
next_page = page_maker[page_number - 1][1]
|
||||
|
||||
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:
|
||||
# https://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)
|
||||
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))
|
||||
next_page_name = '' if blank else initial_next_page['page']
|
||||
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
|
||||
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
|
||||
reported_footnotes = None
|
||||
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)
|
||||
reported_footnotes = context.reported_footnotes
|
||||
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]
|
||||
reported_footnotes = None
|
||||
yield pages[i]
|
||||
|
||||
i += 1
|
||||
if resume_at is None and not reported_footnotes:
|
||||
# Throw away obsolete pages and content
|
||||
context.page_maker = context.page_maker[:i + 1]
|
||||
context.broken_out_of_flow.clear()
|
||||
context.reported_footnotes.clear()
|
||||
return
|
||||
156
app/.venv/Lib/site-packages/weasyprint/layout/percent.py
Normal file
156
app/.venv/Lib/site-packages/weasyprint/layout/percent.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""Resolve percentages into fixed values."""
|
||||
|
||||
from math import inf
|
||||
|
||||
from ..formatting_structure import boxes
|
||||
|
||||
|
||||
def percentage(value, refer_to):
|
||||
"""Return the percentage of the reference value, or the value unchanged.
|
||||
|
||||
``refer_to`` is the length for 100%. If ``refer_to`` is not a number, it
|
||||
just replaces percentages.
|
||||
|
||||
"""
|
||||
if value is None or value == 'auto':
|
||||
return value
|
||||
elif value.unit == 'px':
|
||||
return value.value
|
||||
else:
|
||||
assert value.unit == '%'
|
||||
return refer_to * value.value / 100
|
||||
|
||||
|
||||
def resolve_one_percentage(box, property_name, refer_to,
|
||||
main_flex_direction=None):
|
||||
"""Set a used length value from a computed length value.
|
||||
|
||||
``refer_to`` is the length for 100%. If ``refer_to`` is not a number, it
|
||||
just replaces percentages.
|
||||
|
||||
"""
|
||||
# box.style has computed values
|
||||
value = box.style[property_name]
|
||||
# box attributes are used values
|
||||
percent = percentage(value, refer_to)
|
||||
setattr(box, property_name, percent)
|
||||
if property_name in ('min_width', 'min_height') and percent == 'auto':
|
||||
if (main_flex_direction is None or
|
||||
property_name != (f'min_{main_flex_direction}')):
|
||||
setattr(box, property_name, 0)
|
||||
|
||||
|
||||
def resolve_position_percentages(box, containing_block):
|
||||
cb_width, cb_height = containing_block
|
||||
resolve_one_percentage(box, 'left', cb_width)
|
||||
resolve_one_percentage(box, 'right', cb_width)
|
||||
resolve_one_percentage(box, 'top', cb_height)
|
||||
resolve_one_percentage(box, 'bottom', cb_height)
|
||||
|
||||
|
||||
def resolve_percentages(box, containing_block, main_flex_direction=None):
|
||||
"""Set used values as attributes of the box object."""
|
||||
if isinstance(containing_block, boxes.Box):
|
||||
# cb is short for containing block
|
||||
cb_width = containing_block.width
|
||||
cb_height = containing_block.height
|
||||
else:
|
||||
cb_width, cb_height = containing_block
|
||||
if isinstance(box, boxes.PageBox):
|
||||
maybe_height = cb_height
|
||||
else:
|
||||
maybe_height = cb_width
|
||||
resolve_one_percentage(box, 'margin_left', cb_width)
|
||||
resolve_one_percentage(box, 'margin_right', cb_width)
|
||||
resolve_one_percentage(box, 'margin_top', maybe_height)
|
||||
resolve_one_percentage(box, 'margin_bottom', maybe_height)
|
||||
resolve_one_percentage(box, 'padding_left', cb_width)
|
||||
resolve_one_percentage(box, 'padding_right', cb_width)
|
||||
resolve_one_percentage(box, 'padding_top', maybe_height)
|
||||
resolve_one_percentage(box, 'padding_bottom', maybe_height)
|
||||
resolve_one_percentage(box, 'width', cb_width)
|
||||
resolve_one_percentage(box, 'min_width', cb_width, main_flex_direction)
|
||||
resolve_one_percentage(box, 'max_width', cb_width, main_flex_direction)
|
||||
|
||||
# XXX later: top, bottom, left and right on positioned elements
|
||||
|
||||
if cb_height == 'auto':
|
||||
# Special handling when the height of the containing block
|
||||
# depends on its content.
|
||||
height = box.style['height']
|
||||
if height == 'auto' or height.unit == '%':
|
||||
box.height = 'auto'
|
||||
else:
|
||||
assert height.unit == 'px'
|
||||
box.height = height.value
|
||||
resolve_one_percentage(box, 'min_height', 0, main_flex_direction)
|
||||
resolve_one_percentage(box, 'max_height', inf, main_flex_direction)
|
||||
else:
|
||||
resolve_one_percentage(box, 'height', cb_height)
|
||||
resolve_one_percentage(
|
||||
box, 'min_height', cb_height, main_flex_direction)
|
||||
resolve_one_percentage(
|
||||
box, 'max_height', cb_height, main_flex_direction)
|
||||
|
||||
collapse = box.style['border_collapse'] == 'collapse'
|
||||
# Used value == computed value
|
||||
for side in ('top', 'right', 'bottom', 'left'):
|
||||
prop = f'border_{side}_width'
|
||||
# border-{side}-width would have been resolved
|
||||
# during border conflict resolution for collapsed-borders
|
||||
if not (collapse and hasattr(box, prop)):
|
||||
setattr(box, prop, box.style[prop])
|
||||
|
||||
# Shrink *content* widths and heights according to box-sizing
|
||||
# Thanks heavens and the spec: Our validator rejects negative values
|
||||
# for padding and border-width
|
||||
if box.style['box_sizing'] == 'border-box':
|
||||
horizontal_delta = (
|
||||
box.padding_left + box.padding_right +
|
||||
box.border_left_width + box.border_right_width)
|
||||
vertical_delta = (
|
||||
box.padding_top + box.padding_bottom +
|
||||
box.border_top_width + box.border_bottom_width)
|
||||
elif box.style['box_sizing'] == 'padding-box':
|
||||
horizontal_delta = box.padding_left + box.padding_right
|
||||
vertical_delta = box.padding_top + box.padding_bottom
|
||||
else:
|
||||
assert box.style['box_sizing'] == 'content-box'
|
||||
horizontal_delta = 0
|
||||
vertical_delta = 0
|
||||
|
||||
# Keep at least min_* >= 0 to prevent funny output in case box.width or
|
||||
# box.height become negative.
|
||||
# Restricting max_* seems reasonable, too.
|
||||
if horizontal_delta > 0:
|
||||
if box.width != 'auto':
|
||||
box.width = max(0, box.width - horizontal_delta)
|
||||
box.max_width = max(0, box.max_width - horizontal_delta)
|
||||
if box.min_width != 'auto':
|
||||
box.min_width = max(0, box.min_width - horizontal_delta)
|
||||
if vertical_delta > 0:
|
||||
if box.height != 'auto':
|
||||
box.height = max(0, box.height - vertical_delta)
|
||||
box.max_height = max(0, box.max_height - vertical_delta)
|
||||
if box.min_height != 'auto':
|
||||
box.min_height = max(0, box.min_height - vertical_delta)
|
||||
|
||||
|
||||
def resolve_radii_percentages(box):
|
||||
for corner in ('top_left', 'top_right', 'bottom_right', 'bottom_left'):
|
||||
property_name = f'border_{corner}_radius'
|
||||
rx, ry = box.style[property_name]
|
||||
|
||||
# Short track for common case
|
||||
if (0, 'px') in (rx, ry):
|
||||
setattr(box, property_name, (0, 0))
|
||||
continue
|
||||
|
||||
for side in corner.split('_'):
|
||||
if side in box.remove_decoration_sides:
|
||||
setattr(box, property_name, (0, 0))
|
||||
break
|
||||
else:
|
||||
rx = percentage(rx, box.border_width())
|
||||
ry = percentage(ry, box.border_height())
|
||||
setattr(box, property_name, (rx, ry))
|
||||
779
app/.venv/Lib/site-packages/weasyprint/layout/preferred.py
Normal file
779
app/.venv/Lib/site-packages/weasyprint/layout/preferred.py
Normal file
@@ -0,0 +1,779 @@
|
||||
"""Preferred and minimum preferred width.
|
||||
|
||||
Also known as max-content and min-content width, also known as the
|
||||
shrink-to-fit algorithm.
|
||||
|
||||
Terms used (max-content width, min-content width) are defined in David
|
||||
Baron's unofficial draft (https://dbaron.org/css/intrinsic/).
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
from math import inf
|
||||
|
||||
from ..formatting_structure import boxes
|
||||
from ..text.line_break import split_first_line
|
||||
from .replaced import default_image_sizing
|
||||
|
||||
|
||||
def shrink_to_fit(context, box, available_content_width):
|
||||
"""Return the shrink-to-fit width of ``box``.
|
||||
|
||||
*Warning:* both available_content_width and the return value are
|
||||
for width of the *content area*, not margin area.
|
||||
|
||||
https://www.w3.org/TR/CSS21/visudet.html#float-width
|
||||
|
||||
"""
|
||||
return min(
|
||||
max(
|
||||
min_content_width(context, box, outer=False),
|
||||
available_content_width),
|
||||
max_content_width(context, box, outer=False))
|
||||
|
||||
|
||||
def min_content_width(context, box, outer=True):
|
||||
"""Return the min-content width for ``box``.
|
||||
|
||||
This is the width by breaking at every line-break opportunity.
|
||||
|
||||
"""
|
||||
if box.is_table_wrapper:
|
||||
return table_and_columns_preferred_widths(context, box, outer)[0]
|
||||
elif isinstance(box, boxes.TableCellBox):
|
||||
return table_cell_min_content_width(context, box, outer)
|
||||
elif isinstance(box, (
|
||||
boxes.BlockContainerBox, boxes.TableColumnBox, boxes.FlexBox)):
|
||||
return block_min_content_width(context, box, outer)
|
||||
elif isinstance(box, boxes.TableColumnGroupBox):
|
||||
return column_group_content_width(context, box)
|
||||
elif isinstance(box, (boxes.InlineBox, boxes.LineBox)):
|
||||
return inline_min_content_width(
|
||||
context, box, outer, is_line_start=True)
|
||||
elif isinstance(box, boxes.ReplacedBox):
|
||||
return replaced_min_content_width(box, outer)
|
||||
elif isinstance(box, boxes.FlexContainerBox):
|
||||
return flex_min_content_width(context, box, outer)
|
||||
elif isinstance(box, boxes.GridContainerBox):
|
||||
# TODO: Get real grid size.
|
||||
return block_min_content_width(context, box, outer)
|
||||
else:
|
||||
raise TypeError(
|
||||
f'min-content width for {type(box).__name__} not handled yet')
|
||||
|
||||
|
||||
def max_content_width(context, box, outer=True):
|
||||
"""Return the max-content width for ``box``.
|
||||
|
||||
This is the width by only breaking at forced line breaks.
|
||||
|
||||
"""
|
||||
if box.is_table_wrapper:
|
||||
return table_and_columns_preferred_widths(context, box, outer)[1]
|
||||
elif isinstance(box, boxes.TableCellBox):
|
||||
return table_cell_max_content_width(context, box, outer)
|
||||
elif isinstance(box, (
|
||||
boxes.BlockContainerBox, boxes.TableColumnBox, boxes.FlexBox)):
|
||||
return block_max_content_width(context, box, outer)
|
||||
elif isinstance(box, boxes.TableColumnGroupBox):
|
||||
return column_group_content_width(context, box)
|
||||
elif isinstance(box, (boxes.InlineBox, boxes.LineBox)):
|
||||
return inline_max_content_width(
|
||||
context, box, outer, is_line_start=True)
|
||||
elif isinstance(box, boxes.ReplacedBox):
|
||||
return replaced_max_content_width(box, outer)
|
||||
elif isinstance(box, boxes.FlexContainerBox):
|
||||
return flex_max_content_width(context, box, outer)
|
||||
elif isinstance(box, boxes.GridContainerBox):
|
||||
# TODO: Get real grid size.
|
||||
return block_max_content_width(context, box, outer)
|
||||
else:
|
||||
raise TypeError(
|
||||
f'max-content width for {type(box).__name__} not handled yet')
|
||||
|
||||
|
||||
def _block_content_width(context, box, function, outer):
|
||||
"""Helper to create ``block_*_content_width.``"""
|
||||
width = box.style['width']
|
||||
if width == 'auto' or width.unit == '%':
|
||||
# "percentages on the following properties are treated instead as
|
||||
# though they were the following: width: auto"
|
||||
# https://dbaron.org/css/intrinsic/#outer-intrinsic
|
||||
children_widths = [
|
||||
function(context, child, outer=True) for child in box.children
|
||||
if not child.is_absolutely_positioned()]
|
||||
width = max(children_widths) if children_widths else 0
|
||||
else:
|
||||
assert width.unit == 'px'
|
||||
width = width.value
|
||||
|
||||
return adjust(box, outer, width)
|
||||
|
||||
|
||||
def min_max(box, width):
|
||||
"""Get box width from given width and box min- and max-widths."""
|
||||
min_width = box.style['min_width']
|
||||
max_width = box.style['max_width']
|
||||
if min_width == 'auto' or min_width.unit == '%':
|
||||
min_width = 0
|
||||
else:
|
||||
min_width = min_width.value
|
||||
if max_width == 'auto' or max_width.unit == '%':
|
||||
max_width = inf
|
||||
else:
|
||||
max_width = max_width.value
|
||||
|
||||
if isinstance(box, boxes.ReplacedBox):
|
||||
_, _, ratio = box.replacement.get_intrinsic_size(
|
||||
1, box.style['font_size'])
|
||||
if ratio is not None:
|
||||
min_height = box.style['min_height']
|
||||
if min_height != 'auto' and min_height.unit != '%':
|
||||
min_width = max(min_width, min_height.value * ratio)
|
||||
max_height = box.style['max_height']
|
||||
if max_height != 'auto' and max_height.unit != '%':
|
||||
max_width = min(max_width, max_height.value * ratio)
|
||||
|
||||
return max(min_width, min(width, max_width))
|
||||
|
||||
|
||||
def margin_width(box, width, left=True, right=True):
|
||||
"""Add box paddings, borders and margins to ``width``."""
|
||||
percentages = 0
|
||||
|
||||
# See https://drafts.csswg.org/css-tables-3/#cell-intrinsic-offsets
|
||||
# It is a set of computed values for border-left-width, padding-left,
|
||||
# padding-right, and border-right-width (along with zero values for
|
||||
# margin-left and margin-right)
|
||||
for value in (
|
||||
(['margin_left', 'padding_left'] if left else []) +
|
||||
(['margin_right', 'padding_right'] if right else [])
|
||||
):
|
||||
style_value = box.style[value]
|
||||
if style_value != 'auto':
|
||||
if style_value.unit == 'px':
|
||||
width += style_value.value
|
||||
else:
|
||||
assert style_value.unit == '%'
|
||||
percentages += style_value.value
|
||||
|
||||
collapse = box.style['border_collapse'] == 'collapse'
|
||||
if left:
|
||||
if collapse and hasattr(box, 'border_left_width'):
|
||||
# In collapsed-borders mode: the computed horizontal padding of the
|
||||
# cell and, for border values, the used border-width values of the
|
||||
# cell (half the winning border-width)
|
||||
width += box.border_left_width
|
||||
else:
|
||||
# In separated-borders mode: the computed horizontal padding and
|
||||
# border of the table-cell
|
||||
width += box.style['border_left_width']
|
||||
if right:
|
||||
if collapse and hasattr(box, 'border_right_width'):
|
||||
# [...] the used border-width values of the cell
|
||||
width += box.border_right_width
|
||||
else:
|
||||
# [...] the computed border of the table-cell
|
||||
width += box.style['border_right_width']
|
||||
|
||||
if percentages < 100:
|
||||
return width / (1 - percentages / 100)
|
||||
else:
|
||||
# Pathological case, ignore
|
||||
return 0
|
||||
|
||||
|
||||
def adjust(box, outer, width, left=True, right=True):
|
||||
"""Respect min/max and adjust width depending on ``outer``.
|
||||
|
||||
If ``outer`` is set to ``True``, return margin width, else return content
|
||||
width.
|
||||
|
||||
"""
|
||||
fixed = min_max(box, width)
|
||||
|
||||
if outer:
|
||||
return margin_width(box, fixed, left, right)
|
||||
else:
|
||||
return fixed
|
||||
|
||||
|
||||
def block_min_content_width(context, box, outer=True):
|
||||
"""Return the min-content width for a ``BlockBox``."""
|
||||
return _block_content_width(
|
||||
context, box, min_content_width, outer)
|
||||
|
||||
|
||||
def block_max_content_width(context, box, outer=True):
|
||||
"""Return the max-content width for a ``BlockBox``."""
|
||||
return _block_content_width(context, box, max_content_width, outer)
|
||||
|
||||
|
||||
def inline_min_content_width(context, box, outer=True, skip_stack=None,
|
||||
first_line=False, is_line_start=False):
|
||||
"""Return the min-content width for an ``InlineBox``.
|
||||
|
||||
The width is calculated from the lines from ``skip_stack``. If
|
||||
``first_line`` is ``True``, only the first line minimum width is
|
||||
calculated.
|
||||
|
||||
"""
|
||||
widths = inline_line_widths(
|
||||
context, box, outer, is_line_start, minimum=True,
|
||||
skip_stack=skip_stack, first_line=first_line)
|
||||
width = next(widths) if first_line else max(widths)
|
||||
return adjust(box, outer, width)
|
||||
|
||||
|
||||
def inline_max_content_width(context, box, outer=True, is_line_start=False):
|
||||
"""Return the max-content width for an ``InlineBox``."""
|
||||
widths = list(
|
||||
inline_line_widths(context, box, outer, is_line_start, minimum=False))
|
||||
# Remove trailing space, as split_first_line keeps trailing spaces when
|
||||
# max_width is not set.
|
||||
widths[-1] -= trailing_whitespace_size(context, box)
|
||||
return adjust(box, outer, max(widths))
|
||||
|
||||
|
||||
def column_group_content_width(context, box):
|
||||
"""Return the *-content width for a ``TableColumnGroupBox``."""
|
||||
width = box.style['width']
|
||||
if width == 'auto' or width.unit == '%':
|
||||
width = 0
|
||||
else:
|
||||
assert width.unit == 'px'
|
||||
width = width.value
|
||||
|
||||
return adjust(box, False, width)
|
||||
|
||||
|
||||
def table_cell_min_content_width(context, box, outer):
|
||||
"""Return the min-content width for a ``TableCellBox``."""
|
||||
# See https://www.w3.org/TR/css-tables-3/#outer-min-content
|
||||
# The outer min-content width of a table-cell is
|
||||
# max(min-width, min-content width) adjusted by
|
||||
# the cell intrinsic offsets.
|
||||
children_widths = [
|
||||
min_content_width(context, child)
|
||||
for child in box.children
|
||||
if not child.is_absolutely_positioned()]
|
||||
children_min_width = adjust(
|
||||
box,
|
||||
outer,
|
||||
max(children_widths) if children_widths else 0)
|
||||
|
||||
return children_min_width
|
||||
|
||||
|
||||
def table_cell_max_content_width(context, box, outer):
|
||||
"""Return the max-content width for a ``TableCellBox``."""
|
||||
return max(
|
||||
table_cell_min_content_width(context, box, outer),
|
||||
block_max_content_width(context, box, outer))
|
||||
|
||||
|
||||
def inline_line_widths(context, box, outer, is_line_start, minimum,
|
||||
skip_stack=None, first_line=False):
|
||||
if isinstance(box, boxes.LineBox):
|
||||
if box.style['text_indent'].unit == '%':
|
||||
# TODO: this is wrong, text-indent percentages should be resolved
|
||||
# before calling this function.
|
||||
text_indent = 0
|
||||
else:
|
||||
text_indent = box.style['text_indent'].value
|
||||
else:
|
||||
text_indent = 0
|
||||
|
||||
current_line = 0
|
||||
if skip_stack is None:
|
||||
skip = 0
|
||||
else:
|
||||
(skip, skip_stack), = skip_stack.items()
|
||||
for child in box.children[skip:]:
|
||||
if child.is_absolutely_positioned():
|
||||
continue # Skip
|
||||
|
||||
if isinstance(child, boxes.InlineBox):
|
||||
lines = inline_line_widths(
|
||||
context, child, outer, is_line_start, minimum, skip_stack,
|
||||
first_line)
|
||||
if first_line:
|
||||
lines = [next(lines)]
|
||||
else:
|
||||
lines = list(lines)
|
||||
if len(lines) == 1:
|
||||
lines[0] = adjust(child, outer, lines[0])
|
||||
else:
|
||||
lines[0] = adjust(child, outer, lines[0], right=False)
|
||||
lines[-1] = adjust(child, outer, lines[-1], left=False)
|
||||
elif isinstance(child, boxes.TextBox):
|
||||
space_collapse = child.style['white_space'] in (
|
||||
'normal', 'nowrap', 'pre-line')
|
||||
if skip_stack is None:
|
||||
skip = 0
|
||||
else:
|
||||
(skip, skip_stack), = skip_stack.items()
|
||||
assert skip_stack is None
|
||||
child_text = child.text.encode()[(skip or 0):]
|
||||
if is_line_start and space_collapse:
|
||||
child_text = child_text.lstrip(b' ')
|
||||
if minimum and child_text == b' ':
|
||||
lines = [0, 0]
|
||||
else:
|
||||
max_width = 0 if minimum else None
|
||||
lines = []
|
||||
resume_index = new_resume_index = 0
|
||||
while new_resume_index is not None:
|
||||
resume_index += new_resume_index
|
||||
_, _, new_resume_index, width, _, _ = (
|
||||
split_first_line(
|
||||
child_text[resume_index:].decode(), child.style,
|
||||
context, max_width, child.justification_spacing,
|
||||
is_line_start=is_line_start, minimum=True))
|
||||
lines.append(width)
|
||||
if first_line:
|
||||
break
|
||||
if first_line and new_resume_index:
|
||||
current_line += lines[0]
|
||||
break
|
||||
else:
|
||||
# https://www.w3.org/TR/css-text-3/#overflow-wrap
|
||||
# "The line breaking behavior of a replaced element
|
||||
# or other atomic inline is equivalent to that
|
||||
# of the Object Replacement Character (U+FFFC)."
|
||||
# https://www.unicode.org/reports/tr14/#DescriptionOfProperties
|
||||
# "By default, there is a break opportunity
|
||||
# both before and after any inline object."
|
||||
if minimum:
|
||||
lines = [0, max_content_width(context, child), 0]
|
||||
else:
|
||||
lines = [max_content_width(context, child)]
|
||||
# The first text line goes on the current line
|
||||
current_line += lines[0]
|
||||
if len(lines) > 1:
|
||||
# Forced line break
|
||||
yield current_line + text_indent
|
||||
text_indent = 0
|
||||
if len(lines) > 2:
|
||||
for line in lines[1:-1]:
|
||||
yield line
|
||||
current_line = lines[-1]
|
||||
is_line_start = lines[-1] == 0
|
||||
skip_stack = None
|
||||
yield current_line + text_indent
|
||||
|
||||
|
||||
def _percentage_contribution(box):
|
||||
"""Return the percentage contribution of a cell, column or column group.
|
||||
|
||||
https://dbaron.org/css/intrinsic/#pct-contrib
|
||||
|
||||
"""
|
||||
min_width = (
|
||||
box.style['min_width'].value if box.style['min_width'] != 'auto' and
|
||||
box.style['min_width'].unit == '%' else 0)
|
||||
max_width = (
|
||||
box.style['max_width'].value if box.style['max_width'] != 'auto' and
|
||||
box.style['max_width'].unit == '%' else inf)
|
||||
width = (
|
||||
box.style['width'].value if box.style['width'] != 'auto' and
|
||||
box.style['width'].unit == '%' else 0)
|
||||
return max(min_width, min(width, max_width))
|
||||
|
||||
|
||||
def table_and_columns_preferred_widths(context, box, outer=True):
|
||||
"""Return content widths for the auto layout table and its columns.
|
||||
|
||||
The tuple returned is
|
||||
``(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)``
|
||||
|
||||
https://dbaron.org/css/intrinsic/
|
||||
|
||||
"""
|
||||
from .table import distribute_excess_width
|
||||
|
||||
table = box.get_wrapped_table()
|
||||
result = context.tables.get(table)
|
||||
if result:
|
||||
return result[outer]
|
||||
|
||||
# Create the grid
|
||||
grid_width, grid_height = 0, 0
|
||||
row_number = 0
|
||||
for row_group in table.children:
|
||||
for row in row_group.children:
|
||||
for cell in row.children:
|
||||
grid_width = max(cell.grid_x + cell.colspan, grid_width)
|
||||
grid_height = max(row_number + cell.rowspan, grid_height)
|
||||
row_number += 1
|
||||
grid = [[None] * grid_width for i in range(grid_height)]
|
||||
row_number = 0
|
||||
for row_group in table.children:
|
||||
for row in row_group.children:
|
||||
for cell in row.children:
|
||||
grid[row_number][cell.grid_x] = cell
|
||||
row_number += 1
|
||||
|
||||
zipped_grid = list(zip(*grid))
|
||||
|
||||
# Define the total horizontal border spacing
|
||||
if table.style['border_collapse'] == 'separate' and grid_width > 0:
|
||||
total_horizontal_border_spacing = (
|
||||
table.style['border_spacing'][0] *
|
||||
(1 + len([column for column in zipped_grid if any(column)])))
|
||||
else:
|
||||
total_horizontal_border_spacing = 0
|
||||
|
||||
if grid_width == 0 or grid_height == 0:
|
||||
table.children = []
|
||||
min_width = block_min_content_width(context, table, outer=False)
|
||||
max_width = block_max_content_width(context, table, outer=False)
|
||||
outer_min_width = adjust(
|
||||
box, outer=True, width=block_min_content_width(context, table))
|
||||
outer_max_width = adjust(
|
||||
box, outer=True, width=block_max_content_width(context, table))
|
||||
result = ([], [], [], [], total_horizontal_border_spacing, [])
|
||||
context.tables[table] = result = {
|
||||
False: (min_width, max_width, *result),
|
||||
True: (outer_min_width, outer_max_width, *result),
|
||||
}
|
||||
return result[outer]
|
||||
|
||||
column_groups = [None] * grid_width
|
||||
columns = [None] * grid_width
|
||||
column_number = 0
|
||||
for column_group in table.column_groups:
|
||||
for column in column_group.children:
|
||||
column_groups[column_number] = column_group
|
||||
columns[column_number] = column
|
||||
column_number += 1
|
||||
if column_number == grid_width:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
|
||||
colspan_cells = []
|
||||
|
||||
# Define the intermediate content widths
|
||||
min_content_widths = [0 for i in range(grid_width)]
|
||||
max_content_widths = [0 for i in range(grid_width)]
|
||||
intrinsic_percentages = [0 for i in range(grid_width)]
|
||||
|
||||
# Intermediate content widths for span 1
|
||||
for i in range(grid_width):
|
||||
for groups in (column_groups, columns):
|
||||
if groups[i]:
|
||||
min_content_widths[i] = max(
|
||||
min_content_widths[i],
|
||||
min_content_width(context, groups[i]))
|
||||
max_content_widths[i] = max(
|
||||
max_content_widths[i],
|
||||
max_content_width(context, groups[i]))
|
||||
intrinsic_percentages[i] = max(
|
||||
intrinsic_percentages[i],
|
||||
_percentage_contribution(groups[i]))
|
||||
for cell in zipped_grid[i]:
|
||||
if cell:
|
||||
if cell.colspan == 1:
|
||||
min_content_widths[i] = max(
|
||||
min_content_widths[i],
|
||||
min_content_width(context, cell))
|
||||
max_content_widths[i] = max(
|
||||
max_content_widths[i],
|
||||
max_content_width(context, cell))
|
||||
intrinsic_percentages[i] = max(
|
||||
intrinsic_percentages[i],
|
||||
_percentage_contribution(cell))
|
||||
else:
|
||||
colspan_cells.append(cell)
|
||||
|
||||
# Intermediate content widths for span > 1 is wrong in the 4.1 section, as
|
||||
# explained in its third issue. Min- and max-content widths are handled by
|
||||
# the excess width distribution method, and percentages do not distribute
|
||||
# widths to columns that have originating cells.
|
||||
|
||||
# Intermediate intrinsic percentage widths for span > 1
|
||||
for span in range(1, grid_width):
|
||||
percentage_contributions = []
|
||||
for i in range(grid_width):
|
||||
percentage_contribution = intrinsic_percentages[i]
|
||||
for j, cell in enumerate(zipped_grid[i]):
|
||||
indexes = [k for k in range(i + 1) if grid[j][k]]
|
||||
if not indexes:
|
||||
continue
|
||||
origin = max(indexes)
|
||||
origin_cell = grid[j][origin]
|
||||
if origin_cell.colspan - 1 != span:
|
||||
continue
|
||||
cell_slice = slice(origin, origin + origin_cell.colspan)
|
||||
baseline_percentage = sum(intrinsic_percentages[cell_slice])
|
||||
|
||||
# Cell contribution to intrinsic percentage width
|
||||
if intrinsic_percentages[i] == 0:
|
||||
diff = max(
|
||||
0,
|
||||
_percentage_contribution(origin_cell) -
|
||||
baseline_percentage)
|
||||
other_columns_contributions = [
|
||||
max_content_widths[j]
|
||||
for j in range(
|
||||
origin, origin + origin_cell.colspan)
|
||||
if intrinsic_percentages[j] == 0]
|
||||
other_columns_contributions_sum = sum(
|
||||
other_columns_contributions)
|
||||
if other_columns_contributions_sum == 0:
|
||||
if other_columns_contributions:
|
||||
ratio = 1 / len(other_columns_contributions)
|
||||
else:
|
||||
ratio = 1
|
||||
else:
|
||||
ratio = (
|
||||
max_content_widths[i] /
|
||||
other_columns_contributions_sum)
|
||||
percentage_contribution = max(
|
||||
percentage_contribution,
|
||||
diff * ratio)
|
||||
|
||||
percentage_contributions.append(percentage_contribution)
|
||||
|
||||
intrinsic_percentages = percentage_contributions
|
||||
|
||||
# Define constrainedness
|
||||
constrainedness = [False for i in range(grid_width)]
|
||||
for i in range(grid_width):
|
||||
if (column_groups[i] and column_groups[i].style['width'] != 'auto' and
|
||||
column_groups[i].style['width'].unit != '%'):
|
||||
constrainedness[i] = True
|
||||
continue
|
||||
if (columns[i] and columns[i].style['width'] != 'auto' and
|
||||
columns[i].style['width'].unit != '%'):
|
||||
constrainedness[i] = True
|
||||
continue
|
||||
for cell in zipped_grid[i]:
|
||||
if (cell and cell.colspan == 1 and
|
||||
cell.style['width'] != 'auto' and
|
||||
cell.style['width'].unit != '%'):
|
||||
constrainedness[i] = True
|
||||
break
|
||||
|
||||
intrinsic_percentages = [
|
||||
min(percentage, 100 - sum(intrinsic_percentages[:i]))
|
||||
for i, percentage in enumerate(intrinsic_percentages)]
|
||||
|
||||
# Max- and min-content widths for span > 1
|
||||
for cell in colspan_cells:
|
||||
min_content = min_content_width(context, cell)
|
||||
max_content = max_content_width(context, cell)
|
||||
column_slice = slice(cell.grid_x, cell.grid_x + cell.colspan)
|
||||
columns_min_content = sum(min_content_widths[column_slice])
|
||||
columns_max_content = sum(max_content_widths[column_slice])
|
||||
if table.style['border_collapse'] == 'separate':
|
||||
spacing = (cell.colspan - 1) * table.style['border_spacing'][0]
|
||||
else:
|
||||
spacing = 0
|
||||
|
||||
if min_content > columns_min_content + spacing:
|
||||
excess_width = min_content - (columns_min_content + spacing)
|
||||
distribute_excess_width(
|
||||
context, zipped_grid, excess_width, min_content_widths,
|
||||
constrainedness, intrinsic_percentages, max_content_widths,
|
||||
column_slice)
|
||||
|
||||
if max_content > columns_max_content + spacing:
|
||||
excess_width = max_content - (columns_max_content + spacing)
|
||||
distribute_excess_width(
|
||||
context, zipped_grid, excess_width, max_content_widths,
|
||||
constrainedness, intrinsic_percentages, max_content_widths,
|
||||
column_slice)
|
||||
|
||||
# Calculate the max- and min-content widths of table and columns
|
||||
small_percentage_contributions = [
|
||||
max_content_widths[i] / (intrinsic_percentages[i] / 100)
|
||||
for i in range(grid_width)
|
||||
if intrinsic_percentages[i]]
|
||||
large_percentage_contribution_numerator = sum(
|
||||
max_content_widths[i] for i in range(grid_width)
|
||||
if intrinsic_percentages[i] == 0)
|
||||
large_percentage_contribution_denominator = (
|
||||
(100 - sum(intrinsic_percentages)) / 100)
|
||||
if large_percentage_contribution_denominator == 0:
|
||||
if large_percentage_contribution_numerator == 0:
|
||||
large_percentage_contribution = 0
|
||||
else:
|
||||
# "the large percentage contribution of the table [is] an
|
||||
# infinitely large number if the numerator is nonzero [and] the
|
||||
# denominator of that ratio is 0."
|
||||
#
|
||||
# https://dbaron.org/css/intrinsic/#autotableintrinsic
|
||||
#
|
||||
# Please note that "an infinitely large number" is not "infinite",
|
||||
# and that's probably not a coincindence: putting 'inf' here breaks
|
||||
# some cases (see #305).
|
||||
large_percentage_contribution = sys.maxsize
|
||||
else:
|
||||
large_percentage_contribution = (
|
||||
large_percentage_contribution_numerator /
|
||||
large_percentage_contribution_denominator)
|
||||
|
||||
table_min_content_width = (
|
||||
total_horizontal_border_spacing + sum(min_content_widths))
|
||||
table_max_content_width = (
|
||||
total_horizontal_border_spacing + max([
|
||||
sum(max_content_widths), large_percentage_contribution,
|
||||
*small_percentage_contributions]))
|
||||
|
||||
if table.style['width'] != 'auto' and table.style['width'].unit == 'px':
|
||||
# "percentages on the following properties are treated instead as
|
||||
# though they were the following: width: auto"
|
||||
# https://dbaron.org/css/intrinsic/#outer-intrinsic
|
||||
table_min_width = table_max_width = table.style['width'].value
|
||||
else:
|
||||
table_min_width = table_min_content_width
|
||||
table_max_width = table_max_content_width
|
||||
|
||||
table_min_content_width = max(
|
||||
table_min_content_width, adjust(
|
||||
table, outer=False, width=table_min_width))
|
||||
table_max_content_width = max(
|
||||
table_max_content_width, adjust(
|
||||
table, outer=False, width=table_max_width))
|
||||
table_outer_min_content_width = margin_width(
|
||||
table, margin_width(box, table_min_content_width))
|
||||
table_outer_max_content_width = margin_width(
|
||||
table, margin_width(box, table_max_content_width))
|
||||
|
||||
result = (
|
||||
min_content_widths, max_content_widths, intrinsic_percentages,
|
||||
constrainedness, total_horizontal_border_spacing, zipped_grid)
|
||||
context.tables[table] = result = {
|
||||
False: (table_min_content_width, table_max_content_width, *result),
|
||||
True: (table_outer_min_content_width, table_outer_max_content_width, *result),
|
||||
}
|
||||
return result[outer]
|
||||
|
||||
|
||||
def replaced_min_content_width(box, outer=True):
|
||||
"""Return the min-content width for an ``InlineReplacedBox``."""
|
||||
width = box.style['width']
|
||||
if width == 'auto':
|
||||
height = box.style['height']
|
||||
if height == 'auto' or height.unit == '%':
|
||||
height = 'auto'
|
||||
else:
|
||||
assert height.unit == 'px'
|
||||
height = height.value
|
||||
if (box.style['max_width'] != 'auto' and
|
||||
box.style['max_width'].unit == '%'):
|
||||
# See https://drafts.csswg.org/css-sizing/#intrinsic-contribution
|
||||
width = 0
|
||||
else:
|
||||
image = box.replacement
|
||||
intrinsic_width, intrinsic_height, intrinsic_ratio = (
|
||||
image.get_intrinsic_size(
|
||||
box.style['image_resolution'], box.style['font_size']))
|
||||
width, _ = default_image_sizing(
|
||||
intrinsic_width, intrinsic_height, intrinsic_ratio, 'auto',
|
||||
height, default_width=300, default_height=150)
|
||||
elif box.style['width'].unit == '%':
|
||||
# See https://drafts.csswg.org/css-sizing/#intrinsic-contribution
|
||||
width = 0
|
||||
else:
|
||||
assert width.unit == 'px'
|
||||
width = width.value
|
||||
return adjust(box, outer, width)
|
||||
|
||||
|
||||
def replaced_max_content_width(box, outer=True):
|
||||
"""Return the max-content width for an ``InlineReplacedBox``."""
|
||||
width = box.style['width']
|
||||
if width == 'auto':
|
||||
height = box.style['height']
|
||||
if height == 'auto' or height.unit == '%':
|
||||
height = 'auto'
|
||||
else:
|
||||
assert height.unit == 'px'
|
||||
height = height.value
|
||||
image = box.replacement
|
||||
intrinsic_width, intrinsic_height, intrinsic_ratio = (
|
||||
image.get_intrinsic_size(
|
||||
box.style['image_resolution'], box.style['font_size']))
|
||||
width, _ = default_image_sizing(
|
||||
intrinsic_width, intrinsic_height, intrinsic_ratio, 'auto', height,
|
||||
default_width=300, default_height=150)
|
||||
elif box.style['width'].unit == '%':
|
||||
# See https://drafts.csswg.org/css-sizing/#intrinsic-contribution
|
||||
width = 0
|
||||
else:
|
||||
assert width.unit == 'px'
|
||||
width = width.value
|
||||
return adjust(box, outer, width)
|
||||
|
||||
|
||||
def flex_min_content_width(context, box, outer=True):
|
||||
"""Return the min-content width for an ``FlexContainerBox``."""
|
||||
# TODO: use real values, see
|
||||
# https://www.w3.org/TR/css-flexbox-1/#intrinsic-sizes
|
||||
min_contents = [
|
||||
min_content_width(context, child)
|
||||
for child in box.children if child.is_flex_item]
|
||||
if not min_contents:
|
||||
return adjust(box, outer, 0)
|
||||
if (box.style['flex_direction'].startswith('row') and
|
||||
box.style['flex_wrap'] == 'nowrap'):
|
||||
return adjust(box, outer, sum(min_contents))
|
||||
else:
|
||||
return adjust(box, outer, max(min_contents))
|
||||
|
||||
|
||||
def flex_max_content_width(context, box, outer=True):
|
||||
"""Return the max-content width for an ``FlexContainerBox``."""
|
||||
# TODO: use real values, see
|
||||
# https://www.w3.org/TR/css-flexbox-1/#intrinsic-sizes
|
||||
max_contents = [
|
||||
max_content_width(context, child)
|
||||
for child in box.children if child.is_flex_item]
|
||||
if not max_contents:
|
||||
return adjust(box, outer, 0)
|
||||
if box.style['flex_direction'].startswith('row'):
|
||||
return adjust(box, outer, sum(max_contents))
|
||||
else:
|
||||
return adjust(box, outer, max(max_contents))
|
||||
|
||||
|
||||
def trailing_whitespace_size(context, box):
|
||||
"""Return the size of the trailing whitespace of ``box``."""
|
||||
from .inline import split_first_line, split_text_box
|
||||
|
||||
while isinstance(box, (boxes.InlineBox, boxes.LineBox)):
|
||||
if not box.children:
|
||||
return 0
|
||||
box = box.children[-1]
|
||||
if not (isinstance(box, boxes.TextBox) and box.text and
|
||||
box.style['white_space'] in ('normal', 'nowrap', 'pre-line')):
|
||||
return 0
|
||||
stripped_text = box.text.rstrip(' ')
|
||||
if box.style['font_size'] == 0 or len(stripped_text) == len(box.text):
|
||||
return 0
|
||||
if stripped_text:
|
||||
resume = 0
|
||||
while resume is not None:
|
||||
old_resume = resume
|
||||
old_box, resume, _ = split_text_box(context, box, None, resume)
|
||||
assert old_box
|
||||
stripped_box = box.copy_with_text(stripped_text)
|
||||
stripped_box, resume, _ = split_text_box(
|
||||
context, stripped_box, None, old_resume)
|
||||
if stripped_box is None:
|
||||
# old_box split just before the trailing spaces
|
||||
return old_box.width
|
||||
else:
|
||||
assert resume is None
|
||||
return old_box.width - stripped_box.width
|
||||
else:
|
||||
_, _, _, width, _, _ = split_first_line(
|
||||
box.text, box.style, context, None, box.justification_spacing)
|
||||
return width
|
||||
301
app/.venv/Lib/site-packages/weasyprint/layout/replaced.py
Normal file
301
app/.venv/Lib/site-packages/weasyprint/layout/replaced.py
Normal file
@@ -0,0 +1,301 @@
|
||||
"""Layout for images and other replaced elements.
|
||||
|
||||
See https://drafts.csswg.org/css-images-3/#sizing
|
||||
|
||||
"""
|
||||
|
||||
from .min_max import handle_min_max_height, handle_min_max_width
|
||||
from .percent import percentage
|
||||
|
||||
|
||||
def default_image_sizing(intrinsic_width, intrinsic_height, intrinsic_ratio,
|
||||
specified_width, specified_height,
|
||||
default_width, default_height):
|
||||
"""Default sizing algorithm for the concrete object size.
|
||||
|
||||
Return a ``(concrete_width, concrete_height)`` tuple.
|
||||
|
||||
See https://drafts.csswg.org/css-images-3/#default-sizing
|
||||
|
||||
"""
|
||||
if specified_width == 'auto':
|
||||
specified_width = None
|
||||
if specified_height == 'auto':
|
||||
specified_height = None
|
||||
|
||||
if specified_width is not None and specified_height is not None:
|
||||
return specified_width, specified_height
|
||||
elif specified_width is not None:
|
||||
return specified_width, (
|
||||
specified_width / intrinsic_ratio if intrinsic_ratio is not None
|
||||
else intrinsic_height if intrinsic_height is not None
|
||||
else default_height)
|
||||
elif specified_height is not None:
|
||||
return (
|
||||
specified_height * intrinsic_ratio if intrinsic_ratio is not None
|
||||
else intrinsic_width if intrinsic_width is not None
|
||||
else default_width
|
||||
), specified_height
|
||||
else:
|
||||
if intrinsic_width is not None or intrinsic_height is not None:
|
||||
return default_image_sizing(
|
||||
intrinsic_width, intrinsic_height, intrinsic_ratio,
|
||||
intrinsic_width, intrinsic_height, default_width,
|
||||
default_height)
|
||||
else:
|
||||
return contain_constraint_image_sizing(
|
||||
default_width, default_height, intrinsic_ratio)
|
||||
|
||||
|
||||
def contain_constraint_image_sizing(constraint_width, constraint_height,
|
||||
intrinsic_ratio):
|
||||
"""Contain constraint sizing algorithm for the concrete object size.
|
||||
|
||||
Return a ``(concrete_width, concrete_height)`` tuple.
|
||||
|
||||
See https://drafts.csswg.org/css-images-3/#contain-constraint
|
||||
|
||||
"""
|
||||
return _constraint_image_sizing(
|
||||
constraint_width, constraint_height, intrinsic_ratio, cover=False)
|
||||
|
||||
|
||||
def cover_constraint_image_sizing(constraint_width, constraint_height,
|
||||
intrinsic_ratio):
|
||||
"""Cover constraint sizing algorithm for the concrete object size.
|
||||
|
||||
Return a ``(concrete_width, concrete_height)`` tuple.
|
||||
|
||||
See https://drafts.csswg.org/css-images-3/#cover-constraint
|
||||
|
||||
"""
|
||||
return _constraint_image_sizing(
|
||||
constraint_width, constraint_height, intrinsic_ratio, cover=True)
|
||||
|
||||
|
||||
def _constraint_image_sizing(constraint_width, constraint_height,
|
||||
intrinsic_ratio, cover):
|
||||
if intrinsic_ratio is None:
|
||||
return constraint_width, constraint_height
|
||||
elif cover ^ (constraint_width > constraint_height * intrinsic_ratio):
|
||||
return constraint_height * intrinsic_ratio, constraint_height
|
||||
else:
|
||||
return constraint_width, constraint_width / intrinsic_ratio
|
||||
|
||||
|
||||
def replacedbox_layout(box):
|
||||
# TODO: respect box-sizing ?
|
||||
object_fit = box.style['object_fit']
|
||||
position = box.style['object_position']
|
||||
|
||||
image = box.replacement
|
||||
intrinsic_width, intrinsic_height, intrinsic_ratio = (
|
||||
image.get_intrinsic_size(
|
||||
box.style['image_resolution'], box.style['font_size']))
|
||||
if None in (intrinsic_width, intrinsic_height):
|
||||
intrinsic_width, intrinsic_height = contain_constraint_image_sizing(
|
||||
box.width, box.height, intrinsic_ratio)
|
||||
|
||||
if object_fit == 'fill':
|
||||
draw_width, draw_height = box.width, box.height
|
||||
else:
|
||||
if object_fit in ('contain', 'scale-down'):
|
||||
draw_width, draw_height = contain_constraint_image_sizing(
|
||||
box.width, box.height, intrinsic_ratio)
|
||||
elif object_fit == 'cover':
|
||||
draw_width, draw_height = cover_constraint_image_sizing(
|
||||
box.width, box.height, intrinsic_ratio)
|
||||
else:
|
||||
assert object_fit == 'none', object_fit
|
||||
draw_width, draw_height = intrinsic_width, intrinsic_height
|
||||
|
||||
if object_fit == 'scale-down':
|
||||
draw_width = min(draw_width, intrinsic_width)
|
||||
draw_height = min(draw_height, intrinsic_height)
|
||||
|
||||
origin_x, position_x, origin_y, position_y = position[0]
|
||||
ref_x = box.width - draw_width
|
||||
ref_y = box.height - draw_height
|
||||
|
||||
position_x = percentage(position_x, ref_x)
|
||||
position_y = percentage(position_y, ref_y)
|
||||
if origin_x == 'right':
|
||||
position_x = ref_x - position_x
|
||||
if origin_y == 'bottom':
|
||||
position_y = ref_y - position_y
|
||||
|
||||
position_x += box.content_box_x()
|
||||
position_y += box.content_box_y()
|
||||
|
||||
return draw_width, draw_height, position_x, position_y
|
||||
|
||||
|
||||
@handle_min_max_width
|
||||
def replaced_box_width(box, containing_block):
|
||||
"""Set the used width for replaced boxes."""
|
||||
from .block import block_level_width
|
||||
|
||||
width, height, ratio = box.replacement.get_intrinsic_size(
|
||||
box.style['image_resolution'], box.style['font_size'])
|
||||
|
||||
# This algorithm simply follows the different points of the specification:
|
||||
# https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
|
||||
if box.height == box.width == 'auto':
|
||||
if width is not None:
|
||||
# Point #1
|
||||
box.width = width
|
||||
elif ratio is not None:
|
||||
if height is not None:
|
||||
# Point #2 first part
|
||||
box.width = height * ratio
|
||||
else:
|
||||
# Point #3
|
||||
block_level_width(box, containing_block)
|
||||
|
||||
if box.width == 'auto':
|
||||
if ratio is not None:
|
||||
# Point #2 second part
|
||||
box.width = box.height * ratio
|
||||
elif width is not None:
|
||||
# Point #4
|
||||
box.width = width
|
||||
else:
|
||||
# Point #5
|
||||
# It's pretty useless to rely on device size to set width.
|
||||
box.width = 300
|
||||
|
||||
|
||||
@handle_min_max_height
|
||||
def replaced_box_height(box):
|
||||
"""Compute and set the used height for replaced boxes."""
|
||||
# https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
|
||||
width, height, ratio = box.replacement.get_intrinsic_size(
|
||||
box.style['image_resolution'], box.style['font_size'])
|
||||
|
||||
# Test 'auto' on the computed width, not the used width
|
||||
if box.height == box.width == 'auto':
|
||||
box.height = height
|
||||
elif box.height == 'auto' and ratio:
|
||||
box.height = box.width / ratio
|
||||
|
||||
if box.height == box.width == 'auto' and height is not None:
|
||||
box.height = height
|
||||
elif ratio is not None and box.height == 'auto':
|
||||
box.height = box.width / ratio
|
||||
elif box.height == 'auto' and height is not None:
|
||||
box.height = height
|
||||
elif box.height == 'auto':
|
||||
# It's pretty useless to rely on device size to set width.
|
||||
box.height = 150
|
||||
|
||||
|
||||
def inline_replaced_box_layout(box, containing_block):
|
||||
"""Lay out an inline :class:`boxes.ReplacedBox` ``box``."""
|
||||
for side in ('top', 'right', 'bottom', 'left'):
|
||||
if getattr(box, f'margin_{side}') == 'auto':
|
||||
setattr(box, f'margin_{side}', 0)
|
||||
inline_replaced_box_width_height(box, containing_block)
|
||||
|
||||
|
||||
def inline_replaced_box_width_height(box, containing_block):
|
||||
if box.style['width'] == box.style['height'] == 'auto':
|
||||
replaced_box_width.without_min_max(box, containing_block)
|
||||
replaced_box_height.without_min_max(box)
|
||||
min_max_auto_replaced(box)
|
||||
else:
|
||||
replaced_box_width(box, containing_block)
|
||||
replaced_box_height(box)
|
||||
|
||||
|
||||
def min_max_auto_replaced(box):
|
||||
"""Resolve min/max constraints on replaced elements with 'auto' sizes."""
|
||||
width = box.width
|
||||
height = box.height
|
||||
min_width = box.min_width
|
||||
min_height = box.min_height
|
||||
max_width = max(min_width, box.max_width)
|
||||
max_height = max(min_height, box.max_height)
|
||||
|
||||
# (violation_width, violation_height)
|
||||
violations = (
|
||||
'min' if width < min_width else 'max' if width > max_width else '',
|
||||
'min' if height < min_height else 'max' if height > max_height else '')
|
||||
|
||||
# Work around divisions by zero. These are pathological cases anyway.
|
||||
# TODO: is there a cleaner way?
|
||||
if width == 0:
|
||||
width = 1e-6
|
||||
if height == 0:
|
||||
height = 1e-6
|
||||
|
||||
# ('', ''): nothing to do
|
||||
if violations == ('max', ''):
|
||||
box.width = max_width
|
||||
box.height = max(max_width * height / width, min_height)
|
||||
elif violations == ('min', ''):
|
||||
box.width = min_width
|
||||
box.height = min(min_width * height / width, max_height)
|
||||
elif violations == ('', 'max'):
|
||||
box.width = max(max_height * width / height, min_width)
|
||||
box.height = max_height
|
||||
elif violations == ('', 'min'):
|
||||
box.width = min(min_height * width / height, max_width)
|
||||
box.height = min_height
|
||||
elif violations == ('max', 'max'):
|
||||
if max_width / width <= max_height / height:
|
||||
box.width = max_width
|
||||
box.height = max(min_height, max_width * height / width)
|
||||
else:
|
||||
box.width = max(min_width, max_height * width / height)
|
||||
box.height = max_height
|
||||
elif violations == ('min', 'min'):
|
||||
if min_width / width <= min_height / height:
|
||||
box.width = min(max_width, min_height * width / height)
|
||||
box.height = min_height
|
||||
else:
|
||||
box.width = min_width
|
||||
box.height = min(max_height, min_width * height / width)
|
||||
elif violations == ('min', 'max'):
|
||||
box.width = min_width
|
||||
box.height = max_height
|
||||
elif violations == ('max', 'min'):
|
||||
box.width = max_width
|
||||
box.height = min_height
|
||||
|
||||
|
||||
def block_replaced_box_layout(context, box, containing_block):
|
||||
"""Lay out the block :class:`boxes.ReplacedBox` ``box``."""
|
||||
from .block import block_level_width
|
||||
from .float import avoid_collisions
|
||||
|
||||
box = box.copy()
|
||||
if box.style['width'] == box.style['height'] == 'auto':
|
||||
computed_margins = box.margin_left, box.margin_right
|
||||
block_replaced_width.without_min_max(
|
||||
box, containing_block)
|
||||
replaced_box_height.without_min_max(box)
|
||||
min_max_auto_replaced(box)
|
||||
box.margin_left, box.margin_right = computed_margins
|
||||
block_level_width.without_min_max(box, containing_block)
|
||||
else:
|
||||
block_replaced_width(box, containing_block)
|
||||
replaced_box_height(box)
|
||||
|
||||
# Don't collide with floats
|
||||
# https://www.w3.org/TR/CSS21/visuren.html#floats
|
||||
box.position_x, box.position_y, _ = avoid_collisions(
|
||||
context, box, containing_block, outer=False)
|
||||
resume_at = None
|
||||
next_page = {'break': 'any', 'page': None}
|
||||
adjoining_margins = []
|
||||
collapsing_through = False
|
||||
return box, resume_at, next_page, adjoining_margins, collapsing_through
|
||||
|
||||
|
||||
@handle_min_max_width
|
||||
def block_replaced_width(box, containing_block):
|
||||
from .block import block_level_width
|
||||
|
||||
# https://www.w3.org/TR/CSS21/visudet.html#block-replaced-width
|
||||
replaced_box_width.without_min_max(box, containing_block)
|
||||
block_level_width.without_min_max(box, containing_block)
|
||||
1076
app/.venv/Lib/site-packages/weasyprint/layout/table.py
Normal file
1076
app/.venv/Lib/site-packages/weasyprint/layout/table.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user