A photo that relies on its EXIF orientation is stored sideways #1079

Closed
opened 2026-10-04 13:28:31 +00:00 by claude-bot · 2 comments
Collaborator

Found by the independent review of PR #1078. It is on main today and is not caused by that PR.

What

The image check re-encodes every upload and strips its EXIF (exif=b""), but never applies the orientation tag first. There is no exif_transpose anywhere in app/. Phones commonly store a portrait photo as landscape pixels plus an orientation tag, so such a photo comes out on its side as a page picture, an attachment or a map.

Read from the code by the reviewer; not reproduced with a real photo yet. Do that first.

Intended approach

  • Apply the orientation before re-encoding (ImageOps.exif_transpose), then strip EXIF as now.
  • Memory. A transpose makes a second copy of the pixels. The picture bound in #1078 is sized for one decode in a 512 MB backend with a thin margin, so measure the transposed case at the bound (docker run -m 512m) and adjust the weight for a file that carries a rotating tag if it does not fit. Transposing in place, or by JPEG lossless means, may avoid the copy.
  • Width and height stored for a picture or a map must be the transposed ones.
  • Tests: each of the eight orientation values comes out upright with no EXIF; an untagged picture is unchanged.

#1074, #1078.

Found by the independent review of PR #1078. It is on main today and is not caused by that PR. ## What The image check re-encodes every upload and strips its EXIF (`exif=b""`), but never applies the orientation tag first. There is no `exif_transpose` anywhere in `app/`. Phones commonly store a portrait photo as landscape pixels plus an orientation tag, so such a photo comes out on its side as a page picture, an attachment or a map. Read from the code by the reviewer; not reproduced with a real photo yet. Do that first. ## Intended approach - Apply the orientation before re-encoding (`ImageOps.exif_transpose`), then strip EXIF as now. - **Memory.** A transpose makes a second copy of the pixels. The picture bound in #1078 is sized for one decode in a 512 MB backend with a thin margin, so measure the transposed case at the bound (`docker run -m 512m`) and adjust the weight for a file that carries a rotating tag if it does not fit. Transposing in place, or by JPEG lossless means, may avoid the copy. - Width and height stored for a picture or a map must be the transposed ones. - Tests: each of the eight orientation values comes out upright with no EXIF; an untagged picture is unchanged. ## Related #1074, #1078.
Author
Collaborator

Picking this up, on fix/1079-exif-orientation.

Planned

  • Reproduce first with an Orientation 6 JPEG: the issue was read from the code, not seen.
  • Apply the orientation before the re-encode; metadata is still stripped.
  • Measure the transposed case in a 512 MB container. A 4032×3024 photo with a rotating tag must still be accepted in the backend.
  • Stored width and height are the upright ones.

Not planned: changing pictures already stored. Their tag was stripped at upload, so the orientation cannot be recovered; a sideways picture needs uploading again.

**Picking this up**, on `fix/1079-exif-orientation`. **Planned** - Reproduce first with an Orientation 6 JPEG: the issue was read from the code, not seen. - Apply the orientation before the re-encode; metadata is still stripped. - Measure the transposed case in a 512 MB container. A 4032×3024 photo with a rotating tag must still be accepted in the backend. - Stored width and height are the upright ones. **Not planned:** changing pictures already stored. Their tag was stripped at upload, so the orientation cannot be recovered; a sideways picture needs uploading again.
Author
Collaborator

Fixed in PR #1086, merged to main. CI was green on the PR's head.

  • Reproduced first: a 400×300 JPEG with Orientation 6 came out 400×300.
  • The change: the orientation is applied before the re-encode, for all eight values, in JPEG, PNG and WebP; metadata is still stripped; stored and reported sizes are the upright ones.
  • No extra memory: a turned 4032×3024 photo costs +51 MB, the same as an untagged one (transpose cost +93 MB).
  • Through nginx on a warm backend: an Orientation 6 photo with GPS came back upright, 3024×4032, with no EXIF.

Not changed: pictures already stored. Their tag was stripped at upload; a sideways one needs uploading again.

Follow-up: #1087, a JPEG's EXIF can exhaust memory when the file is opened. Found while building this; being fixed on fix/1087-exif-open-bomb.

Reaches dev with the next prerelease.

**Fixed in PR #1086, merged to main. CI was green on the PR's head.** - **Reproduced first:** a 400×300 JPEG with Orientation 6 came out 400×300. - **The change:** the orientation is applied before the re-encode, for all eight values, in JPEG, PNG and WebP; metadata is still stripped; stored and reported sizes are the upright ones. - **No extra memory:** a turned 4032×3024 photo costs +51 MB, the same as an untagged one (`transpose` cost +93 MB). - **Through nginx on a warm backend:** an Orientation 6 photo with GPS came back upright, 3024×4032, with no EXIF. **Not changed:** pictures already stored. Their tag was stripped at upload; a sideways one needs uploading again. **Follow-up:** #1087, a JPEG's EXIF can exhaust memory when the file is opened. Found while building this; being fixed on `fix/1087-exif-open-bomb`. Reaches dev with the next prerelease.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#1079
No description provided.