forked from 1b-agent/opencadstudio-upstream
feat(plot): Windows printer media and printable areas
The plot dialog asks CUPS printers for their sheets and margins; on Windows the query was a stub, so every printer showed the paper catalogue with unverified margins. The driver is now asked the way the plot dialog expects: - windows_media (compiled and tested everywhere): the three parallel DeviceCapabilities tables (ids, sizes in tenths of a millimetre, 64-WCHAR names) become one PrinterMedia per distinct size — a rotated or borderless twin folds onto its sheet, catalogue sheets keep their name, unknown sizes carry the driver's — with the device's margins on every entry and the driver's default sheet found by id or by user size. - print_to_printer (Windows): DeviceCapabilitiesW for the tables, the default DEVMODE for the default sheet, one portrait printer DC for the unprintable margins (physical offsets and extents, per device — a documented approximation). The DEVMODE fetch is shared with the print path (with_printer_devmode). - A direct print now asks the driver for the plot's own sheet (by id, or as a user-defined size), so an A3 plot prints on A3 rather than shrunk into the driver's default; a driver that refuses the sheet is asked for the orientation alone. - The capability cache remembers printers that could not be asked, so a slow failing query runs once per session, not on every dialog open. - PRINTERS <name> reports a printer's sheets, margins and default sheet. Tests: names (NUL trim, full slot, lossy), media folding and dropping, default by id / user size / none, DC margins, paper id lookup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c36ae08fd0
commit
b8472a00b4
27 files changed
+721
-40
No files matched your search
+35
-14
@@ -190,28 +190,36 @@ impl PrinterCapabilities {
|
||||
}
|
||||
}
|
||||
|
||||
fn capability_cache() -> &'static Mutex<HashMap<String, Arc<PrinterCapabilities>>> {
|
||||
static CACHE: OnceLock<Mutex<HashMap<String, Arc<PrinterCapabilities>>>> = OnceLock::new();
|
||||
/// Answers per printer for the session; a printer that could not be asked
|
||||
/// is remembered as `None` too, so a slow failing query (an offline network
|
||||
/// queue) runs once, not on every dialog open.
|
||||
type CapabilityCache = HashMap<String, Option<Arc<PrinterCapabilities>>>;
|
||||
|
||||
fn capability_cache() -> &'static Mutex<CapabilityCache> {
|
||||
static CACHE: OnceLock<Mutex<CapabilityCache>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
/// The cached capabilities of `printer`, if they were queried already.
|
||||
pub fn cached_printer_capabilities(printer: &str) -> Option<Arc<PrinterCapabilities>> {
|
||||
capability_cache().lock().ok()?.get(printer).cloned()
|
||||
capability_cache().lock().ok()?.get(printer).cloned().flatten()
|
||||
}
|
||||
|
||||
/// Query `printer` for its media (blocking, one process spawn) and cache the
|
||||
/// answer. Returns `None` when the platform cannot answer — the plot dialog
|
||||
/// then falls back to the paper catalogue with unverified margins.
|
||||
/// Query `printer` for its media (blocking: a process spawn or a driver
|
||||
/// round trip) and cache the answer. Returns `None` when the platform or
|
||||
/// the printer cannot answer — the plot dialog then falls back to the paper
|
||||
/// catalogue with unverified margins.
|
||||
pub fn printer_capabilities(printer: &str) -> Option<Arc<PrinterCapabilities>> {
|
||||
if let Some(cached) = cached_printer_capabilities(printer) {
|
||||
return Some(cached);
|
||||
if let Ok(cache) = capability_cache().lock() {
|
||||
if let Some(answer) = cache.get(printer) {
|
||||
return answer.clone();
|
||||
}
|
||||
}
|
||||
let caps = Arc::new(query_printer_capabilities(printer)?);
|
||||
let caps = query_printer_capabilities(printer).map(Arc::new);
|
||||
if let Ok(mut cache) = capability_cache().lock() {
|
||||
cache.insert(printer.to_string(), Arc::clone(&caps));
|
||||
cache.insert(printer.to_string(), caps.clone());
|
||||
}
|
||||
Some(caps)
|
||||
caps
|
||||
}
|
||||
|
||||
/// Name of the system default printer, when the platform reports one.
|
||||
@@ -261,9 +269,15 @@ fn query_printer_capabilities(printer: &str) -> Option<PrinterCapabilities> {
|
||||
parse_ipp_media(&String::from_utf8_lossy(&output.stdout))
|
||||
}
|
||||
|
||||
/// Windows media enumeration (`DeviceCapabilities`) lands with native GDI
|
||||
/// printing; until then the catalogue serves every printer.
|
||||
#[cfg(any(target_arch = "wasm32", target_os = "windows"))]
|
||||
/// The driver's sheet tables and a printer DC's margins (see
|
||||
/// `windows_media`); `None` for a queue that lists no sheet.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn query_printer_capabilities(printer: &str) -> Option<PrinterCapabilities> {
|
||||
let raw = crate::io::print_to_printer::windows_printer_media(printer)?;
|
||||
crate::io::windows_media::printer_media(&raw)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn query_printer_capabilities(_printer: &str) -> Option<PrinterCapabilities> {
|
||||
None
|
||||
}
|
||||
@@ -276,6 +290,7 @@ fn query_printer_capabilities(_printer: &str) -> Option<PrinterCapabilities> {
|
||||
/// with sizes and margins in hundredths of a millimetre, and
|
||||
/// `media-default (keyword) = iso_a4_210x297mm`. Borderless variants repeat a
|
||||
/// size with zero margins; they collapse onto the bordered entry.
|
||||
#[cfg_attr(any(target_arch = "wasm32", target_os = "windows"), allow(dead_code))]
|
||||
fn parse_ipp_media(text: &str) -> Option<PrinterCapabilities> {
|
||||
let database = attribute_value(text, "media-col-database")?;
|
||||
let mut media: Vec<PrinterMedia> = Vec::new();
|
||||
@@ -337,6 +352,7 @@ fn parse_ipp_media(text: &str) -> Option<PrinterCapabilities> {
|
||||
}
|
||||
|
||||
/// The text after `name (…) = ` on the line that declares `name`.
|
||||
#[cfg_attr(any(target_arch = "wasm32", target_os = "windows"), allow(dead_code))]
|
||||
fn attribute_value<'a>(text: &'a str, name: &str) -> Option<&'a str> {
|
||||
text.lines().find_map(|line| {
|
||||
let line = line.trim_start();
|
||||
@@ -350,12 +366,14 @@ fn attribute_value<'a>(text: &'a str, name: &str) -> Option<&'a str> {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(any(target_arch = "wasm32", target_os = "windows"), allow(dead_code))]
|
||||
fn collection_size_mm(entry: &str) -> Option<(f64, f64)> {
|
||||
let x = hundredths(entry, "x-dimension")?;
|
||||
let y = hundredths(entry, "y-dimension")?;
|
||||
(x > 0.0 && y > 0.0).then_some((x, y))
|
||||
}
|
||||
|
||||
#[cfg_attr(any(target_arch = "wasm32", target_os = "windows"), allow(dead_code))]
|
||||
fn hundredths(entry: &str, key: &str) -> Option<f64> {
|
||||
let start = entry.find(key)? + key.len();
|
||||
let rest = entry[start..].trim_start_matches('=');
|
||||
@@ -363,10 +381,12 @@ fn hundredths(entry: &str, key: &str) -> Option<f64> {
|
||||
digits.parse::<f64>().ok().map(|v| v / 100.0)
|
||||
}
|
||||
|
||||
#[cfg_attr(any(target_arch = "wasm32", target_os = "windows"), allow(dead_code))]
|
||||
fn hundredths_mm(entry: &str, key: &str) -> f64 {
|
||||
hundredths(entry, key).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
#[cfg_attr(any(target_arch = "wasm32", target_os = "windows"), allow(dead_code))]
|
||||
fn portrait(w: f64, h: f64) -> (f64, f64) {
|
||||
if w <= h {
|
||||
(w, h)
|
||||
@@ -377,6 +397,7 @@ fn portrait(w: f64, h: f64) -> (f64, f64) {
|
||||
|
||||
/// Dimensions from a PWG self-describing media name such as
|
||||
/// `iso_a4_210x297mm` or `na_letter_8.5x11in`.
|
||||
#[cfg_attr(any(target_arch = "wasm32", target_os = "windows"), allow(dead_code))]
|
||||
fn pwg_size_mm(name: &str) -> Option<(f64, f64)> {
|
||||
let dims = name.rsplit('_').next()?;
|
||||
let (dims, units) = dims
|
||||
|
||||
Reference in new issue
Block a user