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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
//! Avatar module helps to generate avatars according to the initial names.
use rusttype::{point, Font, Scale};
use image::{DynamicImage, Rgba, ImageBuffer};
use std::io::prelude::*;
use std::fs::File;
use std::cmp;
use hex;
use contrast;
use error::Error;
/// Avatar builder that stores the metrics of the image.
#[derive(Debug)]
pub struct AvatarBuilder {
/// Initials name string
pub name: String,
/// Vectorized font data
pub font_data: Vec<u8>,
/// Scale of the font
pub font_scale: Scale,
/// RGB color of the font
pub font_color: Vec<i64>,
/// RGB color of the background
pub background_color: Vec<i64>,
/// Size of the inner-text
pub length: usize,
/// Width of the avatar
pub width: u32,
/// Height of the avatar
pub height: u32,
/// Contrast ratio for the colors
pub contrast_ratio: f32,
/// Private property to hold if colors should be randomly generated
randomized_colors: (bool, bool)
}
/// Result type for the avatar generator
pub type AvatarResult = Result<AvatarBuilder, Error>;
/// Generate vectorized RGB color with range 0 to 255
fn generate_random_rgb() -> Vec<i64> {
use rand::prelude::*;
let mut rng = thread_rng();
let mut vec = Vec::with_capacity(3);
for _ in 0..3 {
vec.push(rng.gen_range(0, 256));
}
vec
}
impl AvatarBuilder {
/// Construct new AvatarBuilder.
pub fn new(name: &str) -> AvatarBuilder {
// unwrap first chars for the each word and store them
// inside the <String>
let mut text = String::new();
for word in name.split_whitespace() {
text.push(word.chars().next().unwrap());
}
// default Avatar settings
AvatarBuilder {
name: text.to_uppercase(),
font_data: include_bytes!("fonts/Hiragino_Sans_GB_W3.ttf").to_vec(),
font_scale: Scale::uniform(150.0),
length: 2,
width: 300,
height: 300,
randomized_colors: (true, true),
contrast_ratio: 4.5,
font_color: vec![255, 255, 255], // default white color
background_color: vec![224, 143, 112], // default background
}
}
/// Change the font of the avatar text. You need to include `.ttf` file.
/// Default style is `Hiragino_Sans`.
pub fn with_font(mut self, font: &str) -> AvatarResult {
let mut f = File::open(font)?;
let mut font_data = Vec::new();
f.read_to_end(&mut font_data).expect("unable to read data");
self.font_data = font_data;
Ok(self)
}
/// Change the font color. You need to specify hex color code.
pub fn with_font_color(mut self, color: &str) -> AvatarResult {
let font_color = hex::parse_hex(color)?;
self.font_color = font_color;
self.randomized_colors.0 = false;
Ok(self)
}
/// Change the uniform scale of font.
/// Default to `150.0`.
pub fn with_font_scale(mut self, scale: f32) -> AvatarResult {
self.font_scale = Scale::uniform(scale);
Ok(self)
}
/// Change the background color of the avatar. You need to specify hex color code.
pub fn with_background_color(mut self, color: &str) -> AvatarResult {
let background_color = hex::parse_hex(color)?;
self.background_color = background_color;
self.randomized_colors.1 = false;
Ok(self)
}
/// Change the length of initials characters taken from the name.
/// Default to `2`.
pub fn with_length(mut self, length: usize) -> AvatarResult {
self.length = length;
Ok(self)
}
/// Change the width of the avatar.
/// Default to `300`.
pub fn with_width(mut self, width: u32) -> AvatarResult {
self.width = width;
Ok(self)
}
/// Change the height of the avatar.
/// Default to `300`.
pub fn with_height(mut self, height: u32) -> AvatarResult {
self.height = height;
Ok(self)
}
/// Change the contrast ratio for the randomly generated avatar.
/// Default to `4.5`. Increase the ratio for more clear avatar.
pub fn with_contrast_ratio(mut self, ratio: f32) -> AvatarResult {
self.contrast_ratio = ratio;
Ok(self)
}
/// Draw the image according to the metrics given.
pub fn draw(self) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
// convert font-data vector to rusttype::Font
let font = Font::from_bytes(&self.font_data as &[u8]).expect("Error constructing Font");
// substract metrics from the font according to the font scale
let v_metrics = font.v_metrics(self.font_scale);
// get the number of characters from the given name
let text: String = self.name
.chars()
.take(cmp::min(self.length, self.name.len()))
.collect();
// layout the glyphs
let glyphs: Vec<_> = font
.layout(&text, self.font_scale, point(0.0, v_metrics.ascent))
.collect();
// substract height/width from the glyphs
let glyphs_height = (v_metrics.ascent - v_metrics.descent).ceil() as u32;
let glyphs_width = glyphs
.iter()
.rev()
.map(|g| g.position().x as f32 + g.unpositioned().h_metrics().advance_width)
.next()
.unwrap_or(0.0)
.ceil() as u32;
// calculate padding for glyphs
let left_padding = (self.width - glyphs_width) / 2;
let top_padding = (self.height - glyphs_height) / 2;
// create dynamic RGBA image
let mut image = DynamicImage::new_rgba8(self.width, self.height).to_rgba();
// randomize colors if not being settled
let mut colors = self.randomized_colors.clone();
let mut background_color = self.background_color.clone();
let mut font_color = self.font_color.clone();
loop {
match colors {
(false, false) => break,
(_, _) => {
if colors.0 {
font_color = generate_random_rgb();
}
if colors.1 {
background_color = generate_random_rgb();
}
colors = match contrast::find_ratio(&font_color, &background_color) {
// match if contrast ratio between colors is as expected
r if r > self.contrast_ratio || r < 1. / self.contrast_ratio => (false, false),
_ => {
if colors.0 | colors.1 {
colors
} else {
(false, true)
}
}
}
},
}
}
for glyph in glyphs {
if let Some(bounding_box) = glyph.pixel_bounding_box() {
// draw the glyph into the image according to font color
glyph.draw(|x, y, v| {
image.put_pixel(
x + bounding_box.min.x as u32 + left_padding,
y + bounding_box.min.y as u32 + top_padding,
Rgba {
data: [
font_color[0] as u8,
font_color[1] as u8,
font_color[2] as u8,
(v * 255.0) as u8
],
},
)
});
}
}
for (_, _, pixel) in image.enumerate_pixels_mut() {
// put background pixels for the uncovered alpha channels
if pixel.data[3] == 0 {
*pixel = Rgba {
data: [
background_color[0] as u8,
background_color[1] as u8,
background_color[2] as u8,
255
]
}
}
}
image
}
}