EXIF orientation: bake it in when normalizing images (#103) #111

Merged
steve merged 2 commits from feat/exif-orientation into main 2026-07-22 05:35:46 +00:00
2 Commits
Author SHA1 Message Date
steveandClaude Opus 4.8 8aad2278a0 Address EXIF review: single alloc, no per-pixel boxing, hardened parser
Build image / build-and-push (push) Successful in 8s
Gadfly on #103:

- (4 models) applyOrientation allocated a WxH RGBA then threw it away for a
  quarter-turn to allocate HxW. Compute the output dims once, allocate one
  buffer.
- (perf) The At/Set loop boxed a color.Color per pixel — millions of heap
  allocs on a full-res rotation. Convert to *image.RGBA once (draw.Draw)
  then copy 4 bytes per pixel by offset. No boxing.
- (2 findings) exifOrientation didn't skip 0xFF fill bytes before a marker,
  so a spec-valid padded APP1 would be misread. Skip them.
- (2 findings) orientationFromApp1 read the tag's inline value without
  checking its type/count — a mistyped LONG/offset would be read as a
  bogus SHORT. Require type=SHORT, count=1; also validate the TIFF 0x2A
  magic agrees with the byte order.
- Tests now cover all 8 orientations (added the mirror/transpose cases
  2/4/5/7, the most error-prone switch arms) as t.Run subtests.

All imagenorm tests green; gofmt clean; still CGO_ENABLED=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
2026-07-22 01:35:19 -04:00
steveandClaude Opus 4.8 0ab90a01cb EXIF orientation: bake it in when normalizing images (#103)
Build image / build-and-push (push) Successful in 8s
Gadfly review (reusable) / review (pull_request) Successful in 7m8s
Adversarial Review (Gadfly) / review (pull_request) Successful in 7m8s
imagenorm decoded and re-encoded to JPEG but ignored the EXIF Orientation
tag. Phone cameras store the sensor pixels one way and set an EXIF flag to
rotate on display, so a "portrait" JPEG is really a landscape bitmap tagged
"rotate 90°" — and our re-encode strips EXIF, so without baking the rotation
in, a packet photographed in portrait reaches the vision model sideways
(bad OCR) and any future thumbnail is wrong.

- exifOrientation: a small pure-Go parser that walks the JPEG APP1/Exif
  segment for tag 0x0112, returning 1 (normal) for non-JPEG or unparseable
  input — never guess a rotation onto a correct image. No cgo, no new dep.
- applyOrientation: bakes in all 8 orientations (the 4 rotations + mirrors)
  after downscale (cheaper to rotate the small image; a 90° turn swaps the
  sides but not the longest edge, so the downscale bound still holds).
- Non-JPEG paths (HEIC/webp/png) are untouched — their decoders own
  orientation and they carry no JPEG EXIF.

Tests build oriented JPEGs (a corner marker + a spliced Exif APP1) and
assert the marker lands where each orientation says, dims swapping for the
quarter-turns; plus parser defaults for non-JPEG / no-EXIF / garbage.
Stays CGO_ENABLED=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
2026-07-22 01:23:08 -04:00