use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use crate::common::error::{BioFormatsError, Result};
use crate::common::metadata::{DimensionOrder, ImageMetadata, MetadataValue};
use crate::common::pixel_type::PixelType;
use crate::common::reader::FormatReader;
use crate::common::region::{crop_full_plane, validate_region};
#[derive(Clone, Copy, PartialEq, Eq)]
enum AmiraCompression {
None,
HxZip,
HxByteRLE,
}
struct AmiraHeader {
nx: u32,
ny: u32,
nz: u32,
pixel_type: PixelType,
data_offset: u64,
little_endian: bool,
ascii: bool,
compression: AmiraCompression,
}
fn parse_amira_header(path: &Path) -> Result<AmiraHeader> {
let f = File::open(path).map_err(BioFormatsError::Io)?;
let mut reader = BufReader::new(f);
let mut nx = 0u32;
let mut ny = 0u32;
let mut nz = 0u32;
let mut pixel_type = PixelType::Uint8;
let mut little_endian = false;
let mut ascii = false;
let mut data_section: u32 = 1; let mut compression = AmiraCompression::None;
let mut line = String::new();
loop {
line.clear();
let n = reader.read_line(&mut line).map_err(BioFormatsError::Io)?;
if n == 0 {
break;
}
let t = line.trim();
if t.starts_with("# AmiraMesh") || t.starts_with("# Avizo") {
let up = t.to_ascii_uppercase();
if up.contains("BINARY-LITTLE-ENDIAN") {
little_endian = true;
} else if up.contains("BINARY") {
little_endian = false;
} else if up.contains("ASCII") {
ascii = true;
little_endian = true; }
}
if t.starts_with("define Lattice") {
let parts: Vec<&str> = t.split_ascii_whitespace().collect();
if parts.len() >= 5 {
nx = parts[2].parse().map_err(|_| {
BioFormatsError::Format(format!(
"Amira Mesh: invalid lattice width {:?}",
parts[2]
))
})?;
ny = parts[3].parse().map_err(|_| {
BioFormatsError::Format(format!(
"Amira Mesh: invalid lattice height {:?}",
parts[3]
))
})?;
nz = parts[4].parse().map_err(|_| {
BioFormatsError::Format(format!(
"Amira Mesh: invalid lattice depth {:?}",
parts[4]
))
})?;
} else if parts.len() >= 4 {
nx = parts[2].parse().map_err(|_| {
BioFormatsError::Format(format!(
"Amira Mesh: invalid lattice width {:?}",
parts[2]
))
})?;
ny = parts[3].parse().map_err(|_| {
BioFormatsError::Format(format!(
"Amira Mesh: invalid lattice height {:?}",
parts[3]
))
})?;
nz = 1;
}
}
if t.starts_with("Lattice") && t.contains("Data") {
let lo = t.to_ascii_lowercase();
pixel_type = if lo.contains("double") {
PixelType::Float64
} else if lo.contains("float") {
PixelType::Float32
} else if lo.contains("ushort") || lo.contains("unsigned short") {
PixelType::Uint16
} else if lo.contains("short") {
PixelType::Int16
} else if lo.contains("int") {
PixelType::Int32
} else if lo.contains("byte") {
PixelType::Uint8
} else {
return Err(BioFormatsError::UnsupportedFormat(format!(
"Amira Mesh: unsupported lattice data type in {t:?}"
)));
};
if let Some(at_pos) = t.rfind('@') {
let rest = t[at_pos + 1..].trim();
let (num_part, comp_part) = match rest.find('(') {
Some(p) => (rest[..p].trim(), Some(&rest[p + 1..])),
None => (rest, None),
};
if let Ok(n) = num_part.parse::<u32>() {
data_section = n;
}
if let Some(comp) = comp_part {
let comp = comp.trim_end_matches(')').trim();
if comp.starts_with("HxZip,") {
compression = AmiraCompression::HxZip;
} else if comp.starts_with("HxByteRLE,") {
compression = AmiraCompression::HxByteRLE;
} else if !comp.is_empty() {
return Err(BioFormatsError::UnsupportedFormat(format!(
"Amira Mesh: unsupported stream compression {comp:?}"
)));
}
}
}
}
if t == format!("@{}", data_section) {
let data_offset = reader.stream_position().map_err(BioFormatsError::Io)?;
validate_positive_dims("Amira Mesh", nx, ny, nz)?;
return Ok(AmiraHeader {
nx,
ny,
nz,
pixel_type,
data_offset,
little_endian,
ascii,
compression,
});
}
}
Err(BioFormatsError::Format(
"Amira Mesh: could not find data section".into(),
))
}
fn validate_positive_dims(format: &str, width: u32, height: u32, depth: u32) -> Result<()> {
if width == 0 || height == 0 || depth == 0 {
return Err(BioFormatsError::UnsupportedFormat(format!(
"{format}: invalid non-positive dimensions {width}x{height}x{depth}"
)));
}
Ok(())
}
fn checked_plane_bytes(format: &str, meta: &ImageMetadata) -> Result<u64> {
(meta.size_x as u64)
.checked_mul(meta.size_y as u64)
.and_then(|pixels| pixels.checked_mul(meta.pixel_type.bytes_per_sample() as u64))
.ok_or_else(|| BioFormatsError::Format(format!("{format}: plane size overflows")))
}
fn validate_payload_len(
format: &str,
path: &Path,
data_offset: u64,
meta: &ImageMetadata,
) -> Result<()> {
let file_len = std::fs::metadata(path).map_err(BioFormatsError::Io)?.len();
let required_len = data_offset
.checked_add(
checked_plane_bytes(format, meta)?
.checked_mul(meta.image_count as u64)
.ok_or_else(|| {
BioFormatsError::Format(format!("{format}: payload size overflows"))
})?,
)
.ok_or_else(|| BioFormatsError::Format(format!("{format}: payload size overflows")))?;
if file_len < required_len {
return Err(BioFormatsError::UnsupportedFormat(format!(
"{format}: pixel payload is shorter than declared ({file_len} < {required_len})"
)));
}
Ok(())
}
pub struct AmiraReader {
path: Option<PathBuf>,
meta: Option<ImageMetadata>,
data_offset: u64,
ascii: bool,
compression: AmiraCompression,
decoded_stack: Option<Vec<u8>>,
}
impl AmiraReader {
pub fn new() -> Self {
AmiraReader {
path: None,
meta: None,
data_offset: 0,
ascii: false,
compression: AmiraCompression::None,
decoded_stack: None,
}
}
fn decode_byte_rle(data: &[u8], expected: usize) -> Result<Vec<u8>> {
let mut out = Vec::with_capacity(expected);
let mut i = 0;
while out.len() < expected && i < data.len() {
let insn = data[i] as i8;
i += 1;
if insn < 0 {
let count = (insn as u8 & 0x7f) as usize;
if i + count > data.len() {
return Err(BioFormatsError::InvalidData(
"Amira HxByteRLE: literal run overruns input".into(),
));
}
out.extend_from_slice(&data[i..i + count]);
i += count;
} else {
let count = insn as usize;
if i >= data.len() {
return Err(BioFormatsError::InvalidData(
"Amira HxByteRLE: fill run missing byte".into(),
));
}
let byte = data[i];
i += 1;
out.resize(out.len() + count, byte);
}
}
if out.len() < expected {
return Err(BioFormatsError::InvalidData(format!(
"Amira HxByteRLE: decoded {} bytes, expected {expected}",
out.len()
)));
}
out.truncate(expected);
Ok(out)
}
fn decode_stack(&self) -> Result<Vec<u8>> {
let meta = self.meta.as_ref().ok_or(BioFormatsError::NotInitialized)?;
let path = self.path.as_ref().ok_or(BioFormatsError::NotInitialized)?;
let expected = (checked_plane_bytes("Amira Mesh", meta)?
.checked_mul(meta.image_count as u64)
.ok_or_else(|| BioFormatsError::Format("Amira Mesh: payload size overflows".into()))?)
as usize;
let mut f = File::open(path).map_err(BioFormatsError::Io)?;
f.seek(SeekFrom::Start(self.data_offset))
.map_err(BioFormatsError::Io)?;
let mut compressed = Vec::new();
f.read_to_end(&mut compressed).map_err(BioFormatsError::Io)?;
let decoded = match self.compression {
AmiraCompression::HxZip => crate::common::codec::decompress_deflate(&compressed)?,
AmiraCompression::HxByteRLE => Self::decode_byte_rle(&compressed, expected)?,
AmiraCompression::None => compressed,
};
if decoded.len() < expected {
return Err(BioFormatsError::InvalidData(format!(
"Amira Mesh: decompressed stack is shorter than declared ({} < {expected})",
decoded.len()
)));
}
Ok(decoded)
}
fn read_ascii_plane(&self, plane_index: u32) -> Result<Vec<u8>> {
let meta = self.meta.as_ref().ok_or(BioFormatsError::NotInitialized)?;
let pixel_type = meta.pixel_type;
let bps = pixel_type.bytes_per_sample();
let count = (meta.size_x * meta.size_y) as usize;
let path = self.path.as_ref().ok_or(BioFormatsError::NotInitialized)?;
let f = File::open(path).map_err(BioFormatsError::Io)?;
let mut reader = BufReader::new(f);
reader
.seek(SeekFrom::Start(self.data_offset))
.map_err(BioFormatsError::Io)?;
let mut text = String::new();
reader
.read_to_string(&mut text)
.map_err(BioFormatsError::Io)?;
let skip = plane_index as usize * count;
let tokens: Vec<&str> = text
.split_ascii_whitespace()
.skip(skip)
.take(count)
.collect();
if tokens.len() != count {
return Err(BioFormatsError::InvalidData(format!(
"Amira ASCII plane {plane_index} has {} samples, expected {count}",
tokens.len()
)));
}
let mut out = vec![0u8; count * bps];
for (i, tok) in tokens.into_iter().enumerate() {
let dst = &mut out[i * bps..(i + 1) * bps];
match pixel_type {
PixelType::Float32 => {
let v: f32 = tok.parse().map_err(|_| {
BioFormatsError::InvalidData(format!(
"Amira ASCII plane {plane_index} contains non-Float32 sample {tok:?}"
))
})?;
dst.copy_from_slice(&v.to_le_bytes());
}
PixelType::Float64 => {
let v: f64 = tok.parse().map_err(|_| {
BioFormatsError::InvalidData(format!(
"Amira ASCII plane {plane_index} contains non-Float64 sample {tok:?}"
))
})?;
dst.copy_from_slice(&v.to_le_bytes());
}
PixelType::Int32 => {
let v: i32 = tok.parse().map_err(|_| {
BioFormatsError::InvalidData(format!(
"Amira ASCII plane {plane_index} contains non-Int32 sample {tok:?}"
))
})?;
dst.copy_from_slice(&v.to_le_bytes());
}
PixelType::Uint16 => {
let v: u16 = tok.parse().map_err(|_| {
BioFormatsError::InvalidData(format!(
"Amira ASCII plane {plane_index} contains non-Uint16 sample {tok:?}"
))
})?;
dst.copy_from_slice(&v.to_le_bytes());
}
PixelType::Int16 => {
let v: i16 = tok.parse().map_err(|_| {
BioFormatsError::InvalidData(format!(
"Amira ASCII plane {plane_index} contains non-Int16 sample {tok:?}"
))
})?;
dst.copy_from_slice(&v.to_le_bytes());
}
_ => {
let v: i64 = tok.parse().map_err(|_| {
BioFormatsError::InvalidData(format!(
"Amira ASCII plane {plane_index} contains non-integer sample {tok:?}"
))
})?;
dst[0] = v as u8;
}
}
}
Ok(out)
}
}
impl Default for AmiraReader {
fn default() -> Self {
Self::new()
}
}
impl FormatReader for AmiraReader {
fn is_this_type_by_name(&self, path: &Path) -> bool {
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase());
matches!(ext.as_deref(), Some("am") | Some("amiramesh"))
}
fn is_this_type_by_bytes(&self, header: &[u8]) -> bool {
let s = std::str::from_utf8(&header[..header.len().min(32)]).unwrap_or("");
s.starts_with("# AmiraMesh") || s.starts_with("# Avizo")
}
fn set_id(&mut self, path: &Path) -> Result<()> {
self.path = None;
self.meta = None;
self.data_offset = 0;
self.ascii = false;
self.compression = AmiraCompression::None;
self.decoded_stack = None;
let hdr = parse_amira_header(path)?;
let image_count = hdr.nz;
let little_endian = if hdr.ascii { true } else { hdr.little_endian };
let meta = ImageMetadata {
size_x: hdr.nx,
size_y: hdr.ny,
size_z: hdr.nz,
size_c: 1,
size_t: 1,
pixel_type: hdr.pixel_type,
bits_per_pixel: (hdr.pixel_type.bytes_per_sample() * 8) as u8,
image_count,
dimension_order: DimensionOrder::XYZCT,
is_rgb: false,
is_interleaved: false,
is_indexed: false,
is_little_endian: little_endian,
resolution_count: 1,
series_metadata: HashMap::new(),
lookup_table: None,
modulo_z: None,
modulo_c: None,
modulo_t: None,
};
if !hdr.ascii && hdr.compression == AmiraCompression::None {
validate_payload_len("Amira Mesh", path, hdr.data_offset, &meta)?;
}
self.meta = Some(meta);
self.data_offset = hdr.data_offset;
self.ascii = hdr.ascii;
self.compression = hdr.compression;
self.path = Some(path.to_path_buf());
Ok(())
}
fn close(&mut self) -> Result<()> {
self.path = None;
self.meta = None;
self.decoded_stack = None;
Ok(())
}
fn series_count(&self) -> usize {
usize::from(self.meta.is_some())
}
fn set_series(&mut self, s: usize) -> Result<()> {
if self.meta.is_none() || s != 0 {
Err(BioFormatsError::SeriesOutOfRange(s))
} else {
Ok(())
}
}
fn series(&self) -> usize {
0
}
fn metadata(&self) -> &ImageMetadata {
self.meta
.as_ref()
.unwrap_or(crate::common::reader::uninitialized_metadata())
}
fn open_bytes(&mut self, plane_index: u32) -> Result<Vec<u8>> {
let meta = self.meta.as_ref().ok_or(BioFormatsError::NotInitialized)?;
if plane_index >= meta.image_count {
return Err(BioFormatsError::PlaneOutOfRange(plane_index));
}
if self.ascii {
return self.read_ascii_plane(plane_index);
}
let plane_bytes = checked_plane_bytes("Amira Mesh", meta)? as usize;
if self.compression != AmiraCompression::None {
if self.decoded_stack.is_none() {
self.decoded_stack = Some(self.decode_stack()?);
}
let stack = self.decoded_stack.as_ref().unwrap();
let start = plane_index as usize * plane_bytes;
let end = start + plane_bytes;
if end > stack.len() {
return Err(BioFormatsError::PlaneOutOfRange(plane_index));
}
return Ok(stack[start..end].to_vec());
}
let offset = self.data_offset + plane_index as u64 * plane_bytes as u64;
let path = self.path.as_ref().ok_or(BioFormatsError::NotInitialized)?;
let mut f = File::open(path).map_err(BioFormatsError::Io)?;
f.seek(SeekFrom::Start(offset))
.map_err(BioFormatsError::Io)?;
let mut buf = vec![0u8; plane_bytes];
f.read_exact(&mut buf).map_err(BioFormatsError::Io)?;
Ok(buf)
}
fn open_bytes_region(
&mut self,
plane_index: u32,
x: u32,
y: u32,
w: u32,
h: u32,
) -> Result<Vec<u8>> {
{
let meta = self.meta.as_ref().ok_or(BioFormatsError::NotInitialized)?;
validate_region("Amira", meta.size_x, meta.size_y, x, y, w, h)?;
}
let full = self.open_bytes(plane_index)?;
let meta = self.meta.as_ref().ok_or(BioFormatsError::NotInitialized)?;
crop_full_plane("Amira", &full, meta, 1, x, y, w, h)
}
fn open_thumb_bytes(&mut self, plane_index: u32) -> Result<Vec<u8>> {
let meta = self.meta.as_ref().ok_or(BioFormatsError::NotInitialized)?;
let (tw, th) = (meta.size_x.min(256), meta.size_y.min(256));
let (tx, ty) = ((meta.size_x - tw) / 2, (meta.size_y - th) / 2);
self.open_bytes_region(plane_index, tx, ty, tw, th)
}
}
fn r_f32_le_w(b: &[u8], off: usize) -> f32 {
f32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
}
fn parse_spider_header(path: &Path) -> Result<(u32, u32, u32, u64)> {
let mut f = File::open(path).map_err(BioFormatsError::Io)?;
let mut hdr = [0u8; 256]; f.read_exact(&mut hdr).map_err(BioFormatsError::Io)?;
let nslice = spider_positive_u32(r_f32_le_w(&hdr, 0), "NSLICE")?;
let nrow = spider_positive_u32(r_f32_le_w(&hdr, 4), "NROW")?;
let iform = r_f32_le_w(&hdr, 16) as i32;
let nsam = spider_positive_u32(r_f32_le_w(&hdr, 44), "NSAM")?;
let labbyt = r_f32_le_w(&hdr, 84) as u64;
let width = nsam;
let height = nrow;
let nz = match iform {
1 | -1 => 1, 3 | -3 => nslice, 11 | -11 | -21 | -22 => nslice, _ => {
return Err(BioFormatsError::UnsupportedFormat(format!(
"Spider: unsupported IFORM {iform}"
)))
}
};
let header_size = if labbyt > 0 {
labbyt
} else {
let labrec = r_f32_le_w(&hdr, 48) as u64;
labrec * nsam as u64 * 4
};
Ok((width, height, nz, header_size))
}
fn spider_positive_u32(value: f32, label: &str) -> Result<u32> {
if !value.is_finite() || value <= 0.0 || value.fract() != 0.0 || value > u32::MAX as f32 {
return Err(BioFormatsError::UnsupportedFormat(format!(
"Spider: invalid {label} dimension {value}"
)));
}
Ok(value as u32)
}
pub struct SpiderReader {
path: Option<PathBuf>,
meta: Option<ImageMetadata>,
data_offset: u64,
}
impl SpiderReader {
pub fn new() -> Self {
SpiderReader {
path: None,
meta: None,
data_offset: 0,
}
}
}
impl Default for SpiderReader {
fn default() -> Self {
Self::new()
}
}
impl FormatReader for SpiderReader {
fn is_this_type_by_name(&self, path: &Path) -> bool {
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase());
matches!(ext.as_deref(), Some("spi") | Some("xmp"))
}
fn is_this_type_by_bytes(&self, header: &[u8]) -> bool {
if header.len() < 52 {
return false;
}
let iform = r_f32_le_w(header, 16) as i32;
let nsam = r_f32_le_w(header, 44);
let nrow = r_f32_le_w(header, 4);
matches!(iform, 1 | 3 | -1 | -3 | 11 | -11 | -21 | -22) && nsam > 0.0 && nrow > 0.0
}
fn set_id(&mut self, path: &Path) -> Result<()> {
self.path = None;
self.meta = None;
self.data_offset = 0;
let (width, height, nz, data_offset) = parse_spider_header(path)?;
let image_count = nz;
let meta = ImageMetadata {
size_x: width,
size_y: height,
size_z: nz,
size_c: 1,
size_t: 1,
pixel_type: PixelType::Float32,
bits_per_pixel: 32,
image_count,
dimension_order: DimensionOrder::XYZCT,
is_rgb: false,
is_interleaved: false,
is_indexed: false,
is_little_endian: true,
resolution_count: 1,
series_metadata: {
let mut m = HashMap::new();
m.insert("format".into(), MetadataValue::String("Spider EM".into()));
m
},
lookup_table: None,
modulo_z: None,
modulo_c: None,
modulo_t: None,
};
validate_payload_len("Spider", path, data_offset, &meta)?;
self.meta = Some(meta);
self.data_offset = data_offset;
self.path = Some(path.to_path_buf());
Ok(())
}
fn close(&mut self) -> Result<()> {
self.path = None;
self.meta = None;
Ok(())
}
fn series_count(&self) -> usize {
usize::from(self.meta.is_some())
}
fn set_series(&mut self, s: usize) -> Result<()> {
if self.meta.is_none() || s != 0 {
Err(BioFormatsError::SeriesOutOfRange(s))
} else {
Ok(())
}
}
fn series(&self) -> usize {
0
}
fn metadata(&self) -> &ImageMetadata {
self.meta
.as_ref()
.unwrap_or(crate::common::reader::uninitialized_metadata())
}
fn open_bytes(&mut self, plane_index: u32) -> Result<Vec<u8>> {
let meta = self.meta.as_ref().ok_or(BioFormatsError::NotInitialized)?;
if plane_index >= meta.image_count {
return Err(BioFormatsError::PlaneOutOfRange(plane_index));
}
let plane_bytes = checked_plane_bytes("Spider", meta)? as usize;
let offset = self.data_offset + plane_index as u64 * plane_bytes as u64;
let path = self.path.as_ref().ok_or(BioFormatsError::NotInitialized)?;
let mut f = File::open(path).map_err(BioFormatsError::Io)?;
f.seek(SeekFrom::Start(offset))
.map_err(BioFormatsError::Io)?;
let mut buf = vec![0u8; plane_bytes];
f.read_exact(&mut buf).map_err(BioFormatsError::Io)?;
Ok(buf)
}
fn open_bytes_region(
&mut self,
plane_index: u32,
x: u32,
y: u32,
w: u32,
h: u32,
) -> Result<Vec<u8>> {
{
let meta = self.meta.as_ref().ok_or(BioFormatsError::NotInitialized)?;
validate_region("Spider", meta.size_x, meta.size_y, x, y, w, h)?;
}
let full = self.open_bytes(plane_index)?;
let meta = self.meta.as_ref().ok_or(BioFormatsError::NotInitialized)?;
crop_full_plane("Spider", &full, meta, 1, x, y, w, h)
}
fn open_thumb_bytes(&mut self, plane_index: u32) -> Result<Vec<u8>> {
let meta = self.meta.as_ref().ok_or(BioFormatsError::NotInitialized)?;
let (tw, th) = (meta.size_x.min(256), meta.size_y.min(256));
let (tx, ty) = ((meta.size_x - tw) / 2, (meta.size_y - th) / 2);
self.open_bytes_region(plane_index, tx, ty, tw, th)
}
}