1use std::ffi::CString;
2use std::os::raw::{c_char, c_int};
3use std::path::Path;
4use anyhow::{Context, Result};
5
6#[repr(C)]
8struct ProcessConfig {
9 downscale_factor: c_int,
10 jpeg_quality: c_int,
11 apply_debayer: c_int,
12 preview_mode: c_int,
13 auto_stretch: c_int,
14 manual_shadows: f32,
15 manual_highlights: f32,
16 manual_midtones: f32,
17}
18
19#[repr(C)]
20struct ProcessedImage {
21 data: *mut u8,
22 width: usize,
23 height: usize,
24 is_color: c_int,
25}
26
27extern "C" {
28 fn process_image_file(
29 path: *const c_char,
30 config: *const ProcessConfig,
31 out_image: *mut ProcessedImage,
32 ) -> c_int;
33
34 fn save_jpeg(
35 image: *const ProcessedImage,
36 output_path: *const c_char,
37 quality: c_int,
38 ) -> c_int;
39
40 fn free_processed_image(image: *mut ProcessedImage);
41
42 fn get_last_error() -> *const c_char;
43}
44
45pub struct FitsConverter {
46 downscale: usize,
47 quality: u8,
48 apply_debayer: bool,
49 preview_mode: bool,
50}
51
52impl FitsConverter {
53 pub fn new() -> Self {
54 FitsConverter {
55 downscale: 1,
56 quality: 95,
57 apply_debayer: true,
58 preview_mode: false,
59 }
60 }
61
62 pub fn with_downscale(mut self, factor: usize) -> Self {
63 self.downscale = factor;
64 self
65 }
66
67 pub fn with_quality(mut self, quality: u8) -> Self {
68 self.quality = quality.clamp(1, 100);
69 self
70 }
71
72 pub fn without_debayer(mut self) -> Self {
73 self.apply_debayer = false;
74 self
75 }
76
77 pub fn with_preview_mode(mut self) -> Self {
78 self.preview_mode = true;
79 self
80 }
81
82 pub fn convert<P: AsRef<Path>, Q: AsRef<Path>>(
83 &self,
84 input_path: P,
85 output_path: Q,
86 ) -> Result<()> {
87 let input_cstr = CString::new(input_path.as_ref().to_str().unwrap())
88 .context("Invalid input path")?;
89 let output_cstr = CString::new(output_path.as_ref().to_str().unwrap())
90 .context("Invalid output path")?;
91
92 let config = ProcessConfig {
93 downscale_factor: self.downscale as c_int,
94 jpeg_quality: self.quality as c_int,
95 apply_debayer: if self.apply_debayer { 1 } else { 0 },
96 preview_mode: if self.preview_mode { 1 } else { 0 },
97 auto_stretch: 1,
98 manual_shadows: 0.0,
99 manual_highlights: 1.0,
100 manual_midtones: 0.5,
101 };
102
103 let mut image = ProcessedImage {
104 data: std::ptr::null_mut(),
105 width: 0,
106 height: 0,
107 is_color: 0,
108 };
109
110 unsafe {
111 let result = process_image_file(
112 input_cstr.as_ptr(),
113 &config as *const ProcessConfig,
114 &mut image as *mut ProcessedImage,
115 );
116
117 if result != 0 {
118 let err_ptr = get_last_error();
119 let err_msg = if !err_ptr.is_null() {
120 std::ffi::CStr::from_ptr(err_ptr)
121 .to_string_lossy()
122 .into_owned()
123 } else {
124 "Unknown error".to_string()
125 };
126 anyhow::bail!("Image processing failed: {}", err_msg);
127 }
128
129 let result = save_jpeg(
130 &image as *const ProcessedImage,
131 output_cstr.as_ptr(),
132 self.quality as c_int,
133 );
134
135 free_processed_image(&mut image as *mut ProcessedImage);
136
137 if result != 0 {
138 anyhow::bail!("JPEG save failed");
139 }
140 }
141
142 Ok(())
143 }
144
145}
146
147impl Default for FitsConverter {
148 fn default() -> Self {
149 Self::new()
150 }
151}