mrc/lib.rs
1//! Read and write MRC-2014 files — the standard in cryo-EM and structural
2//! biology.
3//!
4//! This crate handles file I/O, byte-order detection, and type-safe data
5//! access so you can focus on your science. It's fast (SIMD, parallel
6//! encoding) and works with plain, gzip, and bzip2 files out of the box.
7//!
8//! See the [README](https://github.com/elemeng/mrc#readme) for installation
9//! instructions, CLI tools, and the project roadmap.
10//!
11//! # Quick example
12//!
13//! ```no_run
14//! use mrc::{open, create, VoxelBlock};
15//!
16//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
17//! // Read — auto-detects gzip/bzip2 compression
18//! let reader = open("density.mrc")?;
19//! for slice in reader.convert::<f32>().slices() {
20//! let _block = slice?; // VoxelBlock<f32>
21//! }
22//!
23//! // Write
24//! let mut writer = create("output.mrc")
25//! .shape([512, 512, 256])
26//! .mode::<f32>()
27//! .finish()?;
28//! writer.write_block(&VoxelBlock::new(
29//! [0, 0, 0], [512, 512, 1],
30//! vec![0.0f32; 512 * 512],
31//! )?)?;
32//! writer.finalize()?;
33//! # Ok(()) }
34//! ```
35//!
36//! # Reading files
37//!
38//! Open any MRC file with [`open()`] or [`Reader::open`]. Compression is
39//! detected from magic bytes — no need to hint gzip or bzip2.
40//!
41//! ```no_run
42//! # fn main() -> Result<(), mrc::Error> {
43//! use mrc::Reader;
44//! let reader = Reader::open("tilt_series.mrc")?;
45//! println!("{}×{}×{} voxels, mode {:?}",
46//! reader.shape().nx, reader.shape().ny, reader.shape().nz,
47//! reader.mode());
48//! # Ok(()) }
49//! ```
50//!
51//! Then pick an iteration method:
52//!
53//! * [`slices`](ReaderMethods::slices) — one Z-plane at a time
54//! * [`slabs`](ReaderMethods::slabs) — batches of `k` Z-planes
55//! * [`tiles`](ReaderMethods::tiles) — arbitrary 3D blocks
56//! * [`subregion`](ReaderMethods::subregion) — a single block by coordinate
57//!
58//! > **Trait imports (optional):** Iterator and conversion methods are
59//! > available as inherent methods on `Reader` and `MmapReader` without any
60//! > import. The [`ReaderMethods`] and [`ConvertMethods`] traits are also
61//! > re-exported for advanced use (e.g. generic code over reader types).
62//!
63//! For automatic mode conversion, use [`convert`](ConvertMethods::convert):
64//!
65//! ```no_run
66//! # fn main() -> Result<(), mrc::Error> {
67//! # let reader = mrc::Reader::open("density.mrc")?;
68//! for slice in reader.convert::<f32>().slices() {
69//! let block = slice?;
70//! println!("slice {} mean density: {:.2}",
71//! block.offset[2],
72//! block.data.iter().sum::<f32>() / block.data.len() as f32);
73//! }
74//! # Ok(()) }
75//! ```
76//!
77//! Or read the full volume in one call:
78//!
79//! ```no_run
80//! # fn main() -> Result<(), mrc::Error> {
81//! # let reader = mrc::Reader::open("density.mrc")?;
82//! let block = reader.convert::<f32>().read_volume()?;
83//! println!("read {} voxels", block.data.len());
84//! # Ok(()) }
85//! ```
86//!
87//! When the `ndarray` feature is enabled, get numpy-like multidimensional access:
88//!
89//! ```no_run
90//! # #[cfg(feature = "ndarray")] {
91//! # fn main() -> Result<(), mrc::Error> {
92//! # let reader = mrc::Reader::open("density.mrc")?;
93//! let arr = reader.to_ndarray::<f32>()?;
94//! // arr is ndarray::Array3<f32> with shape [nz, ny, nx]
95//! let center = arr[[arr.shape()[0] / 2, arr.shape()[1] / 2, arr.shape()[2] / 2]];
96//! # Ok(()) }
97//! # }
98//! ```
99//!
100//! ### Large files
101//!
102//! When the file does not fit in RAM, use [`MmapReader`] (requires the
103//! `mmap` feature). Same iterator API, zero-copy [`slab_as`](MmapReader::slab_as),
104//! OS-managed paging.
105//!
106//! ### Quirky files
107//!
108//! Common microscope quirks (NVERSION left at 0, `"MAP\0"` instead of `"MAP "`)
109//! are handled transparently by [`open()`] — no special flags needed.
110//!
111//! For esoteric or severely non-standard files, use
112//! [`Reader::open_permissive`] which turns non-critical header issues into
113//! warnings instead of hard errors:
114//!
115//! ```no_run
116//! # fn main() -> Result<(), mrc::Error> {
117//! # use mrc::Reader;
118//! let (reader, warnings) = Reader::open_permissive("legacy.mrc")?;
119//! for w in &warnings { eprintln!("note: {w}"); }
120//! # Ok(()) }
121//! ```
122//!
123//! # Writing files
124//!
125//! Use [`create()`] to get a [`WriterBuilder`], set the shape and voxel type,
126//! then call [`finish`](WriterBuilder::finish).
127//!
128//! ```no_run
129//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
130//! use mrc::create;
131//! let mut writer = create("output.mrc")
132//! .shape([256, 256, 128])
133//! .mode::<f32>()
134//! .finish()?;
135//! # Ok(()) }
136//! ```
137//!
138//! The lifecycle:
139//!
140//! 1. **Write** blocks with [`write_block`](Writer::write_block). The type
141//! `T` matches the file's mode — a compile-time check that prevents
142//! accidentally treating bytes as the wrong kind of number.
143//! Use [`write_block_as`](Writer::write_block_as) for automatic conversion
144//! (e.g. write `f32` data to an Int16 or Float16 file).
145//! 2. Optionally call [`update_header_stats`](Writer::update_header_stats)
146//! to fill in `dmin`/`dmax`/`dmean`/`rms`.
147//! 3. **Finalize** with [`finalize`](Writer::finalize) to rewrite the header
148//! with final metadata. **Required** — without it the header is stale.
149//!
150//! Four backends through the same builder:
151//!
152//! | Backend | Builder method | Best for |
153//! |---|---|---|
154//! | [`Writer`] | [`finish()`](WriterBuilder::finish) | General use, writes straight to disk |
155//! | [`MmapWriter`] | [`finish_mmap()`](WriterBuilder::finish_mmap) | Very large files (`mmap` feature) |
156//! | [`GzipWriter`] | [`finish_gzip()`](WriterBuilder::finish_gzip) | Compressed output (`gzip` feature) |
157//! | [`Bzip2Writer`] | [`finish_bzip2()`](WriterBuilder::finish_bzip2) | Compressed output (`bzip2` feature) |
158//!
159//! # Data modes
160//!
161//! MRC files encode voxels in one of several numeric modes. [`Mode`]
162//! represents them at runtime; [`Voxel`] ties each Rust type to its mode
163//! at compile time, catching mismatches before any data flows.
164//!
165//! | Mode | Rust type | Typical use |
166//! |---|---|---|
167//! | [`Int8`](Mode::Int8) (0) | `i8` | Binary masks |
168//! | [`Int16`](Mode::Int16) (1) | `i16` | Raw cryo-EM density |
169//! | [`Float32`](Mode::Float32) (2) | `f32` | Processed / reconstructed density |
170//! | [`Int16Complex`](Mode::Int16Complex) (3) | [`Int16Complex`] | Complex data (i16 real + i16 imag) |
171//! | [`Float32Complex`](Mode::Float32Complex) (4) | [`Float32Complex`] | Complex data (f32 real + f32 imag) |
172//! | [`Uint16`](Mode::Uint16) (6) | `u16` | Segmentation labels |
173//! | [`Float16`](Mode::Float16) (12) | `f16` | Half-precision storage (feature `f16`) |
174//! | [`Packed4Bit`](Mode::Packed4Bit) (101) | `u8` via [`slices_u8`](ReaderMethods::slices_u8) | 4-bit packed data; no `Voxel` impl |
175//!
176//! Packed 4-bit data is handled transparently by the unified API:
177//! [`convert::<f32>()`](ConvertMethods::convert) unpacks nibbles to `f32`,
178//! [`slices_u8`](ReaderMethods::slices_u8) / [`slabs_u8`](ReaderMethods::slabs_u8) unpack
179//! to `u8` (0–15), and [`write_u4_block`](Writer::write_u4_block) packs
180//! `u8` values back.
181//!
182//! When you don't know the mode ahead of time, use
183//! [`convert::<f32>()`](ConvertMethods::convert) which converts any mode to `f32`.
184//!
185//! # Headers
186//!
187//! The [`Header`] struct mirrors the 1024-byte MRC-2014 fixed header.
188//! Every field is a typed public member — dimensions, cell parameters,
189//! axis mapping, density statistics, text labels, and more.
190//!
191//! ```
192//! use mrc::Header;
193//! let h = Header::new();
194//! assert_eq!(h.map, *b"MAP ");
195//! ```
196//!
197//! For fluent construction with validation, use [`HeaderBuilder`]:
198//!
199//! ```
200//! use mrc::HeaderBuilder;
201//! let header = HeaderBuilder::new()
202//! .shape([512, 512, 256])
203//! .mode::<f32>()
204//! .build()?;
205//! # Ok::<_, mrc::HeaderValidationError>(())
206//! ```
207//!
208//! Three validation levels:
209//!
210//! * [`validate`](Header::validate) — quick yes / no
211//! * [`validate_detailed`](Header::validate_detailed) — tells you exactly
212//! what is wrong via [`HeaderValidationError`]
213//! * [`validate_permissive`](Header::validate_permissive) — warnings for
214//! non-critical issues
215//!
216//! ### Convenience API
217//!
218//! The [`Header`] provides computed properties for common queries:
219//!
220//! ```rust
221//! use mrc::Header;
222//! let h = Header::new();
223//! let vol = h.cell_volume(); // unit cell volume in ų
224//! let (dmin, dmax, dmean, rms) = h.density_stats();
225//! let sampling = h.sampling(); // [mx, my, mz]
226//! let label = h.label_at(0); // first label, or None
227//! assert!(h.is_standard_map()); // MAP field is "MAP "
228//! ```
229//!
230//! ### Manual header parsing
231//!
232//! Decode a raw 1024-byte header block with automatic endianness detection:
233//!
234//! ```rust
235//! use mrc::Header;
236//! let raw = [0u8; 1024];
237//! // ... fill raw bytes from file ...
238//! let header = Header::decode_from_bytes(&raw);
239//! ```
240//!
241//! When the MACHST byte-order stamp is wrong (common in some EPU files),
242//! the decoder tries the opposite endianness automatically:
243//!
244//! ```rust
245//! # use mrc::Header;
246//! # let raw = [0u8; 1024];
247//! let (header, warning) = Header::decode_from_bytes_with_info(&raw);
248//! if let Some(w) = warning {
249//! eprintln!("byte-order fallback used: {w}");
250//! }
251//! ```
252//!
253//! # Extended headers
254//!
255//! Many MRC files carry additional metadata after the 1024-byte fixed header
256//! in an **extended header** region. The type is identified by the 4-byte
257//! `exttyp` field in the header's `extra[8..12]`.
258//!
259//! The [`ExtHeaderType`] enum identifies the format without parsing:
260//!
261//! ```rust
262//! use mrc::{Header, ExtHeaderType};
263//! let header = Header::new();
264//! match ExtHeaderType::from_header(&header) {
265//! ExtHeaderType::Fei1 => println!("FEI Type 1"),
266//! ExtHeaderType::Ccp4 => println!("CCP4"),
267//! ExtHeaderType::Unknown(id) => {
268//! println!("Unknown: {:?}", std::str::from_utf8(&id));
269//! }
270//! _ => {}
271//! }
272//! ```
273//!
274//! Instead of calling individual parser functions, use the auto-dispatch
275//! method on any open reader:
276//!
277//! ```no_run
278//! # fn main() -> Result<(), mrc::Error> {
279//! # let reader = mrc::Reader::open("file.mrc")?;
280//! use mrc::ExtHeaderData;
281//!
282//! match reader.parse_extended_header() {
283//! ExtHeaderData::Fei1(records) => {
284//! println!("FEI1 tilt series ({} records)", records.len());
285//! for r in &records {
286//! println!(" tilt {:.1}°, defocus {:.1} µm",
287//! r.alpha_tilt, r.defocus);
288//! }
289//! }
290//! ExtHeaderData::Ccp4(records) => {
291//! println!("CCP4 symmetry ({} records)", records.len());
292//! }
293//! ExtHeaderData::Seri(records) => {
294//! println!(" first tilt: {:.1}°", records[0].alpha_tilt);
295//! }
296//! ExtHeaderData::None => println!("No recognised extended header"),
297//! _ => {}
298//! }
299//! # Ok(()) }
300//! ```
301//!
302//! Typed convenience methods give direct access without pattern matching:
303//!
304//! ```no_run
305//! # fn main() -> Result<(), mrc::Error> {
306//! # let reader = mrc::Reader::open("file.mrc")?;
307//! if let Some(records) = reader.fei1_metadata() {
308//! println!("{} FEI1 records", records.len());
309//! }
310//! if let Some(imod) = reader.imod_metadata() {
311//! println!("IMOD type {:?}, tilt increment {:.1}°",
312//! imod.image_type, imod.tilt_increment);
313//! }
314//! # Ok(()) }
315//! ```
316//!
317//! Available: [`fei1_metadata`](crate::Reader::fei1_metadata),
318//! [`fei2_metadata`](crate::Reader::fei2_metadata),
319//! [`ccp4_records`](crate::Reader::ccp4_records),
320//! [`mrco_records`](crate::Reader::mrco_records),
321//! [`seri_records`](crate::Reader::seri_records),
322//! [`agar_records`](crate::Reader::agar_records),
323//! [`imod_metadata`](crate::Reader::imod_metadata).
324//!
325//! # Feature flags
326//!
327//! | Feature | Description | Default |
328//! |---------|-------------|---------|
329//! | `mmap` | Memory-mapped readers and writers | ✅ |
330//! | `f16` | Half-precision float via the `half` crate | ✅ |
331//! | `simd` | AVX2 / NEON acceleration for integer→f32, f16↔f32, byte-swap, stats | ✅ |
332//! | `parallel` | Parallel encoding via `rayon` | ✅ |
333//! | `gzip` | Gzip-compressed I/O | ✅ |
334//! | `bzip2` | Bzip2-compressed I/O | ❌ |
335//! | `ndarray` | Return volumes as `ndarray::Array3<T>` via `to_ndarray()` | ❌ |
336//! | `serde` | Serialize/Deserialize support via `serde` | ❌ |
337//!
338//! # Advanced topics
339//!
340//! ## Error handling
341//!
342//! Fallible functions return `Result<T, Error>`. The errors you will
343//! actually hit in practice:
344//!
345//! * [`Io`](Error::Io) — the file could not be read or written
346//! * [`InvalidHeader`](Error::InvalidHeader) — not a valid MRC file
347//! * [`ModeMismatch`](Error::ModeMismatch) — calling `slices::<f32>()` on
348//! an Int16 file; use `convert::<f32>()` instead
349//! * [`BoundsError`](Error::BoundsError) — read or write outside the volume
350//! * [`FileSizeMismatch`](Error::FileSizeMismatch) — file truncated or
351//! has trailing garbage
352//!
353//! [`HeaderValidationError`] gives fine-grained diagnostics for header
354//! problems (bad dimensions, wrong MAP field, invalid NVERSION ...).
355//!
356//! ## Endianness
357//!
358//! MRC files encode byte order via a 4-byte MACHST stamp. [`FileEndian`]
359//! handles detection and conversion automatically. New files are always
360//! little-endian, matching modern hardware and the Python `mrcfile` library.
361//!
362//! The crate has a fallback: if the MODE field is invalid under the detected
363//! endianness, the opposite byte order is tried. This handles files with a
364//! wrong MACHST stamp but correct data.
365//!
366//! ## Compression auto-detection
367//!
368//! [`Reader::open`] reads the first two bytes of the file:
369//!
370//! | Magic bytes | Format |
371//! |---|---|
372//! | `\x1f\x8b` | Gzip |
373//! | `BZ` | Bzip2 |
374//! | anything else | Plain |
375//!
376//! Plain MRC files are memory-mapped or buffered directly. Compressed files
377//! are fully decompressed into memory on open, with a hard cap of
378//! [`DEFAULT_MAX_DECOMPRESSED_BYTES`] (256 GiB) to prevent bombs.
379//! Use [`Reader::open_gzip_with_limit`] or
380//! [`Reader::open_bzip2_with_limit`] for a custom limit.
381//!
382//! > **Large compressed files:** If the uncompressed data exceeds available RAM,
383//! > decompress with `gunzip` or `bunzip2` first, then use [`MmapReader`] for
384//! > zero-copy access — the OS pages data on demand without loading the whole
385//! > file into memory.
386//!
387//! ## File validation
388//!
389//! [`validate_full`](validate::validate_full) runs comprehensive checks
390//! on a file — header, size, endianness, data statistics (1 % tolerance),
391//! and NaN / Inf scanning. Returns a
392//! [`ValidationReport`](validate::ValidationReport) with categorised issues.
393//!
394//! If you already have an open [`Reader`], use
395//! [`validate_reader`](validate::validate_reader) to avoid re-opening
396//! the file.
397//!
398//! # Real-world workflows
399//!
400//! ## 1. Process a tilt series
401//!
402//! A common cryo-EM workflow: open a tilt series, read the FEI metadata,
403//! then iterate over slices:
404//!
405//! ```no_run
406//! # fn main() -> Result<(), mrc::Error> {
407//! use mrc::{open, parse_fei1_records};
408//!
409//! let reader = open("tiltseries.mrc")?;
410//! println!("{}×{}×{} voxels, mode {:?}",
411//! reader.shape().nx, reader.shape().ny, reader.shape().nz,
412//! reader.mode());
413//!
414//! // Read FEI extended header metadata
415//! if let Some(records) = parse_fei1_records(reader.ext_header_bytes()) {
416//! for (i, r) in records.iter().enumerate() {
417//! println!("tilt {i}: α={:.1}°, defocus={:.1} µm",
418//! r.alpha_tilt, r.defocus);
419//! }
420//! }
421//!
422//! // Process each slice
423//! for slice in reader.convert::<f32>().slices() {
424//! let block = slice?;
425//! // block.data: Vec<f32> — ready for filtering, CTF correction, etc.
426//! }
427//! # Ok(()) }
428//! ```
429//!
430//! If a file fails to open, try [`open_permissive`](Reader::open_permissive)
431//! for lenient header handling, or [`validate_full`](validate::validate_full)
432//! to diagnose the issue.
433//!
434//! ## 2. Write a processed map
435//!
436//! Always call [`finalize`](Writer::finalize) — without it the header is
437//! stale and density statistics will be wrong (tools display wrong contrast).
438//!
439//! ```no_run
440//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
441//! use mrc::create;
442//!
443//! let mut writer = create("reconstructed.mrc")
444//! .shape([512, 512, 256])
445//! .mode::<f32>()
446//! .finish()?;
447//!
448//! for z in 0..256 {
449//! let slice = vec![0.0f32; 512 * 512];
450//! writer.write_block(&mrc::VoxelBlock::new(
451//! [0, 0, z], [512, 512, 1], slice,
452//! )?)?;
453//! }
454//!
455//! writer.update_header_stats()?;
456//! writer.finalize()?;
457//! # Ok(()) }
458//! ```
459//!
460//! ## 3. Read subtomogram averages from a volume stack
461//!
462//! Volume stacks (ISPG 401–630) pack multiple sub-volumes into one file,
463//! each `mz` slices thick. Use [`volumes`](ReaderMethods::volumes) to iterate:
464//!
465//! ```no_run
466//! # fn main() -> Result<(), mrc::Error> {
467//! # let reader = mrc::Reader::open("averages.mrc")?;
468//! for volume in reader.volumes::<f32>()? {
469//! let vol = volume?;
470//! println!("sub-volume at z={} ({}×{}×{} voxels)",
471//! vol.offset[2], vol.shape[0], vol.shape[1], vol.shape[2]);
472//! }
473//! # Ok(()) }
474//! ```
475//!
476//! # Troubleshooting
477//!
478//! | Error | Likely cause | What to try |
479//! |---|---|---|
480//! | [`InvalidHeader`](Error::InvalidHeader) | Not an MRC file, or header corruption | Run `mrc-validate file.mrc`; try [`open_permissive`](Reader::open_permissive) |
481//! | [`FileSizeMismatch`](Error::FileSizeMismatch) | File truncated or has trailing garbage | Re-download or check `mrc-validate` output |
482//! | [`ModeMismatch`](Error::ModeMismatch) | Using `slices::<f32>()` on an Int16 file | Use [`convert::<f32>()`](ConvertMethods::convert) — auto-converts any mode |
483//! | [`BoundsError`](Error::BoundsError) | Block outside volume | Check offset + shape against dimensions |
484//! | [`UnsupportedMode`](Error::UnsupportedMode) | Unrecognised mode, or mode needs the `f16` feature | Enable `f16` feature or convert with another tool |
485//! | `Io` error | File permissions, filesystem issue | Check the file path and permissions |
486//! | Values look wrong | Endianness mismatch | The endianness fallback handles most cases; try `mrc-validate` |
487//!
488//! # Philosophy
489//!
490//! This crate does **one thing** — read and write MRC files. It does no array
491//! arithmetic, image processing, or type conversion beyond MRC-specific
492//! shortcuts (`convert::<f32>()`, `slices_mode0`, `slices_u8`).
493//! Leave those to crates like `ndarray`, or your own code.
494
495#![cfg_attr(
496 not(test),
497 deny(clippy::unwrap_used, clippy::expect_used, clippy::perf)
498)]
499#![warn(missing_docs, clippy::cargo)]
500
501mod engine;
502mod error;
503mod header;
504mod io;
505mod iter;
506mod mode;
507pub mod validate;
508
509#[cfg(feature = "serde")]
510mod serde_byte_array;
511
512// Re-export core types
513pub use engine::block::{VolumeShape, VoxelBlock};
514/// Endianness of MRC file data.
515pub use engine::endian::FileEndian;
516
517// Re-export MRC-specific format utilities
518pub use engine::convert::{convert_u8_slice_to_u16, convert_u16_slice_to_u8, reinterpret_m0};
519
520pub use error::{Error, HeaderValidationError};
521pub use header::{
522 AGAR_RECORD_SIZE, AgarRecord, CCP4_RECORD_SIZE, Ccp4Record, ExtHeaderData, ExtHeaderType,
523 FEI1_RECORD_SIZE, FEI2_RECORD_SIZE, Fei1Metadata, Fei2Metadata, Header, HeaderBuilder,
524 ImodImageType, ImodInfo, ImodMetadata, MRCO_RECORD_SIZE, MrcoRecord, SERI_RECORD_SIZE,
525 SeriRecord, parse_agar_records, parse_ccp4_records, parse_fei1_records, parse_fei2_records,
526 parse_imod_metadata, parse_mrco_records, parse_seri_records,
527};
528
529pub use mode::{
530 ComplexToRealStrategy, Float32Complex, Int16Complex, M0Interpretation, Mode, Voxel,
531};
532
533/// Half-precision floating point type (requires `f16` feature).
534#[cfg(feature = "f16")]
535pub use half::f16;
536/// Buffered MRC reader with lazy slice/slab iterators.
537pub use io::buffered::Reader;
538
539/// MRC file writer and its builder.
540pub use io::writer::{Writer, WriterBuilder};
541/// Lazy iterator over MRC voxel blocks.
542pub use iter::RegionIter;
543/// Stepping strategies for [`RegionIter`].
544pub use iter::{SlabStepper, SliceStepper, TileStepper};
545
546/// Memory-mapped MRC writer (requires `mmap` feature).
547#[cfg(feature = "mmap")]
548pub use io::writer::MmapWriter;
549
550/// Memory-mapped MRC reader (requires `mmap` feature).
551#[cfg(feature = "mmap")]
552pub use io::mmap_reader::MmapReader;
553
554/// Gzip-compressed MRC writer (requires `gzip` feature).
555#[cfg(feature = "gzip")]
556pub use io::gzip::GzipWriter;
557
558/// Bzip2-compressed MRC writer (requires `bzip2` feature).
559#[cfg(feature = "bzip2")]
560pub use io::bzip2::Bzip2Writer;
561
562/// Default decompression safety limit for gzip/bzip2 files (256 GiB).
563///
564/// Applied before the header is parsed, preventing decompression bombs.
565/// Override via [`Reader::open_gzip_with_limit`] or
566/// [`Reader::open_bzip2_with_limit`].
567pub use io::reader_common::DEFAULT_MAX_DECOMPRESSED_BYTES;
568
569/// Universal iterator / read API for all MRC reader types.
570///
571/// Import this trait to call `slices`, `slabs`, `tiles`, `subregion`,
572/// `read_volume`, `slices_u8`, `slabs_u8`, `slices_mode0`, `slabs_mode0`,
573/// `read_volume_u8`, `volumes`, and `to_ndarray` on any reader.
574pub use io::reader_common::ReaderMethods;
575
576/// Auto-conversion API for MRC readers.
577///
578/// Import this trait to call `.convert::<T>()` on any reader, enabling
579/// automatic mode conversion (e.g. Int16 → f32).
580pub use io::reader_common::ConvertMethods;
581
582#[doc(hidden)]
583pub use io::reader::{CompressionType, detect_compression};
584
585/// Open an MRC file for reading, auto-detecting gzip or bzip2 compression.
586///
587/// This is a convenience wrapper around [`Reader::open`].
588/// Common microscope quirks (NVERSION left at 0, `"MAP\0"` instead of `"MAP "`)
589/// are handled transparently — no special flags needed.
590///
591/// For compressed files, decompression is capped at
592/// [`DEFAULT_MAX_DECOMPRESSED_BYTES`] (256 GiB) to prevent bombs.
593/// Use [`Reader::open_gzip_with_limit`] or [`Reader::open_bzip2_with_limit`]
594/// to set a custom limit.
595///
596/// For permissive mode or compressed-file-specific openers,
597/// use [`Reader::open_permissive`], [`Reader::open_gzip`], etc. directly.
598pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Reader, Error> {
599 Reader::open(path)
600}
601
602/// Create a new MRC file for writing.
603///
604/// Returns a [`WriterBuilder`] that must be configured with at least
605/// [`shape`](WriterBuilder::shape) and [`mode`](WriterBuilder::mode)
606/// before calling [`finish`](WriterBuilder::finish) to open the file.
607///
608/// # Example
609/// ```no_run
610/// use mrc::create;
611///
612/// let mut writer = create("output.mrc")
613/// .shape([256, 256, 128])
614/// .mode::<f32>()
615/// .finish()?;
616/// # Ok::<_, Box<dyn std::error::Error>>(())
617/// ```
618pub fn create<P: AsRef<std::path::Path>>(path: P) -> WriterBuilder {
619 WriterBuilder::new(path)
620}