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:
parent
27245aa915
commit
7ac982a01c
3
CHANGES
3
CHANGES
@ -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
|
||||
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user