Skip to content

Commit

Permalink
Merge pull request #2345 from Kozea/small-caps
Browse files Browse the repository at this point in the history
Handle small-caps synthesis
  • Loading branch information
liZe authored Jan 24, 2025
2 parents 7fac29b + 6f21a24 commit 43fe329
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 42 deletions.
4 changes: 0 additions & 4 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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 @@ -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)
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, text_decoration_thickness, validate_non_shorthand)

Expand Down Expand Up @@ -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'
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 @@ -889,7 +889,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
19 changes: 9 additions & 10 deletions weasyprint/draw/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 = []
Expand All @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions weasyprint/pdf/anchors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
26 changes: 12 additions & 14 deletions weasyprint/pdf/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,38 @@


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

Expand All @@ -70,6 +69,7 @@ def __init__(self, pango_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)
Expand Down Expand Up @@ -126,9 +126,7 @@ def clean(self, cmap, hinting):
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:
Expand Down
6 changes: 3 additions & 3 deletions weasyprint/pdf/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,10 @@ def set_blend_mode(self, mode):
}))

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({
Expand Down
10 changes: 10 additions & 0 deletions weasyprint/text/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@
'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
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
Loading

0 comments on commit 43fe329

Please sign in to comment.