From a9ac322618622d9c1aac1bbaa4d1368ba85d6ca7 Mon Sep 17 00:00:00 2001 From: Michael Manganiello Date: Sat, 12 Oct 2024 00:50:20 -0300 Subject: [PATCH] fix: Correctly resize and save small artwork The previous implementation was calling `resize_cover_to_small` within the context manager that was writing the image to the filesystem. This was causing `PIL` to raise an error because it could not identify the open and temporarily created file as a valid image. Instead of saving the original image to the filesystem and then resizing it, we now open the image in memory, resize it, and then save it to the filesystem. We also avoid reading the `BytesIO` object twice by saving small and big images from the same initial `Image` object. Fixes #1191. --- backend/endpoints/collections.py | 37 ++++++++++--------- backend/endpoints/rom.py | 20 +++++----- .../handler/filesystem/resources_handler.py | 20 +++++----- 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/backend/endpoints/collections.py b/backend/endpoints/collections.py index cc221e289..19270244d 100644 --- a/backend/endpoints/collections.py +++ b/backend/endpoints/collections.py @@ -1,7 +1,8 @@ import json +from io import BytesIO from shutil import rmtree -from anyio import open_file +from anyio import Path from config import RESOURCES_BASE_PATH from decorators.auth import protected_route from endpoints.responses import MessageResponse @@ -17,6 +18,7 @@ from handler.filesystem.base_handler import CoverSize from logger.logger import log from models.collection import Collection +from PIL import Image from sqlalchemy.inspection import inspect from utils.router import APIRouter @@ -62,15 +64,14 @@ async def add_collection( artwork_path, ) = await fs_resource_handler.build_artwork_path(_added_collection, file_ext) - artwork_file = artwork.file.read() - file_location_s = f"{artwork_path}/small.{file_ext}" - async with await open_file(file_location_s, "wb+") as artwork_s: - await artwork_s.write(artwork_file) - fs_resource_handler.resize_cover_to_small(file_location_s) - - file_location_l = f"{artwork_path}/big.{file_ext}" - async with await open_file(file_location_l, "wb+") as artwork_l: - await artwork_l.write(artwork_file) + artwork_content = BytesIO(await artwork.read()) + file_location_small = Path(f"{artwork_path}/small.{file_ext}") + file_location_large = Path(f"{artwork_path}/big.{file_ext}") + with Image.open(artwork_content) as img: + img.save(file_location_large) + fs_resource_handler.resize_cover_to_small( + img, save_path=file_location_small + ) else: path_cover_s, path_cover_l = await fs_resource_handler.get_cover( overwrite=True, @@ -183,15 +184,15 @@ async def update_collection( cleaned_data["path_cover_l"] = path_cover_l cleaned_data["path_cover_s"] = path_cover_s - artwork_file = artwork.file.read() - file_location_s = f"{artwork_path}/small.{file_ext}" - async with await open_file(file_location_s, "wb+") as artwork_s: - await artwork_s.write(artwork_file) - fs_resource_handler.resize_cover_to_small(file_location_s) + artwork_content = BytesIO(await artwork.read()) + file_location_small = Path(f"{artwork_path}/small.{file_ext}") + file_location_large = Path(f"{artwork_path}/big.{file_ext}") + with Image.open(artwork_content) as img: + img.save(file_location_large) + fs_resource_handler.resize_cover_to_small( + img, save_path=file_location_small + ) - file_location_l = f"{artwork_path}/big.{file_ext}" - async with await open_file(file_location_l, "wb+") as artwork_l: - await artwork_l.write(artwork_file) cleaned_data.update({"url_cover": ""}) else: if data.get("url_cover", "") != collection.url_cover or not ( diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index 402f59ca9..78d4b14d3 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -1,10 +1,11 @@ import binascii from base64 import b64encode +from io import BytesIO from shutil import rmtree from typing import Annotated from urllib.parse import quote -from anyio import Path, open_file +from anyio import Path from config import ( DEV_MODE, DISABLE_DOWNLOAD_ENDPOINT_AUTH, @@ -23,6 +24,7 @@ from handler.filesystem.base_handler import CoverSize from handler.metadata import meta_igdb_handler, meta_moby_handler from logger.logger import log +from PIL import Image from starlette.requests import ClientDisconnect from starlette.responses import FileResponse from streaming_form_data import StreamingFormDataParser @@ -433,15 +435,15 @@ async def update_rom( {"path_cover_s": path_cover_s, "path_cover_l": path_cover_l} ) - artwork_file = artwork.file.read() - file_location_s = f"{artwork_path}/small.{file_ext}" - async with await open_file(file_location_s, "wb+") as artwork_s: - await artwork_s.write(artwork_file) - fs_resource_handler.resize_cover_to_small(file_location_s) + artwork_content = BytesIO(await artwork.read()) + file_location_small = Path(f"{artwork_path}/small.{file_ext}") + file_location_large = Path(f"{artwork_path}/big.{file_ext}") + with Image.open(artwork_content) as img: + img.save(file_location_large) + fs_resource_handler.resize_cover_to_small( + img, save_path=file_location_small + ) - file_location_l = f"{artwork_path}/big.{file_ext}" - async with await open_file(file_location_l, "wb+") as artwork_l: - await artwork_l.write(artwork_file) cleaned_data.update({"url_cover": ""}) else: if data.get("url_cover", "") != rom.url_cover or not ( diff --git a/backend/handler/filesystem/resources_handler.py b/backend/handler/filesystem/resources_handler.py index b1b3f8bfc..033327ef9 100644 --- a/backend/handler/filesystem/resources_handler.py +++ b/backend/handler/filesystem/resources_handler.py @@ -7,7 +7,7 @@ from logger.logger import log from models.collection import Collection from models.rom import Rom -from PIL import Image +from PIL import Image, ImageFile from utils.context import ctx_httpx_client from .base_handler import CoverSize, FSHandler @@ -33,9 +33,8 @@ async def cover_exists(entity: Rom | Collection, size: CoverSize) -> bool: return False @staticmethod - def resize_cover_to_small(cover_path: str) -> None: - """Path of the cover image to resize""" - cover = Image.open(cover_path) + def resize_cover_to_small(cover: ImageFile.ImageFile, save_path: Path) -> None: + """Resize cover to small size, and save it to filesystem.""" if cover.height >= 1000: ratio = 0.2 else: @@ -44,7 +43,7 @@ def resize_cover_to_small(cover_path: str) -> None: small_height = int(cover.height * ratio) small_size = (small_width, small_height) small_img = cover.resize(small_size) - small_img.save(cover_path) + small_img.save(save_path) async def _store_cover( self, entity: Rom | Collection, url_cover: str, size: CoverSize @@ -57,15 +56,15 @@ async def _store_cover( url_cover: url to get the cover size: size of the cover """ - cover_file = f"{size.value}.png" - cover_path = f"{RESOURCES_BASE_PATH}/{entity.fs_resources_path}/cover" + cover_path = Path(f"{RESOURCES_BASE_PATH}/{entity.fs_resources_path}/cover") + cover_file = cover_path / Path(f"{size.value}.png") httpx_client = ctx_httpx_client.get() try: async with httpx_client.stream("GET", url_cover, timeout=120) as response: if response.status_code == 200: - await Path(cover_path).mkdir(parents=True, exist_ok=True) - async with await open_file(f"{cover_path}/{cover_file}", "wb") as f: + await cover_path.mkdir(parents=True, exist_ok=True) + async with await cover_file.open("wb") as f: async for chunk in response.aiter_raw(): await f.write(chunk) except httpx.NetworkError as exc: @@ -77,7 +76,8 @@ async def _store_cover( log.warning(f"Failure writing cover {url_cover} to file (ProtocolError)") if size == CoverSize.SMALL: - self.resize_cover_to_small(f"{cover_path}/{cover_file}") + with Image.open(cover_file) as img: + self.resize_cover_to_small(img, save_path=cover_file) @staticmethod async def _get_cover_path(entity: Rom | Collection, size: CoverSize) -> str: