imodfile 0.2.2

A pure-Rust IMOD model file decoder/encoder — binary & ASCII, with Python bindings
Documentation
use std::io::{BufReader, BufWriter};

use imodfile::{read_ascii, write_ascii, write_binary, Imod, ImodError, ImodResult};

fn main() {
    let args: Vec<String> = std::env::args().collect();

    if args.len() < 2 || args.contains(&"--help".to_string()) || args.contains(&"-h".to_string())
    {
        print_usage();
        return;
    }

    let input_path = &args[1];
    let output_path = args.get(2).filter(|p| !p.starts_with("--"));
    let to_ascii = args.contains(&"--ascii".to_string())
        || args.contains(&"--to-ascii".to_string());
    let is_ascii_input = args.contains(&"--ascii-in".to_string());
    let dump_points = args.contains(&"--points".to_string());

    // Read the model
    let model = match read_model(input_path, is_ascii_input) {
        Ok(m) => m,
        Err(e) => {
            eprintln!("Error: {}", e);
            std::process::exit(1);
        }
    };

    // Determine actions
    if let Some(out_path) = output_path {
        if dump_points {
            // Write points to output file
            match std::fs::File::create(out_path) {
                Ok(file) => {
                    let mut writer = BufWriter::new(file);
                    if let Err(e) = model.write_points(&mut writer) {
                        eprintln!("Error writing: {}", e);
                        std::process::exit(1);
                    }
                }
                Err(e) => {
                    eprintln!("Error creating {}: {}", out_path, e);
                    std::process::exit(1);
                }
            }
            let total: i32 = model.obj.iter().map(|o| o.cont.iter().map(|c| c.psize).sum::<i32>()).sum();
            eprintln!("Wrote {} points", total);
        } else {
            // Convert: write to output file
            let want_ascii = to_ascii
                || out_path.ends_with(".txt")
                || out_path.ends_with(".ascii");
            match write_model(out_path, &model, want_ascii) {
                Ok(()) => {
                    eprintln!(
                        "Wrote {} with {} objects, {} views",
                        out_path,
                        model.obj.len(),
                        model.view.len()
                    );
                }
                Err(e) => {
                    eprintln!("Error writing: {}", e);
                    std::process::exit(1);
                }
            }
        }
    } else if dump_points {
        // Print points to stdout
        let pts = model.to_points();
        if pts.is_empty() {
            eprintln!("(no points)");
        } else {
            print!("{}", pts);
        }
    } else {
        // No output file: print summary
        print_summary(&model);
    }
}

fn print_usage() {
    eprintln!(
        r#"imodfile — IMOD model file decoder/encoder

USAGE:
  imodfile <input.mod>                     # dump model info
  imodfile <input.mod> <output.mod>        # copy/convert
  imodfile <input.mod> --ascii             # write as ASCII
  imodfile <input.mod> output.txt          # write as ASCII (.txt/.ascii)
  imodfile <input.mod> output.txt --points # save point coordinates to file
  imodfile <input.mod> --ascii-in          # read as ASCII (if extension not .txt)
  imodfile <input.mod> --points            # dump all point coordinates to stdout

FLAGS:
  --ascii, --to-ascii    write output in ASCII format
  --ascii-in             force reading input as ASCII
  --points               print all points (one per line, empty row between contours)
  -h, --help             print this help
"#
    );
}

fn read_model(path: &str, force_ascii: bool) -> ImodResult<Imod> {
    if force_ascii {
        let file = std::fs::File::open(path).map_err(|e| {
            ImodError::Io(std::io::Error::new(e.kind(), format!("cannot open {}: {}", path, e)))
        })?;
        let mut reader = BufReader::new(file);
        return read_ascii(&mut reader);
    }
    Imod::load(path)
}

fn write_model(path: &str, model: &Imod, ascii: bool) -> ImodResult<()> {
    if ascii {
        let file = std::fs::File::create(path).map_err(|e| {
            ImodError::Io(std::io::Error::new(e.kind(), format!("cannot create {}: {}", path, e)))
        })?;
        let mut writer = BufWriter::new(file);
        write_ascii(&mut writer, model)
    } else {
        let mut file = std::fs::File::create(path).map_err(|e| {
            ImodError::Io(std::io::Error::new(e.kind(), format!("cannot create {}: {}", path, e)))
        })?;
        write_binary(&mut file, model)
    }
}

fn print_summary(model: &Imod) {
    println!("IMOD Model: {}", model.name);
    println!("  Objects: {}", model.obj.len());
    println!("  Views:   {}", model.view.len());
    println!(
        "  Size:    {} x {} x {}",
        model.xmax, model.ymax, model.zmax
    );
    println!(
        "  Offset:  {} {} {}",
        model.xoffset, model.yoffset, model.zoffset
    );
    println!(
        "  Scale:   {} {} {}",
        model.xscale, model.yscale, model.zscale
    );
    println!("  Pixsize: {}", model.pixsize);
    println!(
        "  Angles:  {} {} {}",
        model.alpha, model.beta, model.gamma
    );
    println!("  Flags:   0x{:08x}", model.flags);
    println!("  Slicer angles: {}", model.slicer_angles.len());
    if let Some(ref ref_img) = model.ref_image {
        println!("  Ref image: present");
        println!(
            "    cscale: {} {} {}",
            ref_img.cscale.x, ref_img.cscale.y, ref_img.cscale.z
        );
    }

    for (i, obj) in model.obj.iter().enumerate() {
        println!();
        println!("  Object {}: {}", i, obj.name);
        println!(
            "    Contours: {}  Meshes: {}  Surfsize: {}",
            obj.contsize, obj.meshsize, obj.surfsize
        );
        println!(
            "    Color: {} {} {}  trans={}",
            obj.red, obj.green, obj.blue, obj.trans
        );
        println!(
            "    drawmode={}  linewidth={}  pdrawsize={}",
            obj.drawmode, obj.linewidth, obj.pdrawsize
        );
        println!("    Flags: 0x{:08x}", obj.flags);

        // Count total points
        let total_pts: i32 = obj.cont.iter().map(|c| c.psize).sum();
        println!("    Total points: {}", total_pts);

        // Show first few contours
        let show = obj.cont.len().min(5);
        for c in &obj.cont[..show] {
            let first_z = c.pts.first().map_or(0.0, |p| p.z);
            let last_z = c.pts.last().map_or(0.0, |p| p.z);
            println!(
                "      Contour {} pts: flags=0x{:x} time={} surf={} z_range={}..{}",
                c.psize, c.flags, c.time, c.surf, first_z, last_z,
            );
        }
        if obj.cont.len() > 5 {
            println!("      ... and {} more contours", obj.cont.len() - 5);
        }

        // Show meshes
        for (mi, m) in obj.mesh.iter().enumerate() {
            println!(
                "      Mesh {}: {} verts, {} indices, flag=0x{:x}",
                mi, m.vsize, m.lsize, m.flag
            );
        }
    }

    // Views
    if !model.view.is_empty() {
        println!();
        println!("  Views:");
        for (i, v) in model.view.iter().enumerate() {
            println!(
                "    View {}: fovy={} label='{}'",
                i, v.fovy, v.label
            );
        }
    }
}