commit 6a8e8bfe731b86fffdd37caa05eb00a80aebc4e1
parent 480140a7b98b4530bc293f858b0be91cd0a2bf2b
Author: n0tr1v <n0tr1v@protonmail.com>
Date: Wed, 29 Mar 2023 15:20:10 -0700
diff 3
Diffstat:
| M | src/lechatphp/mod.rs | | | 264 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------- |
1 file changed, 210 insertions(+), 54 deletions(-)
diff --git a/src/lechatphp/mod.rs b/src/lechatphp/mod.rs
@@ -1,5 +1,4 @@
use std::collections::{HashMap, HashSet};
-use std::hash::Hash;
use base64::{engine::general_purpose, Engine as _};
use image::{ColorType, DynamicImage, GenericImageView, Rgba};
use lazy_static::lazy_static;
@@ -90,7 +89,7 @@ pub fn solve_b64(b64_str: &str) -> Option<String> {
let img_dec = general_purpose::STANDARD.decode(b64_str.strip_prefix("data:image/gif;base64,")?).ok()?;
let img = image::load_from_memory(&img_dec).ok()?;
if img.dimensions().0 > 60 {
- return solve_difficulty3(&img);
+ return Some(solve_difficulty3(&img).unwrap());
}
solve_difficulty2(&img)
}
@@ -150,6 +149,15 @@ impl Letter {
fn key(&self) -> String {
format!("{}_{}_{}", self.character, self.offset_x, self.offset_y)
}
+
+ fn offset(&self) -> Point {
+ Point::new(self.offset_x, self.offset_y)
+ }
+
+ fn center(&self) -> Point {
+ let offset = self.offset();
+ Point::new(offset.x + 4, offset.y + 6)
+ }
}
// SolveDifficulty3 solve captcha for difficulty 3
@@ -157,12 +165,12 @@ impl Letter {
// verify that we have some "red" in it.
//
// Red circle is 17x17 (initial point)
-fn solve_difficulty3(img: &DynamicImage) -> Option<String> {
+fn solve_difficulty3(img: &DynamicImage) -> Result<String, String> {
+ //img.save(format!("captcha.gif")).unwrap();
let image_width = 150;
let image_height = 200;
let min_px_for_letter = 21;
let min_starting_pt_red_px = 50;
- let mut answer = String::new();
let mut letters_map: HashMap<char, Letter> = HashMap::new();
@@ -203,14 +211,16 @@ fn solve_difficulty3(img: &DynamicImage) -> Option<String> {
}
}
- letters_map.insert(c, Letter{offset_x: x, offset_y: y, character: c}); // Keep letters in hashmap for easy access
+ let letter = Letter{offset_x: x, offset_y: y, character: c};
+ println!("Letter: {:?}", &letter);
+ letters_map.insert(c, letter); // Keep letters in hashmap for easy access
break;
}
}
}
if letters_map.len() != 5 {
- return None; // did not find exactly 5 letters
+ return Err(format!("did not find exactly 5 letters {}", letters_map.len()));
}
let mut starting: Option<Letter> = None;
@@ -218,6 +228,7 @@ fn solve_difficulty3(img: &DynamicImage) -> Option<String> {
for (_, letter) in letters_map.iter() {
let square = img.crop_imm(letter.offset_x-5, letter.offset_y-3, 8+5+6, 14+3+2);
let count_red = count_red_px(&square);
+ // println!("{}, {}", letter.character, count_red);
if count_red > min_starting_pt_red_px {
starting = Some(letter.clone());
break;
@@ -225,86 +236,231 @@ fn solve_difficulty3(img: &DynamicImage) -> Option<String> {
}
if starting.is_none() {
- return None; // could not find starting letter
+ return Err("could not find starting letter".to_owned());
}
- println!("STARTING IS {:?}", starting.clone().unwrap());
+ // println!("STARTING IS {:?}", starting.clone().unwrap());
- let mut code = String::new();
+ let mut answer = String::new();
let mut letter = starting.unwrap();
let mut visited = HashSet::<String>::new();
for i in 0..5 {
if visited.contains(&letter.key()) {
- return None; // already visited node
+ return Err(format!("already visited node {:?}", letter));
}
- code.push(letter.character);
+ answer.push(letter.character);
+ // println!("visiting {:?}", letter);
visited.insert(letter.key());
if i == 4 {
break;
}
+ let top_left = Point::new(letter.offset_x-5, letter.offset_y-3);
+ let mut rect_min = Point::new(top_left.x, top_left.y);
+ let mut rect_dim = (8+5+6, 14+3+2);
+
let mut retry = 0;
'the_loop: loop {
retry += 1;
- let square = img.crop_imm(letter.offset_x-5, letter.offset_y-3, 8+5+6, 14+3+2);
- // let red_px_pts = get_contour_red_pixels();
- // square.save(format!("captcha_{}.gif", letter.key())).ok()?;
+ let square = img.crop_imm(rect_min.x, rect_min.y, rect_dim.0, rect_dim.1);
+ if i == 0 {
+ square.save("root.gif").unwrap();
+ }
+ let red_px_pts = get_contour_red_pixels(&rect_min, &square);
+
+ if i == 0 {
+ if red_px_pts.len() == 0 {
+ if retry < 10 {
+ rect_min.x -= 1;
+ rect_min.y -= 1;
+ rect_dim.0 += 1;
+ rect_dim.1 += 1;
+ continue;
+ }
+ square.save(format!("captcha_root_has_no_line.gif")).unwrap();
+ return Err(format!("root {:?} has no line detected", letter));
+ }
+ if red_px_pts.len() > 1 {
+ square.save(format!("captcha_root_more_than_one_line.gif")).unwrap();
+ return Err(format!("root {:?} has more than one line detected", letter));
+ }
+ let red_pt = red_px_pts.get(0).unwrap();
+ let angle = get_angle(red_pt, &letter.center());
+ let neighbor = get_letter_in_direction(&letter, angle, &letters_map);
+ letter = neighbor.clone();
+ println!("angle: {:?}, neighbor: {:?}", angle, neighbor.clone());
+ break;
+ }
+
+ if red_px_pts.len() == 0 {
+ if retry < 10 {
+ rect_min.x -= 1;
+ rect_min.y -= 1;
+ rect_dim.0 += 1;
+ rect_dim.1 += 1;
+ continue;
+ }
+ square.save(format!("captcha_has_no_line.gif")).unwrap();
+ return Err(format!("letter #{} {:?} has no line detected", i+1, letter));
+ }
+
+ if red_px_pts.len() == 1 {
+ if retry < 10 {
+ rect_min.x -= 1;
+ rect_min.y -= 1;
+ rect_dim.0 += 1;
+ rect_dim.1 += 1;
+ continue;
+ }
+ square.save(format!("captcha_has_only_one_line.gif")).unwrap();
+ return Err(format!("letter #{} {:?} has only 1 line detected", i+1, letter));
+ }
+
+ if red_px_pts.len() > 2 {
+ square.save(format!("captcha_has_more_than_two_lines.gif")).unwrap();
+ return Err(format!("letter #{} {:?} has more than 2 lines detected", i+1, letter));
+ }
+
+ let fst_red_pt = red_px_pts.get(0).unwrap();
+ let angle = get_angle(fst_red_pt, &letter.center());
+ let mut neighbor = get_letter_in_direction(&letter, angle, &letters_map);
+
+ if visited.contains(&neighbor.key()) {
+ let fst_red_pt = red_px_pts.get(1).unwrap();
+ let angle = get_angle(fst_red_pt, &letter.center());
+ neighbor = get_letter_in_direction(&letter, angle, &letters_map);
+ if visited.contains(&neighbor.key()) {
+ if i == 3 {
+ for (_, l) in letters_map.iter() {
+ if !visited.contains(&l.key()) {
+ letter = l.clone();
+ break 'the_loop;
+ }
+ }
+ }
+ return Err(format!("letter #{} {:?} all neighbors already visited", i+1, letter));
+ }
+ }
+ letter = neighbor;
break;
}
}
+ Ok(answer)
+}
- Some(answer)
+
+fn get_letter_in_direction(letter: &Letter, angle: f64, letters_map: &HashMap<char, Letter>) -> Letter {
+ let mut angle = angle;
+ let mut min_angle = f64::MAX;
+ let mut out = Letter{offset_x: 0, offset_y: 0, character: ' '};
+ // Visit every other letters
+ for (_, other_letter) in letters_map.iter() {
+ if other_letter.key() == letter.key() {
+ continue;
+ }
+ // Find the angle between the two letters
+ let mut t = get_angle(&other_letter.center(), &letter.center());
+ if t < 0.0 {
+ t += 2.0 * std::f64::consts::PI;
+ }
+ if angle < 0.0 {
+ angle += 2.0 * std::f64::consts::PI;
+ }
+ let angle_diff = (angle - t).abs();
+ if angle_diff < min_angle {
+ // Keep track of the letter with the smaller angle difference
+ min_angle = angle_diff;
+ out = other_letter.clone();
+ }
+ }
+ out
}
-#[derive(Debug)]
+fn get_angle(p1: &Point, p2: &Point) -> f64 {
+ (p1.y as f64 - p2.y as f64).atan2(p1.x as f64 - p2.x as f64)
+}
+
+#[derive(Debug, Clone)]
struct Point {
x: u32,
y: u32,
}
-// fn get_contour_red_pixels(img: &DynamicImage) -> Vec<Point> {
-// let top_left_pt = img.Bounds().Min;
-// let bottom_right_pt = img.Bounds().Max;
-// for i := 0; i < img.Bounds().Dx(); i++ {
-// pxColor := img.At(top_left_pt.X+i, top_left_pt.Y)
-// if pxColor == redColor {
-// out = append(out, image.Point{X: top_left_pt.X + i, Y: top_left_pt.Y})
-// }
-// pxColor = img.At(top_left_pt.X+i, bottom_right_pt.Y-1)
-// if pxColor == redColor {
-// out = append(out, image.Point{X: top_left_pt.X + i, Y: bottom_right_pt.Y - 1})
-// }
-// }
-// for i := 1; i < img.Bounds().Dy()-1; i++ {
-// pxColor := img.At(top_left_pt.X, top_left_pt.Y+i)
-// if pxColor == redColor {
-// out = append(out, image.Point{X: top_left_pt.X, Y: top_left_pt.Y + i})
-// }
-// if img.Bounds().Dx() < img.Bounds().Dy() {
-// if img.Bounds().Max.X == 150 {
-// continue;
-// }
-// }
-// pxColor = img.At(bottom_right_pt.X-1, top_left_pt.Y+i)
-// if pxColor == redColor {
-// out = append(out, image.Point{X: bottom_right_pt.X - 1, Y: top_left_pt.Y + i})
-// }
-// }
-// return vec![];
-// }
+impl Point {
+ fn new(x: u32, y: u32) -> Self {
+ Self{x, y}
+ }
+
+ fn add(&self, other: Self) -> Self {
+ Self{x: self.x + other.x, y: self.y + other.y}
+ }
+}
+
+impl From<(u32, u32)> for Point {
+ fn from(value: (u32, u32)) -> Self {
+ Self{x: value.0, y: value.1}
+ }
+}
+
+fn get_contour_red_pixels(top_left_pt: &Point, img: &DynamicImage) -> Vec<Point> {
+ let mut out = vec![];
+ let red_color = Rgba::from([204, 2, 4, 255]);
+ let img_dim = img.dimensions();
+ let bottom_right_pt = top_left_pt.add(Point::from(img_dim));
+ for i in 0..img_dim.0 {
+ if let Some(px_color) = get_pixel_in_bound(img, i, 0) {
+ if px_color == red_color {
+ out.push(Point::new(top_left_pt.x+i, 0));
+ }
+ }
+ if let Some(px_color) = get_pixel_in_bound(img, i, img_dim.1-1) {
+ if px_color == red_color {
+ out.push(Point::new(top_left_pt.x+i, bottom_right_pt.y-1));
+ }
+ }
+ }
+ for i in 1..img_dim.1-1 {
+ if let Some(px_color) = get_pixel_in_bound(img, 0, i) {
+ if px_color == red_color {
+ out.push(Point::new(top_left_pt.x, top_left_pt.y+i));
+ }
+ }
+ if img_dim.0 < img_dim.1 {
+ if img_dim.0 == 150 {
+ continue;
+ }
+ }
+ if let Some(px_color) = get_pixel_in_bound(img, img_dim.0-1, i) {
+ if px_color == red_color {
+ out.push(Point::new(bottom_right_pt.x-1, top_left_pt.y+i));
+ }
+ }
+ }
+ out
+}
+
+fn get_pixel_in_bound(img: &DynamicImage, x: u32, y: u32) -> Option<Rgba<u8>> {
+ let dim = img.dimensions();
+ if x > dim.0 || y > dim.1 {
+ return None;
+ }
+ Some(img.get_pixel(x, y))
+}
// give an image and a valid letter image, return either or not the letter is in that image.
fn img_contains_letter(img: &DynamicImage, letter_img1: &DynamicImage) -> bool {
let on_color = Rgba::from([252, 254, 252, 255]);
let red_color = Rgba::from([204, 2, 4, 255]);
- for y in 0..std::cmp::min(img.dimensions().1, letter_img1.dimensions().1) {
- for x in 0..std::cmp::min(img.dimensions().0, letter_img1.dimensions().0) {
- let good_letter_color = letter_img1.get_pixel(x, y);
- let letter_img_color = img.get_pixel(x, y);
- // If we find an Off pixel where it's supposed to be On, skip that letter
- if (good_letter_color == on_color || good_letter_color == red_color) &&
- (letter_img_color != on_color && letter_img_color != red_color) {
- return false;
+ for y in 0..letter_img1.dimensions().1 {
+ for x in 0..letter_img1.dimensions().0 {
+ if let Some(good_letter_color) = get_pixel_in_bound(letter_img1, x, y) {
+ if let Some(letter_img_color) = get_pixel_in_bound(img, x, y) {
+ // If we find an Off pixel where it's supposed to be On, skip that letter
+ if (good_letter_color == on_color || good_letter_color == red_color) &&
+ (letter_img_color != on_color && letter_img_color != red_color) {
+ return false;
+ }
+ }
}
}
}