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:
parent
10413d947a
commit
b9c268943a
73
Lib/fontTools/pens/hashPointPen.py
Normal file
73
Lib/fontTools/pens/hashPointPen.py
Normal 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})]")
|
138
Tests/pens/hashPointPen_test.py
Normal file
138
Tests/pens/hashPointPen_test.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user