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
@@ -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 = الطابعات: لم يسرد النظام أي طابعة
|
||||
|
||||
@@ -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 = Принтери: системата не изброява нито един
|
||||
|
||||
@@ -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í
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = Εκτυπωτές: το σύστημα δεν αναφέρει κανέναν
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = प्रिंटर: सिस्टम ने कोई सूचीबद्ध नहीं किया
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = プリンター: システムに一覧なし
|
||||
|
||||
@@ -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 = 프린터: 시스템에 나열된 항목 없음
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = Принтеры: система не перечисляет ни одного
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = 打印机:系统未列出任何打印机
|
||||
|
||||
@@ -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 = 印表機:系統未列出任何印表機
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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>())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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")),
|
||||
|
||||
Reference in new issue
Block a user