1use std::path::{Path, PathBuf};
2
3use ad_core_rs::error::{ADError, ADResult};
4use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
5use ad_core_rs::ndarray_pool::NDArrayPool;
6use ad_core_rs::plugin::file_base::{NDFileMode, NDFileWriter};
7use ad_core_rs::plugin::file_controller::FilePluginController;
8use ad_core_rs::plugin::runtime::{
9 NDPluginProcess, ParamChangeResult, PluginParamSnapshot, ProcessResult,
10};
11
12use jpeg_encoder::{ColorType as JpegColorType, Encoder as JpegEncoder};
13
14pub struct JpegWriter {
16 current_path: Option<PathBuf>,
17 quality: u8,
18}
19
20impl JpegWriter {
21 pub fn new(quality: u8) -> Self {
22 Self {
23 current_path: None,
24 quality,
25 }
26 }
27
28 pub fn set_quality(&mut self, quality: u8) {
29 self.quality = quality;
30 }
31}
32
33impl NDFileWriter for JpegWriter {
34 fn open_file(&mut self, path: &Path, _mode: NDFileMode, array: &NDArray) -> ADResult<()> {
35 if array.data.data_type() != NDDataType::UInt8 {
36 return Err(ADError::UnsupportedConversion(
37 "JPEG only supports UInt8 data".into(),
38 ));
39 }
40 self.current_path = Some(path.to_path_buf());
41 Ok(())
42 }
43
44 fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
45 let path = self
46 .current_path
47 .as_ref()
48 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
49
50 let info = array.info();
51 let width = info.x_size;
52 let height = info.y_size;
53
54 let data = match &array.data {
55 NDDataBuffer::U8(v) => v.as_slice(),
56 _ => {
57 return Err(ADError::UnsupportedConversion(
58 "JPEG only supports UInt8".into(),
59 ));
60 }
61 };
62
63 let color_type = if info.color_size == 3 {
64 JpegColorType::Rgb
65 } else {
66 JpegColorType::Luma
67 };
68
69 let mut buf = Vec::new();
70 let encoder = JpegEncoder::new(&mut buf, self.quality);
71 encoder
72 .encode(data, width as u16, height as u16, color_type)
73 .map_err(|e| ADError::UnsupportedConversion(format!("JPEG encode error: {}", e)))?;
74
75 std::fs::write(path, &buf)?;
76 Ok(())
77 }
78
79 fn read_file(&mut self) -> ADResult<NDArray> {
80 let path = self
81 .current_path
82 .as_ref()
83 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
84
85 let file_data = std::fs::read(path)?;
86 let mut decoder = jpeg_decoder::Decoder::new(&file_data[..]);
87 let pixels = decoder
88 .decode()
89 .map_err(|e| ADError::UnsupportedConversion(format!("JPEG decode error: {}", e)))?;
90 let info = decoder.info().unwrap();
91
92 let (width, height) = (info.width as usize, info.height as usize);
93
94 let dims = match info.pixel_format {
95 jpeg_decoder::PixelFormat::L8 => {
96 vec![NDDimension::new(width), NDDimension::new(height)]
97 }
98 jpeg_decoder::PixelFormat::RGB24 => {
99 vec![
100 NDDimension::new(3),
101 NDDimension::new(width),
102 NDDimension::new(height),
103 ]
104 }
105 _ => {
106 return Err(ADError::UnsupportedConversion(
107 "unsupported JPEG pixel format".into(),
108 ));
109 }
110 };
111
112 let mut arr = NDArray::new(dims, NDDataType::UInt8);
113 arr.data = NDDataBuffer::U8(pixels);
114 Ok(arr)
115 }
116
117 fn close_file(&mut self) -> ADResult<()> {
118 self.current_path = None;
119 Ok(())
120 }
121
122 fn supports_multiple_arrays(&self) -> bool {
123 false
124 }
125}
126
127pub struct JpegFileProcessor {
129 ctrl: FilePluginController<JpegWriter>,
130 jpeg_quality_idx: Option<usize>,
131}
132
133impl JpegFileProcessor {
134 pub fn new(quality: u8) -> Self {
135 Self {
136 ctrl: FilePluginController::new(JpegWriter::new(quality)),
137 jpeg_quality_idx: None,
138 }
139 }
140}
141
142impl Default for JpegFileProcessor {
143 fn default() -> Self {
144 Self::new(90)
145 }
146}
147
148impl NDPluginProcess for JpegFileProcessor {
149 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
150 self.ctrl.process_array(array)
151 }
152
153 fn plugin_type(&self) -> &str {
154 "NDFileJPEG"
155 }
156
157 fn register_params(
158 &mut self,
159 base: &mut asyn_rs::port::PortDriverBase,
160 ) -> asyn_rs::error::AsynResult<()> {
161 self.ctrl.register_params(base)?;
162 use asyn_rs::param::ParamType;
163 self.jpeg_quality_idx = Some(base.create_param("JPEG_QUALITY", ParamType::Int32)?);
164 Ok(())
165 }
166
167 fn on_param_change(
168 &mut self,
169 reason: usize,
170 params: &PluginParamSnapshot,
171 ) -> ParamChangeResult {
172 if Some(reason) == self.jpeg_quality_idx {
174 let q = params.value.as_i32().clamp(1, 100) as u8;
175 self.ctrl.writer.set_quality(q);
176 return ParamChangeResult::empty();
177 }
178 self.ctrl.on_param_change(reason, params)
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use ad_core_rs::ndarray::{NDDataBuffer, NDDimension};
186 use std::sync::atomic::{AtomicU32, Ordering};
187
188 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
189
190 fn temp_path(prefix: &str) -> PathBuf {
191 let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
192 std::env::temp_dir().join(format!("adcore_test_{}_{}.jpg", prefix, n))
193 }
194
195 #[test]
196 fn test_write_u8() {
197 let path = temp_path("jpeg");
198 let mut writer = JpegWriter::new(90);
199
200 let mut arr = NDArray::new(
201 vec![NDDimension::new(8), NDDimension::new(8)],
202 NDDataType::UInt8,
203 );
204 if let NDDataBuffer::U8(ref mut v) = arr.data {
205 for i in 0..64 {
206 v[i] = (i * 4) as u8;
207 }
208 }
209
210 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
211 writer.write_file(&arr).unwrap();
212 writer.close_file().unwrap();
213
214 let data = std::fs::read(&path).unwrap();
215 assert_eq!(&data[0..2], &[0xFF, 0xD8]);
217 assert_eq!(&data[data.len() - 2..], &[0xFF, 0xD9]);
219
220 std::fs::remove_file(&path).ok();
221 }
222
223 #[test]
224 fn test_rejects_non_u8() {
225 let path = temp_path("jpeg_u16");
226 let mut writer = JpegWriter::new(90);
227
228 let arr = NDArray::new(
229 vec![NDDimension::new(4), NDDimension::new(4)],
230 NDDataType::UInt16,
231 );
232
233 let result = writer.open_file(&path, NDFileMode::Single, &arr);
234 assert!(result.is_err());
235 }
236
237 #[test]
238 fn test_quality_affects_size() {
239 let path_high = temp_path("jpeg_hi");
240 let path_low = temp_path("jpeg_lo");
241
242 let mut arr = NDArray::new(
243 vec![NDDimension::new(32), NDDimension::new(32)],
244 NDDataType::UInt8,
245 );
246 if let NDDataBuffer::U8(ref mut v) = arr.data {
247 for i in 0..v.len() {
248 v[i] = (i % 256) as u8;
249 }
250 }
251
252 let mut writer_high = JpegWriter::new(95);
253 writer_high
254 .open_file(&path_high, NDFileMode::Single, &arr)
255 .unwrap();
256 writer_high.write_file(&arr).unwrap();
257 writer_high.close_file().unwrap();
258
259 let mut writer_low = JpegWriter::new(10);
260 writer_low
261 .open_file(&path_low, NDFileMode::Single, &arr)
262 .unwrap();
263 writer_low.write_file(&arr).unwrap();
264 writer_low.close_file().unwrap();
265
266 let size_high = std::fs::metadata(&path_high).unwrap().len();
267 let size_low = std::fs::metadata(&path_low).unwrap().len();
268 assert!(
269 size_high > size_low,
270 "high quality ({}) should be larger than low quality ({})",
271 size_high,
272 size_low
273 );
274
275 std::fs::remove_file(&path_high).ok();
276 std::fs::remove_file(&path_low).ok();
277 }
278
279 #[test]
280 fn test_roundtrip_luma() {
281 let path = temp_path("jpeg_rt");
282 let mut writer = JpegWriter::new(100);
283
284 let mut arr = NDArray::new(
285 vec![NDDimension::new(8), NDDimension::new(8)],
286 NDDataType::UInt8,
287 );
288 if let NDDataBuffer::U8(ref mut v) = arr.data {
289 for i in 0..64 {
291 v[i] = 128;
292 }
293 }
294
295 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
296 writer.write_file(&arr).unwrap();
297
298 let read_back = writer.read_file().unwrap();
299 assert_eq!(read_back.data.data_type(), NDDataType::UInt8);
300 if let NDDataBuffer::U8(ref v) = read_back.data {
301 for &px in v.iter() {
303 assert!(
304 (px as i16 - 128).unsigned_abs() < 5,
305 "pixel {} too far from 128",
306 px
307 );
308 }
309 }
310
311 writer.close_file().unwrap();
312 std::fs::remove_file(&path).ok();
313 }
314}