Type-safe MRC-2014 reader/writer for Rust — SIMD-accelerated, mmap-enabled, with full cryo-EM metadata support.
A type-safe Rust encoder/decoder for MRC files — the standard format in cryo-electron microscopy and structural biology. Automatically handles endianness, type conversion, and compression with SIMD acceleration, exposing a powerful yet intuitive and friendly read/write API so you can focus on your data.
One line to read any MRC file. One line to write one.
use mrc::{read_as, write_as};
// Read — auto-detects gzip/bzip2, handles quirky headers
let (header, data): (_, Vec<f32>) = read_as("density.mrc")?;
println!("{}×{}×{} = {} voxels",
header.nx, header.ny, header.nz, data.len());
// Write — type-safe, single call
write_as("output.mrc", &data, [512, 512, 256])?;The mrc API is designed so that common operations are one-liners and complex workflows read naturally.
| What you want | How you write it |
|---|---|
| Open any MRC file (plain / gzip / bzip2) | Reader::open("file.mrc")? |
| One-shot read (open + read_volume) | let (h, d): (_, Vec<f32>) = read_as("file.mrc")?; |
| One-shot write (create + write + finalize) | write_as("out.mrc", &data, [512, 512, 256])?; |
Read the whole volume as f32 |
reader.convert::<f32>().read_volume()? |
| Read a sub-region | reader.subregion::<f32>([x, y, z], [sx, sy, sz])? |
| Iterate Z-slices | reader.slices::<f32>() → for slice in ... |
| Iterate sub-volumes in a stack | reader.volumes::<f32>()? → for vol in ... |
| Create a new file | create("out.mrc").shape([512, 512, 256]).mode::<f32>().finish()? |
| Write with auto-conversion (f32 → i16) | writer.write_block_as(&f32_block)? |
| Parse tilt-series metadata | reader.fei1_metadata() or reader.parse_extended_header() |
| Validate a file | validate_full("file.mrc", false)? |
| Open a quirky file | Reader::open_permissive("broken.mrc")? |
| Zero-copy slab access | reader.slab_as::<f32>(z, k)? — mmap or buffered |
No trait imports required. Every one of these is an inherent method — no use SomeTrait needed.
[dependencies]
mrc = "0.5"Enable optional features in Cargo.toml:
mrc = { version = "0.5", features = ["ndarray", "serde", "bzip2"] }For the mrc-cli binary, install the companion crate:
cargo install mrc-cli| Feature | Default | What it adds |
|---|---|---|
mmap |
✅ | Memory-mapped I/O (auto-selected for large files) |
f16 |
✅ | Half-precision float (half::f16) support |
simd |
✅ | AVX2/NEON acceleration |
parallel |
✅ | Parallel encoding via rayon |
gzip |
✅ | Gzip auto-detection and compressed writer |
bzip2 |
❌ | Bzip2 auto-detection and compressed writer |
ndarray |
❌ | Return volumes as ndarray::Array3<T> via to_ndarray() |
serde |
❌ | Serialize/Deserialize for all public types |
See docs.rs/mrc for the full API documentation, runnable examples, and detailed guidance. The examples below are just a few highlights.
use mrc::Reader;
// Open — auto-detects compression and byte order
let reader = Reader::open("tiltseries.mrc")?;
println!("{}×{}×{} voxels, mode {:?}",
reader.shape().nx, reader.shape().ny, reader.shape().nz,
reader.mode());
// Iterate — slices, slabs, tiles, or subregion
for slice in reader.slices::<f32>() { // one Z-plane
let block = slice?; // VoxelBlock<f32>
}
for slab in reader.slabs::<f32>(16) { // 16 planes at a time
let block = slab?;
}
for tile in reader.tiles::<f32>([64, 64, 64])? { // 3D tiles
let block = tile?;
}
// Full volume in one call
let volume = reader.read_volume::<f32>()?;
println!("{} voxels", volume.data.len());
// Any sub-region by coordinate
let patch = reader.subregion::<f32>([10, 10, 5], [32, 32, 8])?;Don't care whether the file is Int8, Int16, Uint16, Float16, or even Packed4Bit? Use convert::<f32>() and the crate handles the rest.
// Read any file mode as f32 — Int16, Uint16, Float16, even Packed4Bit
for slice in reader.convert::<f32>().slices() {
let block: mrc::VoxelBlock<f32> = slice?;
println!("slice {}: mean = {:.2}",
block.offset[2],
block.data.iter().sum::<f32>() / block.data.len() as f32);
}
// Or read the whole converted volume in one call
let block = reader.convert::<f32>().read_volume()?;
// The same converter also supports slabs, tiles, subregion,
// with_complex_strategy, with_m0_interpretation, and to_ndarray().use mrc::create;
// Create a Float32 file
let mut writer = create("output.mrc")
.shape([512, 512, 256])
.mode::<f32>()
.finish()?;
// Write one slice at a time
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,
)?)?;
}
// Or write with auto-conversion: f32 data → i16 file
writer.write_block_as(&mrc::VoxelBlock::new(
[0, 0, 0], [512, 512, 1],
vec![0.0f32; 512 * 512],
)?)?;
// Write in parallel (requires `parallel` feature)
writer.write_block_parallel(&mrc::VoxelBlock::new(
[0, 0, 0], [512, 512, 256], vec![0.0f32; 512 * 512 * 256],
)?)?;
writer.update_header_stats()?; // fills dmin/dmax/dmean/rms
writer.finalize()?; // **required** — rewrites headeruse mrc::{create, CompressionLevel};
// Gzip-compressed output — same API, just finish_gzip()
let mut writer = create("output.mrc.gz")
.shape([256, 256, 128])
.mode::<f32>()
.compression(CompressionLevel::Best)
.finish_gzip()?;
writer.write_block(&mrc::VoxelBlock::new(
[0, 0, 0], [256, 256, 128], vec![0.0f32; 256 * 256 * 128],
)?)?;
writer.finalize()?; // compresses & writes to diskFiles too large for RAM? Reader::open automatically uses memory-mapped I/O (requires mmap feature). The OS pages data on demand.
let reader = Reader::open("huge_volume.mrc")?;
// Zero-copy typed access to Z-planes (mmap or buffered)
let slab: &[f32] = reader.slab_as::<f32>(0, 1)?; // no allocation
println!("first plane has {} voxels", slab.len());use mrc::ExtHeaderData;
// Auto-detect and parse whatever extended header the file has
match reader.parse_extended_header() {
ExtHeaderData::Fei1(records) => {
println!("FEI1 tilt series ({} images)", records.len());
println!("first: tilt {:.1}°, defocus {:.1}µm",
records[0].alpha_tilt, records[0].defocus);
}
ExtHeaderData::Fei2(records) => {
println!("FEI2 — {} records", records.len());
}
ExtHeaderData::Ccp4(records) => {
println!("CCP4 symmetry — {} records", records.len());
}
ExtHeaderData::Seri(records) => {
println!("SerialEM — first tilt {:.1}°", records[0].alpha_tilt);
}
ExtHeaderData::None => println!("No extended header"),
_ => {}
}
// Or use typed convenience methods directly
if let Some(records) = reader.fei1_metadata() {
println!("{} FEI1 records", records.len());
}
if let Some(imod) = reader.imod_metadata() {
println!("IMOD: {:?}, tilt increment {:.1}°",
imod.image_type, imod.tilt_increment);
}Volume stacks (ISPG 401–630) pack multiple sub-volumes in one file.
for result in reader.volumes::<f32>()? {
let vol = result?;
println!("sub-volume at z={}: {}×{}×{}",
vol.offset[2], vol.shape[0], vol.shape[1], vol.shape[2]);
}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);
}
}
}Common microscope quirks (NVERSION left at 0, "MAP\0" instead of "MAP ") are handled transparently by open(). For truly broken files, permissive mode turns non-critical errors into warnings:
let (reader, warnings) = Reader::open_permissive("legacy.mrc")?;
if reader.is_truncated() {
eprintln!("warning: file is incomplete");
}
for w in &warnings { eprintln!("note: {w}"); }use mrc::{open, create, VoxelBlock};
// 1. Open a tilt series from any microscope format
let reader = open("tiltseries.mrc")?;
println!("{}×{}×{}, mode {:?}",
reader.shape().nx, reader.shape().ny, reader.shape().nz,
reader.mode());
// 2. Read FEI metadata (or CCP4, SerialEM, Agard...)
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);
}
}
// 3. Process each slice as f32 (auto-converts from any mode)
for slice in reader.convert::<f32>().slices() {
let block = slice?;
// block.data: Vec<f32> — ready for filtering, CTF, alignment
}
// 4. Write the reconstructed volume
let mut writer = create("reconstructed.mrc")
.shape([512, 512, 256])
.mode::<f32>()
.finish()?;
writer.write_block(&VoxelBlock::new(
[0, 0, 0], [512, 512, 256], processed_data,
)?)?;
writer.update_header_stats()?;
writer.finalize()?;The mrc-cli crate provides the mrc-cli
command-line tool with subcommands for inspection, validation, conversion,
PNG/GIF export, and resampling.
cargo install mrc-cli
mrc-cli info protein.mrc
mrc-cli header density.mrc
mrc-cli validate tiltseries.mrc
mrc-cli stats protein.mrc
mrc-cli invert input.mrc output.mrc
mrc-cli convert input.mrc output.mrc --mode i16
mrc-cli slice volume.mrc -z 42 -o slice.mrc
mrc-cli crop volume.mrc -o roi.mrc --x 100 --y 100 --z 50 -s 128,128,64
mrc-cli unstack tiltseries.mrc -o frame
mrc-cli rescale volume.mrc output.mrc --down 2
mrc-cli png volume.mrc -z 0 -o slice.png
mrc-cli movie volume.mrc -o movie.gif --pingpongSee the mrc-cli crate on crates.io for the
full command reference and examples.
| Resource | What you'll find |
|---|---|
| docs.rs/mrc | Complete API reference with runnable examples on every method |
| APIs.md | Local API surface overview (offline-friendly) |
| mrc-cli on crates.io | CLI binary reference and examples |
| roadmap.md | Release history and planned features |
| AGENTS.md | Code organization & conventions for contributors |
| mrcfile-official.md | The MRC-2014 specification |
| update.md | Per-release changelogs |
- CCP-EM for the MRC-2014 specification
- EMDB for providing real-world test data
- The cryo-EM community for invaluable feedback
Contributions are welcome — whatever your skill level.
This crate is built by and for the cryo-EM community. Whether you're fixing a typo, adding a test, implementing a new feature, or just asking a question, your input makes the project better.
- Report bugs — open an issue with steps to reproduce
- Request features — what format feature or workflow is missing from your pipeline?
- Submit PRs — see AGENTS.md for code organization and conventions
- Improve docs — better examples, clearer explanations, fix typos
- Share real files — MRC files with unusual extended headers or edge cases help us test
All contributions are subject to the MIT License.
Format specs come and go, but cryo-EM data is forever — make yours readable by the next generation of tools.
MIT — see the LICENSE file.