Merge pull request #2345 from Kozea/small-caps

Handle small-caps synthesis
This commit is contained in:
Guillaume Ayoub
2025-01-24 22:52:53 +01:00
committed by GitHub
11 changed files with 198 additions and 42 deletions
-4
View File
@@ -361,10 +361,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.
+137
View File
@@ -1012,3 +1012,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)
+3 -3
View File
@@ -16,8 +16,8 @@ from .properties import ( # isort:skip
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, text_decoration_thickness, validate_non_shorthand)
@@ -678,7 +678,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]) is not None:
suffix = '-variant-caps'
elif font_weight([token]) is not None:
suffix = '-weight'
+1 -1
View File
@@ -889,7 +889,7 @@ def font_feature_settings(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')
+9 -10
View File
@@ -89,8 +89,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)
@@ -137,7 +136,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
previous_pango_font = None
string = ''
x_advance = 0
emojis = []
@@ -152,21 +151,21 @@ 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.
pango_font = glyph_item.item.analysis.font
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:
pango_font = glyph_item.item.analysis.font
if pango_font != previous_pango_font:
# Add font file content and get font size.
previous_pango_font = pango_font
font, font_size = stream.add_font(pango_font)
# Go through the run glyphs.
if string:
stream.show_text(string)
string = ''
stream.set_font_size(font.hash, 1 if font.bitmap else font_size)
last_font = font
string += '<'
for i in range(num_glyphs):
glyph_info = glyphs[i]
+2 -2
View File
@@ -203,7 +203,7 @@ def add_forms(forms, matrix, pdf, page, resources, stream, font_map):
font_description = get_font_description(style)
font = pango.pango_font_map_load_font(
font_map, context, font_description)
font = stream.add_font(font)
font, _ = stream.add_font(font)
font.used_in_forms = True
field_stream.set_font_size(font.hash, font_size)
@@ -252,7 +252,7 @@ def add_forms(forms, matrix, pdf, page, resources, stream, font_map):
font_description = get_font_description(style)
font = pango.pango_font_map_load_font(
font_map, context, font_description)
font = stream.add_font(font)
font, _ = stream.add_font(font)
font.used_in_forms = True
field_stream.set_font_size(font.hash, font_size)
+12 -14
View File
@@ -17,39 +17,38 @@ from ..text.fonts import get_hb_object_data, get_pango_font_hb_face
class Font:
def __init__(self, pango_font):
def __init__(self, pango_font, description, font_size):
self.hb_font = pango.pango_font_get_hb_font(pango_font)
self.hb_face = get_pango_font_hb_face(pango_font)
self.file_content = get_hb_object_data(self.hb_face)
self.index = harfbuzz.hb_face_get_index(self.hb_face)
pango_metrics = pango.pango_font_get_metrics(pango_font, ffi.NULL)
self.description = description = ffi.gc(
pango.pango_font_describe(pango_font), pango.pango_font_description_free)
self.font_size = pango.pango_font_description_get_size(description)
self.font_size = font_size
self.style = pango.pango_font_description_get_style(description)
self.family = ffi.string(pango.pango_font_description_get_family(description))
self.variations = {}
variations = pango.pango_font_description_get_variations(self.description)
variations = pango.pango_font_description_get_variations(description)
if variations != ffi.NULL:
self.variations = {
part.split('=')[0]: float(part.split('=')[1])
for part in ffi.string(variations).decode().split(',')}
if weight := self.variations.get('weight'):
pango.pango_font_description_set_weight(
self.description, int(round(weight)))
self.weight = int(round(weight))
pango.pango_font_description_set_weight(description, weight)
else:
self.weight = pango.pango_font_description_get_weight(description)
if self.variations.get('ital'):
pango.pango_font_description_set_style(
self.description, pango.PANGO_STYLE_ITALIC)
description, pango.PANGO_STYLE_ITALIC)
elif self.variations.get('slnt'):
pango.pango_font_description_set_style(
self.description, pango.PANGO_STYLE_OBLIQUE)
description, pango.PANGO_STYLE_OBLIQUE)
if (width := self.variations.get('wdth')) is not None:
stretch = min(
PANGO_STRETCH_PERCENT.items(),
key=lambda item: abs(item[0] - width))[1]
pango.pango_font_description_set_stretch(self.description, stretch)
pango.pango_font_description_set_stretch(description, stretch)
description_string = ffi.string(
pango.pango_font_description_to_string(description))
@@ -70,6 +69,7 @@ class Font:
# Set ascent and descent.
if self.font_size:
pango_metrics = pango.pango_font_get_metrics(pango_font, ffi.NULL)
self.ascent = int(
pango.pango_font_metrics_get_ascent(pango_metrics) /
self.font_size * 1000)
@@ -126,9 +126,7 @@ class Font:
full_font = io.BytesIO(self.file_content)
ttfont = TTFont(full_font, fontNumber=self.index)
if 'wght' not in self.variations:
weight = pango.pango_font_description_get_weight(
self.description)
self.variations['wght'] = weight
self.variations['wght'] = self.weight
if 'opsz' not in self.variations:
self.variations['opsz'] = self.font_size * FROM_UNITS
if 'slnt' not in self.variations:
+3 -3
View File
@@ -161,10 +161,10 @@ class Stream(pydyf.Stream):
}))
def add_font(self, pango_font):
key = get_pango_font_key(pango_font)
key, description, font_size = get_pango_font_key(pango_font)
if key not in self._fonts:
self._fonts[key] = Font(pango_font)
return self._fonts[key]
self._fonts[key] = Font(pango_font, description, font_size)
return self._fonts[key], font_size
def add_group(self, x, y, width, height):
resources = pydyf.Dictionary({
+10
View File
@@ -38,6 +38,16 @@ PANGO_WRAP_MODE = {
'WRAP_CHAR': pango.PANGO_WRAP_CHAR,
'WRAP_WORD_CHAR': pango.PANGO_WRAP_WORD_CHAR
}
# Some variants have been added in Pango 1.50 and are ignored when used.
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
+12
View File
@@ -145,6 +145,16 @@ ffi.cdef('''
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;
@@ -291,6 +301,8 @@ ffi.cdef('''
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 (
+9 -5
View File
@@ -14,10 +14,11 @@ from ..logger import LOGGER
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)
FROM_UNITS, TO_UNITS, ffi, fontconfig, gobject, harfbuzz, pango, pangoft2,
unicode_to_char_p)
def _check_font_configuration(font_config): # pragma: no cover
@@ -287,7 +288,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
@@ -319,6 +320,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
@@ -362,6 +365,7 @@ def get_pango_font_key(pango_font):
# FontConfiguration object. See https://github.com/Kozea/WeasyPrint/issues/2144
description = ffi.gc(
pango.pango_font_describe(pango_font), pango.pango_font_description_free)
font_size = pango.pango_font_description_get_size(description) * FROM_UNITS
mask = pango.PANGO_FONT_MASK_SIZE + pango.PANGO_FONT_MASK_GRAVITY
pango.pango_font_description_unset_fields(description, mask)
return pango.pango_font_description_hash(description)
return pango.pango_font_description_hash(description), description, font_size