geronimo_captcha/
image.rs

1use crate::SpriteFormat;
2
3use image::{DynamicImage, GenericImageView, Rgba};
4use imageproc::geometric_transformations::{Interpolation, rotate_about_center};
5use rand::Rng;
6use webp::Encoder as WebPEncoder;
7
8#[derive(Clone, Copy, Default)]
9pub enum NoisePattern {
10    Dots,
11    Lines,
12    #[default]
13    Grid,
14}
15
16#[derive(Clone, Copy)]
17pub struct NoiseOptions {
18    pub count: u32,
19    pub size: u32,
20    pub blur_sigma: f32,
21    pub alpha: u8,
22    pub color_range: (u8, u8),
23    pub shape: NoisePattern,
24    pub red: bool,
25    pub green: bool,
26    pub blue: bool,
27}
28
29impl Default for NoiseOptions {
30    fn default() -> Self {
31        NoiseOptions {
32            count: 300 * 9,
33            size: 2,
34            alpha: 100,
35            color_range: (0, 255),
36            shape: NoisePattern::default(),
37            red: true,
38            green: true,
39            blue: true,
40            blur_sigma: 0.7,
41        }
42    }
43}
44
45/// Rotate image by arbitrary angle
46/// using imageproc (nearest-neighbor).
47pub fn rotate_image(img: &DynamicImage, angle_deg: f32) -> DynamicImage {
48    if angle_deg == 0.0 {
49        return img.clone();
50    }
51
52    let rgba = img.to_rgba8();
53    let bg = Rgba([255, 255, 255, 255]);
54    let rotated = rotate_about_center(&rgba, angle_deg.to_radians(), Interpolation::Nearest, bg);
55
56    DynamicImage::ImageRgba8(rotated)
57}
58
59pub fn watermark_with_noise(img: &mut DynamicImage, opts: NoiseOptions) {
60    let mut rng = rand::rng();
61    let (width, height) = img.dimensions();
62    let mut img_buf = img.to_rgba8();
63
64    for _ in 0..opts.count {
65        let x = rng.random_range(0..width);
66        let y = rng.random_range(0..height);
67
68        let r = if opts.red {
69            rng.random_range(opts.color_range.0..=opts.color_range.1)
70        } else {
71            0
72        };
73        let g = if opts.green {
74            rng.random_range(opts.color_range.0..=opts.color_range.1)
75        } else {
76            0
77        };
78        let b = if opts.blue {
79            rng.random_range(opts.color_range.0..=opts.color_range.1)
80        } else {
81            0
82        };
83
84        let color = Rgba([r, g, b, opts.alpha]);
85
86        match opts.shape {
87            NoisePattern::Dots => {
88                img_buf.put_pixel(x, y, color);
89            }
90            NoisePattern::Lines => {
91                for i in 0..opts.size {
92                    if x + i < width {
93                        img_buf.put_pixel(x + i, y, color);
94                    }
95                }
96            }
97            NoisePattern::Grid => {
98                for dx in 0..opts.size {
99                    for dy in 0..opts.size {
100                        if x + dx < width && y + dy < height {
101                            img_buf.put_pixel(x + dx, y + dy, color);
102                        }
103                    }
104                }
105            }
106        }
107    }
108
109    *img = DynamicImage::ImageRgba8(img_buf);
110
111    if opts.blur_sigma > 0.0 {
112        *img = img.fast_blur(opts.blur_sigma);
113    }
114}
115
116pub fn encode_image(
117    img: &DynamicImage,
118    fmt: &SpriteFormat,
119) -> Result<(Vec<u8>, &'static str), image::ImageError> {
120    match *fmt {
121        SpriteFormat::Jpeg { quality } => {
122            let mut buf = Vec::new();
123            let mut enc = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, quality);
124
125            let rgb = img.to_rgb8();
126            let dyn_rgb = image::DynamicImage::ImageRgb8(rgb);
127
128            enc.encode_image(&dyn_rgb)?;
129
130            Ok((buf, "image/jpeg"))
131        }
132        SpriteFormat::Webp { quality, lossless } => {
133            let rgba = img.to_rgba8();
134            let enc = WebPEncoder::from_rgba(rgba.as_raw(), rgba.width(), rgba.height());
135
136            let webp = if lossless {
137                enc.encode_lossless()
138            } else {
139                enc.encode(quality as f32)
140            };
141
142            Ok((webp.to_vec(), "image/webp"))
143        }
144    }
145}