From b2de9cf3f3f49602196ed6a3fcf63fdedcbfc782 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 09:46:41 +0100 Subject: [PATCH 01/42] Fix typo --- python/afdko/otfautohint/report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/afdko/otfautohint/report.py b/python/afdko/otfautohint/report.py index 5f864325b..9f97637ae 100644 --- a/python/afdko/otfautohint/report.py +++ b/python/afdko/otfautohint/report.py @@ -30,7 +30,7 @@ def clear(self): self.hstems_pos.clear() self.vstems_pos.clear() self.char_zones.clear() - self.stem_zones_stems.clear() + self.stem_zone_stems.clear() def charZone(self, l, u): self.char_zones.add((l, u)) From 1448a92822a915767cbb01cd1defee667bd543b4 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 09:46:52 +0100 Subject: [PATCH 02/42] Wait, this whole method is dead --- python/afdko/otfautohint/report.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/python/afdko/otfautohint/report.py b/python/afdko/otfautohint/report.py index 9f97637ae..c3e451ea5 100644 --- a/python/afdko/otfautohint/report.py +++ b/python/afdko/otfautohint/report.py @@ -24,14 +24,6 @@ def __init__(self, name=None, all_stems=False): self.stem_zone_stems = set() self.all_stems = all_stems - def clear(self): - self.hstems.clear() - self.vstems.clear() - self.hstems_pos.clear() - self.vstems_pos.clear() - self.char_zones.clear() - self.stem_zone_stems.clear() - def charZone(self, l, u): self.char_zones.add((l, u)) From fb4e2c5a67e4faae96c0885b33d0027de0c1298d Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 09:47:37 +0100 Subject: [PATCH 03/42] Typo --- python/afdko/otfautohint/hintstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index 691f5d95d..d56a7c233 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -106,7 +106,7 @@ def current(self, orig=None): if self.replacedBy is None: return self if self is orig: - self.error("Cycle in hint segment replacement") + log.error("Cycle in hint segment replacement") return self if orig is None: orig = self From fc4a454dd5948d76f155dfdffc663b406745daac Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 09:57:12 +0100 Subject: [PATCH 04/42] Assertions to avoid unbound/None access --- python/afdko/otfautohint/fdTools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/afdko/otfautohint/fdTools.py b/python/afdko/otfautohint/fdTools.py index ad964866f..b96bea8b0 100644 --- a/python/afdko/otfautohint/fdTools.py +++ b/python/afdko/otfautohint/fdTools.py @@ -505,7 +505,7 @@ def mergeFDDicts(prevDictList): if dList is not None: for width in dList: stemDict[width] = prefDDict.DictName - + assert prefDDict # Now we have collected all the stem widths and zones # from all the dicts. See if we can merge them. goodBlueZoneList = [] @@ -736,6 +736,7 @@ def __init__(self, options, fontInstances, glyphList, isVF=False): log.error("Cannot continue") sys.exit() + assert self.fdSelectMap if options.printFDDictList or options.printAllFDDict: # Print the user defined FontDicts, and exit. print("Private Dictionaries:\n") From 57fbdced9fb09f314b46175468292d3972d667a4 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 10:15:18 +0100 Subject: [PATCH 05/42] ABC annotations only make sense if we're an ABC --- python/afdko/otfautohint/hinter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index a21565387..7131ea2dc 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -10,7 +10,7 @@ import bisect import math from copy import copy, deepcopy -from abc import abstractmethod +from abc import abstractmethod, ABC from collections import namedtuple from fontTools.misc.bezierTools import solveCubic @@ -52,7 +52,7 @@ # whether to replace one value with another. It should be short-lived. -class dimensionHinter: +class dimensionHinter(ABC): """ Common hinting implementation inherited by vertical and horizontal variants From b7e51f628fefb698e836818e8fb48dc4db19d49c Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 10:34:04 +0100 Subject: [PATCH 06/42] More assertions to calm type checkers --- python/afdko/otfautohint/autohint.py | 1 + python/afdko/otfautohint/hinter.py | 13 +++++++++---- python/afdko/otfautohint/ufoFont.py | 3 +++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/python/afdko/otfautohint/autohint.py b/python/afdko/otfautohint/autohint.py index 286da23e1..01b93cdf6 100644 --- a/python/afdko/otfautohint/autohint.py +++ b/python/afdko/otfautohint/autohint.py @@ -295,6 +295,7 @@ def hint(self): pcount = self.options.process_count if pcount is None: pcount = os.cpu_count() + assert pcount is not None if pcount < 0: pcount = os.cpu_count() - pcount if pcount < 0: diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index 7131ea2dc..a27a5ad4d 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -358,6 +358,7 @@ def calcHintValues(self, lnks, force=True, tryCounter=True): Top-level method for calculating stem hints for a glyph in one dimension """ + assert self.glyph is not None self.startHint() if self.glyph.hasHints(doVert=self.isV()): if force: @@ -385,6 +386,7 @@ def calcHintValues(self, lnks, force=True, tryCounter=True): for sv in self.hs.stemValues: sv.show(self.isV(), "postmerge") log.debug("pick main %s" % self.aDesc()) + assert self.glyph is not None self.glyph.syncPositions() lnks.mark(self.hs.stemValues, self.isV()) self.checkVals() @@ -1714,12 +1716,14 @@ def addBBox(self, doSubpaths=False): continue lseg = hintSegment(mn_pt.o, mn_pt.a, mx_pt.a, pbs.extpes[0][peidx], ltype, 0, self.isV(), not self.isV(), "l bbox") - self.hs.getPEState(pbs.extpes[0][peidx], - True).m_segs.append(lseg) + pestate = self.hs.getPEState(pbs.extpes[0][peidx], True) + assert pestate is not None + pestate.m_segs.append(lseg) useg = hintSegment(mx_pt.o, mn_pt.a, mx_pt.a, pbs.extpes[1][peidx], utype, 0, self.isV(), self.isV(), "u bbox") - self.hs.getPEState(pbs.extpes[1][peidx], - True).m_segs.append(useg) + pestate = self.hs.getPEState(pbs.extpes[1][peidx], True) + assert pestate is not None + pestate.m_segs.append(useg) hv = stemValue(mn_pt.o, mx_pt.o, 100, 0, lseg, useg, False) self.insertStemValue(hv, "bboxadd") self.hs.mainValues.append(hv) @@ -1848,6 +1852,7 @@ def calcInstanceStems(self, glidx): iSS = iSSl[sidx][ul] found = False + assert self.glyph if seg0.isBBox(): if seg0.isGBBox(): pbs = self.glyph.getBounds(None) diff --git a/python/afdko/otfautohint/ufoFont.py b/python/afdko/otfautohint/ufoFont.py index 193ee8c24..625542f62 100644 --- a/python/afdko/otfautohint/ufoFont.py +++ b/python/afdko/otfautohint/ufoFont.py @@ -395,6 +395,7 @@ def updateFromGlyph(self, glyph, name): if name in self.processedLayerGlyphMap: layer = PROCESSED_LAYER_NAME glyphset = self._get_glyphset(layer) + assert glyphset gdwrap = GlyphDataWrapper(glyph) glyphset.readGlyph(name, gdwrap) @@ -565,6 +566,8 @@ def _get_glyphset(self, layer_name=None): glyphset = self._reader.getGlyphSet(layer_name) except UFOLibError: pass + if glyphset is None: + raise FontParseError("No glyphset found for layer '%s'" % layer_name) self._glyphsets[layer_name] = glyphset return self._glyphsets[layer_name] From 10700c21a8f42fb53410753c5726f2128c432872 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 10:34:18 +0100 Subject: [PATCH 07/42] Dead code? --- python/afdko/otfautohint/hinter.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index a27a5ad4d..85356c1d8 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -1857,6 +1857,11 @@ def calcInstanceStems(self, glidx): if seg0.isGBBox(): pbs = self.glyph.getBounds(None) else: + # I don't believe this can be reached, because + # peS is a pathElementHintState, and they + # don't have a position attribute, so this + # would crash + raise NotImplementedError pbs = self.glyph.getBounds(peSi.position[0]) if pbs is not None: mn_pt = pt(tuple(pbs.bounds[0])) From bf643ad247c71864317665b7ff5a0fa9b6162e59 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 11:59:28 +0100 Subject: [PATCH 08/42] Hopefully uncontroversial typings --- python/afdko/otfautohint/glyphData.py | 16 +- python/afdko/otfautohint/hinter.py | 266 ++++++++++++++------------ python/afdko/otfautohint/hintstate.py | 201 +++++++++++-------- 3 files changed, 274 insertions(+), 209 deletions(-) diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index 9df302d7a..4df008037 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -18,10 +18,13 @@ from fontTools.pens.basePen import BasePen import logging + log = logging.getLogger(__name__) +Number = Union[int, float] + -def norm_float(value): +def norm_float(value: float) -> Number: """Converts a float (whose decimal part is zero) to integer""" if isinstance(value, float): value = round(value, 4) @@ -31,12 +34,12 @@ def norm_float(value): return value -def feq(a, b, factor=1.52e-5): +def feq(a: float, b: float, factor=1.52e-5) -> bool: """Returns True if a and b are close enough to be considered equal""" return abs(a - b) < factor -def fne(a, b, factor=1.52e-5): +def fne(a: float, b: float, factor=1.52e-5) -> bool: """Returns True if a and b are not close enough to be considered equal""" return abs(a - b) >= factor @@ -787,12 +790,12 @@ def fonttoolsSegment(self): if self.is_line: return [tuple(self.s), tuple(self.e)] else: - return [tuple(self.s), tuple(self.cs), tuple(self.ce), - tuple(self.e)] + return [tuple(self.s), tuple(self.cs), tuple(self.ce), tuple(self.e)] class glyphData(BasePen): """Stores state corresponding to a T2 CharString""" + def __init__(self, roundCoords, name=''): super().__init__() self.roundCoords = roundCoords @@ -1048,7 +1051,7 @@ def drawPoints(self, pen, ufoH=None): wrapi -= 1 w = s[wrapi] pen.beginPath() - wt = 'line' if w.isLine() else "curve" + wt = 'line' if w.isLine() else 'curve' if doHints: pln, pn = ufoH(w, pln, True) pen.addPoint((w.e.x, w.e.y), segmentType=wt, name=pn) @@ -1245,6 +1248,7 @@ def prevSlopePoint(self, c): class glyphiter: """An iterator for a glyphData path""" + __slots__ = ('gd', 'pos') def __init__(self, gd): diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index 85356c1d8..30ae6a097 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -15,17 +15,27 @@ from fontTools.misc.bezierTools import solveCubic -from .glyphData import pt, feq, fne, stem +from .glyphData import Number, pathElement, pt, feq, fne, stem from .hintstate import (hintSegment, stemValue, glyphHintState, links, instanceStemState) from .overlap import removeOverlap from .report import GlyphReport +from .fdTools import FDDict from .logging import logging_reconfig, set_log_parameters -log = logging.getLogger(__name__) -GlyphPE = namedtuple("GlyphPE", "glyph pe") -LocDict = namedtuple("LocDict", "l u used") +class GlyphPE(NamedTuple): + glyph: Any + pe: Any + + +class LocDict(NamedTuple): + l: Any + u: Any + used: Any + + +log: logging.Logger = logging.getLogger(__name__) # A few variable conventions not documented elsewhere: @@ -58,14 +68,14 @@ class dimensionHinter(ABC): variants """ @staticmethod - def diffSign(a, b): + def diffSign(a, b) -> bool: return fne(a, 0) and fne(b, 0) and ((a > 0) != (b > 0)) @staticmethod - def sameSign(a, b): + def sameSign(a, b) -> bool: return fne(a, 0) and fne(b, 0) and ((a > 0) == (b > 0)) - def __init__(self, options): + def __init__(self, options) -> None: self.StemLimit = 22 # ((kStackLimit) - 2) / 2), kStackLimit == 46 """Initialize constant values and miscelaneous state""" self.MaxStemDist = 150 # initial maximum stem width allowed for hints @@ -137,8 +147,12 @@ def __init__(self, options): self.report = None self.name = None self.isMulti = False + self.hs : Optional[glyphHintState] = None + self.fddict: Optional[FDDict] = None + self.Bonus = None + self.Pruning = None - def setGlyph(self, fddicts, report, gllist, name, clearPrev=True): + def setGlyph(self, fddicts, report, gllist, name, clearPrev=True) -> None: """Initialize the state for processing a specific glyph""" self.fddicts = fddicts self.report = report @@ -155,7 +169,7 @@ def setGlyph(self, fddicts, report, gllist, name, clearPrev=True): else: self.hs = self.glyph.hhs = glyphHintState() - def resetForHinting(self): + def resetForHinting(self) -> None: """Reset state for rehinting same glyph""" self.Bonus = None self.Pruning = True @@ -168,7 +182,7 @@ class glIter: """A pathElement set iterator for the glyphData object list""" __slots__ = ('gll', 'il') - def __init__(self, gllist, glidx=None): + def __init__(self, gllist, glidx=None) -> None: if glidx is not None: assert isinstance(glidx, int) and glidx > 0 self.gll = [gllist[0], gllist[glidx]] @@ -176,47 +190,47 @@ def __init__(self, gllist, glidx=None): self.gll = gllist self.il = [gl.__iter__() for gl in self.gll] - def __next__(self): + def __next__(self) -> List[GlyphPE]: return [GlyphPE(self.gll[i], ii.__next__()) for i, ii in enumerate(self.il)] - def __iter__(self): + def __iter__(self) -> Self: return self - def __iter__(self, glidx=None): + def __iter__(self, glidx=None) -> glIter: return self.glIter(self.gllist, glidx) # Methods implemented by subclasses @abstractmethod - def startFlex(self): + def startFlex(self) -> None: pass @abstractmethod - def stopFlex(self): + def stopFlex(self) -> None: pass @abstractmethod - def startHint(self): + def startHint(self) -> None: pass @abstractmethod - def stopHint(self): + def stopHint(self) -> None: pass @abstractmethod - def startStemConvert(self): + def startStemConvert(self) -> None: pass @abstractmethod - def stopStemConvert(self): + def stopStemConvert(self) -> None: pass @abstractmethod - def startMaskConvert(self): + def startMaskConvert(self) -> None: pass @abstractmethod - def stopMaskConvert(self): + def stopMaskConvert(self) -> None: pass @abstractmethod @@ -232,34 +246,34 @@ def dominantStems(self): pass @abstractmethod - def isCounterGlyph(self): + def isCounterGlyph(self) -> Any: pass @abstractmethod - def inBand(self, loc, isBottom=False): + def inBand(self, loc, isBottom=False) -> bool: pass @abstractmethod - def hasBands(self): + def hasBands(self) -> Any: pass @abstractmethod - def aDesc(self): + def aDesc(self) -> Any: pass @abstractmethod - def isSpecial(self, lower=False): + def isSpecial(self, lower=False) -> bool: pass @abstractmethod - def checkTfm(self): + def checkTfm(self) -> Any: pass # Flex - def linearFlexOK(self): + def linearFlexOK(self) -> bool: return False - def addFlex(self, force=True, inited=False): + def addFlex(self, force=True, inited=False) -> None: """Path-level interface to add flex hints to current glyph""" self.startFlex() hasflex = (gl.flex_count != 0 for gl in self.gllist) @@ -276,7 +290,7 @@ def addFlex(self, force=True, inited=False): self.markFlex(cl) self.stopFlex() - def tryFlex(self, gl, c): + def tryFlex(self, gl, c) -> bool: """pathElement-level interface to add flex hints to current glyph""" # Not a curve, already flexed, or flex depth would be too large if not c or c.isLine() or c.flex or c.s.a_dist(c.e) > self.MaxFlex: @@ -342,7 +356,7 @@ def tryFlex(self, gl, c): return True - def markFlex(self, cl): + def markFlex(self, cl) -> None: for gl, c in cl: n = gl.nextInSubpath(c, skipTiny=False) c.flex = 1 @@ -353,7 +367,7 @@ def markFlex(self, cl): log.info("Added flex operators to this glyph.") self.HasFlex = True - def calcHintValues(self, lnks, force=True, tryCounter=True): + def calcHintValues(self, lnks, force=True, tryCounter=True) -> None: """ Top-level method for calculating stem hints for a glyph in one dimension @@ -420,7 +434,7 @@ def calcHintValues(self, lnks, force=True, tryCounter=True): self.stopHint() # Segments - def handleOverlap(self): + def handleOverlap(self) -> bool: if self.options.overlapForcing is True: return True elif self.options.overlapForcing is False: @@ -430,7 +444,7 @@ def handleOverlap(self): else: return self.isMulti - def addSegment(self, fr, to, loc, pe1, pe2, typ, desc, mid=False): + def addSegment(self, fr, to, loc, pe1: Optional[pathElement], pe2: Optional[pathElement], typ, desc, mid=False) -> None: if pe1 is not None and isinstance(pe1.segment_sub, int): subpath, offset = pe1.position t = self.glyph.subpaths[subpath][offset] @@ -461,29 +475,30 @@ def addSegment(self, fr, to, loc, pe1, pe2, typ, desc, mid=False): if not pe1 and not pe2: return + assert self.hs is not None self.hs.addSegment(fr, to, loc, pe1, pe2, typ, self.Bonus, self.isV(), mid1, mid2, desc) - def CPFrom(self, cp2, cp3): + def CPFrom(self, cp2, cp3) -> Any: """Return point cp3 adjusted relative to cp2 by CPFrac""" return (cp3 - cp2) * (1.0 - self.CPfrac) + cp2 - def CPTo(self, cp0, cp1): + def CPTo(self, cp0, cp1) -> Any: """Return point cp1 adjusted relative to cp0 by CPFrac""" return (cp1 - cp0) * self.CPfrac + cp0 - def adjustDist(self, v, q): + def adjustDist(self, v, q) -> Any: return v * q - def testTan(self, p): + def testTan(self, p) -> Any: """Test angle of p (treated as vector) relative to BendTangent""" return abs(p.a) > (abs(p.o) * self.BendTangent) @staticmethod - def interpolate(q, v0, q0, v1, q1): + def interpolate(q, v0, q0, v1, q1) -> Any: return v0 + (q - q0) * (v1 - v0) / (q1 - q0) - def flatQuo(self, p1, p2, doOppo=False): + def flatQuo(self, p1, p2, doOppo=False) -> Any: """ Returns a measure of the flatness of the line between p1 and p2 @@ -513,7 +528,7 @@ def flatQuo(self, p1, p2, doOppo=False): result = 0 return result - def testBend(self, p0, p1, p2): + def testBend(self, p0, p1, p2) -> bool: """Test of the angle between p0-p1 and p1-p2""" d1 = p1 - p0 d2 = p2 - p1 @@ -524,7 +539,7 @@ def testBend(self, p0, p1, p2): return False return dp * dp / q <= 0.5 - def isCCW(self, p0, p1, p2): + def isCCW(self, p0, p1, p2) -> bool: """ Returns true if p0 -> p1 -> p2 is counter-clockwise in glyph space. """ @@ -534,7 +549,7 @@ def isCCW(self, p0, p1, p2): # Generate segments - def relPosition(self, c, lower=False): + def relPosition(self, c, lower=False) -> bool: """ Return value indicates whether c is in the upper (or lower) subpath of the glyph (assuming a strict ordering of subpaths @@ -548,7 +563,7 @@ def relPosition(self, c, lower=False): # I initially combined the two doBends but the result was more confusing # and difficult to debug than having them separate - def doBendsNext(self, c): + def doBendsNext(self, c) -> None: """ Adds a BEND segment (short segments marking somewhat flat areas) at the end of a spline. In some cases the segment is @@ -586,7 +601,7 @@ def doBendsNext(self, c): self.addSegment(end, strt, p1.o, c, None, hintSegment.sType.BEND, 'next bend reverse') - def doBendsPrev(self, c): + def doBendsPrev(self, c) -> None: """ Adds a BEND segment (short segments marking somewhat flat areas) at the start of a spline. In some cases the segment is @@ -620,7 +635,7 @@ def doBendsPrev(self, c): self.addSegment(strt, end, p0.o, cs, None, hintSegment.sType.BEND, 'prev bend forward') - def nodeIsFlat(self, c, doPrev=False): + def nodeIsFlat(self, c, doPrev=False) -> Optional[bool]: """ Returns true if the junction of this spline and the next (or previous) is sufficiently flat, measured by OppoFlatMax @@ -640,7 +655,7 @@ def nodeIsFlat(self, c, doPrev=False): d = (sp - c.ce).abs() return d.o <= self.OppoFlatMax and d.a >= self.FlatMin - def sameDir(self, c, doPrev=False): + def sameDir(self, c, doPrev=False) -> Optional[bool]: """ Returns True if the next (or previous) spline continues in roughly the same direction as c @@ -662,7 +677,7 @@ def sameDir(self, c, doPrev=False): return False return not self.testBend(p0, p1, p2) - def extremaSegment(self, pe, extp, extt, isMn): + def extremaSegment(self, pe, extp, extt, isMn) -> Tuple[Any, Any]: """ Given a curved pathElement pe and a point on that spline extp at t == extt, calculates a segment intersecting extp where all portions @@ -699,7 +714,7 @@ def extremaSegment(self, pe, extp, extt, isMn): return mn_p.a, mx_p.a - def pickSpot(self, p0, p1, dist, pp0, pp1, prv, nxt): + def pickSpot(self, p0, p1, dist, pp0, pp1, prv, nxt) -> Number: """ Picks a segment location based on candidates p0 and p1 and other locations and metrics picked from the spline and @@ -733,7 +748,7 @@ def pickSpot(self, p0, p1, dist, pp0, pp1, prv, nxt): return p1.o return (p0.o + p1.o) / 2 - def cpDirection(self, p0, p1, p2): + def cpDirection(self, p0, p1, p2) -> int: """ Utility function for detecting singly-inflected curves. See original C code or "Fast Detection o the Geometric Form of @@ -748,7 +763,7 @@ def cpDirection(self, p0, p1, p2): return -1 return 0 - def prepForSegs(self): + def prepForSegs(self) -> None: for c in self.glyph: if (not c.isLine() and (self.cpDirection(c.s, c.cs, c.ce) != @@ -757,7 +772,7 @@ def prepForSegs(self): log.debug("splitting at inflection point in %d %d" % (c.position[0], c.position[1] + 1)) - def genSegs(self): + def genSegs(self) -> None: """ Calls genSegsForPathElement for each pe and cleans up the generated segment lists @@ -795,7 +810,7 @@ def genSegs(self): self.hs.cleanup() self.checkTfm() - def genSegsForPathElement(self, c): + def genSegsForPathElement(self, c) -> None: """ Calculates and adds segments for pathElement c. These segments indicate "flat" areas of the glyph in the relevant dimension @@ -994,7 +1009,7 @@ def genSegsForPathElement(self, c): hintSegment.sType.CURVE, "curve extrema", True) - def limitSegs(self): + def limitSegs(self) -> None: maxsegs = max(len(self.hs.increasingSegs), len(self.hs.decreasingSegs)) if (not self.options.explicitGlyphs and maxsegs > self.options.maxSegments): @@ -1002,7 +1017,7 @@ def limitSegs(self): (maxsegs, self.aDesc())) self.hs.deleteSegments() - def showSegs(self): + def showSegs(self) -> None: """ Adds a debug log message for each generated segment. This information is redundant with the genSegs info except that @@ -1022,7 +1037,7 @@ def showSegs(self): # Generate candidate stems with values - def genStemVals(self): + def genStemVals(self) -> None: """ Pairs segments of opposite direction and adds them as potential stems weighted by evalPair(). Also adds ghost stems for segments @@ -1061,7 +1076,7 @@ def genStemVals(self): self.GhostSpecial, ghostSeg, s) self.combineStemValues() - def evalPair(self, ls, us): + def evalPair(self, ls, us) -> Tuple[Any, int]: """ Calculates the initial "value" and "special" weights of a potential stem. @@ -1123,7 +1138,7 @@ def evalPair(self, ls, us): v = min(v, self.MaxValue) return v, spc - def stemMiss(self, ls, us): + def stemMiss(self, ls, us) -> Optional[int]: """ Adds an info message for each stem within two em-units of a dominant stem width @@ -1153,7 +1168,7 @@ def stemMiss(self, ls, us): "curve" if (ls.isCurve() or us.isCurve()) else "linear", loc_d, nearStem, ls.loc, us.loc)) - def addStemValue(self, lloc, uloc, val, spc, lseg, useg): + def addStemValue(self, lloc, uloc, val, spc, lseg, useg) -> None: """Adapts the stem parameters into a stemValue object and adds it""" if val == 0: return @@ -1178,7 +1193,7 @@ def addStemValue(self, lloc, uloc, val, spc, lseg, useg): sv = stemValue(lloc, uloc, val, spc, lseg, useg, ghst) self.insertStemValue(sv) - def insertStemValue(self, sv, note="add"): + def insertStemValue(self, sv, note="add") -> None: """ Adds a stemValue object into the stemValues list in sort order, skipping redundant GHOST stems @@ -1197,7 +1212,7 @@ def insertStemValue(self, sv, note="add"): svl.insert(i, sv) sv.show(self.isV(), note) - def combineStemValues(self): + def combineStemValues(self) -> None: """ Adjusts the values of stems with the same locations to give them each the same combined value. @@ -1220,7 +1235,7 @@ def combineStemValues(self): # Prune unneeded candidate stems - def pruneStemVals(self): + def pruneStemVals(self) -> None: """ Prune (remove) candidate stems based on comparisons to other stems. """ @@ -1283,7 +1298,7 @@ def pruneStemVals(self): break self.hs.stemValues = [sv for sv in self.hs.stemValues if not sv.pruned] - def closeSegs(self, s1, s2): + def closeSegs(self, s1, s2) -> bool: """ Returns true if the segments (and the path between them) are within CloseMerge of one another @@ -1322,7 +1337,7 @@ def closeSegs(self, s1, s2): p = self.glyph.prevInSubpath(p) return False - def prune(self, sv, other_sv, desc): + def prune(self, sv, other_sv, desc) -> None: """ Sets the pruned property on sv and logs it and the "better" stemValue """ @@ -1333,7 +1348,7 @@ def prune(self, sv, other_sv, desc): # Associate segments with the highest valued close stem - def highestStemVals(self): + def highestStemVals(self) -> None: """ Associates each segment in both lists with the highest related stemVal, pruning stemValues with no association @@ -1346,7 +1361,7 @@ def highestStemVals(self): self.hs.stemValues = [sv for sv in self.hs.stemValues if not sv.pruned] - def findHighestValForSegs(self, segl, isU): + def findHighestValForSegs(self, segl, isU) -> None: """Associates each segment in segl with the highest related stemVal""" for seg in segl: ghst = None @@ -1362,7 +1377,7 @@ def findHighestValForSegs(self, segl, isU): highest.pruned = False seg.hintval = highest - def findHighestVal(self, seg, isU, locFlag): + def findHighestVal(self, seg, isU, locFlag) -> Any: """Finds the highest stemVal related to seg""" highest = None svl = self.hs.stemValues @@ -1389,7 +1404,7 @@ def OKcond(sv): log.debug("NULL") return highest - def considerValForSeg(self, sv, seg, isU): + def considerValForSeg(self, sv, seg, isU) -> bool: """Utility test for findHighestVal""" if sv.spc > 0 or self.inBand(seg.loc, not isU): return True @@ -1397,7 +1412,7 @@ def considerValForSeg(self, sv, seg, isU): # Merge related candidate stems - def findBestValues(self): + def findBestValues(self) -> None: """ Looks among stemValues with the same locations and finds the one with the highest spc/val. Assigns that stemValue to the .best @@ -1421,7 +1436,7 @@ def findBestValues(self): for sv in blst: sv.best = svl[b] - def replaceVals(self, oldl, oldu, newl, newu, newbest): + def replaceVals(self, oldl, oldu, newl, newu, newbest) -> None: """ Finds each stemValue at oldl, oldu and gives it a new "best" stemValue reference and its val and spc. @@ -1440,7 +1455,7 @@ def replaceVals(self, oldl, oldu, newl, newu, newbest): sv.best = newbest sv.merge = True - def mergeVals(self): + def mergeVals(self) -> None: """ Finds stem pairs with sides close to one another (in different senses) and uses replaceVals() to substitute one for another @@ -1507,7 +1522,7 @@ def mergeVals(self): # Limit number of stems - def limitVals(self): + def limitVals(self) -> None: """ Limit the number of stem values in a dimension """ @@ -1529,7 +1544,7 @@ def limitVals(self): # Reporting - def checkVals(self): + def checkVals(self) -> None: """Reports stems with widths close to a dominant stem width""" lPrev = uPrev = -1e20 for sv in self.hs.stemValues: @@ -1551,14 +1566,14 @@ def checkVals(self): "%g instead of %g at %g to %g" % (w, mdw, l, u)) lPrev, uPrev = l, u - def findLineSeg(self, loc, isBottom=False): + def findLineSeg(self, loc, isBottom=False) -> bool: """Returns LINE segments with the passed location""" for s in self.segmentLists()[0 if isBottom else 1]: if feq(s.loc, loc) and s.isLine(): return True return False - def reportStems(self): + def reportStems(self) -> None: """Reports stem zones and char ("alignment") zones""" glyphTop = -1e40 glyphBot = 1e40 @@ -1578,7 +1593,7 @@ def reportStems(self): # "Main" stems - def mainVals(self): + def mainVals(self) -> None: """Picks set of highest-valued non-overlapping stems""" mainValues = [] rejectValues = [] @@ -1636,7 +1651,7 @@ def mainVals(self): self.hs.mainValues = mainValues self.hs.rejectValues = rejectValues - def mainOK(self, spc, val, hasHints, prevBV): + def mainOK(self, spc, val, hasHints, prevBV) -> Any: """Utility test for mainVals""" if spc > 0: return True @@ -1648,7 +1663,7 @@ def mainOK(self, spc, val, hasHints, prevBV): return False return not self.Pruning or prevBV <= val * self.PruneC - def tryCounterHinting(self): + def tryCounterHinting(self) -> bool: """ Attempts to counter-hint the dimension with the first three (e.g. highest value) mainValue stems @@ -1688,7 +1703,7 @@ def tryCounterHinting(self): log.info("Near miss for %s counter hints." % self.aDesc()) return False - def addBBox(self, doSubpaths=False): + def addBBox(self, doSubpaths=False) -> None: """ Adds the top and bottom (or left and right) sides of the glyph as a stemValue -- serves as a backup hint stem when few are found @@ -1730,7 +1745,7 @@ def addBBox(self, doSubpaths=False): self.hs.mainValues.sort(key=lambda sv: sv.compVal(self.SpcBonus), reverse=True) - def markStraySegs(self): + def markStraySegs(self) -> None: """ highestStemVals() may not assign a hintval to a given segment. Once the list of stems has been arrived at we go through each @@ -1746,7 +1761,7 @@ def markStraySegs(self): # masks - def convertToStemLists(self): + def convertToStemLists(self) -> bool: """ This method builds up the information needed to mostly get away from looking at stem values when distributing hintmasks. @@ -1806,7 +1821,7 @@ def convertToStemLists(self): self.stopStemConvert() return True - def calcInstanceStems(self, glidx): + def calcInstanceStems(self, glidx) -> None: self.glyph = self.gllist[glidx] self.fddict = self.fddicts[glidx] hs0 = self.hs @@ -1921,7 +1936,7 @@ def calcInstanceStems(self, glidx): set_log_parameters(instance=self.fddict.FontName) self.hs = hs0 - def bestLocation(self, sidx, ul, iSSl, hs0): + def bestLocation(self, sidx, ul, iSSl, hs0) -> Any: loc = iSSl[sidx][ul].bestLocation(ul == 0) if loc is not None: return loc @@ -1937,7 +1952,7 @@ def bestLocation(self, sidx, ul, iSSl, hs0): ('lower' if ul == 0 else 'upper', sidx)) return hs0.stems[0][sidx][ul] - def unconflict(self, sc, curSet=None, pinSet=None): + def unconflict(self, sc, curSet=None, pinSet=None) -> Any: l = len(sc) if curSet is None: @@ -1981,7 +1996,7 @@ def unconflict(self, sc, curSet=None, pinSet=None): r2 = self.unconflict(sc, curSet, pinSet) return r1 if r1[0] > r2[0] else r2 - def convertToMasks(self): + def convertToMasks(self) -> None: """ This method builds up the information needed to mostly get away from looking at stem values when distributing hintmasks. @@ -2151,7 +2166,7 @@ def makePEMask(self, pestate, c): else: pestate.mask = None - def OKToRem(self, loc, spc): + def OKToRem(self, loc, spc) -> bool: return (spc == 0 or (not self.inBand(loc, False) and not self.inBand(loc, True))) @@ -2162,17 +2177,17 @@ def startFlex(self): set_log_parameters(dimension='-') pt.setAlign(False) - def stopFlex(self): + def stopFlex(self) -> None: set_log_parameters(dimension='') pt.clearAlign() - def startHint(self): + def startHint(self) -> None: """ Make pt.a map to x and pt.b map to y and store BlueValue bands for easier processing """ self.startFlex() - blues = self.fddict.BlueValuesPairs + self.fddict.OtherBlueValuesPairs + blues = self.fddict.BlueValuesPairs + self.fddict.OtherBlueValuesPairs # pytype: disable=attribute-error self.topPairs = [pair for pair in blues if not pair[4]] self.bottomPairs = [pair for pair in blues if pair[4]] @@ -2180,14 +2195,14 @@ def startHint(self): stopHint = stopStemConvert = stopMaskConvert = stopFlex - def dominantStems(self): - return self.fddict.DominantH + def dominantStems(self) -> Any: + return self.fddict.DominantH # pytype: disable=attribute-error - def isV(self): + def isV(self) -> bool: """Mark the hinter as horizontal rather than vertical""" return False - def inBand(self, loc, isBottom=False): + def inBand(self, loc, isBottom=False) -> bool: """Return true if loc is within the selected set of bands""" if self.name in self.options.noBlues: return False @@ -2196,36 +2211,37 @@ def inBand(self, loc, isBottom=False): else: pl = self.topPairs for p in pl: - if (p[0] + self.fddict.BlueFuzz >= loc and - p[1] - self.fddict.BlueFuzz <= loc): + if (p[0] + self.fddict.BlueFuzz >= loc and # pytype: disable=attribute-error + p[1] - self.fddict.BlueFuzz <= loc): # pytype: disable=attribute-error return True return False - def hasBands(self): + def hasBands(self) -> bool: return len(self.topPairs) + len(self.bottomPairs) > 0 - def isSpecial(self, lower=False): + def isSpecial(self, lower=False) -> bool: return False - def aDesc(self): + def aDesc(self) -> str: return 'horizontal' - def checkTfm(self): + def checkTfm(self) -> None: + assert self.hs self.checkTfmVal(self.hs.decreasingSegs, self.topPairs) self.checkTfmVal(self.hs.increasingSegs, self.bottomPairs) - def checkTfmVal(self, sl, pl): + def checkTfmVal(self, sl, pl) -> None: for s in sl: if not self.checkInsideBands(s.loc, pl): self.checkNearBands(s.loc, pl) - def checkInsideBands(self, loc, pl): + def checkInsideBands(self, loc, pl) -> bool: for p in pl: if loc <= p[0] and loc >= p[1]: return True return False - def checkNearBands(self, loc, pl): + def checkNearBands(self, loc, pl) -> None: for p in pl: if loc >= p[1] - self.NearFuzz and loc < p[1]: log.info("Near miss above horizontal zone at " + @@ -2242,30 +2258,30 @@ def isCounterGlyph(self): class vhinter(dimensionHinter): - def startFlex(self): + def startFlex(self) -> None: set_log_parameters(dimension='|') pt.setAlign(True) - def stopFlex(self): + def stopFlex(self) -> None: set_log_parameters(dimension='') pt.clearAlign() startHint = startStemConvert = startMaskConvert = startFlex stopHint = stopStemConvert = stopMaskConvert = stopFlex - def isV(self): + def isV(self) -> bool: return True - def dominantStems(self): - return self.fddict.DominantV + def dominantStems(self) -> Any: + return self.fddict.DominantV # pytype: disable=attribute-error - def inBand(self, loc, isBottom=False): + def inBand(self, loc, isBottom=False) -> bool: return False - def hasBands(self): + def hasBands(self) -> bool: return False - def isSpecial(self, lower=False): + def isSpecial(self, lower=False) -> bool: """Check the Specials list for the current glyph""" if lower: return self.name in self.options.lowerSpecials @@ -2291,16 +2307,16 @@ class glyphHinter: Also contains code that uses hints from both dimensions, primarily for hintmask distribution """ - impl = None + impl: Optional[Self] = None @classmethod - def initialize(cls, options, dictRecord, logQueue=None): + def initialize(cls, options, dictRecord, logQueue=None) -> None: cls.impl = cls(options, dictRecord) if logQueue is not None: logging_reconfig(logQueue, options.verbose) @classmethod - def hint(cls, name, glyphTuple=None, fdKey=None): + def hint(cls, name, glyphTuple=None, fdKey=None) -> Any: if cls.impl is None: raise RuntimeError("glyphHinter implementation not initialized") if isinstance(name, tuple): @@ -2308,7 +2324,7 @@ def hint(cls, name, glyphTuple=None, fdKey=None): else: return cls.impl._hint(name, glyphTuple, fdKey) - def __init__(self, options, dictRecord): + def __init__(self, options, dictRecord) -> None: self.options = options self.dictRecord = dictRecord self.hHinter = hhinter(options) @@ -2439,7 +2455,7 @@ def _hint(self, name, glyphTuple, fdKey): set_log_parameters(glyph='', instance='') return name, glyphTuple - def compatiblePaths(self, gllist, fddicts): + def compatiblePaths(self, gllist, fddicts) -> bool: if len(gllist) < 2: return True @@ -2596,7 +2612,7 @@ def distributeMasks(self, glyph): return usedmasks - def buildCounterMasks(self, glyph): + def buildCounterMasks(self, glyph) -> None: """ For glyph dimensions that are counter-hinted, make a cntrmask with all Trues in that dimension (because only h/vstem3 style counter @@ -2668,7 +2684,7 @@ def joinMasks(self, m, cm, log): # XXX log conflict here if log is true return nm, conflict - def bridgeMasks(self, glyph, o, n, used, pe): + def bridgeMasks(self, glyph, o, n, used, pe) -> None: """ For switching hintmasks: Clean up o by adding compatible stems from mainMask and add stems from o to n when they are close to pe @@ -2711,10 +2727,10 @@ def bridgeMasks(self, glyph, o, n, used, pe): nm, _ = self.joinMasks(n, carryMask, False) n[:] = nm - def mergeMain(self, glyph): + def mergeMain(self, glyph) -> bool: return len(glyph.subpaths) <= 5 - def cleanupUnused(self, gllist, usedmasks): + def cleanupUnused(self, gllist, usedmasks) -> None: if (usedmasks is None or (False not in usedmasks[0] and False not in usedmasks[1])): return @@ -2736,12 +2752,12 @@ def cleanupUnused(self, gllist, usedmasks): glyph.startmasks = None glyph.is_hm = False - def delUnused(self, l, ml): + def delUnused(self, l, ml) -> None: """If ml[d][i] is False delete that entry from ml[d]""" for hv in range(2): l[hv][:] = [l[hv][i] for i in range(len(l[hv])) if ml[hv][i]] - def listHintInfo(self, glyph): + def listHintInfo(self, glyph) -> None: """ Output debug messages about which stems are associated with which segments @@ -2756,7 +2772,7 @@ def listHintInfo(self, glyph): for seg in vList: seg.hintval.show(True, "listhint") - def remFlares(self, glyph): + def remFlares(self, glyph) -> None: """ When two paths are witin MaxFlare and connected by a path that also stays within MaxFlare, and both desire different stems, @@ -2807,7 +2823,7 @@ def remFlares(self, glyph): break n = glyph.nextInSubpath(n) - def isFlare(self, loc, glyph, c, n): + def isFlare(self, loc, glyph, c, n) -> bool: """Utility function for remFlares""" while c is not n: v = c.e.x if self.doV else c.e.y @@ -2824,7 +2840,7 @@ def reportRemFlare(self, pe, pe2, desc): ("vertical" if self.doV else "horizontal", pe.e.x, pe.e.y, pe2.e.x, pe2.e.y, desc)) - def otherInstanceStems(self, gllist): + def otherInstanceStems(self, gllist) -> Optional[bool]: if len(gllist) < 2: return True @@ -2836,7 +2852,7 @@ def otherInstanceStems(self, gllist): g.hstems = glyph.hhs.stems[i] g.vstems = glyph.vhs.stems[i] - def otherInstanceMasks(self, gllist): + def otherInstanceMasks(self, gllist) -> Optional[bool]: if len(gllist) < 2: return True diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index d56a7c233..2d8ba93ef 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -7,16 +7,26 @@ import weakref import logging import bisect -from enum import Enum +from enum import IntEnum -from .glyphData import feq -log = logging.getLogger(__name__) +from .glyphData import feq, pathElement, stem +from _weakref import ReferenceType +from typing import Any, Dict, List, Optional, Set, Tuple, Type, Self, Protocol + + +log: logging.Logger = logging.getLogger(__name__) + + +class AHinter(Protocol): + def inBand(self, loc, isBottom=False) -> bool: + ... class hintSegment: """Represents a hint "segment" (one side of a potential stem)""" - class sType(Enum): + + class sType(IntEnum): LINE = 1 BEND = 2 CURVE = 3 @@ -26,7 +36,12 @@ class sType(Enum): UGBBOX = 7 # Calcualted from the upper bound of the whole glyph GHOST = 8 - def __init__(self, aloc, oMin, oMax, pe, typ, bonus, isV, isInc, desc): + pe: Optional[ReferenceType[pathElement]] + + + def __init__(self, aloc: float, oMin: float, oMax: float, + pe: pathElement, typ, bonus, isV, + isInc, desc) -> None: """ Initializes the object @@ -66,39 +81,39 @@ def __init__(self, aloc, oMin, oMax, pe, typ, bonus, isV, isInc, desc): else: self.pe = None self.hintval = None - self.replacedBy = None + self.replacedBy: Optional[Self] = None self.deleted = False self.suppressed = False - def __eq__(self, other): + def __eq__(self, other) -> bool: return feq(self.loc, other.loc) - def __lt__(self, other): + def __lt__(self, other) -> bool: return self.loc < other.loc - def isBend(self): + def isBend(self) -> bool: return self.type == self.sType.BEND - def isCurve(self): + def isCurve(self) -> bool: return self.type == self.sType.CURVE - def isLine(self): + def isLine(self) -> bool: return self.type == self.sType.LINE or self.isBBox() - def isBBox(self): + def isBBox(self) -> bool: return self.type in (self.sType.LSBBOX, self.sType.USBBOX, self.sType.LGBBOX, self.sType.UGBBOX) - def isGBBox(self): + def isGBBox(self) -> bool: return self.type in (self.sType.LGBBOX, self.sType.UGBBOX) - def isUBBox(self): + def isUBBox(self) -> bool: return self.type in (self.sType.USBBOX, self.sType.UGBBOX) - def isGhost(self): + def isGhost(self) -> bool: return self.type == self.sType.GHOST - def current(self, orig=None): + def current(self, orig=None) -> Self: """ Returns the object corresponding to this object relative to self.replacedBy @@ -112,7 +127,7 @@ def current(self, orig=None): orig = self return self.replacedBy.current(orig=self) - def show(self, label): + def show(self, label) -> None: """Logs a debug message about the segment""" if self.isV: pp = (label, 'v', self.loc, self.min, self.loc, self.max, @@ -123,9 +138,15 @@ def show(self, label): log.debug("%s %sseg %g %g to %g %g %s" % pp) +HintSegListWithType = Tuple[str, list[hintSegment]] + + class stemValue: """Represents a potential hint stem""" - def __init__(self, lloc, uloc, val, spc, lseg, useg, isGhost=False): + + def __init__(self, lloc: Number, uloc: Number, val: Number, + spc, lseg: hintSegment, useg: hintSegment, + isGhost=False) -> None: assert lloc <= uloc self.val = val self.spc = spc @@ -140,19 +161,19 @@ def __init__(self, lloc, uloc, val, spc, lseg, useg, isGhost=False): self.initialVal = val self.idx = None - def __eq__(self, other): + def __eq__(self, other: Self) -> bool: slloc, suloc = self.ghosted() olloc, ouloc = other.ghosted() return slloc == olloc and suloc == ouloc - def __lt__(self, other): + def __lt__(self, other: Self) -> bool: """Orders values by lower and then upper location""" slloc, suloc = self.ghosted() olloc, ouloc = other.ghosted() return (slloc < olloc or (slloc == olloc and suloc < ouloc)) # c.f. page 22 of Adobe TN #5177 "The Type 2 Charstring Format" - def ghosted(self): + def ghosted(self) -> Tuple[Any, Any]: """Return the stem range but with ghost stems normalized""" lloc, uloc = self.lloc, self.uloc if self.isGhost: @@ -164,7 +185,7 @@ def ghosted(self): lloc = uloc + 21 return (lloc, uloc) - def compVal(self, spcFactor=1, ghostFactor=1): + def compVal(self, spcFactor=1, ghostFactor=1) -> Tuple[Any, Any]: """Represent self.val and self.spc as a comparable 2-tuple""" v = self.val if self.isGhost: @@ -173,7 +194,7 @@ def compVal(self, spcFactor=1, ghostFactor=1): v *= spcFactor return (v, self.initialVal) - def show(self, isV, typ): + def show(self, isV, typ) -> None: """Add a log message with the content of the object""" tags = ('v', 'l', 'r', 'b', 't') if isV else ('h', 'b', 't', 'l', 'r') start = "%s %sval %s %g %s %g v %g s %g %s" % (typ, tags[0], tags[1], @@ -192,32 +213,37 @@ def show(self, isV, typ): class pathElementHintState: """Stores the intermediate hint state of a pathElement""" - def __init__(self): - self.s_segs = [] - self.m_segs = [] - self.e_segs = [] + + def __init__(self) -> None: + self.s_segs: List[hintSegment] = [] + self.m_segs: List[hintSegment] = [] + self.e_segs: List[hintSegment] = [] self.mask = [] - def cleanup(self): + def cleanup(self) -> None: """Updates and deletes segments according to deleted and replacedBy""" for l in (self.s_segs, self.m_segs, self.e_segs): l[:] = [x for x in (s.current() for s in l) if not x.deleted] - def pruneHintSegs(self): + def pruneHintSegs(self) -> None: """Deletes segments with no assigned hintval""" for l in (self.s_segs, self.m_segs, self.e_segs): l[:] = [s for s in l if s.hintval is not None] - def segments(self): + def segments(self) -> list: return [s for s in self.s_segs + self.m_segs + self.e_segs] - def segLists(self, first=None): + def segLists( + self, first=None + ) -> Tuple[HintSegListWithType, HintSegListWithType, HintSegListWithType]: if first is None or first == 's': return (('s', self.s_segs), ('m', self.m_segs), ('e', self.e_segs)) elif first == 'm': return (('m', self.m_segs), ('s', self.s_segs), ('e', self.e_segs)) elif first == 'e': return (('e', self.e_segs), ('m', self.m_segs), ('s', self.s_segs)) + else: + raise ValueError('First must be s, m, or e') class glyphHintState: @@ -320,7 +346,7 @@ def addSegment(self, fr, to, loc, pe1, pe2, typ, bonus, isV, mid1, mid2, bisect.insort(lst, s) - def compactList(self, l): + def compactList(self, l: List[hintSegment]) -> None: """ Compacts overlapping segments with the same location by picking one segment to represent the pair, adjusting its values, and @@ -349,12 +375,12 @@ def compactList(self, l): j += 1 i += 1 - def compactLists(self): + def compactLists(self) -> None: """Compacts both segment lists""" self.compactList(self.decreasingSegs) self.compactList(self.increasingSegs) - def remExtraBends(self): + def remExtraBends(self) -> None: """ Delete BEND segment x when there is another segment y: 1. At the same location @@ -369,23 +395,29 @@ def remExtraBends(self): for hsd in self.decreasingSegs[lo:hi]: assert hsd.loc == hsi.loc if hsd.min < hsi.max and hsd.max > hsi.min: - if (hsi.type == hintSegment.sType.BEND and - hsd.type != hintSegment.sType.BEND and - hsd.type != hintSegment.sType.GHOST and - (hsd.max - hsd.min) > (hsi.max - hsi.min) * 3): + if ( + hsi.type == hintSegment.sType.BEND + and hsd.type != hintSegment.sType.BEND + and hsd.type != hintSegment.sType.GHOST + and (hsd.max - hsd.min) > (hsi.max - hsi.min) * 3 + ): hsi.deleted = True - log.debug("rem seg loc %g from %g to %g" % - (hsi.loc, hsi.min, hsi.max)) + log.debug( + "rem seg loc %g from %g to %g" % (hsi.loc, hsi.min, hsi.max) + ) break - elif (hsd.type == hintSegment.sType.BEND and - hsi.type != hintSegment.sType.BEND and - hsi.type != hintSegment.sType.GHOST and - (hsi.max - hsi.min) > (hsd.max - hsd.min) * 3): + elif ( + hsd.type == hintSegment.sType.BEND + and hsi.type != hintSegment.sType.BEND + and hsi.type != hintSegment.sType.GHOST + and (hsi.max - hsi.min) > (hsd.max - hsd.min) * 3 + ): hsd.deleted = True - log.debug("rem seg loc %g from %g to %g" % - (hsd.loc, hsd.min, hsd.max)) + log.debug( + "rem seg loc %g from %g to %g" % (hsd.loc, hsd.min, hsd.max) + ) - def deleteSegments(self): + def deleteSegments(self) -> None: for s in self.increasingSegs: s.deleted = True for s in self.decreasingSegs: @@ -394,54 +426,57 @@ def deleteSegments(self): self.decreasingSegs = [] self.cleanup() - def cleanup(self): + def cleanup(self) -> None: """Runs cleanup on all pathElementHintState objects""" self.increasingSegs = [s for s in self.increasingSegs if not s.deleted] self.decreasingSegs = [s for s in self.decreasingSegs if not s.deleted] for pes in self.peStates.values(): pes.cleanup() - def pruneHintSegs(self): + def pruneHintSegs(self) -> None: """Runs pruneHintSegs on all pathElementHintState objects""" for pes in self.peStates.values(): pes.pruneHintSegs() class stemLocCandidate: - strongMultiplier = 1.2 - bandMultiplier = 2.0 + strongMultiplier: float = 1.2 + bandMultiplier: float = 2.0 """ Information about a candidate location for a stem in a region glyph, derived from segments generated for the glyph or, occasionally, directly from point locations. """ - def __init__(self, loc): + + def __init__(self, loc) -> None: self.loc = loc - self.strong = 0 - self.weak = 0 + self.strong: float = 0 + self.weak: float = 0 - def addScore(self, score, strong): + def addScore(self, score: float, strong: bool) -> None: if strong: self.strong += score else: self.weak += score - def weight(self, inBand): - return ((self.strong * self.strongMultiplier + self.weak) * - (self.bandMultiplier if inBand else 1)) + def weight(self, inBand: bool) -> float: + return (self.strong * self.strongMultiplier + self.weak) * ( + self.bandMultiplier if inBand else 1 + ) - def isStrong(self): + def isStrong(self) -> bool: return self.strong > 0 - def isMixed(self): + def isMixed(self) -> bool: return self.strong > 0 and self.weak > 0 - def __eq__(self, other): + def __eq__(self, other) -> bool: return feq(self.strong, other.strong) and feq(self.weak, other.weak) - def __lt__(self, other): - return (self.strong < other.strong or - (feq(self.strong, other.strong) and self.weak < other.weak)) + def __lt__(self, other) -> bool: + return self.strong < other.strong or ( + feq(self.strong, other.strong) and self.weak < other.weak + ) class instanceStemState: @@ -503,7 +538,8 @@ class links: links: A cnt x cnt array of integers modified by mark (Values only 0 or 1 but kept as ints for later arithmetic) """ - def __init__(self, glyph): + + def __init__(self, glyph) -> None: l = len(glyph.subpaths) if l < 4 or l > 100: self.cnt = 0 @@ -511,24 +547,33 @@ def __init__(self, glyph): self.cnt = l self.links = [[0] * l for i in range(l)] - def logLinks(self): + def logLinks(self) -> None: """Prints a log message representing links""" if self.cnt == 0: return log.debug("Links") - log.debug(' '.join((str(i).rjust(2) for i in range(self.cnt)))) + log.debug(" ".join((str(i).rjust(2) for i in range(self.cnt)))) for j in range(self.cnt): - log.debug(' '.join((('Y' if self.links[j][i] else ' ').rjust(2) - for i in range(self.cnt)))) - - def logShort(self, shrt, lab): + log.debug( + " ".join( + ( + ("Y" if self.links[j][i] else " ").rjust(2) + for i in range(self.cnt) + ) + ) + ) + + def logShort(self, shrt, lab) -> None: """Prints a log message representing (1-d) shrt""" log.debug(lab) - log.debug(' '.join((str(i).rjust(2) for i in range(self.cnt)))) - log.debug(' '.join(((str(shrt[i]) if shrt[i] else ' ').rjust(2) - for i in range(self.cnt)))) - - def mark(self, stemValues, isV): + log.debug(" ".join((str(i).rjust(2) for i in range(self.cnt)))) + log.debug( + " ".join( + ((str(shrt[i]) if shrt[i] else " ").rjust(2) for i in range(self.cnt)) + ) + ) + + def mark(self, stemValues: List[stemValue], isV) -> None: """ For each stemValue in hntr, set links[m][n] and links[n][m] to 1 if one side of a stem is in m and the other is in n @@ -546,7 +591,7 @@ def mark(self, stemValues, isV): self.links[lsubp][usubp] = 1 self.links[usubp][lsubp] = 1 - def moveIdx(self, suborder, subidxs, outlinks, idx): + def moveIdx(self, suborder, subidxs: List[int], outlinks, idx: int) -> None: """ Move value idx from subidxs to the end of suborder and update outlinks to record all links shared with idx @@ -557,7 +602,7 @@ def moveIdx(self, suborder, subidxs, outlinks, idx): outlinks[i] += self.links[idx][i] self.logShort(outlinks, "Outlinks") - def shuffle(self): + def shuffle(self) -> Optional[List[int]]: """ Returns suborder list with all subpath indexes in decreasing order of links shared with previous subpath. (The first subpath From d078bfd26070e39c203ca0a6e5610cde1d1a958b Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 12:15:50 +0100 Subject: [PATCH 09/42] Import the types --- python/afdko/otfautohint/glyphData.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index 4df008037..b5527fdc3 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -15,6 +15,7 @@ calcCubicParameters, splitCubicAtT, segmentPointAtT, approximateCubicArcLength) +from typing import Optional, Tuple, Union from fontTools.pens.basePen import BasePen import logging From b7d0ece117c746deec41a25d8c298f228a36fe7f Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 12:22:24 +0100 Subject: [PATCH 10/42] Make point into an ordinary class, not a tuple, because reasons --- python/afdko/otfautohint/glyphData.py | 31 +++++++++++++++------------ 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index b5527fdc3..ae7fb9b59 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -45,12 +45,15 @@ def fne(a: float, b: float, factor=1.52e-5) -> bool: return abs(a - b) >= factor -class pt(tuple): +class pt: """A 2-tuple representing a point in 2D space""" - __slots__ = () + tl = threading.local() tl.align = None + x: Number + y: Number + @classmethod def setAlign(cls, vertical=False): """ @@ -80,27 +83,27 @@ def clearAlign(cls): """ cls.tl.align = None - def __new__(cls, x=0, y=0, roundCoords=False): + def __init__(self, x: Number = 0, y: Number = 0, roundCoords=False): """ Creates a new pt object initialied with x and y. If roundCoords is True the values are rounded before storing """ if isinstance(x, tuple): - y = x[1] - x = x[0] + y = float(x[1]) + x = float(x[0]) if roundCoords: x = round(x) y = round(y) - return _tuple.__new__(cls, (x, y)) - - @property - def x(self): - return self[0] - - @property - def y(self): - return self[1] + self.x = x + self.y = y + + def __getitem__(self, ix) -> Union[int, float]: + if ix == 0: + return self.x + if ix == 1: + return self.y + raise IndexError("pt index out of range") @property def a(self): From 1e37190a1cd46d2b025fff800a360ce2d44941d7 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 12:23:00 +0100 Subject: [PATCH 11/42] Typing requires types. --- python/afdko/otfautohint/hinter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index 30ae6a097..c319ded24 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -12,6 +12,7 @@ from copy import copy, deepcopy from abc import abstractmethod, ABC from collections import namedtuple +from typing import Any, Dict, Iterable, List, NamedTuple, Tuple, Type, TypeVar, Union, Optional, Self from fontTools.misc.bezierTools import solveCubic From b729bb1e50297203e83d1cd3ca7a7771df34e876 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 12:23:26 +0100 Subject: [PATCH 12/42] Uncontroversial typings --- python/afdko/otfautohint/hinter.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index c319ded24..c5509fc56 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -235,7 +235,7 @@ def stopMaskConvert(self) -> None: pass @abstractmethod - def isV(self): + def isV(self) -> bool: pass @abstractmethod @@ -243,7 +243,7 @@ def segmentLists(self): pass @abstractmethod - def dominantStems(self): + def dominantStems(self) -> Any: pass @abstractmethod @@ -2254,7 +2254,7 @@ def checkNearBands(self, loc, pl) -> None: def segmentLists(self): return self.hs.increasingSegs, self.hs.decreasingSegs - def isCounterGlyph(self): + def isCounterGlyph(self) -> bool: return self.name in self.options.hCounterGlyphs @@ -2289,16 +2289,16 @@ def isSpecial(self, lower=False) -> bool: else: return self.name in self.options.upperSpecials - def aDesc(self): + def aDesc(self) -> str: return 'vertical' - def checkTfm(self): + def checkTfm(self) -> None: pass def segmentLists(self): return self.hs.decreasingSegs, self.hs.increasingSegs - def isCounterGlyph(self): + def isCounterGlyph(self) -> bool: return self.name in self.options.vCounterGlyphs From 49f35c27a7ab8665f8e504ba90dc0938a45bfa96 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 12:31:47 +0100 Subject: [PATCH 13/42] Some more typings --- python/afdko/otfautohint/glyphData.py | 19 ++++-- python/afdko/otfautohint/hinter.py | 47 +++++++++----- python/afdko/otfautohint/hintstate.py | 88 +++++++++++++++++---------- python/afdko/otfautohint/ufoFont.py | 2 - 4 files changed, 101 insertions(+), 55 deletions(-) diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index ae7fb9b59..862e4e6ac 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -11,11 +11,17 @@ from math import sqrt from collections import defaultdict from builtins import tuple as _tuple -from fontTools.misc.bezierTools import (solveQuadratic, solveCubic, - calcCubicParameters, - splitCubicAtT, segmentPointAtT, - approximateCubicArcLength) from typing import Optional, Tuple, Union + +# pytype: disable=import-error +from fontTools.misc.bezierTools import ( + solveQuadratic, + solveCubic, + calcCubicParameters, + splitCubicAtT, + segmentPointAtT, + approximateCubicArcLength, +) from fontTools.pens.basePen import BasePen import logging @@ -548,7 +554,7 @@ def __init__(self, *args, is_close=False, masks=None, flex=False, self.masks = masks self.flex = flex self.bounds = None - self.position = position + self.position: Tuple[int, int] = position or (-1, -1) self.segment_sub = None def getBounds(self): @@ -572,6 +578,7 @@ def isClose(self): def isStart(self): """Returns True if this pathElement starts a subpath""" + assert self.position is not None return self.position[1] == 0 def isTiny(self): @@ -1415,7 +1422,7 @@ def toStems(self, data): for i in range(len(data) // 2): low = high + data[i * 2] high = low + data[i * 2 + 1] - sl.append(stem(low, high)) + sl.append(stem(low, high)) # pytype: disable=wrong-arg-count return sl def fromStems(self, stems): diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index c5509fc56..bffd74148 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -142,7 +142,7 @@ def __init__(self, options) -> None: self.HasFlex = False self.options = options - self.fddicts = None + self.fddicts: List[FDDict] = [] self.gllist = None self.glyph = None self.report = None @@ -373,7 +373,7 @@ def calcHintValues(self, lnks, force=True, tryCounter=True) -> None: Top-level method for calculating stem hints for a glyph in one dimension """ - assert self.glyph is not None + assert self.glyph is not None and self.hs is not None self.startHint() if self.glyph.hasHints(doVert=self.isV()): if force: @@ -1826,6 +1826,8 @@ def calcInstanceStems(self, glidx) -> None: self.glyph = self.gllist[glidx] self.fddict = self.fddicts[glidx] hs0 = self.hs + assert hs0 is not None + assert hs0.stems is not None numStems = len(hs0.stems[0]) if numStems == 0: return @@ -1912,6 +1914,8 @@ def calcInstanceStems(self, glidx) -> None: done[sidx][ul] = True sl = hs0.stems[glidx] + lo = None + hi = None for sidx, s0 in enumerate(hs0.stems[0]): isG = s0.isGhost() if isG != 'high': @@ -2012,6 +2016,8 @@ def convertToMasks(self) -> None: self.stopMaskConvert() return + assert self.hs + assert self.hs.stems dsl = self.hs.stems[0] l = len(dsl) hasConflicts = False @@ -2036,7 +2042,8 @@ def convertToMasks(self) -> None: if not g])) self.hs.hasOverlaps = False - self.hs.stemOverlaps = so = [[False] * l for _ in range(l)] + so: List[List[bool]] = [[False] * l for _ in range(l)] + self.hs.stemOverlaps = so for sidx in range(l): if not self.hs.goodMask[sidx]: continue @@ -2051,7 +2058,8 @@ def convertToMasks(self) -> None: if self.hs.counterHinted and self.hs.hasOverlaps and not self.isMulti: log.warning("XXX TEMPORARY WARNING: overlapping counter hints") - self.hs.ghostCompat = gc = [None] * l + gc: List[Optional[List[bool]]] = [None] * l + self.hs.ghostCompat = gc # A ghost stem in the default should be a ghost everywhere for sidx, s in enumerate(dsl): @@ -2063,7 +2071,8 @@ def convertToMasks(self) -> None: continue if all(sl[sidx].ghostCompat(sl[sjdx]) for sl in self.hs.stems): assert so[sidx][sjdx] - gc[sidx][sjdx] = True + assert gc[sidx] is not None + gc[sidx][sjdx] = True # pytype: disable=unsupported-operands # The stems corresponding to mainValues may conflict now. This isn't # a fatal problem because mainMask stems are only added if they don't @@ -2076,6 +2085,7 @@ def convertToMasks(self) -> None: while mv: ok = True c = mv.pop(0) + assert c.idx is not None for s in okl: if so[c.idx][s.idx]: ok = False @@ -2091,8 +2101,9 @@ def convertToMasks(self) -> None: self.makePEMask(pestate, pe) self.stopMaskConvert() - def makePEMask(self, pestate, c): + def makePEMask(self, pestate, c) -> None: """Convert the hints desired by pathElement to a conflict-free mask""" + assert self.hs and self.hs.stems l = len(self.hs.stems[0]) mask = [False] * l segments = pestate.segments() @@ -2173,7 +2184,12 @@ def OKToRem(self, loc, spc) -> bool: class hhinter(dimensionHinter): - def startFlex(self): + def __init__(self, options) -> None: + super().__init__(options) + self.topPairs = [] + self.bottomPairs = [] + + def startFlex(self) -> None: """Make pt.a map to x and pt.b map to y""" set_log_parameters(dimension='-') pt.setAlign(False) @@ -2340,13 +2356,13 @@ def __init__(self, options, dictRecord) -> None: self.MaxHalfMargin = 20 # XXX 10 might better match original C code self.PromotionDistance = 50 - def getSegments(self, glyph, pe, oppo=False): + def getSegments(self, glyph, pe, oppo=False) -> Any: """Returns the list of segments for pe in the requested dimension""" gstate = glyph.vhs if (self.doV == (not oppo)) else glyph.hhs pestate = gstate.getPEState(pe) return pestate.segments() if pestate else [] - def getMasks(self, glyph, pe): + def getMasks(self, glyph, pe) -> list: """ Returns the masks of hints needed by/desired for pe in each dimension """ @@ -2367,7 +2383,7 @@ def getMasks(self, glyph, pe): masks.append(mask) return masks - def _hint(self, name, glyphTuple, fdKey): + def _hint(self, name: str, glyphTuple, fdKey) -> Tuple[str, Optional[Union[GlyphReport, Any]]]: """Top-level flex and stem hinting method for a glyph""" if isinstance(fdKey, tuple): assert len(fdKey) == 2 @@ -2503,7 +2519,7 @@ def compatiblePaths(self, gllist, fddicts) -> bool: return False return True - def distributeMasks(self, glyph): + def distributeMasks(self, glyph) -> Optional[list]: """ When necessary, chose the locations and contents of hintmasks for the glyph @@ -2636,7 +2652,7 @@ def buildCounterMasks(self, glyph) -> None: cntr = [] glyph.cntr = cntr - def joinMasks(self, m, cm, log): + def joinMasks(self, m, cm, log) -> Tuple[list, bool]: """ Try to add the stems in cm to m, or start a new mask if there are conflicts. @@ -2715,7 +2731,8 @@ def bridgeMasks(self, glyph, o, n, used, pe) -> None: _, ms = min(((stems[hv][i].distance(oloc), i) for i in range(len(o[hv])) if goodmask[hv][i])) - o[hv][ms] = True + assert ms < len(o[hv]) + o[hv][ms] = True # pytype: disable=unsupported-operands except ValueError: pass if self.mergeMain(glyph): @@ -2833,10 +2850,10 @@ def isFlare(self, loc, glyph, c, n) -> bool: c = glyph.nextInSubpath(c) return True - def isUSeg(self, loc, uloc, lloc): + def isUSeg(self, loc, uloc, lloc) -> Any: return abs(uloc - loc) <= abs(lloc - loc) - def reportRemFlare(self, pe, pe2, desc): + def reportRemFlare(self, pe, pe2, desc) -> None: log.debug("Removed %s flare at %g %g by %g %g : %s" % ("vertical" if self.doV else "horizontal", pe.e.x, pe.e.y, pe2.e.x, pe2.e.y, desc)) diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index 2d8ba93ef..d973e8a7b 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -274,25 +274,26 @@ class glyphHintState: for m (n has the same location on the relevant side) mainMask: glyphData hintmask-like representation of mainValues """ - def __init__(self): + + def __init__(self) -> None: self.peStates = {} self.overlapRemoved = None - self.increasingSegs = [] - self.decreasingSegs = [] - self.stemValues = [] - self.mainValues = None - self.rejectValues = None + self.increasingSegs: List[hintSegment] = [] + self.decreasingSegs: List[hintSegment] = [] + self.stemValues: List[stemValue] = [] + self.mainValues: List[stemValue] = [] + self.rejectValues: List[stemValue] = [] self.counterHinted = False - self.stems = None # in sorted glyphData format + self.stems: Optional[List[stem]] = None # in sorted glyphData format self.weights = None self.keepHints = None - self.hasOverlaps = None - self.stemOverlaps = None - self.ghostCompat = None - self.goodMask = None - self.mainMask = None + self.hasOverlaps: bool = False + self.stemOverlaps: List[List[bool]] = [] + self.ghostCompat: List[Optional[List[bool]]] = [] + self.goodMask: List[bool] = [] + self.mainMask: List[bool] = [] - def getPEState(self, pe, make=False): + def getPEState(self, pe, make=False) -> Optional[pathElementHintState]: """ Returns the pathElementHintState object for pe, allocating the object if necessary @@ -301,18 +302,36 @@ def getPEState(self, pe, make=False): if s: return s if make: - s = self.peStates[pe] = pathElementHintState() - return s + return self.getPEStateForced(pe) else: return None - def addSegment(self, fr, to, loc, pe1, pe2, typ, bonus, isV, mid1, mid2, - desc): + def getPEStateForced(self, pe) -> pathElementHintState: + """ + Returns the pathElementHintState object for pe, allocating the object + if necessary + """ + return self.peStates.setdefault(pe, pathElementHintState()) + + def addSegment( + self, + fr, + to, + loc, + pe1: Optional[pathElement], + pe2: Optional[pathElement], + typ: hintSegment.sType, + bonus, + isV: bool, + mid1, + mid2, + desc: str, + ) -> None: """Adds a new segment associated with pathElements pe1 and pe2""" if isV: - pp = ('v', loc, fr, loc, to, desc) + pp = ("v", loc, fr, loc, to, desc) else: - pp = ('h', fr, loc, to, loc, desc) + pp = ("h", fr, loc, to, loc, desc) log.debug("add %sseg %g %g to %g %g %s" % pp) if fr > to: mn, mx = to, fr @@ -323,9 +342,9 @@ def addSegment(self, fr, to, loc, pe1, pe2, typ, bonus, isV, mid1, mid2, isInc = True lst = self.increasingSegs - assert pe1 or pe2 - s = hintSegment(loc, mn, mx, pe2 if pe2 else pe1, typ, bonus, isV, - isInc, desc) + aSeg = pe2 or pe1 + assert aSeg + s = hintSegment(loc, mn, mx, aSeg, typ, bonus, isV, isInc, desc) # Segments derived from the first point in a path c are typically # also added to the previous spline p, with p passed as pe1 and c # passed as pe2. Segments derived from the last point in a path @@ -335,14 +354,14 @@ def addSegment(self, fr, to, loc, pe1, pe2, typ, bonus, isV, mid1, mid2, # from the middle of the spline. if pe1: if mid1: - self.getPEState(pe1, True).m_segs.append(s) + self.getPEStateForced(pe1).m_segs.append(s) else: - self.getPEState(pe1, True).e_segs.append(s) + self.getPEStateForced(pe1).e_segs.append(s) if pe2: if mid2: - self.getPEState(pe2, True).m_segs.append(s) + self.getPEStateForced(pe2).m_segs.append(s) else: - self.getPEState(pe2, True).s_segs.append(s) + self.getPEStateForced(pe2).s_segs.append(s) bisect.insort(lst, s) @@ -484,14 +503,16 @@ class instanceStemState: State for the process of deciding on the lower and upper locations for a particular region stem. """ - def __init__(self, loc, dhinter): + + def __init__(self, loc: float, dhinter: AHinter) -> None: self.defaultLoc = loc self.dhinter = dhinter - self.candDict = {} - self.usedSegs = set() + self.candDict: Dict[float, stemLocCandidate] = {} + self.usedSegs: Set[int] = set() self.bb = None - def addToLoc(self, loc, score, strong=False, bb=False, seg=None): + def addToLoc(self, loc: float, score: float, strong=False, + bb=False, seg: Any = None) -> None: if self.bb is not None: if self.bb != bb: log.info("Mixed bounding-box and " @@ -509,7 +530,7 @@ def addToLoc(self, loc, score, strong=False, bb=False, seg=None): sLC = self.candDict[loc] = stemLocCandidate(loc) sLC.addScore(score, strong) - def bestLocation(self, isBottom): + def bestLocation(self, isBottom: bool) -> Any: weights = [(x.weight(self.dhinter.inBand(x.loc, isBottom)), x.loc) for x in self.candDict.values()] try: @@ -583,7 +604,10 @@ def mark(self, stemValues: List[stemValue], isV) -> None: for sv in stemValues: if not sv.lseg or not sv.useg or not sv.lseg.pe or not sv.useg.pe: continue - lsubp, usubp = sv.lseg.pe().position[0], sv.useg.pe().position[0] + lseg_pe = sv.lseg.pe() + useg_pe = sv.useg.pe() + assert lseg_pe and useg_pe + lsubp, usubp = lseg_pe.position[0], useg_pe.position[0] if lsubp == usubp: continue sv.show(isV, "mark") diff --git a/python/afdko/otfautohint/ufoFont.py b/python/afdko/otfautohint/ufoFont.py index 625542f62..815401a1d 100644 --- a/python/afdko/otfautohint/ufoFont.py +++ b/python/afdko/otfautohint/ufoFont.py @@ -566,8 +566,6 @@ def _get_glyphset(self, layer_name=None): glyphset = self._reader.getGlyphSet(layer_name) except UFOLibError: pass - if glyphset is None: - raise FontParseError("No glyphset found for layer '%s'" % layer_name) self._glyphsets[layer_name] = glyphset return self._glyphsets[layer_name] From f8ca0b5585c356004b9a7af7593653d6f8772d42 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 13:30:02 +0100 Subject: [PATCH 14/42] Really void functions --- python/afdko/otfautohint/hinter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index bffd74148..18fe2d475 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -2858,9 +2858,9 @@ def reportRemFlare(self, pe, pe2, desc) -> None: ("vertical" if self.doV else "horizontal", pe.e.x, pe.e.y, pe2.e.x, pe2.e.y, desc)) - def otherInstanceStems(self, gllist) -> Optional[bool]: + def otherInstanceStems(self, gllist) -> None: if len(gllist) < 2: - return True + return glyph = gllist[0] @@ -2870,9 +2870,9 @@ def otherInstanceStems(self, gllist) -> Optional[bool]: g.hstems = glyph.hhs.stems[i] g.vstems = glyph.vhs.stems[i] - def otherInstanceMasks(self, gllist) -> Optional[bool]: + def otherInstanceMasks(self, gllist) -> None: if len(gllist) < 2: - return True + return glyph = gllist[0] From c46b555a745b68a768661dd94f4e158a5964f1f8 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 13:30:15 +0100 Subject: [PATCH 15/42] Better typings --- python/afdko/otfautohint/hintstate.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index d973e8a7b..eac00928c 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -10,7 +10,7 @@ from enum import IntEnum -from .glyphData import feq, pathElement, stem +from .glyphData import Number, feq, pathElement, stem from _weakref import ReferenceType from typing import Any, Dict, List, Optional, Set, Tuple, Type, Self, Protocol @@ -144,9 +144,16 @@ def show(self, label) -> None: class stemValue: """Represents a potential hint stem""" - def __init__(self, lloc: Number, uloc: Number, val: Number, - spc, lseg: hintSegment, useg: hintSegment, - isGhost=False) -> None: + def __init__( + self, + lloc: Number, + uloc: Number, + val: Number, + spc, + lseg: hintSegment, + useg: hintSegment, + isGhost=False, + ) -> None: assert lloc <= uloc self.val = val self.spc = spc @@ -159,9 +166,11 @@ def __init__(self, lloc: Number, uloc: Number, val: Number, self.useg = useg self.best = None self.initialVal = val - self.idx = None + self.idx: Optional[int] = None - def __eq__(self, other: Self) -> bool: + def __eq__(self, other: object) -> bool: + if not isinstance(other, stemValue): + return False slloc, suloc = self.ghosted() olloc, ouloc = other.ghosted() return slloc == olloc and suloc == ouloc @@ -173,7 +182,7 @@ def __lt__(self, other: Self) -> bool: return (slloc < olloc or (slloc == olloc and suloc < ouloc)) # c.f. page 22 of Adobe TN #5177 "The Type 2 Charstring Format" - def ghosted(self) -> Tuple[Any, Any]: + def ghosted(self) -> Tuple[Number, Number]: """Return the stem range but with ghost stems normalized""" lloc, uloc = self.lloc, self.uloc if self.isGhost: From dac1ad049cfd630aa67cd056c34c319b27d8be3e Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 13:30:22 +0100 Subject: [PATCH 16/42] Mainly assertions --- python/afdko/otfautohint/hinter.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index 18fe2d475..403bf7a9c 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -1923,12 +1923,15 @@ def calcInstanceStems(self, glidx) -> None: if isG != 'low': hi = self.bestLocation(sidx, 1, iSSl, hs0) if isG == 'low': + assert lo is not None lo = lo + 21 hi = lo - 21 elif isG == 'high': + assert lo is not None lo = hi hi = lo - 20 else: + assert lo is not None and hi is not None if hi < lo: # Differentiate bad stem from ghost stem lo = hi + 50 @@ -2131,6 +2134,7 @@ def makePEMask(self, pestate, c) -> None: sg.hintval.idx == sjdx))) vali, valj = segi.hintval, segj.hintval assert vali.lloc <= valj.lloc or valj.isGhost + assert self.glyph n = self.glyph.nextInSubpath(c) p = self.glyph.prevInSubpath(c) if (vali.val < self.ConflictValMin and @@ -2204,6 +2208,7 @@ def startHint(self) -> None: for easier processing """ self.startFlex() + assert self.fddict blues = self.fddict.BlueValuesPairs + self.fddict.OtherBlueValuesPairs # pytype: disable=attribute-error self.topPairs = [pair for pair in blues if not pair[4]] self.bottomPairs = [pair for pair in blues if pair[4]] @@ -2221,6 +2226,7 @@ def isV(self) -> bool: def inBand(self, loc, isBottom=False) -> bool: """Return true if loc is within the selected set of bands""" + assert self.fddict if self.name in self.options.noBlues: return False if isBottom: @@ -2268,6 +2274,7 @@ def checkNearBands(self, loc, pl) -> None: "%f instead of %f." % (loc, p[0])) def segmentLists(self): + assert self.hs return self.hs.increasingSegs, self.hs.decreasingSegs def isCounterGlyph(self) -> bool: @@ -2290,6 +2297,7 @@ def isV(self) -> bool: return True def dominantStems(self) -> Any: + assert self.fddict return self.fddict.DominantV # pytype: disable=attribute-error def inBand(self, loc, isBottom=False) -> bool: @@ -2347,6 +2355,7 @@ def __init__(self, options, dictRecord) -> None: self.hHinter = hhinter(options) self.vHinter = vhinter(options) self.cnt = 0 + self.doV: bool = False if options.justReporting(): self.taskDesc = 'analysis' else: @@ -2525,7 +2534,7 @@ def distributeMasks(self, glyph) -> Optional[list]: the glyph """ stems = [None, None] - masks = [None, None] + masks: List[List[bool]] = [[], []] lnstm = [0, 0] # Initial horizontal data # If keepHints was true hhs.stems was already set to glyph.hstems in @@ -2579,6 +2588,7 @@ def distributeMasks(self, glyph) -> Optional[list]: mode = NOTSHORT ns = None c = glyph.nextForHints(glyph) + oldmasks: List[List[bool]] = [] while c: if c.isShort() or c.flex == 2: if mode == NOTSHORT: @@ -2592,6 +2602,7 @@ def distributeMasks(self, glyph) -> Optional[list]: else: ns = c if mode == SHORT: + assert oldmasks oldmasks[:] = masks masks = oldmasks incompatmasks = None @@ -2661,6 +2672,7 @@ def joinMasks(self, m, cm, log) -> Tuple[list, bool]: nm = [None, None] for hv in range(2): hs = self.vHinter.hs if hv == 1 else self.hHinter.hs + assert hs is not None l = len(m[hv]) # if hs.counterHinted: # nm[hv] = [True] * l @@ -2683,9 +2695,10 @@ def joinMasks(self, m, cm, log) -> Tuple[list, bool]: continue if n[j] and hs.stemOverlaps[i][j]: # See if we can do a ghost stem swap - if hs.ghostCompat[i]: + ghostStem = hs.ghostCompat[i] + if ghostStem: for k in range(l): - if not n[k] or not hs.ghostCompat[i][k]: + if not n[k] or not ghostStem[k]: continue else: ireplaced = True From 48551a5a785039a622d465cfb66a5be784840829 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 13:47:27 +0100 Subject: [PATCH 17/42] More type hints --- python/afdko/otfautohint/fdTools.py | 29 ++++++++++++++++++----- python/afdko/otfautohint/glyphData.py | 5 +++- python/afdko/otfautohint/hinter.py | 34 +++++++++++++++++++++------ python/afdko/otfautohint/hintstate.py | 2 +- python/afdko/otfautohint/otfFont.py | 13 ++++++---- 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/python/afdko/otfautohint/fdTools.py b/python/afdko/otfautohint/fdTools.py index b96bea8b0..3b3a4ae5e 100644 --- a/python/afdko/otfautohint/fdTools.py +++ b/python/afdko/otfautohint/fdTools.py @@ -13,6 +13,9 @@ import numbers import sys import os +from typing import Optional, Tuple, List, Dict + +from .glyphData import Number log = logging.getLogger(__name__) @@ -110,11 +113,24 @@ ["StemSnapH", "StemSnapV"]) +BlueValue = Tuple[int, int, str, str, int] +BlueZoneDict = Dict[Tuple[int, int], Tuple[int, str, str]] + + class FontInfoParseError(ValueError): pass class FDDict: + BlueValuesPairs: List[BlueValue] + OtherBlueValuesPairs: List[BlueValue] + BlueFuzz: int + DominantH: int + DominantV: int + BaselineOvershoot: Number + BaselineYCoord: Number + FamilyBaselineYCoord: Number + def __init__(self, fdIndex, dictName=None, fontName=None): self.fdIndex = fdIndex for key in kFDDictKeys: @@ -313,6 +329,7 @@ def parseFontInfoFile(fdArrayMap, data, glyphList, maxY, minY, fontName): maxfdIndex = max(fdArrayMap.keys()) dictValueList = [] dictKeyWord = '' + fdDict: Optional[FDDict] = None state = baseState @@ -377,6 +394,7 @@ def parseFontInfoFile(fdArrayMap, data, glyphList, maxY, minY, fontName): elif state == inDictValue: if token[-1] in ["]", ")"]: dictValueList.append(token[:-1]) + assert fdDict is not None fdDict.setInfo(dictKeyWord, dictValueList) state = dictState # found the last token in the list value. else: @@ -415,6 +433,7 @@ def parseFontInfoFile(fdArrayMap, data, glyphList, maxY, minY, fontName): dictName = None fdDict = None else: + assert fdDict is not None if token in kFDDictKeys: value = tokenList[i] i += 1 @@ -477,8 +496,8 @@ def parseFontInfoFile(fdArrayMap, data, glyphList, maxY, minY, fontName): def mergeFDDicts(prevDictList): # Extract the union of the stem widths and zones from the list # of FDDicts, and replace the current values in the topDict. - blueZoneDict = {} - otherBlueZoneDict = {} + blueZoneDict: BlueZoneDict = {} + otherBlueZoneDict: BlueZoneDict = {} dominantHDict = {} dominantVDict = {} bluePairListNames = [kFontDictBluePairsName, kFontDictOtherBluePairsName] @@ -525,10 +544,9 @@ def mergeFDDicts(prevDictList): goodStemList = goodStemLists[ki] # Zones first. - zoneList = zoneDict.keys() + zoneList = sorted(zoneDict.keys()) if not zoneList: continue - zoneList = sorted(zoneList) # Now check for conflicts. prevZone = zoneList[0] goodZoneList.append(prevZone[1]) @@ -558,10 +576,9 @@ def mergeFDDicts(prevDictList): prevZone = zone - stemList = stemDict.keys() + stemList = sorted(stemDict.keys()) if not stemList: continue - stemList = sorted(stemList) # Now check for conflicts. prevStem = stemList[0] goodStemList.append(prevStem) diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index 862e4e6ac..fa1b68200 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -26,6 +26,8 @@ import logging +from .hintstate import glyphHintState + log = logging.getLogger(__name__) Number = Union[int, float] @@ -829,7 +831,8 @@ def __init__(self, roundCoords, name=''): self.pathEdited = False self.boundsMap = {} - self.hhs = self.vhs = None + self.hhs: Optional[glyphHintState] = None + self.vhs: Optional[glyphHintState] = None # pen methods: diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index 403bf7a9c..1d01dc707 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -16,12 +16,12 @@ from fontTools.misc.bezierTools import solveCubic -from .glyphData import Number, pathElement, pt, feq, fne, stem +from .glyphData import Number, pathElement, pt, feq, fne, stem, glyphData from .hintstate import (hintSegment, stemValue, glyphHintState, links, instanceStemState) from .overlap import removeOverlap from .report import GlyphReport -from .fdTools import FDDict +from .fdTools import BlueValue, FDDict from .logging import logging_reconfig, set_log_parameters @@ -143,17 +143,28 @@ def __init__(self, options) -> None: self.options = options self.fddicts: List[FDDict] = [] - self.gllist = None - self.glyph = None + self.gllist: List[glyphData] = [] + self.glyph: glyphData = glyphData(False) self.report = None self.name = None self.isMulti = False +<<<<<<< HEAD self.hs : Optional[glyphHintState] = None +======= + self.hs: glyphHintState = glyphHintState() +>>>>>>> b7cb160d (More type hints) self.fddict: Optional[FDDict] = None self.Bonus = None self.Pruning = None - def setGlyph(self, fddicts, report, gllist, name, clearPrev=True) -> None: + def setGlyph( + self, + fddicts: List[FDDict], + report: GlyphReport, + gllist: List[glyphData], + name: str, + clearPrev=True, + ) -> None: """Initialize the state for processing a specific glyph""" self.fddicts = fddicts self.report = report @@ -1599,7 +1610,7 @@ def mainVals(self) -> None: mainValues = [] rejectValues = [] svl = copy(self.hs.stemValues) - prevBV = 0 + prevBV: Number = 0 while True: try: _, best = max(((sv.compVal(self.SpcBonus), sv) for sv in svl @@ -1831,6 +1842,7 @@ def calcInstanceStems(self, glidx) -> None: numStems = len(hs0.stems[0]) if numStems == 0: return + assert self.fddict set_log_parameters(instance=self.fddict.FontName) if self.isV(): self.hs = self.glyph.vhs = glyphHintState() @@ -1944,8 +1956,9 @@ def calcInstanceStems(self, glidx) -> None: set_log_parameters(instance=self.fddict.FontName) self.hs = hs0 - def bestLocation(self, sidx, ul, iSSl, hs0) -> Any: + def bestLocation(self, sidx, ul, iSSl, hs0: glyphHintState) -> Any: loc = iSSl[sidx][ul].bestLocation(ul == 0) + assert self.hs if loc is not None: return loc for sv in hs0.stemValues: @@ -2190,9 +2203,15 @@ def OKToRem(self, loc, spc) -> bool: class hhinter(dimensionHinter): def __init__(self, options) -> None: super().__init__(options) +<<<<<<< HEAD self.topPairs = [] self.bottomPairs = [] +======= + self.topPairs: List[BlueValue] = [] + self.bottomPairs: List[BlueValue] = [] + +>>>>>>> b7cb160d (More type hints) def startFlex(self) -> None: """Make pt.a map to x and pt.b map to y""" set_log_parameters(dimension='-') @@ -2218,6 +2237,7 @@ def startHint(self) -> None: stopHint = stopStemConvert = stopMaskConvert = stopFlex def dominantStems(self) -> Any: + assert self.fddict return self.fddict.DominantH # pytype: disable=attribute-error def isV(self) -> bool: diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index eac00928c..23b2db5ec 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -80,7 +80,7 @@ def __init__(self, aloc: float, oMin: float, oMax: float, self.pe = weakref.ref(pe) else: self.pe = None - self.hintval = None + self.hintval: Optional[stemValue] = None self.replacedBy: Optional[Self] = None self.deleted = False self.suppressed = False diff --git a/python/afdko/otfautohint/otfFont.py b/python/afdko/otfautohint/otfFont.py index ebc7170ac..f3e8c5102 100644 --- a/python/afdko/otfautohint/otfFont.py +++ b/python/afdko/otfautohint/otfFont.py @@ -18,6 +18,7 @@ # CFF.desubroutinize: the module adds this class method to the CFF and CFF2 # classes. import fontTools.subset.cff +from typing import List, Dict from . import fdTools, FontParseError from .glyphData import glyphData @@ -221,7 +222,7 @@ def __init__(self, path, font_format): self.font_format = font_format self.is_cff2 = False self.is_vf = False - self.vs_data_models = None + self.vs_data_models: List[VarDataModel] = [] self.desc = None if font_format == "OTF": # It is an OTF font, we can process it directly. @@ -695,7 +696,7 @@ def merge_hinted_programs(charstring, t2_programs, gname, vs_data_model): @_add_method(VarStoreInstancer) -def get_scalars(self, vsindex, region_idx): +def get_scalars(self, vsindex, region_idx) -> Dict[int, float]: varData = self._varData # The index key needs to be the master value index, which includes # the default font value. VarRegionIndex provides the region indices. @@ -714,7 +715,9 @@ def __init__(self, var_data, vsindex, master_vsi_list): self.var_data = var_data self.master_vsi_list = master_vsi_list self._num_masters = len(master_vsi_list) - self.delta_weights = [{}] # for default font value + + # for default font value + self.delta_weights: List[Dict[int, float]] = [{}] for region_idx, vsi in enumerate(master_vsi_list[1:]): scalars = vsi.get_scalars(vsindex, region_idx) self.delta_weights.append(scalars) @@ -723,9 +726,9 @@ def __init__(self, var_data, vsindex, master_vsi_list): def num_masters(self): return self._num_masters - def getDeltas(self, master_values, *, round=noRound): + def getDeltas(self, master_values, *, round=noRound) -> List[float]: assert len(master_values) == len(self.delta_weights) - out = [] + out: List[float] = [] for i, scalars in enumerate(self.delta_weights): delta = master_values[i] for j, scalar in scalars.items(): From 2c24afac2644391a98873cd4eb5d2a4a894b0287 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 14:22:17 +0100 Subject: [PATCH 18/42] Move number upstairs, others --- python/afdko/otfautohint/__init__.py | 5 +++++ python/afdko/otfautohint/fdTools.py | 2 +- python/afdko/otfautohint/glyphData.py | 10 ++++------ python/afdko/otfautohint/hinter.py | 20 ++++++++------------ python/afdko/otfautohint/hintstate.py | 11 +++++++---- python/afdko/otfautohint/report.py | 3 +++ 6 files changed, 28 insertions(+), 23 deletions(-) diff --git a/python/afdko/otfautohint/__init__.py b/python/afdko/otfautohint/__init__.py index 17980ea35..1087e2cd8 100644 --- a/python/afdko/otfautohint/__init__.py +++ b/python/afdko/otfautohint/__init__.py @@ -1,4 +1,9 @@ import os +from typing import Union + + +Number = Union[int, float] + class FontParseError(Exception): diff --git a/python/afdko/otfautohint/fdTools.py b/python/afdko/otfautohint/fdTools.py index 3b3a4ae5e..955f31461 100644 --- a/python/afdko/otfautohint/fdTools.py +++ b/python/afdko/otfautohint/fdTools.py @@ -15,7 +15,7 @@ import os from typing import Optional, Tuple, List, Dict -from .glyphData import Number +from . import Number log = logging.getLogger(__name__) diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index fa1b68200..4ad698647 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -26,13 +26,11 @@ import logging -from .hintstate import glyphHintState +# from .hintstate import glyphHintState +from . import Number log = logging.getLogger(__name__) -Number = Union[int, float] - - def norm_float(value: float) -> Number: """Converts a float (whose decimal part is zero) to integer""" if isinstance(value, float): @@ -831,8 +829,8 @@ def __init__(self, roundCoords, name=''): self.pathEdited = False self.boundsMap = {} - self.hhs: Optional[glyphHintState] = None - self.vhs: Optional[glyphHintState] = None + self.hhs: Optional[object] = None + self.vhs: Optional[object] = None # pen methods: diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index 1d01dc707..dbf5cf097 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -16,7 +16,8 @@ from fontTools.misc.bezierTools import solveCubic -from .glyphData import Number, pathElement, pt, feq, fne, stem, glyphData +from . import Number +from .glyphData import pathElement, pt, feq, fne, stem, glyphData from .hintstate import (hintSegment, stemValue, glyphHintState, links, instanceStemState) from .overlap import removeOverlap @@ -148,11 +149,7 @@ def __init__(self, options) -> None: self.report = None self.name = None self.isMulti = False -<<<<<<< HEAD - self.hs : Optional[glyphHintState] = None -======= self.hs: glyphHintState = glyphHintState() ->>>>>>> b7cb160d (More type hints) self.fddict: Optional[FDDict] = None self.Bonus = None self.Pruning = None @@ -1477,9 +1474,11 @@ def mergeVals(self) -> None: if not self.options.report_zones: return svl = self.hs.stemValues + sv = None for sv in svl: sv.merge = False while True: + assert sv and sv.best try: _, bst = max(((sv.best.compVal(self.SFactor), sv) for sv in svl if not sv.merge)) @@ -1799,6 +1798,7 @@ def convertToStemLists(self) -> bool: self.hs.stems = tuple(([] for g in self.gllist)) if self.options.removeConflicts: wl = self.hs.weights = [] + assert self.hs.stems dsl = self.hs.stems[0] if self.hs.counterHinted: self.hs.stemValues = sorted(self.hs.mainValues) @@ -2008,6 +2008,7 @@ def unconflict(self, sc, curSet=None, pinSet=None) -> Any: for x in range(l) if curSet[sidx])), curSet) else: pinSet[doIdx] = True + assert doConflictSet for sidx in doConflictSet: curSet[sidx] = False r1 = self.unconflict(sc, curSet, pinSet) @@ -2096,7 +2097,7 @@ def convertToMasks(self) -> None: # they're not checked in the optimal order it's better to go through # them again to remove conflicts self.hs.mainMask = mm = [False] * l - okl = [] + okl: List[stemValue] = [] mv = self.hs.mainValues.copy() while mv: ok = True @@ -2203,15 +2204,9 @@ def OKToRem(self, loc, spc) -> bool: class hhinter(dimensionHinter): def __init__(self, options) -> None: super().__init__(options) -<<<<<<< HEAD - self.topPairs = [] - self.bottomPairs = [] - -======= self.topPairs: List[BlueValue] = [] self.bottomPairs: List[BlueValue] = [] ->>>>>>> b7cb160d (More type hints) def startFlex(self) -> None: """Make pt.a map to x and pt.b map to y""" set_log_parameters(dimension='-') @@ -2560,6 +2555,7 @@ def distributeMasks(self, glyph) -> Optional[list]: # If keepHints was true hhs.stems was already set to glyph.hstems in # converttoMasks() stems[0] = glyph.hstems = glyph.hhs.stems[0] + assert stems[0] lnstm[0] = len(stems[0]) if self.hHinter.keepHints: if glyph.startmasks and glyph.startmasks[0]: diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index 23b2db5ec..d0ee24dcc 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -10,7 +10,8 @@ from enum import IntEnum -from .glyphData import Number, feq, pathElement, stem +from . import Number +from .glyphData import feq, pathElement, stem from _weakref import ReferenceType from typing import Any, Dict, List, Optional, Set, Tuple, Type, Self, Protocol @@ -227,7 +228,7 @@ def __init__(self) -> None: self.s_segs: List[hintSegment] = [] self.m_segs: List[hintSegment] = [] self.e_segs: List[hintSegment] = [] - self.mask = [] + self.mask: List[bool] = [] def cleanup(self) -> None: """Updates and deletes segments according to deleted and replacedBy""" @@ -624,7 +625,9 @@ def mark(self, stemValues: List[stemValue], isV) -> None: self.links[lsubp][usubp] = 1 self.links[usubp][lsubp] = 1 - def moveIdx(self, suborder, subidxs: List[int], outlinks, idx: int) -> None: + def moveIdx( + self, suborder: List[int], subidxs: List[int], outlinks, idx: int + ) -> None: """ Move value idx from subidxs to the end of suborder and update outlinks to record all links shared with idx @@ -648,7 +651,7 @@ def shuffle(self) -> Optional[List[int]]: self.logLinks() self.logShort(sumlinks, "Sumlinks") subidxs = list(range(self.cnt)) - suborder = [] + suborder: List[int] = [] while subidxs: # negate s to preserve all-links-equal subpath ordering _, bst = max(((sumlinks[s], -s) for s in subidxs)) diff --git a/python/afdko/otfautohint/report.py b/python/afdko/otfautohint/report.py index c3e451ea5..acb78ee56 100644 --- a/python/afdko/otfautohint/report.py +++ b/python/afdko/otfautohint/report.py @@ -8,6 +8,9 @@ import logging import time from collections import defaultdict +from typing import Dict + +from . import Number log = logging.getLogger(__name__) From cb0459771ed6fae4ace41f93a6d2c1a219898725 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 14:12:34 +0100 Subject: [PATCH 19/42] Complex numbers would be a mistake here --- python/afdko/otfautohint/glyphData.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index 4ad698647..f4f47504e 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -237,14 +237,14 @@ def __mul__(self, other): Returns a new pt object with this object's coordinates multiplied by a scalar value """ - if not isinstance(other, numbers.Number): + if not isinstance(other, (int, float)): raise TypeError('One argument to pt.__mul__ must be a scalar ' + 'number') return pt(self[0] * other, self[1] * other) def __rmul__(self, other): """Same as __mul__ for right-multiplication""" - if not isinstance(other, numbers.Number): + if not isinstance(other, (int, float)): raise TypeError('One argument to pt.__rmul__ must be a scalar ' + 'number') return pt(self[0] * other, self[1] * other) From e338e328b6a9fe72dc7ffac2f6afc51d2f48547f Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 14:12:42 +0100 Subject: [PATCH 20/42] More type hinting --- python/afdko/otfautohint/hintstate.py | 2 +- python/afdko/otfautohint/report.py | 32 +++++++++++++-------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index d0ee24dcc..dc4f880ee 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -286,7 +286,7 @@ class glyphHintState: """ def __init__(self) -> None: - self.peStates = {} + self.peStates: Dict[pathElement, pathElementHintState] = {} self.overlapRemoved = None self.increasingSegs: List[hintSegment] = [] self.decreasingSegs: List[hintSegment] = [] diff --git a/python/afdko/otfautohint/report.py b/python/afdko/otfautohint/report.py index acb78ee56..f38069d92 100644 --- a/python/afdko/otfautohint/report.py +++ b/python/afdko/otfautohint/report.py @@ -17,7 +17,7 @@ class GlyphReport: """Class to store stem and zone data from a particular glyph""" - def __init__(self, name=None, all_stems=False): + def __init__(self, name: str="", all_stems=False): self.name = name self.hstems = {} self.vstems = {} @@ -33,7 +33,7 @@ def charZone(self, l, u): def stemZone(self, l, u): self.stem_zone_stems.add((l, u)) - def stem(self, l, u, isLine, isV=False): + def stem(self, l, u, isLine: bool, isV=False): if not isLine and not self.all_stems: return if isV: @@ -49,22 +49,22 @@ def stem(self, l, u, isLine, isV=False): class Report: def __init__(self): - self.glyphs = {} + self.glyphs: Dict[str, GlyphReport] = {} @staticmethod - def round_value(val): + def round_value(val: float) -> int: if val >= 0: return int(val + 0.5) else: return int(val - 0.5) - def parse_stem_dict(self, stem_dict): + def parse_stem_dict(self, stem_dict: Dict[float, int]) -> Dict[float,int]: """ stem_dict: {45.5: 1, 47.0: 2} """ # key: stem width # value: stem count - width_dict = defaultdict(int) + width_dict: Dict[float, int] = defaultdict(int) for width, count in stem_dict.items(): width = self.round_value(width) width_dict[width] += count @@ -75,8 +75,8 @@ def parse_zone_dicts(self, char_dict, stem_dict): all_zones_dict.update(stem_dict) # key: zone height # value: zone count - top_dict = defaultdict(int) - bot_dict = defaultdict(int) + top_dict: Dict[Number,int] = defaultdict(int) + bot_dict: Dict[Number,int] = defaultdict(int) for bot, top in all_zones_dict: top = self.round_value(top) top_dict[top] += 1 @@ -108,15 +108,15 @@ def _get_lists(self, options): if not (options.report_stems or options.report_zones): return [], [], [], [] - h_stem_items_dict = defaultdict(set) - h_stem_count_dict = defaultdict(int) - v_stem_items_dict = defaultdict(set) - v_stem_count_dict = defaultdict(int) + h_stem_items_dict: Dict[Number, set[str]] = defaultdict(set) + h_stem_count_dict: Dict[Number, int] = defaultdict(int) + v_stem_items_dict: Dict[Number, set[str]] = defaultdict(set) + v_stem_count_dict: Dict[Number, int] = defaultdict(int) - top_zone_items_dict = defaultdict(set) - top_zone_count_dict = defaultdict(int) - bot_zone_items_dict = defaultdict(set) - bot_zone_count_dict = defaultdict(int) + top_zone_items_dict: Dict[Number, set[str]] = defaultdict(set) + top_zone_count_dict: Dict[Number, int] = defaultdict(int) + bot_zone_items_dict: Dict[Number, set[str]] = defaultdict(set) + bot_zone_count_dict: Dict[Number, int] = defaultdict(int) for gName, gr in self.glyphs.items(): if options.report_stems: From 40badd572fb1f4623966f90a00becb57511b7cb4 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 14:30:39 +0100 Subject: [PATCH 21/42] This was a true optional --- python/afdko/otfautohint/otfFont.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/afdko/otfautohint/otfFont.py b/python/afdko/otfautohint/otfFont.py index f3e8c5102..d81a9b7e0 100644 --- a/python/afdko/otfautohint/otfFont.py +++ b/python/afdko/otfautohint/otfFont.py @@ -18,7 +18,7 @@ # CFF.desubroutinize: the module adds this class method to the CFF and CFF2 # classes. import fontTools.subset.cff -from typing import List, Dict +from typing import List, Dict, Optional from . import fdTools, FontParseError from .glyphData import glyphData @@ -222,7 +222,7 @@ def __init__(self, path, font_format): self.font_format = font_format self.is_cff2 = False self.is_vf = False - self.vs_data_models: List[VarDataModel] = [] + self.vs_data_models: Optional[List[VarDataModel]] = None self.desc = None if font_format == "OTF": # It is an OTF font, we can process it directly. From db2a6dcbb58af8f9eb2a78e89413b9cc8f67e804 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 14:35:31 +0100 Subject: [PATCH 22/42] More fixups --- python/afdko/otfautohint/hinter.py | 24 ++++++++++++++---------- python/afdko/otfautohint/hintstate.py | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index dbf5cf097..0e412f858 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -147,12 +147,12 @@ def __init__(self, options) -> None: self.gllist: List[glyphData] = [] self.glyph: glyphData = glyphData(False) self.report = None - self.name = None + self.name = "" self.isMulti = False self.hs: glyphHintState = glyphHintState() self.fddict: Optional[FDDict] = None self.Bonus = None - self.Pruning = None + self.Pruning = False def setGlyph( self, @@ -651,7 +651,7 @@ def nodeIsFlat(self, c, doPrev=False) -> Optional[bool]: and FlatMin """ if not c: - return + return None if doPrev: sp = self.glyph.prevSlopePoint(c) if sp is None: @@ -674,12 +674,12 @@ def sameDir(self, c, doPrev=False) -> Optional[bool]: if doPrev: p = self.glyph.prevInSubpath(c, skipTiny=True, segSub=True) if p is None: - return + return None p0, p1, p2 = c.e, c.s, p.s else: n = self.glyph.nextInSubpath(c, skipTiny=True, segSub=True) if n is None: - return + return None p0, p1, p2 = c.s, c.e, n.e if (self.diffSign(p0.y - p1.y, p1.y - p2.y) or self.diffSign(p0.x - p1.x, p1.x - p2.x)): @@ -1158,7 +1158,7 @@ def stemMiss(self, ls, us) -> Optional[int]: return 0 if us.min > ls.max or us.max < ls.min: # no overlap - return + return None overlen = min(us.max, ls.max) - max(us.min, ls.min) minlen = min(us.max - us.min, ls.max - ls.min) @@ -1167,11 +1167,11 @@ def stemMiss(self, ls, us) -> Optional[int]: else: dist = loc_d * (1 + .4 * (1 - overlen / minlen)) if dist < self.MinDist / 2: - return + return None d, nearStem = min(((abs(s - loc_d), s) for s in self.dominantStems())) if d == 0 or d > 2: - return + return None log.info("%s %s stem near miss: %g instead of %g at %g to %g." % (self.aDesc(), "curve" if (ls.isCurve() or us.isCurve()) else "linear", @@ -1589,6 +1589,7 @@ def reportStems(self) -> None: glyphTop = -1e40 glyphBot = 1e40 isV = self.isV() + assert self.report for sv in self.hs.stemValues: l, u = sv.lloc, sv.uloc glyphBot = min(l, glyphBot) @@ -1680,7 +1681,9 @@ def tryCounterHinting(self) -> bool: (e.g. highest value) mainValue stems """ minloc = midloc = maxloc = 1e40 - mindelta = middelta = maxdelta = 0 + mindelta: Number = 0 + middelta: Number = 0 + maxdelta: Number = 0 hvl = self.hs.mainValues hvll = len(hvl) if hvll < 3: @@ -1797,7 +1800,8 @@ def convertToStemLists(self) -> bool: valuepairmap = {} self.hs.stems = tuple(([] for g in self.gllist)) if self.options.removeConflicts: - wl = self.hs.weights = [] + self.hs.weights = [] + wl = self.hs.weights assert self.hs.stems dsl = self.hs.stems[0] if self.hs.counterHinted: diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index dc4f880ee..f180c32e9 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -295,7 +295,7 @@ def __init__(self) -> None: self.rejectValues: List[stemValue] = [] self.counterHinted = False self.stems: Optional[List[stem]] = None # in sorted glyphData format - self.weights = None + self.weights: List[float] = [] self.keepHints = None self.hasOverlaps: bool = False self.stemOverlaps: List[List[bool]] = [] From c8687bcdee0cf29333f562d92ea790ccec3dff35 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 14:41:55 +0100 Subject: [PATCH 23/42] Oops, they reused a variable --- python/afdko/otfautohint/hinter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index 0e412f858..3ea362c35 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -1478,7 +1478,6 @@ def mergeVals(self) -> None: for sv in svl: sv.merge = False while True: - assert sv and sv.best try: _, bst = max(((sv.best.compVal(self.SFactor), sv) for sv in svl if not sv.merge)) From 916aa853a4178b06a575c2e87e21b12d8bc119dd Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 14:57:16 +0100 Subject: [PATCH 24/42] Fix assertion --- python/afdko/otfautohint/glyphData.py | 2 +- python/afdko/otfautohint/hinter.py | 9 ++++++--- python/afdko/otfautohint/hintstate.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index f4f47504e..630062ab9 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -272,7 +272,7 @@ class stem(tuple): BandMargin = 30 __slots__ = () - def __new__(cls, lb=0, rt=0): + def __new__(cls, lb: Number = 0, rt: Number = 0): if isinstance(lb, tuple): return _tuple.__new__(cls, lb) return _tuple.__new__(cls, (lb, rt)) diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index 3ea362c35..b6dcfa06d 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -247,7 +247,7 @@ def isV(self) -> bool: pass @abstractmethod - def segmentLists(self): + def segmentLists(self) -> Tuple[List[hintSegment], List[hintSegment]]: pass @abstractmethod @@ -1307,7 +1307,7 @@ def pruneStemVals(self) -> None: break self.hs.stemValues = [sv for sv in self.hs.stemValues if not sv.pruned] - def closeSegs(self, s1, s2) -> bool: + def closeSegs(self, s1: hintSegment, s2: hintSegment) -> bool: """ Returns true if the segments (and the path between them) are within CloseMerge of one another @@ -1325,6 +1325,7 @@ def closeSegs(self, s1, s2) -> bool: loca -= self.CloseMerge locb += self.CloseMerge n = s1.pe() + assert n is not None p = self.glyph.prevInSubpath(n) pe2 = s2.pe() ngood = pgood = True @@ -1484,6 +1485,7 @@ def mergeVals(self) -> None: except ValueError: break bst.merge = True + assert bst.best is not None for sv in svl: replace = False if sv.merge or bst.isGhost != sv.isGhost: @@ -1968,6 +1970,7 @@ def bestLocation(self, sidx, ul, iSSl, hs0: glyphHintState) -> Any: if sv.idx != sidx: continue seg = sv.useg if ul == 1 else sv.lseg + assert seg.hintval is not None if seg.hintval.idx != sidx: loc = iSSl[seg.hintval.idx][ul].bestLocation(ul == 0) if loc is not None: @@ -2558,7 +2561,7 @@ def distributeMasks(self, glyph) -> Optional[list]: # If keepHints was true hhs.stems was already set to glyph.hstems in # converttoMasks() stems[0] = glyph.hstems = glyph.hhs.stems[0] - assert stems[0] + assert stems[0] is not None lnstm[0] = len(stems[0]) if self.hHinter.keepHints: if glyph.startmasks and glyph.startmasks[0]: diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index f180c32e9..4fa57f337 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -165,7 +165,7 @@ def __init__( self.merge = False self.lseg = lseg self.useg = useg - self.best = None + self.best: Optional[stemValue] = None self.initialVal = val self.idx: Optional[int] = None From 7d3d512d79aa1c2c2f6115b3be897a8d32e7e50e Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 20:52:31 +0100 Subject: [PATCH 25/42] Make mypy happy without check-untyped-defs --- python/afdko/otfautohint/glyphData.py | 12 ++++++- python/afdko/otfautohint/hinter.py | 50 ++++++++++++++++----------- python/afdko/otfautohint/hintstate.py | 4 +-- python/afdko/otfautohint/report.py | 20 +++++------ 4 files changed, 52 insertions(+), 34 deletions(-) diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index 630062ab9..73fe66dc1 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -529,6 +529,11 @@ class pathElement: assocMatchFactor = 93 tSlop = .005 + s: pt + e: pt + cs: pt + ce: pt + def __init__(self, *args, is_close=False, masks=None, flex=False, position=None): self.is_line = False @@ -635,7 +640,12 @@ def clearHints(self, doVert=False): elif not doVert and self.masks is not None: self.masks = [None, self.masks[1]] if self.masks[1] else None - def cubicParameters(self): + def cubicParameters(self) -> Tuple[ + Tuple[float, float], + Tuple[float, float], + Tuple[float, float], + Tuple[float, float], + ]: """Returns the fontTools cubic parameters for this pathElement""" return calcCubicParameters(self.s, self.cs, self.ce, self.e) diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index b6dcfa06d..a38406034 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -146,12 +146,12 @@ def __init__(self, options) -> None: self.fddicts: List[FDDict] = [] self.gllist: List[glyphData] = [] self.glyph: glyphData = glyphData(False) - self.report = None + self.report: Optional[GlyphReport] = None self.name = "" self.isMulti = False self.hs: glyphHintState = glyphHintState() self.fddict: Optional[FDDict] = None - self.Bonus = None + self.Bonus: int = 0 self.Pruning = False def setGlyph( @@ -171,7 +171,7 @@ def setGlyph( self.isMulti = (len(gllist) > 1) self.name = name self.HasFlex = False - self.Bonus = None + self.Bonus = 0 self.Pruning = True if self.isV(): self.hs = self.glyph.vhs = glyphHintState() @@ -180,7 +180,7 @@ def setGlyph( def resetForHinting(self) -> None: """Reset state for rehinting same glyph""" - self.Bonus = None + self.Bonus = 0 self.Pruning = True if self.isV(): self.hs = self.glyph.vhs = glyphHintState() @@ -686,7 +686,9 @@ def sameDir(self, c, doPrev=False) -> Optional[bool]: return False return not self.testBend(p0, p1, p2) - def extremaSegment(self, pe, extp, extt, isMn) -> Tuple[Any, Any]: + def extremaSegment( + self, pe: pathElement, extp: pt, extt, isMn: bool + ) -> Tuple[Any, Any]: """ Given a curved pathElement pe and a point on that spline extp at t == extt, calculates a segment intersecting extp where all portions @@ -1176,6 +1178,7 @@ def stemMiss(self, ls, us) -> Optional[int]: (self.aDesc(), "curve" if (ls.isCurve() or us.isCurve()) else "linear", loc_d, nearStem, ls.loc, us.loc)) + return None def addStemValue(self, lloc, uloc, val, spc, lseg, useg) -> None: """Adapts the stem parameters into a stemValue object and adds it""" @@ -1480,7 +1483,8 @@ def mergeVals(self) -> None: sv.merge = False while True: try: - _, bst = max(((sv.best.compVal(self.SFactor), sv) for sv in svl + _, bst = max(((sv.best.compVal(self.SFactor), sv) # type: ignore + for sv in svl if not sv.merge)) except ValueError: break @@ -1608,8 +1612,8 @@ def reportStems(self) -> None: def mainVals(self) -> None: """Picks set of highest-valued non-overlapping stems""" - mainValues = [] - rejectValues = [] + mainValues: List[stemValue] = [] + rejectValues: List[stemValue] = [] svl = copy(self.hs.stemValues) prevBV: Number = 0 while True: @@ -1684,7 +1688,7 @@ def tryCounterHinting(self) -> bool: minloc = midloc = maxloc = 1e40 mindelta: Number = 0 middelta: Number = 0 - maxdelta: Number = 0 + maxdelta: Number = 0 hvl = self.hs.mainValues hvll = len(hvl) if hvll < 3: @@ -1727,19 +1731,19 @@ def addBBox(self, doSubpaths=False) -> None: top/bottom or right/left of each subpath """ gl = self.glyph + paraml: Iterable = [None] if doSubpaths: paraml = range(len(gl.subpaths)) ltype = hintSegment.sType.LSBBOX utype = hintSegment.sType.USBBOX else: - paraml = [None] ltype = hintSegment.sType.LGBBOX utype = hintSegment.sType.UGBBOX for param in paraml: pbs = gl.getBounds(param) if pbs is None: continue - mn_pt, mx_pt = pt(tuple(pbs.bounds[0])), pt(tuple(pbs.bounds[1])) + mn_pt, mx_pt = pt(*pbs.bounds[0]), pt(*pbs.bounds[1]) peidx = 0 if self.isV() else 1 if any((hv.lloc <= mx_pt.o and mn_pt.o <= hv.uloc for hv in self.hs.mainValues)): @@ -1794,15 +1798,15 @@ def convertToStemLists(self) -> bool: self.hs.stems = tuple((g.hstems if g.hstems else [] for g in self.gllist)) l = self.hs.stems[0] - self.stopStemConvert() - return all((len(sl) == l for sl in self.hs.stems)) + self.stopStemConvert() + return all((len(sl) == l for sl in self.hs.stems)) # Build up the default stem list - valuepairmap = {} + valuepairmap: Dict[Tuple[Number, Number], int] = {} self.hs.stems = tuple(([] for g in self.gllist)) if self.options.removeConflicts: self.hs.weights = [] - wl = self.hs.weights + wl = self.hs.weights assert self.hs.stems dsl = self.hs.stems[0] if self.hs.counterHinted: @@ -1899,8 +1903,8 @@ def calcInstanceStems(self, glidx) -> None: raise NotImplementedError pbs = self.glyph.getBounds(peSi.position[0]) if pbs is not None: - mn_pt = pt(tuple(pbs.bounds[0])) - mx_pt = pt(tuple(pbs.bounds[1])) + mn_pt = pt(*pbs.bounds[0]) + mx_pt = pt(*pbs.bounds[1]) score = mx_pt.a - mn_pt.a loc = mx_pt.o if seg0.isUBBox() else mn_pt.o iSS.addToLoc(loc, score, strong=True, bb=True) @@ -1954,6 +1958,7 @@ def calcInstanceStems(self, glidx) -> None: lo = hi + 50 log.warning("Stem end is less than start for non-" "ghost stem, will be removed from set") + assert lo is not None and hi is not None sl.append(stem((lo, hi))) self.fddict = self.fddicts[0] @@ -1977,6 +1982,7 @@ def bestLocation(self, sidx, ul, iSSl, hs0: glyphHintState) -> Any: return loc log.warning("No data for %s location of stem %d" % ('lower' if ul == 0 else 'upper', sidx)) + assert hs0.stems is not None return hs0.stems[0][sidx][ul] def unconflict(self, sc, curSet=None, pinSet=None) -> Any: @@ -2094,8 +2100,9 @@ def convertToMasks(self) -> None: continue if all(sl[sidx].ghostCompat(sl[sjdx]) for sl in self.hs.stems): assert so[sidx][sjdx] - assert gc[sidx] is not None - gc[sidx][sjdx] = True # pytype: disable=unsupported-operands + target = gc[sidx] + assert target is not None + target[sjdx] = True # pytype: disable=unsupported-operands # The stems corresponding to mainValues may conflict now. This isn't # a fatal problem because mainMask stems are only added if they don't @@ -2109,8 +2116,9 @@ def convertToMasks(self) -> None: ok = True c = mv.pop(0) assert c.idx is not None - for s in okl: - if so[c.idx][s.idx]: + for sv in okl: + assert sv.idx is not None + if so[c.idx][sv.idx]: ok = False break if ok and self.hs.goodMask[c.idx]: diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index 4fa57f337..faace0b47 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -13,7 +13,7 @@ from . import Number from .glyphData import feq, pathElement, stem from _weakref import ReferenceType -from typing import Any, Dict, List, Optional, Set, Tuple, Type, Self, Protocol +from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Type, Self, Protocol log: logging.Logger = logging.getLogger(__name__) @@ -294,7 +294,7 @@ def __init__(self) -> None: self.mainValues: List[stemValue] = [] self.rejectValues: List[stemValue] = [] self.counterHinted = False - self.stems: Optional[List[stem]] = None # in sorted glyphData format + self.stems: Optional[Sequence[List[stem]]] = None # in sorted glyphData format self.weights: List[float] = [] self.keepHints = None self.hasOverlaps: bool = False diff --git a/python/afdko/otfautohint/report.py b/python/afdko/otfautohint/report.py index f38069d92..25f67753f 100644 --- a/python/afdko/otfautohint/report.py +++ b/python/afdko/otfautohint/report.py @@ -8,7 +8,7 @@ import logging import time from collections import defaultdict -from typing import Dict +from typing import Dict, Tuple from . import Number @@ -19,21 +19,21 @@ class GlyphReport: """Class to store stem and zone data from a particular glyph""" def __init__(self, name: str="", all_stems=False): self.name = name - self.hstems = {} - self.vstems = {} - self.hstems_pos = set() - self.vstems_pos = set() - self.char_zones = set() - self.stem_zone_stems = set() + self.hstems: Dict[float,int] = {} + self.vstems: Dict[float,int] = {} + self.hstems_pos: set[Tuple[Number,Number]] = set() + self.vstems_pos: set[Tuple[Number,Number]] = set() + self.char_zones: set[Tuple[Number,Number]] = set() + self.stem_zone_stems: set[Tuple[Number, Number]] = set() self.all_stems = all_stems - def charZone(self, l, u): + def charZone(self, l: Number, u: Number): self.char_zones.add((l, u)) - def stemZone(self, l, u): + def stemZone(self, l: Number, u: Number): self.stem_zone_stems.add((l, u)) - def stem(self, l, u, isLine: bool, isV=False): + def stem(self, l: Number, u: Number, isLine: bool, isV=False): if not isLine and not self.all_stems: return if isV: From 84fe6b010814cb3d00c3ec92eecec7c861187b09 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 21:53:13 +0100 Subject: [PATCH 26/42] As close to done as I'm getting --- mypy.ini | 2 + python/afdko/otfautohint/autohint.py | 11 ++--- python/afdko/otfautohint/fdTools.py | 40 ++++++++++-------- python/afdko/otfautohint/glyphData.py | 40 ++++++++++-------- python/afdko/otfautohint/otfFont.py | 52 +++++++++++++++--------- python/afdko/otfautohint/splitpsdicts.py | 19 ++++++--- python/afdko/otfautohint/ufoFont.py | 39 +++++++++++------- 7 files changed, 123 insertions(+), 80 deletions(-) create mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 000000000..976ba0294 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +ignore_missing_imports = True diff --git a/python/afdko/otfautohint/autohint.py b/python/afdko/otfautohint/autohint.py index 01b93cdf6..1840d71c1 100644 --- a/python/afdko/otfautohint/autohint.py +++ b/python/afdko/otfautohint/autohint.py @@ -11,6 +11,7 @@ from collections import namedtuple from threading import Thread from multiprocessing import Pool, Manager, current_process +from typing import Iterator, Optional, Union from .otfFont import CFFFontData from .ufoFont import UFOFontData @@ -311,6 +312,7 @@ def hint(self): pool = None logThread = None + gmap: Optional[Iterator] = None try: dictRecord = self.dictManager.getDictRecord() if pcount == 1: @@ -363,6 +365,7 @@ def hint(self): pool.close() pool.join() logQueue.put(None) + assert logThread is not None logThread.join() finally: if pool is not None: @@ -385,17 +388,15 @@ def close(self): f.font.close() -def openFont(path, options): +def openFont(path, options) -> Union[UFOFontData, CFFFontData]: font_format = get_font_format(path) if font_format is None: raise FontParseError(f"{path} is not a supported font format") if font_format == "UFO": - font = UFOFontData(path, options.logOnly, options.writeToDefaultLayer) + return UFOFontData(path, options.logOnly, options.writeToDefaultLayer) else: - font = CFFFontData(path, font_format) - - return font + return CFFFontData(path, font_format) def get_outpath(options, font_path, i): diff --git a/python/afdko/otfautohint/fdTools.py b/python/afdko/otfautohint/fdTools.py index 955f31461..136366a1b 100644 --- a/python/afdko/otfautohint/fdTools.py +++ b/python/afdko/otfautohint/fdTools.py @@ -13,7 +13,7 @@ import numbers import sys import os -from typing import Optional, Tuple, List, Dict +from typing import Any, Optional, Tuple, List, Dict from . import Number @@ -125,13 +125,15 @@ class FDDict: BlueValuesPairs: List[BlueValue] OtherBlueValuesPairs: List[BlueValue] BlueFuzz: int - DominantH: int - DominantV: int + DominantH: List[int] + DominantV: List[int] BaselineOvershoot: Number BaselineYCoord: Number FamilyBaselineYCoord: Number + OrigEmSqUnits: Number + FontName: Optional[str] - def __init__(self, fdIndex, dictName=None, fontName=None): + def __init__(self, fdIndex, dictName=None, fontName: Optional[str] = None): self.fdIndex = fdIndex for key in kFDDictKeys: setattr(self, key, None) @@ -453,6 +455,7 @@ def parseFontInfoFile(fdArrayMap, data, glyphList, maxY, minY, fontName): for dictName, fdIndex in fdIndexDict.items(): fdDict = fdArrayMap[fdIndex] + assert fdDict is not None if fdDict.DominantH is None: log.warning("The FDDict '%s' in fontinfo has no " "DominantH value", dictName) @@ -493,13 +496,13 @@ def parseFontInfoFile(fdArrayMap, data, glyphList, maxY, minY, fontName): return fdSelectMap, finalFDict -def mergeFDDicts(prevDictList): +def mergeFDDicts(prevDictList: List[FDDict]) -> Dict[str, Any]: # Extract the union of the stem widths and zones from the list # of FDDicts, and replace the current values in the topDict. blueZoneDict: BlueZoneDict = {} otherBlueZoneDict: BlueZoneDict = {} - dominantHDict = {} - dominantVDict = {} + dominantHDict: Dict[int, str] = {} + dominantVDict: Dict[int, str] = {} bluePairListNames = [kFontDictBluePairsName, kFontDictOtherBluePairsName] zoneDictList = [blueZoneDict, otherBlueZoneDict] for prefDDict in prevDictList: @@ -519,7 +522,7 @@ def mergeFDDicts(prevDictList): stemDictList = [dominantHDict, dominantVDict] for i in (0, 1): stemFieldName = stemNameList[i] - dList = getattr(prefDDict, stemFieldName) + dList: List[int] = getattr(prefDDict, stemFieldName) stemDict = stemDictList[i] if dList is not None: for width in dList: @@ -527,10 +530,10 @@ def mergeFDDicts(prevDictList): assert prefDDict # Now we have collected all the stem widths and zones # from all the dicts. See if we can merge them. - goodBlueZoneList = [] - goodOtherBlueZoneList = [] - goodHStemList = [] - goodVStemList = [] + goodBlueZoneList: List[int] = [] + goodOtherBlueZoneList: List[int] = [] + goodHStemList: List[int] = [] + goodVStemList: List[int] = [] zoneDictList = [blueZoneDict, otherBlueZoneDict] goodZoneLists = [goodBlueZoneList, goodOtherBlueZoneList] @@ -591,7 +594,7 @@ def mergeFDDicts(prevDictList): else: goodStemList.append(stem) prevStem = stem - privateMap = {} + privateMap: Dict[str, Any] = {} if goodBlueZoneList: privateMap['BlueValues'] = goodBlueZoneList if goodOtherBlueZoneList: @@ -659,7 +662,7 @@ def getFDInfo(font, desc, options, glyphList, isVF): options.noFlex, options.vCounterGlyphs, options.hCounterGlyphs, desc) - fdArrayMap = {0: fdDict} + fdArrayMap: Dict[int, FDDict] = {0: fdDict} minY, maxY = font.get_min_max(fdDict.OrigEmSqUnits) fdSelectMap, finalFDict = parseFontInfoFile( fdArrayMap, srcFontinfoData, glyphList, maxY, minY, desc) @@ -672,7 +675,7 @@ def getFDInfo(font, desc, options, glyphList, isVF): privateMap = mergeFDDicts([finalFDict]) elif isVF: fdSelectMap = {} - dictRecord = {} + dictRecord: Dict[int, Dict[int, FDDict]] = {} fdArraySet = set() for name in glyphList: fdIndex = font.getfdIndex(name) @@ -715,7 +718,7 @@ def __init__(self, options, fontInstances, glyphList, isVF=False): self.fontInstances = fontInstances self.glyphList = glyphList self.isVF = isVF - self.fdSelectMap = None + self.fdSelectMap: Optional[Dict] = None self.auxRecord = {} refI = fontInstances[0] fdArrayCompat = True @@ -784,6 +787,7 @@ def getDictRecord(self): return self.dictRecord def getRecKey(self, gname, vsindex): + assert self.fdSelectMap is not None fdIndex = self.fdSelectMap[gname] if vsindex in self.dictRecord and fdIndex in self.dictRecord[vsindex]: return vsindex, fdIndex @@ -801,7 +805,7 @@ def getRecKey(self, gname, vsindex): self.auxRecord[vsindex][fdIndex] = fddict return fddict - def checkGlyphList(self): + def checkGlyphList(self) -> None: options = self.options glyphSet = set(self.glyphList) # Check for missing glyphs explicitly added via fontinfo or cmd line @@ -815,7 +819,7 @@ def checkGlyphList(self): log.warning("%s glyph named in fontinfo is " % label + "not in font: %s" % name) - def addDict(self, dict1, dict2): + def addDict(self, dict1: Dict, dict2: Dict) -> bool: # This allows for sparse masters, just verifying that if a glyph name # is in both dictionaries it maps to the same index. good = True diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index 73fe66dc1..de2fa245c 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -11,7 +11,8 @@ from math import sqrt from collections import defaultdict from builtins import tuple as _tuple -from typing import Optional, Tuple, Union +from typing import Any, List, Optional, Tuple, Union, Self +from fontTools.config import Option # pytype: disable=import-error from fontTools.misc.bezierTools import ( @@ -558,35 +559,35 @@ def __init__(self, *args, is_close=False, masks=None, flex=False, 'pathElement.__init__') self.masks = masks self.flex = flex - self.bounds = None + self.bounds: Optional[boundsState] = None self.position: Tuple[int, int] = position or (-1, -1) self.segment_sub = None - def getBounds(self): + def getBounds(self) -> boundsState: """Returns the bounds object for the object, generating it if needed""" - if self.bounds: + if self.bounds is not None: return self.bounds self.bounds = boundsState(self) return self.bounds - def clearTempState(self): + def clearTempState(self) -> None: self.bounds = None self.segment_sub = None - def isLine(self): + def isLine(self) -> bool: """Returns True if the spline is a line""" return self.is_line - def isClose(self): + def isClose(self) -> bool: """Returns True if this pathElement implicitly closes a subpath""" return self.is_close - def isStart(self): + def isStart(self) -> bool: """Returns True if this pathElement starts a subpath""" assert self.position is not None return self.position[1] == 0 - def isTiny(self): + def isTiny(self) -> bool: """ Returns True if the start and end points of the spline are within two em-units in both dimensions @@ -594,7 +595,7 @@ def isTiny(self): d = (self.e - self.s).abs() return d.x < 2 and d.y < 2 - def isShort(self): + def isShort(self) -> bool: """ Returns True if the start and end points of the spline are within about six em-units @@ -603,7 +604,7 @@ def isShort(self): mx, mn = sorted(tuple(d)) return mx + mn * .336 < 6 # head.c IsShort - def convertToLine(self): + def convertToLine(self) -> None: """ If the pathElement is not already a line, make it one with the same start and end points @@ -782,7 +783,7 @@ def splitAtInflectionsForSegs(self): return True return False - def splitAt(self, t): + def splitAt(self, t: Number) -> Self: if self.is_line: pb = self.s + (self.e - self.s) * t ret = pathElement(pb, self.e, position=self.position, @@ -804,14 +805,19 @@ def splitAt(self, t): self.bounds = None return ret - def atT(self, t): + def atT(self, t: Number) -> pt: return pt(segmentPointAtT(self.fonttoolsSegment(), t)) - def fonttoolsSegment(self): + def fonttoolsSegment(self) -> List[Tuple[Number, Number]]: if self.is_line: - return [tuple(self.s), tuple(self.e)] + return [(self.s[0], self.s[1]), + (self.e[0], self.e[1])] else: - return [tuple(self.s), tuple(self.cs), tuple(self.ce), tuple(self.e)] + return [ + (self.s[0], self.s[1]), + (self.cs[0], self.cs[1]), + (self.ce[0], self.ce[1]), + (self.e[0], self.e[1])] class glyphData(BasePen): @@ -1023,7 +1029,7 @@ def getBounds(self, subpath=None): self.boundsMap[subpath] = b return b - def T2(self, version=1): + def T2(self, version=1) -> List[Any]: """Returns an array of T2 operators corresponding to the object""" prog = [] diff --git a/python/afdko/otfautohint/otfFont.py b/python/afdko/otfautohint/otfFont.py index d81a9b7e0..84b8fadd7 100644 --- a/python/afdko/otfautohint/otfFont.py +++ b/python/afdko/otfautohint/otfFont.py @@ -14,13 +14,14 @@ from fontTools.misc.roundTools import noRound, otRound from fontTools.varLib.varStore import VarStoreInstancer from fontTools.varLib.cff import CFF2CharStringMergePen, MergeOutlineExtractor + # import subset.cff is needed to load the implementation for # CFF.desubroutinize: the module adds this class method to the CFF and CFF2 # classes. import fontTools.subset.cff -from typing import List, Dict, Optional +from typing import Any, List, Dict, Optional, Tuple, Union -from . import fdTools, FontParseError +from . import Number, fdTools, FontParseError from .glyphData import glyphData # keep linting tools quiet about unused import @@ -34,12 +35,14 @@ class SEACError(Exception): """Raised when a charString has an obsolete 'seac' operator""" + pass def _add_method(*clazzes): """Returns a decorator function that adds a new method to one or more classes.""" + def wrapper(method): done = [] for clazz in clazzes: @@ -56,7 +59,7 @@ def wrapper(method): return wrapper -def hintOn(i, hintMaskBytes): +def hintOn(i: int, hintMaskBytes: list) -> bool: """Used to help convert T2 hintmask bytes to a boolean array""" byteIndex = int(i / 8) byteValue = hintMaskBytes[byteIndex] @@ -86,14 +89,15 @@ class T2ToGlyphDataExtractor(T2OutlineExtractor): Inherits from the fontTools Outline Extractor and adapts some of the operator methods to match the "hint pen" interface of the glyphData class """ - def __init__(self, gd, localSubrs, globalSubrs, nominalWidthX, + + def __init__(self, gd: glyphData, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, readStems=True, readFlex=True): T2OutlineExtractor.__init__(self, gd, localSubrs, globalSubrs, nominalWidthX, defaultWidthX) self.glyph = gd self.readStems = readStems self.readFlex = readFlex - self.hintMaskBytes = None + self.hintMaskBytes: Optional[int] = None self.vhintCount = 0 self.hhintCount = 0 @@ -217,7 +221,9 @@ def _run_tx(args): class CFFFontData: - def __init__(self, path, font_format): + vsIndexMap: Dict[str, Tuple[int, bool]] + + def __init__(self, path: str, font_format: str) -> None: self.inputPath = path self.font_format = font_format self.is_cff2 = False @@ -264,7 +270,7 @@ def __init__(self, path, font_format): if 'fvar' in self.ttFont: # have not yet collected VF global data. self.is_vf = True - self.glyph_programs = [] + self.glyph_programs: List[List[Any]] = [] self.vsIndexMap = {} self.axes = self.ttFont['fvar'].axes CFF2 = self.cffTable @@ -278,10 +284,10 @@ def __init__(self, path, font_format): self.temp_cs_merge = copy.deepcopy(self.charStrings['.notdef']) self.vs_data_models = self.get_vs_data_models(topDict, self.axes) - def getGlyphList(self): + def getGlyphList(self) -> List[str]: return self.ttFont.getGlyphOrder() - def getPSName(self): + def getPSName(self) -> str: if self.is_cff2 and 'name' in self.ttFont: psName = next((name_rec.string for name_rec in self.ttFont[ 'name'].names if (name_rec.nameID == 6) and ( @@ -291,7 +297,7 @@ def getPSName(self): psName = self.cffTable.cff.fontNames[0] return psName - def get_min_max(self, upm): + def get_min_max(self, upm: Number) -> Tuple[Number, Number]: if self.is_cff2 and 'hhea' in self.ttFont: font_max = self.ttFont['hhea'].ascent font_min = self.ttFont['hhea'].descent @@ -303,8 +309,9 @@ def get_min_max(self, upm): font_min = -upm * 0.25 return font_min, font_max - def convertToGlyphData(self, glyphName, readStems, readFlex, - roundCoords, doAll=False): + def convertToGlyphData(self, glyphName: str, readStems: bool, + readFlex: bool, roundCoords: bool, + doAll=False) -> glyphData: t2CharString = self.charStrings[glyphName] try: gl = convertT2ToGlyphData(t2CharString, readStems, readFlex, @@ -315,7 +322,7 @@ def convertToGlyphData(self, glyphName, readStems, readFlex, gl = None return gl - def updateFromGlyph(self, gl, glyphName): + def updateFromGlyph(self, gl: glyphData, glyphName): if gl is None: return t2Program = gl.T2() @@ -392,8 +399,11 @@ def getPrivateDictVal(self, pDict, attr, default, vsindex, vsi): return val - def getPrivateFDDict(self, allowNoBlues, noFlex, vCounterGlyphs, - hCounterGlyphs, desc, fdIndex=0, gl_vsindex=None): + def getPrivateFDDict(self, allowNoBlues: bool, noFlex: bool, + vCounterGlyphs, hCounterGlyphs, desc: str, + fdIndex=0, gl_vsindex: Optional[int] = None) -> Union[ + Tuple[List[fdTools.FDDict], int], # VF + fdTools.FDDict]: # non-VF pTopDict = self.topDict if hasattr(pTopDict, "FDArray"): pDict = pTopDict.FDArray[fdIndex] @@ -403,9 +413,10 @@ def getPrivateFDDict(self, allowNoBlues, noFlex, vCounterGlyphs, privateDict = pDict.Private if self.is_vf: - dict_vsindex = getattr(privateDict, 'vsindex', 0) + dict_vsindex = getattr(privateDict, "vsindex", 0) if gl_vsindex is None: gl_vsindex = dict_vsindex + assert self.vs_data_models is not None vs_data_model = self.vs_data_models[gl_vsindex] vsi_list = vs_data_model.master_vsi_list else: @@ -487,8 +498,8 @@ def getPrivateFDDict(self, allowNoBlues, noFlex, vCounterGlyphs, sstems = self.getPrivateDictVal(privateDict, ssnap, [], dict_vsindex, vsi) elif hasattr(privateDict, stdw): - sstems = [self.getPrivateDictVal(privateDict, stdw, - -1, dict_vsindex, vsi)] + sstems = [self.getPrivateDictVal(privateDict, stdw, -1, + dict_vsindex, vsi)] else: if allowNoBlues or self.is_vf: # XXX adjusted for vf @@ -527,7 +538,7 @@ def getPrivateFDDict(self, allowNoBlues, noFlex, vCounterGlyphs, fdDictArray.append(fdDict) - assert self.is_vf + assert self.is_vf and gl_vsindex is not None return fdDictArray, gl_vsindex def getfdIndex(self, name): @@ -576,6 +587,7 @@ def getInstanceName(vn, vsi, axes): def get_vf_glyphs(self, glyph_name): charstring = self.charStrings[glyph_name] + assert self.vs_data_models is not None if 'vsindex' in charstring.program: op_index = charstring.program.index('vsindex') @@ -622,6 +634,7 @@ def get_vs_data_models(topDict, axes): def merge_hinted_glyphs(self, name): vsindex, addIndex = self.vsIndexMap[name] + assert self.vs_data_models is not None new_t2cs = merge_hinted_programs(self.temp_cs_merge, self.glyph_programs, name, self.vs_data_models[vsindex]) @@ -710,7 +723,6 @@ def get_scalars(self, vsindex, region_idx) -> Dict[int, float]: class VarDataModel(object): - def __init__(self, var_data, vsindex, master_vsi_list): self.var_data = var_data self.master_vsi_list = master_vsi_list diff --git a/python/afdko/otfautohint/splitpsdicts.py b/python/afdko/otfautohint/splitpsdicts.py index 45d3fc3f7..15d6db9a3 100644 --- a/python/afdko/otfautohint/splitpsdicts.py +++ b/python/afdko/otfautohint/splitpsdicts.py @@ -6,6 +6,7 @@ import plistlib import re import sys +from typing import List from fontTools.ttLib import TTFont from fontTools.cffLib import FontDict, FDArrayIndex, PrivateDict, FDSelect @@ -61,7 +62,15 @@ def get_options(args): return options -class dictspec(): +class Dictspec(): + name: str + re: List[re.Pattern] + BluePairs: List[int] + OtherPairs: List[int] + SIV: List[int] + SIH: List[int] + foundGlyph: bool + pass @@ -71,7 +80,7 @@ def getDictmap(options): dictmap = [] for name, d in rawdictmap.items(): - dct = dictspec() + dct = Dictspec() dct.name = name dct.BluePairs = d.get("blue_pairs", []) dct.OtherPairs = d.get("other_blue_pairs", []) @@ -86,7 +95,7 @@ def getDictmap(options): return dictmap -def remapDicts(fpath, dictmap): +def remapDicts(fpath: str, dictmap: List[Dictspec]): f = TTFont(fpath) if 'CFF ' not in f: logger.error("No CFF table in %s: will not modify" % fpath) @@ -103,7 +112,7 @@ def remapDicts(fpath, dictmap): # Create a new FDSelect object for the font and map the glyphs to # dictionary indexes according to the regular expressions in - # dictspec.re + # Dictspec.re fdselect = top.FDSelect = FDSelect() for gn in cff.getGlyphOrder(): done = False @@ -125,7 +134,7 @@ def remapDicts(fpath, dictmap): # wind up with empty dictionaries later. j = 0 indexMap = [] - newdictmap = [] + newdictmap: List[Dictspec] = [] for dictspec in dictmap: if dictspec.foundGlyph: newdictmap.append(dictspec) diff --git a/python/afdko/otfautohint/ufoFont.py b/python/afdko/otfautohint/ufoFont.py index 815401a1d..57e9e0c0c 100644 --- a/python/afdko/otfautohint/ufoFont.py +++ b/python/afdko/otfautohint/ufoFont.py @@ -112,6 +112,7 @@ import shutil from types import SimpleNamespace +from typing import Any, Dict, Optional, Tuple # from fontTools.pens.basePen import BasePen from fontTools.pens.pointPen import AbstractPointPen @@ -378,8 +379,9 @@ def isCID(): def convertToGlyphData(self, name, readStems, readFlex, roundCoords, doAll=False): - glyph, skip = self._get_or_skip_glyph(name, readStems, readFlex, - roundCoords, doAll) + glyph, skip = self._get_or_skip_glyph( + name, readStems, readFlex, roundCoords, doAll + ) if skip: return None return glyph @@ -414,8 +416,10 @@ def save(self, path): if os.path.abspath(self.path) != os.path.abspath(path): # If user has specified a path other than the source font path, # then copy the entire UFO font, and operate on the copy. - log.info("Copying from source UFO font to output UFO font before " - "processing...") + log.info( + "Copying from source UFO font to output UFO font before " + "processing..." + ) if os.path.exists(path): shutil.rmtree(path) shutil.copytree(self.path, path) @@ -424,7 +428,7 @@ def save(self, path): self._reader.formatVersionTuple, validate=False) - layer = PROCESSED_LAYER_NAME + layer: Optional[str] = PROCESSED_LAYER_NAME if self.writeToDefaultLayer: layer = None @@ -494,9 +498,9 @@ def writeHashMap(self, writer): data.append("'%s': %s," % (gName, hashMap[gName])) data.append("}") data.append("") - data = "\n".join(data) + joined = "\n".join(data) - writer.writeData(HASHMAP_NAME, data.encode("utf-8")) + writer.writeData(HASHMAP_NAME, joined.encode("utf-8")) def updateHashEntry(self, glyphName): # srcHash has already been set: we are fixing the history list. @@ -607,7 +611,9 @@ def _get_or_skip_glyph(self, name, readStems, readFlex, roundCoords, def getGlyphList(self): glyphOrder = self._reader.readLib().get(PUBLIC_GLYPH_ORDER, []) - glyphList = list(self._get_glyphset().keys()) + glyphset = self._get_glyphset() + assert glyphset is not None + glyphList = list(glyphset.keys()) # Sort the returned glyph list by the glyph order as we depend in the # order for expanding glyph ranges. @@ -615,12 +621,14 @@ def key_fn(v): if v in glyphOrder: return glyphOrder.index(v) return len(glyphOrder) + return sorted(glyphList, key=key_fn) @property def glyphMap(self): if self._glyphmap is None: glyphset = self._get_glyphset() + assert glyphset is not None self._glyphmap = glyphset.contents return self._glyphmap @@ -774,7 +782,6 @@ def close(): class HashPointPen(AbstractPointPen): - def __init__(self, glyph, glyphset=None): self.glyphset = glyphset self.width = norm_float(round(getattr(glyph, "width", 0), 9)) @@ -829,19 +836,20 @@ class GlyphDataWrapper(object): Wraps a glyphData object while storing the properties set by readGlyph to aid output of hint data in Adobe's "hint format 2" for UFO. """ + def __init__(self, glyph): - self._glyph = glyph + self._glyph: glyphData = glyph self.lib = {} if hasattr(glyph, 'width'): self.width = norm_float(glyph.width) - def addUfoFlex(self, uhl, pointname): + def addUfoFlex(self, uhl: Dict[str, Any], pointname: str): """Mark the named point as starting a flex hint""" if uhl.get(FLEX_INDEX_LIST_NAME, None) is None: uhl[FLEX_INDEX_LIST_NAME] = [] uhl[FLEX_INDEX_LIST_NAME].append(pointname) - def addUfoMask(self, uhl, masks, pointname): + def addUfoMask(self, uhl: Dict[str, Any], masks, pointname: str): """Associates the hint set represented by masks with the named point""" if uhl.get(HINT_SET_LIST_NAME, None) is None: uhl[HINT_SET_LIST_NAME] = [] @@ -870,12 +878,13 @@ def addUfoMask(self, uhl, masks, pointname): p, w = s.UFOVals() ustems.append("%s %s %s" % (opname[i], norm_float(p), norm_float(w))) - hintset = {} + hintset: Dict[str, Any] = {} hintset[POINT_TAG] = pointname hintset[STEMS_NAME] = ustems uhl[HINT_SET_LIST_NAME].append(hintset) - def addUfoHints(self, uhl, pe, labelnum, startSubpath=False): + def addUfoHints(self, uhl: Optional[Dict[str, Any]], pe, labelnum: int, + startSubpath=False) -> Tuple[int, Optional[str]]: """Adds hints to the pathElement, naming points as necessary""" pn = POINT_NAME_PATTERN % labelnum if uhl is None: @@ -900,7 +909,7 @@ def drawPoints(self, pen, ufoHintLib=True): some points and building a library of hint annotations """ if ufoHintLib is not None: - uhl = {} + uhl: Dict[str, Any] = {} ufoH = lambda pe, lm, ss=False: self.addUfoHints(uhl, pe, lm, ss) else: ufoH = None From 6ac132e423a89d8133ac309d35414fbf655cb86e Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 22:04:14 +0100 Subject: [PATCH 27/42] Fix failing test --- python/afdko/otfautohint/glyphData.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index de2fa245c..0211d3381 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -97,8 +97,8 @@ def __init__(self, x: Number = 0, y: Number = 0, roundCoords=False): If roundCoords is True the values are rounded before storing """ if isinstance(x, tuple): - y = float(x[1]) - x = float(x[0]) + y = x[1] + x = x[0] if roundCoords: x = round(x) y = round(y) From ef05a403300fac9aa869e0702ad7debc83dff6d7 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 22:15:34 +0100 Subject: [PATCH 28/42] Use typing_extensions to support <3.11 --- python/afdko/otfautohint/glyphData.py | 4 ++-- python/afdko/otfautohint/hinter.py | 13 +++++++++++-- python/afdko/otfautohint/hintstate.py | 13 +++++++++++-- requirements.txt | 1 + setup.py | 3 ++- 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index 0211d3381..05c6f919f 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -11,8 +11,8 @@ from math import sqrt from collections import defaultdict from builtins import tuple as _tuple -from typing import Any, List, Optional, Tuple, Union, Self -from fontTools.config import Option +from typing import Any, List, Optional, Tuple, Union +from typing_extensions import Self # pytype: disable=import-error from fontTools.misc.bezierTools import ( diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index a38406034..55c195166 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -11,8 +11,17 @@ import math from copy import copy, deepcopy from abc import abstractmethod, ABC -from collections import namedtuple -from typing import Any, Dict, Iterable, List, NamedTuple, Tuple, Type, TypeVar, Union, Optional, Self +from typing import ( + Any, + Dict, + Iterable, + List, + NamedTuple, + Tuple, + Union, + Optional, +) +from typing_extensions import Self from fontTools.misc.bezierTools import solveCubic diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index faace0b47..47269f268 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -13,8 +13,17 @@ from . import Number from .glyphData import feq, pathElement, stem from _weakref import ReferenceType -from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Type, Self, Protocol - +from typing import ( + Any, + Dict, + List, + Optional, + Sequence, + Set, + Tuple, + Protocol, +) +from typing_extensions import Self log: logging.Logger = logging.getLogger(__name__) diff --git a/requirements.txt b/requirements.txt index 144ead91a..e3ee22b6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ psautohint==2.4.0 tqdm==4.66.1 ufonormalizer==0.6.1 ufoProcessor==1.9.0 +typing-extensions==4.8.0 diff --git a/setup.py b/setup.py index 79712cca2..250ba73f9 100644 --- a/setup.py +++ b/setup.py @@ -200,7 +200,8 @@ def main(): 'setuptools_scm', 'scikit-build', 'cmake', - 'ninja' + 'ninja', + 'typing_extensions' ], tests_require=[ 'pytest', From 5e5a0e31fb07d01ef4993848a31499d0b69be0e4 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 18 Oct 2023 22:15:43 +0100 Subject: [PATCH 29/42] Restore formatting --- python/afdko/otfautohint/__init__.py | 1 - python/afdko/otfautohint/glyphData.py | 2 +- python/afdko/otfautohint/report.py | 65 ++++++++++++++------------- python/afdko/otfautohint/ufoFont.py | 4 +- 4 files changed, 39 insertions(+), 33 deletions(-) diff --git a/python/afdko/otfautohint/__init__.py b/python/afdko/otfautohint/__init__.py index 1087e2cd8..62444af42 100644 --- a/python/afdko/otfautohint/__init__.py +++ b/python/afdko/otfautohint/__init__.py @@ -5,7 +5,6 @@ Number = Union[int, float] - class FontParseError(Exception): pass diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index 05c6f919f..eea64ae1e 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -4,7 +4,6 @@ Internal representation of a T2 CharString glyph with hints """ -import numbers import threading import operator from copy import deepcopy @@ -32,6 +31,7 @@ log = logging.getLogger(__name__) + def norm_float(value: float) -> Number: """Converts a float (whose decimal part is zero) to integer""" if isinstance(value, float): diff --git a/python/afdko/otfautohint/report.py b/python/afdko/otfautohint/report.py index 25f67753f..7dcf59bb8 100644 --- a/python/afdko/otfautohint/report.py +++ b/python/afdko/otfautohint/report.py @@ -17,13 +17,14 @@ class GlyphReport: """Class to store stem and zone data from a particular glyph""" - def __init__(self, name: str="", all_stems=False): + + def __init__(self, name: str = "", all_stems=False): self.name = name - self.hstems: Dict[float,int] = {} - self.vstems: Dict[float,int] = {} - self.hstems_pos: set[Tuple[Number,Number]] = set() - self.vstems_pos: set[Tuple[Number,Number]] = set() - self.char_zones: set[Tuple[Number,Number]] = set() + self.hstems: Dict[float, int] = {} + self.vstems: Dict[float, int] = {} + self.hstems_pos: set[Tuple[Number, Number]] = set() + self.vstems_pos: set[Tuple[Number, Number]] = set() + self.char_zones: set[Tuple[Number, Number]] = set() self.stem_zone_stems: set[Tuple[Number, Number]] = set() self.all_stems = all_stems @@ -58,7 +59,7 @@ def round_value(val: float) -> int: else: return int(val - 0.5) - def parse_stem_dict(self, stem_dict: Dict[float, int]) -> Dict[float,int]: + def parse_stem_dict(self, stem_dict: Dict[float, int]) -> Dict[float, int]: """ stem_dict: {45.5: 1, 47.0: 2} """ @@ -75,8 +76,8 @@ def parse_zone_dicts(self, char_dict, stem_dict): all_zones_dict.update(stem_dict) # key: zone height # value: zone count - top_dict: Dict[Number,int] = defaultdict(int) - bot_dict: Dict[Number,int] = defaultdict(int) + top_dict: Dict[Number, int] = defaultdict(int) + bot_dict: Dict[Number, int] = defaultdict(int) for bot, top in all_zones_dict: top = self.round_value(top) top_dict[top] += 1 @@ -146,20 +147,20 @@ def _get_lists(self, options): # item 0: stem count # item 1: stem width # item 2: list of glyph names - h_stem_list = self.assemble_rep_list( - h_stem_items_dict, h_stem_count_dict) + h_stem_list = self.assemble_rep_list(h_stem_items_dict, + h_stem_count_dict) - v_stem_list = self.assemble_rep_list( - v_stem_items_dict, v_stem_count_dict) + v_stem_list = self.assemble_rep_list(v_stem_items_dict, + v_stem_count_dict) # item 0: zone count # item 1: zone height # item 2: list of glyph names - top_zone_list = self.assemble_rep_list( - top_zone_items_dict, top_zone_count_dict) + top_zone_list = self.assemble_rep_list(top_zone_items_dict, + top_zone_count_dict) - bot_zone_list = self.assemble_rep_list( - bot_zone_items_dict, bot_zone_count_dict) + bot_zone_list = self.assemble_rep_list(bot_zone_items_dict, + bot_zone_count_dict) return h_stem_list, v_stem_list, top_zone_list, bot_zone_list @@ -189,25 +190,29 @@ def _sort_val_reversed(t): def save(self, path, options): h_stems, v_stems, top_zones, bot_zones = self._get_lists(options) - items = ([h_stems, self._sort_count], - [v_stems, self._sort_count], - [top_zones, self._sort_val_reversed], - [bot_zones, self._sort_val]) + items = ( + [h_stems, self._sort_count], + [v_stems, self._sort_count], + [top_zones, self._sort_val_reversed], + [bot_zones, self._sort_val], + ) atime = time.asctime() suffixes = (".hstm.txt", ".vstm.txt", ".top.txt", ".bot.txt") - titles = ("Horizontal Stem List for %s on %s\n" % (path, atime), - "Vertical Stem List for %s on %s\n" % (path, atime), - "Top Zone List for %s on %s\n" % (path, atime), - "Bottom Zone List for %s on %s\n" % (path, atime), - ) - headers = (["count width glyphs\n"] * 2 + - ["count height glyphs\n"] * 2) + titles = ( + "Horizontal Stem List for %s on %s\n" % (path, atime), + "Vertical Stem List for %s on %s\n" % (path, atime), + "Top Zone List for %s on %s\n" % (path, atime), + "Bottom Zone List for %s on %s\n" % (path, atime), + ) + headers = ["count width glyphs\n"] * 2 + [ + "count height glyphs\n" + ] * 2 for i, item in enumerate(items): reps, sortFunc = item if not reps: continue - fName = f'{path}{suffixes[i]}' + fName = f"{path}{suffixes[i]}" title = titles[i] header = headers[i] with open(fName, "w") as fp: @@ -215,6 +220,6 @@ def save(self, path, options): fp.write(header) reps.sort(key=sortFunc) for rep in reps: - gnames = ' '.join(rep[2]) + gnames = " ".join(rep[2]) fp.write(f"{rep[0]:5} {rep[1]:5} [{gnames}]\n") log.info("Wrote %s" % fName) diff --git a/python/afdko/otfautohint/ufoFont.py b/python/afdko/otfautohint/ufoFont.py index 57e9e0c0c..fd2ae8c98 100644 --- a/python/afdko/otfautohint/ufoFont.py +++ b/python/afdko/otfautohint/ufoFont.py @@ -689,7 +689,8 @@ def getPrivateFDDict(self, allowNoBlues, noFlex, vCounterGlyphs, for i in range(3, numBlueValues, 2): blueValues[i] = blueValues[i] - blueValues[i - 1] - numBlueValues = min(numBlueValues, len(fdTools.kBlueValueKeys)) + numBlueValues = min(numBlueValues, + len(fdTools.kBlueValueKeys)) for i in range(numBlueValues): key = fdTools.kBlueValueKeys[i] value = blueValues[i] @@ -782,6 +783,7 @@ def close(): class HashPointPen(AbstractPointPen): + def __init__(self, glyph, glyphset=None): self.glyphset = glyphset self.width = norm_float(round(getattr(glyph, "width", 0), 9)) From 60f2ea38266543a0a2ac816986e7d140ec951830 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 20 Oct 2023 22:11:34 +0100 Subject: [PATCH 30/42] Ignore mypy error --- python/afdko/otfautohint/glyphData.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index eea64ae1e..ac47779ed 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -791,7 +791,6 @@ def splitAt(self, t: Number) -> Self: self.e = pb self.is_close = False self.bounds = None - return ret else: s, n = splitCubicAtT(self.s, self.cs, self.ce, self.e, t) ptsn = [pt(p) for p in n] @@ -803,7 +802,9 @@ def splitAt(self, t: Number) -> Self: self.e = pt(s[3]) self.is_close = False self.bounds = None - return ret + # The typing is correct but neither mypy nor pytype handle + # self types well yet. + return ret # type: ignore def atT(self, t: Number) -> pt: return pt(segmentPointAtT(self.fonttoolsSegment(), t)) From f4128b9d0fc107373963bdfcd107b54c6efab023 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 20 Oct 2023 22:12:00 +0100 Subject: [PATCH 31/42] Use lo/hi, not tuple --- python/afdko/otfautohint/hinter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/afdko/otfautohint/hinter.py b/python/afdko/otfautohint/hinter.py index 55c195166..260519224 100644 --- a/python/afdko/otfautohint/hinter.py +++ b/python/afdko/otfautohint/hinter.py @@ -1968,7 +1968,7 @@ def calcInstanceStems(self, glidx) -> None: log.warning("Stem end is less than start for non-" "ghost stem, will be removed from set") assert lo is not None and hi is not None - sl.append(stem((lo, hi))) + sl.append(stem(lo, hi)) self.fddict = self.fddicts[0] self.glyph = self.gllist[0] From f11236d518591d5e736e6742d64e156a5b653fd5 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 20 Oct 2023 22:12:49 +0100 Subject: [PATCH 32/42] pathElement is optional in hintSegment --- python/afdko/otfautohint/hintstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index 47269f268..2b010dd74 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -50,7 +50,7 @@ class sType(IntEnum): def __init__(self, aloc: float, oMin: float, oMax: float, - pe: pathElement, typ, bonus, isV, + pe: Optional[pathElement], typ, bonus, isV, isInc, desc) -> None: """ Initializes the object From 68a6f63b306b184988931d00c61bdabbea6470f8 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 20 Oct 2023 22:13:28 +0100 Subject: [PATCH 33/42] Good catch --- python/afdko/fdkutils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/afdko/fdkutils.py b/python/afdko/fdkutils.py index 7508bef58..156e1c84d 100644 --- a/python/afdko/fdkutils.py +++ b/python/afdko/fdkutils.py @@ -175,6 +175,7 @@ def runShellCmdLogging(cmd, shell=True): proc = subprocess.Popen(cmd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) while 1: + assert proc.stdout output = proc.stdout.readline().rstrip() if output: print(output.decode('utf-8', 'backslashreplace')) From f4c5c6bd8c3392941b32d998cb8d443c94aa9b7b Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 20 Oct 2023 22:18:04 +0100 Subject: [PATCH 34/42] Check all methods --- mypy.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy.ini b/mypy.ini index 976ba0294..281dd2ecb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,2 +1,3 @@ [mypy] ignore_missing_imports = True +check_untyped_defs = True From e8375c94d9ee2e3d3720770bb962ec1a8071f4dc Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 20 Oct 2023 22:18:12 +0100 Subject: [PATCH 35/42] Add mypy to requirements-dev --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4a2bf203f..4ab3017ca 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,6 +3,7 @@ cpplint>=1.4.3 cython>=0.29.5 flake8>=3.7.6 flake8-bugbear>=20.1.4 +mypy>=1.6.1 ninja>=1.9.0 pip>=19.0.2 pytest-cov>=2.6.1 From 4c286d05c7d31dd51ad4f33404cb882f5ee049dc Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 20 Oct 2023 22:18:19 +0100 Subject: [PATCH 36/42] Run mypy on build --- .github/workflows/testpythonpackage.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/testpythonpackage.yml b/.github/workflows/testpythonpackage.yml index f4a33b330..29ef134f3 100644 --- a/.github/workflows/testpythonpackage.yml +++ b/.github/workflows/testpythonpackage.yml @@ -99,6 +99,10 @@ jobs: run: | python -m pytest -n auto --dist loadfile --no-cov tests --color=yes + - name: Test type hints (otfautohint only) + run: | + mypy python/afdko/otfautohint + - name: Test uninstall AFDKO run: | python -m pip uninstall afdko -y From 859f98d369935f8961896a4dd7a1d5cc5725b088 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 20 Oct 2023 22:31:55 +0100 Subject: [PATCH 37/42] Restore code style --- python/afdko/otfautohint/hintstate.py | 55 ++++++++++----------------- python/afdko/otfautohint/report.py | 46 ++++++++++------------ python/afdko/otfautohint/ufoFont.py | 9 ++--- 3 files changed, 44 insertions(+), 66 deletions(-) diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index 2b010dd74..def3a2d56 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -433,27 +433,21 @@ def remExtraBends(self) -> None: for hsd in self.decreasingSegs[lo:hi]: assert hsd.loc == hsi.loc if hsd.min < hsi.max and hsd.max > hsi.min: - if ( - hsi.type == hintSegment.sType.BEND - and hsd.type != hintSegment.sType.BEND - and hsd.type != hintSegment.sType.GHOST - and (hsd.max - hsd.min) > (hsi.max - hsi.min) * 3 - ): + if (hsi.type == hintSegment.sType.BEND and + hsd.type != hintSegment.sType.BEND and + hsd.type != hintSegment.sType.GHOST and + (hsd.max - hsd.min) > (hsi.max - hsi.min) * 3): hsi.deleted = True - log.debug( - "rem seg loc %g from %g to %g" % (hsi.loc, hsi.min, hsi.max) - ) + log.debug("rem seg loc %g from %g to %g" % + (hsi.loc, hsi.min, hsi.max)) break - elif ( - hsd.type == hintSegment.sType.BEND - and hsi.type != hintSegment.sType.BEND - and hsi.type != hintSegment.sType.GHOST - and (hsi.max - hsi.min) > (hsd.max - hsd.min) * 3 - ): + elif (hsd.type == hintSegment.sType.BEND and + hsi.type != hintSegment.sType.BEND and + hsi.type != hintSegment.sType.GHOST and + (hsi.max - hsi.min) > (hsd.max - hsd.min) * 3): hsd.deleted = True - log.debug( - "rem seg loc %g from %g to %g" % (hsd.loc, hsd.min, hsd.max) - ) + log.debug("rem seg loc %g from %g to %g" % + (hsd.loc, hsd.min, hsd.max)) def deleteSegments(self) -> None: for s in self.increasingSegs: @@ -592,26 +586,17 @@ def logLinks(self) -> None: if self.cnt == 0: return log.debug("Links") - log.debug(" ".join((str(i).rjust(2) for i in range(self.cnt)))) + log.debug(' '.join((str(i).rjust(2) for i in range(self.cnt)))) for j in range(self.cnt): - log.debug( - " ".join( - ( - ("Y" if self.links[j][i] else " ").rjust(2) - for i in range(self.cnt) - ) - ) - ) - - def logShort(self, shrt, lab) -> None: + log.debug(' '.join((('Y' if self.links[j][i] else ' ').rjust(2) + for i in range(self.cnt)))) + + def logShort(self, shrt, lab): """Prints a log message representing (1-d) shrt""" log.debug(lab) - log.debug(" ".join((str(i).rjust(2) for i in range(self.cnt)))) - log.debug( - " ".join( - ((str(shrt[i]) if shrt[i] else " ").rjust(2) for i in range(self.cnt)) - ) - ) + log.debug(' '.join((str(i).rjust(2) for i in range(self.cnt)))) + log.debug(' '.join(((str(shrt[i]) if shrt[i] else ' ').rjust(2) + for i in range(self.cnt)))) def mark(self, stemValues: List[stemValue], isV) -> None: """ diff --git a/python/afdko/otfautohint/report.py b/python/afdko/otfautohint/report.py index 7dcf59bb8..b39efd6f4 100644 --- a/python/afdko/otfautohint/report.py +++ b/python/afdko/otfautohint/report.py @@ -147,20 +147,20 @@ def _get_lists(self, options): # item 0: stem count # item 1: stem width # item 2: list of glyph names - h_stem_list = self.assemble_rep_list(h_stem_items_dict, - h_stem_count_dict) + h_stem_list = self.assemble_rep_list( + h_stem_items_dict, h_stem_count_dict) - v_stem_list = self.assemble_rep_list(v_stem_items_dict, - v_stem_count_dict) + v_stem_list = self.assemble_rep_list( + v_stem_items_dict, v_stem_count_dict) # item 0: zone count # item 1: zone height # item 2: list of glyph names - top_zone_list = self.assemble_rep_list(top_zone_items_dict, - top_zone_count_dict) + top_zone_list = self.assemble_rep_list( + top_zone_items_dict, top_zone_count_dict) - bot_zone_list = self.assemble_rep_list(bot_zone_items_dict, - bot_zone_count_dict) + bot_zone_list = self.assemble_rep_list( + bot_zone_items_dict, bot_zone_count_dict) return h_stem_list, v_stem_list, top_zone_list, bot_zone_list @@ -190,29 +190,25 @@ def _sort_val_reversed(t): def save(self, path, options): h_stems, v_stems, top_zones, bot_zones = self._get_lists(options) - items = ( - [h_stems, self._sort_count], - [v_stems, self._sort_count], - [top_zones, self._sort_val_reversed], - [bot_zones, self._sort_val], - ) + items = ([h_stems, self._sort_count], + [v_stems, self._sort_count], + [top_zones, self._sort_val_reversed], + [bot_zones, self._sort_val]) atime = time.asctime() suffixes = (".hstm.txt", ".vstm.txt", ".top.txt", ".bot.txt") - titles = ( - "Horizontal Stem List for %s on %s\n" % (path, atime), - "Vertical Stem List for %s on %s\n" % (path, atime), - "Top Zone List for %s on %s\n" % (path, atime), - "Bottom Zone List for %s on %s\n" % (path, atime), - ) - headers = ["count width glyphs\n"] * 2 + [ - "count height glyphs\n" - ] * 2 + titles = ("Horizontal Stem List for %s on %s\n" % (path, atime), + "Vertical Stem List for %s on %s\n" % (path, atime), + "Top Zone List for %s on %s\n" % (path, atime), + "Bottom Zone List for %s on %s\n" % (path, atime), + ) + headers = (["count width glyphs\n"] * 2 + + ["count height glyphs\n"] * 2) for i, item in enumerate(items): reps, sortFunc = item if not reps: continue - fName = f"{path}{suffixes[i]}" + fName = f'{path}{suffixes[i]}' title = titles[i] header = headers[i] with open(fName, "w") as fp: @@ -220,6 +216,6 @@ def save(self, path, options): fp.write(header) reps.sort(key=sortFunc) for rep in reps: - gnames = " ".join(rep[2]) + gnames = ' '.join(rep[2]) fp.write(f"{rep[0]:5} {rep[1]:5} [{gnames}]\n") log.info("Wrote %s" % fName) diff --git a/python/afdko/otfautohint/ufoFont.py b/python/afdko/otfautohint/ufoFont.py index fd2ae8c98..92565aa29 100644 --- a/python/afdko/otfautohint/ufoFont.py +++ b/python/afdko/otfautohint/ufoFont.py @@ -416,10 +416,8 @@ def save(self, path): if os.path.abspath(self.path) != os.path.abspath(path): # If user has specified a path other than the source font path, # then copy the entire UFO font, and operate on the copy. - log.info( - "Copying from source UFO font to output UFO font before " - "processing..." - ) + log.info("Copying from source UFO font to output UFO font before " + "processing...") if os.path.exists(path): shutil.rmtree(path) shutil.copytree(self.path, path) @@ -689,8 +687,7 @@ def getPrivateFDDict(self, allowNoBlues, noFlex, vCounterGlyphs, for i in range(3, numBlueValues, 2): blueValues[i] = blueValues[i] - blueValues[i - 1] - numBlueValues = min(numBlueValues, - len(fdTools.kBlueValueKeys)) + numBlueValues = min(numBlueValues, len(fdTools.kBlueValueKeys)) for i in range(numBlueValues): key = fdTools.kBlueValueKeys[i] value = blueValues[i] From 2e7f3a2be2bed4a686039ebdecb0a69e46d32e36 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 20 Oct 2023 22:35:19 +0100 Subject: [PATCH 38/42] Slacken off the weakref type to make 3.8 happier --- python/afdko/otfautohint/hintstate.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index def3a2d56..c48683191 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -12,9 +12,9 @@ from . import Number from .glyphData import feq, pathElement, stem -from _weakref import ReferenceType from typing import ( Any, + Callable, Dict, List, Optional, @@ -46,7 +46,9 @@ class sType(IntEnum): UGBBOX = 7 # Calcualted from the upper bound of the whole glyph GHOST = 8 - pe: Optional[ReferenceType[pathElement]] + # This is the *ideal* type hint, but 3.8 doesn't support it. + # pe: Optional[weakref.ReferenceType[pathElement]] + pe: Optional[Callable[[],pathElement]] def __init__(self, aloc: float, oMin: float, oMax: float, @@ -87,7 +89,7 @@ def __init__(self, aloc: float, oMin: float, oMax: float, self.isInc = isInc self.desc = desc if pe: - self.pe = weakref.ref(pe) + self.pe = weakref.ref(pe) # type: ignore else: self.pe = None self.hintval: Optional[stemValue] = None From 904624332a31401ae52c3b7a03e68438fd30079d Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 20 Oct 2023 22:56:51 +0100 Subject: [PATCH 39/42] Remove dead import --- python/afdko/otfautohint/glyphData.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index ac47779ed..0f0faf8a5 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -26,7 +26,6 @@ import logging -# from .hintstate import glyphHintState from . import Number log = logging.getLogger(__name__) From 40522b1b15e3e1095e6a229cff07874eca3d78e9 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 20 Oct 2023 22:57:03 +0100 Subject: [PATCH 40/42] pytype does not support Self yet --- python/afdko/otfautohint/glyphData.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/afdko/otfautohint/glyphData.py b/python/afdko/otfautohint/glyphData.py index 0f0faf8a5..68f7792b7 100644 --- a/python/afdko/otfautohint/glyphData.py +++ b/python/afdko/otfautohint/glyphData.py @@ -11,7 +11,7 @@ from collections import defaultdict from builtins import tuple as _tuple from typing import Any, List, Optional, Tuple, Union -from typing_extensions import Self +from typing_extensions import Self # pytype: disable=not-supported-yet # pytype: disable=import-error from fontTools.misc.bezierTools import ( From d034b4579f39161248f3831734c6f36118140ae9 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 20 Oct 2023 22:57:10 +0100 Subject: [PATCH 41/42] Explain Protocol class --- python/afdko/otfautohint/hintstate.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index c48683191..b4f18aeec 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -27,7 +27,14 @@ log: logging.Logger = logging.getLogger(__name__) - +# instanceStemState stores a hinter as one of its members. We'd +# love to be able to use the abstract base class `dimensionHinter` +# as the type for that member, but that would mean this module +# imports hinter which in turn imports this module. Instead, we +# type check against a "protocol" class (a "trait" in other languages) +# declaring only the method that instanceStemState will expect to +# find on the object. `...` stands for "we don't care, nobody's +# calling it anyway". class AHinter(Protocol): def inBand(self, loc, isBottom=False) -> bool: ... From 0945357c97c8e885a48459e90c9bf2333ab5c2ce Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 20 Oct 2023 23:11:06 +0100 Subject: [PATCH 42/42] List can be subscripted, list can't --- python/afdko/otfautohint/hintstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/afdko/otfautohint/hintstate.py b/python/afdko/otfautohint/hintstate.py index b4f18aeec..117602374 100644 --- a/python/afdko/otfautohint/hintstate.py +++ b/python/afdko/otfautohint/hintstate.py @@ -157,7 +157,7 @@ def show(self, label) -> None: log.debug("%s %sseg %g %g to %g %g %s" % pp) -HintSegListWithType = Tuple[str, list[hintSegment]] +HintSegListWithType = Tuple[str, List[hintSegment]] class stemValue: