word_cloud/visualization/
image.rs

1use image::{DynamicImage, ImageResult, Rgb, RgbImage};
2use imageproc::{
3    drawing::{draw_filled_rect, draw_hollow_rect},
4    rect::Rect,
5};
6use rusttype::Scale;
7
8use crate::{geometry::Orientation, words::Font};
9
10pub struct Image {
11    img: RgbImage,
12    bg_color: Rgb<u8>,
13    width: u32,
14    height: u32,
15}
16
17impl Image {
18    pub fn new(width: u32, height: u32, bg_color: Option<Rgb<u8>>) -> Self {
19        let mut image = Image {
20            img: DynamicImage::new_rgb8(width, height).to_rgb8(),
21            bg_color: match bg_color {
22                Some(color) => color,
23                None => Rgb([255, 255, 255]),
24            },
25            width,
26            height,
27        };
28
29        image.draw_filled_rect(0, 0, width, height, image.bg_color);
30
31        image
32    }
33
34    pub fn save(&self, filename: &str) -> ImageResult<()> {
35        self.img.save(filename)
36    }
37
38    pub fn draw_filled_rect(&mut self, x: i32, y: i32, w: u32, h: u32, color: Rgb<u8>) {
39        let rect = Rect::at(x, y).of_size(w, h);
40        self.img = draw_filled_rect(&self.img, rect, color)
41    }
42
43    pub fn draw_rect(&mut self, x: i32, y: i32, w: u32, h: u32, color: Rgb<u8>) {
44        let rect = Rect::at(x, y).of_size(w, h);
45        self.img = draw_hollow_rect(&self.img, rect, color)
46    }
47
48    pub fn draw_text(
49        &mut self,
50        pos: (i32, i32),
51        text: &str,
52        font: &Font,
53        scale: Scale,
54        color: Rgb<u8>,
55        orientation: &Orientation,
56    ) {
57        let data = font.get_glyph_data(text, scale);
58        let r = data
59            .bounding_box(orientation)
60            .displace(pos.0 as isize, pos.1 as isize)
61            .unwrap();
62
63        let v_metrics = font.get_metrics(scale);
64
65        for glyph in data.glyphs {
66            if let Some(bounding_box) = glyph.pixel_bounding_box() {
67                glyph.draw(|x, y, v| {
68                    let c = Self::alpha_blend(self.bg_color, color, v);
69
70                    let new_x = match orientation {
71                        Orientation::Horizontal => x + bounding_box.min.x as u32 + r.pos().0,
72                        Orientation::Vertical => {
73                            (bounding_box.min.y
74                                + r.pos().0 as i32
75                                + r.size().0 as i32
76                                + v_metrics.descent as i32
77                                + y as i32) as u32
78                        }
79                    };
80
81                    let new_y = match orientation {
82                        Orientation::Horizontal => {
83                            (y as i32
84                                + bounding_box.min.y
85                                + r.pos().1 as i32
86                                + r.size().1 as i32
87                                + v_metrics.descent as i32) as u32
88                        }
89                        Orientation::Vertical => {
90                            (-bounding_box.min.x + r.pos().1 as i32 + r.size().1 as i32 - x as i32)
91                                as u32
92                        }
93                    };
94
95                    if (new_x < self.width) && (new_y < self.height) {
96                        self.img.put_pixel(new_x, new_y, c)
97                    }
98                });
99            }
100        }
101    }
102
103    fn alpha_blend(bg: Rgb<u8>, fg: Rgb<u8>, percentage: f32) -> Rgb<u8> {
104        fn ab(bg: f32, fg: f32, percentage: f32) -> f32 {
105            (fg * percentage) + (bg * (1.0 - percentage))
106        }
107
108        Rgb([
109            ab(bg.0[0] as f32, fg.0[0] as f32, percentage) as u8,
110            ab(bg.0[1] as f32, fg.0[1] as f32, percentage) as u8,
111            ab(bg.0[2] as f32, fg.0[2] as f32, percentage) as u8,
112        ])
113    }
114}