redguard-preservation 0.6.2

A CLI tool to preserve Redguard files
Documentation
use crate::error::Error;
use crate::import::palette::Palette;
use crate::import::png::save_png;
use image::{DynamicImage, Rgb, RgbImage};
use serde_json::json;
use std::path::{Path, PathBuf};

/// Output file paths generated by palette export.
#[derive(Debug, Clone)]
pub struct PaletteExportPaths {
    pub png_path: PathBuf,
    pub json_path: PathBuf,
}

/// Cell size in pixels for each palette swatch.
const CELL_SIZE: u32 = 16;
/// Grid dimension (16x16 = 256 entries).
const GRID_DIM: u32 = 16;

fn ensure_parent_dir(path: &Path) -> Result<(), Error> {
    if let Some(parent) = path.parent()
        && !parent.as_os_str().is_empty()
    {
        std::fs::create_dir_all(parent).map_err(|e| Error::File {
            path: parent.to_path_buf(),
            message: format!("failed to create directory: {e}"),
        })?;
    }
    Ok(())
}

fn paired_output_paths(output: &Path) -> (PathBuf, PathBuf) {
    let ext = output
        .extension()
        .and_then(|e| e.to_str())
        .map(str::to_ascii_lowercase);

    match ext.as_deref() {
        Some("png") => (output.to_path_buf(), output.with_extension("json")),
        Some("json") => (output.with_extension("png"), output.to_path_buf()),
        Some(_) => (output.with_extension("png"), output.with_extension("json")),
        None => {
            let mut png = output.to_path_buf();
            png.set_extension("png");
            let mut json_path = output.to_path_buf();
            json_path.set_extension("json");
            (png, json_path)
        }
    }
}

/// Exports a parsed COL palette as a 16x16 swatch PNG and a JSON metadata file.
pub fn export_col_palette(
    palette: &Palette,
    output_path: &Path,
    compress: bool,
) -> Result<PaletteExportPaths, Error> {
    let img_size = GRID_DIM * CELL_SIZE;
    let mut image = RgbImage::new(img_size, img_size);

    for (idx, color) in palette.colors.iter().enumerate() {
        let idx_u32 = u32::try_from(idx).map_err(|_| {
            Error::Conversion("palette index overflow while exporting swatches".to_string())
        })?;
        let col = idx_u32 % GRID_DIM;
        let row = idx_u32 / GRID_DIM;
        let origin_x = col * CELL_SIZE;
        let origin_y = row * CELL_SIZE;

        let pixel = Rgb([color[0], color[1], color[2]]);
        for y in 0..CELL_SIZE {
            for x in 0..CELL_SIZE {
                image.put_pixel(origin_x + x, origin_y + y, pixel);
            }
        }
    }

    let (png_path, json_path) = paired_output_paths(output_path);
    ensure_parent_dir(&png_path)?;
    ensure_parent_dir(&json_path)?;

    save_png(&DynamicImage::ImageRgb8(image), &png_path, compress)
        .map_err(|e| Error::Conversion(e.to_string()))?;

    let colors: Vec<serde_json::Value> = palette
        .colors
        .iter()
        .enumerate()
        .map(|(i, c)| {
            json!({
                "index": i,
                "r": c[0],
                "g": c[1],
                "b": c[2],
                "hex": format!("#{:02X}{:02X}{:02X}", c[0], c[1], c[2])
            })
        })
        .collect();

    let metadata = json!({
        "format": "redguard-col-v1",
        "entry_count": palette.colors.len(),
        "colors": colors
    });

    let json_text = serde_json::to_string_pretty(&metadata)?;
    std::fs::write(&json_path, json_text)?;

    Ok(PaletteExportPaths {
        png_path,
        json_path,
    })
}