use clap::Parser;
use log::{error, info};
use std::process;
mod cli;
mod error;
mod processor;
mod utils;
#[derive(Parser, Debug)]
#[command(
name = "rawlib",
version,
author,
about = "Extract thumbnails from RAW image files",
long_about = "A fast and efficient tool to extract embedded thumbnails from RAW camera files.\n\
Supports batch processing, recursive directory scanning, and multiple output options.\n\n\
Supported formats: CR2, CR3, NEF, NRW, ARW, SRF, SR2, RAF, ORF, RW2, DNG, and more.",
after_help = "EXAMPLES:\n \
rawlib photo.NEF # Extract to photo.jpg\n \
rawlib photo.NEF -o thumb.jpg # Specify output\n \
rawlib ./photos/ -o ./thumbs/ # Batch process\n \
rawlib ./photos/ -r --progress # Recursive with progress\n \
rawlib ./photos/ --overwrite rename # Rename conflicts\n \
RUST_LOG=debug rawlib photo.NEF -v # Debug logging"
)]
struct Cli {
#[arg(
value_name = "INPUT",
required = true,
help = "Input RAW file(s) or directory"
)]
inputs: Vec<String>,
#[arg(
short = 'o',
long = "output",
value_name = "PATH",
help = "Output file or directory (default: same as input with .jpg extension)"
)]
output: Option<String>,
#[arg(
long = "overwrite",
default_value = "skip",
value_parser = ["skip", "overwrite", "rename"],
help = "File overwrite behavior"
)]
overwrite: String,
#[arg(
short = 'r',
long = "recursive",
help = "Recursively scan directories"
)]
recursive: bool,
#[arg(
short = 'v',
long = "verbose",
help = "Show detailed information"
)]
verbose: bool,
#[arg(
short = 'q',
long = "quiet",
conflicts_with = "verbose",
help = "Suppress output except errors"
)]
quiet: bool,
#[arg(
long = "progress",
help = "Show progress bar"
)]
progress: bool,
#[arg(
short = 'j',
long = "jobs",
value_name = "N",
help = "Number of parallel jobs (default: number of CPU cores)"
)]
jobs: Option<usize>,
#[arg(
short = 'f',
long = "format",
default_value = "auto",
value_parser = ["auto", "jpg", "jpeg", "bmp"],
help = "Output format"
)]
format: String,
#[arg(
long = "exif",
help = "Show EXIF metadata instead of extracting thumbnail"
)]
exif: bool,
#[arg(
long = "json",
help = "Output EXIF data in JSON format (requires --exif)"
)]
json: bool,
#[arg(
long = "extensions",
value_delimiter = ',',
default_value = "cr2,cr3,nef,nrw,arw,srf,sr2,raf,orf,rw2,dng",
help = "Comma-separated RAW file extensions"
)]
extensions: Vec<String>,
}
fn main() {
env_logger::Builder::from_default_env()
.filter_level(log::LevelFilter::Info)
.init();
let cli = Cli::parse();
if cli.exif {
run_exif_mode(&cli);
return;
}
info!("Starting rawlib thumbnail extractor v{}", env!("CARGO_PKG_VERSION"));
let config = match cli::Config::from_cli(cli) {
Ok(config) => config,
Err(e) => {
error!("Configuration error: {}", e);
eprintln!("错误: {}", e);
process::exit(1);
}
};
match cli::run(config) {
Ok(stats) => {
if !stats.quiet {
stats.print_summary();
}
info!("Processing completed successfully");
process::exit(0);
}
Err(e) => {
error!("Processing failed: {}", e);
eprintln!("错误: {}", e);
process::exit(1);
}
}
}
fn run_exif_mode(cli: &Cli) {
use rawlib::exif::{extract_exif, extract_exif_parallel};
let files = collect_input_files(&cli.inputs, cli.recursive, &cli.extensions);
if files.is_empty() {
eprintln!("错误: 未找到 RAW 文件");
process::exit(1);
}
if cli.json {
println!("[");
}
let results = if files.len() == 1 || cli.jobs == Some(1) {
files.iter()
.map(|f| (f.clone(), extract_exif(f)))
.collect::<Vec<_>>()
} else {
extract_exif_parallel(&files, cli.jobs)
};
let mut first = true;
for (path, result) in results {
if cli.json {
if !first {
println!(",");
}
first = false;
match result {
Ok(exif) => {
print_exif_json(&path, &exif);
}
Err(e) => {
println!(" {{\"path\": {:?}, \"error\": {:?}}}", path.display(), e.to_string());
}
}
} else {
match result {
Ok(exif) => {
println!("\n{}:", path.display());
print_exif_human(&exif);
}
Err(e) => {
eprintln!("✗ {}: {}", path.display(), e);
}
}
}
}
if cli.json {
println!("\n]");
}
}
fn print_exif_human(exif: &rawlib::exif::ExifData) {
if let Some(ref make) = exif.make {
println!(" 相机厂商: {}", make);
}
if let Some(ref model) = exif.model {
println!(" 相机型号: {}", model);
}
if let Some(ref lens) = exif.lens_model {
println!(" 镜头型号: {}", lens);
}
if let Some(ref date) = exif.date_time_original {
println!(" 拍摄时间: {}", date);
}
if let Some(ref exp) = exif.exposure_time {
println!(" 快门速度: {}", exp);
}
if let Some(ref fnum) = exif.f_number {
println!(" 光圈: {}", fnum);
}
if let Some(iso) = exif.iso {
println!(" ISO: {}", iso);
}
if let Some(ref focal) = exif.focal_length {
println!(" 焦距: {}", focal);
}
if let (Some(w), Some(h)) = (exif.image_width, exif.image_height) {
println!(" 图像尺寸: {}x{}", w, h);
}
if exif.has_gps() {
if let Some((lat, lon)) = exif.gps_coordinates() {
println!(" GPS: {:.6}, {:.6}", lat, lon);
}
}
}
fn print_exif_json(path: &std::path::Path, exif: &rawlib::exif::ExifData) {
use std::fmt::Write;
let mut json = String::new();
write!(&mut json, " {{\"path\": {:?}", path.display()).unwrap();
if let Some(ref make) = exif.make {
write!(&mut json, ", \"make\": {:?}", make).unwrap();
}
if let Some(ref model) = exif.model {
write!(&mut json, ", \"model\": {:?}", model).unwrap();
}
if let Some(ref lens) = exif.lens_model {
write!(&mut json, ", \"lens_model\": {:?}", lens).unwrap();
}
if let Some(ref date) = exif.date_time_original {
write!(&mut json, ", \"date_time\": {:?}", date).unwrap();
}
if let Some(ref exp) = exif.exposure_time {
write!(&mut json, ", \"exposure_time\": {:?}", exp).unwrap();
}
if let Some(ref fnum) = exif.f_number {
write!(&mut json, ", \"f_number\": {:?}", fnum).unwrap();
}
if let Some(iso) = exif.iso {
write!(&mut json, ", \"iso\": {}", iso).unwrap();
}
if let Some(ref focal) = exif.focal_length {
write!(&mut json, ", \"focal_length\": {:?}", focal).unwrap();
}
if let (Some(w), Some(h)) = (exif.image_width, exif.image_height) {
write!(&mut json, ", \"width\": {}, \"height\": {}", w, h).unwrap();
}
if let Some((lat, lon)) = exif.gps_coordinates() {
write!(&mut json, ", \"gps_latitude\": {}, \"gps_longitude\": {}", lat, lon).unwrap();
}
json.push_str("}");
print!("{}", json);
}
fn collect_input_files(
inputs: &[String],
recursive: bool,
extensions: &[String],
) -> Vec<std::path::PathBuf> {
use walkdir::WalkDir;
let mut files = Vec::new();
let extensions_lower: Vec<String> = extensions
.iter()
.map(|e| e.to_lowercase())
.collect();
for input in inputs {
let path = std::path::Path::new(input);
if !path.exists() {
continue;
}
if path.is_file() {
if is_raw_file(path, &extensions_lower) {
files.push(path.to_path_buf());
}
} else if path.is_dir() {
let walker = if recursive {
WalkDir::new(path).follow_links(false)
} else {
WalkDir::new(path).max_depth(1).follow_links(false)
};
for entry in walker.into_iter().filter_map(|e| e.ok()) {
if entry.file_type().is_file() {
let file_path = entry.path();
if is_raw_file(file_path, &extensions_lower) {
files.push(file_path.to_path_buf());
}
}
}
}
}
files
}
fn is_raw_file(path: &std::path::Path, extensions: &[String]) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| extensions.contains(&ext.to_lowercase()))
.unwrap_or(false)
}
#[cfg(test)]
mod test{
use rawlib::extract_thumbnail;
use std::path::Path;
#[test]
pub fn img(){
let test_file = r"C:\Users\huang\图片\2025\2025-11-30\DSC_5432.NEF";
if !Path::new(test_file).exists() {
eprintln!("警告: 测试文件不存在: {}", test_file);
eprintln!("请将此测试文件路径替换为你本地实际存在的 RAW 文件路径");
return;
}
let thumb_bytes = extract_thumbnail(test_file)
.expect("Extract thumbnail failed.");
std::fs::write("./img.jpg", &thumb_bytes).unwrap();
println!("成功提取缩略图,大小: {} 字节", thumb_bytes.len());
}
}