Created
June 8, 2018 13:36
-
-
Save typesupply/a7416cb867332f0893c9af0d2ba50581 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
This is some very old code that implemented a doctest-like | |
format for .fea files. It was useful, but I seemed to be | |
the only person using it so it fell off my radar. I still | |
think this, or something like it would be extremely useful | |
in the font development workflow. | |
The syntax is defined below. This implementation uses a | |
test file that is separate from the .fea file. It shouldn't | |
be too hard to make it work with tests that are embedded | |
within the .fea. I had that working for something, but I | |
can't find it right now. | |
The font object passed to the FeaTest object was a | |
Compositor font object. | |
-------------- | |
sample.featest | |
-------------- | |
# This is a comment, which shouldn't be visible! | |
# This is a script declaration: | |
^ latn | |
# This is a language declaration: | |
@ None | |
# This is a writing direction start point declaration: | |
* left | |
# the options are "left" for left to right | |
# and "right" for right to left. | |
! This is a note! | |
# Notes are visible in the output. | |
! These are feature state changes: | |
+ liga calt | |
- kern | |
! This is input: | |
> T h i s space i s space a space t e s t exclam | |
! This is input with an expected output as glyph names: | |
> T h i s space i s space a space t e s t exclam | |
< T h i s space i s space a space t e s t exclam | |
! This is input with an expected output as glyph records: | |
> T h i s space i s space a space t e s t exclam | |
< <T 0 0 0 0> <h 0 0 0 0> <i 0 0 0 0> <s 0 0 0 0> <space 0 0 0 0> <i 0 0 0 0> <s 0 0 0 0> <space 0 0 0 0> <a 0 0 0 0> <space 0 0 0 0> <t 0 0 0 0> <e 0 0 0 0> <s 0 0 0 0> <t 0 0 0 0> <exclam 0 0 0 0> | |
! This is a test with a failing expected output: | |
> T h i s space i s space a space t e s t exclam | |
< T h i s space i s space a space f a i l i n g space t e s t exclam | |
""" | |
import os | |
import time | |
import re | |
from compositor.glyphRecord import GlyphRecord | |
class FeaTestError(Exception): pass | |
recordRE = re.compile( | |
"<" # < | |
"\s*" # | |
"([\w._]+)" # glyph name | |
"\s+" # | |
"([\d.-]+)" # x placement | |
"\s+" # | |
"([\d.-]+)" # y placement | |
"\s+" # | |
"([\d.-]+)" # x advance | |
"\s+" # | |
"([\d.-]+)" # y advance | |
"\s*" # | |
">" # > | |
) | |
class FeaTest(object): | |
def __init__(self, font): | |
self.font = font | |
self._results = [] | |
self._runTime = 0 | |
self._testTotal = 0 | |
self._testPass = 0 | |
self._testFail = 0 | |
# --------------- | |
# Test Extraction | |
# --------------- | |
def _parseTestFile(self, path): | |
f = open(path, "r") | |
text = f.read() | |
f.close() | |
parsed = [] | |
for line in text.splitlines(): | |
line = line.strip() | |
if line: | |
token = line[0] | |
content = line[1:].strip() | |
tag = None | |
if not line: | |
continue | |
elif token == "#": | |
continue | |
elif token == "!": | |
tag = "note" | |
elif token == "+": | |
tag = "features on" | |
content = self._spaceSplit(content) | |
elif token == "-": | |
tag = "features off" | |
content = self._spaceSplit(content) | |
elif token == "^": | |
tag = "script" | |
elif token == "@": | |
tag = "language" | |
elif token == "*": | |
tag = "text direction" | |
if content not in ["left", "right"]: | |
raise FeaTestError("Unknown text direction: %s" % content) | |
elif token == ">": | |
tag = "input glyph names" | |
content = self._spaceSplit(content) | |
elif token == "<": | |
tag = "expected result" | |
content = self._parseRecords(content) | |
# XXX alternates! | |
else: | |
raise FeaTestError("Unknown token: %s" % token) | |
parsed.append((tag, content)) | |
return parsed | |
def _parseRecords(self, content): | |
if "<" in content: | |
records = [] | |
while 1: | |
m = recordRE.match(content) | |
if m is None: | |
raise FeaTestError("Invalid glyph record syntax: %s" % content) | |
record = (m.group(1), float(m.group(2)), float(m.group(3)), float(m.group(4)), float(m.group(5))) | |
records.append(record) | |
content = content[m.end():].strip() | |
if not content: | |
break | |
else: | |
records = [(i, None, None, None, None) for i in self._spaceSplit(content)] | |
return records | |
def _spaceSplit(self, text): | |
return [i for i in text.split(" ") if i.strip()] | |
# --------------- | |
# Test Evaluation | |
# --------------- | |
def evaluateTests(self, path): | |
font = self.font | |
test = self._parseTestFile(path) | |
engineChangeTags = [ | |
"features on", | |
"features off", | |
"script", | |
"language", | |
"text direction" | |
] | |
startTime = time.time() | |
currentScript = "latn" | |
currentLanguage = "None" | |
currentDirection = "left" | |
engineState = { | |
"script" : currentScript, | |
"language" : currentLanguage, | |
"text direction" : currentDirection, | |
"feature states" : dict() | |
} | |
haveEngineChange = True | |
# turn all features off | |
for featureName in font.getFeatureList(): | |
font.setFeatureState(featureName, False) | |
engineState["feature states"][featureName] = False | |
# execute | |
results = [] | |
_previousResult = None | |
testTotal = 0 | |
testPass = 0 | |
testFail = 0 | |
for tag, content in test: | |
# as we run through, watch for engine state tags | |
# and as they appear, group them together. | |
if tag in engineChangeTags: | |
haveEngineChange = True | |
if tag == "features on": | |
for feature in content: | |
engineState["feature states"][feature] = True | |
font.setFeatureState(feature, True) | |
elif tag == "features off": | |
for feature in content: | |
engineState["feature states"][feature] = False | |
font.setFeatureState(feature, False) | |
else: | |
if tag == "script": | |
currentScript = content | |
elif tag == "language": | |
currentLanguage = content | |
elif tag == "text direction": | |
currentDirection = content | |
engineState[tag] = content | |
continue | |
# we're exiting a run of engine changes | |
# so log the changes that were made | |
if haveEngineChange: | |
results.append(("engine changes", engineState)) | |
# start a new dict to contain engine changes | |
engineState = {"feature states" : dict()} | |
haveEngineChange = False | |
# input string. convert to glyph records. | |
if tag == "input string": | |
tag = "input" | |
_rawInput = content | |
content = font.stringToGlyphRecords(content) | |
# force record.glyphName to be the requested | |
# glyph name even if the font does not contain | |
# a glyph with that name. | |
for index, glyphName in enumerate(_rawInput): | |
content[index].glyphName = glyphName | |
# input names. convert to glyph records. | |
elif tag == "input glyph names": | |
tag = "input" | |
_rawInput = content | |
content = font.glyphListToGlyphRecords(content) | |
# force record.glyphName to be the requested | |
# glyph name even if the font does not contain | |
# a glyph with that name. | |
for index, glyphName in enumerate(_rawInput): | |
content[index].glyphName = glyphName | |
# expected result. convert to glyph records. | |
elif tag == "expected result": | |
records = [] | |
for glyphName, xP, yP, xA, yA in content: | |
record = GlyphRecord(glyphName) | |
record.xPlacement = xP | |
record.yPlacement = yP | |
record.xAdvance = xA | |
record.yAdvance = yA | |
records.append(record) | |
passed = self._compareResultRecords(_previousResult, records) | |
content = (passed, records) | |
if passed: | |
testPass += 1 | |
else: | |
testFail += 1 | |
# store the tag and the content | |
results.append((tag, content)) | |
# evaluate the test | |
if tag == "input": | |
if currentLanguage is not None and currentLanguage.lower() == "none": | |
currentLanguage = None | |
if currentScript: | |
if len(currentScript) < 4: | |
currentScript += " " * (4 - len(currentScript)) | |
if currentLanguage: | |
if len(currentLanguage) < 4: | |
currentLanguage += " " * (4 - len(currentLanguage)) | |
result = _previousResult = font.process(_rawInput, script=currentScript, langSys=currentLanguage, rightToLeft=currentDirection != "left") | |
results.append(("result", result)) | |
testTotal += 1 | |
runTime = round(time.time() - startTime, 3) | |
if runTime < 0: | |
runTime = 0 | |
self._runTime += runTime | |
self._testTotal += testTotal | |
self._testPass += testPass | |
self._testFail += testFail | |
self._results.extend(results) | |
def _compareResultRecords(self, results1, results2): | |
if len(results1) != len (results2): | |
return False | |
comparison = [] | |
for index, r1 in enumerate(results1): | |
r2 = results2[index] | |
if r1.glyphName != r2.glyphName: | |
comparison.append(False) | |
continue | |
if r2.xPlacement is not None: | |
if r1.xPlacement != r2.xPlacement: | |
comparison.append(False) | |
continue | |
if r2.yPlacement is not None: | |
if r1.yPlacement != r2.yPlacement: | |
comparison.append(False) | |
continue | |
if r2.xAdvance is not None: | |
if r1.xAdvance != r2.xAdvance: | |
comparison.append(False) | |
continue | |
if r2.yAdvance is not None: | |
if r1.yAdvance != r2.yAdvance: | |
comparison.append(False) | |
continue | |
return False not in comparison | |
# ------------- | |
# Result Export | |
# ------------- | |
def exportResults(self): | |
from cStringIO import StringIO | |
from xmlWriter import XMLWriter | |
f = StringIO() | |
writer = XMLWriter(f, encoding="utf-8") | |
writer.begintag("xml") | |
writer.newline() | |
writer.begintag("statistics") | |
writer.newline() | |
writer.simpletag("runTime", value="%s seconds" % str(round(self._runTime, 3))) | |
writer.newline() | |
writer.simpletag("totalTests", value=self._testTotal) | |
writer.newline() | |
writer.simpletag("passedTests", value=self._testPass) | |
writer.newline() | |
writer.simpletag("failedTests", value=self._testFail) | |
writer.newline() | |
writer.endtag("statistics") | |
writer.newline() | |
for tag, data in self._segregateResults(): | |
if tag == "engine changes": | |
writer.begintag("engineChanges") | |
writer.newline() | |
if "script" in data: | |
writer.simpletag("script", value=data["script"]) | |
writer.newline() | |
if "language" in data: | |
writer.simpletag("language", value=data["language"]) | |
writer.newline() | |
if "text direction" in data: | |
if data["text direction"] == "left": | |
t = "left to right" | |
else: | |
t = "right to left" | |
writer.simpletag("textDirection", value=t) | |
writer.newline() | |
if "feature states" in data: | |
onState = [] | |
offState = [] | |
for featureTag, state in sorted(data["feature states"].items()): | |
if state: | |
onState.append(featureTag) | |
else: | |
offState.append(featureTag) | |
writer.begintag("featureStates") | |
writer.newline() | |
writer.begintag("featuresOn") | |
writer.newline() | |
for featureTag in onState: | |
writer.simpletag("feature", name=featureTag) | |
writer.newline() | |
writer.endtag("featuresOn") | |
writer.newline() | |
writer.begintag("featuresOff") | |
writer.newline() | |
for featureTag in offState: | |
writer.simpletag("feature", name=featureTag) | |
writer.newline() | |
writer.endtag("featuresOff") | |
writer.newline() | |
writer.endtag("featureStates") | |
writer.newline() | |
writer.endtag("engineChanges") | |
writer.newline() | |
elif tag == "test": | |
writer.begintag("test") | |
writer.newline() | |
for subTag, subData in data: | |
if subTag == "input": | |
writer.begintag("input") | |
writer.newline() | |
elif subTag == "result": | |
writer.begintag("result") | |
writer.newline() | |
elif subTag == "expected result": | |
writer.begintag("expectedResult", passed=subData[0]) | |
writer.newline() | |
subData = subData[1] | |
for glyphRecord in subData: | |
xP = glyphRecord.xPlacement | |
if xP is not None: | |
xP = int(xP) | |
yP = glyphRecord.yPlacement | |
if yP is not None: | |
yP = int(yP) | |
xA = glyphRecord.xAdvance | |
if xA is not None: | |
xA = int(xA) | |
yA = glyphRecord.yAdvance | |
if yA is not None: | |
yA = int(yA) | |
writer.simpletag("glyphRecord", name=glyphRecord.glyphName, | |
xPlacement=xP, yPlacement=yP, xAdvance=xA, yAdvance=yA) | |
writer.newline() | |
if subTag == "input": | |
writer.endtag("input") | |
writer.newline() | |
elif subTag == "result": | |
writer.endtag("result") | |
writer.newline() | |
elif subTag == "expected result": | |
writer.endtag("expectedResult") | |
writer.newline() | |
writer.endtag("test") | |
writer.newline() | |
else: | |
writer.simpletag("note", text=data) | |
writer.newline() | |
writer.endtag("xml") | |
return f.getvalue() | |
def _segregateResults(self): | |
results = self._results | |
clumps = [] | |
# the stats a already clumped | |
clumps.append(results[0]) | |
currentClump = None | |
for tag, content in results[1:]: | |
# engine changes are already clumped | |
if tag == "engine changes": | |
clumps.append((tag, content)) | |
currentClump = None | |
# notes can't exist within a test run | |
# so they always get appened. this | |
# will force them to appear at the | |
# end of teh test run. | |
elif tag == "note": | |
clumps.append((tag, content)) | |
# clump the test run | |
else: | |
if tag == "input": | |
currentClump = [] | |
clumps.append(("test", currentClump)) | |
currentClump.append((tag, content)) | |
return clumps | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment