Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support text-underline-offset and text-decoration-thickness #2361

Merged
merged 1 commit into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -718,22 +718,25 @@ The custom properties and the ``var()`` notation are supported.

.. _CSS Custom Properties for Cascading Variables Module Level 1: https://www.w3.org/TR/css-variables/

CSS Text Decoration Module Level 3
++++++++++++++++++++++++++++++++++
CSS Text Decoration Module Level 3 / 4
++++++++++++++++++++++++++++++++++++++

The `CSS Text Decoration Module Level 3`_ "contains the features of CSS
relating to text decoration, such as underlines, text shadows, and emphasis
marks."

The ``text-decoration-line``, ``text-decoration-style`` and
``text-decoration-color`` properties are supported, except from the ``wavy``
value of ``text-decoration-style``. The ``text-decoration`` shorthand is also
supported.
The `CSS Text Decoration Module Level 4`_ is a working draft on the same subject.

The ``text-decoration-line``, ``text-decoration-style``,
``text-decoration-color``, ``text-decoration-thickness`` and
``text-underline-offset`` properties are supported. The ``text-decoration``
shorthand is also supported.

The other properties (``text-underline-position``, ``text-emphasis-*``,
``text-shadow``) are not supported.

.. _CSS Text Decoration Module Level 3: https://www.w3.org/TR/css-text-decor-3/
.. _CSS Text Decoration Module Level 4: https://www.w3.org/TR/css-text-decor-4/

CSS Flexible Box Layout Module Level 1
++++++++++++++++++++++++++++++++++++++
Expand Down
12 changes: 10 additions & 2 deletions tests/css/test_expanders.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,17 @@ def test_empty_expander_value(expander):
'text_decoration_line': {'blink', 'line-through', 'overline'},
}),
('red', {'text_decoration_color': parse_color('red')}),
('blue 1px', {
'text_decoration_color': parse_color('blue'),
'text_decoration_thickness': (1, 'px'),
}),
('100% none', {
'text_decoration_line': 'none',
'text_decoration_thickness': (100, '%'),
}),
('inherit', {
f'text_decoration_{key}': 'inherit'
for key in ('color', 'line', 'style')}),
for key in ('color', 'line', 'style', 'thickness')}),
))
def test_text_decoration(rule, result):
assert expand_to_dict(f'text-decoration: {rule}') == result
Expand All @@ -59,8 +67,8 @@ def test_text_decoration(rule, result):
@pytest.mark.parametrize('rule', (
'solid solid',
'red red',
'1px',
'underline none',
'1px 100%',
'none none',
))
def test_text_decoration_invalid(rule):
Expand Down
112 changes: 112 additions & 0 deletions tests/draw/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,118 @@ def test_text_underline(assert_pixels):
<div>abc</div>''')


def test_text_underline_offset(assert_pixels):
assert_pixels('''
_____________
_zzzzzzzzzzz_
_zRRRRRRRRRz_
_zRRRRRRRRRz_
_zzzzzzzzzzz_
_zzzzzzzzzzz_
_zBBBBBBBBBz_
_zzzzzzzzzzz_
_____________
''', '''
<style>
@page {
size: 13px 9px;
margin: 2px;
}
body {
color: red;
font-family: weasyprint;
font-size: 3px;
text-decoration: underline blue;
text-underline-offset: 2px;
}
</style>
<div>abc</div>''')


def test_text_underline_offset_percentage(assert_pixels):
assert_pixels('''
_____________
_zzzzzzzzzzz_
_zRRRRRRRRRz_
_zRRRRRRRRRz_
_zzzzzzzzzzz_
_zzzzzzzzzzz_
_zBBBBBBBBBz_
_zzzzzzzzzzz_
_____________
''', '''
<style>
@page {
size: 13px 9px;
margin: 2px;
}
body {
color: red;
font-family: weasyprint;
font-size: 3px;
text-decoration: underline blue;
text-underline-offset: 70%;
}
</style>
<div>abc</div>''')


def test_text_underline_thickness(assert_pixels):
assert_pixels('''
_____________
_zzzzzzzzzzz_
_zRRRRRRRRRz_
_zRRRRRRRRRz_
_zzzzzzzzzzz_
_zzzzzzzzzzz_
_zBBBBBBBBBz_
_zBBBBBBBBBz_
_zzzzzzzzzzz_
''', '''
<style>
@page {
size: 13px 9px;
margin: 2px;
}
body {
color: red;
font-family: weasyprint;
font-size: 3px;
text-decoration: underline blue 3px;
text-underline-offset: 2px;
}
</style>
<div>abc</div>''')


def test_text_underline_thickness_percentage(assert_pixels):
assert_pixels('''
_____________
_zzzzzzzzzzz_
_zRRRRRRRRRz_
_zRRRRRRRRRz_
_zzzzzzzzzzz_
_zzzzzzzzzzz_
_zBBBBBBBBBz_
_zBBBBBBBBBz_
_zzzzzzzzzzz_
''', '''
<style>
@page {
size: 13px 9px;
margin: 2px;
}
body {
color: red;
font-family: weasyprint;
font-size: 3px;
text-decoration: underline blue 100%;
text-underline-offset: 2px;
}
</style>
<div>abc</div>''')


def test_text_overline(assert_pixels):
# Ascent value seems to be a bit random, don’t try to get the exact
# position of the line
Expand Down
4 changes: 3 additions & 1 deletion weasyprint/css/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,9 @@ def text_decoration(key, value, parent_value, cascaded):
# using specific rules.
# See https://drafts.csswg.org/css-text-decor-3/#line-decoration
# TODO: these rules don’t follow the specification.
if key in ('text_decoration_color', 'text_decoration_style'):
text_properties = (
'text_decoration_color', 'text_decoration_style', 'text_decoration_thickness')
if key in text_properties:
if not cascaded:
value = parent_value
elif key == 'text_decoration_line':
Expand Down
4 changes: 3 additions & 1 deletion weasyprint/css/computed_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,11 @@ def break_before_after(style, name, value):
@register_computer('text-indent')
@register_computer('hyphenate-limit-zone')
@register_computer('flex-basis')
@register_computer('text-underline-offset')
@register_computer('text-decoration-thickness')
def length(style, name, value, font_size=None, pixels_only=False):
"""Compute a length ``value``."""
if value in ('auto', 'content'):
if value in ('auto', 'content', 'from-font'):
return value
if value.value == 0:
return 0 if pixels_only else ZERO_PIXELS
Expand Down
5 changes: 4 additions & 1 deletion weasyprint/css/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,12 @@
'column_gap': 'normal',
'row_gap': 'normal',

# Text Decoration Module 3 (CR): https://www.w3.org/TR/css-text-decor-3/
# Text Decoration Module 3/4 (CR/WD): https://www.w3.org/TR/css-text-decor-4/
'text_decoration_line': 'none',
'text_decoration_color': 'currentcolor',
'text_decoration_style': 'solid',
'text_decoration_thickness': 'auto',
'text_underline_offset': 'auto',

# Overflow Module 3/4 (WD): https://www.w3.org/TR/css-overflow-4/
'block_ellipsis': 'none',
Expand Down Expand Up @@ -310,6 +312,7 @@
'text_align_last',
'text_indent',
'text_transform',
'text_underline_offset',
'visibility',
'white_space',
'widows',
Expand Down
51 changes: 27 additions & 24 deletions weasyprint/css/validation/expanders.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
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, mask_border_mode,
other_colors, overflow_wrap, validate_non_shorthand)
other_colors, overflow_wrap, text_decoration_thickness, validate_non_shorthand)

EXPANDERS = {}

Expand Down Expand Up @@ -520,43 +520,46 @@ def add(name, value):


@expander('text-decoration')
@generic_expander('-line', '-color', '-style')
@generic_expander('-line', '-color', '-style', '-thickness')
def expand_text_decoration(tokens, name):
"""Expand the ``text-decoration`` shorthand property."""
text_decoration_line = []
text_decoration_color = []
text_decoration_style = []
line = []
color = []
style = []
thickness = []
none_in_line = False

for token in tokens:
keyword = get_keyword(token)
if keyword in (
'none', 'underline', 'overline', 'line-through', 'blink'):
text_decoration_line.append(token)
if keyword in ('none', 'underline', 'overline', 'line-through', 'blink'):
line.append(token)
if none_in_line:
raise InvalidValues
elif keyword == 'none':
none_in_line = True
elif keyword in ('solid', 'double', 'dotted', 'dashed', 'wavy'):
if text_decoration_style:
if style:
raise InvalidValues
else:
text_decoration_style.append(token)
else:
color = parse_color(token)
if color is None:
style.append(token)
elif parse_color(token):
if color:
raise InvalidValues
elif text_decoration_color:
color.append(token)
elif text_decoration_thickness([token]):
if thickness:
raise InvalidValues
else:
text_decoration_color.append(token)

if text_decoration_line:
yield '-line', text_decoration_line
if text_decoration_color:
yield '-color', text_decoration_color
if text_decoration_style:
yield '-style', text_decoration_style
thickness.append(token)
else:
raise InvalidValues

if line:
yield '-line', line
if color:
yield '-color', color
if style:
yield '-style', style
if thickness:
yield '-thickness', thickness


def expand_page_break_before_after(tokens, name):
Expand Down
14 changes: 13 additions & 1 deletion weasyprint/css/validation/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,9 +648,10 @@ def counter(tokens, default_integer):
@property('margin-right')
@property('margin-bottom')
@property('margin-left')
@property('text-underline-offset')
@single_token
def lenght_precentage_or_auto(token):
"""``margin-*`` properties validation."""
"""``margin-*`` and various other properties validation."""
length = get_length(token, percentage=True)
if length:
return length
Expand Down Expand Up @@ -1236,6 +1237,17 @@ def text_decoration_style(keyword):
return keyword


@property()
@single_token
def text_decoration_thickness(token):
"""``text-decoration-thickness`` property validation."""
length = get_length(token, percentage=True)
if length:
return length
if keyword := get_keyword(token) in ('auto', 'from-font'):
return keyword


@property()
@single_token
def text_indent(token):
Expand Down
1 change: 1 addition & 0 deletions weasyprint/draw/border.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ def draw_line(stream, x1, y1, x2, y2, thickness, style, color, offset=0):
x = x1 - offset
stream.move_to(x, y1)
while x < x2:
stream.set_line_width(thickness)
stream.curve_to(
x + radius / 2, y1 + up * radius,
x + 3 * radius / 2, y1 + up * radius,
Expand Down
21 changes: 16 additions & 5 deletions weasyprint/draw/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,29 @@ def draw_text(stream, textbox, offset_x, text_overflow, block_ellipsis):
# Draw underline and overline.
text_decoration_values = textbox.style['text_decoration_line']
text_decoration_color = get_color(textbox.style, 'text_decoration_color')
if 'underline' in text_decoration_values or 'overline' in text_decoration_values:
if textbox.style['text_decoration_thickness'] in ('auto', 'from-font'):
thickness = textbox.pango_layout.underline_thickness
elif textbox.style['text_decoration_thickness'].unit == '%':
ratio = textbox.style['text_decoration_thickness'].value / 100
thickness = textbox.style['font_size'] * ratio
else:
thickness = textbox.style['text_decoration_thickness'].value
if 'overline' in text_decoration_values:
thickness = textbox.pango_layout.underline_thickness
offset_y = (
textbox.baseline - textbox.pango_layout.ascent + thickness / 2)
draw_text_decoration(
stream, textbox, offset_x, offset_y, thickness,
text_decoration_color)
if 'underline' in text_decoration_values:
thickness = textbox.pango_layout.underline_thickness
offset_y = (
textbox.baseline - textbox.pango_layout.underline_position +
thickness / 2)
if textbox.style['text_underline_offset'] == 'auto':
underline_offset = - textbox.pango_layout.underline_position
elif textbox.style['text_underline_offset'].unit == '%':
ratio = textbox.style['text_underline_offset'].value / 100
underline_offset = textbox.style['font_size'] * ratio
else:
underline_offset = textbox.style['text_underline_offset'].value
offset_y = textbox.baseline + underline_offset + thickness / 2
draw_text_decoration(
stream, textbox, offset_x, offset_y, thickness,
text_decoration_color)
Expand Down
Loading