Interactive Image Converter Swift Script

This Swift script guides users through selecting an input image from common directories using numbered lists and then converting it to a desired format (PNG, JPEG, or TIFF) while also letting them choose an output directory and filename. It simplifies the image conversion process on macOS by providing an interactive, user-friendly interface.

Click to view script…
#!/usr/bin/env swift
import Foundation
import AppKit

// MARK: - Helper Functions

/// Prompts the user for input with a message.
func prompt(_ message: String) -> String {
    print(message, terminator: " ")
    return readLine() ?? ""
}

/// Presents a numbered list of directory options to the user and returns the selected directory URL.
func selectDirectory(options: [(name: String, url: URL)], promptMessage: String) -> URL {
    for (index, option) in options.enumerated() {
        print("\(index + 1): \(option.name) -> \(option.url.path)")
    }
    let choiceStr = prompt(promptMessage)
    if let choiceNum = Int(choiceStr), choiceNum > 0, choiceNum <= options.count {
        let selected = options[choiceNum - 1]
        if selected.name.hasPrefix("Other") {
            let customPath = prompt("Enter custom directory path:")
            return URL(fileURLWithPath: customPath, isDirectory: true)
        } else {
            return selected.url
        }
    } else {
        let manualPath = prompt("Invalid selection. Enter directory path manually:")
        return URL(fileURLWithPath: manualPath, isDirectory: true)
    }
}

/// Presents a numbered list of files for the user to choose from.
func selectFile(from files: [URL], in directory: URL) -> URL {
    for (index, file) in files.enumerated() {
        print("\(index + 1): \(file.lastPathComponent)")
    }
    let choiceStr = prompt("Enter the number of your choice:")
    if let choiceNum = Int(choiceStr), choiceNum > 0, choiceNum <= files.count {
        return files[choiceNum - 1]
    } else {
        let manualFile = prompt("Invalid selection. Enter the file name (in \(directory.path)):")
        return directory.appendingPathComponent(manualFile)
    }
}

/// Converts a file size (in bytes) to a human-readable string.
func humanReadableSize(_ size: UInt64) -> String {
    let formatter = ByteCountFormatter()
    formatter.countStyle = .file
    formatter.allowedUnits = [.useKB, .useMB, .useGB, .useTB]
    return formatter.string(fromByteCount: Int64(size))
}

// MARK: - Candidate Directories Setup

let fileManager = FileManager.default
var candidateDirs: [(name: String, url: URL)] = []

if let desktop = fileManager.urls(for: .desktopDirectory, in: .userDomainMask).first {
    candidateDirs.append(("Desktop", desktop))
}
if let documents = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
    candidateDirs.append(("Documents", documents))
}
if let downloads = fileManager.urls(for: .downloadsDirectory, in: .userDomainMask).first {
    candidateDirs.append(("Downloads", downloads))
}
candidateDirs.append(("Other (enter path manually)", URL(fileURLWithPath: fileManager.currentDirectoryPath)))

// MARK: - Input Directory and File Selection

print("Select the input directory:")
let inputDirectory = selectDirectory(options: candidateDirs, promptMessage: "Enter the number of your choice:")

// Verify the input directory exists.
var isDir: ObjCBool = false
guard fileManager.fileExists(atPath: inputDirectory.path, isDirectory: &isDir), isDir.boolValue else {
    print("Directory \(inputDirectory.path) does not exist or is not a directory.")
    exit(1)
}

// Define supported image extensions.
let validImageExtensions: Set<String> = ["png", "jpg", "jpeg", "tiff", "gif"]

// Get and filter files in the chosen directory.
guard let filesInDirectory = try? fileManager.contentsOfDirectory(at: inputDirectory,
                                                                   includingPropertiesForKeys: nil,
                                                                   options: [.skipsHiddenFiles]) else {
    print("Unable to read contents of directory \(inputDirectory.path)")
    exit(1)
}
let imageFiles = filesInDirectory.filter { validImageExtensions.contains($0.pathExtension.lowercased()) }

if imageFiles.isEmpty {
    print("No image files found in \(inputDirectory.path)")
    exit(1)
}

print("\nSelect the image file to convert:")
let inputFileURL = selectFile(from: imageFiles, in: inputDirectory)

// Optionally, display file size.
if let attrs = try? fileManager.attributesOfItem(atPath: inputFileURL.path),
   let fileSize = attrs[.size] as? UInt64 {
    print("Selected file: \(inputFileURL.lastPathComponent) (\(humanReadableSize(fileSize)))")
}

// MARK: - Output Format Selection

print("\nSelect the desired output image format:")
print("1: PNG")
print("2: JPEG")
print("3: TIFF")
let formatChoiceStr = prompt("Enter the number of your choice:")
var outputFileType: NSBitmapImageRep.FileType
var outputExtension: String

switch formatChoiceStr {
case "1":
    outputFileType = .png
    outputExtension = "png"
case "2":
    outputFileType = .jpeg
    outputExtension = "jpg"
case "3":
    outputFileType = .tiff
    outputExtension = "tiff"
default:
    print("Invalid selection.")
    exit(1)
}

// MARK: - Output Directory and File Name Selection

print("\nSelect the output directory:")
let outputDirectory = selectDirectory(options: candidateDirs, promptMessage: "Enter the number of your choice:")

isDir = false
guard fileManager.fileExists(atPath: outputDirectory.path, isDirectory: &isDir), isDir.boolValue else {
    print("Output directory \(outputDirectory.path) does not exist or is not a directory.")
    exit(1)
}

let defaultOutputName = inputFileURL.deletingPathExtension().lastPathComponent + "_converted." + outputExtension
let outputFileName = prompt("Enter output file name (default: \(defaultOutputName)):")
let finalOutputFileName = outputFileName.isEmpty ? defaultOutputName : outputFileName
let outputFileURL = outputDirectory.appendingPathComponent(finalOutputFileName)

// MARK: - Image Conversion

print("\nConverting image...")

guard let inputImage = NSImage(contentsOf: inputFileURL) else {
    print("Failed to load image from \(inputFileURL.path)")
    exit(1)
}
guard let tiffData = inputImage.tiffRepresentation else {
    print("Failed to get TIFF representation of the image.")
    exit(1)
}
guard let bitmapRep = NSBitmapImageRep(data: tiffData) else {
    print("Failed to create bitmap representation.")
    exit(1)
}
guard let outputImageData = bitmapRep.representation(using: outputFileType, properties: [:]) else {
    print("Failed to convert image to selected format.")
    exit(1)
}

do {
    try outputImageData.write(to: outputFileURL)
    print("Image successfully converted and saved to \(outputFileURL.path)")
} catch {
    print("Error saving converted image: \(error)")
    exit(1)
}