geronimo_captcha/
image_ops.rs

1use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
2use image::{DynamicImage, GenericImageView, ImageFormat, Rgba};
3use imageproc::geometric_transformations::{Interpolation, rotate_about_center};
4use rand::Rng;
5
6#[derive(Clone, Copy, Default)]
7pub enum NoisePattern {
8    Dots,
9    Lines,
10    #[default]
11    Grid,
12}
13
14#[derive(Clone, Copy)]
15pub struct NoiseOptions {
16    pub count: u32,
17    pub size: u32,
18    pub blur_sigma: f32,
19    pub alpha: u8,
20    pub color_range: (u8, u8),
21    pub shape: NoisePattern,
22    pub red: bool,
23    pub green: bool,
24    pub blue: bool,
25}
26
27impl Default for NoiseOptions {
28    fn default() -> Self {
29        NoiseOptions {
30            count: 300 * 9,
31            size: 2,
32            alpha: 100,
33            color_range: (0, 255),
34            shape: NoisePattern::default(),
35            red: true,
36            green: true,
37            blue: true,
38            blur_sigma: 0.7,
39        }
40    }
41}
42
43/// Rotate image by arbitrary angle using imageproc (nearest-neighbor)
44pub fn rotate_image(img: &DynamicImage, angle_deg: f32) -> DynamicImage {
45    if angle_deg == 0.0 {
46        return img.clone();
47    }
48
49    let rgba = img.to_rgba8();
50    let bg = Rgba([255, 255, 255, 255]);
51    let rotated = rotate_about_center(&rgba, angle_deg.to_radians(), Interpolation::Nearest, bg);
52
53    DynamicImage::ImageRgba8(rotated)
54}
55
56pub fn watermark_with_noise(img: &mut DynamicImage, opts: NoiseOptions) {
57    let mut rng = rand::rng();
58    let (width, height) = img.dimensions();
59    let mut img_buf = img.to_rgba8();
60
61    for _ in 0..opts.count {
62        let x = rng.random_range(0..width);
63        let y = rng.random_range(0..height);
64
65        let r = if opts.red {
66            rng.random_range(opts.color_range.0..=opts.color_range.1)
67        } else {
68            0
69        };
70        let g = if opts.green {
71            rng.random_range(opts.color_range.0..=opts.color_range.1)
72        } else {
73            0
74        };
75        let b = if opts.blue {
76            rng.random_range(opts.color_range.0..=opts.color_range.1)
77        } else {
78            0
79        };
80
81        let color = Rgba([r, g, b, opts.alpha]);
82
83        match opts.shape {
84            NoisePattern::Dots => {
85                img_buf.put_pixel(x, y, color);
86            }
87            NoisePattern::Lines => {
88                for i in 0..opts.size {
89                    if x + i < width {
90                        img_buf.put_pixel(x + i, y, color);
91                    }
92                }
93            }
94            NoisePattern::Grid => {
95                for dx in 0..opts.size {
96                    for dy in 0..opts.size {
97                        if x + dx < width && y + dy < height {
98                            img_buf.put_pixel(x + dx, y + dy, color);
99                        }
100                    }
101                }
102            }
103        }
104    }
105
106    *img = DynamicImage::ImageRgba8(img_buf);
107
108    if opts.blur_sigma > 0.0 {
109        *img = img.fast_blur(opts.blur_sigma);
110    }
111}
112
113pub fn sprite_to_base64(buf: &[u8], format: ImageFormat) -> String {
114    format!(
115        "data:{};base64,{}",
116        format.to_mime_type(),
117        BASE64_STANDARD.encode(buf)
118    )
119}