1use std::path::{Path, PathBuf};
2
3use ad_core_rs::color::{NDColorMode, convert_rgb_layout};
4use ad_core_rs::error::{ADError, ADResult};
5use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
6use ad_core_rs::ndarray_pool::NDArrayPool;
7use ad_core_rs::plugin::file_base::{NDFileMode, NDFileWriter};
8use ad_core_rs::plugin::file_controller::FilePluginController;
9use ad_core_rs::plugin::runtime::{
10 NDPluginProcess, ParamChangeResult, PluginParamSnapshot, ProcessResult,
11};
12
13use jpeg_encoder::{ColorType as JpegColorType, Encoder as JpegEncoder};
14
15pub struct JpegWriter {
17 current_path: Option<PathBuf>,
18 pub(crate) quality: u8,
19}
20
21impl JpegWriter {
22 pub fn new(quality: u8) -> Self {
23 Self {
24 current_path: None,
25 quality,
26 }
27 }
28
29 pub fn set_quality(&mut self, quality: u8) {
30 self.quality = quality;
31 }
32}
33
34impl NDFileWriter for JpegWriter {
35 fn open_file(&mut self, path: &Path, _mode: NDFileMode, array: &NDArray) -> ADResult<()> {
36 let dt = array.data.data_type();
37 if dt != NDDataType::UInt8 && dt != NDDataType::Int8 {
38 return Err(ADError::UnsupportedConversion(
39 "JPEG only supports UInt8/Int8 data".into(),
40 ));
41 }
42 self.current_path = Some(path.to_path_buf());
43 Ok(())
44 }
45
46 fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
47 let path = self
48 .current_path
49 .as_ref()
50 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
51
52 let color_mode = array
54 .attributes
55 .get("ColorMode")
56 .and_then(|attr| attr.value.as_i64())
57 .map(|v| NDColorMode::from_i32(v as i32))
58 .unwrap_or_else(|| {
59 if array.dims.len() == 3 {
60 let d0 = array.dims[0].size;
61 let d1 = array.dims[1].size;
62 let d2 = array.dims[2].size;
63 if d0 == 3 {
64 NDColorMode::RGB1
65 } else if d1 == 3 {
66 NDColorMode::RGB2
67 } else if d2 == 3 {
68 NDColorMode::RGB3
69 } else {
70 NDColorMode::Mono
71 }
72 } else {
73 NDColorMode::Mono
74 }
75 });
76
77 let is_rgb = matches!(
78 color_mode,
79 NDColorMode::RGB1 | NDColorMode::RGB2 | NDColorMode::RGB3
80 );
81 let src = if is_rgb && color_mode != NDColorMode::RGB1 {
82 &convert_rgb_layout(array, color_mode, NDColorMode::RGB1)?
83 } else {
84 array
85 };
86
87 let info = src.info();
88 let width = info.x_size;
89 let height = info.y_size;
90
91 let data: Vec<u8> = match &src.data {
92 NDDataBuffer::U8(v) => v.clone(),
93 NDDataBuffer::I8(v) => v.iter().map(|&b| b as u8).collect(),
94 _ => {
95 return Err(ADError::UnsupportedConversion(
96 "JPEG only supports UInt8/Int8".into(),
97 ));
98 }
99 };
100
101 let color_type = if info.color_size == 3 {
102 JpegColorType::Rgb
103 } else {
104 JpegColorType::Luma
105 };
106
107 let mut buf = Vec::new();
108 let encoder = JpegEncoder::new(&mut buf, self.quality);
109 encoder
110 .encode(&data, width as u16, height as u16, color_type)
111 .map_err(|e| ADError::UnsupportedConversion(format!("JPEG encode error: {}", e)))?;
112
113 std::fs::write(path, &buf)?;
114 Ok(())
115 }
116
117 fn read_file(&mut self) -> ADResult<NDArray> {
118 let path = self
119 .current_path
120 .as_ref()
121 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
122
123 let file_data = std::fs::read(path)?;
124 let mut decoder = jpeg_decoder::Decoder::new(&file_data[..]);
125 let pixels = decoder
126 .decode()
127 .map_err(|e| ADError::UnsupportedConversion(format!("JPEG decode error: {}", e)))?;
128 let info = decoder.info().unwrap();
129
130 let (width, height) = (info.width as usize, info.height as usize);
131
132 let dims = match info.pixel_format {
133 jpeg_decoder::PixelFormat::L8 => {
134 vec![NDDimension::new(width), NDDimension::new(height)]
135 }
136 jpeg_decoder::PixelFormat::RGB24 => {
137 vec![
138 NDDimension::new(3),
139 NDDimension::new(width),
140 NDDimension::new(height),
141 ]
142 }
143 _ => {
144 return Err(ADError::UnsupportedConversion(
145 "unsupported JPEG pixel format".into(),
146 ));
147 }
148 };
149
150 let mut arr = NDArray::new(dims, NDDataType::UInt8);
151 arr.data = NDDataBuffer::U8(pixels);
152 Ok(arr)
153 }
154
155 fn close_file(&mut self) -> ADResult<()> {
156 self.current_path = None;
157 Ok(())
158 }
159
160 fn supports_multiple_arrays(&self) -> bool {
161 false
162 }
163}
164
165pub struct JpegFileProcessor {
167 ctrl: FilePluginController<JpegWriter>,
168 jpeg_quality_idx: Option<usize>,
169}
170
171impl JpegFileProcessor {
172 pub fn new(quality: u8) -> Self {
173 Self {
174 ctrl: FilePluginController::new(JpegWriter::new(quality)),
175 jpeg_quality_idx: None,
176 }
177 }
178}
179
180impl Default for JpegFileProcessor {
181 fn default() -> Self {
182 Self::new(50)
183 }
184}
185
186impl NDPluginProcess for JpegFileProcessor {
187 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
188 self.ctrl.process_array(array)
189 }
190
191 fn plugin_type(&self) -> &str {
192 "NDFileJPEG"
193 }
194
195 fn does_array_callbacks(&self) -> bool {
198 false
199 }
200
201 fn register_params(
202 &mut self,
203 base: &mut asyn_rs::port::PortDriverBase,
204 ) -> asyn_rs::error::AsynResult<()> {
205 self.ctrl.register_params(base)?;
206 use asyn_rs::param::ParamType;
207 let idx = base.create_param("JPEG_QUALITY", ParamType::Int32)?;
208 base.set_int32_param(idx, 0, i32::from(self.ctrl.writer.quality))?;
212 self.jpeg_quality_idx = Some(idx);
213 Ok(())
214 }
215
216 fn on_param_change(
217 &mut self,
218 reason: usize,
219 params: &PluginParamSnapshot,
220 ) -> ParamChangeResult {
221 if Some(reason) == self.jpeg_quality_idx {
223 let q = params.value.as_i32().clamp(1, 100) as u8;
224 self.ctrl.writer.set_quality(q);
225 return ParamChangeResult::empty();
226 }
227 self.ctrl.on_param_change(reason, params)
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use ad_core_rs::ndarray::{NDDataBuffer, NDDimension};
235 use std::sync::atomic::{AtomicU32, Ordering};
236
237 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
238
239 fn temp_path(prefix: &str) -> PathBuf {
240 let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
241 std::env::temp_dir().join(format!("adcore_test_{}_{}.jpg", prefix, n))
242 }
243
244 #[test]
245 fn test_write_u8() {
246 let path = temp_path("jpeg");
247 let mut writer = JpegWriter::new(90);
248
249 let mut arr = NDArray::new(
250 vec![NDDimension::new(8), NDDimension::new(8)],
251 NDDataType::UInt8,
252 );
253 if let NDDataBuffer::U8(ref mut v) = arr.data {
254 for i in 0..64 {
255 v[i] = (i * 4) as u8;
256 }
257 }
258
259 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
260 writer.write_file(&arr).unwrap();
261 writer.close_file().unwrap();
262
263 let data = std::fs::read(&path).unwrap();
264 assert_eq!(&data[0..2], &[0xFF, 0xD8]);
266 assert_eq!(&data[data.len() - 2..], &[0xFF, 0xD9]);
268
269 std::fs::remove_file(&path).ok();
270 }
271
272 #[test]
273 fn test_rejects_non_u8() {
274 let path = temp_path("jpeg_u16");
275 let mut writer = JpegWriter::new(90);
276
277 let arr = NDArray::new(
278 vec![NDDimension::new(4), NDDimension::new(4)],
279 NDDataType::UInt16,
280 );
281
282 let result = writer.open_file(&path, NDFileMode::Single, &arr);
283 assert!(result.is_err());
284 }
285
286 #[test]
287 fn test_quality_affects_size() {
288 let path_high = temp_path("jpeg_hi");
289 let path_low = temp_path("jpeg_lo");
290
291 let mut arr = NDArray::new(
292 vec![NDDimension::new(32), NDDimension::new(32)],
293 NDDataType::UInt8,
294 );
295 if let NDDataBuffer::U8(ref mut v) = arr.data {
296 for i in 0..v.len() {
297 v[i] = (i % 256) as u8;
298 }
299 }
300
301 let mut writer_high = JpegWriter::new(95);
302 writer_high
303 .open_file(&path_high, NDFileMode::Single, &arr)
304 .unwrap();
305 writer_high.write_file(&arr).unwrap();
306 writer_high.close_file().unwrap();
307
308 let mut writer_low = JpegWriter::new(10);
309 writer_low
310 .open_file(&path_low, NDFileMode::Single, &arr)
311 .unwrap();
312 writer_low.write_file(&arr).unwrap();
313 writer_low.close_file().unwrap();
314
315 let size_high = std::fs::metadata(&path_high).unwrap().len();
316 let size_low = std::fs::metadata(&path_low).unwrap().len();
317 assert!(
318 size_high > size_low,
319 "high quality ({}) should be larger than low quality ({})",
320 size_high,
321 size_low
322 );
323
324 std::fs::remove_file(&path_high).ok();
325 std::fs::remove_file(&path_low).ok();
326 }
327
328 #[test]
329 fn test_default_quality_is_50() {
330 assert_eq!(JpegFileProcessor::default().ctrl.writer.quality, 50);
332 assert_eq!(JpegWriter::new(50).quality, 50);
333 }
334
335 #[test]
336 fn test_register_params_seeds_quality_pv() {
337 use asyn_rs::port::{PortDriverBase, PortFlags};
338 let mut base = PortDriverBase::new("jpeg_param_test", 1, PortFlags::default());
339 let mut proc = JpegFileProcessor::new(50);
340 proc.register_params(&mut base).unwrap();
341 let idx = proc.jpeg_quality_idx.unwrap();
342 assert_eq!(base.get_int32_param(idx, 0).unwrap(), 50);
344 }
345
346 #[test]
347 fn test_roundtrip_luma() {
348 let path = temp_path("jpeg_rt");
349 let mut writer = JpegWriter::new(100);
350
351 let mut arr = NDArray::new(
352 vec![NDDimension::new(8), NDDimension::new(8)],
353 NDDataType::UInt8,
354 );
355 if let NDDataBuffer::U8(ref mut v) = arr.data {
356 for i in 0..64 {
358 v[i] = 128;
359 }
360 }
361
362 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
363 writer.write_file(&arr).unwrap();
364
365 let read_back = writer.read_file().unwrap();
366 assert_eq!(read_back.data.data_type(), NDDataType::UInt8);
367 if let NDDataBuffer::U8(ref v) = read_back.data {
368 for &px in v.iter() {
370 assert!(
371 (px as i16 - 128).unsigned_abs() < 5,
372 "pixel {} too far from 128",
373 px
374 );
375 }
376 }
377
378 writer.close_file().unwrap();
379 std::fs::remove_file(&path).ok();
380 }
381
382 fn jpeg_roundtrip_dims(prefix: &str, mode: NDColorMode, dims: Vec<usize>) -> Vec<usize> {
384 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
385 let path = temp_path(prefix);
386 let mut writer = JpegWriter::new(95);
387
388 let mut arr = NDArray::new(
389 dims.iter().map(|&d| NDDimension::new(d)).collect(),
390 NDDataType::UInt8,
391 );
392 arr.attributes.add(NDAttribute::new_static(
393 "ColorMode",
394 "Color mode",
395 NDAttrSource::Driver,
396 NDAttrValue::Int32(mode as i32),
397 ));
398 if let NDDataBuffer::U8(ref mut v) = arr.data {
399 for (i, x) in v.iter_mut().enumerate() {
400 *x = (i % 256) as u8;
401 }
402 }
403
404 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
405 writer.write_file(&arr).unwrap();
406 let read_back = writer.read_file().unwrap();
407 writer.close_file().unwrap();
408 std::fs::remove_file(&path).ok();
409 read_back.dims.iter().map(|d| d.size).collect()
410 }
411
412 #[test]
413 fn test_adp12_rgb2_jpeg_written_as_rgb_not_grayscale() {
414 let dims = jpeg_roundtrip_dims("jpeg_rgb2", NDColorMode::RGB2, vec![5, 3, 4]);
420 assert_eq!(dims, vec![3, 5, 4]);
421 }
422
423 #[test]
424 fn test_adp12_rgb3_jpeg_written_as_rgb_not_grayscale() {
425 let dims = jpeg_roundtrip_dims("jpeg_rgb3", NDColorMode::RGB3, vec![5, 4, 3]);
428 assert_eq!(dims, vec![3, 5, 4]);
429 }
430}