xref: /aosp_15_r20/external/fonttools/Lib/fontTools/pens/hashPointPen.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1# Modified from https://github.com/adobe-type-tools/psautohint/blob/08b346865710ed3c172f1eb581d6ef243b203f99/python/psautohint/ufoFont.py#L800-L838
2import hashlib
3
4from fontTools.pens.basePen import MissingComponentError
5from fontTools.pens.pointPen import AbstractPointPen
6
7
8class HashPointPen(AbstractPointPen):
9    """
10    This pen can be used to check if a glyph's contents (outlines plus
11    components) have changed.
12
13    Components are added as the original outline plus each composite's
14    transformation.
15
16    Example: You have some TrueType hinting code for a glyph which you want to
17    compile. The hinting code specifies a hash value computed with HashPointPen
18    that was valid for the glyph's outlines at the time the hinting code was
19    written. Now you can calculate the hash for the glyph's current outlines to
20    check if the outlines have changed, which would probably make the hinting
21    code invalid.
22
23    > glyph = ufo[name]
24    > hash_pen = HashPointPen(glyph.width, ufo)
25    > glyph.drawPoints(hash_pen)
26    > ttdata = glyph.lib.get("public.truetype.instructions", None)
27    > stored_hash = ttdata.get("id", None)  # The hash is stored in the "id" key
28    > if stored_hash is None or stored_hash != hash_pen.hash:
29    >    logger.error(f"Glyph hash mismatch, glyph '{name}' will have no instructions in font.")
30    > else:
31    >    # The hash values are identical, the outline has not changed.
32    >    # Compile the hinting code ...
33    >    pass
34
35    If you want to compare a glyph from a source format which supports floating point
36    coordinates and transformations against a glyph from a format which has restrictions
37    on the precision of floats, e.g. UFO vs. TTF, you must use an appropriate rounding
38    function to make the values comparable. For TTF fonts with composites, this
39    construct can be used to make the transform values conform to F2Dot14:
40
41    > ttf_hash_pen = HashPointPen(ttf_glyph_width, ttFont.getGlyphSet())
42    > ttf_round_pen = RoundingPointPen(ttf_hash_pen, transformRoundFunc=partial(floatToFixedToFloat, precisionBits=14))
43    > ufo_hash_pen = HashPointPen(ufo_glyph.width, ufo)
44    > ttf_glyph.drawPoints(ttf_round_pen, ttFont["glyf"])
45    > ufo_round_pen = RoundingPointPen(ufo_hash_pen, transformRoundFunc=partial(floatToFixedToFloat, precisionBits=14))
46    > ufo_glyph.drawPoints(ufo_round_pen)
47    > assert ttf_hash_pen.hash == ufo_hash_pen.hash
48    """
49
50    def __init__(self, glyphWidth=0, glyphSet=None):
51        self.glyphset = glyphSet
52        self.data = ["w%s" % round(glyphWidth, 9)]
53
54    @property
55    def hash(self):
56        data = "".join(self.data)
57        if len(data) >= 128:
58            data = hashlib.sha512(data.encode("ascii")).hexdigest()
59        return data
60
61    def beginPath(self, identifier=None, **kwargs):
62        pass
63
64    def endPath(self):
65        self.data.append("|")
66
67    def addPoint(
68        self,
69        pt,
70        segmentType=None,
71        smooth=False,
72        name=None,
73        identifier=None,
74        **kwargs,
75    ):
76        if segmentType is None:
77            pt_type = "o"  # offcurve
78        else:
79            pt_type = segmentType[0]
80        self.data.append(f"{pt_type}{pt[0]:g}{pt[1]:+g}")
81
82    def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
83        tr = "".join([f"{t:+}" for t in transformation])
84        self.data.append("[")
85        try:
86            self.glyphset[baseGlyphName].drawPoints(self)
87        except KeyError:
88            raise MissingComponentError(baseGlyphName)
89        self.data.append(f"({tr})]")
90