Source code for kiva.fonttools.font

# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
""" Defines the Kiva Font class and a utility method to parse free-form font
specification strings into Font instances.
"""
import copy
import warnings

from kiva.constants import (
    BOLD, DECORATIVE, DEFAULT, ITALIC, MODERN, NORMAL, ROMAN, SCRIPT, SWISS,
    TELETYPE, WEIGHT_BOLD, WEIGHT_EXTRABOLD, WEIGHT_EXTRAHEAVY,
    WEIGHT_EXTRALIGHT, WEIGHT_HEAVY, WEIGHT_LIGHT, WEIGHT_MEDIUM,
    WEIGHT_NORMAL, WEIGHT_SEMIBOLD, WEIGHT_THIN, bold_styles, italic_styles,
)
from kiva.fonttools._query import FontQuery
from kiva.fonttools.font_manager import default_font_manager

FAMILIES = {
    'default': DEFAULT,
    'cursive': SCRIPT,
    'decorative': DECORATIVE,
    'fantasy': DECORATIVE,
    'modern': MODERN,
    'monospace': MODERN,
    'roman': ROMAN,
    'sans-serif': SWISS,
    'script': SCRIPT,
    'serif': ROMAN,
    'swiss': SWISS,
    'teletype': TELETYPE,
    'typewriter': TELETYPE,
}
WEIGHTS = {
    'thin': WEIGHT_THIN,
    'extra-light': WEIGHT_EXTRALIGHT,
    'light': WEIGHT_LIGHT,
    'regular': WEIGHT_NORMAL,
    'medium': WEIGHT_MEDIUM,
    'semi-bold': WEIGHT_SEMIBOLD,
    'bold': WEIGHT_BOLD,
    'extra-bold': WEIGHT_EXTRABOLD,
    'heavy': WEIGHT_HEAVY,
    'extra-heavy': WEIGHT_EXTRAHEAVY
}
STYLES = {
    'italic': ITALIC,
    'oblique': ITALIC,
}
DECORATIONS = {'underline'}
NOISE = {'pt', 'point', 'px', 'family'}


[docs]class FontParseError(ValueError): """An exception raised when font parsing fails.""" pass
[docs]def simple_parser(description): """An extremely simple font description parser. The parser is simple, and works by splitting the description on whitespace and examining each resulting token for understood terms: Size The first numeric term is treated as the font size. Weight The following weight terms are accepted: 'thin', 'extra-light', 'light', 'regular', 'medium', 'semi-bold', 'bold', 'extra-bold', 'heavy', 'extra-heavy'. Style The following style terms are accepted: 'italic', 'oblique'. Decorations The following decoration term is accepted: 'underline' Generic Families The following generic family terms are accepted: 'default', 'cursive', 'decorative', 'fantasy', 'modern', 'monospace', 'roman', 'sans-serif', 'script', 'serif', 'swiss', 'teletype', 'typewriter'. In addition, the parser ignores the terms 'pt', 'point', 'px', and 'family'. Any remaining terms are combined into the typeface name. There is no expected order to the terms. This parser is roughly compatible with the various ad-hoc parsers in TraitsUI and Kiva, allowing for the slight differences between them and adding support for additional options supported by Pyface fonts, such as stretch and variants. Parameters ---------- description : str The font description to be parsed. Returns ------- properties : dict Font properties suitable for use in creating a Pyface Font. Notes ----- This is not a particularly good parser, as it will fail to properly parse something like "10 pt times new roman" or "14 pt computer modern" since they have generic font names as part of the font face name. This is derived from Pyface's equivalent simple_parser. Eventually both will be replaced by better parsers that can parse something closer to a CSS font definition. """ face = [] family = DEFAULT size = None weight = WEIGHT_NORMAL style = NORMAL underline = False for word in description.split(): lower_word = word.casefold() if lower_word in NOISE: continue elif lower_word in FAMILIES: family = FAMILIES[lower_word] elif lower_word in WEIGHTS: weight = WEIGHTS[lower_word] elif lower_word in STYLES: style = STYLES[lower_word] elif lower_word in DECORATIONS: underline = True else: if size is None: try: size = int(lower_word) continue except ValueError: pass face.append(word) face_name = " ".join(face) if size is None: size = 10 return { 'face_name': face_name, 'size': size, 'family': family, 'weight': weight, 'style': style, 'underline': underline, }
[docs]def str_to_font(fontspec, parser=simple_parser): """ Converts a string specification of a font into a Font instance. string specifications are of the form: "modern 12", "9 roman italic", and so on. """ font_properties = parser(fontspec) return Font(**font_properties)
[docs]class Font(object): """ Font class for device independent font specification. It is primarily based on wxPython, but looks to be similar to the needs of Mac OS X, etc. The family defaults to SWISS so that font rotation will work correctly under wxPython. Revisit as we get more platforms defined. """ # Maps the constants for font families to names to use when searching for # fonts. familymap = { DEFAULT: "serif", SWISS: "sans-serif", ROMAN: "serif", MODERN: "sans-serif", DECORATIVE: "fantasy", SCRIPT: "cursive", TELETYPE: "monospace", } def __init__(self, face_name="", size=12, family=SWISS, weight=WEIGHT_NORMAL, style=NORMAL, underline=0, encoding=DEFAULT): if not isinstance(face_name, str): raise RuntimeError( f"Expected face name to be a str, got {face_name!r}") if not isinstance(size, int): raise RuntimeError( f"Expected size to be an int, got {size!r}") if not isinstance(family, int): raise RuntimeError( f"Expected family to be an int, got {family!r}") if not isinstance(weight, int): raise RuntimeError( f"Expected weight to be an int, got {weight!r}") if not isinstance(style, int): raise RuntimeError( f"Expected style to be an int, got {style!r}") if not isinstance(underline, int): raise RuntimeError( f"Expected underline to be a int, got {underline!r}") if not isinstance(encoding, int): raise RuntimeError( f"Expected encoding to be an int, got {encoding!r}") self.face_name = face_name self.size = size self.family = family self.weight = weight self.style = style self.underline = bool(underline) self.encoding = encoding # correct the style and weight if needed (can be removed in Enable 7) self.weight = self._get_weight() self.style = style & ~BOLD
[docs] def findfont(self, language=None): """ Returns the file name and face index of the font that most closely matches our font properties. Parameters ---------- language : str [optional] If provided, attempt to find a font which supports ``language``. """ query = self._make_font_query() if language is not None: spec = default_font_manager().find_fallback(query, language) if spec is not None: return spec return default_font_manager().findfont(query)
[docs] def findfontname(self, language=None): """ Returns the name of the font that most closely matches our font properties. Parameters ---------- language : str [optional] If provided, attempt to find a font which supports ``language``. """ query = self._make_font_query() if language is not None: spec = default_font_manager().find_fallback(query, language) if spec is not None: return spec.family return query.get_name()
[docs] def is_bold(self): """Is the font considered bold or not? This is a convenience method for backends which don't fully support font weights. We consider a font to be bold if its weight is more than medium. """ weight = self._get_weight() return (weight > WEIGHT_MEDIUM)
def _make_font_query(self): """ Returns a FontQuery object that encapsulates our font properties. """ weight = self._get_weight() if self.style in italic_styles: style = "italic" else: style = "normal" query = FontQuery( family=self.familymap[self.family], style=style, weight=weight, size=self.size, ) if self.face_name != "": query.set_name(self.face_name) return query def _get_weight(self): """Get a corrected weight value from the font. Note: this is a temporary method that will be removed in Enable 7. """ if self.weight == BOLD: warnings.warn( "Use WEIGHT_BOLD instead of BOLD for Font.weight", DeprecationWarning ) return WEIGHT_BOLD elif self.style in bold_styles: warnings.warn( "Set Font.weight to WEIGHT_BOLD instead of Font.style to " "BOLD or BOLD_STYLE", DeprecationWarning ) # if weight is default, and style is bold, report as bold if self.weight == WEIGHT_NORMAL: return WEIGHT_BOLD return self.weight def _get_name(self): return self.face_name def _set_name(self, val): self.face_name = val name = property(_get_name, _set_name)
[docs] def copy(self): """ Returns a copy of the font object. """ return copy.deepcopy(self)
def __eq__(self, other): try: return (self.family == other.family and self.face_name == other.face_name and self.size == other.size and self.weight == other.weight and self.style == other.style and self.underline == other.underline and self.encoding == other.encoding) except AttributeError: pass return False def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return ( f"Font(face_name='{self.face_name}', size={self.size}, " f"family={self.family}, weight={self.weight}, style={self.style}, " f"underline={self.underline}, encoding={self.encoding})" )