Add HashPointPen from psautohint (#2005)

* Add HashPointPen from psautohint (with changes)
* Decompose components
* Use format string and disambiguate critical changes
* Remove "getHash()" in favour of "hash" property
* Add transformation for composites
* Omit base glyph name from component hash
* Use untransformed coords for components
* Add tests
* Add example code

Co-authored-by: Cosimo Lupo <cosimo@anthrotype.com>
This commit is contained in:
Jens Kutilek 2020-10-22 11:16:24 +02:00 committed by GitHub
parent 10413d947a
commit b9c268943a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 211 additions and 0 deletions

View File

@ -0,0 +1,73 @@
# Modified from https://github.com/adobe-type-tools/psautohint/blob/08b346865710ed3c172f1eb581d6ef243b203f99/python/psautohint/ufoFont.py#L800-L838
import hashlib
from fontTools.pens.pointPen import AbstractPointPen
class HashPointPen(AbstractPointPen):
"""
This pen can be used to check if a glyph's contents (outlines plus
components) have changed.
Components are added as the original outline plus each composite's
transformation.
Example: You have some TrueType hinting code for a glyph which you want to
compile. The hinting code specifies a hash value computed with HashPointPen
that was valid for the glyph's outlines at the time the hinting code was
written. Now you can calculate the hash for the glyph's current outlines to
check if the outlines have changed, which would probably make the hinting
code invalid.
> glyph = ufo[name]
> hash_pen = HashPointPen(glyph.width, ufo)
> glyph.drawPoints(hash_pen)
> ttdata = glyph.lib.get("public.truetype.instructions", None)
> stored_hash = ttdata.get("id", None) # The hash is stored in the "id" key
> if stored_hash is None or stored_hash != hash_pen.hash:
> logger.error(f"Glyph hash mismatch, glyph '{name}' will have no instructions in font.")
> else:
> # The hash values are identical, the outline has not changed.
> # Compile the hinting code ...
> pass
"""
def __init__(self, glyphWidth=0, glyphSet=None):
self.glyphset = glyphSet
self.data = ["w%s" % round(glyphWidth, 9)]
@property
def hash(self):
data = "".join(self.data)
if len(data) >= 128:
data = hashlib.sha512(data.encode("ascii")).hexdigest()
return data
def beginPath(self, identifier=None, **kwargs):
pass
def endPath(self):
self.data.append("|")
def addPoint(
self,
pt,
segmentType=None,
smooth=False,
name=None,
identifier=None,
**kwargs,
):
if segmentType is None:
pt_type = "o" # offcurve
else:
pt_type = segmentType[0]
self.data.append(f"{pt_type}{pt[0]:g}{pt[1]:+g}")
def addComponent(
self, baseGlyphName, transformation, identifier=None, **kwargs
):
tr = "".join([f"{t:+}" for t in transformation])
self.data.append("[")
self.glyphset[baseGlyphName].drawPoints(self)
self.data.append(f"({tr})]")

View File

@ -0,0 +1,138 @@
from fontTools.misc.transform import Identity
from fontTools.pens.hashPointPen import HashPointPen
import pytest
class _TestGlyph(object):
width = 500
def drawPoints(self, pen):
pen.beginPath(identifier="abc")
pen.addPoint((0.0, 0.0), "line", False, "start", identifier="0000")
pen.addPoint((10, 110), "line", False, None, identifier="0001")
pen.addPoint((50.0, 75.0), None, False, None, identifier="0002")
pen.addPoint((60.0, 50.0), None, False, None, identifier="0003")
pen.addPoint((50.0, 0.0), "curve", True, "last", identifier="0004")
pen.endPath()
class _TestGlyph2(_TestGlyph):
def drawPoints(self, pen):
pen.beginPath(identifier="abc")
pen.addPoint((0.0, 0.0), "line", False, "start", identifier="0000")
# Minor difference to _TestGlyph() is in the next line:
pen.addPoint((101, 10), "line", False, None, identifier="0001")
pen.addPoint((50.0, 75.0), None, False, None, identifier="0002")
pen.addPoint((60.0, 50.0), None, False, None, identifier="0003")
pen.addPoint((50.0, 0.0), "curve", True, "last", identifier="0004")
pen.endPath()
class _TestGlyph3(_TestGlyph):
def drawPoints(self, pen):
pen.beginPath(identifier="abc")
pen.addPoint((0.0, 0.0), "line", False, "start", identifier="0000")
pen.addPoint((10, 110), "line", False, None, identifier="0001")
pen.endPath()
# Same segment, but in a different path:
pen.beginPath(identifier="pth2")
pen.addPoint((50.0, 75.0), None, False, None, identifier="0002")
pen.addPoint((60.0, 50.0), None, False, None, identifier="0003")
pen.addPoint((50.0, 0.0), "curve", True, "last", identifier="0004")
pen.endPath()
class _TestGlyph4(_TestGlyph):
def drawPoints(self, pen):
pen.beginPath(identifier="abc")
pen.addPoint((0.0, 0.0), "move", False, "start", identifier="0000")
pen.addPoint((10, 110), "line", False, None, identifier="0001")
pen.addPoint((50.0, 75.0), None, False, None, identifier="0002")
pen.addPoint((60.0, 50.0), None, False, None, identifier="0003")
pen.addPoint((50.0, 0.0), "curve", True, "last", identifier="0004")
pen.endPath()
class _TestGlyph5(_TestGlyph):
def drawPoints(self, pen):
pen.addComponent("b", Identity)
class HashPointPenTest(object):
def test_addComponent(self):
pen = HashPointPen(_TestGlyph().width, {"a": _TestGlyph()})
pen.addComponent("a", (2, 0, 0, 3, -10, 5))
assert pen.hash == "w500[l0+0l10+110o50+75o60+50c50+0|(+2+0+0+3-10+5)]"
def test_NestedComponents(self):
pen = HashPointPen(
_TestGlyph().width, {"a": _TestGlyph5(), "b": _TestGlyph()}
) # "a" contains "b" as a component
pen.addComponent("a", (2, 0, 0, 3, -10, 5))
assert (
pen.hash
== "w500[[l0+0l10+110o50+75o60+50c50+0|(+1+0+0+1+0+0)](+2+0+0+3-10+5)]"
)
def test_outlineAndComponent(self):
pen = HashPointPen(_TestGlyph().width, {"a": _TestGlyph()})
glyph = _TestGlyph()
glyph.drawPoints(pen)
pen.addComponent("a", (2, 0, 0, 2, -10, 5))
assert (
pen.hash
== "w500l0+0l10+110o50+75o60+50c50+0|[l0+0l10+110o50+75o60+50c50+0|(+2+0+0+2-10+5)]"
)
def test_addComponent_missing_raises(self):
pen = HashPointPen(_TestGlyph().width, dict())
with pytest.raises(KeyError) as excinfo:
pen.addComponent("a", Identity)
assert excinfo.value.args[0] == "a"
def test_similarGlyphs(self):
pen = HashPointPen(_TestGlyph().width)
glyph = _TestGlyph()
glyph.drawPoints(pen)
pen2 = HashPointPen(_TestGlyph2().width)
glyph = _TestGlyph2()
glyph.drawPoints(pen2)
assert pen.hash != pen2.hash
def test_similarGlyphs2(self):
pen = HashPointPen(_TestGlyph().width)
glyph = _TestGlyph()
glyph.drawPoints(pen)
pen2 = HashPointPen(_TestGlyph3().width)
glyph = _TestGlyph3()
glyph.drawPoints(pen2)
assert pen.hash != pen2.hash
def test_similarGlyphs3(self):
pen = HashPointPen(_TestGlyph().width)
glyph = _TestGlyph()
glyph.drawPoints(pen)
pen2 = HashPointPen(_TestGlyph4().width)
glyph = _TestGlyph4()
glyph.drawPoints(pen2)
assert pen.hash != pen2.hash
def test_glyphVsComposite(self):
# If a glyph contains a component, the decomposed glyph should still
# compare false
pen = HashPointPen(_TestGlyph().width, {"a": _TestGlyph()})
pen.addComponent("a", Identity)
pen2 = HashPointPen(_TestGlyph().width)
glyph = _TestGlyph()
glyph.drawPoints(pen2)
assert pen.hash != pen2.hash