From 7a75d430cd948c971e4dae07418a9eff868b9378 Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:58:34 +0300 Subject: [PATCH] fix(plot-style): load plot style tables written by other applications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tables copied from other applications into the plot styles folder were listed but refused on selection, showing as "missing" with the reason lost on a console: the loader failed hard on a checksum or a text length that did not add up and on any non-UTF-8 byte — and those files carry their descriptions in Windows-1252. - decompress_ctb reads the file as it is: a checksum or length mismatch is reported and read past (a genuinely cut-off file is still an error), a zlib stream with a bad trailer is retried as raw deflate, and text that is not UTF-8 is read as Windows-1252 (pens and colours are numbers and unaffected; other code pages only show wrong characters in descriptions). - PlotStyleTable carries load_warnings; every load site reports them on the command line, so a table that needed tolerance says so. - load_named explains itself: a .stb name says named tables are not supported yet, a .ctb missing from the folder says which folder to copy it into (instead of "Unknown built-in plot style"). - The Plot dialog shows the reason under the Table row (style_error) — from a selection, from a page setup that names a table, and from the Load… picker, whose error used to be swallowed. - A table picked with Load… from outside the plot styles folder is copied into it (unless a file of that name is already there), so it is listed from then on and page setups naming it resolve after a restart. Tests: flipped checksum, text length off by one, a short payload field, Windows-1252 descriptions, a truncated stream, .stb and missing names. Co-Authored-By: Claude Opus 5 --- locales/ar-SA/opencadstudio.ftl | 2 + locales/bg-BG/opencadstudio.ftl | 2 + locales/cs-CZ/opencadstudio.ftl | 2 + locales/de-DE/opencadstudio.ftl | 2 + locales/el-GR/opencadstudio.ftl | 2 + locales/en-US/opencadstudio.ftl | 2 + locales/es-ES/opencadstudio.ftl | 2 + locales/fi-FI/opencadstudio.ftl | 2 + locales/fr-FR/opencadstudio.ftl | 2 + locales/hi-IN/opencadstudio.ftl | 2 + locales/hu-HU/opencadstudio.ftl | 2 + locales/it-IT/opencadstudio.ftl | 2 + locales/ja-JP/opencadstudio.ftl | 2 + locales/ko-KR/opencadstudio.ftl | 2 + locales/nl-NL/opencadstudio.ftl | 2 + locales/pl-PL/opencadstudio.ftl | 2 + locales/pt-BR/opencadstudio.ftl | 2 + locales/ru-RU/opencadstudio.ftl | 2 + locales/tr-TR/opencadstudio.ftl | 2 + locales/zh-CN/opencadstudio.ftl | 2 + locales/zh-TW/opencadstudio.ftl | 2 + src/app/mod.rs | 4 +- src/app/update/file.rs | 27 +++- src/app/update/mod.rs | 17 ++- src/io/mod.rs | 29 +++- src/io/plot_style.rs | 237 ++++++++++++++++++++++++++++---- src/locale_catalog.rs | 2 + src/ui/window/plot.rs | 16 +++ 28 files changed, 339 insertions(+), 35 deletions(-) diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 1fdd169af..c790158ce 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -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 = الخصائص… diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 7b0c44e00..4fb695724 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -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 = Свойства… diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index dcd36aaa6..a71be8d71 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -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… diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index e4264e697..4527d966a 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -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... diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 1dfe08503..19e07fdb2 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -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 = Ιδιότητες… diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index c959f870b..dc0c2ac63 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -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… diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index e9f255d51..d7fd00560 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -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… diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index b0bc8c856..a5e998227 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -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… diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 5094cc756..545a659ab 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -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... diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 242447240..915fd8558 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -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 = गुण... diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 635e974cd..56d0a3121 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -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… diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 1da38e16a..3632d4fa7 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -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à… diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 22d894f31..d88cafae5 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -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 = プロパティ… diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 2d04ba139..b0d98a38e 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -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 = 속성… diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 555d2247b..2bc2859c3 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -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... diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 3f7438b28..287b729e6 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -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… diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index a2a30fb6e..64f2e0164 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -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… diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 3ee0e3ad4..6f5910fde 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -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 = Свойства... diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index cea64bba1..3acc3daf6 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -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… diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 70dc709d3..4b0abd1fd 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -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 = 特性… diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 041b2911f..8e9237efb 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -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 = 特性… diff --git a/src/app/mod.rs b/src/app/mod.rs index ebaa09771..7024e4fdc 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -3422,7 +3422,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), + /// The Load… picker finished: a table, nothing (cancelled), or why the + /// file could not be read. + PlotStyleLoaded(Result, String>), /// Clear the active plot style table. PlotStyleClear, /// Open/close the Plot Style panel. diff --git a/src/app/update/file.rs b/src/app/update/file.rs index b2044180c..26852668b 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -4468,18 +4468,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); } } } @@ -4744,16 +4748,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 @@ -4901,6 +4908,18 @@ impl OpenCADStudio { d.apply_plot_styles = ps.flags.plot_plot_styles; d.show_plot_styles = ps.flags.show_plot_styles; d.style_missing = !d.style_name.is_empty() && !style_loaded; + 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 diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 5d650515d..411897d53 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -8762,7 +8762,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!( @@ -8774,6 +8774,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).", @@ -8790,7 +8792,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(); @@ -8817,12 +8827,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(); } } diff --git a/src/io/mod.rs b/src/io/mod.rs index fdc687529..46829dcb0 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -1522,7 +1522,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 { +/// 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, 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"]) @@ -1533,8 +1538,26 @@ pub async fn pick_plot_style() -> Option { 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 ───────────────────────────────────────────────────── diff --git a/src/io/plot_style.rs b/src/io/plot_style.rs index 89b2033c6..4e9e3d9e6 100644 --- a/src/io/plot_style.rs +++ b/src/io/plot_style.rs @@ -175,6 +175,10 @@ pub struct PlotStyleTable { pub aci_entries: Vec, // 256 entries, index = ACI /// For STB: named style entries. pub named_entries: HashMap, + /// 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, } 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, raw: &[u8]) -> Result { 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 { @@ -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 { +/// 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> { 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 { 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 { .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 { .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) -> Result, 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) -> (String, Option) { + 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 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 { + 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()); + } +} diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 17536c4f7..12e18770f 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -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")), diff --git a/src/ui/window/plot.rs b/src/ui/window/plot.rs index 451860c9a..4655fac60 100644 --- a/src/ui/window/plot.rs +++ b/src/ui/window/plot.rs @@ -431,6 +431,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, /// Named page setups in the document (refreshed when the dialog opens). #[serde(skip)] pub page_setups: Vec, @@ -489,6 +492,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, @@ -670,6 +674,17 @@ fn check_enabled<'a>( .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> { container(content) .width(Length::Fill) @@ -1241,6 +1256,7 @@ pub fn view_window( PlotDlgMsg::Style, width, ), + style_note(s), check_enabled( t!("Plot with plot styles"), s.apply_plot_styles,