bioformats 0.1.3

Pure Rust reimplementation of Bio-Formats — read/write scientific image formats
Documentation
/// TIFF tag IDs — mirrors constants in Java `IFD.java`.
#[allow(dead_code)]
pub mod tag {
    pub const NEW_SUBFILE_TYPE: u16 = 254;
    pub const IMAGE_WIDTH: u16 = 256;
    pub const IMAGE_LENGTH: u16 = 257;
    pub const BITS_PER_SAMPLE: u16 = 258;
    pub const COMPRESSION: u16 = 259;
    pub const PHOTOMETRIC_INTERPRETATION: u16 = 262;
    pub const FILL_ORDER: u16 = 266;
    pub const IMAGE_DESCRIPTION: u16 = 270;
    pub const STRIP_OFFSETS: u16 = 273;
    pub const SAMPLES_PER_PIXEL: u16 = 277;
    pub const ROWS_PER_STRIP: u16 = 278;
    pub const STRIP_BYTE_COUNTS: u16 = 279;
    pub const X_RESOLUTION: u16 = 282;
    pub const Y_RESOLUTION: u16 = 283;
    pub const PLANAR_CONFIGURATION: u16 = 284;
    pub const RESOLUTION_UNIT: u16 = 296;
    pub const SOFTWARE: u16 = 305;
    pub const DATE_TIME: u16 = 306;
    pub const PREDICTOR: u16 = 317;
    pub const COLOR_MAP: u16 = 320;
    pub const TILE_WIDTH: u16 = 322;
    pub const TILE_LENGTH: u16 = 323;
    pub const TILE_OFFSETS: u16 = 324;
    pub const TILE_BYTE_COUNTS: u16 = 325;
    pub const EXTRA_SAMPLES: u16 = 338;
    pub const SAMPLE_FORMAT: u16 = 339;
    pub const JPEG_TABLES: u16 = 347;
    pub const SUB_IFD: u16 = 330;
    pub const YCBCR_COEFFICIENTS: u16 = 529;
    pub const YCBCR_SUBSAMPLING: u16 = 530;
    pub const REFERENCE_BLACK_WHITE: u16 = 532;
}

/// TIFF compression scheme codes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Compression {
    None,
    Ccitt,
    Group3Fax,
    Group4Fax,
    PackBits,
    Lzw,
    Jpeg,
    JpegNew,
    Deflate,
    DeflateOld,
    Thunderscan,
    Nikon,
    Zstd,
    Jpeg2000,
    JpegXR,
    Unknown(u16),
}

impl Compression {
    /// True if Java's `TiffParser` bit-reverses each byte of the compressed
    /// strip/tile when `FillOrder == 2` for this compression scheme.
    ///
    /// Java condition (TiffParser.readTile / getTile):
    /// `code <= GROUP_4_FAX(4) || code == DEFLATE(8) || code == PROPRIETARY_DEFLATE(32946)`.
    pub fn reverses_bits_on_fill_order_2(self) -> bool {
        matches!(
            self,
            Compression::None
                | Compression::Ccitt
                | Compression::Group3Fax
                | Compression::Group4Fax
                | Compression::Deflate
                | Compression::DeflateOld
        )
    }
}

impl From<u16> for Compression {
    fn from(v: u16) -> Self {
        match v {
            1 => Compression::None,
            2 => Compression::Ccitt,
            3 => Compression::Group3Fax,
            4 => Compression::Group4Fax,
            5 => Compression::Lzw,
            6 => Compression::Jpeg,
            7 => Compression::JpegNew,
            8 => Compression::Deflate,
            22610 => Compression::JpegXR,
            32773 => Compression::PackBits,
            32809 => Compression::Thunderscan,
            32946 => Compression::DeflateOld,
            33003 => Compression::Jpeg2000, // JPEG2000
            33004 => Compression::Jpeg2000, // JPEG2000 lossy
            33005 => Compression::Jpeg2000, // ALT_JPEG2000
            33007 => Compression::Jpeg,     // ALT_JPEG: baseline JPEGCodec (Java TiffCompression)
            34712 => Compression::Jpeg2000, // Olympus JPEG2000
            34713 => Compression::Nikon,
            50000 => Compression::Zstd,
            other => Compression::Unknown(other),
        }
    }
}

/// Photometric interpretation codes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Photometric {
    MinIsWhite = 0,
    MinIsBlack = 1,
    Rgb = 2,
    Palette = 3,
    TransparencyMask = 4,
    Cmyk = 5,
    YCbCr = 6,
    CIELab = 8,
    Unknown,
}

impl From<u16> for Photometric {
    fn from(v: u16) -> Self {
        match v {
            0 => Photometric::MinIsWhite,
            1 => Photometric::MinIsBlack,
            2 => Photometric::Rgb,
            3 => Photometric::Palette,
            4 => Photometric::TransparencyMask,
            5 => Photometric::Cmyk,
            6 => Photometric::YCbCr,
            8 => Photometric::CIELab,
            _ => Photometric::Unknown,
        }
    }
}

/// A TIFF tag value — can hold different numeric types or a byte array.
#[derive(Debug, Clone)]
pub enum IfdValue {
    Byte(Vec<u8>),
    Ascii(String),
    Short(Vec<u16>),
    Long(Vec<u32>),
    Long8(Vec<u64>), // BigTIFF
    Rational(Vec<(u32, u32)>),
    SByte(Vec<i8>),
    Undefined(Vec<u8>),
    SShort(Vec<i16>),
    SLong(Vec<i32>),
    SRational(Vec<(i32, i32)>),
    Float(Vec<f32>),
    Double(Vec<f64>),
    IFD(Vec<u32>),  // IFD offsets stored as LONG
    IFD8(Vec<u64>), // BigTIFF IFD offsets
}

impl IfdValue {
    pub fn as_u64(&self) -> Option<u64> {
        match self {
            IfdValue::Short(v) if !v.is_empty() => Some(v[0] as u64),
            IfdValue::Long(v) if !v.is_empty() => Some(v[0] as u64),
            IfdValue::Long8(v) if !v.is_empty() => Some(v[0]),
            IfdValue::Byte(v) if !v.is_empty() => Some(v[0] as u64),
            _ => None,
        }
    }

    pub fn as_u32(&self) -> Option<u32> {
        self.as_u64().map(|v| v as u32)
    }

    pub fn as_u16(&self) -> Option<u16> {
        match self {
            IfdValue::Short(v) if !v.is_empty() => Some(v[0]),
            IfdValue::Long(v) if !v.is_empty() => Some(v[0] as u16),
            _ => None,
        }
    }

    pub fn as_vec_u64(&self) -> Vec<u64> {
        match self {
            IfdValue::Short(v) => v.iter().map(|&x| x as u64).collect(),
            IfdValue::Long(v) => v.iter().map(|&x| x as u64).collect(),
            IfdValue::Long8(v) => v.clone(),
            IfdValue::Byte(v) => v.iter().map(|&x| x as u64).collect(),
            IfdValue::IFD(v) => v.iter().map(|&x| x as u64).collect(),
            IfdValue::IFD8(v) => v.clone(),
            _ => vec![],
        }
    }

    pub fn as_vec_u32(&self) -> Vec<u32> {
        self.as_vec_u64().into_iter().map(|v| v as u32).collect()
    }

    pub fn as_vec_u16(&self) -> Vec<u16> {
        match self {
            IfdValue::Short(v) => v.clone(),
            _ => self.as_vec_u64().into_iter().map(|v| v as u16).collect(),
        }
    }

    pub fn as_vec_f32(&self) -> Option<&[f32]> {
        match self {
            IfdValue::Float(v) => Some(v),
            _ => None,
        }
    }

    pub fn as_f64(&self) -> Option<f64> {
        match self {
            IfdValue::Float(v) if !v.is_empty() => Some(v[0] as f64),
            IfdValue::Double(v) if !v.is_empty() => Some(v[0]),
            _ => None,
        }
    }

    pub fn as_str(&self) -> Option<&str> {
        match self {
            IfdValue::Ascii(s) => Some(s.as_str()),
            _ => None,
        }
    }

    /// Interpret a rational/numeric value as a vector of `f64`.
    ///
    /// This mirrors `TiffRational.doubleValue()` in upstream Bio-Formats: each
    /// `(num, den)` pair becomes `num as f64 / den as f64` (a zero denominator
    /// yields `0.0`, matching Java's `TiffRational`). Numeric (non-rational)
    /// types are coerced as a best effort so callers can treat them uniformly.
    pub fn as_vec_f64(&self) -> Vec<f64> {
        let ratio = |n: f64, d: f64| if d == 0.0 { 0.0 } else { n / d };
        match self {
            IfdValue::Rational(v) => v.iter().map(|&(n, d)| ratio(n as f64, d as f64)).collect(),
            IfdValue::SRational(v) => v.iter().map(|&(n, d)| ratio(n as f64, d as f64)).collect(),
            IfdValue::Float(v) => v.iter().map(|&x| x as f64).collect(),
            IfdValue::Double(v) => v.clone(),
            IfdValue::Byte(v) => v.iter().map(|&x| x as f64).collect(),
            IfdValue::SByte(v) => v.iter().map(|&x| x as f64).collect(),
            IfdValue::Short(v) => v.iter().map(|&x| x as f64).collect(),
            IfdValue::SShort(v) => v.iter().map(|&x| x as f64).collect(),
            IfdValue::Long(v) => v.iter().map(|&x| x as f64).collect(),
            IfdValue::SLong(v) => v.iter().map(|&x| x as f64).collect(),
            IfdValue::Long8(v) => v.iter().map(|&x| x as f64).collect(),
            _ => vec![],
        }
    }
}

/// One parsed IFD (Image File Directory).
#[derive(Debug, Clone, Default)]
pub struct Ifd {
    pub entries: std::collections::HashMap<u16, IfdValue>,
}

impl Ifd {
    pub fn get(&self, tag: u16) -> Option<&IfdValue> {
        self.entries.get(&tag)
    }

    pub fn get_u32(&self, tag: u16) -> Option<u32> {
        self.get(tag)?.as_u32()
    }

    pub fn get_u64(&self, tag: u16) -> Option<u64> {
        self.get(tag)?.as_u64()
    }

    pub fn get_u16(&self, tag: u16) -> Option<u16> {
        self.get(tag)?.as_u16()
    }

    pub fn get_vec_u64(&self, tag: u16) -> Vec<u64> {
        self.get(tag).map(|v| v.as_vec_u64()).unwrap_or_default()
    }

    pub fn get_vec_u32(&self, tag: u16) -> Vec<u32> {
        self.get(tag).map(|v| v.as_vec_u32()).unwrap_or_default()
    }

    pub fn get_vec_u16(&self, tag: u16) -> Vec<u16> {
        self.get(tag).map(|v| v.as_vec_u16()).unwrap_or_default()
    }

    pub fn get_str(&self, tag: u16) -> Option<&str> {
        self.get(tag)?.as_str()
    }

    /// Read a tag as a vector of `f64`, interpreting rationals as `num/den`.
    /// Empty when the tag is absent. See [`IfdValue::as_vec_f64`].
    pub fn get_vec_f64(&self, tag: u16) -> Vec<f64> {
        self.get(tag).map(|v| v.as_vec_f64()).unwrap_or_default()
    }

    /// Whether the value stored for `tag` is a (signed or unsigned) rational.
    /// Canon DNG distinguishes a present-but-non-rational white-balance entry
    /// (which triggers the hard-coded fallback table) from a rational one.
    pub fn is_rational(&self, tag: u16) -> bool {
        matches!(
            self.get(tag),
            Some(IfdValue::Rational(_)) | Some(IfdValue::SRational(_))
        )
    }

    // Convenience accessors for common structural tags

    pub fn image_width(&self) -> Option<u32> {
        self.get_u32(tag::IMAGE_WIDTH)
    }

    pub fn image_length(&self) -> Option<u32> {
        self.get_u32(tag::IMAGE_LENGTH)
    }

    pub fn compression(&self) -> Compression {
        Compression::from(self.get_u16(tag::COMPRESSION).unwrap_or(1))
    }

    pub fn photometric(&self) -> Photometric {
        // Java IFD.getPhotometricInterpretation (IFD.java:743-750): when the
        // PhotometricInterpretation tag is absent and the compression is OLD_JPEG,
        // the photometric is RGB. Otherwise fall back to the default (BlackIsZero).
        match self.get_u16(tag::PHOTOMETRIC_INTERPRETATION) {
            Some(pi) => Photometric::from(pi),
            None if self.is_old_jpeg() => Photometric::Rgb,
            None => Photometric::from(1),
        }
    }

    /// Raw Compression tag code (TIFF tag 259), before mapping to `Compression`.
    /// OLD_JPEG (code 6) and ALT_JPEG (code 33007) both map to `Compression::Jpeg`,
    /// but Java only special-cases OLD_JPEG, so the raw code is needed to match it.
    fn compression_code(&self) -> u16 {
        self.get_u16(tag::COMPRESSION).unwrap_or(1)
    }

    /// True for old-style JPEG (TIFF compression code 6, Java `TiffCompression.OLD_JPEG`).
    fn is_old_jpeg(&self) -> bool {
        self.compression_code() == 6
    }

    pub fn samples_per_pixel(&self) -> u16 {
        // Java IFD.getSamplesPerPixel (IFD.java:693-698) always returns 3 for
        // OLD_JPEG ("always RGB"), regardless of the SamplesPerPixel tag.
        if self.is_old_jpeg() {
            return 3;
        }
        self.get_u16(tag::SAMPLES_PER_PIXEL).unwrap_or(1)
    }

    pub fn bits_per_sample(&self) -> Vec<u16> {
        let v = self.get_vec_u16(tag::BITS_PER_SAMPLE);
        if v.is_empty() {
            vec![1]
        } else {
            v
        }
    }

    pub fn planar_configuration(&self) -> u16 {
        self.get_u16(tag::PLANAR_CONFIGURATION).unwrap_or(1)
    }

    pub fn predictor(&self) -> u16 {
        self.get_u16(tag::PREDICTOR).unwrap_or(1)
    }

    pub fn is_tiled(&self) -> bool {
        self.entries.contains_key(&tag::TILE_OFFSETS)
    }

    pub fn tile_width(&self) -> Option<u32> {
        self.get_u32(tag::TILE_WIDTH)
    }

    pub fn tile_length(&self) -> Option<u32> {
        self.get_u32(tag::TILE_LENGTH)
    }
}