FLTPHPickerSaveImageToPathOperation.m 9.56 KB
// 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 <Flutter/Flutter.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>

#import "FLTPHPickerSaveImageToPathOperation.h"

#import <os/log.h>

API_AVAILABLE(ios(14))
@interface FLTPHPickerSaveImageToPathOperation ()

@property(strong, nonatomic) PHPickerResult *result;
@property(strong, nonatomic) NSNumber *maxHeight;
@property(strong, nonatomic) NSNumber *maxWidth;
@property(strong, nonatomic) NSNumber *desiredImageQuality;
@property(assign, nonatomic) BOOL requestFullMetadata;

@end

@implementation FLTPHPickerSaveImageToPathOperation {
  BOOL executing;
  BOOL finished;
  FLTGetSavedPath getSavedPath;
}

- (instancetype)initWithResult:(PHPickerResult *)result
                     maxHeight:(NSNumber *)maxHeight
                      maxWidth:(NSNumber *)maxWidth
           desiredImageQuality:(NSNumber *)desiredImageQuality
                  fullMetadata:(BOOL)fullMetadata
                savedPathBlock:(FLTGetSavedPath)savedPathBlock API_AVAILABLE(ios(14)) {
  if (self = [super init]) {
    if (result) {
      self.result = result;
      self.maxHeight = maxHeight;
      self.maxWidth = maxWidth;
      self.desiredImageQuality = desiredImageQuality;
      self.requestFullMetadata = fullMetadata;
      getSavedPath = savedPathBlock;
      executing = NO;
      finished = NO;
    } else {
      return nil;
    }
    return self;
  } else {
    return nil;
  }
}

- (BOOL)isConcurrent {
  return YES;
}

- (BOOL)isExecuting {
  return executing;
}

- (BOOL)isFinished {
  return finished;
}

- (void)setFinished:(BOOL)isFinished {
  [self willChangeValueForKey:@"isFinished"];
  self->finished = isFinished;
  [self didChangeValueForKey:@"isFinished"];
}

- (void)setExecuting:(BOOL)isExecuting {
  [self willChangeValueForKey:@"isExecuting"];
  self->executing = isExecuting;
  [self didChangeValueForKey:@"isExecuting"];
}

- (void)completeOperationWithPath:(NSString *)savedPath error:(FlutterError *)error {
  getSavedPath(savedPath, error);
  [self setExecuting:NO];
  [self setFinished:YES];
}

- (void)start {
  if ([self isCancelled]) {
    [self setFinished:YES];
    return;
  }
  if (@available(iOS 14, *)) {
    [self setExecuting:YES];

    // This supports uniform types that conform to UTTypeImage.
    // This includes UTTypeHEIC, UTTypeHEIF, UTTypeLivePhoto, UTTypeICO, UTTypeICNS, UTTypePNG
    // UTTypeGIF, UTTypeJPEG, UTTypeWebP, UTTypeTIFF, UTTypeBMP, UTTypeSVG, UTTypeRAWImage
    if ([self.result.itemProvider hasItemConformingToTypeIdentifier:UTTypeImage.identifier]) {
      [self.result.itemProvider
          loadDataRepresentationForTypeIdentifier:UTTypeImage.identifier
                                completionHandler:^(NSData *_Nullable data,
                                                    NSError *_Nullable error) {
                                  if (data != nil) {
                                    [self processImage:data];
                                  } else {
                                    FlutterError *flutterError =
                                        [FlutterError errorWithCode:@"invalid_image"
                                                            message:error.localizedDescription
                                                            details:error.domain];
                                    [self completeOperationWithPath:nil error:flutterError];
                                  }
                                }];
    } else if ([self.result.itemProvider
                   // This supports uniform types that conform to UTTypeMovie.
                   // This includes kUTTypeVideo, kUTTypeMPEG4, public.3gpp, kUTTypeMPEG,
                   // public.3gpp2, public.avi, kUTTypeQuickTimeMovie.
                   hasItemConformingToTypeIdentifier:UTTypeMovie.identifier]) {
      [self processVideo];
    } else {
      FlutterError *flutterError = [FlutterError errorWithCode:@"invalid_source"
                                                       message:@"Invalid media source."
                                                       details:nil];
      [self completeOperationWithPath:nil error:flutterError];
    }
  } else {
    [self setFinished:YES];
  }
}

/**
 * Processes the image.
 */
- (void)processImage:(NSData *)pickerImageData API_AVAILABLE(ios(14)) {
  UIImage *localImage = [[UIImage alloc] initWithData:pickerImageData];

  PHAsset *originalAsset;
  // Only if requested, fetch the full "PHAsset" metadata, which requires  "Photo Library Usage"
  // permissions.
  if (self.requestFullMetadata) {
    originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:self.result];
  }

  if (self.maxWidth != nil || self.maxHeight != nil) {
    localImage = [FLTImagePickerImageUtil scaledImage:localImage
                                             maxWidth:self.maxWidth
                                            maxHeight:self.maxHeight
                                  isMetadataAvailable:YES];
  }
  if (originalAsset) {
    void (^resultHandler)(NSData *imageData, NSString *dataUTI, NSDictionary *info) =
        ^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, 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 error:nil];
        };
    if (@available(iOS 13.0, *)) {
      [[PHImageManager defaultManager]
          requestImageDataAndOrientationForAsset:originalAsset
                                         options:nil
                                   resultHandler:^(NSData *_Nullable imageData,
                                                   NSString *_Nullable dataUTI,
                                                   CGImagePropertyOrientation orientation,
                                                   NSDictionary *_Nullable info) {
                                     resultHandler(imageData, dataUTI, info);
                                   }];
    } else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
      [[PHImageManager defaultManager]
          requestImageDataForAsset:originalAsset
                           options:nil
                     resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI,
                                     UIImageOrientation orientation, NSDictionary *_Nullable info) {
                       resultHandler(imageData, dataUTI, info);
                     }];
#pragma clang diagnostic pop
    }
  } else {
    // Image picked without an original asset (e.g. User pick image without permission)
    // maxWidth and maxHeight are used only for GIF images.
    NSString *savedPath =
        [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:pickerImageData
                                                               image:localImage
                                                            maxWidth:self.maxWidth
                                                           maxHeight:self.maxHeight
                                                        imageQuality:self.desiredImageQuality];
    [self completeOperationWithPath:savedPath error:nil];
  }
}

/**
 * Processes the video.
 */
- (void)processVideo API_AVAILABLE(ios(14)) {
  NSString *typeIdentifier = self.result.itemProvider.registeredTypeIdentifiers.firstObject;
  [self.result.itemProvider
      loadFileRepresentationForTypeIdentifier:typeIdentifier
                            completionHandler:^(NSURL *_Nullable videoURL,
                                                NSError *_Nullable error) {
                              if (error != nil) {
                                FlutterError *flutterError =
                                    [FlutterError errorWithCode:@"invalid_image"
                                                        message:error.localizedDescription
                                                        details:error.domain];
                                [self completeOperationWithPath:nil error:flutterError];
                                return;
                              }

                              NSURL *destination =
                                  [FLTImagePickerPhotoAssetUtil saveVideoFromURL:videoURL];
                              if (destination == nil) {
                                [self
                                    completeOperationWithPath:nil
                                                        error:[FlutterError
                                                                  errorWithCode:
                                                                      @"flutter_image_picker_copy_"
                                                                      @"video_error"
                                                                        message:@"Could not cache "
                                                                                @"the video file."
                                                                        details:nil]];
                                return;
                              }

                              [self completeOperationWithPath:[destination path] error:nil];
                            }];
}

@end