Compare commits

..

25 Commits

Author SHA1 Message Date
jarianc
a1cc4d2e27 Merge pull request 'fix(#16): remove panic = abort to enable backtraces' (#29) from fix/issue-16 into master
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / docker-build (push) Has been cancelled
CI / security (push) Has been cancelled
CI / build-result (push) Has been cancelled
2026-07-05 09:52:46 -05:00
opencode
09e02f93c9 fix(#16): remove panic = abort to enable backtraces in release 2026-07-05 14:52:33 +00:00
jarianc
b34b6e4ffb Merge pull request 'fix(#15): convert EV stops to brightness multiplier (2^exposure)' (#28) from fix/issue-15 into master 2026-07-05 09:52:26 -05:00
opencode
70d925a46b fix(#15): convert EV stops to brightness multiplier for libraw_set_bright 2026-07-05 14:52:05 +00:00
jarianc
ec60c2b598 Merge pull request 'fix(#14): add LibRawGuard RAII Drop guard for memory cleanup' (#27) from fix/issue-14 into master 2026-07-05 09:51:53 -05:00
opencode
e30dd662b5 fix(#14): add LibRawGuard RAII Drop guard for automatic memory cleanup 2026-07-05 14:51:49 +00:00
jarianc
9c80ae7f80 Merge pull request 'fix(#13): add license field to Cargo.toml' (#26) from fix/issue-13 into master 2026-07-05 09:51:15 -05:00
opencode
75629fbbdc fix(#13): add MIT license field to Cargo.toml 2026-07-05 14:51:01 +00:00
jarianc
761f16bbea Merge pull request 'fix(#12): map libraw error codes to human-readable strings' (#25) from fix/issue-12 into master 2026-07-05 09:50:55 -05:00
opencode
35d5dc6b2d fix(#12): human-readable libraw error messages 2026-07-05 14:50:40 +00:00
jarianc
2d1a157f33 Merge pull request 'fix(#11): validate RAW file format extension before processing' (#24) from fix/issue-11 into master 2026-07-05 09:50:13 -05:00
opencode
6470ed153d fix(#11): validate RAW file format before processing 2026-07-05 14:50:03 +00:00
jarianc
c883e4d26a Merge pull request 'fix(#7): validate all CLI parameter ranges before processing' (#23) from fix/issue-7 into master 2026-07-05 09:49:48 -05:00
opencode
b3c938cde1 fix(#7): validate parameter ranges at startup 2026-07-05 14:49:43 +00:00
jarianc
b6a3bc3219 Merge pull request 'fix(#6): saturation uses libraw_set_saturation_mul instead of gamma' (#22) from fix/issue-6 into master 2026-07-05 09:49:00 -05:00
opencode
148e6cb0cb fix(#6): use libraw_set_saturation_mul instead of libraw_set_gamma for saturation 2026-07-05 14:48:56 +00:00
jarianc
2a5930ca1e Merge pull request 'fix(#5): contrast uses libraw_set_bright instead of libraw_set_highlight' (#21) from fix/issue-5 into master 2026-07-05 09:48:45 -05:00
opencode
929b5dcba5 fix(#5): contrast uses libraw_set_bright instead of libraw_set_highlight 2026-07-05 14:48:38 +00:00
jarianc
098e8163b9 Merge pull request 'fix(#4): apply all 6 previously ignored CLI parameters' (#20) from fix/issue-4 into master 2026-07-05 09:48:24 -05:00
opencode
6db7d6f56d fix(#4): apply shadows, whites, blacks, vibrance, temperature, tint params 2026-07-05 14:48:08 +00:00
jarianc
bab2f17533 Merge pull request 'fix(#3): add null pointer checks before all libraw FFI calls' (#19) from fix/issue-3 into master 2026-07-05 09:47:38 -05:00
opencode
d838594eb2 fix(#3): add null pointer checks before all libraw FFI calls 2026-07-05 14:47:34 +00:00
jarianc
c73466f5fa Merge pull request 'fix(#2): proper JPEG output via libraw_dcraw_make_mem_image + image crate' (#18) from fix/issue-2 into master 2026-07-05 09:46:43 -05:00
opencode
462541e096 fix(#2): proper JPEG output via libraw_dcraw_make_mem_image + image crate 2026-07-05 14:46:29 +00:00
jarianc
2481ed644b Merge pull request 'fix(#1): sanitize paths - block traversal, resolve to absolute' (#17) from fix/issue-1 into master 2026-07-05 09:45:38 -05:00
2 changed files with 226 additions and 35 deletions

View File

@ -2,11 +2,13 @@
name = "photo_editor"
version = "0.1.0"
edition = "2024"
license = "MIT"
[dependencies]
libraw-sys = "0.1.1"
image = "0.24"
clap = { version = "4.0", features = ["derive"] }
libc = "0.2"
[[bin]]
name = "photo_editor"
@ -16,4 +18,3 @@ path = "src/main.rs"
opt-level = "z" # Optimize for size
lto = true # Enable link-time optimization
codegen-units = 1
panic = "abort"

View File

@ -1,8 +1,10 @@
use clap::Parser;
use image::{DynamicImage, ImageBuffer, Rgb};
use libraw_sys::*;
use std::ffi::CString;
use std::os::raw::c_int;
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)
@ -53,6 +55,44 @@ fn sanitize_path(
}
}
/// 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)]
@ -110,9 +150,51 @@ struct Args {
tint: i32,
}
/// Validate that all adjustment parameters are within their documented ranges.
fn validate_params(args: &Args) -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
let args = Args::parse();
// Validate parameter ranges before processing
validate_params(&args)?;
println!("Photo Editor v0.1");
println!("==================");
println!("Input file: {}", args.input);
@ -135,6 +217,29 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
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);
@ -143,12 +248,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("\n[INFO] Attempting RAW image processing with libraw integration...");
// Initialize libraw
let lr = unsafe { libraw_init(LIBRAW_OPTIONS_NONE) };
if lr.is_null() {
// 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())
@ -156,8 +263,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
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) };
eprintln!("Error: Failed to open file: {}", libraw_error_string(ret));
return Err("Failed to open file".into());
}
@ -166,8 +272,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// 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) };
eprintln!("Error: Failed to unpack RAW data: {}", libraw_error_string(ret));
return Err("Failed to unpack RAW data".into());
}
@ -181,8 +286,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// 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) };
eprintln!("Error: Failed to process image: {}", libraw_error_string(ret));
return Err("Failed to process image".into());
}
@ -195,23 +299,50 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.unwrap_or("jpg")
.to_lowercase();
let output_cstr = CString::new(output_path.to_string_lossy().as_bytes())
.map_err(|_| "Output path contains null bytes, cannot process".into())?;
let ret = unsafe { libraw_dcraw_ppm_tiff_writer(lr, output_cstr.as_ptr()) };
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) };
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");
// 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: {}",
@ -222,43 +353,102 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
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 {
libraw_set_bright(lr, args.exposure);
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 (using highlight parameters for demonstration)
if args.contrast != 0 {
let value = args.contrast.min(100).max(-100) as c_int;
libraw_set_highlight(lr, value);
// 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 {
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 {
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 {
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
// 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)
libraw_set_output_color(lr, 1); // 1 = sRGB
if !lr.is_null() {
libraw_set_output_color(lr, 1); // 1 = sRGB
}
// Set output bits per sample
libraw_set_output_bps(lr, 8);
if !lr.is_null() {
libraw_set_output_bps(lr, 8);
}
}
}