import os
import glob
[docs]
def OpenFonts(directory=None, showInterface=True, fileExtensions=None):
"""
Open all fonts with the given **fileExtensions** located in
**directory**. If **directory** is ``None``, a dialog for
selecting a directory will be opened. **directory** may also
be a list of directories. If **showInterface** is ``False``,
the font should be opened without graphical interface. The default
for **showInterface** is ``True``.
The fonts are located within the directory using the `glob`
<https://docs.python.org/library/glob.html>`_ module. The
patterns are created with ``os.path.join(glob, "*" + fileExtension)``
for every file extension in ``fileExtensions``. If ``fileExtensions``
if ``None`` the environment will use its default fileExtensions.
::
from fontParts.world import *
fonts = OpenFonts()
fonts = OpenFonts(showInterface=False)
"""
from fontParts.ui import GetFileOrFolder
if fileExtensions is None:
fileExtensions = dispatcher["OpenFontsFileExtensions"]
if isinstance(directory, str):
directories = [directory]
elif directory is None:
directories = GetFileOrFolder(allowsMultipleSelection=True)
else:
directories = directory
if directories:
globPatterns = []
for directory in directories:
if os.path.splitext(directory)[-1] in fileExtensions:
globPatterns.append(directory)
elif not os.path.isdir(directory):
pass
else:
for ext in fileExtensions:
globPatterns.append(os.path.join(directory, "*" + ext))
paths = []
for pattern in globPatterns:
paths.extend(glob.glob(pattern))
for path in paths:
yield OpenFont(path, showInterface=showInterface)
[docs]
def OpenFont(path, showInterface=True):
"""
Open font located at **path**. If **showInterface**
is ``False``, the font should be opened without
graphical interface. The default for **showInterface**
is ``True``.
::
from fontParts.world import *
font = OpenFont("/path/to/my/font.ufo")
font = OpenFont("/path/to/my/font.ufo", showInterface=False)
"""
return dispatcher["OpenFont"](pathOrObject=path, showInterface=showInterface)
[docs]
def NewFont(familyName=None, styleName=None, showInterface=True):
"""
Create a new font. **familyName** will be assigned
to ``font.info.familyName`` and **styleName**
will be assigned to ``font.info.styleName``. These
are optional and default to ``None``. If **showInterface**
is ``False``, the font should be created without
graphical interface. The default for **showInterface**
is ``True``.
::
from fontParts.world import *
font = NewFont()
font = NewFont(familyName="My Family", styleName="My Style")
font = NewFont(showInterface=False)
"""
return dispatcher["NewFont"](familyName=familyName, styleName=styleName,
showInterface=showInterface)
[docs]
def CurrentFont():
"""
Get the "current" font.
"""
return dispatcher["CurrentFont"]()
[docs]
def CurrentGlyph():
"""
Get the "current" glyph from :func:`CurrentFont`.
::
from fontParts.world import *
glyph = CurrentGlyph()
"""
return dispatcher["CurrentGlyph"]()
[docs]
def CurrentLayer():
"""
Get the "current" layer from :func:`CurrentGlyph`.
::
from fontParts.world import *
layer = CurrentLayer()
"""
return dispatcher["CurrentLayer"]()
[docs]
def CurrentContours():
"""
Get the "currently" selected contours from :func:`CurrentGlyph`.
::
from fontParts.world import *
contours = CurrentContours()
This returns an immutable list, even when nothing is selected.
"""
return dispatcher["CurrentContours"]()
def _defaultCurrentContours():
glyph = CurrentGlyph()
if glyph is None:
return ()
return glyph.selectedContours
[docs]
def CurrentSegments():
"""
Get the "currently" selected segments from :func:`CurrentContours`.
::
from fontParts.world import *
segments = CurrentSegments()
This returns an immutable list, even when nothing is selected.
"""
return dispatcher["CurrentSegments"]()
def _defaultCurrentSegments():
glyph = CurrentGlyph()
if glyph is None:
return ()
segments = []
for contour in glyph.selectedContours:
segments.extend(contour.selectedSegments)
return tuple(segments)
[docs]
def CurrentPoints():
"""
Get the "currently" selected points from :func:`CurrentContours`.
::
from fontParts.world import *
points = CurrentPoints()
This returns an immutable list, even when nothing is selected.
"""
return dispatcher["CurrentPoints"]()
def _defaultCurrentPoints():
glyph = CurrentGlyph()
if glyph is None:
return ()
points = []
for contour in glyph.selectedContours:
points.extend(contour.selectedPoints)
return tuple(points)
[docs]
def CurrentComponents():
"""
Get the "currently" selected components from :func:`CurrentGlyph`.
::
from fontParts.world import *
components = CurrentComponents()
This returns an immutable list, even when nothing is selected.
"""
return dispatcher["CurrentComponents"]()
def _defaultCurrentComponents():
glyph = CurrentGlyph()
if glyph is None:
return ()
return glyph.selectedComponents
[docs]
def CurrentAnchors():
"""
Get the "currently" selected anchors from :func:`CurrentGlyph`.
::
from fontParts.world import *
anchors = CurrentAnchors()
This returns an immutable list, even when nothing is selected.
"""
return dispatcher["CurrentAnchors"]()
def _defaultCurrentAnchors():
glyph = CurrentGlyph()
if glyph is None:
return ()
return glyph.selectedAnchors
[docs]
def CurrentGuidelines():
"""
Get the "currently" selected guidelines from :func:`CurrentGlyph`.
This will include both font level and glyph level guidelines.
::
from fontParts.world import *
guidelines = CurrentGuidelines()
This returns an immutable list, even when nothing is selected.
"""
return dispatcher["CurrentGuidelines"]()
def _defaultCurrentGuidelines():
guidelines = []
font = CurrentFont()
if font is not None:
guidelines.extend(font.selectedGuidelines)
glyph = CurrentGlyph()
if glyph is not None:
guidelines.extend(glyph.selectedGuidelines)
return tuple(guidelines)
[docs]
def AllFonts(sortOptions=None):
"""
Get a list of all open fonts. Optionally, provide a
value for ``sortOptions`` to sort the fonts. See
:meth:`world.FontList.sortBy` for options.
::
from fontParts.world import *
fonts = AllFonts()
for font in fonts:
# do something
fonts = AllFonts("magic")
for font in fonts:
# do something
fonts = AllFonts(["familyName", "styleName"])
for font in fonts:
# do something
"""
fontList = FontList(dispatcher["AllFonts"]())
if sortOptions is not None:
fontList.sortBy(sortOptions)
return fontList
def RFont(path=None, showInterface=True):
return dispatcher["RFont"](pathOrObject=path, showInterface=showInterface)
def RGlyph():
return dispatcher["RGlyph"]()
# ---------
# Font List
# ---------
[docs]
def FontList(fonts=None):
"""
Get a list with font specific methods.
::
from fontParts.world import *
fonts = FontList()
Refer to :class:`BaseFontList` for full documentation.
"""
l = dispatcher["FontList"]()
if fonts:
l.extend(fonts)
return l
[docs]
class BaseFontList(list):
# Sort
def sortBy(self, sortOptions, reverse=False):
"""
Sort ``fonts`` with the ordering preferences defined
by ``sortBy``. ``sortBy`` must be one of the following:
* sort description string
* :class:`BaseInfo` attribute name
* sort value function
* list/tuple containing sort description strings, :class:`BaseInfo`
attribute names and/or sort value functions
* ``"magic"``
Sort Description Strings
------------------------
The sort description strings, and how they modify the sort, are:
+----------------------+--------------------------------------+
| ``"familyName"`` | Family names by alphabetical order. |
+----------------------+--------------------------------------+
| ``"styleName"`` | Style names by alphabetical order. |
+----------------------+--------------------------------------+
| ``"isItalic"`` | Italics before romans. |
+----------------------+--------------------------------------+
| ``"isRoman"`` | Romans before italics. |
+----------------------+--------------------------------------+
| ``"widthValue"`` | Width values by numerical order. |
+----------------------+--------------------------------------+
| ``"weightValue"`` | Weight values by numerical order. |
+----------------------+--------------------------------------+
| ``"monospace"`` | Monospaced before proportional. |
+----------------------+--------------------------------------+
| ``"isProportional"`` | Proportional before monospaced. |
+----------------------+--------------------------------------+
::
>>> fonts.sortBy(("familyName", "styleName"))
Font Info Attribute Names
-------------------------
Any :class:`BaseFont` attribute name may be included as
a sort option. For example, to sort by x-height value,
you'd use the ``"xHeight"`` attribute name.
::
>>> fonts.sortBy("xHeight")
Sort Value Function
-------------------
A sort value function must be a function that accepts
one argument, ``font``. This function must return
a sortable value for the given font. For example:
::
>>> def glyphCountSortValue(font):
>>> return len(font)
>>>
>>> fonts.sortBy(glyphCountSortValue)
A list of sort description strings and/or sort functions
may also be provided. This should be in order of most
to least important. For example, to sort by family name
and then style name, do this:
"magic"
-------
If "magic" is given for ``sortBy``, the fonts will be
sorted based on this sort description sequence:
* ``"familyName"``
* ``"isProportional"``
* ``"widthValue"``
* ``"weightValue"``
* ``"styleName"``
* ``"isRoman"``
::
>>> fonts.sortBy("magic")
"""
from types import FunctionType
valueGetters = dict(
familyName=_sortValue_familyName,
styleName=_sortValue_styleName,
isRoman=_sortValue_isRoman,
isItalic=_sortValue_isItalic,
widthValue=_sortValue_widthValue,
weightValue=_sortValue_weightValue,
isProportional=_sortValue_isProportional,
isMonospace=_sortValue_isMonospace
)
if isinstance(sortOptions, str) or isinstance(sortOptions, FunctionType):
sortOptions = [sortOptions]
if not isinstance(sortOptions, (list, tuple)):
raise ValueError("sortOptions must a string, list or function.")
if not sortOptions:
raise ValueError("At least one sort option must be defined.")
if sortOptions == ["magic"]:
sortOptions = [
"familyName",
"isProportional",
"widthValue",
"weightValue",
"styleName",
"isRoman"
]
sorter = []
for originalIndex, font in enumerate(self):
sortable = []
for valueName in sortOptions:
if isinstance(valueName, FunctionType):
value = valueName(font)
elif valueName in valueGetters:
value = valueGetters[valueName](font)
elif hasattr(font.info, valueName):
value = getattr(font.info, valueName)
else:
raise ValueError("Unknown sort option: %s" % repr(valueName))
sortable.append(value)
sortable.append(originalIndex)
sortable.append(font)
sorter.append(tuple(sortable))
sorter.sort()
fonts = [i[-1] for i in sorter]
del self[:]
self.extend(fonts)
if reverse:
self.reverse()
# Search
def getFontsByFontInfoAttribute(self, *attributeValuePairs):
"""
Get a list of fonts that match the (attribute, value)
combinations in ``attributeValuePairs``.
::
>>> subFonts = fonts.getFontsByFontInfoAttribute(("xHeight", 20))
>>> subFonts = fonts.getFontsByFontInfoAttribute(("xHeight", 20), ("descender", -150))
This will return an instance of :class:`BaseFontList`.
"""
found = self
for attr, value in attributeValuePairs:
found = self._matchFontInfoAttributes(found, (attr, value))
return found
def _matchFontInfoAttributes(self, fonts, attributeValuePair):
found = self.__class__()
attr, value = attributeValuePair
for font in fonts:
if getattr(font.info, attr) == value:
found.append(font)
return found
def getFontsByFamilyName(self, familyName):
"""
Get a list of fonts that match ``familyName``.
This will return an instance of :class:`BaseFontList`.
"""
return self.getFontsByFontInfoAttribute(("familyName", familyName))
def getFontsByStyleName(self, styleName):
"""
Get a list of fonts that match ``styleName``.
This will return an instance of :class:`BaseFontList`.
"""
return self.getFontsByFontInfoAttribute(("styleName", styleName))
def getFontsByFamilyNameStyleName(self, familyName, styleName):
"""
Get a list of fonts that match ``familyName`` and ``styleName``.
This will return an instance of :class:`BaseFontList`.
"""
return self.getFontsByFontInfoAttribute(("familyName", familyName), ("styleName", styleName))
def _sortValue_familyName(font):
"""
Returns font.info.familyName.
"""
value = font.info.familyName
if value is None:
value = ""
return value
def _sortValue_styleName(font):
"""
Returns font.info.styleName.
"""
value = font.info.styleName
if value is None:
value = ""
return value
def _sortValue_isRoman(font):
"""
Returns 0 if the font is roman.
Returns 1 if the font is not roman.
"""
italic = _sortValue_isItalic(font)
if italic == 1:
return 0
return 1
def _sortValue_isItalic(font):
"""
Returns 0 if the font is italic.
Returns 1 if the font is not italic.
"""
info = font.info
styleMapStyleName = info.styleMapStyleName
if styleMapStyleName is not None and "italic" in styleMapStyleName:
return 0
if info.italicAngle not in (None, 0):
return 0
return 1
def _sortValue_widthValue(font):
"""
Returns font.info.openTypeOS2WidthClass.
"""
value = font.info.openTypeOS2WidthClass
if value is None:
value = -1
return value
def _sortValue_weightValue(font):
"""
Returns font.info.openTypeOS2WeightClass.
"""
value = font.info.openTypeOS2WeightClass
if value is None:
value = -1
return value
def _sortValue_isProportional(font):
"""
Returns 0 if the font is proportional.
Returns 1 if the font is not proportional.
"""
monospace = _sortValue_isMonospace(font)
if monospace == 1:
return 0
return 1
def _sortValue_isMonospace(font):
"""
Returns 0 if the font is monospace.
Returns 1 if the font is not monospace.
"""
if font.info.postscriptIsFixedPitch:
return 0
if not len(font):
return 1
testWidth = None
for glyph in font:
if testWidth is None:
testWidth = glyph.width
else:
if testWidth != glyph.width:
return 1
return 0
# ----------
# Dispatcher
# ----------
class _EnvironmentDispatcher(object):
def __init__(self, registryItems):
self._registry = {item: None for item in registryItems}
def __setitem__(self, name, func):
self._registry[name] = func
def __getitem__(self, name):
func = self._registry[name]
if func is None:
raise NotImplementedError
return func
dispatcher = _EnvironmentDispatcher([
"OpenFontsFileExtensions",
"OpenFont",
"NewFont",
"AllFonts",
"CurrentFont",
"CurrentGlyph",
"CurrentLayer",
"CurrentContours",
"CurrentSegments",
"CurrentPoints",
"CurrentComponents",
"CurrentAnchors",
"CurrentGuidelines",
"FontList",
"RFont",
"RLayer",
"RGlyph",
"RContour",
"RPoint",
"RAnchor",
"RComponent",
"RGuideline",
"RImage",
"RInfo",
"RFeatures",
"RGroups",
"RKerning",
"RLib",
])
# Register the default functions.
dispatcher["CurrentContours"] = _defaultCurrentContours
dispatcher["CurrentSegments"] = _defaultCurrentSegments
dispatcher["CurrentPoints"] = _defaultCurrentPoints
dispatcher["CurrentComponents"] = _defaultCurrentComponents
dispatcher["CurrentAnchors"] = _defaultCurrentAnchors
dispatcher["CurrentGuidelines"] = _defaultCurrentGuidelines
dispatcher["FontList"] = BaseFontList
# -------
# fontshell
# -------
try:
from fontParts import fontshell
# OpenFonts
dispatcher["OpenFontsFileExtensions"] = [".ufo"]
# OpenFont, RFont
def _fontshellRFont(pathOrObject=None, showInterface=True):
return fontshell.RFont(pathOrObject=pathOrObject, showInterface=showInterface)
dispatcher["OpenFont"] = _fontshellRFont
dispatcher["RFont"] = _fontshellRFont
# NewFont
def _fontshellNewFont(familyName=None, styleName=None, showInterface=True):
font = fontshell.RFont(showInterface=showInterface)
if familyName is not None:
font.info.familyName = familyName
if styleName is not None:
font.info.styleName = styleName
return font
dispatcher["NewFont"] = _fontshellNewFont
# RLayer, RGlyph, RContour, RPoint, RAnchor, RComponent, RGuideline, RImage, RInfo, RFeatures, RGroups, RKerning, RLib
dispatcher["RLayer"] = fontshell.RLayer
dispatcher["RGlyph"] = fontshell.RGlyph
dispatcher["RContour"] = fontshell.RContour
dispatcher["RPoint"] = fontshell.RPoint
dispatcher["RAnchor"] = fontshell.RAnchor
dispatcher["RComponent"] = fontshell.RComponent
dispatcher["RGuideline"] = fontshell.RGuideline
dispatcher["RImage"] = fontshell.RImage
dispatcher["RInfo"] = fontshell.RInfo
dispatcher["RFeatures"] = fontshell.RFeatures
dispatcher["RGroups"] = fontshell.RGroups
dispatcher["RKerning"] = fontshell.RKerning
dispatcher["RLib"] = fontshell.RLib
except ImportError:
pass