From ed9432a612189162d4728b4ca674f553be3b2d2e Mon Sep 17 00:00:00 2001 From: YDLuo-1 <102409480+YDLuo-1@users.noreply.github.com> Date: Thu, 27 Aug 2026 09:51:42 +0800 Subject: [PATCH 1/2] fix: keep image deletion metadata consistent --- PPOCRLabel.py | 93 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 33 deletions(-) diff --git a/PPOCRLabel.py b/PPOCRLabel.py index 04f63e5..851bc76 100644 --- a/PPOCRLabel.py +++ b/PPOCRLabel.py @@ -138,6 +138,47 @@ logger = logging.getLogger("PPOCRLabel") +def moveFileToTrash(filePath): + if platform.system() == "Windows": + import ctypes + from ctypes import wintypes + + class SHFILEOPSTRUCTW(ctypes.Structure): + _fields_ = [ + ("hwnd", wintypes.HWND), + ("wFunc", wintypes.UINT), + ("pFrom", wintypes.LPCWSTR), + ("pTo", wintypes.LPCWSTR), + ("fFlags", ctypes.c_ushort), + ("fAnyOperationsAborted", wintypes.BOOL), + ("hNameMappings", ctypes.c_void_p), + ("lpszProgressTitle", wintypes.LPCWSTR), + ] + + operation = SHFILEOPSTRUCTW() + operation.wFunc = 3 # FO_DELETE + operation.pFrom = os.path.abspath(filePath) + "\0" + operation.fFlags = 0x0040 | 0x0010 | 0x0004 | 0x0400 + result = ctypes.windll.shell32.SHFileOperationW(ctypes.byref(operation)) + return result == 0 and not operation.fAnyOperationsAborted + + if platform.system() == "Linux": + return subprocess.call(["trash", filePath]) == 0 + + if platform.system() == "Darwin": + absPath = os.path.abspath(filePath).replace("\\", "\\\\").replace('"', '\\"') + cmd = [ + "osascript", + "-e", + 'tell app "Finder" to move {the POSIX file "' + absPath + '"} to trash', + ] + logger.debug("Executing command: %s", " ".join(cmd)) + with open(os.devnull, "w") as devnull: + return subprocess.call(cmd, stdout=devnull) == 0 + + return False + + __appname__ = "PPOCRLabel" LABEL_COLORMAP = label_colormap() @@ -2831,41 +2872,27 @@ def deleteImg(self): if deletePath is not None: deleteInfo = self.deleteImgDialog() if deleteInfo == QMessageBox.Yes: - if platform.system() == "Windows": - # from win32com import shell, shellcon - # shell.SHFileOperation((0, shellcon.FO_DELETE, deletePath, None, - # shellcon.FOF_SILENT | shellcon.FOF_ALLOWUNDO | shellcon.FOF_NOCONFIRMATION, - # None, None)) - os.remove(deletePath) - # linux - elif platform.system() == "Linux": - cmd = "trash " + deletePath - os.system(cmd) - # macOS - elif platform.system() == "Darwin": - import subprocess - - absPath = ( - os.path.abspath(deletePath) - .replace("\\", "\\\\") - .replace('"', '\\"') + imgidx = self.getImglabelidx(deletePath) + try: + deleteSucceeded = moveFileToTrash(deletePath) + except Exception as error: + logger.exception("Failed to move image to trash: %s", error) + deleteSucceeded = False + + if not deleteSucceeded: + QMessageBox.warning( + self, + "Attention", + "The image could not be moved to the recycle bin.", ) - cmd = [ - "osascript", - "-e", - 'tell app "Finder" to move {the POSIX file "' - + absPath - + '"} to trash', - ] - logger.debug("Executing command: %s", " ".join(cmd)) - subprocess.call(cmd, stdout=open(os.devnull, "w")) - - if self.filePath in self.fileStatedict.keys(): - self.fileStatedict.pop(self.filePath) - imgidx = self.getImglabelidx(self.filePath) - if imgidx in self.PPlabel.keys(): - self.PPlabel.pop(imgidx) + return + self.fileStatedict.pop(imgidx, None) + self.PPlabel.pop(imgidx, None) + self.Cachelabel.pop(imgidx, None) + self.saveFilestate() + self.savePPlabel(mode="Auto") + self.saveCacheLabel() self.importDirImages(self.lastOpenDir, isDelete=True) def deleteImgDialog(self): From ec9e8a8ffaa4f2034ec7fc00446d3f1ec1b80f7d Mon Sep 17 00:00:00 2001 From: YDLuo-1 <102409480+YDLuo-1@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:27:44 +0800 Subject: [PATCH 2/2] fix: keep Windows trash path buffer alive --- PPOCRLabel.py | 4 +++- tests/test_windows_trash.py | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/test_windows_trash.py diff --git a/PPOCRLabel.py b/PPOCRLabel.py index 851bc76..48c8377 100644 --- a/PPOCRLabel.py +++ b/PPOCRLabel.py @@ -157,7 +157,9 @@ class SHFILEOPSTRUCTW(ctypes.Structure): operation = SHFILEOPSTRUCTW() operation.wFunc = 3 # FO_DELETE - operation.pFrom = os.path.abspath(filePath) + "\0" + fromPath = os.path.abspath(filePath) + "\0\0" + fromBuffer = ctypes.create_unicode_buffer(fromPath) + operation.pFrom = ctypes.cast(fromBuffer, wintypes.LPCWSTR) operation.fFlags = 0x0040 | 0x0010 | 0x0004 | 0x0400 result = ctypes.windll.shell32.SHFileOperationW(ctypes.byref(operation)) return result == 0 and not operation.fAnyOperationsAborted diff --git a/tests/test_windows_trash.py b/tests/test_windows_trash.py new file mode 100644 index 0000000..0d6c78b --- /dev/null +++ b/tests/test_windows_trash.py @@ -0,0 +1,45 @@ +import ast +import os +import platform +import subprocess +import tempfile +import unittest +from pathlib import Path + + +sourcePath = Path(__file__).parents[1] / "PPOCRLabel.py" +tree = ast.parse(sourcePath.read_text(encoding="utf-8")) +moveFunction = next( + node + for node in tree.body + if isinstance(node, ast.FunctionDef) and node.name == "moveFileToTrash" +) +namespace = { + "os": os, + "platform": platform, + "subprocess": subprocess, + "logger": type("Logger", (), {"debug": staticmethod(lambda *_args: None)})(), +} +exec( + compile(ast.Module([moveFunction], type_ignores=[]), str(sourcePath), "exec"), + namespace, +) + + +class WindowsTrashTest(unittest.TestCase): + @unittest.skipUnless(platform.system() == "Windows", "Windows-only check") + def test_explicit_path_buffer(self): + with tempfile.NamedTemporaryFile( + prefix="ppocrlabel_trash_", delete=False + ) as file: + tempPath = Path(file.name) + + try: + self.assertTrue(namespace["moveFileToTrash"](str(tempPath))) + self.assertFalse(tempPath.exists()) + finally: + tempPath.unlink(missing_ok=True) + + +if __name__ == "__main__": + unittest.main()