Source code for fontParts.world

# pylint: disable=C0103, C0114
from __future__ import annotations
import os
import glob
from typing import TYPE_CHECKING, Dict, Optional, Tuple, Union
from collections.abc import Callable, Iterable
from collections.abc import Generator
from types import FunctionType

from fontParts.base.annotations import T, CollectionType

if TYPE_CHECKING:
    from fontParts.base.font import BaseFont
    from fontParts.base.glyph import BaseGlyph
    from fontParts.base.layer import BaseLayer
    from fontParts.base.contour import BaseContour
    from fontParts.base.segment import BaseSegment
    from fontParts.base.point import BasePoint
    from fontParts.base.component import BaseComponent
    from fontParts.base.anchor import BaseAnchor
    from fontParts.base.guideline import BaseGuideline

SortOptionType = Union[str, FunctionType, CollectionType[Union[str, FunctionType]]]
BaseTypes = Union[
    "BaseFont",
    "BaseGlyph",
    "BaseLayer",
    "BaseContour",
    "BaseSegment",
    "BasePoint",
    "BaseComponent",
    "BaseAnchor",
    "BaseGuideline",
    "BaseFontList",
]
RegistryType = dict[str, Optional[Callable[[], BaseTypes]]]
InfoType = Union[str, int, float, bool]


[docs] def OpenFonts( directory: str | CollectionType[str] | None = None, showInterface: bool = True, fileExtensions: CollectionType[str] | None = None, ) -> Generator[BaseFont]: """Open all fonts located in the specified directories. The fonts are located within the directory using the :mod:`glob` module. The patterns are created with ``os.path.join(directory, "*" + fileExtension)`` for every file extension in `fileExtensions`. :param directory: The optional directory :class:`str` or the :class:`list` or :class:`tuple` of directories to search for fonts. If :obj:`None` (default), a dialog for selecting a directory will be opened. :param showInterface: A :class:`bool` indicating whether to show the graphical interface. If :obj:`False`, the font should be opened without a graphical interface. Defaults to :obj:`True`. :param fileExtensions: The optional file extensions to search for as a :class:`list` or :class:`tuple` of :class:`str` items. If :obj:`None` (default), the default file extensions will be used. :return: A :class:`generator` yielding the opened fonts. Example:: from fontParts.world import OpenFonts fonts = OpenFonts() fonts = OpenFonts(showInterface=False) """ from fontParts.ui import GetFileOrFolder directories: CollectionType[str] if fileExtensions is None: fileExtensions = dispatcher["OpenFontsFileExtensions"]() if directory is None: directory = GetFileOrFolder(allowsMultipleSelection=True) elif isinstance(directory, str): directories = [directory] else: directories = directory if directories: globPatterns = [] for directory in directories: if not fileExtensions: continue 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: str, showInterface: bool = True) -> BaseFont: """Open font located at the specified path. :param path: The path to the font file to be opened as a :class:`str` :param showInterface: A :class:`bool` indicating whether to show the graphical interface. If :obj:`False`, the font should be opened without a graphical interface. Defaults to :obj:`True`. :return: The newly opened :class:`BaseFont` instance. Example:: from fontParts.world import OpenFont 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: str | None = None, styleName: str | None = None, showInterface: bool = True, ) -> BaseFont: """Create a new font. :param familyName: The optional :attr:`BaseInfo.familyName` to apply to the font as a :class:`str`. :param styleName: The optional :attr:`BaseInfo.styleName` to apply to the font as a :class:`str`. :param showInterface: A :class:`bool` indicating whether to show the graphical interface. If :obj:`False`, the font should be opened without a graphical interface. Defaults to :obj:`True`. :return: The newly created :class:`BaseFont` instance. Example:: from fontParts.world import NewFont 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() -> BaseFont: """Get the currently active font. :return: A :class:`BaseFont` subclass instance representing the currently active font. """ return dispatcher["CurrentFont"]()
[docs] def CurrentGlyph() -> BaseGlyph: """Get the currently active glyph from :func:`CurrentFont`. :return: A :class:`BaseGlyph` subclass instance representing the currently active glyph. Example:: from fontParts.world import CurrentGlyph glyph = CurrentGlyph() """ return dispatcher["CurrentGlyph"]()
[docs] def CurrentLayer() -> BaseLayer: """Get the currently active layer from :func:`CurrentGlyph`. :return: A :class:`BaseLayer` subclass instance representing the currently active glyph layer. Example:: from fontParts.world import CurrentLayer layer = CurrentLayer() """ return dispatcher["CurrentLayer"]()
[docs] def CurrentContours() -> tuple[BaseContour, ...]: """Get the currently selected contours from :func:`CurrentGlyph`. :return: A :class:`tuple` of :class:`BaseContour` subclass instances representing the currently selected glyph contours. If nothing is selected, the :class:`tuple` will be empty. Example:: from fontParts.world import CurrentContours contours = CurrentContours() """ return dispatcher["CurrentContours"]()
def _defaultCurrentContours() -> tuple[BaseContour, ...]: glyph = CurrentGlyph() if glyph is None: return () return glyph.selectedContours
[docs] def CurrentSegments() -> tuple[BaseSegment, ...]: """Get the currently selected segments from :func:`CurrentContours`. :return: A :class:`tuple` of :class:`BaseSegments` subclass instances representing the currently selected contour segments. If nothing is selected, the :class:`tuple` will be empty. Example:: from fontParts.world import CurrentSegments segments = CurrentSegments() """ return dispatcher["CurrentSegments"]()
def _defaultCurrentSegments() -> tuple[BaseSegment, ...]: glyph = CurrentGlyph() if glyph is None: return () segments = [] for contour in glyph.selectedContours: segments.extend(contour.selectedSegments) return tuple(segments)
[docs] def CurrentPoints() -> tuple[BasePoint, ...]: """Get the currently selected points from :func:`CurrentContours`. :return: A :class:`tuple` of :class:`BasePoint` subclass instances representing the currently selected contour points. If nothing is selected, the :class:`tuple` will be empty. Example:: from fontParts.world import CurrentPoints points = CurrentPoints() """ return dispatcher["CurrentPoints"]()
def _defaultCurrentPoints() -> tuple[BasePoint, ...]: glyph = CurrentGlyph() if glyph is None: return () points = [] for contour in glyph.selectedContours: points.extend(contour.selectedPoints) return tuple(points)
[docs] def CurrentComponents() -> tuple[BaseComponent, ...]: """Get the currently selected components from :func:`CurrentGlyph`. :return: A :class:`tuple` of :class:`BaseComponent` subclass instances representing the currently selected glyph components. If nothing is selected, the :class:`tuple` will be empty. Example:: from fontParts.world import CurrentComponents components = CurrentComponents() """ return dispatcher["CurrentComponents"]()
def _defaultCurrentComponents() -> tuple[BaseComponent, ...]: glyph = CurrentGlyph() if glyph is None: return () return glyph.selectedComponents
[docs] def CurrentAnchors() -> tuple[BaseAnchor, ...]: """Get the currently selected anchors from :func:`CurrentGlyph`. :return: A :class:`tuple` of :class:`BaseAnchor` subclass instances representing the currently selected glyph anchors. If nothing is selected, the :class:`tuple` will be empty. Example:: from fontParts.world import CurrentAnchors anchors = CurrentAnchors() """ return dispatcher["CurrentAnchors"]()
def _defaultCurrentAnchors() -> tuple[BaseAnchor, ...]: glyph = CurrentGlyph() if glyph is None: return () return glyph.selectedAnchors
[docs] def CurrentGuidelines() -> tuple[BaseGuideline, ...]: """Get the currently selected guidelines from :func:`CurrentGlyph`. This will include both font level and glyph level guidelines. :return: A :class:`tuple` of :class:`BaseGuideline` subclass instances representing the currently selected guidelines. If nothing is selected, the :class:`tuple` will be empty. Example:: from fontParts.world import CurrentGuidelines guidelines = CurrentGuidelines() """ return dispatcher["CurrentGuidelines"]()
def _defaultCurrentGuidelines() -> tuple[BaseGuideline, ...]: 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: CollectionType[str] | None = None) -> BaseFontList: """Get a list of all open fonts. Optionally, provide a value for `sortOptions` to sort the fonts. See :meth:`BaseFontList.sortBy` for options. :param sortOptions: The optional :class:`list` or :class:`tuple` of :class:`str` sort options to apply to the list. Defaults to :obj:`None`. :return: A :class:`BaseFontList` instance representing all open fonts. Example:: from fontParts.world import AllFonts 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: str | None = None, showInterface: bool = True) -> fontshell.RFont: return dispatcher["RFont"](pathOrObject=path, showInterface=showInterface) def RGlyph() -> fontshell.RGlyph: return dispatcher["RGlyph"]() # --------- # Font List # ---------
[docs] def FontList(fonts: Iterable[T] | None = None): """Get a list with font-specific methods. :return: A :class:`BaseFontList` instance. Example:: from fontParts.world import FontList fonts = FontList() Refer to :class:`BaseFontList` for full documentation. """ list = dispatcher["FontList"]() if fonts: list.extend(fonts) return list
[docs] class BaseFontList(list): """Represent a :class:`list` with font-specific methods.""" # Sort def sortBy(self, sortOptions: SortOptionType, reverse: bool = False) -> None: """Sort items according to specified options. Sorting options may be defined as follows: - A :ref:`sort description <sort-descriptions>` as a :class:`str` - A :ref:`font info attribute name <info-attributes>` as a :class:`str` - A custom `sort value function <sort-value-function>` - A :class:`list` or :class:`tuple` containing a mix of any of the above - The special keyword ``"magic"`` (see :ref:`magic-sorting`) .. _sort-descriptions: Sort Descriptions ----------------- The following string-based sort descriptions determine sorting behavior: +----------------------+--------------------------------------+ | Sort Description | Effect | +======================+======================================+ | ``"familyName"`` | Sort by family name (A-Z). | +----------------------+--------------------------------------+ | ``"styleName"`` | Sort by style name (A-Z). | +----------------------+--------------------------------------+ | ``"isItalic"`` | Sort italics before romans. | +----------------------+--------------------------------------+ | ``"isRoman"`` | Sort romans before italics. | +----------------------+--------------------------------------+ | ``"widthValue"`` | Sort by width value (low-high). | +----------------------+--------------------------------------+ | ``"weightValue"`` | Sort by weight value (low-high). | +----------------------+--------------------------------------+ | ``"monospace"`` | Sort monospaced before proportional. | +----------------------+--------------------------------------+ | ``"isProportional"`` | Sort proportional before monospaced. | +----------------------+--------------------------------------+ .. _info-attributes: Font Info Attribute Names ------------------------- Any attribute of :class:`BaseInfo` may be used as a sorting criterion. For example, sorting by x-height value can be achieved using the attribute name ``"xHeight"``. .. _sort-value-function: Sort Value Function ------------------- A sort value function is a :class:`Callable` that takes a single argument, `font`, and returns a sortable value. Example:: def glyph_count_sort(font): return len(font) fonts.sortBy(glyph_count_sort) A :class:`list` or :class:`tuple` of sort descriptions and/or sort functions may be provided to specify sorting precedence, from most to least important. .. _magic-sorting: Magic Sorting ------------- If ``"magic"`` is specified, fonts are sorted using the following sequence of criteria: #. ``"familyName"`` #. ``"isProportional"`` #. ``"widthValue"`` #. ``"weightValue"`` #. ``"styleName"`` #. ``"isRoman"`` :param sortOptions: The sorting option(s), given as a single :class:`str`, :class:`FunctionType`, or a :class:`list` or :class:`tuple` of several. :param reverse: Whether to reverse the sort order. Defaults to :obj:`False`. :raises TypeError: If `sortOptions` is not a :class:`str`, :class:`FunctionType`, :class:`list` or :class:`tuple`. :raises ValueError: - If `sortOptions` does not conatain any sorting options. - If `sortOptions` contains an unrecognized value or value item. Example:: from fontParts.world import AllFonts fonts = AllFonts() fonts.sortBy("familyName") fonts.sortBy(["familyName", "styleName"]) fonts.sortBy("magic") fonts.sortBy(lambda font: len(font)) """ 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 TypeError("sortOptions must be 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: value = None if isinstance(valueName, FunctionType): value = valueName(font) elif isinstance(valueName, str): if valueName in valueGetters: value = valueGetters[valueName](font) elif hasattr(font.info, valueName): value = getattr(font.info, valueName) else: raise ValueError(f"Unknown sort option: {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: tuple[str, InfoType] ) -> BaseFontList: r"""Get a list of fonts that match the specified attribute-value pairs. This method filters fonts based on one or more ``(attribute, value)`` pairs. When multiple pairs are provided, only fonts that satisfy all conditions are included. :param \*attributeValuePairs: The attribute-value pairs to search for as :class:`tuple` instances, each containing a font attribute name as a :class:`str` and the expected value. :return: A :class:`BaseFontList` instance containing the matching fonts. Example:: >>> subFonts = fonts.getFontsByFontInfoAttribute(("xHeight", 20)) >>> subFonts = fonts.getFontsByFontInfoAttribute(("xHeight", 20), ("descender", -150)) """ found = self for attr, value in attributeValuePairs: found = self._matchFontInfoAttributes(found, (attr, value)) return found def _matchFontInfoAttributes( self, fonts: BaseFontList, attributeValuePair: tuple[str, InfoType] ) -> BaseFontList: 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: str) -> BaseFontList: """Get a list of fonts that match the provided family name. :param familyName: The :attr:`BaseInfo.familyName` to search for as a :class:`str`. :return: A :class:`BaseFontList` instance containing the matching fonts. """ return self.getFontsByFontInfoAttribute(("familyName", familyName)) def getFontsByStyleName(self, styleName: str) -> BaseFontList: """Get a list of fonts that match the provided style name. :param styleName: The :attr:`BaseInfo.styleName` to search for as a :class:`str`. :return: A :class:`BaseFontList` instance containing the matching fonts. """ return self.getFontsByFontInfoAttribute(("styleName", styleName)) def getFontsByFamilyNameStyleName( self, familyName: str, styleName: str ) -> BaseFontList: """Get a list of fonts that match the provided family name and style name. :param familyName: The :attr:`BaseInfo.familyName` to search for as a :class:`str`. :param styleName: The :attr:`BaseInfo.styleName` to search for as a :class:`str`. :return: A :class:`BaseFontList` instance containing the matching fonts. """ return self.getFontsByFontInfoAttribute( ("familyName", familyName), ("styleName", styleName) )
def _sortValue_familyName(font: BaseFont) -> str: """ Returns font.info.familyName. """ value = font.info.familyName if value is None: value = "" return value def _sortValue_styleName(font: BaseFont) -> str: """ Returns font.info.styleName. """ value = font.info.styleName if value is None: value = "" return value def _sortValue_isRoman(font: BaseFont) -> int: """ 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: BaseFont) -> int: """ 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: BaseFont) -> int: """ Returns font.info.openTypeOS2WidthClass. """ value = font.info.openTypeOS2WidthClass if value is None: value = -1 return value def _sortValue_weightValue(font: BaseFont) -> int: """ Returns font.info.openTypeOS2WeightClass. """ value = font.info.openTypeOS2WeightClass if value is None: value = -1 return value def _sortValue_isProportional(font: BaseFont) -> int: """ 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: BaseFont) -> int: """ 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: def __init__(self, registryItems: CollectionType[str]) -> None: self._registry: RegistryType = {item: None for item in registryItems} def __setitem__(self, name: str, func: Callable | None) -> None: self._registry[name] = func def __getitem__(self, name: str) -> Callable: 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"] = lambda: [".ufo", ".ufoz"] # OpenFont, RFont def _fontshellRFont( pathOrObject: str | BaseFont | None = None, showInterface: bool = True ) -> fontshell.RFont: return fontshell.RFont(pathOrObject=pathOrObject, showInterface=showInterface) dispatcher["OpenFont"] = _fontshellRFont dispatcher["RFont"] = _fontshellRFont # NewFont def _fontshellNewFont( familyName: str | None = None, styleName: str | None = None, showInterface: bool = True, ) -> fontshell.RFont: 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