Fix #188: Add Media Type selection and bypass macOS ColorSync and driver color management #190

Merged
gronod merged 1 commits from fix/issue-188 into development 2026-09-02 18:07:34 +01:00
5 changed files with 162 additions and 71 deletions
+34 -14
View File
@@ -32,20 +32,39 @@ pub fn build_lp_args(
.and_then(|o| o.ppd_uncorrected_passthrough) .and_then(|o| o.ppd_uncorrected_passthrough)
.unwrap_or(false); .unwrap_or(false);
if ppd_fallback { // Always apply ColorSync bypass on macOS for targeting
args.push("-o".to_string());
args.push("ColorModel=Gray".to_string());
args.push("-o".to_string());
args.push("cm-calibration".to_string());
} else {
args.push("-o".to_string());
args.push("raw".to_string());
}
// macOS Apple ColorSync suppression flags
args.push("-o".to_string()); args.push("-o".to_string());
args.push("AP_ColorMatchingMode=AP_ApplicationColorMatching".to_string()); args.push("AP_ColorMatchingMode=AP_ApplicationColorMatching".to_string());
// Fetch lpoptions for this printer to detect capabilities
if let Ok(out) = std::process::Command::new("lpoptions")
.args(["-p", printer_name, "-l"])
.output()
{
if out.status.success() {
let output_str = String::from_utf8_lossy(&out.stdout);
if let Some(opts) = options {
if let Some(ref media_type) = opts.media_type {
if !media_type.trim().is_empty() {
let key = crate::print::unix::detect_media_type_key(&output_str);
args.push("-o".to_string());
args.push(format!("{}={}", key, media_type.trim()));
}
}
}
if ppd_fallback {
if let Some((bypass_key, bypass_val)) =
crate::print::unix::detect_driver_color_bypass(&output_str)
{
args.push("-o".to_string());
args.push(format!("{}={}", bypass_key, bypass_val));
}
}
}
}
if let Some(opts) = options { if let Some(opts) = options {
if let Some(ref orient) = opts.orientation { if let Some(ref orient) = opts.orientation {
args.push("-o".to_string()); args.push("-o".to_string());
@@ -104,11 +123,12 @@ pub fn print_target(
/// Open macOS printer queue / properties management or inspect queue options. /// Open macOS printer queue / properties management or inspect queue options.
pub fn show_printer_properties(printer_name: &str) -> Result<(), String> { pub fn show_printer_properties(printer_name: &str) -> Result<(), String> {
// On macOS, open Print & Scan preference pane or query printer status via lpstat // macOS System Preferences don't show PPD driver options.
// We open the CUPS web interface instead.
let url = format!("http://localhost:631/printers/{}", printer_name);
let _ = std::process::Command::new("open") let _ = std::process::Command::new("open")
.args(["x-apple.systempreferences:com.apple.preference.printfax"]) .args([&url])
.spawn(); .spawn();
let _ = printer_name;
Ok(()) Ok(())
} }
+9
View File
@@ -21,10 +21,18 @@ pub struct PrinterPaperSize {
pub name: String, pub name: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PrinterMediaType {
pub id: String,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PrinterCapabilities { pub struct PrinterCapabilities {
pub trays: Vec<PrinterTray>, pub trays: Vec<PrinterTray>,
pub paper_sizes: Vec<PrinterPaperSize>, pub paper_sizes: Vec<PrinterPaperSize>,
#[serde(default)]
pub media_types: Vec<PrinterMediaType>,
pub supports_orientation: bool, pub supports_orientation: bool,
} }
@@ -33,6 +41,7 @@ pub struct PrintOptions {
pub paper_source: Option<u16>, pub paper_source: Option<u16>,
pub orientation: Option<String>, pub orientation: Option<String>,
pub paper_size: Option<String>, pub paper_size: Option<String>,
pub media_type: Option<String>,
pub ppd_uncorrected_passthrough: Option<bool>, pub ppd_uncorrected_passthrough: Option<bool>,
} }
+97 -57
View File
@@ -2,7 +2,7 @@
use std::path::Path; use std::path::Path;
use crate::print::{ use crate::print::{
PrintOptions, Printer, PrinterCapabilities, PrinterPaperSize, PrinterTray, PrintOptions, Printer, PrinterCapabilities, PrinterPaperSize, PrinterTray, PrinterMediaType,
}; };
/// Parse `lpstat -e` output into a list of printer destination names. /// Parse `lpstat -e` output into a list of printer destination names.
@@ -134,10 +134,11 @@ pub fn get_printers() -> Result<Vec<Printer>, String> {
Ok(merge_printer_info(&destinations, &statuses, default_dest.as_deref())) Ok(merge_printer_info(&destinations, &statuses, default_dest.as_deref()))
} }
/// Parse `lpoptions -p <printer> -l` output into trays and paper sizes. /// Parse `lpoptions -p <printer> -l` output into trays and paper sizes and media types.
pub fn parse_lpoptions_l(output: &str) -> (Vec<PrinterTray>, Vec<PrinterPaperSize>) { pub fn parse_lpoptions_l(output: &str) -> (Vec<PrinterTray>, Vec<PrinterPaperSize>, Vec<PrinterMediaType>) {
let mut trays = Vec::new(); let mut trays = Vec::new();
let mut paper_sizes = Vec::new(); let mut paper_sizes = Vec::new();
let mut media_types = Vec::new();
for line in output.lines() { for line in output.lines() {
let trimmed = line.trim(); let trimmed = line.trim();
@@ -165,11 +166,61 @@ pub fn parse_lpoptions_l(output: &str) -> (Vec<PrinterTray>, Vec<PrinterPaperSiz
name: clean_val.to_string(), name: clean_val.to_string(),
}); });
} }
} else if key_name.eq_ignore_ascii_case("CNIJMediaType") || key_name.eq_ignore_ascii_case("MediaType") || key_name.eq_ignore_ascii_case("StpMediaType") {
for val in values {
let clean_val = val.trim_start_matches('*');
media_types.push(PrinterMediaType {
id: clean_val.to_string(),
name: clean_val.to_string(),
});
}
} }
} }
} }
(trays, paper_sizes) (trays, paper_sizes, media_types)
}
pub fn parse_ppd_media_types(ppd_content: &str) -> Vec<PrinterMediaType> {
let mut media = Vec::new();
for line in ppd_content.lines() {
if line.starts_with("*CNIJMediaType ") || line.starts_with("*MediaType ") || line.starts_with("*StpMediaType ") {
if let Some((id_part, name_part)) = line.split_once('/') {
let id = id_part.split_whitespace().last().unwrap_or("").trim().to_string();
let name = name_part.split(':').next().unwrap_or("").trim().to_string();
if !id.is_empty() {
media.push(PrinterMediaType { id, name });
}
}
}
}
media
}
pub fn detect_driver_color_bypass(output: &str) -> Option<(&'static str, &'static str)> {
if output.contains("CNIJIntent2") {
Some(("CNIJIntent2", "4"))
} else if output.contains("CNIJIntent") {
Some(("CNIJIntent", "4"))
} else if output.contains("ColorCorrection") {
Some(("ColorCorrection", "Uncorrected"))
} else if output.contains("StpColorCorrection") {
Some(("StpColorCorrection", "Uncorrected"))
} else if output.contains("EpsonColorMode") {
Some(("EpsonColorMode", "Off"))
} else {
None
}
}
pub fn detect_media_type_key(output: &str) -> &'static str {
if output.contains("CNIJMediaType") {
"CNIJMediaType"
} else if output.contains("StpMediaType") {
"StpMediaType"
} else {
"MediaType"
}
} }
/// Query CUPS printer capabilities via `lpoptions -p <printer> -l`. /// Query CUPS printer capabilities via `lpoptions -p <printer> -l`.
@@ -178,16 +229,24 @@ pub fn get_printer_capabilities(printer_name: &str) -> Result<PrinterCapabilitie
.args(["-p", printer_name, "-l"]) .args(["-p", printer_name, "-l"])
.output(); .output();
let (trays, paper_sizes) = match output { let (trays, paper_sizes, mut media_types) = match output {
Ok(out) if out.status.success() => { Ok(out) if out.status.success() => {
parse_lpoptions_l(&String::from_utf8_lossy(&out.stdout)) parse_lpoptions_l(&String::from_utf8_lossy(&out.stdout))
} }
_ => (Vec::new(), Vec::new()), _ => (Vec::new(), Vec::new(), Vec::new()),
}; };
if let Ok(ppd_content) = std::fs::read_to_string(format!("/etc/cups/ppd/{}.ppd", printer_name)) {
let ppd_media = parse_ppd_media_types(&ppd_content);
if !ppd_media.is_empty() {
media_types = ppd_media;
}
}
Ok(PrinterCapabilities { Ok(PrinterCapabilities {
trays, trays,
paper_sizes, paper_sizes,
media_types,
supports_orientation: true, supports_orientation: true,
}) })
} }
@@ -241,6 +300,14 @@ pub fn build_lp_args(
args.push(format!("PageSize={}", page_size.trim())); args.push(format!("PageSize={}", page_size.trim()));
} }
} }
if let Some(ref media_type) = opts.media_type {
if !media_type.trim().is_empty() {
// Here we just use a generic MediaType, but macos.rs will use the detected key
args.push("-o".to_string());
args.push(format!("MediaType={}", media_type.trim()));
}
}
} }
args.push(tiff_path.to_string()); args.push(tiff_path.to_string());
@@ -384,7 +451,7 @@ PageSize/Media Size: *A4 Letter Legal A3\n\
InputSlot/Media Source: *Auto Upper Lower Rear Manual\n\ InputSlot/Media Source: *Auto Upper Lower Rear Manual\n\
Duplex/2-Sided Printing: *None DuplexNoTumble DuplexTumble\n\ Duplex/2-Sided Printing: *None DuplexNoTumble DuplexTumble\n\
"; ";
let (trays, paper_sizes) = parse_lpoptions_l(sample); let (trays, paper_sizes, media_types) = parse_lpoptions_l(sample);
assert_eq!(trays.len(), 5); assert_eq!(trays.len(), 5);
assert_eq!(trays[0].name, "Auto"); assert_eq!(trays[0].name, "Auto");
assert_eq!(trays[1].name, "Upper"); assert_eq!(trays[1].name, "Upper");
@@ -396,43 +463,30 @@ Duplex/2-Sided Printing: *None DuplexNoTumble DuplexTumble\n\
} }
#[test] #[test]
fn test_build_lp_args_raw() { fn test_parse_ppd_media_types() {
let args = build_lp_args("Epson-Stylus-SX420W", "/tmp/target.tif", None); let sample = "\
assert_eq!( *CNIJMediaType 42/Photo Paper Plus Semi-gloss: \"\"
args, *CNIJMediaType 43/Photo Paper Pro Platinum: \"\"
vec![ ";
"-d", let media = parse_ppd_media_types(sample);
"Epson-Stylus-SX420W", assert_eq!(media.len(), 2);
"-t", assert_eq!(media[0].id, "42");
"ICCery Target - target.tif", assert_eq!(media[0].name, "Photo Paper Plus Semi-gloss");
"-o",
"raw",
"/tmp/target.tif"
]
);
} }
#[test] #[test]
fn test_build_lp_args_ppd_fallback() { fn test_detect_driver_color_bypass() {
let opts = PrintOptions { assert_eq!(detect_driver_color_bypass("CNIJIntent2: *0 1 2 4"), Some(("CNIJIntent2", "4")));
ppd_uncorrected_passthrough: Some(true), assert_eq!(detect_driver_color_bypass("EpsonColorMode: Off *1 2"), Some(("EpsonColorMode", "Off")));
..Default::default() assert_eq!(detect_driver_color_bypass("SomethingElse: 1"), None);
}; }
let args = build_lp_args("Epson-Stylus-SX420W", "/tmp/target.tif", Some(&opts));
assert_eq!( #[test]
args, fn test_build_lp_args_raw() {
vec![ let args = build_lp_args("Epson-Stylus-SX420W", "/tmp/target.tif", None);
"-d", assert_eq!(args.len(), 5);
"Epson-Stylus-SX420W", assert_eq!(args[0], "-d");
"-t", assert_eq!(args[4], "/tmp/target.tif");
"ICCery Target - target.tif",
"-o",
"ColorModel=Gray",
"-o",
"cm-calibration",
"/tmp/target.tif"
]
);
} }
#[test] #[test]
@@ -444,22 +498,8 @@ Duplex/2-Sided Printing: *None DuplexNoTumble DuplexTumble\n\
..Default::default() ..Default::default()
}; };
let args = build_lp_args("Epson-Stylus-SX420W", "/tmp/target.tif", Some(&opts)); let args = build_lp_args("Epson-Stylus-SX420W", "/tmp/target.tif", Some(&opts));
assert_eq!( assert!(args.contains(&"orientation-requested=4".to_string()));
args, assert!(args.contains(&"PageSize=A4".to_string()));
vec![
"-d",
"Epson-Stylus-SX420W",
"-t",
"ICCery Target - target.tif",
"-o",
"raw",
"-o",
"orientation-requested=4",
"-o",
"PageSize=A4",
"/tmp/target.tif"
]
);
} }
#[test] #[test]
+7
View File
@@ -504,6 +504,13 @@
</select> </select>
</div> </div>
<div class="form-group media-group" id="mediaTypeGroup">
<label for="printerMediaTypeSelect">Media Type</label>
<select id="printerMediaTypeSelect">
<option value="" selected>Default / Auto Select</option>
</select>
</div>
<div class="form-group orientation-group"> <div class="form-group orientation-group">
<label>Orientation</label> <label>Orientation</label>
<div class="orientation-toggle-group"> <div class="orientation-toggle-group">
+15
View File
@@ -146,6 +146,7 @@ export function initPrinttarg() {
const btnRefreshPrinters = document.getElementById("btnRefreshPrinters"); const btnRefreshPrinters = document.getElementById("btnRefreshPrinters");
const btnPrinterProperties = document.getElementById("btnPrinterProperties"); const btnPrinterProperties = document.getElementById("btnPrinterProperties");
const printerTraySelect = document.getElementById("printerTraySelect"); const printerTraySelect = document.getElementById("printerTraySelect");
const printerMediaTypeSelect = document.getElementById("printerMediaTypeSelect");
const btnOrientPortrait = document.getElementById("btnOrientPortrait"); const btnOrientPortrait = document.getElementById("btnOrientPortrait");
const btnOrientLandscape = document.getElementById("btnOrientLandscape"); const btnOrientLandscape = document.getElementById("btnOrientLandscape");
const printerStatusBadge = document.getElementById("printerStatusBadge"); const printerStatusBadge = document.getElementById("printerStatusBadge");
@@ -219,6 +220,9 @@ export function initPrinttarg() {
async function loadPrinterCapabilities(printerName) { async function loadPrinterCapabilities(printerName) {
if (!printerTraySelect || !printerName) return; if (!printerTraySelect || !printerName) return;
printerTraySelect.innerHTML = '<option value="" selected>Default / Auto Select</option>'; printerTraySelect.innerHTML = '<option value="" selected>Default / Auto Select</option>';
if (printerMediaTypeSelect) {
printerMediaTypeSelect.innerHTML = '<option value="" selected>Default / Auto Select</option>';
}
try { try {
const caps = await invoke("get_printer_capabilities", { printerName }); const caps = await invoke("get_printer_capabilities", { printerName });
@@ -230,6 +234,15 @@ export function initPrinttarg() {
printerTraySelect.appendChild(opt); printerTraySelect.appendChild(opt);
}); });
} }
if (caps && caps.media_types && caps.media_types.length > 0 && printerMediaTypeSelect) {
caps.media_types.forEach(media => {
const opt = document.createElement("option");
opt.value = media.id;
opt.textContent = media.name;
printerMediaTypeSelect.appendChild(opt);
});
}
} catch (err) { } catch (err) {
console.warn("[ICCery Print] Could not fetch printer capabilities:", err); console.warn("[ICCery Print] Could not fetch printer capabilities:", err);
} }
@@ -242,11 +255,13 @@ export function initPrinttarg() {
const trayVal = printerTraySelect ? printerTraySelect.value : ""; const trayVal = printerTraySelect ? printerTraySelect.value : "";
const paperSource = trayVal ? parseInt(trayVal, 10) : null; const paperSource = trayVal ? parseInt(trayVal, 10) : null;
const ppdFallback = chkPpdFallback ? chkPpdFallback.checked : false; const ppdFallback = chkPpdFallback ? chkPpdFallback.checked : false;
const mediaType = printerMediaTypeSelect ? printerMediaTypeSelect.value : "";
return { return {
paper_source: paperSource, paper_source: paperSource,
orientation: selectedOrientation, orientation: selectedOrientation,
paper_size: pageSizeSelect ? pageSizeSelect.value : null, paper_size: pageSizeSelect ? pageSizeSelect.value : null,
media_type: mediaType ? mediaType : null,
ppd_uncorrected_passthrough: ppdFallback, ppd_uncorrected_passthrough: ppdFallback,
}; };
} }