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.
114 lines
4.4 KiB
Swift
114 lines
4.4 KiB
Swift
import AppKit
|
|
import Foundation
|
|
import PDFKit
|
|
|
|
/// Document conversions. `textutil` handles the Office formats; Markdown and
|
|
/// EPUB are ours; PDF output lays text out with CoreText unless LibreOffice is
|
|
/// available for pixel-perfect fidelity.
|
|
enum DocumentConverter {
|
|
static func convert(
|
|
source: URL,
|
|
to format: Format,
|
|
at destination: URL,
|
|
cancellation: Cancellation? = nil,
|
|
progress: ((Double) -> Void)? = nil
|
|
) throws {
|
|
try cancellation?.check()
|
|
let sourceExtension = FileKind.canonicalExtension(source.pathExtension)
|
|
|
|
switch format {
|
|
case .txt:
|
|
guard let text = TextDocument.plainText(from: source) else {
|
|
throw ConversionError.unreadable(source.lastPathComponent)
|
|
}
|
|
try TextDocument.write(text, to: destination)
|
|
|
|
case .md:
|
|
if sourceExtension == "md" || sourceExtension == "txt" {
|
|
let text = TextDocument.plainText(from: source) ?? ""
|
|
try TextDocument.write(text, to: destination)
|
|
} else {
|
|
let html = try TextDocument.html(from: source)
|
|
try TextDocument.write(Markdown.markdown(fromHTML: html), to: destination)
|
|
}
|
|
|
|
case .html:
|
|
try TextDocument.write(try TextDocument.html(from: source), to: destination)
|
|
|
|
case .pdf:
|
|
try makePDF(from: source, at: destination, cancellation: cancellation, progress: progress)
|
|
|
|
case .epub:
|
|
let title = source.deletingPathExtension().lastPathComponent
|
|
let html = try TextDocument.html(from: source)
|
|
let chapterTitle = title
|
|
try EbookConverter.write(
|
|
title: title,
|
|
author: "Konverter",
|
|
chapters: [(chapterTitle, html)],
|
|
at: destination
|
|
)
|
|
|
|
case .docx, .doc, .rtf, .odt:
|
|
if sourceExtension == "pdf" || sourceExtension == "epub" {
|
|
// No importer for these: start from extracted text/HTML.
|
|
let html = try TextDocument.html(from: source)
|
|
try TextDocument.textutilConvert(sourceHTML: html, to: format, at: destination, cancellation: cancellation)
|
|
} else if sourceExtension == "md" {
|
|
let html = try TextDocument.html(from: source)
|
|
try TextDocument.textutilConvert(sourceHTML: html, to: format, at: destination, cancellation: cancellation)
|
|
} else {
|
|
try TextDocument.textutilConvert(source, to: format, at: destination, cancellation: cancellation)
|
|
}
|
|
|
|
case .pptx, .xlsx, .ods, .pages:
|
|
try Office.convert(source, to: format, at: destination, cancellation: cancellation)
|
|
|
|
default:
|
|
throw ConversionError.unsupported("Documents cannot become \(format.displayName).")
|
|
}
|
|
}
|
|
|
|
private static func makePDF(
|
|
from source: URL,
|
|
at destination: URL,
|
|
cancellation: Cancellation?,
|
|
progress: ((Double) -> Void)?
|
|
) throws {
|
|
let sourceExtension = FileKind.canonicalExtension(source.pathExtension)
|
|
if sourceExtension == "pdf" {
|
|
// "PDF → PDF" means a normalised, linearised copy.
|
|
if let document = PDFDocument(url: source) {
|
|
guard document.write(to: destination) else {
|
|
throw ConversionError.failed("Could not rewrite the PDF.")
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
if Toolchain.hasLibreOffice, Settings.shared.useExternalTools {
|
|
do {
|
|
try Office.convert(source, to: .pdf, at: destination, cancellation: cancellation)
|
|
return
|
|
} catch is CancellationError {
|
|
throw CancellationError()
|
|
} catch {
|
|
// Fall through to the CoreText layout rather than failing the
|
|
// whole job because LibreOffice refused one file.
|
|
}
|
|
}
|
|
|
|
if let attributed = TextDocument.attributedString(from: source) {
|
|
try TextDocument.writePDF(attributed, to: destination)
|
|
progress?(1)
|
|
return
|
|
}
|
|
if let text = TextDocument.plainText(from: source), !text.isEmpty {
|
|
try TextDocument.writePDF(fromText: text, to: destination)
|
|
progress?(1)
|
|
return
|
|
}
|
|
throw ConversionError.failed("Could not lay out \(source.lastPathComponent) as a PDF.")
|
|
}
|
|
}
|