mrc/lib.rs
1//! Read and write MRC-2014 files — the standard in cryo-EM and structural
2//! biology. Handles byte-order, type conversion, and compression so you
3//! can focus on your science. SIMD-accelerated, mmap-enabled.
4//!
5//! # Basic usage
6//!
7//! These APIs are all you need for everyday work. They return typed data
8//! (zero-copy borrowed views) and auto-detect compression, endianness,
9//! and the file's voxel mode.
10//!
11//! ## Quick start
12//!
13//! ```no_run
14//! use mrc::{read_as, write_as};
15//!
16//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
17//! // Read an entire volume in one call
18//! let (header, data): (_, Vec<f32>) = read_as("density.mrc")?;
19//! println!("{}×{}×{} volume, {} voxels",
20//! header.nx, header.ny, header.nz, data.len());
21//!
22//! // Write a volume in one call
23//! write_as("output.mrc", &data, [512, 512, 256])?;
24//! # Ok(()) }
25//! ```
26//!
27//! ## Reading data
28//!
29//! Open any MRC file — compression, byte order, and voxel type are
30//! detected automatically. No need to specify gzip, endianness, or mode.
31//!
32//! ```no_run
33//! # fn main() -> Result<(), mrc::Error> {
34//! use mrc::{open, Reader};
35//!
36//! let reader = open("tilt_series.mrc")?;
37//! println!("{}×{}×{} voxels, mode {:?}",
38//! reader.shape().nx, reader.shape().ny, reader.shape().nz,
39//! reader.mode());
40//! # Ok(()) }
41//! ```
42//!
43//! Then choose an iteration method. Each returns [`DataBlock`] chunks
44//! whose [`DataView`] variant is determined by the file's mode at runtime
45//! — no compile-time guessing, no `ModeMismatch` errors:
46//!
47//! | Method | Returns | Use case |
48//! |---|---|---|
49//! | [`slices`](Reader::slices) | one Z-plane at a time | 2D processing per slice |
50//! | [`slabs`](Reader::slabs) | `k` Z-planes | Batch processing |
51//! | [`tiles`](Reader::tiles) | arbitrary 3D blocks | Local analysis |
52//! | [`subregion`](Reader::subregion) | single block by coordinate | Random access |
53//! | [`read_volume`](Reader::read_volume) | entire volume | Full-volume processing |
54//! | [`volumes`](Reader::volumes) | sub-volumes | Volume stacks (ISPG 401+) |
55//!
56//! ```no_run
57//! # fn main() -> Result<(), mrc::Error> {
58//! # let reader = mrc::Reader::open("density.mrc")?;
59//! for slice in reader.slices() {
60//! let block = slice?;
61//! match block.data() {
62//! mrc::DataView::Float32(data) => { /* process f32 slice */ }
63//! mrc::DataView::Int16(data) => { /* process i16 slice */ }
64//! _ => {} /* other modes handled transparently */
65//! }
66//! }
67//! # Ok(()) }
68//! ```
69//!
70//! Or use [`convert::<f32>()`](Reader::convert) — the **fire-and-forget**
71//! option that reads any mode as `f32`:
72//!
73//! ```no_run
74//! # fn main() -> Result<(), mrc::Error> {
75//! # let reader = mrc::Reader::open("density.mrc")?;
76//! for slice in reader.convert::<f32>().slices() {
77//! let block = slice?;
78//! println!("z={}: {} voxels", block.offset[2], block.data.len());
79//! }
80//! // Full volume in one call:
81//! let block = reader.convert::<f32>().read_volume()?;
82//! # Ok(()) }
83//! ```
84//!
85//! The same iterators — [`slabs`](Reader::slabs),
86//! [`tiles`](Reader::tiles), [`subregion`](Reader::subregion) — work with
87//! `convert::<f32>()` too, as do
88//! [`with_complex_strategy`](crate::ConvertReader::with_complex_strategy)
89//! and [`with_m0_interpretation`](crate::ConvertReader::with_m0_interpretation).
90//!
91//! ## Writing data
92//!
93//! Use [`create()`] to get a [`WriterBuilder`], set shape and mode,
94//! then call [`finish`](WriterBuilder::finish). Write data block by block
95//! — each [`write_data_block`](Writer::write_data_block) encodes and writes
96//! immediately (streaming, no intermediate buffering):
97//!
98//! ```no_run
99//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
100//! use mrc::{create, DataBlock, OwnedData};
101//!
102//! let mut writer = create("output.mrc")
103//! .shape([256, 256, 128])
104//! .mode::<f32>()
105//! .finish()?;
106//!
107//! let block = DataBlock::Owned {
108//! offset: [0, 0, 0],
109//! shape: [256, 256, 1],
110//! data: OwnedData::Float32(vec![0.0f32; 256 * 256]),
111//! };
112//! writer.write_data_block(&block)?;
113//! writer.update_header_stats()?;
114//! writer.finalize()?;
115//! # Ok(()) }
116//! ```
117//!
118//! For a single call, use [`write_as()`] or [`Writer::set_data`]:
119//!
120//! ```no_run
121//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
122//! use mrc::write_as;
123//! let data = vec![0.0f32; 256 * 256 * 128];
124//! write_as("output.mrc", &data, [256, 256, 128])?;
125//! # Ok(()) }
126//! ```
127//!
128//! The writer lifecycle:
129//! 1. Write blocks with [`write_data_block`](Writer::write_data_block)
130//! or [`write_block_as`](Writer::write_block_as) for auto-conversion.
131//! 2. Call [`update_header_stats`](Writer::update_header_stats) to fill
132//! in `dmin`/`dmax`/`dmean`/`rms`.
133//! 3. **Require** [`finalize`](Writer::finalize) — rewrites the header
134//! with final metadata.
135//!
136//! ## Data modes
137//!
138//! MRC files encode voxels in one of several numeric modes.
139//! [`Mode`] represents them at runtime; [`Voxel`] ties each Rust type
140//! to its mode at compile time.
141//!
142//! | Mode | Rust type | Typical use |
143//! |---|---|---|
144//! | [`Int8`](Mode::Int8) (0) | `i8` | Binary masks |
145//! | [`Int16`](Mode::Int16) (1) | `i16` | Raw cryo-EM density |
146//! | [`Float32`](Mode::Float32) (2) | `f32` | Processed / reconstructed density |
147//! | [`Int16Complex`](Mode::Int16Complex) (3) | [`Int16Complex`] | Complex data (i16 real + i16 imag) |
148//! | [`Float32Complex`](Mode::Float32Complex) (4) | [`Float32Complex`] | Complex data (f32 real + f32 imag) |
149//! | [`Uint16`](Mode::Uint16) (6) | `u16` | Segmentation labels |
150//! | [`Float16`](Mode::Float16) (12) | `f16` | Half-precision storage (feature `f16`) |
151//! | [`Packed4Bit`](Mode::Packed4Bit) (101) | `u8` via [`slices_u8`](Reader::slices_u8) | 4-bit packed data |
152//!
153//! When you don't know the mode ahead of time, use
154//! [`convert::<f32>()`](Reader::convert) which converts any mode to `f32`.
155//!
156//! ## Compression auto-detection
157//!
158//! [`Reader::open`] reads the first two bytes to detect compression:
159//!
160//! | Magic bytes | Format |
161//! |---|---|
162//! | `\x1f\x8b` | Gzip |
163//! | `BZ` | Bzip2 |
164//! | anything else | Plain |
165//!
166//! Plain files use mmap (zero-copy) or buffered I/O. Compressed files
167//! are fully decompressed into memory on open (capped at 256 GiB).
168//! Use [`Reader::open_gzip_with_limit`] or
169//! [`Reader::open_bzip2_with_limit`] for a custom limit.
170//!
171//! ## Large files
172//!
173//! [`Reader::open`] automatically uses memory-mapped I/O (requires the
174//! `mmap` feature). Same iterator API, zero-copy [`DataBlock`] views,
175//! OS-managed paging. For very large compressed files, decompress with
176//! `gunzip`/`bunzip2` first, then use plain `Reader::open` for mmap.
177//!
178//! ## Reading from memory / streams
179//!
180//! When data is already in memory (e.g. from a camera readout or network
181//! stream), use [`Reader::from_reader`] or [`Reader::from_bytes`]:
182//!
183//! ```no_run
184//! # fn main() -> Result<(), mrc::Error> {
185//! use mrc::Reader;
186//! use std::io::Cursor;
187//!
188//! let bytes = std::fs::read("density.mrc")?;
189//! let reader = Reader::from_reader(Cursor::new(bytes))?;
190//! # Ok(()) }
191//! ```
192//!
193//! ## Troubleshooting
194//!
195//! | Error | Likely cause | What to try |
196//! |---|---|---|
197//! | [`InvalidHeader`](Error::InvalidHeader) | Not an MRC file | `mrc validate file.mrc` or try [`Reader::open_permissive`] |
198//! | [`FileSizeMismatch`](Error::FileSizeMismatch) | Truncated / trailing garbage | Re-download or run `mrc validate` |
199//! | [`ModeMismatch`](Error::ModeMismatch) | Block type != file mode | Use [`Writer::write_block_as`] for auto-conversion |
200//! | [`BoundsError`](Error::BoundsError) | Block outside volume | Check offset + shape ≤ dimensions |
201//! | [`UnsupportedMode`](Error::UnsupportedMode) | Mode needs `f16` feature | Enable `f16` or convert with another tool |
202//!
203//! ---
204//!
205//! # Advanced API
206//!
207//! These APIs give you lower-level access to raw bytes, headers, extended
208//! metadata, and validation. They are intended for tools, pipelines, and
209//! developers who need more control than the basic iterators provide.
210//!
211//! ## Raw byte access
212//!
213//! ⚠️ **The returned bytes are raw on-disk bytes** — file byte order,
214//! no endian correction, no type conversion. Use [`reader.endian()`](Reader::endian)
215//! and [`reader.mode()`](Reader::mode) to interpret them correctly.
216//!
217//! For typed zero-copy access, use [`subregion`](Reader::subregion) /
218//! [`slices`](Reader::slices) for the basic API, or
219//! [`convert::<f32>()`](Reader::convert) for fire-and-forget conversion.
220//!
221//! | Method | Returns | Copy cost |
222//! |---|---|---|
223//! | [`raw_bytes`](Reader::raw_bytes) | `&[u8]` — whole data region | zero-copy |
224//! | [`read_block_bytes_cow`](Reader::read_block_bytes_cow) | [`Cow<[u8]>`] — sub-block | zero-copy for contiguous XY slabs |
225//! | [`read_block_bytes`](Reader::read_block_bytes) | `Vec<u8>` — sub-block | **always copies** |
226//!
227//! **Contiguous** = offset `[0, y, z]`, shape `[nx, ny, sz]`. Any
228//! sub-XY offset or shape forces a row-by-row gather into owned memory.
229//!
230//! ⚠️ `read_block_bytes` always returns an owned `Vec`, even for
231//! contiguous blocks. For large volumes this causes a full copy of the
232//! block. Use `read_block_bytes_cow` when you want to avoid the
233//! allocation, or use [`subregion`](Reader::subregion) /
234//! [`slices`](Reader::slices) for typed zero-copy access.
235//!
236//! ```no_run
237//! # fn main() -> Result<(), mrc::Error> {
238//! # let reader = mrc::Reader::open("density.mrc")?;
239//! use std::borrow::Cow;
240//!
241//! // Zero-copy: borrows from mmap for contiguous blocks
242//! let cow: Cow<[u8]> = reader.read_block_bytes_cow([0, 0, 0], [256, 256, 64])?;
243//!
244//! // Always owned: allocates + copies even for contiguous blocks
245//! let bytes: Vec<u8> = reader.read_block_bytes([0, 0, 0], [256, 256, 64])?;
246//! # Ok(()) }
247//! ```
248//!
249//! ## Permissive mode / quirky files
250//!
251//! Common microscope quirks (NVERSION left at 0, `"MAP\0"` instead of
252//! `"MAP "`) are handled transparently by [`open()`].
253//!
254//! For severely non-standard files, use
255//! [`Reader::open_permissive`] which turns non-critical header issues
256//! into warnings instead of hard errors, and allows opening files that
257//! are shorter than the header declares. Use [`is_truncated`](Reader::is_truncated)
258//! to detect truncated data:
259//!
260//! ```no_run
261//! # fn main() -> Result<(), mrc::Error> {
262//! let (reader, warnings) = mrc::Reader::open_permissive("legacy.mrc")?;
263//! for w in &warnings { eprintln!("note: {w}"); }
264//! if reader.is_truncated() {
265//! eprintln!("warning: file is incomplete");
266//! }
267//! # Ok(()) }
268//! ```
269//!
270//! Permissive variants [`Reader::from_reader_permissive`] and
271//! [`Reader::from_bytes_permissive`] are also available for in-memory
272//! data.
273//!
274//! ## Special-mode reads (Packed4Bit and Mode 0)
275//!
276//! These modes have no [`Voxel`] implementation — there is no single
277//! Rust type that maps to them safely at compile time — so dedicated
278//! methods read directly as `u8` or `f32`:
279//!
280//! * [`slices_u8`](Reader::slices_u8) / [`slabs_u8`](Reader::slabs_u8) —
281//! unpack `Packed4Bit` nibbles, or narrow Uint16, to `u8`
282//! * [`read_volume_u8`](Reader::read_volume_u8) — full `Packed4Bit`
283//! volume as `u8`
284//! * [`slices_mode0`](Reader::slices_mode0) / [`slabs_mode0`](Reader::slabs_mode0) —
285//! read Mode 0 as `f32`, signed or unsigned
286//!
287//! ## Headers
288//!
289//! The [`Header`] struct mirrors the 1024-byte MRC-2014 fixed header.
290//! Every field is a typed public member.
291//!
292//! ```
293//! use mrc::Header;
294//! let h = Header::new();
295//! assert_eq!(h.map, *b"MAP ");
296//! ```
297//!
298//! For fluent construction, use [`HeaderBuilder`]:
299//!
300//! ```
301//! use mrc::HeaderBuilder;
302//! let header = HeaderBuilder::new()
303//! .shape([512, 512, 256])
304//! .mode::<f32>()
305//! .cell_lengths(1.0, 1.0, 1.0)
306//! .build()?;
307//! # Ok::<_, mrc::HeaderValidationError>(())
308//! ```
309//!
310//! Three validation levels:
311//! * [`validate`](Header::validate) — quick yes / no
312//! * [`validate_detailed`](Header::validate_detailed) — specific diagnosis
313//! * [`validate_permissive`](Header::validate_permissive) — warnings
314//!
315//! Volume type helpers (configure `ispg` and `mz`):
316//!
317//! ```rust
318//! # use mrc::Header;
319//! let mut h = Header::new();
320//! h.nx = 64; h.ny = 64; h.nz = 120;
321//! h.mx = 64; h.my = 64; h.mz = 30;
322//! h.set_volume_stack(30);
323//! assert!(h.is_volume_stack());
324//! assert_eq!(h.logical_shape(), [4, 30, 64, 64]);
325//! ```
326//!
327//! Key computed properties:
328//! [`voxel_size()`](Header::voxel_size), [`sampling()`](Header::sampling),
329//! [`cell_lengths()`](Header::cell_lengths), [`cell_angles()`](Header::cell_angles),
330//! [`cell_volume()`](Header::cell_volume), [`density_stats()`](Header::density_stats),
331//! [`logical_shape()`](Header::logical_shape), [`exttyp()`](Header::exttyp),
332//! [`get_labels()`](Header::get_labels), [`detect_endian()`](Header::detect_endian),
333//! [`is_standard_map()`](Header::is_standard_map), [`decode_from_bytes()`](Header::decode_from_bytes),
334//! [`encode_to_bytes()`](Header::encode_to_bytes).
335//!
336//! ## Extended headers
337//!
338//! Many MRC files carry metadata after the fixed header identified by
339//! a 4-byte `exttyp` field. Use [`parse_extended_header`](Reader::parse_extended_header)
340//! for auto-detection:
341//!
342//! ```no_run
343//! # fn main() -> Result<(), mrc::Error> {
344//! # let reader = mrc::Reader::open("file.mrc")?;
345//! use mrc::ExtHeaderData;
346//!
347//! match reader.parse_extended_header() {
348//! ExtHeaderData::Fei1(records) => {
349//! println!("FEI1 tilt series ({} records)", records.len());
350//! for r in &records {
351//! println!(" tilt {:.1}°, defocus {:.1} µm",
352//! r.alpha_tilt, r.defocus);
353//! }
354//! }
355//! ExtHeaderData::None => println!("No recognized extended header"),
356//! _ => {}
357//! }
358//! # Ok(()) }
359//! ```
360//!
361//! Typed convenience methods: [`fei1_metadata`](Reader::fei1_metadata),
362//! [`fei2_metadata`](Reader::fei2_metadata), [`ccp4_records`](Reader::ccp4_records),
363//! [`mrco_records`](Reader::mrco_records), [`seri_records`](Reader::seri_records),
364//! [`agar_records`](Reader::agar_records), [`imod_metadata`](Reader::imod_metadata).
365//!
366//! ## Validation
367//!
368//! [`validate_full`] checks header, file size, endianness, data
369//! statistics, and NaN/Inf. Returns a [`ValidationReport`]:
370//!
371//! ```no_run
372//! use mrc::{validate_full, Severity};
373//!
374//! let report = mrc::validate_full("protein.mrc", false)?;
375//! if !report.is_valid() {
376//! for issue in &report.issues {
377//! if issue.severity == Severity::Error {
378//! eprintln!("[{}] {}", issue.category, issue.message);
379//! }
380//! }
381//! }
382//! # Ok::<_, Box<dyn std::error::Error>>(())
383//! ```
384//!
385//! Use [`validate_reader`] if the file is already open.
386//!
387//! ## Error types
388//!
389//! Fallible functions return `Result<T, Error>`. Match on variants:
390//!
391//! ```rust
392//! use mrc::Error;
393//!
394//! fn describe(err: &Error) -> &str {
395//! match err {
396//! Error::Io(_) => "I/O failure",
397//! Error::InvalidHeader => "not a valid MRC file",
398//! Error::ModeMismatch { .. } => "wrong voxel type for this file",
399//! Error::BoundsError { .. } => "block outside volume",
400//! Error::FileSizeMismatch { .. } => "file truncated or has trailing data",
401//! _ => "other",
402//! }
403//! }
404//! ```
405//!
406//! [`HeaderValidationError`] gives fine-grained header diagnostics.
407//!
408//! ## Feature flags
409//!
410//! | Feature | Description | Default |
411//! |---------|-------------|---------|
412//! | `mmap` | Memory-mapped readers and writers | ✅ |
413//! | `f16` | Half-precision float via the `half` crate | ✅ |
414//! | `simd` | AVX2 / NEON acceleration | ✅ |
415//! | `parallel` | Parallel decode/convert/encode via rayon | ✅ |
416//! | `gzip` | Gzip-compressed I/O | ✅ |
417//! | `bzip2` | Bzip2-compressed I/O | ❌ |
418//! | `ndarray` | Return volumes as `ndarray::Array3<T>` | ❌ |
419//! | `serde` | Serialize/Deserialize support | ❌ |
420//!
421//! ```no_run
422//! # fn main() -> Result<(), mrc::Error> {
423//! # let reader = mrc::Reader::open("density.mrc")?;
424//! # #[cfg(feature = "ndarray")]
425//! # {
426//! use ndarray::Array3;
427//! let arr: Array3<f32> = reader.convert::<f32>().to_ndarray()?;
428//! println!("{}×{}×{} array", arr.shape()[0], arr.shape()[1], arr.shape()[2]);
429//! # }
430//! # Ok(()) }
431//! ```
432//!
433//! ## Endianness
434//!
435//! MRC files encode byte order via a 4-byte MACHST stamp at file offset 212.
436//! [`FileEndian`] represents the detected byte order:
437//!
438//! ```rust
439//! use mrc::FileEndian;
440//! let stamp: [u8; 4] = [0x44, 0x44, 0x00, 0x00]; // little-endian
441//! assert_eq!(FileEndian::from_machst(&stamp), FileEndian::LittleEndian);
442//! ```
443//!
444//! New files are always little-endian. The crate has a fallback: if the
445//! MODE field is invalid under the detected endianness, the opposite
446//! byte order is tried (handles files with a wrong MACHST stamp).
447//!
448//! ## Philosophy
449//!
450//! This crate does **one thing** — read and write MRC files. It does no
451//! array arithmetic, image processing, or type conversion beyond
452//! MRC-specific shortcuts (`convert::<f32>()`, `slices_mode0`,
453//! `slices_u8`). Leave those to crates like `ndarray`, or your own code.
454
455#![cfg_attr(
456 not(test),
457 deny(clippy::unwrap_used, clippy::expect_used, clippy::perf)
458)]
459#![warn(missing_docs, clippy::cargo)]
460
461mod engine;
462mod error;
463mod header;
464mod io;
465mod iter;
466mod mode;
467mod validate;
468
469#[cfg(feature = "serde")]
470mod serde_byte_array;
471
472// Re-export core types
473pub use engine::block::{VolumeShape, VoxelBlock};
474/// Endianness of MRC file data.
475pub use engine::endian::FileEndian;
476
477// Re-export MRC-specific format utilities
478pub use engine::convert::{convert_u8_slice_to_u16, convert_u16_slice_to_u8, reinterpret_m0};
479
480pub use error::{Error, HeaderValidationError};
481pub use header::{
482 AGAR_RECORD_SIZE, AgarRecord, CCP4_RECORD_SIZE, Ccp4Record, ExtHeaderData, ExtHeaderType,
483 FEI1_RECORD_SIZE, FEI2_RECORD_SIZE, Fei1Metadata, Fei2Metadata, Header, HeaderBuilder,
484 ImodImageType, ImodInfo, ImodMetadata, MRCO_RECORD_SIZE, MrcoRecord, SERI_RECORD_SIZE,
485 SeriRecord, parse_agar_records, parse_ccp4_records, parse_fei1_records, parse_fei2_records,
486 parse_imod_metadata, parse_mrco_records, parse_seri_records,
487};
488
489pub use mode::{
490 ComplexToRealStrategy, DataBlock, DataView, Float32Complex, Int16Complex, M0Interpretation,
491 Mode, OwnedData, Voxel,
492};
493
494/// Half-precision floating point type (requires `f16` feature).
495#[cfg(feature = "f16")]
496pub use half::f16;
497/// Consolidated MRC reader with automatic mmap/buffered backend selection.
498pub use io::reader::Reader;
499
500/// Auto-conversion wrapper returned by [`Reader::convert`].
501pub use io::reader_common::ConvertReader;
502
503/// MRC file writer and its builder.
504pub use io::writer::{Writer, WriterBuilder};
505
506/// Compression level for compressed MRC writers.
507///
508/// See [`WriterBuilder::compression`] for usage.
509pub use io::writer::CompressionLevel;
510
511/// Default decompression safety limit for gzip/bzip2 files (256 GiB).
512///
513/// Applied before the header is parsed, preventing decompression bombs.
514/// Override via [`Reader::open_gzip_with_limit`] or
515/// [`Reader::open_bzip2_with_limit`].
516pub use io::reader_common::DEFAULT_MAX_DECOMPRESSED_BYTES;
517
518#[doc(hidden)]
519pub use engine::codec::{decode_into, swap_bytes_in_place};
520
521#[doc(hidden)]
522pub use io::reader::{CompressionType, detect_compression};
523
524/// Validation report types and comprehensive file checker.
525pub use validate::{Severity, ValidationIssue, ValidationReport, validate_full, validate_reader};
526
527/// Open an MRC file for reading, auto-detecting gzip or bzip2 compression.
528///
529/// This is a convenience wrapper around [`Reader::open`].
530/// Common microscope quirks (NVERSION left at 0, `"MAP\0"` instead of `"MAP "`)
531/// are handled transparently — no special flags needed.
532///
533/// For compressed files, decompression is capped at
534/// [`DEFAULT_MAX_DECOMPRESSED_BYTES`] (256 GiB) to prevent bombs.
535/// Use [`Reader::open_gzip_with_limit`] or [`Reader::open_bzip2_with_limit`]
536/// to set a custom limit.
537///
538/// For permissive mode (returns `(Reader, Vec<String>)` instead of
539/// `Reader`), or compressed-file-specific openers,
540/// use [`Reader::open_permissive`], [`Reader::open_gzip`], etc. directly.
541pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Reader, Error> {
542 Reader::open(path)
543}
544
545/// Create a new MRC file for writing.
546///
547/// Returns a [`WriterBuilder`] that must be configured with at least
548/// [`shape`](WriterBuilder::shape) and [`mode`](WriterBuilder::mode)
549/// before calling [`finish`](WriterBuilder::finish) to open the file.
550///
551/// # Example
552/// ```no_run
553/// use mrc::create;
554///
555/// let mut writer = create("output.mrc")
556/// .shape([256, 256, 128])
557/// .mode::<f32>()
558/// .finish()?;
559/// # Ok::<_, Box<dyn std::error::Error>>(())
560/// ```
561pub fn create<P: AsRef<std::path::Path>>(path: P) -> WriterBuilder {
562 WriterBuilder::new(path)
563}
564
565/// Read an entire MRC volume into a `Vec<T>` with auto-mode detection.
566///
567/// This is a one-shot convenience over manually opening a [`Reader`] and
568/// calling [`convert::<T>()`](Reader::convert) then [`read_volume`](Reader::read_volume).
569/// The file can be in any MRC mode — the data is auto-converted to `T`.
570///
571/// Returns the parsed [`Header`] and the voxel data.
572///
573/// # Examples
574///
575/// ```no_run
576/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
577/// use mrc::read_as;
578/// let (header, data): (_, Vec<f32>) = read_as("density.mrc")?;
579/// println!("{}×{}×{} volume, {} voxels",
580/// header.nx, header.ny, header.nz, data.len());
581/// # Ok(()) }
582/// ```
583pub fn read_as<T: Voxel + crate::engine::convert::ConvertFrom<f32>, P: AsRef<std::path::Path>>(
584 path: P,
585) -> Result<(Header, Vec<T>), Error> {
586 let reader = Reader::open(path)?;
587 let header = *reader.header();
588 let volume = reader.convert::<T>().read_volume()?;
589 Ok((header, volume.data))
590}
591
592/// Write an entire MRC volume from a `&[T]` with a single call.
593///
594/// Creates the file, writes the data, computes density statistics,
595/// and finalizes — all in one step. The type `T` determines the
596/// file's MRC mode (e.g. `f32` → Mode 2, `i16` → Mode 1).
597///
598/// # Examples
599///
600/// ```no_run
601/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
602/// use mrc::write_as;
603/// let data = vec![0.0f32; 64 * 64 * 32];
604/// write_as("output.mrc", &data, [64, 64, 32])?;
605/// # Ok(()) }
606/// ```
607pub fn write_as<T: Voxel, P: AsRef<std::path::Path>>(
608 path: P,
609 data: &[T],
610 shape: [usize; 3],
611) -> Result<(), Error> {
612 let mut writer = WriterBuilder::new(path).shape(shape).mode::<T>().finish()?;
613 writer.set_data(data)?;
614 writer.finalize()?;
615 Ok(())
616}