import CryptoKit import Flutter import MobileCoreServices import Photos // https://stackoverflow.com/a/55839062 extension UIImage { func toData(options: NSDictionary?, type: ImageType) -> Data? { guard cgImage != nil else { return nil } return toData(options: options, type: type.value) } // about properties: https://developer.apple.com/documentation/imageio/1464962-cgimagedestinationaddimage func toData(options: NSDictionary?, type: CFString) -> Data? { guard let cgImage = cgImage else { return nil } return autoreleasepool { () -> Data? in let data = NSMutableData() guard let imageDestination = CGImageDestinationCreateWithData(data as CFMutableData, type, 1, nil) else { return nil } CGImageDestinationAddImage(imageDestination, cgImage, options) CGImageDestinationFinalize(imageDestination) return data as Data } } enum ImageType { case image // abstract image data case jpeg // JPEG image case jpeg2000 // JPEG-2000 image case tiff // TIFF image case pict // Quickdraw PICT format case gif // GIF image case png // PNG image case quickTimeImage // QuickTime image format (OSType 'qtif') case appleICNS // Apple icon data case bmp // Windows bitmap case ico // Windows icon data case rawImage // base type for raw image data (.raw) case scalableVectorGraphics // SVG image case livePhoto // Live Photo var value: CFString { switch self { case .image: return kUTTypeImage case .jpeg: return kUTTypeJPEG case .jpeg2000: return kUTTypeJPEG2000 case .tiff: return kUTTypeTIFF case .pict: return kUTTypePICT case .gif: return kUTTypeGIF case .png: return kUTTypePNG case .quickTimeImage: return kUTTypeQuickTimeImage case .appleICNS: return kUTTypeAppleICNS case .bmp: return kUTTypeBMP case .ico: return kUTTypeICO case .rawImage: return kUTTypeRawImage case .scalableVectorGraphics: return kUTTypeScalableVectorGraphics case .livePhoto: return kUTTypeLivePhoto } } } } class ThumbnailApiImpl: ThumbnailApi { private static let cacheManager = PHImageManager.default() private static let fetchOptions = { let fetchOptions = PHFetchOptions() fetchOptions.fetchLimit = 1 return fetchOptions }() private static let requestOptions = { let requestOptions = PHImageRequestOptions() requestOptions.isNetworkAccessAllowed = true requestOptions.deliveryMode = .highQualityFormat requestOptions.resizeMode = .exact requestOptions.isSynchronous = true requestOptions.version = .current return requestOptions }() private static let processingQueue = DispatchQueue( label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent) func getThumbnail( assetId: String, width: Int64, height: Int64, completion: @escaping (Result) -> Void ) { Self.processingQueue.async { do { let asset = try self.getAsset(assetId: assetId) Self.cacheManager.requestImage( for: asset, targetSize: CGSize(width: Double(width), height: Double(height)), contentMode: .aspectFill, options: Self.requestOptions, resultHandler: { (image, info) -> Void in guard let data = image?.toData(options: nil, type: .bmp) else { return } completion(.success(FlutterStandardTypedData(bytes: data))) } ) } catch { completion( .failure(PigeonError(code: "", message: "Could not get asset data", details: nil))) } } } private func getAsset(assetId: String) throws -> PHAsset { guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions) .firstObject else { throw PigeonError(code: "", message: "Could not fetch asset", details: nil) } return asset } // func cancel(requestId: Int32) { // Self.cacheManager.cancelImageRequest(requestId as PHImageRequestID) // } }