Files
ICCery-CPU/Sources/Printing/CUPSManager.swift
T
Local Admin 1c53e698aa
Build macOS Packages / Build macOS (Intel) (push) Failing after 15s
Build macOS Packages / Build macOS (Apple Silicon) (push) Failing after 29s
Build macOS Packages / Build macOS (Universal) (push) Skipped
build fixes
2026-09-07 06:11:55 -07:00

239 lines
8.9 KiB
Swift

import Foundation
import AppKit
import Darwin
/// SPEC §10 — libcups queue inspection, AirPrint detection, vendor PPD injection.
struct PrinterQueue: Equatable {
var name: String
var instance: String?
var displayName: String
var uri: String
var make: String
var model: String
var isAirPrint: Bool
var mediaSizes: [String]
var mediaTypes: [String]
var trays: [String]
var resolutions: [String]
var ppdText: String
}
enum CUPSManager {
static let airPrintWarning =
"Selected printer uses an AirPrint / driverless queue. Hardware colour management cannot be reliably disabled. Install the official OEM driver (Epson, Canon, etc.) for accurate profiling."
// MARK: - Queue listing
static func listQueues() throws -> [PrinterQueue] {
var dests: UnsafeMutablePointer<cups_dest_t>? = nil
let count = cupsGetDests(&dests)
defer { if let dests = dests { cupsFreeDests(count, dests) } }
guard count > 0, let dests = dests else {
return []
}
var result: [PrinterQueue] = []
for i in 0..<Int(count) {
let dest = dests.advanced(by: i).pointee
guard let cName = dest.name else { continue }
let name = String(cString: cName)
let instance = dest.instance.map { String(cString: $0) }
let uri = destValue(dest, key: "printer-uri-supported")
?? destValue(dest, key: "device-uri")
?? ""
let makeModel = destValue(dest, key: "printer-make-and-model") ?? ""
let (make, model) = splitMakeModel(makeModel)
let ppdText = ppdContents(forQueue: name) ?? ""
let options = discoverPPDOptions(ppdText)
let air = AirPrintDetector.isAirPrint(
uri: uri,
ppdText: ppdText,
make: make,
model: model
)
result.append(PrinterQueue(
name: name,
instance: instance,
displayName: instance.map { "\(name)/\($0)" } ?? name,
uri: uri,
make: make,
model: model,
isAirPrint: air,
mediaSizes: options.pageSizes,
mediaTypes: options.mediaTypes,
trays: options.trays,
resolutions: options.resolutions,
ppdText: ppdText
))
}
return result
}
static func namedQueue(_ name: String) throws -> PrinterQueue? {
try listQueues().first { $0.name == name }
}
// MARK: - PPD
static func ppdPath(forQueue name: String) -> String? {
name.withCString { cName in
guard let cPath = TPCupsGetPPD(cName) else { return nil }
return String(cString: cPath)
}
}
static func ppdContents(forQueue name: String) -> String? {
guard let path = ppdPath(forQueue: name) else { return nil }
defer { unlink(path) }
return try? String(contentsOfFile: path, encoding: .isoLatin1)
}
struct PPDOptions {
var pageSizes: [String] = []
var mediaTypes: [String] = []
var trays: [String] = []
var resolutions: [String] = []
var colorChoices: [(keyword: String, choice: String)] = []
}
static func discoverPPDOptions(_ ppdText: String) -> PPDOptions {
var out = PPDOptions()
out.pageSizes = optionChoices(in: ppdText, keyword: "PageSize")
out.mediaTypes = optionChoices(in: ppdText, keyword: "MediaType")
out.trays = optionChoices(in: ppdText, keyword: "InputSlot")
out.resolutions = optionChoices(in: ppdText, keyword: "Resolution")
// Walk every *OpenUI for Colour/Color keys (SPEC §10.3 generic).
let lines = ppdText.split(whereSeparator: \.isNewline)
var currentKeyword: String?
for line in lines {
let s = String(line)
if s.hasPrefix("*OpenUI") {
if let kw = openUIKeyword(s) { currentKeyword = kw }
} else if s.hasPrefix("*CloseUI") {
currentKeyword = nil
} else if let kw = currentKeyword, looksLikeColorKeyword(kw) {
if let choice = choiceName(s, keyword: kw) {
out.colorChoices.append((kw, choice))
}
}
}
return out
}
/// SPEC §10.3 vendor keys for “off”.
static func vendorColorBypass(make: String, model: String, ppdText: String) -> [String: String] {
let vendor = make.lowercased()
if vendor.contains("epson") {
return ["ColorModel": "RGB", "EPSONColorControls": "Off"]
}
if vendor.contains("canon") {
return ["CNColorMatching": "None"]
}
if vendor.contains("hp") || vendor.contains("hewlett") {
return ["ColorModel": "RGB", "HPColorControl": "Off"]
}
// Generic: prefer any Color/Colour option whose choice is None / Off / No.
let options = discoverPPDOptions(ppdText)
var found: [String: String] = [:]
for pair in options.colorChoices {
if ["none", "off", "no", "nocoloradjustment"].contains(pair.choice.lowercased()) {
found[pair.keyword] = pair.choice
}
}
return found
}
static func applyVendorBypass(_ map: [String: String], to printInfo: NSPrintInfo) {
let dict = printInfo.dictionary()
let settingsKey = NSPrintInfo.AttributeKey(rawValue: "com.apple.print.printSettings")
let nested: NSMutableDictionary
if let existing = dict[settingsKey] as? NSMutableDictionary {
nested = existing
} else {
nested = NSMutableDictionary()
dict[settingsKey] = nested
}
for (k, v) in map {
nested[k] = v
dict[NSPrintInfo.AttributeKey(rawValue: k)] = v
TPLog.info("PPD bypass \(k)=\(v)")
}
}
// MARK: - helpers
private static func destValue(_ dest: cups_dest_t, key: String) -> String? {
guard let cKey = key.cString(using: .utf8) else { return nil }
guard let raw = cupsGetOption(cKey, dest.num_options, dest.options) else { return nil }
return String(cString: raw)
}
private static func splitMakeModel(_ makeModel: String) -> (String, String) {
let parts = makeModel.split(separator: " ", maxSplits: 1, omittingEmptySubsequences: true)
if parts.count == 2 { return (String(parts[0]), String(parts[1])) }
if parts.count == 1 { return (String(parts[0]), "") }
return ("", makeModel)
}
static func optionChoices(in ppdText: String, keyword: String) -> [String] {
var choices: [String] = []
let prefix = "*\(keyword) "
for raw in ppdText.split(whereSeparator: \.isNewline) {
let line = String(raw)
guard line.hasPrefix(prefix) else { continue }
let rest = String(line.dropFirst(prefix.count))
let token = rest.split(whereSeparator: { $0 == "/" || $0 == ":" || $0 == " " }).first
if let token = token {
let name = String(token)
if !choices.contains(name) { choices.append(name) }
}
}
return choices
}
private static func openUIKeyword(_ line: String) -> String? {
// *OpenUI *PageSize/Media Size: PickOne
guard let star = line.firstIndex(of: "*") else { return nil }
let after = line[star...].dropFirst()
guard let second = after.firstIndex(of: "*") else { return nil }
let rest = after[second...].dropFirst()
var kw = ""
for ch in rest {
if ch == "/" || ch == ":" || ch == " " { break }
kw.append(ch)
}
return kw.isEmpty ? nil : kw
}
private static func choiceName(_ line: String, keyword: String) -> String? {
let prefix = "*\(keyword) "
guard line.hasPrefix(prefix) else { return nil }
let rest = String(line.dropFirst(prefix.count))
return rest.split(whereSeparator: { $0 == "/" || $0 == ":" || $0 == " " }).first.map(String.init)
}
private static func looksLikeColorKeyword(_ kw: String) -> Bool {
let l = kw.lowercased()
return l.contains("color") || l.contains("colour")
}
}
/// SPEC §10.2 — pure function so unit tests do not need libcups.
enum AirPrintDetector {
static func isAirPrint(uri: String, ppdText: String, make: String, model: String) -> Bool {
let u = uri.lowercased()
if u.hasPrefix("apple-airprint://") { return true }
if ppdText.range(of: "*APAirPrint:\\s*True", options: [.regularExpression, .caseInsensitive]) != nil {
return true
}
if make.caseInsensitiveCompare("Apple") == .orderedSame && model.lowercased().contains("airprint") {
return true
}
if u.hasPrefix("ipps://") && ppdText.lowercased().contains("airprint") {
return true
}
return false
}
}