PhotoEditor/src/main.rs

347 lines
11 KiB
Rust

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<PathBuf, Box<dyn std::error::Error>> {
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)
}
}
}
/// 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,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
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
})?;
// 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
let lr = unsafe { libraw_init(LIBRAW_OPTIONS_NONE) };
if lr.is_null() {
eprintln!("Error: Failed to initialize libraw");
return Err("Failed to initialize libraw".into());
}
// 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. Error code: {}", ret);
unsafe { libraw_close(lr) };
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. Error code: {}", ret);
unsafe { libraw_close(lr) };
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. Error code: {}", ret);
unsafe { libraw_close(lr) };
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::<Rgb<u8>, Vec<u8>>::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. Error code: {}", ret);
unsafe { libraw_close(lr) };
return Err("Failed to write output file".into());
}
println!("[INFO] Processed image written to disk");
// Clean up
unsafe {
libraw_close(lr);
}
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() {
libraw_set_bright(lr, args.exposure);
}
// Apply contrast (using highlight parameters for demonstration)
if args.contrast != 0 && !lr.is_null() {
let value = args.contrast.min(100).max(-100) as c_int;
libraw_set_highlight(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 saturation
if args.saturation != 0 && !lr.is_null() {
let value = 1.0 + args.saturation.min(100).max(-100) as f32 / 100.0;
libraw_set_gamma(lr, 0, value); // Apply gamma adjustment to simulate saturation
}
// 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);
}
}