import AppKit import CoreGraphics import Foundation /// `--self-test` exercises the real conversion paths end to end and exits /// non-zero on failure. Kept in the shipped binary so a build can be verified /// without opening the UI. enum SelfTest { private static var passes = 0 private static var failures = 0 private static var directory = FileManager.default.temporaryDirectory static func run() -> Int32 { directory = FileManager.default.temporaryDirectory .appendingPathComponent("konverter-selftest-\(UUID().uuidString)", isDirectory: true) try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: directory) } print("Konverter self-test") print("Working in \(directory.path)") print("Engines: \(Toolchain.summary)") print("") testImages() testPDF() testSubtitles() testSpreadsheets() testArchives() testDocuments() testAudio() testEbook() testGeometry() print("") print("\(passes) passed, \(failures) failed") return failures == 0 ? 0 : 1 } // MARK: - Suites private static func testImages() { section("Images") let source = directory.appendingPathComponent("sample.png") check("create sample png") { try makeImage(at: source, width: 320, height: 200) try requireFile(source) } for format in [Format.jpg, .tiff, .gif, .pdf, .webp, .heic, .avif] { let availability = Converter.availability(target: format, source: .image) guard availability.isAvailable else { print("SKIP png → \(format.displayName) (\(availability.reason ?? "unavailable"))") continue } check("png → \(format.displayName)") { let output = directory.appendingPathComponent("out.\(format.ext)") try ImageConverter.convert(source: source, to: format, at: output) try requireFile(output) } } check("image compress") { let output = directory.appendingPathComponent("compressed.jpg") try ImageTools.compress(source, at: output, format: .jpg, quality: 0.4) try requireFile(output) } // PNG has no quality knob, so "compress" trades colour depth for size. check("png compress shrinks a lossless file") { let output = directory.appendingPathComponent("compressed.png") try ImageTools.compress(source, at: output, format: .png, quality: 0.3) try requireFile(output) let before = try fileSize(source) let after = try fileSize(output) guard after < before else { throw TestFailure("expected a smaller png, got \(before) → \(after) bytes") } } check("image resize") { let output = directory.appendingPathComponent("resized.png") try ImageTools.resize(source, at: output, maxWidth: 100, maxHeight: 100, percent: 100) guard let image = Raster.firstImage(at: output), image.width <= 100 else { throw TestFailure("resize did not shrink the image") } } check("image rotate") { let output = directory.appendingPathComponent("rotated.png") try ImageTools.rotate(source, at: output, degrees: 90) guard let image = Raster.firstImage(at: output), image.height > image.width else { throw TestFailure("rotation did not swap dimensions") } } } private static func testPDF() { section("PDF") let image = directory.appendingPathComponent("sample.png") let pdf = directory.appendingPathComponent("sample.pdf") check("image → pdf") { try PDFConverter.imageToPDF(image, at: pdf) try requireFile(pdf) } check("pdf text extraction") { let textPDF = directory.appendingPathComponent("text.pdf") try TextDocument.writePDF(fromText: "Hello Konverter", to: textPDF) let text = PDFConverter.extractText(textPDF) guard text.contains("Hello") else { throw TestFailure("no text found in \(text)") } } check("pdf → png pages") { let folder = directory.appendingPathComponent("pages") let written = try PDFConverter.renderPages(pdf, to: .png, into: folder, dpi: 72) guard !written.isEmpty else { throw TestFailure("no pages rendered") } try requireFile(written[0]) } check("pdf split and merge") { let split = try PDFConverter.split(pdf, pagesPerFile: 1, into: directory.appendingPathComponent("split")) guard split.count == 1 else { throw TestFailure("expected one part, got \(split.count)") } let merged = directory.appendingPathComponent("merged.pdf") try PDFConverter.merge(split, at: merged) guard PDFConverter.pageCount(merged) == 1 else { throw TestFailure("merge lost pages") } } } private static func testSubtitles() { section("Subtitles") let srt = directory.appendingPathComponent("captions.srt") try? """ 1 00:00:01,000 --> 00:00:03,500 First line 2 00:00:04,000 --> 00:00:06,000 Second line """.write(to: srt, atomically: true, encoding: .utf8) check("srt → vtt") { let vtt = directory.appendingPathComponent("captions.vtt") try SubtitleConverter.convert(source: srt, to: .vtt, at: vtt) let text = try String(contentsOf: vtt, encoding: .utf8) guard text.hasPrefix("WEBVTT"), text.contains("00:00:01.000") else { throw TestFailure("unexpected vtt output") } } check("srt → txt") { let text = directory.appendingPathComponent("captions.txt") try SubtitleConverter.convert(source: srt, to: .txt, at: text) let contents = try String(contentsOf: text, encoding: .utf8) guard contents.contains("First line") else { throw TestFailure("missing cue text") } } check("subtitle offset") { let cues = try SubtitleConverter.parse(srt) let shifted = SubtitleConverter.shift(cues, by: 2) guard let first = shifted.first, abs(first.start - 3) < 0.01 else { throw TestFailure("offset was not applied") } } } private static func testSpreadsheets() { section("Spreadsheets") let csv = directory.appendingPathComponent("table.csv") try? "Name,Score\nAda,42\nGrace,\"7,5\"\n".write(to: csv, atomically: true, encoding: .utf8) check("csv parse") { let rows = try SpreadsheetConverter.read(csv) guard rows.count == 3, rows[1][0] == "Ada", rows[2][1] == "7,5" else { throw TestFailure("parsed \(rows)") } } check("csv → xlsx → csv") { let xlsx = directory.appendingPathComponent("table.xlsx") let rows = try SpreadsheetConverter.read(csv) try XLSX.write(rows, to: xlsx) try requireFile(xlsx) let roundTrip = try XLSX.read(xlsx) guard roundTrip.count == rows.count, roundTrip[1][0] == "Ada" else { throw TestFailure("round trip produced \(roundTrip)") } } check("csv → tsv") { let tsv = directory.appendingPathComponent("table.tsv") try SpreadsheetConverter.convert(source: csv, to: .tsv, at: tsv) let text = try String(contentsOf: tsv, encoding: .utf8) guard text.contains("\t") else { throw TestFailure("no tabs in output") } } } private static func testArchives() { section("Archives") let folder = directory.appendingPathComponent("bundle", isDirectory: true) try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) try? "archive me".write(to: folder.appendingPathComponent("note.txt"), atomically: true, encoding: .utf8) check("folder → zip") { let zip = directory.appendingPathComponent("bundle.zip") try ArchiveConverter.convert(source: folder, to: .zip, at: zip) try requireFile(zip) } check("zip → extract") { let zip = directory.appendingPathComponent("bundle.zip") let target = directory.appendingPathComponent("unpacked", isDirectory: true) try ArchiveConverter.extract(source: zip, into: target) let note = target.appendingPathComponent("bundle/note.txt") guard FileManager.default.fileExists(atPath: note.path) else { throw TestFailure("note.txt missing after extraction") } } check("folder → tar") { let tar = directory.appendingPathComponent("bundle.tar") try ArchiveConverter.convert(source: folder, to: .tar, at: tar) try requireFile(tar) } } private static func testDocuments() { section("Documents") let text = directory.appendingPathComponent("note.txt") try? "Hello from Konverter.\n\nSecond paragraph.\n".write(to: text, atomically: true, encoding: .utf8) check("txt → md") { let markdown = directory.appendingPathComponent("note.md") try DocumentConverter.convert(source: text, to: .md, at: markdown) let contents = try String(contentsOf: markdown, encoding: .utf8) guard contents.contains("Hello from Konverter") else { throw TestFailure("text lost") } } check("md → html") { let markdown = directory.appendingPathComponent("note.md") try? "# Title\n\nSome **bold** text.\n".write(to: markdown, atomically: true, encoding: .utf8) let html = directory.appendingPathComponent("note.html") try DocumentConverter.convert(source: markdown, to: .html, at: html) let contents = try String(contentsOf: html, encoding: .utf8) guard contents.contains("bold") else { throw TestFailure("markdown not rendered") } } check("txt → pdf") { let pdf = directory.appendingPathComponent("note.pdf") try DocumentConverter.convert(source: text, to: .pdf, at: pdf) try requireFile(pdf) guard PDFConverter.pageCount(pdf) >= 1 else { throw TestFailure("empty pdf") } } check("txt → docx") { let docx = directory.appendingPathComponent("note.docx") try DocumentConverter.convert(source: text, to: .docx, at: docx) try requireFile(docx) } check("html → md") { let markdown = Markdown.markdown(fromHTML: "
Hello world
") guard markdown.contains("# Title"), markdown.contains("**world**") else { throw TestFailure("html not converted: \(markdown)") } } } private static func testAudio() { section("Audio") let wav = directory.appendingPathComponent("tone.wav") check("create wav") { try makeWAV(at: wav) try requireFile(wav) } check("wav → m4a") { let m4a = directory.appendingPathComponent("tone.m4a") try AVConverter.convert(source: wav, to: .m4a, at: m4a) try requireFile(m4a) } check("wav → aiff") { let aiff = directory.appendingPathComponent("tone.aiff") try CoreAudio.convert(source: wav, to: .aiff, at: aiff) try requireFile(aiff) } check("media info") { let info = MediaInfo.describe(wav) guard info.contains("Duration") else { throw TestFailure("no duration in \(info)") } } } private static func testEbook() { section("EPUB") let html = "An ebook built from scratch.
" let epub = directory.appendingPathComponent("book.epub") check("build epub") { try EbookConverter.write( title: "Test Book", author: "Konverter", chapters: [("One", html), ("Two", "More text.
")], at: epub ) try requireFile(epub) } check("epub → txt") { let text = try EbookConverter.plainText(epub) guard text.contains("ebook built from scratch") else { throw TestFailure("chapter text missing: \(text)") } } check("epub → md") { let markdown = directory.appendingPathComponent("book.md") try EbookConverter.convert(source: epub, to: .md, at: markdown) let contents = try String(contentsOf: markdown, encoding: .utf8) guard contents.contains("ebook built") else { throw TestFailure("markdown lost the text") } } } private static func testGeometry() { section("Wheel geometry") let frame = CGRect(x: 0, y: 0, width: 400, height: 400) let items = (0..<8).map { index in WheelItem( id: "item-\(index)", payload: .format(.png), title: "Item \(index)", subtitle: nil, symbol: "circle", availability: .ok ) } let radius: CGFloat = 160 check("top hit selects the first item") { let index = WheelController.selectionIndex( for: CGPoint(x: 200, y: 340), in: frame, items: items, radius: radius ) guard index == 0 else { throw TestFailure("got \(String(describing: index))") } } check("dead zone selects nothing") { let index = WheelController.selectionIndex( for: CGPoint(x: 200, y: 200), in: frame, items: items, radius: radius ) guard index == nil else { throw TestFailure("got \(String(describing: index))") } } check("clockwise hit selects item 2") { let index = WheelController.selectionIndex( for: CGPoint(x: 360, y: 200), in: frame, items: items, radius: radius ) guard index == 2 else { throw TestFailure("got \(String(describing: index))") } } check("conflict naming") { let existing = directory.appendingPathComponent("taken.txt") try? "x".write(to: existing, atomically: true, encoding: .utf8) let candidate = OutputNaming.availableURL(for: existing) guard candidate.lastPathComponent == "taken 2.txt" else { throw TestFailure("got \(candidate.lastPathComponent)") } } } // MARK: - Harness private struct TestFailure: LocalizedError { let message: String init(_ message: String) { self.message = message } var errorDescription: String? { message } } private static func section(_ name: String) { print("— \(name)") } private static func check(_ name: String, _ body: () throws -> Void) { do { try body() passes += 1 print("PASS \(name)") } catch { failures += 1 let message = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription print("FAIL \(name): \(message)") } } private static func requireFile(_ url: URL) throws { guard FileManager.default.fileExists(atPath: url.path) else { throw TestFailure("\(url.lastPathComponent) was not created") } let size = try fileSize(url) guard size > 0 else { throw TestFailure("\(url.lastPathComponent) is empty") } } private static func fileSize(_ url: URL) throws -> Int { let attributes = try FileManager.default.attributesOfItem(atPath: url.path) return attributes[.size] as? Int ?? 0 } private static func makeImage(at url: URL, width: Int, height: Int) throws { guard let context = CGContext( data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ) else { throw TestFailure("could not create a bitmap") } context.setFillColor(CGColor(srgbRed: 0.98, green: 0.52, blue: 0.18, alpha: 1)) context.fill(CGRect(x: 0, y: 0, width: width, height: height)) context.setFillColor(CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)) context.fillEllipse(in: CGRect(x: width / 4, y: height / 4, width: width / 2, height: height / 2)) guard let image = context.makeImage() else { throw TestFailure("could not render the bitmap") } try Raster.write(image, to: url, format: .png, quality: 1, metadata: nil) } private static func makeWAV(at url: URL) throws { let sampleRate = 22050 let seconds = 0.6 let sampleCount = Int(Double(sampleRate) * seconds) var samples = Data() for index in 0..