mirror of
https://github.com/Kozea/WeasyPrint.git
synced 2024-10-05 00:21:15 +03:00
Merge branch 'float' into 'master'
This commit is contained in:
commit
80af286655
@ -45,6 +45,11 @@ if sys.version_info[0] >= 3:
|
||||
data = data.decode('utf8')
|
||||
return email.message_from_string(data)
|
||||
|
||||
|
||||
def ints_from_bytes(byte_string):
|
||||
"""Return a list of ints from a byte string"""
|
||||
return list(byte_string)
|
||||
|
||||
else:
|
||||
# Python 2
|
||||
from urlparse import urljoin, urlsplit, parse_qs
|
||||
@ -82,3 +87,8 @@ else:
|
||||
if isinstance(data, unicode):
|
||||
data = data.encode('utf8')
|
||||
return email.message_from_string(data)
|
||||
|
||||
|
||||
def ints_from_bytes(byte_string):
|
||||
"""Return a list of ints from a byte string"""
|
||||
return map(ord, byte_string)
|
||||
|
@ -255,6 +255,7 @@ TABLE_WRAPPER_BOX_PROPERTIES = set('''
|
||||
transform_origin
|
||||
vertical_align
|
||||
z_index
|
||||
clear
|
||||
'''.split())
|
||||
|
||||
|
||||
|
@ -341,6 +341,13 @@ def caption_side(keyword):
|
||||
return keyword in ('top', 'bottom')
|
||||
|
||||
|
||||
@validator()
|
||||
@single_keyword
|
||||
def clear(keyword):
|
||||
"""``clear`` property validation."""
|
||||
return keyword in ('left', 'right', 'both', 'none')
|
||||
|
||||
|
||||
@validator()
|
||||
@single_token
|
||||
def clip(token):
|
||||
@ -509,6 +516,13 @@ def display(keyword):
|
||||
'table-row', 'table-column-group', 'table-column', 'table-cell')
|
||||
|
||||
|
||||
@validator()
|
||||
@single_keyword
|
||||
def float(keyword):
|
||||
"""``float`` property validation."""
|
||||
return keyword in ('left', 'right', 'none')
|
||||
|
||||
|
||||
@validator()
|
||||
def font_family(tokens):
|
||||
"""``font-family`` property validation."""
|
||||
|
@ -38,6 +38,9 @@ class Document(object):
|
||||
self._computed_styles = None
|
||||
self._formatting_structure = None
|
||||
self._pages = None
|
||||
self._excluded_shapes_lists = []
|
||||
|
||||
self.create_block_formatting_context()
|
||||
|
||||
# TODO: remove this when Margin boxes variable dimension is correct.
|
||||
self._auto_margin_boxes_warning_shown = False
|
||||
@ -100,6 +103,22 @@ class Document(object):
|
||||
draw.draw_page(self, page, context)
|
||||
yield width, height, surface
|
||||
|
||||
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):
|
||||
excluded_shapes = self._excluded_shapes_lists.pop()
|
||||
self.excluded_shapes = self._excluded_shapes_lists[-1]
|
||||
|
||||
# See http://www.w3.org/TR/CSS2/visudet.html#root-height
|
||||
if root_box.style.height == 'auto':
|
||||
box_bottom = root_box.content_box_y() + root_box.height
|
||||
for shape in excluded_shapes:
|
||||
shape_bottom = shape.position_y + shape.margin_height()
|
||||
if shape_bottom > box_bottom:
|
||||
root_box.height += shape_bottom - box_bottom
|
||||
|
||||
def get_png_pages(self, resolution=None):
|
||||
"""Yield (width, height, png_bytes) tuples, one for each page."""
|
||||
for width, height, surface in self.get_png_surfaces(resolution):
|
||||
|
@ -291,6 +291,7 @@ class BlockLevelBox(Box):
|
||||
``table`` generates a block-level box.
|
||||
|
||||
"""
|
||||
clearance = None
|
||||
|
||||
|
||||
class BlockContainerBox(ParentBox):
|
||||
|
@ -663,7 +663,8 @@ def inline_in_block(box):
|
||||
assert not isinstance(child_box, boxes.LineBox)
|
||||
if new_line_children and child_box.is_absolutely_positioned():
|
||||
new_line_children.append(child_box)
|
||||
elif isinstance(child_box, boxes.InlineLevelBox):
|
||||
elif isinstance(child_box, boxes.InlineLevelBox) or (
|
||||
new_line_children and child_box.is_floated()):
|
||||
# Do not append white space at the start of a line:
|
||||
# It would be removed during layout.
|
||||
if new_line_children or not (
|
||||
@ -769,7 +770,8 @@ def block_in_inline(box):
|
||||
'siblings at this stage, got %r.' % box.children)
|
||||
stack = None
|
||||
while 1:
|
||||
new_line, block, stack = _inner_block_in_inline(child, stack)
|
||||
new_line, block, stack = _inner_block_in_inline(
|
||||
child, skip_stack=stack)
|
||||
if block is None:
|
||||
break
|
||||
anon = boxes.BlockBox.anonymous_from(box, [new_line])
|
||||
@ -833,12 +835,7 @@ def _inner_block_in_inline(box, skip_stack=None):
|
||||
new_child, block_level_box, resume_at = recursion
|
||||
else:
|
||||
assert skip_stack is None # Should not skip here
|
||||
if isinstance(child, boxes.ParentBox):
|
||||
# inline-block or inline-table.
|
||||
new_child = block_in_inline(child)
|
||||
else:
|
||||
# text or replaced box
|
||||
new_child = child
|
||||
# block_level_box is still None.
|
||||
if new_child is not child:
|
||||
changed = True
|
||||
|
@ -13,6 +13,7 @@ from __future__ import division, unicode_literals
|
||||
from .percentages import resolve_percentages, resolve_position_percentages
|
||||
from .preferred import shrink_to_fit
|
||||
from .markers import list_marker_layout
|
||||
from .min_max import handle_min_max_width
|
||||
from .tables import table_wrapper_width
|
||||
from ..formatting_structure import boxes
|
||||
|
||||
@ -50,67 +51,27 @@ class AbsolutePlaceholder(object):
|
||||
setattr(self._box, name, value)
|
||||
|
||||
|
||||
def absolute_layout(document, placeholder, containing_block, fixed_boxes):
|
||||
"""Set the width of absolute positioned ``box``."""
|
||||
box = placeholder._box
|
||||
|
||||
cb = containing_block
|
||||
# TODO: handle inline boxes (point 10.1.4.1)
|
||||
# http://www.w3.org/TR/CSS2/visudet.html#containing-block-details
|
||||
if isinstance(containing_block, boxes.PageBox):
|
||||
cb_x = cb.content_box_x()
|
||||
cb_y = cb.content_box_y()
|
||||
cb_width = cb.width
|
||||
cb_height = cb.height
|
||||
else:
|
||||
cb_x = cb.padding_box_x()
|
||||
cb_y = cb.padding_box_y()
|
||||
cb_width = cb.padding_width()
|
||||
cb_height = cb.padding_height()
|
||||
containing_block = cb_x, cb_y, cb_width, cb_height
|
||||
|
||||
resolve_percentages(box, (cb_width, cb_height))
|
||||
resolve_position_percentages(box, (cb_width, cb_height))
|
||||
|
||||
# Absolute tables are wrapped into block boxes
|
||||
if isinstance(box, boxes.BlockBox):
|
||||
new_box = absolute_block(document, box, containing_block, fixed_boxes)
|
||||
else:
|
||||
assert isinstance(box, boxes.BlockReplacedBox)
|
||||
new_box = absolute_replaced(document, box, containing_block)
|
||||
|
||||
placeholder.set_laid_out_box(new_box)
|
||||
|
||||
|
||||
def absolute_block(document, box, containing_block, fixed_boxes):
|
||||
@handle_min_max_width
|
||||
def absolute_width(box, document, containing_block):
|
||||
# http://www.w3.org/TR/CSS2/visudet.html#abs-replaced-width
|
||||
|
||||
# These names are waaay too long
|
||||
margin_l = box.margin_left
|
||||
margin_r = box.margin_right
|
||||
margin_t = box.margin_top
|
||||
margin_b = box.margin_bottom
|
||||
padding_l = box.padding_left
|
||||
padding_r = box.padding_right
|
||||
padding_t = box.padding_top
|
||||
padding_b = box.padding_bottom
|
||||
border_l = box.border_left_width
|
||||
border_r = box.border_right_width
|
||||
border_t = box.border_top_width
|
||||
border_b = box.border_bottom_width
|
||||
width = box.width
|
||||
height = box.height
|
||||
left = box.left
|
||||
right = box.right
|
||||
top = box.top
|
||||
bottom = box.bottom
|
||||
|
||||
cb_x, cb_y, cb_width, cb_height = containing_block
|
||||
|
||||
# TODO: handle bidi
|
||||
padding_plus_borders_x = padding_l + padding_r + border_l + border_r
|
||||
translate_x = translate_y = 0
|
||||
translate_box_width = translate_box_height = False
|
||||
translate_x = 0
|
||||
translate_box_width = False
|
||||
default_translate_x = cb_x - box.position_x
|
||||
if left == right == width == 'auto':
|
||||
if margin_l == 'auto':
|
||||
@ -161,9 +122,28 @@ def absolute_block(document, box, containing_block, fixed_boxes):
|
||||
elif right == 'auto':
|
||||
translate_x = left + default_translate_x
|
||||
|
||||
return translate_box_width, translate_x
|
||||
|
||||
|
||||
def absolute_height(box, document, containing_block):
|
||||
# These names are waaay too long
|
||||
margin_t = box.margin_top
|
||||
margin_b = box.margin_bottom
|
||||
padding_t = box.padding_top
|
||||
padding_b = box.padding_bottom
|
||||
border_t = box.border_top_width
|
||||
border_b = box.border_bottom_width
|
||||
height = box.height
|
||||
top = box.top
|
||||
bottom = box.bottom
|
||||
|
||||
cb_x, cb_y, cb_width, cb_height = containing_block
|
||||
|
||||
# http://www.w3.org/TR/CSS2/visudet.html#abs-non-replaced-height
|
||||
|
||||
paddings_plus_borders_y = padding_t + padding_b + border_t + border_b
|
||||
translate_y = 0
|
||||
translate_box_height = False
|
||||
default_translate_y = cb_y - box.position_y
|
||||
if top == bottom == height == 'auto':
|
||||
pass # Keep the static position
|
||||
@ -201,6 +181,17 @@ def absolute_block(document, box, containing_block, fixed_boxes):
|
||||
elif bottom == 'auto':
|
||||
translate_y = top + default_translate_y
|
||||
|
||||
return translate_box_height, translate_y
|
||||
|
||||
|
||||
def absolute_block(document, box, containing_block, fixed_boxes):
|
||||
cb_x, cb_y, cb_width, cb_height = containing_block
|
||||
|
||||
translate_box_width, translate_x = absolute_width(
|
||||
box, document, containing_block)
|
||||
translate_box_height, translate_y = absolute_height(
|
||||
box, document, containing_block)
|
||||
|
||||
# This box is the containing block for absolute descendants.
|
||||
absolute_boxes = []
|
||||
|
||||
@ -232,6 +223,40 @@ def absolute_block(document, box, containing_block, fixed_boxes):
|
||||
return new_box
|
||||
|
||||
|
||||
def absolute_layout(document, placeholder, containing_block, fixed_boxes):
|
||||
"""Set the width of absolute positioned ``box``."""
|
||||
box = placeholder._box
|
||||
|
||||
cb = containing_block
|
||||
# TODO: handle inline boxes (point 10.1.4.1)
|
||||
# http://www.w3.org/TR/CSS2/visudet.html#containing-block-details
|
||||
if isinstance(containing_block, boxes.PageBox):
|
||||
cb_x = cb.content_box_x()
|
||||
cb_y = cb.content_box_y()
|
||||
cb_width = cb.width
|
||||
cb_height = cb.height
|
||||
else:
|
||||
cb_x = cb.padding_box_x()
|
||||
cb_y = cb.padding_box_y()
|
||||
cb_width = cb.padding_width()
|
||||
cb_height = cb.padding_height()
|
||||
containing_block = cb_x, cb_y, cb_width, cb_height
|
||||
|
||||
resolve_percentages(box, (cb_width, cb_height))
|
||||
resolve_position_percentages(box, (cb_width, cb_height))
|
||||
|
||||
document.create_block_formatting_context()
|
||||
# Absolute tables are wrapped into block boxes
|
||||
if isinstance(box, boxes.BlockBox):
|
||||
new_box = absolute_block(document, box, containing_block, fixed_boxes)
|
||||
else:
|
||||
assert isinstance(box, boxes.BlockReplacedBox)
|
||||
new_box = absolute_replaced(document, box, containing_block)
|
||||
document.finish_block_formatting_context(new_box)
|
||||
|
||||
placeholder.set_laid_out_box(new_box)
|
||||
|
||||
|
||||
def absolute_replaced(document, box, containing_block):
|
||||
# avoid a circular import
|
||||
from .inlines import inline_replaced_box_width_height
|
||||
|
@ -13,10 +13,11 @@
|
||||
from __future__ import division, unicode_literals
|
||||
|
||||
from .absolute import absolute_layout, AbsolutePlaceholder
|
||||
from .float import float_layout, get_clearance, avoid_collisions
|
||||
from .inlines import (iter_line_boxes, replaced_box_width, replaced_box_height,
|
||||
handle_min_max_width, min_max_replaced_height,
|
||||
min_max_auto_replaced)
|
||||
min_max_replaced_height, min_max_auto_replaced)
|
||||
from .markers import list_marker_layout
|
||||
from .min_max import handle_min_max_width
|
||||
from .tables import table_layout, table_wrapper_width
|
||||
from .percentages import resolve_percentages, resolve_position_percentages
|
||||
from ..formatting_structure import boxes
|
||||
@ -37,12 +38,31 @@ def block_level_layout(document, box, max_position_y, skip_stack,
|
||||
return table_layout(
|
||||
document, box, max_position_y, skip_stack, containing_block,
|
||||
device_size, page_is_empty, absolute_boxes, fixed_boxes)
|
||||
elif isinstance(box, boxes.BlockBox):
|
||||
|
||||
resolve_percentages(box, containing_block)
|
||||
|
||||
if box.margin_top == 'auto':
|
||||
box.margin_top = 0
|
||||
if box.margin_bottom == 'auto':
|
||||
box.margin_bottom = 0
|
||||
|
||||
collapsed_margin = collapse_margin(adjoining_margins + [box.margin_top])
|
||||
box.clearance = get_clearance(document, box, collapsed_margin)
|
||||
if box.clearance is not None:
|
||||
top_border_edge = box.position_y + collapsed_margin + box.clearance
|
||||
box.position_y = top_border_edge - box.margin_top
|
||||
adjoining_margins = []
|
||||
|
||||
if isinstance(box, boxes.BlockBox):
|
||||
return block_box_layout(document, box, max_position_y, skip_stack,
|
||||
containing_block, device_size, page_is_empty,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins)
|
||||
elif isinstance(box, boxes.BlockReplacedBox):
|
||||
box = block_replaced_box_layout(box, containing_block, device_size)
|
||||
# Don't collide with floats
|
||||
# http://www.w3.org/TR/CSS21/visuren.html#floats
|
||||
box.position_x, box.position_y, _ = avoid_collisions(
|
||||
document, box, containing_block, outer=False)
|
||||
resume_at = None
|
||||
next_page = 'any'
|
||||
adjoining_margins = []
|
||||
@ -56,15 +76,22 @@ def block_box_layout(document, box, max_position_y, skip_stack,
|
||||
containing_block, device_size, page_is_empty,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins):
|
||||
"""Lay out the block ``box``."""
|
||||
resolve_percentages(box, containing_block)
|
||||
if box.is_table_wrapper:
|
||||
table_wrapper_width(
|
||||
document, box, (containing_block.width, containing_block.height))
|
||||
block_level_width(box, containing_block)
|
||||
|
||||
new_box, resume_at, next_page, adjoining_margins, collapsing_through = \
|
||||
block_container_layout(
|
||||
document, box, max_position_y, skip_stack, device_size,
|
||||
page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins)
|
||||
if new_box and new_box.is_table_wrapper:
|
||||
# Don't collide with floats
|
||||
# http://www.w3.org/TR/CSS21/visuren.html#floats
|
||||
position_x, position_y, _ = avoid_collisions(
|
||||
document, new_box, containing_block, outer=False)
|
||||
new_box.translate(
|
||||
position_x - new_box.position_x, position_y - new_box.position_y)
|
||||
list_marker_layout(document, new_box)
|
||||
return new_box, resume_at, next_page, adjoining_margins, collapsing_through
|
||||
|
||||
@ -79,12 +106,6 @@ min_max_block_replaced_width = handle_min_max_width(block_replaced_width)
|
||||
|
||||
def block_replaced_box_layout(box, containing_block, device_size):
|
||||
"""Lay out the block :class:`boxes.ReplacedBox` ``box``."""
|
||||
resolve_percentages(box, containing_block)
|
||||
if box.margin_top == 'auto':
|
||||
box.margin_top = 0
|
||||
if box.margin_bottom == 'auto':
|
||||
box.margin_bottom = 0
|
||||
|
||||
if box.style.width == 'auto' and box.style.height == 'auto':
|
||||
block_replaced_width(box, containing_block, device_size)
|
||||
replaced_box_height(box, device_size)
|
||||
@ -198,10 +219,9 @@ def block_container_layout(document, box, max_position_y, skip_stack,
|
||||
#if box.style.overflow != 'visible':
|
||||
# ...
|
||||
|
||||
if box.margin_top == 'auto':
|
||||
box.margin_top = 0
|
||||
if box.margin_bottom == 'auto':
|
||||
box.margin_bottom = 0
|
||||
# See http://www.w3.org/TR/CSS21/visuren.html#block-formatting
|
||||
if not isinstance(box, boxes.BlockBox):
|
||||
document.create_block_formatting_context()
|
||||
|
||||
if adjoining_margins is None:
|
||||
adjoining_margins = []
|
||||
@ -228,6 +248,8 @@ def block_container_layout(document, box, max_position_y, skip_stack,
|
||||
new_children = []
|
||||
next_page = 'any'
|
||||
|
||||
last_in_flow_child = None
|
||||
|
||||
is_start = skip_stack is None
|
||||
if is_start:
|
||||
skip = 0
|
||||
@ -239,8 +261,8 @@ def block_container_layout(document, box, max_position_y, skip_stack,
|
||||
child.position_y = position_y
|
||||
|
||||
if not child.is_in_normal_flow():
|
||||
if child.is_absolutely_positioned():
|
||||
child.position_y += collapse_margin(adjoining_margins)
|
||||
if child.is_absolutely_positioned():
|
||||
placeholder = AbsolutePlaceholder(child)
|
||||
new_children.append(placeholder)
|
||||
if child.style.position == 'absolute':
|
||||
@ -248,7 +270,8 @@ def block_container_layout(document, box, max_position_y, skip_stack,
|
||||
else:
|
||||
fixed_boxes.append(placeholder)
|
||||
elif child.is_floated():
|
||||
# TODO: Floats
|
||||
child = float_layout(
|
||||
document, child, box, absolute_boxes, fixed_boxes)
|
||||
new_children.append(child)
|
||||
continue
|
||||
|
||||
@ -265,7 +288,7 @@ def block_container_layout(document, box, max_position_y, skip_stack,
|
||||
is_page_break = False
|
||||
for line, resume_at in lines_iterator:
|
||||
line.resume_at = resume_at
|
||||
new_position_y = position_y + line.height
|
||||
new_position_y = line.position_y + line.height
|
||||
# Allow overflow if the first line of the page is higher
|
||||
# than the page itself so that we put *something* on this
|
||||
# page and can advance in the document.
|
||||
@ -332,6 +355,39 @@ def block_container_layout(document, box, max_position_y, skip_stack,
|
||||
page_break = 'auto'
|
||||
|
||||
new_containing_block = box
|
||||
|
||||
if not new_containing_block.is_table_wrapper:
|
||||
# TODO: there's no collapsing margins inside tables, right?
|
||||
resolve_percentages(child, new_containing_block)
|
||||
if (child.is_in_normal_flow() and last_in_flow_child is None
|
||||
and collapsing_with_children):
|
||||
# TODO: add the adjoining descendants' margin top to
|
||||
# [child.margin_top]
|
||||
old_collapsed_margin = collapse_margin(adjoining_margins)
|
||||
if child.margin_top == 'auto':
|
||||
child_margin_top = 0
|
||||
else:
|
||||
child_margin_top = child.margin_top
|
||||
new_collapsed_margin = collapse_margin(
|
||||
adjoining_margins + [child_margin_top])
|
||||
collapsed_margin_difference = (
|
||||
new_collapsed_margin - old_collapsed_margin)
|
||||
for previous_new_child in new_children:
|
||||
previous_new_child.translate(
|
||||
dy=collapsed_margin_difference)
|
||||
clearance = get_clearance(
|
||||
document, child, new_collapsed_margin)
|
||||
if clearance is not None:
|
||||
for previous_new_child in new_children:
|
||||
previous_new_child.translate(
|
||||
dy=-collapsed_margin_difference)
|
||||
|
||||
collapsed_margin = collapse_margin(adjoining_margins)
|
||||
box.position_y += collapsed_margin - box.margin_top
|
||||
# Count box.margin_top as we emptied adjoining_margins
|
||||
adjoining_margins = []
|
||||
position_y = box.content_box_y()
|
||||
|
||||
(new_child, resume_at, next_page, next_adjoining_margins,
|
||||
collapsing_through) = block_level_layout(
|
||||
document, child, max_position_y, skip_stack,
|
||||
@ -342,7 +398,7 @@ def block_container_layout(document, box, max_position_y, skip_stack,
|
||||
skip_stack = None
|
||||
|
||||
if new_child is not None:
|
||||
# index in its non-laid-out parent, not in the future new parent.
|
||||
# index in its non-laid-out parent, not in future new parent
|
||||
# May be used in find_earlier_page_break()
|
||||
new_child.index = index
|
||||
|
||||
@ -373,6 +429,10 @@ def block_container_layout(document, box, max_position_y, skip_stack,
|
||||
else:
|
||||
position_y = new_position_y
|
||||
|
||||
if new_child is not None and new_child.clearance is not None:
|
||||
position_y = (
|
||||
new_child.border_box_y() + new_child.border_height())
|
||||
|
||||
if new_child is None:
|
||||
# Nothing fits in the remaining space of this page: break
|
||||
if page_break == 'avoid':
|
||||
@ -405,7 +465,6 @@ def block_container_layout(document, box, max_position_y, skip_stack,
|
||||
if resume_at is not None:
|
||||
resume_at = (index, resume_at)
|
||||
break
|
||||
|
||||
else:
|
||||
resume_at = None
|
||||
|
||||
@ -414,26 +473,33 @@ def block_container_layout(document, box, max_position_y, skip_stack,
|
||||
return None, None, 'any', [], False
|
||||
|
||||
if collapsing_with_children:
|
||||
# this_adjoining_margins contains box.margin_top
|
||||
border_box_y = box.position_y + collapse_margin(
|
||||
this_box_adjoining_margins)
|
||||
box.position_y = border_box_y - box.margin_top
|
||||
box.position_y += (
|
||||
collapse_margin(this_box_adjoining_margins) - box.margin_top)
|
||||
|
||||
for previous_child in reversed(new_children):
|
||||
if previous_child.is_in_normal_flow():
|
||||
last_in_flow_child = previous_child
|
||||
break
|
||||
else:
|
||||
last_in_flow_child = None
|
||||
collapsing_through = False
|
||||
if new_children:
|
||||
if last_in_flow_child is None:
|
||||
collapsed_margin = collapse_margin(adjoining_margins)
|
||||
# top and bottom margin of this box
|
||||
if (box.height in ('auto', 0) and
|
||||
get_clearance(document, box, collapsed_margin) is None and
|
||||
all(v == 0 for v in [
|
||||
box.min_height, box.border_top_width, box.padding_top,
|
||||
box.border_bottom_width, box.padding_bottom])):
|
||||
collapsing_through = True
|
||||
else:
|
||||
position_y += collapsed_margin
|
||||
adjoining_margins = []
|
||||
else:
|
||||
# bottom margin of the last child and bottom margin of this box ...
|
||||
if box.height != 'auto':
|
||||
# not adjoining. (position_y is not used afterwards.)
|
||||
adjoining_margins = []
|
||||
else:
|
||||
# top and bottom margin of this box
|
||||
if box.height in ('auto', 0) and all(v == 0 for v in [
|
||||
box.min_height, box.border_top_width, box.padding_top,
|
||||
box.border_bottom_width, box.padding_bottom]):
|
||||
collapsing_through = True
|
||||
else:
|
||||
position_y += collapse_margin(adjoining_margins)
|
||||
adjoining_margins = []
|
||||
|
||||
if box.border_bottom_width or box.padding_bottom or (
|
||||
establishes_formatting_context(box) or box.is_for_root_element):
|
||||
@ -447,9 +513,6 @@ def block_container_layout(document, box, max_position_y, skip_stack,
|
||||
# http://www.w3.org/TR/CSS21/visudet.html#normal-block
|
||||
if new_box.height == 'auto':
|
||||
new_box.height = position_y - new_box.content_box_y()
|
||||
new_box.height = max(
|
||||
min(new_box.height, new_box.max_height),
|
||||
new_box.min_height)
|
||||
|
||||
if new_box.style.position == 'relative':
|
||||
# New containing block, resolve the layout of the absolute descendants
|
||||
@ -459,6 +522,14 @@ def block_container_layout(document, box, max_position_y, skip_stack,
|
||||
for child in new_box.children:
|
||||
relative_positioning(child, (new_box.width, new_box.height))
|
||||
|
||||
if not isinstance(new_box, boxes.BlockBox):
|
||||
document.finish_block_formatting_context(new_box)
|
||||
|
||||
# After finish_block_formatting_context which may increment new_box.height
|
||||
new_box.height = max(
|
||||
min(new_box.height, new_box.max_height),
|
||||
new_box.min_height)
|
||||
|
||||
return new_box, resume_at, next_page, adjoining_margins, collapsing_through
|
||||
|
||||
|
||||
|
184
weasyprint/layout/float.py
Normal file
184
weasyprint/layout/float.py
Normal file
@ -0,0 +1,184 @@
|
||||
# coding: utf8
|
||||
"""
|
||||
weasyprint.float
|
||||
----------------
|
||||
|
||||
:copyright: Copyright 2011-2012 Simon Sapin and contributors, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import division, unicode_literals
|
||||
|
||||
from .markers import list_marker_layout
|
||||
from .min_max import handle_min_max_width
|
||||
from .percentages import resolve_percentages, resolve_position_percentages
|
||||
from .preferred import shrink_to_fit
|
||||
from .tables import table_wrapper_width
|
||||
from ..formatting_structure import boxes
|
||||
|
||||
|
||||
@handle_min_max_width
|
||||
def float_width(box, document, containing_block):
|
||||
box.width = shrink_to_fit(document, box, containing_block.width)
|
||||
|
||||
|
||||
def float_layout(document, box, containing_block, absolute_boxes, fixed_boxes):
|
||||
"""Set the width and position of floating ``box``."""
|
||||
# avoid a circular imports
|
||||
from .blocks import block_container_layout
|
||||
from .inlines import inline_replaced_box_width_height
|
||||
|
||||
resolve_percentages(box, (containing_block.width, containing_block.height))
|
||||
resolve_position_percentages(
|
||||
box, (containing_block.width, containing_block.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(document, box)
|
||||
if clearance is not None:
|
||||
box.position_y += clearance
|
||||
|
||||
if isinstance(box, boxes.BlockReplacedBox):
|
||||
inline_replaced_box_width_height(box, device_size=None)
|
||||
elif box.width == 'auto':
|
||||
float_width(box, document, containing_block)
|
||||
|
||||
if box.is_table_wrapper:
|
||||
table_wrapper_width(
|
||||
document, box, (containing_block.width, containing_block.height))
|
||||
|
||||
if isinstance(box, boxes.BlockBox):
|
||||
document.create_block_formatting_context()
|
||||
box, _, _, _, _ = block_container_layout(
|
||||
document, box, max_position_y=float('inf'),
|
||||
skip_stack=None, device_size=None, page_is_empty=False,
|
||||
absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes,
|
||||
adjoining_margins=None)
|
||||
list_marker_layout(document, box)
|
||||
document.finish_block_formatting_context(box)
|
||||
else:
|
||||
assert isinstance(box, boxes.BlockReplacedBox)
|
||||
|
||||
box = find_float_position(document, box, containing_block)
|
||||
|
||||
document.excluded_shapes.append(box)
|
||||
|
||||
return box
|
||||
|
||||
|
||||
def find_float_position(document, box, containing_block):
|
||||
"""Get the right position of the float ``box``."""
|
||||
# See http://www.w3.org/TR/CSS2/visuren.html#dis-pos-flo
|
||||
|
||||
# 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 document.excluded_shapes:
|
||||
highest_y = document.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(
|
||||
document, 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(document, 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 document.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(document, box, containing_block, outer=True):
|
||||
excluded_shapes = document.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 = [
|
||||
shape for shape in excluded_shapes
|
||||
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)
|
||||
]
|
||||
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(left_bounds)
|
||||
if right_bounds:
|
||||
max_right_bound = min(right_bounds)
|
||||
# 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
|
||||
|
||||
position_x = max_left_bound
|
||||
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
|
@ -11,12 +11,13 @@
|
||||
"""
|
||||
|
||||
from __future__ import division, unicode_literals
|
||||
import functools
|
||||
|
||||
from .absolute import absolute_layout, AbsolutePlaceholder
|
||||
from .float import avoid_collisions, float_layout
|
||||
from .markers import image_marker_layout
|
||||
from .min_max import handle_min_max_width, handle_min_max_height
|
||||
from .percentages import resolve_percentages, resolve_one_percentage
|
||||
from .preferred import shrink_to_fit
|
||||
from .preferred import shrink_to_fit, inline_preferred_minimum_width
|
||||
from .tables import find_in_flow_baseline, table_wrapper_width
|
||||
from ..text import split_first_line
|
||||
from ..formatting_structure import boxes
|
||||
@ -44,47 +45,64 @@ def iter_line_boxes(document, box, position_y, skip_stack, containing_block,
|
||||
line, resume_at = get_next_linebox(
|
||||
document, box, position_y, skip_stack, containing_block,
|
||||
device_size, absolute_boxes, fixed_boxes)
|
||||
if line:
|
||||
position_y = line.position_y + line.height
|
||||
if line is None:
|
||||
return
|
||||
yield line, resume_at
|
||||
if resume_at is None:
|
||||
return
|
||||
skip_stack = resume_at
|
||||
position_y += line.height
|
||||
|
||||
|
||||
def get_next_linebox(document, linebox, position_y, skip_stack,
|
||||
containing_block, device_size, absolute_boxes,
|
||||
fixed_boxes):
|
||||
"""Return ``(line, resume_at)``."""
|
||||
position_x = linebox.position_x
|
||||
linebox.position_y = position_y
|
||||
max_x = position_x + containing_block.width
|
||||
resolve_percentages(linebox, containing_block)
|
||||
if skip_stack is None:
|
||||
# text-indent only at the start of the first line
|
||||
# Other percentages (margins, width, ...) do not apply.
|
||||
resolve_one_percentage(linebox, 'text_indent', containing_block.width)
|
||||
position_x += linebox.text_indent
|
||||
else:
|
||||
linebox.text_indent = 0
|
||||
|
||||
skip_stack = skip_first_whitespace(linebox, skip_stack)
|
||||
if skip_stack == 'continue':
|
||||
return None, None
|
||||
|
||||
line_placeholders = []
|
||||
linebox.width = inline_preferred_minimum_width(
|
||||
document, linebox, skip_stack=skip_stack, first_line=True)
|
||||
|
||||
linebox.height, _ = strut_layout(linebox.style)
|
||||
linebox.position_y = position_y
|
||||
position_x, position_y, available_width = avoid_collisions(
|
||||
document, linebox, containing_block, outer=False)
|
||||
candidate_height = linebox.height
|
||||
|
||||
excluded_shapes = document.excluded_shapes[:]
|
||||
|
||||
while 1:
|
||||
linebox.position_x = position_x
|
||||
linebox.position_y = position_y
|
||||
max_x = position_x + available_width
|
||||
position_x += linebox.text_indent
|
||||
|
||||
line_placeholders = []
|
||||
line_absolutes = []
|
||||
line_fixed = []
|
||||
waiting_floats = []
|
||||
|
||||
resolve_percentages(linebox, containing_block)
|
||||
line, resume_at, preserved_line_break = split_inline_box(
|
||||
document, linebox, position_x, max_x, skip_stack, containing_block,
|
||||
device_size, absolute_boxes, fixed_boxes, line_placeholders)
|
||||
document, linebox, position_x, max_x, skip_stack,
|
||||
containing_block, device_size, line_absolutes,
|
||||
line_fixed, line_placeholders, waiting_floats)
|
||||
|
||||
remove_last_whitespace(document, line)
|
||||
|
||||
bottom, top = line_box_verticality(line)
|
||||
last = resume_at is None or preserved_line_break
|
||||
offset_x = text_align(document, line, containing_block, last)
|
||||
if bottom is None:
|
||||
# No children at all
|
||||
line.position_y = position_y
|
||||
offset_y = 0
|
||||
if preserved_line_break:
|
||||
# Only the strut.
|
||||
@ -101,10 +119,28 @@ def get_next_linebox(document, linebox, position_y, skip_stack,
|
||||
offset_y = position_y - top
|
||||
line.margin_top = 0
|
||||
line.margin_bottom = 0
|
||||
|
||||
offset_x = text_align(document, line, available_width,
|
||||
last=resume_at is None or preserved_line_break)
|
||||
if offset_x != 0 or offset_y != 0:
|
||||
# This also translates children
|
||||
line.translate(offset_x, offset_y)
|
||||
|
||||
if line.height <= candidate_height:
|
||||
break
|
||||
candidate_height = line.height
|
||||
|
||||
new_excluded_shapes = document.excluded_shapes
|
||||
document.excluded_shapes = excluded_shapes
|
||||
position_x, position_y, available_width = avoid_collisions(
|
||||
document, line, containing_block, outer=False)
|
||||
if (position_x, position_y) == (
|
||||
linebox.position_x, linebox.position_y):
|
||||
document.excluded_shapes = new_excluded_shapes
|
||||
break
|
||||
|
||||
absolute_boxes.extend(line_absolutes)
|
||||
fixed_boxes.extend(line_fixed)
|
||||
|
||||
for placeholder in line_placeholders:
|
||||
if placeholder.style._weasy_specified_display.startswith('inline'):
|
||||
# Inline-level static position:
|
||||
@ -115,6 +151,18 @@ def get_next_linebox(document, linebox, position_y, skip_stack,
|
||||
line.position_x - placeholder.position_x,
|
||||
position_y + line.height - placeholder.position_y)
|
||||
|
||||
float_children = []
|
||||
waiting_floats_y = line.position_y + line.height
|
||||
for waiting_float in waiting_floats:
|
||||
waiting_float.position_y = waiting_floats_y
|
||||
waiting_float = float_layout(
|
||||
document, waiting_float, containing_block, absolute_boxes,
|
||||
fixed_boxes)
|
||||
float_children.append(waiting_float)
|
||||
if float_children:
|
||||
line = line.copy_with_children(
|
||||
line.children + tuple(float_children))
|
||||
|
||||
return line, resume_at
|
||||
|
||||
|
||||
@ -140,7 +188,7 @@ def skip_first_whitespace(box, skip_stack):
|
||||
if white_space in ('normal', 'nowrap', 'pre-line'):
|
||||
while index < length and box.text[index] == ' ':
|
||||
index += 1
|
||||
return index, None
|
||||
return (index, None) if index else None
|
||||
|
||||
if isinstance(box, (boxes.LineBox, boxes.InlineBox)):
|
||||
if index == 0 and not box.children:
|
||||
@ -151,7 +199,7 @@ def skip_first_whitespace(box, skip_stack):
|
||||
if index >= len(box.children):
|
||||
return 'continue'
|
||||
result = skip_first_whitespace(box.children[index], None)
|
||||
return index, result
|
||||
return (index, result) if (index or result) else None
|
||||
|
||||
assert skip_stack is None, 'unexpected skip inside %s' % box
|
||||
return None
|
||||
@ -277,44 +325,6 @@ def replaced_box_height(box, device_size):
|
||||
# box.height = min(150, device_width / 2)
|
||||
|
||||
|
||||
def handle_min_max_width(function):
|
||||
"""Decorate a function that sets the used width of a box to handle
|
||||
{min,max}-width.
|
||||
"""
|
||||
@functools.wraps(function)
|
||||
def wrapper(box, *args):
|
||||
computed_margins = box.margin_left, box.margin_right
|
||||
function(box, *args)
|
||||
if box.width > box.max_width:
|
||||
box.width = box.max_width
|
||||
box.margin_left, box.margin_right = computed_margins
|
||||
function(box, *args)
|
||||
if box.width < box.min_width:
|
||||
box.width = box.min_width
|
||||
box.margin_left, box.margin_right = computed_margins
|
||||
function(box, *args)
|
||||
return wrapper
|
||||
|
||||
|
||||
def handle_min_max_height(function):
|
||||
"""Decorate a function that sets the used height of a box to handle
|
||||
{min,max}-height.
|
||||
"""
|
||||
@functools.wraps(function)
|
||||
def wrapper(box, *args):
|
||||
computed_margins = box.margin_top, box.margin_bottom
|
||||
function(box, *args)
|
||||
if box.height > box.max_height:
|
||||
box.height = box.max_height
|
||||
box.margin_top, box.margin_bottom = computed_margins
|
||||
function(box, *args)
|
||||
if box.height < box.min_height:
|
||||
box.height = box.min_height
|
||||
box.margin_top, box.margin_bottom = computed_margins
|
||||
function(box, *args)
|
||||
return wrapper
|
||||
|
||||
|
||||
min_max_replaced_width = handle_min_max_width(replaced_box_width)
|
||||
min_max_replaced_height = handle_min_max_height(replaced_box_height)
|
||||
|
||||
@ -326,6 +336,7 @@ def inline_replaced_box_layout(box, device_size):
|
||||
setattr(box, 'margin_' + side, 0)
|
||||
inline_replaced_box_width_height(box, device_size)
|
||||
|
||||
|
||||
def inline_replaced_box_width_height(box, device_size):
|
||||
if box.style.width == 'auto' and box.style.height == 'auto':
|
||||
replaced_box_width(box, device_size)
|
||||
@ -464,7 +475,7 @@ def inline_block_width(box, document, containing_block):
|
||||
|
||||
def split_inline_level(document, box, position_x, max_x, skip_stack,
|
||||
containing_block, device_size, absolute_boxes,
|
||||
fixed_boxes, line_placeholders):
|
||||
fixed_boxes, line_placeholders, waiting_floats):
|
||||
"""Fit as much content as possible from an inline-level box in a width.
|
||||
|
||||
Return ``(new_box, resume_at)``. ``resume_at`` is ``None`` if all of the
|
||||
@ -500,7 +511,8 @@ def split_inline_level(document, box, position_x, max_x, skip_stack,
|
||||
box.margin_right = 0
|
||||
new_box, resume_at, preserved_line_break = split_inline_box(
|
||||
document, box, position_x, max_x, skip_stack, containing_block,
|
||||
device_size, absolute_boxes, fixed_boxes, line_placeholders)
|
||||
device_size, absolute_boxes, fixed_boxes, line_placeholders,
|
||||
waiting_floats)
|
||||
elif isinstance(box, boxes.AtomicInlineLevelBox):
|
||||
new_box = atomic_box(
|
||||
document, box, position_x, skip_stack, containing_block,
|
||||
@ -514,32 +526,32 @@ def split_inline_level(document, box, position_x, max_x, skip_stack,
|
||||
|
||||
def split_inline_box(document, box, position_x, max_x, skip_stack,
|
||||
containing_block, device_size, absolute_boxes,
|
||||
fixed_boxes, line_placeholders):
|
||||
fixed_boxes, line_placeholders, waiting_floats):
|
||||
"""Same behavior as split_inline_level."""
|
||||
is_start = skip_stack is None
|
||||
initial_position_x = position_x
|
||||
assert isinstance(box, (boxes.LineBox, boxes.InlineBox))
|
||||
left_spacing = (box.padding_left + box.margin_left +
|
||||
box.border_left_width)
|
||||
right_spacing = (box.padding_right + box.margin_right +
|
||||
box.border_right_width)
|
||||
if is_start:
|
||||
position_x += left_spacing
|
||||
content_box_left = position_x
|
||||
|
||||
children = []
|
||||
preserved_line_break = False
|
||||
|
||||
is_start = skip_stack is None
|
||||
if box.style.position == 'relative':
|
||||
absolute_boxes = []
|
||||
|
||||
if is_start:
|
||||
skip = 0
|
||||
else:
|
||||
skip, skip_stack = skip_stack
|
||||
|
||||
if box.style.position == 'relative':
|
||||
absolute_boxes = []
|
||||
|
||||
for index, child in box.enumerate_skip(skip):
|
||||
child.position_y = box.position_y
|
||||
if not child.is_in_normal_flow():
|
||||
if child.is_absolutely_positioned():
|
||||
child.position_x = position_x
|
||||
placeholder = AbsolutePlaceholder(child)
|
||||
@ -550,15 +562,38 @@ def split_inline_box(document, box, position_x, max_x, skip_stack,
|
||||
absolute_boxes.append(placeholder)
|
||||
else:
|
||||
fixed_boxes.append(placeholder)
|
||||
elif child.style.float in ('left', 'right'):
|
||||
# TODO: Floats
|
||||
assert 0
|
||||
continue
|
||||
elif child.is_floated():
|
||||
child.position_x = position_x
|
||||
float_width = shrink_to_fit(
|
||||
document, child, containing_block.width)
|
||||
if float_width > max_x - position_x or waiting_floats:
|
||||
# TODO: the absolute and fixed boxes in the floats must be
|
||||
# added here, and not in iter_line_boxes
|
||||
waiting_floats.append(child)
|
||||
else:
|
||||
child = float_layout(
|
||||
document, child, containing_block, absolute_boxes,
|
||||
fixed_boxes)
|
||||
children.append(child)
|
||||
# TODO: use the main text direction of the line
|
||||
for old_child in children[:index]:
|
||||
if not old_child.is_in_normal_flow():
|
||||
continue
|
||||
if child.style.float == 'left': # and direction is ltr
|
||||
old_child.translate(dx=child.margin_width())
|
||||
# elif child.style.float == 'right' and direction is rtl:
|
||||
# old_child.translate(dx=-child.margin_width())
|
||||
if child.style.float == 'left':
|
||||
position_x += child.margin_width()
|
||||
elif child.style.float == 'right':
|
||||
max_x -= child.margin_width()
|
||||
continue
|
||||
|
||||
new_child, resume_at, preserved = split_inline_level(
|
||||
document, child, position_x, max_x, skip_stack, containing_block,
|
||||
device_size, absolute_boxes, fixed_boxes, line_placeholders)
|
||||
device_size, absolute_boxes, fixed_boxes, line_placeholders,
|
||||
waiting_floats)
|
||||
skip_stack = None
|
||||
if preserved:
|
||||
preserved_line_break = True
|
||||
@ -708,20 +743,28 @@ def line_box_verticality(box):
|
||||
max_y, min_y = aligned_subtree_verticality(
|
||||
box, top_bottom_subtrees, baseline_y=0)
|
||||
for subtree in top_bottom_subtrees:
|
||||
if subtree.is_floated():
|
||||
sub_min_y = None
|
||||
sub_max_y = None
|
||||
else:
|
||||
sub_max_y, sub_min_y = aligned_subtree_verticality(
|
||||
subtree, top_bottom_subtrees, baseline_y=0)
|
||||
subtrees_with_min_max.append(
|
||||
(subtree, sub_max_y, sub_min_y))
|
||||
|
||||
if subtrees_with_min_max:
|
||||
highest_sub = max(
|
||||
sub_positions = [
|
||||
sub_max_y - sub_min_y
|
||||
for subtree, sub_max_y, sub_min_y in subtrees_with_min_max
|
||||
)
|
||||
if not subtree.is_floated()]
|
||||
if sub_positions:
|
||||
highest_sub = max(sub_positions)
|
||||
max_y = max(max_y, min_y + highest_sub)
|
||||
|
||||
for subtree, sub_max_y, sub_min_y in subtrees_with_min_max:
|
||||
if subtree.style.vertical_align == 'top':
|
||||
if subtree.is_floated():
|
||||
dy = min_y - subtree.position_y
|
||||
elif subtree.style.vertical_align == 'top':
|
||||
dy = min_y - sub_min_y
|
||||
else:
|
||||
assert subtree.style.vertical_align == 'bottom'
|
||||
@ -772,6 +815,8 @@ def inline_box_verticality(box, top_bottom_subtrees, baseline_y):
|
||||
|
||||
for child in box.children:
|
||||
if not child.is_in_normal_flow():
|
||||
if child.is_floated():
|
||||
top_bottom_subtrees.append(child)
|
||||
continue
|
||||
vertical_align = child.style.vertical_align
|
||||
if vertical_align == 'baseline':
|
||||
@ -840,7 +885,7 @@ def inline_box_verticality(box, top_bottom_subtrees, baseline_y):
|
||||
return max_y, min_y
|
||||
|
||||
|
||||
def text_align(document, line, containing_block, last):
|
||||
def text_align(document, line, available_width, last):
|
||||
"""Return how much the line should be moved horizontally according to
|
||||
the `text-align` property.
|
||||
|
||||
@ -855,7 +900,7 @@ def text_align(document, line, containing_block, last):
|
||||
align = 'right' if line.style.direction == 'rtl' else 'left'
|
||||
if align == 'left':
|
||||
return 0
|
||||
offset = containing_block.width - line.width
|
||||
offset = available_width - line.width
|
||||
if align == 'justify':
|
||||
justify_line(document, line, offset)
|
||||
return 0
|
||||
|
52
weasyprint/layout/min_max.py
Normal file
52
weasyprint/layout/min_max.py
Normal file
@ -0,0 +1,52 @@
|
||||
# coding: utf8
|
||||
"""
|
||||
weasyprint.layout.min_max
|
||||
-------------------------
|
||||
|
||||
:copyright: Copyright 2011-2012 Simon Sapin and contributors, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import division, unicode_literals
|
||||
import functools
|
||||
|
||||
|
||||
def handle_min_max_width(function):
|
||||
"""Decorate a function that sets the used width of a box to handle
|
||||
{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
|
||||
return wrapper
|
||||
|
||||
|
||||
def handle_min_max_height(function):
|
||||
"""Decorate a function that sets the used height of a box to handle
|
||||
{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
|
||||
return wrapper
|
@ -478,6 +478,8 @@ def make_page(document, root_box, page_type, resume_at, content_empty):
|
||||
page.width = page.outer_width - page.horizontal_surroundings()
|
||||
page.height = page.outer_height - page.vertical_surroundings()
|
||||
|
||||
document.excluded_shapes = []
|
||||
|
||||
root_box.position_x = page.content_box_x()
|
||||
root_box.position_y = page.content_box_y()
|
||||
page_content_bottom = root_box.position_y + page.height
|
||||
|
@ -84,7 +84,7 @@ def _block_preferred_width(document, box, function, outer):
|
||||
# http://dbaron.org/css/intrinsic/#outer-intrinsic
|
||||
children_widths = [
|
||||
function(document, child, outer=True) for child in box.children
|
||||
if child.is_in_normal_flow()]
|
||||
if not child.is_absolutely_positioned()]
|
||||
width = max(children_widths) if children_widths else 0
|
||||
else:
|
||||
assert width.unit == 'px'
|
||||
@ -142,27 +142,36 @@ def block_preferred_width(document, box, outer=True):
|
||||
return _block_preferred_width(document, box, preferred_width, outer)
|
||||
|
||||
|
||||
def inline_preferred_minimum_width(document, box, outer=True):
|
||||
"""Return the preferred minimum width for an ``InlineBox``."""
|
||||
def inline_preferred_minimum_width(document, box, outer=True, skip_stack=None,
|
||||
first_line=False):
|
||||
"""Return the preferred minimum width for an ``InlineBox``.
|
||||
|
||||
The width is calculated from the lines from ``skip_stack``. If
|
||||
``first_line`` is ``True``, only the first line minimum width is
|
||||
calculated.
|
||||
|
||||
"""
|
||||
widest_line = 0
|
||||
for child in box.children:
|
||||
if not child.is_in_normal_flow():
|
||||
if skip_stack is None:
|
||||
skip = 0
|
||||
else:
|
||||
skip, skip_stack = skip_stack
|
||||
for index, child in box.enumerate_skip(skip):
|
||||
if child.is_absolutely_positioned():
|
||||
continue # Skip
|
||||
|
||||
if isinstance(child, boxes.InlineReplacedBox):
|
||||
# Images are on their own line
|
||||
current_line = replaced_preferred_width(child)
|
||||
elif isinstance(child, boxes.InlineBlockBox):
|
||||
if child.is_table_wrapper:
|
||||
current_line = table_preferred_minimum_width(document, child)
|
||||
else:
|
||||
current_line = block_preferred_minimum_width(document, child)
|
||||
elif isinstance(child, boxes.InlineBox):
|
||||
if isinstance(child, boxes.InlineBox):
|
||||
# TODO: handle forced line breaks
|
||||
current_line = inline_preferred_minimum_width(document, child)
|
||||
current_line = inline_preferred_minimum_width(
|
||||
document, child, skip_stack=skip_stack, first_line=first_line)
|
||||
elif isinstance(child, boxes.TextBox):
|
||||
widths = text.line_widths(document, child, width=0, skip=skip)
|
||||
if first_line:
|
||||
return next(widths)
|
||||
else:
|
||||
assert isinstance(child, boxes.TextBox)
|
||||
current_line = max(text.line_widths(document, child, width=0))
|
||||
current_line = max(widths)
|
||||
else:
|
||||
current_line = preferred_minimum_width(document, child)
|
||||
widest_line = max(widest_line, current_line)
|
||||
return adjust(box, outer, widest_line)
|
||||
|
||||
@ -172,22 +181,13 @@ def inline_preferred_width(document, box, outer=True):
|
||||
widest_line = 0
|
||||
current_line = 0
|
||||
for child in box.children:
|
||||
if not child.is_in_normal_flow():
|
||||
if child.is_absolutely_positioned():
|
||||
continue # Skip
|
||||
|
||||
if isinstance(child, boxes.InlineReplacedBox):
|
||||
# No line break around images
|
||||
current_line += replaced_preferred_width(child)
|
||||
elif isinstance(child, boxes.InlineBlockBox):
|
||||
if child.is_table_wrapper:
|
||||
current_line += table_preferred_width(document, child)
|
||||
else:
|
||||
current_line += block_preferred_width(document, child)
|
||||
elif isinstance(child, boxes.InlineBox):
|
||||
if isinstance(child, boxes.InlineBox):
|
||||
# TODO: handle forced line breaks
|
||||
current_line += inline_preferred_width(document, child)
|
||||
else:
|
||||
assert isinstance(child, boxes.TextBox)
|
||||
elif isinstance(child, boxes.TextBox):
|
||||
lines = list(text.line_widths(document, child, width=None))
|
||||
assert lines
|
||||
# The first text line goes on the current line
|
||||
@ -198,6 +198,8 @@ def inline_preferred_width(document, box, outer=True):
|
||||
if len(lines) > 2:
|
||||
widest_line = max(widest_line, max(lines[1:-1]))
|
||||
current_line = lines[-1]
|
||||
else:
|
||||
current_line += preferred_width(document, child)
|
||||
widest_line = max(widest_line, current_line)
|
||||
|
||||
return adjust(box, outer, widest_line)
|
||||
@ -344,7 +346,7 @@ def table_and_columns_preferred_widths(document, box, outer=True,
|
||||
table_preferred_width = sum(column_preferred_widths) + total_border_spacing
|
||||
|
||||
captions = [child for child in box.children
|
||||
if child is not table and child.is_in_normal_flow()]
|
||||
if child is not table and not child.is_absolutely_positioned()]
|
||||
|
||||
if captions:
|
||||
caption_width = max(
|
||||
|
17
weasyprint/tests/resources/acid2-reference.html
Normal file
17
weasyprint/tests/resources/acid2-reference.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
|
||||
<html>
|
||||
<head>
|
||||
<title>The Second Acid Test (Reference Rendering)</title>
|
||||
<style type="text/css">
|
||||
html { margin: 0; padding: 0; border: 0; overflow: hidden; background: white; }
|
||||
body { margin: 0; padding: 0; border: 0; }
|
||||
h2 { margin: 0; padding: 48px 0 36px 84px; border: 0; font: 24px/24px sans-serif; color: navy; }
|
||||
p { margin: 0; padding: 0 0 0 72px; border: 0; }
|
||||
img { vertical-align: top; margin: 0; padding: 0; border: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Hello World!</h2>
|
||||
<p><a href="reference.png"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAACoCAMAAABDlVWGAAABUFBMVEUAAAABAQADAwAICAAIpQQOpwodHQAhISAmryIsLAAwsy1AQEBDQwBSUgBTwFBUVAB1dQCAgACBgQCCggCDgwCEhACFhQCGhgCJiQCJ1IeKigCLiwCMjACOjgCPjwCPj4+SkgCTkwCVlQCXlwCYmACbmwCdnQCdnZ2fnwCgoACiogCi3aClpQCmpgCpqQCrqwCsrACtrQCvrwCwsLCysgCzswC2tgC8vAC9vQC+vgDBwQDCwgDDwwDExADIyADJyQDKygDKysrLywDNzQDOzgDO7c3Q0ADS0gDT0wDU1ADW1gDX1wDa2gDb29vd3QDf3wDg4ADh4QDk5ADl5QDo6ADo9ujp6QDq6gDq6urr6wDs7ADt7QDv7wDw8ADx8QDy8gDz8wDz+vP19QD29gD39wD4+AD5+QD7+wD7+/v8/AD9/QD9/f3+/gD+/v7//wD///+VIlNwAAADWklEQVR42u3d6VPaQBgGcBBBkDNciiCIgKgRTxTPqijeiCeioohcAiH9/7+VduqMbVNIJruMGZ/96Mz77C/AZt/VDKq+yxgqiUPWXIACCiiggAIKKDXo/ybmJQ45FwAooIACCiiggJKFygHJuQBAAQUUUEABBZQeVBTuuVCTC6sVniWjJULLmeT6YUUutHK4nsyUKUJLmY0Zv2+t/Yo+xm1mk8lsiz+KxX2sqK35/DMbmRItaPVs0euw+4/rfMKq06hVKrVGZ02Ic/5RUT/22x3exbMqHWj1bH7U7po8Kb9EDH3G6G46vRs19hkiL92Zf1eUTyZd9tF5ISkBaHZlmGHCpw0+ohsI5Jqt9mjmAgO6SHfoPxWN0zDDDK9kaUArB6F29naFTxgGlt6KKdbjYVPFt6UBQ9d3X6Cist2+6tBBhQL0YXmEcS8U+EdrX+DtlrXotVq9hb19C/RZu6wowYrCgpsZWX4gD62fTzsZ7z7Hx3XGXJEd7P81BtlizqiLd4YKVnD7XsY5fV4nDq0d+e1M6JLjbZpoM2Xp/z0sqWZUY+sMFazgLkOM3X9UIw6tJscY11ye583q3Rarf59Wz7Z21ebOUOGK/JyLGUtWiUNLe0HGNdvONanSLY/2fVqtp5VWmTpDhSuqsy4muFciv5iOxx1DPz/8pKAPy0OO8WMKq/4iZnfHnsi99U8xtz12QQH6mnAwwWtyi+k6yDgSrzR2ppOpId9mg9TtqbHpG5o6obKF5rdCzokyR+aGz5UnnKGtPJ3uKbsajl1xZLZQ7ioWXs1SavNqNzvfCkIthvSmhOcL33ZuarQaZ+71riHQtElv89o/aty9chSPIg2BNlh64/wxihL0fdy/HyzuxR5FxFYQhvb+6Kwc6GfASf3jBKCAAgoooIACCqi4fGVAySbRgxKPogSlkUUDSimMOJReGlko1TiCybTzSAX3IJBIbm8S5cf2LFJmai8zv8YrqpzPqHJWvXLuo8rZmZSz1yune1JOP6qcDl85ZyblnELxCwhAAQUUUEAB/UxQMQ8W0LgYMXNJfgICUEABBRRQQAGV/RChitAQkw8ooIACCiiggJKF4hsLAAUUUEABBfTrQPFNhIACCiiggAL6daD4DwOAAgoooIACqljoDwseYUYsza58AAAAAElFTkSuQmCC" alt="Follow this link to view the reference image, which should be rendered below the text "Hello World!" on the test page in the same way that this paragraph is rendered below that text on this page."></a></p>
|
||||
</body>
|
||||
</html>
|
148
weasyprint/tests/resources/acid2-test.html
Normal file
148
weasyprint/tests/resources/acid2-test.html
Normal file
@ -0,0 +1,148 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
|
||||
<html>
|
||||
<head>
|
||||
<title>The Second Acid Test</title>
|
||||
<style type="text/css">
|
||||
/* section numbers refer to CSS2.1 */
|
||||
|
||||
/* page setup */
|
||||
html { font: 12px sans-serif; margin: 0; padding: 0; overflow: hidden; /* hides scrollbars on viewport, see 11.1.1:3 */ background: white; color: red; }
|
||||
body { margin: 0; padding: 0; }
|
||||
|
||||
/* introduction message */
|
||||
.intro { font: 2em sans-serif; margin: 3.5em 2em; padding: 0.5em; border: solid thin; background: white; color: black; position: relative; z-index: 2; /* should cover the black and red bars that are fixed-positioned */ }
|
||||
.intro * { font: inherit; margin: 0; padding: 0; }
|
||||
.intro h1 { font-size: 1em; font-weight: bolder; margin: 0; padding: 0; }
|
||||
.intro :link { color: blue; }
|
||||
.intro :visited { color: purple; }
|
||||
|
||||
/* picture setup */
|
||||
#top { margin: 100em 3em 0; padding: 2em 0 0 .5em; text-align: left; font: 2em/24px sans-serif; color: navy; white-space: pre; } /* "Hello World!" text */
|
||||
.picture { position: relative; border: 1em solid transparent; margin: 0 0 100em 3em; } /* containing block for face */
|
||||
.picture { background: red; } /* overriden by preferred stylesheet below */
|
||||
|
||||
/* top line of face (scalp): fixed positioning and min/max height/width */
|
||||
.picture p { position: fixed; margin: 0; padding: 0; border: 0; top: 9em; left: 11em; width: 140%; max-width: 4em; height: 8px; min-height: 1em; max-height: 2mm; /* min-height overrides max-height, see 10.7 */ background: black; border-bottom: 0.5em yellow solid; }
|
||||
|
||||
/* bits that shouldn't be part of the top line (and shouldn't be visible at all): HTML parsing, "+" combinator, stacking order */
|
||||
.picture p.bad { border-bottom: red solid; /* shouldn't matter, because the "p + table + p" rule below should match it too, thus hiding it */ }
|
||||
.picture p + p { background: maroon; z-index: 1; } /* shouldn't match anything */
|
||||
.picture p + table + p { margin-top: 3em; /* should end up under the absolutely positioned table below, and thus not be visible */ }
|
||||
|
||||
/* second line of face: attribute selectors, float positioning */
|
||||
[class~=one].first.one { position: absolute; top: 0; margin: 36px 0 0 60px; padding: 0; border: black 2em; border-style: none solid; /* shrink wraps around float */ }
|
||||
[class~=one][class~=first] [class=second\ two][class="second two"] { float: right; width: 48px; height: 12px; background: yellow; margin: 0; padding: 0; } /* only content of abs pos block */
|
||||
|
||||
/* third line of face: width and overflow */
|
||||
.forehead { margin: 4em; width: 8em; border-left: solid black 1em; border-right: solid black 1em; background: red url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mP4%2F58BAAT%2FAf9jgNErAAAAAElFTkSuQmCC); /* that's a 1x1 yellow pixel PNG */ }
|
||||
.forehead * { width: 12em; line-height: 1em; }
|
||||
|
||||
/* class selectors headache */
|
||||
.two.error.two { background: maroon; } /* shouldn't match */
|
||||
.forehead.error.forehead { background: red; } /* shouldn't match */
|
||||
[class=second two] { background: red; } /* this should be ignored (invalid selector -- grammar says it only accepts IDENTs or STRINGs) */
|
||||
|
||||
/* fourth and fifth lines of face, with eyes: paint order test (see appendix E) and fixed backgrounds */
|
||||
/* the two images are identical: 2-by-2 squares with the top left
|
||||
and bottom right pixels set to yellow and the other two set to
|
||||
transparent. Since they are offset by one pixel from each other,
|
||||
the second one paints exactly over the transparent parts of the
|
||||
first one, thus creating a solid yellow block. */
|
||||
.eyes { position: absolute; top: 5em; left: 3em; margin: 0; padding: 0; background: red; }
|
||||
#eyes-a { height: 0; line-height: 2em; text-align: right; } /* contents should paint top-most because they're inline */
|
||||
#eyes-a object { display: inline; vertical-align: bottom; }
|
||||
#eyes-a object[type] { width: 7.5em; height: 2.5em; } /* should have no effect since that object should fallback to being inline (height/width don't apply to inlines) */
|
||||
#eyes-a object object object { border-right: solid 1em black; padding: 0 12px 0 11px; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAABnRSTlMAAAAAAABupgeRAAAABmJLR0QA%2FwD%2FAP%2BgvaeTAAAAEUlEQVR42mP4%2F58BCv7%2FZwAAHfAD%2FabwPj4AAAAASUVORK5CYII%3D) fixed 1px 0; }
|
||||
#eyes-b { float: left; width: 10em; height: 2em; background: fixed url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAABnRSTlMAAAAAAABupgeRAAAABmJLR0QA%2FwD%2FAP%2BgvaeTAAAAEUlEQVR42mP4%2F58BCv7%2FZwAAHfAD%2FabwPj4AAAAASUVORK5CYII%3D); border-left: solid 1em black; border-right: solid 1em red; } /* should paint in the middle layer because it is a float */
|
||||
#eyes-c { display: block; background: red; border-left: 2em solid yellow; width: 10em; height: 2em; } /* should paint bottom most because it is a block */
|
||||
|
||||
/* lines six to nine, with nose: auto margins */
|
||||
.nose { float: left; margin: -2em 2em -1em; border: solid 1em black; border-top: 0; min-height: 80%; height: 60%; max-height: 3em; /* percentages become auto (see 10.5 and 10.7) and intrinsic height is more than 3em, so 3em wins */ padding: 0; width: 12em; }
|
||||
.nose > div { padding: 1em 1em 3em; height: 0; background: yellow; }
|
||||
.nose div div { width: 2em; height: 2em; background: red; margin: auto; }
|
||||
.nose :hover div { border-color: blue; }
|
||||
.nose div:hover :before { border-bottom-color: inherit; }
|
||||
.nose div:hover :after { border-top-color: inherit; }
|
||||
.nose div div:before { display: block; border-style: none solid solid; border-color: red yellow black yellow; border-width: 1em; content: ''; height: 0; }
|
||||
.nose div :after { display: block; border-style: solid solid none; border-color: black yellow red yellow; border-width: 1em; content: ''; height: 0; }
|
||||
|
||||
/* between lines nine and ten: margin collapsing with 'float' and 'clear' */
|
||||
.empty { margin: 6.25em; height: 10%; /* computes to auto which makes it empty per 8.3.1:7 (own margins) */ }
|
||||
.empty div { margin: 0 2em -6em 4em; }
|
||||
.smile { margin: 5em 3em; clear: both; /* clearance is negative (see 8.3.1 and 9.5.1) */ }
|
||||
|
||||
/* line ten and eleven: containing block for abs pos */
|
||||
.smile div { margin-top: 0.25em; background: black; width: 12em; height: 2em; position: relative; bottom: -1em; }
|
||||
.smile div div { position: absolute; top: 0; right: 1em; width: auto; height: 0; margin: 0; border: yellow solid 1em; }
|
||||
|
||||
/* smile (over lines ten and eleven): backgrounds behind borders, inheritance of 'float', nested floats, negative heights */
|
||||
.smile div div span { display: inline; margin: -1em 0 0 0; border: solid 1em transparent; border-style: none solid; float: right; background: black; height: 1em; }
|
||||
.smile div div span em { float: inherit; border-top: solid yellow 1em; border-bottom: solid black 1em; } /* zero-height block; width comes from (zero-height) child. */
|
||||
.smile div div span em strong { width: 6em; display: block; margin-bottom: -1em; /* should have no effect, since parent has top&bottom borders, so this margin doesn't collapse */ }
|
||||
|
||||
/* line twelve: line-height */
|
||||
.chin { margin: -4em 4em 0; width: 8em; line-height: 1em; border-left: solid 1em black; border-right: solid 1em black; background: yellow url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAFSDNYfAAAAaklEQVR42u3XQQrAIAwAQeP%2F%2F6wf8CJBJTK9lnQ7FpHGaOurt1I34nfH9pMMZAZ8BwMGEvvh%2BBsJCAgICLwIOA8EBAQEBAQEBAQEBK79H5RfIQAAAAAAAAAAAAAAAAAAAAAAAAAAAID%2FABMSqAfj%2FsLmvAAAAABJRU5ErkJggg%3D%3D) /* 64x64 red square */ no-repeat fixed /* shouldn't be visible unless the smiley is moved to the top left of the viewport */; }
|
||||
.chin div { display: inline; font: 2px/4px serif; }
|
||||
|
||||
/* line thirteen: cascade and selector tests */
|
||||
.parser-container div { color: maroon; border: solid; color: orange; } /* setup */
|
||||
div.parser-container * { border-color: black; /* overrides (implied) border-color on previous line */ } /* setup */
|
||||
* div.parser { border-width: 0 2em; /* overrides (implied) declarations on earlier line */ } /* setup */
|
||||
|
||||
/* line thirteen continued: parser tests */
|
||||
.parser { /* comment parsing test -- comment ends before the end of this line, the backslash should have no effect: \*/ }
|
||||
.parser { margin: 0 5em 1em; padding: 0 1em; width: 2em; height: 1em; error: \}; background: yellow; } /* setup with parsing test */
|
||||
* html .parser { background: gray; }
|
||||
\.parser { padding: 2em; }
|
||||
.parser { m\argin: 2em; };
|
||||
.parser { height: 3em; }
|
||||
.parser { width: 200; }
|
||||
.parser { border: 5em solid red ! error; }
|
||||
.parser { background: red pink; }
|
||||
|
||||
/* line fourteen (last line of face): table */
|
||||
ul { display: table; padding: 0; margin: -1em 7em 0; background: red; }
|
||||
ul li { padding: 0; margin: 0; }
|
||||
ul li.first-part { display: table-cell; height: 1em; width: 1em; background: black; }
|
||||
ul li.second-part { display: table; height: 1em; width: 1em; background: black; } /* anonymous table cell wraps around this */
|
||||
ul li.third-part { display: table-cell; height: 0.5em; /* gets stretched to fit row */ width: 1em; background: black; }
|
||||
ul li.fourth-part { list-style: none; height: 1em; width: 1em; background: black; } /* anonymous table cell wraps around this */
|
||||
|
||||
/* bits that shouldn't appear: inline alignment in cells */
|
||||
.image-height-test { height: 10px; overflow: hidden; font: 20em serif; } /* only the area between the top of the line box and the top of the image should be visible */
|
||||
table { margin: 0; border-spacing: 0; }
|
||||
td { padding: 0; }
|
||||
|
||||
</style>
|
||||
<link rel="appendix stylesheet" href="data:text/css,.picture%20%7B%20background%3A%20none%3B%20%7D"> <!-- this stylesheet should be applied by default -->
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="intro">
|
||||
<h1>Standards compliant?</h1>
|
||||
<p><a href="#top">Take The Acid2 Test</a> and compare it to <a href="reference.html">the reference rendering</a>.</p>
|
||||
</div>
|
||||
<h2 id="top">Hello World!</h2>
|
||||
|
||||
<div class="picture">
|
||||
<p><table><tr><td></table><p class="bad"> <!-- <table> closes <p> per the HTML4 DTD -->
|
||||
<blockquote class="first one"><address class="second two"></address></blockquote>
|
||||
<div class="forehead"><div> </div></div>
|
||||
<div class="eyes"><div id="eyes-a"><object data="data:application/x-unknown,ERROR"><object data="http://www.webstandards.org/404/" type="text/html"><object data="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAYCAYAAAFy7sgCAAAGsUlEQVRo3u2ZbWwcZxHHf3s%2B7LNbO3ZjXBtowprGODRX0qpNQCjmJKuVKhMl1P2AkCwhFOIKkCBSm9IXavGFKAixIAECwkmWo5MrhRI3Ub40IEwQgp6aIDg3Cd6eEqyIHEteah%2B1E69vhw%2BZtTaX8704ZzkKjHS6271nZ56ZZ%2BY%2F%2F%2BdZKF%2FCwYshx3EkkggLsD1v4FQkEZZYLCbAKyG9%2Ba9EIsG6hnUAf8x74K3aUC3j4%2BM54HcsR2oAIomwZOezkv%2FnSHpYNh%2BNCmAE7xv94zvFdd1bHsjMZmQkPSxAJP%2B%2FfuBLwK54PC7JZFKAVJmzXLBt2w%2FMvcDLwIb8QS8CeJ4nkURYIomw7J%2FYJ8BvSiiXptGGxWds2%2Fa9%2Bnaxh%2BYAD%2Bgt04NDgABTpQY2cvvSFLzw86gWeBVwC8SzlOSv2YeBPfmDBoBHgKmR9LBEEmHZfDTqGykqfkUE0nA78BzQGfSgUeP3wNeTXwXg7MwZDhw4UHL6ra2ti79%2FOvljgG8AZ4H64Lhm4MvAocxsRppGG%2FxcXihlwLIs6R%2FfKV2HO%2F26uA94pdDYUKUZUU7W1RQYXA98Gnhaf5%2FXWX0HeAHYoQonqa4sZSOsSWMCWeC9Yko%2BCQwBe4E6oNc0Tc91XTl1%2BaTsn9gnI%2Blhyc5nZWxsrBIkKSbl2tiic3tW53YDEwOKaoFBrcOfqKee53lG9xsPMjV784r%2F4lO%2FpPvyJ9iyZcuvFSaXK5XYeAZ4CDgGvB3MS4B54LQuWYPeuy4iRFsevsXqpuYoqVQKIH2bK1CuDQNo11o4XUzh%2FcDWYIe1LEtyuZx4niee54njOGKapgfsqlL%2Bl2OjEXg8nxrc1dJ0h3hbtL%2BGCtz7KPBF4CuBe9uB15VafE8hr9qylI3HgG8C2%2FK7VyHZoJj7MrBRm30qFotJMpkU27YlHo%2F7Ha5a%2BV%2FKRkSJ4KuKRLVLKapTjB1SzAVIjY2NSXY%2BKyPpYdk%2FsU9OXT4pruv6BdZbBQfKsVGnvWlIe1VB6VQO8JxC1vZYLCbZ%2BaxsPhpdZDyRRFhG0sPiOE6ldKBg2lRg4xF1YCDIIIKN7DGgD3gH%2BBXwejKZfPrs2tPs%2FvPN2bKuYR1nd7xLKBSSJeqoXKnERjPwNWAG%2BLn2rZuM%2B4Tpml6vaWlp4eLcxVusZq5lCgVgOVKJjRqdX86ffL4D5wIoZACnTpw4wRMdT96i%2FImOJxERAs4uVyqxUacF%2FPdiCj%2BjdRBRGFtwXVdG0sPSdbhTmkYbpH98p2RmM2JZlig1vl0GWo4NQ%2Fn%2Bs5pKRXfwjweaxy7TND3HcRZbfC6X8xVPVQlGy7WxVWlO5XRXFXm6EZmrQuSXYyPE3SiVoEhE6Wyr0u2rumO6zv%2B21AFdQAswC1wCMuUCXCmyWQus103Qg8qlDO0lxwOb%2Fl4FiK3AB3VS%2FuKKLtK%2FgbeAnwG%2FvUODuRw%2FFrR0H1UC75fwu8oJ%2FhFsW5VIG%2FBUgEIN6Y65O4AHu4Ap0zQ9y7LEcZyb9lRBUHQcRyzL8unZVBW5bFWAvAp%2BhDQ2g4F47dUYtlU6obXA54DnVdFLekjUGGifh4AFy7LEdV3xj3X9I66m0QZpGm2QrsOd0j%2B%2BU0bSw5KZzYjrun6HWlAd961i4FfCj0aN1Usau%2Bc1lmuXPFwvAEumUut7tQQvAb%2FXb%2FT0bCAej9cODg7yt%2Bm%2F8q2%2F7OUHZ76PnZ1k2p0mJzlykmPancbOTnL0whHs7CQfb%2B5mx2d3sH79%2BtCRI0c6FeaOr9ICrIQfLvA%2B8BGNXxi4R6HrisJVUWrxAVW2oMFf0Aczim8o3kV6enowDIPjF9%2Fk%2BMU3S3rrjzMMg56eHr%2BxP7qKFbASfojG6kpeDGs1tiW53RxwWT%2Bin5q8w4xpQK5evQpAR30H7ZH2khNvj7TTUd8BgD4rqmu1ZKX8qNeY%2BfHz4zlXDgT5E8tpCTUq7XSBC4Euv8227TV9fX1E73%2BYtvo27BmbS9cvFVTY3bSRFza9yOcf6Gfmygy7d%2B%2Fm%2FPnzF4DvrsBLhnJlJfwIKXxv1PheAE4qK6p4H9AGbNKTuhngBPBPXYRe4IemaT5kWZbR19fHNbmGnZ1k4r3U4glDR30Hm5qjbGjsImJEOHbsGHv27JFz5869o0eFq01Jq%2BmHAXwI6FFKagMTgHM7GzFDS%2BoeLSMv7zjzC9x4Y7gxFovVDAwMEI1GaWlpWSzRVCrFwYMH%2FXfxZ4AfAa8B%2F7lDaGg1%2FQgp43lfK0yqtRMuJa3ceKe5DfgYsCYAZ2ngD8CfAkzqTpW7xY%2F%2FSznyX%2FVeUb2kVmX4AAAAAElFTkSuQmCC">ERROR</object></object></object></div><div id="eyes-b"></div><div id="eyes-c"></div></div> <!-- that's a PNG with 8bit alpha containing two eyes -->
|
||||
<div class="nose"><div><div></div></div></div>
|
||||
<div class="empty"><div></div></div>
|
||||
|
||||
<div class="smile"><div><div><span><em><strong></strong></em></span></div></div></div>
|
||||
<div class="chin"><div> </div></div>
|
||||
<div class="parser-container"><div class="parser"><!-- ->ERROR<!- --></div></div> <!-- two dashes is what delimits a comment, so the text "->ERROR<!-" earlier on this line is actually part of a comment -->
|
||||
<ul>
|
||||
<li class="first-part"></li>
|
||||
<li class="second-part"></li>
|
||||
<li class="third-part"></li>
|
||||
<li class="fourth-part"></li>
|
||||
|
||||
</ul>
|
||||
<div class="image-height-test"><table><tr><td><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAFSDNYfAAAAaklEQVR42u3XQQrAIAwAQeP%2F%2F6wf8CJBJTK9lnQ7FpHGaOurt1I34nfH9pMMZAZ8BwMGEvvh%2BBsJCAgICLwIOA8EBAQEBAQEBAQEBK79H5RfIQAAAAAAAAAAAAAAAAAAAAAAAAAAAID%2FABMSqAfj%2FsLmvAAAAABJRU5ErkJggg%3D%3D" alt=""></td></tr></table></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -12,11 +12,8 @@
|
||||
|
||||
from __future__ import division, unicode_literals
|
||||
|
||||
import contextlib
|
||||
|
||||
from .testing_utils import (
|
||||
resource_filename, TestPNGDocument, assert_no_logs, capture_logs)
|
||||
from ..css import validation
|
||||
from ..formatting_structure import boxes, build, counters
|
||||
|
||||
|
||||
@ -79,40 +76,8 @@ def to_lists(box_tree):
|
||||
return serialize(unwrap_html_body(box_tree))
|
||||
|
||||
|
||||
def validate_float(
|
||||
real_non_shorthand, base_url, name, values, required=False):
|
||||
"""Fake validator for ``float``."""
|
||||
value = values[0].value
|
||||
if name == 'float' and value == 'left':
|
||||
return [(name, value)]
|
||||
return real_non_shorthand(base_url, name, values, required)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def monkeypatch_validation(replacement):
|
||||
"""Create a context manager patching the validation mechanism.
|
||||
|
||||
This is useful to change the behaviour of the validation for one property
|
||||
not yet supported, without affecting the validation for the other
|
||||
properties.
|
||||
|
||||
"""
|
||||
real_non_shorthand = validation.validate_non_shorthand
|
||||
|
||||
def patched_non_shorthand(*args, **kwargs):
|
||||
"""Wraps the validator into ``replacement``."""
|
||||
return replacement(real_non_shorthand, *args, **kwargs)
|
||||
|
||||
validation.validate_non_shorthand = patched_non_shorthand
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
validation.validate_non_shorthand = real_non_shorthand
|
||||
|
||||
|
||||
def parse(html_content):
|
||||
"""Parse some HTML, apply stylesheets and transform to boxes."""
|
||||
with monkeypatch_validation(validate_float):
|
||||
document = TestPNGDocument(html_content,
|
||||
# Dummy filename, but in the right directory.
|
||||
base_url=resource_filename('<test>'))
|
||||
@ -251,26 +216,19 @@ def test_inline_in_block():
|
||||
box = build.block_in_inline(box)
|
||||
assert_tree(box, expected)
|
||||
|
||||
# Floats however stay out of line boxes
|
||||
# Floats are pull to the top of their containing blocks
|
||||
source = '<p>Hello <em style="float: left">World</em>!</p>'
|
||||
expected = [
|
||||
box = parse(source)
|
||||
box = build.inline_in_block(box)
|
||||
box = build.block_in_inline(box)
|
||||
assert_tree(box, [
|
||||
('p', 'Block', [
|
||||
('p', 'AnonBlock', [
|
||||
('p', 'Line', [
|
||||
('p', 'Text', 'Hello ')])]),
|
||||
('p', 'Text', 'Hello '),
|
||||
('em', 'Block', [
|
||||
('em', 'Line', [
|
||||
('em', 'Text', 'World')])]),
|
||||
('p', 'AnonBlock', [
|
||||
('p', 'Line', [
|
||||
('p', 'Text', '!')])])])]
|
||||
box = parse(source)
|
||||
box = build.inline_in_block(box)
|
||||
import pprint
|
||||
pprint.pprint(to_lists(box))
|
||||
assert_tree(box, expected)
|
||||
box = build.block_in_inline(box)
|
||||
assert_tree(box, expected)
|
||||
('p', 'Text', '!')])])])
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
|
@ -17,12 +17,15 @@ import os.path
|
||||
import tempfile
|
||||
import shutil
|
||||
import itertools
|
||||
import operator
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
import pystacia
|
||||
|
||||
from ..compat import xrange
|
||||
from ..compat import xrange, ints_from_bytes
|
||||
from ..urls import ensure_url
|
||||
from .. import HTML, CSS
|
||||
from .testing_utils import (
|
||||
resource_filename, TestPNGDocument, FONTS, assert_no_logs, capture_logs)
|
||||
|
||||
@ -34,17 +37,6 @@ _ = b'\xff\xff\xff\xff' # white
|
||||
r = b'\xff\x00\x00\xff' # red
|
||||
B = b'\x00\x00\xff\xff' # blue
|
||||
BYTES_PER_PIXELS = 4
|
||||
PIXEL_FORMAT = 'rgba(%i, %i, %i, %i)'
|
||||
|
||||
|
||||
def format_pixel(pixels, width, x, y): # pragma: no cover
|
||||
"""Return the pixel color as ``#RRGGBB``."""
|
||||
start = (y * width + x) * BYTES_PER_PIXELS
|
||||
end = start + BYTES_PER_PIXELS
|
||||
pixel_bytes = pixels[start:end]
|
||||
# Py2/3 compat:
|
||||
pixel_ints = tuple(ord(byte) for byte in pixel_bytes.decode('latin1'))
|
||||
return PIXEL_FORMAT % pixel_ints
|
||||
|
||||
|
||||
def assert_pixels(name, expected_width, expected_height, expected_lines,
|
||||
@ -132,9 +124,11 @@ def document_to_pixels(document, name, expected_width, expected_height,
|
||||
"""
|
||||
Render an HTML document to PNG, checks its size and return pixel data.
|
||||
"""
|
||||
png_bytes = document.write_png()
|
||||
assert len(document.pages) == nb_pages
|
||||
return png_to_pixels(document.write_png(), expected_width, expected_height)
|
||||
|
||||
|
||||
def png_to_pixels(png_bytes, expected_width, expected_height):
|
||||
with contextlib.closing(pystacia.read_blob(png_bytes)) as image:
|
||||
assert image.size == (expected_width, expected_height)
|
||||
raw = image.get_raw('rgba')['raw']
|
||||
@ -142,21 +136,30 @@ def document_to_pixels(document, name, expected_width, expected_height,
|
||||
return raw
|
||||
|
||||
|
||||
def assert_pixels_equal(name, width, height, lines, expected_lines):
|
||||
def assert_pixels_equal(name, width, height, raw, expected_raw, tolerance=0):
|
||||
"""
|
||||
Take 2 matrices of height by width pixels and assert that they
|
||||
are the same.
|
||||
"""
|
||||
if lines != expected_lines: # pragma: no cover
|
||||
write_png(name + '.expected', expected_lines, width, height)
|
||||
write_png(name, lines, width, height)
|
||||
for y in xrange(height):
|
||||
for x in xrange(width):
|
||||
pixel = format_pixel(lines, width, x, y)
|
||||
expected_pixel = format_pixel(expected_lines, width, x, y)
|
||||
assert pixel == expected_pixel , (
|
||||
'Pixel (%i, %i) in %s: expected %s, got %s' % (
|
||||
x, y, name, expected_pixel, pixel))
|
||||
if raw != expected_raw: # pragma: no cover
|
||||
for i, (value, expected) in enumerate(zip(
|
||||
ints_from_bytes(raw),
|
||||
ints_from_bytes(expected_raw)
|
||||
)):
|
||||
if abs(value - expected) > tolerance:
|
||||
write_png(name, raw, width, height)
|
||||
write_png(name + '.expected', expected_raw,
|
||||
width, height)
|
||||
pixel_n = i // BYTES_PER_PIXELS
|
||||
x = pixel_n // width
|
||||
y = pixel_n % width
|
||||
i % BYTES_PER_PIXELS
|
||||
pixel = tuple(ints_from_bytes(raw[i:i + BYTES_PER_PIXELS]))
|
||||
expected_pixel = tuple(ints_from_bytes(
|
||||
expected_raw[i:i + BYTES_PER_PIXELS]))
|
||||
assert 0, (
|
||||
'Pixel (%i, %i) in %s: expected rgba%s, got rgab%s'
|
||||
% (x, y, name, expected_pixel, pixel))
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
@ -1916,3 +1919,24 @@ def test_2d_transform():
|
||||
</style>
|
||||
<div><img src="pattern.png"></div>
|
||||
''')
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_acid2():
|
||||
"""A local version of http://acid2.acidtests.org/"""
|
||||
def get_png_pages(filename):
|
||||
return HTML(resource_filename(filename)).get_png_pages()
|
||||
|
||||
with capture_logs():
|
||||
# This is a copy of http://www.webstandards.org/files/acid2/test.html
|
||||
intro_page, test_page = get_png_pages('acid2-test.html')
|
||||
# Ignore the intro page: it is not in the reference
|
||||
width, height, test_png = test_page
|
||||
|
||||
# This is a copy of http://www.webstandards.org/files/acid2/reference.html
|
||||
(ref_width, ref_height, ref_png), = get_png_pages('acid2-reference.html')
|
||||
|
||||
assert (width, height) == (ref_width, ref_height)
|
||||
assert_pixels_equal(
|
||||
'acid2', width, height, png_to_pixels(test_png, width, height),
|
||||
png_to_pixels(ref_png, width, height), tolerance=2)
|
||||
|
@ -15,7 +15,6 @@ from __future__ import division, unicode_literals
|
||||
|
||||
from .testing_utils import (
|
||||
TestPNGDocument, resource_filename, FONTS, assert_no_logs, capture_logs)
|
||||
from .test_boxes import monkeypatch_validation, validate_float
|
||||
from ..formatting_structure import boxes
|
||||
from ..layout.inlines import split_inline_box
|
||||
from ..layout.percentages import resolve_percentages
|
||||
@ -39,8 +38,6 @@ def parse_without_layout(html_content):
|
||||
|
||||
def parse(html_content, return_document=False):
|
||||
"""Parse some HTML, apply stylesheets, transform to boxes and lay out."""
|
||||
# TODO: remove this patching when floats are validated
|
||||
with monkeypatch_validation(validate_float):
|
||||
document = TestPNGDocument(html_content,
|
||||
base_url=resource_filename('<inline HTML>'))
|
||||
if return_document:
|
||||
@ -49,6 +46,12 @@ def parse(html_content, return_document=False):
|
||||
return document.pages
|
||||
|
||||
|
||||
def outer_area(box):
|
||||
"""Return the (x, y, w, h) rectangle for the outer area of a box."""
|
||||
return (box.position_x, box.position_y,
|
||||
box.margin_width(), box.margin_height())
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_page():
|
||||
"""Test the layout for ``@page`` properties."""
|
||||
@ -1817,7 +1820,8 @@ def test_inlinebox_spliting():
|
||||
while 1:
|
||||
inlinebox.position_y = 0
|
||||
box, skip, _ = split_inline_box(
|
||||
document, inlinebox, 0, width, skip, parent, None, [], [], [])
|
||||
document, inlinebox, 0, width, skip, parent, None,
|
||||
[], [], [], [])
|
||||
yield box
|
||||
if skip is None:
|
||||
break
|
||||
@ -1931,7 +1935,7 @@ def test_inlinebox_text_after_spliting():
|
||||
while 1:
|
||||
inlinebox.position_y = 0
|
||||
box, skip, _ = split_inline_box(
|
||||
document, inlinebox, 0, 100, skip, paragraph, None, [], [], [])
|
||||
document, inlinebox, 0, 100, skip, paragraph, None, [], [], [], [])
|
||||
parts.append(box)
|
||||
if skip is None:
|
||||
break
|
||||
@ -4090,3 +4094,137 @@ def test_absolute_images():
|
||||
assert (img2.width, img2.height) == (4, 4)
|
||||
|
||||
# TODO: test the various cases in absolute_replaced()
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_floats():
|
||||
# adjacent-floats-001
|
||||
page, = parse('''
|
||||
<style>
|
||||
div { float: left }
|
||||
img { width: 100px; vertical-align: top }
|
||||
</style>
|
||||
<div><img src=pattern.png /></div>
|
||||
<div><img src=pattern.png /></div>
|
||||
''')
|
||||
html, = page.children
|
||||
body, = html.children
|
||||
div_1, div_2 = body.children
|
||||
assert outer_area(div_1) == (0, 0, 100, 100)
|
||||
assert outer_area(div_2) == (100, 0, 100, 100)
|
||||
|
||||
# c414-flt-fit-000
|
||||
page, = parse('''
|
||||
<style>
|
||||
body { width: 290px }
|
||||
div { float: left; width: 100px; }
|
||||
img { width: 60px; vertical-align: top }
|
||||
</style>
|
||||
<div><img src=pattern.png /><!-- 1 --></div>
|
||||
<div><img src=pattern.png /><!-- 2 --></div>
|
||||
<div><img src=pattern.png /><!-- 4 --></div>
|
||||
<img src=pattern.png /><!-- 3 -->
|
||||
<img src=pattern.png /><!-- 5 -->
|
||||
''')
|
||||
html, = page.children
|
||||
body, = html.children
|
||||
div_1, div_2, div_4, anon_block = body.children
|
||||
line_3, line_5 = anon_block.children
|
||||
img_3, = line_3.children
|
||||
img_5, = line_5.children
|
||||
assert outer_area(div_1) == (0, 0, 100, 60)
|
||||
assert outer_area(div_2) == (100, 0, 100, 60)
|
||||
assert outer_area(img_3) == (200, 0, 60, 60)
|
||||
|
||||
assert outer_area(div_4) == (0, 60, 100, 60)
|
||||
assert outer_area(img_5) == (100, 60, 60, 60)
|
||||
|
||||
# c414-flt-fit-002
|
||||
page, = parse('''
|
||||
<style type="text/css">
|
||||
body { width: 200px }
|
||||
p { width: 70px; height: 20px }
|
||||
.left { float: left }
|
||||
.right { float: right }
|
||||
</style>
|
||||
<p class="left"> ⇦ A 1 </p>
|
||||
<p class="left"> ⇦ B 2 </p>
|
||||
<p class="left"> ⇦ A 3 </p>
|
||||
<p class="right"> B 4 ⇨ </p>
|
||||
<p class="left"> ⇦ A 5 </p>
|
||||
<p class="right"> B 6 ⇨ </p>
|
||||
<p class="right"> B 8 ⇨ </p>
|
||||
<p class="left"> ⇦ A 7 </p>
|
||||
<p class="left"> ⇦ A 9 </p>
|
||||
<p class="left"> ⇦ B 10 </p>
|
||||
''')
|
||||
html, = page.children
|
||||
body, = html.children
|
||||
positions = [(paragraph.position_x, paragraph.position_y)
|
||||
for paragraph in body.children]
|
||||
assert positions == [
|
||||
(0, 0), (70, 0), (0, 20), (130, 20), (0, 40), (130, 40),
|
||||
(130, 60), (0, 60), (0, 80), (70, 80), ]
|
||||
|
||||
# c414-flt-wrap-000 ... more or less
|
||||
page, = parse('''
|
||||
<style>
|
||||
body { width: 100px }
|
||||
p { float: left; height: 100px }
|
||||
img { width: 60px; vertical-align: top }
|
||||
</style>
|
||||
<p style="width: 20px"></p>
|
||||
<p style="width: 100%"></p>
|
||||
<img src=pattern.png /><img src=pattern.png />
|
||||
''')
|
||||
html, = page.children
|
||||
body, = html.children
|
||||
p_1, p_2, anon_block = body.children
|
||||
line_1, line_2 = anon_block.children
|
||||
assert anon_block.position_y == 0
|
||||
assert (line_1.position_x, line_1.position_y) == (20, 0)
|
||||
assert (line_2.position_x, line_2.position_y) == (0, 200)
|
||||
|
||||
# floats-placement-vertical-001b
|
||||
page, = parse('''
|
||||
<style>
|
||||
body { width: 90px; font-size: 0 }
|
||||
img { vertical-align: top }
|
||||
</style>
|
||||
<body>
|
||||
<span>
|
||||
<img src=pattern.png style="width: 50px" />
|
||||
<img src=pattern.png style="width: 50px" />
|
||||
<img src=pattern.png style="float: left; width: 30px" />
|
||||
</span>
|
||||
''')
|
||||
html, = page.children
|
||||
body, = html.children
|
||||
line_1, line_2 = body.children
|
||||
span_1, = line_1.children
|
||||
span_2, = line_2.children
|
||||
img_1, = span_1.children
|
||||
img_2, img_3 = span_2.children
|
||||
assert outer_area(img_1) == (0, 0, 50, 50)
|
||||
assert outer_area(img_2) == (30, 50, 50, 50)
|
||||
assert outer_area(img_3) == (0, 50, 30, 30)
|
||||
|
||||
# Variant of the above: no <span>
|
||||
page, = parse('''
|
||||
<style>
|
||||
body { width: 90px; font-size: 0 }
|
||||
img { vertical-align: top }
|
||||
</style>
|
||||
<body>
|
||||
<img src=pattern.png style="width: 50px" />
|
||||
<img src=pattern.png style="width: 50px" />
|
||||
<img src=pattern.png style="float: left; width: 30px" />
|
||||
''')
|
||||
html, = page.children
|
||||
body, = html.children
|
||||
line_1, line_2 = body.children
|
||||
img_1, = line_1.children
|
||||
img_2, img_3 = line_2.children
|
||||
assert outer_area(img_1) == (0, 0, 50, 50)
|
||||
assert outer_area(img_2) == (30, 50, 50, 50)
|
||||
assert outer_area(img_3) == (0, 50, 30, 30)
|
||||
|
@ -90,16 +90,14 @@ def test_bookmarks():
|
||||
|
||||
root, bookmarks = get_bookmarks('''
|
||||
<style>
|
||||
* { height: 90pt; margin: 0 0 10pt 0; page-break-inside: auto }
|
||||
* { height: 90pt; margin: 0 0 10pt 0 }
|
||||
</style>
|
||||
<h1>Title 1</h1>
|
||||
<h1>Title 2</h1>
|
||||
<div style="margin-left: 20pt"><h2>Title 3</h2></div>
|
||||
<h2 style="position: relative; left: 20pt">Title 3</h2>
|
||||
<h2>Title 4</h2>
|
||||
<h3>
|
||||
Title 5
|
||||
<h3>Title 5</h3>
|
||||
<span style="display: block; page-break-before: always"></span>
|
||||
</h3>
|
||||
<h2>Title 6</h2>
|
||||
<h1>Title 7</h1>
|
||||
<h2>Title 8</h2>
|
||||
|
@ -26,14 +26,16 @@ import pystacia
|
||||
from weasyprint import HTML, CSS
|
||||
from weasyprint.compat import urlopen
|
||||
|
||||
from .web import read_testinfo
|
||||
|
||||
|
||||
TEST_SUITE_VERSION = '20110323'
|
||||
|
||||
#BASE_URL = 'http://test.csswg.org/suites/css2.1/{}/html4/'
|
||||
# Download and extract the zip file from http://test.csswg.org/suites/css2.1/
|
||||
BASE_URL = 'file://' + os.path.expanduser('~/css2.1_test_suite/{}/html4/')
|
||||
|
||||
BASE_URL = BASE_URL.format(TEST_SUITE_VERSION)
|
||||
BASE_PATH = os.path.expanduser(
|
||||
'~/css2.1_test_suite/{}/html4/').format(TEST_SUITE_VERSION)
|
||||
BASE_URL = 'file://' + BASE_PATH
|
||||
|
||||
RESULTS_DIRECTORY = os.path.join(os.path.dirname(__file__), 'test_results')
|
||||
|
||||
@ -41,6 +43,8 @@ PAGE_SIZE_STYLESHEET = CSS(string='''
|
||||
@page { margin: 0; -weasy-size: 640px }
|
||||
''')
|
||||
|
||||
IGNORED_FLAGS = {'interact', 'dom'}
|
||||
|
||||
|
||||
def get_url(url):
|
||||
return closing(urlopen(BASE_URL + url))
|
||||
@ -73,8 +77,8 @@ def make_test_suite():
|
||||
rendered = {} # Memoize
|
||||
|
||||
def render(name):
|
||||
result = rendered.get(name)
|
||||
if result is None:
|
||||
raw = rendered.get(name)
|
||||
if raw is None:
|
||||
# name is sometimes "support/something.htm"
|
||||
basename = os.path.basename(name)
|
||||
png_filename = os.path.join(RESULTS_DIRECTORY, basename + '.png')
|
||||
@ -83,7 +87,11 @@ def make_test_suite():
|
||||
with closing(pystacia.read(png_filename)) as image:
|
||||
raw = image.get_raw('rgba')
|
||||
rendered[name] = raw
|
||||
return result
|
||||
return raw
|
||||
|
||||
flags_by_id = {
|
||||
test['test_id'] + '.htm': test['flags']
|
||||
for test in read_testinfo(BASE_PATH)}
|
||||
|
||||
for equal, test, references in get_test_list():
|
||||
# Use default parameter values to bind to the current values,
|
||||
@ -93,6 +101,7 @@ def make_test_suite():
|
||||
references_render = map(render, references)
|
||||
return all((test_render != ref_render) ^ equal
|
||||
for ref_render in references_render)
|
||||
if not set(flags_by_id[test]).intersection(IGNORED_FLAGS):
|
||||
yield test, test_function
|
||||
|
||||
|
||||
|
@ -24,7 +24,7 @@ from weasyprint import HTML, CSS
|
||||
|
||||
|
||||
def split(something):
|
||||
return something.split(',') if something else ''
|
||||
return something.split(',') if something else []
|
||||
|
||||
|
||||
def read_testinfo(suite_directory):
|
||||
|
@ -114,9 +114,13 @@ def show_first_line(cairo_context, pango_layout, hinting):
|
||||
PangoCairo.show_layout_line(cairo_context, lines[0])
|
||||
|
||||
|
||||
def line_widths(document, box, width):
|
||||
def line_widths(document, box, width, skip=None):
|
||||
"""Return the width for each line."""
|
||||
layout = create_layout(box.text, box.style, document.enable_hinting, width)
|
||||
# TODO: without the lstrip, we get an extra empty line at the beginning. Is
|
||||
# there a better solution to avoid that?
|
||||
layout = create_layout(
|
||||
box.text[(skip or 0):].lstrip(), box.style,
|
||||
document.enable_hinting, width)
|
||||
for line in layout.get_lines_readonly():
|
||||
_ink_extents, logical_extents = line.get_extents()
|
||||
yield Pango.units_to_double(logical_extents.width)
|
||||
|
Loading…
Reference in New Issue
Block a user