[camera] Add setImageQuality for JPEG compression control#11155
[camera] Add setImageQuality for JPEG compression control#11155Bolling88 wants to merge 15 commits intoflutter:mainfrom
Conversation
|
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. |
There was a problem hiding this comment.
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
8c7c979 to
52b4cff
Compare
CameraTest and CameraTest_getRecordingProfileTest implement CameraFeatureFactory but were missing the new createJpegQualityFeature method, causing compilation failures.
|
Thanks for the contribution!
In the future, please use our checklist rather than replacing it with one you have invented. Our checklist is standardized for a reason.
Please provide the actual issue this is related to. That's not even a |
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.
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? |
As with almost every project, we track bug reports and feature requests in the same issue tracker.
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. | |||
There was a problem hiding this comment.
is this function JPEG only? if so, better to put it in the function name
There was a problem hiding this comment.
The API is intentionally named setImageQuality rather than setJpegQuality for a couple of reasons:
- It's consistent with Flutter's existing
image_pickerplugin which usesimageQualitywith the same 1–100 int scale. - It's more future-proof — if quality control is ever extended to other formats (e.g. HEIF), the API name still makes sense.
- 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, |
There was a problem hiding this comment.
what if file extension is jpeg?
does nil quality mean 100% quality?
can quality be a floating point number?
There was a problem hiding this comment.
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 tofileExtension != "heif"which is more robust and format-oriented. - nil quality:
nilmeans 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 callsetImageQuality. - Float: The int 1–100 scale is consistent with Flutter's
image_pickerplugin (imageQualityparameter) and with Android's nativeCaptureRequest.JPEG_QUALITY(byte 0–100) and CameraX'sImageCapture.Builder.setJpegQuality(int). We convert to float internally on iOS withCGFloat(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 { |
There was a problem hiding this comment.
it seems fileDataRepresentation is used in both case. can reduce duplicate code by putting it in the guard statement above
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
why hardcode filename? and also wanna double check jpg vs jpeg since you used both in this PR
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
can you add comment on what these params are
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
The int 1–100 scale was chosen to be consistent across all platforms:
- Android Camera2:
CaptureRequest.JPEG_QUALITYuses a byte (0–100) - Android CameraX:
ImageCapture.Builder.setJpegQuality(int)uses int 1–100 - Flutter's own
image_pickerplugin:imageQualityuses 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 |
There was a problem hiding this comment.
Any reason why we can't use simpler APIs such as https://developer.apple.com/documentation/uikit/uiimage/jpegdata(compressionquality:)?
There was a problem hiding this comment.
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
UniformTypeIdentifiers requires iOS 14+, but camera_avfoundation supports iOS 13+. Use the equivalent "public.jpeg" string literal.
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 callsetImageQualityretain the platform's native default behavior.Packages changed
camera_platform_interfacecameraCameraController.setImageQualitywith 1–100 range validationcamera_androidJpegQualityFeatureusingCaptureRequest.JPEG_QUALITY(Camera2)camera_android_cameraxImageCaptureviaBuilder.setJpegQuality; preserves locked capture orientationcamera_avfoundationCGImageSource/CGImageDestination(ImageIO), gated on JPEG format onlyPlatform implementation details
JpegQualityFeaturesetsCaptureRequest.JPEG_QUALITYon the capture request builder. Lazily registered so users who never callsetImageQualitykeep the device HAL's native default.setJpegQualityatImageCaptureconstruction time, sosetImageQualityunbinds the currentImageCaptureand recreates it with the requested quality. Locked capture orientation is preserved across recreation.CGImageDestinationwithkCGImageDestinationLossyCompressionQuality, preserving all EXIF metadata from the original capture. HEIF captures are passed through unmodified.Fixes flutter/flutter#183229
Pre-Review Checklist
[shared_preferences]///).Footnotes
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