Files
gronod 474888541b
Build macOS Packages / Build macOS (Intel) (pull_request) Successful in 32s
Build macOS Packages / Build macOS (Apple Silicon) (pull_request) Successful in 37s
Build macOS Packages / Build macOS (Universal) (pull_request) Failing after 10s
feat: add hardware print quality option supporting Canon 5-notch Custom slider and Epson modes (Closes #5)
2026-09-07 23:37:01 +01:00

209 lines
7.5 KiB
Swift

import Foundation
/// SPEC §5 — `TargetJob` v1. Unknown keys ignored; missing required keys fatal.
struct PrintSettings: Equatable {
var printerName: String
var mediaSize: String
var mediaType: String?
var paperSource: String?
var resolution: String?
var printQuality: String?
var scaling: Double
var centered: Bool
var forceUnmanagedColor: Bool
init(
printerName: String,
mediaSize: String,
mediaType: String? = nil,
paperSource: String? = nil,
resolution: String? = nil,
printQuality: String? = nil,
scaling: Double,
centered: Bool,
forceUnmanagedColor: Bool
) {
self.printerName = printerName
self.mediaSize = mediaSize
self.mediaType = mediaType
self.paperSource = paperSource
self.resolution = resolution
self.printQuality = printQuality
self.scaling = scaling
self.centered = centered
self.forceUnmanagedColor = forceUnmanagedColor
}
}
struct UIPolicy: Equatable {
var lockColorManagement: Bool
var allowBasicDriverChanges: Bool
}
struct TargetJob: Equatable {
var version: Int
var jobTitle: String
var files: [String]
var printSettings: PrintSettings
var uiPolicy: UIPolicy
}
enum TargetJobParser {
static func parse(data: Data) throws -> TargetJob {
let json: Any
do {
json = try JSONSerialization.jsonObject(with: data, options: [])
} catch {
throw AppError.invalidJob("JSON parse error")
}
guard let obj = json as? [String: Any] else {
throw AppError.invalidJob("root must be an object")
}
return try parse(object: obj)
}
static func parse(text: String) throws -> TargetJob {
guard let data = text.data(using: .utf8) else {
throw AppError.invalidJob("unreadable UTF-8")
}
return try parse(data: data)
}
static func parse(url: URL) throws -> TargetJob {
let data: Data
do {
data = try Data(contentsOf: url)
} catch {
throw AppError.invalidJob("cannot read \(url.path)")
}
return try parse(data: data)
}
static func parse(object: [String: Any]) throws -> TargetJob {
// Unknown keys ignored (passthrough). Required keys fatal.
guard let version = intValue(object["version"]) else {
throw AppError.invalidJob("version — required")
}
guard version == 1 else {
throw AppError.invalidJob("version — currently only 1 is accepted")
}
guard let jobTitle = stringValue(object["jobTitle"]), !jobTitle.isEmpty else {
throw AppError.invalidJob("jobTitle — required")
}
guard let filesRaw = object["files"] as? [Any], !filesRaw.isEmpty else {
throw AppError.invalidJob("files — required non-empty array")
}
var files: [String] = []
for (i, item) in filesRaw.enumerated() {
guard let s = item as? String, !s.isEmpty else {
throw AppError.invalidJob("files[\(i)] — absolute path required")
}
files.append(s)
}
guard let psObj = object["printSettings"] as? [String: Any] else {
throw AppError.invalidJob("printSettings — required")
}
guard let uiObj = object["uiPolicy"] as? [String: Any] else {
throw AppError.invalidJob("uiPolicy — required")
}
let printSettings = try parsePrintSettings(psObj)
let uiPolicy = try parseUIPolicy(uiObj)
return TargetJob(
version: 1,
jobTitle: jobTitle,
files: files,
printSettings: printSettings,
uiPolicy: uiPolicy
)
}
private static func parsePrintSettings(_ obj: [String: Any]) throws -> PrintSettings {
guard let printerName = stringValue(obj["printerName"]), !printerName.isEmpty else {
throw AppError.invalidJob("printSettings.printerName — required")
}
guard let mediaSize = stringValue(obj["mediaSize"]), !mediaSize.isEmpty else {
throw AppError.invalidJob("printSettings.mediaSize — required")
}
guard let scaling = doubleValue(obj["scaling"]) else {
throw AppError.invalidJob("printSettings.scaling — required number")
}
guard let centered = boolValue(obj["centered"]) else {
throw AppError.invalidJob("printSettings.centered — required bool")
}
guard let force = boolValue(obj["forceUnmanagedColor"]) else {
throw AppError.invalidJob("printSettings.forceUnmanagedColor — required bool")
}
return PrintSettings(
printerName: printerName,
mediaSize: mediaSize,
mediaType: stringValue(obj["mediaType"]),
paperSource: stringValue(obj["paperSource"]),
resolution: stringValue(obj["resolution"]),
printQuality: stringValue(obj["printQuality"]),
scaling: scaling,
centered: centered,
forceUnmanagedColor: force
)
}
private static func parseUIPolicy(_ obj: [String: Any]) throws -> UIPolicy {
guard let lock = boolValue(obj["lockColorManagement"]) else {
throw AppError.invalidJob("uiPolicy.lockColorManagement — required bool")
}
guard let allow = boolValue(obj["allowBasicDriverChanges"]) else {
throw AppError.invalidJob("uiPolicy.allowBasicDriverChanges — required bool")
}
return UIPolicy(lockColorManagement: lock, allowBasicDriverChanges: allow)
}
static func serialize(_ job: TargetJob) throws -> Data {
var ps: [String: Any] = [
"printerName": job.printSettings.printerName,
"mediaSize": job.printSettings.mediaSize,
"scaling": job.printSettings.scaling,
"centered": job.printSettings.centered,
"forceUnmanagedColor": job.printSettings.forceUnmanagedColor,
]
if let v = job.printSettings.mediaType { ps["mediaType"] = v }
if let v = job.printSettings.paperSource { ps["paperSource"] = v }
if let v = job.printSettings.resolution { ps["resolution"] = v }
if let v = job.printSettings.printQuality { ps["printQuality"] = v }
let obj: [String: Any] = [
"version": 1,
"jobTitle": job.jobTitle,
"files": job.files,
"printSettings": ps,
"uiPolicy": [
"lockColorManagement": job.uiPolicy.lockColorManagement,
"allowBasicDriverChanges": job.uiPolicy.allowBasicDriverChanges,
],
]
return try JSONSerialization.data(withJSONObject: obj, options: [.prettyPrinted])
}
private static func stringValue(_ any: Any?) -> String? {
if let s = any as? String { return s }
return nil
}
private static func intValue(_ any: Any?) -> Int? {
if let i = any as? Int { return i }
if let n = any as? NSNumber { return n.intValue }
return nil
}
private static func doubleValue(_ any: Any?) -> Double? {
if let d = any as? Double { return d }
if let i = any as? Int { return Double(i) }
if let n = any as? NSNumber { return n.doubleValue }
return nil
}
private static func boolValue(_ any: Any?) -> Bool? {
if let b = any as? Bool { return b }
if let n = any as? NSNumber { return n.boolValue }
return nil
}
}