Source code for foundrytools.lib.font_finder

from collections.abc import Callable, Generator
from dataclasses import dataclass
from pathlib import Path

from fontTools.ttLib.ttCollection import TTCollection
from fontTools.ttLib.ttFont import TTLibError

from foundrytools.core.font import Font

__all__ = ["FinderError", "FinderFilter", "FinderOptions", "FontFinder"]


[docs] @dataclass class FinderOptions: """A class that specifies the options to pass to the FontFinder class.""" recursive: bool = False lazy: bool | None = None recalc_bboxes: bool = True recalc_timestamp: bool = False
[docs] @dataclass class FinderFilter: """A class that specifies which fonts to filter out when searching for fonts.""" filter_out_tt: bool = False filter_out_ps: bool = False filter_out_woff: bool = False filter_out_woff2: bool = False filter_out_sfnt: bool = False filter_out_static: bool = False filter_out_variable: bool = False
[docs] class FinderError(Exception): """An exception raised by the FontFinder class."""
[docs] class FontFinder: """ A class that finds fonts in a given path. It can search for fonts in a directory and its subdirectories, and can also handle a single font file. The class allows for filtering based on various criteria such as outline format (TrueType or PostScript), font variations (static or variable), and font flavor ('woff', 'woff2' or ``None``). The class returns a list or a generator of Font objects that meet the specified criteria. """ def __init__( self, input_path: str | Path, options: FinderOptions | None = None, filter_: FinderFilter | None = None, ) -> None: """ Initializes the ``FontFinder`` class. :param input_path: The file system path to be processed. This path is resolved to an absolute path. If the path is invalid, a FinderError is raised. :type input_path: Union[str, Path] :param options: Options for customizing the behavior of the finder. If not provided, defaults to a FinderOptions instance. :type options: Optional[FinderOptions] :param filter_: Filter criteria for selecting files or directories. If not provided, defaults to a FinderFilter instance. :type filter_: Optional[FinderFilter] """ # self.input_path = input_path try: self.input_path = Path(input_path).resolve(strict=True) except Exception as e: raise FinderError(f"Invalid input path: {self.input_path}") from e self.filter = filter_ or FinderFilter() self.options = options or FinderOptions() self._filter_conditions = self._generate_filter_conditions(self.filter) self._validate_filter_conditions()
[docs] def find_fonts(self) -> list[Font]: """ Finds fonts in the given input path. :return: A list of ``Font`` objects generated by the generate_fonts method. :rtype: list[Font] """ return list(self.generate_fonts())
[docs] def find_collections(self) -> list[TTCollection]: """ Finds TTCollections in the given input path. :return: A list of ``TTCollection`` objects generated by the generate_collections method. :rtype: list[TTCollection] """ return list(self.generate_collections())
[docs] def generate_fonts(self) -> Generator[Font, None, None]: """ Generates a sequence of ``Font`` objects from the files generated by the ``_generate_files`` method. Filters out fonts based on specified conditions in ``_filter_conditions``. :return: A generator yielding Font objects :rtype: Generator[Font, None, None] """ files = self._generate_files() for file in files: try: font = Font( file, lazy=self.options.lazy, recalc_timestamp=self.options.recalc_timestamp, recalc_bboxes=self.options.recalc_bboxes, ) if not any(condition and func(font) for condition, func in self._filter_conditions): yield font except (TTLibError, PermissionError): pass
[docs] def generate_collections(self) -> Generator[TTCollection, None, None]: """ Generates a sequence of ``TTCollection`` objects from the files generated by the ``_generate_files`` method. :return: A generator yielding TTCollection objects :rtype: Generator[TTCollection, None, None] """ files = self._generate_files() for file in files: try: ttc = TTCollection(file) yield ttc except (TTLibError, PermissionError): pass
[docs] def _generate_files(self) -> Generator[Path, None, None]: is_file = self.input_path.is_file() is_dir = self.input_path.is_dir() if is_file: yield self.input_path elif is_dir: if self.options.recursive: yield from (x for x in self.input_path.rglob("*") if x.is_file()) else: yield from (x for x in self.input_path.glob("*") if x.is_file())
[docs] def _validate_filter_conditions(self) -> None: if self.filter.filter_out_tt and self.filter.filter_out_ps: raise FinderError("Cannot filter out both TrueType and PostScript fonts.") if ( self.filter.filter_out_woff and self.filter.filter_out_woff2 and self.filter.filter_out_sfnt ): raise FinderError("Cannot filter out both web fonts and SFNT fonts.") if self.filter.filter_out_static and self.filter.filter_out_variable: raise FinderError("Cannot filter out both static and variable fonts.")
[docs] @staticmethod def _generate_filter_conditions(filter_: FinderFilter) -> list[tuple[bool, Callable]]: conditions = [ (filter_.filter_out_tt, _is_tt), (filter_.filter_out_ps, _is_ps), (filter_.filter_out_woff, _is_woff), (filter_.filter_out_woff2, _is_woff2), (filter_.filter_out_sfnt, _is_sfnt), (filter_.filter_out_static, _is_static), (filter_.filter_out_variable, _is_variable), ] return conditions
def _is_woff(font: Font) -> bool: return font.is_woff def _is_woff2(font: Font) -> bool: return font.is_woff2 def _is_sfnt(font: Font) -> bool: return font.is_sfnt def _is_ps(font: Font) -> bool: return font.is_ps def _is_tt(font: Font) -> bool: return font.is_tt def _is_static(font: Font) -> bool: return font.is_static def _is_variable(font: Font) -> bool: return font.is_variable