Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
enable / fonttools / _scan_parse.py
Size: Mime:
# (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!
"""
####### NOTE #######
This is based heavily on matplotlib's font_manager.py SVN rev 8713
(git commit f8e4c6ce2408044bc89b78b3c72e54deb1999fb5),
but has been modified quite a bit in the decade since it was copied.
####################
"""
import logging
import os

from fontTools.afmLib import AFM
from fontTools.ttLib import TTCollection, TTFont, TTLibError

from kiva.fonttools._constants import stretch_dict, weight_dict
from kiva.fonttools._database import FontDatabase, FontEntry
from kiva.fonttools._util import get_ttf_prop_dict, weight_as_number

logger = logging.getLogger(__name__)
# Error message when fonts fail to load
_FONT_ENTRY_ERR_MSG = "Could not convert font to FontEntry for file %s"


def create_font_database(fontfiles, fontext="ttf"):
    """ Creates a :class:`FontDatabase` instance from a list of provided
    filepaths.

    The default is to locate TrueType fonts. An AFM database can optionally be
    created.
    """
    # Use a set() to filter out files which were already scanned
    seen = set()

    fontlist = []
    for fpath in fontfiles:
        logger.debug("create_font_database %s", fpath)
        fname = os.path.basename(fpath)
        if fname in seen:
            continue

        seen.add(fname)
        if fontext == "afm":
            fontlist.extend(_build_afm_entries(fpath))
        else:
            fontlist.extend(_build_ttf_entries(fpath))

    return FontDatabase(fontlist)


def update_font_database(database, fontfiles, fontext="ttf"):
    """ Add additional font entries to an existing :class:`FontDatabase`
    instance.
    """
    fontlist = []
    for fpath in fontfiles:
        if fontext == "afm":
            fontlist.extend(_build_afm_entries(fpath))
        else:
            fontlist.extend(_build_ttf_entries(fpath))

    database.add_fonts(fontlist)


# ----------------------------------------------------------------------------
# utility funcs

def _build_afm_entries(fpath):
    """ Given the path to an AFM file, return a list of one :class:`FontEntry`
    instance or an empty list if there was an error.
    """
    try:
        font = AFM(fpath)
    except Exception:
        logger.error(f"Could not parse font file {fpath}", exc_info=True)
        return []

    try:
        return [_afm_font_property(fpath, font)]
    except Exception:
        logger.error(_FONT_ENTRY_ERR_MSG, fpath, exc_info=True)

    return []


def _build_ttf_entries(fpath):
    """ Given the path to a TTF/TTC file, return a list of :class:`FontEntry`
    instances.
    """
    entries = []

    ext = os.path.splitext(fpath)[-1]
    try:
        with open(fpath, "rb") as fp:
            if ext.lower() == ".ttc":
                collection = TTCollection(fp)
                try:
                    for idx, font in enumerate(collection.fonts):
                        entries.append(_ttf_font_property(fpath, font, idx))
                except Exception:
                    logger.error(_FONT_ENTRY_ERR_MSG, fpath, exc_info=True)
            else:
                font = TTFont(fp)
                try:
                    entries.append(_ttf_font_property(fpath, font))
                except Exception:
                    logger.error(_FONT_ENTRY_ERR_MSG, fpath, exc_info=True)
    except (RuntimeError, TTLibError):
        logger.error(f"Could not open font file {fpath}", exc_info=True)
    except UnicodeError:
        logger.error(f"Cannot handle unicode file: {fpath}", exc_info=True)

    return entries


def _afm_font_property(fpath, font):
    """ A function for populating a :class:`FontEntry` instance by
    extracting information from the AFM font file.

    *font* is a class:`AFM` instance.
    """
    family = font.FamilyName
    fontname = font.FullName.lower()

    #  Styles are: italic, oblique, and normal (default)
    if float(font.ItalicAngle) != 0.0 or family.lower().find("italic") >= 0:
        style = "italic"
    elif family.lower().find("oblique") >= 0:
        style = "oblique"
    else:
        style = "normal"

    #  Variants are: small-caps and normal (default)
    # NOTE: Not sure how many fonts actually have these strings in their family
    variant = "normal"
    for value in ("capitals", "small-caps", "smallcaps"):
        if value in family.lower():
            variant = "small-caps"
            break

    #  Weights are: 100, 200, 300, 400 (normal: default), 500 (medium),
    #    600 (semibold, demibold), 700 (bold), 800 (heavy), 900 (black)
    #    lighter and bolder are also allowed.
    weight = weight_as_number(font.Weight.lower())

    #  Stretch can be absolute and relative
    #  Absolute stretches are: ultra-condensed, extra-condensed, condensed,
    #    semi-condensed, normal, semi-expanded, expanded, extra-expanded,
    #    and ultra-expanded.
    #  Relative stretches are: wider, narrower
    #  Child value is: inherit
    if fontname.find("demi cond") >= 0:
        stretch = "semi-condensed"
    elif (fontname.find("narrow") >= 0
            or fontname.find("condensed") >= 0
            or fontname.find("cond") >= 0):
        stretch = "condensed"
    elif fontname.find("wide") >= 0 or fontname.find("expanded") >= 0:
        stretch = "expanded"
    else:
        stretch = "normal"

    #  All AFM fonts are scalable.
    size = "scalable"

    return FontEntry(
        fname=fpath,
        family=family,
        style=style,
        variant=variant,
        weight=weight,
        stretch=stretch,
        size=size,
    )


def _ttf_font_property(fpath, font, face_index=0):
    """ A function for populating the :class:`FontEntry` by extracting
    information from the TrueType font file.

    *font* is a :class:`TTFont` instance.
    """
    props = get_ttf_prop_dict(font)
    family = props.get("family")
    if family is None:
        raise KeyError("No family could be found for: {}".format(fpath))

    # Some properties
    full_name = props.get("full_name", "").lower()
    style_prop = props.get("style", "").lower()
    if style_prop == "":
        # For backwards compatibility with previous parsing behavior
        style_prop = full_name

    # Styles are: italic, oblique, and normal (default)
    # We prefer to get the information from the "slope" property if it is
    # available, as that is more accurate.
    if "slope" in props:
        style = props["slope"]
    elif "oblique" in style_prop:
        style = "oblique"
    elif "italic" in style_prop:
        style = "italic"
    else:
        style = "normal"

    # Variants are: small-caps and normal (default)
    # NOTE: Small caps is usually handled through alternative mappings of the
    # characters to glyphs eg. via "smcp" feature.  However some older fonts
    # may indicate it via the name, in which case they may be preferred.
    variant = "normal"
    for value in ("capitals", "small-caps", "smallcaps"):
        if value in family.lower():
            variant = "small-caps"
            break

    #  Weights are: 100, 200, 300, 400 (normal: default), 500 (medium),
    #    600 (semibold, demibold), 700 (bold), 800 (heavy), 900 (black)
    #    lighter and bolder are also allowed.
    # We prefer weight values from the OS/2 table, if available, otherwise
    # infer from the "name" table's style
    weight = props.get("weight")
    if not weight:
        for w in weight_dict.keys():
            if w in style_prop:
                weight = w
                break
        if not weight:
            weight = 400
    weight = weight_as_number(weight)

    #  Stretch can be absolute and relative
    #  Absolute stretches are: ultra-condensed, extra-condensed, condensed,
    #    semi-condensed, normal, semi-expanded, expanded, extra-expanded,
    #    and ultra-expanded.
    #  Relative stretches are: wider, narrower
    #  Child value is: inherit
    if "stretch" in props:
        stretch = props["stretch"]
    else:
        for stretch in stretch_dict:
            if stretch in full_name:
                break
        else:
            if "demi cond" in full_name:
                stretch = "semi-condensed"
            elif ("narrow" in full_name
                    or "condensed" in full_name
                    or "cond" in full_name):
                stretch = "condensed"
            elif "wide" in full_name or "expanded" in full_name:
                stretch = "expanded"
            else:
                stretch = "normal"

    # TrueType and OpenType fonts are always scalable
    size = "scalable"

    return FontEntry(
        fname=fpath,
        family=family,
        style=style,
        variant=variant,
        weight=weight,
        stretch=stretch,
        size=size,
        face_index=face_index,
        languages=props.get("languages", None),
    )