use std::fs;
use std::path::Path;
use crate::corpus::{Corpus, CorpusImage, ImageCategory};
use crate::error::{Error, Result};
const SUPPORTED_EXTENSIONS: &[&str] = &[
"png", "jpg", "jpeg", "webp", "avif", "jxl", "heic", "heif", "bmp", "tiff", "tif",
];
pub fn discover_corpus(path: &Path) -> Result<Corpus> {
if !path.exists() {
return Err(Error::Corpus(format!(
"Path does not exist: {}",
path.display()
)));
}
if !path.is_dir() {
return Err(Error::Corpus(format!(
"Path is not a directory: {}",
path.display()
)));
}
let name = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("corpus")
.to_string();
let mut corpus = Corpus::new(name, path);
discover_recursive(path, path, &mut corpus.images)?;
infer_categories(&mut corpus);
corpus.update_category_counts();
Ok(corpus)
}
fn discover_recursive(root: &Path, current: &Path, images: &mut Vec<CorpusImage>) -> Result<()> {
let entries = fs::read_dir(current).map_err(|e| {
Error::Corpus(format!(
"Failed to read directory {}: {}",
current.display(),
e
))
})?;
for entry in entries {
let entry = entry.map_err(|e| {
Error::Corpus(format!(
"Failed to read entry in {}: {}",
current.display(),
e
))
})?;
let path = entry.path();
if path.is_dir() {
if path
.file_name()
.and_then(|s| s.to_str())
.map_or(false, |s| s.starts_with('.'))
{
continue;
}
discover_recursive(root, &path, images)?;
} else if path.is_file() {
if let Some(img) = try_load_image_info(&path, root) {
images.push(img);
}
}
}
Ok(())
}
fn try_load_image_info(path: &Path, root: &Path) -> Option<CorpusImage> {
let extension = path.extension()?.to_str()?.to_lowercase();
if !SUPPORTED_EXTENSIONS.contains(&extension.as_str()) {
return None;
}
let metadata = fs::metadata(path).ok()?;
let file_size = metadata.len();
let relative_path = path.strip_prefix(root).ok()?.to_path_buf();
let (width, height) = get_image_dimensions(path).unwrap_or((0, 0));
let format = match extension.as_str() {
"jpg" | "jpeg" => "jpeg",
"jxl" => "jpegxl",
"heic" | "heif" => "heif",
"tif" | "tiff" => "tiff",
other => other,
}
.to_string();
Some(CorpusImage {
relative_path,
category: None,
width,
height,
file_size,
checksum: None,
format,
})
}
fn get_image_dimensions(path: &Path) -> Option<(u32, u32)> {
let data = fs::read(path).ok()?;
if data.starts_with(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]) {
if data.len() >= 24 {
let width = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
let height = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
return Some((width, height));
}
}
if data.starts_with(&[0xFF, 0xD8]) {
return parse_jpeg_dimensions(&data);
}
if data.len() >= 30 && &data[0..4] == b"RIFF" && &data[8..12] == b"WEBP" {
return parse_webp_dimensions(&data);
}
None
}
fn parse_jpeg_dimensions(data: &[u8]) -> Option<(u32, u32)> {
let mut i = 2;
while i + 9 < data.len() {
if data[i] != 0xFF {
i += 1;
continue;
}
let marker = data[i + 1];
if matches!(
marker,
0xC0 | 0xC1
| 0xC2
| 0xC3
| 0xC5
| 0xC6
| 0xC7
| 0xC9
| 0xCA
| 0xCB
| 0xCD
| 0xCE
| 0xCF
) {
let height = u32::from(data[i + 5]) << 8 | u32::from(data[i + 6]);
let width = u32::from(data[i + 7]) << 8 | u32::from(data[i + 8]);
return Some((width, height));
}
if i + 3 >= data.len() {
break;
}
let length = u16::from(data[i + 2]) << 8 | u16::from(data[i + 3]);
i += 2 + length as usize;
}
None
}
fn parse_webp_dimensions(data: &[u8]) -> Option<(u32, u32)> {
if data.len() >= 30 && &data[12..16] == b"VP8 " {
if data.len() >= 26 {
let width = u32::from(data[26]) | (u32::from(data[27] & 0x3F) << 8);
let height = u32::from(data[28]) | (u32::from(data[29] & 0x3F) << 8);
return Some((width, height));
}
}
if data.len() >= 25 && &data[12..16] == b"VP8L" {
let bits = u32::from(data[21])
| (u32::from(data[22]) << 8)
| (u32::from(data[23]) << 16)
| (u32::from(data[24]) << 24);
let width = (bits & 0x3FFF) + 1;
let height = ((bits >> 14) & 0x3FFF) + 1;
return Some((width, height));
}
if data.len() >= 30 && &data[12..16] == b"VP8X" {
let width = u32::from(data[24]) | (u32::from(data[25]) << 8) | (u32::from(data[26]) << 16);
let height = u32::from(data[27]) | (u32::from(data[28]) << 8) | (u32::from(data[29]) << 16);
return Some((width + 1, height + 1));
}
None
}
fn infer_categories(corpus: &mut Corpus) {
for img in &mut corpus.images {
if img.category.is_some() {
continue;
}
for component in img.relative_path.components() {
if let std::path::Component::Normal(name) = component {
if let Some(name_str) = name.to_str() {
if let Some(cat) = ImageCategory::from_str_loose(name_str) {
img.category = Some(cat);
break;
}
}
}
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_png_dimensions() {
let mut png = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
png.extend_from_slice(&[0, 0, 0, 13]); png.extend_from_slice(b"IHDR");
png.extend_from_slice(&100u32.to_be_bytes()); png.extend_from_slice(&50u32.to_be_bytes()); png.extend_from_slice(&[8, 2, 0, 0, 0]);
let dims = get_image_dimensions_from_bytes(&png);
assert_eq!(dims, Some((100, 50)));
}
fn get_image_dimensions_from_bytes(data: &[u8]) -> Option<(u32, u32)> {
if data.starts_with(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]) {
if data.len() >= 24 {
let width = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
let height = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
return Some((width, height));
}
}
None
}
}