forked from 1b-agent/opencadstudio-upstream
fix(print): send Windows plots to the chosen printer, name the default, report failures
On Windows a plot with no printer chosen went to the PDF application's bare "print" verb — whatever that application considered its default — and a viewer without a "printto" verb (Edge, store readers) silently dropped the printer the dialog had chosen. The dialog also never named the system default, and a failed printer enumeration left only "Default" and "PDF" with the reason written to a console nobody sees. - The default printer is looked up by name (GetDefaultPrinterW) both for the dialog's "System default printer (…)" entry and before every job. - A pure route decision (plan_print_route) resolves "Default" to that name and uses the PDF application only when it registered a "printto" verb for .pdf (AssocQueryStringW); otherwise the job goes straight to the spooler through the existing GDI path. The report says which: "Sent to printer: NAME (direct print | via the PDF application)". - list_printers reports the system's own error; the plot dialog shows it under the printer row and the command line warns once. A desktop without CUPS is an empty list, not an error. - PRINTERS command: default printer, listed printers (or the error) and, on Windows, the print route — a report to paste into an issue. - Properties… on the Default entry opens that printer's preferences sheet. - GDI path: the driver's dmSize is kept (its private data lives behind it), the print raster is no longer memoised for the session, and a spooled temporary PDF is removed. Tests: route planning (named / blank / no default / printto or not), the PRINTERS command; the two Windows-only GDI tests pass the resolved name. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
2b38f798c0
commit
c36ae08fd0
29 files changed
+592
-112
No files matched your search
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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<String> = 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:?}");
|
||||
}
|
||||
}
|
||||
@@ -712,6 +712,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"PERF",
|
||||
"PERSP",
|
||||
"PLOT",
|
||||
"PRINTERS",
|
||||
"PLOTSTYLE",
|
||||
"PLOTSTYLEEDITOR",
|
||||
"PLOTSTYLEPANEL",
|
||||
|
||||
+22
-3
@@ -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<Message> {
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -232,8 +232,14 @@ pub fn default_printer_name() -> Option<String> {
|
||||
(!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<String> {
|
||||
crate::io::print_to_printer::windows_default_printer().ok()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn default_printer_name() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
+314
-107
@@ -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<String> {
|
||||
pub fn list_printers() -> Result<Vec<String>, 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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows_sys::Win32::UI::Shell::{AssocQueryStringW, ASSOCF_NOTRUNCATE, ASSOCSTR_COMMAND};
|
||||
|
||||
let wide =
|
||||
|s: &str| -> Vec<u16> { 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<PrintRoute, String> {
|
||||
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<Vec<
|
||||
{
|
||||
return None;
|
||||
}
|
||||
(*dm).dmSize = std::mem::size_of::<DEVMODEW>() 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::<DEVMODEW>() 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<Vec<
|
||||
#[cfg(target_os = "windows")]
|
||||
fn gdi_raster_print(
|
||||
path: &std::path::Path,
|
||||
printer: Option<&str>,
|
||||
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<u16> { 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<u16> { OsStr::new(s).encode_wide().chain(Some(0)).collect() };
|
||||
let path_wide: Vec<u16> = 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::<SHELLEXECUTEINFOW>() 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<String, String> {
|
||||
#[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<u16> { OsStr::new(s).encode_wide().chain(Some(0)).collect() };
|
||||
let path_wide: Vec<u16> = 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<String, String> {
|
||||
for _ in 0..opts.copies.max(1) {
|
||||
let mut info = SHELLEXECUTEINFOW {
|
||||
cbSize: std::mem::size_of::<SHELLEXECUTEINFOW>() 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(
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -69,6 +69,13 @@ pub fn rasterize_page_at_dpi(path: &str, page: &str, dpi: f32) -> Option<Arc<Pdf
|
||||
built
|
||||
}
|
||||
|
||||
/// Rasterise without touching the memo: for one-off consumers such as a
|
||||
/// print job, whose 300 DPI page would otherwise stay cached for the whole
|
||||
/// session (tens of megabytes per sheet).
|
||||
pub fn rasterize_page_at_dpi_uncached(path: &str, page: &str, dpi: f32) -> Option<Arc<PdfPage>> {
|
||||
rasterize_uncached(path, page, dpi)
|
||||
}
|
||||
|
||||
fn rasterize_uncached(path: &str, page: &str, dpi: f32) -> Option<Arc<PdfPage>> {
|
||||
let bytes = source_bytes(path)?;
|
||||
let pdf = Pdf::new(bytes).ok()?;
|
||||
|
||||
@@ -365,6 +365,11 @@ pub struct PlotDialogState {
|
||||
/// sentinels.
|
||||
#[serde(skip)]
|
||||
pub printers: Vec<String>,
|
||||
/// 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<String>,
|
||||
/// Name of the system default printer, shown next to the default entry.
|
||||
#[serde(skip)]
|
||||
pub default_printer: Option<String>,
|
||||
@@ -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."))
|
||||
|
||||
Reference in new issue
Block a user