Skip to content

fix(Android): preserve source EXIF on PNG and WebP output (refs #130) - #395

Merged
AlexV525 merged 2 commits into
mainfrom
fix/android-png-webp-keep-exif
Jul 16, 2026
Merged

fix(Android): preserve source EXIF on PNG and WebP output (refs #130)#395
AlexV525 merged 2 commits into
mainfrom
fix/android-png-webp-keep-exif

Conversation

@AlexV525

@AlexV525 AlexV525 commented Jul 16, 2026

Copy link
Copy Markdown
Member

Second follow-up to #130 (following #394 for the HEIC docs surface).

Stacked on top of fix/android-heic-keep-exif (PR #394) since that PR adds the shared Android test-helper channel this one uses; merge order is #394 → this. GitHub will retarget to main once #394 lands.

Bug

CommonHandler on Android short-circuits ExifKeeper unless the output is JPEG:

```kotlin
if (keepExif && bitmapFormat == Bitmap.CompressFormat.JPEG) { … }
```

PNG and WebP output silently drop source EXIF regardless of keepExif: true.

Fix

  • Replace the JPEG-only guard with supportsExifWrite(format) — a private helper that whitelists JPEG, PNG, WebP (the exact three formats androidx.exifinterface.saveAttributes() supports; disassembly confirms it throws for anything else).
  • Bump androidx.exifinterface 1.3.3 → 1.4.2 in both flutter_image_compress_common/android/build.gradle and the example app's build.gradle.kts (KEEP-IN-SYNC comment). 1.3.3 predates critical WebP writer fixes:
    • VP8 / VP8L-only WebP streams — what Bitmap.compress(WEBP, quality) emits without a VP8X chunk — weren't accepted before ~1.3.7.
    • > 8191px WebP output corrupted before the same era.
    • VP8X E flag wasn't set on the RIFF header, so downstream readers didn't look at the EXIF chunk we just wrote.
  • New runExifKeeperOrRaw helper: if the source doesn't have readable EXIF (ExifKeeper constructor throws), fall back to the raw compressed bytes with a log line rather than propagating up to the handler and returning null for the whole call. keepExif=true degrades gracefully to "compressed but no EXIF".

Verification

Two new integration tests on Android using the test-helper channel from #394:

  • Android PNG + keepExif=true: asserts exif:DateTimeOriginal, exif:DateTimeDigitized, tiff:DateTime from auto-angle.jpg all survive in PNG output.
  • Android WebP + keepExif=true: same assertion for WebP output.

Both pass on Android emulator (API 36, exifinterface 1.4.2). Verified iOS still passes — Android-only tests skip cleanly, existing iOS/macOS keepExif assertions unchanged.

README

The per-format keepExif matrix flips Android PNG and WebP from ❌ to ✅. The follow-ups section now enumerates only the two remaining gaps still tracked under #130:

Adversarial review

Subagent flagged: exifinterface 1.3.3 predates VP8/VP8L / 8191px / VP8X-E-flag WebP writer fixes (FIXED via 1.4.2 bump); double saveAttributes() inside ExifKeeper doubles WebP corruption blast radius on old versions (MITIGATED by version bump); source-parse throw becomes user-visible null return for weird source containers (FIXED via runExifKeeperOrRaw fallback); misleading 1.3.3+ comment (FIXED).

Test plan

  • melos run format
  • melos run analyze
  • Android emulator (API 36): 7/7 keepExif tests pass including the 2 new PNG/WebP regression tests
  • iOS 26.5 simulator: 5/5 keepExif tests pass, 2 Android-only tests skip cleanly
  • Adversarial subagent review of the delta only — 4 real defects caught and fixed
  • CI: iOS/macOS both integration flavors, Android build, Web build

Refs #130.

🤖 Generated with Claude Code

@AlexV525
AlexV525 requested a review from CaiJingLong as a code owner July 16, 2026 04:44
AlexV525 added a commit that referenced this pull request Jul 16, 2026
…e copies

The old "About EXIF information" section listed encoder library names
without saying anything about what actually gets preserved. Rewrite it
so it points at the per-format-per-platform keepExif matrix immediately
above, then briefly names the three coverage tiers (iOS/macOS full
passthrough via CGImageDestination; Android ~90 tags via
androidx.exifinterface; Web/OHOS not supported) and calls out that
Orientation is always normalized to 1 regardless of platform.

Also runs \`melos run cp_files\` to sync the root README into every
package's local README — required per CLAUDE.md's "root README is the
single source of truth" instruction. The per-package diffs accumulated
across #394 and #395 land in this one sync commit.

Refs #130.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AlexV525 and others added 2 commits July 16, 2026 13:08
`CommonHandler` on Android has always short-circuited `ExifKeeper`
unless the output was JPEG:

    if (keepExif && bitmapFormat == Bitmap.CompressFormat.JPEG) { … }

PNG and WebP output silently dropped source EXIF regardless of the
`keepExif: true` request. Users who asked for `keepExif: true` on
PNG or WebP got a valid image back — with none of the EXIF they
expected preserved.

Fix: replace the `bitmapFormat == JPEG` guard with an explicit
`supportsExifWrite(format)` helper that whitelists JPEG, PNG, and
WebP — exactly the three formats `androidx.exifinterface`'s
`saveAttributes()` supports (per bytecode inspection of the current
release: `"ExifInterface only supports saving attributes for JPEG,
PNG, and WebP formats."` for any other input).

Version bump `androidx.exifinterface` from 1.3.3 → 1.4.2. 1.3.3
pre-dates several WebP-writer correctness fixes we now depend on:

- VP8 / VP8L-only WebP streams (what `Bitmap.compress(WEBP, quality)`
  emits when it doesn't prepend a VP8X chunk) were not accepted by
  the writer before ~1.3.7.
- `> 8191px` WebP output silently corrupted before the same era.
- The VP8X `E` flag was not set on the RIFF header, so downstream
  readers didn't look at the EXIF chunk we just wrote.

Also bump the KEEP-IN-SYNC pin in the example app's build.gradle.kts.

Additional hardening: wrap the `ExifKeeper` construction in a
`runExifKeeperOrRaw` helper that falls back to the raw compressed
bytes (with a log line) if the source is corrupt or a container
`ExifInterface` refuses to read. Previously that path would
propagate `IOException` up to the handler wrapper and turn the whole
call into a `null` return — losing the compressed output that was
already produced. Now `keepExif=true` degrades gracefully to
"compressed but no EXIF" instead of failing.

New integration tests exercise the two now-fixed paths against a
real EXIF-rich source (auto-angle.jpg). Verified on Android emulator
(API 36, exifinterface 1.4.2): source `exif:DateTimeOriginal`,
`exif:DateTimeDigitized`, `tiff:DateTime` all survive in both PNG
and WebP outputs. iOS/macOS keepExif regression tests still pass
(the Android-only tests skip on those platforms).

README's per-format keepExif matrix flips Android PNG and Android
WebP from ❌ to ✅, and the follow-ups section now names the two
remaining gaps still tracked under #130: iOS WebP (ImageIO cannot
author WebP metadata) and Android HEIC (ExifInterface refuses HEIF
write; addressed as docs-only in #394).

Refs #130.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e copies

The old "About EXIF information" section listed encoder library names
without saying anything about what actually gets preserved. Rewrite it
so it points at the per-format-per-platform keepExif matrix immediately
above, then briefly names the three coverage tiers (iOS/macOS full
passthrough via CGImageDestination; Android ~90 tags via
androidx.exifinterface; Web/OHOS not supported) and calls out that
Orientation is always normalized to 1 regardless of platform.

Also runs \`melos run cp_files\` to sync the root README into every
package's local README — required per CLAUDE.md's "root README is the
single source of truth" instruction. The per-package diffs accumulated
across #394 and #395 land in this one sync commit.

Refs #130.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AlexV525
AlexV525 force-pushed the fix/android-png-webp-keep-exif branch from e68f3b2 to 4d5b8c6 Compare July 16, 2026 05:09
@AlexV525
AlexV525 merged commit 4ddf139 into main Jul 16, 2026
9 checks passed
@AlexV525
AlexV525 deleted the fix/android-png-webp-keep-exif branch July 16, 2026 05:40
AlexV525 added a commit that referenced this pull request Jul 17, 2026
## Summary

Two commits:

1. **`chore(release): publish packages`** — bumps all six packages via
`melos version`, then hand-trimmed for two problems:
- **Cross-package noise.** `melos run cp_files` syncs the root README
into every sub-package, so a commit that only touched the README (e.g.
`#395 fix(Android): preserve source EXIF...`) shows up as "changed
files" for every package. Melos then lists that commit under every
package's CHANGELOG. Trimmed those from `web`, `ohos`, `macos`, and
`platform_interface` where the commit doesn't actually touch the
package.
- **CI/lint-only entries.** Dropped `FIX(*): resolve lints` and
`FIX(ci): Restore CI green on main` from the user-facing changelogs —
they aren't behavior changes.
2. **`docs: rewrite README for clarity, collapse TOC`** — end-to-end
rewrite of the README prose (previous authors were non-native English
speakers, phrasing had drifted awkward over versions). No facts, tables,
or code samples changed. TOC is now wrapped in `<details>` so the
platform-features table sits closer to the top. Per-package copies
regenerated via `melos run cp_files`.

## Package bumps

| Package | From | To | Kind |
|---|---|---|---|
| `flutter_image_compress` | 2.4.0 | **2.5.0** | minor |
| `flutter_image_compress_common` | 1.0.6 | **1.1.0** | minor |
| `flutter_image_compress_macos` | 1.0.3 | **1.1.0** | minor |
| `flutter_image_compress_platform_interface` | 1.0.5 | **1.1.0** |
minor |
| `flutter_image_compress_web` | 0.1.5 | **0.1.5+1** | patch |
| `flutter_image_compress_ohos` | 0.0.3 | **0.0.3+1** | patch |

## Housekeeping done outside this PR

- Deleted misplaced remote tags `flutter_image_compress_common-v1.1.0`
and `flutter_image_compress_macos-v1.1.0` (they pointed to an unrelated
old commit and 1.1.0 was never actually published).
- Moved `flutter_image_compress_web-v0.1.5` from `3018544` (an old OHOS
merge, pubspec was `0.1.4+1` there) to `42e8c51` (the actual
`chore(release)` commit where pubspec became `0.1.5`).
- Added missing `flutter_image_compress_common-v1.0.6` tag at `b09eac4`
— 1.0.6 was published to pub.dev but never tagged.

## Test plan (all run locally on this branch)

- [x] `melos run format`
- [x] `melos run analyze`
- [ ] `melos run test` — n/a; no unit tests exist in this repo (behavior
baseline lives in `example/integration_test/`)
- [x] `melos run try_build_apk`
- [x] `melos run try_build_ios` (CocoaPods)
- [x] `melos run try_build_ios_swift_package_manager`
- [x] `melos run try_build_macos`
- [x] `melos run try_build_web`
- [x] `melos run verify_macos_behavior` — 13 passed
- [x] `melos run verify_ios_behavior` on iPhone 17 Pro sim (iOS 26.5)

Post-merge:

- [ ] Create GitHub Releases for each bumped package to trigger `melos
publish` via `publish_on_release.yml`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant