diff --git a/src/tagstudio/core/global_settings.py b/src/tagstudio/core/global_settings.py index 5dc25fd2..8cb6ab9c 100644 --- a/src/tagstudio/core/global_settings.py +++ b/src/tagstudio/core/global_settings.py @@ -24,6 +24,10 @@ DEFAULT_GLOBAL_SETTINGS_PATH = ( DEFAULT_THUMB_CACHE_SIZE = 500 # Number in MiB MIN_THUMB_CACHE_SIZE = 10 # Number in MiB +# See: https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#webp-saving +DEFAULT_CACHED_IMAGE_QUALITY = 80 +DEFAULT_CACHED_IMAGE_RES = 256 + class Theme(IntEnum): DARK = 0 @@ -56,6 +60,8 @@ class GlobalSettings(BaseModel): open_last_loaded_on_startup: bool = Field(default=True) generate_thumbs: bool = Field(default=True) thumb_cache_size: float = Field(default=DEFAULT_THUMB_CACHE_SIZE) + cached_thumb_quality: int = Field(default=DEFAULT_CACHED_IMAGE_QUALITY) + cached_thumb_resolution: int = Field(default=DEFAULT_CACHED_IMAGE_RES) autoplay: bool = Field(default=True) loop: bool = Field(default=True) show_filenames_in_grid: bool = Field(default=True) diff --git a/src/tagstudio/qt/cache_manager.py b/src/tagstudio/qt/cache_manager.py index 2c7c5f18..46e15cf4 100644 --- a/src/tagstudio/qt/cache_manager.py +++ b/src/tagstudio/qt/cache_manager.py @@ -12,7 +12,7 @@ import structlog from PIL import Image from tagstudio.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME -from tagstudio.core.global_settings import DEFAULT_THUMB_CACHE_SIZE +from tagstudio.core.global_settings import DEFAULT_CACHED_IMAGE_QUALITY, DEFAULT_THUMB_CACHE_SIZE logger = structlog.get_logger(__name__) @@ -24,19 +24,21 @@ class CacheFolder: class CacheManager: - MAX_FOLDER_SIZE = 10 # Number in MiB + MAX_FOLDER_SIZE = 10 # Absolute maximum size of a folder, number in MiB STAT_MULTIPLIER = 1_000_000 # Multiplier to apply to file stats (bytes) to get user units (MiB) def __init__( self, library_dir: Path, max_size: int | float = DEFAULT_THUMB_CACHE_SIZE, + img_quality: int = DEFAULT_CACHED_IMAGE_QUALITY, ): """A class for managing frontend caches, such as for file thumbnails. Args: library_dir(Path): The path of the folder containing the .TagStudio library folder. max_size: (int | float) The maximum size of the cache, in MiB. + img_quality: (int) The image quality value to save PIL images (0-100, default=80) """ self._lock = RLock() self.cache_path = library_dir / TS_FOLDER_NAME / THUMB_CACHE_NAME @@ -44,6 +46,9 @@ class CacheManager: math.floor(max_size * CacheManager.STAT_MULTIPLIER), math.floor(CacheManager.MAX_FOLDER_SIZE * CacheManager.STAT_MULTIPLIER), ) + self.img_quality = ( + img_quality if img_quality >= 0 and img_quality <= 100 else DEFAULT_CACHED_IMAGE_QUALITY + ) self.folders: list[CacheFolder] = [] self.current_size = 0 @@ -127,12 +132,20 @@ class CacheManager: with self._lock as _lock: cache_folder: CacheFolder = self._get_current_folder() file_path = cache_folder.path / file_name - image.save(file_path, mode=mode) + try: + image.save(file_path, mode=mode, quality=self.img_quality) - size = file_path.stat().st_size - cache_folder.size += size - self.current_size += size - self._cull_folders() + size = file_path.stat().st_size + cache_folder.size += size + self.current_size += size + self._cull_folders() + except FileNotFoundError: + logger.warn( + "[CacheManager] Failed to save cached image, was the folder deleted on disk?", + folder=file_path, + ) + if not cache_folder.path.exists(): + self.folders.remove(cache_folder) def _create_folder(self) -> CacheFolder: with self._lock as _lock: diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index a6062b8b..f73b7309 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -1697,7 +1697,11 @@ class QtDriver(DriverMixin, QObject): open_status = LibraryStatus( success=False, library_path=path, message=type(e).__name__, msg_description=str(e) ) - self.cache_manager = CacheManager(path, max_size=self.settings.thumb_cache_size) + self.cache_manager = CacheManager( + path, + max_size=self.settings.thumb_cache_size, + img_quality=self.settings.cached_thumb_quality, + ) logger.info( f"[Config] Thumbnail Cache Size: {format_size(self.settings.thumb_cache_size)}", ) diff --git a/src/tagstudio/qt/widgets/thumb_renderer.py b/src/tagstudio/qt/widgets/thumb_renderer.py index cd0db2fb..5ad1154f 100644 --- a/src/tagstudio/qt/widgets/thumb_renderer.py +++ b/src/tagstudio/qt/widgets/thumb_renderer.py @@ -55,6 +55,7 @@ from tagstudio.core.constants import ( FONT_SAMPLE_TEXT, ) from tagstudio.core.exceptions import NoRendererError +from tagstudio.core.global_settings import DEFAULT_CACHED_IMAGE_RES from tagstudio.core.library.ignore import Ignore from tagstudio.core.media_types import MediaCategories, MediaType from tagstudio.core.palette import UI_COLORS, ColorType, UiColor, get_ui_color @@ -94,9 +95,7 @@ class ThumbRenderer(QObject): rm: ResourceManager = ResourceManager() updated = Signal(float, QPixmap, QSize, Path) updated_ratio = Signal(float) - - cached_img_res: int = 256 # TODO: Pull this from config - cached_img_ext: str = ".webp" # TODO: Pull this from config + cached_img_ext: str = ".webp" def __init__(self, driver: "QtDriver", library: "Library") -> None: """Initialize the class.""" @@ -104,6 +103,13 @@ class ThumbRenderer(QObject): self.driver = driver self.lib = library + settings_res = self.driver.settings.cached_thumb_resolution + self.cached_img_res = ( + settings_res + if settings_res >= 16 and settings_res <= 2048 + else DEFAULT_CACHED_IMAGE_RES + ) + # Cached thumbnail elements. # Key: Size + Pixel Ratio Tuple + Radius Scale # (Ex. (512, 512, 1.25, 4)) @@ -1404,7 +1410,7 @@ class ThumbRenderer(QObject): image = self._render( timestamp, filepath, - (ThumbRenderer.cached_img_res, ThumbRenderer.cached_img_res), + (self.cached_img_res, self.cached_img_res), 1, is_grid_thumb, save_to_file=file_name,