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