fix: honour EXIF orientation for uploaded images - #3233
Conversation
Portrait photos shot on a rotated camera/phone are stored with landscape pixels plus an EXIF Orientation tag (6 or 8). Anthias ignored that tag and drew the raw pixels, so those images appeared rotated 90° on screen (issue 3232). Two independent render paths both dropped the orientation: - The viewer decoded images with QImage::loadFromData(), which does not apply EXIF orientation. Decode through a QImageReader with setAutoTransform(true) instead, so the rotation/flip is applied during decode for every format that carries the tag (JPEG, PNG, HEIC, ...). This covers JPEG, which the server serves byte-for-byte. - The upload-time normalisation pipeline (HEIC / AVIF / TIFF → lossless WebP) converted and saved without applying the tag, and WebP output carries no Orientation tag — so those formats were baked rotated. Apply ImageOps.exif_transpose() before the RGBA convert; in_place avoids allocating a second full pixel buffer. Validated: - Server: new test feeds a landscape source tagged Orientation=6 through _convert_image_to_webp and asserts the WebP comes out upright (transposed dimensions); it fails without the transpose. - Viewer: QImageReader + setAutoTransform(true) on the reporter's Canon EOS R6m2 JPEG (6000x4000, Orientation 8) yields an upright 4000x6000 image on Qt 6, where the old loadFromData path left it 6000x4000. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01HJ3ucEkn62cbgPoisAZ5LQ
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3233 +/- ##
=========================================
Coverage ? 89.44%
=========================================
Files ? 76
Lines ? 8344
Branches ? 892
=========================================
Hits ? 7463
Misses ? 666
Partials ? 215 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Testbed validationTested on the physical hardware fleet, not just CI. Server path (
|
SonarCloud's bundled Pillow stubs predate the in_place kwarg (added in Pillow 9.5; we pin 12.3.0) and flag python:S930 'unexpected named argument', failing the new-code reliability gate. mypy — which uses the real Pillow py.typed stubs — accepts it. Annotate the genuine false positive with # NOSONAR per repo convention. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01HJ3ucEkn62cbgPoisAZ5LQ
Split two 'assert a and b' assertions into separate statements (SonarCloud python:S5906) so a failure names which half broke. Flagged on this PR's new-code scope after the earlier insertion shifted them in. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01HJ3ucEkn62cbgPoisAZ5LQ
Update: verified on-device with a real viewer screenshotMy earlier note said I hadn't rebuilt the viewer on-device — I have now done that on the reporter's exact board (Raspberry Pi 4).
Result on the physical display:
Same asset, same device, only the viewer binary changed. Testbed restored to the shipped image afterwards. |
…ion-3232 # Conflicts: # src/anthias_server/processing.py # tests/test_processing.py
|



Addresses the rotation half of issue 3232 (Autoscale images for display).
Problem
Portrait photos shot on a rotated camera or phone are stored with landscape pixels plus an EXIF Orientation tag (value 6 or 8). Anthias ignored that tag and drew the raw pixels, so those images appeared rotated 90° on the display. The reporter's example is a Canon EOS R6m2 JPEG:
6000×4000pixels, Orientation = 8.Root cause
Two independent render paths both dropped the orientation:
src/anthias_webview/src/view.cpp): images are decoded withQImage::loadFromData(), which does not apply EXIF orientation. Qt only honours it when decoding through aQImageReaderwithsetAutoTransform(true). This is the path uploaded JPEGs take (the server serves them byte-for-byte).src/anthias_server/processing.py): the upload-time normalisation pipeline (HEIC / AVIF / TIFF → lossless WebP) converts and saves without applying the tag, and WebP output carries no Orientation tag — so a portrait HEIC (e.g. an iPhone photo) was baked rotated.There was no EXIF handling anywhere in the codebase.
Fix
QImageReaderwithsetAutoTransform(true), so the rotation/flip is applied during decode for every format that carries the tag.ImageOps.exif_transpose()before the RGBA convert in_convert_image_to_webp().in_place=Truetransposes the open image without allocating a second full pixel buffer (respecting the memory discipline already documented there).Validation
tests/test_processing.pyfeeds a24×16landscape source tagged Orientation=6 through_convert_image_to_webpand asserts the WebP comes out16×24(upright). It passes with the fix and fails without it (verified by neutralising the transpose line). Fulltest_processing.pysuite: 123 passed.ruff check+ruff format --checkclean.QImageReader+setAutoTransform(true)— against the reporter's actual Canon JPEG on Qt 6. The oldloadFromDatapath yields6000×4000(landscape, wrong); the new path yields4000×6000(portrait, upright). This is pure Qt image I/O, identical behaviour on the Pi's Qt 6.Scope note
This PR is the orientation fix only. The issue also asks for autoscaling to the display: the viewer already scales images to fit with
KeepAspectRatioand a black background (letterboxing), so downscaling large images already works. The remaining question — whether to stop upscaling small images by default, optionally behind a setting — is a separate behaviour change and is intentionally left out of this correctness fix.🤖 Generated with Claude Code
https://claude.ai/code/session_01HJ3ucEkn62cbgPoisAZ5LQ