//! Binary entry point for `ssocr-rs`.
//!
//! This module parses command-line arguments, executes image processing pipelines,
//! detects digit regions in the processed image, and decodes the seven-segment displays
//! using the configured character set.

mod charset;
mod defines;
mod help;
mod imgproc;

use charset::{init_charset, parse_charset, print_digit};
use defines::{
    Charset, D_DECIMAL,
    D_MINUS, D_ONE, D_UNKNOWN, DARK, DEBUG_IMAGE_NAME, DEC_H_RATIO, DEC_W_RATIO, DigitStruct, Direction, FIND_DARK, FIND_LIGHT, HORIZ_DOWN, HORIZ_MID, HORIZ_UP,
    IGNORE_PIXELS, LIGHT, LuminanceType, MIN_CHAR_H, MIN_CHAR_W, MIN_SEGMENT, MINUS_RATIO, NEED_PIXELS, NUMBER_OF_DIGITS, ONE_RATIO, PROG, SPC_FAC, SSOCR_BLACK,
    SSOCR_DEFAULT_BACKGROUND, SSOCR_DEFAULT_FOREGROUND, SSOCR_WHITE, THRESHOLD, UNKNOWN,
    VERT_LEFT_DOWN, VERT_LEFT_UP, VERT_RIGHT_DOWN, VERT_RIGHT_UP,
};
use imgproc::{
    adapt_threshold, closing, crop, dilation, dynamic_threshold, erosion, get_lum,
    get_minmaxval, gray_stretch, grayscale, invert, is_pixel_set, keep_pixels_filter, make_mono,
    mirror, opening, remove_isolated, rotate, set_pixels_filter, shear, white_border,
};

use anyhow::Result;
use image::{Rgba, RgbaImage};
use std::io::Read;

/// Global configuration options parsed from command-line arguments.
struct Config {
    /// Global threshold value (in percent).
    thresh: f64,
    /// Number of active pixels required to count a segment.
    need_pixels: i32,
    /// Minimum dimension (width/height) of a segment.
    min_segment: i32,
    /// Minimum width of a character bounding box.
    min_char_w: i32,
    /// Minimum height of a character bounding box.
    min_char_h: i32,
    /// Number of border pixels ignored when segmenting characters.
    ignore_pixels: i32,
    /// Minimum expected number of digits.
    expected_digits_min: i32,
    /// Maximum expected number of digits.
    expected_digits_max: i32,
    /// Height/width ratio threshold to recognize a '1'.
    one_ratio: i32,
    /// Width/height ratio threshold to recognize a minus sign.
    minus_ratio: i32,
    /// Maximum digit-height to decimal-height ratio to recognize a decimal point.
    dec_h_ratio: i32,
    /// Maximum digit-width to decimal-width ratio to recognize a decimal point.
    dec_w_ratio: i32,
    /// Space factor factor used to calculate space insertion.
    spc_fac: f64,
    /// Path to save the output processed image.
    output_file: Option<String>,
    /// Desired file format for the output processed image.
    output_fmt: Option<String>,
    /// Path to save the intermediate debug/diagnostic image.
    debug_image_file: Option<String>,
    /// Bitmask of options and flag values.
    flags: u32,
    /// The formula to use when converting colors to luminance values.
    lt: LuminanceType,
    /// The character set to use for OCR.
    charset: Charset,
    /// Foreground pixel value (0 for black, 255 for white).
    foreground: u8,
    /// Background pixel value (0 for black, 255 for white).
    background: u8,
    /// Ordered list of image processing commands to execute.
    commands: Vec<String>,
    /// Path to the source input image.
    image_file: String,
}


fn parse_interval(s: &str) -> Option<(i32, i32)> {
    if s == "-1" {
        return Some((-1, -1));
    }
    if let Some(pos) = s.find('-') {
        let left = &s[..pos];
        let right = &s[pos + 1..];
        if left.is_empty() || right.is_empty() {
            return None;
        }
        let min_val = left.parse::<i32>().ok()?;
        let max_val = right.parse::<i32>().ok()?;
        if min_val < 1 || max_val < min_val {
            return None;
        }
        Some((min_val, max_val))
    } else {
        let val = s.parse::<i32>().ok()?;
        if val < 1 {
            return None;
        }
        Some((val, val))
    }
}

fn parse_dimensions(s: &str) -> Option<(i32, i32)> {
    let parts: Vec<&str> = s.split('x').collect();
    if parts.len() != 2 {
        return None;
    }
    let w = parts[0].parse::<i32>().ok()?;
    let h = parts[1].parse::<i32>().ok()?;
    if w < 1 || h < 1 {
        return None;
    }
    Some((w, h))
}

fn parse_args(args: &[String]) -> Result<Config> {
    let mut config = Config {
        thresh: THRESHOLD,
        need_pixels: NEED_PIXELS,
        min_segment: MIN_SEGMENT,
        min_char_w: MIN_CHAR_W,
        min_char_h: MIN_CHAR_H,
        ignore_pixels: IGNORE_PIXELS,
        expected_digits_min: NUMBER_OF_DIGITS,
        expected_digits_max: NUMBER_OF_DIGITS,
        one_ratio: ONE_RATIO,
        minus_ratio: MINUS_RATIO,
        dec_h_ratio: DEC_H_RATIO,
        dec_w_ratio: DEC_W_RATIO,
        spc_fac: SPC_FAC,
        output_file: None,
        output_fmt: None,
        debug_image_file: None,
        flags: 0,
        lt: LuminanceType::Rec709,
        charset: Charset::Full,
        foreground: SSOCR_DEFAULT_FOREGROUND,
        background: SSOCR_DEFAULT_BACKGROUND,
        commands: Vec::new(),
        image_file: String::new(),
    };

    let mut i = 1;
    let mut positional = Vec::new();

    while i < args.len() {
        let arg = &args[i];
        if arg.starts_with('-') && arg != "-" {
            let mut opt_name = arg.clone();
            let mut opt_val: Option<String> = None;

            if arg.starts_with("--") {
                if let Some(pos) = arg.find('=') {
                    opt_name = arg[..pos].to_string();
                    opt_val = Some(arg[pos + 1..].to_string());
                }
            } else {
                if arg.len() > 2 {
                    let char_opt = &arg[1..2];
                    if "tnNMiidrmoOfblcHWAD".contains(char_opt) {
                        opt_name = format!("-{}", char_opt);
                        opt_val = Some(arg[2..].to_string());
                    }
                }
            }

            match opt_name.as_str() {
                "-h" | "--help" => {
                    help::print_help(PROG);
                    std::process::exit(42);
                }
                "-V" | "--version" => {
                    help::print_version();
                    std::process::exit(42);
                }
                "-v" | "--verbose" => {
                    config.flags |= defines::VERBOSE;
                }
                "-a" | "--absolute-threshold" => {
                    config.flags |= defines::ABSOLUTE_THRESHOLD;
                }
                "-T" | "--iter-threshold" => {
                    config.flags |= defines::DO_ITERATIVE_THRESHOLD;
                }
                "-P" | "--debug-output" => {
                    config.flags |= defines::VERBOSE | defines::DEBUG_OUTPUT;
                }
                "-p" | "--process-only" => {
                    config.flags |= defines::PROCESS_ONLY;
                }
                "-I" | "--print-info" => {
                    config.flags |= defines::PRINT_INFO;
                }
                "-g" | "--adjust-gray" => {
                    config.flags |= defines::ADJUST_GRAY;
                }
                "-S" | "--ascii-art-segments" => {
                    config.flags |= defines::ASCII_ART_SEGMENTS;
                }
                "-X" | "--print-as-hex" => {
                    config.flags |= defines::PRINT_AS_HEX;
                }
                "-C" | "--omit-decimal-point" => {
                    config.flags |= defines::OMIT_DECIMAL;
                }
                "-s" | "--print-spaces" => {
                    config.flags |= defines::PRINT_SPACES;
                }
                "-G" | "--space-average" => {
                    config.flags |= defines::SPC_USE_AVG_DST;
                }
                "-F" | "--adapt-after-crop" => {
                    config.flags |= defines::ADAPT_AFTER_CROP;
                }
                "-t" | "--threshold" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if let Ok(v) = val.parse::<f64>()
                        && (0.0..=100.0).contains(&v) {
                            config.thresh = v;
                        }
                }
                "-n" | "--number-pixels" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if let Ok(v) = val.parse::<i32>()
                        && v >= 1 {
                            config.need_pixels = v;
                        }
                }
                "-N" | "--min-segment" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if let Ok(v) = val.parse::<i32>()
                        && v >= 1 {
                            config.min_segment = v;
                            config.need_pixels = v;
                        }
                }
                "-i" | "--ignore-pixels" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if let Ok(v) = val.parse::<i32>()
                        && v >= 0 {
                            config.ignore_pixels = v;
                        }
                }
                "-M" | "--min-char-dims" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if let Some((w, h)) = parse_dimensions(&val) {
                        config.min_char_w = w;
                        config.min_char_h = h;
                    }
                }
                "-d" | "--number-digits" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if let Some((min_d, max_d)) = parse_interval(&val) {
                        config.expected_digits_min = min_d;
                        config.expected_digits_max = max_d;
                    }
                }
                "-r" | "--one-ratio" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if let Ok(v) = val.parse::<i32>()
                        && v >= 2 {
                            config.one_ratio = v;
                        }
                }
                "-m" | "--minus-ratio" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if let Ok(v) = val.parse::<i32>()
                        && v >= 1 {
                            config.minus_ratio = v;
                        }
                }
                "-H" | "--dec-h-ratio" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if let Ok(v) = val.parse::<i32>()
                        && v >= 2 {
                            config.dec_h_ratio = v;
                        }
                }
                "-W" | "--dec-w-ratio" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if let Ok(v) = val.parse::<i32>()
                        && v >= 1 {
                            config.dec_w_ratio = v;
                        }
                }
                "-A" | "--space-factor" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if let Ok(v) = val.parse::<f64>()
                        && v >= 1.0 {
                            config.spc_fac = v;
                        }
                }
                "-o" | "--output-image" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    config.output_file = Some(val);
                }
                "-O" | "--output-format" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    config.output_fmt = Some(val);
                }
                "-D" | "--debug-image" => {
                    config.flags |= defines::USE_DEBUG_IMAGE;
                    if let Some(v) = opt_val {
                        config.debug_image_file = Some(v);
                    } else {
                        config.debug_image_file = Some(DEBUG_IMAGE_NAME.to_string());
                    }
                }
                "-f" | "--foreground" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if val.to_lowercase() == "black" {
                        config.foreground = SSOCR_BLACK;
                        config.background = SSOCR_WHITE;
                    } else if val.to_lowercase() == "white" {
                        config.foreground = SSOCR_WHITE;
                        config.background = SSOCR_BLACK;
                    } else {
                        anyhow::bail!("foreground color must be black or white");
                    }
                }
                "-b" | "--background" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if val.to_lowercase() == "black" {
                        config.background = SSOCR_BLACK;
                        config.foreground = SSOCR_WHITE;
                    } else if val.to_lowercase() == "white" {
                        config.background = SSOCR_WHITE;
                        config.foreground = SSOCR_BLACK;
                    } else {
                        anyhow::bail!("background color must be black or white");
                    }
                }
                "-l" | "--luminance" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if val.to_lowercase() == "help" {
                        help::print_lum_help();
                        std::process::exit(42);
                    }
                    config.lt = match val.to_lowercase().as_str() {
                        "rec601" => LuminanceType::Rec601,
                        "rec709" => LuminanceType::Rec709,
                        "linear" => LuminanceType::Linear,
                        "minimum" => LuminanceType::Minimum,
                        "maximum" => LuminanceType::Maximum,
                        "red" => LuminanceType::Red,
                        "green" => LuminanceType::Green,
                        "blue" => LuminanceType::Blue,
                        _ => anyhow::bail!("unknown luminance formula {}", val),
                    };
                }
                "-c" | "--charset" => {
                    let val = match opt_val {
                        Some(v) => v,
                        None => {
                            i += 1;
                            args[i].clone()
                        }
                    };
                    if val.to_lowercase() == "help" {
                        help::print_cs_help();
                        std::process::exit(42);
                    }
                    if let Some(cs) = parse_charset(&val) {
                        config.charset = cs;
                    } else {
                        anyhow::bail!("unknown charset {}", val);
                    }
                }
                _ => {
                    anyhow::bail!("unknown option {}", arg);
                }
            }
        } else {
            positional.push(arg.clone());
        }
        i += 1;
    }

    if positional.is_empty() {
        anyhow::bail!("no image filename given");
    }

    config.image_file = positional.pop().unwrap();
    config.commands = positional;

    Ok(config)
}

fn draw_horizontal_line(img: &mut RgbaImage, mut x1: i32, mut x2: i32, y: i32, color: Rgba<u8>) {
    if y < 0 || y >= img.height() as i32 {
        return;
    }
    if x1 > x2 {
        std::mem::swap(&mut x1, &mut x2);
    }
    let w = img.width() as i32;
    for x in x1..=x2 {
        if x >= 0 && x < w {
            img.put_pixel(x as u32, y as u32, color);
        }
    }
}

fn draw_vertical_line(img: &mut RgbaImage, x: i32, mut y1: i32, mut y2: i32, color: Rgba<u8>) {
    if x < 0 || x >= img.width() as i32 {
        return;
    }
    if y1 > y2 {
        std::mem::swap(&mut y1, &mut y2);
    }
    let h = img.height() as i32;
    for y in y1..=y2 {
        if y >= 0 && y < h {
            img.put_pixel(x as u32, y as u32, color);
        }
    }
}

fn draw_rectangle(img: &mut RgbaImage, x1: i32, y1: i32, x2: i32, y2: i32, color: Rgba<u8>) {
    draw_horizontal_line(img, x1, x2, y1, color);
    draw_horizontal_line(img, x1, x2, y2, color);
    draw_vertical_line(img, x1, y1, y2, color);
    draw_vertical_line(img, x2, y1, y2, color);
}

#[allow(clippy::too_many_arguments)]
fn scanline(
    debug_image: &mut Option<RgbaImage>,
    image: &RgbaImage,
    x: i32,
    y: i32,
    len: i32,
    dir: Direction,
    d_color: Rgba<u8>,
    thresh: f64,
    lt: LuminanceType,
    flags: u32,
    fg: u8,
) -> u32 {
    let mut found_pixels = 0;
    let start = if dir == Direction::Horizontal { x } else { y };
    let end = start + len;
    let width = image.width() as i32;
    let height = image.height() as i32;

    for i in start..=end {
        let (ix, iy) = if dir == Direction::Horizontal {
            (i, y)
        } else {
            (x, i)
        };
        if ix >= 0 && ix < width && iy >= 0 && iy < height {
            let color = image.get_pixel(ix as u32, iy as u32);
            let lum = get_lum(color, lt);
            if is_pixel_set(lum, thresh, fg) {
                if flags & defines::USE_DEBUG_IMAGE != 0
                    && let Some(debug_img) = debug_image {
                        debug_img.put_pixel(ix as u32, iy as u32, d_color);
                    }
                found_pixels += 1;
            }
        }
    }
    found_pixels
}

fn print_spaces(n: i32) {
    for _ in 0..n {
        print!(" ");
    }
}

fn eprint_spaces(n: i32) {
    for _ in 0..n {
        eprint!(" ");
    }
}

fn main() -> Result<()> {
    let args: Vec<String> = std::env::args().collect();
    if args.len() < 2 {
        help::print_short_usage(PROG);
        std::process::exit(99);
    }

    let config = match parse_args(&args) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("{}: error: {}", PROG, e);
            help::print_short_usage(PROG);
            std::process::exit(99);
        }
    };

    let mut image = if config.image_file == "-" {
        if config.flags & defines::VERBOSE != 0 {
            eprintln!("reading image from standard input");
        }
        let mut buffer = Vec::new();
        std::io::stdin().read_to_end(&mut buffer)?;
        image::load_from_memory(&buffer)?.to_rgba8()
    } else {
        image::open(&config.image_file)?.to_rgba8()
    };

    let mut w = image.width() as i32;
    let mut h = image.height() as i32;

    if (config.flags & defines::DEBUG_OUTPUT != 0) || (config.flags & defines::PRINT_INFO != 0) {
        eprintln!("image width: {}\nimage height: {}", w, h);
        let (min_val, max_val) = get_minmaxval(&image, config.lt);
        eprintln!(
            "{:.2} <= lum <= {:.2} (lum should be in [0,255])",
            min_val, max_val
        );
    }

    let mut thresh = config.thresh;
    let mut is_adapted = false;

    // Process pipeline commands
    if config.flags & defines::VERBOSE != 0 && !config.commands.is_empty() {
        eprint!("got commands");
        for cmd in &config.commands {
            eprint!(" {}", cmd);
        }
        eprintln!();
    }

    let mut cmd_idx = 0;
    while cmd_idx < config.commands.len() {
        let cmd = &config.commands[cmd_idx];
        if cmd.eq_ignore_ascii_case("dilation") {
            let mut n = 1;
            if cmd_idx + 1 < config.commands.len()
                && let Ok(val) = config.commands[cmd_idx + 1].parse::<i32>()
                    && val > 0 {
                        n = val;
                        cmd_idx += 1;
                    }
            thresh = adapt_threshold(
                &image,
                thresh,
                config.lt,
                config.flags,
                false,
                &mut is_adapted,
            );
            image = dilation(
                &image,
                thresh,
                config.lt,
                n,
                config.foreground,
                config.background,
            );
        } else if cmd.eq_ignore_ascii_case("erosion") {
            let mut n = 1;
            if cmd_idx + 1 < config.commands.len()
                && let Ok(val) = config.commands[cmd_idx + 1].parse::<i32>()
                    && val > 0 {
                        n = val;
                        cmd_idx += 1;
                    }
            thresh = adapt_threshold(
                &image,
                thresh,
                config.lt,
                config.flags,
                false,
                &mut is_adapted,
            );
            image = erosion(
                &image,
                thresh,
                config.lt,
                n,
                config.foreground,
                config.background,
            );
        } else if cmd.eq_ignore_ascii_case("opening") {
            let mut n = 1;
            if cmd_idx + 1 < config.commands.len()
                && let Ok(val) = config.commands[cmd_idx + 1].parse::<i32>()
                    && val > 0 {
                        n = val;
                        cmd_idx += 1;
                    }
            thresh = adapt_threshold(
                &image,
                thresh,
                config.lt,
                config.flags,
                false,
                &mut is_adapted,
            );
            image = opening(
                &image,
                thresh,
                config.lt,
                n,
                config.foreground,
                config.background,
            );
        } else if cmd.eq_ignore_ascii_case("closing") {
            let mut n = 1;
            if cmd_idx + 1 < config.commands.len()
                && let Ok(val) = config.commands[cmd_idx + 1].parse::<i32>()
                    && val > 0 {
                        n = val;
                        cmd_idx += 1;
                    }
            thresh = adapt_threshold(
                &image,
                thresh,
                config.lt,
                config.flags,
                false,
                &mut is_adapted,
            );
            image = closing(
                &image,
                thresh,
                config.lt,
                n,
                config.foreground,
                config.background,
            );
        } else if cmd.eq_ignore_ascii_case("remove_isolated") {
            thresh = adapt_threshold(
                &image,
                thresh,
                config.lt,
                config.flags,
                false,
                &mut is_adapted,
            );
            image = remove_isolated(
                &image,
                thresh,
                config.lt,
                config.foreground,
                config.background,
            );
        } else if cmd.eq_ignore_ascii_case("make_mono") {
            thresh = adapt_threshold(
                &image,
                thresh,
                config.lt,
                config.flags,
                false,
                &mut is_adapted,
            );
            image = make_mono(
                &image,
                thresh,
                config.lt,
                config.foreground,
                config.background,
            );
        } else if cmd.eq_ignore_ascii_case("white_border") {
            let mut w = 1;
            if cmd_idx + 1 < config.commands.len()
                && let Ok(val) = config.commands[cmd_idx + 1].parse::<i32>()
                    && val > 0 {
                        w = val;
                        cmd_idx += 1;
                    }
            thresh = adapt_threshold(
                &image,
                thresh,
                config.lt,
                config.flags,
                false,
                &mut is_adapted,
            );
            image = white_border(&image, w, config.background);
        } else if cmd.eq_ignore_ascii_case("shear") {
            if cmd_idx + 1 < config.commands.len() {
                let offset: i32 = config.commands[cmd_idx + 1].parse()?;
                cmd_idx += 1;
                thresh = adapt_threshold(
                    &image,
                    thresh,
                    config.lt,
                    config.flags,
                    false,
                    &mut is_adapted,
                );
                image = shear(&image, offset, config.background);
            } else {
                anyhow::bail!("shear command needs an argument");
            }
        } else if cmd.eq_ignore_ascii_case("set_pixels_filter") {
            if cmd_idx + 1 < config.commands.len() {
                let mask: i32 = config.commands[cmd_idx + 1].parse()?;
                cmd_idx += 1;
                thresh = adapt_threshold(
                    &image,
                    thresh,
                    config.lt,
                    config.flags,
                    false,
                    &mut is_adapted,
                );
                image = set_pixels_filter(
                    &image,
                    thresh,
                    config.lt,
                    mask,
                    config.foreground,
                    config.background,
                );
            } else {
                anyhow::bail!("set_pixels_filter command needs an argument");
            }
        } else if cmd.eq_ignore_ascii_case("keep_pixels_filter") {
            if cmd_idx + 1 < config.commands.len() {
                let mask: i32 = config.commands[cmd_idx + 1].parse()?;
                cmd_idx += 1;
                thresh = adapt_threshold(
                    &image,
                    thresh,
                    config.lt,
                    config.flags,
                    false,
                    &mut is_adapted,
                );
                image = keep_pixels_filter(
                    &image,
                    thresh,
                    config.lt,
                    mask,
                    config.foreground,
                    config.background,
                );
            } else {
                anyhow::bail!("keep_pixels_filter command needs an argument");
            }
        } else if cmd.eq_ignore_ascii_case("dynamic_threshold") {
            if cmd_idx + 2 < config.commands.len() {
                let ww: i32 = config.commands[cmd_idx + 1].parse()?;
                let wh: i32 = config.commands[cmd_idx + 2].parse()?;
                cmd_idx += 2;
                thresh = adapt_threshold(
                    &image,
                    thresh,
                    config.lt,
                    config.flags,
                    false,
                    &mut is_adapted,
                );
                image = dynamic_threshold(
                    &image,
                    thresh,
                    config.lt,
                    ww,
                    wh,
                    config.foreground,
                    config.background,
                );
            } else {
                anyhow::bail!("dynamic_threshold command needs two arguments");
            }
        } else if cmd.eq_ignore_ascii_case("rgb_threshold") {
            thresh = adapt_threshold(
                &image,
                thresh,
                config.lt,
                config.flags,
                false,
                &mut is_adapted,
            );
            image = make_mono(
                &image,
                thresh,
                LuminanceType::Minimum,
                config.foreground,
                config.background,
            );
        } else if cmd.eq_ignore_ascii_case("r_threshold") {
            thresh = adapt_threshold(
                &image,
                thresh,
                config.lt,
                config.flags,
                false,
                &mut is_adapted,
            );
            image = make_mono(
                &image,
                thresh,
                LuminanceType::Red,
                config.foreground,
                config.background,
            );
        } else if cmd.eq_ignore_ascii_case("g_threshold") {
            thresh = adapt_threshold(
                &image,
                thresh,
                config.lt,
                config.flags,
                false,
                &mut is_adapted,
            );
            image = make_mono(
                &image,
                thresh,
                LuminanceType::Green,
                config.foreground,
                config.background,
            );
        } else if cmd.eq_ignore_ascii_case("b_threshold") {
            thresh = adapt_threshold(
                &image,
                thresh,
                config.lt,
                config.flags,
                false,
                &mut is_adapted,
            );
            image = make_mono(
                &image,
                thresh,
                LuminanceType::Blue,
                config.foreground,
                config.background,
            );
        } else if cmd.eq_ignore_ascii_case("invert") {
            thresh = adapt_threshold(
                &image,
                thresh,
                config.lt,
                config.flags,
                false,
                &mut is_adapted,
            );
            image = invert(
                &image,
                thresh,
                config.lt,
                config.foreground,
                config.background,
            );
        } else if cmd.eq_ignore_ascii_case("gray_stretch") {
            if cmd_idx + 2 < config.commands.len() {
                let mut t1: f64 = config.commands[cmd_idx + 1].parse()?;
                let mut t2: f64 = config.commands[cmd_idx + 2].parse()?;
                cmd_idx += 2;
                if config.flags & defines::ADJUST_GRAY != 0 {
                    if config.flags & defines::VERBOSE != 0 {
                        eprintln!(" adjusting T1={:.2} and T2={:.2} to image", t1, t2);
                    }
                    let (min_val, max_val) = get_minmaxval(&image, config.lt);
                    t1 = min_val + t1 / 100.0 * (max_val - min_val);
                    t2 = min_val + t2 / 100.0 * (max_val - min_val);
                    if config.flags & defines::VERBOSE != 0 {
                        eprintln!(" adjusted to T1={:.2} and T2={:.2}", t1, t2);
                    }
                }
                thresh = adapt_threshold(
                    &image,
                    thresh,
                    config.lt,
                    config.flags,
                    false,
                    &mut is_adapted,
                );
                image = gray_stretch(&image, t1, t2, config.lt);
            } else {
                anyhow::bail!("gray_stretch command needs two arguments");
            }
        } else if cmd.eq_ignore_ascii_case("grayscale") {
            image = grayscale(&image, config.lt);
        } else if cmd.eq_ignore_ascii_case("crop") {
            if cmd_idx + 4 < config.commands.len() {
                let x: i32 = config.commands[cmd_idx + 1].parse()?;
                let y: i32 = config.commands[cmd_idx + 2].parse()?;
                let cw: i32 = config.commands[cmd_idx + 3].parse()?;
                let ch: i32 = config.commands[cmd_idx + 4].parse()?;
                cmd_idx += 4;
                if config.flags & defines::ADAPT_AFTER_CROP == 0 {
                    thresh = adapt_threshold(
                        &image,
                        thresh,
                        config.lt,
                        config.flags,
                        false,
                        &mut is_adapted,
                    );
                }
                image = crop(&image, x, y, cw, ch);
                if (config.flags & defines::DEBUG_OUTPUT != 0)
                    || (config.flags & defines::VERBOSE != 0)
                {
                    eprintln!(
                        "  cropped image width: {}\n  cropped image height: {}",
                        image.width(),
                        image.height()
                    );
                }
                if (config.flags & defines::DEBUG_OUTPUT != 0)
                    || (config.flags & defines::PRINT_INFO != 0)
                    || (config.flags & defines::VERBOSE != 0)
                {
                    let (min_val, max_val) = get_minmaxval(&image, config.lt);
                    eprintln!(
                        "  {:.2} <= lum <= {:.2} in cropped image (lum should be in [0,255])",
                        min_val, max_val
                    );
                }
                thresh = adapt_threshold(
                    &image,
                    thresh,
                    config.lt,
                    config.flags,
                    true,
                    &mut is_adapted,
                );
            } else {
                anyhow::bail!("crop command needs 4 arguments");
            }
        } else if cmd.eq_ignore_ascii_case("rotate") {
            if cmd_idx + 1 < config.commands.len() {
                let theta: f64 = config.commands[cmd_idx + 1].parse()?;
                cmd_idx += 1;
                thresh = adapt_threshold(
                    &image,
                    thresh,
                    config.lt,
                    config.flags,
                    false,
                    &mut is_adapted,
                );
                image = rotate(&image, theta, config.background);
            } else {
                anyhow::bail!("rotate command needs an argument");
            }
        } else if cmd.eq_ignore_ascii_case("mirror") {
            if cmd_idx + 1 < config.commands.len() {
                let dir_str = &config.commands[cmd_idx + 1];
                cmd_idx += 1;
                let dir = if dir_str.starts_with("horiz") {
                    Direction::Horizontal
                } else if dir_str.starts_with("vert") {
                    Direction::Vertical
                } else {
                    anyhow::bail!("argument to 'mirror' must be 'horiz' or 'vert'");
                };
                image = mirror(&image, dir);
            } else {
                anyhow::bail!("mirror command needs argument 'horiz' or 'vert'");
            }
        } else {
            anyhow::bail!("unknown command \"{}\"", cmd);
        }
        cmd_idx += 1;
    }

    // Output image if requested
    if let Some(ref out_file) = config.output_file {
        image.save(out_file)?;
    }

    if config.flags & defines::PROCESS_ONLY != 0 {
        std::process::exit(3);
    }

    thresh = adapt_threshold(
        &image,
        thresh,
        config.lt,
        config.flags,
        false,
        &mut is_adapted,
    );

    // Prepare debug image
    let mut debug_image = if config.flags & defines::USE_DEBUG_IMAGE != 0 {
        Some(make_mono(
            &image,
            thresh,
            config.lt,
            config.foreground,
            config.background,
        ))
    } else {
        None
    };

    if config.flags & defines::DEBUG_OUTPUT != 0 {
        eprintln!("starting image segmentation");
        eprintln!("starting horizontal partitioning");
    }

    w = image.width() as i32;
    h = image.height() as i32;

    let mut digits = Vec::new();
    let mut state = if config.foreground == SSOCR_BLACK {
        FIND_DARK
    } else {
        FIND_LIGHT
    };
    let mut current_digit = DigitStruct::default();

    let find_fg = if config.foreground == SSOCR_BLACK {
        DARK
    } else {
        LIGHT
    };
    let find_bg = if config.foreground == SSOCR_BLACK {
        LIGHT
    } else {
        DARK
    };
    let find_fg_state = if config.foreground == SSOCR_BLACK {
        FIND_DARK
    } else {
        FIND_LIGHT
    };
    let find_bg_state = if config.foreground == SSOCR_BLACK {
        FIND_LIGHT
    } else {
        FIND_DARK
    };

    // Horizontal segmentation
    for i in 0..w {
        let mut found_pixels = 0;
        for j in 0..h {
            let color = image.get_pixel(i as u32, j as u32);
            let lum = get_lum(color, config.lt);
            if is_pixel_set(lum, thresh, config.foreground) {
                found_pixels += 1;
            }
        }
        let is_fg_col = found_pixels > config.ignore_pixels;

        if state == find_fg_state && is_fg_col {
            if config.flags & defines::DEBUG_OUTPUT != 0 {
                eprintln!(
                    " start of potential digit {} in image column {}",
                    digits.len(),
                    i
                );
            }
            current_digit.x1 = i;
            current_digit.y1 = 0;
            if config.flags & defines::USE_DEBUG_IMAGE != 0
                && let Some(debug_img) = &mut debug_image {
                    draw_vertical_line(debug_img, i, 0, h - 1, Rgba([255, 0, 0, 255]));
                }
            state = find_bg_state;
        } else if state == find_bg_state && !is_fg_col {
            if config.flags & defines::DEBUG_OUTPUT != 0 {
                eprintln!(
                    " end of potential digit {} in image column {}",
                    digits.len(),
                    i
                );
            }
            current_digit.x2 = i;
            current_digit.y2 = h - 1;
            digits.push(current_digit.clone());
            if config.flags & defines::USE_DEBUG_IMAGE != 0
                && let Some(debug_img) = &mut debug_image {
                    draw_vertical_line(debug_img, i, 0, h - 1, Rgba([0, 0, 255, 255]));
                }
            state = find_fg_state;
        }
    }

    if state == find_bg_state {
        if config.flags & defines::DEBUG_OUTPUT != 0 {
            eprintln!(
                " end of potential digit {} in image column {}",
                digits.len(),
                w - 1
            );
        }
        current_digit.x2 = w - 1;
        current_digit.y2 = h - 1;
        digits.push(current_digit.clone());
    }

    if config.flags & defines::DEBUG_OUTPUT != 0 {
        eprintln!("horizontal partitioning found {} digit(s)", digits.len());
        eprintln!("looking for upper and lower digit boundaries");
    }

    // Vertical boundaries for each digit
    for digit in &mut digits {
        let mut found_top = false;
        let mut state = find_fg_state;
        for j in 0..h {
            let mut row = UNKNOWN;
            let mut found_pixels = 0;
            for i in digit.x1..=digit.x2 {
                let color = image.get_pixel(i as u32, j as u32);
                let lum = get_lum(color, config.lt);
                if is_pixel_set(lum, thresh, config.foreground) {
                    found_pixels += 1;
                    if found_pixels > config.ignore_pixels {
                        row = find_fg;
                    }
                } else if row == UNKNOWN {
                    row = find_bg;
                }
            }

            if state == find_fg_state && row == find_fg {
                if found_top {
                    digit.y2 = j;
                    state = find_bg_state;
                    if config.flags & defines::USE_DEBUG_IMAGE != 0
                        && let Some(debug_img) = &mut debug_image {
                            draw_horizontal_line(
                                debug_img,
                                digit.x1,
                                digit.x2,
                                digit.y2,
                                Rgba([0, 255, 0, 255]),
                            );
                        }
                } else {
                    digit.y1 = j;
                    found_top = true;
                    state = find_bg_state;
                    if config.flags & defines::USE_DEBUG_IMAGE != 0
                        && let Some(debug_img) = &mut debug_image {
                            draw_horizontal_line(
                                debug_img,
                                digit.x1,
                                digit.x2,
                                digit.y1,
                                Rgba([0, 255, 0, 255]),
                            );
                        }
                }
            } else if state == find_bg_state && row == find_bg {
                digit.y2 = j;
                state = find_fg_state;
                if config.flags & defines::USE_DEBUG_IMAGE != 0
                    && let Some(debug_img) = &mut debug_image {
                        draw_horizontal_line(
                            debug_img,
                            digit.x1,
                            digit.x2,
                            digit.y2,
                            Rgba([0, 255, 0, 255]),
                        );
                    }
            }
        }

        if state == find_bg_state {
            digit.y2 = h - 1;
            if config.flags & defines::USE_DEBUG_IMAGE != 0
                && let Some(debug_img) = &mut debug_image {
                    draw_horizontal_line(
                        debug_img,
                        digit.x1,
                        digit.x2,
                        digit.y2,
                        Rgba([0, 255, 0, 255]),
                    );
                }
        }
    }

    if config.flags & defines::DEBUG_OUTPUT != 0 {
        eprintln!("image segmentation found {} potential digits", digits.len());
    }

    // Drop too small digits
    if config.min_char_w > 1 || config.min_char_h > 1 {
        let mut filtered_digits = Vec::new();
        for (d, digit) in digits.iter().enumerate() {
            let width = digit.x2 - digit.x1;
            let height = digit.y2 - digit.y1;
            if width >= config.min_char_w && height >= config.min_char_h {
                if config.flags & defines::DEBUG_OUTPUT != 0 {
                    eprintln!(" keeping sufficiently large digit {}", d);
                }
                filtered_digits.push(digit.clone());
            } else if config.flags & defines::DEBUG_OUTPUT != 0 {
                eprintln!(" dropping too small potential digit {}", d);
            }
        }
        if config.flags & defines::DEBUG_OUTPUT != 0 {
            eprintln!(
                "keeping {} of {} potential digits",
                filtered_digits.len(),
                digits.len()
            );
        }
        if filtered_digits.is_empty() {
            eprintln!("{}: error: no sufficiently large digits found", PROG);
            std::process::exit(1);
        }
        digits = filtered_digits;
    }

    if config.expected_digits_min > -1
        && ((digits.len() as i32) < config.expected_digits_min
            || (digits.len() as i32) > config.expected_digits_max)
    {
        if config.expected_digits_min != config.expected_digits_max {
            eprintln!(
                "{}: expected between {} and {} digits, but found {}",
                PROG,
                config.expected_digits_min,
                config.expected_digits_max,
                digits.len()
            );
        } else {
            eprintln!(
                "{}: expected {} digit{}, but found {}",
                PROG,
                config.expected_digits_min,
                if config.expected_digits_min > 1 {
                    "s"
                } else {
                    ""
                },
                digits.len()
            );
        }
        if let Some(debug_img) = &debug_image
            && let Some(ref debug_file) = config.debug_image_file {
                debug_img.save(debug_file).ok();
            }
        std::process::exit(1);
    }

    if config.flags & defines::DEBUG_OUTPUT != 0 {
        eprintln!("image segmentation found {} digits", digits.len());
    }

    // Draw rectangles around accepted digits
    if config.flags & defines::USE_DEBUG_IMAGE != 0
        && let Some(debug_img) = &mut debug_image {
            for digit in &digits {
                draw_rectangle(
                    debug_img,
                    digit.x1,
                    digit.y1,
                    digit.x2,
                    digit.y2,
                    Rgba([128, 128, 128, 255]),
                );
            }
        }

    let mut max_dig_w = 0;
    let mut max_dig_h = 0;
    for digit in &digits {
        max_dig_w = max_dig_w.max(digit.x2 - digit.x1);
        max_dig_h = max_dig_h.max(digit.y2 - digit.y1);
    }

    if config.flags & defines::DEBUG_OUTPUT != 0 {
        eprintln!(
            "digits are at most {} pixels wide and {} pixels high",
            max_dig_w, max_dig_h
        );
        eprintln!("found {} digits", digits.len());
        for (d, digit) in digits.iter().enumerate() {
            let width = digit.x2 - digit.x1;
            let height = digit.y2 - digit.y1;
            eprintln!(
                "digit {}: ({},{}) -> ({},{}), width: {} height: {}",
                d, digit.x1, digit.y1, digit.x2, digit.y2, width, height
            );
            if width == 0 {
                eprintln!("  height/width (int): NaN, max_dig_w/width (int): NaN, ");
            } else {
                eprintln!(
                    "  height/width (int): {}, max_dig_w/width (int): {}, ",
                    height / width,
                    max_dig_w / width
                );
            }
            if height == 0 {
                eprintln!("max_dig_h/height (int): NaN");
            } else {
                eprintln!("max_dig_h/height (int): {}", max_dig_h / height);
            }
        }
    }

    // Identify digit '1'
    if config.flags & defines::DEBUG_OUTPUT != 0 {
        eprintln!("looking for digit 1");
    }
    for (d, digit) in digits.iter_mut().enumerate() {
        let width = digit.x2 - digit.x1;
        let height = digit.y2 - digit.y1;
        if width < config.min_segment {
            if config.flags & defines::DEBUG_OUTPUT != 0 {
                eprintln!(" skipping too narrow digit {} with width {}", d, width);
            }
            continue;
        }
        if (height as f64 + 0.5) / (width as f64) > config.one_ratio as f64 {
            if config.flags & defines::DEBUG_OUTPUT != 0 {
                eprintln!(
                    " digit {} is a 1 (height/width = {}/{} = (int) {})",
                    d,
                    height,
                    width,
                    height / width
                );
            }
            digit.digit = D_ONE;
        }
    }

    // Identify decimal point
    if config.flags & defines::DEBUG_OUTPUT != 0 {
        eprintln!("looking for decimal points");
    }
    for (d, digit) in digits.iter_mut().enumerate() {
        let width = digit.x2 - digit.x1;
        let height = digit.y2 - digit.y1;
        if width == 0 || height == 0 {
            if config.flags & defines::DEBUG_OUTPUT != 0 {
                eprintln!(" skipping digit {} with zero width or height", d);
            }
            continue;
        }
        if digit.digit == D_UNKNOWN
            && max_dig_h / height > config.dec_h_ratio
            && max_dig_w / width > config.dec_w_ratio
        {
            digit.digit = D_DECIMAL;
            if config.flags & defines::DEBUG_OUTPUT != 0 {
                eprintln!(" digit {} is a decimal point", d);
            }
        }
    }

    // Identify minus sign
    if config.flags & defines::DEBUG_OUTPUT != 0 {
        eprintln!("looking for minus signs");
    }
    for (d, digit) in digits.iter_mut().enumerate() {
        let width = digit.x2 - digit.x1;
        let height = digit.y2 - digit.y1;
        if height < config.min_segment {
            if config.flags & defines::DEBUG_OUTPUT != 0 {
                eprintln!(" skipping too short digit {} with height {}", d, height);
            }
            continue;
        }
        if digit.digit == D_UNKNOWN && width / height >= config.minus_ratio {
            digit.digit = D_MINUS;
            if config.flags & defines::DEBUG_OUTPUT != 0 {
                eprintln!(
                    " digit {} is a minus (width/height = {}/{} = (int) {})",
                    d,
                    width,
                    height,
                    width / height
                );
            }
        }
    }

    // Special case widest is a 1
    if config.flags & defines::DEBUG_OUTPUT != 0 {
        eprintln!("checking for special case of a one as widest character");
    }
    let mut widest_dig_is_one = false;
    for digit in &digits {
        let width = digit.x2 - digit.x1;
        let height = digit.y2 - digit.y1;
        if width == 0 || height == 0 {
            continue;
        }
        if digit.digit == D_ONE && width >= max_dig_w {
            widest_dig_is_one = true;
            if config.flags & defines::DEBUG_OUTPUT != 0 {
                eprintln!(" widest digit is a one -> additional decimal point search");
            }
            break;
        }
    }

    if widest_dig_is_one {
        for (d, digit) in digits.iter_mut().enumerate() {
            let width = digit.x2 - digit.x1;
            let height = digit.y2 - digit.y1;
            if width == 0 || height == 0 {
                continue;
            }
            if digit.digit == D_UNKNOWN && max_dig_h / height > config.dec_h_ratio {
                digit.digit = D_DECIMAL;
                if config.flags & defines::DEBUG_OUTPUT != 0 {
                    eprintln!(" digit {} is a decimal point", d);
                }
            }
        }
    }

    // Scanline recognition
    if config.flags & defines::DEBUG_OUTPUT != 0 {
        eprintln!("starting scanline based recognition for remaining digits");
    }

    for (d, digit) in digits.iter_mut().enumerate() {
        let width = digit.x2 - digit.x1;
        let height = digit.y2 - digit.y1;

        if width < config.min_segment || height < config.min_segment {
            if config.flags & defines::DEBUG_OUTPUT != 0 {
                eprintln!(" skipping digit {} smaller than minimum segment", d);
            }
            continue;
        }

        if digit.digit == D_UNKNOWN {
            let middle = (digit.x1 + digit.x2) / 2;
            let quarter = digit.y1 + height / 4;
            let three_quarters = digit.y1 + 3 * height / 4;

            // Check HORIZ_UP
            let found = scanline(
                &mut debug_image,
                &image,
                middle,
                digit.y1,
                height / 3,
                Direction::Vertical,
                Rgba([255, 0, 0, 255]),
                thresh,
                config.lt,
                config.flags,
                config.foreground,
            );
            if found >= config.need_pixels as u32 {
                digit.digit |= HORIZ_UP;
            }

            // Check HORIZ_MID
            let found = scanline(
                &mut debug_image,
                &image,
                middle,
                digit.y1 + height / 3,
                height / 3,
                Direction::Vertical,
                Rgba([0, 255, 0, 255]),
                thresh,
                config.lt,
                config.flags,
                config.foreground,
            );
            if found >= config.need_pixels as u32 {
                digit.digit |= HORIZ_MID;
            }

            // Check HORIZ_DOWN
            let found = scanline(
                &mut debug_image,
                &image,
                middle,
                digit.y1 + 2 * height / 3,
                height / 3,
                Direction::Vertical,
                Rgba([0, 0, 255, 255]),
                thresh,
                config.lt,
                config.flags,
                config.foreground,
            );
            if found >= config.need_pixels as u32 {
                digit.digit |= HORIZ_DOWN;
            }

            // Check VERT_LEFT_UP
            let found = scanline(
                &mut debug_image,
                &image,
                digit.x1,
                quarter,
                width / 2,
                Direction::Horizontal,
                Rgba([255, 0, 0, 255]),
                thresh,
                config.lt,
                config.flags,
                config.foreground,
            );
            if found >= config.need_pixels as u32 {
                digit.digit |= VERT_LEFT_UP;
            }

            // Check VERT_RIGHT_UP
            let found = scanline(
                &mut debug_image,
                &image,
                middle + 1,
                quarter,
                width / 2 - 1,
                Direction::Horizontal,
                Rgba([0, 255, 0, 255]),
                thresh,
                config.lt,
                config.flags,
                config.foreground,
            );
            if found >= config.need_pixels as u32 {
                digit.digit |= VERT_RIGHT_UP;
            }

            // Check VERT_LEFT_DOWN
            let found = scanline(
                &mut debug_image,
                &image,
                digit.x1,
                three_quarters,
                width / 2,
                Direction::Horizontal,
                Rgba([255, 0, 0, 255]),
                thresh,
                config.lt,
                config.flags,
                config.foreground,
            );
            if found >= config.need_pixels as u32 {
                digit.digit |= VERT_LEFT_DOWN;
            }

            // Check VERT_RIGHT_DOWN
            let found = scanline(
                &mut debug_image,
                &image,
                middle + 1,
                three_quarters,
                width / 2 - 1,
                Direction::Horizontal,
                Rgba([0, 255, 0, 255]),
                thresh,
                config.lt,
                config.flags,
                config.foreground,
            );
            if found >= config.need_pixels as u32 {
                digit.digit |= VERT_RIGHT_DOWN;
            }
        }
    }

    // Space check
    if (config.flags & defines::PRINT_SPACES != 0) && digits.len() > 2 {
        if config.flags & defines::DEBUG_OUTPUT != 0 {
            eprintln!("looking for white space");
        }

        let mut min_dst = digits[1].x2 - digits[0].x2;
        let mut dst_sum = min_dst;

        if config.flags & defines::DEBUG_OUTPUT != 0 {
            eprintln!(" distance between digits 0 and 1 is {}", min_dst);
        }

        for i in 2..digits.len() {
            let cur_dst = digits[i].x2 - digits[i - 1].x2;
            if config.flags & defines::DEBUG_OUTPUT != 0 {
                eprintln!(
                    " distance between digits {} and {} is {}",
                    i - 1,
                    i,
                    cur_dst
                );
            }
            if cur_dst < min_dst {
                min_dst = cur_dst;
            }
            dst_sum += cur_dst;
        }

        let avg_dst = dst_sum / (digits.len() - 1) as i32;
        let base_dst = if config.flags & defines::SPC_USE_AVG_DST != 0 {
            avg_dst
        } else {
            min_dst
        };
        let base_dst = base_dst.max(1);

        if config.flags & defines::DEBUG_OUTPUT != 0 {
            eprintln!(" minimum digit distance: {}", min_dst);
            eprintln!(" average digit distance: {}", avg_dst);
            eprintln!(
                " adding spaces for distance greater than: {}",
                (config.spc_fac * base_dst as f64) as i32
            );
        }

        for i in 0..(digits.len() - 1) {
            let num_spc = ((digits[i + 1].x2 - digits[i].x2) as f64
                / (config.spc_fac * base_dst as f64)) as i32;
            if num_spc > 0 {
                if config.flags & defines::DEBUG_OUTPUT != 0 {
                    eprintln!(" adding {} space character(s) after digit {}", num_spc, i);
                }
                digits[i].spaces = num_spc;
            }
        }
    }

    // ASCII Art Segment output
    if config.flags & (defines::DEBUG_OUTPUT | defines::ASCII_ART_SEGMENTS) != 0 {
        eprintln!("Display as seen by ssocr:");
        // top row
        for digit in &digits {
            eprint!("  ");
            if digit.digit & HORIZ_UP != 0 {
                eprint!("_");
            } else {
                eprint!(" ");
            }
            eprint!(" ");
            eprint_spaces(digit.spaces * 3);
        }
        eprintln!();
        // middle row
        for digit in &digits {
            eprint!(" ");
            if digit.digit & VERT_LEFT_UP != 0 {
                eprint!("|");
            } else {
                eprint!(" ");
            }
            if digit.digit & HORIZ_MID != 0 || digit.digit == D_MINUS {
                eprint!("_");
            } else {
                eprint!(" ");
            }
            if digit.digit & VERT_RIGHT_UP != 0 {
                eprint!("|");
            } else {
                eprint!(" ");
            }
            eprint_spaces(digit.spaces * 3);
        }
        eprintln!();
        // bottom row
        for digit in &digits {
            eprint!(" ");
            if digit.digit & VERT_LEFT_DOWN != 0 {
                eprint!("|");
            } else {
                eprint!(" ");
            }
            if digit.digit & HORIZ_DOWN != 0 {
                eprint!("_");
            } else if digit.digit == D_DECIMAL {
                eprint!(".");
            } else {
                eprint!(" ");
            }
            if digit.digit & VERT_RIGHT_DOWN != 0 {
                eprint!("|");
            } else {
                eprint!(" ");
            }
            eprint_spaces(digit.spaces * 3);
        }
        eprintln!("\n");
    }

    // Final result output
    let mut unknown_digit = 0;
    if config.flags & defines::PRINT_AS_HEX != 0 {
        for (i, digit) in digits.iter().enumerate() {
            if i > 0 {
                print!(":");
            }
            print!("{:02x}", digit.digit);
            print_spaces(digit.spaces);
        }
    } else {
        let charset_array = init_charset(config.charset);
        for digit in &digits {
            unknown_digit += print_digit(digit.digit, config.flags, &charset_array);
            print_spaces(digit.spaces);
        }
    }
    println!();

    // Save debug image if requested
    if let Some(ref debug_file) = config.debug_image_file
        && let Some(debug_img) = &debug_image {
            debug_img.save(debug_file)?;
        }

    if unknown_digit > 0 {
        std::process::exit(2);
    } else {
        std::process::exit(0);
    }
}
