forked from 1b-agent/opencadstudio-upstream
feat(plot): plot device model with AutoCAD names and printable areas
A page setup names its plotter, and AutoCAD looks that name up in its own
configurations: `DWG To PDF.pc3` for PDF output, the spooler name for a
system printer, `None` when nothing is assigned. Drawings saved here used
to carry the dialog's label ("Save to PDF file…"), so AutoCAD reported a
missing device on every layout plotted to PDF from Open CAD Studio.
- New `io::plot_device`: `PlotDevice { None, Pdf, Printer }` with AutoCAD's
spellings, a reader for stored names that keeps any plotter
configuration AutoCAD wrote (an HP `.pc3`, `DWF6 ePlot.pc3` — plotted to
PDF here but preserved for AutoCAD) and upgrades the legacy label.
- Printable areas: the PDF driver's margins per sheet variant (full bleed
edge to edge; standard / expand insets provisional until read off
AutoCAD's plotter configuration), and for CUPS printers the media and
margins the queue reports over IPP (`ipptool` `media-col-database`),
parsed once per printer and cached. Windows enumeration follows with
native GDI printing.
- Page setups now store the device in AutoCAD's spelling and the margins
of the chosen device/sheet, keeping the drawing's own values while
neither changed (AutoCAD's `PAPERUPDATE` behaviour). The paper-space
printable-area rectangle follows.
- Plot dialog: the default entry names the printer it resolves to; a
printer that answered lists its own sheets with a note; the media query
runs in the background and answers for a since-deselected printer are
dropped.
Tests: device names, stored-name resolution, PDF margin table, IPP media
parsing (sizes, margins, borderless folding, default sheet), PWG names,
plus dialog ↔ layout round-trips for PDF, legacy label, preserved plotter
configuration, sheet change → margins, and printer selection.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
567eb90925
commit
c03a40cb33
26 files changed
+760
-38
No files matched your search
@@ -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 = الاتجاه
|
||||
|
||||
@@ -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 = Ориентация
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = Προσανατολισμός
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = अभिविन्यास
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = 方向
|
||||
|
||||
@@ -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 = 오리엔테이션
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = Ориентация
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = 方向
|
||||
|
||||
@@ -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 = 方向
|
||||
|
||||
+209
-29
@@ -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<Message> {
|
||||
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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<PrinterMedia>,
|
||||
/// The printer's default sheet, when reported.
|
||||
pub default_paper: Option<PaperSize>,
|
||||
}
|
||||
|
||||
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<Margins> {
|
||||
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<HashMap<String, Arc<PrinterCapabilities>>> {
|
||||
static CACHE: OnceLock<Mutex<HashMap<String, Arc<PrinterCapabilities>>>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
/// The cached capabilities of `printer`, if they were queried already.
|
||||
pub fn cached_printer_capabilities(printer: &str) -> Option<Arc<PrinterCapabilities>> {
|
||||
capability_cache().lock().ok()?.get(printer).cloned()
|
||||
}
|
||||
|
||||
/// 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<Arc<PrinterCapabilities>> {
|
||||
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<String> {
|
||||
// `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<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
|
||||
fn query_printer_capabilities(printer: &str) -> Option<PrinterCapabilities> {
|
||||
// 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<PrinterCapabilities> {
|
||||
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<PrinterCapabilities> {
|
||||
let database = attribute_value(text, "media-col-database")?;
|
||||
let mut media: Vec<PrinterMedia> = 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<f64> {
|
||||
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::<f64>().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);
|
||||
}
|
||||
}
|
||||
@@ -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")),
|
||||
|
||||
+46
-9
@@ -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<std::sync::Arc<PrinterCapabilities>>),
|
||||
Paper(String),
|
||||
Orientation(String),
|
||||
Area(String),
|
||||
@@ -161,6 +166,13 @@ pub struct PlotDialogState {
|
||||
/// sentinels.
|
||||
#[serde(skip)]
|
||||
pub printers: Vec<String>,
|
||||
/// Name of the system default printer, shown next to the default entry.
|
||||
#[serde(skip)]
|
||||
pub default_printer: Option<String>,
|
||||
/// 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<std::sync::Arc<PrinterCapabilities>>,
|
||||
/// Chosen printer name, or `None` for the system default.
|
||||
pub printer: Option<String>,
|
||||
/// 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<PlotChoice> = 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<PlotChoice> = 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));
|
||||
|
||||
|
||||
Reference in new issue
Block a user