def _getUSpan(key, onlyPrintables=False): """ Given a bit number as the key, returns a Span with the Unicodes in the full version of that code page. If onlyPrintable is True, the Unicodes with property 'Cc' (i.e. control characters) will be filtered out of the result. >>> _printUSpan(_getUSpan(0)) # Latin 1 U+0000 through U+007F U+00A0 through U+00FF U+0152 through U+0153 U+0160 through U+0161 U+0178 U+017D through U+017E U+0192 U+02C6 U+02DC U+2013 through U+2014 U+2018 through U+201A U+201C through U+201E U+2020 through U+2022 U+2026 U+2030 U+2039 through U+203A U+20AC U+2122 >>> _printUSpan(_getUSpan(0, True)) # Latin 1, printables only U+0020 through U+007E U+00A0 through U+00FF U+0152 through U+0153 U+0160 through U+0161 U+0178 U+017D through U+017E U+0192 U+02C6 U+02DC U+2013 through U+2014 U+2018 through U+201A U+201C through U+201E U+2020 through U+2022 U+2026 U+2030 U+2039 through U+203A U+20AC U+2122 """ enc, is16Bit, name = _bitData[key] r = span2.Span(ord(c) for c in str(_b8BitSmall, enc, errors='ignore')) r.update({ord(c) for c in str(_b16BitBig, enc, errors='ignore')}) if onlyPrintables: uc = unicodedata.category f = (lambda c: uc(chr(c)) == 'Cc') r = span2.Span(itertools.filterfalse(f, r)) return r
def _subdivide(self, keepZeroes=False): """ Returns a list with (first, last) pairs of keys that are contiguous and whose associated glyphs are monotonic. >>> obj = Format4({50: 4, 51: 5, 52: 6, 53: 10, 54: 11, 60: 12}) >>> obj._subdivide() [(50, 52), (53, 54), (60, 60)] """ r = [] mgg = utilities.monotonicGroupsGenerator for uFirst, uLast in span2.Span(self).ranges(): gVec = [self[u] for u in range(uFirst, uLast + 1)] pieceIndex = 0 for piece in mgg(gVec, allowZeroes=keepZeroes): pieceLen = piece[1] - piece[0] r.append( (uFirst + pieceIndex, uFirst + pieceIndex + pieceLen - 1)) pieceIndex += pieceLen return r
def _validate(obj, **kwArgs): logger = kwArgs.pop('logger') r = True # gather together all glyphs that refer to a given entry, so we only need # to put one message out for the whole lot, instead of per-glyph messages. idToImmut = {} immutToObjSpan = {} for glyphIndex, csmEntry in obj.items(): entryID = id(csmEntry) if entryID not in idToImmut: idToImmut[entryID] = csmEntry.asImmutable() immut = idToImmut[entryID] if immut not in immutToObjSpan: immutToObjSpan[immut] = (csmEntry, span2.Span()) immutToObjSpan[immut][1].add(glyphIndex) for csmEntry, s in immutToObjSpan.values(): subLogger = logger.getChild("glyphs %s" % (s, )) r = csmEntry.isValid(logger=subLogger, **kwArgs) and r return r
def buildBinary(self, w, **kwArgs): """ Adds the binary content for ``self`` to the specified writer. :param w: A :py:class:`~fontio3.utilities.writer.LinkedWriter` :param kwArgs: Optional keyword arguments (there are none here) :return: None :raises ValueError: if one or more values cannot fit into four bytes >>> utilities.hexdump(_testingValues[0].binaryString()) 0 | 000C 0000 0000 0010 0000 0000 0000 0000 |................| >>> utilities.hexdump(_testingValues[1].binaryString()) 0 | 000C 0000 0000 001C 0000 0000 0000 0001 |................| 10 | 0000 3A00 0000 3A00 0000 0019 |..:...:..... | >>> utilities.hexdump(_testingValues[2].binaryString()) 0 | 000C 0000 0000 0028 0000 0000 0000 0002 |.......(........| 10 | 0000 3A00 0000 3A01 0000 0019 0000 3A02 |..:...:.......:.| 20 | 0000 3A02 0000 001D |..:..... | >>> utilities.hexdump(_testingValues[3].binaryString()) 0 | 000C 0000 0000 0034 0000 0000 0000 0003 |.......4........| 10 | 0000 3A00 0000 3A01 0000 0019 0000 3A02 |..:...:.......:.| 20 | 0000 3A02 0000 001D 000A 2318 000A 2318 |..:.......#...#.| 30 | 0000 001B |.... | """ self._preBuildValidate() startLength = w.byteLength w.add("HH", 12, 0) # format and pad lengthStake = w.addDeferredValue("L") w.add("L", (self.language or 0)) groups = [] if self: mgg = utilities.monotonicGroupsGenerator sp = span2.Span(self) for g in sp.ranges(): if g[1] < 0x7FFFFFFE: vC = [x for x in range(g[0], g[1] + 1)] else: vC = list(range(g[0], g[1] + 1)) charIndex = 0 itSub = mgg(self[c] for c in vC) for glyphStart, glyphStopPlusOne, ignore in itSub: thisLen = glyphStopPlusOne - glyphStart groups.append((vC[charIndex], vC[charIndex + thisLen - 1], glyphStart)) charIndex += thisLen w.add("L", len(groups)) w.addGroup("3L", groups) w.setDeferredValue(lengthStake, "L", w.byteLength - startLength)
def __str__(self): """ Returns a string representation of the object. >>> print(_testingValues[1]) 26-28, 258-259 """ return str(span2.Span(self))
def __str__(self): """ Returns a compact representation of the glyph indices covered by the Groups object. >>> g = Groups() >>> g.update(range(100, 2000)) >>> g.update(range(2200, 4000)) >>> print(g) 100-1999, 2200-3999 """ return str(span2.Span(self))
def buildBinary(self, w, **kwArgs): """ Adds the binary data for the Groups to the specified LinkedWriter. >>> utilities.hexdump(_testingValues[0].binaryString()) 0 | 0000 |.. | >>> utilities.hexdump(_testingValues[1].binaryString()) 0 | 0005 0002 0004 0000 000A 000B 0000 001E |................| 10 | 001F 0000 0021 0021 0000 0023 0027 0000 |.....!.!...#.'..| """ s = span2.Span(self) groups = list(s.ranges()) w.add("H", len(groups)) w.addGroup("2Hxx", groups)
def buildBinary(self, w, **kwArgs): """ Adds the binary content for ``self`` to the specified writer. :param w: A :py:class:`~fontio3.utilities.writer.LinkedWriter` :param kwArgs: Optional keyword arguments (see below) :return: None :raises ValueError: if one or more values cannot fit into two bytes The following ``kwArgs`` are supported: ``keepZeroGlyphUnicodes`` If False (the default) entries explicitly mapping to glyph 0 will be removed, since the defined ``'cmap'`` behavior is to map all unmapped character codes to glyph 0 anyway. However, sometimes the explicit zero should be kept; if you want this, set this flag to True. >>> utilities.hexdump(_testingValues[0].binaryString()) 0 | 0004 0018 0000 0002 0002 0000 0000 FFFF |................| 10 | 0000 FFFF 0001 0000 |........ | >>> utilities.hexdump(_testingValues[1].binaryString()) 0 | 0004 0020 0000 0004 0004 0001 0000 002A |... ...........*| 10 | FFFF 0000 002A FFFF 003A 0001 0000 0000 |.....*...:......| >>> utilities.hexdump(_testingValues[2].binaryString()) 0 | 0004 0020 0000 0004 0004 0001 0000 002B |... ...........+| 10 | FFFF 0000 002A FFFF 003A 0001 0000 0000 |.....*...:......| >>> utilities.hexdump(_testingValues[3].binaryString()) 0 | 0004 0028 0000 0006 0004 0001 0002 002B |...(...........+| 10 | 1234 FFFF 0000 002A 1234 FFFF 003A EDD1 |.4.....*.4...:..| 20 | 0001 0000 0000 0000 |........ | """ keepZeroes = kwArgs.pop('keepZeroGlyphUnicodes', False) self._preBuildValidate() bs = self._buildBinary_sub(list(span2.Span(self).ranges())) if bs is None: bs = self._buildBinary_sub(self._subdivide(keepZeroes=keepZeroes)) w.addString(bs)
def _validate(obj, **kwArgs): logger = kwArgs['logger'] if 'unicodeSpan' in kwArgs: uSpan = kwArgs['unicodeSpan'] else: e = kwArgs['editor'] if e is None or not e.reallyHas(b'cmap'): logger.error(( 'V0553', (), "Unable to validate CodePageRanges because the Editor and/or " "the Cmap are not available.")) return False uSpan = span2.Span(e.cmap.getUnicodeMap()) d = obj.__dict__ for key in set(_bitData): # symbol is validated separately printablesMissing = _getUSpan(key, True) - uSpan if printablesMissing: # there are missing printables from the range if d[obj.rangeIDToName[key]]: logger.warning(( 'W2101', (_bitData[key][0], ["U+%04X" % (n,) for n in sorted(printablesMissing)]), "Code page %s is missing printables %s")) elif not d[obj.rangeIDToName[key]]: # all printables are present, but code page is off logger.warning(( 'W2100', (_bitData[key][0],), "Code page %s has all printables, but is set to False")) return True
class Groups(set, metaclass=setmeta.FontDataMetaclass): """ Sets gathering glyph indices. >>> _testingValues[1].pprint() 2-4, 10-11, 30-31, 33, 35-39 """ # # Class definition variables # setSpec = dict(item_renumberdirect=True, set_pprintfunc=(lambda p, obj: p(str(span2.Span(obj))))) # # Methods # def __str__(self): """ Returns a compact representation of the glyph indices covered by the Groups object. >>> g = Groups() >>> g.update(range(100, 2000)) >>> g.update(range(2200, 4000)) >>> print(g) 100-1999, 2200-3999 """ return str(span2.Span(self)) def buildBinary(self, w, **kwArgs): """ Adds the binary data for the Groups to the specified LinkedWriter. >>> utilities.hexdump(_testingValues[0].binaryString()) 0 | 0000 |.. | >>> utilities.hexdump(_testingValues[1].binaryString()) 0 | 0005 0002 0004 0000 000A 000B 0000 001E |................| 10 | 001F 0000 0021 0021 0000 0023 0027 0000 |.....!.!...#.'..| """ s = span2.Span(self) groups = list(s.ranges()) w.add("H", len(groups)) w.addGroup("2Hxx", groups) @classmethod def from20(cls, g): """ Returns a Groups from a version 2.0 Groups. The keys of the 2.0 Groups are used, and nothing else. """ return cls(g) @classmethod def fromvalidatedwalker(cls, w, **kwArgs): """ Creates and returns a Group object from the specified walker, doing source validation. >>> s = _testingValues[1].binaryString() >>> logger = utilities.makeDoctestLogger("groups15_fvw") >>> fvb = Groups.fromvalidatedbytes >>> obj = fvb(s, logger=logger) groups15_fvw.groups15 - DEBUG - Walker has 32 remaining bytes. groups15_fvw.groups15 - INFO - The group index array is sorted by start glyph. >>> fvb(s[:1], logger=logger) groups15_fvw.groups15 - DEBUG - Walker has 1 remaining bytes. groups15_fvw.groups15 - ERROR - Insufficient bytes. >>> fvb(s[:-1], logger=logger) groups15_fvw.groups15 - DEBUG - Walker has 31 remaining bytes. groups15_fvw.groups15 - ERROR - The group index array is missing or incomplete. >>> obj = fvb(s[:23] + utilities.fromhex("27") + s[24:], logger=logger) groups15_fvw.groups15 - DEBUG - Walker has 32 remaining bytes. groups15_fvw.groups15 - INFO - The group index array is sorted by start glyph. groups15_fvw.groups15 - WARNING - Some of the group index records overlap (that is, some glyphs are members of more than one record). """ logger = kwArgs.pop('logger', logging.getLogger()) logger = logger.getChild("groups15") logger.debug( ('V0001', (w.length(), ), "Walker has %d remaining bytes.")) if w.length() < 2: logger.error(('V0001', (), "Insufficient bytes.")) return None count = w.unpack("H") if w.length() < 6 * count: logger.error(('V0571', (), "The group index array is missing or incomplete.")) return None rawList = w.group("2H2x", count) if list(rawList) != sorted(rawList): logger.info( ('V0572', (), "The group index array is not sorted by start glyph.")) else: logger.info(('V0573', (), "The group index array is sorted by start glyph.")) rawCount = sum(t[1] - t[0] + 1 for t in rawList) theSpan = span2.Span.fromranges(rawList) if len(theSpan) != rawCount: logger.warning(('V0574', ( ), "Some of the group index records overlap (that is, some glyphs " "are members of more than one record).")) return cls(theSpan) @classmethod def fromwalker(cls, w, **kwArgs): """ Creates and returns a Groups from the specified walker. >>> fb = Groups.frombytes >>> _testingValues[0] == fb(_testingValues[0].binaryString()) True >>> _testingValues[1] == fb(_testingValues[1].binaryString()) True """ count = w.unpack("H") rawList = w.group("2H2x", count) theSpan = span2.Span.fromranges(rawList) return cls(theSpan)
def findSizeSharers(self): """ Find potential re-use cases across PPEMs. This method returns a dict whose keys are (ppem, ppem+1) tuples and whose values are Spans with all the glyphs whose images are identical in those two ppems. This information is necessary, but not sufficient. The sharing can really only happen for monospace ranges (since the non-monospace ranges include advance in their metrics, which are not shared). So the dict returned by this method needs to be intersected (as it were) with the monospace glyph ranges, as it only applies there. """ # make the dict of image hashes dSizeGlyphToHash = {} for ppem, stk in self.items(): d = dSizeGlyphToHash.setdefault(ppem, {}) for glyphIndex, data in stk.items(): if data is not None and data.imageFormat == 5: d[glyphIndex] = hash(tuple(tuple(v) for v in data.image)) # walk the dict sortedSizes = sorted(dSizeGlyphToHash) dOutput = {} # (smaller, larger) -> set of glyphs for smaller, larger in zip(sortedSizes, sortedSizes[1:]): if ((smaller[0] != (larger[0] - 1)) or (smaller[1] != (larger[1] - 1)) or (smaller[2] != larger[2])): continue thisSet = (smaller, larger) d = dOutput.setdefault(thisSet, set()) dSmaller = dSizeGlyphToHash[smaller] dLarger = dSizeGlyphToHash[larger] commonKeys = set(dSmaller) & set(dLarger) for key in commonKeys: if dSmaller[key] == dLarger[key]: # I tested tens of thousands of glyphs at lots of PPEMs and # the hash test suffices; image comparison isn't needed. d.add(key) toDel = set() for key in dOutput: d = dOutput[key] if d: dOutput[key] = span2.Span(d) else: toDel.add(key) for key in toDel: del dOutput[key] return dOutput
def iterateSize(self, ppemKey, **kwArgs): """ Given a key (xPPEM, yPPEM, depth), returns a generator over tuples of the form (firstGlyph, lastGlyph, indexFormat, datOffset) for the entries matching that key. The tuples are yielded sorted in firstGlyph order. Note that there may be holes between firstGlyph and lastGlyph. The following keyword arguments are used: datOffset A list of length one, containing the offset to the next available dat offset. It's done this way so any changes to this value can be communicated back to the caller. datPool A dict mapping ppemKeys to dicts, which in turn map glyph indices to base offsets in the related data table ('bdat' or 'EBDT'). forApple If the forApple flag is True, the index formats will be limited to 2 and 3. If the flag is False (the default), index formats 2 through 5 will be used. Index format 1 is never used, as the size is too big. sizeSharers The output from a call to the Sbit object's findSizeSharers() method. If not specified, an empty dict will be used. """ forApple = kwArgs.get('forApple', False) sizeSharers = kwArgs.get('sizeSharers', {}) datPool = kwArgs.get('datPool', {}) datOffset = kwArgs.get('datOffset', [0]) r = [] for k, g in itertools.groupby(self[ppemKey], key=operator.itemgetter(2)): v = list(g) if k[0] is None: # it's a non-monospace group if len(v) == 1: # emit a single index3 group r.append((v[0][0], v[0][1], 3)) continue gapList = [] for thisEntry, nextEntry in zip(v, v[1:]): gapList.append(thisEntry[1] - thisEntry[0] + 1) gapList.append(nextEntry[0] - thisEntry[1] - 1) gapList.append(v[-1][1] - v[-1][0] + 1) currSize = ('3', 18 + 2 * gapList[0]) i = 3 while i <= len(gapList): vSub = gapList[:i] size_3 = 18 + 2 * sum(vSub) size_3s = sum(18 + 2 * n for n in vSub[0::2]) size_prior_plus_3 = currSize[1] + (18 + 2 * vSub[-1]) if not forApple: size_4 = 20 + 4 * sum(vSub[0::2]) best = min(size_3, size_4, size_3s, size_prior_plus_3) else: best = min(size_3, size_3s, size_prior_plus_3) if size_3 == best: currSize = ('3', size_3) i += 2 elif (not forApple) and (size_4 == best): currSize = ('4', size_4) i += 2 elif size_3s == best: currSize = ('3s', size_3s) i += 2 else: # If we get here, we yield the current cumulation as a # block and then start a new cumulation. i -= 1 # now indexes first of new gapList if currSize[0] == '3': r.append((v[0][0], v[(i // 2) - 1][1], 3)) elif currSize[0] == '4': # It never makes sense to split an index4 group, so # if the last group added was a 4, just meld them. if r and r[-1][-1] == 4: oldLast = r.pop() r.append((oldLast[0], v[(i // 2) - 1][1], 4)) else: r.append((v[0][0], v[(i // 2) - 1][1], 4)) else: assert currSize[0] == '3s' for t in v[0:(i // 2)]: r.append((t[0], t[1], 3)) gapList = gapList[i:] v = v[(i // 2):] currSize = ('3', 18 + 2 * gapList[0]) i = 3 if len(v) == 1: break # Handle the final block i -= 1 if currSize[0] == '3': r.append((v[0][0], v[(i // 2) - 1][1], 3)) elif currSize[0] == '4': if r and r[-1][-1] == 4: oldLast = r.pop() r.append((oldLast[0], v[(i // 2) - 1][1], 4)) else: r.append((v[0][0], v[(i // 2) - 1][1], 4)) else: assert currSize[0] == '3s' for t in v[0:(i // 2)]: r.append((t[0], t[1], 3)) else: # it's a monospace group nGroups = len(v) nGlyphs = sum(t[1] - t[0] + 1 for t in v) if forApple or (((14 * nGroups) - 16) < nGlyphs): for t in v: r.append((t[0], t[1], 2)) else: r.append((v[0][0], v[-1][1], 5)) sbitObj = self.sbitObj stk = sbitObj[ppemKey] dp = datPool.setdefault(ppemKey, {}) for t in r: # Before yielding the entries, we need to do some checks for size # limitations of the corresponding bitmaps (since we never want to # have to use index format 1). We'll subdivide ranges where needed. actuals = span2.Span(stk.actualIterator(*t[:2])) ppemSmallerKey = (ppemKey[0] - 1, ppemKey[1] - 1, ppemKey[2]) bSizes = {i: stk[i].binarySize() for i in actuals} if t[2] in {2, 5}: # Index formats 2 and 5 are used with monospace ranges, so we # need to check for sharing of data with the next smaller PPEM. assert len(set(iter(bSizes.values()))) == 1 imageSize = bSizes[next(iter(actuals))] # constant for mono ssKey = (ppemSmallerKey, ppemKey) if ssKey in sizeSharers: ssSmaller = sizeSharers[ssKey] dpSmaller = datPool[ppemSmallerKey] # has to be there # The following list contains tuples. For tuples of shared # images, there are 5 elements: # # True, # firstGlyph, # lastGlyph, # firstGlyph's datOffset # lastGlyph's datOffset # # For tuples of non-shared images, there are only 4 # elements per tuple: # # False, # firstGlyph, # lastGlyph, # firstGlyph's datOffset v = [] for glyph in sorted(actuals): if not v: if glyph in ssSmaller: v.append((True, glyph, glyph, dpSmaller[glyph], dpSmaller[glyph])) dp[glyph] = dpSmaller[glyph] else: v.append((False, glyph, glyph, datOffset[0])) dp[glyph] = datOffset[0] datOffset[0] += imageSize else: tLast = v[-1] if glyph in ssSmaller: if tLast[0]: expectNewOffset = tLast[-1] + imageSize if expectNewOffset == dpSmaller[glyph]: v[-1] = (True, tLast[1], glyph, tLast[3], expectNewOffset) else: v.append((True, glyph, glyph, dpSmaller[glyph], dpSmaller[glyph])) else: v.append( (True, glyph, glyph, dpSmaller[glyph], dpSmaller[glyph])) dp[glyph] = dpSmaller[glyph] else: if tLast[0]: v.append( (False, glyph, glyph, datOffset[0])) else: v[-1] = ((False, tLast[1], glyph, tLast[3])) dp[glyph] = datOffset[0] datOffset[0] += imageSize # Note that in the following, I'm not doing anything to # check to see if there was an untoward proliferation of # little blocks, in an alternating "sharing/not-sharing" # configuration. At some point, if that case arises often # enough, we can optimize for it. for tSub in v: yield (tSub[1], tSub[2], t[2], tSub[3]) else: yield ((t[0], t[1], t[2], datOffset[0])) for glyph in sorted(actuals): dp[glyph] = datOffset[0] datOffset[0] += imageSize elif t[2] in {3, 4}: # Index formats 3 and 4 deals with non-monospace glyphs with an # offset per glyph (including adjacent identical offsets for # empty glyphs). Here we check that the offsets from a relative # zero (at the start of the dat group for this index3 entry) # don't exceed 64K bytes by the end. The group will be # subdivided if that happens (without optimization). if sum(bSizes[i] for i in actuals) < 65536: yield ((t[0], t[1], t[2], datOffset[0])) for glyph in sorted(actuals): dp[glyph] = datOffset[0] datOffset[0] += bSizes[glyph] else: # we need to subdivide here v = [] cumulSize = 0 for glyph in sorted(actuals): thisSize = bSizes[glyph] if not v: v.append([glyph, glyph]) cumulSize = thisSize elif cumulSize + thisSize > 65535: v.append([glyph, glyph]) cumulSize = thisSize else: v[-1][1] = glyph cumulSize += thisSize dp[glyph] = datOffset[0] datOffset[0] += thisSize for firstGlyph, lastGlyph in v: yield ((firstGlyph, lastGlyph, t[2], dp[firstGlyph]))
def _recalc_all(obj, **kwArgs): """ Recalculates all the flag bits. Keyword arguments supported are: base1252 A Boolean which, if True, means the cpthreshold value will be interpreted only with respect to those parts of the code page's repertoire that do not overlap any part of the CP1252 code page. If False, then the comparison is on the whole set by itself. Default is True. Note that this flag has no effect for the CP1252 code page itself, which is always just tested against the cpthreshold value. cpthreshold An integral value indicating what portion of a particular code page's repertoire needs to be present in unicodeSpan for that code page to be flagged as present. The values are interpreted as follows (default is 95): 0 Any one of the code page's members must be present. 1-99 Interpreted as a percentage of the code page's repertoire that must be present. 100 100% of the code page's members must be present in unicodeSpan. unicodeSpan A Span object with the present Unicodes. If not specified, the Span will be constructed from the editor via the cmap.Cmap.getUnicodeMap() method. In this case, if either the Editor or the Cmap are unavailable, an exception (NoEditor or NoCmap) will be raised. """ if 'unicodeSpan' in kwArgs: uSpan = kwArgs['unicodeSpan'] else: e = kwArgs['editor'] if e is None: raise NoEditor() if not e.reallyHas(b'cmap'): raise NoCmap() uSpan = span2.Span(e.cmap.getUnicodeMap()) has30 = bool(e.cmap.findKeys(3, 0)) cpthreshold = kwArgs.get('cpthreshold', 95) base1252 = kwArgs.get('base1252', True) newObj = obj.__copy__() d = newObj.__dict__ for key in set(_bitData) | {31}: if key == 31: d['hasSymbol'] = has30 else: name = _bitData[key][2] uSpanEnc = _getUSpan(key, onlyPrintables=True) if base1252 and (key != 0): uSpanEnc -= _getUSpan(0, onlyPrintables=True) if uSpanEnc: diff = uSpanEnc - uSpan uCount = len(uSpanEnc) dCount = len(diff) if cpthreshold == 100: d[name] = not diff elif cpthreshold == 0: d[name] = dCount != uCount else: presentRatio = float(uCount - dCount) / float(uCount) d[name] = presentRatio >= (cpthreshold / 100.0) return (newObj != obj), newObj
class NoCenters(frozenset, metaclass=setmeta.FontDataMetaclass): """ Objects representing collections of glyph indices for those glyphs not to be auto-centered. >>> nc = NoCenters(set(range(300, 1200)) - set({450})) >>> print(nc) 300-449, 451-1199 >>> d = dict(zip(range(300, 350), range(7000, 7050))) >>> nc.glyphsRenumbered(d).pprint(label="Glyph spans") Glyph spans: 350-449, 451-1199, 7000-7049 >>> r1 = set(range(100, 199, 3)) >>> r2 = set(range(101, 200, 3)) >>> NoCenters(r1 | r2).pprint(label="Ranges", maxWidth=75) Ranges: 100-101, 103-104, 106-107, 109-110, 112-113, 115-116, 118-119, 121-122, 124-125, 127-128, 130-131, 133-134, 136-137, 139-140, 142-143, 145-146, 148-149, 151-152, 154-155, 157-158, 160-161, 163-164, 166-167, 169-170, 172-173, 175-176, 178-179, 181-182, 184-185, 187-188, 190-191, 193-194, 196-197 """ # # Class definition variables # setSpec = dict(item_renumberdirect=True, set_pprintfunc=(lambda p, i: p(str(span2.Span(i))))) # # Methods # def __str__(self): """ Returns a string representation of the object. >>> print(_testingValues[1]) 26-28, 258-259 """ return str(span2.Span(self)) def buildBinary(self, w, **kwArgs): """ Adds the binary data for the NoCenters object to the specified LinkedWriter. >>> utilities.hexdump(_testingValues[0].binaryString()) 0 | 0000 |.. | >>> utilities.hexdump(_testingValues[1].binaryString()) 0 | 0005 001A 001B 001C 0102 0103 |............ | """ w.add("H", len(self)) w.addGroup("H", sorted(self)) @classmethod def fromvalidatedwalker(cls, w, **kwArgs): """ Creates and returns a new NoCenter object from the specified walker, doing source validation. >>> s = _testingValues[1].binaryString() >>> logger = utilities.makeDoctestLogger("nocenters_test") >>> fvb = NoCenters.fromvalidatedbytes >>> print(fvb(s, logger=logger)) nocenters_test.nocenters - DEBUG - Walker has 12 remaining bytes. 26-28, 258-259 >>> fvb(s[:1], logger=logger) nocenters_test.nocenters - DEBUG - Walker has 1 remaining bytes. nocenters_test.nocenters - ERROR - Insufficient bytes. >>> fvb(s[:-1], logger=logger) nocenters_test.nocenters - DEBUG - Walker has 11 remaining bytes. nocenters_test.nocenters - ERROR - There were not enough NoCenter glyphs to match the specified count (5). >>> print(fvb(utilities.fromhex("00 00"), logger=logger), end='') nocenters_test.nocenters - DEBUG - Walker has 2 remaining bytes. nocenters_test.nocenters - INFO - The NoCenter count is zero. >>> fvb(s[:4] + s[6:] + s[4:6], logger=logger) nocenters_test.nocenters - DEBUG - Walker has 12 remaining bytes. nocenters_test.nocenters - ERROR - The NoCenter list is not sorted by glyph index. """ logger = kwArgs.pop('logger', logging.getLogger()) logger = logger.getChild("nocenters") logger.debug( ('V0001', (w.length(), ), "Walker has %d remaining bytes.")) if w.length() < 2: logger.error(('V0004', (), "Insufficient bytes.")) return None count = w.unpack("H") if not count: logger.info(('V0557', (), "The NoCenter count is zero.")) return cls() if w.length() < 2 * count: logger.error(('V0556', (count, ), "There were not enough NoCenter glyphs to match the " "specified count (%d).")) return None v = list(w.group("H", count)) if v != sorted(v): logger.error(('V0567', (), "The NoCenter list is not sorted by glyph index.")) return None return cls(v) @classmethod def fromwalker(cls, w, **kwArgs): """ Creates and returns a new NoCenter object from the specified walker. >>> fb = NoCenters.frombytes >>> _testingValues[0] == fb(_testingValues[0].binaryString()) True >>> _testingValues[1] == fb(_testingValues[1].binaryString()) True """ return cls(w.group("H", w.unpack("H")))
def buildBinary(self, w, **kwArgs): """ Adds the binary content for ``self`` to the specified writer. :param w: A :py:class:`~fontio3.utilities.writer.LinkedWriter` :param kwArgs: Optional keyword arguments (see below) :return: None :raises ValueError: if no eligible subtable format can accommodate the data present in ``self`` The following ``kwArgs`` are supported: ``stakeValue`` A stake that will be placed at the start of the binary data laid down by this method. One will be synthesized if not provided. See :py:class:`~fontio3.utilities.writer.LinkedWriter` for a description of stakes and how they're used. >>> utilities.hexdump(_testingValues[1].binaryString()) 0 | 000E 0000 0023 0000 0001 00FE 0100 0000 |.....#..........| 10 | 0000 0000 1500 0000 0200 614E 0017 0062 |..........aN...b| 20 | 2600 61 |&.a | >>> utilities.hexdump(_testingValues[2].binaryString()) 0 | 000E 0000 0041 0000 0002 00FE 0300 0000 |.....A..........| 10 | 2000 0000 000E 0100 0000 002C 0000 0038 | ..........,...8| 20 | 0000 0002 004E 0100 004E 0800 0000 0002 |.....N...N......| 30 | 0052 2500 022F 4300 0000 0001 0089 EE00 |.R%../C.........| 40 | 29 |) | """ if 'stakeValue' in kwArgs: stakeValue = kwArgs.pop('stakeValue') w.stakeCurrentWithValue(stakeValue) else: stakeValue = w.stakeCurrent() startLength = w.byteLength w.add("H", 14) # format lengthStake = w.addDeferredValue("L") w.add("L", len(self)) defStakes = {} nonDefStakes = {} for vs in sorted(self): w.add("T", vs) d = self[vs] if d.default: defStakes[vs] = w.getNewStake() w.addUnresolvedOffset("L", stakeValue, defStakes[vs]) else: w.add("L", 0) if len(d): nonDefStakes[vs] = w.getNewStake() w.addUnresolvedOffset("L", stakeValue, nonDefStakes[vs]) else: w.add("L", 0) for vs in sorted(self): d = self[vs] if vs in defStakes: w.stakeCurrentWithValue(defStakes[vs]) s = span2.Span(d.default) count = 0 countStake = w.addDeferredValue("L") for first, last in s.ranges(): spanCount = last - first + 1 while spanCount: pieceCount = min(256, spanCount) w.add("TB", first, pieceCount - 1) count += 1 first += pieceCount spanCount -= pieceCount w.setDeferredValue(countStake, "L", count) if vs in nonDefStakes: w.stakeCurrentWithValue(nonDefStakes[vs]) w.add("L", len(d)) w.addGroup("TH", sorted(d.items(), key=operator.itemgetter(0))) #w.alignToByteMultiple(2) w.setDeferredValue(lengthStake, "L", int(w.byteLength - startLength))
def _pprint(p, seq, **kwArgs): s = span2.Span(seq) p.simple(s, **kwArgs)
def buildBinary(self, w, **kwArgs): """ Adds the binary content for ``self`` to the specified writer. :param w: A :py:class:`~fontio3.utilities.writer.LinkedWriter` :param kwArgs: Optional keyword arguments (there are none here) :return: None :raises ValueError: if one or more values cannot fit into four bytes >>> pp.PP().sequence_grouped(_testingValues[0].binaryString()) [0]: 0 [1]: 8 [2-5]: 0 [6]: 32 [7]: 16 [8-8207]: 0 >>> pp.PP().sequence_grouped(_testingValues[1].binaryString()) [0]: 0 [1]: 8 [2-5]: 0 [6]: 32 [7]: 28 [8-11]: 0 [12]: 32 [13-8206]: 0 [8207]: 1 [8208]: 0 [8209]: 2 [8210-8212]: 0 [8213]: 2 [8214-8217]: 0 [8218]: 11 [8219]: 184 >>> pp.PP().sequence_grouped(_testingValues[2].binaryString()) [0]: 0 [1]: 8 [2-5]: 0 [6]: 32 [7]: 28 [8-11]: 0 [12]: 32 [13-8206]: 0 [8207]: 1 [8208]: 0 [8209]: 2 [8210-8212]: 0 [8213]: 2 [8214]: 0 [8215]: 1 [8216-8217]: 0 [8218]: 11 [8219]: 184 """ self._preBuildValidate() startLength = w.byteLength w.add("HH", 8, 0) # format and pad lengthStake = w.addDeferredValue("L") w.add("L", (self.language or 0)) highSet = set(c >> 16 for c in self) v = [0] * 65536 for i in highSet: v[i] = 1 w.addString(utilitiesbackend.utImplode(v)) countStake = w.addDeferredValue("L") count = 0 for spanStart, spanEnd in span2.Span(self).ranges(): if spanEnd < 0x7FFFFFFE: v = [self[i] for i in range(spanStart, spanEnd + 1)] else: v = [self[i] for i in range(spanStart, spanEnd + 1)] for gStart, gStop, ignore in utilities.monotonicGroupsGenerator(v): count += 1 thisLen = gStop - gStart w.add("3L", spanStart, spanStart + thisLen - 1, gStart) spanStart += thisLen w.setDeferredValue(countStake, "L", count) w.setDeferredValue(lengthStake, "L", w.byteLength - startLength)
def _validate(d, **kwArgs): editor = kwArgs['editor'] scaler = kwArgs.get('scaler', None) logger = kwArgs['logger'] logger = logger.getChild('vdmxrecord %d:%d' % (d.ratio.xRatio, d.ratio.yEndRatio)) if editor is None or (not editor.reallyHas(b'maxp')): logger.warning( ('W2503', (), "Unable to validate contents because there was no Editor " "or no 'maxp' table was available.")) return False try: r = kwArgs.get('recalculateditem', None) diff = r != d if r is None: diff, r = _recalc(d, **kwArgs) except: logger.error(('V0554', (), "An error occured in the scaler during device metrics " "calculation, preventing validation.")) return False if diff: fontGlyphCount = editor.maxp.numGlyphs for ppem, recalcrec in r.group.items(): currec = d.group[ppem] if currec.yMax > recalcrec.yMax or currec.yMin < recalcrec.yMin: # VDMX is "too big"...only a warning. This is just # "inefficient"; it won't cause clipping. logger.warning(('W2500', (ppem, currec.yMax, currec.yMin, recalcrec.yMax, recalcrec.yMin), "Record for %d ppem [%d,%d] is larger than " "the calculated value [%d,%d].")) if currec.yMax < recalcrec.yMax or currec.yMin > recalcrec.yMin: # VDMX is "too small"...this is an error, as it will cause # clipping under Windows. If we have a scaler, report on # individual glyphs. if scaler is not None: try: ratio = d.ratio if ratio.xRatio > 1 or ratio.yEndRatio > 1: xs = (ratio.xRatio * 1.0) / (ratio.yEndRatio * 1.0) else: xs = 1 mat = matrix.Matrix(([xs * ppem, 0.0, 0.0], [0.0, ppem * 1.0, 0.0], [0.0, 0.0, 1.0])) try: scaler.setTransform(mat) except: raise ScalerError() badYMax = span2.Span() badYMin = span2.Span() for g in range(fontGlyphCount): try: m = scaler.getOutlineMetrics(g) except: raise ScalerError() if oldRound(m.hi_y) > currec.yMax: badYMax.add(g) if oldRound(m.lo_y) < currec.yMin: badYMin.add(g) if len(badYMax) > 0: logger.error( ('V0775', (currec.yMax, ppem, str(badYMax)), "The following glyphs' actual yMax values " "exceed the stored value of %d at %d ppem: " "%s. Clipping may occur.")) if len(badYMin) > 0: logger.error( ('V0775', (currec.yMin, ppem, str(badYMin)), "The following glyphs' actual yMin values " "exceed the stored value of %d at %d ppem: " "%s. Clipping may occur.")) except ScalerError: logger.error( ('V0554', (), "An error occured in the scaler during device " "metrics calculation, preventing validation.")) return False # Otherwise just report generically for the ppem (using MS # error code). else: logger.error( ('E2500', (ppem, currec.yMax, currec.yMin, recalcrec.yMax, recalcrec.yMin), "Record for %d ppem [%d,%d] is smaller than the " "calculated value [%d,%d]. Clipping may occur.")) return False logger.info(('P2500', (), "The VDMX data matches the expected values.")) return True