1 // Copyright 2021-present 650 Industries. All rights reserved.
2 
3 import CoreGraphics
4 import Photos
5 import UIKit
6 import ABI49_0_0ExpoModulesCore
7 
8 public class ImageManipulatorModule: Module {
9   typealias LoadImageCallback = (Result<UIImage, Error>) -> Void
10   typealias SaveImageResult = (url: URL, data: Data)
11 
definitionnull12   public func definition() -> ModuleDefinition {
13     Name("ExpoImageManipulator")
14 
15     AsyncFunction("manipulateAsync", manipulateImage)
16       .runOnQueue(.main)
17   }
18 
manipulateImagenull19   internal func manipulateImage(url: URL, actions: [ManipulateAction], options: ManipulateOptions, promise: Promise) {
20     loadImage(atUrl: url) { result in
21       switch result {
22       case .failure(let error):
23         return promise.reject(error)
24       case .success(let image):
25         do {
26           let newImage = try manipulate(image: image, actions: actions)
27           let saveResult = try self.saveImage(newImage, options: options)
28 
29           promise.resolve([
30             "uri": saveResult.url.absoluteString,
31             "width": newImage.cgImage?.width ?? 0,
32             "height": newImage.cgImage?.height ?? 0,
33             "base64": options.base64 ? saveResult.data.base64EncodedString() : nil
34           ])
35         } catch {
36           promise.reject(error)
37         }
38       }
39     }
40   }
41 
42   /**
43    Loads the image from given URL.
44    */
loadImagenull45   internal func loadImage(atUrl url: URL, callback: @escaping LoadImageCallback) {
46     if url.scheme == "data" {
47       guard let data = try? Data(contentsOf: url), let image = UIImage(data: data) else {
48         return callback(.failure(CorruptedImageDataException()))
49       }
50       return callback(.success(image))
51     }
52     if url.scheme == "assets-library" {
53       // TODO: ALAsset URLs are deprecated as of iOS 11, we should migrate to `ph://` soon.
54       return loadImageFromPhotoLibrary(url: url, callback: callback)
55     }
56 
57     guard let imageLoader = self.appContext?.imageLoader else {
58       return callback(.failure(ImageLoaderNotFoundException()))
59     }
60     guard let fileSystem = self.appContext?.fileSystem else {
61       return callback(.failure(FileSystemNotFoundException()))
62     }
63     guard fileSystem.permissions(forURI: url).contains(.read) else {
64       return callback(.failure(FileSystemReadPermissionException(url.absoluteString)))
65     }
66 
67     imageLoader.loadImage(for: url) { error, image in
68       guard let image = image, error == nil else {
69         return callback(.failure(ImageLoadingFailedException(error.debugDescription)))
70       }
71       callback(.success(image))
72     }
73   }
74 
75   /**
76    Loads the image from user's photo library.
77    */
loadImageFromPhotoLibrarynull78   internal func loadImageFromPhotoLibrary(url: URL, callback: @escaping LoadImageCallback) {
79     guard let asset = PHAsset.fetchAssets(withALAssetURLs: [url], options: nil).firstObject else {
80       return callback(.failure(ImageNotFoundException()))
81     }
82     let size = CGSize(width: asset.pixelWidth, height: asset.pixelHeight)
83     let options = PHImageRequestOptions()
84 
85     options.resizeMode = .exact
86     options.isNetworkAccessAllowed = true
87     options.isSynchronous = true
88     options.deliveryMode = .highQualityFormat
89 
90     PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFit, options: options) { image, _ in
91       guard let image = image else {
92         return callback(.failure(ImageNotFoundException()))
93       }
94       return callback(.success(image))
95     }
96   }
97 
98   /**
99    Saves the image as a file.
100    */
saveImagenull101   internal func saveImage(_ image: UIImage, options: ManipulateOptions) throws -> SaveImageResult {
102     guard let fileSystem = self.appContext?.fileSystem else {
103       throw FileSystemNotFoundException()
104     }
105     let directory = URL(fileURLWithPath: fileSystem.cachesDirectory).appendingPathComponent("ImageManipulator")
106     let filename = UUID().uuidString.appending(options.format.fileExtension)
107     let fileUrl = directory.appendingPathComponent(filename)
108 
109     fileSystem.ensureDirExists(withPath: directory.path)
110 
111     guard let data = imageData(from: image, format: options.format, compression: options.compress) else {
112       throw CorruptedImageDataException()
113     }
114     do {
115       try data.write(to: fileUrl, options: .atomic)
116     } catch let error {
117       throw ImageWriteFailedException(error.localizedDescription)
118     }
119     return (url: fileUrl, data: data)
120   }
121 }
122 
123 /**
124  Returns pixel data representation of the image.
125  */
imageDatanull126 func imageData(from image: UIImage, format: ImageFormat, compression: Double) -> Data? {
127   switch format {
128   case .jpeg, .jpg:
129     return image.jpegData(compressionQuality: compression)
130   case .png:
131     return image.pngData()
132   }
133 }
134