diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 1fdd169af..037daccd4 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -5111,6 +5111,15 @@ plot = .system-default-printer = طابعة النظام الافتراضية .what-to-plot = ما المراد طباعته .x-mm = X (مم) + .default-printer-name = الطابعة الافتراضية: __ocs_fmt_0__ + .default-printer-none-reported-by-the-system = الطابعة الافتراضية: لم يبلّغ النظام عن أي طابعة + .printers-none-listed-by-the-system = الطابعات: لم يسرد النظام أي طابعة + .printers-count = الطابعات (__ocs_fmt_0__): + .could-not-list-printers-error = تعذّر سرد الطابعات: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = تمر المخططات عبر تطبيق PDF: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = تذهب المخططات مباشرة إلى الطابعة (لا يسجل أي تطبيق PDF أمر طباعة). + .via-the-pdf-application = عبر تطبيق PDF + .direct-print = طباعة مباشرة .y-mm = Y (مم) manage = diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 7b0c44e00..e269ab5e7 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -5099,6 +5099,15 @@ plot = .system-default-printer = Системен принтер по подразбиране .what-to-plot = Какво да се плотира .x-mm = X (мм) + .default-printer-name = Принтер по подразбиране: __ocs_fmt_0__ + .default-printer-none-reported-by-the-system = Принтер по подразбиране: системата не съобщава за такъв + .printers-none-listed-by-the-system = Принтери: системата не изброява нито един + .printers-count = Принтери (__ocs_fmt_0__): + .could-not-list-printers-error = Принтерите не могат да бъдат изброени: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = Печатът минава през PDF приложението: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Печатът отива директно към принтера (никое PDF приложение не регистрира команда за печат). + .via-the-pdf-application = чрез PDF приложението + .direct-print = директен печат .y-mm = Y (мм) manage = diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index dcd36aaa6..f49a9711f 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -5096,6 +5096,15 @@ plot = .system-default-printer = Výchozí tiskárna systému .what-to-plot = Co fabulovat .x-mm = X (mm) + .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í + .printers-count = Tiskárny (__ocs_fmt_0__): + .could-not-list-printers-error = Nelze vypsat tiskárny: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = Tisk prochází aplikací PDF: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Tisk jde přímo na tiskárnu (žádná aplikace PDF neregistruje příkaz tisku). + .via-the-pdf-application = přes aplikaci PDF + .direct-print = přímý tisk .y-mm = Y (mm) manage = diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index e4264e697..62b58bd1d 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -5097,6 +5097,15 @@ plot = .system-default-printer = Systemstandarddrucker .what-to-plot = Was zu plotten .x-mm = X (mm) + .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 + .printers-count = Drucker (__ocs_fmt_0__): + .could-not-list-printers-error = Drucker konnten nicht aufgelistet werden: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = Plots laufen über die PDF-Anwendung: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Plots gehen direkt an den Drucker (keine PDF-Anwendung registriert einen Druckbefehl). + .via-the-pdf-application = über die PDF-Anwendung + .direct-print = Direktdruck .y-mm = Y (mm) manage = diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 1dfe08503..514207bcf 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -5099,6 +5099,15 @@ plot = .system-default-printer = Προεπιλεγμένος εκτυπωτής συστήματος .what-to-plot = Τι θα εκτυπωθεί .x-mm = X (mm) + .default-printer-name = Προεπιλεγμένος εκτυπωτής: __ocs_fmt_0__ + .default-printer-none-reported-by-the-system = Προεπιλεγμένος εκτυπωτής: το σύστημα δεν αναφέρει κανέναν + .printers-none-listed-by-the-system = Εκτυπωτές: το σύστημα δεν αναφέρει κανέναν + .printers-count = Εκτυπωτές (__ocs_fmt_0__): + .could-not-list-printers-error = Δεν ήταν δυνατή η λίστα εκτυπωτών: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = Οι εκτυπώσεις περνούν από την εφαρμογή PDF: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Οι εκτυπώσεις πάνε απευθείας στον εκτυπωτή (καμία εφαρμογή PDF δεν δηλώνει εντολή εκτύπωσης). + .via-the-pdf-application = μέσω της εφαρμογής PDF + .direct-print = απευθείας εκτύπωση .y-mm = Y (mm) manage = diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index c959f870b..bd9a617a1 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -5100,6 +5100,15 @@ plot = .system-default-printer = System default printer .what-to-plot = What to plot .x-mm = X (mm) + .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 + .printers-count = Printers (__ocs_fmt_0__): + .could-not-list-printers-error = Could not list printers: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = Plots go through the PDF application: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Plots go straight to the printer (no PDF application registers a print verb). + .via-the-pdf-application = via the PDF application + .direct-print = direct print .y-mm = Y (mm) manage = diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index e9f255d51..ca34d86dd 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -5099,6 +5099,15 @@ plot = .system-default-printer = Impresora predeterminada del sistema .what-to-plot = Qué trazar .x-mm = X (mm) + .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 + .printers-count = Impresoras (__ocs_fmt_0__): + .could-not-list-printers-error = No se pudieron listar las impresoras: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = Los trazados pasan por la aplicación PDF: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Los trazados van directamente a la impresora (ninguna aplicación PDF registra un comando de impresión). + .via-the-pdf-application = a través de la aplicación PDF + .direct-print = impresión directa .y-mm = Y (mm) manage = diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index b0bc8c856..b0d4c6761 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -5098,6 +5098,15 @@ plot = .system-default-printer = Järjestelmän oletustulostin .what-to-plot = Mitä piirtää .x-mm = X (mm) + .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 + .printers-count = Tulostimet (__ocs_fmt_0__): + .could-not-list-printers-error = Tulostimia ei voitu luetella: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = Tulosteet kulkevat PDF-sovelluksen kautta: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Tulosteet menevät suoraan tulostimelle (mikään PDF-sovellus ei rekisteröi tulostuskomentoa). + .via-the-pdf-application = PDF-sovelluksen kautta + .direct-print = suoratulostus .y-mm = Y (mm) manage = diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 5094cc756..1fbc08786 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -5099,6 +5099,15 @@ plot = .system-default-printer = Imprimante système par défaut .what-to-plot = Quoi faire? .x-mm = X (mm) + .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 + .printers-count = Imprimantes (__ocs_fmt_0__) : + .could-not-list-printers-error = Impossible de lister les imprimantes : __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = Les tracés passent par l'application PDF : __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Les tracés vont directement à l'imprimante (aucune application PDF n'enregistre de commande d'impression). + .via-the-pdf-application = via l'application PDF + .direct-print = impression directe .y-mm = Y (mm) manage = diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 242447240..8b11a80e1 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -5087,6 +5087,15 @@ plot = .system-default-printer = सिस्टम डिफ़ॉल्ट प्रिंटर .what-to-plot = क्या प्लॉट करना है .x-mm = एक्स (मिमी) + .default-printer-name = डिफ़ॉल्ट प्रिंटर: __ocs_fmt_0__ + .default-printer-none-reported-by-the-system = डिफ़ॉल्ट प्रिंटर: सिस्टम ने कोई नहीं बताया + .printers-none-listed-by-the-system = प्रिंटर: सिस्टम ने कोई सूचीबद्ध नहीं किया + .printers-count = प्रिंटर (__ocs_fmt_0__): + .could-not-list-printers-error = प्रिंटर सूचीबद्ध नहीं किए जा सके: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = प्लॉट PDF एप्लिकेशन के माध्यम से जाते हैं: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = प्लॉट सीधे प्रिंटर पर जाते हैं (कोई PDF एप्लिकेशन प्रिंट कमांड पंजीकृत नहीं करता)। + .via-the-pdf-application = PDF एप्लिकेशन के माध्यम से + .direct-print = सीधी प्रिंटिंग .y-mm = Y (मिमी) manage = diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 635e974cd..8284c90cd 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -5098,6 +5098,15 @@ plot = .system-default-printer = Rendszer alapértelmezett nyomtató .what-to-plot = Mit kell kitalálni .x-mm = X (mm) + .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 + .printers-count = Nyomtatók (__ocs_fmt_0__): + .could-not-list-printers-error = A nyomtatók nem listázhatók: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = A nyomtatás a PDF-alkalmazáson keresztül megy: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = A nyomtatás közvetlenül a nyomtatóra megy (egy PDF-alkalmazás sem regisztrál nyomtatási parancsot). + .via-the-pdf-application = a PDF-alkalmazáson keresztül + .direct-print = közvetlen nyomtatás .y-mm = Y (mm) manage = diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 1da38e16a..1a773718a 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -5098,6 +5098,15 @@ plot = .system-default-printer = Stampante predefinita del sistema .what-to-plot = Cosa tracciare .x-mm = X (mm) + .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 + .printers-count = Stampanti (__ocs_fmt_0__): + .could-not-list-printers-error = Impossibile elencare le stampanti: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = Le stampe passano dall'applicazione PDF: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Le stampe vanno direttamente alla stampante (nessuna applicazione PDF registra un comando di stampa). + .via-the-pdf-application = tramite l'applicazione PDF + .direct-print = stampa diretta .y-mm = Y (mm) manage = diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 22d894f31..2404fb4da 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -5087,6 +5087,15 @@ plot = .system-default-printer = システムの既定プリンター .what-to-plot = 印刷対象 .x-mm = X(mm) + .default-printer-name = 既定のプリンター: __ocs_fmt_0__ + .default-printer-none-reported-by-the-system = 既定のプリンター: システムから報告なし + .printers-none-listed-by-the-system = プリンター: システムに一覧なし + .printers-count = プリンター(__ocs_fmt_0__): + .could-not-list-printers-error = プリンターを一覧できませんでした: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = 印刷は PDF アプリケーション経由です: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = 印刷はプリンターへ直接送られます(印刷コマンドを登録した PDF アプリケーションがありません)。 + .via-the-pdf-application = PDF アプリケーション経由 + .direct-print = 直接印刷 .y-mm = Y(mm) manage = diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 2d04ba139..08a74c53d 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -5098,6 +5098,15 @@ plot = .system-default-printer = 시스템 기본 프린터 .what-to-plot = 무엇을 플롯할 것인가 .x-mm = X (mm) + .default-printer-name = 기본 프린터: __ocs_fmt_0__ + .default-printer-none-reported-by-the-system = 기본 프린터: 시스템이 보고하지 않음 + .printers-none-listed-by-the-system = 프린터: 시스템에 나열된 항목 없음 + .printers-count = 프린터 (__ocs_fmt_0__): + .could-not-list-printers-error = 프린터 목록을 가져올 수 없습니다: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = 출력은 PDF 응용 프로그램을 거칩니다: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = 출력이 프린터로 직접 전송됩니다 (인쇄 명령을 등록한 PDF 응용 프로그램이 없음). + .via-the-pdf-application = PDF 응용 프로그램 경유 + .direct-print = 직접 인쇄 .y-mm = Y (mm) manage = diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 555d2247b..5ad2b91cb 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -5097,6 +5097,15 @@ plot = .system-default-printer = Standaardprinter van het systeem .what-to-plot = Wat te plannen .x-mm = X (mm) + .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 + .printers-count = Printers (__ocs_fmt_0__): + .could-not-list-printers-error = Kon printers niet weergeven: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = Plots gaan via de PDF-toepassing: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Plots gaan rechtstreeks naar de printer (geen PDF-toepassing registreert een afdrukopdracht). + .via-the-pdf-application = via de PDF-toepassing + .direct-print = rechtstreeks afdrukken .y-mm = Y (mm) manage = diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 3f7438b28..b752a1351 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -5098,6 +5098,15 @@ plot = .system-default-printer = Domyślna drukarka systemowa .what-to-plot = Co nakreślić .x-mm = X (mm) + .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 + .printers-count = Drukarki (__ocs_fmt_0__): + .could-not-list-printers-error = Nie można wyświetlić listy drukarek: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = Wydruki przechodzą przez aplikację PDF: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Wydruki trafiają bezpośrednio do drukarki (żadna aplikacja PDF nie rejestruje polecenia drukowania). + .via-the-pdf-application = przez aplikację PDF + .direct-print = wydruk bezpośredni .y-mm = Y (mm) manage = diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index a2a30fb6e..bb582dff9 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -5099,6 +5099,15 @@ plot = .system-default-printer = Impressora padrão do sistema .what-to-plot = O que plotar .x-mm = X (mm) + .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 + .printers-count = Impressoras (__ocs_fmt_0__): + .could-not-list-printers-error = Não foi possível listar as impressoras: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = As plotagens passam pelo aplicativo de PDF: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = As plotagens vão direto para a impressora (nenhum aplicativo de PDF registra um comando de impressão). + .via-the-pdf-application = pelo aplicativo de PDF + .direct-print = impressão direta .y-mm = Y (mm) manage = diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 3ee0e3ad4..a31bd840d 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -5100,6 +5100,15 @@ plot = .system-default-printer = Системный принтер по умолчанию .what-to-plot = Что спланировать .x-mm = X (мм) + .default-printer-name = Принтер по умолчанию: __ocs_fmt_0__ + .default-printer-none-reported-by-the-system = Принтер по умолчанию: система не сообщает ни об одном + .printers-none-listed-by-the-system = Принтеры: система не перечисляет ни одного + .printers-count = Принтеры (__ocs_fmt_0__): + .could-not-list-printers-error = Не удалось получить список принтеров: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = Печать идёт через приложение PDF: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Печать идёт напрямую на принтер (ни одно приложение PDF не регистрирует команду печати). + .via-the-pdf-application = через приложение PDF + .direct-print = прямая печать .y-mm = Y (мм) manage = diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index cea64bba1..542d4ed4f 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -5039,6 +5039,15 @@ plot = .system-default-printer = Varsayılan sistem yazıcısı .what-to-plot = Yazdırılacak Alan .x-mm = X (mm) + .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 + .printers-count = Yazıcılar (__ocs_fmt_0__): + .could-not-list-printers-error = Yazıcılar listelenemedi: __ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = Çizimler PDF uygulaması üzerinden gider: __ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Çizimler doğrudan yazıcıya gider (hiçbir PDF uygulaması yazdırma komutu kaydetmiyor). + .via-the-pdf-application = PDF uygulaması üzerinden + .direct-print = doğrudan yazdırma .y-mm = Y (mm) manage = diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 70dc709d3..c6e54824b 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -5082,6 +5082,15 @@ plot = .system-default-printer = 系统默认打印机 .what-to-plot = 打印内容 .x-mm = X (毫米) + .default-printer-name = 默认打印机:__ocs_fmt_0__ + .default-printer-none-reported-by-the-system = 默认打印机:系统未报告 + .printers-none-listed-by-the-system = 打印机:系统未列出任何打印机 + .printers-count = 打印机(__ocs_fmt_0__): + .could-not-list-printers-error = 无法列出打印机:__ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = 打印通过 PDF 应用程序进行:__ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = 打印直接发送到打印机(没有 PDF 应用程序注册打印命令)。 + .via-the-pdf-application = 通过 PDF 应用程序 + .direct-print = 直接打印 .y-mm = Y (毫米) manage = diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 041b2911f..06ca26992 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -5094,6 +5094,15 @@ plot = .system-default-printer = 系統預設印表機 .what-to-plot = 列印內容 .x-mm = X (毫米) + .default-printer-name = 預設印表機:__ocs_fmt_0__ + .default-printer-none-reported-by-the-system = 預設印表機:系統未回報 + .printers-none-listed-by-the-system = 印表機:系統未列出任何印表機 + .printers-count = 印表機(__ocs_fmt_0__): + .could-not-list-printers-error = 無法列出印表機:__ocs_fmt_0__ + .plots-go-through-the-pdf-application-command = 列印透過 PDF 應用程式進行:__ocs_fmt_0__ + .plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = 列印直接送到印表機(沒有 PDF 應用程式註冊列印命令)。 + .via-the-pdf-application = 透過 PDF 應用程式 + .direct-print = 直接列印 .y-mm = Y (毫米) manage = diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index a7604403e..6639f38a3 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -869,6 +869,15 @@ impl OpenCADStudio { "PRINTALL" => { return Some(Task::done(Message::PrintAllOpen)); } + // PRINTERS — what the system reports about printing, on the + // command line: the default printer, every printer it lists (or + // why it could not), and how a plot will reach them. The report + // a user pastes into a bug report instead of a screenshot. + "PRINTERS" => { + for line in crate::io::print_to_printer::printer_report() { + self.command_line.push_output(&line); + } + } "EXPORT" | "EXPORTPDF" => { return Some(Task::done(Message::PlotExport)); } @@ -2094,3 +2103,23 @@ mod tests { } } } + +#[cfg(test)] +mod printers_command_tests { + use crate::app::OpenCADStudio; + + #[test] + fn printers_reports_the_default_and_the_list() { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let before = app.command_line.history.len(); + let _ = app.run_command_line("PRINTERS"); + let lines: Vec = app.command_line.history[before..] + .iter() + .map(|line| line.text.clone()) + .collect(); + // One line for the default printer, at least one for the list (a + // count and names, "none", or the system's error). + assert!(lines.len() >= 2, "{lines:?}"); + } +} diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 7d3d3c611..84b443455 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -712,6 +712,7 @@ inventory::submit!(crate::command::CommandRegistration { "PERF", "PERSP", "PLOT", + "PRINTERS", "PLOTSTYLE", "PLOTSTYLEEDITOR", "PLOTSTYLEPANEL", diff --git a/src/app/update/file.rs b/src/app/update/file.rs index b2044180c..43958e6d6 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -3960,7 +3960,16 @@ impl OpenCADStudio { .collect(); // Keep session-only choices while loading drawing fields from the layout. let d = &mut self.plot_dialog; - d.printers = crate::io::print_to_printer::list_printers(); + match crate::io::print_to_printer::list_printers() { + Ok(printers) => { + d.printers = printers; + d.printers_error = None; + } + Err(error) => { + d.printers = Vec::new(); + d.printers_error = Some(error); + } + } d.default_printer = crate::io::plot_device::default_printer_name(); d.plot_styles = crate::io::plot_style::available_ctb_names(); d.scales = scales; @@ -4011,6 +4020,10 @@ impl OpenCADStudio { self.plot_dialog.scale_lw = false; } self.refresh_page_setups(); + if let Some(error) = self.plot_dialog.printers_error.clone() { + self.command_line + .push_warning(crate::tf!("Could not list printers: {error}").as_ref()); + } self.plot_prev = Some(previous); let cur = self.tabs[self.active_tab].scene.current_layout.clone(); let layout_entry = format!("*{cur}*"); @@ -4179,8 +4192,14 @@ impl OpenCADStudio { /// print job honours. Without a named printer, or where the options /// cannot be listed, the platform's printer settings open instead. fn on_printer_properties(&mut self) -> Task { - let Some(printer) = self.plot_dialog.printer.clone().filter(|_| !self.plot_dialog.to_file) - else { + // "Default" is a real printer: its preferences sheet is the one to + // open, not the system's printer list. + let chosen = self + .plot_dialog + .printer + .clone() + .or_else(|| self.plot_dialog.default_printer.clone()); + let Some(printer) = chosen.filter(|_| !self.plot_dialog.to_file) else { self.open_printer_settings_fallback(); return Task::none(); }; diff --git a/src/io/plot_device.rs b/src/io/plot_device.rs index 39b205602..37a9db3ec 100644 --- a/src/io/plot_device.rs +++ b/src/io/plot_device.rs @@ -232,8 +232,14 @@ pub fn default_printer_name() -> Option { (!name.is_empty()).then(|| name.to_string()) } -/// Windows default printer discovery lands with native GDI printing. -#[cfg(any(target_arch = "wasm32", target_os = "windows"))] +/// The spooler's default printer, by name — the same lookup every direct +/// print job resolves "Default" through, so the dialog names what will print. +#[cfg(target_os = "windows")] +pub fn default_printer_name() -> Option { + crate::io::print_to_printer::windows_default_printer().ok() +} + +#[cfg(target_arch = "wasm32")] pub fn default_printer_name() -> Option { None } diff --git a/src/io/print_to_printer.rs b/src/io/print_to_printer.rs index d2772149d..48b7bc77b 100644 --- a/src/io/print_to_printer.rs +++ b/src/io/print_to_printer.rs @@ -80,30 +80,230 @@ pub fn print_existing_pdf(_path: &std::path::Path, _opts: &PrintOptions) -> Resu /// Enumerate installed printers. Linux/macOS query CUPS via `lpstat -e`; /// Windows queries the spooler's cached local and connected printer list. +/// An error is the system's own explanation (a stopped spooler, a failing +/// RPC); a desktop without CUPS at all is not an error but an empty list. #[cfg(not(target_arch = "wasm32"))] -pub fn list_printers() -> Vec { +pub fn list_printers() -> Result, String> { #[cfg(not(target_os = "windows"))] { - std::process::Command::new("lpstat") - .arg("-e") - .output() - .ok() - .filter(|o| o.status.success()) - .map(|o| { - String::from_utf8_lossy(&o.stdout) - .lines() - .map(|l| l.trim().to_string()) - .filter(|l| !l.is_empty()) - .collect() - }) - .unwrap_or_default() + let output = match std::process::Command::new("lpstat").arg("-e").output() { + Ok(output) => output, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(error) => return Err(error.to_string()), + }; + // `lpstat -e` exits non-zero with "No destinations added" when CUPS + // runs but knows no queue — an empty list, not a failure. + Ok(String::from_utf8_lossy(&output.stdout) + .lines() + .map(|l| l.trim().to_string()) + .filter(|l| !l.is_empty()) + .collect()) } #[cfg(target_os = "windows")] { - windows_printers().unwrap_or_else(|error| { - eprintln!("Could not enumerate printers: {error}"); - Vec::new() - }) + windows_printers().map_err(|error| error.to_string()) + } +} + +/// The printing situation as the system reports it, one line per fact: +/// the default printer, the printers listed (or the reason none were), and +/// on Windows the route a plot takes. Shown by the `PRINTERS` command. +#[cfg(not(target_arch = "wasm32"))] +pub fn printer_report() -> Vec { + let mut lines = Vec::new(); + match crate::io::plot_device::default_printer_name() { + Some(name) => lines.push(crate::tf!("Default printer: {name}").into_owned()), + None => lines.push(crate::t!("Default printer: none reported by the system").into_owned()), + } + match list_printers() { + Ok(printers) if printers.is_empty() => { + lines.push(crate::t!("Printers: none listed by the system").into_owned()); + } + Ok(printers) => { + let count = printers.len(); + lines.push(crate::tf!("Printers ({count}):").into_owned()); + lines.extend(printers.into_iter().map(|name| format!(" {name}"))); + } + Err(error) => lines.push(crate::tf!("Could not list printers: {error}").into_owned()), + } + #[cfg(target_os = "windows")] + { + match pdf_printto_command() { + Some(command) => lines.push( + crate::tf!("Plots go through the PDF application: {command}").into_owned(), + ), + None => lines.push( + crate::t!( + "Plots go straight to the printer (no PDF application registers a print verb)." + ) + .into_owned(), + ), + } + } + lines +} + +#[cfg(target_arch = "wasm32")] +pub fn printer_report() -> Vec { + vec![crate::t!("Printing is not available in the web version.").into_owned()] +} + +/// The Windows default printer, as the spooler names it. +#[cfg(target_os = "windows")] +pub(crate) fn windows_default_printer() -> std::io::Result { + use windows_sys::Win32::Foundation::{GetLastError, ERROR_FILE_NOT_FOUND}; + use windows_sys::Win32::Graphics::Printing::GetDefaultPrinterW; + + let mut needed = 0u32; + // SAFETY: the first call only asks for the size; the second writes into + // a buffer of exactly that size. + unsafe { GetDefaultPrinterW(std::ptr::null_mut(), &mut needed) }; + if needed == 0 { + // Windows answers ERROR_FILE_NOT_FOUND when no printer is the default. + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No default printer is configured on Windows.", + )); + } + let mut buf = vec![0u16; needed as usize]; + if unsafe { GetDefaultPrinterW(buf.as_mut_ptr(), &mut needed) } == 0 { + let code = unsafe { GetLastError() }; + if code == ERROR_FILE_NOT_FOUND { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No default printer is configured on Windows.", + )); + } + return Err(std::io::Error::from_raw_os_error(code as i32)); + } + let end = buf.iter().position(|&u| u == 0).unwrap_or(0); + Ok(String::from_utf16_lossy(&buf[..end])) +} + +/// The command Windows runs for the `printto` verb on `.pdf` files — the +/// registered PDF application's "print to a named printer" entry — or +/// `None` when no application registered one (Edge and store readers only +/// register `open`), in which case a plot goes straight to the spooler. +#[cfg(target_os = "windows")] +pub(crate) fn pdf_printto_command() -> Option { + use std::os::windows::ffi::OsStrExt; + use windows_sys::Win32::UI::Shell::{AssocQueryStringW, ASSOCF_NOTRUNCATE, ASSOCSTR_COMMAND}; + + let wide = + |s: &str| -> Vec { std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect() }; + let extension = wide(".pdf"); + let verb = wide("printto"); + let mut len = 0u32; + // SAFETY: strings are NUL-terminated; a null buffer asks for the size, + // which the call reports with S_FALSE rather than S_OK. + let sized = unsafe { + AssocQueryStringW( + ASSOCF_NOTRUNCATE, + ASSOCSTR_COMMAND, + extension.as_ptr(), + verb.as_ptr(), + std::ptr::null_mut(), + &mut len, + ) + }; + if sized < 0 || len == 0 { + return None; + } + let mut buf = vec![0u16; len as usize]; + let filled = unsafe { + AssocQueryStringW( + ASSOCF_NOTRUNCATE, + ASSOCSTR_COMMAND, + extension.as_ptr(), + verb.as_ptr(), + buf.as_mut_ptr(), + &mut len, + ) + }; + if filled < 0 { + return None; + } + let end = buf.iter().position(|&u| u == 0).unwrap_or(buf.len()); + let command = String::from_utf16_lossy(&buf[..end]); + (!command.trim().is_empty()).then_some(command) +} + +/// Where a Windows plot goes and how. The route is decided before any +/// system call so the decision itself can be tested anywhere. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] +pub(crate) enum PrintRoute { + /// Hand the PDF to the registered PDF application's `printto` verb, + /// naming the printer explicitly. + PdfApplication { printer: String }, + /// Rasterise in-process and draw to the printer through the spooler. + Direct { printer: String }, +} + +impl PrintRoute { + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + pub(crate) fn printer(&self) -> &str { + match self { + PrintRoute::PdfApplication { printer } | PrintRoute::Direct { printer } => printer, + } + } +} + +/// Resolve the dialog's choice to a route. "Default" (no name) becomes the +/// system default printer *by name*, so the job can never drift to whatever +/// the PDF application considers its default; the PDF application is used +/// only when it registered a `printto` verb. +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] +pub(crate) fn plan_print_route( + requested: Option<&str>, + default_printer: Option<&str>, + printto_registered: bool, +) -> Result { + let requested = requested.map(str::trim).filter(|name| !name.is_empty()); + let printer = match requested.or(default_printer.map(str::trim).filter(|n| !n.is_empty())) { + Some(name) => name.to_string(), + None => return Err("No default printer is configured on Windows.".into()), + }; + Ok(if printto_registered { + PrintRoute::PdfApplication { printer } + } else { + PrintRoute::Direct { printer } + }) +} + +#[cfg(test)] +mod print_route_tests { + use super::{plan_print_route, PrintRoute}; + + #[test] + fn a_named_printer_is_kept_and_blank_means_the_default() { + assert_eq!( + plan_print_route(Some("Office Laser"), Some("Home Inkjet"), false), + Ok(PrintRoute::Direct { printer: "Office Laser".into() }) + ); + assert_eq!( + plan_print_route(Some(" "), Some("Home Inkjet"), false), + Ok(PrintRoute::Direct { printer: "Home Inkjet".into() }) + ); + assert_eq!( + plan_print_route(None, Some(" Home Inkjet "), true), + Ok(PrintRoute::PdfApplication { printer: "Home Inkjet".into() }) + ); + } + + #[test] + fn no_printer_at_all_is_an_error_not_a_guess() { + assert!(plan_print_route(None, None, true).is_err()); + assert!(plan_print_route(Some(""), Some(""), false).is_err()); + } + + #[test] + fn the_pdf_application_is_used_only_with_a_printto_verb() { + let direct = plan_print_route(Some("X"), None, false).unwrap(); + let via_app = plan_print_route(Some("X"), None, true).unwrap(); + assert!(matches!(direct, PrintRoute::Direct { .. })); + assert!(matches!(via_app, PrintRoute::PdfApplication { .. })); + assert_eq!(direct.printer(), "X"); } } @@ -408,7 +608,12 @@ fn oriented_printer_devmode(device_wide: &[u16], landscape: bool) -> Option() as u16; + // The driver's own dmSize locates its private data + // (dmDriverExtra) behind the public struct; only a driver that + // left it zero gets the public size. + if (*dm).dmSize == 0 { + (*dm).dmSize = std::mem::size_of::() as u16; + } (*dm).dmFields |= DM_ORIENTATION; (*dm).Anonymous1.Anonymous1.dmOrientation = if landscape { DMORIENT_LANDSCAPE as i16 @@ -438,7 +643,7 @@ fn oriented_printer_devmode(device_wide: &[u16], landscape: bool) -> Option, + printer: &str, copies: u32, output_file: Option<&std::path::Path>, ) -> Result<(), String> { @@ -450,34 +655,21 @@ fn gdi_raster_print( }; // windows-sys 0.61 groups the spooler-document calls (StartDoc/StartPage/ // EndPage/EndDoc/DOCINFOW) under Storage::Xps, not Graphics::Gdi. - use windows_sys::Win32::Graphics::Printing::GetDefaultPrinterW; use windows_sys::Win32::Storage::Xps::{EndDoc, EndPage, StartDocW, StartPage, DOCINFOW}; let wide = |s: &str| -> Vec { std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect() }; - // Resolve the target device: the named printer or the Windows default. - let device = match printer.map(str::trim).filter(|p| !p.is_empty()) { - Some(name) => name.to_string(), - None => { - let mut needed = 0u32; - // SAFETY: first call queries the size, second writes into a - // buffer of exactly that size. - unsafe { GetDefaultPrinterW(std::ptr::null_mut(), &mut needed) }; - if needed == 0 { - return Err("No default printer is configured on Windows.".into()); - } - let mut buf = vec![0u16; needed as usize]; - if unsafe { GetDefaultPrinterW(buf.as_mut_ptr(), &mut needed) } == 0 { - return Err(format!( - "Could not query the default printer (code {}).", - unsafe { windows_sys::Win32::Foundation::GetLastError() } - )); - } - String::from_utf16_lossy(&buf[..buf.iter().position(|&u| u == 0).unwrap_or(0)]) - } - }; + // The device is resolved by the caller (`plan_print_route`): a name the + // dialog chose or the system default looked up by name. + let device = printer.trim(); + if device.is_empty() { + return Err("No printer was chosen for direct printing.".into()); + } + let device = device.to_string(); - let page = crate::scene::model::pdf_raster::rasterize_page_at_dpi( + // A plot is printed once; the on-screen memo of PDF pages must not keep + // a 300 DPI raster of every job for the rest of the session. + let page = crate::scene::model::pdf_raster::rasterize_page_at_dpi_uncached( &path.to_string_lossy(), "1", GDI_PRINT_DPI, @@ -657,6 +849,50 @@ pub fn open_in_viewer(path: &std::path::Path) -> Result<(), String> { .map_err(|e| format!("Could not open preview: {e}")) } +/// Ask the registered PDF application to print `path` on `printer` through +/// its `printto` verb, once per copy. The printer name is quoted: some +/// `printto` command templates do not quote their `%2`. +#[cfg(target_os = "windows")] +fn shell_print_to(path: &std::path::Path, printer: &str, copies: u32) -> Result<(), String> { + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + use windows_sys::Win32::Foundation::{GetLastError, ERROR_NO_ASSOCIATION}; + use windows_sys::Win32::UI::Shell::{ + ShellExecuteExW, SHELLEXECUTEINFOW, SEE_MASK_FLAG_NO_UI, SEE_MASK_NOASYNC, SE_ERR_NOASSOC, + }; + use windows_sys::Win32::UI::WindowsAndMessaging::SW_HIDE; + + let wide = |s: &str| -> Vec { OsStr::new(s).encode_wide().chain(Some(0)).collect() }; + let path_wide: Vec = path.as_os_str().encode_wide().chain(Some(0)).collect(); + let verb = wide("printto"); + let params = wide(&format!("\"{printer}\"")); + for _ in 0..copies.max(1) { + let mut info = SHELLEXECUTEINFOW { + cbSize: std::mem::size_of::() as u32, + fMask: SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC, + lpVerb: verb.as_ptr(), + lpFile: path_wide.as_ptr(), + lpParameters: params.as_ptr(), + nShow: SW_HIDE, + ..Default::default() + }; + // SAFETY: every pointer in `info` outlives the call. + if unsafe { ShellExecuteExW(&mut info) } == 0 { + let shell_code = info.hInstApp as usize; + let code = if (1..=32).contains(&shell_code) { + shell_code as u32 + } else { + unsafe { GetLastError() } + }; + if code == SE_ERR_NOASSOC || code == ERROR_NO_ASSOCIATION { + return Err("Windows has no PDF application registered with Print support.".into()); + } + return Err(format!("Windows print dispatch failed (code {code})")); + } + } + Ok(()) +} + /// Dispatch a PDF to a specific printer with [`PrintOptions`]. #[cfg(not(target_arch = "wasm32"))] fn dispatch_to_printer_opts( @@ -665,67 +901,43 @@ fn dispatch_to_printer_opts( ) -> Result { #[cfg(target_os = "windows")] { - use std::ffi::OsStr; - use std::os::windows::ffi::OsStrExt; - use windows_sys::Win32::Foundation::{GetLastError, ERROR_NO_ASSOCIATION}; - use windows_sys::Win32::UI::Shell::{ - ShellExecuteExW, SHELLEXECUTEINFOW, SEE_MASK_FLAG_NO_UI, SEE_MASK_NOASYNC, - SE_ERR_NOASSOC, - }; - use windows_sys::Win32::UI::WindowsAndMessaging::SW_HIDE; - - let wide = |s: &str| -> Vec { OsStr::new(s).encode_wide().chain(Some(0)).collect() }; - let path_wide: Vec = path.as_os_str().encode_wide().chain(Some(0)).collect(); - let (verb, params, label) = match opts.printer.as_deref() { - Some(p) if !p.is_empty() => (wide("printto"), Some(wide(p)), p.to_string()), - _ => (wide("print"), None, "default printer".to_string()), - }; - let params_ptr = params.as_ref().map(|v| v.as_ptr()).unwrap_or(std::ptr::null()); - let shell = (|| -> Result { - for _ in 0..opts.copies.max(1) { - let mut info = SHELLEXECUTEINFOW { - cbSize: std::mem::size_of::() as u32, - fMask: SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC, - lpVerb: verb.as_ptr(), - lpFile: path_wide.as_ptr(), - lpParameters: params_ptr, - nShow: SW_HIDE, - ..Default::default() - }; - if unsafe { ShellExecuteExW(&mut info) } == 0 { - let shell_code = info.hInstApp as usize; - let code = if (1..=32).contains(&shell_code) { - shell_code as u32 - } else { - unsafe { GetLastError() } - }; - if code == SE_ERR_NOASSOC || code == ERROR_NO_ASSOCIATION { - return Err( - "Windows has no PDF application registered with Print support.".into(), - ); + let default_printer = windows_default_printer().ok(); + let route = plan_print_route( + opts.printer.as_deref(), + default_printer.as_deref(), + pdf_printto_command().is_some(), + )?; + let printer = route.printer().to_string(); + let via_application = match &route { + PrintRoute::PdfApplication { .. } => { + match shell_print_to(path, &printer, opts.copies) { + Ok(()) => true, + // The PDF application refused the job: the spooler route + // still prints it, and the report says so. + Err(_) => { + gdi_raster_print(path, &printer, opts.copies, None)?; + false } - return Err(format!("Windows print dispatch failed (code {code})")); } } - Ok(label) - })(); - if let Ok(sent) = shell { - return Ok(sent); + PrintRoute::Direct { .. } => { + gdi_raster_print(path, &printer, opts.copies, None)?; + false + } + }; + // A direct job is fully spooled when the calls return; the temporary + // PDF behind it has no reader left. The PDF application reads its + // file after ShellExecute returns, so that copy is left to the + // temp folder. + if !via_application && path.starts_with(std::env::temp_dir()) { + let _ = std::fs::remove_file(path); } - // Fallback: hand the page straight to the spooler through GDI, with no - // PDF application involved. Modern PDF readers (Edge, store apps) - // register only an `open` verb, which used to leave every plot - // failing with "no PDF application registered". - gdi_raster_print(path, opts.printer.as_deref(), opts.copies, None).map_err(|gdi| { - format!( - "{}; direct print fallback failed: {gdi}", - shell.err().unwrap_or_default() - ) - })?; - Ok(opts - .printer - .clone() - .unwrap_or_else(|| "default printer".to_string())) + let how = if via_application { + crate::t!("via the PDF application") + } else { + crate::t!("direct print") + }; + Ok(format!("{printer} ({how})")) } #[cfg(not(target_os = "windows"))] @@ -907,12 +1119,7 @@ mod printer_properties_tests { let printed = std::env::temp_dir().join("ocs_gdi_ink_output.pdf"); let _ = std::fs::remove_file(&printed); - let result = super::gdi_raster_print( - &plot, - Some("Microsoft Print to PDF"), - 1, - Some(&printed), - ); + let result = super::gdi_raster_print(&plot, "Microsoft Print to PDF", 1, Some(&printed)); assert!(result.is_ok(), "GDI print failed: {result:?}"); // The spooled PDF must contain the black block: rasterise it and @@ -971,7 +1178,7 @@ mod printer_properties_tests { let printed = std::env::temp_dir().join("ocs_gdi_orientation_output.pdf"); let _ = std::fs::remove_file(&printed); - let result = super::gdi_raster_print(&plot, Some("Microsoft Print to PDF"), 1, Some(&printed)); + let result = super::gdi_raster_print(&plot, "Microsoft Print to PDF", 1, Some(&printed)); assert!(result.is_ok(), "GDI print failed: {result:?}"); let page = crate::scene::model::pdf_raster::rasterize_page( diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 17536c4f7..482c11d3d 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -3144,6 +3144,15 @@ 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")), + "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")), + "Printers ({count}):" => Some(("plot", "printers-count")), + "Could not list printers: {error}" => Some(("plot", "could-not-list-printers-error")), + "Plots go through the PDF application: {command}" => Some(("plot", "plots-go-through-the-pdf-application-command")), + "Plots go straight to the printer (no PDF application registers a print verb)." => Some(("plot", "plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb")), + "via the PDF application" => Some(("plot", "via-the-pdf-application")), + "direct print" => Some(("plot", "direct-print")), "X = %{x}, Y = %{y}, Z = %{z}" => Some(("inquiry", "x-x-y-y-z-z")), "X Scale" => Some(("properties", "x-scale")), "X-Ray" => Some(("draw", "x-ray")), diff --git a/src/scene/model/pdf_raster.rs b/src/scene/model/pdf_raster.rs index 91b24d051..3d6ed2146 100644 --- a/src/scene/model/pdf_raster.rs +++ b/src/scene/model/pdf_raster.rs @@ -69,6 +69,13 @@ pub fn rasterize_page_at_dpi(path: &str, page: &str, dpi: f32) -> Option Option> { + rasterize_uncached(path, page, dpi) +} + fn rasterize_uncached(path: &str, page: &str, dpi: f32) -> Option> { let bytes = source_bytes(path)?; let pdf = Pdf::new(bytes).ok()?; diff --git a/src/ui/window/plot.rs b/src/ui/window/plot.rs index 451860c9a..286bdb59a 100644 --- a/src/ui/window/plot.rs +++ b/src/ui/window/plot.rs @@ -365,6 +365,11 @@ pub struct PlotDialogState { /// sentinels. #[serde(skip)] pub printers: Vec, + /// Why the system gave no printer list (a stopped spooler, a failing + /// print service), shown under the printer row so an empty list is + /// never mistaken for "no printers". + #[serde(skip)] + pub printers_error: Option, /// Name of the system default printer, shown next to the default entry. #[serde(skip)] pub default_printer: Option, @@ -452,6 +457,7 @@ impl Default for PlotDialogState { fn default() -> Self { Self { printers: Vec::new(), + printers_error: None, default_printer: None, printer_media: None, custom_papers: Vec::new(), @@ -1090,6 +1096,13 @@ pub fn view_window( // editor itself unfolds there when Properties… is pressed. let printer_note: Element<'_, Message> = match (&s.printer_editor, &s.printer) { (Some(draft), _) => printer_options_editor(draft, width), + (None, _) if s.printers_error.is_some() => { + let error = s.printers_error.clone().unwrap_or_default(); + text(crate::tf!("Could not list printers: {error}")) + .size(10) + .style(muted_style) + .into() + } (None, Some(printer)) if s.driver_options.get(printer).is_some_and(|o| !o.is_empty()) => { let count = s.driver_options[printer].len(); text(crate::tf!("{count} driver option(s) set for this printer."))