From 980971f9eb5087c6d1a25e9aa36f01912efc8082 Mon Sep 17 00:00:00 2001 From: aschmitz <29508+aschmitz@users.noreply.github.com> Date: Wed, 2 Mar 2022 17:33:04 -0600 Subject: [PATCH 1/2] perf: cache calculations for 'ch' and 'ex' units --- weasyprint/css/__init__.py | 13 +++++++++ weasyprint/css/computed_values.py | 48 +++++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/weasyprint/css/__init__.py b/weasyprint/css/__init__.py index a51fc9240..fb6b85217 100644 --- a/weasyprint/css/__init__.py +++ b/weasyprint/css/__init__.py @@ -33,6 +33,11 @@ None, 'before', 'after', 'marker', 'first-line', 'first-letter', 'footnote-call', 'footnote-marker') +DEFAULT_CACHE = { + 'length_ch': {}, + 'length_ex': {}, +} + PageType = namedtuple('PageType', ['side', 'blank', 'first', 'index', 'name']) @@ -600,6 +605,10 @@ def __init__(self, parent_style): }) self.parent_style = parent_style self.specified = self + if parent_style: + self.cache = parent_style.cache + else: + self.cache = DEFAULT_CACHE.copy() def copy(self): copy = AnonymousStyle(self.parent_style) @@ -629,6 +638,10 @@ def __init__(self, parent_style, cascaded, element, pseudo_type, self.pseudo_type = pseudo_type self.root_style = root_style self.base_url = base_url + if parent_style: + self.cache = parent_style.cache + else: + self.cache = DEFAULT_CACHE.copy() def copy(self): copy = ComputedStyle( diff --git a/weasyprint/css/computed_values.py b/weasyprint/css/computed_values.py index 87cccc2b0..517967ea6 100644 --- a/weasyprint/css/computed_values.py +++ b/weasyprint/css/computed_values.py @@ -178,6 +178,23 @@ def _resolve_var(computed, variable_name, default, parent_style): return computed_value +def _font_style_cache_key(style, font_size): + return str(( + style['font_family'], + font_size, + style['font_style'], + style['font_stretch'], + style['font_weight'], + style['font_variant_ligatures'], + style['font_variant_position'], + style['font_variant_caps'], + style['font_variant_numeric'], + style['font_variant_alternates'], + style['font_variant_east_asian'], + style['font_feature_settings'], + )) + + def register_computer(name): """Decorator registering a property ``name`` for a function.""" name = name.replace('-', '_') @@ -321,17 +338,30 @@ def length(style, name, value, font_size=None, pixels_only=False): if font_size is None: font_size = style['font_size'] if unit == 'ex': - # TODO: cache - result = value.value * font_size * ex_ratio(style) + cache_key = _font_style_cache_key(style, None) + + if cache_key in style.cache['length_ex']: + ratio = style.cache['length_ex'][cache_key] + else: + ratio = ex_ratio(style) + style.cache['length_ex'][cache_key] = ratio + + result = value.value * font_size * ratio elif unit == 'ch': - # TODO: cache # TODO: use context to use @font-face fonts - layout = Layout( - context=None, font_size=font_size, - style=style) - layout.set_text('0') - line, _ = layout.get_first_line() - logical_width, _ = line_size(line, style) + cache_key = _font_style_cache_key(style, font_size) + + if cache_key in style.cache['length_ch']: + logical_width = style.cache['length_ch'][cache_key] + else: + layout = Layout( + context=None, font_size=font_size, + style=style) + layout.set_text('0') + line, _ = layout.get_first_line() + logical_width, _ = line_size(line, style) + style.cache['length_ch'][cache_key] = logical_width + result = value.value * logical_width elif unit == 'em': result = value.value * font_size From 4833bf69c8d9d278d66aef9e1e856f02122d42c8 Mon Sep 17 00:00:00 2001 From: aschmitz <29508+aschmitz@users.noreply.github.com> Date: Thu, 17 Mar 2022 00:17:22 -0500 Subject: [PATCH 2/2] change: use ratios for ex / ch sizes Based on review comments. Also modifies inline_box_verticality to use the cache rather than recalculate every time. --- weasyprint/css/__init__.py | 4 +-- weasyprint/css/computed_values.py | 45 +++++++++++++++---------------- weasyprint/layout/inline.py | 8 ++++-- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/weasyprint/css/__init__.py b/weasyprint/css/__init__.py index fb6b85217..8cae9e985 100644 --- a/weasyprint/css/__init__.py +++ b/weasyprint/css/__init__.py @@ -34,8 +34,8 @@ 'footnote-call', 'footnote-marker') DEFAULT_CACHE = { - 'length_ch': {}, - 'length_ex': {}, + 'ratio_ch': {}, + 'ratio_ex': {}, } diff --git a/weasyprint/css/computed_values.py b/weasyprint/css/computed_values.py index 517967ea6..6447e99dc 100644 --- a/weasyprint/css/computed_values.py +++ b/weasyprint/css/computed_values.py @@ -7,7 +7,7 @@ from ..logger import LOGGER from ..text.ffi import ffi, pango, units_to_double -from ..text.line_break import Layout, first_line_metrics, line_size +from ..text.line_break import Layout, first_line_metrics from ..urls import get_link_attribute from .properties import ( INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES, Dimension) @@ -178,10 +178,9 @@ def _resolve_var(computed, variable_name, default, parent_style): return computed_value -def _font_style_cache_key(style, font_size): +def _font_style_cache_key(style): return str(( style['font_family'], - font_size, style['font_style'], style['font_stretch'], style['font_weight'], @@ -338,31 +337,26 @@ def length(style, name, value, font_size=None, pixels_only=False): if font_size is None: font_size = style['font_size'] if unit == 'ex': - cache_key = _font_style_cache_key(style, None) + cache_key = _font_style_cache_key(style) - if cache_key in style.cache['length_ex']: - ratio = style.cache['length_ex'][cache_key] + if cache_key in style.cache['ratio_ex']: + ratio = style.cache['ratio_ex'][cache_key] else: - ratio = ex_ratio(style) - style.cache['length_ex'][cache_key] = ratio + ratio = _character_ratio(style, 'x') + style.cache['ratio_ex'][cache_key] = ratio result = value.value * font_size * ratio elif unit == 'ch': # TODO: use context to use @font-face fonts - cache_key = _font_style_cache_key(style, font_size) + cache_key = _font_style_cache_key(style) - if cache_key in style.cache['length_ch']: - logical_width = style.cache['length_ch'][cache_key] + if cache_key in style.cache['ratio_ch']: + ratio = style.cache['ratio_ch'][cache_key] else: - layout = Layout( - context=None, font_size=font_size, - style=style) - layout.set_text('0') - line, _ = layout.get_first_line() - logical_width, _ = line_size(line, style) - style.cache['length_ch'][cache_key] = logical_width - - result = value.value * logical_width + ratio = _character_ratio(style, '0') + style.cache['ratio_ch'][cache_key] = ratio + + result = value.value * font_size * ratio elif unit == 'em': result = value.value * font_size elif unit == 'rem': @@ -775,7 +769,7 @@ def strut_layout(style, context=None): return result -def ex_ratio(style): +def _character_ratio(style, character): """Return the ratio 1ex/font_size, according to given style.""" # TODO: use context to use @font-face fonts @@ -788,14 +782,17 @@ def ex_ratio(style): font_size = 1000 layout = Layout(context=None, font_size=font_size, style=style) - layout.set_text('x') + layout.set_text(character) line, _ = layout.get_first_line() ink_extents = ffi.new('PangoRectangle *') pango.pango_layout_line_get_extents(line, ink_extents, ffi.NULL) - height_above_baseline = units_to_double(ink_extents.y) + if character == 'x': + measure = units_to_double(ink_extents.y) + else: + measure = units_to_double(ink_extents.width) ffi.release(ink_extents) # Zero means some kind of failure, fallback is 0.5. # We round to try keeping exact values that were altered by Pango. - return round(-height_above_baseline / font_size, 5) or 0.5 + return round(measure / font_size, 5) or 0.5 diff --git a/weasyprint/layout/inline.py b/weasyprint/layout/inline.py index 36627e193..81770f5b3 100644 --- a/weasyprint/layout/inline.py +++ b/weasyprint/layout/inline.py @@ -4,7 +4,9 @@ from math import inf from ..css import computed_from_cascaded -from ..css.computed_values import ex_ratio, strut_layout +from ..css.computed_values import length as computed_length +from ..css.computed_values import strut_layout +from ..css.properties import Dimension from ..formatting_structure import boxes from ..text.line_break import can_break_text, create_layout, split_first_line from .absolute import AbsolutePlaceholder, absolute_layout @@ -1048,7 +1050,9 @@ def inline_box_verticality(box, top_bottom_subtrees, baseline_y): if vertical_align == 'baseline': child_baseline_y = baseline_y elif vertical_align == 'middle': - one_ex = box.style['font_size'] * ex_ratio(box.style) + one_ex = computed_length( + box.style, 'height', Dimension(1, 'em'), + box.style['font_size'], pixels_only=True) top = baseline_y - (one_ex + child.margin_height()) / 2 child_baseline_y = top + child.baseline elif vertical_align == 'text-top':