From 8d89caea0cf2ec84a35db8dea956da3adf0806d6 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Sun, 19 Apr 2026 14:47:18 +0200 Subject: [PATCH] fix image orientation --- app/images.py | 6 ++++-- tests/test_app.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/app/images.py b/app/images.py index b65e583..cf60ada 100644 --- a/app/images.py +++ b/app/images.py @@ -6,7 +6,7 @@ import subprocess from tempfile import TemporaryDirectory from fastapi import HTTPException, UploadFile -from PIL import Image, UnidentifiedImageError +from PIL import Image, ImageOps, UnidentifiedImageError try: from pillow_heif import register_heif_opener @@ -59,7 +59,9 @@ def process_upload(file: UploadFile | None) -> ProcessedImage | None: def _prepare_image(source_image: Image.Image) -> ProcessedImage: - prepared = _strip_metadata_and_convert(source_image) + # Normalize orientation from EXIF before resizing or stripping metadata. + prepared = ImageOps.exif_transpose(source_image) + prepared = _strip_metadata_and_convert(prepared) prepared.thumbnail((MAX_IMAGE_SIDE, MAX_IMAGE_SIDE)) output = BytesIO() diff --git a/tests/test_app.py b/tests/test_app.py index 702a019..4bdd093 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -63,6 +63,19 @@ def make_image_upload( return (filename, output.getvalue(), "image/png") +def make_oriented_jpeg_upload( + filename="portrait.jpg", + size=(1600, 900), + orientation=6, +): + image = Image.new("RGB", size, (20, 120, 220)) + exif = Image.Exif() + exif[274] = orientation + output = BytesIO() + image.save(output, format="JPEG", exif=exif) + return (filename, output.getvalue(), "image/jpeg") + + def read_jpeg_size(image_bytes): with Image.open(BytesIO(image_bytes)) as image: return image.format, image.size @@ -357,6 +370,21 @@ def test_can_upload_image_for_box_and_process_it(client, db_session): assert image_size == (1600, 800) +def test_image_pipeline_applies_exif_orientation_before_saving(client, db_session): + response = create_box(client, name="Portrait Box", image=make_oriented_jpeg_upload()) + + assert response.status_code == 303 + + box = db_session.query(Box).filter_by(name="Portrait Box").one() + assert box.image_blob is not None + assert box.image_width == 900 + assert box.image_height == 1600 + + image_format, image_size = read_jpeg_size(box.image_blob) + assert image_format == "JPEG" + assert image_size == (900, 1600) + + def test_can_upload_image_for_item(client, db_session): box = Box(name="Main Box") db_session.add(box)