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")
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
# 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(
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
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
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
# 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
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")
# 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