Skip to content

Commit

Permalink
fix: Correctly resize and save small artwork
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
adamantike committed Oct 12, 2024
1 parent 36c77d5 commit a9ac322
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 37 deletions.
37 changes: 19 additions & 18 deletions backend/endpoints/collections.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
Expand Down
20 changes: 11 additions & 9 deletions backend/endpoints/rom.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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 (
Expand Down
20 changes: 10 additions & 10 deletions backend/handler/filesystem/resources_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand Down

0 comments on commit a9ac322

Please sign in to comment.