def _makeTV(): from fontio3.GSUB import single from fontio3.opentype import ( classdef, lookup, pschainclass_classtuple, pschainclass_key, pslookupgroup, pslookuprecord) single_obj = single.Single({2: 41, 4: 42, 6: 43}) lookup_obj = lookup.Lookup([single_obj], sequence=0) pslookuprecord_obj = pslookuprecord.PSLookupRecord(0, lookup_obj) pslookupgroup_obj = pslookupgroup.PSLookupGroup([pslookuprecord_obj]) tBack = pschainclass_classtuple.ClassTuple([1, 2]) tIn = pschainclass_classtuple.ClassTuple([1]) tLook = pschainclass_classtuple.ClassTuple([1]) key_obj = pschainclass_key.Key([tBack, tIn, tLook]) cdBack = classdef.ClassDef({51: 1, 52: 1, 54: 2, 55: 2}) cdIn = classdef.ClassDef({2: 1, 4: 1, 6: 1}) cdLook = classdef.ClassDef({80: 1, 81: 1, 82: 1}) return ChainClass( {key_obj: pslookupgroup_obj}, classDefBacktrack = cdBack, classDefInput = cdIn, classDefLookahead = cdLook)
def fromwalker(cls, w, **kwArgs): """ Creates and returns a Format3 object from the specified walker, which should start at the subtable header. The following keyword arguments are suggested (if they are not present, the default values for coverage and tupleIndex will be used, which won't usually be what's wanted): coverage A Coverage object. tupleIndex The variations tuple index. >>> obj = _testingValues[1] >>> obj == Format3.frombytes(obj.binaryString(), coverage=obj.coverage) True """ glyphCount, kCount, lCount, rCount = w.unpack("4H2x") # flags not used kValues = w.group("h", kCount) lClasses = w.group("H", glyphCount) rClasses = w.group("H", glyphCount) indices = w.group("H", lCount * rCount) # Note the assumption implicit in the following two lines of code: the # [0] element of the kernValue array has a numeric value of zero FUnits # (i.e. a non-kerning pair). This assumption is not documented in # Apple's 'kerx' table documentation. leftCD = classdef.ClassDef((g, c) for g, c in enumerate(lClasses) if c) rightCD = classdef.ClassDef( (g, c) for g, c in enumerate(rClasses) if c) for delKey in {'leftClassDef', 'rightClassDef'}: kwArgs.pop(delKey, None) r = cls({}, leftClassDef=leftCD, rightClassDef=rightCD, **utilities.filterKWArgs(cls, kwArgs)) i = 0 for rowIndex in range(lCount): for colIndex in range(rCount): value = kValues[indices[i]] if value: r[ClassPair([rowIndex, colIndex])] = value i += 1 return r
def fromwalker(cls, w, **kwArgs): """ Creates and returns a Format3 object from the specified walker. >>> obj = _testingValues[1] >>> obj == Format3.frombytes(obj.binaryString(), coverage=obj.coverage) True """ glyphCount, kCount, lCount, rCount = w.unpack("H3Bx") # flags not used kValues = w.group("h", kCount) lClasses = w.group("B", glyphCount) rClasses = w.group("B", glyphCount) indices = w.group("B", lCount * rCount) # Note the assumption implicit in the following two lines of code: the # [0] element of the kernValue array has a numeric value of zero FUnits # (i.e. a non-kerning pair). This assumption is not documented in # Apple's 'kern' table documentation. leftCD = classdef.ClassDef((g, c) for g, c in enumerate(lClasses) if c) rightCD = classdef.ClassDef( (g, c) for g, c in enumerate(rClasses) if c) for delKey in {'leftClassDef', 'rightClassDef'}: kwArgs.pop(delKey, None) r = cls({}, leftClassDef=leftCD, rightClassDef=rightCD, **utilities.filterKWArgs(cls, kwArgs)) i = 0 CP = classpair.ClassPair for rowIndex in range(lCount): for colIndex in range(rCount): value = kValues[indices[i]] if value: r[CP([rowIndex, colIndex])] = value i += 1 return r
def _makeTest(): """ The test case here recognizes the glyph sequence 1-2-3, and adjusts the positioning of class 2. """ from fontio3 import hmtx, utilities from fontio3.GPOS import single, value from fontio3.opentype import (classdef, lookup, pschainclass_classtuple, pschainclass_key, pslookupgroup, pslookuprecord) v1 = value.Value(xPlacement=-25) v2 = value.Value(xPlacement=-29) s1 = single.Single({80: v1, 81: v2, 82: v1}) lk1 = lookup.Lookup([s1]) psr1 = pslookuprecord.PSLookupRecord(0, lk1) psg = pslookupgroup.PSLookupGroup([psr1]) backPart = pschainclass_classtuple.ClassTuple([1]) inPart = pschainclass_classtuple.ClassTuple([1]) lookPart = pschainclass_classtuple.ClassTuple([1]) key = pschainclass_key.Key([backPart, inPart, lookPart]) cdBack = classdef.ClassDef({25: 1}) cdIn = classdef.ClassDef({80: 1, 81: 1, 82: 1}) cdLook = classdef.ClassDef({26: 1}) r = ChainClass({key: psg}, classDefBacktrack=cdBack, classDefInput=cdIn, classDefLookahead=cdLook) e = utilities.fakeEditor(0x10000) e.hmtx = hmtx.Hmtx() e.hmtx[25] = hmtx.MtxEntry(900, 50) e.hmtx[26] = hmtx.MtxEntry(970, 40) e.hmtx[80] = hmtx.MtxEntry(1020, 55) e.hmtx[81] = hmtx.MtxEntry(1090, 85) e.hmtx[82] = hmtx.MtxEntry(1050, 70) return r, e
def _makeTV(): from fontio3.GSUB import single cdBack = classdef.ClassDef({10: 1, 11: 1}) cdIn = classdef.ClassDef({20: 1, 21: 1, 22: 2, 40: 3, 41: 3}) cdLook = classdef.ClassDef({30: 1}) back1 = pschainclass_classtuple.ClassTuple([1]) in1 = pschainclass_classtuple.ClassTuple([1, 2]) look1 = pschainclass_classtuple.ClassTuple([1]) key1 = pschainclass_key.Key([back1, in1, look1], ruleOrder=0) sgl1 = single.Single({20: 40, 21: 41}) lkup1 = lookup.Lookup([sgl1], sequence=22) rec1 = pslookuprecord.PSLookupRecord(sequenceIndex=0, lookup=lkup1) grp1 = pslookupgroup.PSLookupGroup([rec1]) obj = PSChainClass({key1: grp1}, classDefBacktrack=cdBack, classDefInput=cdIn, classDefLookahead=cdLook) return obj
def _makeTV(): from fontio3.GSUB import single from fontio3.opentype import (classdef, lookup, pscontextclass_key, pslookupgroup, pslookuprecord) single_obj = single.Single({2: 41, 8: 42}) lookup_obj = lookup.Lookup([single_obj], sequence=0) pslookuprecord_obj = pslookuprecord.PSLookupRecord(1, lookup_obj) pslookupgroup_obj = pslookupgroup.PSLookupGroup([pslookuprecord_obj]) key_obj = pscontextclass_key.Key([1, 2, 1, 3]) classdef_obj = classdef.ClassDef({19: 1, 2: 2, 8: 2, 12: 3}) return ContextClass({key_obj: pslookupgroup_obj}, classDef=classdef_obj)
def _makeTest(): """ The test case here recognizes the glyph sequence 1-2-3, and adjusts the positioning of classes 1 and 3. """ from fontio3 import hmtx, utilities from fontio3.GPOS import single, value from fontio3.opentype import ( classdef, lookup, pscontextclass_key, pslookupgroup, pslookuprecord) v1 = value.Value(xPlacement=-25) v2 = value.Value(xPlacement=-29) s1 = single.Single({20: v1, 25: v2}) lk1 = lookup.Lookup([s1]) psr1 = pslookuprecord.PSLookupRecord(0, lk1) s2 = single.Single({26: value.Value(xPlacement=25)}) lk2 = lookup.Lookup([s2]) psr2 = pslookuprecord.PSLookupRecord(2, lk2) psg = pslookupgroup.PSLookupGroup([psr1, psr2]) key = pscontextclass_key.Key([1, 2, 3]) cd = classdef.ClassDef({20: 1, 25: 1, 80: 2, 81: 2, 26: 3}) r = ContextClass({key: psg}, classDef=cd) e = utilities.fakeEditor(0x10000) e.hmtx = hmtx.Hmtx() e.hmtx[20] = hmtx.MtxEntry(910, 42) e.hmtx[25] = hmtx.MtxEntry(900, 50) e.hmtx[26] = hmtx.MtxEntry(970, 40) e.hmtx[80] = hmtx.MtxEntry(1020, 55) e.hmtx[81] = hmtx.MtxEntry(1090, 85) return r, e
def fromValidatedFontWorkerSource(cls, fws, **kwArgs): """ Creates and returns a new PairClasses object from the specified FontWorkerSource, with validation of the source data. >>> logger = utilities.makeDoctestLogger("FW_test") >>> pc = PairClasses.fromValidatedFontWorkerSource(_test_FW_fws2, namer=_test_FW_namer, logger=logger) FW_test.pairclasses - ERROR - line 13 -- unexpected token: foo >>> pc.pprint() (First class 1, Second class 2): First adjustment: FUnit adjustment to origin's x-coordinate: -123 (First class 2, Second class 1): Second adjustment: FUnit adjustment to origin's x-coordinate: -456 Class definition table for first glyph: 2: 1 3: 2 Class definition table for second glyph: 23: 1 29: 2 """ terminalStrings = ('subtable end', 'lookup end') startingLineNumber=fws.lineNumber iskernset = kwArgs.get('iskernset', False) logger = kwArgs.pop('logger', logging.getLogger()) logger = logger.getChild("pairclasses") fvfws = classdef.ClassDef.fromValidatedFontWorkerSource PV = pairvalues.PairValues V = value.Value tokenSet = frozenset({ 'left x advance', 'left x placement', 'left y advance', 'left y placement', 'right x advance', 'right x placement', 'right y advance', 'right y placement'}) # place-holders classDef1 = classdef.ClassDef() classDef2 = classdef.ClassDef() pairDict = defaultdict(lambda: [None, None]) for line in fws: if line.lower().strip() in terminalStrings: if iskernset and line.lower() == 'subtable end': continue else: npd, nc1, nc2 = _reindexed( pairDict, classDef1, classDef2, logger = logger) return cls(npd, classDef1=nc1, classDef2=nc2) if len(line) > 0: tokens = [x.strip() for x in line.split('\t')] if tokens[0].lower() == 'firstclass definition begin': classDef1 = fvfws(fws, logger=logger, **kwArgs) elif tokens[0].lower() == 'secondclass definition begin': classDef2 = fvfws(fws, logger=logger, **kwArgs) elif tokens[0].lower() in tokenSet: class1 = int(tokens[1]) class2 = int(tokens[2]) key = (class1, class2) val= int(tokens[3]) if val != 0: pval = pairDict[key] if tokens[0].lower() == 'left x advance': if pval[0] and pval[0].xAdvance: logger.warning(( 'Vxxxx', (fws.lineNumber, tokens[0], tokens[1], tokens[2]), "line %d -- ignoring duplicate %s for class " "pair %s,%s")) else: if pval[0] is None: pval[0] = value.Value() pval[0].xAdvance=val elif tokens[0].lower() == 'right x advance': if pval[1] and pval[1].xAdvance: logger.warning(( 'Vxxxx', (fws.lineNumber, tokens[0], tokens[1], tokens[2]), "line %d -- ignoring duplicate %s for class " "pair %s,%s")) else: if pval[1] is None: pval[1] = value.Value() pval[1].xAdvance=val elif tokens[0].lower() == 'left x placement': if pval[0] and pval[0].xPlacement: logger.warning(( 'Vxxxx', (fws.lineNumber, tokens[0], tokens[1], tokens[2]), "line %d -- ignoring duplicate %s for class " "pair %s,%s")) else: if pval[0] is None: pval[0] = value.Value() pval[0].xPlacement = val elif tokens[0].lower() == 'right x placement': if pval[1] and pval[1].xPlacement: logger.warning(( 'Vxxxx', (fws.lineNumber, tokens[0], tokens[1], tokens[2]), "line %d -- ignoring duplicate %s for class " "pair %s,%s")) else: if pval[1] is None: pval[1] = value.Value() pval[1].xPlacement = val elif tokens[0].lower() == 'left y advance': if pval[0] and pval[0].yAdvance: logger.warning(( 'Vxxxx', (fws.lineNumber, tokens[0], tokens[1], tokens[2]), "line %d -- ignoring duplicate %s for class " "pair %s,%s")) else: if pval[0] is None: pval[0] = value.Value() pval[0].yAdvance=val elif tokens[0].lower() == 'right y advance': if pval[1] and pval[1].yAdvance: logger.warning(( 'Vxxxx', (fws.lineNumber, tokens[0], tokens[1], tokens[2]), "line %d -- ignoring duplicate %s for class " "pair %s,%s")) else: if pval[1] is None: pval[1] = value.Value() pval[1].yAdvance=val elif tokens[0].lower() == 'left y placement': if pval[0] and pval[0].yPlacement: logger.warning(( 'Vxxxx', (fws.lineNumber, tokens[0], tokens[1], tokens[2]), "line %d -- ignoring duplicate %s for class " "pair %s,%s")) else: if pval[0] is None: pval[0] = value.Value() pval[0].yPlacement=val elif tokens[0].lower() == 'right y placement': if pval[1] and pval[1].yPlacement: logger.warning(( 'Vxxxx', (fws.lineNumber, tokens[0], tokens[1], tokens[2]), "line %d -- ignoring duplicate %s for class " "pair %s,%s")) else: if pval[1] is None: pval[1] = value.Value() pval[1].yPlacement=val else: logger.error(( 'V0960', (fws.lineNumber, tokens[0]), 'line %d -- unexpected token: %s')) logger.error(( 'V0958', (startingLineNumber, "/".join(terminalStrings)), 'line %d -- did not find matching \'%s\'')) if pairDict and classDef1 and classDef2: r = cls(pairDict, classDef1=classDef1, classDef2=classDef2) else: logger.error(( 'Vxxxx', (), "Incomplete or invalid lookup data for pair classes.")) r = None return r
pass if __debug__: from fontio3.kerx import coverage _testingValues = (Format3(), Format3( { ClassPair([1, 1]): -25, ClassPair([1, 2]): -10, ClassPair([2, 1]): 15 }, leftClassDef=classdef.ClassDef({ 15: 1, 25: 1, 35: 2 }), rightClassDef=classdef.ClassDef({ 9: 1, 12: 1, 15: 1, 40: 2 }), coverage=coverage.Coverage())) def _test(): import doctest doctest.testmod()
def _reindexed(pairmap, cd1, cd2, **kwArgs): """ Return a 3-tuple consisting of a re-mapped pairmap, classDef1, and classDef2, removing all non-contiguous class entries and unused keys. """ logger = kwArgs['logger'] # first clean up cd1v = set(cd1.values()) cd2v = set(cd2.values()) newpairs_1 = {} for pairkey in pairmap: if pairkey[0] in cd1v and pairkey[1] in cd2v: newpairs_1[pairkey] = pairmap[pairkey] else: logger.warning(( 'Vxxxx', (pairkey,), "Removing class pair %s: first or second not listed in class defs")) k0 = [k[0] for k in newpairs_1] k1 = [k[1] for k in newpairs_1] cd1todelete = [k for k,v in cd1.items() if v not in k0] cd2todelete = [k for k,v in cd2.items() if v not in k1] for k in cd1todelete: logger.warning(( 'Vxxxx', (k, cd1[k]), "Removing glyph %d from firstclasses because its " "class (%d) is not used in any PairClasses key")) del(cd1[k]) for k in cd2todelete: logger.warning(( 'Vxxxx', (k, cd2[k]), "Removing glyph %d from secondclasses because its " "class (%d) is not used in any PairClasses key")) del(cd2[k]) # now reindex oldtonew1 = _getremapping(cd1) oldtonew2 = _getremapping(cd2) newpairs_2 = {} for pairkey in newpairs_1: newkey = (oldtonew1.get(pairkey[0]), oldtonew2.get(pairkey[1])) if newkey[0] and newkey[1]: vv = pairmap[pairkey] newpairs_2[newkey] = pairvalues.PairValues(vv[0], vv[1]) else: logger.warning(( 'Vxxxx', (pairkey,), "Removing class pair %s: first or second class empty or missing")) newcd1 = {g:oldtonew1[cl] for g,cl in cd1.items()} newcd2 = {g:oldtonew2[cl] for g,cl in cd2.items()} return newpairs_2, classdef.ClassDef(newcd1), classdef.ClassDef(newcd2)
def fromwalker(cls, w, **kwArgs): """ Creates and returns a Format2 object from the specified walker. The following keyword arguments are used: coverage A Coverage object (for either version 0 or version 1 'kern' tables). tupleIndex The tuple index (or 0) for a version 1 'kern' table, or None for a version 0 'kern' table. >>> obj = _testingValues[1] >>> obj == Format2.frombytes( ... obj.binaryString(), ... coverage = coverage_v1.Coverage()) True """ headerAdj = (6 if kwArgs['coverage'].format == 0 else 8) rowBytes = w.unpack("H") nCols = rowBytes // 2 leftOffset, rightOffset, arrayBase = w.unpack("3H") # Determine nRows, by finding the largest offset present in the left # class table. wLeft = w.subWalker(leftOffset - headerAdj) leftFirstGlyph, leftCount = wLeft.unpack("2H") vLeft = wLeft.group("H", leftCount) nRows = 1 + ((max(vLeft) - arrayBase) // rowBytes) wArray = w.subWalker(arrayBase - headerAdj) arrayData = wArray.group("h" * nCols, nRows) # Make the two ClassDef objects leftCD = classdef.ClassDef() for n in vLeft: cIndex = (n - arrayBase) // rowBytes if cIndex: leftCD[leftFirstGlyph] = cIndex leftFirstGlyph += 1 wRight = w.subWalker(rightOffset - headerAdj) d = classdef.ClassDef.fromwalker(wRight, forceFormat=1) rightCD = classdef.ClassDef((k, v // 2) for k, v in d.items() if v) # At this point, arrayData is a list of nRows rows, each containing # nCols kerning values. Since we know nRows and nCols explicitly, all # we need to do at this point is walk the data setting the dict's # values directly. for delKey in {'leftClassDef', 'rightClassDef'}: kwArgs.pop(delKey, None) r = cls({}, leftClassDef=leftCD, rightClassDef=rightCD, **utilities.filterKWArgs(cls, kwArgs)) CP = classpair.ClassPair for rowIndex, row in enumerate(arrayData): for colIndex, value in enumerate(row): if value: # most will be zero, remember r[CP([rowIndex, colIndex])] = value return r
def fromvalidatedwalker(cls, w, **kwArgs): """ Creates and returns a Format 2 object from the specified walker, doing source validation. >>> s = _testingValues[1].binaryString() >>> logger = utilities.makeDoctestLogger("format2_fvw") >>> fvb = Format2.fromvalidatedbytes >>> cv = _testingValues[1].coverage.__copy__() >>> obj = fvb(s, logger=logger, coverage=cv) format2_fvw.format2 - DEBUG - Walker has 140 remaining bytes. format2_fvw.format2.right classes.classDef - DEBUG - Walker has 68 remaining bytes. format2_fvw.format2.right classes.classDef - DEBUG - ClassDef is format 1. format2_fvw.format2.right classes.classDef - DEBUG - First is 9, and count is 32 format2_fvw.format2.right classes.classDef - DEBUG - Raw data are (2, 0, 0, 2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4) >>> obj == _testingValues[1] True >>> fvb(s[:3], logger=logger, coverage=cv) format2_fvw.format2 - DEBUG - Walker has 3 remaining bytes. format2_fvw.format2 - ERROR - Insufficient bytes. """ kwArgs.pop('fontGlyphCount', None) logger = kwArgs.pop('logger', logging.getLogger()) logger = logger.getChild("format2") logger.debug( ('V0001', (w.length(), ), "Walker has %d remaining bytes.")) headerAdj = (6 if kwArgs['coverage'].format == 0 else 8) if w.length() < 8: logger.error(('V0001', (), "Insufficient bytes.")) return None rowBytes, leftOffset, rightOffset, arrayBase = w.unpack("4H") if not rowBytes: logger.warning( ('V0605', (), "The rowBytes value is zero, so there is no table data.")) return cls({}, **utilities.filterKWArgs(cls, kwArgs)) nCols = rowBytes // 2 # Determine nRows, by finding the largest offset present in the left # class table. if leftOffset < headerAdj: logger.error(('V0607', (leftOffset, ), "The leftOffset value %d is too small.")) return None wLeft = w.subWalker(leftOffset - headerAdj) if wLeft.length() < 4: logger.error(('V0608', (), "The left-class table is missing or incomplete.")) return None leftFirstGlyph, leftCount = wLeft.unpack("2H") if wLeft.length() < 2 * leftCount: logger.error(('V0608', (), "The left-class table is missing or incomplete.")) return None vLeft = wLeft.group("H", leftCount) nRows = 1 + ((max(vLeft) - arrayBase) // rowBytes) if arrayBase < headerAdj: logger.error(('V0609', (arrayBase, ), "The arrayBase value %d is too small.")) return None wArray = w.subWalker(arrayBase - headerAdj) if wArray.length() < 2 * nCols * nRows: logger.error(('V0610', (), "The array is missing or incomplete.")) arrayData = wArray.group("h" * nCols, nRows) # Make the two ClassDef objects leftCD = classdef.ClassDef() if any(n < arrayBase for n in vLeft): logger.error( ('V0611', (), "At least one left class index is negative.")) return None for n in vLeft: cIndex, modCheck = divmod(n - arrayBase, rowBytes) if modCheck: logger.error( ('V0612', (), "At least one left offset is not an even multiple of " "the rowBytes past the array base.")) return None if cIndex: leftCD[leftFirstGlyph] = cIndex leftFirstGlyph += 1 if rightOffset < headerAdj: logger.error(('V0614', (rightOffset, ), "The rightOffset value %d is too small.")) return None wRight = w.subWalker(rightOffset - headerAdj) d = classdef.ClassDef.fromvalidatedwalker( wRight, logger=logger.getChild("right classes"), forceFormat=1) if d is None: return None rightCD = classdef.ClassDef() for k, v in d.items(): if v: cIndex, modCheck = divmod(v, 2) if modCheck: logger.error(( 'V0615', (), "At least one right offset is not an even multiple of " "the rowBytes past the array base.")) return None rightCD[k] = cIndex # At this point, arrayData is a list of nRows rows, each containing # nCols kerning values. Since we know nRows and nCols explicitly, all # we need to do at this point is walk the data setting the dict's # values directly. for delKey in {'leftClassDef', 'rightClassDef'}: kwArgs.pop(delKey, None) r = cls({}, leftClassDef=leftCD, rightClassDef=rightCD, **utilities.filterKWArgs(cls, kwArgs)) CP = classpair.ClassPair for rowIndex, row in enumerate(arrayData): for colIndex, value in enumerate(row): if value: # most will be zero, remember r[CP([rowIndex, colIndex])] = value return r
def fromformat0(cls, f0): """ Creates and returns a new Format2 object derived from the specified Format0 object. This method can take a while to execute, as it needs to go through and build up deductions about the list of class members for both the left-hand and right-hand sides. """ assert f0.format == 0 allLefts = set(t[0] for t in f0) allRights = set(t[1] for t in f0) dLefts = collections.defaultdict(set) dRights = collections.defaultdict(set) for lg in allLefts: v = [] for (lgTest, rg), value in f0.items(): if lgTest == lg: v.append((value, rg)) t = tuple(sorted(v)) dLefts[t].add(lg) for rg in allRights: v = [] for (lg, rgTest), value in f0.items(): if rgTest == rg: v.append((value, lg)) t = tuple(sorted(v)) dRights[t].add(rg) leftCD = classdef.ClassDef() rightCD = classdef.ClassDef() for i, v in enumerate(sorted(sorted(s) for s in dLefts.values()), 1): for glyph in v: leftCD[glyph] = i for i, v in enumerate(sorted(sorted(s) for s in dRights.values()), 1): for glyph in v: rightCD[glyph] = i invLeft = utilities.invertDictFull(leftCD) invRight = utilities.invertDictFull(rightCD) if not f0.coverage.format: newCov = coverage_v1.Coverage(vertical=f0.coverage.vertical, crossStream=f0.coverage.crossStream) else: newCov = f0.coverage r = cls({}, leftClassDef=leftCD, rightClassDef=rightCD, coverage=newCov, tupleIndex=f0.tupleIndex) CP = classpair.ClassPair for leftClass, vLeft in invLeft.items(): for rightClass, vRight in invRight.items(): key = (vLeft[0], vRight[0]) if key in f0: r[CP([leftClass, rightClass])] = f0[key] return r
def fromwalker(cls, w, **kwArgs): """ Creates and returns a new PSChainClass from the specified walker. There is one required keyword argument: fixupList A list, to which (lookupListIndex, fixupFunc) pairs will be appended. The actual lookup won't be set in the PSLookupRecord until this call is made, usually by the top-level GPOS construction logic. The fixup call takes one argument, the Lookup being set into it. >>> w = writer.LinkedWriter() >>> obj = _testingValues[1] >>> obj.buildBinary(w, forGPOS=False) >>> d = {obj[k][0].lookup.asImmutable(): 22 for k in obj} >>> w.addIndexMap("lookupList_GSUB", d) >>> s = w.binaryString() >>> FL = [] >>> obj2 = PSChainClass.frombytes(s, fixupList=FL) >>> d = {22: obj[k][0].lookup for k in obj} >>> for index, func in FL: ... func(d[index]) At this point we have the object; note that the reconciliation phase has removed some unneeded classes from the ClassDef (see the doctest output for the validated method to see more details on this). >>> obj2.pprint_changes(obj) Class definition table (input): Deleted records: 40: 3 41: 3 """ assert 'fixupList' in kwArgs format = w.unpack("H") assert format == 2 covTable = coverage.Coverage.fromwalker(w.subWalker(w.unpack("H"))) f = classdef.ClassDef.fromwalker backOffset = w.unpack("H") if backOffset: cdBack = f(w.subWalker(backOffset)) else: cdBack = classdef.ClassDef() cdIn = classdef.ClassDef.fromwalker(w.subWalker(w.unpack("H"))) lookOffset = w.unpack("H") if lookOffset: cdLook = f(w.subWalker(lookOffset)) else: cdLook = classdef.ClassDef() r = cls({}, classDefBacktrack=cdBack, classDefInput=cdIn, classDefLookahead=cdLook) setOffsets = w.group("H", w.unpack("H")) f = pslookupgroup.PSLookupGroup.fromwalker fixupList = kwArgs['fixupList'] ClassTuple = pschainclass_classtuple.ClassTuple Key = pschainclass_key.Key for firstClassIndex, setOffset in enumerate(setOffsets): if setOffset: wSet = w.subWalker(setOffset) it = enumerate(wSet.group("H", wSet.unpack("H"))) for ruleOrder, ruleOffset in it: wRule = wSet.subWalker(ruleOffset) tBack = ClassTuple( reversed(wRule.group("H", wRule.unpack("H")))) tIn = ClassTuple((firstClassIndex, ) + wRule.group("H", wRule.unpack("H") - 1)) tLook = ClassTuple(wRule.group("H", wRule.unpack("H"))) key = Key([tBack, tIn, tLook], ruleOrder=ruleOrder) r[key] = f(wRule, count=wRule.unpack("H"), fixupList=fixupList) # Now that we have the keys we can reconcile okToProceed, covSet = coverageutilities.reconcile( covTable, {k[1] for k in r}, [cdIn], **kwArgs) r.coverageExtras.update(covSet - set(cdIn)) if not okToProceed: r.clear() return r
def fromvalidatedwalker(cls, w, **kwArgs): """ Creates and returns a new PSChainClass object from the specified walker, doing source validation. >>> logger = utilities.makeDoctestLogger("pschainclass_test") >>> w = writer.LinkedWriter() >>> obj = _testingValues[1] >>> obj.buildBinary(w, forGPOS=False) >>> d = {obj[k][0].lookup.asImmutable(): 22 for k in obj} >>> w.addIndexMap("lookupList_GSUB", d) >>> s = w.binaryString() >>> FL = [] >>> fvb = PSChainClass.fromvalidatedbytes >>> obj2 = fvb(s, fixupList=FL, logger=logger) pschainclass_test.pschainclass - DEBUG - Walker has 90 remaining bytes. pschainclass_test.pschainclass - DEBUG - Format is 2 pschainclass_test.pschainclass.coverage - DEBUG - Walker has 70 remaining bytes. pschainclass_test.pschainclass.coverage - DEBUG - Format is 1, count is 2 pschainclass_test.pschainclass.coverage - DEBUG - Raw data are [20, 21] pschainclass_test.pschainclass - DEBUG - Backtrack offset is 28 pschainclass_test.pschainclass.classDef - DEBUG - Walker has 62 remaining bytes. pschainclass_test.pschainclass.classDef - DEBUG - ClassDef is format 1. pschainclass_test.pschainclass.classDef - DEBUG - First is 10, and count is 2 pschainclass_test.pschainclass.classDef - DEBUG - Raw data are (1, 1) pschainclass_test.pschainclass - DEBUG - Input offset is 38 pschainclass_test.pschainclass.classDef - DEBUG - Walker has 52 remaining bytes. pschainclass_test.pschainclass.classDef - DEBUG - ClassDef is format 2. pschainclass_test.pschainclass.classDef - DEBUG - Count is 3 pschainclass_test.pschainclass.classDef - DEBUG - Raw data are [(20, 21, 1), (22, 22, 2), (40, 41, 3)] pschainclass_test.pschainclass - DEBUG - Lookahead offset is 60 pschainclass_test.pschainclass.classDef - DEBUG - Walker has 30 remaining bytes. pschainclass_test.pschainclass.classDef - DEBUG - ClassDef is format 1. pschainclass_test.pschainclass.classDef - DEBUG - First is 30, and count is 1 pschainclass_test.pschainclass.classDef - DEBUG - Raw data are (1,) pschainclass_test.pschainclass - DEBUG - Set offsets are (0, 68, 0, 0) pschainclass_test.pschainclass.class index 0 - DEBUG - Set offset is zero pschainclass_test.pschainclass.class index 1 - DEBUG - Set offset is 68 pschainclass_test.pschainclass.class index 1 - DEBUG - Rule count is 1 pschainclass_test.pschainclass.class index 1 - DEBUG - Raw rule offsets are (4,) pschainclass_test.pschainclass.class index 1.rule order 0 - DEBUG - Backtrack count is 1 pschainclass_test.pschainclass.class index 1.rule order 0 - DEBUG - Backtrack classes (reversed) are (1,) pschainclass_test.pschainclass.class index 1.rule order 0 - DEBUG - Input count is 2 pschainclass_test.pschainclass.class index 1.rule order 0 - DEBUG - Input classes are (1, 2) pschainclass_test.pschainclass.class index 1.rule order 0 - DEBUG - Lookahead count is 1 pschainclass_test.pschainclass.class index 1.rule order 0 - DEBUG - Lookahead classes are (1,) pschainclass_test.pschainclass.class index 1.rule order 0 - DEBUG - Action count is 1 pschainclass_test.pschainclass.class index 1.rule order 0.pslookupgroup - DEBUG - Walker has 4 bytes remaining. pschainclass_test.pschainclass.class index 1.rule order 0.pslookupgroup.[0].pslookuprecord - DEBUG - Walker has 4 remaining bytes. pschainclass_test.pschainclass.class index 1.rule order 0.pslookupgroup.[0].pslookuprecord - DEBUG - Sequence index is 0 pschainclass_test.pschainclass.class index 1.rule order 0.pslookupgroup.[0].pslookuprecord - DEBUG - Lookup index is 22 pschainclass_test.pschainclass.class index 2 - DEBUG - Set offset is zero pschainclass_test.pschainclass.class index 3 - DEBUG - Set offset is zero pschainclass_test.pschainclass - WARNING - The classes [3] in the ClassDef are not used in any key, so the corresponding glyphs [40, 41] will be removed from it. pschainclass_test.pschainclass - INFO - The following glyphs appear only in the ClassDef and are not present in the Coverage: [22] pschainclass_test.pschainclass - INFO - The following glyphs appear in the Coverage and the ClassDef: [20, 21] >>> d = {22: obj[k][0].lookup for k in obj} >>> for index, func in FL: ... func(d[index]) >>> fvb(s[:33], logger=logger, fixupList=FL) pschainclass_test.pschainclass - DEBUG - Walker has 33 remaining bytes. pschainclass_test.pschainclass - DEBUG - Format is 2 pschainclass_test.pschainclass.coverage - DEBUG - Walker has 13 remaining bytes. pschainclass_test.pschainclass.coverage - DEBUG - Format is 1, count is 2 pschainclass_test.pschainclass.coverage - DEBUG - Raw data are [20, 21] pschainclass_test.pschainclass - DEBUG - Backtrack offset is 28 pschainclass_test.pschainclass.classDef - DEBUG - Walker has 5 remaining bytes. pschainclass_test.pschainclass.classDef - DEBUG - ClassDef is format 1. pschainclass_test.pschainclass.classDef - ERROR - Insufficient bytes for format 1 header. """ assert 'fixupList' in kwArgs fixupList = kwArgs.pop('fixupList') logger = kwArgs.pop('logger', logging.getLogger()) logger = logger.getChild("pschainclass") logger.debug( ('V0001', (w.length(), ), "Walker has %d remaining bytes.")) if w.length() < 12: logger.error(('V0004', (), "Insufficient bytes.")) return None format = w.unpack("H") if format != 2: logger.error( ('V0002', (format, ), "Expected format 2, but got format %d.")) return None else: logger.debug(('Vxxxx', (), "Format is 2")) covTable = coverage.Coverage.fromvalidatedwalker(w.subWalker( w.unpack("H")), logger=logger) if covTable is None: return None fvw = classdef.ClassDef.fromvalidatedwalker backOffset = w.unpack("H") logger.debug(('Vxxxx', (backOffset, ), "Backtrack offset is %d")) if backOffset: cdBack = fvw(w.subWalker(backOffset), logger=logger) if cdBack is None: return None else: cdBack = classdef.ClassDef() inOffset = w.unpack("H") logger.debug(('Vxxxx', (inOffset, ), "Input offset is %d")) cdIn = fvw(w.subWalker(inOffset), logger=logger) if cdIn is None: return None lookOffset = w.unpack("H") logger.debug(('Vxxxx', (lookOffset, ), "Lookahead offset is %d")) if lookOffset: cdLook = fvw(w.subWalker(lookOffset), logger=logger) if cdLook is None: return None else: cdLook = classdef.ClassDef() r = cls({}, classDefBacktrack=cdBack, classDefInput=cdIn, classDefLookahead=cdLook) setCount = w.unpack("H") if w.length() < 2 * setCount: logger.error( ('V0378', (), "The ChainClassSet offsets are missing or incomplete.")) return None setOffsets = w.group("H", setCount) logger.debug(('Vxxxx', (setOffsets, ), "Set offsets are %s")) ClassTuple = pschainclass_classtuple.ClassTuple Key = pschainclass_key.Key fvw = pslookupgroup.PSLookupGroup.fromvalidatedwalker for firstClassIndex, setOffset in enumerate(setOffsets): subLogger = logger.getChild("class index %d" % (firstClassIndex, )) if setOffset: subLogger.debug(('Vxxxx', (setOffset, ), "Set offset is %d")) wSet = w.subWalker(setOffset) if wSet.length() < 2: subLogger.error( ('V0379', (), "The ChainClassRuleCount is missing or incomplete.")) return None ruleCount = wSet.unpack("H") subLogger.debug(('Vxxxx', (ruleCount, ), "Rule count is %d")) if wSet.length() < 2 * ruleCount: subLogger.error(( 'V0380', (), "The ChainClassRule offsets are missing or incomplete." )) return None ruleOffsets = wSet.group("H", ruleCount) subLogger.debug( ('Vxxxx', (ruleOffsets, ), "Raw rule offsets are %s")) for ruleOrder, ruleOffset in enumerate(ruleOffsets): wRule = wSet.subWalker(ruleOffset) subLogger2 = subLogger.getChild("rule order %d" % (ruleOrder, )) if wRule.length() < 2: subLogger2.error(( 'V0381', (), "The BacktrackGlyphCount is missing or incomplete." )) return None backCount = wRule.unpack("H") subLogger2.debug( ('Vxxxx', (backCount, ), "Backtrack count is %d")) if wRule.length() < 2 * backCount: subLogger2.error(('V0382', ( ), "The Backtrack classes are missing or incomplete.")) return None tBack = ClassTuple(reversed(wRule.group("H", backCount))) subLogger2.debug(('Vxxxx', (tBack, ), "Backtrack classes (reversed) are %s")) if wRule.length() < 2: subLogger2.error( ('V0383', (), "The InputGlyphCount is missing or incomplete.")) return None inCount = wRule.unpack("H") - 1 subLogger2.debug( ('Vxxxx', (inCount + 1, ), "Input count is %d")) if wRule.length() < 2 * inCount: subLogger2.error( ('V0384', (), "The Input classes are missing or incomplete.")) return None tIn = ClassTuple((firstClassIndex, ) + wRule.group("H", inCount)) subLogger2.debug( ('Vxxxx', (tIn, ), "Input classes are %s")) if wRule.length() < 2: subLogger2.error(( 'V0385', (), "The LookaheadGlyphCount is missing or incomplete." )) return None lookCount = wRule.unpack("H") subLogger2.debug( ('Vxxxx', (lookCount, ), "Lookahead count is %d")) if wRule.length() < 2 * lookCount: subLogger2.error(('V0386', ( ), "The Lookahead classes are missing or incomplete.")) return None tLook = ClassTuple(wRule.group("H", lookCount)) subLogger2.debug( ('Vxxxx', (tLook, ), "Lookahead classes are %s")) if wRule.length() < 2: subLogger2.error( ('V0387', (), "The lookup count is missing or incomplete.")) return None posCount = wRule.unpack("H") subLogger2.debug( ('Vxxxx', (posCount, ), "Action count is %d")) key = Key([tBack, tIn, tLook], ruleOrder=ruleOrder) obj = fvw(wRule, count=posCount, fixupList=fixupList, logger=subLogger2, **kwArgs) if obj is None: return None r[key] = obj else: subLogger.debug(('Vxxxx', (), "Set offset is zero")) # Now that we have the keys we can reconcile okToProceed, covSet = coverageutilities.reconcile(covTable, {k[1] for k in r}, [cdIn], logger=logger, **kwArgs) r.coverageExtras.update(covSet - set(cdIn)) if not okToProceed: r.clear() return r
def fromValidatedFontWorkerSource(cls, fws, **kwArgs): """ Creates and returns a new PSChainClass from the specified FontWorkerSource. >>> logger = utilities.makeDoctestLogger("FW_test") >>> obj = PSChainClass.fromValidatedFontWorkerSource( ... _test_FW_fws2, ... namer = _test_FW_namer, ... forGPOS = True, ... lookupDict = _test_FW_lookupDict, ... logger = logger, ... editor = {}) FW_test.pschainclass - WARNING - line 20 -- unexpected token: foo FW_test.pschainclass - WARNING - line 22 -- invalid backtrack class: 4 FW_test.pschainclass - WARNING - line 22 -- invalid input class: 7 FW_test.pschainclass - WARNING - line 22 -- invalid lookahead class: 6 FW_test.pschainclass - WARNING - line 0 -- did not find matching 'subtable end/lookup end' >>> obj.pprint() Key((ClassTuple((2, 1, 0)), ClassTuple((0, 5)), ClassTuple((7, 8, 9, 0))), ruleOrder=0): Effect #1: Sequence index: 0 Lookup: 3: FUnit adjustment to horizontal advance: 678 Effect #2: Sequence index: 0 Lookup: 3: FUnit adjustment to horizontal advance: 901 Class definition table (backtrack): 1: 1 2: 2 3: 3 Class definition table (input): 1: 4 2: 5 3: 6 Class definition table (lookahead): 1: 7 2: 8 3: 9 """ logger = kwArgs.pop('logger', logging.getLogger()) logger = logger.getChild("pschainclass") terminalStrings = ('subtable end', 'lookup end') startingLineNumber = fws.lineNumber # place-holders classDefBacktrack = classdef.ClassDef() classDefInput = classdef.ClassDef() classDefLookahead = classdef.ClassDef() ruleOrders = {} lookupGroups = {} stringKeys = {} for line in fws: if line.lower() in terminalStrings: r = cls(lookupGroups, classDefBacktrack=classDefBacktrack, classDefInput=classDefInput, classDefLookahead=classDefLookahead) return r if len(line) > 0: tokens = [x.strip() for x in line.split('\t')] fVFWS = classdef.ClassDef.fromValidatedFontWorkerSource if tokens[0].lower() == 'backtrackclass definition begin': classDefBacktrack = fVFWS(fws, logger=logger, **kwArgs) cdBackSet = set(classDefBacktrack.values()) elif tokens[0].lower() == 'class definition begin': classDefInput = fVFWS(fws, logger=logger, **kwArgs) cdInputSet = set(classDefInput.values()) elif tokens[0].lower() == 'lookaheadclass definition begin': classDefLookahead = fVFWS(fws, logger=logger, dbg=True, **kwArgs) cdLookSet = set(classDefLookahead.values()) elif tokens[0].lower() == 'class-chain': CT = pschainclass_classtuple.ClassTuple classTuple1 = CT() classTuple2 = CT() classTuple3 = CT() classesOK = True if tokens[1] != '': try: classList1 = [int(x) for x in tokens[1].split(',')] classList1.reverse( ) # backtrack goes in reverse order except: logger.warning(( 'Vxxxx', (fws.lineNumber, tokens[1]), 'line %d -- invalid backtrack definition: %s')) classesOK = False for classNum in classList1: if classNum == 0: continue if not classNum in cdBackSet: logger.warning( ('V0962', (fws.lineNumber, classNum), 'line %d -- invalid backtrack class: %d')) classesOK = False classTuple1 = CT(classList1) if tokens[2] != '': try: classList2 = [int(x) for x in tokens[2].split(',')] except ValueError: logger.warning( ('Vxxxx', (fws.lineNumber, tokens[2]), 'line %d -- invalid input definition: %s')) classesOK = False for classNum in classList2: if classNum == 0: continue if not classNum in cdInputSet: logger.warning( ('V0962', (fws.lineNumber, classNum), 'line %d -- invalid input class: %d')) classesOK = False classTuple2 = CT(classList2) if tokens[3] != '': try: classList3 = [int(x) for x in tokens[3].split(',')] except ValueError: logger.warning(( 'Vxxxx', (fws.lineNumber, tokens[3]), 'line %d -- invalid lookahead definition: %s')) classesOK = False for classNum in classList3: if classNum == 0: continue if not classNum in cdLookSet: logger.warning( ('V0962', (fws.lineNumber, classNum), 'line %d -- invalid lookahead class: %d')) classesOK = False classTuple3 = CT(classList3) if not classesOK: continue lookupList = [] for effect in tokens[4:]: effectTokens = [x.strip() for x in effect.split(',')] sequenceIndex = int(effectTokens[0]) - 1 lookupName = effectTokens[1] lookupList.append( pslookuprecord.PSLookupRecord( sequenceIndex, lookup.Lookup.fromValidatedFontWorkerSource( fws, lookupName, logger=logger, **kwArgs))) stringKey = "(%s), (%s), (%s)" % (",".join([ str(ci) for ci in classTuple1[::-1] ]), ",".join([str(ci) for ci in classTuple2]), ",".join( [str(ci) for ci in classTuple3])) if stringKey in stringKeys: logger.warning(('Vxxxx', ( fws.lineNumber, stringKey, stringKeys[stringKey] ), "line %d -- context '%s' previously defined at line %d" )) else: stringKeys[stringKey] = fws.lineNumber key = pschainclass_key.Key( [classTuple1, classTuple2, classTuple3]) ruleOrder = ruleOrders.get(classTuple2[0], 0) key.ruleOrder = ruleOrder ruleOrders[classTuple2[0]] = ruleOrder + 1 lookupGroup = pslookupgroup.PSLookupGroup(lookupList) lookupGroups[key] = lookupGroup else: logger.warning(('V0960', (fws.lineNumber, tokens[0]), 'line %d -- unexpected token: %s')) logger.warning( ('V0958', (startingLineNumber, "/".join(terminalStrings)), "line %d -- did not find matching '%s'")) r = cls(lookupGroups, classDefBacktrack=classDefBacktrack, classDefInput=classDefInput, classDefLookahead=classDefLookahead) return r
# ----------------------------------------------------------------------------- # # Test code # if 0: def __________________(): pass if __debug__: from fontio3.utilities import namer from fontio3.opentype.fontworkersource import FontWorkerSource from io import StringIO c1 = classdef.ClassDef({5: 1, 6: 1, 7: 2, 15: 1}) c2 = classdef.ClassDef({20: 1, 21: 1, 22: 1}) pv = pairvalues._testingValues kv = pairclasses_key._testingValues _testingValues = ( PairClasses( {kv[0]: pv[0], kv[1]: pv[1], kv[2]: pv[2]}, classDef1 = c1, classDef2 = c2),) del c1, c2, pv _test_FW_namer = namer.Namer(None) _test_FW_namer._nameToGlyph = { 'A': 2,
def fromwalker(cls, w, **kwArgs): """ Creates and returns a Format2 object from the specified walker, which should start at the subtable header. The following keyword arguments are suggested (if they are not present, the default values for coverage and tupleIndex will be used, which won't usually be what's wanted): coverage A Coverage object. tupleIndex The variations tuple index. >>> obj = _testingValues[1] >>> obj == Format2.frombytes( ... obj.binaryString(), ... coverage = coverage.Coverage()) True """ headerAdj = 12 rowBytes = w.unpack("L") nCols = rowBytes // 2 leftOffset, rightOffset, arrayBase = w.unpack("3L") # Determine nRows, by finding the largest offset present in the left # class table. wLeft = w.subWalker(leftOffset - headerAdj) leftLookup = lookup.Lookup.fromwalker(wLeft) leftCD = classdef.ClassDef( {i: j // rowBytes for i, j in leftLookup.items()}) nRows = 1 + max(leftCD.values()) wRight = w.subWalker(rightOffset - headerAdj) rightLookup = lookup.Lookup.fromwalker(wRight) rightCD = classdef.ClassDef( {i: j // 2 for i, j in rightLookup.items()}) wArray = w.subWalker(arrayBase - headerAdj) arrayData = wArray.group("h", nCols * nRows) leftUniques = set(leftLookup.values()) rightUniques = set(rightLookup.values()) for delKey in {'leftClassDef', 'rightClassDef'}: kwArgs.pop(delKey, None) r = cls({}, leftClassDef=leftCD, rightClassDef=rightCD, **utilities.filterKWArgs(cls, kwArgs)) CP = classpair.ClassPair for uLeft in leftUniques: rowIndex = uLeft // rowBytes for uRight in rightUniques: dlt = arrayData[(uLeft + uRight) // 2] if dlt: r[CP([rowIndex, uRight // 2])] = dlt return r
def fromvalidatedwalker(cls, w, **kwArgs): """ Creates and returns a Format2 object from the specified walker, doing source validation. The walker should start at the subtable header. The following keyword arguments are suggested (if they are not present, the default values for coverage and tupleIndex will be used, which won't usually be what's wanted): coverage A Coverage object. logger A logger to which messages will be posted. tupleIndex The variations tuple index. >>> s = _testingValues[1].binaryString() >>> logger = utilities.makeDoctestLogger("fvw") >>> fvb = Format2.fromvalidatedbytes >>> obj = fvb(s, logger=logger) fvw.format2 - DEBUG - Walker has 94 remaining bytes. fvw.format2.left.lookup_aat - DEBUG - Walker has 78 remaining bytes. fvw.format2.left.lookup_aat.binsearch.binsrch header - DEBUG - Walker has 76 remaining bytes. fvw.format2.right.lookup_aat - DEBUG - Walker has 50 remaining bytes. fvw.format2.right.lookup_aat.binsearch.binsrch header - DEBUG - Walker has 48 remaining bytes. >>> fvb(s[:1], logger=logger) fvw.format2 - DEBUG - Walker has 1 remaining bytes. fvw.format2 - ERROR - Insufficient bytes. >>> fvb(s[:-1], logger=logger) fvw.format2 - DEBUG - Walker has 93 remaining bytes. fvw.format2.left.lookup_aat - DEBUG - Walker has 77 remaining bytes. fvw.format2.left.lookup_aat.binsearch.binsrch header - DEBUG - Walker has 75 remaining bytes. fvw.format2.right.lookup_aat - DEBUG - Walker has 49 remaining bytes. fvw.format2.right.lookup_aat.binsearch.binsrch header - DEBUG - Walker has 47 remaining bytes. fvw.format2 - ERROR - The array is missing or incomplete. """ if 'logger' in kwArgs: logger = kwArgs.pop('logger').getChild('format2') else: logger = utilities.makeDoctestLogger('format2') logger.debug( ('V0001', int(w.length()), "Walker has %d remaining bytes.")) kwArgs.pop('fontGlyphCount', None) if w.length() < 16: logger.error(('V0004', (), "Insufficient bytes.")) return None headerAdj = 12 rowBytes = w.unpack("L") if not rowBytes: logger.warning( ('V0605', (), "The rowBytes value is zero, so there is no table data.")) return cls({}, **utilities.filterKWArgs(cls, kwArgs)) nCols = rowBytes // 2 leftOffset, rightOffset, arrayBase = w.unpack("3L") # Determine nRows, by finding the largest offset present in the left # class table. if leftOffset < headerAdj: logger.error(('V0607', (leftOffset, ), "The leftOffset value %d is too small.")) return None wLeft = w.subWalker(leftOffset - headerAdj) leftLookup = lookup.Lookup.fromvalidatedwalker( wLeft, logger=logger.getChild("left"), **kwArgs) if leftLookup is None: return None # error will already have been logged bad = {i for i in leftLookup.values() if i % rowBytes} if bad: logger.error( ('V0612', (sorted(bad), ), "The following left-class values are not multiples of " "the rowBytes value: %s")) return None leftCD = classdef.ClassDef( {i: j // rowBytes for i, j in leftLookup.items()}) usedRows = set(leftCD.values()) nRows = 1 + max(usedRows) unusedRows = (set(range(nRows)) - usedRows) - {0} if unusedRows: logger.warning(('Vxxxx', (sorted(unusedRows), ), "The following rows are never referred to by any " "left-hand entry: %s")) if rightOffset < headerAdj: logger.error(('V0614', (rightOffset, ), "The rightOffset value %d is too small.")) return None wRight = w.subWalker(rightOffset - headerAdj) rightLookup = lookup.Lookup.fromvalidatedwalker( wRight, logger=logger.getChild("right"), **kwArgs) if rightLookup is None: return None # error will already have been logged bad = {i for i in rightLookup.values() if i % 2} if bad: logger.error(( 'V0615', (sorted(bad), ), "The following right-class values are not multiples of 2: %s")) return None rightCD = classdef.ClassDef( {i: j // 2 for i, j in rightLookup.items()}) usedCols = set(rightCD.values()) unusedCols = (set(range(nCols)) - usedCols) - {0} if unusedCols: logger.warning( ('Vxxxx', (sorted(unusedCols), ), "The following columns are never referred to by any " "right-hand entry: %s")) if arrayBase < headerAdj: logger.error(('V0609', (arrayBase, ), "The arrayBase value %d is too small.")) return None wArray = w.subWalker(arrayBase - headerAdj) if wArray.length() < 2 * nCols * nRows: logger.error(('V0610', (), "The array is missing or incomplete.")) return None arrayData = wArray.group("h", nCols * nRows) leftUniques = set(leftLookup.values()) rightUniques = set(rightLookup.values()) for delKey in {'leftClassDef', 'rightClassDef'}: kwArgs.pop(delKey, None) r = cls({}, leftClassDef=leftCD, rightClassDef=rightCD, **utilities.filterKWArgs(cls, kwArgs)) CP = classpair.ClassPair for uLeft in leftUniques: rowIndex = uLeft // rowBytes for uRight in rightUniques: dlt = arrayData[(uLeft + uRight) // 2] if dlt: r[CP([rowIndex, uRight // 2])] = dlt return r
def fromformat0(cls, f0): """ Creates and returns a new Format2 object derived from the specified Format0 object. This method can take a while to execute, as it needs to go through and build up deductions about the list of class members for both the left-hand and right-hand sides. >>> Format2.fromformat0(_f0tv()[1]).pprint() ClassPair((1, 1)): -25 ClassPair((1, 3)): -30 ClassPair((2, 2)): 12 Left-hand classes: 14: 1 18: 2 Right-hand classes: 23: 1 38: 2 96: 3 Header information: Horizontal With-stream No variation kerning Process forward """ assert f0.format == 0 allLefts = set(t[0] for t in f0) allRights = set(t[1] for t in f0) dLefts = collections.defaultdict(set) dRights = collections.defaultdict(set) for lg in allLefts: v = [] for (lgTest, rg), value in f0.items(): if lgTest == lg: v.append((value, rg)) t = tuple(sorted(v)) dLefts[t].add(lg) for rg in allRights: v = [] for (lg, rgTest), value in f0.items(): if rgTest == rg: v.append((value, lg)) t = tuple(sorted(v)) dRights[t].add(rg) leftCD = classdef.ClassDef() rightCD = classdef.ClassDef() for i, v in enumerate(sorted(sorted(s) for s in dLefts.values()), 1): for glyph in v: leftCD[glyph] = i for i, v in enumerate(sorted(sorted(s) for s in dRights.values()), 1): for glyph in v: rightCD[glyph] = i invLeft = utilities.invertDictFull(leftCD) invRight = utilities.invertDictFull(rightCD) r = cls({}, leftClassDef=leftCD, rightClassDef=rightCD, coverage=f0.coverage, tupleIndex=f0.tupleIndex) CP = classpair.ClassPair for leftClass, vLeft in invLeft.items(): for rightClass, vRight in invRight.items(): key = (vLeft[0], vRight[0]) if key in f0: r[CP([leftClass, rightClass])] = f0[key] return r
def fromvalidatedwalker(cls, w, **kwArgs): """ Creates and returns a Format3 object from the specified walker, doing source validation. >>> s = _testingValues[1].binaryString() >>> logger = utilities.makeDoctestLogger("format3_fvw") >>> fvb = Format3.fromvalidatedbytes >>> cv = _testingValues[1].coverage.__copy__() >>> d = {'logger': logger, 'coverage': cv, 'fontGlyphCount': 41} >>> obj = fvb(s, **d) format3_fvw.format3 - DEBUG - Walker has 106 remaining bytes. >>> obj == _testingValues[1] True >>> fvb(s[:3], **d) format3_fvw.format3 - DEBUG - Walker has 3 remaining bytes. format3_fvw.format3 - ERROR - Insufficient bytes. >>> d['fontGlyphCount'] = 100 >>> fvb(s, **d) format3_fvw.format3 - DEBUG - Walker has 106 remaining bytes. format3_fvw.format3 - ERROR - The font has 100 glyphs, but the glyphCount field is 41. """ fgc = kwArgs.pop('fontGlyphCount', 0x10000) logger = kwArgs.pop('logger', logging.getLogger()) logger = logger.getChild("format3") logger.debug( ('V0001', (w.length(), ), "Walker has %d remaining bytes.")) if w.length() < 6: logger.error(('V0004', (), "Insufficient bytes.")) return None glyphCount, kCount, lCount, rCount, flags = w.unpack("H4B") if glyphCount != fgc: logger.error( ('V0617', (fgc, glyphCount), "The font has %d glyphs, but the glyphCount field is %d.")) return None if not all([kCount, lCount, rCount]): logger.warning( ('V0618', (), "One or more of the kernValueCount, leftClassCount, and " "rightClassCount is zero. The table is thus empty.")) return cls({}, **utilities.filterKWArgs(cls, kwArgs)) if flags: logger.warning( ('V0619', (flags, ), "The flags should be zero, but are %d.")) if w.length() < (2 * (kCount + glyphCount)) + (lCount * rCount): logger.error( ('V0620', (), "The arrays are missing or incomplete.")) return None kValues = w.group("h", kCount) lClasses = w.group("B", glyphCount) rClasses = w.group("B", glyphCount) indices = w.group("B", lCount * rCount) # Note the assumption implicit in the following two lines of code: the # [0] element of the kernValue array has a numeric value of zero FUnits # (i.e. a non-kerning pair). This assumption is not documented in # Apple's 'kern' table documentation. leftCD = classdef.ClassDef((g, c) for g, c in enumerate(lClasses) if c) rightCD = classdef.ClassDef( (g, c) for g, c in enumerate(rClasses) if c) for delKey in {'leftClassDef', 'rightClassDef'}: kwArgs.pop(delKey, None) r = cls({}, leftClassDef=leftCD, rightClassDef=rightCD, **utilities.filterKWArgs(cls, kwArgs)) i = 0 CP = classpair.ClassPair for rowIndex in range(lCount): for colIndex in range(rCount): value = kValues[indices[i]] if value: r[CP([rowIndex, colIndex])] = value i += 1 return r