Composite manually added covers - fixes #177
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
|
from pathlib import Path
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
@@ -29,9 +30,10 @@ from src import shared
|
|||||||
from src.errors.friendly_error import FriendlyError
|
from src.errors.friendly_error import FriendlyError
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.game_cover import GameCover
|
from src.game_cover import GameCover
|
||||||
|
from src.store.managers.cover_manager import CoverManager
|
||||||
from src.store.managers.sgdb_manager import SGDBManager
|
from src.store.managers.sgdb_manager import SGDBManager
|
||||||
from src.utils.create_dialog import create_dialog
|
from src.utils.create_dialog import create_dialog
|
||||||
from src.utils.save_cover import resize_cover, save_cover
|
from src.utils.save_cover import convert_cover, save_cover
|
||||||
|
|
||||||
|
|
||||||
@Gtk.Template(resource_path=shared.PREFIX + "/gtk/details-window.ui")
|
@Gtk.Template(resource_path=shared.PREFIX + "/gtk/details-window.ui")
|
||||||
@@ -281,15 +283,17 @@ class DetailsWindow(Adw.Window):
|
|||||||
except GLib.GError:
|
except GLib.GError:
|
||||||
return
|
return
|
||||||
|
|
||||||
def resize() -> None:
|
def thread_func() -> None:
|
||||||
if cover := resize_cover(path):
|
if new_path := convert_cover(
|
||||||
self.game_cover.new_cover(cover)
|
pixbuf=shared.store.managers[CoverManager].composite_cover(Path(path))
|
||||||
self.cover_button_delete_revealer.set_reveal_child(True)
|
):
|
||||||
self.cover_changed = True
|
self.game_cover.new_cover(new_path)
|
||||||
|
self.cover_button_delete_revealer.set_reveal_child(True)
|
||||||
|
self.cover_changed = True
|
||||||
self.toggle_loading()
|
self.toggle_loading()
|
||||||
|
|
||||||
self.toggle_loading()
|
self.toggle_loading()
|
||||||
GLib.Thread.new(None, resize)
|
GLib.Thread.new(None, thread_func)
|
||||||
|
|
||||||
def set_executable(self, _source: Any, result: Gio.Task, *_args: Any) -> None:
|
def set_executable(self, _source: Any, result: Gio.Task, *_args: Any) -> None:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk
|
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk
|
||||||
from PIL import Image, ImageFilter, ImageStat
|
from PIL import Image, ImageFilter, ImageStat
|
||||||
|
|||||||
@@ -150,7 +150,6 @@ class DesktopSourceIterable(SourceIterable):
|
|||||||
try:
|
try:
|
||||||
icon_str = keyfile.get_string("Desktop Entry", "Icon")
|
icon_str = keyfile.get_string("Desktop Entry", "Icon")
|
||||||
except GLib.GError:
|
except GLib.GError:
|
||||||
print("AAAAAAAAAAAAAAAAAAAAAAA")
|
|
||||||
yield game
|
yield game
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ from pathlib import Path
|
|||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from gi.repository import Gio, GdkPixbuf
|
from gi.repository import GdkPixbuf, Gio
|
||||||
from requests.exceptions import HTTPError, SSLError
|
from requests.exceptions import HTTPError, SSLError
|
||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.store.managers.manager import Manager
|
from src.store.managers.manager import Manager
|
||||||
from src.store.managers.steam_api_manager import SteamAPIManager
|
from src.store.managers.steam_api_manager import SteamAPIManager
|
||||||
from src.utils.save_cover import resize_cover, save_cover
|
from src.utils.save_cover import convert_cover, save_cover
|
||||||
|
|
||||||
|
|
||||||
class ImageSize(NamedTuple):
|
class ImageSize(NamedTuple):
|
||||||
@@ -110,18 +110,16 @@ class CoverManager(Manager):
|
|||||||
stretch = 1 - (resized_height / cover_size.height)
|
stretch = 1 - (resized_height / cover_size.height)
|
||||||
return stretch <= max_stretch
|
return stretch <= max_stretch
|
||||||
|
|
||||||
def save_composited_cover(
|
def composite_cover(
|
||||||
self,
|
self,
|
||||||
game: Game,
|
|
||||||
image_path: Path,
|
image_path: Path,
|
||||||
scale: float = 1,
|
scale: float = 1,
|
||||||
blur_size: ImageSize = ImageSize(2, 2),
|
blur_size: ImageSize = ImageSize(2, 2),
|
||||||
) -> None:
|
) -> GdkPixbuf.Pixbuf:
|
||||||
"""
|
"""
|
||||||
Save the image composited with a background blur.
|
Return the image composited with a background blur.
|
||||||
If the image is stretchable, just stretch it.
|
If the image is stretchable, just stretch it.
|
||||||
|
|
||||||
:param game: The game to save the cover for
|
|
||||||
:param path: Path where the source image is located
|
:param path: Path where the source image is located
|
||||||
:param scale:
|
:param scale:
|
||||||
Scale of the smalled image side
|
Scale of the smalled image side
|
||||||
@@ -130,14 +128,15 @@ class CoverManager(Manager):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Load source image
|
# Load source image
|
||||||
source = GdkPixbuf.Pixbuf.new_from_file(str(image_path))
|
source = GdkPixbuf.Pixbuf.new_from_file(
|
||||||
|
str(convert_cover(image_path, resize=False))
|
||||||
|
)
|
||||||
source_size = ImageSize(source.get_width(), source.get_height())
|
source_size = ImageSize(source.get_width(), source.get_height())
|
||||||
cover_size = ImageSize._make(shared.image_size)
|
cover_size = ImageSize._make(shared.image_size)
|
||||||
|
|
||||||
# Stretch if possible
|
# Stretch if possible
|
||||||
if scale == 1 and self.is_stretchable(source_size, cover_size):
|
if scale == 1 and self.is_stretchable(source_size, cover_size):
|
||||||
save_cover(game.game_id, resize_cover(pixbuf=source))
|
return source
|
||||||
return
|
|
||||||
|
|
||||||
# Create the blurred cover background
|
# Create the blurred cover background
|
||||||
# fmt: off
|
# fmt: off
|
||||||
@@ -164,7 +163,7 @@ class CoverManager(Manager):
|
|||||||
GdkPixbuf.InterpType.BILINEAR,
|
GdkPixbuf.InterpType.BILINEAR,
|
||||||
255,
|
255,
|
||||||
)
|
)
|
||||||
save_cover(game.game_id, resize_cover(pixbuf=cover))
|
return cover
|
||||||
|
|
||||||
def main(self, game: Game, additional_data: dict) -> None:
|
def main(self, game: Game, additional_data: dict) -> None:
|
||||||
if game.blacklisted:
|
if game.blacklisted:
|
||||||
@@ -185,13 +184,15 @@ class CoverManager(Manager):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Icon cover
|
# Icon cover
|
||||||
if key == "local_icon_path":
|
composite_kwargs = {}
|
||||||
self.save_composited_cover(
|
|
||||||
game,
|
|
||||||
image_path,
|
|
||||||
scale=0.7,
|
|
||||||
blur_size=ImageSize(1, 2),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.save_composited_cover(game, image_path)
|
if key == "local_icon_path":
|
||||||
|
composite_kwargs["scale"] = 0.7
|
||||||
|
composite_kwargs["blur_size"] = ImageSize(1, 2)
|
||||||
|
|
||||||
|
save_cover(
|
||||||
|
game.game_id,
|
||||||
|
convert_cover(
|
||||||
|
pixbuf=self.composite_cover(image_path, **composite_kwargs)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
@@ -28,12 +28,22 @@ from PIL import Image, ImageSequence, UnidentifiedImageError
|
|||||||
from src import shared
|
from src import shared
|
||||||
|
|
||||||
|
|
||||||
def resize_cover(
|
def convert_cover(
|
||||||
cover_path: Optional[Path] = None, pixbuf: Optional[GdkPixbuf.Pixbuf] = None
|
cover_path: Optional[Path] = None,
|
||||||
|
pixbuf: Optional[GdkPixbuf.Pixbuf] = None,
|
||||||
|
resize: bool = True,
|
||||||
) -> Optional[Path]:
|
) -> Optional[Path]:
|
||||||
if not cover_path and not pixbuf:
|
if not cover_path and not pixbuf:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
pixbuf_extensions = set()
|
||||||
|
for pixbuf_format in GdkPixbuf.Pixbuf.get_formats():
|
||||||
|
for pixbuf_extension in pixbuf_format.get_extensions():
|
||||||
|
pixbuf_extensions.add(pixbuf_extension)
|
||||||
|
|
||||||
|
if not resize and cover_path and cover_path.suffix.lower()[1:] in pixbuf_extensions:
|
||||||
|
return cover_path
|
||||||
|
|
||||||
if pixbuf:
|
if pixbuf:
|
||||||
cover_path = Path(Gio.File.new_tmp("XXXXXX.tiff")[0].get_path())
|
cover_path = Path(Gio.File.new_tmp("XXXXXX.tiff")[0].get_path())
|
||||||
pixbuf.savev(str(cover_path), "tiff", ["compression"], ["1"])
|
pixbuf.savev(str(cover_path), "tiff", ["compression"], ["1"])
|
||||||
@@ -42,7 +52,8 @@ def resize_cover(
|
|||||||
with Image.open(cover_path) as image:
|
with Image.open(cover_path) as image:
|
||||||
if getattr(image, "is_animated", False):
|
if getattr(image, "is_animated", False):
|
||||||
frames = tuple(
|
frames = tuple(
|
||||||
frame.resize((200, 300)) for frame in ImageSequence.Iterator(image)
|
frame.resize((200, 300)) if resize else frame
|
||||||
|
for frame in ImageSequence.Iterator(image)
|
||||||
)
|
)
|
||||||
|
|
||||||
tmp_path = Path(Gio.File.new_tmp("XXXXXX.gif")[0].get_path())
|
tmp_path = Path(Gio.File.new_tmp("XXXXXX.gif")[0].get_path())
|
||||||
@@ -59,7 +70,7 @@ def resize_cover(
|
|||||||
image = image.convert("RGBA")
|
image = image.convert("RGBA")
|
||||||
|
|
||||||
tmp_path = Path(Gio.File.new_tmp("XXXXXX.tiff")[0].get_path())
|
tmp_path = Path(Gio.File.new_tmp("XXXXXX.tiff")[0].get_path())
|
||||||
image.resize(shared.image_size).save(
|
(image.resize(shared.image_size) if resize else image).save(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
compression="tiff_adobe_deflate"
|
compression="tiff_adobe_deflate"
|
||||||
if shared.schema.get_boolean("high-quality-images")
|
if shared.schema.get_boolean("high-quality-images")
|
||||||
@@ -70,7 +81,7 @@ def resize_cover(
|
|||||||
Gdk.Texture.new_from_filename(str(cover_path)).save_to_tiff(
|
Gdk.Texture.new_from_filename(str(cover_path)).save_to_tiff(
|
||||||
tmp_path := Gio.File.new_tmp("XXXXXX.tiff")[0].get_path()
|
tmp_path := Gio.File.new_tmp("XXXXXX.tiff")[0].get_path()
|
||||||
)
|
)
|
||||||
return resize_cover(tmp_path)
|
return convert_cover(tmp_path)
|
||||||
except GLib.GError:
|
except GLib.GError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from requests.exceptions import HTTPError
|
|||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.utils.save_cover import resize_cover, save_cover
|
from src.utils.save_cover import convert_cover, save_cover
|
||||||
|
|
||||||
|
|
||||||
class SGDBError(Exception):
|
class SGDBError(Exception):
|
||||||
@@ -134,7 +134,7 @@ class SGDBHelper:
|
|||||||
tmp_file = Gio.File.new_tmp()[0]
|
tmp_file = Gio.File.new_tmp()[0]
|
||||||
tmp_file_path = tmp_file.get_path()
|
tmp_file_path = tmp_file.get_path()
|
||||||
Path(tmp_file_path).write_bytes(response.content)
|
Path(tmp_file_path).write_bytes(response.content)
|
||||||
save_cover(game.game_id, resize_cover(tmp_file_path))
|
save_cover(game.game_id, convert_cover(tmp_file_path))
|
||||||
except SGDBAuthError as error:
|
except SGDBAuthError as error:
|
||||||
# Let caller handle auth errors
|
# Let caller handle auth errors
|
||||||
raise error
|
raise error
|
||||||
|
|||||||
Reference in New Issue
Block a user