From fa9e170cc81d9fddb346972712dc55f26e0ee654 Mon Sep 17 00:00:00 2001 From: Dan Schafer Date: Wed, 29 Jul 2026 10:28:32 -0700 Subject: [PATCH] Harden text and image puzzle parsing Return errors for gridless images, calculate mark ratios over the inspected image area, and preserve edge spaces as blank partial-state squares. --- CHANGELOG.md | 3 +++ src/file.rs | 22 ++++++++++++++++++++-- src/image.rs | 28 +++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0273bd..5e634ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Give out-of-range values passed to bitset-backed sets consistent, descriptive panics. +- Return an error instead of panicking when an image contains no detectable grid. +- Calculate queen and X pixel ratios against the inspected image area. +- Preserve spaces used as blank markers at the edges of partial-state grids. ## [1.0.0] - 2025-04-23 diff --git a/src/file.rs b/src/file.rs index 4cdce9e..c9ad39b 100644 --- a/src/file.rs +++ b/src/file.rs @@ -56,7 +56,7 @@ impl FromStr for InputSquares { type Err = anyhow::Error; fn from_str(s: &str) -> Result { - let lines = s.trim().lines().collect::>(); + let lines = s.trim_matches(['\n', '\r']).lines().collect::>(); let size = lines.len(); for (line_num, &line) in lines.iter().enumerate() { ensure!( @@ -115,7 +115,7 @@ impl FromStr for QueensFile { type Err = anyhow::Error; fn from_str(s: &str) -> Result { - let lines = s.trim().lines().collect::>(); + let lines = s.trim_matches(['\n', '\r']).lines().collect::>(); let lines_len = lines.len(); ensure!(lines_len != 0, "Invalid solve state: no lines found."); @@ -151,6 +151,15 @@ mod tests { Ok(()) } + #[test] + fn input_squares_preserve_edge_spaces() -> Result<()> { + let input_squares = InputSquares::from_str(" Qxx\nxx..\nx...\n \n")?; + + assert_eq!(input_squares.0[0], None); + assert!(input_squares.0[12..].iter().all(Option::is_none)); + Ok(()) + } + #[test] fn input_squares_display() -> Result<()> { let input_str = "Qxxx\nxx..\nx...\nx..."; @@ -239,4 +248,13 @@ mod tests { ); Ok(()) } + #[test] + fn queens_file_preserves_edge_spaces() -> Result<()> { + let file = QueensFile::from_str("wwww\nkkkk\nrrrr\nbbbb\n\n \n....\n____\n \n")?; + let squares = &file.squares.as_ref().unwrap().0; + + assert!(squares[..4].iter().all(Option::is_none)); + assert!(squares[12..].iter().all(Option::is_none)); + Ok(()) + } } diff --git a/src/image.rs b/src/image.rs index 9bbf9d1..b3ade55 100644 --- a/src/image.rs +++ b/src/image.rs @@ -191,7 +191,9 @@ fn get_other_ratio(view: &SubImage<&RgbImage>, rgb_color: &Rgb) -> f32 { .pixels() .filter(|(_, _, p)| color_distance(*p, *rgb_color) > COLOR_DISTANCE_THRESHOLD) .count(); - (other_count as f32) / ((width * height) as f32) + let center_width = width - (2 * width / BORDER_DENOM); + let center_height = height - (2 * height / BORDER_DENOM); + (other_count as f32) / ((center_width * center_height) as f32) } fn find_grid_ranges(img: &RgbImage, range: Range, is_vertical: bool) -> Vec> { @@ -208,6 +210,9 @@ fn find_grid_ranges(img: &RgbImage, range: Range, is_vertical: bool) -> Vec }); grid_ranges.extend(grid_ranges_iter); let grid_ranges_len = grid_ranges.len(); + if grid_ranges_len == 0 { + return grid_ranges; + } let median_grid_length = grid_ranges .clone() .select_nth_unstable_by(grid_ranges_len / 2, |a, b| a.len().cmp(&b.len())) @@ -318,3 +323,24 @@ fn map_image_to_square_colors(image_colors: &[Rgb]) -> [SquareColor; MAX_UNI image_to_square_color } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn analyze_grid_image_rejects_blank_image() { + let img = RgbImage::from_pixel(64, 64, Rgb([255, 255, 255])); + assert!(analyze_grid_image(&img).is_err()); + } + + #[test] + fn other_ratio_uses_only_the_inspected_area() { + let white = Rgb([255, 255, 255]); + let mut img = RgbImage::from_pixel(10, 10, white); + img.put_pixel(0, 0, Rgb([0, 0, 0])); + img.put_pixel(5, 5, Rgb([0, 0, 0])); + + assert_eq!(get_other_ratio(&img.view(0, 0, 10, 10), &white), 1.0 / 64.0); + } +}