[image_picker] Support reading WebP images for iOS (#4448)
diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md
index 382798f..9ab762a 100644
--- a/packages/image_picker/image_picker/CHANGELOG.md
+++ b/packages/image_picker/image_picker/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.8.4+10
+
+* iOS: allows picking images with WebP format.
+
## 0.8.4+9
* Internal code cleanup for stricter analysis options.
diff --git a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj
index f8fe668..2847bfd 100644
--- a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj
@@ -18,6 +18,11 @@
680049382280F2B9006DD6AB /* pngImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 680049352280F2B8006DD6AB /* pngImage.png */; };
680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 680049362280F2B8006DD6AB /* jpgImage.jpg */; };
6801C8392555D726009DAF8D /* ImagePickerFromGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */; };
+ 86430DF9272D71E9002D9D6C /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; };
+ 86E9A893272754860017E6E0 /* PickerSaveImageToPathOperationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */; };
+ 86E9A894272754A30017E6E0 /* webpImage.webp in Resources */ = {isa = PBXBuildFile; fileRef = 86E9A88F272747B90017E6E0 /* webpImage.webp */; };
+ 86E9A895272769130017E6E0 /* pngImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 680049352280F2B8006DD6AB /* pngImage.png */; };
+ 86E9A896272769150017E6E0 /* jpgImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 680049362280F2B8006DD6AB /* jpgImage.jpg */; };
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
@@ -79,6 +84,8 @@
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+ 86E9A88F272747B90017E6E0 /* webpImage.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = webpImage.webp; sourceTree = "<group>"; };
+ 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PickerSaveImageToPathOperationTests.m; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -132,6 +139,7 @@
F78AF3172342D9D7008449C7 /* ImagePickerTestImages.h */,
F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */,
68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */,
+ 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */,
334733F62668136400DCC49E /* Info.plist */,
);
path = RunnerTests;
@@ -140,6 +148,7 @@
680049282280E33D006DD6AB /* TestImages */ = {
isa = PBXGroup;
children = (
+ 86E9A88F272747B90017E6E0 /* webpImage.webp */,
9FC8F0E8229FA49E00C8D58F /* gifImage.gif */,
680049362280F2B8006DD6AB /* jpgImage.jpg */,
680049352280F2B8006DD6AB /* pngImage.png */,
@@ -352,6 +361,10 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 86430DF9272D71E9002D9D6C /* gifImage.gif in Resources */,
+ 86E9A894272754A30017E6E0 /* webpImage.webp in Resources */,
+ 86E9A895272769130017E6E0 /* pngImage.png in Resources */,
+ 86E9A896272769150017E6E0 /* jpgImage.jpg in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -456,6 +469,7 @@
files = (
334733FD266813F100DCC49E /* MetaDataUtilTests.m in Sources */,
334733FF266813FA00DCC49E /* ImagePickerTestImages.m in Sources */,
+ 86E9A893272754860017E6E0 /* PickerSaveImageToPathOperationTests.m in Sources */,
334733FC266813EE00DCC49E /* ImageUtilTests.m in Sources */,
33473400266813FD00DCC49E /* ImagePickerPluginTests.m in Sources */,
334733FE266813F400DCC49E /* PhotoAssetUtilTests.m in Sources */,
diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m b/packages/image_picker/image_picker/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m
new file mode 100644
index 0000000..f94db83
--- /dev/null
+++ b/packages/image_picker/image_picker/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m
@@ -0,0 +1,100 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <OCMock/OCMock.h>
+#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
+
+@import image_picker;
+@import image_picker.Test;
+@import XCTest;
+
+@interface PickerSaveImageToPathOperationTests : XCTestCase
+
+@end
+
+@implementation PickerSaveImageToPathOperationTests
+
+- (void)testSaveWebPImage API_AVAILABLE(ios(14)) {
+ NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"webpImage"
+ withExtension:@"webp"];
+ NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL];
+ PHPickerResult *result = [self createPickerResultWithProvider:itemProvider
+ withIdentifier:UTTypeWebP.identifier];
+
+ [self verifySavingImageWithPickerResult:result];
+}
+
+- (void)testSavePNGImage API_AVAILABLE(ios(14)) {
+ NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"pngImage"
+ withExtension:@"png"];
+ NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL];
+ PHPickerResult *result = [self createPickerResultWithProvider:itemProvider
+ withIdentifier:UTTypeWebP.identifier];
+
+ [self verifySavingImageWithPickerResult:result];
+}
+
+- (void)testSaveJPGImage API_AVAILABLE(ios(14)) {
+ NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"jpgImage"
+ withExtension:@"jpg"];
+ NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL];
+ PHPickerResult *result = [self createPickerResultWithProvider:itemProvider
+ withIdentifier:UTTypeWebP.identifier];
+
+ [self verifySavingImageWithPickerResult:result];
+}
+
+- (void)testSaveGIFImage API_AVAILABLE(ios(14)) {
+ NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"gifImage"
+ withExtension:@"gif"];
+ NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL];
+ PHPickerResult *result = [self createPickerResultWithProvider:itemProvider
+ withIdentifier:UTTypeWebP.identifier];
+
+ [self verifySavingImageWithPickerResult:result];
+}
+
+/**
+ * Creates a mock picker result using NSItemProvider.
+ *
+ * @param itemProvider an item provider that will be used as picker result
+ * @param identifier local identifier of the asset
+ */
+- (PHPickerResult *)createPickerResultWithProvider:(NSItemProvider *)itemProvider
+ withIdentifier:(NSString *)identifier API_AVAILABLE(ios(14)) {
+ PHPickerResult *result = OCMClassMock([PHPickerResult class]);
+
+ OCMStub([result itemProvider]).andReturn(itemProvider);
+ OCMStub([result assetIdentifier]).andReturn(identifier);
+
+ return result;
+}
+
+/**
+ * Validates a saving process of FLTPHPickerSaveImageToPathOperation.
+ *
+ * FLTPHPickerSaveImageToPathOperation is responsible for saving a picked image to the disk for
+ * later use. It is expected that the saving is always successful.
+ *
+ * @param result the picker result
+ */
+- (void)verifySavingImageWithPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14)) {
+ XCTestExpectation *pathExpectation = [self expectationWithDescription:@"Path was created"];
+
+ FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc]
+ initWithResult:result
+ maxHeight:@100
+ maxWidth:@100
+ desiredImageQuality:@100
+ savedPathBlock:^(NSString *savedPath) {
+ if ([[NSFileManager defaultManager] fileExistsAtPath:savedPath]) {
+ [pathExpectation fulfill];
+ }
+ }];
+
+ [operation start];
+ [self waitForExpectations:@[ pathExpectation ] timeout:30];
+}
+
+@end
diff --git a/packages/image_picker/image_picker/example/ios/TestImages/webpImage.webp b/packages/image_picker/image_picker/example/ios/TestImages/webpImage.webp
new file mode 100644
index 0000000..ab7d40d
--- /dev/null
+++ b/packages/image_picker/image_picker/example/ios/TestImages/webpImage.webp
Binary files differ
diff --git a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m
index 5a084c4..9e7e7e8 100644
--- a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m
+++ b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
+
#import "FLTPHPickerSaveImageToPathOperation.h"
API_AVAILABLE(ios(14))
@@ -82,46 +84,24 @@
}
if (@available(iOS 14, *)) {
[self setExecuting:YES];
+
+ if ([self.result.itemProvider hasItemConformingToTypeIdentifier:UTTypeWebP.identifier]) {
+ [self.result.itemProvider
+ loadDataRepresentationForTypeIdentifier:UTTypeWebP.identifier
+ completionHandler:^(NSData *_Nullable data,
+ NSError *_Nullable error) {
+ UIImage *image = [[UIImage alloc] initWithData:data];
+ [self processImage:image];
+ }];
+ return;
+ }
+
[self.result.itemProvider
loadObjectOfClass:[UIImage class]
completionHandler:^(__kindof id<NSItemProviderReading> _Nullable image,
NSError *_Nullable error) {
if ([image isKindOfClass:[UIImage class]]) {
- __block UIImage *localImage = image;
- PHAsset *originalAsset =
- [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:self.result];
-
- if (self.maxWidth != nil || self.maxHeight != nil) {
- localImage = [FLTImagePickerImageUtil scaledImage:localImage
- maxWidth:self.maxWidth
- maxHeight:self.maxHeight
- isMetadataAvailable:originalAsset != nil];
- }
- __block NSString *savedPath;
- if (!originalAsset) {
- // Image picked without an original asset (e.g. User pick image without permission)
- savedPath =
- [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil
- image:localImage
- imageQuality:self.desiredImageQuality];
- [self completeOperationWithPath:savedPath];
- } else {
- [[PHImageManager defaultManager]
- requestImageDataForAsset:originalAsset
- options:nil
- resultHandler:^(
- NSData *_Nullable imageData, NSString *_Nullable dataUTI,
- UIImageOrientation orientation, NSDictionary *_Nullable info) {
- // maxWidth and maxHeight are used only for GIF images.
- savedPath = [FLTImagePickerPhotoAssetUtil
- saveImageWithOriginalImageData:imageData
- image:localImage
- maxWidth:self.maxWidth
- maxHeight:self.maxHeight
- imageQuality:self.desiredImageQuality];
- [self completeOperationWithPath:savedPath];
- }];
- }
+ [self processImage:image];
}
}];
} else {
@@ -129,4 +109,41 @@
}
}
+/**
+ * Processes the image.
+ */
+- (void)processImage:(UIImage *)localImage API_AVAILABLE(ios(14)) {
+ PHAsset *originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:self.result];
+
+ if (self.maxWidth != nil || self.maxHeight != nil) {
+ localImage = [FLTImagePickerImageUtil scaledImage:localImage
+ maxWidth:self.maxWidth
+ maxHeight:self.maxHeight
+ isMetadataAvailable:originalAsset != nil];
+ }
+ if (originalAsset) {
+ [[PHImageManager defaultManager]
+ requestImageDataForAsset:originalAsset
+ options:nil
+ resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI,
+ UIImageOrientation orientation, NSDictionary *_Nullable info) {
+ // maxWidth and maxHeight are used only for GIF images.
+ NSString *savedPath = [FLTImagePickerPhotoAssetUtil
+ saveImageWithOriginalImageData:imageData
+ image:localImage
+ maxWidth:self.maxWidth
+ maxHeight:self.maxHeight
+ imageQuality:self.desiredImageQuality];
+ [self completeOperationWithPath:savedPath];
+ }];
+ } else {
+ // Image picked without an original asset (e.g. User pick image without permission)
+ NSString *savedPath =
+ [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil
+ image:localImage
+ imageQuality:self.desiredImageQuality];
+ [self completeOperationWithPath:savedPath];
+ }
+}
+
@end
diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml
index 1d280f1..4774711 100755
--- a/packages/image_picker/image_picker/pubspec.yaml
+++ b/packages/image_picker/image_picker/pubspec.yaml
@@ -3,7 +3,7 @@
library, and taking new pictures with the camera.
repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
-version: 0.8.4+9
+version: 0.8.4+10
environment:
sdk: ">=2.14.0 <3.0.0"