1 // Copyright 2021-present 650 Industries. All rights reserved. 2 3 import CoreGraphics 4 import Photos 5 import UIKit 6 import ABI47_0_0ExpoModulesCore 7 8 public class ImageManipulatorModule: Module { 9 typealias LoadImageCallback = (Result<UIImage, Error>) -> Void 10 typealias SaveImageResult = (url: URL, data: Data) 11 12 public func definition() -> ModuleDefinition { 13 Name("ExpoImageManipulator") 14 15 AsyncFunction("manipulateAsync", manipulateImage) 16 .runOnQueue(.main) 17 } 18 19 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 */ 45 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 */ 78 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 */ 101 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 */ 126 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