mrc 0.5.1

MRC-2014 file format reader/writer for cryo-EM — SIMD-accelerated, mmap-enabled
Documentation
// ============================================================================
// SERI — SerialEM format
// ============================================================================

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// Size of a single SERI (SerialEM) record, in bytes.
pub const SERI_RECORD_SIZE: usize = 256;

/// SerialEM extended header record.
///
/// SerialEM stores tilt-series metadata in 256-byte records.  The format
/// is documented in the IMOD documentation at
/// <http://bio3d.colorado.edu/imod/doc/mrc_format.txt>.
///
/// The tilt angle at bytes 0–3 (little-endian `f32`) is the most commonly
/// accessed field and is exposed directly.  All other fields are accessible
/// via the [`raw`](SeriRecord::raw) byte array.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct SeriRecord {
    /// Tilt angle in degrees, read from bytes 0–3 as little-endian `f32`.
    /// This is the single most commonly accessed field in SerialEM records.
    pub alpha_tilt: f32,
    /// Raw 256-byte record — access unparsed fields via this slice.
    #[cfg_attr(feature = "serde", serde(with = "crate::serde_byte_array"))]
    pub raw: [u8; SERI_RECORD_SIZE],
}

impl SeriRecord {
    /// Parse a single SerialEM record from bytes.
    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
        if bytes.len() < SERI_RECORD_SIZE {
            return None;
        }
        let mut raw = [0u8; SERI_RECORD_SIZE];
        raw.copy_from_slice(&bytes[..SERI_RECORD_SIZE]);
        let alpha_tilt = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
        Some(Self { raw, alpha_tilt })
    }
}

/// Parse extended header bytes as SerialEM records.
pub fn parse_seri_records(bytes: &[u8]) -> Option<Vec<SeriRecord>> {
    if bytes.is_empty() || bytes.len() % SERI_RECORD_SIZE != 0 {
        return None;
    }
    let count = bytes.len() / SERI_RECORD_SIZE;
    let mut records = Vec::with_capacity(count);
    for i in 0..count {
        let start = i * SERI_RECORD_SIZE;
        records.push(SeriRecord::from_bytes(
            &bytes[start..start + SERI_RECORD_SIZE],
        )?);
    }
    Some(records)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn seri_roundtrip() {
        let mut buf = vec![0u8; SERI_RECORD_SIZE];
        buf[0..4].copy_from_slice(&(-35.5f32).to_le_bytes());
        buf[4] = 99;
        let records = super::parse_seri_records(&buf).unwrap();
        assert_eq!(records.len(), 1);
        assert!((records[0].alpha_tilt - (-35.5)).abs() < 1e-6);
        assert_eq!(records[0].raw[4], 99);
    }

    #[test]
    fn seri_empty() {
        assert!(super::parse_seri_records(&[]).is_none());
    }
}