fix(webp): address review — drop broken GIF, robust ext check, centralize lists

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).
This commit is contained in:
Tym Rabchuk
2026-06-22 20:33:11 -04:00
parent 0b61ad5c0d
commit 9e1f0cc3a5
3 changed files with 27 additions and 8 deletions
+10 -2
View File
@@ -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
+14 -5
View File
@@ -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
+3 -1
View File
@@ -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: