bioformats 0.1.1

Pure Rust reimplementation of Bio-Formats — read/write scientific image formats
Documentation
//! SimFCS FLIM binary format reader.
//!
//! SimFCS stores raw binary FLIM data with no file header.
//! The file extension indicates the data type.
//!
//! Also includes LambertFlimReader for Lambert Instruments FLIM .asc files.

use std::collections::HashMap;
use std::fs;
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;

// ── SimFCS Reader ─────────────────────────────────────────────────────────────

pub struct SimfcsReader {
    path: Option<PathBuf>,
    meta: Option<ImageMetadata>,
}

impl SimfcsReader {
    pub fn new() -> Self {
        SimfcsReader {
            path: None,
            meta: None,
        }
    }
}

impl Default for SimfcsReader {
    fn default() -> Self {
        Self::new()
    }
}

fn simfcs_pixel_type(ext: &str) -> Option<PixelType> {
    match ext {
        "b64" => Some(PixelType::Uint8),
        "r64" => Some(PixelType::Float32),
        "i64" => Some(PixelType::Int32),
        _ => None,
    }
}

impl FormatReader for SimfcsReader {
    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("b64") | Some("r64") | Some("i64"))
    }

    fn is_this_type_by_bytes(&self, _header: &[u8]) -> bool {
        false
    }

    fn set_id(&mut self, path: &Path) -> Result<()> {
        let ext = path
            .extension()
            .and_then(|e| e.to_str())
            .map(|e| e.to_ascii_lowercase())
            .unwrap_or_default();
        let pixel_type = simfcs_pixel_type(&ext)
            .ok_or_else(|| BioFormatsError::Format(format!("Unknown SimFCS extension: {}", ext)))?;

        let bps = pixel_type.bytes_per_sample();
        let file_size = fs::metadata(path).map_err(BioFormatsError::Io)?.len() as usize;
        let frame_bytes = 256 * 256 * bps;
        if file_size == 0 || file_size % frame_bytes != 0 {
            return Err(BioFormatsError::UnsupportedFormat(format!(
                "SimFCS payload length {file_size} is not a whole number of 256x256 frames"
            )));
        }
        let image_count = (file_size / frame_bytes) as u32;

        let meta = ImageMetadata {
            size_x: 256,
            size_y: 256,
            size_z: image_count,
            size_c: 1,
            size_t: 1,
            pixel_type,
            bits_per_pixel: (bps * 8) as u8,
            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,
        };

        self.path = Some(path.to_path_buf());
        self.meta = Some(meta);
        Ok(())
    }

    fn close(&mut self) -> Result<()> {
        self.path = None;
        self.meta = None;
        Ok(())
    }

    fn series_count(&self) -> usize {
        1
    }
    fn set_series(&mut self, s: usize) -> Result<()> {
        if s != 0 {
            Err(BioFormatsError::SeriesOutOfRange(s))
        } else {
            Ok(())
        }
    }
    fn series(&self) -> usize {
        0
    }

    fn metadata(&self) -> &ImageMetadata {
        self.meta.as_ref().expect("set_id not called")
    }

    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 = 256 * 256 * bps;
        let offset = plane_index as u64 * plane_bytes as u64;

        let path = self.path.as_ref().ok_or(BioFormatsError::NotInitialized)?;
        let mut f = 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 full = self.open_bytes(plane_index)?;
        let meta = self.meta.as_ref().unwrap();
        validate_region(meta, x, y, w, h)?;
        let bps = meta.pixel_type.bytes_per_sample();
        let row_bytes = 256 * bps;
        let out_row = w as usize * bps;
        let mut out = Vec::with_capacity(h as usize * out_row);
        for row in 0..h as usize {
            let src = &full[(y as usize + row) * row_bytes..];
            let s = x as usize * bps;
            out.extend_from_slice(&src[s..s + out_row]);
        }
        Ok(out)
    }

    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 validate_region(meta: &ImageMetadata, x: u32, y: u32, w: u32, h: u32) -> Result<()> {
    let x2 = x
        .checked_add(w)
        .ok_or_else(|| BioFormatsError::Format("SimFCS region width overflows".to_string()))?;
    let y2 = y
        .checked_add(h)
        .ok_or_else(|| BioFormatsError::Format("SimFCS region height overflows".to_string()))?;
    if x2 > meta.size_x || y2 > meta.size_y {
        return Err(BioFormatsError::Format(
            "SimFCS region is outside image bounds".to_string(),
        ));
    }
    Ok(())
}

// ── Lambert FLIM Reader ───────────────────────────────────────────────────────

pub struct LambertFlimReader {
    path: Option<PathBuf>,
    meta: Option<ImageMetadata>,
}

impl LambertFlimReader {
    pub fn new() -> Self {
        LambertFlimReader {
            path: None,
            meta: None,
        }
    }

    fn unsupported() -> BioFormatsError {
        BioFormatsError::UnsupportedFormat(
            "Lambert FLIM ASCII payload decoding is not implemented".to_string(),
        )
    }
}

impl Default for LambertFlimReader {
    fn default() -> Self {
        Self::new()
    }
}

impl FormatReader for LambertFlimReader {
    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("asc"))
    }

    fn is_this_type_by_bytes(&self, header: &[u8]) -> bool {
        // Check for Lambert Instruments ASCII heuristic
        if header.len() < 8 {
            return false;
        }
        let s = std::str::from_utf8(&header[..header.len().min(256)]).unwrap_or("");
        s.contains("Lambert") || s.contains("GlobalImages") || s.starts_with('#')
    }

    fn set_id(&mut self, _path: &Path) -> Result<()> {
        self.path = None;
        self.meta = None;
        Err(Self::unsupported())
    }

    fn close(&mut self) -> Result<()> {
        self.path = None;
        self.meta = None;
        Ok(())
    }

    fn series_count(&self) -> usize {
        1
    }
    fn set_series(&mut self, s: usize) -> Result<()> {
        if s != 0 {
            Err(BioFormatsError::SeriesOutOfRange(s))
        } else {
            Ok(())
        }
    }
    fn series(&self) -> usize {
        0
    }

    fn metadata(&self) -> &ImageMetadata {
        self.meta.as_ref().expect("set_id not called")
    }

    fn open_bytes(&mut self, _plane_index: u32) -> Result<Vec<u8>> {
        Err(Self::unsupported())
    }

    fn open_bytes_region(
        &mut self,
        _plane_index: u32,
        _x: u32,
        _y: u32,
        _w: u32,
        _h: u32,
    ) -> Result<Vec<u8>> {
        Err(Self::unsupported())
    }

    fn open_thumb_bytes(&mut self, _plane_index: u32) -> Result<Vec<u8>> {
        Err(Self::unsupported())
    }
}