# 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