signinum-cli 1.0.2

Command-line tool for signinum: inspect
// SPDX-License-Identifier: Apache-2.0

use std::io::{self, Read};
use std::path::Path;
use std::process::ExitCode;

fn main() -> ExitCode {
    let mut args = std::env::args().skip(1);
    let subcommand = args.next();
    match subcommand.as_deref() {
        Some("inspect") => {
            let path = match args.next() {
                Some(p) => p,
                None => {
                    eprintln!("usage: signinum inspect <file>");
                    return ExitCode::from(2);
                }
            };
            inspect(Path::new(&path))
        }
        Some("--help") | Some("-h") | Some("help") | None => {
            eprintln!("signinum {}", env!("CARGO_PKG_VERSION"));
            eprintln!("Usage:");
            eprintln!(
                "  signinum inspect <file>    Parse JPEG or JPEG 2000 headers and print Info"
            );
            ExitCode::SUCCESS
        }
        Some(other) => {
            eprintln!("unknown subcommand: {other}");
            ExitCode::from(2)
        }
    }
}

fn inspect(path: &Path) -> ExitCode {
    let bytes = match read_file(path) {
        Ok(b) => b,
        Err(e) => {
            eprintln!("error reading {}: {e}", path.display());
            return ExitCode::from(1);
        }
    };
    match inspect_bytes(&bytes) {
        Ok(line) => {
            println!("{line}");
            ExitCode::SUCCESS
        }
        Err(message) => {
            eprintln!("{message}");
            ExitCode::from(1)
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InspectFormat {
    Jpeg,
    J2k,
}

fn detect_inspect_format(bytes: &[u8]) -> InspectFormat {
    if bytes.starts_with(&[0, 0, 0, 12, b'j', b'P', b' ', b' ']) || bytes.starts_with(&[0xFF, 0x4F])
    {
        InspectFormat::J2k
    } else {
        InspectFormat::Jpeg
    }
}

fn inspect_bytes(bytes: &[u8]) -> Result<String, String> {
    match detect_inspect_format(bytes) {
        InspectFormat::Jpeg => match signinum_jpeg::Decoder::inspect(bytes) {
            Ok(info) => Ok(format!(
                "{}×{} {:?} {:?} bit={} samp={:?} mcu={}x{} units={}x{} rst={:?} scans={}",
                info.dimensions.0,
                info.dimensions.1,
                info.sof_kind,
                info.color_space,
                info.bit_depth,
                info.sampling.components(),
                info.mcu_geometry.width,
                info.mcu_geometry.height,
                info.mcu_geometry.columns,
                info.mcu_geometry.rows,
                info.restart_interval,
                info.scan_count,
            )),
            Err(e) => {
                let mut message = format!("error: {e}");
                if e.is_unsupported() {
                    message.push_str(
                        "\nhint: this file is not supported by signinum; try jpeg-decoder or openjpeg",
                    );
                }
                Err(message)
            }
        },
        InspectFormat::J2k => match signinum_j2k::J2kDecoder::inspect(bytes) {
            Ok(info) => Ok(format!(
                "{}×{} {:?} bit={} comps={} levels={} tiles={:?}",
                info.dimensions.0,
                info.dimensions.1,
                info.colorspace,
                info.bit_depth,
                info.components,
                info.resolution_levels,
                info.tile_layout,
            )),
            Err(e) => Err(format!("error: {e}")),
        },
    }
}

fn read_file(path: &Path) -> io::Result<Vec<u8>> {
    let mut file = std::fs::File::open(path)?;
    let mut buf = Vec::new();
    file.read_to_end(&mut buf)?;
    Ok(buf)
}

#[cfg(test)]
mod tests {
    use super::{detect_inspect_format, inspect_bytes, InspectFormat};

    fn minimal_j2k_codestream() -> Vec<u8> {
        let mut bytes = vec![0xFF, 0x4F];
        let mut siz = Vec::new();
        push_u16(&mut siz, 0);
        push_u32(&mut siz, 128);
        push_u32(&mut siz, 64);
        push_u32(&mut siz, 0);
        push_u32(&mut siz, 0);
        push_u32(&mut siz, 64);
        push_u32(&mut siz, 64);
        push_u32(&mut siz, 0);
        push_u32(&mut siz, 0);
        push_u16(&mut siz, 3);
        for _ in 0..3 {
            siz.extend_from_slice(&[0x07, 0x01, 0x01]);
        }
        bytes.extend_from_slice(&[0xFF, 0x51]);
        push_u16(&mut bytes, (siz.len() + 2) as u16);
        bytes.extend_from_slice(&siz);

        let cod = [0x00, 0x00, 0x00, 0x01, 0x01, 0x05, 0x04, 0x04, 0x00, 0x01];
        bytes.extend_from_slice(&[0xFF, 0x52]);
        push_u16(&mut bytes, (cod.len() + 2) as u16);
        bytes.extend_from_slice(&cod);
        bytes.extend_from_slice(&[0xFF, 0x90, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
        bytes
    }

    fn minimal_jp2() -> Vec<u8> {
        let codestream = minimal_j2k_codestream();
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&[0, 0, 0, 12, b'j', b'P', b' ', b' ', 0x0D, 0x0A, 0x87, 0x0A]);
        bytes.extend_from_slice(&[
            0, 0, 0, 20, b'f', b't', b'y', b'p', b'j', b'p', b'2', b' ', 0, 0, 0, 0, b'j', b'p',
            b'2', b' ',
        ]);
        bytes.extend_from_slice(&[
            0, 0, 0, 45, b'j', b'p', b'2', b'h', 0, 0, 0, 22, b'i', b'h', b'd', b'r', 0, 0, 0, 64,
            0, 0, 0, 128, 0, 3, 7, 7, 0, 0, 0, 0, 0, 15, b'c', b'o', b'l', b'r', 1, 0, 0, 0, 0, 0,
            16,
        ]);
        let len = (8 + codestream.len()) as u32;
        bytes.extend_from_slice(&len.to_be_bytes());
        bytes.extend_from_slice(b"jp2c");
        bytes.extend_from_slice(&codestream);
        bytes
    }

    fn push_u16(out: &mut Vec<u8>, value: u16) {
        out.extend_from_slice(&value.to_be_bytes());
    }

    fn push_u32(out: &mut Vec<u8>, value: u32) {
        out.extend_from_slice(&value.to_be_bytes());
    }

    #[test]
    fn detects_j2k_codestream_magic() {
        assert_eq!(
            detect_inspect_format(&minimal_j2k_codestream()),
            InspectFormat::J2k
        );
    }

    #[test]
    fn detects_jp2_magic() {
        assert_eq!(detect_inspect_format(&minimal_jp2()), InspectFormat::J2k);
    }

    #[test]
    fn inspect_bytes_dispatches_to_j2k() {
        let line = inspect_bytes(&minimal_jp2()).expect("jp2 inspect");
        assert!(line.contains("128×64"));
        assert!(line.contains("levels=6"));
    }
}