def buildBinary(self, w, **kwArgs): """ Adds the binary data for the SubtableGlyphCoverageSet object to the specified LinkedWriter. The keyword arguments are: stakeValue The stake value to be used at the start of this output. fontGlyphCount Number of glyphs in the font. >>> s = SubtableGlyphCoverageSet({1, 2, 3, 42}) >>> fontGlyphCount = 43 >>> w = LinkedWriter() >>> stakeValue = w.getNewStake() >>> kwArgs = {'fontGlyphCount': fontGlyphCount, 'stakeValue': stakeValue} >>> s.buildBinary(w, **kwArgs) >>> hexdump(w.binaryString()) 0 | 0E00 0000 0004 0000 |........ | """ if 'stakeValue' in kwArgs: stakeValue = kwArgs.pop('stakeValue') w.stakeCurrentWithValue(stakeValue) # else: # stakeValue = w.stakeCurrent() numGlyphs = getFontGlyphCount(**kwArgs) bytesPerSubtable = getBytesPerSubtableCoverageArray(**kwArgs) data = [0] * bytesPerSubtable for glyph_index in range(numGlyphs): if glyph_index in self: data[glyph_index // 8] |= (1 << (glyph_index & 0x07)) w.addGroup('B', data)
def _validate_sxHeight(value, **kwArgs): logger = kwArgs['logger'] editor = kwArgs['editor'] numGlyphs = utilities.getFontGlyphCount(**kwArgs) if (editor is None) or (not editor.reallyHas(b'cmap')): logger.error(('V0553', (), "Unable to validate sxHeight because the Editor and/or " "Cmap are missing or empty.")) return False if value < 0: logger.error(('Vxxxx', (value, ), "sxHeight (%d) is less than zero!")) return False umap = editor.cmap.getUnicodeMap() gidofx = umap.get(0x0078) if gidofx is not None: if not 0 <= gidofx < numGlyphs: logger.error( ('Vxxxx', (gidofx, ), "sxHeight value could not be validated because the glyph " "corresponding to 'x' (%d) is out of range for the font.")) return False if editor.reallyHas(b'glyf'): gObj = editor.glyf[gidofx] elif editor.reallyHas(b'CFF '): gObj = editor[b'CFF '][gidofx] else: logger.warning( ('Vxxxx', (value, ), "No 'glyf' or 'CFF ' table present; could not validate " "sxHeight value (%d)")) return True if gObj and gObj.bounds: actual = gObj.bounds.yMax else: actual = None if value != actual: logger.warning( ('Vxxxx', (value, actual), "sxHeight value (%d) does not match yMax of 'x' (%s)")) else: logger.warning( ('Vxxxx', (value, ), "sxHeight value (%d) could not be validated because no " "glyph for 'x' is present in the font.")) return True
def frompairclasses(cls, pcObj, **kwArgs): """ Given a PairClasses object, flatten it into a PairGlyphs object. The useSpecialClass0 keyword (default True) will take a class value of zero and map it to the special glyph code None, in order to keep the size of the resulting conversion reasonable. If the keyword is False then all the class 0 entries will be skipped. However, if you want a full expansion, including all the exceptions, then specify the fullExpansion=True keyword. Note that in this case you will also have to provide either an editor or a fontGlyphCount keyword argument, so that the class 0 expansions can be done. If both useSpecialClass0 and fullExpansion are True, useSpecialClass0 wins. """ r = cls() cd1 = utilities.invertDictFull(pcObj.classDef1, asSets=True) cd2 = utilities.invertDictFull(pcObj.classDef2, asSets=True) K = pairglyphs_key.Key useSpecial = kwArgs.get('useSpecialClass0', True) if kwArgs.get('fullExpansion', False): fullExpansion = True c1Set = pcObj.coverageExtras fgc = utilities.getFontGlyphCount(**kwArgs) c2Set = set(range(fgc)) excludes = {g for c, s in cd2.items() if c for g in s} c2Set -= excludes else: fullExpansion = False for cKey, cValue in pcObj.items(): if cKey[0]: it1 = cd1[cKey[0]] elif useSpecial: it1 = [None] elif fullExpansion: it1 = c1Set else: it1 = [] for g1 in it1: if cKey[1]: it2 = cd2[cKey[1]] elif useSpecial: it2 = [None] elif fullExpansion: it2 = c2Set else: it2 = [] for g2 in it2: r[K([g1, g2])] = cValue return r
def fromvalidatedwalker(cls, w, **kwArgs): """ Creates and returns a new GlyphRecord object from the specified walker, doing validation. >>> logger = utilities.makeDoctestLogger("strike") >>> fvb = Strike.fromvalidatedbytes >>> GR = glyphrecord.GlyphRecord >>> rec1 = GR(15, -35, 'tiff', b"Some data here") >>> rec2 = GR(0, 16, 'tiff', b"ABC") >>> obj = Strike({4: rec1, 5: rec2}) >>> bs = obj.binaryString(fontGlyphCount=10) >>> obj = fvb(bs, fontGlyphCount=10, logger=logger) strike.strike - DEBUG - Walker has 77 remaining bytes. strike.strike.glyph 4.glyphrecord - DEBUG - Walker has 22 remaining bytes. strike.strike.glyph 5.glyphrecord - DEBUG - Walker has 11 remaining bytes. """ logger = kwArgs.pop('logger', None) if logger is None: logger = logging.getLogger().getChild('strike') else: logger = logger.getChild('strike') logger.debug( ('V0001', (w.length(), ), "Walker has %d remaining bytes.")) baseOffset = w.getOffset() fgc = utilities.getFontGlyphCount(**kwArgs) if fgc is None: logger.error( ('V1017', (), "Unable to ascertain the font's glyph count.")) return None if w.length() < (4 * (fgc + 1)): logger.error(('V0004', (), "Insufficient bytes.")) return None offsets = w.group("L", fgc + 1) fw = glyphrecord.GlyphRecord.fromvalidatedwalker r = cls() for i in range(fgc): if offsets[i] == offsets[i + 1]: continue wSub = w.subWalker(offsets[i] - 4, newLimit=baseOffset + offsets[i + 1] - 4) subLogger = logger.getChild("glyph %d" % (i, )) r[i] = fw(wSub, logger=subLogger, **kwArgs) if r[i] is None: return None return r
def fromeditor(cls, e, **kwArgs): """ Synthesize a new GlyphClassTable from the information in the specified Editor. This includes the 'maxp', 'cmap', 'GSUB', 'GPOS', and 'glyf' (for TrueType fonts) tables. """ kwArgs['editor'] = e numGlyphs = utilities.getFontGlyphCount(**kwArgs) r = cls({n: 1 for n in range(numGlyphs)}) allOTGlyphs = set() if e.reallyHas(b'hmtx'): zeroAdvs = {i for i, obj in e.hmtx.items() if not obj.advance} else: zeroAdvs = set() if e.reallyHas(b'GPOS'): gposMarks = cls._fromeditor_marks_from_GPOS(e.GPOS) allOTGlyphs.update(e.GPOS.gatheredInputGlyphs()) allOTGlyphs.update(e.GPOS.gatheredOutputGlyphs()) else: gposMarks = set() if e.reallyHas(b'GSUB'): gsubLigs = cls._fromeditor_ligs_from_GSUB(e.GSUB) allOTGlyphs.update(e.GSUB.gatheredInputGlyphs()) allOTGlyphs.update(e.GSUB.gatheredOutputGlyphs()) else: gsubLigs = set() if e.reallyHas(b'glyf') and e.reallyHas(b'cmap'): compGlyphs = cls._fromeditor_comps_from_glyf(e.glyf, e.cmap) else: compGlyphs = set() # Now that the various data sources have been gathered, update the # new object. for i in zeroAdvs & gposMarks: r[i] = 3 # mark for i in gsubLigs: r[i] = 2 # ligature for i in compGlyphs - allOTGlyphs: r[i] = 4 # component only return r
def getBytesPerSubtableCoverageArray(**kwArgs): """ Returns the number of bytes for a subtable glyph coverage array based on the number of glyphs in the font, includes padding bytes if needed to ensure a four-byte boundary. >>> kwArgs = {'fontGlyphCount': 27} >>> getBytesPerSubtableCoverageArray(**kwArgs) 4 """ numGlyphs = getFontGlyphCount(**kwArgs) # Each coverage bitfield is (numGlyphs + CHAR_BIT - 1) / CHAR_BIT bytes # in size... bytesPerSubtable = (numGlyphs + 7) // 8 # ...padded to a four-byte boundary. bytesPerSubtable = ((bytesPerSubtable + 3) // 4) * 4 return bytesPerSubtable
def fromvalidatedwalker(cls, w, **kwArgs): """ Creates and returns a new SubtableGlyphCoverageSet object from the specified walker, doing source validation. >>> wb = StringWalkerBit(bytes.fromhex("0E 00 00 00 00 04 00 00")) >>> fontGlyphCount = 43 >>> logger = makeDoctestLogger("fvw") >>> kwArgs = {'fontGlyphCount': fontGlyphCount, 'logger': logger} >>> SubtableGlyphCoverageSet.fromvalidatedwalker(w=wb, **kwArgs) fvw - DEBUG - Walker has 8 remaining bytes. fvw - DEBUG - Walker has 0 remaining bytes. SubtableGlyphCoverageSet({1, 2, 3, 42}) >>> wb2 = StringWalkerBit(bytes.fromhex("0E 00 00 00 00")) >>> SubtableGlyphCoverageSet.fromvalidatedwalker(w=wb2, **kwArgs) fvw - DEBUG - Walker has 5 remaining bytes. fvw - ERROR - Insufficient bytes -- need 8, have 5. """ logger = kwArgs.pop('logger', logging.getLogger()) logger.debug(( 'V0001', (w.length(),), "Walker has %d remaining bytes.")) r = cls() numGlyphs = getFontGlyphCount(**kwArgs) bytesPerSubtable = getBytesPerSubtableCoverageArray(**kwArgs) bytesAvailable = w.length() bytesNeeded = bytesPerSubtable if bytesAvailable < bytesNeeded: logger.error(('V0004', (bytesNeeded, bytesAvailable), "Insufficient bytes -- need %d, have %d.")) return None data = w.group('B', bytesPerSubtable) for glyph_index in range(numGlyphs): # To determine if a particular glyph is covered by the subtable, # calculate coverageBitfield[glyph/CHAR_BIT] & (1 << (glyph & (CHAR_BIT-1))). # If this is non-zero, the glyph is covered. if data[glyph_index // 8] & (1 << (glyph_index & 0x07)) != 0: r.add(glyph_index) logger.debug(( 'V0001', (w.length(),), "Walker has %d remaining bytes.")) return r
def buildBinary(self, w, **kwArgs): """ Adds the binary content for the Strike to the specified writer. >>> GR = glyphrecord.GlyphRecord >>> rec1 = GR(15, -35, 'tiff', b"Some data here") >>> rec2 = GR(0, 16, 'tiff', b"ABC") >>> obj = Strike({4: rec1, 5: rec2}) >>> utilities.hexdump(obj.binaryString(fontGlyphCount=10)) 0 | 0000 0030 0000 0030 0000 0030 0000 0030 |...0...0...0...0| 10 | 0000 0030 0000 0046 0000 0051 0000 0051 |...0...F...Q...Q| 20 | 0000 0051 0000 0051 0000 0051 000F FFDD |...Q...Q...Q....| 30 | 7469 6666 536F 6D65 2064 6174 6120 6865 |tiffSome data he| 40 | 7265 0000 0010 7469 6666 4142 43 |re....tiffABC | """ if 'stakeValue' in kwArgs: stakeValue = kwArgs.pop('stakeValue') w.stakeCurrentWithValue(stakeValue) else: stakeValue = w.stakeCurrent() # The "+2" in the following accounts for the extra offset needed at # the end, to compute the size of the last entry. maxGlyph = utilities.getFontGlyphCount(**kwArgs) - 1 stakes = [w.getNewStake() for i in range(maxGlyph + 2)] for stake in stakes: # The offsetByteDelta in the following allows us to bypass the # ppem and resolution here, leaving them for the parent Sbix # object to handle. w.addUnresolvedOffset("L", stakeValue, stake, offsetByteDelta=4) for glyphIndex in range(maxGlyph + 1): stake = stakes[glyphIndex] if (glyphIndex not in self) or (self[glyphIndex] is None): w.stakeCurrentWithValue(stake) else: self[glyphIndex].buildBinary(w, stakeValue=stake) w.stakeCurrentWithValue(stakes[-1])
def fromwalker(cls, w, **kwArgs): """ Creates and returns a SubtableGlyphCoverageSet object from the specified walker. >>> wb = StringWalkerBit(bytes.fromhex("0E 00 00 00 00 04 00 00")) >>> fontGlyphCount = 43 >>> kwArgs = {'fontGlyphCount': fontGlyphCount} >>> SubtableGlyphCoverageSet.fromwalker(w=wb, **kwArgs) SubtableGlyphCoverageSet({1, 2, 3, 42}) """ r = cls() numGlyphs = getFontGlyphCount(**kwArgs) bytesPerSubtable = getBytesPerSubtableCoverageArray(**kwArgs) data = w.group('B', bytesPerSubtable) for glyph_index in range(numGlyphs): # To determine if a particular glyph is covered by the subtable, # calculate coverageBitfield[glyph/CHAR_BIT] & (1 << (glyph & (CHAR_BIT-1))). # If this is non-zero, the glyph is covered. if data[glyph_index // 8] & (1 << (glyph_index & 0x07)) != 0: r.add(glyph_index) return r
def fromwalker(cls, w, **kwArgs): """ Creates and returns a new GlyphRecord object from the specified walker. >>> GR = glyphrecord.GlyphRecord >>> rec1 = GR(15, -35, 'tiff', b"Some data here") >>> rec2 = GR(0, 16, 'tiff', b"ABC") >>> obj = Strike({4: rec1, 5: rec2}) >>> bs = obj.binaryString(fontGlyphCount=10) >>> Strike.frombytes(bs, fontGlyphCount=10).pprint() 4: originOffsetX: 15 originOffsetY: -35 graphicType: tiff data: b'Some data here' 5: originOffsetX: 0 originOffsetY: 16 graphicType: tiff data: b'ABC' """ baseOffset = w.getOffset() fgc = utilities.getFontGlyphCount(**kwArgs) offsets = w.group("L", fgc + 1) fw = glyphrecord.GlyphRecord.fromwalker r = cls() for i in range(fgc): if offsets[i] == offsets[i + 1]: continue wSub = w.subWalker(offsets[i] - 4, newLimit=baseOffset + offsets[i + 1] - 4) r[i] = fw(wSub, **kwArgs) return r
def effectExtrema(self, forHorizontal=True, **kwArgs): """ Returns a dict, indexed by glyph, of pairs of values. If forHorizontal is True, these values will be the yMaxDelta and yMinDelta; if False, they will be the xMaxDelta and xMinDelta. These values can then be used to test against the font's ascent/descent values in order to show VDMA-like output, or to be accumulated across all the features that are performed for a given script and lang/sys. Note that either or both of these values may be None; this can arise for cases like mark-to-mark, where potentially infinite stacking of marks can occur. The following keyword arguments are used: cache A dict mapping object IDs to result dicts. This is used during processing to speed up analysis of deeply nested subtables, so the effectExtrema() call need only be made once per subtable. editor The Editor object containing this subtable. >>> obj, ed = _makeTest() >>> d = obj.effectExtrema(forHorizontal=False, editor=ed) >>> for g in sorted(d): ... print(g, d[g]) 80 (0, -25) 81 (0, -29) 82 (0, -25) """ cache = kwArgs.pop('cache', {}) if id(self) in cache: return cache[id(self)] r = {} revMap = utilities.invertDictFull(self.classDefInput, asSets=True) c0Glyphs = None for key, lkGroup in self.items(): rCumul = {} for lkRec in lkGroup: cls = key[1][lkRec.sequenceIndex] lk = lkRec.lookup if cls: gSet = revMap[cls] else: if c0Glyphs is None: count = utilities.getFontGlyphCount(**kwArgs) c0Glyphs = set(range(count)) for remove in revMap.values(): c0Glyphs -= remove gSet = c0Glyphs for subtable in lk: rSub = subtable.effectExtrema(forHorizontal, cache=cache, **kwArgs) for glyph in gSet: if glyph in rSub: if glyph not in rCumul: rCumul[glyph] = rSub[glyph] else: if rCumul[glyph][0] is None or rSub[glyph][ 0] is None: new0 = None else: new0 = rCumul[glyph][0] + rSub[glyph][0] if rCumul[glyph][1] is None or rSub[glyph][ 1] is None: new1 = None else: new1 = rCumul[glyph][1] + rSub[glyph][1] rCumul[glyph] = (new0, new1) for glyph, (valHi, valLo) in rCumul.items(): if glyph not in r: r[glyph] = (valHi, valLo) else: oldHi, oldLo = r[glyph] if oldHi is None or valHi is None: newHi = None else: newHi = max(oldHi, valHi) if oldLo is None or valLo is None: newLo = None else: newLo = min(oldLo, valLo) r[glyph] = (newHi, newLo) cache[id(self)] = r return r
def fromvalidatedwalker(cls, w, **kwArgs): """ Like fromwalker, but with validation. The following keyword arguments are supported: fontGlyphCount The glyph count from the 'maxp' table. This is required. dataTable A TSIDat object (typically TSI1). This is optional, but if not included, only default glyphnames will be used, and any custom names defined in a Control Program will be ignored. logger A logger to which notices will be posted. This is optional; the default logger will be used if this is not provided. >>> fvb=TSI5.fromvalidatedbytes >>> l=utilities.makeDoctestLogger("test") >>> s = utilities.fromhex("00 01 00 02 00 08 00 09 00 FF") >>> obj= fvb(s, fontGlyphCount=5, logger=l, dataTable=_testingDatTbl) test.TSI5 - DEBUG - Walker has 10 remaining bytes. test.TSI5 - INFO - There are 5 entries. test.TSI5 - ERROR - Group index 255 for glyph 4 is beyond the range of defined glyph groups. >>> obj[2], obj[3] ('TestGroup1', 'TestGroup2') """ logger = kwArgs.pop('logger', None) if logger is None: logger = logging.getLogger().getChild('TSI5') else: logger = logger.getChild('TSI5') logger.debug( ('V0001', (w.length(), ), "Walker has %d remaining bytes.")) fontGlyphCount = utilities.getFontGlyphCount(**kwArgs) count = int(w.length() / 2) if count != fontGlyphCount: logger.warning( ('V0482', (), "Length of the table is not consistent with " "font's glyph count.")) else: logger.info(('V0483', (count, ), "There are %d entries.")) dataTable = kwArgs.pop('dataTable', None) groups = _getGroupNames(dataTable) d = {} if count: gc = w.group("H", count) for k, v in enumerate(gc): if v < len(groups): d[k] = groups[v] else: logger.error( ('Vxxxx', (v, k), "Group index %d for glyph %d is beyond the range " "of defined glyph groups.")) d[k] = groups[0] return cls(d, groupnames=groups)
def fromvalidatedwalker(cls, w, **kwArgs): """ Creates and returns a new Ligature object from the specified walker, doing source validation. The walker should be based at the state table start (i.e. at numClasses). >>> origObj = _makeObj().trimmedToValidEntries() >>> s = origObj.binaryString() >>> logger = utilities.makeDoctestLogger("ligature_fvw") >>> fvb = Ligature.fromvalidatedbytes >>> d = { ... 'coverage': origObj.coverage, ... 'maskValue': 0x00000080, ... 'logger': logger, ... 'fontGlyphCount': 0x1000} >>> obj = fvb(s, **d) ligature_fvw.ligature - DEBUG - Walker has 514 remaining bytes. ligature_fvw.ligature.lookup_aat - DEBUG - Walker has 32 remaining bytes. ligature_fvw.ligature - WARNING - The entry for state 2, class 6 does ligature substitution, but does not lead back to the ground state. This might be problematic. ligature_fvw.ligature - WARNING - The entry for state 5, class 6 does ligature substitution, but does not lead back to the ground state. This might be problematic. ligature_fvw.ligature - WARNING - The entry for state 6, class 7 does ligature substitution, but does not lead back to the ground state. This might be problematic. ligature_fvw.ligature - WARNING - The entry for state 2, class 6 does ligature substitution, but does not lead back to the ground state. This might be problematic. ligature_fvw.ligature - WARNING - The entry for state 2, class 6 does ligature substitution, but does not lead back to the ground state. This might be problematic. ligature_fvw.ligature - WARNING - The entry for state 2, class 6 does ligature substitution, but does not lead back to the ground state. This might be problematic. ligature_fvw.ligature - WARNING - The entry for state 2, class 6 does ligature substitution, but does not lead back to the ground state. This might be problematic. ligature_fvw.ligature - WARNING - The entry for state 2, class 6 does ligature substitution, but does not lead back to the ground state. This might be problematic. ligature_fvw.ligature.namestash - DEBUG - Walker has 486 remaining bytes. ligature_fvw.ligature.clstable - DEBUG - Walker has 32 remaining bytes. >>> obj == origObj True >>> fvb(s[:19], **d) ligature_fvw.ligature - DEBUG - Walker has 19 remaining bytes. ligature_fvw.ligature - ERROR - Insufficient bytes. >>> fvb(s[:39], **d) ligature_fvw.ligature - DEBUG - Walker has 39 remaining bytes. ligature_fvw.ligature - ERROR - One or more offsets to state table components are outside the bounds of the state table itself. """ logger = kwArgs.pop('logger', logging.getLogger()) logger = logger.getChild('ligature') logger.debug(( 'V0001', (w.length(),), "Walker has %d remaining bytes.")) analyzer = ligatureanalyzer.Analyzer(w.subWalker(0, relative=True)) analyzer.analyze(logger=logger, **kwArgs) if analyzer.analysis is None: return None # Note that some of the size and other validation was already done in # the analyze() call, and is not reduplicated here. numClasses, oCT, oSA, oET, oLA, oCP, oLG = w.unpack("7L") offsets = (oCT, oSA, oET, oLA, oCP, oLG) wCT, wSA, wET, wLA, wCP, wLG = stutils.offsetsToSubWalkers( w.subWalker(0), *offsets) numStates = analyzer.numStates nsObj = namestash.NameStash.readormake_validated( w, offsets, numStates, numClasses, logger = logger) if nsObj is None: return None stateNames = nsObj.allStateNames() classNames = nsObj.allClassNames() fgc = utilities.getFontGlyphCount(**kwArgs) classTable = classtable.ClassTable.fromvalidatedwalker( wCT, classNames = classNames, logger = logger, fontGlyphCount = fgc) if classTable is None: return None kwArgs.pop('classTable', None) r = cls( {}, classTable = classTable, **utilities.filterKWArgs(cls, kwArgs)) entryPool = {} # entryIndex -> Entry object for stateIndex, rawStateRow in enumerate(analyzer.stateArray): thisRow = staterow_ligature.StateRow() for classIndex, rawEntryIndex in enumerate(rawStateRow): if rawEntryIndex not in entryPool: t = analyzer.entryTable[rawEntryIndex] newStateIndex, flags, laIndex = t entryPool[rawEntryIndex] = entry_ligature.Entry( newState = stateNames[newStateIndex], push = bool(flags & 0x8000), noAdvance = bool(flags & 0x4000)) e = entryPool[rawEntryIndex] d = analyzer.finalDicts.get((stateIndex, classIndex), None) if d is not None: for inGlyphs, outGlyphs in d.items(): gti = glyphtuple.GlyphTupleInput(inGlyphs) if gti not in e.actions: gto = glyphtuple.GlyphTupleOutput(outGlyphs) e.actions[gti] = gto thisRow[classNames[classIndex]] = e r[stateNames[stateIndex]] = thisRow return r.trimmedToValidEntries()
def fromvalidatedwalker(cls, w, **kwArgs): """ Creates and returns a new Contextual object from the specified walker, doing source validation. >>> s = _testingValues[0].binaryString() >>> logger = utilities.makeDoctestLogger("contextual_fvw") >>> fvb = Contextual.fromvalidatedbytes >>> d = { ... 'fontGlyphCount': 1000, ... 'coverage': _testingValues[0].coverage, ... 'maskValue': 0x10000000, ... 'logger': logger} >>> obj = fvb(s, **d) contextual_fvw.contextual - DEBUG - Walker has 160 remaining bytes. contextual_fvw.contextual.classmap.lookup_aat - DEBUG - Walker has 30 remaining bytes. contextual_fvw.contextual.classmap.lookup_aat.binsearch.binsrch header - DEBUG - Walker has 28 remaining bytes. contextual_fvw.contextual.per-glyph 0.lookup_aat - DEBUG - Walker has 14 remaining bytes. contextual_fvw.contextual.namestash - DEBUG - Walker has 140 remaining bytes. contextual_fvw.contextual.clstable - DEBUG - Walker has 30 remaining bytes. contextual_fvw.contextual.clstable.binsearch.binsrch header - DEBUG - Walker has 28 remaining bytes. >>> obj == _testingValues[0] True >>> fvb(s[:5], **d) contextual_fvw.contextual - DEBUG - Walker has 5 remaining bytes. contextual_fvw.contextual - ERROR - Insufficient bytes. >>> fvb(s[:23], **d) contextual_fvw.contextual - DEBUG - Walker has 23 remaining bytes. contextual_fvw.contextual - ERROR - One or more offsets to state table components are outside the bounds of the state table itself. """ logger = kwArgs.pop('logger', logging.getLogger()) logger = logger.getChild("contextual") logger.debug( ('V0001', (w.length(), ), "Walker has %d remaining bytes.")) analyzer = contextanalyzer.Analyzer(w.subWalker(0, relative=True)) markAnalysis, currAnalysis = analyzer.analyze(logger=logger) if markAnalysis is None or currAnalysis is None: return None # Note that some of the size and other validation was already done in # the analyze() call, and is not reduplicated here. t = w.unpack("5L") numClasses, oCT, oSA, oET, oGT = t wCT, wSA, wET, wGT = stutils.offsetsToSubWalkers( w.subWalker(0), *t[1:]) numStates = analyzer.numStates nsObj = namestash.NameStash.readormake_validated(w, t[1:], numStates, numClasses, logger=logger) if nsObj is None: return None stateNames = nsObj.allStateNames() classNames = nsObj.allClassNames() fgc = utilities.getFontGlyphCount(**kwArgs) classTable = classtable.ClassTable.fromvalidatedwalker( wCT, classNames=classNames, logger=logger, fontGlyphCount=fgc) if classTable is None: return None r = cls({}, classTable=classTable, **utilities.filterKWArgs(cls, kwArgs)) entryTable = analyzer.entryTable Entry = entry_contextual.Entry GD = glyphdict.GlyphDict StateRow = staterow_contextual.StateRow for stateIndex, rawState in enumerate(analyzer.stateArray): newRow = staterow_contextual.StateRow() for classIndex, entryIndex in enumerate(rawState): if entryIndex >= len(entryTable): logger.error( ('V0724', (stateNames[stateIndex], classNames[classIndex]), "The state array cell for state '%s' and class '%s' " "specifies an entry index that is out of range.")) return None newStateIndex, flags = entryTable[entryIndex][0:2] newEntry = Entry( newState=stateNames[newStateIndex], mark=bool(flags & 0x8000), noAdvance=bool(flags & 0x4000), markSubstitutionDict=GD(markAnalysis.get(entryIndex, {})), currSubstitutionDict=GD(currAnalysis.get(entryIndex, {}))) newRow[classNames[classIndex]] = newEntry r[stateNames[stateIndex]] = newRow return r
def effectExtrema(self, forHorizontal=True, **kwArgs): """ Returns a dict, indexed by glyph, of pairs of values. If forHorizontal is True, these values will be the yMaxDelta and yMinDelta; if False, they will be the xMaxDelta and xMinDelta. These values can then be used to test against the font's ascent/descent values in order to show VDMA-like output, or to be accumulated across all the features that are performed for a given script and lang/sys. Note that either or both of these values may be None; this can arise for cases like mark-to-mark, where potentially infinite stacking of marks can occur. The following keyword arguments are used: cache A dict mapping object IDs to result dicts. This is used during processing to speed up analysis of deeply nested subtables, so the effectExtrema() call need only be made once per subtable. editor The Editor object containing this subtable. >>> K = pairclasses_key.Key >>> V = value.Value >>> PV = pairvalues.PairValues >>> obj = PairClasses({ ... K([1, 2]): PV(V(xAdvance=20), V(xPlacement=30)), ... K([2, 1]): PV(V(xAdvance=20), V(yPlacement=30)), ... K([2, 2]): PV(V(xAdvance=20), V(xAdvance=30))}, ... classDef1 = classdef.ClassDef({40: 1, 50: 2, 60: 1}), ... classDef2 = classdef.ClassDef({75: 1, 92: 2})) >>> d = obj.effectExtrema(forHorizontal=True) >>> for g in sorted(d): ... print(g, d[g]) 75 (30, 0) >>> d = obj.effectExtrema(forHorizontal=False) >>> for g in sorted(d): ... print(g, d[g]) 92 (30, 0) """ cache = kwArgs.get('cache', {}) if id(self) in cache: return cache[id(self)] r = {} fv = effect.Effect.fromvalue others1 = others2 = None # only fill these on-demand invMap1 = utilities.invertDictFull(self.classDef1, asSets=True) invMap2 = utilities.invertDictFull(self.classDef2, asSets=True) for (cls1, cls2), pvObj in self.items(): if pvObj.first is not None: p = fv(pvObj.first).toPair(forHorizontal) if any(p): if cls1: glyphSet = invMap1[cls1] else: if others1 is None: fgc = utilities.getFontGlyphCount(**kwArgs) others1 = set(range(fgc)) - set(self.classDef1) glyphSet = others1 for g in glyphSet: if g not in r: r[g] = p else: old = r[g] r[g] = tuple(( max(old[0], p[0]), min(old[1], p[1]))) if pvObj.second is not None: p = fv(pvObj.second).toPair(forHorizontal) if any(p): if cls2: glyphSet = invMap2[cls2] else: if others2 is None: fgc = utilities.getFontGlyphCount(**kwArgs) others2 = set(range(fgc)) - set(self.classDef2) glyphSet = others2 for g in glyphSet: if g not in r: r[g] = p else: old = r[g] r[g] = tuple(( max(old[0], p[0]), min(old[1], p[1]))) cache[id(self)] = r return r
def fromvalidatedwalker(cls, w, **kwArgs): """ """ if 'logger' in kwArgs: logger = kwArgs.pop('logger').getChild('gvar') else: logger = utilities.makeDoctestLogger('gvar') logger.debug( ('V0001', int(w.length()), "Walker has %d remaining bytes.")) e = kwArgs['editor'] if not e.reallyHas('fvar'): logger.error( ('V1037', (), "Font has a 'gvar' table but no 'fvar' table; this is " "not a valid state.")) return None fvarObj = e.fvar ao = fvarObj.axisOrder r = cls(axisOrder=ao) origTableSize = w.length() if origTableSize < 20: logger.error(('V0004', (), "Insufficient bytes.")) return None t = w.unpack("L2HL2HL") vers, axisCount, coordCount, coordOff, glyphCount, flags, dataOff = t logger.debug(('Vxxxx', (vers, ), "Version is 0x%08X")) logger.debug(('Vxxxx', (axisCount, ), "Axis count is %d")) logger.debug(('Vxxxx', (coordCount, ), "Global coord count is %d")) logger.debug(('Vxxxx', (coordOff, ), "Global coord offset is 0x%08X")) logger.debug(('Vxxxx', (glyphCount, ), "Glyph count is %d")) logger.debug(('Vxxxx', (flags, ), "Flags is 0x%04X")) logger.debug(('Vxxxx', (dataOff, ), "Data offset is 0x%08X")) if vers != r.VERSION: logger.error(('V1038', ( r.VERSION, vers, ), "Unknown 'gvar' version; was expecting 0x%08X but " "got 0x%08X instead.")) return None if axisCount != len(ao): logger.error( ('V1039', (len(ao), axisCount), "Mismatch between the axis counts in 'fvar' (%d) and " "'gvar' (%d).")) return None if glyphCount != utilities.getFontGlyphCount(**kwArgs): logger.error( ('V1040', (), "Mismatch between the 'maxp' and 'gvar' glyph counts.")) return None if flags not in {0, 1}: logger.warning( ('V1041', (flags, ), "The 'gvar' flags value is 0x%04X; this includes bits " "that are not defined. These will be ignored.")) # Check the offsets for reasonableness offsetSize = (4 if (flags & 1) else 2) if coordCount: globStop = coordOff + (coordCount * len(ao) * 2) lowBounds = 20 + (glyphCount + 1) * offsetSize if (coordOff < lowBounds) or (globStop > origTableSize): logger.error(('V1042', ( ), "The 'gvar' global coordinates do not fit within the bounds " "of the entire 'gvar' table.")) return None if (dataOff < 20) or (dataOff >= origTableSize): logger.error( ('V1043', (), "The 'gvar' variations data starting offset is outside " "the bounds of the entire 'gvar' table")) return None # Read the global coordinates (if any) globalCoords = [] if coordCount: wSub = w.subWalker(coordOff) fvw = axial_coordinates.AxialCoordinates.fromvalidatedwalker globalCoords = [None] * coordCount for i in range(coordCount): subLogger = logger.getChild("globalCoord %d" % (i, )) obj = fvw(wSub, axisOrder=ao, logger=subLogger) if obj is None: return None globalCoords[i] = obj # Get the offsets to the actual glyph variation data if w.length() < offsetSize * (glyphCount + 1): logger.error(('V1043', (), "The 'gvar' glyph variation array extends past the " "table boundaries.")) return None offsets = w.group("HL"[flags & 1], glyphCount + 1) if offsetSize == 2: offsets = [2 * n for n in offsets] if list(offsets) != sorted(offsets): logger.error( ('V1044', (), "The per-glyph offsets are not monotonically increasing.")) return None for glyphIndex, (off1, off2) in enumerate(utilities.pairwise(offsets)): w.align(2) if off1 == off2: continue # For now I'm assuming the presence of a 'glyf' table... glyphObj = e.glyf[glyphIndex] if glyphObj.isComposite: pointCount = len(glyphObj.components) else: pointCount = glyphObj.pointCount() wSub = w.subWalker(off1 + dataOff) subLogger = logger.getChild("glyph %d" % (glyphIndex, )) wasOK = r._fillGlyph_validated(wSub, glyphIndex, globalCoords, pointCount, logger=subLogger, **kwArgs) if not wasOK: return None return r
def buildBinary(self, w, **kwArgs): """ Add the binary data for the Gvar object to the specified writer. Apart from the usual keyword argument ('stakeValue'), this method also supports the 'sharedPointsAllowed' keyword argument (default True). """ if 'stakeValue' in kwArgs: stakeValue = kwArgs.pop('stakeValue') w.stakeCurrentWithValue(stakeValue) else: stakeValue = w.stakeCurrent() sharingEnabled = kwArgs.pop('sharedPointsAllowed', True) e = kwArgs['editor'] w.add("L", self.VERSION) w.add("H", len(self.axisOrder)) dGlobalCoords = self._findGlobalCoords(**kwArgs) w.add("H", len(dGlobalCoords)) if dGlobalCoords: globalCoordsStake = w.getNewStake() w.addUnresolvedOffset("L", stakeValue, globalCoordsStake) else: w.add("L", 0) w.add("H", e.maxp.numGlyphs) w.add("H", 1) # hard-code for 32-bit offsets, for now dataStake = w.getNewStake() w.addUnresolvedOffset("L", stakeValue, dataStake) numGlyphs = utilities.getFontGlyphCount(**kwArgs) glyphStakes = [w.getNewStake() for i in range(numGlyphs + 1)] for stake in glyphStakes: w.addUnresolvedOffset("L", dataStake, stake) # Now emit the global coordinates: if dGlobalCoords: w.stakeCurrentWithValue(globalCoordsStake) it = sorted(iter(dGlobalCoords.items()), key=operator.itemgetter(1)) for coord, globalIndex in it: coord.buildBinary(w, axisOrder=self.axisOrder, **kwArgs) # Now emit the actual variation data w.stakeCurrentWithValue(dataStake) gd = self.glyphData glyfTable = e.glyf hmtxTable = e.hmtx for glyphIndex in range(numGlyphs): w.alignToByteMultiple(2) w.stakeCurrentWithValue(glyphStakes[glyphIndex]) if glyphIndex not in gd: continue glyphObj = glyfTable[glyphIndex] # We only skip this glyph if it has neither outline nor advance. # Zero-advance accents can still vary, as can the width of a space # glyph, so both criteria must be met to be skipped. if not (glyphObj or hmtxTable[glyphIndex].advance): continue if glyphObj.isComposite: pointCount = len(glyphObj.components) + 4 else: pointCount = glyphObj.pointCount(editor=e) + 4 pd = gd[glyphIndex] if sharingEnabled: commonPoints = pd.findCommonPoints() else: commonPoints = None invDict = pd.makeInvertDict() tupleCount = len(invDict) if commonPoints is not None: tupleCount |= 0x8000 w.add("H", tupleCount) sortedCoords = sorted(invDict) tupleDataStake = w.getNewStake() w.addUnresolvedOffset("H", glyphStakes[glyphIndex], tupleDataStake) tupleSizeStakes = [] privatePoints = [] # bools for coord in sortedCoords: dPointToDelta = invDict[coord] tupleSizeStakes.append(w.addDeferredValue("H")) mask = 0 if coord not in dGlobalCoords: mask |= 0x8000 else: mask = dGlobalCoords[coord] if any(obj.effectiveDomain for obj in dPointToDelta.values()): mask |= 0x4000 if commonPoints is not None and frozenset( dPointToDelta) == commonPoints: #if commonPoints is not None and set(dPointToDelta) in commonPoints: privatePoints.append(False) else: mask |= 0x2000 privatePoints.append(True) w.add("H", mask) if mask & 0x8000: coord.buildBinary(w) if mask & 0x4000: # we assume there's one unique domain; check this? domPts = [ p for p, delta in dPointToDelta.items() if delta.effectiveDomain ] dom = dPointToDelta[domPts[0]] dom.effectiveDomain.edge1.buildBinary(w) dom.effectiveDomain.edge2.buildBinary(w) # The array header is now done, so put out the shared points (if # any), and then stake the start of the actual tuple data. w.stakeCurrentWithValue(tupleDataStake) if commonPoints is not None: packedCommons = packed_points.PackedPoints(commonPoints) packedCommons.buildBinary(w, pointCount=pointCount - 4) # Now that the header is done, emit the actual points/deltas for t in zip(sortedCoords, tupleSizeStakes, privatePoints): coord, stake, isPrivate = t wSub = writer.LinkedWriter() dPointToDelta = invDict[coord] sortedPoints = sorted(dPointToDelta) if isPrivate: packed_points.PackedPoints(dPointToDelta).buildBinary( wSub, pointCount=pointCount - 4) # Emit x-deltas, in order, to wSub vx = [dPointToDelta[i].x for i in sortedPoints] self._emitDeltaBand(wSub, vx) # Emit y-deltas, in order, to wSub vy = [dPointToDelta[i].y for i in sortedPoints] self._emitDeltaBand(wSub, vy) bs = wSub.binaryString() w.setDeferredValue(stake, "H", len(bs)) w.addString(bs) w.alignToByteMultiple(2) w.stakeCurrentWithValue(glyphStakes[-1])