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