[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/plotdevice/plotdevice/main/plotdevice/lib/foundry.py [Back]  [Original]

# encoding: utf-8
import os
import re
import sys
import objc
import difflib
from operator import itemgetter, attrgetter
from collections import namedtuple, OrderedDict as odict, defaultdict as ddict
from .cocoa import *
from ..util import numlike

from plotdevice import DeviceError

__all__ = ["font_family", "font_encoding", "font_face", "best_face",
           "family_names", "family_members", "family_name", "standardized", "sanitized",
           "fontspec", "line_metrics", "layout_metrics", "aat_attrs", "aat_features"
           ]

Face = namedtuple('Face', ['family', 'psname', 'weight','wgt', 'width','wid', 'variant', 'italic', 'ascent', 'descent'])
Slug = namedtuple("Slug", ["frame", "bounds", "baseline", "span"])
Vandercook = objc.lookUpClass('Vandercook')

# introspection methods for postscript names & families

def font_exists(psname):
    """Return whether a font exists based on psname"""
    return psname in LIBRARY.font_names

def font_family(psname):
    """Return family name given a psname"""
    return LIBRARY.parent_fam(psname)

def font_encoding(psname):
    """Return encoding name given a psname"""
    return LIBRARY.encoding(psname)

def font_face(psname):
    """Return a Face tuple given a psname"""
    fam = LIBRARY.parent_fam(psname)
    for face in LIBRARY.list_fam(fam):
        if face.psname == psname:
            return face
    notfound = 'Font: no matches for Postscript name "%s"'%basis
    raise DeviceError(notfound)

def family_names():
    """A list of all families installed on the machine"""
    return LIBRARY.fam_names

def family_members(famname, names=False):
    """Returns a sorted list of Face tuples for the fonts in a family"""
    return LIBRARY.list_fam(famname, names)

# fuzzy matchers

def family_name(word):
    """Returns a valid family name if the arg fuzzy matches any existing families"""
    matched = LIBRARY.best_fam(word)
    if isinstance(matched, Exception):
        raise matched
    return matched

def best_face(spec):
    """Returns the PostScript name of the best match for a given fontspec"""
    return LIBRARY.best_face(spec)

# typography arg validators/standardizers

def fontspec(*args, **kwargs):
    """Validate a set of font/stylesheet/text args and return a canonicalized
    font-specification dictionary (which can then be passed to best_face)"""
    spec = font_axes(*args, **kwargs) # parse nsfont-related args & kwargs
    spec.update(line_metrics(kwargs)) # incorporate spacing & alignment kwargs
    spec.update(aat_features(kwargs)) # support AAT/OpenType features
    return spec

def font_axes(*args, **kwargs):
    # start with kwarg values as the canonical settings
    _canon = ('family','size','weight','italic','width','variant','fontsize')
    spec = {k:v for k,v in kwargs.items() if k in _canon}
    basis = kwargs.get('face', kwargs.get('fontname'))

    # be backward compatible with the old arg names
    if 'fontsize' in spec:
        spec.setdefault('size', spec.pop('fontsize'))

    # validate the nsfont-specific spec valuce
    if spec:
        if 'italic' in spec:
            spec['italic'] = bool(spec['italic'])
        if 'family' in spec:
            spec['family'] = family_name(spec['family'])

        # validate the weight and width args (if any)
        if 'weight' in spec and not weighty(spec['weight']):
            print('Font: unknown weight "%s"' % spec.pop('weight'))

        if spec.get('width') is not None and not widthy(spec['width']):
            print('Font: unknown width "%s"' % spec.pop('width'))

    # look for a postscript name passed as `face` or `fontname` and validate it
    if basis and not font_exists(basis):
        notfound = 'Font: no matches for Postscript name "%s"'%basis
        raise DeviceError(notfound)
    elif basis:
        # if the psname exists, inherit its attributes
        face = font_face(basis)
        for axis in ('family','weight','width','variant','italic'):
            spec.setdefault(axis, getattr(face, axis))

    # search the positional args for either name/size or a Font object
    # we want the kwargs to have higher priority, so setdefault everywhere...
    for item in args:
        if hasattr(item, '_spec'):
            # existing Font object
            for k,v in item._spec.items():
                spec.setdefault(k,v)
        elif isinstance(item, str):
            # name-like values
            if fammy(item):
                spec.setdefault('family', family_name(item))
            elif widthy(item):
                spec.setdefault('width', item)
            elif weighty(item):
                spec.setdefault('weight', item)
            else:
                family_name(item) # raise an exception suggesting family names
        elif numlike(item) and 'size' not in kwargs:
            spec['size'] = float(item)
    return spec

def layout_metrics(spec):
    # start with kwarg values as the canonical settings
    _canon = ('align','leading','indent','margin','spacing','hyphenate','lineheight')
    spec = {k:v for k,v in spec.items() if k in _canon}

    # validate alignment
    if spec.get('align','left') not in ('left','right','center','justify'):
        chaoticneutral = 'Text alignment must be LEFT, RIGHT, CENTER, or JUSTIFY'
        raise DeviceError(chaoticneutral)

    # floatify dimensions and hyphenation (mapping bools to 0/1)
    for attr in 'leading', 'indent', 'hyphenate':
        if attr in spec:
            spec[attr] = float(spec[attr])

    for attr in 'margin', 'spacing':
        if attr in spec:
            a,b = 0,0
            if isinstance(spec[attr], (list, tuple)):
                a, b = spec[attr]
            elif spec[attr] is not None:
                a = float(spec[attr])
            spec[attr] = (a, b)

    # be backward compatible with the old arg names
    if 'lineheight' in spec:
        spec.setdefault('leading', spec.pop('lineheight'))
    return spec

def line_metrics(spec):
    # start with kwarg values as the canonical settings
    _canon = ('size','align','leading','tracking','indent','margin','spacing','hyphenate')
    spec = {k:v for k,v in spec.items() if k in _canon}

    # validate alignment
    if spec.get('align','left') not in ('left','right','center','justify'):
        chaoticneutral = 'Text alignment must be LEFT, RIGHT, CENTER, or JUSTIFY'
        raise DeviceError(chaoticneutral)

    # floatify dimensions and hyphenation (mapping bools to 0/1)
    for attr in 'size', 'leading', 'tracking', 'indent', 'hyphenate':
        if attr in spec:
            spec[attr] = float(spec[attr])

    for attr in 'margin', 'spacing':
        if attr in spec:
            a,b = 0,0
            if isinstance(spec[attr], (list, tuple)):
                a, b = spec[attr]
            elif spec[attr] is not None:
                a = float(spec[attr])
            spec[attr] = (a, b)

    # be backward compatible with the old arg names
    if 'lineheight' in spec:
        spec.setdefault('leading', spec['lineheight'])
    return spec

def aat_features(spec):
    """Validate features in a Font spec and normalize settings values"""
    features = {}

    for k,v in spec.items():
        if k=='ss':
            # unpack & validate the ss arg (which might be a sequence of ints)
            ss = (int(v),) if numlike(v) or isinstance(v, bool) else v
            if ss is None or ss==(0,):
                features['ss'] = tuple()
            elif ss is all:
                features['ss'] = tuple(range(1,21))
            else:
                try:
                    if not all([numlike(val) and 0

Web Proxy Viewer  |  New URL  |  Original Page