image_builder/image.rs
1use std::{collections::HashMap, fs::File};
2
3pub use image::imageops::FilterType;
4
5use image::{
6 codecs::png::PngEncoder,
7 imageops::{crop, overlay, resize},
8 ImageBuffer, ImageEncoder, Rgba,
9};
10use imageproc::{
11 drawing::{draw_filled_rect_mut, draw_text_mut, text_size},
12 rect as procRect,
13};
14use rusttype::Font;
15
16use crate::{
17 colors::Color,
18 picture::{self, Picture},
19 rect::{self, Rect},
20 text::{self, Text},
21};
22
23#[derive(Clone)]
24pub enum Element {
25 Text(Text),
26 Rect(Rect),
27 Picture(Picture),
28}
29
30/// This is the structure of the image that will be created.
31///
32/// > Use the `new` function to get started.
33///
34/// It is important to remember that the order in which elements are added to the image defines which
35/// element goes on top of which. For example, adding text that starts at point 0,0 and then adding a
36/// rectangle that also starts at the same point will cause the rectangle to cover the text. However,
37/// by reversing the order and adding the rectangle first, it will be placed underneath the text. It
38/// is essential to keep this order in mind when creating images with multiple elements to ensure that
39/// the elements are in the desired order.
40/// ## Examples
41/// ```rust
42/// # use image_builder::Image;
43/// # use image_builder::Rect;
44/// # use image_builder::Text;
45/// # use image_builder::colors;
46/// let mut image = Image::new(500, 500, colors::WHITE);
47/// image.add_text(Text::new("Image Builder"));
48/// image.add_rect(Rect::new().size(200, 200)); // This rectangle covers the text.
49/// ```
50///
51/// ```rust
52/// # use image_builder::Image;
53/// # use image_builder::Rect;
54/// # use image_builder::Text;
55/// # use image_builder::colors;
56/// let mut image = Image::new(500, 500, colors::WHITE);
57/// image.add_rect(Rect::new().size(200, 200)); // This rectangle is in the background of the text.
58/// image.add_text(Text::new("Image Builder"));
59/// ```
60#[derive(Clone)]
61pub struct Image<'a> {
62 background: Color,
63 size: (u32, u32),
64 fonts: HashMap<&'a str, Font<'a>>,
65 elements: Vec<Element>,
66}
67
68impl<'a> Image<'a> {
69 /// This method creates a new instance of an image, setting the background color, and size in
70 /// pixels, and allocating memory to add fonts and elements to be drawn.
71 /// ## Example
72 /// ```
73 /// use image_builder::{colors, Image};
74 ///
75 /// let mut image = Image::new(400, 300, colors::GRAY);
76 /// ```
77 pub fn new(width: u32, height: u32, background: Color) -> Image<'a> {
78 let default_font = Vec::from(include_bytes!("Roboto-Regular.ttf") as &[u8]);
79 let default_font = Font::try_from_vec(default_font)
80 .expect("Fail to load the default font \"Roboto-Regular.ttf\"");
81
82 Image {
83 background,
84 size: (width, height),
85 fonts: HashMap::from([("default", default_font)]),
86 elements: Vec::new(),
87 }
88 }
89
90 /// The add_custom_font method requires that a .ttf font file (not provided) be loaded using fs.read,
91 /// and internally linked to the provided name in a HashMap. This will allow you to use this font in
92 /// your text by simply passing the font name as a parameter. Trying to use a font that has not been
93 /// loaded cause the application to panic. Additionally, providing an invalid Vec<u8> will also
94 /// result in a panic.
95 /// ## Example
96 /// ```
97 /// use image_builder::Image;
98 /// use std::fs;
99 /// use image_builder::colors;
100 ///
101 /// let mut image = Image::new(500, 500, colors::WHITE);
102 /// let roboto_bold = fs::read("fonts/Roboto/Roboto-Bold.ttf").unwrap();
103 /// image.add_custom_font("Roboto bold", roboto_bold);
104 /// ```
105 pub fn add_custom_font(&mut self, name: &'a str, font: Vec<u8>) {
106 let font = Font::try_from_vec(font).expect(&format!("Fail to load the font \"{}\"", name));
107 self.fonts.insert(name, font);
108 }
109
110 /// With this method, it is possible to add an image on top of the image being built, taking into account
111 /// transparent backgrounds. This means that transparent areas of the added image will not overlap areas
112 /// already drawn in the main image. Please refer to the [`Picture`] for more details.
113 pub fn add_picture(&mut self, picture: Picture) {
114 self.elements.push(Element::Picture(picture));
115 }
116
117 /// This method allows for adding formatted text to the image being built. Refer to the [`Text`] for more details.
118 pub fn add_text(&mut self, text: Text) {
119 self.elements.push(Element::Text(text));
120 }
121
122 /// This method can be used before `add_text` to reqeust the expected width and height of a
123 /// text element.
124 pub fn text_size(&mut self, text: &Text) -> (i32, i32) {
125 let t = text::extract(&text);
126 let font = self.fonts.get(t.font_name).expect(&format!("Unable to load the \"{}\" font, please verify that the name is correct or that it was loaded using the \"add_custom_font\" method.", t.font_name));
127 text_size(t.scale, font, &t.content)
128 }
129
130 /// This method allows for adding rectangular shapes to the image being built. Refer to the [`Rect`] for more details.
131 pub fn add_rect(&mut self, rect: Rect) {
132 self.elements.push(Element::Rect(rect));
133 }
134
135 /// The save method is responsible for the entire rendering process of the library. It creates the image buffer and
136 /// renders the list of elements added in the order they were inserted by the user. Then, it creates the image file,
137 /// adds the generated buffer, and encodes the content to save it to the disk.
138 pub fn save(&mut self, file_name: &str) {
139 let mut image = ImageBuffer::from_pixel(self.size.0, self.size.1, Rgba(self.background));
140
141 for element in self.elements.iter() {
142 match element {
143 Element::Picture(element) => {
144 let p = picture::extract(element);
145 let mut pic = p.img.to_rgba8();
146
147 if let Some(values) = p.resize {
148 pic = resize(&mut pic, values.nwidth, values.nheight, values.filter)
149 }
150 if let Some(values) = p.crop {
151 pic = crop(&mut pic, values.x, values.y, values.width, values.height)
152 .to_image();
153 }
154
155 overlay(&mut image, &pic, p.x, p.y);
156 }
157 Element::Text(element) => {
158 let t = text::extract(&element);
159 let font = self.fonts.get(t.font_name).expect(&format!("Unable to load the \"{}\" font, please verify that the name is correct or that it was loaded using the \"add_custom_font\" method.", t.font_name));
160 let mut text_image =
161 ImageBuffer::from_pixel(self.size.0, self.size.1, Rgba([0, 0, 0, 0]));
162 draw_text_mut(&mut text_image, t.color, 0, 0, t.scale, font, t.content);
163 overlay(&mut image, &text_image, t.x as i64, t.y as i64);
164 }
165 Element::Rect(element) => {
166 let r = rect::extract(element);
167 let mut rect_image =
168 ImageBuffer::from_pixel(r.width, r.height, Rgba([0, 0, 0, 0]));
169
170 draw_filled_rect_mut(
171 &mut rect_image,
172 procRect::Rect::at(0, 0).of_size(r.width, r.height),
173 r.color,
174 );
175
176 overlay(&mut image, &rect_image, r.x as i64, r.y as i64);
177 }
178 }
179 }
180
181 let file = File::create(file_name).expect(&format!(
182 "It was not possible to create the file \"{}\" because the file path does not exist.",
183 file_name
184 ));
185 let encoder = PngEncoder::new(file);
186 encoder
187 .write_image(
188 &image,
189 image.width(),
190 image.height(),
191 image::ColorType::Rgba8,
192 )
193 .unwrap();
194 }
195}