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, MetadataValue};
use crate::common::pixel_type::PixelType;
use crate::common::reader::FormatReader;
use crate::common::region::crop_full_plane;
macro_rules! tiff_wrapper {
(
$(#[$attr:meta])*
pub struct $name:ident;
extensions: [$($ext:literal),+];
) => {
$(#[$attr])*
pub struct $name {
inner: crate::tiff::TiffReader,
}
impl $name {
pub fn new() -> Self {
$name { inner: crate::tiff::TiffReader::new() }
}
}
impl Default for $name {
fn default() -> Self { Self::new() }
}
impl FormatReader for $name {
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($ext))|+)
}
fn is_this_type_by_bytes(&self, _header: &[u8]) -> bool { false }
fn set_id(&mut self, path: &Path) -> Result<()> {
self.inner.set_id(path)
}
fn close(&mut self) -> Result<()> {
self.inner.close()
}
fn series_count(&self) -> usize {
self.inner.series_count()
}
fn set_series(&mut self, s: usize) -> Result<()> {
self.inner.set_series(s)
}
fn series(&self) -> usize {
self.inner.series()
}
fn metadata(&self) -> &ImageMetadata {
self.inner.metadata()
}
fn open_bytes(&mut self, p: u32) -> Result<Vec<u8>> {
self.inner.open_bytes(p)
}
fn open_bytes_region(&mut self, p: u32, x: u32, y: u32, w: u32, h: u32) -> Result<Vec<u8>> {
self.inner.open_bytes_region(p, x, y, w, h)
}
fn open_thumb_bytes(&mut self, p: u32) -> Result<Vec<u8>> {
self.inner.open_thumb_bytes(p)
}
fn resolution_count(&self) -> usize {
self.inner.resolution_count()
}
fn set_resolution(&mut self, level: usize) -> Result<()> {
self.inner.set_resolution(level)
}
}
};
}
pub struct InrReader {
path: Option<PathBuf>,
meta: Option<ImageMetadata>,
}
impl InrReader {
pub fn new() -> Self {
InrReader {
path: None,
meta: None,
}
}
}
impl Default for InrReader {
fn default() -> Self {
Self::new()
}
}
impl FormatReader for InrReader {
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("inr"))
}
fn is_this_type_by_bytes(&self, header: &[u8]) -> bool {
header.len() >= 13 && &header[0..13] == b"#INRIMAGE-4#{"
}
fn set_id(&mut self, path: &Path) -> Result<()> {
self.close()?;
let data = std::fs::read(path).map_err(BioFormatsError::Io)?;
if data.len() < 256 || !data.starts_with(b"#INRIMAGE-4#{") {
return Err(BioFormatsError::UnsupportedFormat(
"INR file is missing the 256-byte INRIMAGE-4 header".into(),
));
}
let header_bytes = &data[..256];
let header_text = String::from_utf8_lossy(header_bytes);
let mut size_x: Option<u32> = None;
let mut size_y: Option<u32> = None;
let mut size_z: u32 = 1;
let mut size_t: u32 = 1;
let mut bpp: Option<u32> = None;
let mut is_signed = false;
let mut little_endian = true;
for line in header_text.split('\n') {
let line = line.trim();
if let Some(pos) = line.find('=') {
let key = line[..pos].trim();
let val = line[pos + 1..].trim();
match key {
"XDIM" => {
if let Ok(n) = val.parse::<u32>() {
size_x = Some(n);
}
}
"YDIM" => {
if let Ok(n) = val.parse::<u32>() {
size_y = Some(n);
}
}
"ZDIM" => {
if let Ok(n) = val.parse::<u32>() {
size_z = n;
}
}
"VDIM" => {
if let Ok(n) = val.parse::<u32>() {
size_t = n;
}
}
"PIXSIZE" => {
if let Some(n_str) = val.split_whitespace().next() {
if let Ok(n) = n_str.parse::<u32>() {
bpp = Some(n);
}
}
}
"TYPE" => {
is_signed = val.to_ascii_lowercase().starts_with("signed");
}
"CPU" => {
little_endian = matches!(val, "decm" | "pc");
if val == "sun" || val == "sgi" {
little_endian = false;
}
}
_ => {}
}
}
}
let size_x = size_x
.filter(|&v| v > 0)
.ok_or_else(|| BioFormatsError::UnsupportedFormat("INR header missing XDIM".into()))?;
let size_y = size_y
.filter(|&v| v > 0)
.ok_or_else(|| BioFormatsError::UnsupportedFormat("INR header missing YDIM".into()))?;
let bpp = bpp.ok_or_else(|| {
BioFormatsError::UnsupportedFormat("INR header missing PIXSIZE".into())
})?;
let bytes = bpp / 8;
let pixel_type = match bytes {
1 if is_signed => PixelType::Int8,
1 => PixelType::Uint8,
2 if is_signed => PixelType::Int16,
2 => PixelType::Uint16,
4 if is_signed => PixelType::Int32,
4 => PixelType::Uint32,
8 => PixelType::Float64,
_ => {
return Err(BioFormatsError::UnsupportedFormat(format!(
"INR unsupported pixel size: {bpp} bits"
)));
}
};
let size_c: u32 = 1;
if size_z == 0 || size_t == 0 {
return Err(BioFormatsError::UnsupportedFormat(
"INR header dimensions must be positive".into(),
));
}
let image_count = size_z
.checked_mul(size_t)
.and_then(|v| v.checked_mul(size_c))
.ok_or_else(|| BioFormatsError::Format("INR image count overflows".into()))?;
let bps = (bpp / 8) as u64;
let expected = 256u64
.checked_add(
(size_x as u64)
.checked_mul(size_y as u64)
.and_then(|v| v.checked_mul(image_count as u64))
.and_then(|v| v.checked_mul(bps))
.ok_or_else(|| BioFormatsError::Format("INR image size overflows".into()))?,
)
.ok_or_else(|| BioFormatsError::Format("INR image size overflows".into()))?;
if (data.len() as u64) < expected {
return Err(BioFormatsError::UnsupportedFormat(
"INR pixel payload is shorter than declared dimensions".into(),
));
}
self.path = Some(path.to_path_buf());
self.meta = Some(ImageMetadata {
size_x,
size_y,
size_z,
size_c,
size_t,
pixel_type,
bits_per_pixel: bpp as u8,
image_count,
dimension_order: DimensionOrder::XYZTC,
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,
});
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() {
return Err(BioFormatsError::NotInitialized);
}
if s == 0 {
Ok(())
} else {
Err(BioFormatsError::SeriesOutOfRange(s))
}
}
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.bits_per_pixel / 8) as usize;
let plane_bytes = meta.size_x as usize * meta.size_y as usize * bps;
let offset = 256u64 + (plane_index as u64) * (plane_bytes as u64);
let path = self.path.clone().ok_or(BioFormatsError::NotInitialized)?;
let mut f = std::fs::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)?
.clone();
if plane_index >= meta.image_count {
return Err(BioFormatsError::PlaneOutOfRange(plane_index));
}
let full = self.open_bytes(plane_index)?;
crop_full_plane("INR", &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)
}
fn resolution_count(&self) -> usize {
1
}
fn set_resolution(&mut self, level: usize) -> Result<()> {
if level != 0 {
Err(BioFormatsError::Format(format!(
"resolution {} out of range",
level
)))
} else {
Ok(())
}
}
}
const FEI_PHILIPS_MAGIC: &[u8; 2] = b"XL";
const FEI_INVALID_PIXELS: u32 = 112;
const FEI_DIMENSION_OFFSET: u64 = 514;
pub struct FeiPhilipsReader {
path: Option<PathBuf>,
meta: Option<ImageMetadata>,
header_size: u64,
}
impl FeiPhilipsReader {
pub fn new() -> Self {
FeiPhilipsReader {
path: None,
meta: None,
header_size: 0,
}
}
}
impl Default for FeiPhilipsReader {
fn default() -> Self {
Self::new()
}
}
fn read_le_u16_at(data: &[u8], offset: usize, label: &str) -> Result<u16> {
let bytes = data.get(offset..offset + 2).ok_or_else(|| {
BioFormatsError::UnsupportedFormat(format!("FEI/Philips header missing {label}"))
})?;
Ok(u16::from_le_bytes([bytes[0], bytes[1]]))
}
fn read_le_f32_at(data: &[u8], offset: usize) -> Option<f32> {
let bytes = data.get(offset..offset + 4)?;
Some(f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
impl FormatReader for FeiPhilipsReader {
fn is_this_type_by_name(&self, path: &Path) -> bool {
path.extension()
.and_then(|e| e.to_str())
.map(|e| e.eq_ignore_ascii_case("img"))
.unwrap_or(false)
}
fn is_this_type_by_bytes(&self, header: &[u8]) -> bool {
header.starts_with(FEI_PHILIPS_MAGIC)
}
fn set_id(&mut self, path: &Path) -> Result<()> {
self.close()?;
let data = std::fs::read(path).map_err(BioFormatsError::Io)?;
if !self.is_this_type_by_bytes(&data) {
return Err(BioFormatsError::UnsupportedFormat(
"FEI/Philips IMG header does not start with XL".into(),
));
}
let stored_width = read_le_u16_at(&data, 514, "width")? as u32;
let height = read_le_u16_at(&data, 516, "height")? as u32;
let header_size = read_le_u16_at(&data, 522, "pixel offset")? as u64;
if stored_width <= FEI_INVALID_PIXELS || height == 0 || header_size < FEI_DIMENSION_OFFSET {
return Err(BioFormatsError::UnsupportedFormat(
"FEI/Philips IMG header contains invalid dimensions or pixel offset".into(),
));
}
let width = stored_width - FEI_INVALID_PIXELS;
if width % 2 != 0 {
return Err(BioFormatsError::UnsupportedFormat(
"FEI/Philips IMG width must be even for interlaced decode".into(),
));
}
let encoded_row_bytes = (width / 2 + FEI_INVALID_PIXELS / 2) as u64 * 2;
let encoded_bytes = encoded_row_bytes
.checked_mul(height as u64)
.ok_or_else(|| BioFormatsError::Format("FEI/Philips payload size overflows".into()))?;
let expected = header_size
.checked_add(encoded_bytes)
.ok_or_else(|| BioFormatsError::Format("FEI/Philips file size overflows".into()))?;
if (data.len() as u64) < expected {
return Err(BioFormatsError::UnsupportedFormat(
"FEI/Philips IMG payload is shorter than declared dimensions".into(),
));
}
let mut series_metadata = HashMap::new();
if let Some(v) = read_le_f32_at(&data, 44) {
series_metadata.insert("Magnification".into(), MetadataValue::Float(v as f64));
}
if let Some(v) = read_le_f32_at(&data, 48) {
series_metadata.insert("kV".into(), MetadataValue::Float((v / 1000.0) as f64));
}
if let Some(v) = read_le_f32_at(&data, 52) {
series_metadata.insert("Working distance".into(), MetadataValue::Float(v as f64));
}
if let Some(v) = read_le_f32_at(&data, 68) {
series_metadata.insert("Spot".into(), MetadataValue::Float(v as f64));
}
self.path = Some(path.to_path_buf());
self.header_size = header_size;
self.meta = Some(ImageMetadata {
size_x: width,
size_y: height,
size_z: 1,
size_c: 1,
size_t: 1,
pixel_type: PixelType::Uint8,
bits_per_pixel: 8,
image_count: 1,
dimension_order: DimensionOrder::XYCZT,
is_rgb: false,
is_interleaved: false,
is_indexed: false,
is_little_endian: true,
resolution_count: 1,
series_metadata,
lookup_table: None,
modulo_z: None,
modulo_c: None,
modulo_t: None,
});
Ok(())
}
fn close(&mut self) -> Result<()> {
self.path = None;
self.meta = None;
self.header_size = 0;
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() {
return Err(BioFormatsError::NotInitialized);
}
if s == 0 {
Ok(())
} else {
Err(BioFormatsError::SeriesOutOfRange(s))
}
}
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 != 0 {
return Err(BioFormatsError::PlaneOutOfRange(plane_index));
}
let path = self.path.clone().ok_or(BioFormatsError::NotInitialized)?;
let mut f = std::fs::File::open(&path).map_err(BioFormatsError::Io)?;
f.seek(SeekFrom::Start(self.header_size))
.map_err(BioFormatsError::Io)?;
let width = meta.size_x as usize;
let height = meta.size_y as usize;
let segment_len = width / 2;
let invalid_len = (FEI_INVALID_PIXELS / 2) as usize;
let mut segment = vec![0u8; segment_len];
let mut invalid = vec![0u8; invalid_len];
let mut plane = vec![0u8; width * height];
for row_pass in 0..4 {
let mut row = row_pass;
while row < height {
for col_pass in 0..2 {
f.read_exact(&mut segment).map_err(BioFormatsError::Io)?;
f.read_exact(&mut invalid).map_err(BioFormatsError::Io)?;
let mut col = col_pass;
while col < width {
plane[row * width + col] = segment[col / 2];
col += 2;
}
}
row += 4;
}
}
Ok(plane)
}
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)?
.clone();
let full = self.open_bytes(plane_index)?;
crop_full_plane("FEI/Philips IMG", &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)
}
}
pub struct VeecoReader {
path: Option<PathBuf>,
meta: Option<ImageMetadata>,
data_offset: usize,
}
impl VeecoReader {
pub fn new() -> Self {
VeecoReader {
path: None,
meta: None,
data_offset: 0,
}
}
}
impl Default for VeecoReader {
fn default() -> Self {
Self::new()
}
}
impl FormatReader for VeecoReader {
fn is_this_type_by_name(&self, path: &Path) -> bool {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
ext.eq_ignore_ascii_case("afm")
|| (ext.len() >= 1 && ext.len() <= 3 && ext.chars().all(|c| c.is_ascii_digit()))
}
fn is_this_type_by_bytes(&self, header: &[u8]) -> bool {
if header.is_empty() || header[0] != b'*' {
return false;
}
let s = String::from_utf8_lossy(&header[..header.len().min(30)]);
s.to_ascii_uppercase().contains("NANOSCOPE")
}
fn set_id(&mut self, path: &Path) -> Result<()> {
self.close()?;
let data = std::fs::read(path).map_err(BioFormatsError::Io)?;
let text = String::from_utf8_lossy(&data).into_owned();
let mut width: Option<u32> = None;
let mut height: Option<u32> = None;
let mut bpp: Option<u32> = None;
let mut data_offset: Option<usize> = None;
for line in text.lines() {
if line.contains("\\Samps/line:") {
if let Some(val) = line.split_whitespace().last() {
if let Ok(n) = val.parse::<u32>() {
width = Some(n);
}
}
} else if line.contains("\\Number of lines:") {
if let Some(val) = line.split_whitespace().last() {
if let Ok(n) = val.parse::<u32>() {
height = Some(n);
}
}
} else if line.contains("\\Bytes/pixel:") {
if let Some(val) = line.split_whitespace().last() {
if let Ok(n) = val.parse::<u32>() {
bpp = Some(n);
}
}
} else if line.contains("\\Data offset:") {
if let Some(val) = line.split_whitespace().last() {
if let Ok(n) = val.parse::<usize>() {
data_offset = Some(n);
}
}
}
}
let width = width.filter(|&v| v > 0).ok_or_else(|| {
BioFormatsError::UnsupportedFormat("Nanoscope header missing Samps/line".into())
})?;
let height = height.filter(|&v| v > 0).ok_or_else(|| {
BioFormatsError::UnsupportedFormat("Nanoscope header missing Number of lines".into())
})?;
let bpp = bpp.filter(|&v| v == 1 || v == 2).ok_or_else(|| {
BioFormatsError::UnsupportedFormat(
"Nanoscope header missing supported Bytes/pixel".into(),
)
})?;
let data_offset = data_offset.ok_or_else(|| {
BioFormatsError::UnsupportedFormat("Nanoscope header missing Data offset".into())
})?;
let expected = (data_offset as u64)
.checked_add(
(width as u64)
.saturating_mul(height as u64)
.saturating_mul(bpp as u64),
)
.ok_or_else(|| BioFormatsError::Format("Nanoscope plane size overflows".into()))?;
if expected > data.len() as u64 {
return Err(BioFormatsError::UnsupportedFormat(
"Nanoscope pixel payload is shorter than declared dimensions".into(),
));
}
let pixel_type = if bpp == 1 {
PixelType::Uint8
} else {
PixelType::Uint16
};
let bits_per_pixel = (bpp * 8) as u8;
self.data_offset = data_offset;
self.path = Some(path.to_path_buf());
self.meta = Some(ImageMetadata {
size_x: width,
size_y: height,
size_z: 1,
size_c: 1,
size_t: 1,
pixel_type,
bits_per_pixel,
image_count: 1,
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,
});
Ok(())
}
fn close(&mut self) -> Result<()> {
self.path = None;
self.meta = None;
self.data_offset = 0;
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() {
return Err(BioFormatsError::NotInitialized);
}
if 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.bits_per_pixel / 8) as usize;
let n_bytes = meta.size_x as usize * meta.size_y as usize * bps;
let path = self.path.clone().ok_or(BioFormatsError::NotInitialized)?;
let mut f = std::fs::File::open(&path).map_err(BioFormatsError::Io)?;
f.seek(SeekFrom::Start(self.data_offset as u64))
.map_err(BioFormatsError::Io)?;
let mut buf = vec![0u8; n_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)?
.clone();
if plane_index >= meta.image_count {
return Err(BioFormatsError::PlaneOutOfRange(plane_index));
}
let full = self.open_bytes(plane_index)?;
crop_full_plane("Nanoscope", &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)
}
fn resolution_count(&self) -> usize {
1
}
fn set_resolution(&mut self, level: usize) -> Result<()> {
if level != 0 {
Err(BioFormatsError::Format(format!(
"resolution {} out of range",
level
)))
} else {
Ok(())
}
}
}
tiff_wrapper! {
pub struct ZeissTiffReader;
extensions: ["tif"];
}
fn unsupported_raw_sem(format_name: &str) -> BioFormatsError {
BioFormatsError::UnsupportedFormat(format!(
"{format_name} native binary layout is unsupported unless explicit strict raw data is present; refusing heuristic dimensions"
))
}
const JEOL_STRICT_MAGIC: &[u8] = b"BIOFORMATS-RS-JEOL-SEM-STRICT-RAW-V1\n";
const ZEISS_LMS_STRICT_MAGIC: &[u8] = b"BIOFORMATS-RS-ZEISS-LMS-STRICT-RAW-V1\n";
const IMROD_STRICT_MAGIC: &[u8] = b"BIOFORMATS-RS-IMROD-STRICT-RAW-V1\n";
fn read_le_u32_strict(data: &[u8], offset: usize, label: &str, format_name: &str) -> Result<u32> {
let bytes = data.get(offset..offset + 4).ok_or_else(|| {
BioFormatsError::UnsupportedFormat(format!("{format_name} strict header missing {label}"))
})?;
Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn read_le_u16_strict(data: &[u8], offset: usize, label: &str, format_name: &str) -> Result<u16> {
let bytes = data.get(offset..offset + 2).ok_or_else(|| {
BioFormatsError::UnsupportedFormat(format!("{format_name} strict header missing {label}"))
})?;
Ok(u16::from_le_bytes([bytes[0], bytes[1]]))
}
fn parse_strict_sem_raw(
path: &Path,
magic: &[u8],
format_name: &str,
) -> Result<(ImageMetadata, u64)> {
let data = std::fs::read(path).map_err(BioFormatsError::Io)?;
if !data.starts_with(magic) {
return Err(unsupported_raw_sem(format_name));
}
let width_offset = magic.len();
let height_offset = width_offset + 4;
let pixel_type_offset = height_offset + 4;
let reserved_offset = pixel_type_offset + 2;
let data_offset = reserved_offset + 2;
let width = read_le_u32_strict(&data, width_offset, "width", format_name)?;
let height = read_le_u32_strict(&data, height_offset, "height", format_name)?;
if width == 0 || height == 0 {
return Err(BioFormatsError::UnsupportedFormat(format!(
"{format_name} strict header dimensions must be non-zero"
)));
}
let pixel_type_code = read_le_u16_strict(&data, pixel_type_offset, "pixel type", format_name)?;
let (pixel_type, bits_per_pixel) = match pixel_type_code {
1 => (PixelType::Uint8, 8),
2 => (PixelType::Uint16, 16),
_ => {
return Err(BioFormatsError::UnsupportedFormat(format!(
"{format_name} strict header has unsupported pixel type code {pixel_type_code}"
)));
}
};
let reserved = read_le_u16_strict(&data, reserved_offset, "reserved field", format_name)?;
if reserved != 0 {
return Err(BioFormatsError::UnsupportedFormat(format!(
"{format_name} strict header reserved field must be zero"
)));
}
let payload_len = (width as u64)
.checked_mul(height as u64)
.and_then(|n| n.checked_mul(pixel_type.bytes_per_sample() as u64))
.ok_or_else(|| BioFormatsError::Format(format!("{format_name} payload size overflows")))?;
let expected_len = (data_offset as u64)
.checked_add(payload_len)
.ok_or_else(|| BioFormatsError::Format(format!("{format_name} file size overflows")))?;
if data.len() as u64 != expected_len {
return Err(BioFormatsError::UnsupportedFormat(format!(
"{format_name} strict payload length mismatch: got {}, expected {expected_len}",
data.len()
)));
}
Ok((
ImageMetadata {
size_x: width,
size_y: height,
size_z: 1,
size_c: 1,
size_t: 1,
pixel_type,
bits_per_pixel,
image_count: 1,
dimension_order: DimensionOrder::XYCZT,
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,
},
data_offset as u64,
))
}
pub struct JeolReader {
path: Option<PathBuf>,
meta: Option<ImageMetadata>,
data_offset: u64,
}
impl JeolReader {
pub fn new() -> Self {
JeolReader {
path: None,
meta: None,
data_offset: 0,
}
}
}
impl Default for JeolReader {
fn default() -> Self {
Self::new()
}
}
impl FormatReader for JeolReader {
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("dat"))
}
fn is_this_type_by_bytes(&self, header: &[u8]) -> bool {
header.starts_with(JEOL_STRICT_MAGIC)
}
fn set_id(&mut self, path: &Path) -> Result<()> {
self.close()?;
let (meta, data_offset) = parse_strict_sem_raw(path, JEOL_STRICT_MAGIC, "JEOL SEM")?;
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 = 0;
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() {
Err(BioFormatsError::NotInitialized)
} else if s == 0 {
Ok(())
} else {
Err(BioFormatsError::SeriesOutOfRange(s))
}
}
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 n_bytes = (meta.size_x as usize)
.checked_mul(meta.size_y as usize)
.and_then(|n| n.checked_mul(meta.pixel_type.bytes_per_sample()))
.ok_or_else(|| BioFormatsError::Format("JEOL SEM plane size overflows".into()))?;
let path = self.path.clone().ok_or(BioFormatsError::NotInitialized)?;
let mut f = std::fs::File::open(&path).map_err(BioFormatsError::Io)?;
f.seek(SeekFrom::Start(self.data_offset))
.map_err(BioFormatsError::Io)?;
let mut buf = vec![0u8; n_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)?
.clone();
if plane_index >= meta.image_count {
return Err(BioFormatsError::PlaneOutOfRange(plane_index));
}
let full = self.open_bytes(plane_index)?;
crop_full_plane("JEOL SEM", &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)
}
fn resolution_count(&self) -> usize {
1
}
fn set_resolution(&mut self, level: usize) -> Result<()> {
if level != 0 {
Err(BioFormatsError::Format(format!(
"resolution {} out of range",
level
)))
} else {
Ok(())
}
}
}
pub struct HitachiReader {
path: Option<PathBuf>,
meta: Option<ImageMetadata>,
pixels_file: Option<PathBuf>,
ini: HashMap<String, String>,
}
impl HitachiReader {
const MAGIC: &'static str = "[SemImageFile]";
pub fn new() -> Self {
HitachiReader {
path: None,
meta: None,
pixels_file: None,
ini: HashMap::new(),
}
}
fn decode_header(bytes: &[u8]) -> String {
let ascii = String::from_utf8_lossy(bytes);
if ascii.contains(Self::MAGIC) {
return ascii.into_owned();
}
for be in [false, true] {
let units: Vec<u16> = bytes
.chunks_exact(2)
.map(|c| {
if be {
u16::from_be_bytes([c[0], c[1]])
} else {
u16::from_le_bytes([c[0], c[1]])
}
})
.collect();
let s = String::from_utf16_lossy(&units);
if s.contains(Self::MAGIC) {
return s;
}
}
ascii.into_owned()
}
fn parse_ini(text: &str) -> HashMap<String, String> {
let mut map = HashMap::new();
let mut in_section = false;
for line in text.lines() {
let line = line.trim();
if line.starts_with('[') && line.ends_with(']') {
in_section = line.eq_ignore_ascii_case(Self::MAGIC);
continue;
}
if !in_section {
continue;
}
if let Some((k, v)) = line.split_once('=') {
map.insert(k.trim().to_string(), v.trim().to_string());
}
}
map
}
}
impl Default for HitachiReader {
fn default() -> Self {
Self::new()
}
}
impl FormatReader for HitachiReader {
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("txt"))
}
fn is_this_type_by_bytes(&self, header: &[u8]) -> bool {
Self::decode_header(header).contains(Self::MAGIC)
}
fn set_id(&mut self, path: &Path) -> Result<()> {
self.close()?;
let txt_path = if path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.eq_ignore_ascii_case("txt"))
.unwrap_or(false)
{
path.to_path_buf()
} else {
path.with_extension("txt")
};
let bytes = std::fs::read(&txt_path).map_err(BioFormatsError::Io)?;
let text = Self::decode_header(&bytes);
if !text.contains(Self::MAGIC) {
return Err(BioFormatsError::UnsupportedFormat(
"Hitachi: missing [SemImageFile] section".into(),
));
}
let ini = Self::parse_ini(&text);
let parent = txt_path.parent().unwrap_or_else(|| Path::new("."));
let mut pixels_file: Option<PathBuf> = None;
if let Some(name) = ini.get("ImageName") {
let candidate = parent.join(name);
if candidate.exists() {
pixels_file = Some(candidate);
}
}
if pixels_file.is_none() {
for ext in ["tif", "jpg", "bmp"] {
let candidate = txt_path.with_extension(ext);
if candidate.exists() {
pixels_file = Some(candidate);
break;
}
}
}
let pixels_file = pixels_file.ok_or_else(|| {
BioFormatsError::UnsupportedFormat("Hitachi: could not find pixels file".into())
})?;
let mut helper = crate::registry::ImageReader::open(&pixels_file)?;
let mut meta = helper.metadata().clone();
helper.close().ok();
for (k, v) in &ini {
meta.series_metadata.insert(
k.clone(),
crate::common::metadata::MetadataValue::String(v.clone()),
);
}
meta.series_metadata.insert(
"format".into(),
crate::common::metadata::MetadataValue::String("Hitachi".into()),
);
self.ini = ini;
self.pixels_file = Some(pixels_file);
self.path = Some(txt_path);
self.meta = Some(meta);
Ok(())
}
fn close(&mut self) -> Result<()> {
self.path = None;
self.meta = None;
self.pixels_file = None;
self.ini.clear();
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() {
return Err(BioFormatsError::NotInitialized);
}
if 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 pixels = self
.pixels_file
.clone()
.ok_or(BioFormatsError::NotInitialized)?;
let mut helper = crate::registry::ImageReader::open(&pixels)?;
let bytes = helper.open_bytes(plane_index);
helper.close().ok();
bytes
}
fn open_bytes_region(
&mut self,
plane_index: u32,
x: u32,
y: u32,
w: u32,
h: u32,
) -> Result<Vec<u8>> {
let pixels = self
.pixels_file
.clone()
.ok_or(BioFormatsError::NotInitialized)?;
let mut helper = crate::registry::ImageReader::open(&pixels)?;
let bytes = helper.open_bytes_region(plane_index, x, y, w, h);
helper.close().ok();
bytes
}
fn open_thumb_bytes(&mut self, plane_index: u32) -> Result<Vec<u8>> {
let pixels = self
.pixels_file
.clone()
.ok_or(BioFormatsError::NotInitialized)?;
let mut helper = crate::registry::ImageReader::open(&pixels)?;
let bytes = helper.open_thumb_bytes(plane_index);
helper.close().ok();
bytes
}
fn resolution_count(&self) -> usize {
1
}
fn set_resolution(&mut self, level: usize) -> Result<()> {
if level != 0 {
Err(BioFormatsError::Format(format!(
"resolution {} out of range",
level
)))
} else {
Ok(())
}
}
}
pub struct LeoReader {
inner: crate::tiff::TiffReader,
meta: Option<ImageMetadata>,
}
impl LeoReader {
const LEO_TAG: u16 = 34118;
pub fn new() -> Self {
LeoReader {
inner: crate::tiff::TiffReader::new(),
meta: None,
}
}
}
impl Default for LeoReader {
fn default() -> Self {
Self::new()
}
}
impl FormatReader for LeoReader {
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("sxm") | Some("tif") | Some("tiff"))
}
fn is_this_type_by_bytes(&self, _header: &[u8]) -> bool {
false
}
fn set_id(&mut self, path: &Path) -> Result<()> {
self.close()?;
self.inner.set_id(path)?;
let first = self
.inner
.ifd(0)
.ok_or_else(|| BioFormatsError::UnsupportedFormat("LEO: no IFD".into()))?;
if first.get(Self::LEO_TAG).is_none() {
let _ = self.inner.close();
return Err(BioFormatsError::UnsupportedFormat(
"LEO: TIFF is missing the LEO tag (34118)".into(),
));
}
let mut meta = self.inner.metadata().clone();
meta.series_metadata
.insert("format".into(), MetadataValue::String("LEO".into()));
if let Some(tag_text) = first.get_str(Self::LEO_TAG) {
let lines: Vec<&str> = tag_text.split('\n').collect();
let mut i = 0usize;
while i < lines.len() {
let t = lines[i].trim();
if (t.starts_with("AP_") || t.starts_with("DP_") || t.starts_with("SV_"))
&& i + 1 < lines.len()
{
let sep = if t == "AP_TIME" || t == "AP_DATE" {
':'
} else {
'='
};
let val_line = lines[i + 1].trim();
if let Some((k, v)) = val_line.split_once(sep) {
meta.series_metadata.insert(
k.trim().to_string(),
MetadataValue::String(v.trim().to_string()),
);
}
i += 2;
} else {
i += 1;
}
}
}
self.meta = Some(meta);
Ok(())
}
fn close(&mut self) -> Result<()> {
self.meta = None;
self.inner.close()
}
fn series_count(&self) -> usize {
if self.meta.is_some() {
self.inner.series_count()
} else {
0
}
}
fn set_series(&mut self, s: usize) -> Result<()> {
if self.meta.is_none() {
return Err(BioFormatsError::NotInitialized);
}
self.inner.set_series(s)
}
fn series(&self) -> usize {
self.inner.series()
}
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>> {
self.inner.open_bytes(plane_index)
}
fn open_bytes_region(
&mut self,
plane_index: u32,
x: u32,
y: u32,
w: u32,
h: u32,
) -> Result<Vec<u8>> {
self.inner.open_bytes_region(plane_index, x, y, w, h)
}
fn open_thumb_bytes(&mut self, plane_index: u32) -> Result<Vec<u8>> {
self.inner.open_thumb_bytes(plane_index)
}
fn resolution_count(&self) -> usize {
self.inner.resolution_count()
}
fn set_resolution(&mut self, level: usize) -> Result<()> {
self.inner.set_resolution(level)
}
}
pub struct ZeissLmsReader {
path: Option<PathBuf>,
meta: Option<ImageMetadata>,
data_offset: u64,
}
impl ZeissLmsReader {
pub fn new() -> Self {
ZeissLmsReader {
path: None,
meta: None,
data_offset: 0,
}
}
}
impl Default for ZeissLmsReader {
fn default() -> Self {
Self::new()
}
}
impl FormatReader for ZeissLmsReader {
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("lms"))
}
fn is_this_type_by_bytes(&self, header: &[u8]) -> bool {
header.starts_with(ZEISS_LMS_STRICT_MAGIC)
}
fn set_id(&mut self, path: &Path) -> Result<()> {
self.close()?;
let (meta, data_offset) = parse_strict_sem_raw(path, ZEISS_LMS_STRICT_MAGIC, "Zeiss LMS")?;
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 = 0;
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() {
Err(BioFormatsError::NotInitialized)
} else if s == 0 {
Ok(())
} else {
Err(BioFormatsError::SeriesOutOfRange(s))
}
}
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 n_bytes = (meta.size_x as usize)
.checked_mul(meta.size_y as usize)
.and_then(|n| n.checked_mul(meta.pixel_type.bytes_per_sample()))
.ok_or_else(|| BioFormatsError::Format("Zeiss LMS plane size overflows".into()))?;
let path = self.path.clone().ok_or(BioFormatsError::NotInitialized)?;
let mut f = std::fs::File::open(&path).map_err(BioFormatsError::Io)?;
f.seek(SeekFrom::Start(self.data_offset))
.map_err(BioFormatsError::Io)?;
let mut buf = vec![0u8; n_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)?
.clone();
if plane_index >= meta.image_count {
return Err(BioFormatsError::PlaneOutOfRange(plane_index));
}
let full = self.open_bytes(plane_index)?;
crop_full_plane("Zeiss LMS", &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)
}
fn resolution_count(&self) -> usize {
1
}
fn set_resolution(&mut self, level: usize) -> Result<()> {
if level != 0 {
Err(BioFormatsError::Format(format!(
"resolution {} out of range",
level
)))
} else {
Ok(())
}
}
}
pub struct ImrodReader {
path: Option<PathBuf>,
meta: Option<ImageMetadata>,
data_offset: u64,
}
impl ImrodReader {
pub fn new() -> Self {
ImrodReader {
path: None,
meta: None,
data_offset: 0,
}
}
}
impl Default for ImrodReader {
fn default() -> Self {
Self::new()
}
}
impl FormatReader for ImrodReader {
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("mod"))
}
fn is_this_type_by_bytes(&self, header: &[u8]) -> bool {
header.starts_with(IMROD_STRICT_MAGIC)
}
fn set_id(&mut self, path: &Path) -> Result<()> {
self.close()?;
let (meta, data_offset) = parse_strict_sem_raw(path, IMROD_STRICT_MAGIC, "IMROD")?;
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 = 0;
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() {
Err(BioFormatsError::NotInitialized)
} else if s == 0 {
Ok(())
} else {
Err(BioFormatsError::SeriesOutOfRange(s))
}
}
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 n_bytes = (meta.size_x as usize)
.checked_mul(meta.size_y as usize)
.and_then(|n| n.checked_mul(meta.pixel_type.bytes_per_sample()))
.ok_or_else(|| BioFormatsError::Format("IMROD plane size overflows".into()))?;
let path = self.path.clone().ok_or(BioFormatsError::NotInitialized)?;
let mut f = std::fs::File::open(&path).map_err(BioFormatsError::Io)?;
f.seek(SeekFrom::Start(self.data_offset))
.map_err(BioFormatsError::Io)?;
let mut buf = vec![0u8; n_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)?
.clone();
if plane_index >= meta.image_count {
return Err(BioFormatsError::PlaneOutOfRange(plane_index));
}
let full = self.open_bytes(plane_index)?;
crop_full_plane("IMROD", &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)
}
fn resolution_count(&self) -> usize {
1
}
fn set_resolution(&mut self, level: usize) -> Result<()> {
if level != 0 {
Err(BioFormatsError::Format(format!(
"resolution {} out of range",
level
)))
} else {
Ok(())
}
}
}
#[cfg(test)]
mod inr_tests {
use super::*;
use std::io::Write;
fn write_inr(dir: &std::path::Path, name: &str, header_body: &str, payload: &[u8]) -> PathBuf {
let mut header = String::from("#INRIMAGE-4#{\n");
header.push_str(header_body);
header.push_str("##}\n");
let mut bytes = header.into_bytes();
assert!(bytes.len() <= 256, "test header exceeds 256 bytes");
bytes.resize(256, b'\n');
bytes.extend_from_slice(payload);
let path = dir.join(name);
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(&bytes).unwrap();
path
}
#[test]
fn vdim_maps_to_size_t() {
let tmp = std::env::temp_dir();
let body = "XDIM=2\nYDIM=2\nZDIM=3\nVDIM=4\nPIXSIZE=8 bits\nTYPE=unsigned fixed\n";
let payload = vec![0u8; 2 * 2 * 3 * 4];
let path = write_inr(&tmp, "inr_vdim_test.inr", body, &payload);
let mut r = InrReader::new();
r.set_id(&path).unwrap();
let m = r.metadata();
assert_eq!(m.size_z, 3);
assert_eq!(m.size_t, 4, "VDIM should populate size_t");
assert_eq!(m.size_c, 1, "size_c must be forced to 1");
assert_eq!(m.image_count, 12, "image_count = z*t*c");
assert_eq!(m.dimension_order, DimensionOrder::XYZTC);
assert_eq!(m.pixel_type, PixelType::Uint8);
let _ = std::fs::remove_file(&path);
}
#[test]
fn pixel_type_by_byte_width() {
let tmp = std::env::temp_dir();
let cases: &[(&str, u32, PixelType)] = &[
("signed fixed", 8, PixelType::Int8),
("unsigned fixed", 8, PixelType::Uint8),
("signed fixed", 16, PixelType::Int16),
("unsigned fixed", 16, PixelType::Uint16),
("signed fixed", 32, PixelType::Int32),
("unsigned fixed", 32, PixelType::Uint32),
("float", 64, PixelType::Float64),
("float", 32, PixelType::Uint32),
];
for (i, (ty, bits, expected)) in cases.iter().enumerate() {
let body = format!("XDIM=1\nYDIM=1\nPIXSIZE={} bits\nTYPE={}\n", bits, ty);
let payload = vec![0u8; (bits / 8) as usize];
let name = format!("inr_pt_{}.inr", i);
let path = write_inr(&tmp, &name, &body, &payload);
let mut r = InrReader::new();
r.set_id(&path).unwrap();
assert_eq!(r.metadata().pixel_type, *expected, "case {}: {} {}", i, ty, bits);
let _ = std::fs::remove_file(&path);
}
}
}