Drag files in Finder, hold a modifier, and drop on a wheel of outputs. Conversions run entirely on-device through ImageIO, PDFKit, AVFoundation, the bundled CLI tools, and optional ffmpeg/LibreOffice. Includes a --self-test suite (41 checks) and a --convert CLI mode so the conversion matrix can be verified without the UI.
67 lines
3.0 KiB
Swift
67 lines
3.0 KiB
Swift
import Foundation
|
|
|
|
/// Where a produced file goes, and what it is called.
|
|
enum OutputNaming {
|
|
/// ``name.pdf`` for ``photo.jpg`` — appends ``-2``, ``-3`` … when the
|
|
/// target already exists (unless the user asked to overwrite).
|
|
static func destination(for source: URL, format: Format, settings: Settings = .shared) -> URL {
|
|
let base = source.deletingPathExtension().lastPathComponent
|
|
let directory: URL
|
|
switch settings.outputLocation {
|
|
case .besideOriginal:
|
|
directory = source.deletingLastPathComponent()
|
|
case .chosenFolder:
|
|
let folder = settings.resolvedOutputFolder
|
|
directory = FileManager.default.fileExists(atPath: folder.path)
|
|
? folder
|
|
: source.deletingLastPathComponent()
|
|
}
|
|
let candidate = directory.appendingPathComponent(base).appendingPathExtension(format.ext)
|
|
return settings.conflictPolicy == .overwrite ? candidate : availableURL(for: candidate)
|
|
}
|
|
|
|
/// A sibling of any output a tool produces (e.g. ``photo-compressed.jpg``).
|
|
static func sibling(for source: URL, suffix: String, extension ext: String? = nil) -> URL {
|
|
let base = source.deletingPathExtension().lastPathComponent
|
|
let directory = source.deletingLastPathComponent()
|
|
let resolvedExtension = ext ?? source.pathExtension
|
|
return availableURL(
|
|
for: directory.appendingPathComponent(base + suffix).appendingPathExtension(resolvedExtension)
|
|
)
|
|
}
|
|
|
|
/// A working directory for multi-file outputs (split pages, extracted
|
|
/// images, unarchived trees).
|
|
static func artifactDirectory(for source: URL, suffix: String) -> URL {
|
|
let base = source.deletingPathExtension().lastPathComponent
|
|
let directory = source.deletingLastPathComponent()
|
|
let url = directory.appendingPathComponent(base + suffix, isDirectory: true)
|
|
var candidate = url
|
|
var index = 2
|
|
while FileManager.default.fileExists(atPath: candidate.path) {
|
|
candidate = directory.appendingPathComponent("\(base + suffix) \(index)", isDirectory: true)
|
|
index += 1
|
|
}
|
|
return candidate
|
|
}
|
|
|
|
/// ``foo 2.txt``, ``foo 3.txt`` … until the name is free.
|
|
static func availableURL(for url: URL) -> URL {
|
|
guard FileManager.default.fileExists(atPath: url.path) else { return url }
|
|
let directory = url.deletingLastPathComponent()
|
|
let ext = url.pathExtension
|
|
let base = url.deletingPathExtension().lastPathComponent
|
|
var index = 2
|
|
while true {
|
|
let name = ext.isEmpty ? "\(base) \(index)" : "\(base) \(index).\(ext)"
|
|
let candidate = directory.appendingPathComponent(name)
|
|
if !FileManager.default.fileExists(atPath: candidate.path) { return candidate }
|
|
index += 1
|
|
}
|
|
}
|
|
|
|
static func uniqueChild(of directory: URL, suggestedName: String) -> URL {
|
|
availableURL(for: directory.appendingPathComponent(suggestedName))
|
|
}
|
|
}
|