From 695cc88d76f8a69eef9a1a564ba7afd04ba9e079 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 14 Jun 2023 00:05:38 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Made=20OnlineCoverManager=20more?= =?UTF-8?q?=20general?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Does compositing of image with a blurred background - Stretches the original image if it's not too much - Handles images that are too wide and images that are too tall - Removed ItchCoverManager --- src/importer/sources/itch_source.py | 2 +- src/main.py | 6 +- src/store/managers/itch_cover_manager.py | 66 ---------------- src/store/managers/online_cover_manager.py | 89 ++++++++++++++++++++-- src/store/managers/sgdb_manager.py | 4 +- 5 files changed, 89 insertions(+), 78 deletions(-) delete mode 100644 src/store/managers/itch_cover_manager.py diff --git a/src/importer/sources/itch_source.py b/src/importer/sources/itch_source.py index e93ad17..43e546c 100644 --- a/src/importer/sources/itch_source.py +++ b/src/importer/sources/itch_source.py @@ -50,7 +50,7 @@ class ItchSourceIterator(SourceIterator): "game_id": self.source.game_id_format.format(game_id=row[0]), "executable": self.source.executable_format.format(cave_id=row[4]), } - additional_data = {"itch_cover_url": row[2], "itch_still_cover_url": row[3]} + additional_data = {"online_cover_url": row[3] or row[2]} game = Game(values, allow_side_effects=False) yield (game, additional_data) diff --git a/src/main.py b/src/main.py index 588d8c1..b59176c 100644 --- a/src/main.py +++ b/src/main.py @@ -37,12 +37,12 @@ from src.importer.sources.itch_source import ItchSource from src.importer.sources.legendary_source import LegendarySource from src.importer.sources.lutris_source import LutrisSource from src.importer.sources.steam_source import SteamSource -from src.logging.setup import setup_logging, log_system_info +from src.logging.setup import log_system_info, setup_logging from src.preferences import PreferencesWindow from src.store.managers.display_manager import DisplayManager from src.store.managers.file_manager import FileManager -from src.store.managers.itch_cover_manager import ItchCoverManager from src.store.managers.local_cover_manager import LocalCoverManager +from src.store.managers.online_cover_manager import OnlineCoverManager from src.store.managers.sgdb_manager import SGDBManager from src.store.managers.steam_api_manager import SteamAPIManager from src.store.store import Store @@ -89,7 +89,7 @@ class CartridgesApplication(Adw.Application): # Add rest of the managers for game imports shared.store.add_manager(LocalCoverManager()) shared.store.add_manager(SteamAPIManager()) - shared.store.add_manager(ItchCoverManager()) + shared.store.add_manager(OnlineCoverManager()) shared.store.add_manager(SGDBManager()) shared.store.add_manager(FileManager()) diff --git a/src/store/managers/itch_cover_manager.py b/src/store/managers/itch_cover_manager.py deleted file mode 100644 index 3d96b0e..0000000 --- a/src/store/managers/itch_cover_manager.py +++ /dev/null @@ -1,66 +0,0 @@ -from pathlib import Path - -import requests -from gi.repository import GdkPixbuf, Gio -from requests.exceptions import HTTPError, SSLError - -from src import shared # pylint: disable=no-name-in-module -from src.game import Game -from src.store.managers.local_cover_manager import LocalCoverManager -from src.store.managers.manager import Manager -from src.utils.save_cover import resize_cover, save_cover - - -# TODO Remove by generalizing OnlineCoverManager - - -class ItchCoverManager(Manager): - """Manager in charge of downloading the game's cover from itch.io""" - - run_after = (LocalCoverManager,) - retryable_on = (HTTPError, SSLError) - - def manager_logic(self, game: Game, additional_data: dict) -> None: - # Get the first matching cover url - base_cover_url: str = additional_data.get("itch_cover_url", None) - still_cover_url: str = additional_data.get("itch_still_cover_url", None) - cover_url = still_cover_url or base_cover_url - if not cover_url: - return - - # Download cover - tmp_file = Gio.File.new_tmp()[0] - with requests.get(cover_url, timeout=5) as cover: - cover.raise_for_status() - Path(tmp_file.get_path()).write_bytes(cover.content) - - # Create background blur - game_cover = GdkPixbuf.Pixbuf.new_from_stream_at_scale( - tmp_file.read(), 2, 2, False - ).scale_simple(*shared.image_size, GdkPixbuf.InterpType.BILINEAR) - - # Resize square image - itch_pixbuf = GdkPixbuf.Pixbuf.new_from_stream(tmp_file.read()) - itch_pixbuf = itch_pixbuf.scale_simple( - shared.image_size[0], - itch_pixbuf.get_height() * (shared.image_size[0] / itch_pixbuf.get_width()), - GdkPixbuf.InterpType.BILINEAR, - ) - - # Composite - itch_pixbuf.composite( - game_cover, - 0, - (shared.image_size[1] - itch_pixbuf.get_height()) / 2, - itch_pixbuf.get_width(), - itch_pixbuf.get_height(), - 0, - (shared.image_size[1] - itch_pixbuf.get_height()) / 2, - 1.0, - 1.0, - GdkPixbuf.InterpType.BILINEAR, - 255, - ) - - # Resize and save the cover - save_cover(game.game_id, resize_cover(pixbuf=game_cover)) diff --git a/src/store/managers/online_cover_manager.py b/src/store/managers/online_cover_manager.py index e9b8411..736e6c6 100644 --- a/src/store/managers/online_cover_manager.py +++ b/src/store/managers/online_cover_manager.py @@ -1,9 +1,12 @@ +import logging from pathlib import Path import requests -from gi.repository import Gio +from gi.repository import Gio, GdkPixbuf from requests.exceptions import HTTPError, SSLError +from PIL import Image +from src import shared from src.game import Game from src.store.managers.local_cover_manager import LocalCoverManager from src.store.managers.manager import Manager @@ -16,15 +19,89 @@ class OnlineCoverManager(Manager): run_after = (LocalCoverManager,) retryable_on = (HTTPError, SSLError) + def save_composited_cover( + self, + game: Game, + image_file: Gio.File, + original_width: int, + original_height: int, + target_width: int, + target_height: int, + ) -> None: + """Save the image composited with a background blur to fit the cover size""" + + logging.debug( + "Compositing image for %s (%s) %dx%d -> %dx%d", + game.name, + game.game_id, + original_width, + original_height, + target_width, + target_height, + ) + + # Load game image + image = GdkPixbuf.Pixbuf.new_from_stream(image_file.read()) + + # Create background blur of the size of the cover + cover = image.scale_simple(2, 2, GdkPixbuf.InterpType.BILINEAR).scale_simple( + target_width, target_height, GdkPixbuf.InterpType.BILINEAR + ) + + # Center the image above the blurred background + scale = min(target_width / original_width, target_height / original_height) + left_padding = (target_width - original_width * scale) / 2 + top_padding = (target_height - original_height * scale) / 2 + image.composite( + cover, + # Top left of overwritten area on the destination + left_padding, + top_padding, + # Size of the overwritten area on the destination + original_width * scale, + original_height * scale, + # Offset + left_padding, + top_padding, + # Scale to apply to the resized image + scale, + scale, + # Compositing stuff + GdkPixbuf.InterpType.BILINEAR, + 255, + ) + + # Resize and save the cover + save_cover(game.game_id, resize_cover(pixbuf=cover)) + def manager_logic(self, game: Game, additional_data: dict) -> None: # Ensure that we have a cover to download - cover_url = additional_data.get("online_cover_url", None) + cover_url = additional_data.get("online_cover_url") if not cover_url: return + # Download cover - tmp_file = Gio.File.new_tmp()[0] + image_file = Gio.File.new_tmp()[0] + image_path = Path(image_file.get_path()) with requests.get(cover_url, timeout=5) as cover: cover.raise_for_status() - Path(tmp_file.get_path()).write_bytes(cover.content) - # Resize and save - save_cover(game.game_id, resize_cover(tmp_file.get_path())) + image_path.write_bytes(cover.content) + + # Get image size + cover_width, cover_height = shared.image_size + with Image.open(image_path) as pil_image: + width, height = pil_image.size + + # Composite the image if its aspect ratio differs too much + # (allow the side that is smaller to be stretched by a small percentage) + max_diff_proportion = 0.12 + scale = min(cover_width / width, cover_height / height) + width_diff = (cover_width - (width * scale)) / cover_width + height_diff = (cover_height - (height * scale)) / cover_height + diff = width_diff + height_diff + if diff < max_diff_proportion: + save_cover(game.game_id, resize_cover(image_path)) + else: + self.save_composited_cover( + game, image_file, width, height, cover_width, cover_height + ) diff --git a/src/store/managers/sgdb_manager.py b/src/store/managers/sgdb_manager.py index 5844204..a3a5811 100644 --- a/src/store/managers/sgdb_manager.py +++ b/src/store/managers/sgdb_manager.py @@ -4,7 +4,7 @@ from requests.exceptions import HTTPError, SSLError from src.game import Game from src.store.managers.async_manager import AsyncManager -from src.store.managers.itch_cover_manager import ItchCoverManager +from src.store.managers.online_cover_manager import OnlineCoverManager from src.store.managers.local_cover_manager import LocalCoverManager from src.store.managers.steam_api_manager import SteamAPIManager from src.utils.steamgriddb import SGDBAuthError, SGDBHelper @@ -13,7 +13,7 @@ from src.utils.steamgriddb import SGDBAuthError, SGDBHelper class SGDBManager(AsyncManager): """Manager in charge of downloading a game's cover from steamgriddb""" - run_after = (SteamAPIManager, LocalCoverManager, ItchCoverManager) + run_after = (SteamAPIManager, LocalCoverManager, OnlineCoverManager) retryable_on = (HTTPError, SSLError, ConnectionError, JSONDecodeError) def manager_logic(self, game: Game, _additional_data: dict) -> None: