macOS CI / build-and-test (push) Skipped
- Detect vendor tray keys in CupsParsers with InputSlot/MediaSource/EPIJ_FdSo/EPIJ_PSrc/CNIJMediaSupply roster; EPIJ_FdSo wins over EPIJ_PSrc. - PrinterTray now carries raw token + display name; PrinterCapabilities exposes trayKey, trayDefault and tray-specific quality maps. - CupsService enumerates trays from the detected key, enriches labels from PPD, seeds selection from CUPS default, and precomputes per-tray media→quality constraints. - MediaQualityConstraints resolves quality availability from both selected media and selected tray, falling back to the media-only map. - TicketWriteResolver emits the selected tray key=token in the locked write order between PageSize and media key. - PrintPanelService applies the Stage 2 tray token to PMPrintSettings on open and captures the tray back from the full roster plus AP_D_InputSlot. - PrintSessionViewModel keeps selectedTrayToken, applies capture-back, preserves unknown captured tokens as synthetic trays, and passes tray into spool overrides. - Stage2View replaces portrait/landscape buttons with an orientation dropdown (printerOrientationSelect) while preserving portrait/landscape semantics and orientation-requested=3|4. - Add/update core, UI and fixture tests for tray detection, enumeration, PPD labels, defaults, unknown-token synthesis, panel capture, ticket write order and tray-aware quality clamping. - Update docs/10-print-system.md, docs/11-print-macos.md, docs/21-ui-reference.md and docs/24-issues-invariants.md. Verified on the macOS 12.7.6 x86_64 CI runner: xcodebuild build-for-testing and ICCeryCoreTests pass (557 tests, 0 failures).
138 lines
5.5 KiB
Swift
138 lines
5.5 KiB
Swift
import Foundation
|
||
import ICCeryCore
|
||
|
||
/// Stage 2 inline overrides applied on top of a rehydrated ticket
|
||
/// (#201 AC3 / D6 — Stage 2 always wins).
|
||
struct TargetPrintOverrides: Equatable {
|
||
var paperSize: String? // CUPS `PageSize` token or `Custom.<w>x<h>`
|
||
var mediaType: String?
|
||
var qualityKey: String? // PrinterCapabilities.qualityKey
|
||
var quality: String?
|
||
var trayKey: String? // PrinterCapabilities.trayKey
|
||
var tray: String? // raw tray token (#218)
|
||
var orientation: String? // "portrait" | "landscape"
|
||
}
|
||
|
||
/// One resolved ticket write.
|
||
struct TicketWrite: Equatable {
|
||
let key: String
|
||
let value: String
|
||
let locked: Bool
|
||
}
|
||
|
||
/// The fully-resolved write list for one job — a **pure value**,
|
||
/// computed with zero Core Printing calls so it is unit- and
|
||
/// UI-testable without a live queue. Replaces the old `lp` argv
|
||
/// builder (#201 D4/D8).
|
||
struct ResolvedTicketWrites: Equatable {
|
||
var writes: [TicketWrite]
|
||
var paperToken: String?
|
||
var orientation: String?
|
||
}
|
||
|
||
enum TicketWriteResolver {
|
||
|
||
/// Quartz vocabulary constants (docs/14 §7) live on
|
||
/// `ColorMatchingAttempts` next to the AP_* half — written
|
||
/// alongside because there is no `lp` path left (D2).
|
||
///
|
||
/// Order is locked and is the contract the tests assert:
|
||
/// 1 `AP_ColorMatchingMode` = AP_ApplicationColorMatching (locked)
|
||
/// 2 `AP.ColorMatchingMode` = AP_ApplicationColorMatching (locked)
|
||
/// 3 `PMColorMatchingMode` = APCustomColorMatching (locked)
|
||
/// 4 `PMCustomColorMatchingProfile` = "" (unlocked)
|
||
/// 5 `com.apple.print.PrintSettings.PMColorMatchingMode`
|
||
/// = APCustomColorMatching (unlocked)
|
||
/// 6 `PageSize` = overrides.paperSize
|
||
/// 7 overrides.trayKey = overrides.tray
|
||
/// 8 detectMediaTypeKey(optionKeys) = overrides.mediaType
|
||
/// 9 overrides.qualityKey = overrides.quality
|
||
/// 10 detectDriverColorBypass(optionKeys) (unlocked)
|
||
/// 11 `orientation-requested` = 3 | 4
|
||
///
|
||
/// Invariants: `raw` can never appear (there is no `lp`); nothing
|
||
/// is suppressed because a captured key already exists (D6 — the
|
||
/// ticket carries the captured keys and Stage 2 writes land on
|
||
/// top unconditionally); an override with a `nil` value emits no
|
||
/// write.
|
||
static func resolve(
|
||
overrides: TargetPrintOverrides,
|
||
optionKeys: Set<String>
|
||
) -> ResolvedTicketWrites {
|
||
var writes: [TicketWrite] = []
|
||
|
||
// 1–2 — locked AP_* pair (layer ③).
|
||
for key in ColorMatchingAttempts.printSettingsKeys {
|
||
writes.append(TicketWrite(
|
||
key: key,
|
||
value: ColorMatchingAttempts.applicationMatchingValue,
|
||
locked: true))
|
||
}
|
||
// 3–5 — Quartz/`NSPrintOperation` vocabulary (D2).
|
||
writes.append(TicketWrite(
|
||
key: ColorMatchingAttempts.quartzModeKey,
|
||
value: ColorMatchingAttempts.quartzCustomMatching,
|
||
locked: true))
|
||
writes.append(TicketWrite(
|
||
key: ColorMatchingAttempts.quartzProfileKey,
|
||
value: "",
|
||
locked: false))
|
||
writes.append(TicketWrite(
|
||
key: ColorMatchingAttempts.quartzLegacyModeKey,
|
||
value: ColorMatchingAttempts.quartzCustomMatching,
|
||
locked: false))
|
||
|
||
// 6 — the Stage 2 paper token.
|
||
var paperToken: String?
|
||
if let paperSize = overrides.paperSize, !paperSize.isEmpty {
|
||
paperToken = paperSize
|
||
writes.append(TicketWrite(
|
||
key: "PageSize", value: paperSize, locked: false))
|
||
}
|
||
|
||
// 7 — paper source via the queue's detected tray roster key.
|
||
if let trayKey = overrides.trayKey, !trayKey.isEmpty,
|
||
let tray = overrides.tray, !tray.isEmpty {
|
||
writes.append(TicketWrite(
|
||
key: trayKey, value: tray, locked: false))
|
||
}
|
||
|
||
// 8 — media type via the queue's detected vendor key.
|
||
if let mediaType = overrides.mediaType,
|
||
let mediaKey = CupsParsers.detectMediaTypeKey(
|
||
optionKeys: optionKeys) {
|
||
writes.append(TicketWrite(
|
||
key: mediaKey, value: mediaType, locked: false))
|
||
}
|
||
|
||
// 9 — print quality; the detected enumeration key travels in
|
||
// the overrides (`PrinterCapabilities.qualityKey`, #183).
|
||
if let qualityKey = overrides.qualityKey,
|
||
let quality = overrides.quality {
|
||
writes.append(TicketWrite(
|
||
key: qualityKey, value: quality, locked: false))
|
||
}
|
||
|
||
// 10 — driver "no colour adjustment" bypass. Never gated on
|
||
// `ppdUncorrectedPassthrough` — macOS always bypasses.
|
||
if let bypass = CupsParsers.detectDriverColorBypass(
|
||
optionKeys: optionKeys) {
|
||
writes.append(TicketWrite(
|
||
key: bypass.key, value: bypass.value, locked: false))
|
||
}
|
||
|
||
// 11 — orientation, CUPS IPP codes portrait=3 / landscape=4.
|
||
if let orientation = overrides.orientation {
|
||
writes.append(TicketWrite(
|
||
key: "orientation-requested",
|
||
value: orientation == "landscape" ? "4" : "3",
|
||
locked: false))
|
||
}
|
||
|
||
return ResolvedTicketWrites(
|
||
writes: writes.filter { $0.key.lowercased() != "raw" },
|
||
paperToken: paperToken,
|
||
orientation: overrides.orientation)
|
||
}
|
||
}
|