forked from 1b-agent/opencadstudio-upstream
Merge pull request #1360 from HakanSeven12/plot/b4-ctb-leniency
fix(plot-style): load plot style tables written by other applications # Conflicts: # src/app/update/file.rs # src/ui/window/plot.rs
This commit is contained in:
commit
f20c823ffe
28 files changed
+339
-35
No files matched your search
@@ -5100,6 +5100,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = جدول نمط الطباعة (تعيينات الأقلام)
|
||||
.plot-transparency = شفافية الطباعة
|
||||
.plot-upside-down = الطباعة مقلوبة
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = جداول أنماط الطباعة المسماة (.stb) غير مدعومة بعد: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = لم يتم العثور على __ocs_fmt_0__ في مجلد أنماط الطباعة (__ocs_fmt_1__).
|
||||
.preview = معاينة
|
||||
.printer-plotter = الطابعة / الراسم
|
||||
.properties = الخصائص…
|
||||
|
||||
@@ -5088,6 +5088,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Таблица със стилове за плотиране (присвоявания на пера)
|
||||
.plot-transparency = Прозрачност при плотиране
|
||||
.plot-upside-down = Плотирай обърнато наопаки
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = Именуваните таблици със стилове на печат (.stb) все още не се поддържат: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = __ocs_fmt_0__ не беше намерен в папката със стилове на печат (__ocs_fmt_1__).
|
||||
.preview = Преглед
|
||||
.printer-plotter = Принтер / плотер
|
||||
.properties = Свойства…
|
||||
|
||||
@@ -5085,6 +5085,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Tabulka stylu vykreslování (přiřazení pera)
|
||||
.plot-transparency =Průhlednost plotru
|
||||
.plot-upside-down = Zápletka vzhůru nohama
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = Pojmenované tabulky stylů vykreslování (.stb) zatím nejsou podporovány: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = __ocs_fmt_0__ nebyl nalezen ve složce stylů vykreslování (__ocs_fmt_1__).
|
||||
.preview = Náhled
|
||||
.printer-plotter = Tiskárna / plotr
|
||||
.properties = Vlastnosti…
|
||||
|
||||
@@ -5086,6 +5086,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Plot-Tabelle (Stiftzuweisungen)
|
||||
.plot-transparency = Transparenz der Parzellen
|
||||
.plot-upside-down = Plot auf dem Kopf
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = Benannte Plotstiltabellen (.stb) werden noch nicht unterstützt: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = __ocs_fmt_0__ wurde im Plotstil-Ordner (__ocs_fmt_1__) nicht gefunden.
|
||||
.preview = Vorschau
|
||||
.printer-plotter = Drucker/Plotter
|
||||
.properties = Eigenschaften...
|
||||
|
||||
@@ -5088,6 +5088,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Πίνακας στυλ εκτύπωσης (αντιστοιχίσεις πενών)
|
||||
.plot-transparency = Εκτύπωση διαφάνειας
|
||||
.plot-upside-down = Εκτύπωση ανάποδα
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = Οι επώνυμοι πίνακες στυλ εκτύπωσης (.stb) δεν υποστηρίζονται ακόμα: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = Το __ocs_fmt_0__ δεν βρέθηκε στον φάκελο των στυλ εκτύπωσης (__ocs_fmt_1__).
|
||||
.preview = Προεπισκόπηση
|
||||
.printer-plotter = Εκτυπωτής / σχεδιογράφος
|
||||
.properties = Ιδιότητες…
|
||||
|
||||
@@ -5089,6 +5089,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Plot style table (pen assignments)
|
||||
.plot-transparency = Plot transparency
|
||||
.plot-upside-down = Plot upside-down
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = Named plot style tables (.stb) are not supported yet: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = __ocs_fmt_0__ was not found in the plot styles folder (__ocs_fmt_1__).
|
||||
.preview = Preview
|
||||
.printer-plotter = Printer / plotter
|
||||
.properties = Properties…
|
||||
|
||||
@@ -5088,6 +5088,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Tabla de estilos de trazado (asignaciones de plumillas)
|
||||
.plot-transparency = Transparencia de trazado
|
||||
.plot-upside-down = Trazar cabeza abajo
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = Las tablas de estilos de trazado con nombre (.stb) aún no se admiten: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = No se encontró __ocs_fmt_0__ en la carpeta de estilos de trazado (__ocs_fmt_1__).
|
||||
.preview = Vista preliminar
|
||||
.printer-plotter = Impresora / trazador
|
||||
.properties = Propiedades…
|
||||
|
||||
@@ -5087,6 +5087,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Piirustyylitaulukko (kynätehtävät)
|
||||
.plot-transparency =Juonen läpinäkyvyys
|
||||
.plot-upside-down = Juoni ylösalaisin
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = Nimettyjä tulostustyylitaulukoita (.stb) ei vielä tueta: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = Tiedostoa __ocs_fmt_0__ ei löytynyt tulostustyylien kansiosta (__ocs_fmt_1__).
|
||||
.preview = Esikatselu
|
||||
.printer-plotter = Tulostin/plotteri
|
||||
.properties = Ominaisuudet…
|
||||
|
||||
@@ -5088,6 +5088,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Table des styles de tracé (affectation des plumes)
|
||||
.plot-transparency = Transparence du lot
|
||||
.plot-upside-down = Placer vers l'envers
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = Les tables de styles de tracé nommés (.stb) ne sont pas encore prises en charge : __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = __ocs_fmt_0__ est introuvable dans le dossier des styles de tracé (__ocs_fmt_1__).
|
||||
.preview = Prévisualisation
|
||||
.printer-plotter = Imprimante / traceur
|
||||
.properties = Propriétés...
|
||||
|
||||
@@ -5076,6 +5076,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = प्लॉट शैली तालिका (पेन असाइनमेंट)
|
||||
.plot-transparency = प्लॉट पारदर्शिता
|
||||
.plot-upside-down = ऊपर की ओर नीचे प्लॉट
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = नामित प्लॉट स्टाइल तालिकाएँ (.stb) अभी समर्थित नहीं हैं: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = __ocs_fmt_0__ प्लॉट स्टाइल फ़ोल्डर (__ocs_fmt_1__) में नहीं मिला।
|
||||
.preview = पूर्वावलोकन
|
||||
.printer-plotter = प्रिंटर / प्लॉटर
|
||||
.properties = गुण...
|
||||
|
||||
@@ -5087,6 +5087,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Nyomtatási stílus táblázat (toll-hozzárendelések)
|
||||
.plot-transparency =A telek átláthatósága
|
||||
.plot-upside-down = Telek fejjel lefelé
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = A nevesített nyomtatási stílustáblák (.stb) még nem támogatottak: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = A(z) __ocs_fmt_0__ nem található a nyomtatási stílusok mappájában (__ocs_fmt_1__).
|
||||
.preview = Előnézet
|
||||
.printer-plotter = Nyomtató / plotter
|
||||
.properties = Tulajdonságok…
|
||||
|
||||
@@ -5087,6 +5087,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Tabella stili di stampa (assegnazioni penne)
|
||||
.plot-transparency =Trasparenza della trama
|
||||
.plot-upside-down = Trama sottosopra
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = Le tabelle di stili di stampa con nome (.stb) non sono ancora supportate: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = __ocs_fmt_0__ non è stato trovato nella cartella degli stili di stampa (__ocs_fmt_1__).
|
||||
.preview = Anteprima
|
||||
.printer-plotter = Stampante/plotter
|
||||
.properties = Proprietà…
|
||||
|
||||
@@ -5076,6 +5076,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = 印刷スタイルテーブル(ペン割当)
|
||||
.plot-transparency = 透明度を印刷
|
||||
.plot-upside-down = 上下反転印刷
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = 名前付き印刷スタイルテーブル (.stb) はまだサポートされていません: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = __ocs_fmt_0__ は印刷スタイルフォルダー (__ocs_fmt_1__) に見つかりませんでした。
|
||||
.preview = プレビュー
|
||||
.printer-plotter = プリンター / プロッター
|
||||
.properties = プロパティ…
|
||||
|
||||
@@ -5087,6 +5087,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = 플롯 스타일 테이블(펜 지정)
|
||||
.plot-transparency =플롯 투명도
|
||||
.plot-upside-down = 거꾸로 플롯
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = 명명된 플롯 스타일 테이블(.stb)은 아직 지원되지 않습니다: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = __ocs_fmt_0__을(를) 플롯 스타일 폴더(__ocs_fmt_1__)에서 찾을 수 없습니다.
|
||||
.preview = 미리보기
|
||||
.printer-plotter = 프린터/플로터
|
||||
.properties = 속성…
|
||||
|
||||
@@ -5086,6 +5086,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Plotstijltabel (pentoewijzingen)
|
||||
.plot-transparency = Doorzichtigheid van de plaats van uitvoering
|
||||
.plot-upside-down = Ondersteboven plotten
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = Benoemde plotstijltabellen (.stb) worden nog niet ondersteund: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = __ocs_fmt_0__ is niet gevonden in de map met plotstijlen (__ocs_fmt_1__).
|
||||
.preview = Voorbeeld
|
||||
.printer-plotter = Printer/plotter
|
||||
.properties = Eigenschappen...
|
||||
|
||||
@@ -5087,6 +5087,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Tabela stylów wydruku (przypisania pisaków)
|
||||
.plot-transparency =Przejrzystość fabuły
|
||||
.plot-upside-down = Fabuła do góry nogami
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = Nazwane tabele stylów wydruku (.stb) nie są jeszcze obsługiwane: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = Nie znaleziono __ocs_fmt_0__ w folderze stylów wydruku (__ocs_fmt_1__).
|
||||
.preview = Podgląd
|
||||
.printer-plotter = Drukarka / ploter
|
||||
.properties = Właściwości…
|
||||
|
||||
@@ -5088,6 +5088,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Tabela de estilos de plotagem (atribuições de penas)
|
||||
.plot-transparency = Transparência de plotagem
|
||||
.plot-upside-down = Plotar de cabeça para baixo
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = Tabelas de estilos de plotagem nomeados (.stb) ainda não são suportadas: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = __ocs_fmt_0__ não foi encontrado na pasta de estilos de plotagem (__ocs_fmt_1__).
|
||||
.preview = Visualização
|
||||
.printer-plotter = Impressora / plotadora
|
||||
.properties = Propriedades…
|
||||
|
||||
@@ -5089,6 +5089,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Стол стиля сюжета (пен-задания)
|
||||
.plot-transparency = Прозрачность сюжета
|
||||
.plot-upside-down = Заговор вверх ногами
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = Именованные таблицы стилей печати (.stb) пока не поддерживаются: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = __ocs_fmt_0__ не найден в папке стилей печати (__ocs_fmt_1__).
|
||||
.preview = Предпросмотр
|
||||
.printer-plotter = Принтер / Плоттер
|
||||
.properties = Свойства...
|
||||
|
||||
@@ -5028,6 +5028,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = Çizim stili tablosu (kalem atamaları)
|
||||
.plot-transparency = Saydamlığı yazdır
|
||||
.plot-upside-down = Ters yazdır
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = Adlandırılmış çizim stili tabloları (.stb) henüz desteklenmiyor: __ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = __ocs_fmt_0__, çizim stilleri klasöründe (__ocs_fmt_1__) bulunamadı.
|
||||
.preview = Önizleme
|
||||
.printer-plotter = Yazıcı / çizici
|
||||
.properties = Özellikler…
|
||||
|
||||
@@ -5071,6 +5071,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = 打印样式表(笔指定)
|
||||
.plot-transparency = 打印透明度
|
||||
.plot-upside-down = 上下颠倒打印
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = 尚不支持命名打印样式表 (.stb):__ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = 在打印样式文件夹 (__ocs_fmt_1__) 中找不到 __ocs_fmt_0__。
|
||||
.preview = 预览
|
||||
.printer-plotter = 打印机/绘图仪
|
||||
.properties = 特性…
|
||||
|
||||
@@ -5083,6 +5083,8 @@ plot =
|
||||
.plot-style-table-pen-assignments = 列印樣式表(筆指定)
|
||||
.plot-transparency = 列印透明度
|
||||
.plot-upside-down = 上下顛倒列印
|
||||
.named-plot-style-tables-stb-are-not-supported-yet-name = 尚不支援具名出圖型式表 (.stb):__ocs_fmt_0__
|
||||
.name-was-not-found-in-the-plot-styles-folder-folder = 在出圖型式資料夾 (__ocs_fmt_1__) 中找不到 __ocs_fmt_0__。
|
||||
.preview = 預覽
|
||||
.printer-plotter = 印表機/繪圖儀
|
||||
.properties = 特性…
|
||||
|
||||
+3
-1
@@ -3474,7 +3474,9 @@ pub enum Message {
|
||||
/// Open file dialog to load a CTB/STB plot style table.
|
||||
PlotStyleLoad,
|
||||
/// Callback when the user picks (or cancels) a CTB/STB file.
|
||||
PlotStyleLoaded(Option<crate::io::plot_style::PlotStyleTable>),
|
||||
/// The Load… picker finished: a table, nothing (cancelled), or why the
|
||||
/// file could not be read.
|
||||
PlotStyleLoaded(Result<Option<crate::io::plot_style::PlotStyleTable>, String>),
|
||||
/// Clear the active plot style table.
|
||||
PlotStyleClear,
|
||||
/// Open/close the Plot Style panel.
|
||||
|
||||
+23
-4
@@ -4670,18 +4670,22 @@ impl OpenCADStudio {
|
||||
self.plot_dialog.apply_plot_styles = false;
|
||||
self.plot_dialog.show_plot_styles = false;
|
||||
self.plot_dialog.style_missing = false;
|
||||
self.plot_dialog.style_error = None;
|
||||
} else {
|
||||
match crate::io::plot_style::PlotStyleTable::load_named(&name) {
|
||||
Ok(table) => {
|
||||
self.plot_dialog.style_name = table.name.clone();
|
||||
self.plot_dialog.apply_plot_styles = true;
|
||||
self.plot_dialog.style_missing = false;
|
||||
self.plot_dialog.style_error = None;
|
||||
self.report_plot_style_warnings(&table);
|
||||
self.active_plot_style = Some(table);
|
||||
}
|
||||
Err(error) => {
|
||||
self.plot_dialog.style_name = name;
|
||||
self.plot_dialog.style_missing = true;
|
||||
self.command_line.push_error(&error);
|
||||
self.plot_dialog.style_error = Some(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4950,16 +4954,19 @@ impl OpenCADStudio {
|
||||
));
|
||||
self.plot_dialog.window = self.plot_window;
|
||||
}
|
||||
let mut style_error = None;
|
||||
if !ps.current_style_sheet.is_empty()
|
||||
&& self
|
||||
.active_plot_style
|
||||
.as_ref()
|
||||
.is_none_or(|table| !table.name.eq_ignore_ascii_case(&ps.current_style_sheet))
|
||||
{
|
||||
if let Ok(table) =
|
||||
crate::io::plot_style::PlotStyleTable::load_named(&ps.current_style_sheet)
|
||||
{
|
||||
self.active_plot_style = Some(table);
|
||||
match crate::io::plot_style::PlotStyleTable::load_named(&ps.current_style_sheet) {
|
||||
Ok(table) => {
|
||||
self.report_plot_style_warnings(&table);
|
||||
self.active_plot_style = Some(table);
|
||||
}
|
||||
Err(error) => style_error = Some(error),
|
||||
}
|
||||
}
|
||||
let active_style_name = self
|
||||
@@ -5113,6 +5120,18 @@ impl OpenCADStudio {
|
||||
d.show_plot_styles = ps.flags.show_plot_styles;
|
||||
d.style_missing = !d.style_name.is_empty() && !style_loaded;
|
||||
self.refresh_custom_scale_fields();
|
||||
d.style_error = if d.style_missing { style_error } else { None };
|
||||
}
|
||||
|
||||
/// Tell the user what a plot style table's loader had to tolerate.
|
||||
pub(in crate::app) fn report_plot_style_warnings(
|
||||
&mut self,
|
||||
table: &crate::io::plot_style::PlotStyleTable,
|
||||
) {
|
||||
for warning in &table.load_warnings {
|
||||
let name = &table.name;
|
||||
self.command_line.push_warning(&format!("{name}: {warning}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy transient paper/scale choices out of the dialog without changing
|
||||
|
||||
+15
-2
@@ -8946,7 +8946,7 @@ impl OpenCADStudio {
|
||||
Message::PlotStyleLoad => {
|
||||
Task::perform(crate::io::pick_plot_style(), Message::PlotStyleLoaded)
|
||||
}
|
||||
Message::PlotStyleLoaded(Some(table)) => {
|
||||
Message::PlotStyleLoaded(Ok(Some(table))) => {
|
||||
if table.is_stb {
|
||||
self.command_line.push_error(
|
||||
crate::t!(
|
||||
@@ -8958,6 +8958,8 @@ impl OpenCADStudio {
|
||||
}
|
||||
self.plot_dialog.style_name = table.name.clone();
|
||||
self.plot_dialog.style_missing = false;
|
||||
self.plot_dialog.style_error = None;
|
||||
self.report_plot_style_warnings(&table);
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"Plot style '{}' loaded ({} color entries).",
|
||||
@@ -8974,7 +8976,15 @@ impl OpenCADStudio {
|
||||
self.plot_dialog.plot_styles = crate::io::plot_style::available_ctb_names();
|
||||
Task::none()
|
||||
}
|
||||
Message::PlotStyleLoaded(None) => Task::none(),
|
||||
Message::PlotStyleLoaded(Ok(None)) => Task::none(),
|
||||
Message::PlotStyleLoaded(Err(error)) => {
|
||||
// The file the user pointed at could not be read: say why,
|
||||
// in the dialog as well as on the command line.
|
||||
self.command_line.push_error(&error);
|
||||
self.plot_dialog.style_missing = true;
|
||||
self.plot_dialog.style_error = Some(error);
|
||||
Task::none()
|
||||
}
|
||||
Message::PlotStyleClear => {
|
||||
self.active_plot_style = None;
|
||||
self.plot_dialog.style_name.clear();
|
||||
@@ -9001,12 +9011,15 @@ impl OpenCADStudio {
|
||||
if needs_load {
|
||||
match crate::io::plot_style::PlotStyleTable::load_named(&selected_style) {
|
||||
Ok(table) => {
|
||||
self.report_plot_style_warnings(&table);
|
||||
self.active_plot_style = Some(table);
|
||||
self.plot_dialog.style_missing = false;
|
||||
self.plot_dialog.style_error = None;
|
||||
}
|
||||
Err(error) => {
|
||||
self.plot_dialog.style_missing = true;
|
||||
self.command_line.push_error(&error);
|
||||
self.plot_dialog.style_error = Some(error);
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
|
||||
+26
-3
@@ -1536,7 +1536,12 @@ mod save_failure_tests {
|
||||
// ── Plot Style Table ──────────────────────────────────────────────────────
|
||||
|
||||
/// Show a file-open dialog and load the selected CTB or STB file.
|
||||
pub async fn pick_plot_style() -> Option<plot_style::PlotStyleTable> {
|
||||
/// Pick a plot style table file and load it. `Ok(None)` when the picker was
|
||||
/// cancelled; `Err` says why the chosen file could not be read. A table
|
||||
/// picked from outside the plot styles folder is copied into it (unless a
|
||||
/// file of that name is already there), so the dialog lists it from now on
|
||||
/// and page setups naming it resolve after a restart.
|
||||
pub async fn pick_plot_style() -> Result<Option<plot_style::PlotStyleTable>, String> {
|
||||
let dialog = crate::sys::file_dialog()
|
||||
.set_title(crate::t!("Load Plot Style Table").as_ref())
|
||||
.add_filter(crate::t!("Plot Style Tables").as_ref(), &["ctb", "CTB"])
|
||||
@@ -1547,8 +1552,26 @@ pub async fn pick_plot_style() -> Option<plot_style::PlotStyleTable> {
|
||||
Ok(dir) => dialog.set_directory(dir),
|
||||
Err(_) => dialog,
|
||||
};
|
||||
let handle = dialog.pick_file().await?;
|
||||
plot_style::PlotStyleTable::load(&crate::sys::handle_path(&handle)).ok()
|
||||
let Some(handle) = dialog.pick_file().await else {
|
||||
return Ok(None);
|
||||
};
|
||||
let path = crate::sys::handle_path(&handle);
|
||||
let table = plot_style::PlotStyleTable::load(&path)?;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let (Ok(dir), Some(file_name)) = (plot_style::ensure_plot_styles_dir(), path.file_name()) {
|
||||
let destination = dir.join(file_name);
|
||||
let same_place = path.parent().is_some_and(|parent| {
|
||||
parent
|
||||
.canonicalize()
|
||||
.ok()
|
||||
.zip(dir.canonicalize().ok())
|
||||
.is_some_and(|(a, b)| a == b)
|
||||
});
|
||||
if !same_place && !destination.exists() {
|
||||
let _ = std::fs::copy(&path, &destination);
|
||||
}
|
||||
}
|
||||
Ok(Some(table))
|
||||
}
|
||||
|
||||
// ── Image file picker ─────────────────────────────────────────────────────
|
||||
|
||||
+212
-25
@@ -175,6 +175,10 @@ pub struct PlotStyleTable {
|
||||
pub aci_entries: Vec<PlotStyleEntry>, // 256 entries, index = ACI
|
||||
/// For STB: named style entries.
|
||||
pub named_entries: HashMap<String, PlotStyleEntry>,
|
||||
/// What had to be tolerated to read the file (a bad checksum, a length
|
||||
/// that did not add up, descriptions in a Windows code page): the file
|
||||
/// loaded, but the user deserves to know it was not quite as written.
|
||||
pub load_warnings: Vec<String>,
|
||||
}
|
||||
|
||||
impl PlotStyleTable {
|
||||
@@ -190,6 +194,7 @@ impl PlotStyleTable {
|
||||
lineweights: LW_TABLE.to_vec(),
|
||||
aci_entries: (0..=255).map(|_| PlotStyleEntry::default()).collect(),
|
||||
named_entries: HashMap::default(),
|
||||
load_warnings: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,8 +212,10 @@ impl PlotStyleTable {
|
||||
pub fn from_bytes(name: impl Into<String>, raw: &[u8]) -> Result<Self, String> {
|
||||
let name = name.into();
|
||||
let is_stb = name.to_ascii_lowercase().ends_with(".stb");
|
||||
let text = decompress_ctb(raw)?;
|
||||
parse_plot_style_text(&text, name, is_stb)
|
||||
let (text, warnings) = decompress_ctb(raw)?;
|
||||
let mut table = parse_plot_style_text(&text, name, is_stb)?;
|
||||
table.load_warnings = warnings;
|
||||
Ok(table)
|
||||
}
|
||||
|
||||
pub fn builtin(name: &str) -> Result<Self, String> {
|
||||
@@ -264,12 +271,18 @@ impl PlotStyleTable {
|
||||
let path = Path::new(name);
|
||||
if path.components().count() != 1
|
||||
|| !matches!(path.components().next(), Some(Component::Normal(_)))
|
||||
|| !path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("ctb"))
|
||||
{
|
||||
return Err(format!("Invalid plot style name: {name}"));
|
||||
}
|
||||
if path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("stb")) {
|
||||
return Err(crate::tf!(
|
||||
"Named plot style tables (.stb) are not supported yet: {name}"
|
||||
)
|
||||
.into_owned());
|
||||
}
|
||||
if !path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("ctb")) {
|
||||
return Err(format!("Invalid plot style name: {name}"));
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
@@ -286,7 +299,12 @@ impl PlotStyleTable {
|
||||
.map(|entry| entry.path());
|
||||
return match matched {
|
||||
Some(path) => Self::load(&path),
|
||||
None => Self::builtin(name),
|
||||
None => Self::builtin(name).map_err(|_| {
|
||||
// Not a built-in either: say where a copy would be found.
|
||||
let folder = dir.display();
|
||||
crate::tf!("{name} was not found in the plot styles folder ({folder}).")
|
||||
.into_owned()
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -424,10 +442,18 @@ impl PlotStyleTable {
|
||||
|
||||
// ── Deflate helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Decompress a CTB/STB file's raw bytes into the text content.
|
||||
/// Decompress a CTB/STB file's raw bytes into the text content, with the
|
||||
/// things that had to be tolerated on the way.
|
||||
///
|
||||
fn decompress_ctb(data: &[u8]) -> Result<String, String> {
|
||||
/// Files written by other applications are read as they are, not as the
|
||||
/// container says they should be: a checksum or a length that does not add
|
||||
/// up is reported and read past (the text is either there or the zlib
|
||||
/// stream fails on its own), a zlib stream with a bad trailer is retried as
|
||||
/// raw deflate, and text that is not UTF-8 is read as Windows-1252 — the
|
||||
/// code page those applications write descriptions in.
|
||||
fn decompress_ctb(data: &[u8]) -> Result<(String, Vec<String>), String> {
|
||||
const PREFIX: &[u8] = b"PIAFILEVERSION_2.0,CTBVER1,compress\r\npmzlibcodec";
|
||||
let mut warnings = Vec::new();
|
||||
let mut decoded = Vec::new();
|
||||
if data.starts_with(PREFIX) {
|
||||
if data.len() < 60 {
|
||||
@@ -436,20 +462,34 @@ fn decompress_ctb(data: &[u8]) -> Result<String, String> {
|
||||
let checksum = u32::from_le_bytes(data[48..52].try_into().unwrap());
|
||||
let text_len = u32::from_le_bytes(data[52..56].try_into().unwrap()) as usize;
|
||||
let compressed_len = u32::from_le_bytes(data[56..60].try_into().unwrap()) as usize;
|
||||
if compressed_len > data.len() - 60 {
|
||||
return Err("CTB compressed payload is truncated".into());
|
||||
}
|
||||
let payload = &data[60..60 + compressed_len];
|
||||
let available = data.len() - 60;
|
||||
let payload = if compressed_len > available {
|
||||
warnings.push(format!(
|
||||
"the header announces {compressed_len} compressed bytes \
|
||||
but the file holds {available}"
|
||||
));
|
||||
&data[60..]
|
||||
} else {
|
||||
&data[60..60 + compressed_len]
|
||||
};
|
||||
if adler32(payload) != checksum {
|
||||
return Err("CTB compressed payload checksum mismatch".into());
|
||||
warnings.push("the compressed payload's checksum does not match".into());
|
||||
}
|
||||
use flate2::read::ZlibDecoder;
|
||||
ZlibDecoder::new(payload)
|
||||
.read_to_end(&mut decoded)
|
||||
.map_err(|e| format!("CTB zlib decompress: {e}"))?;
|
||||
if decoded.len() != text_len {
|
||||
decoded = inflate_lenient(payload, &mut warnings)?;
|
||||
// A byte or a few short is a header that counted its trailing NUL
|
||||
// differently; far short of the announced text is a cut-off file,
|
||||
// which the inflater does not always notice on its own.
|
||||
const TOLERATED_SHORTFALL: usize = 16;
|
||||
if decoded.len() + TOLERATED_SHORTFALL < text_len {
|
||||
return Err(format!(
|
||||
"CTB content length mismatch: expected {text_len}, got {}",
|
||||
"CTB text is incomplete: the header announces {text_len} bytes \
|
||||
but only {} could be read",
|
||||
decoded.len()
|
||||
));
|
||||
}
|
||||
if decoded.len() != text_len {
|
||||
warnings.push(format!(
|
||||
"the header announces {text_len} bytes of text but {} were read",
|
||||
decoded.len()
|
||||
));
|
||||
}
|
||||
@@ -461,10 +501,7 @@ fn decompress_ctb(data: &[u8]) -> Result<String, String> {
|
||||
.unwrap_or(0);
|
||||
let payload = &data[split_at..];
|
||||
if payload.starts_with(&[0x78]) {
|
||||
use flate2::read::ZlibDecoder;
|
||||
ZlibDecoder::new(payload)
|
||||
.read_to_end(&mut decoded)
|
||||
.map_err(|e| format!("legacy CTB zlib decompress: {e}"))?;
|
||||
decoded = inflate_lenient(payload, &mut warnings)?;
|
||||
} else {
|
||||
use flate2::read::DeflateDecoder;
|
||||
DeflateDecoder::new(payload)
|
||||
@@ -472,10 +509,72 @@ fn decompress_ctb(data: &[u8]) -> Result<String, String> {
|
||||
.map_err(|e| format!("legacy CTB deflate decompress: {e}"))?;
|
||||
}
|
||||
}
|
||||
if decoded.last() == Some(&0) {
|
||||
while decoded.last() == Some(&0) {
|
||||
decoded.pop();
|
||||
}
|
||||
String::from_utf8(decoded).map_err(|e| format!("CTB text is not UTF-8: {e}"))
|
||||
let (text, code_page) = decode_plot_style_text(decoded);
|
||||
if let Some(note) = code_page {
|
||||
warnings.push(note);
|
||||
}
|
||||
Ok((text, warnings))
|
||||
}
|
||||
|
||||
/// Inflate a zlib stream, falling back to the raw deflate data behind its
|
||||
/// two-byte header when the stream's own trailer is wrong.
|
||||
fn inflate_lenient(payload: &[u8], warnings: &mut Vec<String>) -> Result<Vec<u8>, String> {
|
||||
use flate2::read::{DeflateDecoder, ZlibDecoder};
|
||||
let mut decoded = Vec::new();
|
||||
match ZlibDecoder::new(payload).read_to_end(&mut decoded) {
|
||||
Ok(_) => Ok(decoded),
|
||||
Err(zlib_error) if payload.len() > 2 => {
|
||||
let mut raw = Vec::new();
|
||||
DeflateDecoder::new(&payload[2..])
|
||||
.read_to_end(&mut raw)
|
||||
.map_err(|_| format!("CTB zlib decompress: {zlib_error}"))?;
|
||||
warnings.push("the zlib stream is damaged; its deflate data was read directly".into());
|
||||
Ok(raw)
|
||||
}
|
||||
Err(zlib_error) => Err(format!("CTB zlib decompress: {zlib_error}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The table's text out of its bytes: UTF-8 when it is, else Windows-1252
|
||||
/// (Latin-1 with the 0x80–0x9F block the code page defines), which is what
|
||||
/// applications on Western systems write into descriptions. Other code
|
||||
/// pages come out as readable but wrong characters in descriptions only —
|
||||
/// pens and colours are numbers and unaffected. The second value says when
|
||||
/// the fallback was taken.
|
||||
pub fn decode_plot_style_text(bytes: Vec<u8>) -> (String, Option<String>) {
|
||||
match String::from_utf8(bytes) {
|
||||
Ok(text) => (text, None),
|
||||
Err(error) => {
|
||||
let text = error
|
||||
.into_bytes()
|
||||
.into_iter()
|
||||
.map(windows_1252_char)
|
||||
.collect();
|
||||
(
|
||||
text,
|
||||
Some("descriptions were read as Windows-1252 text (the file is not UTF-8)".into()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One Windows-1252 byte as a character: Latin-1 except for the 0x80–0x9F
|
||||
/// block, which the code page fills with punctuation and accented letters.
|
||||
fn windows_1252_char(byte: u8) -> char {
|
||||
const HIGH: [char; 32] = [
|
||||
'\u{20AC}', '\u{FFFD}', '\u{201A}', '\u{0192}', '\u{201E}', '\u{2026}', '\u{2020}',
|
||||
'\u{2021}', '\u{02C6}', '\u{2030}', '\u{0160}', '\u{2039}', '\u{0152}', '\u{FFFD}',
|
||||
'\u{017D}', '\u{FFFD}', '\u{FFFD}', '\u{2018}', '\u{2019}', '\u{201C}', '\u{201D}',
|
||||
'\u{2022}', '\u{2013}', '\u{2014}', '\u{02DC}', '\u{2122}', '\u{0161}', '\u{203A}',
|
||||
'\u{0153}', '\u{FFFD}', '\u{017E}', '\u{0178}',
|
||||
];
|
||||
match byte {
|
||||
0x80..=0x9F => HIGH[usize::from(byte - 0x80)],
|
||||
_ => char::from(byte),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compress plot-style text content as a CTB/STB file.
|
||||
@@ -686,6 +785,7 @@ fn parse_plot_style_text(text: &str, name: String, is_stb: bool) -> Result<PlotS
|
||||
lineweights,
|
||||
aci_entries,
|
||||
named_entries,
|
||||
load_warnings: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -797,3 +897,90 @@ mod round_trip_tests {
|
||||
assert_eq!(reloaded.resolve_lineweight(1), Some(0.035));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod lenient_loading_tests {
|
||||
use super::{compress_ctb, decode_plot_style_text, PlotStyleTable};
|
||||
|
||||
fn table_with_description(description: &str) -> PlotStyleTable {
|
||||
let mut table = PlotStyleTable::identity("foreign.ctb");
|
||||
table.description = description.to_string();
|
||||
table.aci_entries[1].lineweight = 13;
|
||||
table
|
||||
}
|
||||
|
||||
fn compressed(table: &PlotStyleTable) -> Vec<u8> {
|
||||
compress_ctb(table.to_text().as_bytes()).expect("compress")
|
||||
}
|
||||
|
||||
/// The pen data must survive every tolerated defect.
|
||||
fn assert_pens_kept(table: &PlotStyleTable) {
|
||||
assert_eq!(table.aci_entries[1].lineweight, 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wrong_checksum_is_reported_not_fatal() {
|
||||
let mut bytes = compressed(&table_with_description("ok"));
|
||||
bytes[48] ^= 0xFF;
|
||||
let table = PlotStyleTable::from_bytes("foreign.ctb", &bytes).expect("loads");
|
||||
assert_pens_kept(&table);
|
||||
assert_eq!(table.load_warnings.len(), 1, "{:?}", table.load_warnings);
|
||||
assert!(table.load_warnings[0].contains("checksum"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_text_length_off_by_one_and_a_short_payload_field_are_tolerated() {
|
||||
let mut bytes = compressed(&table_with_description("ok"));
|
||||
let text_len = u32::from_le_bytes(bytes[52..56].try_into().unwrap());
|
||||
bytes[52..56].copy_from_slice(&(text_len - 1).to_le_bytes());
|
||||
let table = PlotStyleTable::from_bytes("foreign.ctb", &bytes).expect("loads");
|
||||
assert_pens_kept(&table);
|
||||
assert!(table.load_warnings.iter().any(|w| w.contains("bytes of text")));
|
||||
|
||||
let mut bytes = compressed(&table_with_description("ok"));
|
||||
let compressed_len = u32::from_le_bytes(bytes[56..60].try_into().unwrap());
|
||||
bytes[56..60].copy_from_slice(&(compressed_len + 100).to_le_bytes());
|
||||
let table = PlotStyleTable::from_bytes("foreign.ctb", &bytes).expect("loads");
|
||||
assert_pens_kept(&table);
|
||||
assert!(table.load_warnings.iter().any(|w| w.contains("compressed bytes")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_1252_descriptions_are_read_with_a_note() {
|
||||
// "Descripción" and "Ελληνικά" are not what a Western file holds; a
|
||||
// Western one holds e.g. "Plumas – señal" in Windows-1252.
|
||||
let text = table_with_description("marker").to_text();
|
||||
let mut bytes = text.into_bytes();
|
||||
let marker = bytes.windows(6).position(|w| w == b"marker").unwrap();
|
||||
let cp1252 = [
|
||||
0x50, 0x6C, 0x75, 0x6D, 0x61, 0x73, 0x20, 0x96, 0x20, 0x73, 0x65, 0xF1, 0x61, 0x6C,
|
||||
];
|
||||
bytes.splice(marker..marker + 6, cp1252);
|
||||
let compressed = compress_ctb(&bytes).unwrap();
|
||||
let table = PlotStyleTable::from_bytes("foreign.ctb", &compressed).expect("loads");
|
||||
assert_pens_kept(&table);
|
||||
assert_eq!(table.description, "Plumas – señal");
|
||||
assert!(table.load_warnings.iter().any(|w| w.contains("Windows-1252")));
|
||||
// Clean UTF-8 gets no note.
|
||||
let (text, note) = decode_plot_style_text("Ελληνικά".as_bytes().to_vec());
|
||||
assert_eq!(text, "Ελληνικά");
|
||||
assert!(note.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_truncated_stream_is_still_an_error() {
|
||||
let bytes = compressed(&table_with_description("ok"));
|
||||
let cut = &bytes[..bytes.len() / 2];
|
||||
let error = PlotStyleTable::from_bytes("foreign.ctb", cut).unwrap_err();
|
||||
assert!(error.contains("incomplete"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_tables_and_missing_files_get_a_reason() {
|
||||
let error = PlotStyleTable::load_named("styles.stb").unwrap_err();
|
||||
assert!(error.contains(".stb"), "{error}");
|
||||
let error = PlotStyleTable::load_named("nowhere-to-be-found.ctb").unwrap_err();
|
||||
assert!(error.contains("nowhere-to-be-found.ctb"), "{error}");
|
||||
assert!(PlotStyleTable::load_named("../escape.ctb").is_err());
|
||||
}
|
||||
}
|
||||
@@ -2059,6 +2059,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Plot table type" => Some(("properties", "plot-table-type")),
|
||||
"Plot transparency" => Some(("plot", "plot-transparency")),
|
||||
"Plot upside-down" => Some(("plot", "plot-upside-down")),
|
||||
"Named plot style tables (.stb) are not supported yet: {name}" => Some(("plot", "named-plot-style-tables-stb-are-not-supported-yet-name")),
|
||||
"{name} was not found in the plot styles folder ({folder})." => Some(("plot", "name-was-not-found-in-the-plot-styles-folder-folder")),
|
||||
"Plot window: {x0:.2},{y0:.2} to {x1:.2},{y1:.2}" => Some(("common", "plot-window-x0-2-y0-2-to-x1-2-y1-2")),
|
||||
"Plot window: {x1:.2},{y1:.2} to {x2:.2},{y2:.2}" => Some(("common", "plot-window-x1-2-y1-2-to-x2-2-y2-2")),
|
||||
"Plugin '{id}' failed to load: {e}" => Some(("common", "plugin-id-failed-to-load-e")),
|
||||
|
||||
@@ -525,6 +525,9 @@ pub struct PlotDialogState {
|
||||
/// The selected setup references a style table that is not loaded.
|
||||
#[serde(skip)]
|
||||
pub style_missing: bool,
|
||||
/// Why the selected table could not be loaded, shown under the Table row.
|
||||
#[serde(skip)]
|
||||
pub style_error: Option<String>,
|
||||
/// Named page setups in the document (refreshed when the dialog opens).
|
||||
#[serde(skip)]
|
||||
pub page_setups: Vec<String>,
|
||||
@@ -596,6 +599,7 @@ impl Default for PlotDialogState {
|
||||
show_plot_styles: false,
|
||||
plot_styles: Vec::new(),
|
||||
style_missing: false,
|
||||
style_error: None,
|
||||
page_setups: Vec::new(),
|
||||
selected_setup: String::new(),
|
||||
name_input: None,
|
||||
@@ -852,6 +856,17 @@ fn custom_scale_row<'a>(s: &'a PlotDialogState, enabled: bool) -> Element<'a, Me
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Why the selected plot style table is not loaded, under the Table row —
|
||||
/// the file's own problem, or where a copy would be found.
|
||||
fn style_note<'a>(s: &'a PlotDialogState) -> Element<'a, Message> {
|
||||
match &s.style_error {
|
||||
Some(error) if s.style_missing => {
|
||||
text(error.clone()).size(10).style(muted_style).into()
|
||||
}
|
||||
_ => Space::new().height(0).into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn panel<'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
@@ -1460,6 +1475,7 @@ pub fn view_window(
|
||||
PlotDlgMsg::Style,
|
||||
width,
|
||||
),
|
||||
style_note(s),
|
||||
check_enabled(
|
||||
t!("Plot with plot styles"),
|
||||
s.apply_plot_styles,
|
||||
|
||||
Reference in new issue
Block a user