Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

On the fly JPG / HEIC conversion #53

Merged
merged 12 commits into from
Jul 26, 2023
Merged
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ let package = Package(
),
.testTarget(
name: "InfomaniakCoreTests",
dependencies: ["InfomaniakCore","ZIPFoundation"]
dependencies: ["InfomaniakCore","ZIPFoundation"],
resources: [Resource.copy("Ressources/Matterhorn_as_seen_from_Zermatt,_Wallis,_Switzerland,_2012_August,Wikimedia_Commons.heic"),
Resource.copy("Ressources/Matterhorn_as_seen_from_Zermatt,_Wallis,_Switzerland,_2012_August,Wikimedia_Commons.jpg")]
)
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -24,54 +24,78 @@ import InfomaniakDI
/// Something that can provide a `Progress` and an async `Result` in order to load an url from a `NSItemProvider`
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class ItemProviderFileRepresentation: NSObject, ProgressResultable {
/// Progress increment size
private static let progressStep: Int64 = 1

/// Number of steps to complete the task
private static let totalSteps: Int64 = 2

/// Something to transform events to a nice `async Result`
private let flowToAsync = FlowToAsyncResult<Success>()

/// Shorthand for default FileManager
private let fileManager = FileManager.default

/// Domain specific errors
public enum ErrorDomain: Error, Equatable{
public enum ErrorDomain: Error, Equatable {
case UTINotFound
case UnableToLoadFile
}

public typealias Success = URL
public typealias Failure = Error

public init(from itemProvider: NSItemProvider) throws {
/// Init method
/// - Parameters:
/// - itemProvider: The item provider we will be working with
/// - preferredImageFileFormat: Specify an output image file format. Supports HEIC and JPG. Will convert only if
/// itemProvider supports it.
public init(from itemProvider: NSItemProvider, preferredImageFileFormat: UTI? = nil) throws {
guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first else {
throw ErrorDomain.UTINotFound
}

// Keep compiler happy
progress = Progress(totalUnitCount: 1)
progress = Progress(totalUnitCount: Self.totalSteps)

super.init()

// Set progress and hook completion closure to a combine pipe
progress = itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { [self] fileProviderURL, error in
// Check if requested an image conversion, and if conversion is available.
let fileIdentifierToUse = self.preferredImageFileFormat(
itemProvider: itemProvider,
typeIdentifier: typeIdentifier,
preferredImageFileFormat: preferredImageFileFormat
)

// Set progress and hook completion closure
let completionProgress = Progress(totalUnitCount: Self.progressStep)
progress.addChild(completionProgress, withPendingUnitCount: Self.progressStep)

let loadURLProgress = itemProvider.loadFileRepresentation(forTypeIdentifier: fileIdentifierToUse) { [self] fileProviderURL, error in
guard let fileProviderURL, error == nil else {
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(error ?? ErrorDomain.UnableToLoadFile)
return
}

do {
let UTI = UTI(rawValue: typeIdentifier as CFString)
let uti = UTI(rawValue: fileIdentifierToUse as CFString)
@InjectService var pathProvider: AppGroupPathProvidable
let temporaryURL = pathProvider.tmpDirectoryURL
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try fileManager.createDirectory(at: temporaryURL, withIntermediateDirectories: true)

let fileName = fileProviderURL.appendingPathExtension(for: UTI).lastPathComponent
let fileName = fileProviderURL.appendingPathExtension(for: uti).lastPathComponent
let temporaryFileURL = temporaryURL.appendingPathComponent(fileName)
try fileManager.copyItem(atPath: fileProviderURL.path, toPath: temporaryFileURL.path)


completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendSuccess(temporaryFileURL)
} catch {
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(error)
}
}
progress.addChild(loadURLProgress, withPendingUnitCount: Self.progressStep)
}

// MARK: ProgressResultable
Expand All @@ -80,7 +104,35 @@ public final class ItemProviderFileRepresentation: NSObject, ProgressResultable

public var result: Result<URL, Error> {
get async {
await self.flowToAsync.result
await flowToAsync.result
}
}

// MARK: Private

/// Check if a File conversion is possible for the provided `itemProvider` and `typeIdentifier`,
/// returns `typeIdentifier` if no conversion is possible.
///
/// - Parameters:
/// - itemProvider: The ItemProvider we work with
/// - typeIdentifier: top typeIdentifier for ItemProvider
/// - preferredImageFileFormat: The image format the user is requesting
private func preferredImageFileFormat(itemProvider: NSItemProvider,
typeIdentifier: String,
preferredImageFileFormat: UTI?) -> String {
if let preferredImageFileFormat = preferredImageFileFormat {
// Check that itemProvider supports the image types we ask of it
if itemProvider.hasItemConformingToAnyOfTypeIdentifiers([UTI.heic.identifier, UTI.jpeg.identifier]),
itemProvider.hasItemConformingToTypeIdentifier(preferredImageFileFormat.identifier) {
return preferredImageFileFormat.identifier
}
// No conversion if not possible
else {
return typeIdentifier
}
} else {
// No conversion
return typeIdentifier
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import InfomaniakDI
/// Something that can provide a `Progress` and an async `Result` in order to make a raw text file from a `NSItemProvider`
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class ItemProviderTextRepresentation: NSObject, ProgressResultable {
/// Progress increment size
private static let progressStep: Int64 = 1

/// Something to transform events to a nice `async Result`
private let flowToAsync = FlowToAsyncResult<Success>()

Expand All @@ -40,28 +43,22 @@ public final class ItemProviderTextRepresentation: NSObject, ProgressResultable
public typealias Success = URL
public typealias Failure = Error

private static let progressStep: Int64 = 1

public init(from itemProvider: NSItemProvider) throws {
guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first else {
throw ErrorDomain.UTINotFound
}

// Keep compiler happy
progress = Progress(totalUnitCount: 1)
progress = Progress(totalUnitCount: Self.progressStep)

super.init()

let childProgress = Progress()
progress.addChild(childProgress, withPendingUnitCount: Self.progressStep)
let completionProgress = Progress()
progress.addChild(completionProgress, withPendingUnitCount: Self.progressStep)

itemProvider.loadItem(forTypeIdentifier: typeIdentifier) { [self] coding, error in
defer {
childProgress.completedUnitCount += Self.progressStep
}

guard error == nil,
let coding else {
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(error ?? ErrorDomain.unknown)
return
}
Expand All @@ -74,43 +71,58 @@ public final class ItemProviderTextRepresentation: NSObject, ProgressResultable
try fileManager.createDirectory(at: temporaryURL, withIntermediateDirectories: true)

// Is String
guard try !stringHandling(coding, temporaryURL: temporaryURL) else {
guard try !stringHandling(coding, temporaryURL: temporaryURL, completionProgress: completionProgress) else {
return
}

// Is Data
guard try !dataHandling(coding, typeIdentifier: typeIdentifier, temporaryURL: temporaryURL) else {
guard try !dataHandling(
coding,
typeIdentifier: typeIdentifier,
temporaryURL: temporaryURL,
completionProgress: completionProgress
) else {
return
}

// Not supported
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(ErrorDomain.UTINotSupported)

} catch {
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(error)
return
}
}
}

private func stringHandling(_ coding: NSSecureCoding, temporaryURL: URL) throws -> Bool {
private func stringHandling(_ coding: NSSecureCoding, temporaryURL: URL, completionProgress: Progress) throws -> Bool {
guard let text = coding as? String else {
// Not matching type, do nothing.
return false
}
let targetURL = temporaryURL.appendingPathComponent("\(UUID().uuidString).txt")

let targetURL = temporaryURL.appendingPathComponent("\(UUID().uuidString).txt")
try text.write(to: targetURL, atomically: true, encoding: .utf8)

completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendSuccess(targetURL)

return true
}

private func dataHandling(_ coding: NSSecureCoding, typeIdentifier: String, temporaryURL: URL) throws -> Bool {
private func dataHandling(_ coding: NSSecureCoding,
typeIdentifier: String,
temporaryURL: URL,
completionProgress: Progress) throws -> Bool {
guard let data = coding as? Data else {
// Not matching type, do nothing.
return false
}

guard let uti = UTI(typeIdentifier) else {
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(ErrorDomain.UTINotFound)
return false
}
Expand All @@ -120,6 +132,8 @@ public final class ItemProviderTextRepresentation: NSObject, ProgressResultable
.appendingPathExtension(for: uti)

try data.write(to: targetURL)

completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendSuccess(targetURL)

return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ import InfomaniakDI
/// Something that can provide a `Progress` and an async `Result` in order to make a webloc plist from a `NSItemProvider`
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class ItemProviderWeblocRepresentation: NSObject, ProgressResultable {
/// Progress increment size
private static let progressStep: Int64 = 1

/// Number of steps to complete the task
private static let totalSteps: Int64 = 2

/// Something to transform events to a nice `async Result`
private let flowToAsync = FlowToAsyncResult<Success>()

Expand All @@ -39,14 +45,17 @@ public final class ItemProviderWeblocRepresentation: NSObject, ProgressResultabl
public typealias Failure = Error

public init(from itemProvider: NSItemProvider) throws {
// Keep compiler happy
progress = Progress(totalUnitCount: 1)
progress = Progress(totalUnitCount: Self.totalSteps)

super.init()

progress = itemProvider.loadObject(ofClass: URL.self) { [self] path, error in
let completionProgress = Progress(totalUnitCount: Self.progressStep)
progress.addChild(completionProgress, withPendingUnitCount: Self.progressStep)

let loadURLProgress = itemProvider.loadObject(ofClass: URL.self) { [self] path, error in
guard error == nil, let path: URL = path else {
let error: Error = error ?? ErrorDomain.unableToLoadURLForObject
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(error)
return
}
Expand All @@ -60,17 +69,20 @@ public final class ItemProviderWeblocRepresentation: NSObject, ProgressResultabl
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try fileManager.createDirectory(at: tmpDirectoryURL, withIntermediateDirectories: true)

let fileName = path.lastPathComponent
let fileName = path.deletingPathExtension().lastPathComponent
let targetURL = tmpDirectoryURL.appendingPathComponent("\(fileName).webloc")
let encoder = PropertyListEncoder()
let data = try encoder.encode(content)
try data.write(to: targetURL)

completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendSuccess(targetURL)
} catch {
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(error)
}
}
progress.addChild(loadURLProgress, withPendingUnitCount: Self.progressStep)
}

// MARK: ProgressResultable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ import InfomaniakDI
/// Something that can provide a `Progress` and an async `Result` in order to make a zip from a `NSItemProvider`
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class ItemProviderZipRepresentation: NSObject, ProgressResultable {
/// Coordinator for file operations
let coordinator = NSFileCoordinator()

/// Progress increment size
private static let progressStep: Int64 = 1

/// Number of steps to complete the task
private static let totalSteps: Int64 = 2

/// Something to transform events to a nice `async Result`
private let flowToAsync = FlowToAsyncResult<Success>()

Expand All @@ -40,23 +49,22 @@ public final class ItemProviderZipRepresentation: NSObject, ProgressResultable {
public typealias Success = URL
public typealias Failure = Error

private static let progressStep: Int64 = 1

public init(from itemProvider: NSItemProvider) throws {
// It must be a directory for the OS to zip it for us, a file returns a file
guard itemProvider.underlyingType == .isDirectory else {
throw ErrorDomain.notADirectory
}

// Keep compiler happy
progress = Progress(totalUnitCount: 1)
progress = Progress(totalUnitCount: Self.totalSteps)

super.init()

let coordinator = NSFileCoordinator()

progress = itemProvider.loadObject(ofClass: URL.self) { [self] path, error in
let completionProgress = Progress(totalUnitCount: Self.progressStep)
progress.addChild(completionProgress, withPendingUnitCount: Self.progressStep)

let loadURLProgress = itemProvider.loadObject(ofClass: URL.self) { [self] path, error in
guard error == nil, let path: URL = path else {
completionProgress.completedUnitCount += Self.progressStep
flowToAsync.sendFailure(error ?? ErrorDomain.unableToLoadURLForObject)
return
}
Expand All @@ -66,10 +74,6 @@ public final class ItemProviderZipRepresentation: NSObject, ProgressResultable {
// > If you’d like to see such support [ie. for NSProgress] added in the future, I encourage you to file an
// enhancement request

// Minimalist progress file processing support
let childProgress = Progress()
progress.addChild(childProgress, withPendingUnitCount: Self.progressStep)

// compress content of folder and move it somewhere we can safely store it for upload
var error: NSError?
coordinator.coordinate(readingItemAt: path, options: [.forUploading], error: &error) { zipURL in
Expand All @@ -83,13 +87,16 @@ public final class ItemProviderZipRepresentation: NSObject, ProgressResultable {
let targetURL = tmpDirectoryURL.appendingPathComponent("\(fileName).zip")

try self.fileManager.moveItem(at: zipURL, to: targetURL)

completionProgress.completedUnitCount += Self.progressStep
self.flowToAsync.sendSuccess(targetURL)
} catch {
completionProgress.completedUnitCount += Self.progressStep
self.flowToAsync.sendFailure(error)
}
childProgress.completedUnitCount += Self.progressStep
}
}
progress.addChild(loadURLProgress, withPendingUnitCount: Self.progressStep)
}

// MARK: ProgressResultable
Expand Down
Loading