import Foundation import Photos import UIKit final class FileProcessing { static let shared = FileProcessing() static var fileFolder: URL { // swiftlint:disable:next force_unwrapping let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let subdirectoryURL = documentsURL.appendingPathComponent(Const.fileFolder) if !FileManager.default.fileExists(atPath: subdirectoryURL.path) { try? FileManager.default.createDirectory(at: subdirectoryURL, withIntermediateDirectories: true, attributes: nil) } return subdirectoryURL } func createThumbnail(localName: String) -> String? { let thumbnailFileName = "thumb_\(localName)" let thumbnailUrl = FileProcessing.fileFolder.appendingPathComponent(thumbnailFileName) let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName) // check if thumbnail already exists if FileManager.default.fileExists(atPath: thumbnailUrl.path) { return thumbnailFileName } // create thumbnail if not exists switch localName.attachmentType { case .image: guard let image = UIImage(contentsOfFile: localUrl.path) else { print("FileProcessing: Error loading image: \(localUrl)") return nil } let targetSize = CGSize(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) guard let thumbnail = scaleAndCropImage(image, targetSize) else { print("FileProcessing: Error scaling image: \(localUrl)") return nil } guard let data = thumbnail.pngData() else { print("FileProcessing: Error converting thumbnail of \(localUrl) to data") return nil } do { try data.write(to: thumbnailUrl) return thumbnailFileName } catch { print("FileProcessing: Error writing thumbnail: \(error)") return nil } default: return nil } } func fetchGallery() -> [SharingGalleryItem] { let items = syncGalleryEnumerate() .map { SharingGalleryItem( id: $0.localIdentifier, type: $0.mediaType == .image ? .photo : .video, duration: $0.mediaType == .video ? $0.duration.minAndSec : nil ) } return items } func fillGalleryItemsThumbnails(items: [SharingGalleryItem]) -> [SharingGalleryItem] { let ids = items .filter { $0.thumbnail == nil } .map { $0.id } let assets = syncGalleryEnumerate(ids) return assets.compactMap { asset in if asset.mediaType == .image { return syncGalleryProcessImage(asset) { [weak self] image in if let thumbnail = self?.scaleAndCropImage(image, CGSize(width: Const.galleryGridSize, height: Const.galleryGridSize)) { let data = thumbnail.jpegData(compressionQuality: 1.0) ?? Data() return SharingGalleryItem(id: asset.localIdentifier, type: .photo, thumbnail: data) } else { return nil } } } else if asset.mediaType == .video { return syncGalleryProcessVideo(asset) { [weak self] avAsset in // swiftlint:disable:next force_cast let assetURL = avAsset as! AVURLAsset let url = assetURL.url if let thumbnail = self?.generateVideoThumbnail(url, CGSize(width: Const.galleryGridSize, height: Const.galleryGridSize)) { let data = thumbnail.jpegData(compressionQuality: 1.0) ?? Data() return SharingGalleryItem( id: asset.localIdentifier, type: .video, thumbnail: data, duration: asset.duration.minAndSec ) } else { return nil } } } else { return nil } } } // This function also creates new ids for messages for each new attachment func copyGalleryItemsForUploading(items: [SharingGalleryItem]) -> [(String, String)] { let assets = syncGalleryEnumerate(items.map { $0.id }) return assets .compactMap { asset in let newMessageId = UUID().uuidString let fileId = UUID().uuidString if asset.mediaType == .image { return syncGalleryProcessImage(asset) { image in let localName = "\(newMessageId)_\(fileId).jpg" let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName) if let data = image.jpegData(compressionQuality: 1.0) { do { try data.write(to: localUrl) return (newMessageId, localName) } catch { return nil } } else { return nil } } } else if asset.mediaType == .video { return syncGalleryProcessVideo(asset) { avAsset in // swiftlint:disable:next force_cast let assetURL = avAsset as! AVURLAsset let url = assetURL.url let localName = "\(newMessageId)_\(fileId).mov" let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName) do { try FileManager.default.copyItem(at: url, to: localUrl) return (newMessageId, localName) } catch { return nil } } } else { return nil } } } // This function also creates new id for file from camera capturing func copyCameraCapturedForUploading(media: Data, type: SharingCameraMediaType) -> (String, String)? { let newMessageId = UUID().uuidString let fileId = UUID().uuidString let localName: String switch type { case .photo: localName = "\(newMessageId)_\(fileId).jpg" case .video: localName = "\(newMessageId)_\(fileId).mov" } let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName) do { try media.write(to: localUrl) return (newMessageId, localName) } catch { return nil } } // This function also creates new id for file from document sharing func copyDocumentsForUploading(data: [Data], extensions: [String]) -> [(String, String)] { data.enumerated().compactMap { index, data in let newMessageId = UUID().uuidString let fileId = UUID().uuidString let localName = "\(newMessageId)_\(fileId).\(extensions[index])" let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName) do { try data.write(to: localUrl) return (newMessageId, localName) } catch { print("FileProcessing: Error writing document: \(error)") return nil } } } } private extension FileProcessing { func scaleAndCropImage(_ img: UIImage, _ size: CGSize) -> UIImage? { let aspect = img.size.width / img.size.height let targetAspect = size.width / size.height var newWidth: CGFloat var newHeight: CGFloat if aspect < targetAspect { newWidth = size.width newHeight = size.width / aspect } else { newHeight = size.height newWidth = size.height * aspect } UIGraphicsBeginImageContextWithOptions(size, false, 0.0) img.draw(in: CGRect(x: (size.width - newWidth) / 2, y: (size.height - newHeight) / 2, width: newWidth, height: newHeight)) let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return newImage } func syncGalleryEnumerate(_ ids: [String]? = nil) -> [PHAsset] { var result: [PHAsset] = [] let group = DispatchGroup() DispatchQueue.global(qos: .userInitiated).sync { let fetchOptions = PHFetchOptions() fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] if let ids { fetchOptions.predicate = NSPredicate(format: "localIdentifier IN %@", ids) } let assets = PHAsset.fetchAssets(with: fetchOptions) assets.enumerateObjects { asset, _, _ in group.enter() result.append(asset) group.leave() } } group.wait() return result } func syncGalleryProcess(_ assets: [PHAsset], _ block: @escaping (PHAsset) -> T) -> [T] { var result: [T] = [] let group = DispatchGroup() DispatchQueue.global(qos: .userInitiated).sync { for asset in assets { group.enter() let res = block(asset) result.append(res) group.leave() } } group.wait() return result } func syncGalleryProcessImage(_ asset: PHAsset, _ block: @escaping (UIImage) -> T?) -> T? { var result: T? let semaphore = DispatchSemaphore(value: 0) DispatchQueue.global(qos: .userInitiated).sync { let options = PHImageRequestOptions() options.version = .original options.isSynchronous = true PHImageManager.default().requestImage( for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: options ) { image, _ in if let image { result = block(image) } else { result = nil } semaphore.signal() } } semaphore.wait() return result } func syncGalleryProcessVideo(_ asset: PHAsset, _ block: @escaping (AVAsset) -> T?) -> T? { var result: T? let semaphore = DispatchSemaphore(value: 0) _ = DispatchQueue.global(qos: .userInitiated).sync { PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in if let avAsset { result = block(avAsset) } else { result = nil } semaphore.signal() } } semaphore.wait() return result } func generateVideoThumbnail(_ url: URL, _ size: CGSize) -> UIImage? { let asset = AVAsset(url: url) let assetImgGenerate = AVAssetImageGenerator(asset: asset) assetImgGenerate.appliesPreferredTrackTransform = true let time = CMTimeMakeWithSeconds(Float64(1), preferredTimescale: 600) do { let cgImage = try assetImgGenerate.copyCGImage(at: time, actualTime: nil) let image = UIImage(cgImage: cgImage) return scaleAndCropImage(image, size) } catch { return nil } } }