2020-12-16 02:39:38 +01:00
|
|
|
// Copyright © 2020 Metabolist. All rights reserved.
|
|
|
|
|
|
|
|
import Combine
|
|
|
|
import Foundation
|
|
|
|
import ImageIO
|
2021-03-05 04:07:44 +01:00
|
|
|
import UIKit
|
2020-12-16 02:39:38 +01:00
|
|
|
import UniformTypeIdentifiers
|
|
|
|
|
|
|
|
enum MediaProcessingError: Error {
|
|
|
|
case invalidMimeType
|
|
|
|
case fileURLNotFound
|
|
|
|
case unsupportedType
|
|
|
|
case unableToCreateImageSource
|
|
|
|
case unableToDownsample
|
|
|
|
case unableToCreateImageDataDestination
|
|
|
|
}
|
|
|
|
|
2021-01-08 19:06:21 +01:00
|
|
|
public enum MediaProcessingService {}
|
2020-12-16 02:39:38 +01:00
|
|
|
|
|
|
|
public extension MediaProcessingService {
|
2020-12-17 07:48:06 +01:00
|
|
|
static func dataAndMimeType(itemProvider: NSItemProvider) -> AnyPublisher<(data: Data, mimeType: String), Error> {
|
2020-12-16 02:39:38 +01:00
|
|
|
let registeredTypes = itemProvider.registeredTypeIdentifiers.compactMap(UTType.init)
|
|
|
|
|
2021-03-05 04:07:44 +01:00
|
|
|
let mimeType: String
|
2021-03-05 08:29:05 +01:00
|
|
|
let dataPublisher: AnyPublisher<Data, Error>
|
2021-03-05 04:07:44 +01:00
|
|
|
|
2021-03-05 08:29:05 +01:00
|
|
|
if let uniformType = registeredTypes.first(where: {
|
2020-12-16 02:39:38 +01:00
|
|
|
guard let mimeType = $0.preferredMIMEType else { return false }
|
|
|
|
|
2020-12-17 07:48:06 +01:00
|
|
|
return Self.uploadableMimeTypes.contains(mimeType)
|
2021-03-05 08:29:05 +01:00
|
|
|
}), let preferredMIMEType = uniformType.preferredMIMEType {
|
2021-03-05 04:07:44 +01:00
|
|
|
mimeType = preferredMIMEType
|
2021-03-05 08:29:05 +01:00
|
|
|
dataPublisher = Future<Data, Error> { promise in
|
|
|
|
itemProvider.loadFileRepresentation(forTypeIdentifier: uniformType.identifier) { url, error in
|
|
|
|
if let error = error {
|
|
|
|
promise(.failure(error))
|
|
|
|
} else if let url = url {
|
|
|
|
if uniformType.conforms(to: .image) && uniformType != .gif {
|
|
|
|
promise(imageData(url: url, type: uniformType))
|
|
|
|
} else {
|
|
|
|
promise(Result { try Data(contentsOf: url) })
|
|
|
|
}
|
2021-01-05 23:38:15 +01:00
|
|
|
} else {
|
2021-03-05 08:29:05 +01:00
|
|
|
promise(.failure(MediaProcessingError.fileURLNotFound))
|
2020-12-16 02:39:38 +01:00
|
|
|
}
|
2021-03-05 08:29:05 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
.eraseToAnyPublisher()
|
|
|
|
} else if registeredTypes == [UTType.image], let pngMIMEType = UTType.png.preferredMIMEType { // screenshot
|
|
|
|
mimeType = pngMIMEType
|
|
|
|
dataPublisher = Future<Data, Error> { promise in
|
|
|
|
itemProvider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { item, error in
|
|
|
|
if let error = error {
|
|
|
|
promise(.failure(error))
|
|
|
|
} else if let image = item as? UIImage, let data = image.pngData() {
|
|
|
|
do {
|
|
|
|
let url = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
|
|
|
.appendingPathComponent(UUID().uuidString)
|
2021-03-05 04:07:44 +01:00
|
|
|
|
2021-03-05 08:29:05 +01:00
|
|
|
try data.write(to: url)
|
2021-03-05 04:07:44 +01:00
|
|
|
|
2021-03-05 08:29:05 +01:00
|
|
|
promise(imageData(url: url, type: .png))
|
|
|
|
} catch {
|
|
|
|
promise(.failure(error))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
promise(.failure(MediaProcessingError.fileURLNotFound))
|
2021-03-05 04:07:44 +01:00
|
|
|
}
|
2020-12-16 02:39:38 +01:00
|
|
|
}
|
|
|
|
}
|
2021-03-05 08:29:05 +01:00
|
|
|
.eraseToAnyPublisher()
|
|
|
|
} else {
|
|
|
|
return Fail(error: MediaProcessingError.invalidMimeType).eraseToAnyPublisher()
|
2020-12-16 02:39:38 +01:00
|
|
|
}
|
2021-03-05 08:29:05 +01:00
|
|
|
|
|
|
|
return dataPublisher.map { (data: $0, mimeType: mimeType) }.eraseToAnyPublisher()
|
2020-12-16 02:39:38 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private extension MediaProcessingService {
|
2020-12-17 07:48:06 +01:00
|
|
|
static let uploadableMimeTypes = Set(
|
|
|
|
[UTType.png,
|
|
|
|
UTType.jpeg,
|
|
|
|
UTType.gif,
|
|
|
|
UTType.webP,
|
|
|
|
UTType.mpeg4Movie,
|
|
|
|
UTType.quickTimeMovie,
|
|
|
|
UTType.mp3,
|
|
|
|
UTType.wav]
|
|
|
|
.compactMap(\.preferredMIMEType))
|
2020-12-16 02:39:38 +01:00
|
|
|
static let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
|
|
|
|
static let thumbnailOptions = [
|
|
|
|
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
|
|
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
|
|
kCGImageSourceThumbnailMaxPixelSize: 1280
|
|
|
|
] as CFDictionary
|
|
|
|
|
|
|
|
static func imageData(url: URL, type: UTType) -> Result<Data, Error> {
|
|
|
|
guard let source = CGImageSourceCreateWithURL(url as CFURL, Self.imageSourceOptions) else {
|
|
|
|
return .failure(MediaProcessingError.unableToCreateImageSource)
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, thumbnailOptions) else {
|
|
|
|
return .failure(MediaProcessingError.unableToDownsample)
|
|
|
|
}
|
|
|
|
|
|
|
|
let data = NSMutableData()
|
|
|
|
|
|
|
|
guard let imageDestination = CGImageDestinationCreateWithData(data, type.identifier as CFString, 1, nil) else {
|
|
|
|
return .failure(MediaProcessingError.unableToCreateImageDataDestination)
|
|
|
|
}
|
|
|
|
|
|
|
|
CGImageDestinationAddImage(imageDestination, image, nil)
|
|
|
|
CGImageDestinationFinalize(imageDestination)
|
|
|
|
|
|
|
|
return .success(data as Data)
|
|
|
|
}
|
|
|
|
}
|