extern crate base64;
#[cfg(feature = "audio")]
extern crate hound;
extern crate image;
extern crate lodepng;
extern crate rand;
extern crate serde_json;
mod audio;
pub mod filters;
mod fonts;
mod images;
mod samples;
pub use samples::{by_name, generate, CaptchaName, Difficulty};
use filters::Filter;
use fonts::{Default, Font};
use images::{Image, Pixl};
#[cfg(feature = "audio")]
use audio::Audio;
use image::ImageResult as Result;
use rand::prelude::*;
use rand::rng;
use std::cmp::{max, min};
use std::path::Path;
#[derive(Clone, Debug)]
pub struct Geometry {
pub left: u32,
pub right: u32,
pub top: u32,
pub bottom: u32,
}
impl Geometry {
pub fn new(left: u32, right: u32, top: u32, bottom: u32) -> Geometry {
Geometry {
left,
right,
top,
bottom,
}
}
}
pub type Captcha = RngCaptcha<ThreadRng>;
pub struct RngCaptcha<T> {
img: Image,
font: Box<dyn Font>,
text_area: Geometry,
chars: Vec<char>,
use_font_chars: Vec<char>,
color: Option<[u8; 3]>,
rng: T,
}
impl<T: rand::Rng + rand::RngCore> RngCaptcha<T> {
pub fn from_rng(rng: T) -> RngCaptcha<T> {
let w = 400;
let h = 300;
let f = Box::new(Default::new());
RngCaptcha::<T> {
use_font_chars: f.chars(),
img: Image::new(w, h),
font: f,
text_area: Geometry {
left: w / 4,
right: w / 4,
top: h / 2,
bottom: h / 2,
},
chars: vec![],
color: None,
rng,
}
}
pub fn new() -> Captcha {
Captcha::from_rng(rng())
}
pub fn apply_filter<F: Filter>(&mut self, f: F) -> &mut Self {
f.apply(&mut self.img);
self
}
pub fn set_font<F: Font + 'static>(&mut self, f: F) -> &mut Self {
self.font = Box::new(f);
self.use_font_chars = self.font.chars();
self
}
pub fn set_color(&mut self, color: [u8; 3]) -> &mut Self {
self.color = Some(color);
self
}
pub fn save(&self, p: &Path) -> Result<()> {
let i = self.apply_transformations();
i.save(p)
}
pub fn set_chars(&mut self, c: &[char]) -> &mut Self {
self.use_font_chars = c.to_vec();
self
}
fn random_char_as_image(&mut self) -> Option<(char, Image)> {
match self.use_font_chars.choose(&mut self.rng) {
None => None,
Some(c) => match self.font.png(*c) {
None => None,
Some(p) => Image::from_png(p).map(|i| (*c, i)),
},
}
}
pub fn add_char(&mut self) -> &mut Self {
if let Some((c, i)) = self.random_char_as_image() {
let x = self.text_area.right;
let y = (self.text_area.bottom + self.text_area.top) / 2 - i.height() / 2;
self.img.add_image(x, y, &i);
self.text_area.top = min(self.text_area.top, y);
self.text_area.right = x + i.width() - 1;
self.text_area.bottom = max(self.text_area.bottom, y + i.height() - 1);
self.chars.push(c);
}
self
}
pub fn add_text_area(&mut self) -> &mut Self {
for y in self.text_area.top..self.text_area.bottom {
self.img.put_pixel(self.text_area.left, y, Pixl::red());
self.img.put_pixel(self.text_area.right, y, Pixl::red());
}
for x in self.text_area.left..self.text_area.right {
self.img.put_pixel(x, self.text_area.top, Pixl::red());
self.img.put_pixel(x, self.text_area.bottom, Pixl::red());
}
self
}
pub fn text_area(&self) -> Geometry {
self.text_area.clone()
}
pub fn extract(&mut self, area: Geometry) -> &mut Self {
let w = area.right - area.left;
let h = area.bottom - area.top;
let mut i = Image::new(w, h);
for (y, iy) in (area.top..area.bottom).zip(0..h + 1) {
for (x, ix) in (area.left..area.right).zip(0..w + 1) {
i.put_pixel(ix, iy, self.img.get_pixel(x, y));
}
}
self.img = i;
self
}
pub fn view(&mut self, w: u32, h: u32) -> &mut Self {
let mut a = self.text_area();
a.left = (a.right + a.left) / 2 - w / 2;
a.right = a.left + w;
a.top = (a.bottom + a.top) / 2 - h / 2;
a.bottom = a.top + h;
self.extract(a);
self
}
pub fn chars(&self) -> Vec<char> {
self.chars.clone()
}
pub fn chars_as_string(&self) -> String {
self.chars.iter().collect()
}
pub fn add_chars(&mut self, n: u32) -> &mut Self {
for _ in 0..n {
self.add_char();
}
self
}
fn apply_transformations(&self) -> Image {
let mut i = self.img.clone();
if self.color.is_some() {
i.set_color(&self.color.unwrap());
}
i
}
#[cfg(feature = "hound")]
pub fn as_wav(&self) -> Vec<Option<Vec<u8>>> {
let audio = Audio::new();
self.chars().iter().map(|x| audio.as_wav(*x)).collect()
}
pub fn as_png(&self) -> Option<Vec<u8>> {
let i = self.apply_transformations();
i.as_png()
}
pub fn as_base64(&self) -> Option<String> {
self.as_png().map(base64::encode)
}
pub fn as_tuple(&self) -> Option<(String, Vec<u8>)> {
match self.as_png() {
None => None,
Some(p) => Some((self.chars_as_string(), p)),
}
}
pub fn supported_chars(&self) -> Vec<char> {
self.font.chars()
}
}
#[cfg(test)]
mod tests {
use filters::{Grid, Noise};
use fonts::Default;
use Captcha;
use std::path::Path;
#[test]
fn it_works() {
let mut c = Captcha::new();
c.set_font(Default::new())
.add_char()
.add_char()
.add_char()
.apply_filter(Noise::new(0.1))
.apply_filter(Grid::new(20, 10))
.add_text_area();
let a = c.text_area();
c.extract(a)
.save(Path::new("/tmp/captcha.png"))
.expect("save failed");
c.as_png().expect("no png");
}
#[test]
fn image_size() {
let mut c = Captcha::new();
c.view(8, 16);
assert_eq!(&c.img.width(), &8);
assert_eq!(&c.img.height(), &16);
}
}