tato_pipe 0.1.3

Converts PNG files to binary data for the Tato game engine
Documentation
use std::collections::HashMap;

use crate::*;
use image::{io::Reader as ImageReader, DynamicImage};


// Contains a palettized version of an image as well as anim data
#[derive(Debug)]
pub(crate) struct ImageBuilder {
    // pub asset_name: String,
    pub frames_h:u8,
    pub frames_v:u8,
    pub cols_per_frame:u8,
    pub rows_per_frame:u8,
    pub width:usize,
    pub height:usize,
    pub pixels :Vec<u8>,
}


impl ImageBuilder {

    pub fn from_image(specs:Specs, file_name:&str, frames_layout: Option<(u8, u8)>, palette:&mut Palette, palette_hash:&mut HashMap<Color32, u8>) -> ImageBuilder {

        let split = file_name.split('/');
        let last = split.last().unwrap().to_string();
        let mut last_split = last.split('.');
        let asset_name = last_split.next().unwrap().to_string();

        println!("cargo:warning=Converting image {}", asset_name);
        let mut img_rgba = ImageReader::open(file_name)
            .unwrap()
            .decode()
            .unwrap();
        if let DynamicImage::ImageRgba8{..} = img_rgba {
            println!("cargo:warning=Image for '{}' is Rgba8, proceeding... ", asset_name);
        } else {
            println!("cargo:warning=Image for '{}' is not Rgba8, converting... ", asset_name);
            img_rgba = DynamicImage::from(img_rgba.to_rgba8());
        }

        let (width,height) = (img_rgba.width() as usize, img_rgba.height() as usize);
        if ((width % specs.tile_width as usize) != 0) || ((height % specs.tile_height as usize) != 0) {
            panic!("Build error: PNG image cannot fit into {}x{} tiles!", specs.tile_width, specs.tile_height)
        }

        let (cols_per_frame, rows_per_frame) = match frames_layout {
            Some(value) => (value.0, value.1),
            None => (1, 1),
        };

        let frames_h = u8::try_from(
            (width / cols_per_frame as usize)/specs.tile_width as usize
        ).ok().unwrap();

        let frames_v = u8::try_from(
            (height / rows_per_frame as usize)/specs.tile_height as usize
        ).ok().unwrap();

        println!(
            "cargo:warning=    Tilifying '{}' to {} frames with {}x{} tiles",
            asset_name,
            frames_h as usize * frames_v as usize,
            cols_per_frame,
            rows_per_frame
        );

        ImageBuilder {
            // asset_name,
            frames_h,
            frames_v,
            cols_per_frame,
            rows_per_frame,
            width,
            height,
            pixels: Self::palletize(img_rgba, palette, palette_hash),
        }
    }


// Populates the palettized, 1 byte-per-pixel image from a source RGBA image.
pub fn palletize(img:DynamicImage, palette:&mut Palette, color_hash:&mut HashMap<Color32, u8>) -> Vec<u8> {
    let mut pixels = vec![];
    for y in 0 .. img.height() as usize{
        for x in 0 .. img.width() as usize{
            let color = {
                let buf = img.as_bytes();
                let index = x + (y * img.width() as usize);
                let buf_index = index * 4;
                let r = buf[buf_index];
                let g = buf[buf_index+1];
                let b = buf[buf_index+2];
                let a = buf[buf_index+3];
            
                let rgba_color = if a < 255 {
                    Color32{r:0, g:0, b:0, a:0}  // Ensures all transp. colors are always the same in the hashmap.
                } else {
                    Color32{r, g, b, a}
                };

                // Result
                if color_hash.contains_key(&rgba_color) {
                    *color_hash.get(&rgba_color).unwrap()
                } else {
                    let color_head = u8::try_from(color_hash.len()).ok().unwrap();
                    println!("cargo:warning=    Inserting Palette {:02} -> {:02}: {:?}", palette.id(), color_head, rgba_color);
                    color_hash.insert(rgba_color, color_head);
                    palette.push(rgba_color);
                    color_head   
                }

            };
            pixels.push(color)
        }
    }
    pixels
}

}




// Json must be in Aseprite export format
// fn read_anim_from_json(path:PathBuf) -> Result<HashMap<String, Vec<u8>>, Error> {
//     let mut result = HashMap::new();

//     let text = std::fs::read_to_string(path)?;
//     let parsed:JsonValue = text.parse().unwrap();
//     let json: HashMap<_, _> = parsed.try_into().unwrap();

//     // Aseprite parsing
//     let meta = &json["meta"];
//     let frame_tags = &meta["frameTags"];
//     if let JsonValue::Array(tags) = frame_tags {
//         for tag in tags.iter() {
//             let JsonValue::String(name) = &tag["name"] else { break };
//             let JsonValue::Number(head) = &tag["from"] else { break };
//             let JsonValue::Number(tail) = &tag["to"] else { break };            
//             let Ok(head) = u8::try_from(*head as usize) else { break };
//             let Ok(tail) = u8::try_from(*tail as usize) else { break };

//             let range = (head ..= tail).collect(); // indices start at 0! (not in atlas space)
//             result.insert(name.clone(), range);
//         }
//     }

//     Ok(result)
// }


// OLD


// pub fn convert_sprite(file_name:&str, frame_cols:usize, frame_rows:usize, sub_palette:u8) {
//     // Load image
//     let img = AtlasBuilder::from_image(file_name, Some((frame_cols, frame_rows)), sub_palette);

//     // split into tiles, remove redundant tiles and save resulting unique tiles
//     let (img_bytes, tile_ids) = tilify(&img);
//     write( format!("{ASSET_DEST}{file_name}.pix"), img_bytes.as_slice()).unwrap();

//     // Use JSON data to save each animation into a separate file
//     if let Some(anims) = img.anims {
//         for (name, anim) in anims {
//             if name.is_empty() { continue }
//             let mut tiles_per_anim = vec![];
//             let frame_size = frame_cols * frame_rows;
//             for frame in anim {
//                 let offset = frame_size * frame as usize;
//                 // Push all tiles per frame
//                 for index in 0 .. frame_size {
//                     tiles_per_anim.push(tile_ids[index + offset])
//                 }
//             }
//             write(
//                 format!("{ASSET_DEST}{file_name}_{name}.anim"),
//                 tiles_per_anim.as_slice()
//             ).unwrap();
//         }
//     }
// }



// pub fn convert_tiles(file_name:&str, sub_palette:u8, save_map:bool) {
//     // Load image
//     let img = AtlasBuilder::from_image(file_name, None, sub_palette);

//     // split into tiles, remove redundant tiles and save resulting unique tiles
//     let (img_bytes, tile_ids) = tilify(&img);
//     write( format!("{ASSET_DEST}{file_name}.pix"), img_bytes.as_slice()).unwrap();

//     if save_map {
//         write( format!("{ASSET_DEST}{file_name}.map"), tile_ids.as_slice()).unwrap();
//     }
// }