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.") } }