1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-04 07:57:52 +03:00

Do not keep tokens for percentages after validation.

Make (value, unit) namedtuples instead.
Pixel values that could be percentages are not plain numbers anymore.
This commit is contained in:
Simon Sapin 2012-04-03 14:59:06 +02:00
parent d785f4e46d
commit 88c027117c
13 changed files with 350 additions and 366 deletions

View File

@ -15,7 +15,10 @@ from __future__ import division, unicode_literals
import math
from .properties import INITIAL_VALUES
from .properties import INITIAL_VALUES, Dimension
ZERO_PIXELS = Dimension(0, 'px')
# How many CSS pixels is one <unit>?
@ -95,42 +98,45 @@ FONT_WEIGHT_RELATIVE = dict(
# name=(width in pixels, height in pixels)
PAGE_SIZES = dict(
A5=(
148 * LENGTHS_TO_PIXELS['mm'],
210 * LENGTHS_TO_PIXELS['mm'],
Dimension(148, 'mm'),
Dimension(210, 'mm'),
),
A4=(
210 * LENGTHS_TO_PIXELS['mm'],
297 * LENGTHS_TO_PIXELS['mm'],
Dimension(210, 'mm'),
Dimension(297, 'mm'),
),
A3=(
297 * LENGTHS_TO_PIXELS['mm'],
420 * LENGTHS_TO_PIXELS['mm'],
Dimension(297, 'mm'),
Dimension(420, 'mm'),
),
B5=(
176 * LENGTHS_TO_PIXELS['mm'],
250 * LENGTHS_TO_PIXELS['mm'],
Dimension(176, 'mm'),
Dimension(250, 'mm'),
),
B4=(
250 * LENGTHS_TO_PIXELS['mm'],
353 * LENGTHS_TO_PIXELS['mm'],
Dimension(250, 'mm'),
Dimension(353, 'mm'),
),
letter=(
8.5 * LENGTHS_TO_PIXELS['in'],
11 * LENGTHS_TO_PIXELS['in'],
Dimension(8.5, 'in'),
Dimension(11, 'in'),
),
legal=(
8.5 * LENGTHS_TO_PIXELS['in'],
14 * LENGTHS_TO_PIXELS['in'],
Dimension(8.5, 'in'),
Dimension(14, 'in'),
),
ledger=(
11 * LENGTHS_TO_PIXELS['in'],
17 * LENGTHS_TO_PIXELS['in'],
Dimension(11, 'in'),
Dimension(17, 'in'),
),
)
# In "portrait" orientation.
for w, h in PAGE_SIZES.values():
assert w < h
assert w.value < h.value
INITIAL_VALUES['size'] = PAGE_SIZES['A4']
INITIAL_PAGE_SIZE = PAGE_SIZES['A4']
INITIAL_VALUES['size'] = tuple(
d.value * LENGTHS_TO_PIXELS[d.unit] for d in INITIAL_PAGE_SIZE)
def _computing_order():
@ -199,7 +205,8 @@ def compute(element, pseudo_type, specified, computed, parent_style):
return computed
# Let's be coherent, always use ``name`` as an argument even when it is useless
# Let's be consistent, always use ``name`` as an argument even when
# it is useless.
# pylint: disable=W0613
@register_computer('background-color')
@ -217,13 +224,19 @@ def other_color(computer, name, value):
@register_computer('background-position')
@register_computer('transform-origin')
def length_or_percentage_tuple(computer, name, values):
"""Compute the lists of lengths that can be percentages."""
return tuple(length(computer, name, value) for value in values)
@register_computer('border-spacing')
@register_computer('size')
@register_computer('clip')
@register_computer('transform-origin')
def length_list(computer, name, values):
def length_tuple(computer, name, values):
"""Compute the properties with a list of lengths."""
return [length(computer, name, value) for value in values]
return tuple(length(computer, name, value, pixels_only=True)
for value in values)
@register_computer('top')
@ -236,35 +249,42 @@ def length_list(computer, name, values):
@register_computer('margin-left')
@register_computer('height')
@register_computer('width')
@register_computer('letter-spacing')
@register_computer('padding-top')
@register_computer('padding-right')
@register_computer('padding-bottom')
@register_computer('padding-left')
@register_computer('text-indent')
def length(computer, name, value, font_size=None):
def length(computer, name, value, font_size=None, pixels_only=False):
"""Compute a length ``value``."""
if (getattr(value, 'type', 'other') in ('NUMBER', 'INTEGER')
and value.value == 0):
return 0
if value.value == 0:
return 0 if pixels_only else ZERO_PIXELS
if getattr(value, 'type', 'other') != 'DIMENSION':
# No conversion needed.
return value
if value.unit in LENGTHS_TO_PIXELS:
unit = value.unit
if unit == 'px':
return value.value if pixels_only else value
elif unit in LENGTHS_TO_PIXELS:
# Convert absolute lengths to pixels
factor = LENGTHS_TO_PIXELS[value.unit]
elif value.unit in ('em', 'ex'):
factor = LENGTHS_TO_PIXELS[unit]
elif unit in ('em', 'ex'):
if font_size is None:
factor = computer.computed.font_size
else:
factor = font_size
if value.unit == 'ex':
if unit == 'ex':
# TODO: find a better way to measure ex, see
# http://www.w3.org/TR/CSS21/syndata.html#length-units
factor *= 0.5
else:
# A percentage or 'auto': no conversion needed.
return value
return value.value * factor
result = value.value * factor
return result if pixels_only else Dimension(result, 'px')
@register_computer('letter-spacing')
def pixel_length(computer, name, value):
return length(computer, name, value, pixels_only=True)
@register_computer('background-size')
@ -273,7 +293,7 @@ def background_size(computer, name, value):
if value in ('contain', 'cover'):
return value
else:
return length_list(computer, name, value)
return length_or_percentage_tuple(computer, name, value)
@register_computer('border-top-width')
@ -289,7 +309,7 @@ def border_width(computer, name, value):
if value in BORDER_WIDTH_KEYWORDS:
return BORDER_WIDTH_KEYWORDS[value]
return length(computer, name, value)
return length(computer, name, value, pixels_only=True)
@register_computer('content')
@ -342,27 +362,14 @@ def font_size(computer, name, value):
"""Compute the ``font-size`` property."""
if value in FONT_SIZE_KEYWORDS:
return FONT_SIZE_KEYWORDS[value]
# TODO: support 'larger' and 'smaller'
parent_font_size = computer.parent_style['font_size']
if value.type == 'DIMENSION':
if value.unit == 'px':
factor = 1
elif value.unit == 'em':
factor = parent_font_size
elif value.unit == 'ex':
# TODO: find a better way to measure ex, see
# http://www.w3.org/TR/CSS21/syndata.html#length-units
factor = parent_font_size * 0.5
elif value.unit in LENGTHS_TO_PIXELS:
factor = LENGTHS_TO_PIXELS[value.unit]
elif value.type == 'PERCENTAGE':
factor = parent_font_size / 100.
elif value.type in ('NUMBER', 'INTEGER') and value.value == 0:
return 0
# Raise if `factor` is not defined. It should be, because of validation.
return value.value * factor
if value.unit == '%':
return value.value * parent_font_size / 100.
else:
return length(computer, name, value, pixels_only=True,
font_size=parent_font_size)
@register_computer('font-weight')
@ -384,38 +391,52 @@ def font_weight(computer, name, value):
@register_computer('line-height')
def line_height(computer, name, value):
"""Compute the ``line-height`` property."""
# No .type attribute: already computed
if value == 'normal' or not hasattr(value, 'type'):
if value == 'normal':
return value
elif value.type in ('NUMBER', 'INTEGER'):
elif not value.unit:
return ('NUMBER', value.value)
elif value.type == 'PERCENTAGE':
elif value.unit == '%':
factor = value.value / 100.
font_size_value = computer.computed.font_size
pixels = factor * font_size_value
else:
assert value.type == 'DIMENSION'
pixels = length(computer, name, value)
pixels = length(computer, name, value, pixels_only=True)
return ('PIXELS', pixels)
@register_computer('transform')
def transform(computer, name, value):
"""Compute the ``transform`` property."""
result = []
for function, args in value:
if function in ('rotate', 'skewx', 'skewy'):
args = args.value * ANGLE_TO_RADIANS[args.unit]
elif function == 'translate':
args = length_or_percentage_tuple(computer, name, args)
result.append((function, args))
return result
@register_computer('vertical-align')
def vertical_align(computer, name, value):
"""Compute the ``vertical-align`` property."""
# Use +/- half an em for super and sub, same as Pango.
# (See the SUPERSUB_RISE constant in pango-markup.c)
if value == 'super':
if value in ('baseline', 'middle', 'text-top', 'text-bottom',
'top', 'bottom'):
return value
elif value == 'super':
return computer.computed.font_size * 0.5
elif value == 'sub':
return computer.computed.font_size * -0.5
elif getattr(value, 'type', 'other') == 'PERCENTAGE':
elif value.unit == '%':
height = used_line_height({
'line_height': computer.computed.line_height,
'font_size': computer.computed.font_size
})
return height * value.value / 100.
else:
return length(computer, name, value)
return length(computer, name, value, pixels_only=True)
@register_computer('word-spacing')
@ -424,7 +445,7 @@ def word_spacing(computer, name, value):
if value == 'normal':
return 0
else:
return length(computer, name, value)
return length(computer, name, value, pixels_only=True)
def used_line_height(style):
@ -440,9 +461,3 @@ def used_line_height(style):
return value * style['font_size']
else:
return value
def angle_to_radian(value):
"""Take a DIMENSION token for an angle and return the value in radians.
"""
return value.value * ANGLE_TO_RADIANS[value.unit]

View File

@ -11,21 +11,30 @@
"""
from __future__ import division, unicode_literals
import collections
from tinycss.color3 import COLOR_KEYWORDS
from .values import make_percentage_value
Dimension = collections.namedtuple('Dimension', ['value', 'unit'])
class _Auto(str):
unit = None
value = 'auto'
AUTO = _Auto('auto') # for values that can be 'auto' as well as a dimension
# See http://www.w3.org/TR/CSS21/propidx.html
INITIAL_VALUES = {
'background_attachment': 'scroll',
'background_color': COLOR_KEYWORDS['transparent'],
'background_image': 'none',
'background_position': (make_percentage_value(0),) * 2,
'background_position': (Dimension(0, '%'), Dimension(0, '%')),
'background_repeat': 'repeat',
'background_clip': 'border-box', # CSS3
'background_origin': 'padding-box', # CSS3
'background_size': ('auto', 'auto'), # CSS3
'background_size': (AUTO, AUTO), # CSS3
'border_collapse': 'separate',
# http://www.w3.org/TR/css3-color/#currentcolor
'border_top_color': 'currentColor',
@ -41,7 +50,7 @@ INITIAL_VALUES = {
'border_right_width': 3,
'border_bottom_width': 3,
'border_left_width': 3,
'bottom': 'auto',
'bottom': AUTO,
'caption_side': 'top',
'clear': 'none',
'clip': (), # empty collection, computed value for 'auto'
@ -62,46 +71,46 @@ INITIAL_VALUES = {
'font_style': 'normal',
'font_variant': 'normal',
'font_weight': 400,
'height': 'auto',
'left': 'auto',
'height': AUTO,
'left': AUTO,
'letter_spacing': 'normal',
'line_height': 'normal',
'list_style_image': 'none',
'list_style_position': 'outside',
'list_style_type': 'disc',
'margin_top': 0,
'margin_right': 0,
'margin_bottom': 0,
'margin_left': 0,
'margin_top': Dimension(0, 'px'),
'margin_right': Dimension(0, 'px'),
'margin_bottom': Dimension(0, 'px'),
'margin_left': Dimension(0, 'px'),
'max_height': 'none',
'max_width': 'none',
'min_height': 0,
'min_width': 0,
'min_height': Dimension(0, 'px'),
'min_width': Dimension(0, 'px'),
'orphans': 2,
'overflow': 'visible',
'padding_top': 0,
'padding_right': 0,
'padding_bottom': 0,
'padding_left': 0,
'padding_top': Dimension(0, 'px'),
'padding_right': Dimension(0, 'px'),
'padding_bottom': Dimension(0, 'px'),
'padding_left': Dimension(0, 'px'),
'page_break_after': 'auto',
'page_break_before': 'auto',
'page_break_inside': 'auto',
'quotes': list('“”‘’'), # depends on user agent
'position': 'static',
'right': 'auto',
'right': AUTO,
'table_layout': 'auto',
'text_align': '-weasy-start', # Taken from CSS3 Text.
# The only other supported value form CSS3 is -weasy-end.
'text_decoration': 'none',
'text_indent': 0,
'text_indent': Dimension(0, 'px'),
'text_transform': 'none',
'top': 'auto',
'top': AUTO,
'unicode_bidi': 'normal',
'vertical_align': 'baseline',
'visibility': 'visible',
'white_space': 'normal',
'widows': 2,
'width': 'auto',
'width': AUTO,
'word_spacing': 0, # computed value for 'normal'
'z_index': 'auto',
@ -115,8 +124,8 @@ INITIAL_VALUES = {
'opacity': 1,
# CSS3 2D Transforms: http://www.w3.org/TR/css3-2d-transforms
'transform_origin': (make_percentage_value(50),) * 2,
'transform': (), # computed value for 'none'
'transform_origin': (Dimension(50, '%'), Dimension(50, '%')),
'transform': (), # empty sequence: computed value for 'none'
# Taken from SVG:
# http://www.w3.org/TR/SVG/painting.html#ImageRenderingProperty

View File

@ -21,13 +21,16 @@ from tinycss.parsing import split_on_comma
from ..logger import LOGGER
from ..formatting_structure import counters
from ..compat import urljoin
from .values import get_keyword, get_single_keyword, make_percentage_value
from .properties import INITIAL_VALUES, NOT_PRINT_MEDIA
from .properties import INITIAL_VALUES, NOT_PRINT_MEDIA, AUTO, Dimension
from . import computed_values
# TODO: unit-test these validators
# get the sets of keys
LENGTH_UNITS = set(computed_values.LENGTHS_TO_PIXELS) | set(['ex', 'em'])
ANGLE_UNITS = set(computed_values.ANGLE_TO_RADIANS)
# keyword -> (open, insert)
CONTENT_QUOTE_KEYWORDS = {
'open-quote': (True, True),
@ -37,11 +40,11 @@ CONTENT_QUOTE_KEYWORDS = {
}
BACKGROUND_POSITION_PERCENTAGES = {
'top': make_percentage_value(0),
'left': make_percentage_value(0),
'center': make_percentage_value(50),
'bottom': make_percentage_value(100),
'right': make_percentage_value(100),
'top': Dimension(0, '%'),
'left': Dimension(0, '%'),
'center': Dimension(50, '%'),
'bottom': Dimension(100, '%'),
'right': Dimension(100, '%'),
}
@ -91,6 +94,28 @@ def validator(property_name=None, prefixed=False, wants_base_url=False):
return decorator
def get_keyword(token):
"""If ``value`` is a keyword, return its name.
Otherwise return ``None``.
"""
if token.type == 'IDENT':
return token.value.lower()
def get_single_keyword(tokens):
"""If ``values`` is a 1-element list of keywords, return its name.
Otherwise return ``None``.
"""
if len(tokens) == 1:
token = tokens[0]
if token.type == 'IDENT':
return token.value.lower()
def single_keyword(function):
"""Decorator for validators that only accept a single keyword."""
@functools.wraps(function)
@ -113,35 +138,17 @@ def single_token(function):
return single_token_validator
def is_dimension(token, negative=True):
"""Get if ``token`` is a dimension.
The ``negative`` argument sets wether negative tokens are allowed.
"""
type_ = token.type
# Units may be ommited on zero lenghts.
return (
type_ == 'DIMENSION' and (negative or token.value >= 0) and (
token.unit in computed_values.LENGTHS_TO_PIXELS or
token.unit in ('em', 'ex'))
) or (type_ in ('NUMBER', 'INTEGER') and token.value == 0)
def get_length(token, negative=True, percentage=False):
if (token.unit in LENGTH_UNITS or (percentage and token.unit == '%')
or (token.value == 0 and token.type in ('INTEGER', 'NUMBER'))
) and (negative or token.value >= 0):
return Dimension(token.value, token.unit)
def is_dimension_or_percentage(token, negative=True):
"""Get if ``token`` is a dimension or a percentage.
The ``negative`` argument sets wether negative tokens are allowed.
"""
return is_dimension(token, negative) or (
token.type == 'PERCENTAGE' and (negative or token.value >= 0))
def is_angle(token):
def get_angle(token):
"""Return whether the argument is an angle token."""
return token.type == 'DIMENSION' and \
token.unit in computed_values.ANGLE_TO_RADIANS
if token.unit in ANGLE_UNITS:
return Dimension(token.value, token.unit)
@validator()
@ -197,20 +204,26 @@ def background_position(tokens):
keyword = get_keyword(token)
if keyword in BACKGROUND_POSITION_PERCENTAGES:
return BACKGROUND_POSITION_PERCENTAGES[keyword], center
elif is_dimension_or_percentage(token):
return token, center
else:
length = get_length(token, percentage=True)
if length:
return length, center
elif len(tokens) == 2:
token_1, token_2 = tokens
keyword_1, keyword_2 = map(get_keyword, tokens)
if is_dimension_or_percentage(token_1):
length_1 = get_length(token_1, percentage=True)
if length_1:
if keyword_2 in ('top', 'center', 'bottom'):
return token_1, BACKGROUND_POSITION_PERCENTAGES[keyword_2]
elif is_dimension_or_percentage(token_2):
return token_1, token_2
elif is_dimension_or_percentage(token_2):
return length_1, BACKGROUND_POSITION_PERCENTAGES[keyword_2]
length_2 = get_length(token_2, percentage=True)
if length_2:
return length_1, length_2
raise InvalidValues
length_2 = get_length(token_2, percentage=True)
if length_2:
if keyword_1 in ('left', 'center', 'right'):
return BACKGROUND_POSITION_PERCENTAGES[keyword_1], token_2
return BACKGROUND_POSITION_PERCENTAGES[keyword_1], length_2
elif (keyword_1 in ('left', 'center', 'right') and
keyword_2 in ('top', 'center', 'bottom')):
return (BACKGROUND_POSITION_PERCENTAGES[keyword_1],
@ -239,19 +252,22 @@ def background_size(tokens):
if keyword in ('contain', 'cover'):
return keyword
if keyword == 'auto':
return ('auto', 'auto')
if is_dimension_or_percentage(token, negative=False):
return (token, 'auto')
return (AUTO, AUTO)
length = get_length(token, negative=False)
if length:
return (length, AUTO)
elif len(tokens) == 2:
new_tokens = []
values = []
for token in tokens:
if get_keyword(token) == 'auto':
new_tokens.append('auto')
elif is_dimension_or_percentage(token, negative=False):
new_tokens.append(token)
new_tokens.append(AUTO)
else:
return
return tuple(tokens)
length = get_length(token, negative=False)
if length:
new_tokens.append(token)
break
else:
return tuple(values)
@validator('background_clip')
@ -266,11 +282,12 @@ def box(keyword):
@validator()
def border_spacing(tokens):
"""Validator for the `border-spacing` property."""
if all(is_dimension(token, negative=False) for token in tokens):
if len(tokens) == 1:
return (tokens[0], tokens[0])
elif len(tokens) == 2:
return tuple(tokens)
lengths = [get_length(token, negative=False) for token in tokens]
if all(lengths):
if len(lengths) == 1:
return (lengths[0], lengths[0])
elif len(lengths) == 2:
return tuple(lengths)
@validator('border-top-style')
@ -291,8 +308,9 @@ def border_style(keyword):
@single_token
def border_width(token):
"""``border-*-width`` properties validation."""
if is_dimension(token, negative=False):
return token
length = get_length(token, negative=False)
if length:
return length
keyword = get_keyword(token)
if keyword in ('thin', 'medium', 'thick'):
return keyword
@ -323,9 +341,11 @@ def clip(token):
tokens = []
for arg in args:
if get_keyword(arg) == 'auto':
tokens.append('auto')
elif is_dimension(arg, negative=True):
tokens.append(arg)
tokens.append(AUTO)
else:
length = get_length(arg)
if length:
tokens.append(length)
else:
raise InvalidValues
return tokens
@ -443,10 +463,11 @@ def counter(tokens, default_integer):
@single_token
def lenght_precentage_or_auto(token, negative=True):
"""``margin-*`` properties validation."""
if is_dimension_or_percentage(token, negative):
return token
length = get_length(token, negative, percentage=True)
if length:
return length
if get_keyword(token) == 'auto':
return 'auto'
return AUTO
@validator('height')
@ -497,8 +518,9 @@ def font_family(tokens):
@single_token
def font_size(token):
"""``font-size`` property validation."""
if is_dimension_or_percentage(token):
return token
length = get_length(token, percentage=True)
if length:
return length
font_size_keyword = get_keyword(token)
if font_size_keyword in ('smaller', 'larger'):
raise InvalidValues('value not supported yet')
@ -542,8 +564,9 @@ def spacing(token):
"""Validation for ``letter-spacing`` and ``word-spacing``."""
if get_keyword(token) == 'normal':
return 'normal'
if is_dimension(token):
return token
length = get_length(token)
if length:
return length
@validator()
@ -554,7 +577,7 @@ def line_height(token):
return 'normal'
if (token.type in ('NUMBER', 'INTEGER', 'DIMENSION', 'PERCENTAGE') and
token.value >= 0):
return token
return Dimension(token.value, token.unit)
@validator()
@ -578,8 +601,9 @@ def list_style_type(keyword):
@single_token
def length_or_precentage(token):
"""``padding-*`` properties validation."""
if is_dimension_or_percentage(token, negative=False):
return token
length = get_length(token, negative=False)
if length:
return length
@validator()
@ -675,8 +699,9 @@ def text_decoration(tokens):
@single_token
def text_indent(token):
"""``text-indent`` property validation."""
if is_dimension_or_percentage(token, negative=True):
return token
length = get_length(token, percentage=True)
if length:
return length
@validator()
@ -690,8 +715,9 @@ def text_transform(keyword):
@single_token
def vertical_align(token):
"""Validation for the ``vertical-align`` property"""
if is_dimension_or_percentage(token, negative=True):
return token
length = get_length(token, percentage=True)
if length:
return length
keyword = get_keyword(token)
if keyword in ('baseline', 'middle', 'sub', 'super',
'text-top', 'text-bottom', 'top', 'bottom'):
@ -716,7 +742,7 @@ def white_space(keyword):
@single_keyword
def image_rendering(keyword):
"""Validation for ``image-rendering``."""
return keyword in ('auto', 'optimizeSpeed', 'optimizeQuality')
return keyword in ('auto', 'optimizespeed', 'optimizequality')
@validator(prefixed=True) # Not in CR yet
@ -726,22 +752,22 @@ def size(tokens):
See http://www.w3.org/TR/css3-page/#page-size-prop
"""
if is_dimension(tokens[0]):
if len(tokens) == 1:
return tokens * 2
elif len(tokens) == 2 and is_dimension(tokens[1]):
return tokens
lengths = [get_length(token, negative=False) for token in tokens]
if all(lengths):
if len(lengths) == 1:
return (lengths[0], lengths[0])
elif len(lengths) == 2:
return tuple(lengths)
keywords = [get_keyword(v) for v in tokens]
if len(keywords) == 1:
keyword = keywords[0]
if keyword in ('auto', 'portrait'):
return INITIAL_VALUES['size']
elif keyword == 'landscape':
height, width = INITIAL_VALUES['size']
return width, height
elif keyword in computed_values.PAGE_SIZES:
if keyword in computed_values.PAGE_SIZES:
return computed_values.PAGE_SIZES[keyword]
elif keyword in ('auto', 'portrait'):
return computed_values.INITIAL_PAGE_SIZE
elif keyword == 'landscape':
return computed_values.INITIAL_PAGE_SIZE[::-1]
if len(keywords) == 2:
if keywords[0] in ('portrait', 'landscape'):
@ -762,7 +788,7 @@ def size(tokens):
@validator(prefixed=True) # Not in CR yet
def transform(tokens):
if get_single_keyword(tokens) == 'none':
return 'none'
return []
else:
return [transform_function(v) for v in tokens]
@ -774,12 +800,14 @@ def transform_function(token):
name, args = function
if len(args) == 1:
if name in ('rotate', 'skewx', 'skewy') and is_angle(args[0]):
return name, args[0]
elif name in ('translatex', 'translate') and is_dimension_or_percentage(args[0]):
return 'translate', (args[0], 0)
elif name == 'translatey' and is_dimension_or_percentage(args[0]):
return 'translate', (0, args[0])
angle = get_angle(args[0])
length = get_length(args[0], percentage=True)
if name in ('rotate', 'skewx', 'skewy') and angle:
return name, angle
elif name in ('translatex', 'translate') and length:
return 'translate', (length, computed_values.ZERO_PIXELS)
elif name == 'translatey' and length:
return 'translate', (computed_values.ZERO_PIXELS, length)
elif name == 'scalex' and args[0].type in ('NUMBER', 'INTEGER'):
return 'scale', (args[0].value, 1)
elif name == 'scaley' and args[0].type in ('NUMBER', 'INTEGER'):
@ -790,8 +818,9 @@ def transform_function(token):
if name == 'scale' and all(a.type in ('NUMBER', 'INTEGER')
for a in args):
return name, [arg.value for arg in args]
if name == 'translate' and all(map(is_dimension_or_percentage, args)):
return name, args
lengths = [get_length(token, percentage=True) for token in args]
if name == 'translate' and all(lengths):
return name, lengths
elif len(args) == 6 and name == 'matrix' and all(
a.type in ('NUMBER', 'INTEGER') for a in args):
return name, [arg.value for arg in args]

View File

@ -1,61 +0,0 @@
# coding: utf8
"""
weasyprint.css.values
---------------------
Utility function to work with tinycss tokens.
:copyright: Copyright 2011-2012 Simon Sapin and contributors, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from __future__ import division, unicode_literals
import collections
def get_keyword(value):
"""If ``value`` is a keyword, return its name.
Otherwise return ``None``.
"""
if value.type == 'IDENT':
return value.value
def get_single_keyword(values):
"""If ``values`` is a 1-element list of keywords, return its name.
Otherwise return ``None``.
"""
# Fast but unsafe, as it depends on private attributes
if len(values) == 1:
value = values[0]
if value.type == 'IDENT':
return value.value
def get_percentage_value(value):
"""If ``value`` is a percentage, return its value.
Otherwise return ``None``.
"""
if getattr(value, 'type', 'other') == 'PERCENTAGE':
return value.value
else:
# Not a percentage
return None
class FakePercentage(object):
type = 'PERCENTAGE'
def __init__(self, value):
self.value = value
self.as_css = '{}%'.format(value)
make_percentage_value = FakePercentage

View File

@ -18,15 +18,15 @@ import math
import cairo
from .formatting_structure import boxes
from .css.values import get_percentage_value
from .css import computed_values
# Map values of the image-rendering property to cairo FILTER values:
# Values are normalized to lower case.
IMAGE_RENDERING_TO_FILTER = dict(
optimizeSpeed=cairo.FILTER_FAST,
optimizespeed=cairo.FILTER_FAST,
auto=cairo.FILTER_GOOD,
optimizeQuality=cairo.FILTER_BEST,
optimizequality=cairo.FILTER_BEST,
)
@ -188,11 +188,11 @@ def draw_box_background(document, context, page, box):
def percentage(value, refer_to):
"""Return the evaluated percentage value, or the value unchanged."""
percentage_value = get_percentage_value(value)
if percentage_value is None:
return value
if value.unit == 'px':
return value.value
else:
return refer_to * percentage_value / 100
assert value.unit == '%'
return refer_to * value.value / 100
def draw_background(document, context, style, painting_area, positioning_area):
@ -566,18 +566,14 @@ def apply_2d_transforms(context, box):
origin_x += box.border_box_x()
origin_y += box.border_box_y()
def length(value, font_size=box.style.font_size):
return computed_values.length(None, None, value, font_size)
angle = computed_values.angle_to_radian
context.translate(origin_x, origin_y)
for name, args in box.style.transform:
if name == 'scale':
context.scale(*args)
elif name == 'rotate':
context.rotate(angle(args))
context.rotate(args)
elif name == 'translate':
translate_x, translate_y = map(length, args)
translate_x, translate_y = args
context.translate(
percentage(translate_x, border_width),
percentage(translate_y, border_height),

View File

@ -60,6 +60,7 @@
from __future__ import division, unicode_literals
from ..compat import xrange
from ..css.computed_values import ZERO_PIXELS
# The *Box classes have many attributes and methods, but that's the way it is
@ -193,8 +194,8 @@ class Box(object):
setattr(self, 'padding_%s' % side, 0)
setattr(self, 'border_%s_width' % side, 0)
self.style['margin_%s' % side] = 0
self.style['padding_%s' % side] = 0
self.style['margin_%s' % side] = ZERO_PIXELS
self.style['padding_%s' % side] = ZERO_PIXELS
self.style['border_%s_width' % side] = 0
# Positioning schemes

View File

@ -277,7 +277,8 @@ def add_box_marker(document, counter_values, box):
position = style.list_style_position
if position == 'inside':
side = 'right' if style.direction == 'ltr' else 'left'
marker_box.style['margin_' + side] = style.font_size * 0.5
margin = style.font_size * 0.5
marker_box.style['margin_' + side] = properties.Dimension(margin, 'px')
yield marker_box
elif position == 'outside':
box.outside_list_marker = marker_box

View File

@ -17,6 +17,7 @@ from .markers import list_marker_layout
from .tables import table_layout, fixed_table_layout
from .percentages import resolve_percentages
from ..formatting_structure import boxes
from ..css.properties import Dimension
def block_level_layout(document, box, max_position_y, skip_stack,
@ -386,7 +387,8 @@ def block_table_wrapper(document, wrapper, max_position_y, skip_stack,
table.margin_right = 0
fixed_table_layout(table)
wrapper.width = wrapper.style.width = table.border_width()
wrapper.width = table.border_width()
wrapper.style.width = Dimension(wrapper.width, 'px')
return block_box_layout(document, wrapper, max_position_y, skip_stack,
containing_block, device_size, page_is_empty,
adjoining_margins)

View File

@ -13,7 +13,6 @@
from __future__ import division, unicode_literals
from ..formatting_structure import boxes
from ..css.values import get_percentage_value
def resolve_one_percentage(box, property_name, refer_to,
@ -25,23 +24,21 @@ def resolve_one_percentage(box, property_name, refer_to,
"""
# box.style has computed values
values = box.style[property_name]
if isinstance(values, (int, float)):
# Absolute length (was converted to pixels in "computed values")
result = values
value = box.style[property_name]
if value.unit == 'px':
result = value.value
else:
percentage = get_percentage_value(values)
if percentage is not None:
if value.unit == '%':
if isinstance(refer_to, (int, float)):
# A percentage
result = percentage * refer_to / 100.
result = value.value * refer_to / 100.
else:
# Replace percentages when we have no refer_to that
# makes sense.
result = refer_to
else:
# Some other values such as 'auto' may be allowed
result = values
result = value
assert allowed_keywords and result in allowed_keywords
# box attributes are used values
setattr(box, property_name, result)

View File

@ -336,9 +336,9 @@ def test_styles():
assert child.style.color == (0, 0, 1, 1) # blue
# Only non-anonymous boxes have margins
if child.style.anonymous:
assert child.style.margin_top == 0
assert child.style.margin_top == (0, 'px')
else:
assert child.style.margin_top == 42
assert child.style.margin_top == (42, 'px')
@assert_no_logs
@ -390,10 +390,10 @@ def test_page_style():
def assert_page_margins(page_type, top, right, bottom, left):
"""Check the page margin values."""
style = document.style_for(page_type)
assert style.margin_top == top
assert style.margin_right == right
assert style.margin_bottom == bottom
assert style.margin_left == left
assert style.margin_top == (top, 'px')
assert style.margin_right == (right, 'px')
assert style.margin_bottom == (bottom, 'px')
assert style.margin_left == (left, 'px')
assert_page_margins('first_left_page', top=20, right=3, bottom=3, left=10)
assert_page_margins('first_right_page', top=20, right=10, bottom=3, left=3)
@ -591,10 +591,10 @@ def test_table_style():
table, = wrapper.children
assert isinstance(wrapper, boxes.BlockBox)
assert isinstance(table, boxes.TableBox)
assert wrapper.style.margin_top == 1
assert wrapper.style.padding_top == 0
assert table.style.margin_top == 0
assert table.style.padding_top == 2
assert wrapper.style.margin_top == (1, 'px')
assert wrapper.style.padding_top == (0, 'px')
assert table.style.margin_top == (0, 'px')
assert table.style.padding_top == (2, 'px')
@assert_no_logs
@ -610,7 +610,7 @@ def test_column_style():
table, = wrapper.children
colgroup, = table.column_groups
widths = [col.style.width for col in colgroup.children]
assert widths == [10, 10, 10, 'auto', 'auto']
assert widths == [(10, 'px'), (10, 'px'), (10, 'px'), 'auto', 'auto']
assert [col.grid_x for col in colgroup.children] == [0, 1, 2, 3, 4]
# copies, not the same box object
assert colgroup.children[0] is not colgroup.children[1]

View File

@ -118,16 +118,16 @@ def test_expand_shorthands():
assert 'margin-top' not in style
style = dict(
(name, value.as_css)
(name, value)
for _rule, _selectors, declarations in sheet.rules
for name, value, _priority in declarations)
assert 'margin' not in style
assert style['margin_top'] == '2em'
assert style['margin_right'] == '0'
assert style['margin_bottom'] == '2em', \
assert style['margin_top'] == (2, 'em')
assert style['margin_right'] == (0, None)
assert style['margin_bottom'] == (2, 'em'), \
'3em was before the shorthand, should be masked'
assert style['margin_left'] == '4em', \
assert style['margin_left'] == (4, 'em'), \
'4em was after the shorthand, should not be masked'
@ -163,17 +163,17 @@ def test_annotate_document():
assert h1.font_size == 32 # 4ex
# 32px = 1em * font-size = x-large = 3/2 * initial 16px = 24px
assert p.margin_top == 24
assert p.margin_right == 0
assert p.margin_bottom == 24
assert p.margin_left == 0
assert p.margin_top == (24, 'px')
assert p.margin_right == (0, 'px')
assert p.margin_bottom == (24, 'px')
assert p.margin_left == (0, 'px')
assert p.background_color == (0, 0, 1, 1) # blue
# 80px = 2em * 5ex = 10 * half of initial 16px
assert ul.margin_top == 80
assert ul.margin_right == 80
assert ul.margin_bottom == 80
assert ul.margin_left == 80
assert ul.margin_top == (80, 'px')
assert ul.margin_right == (80, 'px')
assert ul.margin_bottom == (80, 'px')
assert ul.margin_left == (80, 'px')
assert ul.font_weight == 700
# thick = 5px, 0.25 inches = 96*.25 = 24px
@ -184,17 +184,17 @@ def test_annotate_document():
assert li_0.font_weight == 900
assert li_0.font_size == 8 # 6pt
assert li_0.margin_top == 16 # 2em
assert li_0.margin_right == 0
assert li_0.margin_bottom == 16
assert li_0.margin_left == 32 # 4em
assert li_0.margin_top == (16, 'px') # 2em
assert li_0.margin_right == (0, 'px')
assert li_0.margin_bottom == (16, 'px')
assert li_0.margin_left == (32, 'px') # 4em
assert a.text_decoration == frozenset(['underline'])
assert a.font_size == 24 # 300% of 8px
assert a.padding_top == 1
assert a.padding_right == 2
assert a.padding_bottom == 3
assert a.padding_left == 4
assert a.padding_top == (1, 'px')
assert a.padding_right == (2, 'px')
assert a.padding_bottom == (3, 'px')
assert a.padding_left == (4, 'px')
assert a.color == (1, 0, 0, 1)
@ -247,27 +247,27 @@ def test_page():
])
style = document.style_for('first_left_page')
assert style.margin_top == 5
assert style.margin_left == 10
assert style.margin_bottom == 10
assert style.margin_top == (5, 'px')
assert style.margin_left == (10, 'px')
assert style.margin_bottom == (10, 'px')
assert style.color == (1, 0, 0, 1) # red, inherited from html
style = document.style_for('first_right_page')
assert style.margin_top == 5
assert style.margin_left == 10
assert style.margin_bottom == 16
assert style.margin_top == (5, 'px')
assert style.margin_left == (10, 'px')
assert style.margin_bottom == (16, 'px')
assert style.color == (0, 0, 1, 1) # blue
style = document.style_for('left_page')
assert style.margin_top == 10
assert style.margin_left == 10
assert style.margin_bottom == 10
assert style.margin_top == (10, 'px')
assert style.margin_left == (10, 'px')
assert style.margin_bottom == (10, 'px')
assert style.color == (1, 0, 0, 1) # red, inherited from html
style = document.style_for('right_page')
assert style.margin_top == 10
assert style.margin_left == 10
assert style.margin_bottom == 16
assert style.margin_top == (10, 'px')
assert style.margin_left == (10, 'px')
assert style.margin_bottom == (16, 'px')
assert style.color == (0, 0, 1, 1) # blue
style = document.style_for('first_left_page', '@top-left')
@ -275,7 +275,7 @@ def test_page():
style = document.style_for('first_right_page', '@top-left')
assert style.font_size == 20 # inherited from @page
assert style.width == 200
assert style.width == (200, 'px')
style = document.style_for('first_right_page', '@top-right')
assert style.font_size == 10

View File

@ -28,11 +28,7 @@ def expand_to_dict(short_name, short_values):
assert not errors
assert len(declarations) == 1
tokens = remove_whitespace(declarations[0].value)
expanded = validation.EXPANDERS[short_name]('', short_name, tokens)
return dict((name, tuple(v.as_css for v in value)
if name == 'background_position'
else getattr(value, 'as_css', value))
for name, value in expanded)
return dict(validation.EXPANDERS[short_name]('', short_name, tokens))
@assert_no_logs
@ -45,28 +41,28 @@ def test_expand_four_sides():
'margin_left': 'inherit',
}
assert expand_to_dict('margin', '1em') == {
'margin_top': '1em',
'margin_right': '1em',
'margin_bottom': '1em',
'margin_left': '1em',
'margin_top': (1, 'em'),
'margin_right': (1, 'em'),
'margin_bottom': (1, 'em'),
'margin_left': (1, 'em'),
}
assert expand_to_dict('padding', '1em 0') == {
'padding_top': '1em',
'padding_right': '0',
'padding_bottom': '1em',
'padding_left': '0',
'padding_top': (1, 'em'),
'padding_right': (0, None),
'padding_bottom': (1, 'em'),
'padding_left': (0, None),
}
assert expand_to_dict('padding', '1em 0 2em') == {
'padding_top': '1em',
'padding_right': '0',
'padding_bottom': '2em',
'padding_left': '0',
'padding_top': (1, 'em'),
'padding_right': (0, None),
'padding_bottom': (2, 'em'),
'padding_left': (0, None),
}
assert expand_to_dict('padding', '1em 0 2em 5px') == {
'padding_top': '1em',
'padding_right': '0',
'padding_bottom': '2em',
'padding_left': '5px',
'padding_top': (1, 'em'),
'padding_right': (0, None),
'padding_bottom': (2, 'em'),
'padding_left': (5, 'px'),
}
with raises(ValueError):
expand_to_dict('padding', '1 2 3 4 5')
@ -76,39 +72,39 @@ def test_expand_four_sides():
def test_expand_borders():
"""Test the ``border`` property."""
assert expand_to_dict('border_top', '3px dotted red') == {
'border_top_width': '3px',
'border_top_width': (3, 'px'),
'border_top_style': 'dotted',
'border_top_color': (1, 0, 0, 1), # red
}
assert expand_to_dict('border_top', '3px dotted') == {
'border_top_width': '3px',
'border_top_width': (3, 'px'),
'border_top_style': 'dotted',
'border_top_color': 'currentColor',
}
assert expand_to_dict('border_top', '3px red') == {
'border_top_width': '3px',
'border_top_width': (3, 'px'),
'border_top_style': 'none',
'border_top_color': (1, 0, 0, 1), # red
}
assert expand_to_dict('border_top', 'solid') == {
'border_top_width': 3,
'border_top_width': 3, # initial value
'border_top_style': 'solid',
'border_top_color': 'currentColor',
}
assert expand_to_dict('border', '6px dashed lime') == {
'border_top_width': '6px',
'border_top_width': (6, 'px'),
'border_top_style': 'dashed',
'border_top_color': (0, 1, 0, 1), # lime
'border_left_width': '6px',
'border_left_width': (6, 'px'),
'border_left_style': 'dashed',
'border_left_color': (0, 1, 0, 1), # lime
'border_bottom_width': '6px',
'border_bottom_width': (6, 'px'),
'border_bottom_style': 'dashed',
'border_bottom_color': (0, 1, 0, 1), # lime
'border_right_width': '6px',
'border_right_width': (6, 'px'),
'border_right_style': 'dashed',
'border_right_color': (0, 1, 0, 1), # lime
}
@ -162,7 +158,7 @@ def test_expand_background():
image='none',
repeat='repeat',
attachment='scroll',
position=('0%', '0%'),
position=((0, '%'), (0, '%')),
)
assert_background(
@ -171,7 +167,7 @@ def test_expand_background():
image='foo.png', ##
repeat='repeat',
attachment='scroll',
position=('0%', '0%'),
position=((0, '%'), (0, '%')),
)
assert_background(
'no-repeat',
@ -179,7 +175,7 @@ def test_expand_background():
image='none',
repeat='no-repeat', ##
attachment='scroll',
position=('0%', '0%'),
position=((0, '%'), (0, '%')),
)
assert_background(
'fixed',
@ -187,7 +183,7 @@ def test_expand_background():
image='none',
repeat='repeat',
attachment='fixed', ##
position=('0%', '0%'),
position=((0, '%'), (0, '%')),
)
assert_background(
'top right',
@ -196,7 +192,7 @@ def test_expand_background():
repeat='repeat',
attachment='scroll',
# Order swapped to be in (horizontal, vertical) order.
position=('100%', '0%'), ##
position=((100, '%'), (0, '%')), ##
)
assert_background(
'url(bar) #f00 repeat-y center left fixed',
@ -205,7 +201,7 @@ def test_expand_background():
repeat='repeat-y', ##
attachment='fixed', ##
# Order swapped to be in (horizontal, vertical) order.
position=('0%', '50%'), ##
position=((0, '%'), (50, '%')), ##
)
assert_background(
'#00f 10% 200px',
@ -213,7 +209,7 @@ def test_expand_background():
image='none',
repeat='repeat',
attachment='scroll',
position=('10%', '200px'), ##
position=((10, '%'), (200, 'px')), ##
)
assert_background(
'right 78px fixed',
@ -221,7 +217,7 @@ def test_expand_background():
image='none',
repeat='repeat',
attachment='fixed', ##
position=('100%', '78px'), ##
position=((100, '%'), (78, 'px')), ##
)
@ -232,7 +228,7 @@ def test_font():
'font_style': 'normal',
'font_variant': 'normal',
'font_weight': 400,
'font_size': '12px', ##
'font_size': (12, 'px'), ##
'line_height': 'normal',
'font_family': ['My Fancy Font', 'serif'], ##
}
@ -241,7 +237,7 @@ def test_font():
'font_variant': 'normal',
'font_weight': 400,
'font_size': 'small', ##
'line_height': '1.2', ##
'line_height': (1.2, None), ##
'font_family': ['Some Font', 'serif'], ##
}
assert expand_to_dict('font', 'small-caps italic 700 large serif') == {

View File

@ -691,7 +691,6 @@ def test_table_page_breaks():
for i, page in enumerate(pages):
html, = page.children
body, = html.children
print(body.children)
if i == 0:
body_children = body.children[1:] # skip h1
else: