Skip to content

[camera] Add setImageQuality for JPEG compression control#11155

Open
Bolling88 wants to merge 15 commits intoflutter:mainfrom
Bolling88:fix-set-quality
Open

[camera] Add setImageQuality for JPEG compression control#11155
Bolling88 wants to merge 15 commits intoflutter:mainfrom
Bolling88:fix-set-quality

Conversation

@Bolling88
Copy link

@Bolling88 Bolling88 commented Mar 2, 2026

Adds setImageQuality(int quality) (1–100 scale) across the camera plugin ecosystem, allowing developers to control JPEG compression quality for still image capture. Users who do not call setImageQuality retain the platform's native default behavior.

Packages changed

Package Version Notes
camera_platform_interface 2.12.0 → 2.13.0 Abstract method + method channel impl
camera 0.12.0 → 0.12.1 CameraController.setImageQuality with 1–100 range validation
camera_android 0.10.10+15 → 0.10.11 JpegQualityFeature using CaptureRequest.JPEG_QUALITY (Camera2)
camera_android_camerax 0.7.0+1 → 0.7.1 Recreates ImageCapture via Builder.setJpegQuality; preserves locked capture orientation
camera_avfoundation 0.10.0+3 → 0.10.1 EXIF-preserving JPEG recompression via CGImageSource/CGImageDestination (ImageIO), gated on JPEG format only

Platform implementation details

  • Android (Camera2): New JpegQualityFeature sets CaptureRequest.JPEG_QUALITY on the capture request builder. Lazily registered so users who never call setImageQuality keep the device HAL's native default.
  • Android (CameraX): CameraX only supports setJpegQuality at ImageCapture construction time, so setImageQuality unbinds the current ImageCapture and recreates it with the requested quality. Locked capture orientation is preserved across recreation.
  • iOS (AVFoundation): When quality < 100 and format is JPEG, re-encodes using CGImageDestination with kCGImageDestinationLossyCompressionQuality, preserving all EXIF metadata from the original capture. HEIF captures are passed through unmodified.

Fixes flutter/flutter#183229

Pre-Review Checklist

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. 2

@google-cla
Copy link

google-cla bot commented Mar 2, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the setImageQuality method to control JPEG compression quality for still image captures across the camera plugin. The implementation is federated, with platform-specific logic for Android (Camera2 and CameraX) and iOS. The Android Camera2 implementation uses CaptureRequest.JPEG_QUALITY. The CameraX implementation recreates the ImageCapture use case to apply the quality setting, preserving any locked capture orientation. The iOS implementation re-encodes JPEG images to the specified quality while retaining EXIF metadata. The changes are accompanied by updates to the platform interface, method channels, and comprehensive tests for each platform.

Adds `setImageQuality(int quality)` (1-100 scale) across the camera
plugin ecosystem to allow developers to control JPEG compression
quality for still image capture. Users who do not call setImageQuality
retain the platform's native default behavior.

Implementation details per platform:
- Platform interface: abstract method + method channel implementation
- App-facing: CameraController.setImageQuality with range validation
- Android (Camera2): JpegQualityFeature using CaptureRequest.JPEG_QUALITY
- Android (CameraX): Recreates ImageCapture with Builder.setJpegQuality,
  preserving locked capture orientation
- iOS (AVFoundation): EXIF-preserving JPEG recompression via
  CGImageSource/CGImageDestination (ImageIO), gated on JPEG format only
@stuartmorgan-g
Copy link
Collaborator

Thanks for the contribution!

Pre-Review Checklist

In the future, please use our checklist rather than replacing it with one you have invented. Our checklist is standardized for a reason.

Related issues

Fixes flutter/flutter#60397

Please provide the actual issue this is related to. That's not even a camera issue.

Versions 0.7.1 (camera_android_camerax) and 0.10.1 (camera_avfoundation)
were already taken by upstream changes. Bumps to next minor and includes
upstream CHANGELOG entries.
@Bolling88
Copy link
Author

Thanks for the contribution!

Pre-Review Checklist

In the future, please use our checklist rather than replacing it with one you have invented. Our checklist is standardized for a reason.

Related issues

Fixes flutter/flutter#60397

Please provide the actual issue this is related to. That's not even a camera issue.

Sorry for that! My first contribution, and still learning!

Regarding the issue, I just took a placeholder and forgot to change it. This is rather a feature improvement than resolving an issue, but I found and linked a closed issue where one of the comments requested the possibility to set image quality. If that is not enough, can I just create a new issue?

@stuartmorgan-g
Copy link
Collaborator

This is rather a feature improvement than resolving an issue

As with almost every project, we track bug reports and feature requests in the same issue tracker.

but I found and linked a closed issue where one of the comments requested the possibility to set image quality. If that is not enough, can I just create a new issue?

An issue that was fixed seven years ago does not provide an explanation of what current use case needs new APIs. If there isn't an issue covering the use case you are trying to address, then you should file one.

@Bolling88
Copy link
Author

This is rather a feature improvement than resolving an issue

As with almost every project, we track bug reports and feature requests in the same issue tracker.

but I found and linked a closed issue where one of the comments requested the possibility to set image quality. If that is not enough, can I just create a new issue?

An issue that was fixed seven years ago does not provide an explanation of what current use case needs new APIs. If there isn't an issue covering the use case you are trying to address, then you should file one.

Thank you for your patience. I have created a new issue ticket that clearly explains the problem/feature.

@@ -1,3 +1,7 @@
## 0.12.1

* Adds `setImageQuality` for controlling JPEG compression quality.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this function JPEG only? if so, better to put it in the function name

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API is intentionally named setImageQuality rather than setJpegQuality for a couple of reasons:

  1. It's consistent with Flutter's existing image_picker plugin which uses imageQuality with the same 1–100 int scale.
  2. It's more future-proof — if quality control is ever extended to other formats (e.g. HEIF), the API name still makes sense.
  3. From the app developer's perspective, they're setting the quality of the captured image, regardless of the underlying format.

The current implementation applies to JPEG only (HEIF passes through unmodified), but the abstraction level feels right for a cross-platform API.

let savePhotoDelegate = SavePhotoDelegate(
path: path,
ioQueue: photoIOQueue,
imageQuality: fileExtension == "jpg" && imageQuality < 100 ? imageQuality : nil,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if file extension is jpeg?
does nil quality mean 100% quality?
can quality be a floating point number?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good questions!

  • File extension: The extension is always set by us in captureToFile (either "heif" or "jpg"), so "jpeg" won't occur. But I've changed the check to fileExtension != "heif" which is more robust and format-oriented.
  • nil quality: nil means no quality override was requested — the original photo data is written as-is without re-encoding. This is the default path for users who never call setImageQuality.
  • Float: The int 1–100 scale is consistent with Flutter's image_picker plugin (imageQuality parameter) and with Android's native CaptureRequest.JPEG_QUALITY (byte 0–100) and CameraX's ImageCapture.Builder.setJpegQuality(int). We convert to float internally on iOS with CGFloat(quality) / 100.0. A cross-platform API should use a single consistent type, and int 1–100 is the natural fit for the Android side.

guard let quality = self?.imageQuality, quality < 100 else {
return photo.fileDataRepresentation()
}
guard let originalData = photo.fileDataRepresentation() else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems fileDataRepresentation is used in both case. can reduce duplicate code by putting it in the guard statement above

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Refactored to call fileDataRepresentation() once at the top, then conditionally re-encode. Done in the latest commit.

guard let originalData = photo.fileDataRepresentation() else {
return nil
}
return Self.reencodeJPEG(data: originalData, quality: quality)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it guaranteed to be JPEG format at this point? it is not clear from reading this chunk of code, without looking at the caller site.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — the caller (DefaultCamera.captureToFile) only passes a non-nil imageQuality when the format is JPEG (checked via fileExtension != "heif"). I've added an inline comment in the latest commit to make this guarantee visible without needing to check the caller.

guard
let destination = CGImageDestinationCreateWithData(
mutableData as CFMutableData,
"public.jpeg" as CFString,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why hardcode filename? and also wanna double check jpg vs jpeg since you used both in this PR

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The `"public.jpeg"` string was the UTI for the output format passed to `CGImageDestinationCreateWithData`. I've replaced it with `UTType.jpeg.identifier` in the latest commit which is cleaner.

Regarding `jpg` vs `jpeg` — the only place we use `"jpg"` is the file extension (set by us in `captureToFile`), and `"public.jpeg"` / `UTType.jpeg` is the standard UTI for the JPEG format. These are different things (file extension vs format identifier) so the difference is expected.

mutableData as CFMutableData,
"public.jpeg" as CFString,
1,
nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add comment on what these params are

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added doc comments to the reencodeJPEG method with - Parameters: section explaining both data and quality. Done in the latest commit.

}

var properties = metadata
properties[kCGImageDestinationLossyCompressionQuality] = CGFloat(quality) / 100.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like quality can be a random floating point, and it doesn't have to be a multiple of 0.01? If so, a better API would be just to pass in a float, just like this Apple API.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The int 1–100 scale was chosen to be consistent across all platforms:

  • Android Camera2: CaptureRequest.JPEG_QUALITY uses a byte (0–100)
  • Android CameraX: ImageCapture.Builder.setJpegQuality(int) uses int 1–100
  • Flutter's own image_picker plugin: imageQuality uses int 0–100

Using a float would match the iOS-native API but would be inconsistent with the Android side and existing Flutter conventions. The conversion CGFloat(quality) / 100.0 is straightforward and lossless for all int values in the 1–100 range.

return data
}

return mutableData as Data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UIImage.jpegData(compressionQuality:) strips all EXIF metadata (GPS coordinates, camera info, orientation, etc.). For a camera plugin, preserving this metadata is important — users expect captured photos to retain location data, orientation, and other properties.

CGImageDestination lets us re-encode the pixels at a lower quality while copying all original metadata properties through CGImageSourceCopyPropertiesAtIndex. I've added a doc comment to reencodeJPEG explaining this rationale.

- Deduplicate fileDataRepresentation() call in photoOutput
- Add comment explaining why CGImageDestination is used over UIImage.jpegData
  (EXIF metadata preservation)
- Add doc comments to reencodeJPEG parameters
- Use UTType.jpeg.identifier instead of hardcoded "public.jpeg" string
- Use format-based check (fileExtension != "heif") instead of "jpg" string match
- Add inline comment clarifying JPEG format guarantee from caller
@Bolling88 Bolling88 requested a review from hellohuanlin March 9, 2026 11:10
UniformTypeIdentifiers requires iOS 14+, but camera_avfoundation
supports iOS 13+. Use the equivalent "public.jpeg" string literal.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal:[camera] Add API to control JPEG compression quality

3 participants