class UFOFontData: def __init__(self, path, log_only, write_to_default_layer): self._reader = UFOReader(path, validate=False) self.path = path self._glyphmap = None self._processed_layer_glyphmap = None self.newGlyphMap = {} self._fontInfo = None self._glyphsets = {} # If True, we are running in report mode and not doing any changes, so # we skip the hash map and process all glyphs. self.log_only = log_only # Used to store the hash of glyph data of already processed glyphs. If # the stored hash matches the calculated one, we skip the glyph. self._hashmap = None self.fontDict = None self.hashMapChanged = False # If True, then write data to the default layer self.writeToDefaultLayer = write_to_default_layer def getUnitsPerEm(self): return self.fontInfo.get("unitsPerEm", 1000) def getPSName(self): return self.fontInfo.get("postscriptFontName", "PSName-Undefined") @staticmethod def isCID(): return False def convertToBez(self, name, read_hints, round_coords, doAll=False): # We do not yet support reading hints, so read_hints is ignored. width, bez, skip = self._get_or_skip_glyph(name, round_coords, doAll) if skip: return None, width bezString = "\n".join(bez) bezString = "\n".join(["% " + name, "sc", bezString, "ed", ""]) return bezString, width def updateFromBez(self, bezData, name, width, mm_hint_info=None): # For UFO font, we don't use the width parameter: # it is carried over from the input glif file. layer = None if name in self.processedLayerGlyphMap: layer = PROCESSED_LAYER_NAME glyphset = self._get_glyphset(layer) glyph = BezGlyph(bezData) glyphset.readGlyph(name, glyph) if hasattr(glyph, 'width'): glyph.width = norm_float(glyph.width) self.newGlyphMap[name] = glyph # updateFromBez is called only if the glyph has been autohinted which # might also change its outline data. We need to update the edit status # in the hash map entry. I assume that convertToBez has been run # before, which will add an entry for this glyph. self.updateHashEntry(name) def save(self, path): if path is None: path = self.path if os.path.abspath(self.path) != os.path.abspath(path): # If user has specified a path other than the source font path, # then copy the entire UFO font, and operate on the copy. log.info("Copying from source UFO font to output UFO font before " "processing...") if os.path.exists(path): shutil.rmtree(path) shutil.copytree(self.path, path) writer = UFOWriter(path, self._reader.formatVersion, validate=False) layer = PROCESSED_LAYER_NAME if self.writeToDefaultLayer: layer = None # Write layer contents. layers = writer.layerContents.copy() if self.writeToDefaultLayer and PROCESSED_LAYER_NAME in layers: # Delete processed glyphs directory writer.deleteGlyphSet(PROCESSED_LAYER_NAME) # Remove entry from 'layercontents.plist' file del layers[PROCESSED_LAYER_NAME] elif self.processedLayerGlyphMap or not self.writeToDefaultLayer: layers[PROCESSED_LAYER_NAME] = PROCESSED_GLYPHS_DIRNAME writer.layerContents.update(layers) writer.writeLayerContents() # Write glyphs. glyphset = writer.getGlyphSet(layer, defaultLayer=layer is None) for name, glyph in self.newGlyphMap.items(): filename = self.glyphMap[name] if not self.writeToDefaultLayer and \ name in self.processedLayerGlyphMap: filename = self.processedLayerGlyphMap[name] # Recalculate glyph hashes if self.writeToDefaultLayer: self.recalcHashEntry(name, glyph) glyphset.contents[name] = filename glyphset.writeGlyph(name, glyph, glyph.drawPoints) glyphset.writeContents() # Write hashmap if self.hashMapChanged: self.writeHashMap(writer) @property def hashMap(self): if self._hashmap is None: try: data = self._reader.readData(HASHMAP_NAME) except UFOLibError: data = None if data: hashmap = ast.literal_eval(data.decode("utf-8")) else: hashmap = {HASHMAP_VERSION_NAME: HASHMAP_VERSION} version = (0, 0) if HASHMAP_VERSION_NAME in hashmap: version = hashmap[HASHMAP_VERSION_NAME] if version[0] > HASHMAP_VERSION[0]: raise FontParseError("Hash map version is newer than " "psautohint. Please update.") elif version[0] < HASHMAP_VERSION[0]: log.info("Updating hash map: was older version") hashmap = {HASHMAP_VERSION_NAME: HASHMAP_VERSION} self._hashmap = hashmap return self._hashmap def writeHashMap(self, writer): hashMap = self.hashMap if not hashMap: return # no glyphs were processed. data = ["{"] for gName in sorted(hashMap.keys()): data.append("'%s': %s," % (gName, hashMap[gName])) data.append("}") data.append("") data = "\n".join(data) writer.writeData(HASHMAP_NAME, data.encode("utf-8")) def updateHashEntry(self, glyphName): # srcHash has already been set: we are fixing the history list. # Get hash entry for glyph srcHash, historyList = self.hashMap[glyphName] self.hashMapChanged = True # If the program is not in the history list, add it. if AUTOHINT_NAME not in historyList: historyList.append(AUTOHINT_NAME) def recalcHashEntry(self, glyphName, glyph): hashBefore, historyList = self.hashMap[glyphName] hash_pen = HashPointPen(glyph) glyph.drawPoints(hash_pen) hashAfter = hash_pen.getHash() if hashAfter != hashBefore: self.hashMap[glyphName] = [tostr(hashAfter), historyList] self.hashMapChanged = True def checkSkipGlyph(self, glyphName, newSrcHash, doAll): skip = False if self.log_only: return skip srcHash = None historyList = [] # Get hash entry for glyph if glyphName in self.hashMap: srcHash, historyList = self.hashMap[glyphName] if srcHash == newSrcHash: if AUTOHINT_NAME in historyList: # The glyph has already been autohinted, and there have been no # changes since. skip = not doAll if not skip and AUTOHINT_NAME not in historyList: historyList.append(AUTOHINT_NAME) else: if CHECKOUTLINE_NAME in historyList: log.error( "Glyph '%s' has been edited. You must first " "run '%s' before running '%s'. Skipping.", glyphName, CHECKOUTLINE_NAME, AUTOHINT_NAME) skip = True # If the source hash has changed, we need to delete the processed # layer glyph. self.hashMapChanged = True self.hashMap[glyphName] = [tostr(newSrcHash), [AUTOHINT_NAME]] if glyphName in self.processedLayerGlyphMap: del self.processedLayerGlyphMap[glyphName] return skip def _get_glyphset(self, layer_name=None): if layer_name not in self._glyphsets: glyphset = None try: glyphset = self._reader.getGlyphSet(layer_name) except UFOLibError: pass self._glyphsets[layer_name] = glyphset return self._glyphsets[layer_name] @staticmethod def get_glyph_bez(glyph, round_coords): pen = BezPen(glyph.glyphSet, round_coords) glyph.draw(pen) if not hasattr(glyph, "width"): glyph.width = 0 return pen.bez def _get_or_skip_glyph(self, name, round_coords, doAll): # Get default glyph layer data, so we can check if the glyph # has been edited since this program was last run. # If the program name is in the history list, and the srcHash # matches the default glyph layer data, we can skip. glyphset = self._get_glyphset() glyph = glyphset[name] bez = self.get_glyph_bez(glyph, round_coords) # Hash is always from the default glyph layer. hash_pen = HashPointPen(glyph) glyph.drawPoints(hash_pen) skip = self.checkSkipGlyph(name, hash_pen.getHash(), doAll) # If there is a glyph in the processed layer, get the outline from it. if name in self.processedLayerGlyphMap: glyphset = self._get_glyphset(PROCESSED_LAYER_NAME) glyph = glyphset[name] bez = self.get_glyph_bez(glyph, round_coords) return glyph.width, bez, skip def getGlyphList(self): glyphOrder = self._reader.readLib().get(PUBLIC_GLYPH_ORDER, []) glyphList = list(self._get_glyphset().keys()) # Sort the returned glyph list by the glyph order as we depend in the # order for expanding glyph ranges. def key_fn(v): if v in glyphOrder: return glyphOrder.index(v) return len(glyphOrder) return sorted(glyphList, key=key_fn) @property def glyphMap(self): if self._glyphmap is None: glyphset = self._get_glyphset() self._glyphmap = glyphset.contents return self._glyphmap @property def processedLayerGlyphMap(self): if self._processed_layer_glyphmap is None: self._processed_layer_glyphmap = {} glyphset = self._get_glyphset(PROCESSED_LAYER_NAME) if glyphset is not None: self._processed_layer_glyphmap = glyphset.contents return self._processed_layer_glyphmap @property def fontInfo(self): if self._fontInfo is None: info = SimpleNamespace() self._reader.readInfo(info) self._fontInfo = vars(info) return self._fontInfo def getFontInfo(self, allow_no_blues, noFlex, vCounterGlyphs, hCounterGlyphs, fdIndex=0): if self.fontDict is not None: return self.fontDict fdDict = fdTools.FDDict() # should be 1 if the glyphs are ideographic, else 0. fdDict.LanguageGroup = self.fontInfo.get("languagegroup", "0") fdDict.OrigEmSqUnits = self.getUnitsPerEm() fdDict.FontName = self.getPSName() upm = self.getUnitsPerEm() low = min(-upm * 0.25, self.fontInfo.get("openTypeOS2WinDescent", 0) - 200) high = max(upm * 1.25, self.fontInfo.get("openTypeOS2WinAscent", 0) + 200) # Make a set of inactive alignment zones: zones outside of the font # bbox so as not to affect hinting. Used when src font has no # BlueValues or has invalid BlueValues. Some fonts have bad BBox # values, so I don't let this be smaller than -upm*0.25, upm*1.25. inactiveAlignmentValues = [low, low, high, high] blueValues = self.fontInfo.get("postscriptBlueValues", []) numBlueValues = len(blueValues) if numBlueValues < 4: if allow_no_blues: blueValues = inactiveAlignmentValues numBlueValues = len(blueValues) else: raise FontParseError( "Font must have at least four values in its " "BlueValues array for PSAutoHint to work!") blueValues.sort() # The first pair only is a bottom zone, where the first value is the # overshoot position; the rest are top zones, and second value of the # pair is the overshoot position. blueValues[0] = blueValues[0] - blueValues[1] for i in range(3, numBlueValues, 2): blueValues[i] = blueValues[i] - blueValues[i - 1] blueValues = [str(v) for v in blueValues] numBlueValues = min(numBlueValues, len(fdTools.kBlueValueKeys)) for i in range(numBlueValues): key = fdTools.kBlueValueKeys[i] value = blueValues[i] setattr(fdDict, key, value) otherBlues = self.fontInfo.get("postscriptOtherBlues", []) numBlueValues = len(otherBlues) if len(otherBlues) > 0: i = 0 numBlueValues = len(otherBlues) otherBlues.sort() for i in range(0, numBlueValues, 2): otherBlues[i] = otherBlues[i] - otherBlues[i + 1] otherBlues = [str(v) for v in otherBlues] numBlueValues = min(numBlueValues, len(fdTools.kOtherBlueValueKeys)) for i in range(numBlueValues): key = fdTools.kOtherBlueValueKeys[i] value = otherBlues[i] setattr(fdDict, key, value) vstems = self.fontInfo.get("postscriptStemSnapV", []) if not vstems: if allow_no_blues: # dummy value. Needs to be larger than any hint will likely be, # as the autohint program strips out any hint wider than twice # the largest global stem width. vstems = [fdDict.OrigEmSqUnits] else: raise FontParseError("Font does not have postscriptStemSnapV!") vstems.sort() if not vstems or (len(vstems) == 1 and vstems[0] < 1): # dummy value that will allow PyAC to run vstems = [fdDict.OrigEmSqUnits] log.warning("There is no value or 0 value for DominantV.") fdDict.DominantV = "[" + " ".join([str(v) for v in vstems]) + "]" hstems = self.fontInfo.get("postscriptStemSnapH", []) if not hstems: if allow_no_blues: # dummy value. Needs to be larger than any hint will likely be, # as the autohint program strips out any hint wider than twice # the largest global stem width. hstems = [fdDict.OrigEmSqUnits] else: raise FontParseError("Font does not have postscriptStemSnapH!") hstems.sort() if not hstems or (len(hstems) == 1 and hstems[0] < 1): # dummy value that will allow PyAC to run hstems = [fdDict.OrigEmSqUnits] log.warning("There is no value or 0 value for DominantH.") fdDict.DominantH = "[" + " ".join([str(v) for v in hstems]) + "]" if noFlex: fdDict.FlexOK = "false" else: fdDict.FlexOK = "true" # Add candidate lists for counter hints, if any. if vCounterGlyphs: temp = " ".join(vCounterGlyphs) fdDict.VCounterChars = "( %s )" % (temp) if hCounterGlyphs: temp = " ".join(hCounterGlyphs) fdDict.HCounterChars = "( %s )" % (temp) fdDict.BlueFuzz = self.fontInfo.get("postscriptBlueFuzz", 1) # postscriptBlueShift # postscriptBlueScale self.fontDict = fdDict return fdDict def getfdInfo(self, allow_no_blues, noFlex, vCounterGlyphs, hCounterGlyphs, glyphList, fdIndex=0): fdGlyphDict = None fdDict = self.getFontInfo(allow_no_blues, noFlex, vCounterGlyphs, hCounterGlyphs, fdIndex) fontDictList = [fdDict] # Check the fontinfo file, and add any other font dicts srcFontInfo = os.path.dirname(self.path) srcFontInfo = os.path.join(srcFontInfo, "fontinfo") maxX = self.getUnitsPerEm() * 2 maxY = maxX minY = -self.getUnitsPerEm() if os.path.exists(srcFontInfo): with open(srcFontInfo, "r", encoding="utf-8") as fi: fontInfoData = fi.read() fontInfoData = re.sub(r"#[^\r\n]+", "", fontInfoData) if "FDDict" in fontInfoData: fdGlyphDict, fontDictList, finalFDict = \ fdTools.parseFontInfoFile( fontDictList, fontInfoData, glyphList, maxY, minY, self.getPSName()) if finalFDict is None: # If a font dict was not explicitly specified for the # output font, use the first user-specified font dict. fdTools.mergeFDDicts(fontDictList[1:], self.fontDict) else: fdTools.mergeFDDicts([finalFDict], self.fontDict) return fdGlyphDict, fontDictList @staticmethod def close(): return
def read_data(reader: UFOReader, filename: str) -> bytes: """Returns the data at filename within the store.""" return reader.readData(filename) # type: ignore