use std::borrow::Cow;
use std::path::Path;
use crate::render::save::SaveFormat;
const CSV_FLOAT_PRECISION: usize = 18;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SaveTarget {
Figure(SaveFormat),
CurveCsv,
}
impl SaveTarget {
pub fn from_extension(ext: &str) -> Option<Self> {
if ext.eq_ignore_ascii_case("csv") {
return Some(SaveTarget::CurveCsv);
}
SaveFormat::from_extension(ext).map(SaveTarget::Figure)
}
pub fn from_path(path: &Path) -> Option<Self> {
path.extension()
.and_then(|e| e.to_str())
.and_then(Self::from_extension)
}
}
fn format_csv_float(v: f64) -> String {
let s = format!("{v:.*e}", CSV_FLOAT_PRECISION);
match s.split_once('e') {
Some((mantissa, exp)) => {
let exp: i32 = exp.parse().unwrap_or(0);
let sign = if exp < 0 { '-' } else { '+' };
format!("{mantissa}e{sign}{:02}", exp.unsigned_abs())
}
None => s,
}
}
pub fn curve_to_csv(x: &[f64], y: &[f64]) -> String {
let mut out = String::from("x,y\n");
for (xv, yv) in x.iter().zip(y.iter()) {
out.push_str(&format_csv_float(*xv));
out.push(',');
out.push_str(&format_csv_float(*yv));
out.push('\n');
}
out
}
pub fn decode_png_to_rgba(png_bytes: &[u8]) -> std::io::Result<(u32, u32, Vec<u8>)> {
let decoder = png::Decoder::new(std::io::Cursor::new(png_bytes));
let mut reader = decoder
.read_info()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let buf_size = reader.output_buffer_size().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "PNG output size overflow")
})?;
let mut buf = vec![0u8; buf_size];
let info = reader
.next_frame(&mut buf)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
if info.color_type != png::ColorType::Rgba || info.bit_depth != png::BitDepth::Eight {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"expected 8-bit RGBA PNG",
));
}
buf.truncate(info.buffer_size());
Ok((info.width, info.height, buf))
}
pub fn rgba_to_clipboard_image(
rgba: &[u8],
width: u32,
height: u32,
) -> Option<arboard::ImageData<'static>> {
let expected = (width as usize)
.checked_mul(height as usize)?
.checked_mul(4)?;
if rgba.len() != expected {
return None;
}
Some(arboard::ImageData {
width: width as usize,
height: height as usize,
bytes: Cow::Owned(rgba.to_vec()),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn save_target_from_extension_maps_csv_and_raster() {
assert_eq!(
SaveTarget::from_extension("csv"),
Some(SaveTarget::CurveCsv)
);
assert_eq!(
SaveTarget::from_extension("CSV"),
Some(SaveTarget::CurveCsv)
);
assert_eq!(
SaveTarget::from_extension("png"),
Some(SaveTarget::Figure(SaveFormat::Png))
);
assert_eq!(
SaveTarget::from_extension("PNG"),
Some(SaveTarget::Figure(SaveFormat::Png))
);
assert_eq!(
SaveTarget::from_extension("svg"),
Some(SaveTarget::Figure(SaveFormat::Svg))
);
assert_eq!(SaveTarget::from_extension("pdf"), None);
assert_eq!(SaveTarget::from_extension("xyz"), None);
}
#[test]
fn save_target_from_path_uses_extension() {
assert_eq!(
SaveTarget::from_path(Path::new("/tmp/curve.csv")),
Some(SaveTarget::CurveCsv)
);
assert_eq!(
SaveTarget::from_path(Path::new("/tmp/plot.png")),
Some(SaveTarget::Figure(SaveFormat::Png))
);
assert_eq!(SaveTarget::from_path(Path::new("/tmp/noext")), None);
}
#[test]
fn curve_to_csv_produces_exact_silx_style_output() {
let x = [0.0, 1.5];
let y = [-2.0, 3.25];
let csv = curve_to_csv(&x, &y);
let expected = "x,y\n\
0.000000000000000000e+00,-2.000000000000000000e+00\n\
1.500000000000000000e+00,3.250000000000000000e+00\n";
assert_eq!(csv, expected);
}
#[test]
fn format_csv_float_matches_c_printf_exponent() {
assert_eq!(format_csv_float(0.0), "0.000000000000000000e+00");
assert_eq!(format_csv_float(1000.0), "1.000000000000000000e+03");
assert_eq!(format_csv_float(0.001), "1.000000000000000021e-03");
assert_eq!(format_csv_float(-3.25), "-3.250000000000000000e+00");
}
#[test]
fn curve_to_csv_empty_is_header_only() {
assert_eq!(curve_to_csv(&[], &[]), "x,y\n");
}
#[test]
fn rgba_to_clipboard_image_shapes_a_valid_buffer() {
let rgba: Vec<u8> = vec![10, 20, 30, 255, 40, 50, 60, 128];
let image = rgba_to_clipboard_image(&rgba, 2, 1).expect("valid buffer");
assert_eq!(image.width, 2);
assert_eq!(image.height, 1);
assert_eq!(image.bytes.len(), 8);
assert_eq!(image.bytes.as_ref(), rgba.as_slice());
}
#[test]
fn rgba_to_clipboard_image_rejects_wrong_length() {
assert!(rgba_to_clipboard_image(&[0; 7], 2, 1).is_none());
assert!(rgba_to_clipboard_image(&[0; 9], 2, 1).is_none());
}
#[test]
fn decode_png_to_rgba_round_trips_encode_png() {
use crate::render::save::encode_png;
let rgba: Vec<u8> = vec![
1, 2, 3, 255, 4, 5, 6, 255, 7, 8, 9, 255, 10, 11, 12, 255, ];
let png = encode_png(&rgba, 2, 2).expect("encode");
let (w, h, decoded) = decode_png_to_rgba(&png).expect("decode");
assert_eq!((w, h), (2, 2));
assert_eq!(decoded, rgba);
}
}