conversations-classic-ios/ConversationsClassic/AppCore/Files/FileProcessing.swift

285 lines
11 KiB
Swift
Raw Normal View History

2024-07-13 01:29:46 +00:00
import Foundation
2024-07-14 13:42:51 +00:00
import Photos
2024-07-13 01:29:46 +00:00
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!
2024-07-14 08:52:15 +00:00
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
2024-07-13 01:29:46 +00:00
}
2024-07-14 10:08:51 +00:00
func createThumbnail(localName: String) -> String? {
let thumbnailFileName = "thumb_\(localName)"
2024-07-14 08:52:15 +00:00
let thumbnailUrl = FileProcessing.fileFolder.appendingPathComponent(thumbnailFileName)
2024-07-14 10:08:51 +00:00
let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName)
2024-07-13 01:29:46 +00:00
// check if thumbnail already exists
if FileManager.default.fileExists(atPath: thumbnailUrl.path) {
2024-07-14 10:08:51 +00:00
return thumbnailFileName
2024-07-13 01:29:46 +00:00
}
// create thumbnail if not exists
2024-07-14 10:08:51 +00:00
switch localName.attachmentType {
2024-07-13 01:29:46 +00:00
case .image:
2024-07-16 15:13:16 +00:00
guard let image = UIImage(contentsOfFile: localUrl.path) else {
print("FileProcessing: Error loading image: \(localUrl)")
return nil
}
2024-07-13 01:29:46 +00:00
let targetSize = CGSize(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
2024-07-16 15:13:16 +00:00
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
}
2024-07-13 01:29:46 +00:00
do {
try data.write(to: thumbnailUrl)
2024-07-14 10:08:51 +00:00
return thumbnailFileName
2024-07-13 01:29:46 +00:00
} catch {
2024-07-16 15:13:16 +00:00
print("FileProcessing: Error writing thumbnail: \(error)")
2024-07-13 01:29:46 +00:00
return nil
}
default:
return nil
}
}
2024-07-14 13:42:51 +00:00
func fetchGallery() -> [SharingGalleryItem] {
2024-07-14 15:02:41 +00:00
let items = syncGalleryEnumerate()
.map {
SharingGalleryItem(
id: $0.localIdentifier,
type: $0.mediaType == .image ? .photo : .video,
duration: $0.mediaType == .video ? $0.duration.minAndSec : nil
)
2024-07-14 13:42:51 +00:00
}
return items
}
func fillGalleryItemsThumbnails(items: [SharingGalleryItem]) -> [SharingGalleryItem] {
let ids = items
.filter { $0.thumbnail == nil }
.map { $0.id }
2024-07-14 15:02:41 +00:00
let assets = syncGalleryEnumerate(ids)
return assets.compactMap { asset in
2024-07-14 13:42:51 +00:00
if asset.mediaType == .image {
2024-07-14 15:02:41 +00:00
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
2024-07-14 13:42:51 +00:00
}
}
} else if asset.mediaType == .video {
2024-07-14 15:02:41 +00:00
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
2024-07-14 13:42:51 +00:00
}
}
2024-07-14 15:02:41 +00:00
} else {
return nil
2024-07-14 13:42:51 +00:00
}
}
}
2024-07-14 16:53:33 +00:00
// 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
}
}
}
2024-07-16 11:48:50 +00:00
// 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
}
}
2024-07-13 01:29:46 +00:00
}
2024-07-14 13:42:51 +00:00
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
2024-07-13 16:37:26 +00:00
}
2024-07-13 01:29:46 +00:00
2024-07-14 15:02:41 +00:00
func syncGalleryEnumerate(_ ids: [String]? = nil) -> [PHAsset] {
2024-07-14 13:42:51 +00:00
var result: [PHAsset] = []
2024-07-13 01:29:46 +00:00
2024-07-14 15:02:41 +00:00
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()
}
2024-07-14 13:42:51 +00:00
}
2024-07-14 15:02:41 +00:00
group.wait()
return result
}
func syncGalleryProcess<T>(_ 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()
}
2024-07-14 13:42:51 +00:00
}
2024-07-14 15:02:41 +00:00
group.wait()
2024-07-14 13:42:51 +00:00
return result
}
2024-07-14 15:02:41 +00:00
func syncGalleryProcessImage<T>(_ 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<T>(_ 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
}
}
2024-07-13 01:29:46 +00:00
}