def run_ufolib_import_validation(self): """ ufoLib UFOReader.readInfo method validates value types in the fontinfo.plist file :return: (list) list of test failure Result objects """ res = Result(self.testpath) ss = StdStreamer(self.ufopath) if file_exists(self.testpath) is False: res.test_failed = ( False ) # not a mandatory file in UFO spec, test passes if missing ss.stream_result(res) return self.test_fail_list try: # read fontinfo.plist with ufoLib - the ufoLib library performs type validations on values on read ufolib_reader = UFOReader(self.ufopath, validate=True) ufolib_reader.readInfo(self.fontinfo_obj) res.test_failed = False ss.stream_result(res) except Exception as e: res.test_failed = True res.test_long_stdstream_string = ( self.testpath + " failed ufoLib import test with error: " + str(e) ) ss.stream_result(res) self.test_fail_list.append(res) return self.test_fail_list
def testUFO2(self): self.makeUFO(formatVersion=2) reader = UFOReader(self.ufoPath, validate=True) kerning = reader.readKerning() self.assertEqual(self.expectedKerning, kerning) groups = reader.readGroups() self.assertEqual(self.expectedGroups, groups) info = TestInfoObject() reader.readInfo(info)
def testUFO2(self): self.makeUFO(formatVersion=2) reader = UFOReader(self.ufoPath, validate=True) kerning = reader.readKerning() self.assertEqual(self.expectedKerning, kerning) groups = reader.readGroups() self.assertEqual(self.expectedGroups, groups) info = TestInfoObject() reader.readInfo(info)
def testRead(self): originalData = dict(fontInfoVersion1) self._writeInfoToPlist(originalData) infoObject = TestInfoObject() reader = UFOReader(self.dstDir, validate=True) reader.readInfo(infoObject) for attr in dir(infoObject): if attr not in fontInfoVersion2: continue originalValue = fontInfoVersion2[attr] readValue = getattr(infoObject, attr) self.assertEqual(originalValue, readValue)
def testRead(self): originalData = dict(fontInfoVersion1) self._writeInfoToPlist(originalData) infoObject = TestInfoObject() reader = UFOReader(self.dstDir, validate=True) reader.readInfo(infoObject) for attr in dir(infoObject): if attr not in fontInfoVersion2: continue originalValue = fontInfoVersion2[attr] readValue = getattr(infoObject, attr) self.assertEqual(originalValue, readValue)
def testFontStyleConversion(self): fontStyle1To2 = { 64: "regular", 1: "italic", 32: "bold", 33: "bold italic" } for old, new in list(fontStyle1To2.items()): info = dict(fontInfoVersion1) info["fontStyle"] = old self._writeInfoToPlist(info) reader = UFOReader(self.dstDir, validate=True) infoObject = TestInfoObject() reader.readInfo(infoObject) self.assertEqual(new, infoObject.styleMapStyleName)
def testFontStyleConversion(self): fontStyle1To2 = { 64 : "regular", 1 : "italic", 32 : "bold", 33 : "bold italic" } for old, new in list(fontStyle1To2.items()): info = dict(fontInfoVersion1) info["fontStyle"] = old self._writeInfoToPlist(info) reader = UFOReader(self.dstDir, validate=True) infoObject = TestInfoObject() reader.readInfo(infoObject) self.assertEqual(new, infoObject.styleMapStyleName)
def extractFontFromVFB( pathOrFile, destination, doGlyphs=True, doInfo=True, doKerning=True, doGroups=True, doFeatures=True, doLib=True, customFunctions=[], ): ufoPath = tempfile.mkdtemp(suffix=".ufo") cmds = [_ufo2vfbLocation, "-64", "-fo", pathOrFile, ufoPath] cmds = subprocess.list2cmdline(cmds) popen = subprocess.Popen(cmds, shell=True) popen.wait() try: # vfb2ufo writes ufo2, and has no update since 2015...so dont get to crazy here... # dont validate as vfb2ufo writes invalid ufos source = UFOReader(ufoPath, validate=False) if doInfo: source.readInfo(destination.info) if doKerning: kerning = source.readKerning() destination.kerning.update(kerning) if doGroups: groups = source.readGroups() destination.groups.update(groups) if doFeatures: features = source.readFeatures() destination.features.text = features if doLib: lib = source.readLib() destination.lib.update(lib) if doGlyphs: glyphSet = source.getGlyphSet() for glyphName in glyphSet.keys(): destination.newGlyph(glyphName) glyph = destination[glyphName] pointPen = glyph.getPointPen() glyphSet.readGlyph( glyphName=glyphName, glyphObject=glyph, pointPen=pointPen ) for function in customFunctions: function(source, destination) finally: shutil.rmtree(ufoPath)
def compileUFOToFont(ufoPath): """Compile the source UFO to a TTF with the smallest amount of tables needed to let HarfBuzz do its work. That would be 'cmap', 'post' and whatever OTL tables are needed for the features. Return the compiled font data. This function may do some redundant work (eg. we need an UFOReader elsewhere, too), but having a picklable argument and return value allows us to run it in a separate process, enabling parallelism. """ reader = UFOReader(ufoPath, validate=False) glyphSet = reader.getGlyphSet() info = SimpleNamespace() reader.readInfo(info) glyphOrder = sorted(glyphSet.keys()) # no need for the "real" glyph order if ".notdef" not in glyphOrder: # We need a .notdef glyph, so let's make one. glyphOrder.insert(0, ".notdef") cmap, revCmap, anchors = fetchCharacterMappingAndAnchors(glyphSet, ufoPath) fb = FontBuilder(round(info.unitsPerEm)) fb.setupGlyphOrder(glyphOrder) fb.setupCharacterMap(cmap) fb.setupPost() # This makes sure we store the glyph names ttFont = fb.font # Store anchors in the font as a private table: this is valuable # data that our parent process can use to do faster reloading upon # changes. ttFont["FGAx"] = newTable("FGAx") ttFont["FGAx"].data = pickle.dumps(anchors) ufo = MinimalFontObject(ufoPath, reader, revCmap, anchors) feaComp = FeatureCompiler(ufo, ttFont) try: feaComp.compile() except FeatureLibError as e: error = f"{e.__class__.__name__}: {e}" except Exception: # This is most likely a bug, and not an input error, so perhaps # we shouldn't even catch it here. error = traceback.format_exc() else: error = None return ttFont, error
def testWidthNameConversion(self): widthName1To2 = { "Ultra-condensed": 1, "Extra-condensed": 2, "Condensed": 3, "Semi-condensed": 4, "Medium (normal)": 5, "Semi-expanded": 6, "Expanded": 7, "Extra-expanded": 8, "Ultra-expanded": 9 } for old, new in list(widthName1To2.items()): info = dict(fontInfoVersion1) info["widthName"] = old self._writeInfoToPlist(info) reader = UFOReader(self.dstDir, validate=True) infoObject = TestInfoObject() reader.readInfo(infoObject) self.assertEqual(new, infoObject.openTypeOS2WidthClass)
def testWidthNameConversion(self): widthName1To2 = { "Ultra-condensed" : 1, "Extra-condensed" : 2, "Condensed" : 3, "Semi-condensed" : 4, "Medium (normal)" : 5, "Semi-expanded" : 6, "Expanded" : 7, "Extra-expanded" : 8, "Ultra-expanded" : 9 } for old, new in list(widthName1To2.items()): info = dict(fontInfoVersion1) info["widthName"] = old self._writeInfoToPlist(info) reader = UFOReader(self.dstDir, validate=True) infoObject = TestInfoObject() reader.readInfo(infoObject) self.assertEqual(new, infoObject.openTypeOS2WidthClass)
def getSortInfoUFO(fontPath: PathLike, fontNum: int): from fontTools.ufoLib import UFOReader assert fontNum == 0 suffix = fontPath.suffix.lower().lstrip(".") reader = UFOReader(fontPath, validate=False) info = SimpleNamespace() reader.readInfo(info) sortInfo = dict(suffix=suffix) ufoAttrs = [ ("familyName", "familyName"), ("styleName", "styleName"), ("weight", "openTypeOS2WeightClass"), ("width", "openTypeOS2WidthClass"), ("italicAngle", "italicAngle"), ] for key, attr in ufoAttrs: val = getattr(info, attr, None) if val is not None: if key == "italicAngle": val = -val # negative for intuitive sort order sortInfo[key] = val return sortInfo
def main(args=None): from io import open options = parse_args(args) config = getConfig(options.config) svg_file = options.infile # Parse SVG to read the width, height attributes defined in it svgObj = parseSvg(svg_file) width = float(svgObj.attrib['width'].replace("px", " ")) height = float(svgObj.attrib['height'].replace("px", " ")) name = os.path.splitext(os.path.basename(svg_file))[0] ufo_font_path = config['font']['ufo'] # Get the font metadata from UFO reader = UFOReader(ufo_font_path) writer = UFOWriter(ufo_font_path) infoObject = InfoObject() reader.readInfo(infoObject) fontName = config['font']['name'] # Get the configuration for this svg try: svg_config = config['svgs'][name] except KeyError: print("\033[93mSkip: Configuration not found for svg : %r\033[0m" % name) return if 'unicode' in svg_config: unicodeVal = unicode_hex_list(svg_config['unicode']) else: unicodeVal = None glyphWidth = width + int(svg_config['left']) + int(svg_config['right']) if glyphWidth < 0: raise UFOLibError("Glyph %s has negative width." % name) contentsPlistPath = ufo_font_path + '/glyphs/contents.plist' try: with open(contentsPlistPath, "rb") as f: contentsPlist = load(f) except: raise UFOLibError("The file %s could not be read." % contentsPlistPath) glyph_name = svg_config['glyph_name'] # Replace all capital letters with a following '_' to avoid file name clash in Windows glyph_file_name = re.sub(r'([A-Z]){1}', lambda pat: pat.group(1) + '_', glyph_name) + '.glif' if glyph_name in contentsPlist: existing_glyph = True else: existing_glyph = False # Calculate the transformation to do transform = transform_list(config['font']['transform']) base = 0 if 'base' in svg_config: base = int(svg_config['base']) transform[4] += int(svg_config['left']) # X offset = left bearing transform[5] += height + base # Y offset glif = svg2glif(svg_file, name=svg_config['glyph_name'], width=glyphWidth, height=getattr(infoObject, 'unitsPerEm'), unicodes=unicodeVal, transform=transform, version=config['font']['version']) if options.outfile is None: output_file = ufo_font_path + '/glyphs/' + glyph_file_name else: output_file = options.outfile with open(output_file, 'w', encoding='utf-8') as f: f.write(glif) print( "\033[94m[%s]\033[0m \033[92mConvert\033[0m %s -> %s \033[92m✔️\033[0m" % (fontName, name, output_file)) # If this is a new glyph, add it to the UFO/glyphs/contents.plist if not existing_glyph: contentsPlist[glyph_name] = glyph_file_name writePlistAtomically(contentsPlist, contentsPlistPath) print( "\033[94m[%s]\033[0m \033[92mAdd\033[0m %s -> %s \033[92m✔️\033[0m" % (fontName, glyph_name, glyph_file_name)) lib_obj = reader.readLib() lib_obj['public.glyphOrder'].append(glyph_name) writer.writeLib(lib_obj)
class UFOFont(BaseFont): ufoState = None def resetCache(self): super().resetCache() del self.defaultVerticalAdvance del self.defaultVerticalOriginY del self.globalColorLayerMapping def _setupReaderAndGlyphSet(self): self.reader = UFOReader(self.fontPath, validate=False) self.glyphSet = self.reader.getGlyphSet() self.glyphSet.glyphClass = Glyph self.layerGlyphSets = {} async def load(self, outputWriter): if hasattr(self, "reader"): self._cachedGlyphs = {} return self._setupReaderAndGlyphSet() self.info = SimpleNamespace() self.reader.readInfo(self.info) self.lib = self.reader.readLib() self._cachedGlyphs = {} if self.ufoState is None: includedFeatureFiles = extractIncludedFeatureFiles( self.fontPath, self.reader) self.ufoState = UFOState( self.reader, self.glyphSet, getUnicodesAndAnchors=self._getUnicodesAndAnchors, includedFeatureFiles=includedFeatureFiles) fontData = await compileUFOToBytes(self.fontPath, outputWriter) f = io.BytesIO(fontData) self.ttFont = TTFont(f, lazy=True) self.shaper = self._getShaper(fontData) def updateFontPath(self, newFontPath): """This gets called when the source file was moved.""" super().updateFontPath(newFontPath) self._setupReaderAndGlyphSet() def getExternalFiles(self): return self.ufoState.includedFeatureFiles def canReloadWithChange(self, externalFilePath): if self.reader.fileStructure != UFOFileStructure.PACKAGE: # We can't (won't) partially reload .ufoz return False if externalFilePath: # Features need to be recompiled no matter what return False self.glyphSet.rebuildContents() self.ufoState = self.ufoState.newState() (needsFeaturesUpdate, needsGlyphUpdate, needsInfoUpdate, needsCmapUpdate, needsLibUpdate) = self.ufoState.getUpdateInfo() if needsFeaturesUpdate: return False if needsInfoUpdate: # font.info changed, all we care about is a possibly change unitsPerEm self.info = SimpleNamespace() self.reader.readInfo(self.info) if needsCmapUpdate: # The cmap changed. Let's update it in-place and only rebuild the shaper newCmap = { code: gn for gn, codes in self.ufoState.unicodes.items() for code in codes } fb = FontBuilder(font=self.ttFont) fb.setupCharacterMap(newCmap) f = io.BytesIO() self.ttFont.save(f, reorderTables=False) self.shaper = self._getShaper(f.getvalue()) if needsLibUpdate: self.lib = self.reader.readLib() # We don't explicitly track changes in layers, but they may be involved # in building layered color glyphs, so let's just always reset the cache. self.resetCache() return True def _getUnicodesAndAnchors(self): unicodes = defaultdict(list) for code, gn in self.ttFont.getBestCmap().items(): unicodes[gn].append(code) anchors = pickle.loads(self.ttFont["FGAx"].data) return unicodes, anchors def _getShaper(self, fontData): return HBShape(fontData, getHorizontalAdvance=self._getHorizontalAdvance, getVerticalAdvance=self._getVerticalAdvance, getVerticalOrigin=self._getVerticalOrigin, ttFont=self.ttFont) @cachedProperty def unitsPerEm(self): return self.info.unitsPerEm def _getGlyph(self, glyphName, layerName=None): glyph = self._cachedGlyphs.get((layerName, glyphName)) if glyph is None: if glyphName == ".notdef" and glyphName not in self.glyphSet: # We need a .notdef glyph, so let's make one. glyph = NotDefGlyph(self.info.unitsPerEm) self._addOutlinePathToGlyph(glyph) else: try: if layerName is None: glyph = self.glyphSet[glyphName] else: glyph = self.getLayerGlyphSet(layerName)[glyphName] self._addOutlinePathToGlyph(glyph) except Exception as e: # TODO: logging would be better but then capturing in mainWindow.py is harder print(f"Glyph '{glyphName}' could not be read: {e!r}", file=sys.stderr) glyph = self._getGlyph(".notdef") self._cachedGlyphs[(layerName, glyphName)] = glyph return glyph def _addOutlinePathToGlyph(self, glyph): if self.cocoa: pen = CocoaPen(self.glyphSet) glyph.draw(pen) glyph.outline = pen.path else: pen = RecordingPen() glyph.draw(pen) glyph.outline = pen def _getHorizontalAdvance(self, glyphName): glyph = self._getGlyph(glyphName) return glyph.width @cachedProperty def defaultVerticalAdvance(self): ascender = getattr(self.info, "ascender", None) descender = getattr(self.info, "descender", None) if ascender is None or descender is None: return self.info.unitsPerEm else: return ascender + abs(descender) @cachedProperty def defaultVerticalOriginY(self): ascender = getattr(self.info, "ascender", None) if ascender is None: return self.info.unitsPerEm # ??? else: return ascender def _getVerticalAdvance(self, glyphName): glyph = self._getGlyph(glyphName) vAdvance = glyph.height if vAdvance is None or vAdvance == 0: # XXX default vAdv == 0 -> bad UFO spec vAdvance = self.defaultVerticalAdvance return -abs(vAdvance) def _getVerticalOrigin(self, glyphName): glyph = self._getGlyph(glyphName) vOrgX = glyph.width / 2 lib = getattr(glyph, "lib", {}) vOrgY = lib.get("public.verticalOrigin") if vOrgY is None: vOrgY = self.defaultVerticalOriginY return True, vOrgX, vOrgY def _getGlyphDrawing(self, glyphName, colorLayers): glyph = self._getGlyph(glyphName) if colorLayers: colorLayerMapping = glyph.lib.get(COLOR_LAYER_MAPPING_KEY) if colorLayerMapping is None: colorLayerMapping = self.globalColorLayerMapping if colorLayerMapping is not None: layers = [] for layerName, colorID in colorLayerMapping: glyph = self._getGlyph(glyphName, layerName) if not isinstance(glyph, NotDefGlyph): layers.append((glyph.outline, colorID)) if layers: return GlyphDrawing(layers) return GlyphDrawing([(glyph.outline, None)]) @cachedProperty def colorPalettes(self): return self.lib.get(COLOR_PALETTES_KEY) @cachedProperty def globalColorLayerMapping(self): return self.lib.get(COLOR_LAYER_MAPPING_KEY) def getLayerGlyphSet(self, layerName): layerGlyphSet = self.layerGlyphSets.get(layerName) if layerGlyphSet is None: layerGlyphSet = self.reader.getGlyphSet(layerName) self.layerGlyphSets[layerName] = layerGlyphSet return layerGlyphSet
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(cls, reader: UFOReader) -> "Info": """Instantiates a Info object from a :class:`fontTools.ufoLib.UFOReader`.""" self = cls() reader.readInfo(self) return self