pub mod agar;
pub mod ccp4;
pub mod fei;
pub mod mrco;
pub mod seri;
pub use agar::{AGAR_RECORD_SIZE, AgarRecord, parse_agar_records};
pub use ccp4::{CCP4_RECORD_SIZE, Ccp4Record, parse_ccp4_records};
pub use fei::{
FEI1_RECORD_SIZE, FEI2_RECORD_SIZE, Fei1Metadata, Fei2Metadata, parse_fei1_records,
parse_fei2_records,
};
pub use mrco::{MRCO_RECORD_SIZE, MrcoRecord, parse_mrco_records};
pub use seri::{SERI_RECORD_SIZE, SeriRecord, parse_seri_records};
use crate::Mode;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum ExtHeaderType {
Ccp4,
Mrco,
Seri,
Agar,
Fei1,
Fei2,
Hdf5,
Unknown([u8; 4]),
}
impl ExtHeaderType {
pub fn from_exttyp(exttyp: [u8; 4]) -> Self {
match &exttyp {
b"CCP4" => Self::Ccp4,
b"MRCO" => Self::Mrco,
b"SERI" => Self::Seri,
b"AGAR" => Self::Agar,
b"FEI1" => Self::Fei1,
b"FEI2" => Self::Fei2,
b"HDF5" => Self::Hdf5,
_ => Self::Unknown(exttyp),
}
}
#[inline]
pub fn from_header(header: &Header) -> Self {
Self::from_exttyp(header.exttyp())
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum ExtHeaderData {
Ccp4(Vec<Ccp4Record>),
Mrco(Vec<MrcoRecord>),
Seri(Vec<SeriRecord>),
Agar(Vec<AgarRecord>),
Fei1(Vec<Fei1Metadata>),
Fei2(Vec<Fei2Metadata>),
None,
}
impl ExtHeaderData {
pub fn parse(ext_type: ExtHeaderType, bytes: &[u8]) -> Self {
if bytes.is_empty() {
return Self::None;
}
match ext_type {
ExtHeaderType::Ccp4 => parse_ccp4_records(bytes)
.map(Self::Ccp4)
.unwrap_or(Self::None),
ExtHeaderType::Mrco => parse_mrco_records(bytes)
.map(Self::Mrco)
.unwrap_or(Self::None),
ExtHeaderType::Seri => parse_seri_records(bytes)
.map(Self::Seri)
.unwrap_or(Self::None),
ExtHeaderType::Agar => parse_agar_records(bytes)
.map(Self::Agar)
.unwrap_or(Self::None),
ExtHeaderType::Fei1 => parse_fei1_records(bytes)
.map(Self::Fei1)
.unwrap_or(Self::None),
ExtHeaderType::Fei2 => parse_fei2_records(bytes)
.map(Self::Fei2)
.unwrap_or(Self::None),
ExtHeaderType::Hdf5 | ExtHeaderType::Unknown(_) => Self::None,
}
}
#[inline]
pub fn from_header(header: &Header, bytes: &[u8]) -> Self {
Self::parse(ExtHeaderType::from_header(header), bytes)
}
}
const OFFSET_NX: usize = 0;
const OFFSET_NY: usize = 4;
const OFFSET_NZ: usize = 8;
const OFFSET_MODE: usize = 12;
const OFFSET_NXSTART: usize = 16;
const OFFSET_NYSTART: usize = 20;
const OFFSET_NZSTART: usize = 24;
const OFFSET_MX: usize = 28;
const OFFSET_MY: usize = 32;
const OFFSET_MZ: usize = 36;
const OFFSET_XLEN: usize = 40;
const OFFSET_YLEN: usize = 44;
const OFFSET_ZLEN: usize = 48;
const OFFSET_ALPHA: usize = 52;
const OFFSET_BETA: usize = 56;
const OFFSET_GAMMA: usize = 60;
const OFFSET_MAPC: usize = 64;
const OFFSET_MAPR: usize = 68;
const OFFSET_MAPS: usize = 72;
const OFFSET_DMIN: usize = 76;
const OFFSET_DMAX: usize = 80;
const OFFSET_DMEAN: usize = 84;
const OFFSET_ISPG: usize = 88;
const OFFSET_NSYMBT: usize = 92;
const OFFSET_EXTRA: usize = 96;
const OFFSET_EXTTYP: usize = 104; const OFFSET_NVERSION: usize = 108; const OFFSET_ORIGIN: usize = 196;
const OFFSET_MAP: usize = 208;
const OFFSET_MACHST: usize = 212;
const OFFSET_RMS: usize = 216;
const OFFSET_NLABL: usize = 220;
const OFFSET_LABEL: usize = 224;
const DEFAULT_EXTRA: [u8; 100] = {
let mut e = [0u8; 100];
e[12] = 0xAD;
e[13] = 0x4E;
e[14] = 0x00;
e[15] = 0x00;
e
};
#[repr(C, align(4))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Header {
pub nx: i32,
pub ny: i32,
pub nz: i32,
pub mode: i32,
pub nxstart: i32,
pub nystart: i32,
pub nzstart: i32,
pub mx: i32,
pub my: i32,
pub mz: i32,
pub xlen: f32,
pub ylen: f32,
pub zlen: f32,
pub alpha: f32,
pub beta: f32,
pub gamma: f32,
pub mapc: i32,
pub mapr: i32,
pub maps: i32,
pub dmin: f32,
pub dmax: f32,
pub dmean: f32,
pub ispg: i32,
pub nsymbt: i32,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_byte_array"))]
pub extra: [u8; 100],
pub origin: [f32; 3],
pub map: [u8; 4],
pub machst: [u8; 4],
pub rms: f32,
pub nlabl: i32,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_byte_array"))]
pub label: [u8; 800],
}
impl Default for Header {
fn default() -> Self {
Self::new()
}
}
impl Header {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
nx: 0,
ny: 0,
nz: 0,
mode: 2, nxstart: 0,
nystart: 0,
nzstart: 0,
mx: 0,
my: 0,
mz: 0,
xlen: 1.0, ylen: 1.0,
zlen: 1.0,
alpha: 90.0,
beta: 90.0,
gamma: 90.0,
mapc: 1, mapr: 2, maps: 3, dmin: 0.0, dmax: -1.0, dmean: -2.0, ispg: 1, nsymbt: 0,
extra: DEFAULT_EXTRA,
origin: [0.0; 3],
map: *b"MAP ",
machst: [0x44, 0x44, 0x00, 0x00], rms: -1.0, nlabl: 0,
label: [0; 800],
}
}
#[inline]
pub const fn data_offset(&self) -> usize {
if self.nsymbt < 0 {
1024
} else {
1024 + self.nsymbt as usize
}
}
#[inline]
pub fn data_size(&self) -> Option<usize> {
let nx = self.nx.max(0) as usize;
let ny = self.ny.max(0) as usize;
let nz = self.nz.max(0) as usize;
match Mode::from_i32(self.mode) {
Some(mode) => {
match mode {
Mode::Packed4Bit => {
let row_bytes = nx.div_ceil(2);
ny.checked_mul(row_bytes)?.checked_mul(nz)
}
_ => nx
.checked_mul(ny)?
.checked_mul(nz)?
.checked_mul(mode.byte_size()),
}
}
None => None, }
}
#[inline]
pub fn validate(&self) -> bool {
self.validate_detailed().is_ok()
}
#[inline]
pub fn validate_detailed(&self) -> Result<(), crate::HeaderValidationError> {
use crate::HeaderValidationError;
if self.nx <= 0 || self.ny <= 0 || self.nz <= 0 {
return Err(HeaderValidationError::InvalidDimensions {
nx: self.nx,
ny: self.ny,
nz: self.nz,
});
}
if Mode::from_i32(self.mode).is_none() {
return Err(HeaderValidationError::UnsupportedMode(self.mode));
}
if !self.validate_map() {
return Err(HeaderValidationError::InvalidMap(self.map));
}
if !(self.ispg == 0
|| (self.ispg >= 1 && self.ispg <= 230)
|| (self.ispg >= 400 && self.ispg <= 630))
{
return Err(HeaderValidationError::InvalidIspg(self.ispg));
}
if !(matches!(self.mapc, 1..=3)
&& matches!(self.mapr, 1..=3)
&& matches!(self.maps, 1..=3)
&& self.mapc != self.mapr
&& self.mapc != self.maps
&& self.mapr != self.maps)
{
return Err(HeaderValidationError::InvalidAxisMapping {
mapc: self.mapc,
mapr: self.mapr,
maps: self.maps,
});
}
if self.nsymbt < 0 {
return Err(HeaderValidationError::InvalidNsymbt(self.nsymbt));
}
if self.nlabl < 0 || self.nlabl > 10 {
return Err(HeaderValidationError::InvalidNlabl(self.nlabl));
}
let actual_labels = self.count_non_empty_labels();
if actual_labels != self.nlabl as usize {
return Err(HeaderValidationError::LabelCountMismatch {
nlabl: self.nlabl,
actual: actual_labels as i32,
});
}
for i in 0..self.nlabl as usize {
if self.label_is_empty(i) {
return Err(HeaderValidationError::EmptyLabelBeforeFilled { index: i as i32 });
}
}
let nversion = self.nversion();
if nversion != 0 && nversion != 20140 && nversion != 20141 {
return Err(HeaderValidationError::InvalidNversion(nversion));
}
if self.mx <= 0 || self.my <= 0 || self.mz <= 0 {
return Err(HeaderValidationError::InvalidSampling {
mx: self.mx,
my: self.my,
mz: self.mz,
});
}
if self.ispg >= 400 && self.ispg <= 630 && self.mz != 0 && self.nz % self.mz != 0 {
return Err(HeaderValidationError::InvalidVolumeStack {
nz: self.nz,
mz: self.mz,
ispg: self.ispg,
});
}
Ok(())
}
pub fn validate_permissive(&self) -> Result<Vec<String>, crate::HeaderValidationError> {
use crate::HeaderValidationError;
let mut warnings = Vec::new();
if self.nx <= 0 || self.ny <= 0 || self.nz <= 0 {
return Err(HeaderValidationError::InvalidDimensions {
nx: self.nx,
ny: self.ny,
nz: self.nz,
});
}
if Mode::from_i32(self.mode).is_none() {
return Err(HeaderValidationError::UnsupportedMode(self.mode));
}
if !self.validate_map() {
warnings.push(format!(
"MAP field is non-standard: {:?}",
String::from_utf8_lossy(&self.map)
));
}
if !(self.ispg == 0
|| (self.ispg >= 1 && self.ispg <= 230)
|| (self.ispg >= 400 && self.ispg <= 630))
{
warnings.push(format!(
"ISPG {} is outside the standard ranges (0, 1-230, 400-630)",
self.ispg
));
}
if !(self.mapc != 0
&& self.mapc.abs() <= 3
&& self.mapr != 0
&& self.mapr.abs() <= 3
&& self.maps != 0
&& self.maps.abs() <= 3
&& self.mapc.abs() != self.mapr.abs()
&& self.mapc.abs() != self.maps.abs()
&& self.mapr.abs() != self.maps.abs())
{
warnings.push(format!(
"Axis mapping ({}, {}, {}) is not a valid permutation of axis indices",
self.mapc, self.mapr, self.maps
));
} else if self.mapr == -2 {
warnings.push("mapr = -2 indicates Y-inverted image data (IMOD convention)".into());
}
if self.nsymbt < 0 {
warnings.push(format!("NSYMBT is negative ({})", self.nsymbt));
}
if self.nlabl < 0 || self.nlabl > 10 {
warnings.push(format!("NLABL {} is outside 0-10", self.nlabl));
}
let nversion = self.nversion();
if nversion != 20140 && nversion != 20141 {
warnings.push(format!("NVERSION {} is not 20140 or 20141", nversion));
}
if self.mx <= 0 || self.my <= 0 || self.mz <= 0 {
warnings.push(format!(
"Sampling (mx={}, my={}, mz={}) is not all positive",
self.mx, self.my, self.mz
));
}
if self.ispg >= 400 && self.ispg <= 630 && self.mz != 0 && self.nz % self.mz != 0 {
warnings.push(format!(
"Volume stack: nz ({}) is not divisible by mz ({}) for ispg={}",
self.nz, self.mz, self.ispg
));
}
Ok(warnings)
}
#[inline]
fn validate_map(&self) -> bool {
if self.map == *b"MAP " {
return true;
}
if &self.map[..3] == b"MAP"
&& (self.map[3] == b' ' || self.map[3] == 0 || self.map[3] == b'I')
{
return true;
}
if self.map == [0; 4] {
return true;
}
false
}
#[inline]
pub fn exttyp(&self) -> [u8; 4] {
[
self.extra[OFFSET_EXTTYP - OFFSET_EXTRA],
self.extra[OFFSET_EXTTYP - OFFSET_EXTRA + 1],
self.extra[OFFSET_EXTTYP - OFFSET_EXTRA + 2],
self.extra[OFFSET_EXTTYP - OFFSET_EXTRA + 3],
]
}
#[inline]
pub fn set_exttyp(&mut self, value: [u8; 4]) {
let start = OFFSET_EXTTYP - OFFSET_EXTRA;
self.extra[start..start + 4].copy_from_slice(&value);
}
#[inline]
pub fn exttyp_str(&self) -> Result<&str, core::str::Utf8Error> {
let start = OFFSET_EXTTYP - OFFSET_EXTRA;
core::str::from_utf8(&self.extra[start..start + 4])
}
#[inline]
pub fn set_exttyp_str(&mut self, value: &str) -> Result<(), &'static str> {
if value.len() != 4 {
return Err("EXTTYP must be exactly 4 characters");
}
let bytes = value.as_bytes();
let start = OFFSET_EXTTYP - OFFSET_EXTRA;
self.extra[start..start + 4].copy_from_slice(bytes);
Ok(())
}
#[inline]
pub fn nversion(&self) -> i32 {
use crate::engine::codec::EndianCodec;
let file_endian = self.detect_endian();
let start = OFFSET_NVERSION - OFFSET_EXTRA;
i32::decode(&self.extra[start..start + 4], 0, file_endian)
}
#[inline]
pub fn set_nversion(&mut self, value: i32) {
use crate::engine::codec::EndianCodec;
let file_endian = self.detect_endian();
let start = OFFSET_NVERSION - OFFSET_EXTRA;
value.encode(&mut self.extra[start..start + 4], 0, file_endian);
}
pub fn get_labels(&self) -> Vec<String> {
let count = self.nlabl.clamp(0, 10) as usize;
let mut labels = Vec::with_capacity(count);
for i in 0..count {
let start = i * 80;
let bytes = &self.label[start..start + 80];
let text = String::from_utf8_lossy(bytes);
labels.push(text.trim_end().to_string());
}
labels
}
fn label_is_empty(&self, index: usize) -> bool {
let start = index * 80;
self.label[start..start + 80]
.iter()
.all(|&b| b == 0 || b == b' ')
}
fn count_non_empty_labels(&self) -> usize {
(0..10).filter(|&i| !self.label_is_empty(i)).count()
}
pub fn add_label(&mut self, text: &str) {
let filtered: String = text
.chars()
.map(|c| {
if c.is_ascii_graphic() || c == ' ' {
c
} else {
' '
}
})
.take(80)
.collect();
let bytes = filtered.as_bytes();
let len = bytes.len();
let count = self.count_non_empty_labels();
if count < 10 {
let slot = (0..10).find(|&i| self.label_is_empty(i)).unwrap_or(count);
let start = slot * 80;
self.label[start..start + 80].fill(b' ');
self.label[start..start + len].copy_from_slice(bytes);
} else {
self.label.copy_within(80..800, 0);
let start = 9 * 80;
self.label[start..start + 80].fill(b' ');
self.label[start..start + len].copy_from_slice(bytes);
}
self.nlabl = self.count_non_empty_labels() as i32;
}
#[inline]
pub fn detect_endian(&self) -> crate::FileEndian {
crate::FileEndian::from_machst(&self.machst)
}
#[inline]
pub fn set_file_endian(&mut self, endian: crate::FileEndian) {
let current_nversion = self.nversion();
self.machst = endian.to_machst();
self.set_nversion(current_nversion);
}
pub fn is_single_image(&self) -> bool {
self.nz == 1
}
pub fn is_image_stack(&self) -> bool {
self.ispg == 0
}
pub fn is_volume(&self) -> bool {
!self.is_image_stack() && !self.is_volume_stack()
}
pub fn is_volume_stack(&self) -> bool {
(400..=630).contains(&self.ispg)
}
pub fn set_image_stack(&mut self) {
self.ispg = 0;
self.mz = 1;
}
pub fn set_volume(&mut self) {
self.ispg = 1;
self.mz = self.nz;
}
pub fn set_volume_stack(&mut self, mz: i32) {
self.ispg = 401;
self.mz = mz;
}
pub fn voxel_size(&self) -> [f32; 3] {
[
if self.mx == 0 {
0.0
} else {
self.xlen / self.mx as f32
},
if self.my == 0 {
0.0
} else {
self.ylen / self.my as f32
},
if self.mz == 0 {
0.0
} else {
self.zlen / self.mz as f32
},
]
}
pub fn nstart(&self) -> [i32; 3] {
[self.nxstart, self.nystart, self.nzstart]
}
pub fn cell_lengths(&self) -> [f32; 3] {
[self.xlen, self.ylen, self.zlen]
}
pub fn cell_angles(&self) -> [f32; 3] {
[self.alpha, self.beta, self.gamma]
}
pub fn logical_shape(&self) -> [usize; 4] {
if self.is_volume_stack() && self.mz > 0 {
let nvolumes = (self.nz / self.mz) as usize;
[
nvolumes,
self.mz as usize,
self.ny as usize,
self.nx as usize,
]
} else {
[1, self.nz as usize, self.ny as usize, self.nx as usize]
}
}
#[inline]
pub fn sampling(&self) -> [i32; 3] {
[self.mx, self.my, self.mz]
}
#[inline]
pub fn density_stats(&self) -> (f32, f32, f32, f32) {
(self.dmin, self.dmax, self.dmean, self.rms)
}
#[inline]
pub fn is_standard_map(&self) -> bool {
self.map == *b"MAP "
}
pub fn label_at(&self, index: usize) -> Option<&str> {
if index >= self.nlabl.clamp(0, 10) as usize || self.label_is_empty(index) {
return None;
}
let start = index * 80;
let end = start
+ self.label[start..start + 80]
.iter()
.rposition(|&b| b != b' ')
.map_or(0, |p| p + 1);
let trimmed = core::str::from_utf8(&self.label[start..end]).unwrap_or("<invalid utf-8>");
Some(trimmed)
}
pub fn cell_volume(&self) -> f64 {
let a = self.xlen as f64;
let b = self.ylen as f64;
let c = self.zlen as f64;
if a <= 0.0 || b <= 0.0 || c <= 0.0 {
return 0.0;
}
let alpha = self.alpha as f64 * (core::f64::consts::PI / 180.0);
let beta = self.beta as f64 * (core::f64::consts::PI / 180.0);
let gamma = self.gamma as f64 * (core::f64::consts::PI / 180.0);
let cos_a = alpha.cos();
let cos_b = beta.cos();
let cos_g = gamma.cos();
a * b
* c
* (1.0 - cos_a * cos_a - cos_b * cos_b - cos_g * cos_g + 2.0 * cos_a * cos_b * cos_g)
.sqrt()
}
pub fn decode_from_bytes(bytes: &[u8; 1024]) -> Self {
Self::decode_from_bytes_with_info(bytes).0
}
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EndianFallbackWarning {
MachstLeDataBe,
MachstBeDataLe,
}
impl core::fmt::Display for EndianFallbackWarning {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::MachstLeDataBe => write!(
f,
"MACHST indicates little-endian but MODE is valid only as \
big-endian; using big-endian"
),
Self::MachstBeDataLe => write!(
f,
"MACHST indicates big-endian but MODE is valid only as \
little-endian; using little-endian"
),
}
}
}
impl Header {
pub fn decode_from_bytes_with_info(
bytes: &[u8; 1024],
) -> (Self, Option<EndianFallbackWarning>) {
use crate::engine::endian::FileEndian;
let machst = [
bytes[OFFSET_MACHST],
bytes[OFFSET_MACHST + 1],
bytes[OFFSET_MACHST + 2],
bytes[OFFSET_MACHST + 3],
];
let detected = FileEndian::from_machst(&machst);
let header = Self::decode_with_endian(bytes, detected);
if crate::Mode::from_i32(header.mode).is_none() {
let opposite = detected.opposite();
let candidate = Self::decode_with_endian(bytes, opposite);
if crate::Mode::from_i32(candidate.mode).is_some() {
let warning = match detected {
FileEndian::LittleEndian => EndianFallbackWarning::MachstLeDataBe,
FileEndian::BigEndian => EndianFallbackWarning::MachstBeDataLe,
};
return (candidate, Some(warning));
}
}
(header, None)
}
fn decode_with_endian(bytes: &[u8; 1024], file_endian: crate::FileEndian) -> Self {
use crate::engine::codec::EndianCodec;
let mut header = Self::new();
header.nx = i32::decode(bytes, OFFSET_NX, file_endian);
header.ny = i32::decode(bytes, OFFSET_NY, file_endian);
header.nz = i32::decode(bytes, OFFSET_NZ, file_endian);
header.mode = i32::decode(bytes, OFFSET_MODE, file_endian);
header.nxstart = i32::decode(bytes, OFFSET_NXSTART, file_endian);
header.nystart = i32::decode(bytes, OFFSET_NYSTART, file_endian);
header.nzstart = i32::decode(bytes, OFFSET_NZSTART, file_endian);
header.mx = i32::decode(bytes, OFFSET_MX, file_endian);
header.my = i32::decode(bytes, OFFSET_MY, file_endian);
header.mz = i32::decode(bytes, OFFSET_MZ, file_endian);
header.xlen = f32::decode(bytes, OFFSET_XLEN, file_endian);
header.ylen = f32::decode(bytes, OFFSET_YLEN, file_endian);
header.zlen = f32::decode(bytes, OFFSET_ZLEN, file_endian);
header.alpha = f32::decode(bytes, OFFSET_ALPHA, file_endian);
header.beta = f32::decode(bytes, OFFSET_BETA, file_endian);
header.gamma = f32::decode(bytes, OFFSET_GAMMA, file_endian);
header.mapc = i32::decode(bytes, OFFSET_MAPC, file_endian);
header.mapr = i32::decode(bytes, OFFSET_MAPR, file_endian);
header.maps = i32::decode(bytes, OFFSET_MAPS, file_endian);
header.dmin = f32::decode(bytes, OFFSET_DMIN, file_endian);
header.dmax = f32::decode(bytes, OFFSET_DMAX, file_endian);
header.dmean = f32::decode(bytes, OFFSET_DMEAN, file_endian);
header.ispg = i32::decode(bytes, OFFSET_ISPG, file_endian);
header.nsymbt = i32::decode(bytes, OFFSET_NSYMBT, file_endian);
header
.extra
.copy_from_slice(&bytes[OFFSET_EXTRA..OFFSET_ORIGIN]);
header.origin[0] = f32::decode(bytes, OFFSET_ORIGIN, file_endian);
header.origin[1] = f32::decode(bytes, OFFSET_ORIGIN + 4, file_endian);
header.origin[2] = f32::decode(bytes, OFFSET_ORIGIN + 8, file_endian);
header
.map
.copy_from_slice(&bytes[OFFSET_MAP..OFFSET_MACHST]);
header
.machst
.copy_from_slice(&bytes[OFFSET_MACHST..OFFSET_RMS]);
header.rms = f32::decode(bytes, OFFSET_RMS, file_endian);
header.nlabl = i32::decode(bytes, OFFSET_NLABL, file_endian);
header.label.copy_from_slice(&bytes[OFFSET_LABEL..1024]);
header
}
pub fn encode_to_bytes(&self, out: &mut [u8; 1024]) {
use crate::engine::codec::EndianCodec;
let file_endian = self.detect_endian();
self.nx.encode(out, OFFSET_NX, file_endian);
self.ny.encode(out, OFFSET_NY, file_endian);
self.nz.encode(out, OFFSET_NZ, file_endian);
self.mode.encode(out, OFFSET_MODE, file_endian);
self.nxstart.encode(out, OFFSET_NXSTART, file_endian);
self.nystart.encode(out, OFFSET_NYSTART, file_endian);
self.nzstart.encode(out, OFFSET_NZSTART, file_endian);
self.mx.encode(out, OFFSET_MX, file_endian);
self.my.encode(out, OFFSET_MY, file_endian);
self.mz.encode(out, OFFSET_MZ, file_endian);
self.xlen.encode(out, OFFSET_XLEN, file_endian);
self.ylen.encode(out, OFFSET_YLEN, file_endian);
self.zlen.encode(out, OFFSET_ZLEN, file_endian);
self.alpha.encode(out, OFFSET_ALPHA, file_endian);
self.beta.encode(out, OFFSET_BETA, file_endian);
self.gamma.encode(out, OFFSET_GAMMA, file_endian);
self.mapc.encode(out, OFFSET_MAPC, file_endian);
self.mapr.encode(out, OFFSET_MAPR, file_endian);
self.maps.encode(out, OFFSET_MAPS, file_endian);
self.dmin.encode(out, OFFSET_DMIN, file_endian);
self.dmax.encode(out, OFFSET_DMAX, file_endian);
self.dmean.encode(out, OFFSET_DMEAN, file_endian);
self.ispg.encode(out, OFFSET_ISPG, file_endian);
self.nsymbt.encode(out, OFFSET_NSYMBT, file_endian);
out[OFFSET_EXTRA..OFFSET_ORIGIN].copy_from_slice(&self.extra);
self.origin[0].encode(out, OFFSET_ORIGIN, file_endian);
self.origin[1].encode(out, OFFSET_ORIGIN + 4, file_endian);
self.origin[2].encode(out, OFFSET_ORIGIN + 8, file_endian);
out[OFFSET_MAP..OFFSET_MACHST].copy_from_slice(&self.map);
out[OFFSET_MACHST..OFFSET_RMS].copy_from_slice(&self.machst);
self.rms.encode(out, OFFSET_RMS, file_endian);
self.nlabl.encode(out, OFFSET_NLABL, file_endian);
out[OFFSET_LABEL..1024].copy_from_slice(&self.label);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ImodInfo {
pub bytes_are_signed: bool,
}
impl Header {
pub fn detect_imod(&self) -> Option<ImodInfo> {
if self.extra[56..60] == [0x49, 0x4D, 0x4F, 0x44] {
Some(ImodInfo {
bytes_are_signed: (self.extra[60] & 1) != 0,
})
} else {
None
}
}
pub fn is_y_inverted(&self) -> bool {
self.mapr == -2
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum ImodImageType {
Mono,
Tilt,
Tilts,
Lina,
Lins,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ImodMetadata {
pub bytes_are_signed: bool,
pub imod_flags: u16,
pub image_type: ImodImageType,
pub tilt_axis: u8,
pub tilt_increment: f32,
pub start_angle: f32,
pub original_angles: [f32; 3],
pub current_angles: [f32; 3],
pub x_origin: f32,
pub y_origin: f32,
pub z_origin: f32,
pub x_cell_size: f32,
pub y_cell_size: f32,
pub z_cell_size: f32,
}
pub fn parse_imod_metadata(header: &Header) -> Option<ImodMetadata> {
if header.extra[56..60] != [0x49, 0x4D, 0x4F, 0x44] {
return None;
}
let le_i16 = |offset: usize| -> i16 {
i16::from_le_bytes([header.extra[offset], header.extra[offset + 1]])
};
let le_f32 = |offset: usize| -> f32 {
f32::from_le_bytes([
header.extra[offset],
header.extra[offset + 1],
header.extra[offset + 2],
header.extra[offset + 3],
])
};
let idtype = le_i16(64);
let image_type = match idtype {
0 => ImodImageType::Mono,
1 => ImodImageType::Tilt,
2 => ImodImageType::Tilts,
3 => ImodImageType::Lina,
4 => ImodImageType::Lins,
_ => ImodImageType::Mono, };
let flags = le_i16(60) as u16; let bytes_are_signed = (flags & 1) != 0;
let tilt_axis = le_i16(68).clamp(1, 3) as u8;
let tilt_increment = le_i16(72) as f32 / 100.0;
let start_angle = le_i16(74) as f32 / 100.0;
let original_angles = [le_f32(76), le_f32(80), le_f32(84)];
let current_angles = [le_f32(88), le_f32(92), le_f32(96)];
let x_origin = le_f32(0);
let y_origin = le_f32(4);
let z_origin = le_f32(8);
let x_cell_size = le_f32(12);
let y_cell_size = le_f32(16);
let z_cell_size = le_f32(20);
Some(ImodMetadata {
bytes_are_signed,
imod_flags: flags,
image_type,
tilt_axis,
tilt_increment,
start_angle,
original_angles,
current_angles,
x_origin,
y_origin,
z_origin,
x_cell_size,
y_cell_size,
z_cell_size,
})
}
#[derive(Debug, Clone)]
pub struct HeaderBuilder {
header: Header,
}
impl HeaderBuilder {
#[must_use]
pub fn new() -> Self {
Self {
header: Header::new(),
}
}
#[must_use]
pub fn shape(mut self, shape: [usize; 3]) -> Self {
self.header.nx = shape[0] as i32;
self.header.ny = shape[1] as i32;
self.header.nz = shape[2] as i32;
self.header.mx = self.header.nx;
self.header.my = self.header.ny;
self.header.mz = self.header.nz;
self
}
#[must_use]
pub fn mode<T: crate::mode::Voxel>(mut self) -> Self {
self.header.mode = T::MODE.as_i32();
self
}
#[must_use]
pub fn mode_raw(mut self, mode: i32) -> Self {
self.header.mode = mode;
self
}
#[must_use]
pub fn cell_lengths(mut self, xlen: f32, ylen: f32, zlen: f32) -> Self {
self.header.xlen = xlen;
self.header.ylen = ylen;
self.header.zlen = zlen;
self
}
#[must_use]
pub fn cell_angles(mut self, alpha: f32, beta: f32, gamma: f32) -> Self {
self.header.alpha = alpha;
self.header.beta = beta;
self.header.gamma = gamma;
self
}
#[must_use]
pub fn ispg(mut self, ispg: i32) -> Self {
self.header.ispg = ispg;
self
}
#[must_use]
pub fn set_volume_stack(mut self, mz: i32) -> Self {
self.header.set_volume_stack(mz);
self
}
#[must_use]
pub fn exttyp(mut self, exttyp: [u8; 4]) -> Self {
self.header.set_exttyp(exttyp);
self
}
#[must_use]
pub fn nsymbt(mut self, nsymbt: i32) -> Self {
self.header.nsymbt = nsymbt;
self
}
#[must_use]
pub fn origin(mut self, origin: [f32; 3]) -> Self {
self.header.origin = origin;
self
}
#[must_use]
pub fn nstart(mut self, nstart: [i32; 3]) -> Self {
self.header.nxstart = nstart[0];
self.header.nystart = nstart[1];
self.header.nzstart = nstart[2];
self
}
#[must_use]
pub fn sampling(mut self, sampling: [i32; 3]) -> Self {
self.header.mx = sampling[0];
self.header.my = sampling[1];
self.header.mz = sampling[2];
self
}
#[must_use]
pub fn axis_mapping(mut self, mapping: [i32; 3]) -> Self {
self.header.mapc = mapping[0];
self.header.mapr = mapping[1];
self.header.maps = mapping[2];
self
}
#[must_use]
pub fn add_label(mut self, text: &str) -> Self {
self.header.add_label(text);
self
}
pub fn build(self) -> Result<Header, crate::HeaderValidationError> {
self.header.validate_detailed()?;
Ok(self.header)
}
}
impl Default for HeaderBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn le_header_bytes() -> [u8; 1024] {
let mut h = Header::new();
h.nx = 64;
h.ny = 64;
h.nz = 64;
h.mx = 64;
h.my = 64;
h.mz = 64;
h.mode = 2; h.set_file_endian(crate::FileEndian::LittleEndian);
let mut bytes = [0u8; 1024];
h.encode_to_bytes(&mut bytes);
bytes
}
fn be_header_bytes() -> [u8; 1024] {
let mut h = Header::new();
h.nx = 64;
h.ny = 64;
h.nz = 64;
h.mx = 64;
h.my = 64;
h.mz = 64;
h.mode = 2; h.set_file_endian(crate::FileEndian::BigEndian);
let mut bytes = [0u8; 1024];
h.encode_to_bytes(&mut bytes);
bytes
}
#[test]
fn test_decode_roundtrip_le() {
let original = le_header_bytes();
let decoded = Header::decode_from_bytes(&original);
assert_eq!(decoded.nx, 64);
assert_eq!(decoded.mode, 2);
assert_eq!(decoded.detect_endian(), crate::FileEndian::LittleEndian);
}
#[test]
fn test_decode_roundtrip_be() {
let original = be_header_bytes();
let decoded = Header::decode_from_bytes(&original);
assert_eq!(decoded.nx, 64);
assert_eq!(decoded.mode, 2);
assert_eq!(decoded.detect_endian(), crate::FileEndian::BigEndian);
}
#[test]
fn test_byte_order_fallback_le_stamp_be_data() {
let mut bytes = be_header_bytes();
bytes[212] = 0x44;
bytes[213] = 0x44;
bytes[214] = 0x00;
bytes[215] = 0x00;
let (decoded, warning) = Header::decode_from_bytes_with_info(&bytes);
assert_eq!(
decoded.nx, 64,
"byte-order fallback should have corrected nx"
);
assert_eq!(decoded.mode, 2);
assert!(
warning.is_some(),
"should emit a warning when MACHST mismatches actual byte order"
);
}
#[test]
fn test_byte_order_fallback_be_stamp_le_data() {
let mut bytes = le_header_bytes();
bytes[212] = 0x11;
bytes[213] = 0x11;
bytes[214] = 0x00;
bytes[215] = 0x00;
let (decoded, warning) = Header::decode_from_bytes_with_info(&bytes);
assert_eq!(
decoded.nx, 64,
"byte-order fallback should have corrected nx"
);
assert_eq!(decoded.mode, 2);
assert!(warning.is_some());
}
#[test]
fn test_no_fallback_when_machst_matches() {
let bytes = le_header_bytes();
let (decoded, warning) = Header::decode_from_bytes_with_info(&bytes);
assert_eq!(decoded.nx, 64);
assert!(warning.is_none(), "no warning when MACHST is correct");
}
#[test]
fn test_ccp41_machst_recognised() {
let mut bytes = le_header_bytes();
bytes[212] = 0x44;
bytes[213] = 0x41;
let decoded = Header::decode_from_bytes(&bytes);
assert_eq!(decoded.nx, 64);
assert_eq!(decoded.detect_endian(), crate::FileEndian::LittleEndian);
}
#[test]
fn test_nversion_le() {
let mut h = Header::new();
h.set_file_endian(crate::FileEndian::LittleEndian);
assert_eq!(h.nversion(), 20141);
}
#[test]
fn test_nversion_be() {
let mut h = Header::new();
h.set_file_endian(crate::FileEndian::BigEndian);
assert_eq!(h.nversion(), 20141);
}
#[test]
fn test_nversion_zero_accepted_by_validate() {
let mut h = Header::new();
h.nx = 64;
h.ny = 64;
h.nz = 1;
h.mx = 64;
h.my = 64;
h.mz = 1;
h.nlabl = 0;
h.set_nversion(0);
assert_eq!(h.nversion(), 0);
assert!(h.validate(), "NVERSION=0 should pass strict validation");
}
}