fix: gallery records raw instead of display dimensions for EXIF-rotated photos (#1667)
This commit is contained in:
@@ -32,10 +32,21 @@ def _extract_exif(content: bytes) -> dict:
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
img = Image.open(BytesIO(content))
|
||||
# Read the raw EXIF before any transpose: exif_transpose strips the
|
||||
# orientation tag and with it the parsed EXIF view.
|
||||
exif = img._getexif() if hasattr(img, '_getexif') else None
|
||||
|
||||
# Record DISPLAY dimensions (EXIF-rotated), matching upload_handler.
|
||||
# A phone photo with Orientation 6/8 is stored landscape but shown
|
||||
# portrait, so the raw width/height swap the aspect ratio.
|
||||
try:
|
||||
from PIL import ImageOps
|
||||
img = ImageOps.exif_transpose(img) or img
|
||||
except Exception:
|
||||
pass
|
||||
result["width"] = img.width
|
||||
result["height"] = img.height
|
||||
|
||||
exif = img._getexif() if hasattr(img, '_getexif') else None
|
||||
if not exif:
|
||||
return result
|
||||
|
||||
|
||||
71
tests/test_gallery_exif_orientation.py
Normal file
71
tests/test_gallery_exif_orientation.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Gallery EXIF extraction must report display (EXIF-rotated) dimensions.
|
||||
|
||||
A phone photo with EXIF Orientation 6 or 8 is stored e.g. 400x300 but
|
||||
displayed 300x400. _extract_exif read img.width/img.height from the raw
|
||||
buffer, so the gallery recorded the wrong aspect ratio for rotated photos
|
||||
while upload_handler (which applies ImageOps.exif_transpose) got it right.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
import types
|
||||
from io import BytesIO
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("PIL")
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def extract_exif(monkeypatch):
|
||||
"""Import routes.gallery_helpers under a core.database stub.
|
||||
|
||||
_extract_exif never touches the DB, but the module imports GalleryImage
|
||||
at import time and the conftest sqlalchemy stubs make the real
|
||||
core.database unimportable in isolation.
|
||||
"""
|
||||
|
||||
class _DBStub(types.ModuleType):
|
||||
def __getattr__(self, name):
|
||||
return MagicMock()
|
||||
|
||||
monkeypatch.setitem(sys.modules, "core.database", _DBStub("core.database"))
|
||||
monkeypatch.delitem(sys.modules, "routes.gallery_helpers", raising=False)
|
||||
mod = importlib.import_module("routes.gallery_helpers")
|
||||
return mod._extract_exif
|
||||
|
||||
|
||||
def _jpeg(width, height, orientation=None, make=None):
|
||||
img = Image.new("RGB", (width, height), "blue")
|
||||
exif = Image.Exif()
|
||||
if orientation is not None:
|
||||
exif[0x0112] = orientation # Orientation
|
||||
if make is not None:
|
||||
exif[0x010F] = make # Make
|
||||
buf = BytesIO()
|
||||
img.save(buf, format="JPEG", exif=exif)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def test_orientation_6_reports_display_dimensions(extract_exif):
|
||||
res = extract_exif(_jpeg(400, 300, orientation=6))
|
||||
assert (res["width"], res["height"]) == (300, 400)
|
||||
|
||||
|
||||
def test_orientation_8_reports_display_dimensions(extract_exif):
|
||||
res = extract_exif(_jpeg(400, 300, orientation=8))
|
||||
assert (res["width"], res["height"]) == (300, 400)
|
||||
|
||||
|
||||
def test_no_orientation_keeps_raw_dimensions(extract_exif):
|
||||
res = extract_exif(_jpeg(400, 300))
|
||||
assert (res["width"], res["height"]) == (400, 300)
|
||||
|
||||
|
||||
def test_camera_fields_survive_the_transpose(extract_exif):
|
||||
# exif_transpose strips the EXIF view, so tags must be read before it
|
||||
res = extract_exif(_jpeg(400, 300, orientation=6, make="TestMake"))
|
||||
assert res["camera_make"] == "TestMake"
|
||||
assert (res["width"], res["height"]) == (300, 400)
|
||||
Reference in New Issue
Block a user