1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-05 00:21:15 +03:00

Implement {min,max}-{width,height} on images.

This commit is contained in:
Simon Sapin 2012-04-06 19:44:09 +02:00
parent 27245aa915
commit 7ac982a01c
4 changed files with 185 additions and 68 deletions

View File

@ -4,7 +4,8 @@ Version 0.8, released on 2012-XX-XX
* Switch from cssutils to tinycss as the CSS parser.
* Great speed improvements, in part thanks to tinycss.
* Add support for the ``min-width``, ``max-width``, ``min-height`` and
``max-height`` properties, but only on blocks for now.
``max-height`` properties, but only on blocks and images (replaced
elements) for now.
* Many bug fixes

View File

@ -11,9 +11,10 @@
"""
from __future__ import division, unicode_literals
import functools
from .inlines import iter_line_boxes, replaced_box_width, replaced_box_height
from .inlines import (iter_line_boxes, replaced_box_width, replaced_box_height,
handle_min_max_width, min_max_replaced_height,
min_max_auto_replaced)
from .markers import list_marker_layout
from .tables import table_layout, fixed_table_layout
from .percentages import resolve_percentages
@ -69,41 +70,33 @@ def block_box_layout(document, box, max_position_y, skip_stack,
return new_box, resume_at, next_page, adjoining_margins, collapsing_through
def block_replaced_box_layout(box, containing_block, device_size):
"""Lay out the block :class:`boxes.ReplacedBox` ``box``."""
assert isinstance(box, boxes.ReplacedBox)
resolve_percentages(box, containing_block)
def block_replaced_width(box, containing_block, device_size):
# http://www.w3.org/TR/CSS21/visudet.html#block-replaced-width
replaced_box_width(box, device_size)
block_level_width(box, containing_block)
# http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
replaced_box_height(box, device_size)
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)
min_max_auto_replaced(box)
else:
min_max_block_replaced_width(box, containing_block, device_size)
min_max_replaced_height(box, device_size)
return box
def handle_min_max_width(function):
@functools.wraps(function)
def wrapper(box, containing_block):
computed_margins = box.margin_left, box.margin_right
function(box, containing_block)
if box.width > box.max_width:
box.width = box.max_width
box.margin_left, box.margin_right = computed_margins
function(box, containing_block)
if box.width < box.min_width:
box.width = box.min_width
box.margin_left, box.margin_right = computed_margins
function(box, containing_block)
return wrapper
@handle_min_max_width
def block_level_width(box, containing_block):
"""Set the ``box`` width."""

View File

@ -11,6 +11,7 @@
"""
from __future__ import division, unicode_literals
import functools
import cairo
@ -184,31 +185,11 @@ def remove_last_whitespace(document, box):
# 'white-space' set to 'pre-wrap', UAs may visually collapse them.
def inline_replaced_box_layout(box, containing_block, device_size):
"""Lay out an inline :class:`boxes.ReplacedBox` ``box``."""
assert isinstance(box, boxes.ReplacedBox)
# Compute width:
# http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
if box.margin_left == 'auto':
box.margin_left = 0
if box.margin_right == 'auto':
box.margin_right = 0
replaced_box_width(box, device_size)
# Compute height
# http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
if box.margin_top == 'auto':
box.margin_top = 0
if box.margin_bottom == 'auto':
box.margin_bottom = 0
replaced_box_height(box, device_size)
def replaced_box_width(box, device_size):
"""
Compute and set the used width for replaced boxes (inline- or block-level)
"""
# http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
_surface, intrinsic_width, _intrinsic_height = box.replacement
# TODO: update this when we have replaced elements that do not
# always have an intrinsic width. (See commented code below.)
@ -250,6 +231,7 @@ def replaced_box_height(box, device_size):
"""
Compute and set the used height for replaced boxes (inline- or block-level)
"""
# http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
_surface, intrinsic_width, intrinsic_height = box.replacement
# TODO: update this when we have replaced elements that do not
# always have intrinsic dimensions. (See commented code below.)
@ -281,6 +263,120 @@ 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)
def inline_replaced_box_layout(box, containing_block, device_size):
"""Lay out an inline :class:`boxes.ReplacedBox` ``box``."""
for side in ['top', 'right', 'bottom', 'left']:
if getattr(box, 'margin_' + side) == 'auto':
setattr(box, 'margin_' + side, 0)
if box.style.width == 'auto' and box.style.height == 'auto':
replaced_box_width(box, device_size)
replaced_box_height(box, device_size)
min_max_auto_replaced(box)
else:
min_max_replaced_width(box, device_size)
min_max_replaced_height(box, device_size)
def min_max_auto_replaced(box):
"""Resolve {min,max}-{width,height} constraints on replaced elements
that have 'auto' width and heights.
"""
width = box.width
height = box.height
min_width = box.min_width
min_height = box.min_height
max_width = max(min_width, box.max_width)
max_height = max(min_height, box.max_height)
# (violation_width, violation_height)
violations = (
'min' if width < min_width else 'max' if width > max_width else '',
'min' if height < min_height else 'max' if height > max_height else '')
# Work around divisions by zero. These are pathological cases anyway.
if width == 0:
width = 1e-6
if height == 0:
height = 1e-6
# ('', ''): nothing to do
if violations == ('max', ''):
box.width = max_width
box.height = max(max_width * height / width, min_height)
elif violations == ('min', ''):
box.width = min_width
box.height = min(min_width * height / width, max_height)
elif violations == ('', 'max'):
box.width = max(max_height * width / height, min_width)
box.height = max_height
elif violations == ('', 'min'):
box.width = min(min_height * width / height, max_width)
box.height = min_height
elif violations == ('max', 'max'):
if max_width / width <= max_height / height:
box.width = max_width
box.height = max(min_height, max_width * height / width)
else:
box.width = max(min_width, max_height * width / height)
box.height = max_height
elif violations == ('min', 'min'):
if min_width / width <= min_height / height:
box.width = min(max_width, min_height * width / height)
box.height = min_height
else:
box.width = min_width
box.height = min(max_height, min_width * height / width)
elif violations == ('min', 'max'):
box.width = min_width
box.height = max_height
elif violations == ('max', 'min'):
box.width = max_width
box.height = min_height
def atomic_box(box, containing_block, device_size):
"""Compute the width and the height of the atomic ``box``."""
if isinstance(box, boxes.ReplacedBox):

View File

@ -1048,6 +1048,14 @@ def test_whitespace_processing():
@assert_no_logs
def test_images():
"""Test that width, height and ratio of images are respected."""
def get_img(html):
page, = parse(html)
html, = page.children
body, = html.children
line, = body.children
img, = line.children
return body, img
# Try a few image formats
for html in [
'<img src="%s">' % url for url in [
@ -1066,21 +1074,13 @@ def test_images():
'<object data=really-a-png.svg type=image/png>',
'<object data=really-a-svg.png type=image/svg+xml>',
]:
page, = parse(html)
html, = page.children
body, = html.children
line, = body.children
img, = line.children
body, img = get_img(html)
assert img.width == 4
assert img.height == 4
# With physical units
url = "data:image/svg+xml,<svg width='2.54cm' height='0.5in'></svg>"
page, = parse('<img src="%s">' % url)
html, = page.children
body, = html.children
line, = body.children
img, = line.children
body, img = get_img('<img src="%s">' % url)
assert img.width == 96
assert img.height == 48
@ -1100,28 +1100,38 @@ def test_images():
'really-a-svg.png',
]:
with capture_logs() as logs:
page, = parse("<p><img src='%s' alt='invalid image'>" % url)
body, img = get_img("<img src='%s' alt='invalid image'>" % url)
assert len(logs) == 1
assert 'WARNING: Error for image' in logs[0]
html, = page.children
body, = html.children
paragraph, = body.children
line, = paragraph.children
img, = line.children
assert isinstance(img, boxes.InlineBox) # not a replaced box
text, = img.children
assert text.text == 'invalid image', url
# Layout rules try to preserve the ratio, so the height should be 40px too:
page, = parse('<img src="pattern.png" style="width: 40px">')
html, = page.children
body, = html.children
line, = body.children
img, = line.children
body, img = get_img('<img src="pattern.png" style="width: 40px">')
assert body.height == 40
assert img.position_y == 0
assert img.width == 40
assert img.height == 40
# Same with percentages
body, img = get_img('''<p style="width: 200px">
<img src="pattern.png" style="width: 20%">''')
assert body.height == 40
assert img.position_y == 0
assert img.width == 40
assert img.height == 40
body, img = get_img('<img src="pattern.png" style="min-width: 40px">')
assert body.height == 40
assert img.position_y == 0
assert img.width == 40
assert img.height == 40
body, img = get_img('<img src="pattern.png" style="max-width: 2px">')
assert img.width == 2
assert img.height == 2
# display: table-cell is ignored
page, = parse('''
<img src="pattern.png" style="width: 40px">
@ -1157,6 +1167,23 @@ def test_images():
assert img.content_box_x() == 30 # (100 - 40) / 2 == 30px for margin-left
assert img.content_box_y() == 10
page, = parse('''
<style>
@page { -weasy-size: 100px }
img { min-width: 40%; margin: 10px auto; display: block }
</style>
<body>
<img src="pattern.png">
''')
html, = page.children
body, = html.children
img, = body.children
assert img.element_tag == 'img'
assert img.position_x == 0
assert img.position_y == 0
assert img.content_box_x() == 30 # (100 - 40) / 2 == 30px for margin-left
assert img.content_box_y() == 10
@assert_no_logs