[ttLib.removeOverlaps] Support CFF table
This commit is contained in:
parent
a531041f3e
commit
2f9033b22d
@ -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,11 @@ 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 +120,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 +139,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 +163,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 +178,15 @@ 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,6 +217,100 @@ def removeOverlaps(
|
||||
log.debug("Removed overlaps for %s glyphs:\n%s", len(modified), " ".join(modified))
|
||||
|
||||
|
||||
def _remove_charstring_overlaps(
|
||||
glyphName: str,
|
||||
glyphSet: _TTGlyphMapping,
|
||||
cffFontSet: CFFFontSet,
|
||||
removeHinting: bool,
|
||||
) -> 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
|
||||
|
||||
if removeHinting:
|
||||
raise NotImplementedError(
|
||||
"Hinting removal is not implemented for CFF fonts yet"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _remove_cff_overlaps(
|
||||
font: ttFont.TTFont,
|
||||
glyphNames: Iterable[str],
|
||||
glyphSet: _TTGlyphMapping,
|
||||
removeHinting: bool,
|
||||
ignoreErrors: bool,
|
||||
) -> None:
|
||||
cffFontSet = font["CFF "].cff
|
||||
modified = set()
|
||||
for glyphName in glyphNames:
|
||||
try:
|
||||
if _remove_charstring_overlaps(
|
||||
glyphName,
|
||||
glyphSet,
|
||||
cffFontSet,
|
||||
removeHinting,
|
||||
):
|
||||
modified.add(glyphName)
|
||||
except RemoveOverlapsError:
|
||||
if not ignoreErrors:
|
||||
raise
|
||||
log.error("Failed to remove overlaps for '%s'", glyphName)
|
||||
|
||||
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,
|
||||
) -> 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).
|
||||
"""
|
||||
|
||||
if "glyf" not in font and "CFF " not in font:
|
||||
raise NotImplementedError(
|
||||
"No outline data found in the font: missing 'glyf' or 'CFF ' table"
|
||||
)
|
||||
|
||||
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, glyphNames, glyphSet, removeHinting, ignoreErrors)
|
||||
|
||||
if "CFF " in font:
|
||||
_remove_cff_overlaps(font, glyphNames, glyphSet, removeHinting, ignoreErrors)
|
||||
|
||||
|
||||
def main(args=None):
|
||||
"""Simplify glyphs in TTFont by merging overlapping contours."""
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user