1*e1fe3e4aSElliott Hughes""" Simplify TrueType glyphs by merging overlapping contours/components. 2*e1fe3e4aSElliott Hughes 3*e1fe3e4aSElliott HughesRequires https://github.com/fonttools/skia-pathops 4*e1fe3e4aSElliott Hughes""" 5*e1fe3e4aSElliott Hughes 6*e1fe3e4aSElliott Hughesimport itertools 7*e1fe3e4aSElliott Hughesimport logging 8*e1fe3e4aSElliott Hughesfrom typing import Callable, Iterable, Optional, Mapping 9*e1fe3e4aSElliott Hughes 10*e1fe3e4aSElliott Hughesfrom fontTools.misc.roundTools import otRound 11*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import ttFont 12*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables import _g_l_y_f 13*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables import _h_m_t_x 14*e1fe3e4aSElliott Hughesfrom fontTools.pens.ttGlyphPen import TTGlyphPen 15*e1fe3e4aSElliott Hughes 16*e1fe3e4aSElliott Hughesimport pathops 17*e1fe3e4aSElliott Hughes 18*e1fe3e4aSElliott Hughes 19*e1fe3e4aSElliott Hughes__all__ = ["removeOverlaps"] 20*e1fe3e4aSElliott Hughes 21*e1fe3e4aSElliott Hughes 22*e1fe3e4aSElliott Hughesclass RemoveOverlapsError(Exception): 23*e1fe3e4aSElliott Hughes pass 24*e1fe3e4aSElliott Hughes 25*e1fe3e4aSElliott Hughes 26*e1fe3e4aSElliott Hugheslog = logging.getLogger("fontTools.ttLib.removeOverlaps") 27*e1fe3e4aSElliott Hughes 28*e1fe3e4aSElliott Hughes_TTGlyphMapping = Mapping[str, ttFont._TTGlyph] 29*e1fe3e4aSElliott Hughes 30*e1fe3e4aSElliott Hughes 31*e1fe3e4aSElliott Hughesdef skPathFromGlyph(glyphName: str, glyphSet: _TTGlyphMapping) -> pathops.Path: 32*e1fe3e4aSElliott Hughes path = pathops.Path() 33*e1fe3e4aSElliott Hughes pathPen = path.getPen(glyphSet=glyphSet) 34*e1fe3e4aSElliott Hughes glyphSet[glyphName].draw(pathPen) 35*e1fe3e4aSElliott Hughes return path 36*e1fe3e4aSElliott Hughes 37*e1fe3e4aSElliott Hughes 38*e1fe3e4aSElliott Hughesdef skPathFromGlyphComponent( 39*e1fe3e4aSElliott Hughes component: _g_l_y_f.GlyphComponent, glyphSet: _TTGlyphMapping 40*e1fe3e4aSElliott Hughes): 41*e1fe3e4aSElliott Hughes baseGlyphName, transformation = component.getComponentInfo() 42*e1fe3e4aSElliott Hughes path = skPathFromGlyph(baseGlyphName, glyphSet) 43*e1fe3e4aSElliott Hughes return path.transform(*transformation) 44*e1fe3e4aSElliott Hughes 45*e1fe3e4aSElliott Hughes 46*e1fe3e4aSElliott Hughesdef componentsOverlap(glyph: _g_l_y_f.Glyph, glyphSet: _TTGlyphMapping) -> bool: 47*e1fe3e4aSElliott Hughes if not glyph.isComposite(): 48*e1fe3e4aSElliott Hughes raise ValueError("This method only works with TrueType composite glyphs") 49*e1fe3e4aSElliott Hughes if len(glyph.components) < 2: 50*e1fe3e4aSElliott Hughes return False # single component, no overlaps 51*e1fe3e4aSElliott Hughes 52*e1fe3e4aSElliott Hughes component_paths = {} 53*e1fe3e4aSElliott Hughes 54*e1fe3e4aSElliott Hughes def _get_nth_component_path(index: int) -> pathops.Path: 55*e1fe3e4aSElliott Hughes if index not in component_paths: 56*e1fe3e4aSElliott Hughes component_paths[index] = skPathFromGlyphComponent( 57*e1fe3e4aSElliott Hughes glyph.components[index], glyphSet 58*e1fe3e4aSElliott Hughes ) 59*e1fe3e4aSElliott Hughes return component_paths[index] 60*e1fe3e4aSElliott Hughes 61*e1fe3e4aSElliott Hughes return any( 62*e1fe3e4aSElliott Hughes pathops.op( 63*e1fe3e4aSElliott Hughes _get_nth_component_path(i), 64*e1fe3e4aSElliott Hughes _get_nth_component_path(j), 65*e1fe3e4aSElliott Hughes pathops.PathOp.INTERSECTION, 66*e1fe3e4aSElliott Hughes fix_winding=False, 67*e1fe3e4aSElliott Hughes keep_starting_points=False, 68*e1fe3e4aSElliott Hughes ) 69*e1fe3e4aSElliott Hughes for i, j in itertools.combinations(range(len(glyph.components)), 2) 70*e1fe3e4aSElliott Hughes ) 71*e1fe3e4aSElliott Hughes 72*e1fe3e4aSElliott Hughes 73*e1fe3e4aSElliott Hughesdef ttfGlyphFromSkPath(path: pathops.Path) -> _g_l_y_f.Glyph: 74*e1fe3e4aSElliott Hughes # Skia paths have no 'components', no need for glyphSet 75*e1fe3e4aSElliott Hughes ttPen = TTGlyphPen(glyphSet=None) 76*e1fe3e4aSElliott Hughes path.draw(ttPen) 77*e1fe3e4aSElliott Hughes glyph = ttPen.glyph() 78*e1fe3e4aSElliott Hughes assert not glyph.isComposite() 79*e1fe3e4aSElliott Hughes # compute glyph.xMin (glyfTable parameter unused for non composites) 80*e1fe3e4aSElliott Hughes glyph.recalcBounds(glyfTable=None) 81*e1fe3e4aSElliott Hughes return glyph 82*e1fe3e4aSElliott Hughes 83*e1fe3e4aSElliott Hughes 84*e1fe3e4aSElliott Hughesdef _round_path( 85*e1fe3e4aSElliott Hughes path: pathops.Path, round: Callable[[float], float] = otRound 86*e1fe3e4aSElliott Hughes) -> pathops.Path: 87*e1fe3e4aSElliott Hughes rounded_path = pathops.Path() 88*e1fe3e4aSElliott Hughes for verb, points in path: 89*e1fe3e4aSElliott Hughes rounded_path.add(verb, *((round(p[0]), round(p[1])) for p in points)) 90*e1fe3e4aSElliott Hughes return rounded_path 91*e1fe3e4aSElliott Hughes 92*e1fe3e4aSElliott Hughes 93*e1fe3e4aSElliott Hughesdef _simplify(path: pathops.Path, debugGlyphName: str) -> pathops.Path: 94*e1fe3e4aSElliott Hughes # skia-pathops has a bug where it sometimes fails to simplify paths when there 95*e1fe3e4aSElliott Hughes # are float coordinates and control points are very close to one another. 96*e1fe3e4aSElliott Hughes # Rounding coordinates to integers works around the bug. 97*e1fe3e4aSElliott Hughes # Since we are going to round glyf coordinates later on anyway, here it is 98*e1fe3e4aSElliott Hughes # ok(-ish) to also round before simplify. Better than failing the whole process 99*e1fe3e4aSElliott Hughes # for the entire font. 100*e1fe3e4aSElliott Hughes # https://bugs.chromium.org/p/skia/issues/detail?id=11958 101*e1fe3e4aSElliott Hughes # https://github.com/google/fonts/issues/3365 102*e1fe3e4aSElliott Hughes # TODO(anthrotype): remove once this Skia bug is fixed 103*e1fe3e4aSElliott Hughes try: 104*e1fe3e4aSElliott Hughes return pathops.simplify(path, clockwise=path.clockwise) 105*e1fe3e4aSElliott Hughes except pathops.PathOpsError: 106*e1fe3e4aSElliott Hughes pass 107*e1fe3e4aSElliott Hughes 108*e1fe3e4aSElliott Hughes path = _round_path(path) 109*e1fe3e4aSElliott Hughes try: 110*e1fe3e4aSElliott Hughes path = pathops.simplify(path, clockwise=path.clockwise) 111*e1fe3e4aSElliott Hughes log.debug( 112*e1fe3e4aSElliott Hughes "skia-pathops failed to simplify '%s' with float coordinates, " 113*e1fe3e4aSElliott Hughes "but succeded using rounded integer coordinates", 114*e1fe3e4aSElliott Hughes debugGlyphName, 115*e1fe3e4aSElliott Hughes ) 116*e1fe3e4aSElliott Hughes return path 117*e1fe3e4aSElliott Hughes except pathops.PathOpsError as e: 118*e1fe3e4aSElliott Hughes if log.isEnabledFor(logging.DEBUG): 119*e1fe3e4aSElliott Hughes path.dump() 120*e1fe3e4aSElliott Hughes raise RemoveOverlapsError( 121*e1fe3e4aSElliott Hughes f"Failed to remove overlaps from glyph {debugGlyphName!r}" 122*e1fe3e4aSElliott Hughes ) from e 123*e1fe3e4aSElliott Hughes 124*e1fe3e4aSElliott Hughes raise AssertionError("Unreachable") 125*e1fe3e4aSElliott Hughes 126*e1fe3e4aSElliott Hughes 127*e1fe3e4aSElliott Hughesdef removeTTGlyphOverlaps( 128*e1fe3e4aSElliott Hughes glyphName: str, 129*e1fe3e4aSElliott Hughes glyphSet: _TTGlyphMapping, 130*e1fe3e4aSElliott Hughes glyfTable: _g_l_y_f.table__g_l_y_f, 131*e1fe3e4aSElliott Hughes hmtxTable: _h_m_t_x.table__h_m_t_x, 132*e1fe3e4aSElliott Hughes removeHinting: bool = True, 133*e1fe3e4aSElliott Hughes) -> bool: 134*e1fe3e4aSElliott Hughes glyph = glyfTable[glyphName] 135*e1fe3e4aSElliott Hughes # decompose composite glyphs only if components overlap each other 136*e1fe3e4aSElliott Hughes if ( 137*e1fe3e4aSElliott Hughes glyph.numberOfContours > 0 138*e1fe3e4aSElliott Hughes or glyph.isComposite() 139*e1fe3e4aSElliott Hughes and componentsOverlap(glyph, glyphSet) 140*e1fe3e4aSElliott Hughes ): 141*e1fe3e4aSElliott Hughes path = skPathFromGlyph(glyphName, glyphSet) 142*e1fe3e4aSElliott Hughes 143*e1fe3e4aSElliott Hughes # remove overlaps 144*e1fe3e4aSElliott Hughes path2 = _simplify(path, glyphName) 145*e1fe3e4aSElliott Hughes 146*e1fe3e4aSElliott Hughes # replace TTGlyph if simplified path is different (ignoring contour order) 147*e1fe3e4aSElliott Hughes if {tuple(c) for c in path.contours} != {tuple(c) for c in path2.contours}: 148*e1fe3e4aSElliott Hughes glyfTable[glyphName] = glyph = ttfGlyphFromSkPath(path2) 149*e1fe3e4aSElliott Hughes # simplified glyph is always unhinted 150*e1fe3e4aSElliott Hughes assert not glyph.program 151*e1fe3e4aSElliott Hughes # also ensure hmtx LSB == glyph.xMin so glyph origin is at x=0 152*e1fe3e4aSElliott Hughes width, lsb = hmtxTable[glyphName] 153*e1fe3e4aSElliott Hughes if lsb != glyph.xMin: 154*e1fe3e4aSElliott Hughes hmtxTable[glyphName] = (width, glyph.xMin) 155*e1fe3e4aSElliott Hughes return True 156*e1fe3e4aSElliott Hughes 157*e1fe3e4aSElliott Hughes if removeHinting: 158*e1fe3e4aSElliott Hughes glyph.removeHinting() 159*e1fe3e4aSElliott Hughes return False 160*e1fe3e4aSElliott Hughes 161*e1fe3e4aSElliott Hughes 162*e1fe3e4aSElliott Hughesdef removeOverlaps( 163*e1fe3e4aSElliott Hughes font: ttFont.TTFont, 164*e1fe3e4aSElliott Hughes glyphNames: Optional[Iterable[str]] = None, 165*e1fe3e4aSElliott Hughes removeHinting: bool = True, 166*e1fe3e4aSElliott Hughes ignoreErrors=False, 167*e1fe3e4aSElliott Hughes) -> None: 168*e1fe3e4aSElliott Hughes """Simplify glyphs in TTFont by merging overlapping contours. 169*e1fe3e4aSElliott Hughes 170*e1fe3e4aSElliott Hughes Overlapping components are first decomposed to simple contours, then merged. 171*e1fe3e4aSElliott Hughes 172*e1fe3e4aSElliott Hughes Currently this only works with TrueType fonts with 'glyf' table. 173*e1fe3e4aSElliott Hughes Raises NotImplementedError if 'glyf' table is absent. 174*e1fe3e4aSElliott Hughes 175*e1fe3e4aSElliott Hughes Note that removing overlaps invalidates the hinting. By default we drop hinting 176*e1fe3e4aSElliott Hughes from all glyphs whether or not overlaps are removed from a given one, as it would 177*e1fe3e4aSElliott Hughes look weird if only some glyphs are left (un)hinted. 178*e1fe3e4aSElliott Hughes 179*e1fe3e4aSElliott Hughes Args: 180*e1fe3e4aSElliott Hughes font: input TTFont object, modified in place. 181*e1fe3e4aSElliott Hughes glyphNames: optional iterable of glyph names (str) to remove overlaps from. 182*e1fe3e4aSElliott Hughes By default, all glyphs in the font are processed. 183*e1fe3e4aSElliott Hughes removeHinting (bool): set to False to keep hinting for unmodified glyphs. 184*e1fe3e4aSElliott Hughes ignoreErrors (bool): set to True to ignore errors while removing overlaps, 185*e1fe3e4aSElliott Hughes thus keeping the tricky glyphs unchanged (fonttools/fonttools#2363). 186*e1fe3e4aSElliott Hughes """ 187*e1fe3e4aSElliott Hughes try: 188*e1fe3e4aSElliott Hughes glyfTable = font["glyf"] 189*e1fe3e4aSElliott Hughes except KeyError: 190*e1fe3e4aSElliott Hughes raise NotImplementedError("removeOverlaps currently only works with TTFs") 191*e1fe3e4aSElliott Hughes 192*e1fe3e4aSElliott Hughes hmtxTable = font["hmtx"] 193*e1fe3e4aSElliott Hughes # wraps the underlying glyf Glyphs, takes care of interfacing with drawing pens 194*e1fe3e4aSElliott Hughes glyphSet = font.getGlyphSet() 195*e1fe3e4aSElliott Hughes 196*e1fe3e4aSElliott Hughes if glyphNames is None: 197*e1fe3e4aSElliott Hughes glyphNames = font.getGlyphOrder() 198*e1fe3e4aSElliott Hughes 199*e1fe3e4aSElliott Hughes # process all simple glyphs first, then composites with increasing component depth, 200*e1fe3e4aSElliott Hughes # so that by the time we test for component intersections the respective base glyphs 201*e1fe3e4aSElliott Hughes # have already been simplified 202*e1fe3e4aSElliott Hughes glyphNames = sorted( 203*e1fe3e4aSElliott Hughes glyphNames, 204*e1fe3e4aSElliott Hughes key=lambda name: ( 205*e1fe3e4aSElliott Hughes ( 206*e1fe3e4aSElliott Hughes glyfTable[name].getCompositeMaxpValues(glyfTable).maxComponentDepth 207*e1fe3e4aSElliott Hughes if glyfTable[name].isComposite() 208*e1fe3e4aSElliott Hughes else 0 209*e1fe3e4aSElliott Hughes ), 210*e1fe3e4aSElliott Hughes name, 211*e1fe3e4aSElliott Hughes ), 212*e1fe3e4aSElliott Hughes ) 213*e1fe3e4aSElliott Hughes modified = set() 214*e1fe3e4aSElliott Hughes for glyphName in glyphNames: 215*e1fe3e4aSElliott Hughes try: 216*e1fe3e4aSElliott Hughes if removeTTGlyphOverlaps( 217*e1fe3e4aSElliott Hughes glyphName, glyphSet, glyfTable, hmtxTable, removeHinting 218*e1fe3e4aSElliott Hughes ): 219*e1fe3e4aSElliott Hughes modified.add(glyphName) 220*e1fe3e4aSElliott Hughes except RemoveOverlapsError: 221*e1fe3e4aSElliott Hughes if not ignoreErrors: 222*e1fe3e4aSElliott Hughes raise 223*e1fe3e4aSElliott Hughes log.error("Failed to remove overlaps for '%s'", glyphName) 224*e1fe3e4aSElliott Hughes 225*e1fe3e4aSElliott Hughes log.debug("Removed overlaps for %s glyphs:\n%s", len(modified), " ".join(modified)) 226*e1fe3e4aSElliott Hughes 227*e1fe3e4aSElliott Hughes 228*e1fe3e4aSElliott Hughesdef main(args=None): 229*e1fe3e4aSElliott Hughes import sys 230*e1fe3e4aSElliott Hughes 231*e1fe3e4aSElliott Hughes if args is None: 232*e1fe3e4aSElliott Hughes args = sys.argv[1:] 233*e1fe3e4aSElliott Hughes 234*e1fe3e4aSElliott Hughes if len(args) < 2: 235*e1fe3e4aSElliott Hughes print( 236*e1fe3e4aSElliott Hughes f"usage: fonttools ttLib.removeOverlaps INPUT.ttf OUTPUT.ttf [GLYPHS ...]" 237*e1fe3e4aSElliott Hughes ) 238*e1fe3e4aSElliott Hughes sys.exit(1) 239*e1fe3e4aSElliott Hughes 240*e1fe3e4aSElliott Hughes src = args[0] 241*e1fe3e4aSElliott Hughes dst = args[1] 242*e1fe3e4aSElliott Hughes glyphNames = args[2:] or None 243*e1fe3e4aSElliott Hughes 244*e1fe3e4aSElliott Hughes with ttFont.TTFont(src) as f: 245*e1fe3e4aSElliott Hughes removeOverlaps(f, glyphNames) 246*e1fe3e4aSElliott Hughes f.save(dst) 247*e1fe3e4aSElliott Hughes 248*e1fe3e4aSElliott Hughes 249*e1fe3e4aSElliott Hughesif __name__ == "__main__": 250*e1fe3e4aSElliott Hughes main() 251