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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
use image::{
    imageops::{overlay, resize},
    DynamicImage, GenericImage, GenericImageView, Rgb, Rgba,
};
use reqwest;
use rusttype::{point, Font, Scale};
use wasm_bindgen::prelude::*;

/// Represents a Preset
pub struct SiPreset {
    pub cb: Box<dyn Fn(&mut SiImage, std::collections::HashMap<String, Box<dyn std::any::Any>>) -> SiImage>,
}

impl SiPreset {
    pub fn new(cb: Box<dyn Fn(&mut SiImage, std::collections::HashMap<String, Box<dyn std::any::Any>>) -> SiImage>) -> Box<SiPreset> {
        Box::new(SiPreset { cb: Box::new(cb) })
    }
}

/// Represents a font used for text rendering.
#[wasm_bindgen]
#[derive(Clone)]
pub struct SiFont {
    font: Option<Font<'static>>,
}

#[wasm_bindgen]
impl SiFont {
    /// Creates a new SiFont from a vector of font data.
    #[wasm_bindgen(constructor)]
    pub fn new(vec: Vec<u8>) -> Self {
        Self::from_vec(vec)
    }

    /// Creates a new SiFont from a vector of font data.
    #[wasm_bindgen]
    pub fn from_vec(vec: Vec<u8>) -> SiFont {
        let font = Font::try_from_vec(vec);
        SiFont { font }
    }

    /// Creates a new SiFont from font data fetched from a network URL asynchronously.
    ///
    /// # Arguments
    ///
    /// * `url` - The URL from which to fetch the font data.
    #[wasm_bindgen]
    #[cfg(feature = "async")]
    pub async fn from_network_async(url: &str) -> Result<SiFont, JsValue> {
        let font_data: Vec<u8> = reqwest::get(url)
            .await
            .expect("Could not fetch font")
            .bytes()
            .await
            .expect("Could not extract font")
            .into();
        let font = Font::try_from_vec(font_data);
        return Ok(SiFont { font });
    }

    /// Placeholder method for when async feature is not enabled.
    #[cfg(not(feature = "async"))]
    pub fn from_network_async(_url: &str) {
        panic!("async feature not enabled")
    }

    /// Creates a new SiFont from font data fetched from a network URL synchronously.
    ///
    /// # Arguments
    ///
    /// * `src` - The URL from which to fetch the font data.
    #[cfg(feature = "blocking")]
    pub fn from_network(src: &str) -> SiFont {
        // Load font data from either URL or provided bytes.
        let font_data: Vec<u8> = reqwest::blocking::get(src)
            .expect("Could not fetch font")
            .bytes()
            .expect("Could not extract font")
            .into();
        let font = Font::try_from_vec(font_data);
        SiFont { font }
    }

    /// Placeholder method for when blocking feature is not enabled.
    #[cfg(not(feature = "blocking"))]
    pub fn from_network(url: &str) {
        panic!("blocking feature not enabled")
    }
}

/// Represents an image with text rendering capabilities.
#[wasm_bindgen]
#[derive(Clone)]
pub struct SiImage {
    image: DynamicImage,
    height: u32,
    width: u32,
}

#[wasm_bindgen]
impl SiImage {
    /// Creates a new SiImage from a vector of image data and a SiFont.
    ///
    /// # Arguments
    ///
    /// * `src` - The vector of image data.
    #[wasm_bindgen(constructor)]
    pub fn new(src: Vec<u8>) -> Self {
        Self::from_vec(src)
    }

    /// Creates a new SiImage from a vector of image data.
    #[wasm_bindgen]
    pub fn from_vec(vec: Vec<u8>) -> SiImage {
        let image = image::load_from_memory(&vec).expect("Could not decode image");
        let (width, height) = image.dimensions();
        SiImage {
            image,
            height,
            width,
        }
    }

    /// Creates a new SiImage from image data fetched from a network URL asynchronously.
    ///
    /// # Arguments
    ///
    /// * `image_url` - The URL from which to fetch the image data.
    #[wasm_bindgen]
    #[cfg(feature = "async")]
    pub async fn from_network_async(image_url: &str) -> SiImage {
        let image_data: Vec<u8> = reqwest::get(image_url)
            .await
            .expect("Could not fetch image")
            .bytes()
            .await
            .expect("Could not extract image")
            .into();
        let image = image::load_from_memory(&image_data).expect("Could not decode image");
        let (width, height) = image.dimensions();
        Self {
            image,
            height,
            width,
        }
    }

    /// Placeholder method for when async feature is not enabled.
    #[cfg(not(feature = "async"))]
    pub fn from_network_async(_image_url: &str) {
        panic!("async feature not enabled")
    }

    /// Creates a new SiImage from image data fetched from a network URL synchronously.
    ///
    /// # Arguments
    ///
    /// * `image_url` - The URL from which to fetch the image data.
    #[cfg(feature = "blocking")]
    pub fn from_network(image_url: &str) -> SiImage {
        // Load image data from either URL or provided bytes.
        let image_data: Vec<u8> = reqwest::blocking::get(image_url)
            .expect("Could not fetch image")
            .bytes()
            .expect("Could not extract image")
            .into();

        let image = image::load_from_memory(&image_data).expect("Could not decode image");
        let (width, height) = image.dimensions();

        Self {
            image,
            height,
            width,
        }
    }

    /// Placeholder method for when blocking feature is not enabled.
    #[cfg(not(feature = "blocking"))]
    pub fn from_network(image_url: &str) {
        panic!("blocking feature not enabled")
    }

    /// Renders text onto the image.
    ///
    /// # Arguments
    ///
    /// * `text` - The text to render on the image.
    /// * `text_scale` - The scale of the rendered text.
    /// * `pos_x` - The X-coordinate position for rendering.
    /// * `pos_y` - The Y-coordinate position for rendering.
    /// * `color` - The color of the rendered text in hexadecimal format (e.g., "#RRGGBB").
    /// * `using_font` - The SiFont used for text rendering on the image.
    ///
    /// # Returns
    ///
    /// A mutable instance of the main image, with the text rendered on it.
    #[wasm_bindgen(js_name = "text")]
    pub fn render_text(
        mut self,
        text: &str,
        text_scale: f32,
        pos_x: f32,
        pos_y: f32,
        color: Option<String>,
        using_font: &SiFont,
    ) -> SiImage {
        let mut image = self.image.clone();
        let _font = using_font
            .clone()
            .font;
        let font = _font
            .as_ref()
            .ok_or("Could not reference font")
            .expect("Could not load font");
        let scale = Scale::uniform(text_scale);
        let v_metrics = font.v_metrics(scale);
        let offset = point(pos_x, pos_y + v_metrics.ascent);

        let parsed_color = match color.clone() {
            Some(c) => hex_to_rgb(&c).unwrap_or(Rgb([0, 0, 0])),
            None => Rgb([0, 0, 0]),
        };

        for glyph in font.layout(text, scale, offset) {
            if let Some(bb) = glyph.pixel_bounding_box() {
                glyph.draw(|x, y, v| {
                    let x = x as i32 + bb.min.x;
                    let y = y as i32 + bb.min.y;
                    let pixel = image.get_pixel(x as u32, y as u32);
                    let new_pixel = Rgba([
                        (((parsed_color[0] as f32 * (v)) as f32) + (pixel[0] as f32 * (1.0 - v)))
                            as u8,
                        ((parsed_color[1] as f32 * (v)) as f32 + (pixel[1] as f32 * (1.0 - v)))
                            as u8,
                        ((parsed_color[2] as f32 * (v)) as f32 + (pixel[2] as f32 * (1.0 - v)))
                            as u8,
                        255 as u8,
                    ]);
                    image.put_pixel(x as u32, y as u32, new_pixel);
                });
            }
        }

        let _ = std::mem::replace(&mut self.image, image);
        self
    }

    /// Renders some image into the image
    ///
    /// # Arguments
    ///
    /// * `image` - The SiImage to render.
    /// * `pos_x` - The X-coordinate position for rendering.
    /// * `pos_y` - The Y-coordinate position for rendering.
    ///
    /// # Returns
    ///
    /// A mutable instance of the main image, with overlay of the provided one
    #[wasm_bindgen(js_name = "image")]
    pub fn render_image(mut self, image: SiImage, pos_x: i64, pos_y: i64) -> SiImage {
        overlay(&mut self.image, &image.image, pos_x, pos_y);
        self
    }

    /// Gets the image data as bytes in PNG format.
    ///
    /// # Returns
    ///
    /// The image data as bytes in PNG format
    #[wasm_bindgen]
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut v = std::io::Cursor::new(Vec::new());
        self.image
            .write_to(&mut v, image::ImageFormat::Png)
            .expect("Could not write bytes");
        v.into_inner()
    }

    /// Gets the height of the image.
    ///
    /// # Returns
    ///
    /// The height of the image
    #[wasm_bindgen(getter)]
    pub fn height(&self) -> u32 {
        self.height
    }

    /// Gets the width of the image.
    ///
    /// # Returns
    ///
    /// The width of the image
    #[wasm_bindgen(getter)]
    pub fn width(&self) -> u32 {
        self.width
    }

    /// Resizes the image
    ///
    /// # Arguments
    ///
    /// * `width` - The new width of the image
    /// * `height` - The new height of the image
    ///
    /// # Returns
    ///
    /// A mutable instance of the main image, with the resized image
    #[wasm_bindgen]
    pub fn resize(mut self, width: u32, height: u32) -> SiImage {
        let new_image = DynamicImage::ImageRgba8(resize(
            &self.image,
            width,
            height,
            image::imageops::FilterType::Triangle,
        ));
        let _ = std::mem::replace(&mut self.image, new_image);
        let _ = std::mem::replace(&mut self.width, width);
        let _ = std::mem::replace(&mut self.height, height);
        self
    }
}

impl SiImage {
    /// Load a preset.
    /// **NOTE**: It doesn't work in WASM. Only for direct usage.
    pub fn load_preset(&mut self, preset: Box<SiPreset>, values: std::collections::HashMap<String, Box<dyn std::any::Any>>) -> &mut SiImage {
        let res = (preset.cb)(self, values);
        let _ = std::mem::replace(self, res);
        self
    }
}

/// Converts a hexadecimal color code to an RGB color.
///
/// # Arguments
///
/// * `hex` - The hexadecimal color code (e.g., "#RRGGBB").
///
/// # Returns
///
/// An `Option` containing the RGB color as `Rgb<u8>`, or `None` if the conversion fails.
pub fn hex_to_rgb(hex: &str) -> Option<Rgb<u8>> {
    let hex = hex.trim_start_matches('#'); // Remove "#" if present
    if hex.len() == 6 {
        let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
        let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
        let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
        Some(Rgb([r, g, b]))
    } else if hex.len() == 3 {
        let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?;
        let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?;
        let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?;
        Some(Rgb([r, g, b]))
    } else {
        Some(Rgb([255, 255, 255]))
    }
}