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