Example #1
0
class GDEF(object, metaclass=simplemeta.FontDataMetaclass):
    """
    Objects representing entire GDEF tables. These are simple objects with the
    following attribute values:
        version         A Version object.

        attachments     An attachlist.AttachListTable object, or None.

        glyphClasses    A glyphclass.GlyphClassTable object, or None.

        ligCarets       A ligcaret.LigCaretTable object, or None.

        markClasses     A markclass.MarkClassTable object, or None.

        markSets        A markset.MarkSetTable object, or None.

    <<< _testingValues[1].pprint(namer=namer.testingNamer())
    Version:
      Major version: 1
      Minor version: 3
    Glyph Classes:
      Base glyph: xyz8
      Ligature glyph: xyz5 - xyz7, xyz11 - xyz12, xyz16
    Attachment Points:
      afii60003: [1, 4]
      xyz46: [3, 19, 20]
      xyz51: [1, 4]
    Ligature Caret Points:
      afii60003:
        CaretValue #1:
          Caret value in FUnits: 500
          Device record:
            Tweak at 12 ppem: -2
            Tweak at 13 ppem: -1
            Tweak at 16 ppem: 1
            Tweak at 17 ppem: 2
      xyz72:
        CaretValue #1:
          Simple caret value in FUnits: 500
    Mark Classes:
      Mark class 1: xyz8
      Mark class 2: xyz5 - xyz7, xyz11 - xyz12, xyz16
    Mark Sets:
      Mark Glyph Set 0: xyz16, xyz23 - xyz24, xyz31
      Mark Glyph Set 1: xyz13, U+0163
      Mark Glyph Set 2: xyz16, xyz23 - xyz24, xyz31
    """

    #
    # Class definition variables
    #

    attrSpec = dict(
        version=dict(
            attr_followsprotocol=True,
            attr_initfunc=lambda: otversion.Version(minor=3),
            attr_label="Version"),

        glyphClasses=dict(
            attr_followsprotocol=True,
            attr_initfunc=glyphclass.GlyphClassTable,
            attr_label="Glyph Classes",
            attr_showonlyiftrue=True),

        attachments=dict(
            attr_followsprotocol=True,
            attr_initfunc=attachlist.AttachListTable,
            attr_label="Attachment Points",
            attr_showonlyiftrue=True),

        ligCarets=dict(
            attr_followsprotocol=True,
            attr_initfunc=ligcaret.LigCaretTable,
            attr_label="Ligature Caret Points",
            attr_showonlyiftrue=True),

        markClasses=dict(
            attr_followsprotocol=True,
            attr_initfunc=markclass.MarkClassTable,
            attr_label="Mark Classes",
            attr_showonlyiftrue=True),

        markSets=dict(
            attr_followsprotocol=True,
            attr_initfunc=markset.MarkSetTable,
            attr_label="Mark Sets",
            attr_showonlyiftrue=True))

    attrSorted = (
      'version',
      'glyphClasses',
      'attachments',
      'ligCarets',
      'markClasses',
      'markSets')

    #
    # Methods
    #

    def buildBinary(self, w, **kwArgs):
        """
        Adds the binary data to the specified LinkedWriter.

        A single kwArg is recognized:

            'otIVS'    if present, should be a tuple of (ivsBinaryString,
                       LivingDelta-to-DeltaSetIndexMap). The ivsBinaryString
                       will be written here. The map is used to write various
                       structures that reference the deltas.

        <<< utilities.hexdump(_testingValues[1].binaryString())
               0 | 0001 0003 0012 002E  0050 007C 0098 0000 |.........P.|....|
              10 | 0000 0002 0004 0004  0006 0002 0007 0007 |................|
              20 | 0001 000A 000B 0002  000F 000F 0002 000A |................|
              30 | 0003 0014 001C 001C  0001 0003 002D 0032 |.............-.2|
              40 | 0062 0003 0003 0013  0014 0002 0001 0004 |.b..............|
              50 | 0008 0002 0010 0014  0001 0002 0047 0062 |.............G.b|
              60 | 0001 0008 0001 0008  0001 01F4 0003 01F4 |................|
              70 | 0006 000C 0011 0002  EF00 1200 0002 0004 |................|
              80 | 0004 0006 0002 0007  0007 0001 000A 000B |................|
              90 | 0002 000F 000F 0002  0001 0003 0000 0010 |................|
              A0 | 0000 001C 0000 0010  0001 0004 000F 0016 |................|
              B0 | 0017 001E 0001 0002  000C 0063           |...........c    |
        """

        if 'stakeValue' in kwArgs:
            stakeValue = kwArgs.pop('stakeValue')
            w.stakeCurrentWithValue(stakeValue)
        else:
            stakeValue = w.stakeCurrent()

        editor = kwArgs.get('editor')
        if editor is not None:
            if editor.reallyHas(b'fvar'):
                ao = editor.fvar.axisOrder

            gdce = getattr(self, '_creationExtras', {})
            otcd = gdce.get('otcommondeltas')
            if otcd:
                bsfd = living_variations.IVS.binaryStringFromDeltas
                ivsBs, LDtoDSMap = bsfd(otcd, axisOrder=ao)

        # Add content and unresolved references
        w.add("L", 0x10003)
        d = self.__dict__
        toBeWritten = []

        san = list(self.getSortedAttrNames())
        san.remove('version')
        for key in san:
            table = d[key]

            if table:
                tableStake = w.getNewStake()
                toBeWritten.append((table, tableStake))
                w.addUnresolvedOffset("H", stakeValue, tableStake)
            else:
                w.add("H", 0)

        if ivsBs:
            ivsStake = w.getNewStake()
            w.addUnresolvedOffset("L", stakeValue, ivsStake)
        else:
            w.add("L", 0)

        # Resolve subtable references
        for table, tableStake in toBeWritten:
            table.buildBinary(w, stakeValue=tableStake, **kwArgs)

        if ivsBs:
            w.stakeCurrentWithValue(ivsStake)
            w.addString(ivsBs)

    @classmethod
    def frommtxxtables(cls, editor, **kwArgs):
        """
        Returns a GDEF object initialized via the MTxx (and other) tables in
        the specified editor.
        """

        if editor is None:
            return cls()

        hm = editor.get(b'hmtx')

        if not hm:
            return cls()

        sf = editor.get(b'MTsf', {})
        # when there's a MTxx table for marksets, add that here

        glyphClasses = glyphclass.GlyphClassTable()
        attachments = attachlist.AttachListTable()
        ligCarets = ligcaret.LigCaretTable()
        markClasses = markclass.MarkClassTable()
        markSets = markset.MarkSetTable()
        excluded = frozenset([0xFFFF, 0xFFFE])

        for glyphIndex, mtapRecord in editor.get(b'MTap', {}).items():
            # glyph class
            glyphClass = mtapRecord.glyphClass

            if glyphClass:
                glyphClasses[glyphIndex] = glyphClass

            # attachments
            anchor = mtapRecord.anchorData

            t1 = (
              () if anchor is None
              else tuple(anchor.pointGenerator(excludeFFFx=True)))

            conn = mtapRecord.connectionData

            if conn is None:
                t2 = ()

            else:
                it = conn.pointGenerator()

                t2 = tuple(
                  c.pointIndex
                  for c in it
                  if c.pointIndex not in excluded)

            if t1 or t2:
                attachments[glyphIndex] = attachlist.AttachPointTable(t1 + t2)

            # lig carets
            if glyphClass == 2:  # ligature
                caretValues = mtapRecord.caretValuesFromMetrics(
                  hm[glyphIndex][0])

                if caretValues:
                    ligCarets[glyphIndex] = ligcaret.LigGlyphTable(caretValues)

            # marks
            elif glyphClass == 3 and sf:  # mark
                markClass = sf[glyphIndex].originalClassIndex

                markClasses[glyphIndex] = (
                  0 if markClass == 0xFFFF
                  else markClass + 1)

        return cls(
          attachments=attachments,
          glyphClasses=glyphClasses,
          ligCarets=ligCarets,
          markClasses=markClasses,
          markSets=markSets)

    @classmethod
    def fromValidatedFontWorkerSource(cls, fws, **kwArgs):
        """
        Creates and returns a new GDEF from the specified FontWorker Source
        code with extensive validation via the logging module (the client
        should have done a logging.basicConfig call prior to calling this
        method, unless a logger is passed in via the 'logger' keyword
        argument).

        >>> _test_FW_fws.goto(1) # go back to first line
        >>> logger = utilities.makeDoctestLogger('test_FW')
        >>> fvfws = GDEF.fromValidatedFontWorkerSource
        >>> gdef = fvfws(_test_FW_fws, namer=_test_FW_namer,
        ...   logger=logger, editor=None)
        >>> gdef.pprint()
        Version:
          Major version: 1
          Minor version: 3
        Glyph Classes:
          Base glyph: 5
          Ligature glyph: 7
          Mark glyph: 11
        Attachment Points:
          5: [23]
          7: [29, 31]
          11: [37, 41, 43]
        Ligature Caret Points:
          5:
            CaretValue #1:
              Simple caret value in FUnits: 42
          7:
            CaretValue #1:
              Simple caret value in FUnits: 50
            CaretValue #2:
              Simple caret value in FUnits: 55
          11:
            CaretValue #1:
              Simple caret value in FUnits: 12
            CaretValue #2:
              Simple caret value in FUnits: 17
            CaretValue #3:
              Simple caret value in FUnits: 37
        Mark Classes:
          Mark class 8: 5
          Mark class 10: 7
          Mark class 12: 11
        Mark Sets:
          Mark Glyph Set 0: 5, 7
          Mark Glyph Set 1: 5
          Mark Glyph Set 2: 11
        >>> gdef2 = fvfws(_test_FW_fws2, namer=_test_FW_namer,
        ...   logger=logger, editor=None)
        test_FW.GDEF.classDef - WARNING - line 8 -- glyph 'w' not found
        test_FW.GDEF.attachlist - WARNING - line 13 -- missing attachment points
        test_FW.GDEF.attachlist - WARNING - line 16 -- glyph 'x' not found
        test_FW.GDEF.ligcaret - ERROR - line 21 -- mismatch between caret count (2);and number of carets actually listed (3); discarding.
        test_FW.GDEF.ligcaret - ERROR - line 23 -- mismatch between caret count (3);and number of carets actually listed (2); discarding.
        test_FW.GDEF.ligcaret - WARNING - line 24 -- glyph 'y' not found
        test_FW.GDEF.classDef - WARNING - line 31 -- incorrect number of tokens, expected 2, found 1
        test_FW.GDEF.classDef - WARNING - line 32 -- glyph 'z' not found
        test_FW.GDEF.markset - WARNING - line 40 -- glyph 'asdfjkl' not found
        test_FW.GDEF.markset - WARNING - MarkFilterSet 1 will be renumbered to 0
        test_FW.GDEF.markset - WARNING - MarkFilterSet 99 will be renumbered to 1
        >>> gdef2.pprint()
        Version:
          Major version: 1
          Minor version: 3
        Glyph Classes:
          Base glyph: 5
          Ligature glyph: 7
          Mark glyph: 11
        Attachment Points:
          5: [23]
          11: [37, 41, 43]
        Ligature Caret Points:
          5:
            CaretValue #1:
              Simple caret value in FUnits: 42
        Mark Classes:
          Mark class 1: 5
          Mark class 2: 7
        Mark Sets:
          Mark Glyph Set 0: 5, 11
          Mark Glyph Set 1: 5, 7
        """

        editor = kwArgs['editor']
        namer = kwArgs['namer']

        logger = kwArgs.pop('logger', None)

        if logger is None:
            logger = logging.getLogger().getChild('GDEF')
        else:
            logger = logger.getChild('GDEF')

        # create empty versions of these tables for now,
        # replace them with full versions later if the data is available
        glyphClasses = glyphclass.GlyphClassTable()
        attachments = attachlist.AttachListTable()
        ligCarets = ligcaret.LigCaretTable()
        markClasses = markclass.MarkClassTable()
        markSets = markset.MarkSetTable()

        firstLine = 'FontDame GDEF table'
        line = next(fws)
        if line != firstLine:
            logger.error((
              'V0958',
              (firstLine,),
              "Expected '%s' in first line."))
            return None

        for line in fws:
            if len(line) != 0:
                if line.lower() == 'class definition begin':
                    glyphClasses = \
                        glyphclass.GlyphClassTable.fromValidatedFontWorkerSource(
                          fws, namer=namer, logger=logger)
                elif line.lower() == 'attachment list begin':
                    attachments = \
                        attachlist.AttachListTable.fromValidatedFontWorkerSource(
                          fws, namer=namer, logger=logger)
                elif line.lower() == 'carets begin':
                    ligCarets = \
                        ligcaret.LigCaretTable.fromValidatedFontWorkerSource(
                          fws, namer=namer, logger=logger, editor=editor)
                elif line.lower() == 'mark attachment class definition begin':
                    markClasses = \
                        markclass.MarkClassTable.fromValidatedFontWorkerSource(
                          fws, namer=namer, logger=logger)
                elif line.lower() == 'markfilter set definition begin':
                    markSets = \
                        markset.MarkSetTable.fromValidatedFontWorkerSource(
                          fws, namer=namer, logger=logger)
                else:
                    logger.warning((
                      'V0960',
                      (fws.lineNumber, line),
                      'line %d -- unexpected token: \'%s\''))

        return cls(
            attachments=attachments,
            glyphClasses=glyphClasses,
            ligCarets=ligCarets,
            markClasses=markClasses,
            markSets=markSets)

    @classmethod
    def fromvalidatedwalker(cls, w, **kwArgs):
        """
        Like fromwalker(), this method returns a new GDEF. However, it also
        does extensive validation via the logging module (the client should
        have done a logging.basicConfig call prior to calling this method,
        unless a logger is passed in via the 'logger' keyword argument).

        >>> ed = utilities.fakeEditor(5)
        >>> from fontio3.fvar import fvar, axial_coordinate, axis_info
        >>> acmin = axial_coordinate.AxialCoordinate(300)
        >>> acdef = axial_coordinate.AxialCoordinate(400)
        >>> acmax = axial_coordinate.AxialCoordinate(700)
        >>> wght_ax_info = axis_info.AxisInfo(
        ...   minValue=acmin, defaultValue=acdef, maxValue=acmax)
        >>> ed.fvar = fvar.Fvar({'wght': wght_ax_info}, axisOrder=('wght',))
        >>> logger = utilities.makeDoctestLogger('test')
        >>> s = ("00010003"  # version
        ...      "0012"      # GlyphClassDef offset
        ...      "001A"      # AttachList offset
        ...      "002A"      # LigCaretList offset
        ...      "0046"      # MarkAttachClassDef offset
        ...      "0050"      # MarkGlyphSetsDef offset
        ...      "0000005E"  # ItemVarStore offset
        ...      "0001 0001 0001 0003"  # GlyphClassDef data
        ...      "0006 0001 000C  0001 0001 0004  0001 0005" # AttachList data
        ...      "0006 0001 000C  0001 0001 0003"  # LigCaret partial data
        ...      "0001 0004  0003 0048 0006  0000 0000 8000"  # LigCaret data
        ...      "0002 0001 0000 0004 0003"  # MarkAttachClassDef data
        ...      "0001 0001 00000008  0001 0001 0002"  # MarkGlyphSetsDef
        ...      "0001 0000000C 0001 0000001C"  # IVS data
        ...      "0001 0001 E000 1000 4000 C000 0000 4000"  # IVS data
        ...      "0001 0001 0001 0000 FF4C"  # IVS data
        ...      )
        >>> b = utilities.fromhex(s)
        >>> fvb = GDEF.fromvalidatedbytes
        >>> obj = fvb(b, logger=logger, editor=ed)
        test.GDEF - DEBUG - Walker has 132 remaining bytes.
        test.GDEF.version - DEBUG - Walker has 132 remaining bytes.
        test.GDEF.IVS - DEBUG - Walker has 38 remaining bytes.
        test.GDEF.IVS - INFO - Format 1
        test.GDEF.IVS - DEBUG - Data count is 1
        test.GDEF.IVS - DEBUG - Axis count is 1
        test.GDEF.IVS - DEBUG - Region count is 1
        test.GDEF.IVS - DEBUG - Delta (0, 0)
        test.GDEF.glyphclass.classDef - DEBUG - Walker has 114 remaining bytes.
        test.GDEF.glyphclass.classDef - DEBUG - ClassDef is format 1.
        test.GDEF.glyphclass.classDef - DEBUG - First is 1, and count is 1
        test.GDEF.glyphclass.classDef - DEBUG - Raw data are (3,)
        test.GDEF.attachlist - DEBUG - Walker has 106 remaining bytes.
        test.GDEF.attachlist.coverage - DEBUG - Walker has 100 remaining bytes.
        test.GDEF.attachlist.coverage - DEBUG - Format is 1, count is 1
        test.GDEF.attachlist.coverage - DEBUG - Raw data are [4]
        test.GDEF.attachlist - INFO - Number of groups: 1.
        test.GDEF.ligcaret - DEBUG - Walker has 90 remaining bytes.
        test.GDEF.ligcaret.coverage - DEBUG - Walker has 84 remaining bytes.
        test.GDEF.ligcaret.coverage - DEBUG - Format is 1, count is 1
        test.GDEF.ligcaret.coverage - DEBUG - Raw data are [3]
        test.GDEF.ligcaret - INFO - Number of LigGlyphTables: 1.
        test.GDEF.ligcaret.[0].ligglyph - DEBUG - Walker has 78 remaining bytes.
        test.GDEF.ligcaret.[0].ligglyph - INFO - Number of splits: 1.
        test.GDEF.ligcaret.[0].ligglyph.[0].caretvalue.variation - DEBUG - Walker has 74 remaining bytes.
        test.GDEF.ligcaret.[0].ligglyph.[0].caretvalue.variation.device - DEBUG - Walker has 68 remaining bytes.
        test.GDEF.ligcaret.[0].ligglyph.[0].caretvalue.variation.device - DEBUG - VariationIndex (0, 0)
        test.GDEF.ligcaret.[0].ligglyph.[0].caretvalue.variation - DEBUG - LivingDeltas ('wght': (start -0.5, peak 0.25, end 1.0), -180)
        test.GDEF.markclass.classDef - DEBUG - Walker has 62 remaining bytes.
        test.GDEF.markclass.classDef - DEBUG - ClassDef is format 2.
        test.GDEF.markclass.classDef - DEBUG - Count is 1
        test.GDEF.markclass.classDef - DEBUG - Raw data are [(0, 4, 3)]
        test.GDEF.markset - DEBUG - Walker has 52 remaining bytes.
        test.GDEF.markset - INFO - MarkSet has 1 element(s).
        test.GDEF.markset.[0].coverage - DEBUG - Walker has 44 remaining bytes.
        test.GDEF.markset.[0].coverage - DEBUG - Format is 1, count is 1
        test.GDEF.markset.[0].coverage - DEBUG - Raw data are [2]

        >>> fvb(b[:1], logger=logger, editor=ed)
        test.GDEF - DEBUG - Walker has 1 remaining bytes.
        test.GDEF - ERROR - Insufficient bytes.
        """

        logger = kwArgs.pop('logger', None)
        editor = kwArgs.pop('editor')

        if logger is None:
            logger = logging.getLogger().getChild('GDEF')
        else:
            logger = logger.getChild('GDEF')

        logger.debug((
          'V0001',
          (w.length(),),
          "Walker has %d remaining bytes."))

        if w.length() < 4:
            logger.error(('V0004', (), "Insufficient bytes."))
            return None

        version = otversion.Version.fromvalidatedwalker(w, logger=logger, **kwArgs)

        if version.major != 1 or version.minor != 3:
            logger.error((
              'V0113',
              (version,),
              "Expected version 1.3, got %s."))

            return None

        if w.length() < 8:
            logger.error(('V0114', (), "Insufficient bytes for offsets."))
            return None

        r = cls(version=version)

        stoffsets = w.group("H", 5)
        oIVS = w.unpack("L")

        # Get IVS first, since it may be used by the LigCaretTable.
        if oIVS:
            wSub = w.subWalker(oIVS)
            ao = editor.fvar.axisOrder
            fvw = living_variations.IVS.fromvalidatedwalker
            ivs = fvw(wSub, logger=logger, axisOrder=ao, **kwArgs)
            r.__dict__['_creationExtras'] = {'otcommondeltas': ivs}
            kwArgs['otcommondeltas'] = ivs

        san = list(r.getSortedAttrNames())
        loggers = [logger] * 5
        loggers[0] = logger.getChild("glyphclass")
        loggers[3] = logger.getChild("markclass")
        for sti, sto in enumerate(stoffsets):
            if sto:
                fw = _makers[sti].fromvalidatedwalker
                wSub = w.subWalker(sto)
                st = fw(wSub, logger=loggers[sti], **kwArgs)
                r.__dict__[san[sti+1]] = st
            else:
                r.__dict__[san[sti+1]] = _makers[sti]()

        return r

    @classmethod
    def fromwalker(cls, w, **kwArgs):
        """
        Creates and returns a new GDEF object from the specified walker.

        >>> ed = utilities.fakeEditor(5)
        >>> from fontio3.fvar import fvar, axial_coordinate, axis_info
        >>> acmin = axial_coordinate.AxialCoordinate(300)
        >>> acdef = axial_coordinate.AxialCoordinate(400)
        >>> acmax = axial_coordinate.AxialCoordinate(700)
        >>> wght_ax_info = axis_info.AxisInfo(
        ...   minValue=acmin, defaultValue=acdef, maxValue=acmax)
        >>> ed.fvar = fvar.Fvar({'wdth': wght_ax_info}, axisOrder=('wdth',))
        >>> s = ("00010003"  # version
        ...      "0012"      # GlyphClassDef offset
        ...      "001A"      # AttachList offset
        ...      "002A"      # LigCaretList offset
        ...      "0046"      # MarkAttachClassDef offset
        ...      "0050"      # MarkGlyphSetsDef offset
        ...      "0000005E"  # ItemVarStore offset
        ...      "0001 0001 0001 0002"  # GlyphClassDef data
        ...      "0006 0001 000C  0001 0001 0004  0001 000A" # AttachList data
        ...      "0006 0001 000C  0001 0001 0003"  # LigCaret partial data
        ...      "0001 0004  0003 0048 0006  0000 0000 8000"  # LigCaret data
        ...      "0002 0001 0000 0004 0003"  # MarkAttachClassDef data
        ...      "0001 0001 00000008  0001 0001 0002"  # MarkGlyphSetsDef
        ...      "0001 0000000C 0001 0000001C"  # IVS data
        ...      "0001 0001 E000 1000 4000 C000 0000 4000"  # IVS data
        ...      "0001 0001 0001 0000 FF4E"  # IVS data
        ...      )
        >>> b = utilities.fromhex(s)
        >>> obj = GDEF.frombytes(b, editor=ed)
        >>> obj.pprint()
        Version:
          Major version: 1
          Minor version: 3
        Glyph Classes:
          Ligature glyph: 1
        Attachment Points:
          4: [10]
        Ligature Caret Points:
          3:
            CaretValue #1:
              Caret value in FUnits: 72
              Variation Record:
                A delta of -178 applies in region 'wdth': (start -0.5, peak 0.25, end 1.0)
        Mark Classes:
          Mark class 3: 0-4
        Mark Sets:
          Mark Glyph Set 0: 2
        """

        editor = kwArgs.pop('editor')

        version = otversion.Version.fromwalker(w, **kwArgs)

        if version.major != 1 or version.minor != 3:
            raise ValueError("Unsupported GDEF %s" % (version,))

        r = cls(version=version)

        stoffsets = w.group("H", 5)
        oIVS = w.unpack("L")

        # Get IVS first, since it may be used by the LigCaretTable.
        if oIVS:
            wSub = w.subWalker(oIVS)
            ao = editor.fvar.axisOrder
            ivs = living_variations.IVS.fromwalker(wSub, axisOrder=ao, **kwArgs)
            r.__dict__['_creationExtras'] = {'otcommondeltas': ivs}
            kwArgs['otcommondeltas'] = ivs

        san = list(r.getSortedAttrNames())
        for sti, sto in enumerate(stoffsets):
            if sto:
                fw = _makers[sti].fromwalker
                wSub = w.subWalker(sto)
                st = fw(wSub, **kwArgs)
                r.__dict__[san[sti+1]] = st
            else:
                r.__dict__[san[sti+1]] = _makers[sti]()

        return r

    def writeFontWorkerSource(self, s, **kwArgs):
        """
        Write the GDEF table to stream 's' in Font Worker dump format.
        kwArg 'namer' is required.
        """
        namer = kwArgs.get('namer')
        bnfgi = namer.bestNameForGlyphIndex

        s.write("FontDame GDEF table\n\n")

        if self.glyphClasses:
            s.write("class definition begin\n")
            for gi in sorted(
              self.glyphClasses,
              key=lambda x: (self.glyphClasses[x], x)):
                gc = self.glyphClasses[gi]
                s.write("%s\t%d\n" % (bnfgi(gi), gc))
            s.write("class definition end\n\n")

        if self.attachments:
            s.write("attachment list begin\n")
            for gi, atl in sorted(self.attachments.items()):
                s.write("%s\t%s\n" % (bnfgi(gi), "\t".join([str(at) for at in atl])))
            s.write("attachment list end\n\n")

        if self.ligCarets:
            s.write("carets begin\n")
            for gi, lc in sorted(self.ligCarets.items()):
                s.write("%s\t%d\t%s\n" % (
                  bnfgi(gi),
                  len(lc),
                  "\t".join([str(c) for c in lc])))
            s.write("carets end\n\n")

        if self.markClasses:
            s.write("mark attachment class definition begin\n")
            for gi in sorted(
              self.markClasses,
              key=lambda x: (self.markClasses[x], x)):
                mc = self.markClasses[gi]
                s.write("%s\t%d\n" % (bnfgi(gi), mc))
            s.write("class definition end\n\n")

        if self.markSets:
            s.write("markfilter set definition begin\n")
            for gsi, gs in enumerate(self.markSets):
                for gi in sorted(gs):
                    s.write("%s\t%d\n" % (bnfgi(gi), gsi))
            s.write("set definition end\n\n")
Example #2
0
class STAT(dict, metaclass=mapmeta.FontDataMetaclass):
    """
    Objects representing entire STAT tables. These are dicts whose keys are
    axis tags and values are AxisRecords.

    >>> flags = axisvalueflags.AxisValueFlags()
    >>> AV1 = axisvalue_format1.AxisValue
    >>> AC = axial_coordinate.AxialCoordinate
    >>> av1 = AV1(value=AC(-34), nameID=2353, flags=flags)
    >>> avs = axisvalues.AxisValues([av1])
    >>> ar = axisrecord.AxisRecord(nameID=0x123, ordering=90, axisValues=avs)
    >>> obj = STAT({'wght': ar})
    >>> obj.pprint()
    'wght':
      Designation in the 'name' table: 291
      Ordering: 90
      Axis Values:
        0:
          Flags:
            Older Sibling Font Attribute: False
            Elidable Axis Value Name: False
          Designation in the 'name' table: 2353
          Value: -34.0
    Version:
      Major version: 1
      Minor version: 1
    ElidedFallbackNameID: 2
   
    >>> logger=utilities.makeDoctestLogger("test")
    >>> ed = utilities.fakeEditor(100, name=True)
    >>> obj.isValid(logger=logger, editor=ed)
    test - INFO - 'wght' is a registered axis tag.
    test - ERROR - value -34.0 is out of range for axis 'wght'
    test - INFO - The elidedFallbackNameID is 2 ('Regular')
    test.[wght] - ERROR - AxisRecord nameID 291 not found in the font's 'name' table.
    test.[wght].axisValues.[0] - ERROR - NameID 2353 not found in the font's 'name' table.
    test.[wght].nameID - ERROR - Name table index 291 not present in 'name' table.
    False

    >>> obj = STAT({'0Xft': ar}, elidedFallbackNameID=666)
    >>> logger.logger.setLevel("WARNING")
    >>> obj.isValid(editor=ed, logger=logger)
    test - ERROR - '0Xft' is neither a registered axis tag nor a valid private tag.
    test - ERROR - The elidedFallbackNameID 666 is not present in the 'name' table.
    test.[0Xft] - ERROR - AxisRecord nameID 291 not found in the font's 'name' table.
    test.[0Xft].axisValues.[0] - ERROR - NameID 2353 not found in the font's 'name' table.
    test.[0Xft].nameID - ERROR - Name table index 291 not present in 'name' table.
    False
    """

    #
    # Class definition variables
    #

    mapSpec = dict(item_ensurekeytype=str,
                   item_followsprotocol=True,
                   item_keyfollowsprotocol=True,
                   item_pprintlabelpresort=True,
                   map_validatefunc_partial=_validate)

    attrSpec = dict(
        version=dict(attr_followsprotocol=True,
                     attr_initfunc=lambda: otversion.Version(minor=1),
                     attr_label='Version'),
        elidedFallbackNameID=dict(attr_initfunc=lambda: 2,
                                  attr_label='ElidedFallbackNameID'))

    attrSorted = ('version', 'elidedFallbackNameID')

    #
    # Methods
    #

    def buildBinary(self, w, **kwArgs):
        """
        Adds the binary data for the STAT to the specified walker.

        >>> obj = _testingValues[1]
        >>> s = obj.binaryString()
        >>> utilities.hexdump(s)
               0 | 0001 0001 0008 0002  0000 0014 0002 0000 |................|
              10 | 0024 0002 7465 7374  0123 0001 7767 6874 |.$..test.#..wght|
              20 | 0100 0000 0004 0010  0001 0000 0000 0931 |...............1|
              30 | FFDE 0000 0001 0001  0000 0931 FFDE 0000 |...........1....|
        """

        if 'stakeValue' in kwArgs:
            stakeValue = kwArgs.pop('stakeValue')
            w.stakeCurrentWithValue(stakeValue)
        else:
            stakeValue = w.stakeCurrent()

        stake_tableStart = w.stakeCurrent()

        self.version.buildBinary(w)
        w.add("H", _v1_1_DESIGN_AXIS_SIZE)
        w.add("H", len(self))
        w.add("L", 20)  # size of header
        axisValueCount = sum([len(ar.axisValues) for ar in self.values()])
        w.add("H", axisValueCount)

        if axisValueCount:
            stake_endOfAxisRecords = w.getNewStake()
            w.addUnresolvedOffset("L", stake_tableStart,
                                  stake_endOfAxisRecords)
        else:
            w.add("L", 0)

        w.add("H", self.elidedFallbackNameID)

        allAxisValues = list()
        for i, (tag, ar) in enumerate(sorted(self.items())):
            ar.buildBinary(w, axisTag=tag, **kwArgs)
            for av in ar.axisValues:
                allAxisValues.append((i, av))

        if axisValueCount:
            w.stakeCurrentWithValue(stake_endOfAxisRecords)
        else:
            return  # end of table

        stake_AVOArrayStart = w.stakeCurrent()
        stakedict_AVOs = {}
        for i in range(len(allAxisValues)):
            stakedict_AVOs[i] = w.getNewStake()
            w.addUnresolvedOffset("H", stake_AVOArrayStart, stakedict_AVOs[i])

        aai = 0
        for ai, av in allAxisValues:
            w.stakeCurrentWithValue(stakedict_AVOs[aai])
            av.buildBinary(w, axisIndex=ai)
            aai += 1

    @classmethod
    def fromvalidatedwalker(cls, w, **kwArgs):
        """
        Like fromwalker(), this method returns a new STAT. However, it also
        does extensive validation via the logging module (the client should
        have done a logging.basicConfig call prior to calling this method,
        unless a logger is passed in via the 'logger' keyword argument).

        >>> logger = utilities.makeDoctestLogger('test')
        >>> s = ("00 01 00 01 00 08 00 01 00 00 00 14 00 01 00 00 "
        ...      "00 1C 00 02 74 65 73 74 01 23 00 01 00 02 00 01 "
        ...      "00 00 00 00 09 31 00 02 4C CD")
        >>> b = utilities.fromhex(s)
        >>> obj = STAT.fromvalidatedbytes(b, logger=logger)
        test.STAT - DEBUG - Walker has 42 remaining bytes.
        test.STAT.version - DEBUG - Walker has 42 remaining bytes.
        test.STAT - INFO - Version is 1.1
        test.STAT - INFO - 1 design axis
        test.STAT - INFO - Elided Fallback Name ID is 2
        test.STAT.axisrecord - DEBUG - Walker has 22 remaining bytes.
        test.STAT.axisvalues - DEBUG - Walker has 12 remaining bytes.
        test.STAT.axisvalues.axisvalue_format1 - DEBUG - Walker has 12 remaining bytes.
        test.STAT.axisvalues.axisvalue_format1.AxialCoordinate - DEBUG - Walker has 4 remaining bytes.

        >>> obj.binaryString() == b
        True

        >>> obj = STAT.fromvalidatedbytes(b'XYZ', logger=logger)
        test.STAT - DEBUG - Walker has 3 remaining bytes.
        test.STAT - ERROR - Insufficient bytes

        >>> s = ("00 01 00 15 00 08 00 01 00 00 00 14 00 01 00 00 "
        ...      "00 1A 74 65 73 74 01 23 00 01 00 02 00 01 00 00 "
        ...      "00 00 09 31 00 02 4C CD")
        >>> b = utilities.fromhex(s)
        >>> obj = STAT.fromvalidatedbytes(b, logger=logger)
        test.STAT - DEBUG - Walker has 40 remaining bytes.
        test.STAT.version - DEBUG - Walker has 40 remaining bytes.
        test.STAT - ERROR - Expected version 1.1, but got Major version = 1, Minor version = 21 instead.

        >>> s = ("00 01 00 01 08 00 00 01 00 00 00 14 00 01 00 00 "
        ...      "00 1A 74 65 73 74 01 23 00 01 00 02 00 01 00 00 "
        ...      "00 00 09 31 00 02 4C CD")
        >>> b = utilities.fromhex(s)
        >>> obj = STAT.fromvalidatedbytes(b, logger=logger)
        test.STAT - DEBUG - Walker has 40 remaining bytes.
        test.STAT.version - DEBUG - Walker has 40 remaining bytes.
        test.STAT - INFO - Version is 1.1
        test.STAT - ERROR - Expected designAxisSize = 8 but got 2048 instead.

        >>> s = ("00 01 00 01 00 08 00 05 00 00 00 14 00 01 00 00 "
        ...      "00 1A 74 65 73 74 01 23 00 01 00 02 00 01 00 00 "
        ...      "00 00 09 31 00 02 4C CD")
        >>> b = utilities.fromhex(s)
        >>> obj = STAT.fromvalidatedbytes(b, logger=logger)
        test.STAT - DEBUG - Walker has 40 remaining bytes.
        test.STAT.version - DEBUG - Walker has 40 remaining bytes.
        test.STAT - INFO - Version is 1.1
        test.STAT - INFO - 5 design axes
        test.STAT - ERROR - Table is too short for the advertised number * size of design axes

        >>> s = ("00 01 00 01 00 08 00 01 00 00 00 14 00 00 00 00 "
        ...      "00 1C 00 02 74 65 73 74 01 23 00 01 00 02 00 01 "
        ...      "00 00 00 00 09 31 00 02 4C CD")
        >>> b = utilities.fromhex(s)
        >>> obj = STAT.fromvalidatedbytes(b, logger=logger)
        test.STAT - DEBUG - Walker has 42 remaining bytes.
        test.STAT.version - DEBUG - Walker has 42 remaining bytes.
        test.STAT - INFO - Version is 1.1
        test.STAT - INFO - 1 design axis
        test.STAT - WARNING - AxisValueTable count is zero
        test.STAT - INFO - Elided Fallback Name ID is 2
        test.STAT.axisrecord - DEBUG - Walker has 22 remaining bytes.
        test.STAT - WARNING - axisValueCount of zero with non-zero offsetToAxisValueOffsets

        >>> s = ("00 01 00 01 00 08 00 01 00 00 00 14 00 01 00 00 "
        ...      "00 1C 00 02 74 65 73 74 01 23 00 01 00 02 00 01 "
        ...      "02 00 00 00 09 31 00 02 4C CD")
        >>> b = utilities.fromhex(s)
        >>> obj = STAT.fromvalidatedbytes(b, logger=logger)
        test.STAT - DEBUG - Walker has 42 remaining bytes.
        test.STAT.version - DEBUG - Walker has 42 remaining bytes.
        test.STAT - INFO - Version is 1.1
        test.STAT - INFO - 1 design axis
        test.STAT - INFO - Elided Fallback Name ID is 2
        test.STAT.axisrecord - DEBUG - Walker has 22 remaining bytes.
        test.STAT.axisvalues - DEBUG - Walker has 12 remaining bytes.
        test.STAT.axisvalues.axisvalue_format1 - DEBUG - Walker has 12 remaining bytes.
        test.STAT.axisvalues.axisvalue_format1.AxialCoordinate - DEBUG - Walker has 4 remaining bytes.
        test.STAT - ERROR - AxisValueTable[0] refers to out-of-range axis index 512
        """

        logger = kwArgs.pop('logger', None)

        if logger is None:
            logger = logging.getLogger().getChild('STAT')
        else:
            logger = logger.getChild('STAT')

        wholeTableLength = w.length()

        logger.debug(
            ('V0001', (wholeTableLength, ), "Walker has %d remaining bytes."))

        if wholeTableLength < 20:
            logger.error(('V0004', (), "Insufficient bytes"))
            return None

        version = otversion.Version.fromvalidatedwalker(w, logger=logger)

        if version.major != 1 or version.minor != 1:
            logger.error(('V0002', (version, ),
                          "Expected version 1.1, but got %s instead."))

            return None

        else:
            logger.info(('V1076', (), "Version is 1.1"))

        designAxisSize, designAxisCount = w.unpack("2H")

        if designAxisSize != _v1_1_DESIGN_AXIS_SIZE:
            logger.error(('V1077', (designAxisSize, ),
                          "Expected designAxisSize = 8 but got %d instead."))

            return None

        logger.info(
            ('Vxxxx', (designAxisCount, 'i' if designAxisCount == 1 else 'e'),
             "%d design ax%ss"))

        offsetToDesignAxes = w.unpack("L")

        if designAxisSize * designAxisCount >= wholeTableLength:
            logger.error(('V0004', (
            ), "Table is too short for the advertised number * size of design axes"
                          ))

            return None

        axisValueCount = w.unpack("H")

        if axisValueCount <= 0:
            logger.warning(('Vxxxx', (), "AxisValueTable count is zero"))

        offsetToAxisValueOffsets = w.unpack("L")

        if offsetToAxisValueOffsets > wholeTableLength:
            logger.error(
                ('V0004', (offsetToAxisValueOffsets, ),
                 "Table is too short for offset to AxisValueOffsets %d"))

            return None

        if w.length() < 2:
            logger.error(('V0004', (), "Insufficient bytes"))
            return None

        efnid = w.unpack("H")

        logger.info(('V1103', (efnid, ), "Elided Fallback Name ID is %d"))

        r = cls(version=version, elidedFallbackNameID=efnid)

        w.setOffset(offsetToDesignAxes)
        axisrecords = w.group("4s2H", designAxisCount)  # pre-read to get tags
        w.setOffset(-(designAxisCount * _v1_1_DESIGN_AXIS_SIZE),
                    relative=True)  # back up
        ARfvw = axisrecord.AxisRecord.fromvalidatedwalker
        origOrder = {}

        for i, (tag, nameID, ordering) in enumerate(axisrecords):
            tag_ascii = tag.decode('ascii')
            origOrder[i] = tag_ascii
            axisrec = ARfvw(w, logger=logger, **kwArgs)  # read validated
            r[tag_ascii] = axisrec

        if axisValueCount:
            # now process AxisValueTables and attach to our AxisRecords
            # according to their ._axisIndex
            AVRS = axisvalues.AxisValues
            w.setOffset(offsetToAxisValueOffsets)
            axisValueOffsets = w.group("H", axisValueCount)

            for oidx, o in enumerate(axisValueOffsets):
                w.setOffset(offsetToAxisValueOffsets + o)
                tmp = AVRS.fromvalidatedwalker(w,
                                               axisValueCount=1,
                                               logger=logger)
                if tmp and tmp[0] is not None:
                    axidx = tmp[0]._axisIndex
                    if axidx not in origOrder:
                        logger.error(('V1078', (
                            oidx, axidx
                        ), "AxisValueTable[%d] refers to out-of-range axis index %d"
                                      ))
                    else:
                        axtag = origOrder.get(axidx)
                        r[axtag].axisValues.append(tmp[0])
        else:
            if offsetToAxisValueOffsets != 0:
                logger.warning(('Vxxxx', (
                ), "axisValueCount of zero with non-zero offsetToAxisValueOffsets"
                                ))

        return r

    @classmethod
    def fromwalker(cls, w, **kwArgs):
        """
        Creates and returns a new STAT object from the specified walker.

        >>> b = _testingValues[1].binaryString()
        >>> utilities.hexdump(b)
               0 | 0001 0001 0008 0002  0000 0014 0002 0000 |................|
              10 | 0024 0002 7465 7374  0123 0001 7767 6874 |.$..test.#..wght|
              20 | 0100 0000 0004 0010  0001 0000 0000 0931 |...............1|
              30 | FFDE 0000 0001 0001  0000 0931 FFDE 0000 |...........1....|
        >>> obj = STAT.frombytes(b)
        >>> obj == _testingValues[1]
        True
        """
        version = otversion.Version.fromwalker(w, **kwArgs)
        designAxisSize, designAxisCount = w.unpack("2H")
        offsetToDesignAxes = w.unpack("L")
        axisValueCount = w.unpack("H")
        offsetToAxisValueOffsets = w.unpack("L")
        efnid = w.unpack("H")

        r = cls(version=version, elidedFallbackNameID=efnid)

        w.setOffset(offsetToDesignAxes)
        axisrecords = w.group("4s2H", designAxisCount)
        w.setOffset(-(designAxisCount * _v1_1_DESIGN_AXIS_SIZE), relative=True)
        ARfw = axisrecord.AxisRecord.fromwalker
        origOrder = {}

        for i, (tag, nameID, ordering) in enumerate(axisrecords):
            tag_ascii = tag.decode('ascii')
            origOrder[i] = tag_ascii
            axisrec = ARfw(w, **kwArgs)
            r[tag_ascii] = axisrec

        if axisValueCount:
            # process AxisValueTables and attach to our AxisRecords by index
            AVRS = axisvalues.AxisValues
            w.setOffset(offsetToAxisValueOffsets)
            axisValueOffsets = w.group("H", axisValueCount)

            for o in axisValueOffsets:
                w.setOffset(offsetToAxisValueOffsets + o)
                tmp = AVRS.fromwalker(w, axisValueCount=1)
                if tmp and tmp[0] is not None:
                    axidx = tmp[0]._axisIndex
                    if axidx in origOrder:
                        axtag = origOrder.get(axidx)
                        r[axtag].axisValues.append(tmp[0])

        return r
Example #3
0
#

if 0:
    def __________________(): pass

if __debug__:
    from fontio3 import utilities
    from fontio3.utilities import namer
    from io import StringIO
    from fontio3.opentype.fontworkersource import FontWorkerSource
    
    _testingValues = (
        GDEF(),

        GDEF(
          version=otversion.Version(minor=3),
          glyphClasses=glyphclass._testingValues[1],
          attachments=attachlist._testingValues[1],
          ligCarets=ligcaret._testingValues[2],
          markClasses=markclass._testingValues[1],
          markSets=markset._testingValues[1]))

    _test_FW_namer = namer.Namer(None)
    _test_FW_namer._nameToGlyph = {
        'a': 5,
        'b': 7,
        'c': 11
    }
    _test_FW_namer._initialized = True

    _test_FW_fws = FontWorkerSource(StringIO(
Example #4
0
class GSUB(GSUB_v10, metaclass=simplemeta.FontDataMetaclass):
    """
    Top-level GSUB objects. These are simple objects with four attributes:
    
        version             A Version object.
        features            A FeatureDict object.
        scripts             A ScriptDict object.
        featureVariations   A FeatureVariationsTable object.
    """

    #
    # Class definition variables
    #

    attrSpec = dict(version=dict(
        attr_followsprotocol=True,
        attr_initfunc=lambda: otversion.Version(minor=1),
        attr_label="Version"),
                    featureVariations=dict(
                        attr_followsprotocol=True,
                        attr_initfunc=featurevariations.FeatureVariations,
                        attr_label="Feature Variations"))

    attrSorted = ('version', 'features', 'scripts', 'featureVariations')

    #
    # Methods
    #

    def buildBinary(self, w, **kwArgs):
        """
        Adds the binary data for the GSUB object to the specified LinkedWriter.
        (Note that this class has an explicit binaryString() method as well).
        """

        if 'stakeValue' in kwArgs:
            stakeValue = kwArgs.pop('stakeValue')
            w.stakeCurrentWithValue(stakeValue)
        else:
            stakeValue = w.stakeCurrent()

        editor = kwArgs.get('editor')
        if editor.reallyHas(b'GDEF'):
            kwArgs['GDEF'] = editor.GDEF

        # We only bother adding content if the feature dict is not empty,
        # or it is explicitly forced via the forceForEmpty keyword.
        ffe = kwArgs.get('forceForEmpty', False)

        if self.features or ffe:
            if not ffe:
                self.scripts.trimToValidFeatures(set(self.features))

            ll = lookuplist.LookupList.fromtoplevel(self.features)

            if self.featureVariations:
                llIDset = set(id(obj) for obj in ll)
                llv = lookuplist.LookupList.fromtoplevel(
                    self.featureVariations)
                for lkp in llv:
                    if id(lkp) not in llIDset:
                        ll.append(lkp)
                        llIDset.add(id(lkp))
                ll._sort()

            w.addIndexMap(
                "lookupList_GSUB",
                dict((obj.asImmutable(), i) for i, obj in enumerate(ll)))

            ttfi = dict(zip(sorted(self.features), itertools.count()))
            w.add("L", 0x10001)  # Version (1.1)
            stakes = [w.getNewStake(), w.getNewStake(), w.getNewStake()]

            for stake in stakes:
                w.addUnresolvedOffset("H", stakeValue, stake)

            if self.featureVariations:
                fvstake = w.getNewStake()
                w.addUnresolvedOffset("L", stakeValue, fvstake)
            else:
                w.add("L", 0)  # offset to FeatureVariationsTable

            self.scripts.buildBinary(w,
                                     stakeValue=stakes[0],
                                     tagToFeatureIndex=ttfi)

            self.features.buildBinary(w, stakeValue=stakes[1], lookupList=ll)
            extPool = {}
            kwArgs.pop('extensionPool', None)
            kwArgs['memo'] = {}

            ll.buildBinary(w,
                           stakeValue=stakes[2],
                           extensionPool=extPool,
                           **kwArgs)

            if self.featureVariations:
                if 'axisOrder' not in kwArgs:
                    kwArgs['axisOrder'] = editor.fvar.axisOrder

                self.featureVariations.buildBinary(w,
                                                   lookupList=ll,
                                                   stakeValue=fvstake,
                                                   tagToFeatureIndex=ttfi,
                                                   **kwArgs)

            for i, obj, stake in sorted(extPool.values()):
                obj.buildBinary(w, stakeValue=stake, **kwArgs)

    @classmethod
    def fromvalidatedwalker(cls, w, **kwArgs):
        """
        Creates and returns a new GSUB object from the specified walker, doing
        source validation.
        
        
        Bare-minimum example...1 script, 1 feature, 1 lookup, etc.
        >>> s = ("0001 0001 000E 0022 0030 0000 0050 0001"
        ...      "7363 7230 0008 0004 0000 0000 FFFF 0001"
        ...      "0000 0001 6665 6130 0008 0000 0001 0000"
        ...      "0001 0004 0007 0000 0001 0008 0001 0001"
        ...      "0000 0008 0001 0006 001A 0001 0001 0004"
        ...      "0001 0000 0000 0001 0000 0010 0000 001E"
        ...      "0001 0000 0006 0001 0001 8000 7FFF 0000"
        ...      "0001 0001 0000 0000 000C 0000 0001 0000")

        >>> b = utilities.fromhex(s)

        <<< utilities.hexdump(b)  # for debugging
        >>> ao = ('wght', 'wdth')
        >>> l = utilities.makeDoctestLogger("test")
        >>> ll = _lltv[1]
        >>> fvb = GSUB.fromvalidatedbytes
        >>> obj = fvb(b, logger=l, axisOrder=ao, lookupList=ll)
        test.GSUB - DEBUG - Walker has 128 remaining bytes.
        test.GSUB.version - DEBUG - Walker has 128 remaining bytes.
        test.GSUB.lookuplist - DEBUG - Walker has 80 bytes remaining.
        test.GSUB.lookuplist - DEBUG - Offset count is 1
        test.GSUB.lookuplist - DEBUG - Offset 0 is 4
        test.GSUB.lookuplist.lookup 0.lookup - DEBUG - Walker has 76 remaining bytes.
        test.GSUB.lookuplist.lookup 0.lookup - DEBUG - Kind is 7
        test.GSUB.lookuplist.lookup 0.lookup - DEBUG - Number of subtables is 1
        test.GSUB.lookuplist.lookup 0.lookup - DEBUG - Subtable offset 0 is 8
        test.GSUB.lookuplist.lookup 0.lookup.subtable 0.single - DEBUG - Walker has 60 bytes remaining.
        test.GSUB.lookuplist.lookup 0.lookup.subtable 0.single.coverage - DEBUG - Walker has 54 remaining bytes.
        test.GSUB.lookuplist.lookup 0.lookup.subtable 0.single.coverage - DEBUG - Format is 1, count is 1
        test.GSUB.lookuplist.lookup 0.lookup.subtable 0.single.coverage - DEBUG - Raw data are [4]
        test.GSUB.featuredict - DEBUG - Walker has 94 remaining bytes.
        test.GSUB.featuredict - DEBUG - Count is 1
        test.GSUB.featuredict - DEBUG - Feature 0: tag is 'fea0', offset is 8
        test.GSUB.featuredict.feature table 0.featuretable - DEBUG - Walker has 86 remaining bytes.
        test.GSUB.featuredict.feature table 0.featuretable - DEBUG - FeatureParams offset is 0, lookupCount is 1
        test.GSUB.featuredict.feature table 0.featuretable - DEBUG - Entry 0 is Lookup 0
        test.GSUB.scriptdict - DEBUG - Walker has 114 remaining bytes.
        test.GSUB.scriptdict - DEBUG - Group count is 1
        test.GSUB.scriptdict - DEBUG - Script rec for tag 'scr0' at offset 8
        test.GSUB.scriptdict.script scr0.langsysdict - DEBUG - Walker has 106 remaining bytes.
        test.GSUB.scriptdict.script scr0.langsysdict - DEBUG - Default offset is 4; langSys count is 0
        test.GSUB.scriptdict.script scr0.langsysdict.default.langsys - DEBUG - Walker has 102 bytes remaining.
        test.GSUB.scriptdict.script scr0.langsysdict.default.langsys - DEBUG - Lookup order is 0, Required index is 65535, feature count is 1
        test.GSUB.scriptdict.script scr0.langsysdict.default.langsys - DEBUG - Optional feature 0 is 0
        test.GSUB.featurevariations - DEBUG - Walker has 48 remaining bytes.
        test.GSUB.version - DEBUG - Walker has 48 remaining bytes.
        test.GSUB.featurevariations - INFO - FeatureVariationRecordCount is 1
        test.GSUB.featurevariations - DEBUG - offsetConditionSet: 0x00000010, offsetFeatureTableSubst: 0x0000001E
        test.GSUB.conditionset - DEBUG - Walker has 32 remaining bytes.
        test.GSUB.conditionset - INFO - ConditionCount is 1
        test.GSUB.condition - DEBUG - Walker has 26 remaining bytes.
        test.GSUB.condition - INFO - AxisTag 'wdth'
        test.GSUB.condition - INFO - Min: -2.000000, Max: 1.999939
        test.GSUB.featuretablesubst - DEBUG - Walker has 18 remaining bytes.
        test.GSUB.version - DEBUG - Walker has 18 remaining bytes.
        test.GSUB.featuretablesubst - INFO - SubstitutionCount is 1
        test.GSUB.featuretablesubst - DEBUG - Feature index 0, offset 0x0000000C
        test.GSUB.featuretable - DEBUG - Walker has 6 remaining bytes.
        test.GSUB.featuretable - DEBUG - FeatureParams offset is 0, lookupCount is 1
        test.GSUB.featuretable - DEBUG - Entry 0 is Lookup 0
        """

        editor = kwArgs.get('editor')
        if editor:
            if editor.reallyHas(b'GDEF') and 'GDEF' not in kwArgs:
                kwArgs['GDEF'] = editor.GDEF
            if editor.reallyHas(b'fvar') and 'axisOrder' not in kwArgs:
                kwArgs['axisOrder'] = editor.fvar.axisOrder

        logger = kwArgs.pop('logger', logging.getLogger())
        logger = logger.getChild("GSUB")

        logger.debug(
            ('V0001', (w.length(), ), "Walker has %d remaining bytes."))

        if w.length() < 20:
            logger.error(('V0004', (), "Insufficient bytes."))
            return None

        version = otversion.Version.fromvalidatedwalker(w,
                                                        logger=logger,
                                                        **kwArgs)
        slOffset, flOffset, llOffset, fvtOffset = w.unpack("3HL")

        if version.major != 1 or version.minor != 1:
            logger.error(
                ('V0454', (version, ), "Expected version 1.1, but got %s"))

            return None

        if not slOffset:
            logger.warning(
                ('V0455', (), "The ScriptList offset is zero; no further GSUB "
                 "validation will be performed."))

        if not flOffset:
            logger.warning(('V0456', (),
                            "The FeatureList offset is zero; no further GSUB "
                            "validation will be performed."))

        if not llOffset:
            logger.warning(
                ('V0457', (), "The LookupList offset is zero; no further GSUB "
                 "validation will be performed."))

        if not all([slOffset, flOffset, llOffset]):
            return cls()  # note: okay to have null offset to FV table

        delKeys = {'featureIndexToTag', 'forGPOS', 'lookupList'}

        for delKey in delKeys:
            kwArgs.pop(delKey, None)

        fitt = []

        ll = lookuplist.LookupList.fromvalidatedwalker(w.subWalker(llOffset),
                                                       forGPOS=False,
                                                       logger=logger,
                                                       **kwArgs)

        if ll is None:
            return None

        fd = featuredict.FeatureDict.fromvalidatedwalker(
            w.subWalker(flOffset),
            featureIndexToTag=fitt,
            forGPOS=False,
            lookupList=ll,
            logger=logger,
            **kwArgs)

        if fd is None:
            return None

        sd = scriptdict.ScriptDict.fromvalidatedwalker(w.subWalker(slOffset),
                                                       featureIndexToTag=fitt,
                                                       logger=logger,
                                                       **kwArgs)

        if sd is None:
            return None

        if fvtOffset:
            fvt = featurevariations.FeatureVariations.fromvalidatedwalker(
                w.subWalker(fvtOffset),
                featureIndexToTag=fitt,
                logger=logger,
                lookupList=ll,
                **kwArgs)
        else:
            fvt = featurevariations.FeatureVariations()

        r = cls(version=version,
                features=fd,
                scripts=sd,
                featureVariations=fvt)
        r._cachedOriginalLookupList = ll.__copy__()
        return r

    @classmethod
    def fromversion10(cls, v10table, **kwArgs):
        """
        Up-convert from v1.0 GSUB to v1.1
        """

        r = cls(features=v10table.features, scripts=v10table.scripts)

        return r

    @classmethod
    def fromwalker(cls, w, **kwArgs):
        """
        Creates and returns a new GSUB object from the specified walker.
        """

        editor = kwArgs.get('editor')
        if editor:
            if editor.reallyHas(b'GDEF') and 'GDEF' not in kwArgs:
                kwArgs['GDEF'] = editor.GDEF
            if editor.reallyHas(b'fvar') and 'axisOrder' not in kwArgs:
                kwArgs['axisOrder'] = editor.fvar.axisOrder

        version = otversion.Version.fromwalker(w, **kwArgs)
        slOffset, flOffset, llOffset, fvtOffset = w.unpack("3HL")
        fitt = []

        if not all([slOffset, flOffset, llOffset]):
            return cls()  # note: okay to have null offset to FV table

        delKeys = {'featureIndexToTag', 'forGPOS', 'lookupList'}

        for delKey in delKeys:
            kwArgs.pop(delKey, None)

        ll = lookuplist.LookupList.fromwalker(w.subWalker(llOffset),
                                              forGPOS=False,
                                              **kwArgs)

        fd = featuredict.FeatureDict.fromwalker(w.subWalker(flOffset),
                                                featureIndexToTag=fitt,
                                                forGPOS=False,
                                                lookupList=ll,
                                                **kwArgs)

        sd = scriptdict.ScriptDict.fromwalker(w.subWalker(slOffset),
                                              featureIndexToTag=fitt,
                                              **kwArgs)

        if fvtOffset:
            fvt = featurevariations.FeatureVariations.fromwalker(
                w.subWalker(fvtOffset),
                featureIndexToTag=fitt,
                lookupList=ll,
                **kwArgs)
        else:
            fvt = featurevariations.FeatureVariations()

        r = cls(version=version,
                features=fd,
                scripts=sd,
                featureVariations=fvt)
        r._cachedOriginalLookupList = ll.__copy__()
        return r

    def isValid(self, **kwArgs):
        """
        """

        logger = kwArgs.pop('logger', logging.getLogger())
        r = self.scripts.isValid(logger=logger.getChild("scripts"), **kwArgs)
        logger.debug(('Vxxxx', (), "Gathering Lookups..."))
        keyTrace = {}
        kwArgs.pop('suppressDeepValidation', None)

        try:
            ll = lookuplist.LookupList.fromtoplevel(self.features,
                                                    keyTrace=keyTrace)

        except utilities.CycleError:
            logger.error(
                ('V0912', (),
                 "Cannot validate the 'GSUB' table, because one or more "
                 "Lookups include circular references."))

            return False

        for i, obj in enumerate(ll):
            logger.debug(('Vxxxx', (i, ), "Lookup %d"))

            if id(obj) in keyTrace:
                s = sorted({x[0] for x in keyTrace[id(obj)]})
                subLogger = logger.getChild(str(s))
            else:
                subLogger = logger.getChild("feature")

            rThis = obj.isValid(suppressDeepValidation=True,
                                logger=subLogger,
                                **kwArgs)

            r = rThis and r

        for featKey, featTable in self.features.items():
            fp = featTable.featureParams
            if fp is not None:
                subLogger = logger.getChild(str(featKey))
                r = fp.isValid(logger=subLogger, **kwArgs) and r

        # detect orphaned features
        allFeatures = set(self.features.keys())
        for scriptTag, scriptTable in self.scripts.items():
            if scriptTable.defaultLangSys:
                defLangSys = scriptTable.defaultLangSys
                if defLangSys.requiredFeature:
                    allFeatures.discard(defLangSys.requiredFeature)
                for optFeatTag in sorted(defLangSys.optionalFeatures):
                    allFeatures.discard(optFeatTag)

            for langSysTag, langSysTable in scriptTable.items():
                if langSysTable.requiredFeature:
                    allFeatures.discard(langSysTable.requiredFeature)
                for optFeatTag in sorted(langSysTable.optionalFeatures):
                    allFeatures.discard(optFeatTag)

        # allFeatures should be empty if all are accounted for
        # in the scriptTable; error otherwise.
        if len(allFeatures):
            logger.error(('V0959', (",".join([
                f.decode('ascii', errors='ignore') for f in sorted(allFeatures)
            ]), ), "The following features are orphaned (not referenced "
                          "in the Script Table): %s"))
            r = False

        return r
Example #5
0
class FeatureTableSubst(dict, metaclass=mapmeta.FontDataMetaclass):
    """
    Objects representing FeatureTableSubstitution Tables. These are maps of
    default feature tags to alternate FeatureTables to be applied if the
    object's corresponding ConditionSet (from the parent
    FeatureVariationRecord) is met.
    """

    #
    # Class definition variables
    #

    mapSpec = dict(item_followsprotocol=True)

    version = otversion.Version(major=1,
                                minor=0)  # currently only version defined

    def buildBinary(self, w, **kwArgs):
        """
        Adds the binary data for the FeatureTableSubst Table object to the
        specified LinkedWriter. The following kwArgs are recognized:
        
            tagToFeatureIndex   A dict mapping feature tags to indices in the
                                top-level table's FeatureTable.
        
        >>> ttfi = {b'test0001':1, b'foob0002':0}
        >>> b = _testingValues[0].binaryString(tagToFeatureIndex=ttfi)
        >>> utilities.hexdump(b)
               0 | 0001 0000 0000                           |......          |

        >>> ll = lltv[1]
        >>> b = _testingValues[1].binaryString(lookupList=ll, tagToFeatureIndex=ttfi)
        >>> utilities.hexdump(b)
               0 | 0001 0000 0001 0001  0000 000C 0000 0001 |................|
              10 | 0001                                     |..              |
        """

        if 'stakeValue' in kwArgs:
            stakeValue = kwArgs.pop('stakeValue')
            w.stakeCurrentWithValue(stakeValue)
        else:
            stakeValue = w.stakeCurrent()

        ttfi = kwArgs.pop('tagToFeatureIndex')

        FeatureTableSubst.version.buildBinary(w, **kwArgs)
        substCount = len(self)
        w.add("H", substCount)

        if substCount:
            featureSubs = [(ttfi[tag], self[tag]) for tag in self]

            stakesDict = {
                locIdx: w.getNewStake()
                for locIdx in range(substCount)
            }

            for locIdx, (fidx, aft) in enumerate(sorted(featureSubs)):
                w.add("H", fidx)  # index into default FeatureTable
                w.addUnresolvedOffset("L", stakeValue,
                                      stakesDict[locIdx])  # local idx

            for locIdx, (fidx, aft) in enumerate(sorted(featureSubs)):
                aft.buildBinary(w, stakeValue=stakesDict[locIdx], **kwArgs)

    @classmethod
    def fromvalidatedwalker(cls, w, **kwArgs):
        """
        Creates and returns a new FeatureTableSubst object from the specified walker,
        doing source validation.
        
        The following kwArgs are recognized:
            logger              A logger to post messages to (required).
            
            defaultFeatures     The default GSUB FeatureTable to index into
                                for replacement (required).
        
        >>> logger = utilities.makeDoctestLogger("test")
        >>> fitt = [b'test0001', b'foob0002']
        >>> s = ("00010000 0000")
        >>> b = utilities.fromhex(s)
        >>> fvb = FeatureTableSubst.fromvalidatedbytes
        >>> obj = fvb(b, featureIndexToTag=fitt, logger=logger)
        test.featuretablesubst - DEBUG - Walker has 6 remaining bytes.
        test.version - DEBUG - Walker has 6 remaining bytes.
        test.featuretablesubst - INFO - SubstitutionCount is 0

        >>> ll = lltv[1]
        >>> s = ("00010000"       # Version
        ...      "0001"           # count = 1
        ...      "0001 0000000C"  # feature index, offset to alt feature table
        ...      "0000 0000")     # alt feature table data (@offset)
        >>> b = utilities.fromhex(s)
        >>> obj = fvb(b, featureIndexToTag = fitt, logger=logger, lookupList=ll)
        test.featuretablesubst - DEBUG - Walker has 16 remaining bytes.
        test.version - DEBUG - Walker has 16 remaining bytes.
        test.featuretablesubst - INFO - SubstitutionCount is 1
        test.featuretablesubst - DEBUG - Feature index 1, offset 0x0000000C
        test.featuretable - DEBUG - Walker has 4 remaining bytes.
        test.featuretable - DEBUG - FeatureParams offset is 0, lookupCount is 0

        >>> obj = fvb(b[0:4], featureIndexToTag = fitt, logger=logger, lookupList=ll)
        test.featuretablesubst - DEBUG - Walker has 4 remaining bytes.
        test.featuretablesubst - ERROR - Insufficient bytes.
        
        >>> s = "00010000 0005 0001 0000000C"
        >>> b = utilities.fromhex(s)
        >>> obj = fvb(b, featureIndexToTag = fitt, logger=logger, lookupList=ll)
        test.featuretablesubst - DEBUG - Walker has 12 remaining bytes.
        test.version - DEBUG - Walker has 12 remaining bytes.
        test.featuretablesubst - ERROR - Insufficient bytes for declared count of 5.
        """

        logger = kwArgs.get('logger', None)

        if logger is None:
            logger = logging.getLogger()

        logger = logger.getChild("featuretablesubst")

        fitt = kwArgs.pop('featureIndexToTag')

        logger.debug(
            ('V0001', (w.length(), ), "Walker has %d remaining bytes."))

        if w.length() < 6:
            logger.error(('V0004', (), "Insufficient bytes."))
            return None

        vers = otversion.Version.fromvalidatedwalker(w, **kwArgs)

        count = w.unpack("H")

        if w.length() < count * 6:
            logger.error(('V0004', (count, ),
                          "Insufficient bytes for declared count of %d."))
            return None

        logger.info(('Vxxxx', (count, ), "SubstitutionCount is %d"))

        idxOffs = w.group("HL", count)
        d = {}
        fvw = featuretable.FeatureTable.fromvalidatedwalker
        for fidx, offset in idxOffs:
            logger.debug(
                ('Vxxxx', (fidx, offset), "Feature index %d, offset 0x%08X"))
            wSub = w.subWalker(offset)
            tag = fitt[fidx]
            d[tag] = fvw(wSub, **kwArgs)

        r = cls(d)

        return r

    @classmethod
    def fromwalker(cls, w, **kwArgs):
        """
        Creates and returns a new FeatureTableSubst object from the specified
        walker.

        >>> ll = lltv[1]
        >>> fitt = [b'test0001', b'foob0002']
        >>> s = ("00010000 0001 0001 0000000C"  # FeatTableSubst header
        ...      "0000 0000")                   # AltFeatureTable
        >>> b = utilities.fromhex(s)
        >>> fb = FeatureTableSubst.frombytes
        >>> obj = fb(b, featureIndexToTag=fitt, lookupList=ll)
        >>> sorted(obj.keys())
        [b'foob0002']
        """

        fitt = kwArgs.pop('featureIndexToTag')

        vers = otversion.Version.fromwalker(w, **kwArgs)
        count = w.unpack("H")

        idxOffs = w.group("HL", count)
        d = {}
        fw = featuretable.FeatureTable.fromwalker

        for fidx, offset in idxOffs:
            wSub = w.subWalker(offset)
            tag = fitt[fidx]
            d[tag] = fw(wSub, **kwArgs)

        r = cls(d)

        return r
Example #6
0
class GPOS(GPOS_v10, metaclass=simplemeta.FontDataMetaclass):
    """
    Top-level GPOS objects. These are simple objects with four attributes:
    
        version             A Version object.
        features            A FeatureDict object.
        scripts             A ScriptDict object.
        featureVariations   A FeatureVariations object.
    
    >>> _testingValues[0].pprint()
    Version:
      Major version: 1
      Minor version: 1
    Features:
    Scripts:
    Feature Variations:
    
    >>> bool(_testingValues[0].features and _testingValues[0].scripts)
    False
    
    >>> _testingValues[1].pprint()
    Version:
      Major version: 1
      Minor version: 1
    Features:
      Feature 'abcd0001':
        Lookup 0:
          Subtable 0 (Pair (glyph) positioning table):
            Key((8, 15)):
              Second adjustment:
                FUnit adjustment to origin's x-coordinate: -10
            Key((8, 20)):
              First adjustment:
                Device for vertical advance:
                  Tweak at 12 ppem: -2
                  Tweak at 14 ppem: -1
                  Tweak at 18 ppem: 1
            Key((10, 20)):
              First adjustment:
                FUnit adjustment to origin's x-coordinate: 30
                Device for vertical advance:
                  Tweak at 12 ppem: -2
                  Tweak at 14 ppem: -1
                  Tweak at 18 ppem: 1
              Second adjustment:
                Device for origin's x-coordinate:
                  Tweak at 12 ppem: -2
                  Tweak at 14 ppem: -1
                  Tweak at 18 ppem: 1
                Device for origin's y-coordinate:
                  Tweak at 12 ppem: -5
                  Tweak at 13 ppem: -3
                  Tweak at 14 ppem: -1
                  Tweak at 18 ppem: 2
                  Tweak at 20 ppem: 3
          Lookup flags:
            Right-to-left for Cursive: False
            Ignore base glyphs: True
            Ignore ligatures: False
            Ignore marks: False
          Sequence order (lower happens first): 1
      Feature 'size0002':
        Feature parameters object:
          Design size in decipoints: 80
          Subfamily value: 4
          Name table index of common subfamily: 300
          Small end of usage range in decipoints: 80
          Large end of usage range in decipoints: 120
      Feature 'wxyz0003':
        Lookup 0:
          Subtable 0 (Single positioning table):
            10:
              FUnit adjustment to origin's x-coordinate: -10
          Lookup flags:
            Right-to-left for Cursive: False
            Ignore base glyphs: False
            Ignore ligatures: False
            Ignore marks: False
          Sequence order (lower happens first): 3
    Scripts:
      Script object latn:
        LangSys object enGB:
          Required feature tag: wxyz0003
        LangSys object enUS:
          Optional feature tags:
            abcd0001
            size0002
        Default LangSys object:
          Required feature tag: wxyz0003
          Optional feature tags:
            abcd0001
            size0002
    Feature Variations: (no data)
    """

    #
    # Class definition variables
    #

    attrSpec = dict(version=dict(
        attr_followsprotocol=True,
        attr_initfunc=lambda: otversion.Version(minor=1),
        attr_label="Version"),
                    featureVariations=dict(
                        attr_followsprotocol=True,
                        attr_initfunc=featurevariations.FeatureVariations,
                        attr_label="Feature Variations"))

    attrSorted = ('version', 'features', 'scripts', 'featureVariations')

    #
    # Methods
    #

    def buildBinary(self, w, **kwArgs):
        """
        Adds the binary data for the GPOS object to the specified LinkedWriter.
        (Note that this class has an explicit binaryString() method as well).
        
        >>> utilities.hexdump(_testingValues[1].binaryString())
               0 | 0001 0001 000E 0040  006E 0000 0000 0001 |[email protected]......|
              10 | 6C61 746E 0008 0010  0002 656E 4742 001A |latn......enGB..|
              20 | 656E 5553 0020 0000  0002 0002 0000 0001 |enUS. ..........|
              30 | 0000 0002 0000 0000  FFFF 0002 0000 0001 |................|
              40 | 0003 6162 6364 0014  7369 7A65 001A 7778 |..abcd..size..wx|
              50 | 797A 0028 0000 0001  0000 0004 0000 0050 |yz.(...........P|
              60 | 0004 012C 0050 0078  0000 0001 0001 0002 |...,.P.x........|
              70 | 0006 0016 0009 0002  0001 0008 0001 0002 |................|
              80 | 0000 0018 0009 0000  0001 0008 0001 0001 |................|
              90 | 0000 005A 0001 000E  0081 0031 0002 0016 |...Z.......1....|
              A0 | 0030 0001 0002 0008  000A 0002 000F 0000 |.0..............|
              B0 | 0000 FFF6 0000 0000  0014 0000 0034 0000 |.............4..|
              C0 | 0000 0000 0001 0014  001E 001A 0000 001A |................|
              D0 | 000E 000C 0014 0002  BDF0 0020 3000 000C |........... 0...|
              E0 | 0012 0001 8C04 0001  0008 0001 FFF6 0001 |................|
              F0 | 0001 000A                                |....            |
        """

        if 'stakeValue' in kwArgs:
            stakeValue = kwArgs.pop('stakeValue')
            w.stakeCurrentWithValue(stakeValue)
        else:
            stakeValue = w.stakeCurrent()

        editor = kwArgs.get('editor')
        if editor is not None:
            if editor.reallyHas(b'fvar'):
                ao = editor.fvar.axisOrder
                kwArgs['axisOrder'] = ao
            else:
                ao = None

            if editor.reallyHas(b'GDEF'):
                kwArgs['GDEF'] = editor.GDEF
                gdce = getattr(editor.GDEF, '_creationExtras', {})
                otcd = gdce.get('otcommondeltas')
                if otcd:
                    bsfd = living_variations.IVS.binaryStringFromDeltas
                    otIVS = bsfd(otcd, axisOrder=ao)
                    kwArgs['otIVS'] = otIVS

        # We only bother adding content if the feature dict is not empty,
        # or it is explicitly forced via the forceForEmpty keyword.
        ffe = kwArgs.get('forceForEmpty', False)

        if self.features or ffe:
            if not ffe:
                self.scripts.trimToValidFeatures(set(self.features))

            ll = lookuplist.LookupList.fromtoplevel(self.features)

            if self.featureVariations:
                llIDset = set(id(obj) for obj in ll)
                llv = lookuplist.LookupList.fromtoplevel(
                    self.featureVariations)
                for lkp in llv:
                    if id(lkp) not in llIDset:
                        ll.append(lkp)
                        llIDset.add(id(lkp))
                ll._sort()

            w.addIndexMap(
                "lookupList_GPOS",
                dict((obj.asImmutable(), i) for i, obj in enumerate(ll)))

            ttfi = dict(zip(sorted(self.features), itertools.count()))
            w.add("L", 0x10001)  # Version (1.1)
            stakes = [w.getNewStake(), w.getNewStake(), w.getNewStake()]

            for stake in stakes:
                w.addUnresolvedOffset("H", stakeValue, stake)

            if self.featureVariations:
                fvstake = w.getNewStake()
                w.addUnresolvedOffset("L", stakeValue, fvstake)
            else:
                w.add("L", 0)  # offset to FeatureVariationsTable

            self.scripts.buildBinary(w,
                                     stakeValue=stakes[0],
                                     tagToFeatureIndex=ttfi)

            self.features.buildBinary(w, stakeValue=stakes[1], lookupList=ll)
            extPool = {}
            kwArgs.pop('extensionPool', None)
            kwArgs['memo'] = {}

            ll.buildBinary(w,
                           stakeValue=stakes[2],
                           extensionPool=extPool,
                           **kwArgs)

            if self.featureVariations:
                self.featureVariations.buildBinary(w,
                                                   lookupList=ll,
                                                   stakeValue=fvstake,
                                                   tagToFeatureIndex=ttfi,
                                                   **kwArgs)

            for i, obj, stake in sorted(extPool.values()):
                obj.buildBinary(w, stakeValue=stake, **kwArgs)

    @classmethod
    def fromvalidatedwalker(cls, w, **kwArgs):
        """
        Creates and returns a new GPOS object from the specified walker, doing
        source validation.
        
        >>> s = _testingValues[1].binaryString()
        >>> fvb = GPOS.fromvalidatedbytes
        >>> logger = utilities.makeDoctestLogger("GPOS_test")
        >>> obj = fvb(s, logger=logger)
        GPOS_test.GPOS - DEBUG - Walker has 244 remaining bytes.
        GPOS_test.GPOS.version - DEBUG - Walker has 244 remaining bytes.
        GPOS_test.GPOS.lookuplist - DEBUG - Walker has 134 bytes remaining.
        GPOS_test.GPOS.lookuplist - DEBUG - Offset count is 2
        GPOS_test.GPOS.lookuplist - DEBUG - Offset 0 is 6
        GPOS_test.GPOS.lookuplist.lookup 0.lookup - DEBUG - Walker has 128 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup - DEBUG - Kind is 9
        GPOS_test.GPOS.lookuplist.lookup 0.lookup - DEBUG - Number of subtables is 1
        GPOS_test.GPOS.lookuplist.lookup 0.lookup - DEBUG - Subtable offset 0 is 8
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair - DEBUG - Walker has 96 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs - DEBUG - Walker has 96 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.coverage - DEBUG - Walker has 82 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.coverage - DEBUG - Format is 1, count is 2
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.coverage - DEBUG - Raw data are [8, 10]
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 15.pairvalues - DEBUG - Walker has 70 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 15.pairvalues.value - DEBUG - Walker has 70 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 15.pairvalues.value - DEBUG - Walker has 66 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues - DEBUG - Walker has 58 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value - DEBUG - Walker has 58 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value.yAdvDevice.device - DEBUG - Walker has 22 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value.yAdvDevice.device - DEBUG - StartSize=12, endSize=18, format=1
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value.yAdvDevice.device - DEBUG - Data are (35844,)
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value - DEBUG - Walker has 54 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues - DEBUG - Walker has 44 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value - DEBUG - Walker has 44 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value.yAdvDevice.device - DEBUG - Walker has 22 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value.yAdvDevice.device - DEBUG - StartSize=12, endSize=18, format=1
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value.yAdvDevice.device - DEBUG - Data are (35844,)
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value - DEBUG - Walker has 40 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value.xPlaDevice.device - DEBUG - Walker has 22 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value.xPlaDevice.device - DEBUG - StartSize=12, endSize=18, format=1
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value.xPlaDevice.device - DEBUG - Data are (35844,)
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value.yPlaDevice.device - DEBUG - Walker has 34 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value.yPlaDevice.device - DEBUG - StartSize=12, endSize=20, format=2
        GPOS_test.GPOS.lookuplist.lookup 0.lookup.subtable 0.pair.pairglyphs.second glyph 20.pairvalues.value.yPlaDevice.device - DEBUG - Data are (48624, 32, 12288)
        GPOS_test.GPOS.lookuplist - DEBUG - Offset 1 is 22
        GPOS_test.GPOS.lookuplist.lookup 1.lookup - DEBUG - Walker has 112 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 1.lookup - DEBUG - Kind is 9
        GPOS_test.GPOS.lookuplist.lookup 1.lookup - DEBUG - Number of subtables is 1
        GPOS_test.GPOS.lookuplist.lookup 1.lookup - DEBUG - Subtable offset 0 is 8
        GPOS_test.GPOS.lookuplist.lookup 1.lookup.subtable 0.single - DEBUG - Walker has 14 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 1.lookup.subtable 0.single.coverage - DEBUG - Walker has 6 remaining bytes.
        GPOS_test.GPOS.lookuplist.lookup 1.lookup.subtable 0.single.coverage - DEBUG - Format is 1, count is 1
        GPOS_test.GPOS.lookuplist.lookup 1.lookup.subtable 0.single.coverage - DEBUG - Raw data are [10]
        GPOS_test.GPOS.lookuplist.lookup 1.lookup.subtable 0.single.value - DEBUG - Walker has 8 remaining bytes.
        GPOS_test.GPOS.featuredict - DEBUG - Walker has 180 remaining bytes.
        GPOS_test.GPOS.featuredict - DEBUG - Count is 3
        GPOS_test.GPOS.featuredict - DEBUG - Feature 0: tag is 'abcd', offset is 20
        GPOS_test.GPOS.featuredict.feature table 0.featuretable - DEBUG - Walker has 160 remaining bytes.
        GPOS_test.GPOS.featuredict.feature table 0.featuretable - DEBUG - FeatureParams offset is 0, lookupCount is 1
        GPOS_test.GPOS.featuredict.feature table 0.featuretable - DEBUG - Entry 0 is Lookup 0
        GPOS_test.GPOS.featuredict - DEBUG - Feature 1: tag is 'size', offset is 26
        GPOS_test.GPOS.featuredict.feature table 1.featuretable - DEBUG - Walker has 154 remaining bytes.
        GPOS_test.GPOS.featuredict.feature table 1.featuretable - DEBUG - FeatureParams offset is 4, lookupCount is 0
        GPOS_test.GPOS.featuredict.feature table 1.featuretable.featureparams_GPOS_size - DEBUG - Walker has 150 remaining bytes.
        GPOS_test.GPOS.featuredict.feature table 1.featuretable.featureparams_GPOS_size - DEBUG - Data are (80, 4, 300, 80, 120)
        GPOS_test.GPOS.featuredict - DEBUG - Feature 2: tag is 'wxyz', offset is 40
        GPOS_test.GPOS.featuredict.feature table 2.featuretable - DEBUG - Walker has 140 remaining bytes.
        GPOS_test.GPOS.featuredict.feature table 2.featuretable - DEBUG - FeatureParams offset is 0, lookupCount is 1
        GPOS_test.GPOS.featuredict.feature table 2.featuretable - DEBUG - Entry 0 is Lookup 1
        GPOS_test.GPOS.scriptdict - DEBUG - Walker has 230 remaining bytes.
        GPOS_test.GPOS.scriptdict - DEBUG - Group count is 1
        GPOS_test.GPOS.scriptdict - DEBUG - Script rec for tag 'latn' at offset 8
        GPOS_test.GPOS.scriptdict.script latn.langsysdict - DEBUG - Walker has 222 remaining bytes.
        GPOS_test.GPOS.scriptdict.script latn.langsysdict - DEBUG - Default offset is 16; langSys count is 2
        GPOS_test.GPOS.scriptdict.script latn.langsysdict.default.langsys - DEBUG - Walker has 206 bytes remaining.
        GPOS_test.GPOS.scriptdict.script latn.langsysdict.default.langsys - DEBUG - Lookup order is 0, Required index is 2, feature count is 2
        GPOS_test.GPOS.scriptdict.script latn.langsysdict.default.langsys - DEBUG - Optional feature 0 is 0
        GPOS_test.GPOS.scriptdict.script latn.langsysdict.default.langsys - DEBUG - Optional feature 1 is 1
        GPOS_test.GPOS.scriptdict.script latn.langsysdict - DEBUG - LangSys tag 'enGB' has offset 26
        GPOS_test.GPOS.scriptdict.script latn.langsysdict.tag enGB.langsys - DEBUG - Walker has 196 bytes remaining.
        GPOS_test.GPOS.scriptdict.script latn.langsysdict.tag enGB.langsys - DEBUG - Lookup order is 0, Required index is 2, feature count is 0
        GPOS_test.GPOS.scriptdict.script latn.langsysdict - DEBUG - LangSys tag 'enUS' has offset 32
        GPOS_test.GPOS.scriptdict.script latn.langsysdict.tag enUS.langsys - DEBUG - Walker has 190 bytes remaining.
        GPOS_test.GPOS.scriptdict.script latn.langsysdict.tag enUS.langsys - DEBUG - Lookup order is 0, Required index is 65535, feature count is 2
        GPOS_test.GPOS.scriptdict.script latn.langsysdict.tag enUS.langsys - DEBUG - Optional feature 0 is 0
        GPOS_test.GPOS.scriptdict.script latn.langsysdict.tag enUS.langsys - DEBUG - Optional feature 1 is 1
        """

        logger = kwArgs.pop('logger', logging.getLogger())
        logger = logger.getChild("GPOS")

        logger.debug(
            ('V0001', (w.length(), ), "Walker has %d remaining bytes."))

        if w.length() < 10:
            logger.error(('V0004', (), "Insufficient bytes."))
            return None

        version = otversion.Version.fromvalidatedwalker(w,
                                                        logger=logger,
                                                        **kwArgs)
        slOffset, flOffset, llOffset, fvtOffset = w.unpack("3HL")

        if version.major != 1 or version.minor != 1:
            logger.error(
                ('V0454', (version, ), "Expected version 1.1, but got %s."))

            return None

        if not slOffset:
            logger.warning(
                ('V0455', (), "The ScriptList offset is zero; no further GPOS "
                 "validation will be performed."))

        if not flOffset:
            logger.warning(('V0456', (),
                            "The FeatureList offset is zero; no further GPOS "
                            "validation will be performed."))

        if not llOffset:
            logger.warning(
                ('V0457', (), "The LookupList offset is zero; no further GPOS "
                 "validation will be performed."))

        if not all([slOffset, flOffset, llOffset]):
            return cls()  # note: okay to have null offset to FV table

        delKeys = {'featureIndexToTag', 'forGPOS', 'lookupList'}

        for delKey in delKeys:
            kwArgs.pop(delKey, None)

        fitt = []

        ll = lookuplist.LookupList.fromvalidatedwalker(w.subWalker(llOffset),
                                                       forGPOS=True,
                                                       logger=logger,
                                                       **kwArgs)

        if ll is None:
            return None

        fd = featuredict.FeatureDict.fromvalidatedwalker(
            w.subWalker(flOffset),
            featureIndexToTag=fitt,
            forGPOS=True,
            lookupList=ll,
            logger=logger,
            **kwArgs)

        if fd is None:
            return None

        sd = scriptdict.ScriptDict.fromvalidatedwalker(w.subWalker(slOffset),
                                                       featureIndexToTag=fitt,
                                                       logger=logger,
                                                       **kwArgs)

        if sd is None:
            return None

        if fvtOffset:
            fvt = featurevariations.FeatureVariationsTable.fromvalidatedwalker(
                w.subWalker(fvtOffset), logger=logger, lookupList=ll, **kwArgs)
        else:
            fvt = None

        r = cls(version=version,
                features=fd,
                scripts=sd,
                featureVariations=fvt)
        r._cachedOriginalLookupList = ll.__copy__()
        return r

    @classmethod
    def fromwalker(cls, w, **kwArgs):
        """
        Creates and returns a new GPOS object from the specified walker.
        
        >>> obj = _testingValues[1]

        <<< obj == GPOS.frombytes(obj.binaryString())
        >>> obj.pprint_changes(GPOS.frombytes(obj.binaryString()))
        """

        version = otversion.Version.fromwalker(w, **kwArgs)
        slOffset, flOffset, llOffset, fvtOffset = w.unpack("3HL")
        fitt = []

        if not all([slOffset, flOffset, llOffset]):
            return cls()  # note: okay to have null offset to FV table

        delKeys = {'featureIndexToTag', 'forGPOS', 'lookupList'}

        for delKey in delKeys:
            kwArgs.pop(delKey, None)

        ll = lookuplist.LookupList.fromwalker(w.subWalker(llOffset),
                                              forGPOS=True,
                                              **kwArgs)

        fd = featuredict.FeatureDict.fromwalker(w.subWalker(flOffset),
                                                featureIndexToTag=fitt,
                                                forGPOS=True,
                                                lookupList=ll,
                                                **kwArgs)

        sd = scriptdict.ScriptDict.fromwalker(w.subWalker(slOffset),
                                              featureIndexToTag=fitt,
                                              **kwArgs)

        if fvtOffset:
            fvt = featurevariationstable.FeatureVariationsTable.fromwalker(
                w.subWalker(fvtOffset), lookupList=ll, **kwArgs)
        else:
            fvt = None

        r = cls(version=version,
                features=fd,
                scripts=sd,
                featureVariations=fvt)
        r._cachedOriginalLookupList = ll.__copy__()
        return r
Example #7
0
#

if 0:

    def __________________():
        pass


if __debug__:
    from fontio3 import utilities
    from fontio3.utilities import namer
    from io import StringIO
    from fontio3.opentype.fontworkersource import FontWorkerSource

    _testingValues = (GDEF(),
                      GDEF(version=otversion.Version(),
                           glyphClasses=glyphclass._testingValues[1],
                           attachments=attachlist._testingValues[1],
                           ligCarets=ligcaret._testingValues[1],
                           markClasses=markclass._testingValues[1]))

    _test_FW_namer = namer.Namer(None)
    _test_FW_namer._nameToGlyph = {'a': 5, 'b': 7, 'c': 11}
    _test_FW_namer._initialized = True

    _test_FW_fws = FontWorkerSource(
        StringIO("""FontDame GDEF table

        class definition begin
        a\t1
Example #8
0
class GDEF(object, metaclass=simplemeta.FontDataMetaclass):
    """
    Objects representing entire GDEF tables. These are simple objects with the
    following attribute values:
        version         A Version object.
    
        attachments     An attachlist.AttachListTable object, or None.
        
        glyphClasses    A glyphclass.GlyphClassTable object, or None.
        
        ligCarets       A ligcaret.LigCaretTable object, or None.
        
        markClasses     A markclass.MarkClassTable object, or None.
        
        markSets        A markset.MarkSetTable object, or None.
    
    >>> _testingValues[1].pprint(namer=namer.testingNamer())
    Version:
      Major version: 1
      Minor version: 2
    Glyph Classes:
      Base glyph: xyz8
      Ligature glyph: xyz5 - xyz7, xyz11 - xyz12, xyz16
    Attachment Points:
      afii60003: [1, 4]
      xyz46: [3, 19, 20]
      xyz51: [1, 4]
    Ligature Caret Points:
      afii60003:
        CaretValue #1:
          Caret value in FUnits: 500
          Device record:
            Tweak at 12 ppem: -2
            Tweak at 13 ppem: -1
            Tweak at 16 ppem: 1
            Tweak at 17 ppem: 2
      xyz72:
        CaretValue #1:
          Simple caret value in FUnits: 500
    Mark Classes:
      Mark class 1: xyz8
      Mark class 2: xyz5 - xyz7, xyz11 - xyz12, xyz16
    Mark Sets:
      Mark Glyph Set 0: xyz16, xyz23 - xyz24, xyz31
      Mark Glyph Set 1: xyz13, U+0163
      Mark Glyph Set 2: xyz16, xyz23 - xyz24, xyz31
    """
    
    #
    # Class definition variables
    #
    
    attrSpec = dict(
        version = dict(
            attr_followsprotocol = True,
            attr_initfunc = lambda: otversion.Version(minor=2),
            attr_label = "Version"),
            
        glyphClasses = dict(
            attr_followsprotocol = True,
            attr_initfunc = glyphclass.GlyphClassTable,
            attr_label = "Glyph Classes",
            attr_showonlyiftrue = True),
        
        attachments = dict(
            attr_followsprotocol = True,
            attr_initfunc = attachlist.AttachListTable,
            attr_label = "Attachment Points",
            attr_showonlyiftrue = True),
        
        ligCarets = dict(
            attr_followsprotocol = True,
            attr_initfunc = ligcaret.LigCaretTable,
            attr_label = "Ligature Caret Points",
            attr_showonlyiftrue = True),
        
        markClasses = dict(
            attr_followsprotocol = True,
            attr_initfunc = markclass.MarkClassTable,
            attr_label = "Mark Classes",
            attr_showonlyiftrue = True),
        
        markSets = dict(
            attr_followsprotocol = True,
            attr_initfunc = markset.MarkSetTable,
            attr_label = "Mark Sets",
            attr_showonlyiftrue = True))
    
    attrSorted = (
      'version',
      'glyphClasses',
      'attachments',
      'ligCarets',
      'markClasses',
      'markSets')
    
    #
    # Methods
    #
    
    def buildBinary(self, w, **kwArgs):
        """
        Adds the binary data to the specified LinkedWriter.
        
        >>> utilities.hexdump(_testingValues[1].binaryString())
               0 | 0001 0002 000E 002A  004C 0078 0094 0002 |.......*.L.x....|
              10 | 0004 0004 0006 0002  0007 0007 0001 000A |................|
              20 | 000B 0002 000F 000F  0002 000A 0003 0014 |................|
              30 | 001C 001C 0001 0003  002D 0032 0062 0003 |.........-.2.b..|
              40 | 0003 0013 0014 0002  0001 0004 0008 0002 |................|
              50 | 0010 0014 0001 0002  0047 0062 0001 0008 |.........G.b....|
              60 | 0001 0008 0001 01F4  0003 01F4 0006 000C |................|
              70 | 0011 0002 EF00 1200  0002 0004 0004 0006 |................|
              80 | 0002 0007 0007 0001  000A 000B 0002 000F |................|
              90 | 000F 0002 0001 0003  0000 0010 0000 001C |................|
              A0 | 0000 0010 0001 0004  000F 0016 0017 001E |................|
              B0 | 0001 0002 000C 0063                      |.......c        |
        """
        
        if 'stakeValue' in kwArgs:
            stakeValue = kwArgs.pop('stakeValue')
            w.stakeCurrentWithValue(stakeValue)
        else:
            stakeValue = w.stakeCurrent()
        
        # Add content and unresolved references
        w.add("L", 0x10002)
        d = self.__dict__
        toBeWritten = []
        
        san = list(self.getSortedAttrNames())
        san.remove('version')
        for key in san:
            table = d[key]
            
            if table:
                tableStake = w.getNewStake()
                toBeWritten.append((table, tableStake))
                w.addUnresolvedOffset("H", stakeValue, tableStake)
            
            else:
                w.add("H", 0)
        
        # Resolve the references
        for table, tableStake in toBeWritten:
            table.buildBinary(w, stakeValue=tableStake, **kwArgs)
    
    @classmethod
    def frommtxxtables(cls, editor, **kwArgs):
        """
        Returns a GDEF object initialized via the MTxx (and other) tables in
        the specified editor.
        """
        
        if editor is None:
            return cls()
        
        hm = editor.get(b'hmtx')
        
        if not hm:
            return cls()
        
        sf = editor.get(b'MTsf', {})
        # when there's a MTxx table for marksets, add that here
        
        glyphClasses = glyphclass.GlyphClassTable()
        attachments = attachlist.AttachListTable()
        ligCarets = ligcaret.LigCaretTable()
        markClasses = markclass.MarkClassTable()
        markSets = markset.MarkSetTable()
        excluded = frozenset([0xFFFF, 0xFFFE])
        
        for glyphIndex, mtapRecord in editor.get(b'MTap', {}).items():
            # glyph class
            glyphClass = mtapRecord.glyphClass
            
            if glyphClass:
                glyphClasses[glyphIndex] = glyphClass
            
            # attachments
            anchor = mtapRecord.anchorData
            
            t1 = (
              () if anchor is None
              else tuple(anchor.pointGenerator(excludeFFFx=True)))
            
            conn = mtapRecord.connectionData

            if conn is None:
                t2 = ()
            
            else:
                it = conn.pointGenerator()
                
                t2 = tuple(
                  c.pointIndex
                  for c in it
                  if c.pointIndex not in excluded)
            
            if t1 or t2:
                attachments[glyphIndex] = attachlist.AttachPointTable(t1 + t2)
            
            # lig carets
            if glyphClass == 2: # ligature
                caretValues = mtapRecord.caretValuesFromMetrics(
                  hm[glyphIndex][0])
                
                if caretValues:
                    ligCarets[glyphIndex] = ligcaret.LigGlyphTable(caretValues)
            
            # marks
            elif glyphClass == 3 and sf: # mark
                markClass = sf[glyphIndex].originalClassIndex
                
                markClasses[glyphIndex] = (
                  0 if markClass == 0xFFFF
                  else markClass + 1)
        
        return cls(
          attachments = attachments,
          glyphClasses = glyphClasses,
          ligCarets = ligCarets,
          markClasses = markClasses,
          markSets = markSets)
    
    @classmethod
    def fromValidatedFontWorkerSource(cls, fws, **kwArgs):
        """
        Creates and returns a new GDEF from the specified FontWorker Source
        code with extensive validation via the logging module (the client
        should have done a logging.basicConfig call prior to calling this
        method, unless a logger is passed in via the 'logger' keyword argument).

        >>> _test_FW_fws.goto(1) # go back to first line
        >>> logger = utilities.makeDoctestLogger('test_FW')
        >>> gdef = GDEF.fromValidatedFontWorkerSource(_test_FW_fws, namer=_test_FW_namer, logger=logger, editor=None)
        >>> gdef.pprint()
        Version:
          Major version: 1
          Minor version: 2
        Glyph Classes:
          Base glyph: 5
          Ligature glyph: 7
          Mark glyph: 11
        Attachment Points:
          5: [23]
          7: [29, 31]
          11: [37, 41, 43]
        Ligature Caret Points:
          5:
            CaretValue #1:
              Simple caret value in FUnits: 42
          7:
            CaretValue #1:
              Simple caret value in FUnits: 50
            CaretValue #2:
              Simple caret value in FUnits: 55
          11:
            CaretValue #1:
              Simple caret value in FUnits: 12
            CaretValue #2:
              Simple caret value in FUnits: 17
            CaretValue #3:
              Simple caret value in FUnits: 37
        Mark Classes:
          Mark class 8: 5
          Mark class 10: 7
          Mark class 12: 11
        Mark Sets:
          Mark Glyph Set 0: 5, 7
          Mark Glyph Set 1: 5
          Mark Glyph Set 2: 11
        >>> gdef2 = GDEF.fromValidatedFontWorkerSource(_test_FW_fws2, namer=_test_FW_namer, logger=logger, editor=None)
        test_FW.GDEF.classDef - WARNING - line 8 -- glyph 'w' not found
        test_FW.GDEF.attachlist - WARNING - line 13 -- missing attachment points
        test_FW.GDEF.attachlist - WARNING - line 16 -- glyph 'x' not found
        test_FW.GDEF.ligcaret - ERROR - line 21 -- mismatch between caret count (2);and number of carets actually listed (3); discarding.
        test_FW.GDEF.ligcaret - ERROR - line 23 -- mismatch between caret count (3);and number of carets actually listed (2); discarding.
        test_FW.GDEF.ligcaret - WARNING - line 24 -- glyph 'y' not found
        test_FW.GDEF.classDef - WARNING - line 31 -- incorrect number of tokens, expected 2, found 1
        test_FW.GDEF.classDef - WARNING - line 32 -- glyph 'z' not found
        test_FW.GDEF.markset - WARNING - line 40 -- glyph 'asdfjkl' not found
        test_FW.GDEF.markset - WARNING - MarkFilterSet 1 will be renumbered to 0
        test_FW.GDEF.markset - WARNING - MarkFilterSet 99 will be renumbered to 1
        >>> gdef2.pprint()
        Version:
          Major version: 1
          Minor version: 2
        Glyph Classes:
          Base glyph: 5
          Ligature glyph: 7
          Mark glyph: 11
        Attachment Points:
          5: [23]
          11: [37, 41, 43]
        Ligature Caret Points:
          5:
            CaretValue #1:
              Simple caret value in FUnits: 42
        Mark Classes:
          Mark class 1: 5
          Mark class 2: 7
        Mark Sets:
          Mark Glyph Set 0: 5, 11
          Mark Glyph Set 1: 5, 7
        """

        editor = kwArgs['editor']
        namer = kwArgs['namer']

        logger = kwArgs.pop('logger', None)

        if logger is None:
            logger = logging.getLogger().getChild('GDEF')
        else:
            logger = logger.getChild('GDEF')

        # create empty versions of these tables for now,
        # replace them with full versions later if the data is available
        glyphClasses = glyphclass.GlyphClassTable()
        attachments = attachlist.AttachListTable()
        ligCarets = ligcaret.LigCaretTable()
        markClasses = markclass.MarkClassTable()
        markSets = markset.MarkSetTable()

        firstLine = 'FontDame GDEF table'
        line = next(fws)
        if line != firstLine:
            logger.error((
              'V0958',
              (firstLine,),
              "Expected '%s' in first line."))
            return None

        for line in fws:
            if len(line) != 0:
                if line.lower() == 'class definition begin':
                    glyphClasses = \
                        glyphclass.GlyphClassTable.fromValidatedFontWorkerSource(
                        fws, namer=namer, logger=logger)
                elif line.lower() == 'attachment list begin':
                    attachments = \
                        attachlist.AttachListTable.fromValidatedFontWorkerSource(
                        fws, namer=namer, logger=logger)
                elif line.lower() == 'carets begin':
                    ligCarets = \
                        ligcaret.LigCaretTable.fromValidatedFontWorkerSource(
                            fws, namer=namer, logger=logger, editor=editor)
                elif line.lower() == 'mark attachment class definition begin':
                    markClasses = \
                        markclass.MarkClassTable.fromValidatedFontWorkerSource(
                            fws, namer=namer, logger=logger)
                elif line.lower() == 'markfilter set definition begin':
                    markSets = \
                        markset.MarkSetTable.fromValidatedFontWorkerSource(
                            fws, namer=namer, logger=logger)
                else:
                    logger.warning(('V0960',
                        (fws.lineNumber, line),
                        'line %d -- unexpected token: \'%s\''))

        return cls(
            attachments = attachments,
            glyphClasses = glyphClasses,
            ligCarets = ligCarets,
            markClasses = markClasses,
            markSets = markSets)

    @classmethod
    def fromvalidatedwalker(cls, w, **kwArgs):
        """
        Like fromwalker(), this method returns a new GDEF. However, it also
        does extensive validation via the logging module (the client should
        have done a logging.basicConfig call prior to calling this method,
        unless a logger is passed in via the 'logger' keyword argument).
        
        >>> logger = utilities.makeDoctestLogger('test')
        >>> s = _testingValues[1].binaryString()
        >>> fvb = GDEF.fromvalidatedbytes
        >>> obj = fvb(s, logger=logger)
        test.GDEF - DEBUG - Walker has 184 remaining bytes.
        test.GDEF.version - DEBUG - Walker has 184 remaining bytes.
        test.GDEF.glyphclass.classDef - DEBUG - Walker has 170 remaining bytes.
        test.GDEF.glyphclass.classDef - DEBUG - ClassDef is format 2.
        test.GDEF.glyphclass.classDef - DEBUG - Count is 4
        test.GDEF.glyphclass.classDef - DEBUG - Raw data are [(4, 6, 2), (7, 7, 1), (10, 11, 2), (15, 15, 2)]
        test.GDEF.attachlist - DEBUG - Walker has 142 remaining bytes.
        test.GDEF.attachlist.coverage - DEBUG - Walker has 132 remaining bytes.
        test.GDEF.attachlist.coverage - DEBUG - Format is 1, count is 3
        test.GDEF.attachlist.coverage - DEBUG - Raw data are [45, 50, 98]
        test.GDEF.attachlist - INFO - Number of groups: 3.
        test.GDEF.ligcaret - DEBUG - Walker has 108 remaining bytes.
        test.GDEF.ligcaret.coverage - DEBUG - Walker has 100 remaining bytes.
        test.GDEF.ligcaret.coverage - DEBUG - Format is 1, count is 2
        test.GDEF.ligcaret.coverage - DEBUG - Raw data are [71, 98]
        test.GDEF.ligcaret - INFO - Number of LigGlyphTables: 2.
        test.GDEF.ligcaret.[0].ligglyph - DEBUG - Walker has 92 remaining bytes.
        test.GDEF.ligcaret.[0].ligglyph - INFO - Number of splits: 1.
        test.GDEF.ligcaret.[0].ligglyph.[0].caretvalue.coordinate - DEBUG - Walker has 84 remaining bytes.
        test.GDEF.ligcaret.[1].ligglyph - DEBUG - Walker has 88 remaining bytes.
        test.GDEF.ligcaret.[1].ligglyph - INFO - Number of splits: 1.
        test.GDEF.ligcaret.[1].ligglyph.[0].caretvalue.device - DEBUG - Walker has 80 remaining bytes.
        test.GDEF.ligcaret.[1].ligglyph.[0].caretvalue.device.device - DEBUG - Walker has 74 remaining bytes.
        test.GDEF.ligcaret.[1].ligglyph.[0].caretvalue.device.device - DEBUG - StartSize=12, endSize=17, format=2
        test.GDEF.ligcaret.[1].ligglyph.[0].caretvalue.device.device - DEBUG - Data are (61184, 4608)
        test.GDEF.markclass.classDef - DEBUG - Walker has 64 remaining bytes.
        test.GDEF.markclass.classDef - DEBUG - ClassDef is format 2.
        test.GDEF.markclass.classDef - DEBUG - Count is 4
        test.GDEF.markclass.classDef - DEBUG - Raw data are [(4, 6, 2), (7, 7, 1), (10, 11, 2), (15, 15, 2)]
        test.GDEF.markset - DEBUG - Walker has 36 remaining bytes.
        test.GDEF.markset - INFO - MarkSet has 3 element(s).
        test.GDEF.markset.[0].coverage - DEBUG - Walker has 20 remaining bytes.
        test.GDEF.markset.[0].coverage - DEBUG - Format is 1, count is 4
        test.GDEF.markset.[0].coverage - DEBUG - Raw data are [15, 22, 23, 30]
        test.GDEF.markset.[1].coverage - DEBUG - Walker has 8 remaining bytes.
        test.GDEF.markset.[1].coverage - DEBUG - Format is 1, count is 2
        test.GDEF.markset.[1].coverage - DEBUG - Raw data are [12, 99]
        test.GDEF.markset.[2].coverage - DEBUG - Walker has 20 remaining bytes.
        test.GDEF.markset.[2].coverage - DEBUG - Format is 1, count is 4
        test.GDEF.markset.[2].coverage - DEBUG - Raw data are [15, 22, 23, 30]
        
        >>> fvb(s[:1], logger=logger)
        test.GDEF - DEBUG - Walker has 1 remaining bytes.
        test.GDEF - ERROR - Insufficient bytes.
        """
        
        logger = kwArgs.pop('logger', None)
        
        if logger is None:
            logger = logging.getLogger().getChild('GDEF')
        else:
            logger = logger.getChild('GDEF')
        
        logger.debug((
          'V0001',
          (w.length(),),
          "Walker has %d remaining bytes."))
        
        if w.length() < 4:
            logger.error(('V0004', (), "Insufficient bytes."))
            return None
        
        version = otversion.Version.fromvalidatedwalker(w, logger=logger, **kwArgs)

        if version.major != 1 or version.minor != 2:
            logger.error((
              'V0113',
              (version,),
              "Expected version 1.2, got %s."))
            
            return None
        
        if w.length() < 10:
            logger.error(('V0114', (), "Insufficient bytes for offsets."))
            return None
        
        r = cls(version=version)

        stoffsets = w.group("H", 5)

        san = list(r.getSortedAttrNames())
        loggers = [logger] * 5
        loggers[0] = logger.getChild("glyphclass")
        loggers[3] = logger.getChild("markclass")
        for sti, sto in enumerate(stoffsets):
            if sto:
                fw = _makers[sti].fromvalidatedwalker
                wSub = w.subWalker(sto)
                st = fw(wSub, logger=loggers[sti], **kwArgs)
                r.__dict__[san[sti+1]] = st
            else:
                r.__dict__[san[sti+1]] = _makers[sti]()
                
        return r
    
    @classmethod
    def fromwalker(cls, w, **kwArgs):
        """
        Creates and returns a new GDEF object from the specified walker.
        
        >>> obj = _testingValues[1]
        >>> obj == GDEF.frombytes(obj.binaryString())
        True
        """

        version = otversion.Version.fromwalker(w, **kwArgs)
        
        if version.major != 1 or version.minor != 2:
            raise ValueError("Unsupported GDEF %s" % (version,))
        
        r = cls(version=version)

        stoffsets = w.group("H", 5)

        san = list(r.getSortedAttrNames())
        for sti, sto in enumerate(stoffsets):
            if sto:
                fw = _makers[sti].fromwalker
                wSub = w.subWalker(sto)
                st = fw(wSub, **kwArgs)
                r.__dict__[san[sti+1]] = st
            else:
                r.__dict__[san[sti+1]] = _makers[sti]()
                
        return r


    def writeFontWorkerSource(self, s, **kwArgs):
        """
        Write the GDEF table to stream 's' in Font Worker dump format.
        kwArg 'namer' is required.
        """
        namer = kwArgs.get('namer')
        bnfgi = namer.bestNameForGlyphIndex
            
        s.write("FontDame GDEF table\n\n")

        if self.glyphClasses:
            s.write("class definition begin\n")
            for gi in sorted(
              self.glyphClasses,
              key=lambda x:(self.glyphClasses[x], x)):
                gc = self.glyphClasses[gi]
                s.write("%s\t%d\n" % (bnfgi(gi), gc))
            s.write("class definition end\n\n")

        if self.attachments:
            s.write("attachment list begin\n")
            for gi,atl in sorted(self.attachments.items()):
                s.write("%s\t%s\n" % (bnfgi(gi), "\t".join([str(at) for at in atl])))
            s.write("attachment list end\n\n")
            
        if self.ligCarets:
            s.write("carets begin\n")
            for gi,lc in sorted(self.ligCarets.items()):
                s.write("%s\t%d\t%s\n" % (
                  bnfgi(gi),
                  len(lc),
                  "\t".join([str(c) for c in lc])))
            s.write("carets end\n\n")
            
        if self.markClasses:
            s.write("mark attachment class definition begin\n")
            for gi in sorted(
              self.markClasses,
              key=lambda x:(self.markClasses[x], x)):
                mc = self.markClasses[gi]
                s.write("%s\t%d\n" % (bnfgi(gi), mc))
            s.write("class definition end\n\n")
            
        if self.markSets:
            s.write("markfilter set definition begin\n")
            for gsi, gs in enumerate(self.markSets):
                for gi in sorted(gs):
                    s.write("%s\t%d\n" % (bnfgi(gi), gsi))
            s.write("set definition end\n\n")
Example #9
0
#

if 0:
    def __________________(): pass

if __debug__:
    from fontio3 import utilities
    from fontio3.utilities import namer
    from io import StringIO
    from fontio3.opentype.fontworkersource import FontWorkerSource

    _testingValues = (
        GDEF(),
        
        GDEF(
          version = otversion.Version(minor=2),
          glyphClasses = glyphclass._testingValues[1],
          attachments = attachlist._testingValues[1],
          ligCarets = ligcaret._testingValues[1],
          markClasses = markclass._testingValues[1],
          markSets = markset._testingValues[1]))

    _test_FW_namer = namer.Namer(None)
    _test_FW_namer._nameToGlyph = {
        'a': 5,
        'b': 7,
        'c': 11
    }
    _test_FW_namer._initialized = True

    _test_FW_fws = FontWorkerSource(StringIO(
class FeatureVariations(dict, metaclass=mapmeta.FontDataMetaclass):
    """
    Objects representing FeatureVariations Tables. These are maps of
    ConditionSets to FeatureTableSubsts. Clients can test a given set of axial
    coordinates against each ConditionSet to obtain a FeatureTableSubst to use
    in the case of a match.
    """

    #
    # Class definition variables
    #

    mapSpec = dict(item_keyfollowsprotocol=True, item_followsprotocol=True)

    version = otversion.Version(major=1, minor=0)

    def __iter__(self):
        """
        Returns a custom iterator that respects the ConditionSet ordering.
        
        xxx add doctests...
        """

        d = dict(self)  # avoid infinite loop in __iter__()...

        for k in sorted(d, key=operator.attrgetter('sequence')):
            yield k

    def buildBinary(self, w, **kwArgs):
        """
        Adds the binary data for the FeatureVariations Table object to the
        specified LinkedWriter.
        
        >>> b = _testingValues[0].binaryString()
        >>> utilities.hexdump(b)
               0 | 0001 0000 0000 0000                      |........        |
        
        >>> ao = ('wght', 'wdth')
        >>> ll = lltv[1]
        >>> ttfi = {b'fea10001':0, b'test0001':1}
        >>> bsf = _testingValues[1].binaryString
        >>> b = bsf(axisOrder=ao, lookupList=ll, tagToFeatureIndex=ttfi)
        >>> utilities.hexdump(b)
               0 | 0001 0000 0000 0001  0000 0010 0000 002A |...............*|
              10 | 0002 0000 000A 0000  0012 0001 0001 C000 |................|
              20 | 4000 0001 0000 0010  4000 0001 0000 0001 |@.......@.......|
              30 | 0001 0000 000C 0000  0001 0001           |............    |
        """

        if 'stakeValue' in kwArgs:
            stakeValue = kwArgs.pop('stakeValue')
            w.stakeCurrentWithValue(stakeValue)
        else:
            stakeValue = w.stakeCurrent()

        FeatureVariations.version.buildBinary(w, **kwArgs)

        reccount = len(self)
        w.add("L", reccount)

        stakesDict = dict()

        for i in range(len(self)):
            stakesDict[i] = (w.getNewStake(), w.getNewStake())
            w.addUnresolvedOffset("L", stakeValue, stakesDict[i][0])
            w.addUnresolvedOffset("L", stakeValue, stakesDict[i][1])

        for i, cs in enumerate(sorted(self)):  # seq order thanks to __iter__()
            fts = self[cs]
            w.stakeCurrentWithValue(stakesDict[i][0])
            cs.buildBinary(w, **kwArgs)
            w.stakeCurrentWithValue(stakesDict[i][1])
            fts.buildBinary(w, **kwArgs)

    @classmethod
    def fromvalidatedwalker(cls, w, **kwArgs):
        """
        Creates and returns a new FeatureVariations object from the specified walker,
        doing source validation.
        
        >>> logger = utilities.makeDoctestLogger("test")
        >>> s = ("00010000"                                # Version
        ...      "00000001"                                # count
        ...      "00000010 0000001E"                       # offsets[0]
        ...      "0001 00000006 0001 0000 0666 4000"       # CondSetData [0]
        ...      "00010000 0001 0001 0000000C 0000 0000")  # FeatTableSubst data [0]
        >>> b = utilities.fromhex(s)
        >>> ll = lltv[1]
        >>> ao = ('wght', 'wdth')
        >>> fitt = [b'fea10001', b'feat20002']
        >>> fvb = FeatureVariations.fromvalidatedbytes
        >>> obj = fvb(b, featureIndexToTag=fitt, logger=logger, lookupList=ll, axisOrder=ao)
        test.featurevariations - DEBUG - Walker has 46 remaining bytes.
        test.version - DEBUG - Walker has 46 remaining bytes.
        test.featurevariations - INFO - FeatureVariationRecordCount is 1
        test.featurevariations - DEBUG - offsetConditionSet: 0x00000010, offsetFeatureTableSubst: 0x0000001E
        test.conditionset - DEBUG - Walker has 30 remaining bytes.
        test.conditionset - INFO - ConditionCount is 1
        test.condition - DEBUG - Walker has 24 remaining bytes.
        test.condition - INFO - AxisTag 'wght'
        test.condition - INFO - Min: 0.099976, Max: 1.000000
        test.featuretablesubst - DEBUG - Walker has 16 remaining bytes.
        test.version - DEBUG - Walker has 16 remaining bytes.
        test.featuretablesubst - INFO - SubstitutionCount is 1
        test.featuretablesubst - DEBUG - Feature index 1, offset 0x0000000C
        test.featuretable - DEBUG - Walker has 4 remaining bytes.
        test.featuretable - DEBUG - FeatureParams offset is 0, lookupCount is 0

        >>> sorted(obj.keys())
        [ConditionSet(frozenset({Condition(axisTag='wght', filterMin=0.0999755859375, filterMax=1.0)}), sequence=0)]
        """

        logger = kwArgs.get('logger', None)

        if logger is None:
            logger = logging.getLogger()

        logger = logger.getChild("featurevariations")

        logger.debug(
            ('V0001', (w.length(), ), "Walker has %d remaining bytes."))

        if w.length() < 8:
            logger.error(('V0004', (), "Insufficient bytes."))
            return None

        vers = otversion.Version.fromvalidatedwalker(w, **kwArgs)

        if (vers.major != FeatureVariations.version.major
                or vers.minor != FeatureVariations.version.minor):
            logger.error(('V0002', (vers, ), "Expected Version 1.0, got %s"))

            return None

        count = w.unpack("L")

        logger.info(('Vxxxx', (count, ), "FeatureVariationRecordCount is %d"))

        if w.length() < count * 8:
            logger.error(
                ('V0004', (count, ), "Insufficient bytes for count of %d"))
            return None

        offsets = w.group("LL",
                          count)  # pre-check offsets before attempting read?

        csfvw = conditionset.ConditionSet.fromvalidatedwalker
        ftsfvw = featuretablesubst.FeatureTableSubst.fromvalidatedwalker

        d = {}

        for seq, (cso, ftso) in enumerate(offsets):
            logger.debug((
                'Vxxxx', (cso, ftso),
                "offsetConditionSet: 0x%08X, offsetFeatureTableSubst: 0x%08X"))

            if cso:
                wSubC = w.subWalker(cso)
                cs = csfvw(wSubC, sequence=seq, **kwArgs)  # ConditionSet
            else:
                cs = None

            if ftso:
                wSubF = w.subWalker(ftso)
                fts = ftsfvw(wSubF, **kwArgs)
            else:
                fts = None

            d[cs] = fts

        r = cls(d)

        return r

    @classmethod
    def fromwalker(cls, w, **kwArgs):
        """
        Creates and returns a new FeatureVariations object from the specified
        walker.

        >>> s = ("00010000"                                # Version
        ...      "00000001"                                # count
        ...      "00000010 0000001E"                       # offsets[0]
        ...      "0001 00000006 0001 0000 0666 4000"       # CondSetData [0]
        ...      "00010000 0001 0001 0000000C 0000 0000")  # FeatTableSubst data [0]
        >>> b = utilities.fromhex(s)
        >>> ll = lltv[1]
        >>> ao = ('wght', 'wdth')
        >>> fb = FeatureVariations.frombytes
        >>> fitt = [b'fea10001', b'fea20002']
        >>> obj = fb(b, featureIndexToTag=fitt, lookupList=ll, axisOrder=ao)
        >>> sorted(obj.keys())
        [ConditionSet(frozenset({Condition(axisTag='wght', filterMin=0.0999755859375, filterMax=1.0)}), sequence=0)]
        """

        vers = otversion.Version.fromwalker(w, **kwArgs)

        if vers.major != 1 and vers.minor != 0:
            raise ValueError("Expected version 1.0, got %s" % (vers, ))

        count = w.unpack("L")
        offsets = w.group("LL", count)

        csfw = conditionset.ConditionSet.fromwalker
        ftsfw = featuretablesubst.FeatureTableSubst.fromwalker

        d = {}

        for seq, (cso, ftso) in enumerate(offsets):
            if cso:
                wSubC = w.subWalker(cso)
                cs = csfw(wSubC, sequence=seq, **kwArgs)
            else:
                cs = None

            if ftso:
                wSubF = w.subWalker(ftso)
                fts = ftsfw(wSubF, **kwArgs)
            else:
                fts = None

            d[cs] = fts

        r = cls(d)

        return r