geronimo_captcha/
image_ops.rs1use 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
43pub 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}