Source code for foundrytools.app.fix_monospace

# pylint: disable=import-outside-toplevel
from collections import Counter

from foundrytools import Font


[docs] class FixMonospaceError(Exception): """Raised when an error occurs in the fix_monospace method."""
# Copied from fontbakery/profiles/shared_conditions.py
[docs] def _get_glyph_metrics_stats(font: Font) -> dict[str, bool | int]: """ Returns a dict containing whether the font seems_monospaced, what's the maximum glyph width and what's the most common width. For a font to be considered monospaced, if at least 80% of ASCII characters have glyphs, then at least 80% of those must have the same width, otherwise all glyphs of printable characters must have one of two widths or be zero-width. :param font: The TTFont object. :type font: TTFont :return: A dictionary containing the metrics. :rtype: dict[str, Union[bool, int]] """ glyph_metrics = font.t_hmtx.table.metrics # NOTE: `range(a, b)` includes `a` and does not include `b`. # Here we don't include 0-31 as well as 127 # because these are control characters. ascii_glyph_names = [ font.ttfont.getBestCmap()[c] for c in range(32, 127) if c in font.ttfont.getBestCmap() ] if len(ascii_glyph_names) > 0.8 * (127 - 32): ascii_widths = [ adv for name, (adv, lsb) in glyph_metrics.items() if name in ascii_glyph_names and adv != 0 ] ascii_width_count = Counter(ascii_widths) ascii_most_common_width = ascii_width_count.most_common(1)[0][1] seems_monospaced = ascii_most_common_width >= len(ascii_widths) * 0.8 else: from fontTools import unicodedata # Collect relevant glyphs. relevant_glyph_names = set() # Add character glyphs that are in one of these categories: # Letter, Mark, Number, Punctuation, Symbol, Space_Separator. # This excludes Line_Separator, Paragraph_Separator and Control. for value, name in font.ttfont.getBestCmap().items(): if unicodedata.category(chr(value)).startswith(("L", "M", "N", "P", "S", "Zs")): relevant_glyph_names.add(name) # Remove character glyphs that are mark glyphs. gdef = font.t_gdef.table if gdef and gdef.table.GlyphClassDef: marks = {name for name, c in gdef.table.GlyphClassDef.classDefs.items() if c == 3} relevant_glyph_names.difference_update(marks) widths = sorted( { adv for name, (adv, lsb) in glyph_metrics.items() if name in relevant_glyph_names and adv != 0 } ) seems_monospaced = len(widths) <= 2 width_max = max(adv for k, (adv, lsb) in glyph_metrics.items()) most_common_width = Counter([g for g in glyph_metrics.values() if g[0] != 0]).most_common(1)[0][ 0 ][0] return { "seems_monospaced": seems_monospaced, "width_max": width_max, "most_common_width": most_common_width, }
[docs] def run(font: Font) -> bool: """ Fix the monospace attribute of a font. :param font: The Font. :type font: Font :return: Whether the font was modified. """ try: glyph_metrics = _get_glyph_metrics_stats(font) seems_monospaced = glyph_metrics["seems_monospaced"] width_max = glyph_metrics["width_max"] if seems_monospaced: font.t_post.fixed_pitch = True font.t_os_2.table.panose.bFamilyType = 2 font.t_os_2.table.panose.bProportion = 9 font.t_hhea.advance_width_max = width_max modified = font.t_os_2.is_modified or font.t_post.is_modified or font.t_hhea.is_modified if font.is_ps and not font.t_cff_.top_dict.isFixedPitch: font.t_cff_.top_dict.isFixedPitch = True modified = True return modified return False except Exception as e: raise FixMonospaceError(f"{e}") from e