diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 393bce6b8..f54eb5651 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -5025,6 +5025,7 @@ plot = .export-pdf = تصدير PDF .fit-to-paper = ملاءمة للورق .layout-plots-the-current-sheet-using-the-selec = يطبع التخطيط الورقة الحالية باستخدام حجم الورق المحدد. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = دمج الخطوط المتداخلة .object-lineweights = سماكات خطوط الكائنات .orientation = الاتجاه diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index fa07928a7..4cd537a82 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -5013,6 +5013,7 @@ plot = .export-pdf = Експортирай PDF .fit-to-paper = Побери в хартията .layout-plots-the-current-sheet-using-the-selec = Оформлението плотира текущия лист, използвайки избрания размер хартия. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = Обедини припокриващи се линии .object-lineweights = Дебелини на линии на обект .orientation = Ориентация diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 657eadb7a..cd37022e6 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -5010,6 +5010,7 @@ plot = .export-pdf = Exportujte PDF .fit-to-paper = Přizpůsobit papíru .layout-plots-the-current-sheet-using-the-selec = Rozvržení vykreslí aktuální list pomocí zvolené velikosti papíru. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = Sloučit překrývající se čáry .object-lineweights = Tloušťky čar objektů .orientation = Orientace diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 2ee047680..e41fa3534 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -5011,6 +5011,7 @@ plot = .export-pdf = PDF exportieren .fit-to-paper = An Papier anpassen .layout-plots-the-current-sheet-using-the-selec = Das Layout plottet das aktuelle Blatt mit dem ausgewählten Papierformat. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = Zusammenführung überlappender Leitungen .object-lineweights = Objektliniengewichte .orientation = Ausrichtung diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 77a87777c..5911988ce 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -5013,6 +5013,7 @@ plot = .export-pdf = Εξαγωγή PDF .fit-to-paper = Προσαρμογή στο χαρτί .layout-plots-the-current-sheet-using-the-selec = Η επιλογή Διάταξη εκτυπώνει το τρέχον φύλλο με το επιλεγμένο μέγεθος χαρτιού. + .sheet-sizes-and-printable-areas-reported-by-th = Μεγέθη φύλλων και εκτυπώσιμες περιοχές όπως τα αναφέρει ο εκτυπωτής. .merge-overlapping-lines = Συγχώνευση επικαλυπτόμενων γραμμών .object-lineweights = Πάχη γραμμών αντικειμένων .orientation = Προσανατολισμός diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index c767a4e9d..eddfd0028 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -5014,6 +5014,7 @@ plot = .export-pdf = Export PDF .fit-to-paper = Fit to paper .layout-plots-the-current-sheet-using-the-selec = Layout plots the current sheet using the selected paper size. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = Merge overlapping lines .object-lineweights = Object lineweights .orientation = Orientation diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 7aebd0d2e..2a52f63aa 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -5013,6 +5013,7 @@ plot = .export-pdf = Exportar PDF .fit-to-paper = Ajustar al papel .layout-plots-the-current-sheet-using-the-selec = La presentación traza la hoja actual con el tamaño de papel seleccionado. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = Fusionar líneas solapadas .object-lineweights = Grosores de línea de objetos .orientation = Orientación diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index b9404a17a..e3fbfd865 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -5012,6 +5012,7 @@ plot = .export-pdf = Vie PDF .fit-to-paper = Sovita paperille .layout-plots-the-current-sheet-using-the-selec = Asettelu piirtää nykyisen arkin käyttämällä valittua paperikokoa. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = Yhdistä päällekkäiset rivit .object-lineweights = Objektin viivan painot .orientation = Suuntautuminen diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index cae171e76..0cbfa07b4 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -5013,6 +5013,7 @@ plot = .export-pdf = Exportation PDF .fit-to-paper = Ajuster au papier .layout-plots-the-current-sheet-using-the-selec = La présentation trace la feuille courante avec le format de papier sélectionné. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = Fusionner les lignes qui se chevauchent .object-lineweights = Poids de ligne d'objet .orientation = Orientation diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 01edb1512..5a2a4f100 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -5001,6 +5001,7 @@ plot = .export-pdf = निर्यात PDF .fit-to-paper = कागज़ पर फ़िट करें .layout-plots-the-current-sheet-using-the-selec = लेआउट चयनित कागज़ आकार पर वर्तमान शीट प्लॉट करता है। + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = मर्ज ओवरलैपिंग लाइन .object-lineweights = ऑब्जेक्ट लाइनवेट .orientation = अभिविन्यास diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 4b13d66b2..5222d1072 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -5012,6 +5012,7 @@ plot = .export-pdf = PDF exportálása .fit-to-paper = Papírra illeszthető .layout-plots-the-current-sheet-using-the-selec = Az Elrendezés az aktuális lapot a kiválasztott papírmérettel ábrázolja. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = Egyesítse az átfedő vonalakat .object-lineweights = Objektumvonalvastagságok .orientation = Tájékozódás diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index af9406dff..d5277f8b7 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -5012,6 +5012,7 @@ plot = .export-pdf = Esporta PDF .fit-to-paper = Adatta alla carta .layout-plots-the-current-sheet-using-the-selec = Layout traccia il foglio corrente utilizzando il formato carta selezionato. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = Unisci linee sovrapposte .object-lineweights = Spessori linea degli oggetti .orientation = Orientamento diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 13ee9599a..4d85663b0 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -5001,6 +5001,7 @@ plot = .export-pdf = PDF 書き出し .fit-to-paper = 用紙にフィット .layout-plots-the-current-sheet-using-the-selec = 選択した用紙サイズで現在のシートを印刷します。 + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = 重複線を結合 .object-lineweights = オブジェクトの線の太さ .orientation = 方向 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index c93a7cc5d..22cb62a7c 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -5012,6 +5012,7 @@ plot = .export-pdf = PDF 내보내기 .fit-to-paper = 종이에 맞추기 .layout-plots-the-current-sheet-using-the-selec = 레이아웃은 선택한 용지 크기를 사용하여 현재 시트를 플롯합니다. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = 겹치는 선 병합 .object-lineweights = 객체 선가중치 .orientation = 오리엔테이션 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 77d3c1c86..7cd127c2f 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -5011,6 +5011,7 @@ plot = .export-pdf = Exporteren PDF .fit-to-paper = Aan papier aanpassen .layout-plots-the-current-sheet-using-the-selec = De lay-out plot het huidige vel op het geselecteerde papierformaat. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = Overlaplijnen samenvoegen .object-lineweights = Objectlijndiktes .orientation = Oriëntatie diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index eb4ed4594..c215855f1 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -5012,6 +5012,7 @@ plot = .export-pdf = Eksportuj PDF .fit-to-paper = Dopasuj do papieru .layout-plots-the-current-sheet-using-the-selec = Układ drukuje bieżący arkusz przy użyciu wybranego rozmiaru papieru. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = Połącz nakładające się linie .object-lineweights = Szerokości linii obiektu .orientation = Orientacja diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index bc244f710..5e306b0ea 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -5013,6 +5013,7 @@ plot = .export-pdf = Exportar PDF .fit-to-paper = Ajustar ao papel .layout-plots-the-current-sheet-using-the-selec = Layout plota a folha atual usando o tamanho de papel selecionado. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = Mesclar linhas sobrepostas .object-lineweights = Espessuras de linha dos objetos .orientation = Orientação diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 31b35ab9a..92727daae 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -5014,6 +5014,7 @@ plot = .export-pdf = Экспорт PDF .fit-to-paper = Вписать в лист .layout-plots-the-current-sheet-using-the-selec = Лист печатается на выбранном формате бумаги. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = Слияние пересекающихся линий .object-lineweights = Весы для объектов .orientation = Ориентация diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 96a534d60..f24afc924 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -4953,6 +4953,7 @@ plot = .export-pdf = PDF'ye Aktar .fit-to-paper = Kağıda sığdır .layout-plots-the-current-sheet-using-the-selec = Yerleşim, geçerli sayfayı seçilen kağıt boyutuyla çizer. + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = Çakışan çizgileri birleştir .object-lineweights = Nesne çizgi kalınlıkları .orientation = Yönelim diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 2f1bc69af..34bc20fc7 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -4996,6 +4996,7 @@ plot = .export-pdf = 导出 PDF .fit-to-paper = 布满图纸 .layout-plots-the-current-sheet-using-the-selec = 布局使用所选纸张尺寸打印当前图纸。 + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = 合并重叠线 .object-lineweights = 对象线宽 .orientation = 方向 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index a26fd898d..49f85db3c 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -5008,6 +5008,7 @@ plot = .export-pdf = 匯出 PDF .fit-to-paper = 布滿圖紙 .layout-plots-the-current-sheet-using-the-selec = 配置使用所選紙張尺寸列印目前圖紙。 + .sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer. .merge-overlapping-lines = 合併重疊線 .object-lineweights = 物件線寬 .orientation = 方向 diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 4cb7e81e2..9a32a2dbc 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -120,6 +120,21 @@ fn plot_dialog_sheet_mm(d: &crate::ui::window::plot::PlotDialogState) -> (f64, f plot_dialog_paper(d).sheet_mm(plot_dialog_orientation(d)) } +/// Whether the dialog still names the sheet the stored settings carry. +fn paper_unchanged( + d: &crate::ui::window::plot::PlotDialogState, + stored: &acadrust::objects::PlotSettings, +) -> bool { + !stored.paper_size.is_empty() + && crate::io::paper_catalog::from_drawing( + &stored.paper_size, + stored.paper_width, + stored.paper_height, + ) + .canonical + == d.paper +} + /// The media name to store for the dialog's sheet. A drawing keeps its own /// spelling of a sheet the user did not change — a system printer's `A4` or a /// plotter's custom name must not turn into the catalogue's canonical name @@ -130,21 +145,62 @@ fn paper_name_for_settings( d: &crate::ui::window::plot::PlotDialogState, stored: &acadrust::objects::PlotSettings, ) -> String { - let unchanged = !stored.paper_size.is_empty() - && crate::io::paper_catalog::from_drawing( - &stored.paper_size, - stored.paper_width, - stored.paper_height, - ) - .canonical - == d.paper; - if unchanged { + if paper_unchanged(d, stored) { stored.paper_size.clone() } else { d.paper.clone() } } +/// The plot device the dialog currently targets. +fn plot_dialog_device(d: &crate::ui::window::plot::PlotDialogState) -> crate::io::plot_device::PlotDevice { + use crate::io::plot_device::PlotDevice; + if d.to_file { + PlotDevice::Pdf + } else { + match &d.printer { + Some(name) => PlotDevice::Printer(name.clone()), + None => PlotDevice::None, + } + } +} + +/// The plotter name to store. Same rule as the sheet: a device name AutoCAD +/// wrote (its PDF driver, a plotter `.pc3` we plot to PDF for it) stays as it +/// is while the dialog still points at the same kind of device; a changed +/// device, or one of our own legacy labels, is written in AutoCAD's spelling. +fn device_name_for_settings( + d: &crate::ui::window::plot::PlotDialogState, + stored: &acadrust::objects::PlotSettings, +) -> String { + use crate::io::plot_device::PlotDevice; + let device = plot_dialog_device(d); + let (stored_device, keep_stored) = PlotDevice::from_stored_name(&stored.printer_name); + if keep_stored && stored_device == device { + stored.printer_name.clone() + } else { + device.canonical_name() + } +} + +/// The unprintable margins to store: the drawing's own while neither the +/// sheet nor the device changed (they came from AutoCAD's driver and we have +/// no better source), otherwise what our device reports for the new sheet — +/// the equivalent of AutoCAD's `PAPERUPDATE`. +fn margins_for_settings( + d: &crate::ui::window::plot::PlotDialogState, + stored: &acadrust::objects::PlotSettings, +) -> acadrust::objects::PaperMargin { + use crate::io::plot_device::PlotDevice; + let device = plot_dialog_device(d); + let (stored_device, _) = PlotDevice::from_stored_name(&stored.printer_name); + if paper_unchanged(d, stored) && stored_device == device { + return stored.margins; + } + let margins = device.margins_mm(&plot_dialog_paper(d)); + acadrust::objects::PaperMargin::new(margins.left, margins.bottom, margins.right, margins.top) +} + fn plot_content_extents(content: &PlotContent) -> Option<(f64, f64, f64, f64)> { let PlotContent { wires, hatches, wipeouts, images, .. } = content; let mut bounds = ( @@ -2944,7 +3000,14 @@ impl OpenCADStudio { .clone() .or_else(|| self.tabs[i].scene.plot_settings_for(&layout_name)) .unwrap_or_else(|| PlotSettings::new("")); - ps.paper_size = paper_name_for_settings(&dialog, &ps); + // Names and margins are decided against the stored settings before + // any field is overwritten. + let paper_size = paper_name_for_settings(&dialog, &ps); + let printer_name = device_name_for_settings(&dialog, &ps); + let margins = margins_for_settings(&dialog, &ps); + ps.paper_size = paper_size; + ps.printer_name = printer_name; + ps.margins = margins; ps.paper_width = w; ps.paper_height = h; ps.plot_type = match plot_area { @@ -2983,11 +3046,6 @@ impl OpenCADStudio { ps.standard_scale_factor = factor; ps.flags.use_standard_scale = false; } - ps.printer_name = if dialog.to_file { - crate::ui::window::plot::OUT_PDF.into() - } else { - dialog.printer.clone().unwrap_or_default() - }; ps.current_style_sheet = dialog.style_name.clone(); ps.flags.scale_lineweights = dialog.scale_lw; ps.flags.print_lineweights = dialog.lineweights; @@ -3688,6 +3746,7 @@ impl OpenCADStudio { // 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(); + d.default_printer = crate::io::plot_device::default_printer_name(); d.plot_styles = crate::io::plot_style::available_ctb_names(); d.scales = scales; d.plot_views = plot_views; @@ -3771,7 +3830,29 @@ impl OpenCADStudio { } } self.active_modal = Some(crate::app::ModalKind::Plot); - Task::none() + self.request_printer_media() + } + + /// Keep the dialog's view of the selected printer's media current: use the + /// session cache when the printer was asked before, otherwise ask it in + /// the background and clear the stale answer meanwhile. PDF output and + /// the default printer list the catalogue instead. + fn request_printer_media(&mut self) -> Task { + let d = &mut self.plot_dialog; + let Some(printer) = (!d.to_file).then(|| d.printer.clone()).flatten() else { + d.printer_media = None; + return Task::none(); + }; + if let Some(caps) = crate::io::plot_device::cached_printer_capabilities(&printer) { + d.printer_media = Some(caps); + return Task::none(); + } + d.printer_media = None; + let name = printer.clone(); + background_task( + move || crate::io::plot_device::printer_capabilities(&printer), + move |caps| Message::PlotDlg(crate::ui::window::plot::PlotDlgMsg::PrinterMedia(name, caps)), + ) } fn normalize_common_plot_dialog(&mut self) { @@ -3805,6 +3886,13 @@ impl OpenCADStudio { self.plot_dialog.to_file = false; self.plot_dialog.printer = Some(s); } + self.request_printer_media() + } + M::PrinterMedia(printer, caps) => { + let d = &mut self.plot_dialog; + if !d.to_file && d.printer.as_deref() == Some(printer.as_str()) { + d.printer_media = caps; + } Task::none() } M::PrinterProperties => { @@ -3967,7 +4055,8 @@ impl OpenCADStudio { self.plot_dialog.paper_space = true; self.plot_dialog.area = "Layout".into(); } - Task::none() + // The setup may name a different printer. + self.request_printer_media() } M::SetCurrent => { self.apply_dialog_to_layout(); @@ -4194,7 +4283,12 @@ impl OpenCADStudio { settings } }; - ps.paper_size = paper_name_for_settings(d, &ps); + let paper_size = paper_name_for_settings(d, &ps); + let printer_name = device_name_for_settings(d, &ps); + let margins = margins_for_settings(d, &ps); + ps.paper_size = paper_size; + ps.printer_name = printer_name; + ps.margins = margins; ps.paper_width = w; ps.paper_height = h; ps.plot_type = match d.area.as_str() { @@ -4242,11 +4336,6 @@ impl OpenCADStudio { ps.standard_scale_factor = factor; ps.flags.use_standard_scale = false; } - ps.printer_name = if d.to_file { - crate::ui::window::plot::OUT_PDF.into() - } else { - d.printer.clone().unwrap_or_default() - }; ps.current_style_sheet = d.style_name.clone(); ps.flags.scale_lineweights = d.scale_lw; ps.flags.print_lineweights = d.lineweights; @@ -4402,12 +4491,19 @@ impl OpenCADStudio { _ => "Normal", } .into(); - if ps.printer_name.to_ascii_lowercase().contains("pdf") { - d.to_file = true; - d.printer = None; - } else { - d.to_file = false; - d.printer = (!ps.printer_name.is_empty()).then(|| ps.printer_name.clone()); + match crate::io::plot_device::PlotDevice::from_stored_name(&ps.printer_name).0 { + crate::io::plot_device::PlotDevice::Pdf => { + d.to_file = true; + d.printer = None; + } + crate::io::plot_device::PlotDevice::Printer(name) => { + d.to_file = false; + d.printer = Some(name); + } + crate::io::plot_device::PlotDevice::None => { + d.to_file = false; + d.printer = None; + } } d.style_name = style_name; d.apply_plot_styles = ps.flags.plot_plot_styles; @@ -4897,6 +4993,90 @@ mod plot_paper_tests { assert_eq!(app.plot_paper.canonical, "ISO_A3_(297.00_x_420.00_MM)"); } + #[test] + fn pdf_output_is_stored_as_the_autocad_pdf_driver() { + let mut app = app_with_printer_named_sheet(); + let _ = app.on_plot_dialog_open(); + let _ = app.on_plot_dlg(PlotDlgMsg::Printer(crate::ui::window::plot::OUT_PDF.into())); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = app.tabs[app.active_tab].scene.plot_settings_for("Layout1").unwrap(); + assert_eq!(ps.printer_name, "DWG To PDF.pc3"); + // A changed device takes the PDF driver's printable area for the sheet. + assert_eq!(ps.margins, acadrust::objects::PaperMargin::new(5.0, 5.0, 5.0, 5.0)); + } + + #[test] + fn legacy_pdf_label_is_upgraded_on_save() { + let mut app = app_with_printer_named_sheet(); + let i = app.active_tab; + let mut ps = app.tabs[i].scene.plot_settings_for("Layout1").unwrap(); + ps.printer_name = "Save to PDF file…".into(); + assert!(app.tabs[i].scene.set_layout_plot_settings("Layout1", &ps)); + let _ = app.on_plot_dialog_open(); + assert!(app.plot_dialog.to_file, "the legacy label still means PDF output"); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = app.tabs[i].scene.plot_settings_for("Layout1").unwrap(); + assert_eq!(ps.printer_name, "DWG To PDF.pc3"); + } + + #[test] + fn a_plotter_configuration_from_autocad_is_preserved_with_its_margins() { + let mut app = app_with_printer_named_sheet(); + let i = app.active_tab; + let mut ps = app.tabs[i].scene.plot_settings_for("Layout1").unwrap(); + ps.printer_name = "DWF6 ePlot.pc3".into(); + ps.paper_size = "ISO_A4_(210.00_x_297.00_MM)".into(); + ps.margins = acadrust::objects::PaperMargin::new(5.793749, 17.793753, 5.793744, 17.793747); + assert!(app.tabs[i].scene.set_layout_plot_settings("Layout1", &ps)); + let _ = app.on_plot_dialog_open(); + assert!(app.plot_dialog.to_file, "an unknown plotter plots to PDF here"); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let after = app.tabs[i].scene.plot_settings_for("Layout1").unwrap(); + assert_eq!(after.printer_name, "DWF6 ePlot.pc3"); + assert_eq!(after.margins, ps.margins, "AutoCAD's driver margins survive an unchanged setup"); + } + + #[test] + fn changing_the_sheet_updates_the_printable_area() { + let mut app = app_with_printer_named_sheet(); + let i = app.active_tab; + let mut ps = app.tabs[i].scene.plot_settings_for("Layout1").unwrap(); + ps.printer_name = "DWG To PDF.pc3".into(); + ps.paper_size = "ISO_A4_(210.00_x_297.00_MM)".into(); + ps.margins = acadrust::objects::PaperMargin::new(5.0, 5.0, 5.0, 5.0); + assert!(app.tabs[i].scene.set_layout_plot_settings("Layout1", &ps)); + let _ = app.on_plot_dialog_open(); + let _ = app.on_plot_dlg(PlotDlgMsg::Paper("ISO_full_bleed_A4_(210.00_x_297.00_MM)".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = app.tabs[i].scene.plot_settings_for("Layout1").unwrap(); + assert_eq!(ps.paper_size, "ISO_full_bleed_A4_(210.00_x_297.00_MM)"); + assert_eq!(ps.margins, acadrust::objects::PaperMargin::new(0.0, 0.0, 0.0, 0.0)); + let _ = app.on_plot_dlg(PlotDlgMsg::Paper("ISO_A3_(297.00_x_420.00_MM)".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = app.tabs[i].scene.plot_settings_for("Layout1").unwrap(); + assert_eq!(ps.margins, acadrust::objects::PaperMargin::new(5.0, 5.0, 5.0, 5.0)); + assert_eq!((ps.paper_width, ps.paper_height), (420.0, 297.0)); + } + + #[test] + fn a_selected_printer_is_stored_by_name() { + let mut app = app_with_printer_named_sheet(); + let _ = app.on_plot_dialog_open(); + let _ = app.on_plot_dlg(PlotDlgMsg::Printer("OCS Test Printer".into())); + assert!(!app.plot_dialog.to_file); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = app.tabs[app.active_tab].scene.plot_settings_for("Layout1").unwrap(); + assert_eq!(ps.printer_name, "OCS Test Printer"); + // Reopening resolves the stored name back to the same printer. + let _ = app.on_plot_dialog_open(); + assert_eq!(app.plot_dialog.printer.as_deref(), Some("OCS Test Printer")); + // A media answer for a printer that is no longer selected is ignored. + let _ = app.on_plot_dlg(PlotDlgMsg::Printer(crate::ui::window::plot::OUT_PDF.into())); + let caps = std::sync::Arc::new(crate::io::plot_device::PrinterCapabilities::default()); + let _ = app.on_plot_dlg(PlotDlgMsg::PrinterMedia("OCS Test Printer".into(), Some(caps))); + assert!(app.plot_dialog.printer_media.is_none()); + } + #[test] fn an_unknown_sheet_from_a_drawing_keeps_its_own_dimensions() { let mut app = app_with_printer_named_sheet(); diff --git a/src/io/mod.rs b/src/io/mod.rs index 886f49aee..8bc9c7506 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -21,6 +21,7 @@ pub mod linetypes; pub mod patterns; pub mod update_check; pub mod paper_catalog; +pub mod plot_device; pub mod thumbnail; #[cfg(target_arch = "wasm32")] mod web_worker; diff --git a/src/io/plot_device.rs b/src/io/plot_device.rs new file mode 100644 index 000000000..a428c0f39 --- /dev/null +++ b/src/io/plot_device.rs @@ -0,0 +1,482 @@ +//! Plot devices: what a page setup names as its plotter, and what that device +//! can print. +//! +//! AutoCAD stores the device in `PlotSettings::printer_name` as the plotter +//! configuration's name — `DWG To PDF.pc3` for its PDF driver, the Windows +//! printer's name for a system printer, `None` when nothing is assigned. A +//! drawing saved here has to use the same spelling so AutoCAD attaches the +//! layout to its own PDF driver instead of reporting a missing device; and a +//! device name that arrives from AutoCAD (an HP plotter's `.pc3`, `DWF6 +//! ePlot.pc3`) must go back out untouched even though we cannot drive it. +//! +//! The device also decides the *printable area*: the sheet inset by the +//! driver's unprintable margins, which AutoCAD writes into the layout and +//! draws as the dashed rectangle in paper space. The PDF driver's margins are +//! a fixed table; a CUPS printer reports its media and margins over IPP +//! (`media-col-database`), which is queried once per printer and cached. + +use crate::io::paper_catalog::{self, PaperSize, PaperUnits, PaperVariant}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, OnceLock}; + +/// AutoCAD's plotter configuration name for PDF output. +pub const PDF_DEVICE_NAME: &str = "DWG To PDF.pc3"; +/// AutoCAD's spelling for "no plotter assigned". +pub const NO_DEVICE_NAME: &str = "None"; +/// The label older Open CAD Studio builds wrote into drawings for PDF output. +/// Recognised on read so those drawings keep plotting to PDF, and always +/// rewritten with [`PDF_DEVICE_NAME`] on save. +const LEGACY_PDF_LABEL: &str = "Save to PDF file…"; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PlotDevice { + /// No plotter assigned (AutoCAD `None`). We plot to the system default + /// printer in that case, which is what the plot dialog offers. + None, + /// PDF file output, AutoCAD's `DWG To PDF.pc3`. + Pdf, + /// A system printer by its queue / spooler name. + Printer(String), +} + +impl PlotDevice { + /// The name AutoCAD would store for this device. + pub fn canonical_name(&self) -> String { + match self { + PlotDevice::None => NO_DEVICE_NAME.to_string(), + PlotDevice::Pdf => PDF_DEVICE_NAME.to_string(), + PlotDevice::Printer(name) => name.clone(), + } + } + + /// Interpret a stored `printer_name`. Returns the device and whether the + /// stored spelling should be preserved on save: an AutoCAD plotter + /// configuration we cannot drive (`DWF6 ePlot.pc3`, an HP `.pc3`) is + /// treated as PDF output but keeps its name, so the drawing still attaches + /// to that plotter in AutoCAD; our own legacy label and an empty name are + /// not worth preserving. + pub fn from_stored_name(name: &str) -> (Self, bool) { + let name = name.trim(); + if name.is_empty() || name.eq_ignore_ascii_case(NO_DEVICE_NAME) { + return (PlotDevice::None, false); + } + if name == LEGACY_PDF_LABEL { + return (PlotDevice::Pdf, false); + } + let lower = name.to_ascii_lowercase(); + if lower.ends_with(".pc3") { + // Any AutoCAD plotter configuration: PDF ones are ours to drive, + // the rest plot to PDF here but keep their name for AutoCAD. + return (PlotDevice::Pdf, true); + } + (PlotDevice::Printer(name.to_string()), true) + } + + /// Unprintable margins the device imposes on `paper`, in millimetres. + pub fn margins_mm(&self, paper: &PaperSize) -> Margins { + match self { + PlotDevice::Pdf => pdf_margins_mm(paper), + PlotDevice::Printer(name) => printer_margins_mm(name, paper), + // No device: AutoCAD keeps the margins of the last device or its + // defaults; without one we show the full sheet as printable. + PlotDevice::None => Margins::ZERO, + } + } +} + +/// Unprintable margins in millimetres, in the order AutoCAD stores them. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Margins { + pub left: f64, + pub bottom: f64, + pub right: f64, + pub top: f64, +} + +impl Margins { + pub const ZERO: Margins = Margins { + left: 0.0, + bottom: 0.0, + right: 0.0, + top: 0.0, + }; + + pub const fn uniform(value: f64) -> Self { + Margins { + left: value, + bottom: value, + right: value, + top: value, + } + } +} + +// ── PDF driver ──────────────────────────────────────────────────────────── + +/// Margins of AutoCAD's `DWG To PDF.pc3` sheets. +/// +/// Full-bleed sheets print edge to edge by definition. The standard and +/// expand values are provisional — the driver's own printable areas have to +/// be read off AutoCAD's Plotter Configuration Editor (Modify Standard Paper +/// Sizes) and copied here; until then a symmetric inset keeps the printable +/// rectangle visibly distinct from the sheet. +const PDF_STANDARD_MARGIN_MM: f64 = 5.0; +const PDF_EXPAND_MARGIN_MM: f64 = 2.0; + +fn pdf_margins_mm(paper: &PaperSize) -> Margins { + match paper.variant { + PaperVariant::FullBleed => Margins::ZERO, + PaperVariant::Expand => Margins::uniform(PDF_EXPAND_MARGIN_MM), + PaperVariant::Standard => Margins::uniform(PDF_STANDARD_MARGIN_MM), + } +} + +// ── System printers ─────────────────────────────────────────────────────── + +/// One sheet a printer reports it can load, with its unprintable margins. +#[derive(Clone, Debug, PartialEq)] +pub struct PrinterMedia { + /// The sheet as our catalogue knows it (matched by size), or a custom + /// sheet built from the reported dimensions. + pub paper: PaperSize, + pub margins: Margins, + /// `true` when the printer also reports a borderless variant of this size. + pub borderless: bool, +} + +/// What a printer reported about its media, cached for the session. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct PrinterCapabilities { + /// Sheets in the printer's own order, one entry per distinct size. + pub media: Vec, + /// The printer's default sheet, when reported. + pub default_paper: Option, +} + +impl PrinterCapabilities { + /// Margins the printer reports for `paper`, matched by size. `None` when + /// the printer never mentioned a sheet of that size. + pub fn margins_for(&self, paper: &PaperSize) -> Option { + let (w, h) = paper.portrait_mm(); + self.media + .iter() + .find(|media| { + let (mw, mh) = media.paper.portrait_mm(); + (mw - w).abs() <= 0.5 && (mh - h).abs() <= 0.5 + }) + .map(|media| media.margins) + } +} + +fn capability_cache() -> &'static Mutex>> { + static CACHE: OnceLock>>> = OnceLock::new(); + CACHE.get_or_init(|| Mutex::new(HashMap::new())) +} + +/// The cached capabilities of `printer`, if they were queried already. +pub fn cached_printer_capabilities(printer: &str) -> Option> { + capability_cache().lock().ok()?.get(printer).cloned() +} + +/// Query `printer` for its media (blocking, one process spawn) and cache the +/// answer. Returns `None` when the platform cannot answer — the plot dialog +/// then falls back to the paper catalogue with unverified margins. +pub fn printer_capabilities(printer: &str) -> Option> { + if let Some(cached) = cached_printer_capabilities(printer) { + return Some(cached); + } + let caps = Arc::new(query_printer_capabilities(printer)?); + if let Ok(mut cache) = capability_cache().lock() { + cache.insert(printer.to_string(), Arc::clone(&caps)); + } + Some(caps) +} + +fn printer_margins_mm(printer: &str, paper: &PaperSize) -> Margins { + cached_printer_capabilities(printer) + .and_then(|caps| caps.margins_for(paper)) + .unwrap_or(Margins::ZERO) +} + +/// Name of the system default printer, when the platform reports one. +#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))] +pub fn default_printer_name() -> Option { + // `lpstat -d` → "system default destination: NAME" (the prefix is + // localized, the name follows the last colon). + let output = std::process::Command::new("lpstat") + .arg("-d") + .output() + .ok()?; + if !output.status.success() { + return None; + } + let text = String::from_utf8_lossy(&output.stdout); + let line = text.lines().find(|line| line.contains(':'))?; + let name = line.rsplit(':').next()?.trim(); + (!name.is_empty()).then(|| name.to_string()) +} + +/// Windows default printer discovery lands with native GDI printing. +#[cfg(any(target_arch = "wasm32", target_os = "windows"))] +pub fn default_printer_name() -> Option { + None +} + +#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))] +fn query_printer_capabilities(printer: &str) -> Option { + // cupsd answers for the queue itself, so this is quick even when the + // printer is off; `ipptool` is part of every CUPS install that has + // `lpstat`. The standard test file asks for every attribute; the two we + // read are `media-col-database` and `media-default`. + let uri = format!("ipp://localhost/printers/{}", printer.replace(' ', "%20")); + let output = std::process::Command::new("ipptool") + .args(["-tv", &uri, "get-printer-attributes.test"]) + .output() + .ok()?; + if !output.status.success() { + return None; + } + parse_ipp_media(&String::from_utf8_lossy(&output.stdout)) +} + +/// Windows media enumeration (`DeviceCapabilities`) lands with native GDI +/// printing; until then the catalogue serves every printer. +#[cfg(any(target_arch = "wasm32", target_os = "windows"))] +fn query_printer_capabilities(_printer: &str) -> Option { + None +} + +/// Read the media a printer reports from `ipptool -tv` output. +/// +/// The relevant lines look like +/// `media-col-database (1setOf collection) = {media-size={x-dimension=21000 +/// y-dimension=29700} media-bottom-margin=300 media-left-margin=300 …},{…}` +/// with sizes and margins in hundredths of a millimetre, and +/// `media-default (keyword) = iso_a4_210x297mm`. Borderless variants repeat a +/// size with zero margins; they collapse onto the bordered entry. +fn parse_ipp_media(text: &str) -> Option { + let database = attribute_value(text, "media-col-database")?; + let mut media: Vec = Vec::new(); + for entry in database.split("},{") { + let Some((w, h)) = collection_size_mm(entry) else { + continue; + }; + let margins = Margins { + left: hundredths_mm(entry, "media-left-margin"), + bottom: hundredths_mm(entry, "media-bottom-margin"), + right: hundredths_mm(entry, "media-right-margin"), + top: hundredths_mm(entry, "media-top-margin"), + }; + let borderless = margins == Margins::ZERO; + if let Some(existing) = media.iter_mut().find(|m| { + let (mw, mh) = m.paper.portrait_mm(); + let (pw, ph) = portrait(w, h); + (mw - pw).abs() <= 0.5 && (mh - ph).abs() <= 0.5 + }) { + // Keep the bordered margins as the sheet's printable area and + // remember that edge-to-edge is possible too. + if borderless { + existing.borderless = true; + } else if existing.margins == Margins::ZERO { + existing.margins = margins; + existing.borderless = true; + } + continue; + } + let (pw, ph) = portrait(w, h); + let paper = paper_catalog::match_dimensions_mm(pw, ph) + .cloned() + .unwrap_or_else(|| PaperSize::custom(pw, ph, PaperUnits::Millimeters)); + media.push(PrinterMedia { + paper, + margins, + borderless, + }); + } + if media.is_empty() { + return None; + } + let default_paper = attribute_value(text, "media-default") + .and_then(|name| pwg_size_mm(name.trim())) + .and_then(|(w, h)| { + let (w, h) = portrait(w, h); + media + .iter() + .find(|m| { + let (mw, mh) = m.paper.portrait_mm(); + (mw - w).abs() <= 0.5 && (mh - h).abs() <= 0.5 + }) + .map(|m| m.paper.clone()) + }); + Some(PrinterCapabilities { + media, + default_paper, + }) +} + +/// The text after `name (…) = ` on the line that declares `name`. +fn attribute_value<'a>(text: &'a str, name: &str) -> Option<&'a str> { + text.lines().find_map(|line| { + let line = line.trim_start(); + let rest = line.strip_prefix(name)?; + // The attribute name is followed by its syntax in parentheses. + if !rest.starts_with(' ') && !rest.starts_with('(') { + return None; + } + let (_, value) = rest.split_once('=')?; + Some(value.trim()) + }) +} + +fn collection_size_mm(entry: &str) -> Option<(f64, f64)> { + let x = hundredths(entry, "x-dimension")?; + let y = hundredths(entry, "y-dimension")?; + (x > 0.0 && y > 0.0).then_some((x, y)) +} + +fn hundredths(entry: &str, key: &str) -> Option { + let start = entry.find(key)? + key.len(); + let rest = entry[start..].trim_start_matches('='); + let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect(); + digits.parse::().ok().map(|v| v / 100.0) +} + +fn hundredths_mm(entry: &str, key: &str) -> f64 { + hundredths(entry, key).unwrap_or(0.0) +} + +fn portrait(w: f64, h: f64) -> (f64, f64) { + if w <= h { + (w, h) + } else { + (h, w) + } +} + +/// Dimensions from a PWG self-describing media name such as +/// `iso_a4_210x297mm` or `na_letter_8.5x11in`. +fn pwg_size_mm(name: &str) -> Option<(f64, f64)> { + let dims = name.rsplit('_').next()?; + let (dims, units) = dims + .strip_suffix("mm") + .map(|d| (d, PaperUnits::Millimeters)) + .or_else(|| dims.strip_suffix("in").map(|d| (d, PaperUnits::Inches)))?; + let (w, h) = dims.split_once('x')?; + let w: f64 = w.parse().ok()?; + let h: f64 = h.parse().ok()?; + Some((units.to_mm(w), units.to_mm(h))) +} + +#[cfg(test)] +mod tests { + use super::*; + + const IPP_SAMPLE: &str = " media-default (keyword) = iso_a4_210x297mm\n\ + media-supported (1setOf keyword) = iso_a4_210x297mm,om_a-4.borderless_210x297mm,na_letter_8.5x11in,custom_215x345mm_215x345mm\n\ + media-col-database (1setOf collection) = {media-size={x-dimension=21000 y-dimension=29700} media-bottom-margin=300 media-left-margin=300 media-right-margin=300 media-top-margin=300},{media-size={x-dimension=21000 y-dimension=29700} media-bottom-margin=0 media-left-margin=0 media-right-margin=0 media-top-margin=0},{media-size={x-dimension=21590 y-dimension=27940} media-bottom-margin=635 media-left-margin=635 media-right-margin=635 media-top-margin=635},{media-size={x-dimension=21500 y-dimension=34500} media-bottom-margin=300 media-left-margin=300 media-right-margin=300 media-top-margin=300}\n"; + + #[test] + fn canonical_names_follow_autocad() { + assert_eq!(PlotDevice::Pdf.canonical_name(), "DWG To PDF.pc3"); + assert_eq!(PlotDevice::None.canonical_name(), "None"); + assert_eq!( + PlotDevice::Printer("L3270".into()).canonical_name(), + "L3270" + ); + } + + #[test] + fn stored_names_resolve_and_say_whether_to_keep_them() { + assert_eq!(PlotDevice::from_stored_name(""), (PlotDevice::None, false)); + assert_eq!( + PlotDevice::from_stored_name("None"), + (PlotDevice::None, false) + ); + assert_eq!( + PlotDevice::from_stored_name("Save to PDF file…"), + (PlotDevice::Pdf, false) + ); + assert_eq!( + PlotDevice::from_stored_name("DWG To PDF.pc3"), + (PlotDevice::Pdf, true) + ); + assert_eq!( + PlotDevice::from_stored_name("AutoCAD PDF (High Quality Print).pc3"), + (PlotDevice::Pdf, true) + ); + // A plotter we cannot drive plots to PDF here but keeps its name. + assert_eq!( + PlotDevice::from_stored_name("DWF6 ePlot.pc3"), + (PlotDevice::Pdf, true) + ); + assert_eq!( + PlotDevice::from_stored_name("HP LaserJet 4"), + (PlotDevice::Printer("HP LaserJet 4".into()), true) + ); + } + + #[test] + fn pdf_margins_depend_on_the_sheet_variant() { + let a4 = paper_catalog::resolve("ISO_A4_(210.00_x_297.00_MM)").unwrap(); + let bleed = paper_catalog::resolve("ISO_full_bleed_A4_(210.00_x_297.00_MM)").unwrap(); + let expand = paper_catalog::resolve("ISO_expand_A4_(210.00_x_297.00_MM)").unwrap(); + assert_eq!(PlotDevice::Pdf.margins_mm(&a4), Margins::uniform(5.0)); + assert_eq!(PlotDevice::Pdf.margins_mm(&bleed), Margins::ZERO); + assert_eq!(PlotDevice::Pdf.margins_mm(&expand), Margins::uniform(2.0)); + assert_eq!(PlotDevice::None.margins_mm(&a4), Margins::ZERO); + } + + #[test] + fn ipp_media_parses_sizes_margins_borderless_and_default() { + let caps = parse_ipp_media(IPP_SAMPLE).expect("media parsed"); + assert_eq!( + caps.media.len(), + 3, + "borderless A4 folds into the bordered entry" + ); + let a4 = &caps.media[0]; + assert_eq!(a4.paper.canonical, "ISO_A4_(210.00_x_297.00_MM)"); + assert_eq!(a4.margins, Margins::uniform(3.0)); + assert!(a4.borderless); + let letter = &caps.media[1]; + assert_eq!(letter.paper.canonical, "ANSI_A_(8.50_x_11.00_Inches)"); + assert_eq!(letter.margins, Margins::uniform(6.35)); + assert!(!letter.borderless); + let custom = &caps.media[2]; + assert_eq!( + custom.paper.canonical, + "UserDefinedMetric_(215.00_x_345.00_MM)" + ); + assert_eq!( + caps.default_paper.as_ref().map(|p| p.canonical.as_ref()), + Some("ISO_A4_(210.00_x_297.00_MM)") + ); + // Margin lookup by sheet size, either orientation, tolerant of the + // catalogue's inch → mm rounding. + let ansi_a = paper_catalog::resolve("Letter_(8.50_x_11.00_Inches)").unwrap(); + assert_eq!(caps.margins_for(&ansi_a), Some(Margins::uniform(6.35))); + let a3 = paper_catalog::resolve("ISO_A3_(297.00_x_420.00_MM)").unwrap(); + assert_eq!(caps.margins_for(&a3), None); + } + + #[test] + fn ipp_output_without_media_is_rejected() { + assert!(parse_ipp_media("printer-state (enum) = 3\n").is_none()); + assert!(parse_ipp_media( + "media-col-database (1setOf collection) = {media-size={x-dimension=0 y-dimension=0}}\n" + ) + .is_none()); + } + + #[test] + fn pwg_names_carry_their_size() { + assert_eq!(pwg_size_mm("iso_a4_210x297mm"), Some((210.0, 297.0))); + let (w, h) = pwg_size_mm("na_letter_8.5x11in").unwrap(); + assert!((w - 215.9).abs() < 1e-9 && (h - 279.4).abs() < 1e-9); + assert_eq!( + pwg_size_mm("custom_215x345mm_215x345mm"), + Some((215.0, 345.0)) + ); + assert_eq!(pwg_size_mm("iso_a4"), None); + } +} diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index ee157405b..4ae3bd537 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -1456,6 +1456,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Layout Print Configuration" => Some(("properties", "layout-print-configuration")), "Layout name cannot be empty." => Some(("common", "layout-name-cannot-be-empty")), "Layout plots the current sheet using the selected paper size." => Some(("plot", "layout-plots-the-current-sheet-using-the-selec")), + "Sheet sizes and printable areas reported by the printer." => Some(("plot", "sheet-sizes-and-printable-areas-reported-by-th")), "Layout renamed: '{old_name}' → '{new_name}'" => Some(("common", "layout-renamed-old-name-new-name")), "Layouts" => Some(("layout", "layouts")), "Leader" => Some(("common", "leader")), diff --git a/src/ui/window/plot.rs b/src/ui/window/plot.rs index 45f992c30..ee6064326 100644 --- a/src/ui/window/plot.rs +++ b/src/ui/window/plot.rs @@ -6,6 +6,7 @@ use crate::app::Message; use crate::io::paper_catalog::{self, PaperSize}; +use crate::io::plot_device::PrinterCapabilities; use iced::widget::{ button, checkbox, column, container, mouse_area, row, scrollable, text, text_input, Space, @@ -113,6 +114,10 @@ pub enum PlotDlgMsg { Preview, PrinterProperties, Printer(String), + /// A printer answered (or failed) the media query started when it was + /// selected; carries the printer's name so a stale answer for a printer + /// the user has since left is ignored. + PrinterMedia(String, Option>), Paper(String), Orientation(String), Area(String), @@ -161,6 +166,13 @@ pub struct PlotDialogState { /// sentinels. #[serde(skip)] pub printers: Vec, + /// Name of the system default printer, shown next to the default entry. + #[serde(skip)] + pub default_printer: Option, + /// Sheets and printable areas the selected printer reported; `None` + /// while unknown, when the platform cannot ask, or for PDF output. + #[serde(skip)] + pub printer_media: Option>, /// Chosen printer name, or `None` for the system default. pub printer: Option, /// Output goes to a PDF file instead of a printer. @@ -224,6 +236,8 @@ impl Default for PlotDialogState { fn default() -> Self { Self { printers: Vec::new(), + default_printer: None, + printer_media: None, printer: None, to_file: false, paper: paper_catalog::default_paper().canonical.to_string(), @@ -662,7 +676,17 @@ pub fn view_window( .spacing(4); // ── Printer / plotter ───────────────────────────────────────────────── - let mut printer_opts = vec![PlotChoice::localized(OUT_DEFAULT)]; + // The default entry names the printer it resolves to, when known, so the + // user sees where a plot will go without leaving the dialog. + let default_entry = match &s.default_printer { + Some(name) => PlotChoice { + raw: OUT_DEFAULT.to_string(), + localized: true, + display: Some(format!("{} ({name})", crate::i18n::translate(OUT_DEFAULT))), + }, + None => PlotChoice::localized(OUT_DEFAULT), + }; + let mut printer_opts = vec![default_entry.clone()]; printer_opts.extend(s.printers.iter().cloned().map(PlotChoice::raw)); printer_opts.push(PlotChoice::localized(OUT_PDF)); let printer_sel = if s.to_file { @@ -670,21 +694,33 @@ pub fn view_window( } else { Some(match &s.printer { Some(printer) => PlotChoice::raw(printer.clone()), - None => PlotChoice::localized(OUT_DEFAULT), + None => default_entry, }) }; - // The picker lists the catalogue by series; a sheet that only exists in - // the drawing (a driver's custom size, an old config's bare name) is added - // at the top so the current selection is always visible and re-selectable. + // A printer that reported its media lists those sheets (with the + // printable areas it will actually honour); PDF output and printers that + // could not be asked list the catalogue by series. A sheet that only + // exists in the drawing (a driver's custom size, an old config's bare + // name) is added at the top so the current selection stays selectable. let selected_paper = paper_catalog::from_drawing(&s.paper, s.paper_width_mm, s.paper_height_mm); - let mut paper_opts: Vec = paper_catalog::catalog() - .iter() - .map(PlotChoice::paper) - .collect(); + let printer_sheets = (!s.to_file).then_some(()).and(s.printer_media.as_deref()); + let mut paper_opts: Vec = match printer_sheets { + Some(caps) => caps.media.iter().map(|media| PlotChoice::paper(&media.paper)).collect(), + None => paper_catalog::catalog().iter().map(PlotChoice::paper).collect(), + }; let paper_sel = PlotChoice::paper(&selected_paper); if !paper_opts.contains(&paper_sel) { paper_opts.insert(0, paper_sel.clone()); } + let paper_source_note: Element<'_, Message> = if printer_sheets.is_some() { + text(t!("Sheet sizes and printable areas reported by the printer.")) + .size(10) + .style(muted_style) + .width(width) + .into() + } else { + Space::new().height(0).into() + }; let paper_note: Element<'_, Message> = if s.area == "Layout" { text(t!("Layout plots the current sheet using the selected paper size.")) .size(10) @@ -735,6 +771,7 @@ pub fn view_window( let paper_panel = panel(column![ section_label(t!("Paper")), drop_row(t!("Size"), paper_opts, Some(paper_sel), PlotDlgMsg::Paper, width), + paper_source_note, paper_note, ].spacing(7));