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}