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

Handle small-caps synthesis #2345

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 0 additions & 4 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -369,10 +369,6 @@ WeasyPrint does **not** support the ``@font-feature-values`` rule and the
values of ``font-variant-alternates`` other than ``normal`` and
``historical-forms``.

The ``font-variant-caps`` property is supported but needs the small-caps variant of
the font to be installed. WeasyPrint does **not** simulate missing small-caps
fonts.

From `CSS Fonts Module Level 4`_ we only support the
``font-variation-settings`` property enabling specific font variations.

Expand Down
137 changes: 137 additions & 0 deletions tests/draw/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -900,3 +900,140 @@ def test_huge_justification(assert_pixels):
}
</style>
A B''')


def test_font_variant_caps_small(assert_pixels):
assert_pixels('''
________
_BB_BB__
_BB_B_B_
_B__BB__
_B__B___
________
''', '''
<style>
@page {size: 8px 6px}
p {
color: blue;
font-variant-caps: small-caps;
font-family: %s;
font-size: 6px;
line-height: 1;
}
</style>
<p>Pp</p>
''' % SANS_FONTS)


def test_font_variant_caps_all_small(assert_pixels):
assert_pixels('''
________
BB_BB___
B_BB_B__
BB_BB___
B__B____
________
''', '''
<style>
@page {size: 8px 6px}
p {
color: blue;
font-variant-caps: all-small-caps;
font-family: %s;
font-size: 6px;
line-height: 1;
}
</style>
<p>Pp</p>
''' % SANS_FONTS)


def test_font_variant_caps_petite(assert_pixels):
assert_pixels('''
________
_BB_BB__
_BB_B_B_
_B__BB__
_B__B___
________
''', '''
<style>
@page {size: 8px 6px}
p {
color: blue;
font-variant-caps: petite-caps;
font-family: %s;
font-size: 6px;
line-height: 1;
}
</style>
<p>Pp</p>
''' % SANS_FONTS)


def test_font_variant_caps_all_petite(assert_pixels):
assert_pixels('''
________
BB_BB___
B_BB_B__
BB_BB___
B__B____
________
''', '''
<style>
@page {size: 8px 6px}
p {
color: blue;
font-variant-caps: all-petite-caps;
font-family: %s;
font-size: 6px;
line-height: 1;
}
</style>
<p>Pp</p>
''' % SANS_FONTS)


def test_font_variant_caps_unicase(assert_pixels):
assert_pixels('''
________
BB______
B_B_BB__
BB__B_B_
B___BB__
____B___
''', '''
<style>
@page {size: 8px 6px}
p {
color: blue;
font-variant-caps: unicase;
font-family: %s;
font-size: 6px;
line-height: 1;
}
</style>
<p>Pp</p>
''' % SANS_FONTS)


def test_font_variant_caps_titling(assert_pixels):
assert_pixels('''
_BB_____
_BB_____
_BB__BB_
_B___B_B
_____BB_
_____B__
''', '''
<style>
@page {size: 8px 6px}
p {
color: blue;
font-family: %s;
font-size: 6px;
line-height: 1;
}
</style>
<p>Pp</p>
''' % SANS_FONTS)
6 changes: 3 additions & 3 deletions weasyprint/css/validation/expanders.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
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, gap, grid_line, grid_template, line_height,
flex_grow_shrink, flex_wrap, font_family, font_size, font_stretch, font_style,
font_variant_caps, 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)

Expand Down Expand Up @@ -675,7 +675,7 @@ def expand_font(tokens, name):

if font_style([token]) is not None:
suffix = '-style'
elif get_keyword(token) in ('normal', 'small-caps'):
elif font_variant_caps([token]) in ('normal', 'small-caps'):
suffix = '-variant-caps'
elif font_weight([token]) is not None:
suffix = '-weight'
Expand Down
2 changes: 1 addition & 1 deletion weasyprint/css/validation/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,7 @@ def font_feature_settings_list(tokens):
@single_keyword
def font_variant_alternates(keyword):
# TODO: support other values
# See https://www.w3.org/TR/css-fonts-3/#font-variant-caps-prop
# See https://drafts.csswg.org/css-fonts/#font-variant-alternates-prop
return keyword in ('normal', 'historical-forms')


Expand Down
13 changes: 7 additions & 6 deletions weasyprint/draw/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, matrix):
if not textbox.text.strip():
return []

font_size = textbox.style['font_size']
if font_size < 1e-6: # default float precision used by pydyf
if textbox.style['font_size'] < 1e-6: # default float precision used by pydyf
return []

pango.pango_layout_set_single_paragraph_mode(textbox.pango_layout.layout, True)
Expand Down Expand Up @@ -126,7 +125,7 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, matrix):
utf8_text = textbox.pango_layout.text.encode()
previous_utf8_position = 0
stream.set_text_matrix(*matrix.values)
last_font = None
last_font = last_font_size = None
string = ''
x_advance = 0
emojis = []
Expand All @@ -141,21 +140,23 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, matrix):
offset = glyph_item.item.offset
clusters = glyph_string.log_clusters

# Add font file content.
# Add font file content and get font size.
pango_font = glyph_item.item.analysis.font
description = pango.pango_font_describe(pango_font)
font_size = pango.pango_font_description_get_size(description) * FROM_UNITS
font = stream.add_font(pango_font)

# Get positions of the glyphs in the UTF-8 string.
utf8_positions = [offset + clusters[i] for i in range(1, num_glyphs)]
utf8_positions.append(offset + glyph_item.item.length)

# Go through the run glyphs.
if font != last_font:
if (font, font_size) != (last_font, last_font_size):
if string:
stream.show_text(string)
string = ''
stream.set_font_size(font.hash, 1 if font.bitmap else font_size)
last_font = font
last_font, last_font_size = font, font_size
string += '<'
for i in range(num_glyphs):
glyph_info = glyphs[i]
Expand Down
22 changes: 22 additions & 0 deletions weasyprint/text/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,28 @@
'WRAP_WORD_CHAR': pango.PANGO_WRAP_WORD_CHAR
}

if pango.pango_version() < 15000:
# Some variants have been added in Pango 1.50.
PANGO_VARIANT = {
'normal': pango.PANGO_VARIANT_NORMAL,
'small-caps': pango.PANGO_VARIANT_SMALL_CAPS,
'all-small-caps': pango.PANGO_VARIANT_SMALL_CAPS,
'petite-caps': pango.PANGO_VARIANT_SMALL_CAPS,
'all-petite-caps': pango.PANGO_VARIANT_SMALL_CAPS,
'unicase': pango.PANGO_VARIANT_NORMAL,
'titling-caps': pango.PANGO_VARIANT_NORMAL,
}
else:
PANGO_VARIANT = {
'normal': pango.PANGO_VARIANT_NORMAL,
'small-caps': pango.PANGO_VARIANT_SMALL_CAPS,
'all-small-caps': pango.PANGO_VARIANT_ALL_SMALL_CAPS,
'petite-caps': pango.PANGO_VARIANT_PETITE_CAPS,
'all-petite-caps': pango.PANGO_VARIANT_ALL_PETITE_CAPS,
'unicase': pango.PANGO_VARIANT_UNICASE,
'titling-caps': pango.PANGO_VARIANT_TITLE_CAPS,
}

# Language system tags
# From https://docs.microsoft.com/typography/opentype/spec/languagetags
LST_TO_ISO = {
Expand Down
12 changes: 12 additions & 0 deletions weasyprint/text/ffi.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@
PANGO_WRAP_WORD_CHAR
} PangoWrapMode;

typedef enum {
PANGO_VARIANT_NORMAL,
PANGO_VARIANT_SMALL_CAPS,
PANGO_VARIANT_ALL_SMALL_CAPS,
PANGO_VARIANT_PETITE_CAPS,
PANGO_VARIANT_ALL_PETITE_CAPS,
PANGO_VARIANT_UNICASE,
PANGO_VARIANT_TITLE_CAPS,
} PangoVariant;

typedef enum {
PANGO_TAB_LEFT
} PangoTabAlign;
Expand Down Expand Up @@ -291,6 +301,8 @@
PangoFontDescription *desc, double size);
void pango_font_description_set_variations (
PangoFontDescription* desc, const char* variations);
void pango_font_description_set_variant (
PangoFontDescription* desc, PangoVariant variant);

PangoStyle pango_font_description_get_style (const PangoFontDescription *desc);
const char* pango_font_description_get_variations (
Expand Down
8 changes: 5 additions & 3 deletions weasyprint/text/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
from ..urls import FILESYSTEM_ENCODING, fetch

from .constants import ( # isort:skip
CAPS_KEYS, EAST_ASIAN_KEYS, FONTCONFIG_STRETCH, FONTCONFIG_STYLE,
FONTCONFIG_WEIGHT, LIGATURE_KEYS, NUMERIC_KEYS, PANGO_STRETCH, PANGO_STYLE)
CAPS_KEYS, EAST_ASIAN_KEYS, FONTCONFIG_STRETCH, FONTCONFIG_STYLE, FONTCONFIG_WEIGHT,
LIGATURE_KEYS, NUMERIC_KEYS, PANGO_STRETCH, PANGO_STYLE, PANGO_VARIANT)
from .ffi import ( # isort:skip
TO_UNITS, ffi, fontconfig, gobject, harfbuzz, pango, pangoft2, unicode_to_char_p)

Expand Down Expand Up @@ -287,7 +287,7 @@ def font_features(font_kerning='normal', font_variant_ligatures='normal',

if font_variant_alternates != 'normal':
# TODO: support other values
# See https://www.w3.org/TR/css-fonts-3/#font-variant-caps-prop
# See https://drafts.csswg.org/css-fonts/#font-variant-alternates-prop
if font_variant_alternates == 'historical-forms':
features['hist'] = 1

Expand Down Expand Up @@ -319,6 +319,8 @@ def get_font_description(style):
pango.pango_font_description_set_weight(font_description, font_weight)
font_size = int(style['font_size'] * TO_UNITS)
pango.pango_font_description_set_absolute_size(font_description, font_size)
font_variant = PANGO_VARIANT[style['font_variant_caps']]
pango.pango_font_description_set_variant(font_description, font_variant)
if style['font_variation_settings'] != 'normal':
string = ','.join(
f'{key}={value}' for key, value in
Expand Down
Loading