captcha.rs (11821B)
1 use std::collections::{HashMap, HashSet}; 2 use std::fmt::{Display, Formatter}; 3 use std::hash::Hash; 4 use base64::{engine::general_purpose, Engine as _}; 5 use bresenham::Bresenham; 6 use image::{DynamicImage, GenericImageView, Rgba}; 7 use lazy_static::lazy_static; 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.get(&letter).expect(format!("letter image not found for {}", letter).as_str()); 90 let img_dec = general_purpose::STANDARD.decode(format!("{}{}", B64_PREFIX, b64_suffix)).unwrap(); 91 image::load_from_memory(&img_dec).unwrap() 92 } 93 94 pub fn solve_b64(b64_str: &str) -> Option<String> { 95 let img_dec = general_purpose::STANDARD.decode(b64_str.strip_prefix("data:image/gif;base64,")?).ok()?; 96 let img = image::load_from_memory(&img_dec).ok()?; 97 if img.width() > 60 { 98 return match solve_difficulty3(&img) { 99 Ok(answer) => Some(answer), 100 Err(e) => { 101 println!("{:?}", e); 102 None 103 }, 104 }; 105 } 106 solve_difficulty2(&img) 107 } 108 109 // This function can solve both difficulty 1 and 2. 110 fn solve_difficulty2(img: &DynamicImage) -> Option<String> { 111 let mut answer = String::new(); 112 for i in 0..NB_CHARS { 113 let sub_img = img.crop_imm(LEFT_PADDING + ((LETTER_WIDTH +1)*i), TOP_PADDING, LETTER_WIDTH, LETTER_HEIGHT); 114 for c in ALPHABET1.chars() { 115 if img_contains_letter(&sub_img, c) { 116 answer.push(c); 117 break; 118 } 119 } 120 } 121 Some(answer) 122 } 123 124 #[derive(Debug, PartialEq, Eq, Hash)] 125 struct Letter { 126 offset: Point, 127 character: char, 128 } 129 130 impl Letter { 131 fn new(offset: Point, character: char) -> Self { 132 Self { offset, character } 133 } 134 135 fn offset(&self) -> Point { 136 self.offset.clone() 137 } 138 139 fn center(&self) -> Point { 140 let offset = self.offset(); 141 Point::new(offset.x + LETTER_WIDTH/2, offset.y + LETTER_HEIGHT/2 - 1) 142 } 143 } 144 145 #[derive(Debug)] 146 struct CaptchaErr(String); 147 148 impl Display for CaptchaErr { 149 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 150 write!(f, "{}", self.0) 151 } 152 } 153 154 impl std::error::Error for CaptchaErr {} 155 156 // SolveDifficulty3 solve captcha for difficulty 3 157 // For each pixel, verify if a match is found. If we do have a match, 158 // verify that we have some "red" in it. 159 // 160 // Red circle is 17x17 (initial point) 161 fn solve_difficulty3(img: &DynamicImage) -> Result<String, CaptchaErr> { 162 //img.save(format!("captcha.gif")).unwrap(); 163 164 // Step1: Find all letters with red on the center 165 let letters_set = find_letters(&img)?; 166 167 // Step2: Find the starting letter 168 let starting = get_starting_letter(&img, &letters_set) 169 .ok_or(CaptchaErr("could not find starting letter".to_owned()))?; 170 171 // Step3: Solve path 172 let answer = solve_path(starting, &letters_set, &img); 173 Ok(answer) 174 } 175 176 // Bresenham algorithm will return an iterator of all the pixels that makes a line in between two points. 177 // From the starting letter, we trace a line to all other letters and count how many red pixels were on the line. 178 // The next letter will be the one that had the most red pixels. 179 // Repeat until we find the whole path. 180 fn solve_path(starting: &Letter, letters_set: &HashSet<Letter>, img: &DynamicImage) -> String { 181 let mut answer = String::new(); 182 let mut remaining: HashSet<_> = letters_set.iter().collect(); 183 let mut letter = remaining.take(&starting).unwrap(); 184 for _ in 0..NB_CHARS { 185 answer.push(letter.character); 186 let mut dest_count = HashMap::<&Letter, usize>::new(); 187 for dest in remaining.iter() { 188 let red = Bresenham::new(letter.center().into(), dest.center().into()) 189 .filter(|(x, y)| is_red(img.get_pixel(*x as u32, *y as u32))) 190 .count(); 191 dest_count.insert(dest, red); 192 } 193 if let Some((dest_max, _)) = dest_count.into_iter().max_by_key(|e| e.1) { 194 letter = remaining.take(dest_max).unwrap(); 195 } 196 } 197 answer 198 } 199 200 fn find_letters(img: &DynamicImage) -> Result<HashSet<Letter>, CaptchaErr> { 201 const IMAGE_WIDTH: u32 = 150; 202 const IMAGE_HEIGHT: u32 = 200; 203 const MIN_PX_FOR_LETTER: usize = 21; 204 let mut letters_set = HashSet::new(); 205 for y in 0..IMAGE_HEIGHT-LETTER_HEIGHT { 206 for x in 0..IMAGE_WIDTH-LETTER_WIDTH { 207 let letter_img = img.crop_imm(x, y, LETTER_WIDTH, LETTER_HEIGHT); 208 // We know that minimum amount of pixels on to form a letter is 21 209 // We can skip squares that do not have this prerequisite 210 // Check middle pixels for red, if no red pixels, we can ignore that square 211 if count_px_on(&letter_img) < MIN_PX_FOR_LETTER || !has_red_in_center_area(&letter_img) { 212 continue; 213 } 214 'alphabet_loop: for c in ALPHABET1.chars() { 215 if !img_contains_letter(&letter_img, c) { 216 continue; 217 } 218 // "w" fits in "W". So if we find "W" 1 px bellow, discard "w" 219 for (a, b, x, y) in vec![('w', 'W', x, y+1), ('k', 'K', x+1, y+1)] { 220 if c == a { 221 let one_px_down_img = img.crop_imm(x, y, LETTER_WIDTH, LETTER_HEIGHT); 222 if img_contains_letter(&one_px_down_img, b) { 223 continue 'alphabet_loop; 224 } 225 } 226 } 227 letters_set.insert(Letter::new(Point::new(x, y), c)); 228 break; 229 } 230 } 231 } 232 if letters_set.len() != NB_CHARS as usize { 233 return Err(CaptchaErr(format!("did not find exactly 5 letters {}", letters_set.len()))); 234 } 235 Ok(letters_set) 236 } 237 238 fn get_starting_letter<'a>(img: &DynamicImage, letters_set: &'a HashSet<Letter>) -> Option<&'a Letter> { 239 const MIN_STARTING_PT_RED_PX: usize = 50; 240 for letter in letters_set.iter() { 241 let square = img.crop_imm(letter.offset.x-5, letter.offset.y-3, LETTER_WIDTH+5+6, LETTER_HEIGHT+3+2); 242 let count_red = count_red_px(&square); 243 if count_red > MIN_STARTING_PT_RED_PX { 244 return Some(letter); 245 } 246 } 247 None 248 } 249 250 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 251 struct Point { 252 x: u32, 253 y: u32, 254 } 255 256 impl Point { 257 fn new(x: u32, y: u32) -> Self { 258 Self{x, y} 259 } 260 } 261 262 impl From<Point> for bresenham::Point { 263 fn from(value: Point) -> Self { 264 (value.x as isize, value.y as isize) 265 } 266 } 267 268 // give an image and a valid letter image, return either or not the letter is in that image. 269 fn img_contains_letter(img: &DynamicImage, c: char) -> bool { 270 let letter_img = get_letter_img(c); 271 if letter_img.dimensions() != img.dimensions() { 272 return false; 273 } 274 for y in 0..LETTER_HEIGHT { 275 for x in 0..LETTER_WIDTH { 276 let good_letter_color = letter_img.get_pixel(x, y); 277 let letter_img_color = img.get_pixel(x, y); 278 // If we find an Off pixel where it's supposed to be On, skip that letter 279 if is_on(good_letter_color) && !is_on(letter_img_color) { 280 return false; 281 } 282 } 283 } 284 true 285 } 286 287 fn is_on(c: Rgba<u8>) -> bool { 288 c == *ON_COLOR || c == *RED_COLOR 289 } 290 291 fn is_red(c: Rgba<u8>) -> bool { 292 c == *RED_COLOR 293 } 294 295 fn has_red_in_center_area(letter_img: &DynamicImage) -> bool { 296 letter_img.view(LETTER_WIDTH/2 - 1, LETTER_HEIGHT/2 - 1, 2, 2) 297 .pixels() 298 .any(|(_, _, c)| is_red(c)) 299 } 300 301 // Count pixels that are On (either white or red) 302 fn count_px_on(img: &DynamicImage) -> usize { 303 img.pixels() 304 .filter(|(_, _, c)| is_on(*c)) 305 .count() 306 } 307 308 // Count pixels that are red 309 fn count_red_px(img: &DynamicImage) -> usize { 310 img.pixels() 311 .filter(|(_, _, c)| is_red(*c)) 312 .count() 313 }