bhcli

A TUI for chatting on LE PHP Chats (onion)
git clone https://git.dasho.dev/n0tr1v/bhcli.git
Log | Files | Refs | README

commit 6a8e8bfe731b86fffdd37caa05eb00a80aebc4e1
parent 480140a7b98b4530bc293f858b0be91cd0a2bf2b
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Wed, 29 Mar 2023 15:20:10 -0700

diff 3

Diffstat:
Msrc/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; + } + } } } }