Example #1
0
 def test_substitution_single_in_contexts(self):
     parser = self.parser(
         'DEF_GROUP "Hebrew" ENUM GLYPH "uni05D0" GLYPH "uni05D1" '
         'END_ENUM END_GROUP\n'
         'DEF_LOOKUP "HebrewCurrency" PROCESS_BASE PROCESS_MARKS ALL '
         'DIRECTION LTR\n'
         'IN_CONTEXT\n'
         'RIGHT GROUP "Hebrew"\n'
         'RIGHT GLYPH "one.Hebr"\n'
         'END_CONTEXT\n'
         'IN_CONTEXT\n'
         'LEFT GROUP "Hebrew"\n'
         'LEFT GLYPH "one.Hebr"\n'
         'END_CONTEXT\n'
         'AS_SUBSTITUTION\n'
         'SUB GLYPH "dollar"\n'
         'WITH GLYPH "dollar.Hebr"\n'
         'END_SUB\n'
         'END_SUBSTITUTION')
     [group, lookup] = parser.parse().statements
     context1 = lookup.context[0]
     context2 = lookup.context[1]
     self.assertEqual(
         (lookup.name, context1.ex_or_in, context1.left, context1.right,
          context2.ex_or_in, context2.left, context2.right),
         ("HebrewCurrency", "IN_CONTEXT", [], [
             ast.Enum([ast.GroupName("Hebrew", parser)]),
             self.enum(["one.Hebr"])
         ], "IN_CONTEXT", [
             ast.Enum([ast.GroupName("Hebrew", parser)]),
             self.enum(["one.Hebr"])
         ], []))
Example #2
0
 def test_def_group_groups_not_yet_defined(self):
     parser = self.parser(
         'DEF_GROUP "Group1"\n'
         'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n'
         'END_GROUP\n'
         'DEF_GROUP "TestGroup1"\n'
         'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n'
         'END_GROUP\n'
         'DEF_GROUP "TestGroup2"\n'
         'ENUM GROUP "Group2" END_ENUM\n'
         'END_GROUP\n'
         'DEF_GROUP "TestGroup3"\n'
         'ENUM GROUP "Group2" GROUP "Group1" END_ENUM\n'
         'END_GROUP\n'
         'DEF_GROUP "Group2"\n'
         'ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n'
         'END_GROUP\n')
     [group1, test_group1, test_group2, test_group3, group2] = \
             parser.parse().statements
     self.assertEqual((test_group1.name, test_group1.enum),
                      ("TestGroup1",
                       ast.Enum([
                           ast.GroupName("Group1", parser),
                           ast.GroupName("Group2", parser)
                       ])))
     self.assertEqual(
         (test_group2.name, test_group2.enum),
         ("TestGroup2", ast.Enum([ast.GroupName("Group2", parser)])))
     self.assertEqual((test_group3.name, test_group3.enum),
                      ("TestGroup3",
                       ast.Enum([
                           ast.GroupName("Group2", parser),
                           ast.GroupName("Group1", parser)
                       ])))
Example #3
0
 def test_substitution_single_in_context(self):
     parser = self.parser(
         'DEF_GROUP "Denominators" ENUM GLYPH "one.dnom" GLYPH "two.dnom" '
         'END_ENUM END_GROUP\n'
         'DEF_LOOKUP "fracdnom" PROCESS_BASE PROCESS_MARKS ALL '
         'DIRECTION LTR\n'
         'IN_CONTEXT LEFT ENUM GROUP "Denominators" GLYPH "fraction" '
         'END_ENUM\n'
         'END_CONTEXT\n'
         'AS_SUBSTITUTION\n'
         'SUB GLYPH "one"\n'
         'WITH GLYPH "one.dnom"\n'
         'END_SUB\n'
         'SUB GLYPH "two"\n'
         'WITH GLYPH "two.dnom"\n'
         'END_SUB\n'
         'END_SUBSTITUTION')
     [group, lookup] = parser.parse().statements
     context = lookup.context[0]
     self.assertEqual((lookup.name, list(lookup.sub.mapping.items()),
                       context.ex_or_in, context.left, context.right),
                      ("fracdnom", [
                          (self.enum(["one"]), self.enum(["one.dnom"])),
                          (self.enum(["two"]), self.enum(["two.dnom"]))
                      ], "IN_CONTEXT", [
                          ast.Enum([
                              ast.GroupName("Denominators", parser=parser),
                              ast.GlyphName("fraction")
                          ])
                      ], []))
Example #4
0
 def test_substitution_reverse_chaining_single(self):
     parser = self.parser(
         'DEF_GLYPH "zero" ID 1 UNICODE 48 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "one" ID 2 UNICODE 49 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "two" ID 3 UNICODE 50 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "three" ID 4 UNICODE 51 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "four" ID 5 UNICODE 52 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "five" ID 6 UNICODE 53 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "six" ID 7 UNICODE 54 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "seven" ID 8 UNICODE 55 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "eight" ID 9 UNICODE 56 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "nine" ID 10 UNICODE 57 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "zero.numr" ID 11 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "one.numr" ID 12 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "two.numr" ID 13 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "three.numr" ID 14 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "four.numr" ID 15 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "five.numr" ID 16 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "six.numr" ID 17 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "seven.numr" ID 18 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "eight.numr" ID 19 TYPE BASE END_GLYPH\n'
         'DEF_GLYPH "nine.numr" ID 20 TYPE BASE END_GLYPH\n'
         'DEF_LOOKUP "numr" PROCESS_BASE PROCESS_MARKS ALL '
         'DIRECTION LTR REVERSAL\n'
         'IN_CONTEXT\n'
         'RIGHT ENUM '
         'GLYPH "fraction" '
         'RANGE "zero.numr" TO "nine.numr" '
         'END_ENUM\n'
         'END_CONTEXT\n'
         'AS_SUBSTITUTION\n'
         'SUB RANGE "zero" TO "nine"\n'
         'WITH RANGE "zero.numr" TO "nine.numr"\n'
         'END_SUB\n'
         'END_SUBSTITUTION')
     lookup = parser.parse().statements[-1]
     self.assertEqual(
         (lookup.name, lookup.context[0].right,
          list(lookup.sub.mapping.items())),
         ("numr", [(ast.Enum([
             ast.GlyphName("fraction"),
             ast.Range("zero.numr", "nine.numr", parser)
         ]))], [(ast.Enum([ast.Range("zero", "nine", parser)]),
                 ast.Enum([ast.Range("zero.numr", "nine.numr", parser)]))]))
Example #5
0
def _kern_coverage(names, classes=None):
    ret = []
    for name in names:
        if name.startswith('@'):
            name = name[1:]
            if classes is not None:
                glyphs = tuple(ast.GlyphName(g) for g in sorted(classes[name]))
                ret.append([ast.Enum(glyphs)])
            else:
                ret.append([ast.GroupName(f'KERN{name}', None)])
        else:
            ret.append([ast.GlyphName(name)])
    return ret
Example #6
0
 def test_def_group_groups(self):
     parser = self.parser(
         'DEF_GROUP "Group1"\n'
         'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n'
         'END_GROUP\n'
         'DEF_GROUP "Group2"\n'
         'ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n'
         'END_GROUP\n'
         'DEF_GROUP "TestGroup"\n'
         'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n'
         'END_GROUP\n')
     [group1, group2, test_group] = parser.parse().statements
     self.assertEqual((test_group.name, test_group.enum),
                      ("TestGroup",
                       ast.Enum([
                           ast.GroupName("Group1", parser),
                           ast.GroupName("Group2", parser)
                       ])))
Example #7
0
 def parse_coverage_(self):
     coverage = []
     location = self.cur_token_location_
     while self.next_token_ in ("GLYPH", "GROUP", "RANGE", "ENUM"):
         if self.next_token_ == "ENUM":
             self.advance_lexer_()
             enum = self.parse_enum_()
             coverage.append(enum)
         elif self.next_token_ == "GLYPH":
             self.expect_keyword_("GLYPH")
             name = self.expect_string_()
             coverage.append(ast.GlyphName(name, location=location))
         elif self.next_token_ == "GROUP":
             self.expect_keyword_("GROUP")
             name = self.expect_string_()
             coverage.append(ast.GroupName(name, self, location=location))
         elif self.next_token_ == "RANGE":
             self.expect_keyword_("RANGE")
             start = self.expect_string_()
             self.expect_keyword_("TO")
             end = self.expect_string_()
             coverage.append(ast.Range(start, end, self, location=location))
     return ast.Enum(coverage, location=location)
Example #8
0
 def parse_enum_(self):
     self.expect_keyword_("ENUM")
     location = self.cur_token_location_
     enum = ast.Enum(self.parse_coverage_(), location=location)
     self.expect_keyword_("END_ENUM")
     return enum
Example #9
0
def exportVoltAnchors(font):
    glyphOrder = [g.name for g in font]

    # Save groups and lookups files for each master.
    for master in font.masters:
        # Collect anchors that need mkmk lookups (base glyph is a mark).
        mkmk = set()
        for glyph in font:
            layer = glyph.layers[master.name]
            for anchor in layer.anchors:
                name = anchor.name
                if not name.startswith('_') and glyph.openTypeGlyphClass == 3:
                    mkmk.add(name.split('_')[0])

        # Collect anchors that need ligature lookups (anchor with index).
        ligs = set()
        for glyph in font:
            layer = glyph.layers[master.name]
            for anchor in layer.anchors:
                name = anchor.name
                if name.startswith('_'):
                    name = name[1:]
                if '_' in name:
                    ligs.add(name.split('_')[0])

        lookups = {}
        groups = {}
        anchors = []
        for glyph in font:
            layer = glyph.layers[master.name]
            for anchor in layer.anchors:
                x = otRound(anchor.x)
                y = otRound(anchor.y)
                if anchor.name.startswith('_'):
                    # Mark anchor.
                    if glyph.openTypeGlyphClass != 3:
                        # Not a mark glyph? Ignore the anchor or VOLT will error.
                        continue

                    name = anchor.name[1:]

                    # Add to groups. We build groups for mark glyphs by anchor.
                    group = f'MARK_{name}'
                    if group not in groups:
                        groups[group] = set()
                    groups[group].add(glyph.name)

                    # mkmk anchors are added to both mark and mkmk lookups
                    # because they might be used with non-mark bases after
                    # anchor propagation.
                    lookupnames = [f'mark_{name}']
                    if name in mkmk:
                        lookupnames.append(f'mkmk_{name}')
                    if name in ligs:
                        lookupnames.append(f'mark_{name}_ligs')

                    # Add the glyph to respective lookup(s).
                    for lookupname in lookupnames:
                        if lookupname not in lookups:
                            lookups[lookupname] = _attachment_lookup(
                                lookupname)

                        # For mkmk lookups we use individual glyphs, for mark
                        # lookups we use groups. There is no technical reason
                        # for this, just how JH likes it.
                        if lookupname.startswith('mkmk'):
                            to = ([ast.GlyphName(glyph.name)], name)
                            lookups[lookupname].pos.coverage_to.append(to)
                        elif not lookups[lookupname].pos.coverage_to:
                            to = ([ast.GroupName(group, None)], name)
                            lookups[lookupname].pos.coverage_to.append(to)

                    # Add the anchor.
                    name, comp = f'MARK_{name}', 1
                    pos = ast.Pos(None, x, y, {}, {}, {})
                    gid = glyphOrder.index(glyph.name)
                    anchors.append(
                        ast.AnchorDefinition(name, gid, glyph.name, comp,
                                             False, pos))
                else:
                    # Base anchor.
                    name, comp = anchor.name, 1
                    lookupname = f'mark_{name}'
                    if '_' in name:
                        # Split ligature anchor (e.g. “top_1” and use the
                        # number for ligature component.
                        name, comp = name.split('_')
                        lookupname = f'mark_{name}_ligs'

                    if glyph.openTypeGlyphClass == 3:
                        # If this is a mark glyph, then add to mkmk lookup.
                        lookupname = f'mkmk_{name}'

                    # Add the glyph to respective lookup.
                    if lookupname not in lookups:
                        lookups[lookupname] = _attachment_lookup(lookupname)
                    lookups[lookupname].pos.coverage.add(glyph.name)

                    # Add the anchor.
                    pos = ast.Pos(None, x, y, {}, {}, {})
                    gid = glyphOrder.index(glyph.name)
                    anchors.append(
                        ast.AnchorDefinition(name, gid, glyph.name, comp,
                                             False, pos))

        # Save groups file.
        with open(master.psn + '-anchors.vtg', 'w') as fp:
            doc = ast.VoltFile()
            for group in groups:
                glyphs = tuple(ast.GlyphName(g) for g in sorted(groups[group]))
                enum = ast.Enum(glyphs)
                doc.statements.append(ast.GroupDefinition(group, enum))
            fp.write(str(doc))

        # Save lookups file.
        with open(master.psn + '-anchors.vtl', 'w') as fp:
            doc = ast.VoltFile()
            for lookup in lookups.values():
                # Sort coverage by glyph ID to be stable.
                lookup.pos.coverage = sorted(
                    [ast.GlyphName(g) for g in lookup.pos.coverage],
                    key=lambda g: glyphOrder.index(g.glyph),
                )
                doc.statements.append(lookup)
            doc.statements += anchors
            fp.write(str(doc))
Example #10
0
def exportVoltKerning(font):
    # Save groups and lookups files for each master.
    for master in font.masters:
        classes = {k.name: k.names for k in master.kerning.classes}
        pairs = master.kerning.pairs.copy()

        format1 = _pair_lookup(r'kern\1_PPF1')
        format2 = _pair_lookup(r'kern\2_PPF2')
        nullpos = ast.Pos(None, None, None, {}, {}, {})

        # Left exceptions
        filtered = {}
        for left in pairs:
            if left.startswith('@'):
                names = []
                for name in classes[left[1:]]:
                    if name in pairs:
                        pairs[name] = {**pairs[left], **pairs[name]}
                    else:
                        names.append(name)
                filtered[left[1:]] = names

        # Right exceptions
        rights = set()
        for left in pairs:
            rights.update(pairs[left])

        for right in sorted(rights):
            if right.startswith('@'):
                names = []
                for name in classes[right[1:]]:
                    if name in rights:
                        for left in pairs:
                            if right in pairs[left]:
                                pairs[left][name] = pairs[left][right]
                    else:
                        names.append(name)
                filtered[right[1:]] = names

        groups = set()
        values = {}
        for left in pairs:
            for right, value in pairs[left].items():
                lookup = format1
                if left.startswith('@') and right.startswith('@'):
                    lookup = format2
                    groups.update([left[1:], right[1:]])
                else:
                    if left.startswith('@') and not filtered[left[1:]]:
                        continue
                    if right.startswith('@') and not filtered[right[1:]]:
                        continue

                if left not in lookup.pos.coverages_1:
                    lookup.pos.coverages_1.append(left)
                id1 = lookup.pos.coverages_1.index(left) + 1

                if right not in lookup.pos.coverages_2:
                    lookup.pos.coverages_2.append(right)
                id2 = lookup.pos.coverages_2.index(right) + 1

                pos = ast.Pos(otRound(float(value)), None, None, {}, {}, {})
                lookup.pos.adjust_pair[(id1, id2)] = (pos, nullpos)

        _warn_overlapping_classes(master, groups)

        # Save groups file.
        with open(master.psn + '-kerning.vtg', 'w') as fp:
            doc = ast.VoltFile()
            for name in sorted(groups):
                glyphs = tuple(ast.GlyphName(g) for g in sorted(classes[name]))
                enum = ast.Enum(glyphs)
                doc.statements.append(ast.GroupDefinition(f'KERN{name}', enum))
            fp.write(str(doc))

        # Save lookups file.
        with open(master.psn + '-kerning.vtl', 'w') as fp:
            format1.pos.coverages_1 = _kern_coverage(format1.pos.coverages_1,
                                                     filtered)
            format1.pos.coverages_2 = _kern_coverage(format1.pos.coverages_2,
                                                     filtered)

            format2.pos.coverages_1 = _kern_coverage(format2.pos.coverages_1)
            format2.pos.coverages_2 = _kern_coverage(format2.pos.coverages_2)

            doc = ast.VoltFile()
            doc.statements = [format1, format2]
            fp.write(str(doc))
Example #11
0
 def enum(self, glyphs):
     return ast.Enum([ast.GlyphName(g) for g in glyphs])