From 0b61ad5c0d2081cf04f482de4b9ff81b0cb16928 Mon Sep 17 00:00:00 2001 From: Tym Rabchuk Date: Mon, 18 May 2026 16:51:59 -0400 Subject: [PATCH 1/3] feat: webp source image support Ported from April 2026 Fork: - has_image_extension() now recognizes .webp/.gif/.bmp - is_image() checks extension before mimetypes (Windows mimetypes doesn't always register webp) - File dialog filter includes *.webp --- modules/globals.py | 2 +- modules/ui.py | 10 +++++----- modules/utilities.py | 5 ++++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/modules/globals.py b/modules/globals.py index 3d88931..f3a1944 100644 --- a/modules/globals.py +++ b/modules/globals.py @@ -7,7 +7,7 @@ ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) WORKFLOW_DIR = os.path.join(ROOT_DIR, "workflow") file_types = [ - ("Image", ("*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp")), + ("Image", ("*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp", "*.webp")), ("Video", ("*.mp4", "*.mkv")), ] diff --git a/modules/ui.py b/modules/ui.py index cade5f5..53fecbd 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -733,7 +733,7 @@ class MainWindow(QMainWindow): path, _filter = QFileDialog.getOpenFileName( self, _("select an source image"), _RECENT_SOURCE_DIR or "", - "Images (*.png *.jpg *.jpeg *.gif *.bmp)", + "Images (*.png *.jpg *.jpeg *.gif *.bmp *.webp)", ) if path and is_image(path): modules.globals.source_path = path @@ -754,7 +754,7 @@ class MainWindow(QMainWindow): path, _filter = QFileDialog.getOpenFileName( self, _("select an target image or video"), _RECENT_TARGET_DIR or "", - "Media (*.png *.jpg *.jpeg *.gif *.bmp *.mp4 *.mkv)", + "Media (*.png *.jpg *.jpeg *.gif *.bmp *.webp *.mp4 *.mkv)", ) if not path: return @@ -885,7 +885,7 @@ class MainWindow(QMainWindow): path, _f = QFileDialog.getSaveFileName( self, _("save image output file"), os.path.join(_RECENT_OUTPUT_DIR or "", "output.png"), - "Images (*.png *.jpg *.jpeg *.bmp)", + "Images (*.png *.jpg *.jpeg *.gif *.bmp *.webp)", ) elif is_video(modules.globals.target_path): path, _f = QFileDialog.getSaveFileName( @@ -1333,7 +1333,7 @@ class MapperDialog(QDialog): path, _f = QFileDialog.getOpenFileName( self, _("select an source image"), _RECENT_SOURCE_DIR or "", - "Images (*.png *.jpg *.jpeg *.gif *.bmp)", + "Images (*.png *.jpg *.jpeg *.gif *.bmp *.webp)", ) if not path: return @@ -1438,7 +1438,7 @@ class LiveMapperDialog(QDialog): path, _f = QFileDialog.getOpenFileName( self, _("select an source image"), _RECENT_SOURCE_DIR or "", - "Images (*.png *.jpg *.jpeg *.gif *.bmp)", + "Images (*.png *.jpg *.jpeg *.gif *.bmp *.webp)", ) if not path: return diff --git a/modules/utilities.py b/modules/utilities.py index 953ef3c..7ad5df9 100644 --- a/modules/utilities.py +++ b/modules/utilities.py @@ -262,11 +262,14 @@ def clean_temp(target_path: str) -> None: def has_image_extension(image_path: str) -> bool: - return image_path.lower().endswith(("png", "jpg", "jpeg")) + return image_path.lower().endswith(("png", "jpg", "jpeg", "gif", "bmp", "webp")) def is_image(image_path: str) -> bool: if image_path and os.path.isfile(image_path): + # Extension check first — Windows mimetypes doesn't always register webp + if has_image_extension(image_path): + return True mimetype, _ = mimetypes.guess_type(image_path) return bool(mimetype and mimetype.startswith("image/")) return False From 9e1f0cc3a5a480e1683178805eface7c2e5b69f2 Mon Sep 17 00:00:00 2001 From: Tym Rabchuk Date: Mon, 22 Jun 2026 20:33:11 -0400 Subject: [PATCH 2/3] =?UTF-8?q?fix(webp):=20address=20review=20=E2=80=94?= =?UTF-8?q?=20drop=20broken=20GIF,=20robust=20ext=20check,=20centralize=20?= =?UTF-8?q?lists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback on #1831: - Remove *.gif from the save/output dialog filter (PR had added it there). Verified empirically that cv2.imread/imwrite cannot decode OR encode GIF on OpenCV 4.10 *or* 4.11 (write raises, read returns None), so GIF silently failed on both ends — dropped from every dialog and from has_image_extension. - has_image_extension now uses os.path.splitext so only the true extension counts ('photo.png.bak' / 'clip.webp.mp4' are no longer treated as images). - Centralize the supported-extension set in modules.globals (IMAGE_EXTENSIONS / VIDEO_EXTENSIONS); file_types, all QFileDialog filters and has_image_extension now derive from it instead of hand-copied lists that had already drifted. WEBP itself is unchanged and works (libwebp ships with opencv-python). --- modules/globals.py | 12 ++++++++++-- modules/ui.py | 19 ++++++++++++++----- modules/utilities.py | 4 +++- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/modules/globals.py b/modules/globals.py index f3a1944..0177f8e 100644 --- a/modules/globals.py +++ b/modules/globals.py @@ -6,9 +6,17 @@ from typing import List, Dict, Any ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) WORKFLOW_DIR = os.path.join(ROOT_DIR, "workflow") +# Canonical media extensions, defined once so file dialogs, file_types and +# has_image_extension never drift. GIF is intentionally excluded: OpenCV's +# cv2.imread/imwrite (the only image I/O this app uses) cannot decode or +# encode GIF on 4.10 or 4.11, so offering it would silently fail. WEBP works +# via the libwebp bundled with opencv-python. +IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".webp") +VIDEO_EXTENSIONS = (".mp4", ".mkv") + file_types = [ - ("Image", ("*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp", "*.webp")), - ("Video", ("*.mp4", "*.mkv")), + ("Image", tuple(f"*{ext}" for ext in IMAGE_EXTENSIONS)), + ("Video", tuple(f"*{ext}" for ext in VIDEO_EXTENSIONS)), ] # Face Mapping Data diff --git a/modules/ui.py b/modules/ui.py index 53fecbd..0cf37ad 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -236,6 +236,15 @@ _RECENT_SOURCE_DIR: Optional[str] = None _RECENT_TARGET_DIR: Optional[str] = None _RECENT_OUTPUT_DIR: Optional[str] = None +# QFileDialog filter strings, built from the canonical extension sets in +# globals so every dialog stays in sync (no hand-copied lists to drift). +_IMAGE_FILE_FILTER = "Images (" + " ".join( + f"*{ext}" for ext in modules.globals.IMAGE_EXTENSIONS +) + ")" +_MEDIA_FILE_FILTER = "Media (" + " ".join( + f"*{ext}" for ext in (*modules.globals.IMAGE_EXTENSIONS, *modules.globals.VIDEO_EXTENSIONS) +) + ")" + # ─── image utilities ───────────────────────────────────────────────────── @@ -733,7 +742,7 @@ class MainWindow(QMainWindow): path, _filter = QFileDialog.getOpenFileName( self, _("select an source image"), _RECENT_SOURCE_DIR or "", - "Images (*.png *.jpg *.jpeg *.gif *.bmp *.webp)", + _IMAGE_FILE_FILTER, ) if path and is_image(path): modules.globals.source_path = path @@ -754,7 +763,7 @@ class MainWindow(QMainWindow): path, _filter = QFileDialog.getOpenFileName( self, _("select an target image or video"), _RECENT_TARGET_DIR or "", - "Media (*.png *.jpg *.jpeg *.gif *.bmp *.webp *.mp4 *.mkv)", + _MEDIA_FILE_FILTER, ) if not path: return @@ -885,7 +894,7 @@ class MainWindow(QMainWindow): path, _f = QFileDialog.getSaveFileName( self, _("save image output file"), os.path.join(_RECENT_OUTPUT_DIR or "", "output.png"), - "Images (*.png *.jpg *.jpeg *.gif *.bmp *.webp)", + _IMAGE_FILE_FILTER, ) elif is_video(modules.globals.target_path): path, _f = QFileDialog.getSaveFileName( @@ -1333,7 +1342,7 @@ class MapperDialog(QDialog): path, _f = QFileDialog.getOpenFileName( self, _("select an source image"), _RECENT_SOURCE_DIR or "", - "Images (*.png *.jpg *.jpeg *.gif *.bmp *.webp)", + _IMAGE_FILE_FILTER, ) if not path: return @@ -1438,7 +1447,7 @@ class LiveMapperDialog(QDialog): path, _f = QFileDialog.getOpenFileName( self, _("select an source image"), _RECENT_SOURCE_DIR or "", - "Images (*.png *.jpg *.jpeg *.gif *.bmp *.webp)", + _IMAGE_FILE_FILTER, ) if not path: return diff --git a/modules/utilities.py b/modules/utilities.py index 7ad5df9..799fd99 100644 --- a/modules/utilities.py +++ b/modules/utilities.py @@ -262,7 +262,9 @@ def clean_temp(target_path: str) -> None: def has_image_extension(image_path: str) -> bool: - return image_path.lower().endswith(("png", "jpg", "jpeg", "gif", "bmp", "webp")) + # splitext so only the real extension counts (e.g. "photo.png.bak" is not + # an image); the set is centralized in globals to stay in sync with dialogs. + return os.path.splitext(image_path)[1].lower() in modules.globals.IMAGE_EXTENSIONS def is_image(image_path: str) -> bool: From 47dffeb30750cd030d5b0a4525dc916c34e8fc26 Mon Sep 17 00:00:00 2001 From: Tym Rabchuk Date: Tue, 23 Jun 2026 19:30:07 -0400 Subject: [PATCH 3/3] fix(webp): finish extension centralization from pre-submission review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - build the video save-dialog filter from VIDEO_EXTENSIONS (_VIDEO_FILE_FILTER) instead of a hardcoded "Videos (*.mp4 *.mkv)" — the last filter that still drifted from the canonical set - remove the now-dead file_types list (unused in both the fork and upstream; the PySide6 dialogs use the QFileDialog filter strings) and drop it from the centralization comment --- modules/globals.py | 7 +------ modules/ui.py | 5 ++++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/globals.py b/modules/globals.py index 0177f8e..2fbec38 100644 --- a/modules/globals.py +++ b/modules/globals.py @@ -6,7 +6,7 @@ from typing import List, Dict, Any ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) WORKFLOW_DIR = os.path.join(ROOT_DIR, "workflow") -# Canonical media extensions, defined once so file dialogs, file_types and +# Canonical media extensions, defined once so the file dialogs and # has_image_extension never drift. GIF is intentionally excluded: OpenCV's # cv2.imread/imwrite (the only image I/O this app uses) cannot decode or # encode GIF on 4.10 or 4.11, so offering it would silently fail. WEBP works @@ -14,11 +14,6 @@ WORKFLOW_DIR = os.path.join(ROOT_DIR, "workflow") IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".webp") VIDEO_EXTENSIONS = (".mp4", ".mkv") -file_types = [ - ("Image", tuple(f"*{ext}" for ext in IMAGE_EXTENSIONS)), - ("Video", tuple(f"*{ext}" for ext in VIDEO_EXTENSIONS)), -] - # Face Mapping Data source_target_map: List[Dict[str, Any]] = [] # Stores detailed map for image/video processing simple_map: Dict[str, Any] = {} # Stores simplified map (embeddings/faces) for live/simple mode diff --git a/modules/ui.py b/modules/ui.py index 0cf37ad..d19993e 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -244,6 +244,9 @@ _IMAGE_FILE_FILTER = "Images (" + " ".join( _MEDIA_FILE_FILTER = "Media (" + " ".join( f"*{ext}" for ext in (*modules.globals.IMAGE_EXTENSIONS, *modules.globals.VIDEO_EXTENSIONS) ) + ")" +_VIDEO_FILE_FILTER = "Videos (" + " ".join( + f"*{ext}" for ext in modules.globals.VIDEO_EXTENSIONS +) + ")" # ─── image utilities ───────────────────────────────────────────────────── @@ -900,7 +903,7 @@ class MainWindow(QMainWindow): path, _f = QFileDialog.getSaveFileName( self, _("save video output file"), os.path.join(_RECENT_OUTPUT_DIR or "", "output.mp4"), - "Videos (*.mp4 *.mkv)", + _VIDEO_FILE_FILTER, ) else: return