Skip to main content

ad_plugins_rs/
file_jpeg.rs

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
14/// JPEG file writer using `jpeg-encoder` for encoding and `jpeg-decoder` for decoding.
15pub 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
127/// JPEG file processor wrapping FilePluginController<JpegWriter>.
128pub 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        // JPEG-specific: quality change
173        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        // Check JPEG SOI marker
216        assert_eq!(&data[0..2], &[0xFF, 0xD8]);
217        // Check JPEG EOI marker at end
218        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            // Use uniform value so JPEG compression is lossless at quality 100
290            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            // With uniform input at max quality, decoded values should be close
302            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}