From 4ce7e560852d9e62866df63ea38059396b7cf9ba Mon Sep 17 00:00:00 2001 From: Xavid Pretzer Date: Sun, 28 Apr 2024 00:34:01 -0400 Subject: [PATCH 1/4] Implement mask-border-* properties. --- tests/draw/test_box.py | 62 ++++++++++++++++++ tests/resources/mask.svg | 44 +++++++++++++ weasyprint/css/computed_values.py | 4 ++ weasyprint/css/properties.py | 11 ++++ weasyprint/css/validation/properties.py | 21 +++++-- weasyprint/draw.py | 84 +++++++++++++++++-------- weasyprint/layout/background.py | 7 +++ weasyprint/pdf/stream.py | 4 +- 8 files changed, 204 insertions(+), 33 deletions(-) create mode 100644 tests/resources/mask.svg diff --git a/tests/draw/test_box.py b/tests/draw/test_box.py index 168aeb19..dc070374 100644 --- a/tests/draw/test_box.py +++ b/tests/draw/test_box.py @@ -576,3 +576,65 @@ def test_border_image_gradient(assert_pixels):
''') + + +@assert_no_logs +def test_mask_border(assert_pixels): + assert_pixels(''' + __________ + __RR__RRR_ + _R______R_ + _R______R_ + _s______R_ + _s______R_ + _R______R_ + _R______R_ + __RRRRRRR_ + __________ + ''', ''' + +
+ ''') + + +@assert_no_logs +def test_mask_border_fill(assert_pixels): + assert_pixels(''' + __________ + __RR__RRR_ + _RRRRRRRR_ + _RRRRRRRR_ + _sRR__RRR_ + _sRR__RRR_ + _RRRRRRRR_ + _RRRRRRRR_ + __RRRRRRR_ + __________ + ''', ''' + +
+ ''') diff --git a/tests/resources/mask.svg b/tests/resources/mask.svg new file mode 100644 index 00000000..f759c968 --- /dev/null +++ b/tests/resources/mask.svg @@ -0,0 +1,44 @@ + + + + + + + diff --git a/weasyprint/css/computed_values.py b/weasyprint/css/computed_values.py index 4e10f942..d02fbf3e 100644 --- a/weasyprint/css/computed_values.py +++ b/weasyprint/css/computed_values.py @@ -362,6 +362,7 @@ def border_width(style, name, value): @register_computer('border-image-slice') +@register_computer('mask-border-slice') def border_image_slice(style, name, values): """Compute the ``border-image-slice`` property.""" computed_values = [] @@ -385,6 +386,7 @@ def border_image_slice(style, name, values): @register_computer('border-image-width') +@register_computer('mask-border-width') def border_image_width(style, name, values): """Compute the ``border-image-width`` property.""" computed_values = [] @@ -404,6 +406,7 @@ def border_image_width(style, name, values): @register_computer('border-image-outset') +@register_computer('mask-border-outset') def border_image_outset(style, name, values): """Compute the ``border-image-outset`` property.""" computed_values = [ @@ -419,6 +422,7 @@ def border_image_outset(style, name, values): @register_computer('border-image-repeat') +@register_computer('mask-border-repeat') def border_image_repeat(style, name, values): """Compute the ``border-image-repeat`` property.""" return (values * 2) if len(values) == 1 else values diff --git a/weasyprint/css/properties.py b/weasyprint/css/properties.py index 450b7235..365b5cd1 100644 --- a/weasyprint/css/properties.py +++ b/weasyprint/css/properties.py @@ -77,6 +77,17 @@ INITIAL_VALUES = { Dimension(0, None), Dimension(0, None), Dimension(0, None), Dimension(0, None)), 'border_image_repeat': ('stretch', 'stretch'), + 'mask_border_source': ('none', None), + 'mask_border_slice': ( + Dimension(100, '%'), Dimension(100, '%'), + Dimension(100, '%'), Dimension(100, '%'), + None), + 'mask_border_width': ('auto', 'auto', 'auto', 'auto'), + 'mask_border_outset': ( + Dimension(0, None), Dimension(0, None), + Dimension(0, None), Dimension(0, None)), + 'mask_border_repeat': ('stretch', 'stretch'), + 'mask_border_mode': 'alpha', # 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 57ae504c..27e64568 100644 --- a/weasyprint/css/validation/properties.py +++ b/weasyprint/css/validation/properties.py @@ -421,7 +421,8 @@ def border_width(token): return keyword -@property(wants_base_url=True) +@property('border-image-source', wants_base_url=True) +@property('mask-border-source', wants_base_url=True) @single_token def border_image_source(token, base_url): if get_keyword(token) == 'none': @@ -429,7 +430,8 @@ def border_image_source(token, base_url): return get_image(token, base_url) -@property() +@property('border-image-slice') +@property('mask-border-slice') def border_image_slice(tokens): values = [] fill = False @@ -449,7 +451,8 @@ def border_image_slice(tokens): return tuple(values) -@property() +@property('border-image-width') +@property('mask-border-width') def border_image_width(tokens): values = [] for token in tokens: @@ -467,7 +470,8 @@ def border_image_width(tokens): return tuple(values) -@property() +@property('border-image-outset') +@property('mask-border-outset') def border_image_outset(tokens): values = [] for token in tokens: @@ -483,7 +487,8 @@ def border_image_outset(tokens): return tuple(values) -@property() +@property('border-image-repeat') +@property('mask-border-repeat') def border_image_repeat(tokens): if 1 <= len(tokens) <= 2: keywords = tuple(get_keyword(token) for token in tokens) @@ -491,6 +496,12 @@ def border_image_repeat(tokens): return keywords +@property() +@single_keyword +def mask_border_mode(keyword): + return keyword in ('luminance', 'alpha') + + @property(unstable=True) @single_token def column_width(token): diff --git a/weasyprint/draw.py b/weasyprint/draw.py index f619ed36..8a15cdd1 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -116,6 +116,7 @@ def draw_stacking_context(stream, stacking_context): if isinstance(box, (boxes.BlockBox, boxes.MarginBox, boxes.InlineBlockBox, boxes.TableCellBox, boxes.FlexContainerBox, boxes.ReplacedBox)): + maybe_set_mask_border(box, stream) # The canvas background was removed by layout_backgrounds draw_background(stream, box.background) draw_border(stream, box) @@ -137,6 +138,8 @@ def draw_stacking_context(stream, stacking_context): # Point 4 for block in stacking_context.block_level_boxes: + maybe_set_mask_border(block, stream) + if isinstance(block, boxes.TableBox): draw_table(stream, block) else: @@ -479,18 +482,18 @@ def draw_border(stream, box): stream, box, style, styled_color(style, color, side)) -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 +def draw_border_image_impl(box, stream, image, slice_property, + repeats, outsets, width_property): + """Draw ``image`` as a border image for ``box`` on ``stream`` as specified.""" + # Shared by border-image-* and mask-border-* 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] + image_slice = slice_property[:4] + should_fill = slice_property[4] def compute_slice_dimension(dimension, intrinsic): if isinstance(dimension, (int, float)): @@ -504,7 +507,7 @@ def draw_border_image(box, stream): 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'] + style_repeat_x, style_repeat_y = repeats x, y, w, h, tl, tr, br, bl = box.rounded_border_box() px, py, pw, ph, ptl, ptr, pbr, pbl = box.rounded_padding_box() @@ -520,7 +523,6 @@ def draw_border_image(box, stream): 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) @@ -548,20 +550,19 @@ def draw_border_image(box, stream): # 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) + width_property[0], border_top, slice_top, h) border_right = compute_width_adjustment( - widths[1], border_right, slice_right, w) + width_property[1], border_right, slice_right, w) border_bottom = compute_width_adjustment( - widths[2], border_bottom, slice_bottom, h) + width_property[2], border_bottom, slice_bottom, h) border_left = compute_width_adjustment( - widths[3], border_left, slice_left, w) + width_property[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): + def draw_border_image_region(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: @@ -636,29 +637,29 @@ def draw_border_image(box, stream): return scale_x, scale_y # Top left. - scale_left, scale_top = draw_border_image( + scale_left, scale_top = draw_border_image_region( x, y, border_left, border_top, 0, 0, slice_left, slice_top) # Top right. - draw_border_image( + draw_border_image_region( 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( + scale_right, scale_bottom = draw_border_image_region( 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( + draw_border_image_region( 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( + draw_border_image_region( 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( + draw_border_image_region( x + border_left, y + h - border_bottom, w - border_left - border_right, border_bottom, slice_left, intrinsic_height - slice_bottom, @@ -666,14 +667,14 @@ def draw_border_image(box, stream): repeat_x=style_repeat_x) if slice_top + slice_bottom < intrinsic_height: # Right middle. - draw_border_image( + draw_border_image_region( 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( + draw_border_image_region( x, y + border_top, border_left, h - border_top - border_bottom, 0, slice_top, slice_left, intrinsic_height - slice_top - slice_bottom, @@ -681,7 +682,7 @@ def draw_border_image(box, stream): if (should_fill and slice_left + slice_right < intrinsic_width and slice_top + slice_bottom < intrinsic_height): # Fill middle. - draw_border_image( + draw_border_image_region( 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, @@ -690,6 +691,36 @@ def draw_border_image(box, stream): scale_x=scale_left or scale_right, scale_y=scale_top or scale_bottom) +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 + + draw_border_image_impl( + box, stream, image, box.style['border_image_slice'], + box.style['border_image_repeat'], box.style['border_image_outset'], + box.style['border_image_width']) + + +def maybe_set_mask_border(box, stream): + """Set ``box`` mask border as alpha state on ``stream``.""" + # See https://drafts.fxtf.org/css-masking/#the-mask-border + if (box.style['mask_border_source'][0] != 'none' + and box.mask_border_image is not None): + x, y, w, h, tl, tr, br, bl = box.rounded_border_box() + matrix = Matrix(e=x, f=y) + matrix @= stream.ctm + mask_stream = stream.set_alpha_state( + x, y, w, h, box.style['mask_border_mode'] != 'luminosity') + + image = box.mask_border_image + + draw_border_image_impl( + box, mask_stream, image, box.style['mask_border_slice'], + box.style['mask_border_repeat'], box.style['mask_border_outset'], + box.style['mask_border_width']) + + def clip_border_segment(stream, style, width, side, border_box, border_widths=None, radii=None): """Clip one segment of box border. @@ -1205,6 +1236,7 @@ def draw_inline_level(stream, page, box, offset_x=0, text_overflow='clip', boxes.InlineBlockBox, boxes.InlineFlexBox, boxes.InlineGridBox)) draw_stacking_context(stream, stacking_context) else: + maybe_set_mask_border(box, stream) draw_background(stream, box.background) draw_border(stream, box) if isinstance(box, (boxes.InlineBox, boxes.LineBox)): diff --git a/weasyprint/layout/background.py b/weasyprint/layout/background.py index 09599165..3ca357ef 100644 --- a/weasyprint/layout/background.py +++ b/weasyprint/layout/background.py @@ -56,6 +56,13 @@ def layout_box_backgrounds(page, box, get_image_from_uri, layout_children=True, else: box.border_image = value + if style['mask_border_source'][0] != 'none': + type_, value = style['mask_border_source'] + if type_ == 'url': + box.mask_border_image = get_image_from_uri(url=value) + else: + box.mask_border_image = value + if style['visibility'] == 'hidden': images = [] color = parse_color('transparent') diff --git a/weasyprint/pdf/stream.py b/weasyprint/pdf/stream.py index a1d7abb0..72b8d416 100644 --- a/weasyprint/pdf/stream.py +++ b/weasyprint/pdf/stream.py @@ -112,13 +112,13 @@ class Stream(pydyf.Stream): self._states[key] = pydyf.Dictionary({'ca': alpha}) super().set_state(key) - def set_alpha_state(self, x, y, width, height): + def set_alpha_state(self, x, y, width, height, is_alpha_based=False): alpha_stream = self.add_group(x, y, width, height) alpha_state = pydyf.Dictionary({ 'Type': '/ExtGState', 'SMask': pydyf.Dictionary({ 'Type': '/Mask', - 'S': '/Luminosity', + 'S': '/Alpha' if is_alpha_based else '/Luminosity', 'G': alpha_stream, }), 'ca': 1, From 03781c3f500f7514f5aa70085cc434870511b8e8 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Mon, 27 May 2024 10:16:27 +0200 Subject: [PATCH 2/4] Support mask-border shorthand property. --- tests/css/test_expanders.py | 68 +++++++++++++++++++++++++ weasyprint/css/validation/expanders.py | 70 +++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 2 deletions(-) diff --git a/tests/css/test_expanders.py b/tests/css/test_expanders.py index e5654e61..1c2b830a 100644 --- a/tests/css/test_expanders.py +++ b/tests/css/test_expanders.py @@ -454,6 +454,74 @@ def test_border_image_invalid(rule, reason): assert_invalid(f'border-image: {rule}', reason) +@assert_no_logs +@pytest.mark.parametrize('rule, result', ( + ('url(border.png) 27', { + 'mask_border_source': ('url', 'https://weasyprint.org/foo/border.png'), + 'mask_border_slice': ((27, None),), + }), + ('url(border.png) 10 / 4 / 2 round stretch', { + 'mask_border_source': ('url', 'https://weasyprint.org/foo/border.png'), + 'mask_border_slice': ((10, None),), + 'mask_border_width': ((4, None),), + 'mask_border_outset': ((2, None),), + 'mask_border_repeat': (('round', 'stretch')), + }), + ('10 // 2', { + 'mask_border_slice': ((10, None),), + 'mask_border_outset': ((2, None),), + }), + ('5.5%', { + 'mask_border_slice': ((5.5, '%'),), + }), + ('stretch 2 url("border.png")', { + 'mask_border_source': ('url', 'https://weasyprint.org/foo/border.png'), + 'mask_border_slice': ((2, None),), + 'mask_border_repeat': (('stretch',)), + }), + ('1/2 round', { + 'mask_border_slice': ((1, None),), + 'mask_border_width': ((2, None),), + 'mask_border_repeat': (('round',)), + }), + ('none', { + 'mask_border_source': ('none', None), + }), + ('url(border.png) 27 alpha', { + 'mask_border_source': ('url', 'https://weasyprint.org/foo/border.png'), + 'mask_border_slice': ((27, None),), + 'mask_border_mode': 'alpha', + }), + ('url(border.png) 27 luminance', { + 'mask_border_source': ('url', 'https://weasyprint.org/foo/border.png'), + 'mask_border_slice': ((27, None),), + 'mask_border_mode': 'luminance', + }), +)) +def test_mask_border(rule, result): + assert expand_to_dict(f'mask-border: {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'), + ('alpha alpha', 'multiple mode'), + ('alpha luminance', 'multiple mode'), +)) +def test_mask_border_invalid(rule, reason): + assert_invalid(f'mask-border: {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 51794f62..e6358571 100644 --- a/weasyprint/css/validation/expanders.py +++ b/weasyprint/css/validation/expanders.py @@ -18,8 +18,8 @@ from .properties import ( # isort:skip 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, gap, grid_line, grid_template, line_height, - list_style_image, list_style_position, list_style_type, other_colors, - overflow_wrap, validate_non_shorthand) + list_style_image, list_style_position, list_style_type, mask_border_mode, + other_colors, overflow_wrap, validate_non_shorthand) EXPANDERS = {} @@ -350,6 +350,72 @@ def expand_border_image(tokens, name, base_url): raise InvalidValues +@expander('mask-border') +@generic_expander('-outset', '-repeat', '-slice', '-source', '-width', '-mode', + wants_base_url=True) +def expand_mask_border(tokens, name, base_url): + """Expand the ``mask-border-*`` shorthand properties. + + See https://drafts.fxtf.org/css-masking/#the-mask-border + + """ + tokens = list(tokens) + while tokens: + if border_image_source(tokens[:1], base_url): + yield '-source', [tokens.pop(0)] + elif mask_border_mode(tokens[:1]): + yield '-mode', [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: + # 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 + else: + raise InvalidValues + + @expander('background') def expand_background(tokens, name, base_url): """Expand the ``background`` shorthand property. From a846e0062ff1e12cf8ee9471b03e3632fa8d52f3 Mon Sep 17 00:00:00 2001 From: Xavid Pretzer Date: Sun, 28 Apr 2024 11:13:17 -0400 Subject: [PATCH 3/4] Add validation tests for mask-border-* properties. --- tests/css/test_validation.py | 129 +++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/tests/css/test_validation.py b/tests/css/test_validation.py index cf01bba2..c91adac6 100644 --- a/tests/css/test_validation.py +++ b/tests/css/test_validation.py @@ -470,6 +470,135 @@ def test_border_image_repeat_invalid(rule): assert_invalid(f'border-image-repeat: {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_mask_border_slice(rule, value): + assert get_value(f'mask-border-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_mask_border_slice_invalid(rule): + assert_invalid(f'mask-border-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_mask_border_width(rule, value): + assert get_value(f'mask-border-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_mask_border_width_invalid(rule): + assert_invalid(f'mask-border-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_mask_border_outset(rule, value): + assert get_value(f'mask-border-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_mask_border_outset_invalid(rule): + assert_invalid(f'mask-border-outset: {rule}') + + +@assert_no_logs +@pytest.mark.parametrize('rule, value', ( + ('stretch', ('stretch',)), + ('repeat repeat', ('repeat', 'repeat')), + ('round space', ('round', 'space')), +)) +def test_mask_border_repeat(rule, value): + assert get_value(f'mask-border-repeat: {rule}') == value + + +@assert_no_logs +@pytest.mark.parametrize('rule', ( + 'none', + 'test', + 'round round round', + 'stretch space round', + 'repeat test', +)) +def test_mask_border_repeat_invalid(rule): + assert_invalid(f'mask-border-repeat: {rule}') + + +@assert_no_logs +@pytest.mark.parametrize('rule, value', ( + ('alpha', 'alpha'), + ('luminance', 'luminance'), + ('alpha ', 'alpha'), +)) +def test_mask_border_mode(rule, value): + assert get_value(f'mask-border-mode: {rule}') == value + + +@assert_no_logs +@pytest.mark.parametrize('rule', ( + 'none', + 'test', + 'alpha alpha', + 'alpha luminance', +)) +def test_mask_border_mode_invalid(rule): + assert_invalid(f'mask-border-mode: {rule}') + + @assert_no_logs @pytest.mark.parametrize('rule, value', ( ('test content(text)', (('test', (('content()', 'text'),)),)), From 03b8f1acc32ae660bf7d62790df42aa6cd975735 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sat, 8 Jun 2024 22:57:15 +0200 Subject: [PATCH 4/4] Slightly simplify border masks --- tests/resources/mask.svg | 46 ++----------------- weasyprint/draw.py | 98 +++++++++++++++++----------------------- weasyprint/pdf/stream.py | 4 +- 3 files changed, 47 insertions(+), 101 deletions(-) diff --git a/tests/resources/mask.svg b/tests/resources/mask.svg index f759c968..7c3469e7 100644 --- a/tests/resources/mask.svg +++ b/tests/resources/mask.svg @@ -1,44 +1,4 @@ - - - - - - + + + diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 8a15cdd1..e76e1833 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -62,6 +62,7 @@ def draw_page(page, stream): draw_background( stream, stacking_context.box.background, clip_box=False, bleed=page.bleed, marks=marks) + set_mask_border(stream, page) draw_background(stream, page.canvas_background, clip_box=False) draw_border(stream, page) draw_stacking_context(stream, stacking_context) @@ -116,7 +117,7 @@ def draw_stacking_context(stream, stacking_context): if isinstance(box, (boxes.BlockBox, boxes.MarginBox, boxes.InlineBlockBox, boxes.TableCellBox, boxes.FlexContainerBox, boxes.ReplacedBox)): - maybe_set_mask_border(box, stream) + set_mask_border(stream, box) # The canvas background was removed by layout_backgrounds draw_background(stream, box.background) draw_border(stream, box) @@ -138,7 +139,7 @@ def draw_stacking_context(stream, stacking_context): # Point 4 for block in stacking_context.block_level_boxes: - maybe_set_mask_border(block, stream) + set_mask_border(stream, block) if isinstance(block, boxes.TableBox): draw_table(stream, block) @@ -444,7 +445,10 @@ def draw_border(stream, box): # If there's a border image, that takes precedence. if box.style['border_image_source'][0] != 'none' and box.border_image is not None: - draw_border_image(box, stream) + draw_border_image( + box, stream, box.border_image, box.style['border_image_slice'], + box.style['border_image_repeat'], box.style['border_image_outset'], + box.style['border_image_width']) return widths = [getattr(box, f'border_{side}_width') for side in SIDES] @@ -482,8 +486,8 @@ def draw_border(stream, box): stream, box, style, styled_color(style, color, side)) -def draw_border_image_impl(box, stream, image, slice_property, - repeats, outsets, width_property): +def draw_border_image(box, stream, image, border_slice, border_repeat, border_outset, + border_width): """Draw ``image`` as a border image for ``box`` on ``stream`` as specified.""" # Shared by border-image-* and mask-border-* width, height, ratio = image.get_intrinsic_size( @@ -492,8 +496,8 @@ def draw_border_image_impl(box, stream, image, slice_property, width, height, ratio, specified_width=None, specified_height=None, default_width=box.border_width(), default_height=box.border_height()) - image_slice = slice_property[:4] - should_fill = slice_property[4] + image_slice = border_slice[:4] + should_fill = border_slice[4] def compute_slice_dimension(dimension, intrinsic): if isinstance(dimension, (int, float)): @@ -507,7 +511,7 @@ def draw_border_image_impl(box, stream, image, slice_property, 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 = repeats + repeat_x, repeat_y = border_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() @@ -523,10 +527,10 @@ def draw_border_image_impl(box, stream, image, slice_property, assert dimension.unit == 'px' return dimension.value - 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) + outset_top = compute_outset_dimension(border_outset[0], border_top) + outset_right = compute_outset_dimension(border_outset[1], border_right) + outset_bottom = compute_outset_dimension(border_outset[2], border_bottom) + outset_left = compute_outset_dimension(border_outset[3], border_left) x -= outset_left y -= outset_top @@ -551,17 +555,16 @@ def draw_border_image_impl(box, stream, image, slice_property, # for percentage-based border-image-width values includes any expanded # area due to border-image-outset. border_top = compute_width_adjustment( - width_property[0], border_top, slice_top, h) + border_width[0], border_top, slice_top, h) border_right = compute_width_adjustment( - width_property[1], border_right, slice_right, w) + border_width[1], border_right, slice_right, w) border_bottom = compute_width_adjustment( - width_property[2], border_bottom, slice_bottom, h) + border_width[2], border_bottom, slice_bottom, h) border_left = compute_width_adjustment( - width_property[3], border_left, slice_left, w) + border_width[3], border_left, slice_left, w) - def draw_border_image_region(x, y, width, height, slice_x, slice_y, - slice_width, slice_height, - repeat_x='stretch', repeat_y='stretch', + def draw_border_image_region(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 @@ -652,73 +655,56 @@ def draw_border_image_impl(box, stream, image, slice_property, draw_border_image_region( 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: + if x_middle := slice_left + slice_right < intrinsic_width: # Top middle. draw_border_image_region( 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) + slice_top, repeat_x=repeat_x) # Bottom middle. draw_border_image_region( 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: + repeat_x=repeat_x) + if y_middle := slice_top + slice_bottom < intrinsic_height: # Right middle. draw_border_image_region( 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) + repeat_y=repeat_y) # Left middle. draw_border_image_region( 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): + repeat_y=repeat_y) + if should_fill and x_middle and y_middle: # Fill middle. draw_border_image_region( 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, + repeat_x=repeat_x, repeat_y=repeat_y, scale_x=scale_left or scale_right, scale_y=scale_top or scale_bottom) -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 - - draw_border_image_impl( - box, stream, image, box.style['border_image_slice'], - box.style['border_image_repeat'], box.style['border_image_outset'], - box.style['border_image_width']) - - -def maybe_set_mask_border(box, stream): +def set_mask_border(stream, box): """Set ``box`` mask border as alpha state on ``stream``.""" - # See https://drafts.fxtf.org/css-masking/#the-mask-border - if (box.style['mask_border_source'][0] != 'none' - and box.mask_border_image is not None): - x, y, w, h, tl, tr, br, bl = box.rounded_border_box() - matrix = Matrix(e=x, f=y) - matrix @= stream.ctm - mask_stream = stream.set_alpha_state( - x, y, w, h, box.style['mask_border_mode'] != 'luminosity') - - image = box.mask_border_image - - draw_border_image_impl( - box, mask_stream, image, box.style['mask_border_slice'], - box.style['mask_border_repeat'], box.style['mask_border_outset'], - box.style['mask_border_width']) + if box.style['mask_border_source'][0] == 'none' or box.mask_border_image is None: + return + x, y, w, h, tl, tr, br, bl = box.rounded_border_box() + matrix = Matrix(e=x, f=y) + matrix @= stream.ctm + mask_stream = stream.set_alpha_state(x, y, w, h, box.style['mask_border_mode']) + draw_border_image( + box, mask_stream, box.mask_border_image, box.style['mask_border_slice'], + box.style['mask_border_repeat'], box.style['mask_border_outset'], + box.style['mask_border_width']) def clip_border_segment(stream, style, width, side, border_box, @@ -1236,7 +1222,7 @@ def draw_inline_level(stream, page, box, offset_x=0, text_overflow='clip', boxes.InlineBlockBox, boxes.InlineFlexBox, boxes.InlineGridBox)) draw_stacking_context(stream, stacking_context) else: - maybe_set_mask_border(box, stream) + set_mask_border(stream, box) draw_background(stream, box.background) draw_border(stream, box) if isinstance(box, (boxes.InlineBox, boxes.LineBox)): diff --git a/weasyprint/pdf/stream.py b/weasyprint/pdf/stream.py index 72b8d416..0c7ba66d 100644 --- a/weasyprint/pdf/stream.py +++ b/weasyprint/pdf/stream.py @@ -112,13 +112,13 @@ class Stream(pydyf.Stream): self._states[key] = pydyf.Dictionary({'ca': alpha}) super().set_state(key) - def set_alpha_state(self, x, y, width, height, is_alpha_based=False): + def set_alpha_state(self, x, y, width, height, mode='luminosity'): alpha_stream = self.add_group(x, y, width, height) alpha_state = pydyf.Dictionary({ 'Type': '/ExtGState', 'SMask': pydyf.Dictionary({ 'Type': '/Mask', - 'S': '/Alpha' if is_alpha_based else '/Luminosity', + 'S': f'/{mode.capitalize()}', 'G': alpha_stream, }), 'ca': 1,