use clap::Parser; use image::{DynamicImage, ImageBuffer, Rgb}; use libraw_sys::*; use std::ffi::CString; use std::os::raw::{c_int, c_size_t}; use std::path::{Path, PathBuf}; use std::ptr::null_mut; /// Resolve and sanitize a user-provided path. /// - Resolves to absolute path via canonicalize (or parent canonicalization for output) /// - Rejects paths containing `..` components after resolution /// - Returns human-readable error on failure fn sanitize_path( path_str: &str, must_exist: bool, ) -> Result> { let path = Path::new(path_str); // Block explicit traversal sequences in user input for component in path.components() { if component.as_os_str() == ".." { return Err(format!( "Path traversal ('..') is not allowed in: {}", path_str ) .into()); } } if must_exist { let resolved = path.canonicalize().map_err(|e| { format!("Failed to resolve input path '{}': {}", path_str, e) })?; Ok(resolved) } else { // For output paths that may not exist yet, resolve the parent directory if let Some(parent) = path.parent() { let resolved_parent = parent.canonicalize().map_err(|e| { format!( "Output directory '{}' does not exist or is not accessible: {}", parent.display(), e ) })?; let filename = path .file_name() .unwrap_or(path.as_os_str()) .to_string_lossy() .to_string(); Ok(resolved_parent.join(filename)) } else { let resolved = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); Ok(resolved) } } } /// Map libraw error codes to human-readable strings. fn libraw_error_string(code: c_int) -> &'static str { match code { 0 => "Success", -1 => "General failure (LIBRAW_ERROR_UNSUCCESSFUL)", -2 => "Unsupported file format", -3 => "File open failed (LIBRAW_ERROR_FILE_OPEN_FAIL)", -4 => "File is not recognized as RAW", -5 => "Out of memory", -6 => "File read failed", -7 => "Internal error", -8 => "Unsupported operation", _ => "Unknown error code", } } /// RAII guard for libraw memory cleanup. /// Automatically calls libraw_close() and libraw_free() on drop, /// eliminating the risk of leaked handles or double-free. struct LibRawGuard(*mut libraw_data_t); impl Drop for LibRawGuard { fn drop(&mut self) { if !self.0.is_null() { unsafe { libraw_close(self.0); libraw_free(self.0); } } } } impl LibRawGuard { fn ptr(&self) -> *mut libraw_data_t { self.0 } } /// Photo Editor - A Rust-based photo editor with RAW image processing capabilities using libraw #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { /// Input raw image file to process #[arg(short, long)] input: String, /// Output processed image file #[arg(short, long)] output: String, /// Exposure adjustment (-2.0 to 2.0) #[arg(long, default_value = "0.0")] exposure: f32, /// Contrast adjustment (-100 to 100) #[arg(long, default_value = "0")] contrast: i32, /// Highlights adjustment (-100 to 100) #[arg(long, default_value = "0")] highlights: i32, /// Shadows adjustment (-100 to 100) #[arg(long, default_value = "0")] shadows: i32, /// Whites adjustment (-100 to 100) #[arg(long, default_value = "0")] whites: i32, /// Blacks adjustment (-100 to 100) #[arg(long, default_value = "0")] blacks: i32, /// Clarity adjustment (-100 to 100) #[arg(long, default_value = "0")] clarity: i32, /// Vibrance adjustment (-100 to 100) #[arg(long, default_value = "0")] vibrance: i32, /// Saturation adjustment (-100 to 100) #[arg(long, default_value = "0")] saturation: i32, /// Temperature adjustment (-100 to 100) #[arg(long, default_value = "0")] temperature: i32, /// Tint adjustment (-100 to 100) #[arg(long, default_value = "0")] tint: i32, } /// Validate that all adjustment parameters are within their documented ranges. fn validate_params(args: &Args) -> Result<(), Box> { if args.exposure < -2.0 || args.exposure > 2.0 { return Err("--exposure must be between -2.0 and 2.0".into()); } let range = -100i32..=100i32; if !range.contains(&args.contrast) { return Err("--contrast must be between -100 and 100".into()); } if !range.contains(&args.highlights) { return Err("--highlights must be between -100 and 100".into()); } if !range.contains(&args.shadows) { return Err("--shadows must be between -100 and 100".into()); } if !range.contains(&args.whites) { return Err("--whites must be between -100 and 100".into()); } if !range.contains(&args.blacks) { return Err("--blacks must be between -100 and 100".into()); } if !range.contains(&args.clarity) { return Err("--clarity must be between -100 and 100".into()); } if !range.contains(&args.vibrance) { return Err("--vibrance must be between -100 and 100".into()); } if !range.contains(&args.saturation) { return Err("--saturation must be between -100 and 100".into()); } if !range.contains(&args.temperature) { return Err("--temperature must be between -100 and 100".into()); } if !range.contains(&args.tint) { return Err("--tint must be between -100 and 100".into()); } Ok(()) } fn main() -> Result<(), Box> { let args = Args::parse(); // Validate parameter ranges before processing validate_params(&args)?; println!("Photo Editor v0.1"); println!("=================="); println!("Input file: {}", args.input); println!("Output file: {}", args.output); println!("Exposure: {}", args.exposure); println!("Contrast: {}", args.contrast); println!("Highlights: {}", args.highlights); println!("Shadows: {}", args.shadows); println!("Whites: {}", args.whites); println!("Blacks: {}", args.blacks); println!("Clarity: {}", args.clarity); println!("Vibrance: {}", args.vibrance); println!("Saturation: {}", args.saturation); println!("Temperature: {}", args.temperature); println!("Tint: {}", args.tint); // Sanitize and resolve input path let input_path = sanitize_path(&args.input, true).map_err(|e| { eprintln!("Error: {}", e); e })?; // Validate RAW file format by extension let raw_exts = [ "arw", "cr2", "cr3", "crw", "dcr", "dng", "erf", "fff", "iiq", "k25", "kdc", "mdc", "mef", "mos", "mrw", "nef", "orf", "pef", "ptx", "pxn", "raf", "raw", "rdc", "sr2", "srf", "srw", "x3f", ]; if let Some(ext) = input_path.extension().and_then(|s| s.to_str()) { let ext_lower = ext.to_lowercase(); if !raw_exts.contains(&ext_lower.as_str()) { eprintln!( "Error: '{}' is not a supported RAW format (extension: .{})", input_path.display(), ext_lower ); eprintln!("Supported formats: {}", raw_exts.join(", ")); return Err("Unsupported file format".into()); } } else { eprintln!("Error: Input file has no extension: {}", input_path.display()); return Err("File has no extension".into()); } // Sanitize and resolve output path let output_path = sanitize_path(&args.output, false).map_err(|e| { eprintln!("Error: {}", e); e })?; println!("\n[INFO] Attempting RAW image processing with libraw integration..."); // Initialize libraw with RAII guard for automatic cleanup let lr_raw = unsafe { libraw_init(LIBRAW_OPTIONS_NONE) }; if lr_raw.is_null() { eprintln!("Error: Failed to initialize libraw"); return Err("Failed to initialize libraw".into()); } let lr_guard = LibRawGuard(lr_raw); let lr = lr_guard.ptr(); // Open the RAW file let input_cstr = CString::new(input_path.to_string_lossy().as_bytes()) .map_err(|_| "Input path contains null bytes, cannot process".into())?; let ret = unsafe { libraw_open_file(lr, input_cstr.as_ptr()) }; if ret != LIBRAW_SUCCESS { eprintln!("Error: Failed to open file: {}", libraw_error_string(ret)); return Err("Failed to open file".into()); } println!("[INFO] File opened successfully"); // Process the RAW data let ret = unsafe { libraw_unpack(lr) }; if ret != LIBRAW_SUCCESS { eprintln!("Error: Failed to unpack RAW data: {}", libraw_error_string(ret)); return Err("Failed to unpack RAW data".into()); } println!("[INFO] RAW data unpacked successfully"); // Adjust image parameters based on user input apply_image_parameters(lr, &args); println!("[INFO] Image parameters applied"); // Process the image let ret = unsafe { libraw_dcraw_process(lr) }; if ret != LIBRAW_SUCCESS { eprintln!("Error: Failed to process image: {}", libraw_error_string(ret)); return Err("Failed to process image".into()); } println!("[INFO] Image processed successfully"); // Determine output format from extension let ext = output_path .extension() .and_then(|s| s.to_str()) .unwrap_or("jpg") .to_lowercase(); let ret = if ext == "jpg" || ext == "jpeg" { // Use libraw_dcraw_make_mem_image + image crate for proper JPEG encoding let mut imgdata: *mut std::os::raw::c_uchar = null_mut(); let mut imgsize: c_size_t = 0; let ret = unsafe { libraw_dcraw_make_mem_image(lr, &mut imgdata, &mut imgsize) }; if ret != LIBRAW_SUCCESS { ret } else { let width = unsafe { (*(*lr).internal_data).imgdata.idata.width } as u32; let height = unsafe { (*(*lr).internal_data).imgdata.idata.height } as u32; let bytes = unsafe { std::slice::from_raw_parts(imgdata, imgsize as usize) }; let img = DynamicImage::ImageRgb8(ImageBuffer::, Vec>::from_raw( width, height, bytes.to_vec(), ).ok_or("Failed to create image from libraw data")?); unsafe { let freedata = *(*lr).internal_data.imgdata.freedata; let freesize = *(*lr).internal_data.imgdata.freesize; if !freedata.is_null() { libc::free(freedata as *mut std::os::raw::c_void); } } img.save_with_format(&output_path, image::ImageOutputFormat::Jpeg(90)) .map_err(|e| { eprintln!("Error: Failed to write JPEG: {}", e); "Failed to write JPEG".to_string() })?; LIBRAW_SUCCESS } } else { // For TIFF/PPM, use libraw_dcraw_ppm_tiff_writer let output_cstr = CString::new(output_path.to_string_lossy().as_bytes()) .map_err(|_| "Output path contains null bytes, cannot process".into())?; unsafe { libraw_dcraw_ppm_tiff_writer(lr, output_cstr.as_ptr()) } }; if ret != LIBRAW_SUCCESS { eprintln!("Error: Failed to write output file: {}", libraw_error_string(ret)); return Err("Failed to write output file".into()); } println!("[INFO] Processed image written to disk"); println!("\n[SUCCESS] Photo editing completed successfully with libraw integration!"); println!( "[NOTICE] The edited image has been saved as: {}", output_path.display() ); Ok(()) } fn apply_image_parameters(lr: *mut libraw_data_t, args: &Args) { if lr.is_null() { eprintln!("Warning: libraw pointer is null, skipping parameter application"); return; } unsafe { // Set various processing parameters based on user input // Apply exposure adjustment if args.exposure != 0.0 && !lr.is_null() { // Convert EV stops to brightness multiplier: EV 1.0 → 2x, EV -1.0 → 0.5x let multiplier = 2.0f32.powf(args.exposure); libraw_set_bright(lr, multiplier); } // Apply contrast (map to brightness multiplier for tonal range) if args.contrast != 0 && !lr.is_null() { let value = args.contrast.min(100).max(-100) as f32 / 100.0 * 100.0; libraw_set_bright(lr, value); } // Apply highlights adjustment if args.highlights != 0 && !lr.is_null() { let value = args.highlights.min(100).max(-100) as c_int; libraw_set_highlight(lr, value); } // Apply clarity (using noise reduction parameters) if args.clarity != 0 && !lr.is_null() { let value = args.clarity.min(100).max(-100) as c_int; libraw_set_fbdd_noiserd(lr, value); } // Apply shadows adjustment (map to brightness for dark areas) if args.shadows != 0 && !lr.is_null() { let value = args.shadows.min(100).max(-100) as f32 / 100.0 * 200.0; libraw_set_bright(lr, value); } // Apply whites adjustment (map to highlight recovery) if args.whites != 0 && !lr.is_null() { let value = args.whites.min(100).max(-100) as c_int; libraw_set_highlight(lr, value); } // Apply blacks adjustment (map to brightness offset for dark point) if args.blacks != 0 && !lr.is_null() { let value = args.blacks.min(100).max(-100) as f32 / 100.0 * -100.0; libraw_set_bright(lr, value); } // Apply vibrance adjustment (selective saturation, map to saturation multiplier) if args.vibrance != 0 && !lr.is_null() { let value = 1.0 + args.vibrance.min(100).max(-100) as f32 / 200.0; libraw_set_gamma(lr, 0, value); } // Apply temperature adjustment (white balance via user_mul) if args.temperature != 0 && !lr.is_null() { let temp_mul = [ 256i16 as c_int, 256i16 as c_int + args.temperature.min(100).max(-100) as c_int, 256i16 as c_int, 256i16 as c_int, ]; libraw_set_user_mul(lr, temp_mul.as_ptr()); libraw_set_use_auto_wb(lr, 0); } // Apply tint adjustment (green-magenta white balance axis) if args.tint != 0 && !lr.is_null() { let tint_mul = [ 256i16 as c_int + args.tint.min(100).max(-100) as c_int, 256i16 as c_int, 256i16 as c_int, 256i16 as c_int - args.tint.min(100).max(-100) as c_int, ]; libraw_set_user_mul(lr, tint_mul.as_ptr()); libraw_set_use_auto_wb(lr, 0); } // Apply saturation (use saturation multiplier, not gamma) if args.saturation != 0 && !lr.is_null() { let mul = (256i32 + args.saturation.min(100).max(-100) as i32 * 2) as c_int; let sat_mul = [mul, mul, mul, mul]; libraw_set_saturation_mul(lr, sat_mul.as_ptr()); } // Set output color space (sRGB in this case) if !lr.is_null() { libraw_set_output_color(lr, 1); // 1 = sRGB } // Set output bits per sample if !lr.is_null() { libraw_set_output_bps(lr, 8); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_args_parsing() { let args = Args::parse_from([ "photo_editor", "--input", "test.raw", "--output", "output.jpg", ]); assert_eq!(args.input, "test.raw"); assert_eq!(args.output, "output.jpg"); } #[test] fn test_args_with_positive_values() { let args = Args::parse_from([ "photo_editor", "--input", "input.raw", "--output", "output.jpg", "--exposure", "0.5", "--contrast", "20", "--highlights", "10", ]); assert_eq!(args.input, "input.raw"); assert_eq!(args.output, "output.jpg"); assert_eq!(args.exposure, 0.5); assert_eq!(args.contrast, 20); assert_eq!(args.highlights, 10); } }