From 0f6a989142003e9a7e483ee2e56b9cf3c37fca72 Mon Sep 17 00:00:00 2001 From: kramo <93832451+kra-mo@users.noreply.github.com> Date: Tue, 11 Apr 2023 14:49:18 +0200 Subject: [PATCH] Reimplement animated covers with Pillow --- .github/workflows/windows.yml | 2 +- hu.kramo.Cartridges.json | 14 ++++++++ src/game.py | 20 ++++++----- src/game_cover.py | 54 ++++++++++++++++++++++++++---- src/preferences.py | 18 +++++----- src/utils/create_details_window.py | 48 +++++++++++++------------- src/utils/save_cover.py | 48 ++++++++++++++++++++++---- src/window.py | 5 +-- 8 files changed, 152 insertions(+), 57 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index f8c9bd5..636492c 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -15,7 +15,7 @@ jobs: with: msystem: UCRT64 update: true - install: mingw-w64-ucrt-x86_64-gtk4 mingw-w64-ucrt-x86_64-libadwaita mingw-w64-ucrt-x86_64-python-gobject mingw-w64-ucrt-x86_64-python-yaml mingw-w64-ucrt-x86_64-python-requests mingw-w64-ucrt-x86_64-desktop-file-utils mingw-w64-ucrt-x86_64-ca-certificates mingw-w64-ucrt-x86_64-meson git + install: mingw-w64-ucrt-x86_64-gtk4 mingw-w64-ucrt-x86_64-libadwaita mingw-w64-ucrt-x86_64-python-gobject mingw-w64-ucrt-x86_64-python-yaml mingw-w64-ucrt-x86_64-python-requests mingw-w64-ucrt-x86_64-python-pillow mingw-w64-ucrt-x86_64-desktop-file-utils mingw-w64-ucrt-x86_64-ca-certificates mingw-w64-ucrt-x86_64-meson git - name: Compile shell: msys2 {0} run: | diff --git a/hu.kramo.Cartridges.json b/hu.kramo.Cartridges.json index 2649c11..7474def 100644 --- a/hu.kramo.Cartridges.json +++ b/hu.kramo.Cartridges.json @@ -87,6 +87,20 @@ "sha256": "aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42" } ] + }, + { + "name": "python3-pillow", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pillow\" --no-build-isolation" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/00/d5/4903f310765e0ff2b8e91ffe55031ac6af77d982f0156061e20a4d1a8b2d/Pillow-9.5.0.tar.gz", + "sha256": "bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1" + } + ] } ] }, diff --git a/src/game.py b/src/game.py index 00c4156..66f9c68 100644 --- a/src/game.py +++ b/src/game.py @@ -58,8 +58,9 @@ class game(Gtk.Box): # pylint: disable=invalid-name self.removed = "removed" in data self.blacklisted = "blacklisted" in data - self.game_cover = GameCover(self.cover, self.get_cover()) + self.game_cover = GameCover(self.cover, path=self.get_cover()) self.pixbuf = self.game_cover.get_pixbuf() + self.animation = self.game_cover.get_animation() self.title.set_label(self.name) @@ -118,11 +119,16 @@ class game(Gtk.Box): # pylint: disable=invalid-name save_game(self.parent_widget, data) def get_cover(self): - # If the cover is already in memory, return - if self.game_id in self.parent_widget.pixbufs: - return self.parent_widget.pixbufs[self.game_id] + animated_cover_path = ( + self.parent_widget.data_dir + / "cartridges" + / "animated_covers" + / f"{self.game_id}.gif" + ) + + if animated_cover_path.is_file(): + return animated_cover_path - # Create a new pixbuf cover_path = ( self.parent_widget.data_dir / "cartridges" @@ -131,9 +137,7 @@ class game(Gtk.Box): # pylint: disable=invalid-name ) if cover_path.is_file(): - pixbuf = GdkPixbuf.Pixbuf.new_from_file(str(cover_path)) - self.parent_widget.pixbufs[self.game_id] = pixbuf - return pixbuf + return cover_path return None diff --git a/src/game_cover.py b/src/game_cover.py index 6033e11..b0b1079 100644 --- a/src/game_cover.py +++ b/src/game_cover.py @@ -17,28 +17,68 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from gi.repository import GdkPixbuf +from gi.repository import GdkPixbuf, GLib + +from .save_cover import resize_animation class GameCover: + pixbuf = None + path = None + animation = None + placeholder_pixbuf = GdkPixbuf.Pixbuf.new_from_resource_at_scale( "/hu/kramo/Cartridges/library_placeholder.svg", 400, 600, False ) def __init__(self, picture, pixbuf=None, path=None): self.picture = picture - self.pixbuf = pixbuf + self.new_pixbuf(pixbuf, path) + + def new_pixbuf(self, pixbuf=None, path=None): + self.animation = None + self.pixbuf = None + self.path = None + + if pixbuf: + self.pixbuf = pixbuf if path: - self.pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) + if str(path).rsplit(".", maxsplit=1)[-1] == "gif": + self.path = resize_animation(path) + self.animation = GdkPixbuf.PixbufAnimation.new_from_file(str(self.path)) + self.anim_iter = self.animation.get_iter() + self.update_animation(self.animation) + else: + self.path = path + self.pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( + str(path), 200, 300, False + ) if not self.pixbuf: self.pixbuf = self.placeholder_pixbuf - self.update_pixbuf() + if not self.animation: + self.set_pixbuf(self.pixbuf) def get_pixbuf(self): - return self.pixbuf + return self.animation.get_static_image() if self.animation else self.pixbuf - def update_pixbuf(self): - self.picture.set_pixbuf(self.pixbuf) + def get_animation(self): + return self.path if self.animation else None + + def set_pixbuf(self, pixbuf): + self.picture.set_pixbuf(pixbuf) + + def update_animation(self, animation): + if self.animation == animation: + self.anim_iter.advance() + + self.set_pixbuf(self.anim_iter.get_pixbuf()) + + delay_time = self.anim_iter.get_delay_time() + GLib.timeout_add( + 20 if delay_time < 20 else delay_time, + self.update_animation, + animation, + ) diff --git a/src/preferences.py b/src/preferences.py index 1138197..1bfb63e 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -376,15 +376,15 @@ class PreferencesWindow(Adw.PreferencesWindow): game["removed"] = True save_game(self.parent_widget, game) - if game["game_id"] in self.parent_widget.pixbufs: - move( - self.parent_widget.data_dir - / "cartridges" - / "covers" - / f'{game["game_id"]}.tiff', - deleted_covers_dir / f'{game["game_id"]}.tiff', - ) - self.parent_widget.pixbufs.pop(game["game_id"]) + cover_path = ( + self.parent_widget.data_dir + / "cartridges" + / "covers" + / f'{game["game_id"]}.tiff' + ) + + if cover_path.is_file(): + move(cover_path, deleted_covers_dir / f'{game["game_id"]}.tiff') self.parent_widget.update_games(self.parent_widget.games) if self.parent_widget.stack.get_visible_child() == self.parent_widget.overview: diff --git a/src/utils/create_details_window.py b/src/utils/create_details_window.py index 53aa12b..6a9fd46 100644 --- a/src/utils/create_details_window.py +++ b/src/utils/create_details_window.py @@ -22,7 +22,7 @@ import os import shlex import time -from gi.repository import Adw, GdkPixbuf, Gio, GLib, GObject, Gtk +from gi.repository import Adw, Gio, GLib, GObject, Gtk from .game_cover import GameCover from .create_dialog import create_dialog @@ -37,7 +37,6 @@ def create_details_window(parent_widget, game_id=None): ) games = parent_widget.games - pixbuf = None cover_deleted = False cover_button_edit = Gtk.Button( @@ -59,13 +58,12 @@ def create_details_window(parent_widget, game_id=None): ) def delete_pixbuf(_widget): - nonlocal pixbuf + nonlocal game_cover nonlocal cover_deleted - GameCover(cover) + game_cover.new_pixbuf() cover_button_delete_revealer.set_reveal_child(False) - pixbuf = None cover_deleted = True cover_button_delete.connect("clicked", delete_pixbuf) @@ -77,17 +75,17 @@ def create_details_window(parent_widget, game_id=None): ) cover = Gtk.Picture.new() + game_cover = GameCover(cover) if not game_id: window.set_title(_("Add New Game")) - GameCover(cover) name = Gtk.Entry() developer = Gtk.Entry() executable = Gtk.Entry() apply_button = Gtk.Button.new_with_label(_("Confirm")) else: window.set_title(_("Edit Game Details")) - GameCover(cover, parent_widget.games[game_id].pixbuf) + game_cover.new_pixbuf(path=parent_widget.games[game_id].get_cover()) developer = Gtk.Entry.new_with_buffer( Gtk.EntryBuffer.new(games[game_id].developer, -1) ) @@ -221,28 +219,22 @@ def create_details_window(parent_widget, game_id=None): filechooser.open(window, None, set_cover, None) def set_cover(_source, result, _unused): - nonlocal pixbuf + nonlocal game_cover + nonlocal cover_deleted + try: path = filechooser.open_finish(result).get_path() except GLib.GError: return - try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(path, 200, 300, False) - except GLib.GError: - animated_pixbuf = GdkPixbuf.PixbufAnimation.new_from_file(path) - pixbuf = animated_pixbuf.get_static_image().scale_simple( - 200, 300, GdkPixbuf.InterpType.BILINEAR - ) - cover_button_delete_revealer.set_reveal_child(True) - GameCover(cover, pixbuf) + cover_deleted = True + game_cover.new_pixbuf(path=path) def close_window(_widget, _callback=None): window.close() def apply_preferences(_widget, _callback=None): - nonlocal pixbuf nonlocal cover_deleted nonlocal game_id @@ -321,18 +313,26 @@ def create_details_window(parent_widget, game_id=None): ( parent_widget.data_dir / "cartridges" / "covers" / f"{game_id}.tiff" ).unlink(missing_ok=True) - parent_widget.pixbufs.pop(game_id) + ( + parent_widget.data_dir + / "cartridges" + / "animated_covers" + / f"{game_id}.gif" + ).unlink(missing_ok=True) - if pixbuf: - if game_id in parent_widget.pixbufs: - parent_widget.pixbufs.pop(game_id) - - save_cover(parent_widget, game_id, None, pixbuf) elif not ( parent_widget.data_dir / "cartridges" / "covers" / f"{game_id}.tiff" ).is_file(): SGDBSave(parent_widget, {(game_id, values["name"])}) + save_cover( + parent_widget, + game_id, + None, + game_cover.get_pixbuf(), + game_cover.get_animation(), + ) + path = parent_widget.data_dir / "cartridges" / "games" / f"{game_id}.json" if path.exists(): diff --git a/src/utils/save_cover.py b/src/utils/save_cover.py index 2ad5aa8..f8dd244 100644 --- a/src/utils/save_cover.py +++ b/src/utils/save_cover.py @@ -18,18 +18,48 @@ # SPDX-License-Identifier: GPL-3.0-or-later -from gi.repository import GdkPixbuf, Gio +from pathlib import Path +from shutil import copyfile + +from gi.repository import GdkPixbuf, Gio, GLib +from PIL import Image, ImageSequence -def save_cover(parent_widget, game_id, cover_path=None, pixbuf=None): +def resize_animation(cover_path): + image = Image.open(cover_path) + frames = tuple( + frame.copy().resize((200, 300)) for frame in ImageSequence.Iterator(image) + ) + + tmp_path = Path(Gio.File.new_tmp(None)[0].get_path()) + frames[0].save( + tmp_path, + format="gif", + save_all=True, + append_images=frames[1:], + ) + + return tmp_path + + +def save_cover( + parent_widget, game_id, cover_path=None, pixbuf=None, animation_path=None +): covers_dir = parent_widget.data_dir / "cartridges" / "covers" covers_dir.mkdir(parents=True, exist_ok=True) - if not pixbuf: - pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( - str(cover_path), 400, 600, False - ) + if animation_path: + pixbuf = GdkPixbuf.PixbufAnimation.new_from_file( + str(animation_path) + ).get_static_image() + elif not pixbuf: + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( + str(cover_path), 400, 600, False + ) + except GLib.GError: + return open_file = Gio.File.new_for_path(str(covers_dir / f"{game_id}.tiff")) pixbuf.save_to_streamv( @@ -38,3 +68,9 @@ def save_cover(parent_widget, game_id, cover_path=None, pixbuf=None): ["compression"], ["8"] if parent_widget.schema.get_boolean("high-quality-images") else ["7"], ) + + if animation_path: + animation_dir = parent_widget.data_dir / "cartridges" / "animated_covers" + animation_dir.mkdir(parents=True, exist_ok=True) + + copyfile(animation_path, animation_dir / f"{game_id}.gif") diff --git a/src/window.py b/src/window.py index cc52072..d088f2f 100644 --- a/src/window.py +++ b/src/window.py @@ -96,7 +96,6 @@ class CartridgesWindow(Adw.ApplicationWindow): self.hidden_filtered = {} self.previous_page = self.library_view self.toasts = {} - self.pixbufs = {} self.active_game_id = None self.loading = None self.scaled_pixbuf = None @@ -122,6 +121,8 @@ class CartridgesWindow(Adw.ApplicationWindow): self.update_games(get_games(self)) + self.overview_game_cover = GameCover(self.overview_cover) + # Connect signals self.search_entry.connect("search-changed", self.search_changed, False) self.hidden_search_entry.connect("search-changed", self.search_changed, True) @@ -282,7 +283,7 @@ class CartridgesWindow(Adw.ApplicationWindow): self.active_game_id = game_id pixbuf = current_game.pixbuf - GameCover(self.overview_cover, pixbuf) + self.overview_game_cover.new_pixbuf(path=current_game.get_cover()) self.scaled_pixbuf = pixbuf.scale_simple(2, 3, GdkPixbuf.InterpType.BILINEAR) self.overview_blurred_cover.set_pixbuf(self.scaled_pixbuf)