1
1
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:
Simon Sapin 2012-06-29 11:40:00 +02:00
commit 80af286655
22 changed files with 1053 additions and 334 deletions

View File

@ -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)

View File

@ -255,6 +255,7 @@ TABLE_WRAPPER_BOX_PROPERTIES = set('''
transform_origin
vertical_align
z_index
clear
'''.split())

View File

@ -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."""

View File

@ -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):

View File

@ -291,6 +291,7 @@ class BlockLevelBox(Box):
``table`` generates a block-level box.
"""
clearance = None
class BlockContainerBox(ParentBox):

View File

@ -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
new_child = block_in_inline(child)
# block_level_box is still None.
if new_child is not child:
changed = True

View File

@ -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

View File

@ -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():
child.position_y += collapse_margin(adjoining_margins)
if child.is_absolutely_positioned():
child.position_y += collapse_margin(adjoining_margins)
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
@ -566,7 +637,7 @@ def find_earlier_page_break(children, absolute_boxes, fixed_boxes):
resume_at = (new_child.index, resume_at)
break
elif isinstance(child, boxes.TableBox):
pass # TODO: find an earlier break between table rows.
pass # TODO: find an earlier break between table rows.
if child.is_in_normal_flow():
if previous_in_flow is not None and (
block_level_page_break(child, previous_in_flow)

184
weasyprint/layout/float.py Normal file
View 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

View File

@ -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,66 +45,101 @@ 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)
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)
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
remove_last_whitespace(document, line)
excluded_shapes = document.excluded_shapes[:]
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.
line.baseline = line.margin_top
line.height += line.margin_top + line.margin_bottom
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 = []
line, resume_at, preserved_line_break = split_inline_box(
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)
if bottom is None:
# No children at all
offset_y = 0
if preserved_line_break:
# Only the strut.
line.baseline = line.margin_top
line.height += line.margin_top + line.margin_bottom
else:
line.height = 0
line.baseline = 0
else:
line.height = 0
line.baseline = 0
else:
assert top is not None
line.baseline = -top
line.position_y = top
line.height = bottom - top
offset_y = position_y - top
line.margin_top = 0
line.margin_bottom = 0
if offset_x != 0 or offset_y != 0:
# This also translates children
line.translate(offset_x, offset_y)
assert top is not None
line.baseline = -top
line.position_y = top
line.height = bottom - top
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:
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'):
@ -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,51 +526,74 @@ 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)
position_x += left_spacing
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)
line_placeholders.append(placeholder)
children.append(placeholder)
if child.is_absolutely_positioned():
child.position_x = position_x
placeholder = AbsolutePlaceholder(child)
line_placeholders.append(placeholder)
children.append(placeholder)
absolute_boxes.append(placeholder)
if child.style.position == 'absolute':
absolute_boxes.append(placeholder)
if child.style.position == 'absolute':
absolute_boxes.append(placeholder)
else:
fixed_boxes.append(placeholder)
elif child.style.float in ('left', 'right'):
# TODO: Floats
assert 0
else:
fixed_boxes.append(placeholder)
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:
sub_max_y, sub_min_y = aligned_subtree_verticality(
subtree, top_bottom_subtrees, baseline_y=0)
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
)
max_y = max(max_y, min_y + highest_sub)
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

View 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

View File

@ -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

View File

@ -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:
current_line = max(widths)
else:
assert isinstance(child, boxes.TextBox)
current_line = max(text.line_widths(document, child, width=0))
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(

View 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&nbsp;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 &quot;Hello World!&quot; on the test page in the same way that this paragraph is rendered below that text on this page."></a></p>
</body>
</html>

View 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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</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>&nbsp;</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>

View File

@ -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,45 +76,13 @@ 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>'))
box, = build.dom_to_box(document, document.dom)
return box
document = TestPNGDocument(html_content,
# Dummy filename, but in the right directory.
base_url=resource_filename('<test>'))
box, = build.dom_to_box(document, document.dom)
return box
def parse_all(html_content, 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 = [
('p', 'Block', [
('p', 'AnonBlock', [
('p', 'Line', [
('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)
assert_tree(box, [
('p', 'Block', [
('p', 'Line', [
('p', 'Text', 'Hello '),
('em', 'Block', [
('em', 'Line', [
('em', 'Text', 'World')])]),
('p', 'Text', '!')])])])
@assert_no_logs

View File

@ -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)

View File

@ -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,14 +38,18 @@ 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:
return document
else:
return document.pages
document = TestPNGDocument(html_content,
base_url=resource_filename('<inline HTML>'))
if return_document:
return document
else:
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
@ -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)

View File

@ -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
<span style="display: block; page-break-before: always"></span>
</h3>
<h3>Title 5</h3>
<span style="display: block; page-break-before: always"></span>
<h2>Title 6</h2>
<h1>Title 7</h1>
<h2>Title 8</h2>

View File

@ -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,7 +101,8 @@ def make_test_suite():
references_render = map(render, references)
return all((test_render != ref_render) ^ equal
for ref_render in references_render)
yield test, test_function
if not set(flags_by_id[test]).intersection(IGNORED_FLAGS):
yield test, test_function
def main():

View File

@ -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):

View File

@ -70,7 +70,7 @@ def create_layout(text, style, hinting, max_width):
' ', '<span letter_spacing="%i"> </span>' % (
word_spacing + letter_spacing,))
markup = '<span letter_spacing="%i">%s</span>' % (
letter_spacing , markup)
letter_spacing, markup)
attributes_list = Pango.parse_markup(markup, -1, '\x00')[1]
layout.set_attributes(attributes_list)
return layout
@ -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)