Skip to content

Commit

Permalink
Split FontProperties out into its own module (#701)
Browse files Browse the repository at this point in the history
  • Loading branch information
jwiggins authored Mar 11, 2021
1 parent 14e8440 commit 1970b3e
Show file tree
Hide file tree
Showing 3 changed files with 404 additions and 0 deletions.
25 changes: 25 additions & 0 deletions kiva/fonttools/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,31 @@
#
# Thanks for using Enthought open source!

font_scalings = {
"xx-small": 0.579,
"x-small": 0.694,
"small": 0.833,
"medium": 1.0,
"large": 1.200,
"x-large": 1.440,
"xx-large": 1.728,
"larger": 1.2,
"smaller": 0.833,
None: 1.0,
}

stretch_dict = {
"ultra-condensed": 100,
"extra-condensed": 200,
"condensed": 300,
"semi-condensed": 400,
"normal": 500,
"semi-expanded": 600,
"expanded": 700,
"extra-expanded": 800,
"ultra-expanded": 900,
}

weight_dict = {
"ultralight": 100,
"light": 200,
Expand Down
262 changes: 262 additions & 0 deletions kiva/fonttools/_font_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# (C) Copyright 2005-2021 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!
from fontTools.afmLib import AFM
from fontTools.ttLib import TTFont

from kiva.fonttools._constants import font_scalings, stretch_dict, weight_dict
from kiva.fonttools._util import get_ttf_prop_dict
from kiva.fonttools.font_manager import default_font_manager


class FontProperties(object):
""" A class for storing and manipulating font properties.
The font properties are those described in the `W3C Cascading
Style Sheet, Level 1
<http://www.w3.org/TR/1998/REC-CSS2-19980512/>`_ font
specification. The six properties are:
- family: A list of font names in decreasing order of priority.
The items may include a generic font family name, either
'serif', 'sans-serif', 'cursive', 'fantasy', or 'monospace'.
- style: Either 'normal', 'italic' or 'oblique'.
- variant: Either 'normal' or 'small-caps'.
- stretch: A numeric value in the range 0-1000 or one of
'ultra-condensed', 'extra-condensed', 'condensed',
'semi-condensed', 'normal', 'semi-expanded', 'expanded',
'extra-expanded' or 'ultra-expanded'
- weight: A numeric value in the range 0-1000 or one of
'ultralight', 'light', 'normal', 'regular', 'book', 'medium',
'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy',
'extra bold', 'black'
- size: Either an relative value of 'xx-small', 'x-small',
'small', 'medium', 'large', 'x-large', 'xx-large' or an
absolute font size, e.g. 12
Alternatively, a font may be specified using an absolute path to a
.ttf file, by using the *fname* kwarg.
The preferred usage of font sizes is to use the relative values,
e.g. 'large', instead of absolute font sizes, e.g. 12. This
approach allows all text sizes to be made larger or smaller based
on the font manager's default font size.
"""
def __init__(self, family=None, style=None, variant=None, weight=None,
stretch=None, size=None, fname=None, _init=None):
# if fname is set, it's a hardcoded filename to use
# _init is used only by copy()

self._family = None
self._slant = None
self._variant = None
self._weight = None
self._stretch = None
self._size = None
self._file = None

# This is used only by copy()
if _init is not None:
self.__dict__.update(_init.__dict__)
return

self.set_family(family)
self.set_style(style)
self.set_variant(variant)
self.set_weight(weight)
self.set_stretch(stretch)
self.set_file(fname)
self.set_size(size)

def __hash__(self):
lst = [(k, getattr(self, "get" + k)()) for k in sorted(self.__dict__)]
return hash(repr(lst))

def __str__(self):
attrs = (
self._family, self._slant, self._variant, self._weight,
self._stretch, self._size,
)
return str(attrs)

def get_family(self):
""" Return a list of font names that comprise the font family.
"""
return self._family

def get_name(self):
""" Return the name of the font that best matches the font properties.
"""
spec = default_font_manager().findfont(self)
if spec.filename.endswith(".afm"):
return AFM().FamilyName

prop_dict = get_ttf_prop_dict(
TTFont(spec.filename, fontNumber=spec.face_index)
)
return prop_dict["family"]

def get_style(self):
""" Return the font style.
Values are: 'normal', 'italic' or 'oblique'.
"""
return self._slant

get_slant = get_style

def get_variant(self):
""" Return the font variant.
Values are: 'normal' or 'small-caps'.
"""
return self._variant

def get_weight(self):
""" Set the font weight.
Options are: A numeric value in the range 0-1000 or one of 'light',
'normal', 'regular', 'book', 'medium', 'roman', 'semibold', 'demibold',
'demi', 'bold', 'heavy', 'extra bold', 'black'
"""
return self._weight

def get_stretch(self):
""" Return the font stretch or width.
Options are: 'ultra-condensed', 'extra-condensed', 'condensed',
'semi-condensed', 'normal', 'semi-expanded', 'expanded',
'extra-expanded', 'ultra-expanded'.
"""
return self._stretch

def get_size(self):
""" Return the font size.
"""
return self._size

def get_size_in_points(self):
if self._size is not None:
try:
return float(self._size)
except ValueError:
pass
default_size = default_font_manager().get_default_size()
return default_size * font_scalings.get(self._size)

def get_file(self):
""" Return the filename of the associated font.
"""
return self._file

def set_family(self, family):
""" Change the font family.
May be either an alias (generic name is CSS parlance), such as:
'serif', 'sans-serif', 'cursive', 'fantasy', or 'monospace', or
a real font name.
"""
if family is None:
self._family = None
else:
if isinstance(family, bytes):
family = [family.decode("utf8")]
elif isinstance(family, str):
family = [family]
self._family = family

set_name = set_family

def set_style(self, style):
""" Set the font style.
Values are: 'normal', 'italic' or 'oblique'.
"""
if style not in ("normal", "italic", "oblique", None):
raise ValueError("style must be normal, italic or oblique")
self._slant = style

set_slant = set_style

def set_variant(self, variant):
""" Set the font variant.
Values are: 'normal' or 'small-caps'.
"""
if variant not in ("normal", "small-caps", None):
raise ValueError("variant must be normal or small-caps")
self._variant = variant

def set_weight(self, weight):
""" Set the font weight.
May be either a numeric value in the range 0-1000 or one of
'ultralight', 'light', 'normal', 'regular', 'book', 'medium', 'roman',
'semibold', 'demibold', 'demi', 'bold', 'heavy', 'extra bold', 'black'.
"""
if weight is not None:
try:
weight = int(weight)
if weight < 0 or weight > 1000:
raise ValueError()
except ValueError:
if weight not in weight_dict:
raise ValueError("weight is invalid")
self._weight = weight

def set_stretch(self, stretch):
""" Set the font stretch or width.
Options are: 'ultra-condensed', 'extra-condensed', 'condensed',
'semi-condensed', 'normal', 'semi-expanded', 'expanded',
'extra-expanded' or 'ultra-expanded', or a numeric value in the
range 0-1000.
"""
if stretch is not None:
try:
stretch = int(stretch)
if stretch < 0 or stretch > 1000:
raise ValueError()
except ValueError:
if stretch not in stretch_dict:
raise ValueError("stretch is invalid")
else:
stretch = 500
self._stretch = stretch

def set_size(self, size):
""" Set the font size.
Either an relative value of 'xx-small', 'x-small', 'small', 'medium',
'large', 'x-large', 'xx-large' or an absolute font size, e.g. 12.
"""
if size is not None:
try:
size = float(size)
except ValueError:
if size is not None and size not in font_scalings:
raise ValueError("size is invalid")
self._size = size

def set_file(self, file):
""" Set the filename of the fontfile to use.
In this case, all other properties will be ignored.
"""
self._file = file

def copy(self):
""" Return a deep copy of self
"""
return FontProperties(_init=self)
Loading

0 comments on commit 1970b3e

Please sign in to comment.