普通文本  |  1063行  |  50.74 KB

from __future__ import print_function, division, absolute_import
from __future__ import unicode_literals
from fontTools.misc.testTools import getXML
from fontTools.otlLib import builder
from fontTools.ttLib.tables import otTables
from itertools import chain
import unittest


class BuilderTest(unittest.TestCase):
    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 __init__(self, methodName):
        unittest.TestCase.__init__(self, methodName)
        # Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
        # and fires deprecation warnings if a program uses the old name.
        if not hasattr(self, "assertRaisesRegex"):
            self.assertRaisesRegex = self.assertRaisesRegexp

    @classmethod
    def setUpClass(cls):
        cls.maxDiff = None

    def test_buildAnchor_format1(self):
        anchor = builder.buildAnchor(23, 42)
        self.assertEqual(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)
        self.assertEqual(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}))
        self.assertEqual(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)
        self.assertEqual(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):
        self.assertIsNone(builder.buildAttachList({}, self.GLYPHMAP))

    def test_buildAttachPoint(self):
        attachPoint = builder.buildAttachPoint([7, 3])
        self.assertEqual(getXML(attachPoint.toXML),
                         ['<AttachPoint>',
                          '  <!-- PointCount=2 -->',
                          '  <PointIndex index="0" value="3"/>',
                          '  <PointIndex index="1" value="7"/>',
                          '</AttachPoint>'])

    def test_buildAttachPoint_empty(self):
        self.assertIsNone(builder.buildAttachPoint([]))

    def test_buildAttachPoint_duplicate(self):
        attachPoint = builder.buildAttachPoint([7, 3, 7])
        self.assertEqual(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)
        self.assertEqual(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)])
        self.assertEqual(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)
        self.assertEqual(getXML(caret.toXML),
                         ['<CaretValue Format="1">',
                          '  <Coordinate value="500"/>',
                          '</CaretValue>'])

    def test_buildCaretValueForPoint(self):
        caret = builder.buildCaretValueForPoint(23)
        self.assertEqual(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)])
        self.assertEqual(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):
        self.assertIsNone(builder.buildComponentRecord([]))

    def test_buildComponentRecord_None(self):
        self.assertIsNone(builder.buildComponentRecord(None))

    def test_buildCoverage(self):
        cov = builder.buildCoverage({"two", "four"}, {"two": 2, "four": 4})
        self.assertEqual(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)
        self.assertEqual(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})
        self.assertEqual(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})
        self.assertEqual(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})
        self.assertEqual(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)
        self.assertEqual(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]])
        self.assertEqual(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])
        self.assertEqual(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([])
        self.assertEqual(getXML(attach.toXML),
                         ['<LigatureAttach>',
                          '  <!-- ComponentCount=0 -->',
                          '</LigatureAttach>'])

    def test_buildLigCaretList(self):
        carets = builder.buildLigCaretList(
            {"f_f_i": [300, 600]}, {"c_t": [42]}, self.GLYPHMAP)
        self.assertEqual(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)
        self.assertEqual(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):
        self.assertIsNone(builder.buildLigCaretList({}, {}, self.GLYPHMAP))

    def test_buildLigCaretList_None(self):
        self.assertIsNone(builder.buildLigCaretList(None, None, self.GLYPHMAP))

    def test_buildLigGlyph_coords(self):
        lig = builder.buildLigGlyph([500, 800], None)
        self.assertEqual(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):
        self.assertIsNone(builder.buildLigGlyph([], []))

    def test_buildLigGlyph_None(self):
        self.assertIsNone(builder.buildLigGlyph(None, None))

    def test_buildLigGlyph_points(self):
        lig = builder.buildLigGlyph(None, [2])
        self.assertEqual(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)
        self.assertEqual(getXML(lookup.toXML),
                         ['<Lookup>',
                          '  <LookupType value="1"/>',
                          '  <LookupFlag value="7"/>',
                          '  <!-- 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"})
        self.assertRaisesRegex(
            AssertionError, "if markFilterSet is None, "
            "flags must not set LOOKUP_FLAG_USE_MARK_FILTERING_SET; "
            "flags=0x0010",
            builder.buildLookup, [s],
            builder.LOOKUP_FLAG_USE_MARK_FILTERING_SET, None)
        self.assertRaisesRegex(
            AssertionError, "if markFilterSet is not None, "
            "flags must set LOOKUP_FLAG_USE_MARK_FILTERING_SET; "
            "flags=0x0004",
            builder.buildLookup, [s],
            builder.LOOKUP_FLAG_IGNORE_LIGATURES, 777)

    def test_buildLookup_conflictingSubtableTypes(self):
        s1 = builder.buildSingleSubstSubtable({"one": "two"})
        s2 = builder.buildAlternateSubstSubtable({"one": ["two", "three"]})
        self.assertRaisesRegex(
            AssertionError, "all subtables must have the same LookupType",
            builder.buildLookup, [s1, s2])

    def test_buildLookup_noSubtables(self):
        self.assertIsNone(builder.buildLookup([]))
        self.assertIsNone(builder.buildLookup(None))
        self.assertIsNone(builder.buildLookup([None]))
        self.assertIsNone(builder.buildLookup([None, 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)
        self.assertEqual(getXML(lookup.toXML),
                         ['<Lookup>',
                          '  <LookupType value="1"/>',
                          '  <LookupFlag value="17"/>',
                          '  <!-- 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)
        self.assertLess(self.GLYPHMAP["grave"], self.GLYPHMAP["acute"])
        self.assertEqual(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)
        self.assertEqual(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)
        self.assertEqual(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):
        self.assertIsNone(builder.buildMarkGlyphSetsDef([], self.GLYPHMAP))

    def test_buildMarkGlyphSetsDef_None(self):
        self.assertIsNone(builder.buildMarkGlyphSetsDef(None, self.GLYPHMAP))

    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)
        self.assertEqual(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))
        self.assertEqual(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)])
        self.assertEqual(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)
        self.assertEqual(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">',
                          '    </Class2Record>',
                          '    <Class2Record index="1">',
                          '    </Class2Record>',
                          '    <Class2Record index="2">',
                          '      <Value1 XPlacement="-80" YPlacement="-20"/>',
                          '      <Value2 XPlacement="-50"/>',
                          '    </Class2Record>',
                          '  </Class1Record>',
                          '  <Class1Record index="1">',
                          '    <Class2Record index="0">',
                          '    </Class2Record>',
                          '    <Class2Record index="1">',
                          '      <Value2 XPlacement="-20"/>',
                          '    </Class2Record>',
                          '    <Class2Record index="2">',
                          '      <Value1/>',
                          '      <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)
        self.assertEqual(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)
        self.assertEqual(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"/>',
                          '      <Value2 XPlacement="-50"/>',
                          '    </PairValueRecord>',
                          '    <PairValueRecord index="1">',
                          '      <SecondGlyph value="one"/>',
                          '      <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)
        self.assertEqual(sum([getXML(t.toXML) for t in subtables], []),
                         ['<SinglePos Format="1">',
                          '  <Coverage>',
                          '    <Glyph value="one"/>',
                          '    <Glyph value="two"/>',
                          '    <Glyph value="five"/>',
                          '  </Coverage>',
                          '  <ValueFormat value="1"/>',
                          '  <Value XPlacement="500"/>',
                          '</SinglePos>',
                          '<SinglePos Format="2">',
                          '  <Coverage>',
                          '    <Glyph value="three"/>',
                          '    <Glyph value="four"/>',
                          '  </Coverage>',
                          '  <ValueFormat value="1"/>',
                          '  <!-- ValueCount=2 -->',
                          '  <Value index="0" XPlacement="200"/>',
                          '  <Value index="1" XPlacement="400"/>',
                          '</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)
        self.assertEqual(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)
        self.assertEqual(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)
        self.assertEqual(getXML(subtable.toXML),
                         ['<SinglePos Format="2">',
                          '  <Coverage>',
                          '    <Glyph value="one"/>',
                          '    <Glyph value="two"/>',
                          '  </Coverage>',
                          '  <ValueFormat value="3"/>',
                          '  <!-- ValueCount=2 -->',
                          '  <Value index="0" XPlacement="777"/>',
                          '  <Value index="1" YPlacement="-888"/>',
                          '</SinglePos>'])

    def test_buildValue(self):
        value = builder.buildValue({"XPlacement": 7, "YPlacement": 23})
        func = lambda writer, font: value.toXML(writer, font, valueName="Val")
        self.assertEqual(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)
        self.assertEqual(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)
        self.assertEqual(keyA1, keyA2)
        self.assertEqual(hash(keyA1), hash(keyA2))
        self.assertNotEqual(keyA1, keyB)
        self.assertNotEqual(hash(keyA1), hash(keyB))


class ClassDefBuilderTest(unittest.TestCase):
    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()
        self.assertIsInstance(cdef, otTables.ClassDef)
        self.assertEqual(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()
        self.assertIsInstance(cdef, otTables.ClassDef)
        self.assertEqual(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"})
        self.assertTrue(b.canAdd({"a", "b", "c", "d"}))
        self.assertTrue(b.canAdd({"e", "f"}))
        self.assertTrue(b.canAdd({"g", "h", "i"}))
        self.assertFalse(b.canAdd({"b", "c", "d"}))
        self.assertFalse(b.canAdd({"a", "b", "c", "d", "e", "f"}))
        self.assertFalse(b.canAdd({"d", "e", "f"}))
        self.assertFalse(b.canAdd({"f"}))


if __name__ == "__main__":
    import sys
    sys.exit(unittest.main())