def test_pointCollector(): ufoPath = getFontPath("MutatorSansBoldWideMutated.ufo") reader = UFOReader(ufoPath) glyphSet = reader.getGlyphSet() pen = PointCollector(glyphSet) glyphSet["B"].draw(pen) assert len(pen.points) == 38 assert len(pen.tags) == 38 assert pen.contours == [3, 37] assert pen.tags[:12] == [1, 1, 1, 1, 1, 1, 2, 2, 1, 2, 2, 1] pen = PointCollector(glyphSet, decompose=False) glyphSet["Aacute"].draw(pen) assert len(pen.points) == 0 assert pen.contours == [] assert pen.components == [("A", (1, 0, 0, 1, 0, 0)), ("acute", (1, 0, 0, 1, 484, 20))] pen = PointCollector(glyphSet, decompose=True) glyphSet["Aacute"].draw(pen) assert len(pen.points) == 20 assert pen.contours == [3, 7, 11, 15, 19] assert pen.components == [] pen = PointCollector(glyphSet) glyphSet["O"].draw(pen) assert len(pen.points) == 28 assert pen.contours == [13, 27]
def reloadLayers(self, layerData): """ Reload the layers. This should not be called externally. """ reader = UFOReader(self.font.path, validate=self.font.ufoLibReadValidate) # handle the layers currentLayerOrder = self.layerOrder for layerName, l in layerData.get("layers", {}).items(): # new layer if layerName not in currentLayerOrder: glyphSet = reader.getGlyphSet(layerName, validateRead=self.ufoLibReadValidate, validateWrite=self.font.ufoLibWriteValidate) self.newLayer(layerName, glyphSet) # get the layer layer = self[layerName] # reload the layer info if l.get("info"): layer.color = None layer.lib.clear() layer._glyphSet.readLayerInfo(layer) self._stampLayerInfoDataState(layer) # reload the glyphs glyphNames = l.get("glyphNames", []) if glyphNames: layer.reloadGlyphs(glyphNames) # handle the order if layerData.get("order", False): newLayerOrder = reader.getLayerNames() for layerName in self.layerOrder: if layerName not in newLayerOrder: newLayerOrder.append(layerName) self.layerOrder = newLayerOrder # handle the default layer if layerData.get("default", False): newDefaultLayerName = reader.getDefaultLayerName() self.defaultLayer = self[newDefaultLayerName]
def test_ufoCharacterMapping_glyphNames(): ufoPath = getFontPath("MutatorSansBoldWideMutated.ufo") reader = UFOReader(ufoPath) cmap, revCmap, anchors = fetchCharacterMappingAndAnchors( reader.getGlyphSet(), ufoPath, ["A"]) assert cmap[0x0041] == "A" assert revCmap["A"] == [0x0041] assert anchors == {"A": [("top", 645, 815)]}
def test_getUpdateInfo(tmpdir): ufoSource = getFontPath("MutatorSansBoldWideMutated.ufo") ufoPath = shutil.copytree(ufoSource, tmpdir / "test.ufo") reader = UFOReader(ufoPath, validate=False) glyphSet = reader.getGlyphSet() cmap, unicodes, anchors = fetchCharacterMappingAndAnchors(glyphSet, ufoPath) state = UFOState(reader, glyphSet, getUnicodesAndAnchors=lambda: (unicodes, anchors)) feaPath = pathlib.Path(reader.fs.getsyspath("/features.fea")) feaPath.touch() state = state.newState() (needsFeaturesUpdate, needsGlyphUpdate, needsInfoUpdate, needsCmapUpdate, needsLibUpdate) = state.getUpdateInfo() assert needsFeaturesUpdate assert not needsGlyphUpdate assert not needsInfoUpdate assert not needsCmapUpdate infoPath = pathlib.Path(reader.fs.getsyspath("/fontinfo.plist")) infoPath.touch() state = state.newState() (needsFeaturesUpdate, needsGlyphUpdate, needsInfoUpdate, needsCmapUpdate, needsLibUpdate) = state.getUpdateInfo() assert not needsFeaturesUpdate assert not needsGlyphUpdate assert needsInfoUpdate assert not needsCmapUpdate glyph = Glyph("A", None) ppen = RecordingPointPen() glyphSet.readGlyph("A", glyph, ppen) glyph.anchors[0]["x"] = 123 glyphSet.writeGlyph("A", glyph, ppen.replay) state = state.newState() (needsFeaturesUpdate, needsGlyphUpdate, needsInfoUpdate, needsCmapUpdate, needsLibUpdate) = state.getUpdateInfo() assert needsFeaturesUpdate assert needsGlyphUpdate assert not needsInfoUpdate assert not needsCmapUpdate glyph = Glyph("A", None) ppen = RecordingPointPen() glyphSet.readGlyph("A", glyph, ppen) glyph.unicodes = [123] glyphSet.writeGlyph("A", glyph, ppen.replay) state = state.newState() (needsFeaturesUpdate, needsGlyphUpdate, needsInfoUpdate, needsCmapUpdate, needsLibUpdate) = state.getUpdateInfo() assert not needsFeaturesUpdate assert needsGlyphUpdate assert not needsInfoUpdate assert needsCmapUpdate
def test_pointCollectorQuad(): ufoPath = getFontPath("QuadTest-Regular.ufo") reader = UFOReader(ufoPath) glyphSet = reader.getGlyphSet() pen = PointCollector(glyphSet) glyphSet["a"].draw(pen) assert len(pen.points) == 4 assert len(pen.tags) == 4 assert pen.contours == [3] assert pen.tags == [0, 0, 0, 0]
def _fontSaveWasCompleted(self): """ When saving a UFOZ, the underlying ZipFS object is closed. The objects then stored in layer._glyphSet contain references to a closed, and therefore unusable, filesystem. To remedy this, after the save is completed this method will be called and new GlyphSet objects will be created and assigned to the layers. """ reader = UFOReader(self.font.path, validate=self.font.ufoLibReadValidate) if reader.fileStructure is UFOFileStructure.ZIP: for layerName in self.layerOrder: layer = self[layerName] layer._glyphSet = reader.getGlyphSet(layerName=layerName, validateRead=self.ufoLibReadValidate)
def _fontSaveWasCompleted(self): """ When saving a UFOZ, the underlying ZipFS object is closed. The objects then stored in layer._glyphSet contain references to a closed, and therefore unusable, filesystem. To remedy this, after the save is completed this method will be called and new GlyphSet objects will be created and assigned to the layers. """ reader = UFOReader(self.font.path, validate=self.font.ufoLibReadValidate) if reader.fileStructure is UFOFileStructure.ZIP: for layerName in self.layerOrder: layer = self[layerName] layer._glyphSet = reader.getGlyphSet( layerName=layerName, validateRead=self.ufoLibReadValidate)
def test_ufoCharacterMapping(): ufoPath = getFontPath("MutatorSansBoldWideMutated.ufo") reader = UFOReader(ufoPath) cmap, revCmap, anchors = fetchCharacterMappingAndAnchors( reader.getGlyphSet(), ufoPath) assert cmap[0x0041] == "A" assert revCmap["A"] == [0x0041] # MutatorSansBoldWideMutated.ufo/glyphs/A_.glif contains a commented-out <unicode> # tag, that must not be parsed, as well as a commented-out <anchor>. assert 0x1234 not in cmap assert anchors == { "A": [("top", 645, 815)], "E": [("top", 582.5, 815)], "macroncmb": [("_top", 0, 815)] }
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 test_pointCollector(): ufoPath = getFontPath("MutatorSansBoldWideMutated.ufo") reader = UFOReader(ufoPath) glyphSet = reader.getGlyphSet() pen = PointCollector(glyphSet) glyphSet["B"].draw(pen) assert len(pen.points) == 38 assert len(pen.tags) == 38 assert pen.contours == [3, 37] assert pen.tags[:12] == [1, 1, 1, 1, 1, 1, 2, 2, 1, 2, 2, 1] pen = PointCollector(glyphSet) glyphSet["Aacute"].draw(pen) assert len(pen.points) == 20 assert pen.contours == [3, 7, 11, 15, 19] pen = PointCollector(glyphSet) glyphSet["O"].draw(pen) assert len(pen.points) == 28 assert pen.contours == [13, 27]
def reloadLayers(self, layerData): """ Reload the layers. This should not be called externally. """ reader = UFOReader(self.font.path, validate=self.font.ufoLibReadValidate) # handle the layers currentLayerOrder = self.layerOrder for layerName, l in layerData.get("layers", {}).items(): # new layer if layerName not in currentLayerOrder: glyphSet = reader.getGlyphSet( layerName, validateRead=self.ufoLibReadValidate, validateWrite=self.font.ufoLibWriteValidate) self.newLayer(layerName, glyphSet) # get the layer layer = self[layerName] # reload the layer info if l.get("info"): layer.color = None layer.lib.clear() layer._glyphSet.readLayerInfo(layer) self._stampLayerInfoDataState(layer) # reload the glyphs glyphNames = l.get("glyphNames", []) if glyphNames: layer.reloadGlyphs(glyphNames) # handle the order if layerData.get("order", False): newLayerOrder = reader.getLayerNames() for layerName in self.layerOrder: if layerName not in newLayerOrder: newLayerOrder.append(layerName) self.layerOrder = newLayerOrder # handle the default layer if layerData.get("default", False): newDefaultLayerName = reader.getDefaultLayerName() self.defaultLayer = self[newDefaultLayerName]
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
async def load(self, outputWriter): if self.doc is None: self.doc = DesignSpaceDocument.fromfile(self.fontPath) self.doc.findDefault() with tempfile.TemporaryDirectory( prefix="fontgoggles_temp") as ttFolder: sourcePathToTTPath = getTTPaths(self.doc, ttFolder) ufosToCompile = [] ttPaths = [] outputs = [] coros = [] self._sourceFiles = defaultdict(list) self._includedFeatureFiles = defaultdict(list) previousUFOs = self._ufos self._ufos = {} previousSourceData = self._sourceFontData self._sourceFontData = {} for source in self.doc.sources: sourceKey = (source.path, source.layerName) self._sourceFiles[pathlib.Path(source.path)].append(sourceKey) ufoState = previousUFOs.get(sourceKey) if ufoState is None: reader = UFOReader(source.path, validate=False) glyphSet = reader.getGlyphSet(layerName=source.layerName) glyphSet.glyphClass = Glyph if source.layerName is None: includedFeatureFiles = extractIncludedFeatureFiles( source.path, reader) getUnicodesAndAnchors = functools.partial( self._getUnicodesAndAnchors, source.path) else: includedFeatureFiles = [] # We're not compiling features nor do we need cmaps for these sparse layers, # so we don't need need proper anchor or unicode data def getUnicodesAndAnchors(): return ({}, {}) ufoState = UFOState( reader, glyphSet, getUnicodesAndAnchors=getUnicodesAndAnchors, includedFeatureFiles=includedFeatureFiles) for includedFeaFile in ufoState.includedFeatureFiles: self._includedFeatureFiles[includedFeaFile].append( sourceKey) self._ufos[sourceKey] = ufoState if source.layerName is not None: continue if source.path in ufosToCompile: continue ttPath = sourcePathToTTPath[source.path] if source.path in previousSourceData: with open(ttPath, "wb") as f: f.write(previousSourceData[source.path]) self._sourceFontData[source.path] = previousSourceData[ source.path] else: ufosToCompile.append(source.path) ttPaths.append(ttPath) output = io.StringIO() outputs.append(output) coros.append( compileUFOToPath(source.path, ttPath, output.write)) # print(f"compiling {len(coros)} fonts") errors = await asyncio.gather(*coros, return_exceptions=True) for sourcePath, exc, output in zip(ufosToCompile, errors, outputs): output = output.getvalue() if output or exc is not None: outputWriter(f"compile output for {sourcePath}:\n") if output: outputWriter(output) if exc is not None: outputWriter(f"{exc!r}\n") if any(errors): raise DesignSpaceSourceError( f"Could not build '{os.path.basename(self.fontPath)}': " "some sources did not successfully compile") for sourcePath, ttPath in zip(ufosToCompile, ttPaths): # Store compiled tt data so we can reuse it to rebuild ourselves # without recompiling the source. with open(ttPath, "rb") as f: self._sourceFontData[sourcePath] = f.read() if not ufosToCompile and not self._needsVFRebuild: # self.ttFont and self.shaper are still up-to-date return vfFontData = await compileDSToBytes(self.fontPath, ttFolder, outputWriter) f = io.BytesIO(vfFontData) self.ttFont = TTFont(f, lazy=True) # Nice cookie for us from the worker self.masterModel = pickle.loads(self.ttFont["MPcl"].data) assert len(self.masterModel.deltaWeights) == len(self.doc.sources) self.shaper = HBShape(vfFontData, getHorizontalAdvance=self._getHorizontalAdvance, getVerticalAdvance=self._getVerticalAdvance, getVerticalOrigin=self._getVerticalOrigin, ttFont=self.ttFont) self._needsVFRebuild = False
def _loadLayer(reader: UFOReader, layerName: str, lazy: bool = True, default: bool = False) -> Layer: glyphSet = reader.getGlyphSet(layerName) return Layer.read(layerName, glyphSet, lazy=lazy, default=default)
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