diff --git a/src/fontra_rcjk/backend_fs.py b/src/fontra_rcjk/backend_fs.py index b2fc057..7830347 100644 --- a/src/fontra_rcjk/backend_fs.py +++ b/src/fontra_rcjk/backend_fs.py @@ -84,7 +84,7 @@ def getGlyphSetForGlyph(self, glyphName): return self.characterGlyphGlyphSet async def getGlyphMap(self): - return self._glyphMap + return dict(self._glyphMap) async def getGlobalAxes(self): axes = getattr(self, "_globalAxes", None) @@ -142,6 +142,16 @@ async def putGlyph(self, glyphName, glyph, unicodes): self._glyphMap[glyphName] = unicodes self._tempGlyphCache[glyphName] = layerGlyphs + async def deleteGlyph(self, glyphName): + if glyphName not in self._glyphMap: + raise KeyError(f"Glyph '{glyphName}' does not exist") + + for gs, _ in self._iterGlyphSets(): + if glyphName in gs: + gs.deleteGlyph(glyphName) + + del self._glyphMap[glyphName] + async def getFontLib(self): fontLib = {} libPath = self.path / "fontLib.json" @@ -270,3 +280,18 @@ def putGlyphLayerData(self, glyphName, glyphLayerData): layerPath.unlink(missing_ok=True) self.registerWrittenPath(layerPath, deleted=True) del layerContents[mainFileName] + + def deleteGlyph(self, glyphName): + mainPath = self.contents[glyphName] + del self.contents[glyphName] + pathsToDelete = [mainPath] + mainFileName = mainPath.name + for layerName, layerContents in self.layers.items(): + layerPath = layerContents.get(mainFileName) + if layerPath is not None: + pathsToDelete.append(layerPath) + del layerContents[mainFileName] + for layerPath in pathsToDelete: + layerPath.unlink() + self.registerWrittenPath(layerPath, deleted=True) + del self.glyphMap[glyphName] diff --git a/src/fontra_rcjk/backend_mysql.py b/src/fontra_rcjk/backend_mysql.py index 653f68e..8ad85ce 100644 --- a/src/fontra_rcjk/backend_mysql.py +++ b/src/fontra_rcjk/backend_mysql.py @@ -241,6 +241,7 @@ async def _newGlyph(self, glyphName, unicodes): # with a lock acquired. Our dummy glyph has to have a glyph name, but # we're setting .width to an arbitrary positive value so we can still # see it if anything goes wrong. + logger.info(f"Creating new glyph '{glyphName}'") dummyGlyph = GLIFGlyph() dummyGlyph.name = glyphName dummyGlyph.unicodes = unicodes @@ -254,6 +255,26 @@ async def _newGlyph(self, glyphName, unicodes): self._rcjkGlyphInfo[glyphName] = ("CG", glyphID) self._glyphTimeStamps[glyphName] = getUpdatedTimeStamp(response["data"]) + async def deleteGlyph(self, glyphName): + if glyphName not in self._rcjkGlyphInfo: + raise KeyError(f"Glyph '{glyphName}' does not exist") + + logger.info(f"Deleting glyph '{glyphName}'") + + _ = await self._callGlyphMethod(glyphName, "lock", return_data=False) + try: + _ = await self._callGlyphMethod(glyphName, "delete") + except Exception: + _ = await self._callGlyphMethod(glyphName, "unlock", return_data=False) + raise + + # We set the time stamp to None so we can later distinguish "we deleted this + # glyph" from "this glyph was externally deleted" + self._glyphTimeStamps[glyphName] = None + del self._rcjkGlyphInfo[glyphName] + del self._glyphMap[glyphName] + self._glyphCache.pop(glyphName, None) + async def _callGlyphMethod(self, glyphName, methodName, *args, **kwargs): typeCode, glyphID = self._rcjkGlyphInfo.get(glyphName, ("CG", None)) assert glyphID is not None @@ -303,6 +324,9 @@ async def _pollOnceForChanges(self): for glyphInfo in responseData.get("deleted_glifs", []): glyphName = glyphInfo["name"] if not glyphInfo["group_name"]: + if self._glyphTimeStamps.get(glyphName) is None: + # We made this change ourselves + continue logger.info(f"Found deleted glyph {glyphName}") glyphMapUpdates[glyphName] = None del self._glyphMap[glyphName] diff --git a/tests/test_font.py b/tests/test_font.py index 2f60ccb..7da8c32 100644 --- a/tests/test_font.py +++ b/tests/test_font.py @@ -1017,3 +1017,26 @@ async def test_writeMixClassicAndVariableComponents(writableTestFont): mainGlifPath = writableTestFont.path / "characterGlyph" / "b.glif" glifData = mainGlifPath.read_text() assert expectedWriteMixedComponentTestData == glifData.splitlines() + + +async def test_deleteGlyph(writableTestFont): + glyphName = "eight_00" + with contextlib.closing(writableTestFont): + glyphMap = await writableTestFont.getGlyphMap() + assert glyphName in glyphMap + glyphPaths = list(writableTestFont.path.glob(f"**/{glyphName}.glif")) + assert len(glyphPaths) == 3 + + await writableTestFont.deleteGlyph(glyphName) + + glyphMap = await writableTestFont.getGlyphMap() + assert glyphName not in glyphMap + glyphPaths = list(writableTestFont.path.glob(f"**/{glyphName}.glif")) + assert len(glyphPaths) == 0 + + +async def test_deleteGlyphRaisesKeyError(writableTestFont): + glyphName = "A.doesnotexist" + with contextlib.closing(writableTestFont): + with pytest.raises(KeyError, match="Glyph 'A.doesnotexist' does not exist"): + await writableTestFont.deleteGlyph(glyphName)