geronimo_captcha/
image.rs1use 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
45pub 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}