This commit is contained in:
Ignacio Serantes
2026-04-19 17:39:01 +02:00
parent b5b70326b1
commit a824a01579
10 changed files with 118 additions and 43 deletions

View File

@@ -14,7 +14,7 @@ Classes:
MainWindow: The main application window containing the thumbnail grid and docks.
"""
__appname__ = "BagheeraView"
__version__ = "0.9.23"
__version__ = "0.9.24"
__author__ = "Ignacio Serantes"
__email__ = "kde@aynoa.net"
__license__ = "LGPL"

View File

@@ -29,7 +29,7 @@ if FORCE_X11:
# --- CONFIGURATION ---
PROG_NAME = "Bagheera Image Viewer"
PROG_ID = "bagheeraview"
PROG_VERSION = "0.9.23"
PROG_VERSION = "0.9.24"
PROG_AUTHOR = "Ignacio Serantes"
# --- CACHE SETTINGS ---
@@ -57,17 +57,21 @@ DISK_CACHE_MAX_BYTES = 10 * 1024 * 1024 * 1024
# --- PATHS ---
CONFIG_FILE = f"{PROG_ID}rc"
CONFIG_LOCATION = '.config/iserantes'
CONFIG_DIR = os.path.join(os.path.expanduser("~"), CONFIG_LOCATION, PROG_ID)
CONFIG_LOCATION = os.environ.get('XDG_CONFIG_HOME')
CONFIG_DIR = os.path.join(CONFIG_LOCATION, 'iserantes', PROG_ID)
CONFIG_PATH = os.path.join(CONFIG_DIR, CONFIG_FILE)
CACHE_PATH = os.path.join(CONFIG_DIR, "thumbnails")
APP_DATA_LOCATION = os.path.expanduser('~/.local/share')
APP_DATA_DIR = os.path.join(APP_DATA_LOCATION, 'iserantes', PROG_ID)
CACHE_PATH = os.path.join(APP_DATA_DIR, "thumbnails")
HISTORY_FILE = "history.json"
HISTORY_PATH = os.path.join(CONFIG_DIR, HISTORY_FILE)
LAYOUTS_DIR = os.path.join(CONFIG_DIR, "layouts") # Layouts saving directory
HISTORY_PATH = os.path.join(APP_DATA_DIR, HISTORY_FILE)
LAYOUTS_DIR = os.path.join(APP_DATA_DIR, "layouts") # Layouts saving directory
FAVORITES_FILE = "favorites.json"
FAVORITES_PATH = os.path.join(CONFIG_DIR, FAVORITES_FILE)
DUPLICATE_CACHE_PATH = os.path.join(CONFIG_DIR, "duplicates")
FAVORITES_PATH = os.path.join(APP_DATA_DIR, FAVORITES_FILE)
DUPLICATE_CACHE_PATH = os.path.join(APP_DATA_DIR, "duplicates")
DUPLICATE_HASH_DB_NAME = b"hashes"
DUPLICATE_EXCEPTIONS_DB_NAME = b"exceptions"
DUPLICATE_PENDING_DB_NAME = b"pending"
@@ -204,15 +208,15 @@ if importlib.util.find_spec("mediapipe") is not None:
pass
HAVE_FACE_RECOGNITION = importlib.util.find_spec("face_recognition") is not None
MEDIAPIPE_FACE_MODEL_PATH = os.path.join(CONFIG_DIR,
"blaze_face_short_range.tflite")
MEDIAPIPE_FACE_MODEL_PATH = os.path.join(
APP_DATA_DIR, "blaze_face_short_range.tflite")
MEDIAPIPE_FACE_MODEL_URL = (
"https://storage.googleapis.com/mediapipe-models/face_detector/"
"blaze_face_short_range/float16/1/blaze_face_short_range.tflite"
)
MEDIAPIPE_OBJECT_MODEL_PATH = os.path.join(CONFIG_DIR,
"efficientdet_lite0.tflite")
MEDIAPIPE_OBJECT_MODEL_PATH = os.path.join(
APP_DATA_DIR, "efficientdet_lite0.tflite")
MEDIAPIPE_OBJECT_MODEL_URL = (
"https://storage.googleapis.com/mediapipe-models/object_detector/"
"efficientdet_lite0/float16/1/efficientdet_lite0.tflite"
@@ -782,6 +786,7 @@ _UI_TEXTS = {
"RENAME_ERROR_EXISTS": "File '{}' already exists.",
"FILE_RENAMED": "File renamed to {}",
"ERROR_RENAME": "Could not rename file: {}",
"ERROR_JPEG_METADATA_LIMIT": "Metadata size limit exceeded for '{}'. This JPEG file has too much existing metadata (XMP) to save more.",
"MAIN_DOCK_TITLE": "",
"LAYOUTS_TAB": "Layouts",
"LAYOUTS_TABLE_HEADER": ["Name", "Last Modified"],
@@ -1333,6 +1338,7 @@ _UI_TEXTS = {
"RENAME_ERROR_EXISTS": "El archivo '{}' ya existe.",
"FILE_RENAMED": "Archivo renombrado a {}",
"ERROR_RENAME": "No se pudo renombrar el archivo: {}",
"ERROR_JPEG_METADATA_LIMIT": "Límite de metadatos excedido para '{}'. Este archivo JPEG ya tiene demasiados metadatos (XMP) para guardar más.",
"MAIN_DOCK_TITLE": "Panel principal",
"LAYOUTS_TAB": "Diseños",
"LAYOUTS_TABLE_HEADER": ["Nombre", "Última Modificación"],
@@ -1885,6 +1891,7 @@ _UI_TEXTS = {
"RENAME_ERROR_EXISTS": "O ficheiro '{}' xa existe.",
"FILE_RENAMED": "Ficheiro renomeado a {}",
"ERROR_RENAME": "Non se puido renomear o ficheiro: {}",
"ERROR_JPEG_METADATA_LIMIT": "Límite de metadatos excedido para '{}'. Este ficheiro JPEG xa ten demasiados metadatos (XMP) para gardar máis.",
"MAIN_DOCK_TITLE": "Panel principal",
"LAYOUTS_TAB": "Deseños",
"LAYOUTS_TABLE_HEADER": ["Nome", "Última Modificación"],

View File

@@ -345,7 +345,7 @@ class ImageController(QObject):
faces_to_save.append(face_copy)
XmpManager.save_faces(path, faces_to_save)
return XmpManager.save_faces(path, faces_to_save)
def add_face(self, name, x, y, w, h, region_type="Face"):
"""Adds a new face. The full tag path should be passed as 'name'."""
@@ -390,8 +390,8 @@ class ImageController(QObject):
self.metadata_changed.emit(current_path,
{'tags': new_tags_list,
'rating': self._current_rating})
except IOError as e:
print(f"Error setting tags for {current_path}: {e}")
except Exception:
raise
def set_rating(self, new_rating):
current_path = self.get_current_path()

View File

@@ -36,9 +36,9 @@ from PySide6.QtGui import QImage, QImageReader, QImageIOHandler, QIcon
from PySide6.QtWidgets import QApplication
from constants import (
APP_CONFIG, CACHE_PATH, CACHE_MAX_SIZE, CACHE_MAX_RAM_BYTES, CONFIG_DIR,
MIN_FREE_RAM_PERCENT, DISK_CACHE_MAX_BYTES, HAVE_BAGHEERASEARCH_LIB,
IMAGE_EXTENSIONS,
APP_CONFIG, CACHE_PATH, CACHE_MAX_SIZE, CACHE_MAX_RAM_BYTES,
APP_DATA_DIR, MIN_FREE_RAM_PERCENT, DISK_CACHE_MAX_BYTES,
HAVE_BAGHEERASEARCH_LIB, IMAGE_EXTENSIONS,
SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, THUMBNAIL_SIZES,
UITexts
)
@@ -543,7 +543,7 @@ class ThumbnailCache(QObject):
def lmdb_open(self):
# Initialize LMDB environment
cache_dir = Path(CONFIG_DIR)
cache_dir = Path(APP_DATA_DIR)
cache_dir.mkdir(parents=True, exist_ok=True)
try:

View File

@@ -239,7 +239,10 @@ class FastTagManager:
current_path = controller.get_current_path() if controller else None
if not current_path:
return
controller.toggle_tag(tag_name, is_checked)
try:
controller.toggle_tag(tag_name, is_checked)
except Exception as e:
QMessageBox.critical(self.viewer, UITexts.ERROR, str(e))
self.viewer.update_status_bar()
if self.main_win:
if is_checked:
@@ -2836,8 +2839,11 @@ class ImageViewer(QWidget):
self.main_win.face_names_history = updated_history
# Save changes and add new tag
self.controller.save_faces()
self.controller.toggle_tag(new_full_tag, True)
try:
self.controller.save_faces()
self.controller.toggle_tag(new_full_tag, True)
except Exception as e:
QMessageBox.critical(self, UITexts.ERROR, str(e))
if self.canvas:
self.canvas.update()
@@ -3137,7 +3143,10 @@ class ImageViewer(QWidget):
self.canvas.update()
if added_count > 0:
self.controller.save_faces()
try:
self.controller.save_faces()
except Exception as e:
QMessageBox.critical(self, UITexts.ERROR, str(e))
def run_pet_detection(self):
"""Runs pet detection on the current image."""
@@ -3196,7 +3205,10 @@ class ImageViewer(QWidget):
self.canvas.update()
if added_count > 0:
self.controller.save_faces()
try:
self.controller.save_faces()
except Exception as e:
QMessageBox.critical(self, UITexts.ERROR, str(e))
def run_body_detection(self):
"""Runs body detection on the current image."""
@@ -3257,7 +3269,10 @@ class ImageViewer(QWidget):
self.canvas.update()
if added_count > 0:
self.controller.save_faces()
try:
self.controller.save_faces()
except Exception as e:
QMessageBox.critical(self, UITexts.ERROR, str(e))
def toggle_filmstrip(self):
"""Shows or hides the filmstrip widget."""

View File

@@ -9,6 +9,7 @@ Classes:
"""
import os
import collections
import logging
from PySide6.QtDBus import QDBusConnection, QDBusMessage, QDBus
try:
import exiv2
@@ -21,6 +22,8 @@ except ImportError:
from utils import preserve_mtime
from constants import RATING_XATTR_NAME, XATTR_NAME
logger = logging.getLogger(__name__)
_app_modified_callback = None
MetadataResult = collections.namedtuple('MetadataResult', ['tags', 'rating'])
@@ -141,6 +144,31 @@ class MetadataManager:
iptc = image.iptcData()
xmp = image.xmpData()
# Remove keys that are no longer in the dictionary
containers = [
(exif, exiv2.ExifKey, "Exif."),
(iptc, exiv2.IptcKey, "Iptc."),
(xmp, exiv2.XmpKey, "Xmp.")
]
for container, key_class, prefix in containers:
keys_to_remove = []
for datum in container:
k = datum.key()
# Only consider keys belonging to this specific container
if k.startswith(prefix) and k not in metadata_dict:
keys_to_remove.append(k)
for key in keys_to_remove:
try:
x_key = key_class(key)
it = container.findKey(x_key)
if it != container.end():
container.erase(it)
except Exception as e:
print(f"Error removing metadata key {key}: {e}")
# Set or update values from the dictionary
for key, value in metadata_dict.items():
try:
if key.startswith("Exif."):
@@ -156,7 +184,13 @@ class MetadataManager:
notify_baloo(path)
mark_app_modified(path)
except Exception as e:
print(f"Error writing metadata for {path}: {e}")
error_msg = str(e)
if "kerTooLargeJpegSegment" in error_msg or "38" in error_msg:
msg = UITexts.ERROR_JPEG_METADATA_LIMIT.format(os.path.basename(path))
logger.error(msg)
raise IOError(msg) from e
logger.error(f"Error writing metadata for {path}: {e}")
raise
class XattrManager:

View File

@@ -139,8 +139,9 @@ class PropertiesDialog(QDialog):
meta_layout = QVBoxLayout(meta_widget)
self.meta_toolbar = QToolBar()
self._setup_table_toolbar(self.meta_toolbar, self.on_add_meta, self.on_delete_meta,
self.on_delete_all_meta, self.on_save_meta, self.on_cancel_meta)
self._setup_table_toolbar(
self.meta_toolbar, self.on_add_meta, self.on_delete_meta,
self.on_delete_all_meta, self.on_save_meta, self.on_cancel_meta)
meta_layout.addWidget(self.meta_toolbar)
self.table = QTableWidget()
@@ -172,8 +173,9 @@ class PropertiesDialog(QDialog):
exif_layout = QVBoxLayout(exif_widget)
self.exif_toolbar = QToolBar()
self._setup_table_toolbar(self.exif_toolbar, self.on_add_exif, self.on_delete_exif,
self.on_delete_all_exif, self.on_save_exif, self.on_cancel_exif)
self._setup_table_toolbar(
self.exif_toolbar, self.on_add_exif, self.on_delete_exif,
self.on_delete_all_exif, self.on_save_exif, self.on_cancel_exif)
exif_layout.addWidget(self.exif_toolbar)
self.exif_table = QTableWidget()
@@ -219,17 +221,21 @@ class PropertiesDialog(QDialog):
# Start background loading
self.reload_metadata()
def _setup_table_toolbar(self, toolbar, add_slot, del_slot, del_all_slot, save_slot, cancel_slot):
def _setup_table_toolbar(self, toolbar, add_slot, del_slot, del_all_slot, save_slot,
cancel_slot):
"""Helper to populate toolbars with buttons."""
toolbar.addAction(QIcon.fromTheme("list-add"), UITexts.CREATE, add_slot)
toolbar.addAction(QIcon.fromTheme("list-remove"), UITexts.DELETE, del_slot)
toolbar.addAction(QIcon.fromTheme("edit-clear-all"), UITexts.PROPERTIES_DELETE_ALL, del_all_slot)
toolbar.addAction(
QIcon.fromTheme("edit-clear-all"), UITexts.PROPERTIES_DELETE_ALL,
del_all_slot)
toolbar.addSeparator()
toolbar.addAction(QIcon.fromTheme("document-save"), UITexts.SAVE, save_slot)
toolbar.addAction(QIcon.fromTheme("edit-undo"), UITexts.CANCEL, cancel_slot)
def on_add_meta(self):
key, ok = QInputDialog.getText(self, UITexts.PROPERTIES_ADD_ATTR, UITexts.PROPERTIES_ADD_ATTR_NAME)
key, ok = QInputDialog.getText(
self, UITexts.PROPERTIES_ADD_ATTR, UITexts.PROPERTIES_ADD_ATTR_NAME)
if ok and key:
row = self.table.rowCount()
self.table.insertRow(row)
@@ -240,7 +246,8 @@ class PropertiesDialog(QDialog):
self.table.editItem(v_item)
def on_delete_meta(self):
rows = sorted(set(index.row() for index in self.table.selectedIndexes()), reverse=True)
rows = sorted(set(index.row() for index in self.table.selectedIndexes()),
reverse=True)
for row in rows:
self.table.removeRow(row)
@@ -261,13 +268,14 @@ class PropertiesDialog(QDialog):
XattrManager.set_attribute(self.path, k, v)
self.reload_metadata()
except Exception as e:
QMessageBox.warning(self, UITexts.ERROR, f"Error: {e}")
QMessageBox.critical(self, UITexts.ERROR, str(e))
def on_cancel_meta(self):
self.update_metadata_table(self.original_xattrs)
def on_add_exif(self):
key, ok = QInputDialog.getText(self, UITexts.PROPERTIES_ADD_ATTR, UITexts.PROPERTIES_ADD_ATTR_NAME)
key, ok = QInputDialog.getText(
self, UITexts.PROPERTIES_ADD_ATTR, UITexts.PROPERTIES_ADD_ATTR_NAME)
if ok and key:
row = self.exif_table.rowCount()
self.exif_table.insertRow(row)
@@ -278,7 +286,9 @@ class PropertiesDialog(QDialog):
self.exif_table.editItem(v_item)
def on_delete_exif(self):
rows = sorted(set(index.row() for index in self.exif_table.selectedIndexes()), reverse=True)
rows = sorted(
set(index.row() for index in self.exif_table.selectedIndexes()),
reverse=True)
for row in rows:
self.exif_table.removeRow(row)
@@ -295,7 +305,7 @@ class PropertiesDialog(QDialog):
MetadataManager.write_metadata(self.path, new_exif)
self.reload_metadata()
except Exception as e:
QMessageBox.warning(self, UITexts.ERROR, f"Error: {e}")
QMessageBox.critical(self, UITexts.ERROR, str(e))
def on_cancel_exif(self):
self.update_exif_table(self.original_exif)

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "bagheeraview"
version = "0.9.23"
version = "0.9.24"
authors = [
{ name = "Ignacio Serantes" }
]

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="bagheeraview",
version="0.9.23",
version="0.9.24",
author="Ignacio Serantes",
description="Bagheera Image Viewer - An image viewer for KDE with Baloo in mind",
long_description="A fast image viewer built with PySide6, featuring search and "

View File

@@ -17,13 +17,17 @@ Dependencies:
"""
import os
import re
import logging
from utils import preserve_mtime
from metadatamanager import notify_baloo, mark_app_modified
from constants import UITexts
try:
import exiv2
except ImportError:
exiv2 = None
logger = logging.getLogger(__name__)
class XmpManager:
"""
@@ -167,5 +171,10 @@ class XmpManager:
mark_app_modified(path)
return True
except Exception as e:
print(f"Error saving faces to XMP: {e}")
return False
error_msg = str(e)
if "kerTooLargeJpegSegment" in error_msg or "38" in error_msg:
msg = UITexts.ERROR_JPEG_METADATA_LIMIT.format(os.path.basename(path))
logger.error(msg)
raise IOError(msg) from e
logger.error(f"Error saving faces to XMP: {e}")
raise