diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 037daccd4..6fd1fc8ef 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -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 = الطابعات: لم يسرد النظام أي طابعة diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index e269ab5e7..5bf8a5770 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -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 = Принтери: системата не изброява нито един diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index f49a9711f..5f46c0018 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -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í diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 62b58bd1d..f64e0bb14 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -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 diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 514207bcf..f5b744838 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -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 = Εκτυπωτές: το σύστημα δεν αναφέρει κανέναν diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index bd9a617a1..d97f8f13d 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -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 diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index ca34d86dd..6f815698e 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -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 diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index b0d4c6761..61760c3b3 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -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 diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 1fbc08786..1effc6621 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -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 diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 8b11a80e1..af4194c6f 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -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 = प्रिंटर: सिस्टम ने कोई सूचीबद्ध नहीं किया diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 8284c90cd..387d32f0b 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -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 diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 1a773718a..030517924 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -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 diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 2404fb4da..5a08dac77 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -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 = プリンター: システムに一覧なし diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 08a74c53d..b4efd61a2 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -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 = 프린터: 시스템에 나열된 항목 없음 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 5ad2b91cb..4a894196b 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -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 diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index b752a1351..da627a17a 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -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 diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index bb582dff9..0d2bd4238 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -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 diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index a31bd840d..c96e1f559 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -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 = Принтеры: система не перечисляет ни одного diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 542d4ed4f..5f86f09c8 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -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 diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index c6e54824b..7c824c61b 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -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 = 打印机:系统未列出任何打印机 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 06ca26992..4b5184374 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -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 = 印表機:系統未列出任何印表機 diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index 6639f38a3..61880b344 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -878,6 +878,15 @@ impl OpenCADStudio { self.command_line.push_output(&line); } } + // PRINTERS — 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)); } diff --git a/src/io/mod.rs b/src/io/mod.rs index fdc687529..7f1a6c062 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -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; diff --git a/src/io/plot_device.rs b/src/io/plot_device.rs index 37a9db3ec..08aaaf4e0 100644 --- a/src/io/plot_device.rs +++ b/src/io/plot_device.rs @@ -190,28 +190,36 @@ impl PrinterCapabilities { } } -fn capability_cache() -> &'static Mutex>> { - static CACHE: OnceLock>>> = 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>>; + +fn capability_cache() -> &'static Mutex { + static CACHE: OnceLock> = 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> { - 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> { - 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 { 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 { + 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 { None } @@ -276,6 +290,7 @@ fn query_printer_capabilities(_printer: &str) -> Option { /// 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 { let database = attribute_value(text, "media-col-database")?; let mut media: Vec = Vec::new(); @@ -337,6 +352,7 @@ fn parse_ipp_media(text: &str) -> Option { } /// 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 { 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 { digits.parse::().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 diff --git a/src/io/print_to_printer.rs b/src/io/print_to_printer.rs index 48b7bc77b..258928f73 100644 --- a/src/io/print_to_printer.rs +++ b/src/io/print_to_printer.rs @@ -143,6 +143,47 @@ pub fn printer_report() -> Vec { lines } +/// What `printer` reports about its sheets, one line per sheet plus the +/// default and the margins — `PRINTERS `. 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 { + 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 { + vec![crate::t!("Printing is not available in the web version.").into_owned()] +} + #[cfg(target_arch = "wasm32")] pub fn printer_report() -> Vec { 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 { 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> { - 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) -> bool, +) -> Option> { + 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() 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, +) -> Option> { + 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| { + 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 { + let tenths = |mm: f64| -> Option { 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 { + 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 { 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 { + 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::(), + 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::()) + .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::()) diff --git a/src/io/windows_media.rs b/src/io/windows_media.rs new file mode 100644 index 000000000..d8544ef9f --- /dev/null +++ b/src/io/windows_media.rs @@ -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, + /// `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, + /// The default DEVMODE's `dmPaperSize` when `DM_PAPERSIZE` is set. + pub default_id: Option, + /// 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, +} + +/// 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 { + 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 { + 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 { + 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 = 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 { + 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 { + let mut units: Vec = 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); + } +} diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 482c11d3d..13e4d5155 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -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")),