bioformats 0.1.3

Pure Rust reimplementation of Bio-Formats — read/write scientific image formats
Documentation
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;
use std::fs;
use std::path::{Path, PathBuf};

pub struct PngReader {
    path: Option<PathBuf>,
    meta: Option<ImageMetadata>,
    pixels: Option<Vec<u8>>,
}

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

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

fn load_png(path: &Path) -> Result<(ImageMetadata, Vec<u8>)> {
    use image::GenericImageView;
    let img = image::open(path).map_err(|e| BioFormatsError::Format(e.to_string()))?;
    let (w, h) = img.dimensions();

    let (pixel_type, is_rgb, spp, raw) = match img {
        image::DynamicImage::ImageLuma8(buf) => (PixelType::Uint8, false, 1u32, buf.into_raw()),
        image::DynamicImage::ImageLumaA8(buf) => (PixelType::Uint8, false, 2, buf.into_raw()),
        image::DynamicImage::ImageRgb8(buf) => (PixelType::Uint8, true, 3, buf.into_raw()),
        image::DynamicImage::ImageRgba8(buf) => (PixelType::Uint8, true, 4, buf.into_raw()),
        image::DynamicImage::ImageLuma16(buf) => {
            let raw: Vec<u8> = buf
                .into_raw()
                .iter()
                .flat_map(|v| v.to_le_bytes())
                .collect();
            (PixelType::Uint16, false, 1, raw)
        }
        image::DynamicImage::ImageRgb16(buf) => {
            let raw: Vec<u8> = buf
                .into_raw()
                .iter()
                .flat_map(|v| v.to_le_bytes())
                .collect();
            (PixelType::Uint16, true, 3, raw)
        }
        image::DynamicImage::ImageRgba16(buf) => {
            let raw: Vec<u8> = buf
                .into_raw()
                .iter()
                .flat_map(|v| v.to_le_bytes())
                .collect();
            (PixelType::Uint16, true, 4, raw)
        }
        other => {
            let rgb8 = other.to_rgb8();
            (PixelType::Uint8, true, 3, rgb8.into_raw())
        }
    };

    let bpp = pixel_type.bytes_per_sample() as u8 * 8;
    let meta = ImageMetadata {
        size_x: w,
        size_y: h,
        size_z: 1,
        size_c: spp,
        size_t: 1,
        pixel_type,
        bits_per_pixel: bpp,
        image_count: 1,
        dimension_order: DimensionOrder::XYCZT,
        is_rgb,
        is_interleaved: true,
        is_indexed: false,
        is_little_endian: true,
        resolution_count: 1,
        ..Default::default()
    };
    Ok((meta, raw))
}

fn contains_apng_animation_control(path: &Path) -> Result<bool> {
    let bytes = fs::read(path)?;
    let Some(mut offset) = bytes
        .strip_prefix(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A])
        .map(|_| 8usize)
    else {
        return Ok(false);
    };

    while offset.checked_add(8).is_some_and(|end| end <= bytes.len()) {
        let length = u32::from_be_bytes([
            bytes[offset],
            bytes[offset + 1],
            bytes[offset + 2],
            bytes[offset + 3],
        ]) as usize;
        let chunk_type = &bytes[offset + 4..offset + 8];
        if chunk_type == b"acTL" {
            return Ok(true);
        }
        if chunk_type == b"IDAT" || chunk_type == b"IEND" {
            return Ok(false);
        }
        offset = offset
            .checked_add(12)
            .and_then(|v| v.checked_add(length))
            .ok_or_else(|| BioFormatsError::InvalidData("PNG chunk offset overflows".into()))?;
    }

    Ok(false)
}

impl FormatReader for PngReader {
    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("png"))
            .unwrap_or(false)
    }

    fn is_this_type_by_bytes(&self, header: &[u8]) -> bool {
        header.starts_with(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A])
    }

    fn set_id(&mut self, path: &Path) -> Result<()> {
        self.close()?;
        if contains_apng_animation_control(path)? {
            return Err(BioFormatsError::UnsupportedFormat(
                "animated PNG is not supported; use a still PNG image".into(),
            ));
        }
        let (meta, pixels) = load_png(path)?;
        self.path = Some(path.to_path_buf());
        self.meta = Some(meta);
        self.pixels = Some(pixels);
        Ok(())
    }

    fn close(&mut self) -> Result<()> {
        self.path = None;
        self.meta = None;
        self.pixels = 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>> {
        if plane_index != 0 {
            return Err(BioFormatsError::PlaneOutOfRange(plane_index));
        }
        self.pixels.clone().ok_or(BioFormatsError::NotInitialized)
    }

    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();
        crop_full_plane("PNG", &full, meta, meta.size_c as usize, 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)
    }
}

use crate::common::writer::FormatWriter;

pub struct PngWriter {
    path: Option<PathBuf>,
    meta: Option<ImageMetadata>,
    wrote: bool,
}

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

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

impl FormatWriter for PngWriter {
    fn is_this_type(&self, path: &Path) -> bool {
        path.extension()
            .and_then(|e| e.to_str())
            .map(|e| e.eq_ignore_ascii_case("png"))
            .unwrap_or(false)
    }

    fn set_metadata(&mut self, meta: &ImageMetadata) -> Result<()> {
        let logical_c = if meta.is_rgb { 1 } else { meta.size_c.max(1) };
        let required_planes = meta
            .size_z
            .max(1)
            .checked_mul(logical_c)
            .and_then(|v| v.checked_mul(meta.size_t.max(1)))
            .ok_or_else(|| BioFormatsError::Format("PNG writer plane count overflow".into()))?;
        if required_planes > 1 || meta.image_count > 1 {
            return Err(BioFormatsError::UnsupportedFormat(
                "PNG writer supports only one plane".into(),
            ));
        }
        self.meta = Some(meta.clone());
        self.wrote = false;
        Ok(())
    }

    fn set_id(&mut self, path: &Path) -> Result<()> {
        self.meta
            .as_ref()
            .ok_or_else(|| BioFormatsError::Format("set_metadata first".into()))?;
        self.path = Some(path.to_path_buf());
        Ok(())
    }

    fn close(&mut self) -> Result<()> {
        if self.path.is_some() && !self.wrote {
            return Err(BioFormatsError::Format(
                "PNG writer closed before plane 0 was written".into(),
            ));
        }
        self.path = None;
        self.meta = None;
        self.wrote = false;
        Ok(())
    }

    fn save_bytes(&mut self, plane_index: u32, data: &[u8]) -> Result<()> {
        if plane_index != 0 {
            return Err(BioFormatsError::Format(
                "PNG writer supports only one plane".into(),
            ));
        }
        if self.wrote {
            return Err(BioFormatsError::Format(
                "PNG writer already wrote plane 0".into(),
            ));
        }
        let meta = self.meta.as_ref().ok_or(BioFormatsError::NotInitialized)?;
        let path = self.path.as_ref().ok_or(BioFormatsError::NotInitialized)?;

        let (w, h) = (meta.size_x, meta.size_y);
        let spp = meta.size_c as usize;
        let expected_len = (meta.size_x as usize)
            .checked_mul(meta.size_y as usize)
            .and_then(|px| px.checked_mul(spp))
            .and_then(|samples| samples.checked_mul(meta.pixel_type.bytes_per_sample()))
            .ok_or_else(|| BioFormatsError::Format("PNG writer image plane is too large".into()))?;
        if data.len() != expected_len {
            return Err(BioFormatsError::InvalidData(format!(
                "PNG writer: plane 0 has {} bytes, expected {}",
                data.len(),
                expected_len
            )));
        }

        let img: image::DynamicImage = match (meta.pixel_type, spp) {
            (PixelType::Uint8, 1) => image::GrayImage::from_raw(w, h, data.to_vec())
                .map(image::DynamicImage::ImageLuma8)
                .ok_or_else(|| BioFormatsError::InvalidData("bad data length".into()))?,
            (PixelType::Uint8, 3) => image::RgbImage::from_raw(w, h, data.to_vec())
                .map(image::DynamicImage::ImageRgb8)
                .ok_or_else(|| BioFormatsError::InvalidData("bad data length".into()))?,
            (PixelType::Uint8, 4) => image::RgbaImage::from_raw(w, h, data.to_vec())
                .map(image::DynamicImage::ImageRgba8)
                .ok_or_else(|| BioFormatsError::InvalidData("bad data length".into()))?,
            (PixelType::Uint16, 1) => {
                let pixels: Vec<u16> = data
                    .chunks_exact(2)
                    .map(|c| u16::from_le_bytes([c[0], c[1]]))
                    .collect();
                image::ImageBuffer::<image::Luma<u16>, _>::from_raw(w, h, pixels)
                    .map(image::DynamicImage::ImageLuma16)
                    .ok_or_else(|| BioFormatsError::InvalidData("bad data length".into()))?
            }
            (PixelType::Uint16, 3) => {
                let pixels: Vec<u16> = data
                    .chunks_exact(2)
                    .map(|c| u16::from_le_bytes([c[0], c[1]]))
                    .collect();
                image::ImageBuffer::<image::Rgb<u16>, _>::from_raw(w, h, pixels)
                    .map(image::DynamicImage::ImageRgb16)
                    .ok_or_else(|| BioFormatsError::InvalidData("bad data length".into()))?
            }
            _ => {
                return Err(BioFormatsError::UnsupportedFormat(format!(
                    "PNG writer: unsupported {:?} spp={}",
                    meta.pixel_type, spp
                )));
            }
        };

        img.save(path)
            .map_err(|e| BioFormatsError::Format(e.to_string()))?;
        self.wrote = true;
        Ok(())
    }

    fn can_do_stacks(&self) -> bool {
        false
    }
}