import SDWebImageWebPCoder import Photos import ExpoModulesCore internal typealias SaveImageResult = (url: URL, data: Data) /** Loads the image from given URL. */ internal func loadImage(atUrl url: URL, appContext: AppContext) async throws -> UIImage { if url.scheme == "data" { guard let data = try? Data(contentsOf: url), let image = UIImage(data: data) else { throw CorruptedImageDataException() } return image } if url.scheme == "ph" || url.scheme == "assets-library" { return try await loadImageFromPhotoLibrary(url: url) } guard let imageLoader = appContext.imageLoader else { throw ImageLoaderNotFoundException() } guard FileSystemUtilities.permissions(appContext, for: url).contains(.read) && FileManager.default.isReadableFile(atPath: url.path) else { throw FileSystemReadPermissionException(url.absoluteString) } do { if let result = try await imageLoader.loadImage(for: url) { return result } } catch { throw ImageLoadingFailedException((error as NSError).debugDescription) } // TODO: throw something better throw ImageLoadingFailedException("") } /** Loads the image from user's photo library. */ internal func loadImageFromPhotoLibrary(url: URL) async throws -> UIImage { guard let asset = retrieveAsset(from: url) else { throw ImageNotFoundException() } let size = CGSize(width: asset.pixelWidth, height: asset.pixelHeight) let options = PHImageRequestOptions() options.resizeMode = .exact options.isNetworkAccessAllowed = true options.isSynchronous = true options.deliveryMode = .highQualityFormat return try await withCheckedThrowingContinuation { continuation in PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFit, options: options) { image, _ in if let image { continuation.resume(returning: image) } else { continuation.resume(throwing: ImageNotFoundException()) } } } } /** Returns pixel data representation of the image. */ func imageData(from image: UIImage, format: ImageFormat, compression: Double) -> Data? { switch format { case .jpeg, .jpg: return image.jpegData(compressionQuality: compression) case .png: return image.pngData() case .webp: return SDImageWebPCoder.shared.encodedData(with: image, format: .webP, options: [.encodeCompressionQuality: compression]) } } /** Checks if we are dealing with a ph asset URL and uses the correct method to fetch it. */ func retrieveAsset(from url: URL) -> PHAsset? { if url.scheme == "ph" { let identifier = String(url.absoluteString.dropFirst(5)) // removes ph:// return PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil).firstObject } return PHAsset.fetchAssets(withALAssetURLs: [url], options: nil).firstObject } /** Helper function for drawing the image in graphics context. Throws appropriate exceptions when the context is missing or the image couldn't be rendered. */ internal func drawInNewContext(size: CGSize, drawing: (UIGraphicsImageRendererContext) -> Void) -> UIImage { let format = UIGraphicsImageRendererFormat() format.scale = 1 let renderer = UIGraphicsImageRenderer(size: size, format: format) return renderer.image { context in drawing(context) } } /** Saves the image as a file. */ internal func saveImage(_ image: UIImage, options: ManipulateOptions, appContext: AppContext) throws -> SaveImageResult { guard let cachesDirectory = appContext.config.cacheDirectory else { throw FileSystemNotFoundException() } let directory = URL(fileURLWithPath: cachesDirectory.path).appendingPathComponent("ImageManipulator") let filename = UUID().uuidString.appending(options.format.fileExtension) let fileUrl = directory.appendingPathComponent(filename) FileSystemUtilities.ensureDirExists(at: directory) guard let data = imageData(from: image, format: options.format, compression: options.compress) else { throw CorruptedImageDataException() } do { try data.write(to: fileUrl, options: .atomic) } catch let error { throw ImageWriteFailedException(error.localizedDescription) } return (url: fileUrl, data: data) }