mirror of
https://github.com/kfattic/cupsadmin.git
synced 2026-11-13 19:06:43 -05:00
Canon and HP driver profiles for quick actions
Canon: CNColorMode color/mono, CNDuplex, PageSize=Letter. HP: HPColorAsGray or HPColorMode; duplex and Letter from the generic profile. Profiles can mark an action unavailable with a reason (Canon department IDs, HP accounting codes). Use Letter Paper only says Fit to Nearest Size when the profile sets a fit option. Offline tests sweep every Canon and HP PPD found in CUPSKIT_PPD_DIRS or /Library/Printers/PPDs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,8 +39,10 @@ Private maintainer notes, if present, are in `CLAUDE.local.md` (gitignored).
|
||||
from `Resources/driver-profiles.json` (embedded in code — the CLI is a single binary), matched on the
|
||||
PPD's `*Manufacturer`/`*NickName`, vendor profiles first and `generic` (standard PPD keywords, IPP
|
||||
Everywhere `*-default` attributes) last. A pair is used only if that queue supports the keyword and
|
||||
value; otherwise the action is unavailable with the reason. `cupsadmin ppdreport <queue>` shows a
|
||||
driver's keywords and which actions its profile enables.
|
||||
value; otherwise the action is unavailable with the reason. A profile's `unavailable` map gives
|
||||
the reason an action can't be done with that driver (e.g. Canon department IDs). Use Letter Paper is
|
||||
titled ", Fit to Nearest Size" only when the profile also sets a fit option. `cupsadmin ppdreport
|
||||
<queue | file.ppd[.gz]>` shows a driver's keywords and which actions its profile enables.
|
||||
|
||||
## Build and test
|
||||
```
|
||||
@@ -67,7 +69,8 @@ swift build # debug build of everything
|
||||
- Live tests are opt-in and write to queues: `CUPSKIT_LIVE_QUEUE=<queue>`, `CUPSKIT_LIVE_JOBS=<a>,<b>`,
|
||||
`CUPSKIT_LIVE_QUICK=<ricoh>,<generic>`. Use throwaway queues (e.g. `lpd://127.0.0.1/…`, paused) and
|
||||
delete them afterwards; the live tests restore what they change. Offline tests use vendor PPDs from
|
||||
`/Library/Printers/PPDs` and skip when they aren't installed.
|
||||
`CUPSKIT_PPD_DIRS` (colon-separated, e.g. Canon/HP PPDs extracted with `pkgutil --expand-full`, not
|
||||
installed) and `/Library/Printers/PPDs`, and skip when they aren't there.
|
||||
|
||||
## Conventions
|
||||
- **US spelling everywhere** — code, UI, CLI output, docs (color, canceled, gray).
|
||||
|
||||
12
README.md
12
README.md
@@ -33,7 +33,7 @@ cupsadmin options <queue> # PPD options and IPP *-default values, like lp
|
||||
cupsadmin add <queue> -v <uri> [-m everywhere | -P file.ppd] [-D desc] [-L location] [--shared] [-o k=v ...]
|
||||
cupsadmin set <queue> -o k=v ... # change queue defaults, then read them back and report
|
||||
cupsadmin quick <action> <queue> [value] # task-named shortcuts, see below
|
||||
cupsadmin ppdreport <queue> # every PPD group and option: keyword, label, type, default, choices
|
||||
cupsadmin ppdreport <queue | file.ppd[.gz]> # every PPD group and option: keyword, label, type, default, choices
|
||||
```
|
||||
|
||||
Every run prints `started HH:MM:SS`, one status line (`OK: …` or `ERROR: …`) and `finished HH:MM:SS (total N min)` — on every exit path. Status lines go to stderr so table output pipes cleanly.
|
||||
@@ -50,7 +50,7 @@ cupsadmin quick bw <queue> # always print black & white
|
||||
cupsadmin quick usercode <queue> 12345 # set a Ricoh user code (enables it too)
|
||||
cupsadmin quick duplex <queue> # default to two-sided
|
||||
cupsadmin quick simplex <queue>
|
||||
cupsadmin quick letter <queue> # Letter paper + fit to nearest size (no "prompt user" at the printer)
|
||||
cupsadmin quick letter <queue> # Letter paper; also fit to nearest size where the driver can (Ricoh)
|
||||
cupsadmin quick default <queue> # make this the server default printer
|
||||
```
|
||||
|
||||
@@ -65,13 +65,15 @@ Which keywords an action writes comes from a driver profile (`Sources/CupsKit/Re
|
||||
| Ricoh M C251FW PS | ✓ color, black & white, duplex, Letter (no user code) |
|
||||
| Ricoh PCL (e.g. SP 3710DN) | ✓ duplex, Letter (generic) |
|
||||
| Any other PPD, IPP Everywhere / AirPrint | ✓ color, black & white, duplex, Letter where the queue supports them (generic) |
|
||||
| Canon, HP, Xerox, Konica Minolta user/department codes | no built-in profile — add one (below) |
|
||||
| Canon UFR II / LIPSLX / CARPS2 | ✓ color, black & white (color models), duplex, Letter (no fit-to-page option); department IDs are set in the Canon driver, not the PPD |
|
||||
| HP (`HPColorAsGray` or `HPColorMode` drivers) | ✓ color, black & white, duplex, Letter (no fit-to-page option); no accounting code option |
|
||||
| Xerox, Konica Minolta and others | generic only — add a profile (below) |
|
||||
|
||||
### Adding your printer's driver
|
||||
|
||||
1. `cupsadmin ppdreport <queue>` lists every option your driver has; find the color, duplex, paper and accounting/user-code keywords and their choice values.
|
||||
1. `cupsadmin ppdreport <queue>` (or `ppdreport <file.ppd.gz>` for a PPD from a driver package you haven't installed — `pkgutil --expand-full` it first) lists every option your driver has; find the color, duplex, paper and accounting/user-code keywords and their choice values.
|
||||
2. Copy the `ricoh` entry in `Sources/CupsKit/Resources/driver-profiles.json`, give it an `id`, and set `match` to regular expressions for your PPD's `Manufacturer` and `NickName` (both are printed at the top of the report).
|
||||
3. Replace the `keyword=value` pairs. `{value}` is the code the user types; typed options take `Custom.{value}`. Leave out actions your driver can't do. Keep your profile above `generic`.
|
||||
3. Replace the `keyword=value` pairs. `{value}` is the code the user types; typed options take `Custom.{value}`. Leave out actions your driver can't do, or explain why under `unavailable`. Keep your profile above `generic`.
|
||||
4. `swift build && .build/debug/cupsadmin ppdreport <queue>` shows which quick actions are now available; try one with `cupsadmin quick`, then open a pull request.
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -300,7 +300,7 @@ struct QuickActionItems: View {
|
||||
ForEach(QuickAction.all) { action in
|
||||
let reason = store.unavailableReason(action, queue: queue)
|
||||
Button { choose(action) } label: {
|
||||
Label(action.title, systemImage: action.systemImage)
|
||||
Label(action.title(in: store.drivers[queue]), systemImage: action.systemImage)
|
||||
}
|
||||
.disabled(reason != nil)
|
||||
.help(reason ?? action.summary)
|
||||
|
||||
@@ -45,6 +45,9 @@ final class QuickActionModel: Identifiable {
|
||||
|
||||
var currentCode: String? { action.currentCode(context) }
|
||||
|
||||
/// "Use Letter Paper" or "Use Letter Paper, Fit to Nearest Size", depending on the driver.
|
||||
var title: String { action.title(in: state == nil ? nil : context).replacingOccurrences(of: "…", with: "") }
|
||||
|
||||
var validationError: String? { action.validationError(code, context: context) }
|
||||
|
||||
/// What will be written, or why the action can't run here.
|
||||
@@ -85,7 +88,7 @@ final class QuickActionModel: Identifiable {
|
||||
readBack = Dictionary(uniqueKeysWithValues: outcome.readBack.map { ($0.key, $0) })
|
||||
state = try? await OptionState.load(client: store.client, queue: queue)
|
||||
let seconds = elapsedText(since: started)
|
||||
let title = clearing ? "Cleared user code" : action.title.replacingOccurrences(of: "…", with: "")
|
||||
let title = clearing ? "Cleared user code" : self.title
|
||||
if outcome.succeeded {
|
||||
resultMessage = nil
|
||||
store.report("\(title) on \(queue) in \(seconds)")
|
||||
@@ -99,7 +102,7 @@ final class QuickActionModel: Identifiable {
|
||||
phase = .finished(succeeded: false)
|
||||
} catch {
|
||||
resultMessage = PrinterStore.explain(String(describing: error))
|
||||
store.report("ERROR: \(action.title) on \(queue): \(error)")
|
||||
store.report("ERROR: \(title) on \(queue): \(error)")
|
||||
phase = .finished(succeeded: false)
|
||||
}
|
||||
store.noteChange(queue: queue)
|
||||
@@ -114,7 +117,7 @@ struct QuickActionSheet: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Label(model.action.title.replacingOccurrences(of: "…", with: ""), systemImage: model.action.systemImage)
|
||||
Label(model.title, systemImage: model.action.systemImage)
|
||||
.font(.title3.weight(.semibold))
|
||||
Text("\(model.queue) · saved as the queue default for every user on this Mac")
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
@@ -52,8 +52,10 @@ public struct DriverProfile: Decodable, Identifiable {
|
||||
public let match: Match
|
||||
/// Action id -> alternatives, each a list of `keyword=value` pairs applied together.
|
||||
public let actions: [String: [[String]]]
|
||||
/// Action id -> why this driver can't do it (e.g. accounting codes set outside the PPD).
|
||||
public let unavailable: [String: String]
|
||||
|
||||
enum CodingKeys: String, CodingKey { case id, name, match, actions }
|
||||
enum CodingKeys: String, CodingKey { case id, name, match, actions, unavailable }
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
@@ -65,6 +67,7 @@ public struct DriverProfile: Decodable, Identifiable {
|
||||
let raw = try c.decode([String: FlexiblePairs].self, forKey: .actions)
|
||||
for (action, pairs) in raw { actions[action] = pairs.alternatives }
|
||||
self.actions = actions
|
||||
unavailable = try c.decodeIfPresent([String: String].self, forKey: .unavailable) ?? [:]
|
||||
}
|
||||
|
||||
private struct FlexiblePairs: Decodable {
|
||||
|
||||
@@ -30,8 +30,8 @@ public struct QuickAction: Identifiable, Hashable {
|
||||
summary: "Two-sided, long edge"),
|
||||
QuickAction(id: "simplex", title: "Default to Single-Sided", systemImage: "doc", input: .none,
|
||||
summary: "One-sided"),
|
||||
QuickAction(id: "letter", title: "Use Letter Paper, Fit to Nearest Size", systemImage: "arrow.up.left.and.arrow.down.right",
|
||||
input: .none, summary: "Letter paper, and fit to the nearest size where the driver can (never prompt at the printer)"),
|
||||
QuickAction(id: "letter", title: "Use Letter Paper", systemImage: "arrow.up.left.and.arrow.down.right",
|
||||
input: .none, summary: "Letter paper, and fit to the nearest size where the driver has that option (never prompt at the printer)"),
|
||||
QuickAction(id: "default", title: "Make Default Printer", systemImage: "star", input: .none,
|
||||
summary: "Server default printer (lpadmin -d)"),
|
||||
]
|
||||
@@ -40,12 +40,25 @@ public struct QuickAction: Identifiable, Hashable {
|
||||
all.first { $0.id == id }
|
||||
}
|
||||
|
||||
/// The title for a queue: Use Letter Paper gains ", Fit to Nearest Size" only when its driver
|
||||
/// profile also sets a fit-to-paper option (e.g. Ricoh RIPaperPolicy), not just the page size.
|
||||
public func title(in context: DriverContext?) -> String {
|
||||
guard id == "letter", let context, case .success(let resolved) = resolve(context),
|
||||
resolved.changes.contains(where: { !Self.paperSizeKeys.contains($0.key) }) else { return title }
|
||||
return title + ", Fit to Nearest Size"
|
||||
}
|
||||
|
||||
static let paperSizeKeys: Set<String> = ["PageSize", "media-default"]
|
||||
|
||||
/// The profile and option changes this action would write on a queue, or why it can't.
|
||||
public func resolve(_ context: DriverContext, value: String? = nil, clearing: Bool = false,
|
||||
profiles: [DriverProfile] = DriverProfiles.builtIn) -> Result<ResolvedQuickAction, QuickActionUnavailable> {
|
||||
let key = clearing ? id + "-clear" : id
|
||||
var firstMissing: String?
|
||||
for profile in DriverProfiles.matching(context, in: profiles) {
|
||||
if profile.actions[key] == nil, let reason = profile.unavailable[id] {
|
||||
return .failure(QuickActionUnavailable(reason: reason))
|
||||
}
|
||||
guard let alternatives = profile.actions[key] else { continue }
|
||||
for pairs in alternatives {
|
||||
let parsed = pairs.compactMap(Self.parse)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"comment": "Quick Action mappings per driver. First matching profile that defines an action AND whose keyword=value pairs this queue supports wins; 'generic' (empty match) must stay last. An action is either one list of pairs (all applied together) or a list of alternative lists. {value} is replaced with the user's input. Find keywords with: cupsadmin ppdreport <queue>",
|
||||
"comment": "Quick Action mappings per driver. First matching profile that defines an action AND whose keyword=value pairs this queue supports wins; 'generic' (empty match) must stay last. An action is either one list of pairs (all applied together) or a list of alternative lists. {value} is replaced with the user's input. 'unavailable' maps an action id to the reason it can't be done with this driver (no later profile is tried). Find keywords with: cupsadmin ppdreport <queue | file.ppd>",
|
||||
"profiles": [
|
||||
{
|
||||
"id": "ricoh-m-c251fw",
|
||||
@@ -27,6 +27,33 @@
|
||||
"letter": ["PageSize=Letter", "RIPaperPolicy=NearestSizeAdjust"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "canon",
|
||||
"name": "Canon UFR II / LIPSLX / CARPS2",
|
||||
"match": { "manufacturer": "^Canon$" },
|
||||
"actions": {
|
||||
"color": ["CNColorMode=color"],
|
||||
"bw": ["CNColorMode=mono"],
|
||||
"duplex": ["CNDuplex=DuplexFront"],
|
||||
"simplex": ["CNDuplex=None"],
|
||||
"letter": ["PageSize=Letter"]
|
||||
},
|
||||
"unavailable": {
|
||||
"usercode": "Canon department IDs are set by the Canon driver, not the PPD"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "hp",
|
||||
"name": "HP",
|
||||
"match": { "manufacturer": "^HP$" },
|
||||
"actions": {
|
||||
"color": [["HPColorAsGray=False"], ["HPColorMode=colorsmart"]],
|
||||
"bw": [["HPColorAsGray=True"], ["HPColorMode=grayscale"]]
|
||||
},
|
||||
"unavailable": {
|
||||
"usercode": "HP drivers have no accounting code option"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "generic",
|
||||
"name": "Generic (standard PPD keywords, IPP Everywhere)",
|
||||
|
||||
@@ -18,12 +18,13 @@ private func profileID(_ id: String, _ context: DriverContext) -> String? {
|
||||
@Suite struct DriverProfileFile {
|
||||
@Test func builtInProfilesAreWellFormed() {
|
||||
let profiles = DriverProfiles.builtIn
|
||||
#expect(profiles.map(\.id) == ["ricoh-m-c251fw", "ricoh", "generic"])
|
||||
#expect(profiles.map(\.id) == ["ricoh-m-c251fw", "ricoh", "canon", "hp", "generic"])
|
||||
#expect(profiles.last?.isGeneric == true)
|
||||
#expect(profiles.dropLast().allSatisfy { !$0.isGeneric })
|
||||
let known = Set(QuickAction.all.map(\.id)).union(["usercode-clear"])
|
||||
for profile in profiles {
|
||||
#expect(Set(profile.actions.keys).isSubset(of: known), "\(profile.id): \(profile.actions.keys.sorted())")
|
||||
#expect(Set(profile.unavailable.keys).isSubset(of: known), "\(profile.id): \(profile.unavailable.keys.sorted())")
|
||||
for alternatives in profile.actions.values {
|
||||
#expect(alternatives.allSatisfy { !$0.isEmpty && $0.allSatisfy { QuickAction.parse($0) != nil } })
|
||||
}
|
||||
@@ -45,6 +46,7 @@ private func profileID(_ id: String, _ context: DriverContext) -> String? {
|
||||
let letter = try QuickAction.named("letter")!.resolve(c).get().changes
|
||||
#expect(letter == [OptionChange(key: "PageSize", value: "Letter", isSecret: false),
|
||||
OptionChange(key: "RIPaperPolicy", value: "NearestSizeAdjust", isSecret: false)])
|
||||
#expect(QuickAction.named("letter")!.title(in: c) == "Use Letter Paper, Fit to Nearest Size")
|
||||
let code = try QuickAction.named("usercode")!.resolve(c, value: "4321").get().changes
|
||||
#expect(code.map(\.value) == ["True", "Custom.4321"])
|
||||
let clear = try QuickAction.named("usercode")!.resolve(c, clearing: true).get().changes
|
||||
@@ -76,6 +78,7 @@ private func profileID(_ id: String, _ context: DriverContext) -> String? {
|
||||
// No RIPaperPolicy on this driver: the Ricoh letter mapping doesn't fit, generic Letter does.
|
||||
#expect(profileID("letter", c) == "generic")
|
||||
#expect(try QuickAction.named("letter")!.resolve(c).get().changes == [OptionChange(key: "PageSize", value: "Letter", isSecret: false)])
|
||||
#expect(QuickAction.named("letter")!.title(in: c) == "Use Letter Paper")
|
||||
}
|
||||
|
||||
@Test(.enabled(if: hasVendorPPD("RICOH IM C4500")))
|
||||
|
||||
148
Tests/CupsKitTests/VendorProfileTests.swift
Normal file
148
Tests/CupsKitTests/VendorProfileTests.swift
Normal file
@@ -0,0 +1,148 @@
|
||||
@testable import CupsKit
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
/// Canon and HP profile tests read the vendors' gzipped PPDs, read-only. They look in CUPSKIT_PPD_DIRS
|
||||
/// (colon-separated, e.g. PPDs extracted with `pkgutil --expand-full` from a driver package you haven't
|
||||
/// installed) and then /Library/Printers/PPDs; each test is skipped when its PPDs aren't found.
|
||||
enum ExtraPPDs {
|
||||
static let directories: [String] =
|
||||
(ProcessInfo.processInfo.environment["CUPSKIT_PPD_DIRS"] ?? "").split(separator: ":").map(String.init) + [vendorPPDs]
|
||||
|
||||
static func path(_ name: String) -> String? {
|
||||
directories.map { ($0 as NSString).appendingPathComponent(name) }.first { FileManager.default.fileExists(atPath: $0) }
|
||||
}
|
||||
|
||||
static func files(prefix: String) -> [String] {
|
||||
var seen: Set<String> = []
|
||||
return directories.flatMap { dir in
|
||||
((try? FileManager.default.contentsOfDirectory(atPath: dir)) ?? [])
|
||||
.filter { $0.hasPrefix(prefix) && seen.insert($0).inserted }
|
||||
.sorted()
|
||||
.map { (dir as NSString).appendingPathComponent($0) }
|
||||
}
|
||||
}
|
||||
|
||||
static func context(path: String) -> DriverContext? {
|
||||
(try? PPD.text(contentsOfFile: path)).map { DriverContext(ppd: PPD(text: $0), attributes: nil) }
|
||||
}
|
||||
|
||||
static func context(_ name: String) -> DriverContext? { path(name).flatMap(context(path:)) }
|
||||
}
|
||||
|
||||
private func resolved(_ id: String, _ c: DriverContext) -> ResolvedQuickAction? {
|
||||
try? QuickAction.named(id)!.resolve(c, value: "0").get()
|
||||
}
|
||||
|
||||
private func pairs(_ id: String, _ c: DriverContext) -> [String]? {
|
||||
resolved(id, c)?.changes.map { "\($0.key)=\($0.value)" }
|
||||
}
|
||||
|
||||
private func hasChoice(_ c: DriverContext, _ keyword: String, _ choice: String) -> Bool {
|
||||
c.ppd?.option(keyword)?.choices.contains(choice) == true
|
||||
}
|
||||
|
||||
@Suite struct CanonProfile {
|
||||
static let adv = "CNPZUIRAC3530IIIZU.ppd.gz"
|
||||
|
||||
@Test(.enabled(if: ExtraPPDs.path(adv) != nil))
|
||||
func imageRunnerAdvance() throws {
|
||||
let c = try #require(ExtraPPDs.context(Self.adv))
|
||||
#expect(DriverProfiles.matching(c).map(\.id) == ["canon", "generic"])
|
||||
#expect(pairs("color", c) == ["CNColorMode=color"])
|
||||
#expect(pairs("bw", c) == ["CNColorMode=mono"])
|
||||
#expect(pairs("duplex", c) == ["CNDuplex=DuplexFront"])
|
||||
#expect(pairs("simplex", c) == ["CNDuplex=None"])
|
||||
#expect(pairs("letter", c) == ["PageSize=Letter"])
|
||||
for id in ["color", "bw", "duplex", "simplex", "letter"] { #expect(resolved(id, c)?.profile.id == "canon", "\(id)") }
|
||||
// No fit-to-paper option in Canon's PPDs: Letter sets the page size only, and the title says so.
|
||||
#expect(QuickAction.named("letter")!.title(in: c) == "Use Letter Paper")
|
||||
#expect(QuickAction.named("usercode")!.unavailableReason(in: c) == "Canon department IDs are set by the Canon driver, not the PPD")
|
||||
#expect(throws: QuickActionUnavailable.self) { try QuickAction.named("usercode")!.resolve(c, clearing: true).get() }
|
||||
}
|
||||
|
||||
/// Every Canon PPD in the package: an action is offered exactly when the PPD has its keyword and choice.
|
||||
@Test(.enabled(if: !ExtraPPDs.files(prefix: "CNPZU").isEmpty))
|
||||
func everyCanonPPD() throws {
|
||||
let files = ExtraPPDs.files(prefix: "CNPZU")
|
||||
var mono = 0
|
||||
for file in files {
|
||||
let c = try #require(ExtraPPDs.context(path: file), "\(file)")
|
||||
let name = (file as NSString).lastPathComponent
|
||||
#expect(DriverProfiles.matching(c).first?.id == "canon", "\(name)")
|
||||
#expect(QuickAction.named("color")!.isAvailable(in: c) == hasChoice(c, "CNColorMode", "color"), "\(name)")
|
||||
#expect(QuickAction.named("bw")!.isAvailable(in: c) == hasChoice(c, "CNColorMode", "mono"), "\(name)")
|
||||
#expect(QuickAction.named("duplex")!.isAvailable(in: c) == hasChoice(c, "CNDuplex", "DuplexFront"), "\(name)")
|
||||
#expect(QuickAction.named("simplex")!.isAvailable(in: c) == hasChoice(c, "CNDuplex", "None"), "\(name)")
|
||||
#expect(pairs("letter", c) == ["PageSize=Letter"], "\(name)")
|
||||
#expect(QuickAction.named("letter")!.title(in: c) == "Use Letter Paper", "\(name)")
|
||||
#expect(QuickAction.named("usercode")!.unavailableReason(in: c) == "Canon department IDs are set by the Canon driver, not the PPD")
|
||||
if !hasChoice(c, "CNColorMode", "color") { mono += 1 }
|
||||
}
|
||||
#expect(files.count - mono > 0 && mono > 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Suite struct HPProfile {
|
||||
static let colorLaser = "HP Color LaserJet M750.gz"
|
||||
static let officejet = "HP Officejet Pro 8600.ppd.gz"
|
||||
static let monoLaser = "HP LaserJet 600 M601 M602 M603.gz"
|
||||
static let reason = "HP drivers have no accounting code option"
|
||||
|
||||
@Test(.enabled(if: ExtraPPDs.path(colorLaser) != nil))
|
||||
func colorLaserJetEnterprise() throws {
|
||||
let c = try #require(ExtraPPDs.context(Self.colorLaser))
|
||||
#expect(DriverProfiles.matching(c).map(\.id) == ["hp", "generic"])
|
||||
#expect(pairs("color", c) == ["HPColorAsGray=False"])
|
||||
#expect(pairs("bw", c) == ["HPColorAsGray=True"])
|
||||
#expect(resolved("color", c)?.profile.id == "hp")
|
||||
#expect(pairs("duplex", c) == ["Duplex=DuplexNoTumble"])
|
||||
#expect(pairs("simplex", c) == ["Duplex=None"])
|
||||
#expect(resolved("duplex", c)?.profile.id == "generic")
|
||||
#expect(pairs("letter", c) == ["PageSize=Letter"])
|
||||
#expect(QuickAction.named("letter")!.title(in: c) == "Use Letter Paper")
|
||||
#expect(QuickAction.named("usercode")!.unavailableReason(in: c) == Self.reason)
|
||||
}
|
||||
|
||||
@Test(.enabled(if: ExtraPPDs.path(officejet) != nil))
|
||||
func officejetProUsesHPColorMode() throws {
|
||||
let c = try #require(ExtraPPDs.context(Self.officejet))
|
||||
#expect(pairs("color", c) == ["HPColorMode=colorsmart"])
|
||||
#expect(pairs("bw", c) == ["HPColorMode=grayscale"])
|
||||
#expect(resolved("bw", c)?.profile.id == "hp")
|
||||
#expect(pairs("duplex", c) == ["Duplex=DuplexNoTumble"])
|
||||
#expect(pairs("letter", c) == ["PageSize=Letter"])
|
||||
#expect(QuickAction.named("usercode")!.unavailableReason(in: c) == Self.reason)
|
||||
}
|
||||
|
||||
@Test(.enabled(if: ExtraPPDs.path(monoLaser) != nil))
|
||||
func monoLaserJet() throws {
|
||||
let c = try #require(ExtraPPDs.context(Self.monoLaser))
|
||||
#expect(!QuickAction.named("color")!.isAvailable(in: c) && !QuickAction.named("bw")!.isAvailable(in: c))
|
||||
#expect(pairs("duplex", c) == ["Duplex=DuplexNoTumble"])
|
||||
#expect(pairs("letter", c) == ["PageSize=Letter"])
|
||||
#expect(QuickAction.named("usercode")!.unavailableReason(in: c) == Self.reason)
|
||||
}
|
||||
|
||||
/// Every HP PPD: color and black & white come from the HP profile whenever the PPD has HPColorAsGray or HPColorMode.
|
||||
@Test(.enabled(if: ExtraPPDs.files(prefix: "HP ").count > 100))
|
||||
func everyHPPPD() throws {
|
||||
var hpColor = 0
|
||||
for file in ExtraPPDs.files(prefix: "HP ") {
|
||||
let c = try #require(ExtraPPDs.context(path: file), "\(file)")
|
||||
let name = (file as NSString).lastPathComponent
|
||||
guard c.manufacturer == "HP" else { continue }
|
||||
#expect(DriverProfiles.matching(c).first?.id == "hp", "\(name)")
|
||||
#expect(QuickAction.named("usercode")!.unavailableReason(in: c) == Self.reason, "\(name)")
|
||||
if hasChoice(c, "HPColorAsGray", "True") || hasChoice(c, "HPColorMode", "grayscale") {
|
||||
#expect(resolved("bw", c)?.profile.id == "hp", "\(name)")
|
||||
hpColor += 1
|
||||
}
|
||||
if hasChoice(c, "HPColorAsGray", "False") || hasChoice(c, "HPColorMode", "colorsmart") {
|
||||
#expect(resolved("color", c)?.profile.id == "hp", "\(name)")
|
||||
}
|
||||
if hasChoice(c, "PageSize", "Letter") { #expect(QuickAction.named("letter")!.title(in: c) == "Use Letter Paper", "\(name)") }
|
||||
}
|
||||
#expect(hpColor > 0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user