From 69e9c554145408ad96f257ce6fdabc46df58e3d4 Mon Sep 17 00:00:00 2001 From: Xavid Pretzer Date: Wed, 10 Apr 2024 08:01:19 -0400 Subject: [PATCH 01/30] Add basic support for border-image-source and border-image-slice with percents. --- tests/draw/__init__.py | 3 + tests/draw/test_box.py | 94 +++++++++++++++++ tests/resources/border.svg | 129 ++++++++++++++++++++++++ weasyprint/css/properties.py | 6 ++ weasyprint/css/validation/expanders.py | 41 ++++++++ weasyprint/css/validation/properties.py | 74 ++++++++++++++ weasyprint/draw.py | 104 +++++++++++++++++++ weasyprint/layout/background.py | 7 ++ 8 files changed, 458 insertions(+) create mode 100644 tests/resources/border.svg diff --git a/tests/draw/__init__.py b/tests/draw/__init__.py index e0d757fe7..8d758f7b3 100644 --- a/tests/draw/__init__.py +++ b/tests/draw/__init__.py @@ -17,6 +17,9 @@ 'G': (0, 255, 0), # lime green 'V': (191, 0, 64), # average of 1*B and 3*R 'S': (255, 63, 63), # R above R above _ + 'C': (0, 255, 255), # cyan + 'M': (255, 0, 255), # magenta + 'Y': (255, 255, 0), # yellow 'K': (0, 0, 0), # black 'r': (255, 0, 0), # red 'g': (0, 128, 0), # half green diff --git a/tests/draw/test_box.py b/tests/draw/test_box.py index 731df99ac..d82c28b00 100644 --- a/tests/draw/test_box.py +++ b/tests/draw/test_box.py @@ -273,3 +273,97 @@ def test_draw_split_border_radius(assert_pixels):
a b c
''') + + +@assert_no_logs +def test_border_image(assert_pixels): + # Shows default "stretch" behavior for border images. + # Note that we use a svg image to avoid antialiasing. + assert_pixels(''' + __________ + _RYYYMMMG_ + _M______C_ + _M______C_ + _Y______Y_ + _Y______Y_ + _BYYYCCCK_ + __________ + ''', ''' + +
+ ''') + + +@assert_no_logs +def test_border_image_fill(assert_pixels): + assert_pixels(''' + __________ + _RYYYMMMG_ + _MbbbgggC_ + _MbbbgggC_ + _YgggbbbY_ + _YgggbbbY_ + _BYYYCCCK_ + __________ + ''', ''' + +
+ ''') + + +@assert_no_logs +def test_border_image_default_sllice(assert_pixels): + # By default, border-image-slice is 100%, so the whole image is in the + # four corners and nothing is in-between. + assert_pixels(''' + _____________ + _RYMG___RYMG_ + _MbgC___MbgC_ + _YgbY___YgbY_ + _BYCK___BYCK_ + _____________ + _____________ + _RYMG___RYMG_ + _MbgC___MbgC_ + _YgbY___YgbY_ + _BYCK___BYCK_ + _____________ + ''', ''' + +
+ ''') diff --git a/tests/resources/border.svg b/tests/resources/border.svg new file mode 100644 index 000000000..1ba32cada --- /dev/null +++ b/tests/resources/border.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/weasyprint/css/properties.py b/weasyprint/css/properties.py index 2c51366ce..6e644fab3 100644 --- a/weasyprint/css/properties.py +++ b/weasyprint/css/properties.py @@ -67,6 +67,12 @@ 'border_top_right_radius': (ZERO_PIXELS, ZERO_PIXELS), 'border_top_style': 'none', 'border_top_width': 3, # computed value for 'medium' + 'border_image_source': ('none', None), + 'border_image_slice': [Dimension(100, '%')], + 'border_image_width': 1, + 'border_image_outset': 0, + 'border_image_repeat': ('stretch',), + # Color 3 (REC): https://www.w3.org/TR/css-color-3/ 'opacity': 1, diff --git a/weasyprint/css/validation/expanders.py b/weasyprint/css/validation/expanders.py index bb95d6ef1..44ca11726 100644 --- a/weasyprint/css/validation/expanders.py +++ b/weasyprint/css/validation/expanders.py @@ -285,6 +285,47 @@ def expand_border_side(tokens, name): yield suffix, [token] +@expander('border-image') +@generic_expander('-outset', '-repeat', '-slice', '-source', '-width') +def expand_border_image(tokens, name): + """Expand the ``border-image-*`` shorthand properties. + + See https://drafts.csswg.org/css-backgrounds/#the-border-image + + """ + tokens = list(tokens) + while len(tokens) > 0: + token = tokens.pop(0) + if token.type in ('function', 'url'): + yield '-source', [token] + elif get_keyword(token) in ('stretch', 'repeat', 'round', 'space'): + yield '-repeat', [token] + elif (token.type in ('percentage', 'number') + or get_keyword(token) == 'fill'): + current = [token] + numish_suffixes = ['-slice', '-width', '-outset'] + while len(tokens) > 0 and ( + tokens[0].type in ( + 'percentage', 'number', 'literal', 'dimension') + or get_keyword(tokens[0]) in ('fill', 'auto')): + token = tokens.pop(0) + if token.type == 'literal' and token.value == '/': + if len(current) == 0: + if numish_suffixes[0] != '-width': + raise InvalidValues + else: + yield numish_suffixes[0], current + current = [] + numish_suffixes.pop(0) + else: + current.append(token) + if len(current) == 0: + raise InvalidValues + yield numish_suffixes[0], current + else: + raise InvalidValues + + @expander('background') def expand_background(tokens, name, base_url): """Expand the ``background`` shorthand property. diff --git a/weasyprint/css/validation/properties.py b/weasyprint/css/validation/properties.py index 36727376f..de2ac1bd9 100644 --- a/weasyprint/css/validation/properties.py +++ b/weasyprint/css/validation/properties.py @@ -421,6 +421,80 @@ def border_width(token): return keyword +@property(wants_base_url=True) +@single_token +def border_image_source(token, base_url): + if token.type not in ('function', 'url'): + if get_keyword(token) == 'none': + return 'none', None + return get_image(token, base_url) + + +@property() +def border_image_slice(tokens): + values = [] + keyword_cnt = 0 + for token in tokens: + # Don't use get_length() because a dimension with a unit is disallowed. + if token.type == 'percentage' and token.value >= 0: + values.append(Dimension(min(token.value, 100), '%')) + elif token.type == 'number': + values.append(token.value) + elif get_keyword(token) == 'fill': + keyword_cnt += 1 + else: + return None + if keyword_cnt > 0: + # Always put it at the beginning to make processing easier. + # Would it be better to use some sort of object here? + values = ['fill'] + values + if keyword_cnt <= 1 and (1 <= len(values) - keyword_cnt <= 4): + return values + + +@property() +def border_image_width(tokens): + if get_single_keyword(tokens) == 'auto': + return 'auto' + values = [] + for token in tokens: + if token.type == 'number' and token.value >= 0: + values.append(token.value) + else: + length = get_length(token, negative=False, percentage=True) + if length: + values.append(length) + else: + return None + if 1 <= len(values) <= 4: + return values + + +@property() +def border_image_outset(tokens): + values = [] + for token in tokens: + if token.type == 'number': + values.append(token.value) + else: + length = get_length(token) + if length: + values.append(length) + else: + return None + if 1 <= len(values) <= 4: + return values + + +@property() +def border_image_repeat(tokens): + if 1 <= len(tokens) <= 2: + keywords = tuple(get_keyword(token) for token in tokens) + if all(kw in ('stretch', 'repeat', 'round', 'space') + for kw in keywords): + return keywords + + @property(unstable=True) @single_token def column_width(token): diff --git a/weasyprint/draw.py b/weasyprint/draw.py index ef15737f2..3291e2918 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -453,6 +453,110 @@ def draw_column_border(): draw_column_border() return + # If there's a border image, that takes precedence. + if (box.style['border_image_source'][0] != 'none' + and box.border_image is not None): + should_fill = False + image_slice = box.style['border_image_slice'] + if image_slice[0] == 'fill': + image_slice.pop(0) + should_fill = True + slice_dims = [d.value / 100. for d in image_slice] + if len(slice_dims) == 1: + slice_top = slice_bottom = slice_left = slice_right = slice_dims[0] + elif len(slice_dims) == 2: + slice_top = slice_bottom = slice_dims[0] + slice_left = slice_right = slice_dims[1] + elif len(slice_dims) == 3: + slice_top = slice_dims[0] + slice_left = slice_right = slice_dims[1] + slice_bottom = slice_dims[2] + else: + slice_top = slice_dims[0] + slice_right = slice_dims[1] + slice_bottom = slice_dims[2] + slice_left = slice_dims[3] + + x, y, w, h, tl, tr, br, bl = box.rounded_border_box() + px, py, pw, ph, ptl, ptr, pbr, pbl = box.rounded_padding_box() + border_width = px - x + border_height = py - y + + img = box.border_image + iw, ih, iratio = img.get_intrinsic_size( + box.style['image_resolution'], + box.style['font_size']) + + def draw_border_image(x, y, width, height, + slice_x_frac, slice_y_frac, + slice_width_frac, slice_height_frac): + with stacked(stream): + img_width = width / slice_width_frac + img_height = height / slice_height_frac + scale_x = img_width / iw + scale_y = img_height / ih + stream.rectangle(x, y, width, height) + stream.clip() + stream.end() + stream.transform( + e=x - img_width * slice_x_frac, + f=y - img_height * slice_y_frac) + stream.transform(a=scale_x, d=scale_y) + img.draw(stream, iw, ih, box.style['image_rendering']) + + # Top left. + draw_border_image(x, y, border_width, border_height, + 0, 0, slice_left, slice_top) + # Top right. + draw_border_image(x + w - border_width, y, + border_width, border_height, + (1-slice_right), 0, slice_right, slice_top) + # Bottom right. + draw_border_image(x + w - border_width, + y + h - border_height, + border_width, border_height, + (1-slice_right), (1-slice_bottom), + slice_right, slice_bottom) + # Bottom left. + draw_border_image(x, y + h - border_height, + border_width, border_height, + 0, (1-slice_bottom), slice_left, slice_bottom) + if slice_left + slice_right < 1.0: + # Top middle. + draw_border_image(x + border_width, y, + w - 2*border_width, border_height, + slice_left, 0, + 1 - slice_left - slice_right, slice_top) + # Bottom middle. + draw_border_image(x + border_width, + y + h - border_height, + w - 2*border_width, border_height, + slice_left, 1 - slice_bottom, + 1 - slice_left - slice_right, slice_bottom) + if slice_top + slice_bottom < 1.0: + # Right middle. + draw_border_image(x + w - border_width, + y + border_height, + border_width, h - 2*border_height, + (1 - slice_right), slice_top, + slice_right, 1 - slice_top - slice_bottom) + # Left middle. + draw_border_image(x, y + border_height, + border_width, h - 2*border_height, + 0, slice_top, + slice_left, 1 - slice_top - slice_bottom) + if (should_fill and slice_left + slice_right < 1.0 + and slice_top + slice_bottom < 1.0): + # Fill middle middle. + draw_border_image(x + border_width, y + border_height, + w - 2*border_width, h - 2*border_height, + slice_left, slice_top, + 1 - slice_left - slice_right, + 1 - slice_top - slice_bottom) + + draw_column_border() + return + colors = [get_color(box.style, f'border_{side}_color') for side in SIDES] styles = [ colors[i].alpha and box.style[f'border_{side}_style'] diff --git a/weasyprint/layout/background.py b/weasyprint/layout/background.py index 65ee937a9..429159fd2 100644 --- a/weasyprint/layout/background.py +++ b/weasyprint/layout/background.py @@ -47,6 +47,13 @@ def layout_box_backgrounds(page, box, get_image_from_uri, layout_children=True, if style is None: style = box.style + # This is for the border image, not the background, but this is a + # convenient place to get the image. + if (style['border_image_source'] + and style['border_image_source'][0] != 'none'): + box.border_image = get_image_from_uri( + url=style['border_image_source'][1]) + if style['visibility'] == 'hidden': images = [] color = parse_color('transparent') From 136ab0952b5453ab66472f18c1edbb51edc5ed3a Mon Sep 17 00:00:00 2001 From: Xavid Pretzer Date: Wed, 10 Apr 2024 09:48:55 -0400 Subject: [PATCH 02/30] Handle borders of uneven width with border images. --- tests/draw/test_box.py | 30 +++++++++++++++++++++++++++ weasyprint/draw.py | 47 ++++++++++++++++++++++-------------------- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/tests/draw/test_box.py b/tests/draw/test_box.py index d82c28b00..39bf30206 100644 --- a/tests/draw/test_box.py +++ b/tests/draw/test_box.py @@ -367,3 +367,33 @@ def test_border_image_default_sllice(assert_pixels):
''') + + +@assert_no_logs +def test_border_image_uneven_width(assert_pixels): + assert_pixels(''' + ____________ + _RRRYYYMMMG_ + _MMM______C_ + _MMM______C_ + _YYY______Y_ + _YYY______Y_ + _BBBYYYCCCK_ + ____________ + ''', ''' + +
+ ''') diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 3291e2918..4005b7e5c 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -479,8 +479,10 @@ def draw_column_border(): x, y, w, h, tl, tr, br, bl = box.rounded_border_box() px, py, pw, ph, ptl, ptr, pbr, pbl = box.rounded_padding_box() - border_width = px - x - border_height = py - y + border_left = px - x + border_top = py - y + border_right = w - pw - border_left + border_bottom = h - ph - border_top img = box.border_image iw, ih, iratio = img.get_intrinsic_size( @@ -505,51 +507,52 @@ def draw_border_image(x, y, width, height, img.draw(stream, iw, ih, box.style['image_rendering']) # Top left. - draw_border_image(x, y, border_width, border_height, + draw_border_image(x, y, border_left, border_top, 0, 0, slice_left, slice_top) # Top right. - draw_border_image(x + w - border_width, y, - border_width, border_height, + draw_border_image(x + w - border_right, y, + border_right, border_top, (1-slice_right), 0, slice_right, slice_top) # Bottom right. - draw_border_image(x + w - border_width, - y + h - border_height, - border_width, border_height, + draw_border_image(x + w - border_right, + y + h - border_bottom, + border_right, border_bottom, (1-slice_right), (1-slice_bottom), slice_right, slice_bottom) # Bottom left. - draw_border_image(x, y + h - border_height, - border_width, border_height, + draw_border_image(x, y + h - border_bottom, + border_left, border_bottom, 0, (1-slice_bottom), slice_left, slice_bottom) if slice_left + slice_right < 1.0: # Top middle. - draw_border_image(x + border_width, y, - w - 2*border_width, border_height, + draw_border_image(x + border_left, y, + w - border_left - border_right, border_top, slice_left, 0, 1 - slice_left - slice_right, slice_top) # Bottom middle. - draw_border_image(x + border_width, - y + h - border_height, - w - 2*border_width, border_height, + draw_border_image(x + border_left, + y + h - border_bottom, + w - border_left - border_right, border_bottom, slice_left, 1 - slice_bottom, 1 - slice_left - slice_right, slice_bottom) if slice_top + slice_bottom < 1.0: # Right middle. - draw_border_image(x + w - border_width, - y + border_height, - border_width, h - 2*border_height, + draw_border_image(x + w - border_right, + y + border_top, + border_right, h - border_top - border_bottom, (1 - slice_right), slice_top, slice_right, 1 - slice_top - slice_bottom) # Left middle. - draw_border_image(x, y + border_height, - border_width, h - 2*border_height, + draw_border_image(x, y + border_top, + border_left, h - border_top - border_bottom, 0, slice_top, slice_left, 1 - slice_top - slice_bottom) if (should_fill and slice_left + slice_right < 1.0 and slice_top + slice_bottom < 1.0): # Fill middle middle. - draw_border_image(x + border_width, y + border_height, - w - 2*border_width, h - 2*border_height, + draw_border_image(x + border_top, y + border_left, + w - border_left - border_right, + h - border_top - border_bottom, slice_left, slice_top, 1 - slice_left - slice_right, 1 - slice_top - slice_bottom) From c1966b60b15a00af2df928c927b5923f01fd7a8f Mon Sep 17 00:00:00 2001 From: Xavid Pretzer Date: Thu, 11 Apr 2024 09:24:00 -0400 Subject: [PATCH 03/30] Support non-percent-based coordinates in border-image-slice. --- tests/draw/test_box.py | 29 +++++++++++++ weasyprint/draw.py | 92 +++++++++++++++++++++++------------------- 2 files changed, 79 insertions(+), 42 deletions(-) diff --git a/tests/draw/test_box.py b/tests/draw/test_box.py index 39bf30206..4f6d4678e 100644 --- a/tests/draw/test_box.py +++ b/tests/draw/test_box.py @@ -397,3 +397,32 @@ def test_border_image_uneven_width(assert_pixels):
''') + + +@assert_no_logs +def test_border_image_not_percent(assert_pixels): + assert_pixels(''' + __________ + _RYYYMMMG_ + _M______C_ + _M______C_ + _Y______Y_ + _Y______Y_ + _BYYYCCCK_ + __________ + ''', ''' + +
+ ''') diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 4005b7e5c..d60d8c705 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -456,26 +456,39 @@ def draw_column_border(): # If there's a border image, that takes precedence. if (box.style['border_image_source'][0] != 'none' and box.border_image is not None): + img = box.border_image + iw, ih, iratio = img.get_intrinsic_size( + box.style['image_resolution'], + box.style['font_size']) + should_fill = False image_slice = box.style['border_image_slice'] if image_slice[0] == 'fill': image_slice.pop(0) should_fill = True - slice_dims = [d.value / 100. for d in image_slice] - if len(slice_dims) == 1: - slice_top = slice_bottom = slice_left = slice_right = slice_dims[0] - elif len(slice_dims) == 2: - slice_top = slice_bottom = slice_dims[0] - slice_left = slice_right = slice_dims[1] - elif len(slice_dims) == 3: - slice_top = slice_dims[0] - slice_left = slice_right = slice_dims[1] - slice_bottom = slice_dims[2] + + def compute_dim(d, intrinsic): + if isinstance(d, (int, float)): + return d + else: + assert d.unit == '%' + return d.value / 100. * intrinsic + + if len(image_slice) == 1: + slice_top = slice_bottom = compute_dim(image_slice[0], ih) + slice_left = slice_right = compute_dim(image_slice[0], iw) + elif len(image_slice) == 2: + slice_top = slice_bottom = compute_dim(image_slice[0], ih) + slice_left = slice_right = compute_dim(image_slice[1], iw) + elif len(image_slice) == 3: + slice_top = compute_dim(image_slice[0], ih) + slice_left = slice_right = compute_dim(image_slice[1], iw) + slice_bottom = compute_dim(image_slice[2], ih) else: - slice_top = slice_dims[0] - slice_right = slice_dims[1] - slice_bottom = slice_dims[2] - slice_left = slice_dims[3] + slice_top = compute_dim(image_slice[0], ih) + slice_right = compute_dim(image_slice[1], iw) + slice_bottom = compute_dim(image_slice[2], ih) + slice_left = compute_dim(image_slice[3], iw) x, y, w, h, tl, tr, br, bl = box.rounded_border_box() px, py, pw, ph, ptl, ptr, pbr, pbl = box.rounded_padding_box() @@ -484,25 +497,20 @@ def draw_column_border(): border_right = w - pw - border_left border_bottom = h - ph - border_top - img = box.border_image - iw, ih, iratio = img.get_intrinsic_size( - box.style['image_resolution'], - box.style['font_size']) - def draw_border_image(x, y, width, height, - slice_x_frac, slice_y_frac, - slice_width_frac, slice_height_frac): + slice_x, slice_y, + slice_width, slice_height): with stacked(stream): - img_width = width / slice_width_frac - img_height = height / slice_height_frac - scale_x = img_width / iw - scale_y = img_height / ih + scale_x = width / slice_width + scale_y = height / slice_height + rendered_width = iw * scale_x + rendered_height = ih * scale_y stream.rectangle(x, y, width, height) stream.clip() stream.end() stream.transform( - e=x - img_width * slice_x_frac, - f=y - img_height * slice_y_frac) + e=x - rendered_width * slice_x / iw, + f=y - rendered_height * slice_y / ih) stream.transform(a=scale_x, d=scale_y) img.draw(stream, iw, ih, box.style['image_rendering']) @@ -512,50 +520,50 @@ def draw_border_image(x, y, width, height, # Top right. draw_border_image(x + w - border_right, y, border_right, border_top, - (1-slice_right), 0, slice_right, slice_top) + iw - slice_right, 0, slice_right, slice_top) # Bottom right. draw_border_image(x + w - border_right, y + h - border_bottom, border_right, border_bottom, - (1-slice_right), (1-slice_bottom), + iw - slice_right, ih - slice_bottom, slice_right, slice_bottom) # Bottom left. draw_border_image(x, y + h - border_bottom, border_left, border_bottom, - 0, (1-slice_bottom), slice_left, slice_bottom) - if slice_left + slice_right < 1.0: + 0, ih - slice_bottom, slice_left, slice_bottom) + if slice_left + slice_right < iw: # Top middle. draw_border_image(x + border_left, y, w - border_left - border_right, border_top, slice_left, 0, - 1 - slice_left - slice_right, slice_top) + iw - slice_left - slice_right, slice_top) # Bottom middle. draw_border_image(x + border_left, y + h - border_bottom, w - border_left - border_right, border_bottom, - slice_left, 1 - slice_bottom, - 1 - slice_left - slice_right, slice_bottom) - if slice_top + slice_bottom < 1.0: + slice_left, ih - slice_bottom, + iw - slice_left - slice_right, slice_bottom) + if slice_top + slice_bottom < ih: # Right middle. draw_border_image(x + w - border_right, y + border_top, border_right, h - border_top - border_bottom, - (1 - slice_right), slice_top, - slice_right, 1 - slice_top - slice_bottom) + iw - slice_right, slice_top, + slice_right, ih - slice_top - slice_bottom) # Left middle. draw_border_image(x, y + border_top, border_left, h - border_top - border_bottom, 0, slice_top, - slice_left, 1 - slice_top - slice_bottom) - if (should_fill and slice_left + slice_right < 1.0 - and slice_top + slice_bottom < 1.0): + slice_left, ih - slice_top - slice_bottom) + if (should_fill and slice_left + slice_right < iw + and slice_top + slice_bottom < ih): # Fill middle middle. draw_border_image(x + border_top, y + border_left, w - border_left - border_right, h - border_top - border_bottom, slice_left, slice_top, - 1 - slice_left - slice_right, - 1 - slice_top - slice_bottom) + iw - slice_left - slice_right, + ih - slice_top - slice_bottom) draw_column_border() return From 9be367dab2435430af8baf2ea63e29ed8bd025f5 Mon Sep 17 00:00:00 2001 From: Xavid Pretzer Date: Thu, 11 Apr 2024 09:34:02 -0400 Subject: [PATCH 04/30] Fix silly mistake with fill in border-image-slice. --- weasyprint/draw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index d60d8c705..5d2ca98dd 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -558,7 +558,7 @@ def draw_border_image(x, y, width, height, if (should_fill and slice_left + slice_right < iw and slice_top + slice_bottom < ih): # Fill middle middle. - draw_border_image(x + border_top, y + border_left, + draw_border_image(x + border_left, y + border_top, w - border_left - border_right, h - border_top - border_bottom, slice_left, slice_top, From f6a30fc04bd0366db82194cde1748509de510cf6 Mon Sep 17 00:00:00 2001 From: Xavid Pretzer Date: Sat, 20 Apr 2024 13:32:54 -0400 Subject: [PATCH 05/30] Avoid mutating the image-slice Avoid mutating border_image_slice while drawing image borders. --- weasyprint/draw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 5d2ca98dd..3b657f3cd 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -464,7 +464,7 @@ def draw_column_border(): should_fill = False image_slice = box.style['border_image_slice'] if image_slice[0] == 'fill': - image_slice.pop(0) + image_slice = image_slice[1:]; should_fill = True def compute_dim(d, intrinsic): From 30602633318308c7c97ee814bddb96227aa8b98b Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Wed, 24 Apr 2024 17:10:44 +0200 Subject: [PATCH 06/30] Rename tests and remove extra comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We usually don’t explain specifications in tests for "simple" cases. --- tests/draw/test_box.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/draw/test_box.py b/tests/draw/test_box.py index 4f6d4678e..67c97f78c 100644 --- a/tests/draw/test_box.py +++ b/tests/draw/test_box.py @@ -276,9 +276,7 @@ def test_draw_split_border_radius(assert_pixels): @assert_no_logs -def test_border_image(assert_pixels): - # Shows default "stretch" behavior for border images. - # Note that we use a svg image to avoid antialiasing. +def test_border_image_stretch(assert_pixels): assert_pixels(''' __________ _RYYYMMMG_ @@ -336,9 +334,7 @@ def test_border_image_fill(assert_pixels): @assert_no_logs -def test_border_image_default_sllice(assert_pixels): - # By default, border-image-slice is 100%, so the whole image is in the - # four corners and nothing is in-between. +def test_border_image_default_slice(assert_pixels): assert_pixels(''' _____________ _RYMG___RYMG_ From aea95e06b8fb32f150d70f1f6b6bdecc8297666d Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Wed, 24 Apr 2024 17:20:19 +0200 Subject: [PATCH 07/30] Simplify SVG file --- tests/resources/border.svg | 145 +++++-------------------------------- 1 file changed, 17 insertions(+), 128 deletions(-) diff --git a/tests/resources/border.svg b/tests/resources/border.svg index 1ba32cada..3da4da4d2 100644 --- a/tests/resources/border.svg +++ b/tests/resources/border.svg @@ -1,129 +1,18 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + From 9e042c236aa9cdb63b3821d885343a7c203eeced Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Wed, 24 Apr 2024 19:07:01 +0200 Subject: [PATCH 08/30] Correctly split specified and computed values --- weasyprint/css/computed_values.py | 63 +++++++++++++++++++++++ weasyprint/css/properties.py | 11 ++-- weasyprint/css/validation/properties.py | 67 ++++++++++++------------- weasyprint/draw.py | 26 +++------- 4 files changed, 107 insertions(+), 60 deletions(-) diff --git a/weasyprint/css/computed_values.py b/weasyprint/css/computed_values.py index 6ea74041f..aeb11c37d 100644 --- a/weasyprint/css/computed_values.py +++ b/weasyprint/css/computed_values.py @@ -361,6 +361,69 @@ def border_width(style, name, value): return length(style, name, value, pixels_only=True) +@register_computer('border-image-slice') +def border_image_slice(style, name, values): + """Compute the ``border-image-slice`` property.""" + computed_values = [] + fill = None + for value in values: + if value == 'fill': + fill = value + else: + number, unit = value + if unit is None: + computed_values.append(number) + else: + computed_values.append(Dimension(min(number, 100), '%')) + if len(computed_values) == 1: + computed_values *= 4 + elif len(computed_values) == 2: + computed_values *= 2 + elif len(computed_values) == 3: + computed_values.append(computed_values[1]) + return (*computed_values, fill) + + +@register_computer('border-image-width') +def border_image_width(style, name, values): + """Compute the ``border-image-width`` property.""" + computed_values = [] + for value in values: + if value == 'auto': + computed_values.append(value) + else: + number, unit = value + computed_values.append(number if unit is None else value) + if len(computed_values) == 1: + computed_values *= 4 + elif len(computed_values) == 2: + computed_values *= 2 + elif len(computed_values) == 3: + computed_values.append(computed_values[1]) + return tuple(computed_values) + + +@register_computer('border-image-outset') +def border_image_outset(style, name, values): + """Compute the ``border-image-outset`` property.""" + computed_values = [ + value if isinstance(value, (int, float)) else length(style, name, value) + for value in values] + if len(computed_values) == 1: + computed_values *= 4 + elif len(computed_values) == 2: + computed_values *= 2 + elif len(computed_values) == 3: + computed_values.append(computed_values[1]) + return tuple(computed_values) + + +@register_computer('border-image-repeat') +def border_image_repeat(style, name, values): + """Compute the ``border-image-repeat`` property.""" + return (values * 2) if len(values) == 1 else values + + @register_computer('column-width') def column_width(style, name, value): """Compute the ``column-width`` property.""" diff --git a/weasyprint/css/properties.py b/weasyprint/css/properties.py index 6e644fab3..1c2781de8 100644 --- a/weasyprint/css/properties.py +++ b/weasyprint/css/properties.py @@ -68,10 +68,13 @@ 'border_top_style': 'none', 'border_top_width': 3, # computed value for 'medium' 'border_image_source': ('none', None), - 'border_image_slice': [Dimension(100, '%')], - 'border_image_width': 1, - 'border_image_outset': 0, - 'border_image_repeat': ('stretch',), + 'border_image_slice': ( + Dimension(100, '%'), Dimension(100, '%'), + Dimension(100, '%'), Dimension(100, '%'), + None), + 'border_image_width': (1, 1, 1, 1), + 'border_image_outset': (0, 0, 0, 0), + 'border_image_repeat': ('stretch', 'stretch'), # Color 3 (REC): https://www.w3.org/TR/css-color-3/ diff --git a/weasyprint/css/validation/properties.py b/weasyprint/css/validation/properties.py index de2ac1bd9..377a75ac1 100644 --- a/weasyprint/css/validation/properties.py +++ b/weasyprint/css/validation/properties.py @@ -167,9 +167,8 @@ def color(token): @comma_separated_list @single_token def background_image(token, base_url): - if token.type != 'function': - if get_keyword(token) == 'none': - return 'none', None + if get_keyword(token) == 'none': + return 'none', None return get_image(token, base_url) @@ -424,74 +423,70 @@ def border_width(token): @property(wants_base_url=True) @single_token def border_image_source(token, base_url): - if token.type not in ('function', 'url'): - if get_keyword(token) == 'none': - return 'none', None + if get_keyword(token) == 'none': + return 'none', None return get_image(token, base_url) @property() def border_image_slice(tokens): values = [] - keyword_cnt = 0 - for token in tokens: + fill = False + for i, token in enumerate(tokens): # Don't use get_length() because a dimension with a unit is disallowed. if token.type == 'percentage' and token.value >= 0: - values.append(Dimension(min(token.value, 100), '%')) - elif token.type == 'number': - values.append(token.value) - elif get_keyword(token) == 'fill': - keyword_cnt += 1 + values.append(Dimension(token.value, '%')) + elif token.type == 'number' and token.value >= 0: + values.append(Dimension(token.value, None)) + elif get_keyword(token) == 'fill' and not fill and i in (0, len(tokens) - 1): + fill = True + values.append('fill') else: - return None - if keyword_cnt > 0: - # Always put it at the beginning to make processing easier. - # Would it be better to use some sort of object here? - values = ['fill'] + values - if keyword_cnt <= 1 and (1 <= len(values) - keyword_cnt <= 4): - return values + return + + if 1 <= len(values) - int(fill) <= 4: + return tuple(values) @property() def border_image_width(tokens): - if get_single_keyword(tokens) == 'auto': - return 'auto' values = [] for token in tokens: - if token.type == 'number' and token.value >= 0: - values.append(token.value) + if get_keyword(token) == 'auto': + values.append('auto') + elif token.type == 'number' and token.value >= 0: + values.append(Dimension(token.value, None)) else: - length = get_length(token, negative=False, percentage=True) - if length: + if length := get_length(token, negative=False, percentage=True): values.append(length) else: - return None + return + if 1 <= len(values) <= 4: - return values + return tuple(values) @property() def border_image_outset(tokens): values = [] for token in tokens: - if token.type == 'number': - values.append(token.value) + if token.type == 'number' and token.value >= 0: + values.append(Dimension(token.value, None)) else: - length = get_length(token) - if length: + if length := get_length(token, negative=False): values.append(length) else: - return None + return + if 1 <= len(values) <= 4: - return values + return tuple(values) @property() def border_image_repeat(tokens): if 1 <= len(tokens) <= 2: keywords = tuple(get_keyword(token) for token in tokens) - if all(kw in ('stretch', 'repeat', 'round', 'space') - for kw in keywords): + if set(keywords) <= {'stretch', 'repeat', 'round', 'space'}: return keywords diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 3b657f3cd..2d031c8ed 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -461,11 +461,8 @@ def draw_column_border(): box.style['image_resolution'], box.style['font_size']) - should_fill = False - image_slice = box.style['border_image_slice'] - if image_slice[0] == 'fill': - image_slice = image_slice[1:]; - should_fill = True + image_slice = box.style['border_image_slice'][:4] + should_fill = box.style['border_image_slice'][4] def compute_dim(d, intrinsic): if isinstance(d, (int, float)): @@ -474,21 +471,10 @@ def compute_dim(d, intrinsic): assert d.unit == '%' return d.value / 100. * intrinsic - if len(image_slice) == 1: - slice_top = slice_bottom = compute_dim(image_slice[0], ih) - slice_left = slice_right = compute_dim(image_slice[0], iw) - elif len(image_slice) == 2: - slice_top = slice_bottom = compute_dim(image_slice[0], ih) - slice_left = slice_right = compute_dim(image_slice[1], iw) - elif len(image_slice) == 3: - slice_top = compute_dim(image_slice[0], ih) - slice_left = slice_right = compute_dim(image_slice[1], iw) - slice_bottom = compute_dim(image_slice[2], ih) - else: - slice_top = compute_dim(image_slice[0], ih) - slice_right = compute_dim(image_slice[1], iw) - slice_bottom = compute_dim(image_slice[2], ih) - slice_left = compute_dim(image_slice[3], iw) + slice_top = compute_dim(image_slice[0], ih) + slice_right = compute_dim(image_slice[1], iw) + slice_bottom = compute_dim(image_slice[2], ih) + slice_left = compute_dim(image_slice[3], iw) x, y, w, h, tl, tr, br, bl = box.rounded_border_box() px, py, pw, ph, ptl, ptr, pbr, pbl = box.rounded_padding_box() From 6f9550b5c288b44a708c0c02ac24b4a94a90aefd Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Wed, 24 Apr 2024 19:12:22 +0200 Subject: [PATCH 09/30] Clean border image drawing code --- weasyprint/draw.py | 129 ++++++++++++++++++++++----------------------- 1 file changed, 63 insertions(+), 66 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 2d031c8ed..13d12155f 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -454,27 +454,28 @@ def draw_column_border(): return # If there's a border image, that takes precedence. - if (box.style['border_image_source'][0] != 'none' - and box.border_image is not None): + if box.style['border_image_source'][0] != 'none' and box.border_image is not None: img = box.border_image - iw, ih, iratio = img.get_intrinsic_size( - box.style['image_resolution'], - box.style['font_size']) + width, height, ratio = img.get_intrinsic_size( + box.style['image_resolution'], box.style['font_size']) + intrinsic_width, intrinsic_height = replaced.default_image_sizing( + width, height, ratio, specified_width=None, specified_height=None, + default_width=box.border_width(), default_height=box.border_height()) image_slice = box.style['border_image_slice'][:4] should_fill = box.style['border_image_slice'][4] - def compute_dim(d, intrinsic): - if isinstance(d, (int, float)): - return d + def compute_slice_dimension(dimension, intrinsic): + if isinstance(dimension, (int, float)): + return dimension else: - assert d.unit == '%' - return d.value / 100. * intrinsic + assert dimension.unit == '%' + return dimension.value / 100 * intrinsic - slice_top = compute_dim(image_slice[0], ih) - slice_right = compute_dim(image_slice[1], iw) - slice_bottom = compute_dim(image_slice[2], ih) - slice_left = compute_dim(image_slice[3], iw) + slice_top = compute_slice_dimension(image_slice[0], intrinsic_height) + slice_right = compute_slice_dimension(image_slice[1], intrinsic_width) + slice_bottom = compute_slice_dimension(image_slice[2], intrinsic_height) + slice_left = compute_slice_dimension(image_slice[3], intrinsic_width) x, y, w, h, tl, tr, br, bl = box.rounded_border_box() px, py, pw, ph, ptl, ptr, pbr, pbl = box.rounded_padding_box() @@ -483,73 +484,70 @@ def compute_dim(d, intrinsic): border_right = w - pw - border_left border_bottom = h - ph - border_top - def draw_border_image(x, y, width, height, - slice_x, slice_y, - slice_width, slice_height): + def draw_border_image(x, y, width, height, slice_x, slice_y, slice_width, + slice_height): with stacked(stream): scale_x = width / slice_width scale_y = height / slice_height - rendered_width = iw * scale_x - rendered_height = ih * scale_y + rendered_width = intrinsic_width * scale_x + rendered_height = intrinsic_height * scale_y stream.rectangle(x, y, width, height) stream.clip() stream.end() stream.transform( - e=x - rendered_width * slice_x / iw, - f=y - rendered_height * slice_y / ih) + e=x - rendered_width * slice_x / intrinsic_width, + f=y - rendered_height * slice_y / intrinsic_height) stream.transform(a=scale_x, d=scale_y) - img.draw(stream, iw, ih, box.style['image_rendering']) + img.draw( + stream, intrinsic_width, intrinsic_height, + box.style['image_rendering']) # Top left. - draw_border_image(x, y, border_left, border_top, - 0, 0, slice_left, slice_top) + draw_border_image( + x, y, border_left, border_top, 0, 0, slice_left, slice_top) # Top right. - draw_border_image(x + w - border_right, y, - border_right, border_top, - iw - slice_right, 0, slice_right, slice_top) + draw_border_image( + x + w - border_right, y, border_right, border_top, + intrinsic_width - slice_right, 0, slice_right, slice_top) # Bottom right. - draw_border_image(x + w - border_right, - y + h - border_bottom, - border_right, border_bottom, - iw - slice_right, ih - slice_bottom, - slice_right, slice_bottom) + draw_border_image( + x + w - border_right, y + h - border_bottom, border_right, border_bottom, + intrinsic_width - slice_right, intrinsic_height - slice_bottom, + slice_right, slice_bottom) # Bottom left. - draw_border_image(x, y + h - border_bottom, - border_left, border_bottom, - 0, ih - slice_bottom, slice_left, slice_bottom) - if slice_left + slice_right < iw: + draw_border_image( + x, y + h - border_bottom, border_left, border_bottom, + 0, intrinsic_height - slice_bottom, slice_left, slice_bottom) + if slice_left + slice_right < intrinsic_width: # Top middle. - draw_border_image(x + border_left, y, - w - border_left - border_right, border_top, - slice_left, 0, - iw - slice_left - slice_right, slice_top) + draw_border_image( + x + border_left, y, w - border_left - border_right, border_top, + slice_left, 0, intrinsic_width - slice_left - slice_right, slice_top) # Bottom middle. - draw_border_image(x + border_left, - y + h - border_bottom, - w - border_left - border_right, border_bottom, - slice_left, ih - slice_bottom, - iw - slice_left - slice_right, slice_bottom) - if slice_top + slice_bottom < ih: + draw_border_image( + x + border_left, y + h - border_bottom, + w - border_left - border_right, border_bottom, + slice_left, intrinsic_height - slice_bottom, + intrinsic_width - slice_left - slice_right, slice_bottom) + if slice_top + slice_bottom < intrinsic_height: # Right middle. - draw_border_image(x + w - border_right, - y + border_top, - border_right, h - border_top - border_bottom, - iw - slice_right, slice_top, - slice_right, ih - slice_top - slice_bottom) + draw_border_image( + x + w - border_right, y + border_top, + border_right, h - border_top - border_bottom, + intrinsic_width - slice_right, slice_top, + slice_right, intrinsic_height - slice_top - slice_bottom) # Left middle. - draw_border_image(x, y + border_top, - border_left, h - border_top - border_bottom, - 0, slice_top, - slice_left, ih - slice_top - slice_bottom) - if (should_fill and slice_left + slice_right < iw - and slice_top + slice_bottom < ih): - # Fill middle middle. - draw_border_image(x + border_left, y + border_top, - w - border_left - border_right, - h - border_top - border_bottom, - slice_left, slice_top, - iw - slice_left - slice_right, - ih - slice_top - slice_bottom) + draw_border_image( + x, y + border_top, border_left, h - border_top - border_bottom, + 0, slice_top, slice_left, intrinsic_height - slice_top - slice_bottom) + if (should_fill and slice_left + slice_right < intrinsic_width + and slice_top + slice_bottom < intrinsic_height): + # Fill middle. + draw_border_image( + x + border_left, y + border_top, w - border_left - border_right, + h - border_top - border_bottom, slice_left, slice_top, + intrinsic_width - slice_left - slice_right, + intrinsic_height - slice_top - slice_bottom) draw_column_border() return @@ -561,8 +559,7 @@ def draw_border_image(x, y, width, height, # The 4 sides are solid or double, and they have the same color. Oh yeah! # We can draw them so easily! - if set(styles) in (set(('solid',)), set(('double',))) and ( - len(set(colors)) == 1): + if set(styles) in (set(('solid',)), set(('double',))) and (len(set(colors)) == 1): draw_rounded_border(stream, box, styles[0], colors[0]) draw_column_border() return From 0bb0f5d78675ea429b8f23960103ea87cea215a3 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Thu, 25 Apr 2024 11:13:03 +0200 Subject: [PATCH 10/30] Add validation tests --- tests/css/test_validation.py | 108 +++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/tests/css/test_validation.py b/tests/css/test_validation.py index 1a428b887..97718ad7c 100644 --- a/tests/css/test_validation.py +++ b/tests/css/test_validation.py @@ -362,6 +362,114 @@ def test_image_orientation_invalid(rule): assert_invalid(f'image-orientation: {rule}') +@assert_no_logs +@pytest.mark.parametrize('rule, value', ( + ('1', ((1, None),)), + ('1 2 3 4', ((1, None), (2, None), (3, None), (4, None))), + ('50% 1000.1 0', ((50, '%'), (1000.1, None), (0, None))), + ('1% 2% 3% 4%', ((1, '%'), (2, '%'), (3, '%'), (4, '%'))), + ('fill 10% 20', ('fill', (10, '%'), (20, None))), + ('0 1 0.5 fill', ((0, None), (1, None), (0.5, None), 'fill')), +)) +def test_border_image_slice(rule, value): + assert get_value(f'border-image-slice: {rule}') == value + + +@assert_no_logs +@pytest.mark.parametrize('rule', ( + 'none', + '1, 2', + '-10', + '-10%', + '1 2 3 -10%', + '-0.3', + '1 fill 2', + 'fill 1 2 3 fill', +)) +def test_border_image_slice_invalid(rule): + assert_invalid(f'border-image-slice: {rule}') + + +@assert_no_logs +@pytest.mark.parametrize('rule, value', ( + ('1', ((1, None),)), + ('1 2 3 4', ((1, None), (2, None), (3, None), (4, None))), + ('50% 1000.1 0', ((50, '%'), (1000.1, None), (0, None))), + ('1% 2px 3em 4', ((1, '%'), (2, 'px'), (3, 'em'), (4, None))), + ('auto', ('auto',)), + ('1 auto', ((1, None), 'auto')), + ('auto auto', ('auto', 'auto')), + ('auto auto auto 2', ('auto', 'auto', 'auto', (2, None))), +)) +def test_border_image_width(rule, value): + assert get_value(f'border-image-width: {rule}') == value + + +@assert_no_logs +@pytest.mark.parametrize('rule', ( + 'none', + '1, 2', + '1 -2', + '-10', + '-10%', + '1px 2px 3px -10%', + '-3px', + 'auto auto auto auto auto', + '1 2 3 4 5', +)) +def test_border_image_width_invalid(rule): + assert_invalid(f'border-image-width: {rule}') + + +@assert_no_logs +@pytest.mark.parametrize('rule, value', ( + ('1', ((1, None),)), + ('1 2 3 4', ((1, None), (2, None), (3, None), (4, None))), + ('50px 1000.1 0', ((50, 'px'), (1000.1, None), (0, None))), + ('1in 2px 3em 4', ((1, 'in'), (2, 'px'), (3, 'em'), (4, None))), +)) +def test_border_image_outset(rule, value): + assert get_value(f'border-image-outset: {rule}') == value + + +@assert_no_logs +@pytest.mark.parametrize('rule', ( + 'none', + 'auto', + '1, 2', + '-10', + '1 -2', + '10%', + '1px 2px 3px -10px', + '-3px', + '1 2 3 4 5', +)) +def test_border_image_outset_invalid(rule): + assert_invalid(f'border-image-outset: {rule}') + + +@assert_no_logs +@pytest.mark.parametrize('rule, value', ( + ('stretch', ('stretch',)), + ('repeat repeat', ('repeat', 'repeat')), + ('round space', ('round', 'space')), +)) +def test_border_image_repeat(rule, value): + assert get_value(f'border-image-repeat: {rule}') == value + + +@assert_no_logs +@pytest.mark.parametrize('rule', ( + 'none', + 'test', + 'round round round', + 'stretch space round', + 'repeat test', +)) +def test_border_image_repeat_invalid(rule): + assert_invalid(f'border-image-repeat: {rule}') + + @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('test content(text)', (('test', (('content()', 'text'),)),)), From d7bd8287d8545f13c78baff18d544b25b9ebbc21 Mon Sep 17 00:00:00 2001 From: Xavid Pretzer Date: Thu, 25 Apr 2024 20:56:30 -0400 Subject: [PATCH 11/30] Add support for the border-image-repeat CSS property. --- tests/draw/test_box.py | 59 +++++++++++ tests/resources/border2.svg | 191 ++++++++++++++++++++++++++++++++++++ weasyprint/draw.py | 94 ++++++++++++++---- 3 files changed, 327 insertions(+), 17 deletions(-) create mode 100644 tests/resources/border2.svg diff --git a/tests/draw/test_box.py b/tests/draw/test_box.py index 67c97f78c..0dcfe55fe 100644 --- a/tests/draw/test_box.py +++ b/tests/draw/test_box.py @@ -422,3 +422,62 @@ def test_border_image_not_percent(assert_pixels):
''') + +@assert_no_logs +def test_border_image_repeat(assert_pixels): + assert_pixels(''' + ___________ + _RYMYMYMYG_ + _M_______C_ + _Y_______Y_ + _M_______C_ + _Y_______Y_ + _BYCYCYCYK_ + ___________ + ''', ''' + +
+ ''') + +@assert_no_logs +def test_border_image_space(assert_pixels): + assert_pixels(''' + _________ + _R_YMC_G_ + _________ + _M_____C_ + _Y_____Y_ + _C_____M_ + _________ + _B_YCM_K_ + _________ + ''', ''' + +
+ ''') diff --git a/tests/resources/border2.svg b/tests/resources/border2.svg new file mode 100644 index 000000000..1a6458ca4 --- /dev/null +++ b/tests/resources/border2.svg @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 9b6cf2cf4..913e7c106 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -465,6 +465,12 @@ def draw_column_border(): image_slice = box.style['border_image_slice'][:4] should_fill = box.style['border_image_slice'][4] + if len(box.style['border_image_repeat']) == 1: + style_repeat_x = style_repeat_y = box.style['border_image_repeat'] + else: + style_repeat_y = box.style['border_image_repeat'][0] + style_repeat_x = box.style['border_image_repeat'][1] + def compute_slice_dimension(dimension, intrinsic): if isinstance(dimension, (int, float)): return dimension @@ -484,23 +490,70 @@ def compute_slice_dimension(dimension, intrinsic): border_right = w - pw - border_left border_bottom = h - ph - border_top - def draw_border_image(x, y, width, height, slice_x, slice_y, slice_width, - slice_height): + def draw_border_image(x, y, width, height, slice_x, slice_y, + slice_width, slice_height, + repeat_x='stretch', repeat_y='stretch'): with stacked(stream): - scale_x = width / slice_width - scale_y = height / slice_height - rendered_width = intrinsic_width * scale_x - rendered_height = intrinsic_height * scale_y stream.rectangle(x, y, width, height) stream.clip() stream.end() - stream.transform( - e=x - rendered_width * slice_x / intrinsic_width, - f=y - rendered_height * slice_y / intrinsic_height) + extra_dx = 0 + extra_dy = 0 + if repeat_x == 'repeat': + n_repeats_x = ceil(width / slice_width) + scale_x = 1 + elif repeat_x == 'space': + n_repeats_x = floor(width / slice_width) + scale_x = 1 + # Space is before the first repeat and after the last, + # so there's one more space than repeat. + extra_dx = ((width - n_repeats_x * slice_width) + / (n_repeats_x + 1)) + elif repeat_x == 'round': + n_repeats_x = round(width / slice_width) + scale_x = width / (n_repeats_x * slice_width) + else: + n_repeats_x = 1 + scale_x = width / slice_width + if repeat_y == 'repeat': + n_repeats_y = ceil(height / slice_height) + scale_y = 1 + elif repeat_y == 'space': + n_repeats_y = floor(height / slice_height) + scale_y = 1 + # Space is before the first repeat and after the last, + # so there's one more space than repeat. + extra_dy = ((height - n_repeats_y * slice_height) + / (n_repeats_y + 1)) + elif repeat_y == 'round': + n_repeats_y = round(height / slice_height) + scale_y = height / (n_repeats_y * slice_height) + else: + n_repeats_y = 1 + scale_y = height / slice_height + rendered_width = intrinsic_width * scale_x + rendered_height = intrinsic_height * scale_y + offset_x = rendered_width * slice_x / intrinsic_width + offset_y = rendered_height * slice_y / intrinsic_height + stream.transform(e=x - offset_x + extra_dx, + f=y - offset_y + extra_dy) stream.transform(a=scale_x, d=scale_y) - img.draw( - stream, intrinsic_width, intrinsic_height, - box.style['image_rendering']) + for xcnt in range(n_repeats_x): + with stacked(stream): + for ycnt in range(n_repeats_y): + with stacked(stream): + stream.rectangle(offset_x / scale_x, + offset_y / scale_y, + slice_width, + slice_height) + stream.clip() + stream.end() + img.draw(stream, intrinsic_width, + intrinsic_height, + box.style['image_rendering']) + stream.transform(f=slice_height + extra_dy) + stream.transform(e=slice_width + extra_dx) + # Top left. draw_border_image( @@ -522,24 +575,29 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, slice_width, # Top middle. draw_border_image( x + border_left, y, w - border_left - border_right, border_top, - slice_left, 0, intrinsic_width - slice_left - slice_right, slice_top) + slice_left, 0, intrinsic_width - slice_left - slice_right, + slice_top, repeat_x=style_repeat_x) # Bottom middle. draw_border_image( x + border_left, y + h - border_bottom, w - border_left - border_right, border_bottom, slice_left, intrinsic_height - slice_bottom, - intrinsic_width - slice_left - slice_right, slice_bottom) + intrinsic_width - slice_left - slice_right, slice_bottom, + repeat_x=style_repeat_x) if slice_top + slice_bottom < intrinsic_height: # Right middle. draw_border_image( x + w - border_right, y + border_top, border_right, h - border_top - border_bottom, intrinsic_width - slice_right, slice_top, - slice_right, intrinsic_height - slice_top - slice_bottom) + slice_right, intrinsic_height - slice_top - slice_bottom, + repeat_y=style_repeat_y) # Left middle. draw_border_image( x, y + border_top, border_left, h - border_top - border_bottom, - 0, slice_top, slice_left, intrinsic_height - slice_top - slice_bottom) + 0, slice_top, slice_left, + intrinsic_height - slice_top - slice_bottom, + repeat_y=style_repeat_y) if (should_fill and slice_left + slice_right < intrinsic_width and slice_top + slice_bottom < intrinsic_height): # Fill middle. @@ -547,7 +605,9 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, slice_width, x + border_left, y + border_top, w - border_left - border_right, h - border_top - border_bottom, slice_left, slice_top, intrinsic_width - slice_left - slice_right, - intrinsic_height - slice_top - slice_bottom) + intrinsic_height - slice_top - slice_bottom, + repeat_x=style_repeat_x, + repeat_y=style_repeat_y) draw_column_border() return From e9ac268e0b9360f0ce71a207556a15aa6df0cb5b Mon Sep 17 00:00:00 2001 From: Xavid Pretzer Date: Thu, 25 Apr 2024 21:08:14 -0400 Subject: [PATCH 12/30] Remove unnecessary check for length of border-image-repeat, that's handled at validation. --- weasyprint/draw.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 913e7c106..2f8c9cd58 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -465,11 +465,8 @@ def draw_column_border(): image_slice = box.style['border_image_slice'][:4] should_fill = box.style['border_image_slice'][4] - if len(box.style['border_image_repeat']) == 1: - style_repeat_x = style_repeat_y = box.style['border_image_repeat'] - else: - style_repeat_y = box.style['border_image_repeat'][0] - style_repeat_x = box.style['border_image_repeat'][1] + style_repeat_y = box.style['border_image_repeat'][0] + style_repeat_x = box.style['border_image_repeat'][1] def compute_slice_dimension(dimension, intrinsic): if isinstance(dimension, (int, float)): From bd51459201c08361c334936eea8037baaafa44de Mon Sep 17 00:00:00 2001 From: Xavid Pretzer Date: Thu, 25 Apr 2024 21:10:19 -0400 Subject: [PATCH 13/30] Move border-image-repeat logic to not be in the middle of the slice code. --- weasyprint/draw.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 2f8c9cd58..4229c1bda 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -465,9 +465,6 @@ def draw_column_border(): image_slice = box.style['border_image_slice'][:4] should_fill = box.style['border_image_slice'][4] - style_repeat_y = box.style['border_image_repeat'][0] - style_repeat_x = box.style['border_image_repeat'][1] - def compute_slice_dimension(dimension, intrinsic): if isinstance(dimension, (int, float)): return dimension @@ -480,6 +477,9 @@ def compute_slice_dimension(dimension, intrinsic): slice_bottom = compute_slice_dimension(image_slice[2], intrinsic_height) slice_left = compute_slice_dimension(image_slice[3], intrinsic_width) + style_repeat_y = box.style['border_image_repeat'][0] + style_repeat_x = box.style['border_image_repeat'][1] + x, y, w, h, tl, tr, br, bl = box.rounded_border_box() px, py, pw, ph, ptl, ptr, pbr, pbl = box.rounded_padding_box() border_left = px - x From 2fe3364136f2f021ca4b89adb6300ae19ea994e3 Mon Sep 17 00:00:00 2001 From: Xavid Pretzer Date: Thu, 25 Apr 2024 21:31:19 -0400 Subject: [PATCH 14/30] Add support for border-image-outset. --- tests/draw/test_box.py | 35 +++++++++++++++++++++++++++++++++++ weasyprint/css/properties.py | 4 +++- weasyprint/draw.py | 18 ++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/tests/draw/test_box.py b/tests/draw/test_box.py index 0dcfe55fe..a8c37e767 100644 --- a/tests/draw/test_box.py +++ b/tests/draw/test_box.py @@ -423,6 +423,7 @@ def test_border_image_not_percent(assert_pixels):
''') + @assert_no_logs def test_border_image_repeat(assert_pixels): assert_pixels(''' @@ -452,6 +453,7 @@ def test_border_image_repeat(assert_pixels):
''') + @assert_no_logs def test_border_image_space(assert_pixels): assert_pixels(''' @@ -481,3 +483,36 @@ def test_border_image_space(assert_pixels):
''') + + +@assert_no_logs +def test_border_image_outset(assert_pixels): + assert_pixels(''' + ____________ + _RYYYYMMMMG_ + _M________C_ + _M_bbbbbb_C_ + _M_bbbbbb_C_ + _Y_bbbbbb_Y_ + _Y_bbbbbb_Y_ + _Y________Y_ + _BYYYYCCCCK_ + ____________ + ''', ''' + +
+ ''') diff --git a/weasyprint/css/properties.py b/weasyprint/css/properties.py index 1c2781de8..1917fe122 100644 --- a/weasyprint/css/properties.py +++ b/weasyprint/css/properties.py @@ -73,7 +73,9 @@ Dimension(100, '%'), Dimension(100, '%'), None), 'border_image_width': (1, 1, 1, 1), - 'border_image_outset': (0, 0, 0, 0), + 'border_image_outset': ( + Dimension(0, None), Dimension(0, None), + Dimension(0, None), Dimension(0, None)), 'border_image_repeat': ('stretch', 'stretch'), diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 4229c1bda..d434b116f 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -487,6 +487,24 @@ def compute_slice_dimension(dimension, intrinsic): border_right = w - pw - border_left border_bottom = h - ph - border_top + def compute_outset_dimension(dimension, from_border): + if dimension.unit is None: + return dimension.value * from_border + else: + assert dimension.unit == 'px' + return dimension.value + + outsets = box.style['border_image_outset'] + outset_top = compute_outset_dimension(outsets[0], border_top) + outset_right = compute_outset_dimension(outsets[1], border_right) + outset_bottom = compute_outset_dimension(outsets[2], border_bottom) + outset_left = compute_outset_dimension(outsets[3], border_left) + + x -= outset_left + y -= outset_top + w += outset_left + outset_right + h += outset_top + outset_bottom + def draw_border_image(x, y, width, height, slice_x, slice_y, slice_width, slice_height, repeat_x='stretch', repeat_y='stretch'): From b3efa784d9fc9940f70fa566a998331cda30e63a Mon Sep 17 00:00:00 2001 From: Xavid Pretzer Date: Fri, 26 Apr 2024 00:05:59 -0400 Subject: [PATCH 15/30] Support border-image-width. --- tests/draw/test_box.py | 30 ++++++++++++++++++++++++++++++ weasyprint/draw.py | 27 +++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/tests/draw/test_box.py b/tests/draw/test_box.py index a8c37e767..55152b48c 100644 --- a/tests/draw/test_box.py +++ b/tests/draw/test_box.py @@ -516,3 +516,33 @@ def test_border_image_outset(assert_pixels):
''') + + +@assert_no_logs +def test_border_image_width(assert_pixels): + assert_pixels(''' + __________ + _RRYYMMGG_ + _RRYYMMGG_ + _MM____CC_ + _YY____YY_ + _BBYYCCKK_ + _BBYYCCKK_ + __________ + ''', ''' + +
+ ''') diff --git a/weasyprint/draw.py b/weasyprint/draw.py index d434b116f..a17a131c7 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -505,6 +505,33 @@ def compute_outset_dimension(dimension, from_border): w += outset_left + outset_right h += outset_top + outset_bottom + def compute_width_adjustment(dimension, original, intrinsic, + area_dimension): + if dimension == 'auto': + return intrinsic + elif isinstance(dimension, (int, float)): + return dimension * original + elif dimension.unit == '%': + return dimension.value / 100. * area_dimension + else: + assert dimension.unit == 'px' + return dimension.value + + # We make adjustments to the border_* variables after handling outsets + # because numerical outsets are relative to border-width, not + # border-image-width. Also, the border image area that is used + # for percentage-based border-image-width values includes any expanded + # area due to border-image-outset. + width_adjs = box.style['border_image_width'] + border_top = compute_width_adjustment(width_adjs[0], border_top, + slice_top, h) + border_right = compute_width_adjustment(width_adjs[1], border_right, + slice_right, w) + border_bottom = compute_width_adjustment(width_adjs[2], border_bottom, + slice_bottom, h) + border_left = compute_width_adjustment(width_adjs[3], border_left, + slice_left, w) + def draw_border_image(x, y, width, height, slice_x, slice_y, slice_width, slice_height, repeat_x='stretch', repeat_y='stretch'): From eae6ebcd4498f0a146d26bde5387221406cc2c6b Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 10:33:06 +0200 Subject: [PATCH 16/30] Clean coding style --- weasyprint/draw.py | 46 ++++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index a17a131c7..6b4e54406 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -477,8 +477,7 @@ def compute_slice_dimension(dimension, intrinsic): slice_bottom = compute_slice_dimension(image_slice[2], intrinsic_height) slice_left = compute_slice_dimension(image_slice[3], intrinsic_width) - style_repeat_y = box.style['border_image_repeat'][0] - style_repeat_x = box.style['border_image_repeat'][1] + style_repeat_y, style_repeat_x = box.style['border_image_repeat'] x, y, w, h, tl, tr, br, bl = box.rounded_border_box() px, py, pw, ph, ptl, ptr, pbr, pbl = box.rounded_padding_box() @@ -512,7 +511,7 @@ def compute_width_adjustment(dimension, original, intrinsic, elif isinstance(dimension, (int, float)): return dimension * original elif dimension.unit == '%': - return dimension.value / 100. * area_dimension + return dimension.value / 100 * area_dimension else: assert dimension.unit == 'px' return dimension.value @@ -523,14 +522,14 @@ def compute_width_adjustment(dimension, original, intrinsic, # for percentage-based border-image-width values includes any expanded # area due to border-image-outset. width_adjs = box.style['border_image_width'] - border_top = compute_width_adjustment(width_adjs[0], border_top, - slice_top, h) - border_right = compute_width_adjustment(width_adjs[1], border_right, - slice_right, w) - border_bottom = compute_width_adjustment(width_adjs[2], border_bottom, - slice_bottom, h) - border_left = compute_width_adjustment(width_adjs[3], border_left, - slice_left, w) + border_top = compute_width_adjustment( + width_adjs[0], border_top, slice_top, h) + border_right = compute_width_adjustment( + width_adjs[1], border_right, slice_right, w) + border_bottom = compute_width_adjustment( + width_adjs[2], border_bottom, slice_bottom, h) + border_left = compute_width_adjustment( + width_adjs[3], border_left, slice_left, w) def draw_border_image(x, y, width, height, slice_x, slice_y, slice_width, slice_height, @@ -549,8 +548,7 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, scale_x = 1 # Space is before the first repeat and after the last, # so there's one more space than repeat. - extra_dx = ((width - n_repeats_x * slice_width) - / (n_repeats_x + 1)) + extra_dx = (width - n_repeats_x * slice_width) / (n_repeats_x + 1) elif repeat_x == 'round': n_repeats_x = round(width / slice_width) scale_x = width / (n_repeats_x * slice_width) @@ -565,8 +563,7 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, scale_y = 1 # Space is before the first repeat and after the last, # so there's one more space than repeat. - extra_dy = ((height - n_repeats_y * slice_height) - / (n_repeats_y + 1)) + extra_dy = (height - n_repeats_y * slice_height) / (n_repeats_y + 1) elif repeat_y == 'round': n_repeats_y = round(height / slice_height) scale_y = height / (n_repeats_y * slice_height) @@ -577,29 +574,26 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, rendered_height = intrinsic_height * scale_y offset_x = rendered_width * slice_x / intrinsic_width offset_y = rendered_height * slice_y / intrinsic_height - stream.transform(e=x - offset_x + extra_dx, - f=y - offset_y + extra_dy) + stream.transform(e=x - offset_x + extra_dx, f=y - offset_y + extra_dy) stream.transform(a=scale_x, d=scale_y) for xcnt in range(n_repeats_x): with stacked(stream): for ycnt in range(n_repeats_y): with stacked(stream): - stream.rectangle(offset_x / scale_x, - offset_y / scale_y, - slice_width, - slice_height) + stream.rectangle( + offset_x / scale_x, offset_y / scale_y, + slice_width, slice_height) stream.clip() stream.end() - img.draw(stream, intrinsic_width, - intrinsic_height, - box.style['image_rendering']) + img.draw( + stream, intrinsic_width, intrinsic_height, + box.style['image_rendering']) stream.transform(f=slice_height + extra_dy) stream.transform(e=slice_width + extra_dx) # Top left. - draw_border_image( - x, y, border_left, border_top, 0, 0, slice_left, slice_top) + draw_border_image(x, y, border_left, border_top, 0, 0, slice_left, slice_top) # Top right. draw_border_image( x + w - border_right, y, border_right, border_top, From 800520776dad959dec44904faced97b5687e5ce0 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 14:46:39 +0200 Subject: [PATCH 17/30] Handle gradients for border images --- tests/draw/test_box.py | 30 ++++++++++++++++++++++++++++++ weasyprint/layout/background.py | 7 +++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/draw/test_box.py b/tests/draw/test_box.py index 55152b48c..168aeb198 100644 --- a/tests/draw/test_box.py +++ b/tests/draw/test_box.py @@ -546,3 +546,33 @@ def test_border_image_width(assert_pixels):
''') + + +@assert_no_logs +def test_border_image_gradient(assert_pixels): + assert_pixels(''' + __________ + _RRRRRRRR_ + _RRRRRRRR_ + _RR____RR_ + _BB____BB_ + _BBBBBBBB_ + _BBBBBBBB_ + __________ + ''', ''' + +
+ ''') diff --git a/weasyprint/layout/background.py b/weasyprint/layout/background.py index 429159fd2..dd997c54e 100644 --- a/weasyprint/layout/background.py +++ b/weasyprint/layout/background.py @@ -51,8 +51,11 @@ def layout_box_backgrounds(page, box, get_image_from_uri, layout_children=True, # convenient place to get the image. if (style['border_image_source'] and style['border_image_source'][0] != 'none'): - box.border_image = get_image_from_uri( - url=style['border_image_source'][1]) + type_, value = style['border_image_source'] + if type_ == 'url': + box.border_image = get_image_from_uri(url=value) + else: + box.border_image = value if style['visibility'] == 'hidden': images = [] From 1f702be37f59e175a5a19ddf0385d60c2d8ca7d6 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 14:47:09 +0200 Subject: [PATCH 18/30] Correctly handle border image slices larger than the image --- weasyprint/css/computed_values.py | 2 +- weasyprint/draw.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/weasyprint/css/computed_values.py b/weasyprint/css/computed_values.py index aeb11c37d..a9f833753 100644 --- a/weasyprint/css/computed_values.py +++ b/weasyprint/css/computed_values.py @@ -374,7 +374,7 @@ def border_image_slice(style, name, values): if unit is None: computed_values.append(number) else: - computed_values.append(Dimension(min(number, 100), '%')) + computed_values.append(Dimension(number, '%')) if len(computed_values) == 1: computed_values *= 4 elif len(computed_values) == 2: diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 6b4e54406..19c7ae05e 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -467,10 +467,10 @@ def draw_column_border(): def compute_slice_dimension(dimension, intrinsic): if isinstance(dimension, (int, float)): - return dimension + return min(dimension, intrinsic) else: assert dimension.unit == '%' - return dimension.value / 100 * intrinsic + return min(100, dimension.value) / 100 * intrinsic slice_top = compute_slice_dimension(image_slice[0], intrinsic_height) slice_right = compute_slice_dimension(image_slice[1], intrinsic_width) From 703cd039b0d37543571e607318ae4b93963e8efc Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 14:48:16 +0200 Subject: [PATCH 19/30] =?UTF-8?q?Don=E2=80=99t=20swap=20border=20image=20r?= =?UTF-8?q?epeat=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- weasyprint/draw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 19c7ae05e..b4bda91e4 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -477,7 +477,7 @@ def compute_slice_dimension(dimension, intrinsic): slice_bottom = compute_slice_dimension(image_slice[2], intrinsic_height) slice_left = compute_slice_dimension(image_slice[3], intrinsic_width) - style_repeat_y, style_repeat_x = box.style['border_image_repeat'] + style_repeat_x, style_repeat_y = box.style['border_image_repeat'] x, y, w, h, tl, tr, br, bl = box.rounded_border_box() px, py, pw, ph, ptl, ptr, pbr, pbl = box.rounded_padding_box() From dbcd901a0ed83388c0311236eaf41f6cb3bb37c5 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 14:49:22 +0200 Subject: [PATCH 20/30] Avoid divisions by 0 when drawing border images --- weasyprint/draw.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index b4bda91e4..80eeff12d 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -534,12 +534,18 @@ def compute_width_adjustment(dimension, original, intrinsic, def draw_border_image(x, y, width, height, slice_x, slice_y, slice_width, slice_height, repeat_x='stretch', repeat_y='stretch'): + if not all(( + intrinsic_width, intrinsic_height, + width, height, slice_width, slice_height)): + return + with stacked(stream): stream.rectangle(x, y, width, height) stream.clip() stream.end() extra_dx = 0 extra_dy = 0 + if repeat_x == 'repeat': n_repeats_x = ceil(width / slice_width) scale_x = 1 @@ -550,11 +556,12 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, # so there's one more space than repeat. extra_dx = (width - n_repeats_x * slice_width) / (n_repeats_x + 1) elif repeat_x == 'round': - n_repeats_x = round(width / slice_width) - scale_x = width / (n_repeats_x * slice_width) + n_repeats_x = max(1, round(width / slice_width)) + scale_x = width / (n_repeats_x * slice_width) else: n_repeats_x = 1 scale_x = width / slice_width + if repeat_y == 'repeat': n_repeats_y = ceil(height / slice_height) scale_y = 1 @@ -565,11 +572,12 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, # so there's one more space than repeat. extra_dy = (height - n_repeats_y * slice_height) / (n_repeats_y + 1) elif repeat_y == 'round': - n_repeats_y = round(height / slice_height) - scale_y = height / (n_repeats_y * slice_height) + n_repeats_y = max(1, round(height / slice_height)) + scale_y = height / (n_repeats_y * slice_height) else: n_repeats_y = 1 scale_y = height / slice_height + rendered_width = intrinsic_width * scale_x rendered_height = intrinsic_height * scale_y offset_x = rendered_width * slice_x / intrinsic_width @@ -591,7 +599,6 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, stream.transform(f=slice_height + extra_dy) stream.transform(e=slice_width + extra_dx) - # Top left. draw_border_image(x, y, border_left, border_top, 0, 0, slice_left, slice_top) # Top right. From 9c098d6c3f7eeec749be83e4b0538f2419912057 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 15:31:32 +0200 Subject: [PATCH 21/30] Use consistent variable name --- weasyprint/draw.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 80eeff12d..5add5efed 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -521,15 +521,15 @@ def compute_width_adjustment(dimension, original, intrinsic, # border-image-width. Also, the border image area that is used # for percentage-based border-image-width values includes any expanded # area due to border-image-outset. - width_adjs = box.style['border_image_width'] + widths = box.style['border_image_width'] border_top = compute_width_adjustment( - width_adjs[0], border_top, slice_top, h) + widths[0], border_top, slice_top, h) border_right = compute_width_adjustment( - width_adjs[1], border_right, slice_right, w) + widths[1], border_right, slice_right, w) border_bottom = compute_width_adjustment( - width_adjs[2], border_bottom, slice_bottom, h) + widths[2], border_bottom, slice_bottom, h) border_left = compute_width_adjustment( - width_adjs[3], border_left, slice_left, w) + widths[3], border_left, slice_left, w) def draw_border_image(x, y, width, height, slice_x, slice_y, slice_width, slice_height, From acd91ebb7706820db3da61b3b5ee7c90f86b9874 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 15:31:57 +0200 Subject: [PATCH 22/30] Calculate scale and repeat outside the drawing stack --- weasyprint/draw.py | 76 ++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 5add5efed..1116bad91 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -539,49 +539,47 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, width, height, slice_width, slice_height)): return + extra_dx = 0 + scale_x = 1 + if repeat_x == 'repeat': + n_repeats_x = ceil(width / slice_width) + elif repeat_x == 'space': + n_repeats_x = floor(width / slice_width) + # Space is before the first repeat and after the last, + # so there's one more space than repeat. + extra_dx = (width - n_repeats_x * slice_width) / (n_repeats_x + 1) + elif repeat_x == 'round': + n_repeats_x = max(1, round(width / slice_width)) + scale_x = width / (n_repeats_x * slice_width) + else: + n_repeats_x = 1 + scale_x = width / slice_width + + extra_dy = 0 + scale_y = 1 + if repeat_y == 'repeat': + n_repeats_y = ceil(height / slice_height) + elif repeat_y == 'space': + n_repeats_y = floor(height / slice_height) + # Space is before the first repeat and after the last, + # so there's one more space than repeat. + extra_dy = (height - n_repeats_y * slice_height) / (n_repeats_y + 1) + elif repeat_y == 'round': + n_repeats_y = max(1, round(height / slice_height)) + scale_y = height / (n_repeats_y * slice_height) + else: + n_repeats_y = 1 + scale_y = height / slice_height + + rendered_width = intrinsic_width * scale_x + rendered_height = intrinsic_height * scale_y + offset_x = rendered_width * slice_x / intrinsic_width + offset_y = rendered_height * slice_y / intrinsic_height + with stacked(stream): stream.rectangle(x, y, width, height) stream.clip() stream.end() - extra_dx = 0 - extra_dy = 0 - - if repeat_x == 'repeat': - n_repeats_x = ceil(width / slice_width) - scale_x = 1 - elif repeat_x == 'space': - n_repeats_x = floor(width / slice_width) - scale_x = 1 - # Space is before the first repeat and after the last, - # so there's one more space than repeat. - extra_dx = (width - n_repeats_x * slice_width) / (n_repeats_x + 1) - elif repeat_x == 'round': - n_repeats_x = max(1, round(width / slice_width)) - scale_x = width / (n_repeats_x * slice_width) - else: - n_repeats_x = 1 - scale_x = width / slice_width - - if repeat_y == 'repeat': - n_repeats_y = ceil(height / slice_height) - scale_y = 1 - elif repeat_y == 'space': - n_repeats_y = floor(height / slice_height) - scale_y = 1 - # Space is before the first repeat and after the last, - # so there's one more space than repeat. - extra_dy = (height - n_repeats_y * slice_height) / (n_repeats_y + 1) - elif repeat_y == 'round': - n_repeats_y = max(1, round(height / slice_height)) - scale_y = height / (n_repeats_y * slice_height) - else: - n_repeats_y = 1 - scale_y = height / slice_height - - rendered_width = intrinsic_width * scale_x - rendered_height = intrinsic_height * scale_y - offset_x = rendered_width * slice_x / intrinsic_width - offset_y = rendered_height * slice_y / intrinsic_height stream.transform(e=x - offset_x + extra_dx, f=y - offset_y + extra_dy) stream.transform(a=scale_x, d=scale_y) for xcnt in range(n_repeats_x): From 76197f5fb89d70b9a8f35a3ec179a5cedf779b3e Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 15:37:57 +0200 Subject: [PATCH 23/30] Avoid nested stacks for border images --- weasyprint/draw.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 1116bad91..94974c3c1 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -582,20 +582,20 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, stream.end() stream.transform(e=x - offset_x + extra_dx, f=y - offset_y + extra_dy) stream.transform(a=scale_x, d=scale_y) - for xcnt in range(n_repeats_x): - with stacked(stream): - for ycnt in range(n_repeats_y): - with stacked(stream): - stream.rectangle( - offset_x / scale_x, offset_y / scale_y, - slice_width, slice_height) - stream.clip() - stream.end() - img.draw( - stream, intrinsic_width, intrinsic_height, - box.style['image_rendering']) - stream.transform(f=slice_height + extra_dy) - stream.transform(e=slice_width + extra_dx) + for i in range(n_repeats_x): + for j in range(n_repeats_y): + with stacked(stream): + translate_x = i * (slice_width + extra_dx) + translate_y = j * (slice_height + extra_dy) + stream.transform(e=translate_x, f=translate_y) + stream.rectangle( + offset_x / scale_x, offset_y / scale_y, + slice_width, slice_height) + stream.clip() + stream.end() + img.draw( + stream, intrinsic_width, intrinsic_height, + box.style['image_rendering']) # Top left. draw_border_image(x, y, border_left, border_top, 0, 0, slice_left, slice_top) From d5275f32b088ad14b8c01e780f9a853f4c525a07 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 16:18:20 +0200 Subject: [PATCH 24/30] Fix corner cases for border images --- weasyprint/draw.py | 88 ++++++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 94974c3c1..c5a602758 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -533,43 +533,50 @@ def compute_width_adjustment(dimension, original, intrinsic, def draw_border_image(x, y, width, height, slice_x, slice_y, slice_width, slice_height, - repeat_x='stretch', repeat_y='stretch'): - if not all(( - intrinsic_width, intrinsic_height, - width, height, slice_width, slice_height)): - return - - extra_dx = 0 - scale_x = 1 - if repeat_x == 'repeat': - n_repeats_x = ceil(width / slice_width) - elif repeat_x == 'space': - n_repeats_x = floor(width / slice_width) - # Space is before the first repeat and after the last, - # so there's one more space than repeat. - extra_dx = (width - n_repeats_x * slice_width) / (n_repeats_x + 1) - elif repeat_x == 'round': - n_repeats_x = max(1, round(width / slice_width)) - scale_x = width / (n_repeats_x * slice_width) + repeat_x='stretch', repeat_y='stretch', + scale_x=None, scale_y=None): + if 0 in (intrinsic_width, width, slice_width): + scale_x = 0 else: - n_repeats_x = 1 - scale_x = width / slice_width - - extra_dy = 0 - scale_y = 1 - if repeat_y == 'repeat': - n_repeats_y = ceil(height / slice_height) - elif repeat_y == 'space': - n_repeats_y = floor(height / slice_height) - # Space is before the first repeat and after the last, - # so there's one more space than repeat. - extra_dy = (height - n_repeats_y * slice_height) / (n_repeats_y + 1) - elif repeat_y == 'round': - n_repeats_y = max(1, round(height / slice_height)) - scale_y = height / (n_repeats_y * slice_height) + extra_dx = 0 + if not scale_x: + scale_x = (height / slice_height) if slice_height else 1 + if repeat_x == 'repeat': + n_repeats_x = ceil(width / slice_width / scale_x) + elif repeat_x == 'space': + n_repeats_x = floor(width / slice_width / scale_x) + # Space is before the first repeat and after the last, + # so there's one more space than repeat. + extra_dx = (width - n_repeats_x * slice_width) / (n_repeats_x + 1) + elif repeat_x == 'round': + n_repeats_x = max(1, round(width / slice_width / scale_x)) + scale_x = width / (n_repeats_x * slice_width) + else: + n_repeats_x = 1 + scale_x = width / slice_width + + if 0 in (intrinsic_height, height, slice_height): + scale_y = 0 else: - n_repeats_y = 1 - scale_y = height / slice_height + extra_dy = 0 + if not scale_y: + scale_y = (width / slice_width) if slice_width else 1 + if repeat_y == 'repeat': + n_repeats_y = ceil(height / slice_height / scale_y) + elif repeat_y == 'space': + n_repeats_y = floor(height / slice_height / scale_y) + # Space is before the first repeat and after the last, + # so there's one more space than repeat. + extra_dy = (height - n_repeats_y * slice_height) / (n_repeats_y + 1) + elif repeat_y == 'round': + n_repeats_y = max(1, round(height / slice_height / scale_y)) + scale_y = height / (n_repeats_y * slice_height) + else: + n_repeats_y = 1 + scale_y = height / slice_height + + if 0 in (scale_x, scale_y): + return scale_x, scale_y rendered_width = intrinsic_width * scale_x rendered_height = intrinsic_height * scale_y @@ -597,14 +604,17 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, stream, intrinsic_width, intrinsic_height, box.style['image_rendering']) + return scale_x, scale_y + # Top left. - draw_border_image(x, y, border_left, border_top, 0, 0, slice_left, slice_top) + scale_left, scale_top = draw_border_image( + x, y, border_left, border_top, 0, 0, slice_left, slice_top) # Top right. draw_border_image( x + w - border_right, y, border_right, border_top, intrinsic_width - slice_right, 0, slice_right, slice_top) # Bottom right. - draw_border_image( + scale_right, scale_bottom = draw_border_image( x + w - border_right, y + h - border_bottom, border_right, border_bottom, intrinsic_width - slice_right, intrinsic_height - slice_bottom, slice_right, slice_bottom) @@ -647,8 +657,8 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, h - border_top - border_bottom, slice_left, slice_top, intrinsic_width - slice_left - slice_right, intrinsic_height - slice_top - slice_bottom, - repeat_x=style_repeat_x, - repeat_y=style_repeat_y) + repeat_x=style_repeat_x, repeat_y=style_repeat_y, + scale_x=scale_left or scale_right, scale_y=scale_top or scale_bottom) draw_column_border() return From 48a30e897b81ba1ed608fd434ad404ca896897d3 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 16:38:11 +0200 Subject: [PATCH 25/30] Avoid more divisions by 0 --- weasyprint/draw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index c5a602758..78b5116d6 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -540,7 +540,7 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, else: extra_dx = 0 if not scale_x: - scale_x = (height / slice_height) if slice_height else 1 + scale_x = (height / slice_height) if height and slice_height else 1 if repeat_x == 'repeat': n_repeats_x = ceil(width / slice_width / scale_x) elif repeat_x == 'space': @@ -560,7 +560,7 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, else: extra_dy = 0 if not scale_y: - scale_y = (width / slice_width) if slice_width else 1 + scale_y = (width / slice_width) if width and slice_width else 1 if repeat_y == 'repeat': n_repeats_y = ceil(height / slice_height / scale_y) elif repeat_y == 'space': From 1e20535440b594adb95fa6afb2a05ebb1bacacbf Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 16:39:05 +0200 Subject: [PATCH 26/30] =?UTF-8?q?Display=20border=20images=20even=20when?= =?UTF-8?q?=20there=E2=80=99s=20no=20border?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- weasyprint/draw.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 78b5116d6..33358d96d 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -446,13 +446,6 @@ def draw_column_border(): draw_column_border() return - widths = [getattr(box, f'border_{side}_width') for side in SIDES] - - # No border, return early. - if all(width == 0 for width in widths): - draw_column_border() - return - # If there's a border image, that takes precedence. if box.style['border_image_source'][0] != 'none' and box.border_image is not None: img = box.border_image @@ -663,6 +656,13 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, draw_column_border() return + widths = [getattr(box, f'border_{side}_width') for side in SIDES] + + # No border, return early. + if all(width == 0 for width in widths): + draw_column_border() + return + colors = [get_color(box.style, f'border_{side}_color') for side in SIDES] styles = [ colors[i].alpha and box.style[f'border_{side}_style'] From aa79825934e9640b01da3437f1a0fb2f1a19f29f Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 16:39:25 +0200 Subject: [PATCH 27/30] Fix extra space value --- weasyprint/draw.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 33358d96d..88f37326d 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -540,7 +540,9 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, n_repeats_x = floor(width / slice_width / scale_x) # Space is before the first repeat and after the last, # so there's one more space than repeat. - extra_dx = (width - n_repeats_x * slice_width) / (n_repeats_x + 1) + extra_dx = ( + (width / scale_x - n_repeats_x * slice_width) / + (n_repeats_x + 1)) elif repeat_x == 'round': n_repeats_x = max(1, round(width / slice_width / scale_x)) scale_x = width / (n_repeats_x * slice_width) @@ -560,7 +562,9 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, n_repeats_y = floor(height / slice_height / scale_y) # Space is before the first repeat and after the last, # so there's one more space than repeat. - extra_dy = (height - n_repeats_y * slice_height) / (n_repeats_y + 1) + extra_dy = ( + (height / scale_y - n_repeats_y * slice_height) / + (n_repeats_y + 1)) elif repeat_y == 'round': n_repeats_y = max(1, round(height / slice_height / scale_y)) scale_y = height / (n_repeats_y * slice_height) From 65bcfbb7b1b53b85fd17b15ed0250e17c3735c2d Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 17:42:39 +0200 Subject: [PATCH 28/30] Test border-image shorthand --- tests/css/test_expanders.py | 56 ++++++++++++++++ weasyprint/css/validation/expanders.py | 93 ++++++++++++++++---------- 2 files changed, 114 insertions(+), 35 deletions(-) diff --git a/tests/css/test_expanders.py b/tests/css/test_expanders.py index 645737491..c41ff5ecf 100644 --- a/tests/css/test_expanders.py +++ b/tests/css/test_expanders.py @@ -398,6 +398,62 @@ def test_border_radius_invalid(rule, message): assert_invalid(f'border-radius: {rule}', message) +@assert_no_logs +@pytest.mark.parametrize('rule, result', ( + ('url(border.png) 27', { + 'border_image_source': ('url', 'https://weasyprint.org/foo/border.png'), + 'border_image_slice': ((27, None),), + }), + ('url(border.png) 10 / 4 / 2 round stretch', { + 'border_image_source': ('url', 'https://weasyprint.org/foo/border.png'), + 'border_image_slice': ((10, None),), + 'border_image_width': ((4, None),), + 'border_image_outset': ((2, None),), + 'border_image_repeat': (('round', 'stretch')), + }), + ('10 // 2', { + 'border_image_slice': ((10, None),), + 'border_image_outset': ((2, None),), + }), + ('5.5%', { + 'border_image_slice': ((5.5, '%'),), + }), + ('stretch 2 url("border.png")', { + 'border_image_source': ('url', 'https://weasyprint.org/foo/border.png'), + 'border_image_slice': ((2, None),), + 'border_image_repeat': (('stretch',)), + }), + ('1/2 round', { + 'border_image_slice': ((1, None),), + 'border_image_width': ((2, None),), + 'border_image_repeat': (('round',)), + }), + ('none', { + 'border_image_source': ('none', None), + }), +)) +def test_border_image(rule, result): + assert expand_to_dict(f'border-image: {rule}') == result + + +@assert_no_logs +@pytest.mark.parametrize('rule, reason', ( + ('url(border.png) url(border.png)', 'multiple source'), + ('10 10 10 10 10', 'multiple slice'), + ('1 / 2 / 3 / 4', 'invalid'), + ('/1', 'invalid'), + ('/1', 'invalid'), + ('round round round', 'invalid'), + ('-1', 'invalid'), + ('1 repeat 2', 'multiple slice'), + ('1% // 1%', 'invalid'), + ('1 / repeat', 'invalid'), + ('', 'no value'), +)) +def test_border_image_invalid(rule, reason): + assert_invalid(f'border-image: {rule}', reason) + + @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('12px My Fancy Font, serif', { diff --git a/weasyprint/css/validation/expanders.py b/weasyprint/css/validation/expanders.py index 44ca11726..4a9de12fa 100644 --- a/weasyprint/css/validation/expanders.py +++ b/weasyprint/css/validation/expanders.py @@ -11,14 +11,14 @@ from ..utils import ( # isort:skip InvalidValues, Pending, check_var_function, get_keyword, get_single_keyword, split_on_comma) -from .properties import ( # isort:skip - background_attachment, background_image, background_position, - background_repeat, background_size, block_ellipsis, border_style, +from .properties import ( # isort:skip + background_attachment, background_image, background_position, background_repeat, + background_size, block_ellipsis, border_image_source, border_image_slice, + border_image_width, border_image_outset, border_image_repeat, border_style, border_width, box, column_count, column_width, flex_basis, flex_direction, - flex_grow_shrink, flex_wrap, font_family, font_size, font_stretch, - font_style, font_weight, line_height, list_style_image, - list_style_position, list_style_type, other_colors, overflow_wrap, - validate_non_shorthand) + flex_grow_shrink, flex_wrap, font_family, font_size, font_stretch, font_style, + font_weight, line_height, list_style_image, list_style_position, list_style_type, + other_colors, overflow_wrap, validate_non_shorthand) EXPANDERS = {} @@ -286,42 +286,65 @@ def expand_border_side(tokens, name): @expander('border-image') -@generic_expander('-outset', '-repeat', '-slice', '-source', '-width') -def expand_border_image(tokens, name): +@generic_expander('-outset', '-repeat', '-slice', '-source', '-width', + wants_base_url=True) +def expand_border_image(tokens, name, base_url): """Expand the ``border-image-*`` shorthand properties. See https://drafts.csswg.org/css-backgrounds/#the-border-image """ tokens = list(tokens) - while len(tokens) > 0: - token = tokens.pop(0) - if token.type in ('function', 'url'): - yield '-source', [token] - elif get_keyword(token) in ('stretch', 'repeat', 'round', 'space'): - yield '-repeat', [token] - elif (token.type in ('percentage', 'number') - or get_keyword(token) == 'fill'): - current = [token] - numish_suffixes = ['-slice', '-width', '-outset'] - while len(tokens) > 0 and ( - tokens[0].type in ( - 'percentage', 'number', 'literal', 'dimension') - or get_keyword(tokens[0]) in ('fill', 'auto')): - token = tokens.pop(0) - if token.type == 'literal' and token.value == '/': - if len(current) == 0: - if numish_suffixes[0] != '-width': - raise InvalidValues - else: - yield numish_suffixes[0], current - current = [] - numish_suffixes.pop(0) + while tokens: + if border_image_source(tokens[:1], base_url): + yield '-source', [tokens.pop(0)] + elif border_image_repeat(tokens[:1]): + repeats = [tokens.pop(0)] + while tokens and border_image_repeat(tokens[:1]): + repeats.append(tokens.pop(0)) + yield '-repeat', repeats + elif border_image_slice(tokens[:1]) or get_keyword(tokens[0]) == 'fill': + slices = [tokens.pop(0)] + while tokens and border_image_slice(slices + tokens[:1]): + slices.append(tokens.pop(0)) + yield '-slice', slices + if tokens and tokens[0].type == 'literal' and tokens[0].value == '/': + # slices / * + tokens.pop(0) + else: + # slices other + continue + if not tokens: + # slices / + raise InvalidValues + if border_image_width(tokens[:1]): + widths = [tokens.pop(0)] + while tokens and border_image_width(widths + tokens[:1]): + widths.append(tokens.pop(0)) + yield '-width', widths + if tokens and tokens[0].type == 'literal' and tokens[0].value == '/': + # slices / widths / slash * + tokens.pop(0) else: - current.append(token) - if len(current) == 0: + # slices / widths other + continue + elif tokens and tokens[0].type == 'literal' and tokens[0].value == '/': + # slices / / * + tokens.pop(0) + else: + # slices / other + raise InvalidValues + if not tokens: + # slices / * / + raise InvalidValues + if border_image_outset(tokens[:1]): + outsets = [tokens.pop(0)] + while tokens and border_image_outset(outsets + tokens[:1]): + outsets.append(tokens.pop(0)) + yield '-outset', outsets + else: + # slash / * / other raise InvalidValues - yield numish_suffixes[0], current else: raise InvalidValues From 886cefef8cbd65a85ce4f1177fc979ad665440d3 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 17:56:19 +0200 Subject: [PATCH 29/30] Put border image drawing in a specific function --- weasyprint/draw.py | 421 ++++++++++++++++---------------- weasyprint/layout/background.py | 3 +- 2 files changed, 213 insertions(+), 211 deletions(-) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 88f37326d..dbacd4fa5 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -448,215 +448,7 @@ def draw_column_border(): # If there's a border image, that takes precedence. if box.style['border_image_source'][0] != 'none' and box.border_image is not None: - img = box.border_image - width, height, ratio = img.get_intrinsic_size( - box.style['image_resolution'], box.style['font_size']) - intrinsic_width, intrinsic_height = replaced.default_image_sizing( - width, height, ratio, specified_width=None, specified_height=None, - default_width=box.border_width(), default_height=box.border_height()) - - image_slice = box.style['border_image_slice'][:4] - should_fill = box.style['border_image_slice'][4] - - def compute_slice_dimension(dimension, intrinsic): - if isinstance(dimension, (int, float)): - return min(dimension, intrinsic) - else: - assert dimension.unit == '%' - return min(100, dimension.value) / 100 * intrinsic - - slice_top = compute_slice_dimension(image_slice[0], intrinsic_height) - slice_right = compute_slice_dimension(image_slice[1], intrinsic_width) - slice_bottom = compute_slice_dimension(image_slice[2], intrinsic_height) - slice_left = compute_slice_dimension(image_slice[3], intrinsic_width) - - style_repeat_x, style_repeat_y = box.style['border_image_repeat'] - - x, y, w, h, tl, tr, br, bl = box.rounded_border_box() - px, py, pw, ph, ptl, ptr, pbr, pbl = box.rounded_padding_box() - border_left = px - x - border_top = py - y - border_right = w - pw - border_left - border_bottom = h - ph - border_top - - def compute_outset_dimension(dimension, from_border): - if dimension.unit is None: - return dimension.value * from_border - else: - assert dimension.unit == 'px' - return dimension.value - - outsets = box.style['border_image_outset'] - outset_top = compute_outset_dimension(outsets[0], border_top) - outset_right = compute_outset_dimension(outsets[1], border_right) - outset_bottom = compute_outset_dimension(outsets[2], border_bottom) - outset_left = compute_outset_dimension(outsets[3], border_left) - - x -= outset_left - y -= outset_top - w += outset_left + outset_right - h += outset_top + outset_bottom - - def compute_width_adjustment(dimension, original, intrinsic, - area_dimension): - if dimension == 'auto': - return intrinsic - elif isinstance(dimension, (int, float)): - return dimension * original - elif dimension.unit == '%': - return dimension.value / 100 * area_dimension - else: - assert dimension.unit == 'px' - return dimension.value - - # We make adjustments to the border_* variables after handling outsets - # because numerical outsets are relative to border-width, not - # border-image-width. Also, the border image area that is used - # for percentage-based border-image-width values includes any expanded - # area due to border-image-outset. - widths = box.style['border_image_width'] - border_top = compute_width_adjustment( - widths[0], border_top, slice_top, h) - border_right = compute_width_adjustment( - widths[1], border_right, slice_right, w) - border_bottom = compute_width_adjustment( - widths[2], border_bottom, slice_bottom, h) - border_left = compute_width_adjustment( - widths[3], border_left, slice_left, w) - - def draw_border_image(x, y, width, height, slice_x, slice_y, - slice_width, slice_height, - repeat_x='stretch', repeat_y='stretch', - scale_x=None, scale_y=None): - if 0 in (intrinsic_width, width, slice_width): - scale_x = 0 - else: - extra_dx = 0 - if not scale_x: - scale_x = (height / slice_height) if height and slice_height else 1 - if repeat_x == 'repeat': - n_repeats_x = ceil(width / slice_width / scale_x) - elif repeat_x == 'space': - n_repeats_x = floor(width / slice_width / scale_x) - # Space is before the first repeat and after the last, - # so there's one more space than repeat. - extra_dx = ( - (width / scale_x - n_repeats_x * slice_width) / - (n_repeats_x + 1)) - elif repeat_x == 'round': - n_repeats_x = max(1, round(width / slice_width / scale_x)) - scale_x = width / (n_repeats_x * slice_width) - else: - n_repeats_x = 1 - scale_x = width / slice_width - - if 0 in (intrinsic_height, height, slice_height): - scale_y = 0 - else: - extra_dy = 0 - if not scale_y: - scale_y = (width / slice_width) if width and slice_width else 1 - if repeat_y == 'repeat': - n_repeats_y = ceil(height / slice_height / scale_y) - elif repeat_y == 'space': - n_repeats_y = floor(height / slice_height / scale_y) - # Space is before the first repeat and after the last, - # so there's one more space than repeat. - extra_dy = ( - (height / scale_y - n_repeats_y * slice_height) / - (n_repeats_y + 1)) - elif repeat_y == 'round': - n_repeats_y = max(1, round(height / slice_height / scale_y)) - scale_y = height / (n_repeats_y * slice_height) - else: - n_repeats_y = 1 - scale_y = height / slice_height - - if 0 in (scale_x, scale_y): - return scale_x, scale_y - - rendered_width = intrinsic_width * scale_x - rendered_height = intrinsic_height * scale_y - offset_x = rendered_width * slice_x / intrinsic_width - offset_y = rendered_height * slice_y / intrinsic_height - - with stacked(stream): - stream.rectangle(x, y, width, height) - stream.clip() - stream.end() - stream.transform(e=x - offset_x + extra_dx, f=y - offset_y + extra_dy) - stream.transform(a=scale_x, d=scale_y) - for i in range(n_repeats_x): - for j in range(n_repeats_y): - with stacked(stream): - translate_x = i * (slice_width + extra_dx) - translate_y = j * (slice_height + extra_dy) - stream.transform(e=translate_x, f=translate_y) - stream.rectangle( - offset_x / scale_x, offset_y / scale_y, - slice_width, slice_height) - stream.clip() - stream.end() - img.draw( - stream, intrinsic_width, intrinsic_height, - box.style['image_rendering']) - - return scale_x, scale_y - - # Top left. - scale_left, scale_top = draw_border_image( - x, y, border_left, border_top, 0, 0, slice_left, slice_top) - # Top right. - draw_border_image( - x + w - border_right, y, border_right, border_top, - intrinsic_width - slice_right, 0, slice_right, slice_top) - # Bottom right. - scale_right, scale_bottom = draw_border_image( - x + w - border_right, y + h - border_bottom, border_right, border_bottom, - intrinsic_width - slice_right, intrinsic_height - slice_bottom, - slice_right, slice_bottom) - # Bottom left. - draw_border_image( - x, y + h - border_bottom, border_left, border_bottom, - 0, intrinsic_height - slice_bottom, slice_left, slice_bottom) - if slice_left + slice_right < intrinsic_width: - # Top middle. - draw_border_image( - x + border_left, y, w - border_left - border_right, border_top, - slice_left, 0, intrinsic_width - slice_left - slice_right, - slice_top, repeat_x=style_repeat_x) - # Bottom middle. - draw_border_image( - x + border_left, y + h - border_bottom, - w - border_left - border_right, border_bottom, - slice_left, intrinsic_height - slice_bottom, - intrinsic_width - slice_left - slice_right, slice_bottom, - repeat_x=style_repeat_x) - if slice_top + slice_bottom < intrinsic_height: - # Right middle. - draw_border_image( - x + w - border_right, y + border_top, - border_right, h - border_top - border_bottom, - intrinsic_width - slice_right, slice_top, - slice_right, intrinsic_height - slice_top - slice_bottom, - repeat_y=style_repeat_y) - # Left middle. - draw_border_image( - x, y + border_top, border_left, h - border_top - border_bottom, - 0, slice_top, slice_left, - intrinsic_height - slice_top - slice_bottom, - repeat_y=style_repeat_y) - if (should_fill and slice_left + slice_right < intrinsic_width - and slice_top + slice_bottom < intrinsic_height): - # Fill middle. - draw_border_image( - x + border_left, y + border_top, w - border_left - border_right, - h - border_top - border_bottom, slice_left, slice_top, - intrinsic_width - slice_left - slice_right, - intrinsic_height - slice_top - slice_bottom, - repeat_x=style_repeat_x, repeat_y=style_repeat_y, - scale_x=scale_left or scale_right, scale_y=scale_top or scale_bottom) - + draw_border_image(box, stream) draw_column_border() return @@ -697,6 +489,217 @@ def draw_border_image(x, y, width, height, slice_x, slice_y, draw_column_border() +def draw_border_image(box, stream): + """Draw ``box`` border image on ``stream``.""" + # See https://drafts.csswg.org/css-backgrounds-3/#border-images + image = box.border_image + width, height, ratio = image.get_intrinsic_size( + box.style['image_resolution'], box.style['font_size']) + intrinsic_width, intrinsic_height = replaced.default_image_sizing( + width, height, ratio, specified_width=None, specified_height=None, + default_width=box.border_width(), default_height=box.border_height()) + + image_slice = box.style['border_image_slice'][:4] + should_fill = box.style['border_image_slice'][4] + + def compute_slice_dimension(dimension, intrinsic): + if isinstance(dimension, (int, float)): + return min(dimension, intrinsic) + else: + assert dimension.unit == '%' + return min(100, dimension.value) / 100 * intrinsic + + slice_top = compute_slice_dimension(image_slice[0], intrinsic_height) + slice_right = compute_slice_dimension(image_slice[1], intrinsic_width) + slice_bottom = compute_slice_dimension(image_slice[2], intrinsic_height) + slice_left = compute_slice_dimension(image_slice[3], intrinsic_width) + + style_repeat_x, style_repeat_y = box.style['border_image_repeat'] + + x, y, w, h, tl, tr, br, bl = box.rounded_border_box() + px, py, pw, ph, ptl, ptr, pbr, pbl = box.rounded_padding_box() + border_left = px - x + border_top = py - y + border_right = w - pw - border_left + border_bottom = h - ph - border_top + + def compute_outset_dimension(dimension, from_border): + if dimension.unit is None: + return dimension.value * from_border + else: + assert dimension.unit == 'px' + return dimension.value + + outsets = box.style['border_image_outset'] + outset_top = compute_outset_dimension(outsets[0], border_top) + outset_right = compute_outset_dimension(outsets[1], border_right) + outset_bottom = compute_outset_dimension(outsets[2], border_bottom) + outset_left = compute_outset_dimension(outsets[3], border_left) + + x -= outset_left + y -= outset_top + w += outset_left + outset_right + h += outset_top + outset_bottom + + def compute_width_adjustment(dimension, original, intrinsic, + area_dimension): + if dimension == 'auto': + return intrinsic + elif isinstance(dimension, (int, float)): + return dimension * original + elif dimension.unit == '%': + return dimension.value / 100 * area_dimension + else: + assert dimension.unit == 'px' + return dimension.value + + # We make adjustments to the border_* variables after handling outsets + # because numerical outsets are relative to border-width, not + # border-image-width. Also, the border image area that is used + # for percentage-based border-image-width values includes any expanded + # area due to border-image-outset. + widths = box.style['border_image_width'] + border_top = compute_width_adjustment( + widths[0], border_top, slice_top, h) + border_right = compute_width_adjustment( + widths[1], border_right, slice_right, w) + border_bottom = compute_width_adjustment( + widths[2], border_bottom, slice_bottom, h) + border_left = compute_width_adjustment( + widths[3], border_left, slice_left, w) + + def draw_border_image(x, y, width, height, slice_x, slice_y, + slice_width, slice_height, + repeat_x='stretch', repeat_y='stretch', + scale_x=None, scale_y=None): + if 0 in (intrinsic_width, width, slice_width): + scale_x = 0 + else: + extra_dx = 0 + if not scale_x: + scale_x = (height / slice_height) if height and slice_height else 1 + if repeat_x == 'repeat': + n_repeats_x = ceil(width / slice_width / scale_x) + elif repeat_x == 'space': + n_repeats_x = floor(width / slice_width / scale_x) + # Space is before the first repeat and after the last, + # so there's one more space than repeat. + extra_dx = ( + (width / scale_x - n_repeats_x * slice_width) / (n_repeats_x + 1)) + elif repeat_x == 'round': + n_repeats_x = max(1, round(width / slice_width / scale_x)) + scale_x = width / (n_repeats_x * slice_width) + else: + n_repeats_x = 1 + scale_x = width / slice_width + + if 0 in (intrinsic_height, height, slice_height): + scale_y = 0 + else: + extra_dy = 0 + if not scale_y: + scale_y = (width / slice_width) if width and slice_width else 1 + if repeat_y == 'repeat': + n_repeats_y = ceil(height / slice_height / scale_y) + elif repeat_y == 'space': + n_repeats_y = floor(height / slice_height / scale_y) + # Space is before the first repeat and after the last, + # so there's one more space than repeat. + extra_dy = ( + (height / scale_y - n_repeats_y * slice_height) / (n_repeats_y + 1)) + elif repeat_y == 'round': + n_repeats_y = max(1, round(height / slice_height / scale_y)) + scale_y = height / (n_repeats_y * slice_height) + else: + n_repeats_y = 1 + scale_y = height / slice_height + + if 0 in (scale_x, scale_y): + return scale_x, scale_y + + rendered_width = intrinsic_width * scale_x + rendered_height = intrinsic_height * scale_y + offset_x = rendered_width * slice_x / intrinsic_width + offset_y = rendered_height * slice_y / intrinsic_height + + with stacked(stream): + stream.rectangle(x, y, width, height) + stream.clip() + stream.end() + stream.transform(e=x - offset_x + extra_dx, f=y - offset_y + extra_dy) + stream.transform(a=scale_x, d=scale_y) + for i in range(n_repeats_x): + for j in range(n_repeats_y): + with stacked(stream): + translate_x = i * (slice_width + extra_dx) + translate_y = j * (slice_height + extra_dy) + stream.transform(e=translate_x, f=translate_y) + stream.rectangle( + offset_x / scale_x, offset_y / scale_y, + slice_width, slice_height) + stream.clip() + stream.end() + image.draw( + stream, intrinsic_width, intrinsic_height, + box.style['image_rendering']) + + return scale_x, scale_y + + # Top left. + scale_left, scale_top = draw_border_image( + x, y, border_left, border_top, 0, 0, slice_left, slice_top) + # Top right. + draw_border_image( + x + w - border_right, y, border_right, border_top, + intrinsic_width - slice_right, 0, slice_right, slice_top) + # Bottom right. + scale_right, scale_bottom = draw_border_image( + x + w - border_right, y + h - border_bottom, border_right, border_bottom, + intrinsic_width - slice_right, intrinsic_height - slice_bottom, + slice_right, slice_bottom) + # Bottom left. + draw_border_image( + x, y + h - border_bottom, border_left, border_bottom, + 0, intrinsic_height - slice_bottom, slice_left, slice_bottom) + if slice_left + slice_right < intrinsic_width: + # Top middle. + draw_border_image( + x + border_left, y, w - border_left - border_right, border_top, + slice_left, 0, intrinsic_width - slice_left - slice_right, + slice_top, repeat_x=style_repeat_x) + # Bottom middle. + draw_border_image( + x + border_left, y + h - border_bottom, + w - border_left - border_right, border_bottom, + slice_left, intrinsic_height - slice_bottom, + intrinsic_width - slice_left - slice_right, slice_bottom, + repeat_x=style_repeat_x) + if slice_top + slice_bottom < intrinsic_height: + # Right middle. + draw_border_image( + x + w - border_right, y + border_top, + border_right, h - border_top - border_bottom, + intrinsic_width - slice_right, slice_top, + slice_right, intrinsic_height - slice_top - slice_bottom, + repeat_y=style_repeat_y) + # Left middle. + draw_border_image( + x, y + border_top, border_left, h - border_top - border_bottom, + 0, slice_top, slice_left, + intrinsic_height - slice_top - slice_bottom, + repeat_y=style_repeat_y) + if (should_fill and slice_left + slice_right < intrinsic_width + and slice_top + slice_bottom < intrinsic_height): + # Fill middle. + draw_border_image( + x + border_left, y + border_top, w - border_left - border_right, + h - border_top - border_bottom, slice_left, slice_top, + intrinsic_width - slice_left - slice_right, + intrinsic_height - slice_top - slice_bottom, + repeat_x=style_repeat_x, repeat_y=style_repeat_y, + scale_x=scale_left or scale_right, scale_y=scale_top or scale_bottom) + + def clip_border_segment(stream, style, width, side, border_box, border_widths=None, radii=None): """Clip one segment of box border. diff --git a/weasyprint/layout/background.py b/weasyprint/layout/background.py index dd997c54e..09599165a 100644 --- a/weasyprint/layout/background.py +++ b/weasyprint/layout/background.py @@ -49,8 +49,7 @@ def layout_box_backgrounds(page, box, get_image_from_uri, layout_children=True, # This is for the border image, not the background, but this is a # convenient place to get the image. - if (style['border_image_source'] - and style['border_image_source'][0] != 'none'): + if style['border_image_source'][0] != 'none': type_, value = style['border_image_source'] if type_ == 'url': box.border_image = get_image_from_uri(url=value) From c8633734213d69bb4610999a8d87200b9bff3b02 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Apr 2024 17:59:49 +0200 Subject: [PATCH 30/30] Update documentation about border images --- docs/api_reference.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 4d3a43423..6826269d0 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -558,10 +558,10 @@ background layers per box), including the ``background``, ``background-color``, WeasyPrint also supports the `rounded corners part`_ of this module, including the ``border-radius`` property. -WeasyPrint does **not** support the `border images part`_ of this module, -including the ``border-image``, ``border-image-source``, -``border-image-slice``, ``border-image-width``, ``border-image-outset`` and -``border-image-repeat`` properties. +WeasyPrint also supports the `border images part`_ of this module, including the +``border-image``, ``border-image-source``, ``border-image-slice``, +``border-image-width``, ``border-image-outset`` and ``border-image-repeat`` +properties. WeasyPrint does **not** support the `box shadow part`_ of this module, including the ``box-shadow`` property. This feature has been implemented in a