forked from 1b-agent/opencadstudio-upstream
fix(plot-style): load plot style tables written by other applications
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 <[email protected]>
This commit is contained in:
1 parent
2b38f798c0
commit
7a75d430cd
28 files changed
+339
-35
No files matched your search
+3
-1
@@ -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<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
@@ -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
|
||||
|
||||
+15
-2
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+26
-3
@@ -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<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"])
|
||||
@@ -1533,8 +1538,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")),
|
||||
|
||||
@@ -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<String>,
|
||||
/// Named page setups in the document (refreshed when the dialog opens).
|
||||
#[serde(skip)]
|
||||
pub page_setups: Vec<String>,
|
||||
@@ -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>>) -> 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,
|
||||
|
||||
Reference in new issue
Block a user