tatlap 0.1.1

Texture atlas packer
Documentation
use image::GenericImage;
use img_hash::HasherConfig;

use crate::entry::{Entry, EntryRect};

struct Rect {
    x: u32,
    y: u32,
    w: u32,
    h: u32,
}

impl Rect {
    pub fn contains(&self, other: &Entry) -> bool {
        !(self.w < other.w() || self.h < other.h())
    }
}

fn find_space(spaces: &Vec<Rect>, entry: &Entry) -> Option<usize> {
    for (i, s) in spaces.iter().enumerate().rev() {
        if s.contains(entry) {
            return Some(i);
        }
    }
    None
}

pub struct Channel {
    pub entries: Vec<Entry>,
    pub w: u32,
    pub h: u32,
    pub num_pixels: u32,
    pub num_packed: u32,
    bpp: u8,
}

impl Channel {
    pub fn new(bpp: u8) -> Self {
        Self {
            entries: Vec::new(),
            w: 0,
            h: 0,
            num_packed: 0,
            num_pixels: 0,
            bpp,
        }
    }

    fn calculate_size(&self) -> u32 {
        let mut s = 0;
        for e in &self.entries {
            s += e.w() * e.h();
        }
        s
    }

    pub fn estimate_width(&self) -> u32 {
        let size = self.calculate_size();
        ((size as f32).powf(0.5) * 1.1) as u32
    }

    fn get_default_sizes(&self, w: u32, h: u32) -> (u32, u32) {
        let s = self.estimate_width();
        (if w == 0 { s } else { w }, if h == 0 { s * 2 } else { h })
    }

    pub fn pack_rows(&mut self, max_width: u32, max_height: u32) {
        let (max_width, max_height) = self.get_default_sizes(max_width, max_height);
        // Sort entries by height
        self.entries.sort_by(|a, b| b.h().cmp(&a.h()));

        let mut xpos = 032;
        let mut ypos = 0u32;
        let mut largest_height_this_row = 0u32;

        for e in &mut self.entries {
            // Check if we need to start a new row
            if xpos + e.w() > max_width {
                self.w = self.w.max(xpos);
                ypos += largest_height_this_row;
                xpos = 0;
                largest_height_this_row = 0;
            }

            // Check if there's still y space
            if ypos + e.h() > max_height {
                println!("Couldn't fit them all!");
                continue;
            }

            e.packed = Some(EntryRect { x: xpos, y: ypos });
            xpos += e.w();
            self.num_pixels += e.w() * e.h();
            self.num_packed += 1;

            // ypos += e.h;
            if e.h() > largest_height_this_row {
                largest_height_this_row = e.h();
            }
        }

        self.h = ypos + largest_height_this_row;
        assert!(self.h <= max_height);
    }

    pub fn pack_splits(&mut self, max_width: u32, max_height: u32) {
        let (max_width, max_height) = self.get_default_sizes(max_width, max_height);

        self.entries.sort_by(|a, b| b.h().cmp(&a.h()));

        let shortest = self.entries.last().unwrap().h();

        let mut empty_spaces = Vec::new();

        empty_spaces.push(Rect {
            x: 0,
            y: 0,
            w: max_width,
            h: max_height,
        });

        for e in &mut self.entries {
            e.packed = None;

            let w = e.w();
            let h = e.h();

            // Get an empty space
            let index = match find_space(&empty_spaces, &e) {
                Some(v) => v,
                None => continue,
            };

            // Remove the space
            let space = empty_spaces.remove(index);

            let r = EntryRect {
                x: space.x,
                y: space.y,
            };
            self.num_pixels += w * h;
            self.num_packed += 1;

            // Update the final image size
            self.w = self.w.max(r.x + e.w());
            self.h = self.h.max(r.y + e.h());

            e.packed = Some(r);

            // Split the space. If image fits perfectly, don't add a new split.
            if space.w - w <= (space.h - h) * 7 && space.h - h >= shortest {
                /*
                 * Split along the x axis, but avoid making long strips.
                 * +-----------+
                 * |image|small|
                 * +-----------+
                 * |   big     |
                 * +-----------+
                 */
                // Big
                if space.h != h {
                    empty_spaces.push(Rect {
                        x: space.x,
                        y: space.y + h,
                        w: space.w,
                        h: space.h - h,
                    });
                }
                // Small
                if space.w != w {
                    empty_spaces.push(Rect {
                        x: space.x + w,
                        y: space.y,
                        w: space.w - w,
                        h,
                    });
                }
            } else {
                /*
                 * The leftover space is much wider than tall.
                 * +-----------+
                 * |image|     |
                 * +-----+ big |
                 * |small|     |
                 * +-----+-----+
                 */
                // Big
                if space.w != w {
                    empty_spaces.push(Rect {
                        x: space.x + w,
                        y: space.y,
                        w: space.w - w,
                        h: space.h,
                    });
                }
                // Small
                if space.h != h {
                    empty_spaces.push(Rect {
                        x: space.x,
                        y: space.y + h,
                        w,
                        h: space.h - h,
                    });
                }
            }
            // Sort the empty spaces. Takes a bit longer but gives about
            // 2% better packing ratio.
            // height
            // empty_spaces[index..].sort_by(|a, b| b.h.cmp(&a.h));
            // area
            // empty_spaces[index..].sort_by(|a, b| (b.h*b.w).cmp(&(a.h*a.w)));
            // perimeter
            empty_spaces[index..].sort_by(|a, b| (b.h + b.w).cmp(&(a.h + a.w)));
        }
    }

    pub fn add_entry(&mut self, e: Entry) {
        assert!(e.image.color().bytes_per_pixel() == self.bpp);
        self.entries.push(e);
    }

    fn get_image(&self) -> image::DynamicImage {
        match self.bpp {
            1 => image::DynamicImage::new_luma8(self.w, self.h),
            2 => image::DynamicImage::new_luma_a8(self.w, self.h),
            3 => image::DynamicImage::new_rgb8(self.w, self.h),
            _ => image::DynamicImage::new_rgba8(self.w, self.h),
        }
    }

    pub fn save_image(&self, name: &str) -> image::ImageResult<()> {
        if self.w > 0 && self.h > 0 {
            let mut image = self.get_image();

            for e in &self.entries {
                if let Some(r) = &e.packed {
                    image.copy_from(&e.image, r.x, r.y)?;
                }
            }

            image.save(format!("{}{}.png", name, self.bpp))?;
        }
        Ok(())
    }

    pub fn remove_duplicates(&mut self) {
        let hasher = HasherConfig::new();
        let hasher = hasher.hash_size(16, 16);
        let hasher = hasher.to_hasher();

        let mut hashes = Vec::new();

        let mut num_dups = 0;

        let mut i = 0;
        while i < self.entries.len() {
            assert_eq!(i, hashes.len());
            // Get this image's hash
            let hash = hasher.hash_image(&self.entries[i].image);

            // Try to find a matching hash in all previous hashes
            if let Some((j, _)) = hashes.iter().enumerate().find(|(j, h)| {
                self.entries[i].w() == self.entries[*j].w()
                    && self.entries[i].h() == self.entries[*j].h()
                    && hash.dist(h) == 0
            }) {
                // If found, remove this entry
                let e = self.entries.swap_remove(i);
                assert_eq!(e.w(), self.entries[j].w());
                assert_eq!(e.h(), self.entries[j].h());
                // And append it's users to the duplicate
                self.entries[j].users.extend(e.users);
                num_dups += 1;
            } else {
                // Otherwise add this hash to the list
                hashes.push(hash);
                // And check the next one
                i += 1;
            }
        }
        println!("Found {} duplicates in {}", num_dups, self.bpp);
    }
}