use std::collections::HashMap;
use std::io::{Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use crate::common::error::{BioFormatsError, Result};
use crate::common::metadata::{DimensionOrder, ImageMetadata};
use crate::common::pixel_type::PixelType;
use crate::common::reader::FormatReader;
use crate::common::region::{crop_full_plane, validate_region};
pub struct AimReader {
path: Option<PathBuf>,
meta: Option<ImageMetadata>,
data_offset: u64,
}
impl AimReader {
pub fn new() -> Self {
AimReader {
path: None,
meta: None,
data_offset: 512,
}
}
}
impl Default for AimReader {
fn default() -> Self {
Self::new()
}
}
fn read_cstring(f: &mut std::fs::File) -> Result<(String, u64)> {
let mut bytes = Vec::new();
let mut byte = [0u8; 1];
loop {
let n = f.read(&mut byte).map_err(BioFormatsError::Io)?;
if n == 0 {
break; }
if byte[0] == 0 {
break;
}
bytes.push(byte[0]);
}
let pos = f.stream_position().map_err(BioFormatsError::Io)?;
Ok((String::from_utf8_lossy(&bytes).into_owned(), pos))
}
fn read_i32_le(f: &mut std::fs::File) -> Result<i32> {
let mut b = [0u8; 4];
f.read_exact(&mut b).map_err(BioFormatsError::Io)?;
Ok(i32::from_le_bytes(b))
}
fn read_i64_le(f: &mut std::fs::File) -> Result<i64> {
let mut b = [0u8; 8];
f.read_exact(&mut b).map_err(BioFormatsError::Io)?;
Ok(i64::from_le_bytes(b))
}
fn load_aim_header(path: &Path) -> Result<(ImageMetadata, u64)> {
let mut f = std::fs::File::open(path).map_err(BioFormatsError::Io)?;
let file_len = f.metadata().map_err(BioFormatsError::Io)?.len();
let mut version = [0u8; 16];
let n = f.read(&mut version).map_err(BioFormatsError::Io)?;
let version_str = String::from_utf8_lossy(&version[..n]).into_owned();
let is_isq = n >= 16 && &version[..16] == b"CTDATA-HEADER_V1";
if is_isq {
f.seek(SeekFrom::Start(28)).map_err(BioFormatsError::Io)?;
let w = positive_dim(read_i32_le(&mut f)?, "ISQ width")?;
let h = positive_dim(read_i32_le(&mut f)?, "ISQ height")?;
let d = positive_dim(read_i32_le(&mut f)?, "ISQ depth")?;
let meta = aim_metadata(w, h, d);
validate_payload_len(file_len, 512, &meta)?;
return Ok((meta, 512));
}
if !version_str.starts_with("AIMDATA") {
return Err(BioFormatsError::UnsupportedFormat(
"AIM file is missing AIMDATA header".into(),
));
}
let wider_offsets = version_str.starts_with("AIMDATA_V030");
let (w, h, d) = if wider_offsets {
f.seek(SeekFrom::Start(96)).map_err(BioFormatsError::Io)?;
let w = positive_dim_i64(read_i64_le(&mut f)?, "AIM width")?;
let h = positive_dim_i64(read_i64_le(&mut f)?, "AIM height")?;
let d = positive_dim_i64(read_i64_le(&mut f)?, "AIM depth")?;
f.seek(SeekFrom::Start(280)).map_err(BioFormatsError::Io)?;
(w, h, d)
} else {
f.seek(SeekFrom::Start(56)).map_err(BioFormatsError::Io)?;
let w = positive_dim(read_i32_le(&mut f)?, "AIM width")?;
let h = positive_dim(read_i32_le(&mut f)?, "AIM height")?;
let d = positive_dim(read_i32_le(&mut f)?, "AIM depth")?;
f.seek(SeekFrom::Start(160)).map_err(BioFormatsError::Io)?;
(w, h, d)
};
let (processing_log, pixel_offset) = read_cstring(&mut f)?;
let mut meta = aim_metadata(w, h, d);
validate_payload_len(file_len, pixel_offset, &meta)?;
for line in processing_log.split('\n') {
let line = line.trim();
if let Some(split) = line.find(" ") {
let key = line[..split].trim();
let value = line[split..].trim();
if !key.is_empty() {
meta.series_metadata.insert(
key.to_string(),
crate::common::metadata::MetadataValue::String(value.to_string()),
);
}
}
}
Ok((meta, pixel_offset))
}
fn positive_dim(value: i32, label: &str) -> Result<u32> {
if value <= 0 {
return Err(BioFormatsError::UnsupportedFormat(format!(
"AIM header has non-positive {label}"
)));
}
Ok(value as u32)
}
fn positive_dim_i64(value: i64, label: &str) -> Result<u32> {
if value <= 0 || value > u32::MAX as i64 {
return Err(BioFormatsError::UnsupportedFormat(format!(
"AIM header has invalid {label}"
)));
}
Ok(value as u32)
}
fn validate_payload_len(file_len: u64, data_offset: u64, meta: &ImageMetadata) -> Result<()> {
let plane_bytes = (meta.size_x as u64)
.checked_mul(meta.size_y as u64)
.and_then(|px| px.checked_mul(meta.pixel_type.bytes_per_sample() as u64))
.ok_or_else(|| BioFormatsError::Format("AIM plane size overflows".into()))?;
let required_len = data_offset
.checked_add(
plane_bytes
.checked_mul(meta.image_count as u64)
.ok_or_else(|| BioFormatsError::Format("AIM payload size overflows".into()))?,
)
.ok_or_else(|| BioFormatsError::Format("AIM payload size overflows".into()))?;
if file_len < required_len {
return Err(BioFormatsError::UnsupportedFormat(format!(
"AIM pixel payload is shorter than declared ({file_len} < {required_len})"
)));
}
Ok(())
}
fn aim_metadata(width: u32, height: u32, depth: u32) -> ImageMetadata {
let image_count = depth.max(1);
ImageMetadata {
size_x: width,
size_y: height,
size_z: image_count,
size_c: 1,
size_t: 1,
pixel_type: PixelType::Int16,
bits_per_pixel: 16,
image_count,
dimension_order: DimensionOrder::XYZCT,
is_rgb: false,
is_interleaved: false,
is_indexed: false,
is_little_endian: true,
resolution_count: 1,
series_metadata: HashMap::new(),
lookup_table: None,
modulo_z: None,
modulo_c: None,
modulo_t: None,
}
}
impl FormatReader for AimReader {
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("aim") | Some("isq"))
}
fn is_this_type_by_bytes(&self, header: &[u8]) -> bool {
(header.len() >= 16 && &header[..16] == b"CTDATA-HEADER_V1")
|| (header.len() >= 7 && &header[..7] == b"AIMDATA")
}
fn set_id(&mut self, path: &Path) -> Result<()> {
self.path = None;
self.meta = None;
self.data_offset = 512;
let (meta, data_offset) = load_aim_header(path)?;
self.path = Some(path.to_path_buf());
self.meta = Some(meta);
self.data_offset = data_offset;
Ok(())
}
fn close(&mut self) -> Result<()> {
self.path = None;
self.meta = None;
self.data_offset = 512;
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 bps = meta.pixel_type.bytes_per_sample();
let plane_bytes = meta.size_x as usize * meta.size_y as usize * bps;
let file_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 = std::fs::File::open(path).map_err(BioFormatsError::Io)?;
f.seek(SeekFrom::Start(file_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("AIM", 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("AIM", &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 = meta.size_x.min(256);
let th = meta.size_y.min(256);
let tx = (meta.size_x - tw) / 2;
let ty = (meta.size_y - th) / 2;
self.open_bytes_region(plane_index, tx, ty, tw, th)
}
}