diff --git a/modules/globals.py b/modules/globals.py index 3d88931..2fbec38 100644 --- a/modules/globals.py +++ b/modules/globals.py @@ -6,10 +6,13 @@ from typing import List, Dict, Any 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")), - ("Video", ("*.mp4", "*.mkv")), -] +# 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 +# via the libwebp bundled with opencv-python. +IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".webp") +VIDEO_EXTENSIONS = (".mp4", ".mkv") # Face Mapping Data source_target_map: List[Dict[str, Any]] = [] # Stores detailed map for image/video processing diff --git a/modules/ui.py b/modules/ui.py index 4c89e3e..223e968 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -237,6 +237,18 @@ _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) +) + ")" +_VIDEO_FILE_FILTER = "Videos (" + " ".join( + f"*{ext}" for ext in modules.globals.VIDEO_EXTENSIONS +) + ")" + # ─── image utilities ───────────────────────────────────────────────────── @@ -734,7 +746,7 @@ class MainWindow(QMainWindow): path, _filter = QFileDialog.getOpenFileName( self, _("select an source image"), _RECENT_SOURCE_DIR or "", - "Images (*.png *.jpg *.jpeg *.gif *.bmp)", + _IMAGE_FILE_FILTER, ) if path and is_image(path): modules.globals.source_path = path @@ -755,7 +767,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_FILE_FILTER, ) if not path: return @@ -886,13 +898,13 @@ 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)", + _IMAGE_FILE_FILTER, ) elif is_video(modules.globals.target_path): 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 @@ -1334,7 +1346,7 @@ class MapperDialog(QDialog): path, _f = QFileDialog.getOpenFileName( self, _("select an source image"), _RECENT_SOURCE_DIR or "", - "Images (*.png *.jpg *.jpeg *.gif *.bmp)", + _IMAGE_FILE_FILTER, ) if not path: return @@ -1439,7 +1451,7 @@ class LiveMapperDialog(QDialog): path, _f = QFileDialog.getOpenFileName( self, _("select an source image"), _RECENT_SOURCE_DIR or "", - "Images (*.png *.jpg *.jpeg *.gif *.bmp)", + _IMAGE_FILE_FILTER, ) if not path: return diff --git a/modules/utilities.py b/modules/utilities.py index 953ef3c..799fd99 100644 --- a/modules/utilities.py +++ b/modules/utilities.py @@ -262,11 +262,16 @@ def clean_temp(target_path: str) -> None: def has_image_extension(image_path: str) -> bool: - return image_path.lower().endswith(("png", "jpg", "jpeg")) + # 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: 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