captcha.rs (12093B)
1 use base64::{engine::general_purpose, Engine as _}; 2 use bresenham::Bresenham; 3 use image::{DynamicImage, GenericImageView, Rgba}; 4 use lazy_static::lazy_static; 5 use std::collections::{HashMap, HashSet}; 6 use std::fmt::{Display, Formatter}; 7 use std::hash::Hash; 8 9 const B64_PREFIX: &'static str = "R0lGODlhCAAOAIAAAAAAAAAAACH5BAgAAAAALAAAAAAIAA4AgAQCBPz+/AI"; 10 // list of letters that contains other letters: (h, n) (I, l) (y, u) (Q, O) (B, 3) (E, L) (R, P) 11 // So our alphabet needs to have "I" before "l" since "l" is contained by "I". 12 const ALPHABET1: &'static str = "abdcefgh1ijkImnpoqrstyQuvwxzABCDEGJKMNHLORPFSTlUVWXYZ023456789"; 13 const LETTER_WIDTH: u32 = 8; 14 const LETTER_HEIGHT: u32 = 14; 15 const NB_CHARS: u32 = 5; 16 const LEFT_PADDING: u32 = 5; // left padding for difficulty 1 and 2 17 const TOP_PADDING: u32 = 7; // top padding for difficulty 1 and 2 18 19 lazy_static! { 20 static ref B64_MAP: HashMap<char, &'static str> = HashMap::from([ 21 ('0', "VhI8Qkbv3FIvGMeiQ1fPSzSXiSAIFADs="), 22 ('1', "UhI8QkcvnHlpJSXgNbdnO2FViVQAAOw=="), 23 ('2', "UhI+hcWruGkMgSmrfvGnrtVDiKBYAOw=="), 24 ('3', "UhH+hatyBEkTuzVjpldWtHIUiUgAAOw=="), 25 ('4', "VhI8XkcvqIFiGTmbvdRFl2TzJSCYFADs="), 26 ('5', "WhG+hG6CYGnwrygedRIw3jGlhRpZGAQA7"), 27 ('6', "XhI+hcWruGoiJrRcha5fPTS0bQpamUQAAOw=="), 28 ('7', "ThG+hq5jhQEPz1OeuhJT3CIZiAQA7"), 29 ('8', "XhI+hcWruGosRLPYwxLnaqXEXQpYmUgAAOw=="), 30 ('9', "VhI+hcWru2kosTjAjxduydyHiSBoFADs="), 31 ('A', "VhI8Qkbv3FIvGMeiQ3RlT+X3JSBoFADs="), 32 ('B', "VhG+hm3EK4GMtLimtntlmeHXISJYFADs="), 33 ('C', "VhI+hatyLAIwuhSgv1edWt1TYSAIFADs="), 34 ('D', "WhG+hm3EK4GNTNhvpdZXnvHjISJZAAQA7"), 35 ('E', "UhG+hG6CYGnyTSYrw0RE6K3niaBQAOw=="), 36 ('F', "ThG+hy5jhgIpsSugs0oe/CIZiAQA7"), 37 ('G', "VhI+hatyLXgQuhbMqhfhWl13TSB4FADs="), 38 ('H', "UhG8RqMr93Gm0xjVhlkl3BIaiUQAAOw=="), 39 ('I', "UhH+hi4rmXmzhgZmq1JQuboUiUAAAOw=="), 40 ('J', "UhI8QG6mdlpMRInqhpRI/64TiUQAAOw=="), 41 ('K', "XhG8RqHruQIQrNXbfirLO2oUaQpamUQAAOw=="), 42 ('L', "UhG8RmMvKAHwOTVvP1bmpH4UiUgAAOw=="), 43 ('M', "WhG8RqJ0NI1DTUVgrPZg7vz3ISJZGAQA7"), 44 ('N', "WhG8RqH3tAFTJUSgXzpvTGlXISJZGAQA7"), 45 ('O', "VhI+hcWru2kpTxlfxBZBx/23ISJYFADs="), 46 ('P', "ThG+hG6DI4JJsPuQavhJnD4ZiAQA7"), 47 ('Q', "WhI+hcWru2kpTxjdhXSxeDjDISJZAAQA7"), 48 ('R', "WhG+hG6DI4JJs1vNc07jlNHXISJZGAQA7"), 49 ('S', "UhH+hC4obkHGywVjpw1tbC1XiiBQAOw=="), 50 ('T', "UhG+hq9cIHpIuwGghTXn2eoXiVQAAOw=="), 51 ('U', "UhG8RqMr93GlrQivT1UBmioTiiBQAOw=="), 52 ('V', "VhG8RqMr9gJOLpjon1bsefiHiSIoFADs="), 53 ('W', "UhG8RqMr93GnLUWBhplzyh4TiaBQAOw=="), 54 ('X', "UhG8RqMrrQJQNUXvfjG5S72GiWAAAOw=="), 55 ('Y', "UhG8RqMrrQJQNUXvtPDst/WHiiBQAOw=="), 56 ('Z', "UhG+hG5jtVIRHIlvpcwBnpnHiWAAAOw=="), 57 ('a', "ThI+pq+FuYAyNvitnfuB2yoRKAQA7"), 58 ('b', "WhG8RmMvKAFSONukaPDRji4VgRJZHAQA7"), 59 ('c', "RhI+pq+FuYHwt1CWBfJn5VAAAOw=="), 60 ('d', "VhI8YkbD93JtMrmoutpvmeEnNSB4FADs="), 61 ('e', "RhI+pq+FxnJEyvntXBRWzzxQAOw=="), 62 ('f', "VhI8QG7f2VJNwoliZpQm7XSXiSBoFADs="), 63 ('g', "VhI+pm+EO3nnQwBqDpXvRq03aFy4FADs="), 64 ('h', "VhG8RmMvKAFSONukaPDTjrXlgRJYFADs="), 65 ('i', "ThI8Qkcrd1kMrzlNTpldKCIZAAQA7"), 66 ('j', "UhI8XkbANF0uPTlTxVSw/P0kZVAAAOw=="), 67 ('k', "VhH8RaMrgWJwrQrUSRs7Sll3PSB4FADs="), 68 ('l', "RhI+haMueAgPw1CZfjvrODxYAOw=="), 69 ('m', "ThI+piwHh4ItUWkjn1Rl3x4RKAQA7"), 70 ('n', "UhI+pixHgHnSG0hNljY1jvzHiUgAAOw=="), 71 ('o', "ThI+pq+FxnJEyPvSgBbXyy4RMAQA7"), 72 ('p', "VhI+pixHgHnSG0hNljS3vOUlXxygFADs="), 73 ('q', "VhI+pq+HwHnTS0IBuxpLaiCXVMSIFADs="), 74 ('r', "ShI+pixHgnInzyXTbw1uzDxoFADs="), 75 ('s', "RhI+pm+EPHHphUanorLeyvxQAOw=="), 76 ('t', "UhI95EcrIYlsTVuqueYD3qIQiUgAAOw=="), 77 ('u', "ThI+pixHt3onSUOggyJvHzoRLAQA7"), 78 ('v', "ThI+pixHt3nGAVmnt1VtOz4RLAQA7"), 79 ('w', "UhI+pixHt3onPAXprzJliyzHiUgAAOw=="), 80 ('x', "ThI+pixH9nJHTPZjsBVTSyoRLAQA7"), 81 ('y', "VhI+pixHt3onSUOggyJvHXlkPxS0FADs="), 82 ('z', "PhI+pm+GvXAuzIjkfZXwVADs="), 83 ]); 84 static ref RED_COLOR: Rgba<u8> = Rgba::from([204, 2, 4, 255]); 85 static ref ON_COLOR: Rgba<u8> = Rgba::from([252, 254, 252, 255]); 86 } 87 88 fn get_letter_img(letter: char) -> DynamicImage { 89 let b64_suffix = B64_MAP 90 .get(&letter) 91 .expect(format!("letter image not found for {}", letter).as_str()); 92 let img_dec = general_purpose::STANDARD 93 .decode(format!("{}{}", B64_PREFIX, b64_suffix)) 94 .unwrap(); 95 image::load_from_memory(&img_dec).unwrap() 96 } 97 98 pub fn solve_b64(b64_str: &str) -> Option<String> { 99 let img_dec = general_purpose::STANDARD 100 .decode(b64_str.strip_prefix("data:image/gif;base64,")?) 101 .ok()?; 102 let img = image::load_from_memory(&img_dec).ok()?; 103 if img.width() > 60 { 104 return match solve_difficulty3(&img) { 105 Ok(answer) => Some(answer), 106 Err(e) => { 107 println!("{:?}", e); 108 None 109 } 110 }; 111 } 112 solve_difficulty2(&img) 113 } 114 115 // This function can solve both difficulty 1 and 2. 116 fn solve_difficulty2(img: &DynamicImage) -> Option<String> { 117 let mut answer = String::new(); 118 for i in 0..NB_CHARS { 119 let sub_img = img.crop_imm( 120 LEFT_PADDING + ((LETTER_WIDTH + 1) * i), 121 TOP_PADDING, 122 LETTER_WIDTH, 123 LETTER_HEIGHT, 124 ); 125 for c in ALPHABET1.chars() { 126 if img_contains_letter(&sub_img, c) { 127 answer.push(c); 128 break; 129 } 130 } 131 } 132 Some(answer) 133 } 134 135 #[derive(Debug, PartialEq, Eq, Hash)] 136 struct Letter { 137 offset: Point, 138 character: char, 139 } 140 141 impl Letter { 142 fn new(offset: Point, character: char) -> Self { 143 Self { offset, character } 144 } 145 146 fn offset(&self) -> Point { 147 self.offset.clone() 148 } 149 150 fn center(&self) -> Point { 151 let offset = self.offset(); 152 Point::new( 153 offset.x + LETTER_WIDTH / 2, 154 offset.y + LETTER_HEIGHT / 2 - 1, 155 ) 156 } 157 } 158 159 #[derive(Debug)] 160 struct CaptchaErr(String); 161 162 impl Display for CaptchaErr { 163 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 164 write!(f, "{}", self.0) 165 } 166 } 167 168 impl std::error::Error for CaptchaErr {} 169 170 // SolveDifficulty3 solve captcha for difficulty 3 171 // For each pixel, verify if a match is found. If we do have a match, 172 // verify that we have some "red" in it. 173 // 174 // Red circle is 17x17 (initial point) 175 fn solve_difficulty3(img: &DynamicImage) -> Result<String, CaptchaErr> { 176 //img.save(format!("captcha.gif")).unwrap(); 177 178 // Step1: Find all letters with red on the center 179 let letters_set = find_letters(&img)?; 180 181 // Step2: Find the starting letter 182 let starting = get_starting_letter(&img, &letters_set) 183 .ok_or(CaptchaErr("could not find starting letter".to_owned()))?; 184 185 // Step3: Solve path 186 let answer = solve_path(starting, &letters_set, &img); 187 Ok(answer) 188 } 189 190 // Bresenham algorithm will return an iterator of all the pixels that makes a line in between two points. 191 // From the starting letter, we trace a line to all other letters and count how many red pixels were on the line. 192 // The next letter will be the one that had the most red pixels. 193 // Repeat until we find the whole path. 194 fn solve_path(starting: &Letter, letters_set: &HashSet<Letter>, img: &DynamicImage) -> String { 195 let mut answer = String::new(); 196 let mut remaining: HashSet<_> = letters_set.iter().collect(); 197 let mut letter = remaining.take(&starting).unwrap(); 198 for _ in 0..NB_CHARS { 199 answer.push(letter.character); 200 let mut dest_count = HashMap::<&Letter, usize>::new(); 201 for dest in remaining.iter() { 202 let red = Bresenham::new(letter.center().into(), dest.center().into()) 203 .filter(|(x, y)| is_red(img.get_pixel(*x as u32, *y as u32))) 204 .count(); 205 dest_count.insert(dest, red); 206 } 207 if let Some((dest_max, _)) = dest_count.into_iter().max_by_key(|e| e.1) { 208 letter = remaining.take(dest_max).unwrap(); 209 } 210 } 211 answer 212 } 213 214 fn find_letters(img: &DynamicImage) -> Result<HashSet<Letter>, CaptchaErr> { 215 const IMAGE_WIDTH: u32 = 150; 216 const IMAGE_HEIGHT: u32 = 200; 217 const MIN_PX_FOR_LETTER: usize = 21; 218 let mut letters_set = HashSet::new(); 219 for y in 0..IMAGE_HEIGHT - LETTER_HEIGHT { 220 for x in 0..IMAGE_WIDTH - LETTER_WIDTH { 221 let letter_img = img.crop_imm(x, y, LETTER_WIDTH, LETTER_HEIGHT); 222 // We know that minimum amount of pixels on to form a letter is 21 223 // We can skip squares that do not have this prerequisite 224 // Check middle pixels for red, if no red pixels, we can ignore that square 225 if count_px_on(&letter_img) < MIN_PX_FOR_LETTER || !has_red_in_center_area(&letter_img) 226 { 227 continue; 228 } 229 'alphabet_loop: for c in ALPHABET1.chars() { 230 if !img_contains_letter(&letter_img, c) { 231 continue; 232 } 233 // "w" fits in "W". So if we find "W" 1 px bellow, discard "w" 234 for (a, b, x, y) in vec![('w', 'W', x, y + 1), ('k', 'K', x + 1, y + 1)] { 235 if c == a { 236 let one_px_down_img = img.crop_imm(x, y, LETTER_WIDTH, LETTER_HEIGHT); 237 if img_contains_letter(&one_px_down_img, b) { 238 continue 'alphabet_loop; 239 } 240 } 241 } 242 letters_set.insert(Letter::new(Point::new(x, y), c)); 243 break; 244 } 245 } 246 } 247 if letters_set.len() != NB_CHARS as usize { 248 return Err(CaptchaErr(format!( 249 "did not find exactly 5 letters {}", 250 letters_set.len() 251 ))); 252 } 253 Ok(letters_set) 254 } 255 256 fn get_starting_letter<'a>( 257 img: &DynamicImage, 258 letters_set: &'a HashSet<Letter>, 259 ) -> Option<&'a Letter> { 260 const MIN_STARTING_PT_RED_PX: usize = 50; 261 for letter in letters_set.iter() { 262 let square = img.crop_imm( 263 letter.offset.x - 5, 264 letter.offset.y - 3, 265 LETTER_WIDTH + 5 + 6, 266 LETTER_HEIGHT + 3 + 2, 267 ); 268 let count_red = count_red_px(&square); 269 if count_red > MIN_STARTING_PT_RED_PX { 270 return Some(letter); 271 } 272 } 273 None 274 } 275 276 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 277 struct Point { 278 x: u32, 279 y: u32, 280 } 281 282 impl Point { 283 fn new(x: u32, y: u32) -> Self { 284 Self { x, y } 285 } 286 } 287 288 impl From<Point> for bresenham::Point { 289 fn from(value: Point) -> Self { 290 (value.x as isize, value.y as isize) 291 } 292 } 293 294 // give an image and a valid letter image, return either or not the letter is in that image. 295 fn img_contains_letter(img: &DynamicImage, c: char) -> bool { 296 let letter_img = get_letter_img(c); 297 if letter_img.dimensions() != img.dimensions() { 298 return false; 299 } 300 for y in 0..LETTER_HEIGHT { 301 for x in 0..LETTER_WIDTH { 302 let good_letter_color = letter_img.get_pixel(x, y); 303 let letter_img_color = img.get_pixel(x, y); 304 // If we find an Off pixel where it's supposed to be On, skip that letter 305 if is_on(good_letter_color) && !is_on(letter_img_color) { 306 return false; 307 } 308 } 309 } 310 true 311 } 312 313 fn is_on(c: Rgba<u8>) -> bool { 314 c == *ON_COLOR || c == *RED_COLOR 315 } 316 317 fn is_red(c: Rgba<u8>) -> bool { 318 c == *RED_COLOR 319 } 320 321 fn has_red_in_center_area(letter_img: &DynamicImage) -> bool { 322 letter_img 323 .view(LETTER_WIDTH / 2 - 1, LETTER_HEIGHT / 2 - 1, 2, 2) 324 .pixels() 325 .any(|(_, _, c)| is_red(c)) 326 } 327 328 // Count pixels that are On (either white or red) 329 fn count_px_on(img: &DynamicImage) -> usize { 330 img.pixels().filter(|(_, _, c)| is_on(*c)).count() 331 } 332 333 // Count pixels that are red 334 fn count_red_px(img: &DynamicImage) -> usize { 335 img.pixels().filter(|(_, _, c)| is_red(*c)).count() 336 }