forked from 1b-agent/opencadstudio-upstream
A page setup names its plotter, and AutoCAD looks that name up in its own
configurations: `DWG To PDF.pc3` for PDF output, the spooler name for a
system printer, `None` when nothing is assigned. Drawings saved here used
to carry the dialog's label ("Save to PDF file…"), so AutoCAD reported a
missing device on every layout plotted to PDF from Open CAD Studio.
- New `io::plot_device`: `PlotDevice { None, Pdf, Printer }` with AutoCAD's
spellings, a reader for stored names that keeps any plotter
configuration AutoCAD wrote (an HP `.pc3`, `DWF6 ePlot.pc3` — plotted to
PDF here but preserved for AutoCAD) and upgrades the legacy label.
- Printable areas: the PDF driver's margins per sheet variant (full bleed
edge to edge; standard / expand insets provisional until read off
AutoCAD's plotter configuration), and for CUPS printers the media and
margins the queue reports over IPP (`ipptool` `media-col-database`),
parsed once per printer and cached. Windows enumeration follows with
native GDI printing.
- Page setups now store the device in AutoCAD's spelling and the margins
of the chosen device/sheet, keeping the drawing's own values while
neither changed (AutoCAD's `PAPERUPDATE` behaviour). The paper-space
printable-area rectangle follows.
- Plot dialog: the default entry names the printer it resolves to; a
printer that answered lists its own sheets with a note; the media query
runs in the background and answers for a since-deselected printer are
dropped.
Tests: device names, stored-name resolution, PDF margin table, IPP media
parsing (sizes, margins, borderless folding, default sheet), PWG names,
plus dialog ↔ layout round-trips for PDF, legacy label, preserved plotter
configuration, sheet change → margins, and printer selection.
Co-Authored-By: Claude Opus 5 <[email protected]>
483 lines
19 KiB
Rust
483 lines
19 KiB
Rust
//! Plot devices: what a page setup names as its plotter, and what that device
|
|
//! can print.
|
|
//!
|
|
//! AutoCAD stores the device in `PlotSettings::printer_name` as the plotter
|
|
//! configuration's name — `DWG To PDF.pc3` for its PDF driver, the Windows
|
|
//! printer's name for a system printer, `None` when nothing is assigned. A
|
|
//! drawing saved here has to use the same spelling so AutoCAD attaches the
|
|
//! layout to its own PDF driver instead of reporting a missing device; and a
|
|
//! device name that arrives from AutoCAD (an HP plotter's `.pc3`, `DWF6
|
|
//! ePlot.pc3`) must go back out untouched even though we cannot drive it.
|
|
//!
|
|
//! The device also decides the *printable area*: the sheet inset by the
|
|
//! driver's unprintable margins, which AutoCAD writes into the layout and
|
|
//! draws as the dashed rectangle in paper space. The PDF driver's margins are
|
|
//! a fixed table; a CUPS printer reports its media and margins over IPP
|
|
//! (`media-col-database`), which is queried once per printer and cached.
|
|
|
|
use crate::io::paper_catalog::{self, PaperSize, PaperUnits, PaperVariant};
|
|
use std::collections::HashMap;
|
|
use std::sync::{Arc, Mutex, OnceLock};
|
|
|
|
/// AutoCAD's plotter configuration name for PDF output.
|
|
pub const PDF_DEVICE_NAME: &str = "DWG To PDF.pc3";
|
|
/// AutoCAD's spelling for "no plotter assigned".
|
|
pub const NO_DEVICE_NAME: &str = "None";
|
|
/// The label older Open CAD Studio builds wrote into drawings for PDF output.
|
|
/// Recognised on read so those drawings keep plotting to PDF, and always
|
|
/// rewritten with [`PDF_DEVICE_NAME`] on save.
|
|
const LEGACY_PDF_LABEL: &str = "Save to PDF file…";
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum PlotDevice {
|
|
/// No plotter assigned (AutoCAD `None`). We plot to the system default
|
|
/// printer in that case, which is what the plot dialog offers.
|
|
None,
|
|
/// PDF file output, AutoCAD's `DWG To PDF.pc3`.
|
|
Pdf,
|
|
/// A system printer by its queue / spooler name.
|
|
Printer(String),
|
|
}
|
|
|
|
impl PlotDevice {
|
|
/// The name AutoCAD would store for this device.
|
|
pub fn canonical_name(&self) -> String {
|
|
match self {
|
|
PlotDevice::None => NO_DEVICE_NAME.to_string(),
|
|
PlotDevice::Pdf => PDF_DEVICE_NAME.to_string(),
|
|
PlotDevice::Printer(name) => name.clone(),
|
|
}
|
|
}
|
|
|
|
/// Interpret a stored `printer_name`. Returns the device and whether the
|
|
/// stored spelling should be preserved on save: an AutoCAD plotter
|
|
/// configuration we cannot drive (`DWF6 ePlot.pc3`, an HP `.pc3`) is
|
|
/// treated as PDF output but keeps its name, so the drawing still attaches
|
|
/// to that plotter in AutoCAD; our own legacy label and an empty name are
|
|
/// not worth preserving.
|
|
pub fn from_stored_name(name: &str) -> (Self, bool) {
|
|
let name = name.trim();
|
|
if name.is_empty() || name.eq_ignore_ascii_case(NO_DEVICE_NAME) {
|
|
return (PlotDevice::None, false);
|
|
}
|
|
if name == LEGACY_PDF_LABEL {
|
|
return (PlotDevice::Pdf, false);
|
|
}
|
|
let lower = name.to_ascii_lowercase();
|
|
if lower.ends_with(".pc3") {
|
|
// Any AutoCAD plotter configuration: PDF ones are ours to drive,
|
|
// the rest plot to PDF here but keep their name for AutoCAD.
|
|
return (PlotDevice::Pdf, true);
|
|
}
|
|
(PlotDevice::Printer(name.to_string()), true)
|
|
}
|
|
|
|
/// Unprintable margins the device imposes on `paper`, in millimetres.
|
|
pub fn margins_mm(&self, paper: &PaperSize) -> Margins {
|
|
match self {
|
|
PlotDevice::Pdf => pdf_margins_mm(paper),
|
|
PlotDevice::Printer(name) => printer_margins_mm(name, paper),
|
|
// No device: AutoCAD keeps the margins of the last device or its
|
|
// defaults; without one we show the full sheet as printable.
|
|
PlotDevice::None => Margins::ZERO,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Unprintable margins in millimetres, in the order AutoCAD stores them.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct Margins {
|
|
pub left: f64,
|
|
pub bottom: f64,
|
|
pub right: f64,
|
|
pub top: f64,
|
|
}
|
|
|
|
impl Margins {
|
|
pub const ZERO: Margins = Margins {
|
|
left: 0.0,
|
|
bottom: 0.0,
|
|
right: 0.0,
|
|
top: 0.0,
|
|
};
|
|
|
|
pub const fn uniform(value: f64) -> Self {
|
|
Margins {
|
|
left: value,
|
|
bottom: value,
|
|
right: value,
|
|
top: value,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── PDF driver ────────────────────────────────────────────────────────────
|
|
|
|
/// Margins of AutoCAD's `DWG To PDF.pc3` sheets.
|
|
///
|
|
/// Full-bleed sheets print edge to edge by definition. The standard and
|
|
/// expand values are provisional — the driver's own printable areas have to
|
|
/// be read off AutoCAD's Plotter Configuration Editor (Modify Standard Paper
|
|
/// Sizes) and copied here; until then a symmetric inset keeps the printable
|
|
/// rectangle visibly distinct from the sheet.
|
|
const PDF_STANDARD_MARGIN_MM: f64 = 5.0;
|
|
const PDF_EXPAND_MARGIN_MM: f64 = 2.0;
|
|
|
|
fn pdf_margins_mm(paper: &PaperSize) -> Margins {
|
|
match paper.variant {
|
|
PaperVariant::FullBleed => Margins::ZERO,
|
|
PaperVariant::Expand => Margins::uniform(PDF_EXPAND_MARGIN_MM),
|
|
PaperVariant::Standard => Margins::uniform(PDF_STANDARD_MARGIN_MM),
|
|
}
|
|
}
|
|
|
|
// ── System printers ───────────────────────────────────────────────────────
|
|
|
|
/// One sheet a printer reports it can load, with its unprintable margins.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct PrinterMedia {
|
|
/// The sheet as our catalogue knows it (matched by size), or a custom
|
|
/// sheet built from the reported dimensions.
|
|
pub paper: PaperSize,
|
|
pub margins: Margins,
|
|
/// `true` when the printer also reports a borderless variant of this size.
|
|
pub borderless: bool,
|
|
}
|
|
|
|
/// What a printer reported about its media, cached for the session.
|
|
#[derive(Clone, Debug, Default, PartialEq)]
|
|
pub struct PrinterCapabilities {
|
|
/// Sheets in the printer's own order, one entry per distinct size.
|
|
pub media: Vec<PrinterMedia>,
|
|
/// The printer's default sheet, when reported.
|
|
pub default_paper: Option<PaperSize>,
|
|
}
|
|
|
|
impl PrinterCapabilities {
|
|
/// Margins the printer reports for `paper`, matched by size. `None` when
|
|
/// the printer never mentioned a sheet of that size.
|
|
pub fn margins_for(&self, paper: &PaperSize) -> Option<Margins> {
|
|
let (w, h) = paper.portrait_mm();
|
|
self.media
|
|
.iter()
|
|
.find(|media| {
|
|
let (mw, mh) = media.paper.portrait_mm();
|
|
(mw - w).abs() <= 0.5 && (mh - h).abs() <= 0.5
|
|
})
|
|
.map(|media| media.margins)
|
|
}
|
|
}
|
|
|
|
fn capability_cache() -> &'static Mutex<HashMap<String, Arc<PrinterCapabilities>>> {
|
|
static CACHE: OnceLock<Mutex<HashMap<String, Arc<PrinterCapabilities>>>> = 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()
|
|
}
|
|
|
|
/// 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.
|
|
pub fn printer_capabilities(printer: &str) -> Option<Arc<PrinterCapabilities>> {
|
|
if let Some(cached) = cached_printer_capabilities(printer) {
|
|
return Some(cached);
|
|
}
|
|
let caps = Arc::new(query_printer_capabilities(printer)?);
|
|
if let Ok(mut cache) = capability_cache().lock() {
|
|
cache.insert(printer.to_string(), Arc::clone(&caps));
|
|
}
|
|
Some(caps)
|
|
}
|
|
|
|
fn printer_margins_mm(printer: &str, paper: &PaperSize) -> Margins {
|
|
cached_printer_capabilities(printer)
|
|
.and_then(|caps| caps.margins_for(paper))
|
|
.unwrap_or(Margins::ZERO)
|
|
}
|
|
|
|
/// Name of the system default printer, when the platform reports one.
|
|
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
|
|
pub fn default_printer_name() -> Option<String> {
|
|
// `lpstat -d` → "system default destination: NAME" (the prefix is
|
|
// localized, the name follows the last colon).
|
|
let output = std::process::Command::new("lpstat")
|
|
.arg("-d")
|
|
.output()
|
|
.ok()?;
|
|
if !output.status.success() {
|
|
return None;
|
|
}
|
|
let text = String::from_utf8_lossy(&output.stdout);
|
|
let line = text.lines().find(|line| line.contains(':'))?;
|
|
let name = line.rsplit(':').next()?.trim();
|
|
(!name.is_empty()).then(|| name.to_string())
|
|
}
|
|
|
|
/// Windows default printer discovery lands with native GDI printing.
|
|
#[cfg(any(target_arch = "wasm32", target_os = "windows"))]
|
|
pub fn default_printer_name() -> Option<String> {
|
|
None
|
|
}
|
|
|
|
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
|
|
fn query_printer_capabilities(printer: &str) -> Option<PrinterCapabilities> {
|
|
// cupsd answers for the queue itself, so this is quick even when the
|
|
// printer is off; `ipptool` is part of every CUPS install that has
|
|
// `lpstat`. The standard test file asks for every attribute; the two we
|
|
// read are `media-col-database` and `media-default`.
|
|
let uri = format!("ipp://localhost/printers/{}", printer.replace(' ', "%20"));
|
|
let output = std::process::Command::new("ipptool")
|
|
.args(["-tv", &uri, "get-printer-attributes.test"])
|
|
.output()
|
|
.ok()?;
|
|
if !output.status.success() {
|
|
return None;
|
|
}
|
|
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"))]
|
|
fn query_printer_capabilities(_printer: &str) -> Option<PrinterCapabilities> {
|
|
None
|
|
}
|
|
|
|
/// Read the media a printer reports from `ipptool -tv` output.
|
|
///
|
|
/// The relevant lines look like
|
|
/// `media-col-database (1setOf collection) = {media-size={x-dimension=21000
|
|
/// y-dimension=29700} media-bottom-margin=300 media-left-margin=300 …},{…}`
|
|
/// 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.
|
|
fn parse_ipp_media(text: &str) -> Option<PrinterCapabilities> {
|
|
let database = attribute_value(text, "media-col-database")?;
|
|
let mut media: Vec<PrinterMedia> = Vec::new();
|
|
for entry in database.split("},{") {
|
|
let Some((w, h)) = collection_size_mm(entry) else {
|
|
continue;
|
|
};
|
|
let margins = Margins {
|
|
left: hundredths_mm(entry, "media-left-margin"),
|
|
bottom: hundredths_mm(entry, "media-bottom-margin"),
|
|
right: hundredths_mm(entry, "media-right-margin"),
|
|
top: hundredths_mm(entry, "media-top-margin"),
|
|
};
|
|
let borderless = margins == Margins::ZERO;
|
|
if let Some(existing) = media.iter_mut().find(|m| {
|
|
let (mw, mh) = m.paper.portrait_mm();
|
|
let (pw, ph) = portrait(w, h);
|
|
(mw - pw).abs() <= 0.5 && (mh - ph).abs() <= 0.5
|
|
}) {
|
|
// Keep the bordered margins as the sheet's printable area and
|
|
// remember that edge-to-edge is possible too.
|
|
if borderless {
|
|
existing.borderless = true;
|
|
} else if existing.margins == Margins::ZERO {
|
|
existing.margins = margins;
|
|
existing.borderless = true;
|
|
}
|
|
continue;
|
|
}
|
|
let (pw, ph) = portrait(w, h);
|
|
let paper = paper_catalog::match_dimensions_mm(pw, ph)
|
|
.cloned()
|
|
.unwrap_or_else(|| PaperSize::custom(pw, ph, PaperUnits::Millimeters));
|
|
media.push(PrinterMedia {
|
|
paper,
|
|
margins,
|
|
borderless,
|
|
});
|
|
}
|
|
if media.is_empty() {
|
|
return None;
|
|
}
|
|
let default_paper = attribute_value(text, "media-default")
|
|
.and_then(|name| pwg_size_mm(name.trim()))
|
|
.and_then(|(w, h)| {
|
|
let (w, h) = portrait(w, h);
|
|
media
|
|
.iter()
|
|
.find(|m| {
|
|
let (mw, mh) = m.paper.portrait_mm();
|
|
(mw - w).abs() <= 0.5 && (mh - h).abs() <= 0.5
|
|
})
|
|
.map(|m| m.paper.clone())
|
|
});
|
|
Some(PrinterCapabilities {
|
|
media,
|
|
default_paper,
|
|
})
|
|
}
|
|
|
|
/// The text after `name (…) = ` on the line that declares `name`.
|
|
fn attribute_value<'a>(text: &'a str, name: &str) -> Option<&'a str> {
|
|
text.lines().find_map(|line| {
|
|
let line = line.trim_start();
|
|
let rest = line.strip_prefix(name)?;
|
|
// The attribute name is followed by its syntax in parentheses.
|
|
if !rest.starts_with(' ') && !rest.starts_with('(') {
|
|
return None;
|
|
}
|
|
let (_, value) = rest.split_once('=')?;
|
|
Some(value.trim())
|
|
})
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
fn hundredths(entry: &str, key: &str) -> Option<f64> {
|
|
let start = entry.find(key)? + key.len();
|
|
let rest = entry[start..].trim_start_matches('=');
|
|
let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
|
|
digits.parse::<f64>().ok().map(|v| v / 100.0)
|
|
}
|
|
|
|
fn hundredths_mm(entry: &str, key: &str) -> f64 {
|
|
hundredths(entry, key).unwrap_or(0.0)
|
|
}
|
|
|
|
fn portrait(w: f64, h: f64) -> (f64, f64) {
|
|
if w <= h {
|
|
(w, h)
|
|
} else {
|
|
(h, w)
|
|
}
|
|
}
|
|
|
|
/// Dimensions from a PWG self-describing media name such as
|
|
/// `iso_a4_210x297mm` or `na_letter_8.5x11in`.
|
|
fn pwg_size_mm(name: &str) -> Option<(f64, f64)> {
|
|
let dims = name.rsplit('_').next()?;
|
|
let (dims, units) = dims
|
|
.strip_suffix("mm")
|
|
.map(|d| (d, PaperUnits::Millimeters))
|
|
.or_else(|| dims.strip_suffix("in").map(|d| (d, PaperUnits::Inches)))?;
|
|
let (w, h) = dims.split_once('x')?;
|
|
let w: f64 = w.parse().ok()?;
|
|
let h: f64 = h.parse().ok()?;
|
|
Some((units.to_mm(w), units.to_mm(h)))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const IPP_SAMPLE: &str = " media-default (keyword) = iso_a4_210x297mm\n\
|
|
media-supported (1setOf keyword) = iso_a4_210x297mm,om_a-4.borderless_210x297mm,na_letter_8.5x11in,custom_215x345mm_215x345mm\n\
|
|
media-col-database (1setOf collection) = {media-size={x-dimension=21000 y-dimension=29700} media-bottom-margin=300 media-left-margin=300 media-right-margin=300 media-top-margin=300},{media-size={x-dimension=21000 y-dimension=29700} media-bottom-margin=0 media-left-margin=0 media-right-margin=0 media-top-margin=0},{media-size={x-dimension=21590 y-dimension=27940} media-bottom-margin=635 media-left-margin=635 media-right-margin=635 media-top-margin=635},{media-size={x-dimension=21500 y-dimension=34500} media-bottom-margin=300 media-left-margin=300 media-right-margin=300 media-top-margin=300}\n";
|
|
|
|
#[test]
|
|
fn canonical_names_follow_autocad() {
|
|
assert_eq!(PlotDevice::Pdf.canonical_name(), "DWG To PDF.pc3");
|
|
assert_eq!(PlotDevice::None.canonical_name(), "None");
|
|
assert_eq!(
|
|
PlotDevice::Printer("L3270".into()).canonical_name(),
|
|
"L3270"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stored_names_resolve_and_say_whether_to_keep_them() {
|
|
assert_eq!(PlotDevice::from_stored_name(""), (PlotDevice::None, false));
|
|
assert_eq!(
|
|
PlotDevice::from_stored_name("None"),
|
|
(PlotDevice::None, false)
|
|
);
|
|
assert_eq!(
|
|
PlotDevice::from_stored_name("Save to PDF file…"),
|
|
(PlotDevice::Pdf, false)
|
|
);
|
|
assert_eq!(
|
|
PlotDevice::from_stored_name("DWG To PDF.pc3"),
|
|
(PlotDevice::Pdf, true)
|
|
);
|
|
assert_eq!(
|
|
PlotDevice::from_stored_name("AutoCAD PDF (High Quality Print).pc3"),
|
|
(PlotDevice::Pdf, true)
|
|
);
|
|
// A plotter we cannot drive plots to PDF here but keeps its name.
|
|
assert_eq!(
|
|
PlotDevice::from_stored_name("DWF6 ePlot.pc3"),
|
|
(PlotDevice::Pdf, true)
|
|
);
|
|
assert_eq!(
|
|
PlotDevice::from_stored_name("HP LaserJet 4"),
|
|
(PlotDevice::Printer("HP LaserJet 4".into()), true)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pdf_margins_depend_on_the_sheet_variant() {
|
|
let a4 = paper_catalog::resolve("ISO_A4_(210.00_x_297.00_MM)").unwrap();
|
|
let bleed = paper_catalog::resolve("ISO_full_bleed_A4_(210.00_x_297.00_MM)").unwrap();
|
|
let expand = paper_catalog::resolve("ISO_expand_A4_(210.00_x_297.00_MM)").unwrap();
|
|
assert_eq!(PlotDevice::Pdf.margins_mm(&a4), Margins::uniform(5.0));
|
|
assert_eq!(PlotDevice::Pdf.margins_mm(&bleed), Margins::ZERO);
|
|
assert_eq!(PlotDevice::Pdf.margins_mm(&expand), Margins::uniform(2.0));
|
|
assert_eq!(PlotDevice::None.margins_mm(&a4), Margins::ZERO);
|
|
}
|
|
|
|
#[test]
|
|
fn ipp_media_parses_sizes_margins_borderless_and_default() {
|
|
let caps = parse_ipp_media(IPP_SAMPLE).expect("media parsed");
|
|
assert_eq!(
|
|
caps.media.len(),
|
|
3,
|
|
"borderless A4 folds into the bordered entry"
|
|
);
|
|
let a4 = &caps.media[0];
|
|
assert_eq!(a4.paper.canonical, "ISO_A4_(210.00_x_297.00_MM)");
|
|
assert_eq!(a4.margins, Margins::uniform(3.0));
|
|
assert!(a4.borderless);
|
|
let letter = &caps.media[1];
|
|
assert_eq!(letter.paper.canonical, "ANSI_A_(8.50_x_11.00_Inches)");
|
|
assert_eq!(letter.margins, Margins::uniform(6.35));
|
|
assert!(!letter.borderless);
|
|
let custom = &caps.media[2];
|
|
assert_eq!(
|
|
custom.paper.canonical,
|
|
"UserDefinedMetric_(215.00_x_345.00_MM)"
|
|
);
|
|
assert_eq!(
|
|
caps.default_paper.as_ref().map(|p| p.canonical.as_ref()),
|
|
Some("ISO_A4_(210.00_x_297.00_MM)")
|
|
);
|
|
// Margin lookup by sheet size, either orientation, tolerant of the
|
|
// catalogue's inch → mm rounding.
|
|
let ansi_a = paper_catalog::resolve("Letter_(8.50_x_11.00_Inches)").unwrap();
|
|
assert_eq!(caps.margins_for(&ansi_a), Some(Margins::uniform(6.35)));
|
|
let a3 = paper_catalog::resolve("ISO_A3_(297.00_x_420.00_MM)").unwrap();
|
|
assert_eq!(caps.margins_for(&a3), None);
|
|
}
|
|
|
|
#[test]
|
|
fn ipp_output_without_media_is_rejected() {
|
|
assert!(parse_ipp_media("printer-state (enum) = 3\n").is_none());
|
|
assert!(parse_ipp_media(
|
|
"media-col-database (1setOf collection) = {media-size={x-dimension=0 y-dimension=0}}\n"
|
|
)
|
|
.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn pwg_names_carry_their_size() {
|
|
assert_eq!(pwg_size_mm("iso_a4_210x297mm"), Some((210.0, 297.0)));
|
|
let (w, h) = pwg_size_mm("na_letter_8.5x11in").unwrap();
|
|
assert!((w - 215.9).abs() < 1e-9 && (h - 279.4).abs() < 1e-9);
|
|
assert_eq!(
|
|
pwg_size_mm("custom_215x345mm_215x345mm"),
|
|
Some((215.0, 345.0))
|
|
);
|
|
assert_eq!(pwg_size_mm("iso_a4"), None);
|
|
}
|
|
}
|