1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
use crate::error::*;
use image::{DynamicImage, GenericImageView, ImageFormat, Rgba};
use serde::{Deserialize, Serialize};
use std::{fs::File, path::PathBuf};

#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct ImageGeneration {
    /// The path to the source icon will be provided to generate mipmap resources.
    pub icon_path: PathBuf,
    /// The output name of the icon that will be generated in mipmap resources.
    pub out_icon_name: String,
    /// Output path to Android resources for generated mipmap resources.
    pub output_path: PathBuf,
    /// Overwrite android resource directory.
    pub force: bool,
}

impl ImageGeneration {
    /// Generate mipmap resources from the icon. Width and height of the icon must be
    /// equal.
    pub fn gen_mipmap_res_from_icon(&self) -> Result<()> {
        let image = image::open(&self.icon_path)?;
        let (width, height) = image.dimensions();
        if width != height || height % 2 != 0 {
            return Err(Error::WidthAndHeightDifSizes);
        }
        for (name, size) in get_icon_sizes() {
            let scaled = image.thumbnail(size, size);
            let img = Self::round_image(&scaled, size);
            // TODO: Add shadow. See this: https://github.com/romannurik/AndroidAssetStudio
            self.write_image(&name, img)?;
        }
        Ok(())
    }

    fn round_image(scaled: &DynamicImage, size: u32) -> DynamicImage {
        let border = size as f64 / 25.0;
        let radius = size as f64 / 2.0;
        let cxy = size as f64 / 2.0 - 1.0;
        let mut img = scaled.to_rgba8();
        for pix in img.clone().enumerate_pixels() {
            let s = (pix.0 as f64 - cxy).hypot(pix.1 as f64 - cxy);
            if s > radius - border {
                img.put_pixel(pix.0, pix.1, Rgba([0, 0, 0, 0]));
            }
        }
        img.into()
    }

    /// Check res directory and then create mipmap resource if it's empty
    pub fn write_image(&self, mipmap_name: &str, scaled: DynamicImage) -> Result<()> {
        let mipmap_dirs = self.output_path.join(format!("mipmap-{}", mipmap_name));
        if mipmap_dirs.exists() {
            if self.force {
                std::fs::remove_dir_all(&mipmap_dirs).ok();
                std::fs::create_dir_all(&mipmap_dirs)?;
            }
        } else {
            std::fs::create_dir_all(&mipmap_dirs)?;
        }
        let mut output = File::create(mipmap_dirs.join(&self.out_icon_name))?;
        scaled.write_to(&mut output, ImageFormat::Png)?;
        Ok(())
    }
}

/// Scale image down according to scale ratio.
fn get_icon_sizes() -> Vec<(String, u32)> {
    vec![
        (MipmapDpi::Xxxhdpi.to_string(), 192),
        (MipmapDpi::Xxhdpi.to_string(), 144),
        (MipmapDpi::Xhdpi.to_string(), 96),
        (MipmapDpi::Hdpi.to_string(), 72),
        (MipmapDpi::Mdpi.to_string(), 48),
        (MipmapDpi::Ldpi.to_string(), 36),
    ]
}

pub enum MipmapDpi {
    Xxxhdpi,
    Xxhdpi,
    Xhdpi,
    Hdpi,
    Mdpi,
    Ldpi,
}

impl ToString for MipmapDpi {
    fn to_string(&self) -> String {
        match self {
            Self::Xxxhdpi => "xxxhdpi".to_string(),
            Self::Xxhdpi => "xxhdpi".to_string(),
            Self::Xhdpi => "xhdpi".to_string(),
            Self::Hdpi => "hdpi".to_string(),
            Self::Mdpi => "mdpi".to_string(),
            Self::Ldpi => "ldpi".to_string(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::env::current_dir;

    #[test]
    fn test_icon_gen() {
        let tempfile = tempfile::tempdir().unwrap();
        let res_dir_path = tempfile.path().to_path_buf();
        let icon_path = current_dir()
            .unwrap()
            .parent()
            .unwrap()
            .parent()
            .unwrap()
            .join("assets")
            .join("images")
            .join("icon.png");
        let image_generation = ImageGeneration {
            icon_path,
            out_icon_name: "ic_launcher.png".to_owned(),
            output_path: res_dir_path.join("res"),
            force: false,
        };
        image_generation.gen_mipmap_res_from_icon().unwrap();
        assert!(res_dir_path
            .join("res")
            .join("mipmap-hdpi")
            .join("ic_launcher.png")
            .exists())
    }
}