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
use std::fs;
use fontdue::{Font, FontSettings};
use image::RgbaImage;
use itertools::enumerate;
use log::{error, debug, warn};
use super::{
    colors::{ColorScheme, ColorGen}, Token, wordcloud::WorldCloud, 
    rasterisable::Rasterisable, text::Text, image::Image
};
use twemoji_rs::get_twemoji;

fn convert_emojis(tokens: &mut Vec<(Token, f32)>) {
    // Convert unicode emojis to images with Twemoji
    tokens.iter_mut().for_each(|(token, _v)| {
        if let Token::Text(str) = token {
            if let Some(path) = get_twemoji(str) {
                *token = Token::from(&path);
            }
        }
    });
}

fn size_factor(dim: (usize, usize), tokens: &Vec<(Token, f32)>) -> f32 {
    let sum = tokens.iter().fold(0., |i, (_, s)| i+s);
    // magical formula that seems to work well ¯\_(ツ)_/¯
    2.*(tokens.len() as f32).log(10.)*dim.0 as f32/sum
}

fn wordcloud(font: &Font, dim: (usize, usize), mut tokens: Vec<(Token, f32)>, colors: &mut Box<dyn ColorGen>) -> RgbaImage {
    tokens.sort_by(|(_, s1), (_, s2)| s2.partial_cmp(s1).unwrap());
    tokens.truncate(100);
    tokens.iter_mut().for_each(|(_, v)| *v = v.sqrt());
    convert_emojis(&mut tokens);
    let c = size_factor(dim, &tokens); 
    let mut wc = WorldCloud::new(dim);
    // shrink tokens if they don't fit, up to a point
    let len = tokens.len();
    'outer: for (i, (token, size)) in enumerate(tokens) {
        let mut adjust = 1.;
        debug!(target: "wordcloud", "{} {}", size, token);
        loop {
            let rasterisable: Box<dyn Rasterisable> = match token.clone() {
                Token::Text(text) => Box::new(Text::new(text, font.clone(), (2.+size*c)*adjust, colors.get())),
                Token::Img(image) => Box::new(Image::new(image, (2.+size*c)*adjust*1.5))
            };
            if wc.add(rasterisable) {
                break;
            }
            if adjust < 0.5 {
                warn!(target: "wordcloud", "Could only fit {}/{} tokens", i, len);
                break 'outer;
            }
            adjust -= 0.1;
            warn!(target: "wordcloud", "Adjusting scale to {}", adjust)
        };
    }
    wc.image
}

pub struct Builder {
    dim: (usize, usize),
    font: Font,
    colors: Box<dyn ColorGen>,
}

impl Builder {
    pub fn new() -> Self {
        let font = include_bytes!("../assets/whitneymedium.otf") as &[u8];
        // Parse it into the font type.
        let font = Font::from_bytes(font, FontSettings::default()).unwrap();
        Self {
            dim: (800, 400),
            font, 
            colors: ColorScheme::Rainbow {luminance: 70., chroma: 100.}.into()
        }
    }

    pub fn font(mut self, path: &str) -> Self {
        match fs::read(path) {
            Ok(bytes) => match Font::from_bytes(bytes, FontSettings::default()) {
                Ok(font) => self.font = font,
                Err(err) => error!("{}", err)
            },
            Err(err) => error!("{}", err)
        }
        self
    }

    pub fn dim(mut self, width: usize, height: usize) -> Self {
        self.dim = (width, height);
        self
    }

    pub fn colors(mut self, colors: impl Into<Box<dyn ColorGen>>) -> Self {
        self.colors = colors.into();
        self
    }

    pub fn generate(&mut self, tokens: Vec<(Token, f32)>) -> RgbaImage {
        wordcloud(&self.font, self.dim, tokens, &mut self.colors)
    }
}