Expand description
Read and write MRC-2014 files — the standard in cryo-EM and structural biology.
This crate handles file I/O, byte-order detection, and type-safe data access so you can focus on your science. It’s fast (SIMD, parallel encoding) and works with plain, gzip, and (optionally) bzip2 files.
See the README for installation
instructions. The mrc-cli companion
crate provides a mrc-cli binary with subcommands for inspection,
validation, conversion, PNG/GIF export, and resampling — install it with
cargo install mrc-cli.
§Quick example
use mrc::{read_as, write_as};
// Read an entire volume in one call
let (header, data): (_, Vec<f32>) = read_as("density.mrc")?;
println!("{}×{}×{} volume, {} voxels",
header.nx, header.ny, header.nz, data.len());
// Write a volume in one call
write_as("output.mrc", &data, [512, 512, 256])?;§Reading files
Open any MRC file with open() or Reader::open. Compression is
detected from magic bytes — no need to hint gzip or bzip2.
use mrc::Reader;
let reader = Reader::open("tilt_series.mrc")?;
println!("{}×{}×{} voxels, mode {:?}",
reader.shape().nx, reader.shape().ny, reader.shape().nz,
reader.mode());Then pick an iteration method, each returning VoxelBlock<T> chunks:
slices— one Z-plane at a timeslabs— batches ofkZ-planestiles— arbitrary 3D blocksvolumes— sub-volumes in a volume stacksubregion— a single block by coordinateread_volume— the entire volume as one block
All iteration methods share the same API pattern:
// One slice at a time:
for slice in reader.slices::<f32>() {
let block = slice?;
}
// Or batches of 16 Z-planes:
for slab in reader.slabs::<f32>(16) {
let block = slab?;
}
// Or arbitrary 3D tiles:
for tile in reader.tiles::<f32>([64, 64, 64])? {
let block = tile?;
}Each block contains the offset, shape, and flat data: Vec<T>.
Read the entire volume with read_volume:
let block: mrc::VoxelBlock<f32> = reader.read_volume::<f32>()?;
println!("shape: {:?}, voxels: {}", block.shape, block.data.len());No trait imports needed: All iterator and conversion methods are available as inherent methods on
Readerwithout any import.
For automatic mode conversion, use convert:
for slice in reader.convert::<f32>().slices() {
let block = slice?;
println!("slice {} mean density: {:.2}",
block.offset[2],
block.data.iter().sum::<f32>() / block.data.len() as f32);
}Or read the full volume in one call:
let block = reader.convert::<f32>().read_volume()?;
println!("read {} voxels", block.data.len());The same iteration methods — slabs,
tiles, subregion — plus
with_complex_strategy and
with_m0_interpretation
are all available on the returned converter.
§Special-mode reads (Packed4Bit and Mode 0)
These modes have no Voxel implementation — there is no single Rust
type that maps to them safely at compile time — so dedicated methods read
directly as u8 or f32:
slices_u8/slabs_u8— unpack Packed4Bit nibbles, or narrow Uint16, tou8read_volume_u8— full volume asu8(Packed4Bit only)slices_mode0/slabs_mode0— read Mode 0 asf32, choosing signed or unsigned interpretation
§Reading from memory or streams
When your data is already in memory (e.g. from a camera readout, network
stream, or embedded resource), use Reader::from_reader or
Reader::from_bytes:
use mrc::Reader;
use std::io::Cursor;
let bytes = std::fs::read("density.mrc")?;
let reader = Reader::from_reader(Cursor::new(bytes))?;Permissive variants Reader::from_reader_permissive and
Reader::from_bytes_permissive accept non-critical header issues as
warnings without failing, mirroring Reader::open_permissive.
§Large files
When the file does not fit in RAM, Reader::open automatically uses
memory-mapped I/O (requires the mmap feature). Same iterator API,
zero-copy slab_as, OS-managed paging.
For buffered readers (in-memory buffers, compressed files),
slab_as also provides zero-copy typed access
under the same conditions (native endian, matching type).
§Quirky files
Common microscope quirks (NVERSION left at 0, "MAP\0" instead of "MAP ")
are handled transparently by open() — no special flags needed.
For esoteric or severely non-standard files, use
Reader::open_permissive which turns non-critical header issues into
warnings instead of hard errors, and allows opening files that are shorter
than the header declares. Use is_truncated to
detect truncated data after a permissive open:
let (reader, warnings) = Reader::open_permissive("legacy.mrc")?;
if reader.is_truncated() {
eprintln!("warning: file is incomplete");
}
for w in &warnings { eprintln!("note: {w}"); }§Writing files
Use create() to get a WriterBuilder, set the shape and voxel type,
then call finish.
For the simplest case — write an entire volume — use write_as() or
Writer::set_data:
use mrc::write_as;
let data = vec![0.0f32; 256 * 256 * 128];
write_as("output.mrc", &data, [256, 256, 128])?;For streaming writes (one slice at a time), use the builder API:
use mrc::{create, VoxelBlock};
let mut writer = create("output.mrc")
.shape([256, 256, 128])
.mode::<f32>()
.finish()?;
// Write some data
writer.write_block(&VoxelBlock::new(
[0, 0, 0], [256, 256, 1],
vec![0.0f32; 256 * 256],
)?)?;
// Optionally compute and store density statistics
writer.update_header_stats()?;
// Finalize — required, rewrites header with final metadata
writer.finalize()?;The lifecycle:
-
Write blocks with
write_block. The typeTmatches the file’s mode — a compile-time check that prevents accidentally treating bytes as the wrong kind of number. Usewrite_block_asfor automatic conversion (e.g. writef32data to an Int16 or Float16 file)For Uint16 files,
write_u8_blockauto-widensu8data; for Packed4Bit files,write_u4_blockpacksu8values (0–15) two-per-byte. -
Optionally call
update_header_statsto fill indmin/dmax/dmean/rms. -
Finalize with
finalizeto rewrite the header with final metadata. Required — without it the header is stale.
Four backends through the same builder, plus in-memory output:
| Backend | Builder method | Best for |
|---|---|---|
Writer | finish() | General use, writes straight to disk |
Writer (in-memory) | Writer::from_writer | Memory buffer, e.g. Cursor<Vec<u8>> |
Writer (mmap) | finish_mmap() / from_writer_mmap | Very large files (mmap feature) |
Writer (gzip) | finish_gzip() / from_writer_gzip | Compressed output (gzip feature) |
Writer (bzip2) | finish_bzip2() / from_writer_bzip2 | Compressed output (bzip2 feature) |
Compressed writers support configurable CompressionLevel level via
WriterBuilder::compression:
use mrc::{CompressionLevel, create};
let mut writer = create("output.mrc.gz")
.shape([256, 256, 128])
.mode::<f32>()
.compression(CompressionLevel::Best)
.finish_gzip()?;§Data modes
MRC files encode voxels in one of several numeric modes. Mode
represents them at runtime; Voxel ties each Rust type to its mode
at compile time, catching mismatches before any data flows.
| Mode | Rust type | Typical use |
|---|---|---|
Int8 (0) | i8 | Binary masks |
Int16 (1) | i16 | Raw cryo-EM density |
Float32 (2) | f32 | Processed / reconstructed density |
Int16Complex (3) | Int16Complex | Complex data (i16 real + i16 imag) |
Float32Complex (4) | Float32Complex | Complex data (f32 real + f32 imag) |
Uint16 (6) | u16 | Segmentation labels |
Float16 (12) | f16 | Half-precision storage (feature f16) |
Packed4Bit (101) | u8 via slices_u8 | 4-bit packed data; no Voxel impl |
Packed 4-bit data is handled transparently by the unified API:
convert::<f32>() unpacks nibbles to f32,
slices_u8 / slabs_u8 unpack
to u8 (0–15), and write_u4_block packs
u8 values back.
When you don’t know the mode ahead of time, use
convert::<f32>() which converts any mode to f32.
§Feature flags
| Feature | Description | Default |
|---|---|---|
mmap | Memory-mapped readers and writers | ✅ |
f16 | Half-precision float via the half crate | ✅ |
simd | AVX2 / NEON acceleration for integer↔f32, f16↔f32, byte-swap, stats, and f32→integer clamping | ✅ |
parallel | Parallel encoding via rayon | ✅ |
gzip | Gzip-compressed I/O | ✅ |
bzip2 | Bzip2-compressed I/O | ❌ |
ndarray | Return volumes as ndarray::Array3<T> via to_ndarray() | ❌ |
serde | Serialize/Deserialize support via serde | ❌ |
use ndarray::Array3;
let arr: Array3<f32> = reader.convert::<f32>().to_ndarray()?;
println!("{}×{}×{} array", arr.shape()[0], arr.shape()[1], arr.shape()[2]);§Headers
The Header struct mirrors the 1024-byte MRC-2014 fixed header.
Every field is a typed public member — dimensions, cell parameters,
axis mapping, density statistics, text labels, and more.
use mrc::Header;
let h = Header::new();
assert_eq!(h.map, *b"MAP ");For fluent construction with validation, use HeaderBuilder:
use mrc::HeaderBuilder;
let header = HeaderBuilder::new()
.shape([512, 512, 256])
.mode::<f32>()
.cell_lengths(1.0, 1.0, 1.0)
.cell_angles(90.0, 90.0, 90.0)
.origin([0.0, 0.0, 0.0])
.nstart([0, 0, 0])
.sampling([512, 512, 256])
.axis_mapping([1, 2, 3])
.add_label("reconstructed map")
.build()?;Three validation levels:
validate— quick yes / novalidate_detailed— tells you exactly what is wrong viaHeaderValidationErrorvalidate_permissive— warnings for non-critical issues
§Volume type helpers
The header distinguishes four volume types. Configure them explicitly when creating files that are not single 3D volumes:
let mut h = Header::new();
h.nx = 64; h.ny = 64; h.nz = 120;
h.mx = 64; h.my = 64; h.mz = 30;
h.set_volume_stack(30); // ispg = 401, mz = 30
assert!(h.is_volume_stack());
assert_eq!(h.logical_shape(), [4, 30, 64, 64]); // 4 sub-volumes§Convenience API
The Header provides computed properties for common queries:
use mrc::Header;
let h = Header::new();
let vol = h.cell_volume(); // unit cell volume in ų
let (dmin, dmax, dmean, rms) = h.density_stats();
let sampling = h.sampling(); // [mx, my, mz]
let label = h.label_at(0); // first label, or None
assert!(h.is_standard_map()); // MAP field is "MAP "§Manual header parsing
Decode a raw 1024-byte header block with automatic endianness detection:
use mrc::Header;
let raw = [0u8; 1024];
// ... fill raw bytes from file ...
let header = Header::decode_from_bytes(&raw);When the MACHST byte-order stamp is wrong (common in some EPU files), the decoder tries the opposite endianness automatically:
let (header, warning) = Header::decode_from_bytes_with_info(&raw);
if let Some(w) = warning {
eprintln!("byte-order fallback used: {w}");
}§Extended headers
Many MRC files carry additional metadata after the 1024-byte fixed header
in an extended header region. The type is identified by the 4-byte
exttyp field in the header’s extra[8..12].
The ExtHeaderType enum identifies the format without parsing:
use mrc::{Header, ExtHeaderType};
let header = Header::new();
match ExtHeaderType::from_header(&header) {
ExtHeaderType::Fei1 => println!("FEI Type 1"),
ExtHeaderType::Ccp4 => println!("CCP4"),
ExtHeaderType::Unknown(id) => {
println!("Unknown: {:?}", std::str::from_utf8(&id));
}
_ => {}
}Instead of calling individual parser functions, use the auto-dispatch method on any open reader:
use mrc::ExtHeaderData;
match reader.parse_extended_header() {
ExtHeaderData::Fei1(records) => {
println!("FEI1 tilt series ({} records)", records.len());
for r in &records {
println!(" tilt {:.1}°, defocus {:.1} µm",
r.alpha_tilt, r.defocus);
}
}
ExtHeaderData::Ccp4(records) => {
println!("CCP4 symmetry ({} records)", records.len());
}
ExtHeaderData::Seri(records) => {
println!(" first tilt: {:.1}°", records[0].alpha_tilt);
}
ExtHeaderData::None => println!("No recognized extended header"),
_ => {}
}Typed convenience methods give direct access without pattern matching:
if let Some(records) = reader.fei1_metadata() {
println!("{} FEI1 records", records.len());
}
if let Some(imod) = reader.imod_metadata() {
println!("IMOD type {:?}, tilt increment {:.1}°",
imod.image_type, imod.tilt_increment);
}Available: fei1_metadata,
fei2_metadata,
ccp4_records,
mrco_records,
seri_records,
agar_records,
imod_metadata.
§Error handling
Fallible functions return Result<T, Error>. Match on specific variants
to handle different failure modes:
use mrc::Error;
fn describe(err: &Error) -> &str {
match err {
Error::Io(_) => "I/O failure",
Error::InvalidHeader => "not a valid MRC file",
Error::ModeMismatch { .. } => "wrong voxel type for this file",
Error::BoundsError { .. } => "block outside volume",
Error::FileSizeMismatch { .. } => "file truncated or has trailing data",
_ => "other",
}
}The errors you will actually hit in practice:
Io— the file could not be read or writtenInvalidHeader— not a valid MRC fileModeMismatch— callingslices::<f32>()on an Int16 file; useconvert::<f32>()insteadBoundsError— read or write outside the volumeFileSizeMismatch— file truncated or has trailing garbage
HeaderValidationError gives fine-grained diagnostics for header
problems (bad dimensions, wrong MAP field, invalid NVERSION …).
§Endianness
MRC files encode byte order via a 4-byte MACHST stamp at file offset 212.
FileEndian represents the detected byte order:
use mrc::FileEndian;
let stamp: [u8; 4] = [0x44, 0x44, 0x00, 0x00]; // little-endian
assert_eq!(FileEndian::from_machst(&stamp), FileEndian::LittleEndian);Use FileEndian::native to query the host platform, and
reader.endian() to get a file’s actual byte order.
New files are always little-endian, matching modern hardware and the Python
mrcfile library.
The crate has a fallback: if the MODE field is invalid under the detected endianness, the opposite byte order is tried. This handles files with a wrong MACHST stamp but correct data.
§Compression auto-detection
Reader::open reads the first two bytes of the file:
| Magic bytes | Format |
|---|---|
\x1f\x8b | Gzip |
BZ | Bzip2 |
| anything else | Plain |
Plain MRC files are memory-mapped or buffered directly. Compressed files
are fully decompressed into memory on open, with a hard cap of
DEFAULT_MAX_DECOMPRESSED_BYTES (256 GiB) to prevent bombs.
Use Reader::open_gzip_with_limit or
[Reader::open_bzip2_with_limit] for a custom limit.
Large compressed files: If the uncompressed data exceeds available RAM, decompress with
gunziporbunzip2first, then useReader::openfor zero-copy access — the OS pages data on demand without loading the whole file into memory.
§File validation
validate_full runs comprehensive checks
on a file — header, size, endianness, data statistics (1% tolerance),
and NaN / Inf scanning. Returns a
ValidationReport with categorized issues:
use mrc::validate::{validate_full, Severity};
let report = validate_full("protein.mrc", false)?;
if !report.is_valid() {
for issue in &report.issues {
if issue.severity == Severity::Error {
eprintln!("[{}] {}", issue.category, issue.message);
}
}
}If you already have an open Reader, use
validate_reader to avoid re-opening
the file.
§Real-world workflows
§1. Process a tilt series
A common cryo-EM workflow: open a tilt series, read the FEI metadata, then iterate over slices:
use mrc::open;
let reader = open("tiltseries.mrc")?;
println!("{}×{}×{} voxels, mode {:?}",
reader.shape().nx, reader.shape().ny, reader.shape().nz,
reader.mode());
// Read FEI extended header metadata via convenience method
if let Some(records) = reader.fei1_metadata() {
for (i, r) in records.iter().enumerate() {
println!("tilt {i}: α={:.1}°, defocus={:.1} µm",
r.alpha_tilt, r.defocus);
}
}
// Process each slice
for slice in reader.convert::<f32>().slices() {
let block = slice?;
// block.data: Vec<f32> — ready for filtering, CTF correction, etc.
}If a file fails to open, try open_permissive
for lenient header handling, or validate_full
to diagnose the issue.
§2. Write a processed map
Always call finalize — without it the header is
stale and density statistics will be wrong (tools display wrong contrast).
use mrc::create;
let mut writer = create("reconstructed.mrc")
.shape([512, 512, 256])
.mode::<f32>()
.finish()?;
for z in 0..256 {
let slice = vec![0.0f32; 512 * 512];
writer.write_block(&mrc::VoxelBlock::new(
[0, 0, z], [512, 512, 1], slice,
)?)?;
}
writer.update_header_stats()?;
writer.finalize()?;§3. Read subtomogram averages from a volume stack
Volume stacks (ISPG 401–630) pack multiple sub-volumes into one file,
each mz slices thick. Use volumes to iterate:
for volume in reader.volumes::<f32>()? {
let vol = volume?;
println!("sub-volume at z={} ({}×{}×{} voxels)",
vol.offset[2], vol.shape[0], vol.shape[1], vol.shape[2]);
}§Troubleshooting
| Error | Likely cause | What to try |
|---|---|---|
InvalidHeader | Not an MRC file, or header corruption | Run mrc validate file.mrc; try open_permissive |
FileSizeMismatch | File truncated or has trailing garbage | Re-download or check mrc validate output |
ModeMismatch | Using slices::<f32>() on an Int16 file | Use convert::<f32>() — auto-converts any mode |
BoundsError | Block outside volume | Check offset + shape against dimensions |
UnsupportedMode | Unrecognized mode, or mode needs the f16 feature | Enable f16 feature or convert with another tool |
Io error | File permissions, filesystem issue | Check the file path and permissions |
| Values look wrong | Endianness mismatch | The endianness fallback handles most cases; try mrc validate |
§Philosophy
This crate does one thing — read and write MRC files. It does no array
arithmetic, image processing, or type conversion beyond MRC-specific
shortcuts (convert::<f32>(), slices_mode0, slices_u8).
Leave those to crates like ndarray, or your own code.
Modules§
- validate
- MRC file validation infrastructure.
Structs§
- Agar
Record - Agard extended header record.
- Ccp4
Record - A single CCP4 symmetry record — an 80-character text line containing space group symmetry operators.
- Convert
Reader - Auto-conversion wrapper returned by
Reader::convert. A reader wrapper that auto-converts all voxel data to typeT. - Fei1
Metadata - Common FEI1 metadata fields.
- Fei2
Metadata - FEI2 metadata extends FEI1 with additional v2 fields.
- Float32
Complex - A complex number with 32-bit float real and imaginary components.
- Header
- Mirror of the 1024-byte MRC-2014 fixed header.
- Header
Builder - Builder for constructing validated MRC headers.
- Imod
Info - IMOD-specific metadata parsed from the
extrablock (bytes 56-63). - Imod
Metadata - IMOD-specific metadata parsed from the main header’s
extrabytes. - Int16
Complex - A complex number with 16-bit signed integer real and imaginary components.
- Mrco
Record - A legacy MRCO extended header record.
- Reader
- Consolidated MRC reader with automatic mmap/buffered backend selection. MRC file reader with automatic backend selection.
- Region
Iter - Lazy iterator over MRC voxel blocks.
Lazy iterator over a volume as a sequence of
VoxelBlocks. - Seri
Record - SerialEM extended header record.
- Slab
Stepper - Stepping strategies for
RegionIter. Stepkcontiguous Z-slices at a time ([nx, ny, k]). - Slice
Stepper - Stepping strategies for
RegionIter. Step one Z-plane at a time ([nx, ny, 1]). - Tile
Stepper - Stepping strategies for
RegionIter. Step arbitrary 3D tiles across a volume. - Volume
Shape - Volume geometry in voxels.
- Voxel
Block - A contiguous chunk of voxel data with a 3D offset and shape.
- Writer
- MRC file writer and its builder. MRC file writer using standard file I/O.
- Writer
Builder - MRC file writer and its builder. Builder for configuring and creating a new MRC file writer.
- f16
- Half-precision floating point type (requires
f16feature). A 16-bit floating point type implementing the IEEE 754-2008 standardbinary16a.k.a “half” format.
Enums§
- Complex
ToReal Strategy - Strategy for converting complex numbers to real values.
- Compression
Level - Compression level for compressed MRC writers.
- Error
- The top-level error type for MRC I/O operations.
- ExtHeader
Data - Parsed extended header data, dispatched by
ExtHeaderType. - ExtHeader
Type - Known extended header types identified by the 4-byte EXTTYP field.
- File
Endian - Endianness of MRC file data. Endianness of MRC file data.
- Header
Validation Error - Errors that can occur during detailed header validation.
- Imod
Image Type - IMOD image type classification from the
idtypefield. - M0Interpretation
- Interpretation of Mode 0 (8-bit) data for legacy files.
- Mode
- MRC data mode defining the on-disk representation of voxel values.
Constants§
- AGAR_
RECORD_ SIZE - Size of a single AGAR record, in bytes.
- CCP4_
RECORD_ SIZE - Size of a single CCP4 symmetry record, in bytes.
- DEFAULT_
MAX_ DECOMPRESSED_ BYTES - Default decompression safety limit for gzip/bzip2 files (256 GiB).
- FEI1_
RECORD_ SIZE - Size of a single FEI1 metadata record, in bytes.
- FEI2_
RECORD_ SIZE - Size of a single FEI2 metadata record, in bytes.
- MRCO_
RECORD_ SIZE - Size of a single MRCO record, in bytes.
- SERI_
RECORD_ SIZE - Size of a single SERI (SerialEM) record, in bytes.
Traits§
- Voxel
- Trait for MRC voxel types with compile-time mode tracking.
Functions§
- convert_
u8_ slice_ to_ u16 - Widen a
u8slice tou16for writing as Mode 6 (Uint16). - convert_
u16_ slice_ to_ u8 - Narrow a
u16slice tou8, returningErrif any value exceeds 255. - create
- Create a new MRC file for writing.
- open
- Open an MRC file for reading, auto-detecting gzip or bzip2 compression.
- parse_
agar_ records - Parse extended header bytes as typed records.
- parse_
ccp4_ records - Parse extended header bytes as typed records.
- parse_
fei1_ records - Parse a raw extended header byte slice as a vector of FEI1 records.
- parse_
fei2_ records - Parse a raw extended header byte slice as a vector of FEI2 records.
- parse_
imod_ metadata - Parse IMOD metadata from the main header’s
extrabytes. - parse_
mrco_ records - Parse extended header bytes as typed records.
- parse_
seri_ records - Parse extended header bytes as typed records.
- read_as
- Read an entire MRC volume into a
Vec<T>with auto-mode detection. - reinterpret_
m0 - Reinterpret Mode 0 (8-bit) data as signed or unsigned and convert to
f32. - write_
as - Write an entire MRC volume from a
&[T]with a single call.