mrc/header/mod.rs
1//! MRC-2014 header structure and builder.
2//!
3//! The [`Header`] struct mirrors the 1024-byte fixed header defined by the
4//! MRC-2014 specification. Every field is a typed public member — dimensions,
5//! cell parameters, axis mapping, density statistics, text labels, and more.
6//!
7//! The `Header` provides encode/decode methods for raw bytes, validation
8//! helpers at three levels (basic, detailed, permissive), and convenience
9//! accessors for common metadata (voxel size, cell parameters, volume type,
10//! labels, FEI extended header info).
11//!
12//! Use [`HeaderBuilder`] to construct new headers with a fluent API that
13//! validates on build.
14//!
15//! # Example — decode/encode round-trip
16//!
17//! ```
18//! use mrc::Header;
19//!
20//! let mut raw = [0u8; 1024];
21//! // Standard MRC-2014 markers
22//! raw[208..212].copy_from_slice(b"MAP ");
23//! // Little-endian MACHST
24//! raw[212..216].copy_from_slice(&[0x44, 0x44, 0x00, 0x00]);
25//! // Dimensions: 64 x 64 x 1
26//! raw[0..4].copy_from_slice(&(64i32).to_le_bytes());
27//! raw[4..8].copy_from_slice(&(64i32).to_le_bytes());
28//! raw[8..12].copy_from_slice(&(1i32).to_le_bytes());
29//! // Mode 2 (Float32)
30//! raw[12..16].copy_from_slice(&(2i32).to_le_bytes());
31//!
32//! let header = Header::decode_from_bytes(&raw);
33//! assert_eq!(header.nx, 64);
34//! assert_eq!(header.ny, 64);
35//! assert_eq!(header.nz, 1);
36//! assert_eq!(header.mode, 2);
37//!
38//! let mut encoded = [0u8; 1024];
39//! header.encode_to_bytes(&mut encoded);
40//! assert_eq!(raw, encoded);
41//! ```
42
43pub mod agar;
44pub mod ccp4;
45pub mod fei;
46pub mod mrco;
47pub mod seri;
48
49pub use agar::{AGAR_RECORD_SIZE, AgarRecord, parse_agar_records};
50pub use ccp4::{CCP4_RECORD_SIZE, Ccp4Record, parse_ccp4_records};
51pub use fei::{
52 FEI1_RECORD_SIZE, FEI2_RECORD_SIZE, Fei1Metadata, Fei2Metadata, parse_fei1_records,
53 parse_fei2_records,
54};
55pub use mrco::{MRCO_RECORD_SIZE, MrcoRecord, parse_mrco_records};
56pub use seri::{SERI_RECORD_SIZE, SeriRecord, parse_seri_records};
57
58use crate::Mode;
59
60// ============================================================================
61// Macro: generate parse_*_records() for fixed-size extended header types.
62// The `from_bytes` method on the record type must exist and return Option.
63// ============================================================================
64#[doc(hidden)]
65#[macro_export]
66macro_rules! impl_record_parser {
67 ($record:ident, $size:ident, $parse_fn:ident) => {
68 /// Parse extended header bytes as typed records.
69 ///
70 /// Returns `None` if `bytes` is empty or if its length is not a
71 /// multiple of the record size.
72 pub fn $parse_fn(bytes: &[u8]) -> Option<Vec<$record>> {
73 if bytes.is_empty() || bytes.len() % $size != 0 {
74 return None;
75 }
76 let count = bytes.len() / $size;
77 let mut records = Vec::with_capacity(count);
78 for i in 0..count {
79 let start = i * $size;
80 records.push($record::from_bytes(&bytes[start..start + $size])?);
81 }
82 Some(records)
83 }
84 };
85}
86
87#[cfg(feature = "serde")]
88use serde::{Deserialize, Serialize};
89
90/// Known extended header types identified by the 4-byte EXTTYP field.
91///
92/// This enum maps the `exttyp` identifier stored in `extra[8..12]` of the
93/// MRC-2014 header to a Rust type for dispatch. Unknown identifiers are
94/// captured as [`Unknown`](ExtHeaderType::Unknown) with the raw bytes.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
97#[non_exhaustive]
98pub enum ExtHeaderType {
99 /// CCP4 symmetry records (`"CCP4"`).
100 Ccp4,
101 /// Legacy MRCO format records (`"MRCO"`).
102 Mrco,
103 /// SerialEM tilt-series records (`"SERI"`).
104 Seri,
105 /// Agard microscope records (`"AGAR"`).
106 Agar,
107 /// FEI/Thermo Fisher Type 1 metadata (`"FEI1"`).
108 Fei1,
109 /// FEI/Thermo Fisher Type 2 metadata (`"FEI2"`).
110 Fei2,
111 /// HDF5-based extended header (`"HDF5"`).
112 Hdf5,
113 /// Any unrecognized extended header type.
114 Unknown([u8; 4]),
115}
116
117impl ExtHeaderType {
118 /// Detect the extended header type from a 4-byte EXTTYP identifier.
119 ///
120 /// # Examples
121 ///
122 /// ```
123 /// use mrc::ExtHeaderType;
124 /// let typ = ExtHeaderType::from_exttyp(*b"CCP4");
125 /// assert_eq!(typ, ExtHeaderType::Ccp4);
126 /// ```
127 pub fn from_exttyp(exttyp: [u8; 4]) -> Self {
128 match &exttyp {
129 b"CCP4" => Self::Ccp4,
130 b"MRCO" => Self::Mrco,
131 b"SERI" => Self::Seri,
132 b"AGAR" => Self::Agar,
133 b"FEI1" => Self::Fei1,
134 b"FEI2" => Self::Fei2,
135 b"HDF5" => Self::Hdf5,
136 _ => Self::Unknown(exttyp),
137 }
138 }
139
140 /// Detect the extended header type from a [`Header`].
141 ///
142 /// # Examples
143 ///
144 /// ```
145 /// use mrc::{Header, ExtHeaderType};
146 /// let mut h = Header::new();
147 /// h.set_exttyp(*b"FEI1");
148 /// let typ = ExtHeaderType::from_header(&h);
149 /// assert_eq!(typ, ExtHeaderType::Fei1);
150 /// ```
151 #[inline]
152 pub fn from_header(header: &Header) -> Self {
153 Self::from_exttyp(header.exttyp())
154 }
155}
156
157/// Parsed extended header data, dispatched by [`ExtHeaderType`].
158///
159/// Returned by [`Reader::parse_extended_header`](crate::Reader::parse_extended_header).
160/// Each variant wraps the fully-parsed records for that extended header type.
161#[derive(Debug, Clone, PartialEq)]
162#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
163#[non_exhaustive]
164pub enum ExtHeaderData {
165 /// CCP4 symmetry records.
166 Ccp4(Vec<Ccp4Record>),
167 /// Legacy MRCO format records.
168 Mrco(Vec<MrcoRecord>),
169 /// SerialEM tilt-series records.
170 Seri(Vec<SeriRecord>),
171 /// Agard microscope records.
172 Agar(Vec<AgarRecord>),
173 /// FEI/Thermo Fisher Type 1 metadata records.
174 Fei1(Vec<Fei1Metadata>),
175 /// FEI/Thermo Fisher Type 2 metadata records.
176 Fei2(Vec<Fei2Metadata>),
177 /// No extended header data (nsymbt == 0) or unrecognized type.
178 None,
179}
180
181impl ExtHeaderData {
182 /// Parse extended header bytes according to the given [`ExtHeaderType`].
183 ///
184 /// Returns [`None`](ExtHeaderData::None) when `bytes` is empty or the
185 /// extended header type is unknown.
186 ///
187 /// # Examples
188 ///
189 /// ```
190 /// use mrc::{ExtHeaderType, ExtHeaderData};
191 /// let data = ExtHeaderData::parse(ExtHeaderType::Ccp4, &[]);
192 /// assert_eq!(data, ExtHeaderData::None);
193 /// ```
194 pub fn parse(ext_type: ExtHeaderType, bytes: &[u8]) -> Self {
195 if bytes.is_empty() {
196 return Self::None;
197 }
198 match ext_type {
199 ExtHeaderType::Ccp4 => parse_ccp4_records(bytes)
200 .map(Self::Ccp4)
201 .unwrap_or(Self::None),
202 ExtHeaderType::Mrco => parse_mrco_records(bytes)
203 .map(Self::Mrco)
204 .unwrap_or(Self::None),
205 ExtHeaderType::Seri => parse_seri_records(bytes)
206 .map(Self::Seri)
207 .unwrap_or(Self::None),
208 ExtHeaderType::Agar => parse_agar_records(bytes)
209 .map(Self::Agar)
210 .unwrap_or(Self::None),
211 ExtHeaderType::Fei1 => parse_fei1_records(bytes)
212 .map(Self::Fei1)
213 .unwrap_or(Self::None),
214 ExtHeaderType::Fei2 => parse_fei2_records(bytes)
215 .map(Self::Fei2)
216 .unwrap_or(Self::None),
217 ExtHeaderType::Hdf5 | ExtHeaderType::Unknown(_) => Self::None,
218 }
219 }
220
221 /// Parse using the [`ExtHeaderType`] detected from a [`Header`].
222 ///
223 /// # Examples
224 ///
225 /// ```
226 /// use mrc::{Header, ExtHeaderData};
227 /// let h = Header::new();
228 /// let data = ExtHeaderData::from_header(&h, &[]);
229 /// assert_eq!(data, ExtHeaderData::None);
230 /// ```
231 #[inline]
232 pub fn from_header(header: &Header, bytes: &[u8]) -> Self {
233 Self::parse(ExtHeaderType::from_header(header), bytes)
234 }
235}
236
237// Header field offsets (MRC2014 format)
238const OFFSET_NX: usize = 0;
239const OFFSET_NY: usize = 4;
240const OFFSET_NZ: usize = 8;
241const OFFSET_MODE: usize = 12;
242const OFFSET_NXSTART: usize = 16;
243const OFFSET_NYSTART: usize = 20;
244const OFFSET_NZSTART: usize = 24;
245const OFFSET_MX: usize = 28;
246const OFFSET_MY: usize = 32;
247const OFFSET_MZ: usize = 36;
248const OFFSET_XLEN: usize = 40;
249const OFFSET_YLEN: usize = 44;
250const OFFSET_ZLEN: usize = 48;
251const OFFSET_ALPHA: usize = 52;
252const OFFSET_BETA: usize = 56;
253const OFFSET_GAMMA: usize = 60;
254const OFFSET_MAPC: usize = 64;
255const OFFSET_MAPR: usize = 68;
256const OFFSET_MAPS: usize = 72;
257const OFFSET_DMIN: usize = 76;
258const OFFSET_DMAX: usize = 80;
259const OFFSET_DMEAN: usize = 84;
260const OFFSET_ISPG: usize = 88;
261const OFFSET_NSYMBT: usize = 92;
262const OFFSET_EXTRA: usize = 96;
263const OFFSET_EXTTYP: usize = 104; // extra[8..12]
264const OFFSET_NVERSION: usize = 108; // extra[12..16]
265const OFFSET_ORIGIN: usize = 196;
266const OFFSET_MAP: usize = 208;
267const OFFSET_MACHST: usize = 212;
268const OFFSET_RMS: usize = 216;
269const OFFSET_NLABL: usize = 220;
270const OFFSET_LABEL: usize = 224;
271
272/// Default `extra` bytes with NVERSION=20141 encoded in little-endian.
273const DEFAULT_EXTRA: [u8; 100] = {
274 let mut e = [0u8; 100];
275 // NVERSION = 20141 (latest MRC2014 update), stored little-endian in extra[12..16]
276 e[12] = 0xAD;
277 e[13] = 0x4E;
278 e[14] = 0x00;
279 e[15] = 0x00;
280 e
281};
282/// Mirror of the 1024-byte MRC-2014 fixed header.
283///
284/// Every field is a typed public member — dimensions, cell parameters,
285/// axis mapping, density statistics, text labels, and more.
286///
287/// Construct via [`Header::new()`] or [`HeaderBuilder`], decode from raw
288/// bytes via [`Header::decode_from_bytes`], and encode via
289/// [`Header::encode_to_bytes`].
290///
291/// # Official MRC2014 specification: <https://www.ccpem.ac.uk/mrc-format/mrc2014/>
292#[repr(C, align(4))]
293#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
294#[derive(Debug, Clone, Copy, PartialEq)]
295pub struct Header {
296 /// Number of columns in 3D data array (fast axis)
297 pub nx: i32,
298 /// Number of rows in 3D data array (medium axis)
299 pub ny: i32,
300 /// Number of sections in 3D data array (slow axis)
301 pub nz: i32,
302 /// Mode value (see `Mode` enum)
303 pub mode: i32,
304 /// Location of first column in unit cell
305 pub nxstart: i32,
306 /// Location of first row in unit cell
307 pub nystart: i32,
308 /// Location of first section in unit cell
309 pub nzstart: i32,
310 /// Sampling along X axis of unit cell
311 pub mx: i32,
312 /// Sampling along Y axis of unit cell
313 pub my: i32,
314 /// Sampling along Z axis of unit cell
315 pub mz: i32,
316 /// CELLA: Cell dimensions (unit cell edge length) in angstroms (Å) along X axis
317 pub xlen: f32,
318 /// CELLA: Cell dimensions (unit cell edge length) in angstroms (Å) along Y axis
319 pub ylen: f32,
320 /// CELLA: Cell dimensions (unit cell edge length) in angstroms (Å) along Z axis
321 pub zlen: f32,
322 /// CELLB: Cell angles in degrees between the crystallographic axes Y and Z axes
323 pub alpha: f32,
324 /// CELLB: Cell angles in degrees between the crystallographic axes X and Z axes
325 pub beta: f32,
326 /// CELLB: Cell angles in degrees between the crystallographic axes X and Y axes
327 pub gamma: f32,
328 /// One-based index of column axis (1, 2, 3 for X, Y, Z)
329 pub mapc: i32,
330 /// One-based index of row axis (1, 2, 3 for X, Y, Z)
331 pub mapr: i32,
332 /// One-based index of section axis (1, 2, 3 for X, Y, Z)
333 pub maps: i32,
334 /// Minimum density value
335 pub dmin: f32,
336 /// Maximum density value
337 pub dmax: f32,
338 /// Mean density value
339 pub dmean: f32,
340 /// Space group number; 0 implies 2D image or image stack.
341 ///
342 /// For crystallography, represents the actual space group.
343 /// For volume stacks, conventionally ISPG = space group number + 400.
344 pub ispg: i32,
345 /// Size of extended header (which follows main header) in bytes.
346 /// May contain symmetry records or other metadata (indicated by EXTTYP).
347 pub nsymbt: i32,
348 /// Extra space used for anything.
349 /// Bytes 8–11 hold EXTTYP, 12–15 NVERSION.
350 #[cfg_attr(feature = "serde", serde(with = "crate::serde_byte_array"))]
351 pub extra: [u8; 100],
352 /// Volume/phase origin (pixels/voxels) or origin of subvolume
353 pub origin: [f32; 3],
354 /// Must contain "MAP " to identify file type
355 pub map: [u8; 4],
356 /// Machine stamp that encodes byte order of data.
357 ///
358 /// Little-endian files use `0x44 0x44 0x00 0x00`.
359 pub machst: [u8; 4],
360 /// RMS deviation of map from mean density
361 pub rms: f32,
362 /// Number of valid labels in `label` field (0–10)
363 pub nlabl: i32,
364 /// Ten text labels of 80 bytes each.
365 #[cfg_attr(feature = "serde", serde(with = "crate::serde_byte_array"))]
366 pub label: [u8; 800],
367}
368
369impl Default for Header {
370 fn default() -> Self {
371 Self::new()
372 }
373}
374
375impl Header {
376 #[inline]
377 /// Constructs a default header suitable for a new MRC file.
378 ///
379 /// All dimensions are zero, the mode is 32-bit float, and
380 /// cell angles are 90°. Other fields are set to safe neutral values.
381 ///
382 /// # Endianness
383 /// Per crate policy, new MRC files are always written in little-endian format.
384 /// This constructor sets `machst` to little-endian by default and initializes
385 /// `nversion` to `20141` (latest MRC2014 update).
386 #[must_use]
387 pub const fn new() -> Self {
388 Self {
389 nx: 0,
390 ny: 0,
391 nz: 0,
392 mode: 2, // 32-bit floating point
393 nxstart: 0,
394 nystart: 0,
395 nzstart: 0,
396 mx: 0,
397 my: 0,
398 mz: 0,
399 xlen: 1.0, // Avoid division by zero.
400 ylen: 1.0,
401 zlen: 1.0,
402 alpha: 90.0,
403 beta: 90.0,
404 gamma: 90.0,
405 mapc: 1, // Column → X
406 mapr: 2, // Row → Y
407 maps: 3, // Section→ Z
408 dmin: 0.0, // Set higher than dmax to indicate not well-determined
409 dmax: -1.0, // Set lower than dmin to indicate not well-determined
410 dmean: -2.0, // Less than both to indicate not well-determined
411 ispg: 1, // P1 space group.
412 nsymbt: 0,
413 extra: DEFAULT_EXTRA,
414 origin: [0.0; 3],
415 map: *b"MAP ",
416 machst: [0x44, 0x44, 0x00, 0x00], // Little-endian (crate policy for new files)
417 rms: -1.0, // Negative indicates not well-determined
418 nlabl: 0,
419 label: [0; 800],
420 }
421 }
422
423 #[inline]
424 /// Offset, in bytes, from file start to the first voxel value.
425 ///
426 /// Returns `1024` when `nsymbt` is negative (to avoid integer wrap-around
427 /// on malformed headers).
428 ///
429 /// ```
430 /// use mrc::Header;
431 /// let h = Header::new();
432 /// assert_eq!(h.data_offset(), 1024);
433 /// ```
434 pub const fn data_offset(&self) -> usize {
435 if self.nsymbt < 0 {
436 1024
437 } else {
438 1024 + self.nsymbt as usize
439 }
440 }
441
442 #[inline]
443 /// Size, in bytes, of the voxel data block.
444 ///
445 /// Returns `None` if the dimensions are so large that the calculation
446 /// overflows `usize`.
447 ///
448 /// ```
449 /// use mrc::Header;
450 /// let mut h = Header::new();
451 /// h.nx = 64; h.ny = 64; h.nz = 32;
452 /// h.mode = 2; // Float32 → 4 bytes per voxel
453 /// assert_eq!(h.data_size(), Some(64 * 64 * 32 * 4));
454 /// ```
455 pub fn data_size(&self) -> Option<usize> {
456 let nx = self.nx.max(0) as usize;
457 let ny = self.ny.max(0) as usize;
458 let nz = self.nz.max(0) as usize;
459 match Mode::from_i32(self.mode) {
460 Some(mode) => {
461 match mode {
462 // For Packed4Bit, each row is padded to a whole byte boundary:
463 // row_bytes = nx.div_ceil(2), total = ny * row_bytes * nz
464 Mode::Packed4Bit => {
465 let row_bytes = nx.div_ceil(2);
466 ny.checked_mul(row_bytes)?.checked_mul(nz)
467 }
468 _ => nx
469 .checked_mul(ny)?
470 .checked_mul(nz)?
471 .checked_mul(mode.byte_size()),
472 }
473 }
474 None => None, // unknown/unsupported mode
475 }
476 }
477
478 #[inline]
479 /// True when dimensions are positive and mode is supported.
480 ///
481 /// ```
482 /// use mrc::Header;
483 /// let h = Header::new();
484 /// // Default header has zero dimensions → invalid
485 /// assert!(!h.validate());
486 /// ```
487 pub fn validate(&self) -> bool {
488 self.validate_detailed().is_ok()
489 }
490
491 #[inline]
492 /// Detailed header validation returning specific error information.
493 ///
494 /// ```
495 /// use mrc::Header;
496 /// let h = Header::new();
497 /// match h.validate_detailed() {
498 /// Err(e) => assert!(e.to_string().contains("dimensions")),
499 /// Ok(()) => unreachable!(),
500 /// }
501 /// ```
502 pub fn validate_detailed(&self) -> Result<(), crate::HeaderValidationError> {
503 use crate::HeaderValidationError;
504
505 if self.nx <= 0 || self.ny <= 0 || self.nz <= 0 {
506 return Err(HeaderValidationError::InvalidDimensions {
507 nx: self.nx,
508 ny: self.ny,
509 nz: self.nz,
510 });
511 }
512
513 if Mode::from_i32(self.mode).is_none() {
514 return Err(HeaderValidationError::UnsupportedMode(self.mode));
515 }
516
517 if !self.validate_map() {
518 return Err(HeaderValidationError::InvalidMap(self.map));
519 }
520
521 if !(self.ispg == 0
522 || (self.ispg >= 1 && self.ispg <= 230)
523 || (self.ispg >= 400 && self.ispg <= 630))
524 {
525 return Err(HeaderValidationError::InvalidIspg(self.ispg));
526 }
527
528 if !(matches!(self.mapc, 1..=3)
529 && matches!(self.mapr, 1..=3)
530 && matches!(self.maps, 1..=3)
531 && self.mapc != self.mapr
532 && self.mapc != self.maps
533 && self.mapr != self.maps)
534 {
535 return Err(HeaderValidationError::InvalidAxisMapping {
536 mapc: self.mapc,
537 mapr: self.mapr,
538 maps: self.maps,
539 });
540 }
541
542 if self.nsymbt < 0 {
543 return Err(HeaderValidationError::InvalidNsymbt(self.nsymbt));
544 }
545
546 if self.nlabl < 0 || self.nlabl > 10 {
547 return Err(HeaderValidationError::InvalidNlabl(self.nlabl));
548 }
549
550 // Label sequence validation: nlabl must match actual non-empty labels,
551 // and no empty labels may appear between filled ones.
552 let actual_labels = self.count_non_empty_labels();
553 if actual_labels != self.nlabl as usize {
554 return Err(HeaderValidationError::LabelCountMismatch {
555 nlabl: self.nlabl,
556 actual: actual_labels as i32,
557 });
558 }
559 for i in 0..self.nlabl as usize {
560 if self.label_is_empty(i) {
561 return Err(HeaderValidationError::EmptyLabelBeforeFilled { index: i as i32 });
562 }
563 }
564
565 let nversion = self.nversion();
566 if nversion != 0 && nversion != 20140 && nversion != 20141 {
567 return Err(HeaderValidationError::InvalidNversion(nversion));
568 }
569
570 if self.mx <= 0 || self.my <= 0 || self.mz <= 0 {
571 return Err(HeaderValidationError::InvalidSampling {
572 mx: self.mx,
573 my: self.my,
574 mz: self.mz,
575 });
576 }
577
578 if self.ispg >= 400 && self.ispg <= 630 && self.mz != 0 && self.nz % self.mz != 0 {
579 return Err(HeaderValidationError::InvalidVolumeStack {
580 nz: self.nz,
581 mz: self.mz,
582 ispg: self.ispg,
583 });
584 }
585
586 Ok(())
587 }
588
589 /// Permissive validation that returns warnings instead of hard errors
590 /// for most non-critical issues.
591 ///
592 /// Only **fatal** problems (dimensions ≤ 0 or completely unsupported mode)
593 /// produce an `Err`. Everything else is collected as a human-readable
594 /// warning string.
595 ///
596 /// # Examples
597 ///
598 /// ```
599 /// use mrc::Header;
600 /// let mut h = Header::new();
601 /// h.nx = 64; h.ny = 64; h.nz = 32;
602 /// h.mx = 64; h.my = 64; h.mz = 32;
603 /// h.mode = 2;
604 /// assert!(h.validate_permissive().is_ok());
605 /// ```
606 pub fn validate_permissive(&self) -> Result<Vec<String>, crate::HeaderValidationError> {
607 use crate::HeaderValidationError;
608 let mut warnings = Vec::new();
609
610 if self.nx <= 0 || self.ny <= 0 || self.nz <= 0 {
611 return Err(HeaderValidationError::InvalidDimensions {
612 nx: self.nx,
613 ny: self.ny,
614 nz: self.nz,
615 });
616 }
617
618 if Mode::from_i32(self.mode).is_none() {
619 return Err(HeaderValidationError::UnsupportedMode(self.mode));
620 }
621
622 if !self.validate_map() {
623 warnings.push(format!(
624 "MAP field is non-standard: {:?}",
625 String::from_utf8_lossy(&self.map)
626 ));
627 }
628
629 if !(self.ispg == 0
630 || (self.ispg >= 1 && self.ispg <= 230)
631 || (self.ispg >= 400 && self.ispg <= 630))
632 {
633 warnings.push(format!(
634 "ISPG {} is outside the standard ranges (0, 1-230, 400-630)",
635 self.ispg
636 ));
637 }
638
639 if !(self.mapc != 0
640 && self.mapc.abs() <= 3
641 && self.mapr != 0
642 && self.mapr.abs() <= 3
643 && self.maps != 0
644 && self.maps.abs() <= 3
645 && self.mapc.abs() != self.mapr.abs()
646 && self.mapc.abs() != self.maps.abs()
647 && self.mapr.abs() != self.maps.abs())
648 {
649 warnings.push(format!(
650 "Axis mapping ({}, {}, {}) is not a valid permutation of axis indices",
651 self.mapc, self.mapr, self.maps
652 ));
653 } else if self.mapr == -2 {
654 warnings.push("mapr = -2 indicates Y-inverted image data (IMOD convention)".into());
655 }
656
657 if self.nsymbt < 0 {
658 warnings.push(format!("NSYMBT is negative ({})", self.nsymbt));
659 }
660
661 if self.nlabl < 0 || self.nlabl > 10 {
662 warnings.push(format!("NLABL {} is outside 0-10", self.nlabl));
663 }
664
665 let nversion = self.nversion();
666 if nversion != 20140 && nversion != 20141 {
667 warnings.push(format!("NVERSION {} is not 20140 or 20141", nversion));
668 }
669
670 if self.mx <= 0 || self.my <= 0 || self.mz <= 0 {
671 warnings.push(format!(
672 "Sampling (mx={}, my={}, mz={}) is not all positive",
673 self.mx, self.my, self.mz
674 ));
675 }
676
677 if self.ispg >= 400 && self.ispg <= 630 && self.mz != 0 && self.nz % self.mz != 0 {
678 warnings.push(format!(
679 "Volume stack: nz ({}) is not divisible by mz ({}) for ispg={}",
680 self.nz, self.mz, self.ispg
681 ));
682 }
683
684 Ok(warnings)
685 }
686
687 #[inline]
688 /// Validate the MAP field, allowing for legacy variants.
689 ///
690 /// Standard MRC2014 requires "MAP ", but some legacy files may use:
691 /// - "MAP\0" (null-terminated)
692 /// - "MAPI" (older format)
693 /// - All zeros (uninitialized)
694 fn validate_map(&self) -> bool {
695 // Standard MRC2014 format
696 if self.map == *b"MAP " {
697 return true;
698 }
699 // Accept legacy variants: "MAP\0" or "MAPI"
700 if &self.map[..3] == b"MAP"
701 && (self.map[3] == b' ' || self.map[3] == 0 || self.map[3] == b'I')
702 {
703 return true;
704 }
705 // Accept all zeros (uninitialized, common in some generated files)
706 if self.map == [0; 4] {
707 return true;
708 }
709 false
710 }
711
712 #[inline]
713 /// Reads the 4-byte EXTTYP identifier stored in `extra[8..12]`.
714 ///
715 /// EXTTYP is a 4-byte ASCII string indicating the type of extended header.
716 /// Common values: "CCP4", "MRCO", "SERI", "AGAR", "FEI1", "FEI2", "HDF5".
717 ///
718 /// ```
719 /// use mrc::Header;
720 /// let mut h = Header::new();
721 /// h.set_exttyp(*b"CCP4");
722 /// assert_eq!(h.exttyp(), *b"CCP4");
723 /// ```
724 pub fn exttyp(&self) -> [u8; 4] {
725 [
726 self.extra[OFFSET_EXTTYP - OFFSET_EXTRA],
727 self.extra[OFFSET_EXTTYP - OFFSET_EXTRA + 1],
728 self.extra[OFFSET_EXTTYP - OFFSET_EXTRA + 2],
729 self.extra[OFFSET_EXTTYP - OFFSET_EXTRA + 3],
730 ]
731 }
732
733 #[inline]
734 /// Stores the 4-byte EXTTYP identifier into `extra[8..12]`.
735 ///
736 /// EXTTYP is a 4-byte ASCII string indicating the type of extended header.
737 ///
738 /// # Examples
739 ///
740 /// ```
741 /// use mrc::Header;
742 /// let mut h = Header::new();
743 /// h.set_exttyp(*b"CCP4");
744 /// assert_eq!(h.exttyp(), *b"CCP4");
745 /// ```
746 pub fn set_exttyp(&mut self, value: [u8; 4]) {
747 let start = OFFSET_EXTTYP - OFFSET_EXTRA;
748 self.extra[start..start + 4].copy_from_slice(&value);
749 }
750
751 #[inline]
752 /// Interprets EXTTYP as an ASCII string.
753 ///
754 /// # Examples
755 ///
756 /// ```
757 /// use mrc::Header;
758 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
759 /// let mut h = Header::new();
760 /// h.set_exttyp(*b"FEI1");
761 /// assert_eq!(h.exttyp_str()?, "FEI1");
762 /// # Ok(())
763 /// # }
764 /// ```
765 pub fn exttyp_str(&self) -> Result<&str, core::str::Utf8Error> {
766 let start = OFFSET_EXTTYP - OFFSET_EXTRA;
767 core::str::from_utf8(&self.extra[start..start + 4])
768 }
769
770 #[inline]
771 /// Sets EXTTYP from a 4-character ASCII string.
772 ///
773 /// # Examples
774 ///
775 /// ```
776 /// use mrc::Header;
777 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
778 /// let mut h = Header::new();
779 /// h.set_exttyp_str("FEI1")?;
780 /// assert_eq!(h.exttyp(), *b"FEI1");
781 /// # Ok(())
782 /// # }
783 /// ```
784 pub fn set_exttyp_str(&mut self, value: &str) -> Result<(), &'static str> {
785 if value.len() != 4 {
786 return Err("EXTTYP must be exactly 4 characters");
787 }
788 let bytes = value.as_bytes();
789 let start = OFFSET_EXTTYP - OFFSET_EXTRA;
790 self.extra[start..start + 4].copy_from_slice(bytes);
791 Ok(())
792 }
793
794 #[inline]
795 /// Reads the 4-byte NVERSION number stored in `extra[12..16]`.
796 ///
797 /// This value is a numeric i32 and respects the file's endianness.
798 ///
799 /// ```
800 /// use mrc::Header;
801 /// let h = Header::new();
802 /// assert_eq!(h.nversion(), 20141);
803 /// ```
804 pub fn nversion(&self) -> i32 {
805 use crate::engine::codec::EndianCodec;
806 let file_endian = self.detect_endian();
807 let start = OFFSET_NVERSION - OFFSET_EXTRA;
808 i32::decode(&self.extra[start..start + 4], 0, file_endian)
809 }
810
811 #[inline]
812 /// Stores the 4-byte NVERSION number into `extra[12..16]`.
813 ///
814 /// This value is a numeric i32 and respects the file's endianness.
815 ///
816 /// # Examples
817 ///
818 /// ```
819 /// use mrc::Header;
820 /// let mut h = Header::new();
821 /// h.set_nversion(20140);
822 /// assert_eq!(h.nversion(), 20140);
823 /// ```
824 pub fn set_nversion(&mut self, value: i32) {
825 use crate::engine::codec::EndianCodec;
826 let file_endian = self.detect_endian();
827 let start = OFFSET_NVERSION - OFFSET_EXTRA;
828 value.encode(&mut self.extra[start..start + 4], 0, file_endian);
829 }
830
831 /// Get the list of non-empty text labels.
832 ///
833 /// Returns up to `nlabl` labels, each trimmed of trailing whitespace.
834 ///
835 /// ```
836 /// use mrc::Header;
837 /// let mut h = Header::new();
838 /// h.add_label("my sample");
839 /// h.add_label("defocus series");
840 /// let labels = h.get_labels();
841 /// assert_eq!(labels, vec!["my sample", "defocus series"]);
842 /// ```
843 pub fn get_labels(&self) -> Vec<String> {
844 let count = self.nlabl.clamp(0, 10) as usize;
845 let mut labels = Vec::with_capacity(count);
846 for i in 0..count {
847 let start = i * 80;
848 let bytes = &self.label[start..start + 80];
849 let text = String::from_utf8_lossy(bytes);
850 labels.push(text.trim_end().to_string());
851 }
852 labels
853 }
854
855 /// Check whether the i-th label is empty (all whitespace / zeros).
856 fn label_is_empty(&self, index: usize) -> bool {
857 let start = index * 80;
858 self.label[start..start + 80]
859 .iter()
860 .all(|&b| b == 0 || b == b' ')
861 }
862
863 /// Count how many of the 10 label slots contain non-empty text.
864 fn count_non_empty_labels(&self) -> usize {
865 (0..10).filter(|&i| !self.label_is_empty(i)).count()
866 }
867
868 /// Add a text label to the header.
869 ///
870 /// Labels are truncated to 80 bytes and non-printable ASCII characters
871 /// (outside 0x20–0x7E) are replaced with spaces. If 10 labels are already
872 /// stored, the oldest label is dropped (FIFO).
873 ///
874 /// # Examples
875 ///
876 /// ```
877 /// use mrc::Header;
878 /// let mut h = Header::new();
879 /// h.add_label("my sample");
880 /// assert_eq!(h.get_labels(), vec!["my sample"]);
881 /// ```
882 pub fn add_label(&mut self, text: &str) {
883 // Filter to printable ASCII and truncate to 80 bytes
884 let filtered: String = text
885 .chars()
886 .map(|c| {
887 if c.is_ascii_graphic() || c == ' ' {
888 c
889 } else {
890 ' '
891 }
892 })
893 .take(80)
894 .collect();
895 let bytes = filtered.as_bytes();
896 let len = bytes.len();
897
898 let count = self.count_non_empty_labels();
899 if count < 10 {
900 // Find the first empty slot
901 let slot = (0..10).find(|&i| self.label_is_empty(i)).unwrap_or(count);
902 let start = slot * 80;
903 self.label[start..start + 80].fill(b' ');
904 self.label[start..start + len].copy_from_slice(bytes);
905 } else {
906 // Shift existing labels up (FIFO) and store in slot 9.
907 // Drop slot 0 (oldest), shift slots 1..9 to 0..8.
908 self.label.copy_within(80..800, 0);
909 let start = 9 * 80;
910 self.label[start..start + 80].fill(b' ');
911 self.label[start..start + len].copy_from_slice(bytes);
912 }
913 self.nlabl = self.count_non_empty_labels() as i32;
914 }
915
916 #[inline]
917 /// Detect the file endianness from the MACHST machine stamp
918 ///
919 /// ```
920 /// use mrc::{Header, FileEndian};
921 /// let h = Header::new();
922 /// assert_eq!(h.detect_endian(), FileEndian::LittleEndian);
923 /// ```
924 pub fn detect_endian(&self) -> crate::FileEndian {
925 crate::FileEndian::from_machst(&self.machst)
926 }
927
928 #[inline]
929 /// Set the file endianness for this header
930 ///
931 /// This sets the MACHST machine stamp to the appropriate value for the
932 /// specified endianness and re-encodes NVERSION so that it remains valid.
933 ///
934 /// # Note
935 /// Per crate policy, new MRC files are always written in little-endian format.
936 /// This method is not intended for creating big-endian files from scratch.
937 ///
938 /// ```
939 /// use mrc::{Header, FileEndian};
940 /// let mut h = Header::new();
941 /// h.set_file_endian(FileEndian::BigEndian);
942 /// assert_eq!(h.detect_endian(), FileEndian::BigEndian);
943 /// ```
944 pub fn set_file_endian(&mut self, endian: crate::FileEndian) {
945 // Preserve the current nversion value before swapping endianness,
946 // then re-encode it in the new byte order.
947 let current_nversion = self.nversion();
948 self.machst = endian.to_machst();
949 self.set_nversion(current_nversion);
950 }
951
952 // -------------------------------------------------------------------------
953 // Volume type introspection (following Python mrcfile conventions)
954 // -------------------------------------------------------------------------
955
956 /// Returns `true` if this is a single 2D image (`nz == 1`).
957 ///
958 /// ```
959 /// use mrc::Header;
960 /// let mut h = Header::new();
961 /// h.nz = 1;
962 /// assert!(h.is_single_image());
963 /// h.nz = 10;
964 /// assert!(!h.is_single_image());
965 /// ```
966 pub fn is_single_image(&self) -> bool {
967 self.nz == 1
968 }
969
970 /// Returns `true` if this is an image stack (`ispg == 0`).
971 ///
972 /// ```
973 /// use mrc::Header;
974 /// let mut h = Header::new();
975 /// h.ispg = 0;
976 /// assert!(h.is_image_stack());
977 /// ```
978 pub fn is_image_stack(&self) -> bool {
979 self.ispg == 0
980 }
981
982 /// Returns `true` if this is a single 3D volume (`ispg != 0` and not a
983 /// volume stack).
984 ///
985 /// # Examples
986 ///
987 /// ```
988 /// use mrc::Header;
989 /// let mut h = Header::new();
990 /// h.ispg = 1;
991 /// assert!(h.is_volume());
992 /// ```
993 pub fn is_volume(&self) -> bool {
994 !self.is_image_stack() && !self.is_volume_stack()
995 }
996
997 /// Returns `true` if this is a volume stack (`ispg` in 401–630).
998 ///
999 /// # Examples
1000 ///
1001 /// ```
1002 /// use mrc::Header;
1003 /// let mut h = Header::new();
1004 /// h.ispg = 401;
1005 /// assert!(h.is_volume_stack());
1006 /// ```
1007 pub fn is_volume_stack(&self) -> bool {
1008 (400..=630).contains(&self.ispg)
1009 }
1010
1011 /// Configure the header as an image stack.
1012 ///
1013 /// Sets `ispg = 0` and `mz = 1`.
1014 ///
1015 /// # Examples
1016 ///
1017 /// ```
1018 /// use mrc::Header;
1019 /// let mut h = Header::new();
1020 /// h.set_image_stack();
1021 /// assert!(h.is_image_stack());
1022 /// ```
1023 pub fn set_image_stack(&mut self) {
1024 self.ispg = 0;
1025 self.mz = 1;
1026 }
1027
1028 /// Configure the header as a single volume.
1029 ///
1030 /// Sets `ispg = 1` and `mz = nz`.
1031 ///
1032 /// # Examples
1033 ///
1034 /// ```
1035 /// use mrc::Header;
1036 /// let mut h = Header::new();
1037 /// h.nz = 32;
1038 /// h.set_volume();
1039 /// assert!(h.is_volume());
1040 /// assert_eq!(h.mz, 32);
1041 /// ```
1042 pub fn set_volume(&mut self) {
1043 self.ispg = 1;
1044 self.mz = self.nz;
1045 }
1046
1047 /// Configure the header as a volume stack.
1048 ///
1049 /// Sets `ispg = 401` and `mz` to the given sub-volume size.
1050 /// `nz` must be divisible by `mz` for the header to validate.
1051 ///
1052 /// # Examples
1053 ///
1054 /// ```
1055 /// use mrc::Header;
1056 /// let mut h = Header::new();
1057 /// h.set_volume_stack(16);
1058 /// assert!(h.is_volume_stack());
1059 /// assert_eq!(h.mz, 16);
1060 /// ```
1061 pub fn set_volume_stack(&mut self, mz: i32) {
1062 self.ispg = 401;
1063 self.mz = mz;
1064 }
1065
1066 // -------------------------------------------------------------------------
1067 // Computed convenience properties
1068 // -------------------------------------------------------------------------
1069
1070 /// Voxel size in Ångströms per pixel, computed as `cella / mxyz`.
1071 ///
1072 /// Returns `[xlen / mx, ylen / my, zlen / mz]`.
1073 /// If any of `mx`, `my`, `mz` is zero, that component returns `0.0`.
1074 ///
1075 /// # Examples
1076 ///
1077 /// ```
1078 /// use mrc::Header;
1079 /// let mut h = Header::new();
1080 /// h.xlen = 10.0; h.mx = 100;
1081 /// h.ylen = 10.0; h.my = 100;
1082 /// h.zlen = 20.0; h.mz = 200;
1083 /// let vs = h.voxel_size();
1084 /// assert!((vs[0] - 0.1).abs() < 1e-6);
1085 /// ```
1086 pub fn voxel_size(&self) -> [f32; 3] {
1087 [
1088 if self.mx == 0 {
1089 0.0
1090 } else {
1091 self.xlen / self.mx as f32
1092 },
1093 if self.my == 0 {
1094 0.0
1095 } else {
1096 self.ylen / self.my as f32
1097 },
1098 if self.mz == 0 {
1099 0.0
1100 } else {
1101 self.zlen / self.mz as f32
1102 },
1103 ]
1104 }
1105
1106 /// Starting grid point / origin offset.
1107 ///
1108 /// Returns `[nxstart, nystart, nzstart]`.
1109 ///
1110 /// # Examples
1111 ///
1112 /// ```
1113 /// use mrc::Header;
1114 /// let mut h = Header::new();
1115 /// h.nxstart = 10; h.nystart = 20; h.nzstart = 30;
1116 /// assert_eq!(h.nstart(), [10, 20, 30]);
1117 /// ```
1118 pub fn nstart(&self) -> [i32; 3] {
1119 [self.nxstart, self.nystart, self.nzstart]
1120 }
1121
1122 /// Cell dimensions (unit cell edge lengths) in ångströms.
1123 ///
1124 /// Returns `[xlen, ylen, zlen]`.
1125 ///
1126 /// # Examples
1127 ///
1128 /// ```
1129 /// use mrc::Header;
1130 /// let h = Header::new();
1131 /// assert_eq!(h.cell_lengths(), [1.0, 1.0, 1.0]);
1132 /// ```
1133 pub fn cell_lengths(&self) -> [f32; 3] {
1134 [self.xlen, self.ylen, self.zlen]
1135 }
1136
1137 /// Cell angles in degrees.
1138 ///
1139 /// Returns `[alpha, beta, gamma]`.
1140 ///
1141 /// # Examples
1142 ///
1143 /// ```
1144 /// use mrc::Header;
1145 /// let h = Header::new();
1146 /// assert_eq!(h.cell_angles(), [90.0, 90.0, 90.0]);
1147 /// ```
1148 pub fn cell_angles(&self) -> [f32; 3] {
1149 [self.alpha, self.beta, self.gamma]
1150 }
1151
1152 /// Logical data shape following Python `mrcfile` conventions.
1153 ///
1154 /// | Type | Shape |
1155 /// |------|-------|
1156 /// | Single image | `(1, 1, ny, nx)` |
1157 /// | Image stack | `(1, nz, ny, nx)` |
1158 /// | Volume | `(1, nz, ny, nx)` |
1159 /// | Volume stack | `(nz / mz, mz, ny, nx)` |
1160 ///
1161 /// # Examples
1162 ///
1163 /// ```
1164 /// use mrc::Header;
1165 /// let mut h = Header::new();
1166 /// h.nx = 64; h.ny = 64; h.nz = 32;
1167 /// // Default ispg=1 (volume) → shape is (1, nz, ny, nx)
1168 /// assert_eq!(h.logical_shape(), [1, 32, 64, 64]);
1169 /// ```
1170 pub fn logical_shape(&self) -> [usize; 4] {
1171 if self.is_volume_stack() && self.mz > 0 {
1172 let nvolumes = (self.nz / self.mz) as usize;
1173 [
1174 nvolumes,
1175 self.mz as usize,
1176 self.ny as usize,
1177 self.nx as usize,
1178 ]
1179 } else if self.is_volume_stack() {
1180 // Volume stack with mz ≤ 0 is degenerate — match the Err from
1181 // Reader::volumes() by returning 0 sub-volumes.
1182 [0, 0, self.ny as usize, self.nx as usize]
1183 } else {
1184 [1, self.nz as usize, self.ny as usize, self.nx as usize]
1185 }
1186 }
1187
1188 /// Sampling rates (mx, my, mz).
1189 ///
1190 /// # Examples
1191 ///
1192 /// ```
1193 /// use mrc::Header;
1194 /// let mut h = Header::new();
1195 /// h.mx = 100; h.my = 100; h.mz = 50;
1196 /// assert_eq!(h.sampling(), [100, 100, 50]);
1197 /// ```
1198 #[inline]
1199 pub fn sampling(&self) -> [i32; 3] {
1200 [self.mx, self.my, self.mz]
1201 }
1202
1203 /// Header density statistics `(dmin, dmax, dmean, rms)`.
1204 ///
1205 /// # Examples
1206 ///
1207 /// ```
1208 /// use mrc::Header;
1209 /// let h = Header::new();
1210 /// assert_eq!(h.density_stats(), (0.0, -1.0, -2.0, -1.0));
1211 /// ```
1212 #[inline]
1213 pub fn density_stats(&self) -> (f32, f32, f32, f32) {
1214 (self.dmin, self.dmax, self.dmean, self.rms)
1215 }
1216
1217 /// Returns `true` when the MAP field is exactly `"MAP "` (MRC-2014 standard).
1218 ///
1219 /// # Examples
1220 ///
1221 /// ```
1222 /// use mrc::Header;
1223 /// let h = Header::new();
1224 /// assert!(h.is_standard_map());
1225 /// ```
1226 #[inline]
1227 pub fn is_standard_map(&self) -> bool {
1228 self.map == *b"MAP "
1229 }
1230
1231 /// Get the i-th text label as a trimmed `&str`, or `None` if the label slot
1232 /// is empty or `i >= nlabl`.
1233 ///
1234 /// Labels are 80-byte fixed-width fields. Trailing whitespace is trimmed.
1235 /// Non-UTF-8 bytes are replaced with `U+FFFD` (this is rare in practice).
1236 ///
1237 /// # Examples
1238 ///
1239 /// ```
1240 /// use mrc::Header;
1241 /// let mut h = Header::new();
1242 /// h.add_label("my sample");
1243 /// assert_eq!(h.label_at(0), Some("my sample"));
1244 /// ```
1245 pub fn label_at(&self, index: usize) -> Option<&str> {
1246 if index >= self.nlabl.clamp(0, 10) as usize || self.label_is_empty(index) {
1247 return None;
1248 }
1249 let start = index * 80;
1250 let end = start
1251 + self.label[start..start + 80]
1252 .iter()
1253 .rposition(|&b| b != b' ')
1254 .map_or(0, |p| p + 1);
1255 let trimmed = core::str::from_utf8(&self.label[start..end]).unwrap_or("<invalid utf-8>");
1256 Some(trimmed)
1257 }
1258
1259 /// Compute the unit cell volume in cubic ångströms.
1260 ///
1261 /// Uses the general formula for a triclinic cell:
1262 ///
1263 /// `V = a * b * c * sqrt(1 - cos²α - cos²β - cos²γ + 2 * cosα * cosβ * cosγ)`
1264 ///
1265 /// where `a`, `b`, `c` are cell lengths and `α`, `β`, `γ` are cell angles.
1266 /// Returns `0.0` for degenerate cells (any length ≤ 0).
1267 ///
1268 /// # Examples
1269 ///
1270 /// ```
1271 /// use mrc::Header;
1272 /// let mut h = Header::new();
1273 /// h.xlen = 10.0; h.ylen = 10.0; h.zlen = 10.0;
1274 /// let vol = h.cell_volume();
1275 /// assert!((vol - 1000.0).abs() < 1e-6);
1276 /// ```
1277 pub fn cell_volume(&self) -> f64 {
1278 let a = self.xlen as f64;
1279 let b = self.ylen as f64;
1280 let c = self.zlen as f64;
1281 if a <= 0.0 || b <= 0.0 || c <= 0.0 {
1282 return 0.0;
1283 }
1284 let alpha = self.alpha as f64 * (core::f64::consts::PI / 180.0);
1285 let beta = self.beta as f64 * (core::f64::consts::PI / 180.0);
1286 let gamma = self.gamma as f64 * (core::f64::consts::PI / 180.0);
1287 let cos_a = alpha.cos();
1288 let cos_b = beta.cos();
1289 let cos_g = gamma.cos();
1290 a * b
1291 * c
1292 * (1.0 - cos_a * cos_a - cos_b * cos_b - cos_g * cos_g + 2.0 * cos_a * cos_b * cos_g)
1293 .sqrt()
1294 }
1295
1296 /// Decode header from raw bytes with correct endianness.
1297 ///
1298 /// Endianness is detected from the MACHST field and applied automatically.
1299 /// If the detected endianness produces an invalid MODE value, the opposite
1300 /// endianness is tried as a fallback (matching the behaviour of the
1301 /// reference Python `mrcfile` library).
1302 ///
1303 /// # Examples
1304 ///
1305 /// ```
1306 /// use mrc::Header;
1307 /// let mut raw = [0u8; 1024];
1308 /// raw[0..4].copy_from_slice(&(64i32).to_le_bytes());
1309 /// raw[4..8].copy_from_slice(&(64i32).to_le_bytes());
1310 /// raw[8..12].copy_from_slice(&(1i32).to_le_bytes());
1311 /// raw[12..16].copy_from_slice(&(2i32).to_le_bytes());
1312 /// raw[208..212].copy_from_slice(b"MAP ");
1313 /// raw[212..216].copy_from_slice(&[0x44, 0x44, 0x00, 0x00]);
1314 /// let h = Header::decode_from_bytes(&raw);
1315 /// assert_eq!(h.nx, 64);
1316 /// ```
1317 pub fn decode_from_bytes(bytes: &[u8; 1024]) -> Self {
1318 Self::decode_from_bytes_with_info(bytes).0
1319 }
1320}
1321
1322/// Structured warning emitted when the MACHST byte-order stamp does not
1323/// match the actual data endianness, and the decoder had to fall back.
1324#[doc(hidden)]
1325#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1326pub enum EndianFallbackWarning {
1327 /// MACHST says little-endian but MODE is only valid as big-endian.
1328 MachstLeDataBe,
1329 /// MACHST says big-endian but MODE is only valid as little-endian.
1330 MachstBeDataLe,
1331}
1332
1333impl core::fmt::Display for EndianFallbackWarning {
1334 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1335 match self {
1336 Self::MachstLeDataBe => write!(
1337 f,
1338 "MACHST indicates little-endian but MODE is valid only as \
1339 big-endian; using big-endian"
1340 ),
1341 Self::MachstBeDataLe => write!(
1342 f,
1343 "MACHST indicates big-endian but MODE is valid only as \
1344 little-endian; using little-endian"
1345 ),
1346 }
1347 }
1348}
1349
1350// ============================================================================
1351// Macro: generate decode + encode for all numeric header fields from one list.
1352// Defines `decode_numeric_fields!` and `encode_numeric_fields!` helpers that
1353// expand to the field-by-field read/write calls. The field order in the
1354// macro matches the MRC2014 specification layout.
1355// ============================================================================
1356macro_rules! header_numeric_fields {
1357 ( $( ($field:ident, $offset:ident, $ty:ty) ),+ $(,)? ) => {
1358 macro_rules! decode_numeric_fields {
1359 ($_h:ident, $_b:expr, $_e:expr) => {
1360 $( $_h.$field = <$ty>::decode($_b, $offset, $_e); )+
1361 }
1362 }
1363 macro_rules! encode_numeric_fields {
1364 ($_h:expr, $_o:expr, $_e:expr) => {
1365 $( $_h.$field.encode($_o, $offset, $_e); )+
1366 }
1367 }
1368 }
1369}
1370
1371header_numeric_fields! {
1372 (nx, OFFSET_NX, i32),
1373 (ny, OFFSET_NY, i32),
1374 (nz, OFFSET_NZ, i32),
1375 (mode, OFFSET_MODE, i32),
1376 (nxstart, OFFSET_NXSTART, i32),
1377 (nystart, OFFSET_NYSTART, i32),
1378 (nzstart, OFFSET_NZSTART, i32),
1379 (mx, OFFSET_MX, i32),
1380 (my, OFFSET_MY, i32),
1381 (mz, OFFSET_MZ, i32),
1382 (xlen, OFFSET_XLEN, f32),
1383 (ylen, OFFSET_YLEN, f32),
1384 (zlen, OFFSET_ZLEN, f32),
1385 (alpha, OFFSET_ALPHA, f32),
1386 (beta, OFFSET_BETA, f32),
1387 (gamma, OFFSET_GAMMA, f32),
1388 (mapc, OFFSET_MAPC, i32),
1389 (mapr, OFFSET_MAPR, i32),
1390 (maps, OFFSET_MAPS, i32),
1391 (dmin, OFFSET_DMIN, f32),
1392 (dmax, OFFSET_DMAX, f32),
1393 (dmean, OFFSET_DMEAN, f32),
1394 (ispg, OFFSET_ISPG, i32),
1395 (nsymbt, OFFSET_NSYMBT, i32),
1396}
1397
1398impl Header {
1399 /// Decode header and return any byte-order fallback that occurred.
1400 ///
1401 /// Returns `(header, warning)` where `warning` is `Some` if the MACHST
1402 /// indicated one endianness but the MODE field was only valid under the
1403 /// opposite endianness.
1404 ///
1405 /// # Examples
1406 ///
1407 /// ```
1408 /// use mrc::Header;
1409 /// let mut raw = [0u8; 1024];
1410 /// raw[0..4].copy_from_slice(&(64i32).to_le_bytes());
1411 /// raw[4..8].copy_from_slice(&(64i32).to_le_bytes());
1412 /// raw[8..12].copy_from_slice(&(1i32).to_le_bytes());
1413 /// raw[12..16].copy_from_slice(&(2i32).to_le_bytes());
1414 /// raw[208..212].copy_from_slice(b"MAP ");
1415 /// raw[212..216].copy_from_slice(&[0x44, 0x44, 0x00, 0x00]);
1416 /// let (h, warn) = Header::decode_from_bytes_with_info(&raw);
1417 /// assert_eq!(h.nx, 64);
1418 /// assert!(warn.is_none());
1419 /// ```
1420 pub fn decode_from_bytes_with_info(
1421 bytes: &[u8; 1024],
1422 ) -> (Self, Option<EndianFallbackWarning>) {
1423 use crate::engine::endian::FileEndian;
1424
1425 let machst = [
1426 bytes[OFFSET_MACHST],
1427 bytes[OFFSET_MACHST + 1],
1428 bytes[OFFSET_MACHST + 2],
1429 bytes[OFFSET_MACHST + 3],
1430 ];
1431 let detected = FileEndian::from_machst(&machst);
1432
1433 let header = Self::decode_with_endian(bytes, detected);
1434
1435 // Byte-order fallback: if MODE is invalid under detected endianness,
1436 // try the opposite endianness. This handles malformed files where
1437 // the MACHST is wrong but the rest of the file is correctly encoded.
1438 if crate::Mode::from_i32(header.mode).is_none() {
1439 let opposite = detected.opposite();
1440 let candidate = Self::decode_with_endian(bytes, opposite);
1441 if crate::Mode::from_i32(candidate.mode).is_some() {
1442 let warning = match detected {
1443 FileEndian::LittleEndian => EndianFallbackWarning::MachstLeDataBe,
1444 FileEndian::BigEndian => EndianFallbackWarning::MachstBeDataLe,
1445 };
1446 return (candidate, Some(warning));
1447 }
1448 }
1449
1450 (header, None)
1451 }
1452
1453 fn decode_with_endian(bytes: &[u8; 1024], file_endian: crate::FileEndian) -> Self {
1454 use crate::engine::codec::EndianCodec;
1455
1456 let mut header = Self::new();
1457
1458 decode_numeric_fields!(header, bytes, file_endian);
1459
1460 header
1461 .extra
1462 .copy_from_slice(&bytes[OFFSET_EXTRA..OFFSET_ORIGIN]);
1463
1464 header.origin[0] = f32::decode(bytes, OFFSET_ORIGIN, file_endian);
1465 header.origin[1] = f32::decode(bytes, OFFSET_ORIGIN + 4, file_endian);
1466 header.origin[2] = f32::decode(bytes, OFFSET_ORIGIN + 8, file_endian);
1467
1468 header
1469 .map
1470 .copy_from_slice(&bytes[OFFSET_MAP..OFFSET_MACHST]);
1471 header
1472 .machst
1473 .copy_from_slice(&bytes[OFFSET_MACHST..OFFSET_RMS]);
1474
1475 header.rms = f32::decode(bytes, OFFSET_RMS, file_endian);
1476 header.nlabl = i32::decode(bytes, OFFSET_NLABL, file_endian);
1477 header.label.copy_from_slice(&bytes[OFFSET_LABEL..1024]);
1478
1479 header
1480 }
1481
1482 /// Encode header to raw bytes with correct endianness.
1483 ///
1484 /// Endianness is determined from the MACHST field and applied automatically.
1485 ///
1486 /// # Examples
1487 ///
1488 /// ```
1489 /// use mrc::Header;
1490 /// let h = Header::new();
1491 /// let mut encoded = [0u8; 1024];
1492 /// h.encode_to_bytes(&mut encoded);
1493 /// assert_eq!(&encoded[208..212], b"MAP ");
1494 /// ```
1495 pub fn encode_to_bytes(&self, out: &mut [u8; 1024]) {
1496 use crate::engine::codec::EndianCodec;
1497
1498 let file_endian = self.detect_endian();
1499
1500 // Write all numeric fields via the shared macro (22 fields)
1501 encode_numeric_fields!(self, out, file_endian);
1502
1503 // Write extra bytes
1504 out[OFFSET_EXTRA..OFFSET_ORIGIN].copy_from_slice(&self.extra);
1505
1506 // Write origin coordinates
1507 self.origin[0].encode(out, OFFSET_ORIGIN, file_endian);
1508 self.origin[1].encode(out, OFFSET_ORIGIN + 4, file_endian);
1509 self.origin[2].encode(out, OFFSET_ORIGIN + 8, file_endian);
1510
1511 // Write MAP identifier - ASCII, no endian conversion
1512 out[OFFSET_MAP..OFFSET_MACHST].copy_from_slice(&self.map);
1513
1514 // Write MACHST - byte signature, no endian conversion
1515 out[OFFSET_MACHST..OFFSET_RMS].copy_from_slice(&self.machst);
1516
1517 // Write RMS
1518 self.rms.encode(out, OFFSET_RMS, file_endian);
1519
1520 // Write label count
1521 self.nlabl.encode(out, OFFSET_NLABL, file_endian);
1522
1523 // Write labels - ASCII, no endian conversion
1524 out[OFFSET_LABEL..1024].copy_from_slice(&self.label);
1525 }
1526}
1527
1528/// IMOD-specific metadata parsed from the `extra` block (bytes 56-63).
1529///
1530/// IMOD stores metadata in the MRC-2014 `extra` free-form area at offsets
1531/// 152-159. The `imodStamp` at offset 152 spells `"IMOD"` in ASCII and
1532/// identifies the file as IMOD-created. The `imodFlags` at offset 156
1533/// contain bit flags for signedness, origin convention, etc.
1534#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1535pub struct ImodInfo {
1536 /// When `true`, Mode 0 (Int8) bytes are signed (matching MRC-2014).
1537 /// When `false`, bytes are unsigned (IMOD legacy convention).
1538 pub bytes_are_signed: bool,
1539}
1540
1541impl Header {
1542 /// Detect IMOD-specific metadata from the `extra` bytes.
1543 ///
1544 /// Returns `None` if the `imodStamp` is not present (file is not
1545 /// IMOD-created or uses a very old IMOD version).
1546 ///
1547 /// When this returns `Some`, the `imodFlags` at `extra[60]` indicate
1548 /// whether Mode 0 bytes are signed or unsigned:
1549 /// - `bytes_are_signed: true` → bit 0 set → standard MRC-2014 signed bytes
1550 /// - `bytes_are_signed: false` → bit 0 clear → IMOD legacy unsigned bytes
1551 ///
1552 /// # Examples
1553 ///
1554 /// ```
1555 /// use mrc::Header;
1556 /// let mut h = Header::new();
1557 /// h.extra[56..60].copy_from_slice(b"IMOD");
1558 /// assert!(h.detect_imod().is_some());
1559 /// ```
1560 pub fn detect_imod(&self) -> Option<ImodInfo> {
1561 // imodStamp at extra[56..60] = little-endian "IMOD" (1146047817)
1562 if self.extra[56..60] == [0x49, 0x4D, 0x4F, 0x44] {
1563 Some(ImodInfo {
1564 bytes_are_signed: (self.extra[60] & 1) != 0,
1565 })
1566 } else {
1567 None
1568 }
1569 }
1570
1571 /// Returns `true` when `mapr == -2`, indicating Y-inverted image data
1572 /// (an IMOD convention not part of standard MRC-2014).
1573 ///
1574 /// # Examples
1575 ///
1576 /// ```
1577 /// use mrc::Header;
1578 /// let mut h = Header::new();
1579 /// h.mapr = -2;
1580 /// assert!(h.is_y_inverted());
1581 /// ```
1582 pub fn is_y_inverted(&self) -> bool {
1583 self.mapr == -2
1584 }
1585}
1586
1587/// IMOD image type classification from the `idtype` field.
1588#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1589#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1590#[non_exhaustive]
1591pub enum ImodImageType {
1592 /// Single image or untitled stack.
1593 Mono,
1594 /// Single tilt series.
1595 Tilt,
1596 /// Multiple tilt series.
1597 Tilts,
1598 /// Linear interpolation data.
1599 Lina,
1600 /// Linear interpolation data with extra parameters.
1601 Lins,
1602}
1603
1604/// IMOD-specific metadata parsed from the main header's `extra` bytes.
1605///
1606/// Returned by [`parse_imod_metadata`]. Only populated when the `imodStamp`
1607/// is present, indicating an IMOD-created file.
1608#[derive(Debug, Clone, PartialEq)]
1609#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1610pub struct ImodMetadata {
1611 /// Whether Mode 0 bytes are signed (true) or unsigned (false).
1612 pub bytes_are_signed: bool,
1613 /// Raw IMOD flags from `extra[60..62]` (bit 0 = signed mode 0).
1614 pub imod_flags: u16,
1615 /// Image stack type classification.
1616 pub image_type: ImodImageType,
1617 /// Tilt axis (1=X, 2=Y, 3=Z).
1618 pub tilt_axis: u8,
1619 /// Tilt increment in degrees (`vd1 / 100.0`).
1620 pub tilt_increment: f32,
1621 /// Starting tilt angle in degrees (`vd2 / 100.0`).
1622 pub start_angle: f32,
1623 /// Original tilt angles `[tilt_x, tilt_y, tilt_z]`.
1624 pub original_angles: [f32; 3],
1625 /// Current tilt angles `[tilt_x, tilt_y, tilt_z]`.
1626 pub current_angles: [f32; 3],
1627 /// X origin in pixels (`extra[0..4]` as f32 LE).
1628 pub x_origin: f32,
1629 /// Y origin in pixels (`extra[4..8]` as f32 LE).
1630 pub y_origin: f32,
1631 /// Z origin in pixels (`extra[8..12]` as f32 LE).
1632 pub z_origin: f32,
1633 /// Cell size in X dimension in Å (`extra[12..16]` as f32 LE).
1634 pub x_cell_size: f32,
1635 /// Cell size in Y dimension in Å (`extra[16..20]` as f32 LE).
1636 pub y_cell_size: f32,
1637 /// Cell size in Z dimension in Å (`extra[20..24]` as f32 LE).
1638 pub z_cell_size: f32,
1639}
1640
1641/// Parse IMOD metadata from the main header's `extra` bytes.
1642///
1643/// Returns `None` if the `imodStamp` is not present (file is not IMOD-created).
1644///
1645/// Fields are decoded from little-endian integers and floats stored in the
1646/// MRC-2014 `extra` free-form block (offsets 152–195).
1647pub fn parse_imod_metadata(header: &Header) -> Option<ImodMetadata> {
1648 // Check for imodStamp
1649 if header.extra[56..60] != [0x49, 0x4D, 0x4F, 0x44] {
1650 return None;
1651 }
1652
1653 let le_i16 = |offset: usize| -> i16 {
1654 i16::from_le_bytes([header.extra[offset], header.extra[offset + 1]])
1655 };
1656
1657 let le_f32 = |offset: usize| -> f32 {
1658 f32::from_le_bytes([
1659 header.extra[offset],
1660 header.extra[offset + 1],
1661 header.extra[offset + 2],
1662 header.extra[offset + 3],
1663 ])
1664 };
1665
1666 let idtype = le_i16(64);
1667 let image_type = match idtype {
1668 0 => ImodImageType::Mono,
1669 1 => ImodImageType::Tilt,
1670 2 => ImodImageType::Tilts,
1671 3 => ImodImageType::Lina,
1672 4 => ImodImageType::Lins,
1673 _ => ImodImageType::Mono, // fallback
1674 };
1675
1676 let flags = le_i16(60) as u16; // lower 2 bytes of imodFlags
1677 let bytes_are_signed = (flags & 1) != 0;
1678 let tilt_axis = le_i16(68).clamp(1, 3) as u8;
1679 let tilt_increment = le_i16(72) as f32 / 100.0;
1680 let start_angle = le_i16(74) as f32 / 100.0;
1681
1682 // tiltangles[6] at extra[76..100], 6 little-endian f32 values
1683 let original_angles = [le_f32(76), le_f32(80), le_f32(84)];
1684 let current_angles = [le_f32(88), le_f32(92), le_f32(96)];
1685
1686 // IMOD origin and cell size from the beginning of extra bytes
1687 let x_origin = le_f32(0);
1688 let y_origin = le_f32(4);
1689 let z_origin = le_f32(8);
1690 let x_cell_size = le_f32(12);
1691 let y_cell_size = le_f32(16);
1692 let z_cell_size = le_f32(20);
1693
1694 Some(ImodMetadata {
1695 bytes_are_signed,
1696 imod_flags: flags,
1697 image_type,
1698 tilt_axis,
1699 tilt_increment,
1700 start_angle,
1701 original_angles,
1702 current_angles,
1703 x_origin,
1704 y_origin,
1705 z_origin,
1706 x_cell_size,
1707 y_cell_size,
1708 z_cell_size,
1709 })
1710}
1711
1712/// Builder for constructing validated MRC headers.
1713///
1714/// # Example
1715/// ```
1716/// use mrc::HeaderBuilder;
1717///
1718/// let header = HeaderBuilder::new()
1719/// .shape([512, 512, 256])
1720/// .mode::<f32>()
1721/// .build()
1722/// .unwrap();
1723/// ```
1724#[derive(Debug, Clone)]
1725pub struct HeaderBuilder {
1726 header: Header,
1727}
1728
1729impl HeaderBuilder {
1730 /// Create a new header builder with sensible defaults.
1731 ///
1732 /// # Examples
1733 ///
1734 /// ```
1735 /// use mrc::HeaderBuilder;
1736 /// let builder = HeaderBuilder::new();
1737 /// ```
1738 #[must_use]
1739 pub fn new() -> Self {
1740 Self {
1741 header: Header::new(),
1742 }
1743 }
1744
1745 /// Set the volume dimensions.
1746 ///
1747 /// Also synchronises `mx`, `my`, `mz` to match `nx`, `ny`, `nz`, following
1748 /// the convention used by the reference Python `mrcfile` library.
1749 ///
1750 /// # Examples
1751 ///
1752 /// ```
1753 /// use mrc::HeaderBuilder;
1754 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1755 /// let h = HeaderBuilder::new()
1756 /// .shape([512, 512, 256])
1757 /// .mode::<f32>()
1758 /// .build()?;
1759 /// assert_eq!(h.nx, 512);
1760 /// # Ok(())
1761 /// # }
1762 /// ```
1763 #[must_use]
1764 pub fn shape(mut self, shape: [usize; 3]) -> Self {
1765 self.header.nx = shape[0] as i32;
1766 self.header.ny = shape[1] as i32;
1767 self.header.nz = shape[2] as i32;
1768 self.header.mx = self.header.nx;
1769 self.header.my = self.header.ny;
1770 self.header.mz = self.header.nz;
1771 self
1772 }
1773
1774 /// Set the voxel mode from a Rust type.
1775 ///
1776 /// # Examples
1777 ///
1778 /// ```
1779 /// use mrc::HeaderBuilder;
1780 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1781 /// let h = HeaderBuilder::new()
1782 /// .shape([64, 64, 1])
1783 /// .mode::<f32>()
1784 /// .build()?;
1785 /// assert_eq!(h.mode, 2);
1786 /// # Ok(())
1787 /// # }
1788 /// ```
1789 #[must_use]
1790 pub fn mode<T: crate::mode::Voxel>(mut self) -> Self {
1791 self.header.mode = T::MODE.as_i32();
1792 self
1793 }
1794
1795 /// Set the MRC mode by raw integer value (for modes without a [`crate::Voxel`] impl).
1796 ///
1797 /// This is primarily useful for [`Mode::Packed4Bit`] (mode 101) which does not
1798 /// implement `Voxel`. Invalid mode constants are caught by header validation
1799 /// at `build()` time.
1800 ///
1801 /// # Examples
1802 ///
1803 /// ```
1804 /// use mrc::HeaderBuilder;
1805 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1806 /// let h = HeaderBuilder::new()
1807 /// .shape([8, 8, 1])
1808 /// .mode_raw(101)
1809 /// .build()?;
1810 /// assert_eq!(h.mode, 101);
1811 /// # Ok(())
1812 /// # }
1813 /// ```
1814 #[must_use]
1815 pub fn mode_raw(mut self, mode: i32) -> Self {
1816 self.header.mode = mode;
1817 self
1818 }
1819
1820 /// Set the cell dimensions in Angstroms.
1821 ///
1822 /// # Examples
1823 ///
1824 /// ```
1825 /// use mrc::HeaderBuilder;
1826 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1827 /// let h = HeaderBuilder::new()
1828 /// .shape([64, 64, 1])
1829 /// .mode::<f32>()
1830 /// .cell_lengths(100.0, 100.0, 100.0)
1831 /// .build()?;
1832 /// assert_eq!(h.xlen, 100.0);
1833 /// # Ok(())
1834 /// # }
1835 /// ```
1836 #[must_use]
1837 pub fn cell_lengths(mut self, xlen: f32, ylen: f32, zlen: f32) -> Self {
1838 self.header.xlen = xlen;
1839 self.header.ylen = ylen;
1840 self.header.zlen = zlen;
1841 self
1842 }
1843
1844 /// Set the cell angles in degrees (alpha, beta, gamma).
1845 ///
1846 /// # Examples
1847 ///
1848 /// ```
1849 /// use mrc::HeaderBuilder;
1850 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1851 /// let h = HeaderBuilder::new()
1852 /// .shape([64, 64, 1])
1853 /// .mode::<f32>()
1854 /// .cell_angles(90.0, 90.0, 120.0)
1855 /// .build()?;
1856 /// assert_eq!(h.gamma, 120.0);
1857 /// # Ok(())
1858 /// # }
1859 /// ```
1860 #[must_use]
1861 pub fn cell_angles(mut self, alpha: f32, beta: f32, gamma: f32) -> Self {
1862 self.header.alpha = alpha;
1863 self.header.beta = beta;
1864 self.header.gamma = gamma;
1865 self
1866 }
1867
1868 /// Set the space group number.
1869 ///
1870 /// # Examples
1871 ///
1872 /// ```
1873 /// use mrc::HeaderBuilder;
1874 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1875 /// let h = HeaderBuilder::new()
1876 /// .shape([64, 64, 1])
1877 /// .mode::<f32>()
1878 /// .ispg(0)
1879 /// .build()?;
1880 /// assert_eq!(h.ispg, 0);
1881 /// # Ok(())
1882 /// # }
1883 /// ```
1884 #[must_use]
1885 pub fn ispg(mut self, ispg: i32) -> Self {
1886 self.header.ispg = ispg;
1887 self
1888 }
1889
1890 /// Configure as a volume stack with the given sub-volume thickness.
1891 ///
1892 /// Shorthand for calling [`ispg(401)`](Self::ispg) and setting
1893 /// `mz` to the given value. `nz` must be divisible by `mz` for
1894 /// the header to validate.
1895 ///
1896 /// # Examples
1897 ///
1898 /// ```
1899 /// use mrc::HeaderBuilder;
1900 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1901 /// let h = HeaderBuilder::new()
1902 /// .shape([64, 64, 32])
1903 /// .mode::<f32>()
1904 /// .set_volume_stack(16)
1905 /// .build()?;
1906 /// assert!(h.is_volume_stack());
1907 /// # Ok(())
1908 /// # }
1909 /// ```
1910 #[must_use]
1911 pub fn set_volume_stack(mut self, mz: i32) -> Self {
1912 self.header.set_volume_stack(mz);
1913 self
1914 }
1915
1916 /// Set the extended header type (4-byte ASCII identifier).
1917 ///
1918 /// # Examples
1919 ///
1920 /// ```
1921 /// use mrc::HeaderBuilder;
1922 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1923 /// let h = HeaderBuilder::new()
1924 /// .shape([64, 64, 1])
1925 /// .mode::<f32>()
1926 /// .exttyp(*b"CCP4")
1927 /// .build()?;
1928 /// assert_eq!(h.exttyp(), *b"CCP4");
1929 /// # Ok(())
1930 /// # }
1931 /// ```
1932 #[must_use]
1933 pub fn exttyp(mut self, exttyp: [u8; 4]) -> Self {
1934 self.header.set_exttyp(exttyp);
1935 self
1936 }
1937
1938 /// Set the extended header size in bytes.
1939 ///
1940 /// # Examples
1941 ///
1942 /// ```
1943 /// use mrc::HeaderBuilder;
1944 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1945 /// let h = HeaderBuilder::new()
1946 /// .shape([64, 64, 1])
1947 /// .mode::<f32>()
1948 /// .nsymbt(256)
1949 /// .build()?;
1950 /// assert_eq!(h.nsymbt, 256);
1951 /// # Ok(())
1952 /// # }
1953 /// ```
1954 #[must_use]
1955 pub fn nsymbt(mut self, nsymbt: i32) -> Self {
1956 self.header.nsymbt = nsymbt;
1957 self
1958 }
1959
1960 /// Set the origin coordinates.
1961 ///
1962 /// # Examples
1963 ///
1964 /// ```
1965 /// use mrc::HeaderBuilder;
1966 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1967 /// let h = HeaderBuilder::new()
1968 /// .shape([64, 64, 1])
1969 /// .mode::<f32>()
1970 /// .origin([1.0, 2.0, 3.0])
1971 /// .build()?;
1972 /// assert_eq!(h.origin, [1.0, 2.0, 3.0]);
1973 /// # Ok(())
1974 /// # }
1975 /// ```
1976 #[must_use]
1977 pub fn origin(mut self, origin: [f32; 3]) -> Self {
1978 self.header.origin = origin;
1979 self
1980 }
1981
1982 /// Set the sub-volume origin in pixels (`nxstart`, `nystart`, `nzstart`).
1983 ///
1984 /// This is the starting point of the image data within the unit cell.
1985 /// Commonly used in IMOD and tilt-series metadata.
1986 ///
1987 /// # Examples
1988 ///
1989 /// ```
1990 /// use mrc::HeaderBuilder;
1991 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1992 /// let h = HeaderBuilder::new()
1993 /// .shape([64, 64, 1])
1994 /// .mode::<f32>()
1995 /// .nstart([10, 20, 0])
1996 /// .build()?;
1997 /// assert_eq!(h.nxstart, 10);
1998 /// # Ok(())
1999 /// # }
2000 /// ```
2001 #[must_use]
2002 pub fn nstart(mut self, nstart: [i32; 3]) -> Self {
2003 self.header.nxstart = nstart[0];
2004 self.header.nystart = nstart[1];
2005 self.header.nzstart = nstart[2];
2006 self
2007 }
2008
2009 /// Set the sampling rates (`mx`, `my`, `mz`) independently of the volume
2010 /// dimensions.
2011 ///
2012 /// By default [`shape`](Self::shape) syncs `mx`, `my`, `mz` to `nx`, `ny`,
2013 /// `nz`. Use this method to override them when the cell sampling differs
2014 /// from the pixel dimensions.
2015 ///
2016 /// # Examples
2017 ///
2018 /// ```
2019 /// use mrc::HeaderBuilder;
2020 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2021 /// let h = HeaderBuilder::new()
2022 /// .shape([64, 64, 32])
2023 /// .mode::<f32>()
2024 /// .sampling([64, 64, 32])
2025 /// .build()?;
2026 /// assert_eq!(h.mx, 64);
2027 /// # Ok(())
2028 /// # }
2029 /// ```
2030 #[must_use]
2031 pub fn sampling(mut self, sampling: [i32; 3]) -> Self {
2032 self.header.mx = sampling[0];
2033 self.header.my = sampling[1];
2034 self.header.mz = sampling[2];
2035 self
2036 }
2037
2038 /// Set the axis mapping (`mapc`, `mapr`, `maps`) — a permutation of
2039 /// `1` (X), `2` (Y), `3` (Z) that defines which axis is column, row,
2040 /// and section.
2041 ///
2042 /// The default is `[1, 2, 3]` (X=column, Y=row, Z=section), which
2043 /// covers nearly all MRC files. Override only when you need a
2044 /// non-standard axis layout.
2045 ///
2046 /// # Examples
2047 ///
2048 /// ```
2049 /// use mrc::HeaderBuilder;
2050 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2051 /// let h = HeaderBuilder::new()
2052 /// .shape([64, 64, 32])
2053 /// .mode::<f32>()
2054 /// .axis_mapping([2, 1, 3])
2055 /// .build()?;
2056 /// assert_eq!(h.mapc, 2);
2057 /// # Ok(())
2058 /// # }
2059 /// ```
2060 #[must_use]
2061 pub fn axis_mapping(mut self, mapping: [i32; 3]) -> Self {
2062 self.header.mapc = mapping[0];
2063 self.header.mapr = mapping[1];
2064 self.header.maps = mapping[2];
2065 self
2066 }
2067
2068 /// Append a text label.
2069 ///
2070 /// Delegates to [`Header::add_label`]. Labels are stored in the
2071 /// 800-byte label field (up to 10 labels). When full, the oldest
2072 /// label is dropped (FIFO).
2073 ///
2074 /// # Examples
2075 ///
2076 /// ```
2077 /// use mrc::HeaderBuilder;
2078 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2079 /// let h = HeaderBuilder::new()
2080 /// .shape([64, 64, 1])
2081 /// .mode::<f32>()
2082 /// .add_label("my sample")
2083 /// .build()?;
2084 /// assert_eq!(h.get_labels(), vec!["my sample"]);
2085 /// # Ok(())
2086 /// # }
2087 /// ```
2088 #[must_use]
2089 pub fn add_label(mut self, text: &str) -> Self {
2090 self.header.add_label(text);
2091 self
2092 }
2093
2094 /// Consume the builder and return the header.
2095 ///
2096 /// # Examples
2097 ///
2098 /// ```
2099 /// use mrc::HeaderBuilder;
2100 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2101 /// let h = HeaderBuilder::new()
2102 /// .shape([64, 64, 1])
2103 /// .mode::<f32>()
2104 /// .build()?;
2105 /// assert!(h.validate());
2106 /// # Ok(())
2107 /// # }
2108 /// ```
2109 pub fn build(self) -> Result<Header, crate::HeaderValidationError> {
2110 self.header.validate_detailed()?;
2111 Ok(self.header)
2112 }
2113}
2114
2115impl Default for HeaderBuilder {
2116 fn default() -> Self {
2117 Self::new()
2118 }
2119}
2120
2121#[cfg(test)]
2122mod tests {
2123 use super::*;
2124
2125 /// Build a valid little-endian header encoded as raw bytes.
2126 fn le_header_bytes() -> [u8; 1024] {
2127 let mut h = Header::new();
2128 h.nx = 64;
2129 h.ny = 64;
2130 h.nz = 64;
2131 h.mx = 64;
2132 h.my = 64;
2133 h.mz = 64;
2134 h.mode = 2; // Float32
2135 h.set_file_endian(crate::FileEndian::LittleEndian);
2136 let mut bytes = [0u8; 1024];
2137 h.encode_to_bytes(&mut bytes);
2138 bytes
2139 }
2140
2141 /// Build a valid big-endian header encoded as raw bytes.
2142 fn be_header_bytes() -> [u8; 1024] {
2143 let mut h = Header::new();
2144 h.nx = 64;
2145 h.ny = 64;
2146 h.nz = 64;
2147 h.mx = 64;
2148 h.my = 64;
2149 h.mz = 64;
2150 h.mode = 2; // Float32
2151 h.set_file_endian(crate::FileEndian::BigEndian);
2152 let mut bytes = [0u8; 1024];
2153 h.encode_to_bytes(&mut bytes);
2154 bytes
2155 }
2156
2157 #[test]
2158 fn test_decode_roundtrip_le() {
2159 let original = le_header_bytes();
2160 let decoded = Header::decode_from_bytes(&original);
2161 assert_eq!(decoded.nx, 64);
2162 assert_eq!(decoded.mode, 2);
2163 assert_eq!(decoded.detect_endian(), crate::FileEndian::LittleEndian);
2164 }
2165
2166 #[test]
2167 fn test_decode_roundtrip_be() {
2168 let original = be_header_bytes();
2169 let decoded = Header::decode_from_bytes(&original);
2170 assert_eq!(decoded.nx, 64);
2171 assert_eq!(decoded.mode, 2);
2172 assert_eq!(decoded.detect_endian(), crate::FileEndian::BigEndian);
2173 }
2174
2175 #[test]
2176 fn test_byte_order_fallback_le_stamp_be_data() {
2177 // Create a file that claims to be LE (0x44 0x44) but is actually BE-encoded.
2178 let mut bytes = be_header_bytes();
2179 // Overwrite MACHST to claim LE
2180 bytes[212] = 0x44;
2181 bytes[213] = 0x44;
2182 bytes[214] = 0x00;
2183 bytes[215] = 0x00;
2184
2185 let (decoded, warning) = Header::decode_from_bytes_with_info(&bytes);
2186
2187 // Without fallback, nx would be 0x4000_0000 (garbage under LE interpretation)
2188 // With fallback, nx should correctly decode as 64.
2189 assert_eq!(
2190 decoded.nx, 64,
2191 "byte-order fallback should have corrected nx"
2192 );
2193 assert_eq!(decoded.mode, 2);
2194 assert!(
2195 warning.is_some(),
2196 "should emit a warning when MACHST mismatches actual byte order"
2197 );
2198 }
2199
2200 #[test]
2201 fn test_byte_order_fallback_be_stamp_le_data() {
2202 // Create a file that claims to be BE (0x11 0x11) but is actually LE-encoded.
2203 let mut bytes = le_header_bytes();
2204 // Overwrite MACHST to claim BE
2205 bytes[212] = 0x11;
2206 bytes[213] = 0x11;
2207 bytes[214] = 0x00;
2208 bytes[215] = 0x00;
2209
2210 let (decoded, warning) = Header::decode_from_bytes_with_info(&bytes);
2211
2212 assert_eq!(
2213 decoded.nx, 64,
2214 "byte-order fallback should have corrected nx"
2215 );
2216 assert_eq!(decoded.mode, 2);
2217 assert!(warning.is_some());
2218 }
2219
2220 #[test]
2221 fn test_no_fallback_when_machst_matches() {
2222 let bytes = le_header_bytes();
2223 let (decoded, warning) = Header::decode_from_bytes_with_info(&bytes);
2224 assert_eq!(decoded.nx, 64);
2225 assert!(warning.is_none(), "no warning when MACHST is correct");
2226 }
2227
2228 #[test]
2229 fn test_ccp41_machst_recognised() {
2230 let mut bytes = le_header_bytes();
2231 bytes[212] = 0x44;
2232 bytes[213] = 0x41;
2233 let decoded = Header::decode_from_bytes(&bytes);
2234 assert_eq!(decoded.nx, 64);
2235 assert_eq!(decoded.detect_endian(), crate::FileEndian::LittleEndian);
2236 }
2237
2238 #[test]
2239 fn test_nversion_le() {
2240 let mut h = Header::new();
2241 h.set_file_endian(crate::FileEndian::LittleEndian);
2242 assert_eq!(h.nversion(), 20141);
2243 }
2244
2245 #[test]
2246 fn test_nversion_be() {
2247 let mut h = Header::new();
2248 h.set_file_endian(crate::FileEndian::BigEndian);
2249 assert_eq!(h.nversion(), 20141);
2250 }
2251
2252 #[test]
2253 fn test_nversion_zero_accepted_by_validate() {
2254 // EPU files often leave NVERSION at 0 (uninitialized).
2255 let mut h = Header::new();
2256 h.nx = 64;
2257 h.ny = 64;
2258 h.nz = 1;
2259 h.mx = 64;
2260 h.my = 64;
2261 h.mz = 1;
2262 h.nlabl = 0;
2263 h.set_nversion(0);
2264 assert_eq!(h.nversion(), 0);
2265 assert!(h.validate(), "NVERSION=0 should pass strict validation");
2266 }
2267}