Fix #284 -- Make image dimensions EXIF rotation aware#287
Conversation
codingjoe
left a comment
There was a problem hiding this comment.
Hi @SAY-5,
Thank you for the contribution. I am actually hoping to get this resolved in Django, but it's a good idea to ship a patch now.
I left you a few notes. Please make sure to run the benchmark tests locally to monitor the performance impact of your change. You might even want to add a benchmark test if this part of the code isn't covered.
The performance of this package is important; people might process millions of images, and a few milliseconds quickly add up to days.
This part is extra critical as it happens during the request (or whenever a model is saved). So extra I/O to an external blob store might even open a DOS vector.
Please notify me when you are ready for another review.
Cheers,
Joe
|
Thanks for the review and the context on the perf sensitivity. I'll run the benchmark tests locally and report the before/after numbers for this path, and add a benchmark if it isn't already covered, before pushing the updates for your other notes. |
49383bf to
11a2dd0
Compare
|
Reworked _get_image_dimensions to open the file once and read both size and orientation in the same pass instead of going through the super() call, dropped the inherited-method docstring, and added a comment on the orientation tag. The test now asserts the fixture's raw size and orientation up front. Tests pass locally. |
11a2dd0 to
7459b78
Compare
|
Opened the file once via Image.open and read both size and orientation in the same pass; dropped the super call. Test asserts the fixture's raw size and orientation up front. All tests pass locally. |
7459b78 to
cff9200
Compare
|
Updated per review: dimensions come from django.core.files.images.get_image_dimensions on the already-open handle (no extra open/close), and the orientation tag is read in the same try block via a small Image.open getexif call. Added a comment explaining 5-8. The test now asserts the fixture invariants (size 2000x3000, orientation 8) up front. Test suite passes locally. |
codingjoe
left a comment
There was a problem hiding this comment.
Thanks @SAY-5!
I took the liberty and added more tests, including a benchmark. With the benchmark I found an 80% performance degradation.
Therefore, I updated the implementation to only load the image once and added more resource tests.
The main caveat is that we now load the file image into memory. Which isn't great, but Pillow doesn't offer an easy way to extract EXIF information from partial byte streams.
Since I wrote a lot of new code, I will have copilot do a review. Don't hesitate to give it another review yourself.
Cheers!
Joe
There was a problem hiding this comment.
Pull request overview
This PR fixes incorrect PictureFieldFile dimension reporting for images that rely on EXIF Orientation (90°/270° rotations), ensuring width_field/height_field and generated <img width/height> attributes match how browsers (and ImageOps.exif_transpose) display the image.
Changes:
- Override
PictureFieldFile._get_image_dimensions()to account for EXIF Orientation values that swap width/height and cache the result. - Update the existing large template snapshot to reflect corrected
width/heightoutput and srcset widths. - Add targeted tests for EXIF-rotated dimensions and for preserving file open/close state and cursor position; adjust pytest-benchmark grouping configuration.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
pictures/models.py |
Adds EXIF-aware dimension calculation and caching for PictureFieldFile. |
tests/test_models.py |
Adds regression test for EXIF-rotated width/height and behavior/performance tests for _get_image_dimensions(). |
tests/test_templatetags.py |
Updates snapshot expectations for <img width/height> and srcset after EXIF-aware dimensions. |
tests/test_tasks.py |
Assigns a stable pytest-benchmark group name to the task benchmark. |
tests/test_migrations.py |
Assigns a stable pytest-benchmark group name to the migration benchmark. |
pyproject.toml |
Removes global benchmark grouping-by-fullname to rely on explicit per-test grouping. |
Closes #284.
Browsers and
PIL.ImageOps.exif_transposerotate images based on their EXIF orientation tag, so the byte-level dimensions can differ from the displayed dimensions. Since source sets are already rotated on save, the width/height reported onPictureFieldFilemust match the displayed image. Otherwisewidth_field/height_fieldcache the wrong orientation and the<img>tag emits swappedwidth/heightattributes.PictureFieldFile._get_image_dimensionsnow swaps the returned width/height when the EXIF orientation tag (0x0112) indicates a 90 or 270 degree rotation (values 5, 6, 7, 8). The dimensions are cached, matching Django's parent behavior.The existing
test_picture__largetemplate snapshot was asserting the buggy pre-fix output; it is updated to reflect the correctly rotated dimensions. A newtest_width_height__exif_rotatedtest pins the fix directly.