# imodfile
**Decode, encode, and inspect IMOD model files — from Rust and Python.**
Read and write `.mod` files used by the [IMOD](https://bio3d.colorado.edu/imod/)
electron microscopy toolkit, in both binary and ASCII formats.
<p>
<a href="https://crates.io/crates/imodfile"><img src="https://img.shields.io/crates/v/imodfile.svg" alt="Crates.io"></a>
<a href="https://docs.rs/imodfile"><img src="https://img.shields.io/docsrs/imodfile" alt="Documentation"></a>
<a href="https://github.com/elemeng/imodfile/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT licensed"></a>
<a href="https://github.com/elemeng/imodfile/actions"><img src="https://img.shields.io/github/actions/workflow/status/elemeng/imodfile/ci.yml?branch=main" alt="CI"></a>
</p>
---
## Install
### Rust crate
```bash
cargo add imodfile
```
### Python package
```bash
pip install git+https://github.com/elemeng/imodfile.git
# Or build from source:
pip install maturin
maturin build --features python
pip install target/wheels/imodfile-*.whl
```
### CLI tool
```bash
cargo install imodfile
```
## Quick start (Rust)
```rust,no_run
use imodfile::Imod;
let model = Imod::load("cells.mod").unwrap();
println!("{} objects, {} views", model.obj.len(), model.view.len());
// Iterate over every point with indices
for (oi, ci, pi, pt) in model.points() {
println!("[obj {oi}][cont {ci}][pt {pi}] {} {} {}", pt.x, pt.y, pt.z);
}
// Dump all points as text
model.save_points("points.txt").unwrap();
// Save back as binary or ASCII (chosen by extension)
model.save("output.mod").unwrap();
model.save("output.txt").unwrap();
```
## Quick start (Python)
```python
import imodfile
import numpy as np
model = imodfile.load("cells.mod")
print(model.name, model.image_size)
for obj in model.objects:
for cont in obj.contours:
pts = cont.points # (N, 3) float32 array
xs, ys, zs = pts[:, 0], pts[:, 1], pts[:, 2]
# Create new objects and contours
obj = model.add_object()
obj.name = "mitochondria"
obj.color = (1.0, 0.0, 0.0)
cont = obj.add_contour()
cont.points = np.array([[0,0,0],[1,0,0],[0,1,0]], dtype=np.float32)
model.save("output.mod")
model.save("output.txt") # ASCII format
model.save_points("points.txt") # point dump
```
## Testing
```bash
# Full test suite (Rust + Python)
bash tests/test.sh
# Rust tests only
cargo test
# Python tests only (requires built wheel)
python -m pytest tests/test_python.py -v
# Property-based tests (randomized model generation)
cargo test --test proptests
# Fuzz the binary parser (runs indefinitely)
cargo fuzz run fuzz_binary
```
The test suite covers:
| Rust integration | 13 tests — binary/ASCII round-trip, labels, point sizes, clip planes, views, edge cases, real-file round-trip |
| Property-based | 2 tests — randomized model generation verifies write→read consistency and field invariants |
| Doc-tests | 9 tests — API documentation examples that compile and run |
| Raw byte verification | Binary writer magic, tags, field layout, IEOF marker |
| Python bindings | 15 tests — load, read, create, modify, save, error handling, round-trip |
| Fuzz testing | `cargo-fuzz` target for the binary parser (crash-resistant input handling) |
The CI pipeline (GitHub Actions) runs all Rust tests, clippy (zero-warnings enforced), Python
bindings build + pytest, and documentation build on every push and pull request.
## Data model
```text
Imod (top-level model)
├── name, flags, pixsize, units
├── obj: Vec<Iobj>
│ ├── Iobj (object)
│ │ ├── name, color, flags, drawmode
│ │ ├── cont: Vec<Icont>
│ │ │ └── Icont (contour)
│ │ │ ├── pts: Vec<Ipoint> ← 3-D coordinates
│ │ │ ├── sizes, flags, time, surf
│ │ │ └── label, store
│ │ ├── mesh: Vec<Imesh>
│ │ │ └── Imesh (mesh)
│ │ │ ├── vert: Vec<Ipoint>
│ │ │ ├── list: Vec<i32> (indices + sentinels)
│ │ │ └── flag, time, surf
│ │ ├── clips: IclipPlanes
│ │ ├── label, mesh_param
│ │ └── store: Istore
│ └── ...
├── view: Vec<Iview> (camera positions)
├── slicer_angles: Vec<SlicerAngles>
├── ref_image: Option<IrefImage>
└── store: Istore
Ipoint { x: f32, y: f32, z: f32 }
```
## Key types
**Most users primarily work with `Imod` (model), `Iobj` (object), `Icont` (contour),
and `Ipoint` (3-D coordinate).** All types, flag constants, and reader/writer
functions are re-exported at the crate root for convenience, and also available
under their respective modules for qualified access.
| [`Imod`] / `Model` | Top-level model — image dimensions, transforms, views, objects |
| [`Iobj`] / `Object` | A named object with colour, material, contours, and meshes |
| [`Icont`] / `Contour` | A contour — open or closed series of [`Ipoint`]s |
| [`Imesh`] / `Mesh` | Triangle/quad mesh with vertex and index lists |
| [`Ipoint`] | Single 3-D coordinate `{ x, y, z }` |
For secondary types (`Iview`, `IclipPlanes`, `Ilabel`, `Istore`, `ImeshParams`,
`IrefImage`, `SlicerAngles`, flag constants, and the low-level
`read_binary` / `write_binary` / `read_ascii` / `write_ascii` functions),
import from `imodfile::model` or `imodfile::binary` / `imodfile::ascii` as needed.
## Rust API
| Load a file | `Imod::load(path)` — auto-detects binary vs ASCII |
| Save a file | `model.save(path)` — binary, or ASCII if path ends with `.txt` |
| Iterate points | `model.points()` → `(obj, cont, pt, &Ipoint)` |
| Format as text | `model.to_points()` → `String` |
| Write to writer | `model.write_points(&mut writer)` |
| Low-level read | `read_binary(&mut reader)` / `read_ascii(&mut reader)` |
| Low-level write | `write_binary(&mut writer, &model)` / `write_ascii(...)` |
## Python API
| `imodfile.load(path)` → `Model` | Load a file (auto-detect format) |
| `model.save(path)` | Save (binary, or ASCII if `.txt`) |
| `model.objects` → `list[Object]` | All objects |
| `obj.contours` → `list[Contour]` | All contours in an object |
| `cont.points` → `ndarray(N, 3)` | Point coordinates (read/write) |
| `cont.surface`, `cont.time`, `cont.flags` | Contour metadata (read/write) |
| `obj.add_contour()` → `Contour` | Create a new contour |
| `model.add_object()` → `Object` | Create a new object |
The `.pyi` stub is bundled in the wheel for IDE autocomplete.
## CLI
```text
imodfile <input.mod> # dump model info
imodfile <input.mod> <output.mod> # copy / convert
imodfile <input.mod> output.txt # convert to ASCII
imodfile <input.mod> output.txt --points # extract point coordinates
imodfile <input.mod> --points # print points to stdout
```
## Format support
| Binary (big-endian) | ✓ | ✓ |
| ASCII text | ✓ | ✓ |
| Legacy V0.1 | ✓ | — |
All chunk types are supported: objects, contours, meshes, materials, clip planes,
labels, point sizes, views, slicer angles, reference transforms, meshing
parameters, and general-purpose storage.
The binary parser is fuzz-tested via `cargo-fuzz` for crash-resistant handling of
arbitrary or corrupted input. Property-based tests with `proptest` verify write→read
round-trip correctness across randomly generated models.
## Read/write flow
```text
┌──────────────────┐
│ imodfile crate │
└─────────┬────────┘
│
┌─────────────┴─────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Imod::load() │ │ Imod::save() │
│ / read() │ │ / write() │
└───────┬───────┘ └───────┬───────┘
│ │
┌───────┴───────┐ ┌───────┴───────┐
│ detect │ │ path ends │
│ format │ │ with .txt? │
└───────┬───────┘ └───────┬───────┘
│ │
┌───────┴───────┐ ┌───────┴───────┐
▼ ▼ ▼ ▼
┌──────┐ ┌──────────┐ ┌──────────┐ ┌──────┐
│binary│ │ ASCII │ │ ASCII │ │binary│
│reader│ │ reader │ │ writer │ │writer│
└──────┘ └──────────┘ └──────────┘ └──────┘
│ │ │ │
▼ ▼ ▼ ▼
.mod file .txt file .txt file .mod file
(binary) (ASCII) (ASCII) (binary)
```
## Modules
| [`model`] | Data types — `Imod`, `Iobj`, `Icont`, `Imesh`, `Ipoint`, ... |
| [`binary`] | Binary format reader and writer |
| [`ascii`] | ASCII format reader and writer |
| [`chunk_ids`] | 4-byte magic constants |
| [`error`] | `ImodError` / `ImodResult` |
| [`store`] | General-purpose storage helpers |
## License
MIT — see [LICENSE](https://github.com/elemeng/imodfile/blob/main/LICENSE) for the full text.