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:
PanosandClaude Opus 5 committed 2026-09-18 18:42:03 +03:00
1 parent c36ae08fd0
commit b8472a00b4
27 files changed
+721 -40

No files matched your search

+4
View File
@@ -5111,6 +5111,10 @@ plot =
.system-default-printer = طابعة النظام الافتراضية
.what-to-plot = ما المراد طباعته
.x-mm = X (مم)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: لا تبلغ الطابعة عن أي أوراق؛ يُستخدم كتالوج الورق.
.printer-count-sheet-s-reported = __ocs_fmt_0__: تم الإبلاغ عن __ocs_fmt_1__ ورقة/أوراق
.default-sheet-sheet = الورقة الافتراضية: __ocs_fmt_0__
.default-sheet-not-reported = الورقة الافتراضية: غير مُبلّغ عنها
.default-printer-name = الطابعة الافتراضية: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = الطابعة الافتراضية: لم يبلّغ النظام عن أي طابعة
.printers-none-listed-by-the-system = الطابعات: لم يسرد النظام أي طابعة
+4
View File
@@ -5099,6 +5099,10 @@ plot =
.system-default-printer = Системен принтер по подразбиране
.what-to-plot = Какво да се плотира
.x-mm = X (мм)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: принтерът не съобщава листове; използва се каталогът с хартия.
.printer-count-sheet-s-reported = __ocs_fmt_0__: съобщени __ocs_fmt_1__ листа
.default-sheet-sheet = Лист по подразбиране: __ocs_fmt_0__
.default-sheet-not-reported = Лист по подразбиране: не е съобщен
.default-printer-name = Принтер по подразбиране: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Принтер по подразбиране: системата не съобщава за такъв
.printers-none-listed-by-the-system = Принтери: системата не изброява нито един
+4
View File
@@ -5096,6 +5096,10 @@ plot =
.system-default-printer = Výchozí tiskárna systému
.what-to-plot = Co fabulovat
.x-mm = X (mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: tiskárna nehlásí žádné listy; použije se katalog papíru.
.printer-count-sheet-s-reported = __ocs_fmt_0__: nahlášeno listů: __ocs_fmt_1__
.default-sheet-sheet = Výchozí list: __ocs_fmt_0__
.default-sheet-not-reported = Výchozí list: nenahlášen
.default-printer-name = Výchozí tiskárna: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Výchozí tiskárna: systém žádnou nehlásí
.printers-none-listed-by-the-system = Tiskárny: systém žádné neuvádí
+4
View File
@@ -5097,6 +5097,10 @@ plot =
.system-default-printer = Systemstandarddrucker
.what-to-plot = Was zu plotten
.x-mm = X (mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: Der Drucker meldet keine Blätter; der Papierkatalog wird verwendet.
.printer-count-sheet-s-reported = __ocs_fmt_0__: __ocs_fmt_1__ Blatt/Blätter gemeldet
.default-sheet-sheet = Standardblatt: __ocs_fmt_0__
.default-sheet-not-reported = Standardblatt: nicht gemeldet
.default-printer-name = Standarddrucker: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Standarddrucker: vom System keiner gemeldet
.printers-none-listed-by-the-system = Drucker: vom System keiner aufgeführt
+4
View File
@@ -5099,6 +5099,10 @@ plot =
.system-default-printer = Προεπιλεγμένος εκτυπωτής συστήματος
.what-to-plot = Τι θα εκτυπωθεί
.x-mm = X (mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: ο εκτυπωτής δεν αναφέρει φύλλα· χρησιμοποιείται ο κατάλογος χαρτιών.
.printer-count-sheet-s-reported = __ocs_fmt_0__: αναφέρθηκαν __ocs_fmt_1__ φύλλα
.default-sheet-sheet = Προεπιλεγμένο φύλλο: __ocs_fmt_0__
.default-sheet-not-reported = Προεπιλεγμένο φύλλο: δεν αναφέρθηκε
.default-printer-name = Προεπιλεγμένος εκτυπωτής: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Προεπιλεγμένος εκτυπωτής: το σύστημα δεν αναφέρει κανέναν
.printers-none-listed-by-the-system = Εκτυπωτές: το σύστημα δεν αναφέρει κανέναν
+4
View File
@@ -5100,6 +5100,10 @@ plot =
.system-default-printer = System default printer
.what-to-plot = What to plot
.x-mm = X (mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: the printer reports no sheets; the paper catalogue is used.
.printer-count-sheet-s-reported = __ocs_fmt_0__: __ocs_fmt_1__ sheet(s) reported
.default-sheet-sheet = Default sheet: __ocs_fmt_0__
.default-sheet-not-reported = Default sheet: not reported
.default-printer-name = Default printer: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Default printer: none reported by the system
.printers-none-listed-by-the-system = Printers: none listed by the system
+4
View File
@@ -5099,6 +5099,10 @@ plot =
.system-default-printer = Impresora predeterminada del sistema
.what-to-plot = Qué trazar
.x-mm = X (mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: la impresora no informa hojas; se usa el catálogo de papel.
.printer-count-sheet-s-reported = __ocs_fmt_0__: __ocs_fmt_1__ hoja(s) informada(s)
.default-sheet-sheet = Hoja predeterminada: __ocs_fmt_0__
.default-sheet-not-reported = Hoja predeterminada: no informada
.default-printer-name = Impresora predeterminada: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Impresora predeterminada: el sistema no informa ninguna
.printers-none-listed-by-the-system = Impresoras: el sistema no lista ninguna
+4
View File
@@ -5098,6 +5098,10 @@ plot =
.system-default-printer = Järjestelmän oletustulostin
.what-to-plot = Mitä piirtää
.x-mm = X (mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: tulostin ei ilmoita arkkeja; käytetään paperiluetteloa.
.printer-count-sheet-s-reported = __ocs_fmt_0__: __ocs_fmt_1__ arkkia ilmoitettu
.default-sheet-sheet = Oletusarkki: __ocs_fmt_0__
.default-sheet-not-reported = Oletusarkki: ei ilmoitettu
.default-printer-name = Oletustulostin: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Oletustulostin: järjestelmä ei ilmoita yhtään
.printers-none-listed-by-the-system = Tulostimet: järjestelmä ei luettele yhtään
+4
View File
@@ -5099,6 +5099,10 @@ plot =
.system-default-printer = Imprimante système par défaut
.what-to-plot = Quoi faire?
.x-mm = X (mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__ : l'imprimante ne signale aucune feuille ; le catalogue de papiers est utilisé.
.printer-count-sheet-s-reported = __ocs_fmt_0__ : __ocs_fmt_1__ feuille(s) signalée(s)
.default-sheet-sheet = Feuille par défaut : __ocs_fmt_0__
.default-sheet-not-reported = Feuille par défaut : non signalée
.default-printer-name = Imprimante par défaut : __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Imprimante par défaut : aucune signalée par le système
.printers-none-listed-by-the-system = Imprimantes : aucune répertoriée par le système
+4
View File
@@ -5087,6 +5087,10 @@ plot =
.system-default-printer = सिस्टम डिफ़ॉल्ट प्रिंटर
.what-to-plot = क्या प्लॉट करना है
.x-mm = एक्स (मिमी)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: प्रिंटर कोई शीट नहीं बताता; पेपर कैटलॉग का उपयोग किया जाता है।
.printer-count-sheet-s-reported = __ocs_fmt_0__: __ocs_fmt_1__ शीट बताई गईं
.default-sheet-sheet = डिफ़ॉल्ट शीट: __ocs_fmt_0__
.default-sheet-not-reported = डिफ़ॉल्ट शीट: नहीं बताई गई
.default-printer-name = डिफ़ॉल्ट प्रिंटर: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = डिफ़ॉल्ट प्रिंटर: सिस्टम ने कोई नहीं बताया
.printers-none-listed-by-the-system = प्रिंटर: सिस्टम ने कोई सूचीबद्ध नहीं किया
+4
View File
@@ -5098,6 +5098,10 @@ plot =
.system-default-printer = Rendszer alapértelmezett nyomtató
.what-to-plot = Mit kell kitalálni
.x-mm = X (mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: a nyomtató nem jelez lapokat; a papírkatalógus lesz használva.
.printer-count-sheet-s-reported = __ocs_fmt_0__: __ocs_fmt_1__ lap jelezve
.default-sheet-sheet = Alapértelmezett lap: __ocs_fmt_0__
.default-sheet-not-reported = Alapértelmezett lap: nincs jelezve
.default-printer-name = Alapértelmezett nyomtató: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Alapértelmezett nyomtató: a rendszer nem jelez egyet sem
.printers-none-listed-by-the-system = Nyomtatók: a rendszer nem sorol fel egyet sem
+4
View File
@@ -5098,6 +5098,10 @@ plot =
.system-default-printer = Stampante predefinita del sistema
.what-to-plot = Cosa tracciare
.x-mm = X (mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: la stampante non segnala fogli; viene usato il catalogo carta.
.printer-count-sheet-s-reported = __ocs_fmt_0__: __ocs_fmt_1__ foglio/i segnalato/i
.default-sheet-sheet = Foglio predefinito: __ocs_fmt_0__
.default-sheet-not-reported = Foglio predefinito: non segnalato
.default-printer-name = Stampante predefinita: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Stampante predefinita: nessuna segnalata dal sistema
.printers-none-listed-by-the-system = Stampanti: nessuna elencata dal sistema
+4
View File
@@ -5087,6 +5087,10 @@ plot =
.system-default-printer = システムの既定プリンター
.what-to-plot = 印刷対象
.x-mm = X(mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: プリンターは用紙を報告しません。用紙カタログを使用します。
.printer-count-sheet-s-reported = __ocs_fmt_0__: __ocs_fmt_1__ 種類の用紙が報告されました
.default-sheet-sheet = 既定の用紙: __ocs_fmt_0__
.default-sheet-not-reported = 既定の用紙: 報告なし
.default-printer-name = 既定のプリンター: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = 既定のプリンター: システムから報告なし
.printers-none-listed-by-the-system = プリンター: システムに一覧なし
+4
View File
@@ -5098,6 +5098,10 @@ plot =
.system-default-printer = 시스템 기본 프린터
.what-to-plot = 무엇을 플롯할 것인가
.x-mm = X (mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: 프린터가 용지를 보고하지 않습니다. 용지 카탈로그를 사용합니다.
.printer-count-sheet-s-reported = __ocs_fmt_0__: 용지 __ocs_fmt_1__개 보고됨
.default-sheet-sheet = 기본 용지: __ocs_fmt_0__
.default-sheet-not-reported = 기본 용지: 보고되지 않음
.default-printer-name = 기본 프린터: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = 기본 프린터: 시스템이 보고하지 않음
.printers-none-listed-by-the-system = 프린터: 시스템에 나열된 항목 없음
+4
View File
@@ -5097,6 +5097,10 @@ plot =
.system-default-printer = Standaardprinter van het systeem
.what-to-plot = Wat te plannen
.x-mm = X (mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: de printer meldt geen vellen; de papiercatalogus wordt gebruikt.
.printer-count-sheet-s-reported = __ocs_fmt_0__: __ocs_fmt_1__ vel(len) gemeld
.default-sheet-sheet = Standaardvel: __ocs_fmt_0__
.default-sheet-not-reported = Standaardvel: niet gemeld
.default-printer-name = Standaardprinter: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Standaardprinter: geen gemeld door het systeem
.printers-none-listed-by-the-system = Printers: geen vermeld door het systeem
+4
View File
@@ -5098,6 +5098,10 @@ plot =
.system-default-printer = Domyślna drukarka systemowa
.what-to-plot = Co nakreślić
.x-mm = X (mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: drukarka nie zgłasza arkuszy; używany jest katalog papieru.
.printer-count-sheet-s-reported = __ocs_fmt_0__: zgłoszono __ocs_fmt_1__ arkuszy
.default-sheet-sheet = Domyślny arkusz: __ocs_fmt_0__
.default-sheet-not-reported = Domyślny arkusz: nie zgłoszono
.default-printer-name = Drukarka domyślna: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Drukarka domyślna: system nie zgłasza żadnej
.printers-none-listed-by-the-system = Drukarki: system nie wymienia żadnej
+4
View File
@@ -5099,6 +5099,10 @@ plot =
.system-default-printer = Impressora padrão do sistema
.what-to-plot = O que plotar
.x-mm = X (mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: a impressora não informa folhas; o catálogo de papel é usado.
.printer-count-sheet-s-reported = __ocs_fmt_0__: __ocs_fmt_1__ folha(s) informada(s)
.default-sheet-sheet = Folha padrão: __ocs_fmt_0__
.default-sheet-not-reported = Folha padrão: não informada
.default-printer-name = Impressora padrão: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Impressora padrão: nenhuma informada pelo sistema
.printers-none-listed-by-the-system = Impressoras: nenhuma listada pelo sistema
+4
View File
@@ -5100,6 +5100,10 @@ plot =
.system-default-printer = Системный принтер по умолчанию
.what-to-plot = Что спланировать
.x-mm = X (мм)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: принтер не сообщает форматов; используется каталог бумаги.
.printer-count-sheet-s-reported = __ocs_fmt_0__: сообщено форматов: __ocs_fmt_1__
.default-sheet-sheet = Формат по умолчанию: __ocs_fmt_0__
.default-sheet-not-reported = Формат по умолчанию: не сообщён
.default-printer-name = Принтер по умолчанию: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Принтер по умолчанию: система не сообщает ни об одном
.printers-none-listed-by-the-system = Принтеры: система не перечисляет ни одного
+4
View File
@@ -5039,6 +5039,10 @@ plot =
.system-default-printer = Varsayılan sistem yazıcısı
.what-to-plot = Yazdırılacak Alan
.x-mm = X (mm)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__: yazıcı sayfa bildirmiyor; kağıt kataloğu kullanılıyor.
.printer-count-sheet-s-reported = __ocs_fmt_0__: __ocs_fmt_1__ sayfa bildirildi
.default-sheet-sheet = Varsayılan sayfa: __ocs_fmt_0__
.default-sheet-not-reported = Varsayılan sayfa: bildirilmedi
.default-printer-name = Varsayılan yazıcı: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Varsayılan yazıcı: sistem hiçbirini bildirmiyor
.printers-none-listed-by-the-system = Yazıcılar: sistem hiçbirini listelemiyor
+4
View File
@@ -5082,6 +5082,10 @@ plot =
.system-default-printer = 系统默认打印机
.what-to-plot = 打印内容
.x-mm = X (毫米)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__:打印机未报告任何纸张;使用纸张目录。
.printer-count-sheet-s-reported = __ocs_fmt_0__:报告了 __ocs_fmt_1__ 种纸张
.default-sheet-sheet = 默认纸张:__ocs_fmt_0__
.default-sheet-not-reported = 默认纸张:未报告
.default-printer-name = 默认打印机:__ocs_fmt_0__
.default-printer-none-reported-by-the-system = 默认打印机:系统未报告
.printers-none-listed-by-the-system = 打印机:系统未列出任何打印机
+4
View File
@@ -5094,6 +5094,10 @@ plot =
.system-default-printer = 系統預設印表機
.what-to-plot = 列印內容
.x-mm = X (毫米)
.printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used = __ocs_fmt_0__:印表機未回報任何紙張;使用紙張目錄。
.printer-count-sheet-s-reported = __ocs_fmt_0__:回報了 __ocs_fmt_1__ 種紙張
.default-sheet-sheet = 預設紙張:__ocs_fmt_0__
.default-sheet-not-reported = 預設紙張:未回報
.default-printer-name = 預設印表機:__ocs_fmt_0__
.default-printer-none-reported-by-the-system = 預設印表機:系統未回報
.printers-none-listed-by-the-system = 印表機:系統未列出任何印表機
+9
View File
@@ -878,6 +878,15 @@ impl OpenCADStudio {
self.command_line.push_output(&line);
}
}
// PRINTERS <name> — what that printer reports about its sheets
// and printable area (asks the driver; may take a moment for an
// offline network queue).
cmd if cmd.starts_with("PRINTERS ") => {
let name = cmd["PRINTERS ".len()..].trim();
for line in crate::io::print_to_printer::printer_media_report(name) {
self.command_line.push_output(&line);
}
}
"EXPORT" | "EXPORTPDF" => {
return Some(Task::done(Message::PlotExport));
}
+1
View File
@@ -24,6 +24,7 @@ pub mod patterns;
pub mod update_check;
pub mod paper_catalog;
pub mod plot_device;
pub mod windows_media;
pub mod thumbnail;
#[cfg(target_arch = "wasm32")]
mod web_worker;
+35 -14
View File
@@ -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
+276 -26
View File
@@ -143,6 +143,47 @@ pub fn printer_report() -> Vec<String> {
lines
}
/// What `printer` reports about its sheets, one line per sheet plus the
/// default and the margins — `PRINTERS <name>`. Asks the driver (and caches
/// the answer like the plot dialog does).
#[cfg(not(target_arch = "wasm32"))]
pub fn printer_media_report(printer: &str) -> Vec<String> {
let Some(caps) = crate::io::plot_device::printer_capabilities(printer) else {
return vec![crate::tf!(
"{printer}: the printer reports no sheets; the paper catalogue is used."
)
.into_owned()];
};
let mut lines = Vec::new();
let count = caps.media.len();
lines.push(crate::tf!("{printer}: {count} sheet(s) reported").into_owned());
for media in &caps.media {
let m = media.margins;
let borderless = if media.borderless { " (borderless available)" } else { "" };
lines.push(format!(
" {} — margins L {:.1} B {:.1} R {:.1} T {:.1} mm{borderless}",
media.paper.display(),
m.left,
m.bottom,
m.right,
m.top
));
}
match &caps.default_paper {
Some(paper) => {
let sheet = paper.display();
lines.push(crate::tf!("Default sheet: {sheet}").into_owned());
}
None => lines.push(crate::t!("Default sheet: not reported").into_owned()),
}
lines
}
#[cfg(target_arch = "wasm32")]
pub fn printer_media_report(_printer: &str) -> Vec<String> {
vec![crate::t!("Printing is not available in the web version.").into_owned()]
}
#[cfg(target_arch = "wasm32")]
pub fn printer_report() -> Vec<String> {
vec![crate::t!("Printing is not available in the web version.").into_owned()]
@@ -564,17 +605,17 @@ fn rotate_rgba90(pixels: &[u8], width: u32, height: u32) -> Vec<u8> {
rotated
}
/// Fetch the printer driver's DEVMODE and flip its orientation to match the
/// plot's aspect (`landscape` = image wider than tall). The merged DEVMODE is
/// returned as raw bytes (drivers append private data after the struct).
/// `None` when the driver could not be queried — CreateDC then falls back to
/// the driver default.
/// Open `device`, fetch the driver's default DEVMODE (public struct plus the
/// driver's private data behind it) and let `edit` change it while the
/// printer is still open — a change has to be reconciled by the driver
/// through `DocumentPropertiesW` with that handle. `edit` returns `false`
/// to reject the buffer. `None` when the driver could not be queried.
#[cfg(target_os = "windows")]
fn oriented_printer_devmode(device_wide: &[u16], landscape: bool) -> Option<Vec<u8>> {
use windows_sys::Win32::Graphics::Gdi::{
DEVMODEW, DM_IN_BUFFER, DM_ORIENTATION, DM_OUT_BUFFER, DMORIENT_LANDSCAPE,
DMORIENT_PORTRAIT,
};
fn with_printer_devmode(
device_wide: &[u16],
edit: impl FnOnce(windows_sys::Win32::Graphics::Printing::PRINTER_HANDLE, &mut Vec<u8>) -> bool,
) -> Option<Vec<u8>> {
use windows_sys::Win32::Graphics::Gdi::{DEVMODEW, DM_OUT_BUFFER};
use windows_sys::Win32::Graphics::Printing::{
ClosePrinter, DocumentPropertiesW, OpenPrinterW, PRINTER_HANDLE,
};
@@ -614,32 +655,238 @@ fn oriented_printer_devmode(device_wide: &[u16], landscape: bool) -> Option<Vec<
if (*dm).dmSize == 0 {
(*dm).dmSize = std::mem::size_of::<DEVMODEW>() as u16;
}
edit(handle, &mut buf).then_some(buf)
})();
ClosePrinter(handle);
result
}
}
/// The sheet a print job asks the driver for: one of the driver's own
/// sheets by id, or a user-defined size in tenths of a millimetre.
#[cfg(target_os = "windows")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DevmodeSheet {
Id(u16),
UserMm { width_tenths: i16, length_tenths: i16 },
}
/// Fetch the printer driver's DEVMODE, flip its orientation to match the
/// plot's aspect (`landscape` = image wider than tall) and, when a sheet is
/// given, select it. The merged DEVMODE is returned as raw bytes (drivers
/// append private data after the struct). A driver that rejects the sheet
/// is asked again for the orientation alone; `None` when the driver could
/// not be queried — CreateDC then falls back to the driver default.
#[cfg(target_os = "windows")]
fn oriented_printer_devmode(
device_wide: &[u16],
landscape: bool,
sheet: Option<DevmodeSheet>,
) -> Option<Vec<u8>> {
use windows_sys::Win32::Graphics::Gdi::{
DEVMODEW, DM_IN_BUFFER, DM_ORIENTATION, DM_OUT_BUFFER, DM_PAPERLENGTH, DM_PAPERSIZE,
DM_PAPERWIDTH, DMORIENT_LANDSCAPE, DMORIENT_PORTRAIT,
};
use windows_sys::Win32::Graphics::Printing::DocumentPropertiesW;
let attempt = |sheet: Option<DevmodeSheet>| {
with_printer_devmode(device_wide, |handle, buf| unsafe {
let dm = buf.as_mut_ptr() as *mut DEVMODEW;
(*dm).dmFields |= DM_ORIENTATION;
(*dm).Anonymous1.Anonymous1.dmOrientation = if landscape {
DMORIENT_LANDSCAPE as i16
} else {
DMORIENT_PORTRAIT as i16
};
// Let the driver reconcile fields that depend on orientation
// (paper dimensions, imageable area) before we print with it.
if DocumentPropertiesW(
match sheet {
Some(DevmodeSheet::Id(id)) => {
(*dm).dmFields |= DM_PAPERSIZE;
(*dm).dmFields &= !(DM_PAPERWIDTH | DM_PAPERLENGTH);
(*dm).Anonymous1.Anonymous1.dmPaperSize = id as i16;
}
Some(DevmodeSheet::UserMm { width_tenths, length_tenths }) => {
(*dm).dmFields |= DM_PAPERSIZE | DM_PAPERWIDTH | DM_PAPERLENGTH;
(*dm).Anonymous1.Anonymous1.dmPaperSize =
crate::io::windows_media::DMPAPER_USER as i16;
(*dm).Anonymous1.Anonymous1.dmPaperWidth = width_tenths;
(*dm).Anonymous1.Anonymous1.dmPaperLength = length_tenths;
}
None => {}
}
// Let the driver reconcile fields that depend on orientation and
// sheet (paper dimensions, imageable area) before we print with it.
DocumentPropertiesW(
std::ptr::null_mut(),
handle,
device_wide.as_ptr(),
dm,
dm,
DM_IN_BUFFER | DM_OUT_BUFFER,
) <= 0
{
return None;
}
Some(buf)
})();
ClosePrinter(handle);
result
) > 0
})
};
match sheet {
Some(sheet) => attempt(Some(sheet)).or_else(|| attempt(None)),
None => attempt(None),
}
}
/// The DEVMODE sheet for a plot of `width_mm` × `height_mm`: the driver's
/// own id when it lists that size, else a user-defined size (which the
/// driver may still refuse — the caller then prints on the default sheet).
#[cfg(target_os = "windows")]
fn devmode_sheet_for(printer: &str, width_mm: f64, height_mm: f64) -> Option<DevmodeSheet> {
let tenths = |mm: f64| -> Option<i16> { i16::try_from((mm * 10.0).round() as i64).ok() };
match windows_printer_media(printer) {
Some(raw) => {
let listed = crate::io::windows_media::paper_id_for_sheet(&raw, width_mm, height_mm);
if let Some(id) = listed {
return Some(DevmodeSheet::Id(id));
}
// A driver that lists sheets but not this one gets it as a
// user-defined size, portrait (the orientation flag turns it).
let (w, h) = if width_mm <= height_mm {
(width_mm, height_mm)
} else {
(height_mm, width_mm)
};
Some(DevmodeSheet::UserMm {
width_tenths: tenths(w)?,
length_tenths: tenths(h)?,
})
}
None => None,
}
}
/// What the driver of `printer` says about its sheets: the three parallel
/// `DeviceCapabilities` tables, the default DEVMODE's sheet and the margins
/// of a portrait printer DC. `None` when the driver lists no sheet (a fax or
/// virtual queue) or cannot be asked at all — the dialog keeps the catalogue.
/// Slow for an offline network queue (the spooler may fetch the driver), so
/// it is only ever called from a background task.
#[cfg(target_os = "windows")]
pub(crate) fn windows_printer_media(
printer: &str,
) -> Option<crate::io::windows_media::RawPrinterMedia> {
use crate::io::windows_media::{DeviceCapsSample, RawPrinterMedia, PAPER_NAME_CHARS};
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Foundation::POINT;
use windows_sys::Win32::Graphics::Gdi::{
CreateDCW, DeleteDC, GetDeviceCaps, DEVMODEW, DM_PAPERLENGTH, DM_PAPERSIZE,
DM_PAPERWIDTH, HORZRES, LOGPIXELSX, LOGPIXELSY, PHYSICALHEIGHT, PHYSICALOFFSETX,
PHYSICALOFFSETY, PHYSICALWIDTH, VERTRES,
};
use windows_sys::Win32::Storage::Xps::{
DeviceCapabilitiesW, DC_PAPERNAMES, DC_PAPERS, DC_PAPERSIZE,
};
let wide =
|s: &str| -> Vec<u16> { std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect() };
let device_wide = wide(printer.trim());
// SAFETY: the device name is NUL-terminated; a null output buffer asks
// for the count only.
let count = |capability: u16| -> Option<usize> {
let n = unsafe {
DeviceCapabilitiesW(
device_wide.as_ptr(),
std::ptr::null(),
capability,
std::ptr::null_mut(),
std::ptr::null(),
)
};
(n > 0).then_some(n as usize)
};
// The three tables are parallel; a driver that disagrees on their
// lengths is read up to the shortest.
let count = count(DC_PAPERS)?
.min(count(DC_PAPERSIZE)?)
.min(count(DC_PAPERNAMES)?);
let mut ids = vec![0u16; count];
let mut sizes = vec![POINT { x: 0, y: 0 }; count];
let mut names = vec![0u16; count * PAPER_NAME_CHARS];
// SAFETY: each buffer holds `count` entries of the type the capability
// writes (WORD, POINT, 64 WCHARs); the calls fill at most that many.
let filled = unsafe {
DeviceCapabilitiesW(
device_wide.as_ptr(),
std::ptr::null(),
DC_PAPERS,
ids.as_mut_ptr(),
std::ptr::null(),
) > 0
&& DeviceCapabilitiesW(
device_wide.as_ptr(),
std::ptr::null(),
DC_PAPERSIZE,
sizes.as_mut_ptr().cast::<u16>(),
std::ptr::null(),
) > 0
&& DeviceCapabilitiesW(
device_wide.as_ptr(),
std::ptr::null(),
DC_PAPERNAMES,
names.as_mut_ptr(),
std::ptr::null(),
) > 0
};
if !filled {
return None;
}
// The driver's default sheet, from its default DEVMODE — which also
// makes the margin DC start portrait and on that sheet.
let devmode = with_printer_devmode(&device_wide, |_, _| true);
let (default_id, default_tenths_mm) = devmode
.as_ref()
.map(|buf| {
// SAFETY: the buffer holds at least a public DEVMODEW.
let dm = unsafe { &*(buf.as_ptr() as *const DEVMODEW) };
let fields = dm.dmFields;
let paper = unsafe { dm.Anonymous1.Anonymous1 };
let id = (fields & DM_PAPERSIZE != 0).then_some(paper.dmPaperSize as u16);
let size = (fields & DM_PAPERWIDTH != 0 && fields & DM_PAPERLENGTH != 0)
.then_some((i32::from(paper.dmPaperWidth), i32::from(paper.dmPaperLength)));
(id, size)
})
.unwrap_or((None, None));
let winspool = wide("WINSPOOL");
let init = devmode
.as_ref()
.map(|b| b.as_ptr().cast::<DEVMODEW>())
.unwrap_or(std::ptr::null());
// SAFETY: the strings are NUL-terminated and `devmode` outlives the DC.
let hdc = unsafe { CreateDCW(winspool.as_ptr(), device_wide.as_ptr(), std::ptr::null(), init) };
let margins = if hdc.is_null() {
None
} else {
// SAFETY: hdc is a valid printer DC; each index is documented.
let caps = |index: u32| unsafe { GetDeviceCaps(hdc, index as i32) };
let sample = DeviceCapsSample {
physical_width: caps(PHYSICALWIDTH),
physical_height: caps(PHYSICALHEIGHT),
offset_x: caps(PHYSICALOFFSETX),
offset_y: caps(PHYSICALOFFSETY),
horz_res: caps(HORZRES),
vert_res: caps(VERTRES),
dpi_x: caps(LOGPIXELSX),
dpi_y: caps(LOGPIXELSY),
};
unsafe { DeleteDC(hdc) };
crate::io::windows_media::device_margins_mm(&sample)
};
Some(RawPrinterMedia {
ids,
sizes_tenths_mm: sizes.into_iter().map(|p| (p.x, p.y)).collect(),
names_utf16: names,
default_id,
default_tenths_mm,
margins,
})
}
#[cfg(target_os = "windows")]
fn gdi_raster_print(
path: &std::path::Path,
@@ -678,11 +925,14 @@ fn gdi_raster_print(
let winspool = wide("WINSPOOL");
let device_wide = wide(&device);
// The dialog's Landscape/Portrait choice rides in through the DEVMODE:
// the sheet in the raster decides, and the driver's default must not
// override it (a landscape plot into a portrait page used to shrink and
// misorient the output).
let dm_buf = oriented_printer_devmode(&device_wide, page.width > page.height);
// The dialog's sheet and Landscape/Portrait choice ride in through the
// DEVMODE: the raster's size decides both, and the driver's default must
// not override them (a landscape A3 plot into the driver's portrait A4
// used to shrink and misorient the output).
let sheet_w_mm = f64::from(page.width) / f64::from(GDI_PRINT_DPI) * 25.4;
let sheet_h_mm = f64::from(page.height) / f64::from(GDI_PRINT_DPI) * 25.4;
let sheet = devmode_sheet_for(&device, sheet_w_mm, sheet_h_mm);
let dm_buf = oriented_printer_devmode(&device_wide, page.width > page.height, sheet);
let init = dm_buf
.as_ref()
.map(|b| b.as_ptr().cast::<DEVMODEW>())
+312
View File
@@ -0,0 +1,312 @@
//! What a Windows printer driver says about its sheets, turned into the plot
//! dialog's [`PrinterCapabilities`] — the counterpart of the CUPS media
//! parser in `plot_device.rs`.
//!
//! The driver answers three `DeviceCapabilities` questions with parallel
//! arrays (a paper id, a size in tenths of a millimetre and a 64-character
//! name per sheet) and, through its default DEVMODE, which sheet it starts
//! with; the unprintable margins come from a printer DC. Everything here
//! works on those raw arrays so it compiles and is tested on every platform;
//! the calls that fill them live in `print_to_printer.rs`.
use crate::io::paper_catalog::{self, Margins, PaperSize, PaperUnits};
use crate::io::plot_device::{PrinterCapabilities, PrinterMedia};
/// `DC_PAPERNAMES` returns names in fixed-width slots of this many UTF-16
/// units, NUL-padded (and not NUL-terminated when exactly full).
pub const PAPER_NAME_CHARS: usize = 64;
/// `dmPaperSize` of a user-defined sheet: its size is in `dmPaperWidth` /
/// `dmPaperLength` rather than in the driver's table.
pub const DMPAPER_USER: u16 = 256;
/// Sheets a driver would list but cannot mean: a "user defined" slot is
/// reported as 0 × 0, and some drivers pad with absurd extents.
const MAX_SHEET_MM: f64 = 10_000.0;
/// The driver's answers, as the Win32 calls hand them over.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct RawPrinterMedia {
/// `DC_PAPERS`: one `DMPAPER_*` id per sheet.
pub ids: Vec<u16>,
/// `DC_PAPERSIZE`: one `(width, height)` per sheet, tenths of a millimetre.
pub sizes_tenths_mm: Vec<(i32, i32)>,
/// `DC_PAPERNAMES`: `count × PAPER_NAME_CHARS` UTF-16 units, flat.
pub names_utf16: Vec<u16>,
/// The default DEVMODE's `dmPaperSize` when `DM_PAPERSIZE` is set.
pub default_id: Option<u16>,
/// The default DEVMODE's `dmPaperWidth` / `dmPaperLength` (tenths of a
/// millimetre) for a `DMPAPER_USER` default.
pub default_tenths_mm: Option<(i32, i32)>,
/// Unprintable margins measured on the printer DC, portrait, mm.
pub margins: Option<Margins>,
}
/// The `GetDeviceCaps` values a printable-area calculation needs, taken
/// from one printer DC in portrait.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DeviceCapsSample {
/// `PHYSICALWIDTH` / `PHYSICALHEIGHT`: the sheet, in device pixels.
pub physical_width: i32,
pub physical_height: i32,
/// `PHYSICALOFFSETX` / `PHYSICALOFFSETY`: the printable area's corner.
pub offset_x: i32,
pub offset_y: i32,
/// `HORZRES` / `VERTRES`: the printable area, in device pixels.
pub horz_res: i32,
pub vert_res: i32,
/// `LOGPIXELSX` / `LOGPIXELSY`: device pixels per inch.
pub dpi_x: i32,
pub dpi_y: i32,
}
/// The sheet names out of a `DC_PAPERNAMES` buffer: each slot is trimmed at
/// its first NUL (a name that fills its slot has none) and decoded lossily.
pub fn paper_names(names_utf16: &[u16], count: usize) -> Vec<String> {
names_utf16
.chunks(PAPER_NAME_CHARS)
.take(count)
.map(|slot| {
let end = slot.iter().position(|&unit| unit == 0).unwrap_or(slot.len());
String::from_utf16_lossy(&slot[..end]).trim().to_string()
})
.collect()
}
/// The unprintable margins a DC sample describes, in millimetres — `None`
/// when the driver reported no usable resolution or extents.
pub fn device_margins_mm(sample: &DeviceCapsSample) -> Option<Margins> {
if sample.dpi_x <= 0
|| sample.dpi_y <= 0
|| sample.physical_width <= 0
|| sample.physical_height <= 0
|| sample.horz_res <= 0
|| sample.vert_res <= 0
{
return None;
}
let mm_x = |pixels: i32| f64::from(pixels) / f64::from(sample.dpi_x) * 25.4;
let mm_y = |pixels: i32| f64::from(pixels) / f64::from(sample.dpi_y) * 25.4;
let margins = Margins {
left: mm_x(sample.offset_x),
top: mm_y(sample.offset_y),
right: mm_x(sample.physical_width - sample.offset_x - sample.horz_res),
bottom: mm_y(sample.physical_height - sample.offset_y - sample.vert_res),
};
let sane = |value: f64| value.is_finite() && value >= 0.0;
(sane(margins.left) && sane(margins.top) && sane(margins.right) && sane(margins.bottom))
.then_some(margins)
}
/// The driver's sheets as the plot dialog lists them: one entry per distinct
/// size (a rotated or borderless twin folds onto its sheet), catalogue
/// sheets under their own name, unknown sizes under the driver's name, the
/// device's margins on every entry, and the driver's default sheet when it
/// is one of them. `None` when the driver listed nothing usable, so the
/// dialog falls back to the catalogue.
pub fn printer_media(raw: &RawPrinterMedia) -> Option<PrinterCapabilities> {
let count = raw.ids.len().min(raw.sizes_tenths_mm.len());
let names = paper_names(&raw.names_utf16, count);
let margins = raw.margins.unwrap_or(Margins::ZERO);
let mut media: Vec<PrinterMedia> = Vec::new();
// Which listed sheet each id resolves to, so the default can be found by
// id even when its own entry folded onto an earlier twin.
let mut sheet_of_id: Vec<(u16, usize)> = Vec::new();
for index in 0..count {
let (w, h) = raw.sizes_tenths_mm[index];
let (w, h) = (f64::from(w) / 10.0, f64::from(h) / 10.0);
if !(w > 0.0 && h > 0.0 && w <= MAX_SHEET_MM && h <= MAX_SHEET_MM) {
continue;
}
let (pw, ph) = if w <= h { (w, h) } else { (h, w) };
let name = names.get(index).cloned().unwrap_or_default();
if let Some(position) = media.iter().position(|m| same_size(&m.paper, pw, ph)) {
if name.to_ascii_lowercase().contains("borderless") {
media[position].borderless = true;
}
sheet_of_id.push((raw.ids[index], position));
continue;
}
let paper = paper_catalog::match_dimensions_mm(pw, ph).cloned().unwrap_or_else(|| {
if name.is_empty() {
PaperSize::custom(pw, ph, PaperUnits::Millimeters)
} else {
PaperSize::custom_named(&name, pw, ph, PaperUnits::Millimeters)
}
});
sheet_of_id.push((raw.ids[index], media.len()));
media.push(PrinterMedia {
paper,
margins,
borderless: name.to_ascii_lowercase().contains("borderless"),
});
}
if media.is_empty() {
return None;
}
let default_paper = match raw.default_id {
Some(DMPAPER_USER) => raw.default_tenths_mm.and_then(|(w, h)| {
let (w, h) = (f64::from(w) / 10.0, f64::from(h) / 10.0);
let (pw, ph) = if w <= h { (w, h) } else { (h, w) };
media
.iter()
.find(|m| same_size(&m.paper, pw, ph))
.map(|m| m.paper.clone())
}),
Some(id) => sheet_of_id
.iter()
.find(|(candidate, _)| *candidate == id)
.map(|(_, position)| media[*position].paper.clone()),
None => None,
};
Some(PrinterCapabilities {
media,
default_paper,
})
}
/// The driver's paper id for a sheet of this size in either orientation —
/// what a print job's DEVMODE needs so the sheet is printed at its own size
/// rather than on the driver's default.
pub fn paper_id_for_sheet(raw: &RawPrinterMedia, width_mm: f64, height_mm: f64) -> Option<u16> {
let (pw, ph) = if width_mm <= height_mm {
(width_mm, height_mm)
} else {
(height_mm, width_mm)
};
raw.ids
.iter()
.zip(&raw.sizes_tenths_mm)
.find(|(_, (w, h))| {
let (w, h) = (f64::from(*w) / 10.0, f64::from(*h) / 10.0);
let (sw, sh) = if w <= h { (w, h) } else { (h, w) };
(sw - pw).abs() <= 0.5 && (sh - ph).abs() <= 0.5
})
.map(|(id, _)| *id)
}
fn same_size(paper: &PaperSize, portrait_w: f64, portrait_h: f64) -> bool {
let (w, h) = paper.portrait_mm();
(w - portrait_w).abs() <= 0.5 && (h - portrait_h).abs() <= 0.5
}
#[cfg(test)]
mod tests {
use super::*;
fn slot(name: &str) -> Vec<u16> {
let mut units: Vec<u16> = name.encode_utf16().collect();
units.resize(PAPER_NAME_CHARS, 0);
units
}
fn raw(entries: &[(u16, (i32, i32), &str)]) -> RawPrinterMedia {
RawPrinterMedia {
ids: entries.iter().map(|(id, _, _)| *id).collect(),
sizes_tenths_mm: entries.iter().map(|(_, size, _)| *size).collect(),
names_utf16: entries.iter().flat_map(|(_, _, name)| slot(name)).collect(),
default_id: None,
default_tenths_mm: None,
margins: Some(Margins {
left: 3.0,
bottom: 3.0,
right: 3.0,
top: 3.0,
}),
}
}
#[test]
fn names_are_trimmed_at_the_first_nul_and_survive_a_full_slot() {
let mut buffer = slot("A4");
buffer.extend(std::iter::repeat_n(b'x' as u16, PAPER_NAME_CHARS));
// A lone surrogate decodes lossily instead of failing.
let mut broken = slot("Letter");
broken[6] = 0xD800;
broken[7] = 0;
buffer.extend(broken);
let names = paper_names(&buffer, 3);
assert_eq!(names[0], "A4");
assert_eq!(names[1].len(), PAPER_NAME_CHARS);
assert!(names[2].starts_with("Letter"));
assert_eq!(paper_names(&buffer, 1).len(), 1, "count caps the slots read");
}
#[test]
fn twins_fold_onto_their_sheet_and_nonsense_sizes_are_dropped() {
let caps = printer_media(&raw(&[
(9, (2100, 2970), "A4"),
(77, (2970, 2100), "A4 Rotated"),
(1, (2159, 2794), "Letter"),
(256, (0, 0), "User Defined"),
(300, (2100, 2970), "A4 Borderless"),
(301, (1000, 1500), "Photo 10x15"),
]))
.expect("usable sheets");
assert_eq!(caps.media.len(), 3);
assert_eq!(caps.media[0].paper.canonical, "ISO_A4_(210.00_x_297.00_MM)");
assert!(caps.media[0].borderless, "the borderless twin marks the sheet");
assert_eq!(caps.media[1].paper.canonical, "ANSI_A_(8.50_x_11.00_Inches)");
assert_eq!(caps.media[2].paper.label, "Photo 10x15", "the driver's name is kept");
assert_eq!(caps.media[2].margins.left, 3.0, "the device margins go on every sheet");
assert!(caps.default_paper.is_none());
}
#[test]
fn the_default_sheet_is_found_by_id_or_by_user_size() {
let mut media = raw(&[
(9, (2100, 2970), "A4"),
(77, (2970, 2100), "A4 Rotated"),
(8, (2970, 4200), "A3"),
]);
media.default_id = Some(77);
let caps = printer_media(&media).unwrap();
assert_eq!(
caps.default_paper.as_ref().map(|p| p.canonical.as_ref()),
Some("ISO_A4_(210.00_x_297.00_MM)"),
"the rotated twin's id still names A4"
);
media.default_id = Some(DMPAPER_USER);
media.default_tenths_mm = Some((4200, 2970));
let caps = printer_media(&media).unwrap();
assert_eq!(
caps.default_paper.as_ref().map(|p| p.canonical.as_ref()),
Some("ISO_A3_(297.00_x_420.00_MM)")
);
media.default_id = Some(999);
assert!(printer_media(&media).unwrap().default_paper.is_none());
assert!(printer_media(&RawPrinterMedia::default()).is_none(), "nothing listed → catalogue");
}
#[test]
fn device_margins_come_from_the_dc_geometry() {
let sample = DeviceCapsSample {
physical_width: 4960,
physical_height: 7016,
offset_x: 100,
offset_y: 100,
horz_res: 4760,
vert_res: 6716,
dpi_x: 600,
dpi_y: 600,
};
let margins = device_margins_mm(&sample).unwrap();
let close = |a: f64, b: f64| (a - b).abs() < 1e-6;
assert!(close(margins.left, 100.0 / 600.0 * 25.4));
assert!(close(margins.right, 100.0 / 600.0 * 25.4));
assert!(close(margins.top, 100.0 / 600.0 * 25.4));
assert!(close(margins.bottom, 200.0 / 600.0 * 25.4));
assert!(device_margins_mm(&DeviceCapsSample::default()).is_none());
let inverted = DeviceCapsSample { horz_res: 9000, ..sample };
assert!(device_margins_mm(&inverted).is_none(), "printable area wider than the sheet");
}
#[test]
fn the_paper_id_matches_either_orientation_within_half_a_millimetre() {
let media = raw(&[(9, (2100, 2970), "A4"), (8, (2970, 4200), "A3")]);
assert_eq!(paper_id_for_sheet(&media, 297.0, 210.0), Some(9));
assert_eq!(paper_id_for_sheet(&media, 210.3, 297.4), Some(9));
assert_eq!(paper_id_for_sheet(&media, 420.0, 297.0), Some(8));
assert_eq!(paper_id_for_sheet(&media, 216.0, 279.0), None);
}
}
+4
View File
@@ -3144,6 +3144,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"World" => Some(("common", "world")),
"Write Block" => Some(("insert", "write-block")),
"X (mm)" => Some(("plot", "x-mm")),
"{printer}: the printer reports no sheets; the paper catalogue is used." => Some(("plot", "printer-the-printer-reports-no-sheets-the-paper-catalogue-is-used")),
"{printer}: {count} sheet(s) reported" => Some(("plot", "printer-count-sheet-s-reported")),
"Default sheet: {sheet}" => Some(("plot", "default-sheet-sheet")),
"Default sheet: not reported" => Some(("plot", "default-sheet-not-reported")),
"Default printer: {name}" => Some(("plot", "default-printer-name")),
"Default printer: none reported by the system" => Some(("plot", "default-printer-none-reported-by-the-system")),
"Printers: none listed by the system" => Some(("plot", "printers-none-listed-by-the-system")),