bug: Media type selection box displays numeric PPD choice codes instead of human-readable driver names #1

Closed
opened 2026-09-07 21:38:34 +01:00 by gronod · 0 comments
Owner

Summary

In the TargetPrint Inspector sidebar, the "Media type" dropdown (typePop) displays raw numeric choice codes (e.g. 0, 13, 15, 92, 145) rather than the human-readable media type names defined by the printer driver (e.g. plain papers, Epson Premium Glossy, Epson Premium Semigloss, Epson Ultra Glossy, Photo Paper Glossy).


Investigation & Root Cause Analysis

1. PPD Translation Strings Discarded during Option Parsing

In Adobe PostScript Printer Description (PPD) files (Spec v4.3 §3.2), option choices are specified in the format:

*<OptionKeyword> <ChoiceName>[/<TranslationString>]: "<InvocationCode>"

Where:

  • <ChoiceName> is the internal PostScript / CUPS choice identifier (often an integer ID in vendor inkjet drivers, e.g. 0, 13, 92).
  • /<TranslationString> is the user-facing localized display name (e.g. /Epson Premium Glossy).

In Sources/Printing/CUPSManager.swift (optionChoices(in:keyword:), lines 179–193):

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
}

Because rest.split includes / as a delimiter and takes only .first, it extracts solely the token before / (the internal choice code 13) and completely drops the human-readable translation string (Epson Premium Glossy).

2. Raw Choice Codes Fed Directly to UI PopUpButton

In Sources/Views/InspectorView.swift:

refill(typePop, q?.mediaTypes ?? ["Plain"])

typePop (NSPopUpButton) is populated directly from q.mediaTypes. Because q.mediaTypes contains only raw extracted tokens (["0", "92", "13", "15", "145", ...]), the dropdown presents these meaningless numbers to the user.

3. Dual-Identity Requirement (UI Title vs. Driver PPD Value)

Crucially, when printing via PrintEngine.swift:

private func applyOptionalPPDKeys(to info: NSPrintInfo) {
    var extras: [String: String] = [:]
    if let mediaType = mediaType { extras["MediaType"] = mediaType }
    ...
    CUPSManager.applyVendorBypass(extras, to: info)
}

The printer driver and CUPS filters expect the computer-readable ChoiceName (e.g. MediaType=13), not the human-readable title MediaType=Epson Premium Glossy. If the UI is changed to solely store the human-readable text in engine.mediaType, driver-level option matching will fail. Each choice must preserve both:

  1. name: The computer-readable PPD choice code ("13") to be sent to CUPS / NSPrintInfo.
  2. title: The human-readable label ("Epson Premium Glossy") shown in the UI.

4. Additional Discovered Edge Cases

  • Hex character escapes in PPD translation strings: PPDs encode special characters as hex pairs like <2F> for / and <2E> for . (e.g. *MediaType 26/CD<2F>DVD: "" in Epson drivers). These must be decoded to display CD/DVD properly.
  • Vendor-specific keyword prefixes: Canon drivers (e.g. Canon Pro9500 Mark II) use *CNIJMediaType instead of *MediaType (*OpenUI *CNIJMediaType/Media Type: PickOne). Because discoverPPDOptions only searches for MediaType, Canon queues currently discover 0 media types and fall back to ["Plain"].

Steps to Reproduce

  1. Install or configure an OEM inkjet printer queue (such as an Epson XP-55 or stylus series driver).
  2. Launch TargetPrint.
  3. Select the printer in the Inspector sidebar.
  4. Inspect the "Media type" dropdown.
  5. Observed: The dropdown contains numbers: 0, 92, 13, 15, 145, etc.
  6. Expected: The dropdown displays human-readable names: plain papers, Epson Ultra Glossy, Epson Premium Glossy, Epson Premium Semigloss, Photo Paper Glossy, etc.

Implementation Plan for Fix

Phase 1: PPD Option Choice Model & Parsing (CUPSManager.swift)

  1. Introduce a choice representation struct in CUPSManager.swift:
    struct PPDChoice: Equatable {
        var name: String    // Computer-readable identifier (e.g. "13")
        var title: String   // Human-readable display label (e.g. "Epson Premium Glossy")
    }
    
  2. Implement PPD hex sequence decoding:
    • Helper function decodePPDString(_:) that translates <2F> -> /, <2E> -> ., etc.
  3. Replace optionChoices(in:keyword:) with a structured parser optionDetails(in:keywords:):
    • Match option lines: *<keyword> <choice>[/<translation>]:
    • Extract <choice> as name.
    • If /<translation> exists, trim whitespace, strip surrounding double-quotes if present, decode hex escapes, and use as title. If absent, use name as title.
    • Deduplicate choices by name.
  4. Support media type keyword aliases in discoverPPDOptions:
    • Search for MediaType as well as vendor variants like CNIJMediaType (or scan *OpenUI *<kw>/Media Type).
    • Populate structured mediaTypeChoices: [PPDChoice] in PPDOptions and PrinterQueue (retaining mediaTypes: [String] for backward compatibility where appropriate).

Phase 2: Inspector UI Integration (InspectorView.swift)

  1. Update refill helper (or add a specialized overload for [PPDChoice]):
    • For each choice, create an NSMenuItem with title = choice.title and representedObject = choice.name.
  2. Update selection logic (select(typePop, ...)):
    • Support matching by either representedObject as? String == value (if given a PPD choice code like "13") OR title == value (if given a display name like "Epson Premium Glossy").
  3. Update push(into engine:):
    • Set engine.mediaType to typePop.selectedItem?.representedObject as? String ?? typePop.titleOfSelectedItem.
    • This ensures the underlying numeric code is passed to the printing pipeline.

Phase 3: PrintEngine & TargetJob Compatibility (PrintEngine.swift, TargetJob.swift)

  1. Verify that applyOptionalPPDKeys passes the resolved choice name to extras["MediaType"] (or vendor key like CNIJMediaType if applicable).
  2. Ensure TargetJob deserialization tolerates either the PPD choice code or the display name when setting printSettings.mediaType.

Phase 4: Unit Testing & Verification

  1. Add tests in Tests/:
    • Test parsing Epson PPD media type lines with integer choices and translation strings.
    • Test parsing Canon PPD lines with *CNIJMediaType.
    • Test hex decoding (CD<2F>DVD -> CD/DVD).
    • Test InspectorView / PrintEngine selection mapping between choice code and display title.
  2. Verify existing test suite passes (swift test).
### Summary In the TargetPrint Inspector sidebar, the "Media type" dropdown (`typePop`) displays raw numeric choice codes (e.g. `0`, `13`, `15`, `92`, `145`) rather than the human-readable media type names defined by the printer driver (e.g. `plain papers`, `Epson Premium Glossy`, `Epson Premium Semigloss`, `Epson Ultra Glossy`, `Photo Paper Glossy`). --- ### Investigation & Root Cause Analysis #### 1. PPD Translation Strings Discarded during Option Parsing In Adobe PostScript Printer Description (PPD) files (Spec v4.3 §3.2), option choices are specified in the format: ```ppd *<OptionKeyword> <ChoiceName>[/<TranslationString>]: "<InvocationCode>" ``` Where: - `<ChoiceName>` is the internal PostScript / CUPS choice identifier (often an integer ID in vendor inkjet drivers, e.g. `0`, `13`, `92`). - `/<TranslationString>` is the user-facing localized display name (e.g. `/Epson Premium Glossy`). In `Sources/Printing/CUPSManager.swift` (`optionChoices(in:keyword:)`, lines 179–193): ```swift 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 } ``` Because `rest.split` includes `/` as a delimiter and takes only `.first`, it extracts solely the token before `/` (the internal choice code `13`) and completely drops the human-readable translation string (`Epson Premium Glossy`). #### 2. Raw Choice Codes Fed Directly to UI PopUpButton In `Sources/Views/InspectorView.swift`: ```swift refill(typePop, q?.mediaTypes ?? ["Plain"]) ``` `typePop` (`NSPopUpButton`) is populated directly from `q.mediaTypes`. Because `q.mediaTypes` contains only raw extracted tokens (`["0", "92", "13", "15", "145", ...]`), the dropdown presents these meaningless numbers to the user. #### 3. Dual-Identity Requirement (UI Title vs. Driver PPD Value) Crucially, when printing via `PrintEngine.swift`: ```swift private func applyOptionalPPDKeys(to info: NSPrintInfo) { var extras: [String: String] = [:] if let mediaType = mediaType { extras["MediaType"] = mediaType } ... CUPSManager.applyVendorBypass(extras, to: info) } ``` The printer driver and CUPS filters expect the computer-readable `ChoiceName` (e.g. `MediaType=13`), not the human-readable title `MediaType=Epson Premium Glossy`. If the UI is changed to solely store the human-readable text in `engine.mediaType`, driver-level option matching will fail. Each choice must preserve both: 1. `name`: The computer-readable PPD choice code (`"13"`) to be sent to CUPS / `NSPrintInfo`. 2. `title`: The human-readable label (`"Epson Premium Glossy"`) shown in the UI. #### 4. Additional Discovered Edge Cases - **Hex character escapes in PPD translation strings**: PPDs encode special characters as hex pairs like `<2F>` for `/` and `<2E>` for `.` (e.g. `*MediaType 26/CD<2F>DVD: ""` in Epson drivers). These must be decoded to display `CD/DVD` properly. - **Vendor-specific keyword prefixes**: Canon drivers (e.g. Canon Pro9500 Mark II) use `*CNIJMediaType` instead of `*MediaType` (`*OpenUI *CNIJMediaType/Media Type: PickOne`). Because `discoverPPDOptions` only searches for `MediaType`, Canon queues currently discover 0 media types and fall back to `["Plain"]`. --- ### Steps to Reproduce 1. Install or configure an OEM inkjet printer queue (such as an Epson XP-55 or stylus series driver). 2. Launch `TargetPrint`. 3. Select the printer in the Inspector sidebar. 4. Inspect the "Media type" dropdown. 5. **Observed:** The dropdown contains numbers: `0`, `92`, `13`, `15`, `145`, etc. 6. **Expected:** The dropdown displays human-readable names: `plain papers`, `Epson Ultra Glossy`, `Epson Premium Glossy`, `Epson Premium Semigloss`, `Photo Paper Glossy`, etc. --- ### Implementation Plan for Fix #### Phase 1: PPD Option Choice Model & Parsing (`CUPSManager.swift`) 1. Introduce a choice representation struct in `CUPSManager.swift`: ```swift struct PPDChoice: Equatable { var name: String // Computer-readable identifier (e.g. "13") var title: String // Human-readable display label (e.g. "Epson Premium Glossy") } ``` 2. Implement PPD hex sequence decoding: - Helper function `decodePPDString(_:)` that translates `<2F>` -> `/`, `<2E>` -> `.`, etc. 3. Replace `optionChoices(in:keyword:)` with a structured parser `optionDetails(in:keywords:)`: - Match option lines: `*<keyword> <choice>[/<translation>]:` - Extract `<choice>` as `name`. - If `/<translation>` exists, trim whitespace, strip surrounding double-quotes if present, decode hex escapes, and use as `title`. If absent, use `name` as `title`. - Deduplicate choices by `name`. 4. Support media type keyword aliases in `discoverPPDOptions`: - Search for `MediaType` as well as vendor variants like `CNIJMediaType` (or scan `*OpenUI *<kw>/Media Type`). - Populate structured `mediaTypeChoices: [PPDChoice]` in `PPDOptions` and `PrinterQueue` (retaining `mediaTypes: [String]` for backward compatibility where appropriate). #### Phase 2: Inspector UI Integration (`InspectorView.swift`) 1. Update `refill` helper (or add a specialized overload for `[PPDChoice]`): - For each choice, create an `NSMenuItem` with `title = choice.title` and `representedObject = choice.name`. 2. Update selection logic (`select(typePop, ...)`): - Support matching by either `representedObject as? String == value` (if given a PPD choice code like `"13"`) OR `title == value` (if given a display name like `"Epson Premium Glossy"`). 3. Update `push(into engine:)`: - Set `engine.mediaType` to `typePop.selectedItem?.representedObject as? String ?? typePop.titleOfSelectedItem`. - This ensures the underlying numeric code is passed to the printing pipeline. #### Phase 3: PrintEngine & TargetJob Compatibility (`PrintEngine.swift`, `TargetJob.swift`) 1. Verify that `applyOptionalPPDKeys` passes the resolved choice name to `extras["MediaType"]` (or vendor key like `CNIJMediaType` if applicable). 2. Ensure `TargetJob` deserialization tolerates either the PPD choice code or the display name when setting `printSettings.mediaType`. #### Phase 4: Unit Testing & Verification 1. Add tests in `Tests/`: - Test parsing Epson PPD media type lines with integer choices and translation strings. - Test parsing Canon PPD lines with `*CNIJMediaType`. - Test hex decoding (`CD<2F>DVD` -> `CD/DVD`). - Test `InspectorView` / `PrintEngine` selection mapping between choice code and display title. 2. Verify existing test suite passes (`swift test`).
gronod added the Kind/Bug label 2026-09-07 21:38:34 +01:00
Sign in to join this conversation.