1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-08-18 00:50:32 +03:00

Merge pull request #2139 from xavidotron/main

Add support for mask-border-* properties
This commit is contained in:
Guillaume Ayoub 2024-06-08 23:03:24 +02:00 committed by GitHub
commit dd57e17632
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 429 additions and 49 deletions

View File

@ -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', {

View File

@ -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'),)),)),

View File

@ -576,3 +576,65 @@ def test_border_image_gradient(assert_pixels):
</style>
<div></div>
''')
@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_
__________
''', '''
<style>
@page {
size: 10px 10px;
}
div {
background: red;
mask-border-source: url(mask.svg);
mask-border-slice: 20%;
height: 8px;
width: 8px;
margin: 1px;
}
</style>
<div></div>
''')
@assert_no_logs
def test_mask_border_fill(assert_pixels):
assert_pixels('''
__________
__RR__RRR_
_RRRRRRRR_
_RRRRRRRR_
_sRR__RRR_
_sRR__RRR_
_RRRRRRRR_
_RRRRRRRR_
__RRRRRRR_
__________
''', '''
<style>
@page {
size: 10px 10px;
}
div {
background: red;
mask-border-source: url(mask.svg);
mask-border-slice: 20% fill;
height: 8px;
width: 8px;
margin: 1px;
}
</style>
<div></div>
''')

4
tests/resources/mask.svg Normal file
View File

@ -0,0 +1,4 @@
<svg height="5" width="5" xmlns="http://www.w3.org/2000/svg">
<path d="M 1 0 L 1 1 L 0 1 L 0 2 L 1 2 L 1 3 L 0 3 L 0 4 L 1 4 L 1 5 L 2 5 L 3 5 L 4 5 L 5 5 L 5 4 L 5 3 L 5 2 L 5 1 L 5 0 L 4 0 L 3 0 L 3 1 L 2 1 L 2 0 L 1 0 z M 2 2 L 3 2 L 3 3 L 2 3 L 2 2 z " />
<rect fill="black" opacity="0.5" width="1" height="1" x="0" y="2" />
</svg>

After

Width:  |  Height:  |  Size: 340 B

View File

@ -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

View File

@ -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/

View File

@ -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.

View File

@ -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):

View File

@ -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,6 +117,7 @@ def draw_stacking_context(stream, stacking_context):
if isinstance(box, (boxes.BlockBox, boxes.MarginBox,
boxes.InlineBlockBox, boxes.TableCellBox,
boxes.FlexContainerBox, boxes.ReplacedBox)):
set_mask_border(stream, box)
# The canvas background was removed by layout_backgrounds
draw_background(stream, box.background)
draw_border(stream, box)
@ -137,6 +139,8 @@ def draw_stacking_context(stream, stacking_context):
# Point 4
for block in stacking_context.block_level_boxes:
set_mask_border(stream, block)
if isinstance(block, boxes.TableBox):
draw_table(stream, block)
else:
@ -441,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]
@ -479,18 +486,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(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(
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 = border_slice[:4]
should_fill = border_slice[4]
def compute_slice_dimension(dimension, intrinsic):
if isinstance(dimension, (int, float)):
@ -504,7 +511,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']
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()
@ -520,11 +527,10 @@ 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)
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
@ -548,20 +554,18 @@ 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)
border_width[0], border_top, slice_top, h)
border_right = compute_width_adjustment(
widths[1], border_right, slice_right, w)
border_width[1], border_right, slice_right, w)
border_bottom = compute_width_adjustment(
widths[2], border_bottom, slice_bottom, h)
border_width[2], border_bottom, slice_bottom, h)
border_left = compute_width_adjustment(
widths[3], border_left, slice_left, w)
border_width[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,60 +640,73 @@ 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:
if x_middle := 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)
slice_top, repeat_x=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,
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(
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(
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(
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 set_mask_border(stream, box):
"""Set ``box`` mask border as alpha state on ``stream``."""
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,
border_widths=None, radii=None):
"""Clip one segment of box border.
@ -1205,6 +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:
set_mask_border(stream, box)
draw_background(stream, box.background)
draw_border(stream, box)
if isinstance(box, (boxes.InlineBox, boxes.LineBox)):

View File

@ -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')

View File

@ -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, mode='luminosity'):
alpha_stream = self.add_group(x, y, width, height)
alpha_state = pydyf.Dictionary({
'Type': '/ExtGState',
'SMask': pydyf.Dictionary({
'Type': '/Mask',
'S': '/Luminosity',
'S': f'/{mode.capitalize()}',
'G': alpha_stream,
}),
'ca': 1,