1use std::fmt;
31use std::path::Path;
32
33use egui::{Pos2, Rect};
34use egui_wgpu::{RenderState, wgpu};
35
36use crate::core::plot::Plot;
37use crate::core::transform::Scale;
38use crate::render::backend_wgpu::WgpuResources;
39
40#[derive(Debug)]
42pub enum SaveError {
43 Io(std::io::Error),
45 Encode(png::EncodingError),
47 Readback(String),
49}
50
51impl fmt::Display for SaveError {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 SaveError::Io(e) => write!(f, "save_graph: writing PNG: {e}"),
55 SaveError::Encode(e) => write!(f, "save_graph: encoding PNG: {e}"),
56 SaveError::Readback(e) => write!(f, "save_graph: GPU readback: {e}"),
57 }
58 }
59}
60
61impl std::error::Error for SaveError {
62 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
63 match self {
64 SaveError::Io(e) => Some(e),
65 SaveError::Encode(e) => Some(e),
66 SaveError::Readback(_) => None,
67 }
68 }
69}
70
71impl From<std::io::Error> for SaveError {
72 fn from(e: std::io::Error) -> Self {
73 SaveError::Io(e)
74 }
75}
76
77impl From<png::EncodingError> for SaveError {
78 fn from(e: png::EncodingError) -> Self {
79 SaveError::Encode(e)
80 }
81}
82
83pub(crate) fn padded_bytes_per_row(width: u32) -> u32 {
87 let unpadded = 4 * width;
88 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
89 unpadded.div_ceil(align) * align
90}
91
92pub(crate) fn rows_to_rgba8(
96 mapped: &[u8],
97 width: u32,
98 height: u32,
99 bytes_per_row: u32,
100 format: wgpu::TextureFormat,
101) -> Vec<u8> {
102 let w = width as usize;
103 let h = height as usize;
104 let bpr = bytes_per_row as usize;
105 let row_bytes = w * 4;
106 let swap = matches!(
107 format,
108 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
109 );
110
111 let mut out = vec![0u8; w * h * 4];
112 for row in 0..h {
113 let src = &mapped[row * bpr..row * bpr + row_bytes];
114 let dst = &mut out[row * row_bytes..(row + 1) * row_bytes];
115 if swap {
116 for (s, d) in src.chunks_exact(4).zip(dst.chunks_exact_mut(4)) {
117 d[0] = s[2];
118 d[1] = s[1];
119 d[2] = s[0];
120 d[3] = s[3];
121 }
122 } else {
123 dst.copy_from_slice(src);
124 }
125 }
126 out
127}
128
129pub fn encode_png(rgba: &[u8], width: u32, height: u32) -> Result<Vec<u8>, png::EncodingError> {
131 let mut out = Vec::new();
132 {
133 let mut encoder = png::Encoder::new(&mut out, width, height);
134 encoder.set_color(png::ColorType::Rgba);
135 encoder.set_depth(png::BitDepth::Eight);
136 let mut writer = encoder.write_header()?;
137 writer.write_image_data(rgba)?;
138 }
139 Ok(out)
140}
141
142pub fn rgba_to_rgb(rgba: &[u8], width: u32, height: u32) -> Vec<u8> {
149 let n = (width as usize) * (height as usize);
150 let mut out = Vec::with_capacity(n * 3);
151 for px in rgba.chunks_exact(4).take(n) {
152 out.push(px[0]);
153 out.push(px[1]);
154 out.push(px[2]);
155 }
156 out
157}
158
159pub fn encode_ppm(rgba: &[u8], width: u32, height: u32) -> Vec<u8> {
166 let rgb = rgba_to_rgb(rgba, width, height);
167 let header = format!("P6\n{width} {height}\n255\n");
168 let mut out = Vec::with_capacity(header.len() + rgb.len());
169 out.extend_from_slice(header.as_bytes());
170 out.extend_from_slice(&rgb);
171 out
172}
173
174pub fn encode_mask_npy(height: u32, width: u32, data: &[u8]) -> Vec<u8> {
185 const MAGIC: &[u8] = b"\x93NUMPY";
186 let header =
187 format!("{{'descr': '|u1', 'fortran_order': False, 'shape': ({height}, {width}), }}");
188 let unpadded = MAGIC.len() + 2 + 2 + header.len() + 1;
191 let pad = (64 - (unpadded % 64)) % 64;
192 let header_len = header.len() + pad + 1; debug_assert!(header_len <= u16::MAX as usize);
194
195 let mut out = Vec::with_capacity(unpadded + pad + data.len());
196 out.extend_from_slice(MAGIC);
197 out.extend_from_slice(&[1u8, 0u8]); out.extend_from_slice(&(header_len as u16).to_le_bytes());
199 out.extend_from_slice(header.as_bytes());
200 out.extend(std::iter::repeat_n(b' ', pad));
201 out.push(b'\n');
202 out.extend_from_slice(data);
203 out
204}
205
206pub fn decode_mask_npy(bytes: &[u8]) -> std::io::Result<(u32, u32, Vec<u8>)> {
216 use std::io::Read;
217 let mut r = bytes;
218 let invalid = |msg: &str| std::io::Error::new(std::io::ErrorKind::InvalidData, msg.to_string());
219
220 let mut magic = [0u8; 6];
221 r.read_exact(&mut magic)?;
222 if &magic != b"\x93NUMPY" {
223 return Err(invalid("not a .npy file (bad magic)"));
224 }
225
226 let mut version = [0u8; 2];
227 r.read_exact(&mut version)?;
228 let header_len = if version[0] >= 2 {
230 let mut len = [0u8; 4];
231 r.read_exact(&mut len)?;
232 u32::from_le_bytes(len) as usize
233 } else {
234 let mut len = [0u8; 2];
235 r.read_exact(&mut len)?;
236 u16::from_le_bytes(len) as usize
237 };
238
239 let mut header_bytes = vec![0u8; header_len];
240 r.read_exact(&mut header_bytes)?;
241 let header =
242 std::str::from_utf8(&header_bytes).map_err(|_| invalid("npy header is not UTF-8"))?;
243
244 let descr =
245 npy_header_field(header, "descr").ok_or_else(|| invalid("npy header missing 'descr'"))?;
246 if !matches!(descr.as_str(), "|u1" | "<u1" | ">u1" | "u1") {
248 return Err(invalid("npy mask must be uint8 ('|u1')"));
249 }
250
251 let fortran = npy_header_field(header, "fortran_order")
252 .ok_or_else(|| invalid("npy header missing 'fortran_order'"))?;
253 if fortran != "False" {
254 return Err(invalid("npy mask must be C-order (fortran_order: False)"));
255 }
256
257 let (height, width) = npy_shape_2d(header)?;
258
259 let count = (height as usize) * (width as usize);
260 let mut data = vec![0u8; count];
261 r.read_exact(&mut data)?;
262 Ok((height, width, data))
263}
264
265fn npy_header_field(header: &str, key: &str) -> Option<String> {
269 let needle = format!("'{key}':");
271 let start = header.find(&needle)? + needle.len();
272 let rest = &header[start..];
273 let end = rest.find([',', '}'])?;
274 let value = rest[..end].trim();
275 Some(value.trim_matches(['\'', '"']).to_string())
276}
277
278fn npy_shape_2d(header: &str) -> std::io::Result<(u32, u32)> {
283 let invalid = |msg: &str| std::io::Error::new(std::io::ErrorKind::InvalidData, msg.to_string());
284 let start = header
285 .find("'shape':")
286 .ok_or_else(|| invalid("npy header missing 'shape'"))?
287 + "'shape':".len();
288 let rest = &header[start..];
289 let open = rest.find('(').ok_or_else(|| invalid("malformed shape"))?;
290 let close = rest.find(')').ok_or_else(|| invalid("malformed shape"))?;
291 let dims: Vec<u32> = rest[open + 1..close]
292 .split(',')
293 .map(str::trim)
294 .filter(|s| !s.is_empty())
295 .map(|s| s.parse::<u32>())
296 .collect::<Result<_, _>>()
297 .map_err(|_| invalid("non-integer shape dimension"))?;
298 if dims.len() != 2 {
299 return Err(invalid("npy mask must be 2D"));
300 }
301 Ok((dims[0], dims[1]))
302}
303
304pub fn encode_mask_edf(height: u32, width: u32, data: &[u8]) -> Vec<u8> {
317 const BLOCK: usize = 512;
318 let size = (height as usize) * (width as usize);
319 let mut header = String::from("{\n");
320 header.push_str("HeaderID = EH:000001:000000:000000 ;\n");
321 header.push_str("Image = 1 ;\n");
322 header.push_str("ByteOrder = LowByteFirst ;\n");
323 header.push_str("DataType = UnsignedByte ;\n");
324 header.push_str(&format!("Dim_1 = {width} ;\n"));
325 header.push_str(&format!("Dim_2 = {height} ;\n"));
326 header.push_str(&format!("Size = {size} ;\n"));
327 let unpadded = header.len() + 2; let pad = (BLOCK - (unpadded % BLOCK)) % BLOCK;
330 header.extend(std::iter::repeat_n(' ', pad));
331 header.push_str("}\n");
332
333 let mut out = Vec::with_capacity(header.len() + data.len());
334 out.extend_from_slice(header.as_bytes());
335 out.extend_from_slice(data);
336 out
337}
338
339pub fn decode_mask_edf(bytes: &[u8]) -> std::io::Result<(u32, u32, Vec<u8>)> {
351 let invalid = |msg: &str| std::io::Error::new(std::io::ErrorKind::InvalidData, msg.to_string());
352 let open = bytes
353 .iter()
354 .position(|&b| b == b'{')
355 .ok_or_else(|| invalid("not an EDF file (no '{')"))?;
356 let close = bytes[open..]
357 .iter()
358 .position(|&b| b == b'}')
359 .ok_or_else(|| invalid("EDF header not terminated ('}' missing)"))?
360 + open;
361 let header = std::str::from_utf8(&bytes[open + 1..close])
362 .map_err(|_| invalid("EDF header is not UTF-8"))?;
363
364 let data_type = edf_header_field(header, "DataType")
365 .ok_or_else(|| invalid("EDF header missing 'DataType'"))?;
366 if data_type != "UnsignedByte" {
367 return Err(invalid("EDF mask must be UnsignedByte"));
368 }
369 let width: u32 = edf_header_field(header, "Dim_1")
370 .ok_or_else(|| invalid("EDF header missing 'Dim_1'"))?
371 .parse()
372 .map_err(|_| invalid("EDF 'Dim_1' is not an integer"))?;
373 let height: u32 = edf_header_field(header, "Dim_2")
374 .ok_or_else(|| invalid("EDF header missing 'Dim_2'"))?
375 .parse()
376 .map_err(|_| invalid("EDF 'Dim_2' is not an integer"))?;
377
378 let mut data_start = close + 1;
381 if bytes.get(data_start) == Some(&b'\n') {
382 data_start += 1;
383 }
384 let count = (height as usize) * (width as usize);
385 if bytes.len().saturating_sub(data_start) < count {
386 return Err(invalid("EDF body shorter than Dim_1 * Dim_2"));
387 }
388 let data = bytes[data_start..data_start + count].to_vec();
389 Ok((height, width, data))
390}
391
392fn edf_header_field(header: &str, key: &str) -> Option<String> {
396 header.split(';').find_map(|entry| {
397 let (k, v) = entry.split_once('=')?;
398 (k.trim() == key).then(|| v.trim().to_string())
399 })
400}
401
402pub fn encode_mask_tiff(height: u32, width: u32, data: &[u8]) -> std::io::Result<Vec<u8>> {
411 use tiff::encoder::{TiffEncoder, colortype::Gray8};
412 let to_io = |e: tiff::TiffError| std::io::Error::new(std::io::ErrorKind::InvalidData, e);
413 let mut cursor = std::io::Cursor::new(Vec::new());
414 let mut encoder = TiffEncoder::new(&mut cursor).map_err(to_io)?;
415 encoder
416 .write_image::<Gray8>(width, height, data)
417 .map_err(to_io)?;
418 Ok(cursor.into_inner())
419}
420
421pub fn decode_mask_tiff(bytes: &[u8]) -> std::io::Result<(u32, u32, Vec<u8>)> {
431 use tiff::decoder::{Decoder, DecodingResult};
432 let invalid = |msg: &str| std::io::Error::new(std::io::ErrorKind::InvalidData, msg.to_string());
433 let to_io = |e: tiff::TiffError| std::io::Error::new(std::io::ErrorKind::InvalidData, e);
434
435 let mut decoder = Decoder::new(std::io::Cursor::new(bytes)).map_err(to_io)?;
436 let (width, height) = decoder.dimensions().map_err(to_io)?;
437 let data = match decoder.read_image().map_err(to_io)? {
438 DecodingResult::U8(v) => v,
439 _ => return Err(invalid("TIFF mask must be 8-bit (uint8) samples")),
440 };
441 let count = (width as usize) * (height as usize);
444 if data.len() != count {
445 return Err(invalid(
446 "TIFF mask must be single-channel (width * height uint8 samples)",
447 ));
448 }
449 Ok((height, width, data))
450}
451
452pub fn write_mask_hdf5(
466 path: &std::path::Path,
467 data_path: &str,
468 height: u32,
469 width: u32,
470 data: &[u8],
471) -> std::io::Result<()> {
472 use rust_hdf5::H5File;
473 let to_io = |e: rust_hdf5::Hdf5Error| std::io::Error::other(e.to_string());
474 let file = if path.exists() {
476 H5File::open_rw(path).map_err(to_io)?
477 } else {
478 H5File::create(path).map_err(to_io)?
479 };
480 if file.dataset_names().iter().any(|n| n == data_path) {
482 file.delete_dataset(data_path).map_err(to_io)?;
483 }
484 let ds = file
485 .new_dataset::<u8>()
486 .shape([height as usize, width as usize])
487 .create(data_path)
488 .map_err(to_io)?;
489 ds.write_raw(data).map_err(to_io)?;
490 file.close().map_err(to_io)?;
492 Ok(())
493}
494
495pub fn list_mask_datasets_hdf5(path: &std::path::Path) -> std::io::Result<Vec<String>> {
504 use rust_hdf5::H5File;
505 let to_io = |e: rust_hdf5::Hdf5Error| std::io::Error::other(e.to_string());
506 let file = H5File::open(path).map_err(to_io)?;
507 let mut out = Vec::new();
508 for name in file.dataset_names() {
509 let ds = file.dataset(&name).map_err(to_io)?;
510 if ds.ndims() == 2 {
511 out.push(name);
512 }
513 }
514 out.sort();
515 Ok(out)
516}
517
518pub fn read_mask_hdf5(
528 path: &std::path::Path,
529 data_path: &str,
530) -> std::io::Result<(u32, u32, Vec<u8>)> {
531 use rust_hdf5::H5File;
532 let to_io = |e: rust_hdf5::Hdf5Error| std::io::Error::other(e.to_string());
533 let invalid = |msg: &str| std::io::Error::new(std::io::ErrorKind::InvalidData, msg.to_string());
534 let file = H5File::open(path).map_err(to_io)?;
535 let ds = file.dataset(data_path).map_err(to_io)?;
536 if ds.ndims() != 2 {
537 return Err(invalid("HDF5 mask dataset must be 2-dimensional"));
538 }
539 let shape = ds.shape();
540 let (height, width) = (shape[0], shape[1]);
541 let data: Vec<u8> = ds.read_raw().map_err(to_io)?;
542 if data.len() != height * width {
543 return Err(invalid(
544 "HDF5 mask body length does not match its (height, width) shape",
545 ));
546 }
547 Ok((height as u32, width as u32, data))
548}
549
550pub fn read_mask_hdf5_auto(path: &std::path::Path) -> std::io::Result<(u32, u32, Vec<u8>)> {
559 let datasets = list_mask_datasets_hdf5(path)?;
560 let first = datasets.first().ok_or_else(|| {
561 std::io::Error::new(
562 std::io::ErrorKind::InvalidData,
563 "HDF5 file contains no 2D dataset to load a mask from",
564 )
565 })?;
566 read_mask_hdf5(path, first)
567}
568
569fn read_hdf5_elements_as_f32(
582 ds: &rust_hdf5::H5Dataset,
583 elements: usize,
584) -> std::io::Result<Vec<f32>> {
585 let to_io = |e: rust_hdf5::Hdf5Error| std::io::Error::other(e.to_string());
586 let invalid = |msg: String| std::io::Error::new(std::io::ErrorKind::InvalidData, msg);
587 let data: Vec<f32> = match ds.element_size() {
588 4 => ds.read_raw::<f32>().map_err(to_io)?,
589 8 => ds
590 .read_raw::<f64>()
591 .map_err(to_io)?
592 .into_iter()
593 .map(|x| x as f32)
594 .collect(),
595 n => {
596 return Err(invalid(format!(
597 "unsupported HDF5 element size {n} (only 4-byte f32 / 8-byte f64 images are read)"
598 )));
599 }
600 };
601 if data.len() != elements {
602 return Err(invalid(format!(
603 "HDF5 image body length {} does not match its shape ({elements} elements)",
604 data.len()
605 )));
606 }
607 Ok(data)
608}
609
610pub fn read_image_hdf5(
620 path: &std::path::Path,
621 data_path: &str,
622) -> std::io::Result<(u32, u32, Vec<f32>)> {
623 use rust_hdf5::H5File;
624 let to_io = |e: rust_hdf5::Hdf5Error| std::io::Error::other(e.to_string());
625 let invalid = |msg: &str| std::io::Error::new(std::io::ErrorKind::InvalidData, msg.to_string());
626 let file = H5File::open(path).map_err(to_io)?;
627 let ds = file.dataset(data_path).map_err(to_io)?;
628 if ds.ndims() != 2 {
629 return Err(invalid("HDF5 image dataset must be 2-dimensional"));
630 }
631 let shape = ds.shape();
632 let (height, width) = (shape[0], shape[1]);
633 let data = read_hdf5_elements_as_f32(&ds, height.saturating_mul(width))?;
634 Ok((height as u32, width as u32, data))
635}
636
637pub fn read_image_hdf5_slice(
647 path: &std::path::Path,
648 data_path: &str,
649 index: usize,
650) -> std::io::Result<(u32, u32, Vec<f32>)> {
651 use rust_hdf5::H5File;
652 let to_io = |e: rust_hdf5::Hdf5Error| std::io::Error::other(e.to_string());
653 let invalid = |msg: String| std::io::Error::new(std::io::ErrorKind::InvalidData, msg);
654 let file = H5File::open(path).map_err(to_io)?;
655 let ds = file.dataset(data_path).map_err(to_io)?;
656 if ds.ndims() != 3 {
657 return Err(invalid(
658 "HDF5 image-stack dataset must be 3-dimensional".into(),
659 ));
660 }
661 let shape = ds.shape();
662 let (n, height, width) = (shape[0], shape[1], shape[2]);
663 if index >= n {
664 return Err(invalid(format!(
665 "HDF5 slice index {index} out of range (stack has {n} frames)"
666 )));
667 }
668 let elements = height.saturating_mul(width);
669 let data: Vec<f32> = match ds.element_size() {
670 4 => ds
671 .read_slice::<f32>(&[index, 0, 0], &[1, height, width])
672 .map_err(to_io)?,
673 8 => ds
674 .read_slice::<f64>(&[index, 0, 0], &[1, height, width])
675 .map_err(to_io)?
676 .into_iter()
677 .map(|x| x as f32)
678 .collect(),
679 sz => {
680 return Err(invalid(format!(
681 "unsupported HDF5 element size {sz} (only 4-byte f32 / 8-byte f64 images are read)"
682 )));
683 }
684 };
685 if data.len() != elements {
686 return Err(invalid(format!(
687 "HDF5 slice body length {} does not match its shape ({elements} elements)",
688 data.len()
689 )));
690 }
691 Ok((height as u32, width as u32, data))
692}
693
694pub fn encode_mask_msk(height: u32, width: u32, data: &[u8]) -> Vec<u8> {
706 let (h, w) = (height as usize, width as usize);
707 let num_ints = w.div_ceil(32);
708 let bytes_per_row = num_ints * 4;
709 let mut out = vec![0u8; 1024 + h * bytes_per_row];
710 out[0] = b'M';
712 out[4] = b'A';
713 out[8] = b'S';
714 out[12] = b'K';
715 out[16..20].copy_from_slice(&width.to_le_bytes());
716 out[20..24].copy_from_slice(&height.to_le_bytes());
717 out[24] = 1;
718 for y in 0..h {
720 let row = 1024 + y * bytes_per_row;
721 for x in 0..w {
722 if data[y * w + x] != 0 {
723 out[row + (x >> 3)] |= 1 << (x & 7);
724 }
725 }
726 }
727 out
728}
729
730pub fn decode_mask_msk(bytes: &[u8]) -> std::io::Result<(u32, u32, Vec<u8>)> {
741 let invalid = |msg: &str| std::io::Error::new(std::io::ErrorKind::InvalidData, msg.to_string());
742 if bytes.len() < 1024 {
743 return Err(invalid(
744 "fit2d mask file is shorter than its 1024-byte header",
745 ));
746 }
747 if bytes[0] != b'M' || bytes[4] != b'A' || bytes[8] != b'S' || bytes[12] != b'K' {
749 return Err(invalid("Not a fit2d mask file (bad MASK magic)"));
750 }
751 let width = u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
752 let height = u32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
753 let (h, w) = (height as usize, width as usize);
754 let num_ints = w.div_ceil(32);
755 let bytes_per_row = num_ints * 4;
756 let total = h * bytes_per_row;
757 if bytes.len() < 1024 + total {
758 return Err(invalid(
759 "fit2d mask body is shorter than its (height, width) shape requires",
760 ));
761 }
762 let mut data = vec![0u8; w * h];
763 for y in 0..h {
764 let row = 1024 + y * bytes_per_row;
765 for x in 0..w {
766 let bit = (bytes[row + (x >> 3)] >> (x & 7)) & 1;
767 data[y * w + x] = bit;
768 }
769 }
770 Ok((height, width, data))
771}
772
773const BASE64_ALPHABET: &[u8; 64] =
775 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
776
777fn base64_encode(data: &[u8]) -> String {
782 let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
783 for chunk in data.chunks(3) {
784 let b0 = chunk[0] as u32;
785 let b1 = *chunk.get(1).unwrap_or(&0) as u32;
786 let b2 = *chunk.get(2).unwrap_or(&0) as u32;
787 let n = (b0 << 16) | (b1 << 8) | b2;
788 out.push(BASE64_ALPHABET[((n >> 18) & 0x3F) as usize] as char);
789 out.push(BASE64_ALPHABET[((n >> 12) & 0x3F) as usize] as char);
790 if chunk.len() > 1 {
791 out.push(BASE64_ALPHABET[((n >> 6) & 0x3F) as usize] as char);
792 } else {
793 out.push('=');
794 }
795 if chunk.len() > 2 {
796 out.push(BASE64_ALPHABET[(n & 0x3F) as usize] as char);
797 } else {
798 out.push('=');
799 }
800 }
801 out
802}
803
804fn encode_rgb_png(rgb: &[u8], width: u32, height: u32) -> Result<Vec<u8>, png::EncodingError> {
806 let mut out = Vec::new();
807 {
808 let mut encoder = png::Encoder::new(&mut out, width, height);
809 encoder.set_color(png::ColorType::Rgb);
810 encoder.set_depth(png::BitDepth::Eight);
811 let mut writer = encoder.write_header()?;
812 writer.write_image_data(rgb)?;
813 }
814 Ok(out)
815}
816
817pub fn encode_svg(rgba: &[u8], width: u32, height: u32) -> Result<String, png::EncodingError> {
829 let rgb = rgba_to_rgb(rgba, width, height);
830 let png = encode_rgb_png(&rgb, width, height)?;
831 let b64 = base64_encode(&png);
832 let mut s = String::new();
833 s.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n");
834 s.push_str("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n");
835 s.push_str(" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n");
836 s.push_str("<svg xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n");
837 s.push_str(" xmlns=\"http://www.w3.org/2000/svg\"\n");
838 s.push_str(" version=\"1.1\"\n");
839 s.push_str(&format!(" width=\"{width}\"\n"));
840 s.push_str(&format!(" height=\"{height}\">\n"));
841 s.push_str(" <image xlink:href=\"data:image/png;base64,");
842 s.push_str(&b64);
843 s.push_str("\"\n");
844 s.push_str(" x=\"0\"\n");
845 s.push_str(" y=\"0\"\n");
846 s.push_str(&format!(" width=\"{width}\"\n"));
847 s.push_str(&format!(" height=\"{height}\"\n"));
848 s.push_str(" id=\"image\" />\n");
849 s.push_str("</svg>");
850 Ok(s)
851}
852
853pub fn encode_tiff(rgba: &[u8], width: u32, height: u32, dpi: u32) -> Vec<u8> {
874 let rgb = rgba_to_rgb(rgba, width, height);
875 let dpi = dpi.max(1);
876
877 const N_ENTRIES: u16 = 12;
879 let ifd_start: u32 = 8;
881 let ifd_len: u32 = 2 + 12 * (N_ENTRIES as u32) + 4;
882 let after_ifd: u32 = ifd_start + ifd_len;
886 let bits_offset: u32 = after_ifd;
887 let xres_offset: u32 = bits_offset + 6;
888 let yres_offset: u32 = xres_offset + 8;
889 let strip_offset: u32 = yres_offset + 8;
890 let strip_byte_count: u32 = width * height * 3;
891
892 let mut out: Vec<u8> = Vec::with_capacity(strip_offset as usize + rgb.len());
893
894 out.extend_from_slice(b"II"); out.extend_from_slice(&42u16.to_le_bytes()); out.extend_from_slice(&ifd_start.to_le_bytes()); out.extend_from_slice(&N_ENTRIES.to_le_bytes());
901
902 let mut entry = |tag: u16, typ: u16, count: u32, value_or_offset: u32, is_short: bool| {
905 out.extend_from_slice(&tag.to_le_bytes());
906 out.extend_from_slice(&typ.to_le_bytes());
907 out.extend_from_slice(&count.to_le_bytes());
908 if is_short {
909 out.extend_from_slice(&(value_or_offset as u16).to_le_bytes());
911 out.extend_from_slice(&0u16.to_le_bytes());
912 } else {
913 out.extend_from_slice(&value_or_offset.to_le_bytes());
914 }
915 };
916
917 entry(256, 4, 1, width, false); entry(257, 4, 1, height, false); entry(258, 3, 3, bits_offset, false); entry(259, 3, 1, 1, true); entry(262, 3, 1, 2, true); entry(273, 4, 1, strip_offset, false); entry(277, 3, 1, 3, true); entry(278, 4, 1, height, false); entry(279, 4, 1, strip_byte_count, false); entry(282, 5, 1, xres_offset, false); entry(283, 5, 1, yres_offset, false); entry(296, 3, 1, 2, true); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&8u16.to_le_bytes());
935 out.extend_from_slice(&8u16.to_le_bytes());
936 out.extend_from_slice(&8u16.to_le_bytes());
937 out.extend_from_slice(&dpi.to_le_bytes());
939 out.extend_from_slice(&1u32.to_le_bytes());
940 out.extend_from_slice(&dpi.to_le_bytes());
942 out.extend_from_slice(&1u32.to_le_bytes());
943
944 debug_assert_eq!(out.len() as u32, strip_offset);
946 out.extend_from_slice(&rgb);
947 out
948}
949
950const HEX_DIGITS: &[u8; 16] = b"0123456789ABCDEF";
952
953fn push_ascii_hex(s: &mut String, data: &[u8]) {
956 for (i, &b) in data.iter().enumerate() {
957 s.push(HEX_DIGITS[(b >> 4) as usize] as char);
958 s.push(HEX_DIGITS[(b & 0x0F) as usize] as char);
959 if (i + 1) % 40 == 0 {
960 s.push('\n');
961 }
962 }
963}
964
965pub fn encode_eps(rgba: &[u8], width: u32, height: u32) -> Vec<u8> {
976 let rgb = rgba_to_rgb(rgba, width, height);
977 let mut s = String::with_capacity(200 + rgb.len() * 2);
978 s.push_str("%!PS-Adobe-3.0 EPSF-3.0\n");
979 s.push_str(&format!("%%BoundingBox: 0 0 {width} {height}\n"));
980 s.push_str("%%EndComments\n");
981 s.push_str("gsave\n");
982 s.push_str(&format!("{width} {height} scale\n"));
984 s.push_str(&format!("/picstr {} string def\n", width as usize * 3));
985 s.push_str(&format!(
986 "{width} {height} 8 [ {width} 0 0 -{height} 0 {height} ]\n"
987 ));
988 s.push_str("{ currentfile picstr readhexstring pop } false 3 colorimage\n");
989 push_ascii_hex(&mut s, &rgb);
990 s.push('\n');
991 s.push_str("grestore\nshowpage\n");
992 s.push_str("%%EOF\n");
993 s.into_bytes()
994}
995
996pub fn encode_pdf(rgba: &[u8], width: u32, height: u32) -> Vec<u8> {
1009 let rgb = rgba_to_rgb(rgba, width, height);
1010 let mut hex = String::with_capacity(rgb.len() * 2 + 1);
1012 push_ascii_hex(&mut hex, &rgb);
1013 hex.push('>');
1014 let img_len = hex.len();
1015
1016 let content = format!("q {width} 0 0 {height} 0 0 cm /Im0 Do Q");
1018 let content_len = content.len();
1019
1020 let mut out: Vec<u8> = Vec::with_capacity(img_len + 600);
1021 let mut offsets = [0usize; 6];
1023
1024 out.extend_from_slice(b"%PDF-1.4\n");
1025
1026 offsets[1] = out.len();
1027 out.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
1028
1029 offsets[2] = out.len();
1030 out.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");
1031
1032 offsets[3] = out.len();
1033 out.extend_from_slice(
1034 format!(
1035 "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {width} {height}] \
1036 /Resources << /XObject << /Im0 4 0 R >> >> /Contents 5 0 R >>\nendobj\n"
1037 )
1038 .as_bytes(),
1039 );
1040
1041 offsets[4] = out.len();
1042 out.extend_from_slice(
1043 format!(
1044 "4 0 obj\n<< /Type /XObject /Subtype /Image /Width {width} /Height {height} \
1045 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /ASCIIHexDecode \
1046 /Length {img_len} >>\nstream\n"
1047 )
1048 .as_bytes(),
1049 );
1050 out.extend_from_slice(hex.as_bytes());
1051 out.extend_from_slice(b"\nendstream\nendobj\n");
1052
1053 offsets[5] = out.len();
1054 out.extend_from_slice(format!("5 0 obj\n<< /Length {content_len} >>\nstream\n").as_bytes());
1055 out.extend_from_slice(content.as_bytes());
1056 out.extend_from_slice(b"\nendstream\nendobj\n");
1057
1058 let xref_offset = out.len();
1060 out.extend_from_slice(b"xref\n0 6\n");
1061 out.extend_from_slice(b"0000000000 65535 f \n");
1062 for &off in &offsets[1..6] {
1063 out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
1064 }
1065 out.extend_from_slice(
1066 format!("trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n{xref_offset}\n%%EOF\n").as_bytes(),
1067 );
1068 out
1069}
1070
1071fn axis_log_flags(t: &crate::core::transform::Transform) -> [f32; 2] {
1074 [
1075 f32::from(t.x.scale == Scale::Log10),
1076 f32::from(t.y.scale == Scale::Log10),
1077 ]
1078}
1079
1080#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1089pub enum SaveFormat {
1090 Png,
1092 Ppm,
1094 Svg,
1096 Tiff,
1099 Jpeg,
1102 Eps,
1104 Pdf,
1106}
1107
1108impl SaveFormat {
1109 pub fn from_extension(ext: &str) -> Option<Self> {
1115 match ext.to_ascii_lowercase().as_str() {
1116 "png" => Some(SaveFormat::Png),
1117 "ppm" => Some(SaveFormat::Ppm),
1118 "svg" => Some(SaveFormat::Svg),
1119 "tif" | "tiff" => Some(SaveFormat::Tiff),
1120 "jpg" | "jpeg" => Some(SaveFormat::Jpeg),
1121 "eps" => Some(SaveFormat::Eps),
1122 "pdf" => Some(SaveFormat::Pdf),
1123 _ => None,
1124 }
1125 }
1126
1127 pub fn from_path(path: &Path) -> Option<Self> {
1129 path.extension()
1130 .and_then(|e| e.to_str())
1131 .and_then(Self::from_extension)
1132 }
1133}
1134
1135pub fn render_plot_rgba(
1145 render_state: &RenderState,
1146 plot: &Plot,
1147 size: (u32, u32),
1148) -> Result<Vec<u8>, SaveError> {
1149 let (w, h) = size;
1150 if w == 0 || h == 0 {
1151 return Err(SaveError::Readback("zero-size target".into()));
1152 }
1153
1154 let area = Rect::from_min_size(Pos2::ZERO, egui::vec2(w as f32, h as f32));
1157 let transform = plot.transform(area);
1158 let transform_right = plot.transform_y2(area);
1159 let ortho_left = transform.ortho_matrix();
1160 let axis_log_left = axis_log_flags(&transform);
1161 let (ortho_right, axis_log_right) = match &transform_right {
1162 Some(t) => (t.ortho_matrix(), axis_log_flags(t)),
1163 None => (ortho_left, axis_log_left),
1164 };
1165 let mut ortho_extra = Vec::with_capacity(plot.extra.len());
1169 let mut axis_log_extra = Vec::with_capacity(plot.extra.len());
1170 for i in 0..plot.extra.len() {
1171 match plot.transform_extra(i, area) {
1172 Some(t) => {
1173 ortho_extra.push(t.ortho_matrix());
1174 axis_log_extra.push(axis_log_flags(&t));
1175 }
1176 None => {
1177 ortho_extra.push(ortho_left);
1178 axis_log_extra.push(axis_log_left);
1179 }
1180 }
1181 }
1182 let bg = egui::Rgba::from(plot.data_background).to_array();
1183
1184 let renderer = render_state.renderer.read();
1185 let res: &WgpuResources = renderer
1186 .callback_resources
1187 .get()
1188 .expect("WgpuResources not installed — call siplot::install() first");
1189 res.render_to_rgba(
1190 &render_state.device,
1191 &render_state.queue,
1192 render_state.target_format,
1193 plot.id,
1194 size,
1195 bg,
1196 ortho_left,
1197 axis_log_left,
1198 ortho_right,
1199 axis_log_right,
1200 &ortho_extra,
1201 &axis_log_extra,
1202 )
1203}
1204
1205pub fn save_graph(
1209 render_state: &RenderState,
1210 plot: &Plot,
1211 size: (u32, u32),
1212 path: impl AsRef<Path>,
1213) -> Result<(), SaveError> {
1214 let (w, h) = size;
1215 let rgba = render_plot_rgba(render_state, plot, size)?;
1216 let png = encode_png(&rgba, w, h)?;
1217 std::fs::write(path, png)?;
1218 Ok(())
1219}
1220
1221pub fn save_graph_with_format(
1231 render_state: &RenderState,
1232 plot: &Plot,
1233 size: (u32, u32),
1234 path: impl AsRef<Path>,
1235 format: SaveFormat,
1236 dpi: u32,
1237) -> Result<(), SaveError> {
1238 let (w, h) = size;
1239 let rgba = render_plot_rgba(render_state, plot, size)?;
1240 match format {
1241 SaveFormat::Png => {
1242 let bytes = encode_png(&rgba, w, h)?;
1243 std::fs::write(path, bytes)?;
1244 }
1245 SaveFormat::Ppm => {
1246 let bytes = encode_ppm(&rgba, w, h);
1247 std::fs::write(path, bytes)?;
1248 }
1249 SaveFormat::Svg => {
1250 let svg = encode_svg(&rgba, w, h)?;
1251 std::fs::write(path, svg)?;
1252 }
1253 SaveFormat::Tiff => {
1254 let bytes = encode_tiff(&rgba, w, h, dpi);
1255 std::fs::write(path, bytes)?;
1256 }
1257 SaveFormat::Jpeg => {
1258 let bytes = crate::render::jpeg::encode_jpeg(&rgba, w, h, dpi);
1259 std::fs::write(path, bytes)?;
1260 }
1261 SaveFormat::Eps => {
1262 let bytes = encode_eps(&rgba, w, h);
1263 std::fs::write(path, bytes)?;
1264 }
1265 SaveFormat::Pdf => {
1266 let bytes = encode_pdf(&rgba, w, h);
1267 std::fs::write(path, bytes)?;
1268 }
1269 }
1270 Ok(())
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275 use super::*;
1276
1277 type IfdEntry = (u16, u32, [u8; 4]);
1279 type IfdTags = std::collections::HashMap<u16, IfdEntry>;
1281 type ParsedTiff = (u32, u32, IfdTags, Vec<u8>);
1283
1284 #[test]
1285 fn bytes_per_row_rounds_up_to_256() {
1286 assert_eq!(padded_bytes_per_row(1), 256); assert_eq!(padded_bytes_per_row(64), 256); assert_eq!(padded_bytes_per_row(65), 512); assert_eq!(padded_bytes_per_row(100), 512); }
1291
1292 #[test]
1293 fn rows_to_rgba8_unpads_and_passes_rgba_through() {
1294 let bpr = padded_bytes_per_row(1);
1296 let mut mapped = vec![0u8; (bpr as usize) * 2];
1297 mapped[0..4].copy_from_slice(&[10, 20, 30, 40]); mapped[bpr as usize..bpr as usize + 4].copy_from_slice(&[50, 60, 70, 80]); let out = rows_to_rgba8(&mapped, 1, 2, bpr, wgpu::TextureFormat::Rgba8UnormSrgb);
1300 assert_eq!(out, vec![10, 20, 30, 40, 50, 60, 70, 80]);
1301 }
1302
1303 #[test]
1304 fn rows_to_rgba8_swaps_bgra_to_rgba() {
1305 let bpr = padded_bytes_per_row(1);
1306 let mut mapped = vec![0u8; bpr as usize];
1307 mapped[0..4].copy_from_slice(&[30, 20, 10, 40]); let out = rows_to_rgba8(&mapped, 1, 1, bpr, wgpu::TextureFormat::Bgra8UnormSrgb);
1309 assert_eq!(out, vec![10, 20, 30, 40]); }
1311
1312 fn temp_h5(tag: &str) -> std::path::PathBuf {
1315 let mut path = std::env::temp_dir();
1316 path.push(format!("siplot_image_h5_{}_{}.h5", tag, std::process::id()));
1317 let _ = std::fs::remove_file(&path);
1318 path
1319 }
1320
1321 fn seed_dataset<T: rust_hdf5::types::H5Type>(
1323 path: &std::path::Path,
1324 name: &str,
1325 shape: &[usize],
1326 data: &[T],
1327 ) {
1328 use rust_hdf5::H5File;
1329 let file = if path.exists() {
1330 H5File::open_rw(path).unwrap()
1331 } else {
1332 H5File::create(path).unwrap()
1333 };
1334 let ds = file.new_dataset::<T>().shape(shape).create(name).unwrap();
1335 ds.write_raw(data).unwrap();
1336 file.close().unwrap();
1337 }
1338
1339 #[test]
1340 fn read_image_hdf5_roundtrips_f32() {
1341 let path = temp_h5("f32_2d");
1342 seed_dataset(&path, "/img", &[2, 3], &[0.0f32, 1.0, 2.0, 3.0, 4.0, 5.0]);
1344 let (h, w, data) = read_image_hdf5(&path, "/img").expect("read 2D f32");
1345 assert_eq!((h, w), (2, 3));
1346 assert_eq!(data, vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]);
1347 let _ = std::fs::remove_file(&path);
1348 }
1349
1350 #[test]
1351 fn read_image_hdf5_casts_f64_to_f32() {
1352 let path = temp_h5("f64_2d");
1353 seed_dataset(&path, "/img", &[1, 2], &[1.5f64, 2.5]);
1354 let (h, w, data) = read_image_hdf5(&path, "/img").expect("read 2D f64");
1355 assert_eq!((h, w), (1, 2));
1356 assert_eq!(data, vec![1.5f32, 2.5]);
1357 let _ = std::fs::remove_file(&path);
1358 }
1359
1360 #[test]
1361 fn read_image_hdf5_rejects_non_2d() {
1362 let path = temp_h5("rank");
1363 seed_dataset(&path, "/vec", &[4], &[0.0f32, 1.0, 2.0, 3.0]);
1364 assert!(read_image_hdf5(&path, "/vec").is_err());
1365 let _ = std::fs::remove_file(&path);
1366 }
1367
1368 #[test]
1369 fn read_image_hdf5_slice_reads_one_frame() {
1370 let path = temp_h5("f32_3d");
1371 seed_dataset(
1373 &path,
1374 "/stack",
1375 &[2, 2, 2],
1376 &[0.0f32, 1.0, 2.0, 3.0, 10.0, 11.0, 12.0, 13.0],
1377 );
1378 let (h, w, f0) = read_image_hdf5_slice(&path, "/stack", 0).expect("slice 0");
1379 assert_eq!((h, w), (2, 2));
1380 assert_eq!(f0, vec![0.0, 1.0, 2.0, 3.0]);
1381 let (_, _, f1) = read_image_hdf5_slice(&path, "/stack", 1).expect("slice 1");
1382 assert_eq!(f1, vec![10.0, 11.0, 12.0, 13.0]);
1383 let _ = std::fs::remove_file(&path);
1384 }
1385
1386 #[test]
1387 fn read_image_hdf5_slice_rejects_oob_and_non_3d() {
1388 let path = temp_h5("slice_reject");
1389 seed_dataset(&path, "/stack", &[2, 1, 1], &[0.0f32, 1.0]);
1390 assert!(read_image_hdf5_slice(&path, "/stack", 2).is_err());
1392 seed_dataset(&path, "/img2d", &[1, 2], &[0.0f32, 1.0]);
1394 assert!(read_image_hdf5_slice(&path, "/img2d", 0).is_err());
1395 let _ = std::fs::remove_file(&path);
1396 }
1397
1398 #[test]
1399 fn encode_png_round_trips() {
1400 let rgba: Vec<u8> = (0..16).map(|i| i as u8 * 16).collect();
1402 let png = encode_png(&rgba, 2, 2).expect("encode");
1403
1404 let decoder = png::Decoder::new(std::io::Cursor::new(&png));
1405 let mut reader = decoder.read_info().expect("read info");
1406 let mut buf = vec![0u8; reader.output_buffer_size().expect("buffer size")];
1407 let info = reader.next_frame(&mut buf).expect("frame");
1408 assert_eq!(info.width, 2);
1409 assert_eq!(info.height, 2);
1410 assert_eq!(info.color_type, png::ColorType::Rgba);
1411 assert_eq!(&buf[..rgba.len()], rgba.as_slice());
1412 }
1413
1414 #[test]
1415 fn save_format_from_extension_maps_silx_raster_formats() {
1416 assert_eq!(SaveFormat::from_extension("png"), Some(SaveFormat::Png));
1417 assert_eq!(SaveFormat::from_extension("PNG"), Some(SaveFormat::Png));
1418 assert_eq!(SaveFormat::from_extension("ppm"), Some(SaveFormat::Ppm));
1419 assert_eq!(SaveFormat::from_extension("svg"), Some(SaveFormat::Svg));
1420 assert_eq!(SaveFormat::from_extension("tif"), Some(SaveFormat::Tiff));
1421 assert_eq!(SaveFormat::from_extension("TIFF"), Some(SaveFormat::Tiff));
1422 assert_eq!(SaveFormat::from_extension("eps"), Some(SaveFormat::Eps));
1423 assert_eq!(SaveFormat::from_extension("EPS"), Some(SaveFormat::Eps));
1424 assert_eq!(SaveFormat::from_extension("pdf"), Some(SaveFormat::Pdf));
1425 assert_eq!(SaveFormat::from_extension("PDF"), Some(SaveFormat::Pdf));
1426 assert_eq!(SaveFormat::from_extension("jpg"), Some(SaveFormat::Jpeg));
1427 assert_eq!(SaveFormat::from_extension("JPEG"), Some(SaveFormat::Jpeg));
1428 }
1429
1430 #[test]
1431 fn save_format_rejects_still_unsupported_and_unknown_extensions() {
1432 assert_eq!(SaveFormat::from_extension("ps"), None);
1435 assert_eq!(SaveFormat::from_extension("bmp"), None);
1436 assert_eq!(SaveFormat::from_extension(""), None);
1437 }
1438
1439 #[test]
1440 fn save_format_from_path_uses_extension() {
1441 use std::path::Path;
1442 assert_eq!(
1443 SaveFormat::from_path(Path::new("/tmp/out.tiff")),
1444 Some(SaveFormat::Tiff)
1445 );
1446 assert_eq!(SaveFormat::from_path(Path::new("/tmp/noext")), None);
1447 }
1448
1449 #[test]
1450 fn rgba_to_rgb_drops_alpha() {
1451 let rgba = [10, 20, 30, 99, 40, 50, 60, 88];
1453 let rgb = rgba_to_rgb(&rgba, 2, 1);
1454 assert_eq!(rgb, vec![10, 20, 30, 40, 50, 60]);
1455 }
1456
1457 #[test]
1458 fn encode_ppm_header_and_pixels_round_trip() {
1459 let rgba = [1, 2, 3, 255, 4, 5, 6, 255];
1461 let ppm = encode_ppm(&rgba, 2, 1);
1462
1463 let header = b"P6\n2 1\n255\n";
1465 assert_eq!(&ppm[..header.len()], header);
1466 assert_eq!(&ppm[header.len()..], &[1, 2, 3, 4, 5, 6]);
1468 assert_eq!(ppm.len(), header.len() + 6);
1470 }
1471
1472 #[test]
1473 fn encode_eps_is_well_formed_and_hex_body_round_trips() {
1474 let rgba = [
1476 10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 100, 110, 120, 255, ];
1481 let eps = encode_eps(&rgba, 2, 2);
1482 let text = std::str::from_utf8(&eps).expect("EPS is ASCII");
1483
1484 assert!(text.starts_with("%!PS-Adobe-3.0 EPSF-3.0\n"));
1485 assert!(text.contains("%%BoundingBox: 0 0 2 2\n"));
1486 assert!(text.contains("2 2 8 [ 2 0 0 -2 0 2 ]"));
1488 assert!(text.contains("false 3 colorimage"));
1489 assert!(text.trim_end().ends_with("%%EOF"));
1490
1491 let body = text
1494 .split("colorimage\n")
1495 .nth(1)
1496 .expect("body after colorimage")
1497 .split("\ngrestore")
1498 .next()
1499 .expect("hex before grestore");
1500 let hex: String = body.chars().filter(|c| c.is_ascii_hexdigit()).collect();
1501 let decoded: Vec<u8> = hex
1502 .as_bytes()
1503 .chunks_exact(2)
1504 .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())
1505 .collect();
1506 assert_eq!(decoded, rgba_to_rgb(&rgba, 2, 2));
1507 }
1508
1509 #[test]
1510 fn encode_pdf_is_well_formed_and_xref_offsets_point_at_objects() {
1511 let rgba = [
1512 10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 100, 110, 120, 255, ];
1517 let pdf = encode_pdf(&rgba, 2, 2);
1518 let text = std::str::from_utf8(&pdf).expect("PDF body is ASCII here");
1519
1520 assert!(text.starts_with("%PDF-1.4\n"));
1521 assert!(text.contains("/Type /Catalog"));
1522 assert!(text.contains("/MediaBox [0 0 2 2]"));
1523 assert!(text.contains("/Subtype /Image /Width 2 /Height 2"));
1524 assert!(text.contains("/ColorSpace /DeviceRGB"));
1525 assert!(text.contains("/Filter /ASCIIHexDecode"));
1526 assert!(text.trim_end().ends_with("%%EOF"));
1527
1528 let sx = text.rfind("startxref\n").expect("startxref");
1530 let after = &text[sx + "startxref\n".len()..];
1531 let xref_off: usize = after
1532 .lines()
1533 .next()
1534 .unwrap()
1535 .trim()
1536 .parse()
1537 .expect("xref offset int");
1538 assert!(
1539 text[xref_off..].starts_with("xref\n"),
1540 "startxref must point at xref"
1541 );
1542
1543 let stream_at = text.find("/ASCIIHexDecode").expect("image dict");
1545 let body = &text[stream_at..];
1546 let body = &body[body.find("stream\n").expect("stream kw") + "stream\n".len()..];
1547 let hex = &body[..body.find('>').expect("hex EOD marker")];
1548 let hex: String = hex.chars().filter(|c| c.is_ascii_hexdigit()).collect();
1549 let decoded: Vec<u8> = hex
1550 .as_bytes()
1551 .chunks_exact(2)
1552 .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())
1553 .collect();
1554 assert_eq!(decoded, rgba_to_rgb(&rgba, 2, 2));
1555 }
1556
1557 #[test]
1560 fn mask_npy_round_trips_bytes_and_shape() {
1561 let data: Vec<u8> = vec![0, 1, 2, 250, 254, 255];
1564 let bytes = encode_mask_npy(2, 3, &data);
1565 let (h, w, out) = decode_mask_npy(&bytes).expect("decode");
1566 assert_eq!((h, w), (2, 3));
1567 assert_eq!(out, data);
1568 }
1569
1570 #[test]
1571 fn mask_npy_header_is_valid_v1_format() {
1572 let data = vec![7u8; 4];
1573 let bytes = encode_mask_npy(2, 2, &data);
1574 assert_eq!(&bytes[0..6], b"\x93NUMPY");
1576 assert_eq!(&bytes[6..8], &[1, 0]);
1577 let header_len = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
1579 let preamble = 10 + header_len;
1580 assert_eq!(preamble % 64, 0, "preamble {preamble} not 64-aligned");
1581 let header = std::str::from_utf8(&bytes[10..preamble]).expect("ascii header");
1583 assert!(header.contains("'descr': '|u1'"));
1584 assert!(header.contains("'fortran_order': False"));
1585 assert!(header.contains("'shape': (2, 2)"));
1586 assert!(header.ends_with('\n'));
1587 assert_eq!(&bytes[preamble..], data.as_slice());
1589 }
1590
1591 #[test]
1592 fn mask_npy_rejects_bad_magic_and_non_uint8() {
1593 let err = decode_mask_npy(b"not-a-npy-file-at-all").unwrap_err();
1595 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
1596
1597 let mut bytes = encode_mask_npy(1, 1, &[0]);
1599 let header_len = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
1600 let header = std::str::from_utf8(&bytes[10..10 + header_len])
1601 .unwrap()
1602 .replace("|u1", "<f8");
1603 bytes.splice(10..10 + header_len, header.bytes());
1604 let err = decode_mask_npy(&bytes).unwrap_err();
1605 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
1606 }
1607
1608 #[test]
1609 fn mask_npy_rejects_non_2d_shape() {
1610 let mut bytes = encode_mask_npy(1, 1, &[0]);
1612 let header_len = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
1613 let header = std::str::from_utf8(&bytes[10..10 + header_len])
1614 .unwrap()
1615 .replace("(1, 1)", "(1, 1, 1)");
1616 let mut header = header;
1618 while header.len() < header_len {
1619 header.insert(header.len() - 1, ' ');
1620 }
1621 let header = &header[..header_len];
1622 bytes.splice(10..10 + header_len, header.bytes());
1623 let err = decode_mask_npy(&bytes).unwrap_err();
1624 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
1625 }
1626
1627 #[test]
1628 fn mask_edf_round_trips_bytes_and_shape() {
1629 let data: Vec<u8> = vec![0, 10, 32, 250, 254, 255];
1634 let bytes = encode_mask_edf(2, 3, &data);
1635 let (h, w, out) = decode_mask_edf(&bytes).expect("decode");
1636 assert_eq!((h, w), (2, 3));
1637 assert_eq!(out, data);
1638 }
1639
1640 #[test]
1641 fn mask_edf_header_is_512_aligned_and_self_describing() {
1642 let data = vec![7u8; 6];
1643 let bytes = encode_mask_edf(2, 3, &data);
1644 let body_start = bytes.len() - data.len();
1646 assert_eq!(
1647 body_start % 512,
1648 0,
1649 "header block {body_start} not 512-aligned"
1650 );
1651 let header = std::str::from_utf8(&bytes[..body_start]).expect("ascii header");
1652 assert!(header.starts_with('{'));
1653 assert!(header.contains("DataType = UnsignedByte ;"));
1654 assert!(header.contains("Dim_1 = 3 ;")); assert!(header.contains("Dim_2 = 2 ;")); assert!(header.contains("Size = 6 ;"));
1657 assert!(header.trim_end().ends_with('}'));
1658 assert_eq!(&bytes[body_start..], data.as_slice());
1660 }
1661
1662 #[test]
1663 fn mask_edf_rejects_non_byte_type_and_truncated_body() {
1664 let bytes = encode_mask_edf(1, 1, &[0]);
1666 let header_end = bytes.len() - 1;
1667 let header = std::str::from_utf8(&bytes[..header_end])
1668 .unwrap()
1669 .replace("UnsignedByte", "FloatValue ");
1670 let mut tampered = header.into_bytes();
1671 tampered.push(0);
1672 let err = decode_mask_edf(&tampered).unwrap_err();
1673 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
1674
1675 let mut short = encode_mask_edf(4, 4, &[1u8; 16]);
1677 short.truncate(short.len() - 8);
1678 let err = decode_mask_edf(&short).unwrap_err();
1679 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
1680 }
1681
1682 #[test]
1683 fn mask_tiff_round_trips_bytes_and_shape() {
1684 let data: Vec<u8> = vec![0, 1, 127, 200, 254, 255];
1687 let bytes = encode_mask_tiff(2, 3, &data).expect("encode");
1688 let (h, w, out) = decode_mask_tiff(&bytes).expect("decode");
1689 assert_eq!((h, w), (2, 3));
1690 assert_eq!(out, data);
1691 }
1692
1693 #[test]
1694 fn mask_tiff_rejects_a_multichannel_image() {
1695 use tiff::encoder::{TiffEncoder, colortype::RGB8};
1699 let mut cursor = std::io::Cursor::new(Vec::new());
1700 TiffEncoder::new(&mut cursor)
1701 .unwrap()
1702 .write_image::<RGB8>(2, 2, &[0u8; 12])
1703 .unwrap();
1704 let rgb_tiff = cursor.into_inner();
1705 let err = decode_mask_tiff(&rgb_tiff).unwrap_err();
1706 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
1707 }
1708
1709 #[test]
1710 fn mask_msk_round_trips_as_binary_with_odd_width() {
1711 let (h, w) = (2u32, 33u32);
1715 let mut data = vec![0u8; (h * w) as usize];
1716 data[0] = 5; data[32] = 200; data[(w as usize) + 1] = 1; let bytes = encode_mask_msk(h, w, &data);
1720 assert_eq!(bytes.len(), 1024 + 16);
1722 let (dh, dw, out) = decode_mask_msk(&bytes).expect("decode");
1723 assert_eq!((dh, dw), (h, w));
1724 let mut expected = vec![0u8; (h * w) as usize];
1725 expected[0] = 1;
1726 expected[32] = 1;
1727 expected[(w as usize) + 1] = 1;
1728 assert_eq!(out, expected);
1729 }
1730
1731 #[test]
1732 fn mask_msk_rejects_bad_magic_and_short_header() {
1733 assert_eq!(
1737 decode_mask_msk(&[0u8; 16]).unwrap_err().kind(),
1738 std::io::ErrorKind::InvalidData
1739 );
1740 let mut buf = vec![0u8; 1024 + 4];
1741 buf[0] = b'X'; assert_eq!(
1743 decode_mask_msk(&buf).unwrap_err().kind(),
1744 std::io::ErrorKind::InvalidData
1745 );
1746 }
1747
1748 #[test]
1749 fn mask_msk_matches_fabio_reference_bytes() {
1750 let data: Vec<u8> = vec![0, 1, 0, 1, 1, 0, 0, 1, 0, 0];
1757 let mut expected = vec![0u8; 1024 + 8];
1758 expected[0] = b'M';
1759 expected[4] = b'A';
1760 expected[8] = b'S';
1761 expected[12] = b'K';
1762 expected[16] = 5; expected[20] = 2; expected[24] = 1;
1765 expected[1024] = 0x1A;
1766 expected[1028] = 0x04;
1767 assert_eq!(encode_mask_msk(2, 5, &data), expected);
1768 let (h, w, out) = decode_mask_msk(&expected).expect("decode");
1770 assert_eq!((h, w), (2, 5));
1771 assert_eq!(out, data);
1772 }
1773
1774 #[test]
1775 fn base64_encode_matches_known_vector() {
1776 assert_eq!(base64_encode(b""), "");
1778 assert_eq!(base64_encode(b"f"), "Zg==");
1779 assert_eq!(base64_encode(b"fo"), "Zm8=");
1780 assert_eq!(base64_encode(b"foo"), "Zm9v");
1781 assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
1782 assert_eq!(base64_encode(b"fooba"), "Zm9vYmE=");
1783 assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
1784 }
1785
1786 #[test]
1787 fn encode_svg_is_well_formed_with_size_and_png_payload() {
1788 let rgba = [
1789 11, 22, 33, 255, 44, 55, 66, 255, 77, 88, 99, 255, 1, 2, 3, 255,
1790 ];
1791 let svg = encode_svg(&rgba, 2, 2).expect("svg");
1792
1793 assert!(svg.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>"));
1795 assert!(svg.contains("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\""));
1796 assert!(svg.contains("width=\"2\""));
1798 assert!(svg.contains("height=\"2\""));
1799 assert!(svg.contains("<image xlink:href=\"data:image/png;base64,"));
1801 assert!(svg.contains("x=\"0\""));
1802 assert!(svg.contains("y=\"0\""));
1803 assert!(svg.contains("id=\"image\" />"));
1804 assert!(svg.trim_end().ends_with("</svg>"));
1805
1806 let marker = "base64,";
1809 let start = svg.find(marker).expect("data uri") + marker.len();
1810 let end = svg[start..].find('"').expect("end quote") + start;
1811 let b64 = &svg[start..end];
1812 let png_bytes = base64_decode_for_test(b64);
1813 let decoder = png::Decoder::new(std::io::Cursor::new(&png_bytes));
1814 let mut reader = decoder.read_info().expect("read info");
1815 let mut buf = vec![0u8; reader.output_buffer_size().expect("buffer size")];
1816 let info = reader.next_frame(&mut buf).expect("frame");
1817 assert_eq!(info.width, 2);
1818 assert_eq!(info.height, 2);
1819 assert_eq!(info.color_type, png::ColorType::Rgb);
1820 let expected_rgb = rgba_to_rgb(&rgba, 2, 2);
1821 assert_eq!(&buf[..expected_rgb.len()], expected_rgb.as_slice());
1822 }
1823
1824 fn base64_decode_for_test(s: &str) -> Vec<u8> {
1826 fn val(c: u8) -> Option<u8> {
1827 match c {
1828 b'A'..=b'Z' => Some(c - b'A'),
1829 b'a'..=b'z' => Some(c - b'a' + 26),
1830 b'0'..=b'9' => Some(c - b'0' + 52),
1831 b'+' => Some(62),
1832 b'/' => Some(63),
1833 _ => None,
1834 }
1835 }
1836 let mut out = Vec::new();
1837 let mut acc = 0u32;
1838 let mut bits = 0u32;
1839 for &c in s.as_bytes() {
1840 if c == b'=' {
1841 break;
1842 }
1843 let Some(v) = val(c) else { continue };
1844 acc = (acc << 6) | v as u32;
1845 bits += 6;
1846 if bits >= 8 {
1847 bits -= 8;
1848 out.push((acc >> bits) as u8);
1849 }
1850 }
1851 out
1852 }
1853
1854 fn parse_tiff(bytes: &[u8]) -> ParsedTiff {
1861 assert_eq!(&bytes[0..2], b"II", "byte order must be little-endian");
1862 assert_eq!(u16::from_le_bytes([bytes[2], bytes[3]]), 42, "magic 42");
1863 let ifd_off = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize;
1864 let n = u16::from_le_bytes([bytes[ifd_off], bytes[ifd_off + 1]]) as usize;
1865 let mut tags = std::collections::HashMap::new();
1866 for i in 0..n {
1867 let base = ifd_off + 2 + i * 12;
1868 let tag = u16::from_le_bytes([bytes[base], bytes[base + 1]]);
1869 let typ = u16::from_le_bytes([bytes[base + 2], bytes[base + 3]]);
1870 let count = u32::from_le_bytes([
1871 bytes[base + 4],
1872 bytes[base + 5],
1873 bytes[base + 6],
1874 bytes[base + 7],
1875 ]);
1876 let val = [
1877 bytes[base + 8],
1878 bytes[base + 9],
1879 bytes[base + 10],
1880 bytes[base + 11],
1881 ];
1882 tags.insert(tag, (typ, count, val));
1883 }
1884 let next_off = ifd_off + 2 + n * 12;
1886 assert_eq!(
1887 u32::from_le_bytes([
1888 bytes[next_off],
1889 bytes[next_off + 1],
1890 bytes[next_off + 2],
1891 bytes[next_off + 3]
1892 ]),
1893 0,
1894 "single-image TIFF: next IFD offset is 0"
1895 );
1896
1897 let width = le_u32(&tags[&256].2);
1898 let height = le_u32(&tags[&257].2);
1899 let strip_off = le_u32(&tags[&273].2) as usize;
1900 let strip_len = le_u32(&tags[&279].2) as usize;
1901 let pixels = bytes[strip_off..strip_off + strip_len].to_vec();
1902 (width, height, tags, pixels)
1903 }
1904
1905 fn le_u32(v: &[u8; 4]) -> u32 {
1906 u32::from_le_bytes(*v)
1907 }
1908
1909 fn le_short(v: &[u8; 4]) -> u16 {
1911 u16::from_le_bytes([v[0], v[1]])
1912 }
1913
1914 fn read_rational(bytes: &[u8], off: u32) -> (u32, u32) {
1917 let o = off as usize;
1918 let num = u32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]]);
1919 let den = u32::from_le_bytes([bytes[o + 4], bytes[o + 5], bytes[o + 6], bytes[o + 7]]);
1920 (num, den)
1921 }
1922
1923 #[test]
1924 fn encode_tiff_header_tags_and_pixels_round_trip() {
1925 let rgba = [
1927 10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 100, 110, 120, 255,
1928 ];
1929 let tiff = encode_tiff(&rgba, 2, 2, 96);
1930 let (w, h, tags, pixels) = parse_tiff(&tiff);
1931
1932 assert_eq!((w, h), (2, 2));
1933 assert_eq!(le_short(&tags[&259].2), 1, "Compression = none");
1935 assert_eq!(le_short(&tags[&262].2), 2, "Photometric = RGB");
1936 assert_eq!(le_short(&tags[&277].2), 3, "SamplesPerPixel = 3");
1937 assert_eq!(le_u32(&tags[&278].2), 2, "RowsPerStrip = height");
1938 assert_eq!(le_u32(&tags[&279].2), 2 * 2 * 3, "StripByteCounts = w*h*3");
1939
1940 let (typ, count, bits_val) = tags[&258];
1942 assert_eq!(typ, 3);
1943 assert_eq!(count, 3);
1944 let bits_off = le_u32(&bits_val) as usize;
1945 assert_eq!(
1946 &tiff[bits_off..bits_off + 6],
1947 &[8, 0, 8, 0, 8, 0],
1948 "BitsPerSample = 8,8,8"
1949 );
1950
1951 let expected_rgb = rgba_to_rgb(&rgba, 2, 2);
1953 assert_eq!(pixels, expected_rgb);
1954 }
1955
1956 #[test]
1957 fn encode_tiff_resolution_tags_reflect_dpi() {
1958 let rgba = [1, 2, 3, 255];
1959 let tiff = encode_tiff(&rgba, 1, 1, 300);
1960 let (_, _, tags, _) = parse_tiff(&tiff);
1961
1962 assert_eq!(le_short(&tags[&296].2), 2, "ResolutionUnit = inch");
1964 let xres = read_rational(&tiff, le_u32(&tags[&282].2));
1966 let yres = read_rational(&tiff, le_u32(&tags[&283].2));
1967 assert_eq!(xres, (300, 1), "XResolution = 300 dpi");
1968 assert_eq!(yres, (300, 1), "YResolution = 300 dpi");
1969 }
1970
1971 #[test]
1972 fn encode_tiff_clamps_zero_dpi_to_one() {
1973 let rgba = [1, 2, 3, 255];
1975 let tiff = encode_tiff(&rgba, 1, 1, 0);
1976 let (_, _, tags, _) = parse_tiff(&tiff);
1977 assert_eq!(read_rational(&tiff, le_u32(&tags[&282].2)), (1, 1));
1978 }
1979}