Source code for foundrytools.core.tables.cff_

import contextlib
from typing import Any

from fontTools.cffLib import PrivateDict, TopDict
from fontTools.pens.recordingPen import RecordingPen
from fontTools.pens.roundingPen import RoundingPen
from fontTools.pens.t2CharStringPen import T2CharStringPen
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables.C_F_F_ import table_C_F_F_

from foundrytools.constants import T_CFF
from foundrytools.core.tables.default import DefaultTbl
from foundrytools.lib.pathops import correct_cff_contours

HINTING_ATTRS = (
    "BlueValues",
    "OtherBlues",
    "FamilyBlues",
    "FamilyOtherBlues",
    "BlueScale",
    "BlueShift",
    "BlueFuzz",
    "StemSnapH",
    "StemSnapV",
    "StdHW",
    "StdVW",
    "ForceBold",
    "LanguageGroup",
    "ExpansionFactor",
)


[docs] class CFFTable(DefaultTbl): """ A class that wraps and manages the CFF table of a font, providing methods to manipulate hinting data, font names, and glyph contours. """ def __init__(self, ttfont: TTFont) -> None: """ Initializes the ``CFF`` table wrapper. :param ttfont: The ``TTFont`` object. :type ttfont: TTFont """ super().__init__(ttfont=ttfont, table_tag=T_CFF) self._raw_dict_copy: dict = self.table.cff.topDictIndex[0].Private.rawDict.copy() @property def table(self) -> table_C_F_F_: """ The wrapped ``table_C_F_F_`` object. """ return self._table @table.setter def table(self, value: table_C_F_F_) -> None: """ Wraps a new ``table_C_F_F_`` object. """ self._table = value @property def top_dict(self) -> TopDict: """ Returns the topDictIndex field of the ``CFF`` table. :return: The topDictIndex field of the ``CFF`` table. :rtype: TopDict """ return self.table.cff.topDictIndex[0] @property def private_dict(self) -> PrivateDict: """ Returns the private field of the ``CFF`` table. :return: The private field of the ``CFF`` table. :rtype: PrivateDict """ return self.top_dict.Private
[docs] def get_hinting_data(self) -> dict[str, Any]: """ Returns the hinting data from the ``CFF`` table. :return: The hinting data. :rtype: dict[str, Any] """ hinting_data = {} for attr in HINTING_ATTRS: if hasattr(self.private_dict, attr): hinting_data[attr] = getattr(self.private_dict, attr) return hinting_data
[docs] def set_hinting_data(self, **kwargs: dict[str, Any]) -> None: """ Sets the hinting data in the ``CFF`` table. :param kwargs: The hinting data to set. :type kwargs: dict[str, Any] """ for attr, value in kwargs.items(): setattr(self.private_dict, attr, value)
[docs] def _restore_hinting_data(self) -> None: """Restore the original hinting data to the ``CFF`` table.""" for attr in HINTING_ATTRS: setattr(self.private_dict, attr, self._raw_dict_copy.get(attr))
[docs] def set_names(self, **kwargs: dict[str, str]) -> None: """ Sets the ``cff.fontNames[0]`` and ``topDictIndex[0]`` values. :param kwargs: The values to set in the CFF table. :type kwargs: dict[str, str] """ font_name = str(kwargs.get("fontNames")) if font_name: self._set_cff_font_names(font_name=font_name) del kwargs["fontNames"] top_dict_names: dict[str, str] = {k: str(v) for k, v in kwargs.items() if v is not None} if top_dict_names: self._set_top_dict_names(top_dict_names)
[docs] def _set_cff_font_names(self, font_name: str) -> None: """ Sets the ``cff.fontNames`` value. :param font_name: The font name to set. :type font_name: str """ self.table.cff.fontNames = [font_name]
[docs] def _set_top_dict_names(self, names: dict[str, str]) -> None: """ Sets the names of the ``CFF`` table. :param names: The names to set. :type names: dict[str, str] """ for attr_name, attr_value in names.items(): setattr(self.top_dict, attr_name, attr_value)
[docs] def del_names(self, **kwargs: dict[str, str]) -> None: """ Deletes names from ``topDictIndex[0]`` using the provided keyword arguments. :param kwargs: The names to delete from the ``CFF`` table ``TopDict``. :type kwargs: dict[str, str] """ for k, v in kwargs.items(): if v is not None: with contextlib.suppress(KeyError): del self.top_dict.rawDict[k]
[docs] def find_replace(self, old_string: str, new_string: str) -> None: """ Find and replace a string in the ``CFF`` table. :param old_string: The string to find. :type old_string: str :param new_string: The string to replace the old string with. :type new_string: str """ self._find_replace_in_font_names(old_string=old_string, new_string=new_string) self._find_replace_in_top_dict(old_string=old_string, new_string=new_string)
[docs] def _find_replace_in_font_names(self, old_string: str, new_string: str) -> None: cff_font_names = self.table.cff.fontNames[0] self.table.cff.fontNames = [ cff_font_names.replace(old_string, new_string).replace(" ", " ").strip() ]
[docs] def _find_replace_in_top_dict(self, old_string: str, new_string: str) -> None: top_dict = self.top_dict attr_list = [ "version", "FullName", "FamilyName", "Weight", "Copyright", "Notice", ] for attr_name in attr_list: with contextlib.suppress(AttributeError): old_value = str(getattr(top_dict, attr_name)) new_value = old_value.replace(old_string, new_string).replace(" ", " ").strip() setattr(top_dict, attr_name, new_value)
[docs] def remove_hinting(self, drop_hinting_data: bool = False) -> None: """ Removes hinting data from a PostScript font. :param drop_hinting_data: If True, the hinting data will be removed from the font. :type drop_hinting_data: bool """ self.table.cff.remove_hints() if not drop_hinting_data: self._restore_hinting_data()
[docs] def round_coordinates(self, drop_hinting_data: bool = False) -> set[str]: """ Round the coordinates of the font's glyphs using the ``RoundingPen``. :return: A set of glyph names whose coordinates were rounded. :rtype: set[str] """ glyph_names = self.ttfont.getGlyphOrder() glyph_set = self.ttfont.getGlyphSet() charstrings = self.table.cff.topDictIndex[0].CharStrings rounded_charstrings = set() for glyph_name in glyph_names: charstring = charstrings[glyph_name] # Record the original charstring and store the value rec_pen = RecordingPen() glyph_set[glyph_name].draw(rec_pen) value = rec_pen.value # https://github.com/fonttools/fonttools/commit/40b525c1e3cc20b4b64004b8e3224a67adc2adf1 # The width argument of `T2CharStringPen()` is inserted directly into the CharString # program, so it must be relative to Private.nominalWidthX. glyph_width = glyph_set[glyph_name].width if glyph_width == charstring.private.defaultWidthX: width = None else: width = glyph_width - charstring.private.nominalWidthX # Round the charstring t2_pen = T2CharStringPen(width=width, glyphSet=glyph_set) rounding_pen = RoundingPen(outPen=t2_pen) glyph_set[glyph_name].draw(rounding_pen) rounded_charstring = t2_pen.getCharString(private=charstring.private) # Record the rounded charstring rec_pen_2 = RecordingPen() rounded_charstring.draw(rec_pen_2) value_2 = rec_pen_2.value # Update the charstring only if the rounded charstring is different if value != value_2: charstrings[glyph_name] = rounded_charstring rounded_charstrings.add(glyph_name) if not drop_hinting_data: self._restore_hinting_data() return rounded_charstrings
[docs] def correct_contours( self, remove_hinting: bool = True, ignore_errors: bool = True, remove_unused_subroutines: bool = True, min_area: int = 25, drop_hinting_data: bool = False, ) -> set[str]: """ Corrects contours of the CFF table by removing overlaps, correcting the direction of the contours, and removing tiny paths. :param remove_hinting: Whether to remove hinting from the font if one or more glyphs are modified. :type remove_hinting: bool :param ignore_errors: Whether to ignore skia pathops errors while correcting contours. :type ignore_errors: bool :param remove_unused_subroutines: Whether to remove unused subroutines from the font. :type remove_unused_subroutines: bool :param min_area: The minimum area of a contour to be considered. Default is 25. :type min_area: int :param drop_hinting_data: If True, the hinting data will be removed from the font. :type drop_hinting_data: bool :return: A set of glyph names that were modified. :rtype: set[str] """ fixed_glyphs = correct_cff_contours( font=self.ttfont, remove_hinting=remove_hinting, ignore_errors=ignore_errors, remove_unused_subroutines=remove_unused_subroutines, min_area=min_area, ) if not drop_hinting_data: self._restore_hinting_data() return fixed_glyphs