asefile/util.rs
1//! Utilities not directly related to Aseprite, but useful for processing the
2//! resulting image data. (Requires feature `utils`.)
3//!
4//! This module is not available by default. To use it, you must enable the
5//! feature `utils` in your `Cargo.toml`.
6//!
7//! ```toml
8//! [dependencies]
9//! asefile = { version = "0.3", features = ["utils"] }
10//! ```
11
12use image::RgbaImage;
13use nohash::IntMap;
14use std::iter::once;
15
16use crate::ColorPalette;
17
18/// Add a 1 pixel border around the input image by duplicating the outmost
19/// pixels.
20///
21/// This can be useful when creating a texture atlas for sprites that represent
22/// tiles. Without this, under certain zoom levels there might be small gaps
23/// between tiles. For an example, see this [discussion of the problem on
24/// StackOverflow][1].
25///
26/// [1]: https://gamedev.stackexchange.com/questions/148247/prevent-tile-layout-gaps
27///
28/// Many sprite atlas generation tools have this as a built-in feature. In that
29/// case you don't need to use this function.
30pub fn extrude_border(image: RgbaImage) -> RgbaImage {
31 let (w, h) = image.dimensions();
32 let w = w as usize;
33 let h = h as usize;
34 let src = image.as_raw();
35 let bpp = 4; // bytes per pixel
36 let mut data: Vec<u8> = Vec::with_capacity(4 * (w + 2) * (h + 2));
37 let bpp_w = bpp * w;
38 for src_row in once(0).chain(0..h).chain(once(h - 1)) {
39 let ofs = src_row * bpp * w;
40 data.extend_from_slice(&src[ofs..ofs + bpp]); // (0, r)
41 data.extend_from_slice(&src[ofs..ofs + bpp_w]); // (0, r)..(w-1, r);
42 data.extend_from_slice(&src[ofs + bpp_w - bpp..ofs + bpp_w]);
43 }
44 RgbaImage::from_raw((w + 2) as u32, (h + 2) as u32, data).unwrap()
45}
46
47/// A helper for mapping `Rgba` values into indexes in a color palette.
48pub struct PaletteMapper {
49 map: IntMap<u32, u8>,
50 transparent: u8,
51 failure: u8,
52}
53
54/// Configuration of palette mapping.
55pub struct MappingOptions {
56 /// If pixel is not in the palette, use this index.
57 pub failure: u8,
58 /// If pixel is transparent (`alpha != 255`), use this index. If `None`
59 /// transparent pixels are treated as failures.
60 pub transparent: Option<u8>,
61}
62
63impl PaletteMapper {
64 /// Create a new mapper from a color palette.
65 pub fn new(palette: &ColorPalette, options: MappingOptions) -> PaletteMapper {
66 let mut map = IntMap::default();
67 for (idx, entry) in palette.entries.iter() {
68 let m =
69 entry.red() as u32 + ((entry.green() as u32) << 8) + ((entry.blue() as u32) << 16);
70 let col = if *idx < 256 {
71 *idx as u8
72 } else {
73 options.failure
74 };
75 let _ = map.insert(m, col);
76 }
77 PaletteMapper {
78 map,
79 transparent: options.transparent.unwrap_or(options.failure),
80 failure: options.failure,
81 }
82 }
83
84 /// Look up a color in the palette.
85 ///
86 /// An `alpha` other than `255` is considered transparent. If the color
87 /// is not in the palette returns the failure color.
88 pub fn lookup(&self, r: u8, g: u8, b: u8, alpha: u8) -> u8 {
89 if alpha != 255 {
90 return self.transparent;
91 }
92 let m = r as u32 + ((g as u32) << 8) + ((b as u32) << 16);
93 *self.map.get(&m).unwrap_or(&self.failure)
94 }
95}
96
97/// Turn an `RgbaImage` into an indexed image.
98///
99/// Returns image dimensions and raw index data.
100///
101/// # Example
102///
103/// ```
104/// # use asefile::AsepriteFile;
105/// # use std::path::Path;
106/// # let asefile_path = Path::new("./tests/data/util_indexed.aseprite");
107/// # let output_dir = Path::new("./tests/data");
108/// # let ase = AsepriteFile::read_file(&asefile_path).unwrap();
109/// use asefile::util::{PaletteMapper, MappingOptions, to_indexed_image};
110/// let img = ase.frame(0).image();
111/// assert!(ase.is_indexed_color());
112/// let mapper = PaletteMapper::new(
113/// ase.palette().unwrap(),
114/// MappingOptions {
115/// transparent: ase.transparent_color_index(),
116/// failure: 0,
117/// }
118/// );
119/// let ((w, h), data) = to_indexed_image(img, &mapper);
120/// assert_eq!(data.len(), (w * h) as usize);
121/// ```
122pub fn to_indexed_image(image: RgbaImage, mapper: &PaletteMapper) -> ((u32, u32), Vec<u8>) {
123 let data = image
124 .pixels()
125 .map(|c| mapper.lookup(c.0[0], c.0[1], c.0[2], c.0[3]))
126 .collect();
127 (image.dimensions(), data)
128}