|
@@ -7,11 +7,14 @@ import itertools
|
|
|
import logging
|
|
|
from typing import Callable, Iterable, Optional, Mapping
|
|
|
|
|
|
-from fontTools.misc.roundTools import otRound
|
|
|
+from fontTools.cffLib import CFFFontSet
|
|
|
from fontTools.ttLib import ttFont
|
|
|
from fontTools.ttLib.tables import _g_l_y_f
|
|
|
from fontTools.ttLib.tables import _h_m_t_x
|
|
|
+from fontTools.misc.psCharStrings import T2CharString
|
|
|
+from fontTools.misc.roundTools import otRound, noRound
|
|
|
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
|
|
+from fontTools.pens.t2CharStringPen import T2CharStringPen
|
|
|
|
|
|
import pathops
|
|
|
|
|
@@ -81,6 +84,14 @@ def ttfGlyphFromSkPath(path: pathops.Path) -> _g_l_y_f.Glyph:
|
|
|
return glyph
|
|
|
|
|
|
|
|
|
+def _charString_from_SkPath(
|
|
|
+ path: pathops.Path, charString: T2CharString
|
|
|
+) -> T2CharString:
|
|
|
+ t2Pen = T2CharStringPen(width=charString.width, glyphSet=None)
|
|
|
+ path.draw(t2Pen)
|
|
|
+ return t2Pen.getCharString(charString.private, charString.globalSubrs)
|
|
|
+
|
|
|
+
|
|
|
def _round_path(
|
|
|
path: pathops.Path, round: Callable[[float], float] = otRound
|
|
|
) -> pathops.Path:
|
|
@@ -90,7 +101,12 @@ def _round_path(
|
|
|
return rounded_path
|
|
|
|
|
|
|
|
|
-def _simplify(path: pathops.Path, debugGlyphName: str) -> pathops.Path:
|
|
|
+def _simplify(
|
|
|
+ path: pathops.Path,
|
|
|
+ debugGlyphName: str,
|
|
|
+ *,
|
|
|
+ round: Callable[[float], float] = otRound,
|
|
|
+) -> pathops.Path:
|
|
|
# skia-pathops has a bug where it sometimes fails to simplify paths when there
|
|
|
# are float coordinates and control points are very close to one another.
|
|
|
# Rounding coordinates to integers works around the bug.
|
|
@@ -105,7 +121,7 @@ def _simplify(path: pathops.Path, debugGlyphName: str) -> pathops.Path:
|
|
|
except pathops.PathOpsError:
|
|
|
pass
|
|
|
|
|
|
- path = _round_path(path)
|
|
|
+ path = _round_path(path, round=round)
|
|
|
try:
|
|
|
path = pathops.simplify(path, clockwise=path.clockwise)
|
|
|
log.debug(
|
|
@@ -124,6 +140,10 @@ def _simplify(path: pathops.Path, debugGlyphName: str) -> pathops.Path:
|
|
|
raise AssertionError("Unreachable")
|
|
|
|
|
|
|
|
|
+def _same_path(path1: pathops.Path, path2: pathops.Path) -> bool:
|
|
|
+ return {tuple(c) for c in path1.contours} == {tuple(c) for c in path2.contours}
|
|
|
+
|
|
|
+
|
|
|
def removeTTGlyphOverlaps(
|
|
|
glyphName: str,
|
|
|
glyphSet: _TTGlyphMapping,
|
|
@@ -144,7 +164,7 @@ def removeTTGlyphOverlaps(
|
|
|
path2 = _simplify(path, glyphName)
|
|
|
|
|
|
# replace TTGlyph if simplified path is different (ignoring contour order)
|
|
|
- if {tuple(c) for c in path.contours} != {tuple(c) for c in path2.contours}:
|
|
|
+ if not _same_path(path, path2):
|
|
|
glyfTable[glyphName] = glyph = ttfGlyphFromSkPath(path2)
|
|
|
# simplified glyph is always unhinted
|
|
|
assert not glyph.program
|
|
@@ -159,42 +179,16 @@ def removeTTGlyphOverlaps(
|
|
|
return False
|
|
|
|
|
|
|
|
|
-def removeOverlaps(
|
|
|
+def _remove_glyf_overlaps(
|
|
|
+ *,
|
|
|
font: ttFont.TTFont,
|
|
|
- glyphNames: Optional[Iterable[str]] = None,
|
|
|
- removeHinting: bool = True,
|
|
|
- ignoreErrors=False,
|
|
|
+ glyphNames: Iterable[str],
|
|
|
+ glyphSet: _TTGlyphMapping,
|
|
|
+ removeHinting: bool,
|
|
|
+ ignoreErrors: bool,
|
|
|
) -> None:
|
|
|
- """Simplify glyphs in TTFont by merging overlapping contours.
|
|
|
-
|
|
|
- Overlapping components are first decomposed to simple contours, then merged.
|
|
|
-
|
|
|
- Currently this only works with TrueType fonts with 'glyf' table.
|
|
|
- Raises NotImplementedError if 'glyf' table is absent.
|
|
|
-
|
|
|
- Note that removing overlaps invalidates the hinting. By default we drop hinting
|
|
|
- from all glyphs whether or not overlaps are removed from a given one, as it would
|
|
|
- look weird if only some glyphs are left (un)hinted.
|
|
|
-
|
|
|
- Args:
|
|
|
- font: input TTFont object, modified in place.
|
|
|
- glyphNames: optional iterable of glyph names (str) to remove overlaps from.
|
|
|
- By default, all glyphs in the font are processed.
|
|
|
- removeHinting (bool): set to False to keep hinting for unmodified glyphs.
|
|
|
- ignoreErrors (bool): set to True to ignore errors while removing overlaps,
|
|
|
- thus keeping the tricky glyphs unchanged (fonttools/fonttools#2363).
|
|
|
- """
|
|
|
- try:
|
|
|
- glyfTable = font["glyf"]
|
|
|
- except KeyError:
|
|
|
- raise NotImplementedError("removeOverlaps currently only works with TTFs")
|
|
|
-
|
|
|
+ glyfTable = font["glyf"]
|
|
|
hmtxTable = font["hmtx"]
|
|
|
- # wraps the underlying glyf Glyphs, takes care of interfacing with drawing pens
|
|
|
- glyphSet = font.getGlyphSet()
|
|
|
-
|
|
|
- if glyphNames is None:
|
|
|
- glyphNames = font.getGlyphOrder()
|
|
|
|
|
|
# process all simple glyphs first, then composites with increasing component depth,
|
|
|
# so that by the time we test for component intersections the respective base glyphs
|
|
@@ -225,27 +219,170 @@ def removeOverlaps(
|
|
|
log.debug("Removed overlaps for %s glyphs:\n%s", len(modified), " ".join(modified))
|
|
|
|
|
|
|
|
|
-def main(args=None):
|
|
|
- """Simplify glyphs in TTFont by merging overlapping contours."""
|
|
|
+def _remove_charstring_overlaps(
|
|
|
+ *,
|
|
|
+ glyphName: str,
|
|
|
+ glyphSet: _TTGlyphMapping,
|
|
|
+ cffFontSet: CFFFontSet,
|
|
|
+) -> bool:
|
|
|
+ path = skPathFromGlyph(glyphName, glyphSet)
|
|
|
+
|
|
|
+ # remove overlaps
|
|
|
+ path2 = _simplify(path, glyphName, round=noRound)
|
|
|
+
|
|
|
+ # replace TTGlyph if simplified path is different (ignoring contour order)
|
|
|
+ if not _same_path(path, path2):
|
|
|
+ charStrings = cffFontSet[0].CharStrings
|
|
|
+ charStrings[glyphName] = _charString_from_SkPath(path2, charStrings[glyphName])
|
|
|
+ return True
|
|
|
|
|
|
- import sys
|
|
|
+ return False
|
|
|
|
|
|
- if args is None:
|
|
|
- args = sys.argv[1:]
|
|
|
|
|
|
- if len(args) < 2:
|
|
|
- print(
|
|
|
- f"usage: fonttools ttLib.removeOverlaps INPUT.ttf OUTPUT.ttf [GLYPHS ...]"
|
|
|
+def _remove_cff_overlaps(
|
|
|
+ *,
|
|
|
+ font: ttFont.TTFont,
|
|
|
+ glyphNames: Iterable[str],
|
|
|
+ glyphSet: _TTGlyphMapping,
|
|
|
+ removeHinting: bool,
|
|
|
+ ignoreErrors: bool,
|
|
|
+ removeUnusedSubroutines: bool = True,
|
|
|
+) -> None:
|
|
|
+ cffFontSet = font["CFF "].cff
|
|
|
+ modified = set()
|
|
|
+ for glyphName in glyphNames:
|
|
|
+ try:
|
|
|
+ if _remove_charstring_overlaps(
|
|
|
+ glyphName=glyphName,
|
|
|
+ glyphSet=glyphSet,
|
|
|
+ cffFontSet=cffFontSet,
|
|
|
+ ):
|
|
|
+ modified.add(glyphName)
|
|
|
+ except RemoveOverlapsError:
|
|
|
+ if not ignoreErrors:
|
|
|
+ raise
|
|
|
+ log.error("Failed to remove overlaps for '%s'", glyphName)
|
|
|
+
|
|
|
+ if not modified:
|
|
|
+ log.debug("No overlaps found in the specified CFF glyphs")
|
|
|
+ return
|
|
|
+
|
|
|
+ if removeHinting:
|
|
|
+ cffFontSet.remove_hints()
|
|
|
+
|
|
|
+ if removeUnusedSubroutines:
|
|
|
+ cffFontSet.remove_unused_subroutines()
|
|
|
+
|
|
|
+ log.debug("Removed overlaps for %s glyphs:\n%s", len(modified), " ".join(modified))
|
|
|
+
|
|
|
+
|
|
|
+def removeOverlaps(
|
|
|
+ font: ttFont.TTFont,
|
|
|
+ glyphNames: Optional[Iterable[str]] = None,
|
|
|
+ removeHinting: bool = True,
|
|
|
+ ignoreErrors: bool = False,
|
|
|
+ *,
|
|
|
+ removeUnusedSubroutines: bool = True,
|
|
|
+) -> None:
|
|
|
+ """Simplify glyphs in TTFont by merging overlapping contours.
|
|
|
+
|
|
|
+ Overlapping components are first decomposed to simple contours, then merged.
|
|
|
+
|
|
|
+ Currently this only works for fonts with 'glyf' or 'CFF ' tables.
|
|
|
+ Raises NotImplementedError if 'glyf' or 'CFF ' tables are absent.
|
|
|
+
|
|
|
+ Note that removing overlaps invalidates the hinting. By default we drop hinting
|
|
|
+ from all glyphs whether or not overlaps are removed from a given one, as it would
|
|
|
+ look weird if only some glyphs are left (un)hinted.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ font: input TTFont object, modified in place.
|
|
|
+ glyphNames: optional iterable of glyph names (str) to remove overlaps from.
|
|
|
+ By default, all glyphs in the font are processed.
|
|
|
+ removeHinting (bool): set to False to keep hinting for unmodified glyphs.
|
|
|
+ ignoreErrors (bool): set to True to ignore errors while removing overlaps,
|
|
|
+ thus keeping the tricky glyphs unchanged (fonttools/fonttools#2363).
|
|
|
+ removeUnusedSubroutines (bool): set to False to keep unused subroutines
|
|
|
+ in CFF table after removing overlaps. Default is to remove them if
|
|
|
+ any glyphs are modified.
|
|
|
+ """
|
|
|
+
|
|
|
+ if "glyf" not in font and "CFF " not in font:
|
|
|
+ raise NotImplementedError(
|
|
|
+ "No outline data found in the font: missing 'glyf' or 'CFF ' table"
|
|
|
)
|
|
|
- sys.exit(1)
|
|
|
|
|
|
- src = args[0]
|
|
|
- dst = args[1]
|
|
|
- glyphNames = args[2:] or None
|
|
|
+ if glyphNames is None:
|
|
|
+ glyphNames = font.getGlyphOrder()
|
|
|
+
|
|
|
+ # Wraps the underlying glyphs, takes care of interfacing with drawing pens
|
|
|
+ glyphSet = font.getGlyphSet()
|
|
|
+
|
|
|
+ if "glyf" in font:
|
|
|
+ _remove_glyf_overlaps(
|
|
|
+ font=font,
|
|
|
+ glyphNames=glyphNames,
|
|
|
+ glyphSet=glyphSet,
|
|
|
+ removeHinting=removeHinting,
|
|
|
+ ignoreErrors=ignoreErrors,
|
|
|
+ )
|
|
|
+
|
|
|
+ if "CFF " in font:
|
|
|
+ _remove_cff_overlaps(
|
|
|
+ font=font,
|
|
|
+ glyphNames=glyphNames,
|
|
|
+ glyphSet=glyphSet,
|
|
|
+ removeHinting=removeHinting,
|
|
|
+ ignoreErrors=ignoreErrors,
|
|
|
+ removeUnusedSubroutines=removeUnusedSubroutines,
|
|
|
+ )
|
|
|
|
|
|
- with ttFont.TTFont(src) as f:
|
|
|
- removeOverlaps(f, glyphNames)
|
|
|
- f.save(dst)
|
|
|
+
|
|
|
+def main(args=None):
|
|
|
+ """Simplify glyphs in TTFont by merging overlapping contours."""
|
|
|
+
|
|
|
+ import argparse
|
|
|
+
|
|
|
+ parser = argparse.ArgumentParser(
|
|
|
+ "fonttools ttLib.removeOverlaps", description=__doc__
|
|
|
+ )
|
|
|
+
|
|
|
+ parser.add_argument("input", metavar="INPUT.ttf", help="Input font file")
|
|
|
+ parser.add_argument("output", metavar="OUTPUT.ttf", help="Output font file")
|
|
|
+ parser.add_argument(
|
|
|
+ "glyphs",
|
|
|
+ metavar="GLYPHS",
|
|
|
+ nargs="*",
|
|
|
+ help="Optional list of glyph names to remove overlaps from",
|
|
|
+ )
|
|
|
+ parser.add_argument(
|
|
|
+ "--keep-hinting",
|
|
|
+ action="store_true",
|
|
|
+ help="Keep hinting for unmodified glyphs, default is to drop hinting",
|
|
|
+ )
|
|
|
+ parser.add_argument(
|
|
|
+ "--ignore-errors",
|
|
|
+ action="store_true",
|
|
|
+ help="ignore errors while removing overlaps, "
|
|
|
+ "thus keeping the tricky glyphs unchanged",
|
|
|
+ )
|
|
|
+ parser.add_argument(
|
|
|
+ "--keep-unused-subroutines",
|
|
|
+ action="store_true",
|
|
|
+ help="Keep unused subroutines in CFF table after removing overlaps, "
|
|
|
+ "default is to remove them if any glyphs are modified",
|
|
|
+ )
|
|
|
+ args = parser.parse_args(args)
|
|
|
+
|
|
|
+ with ttFont.TTFont(args.input) as font:
|
|
|
+ removeOverlaps(
|
|
|
+ font=font,
|
|
|
+ glyphNames=args.glyphs or None,
|
|
|
+ removeHinting=not args.keep_hinting,
|
|
|
+ ignoreErrors=args.ignore_errors,
|
|
|
+ removeUnusedSubroutines=not args.keep_unused_subroutines,
|
|
|
+ )
|
|
|
+ font.save(args.output)
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|