You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1467 lines
56 KiB
1467 lines
56 KiB
import io
|
|
import struct
|
|
from fontTools.misc.fixedTools import floatToFixed
|
|
from fontTools.misc.testTools import getXML
|
|
from fontTools.otlLib import builder, error
|
|
from fontTools import ttLib
|
|
from fontTools.ttLib.tables import otTables
|
|
import pytest
|
|
|
|
|
|
class BuilderTest(object):
|
|
GLYPHS = (
|
|
".notdef space zero one two three four five six "
|
|
"A B C a b c grave acute cedilla f_f_i f_i c_t"
|
|
).split()
|
|
GLYPHMAP = {name: num for num, name in enumerate(GLYPHS)}
|
|
|
|
ANCHOR1 = builder.buildAnchor(11, -11)
|
|
ANCHOR2 = builder.buildAnchor(22, -22)
|
|
ANCHOR3 = builder.buildAnchor(33, -33)
|
|
|
|
def test_buildAnchor_format1(self):
|
|
anchor = builder.buildAnchor(23, 42)
|
|
assert getXML(anchor.toXML) == [
|
|
'<Anchor Format="1">',
|
|
' <XCoordinate value="23"/>',
|
|
' <YCoordinate value="42"/>',
|
|
"</Anchor>",
|
|
]
|
|
|
|
def test_buildAnchor_format2(self):
|
|
anchor = builder.buildAnchor(23, 42, point=17)
|
|
assert getXML(anchor.toXML) == [
|
|
'<Anchor Format="2">',
|
|
' <XCoordinate value="23"/>',
|
|
' <YCoordinate value="42"/>',
|
|
' <AnchorPoint value="17"/>',
|
|
"</Anchor>",
|
|
]
|
|
|
|
def test_buildAnchor_format3(self):
|
|
anchor = builder.buildAnchor(
|
|
23,
|
|
42,
|
|
deviceX=builder.buildDevice({1: 1, 0: 0}),
|
|
deviceY=builder.buildDevice({7: 7}),
|
|
)
|
|
assert getXML(anchor.toXML) == [
|
|
'<Anchor Format="3">',
|
|
' <XCoordinate value="23"/>',
|
|
' <YCoordinate value="42"/>',
|
|
" <XDeviceTable>",
|
|
' <StartSize value="0"/>',
|
|
' <EndSize value="1"/>',
|
|
' <DeltaFormat value="1"/>',
|
|
' <DeltaValue value="[0, 1]"/>',
|
|
" </XDeviceTable>",
|
|
" <YDeviceTable>",
|
|
' <StartSize value="7"/>',
|
|
' <EndSize value="7"/>',
|
|
' <DeltaFormat value="2"/>',
|
|
' <DeltaValue value="[7]"/>',
|
|
" </YDeviceTable>",
|
|
"</Anchor>",
|
|
]
|
|
|
|
def test_buildAttachList(self):
|
|
attachList = builder.buildAttachList(
|
|
{"zero": [23, 7], "one": [1]}, self.GLYPHMAP
|
|
)
|
|
assert getXML(attachList.toXML) == [
|
|
"<AttachList>",
|
|
" <Coverage>",
|
|
' <Glyph value="zero"/>',
|
|
' <Glyph value="one"/>',
|
|
" </Coverage>",
|
|
" <!-- GlyphCount=2 -->",
|
|
' <AttachPoint index="0">',
|
|
" <!-- PointCount=2 -->",
|
|
' <PointIndex index="0" value="7"/>',
|
|
' <PointIndex index="1" value="23"/>',
|
|
" </AttachPoint>",
|
|
' <AttachPoint index="1">',
|
|
" <!-- PointCount=1 -->",
|
|
' <PointIndex index="0" value="1"/>',
|
|
" </AttachPoint>",
|
|
"</AttachList>",
|
|
]
|
|
|
|
def test_buildAttachList_empty(self):
|
|
assert builder.buildAttachList({}, self.GLYPHMAP) is None
|
|
|
|
def test_buildAttachPoint(self):
|
|
attachPoint = builder.buildAttachPoint([7, 3])
|
|
assert getXML(attachPoint.toXML) == [
|
|
"<AttachPoint>",
|
|
" <!-- PointCount=2 -->",
|
|
' <PointIndex index="0" value="3"/>',
|
|
' <PointIndex index="1" value="7"/>',
|
|
"</AttachPoint>",
|
|
]
|
|
|
|
def test_buildAttachPoint_empty(self):
|
|
assert builder.buildAttachPoint([]) is None
|
|
|
|
def test_buildAttachPoint_duplicate(self):
|
|
attachPoint = builder.buildAttachPoint([7, 3, 7])
|
|
assert getXML(attachPoint.toXML) == [
|
|
"<AttachPoint>",
|
|
" <!-- PointCount=2 -->",
|
|
' <PointIndex index="0" value="3"/>',
|
|
' <PointIndex index="1" value="7"/>',
|
|
"</AttachPoint>",
|
|
]
|
|
|
|
def test_buildBaseArray(self):
|
|
anchor = builder.buildAnchor
|
|
baseArray = builder.buildBaseArray(
|
|
{"a": {2: anchor(300, 80)}, "c": {1: anchor(300, 80), 2: anchor(300, -20)}},
|
|
numMarkClasses=4,
|
|
glyphMap=self.GLYPHMAP,
|
|
)
|
|
assert getXML(baseArray.toXML) == [
|
|
"<BaseArray>",
|
|
" <!-- BaseCount=2 -->",
|
|
' <BaseRecord index="0">',
|
|
' <BaseAnchor index="0" empty="1"/>',
|
|
' <BaseAnchor index="1" empty="1"/>',
|
|
' <BaseAnchor index="2" Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="80"/>',
|
|
" </BaseAnchor>",
|
|
' <BaseAnchor index="3" empty="1"/>',
|
|
" </BaseRecord>",
|
|
' <BaseRecord index="1">',
|
|
' <BaseAnchor index="0" empty="1"/>',
|
|
' <BaseAnchor index="1" Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="80"/>',
|
|
" </BaseAnchor>",
|
|
' <BaseAnchor index="2" Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="-20"/>',
|
|
" </BaseAnchor>",
|
|
' <BaseAnchor index="3" empty="1"/>',
|
|
" </BaseRecord>",
|
|
"</BaseArray>",
|
|
]
|
|
|
|
def test_buildBaseRecord(self):
|
|
a = builder.buildAnchor
|
|
rec = builder.buildBaseRecord([a(500, -20), None, a(300, -15)])
|
|
assert getXML(rec.toXML) == [
|
|
"<BaseRecord>",
|
|
' <BaseAnchor index="0" Format="1">',
|
|
' <XCoordinate value="500"/>',
|
|
' <YCoordinate value="-20"/>',
|
|
" </BaseAnchor>",
|
|
' <BaseAnchor index="1" empty="1"/>',
|
|
' <BaseAnchor index="2" Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="-15"/>',
|
|
" </BaseAnchor>",
|
|
"</BaseRecord>",
|
|
]
|
|
|
|
def test_buildCaretValueForCoord(self):
|
|
caret = builder.buildCaretValueForCoord(500)
|
|
assert getXML(caret.toXML) == [
|
|
'<CaretValue Format="1">',
|
|
' <Coordinate value="500"/>',
|
|
"</CaretValue>",
|
|
]
|
|
|
|
def test_buildCaretValueForPoint(self):
|
|
caret = builder.buildCaretValueForPoint(23)
|
|
assert getXML(caret.toXML) == [
|
|
'<CaretValue Format="2">',
|
|
' <CaretValuePoint value="23"/>',
|
|
"</CaretValue>",
|
|
]
|
|
|
|
def test_buildComponentRecord(self):
|
|
a = builder.buildAnchor
|
|
rec = builder.buildComponentRecord([a(500, -20), None, a(300, -15)])
|
|
assert getXML(rec.toXML) == [
|
|
"<ComponentRecord>",
|
|
' <LigatureAnchor index="0" Format="1">',
|
|
' <XCoordinate value="500"/>',
|
|
' <YCoordinate value="-20"/>',
|
|
" </LigatureAnchor>",
|
|
' <LigatureAnchor index="1" empty="1"/>',
|
|
' <LigatureAnchor index="2" Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="-15"/>',
|
|
" </LigatureAnchor>",
|
|
"</ComponentRecord>",
|
|
]
|
|
|
|
def test_buildComponentRecord_empty(self):
|
|
assert builder.buildComponentRecord([]) is None
|
|
|
|
def test_buildComponentRecord_None(self):
|
|
assert builder.buildComponentRecord(None) is None
|
|
|
|
def test_buildCoverage(self):
|
|
cov = builder.buildCoverage({"two", "four"}, {"two": 2, "four": 4})
|
|
assert getXML(cov.toXML) == [
|
|
"<Coverage>",
|
|
' <Glyph value="two"/>',
|
|
' <Glyph value="four"/>',
|
|
"</Coverage>",
|
|
]
|
|
|
|
def test_buildCursivePos(self):
|
|
pos = builder.buildCursivePosSubtable(
|
|
{"two": (self.ANCHOR1, self.ANCHOR2), "four": (self.ANCHOR3, self.ANCHOR1)},
|
|
self.GLYPHMAP,
|
|
)
|
|
assert getXML(pos.toXML) == [
|
|
'<CursivePos Format="1">',
|
|
" <Coverage>",
|
|
' <Glyph value="two"/>',
|
|
' <Glyph value="four"/>',
|
|
" </Coverage>",
|
|
" <!-- EntryExitCount=2 -->",
|
|
' <EntryExitRecord index="0">',
|
|
' <EntryAnchor Format="1">',
|
|
' <XCoordinate value="11"/>',
|
|
' <YCoordinate value="-11"/>',
|
|
" </EntryAnchor>",
|
|
' <ExitAnchor Format="1">',
|
|
' <XCoordinate value="22"/>',
|
|
' <YCoordinate value="-22"/>',
|
|
" </ExitAnchor>",
|
|
" </EntryExitRecord>",
|
|
' <EntryExitRecord index="1">',
|
|
' <EntryAnchor Format="1">',
|
|
' <XCoordinate value="33"/>',
|
|
' <YCoordinate value="-33"/>',
|
|
" </EntryAnchor>",
|
|
' <ExitAnchor Format="1">',
|
|
' <XCoordinate value="11"/>',
|
|
' <YCoordinate value="-11"/>',
|
|
" </ExitAnchor>",
|
|
" </EntryExitRecord>",
|
|
"</CursivePos>",
|
|
]
|
|
|
|
def test_buildDevice_format1(self):
|
|
device = builder.buildDevice({1: 1, 0: 0})
|
|
assert getXML(device.toXML) == [
|
|
"<Device>",
|
|
' <StartSize value="0"/>',
|
|
' <EndSize value="1"/>',
|
|
' <DeltaFormat value="1"/>',
|
|
' <DeltaValue value="[0, 1]"/>',
|
|
"</Device>",
|
|
]
|
|
|
|
def test_buildDevice_format2(self):
|
|
device = builder.buildDevice({2: 2, 0: 1, 1: 0})
|
|
assert getXML(device.toXML) == [
|
|
"<Device>",
|
|
' <StartSize value="0"/>',
|
|
' <EndSize value="2"/>',
|
|
' <DeltaFormat value="2"/>',
|
|
' <DeltaValue value="[1, 0, 2]"/>',
|
|
"</Device>",
|
|
]
|
|
|
|
def test_buildDevice_format3(self):
|
|
device = builder.buildDevice({5: 3, 1: 77})
|
|
assert getXML(device.toXML) == [
|
|
"<Device>",
|
|
' <StartSize value="1"/>',
|
|
' <EndSize value="5"/>',
|
|
' <DeltaFormat value="3"/>',
|
|
' <DeltaValue value="[77, 0, 0, 0, 3]"/>',
|
|
"</Device>",
|
|
]
|
|
|
|
def test_buildLigatureArray(self):
|
|
anchor = builder.buildAnchor
|
|
ligatureArray = builder.buildLigatureArray(
|
|
{
|
|
"f_i": [{2: anchor(300, -20)}, {}],
|
|
"c_t": [{}, {1: anchor(500, 350), 2: anchor(1300, -20)}],
|
|
},
|
|
numMarkClasses=4,
|
|
glyphMap=self.GLYPHMAP,
|
|
)
|
|
assert getXML(ligatureArray.toXML) == [
|
|
"<LigatureArray>",
|
|
" <!-- LigatureCount=2 -->",
|
|
' <LigatureAttach index="0">', # f_i
|
|
" <!-- ComponentCount=2 -->",
|
|
' <ComponentRecord index="0">',
|
|
' <LigatureAnchor index="0" empty="1"/>',
|
|
' <LigatureAnchor index="1" empty="1"/>',
|
|
' <LigatureAnchor index="2" Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="-20"/>',
|
|
" </LigatureAnchor>",
|
|
' <LigatureAnchor index="3" empty="1"/>',
|
|
" </ComponentRecord>",
|
|
' <ComponentRecord index="1">',
|
|
' <LigatureAnchor index="0" empty="1"/>',
|
|
' <LigatureAnchor index="1" empty="1"/>',
|
|
' <LigatureAnchor index="2" empty="1"/>',
|
|
' <LigatureAnchor index="3" empty="1"/>',
|
|
" </ComponentRecord>",
|
|
" </LigatureAttach>",
|
|
' <LigatureAttach index="1">',
|
|
" <!-- ComponentCount=2 -->",
|
|
' <ComponentRecord index="0">',
|
|
' <LigatureAnchor index="0" empty="1"/>',
|
|
' <LigatureAnchor index="1" empty="1"/>',
|
|
' <LigatureAnchor index="2" empty="1"/>',
|
|
' <LigatureAnchor index="3" empty="1"/>',
|
|
" </ComponentRecord>",
|
|
' <ComponentRecord index="1">',
|
|
' <LigatureAnchor index="0" empty="1"/>',
|
|
' <LigatureAnchor index="1" Format="1">',
|
|
' <XCoordinate value="500"/>',
|
|
' <YCoordinate value="350"/>',
|
|
" </LigatureAnchor>",
|
|
' <LigatureAnchor index="2" Format="1">',
|
|
' <XCoordinate value="1300"/>',
|
|
' <YCoordinate value="-20"/>',
|
|
" </LigatureAnchor>",
|
|
' <LigatureAnchor index="3" empty="1"/>',
|
|
" </ComponentRecord>",
|
|
" </LigatureAttach>",
|
|
"</LigatureArray>",
|
|
]
|
|
|
|
def test_buildLigatureAttach(self):
|
|
anchor = builder.buildAnchor
|
|
attach = builder.buildLigatureAttach(
|
|
[[anchor(500, -10), None], [None, anchor(300, -20), None]]
|
|
)
|
|
assert getXML(attach.toXML) == [
|
|
"<LigatureAttach>",
|
|
" <!-- ComponentCount=2 -->",
|
|
' <ComponentRecord index="0">',
|
|
' <LigatureAnchor index="0" Format="1">',
|
|
' <XCoordinate value="500"/>',
|
|
' <YCoordinate value="-10"/>',
|
|
" </LigatureAnchor>",
|
|
' <LigatureAnchor index="1" empty="1"/>',
|
|
" </ComponentRecord>",
|
|
' <ComponentRecord index="1">',
|
|
' <LigatureAnchor index="0" empty="1"/>',
|
|
' <LigatureAnchor index="1" Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="-20"/>',
|
|
" </LigatureAnchor>",
|
|
' <LigatureAnchor index="2" empty="1"/>',
|
|
" </ComponentRecord>",
|
|
"</LigatureAttach>",
|
|
]
|
|
|
|
def test_buildLigatureAttach_emptyComponents(self):
|
|
attach = builder.buildLigatureAttach([[], None])
|
|
assert getXML(attach.toXML) == [
|
|
"<LigatureAttach>",
|
|
" <!-- ComponentCount=2 -->",
|
|
' <ComponentRecord index="0" empty="1"/>',
|
|
' <ComponentRecord index="1" empty="1"/>',
|
|
"</LigatureAttach>",
|
|
]
|
|
|
|
def test_buildLigatureAttach_noComponents(self):
|
|
attach = builder.buildLigatureAttach([])
|
|
assert getXML(attach.toXML) == [
|
|
"<LigatureAttach>",
|
|
" <!-- ComponentCount=0 -->",
|
|
"</LigatureAttach>",
|
|
]
|
|
|
|
def test_buildLigCaretList(self):
|
|
carets = builder.buildLigCaretList(
|
|
{"f_f_i": [300, 600]}, {"c_t": [42]}, self.GLYPHMAP
|
|
)
|
|
assert getXML(carets.toXML) == [
|
|
"<LigCaretList>",
|
|
" <Coverage>",
|
|
' <Glyph value="f_f_i"/>',
|
|
' <Glyph value="c_t"/>',
|
|
" </Coverage>",
|
|
" <!-- LigGlyphCount=2 -->",
|
|
' <LigGlyph index="0">',
|
|
" <!-- CaretCount=2 -->",
|
|
' <CaretValue index="0" Format="1">',
|
|
' <Coordinate value="300"/>',
|
|
" </CaretValue>",
|
|
' <CaretValue index="1" Format="1">',
|
|
' <Coordinate value="600"/>',
|
|
" </CaretValue>",
|
|
" </LigGlyph>",
|
|
' <LigGlyph index="1">',
|
|
" <!-- CaretCount=1 -->",
|
|
' <CaretValue index="0" Format="2">',
|
|
' <CaretValuePoint value="42"/>',
|
|
" </CaretValue>",
|
|
" </LigGlyph>",
|
|
"</LigCaretList>",
|
|
]
|
|
|
|
def test_buildLigCaretList_bothCoordsAndPointsForSameGlyph(self):
|
|
carets = builder.buildLigCaretList(
|
|
{"f_f_i": [300]}, {"f_f_i": [7]}, self.GLYPHMAP
|
|
)
|
|
assert getXML(carets.toXML) == [
|
|
"<LigCaretList>",
|
|
" <Coverage>",
|
|
' <Glyph value="f_f_i"/>',
|
|
" </Coverage>",
|
|
" <!-- LigGlyphCount=1 -->",
|
|
' <LigGlyph index="0">',
|
|
" <!-- CaretCount=2 -->",
|
|
' <CaretValue index="0" Format="1">',
|
|
' <Coordinate value="300"/>',
|
|
" </CaretValue>",
|
|
' <CaretValue index="1" Format="2">',
|
|
' <CaretValuePoint value="7"/>',
|
|
" </CaretValue>",
|
|
" </LigGlyph>",
|
|
"</LigCaretList>",
|
|
]
|
|
|
|
def test_buildLigCaretList_empty(self):
|
|
assert builder.buildLigCaretList({}, {}, self.GLYPHMAP) is None
|
|
|
|
def test_buildLigCaretList_None(self):
|
|
assert builder.buildLigCaretList(None, None, self.GLYPHMAP) is None
|
|
|
|
def test_buildLigGlyph_coords(self):
|
|
lig = builder.buildLigGlyph([500, 800], None)
|
|
assert getXML(lig.toXML) == [
|
|
"<LigGlyph>",
|
|
" <!-- CaretCount=2 -->",
|
|
' <CaretValue index="0" Format="1">',
|
|
' <Coordinate value="500"/>',
|
|
" </CaretValue>",
|
|
' <CaretValue index="1" Format="1">',
|
|
' <Coordinate value="800"/>',
|
|
" </CaretValue>",
|
|
"</LigGlyph>",
|
|
]
|
|
|
|
def test_buildLigGlyph_empty(self):
|
|
assert builder.buildLigGlyph([], []) is None
|
|
|
|
def test_buildLigGlyph_None(self):
|
|
assert builder.buildLigGlyph(None, None) is None
|
|
|
|
def test_buildLigGlyph_points(self):
|
|
lig = builder.buildLigGlyph(None, [2])
|
|
assert getXML(lig.toXML) == [
|
|
"<LigGlyph>",
|
|
" <!-- CaretCount=1 -->",
|
|
' <CaretValue index="0" Format="2">',
|
|
' <CaretValuePoint value="2"/>',
|
|
" </CaretValue>",
|
|
"</LigGlyph>",
|
|
]
|
|
|
|
def test_buildLookup(self):
|
|
s1 = builder.buildSingleSubstSubtable({"one": "two"})
|
|
s2 = builder.buildSingleSubstSubtable({"three": "four"})
|
|
lookup = builder.buildLookup([s1, s2], flags=7)
|
|
assert getXML(lookup.toXML) == [
|
|
"<Lookup>",
|
|
' <LookupType value="1"/>',
|
|
' <LookupFlag value="7"/><!-- rightToLeft ignoreBaseGlyphs ignoreLigatures -->',
|
|
" <!-- SubTableCount=2 -->",
|
|
' <SingleSubst index="0">',
|
|
' <Substitution in="one" out="two"/>',
|
|
" </SingleSubst>",
|
|
' <SingleSubst index="1">',
|
|
' <Substitution in="three" out="four"/>',
|
|
" </SingleSubst>",
|
|
"</Lookup>",
|
|
]
|
|
|
|
def test_buildLookup_badFlags(self):
|
|
s = builder.buildSingleSubstSubtable({"one": "two"})
|
|
with pytest.raises(
|
|
AssertionError,
|
|
match=(
|
|
"if markFilterSet is None, flags must not set "
|
|
"LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x0010"
|
|
),
|
|
) as excinfo:
|
|
builder.buildLookup([s], builder.LOOKUP_FLAG_USE_MARK_FILTERING_SET, None)
|
|
|
|
def test_buildLookup_conflictingSubtableTypes(self):
|
|
s1 = builder.buildSingleSubstSubtable({"one": "two"})
|
|
s2 = builder.buildAlternateSubstSubtable({"one": ["two", "three"]})
|
|
with pytest.raises(
|
|
AssertionError, match="all subtables must have the same LookupType"
|
|
) as excinfo:
|
|
builder.buildLookup([s1, s2])
|
|
|
|
def test_buildLookup_noSubtables(self):
|
|
assert builder.buildLookup([]) is None
|
|
assert builder.buildLookup(None) is None
|
|
assert builder.buildLookup([None]) is None
|
|
assert builder.buildLookup([None, None]) is None
|
|
|
|
def test_buildLookup_markFilterSet(self):
|
|
s = builder.buildSingleSubstSubtable({"one": "two"})
|
|
flags = (
|
|
builder.LOOKUP_FLAG_RIGHT_TO_LEFT
|
|
| builder.LOOKUP_FLAG_USE_MARK_FILTERING_SET
|
|
)
|
|
lookup = builder.buildLookup([s], flags, markFilterSet=999)
|
|
assert getXML(lookup.toXML) == [
|
|
"<Lookup>",
|
|
' <LookupType value="1"/>',
|
|
' <LookupFlag value="17"/><!-- rightToLeft useMarkFilteringSet -->',
|
|
" <!-- SubTableCount=1 -->",
|
|
' <SingleSubst index="0">',
|
|
' <Substitution in="one" out="two"/>',
|
|
" </SingleSubst>",
|
|
' <MarkFilteringSet value="999"/>',
|
|
"</Lookup>",
|
|
]
|
|
|
|
def test_buildMarkArray(self):
|
|
markArray = builder.buildMarkArray(
|
|
{
|
|
"acute": (7, builder.buildAnchor(300, 800)),
|
|
"grave": (2, builder.buildAnchor(10, 80)),
|
|
},
|
|
self.GLYPHMAP,
|
|
)
|
|
assert self.GLYPHMAP["grave"] < self.GLYPHMAP["acute"]
|
|
assert getXML(markArray.toXML) == [
|
|
"<MarkArray>",
|
|
" <!-- MarkCount=2 -->",
|
|
' <MarkRecord index="0">',
|
|
' <Class value="2"/>',
|
|
' <MarkAnchor Format="1">',
|
|
' <XCoordinate value="10"/>',
|
|
' <YCoordinate value="80"/>',
|
|
" </MarkAnchor>",
|
|
" </MarkRecord>",
|
|
' <MarkRecord index="1">',
|
|
' <Class value="7"/>',
|
|
' <MarkAnchor Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="800"/>',
|
|
" </MarkAnchor>",
|
|
" </MarkRecord>",
|
|
"</MarkArray>",
|
|
]
|
|
|
|
def test_buildMarkBasePosSubtable(self):
|
|
anchor = builder.buildAnchor
|
|
marks = {
|
|
"acute": (0, anchor(300, 700)),
|
|
"cedilla": (1, anchor(300, -100)),
|
|
"grave": (0, anchor(300, 700)),
|
|
}
|
|
bases = {
|
|
# Make sure we can handle missing entries.
|
|
"A": {}, # no entry for any markClass
|
|
"B": {0: anchor(500, 900)}, # only markClass 0 specified
|
|
"C": {1: anchor(500, -10)}, # only markClass 1 specified
|
|
"a": {0: anchor(500, 400), 1: anchor(500, -20)},
|
|
"b": {0: anchor(500, 800), 1: anchor(500, -20)},
|
|
}
|
|
table = builder.buildMarkBasePosSubtable(marks, bases, self.GLYPHMAP)
|
|
assert getXML(table.toXML) == [
|
|
'<MarkBasePos Format="1">',
|
|
" <MarkCoverage>",
|
|
' <Glyph value="grave"/>',
|
|
' <Glyph value="acute"/>',
|
|
' <Glyph value="cedilla"/>',
|
|
" </MarkCoverage>",
|
|
" <BaseCoverage>",
|
|
' <Glyph value="A"/>',
|
|
' <Glyph value="B"/>',
|
|
' <Glyph value="C"/>',
|
|
' <Glyph value="a"/>',
|
|
' <Glyph value="b"/>',
|
|
" </BaseCoverage>",
|
|
" <!-- ClassCount=2 -->",
|
|
" <MarkArray>",
|
|
" <!-- MarkCount=3 -->",
|
|
' <MarkRecord index="0">', # grave
|
|
' <Class value="0"/>',
|
|
' <MarkAnchor Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="700"/>',
|
|
" </MarkAnchor>",
|
|
" </MarkRecord>",
|
|
' <MarkRecord index="1">', # acute
|
|
' <Class value="0"/>',
|
|
' <MarkAnchor Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="700"/>',
|
|
" </MarkAnchor>",
|
|
" </MarkRecord>",
|
|
' <MarkRecord index="2">', # cedilla
|
|
' <Class value="1"/>',
|
|
' <MarkAnchor Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="-100"/>',
|
|
" </MarkAnchor>",
|
|
" </MarkRecord>",
|
|
" </MarkArray>",
|
|
" <BaseArray>",
|
|
" <!-- BaseCount=5 -->",
|
|
' <BaseRecord index="0">', # A
|
|
' <BaseAnchor index="0" empty="1"/>',
|
|
' <BaseAnchor index="1" empty="1"/>',
|
|
" </BaseRecord>",
|
|
' <BaseRecord index="1">', # B
|
|
' <BaseAnchor index="0" Format="1">',
|
|
' <XCoordinate value="500"/>',
|
|
' <YCoordinate value="900"/>',
|
|
" </BaseAnchor>",
|
|
' <BaseAnchor index="1" empty="1"/>',
|
|
" </BaseRecord>",
|
|
' <BaseRecord index="2">', # C
|
|
' <BaseAnchor index="0" empty="1"/>',
|
|
' <BaseAnchor index="1" Format="1">',
|
|
' <XCoordinate value="500"/>',
|
|
' <YCoordinate value="-10"/>',
|
|
" </BaseAnchor>",
|
|
" </BaseRecord>",
|
|
' <BaseRecord index="3">', # a
|
|
' <BaseAnchor index="0" Format="1">',
|
|
' <XCoordinate value="500"/>',
|
|
' <YCoordinate value="400"/>',
|
|
" </BaseAnchor>",
|
|
' <BaseAnchor index="1" Format="1">',
|
|
' <XCoordinate value="500"/>',
|
|
' <YCoordinate value="-20"/>',
|
|
" </BaseAnchor>",
|
|
" </BaseRecord>",
|
|
' <BaseRecord index="4">', # b
|
|
' <BaseAnchor index="0" Format="1">',
|
|
' <XCoordinate value="500"/>',
|
|
' <YCoordinate value="800"/>',
|
|
" </BaseAnchor>",
|
|
' <BaseAnchor index="1" Format="1">',
|
|
' <XCoordinate value="500"/>',
|
|
' <YCoordinate value="-20"/>',
|
|
" </BaseAnchor>",
|
|
" </BaseRecord>",
|
|
" </BaseArray>",
|
|
"</MarkBasePos>",
|
|
]
|
|
|
|
def test_buildMarkGlyphSetsDef(self):
|
|
marksets = builder.buildMarkGlyphSetsDef(
|
|
[{"acute", "grave"}, {"cedilla", "grave"}], self.GLYPHMAP
|
|
)
|
|
assert getXML(marksets.toXML) == [
|
|
"<MarkGlyphSetsDef>",
|
|
' <MarkSetTableFormat value="1"/>',
|
|
" <!-- MarkSetCount=2 -->",
|
|
' <Coverage index="0">',
|
|
' <Glyph value="grave"/>',
|
|
' <Glyph value="acute"/>',
|
|
" </Coverage>",
|
|
' <Coverage index="1">',
|
|
' <Glyph value="grave"/>',
|
|
' <Glyph value="cedilla"/>',
|
|
" </Coverage>",
|
|
"</MarkGlyphSetsDef>",
|
|
]
|
|
|
|
def test_buildMarkGlyphSetsDef_empty(self):
|
|
assert builder.buildMarkGlyphSetsDef([], self.GLYPHMAP) is None
|
|
|
|
def test_buildMarkGlyphSetsDef_None(self):
|
|
assert builder.buildMarkGlyphSetsDef(None, self.GLYPHMAP) is None
|
|
|
|
def test_buildMarkLigPosSubtable(self):
|
|
anchor = builder.buildAnchor
|
|
marks = {
|
|
"acute": (0, anchor(300, 700)),
|
|
"cedilla": (1, anchor(300, -100)),
|
|
"grave": (0, anchor(300, 700)),
|
|
}
|
|
bases = {
|
|
"f_i": [{}, {0: anchor(200, 400)}], # nothing on f; only 1 on i
|
|
"c_t": [
|
|
{0: anchor(500, 600), 1: anchor(500, -20)}, # c
|
|
{0: anchor(1300, 800), 1: anchor(1300, -20)}, # t
|
|
],
|
|
}
|
|
table = builder.buildMarkLigPosSubtable(marks, bases, self.GLYPHMAP)
|
|
assert getXML(table.toXML) == [
|
|
'<MarkLigPos Format="1">',
|
|
" <MarkCoverage>",
|
|
' <Glyph value="grave"/>',
|
|
' <Glyph value="acute"/>',
|
|
' <Glyph value="cedilla"/>',
|
|
" </MarkCoverage>",
|
|
" <LigatureCoverage>",
|
|
' <Glyph value="f_i"/>',
|
|
' <Glyph value="c_t"/>',
|
|
" </LigatureCoverage>",
|
|
" <!-- ClassCount=2 -->",
|
|
" <MarkArray>",
|
|
" <!-- MarkCount=3 -->",
|
|
' <MarkRecord index="0">',
|
|
' <Class value="0"/>',
|
|
' <MarkAnchor Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="700"/>',
|
|
" </MarkAnchor>",
|
|
" </MarkRecord>",
|
|
' <MarkRecord index="1">',
|
|
' <Class value="0"/>',
|
|
' <MarkAnchor Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="700"/>',
|
|
" </MarkAnchor>",
|
|
" </MarkRecord>",
|
|
' <MarkRecord index="2">',
|
|
' <Class value="1"/>',
|
|
' <MarkAnchor Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="-100"/>',
|
|
" </MarkAnchor>",
|
|
" </MarkRecord>",
|
|
" </MarkArray>",
|
|
" <LigatureArray>",
|
|
" <!-- LigatureCount=2 -->",
|
|
' <LigatureAttach index="0">',
|
|
" <!-- ComponentCount=2 -->",
|
|
' <ComponentRecord index="0">',
|
|
' <LigatureAnchor index="0" empty="1"/>',
|
|
' <LigatureAnchor index="1" empty="1"/>',
|
|
" </ComponentRecord>",
|
|
' <ComponentRecord index="1">',
|
|
' <LigatureAnchor index="0" Format="1">',
|
|
' <XCoordinate value="200"/>',
|
|
' <YCoordinate value="400"/>',
|
|
" </LigatureAnchor>",
|
|
' <LigatureAnchor index="1" empty="1"/>',
|
|
" </ComponentRecord>",
|
|
" </LigatureAttach>",
|
|
' <LigatureAttach index="1">',
|
|
" <!-- ComponentCount=2 -->",
|
|
' <ComponentRecord index="0">',
|
|
' <LigatureAnchor index="0" Format="1">',
|
|
' <XCoordinate value="500"/>',
|
|
' <YCoordinate value="600"/>',
|
|
" </LigatureAnchor>",
|
|
' <LigatureAnchor index="1" Format="1">',
|
|
' <XCoordinate value="500"/>',
|
|
' <YCoordinate value="-20"/>',
|
|
" </LigatureAnchor>",
|
|
" </ComponentRecord>",
|
|
' <ComponentRecord index="1">',
|
|
' <LigatureAnchor index="0" Format="1">',
|
|
' <XCoordinate value="1300"/>',
|
|
' <YCoordinate value="800"/>',
|
|
" </LigatureAnchor>",
|
|
' <LigatureAnchor index="1" Format="1">',
|
|
' <XCoordinate value="1300"/>',
|
|
' <YCoordinate value="-20"/>',
|
|
" </LigatureAnchor>",
|
|
" </ComponentRecord>",
|
|
" </LigatureAttach>",
|
|
" </LigatureArray>",
|
|
"</MarkLigPos>",
|
|
]
|
|
|
|
def test_buildMarkRecord(self):
|
|
rec = builder.buildMarkRecord(17, builder.buildAnchor(500, -20))
|
|
assert getXML(rec.toXML) == [
|
|
"<MarkRecord>",
|
|
' <Class value="17"/>',
|
|
' <MarkAnchor Format="1">',
|
|
' <XCoordinate value="500"/>',
|
|
' <YCoordinate value="-20"/>',
|
|
" </MarkAnchor>",
|
|
"</MarkRecord>",
|
|
]
|
|
|
|
def test_buildMark2Record(self):
|
|
a = builder.buildAnchor
|
|
rec = builder.buildMark2Record([a(500, -20), None, a(300, -15)])
|
|
assert getXML(rec.toXML) == [
|
|
"<Mark2Record>",
|
|
' <Mark2Anchor index="0" Format="1">',
|
|
' <XCoordinate value="500"/>',
|
|
' <YCoordinate value="-20"/>',
|
|
" </Mark2Anchor>",
|
|
' <Mark2Anchor index="1" empty="1"/>',
|
|
' <Mark2Anchor index="2" Format="1">',
|
|
' <XCoordinate value="300"/>',
|
|
' <YCoordinate value="-15"/>',
|
|
" </Mark2Anchor>",
|
|
"</Mark2Record>",
|
|
]
|
|
|
|
def test_buildPairPosClassesSubtable(self):
|
|
d20 = builder.buildValue({"XPlacement": -20})
|
|
d50 = builder.buildValue({"XPlacement": -50})
|
|
d0 = builder.buildValue({})
|
|
d8020 = builder.buildValue({"XPlacement": -80, "YPlacement": -20})
|
|
subtable = builder.buildPairPosClassesSubtable(
|
|
{
|
|
(tuple("A"), tuple(["zero"])): (d0, d50),
|
|
(tuple("A"), tuple(["one", "two"])): (None, d20),
|
|
(tuple(["B", "C"]), tuple(["zero"])): (d8020, d50),
|
|
},
|
|
self.GLYPHMAP,
|
|
)
|
|
assert getXML(subtable.toXML) == [
|
|
'<PairPos Format="2">',
|
|
" <Coverage>",
|
|
' <Glyph value="A"/>',
|
|
' <Glyph value="B"/>',
|
|
' <Glyph value="C"/>',
|
|
" </Coverage>",
|
|
' <ValueFormat1 value="3"/>',
|
|
' <ValueFormat2 value="1"/>',
|
|
" <ClassDef1>",
|
|
' <ClassDef glyph="A" class="1"/>',
|
|
" </ClassDef1>",
|
|
" <ClassDef2>",
|
|
' <ClassDef glyph="one" class="1"/>',
|
|
' <ClassDef glyph="two" class="1"/>',
|
|
' <ClassDef glyph="zero" class="2"/>',
|
|
" </ClassDef2>",
|
|
" <!-- Class1Count=2 -->",
|
|
" <!-- Class2Count=3 -->",
|
|
' <Class1Record index="0">',
|
|
' <Class2Record index="0">',
|
|
' <Value1 XPlacement="0" YPlacement="0"/>',
|
|
' <Value2 XPlacement="0"/>',
|
|
" </Class2Record>",
|
|
' <Class2Record index="1">',
|
|
' <Value1 XPlacement="0" YPlacement="0"/>',
|
|
' <Value2 XPlacement="0"/>',
|
|
" </Class2Record>",
|
|
' <Class2Record index="2">',
|
|
' <Value1 XPlacement="-80" YPlacement="-20"/>',
|
|
' <Value2 XPlacement="-50"/>',
|
|
" </Class2Record>",
|
|
" </Class1Record>",
|
|
' <Class1Record index="1">',
|
|
' <Class2Record index="0">',
|
|
' <Value1 XPlacement="0" YPlacement="0"/>',
|
|
' <Value2 XPlacement="0"/>',
|
|
" </Class2Record>",
|
|
' <Class2Record index="1">',
|
|
' <Value1 XPlacement="0" YPlacement="0"/>',
|
|
' <Value2 XPlacement="-20"/>',
|
|
" </Class2Record>",
|
|
' <Class2Record index="2">',
|
|
' <Value1 XPlacement="0" YPlacement="0"/>',
|
|
' <Value2 XPlacement="-50"/>',
|
|
" </Class2Record>",
|
|
" </Class1Record>",
|
|
"</PairPos>",
|
|
]
|
|
|
|
def test_buildPairPosGlyphs(self):
|
|
d50 = builder.buildValue({"XPlacement": -50})
|
|
d8020 = builder.buildValue({"XPlacement": -80, "YPlacement": -20})
|
|
subtables = builder.buildPairPosGlyphs(
|
|
{("A", "zero"): (None, d50), ("A", "one"): (d8020, d50)}, self.GLYPHMAP
|
|
)
|
|
assert sum([getXML(t.toXML) for t in subtables], []) == [
|
|
'<PairPos Format="1">',
|
|
" <Coverage>",
|
|
' <Glyph value="A"/>',
|
|
" </Coverage>",
|
|
' <ValueFormat1 value="0"/>',
|
|
' <ValueFormat2 value="1"/>',
|
|
" <!-- PairSetCount=1 -->",
|
|
' <PairSet index="0">',
|
|
" <!-- PairValueCount=1 -->",
|
|
' <PairValueRecord index="0">',
|
|
' <SecondGlyph value="zero"/>',
|
|
' <Value2 XPlacement="-50"/>',
|
|
" </PairValueRecord>",
|
|
" </PairSet>",
|
|
"</PairPos>",
|
|
'<PairPos Format="1">',
|
|
" <Coverage>",
|
|
' <Glyph value="A"/>',
|
|
" </Coverage>",
|
|
' <ValueFormat1 value="3"/>',
|
|
' <ValueFormat2 value="1"/>',
|
|
" <!-- PairSetCount=1 -->",
|
|
' <PairSet index="0">',
|
|
" <!-- PairValueCount=1 -->",
|
|
' <PairValueRecord index="0">',
|
|
' <SecondGlyph value="one"/>',
|
|
' <Value1 XPlacement="-80" YPlacement="-20"/>',
|
|
' <Value2 XPlacement="-50"/>',
|
|
" </PairValueRecord>",
|
|
" </PairSet>",
|
|
"</PairPos>",
|
|
]
|
|
|
|
def test_buildPairPosGlyphsSubtable(self):
|
|
d20 = builder.buildValue({"XPlacement": -20})
|
|
d50 = builder.buildValue({"XPlacement": -50})
|
|
d0 = builder.buildValue({})
|
|
d8020 = builder.buildValue({"XPlacement": -80, "YPlacement": -20})
|
|
subtable = builder.buildPairPosGlyphsSubtable(
|
|
{
|
|
("A", "zero"): (d0, d50),
|
|
("A", "one"): (None, d20),
|
|
("B", "five"): (d8020, d50),
|
|
|
|
},
|
|
self.GLYPHMAP,
|
|
)
|
|
|
|
assert getXML(subtable.toXML) == [
|
|
'<PairPos Format="1">',
|
|
" <Coverage>",
|
|
' <Glyph value="A"/>',
|
|
' <Glyph value="B"/>',
|
|
" </Coverage>",
|
|
' <ValueFormat1 value="3"/>',
|
|
' <ValueFormat2 value="1"/>',
|
|
" <!-- PairSetCount=2 -->",
|
|
' <PairSet index="0">',
|
|
" <!-- PairValueCount=2 -->",
|
|
' <PairValueRecord index="0">',
|
|
' <SecondGlyph value="zero"/>',
|
|
' <Value1 XPlacement="0" YPlacement="0"/>',
|
|
' <Value2 XPlacement="-50"/>',
|
|
" </PairValueRecord>",
|
|
' <PairValueRecord index="1">',
|
|
' <SecondGlyph value="one"/>',
|
|
' <Value1 XPlacement="0" YPlacement="0"/>',
|
|
' <Value2 XPlacement="-20"/>',
|
|
" </PairValueRecord>",
|
|
" </PairSet>",
|
|
' <PairSet index="1">',
|
|
" <!-- PairValueCount=1 -->",
|
|
' <PairValueRecord index="0">',
|
|
' <SecondGlyph value="five"/>',
|
|
' <Value1 XPlacement="-80" YPlacement="-20"/>',
|
|
' <Value2 XPlacement="-50"/>',
|
|
" </PairValueRecord>",
|
|
" </PairSet>",
|
|
"</PairPos>",
|
|
]
|
|
|
|
def test_buildSinglePos(self):
|
|
subtables = builder.buildSinglePos(
|
|
{
|
|
"one": builder.buildValue({"XPlacement": 500}),
|
|
"two": builder.buildValue({"XPlacement": 500}),
|
|
"three": builder.buildValue({"XPlacement": 200}),
|
|
"four": builder.buildValue({"XPlacement": 400}),
|
|
"five": builder.buildValue({"XPlacement": 500}),
|
|
"six": builder.buildValue({"YPlacement": -6}),
|
|
},
|
|
self.GLYPHMAP,
|
|
)
|
|
assert sum([getXML(t.toXML) for t in subtables], []) == [
|
|
'<SinglePos Format="2">',
|
|
" <Coverage>",
|
|
' <Glyph value="one"/>',
|
|
' <Glyph value="two"/>',
|
|
' <Glyph value="three"/>',
|
|
' <Glyph value="four"/>',
|
|
' <Glyph value="five"/>',
|
|
" </Coverage>",
|
|
' <ValueFormat value="1"/>',
|
|
" <!-- ValueCount=5 -->",
|
|
' <Value index="0" XPlacement="500"/>',
|
|
' <Value index="1" XPlacement="500"/>',
|
|
' <Value index="2" XPlacement="200"/>',
|
|
' <Value index="3" XPlacement="400"/>',
|
|
' <Value index="4" XPlacement="500"/>',
|
|
"</SinglePos>",
|
|
'<SinglePos Format="1">',
|
|
" <Coverage>",
|
|
' <Glyph value="six"/>',
|
|
" </Coverage>",
|
|
' <ValueFormat value="2"/>',
|
|
' <Value YPlacement="-6"/>',
|
|
"</SinglePos>",
|
|
]
|
|
|
|
def test_buildSinglePos_ValueFormat0(self):
|
|
subtables = builder.buildSinglePos(
|
|
{"zero": builder.buildValue({})}, self.GLYPHMAP
|
|
)
|
|
assert sum([getXML(t.toXML) for t in subtables], []) == [
|
|
'<SinglePos Format="1">',
|
|
" <Coverage>",
|
|
' <Glyph value="zero"/>',
|
|
" </Coverage>",
|
|
' <ValueFormat value="0"/>',
|
|
"</SinglePos>",
|
|
]
|
|
|
|
def test_buildSinglePosSubtable_format1(self):
|
|
subtable = builder.buildSinglePosSubtable(
|
|
{
|
|
"one": builder.buildValue({"XPlacement": 777}),
|
|
"two": builder.buildValue({"XPlacement": 777}),
|
|
},
|
|
self.GLYPHMAP,
|
|
)
|
|
assert getXML(subtable.toXML) == [
|
|
'<SinglePos Format="1">',
|
|
" <Coverage>",
|
|
' <Glyph value="one"/>',
|
|
' <Glyph value="two"/>',
|
|
" </Coverage>",
|
|
' <ValueFormat value="1"/>',
|
|
' <Value XPlacement="777"/>',
|
|
"</SinglePos>",
|
|
]
|
|
|
|
def test_buildSinglePosSubtable_format2(self):
|
|
subtable = builder.buildSinglePosSubtable(
|
|
{
|
|
"one": builder.buildValue({"XPlacement": 777}),
|
|
"two": builder.buildValue({"YPlacement": -888}),
|
|
},
|
|
self.GLYPHMAP,
|
|
)
|
|
assert getXML(subtable.toXML) == [
|
|
'<SinglePos Format="2">',
|
|
" <Coverage>",
|
|
' <Glyph value="one"/>',
|
|
' <Glyph value="two"/>',
|
|
" </Coverage>",
|
|
' <ValueFormat value="3"/>',
|
|
" <!-- ValueCount=2 -->",
|
|
' <Value index="0" XPlacement="777" YPlacement="0"/>',
|
|
' <Value index="1" XPlacement="0" YPlacement="-888"/>',
|
|
"</SinglePos>",
|
|
]
|
|
|
|
def test_buildValue(self):
|
|
value = builder.buildValue({"XPlacement": 7, "YPlacement": 23})
|
|
func = lambda writer, font: value.toXML(writer, font, valueName="Val")
|
|
assert getXML(func) == ['<Val XPlacement="7" YPlacement="23"/>']
|
|
|
|
def test_getLigatureKey(self):
|
|
components = lambda s: [tuple(word) for word in s.split()]
|
|
c = components("fi fl ff ffi fff")
|
|
c.sort(key=builder._getLigatureKey)
|
|
assert c == components("fff ffi ff fi fl")
|
|
|
|
def test_getSinglePosValueKey(self):
|
|
device = builder.buildDevice({10: 1, 11: 3})
|
|
a1 = builder.buildValue({"XPlacement": 500, "XPlaDevice": device})
|
|
a2 = builder.buildValue({"XPlacement": 500, "XPlaDevice": device})
|
|
b = builder.buildValue({"XPlacement": 500})
|
|
keyA1 = builder._getSinglePosValueKey(a1)
|
|
keyA2 = builder._getSinglePosValueKey(a1)
|
|
keyB = builder._getSinglePosValueKey(b)
|
|
assert keyA1 == keyA2
|
|
assert hash(keyA1) == hash(keyA2)
|
|
assert keyA1 != keyB
|
|
assert hash(keyA1) != hash(keyB)
|
|
|
|
|
|
class ClassDefBuilderTest(object):
|
|
def test_build_usingClass0(self):
|
|
b = builder.ClassDefBuilder(useClass0=True)
|
|
b.add({"aa", "bb"})
|
|
b.add({"a", "b"})
|
|
b.add({"c"})
|
|
b.add({"e", "f", "g", "h"})
|
|
cdef = b.build()
|
|
assert isinstance(cdef, otTables.ClassDef)
|
|
assert cdef.classDefs == {"a": 2, "b": 2, "c": 3, "aa": 1, "bb": 1}
|
|
|
|
def test_build_notUsingClass0(self):
|
|
b = builder.ClassDefBuilder(useClass0=False)
|
|
b.add({"a", "b"})
|
|
b.add({"c"})
|
|
b.add({"e", "f", "g", "h"})
|
|
cdef = b.build()
|
|
assert isinstance(cdef, otTables.ClassDef)
|
|
assert cdef.classDefs == {
|
|
"a": 2,
|
|
"b": 2,
|
|
"c": 3,
|
|
"e": 1,
|
|
"f": 1,
|
|
"g": 1,
|
|
"h": 1,
|
|
}
|
|
|
|
def test_canAdd(self):
|
|
b = builder.ClassDefBuilder(useClass0=True)
|
|
b.add({"a", "b", "c", "d"})
|
|
b.add({"e", "f"})
|
|
assert b.canAdd({"a", "b", "c", "d"})
|
|
assert b.canAdd({"e", "f"})
|
|
assert b.canAdd({"g", "h", "i"})
|
|
assert not b.canAdd({"b", "c", "d"})
|
|
assert not b.canAdd({"a", "b", "c", "d", "e", "f"})
|
|
assert not b.canAdd({"d", "e", "f"})
|
|
assert not b.canAdd({"f"})
|
|
|
|
def test_add_exception(self):
|
|
b = builder.ClassDefBuilder(useClass0=True)
|
|
b.add({"a", "b", "c"})
|
|
with pytest.raises(error.OpenTypeLibError):
|
|
b.add({"a", "d"})
|
|
|
|
|
|
buildStatTable_test_data = [
|
|
([
|
|
dict(
|
|
tag="wght",
|
|
name="Weight",
|
|
values=[
|
|
dict(value=100, name='Thin'),
|
|
dict(value=400, name='Regular', flags=0x2),
|
|
dict(value=900, name='Black')])], None, "Regular", [
|
|
' <STAT>',
|
|
' <Version value="0x00010001"/>',
|
|
' <DesignAxisRecordSize value="8"/>',
|
|
' <!-- DesignAxisCount=1 -->',
|
|
' <DesignAxisRecord>',
|
|
' <Axis index="0">',
|
|
' <AxisTag value="wght"/>',
|
|
' <AxisNameID value="257"/> <!-- Weight -->',
|
|
' <AxisOrdering value="0"/>',
|
|
' </Axis>',
|
|
' </DesignAxisRecord>',
|
|
' <!-- AxisValueCount=3 -->',
|
|
' <AxisValueArray>',
|
|
' <AxisValue index="0" Format="1">',
|
|
' <AxisIndex value="0"/>',
|
|
' <Flags value="0"/>',
|
|
' <ValueNameID value="258"/> <!-- Thin -->',
|
|
' <Value value="100.0"/>',
|
|
' </AxisValue>',
|
|
' <AxisValue index="1" Format="1">',
|
|
' <AxisIndex value="0"/>',
|
|
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
|
|
' <ValueNameID value="256"/> <!-- Regular -->',
|
|
' <Value value="400.0"/>',
|
|
' </AxisValue>',
|
|
' <AxisValue index="2" Format="1">',
|
|
' <AxisIndex value="0"/>',
|
|
' <Flags value="0"/>',
|
|
' <ValueNameID value="259"/> <!-- Black -->',
|
|
' <Value value="900.0"/>',
|
|
' </AxisValue>',
|
|
' </AxisValueArray>',
|
|
' <ElidedFallbackNameID value="256"/> <!-- Regular -->',
|
|
' </STAT>']),
|
|
([
|
|
dict(
|
|
tag="wght",
|
|
name=dict(en="Weight", nl="Gewicht"),
|
|
values=[
|
|
dict(value=100, name=dict(en='Thin', nl='Dun')),
|
|
dict(value=400, name='Regular', flags=0x2),
|
|
dict(value=900, name='Black'),
|
|
]),
|
|
dict(
|
|
tag="wdth",
|
|
name="Width",
|
|
values=[
|
|
dict(value=50, name='Condensed'),
|
|
dict(value=100, name='Regular', flags=0x2),
|
|
dict(value=200, name='Extended')])], None, 2, [
|
|
' <STAT>',
|
|
' <Version value="0x00010001"/>',
|
|
' <DesignAxisRecordSize value="8"/>',
|
|
' <!-- DesignAxisCount=2 -->',
|
|
' <DesignAxisRecord>',
|
|
' <Axis index="0">',
|
|
' <AxisTag value="wght"/>',
|
|
' <AxisNameID value="256"/> <!-- Weight -->',
|
|
' <AxisOrdering value="0"/>',
|
|
' </Axis>',
|
|
' <Axis index="1">',
|
|
' <AxisTag value="wdth"/>',
|
|
' <AxisNameID value="260"/> <!-- Width -->',
|
|
' <AxisOrdering value="1"/>',
|
|
' </Axis>',
|
|
' </DesignAxisRecord>',
|
|
' <!-- AxisValueCount=6 -->',
|
|
' <AxisValueArray>',
|
|
' <AxisValue index="0" Format="1">',
|
|
' <AxisIndex value="0"/>',
|
|
' <Flags value="0"/>',
|
|
' <ValueNameID value="257"/> <!-- Thin -->',
|
|
' <Value value="100.0"/>',
|
|
' </AxisValue>',
|
|
' <AxisValue index="1" Format="1">',
|
|
' <AxisIndex value="0"/>',
|
|
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
|
|
' <ValueNameID value="258"/> <!-- Regular -->',
|
|
' <Value value="400.0"/>',
|
|
' </AxisValue>',
|
|
' <AxisValue index="2" Format="1">',
|
|
' <AxisIndex value="0"/>',
|
|
' <Flags value="0"/>',
|
|
' <ValueNameID value="259"/> <!-- Black -->',
|
|
' <Value value="900.0"/>',
|
|
' </AxisValue>',
|
|
' <AxisValue index="3" Format="1">',
|
|
' <AxisIndex value="1"/>',
|
|
' <Flags value="0"/>',
|
|
' <ValueNameID value="261"/> <!-- Condensed -->',
|
|
' <Value value="50.0"/>',
|
|
' </AxisValue>',
|
|
' <AxisValue index="4" Format="1">',
|
|
' <AxisIndex value="1"/>',
|
|
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
|
|
' <ValueNameID value="258"/> <!-- Regular -->',
|
|
' <Value value="100.0"/>',
|
|
' </AxisValue>',
|
|
' <AxisValue index="5" Format="1">',
|
|
' <AxisIndex value="1"/>',
|
|
' <Flags value="0"/>',
|
|
' <ValueNameID value="262"/> <!-- Extended -->',
|
|
' <Value value="200.0"/>',
|
|
' </AxisValue>',
|
|
' </AxisValueArray>',
|
|
' <ElidedFallbackNameID value="2"/> <!-- missing from name table -->',
|
|
' </STAT>']),
|
|
([
|
|
dict(
|
|
tag="wght",
|
|
name="Weight",
|
|
values=[
|
|
dict(value=400, name='Regular', flags=0x2),
|
|
dict(value=600, linkedValue=650, name='Bold')])], None, 18, [
|
|
' <STAT>',
|
|
' <Version value="0x00010001"/>',
|
|
' <DesignAxisRecordSize value="8"/>',
|
|
' <!-- DesignAxisCount=1 -->',
|
|
' <DesignAxisRecord>',
|
|
' <Axis index="0">',
|
|
' <AxisTag value="wght"/>',
|
|
' <AxisNameID value="256"/> <!-- Weight -->',
|
|
' <AxisOrdering value="0"/>',
|
|
' </Axis>',
|
|
' </DesignAxisRecord>',
|
|
' <!-- AxisValueCount=2 -->',
|
|
' <AxisValueArray>',
|
|
' <AxisValue index="0" Format="1">',
|
|
' <AxisIndex value="0"/>',
|
|
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
|
|
' <ValueNameID value="257"/> <!-- Regular -->',
|
|
' <Value value="400.0"/>',
|
|
' </AxisValue>',
|
|
' <AxisValue index="1" Format="3">',
|
|
' <AxisIndex value="0"/>',
|
|
' <Flags value="0"/>',
|
|
' <ValueNameID value="258"/> <!-- Bold -->',
|
|
' <Value value="600.0"/>',
|
|
' <LinkedValue value="650.0"/>',
|
|
' </AxisValue>',
|
|
' </AxisValueArray>',
|
|
' <ElidedFallbackNameID value="18"/> <!-- missing from name table -->',
|
|
' </STAT>']),
|
|
([
|
|
dict(
|
|
tag="opsz",
|
|
name="Optical Size",
|
|
values=[
|
|
dict(nominalValue=6, rangeMaxValue=10, name='Small'),
|
|
dict(rangeMinValue=10, nominalValue=14, rangeMaxValue=24, name='Text', flags=0x2),
|
|
dict(rangeMinValue=24, nominalValue=600, name='Display')])], None, 2, [
|
|
' <STAT>',
|
|
' <Version value="0x00010001"/>',
|
|
' <DesignAxisRecordSize value="8"/>',
|
|
' <!-- DesignAxisCount=1 -->',
|
|
' <DesignAxisRecord>',
|
|
' <Axis index="0">',
|
|
' <AxisTag value="opsz"/>',
|
|
' <AxisNameID value="256"/> <!-- Optical Size -->',
|
|
' <AxisOrdering value="0"/>',
|
|
' </Axis>',
|
|
' </DesignAxisRecord>',
|
|
' <!-- AxisValueCount=3 -->',
|
|
' <AxisValueArray>',
|
|
' <AxisValue index="0" Format="2">',
|
|
' <AxisIndex value="0"/>',
|
|
' <Flags value="0"/>',
|
|
' <ValueNameID value="257"/> <!-- Small -->',
|
|
' <NominalValue value="6.0"/>',
|
|
' <RangeMinValue value="-32768.0"/>',
|
|
' <RangeMaxValue value="10.0"/>',
|
|
' </AxisValue>',
|
|
' <AxisValue index="1" Format="2">',
|
|
' <AxisIndex value="0"/>',
|
|
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
|
|
' <ValueNameID value="258"/> <!-- Text -->',
|
|
' <NominalValue value="14.0"/>',
|
|
' <RangeMinValue value="10.0"/>',
|
|
' <RangeMaxValue value="24.0"/>',
|
|
' </AxisValue>',
|
|
' <AxisValue index="2" Format="2">',
|
|
' <AxisIndex value="0"/>',
|
|
' <Flags value="0"/>',
|
|
' <ValueNameID value="259"/> <!-- Display -->',
|
|
' <NominalValue value="600.0"/>',
|
|
' <RangeMinValue value="24.0"/>',
|
|
' <RangeMaxValue value="32767.99998"/>',
|
|
' </AxisValue>',
|
|
' </AxisValueArray>',
|
|
' <ElidedFallbackNameID value="2"/> <!-- missing from name table -->',
|
|
' </STAT>']),
|
|
([
|
|
dict(
|
|
tag="wght",
|
|
name="Weight",
|
|
ordering=1,
|
|
values=[]),
|
|
dict(
|
|
tag="ABCD",
|
|
name="ABCDTest",
|
|
ordering=0,
|
|
values=[
|
|
dict(value=100, name="Regular", flags=0x2)])],
|
|
[dict(location=dict(wght=300, ABCD=100), name='Regular ABCD')], 18, [
|
|
' <STAT>',
|
|
' <Version value="0x00010002"/>',
|
|
' <DesignAxisRecordSize value="8"/>',
|
|
' <!-- DesignAxisCount=2 -->',
|
|
' <DesignAxisRecord>',
|
|
' <Axis index="0">',
|
|
' <AxisTag value="wght"/>',
|
|
' <AxisNameID value="256"/> <!-- Weight -->',
|
|
' <AxisOrdering value="1"/>',
|
|
' </Axis>',
|
|
' <Axis index="1">',
|
|
' <AxisTag value="ABCD"/>',
|
|
' <AxisNameID value="257"/> <!-- ABCDTest -->',
|
|
' <AxisOrdering value="0"/>',
|
|
' </Axis>',
|
|
' </DesignAxisRecord>',
|
|
' <!-- AxisValueCount=2 -->',
|
|
' <AxisValueArray>',
|
|
' <AxisValue index="0" Format="4">',
|
|
' <!-- AxisCount=2 -->',
|
|
' <Flags value="0"/>',
|
|
' <ValueNameID value="259"/> <!-- Regular ABCD -->',
|
|
' <AxisValueRecord index="0">',
|
|
' <AxisIndex value="0"/>',
|
|
' <Value value="300.0"/>',
|
|
' </AxisValueRecord>',
|
|
' <AxisValueRecord index="1">',
|
|
' <AxisIndex value="1"/>',
|
|
' <Value value="100.0"/>',
|
|
' </AxisValueRecord>',
|
|
' </AxisValue>',
|
|
' <AxisValue index="1" Format="1">',
|
|
' <AxisIndex value="1"/>',
|
|
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
|
|
' <ValueNameID value="258"/> <!-- Regular -->',
|
|
' <Value value="100.0"/>',
|
|
' </AxisValue>',
|
|
' </AxisValueArray>',
|
|
' <ElidedFallbackNameID value="18"/> <!-- missing from name table -->',
|
|
' </STAT>']),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("axes, axisValues, elidedFallbackName, expected_ttx", buildStatTable_test_data)
|
|
def test_buildStatTable(axes, axisValues, elidedFallbackName, expected_ttx):
|
|
font = ttLib.TTFont()
|
|
font["name"] = ttLib.newTable("name")
|
|
font["name"].names = []
|
|
# https://github.com/fonttools/fonttools/issues/1985
|
|
# Add nameID < 256 that matches a test axis name, to test whether
|
|
# the nameID is not reused: AxisNameIDs must be > 255 according
|
|
# to the spec.
|
|
font["name"].addMultilingualName(dict(en="ABCDTest"), nameID=6)
|
|
builder.buildStatTable(font, axes, axisValues, elidedFallbackName)
|
|
f = io.StringIO()
|
|
font.saveXML(f, tables=["STAT"])
|
|
ttx = f.getvalue().splitlines()
|
|
ttx = ttx[3:-2] # strip XML header and <ttFont> element
|
|
assert expected_ttx == ttx
|
|
# Compile and round-trip
|
|
f = io.BytesIO()
|
|
font.save(f)
|
|
font = ttLib.TTFont(f)
|
|
f = io.StringIO()
|
|
font.saveXML(f, tables=["STAT"])
|
|
ttx = f.getvalue().splitlines()
|
|
ttx = ttx[3:-2] # strip XML header and <ttFont> element
|
|
assert expected_ttx == ttx
|
|
|
|
|
|
def test_stat_infinities():
|
|
negInf = floatToFixed(builder.AXIS_VALUE_NEGATIVE_INFINITY, 16)
|
|
assert struct.pack(">l", negInf) == b"\x80\x00\x00\x00"
|
|
posInf = floatToFixed(builder.AXIS_VALUE_POSITIVE_INFINITY, 16)
|
|
assert struct.pack(">l", posInf) == b"\x7f\xff\xff\xff"
|
|
|
|
|
|
class ChainContextualRulesetTest(object):
|
|
def test_makeRulesets(self):
|
|
font = ttLib.TTFont()
|
|
font.setGlyphOrder(["a","b","c","d","A","B","C","D","E"])
|
|
sb = builder.ChainContextSubstBuilder(font, None)
|
|
prefix, input_, suffix, lookups = [["a"], ["b"]], [["c"]], [], [None]
|
|
sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
|
|
|
|
prefix, input_, suffix, lookups = [["a"], ["d"]], [["c"]], [], [None]
|
|
sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
|
|
|
|
sb.add_subtable_break(None)
|
|
|
|
# Second subtable has some glyph classes
|
|
prefix, input_, suffix, lookups = [["A"]], [["E"]], [], [None]
|
|
sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
|
|
prefix, input_, suffix, lookups = [["A"]], [["C","D"]], [], [None]
|
|
sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
|
|
prefix, input_, suffix, lookups = [["A", "B"]], [["E"]], [], [None]
|
|
sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
|
|
|
|
sb.add_subtable_break(None)
|
|
|
|
# Third subtable has no pre/post context
|
|
prefix, input_, suffix, lookups = [], [["E"]], [], [None]
|
|
sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
|
|
prefix, input_, suffix, lookups = [], [["C","D"]], [], [None]
|
|
sb.rules.append(builder.ChainContextualRule(prefix, input_, suffix, lookups))
|
|
|
|
rulesets = sb.rulesets()
|
|
assert len(rulesets) == 3
|
|
assert rulesets[0].hasPrefixOrSuffix
|
|
assert not rulesets[0].hasAnyGlyphClasses
|
|
cd = rulesets[0].format2ClassDefs()
|
|
assert set(cd[0].classes()[1:]) == set([("d",),("b",),("a",)])
|
|
assert set(cd[1].classes()[1:]) == set([("c",)])
|
|
assert set(cd[2].classes()[1:]) == set()
|
|
|
|
assert rulesets[1].hasPrefixOrSuffix
|
|
assert rulesets[1].hasAnyGlyphClasses
|
|
assert not rulesets[1].format2ClassDefs()
|
|
|
|
assert not rulesets[2].hasPrefixOrSuffix
|
|
assert rulesets[2].hasAnyGlyphClasses
|
|
assert rulesets[2].format2ClassDefs()
|
|
cd = rulesets[2].format2ClassDefs()
|
|
assert set(cd[0].classes()[1:]) == set()
|
|
assert set(cd[1].classes()[1:]) == set([("C","D"), ("E",)])
|
|
assert set(cd[2].classes()[1:]) == set()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(sys.argv))
|