Skip to content

Fix: Preserve ICC color profile during lossless JPEG rotation#6776

Draft
shankarpriyank wants to merge 17 commits intocommons-app:mainfrom
shankarpriyank:fix/preserve-icc-profile-rotation
Draft

Fix: Preserve ICC color profile during lossless JPEG rotation#6776
shankarpriyank wants to merge 17 commits intocommons-app:mainfrom
shankarpriyank:fix/preserve-icc-profile-rotation

Conversation

@shankarpriyank
Copy link
Copy Markdown
Contributor

@shankarpriyank shankarpriyank commented Mar 22, 2026

Summary

Fixes #6659 — Rotating images strips the ICC Color Profile and EXIF metadata, causing a brightness/color shift and loss of camera information.

Root causes:

  1. Android's ExifInterface.saveAttributes() physically restructures the metadata block and strips the APP2 segment containing the ICC color profile.
  2. LLJTran's refreshAppx() re-serializes the EXIF structure from its internal representation, dropping ExifIFD sub-tags (FNumber, ExposureTime, GPS, Make, Model, etc.).

Fix: Skip both ExifInterface.saveAttributes() and refreshAppx(). Instead, save with OPT_WRITE_ALL (which writes raw APP segment bytes untouched), then patch only the 2-byte EXIF orientation value directly in the output file using RandomAccessFile. This preserves:

  • ICC color profile (APP2 segment)
  • All EXIF tags (FNumber, DateTime, ExposureTime, Flash, FocalLength, GPS coordinates, Make, Model, WhiteBalance, ISO, etc.)
  • Embedded thumbnails

Changes

  • TransformImageImpl.kt: Replace refreshAppx() with patchExifOrientation() that does a binary patch of the orientation tag in IFD0. Remove Exif import.
  • EditActivity.kt: Remove manual EXIF tag collection (sourceExifAttributeList) and copyExifData() — LLJTran preserves all metadata natively with OPT_WRITE_ALL.
  • TransformImageTest.kt: Add EXIF tag preservation test (verifies FNumber, DateTime, ExposureTime, Flash, FocalLength, GPS tags, Make, Model, WhiteBalance, ISO survive rotation). Add ICC profile preservation test. Re-enable 360° rotation cycle test.
  • ICC_test.jpg: New test resource with embedded ICC color profile.

Verification

Unit tests (all pass)

Test Result
EXIF tags (FNumber, GPS, Make, Model, etc.) preserved after 90°/180°/270° PASS
ICC profile bytes identical after 90°/180°/270° rotation PASS
360° rotation cycle pixel-identical (19 EXIF test images) PASS

Manual verification with exiftool

Check Result
exiftool -b -ICC_Profile raw bytes: original vs rotated Byte-identical (MD5: 22585b96114de9db971ff4ea6c9938b6, 220 bytes)
jpegtran -copy all reference: ICC preserved after 90° Identical
jpegtran 360° cycle: pixel data via djpeg comparison Identical
Full metadata diff (exiftool -a -G1 -s): only expected fields differ Confirmed (dimensions swap, orientation set to Normal)

On-device verification (Pixel 9 Pro Fold, Android 16)

Check Result
ICC profile preserved after 90° rotation in the app PASS (MD5 identical)
DCT encoding preserved (Baseline DCT, Huffman) PASS
Color subsampling preserved (YCbCr4:2:0) PASS
No brightness/color shift PASS

shankarpriyank and others added 17 commits February 7, 2026 02:09
# Conflicts:
#	app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt
#	app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt
#	app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt
#	app/src/main/res/layout/activity_edit.xml
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Android's ExifInterface.saveAttributes() with LLJTran's native
Exif API to set orientation after rotation. ExifInterface repackages
the metadata block and strips the APP2 segment containing the ICC color
profile, causing a visible brightness/color shift.

Changes:
- Use LLJTran's Exif.getTagValue() + refreshAppx() to update orientation
  without touching the ICC profile (APP2 segment)
- Remove manual EXIF tag collection and copyExifData() from EditActivity
  since LLJTran preserves all metadata natively with OPT_WRITE_ALL
- Add ICC profile preservation unit test that verifies raw ICC bytes are
  identical after 90/180/270 degree rotations
- Add ICC_test.jpg test resource with embedded ICC color profile
- Re-enable 360 degree rotation cycle test (was disabled due to this bug)
- Switch assertImagesAreIdentical to pixel-level comparison

Fixes commons-app#6659
Replace LLJTran's refreshAppx() with a direct binary patch of the
orientation byte in the saved JPEG file. refreshAppx() re-serializes
the entire EXIF structure from LLJTran's internal representation,
which drops ExifIFD sub-tags (FNumber, ExposureTime, GPS, Make,
Model, WhiteBalance, etc.).

The new patchExifOrientation() locates the orientation entry in IFD0
and overwrites only its 2-byte value to NORMAL (1), leaving all other
metadata completely untouched. This preserves:
- ICC color profile (APP2 segment)
- All EXIF tags (FNumber, DateTime, GPS coordinates, Make, Model, etc.)
- Embedded thumbnails

Add unit test verifying all standard EXIF tags survive 90/180/270
degree rotations.
Instead of shipping a separate test image, inject a minimal sRGB ICC
profile into Landscape_0.jpg at runtime during the test. This avoids
adding a new binary to the repository while still verifying that ICC
profiles survive lossless rotation.
- Remove unused app1Start variable in patchExifOrientation
- Replace app1Len read with skipBytes(2) since value was discarded
- Guard patchExifOrientation to only run for 90/180/270 rotations
- Fix typo "thishould" → "this should" in test comment
@github-actions
Copy link
Copy Markdown

✅ Generated APK variants!

@RitikaPahwa4444
Copy link
Copy Markdown
Collaborator

Hi @shankarpriyank, we suggest contributors to comment on the issue if they wish to get it assigned to prevent redundant effort. @Kota-Jagadeesh had volunteered to work on it. Even if there's no visible activity, we can always check with the assignee if they have any updates to share :)

@Kota-Jagadeesh - hope you'd not started working on it.

@Kota-Jagadeesh
Copy link
Copy Markdown
Collaborator

@Kota-Jagadeesh - hope you'd not started working on it.

Sorry @RitikaPahwa4444 for not updating my work. I’m working on it and trying to find the best approach. Since it’s also a GSoC task, I’m exploring the best ways to proceed.

I didn’t update on the issue earlier because I’ve been busy writing my GSOC proposal and working on other issues, including #6743, #6775, and a few issues. 🙂

@shankarpriyank
Copy link
Copy Markdown
Contributor Author

Hi @RitikaPahwa4444,

You're right, I should have commented on the issue first. I came across this issue after Nicolas suggested it in a comment on my proposal, and while investigating it I found a fix, so I went ahead and opened a draft PR.

My plan was still to comment on the issue before moving the PR out of draft, but I understand the concern. I'm happy to close this PR since @Kota-Jagadeesh is already assigned to it.
Screenshot 2026-03-23 at 14 35 42

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Rotating images drops the ICC Color Profile causing a brightness shift

4 participants