Merge pull request #2345 from Kozea/small-caps
Handle small-caps synthesis
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user