diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/pdfgadget-cli.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/pdfgadget-cli.xcscheme new file mode 100644 index 0000000..797876b --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/pdfgadget-cli.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift index b0e71fc..3d9d921 100644 --- a/Package.swift +++ b/Package.swift @@ -12,9 +12,14 @@ let package = Package( .library( name: "PDFGadget", targets: ["PDFGadget"] + ), + .executable( + name: "pdfgadget-cli", + targets: ["pdfgadget-cli"] ) ], dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.1"), .package(url: "https://github.com/orchetect/OTCore.git", from: "1.7.6"), .package(url: "https://github.com/orchetect/swift-testing-extensions.git", from: "0.2.1") ], @@ -33,6 +38,14 @@ let package = Package( .product(name: "TestingExtensions", package: "swift-testing-extensions") ], resources: [.copy("TestResource/PDF Files")] + ), + .executableTarget( + name: "pdfgadget-cli", + dependencies: [ + "OTCore", + "PDFGadget", + .product(name: "ArgumentParser", package: "swift-argument-parser") + ] ) ] ) diff --git a/Sources/pdfgadget-cli/Extensions/LoggingLevel+ArgumentParser.swift b/Sources/pdfgadget-cli/Extensions/LoggingLevel+ArgumentParser.swift new file mode 100644 index 0000000..cb5cf5f --- /dev/null +++ b/Sources/pdfgadget-cli/Extensions/LoggingLevel+ArgumentParser.swift @@ -0,0 +1,14 @@ +// +// LoggingLevel+ArgumentParser.swift +// PDFGadget • https://github.com/orchetect/PDFGadget +// © 2023-2024 Steffan Andrews • Licensed under MIT License +// + +#if os(macOS) + +import ArgumentParser +import os.log + +extension OSLogType: @retroactive ExpressibleByArgument { } + +#endif diff --git a/Sources/pdfgadget-cli/Extensions/Types+ArgumentParser.swift b/Sources/pdfgadget-cli/Extensions/Types+ArgumentParser.swift new file mode 100644 index 0000000..3cc6fa4 --- /dev/null +++ b/Sources/pdfgadget-cli/Extensions/Types+ArgumentParser.swift @@ -0,0 +1,18 @@ +// +// Types+ArgumentParser.swift +// PDFGadget • https://github.com/orchetect/PDFGadget +// © 2023-2024 Steffan Andrews • Licensed under MIT License +// + +#if os(macOS) + +import ArgumentParser +import PDFGadget + +// Export + +// extension PDFOperation: ExpressibleByArgument { } +// extension PDFPageSet: ExpressibleByArgument { } +// extension PDFPagesFilter: ExpressibleByArgument { } + +#endif diff --git a/Sources/pdfgadget-cli/Logging/Logger.swift b/Sources/pdfgadget-cli/Logging/Logger.swift new file mode 100644 index 0000000..16a9e86 --- /dev/null +++ b/Sources/pdfgadget-cli/Logging/Logger.swift @@ -0,0 +1,45 @@ +// +// Logger.swift +// PDFGadget • https://github.com/orchetect/PDFGadget +// © 2023-2024 Steffan Andrews • Licensed under MIT License +// + +#if os(macOS) + +import Foundation +import os.log + +let logger = Logger(subsystem: "com.orchetect.PDFGadget", category: "CLI") + +extension OSLogType: @retroactive CaseIterable { + public static let allCases: [OSLogType] = [ + .debug, .info, .default, .error, .fault + ] +} + +extension OSLogType { + public var name: String { + switch self { + case .debug: return "debug" + case .info: return "info" + case .default: return "default" + case .error: return "error" + case .fault: return "fault" + default: + assertionFailure("Unhandled OSLogType case: \(String(describing: self))") + return String(describing: self) + } + } + + public init?(name: String) { + guard let match = Self.allCases.first(where: { $0.name == name }) + else { + assertionFailure("Unhandled OSLogType case: \(name)") + return nil + } + + self = match + } +} + +#endif diff --git a/Sources/pdfgadget-cli/PDFGadgetCLI.swift b/Sources/pdfgadget-cli/PDFGadgetCLI.swift new file mode 100644 index 0000000..38fce5c --- /dev/null +++ b/Sources/pdfgadget-cli/PDFGadgetCLI.swift @@ -0,0 +1,84 @@ +// +// PDFGadgetCLI.swift +// PDFGadget • https://github.com/orchetect/PDFGadget +// © 2023-2024 Steffan Andrews • Licensed under MIT License +// + +#if os(macOS) + +import ArgumentParser +import Foundation +import os.log +internal import OTCore +import PDFGadget + +struct PDFGadgetCLI: ParsableCommand { + // MARK: - Config + + static let configuration = CommandConfiguration( + abstract: "PDF processing utilities.", + discussion: "https://github.com/orchetect/PDFGadget", + version: "0.1.1" + ) + + // MARK: - Arguments + + @Option( + name: [.customLong("source"), .customShort("s")], + parsing: .upToNextOption, // allows multiple values + help: "One or more input PDF file(s).", + transform: URL.init(fileURLWithPath:) + ) + var source: [URL] + + @Option( + name: [.customLong("destination"), .customShort("d")], + help: "Output directory. Defaults to same director as first input file.", + transform: URL.init(fileURLWithPath:) + ) + var outputDir: URL? + + @Option( + help: ArgumentHelp( + "Log level.", + valueName: OSLogType.allCases.map { $0.name }.joined(separator: ", ") + ) + ) + var logLevel: OSLogType = .info + + @Flag(name: [.customLong("quiet")], help: "Disable log.") + var logQuiet = false + + @Option( + name: [.customLong("operations"), .customShort("o")], + parsing: .upToNextOption, // allows multiple values + help: "PDF editing operations." + ) + var operations: [PDFOperation] + + // MARK: - Protocol Method Implementations + + mutating func validate() throws { + #warning("> additional validation here") + } + + mutating func run() throws { + let settings: PDFGadget.Settings + + do { + settings = try PDFGadget.Settings( + sourcePDFs: source, + outputDir: outputDir, + operations: operations, + savePDFs: true + // ... + ) + } catch let PDFGadgetError.validationError(error) { + throw ValidationError(error) + } + + try PDFGadget().run(using: settings) + } +} + +#endif diff --git a/Sources/pdfgadget-cli/PDFOperation Commands.swift b/Sources/pdfgadget-cli/PDFOperation Commands.swift new file mode 100644 index 0000000..34081db --- /dev/null +++ b/Sources/pdfgadget-cli/PDFOperation Commands.swift @@ -0,0 +1,130 @@ +// +// PDFOperation Commands.swift +// PDFGadget • https://github.com/orchetect/PDFGadget +// © 2023-2024 Steffan Andrews • Licensed under MIT License +// + +#if os(macOS) + +import ArgumentParser +import Foundation +import PDFGadget +internal import OTCore + +// We can safely use `@retroactive` since we are the owner of this type in the package. +extension PDFOperation: ExpressibleByArgument { + public init?(argument: String) { + // TODO: ⚠️ THIS IS A WORK IN PROGRESS + + // ideas for each operation's arguments: + // - JSON ie: {"from":0,"to":1} + // - like a method call ie: filterPages(from:0,to:1) + + guard let id = Identifier.allCases.first(where: { + argument.hasPrefix("\($0):") || argument == $0.rawValue + }) else { return nil } + let rawParams = String(argument.dropFirst(id.rawValue.count + 1)) + + logger.info("\(id.rawValue) with body: \(String(rawParams).quoted)") + + switch id { + case .newFile: + self = .newFile + case .cloneFile: + fatalError("Not yet implemented.") +// self = .cloneFile(file: ) + case .filterFiles: + fatalError("Not yet implemented.") + case .mergeFiles: + fatalError("Not yet implemented.") + case .splitFile: + fatalError("Not yet implemented.") + case .setFilename: + fatalError("Not yet implemented.") + case .removeFileAttributes: + fatalError("Not yet implemented.") + case .setFileAttribute: + fatalError("Not yet implemented.") + case .filterPages: + fatalError("Not yet implemented.") + case .copyPages: + fatalError("Not yet implemented.") + case .movePages: + fatalError("Not yet implemented.") + case .replacePages: + fatalError("Not yet implemented.") + case .reversePageOrder: + fatalError("Not yet implemented.") + case .rotatePages: + fatalError("Not yet implemented.") + case .filterAnnotations: + fatalError("Not yet implemented.") + } + } + + private enum Identifier: String, CaseIterable { + case newFile + case cloneFile + case filterFiles + case mergeFiles + case splitFile + case setFilename + case removeFileAttributes + case setFileAttribute + case filterPages + case copyPages + case movePages + case replacePages + case reversePageOrder + case rotatePages + case filterAnnotations + } +} + +// extension PDFGadgetCLI { +// struct Foo: ExpressibleByArgument { +// init?(argument: String) { +// #warning("> run sub-parser") +// } +// +// +// } +// struct OperationsBuilder: ParsableArguments { +// internal var wrapper: Operations = .init() +// +// init() { } +// +// init(_ string: String) { +// #warning("> run sub-parser") +// } +// +// internal struct Operations: Codable { +// var operations: [PDFOperation] +// +// init(operations: [PDFOperation] = []) { +// self.operations = operations +// } +// +// func encode(to encoder: Encoder) throws { +// // we're not actually implementing Codable, it's just to satisfy ParsableArguments +// } +// +// init(from decoder: Decoder) throws { +// // we're not actually implementing Codable, it's just to satisfy ParsableArguments +// self.init() +// } +// } +// } +// +// struct NewFile: ParsableCommand { +// static var configuration = CommandConfiguration(commandName: "newfile") +// +// @OptionGroup var builder: OperationsBuilder +// +// mutating func run() { +// builder.wrapper.operations.append(.newFile) +// } +// } +// } + +#endif diff --git a/Sources/pdfgadget-cli/main.swift b/Sources/pdfgadget-cli/main.swift new file mode 100644 index 0000000..79804eb --- /dev/null +++ b/Sources/pdfgadget-cli/main.swift @@ -0,0 +1,22 @@ +// +// main.swift +// PDFGadget • https://github.com/orchetect/PDFGadget +// © 2023-2024 Steffan Andrews • Licensed under MIT License +// + +#if os(macOS) + +import PDFGadget + +func main() { + do { + var command = try PDFGadgetCLI.parseAsRoot() + try command.run() + } catch { + PDFGadgetCLI.exit(withError: error) + } +} + +main() + +#endif