1use std::path::{Path, PathBuf};
2
3use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
4use ad_core_rs::color::{NDColorMode, convert_rgb_layout};
5use ad_core_rs::error::{ADError, ADResult};
6use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
7use ad_core_rs::ndarray_pool::NDArrayPool;
8use ad_core_rs::plugin::file_base::{NDFileMode, NDFileWriter};
9use ad_core_rs::plugin::file_controller::FilePluginController;
10use ad_core_rs::plugin::runtime::{
11 NDPluginProcess, ParamChangeResult, PluginParamSnapshot, ProcessResult,
12};
13
14use tiff::ColorType;
15use tiff::decoder::Decoder;
16use tiff::encoder::TiffEncoder;
17use tiff::encoder::colortype;
18use tiff::tags::Tag;
19
20const TIFFTAG_NDTIMESTAMP: u16 = 65000;
22const TIFFTAG_UNIQUEID: u16 = 65001;
23const TIFFTAG_EPICSTSSEC: u16 = 65002;
24const TIFFTAG_EPICSTSNSEC: u16 = 65003;
25const TIFFTAG_FIRST_ATTRIBUTE: u16 = 65010;
26
27mod signed_rgb {
32 use tiff::encoder::colortype::ColorType;
33 use tiff::tags::{PhotometricInterpretation, SampleFormat};
34
35 macro_rules! signed_rgb {
36 ($name:ident, $inner:ty, $bits:expr) => {
37 pub struct $name;
38 impl ColorType for $name {
39 type Inner = $inner;
40 const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB;
41 const BITS_PER_SAMPLE: &'static [u16] = &[$bits, $bits, $bits];
42 const SAMPLE_FORMAT: &'static [SampleFormat] =
43 &[SampleFormat::Int, SampleFormat::Int, SampleFormat::Int];
44 }
45 };
46 }
47
48 signed_rgb!(RGBI8, i8, 8);
49 signed_rgb!(RGBI16, i16, 16);
50 signed_rgb!(RGBI32, i32, 32);
51 signed_rgb!(RGBI64, i64, 64);
52}
53
54fn attribute_tag_string(attr: &NDAttribute) -> String {
58 let value = match &attr.value {
59 NDAttrValue::Int8(v) => format!("{}", v),
60 NDAttrValue::Int16(v) => format!("{}", v),
61 NDAttrValue::Int32(v) => format!("{}", v),
62 NDAttrValue::Int64(v) => format!("{}", v),
63 NDAttrValue::UInt8(v) => format!("{}", v),
64 NDAttrValue::UInt16(v) => format!("{}", v),
65 NDAttrValue::UInt32(v) => format!("{}", v),
66 NDAttrValue::UInt64(v) => format!("{}", v),
67 NDAttrValue::Float32(v) => format!("{:.6}", v),
69 NDAttrValue::Float64(v) => format!("{:.6}", v),
70 NDAttrValue::String(s) => s.clone(),
71 NDAttrValue::Undefined => String::new(),
72 };
73 format!("{}:{}", attr.name, value)
74}
75
76pub struct TiffWriter {
78 current_path: Option<PathBuf>,
79}
80
81impl TiffWriter {
82 pub fn new() -> Self {
83 Self { current_path: None }
84 }
85
86 fn array_color_mode(array: &NDArray) -> NDColorMode {
87 array
88 .attributes
89 .get("ColorMode")
90 .and_then(|attr| attr.value.as_i64())
91 .map(|v| NDColorMode::from_i32(v as i32))
92 .unwrap_or_else(|| match array.dims.as_slice() {
93 [a, _, _] if a.size == 3 => NDColorMode::RGB1,
94 [_, b, _] if b.size == 3 => NDColorMode::RGB2,
95 [_, _, c] if c.size == 3 => NDColorMode::RGB3,
96 _ => NDColorMode::Mono,
97 })
98 }
99
100 fn normalize_for_write(array: &NDArray) -> ADResult<(NDArray, u32, u32, bool)> {
101 match array.dims.as_slice() {
102 [x] => {
103 let mut normalized = NDArray::new(
104 vec![NDDimension::new(x.size), NDDimension::new(1)],
105 array.data.data_type(),
106 );
107 normalized.data = array.data.clone();
108 normalized.unique_id = array.unique_id;
109 normalized.timestamp = array.timestamp;
110 normalized.attributes = array.attributes.clone();
111 normalized.codec = array.codec.clone();
112 Ok((normalized, x.size as u32, 1, false))
113 }
114 [x, y] => Ok((array.clone(), x.size as u32, y.size as u32, false)),
115 [_, _, _] => {
116 let color_mode = Self::array_color_mode(array);
117 let rgb1 = match color_mode {
118 NDColorMode::RGB1 => array.clone(),
119 NDColorMode::RGB2 | NDColorMode::RGB3 => {
120 convert_rgb_layout(array, color_mode, NDColorMode::RGB1)?
121 }
122 other => {
123 return Err(ADError::UnsupportedConversion(format!(
124 "unsupported TIFF color mode: {:?}",
125 other
126 )));
127 }
128 };
129 Ok((
130 rgb1.clone(),
131 rgb1.dims[1].size as u32,
132 rgb1.dims[2].size as u32,
133 true,
134 ))
135 }
136 _ => Err(ADError::InvalidDimensions(
137 "unsupported TIFF array dimensions".into(),
138 )),
139 }
140 }
141
142 fn attach_color_mode(array: &mut NDArray, color_mode: NDColorMode) {
143 array.attributes.add(NDAttribute::new_static(
144 "ColorMode",
145 "Color mode",
146 NDAttrSource::Driver,
147 NDAttrValue::Int32(color_mode as i32),
148 ));
149 }
150}
151
152impl NDFileWriter for TiffWriter {
153 fn open_file(&mut self, path: &Path, _mode: NDFileMode, _array: &NDArray) -> ADResult<()> {
154 self.current_path = Some(path.to_path_buf());
155 Ok(())
156 }
157
158 fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
159 let path = self
160 .current_path
161 .as_ref()
162 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
163 let (array, width, height, is_rgb) = Self::normalize_for_write(array)?;
164
165 let file = std::fs::File::create(path)?;
166 let mut encoder = TiffEncoder::new(file)
167 .map_err(|e| ADError::UnsupportedConversion(format!("TIFF encoder error: {}", e)))?;
168
169 let attr_tags: Vec<(u16, String)> = array
173 .attributes
174 .iter()
175 .enumerate()
176 .map(|(i, attr)| {
177 let tag_num = TIFFTAG_FIRST_ATTRIBUTE.saturating_add(i as u16);
178 (tag_num, attribute_tag_string(attr))
179 })
180 .collect();
181
182 let model = array
184 .attributes
185 .get("Model")
186 .map(|a| a.value.as_string())
187 .unwrap_or_else(|| "Unknown".to_string());
188 let make = array
189 .attributes
190 .get("Manufacturer")
191 .map(|a| a.value.as_string())
192 .unwrap_or_else(|| "Unknown".to_string());
193 let image_description = array
194 .attributes
195 .get("TIFFImageDescription")
196 .map(|a| a.value.as_string());
197
198 let unique_id = array.unique_id;
199 let time_stamp = array.time_stamp;
200 let ts_sec = array.timestamp.sec;
201 let ts_nsec = array.timestamp.nsec;
202
203 macro_rules! write_with_tags {
205 ($ct:ty, $data:expr) => {{
206 let mut image = encoder.new_image::<$ct>(width, height).map_err(|e| {
207 ADError::UnsupportedConversion(format!("TIFF encoder error: {}", e))
208 })?;
209
210 macro_rules! tag {
211 ($tag:expr, $val:expr) => {
212 image.encoder().write_tag($tag, $val).map_err(|e| {
213 ADError::UnsupportedConversion(format!("TIFF tag write error: {}", e))
214 })?;
215 };
216 }
217
218 tag!(Tag::Unknown(TIFFTAG_NDTIMESTAMP), time_stamp);
220 tag!(Tag::Unknown(TIFFTAG_UNIQUEID), unique_id as u32);
221 tag!(Tag::Unknown(TIFFTAG_EPICSTSSEC), ts_sec);
222 tag!(Tag::Unknown(TIFFTAG_EPICSTSNSEC), ts_nsec);
223
224 tag!(Tag::Software, "EPICS areaDetector");
226 tag!(Tag::Model, &*model);
227 tag!(Tag::Make, &*make);
228 if let Some(desc) = &image_description {
229 tag!(Tag::ImageDescription, &**desc);
230 }
231
232 for (tag_num, tag_val) in &attr_tags {
234 tag!(Tag::Unknown(*tag_num), &**tag_val);
235 }
236
237 image
238 .write_data($data)
239 .map_err(|e| ADError::UnsupportedConversion(format!("TIFF write error: {}", e)))
240 }};
241 }
242
243 match &array.data {
244 NDDataBuffer::U8(v) => {
245 if is_rgb {
246 write_with_tags!(colortype::RGB8, v)
247 } else {
248 write_with_tags!(colortype::Gray8, v)
249 }
250 }
251 NDDataBuffer::I8(v) => {
252 if is_rgb {
253 write_with_tags!(signed_rgb::RGBI8, v)
254 } else {
255 write_with_tags!(colortype::GrayI8, v)
256 }
257 }
258 NDDataBuffer::U16(v) => {
259 if is_rgb {
260 write_with_tags!(colortype::RGB16, v)
261 } else {
262 write_with_tags!(colortype::Gray16, v)
263 }
264 }
265 NDDataBuffer::I16(v) => {
266 if is_rgb {
267 write_with_tags!(signed_rgb::RGBI16, v)
268 } else {
269 write_with_tags!(colortype::GrayI16, v)
270 }
271 }
272 NDDataBuffer::U32(v) => {
273 if is_rgb {
274 write_with_tags!(colortype::RGB32, v)
275 } else {
276 write_with_tags!(colortype::Gray32, v)
277 }
278 }
279 NDDataBuffer::I32(v) => {
280 if is_rgb {
281 write_with_tags!(signed_rgb::RGBI32, v)
282 } else {
283 write_with_tags!(colortype::GrayI32, v)
284 }
285 }
286 NDDataBuffer::I64(v) => {
287 if is_rgb {
288 write_with_tags!(signed_rgb::RGBI64, v)
289 } else {
290 write_with_tags!(colortype::GrayI64, v)
291 }
292 }
293 NDDataBuffer::U64(v) => {
294 if is_rgb {
295 write_with_tags!(colortype::RGB64, v)
296 } else {
297 write_with_tags!(colortype::Gray64, v)
298 }
299 }
300 NDDataBuffer::F32(v) => {
301 if is_rgb {
302 write_with_tags!(colortype::RGB32Float, v)
303 } else {
304 write_with_tags!(colortype::Gray32Float, v)
305 }
306 }
307 NDDataBuffer::F64(v) => {
308 if is_rgb {
309 write_with_tags!(colortype::RGB64Float, v)
310 } else {
311 write_with_tags!(colortype::Gray64Float, v)
312 }
313 }
314 }?;
315
316 Ok(())
317 }
318
319 fn read_file(&mut self) -> ADResult<NDArray> {
320 let path = self
321 .current_path
322 .as_ref()
323 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
324
325 let file = std::fs::File::open(path)?;
326 let mut decoder = Decoder::new(file)
327 .map_err(|e| ADError::UnsupportedConversion(format!("TIFF decode error: {}", e)))?;
328
329 let (width, height) = decoder
330 .dimensions()
331 .map_err(|e| ADError::UnsupportedConversion(format!("TIFF dimensions error: {}", e)))?;
332 let color_type = decoder
333 .colortype()
334 .map_err(|e| ADError::UnsupportedConversion(format!("TIFF colortype error: {}", e)))?;
335
336 let result = decoder
337 .read_image()
338 .map_err(|e| ADError::UnsupportedConversion(format!("TIFF read error: {}", e)))?;
339
340 let (dims, color_mode) = match color_type {
341 ColorType::Gray(_) => (
342 vec![
343 NDDimension::new(width as usize),
344 NDDimension::new(height as usize),
345 ],
346 NDColorMode::Mono,
347 ),
348 ColorType::RGB(_) => (
349 vec![
350 NDDimension::new(3),
351 NDDimension::new(width as usize),
352 NDDimension::new(height as usize),
353 ],
354 NDColorMode::RGB1,
355 ),
356 other => {
357 return Err(ADError::UnsupportedConversion(format!(
358 "unsupported TIFF color type: {:?}",
359 other
360 )));
361 }
362 };
363
364 let mut array = match result {
365 tiff::decoder::DecodingResult::U8(data) => {
366 let mut arr = NDArray::new(dims.clone(), NDDataType::UInt8);
367 arr.data = NDDataBuffer::U8(data);
368 arr
369 }
370 tiff::decoder::DecodingResult::U16(data) => {
371 let mut arr = NDArray::new(dims.clone(), NDDataType::UInt16);
372 arr.data = NDDataBuffer::U16(data);
373 arr
374 }
375 tiff::decoder::DecodingResult::U32(data) => {
376 let mut arr = NDArray::new(dims.clone(), NDDataType::UInt32);
377 arr.data = NDDataBuffer::U32(data);
378 arr
379 }
380 tiff::decoder::DecodingResult::U64(data) => {
381 let mut arr = NDArray::new(dims.clone(), NDDataType::UInt64);
382 arr.data = NDDataBuffer::U64(data);
383 arr
384 }
385 tiff::decoder::DecodingResult::I8(data) => {
386 let mut arr = NDArray::new(dims.clone(), NDDataType::Int8);
387 arr.data = NDDataBuffer::I8(data);
388 arr
389 }
390 tiff::decoder::DecodingResult::I16(data) => {
391 let mut arr = NDArray::new(dims.clone(), NDDataType::Int16);
392 arr.data = NDDataBuffer::I16(data);
393 arr
394 }
395 tiff::decoder::DecodingResult::I32(data) => {
396 let mut arr = NDArray::new(dims.clone(), NDDataType::Int32);
397 arr.data = NDDataBuffer::I32(data);
398 arr
399 }
400 tiff::decoder::DecodingResult::I64(data) => {
401 let mut arr = NDArray::new(dims.clone(), NDDataType::Int64);
402 arr.data = NDDataBuffer::I64(data);
403 arr
404 }
405 tiff::decoder::DecodingResult::F32(data) => {
406 let mut arr = NDArray::new(dims.clone(), NDDataType::Float32);
407 arr.data = NDDataBuffer::F32(data);
408 arr
409 }
410 tiff::decoder::DecodingResult::F64(data) => {
411 let mut arr = NDArray::new(dims.clone(), NDDataType::Float64);
412 arr.data = NDDataBuffer::F64(data);
413 arr
414 }
415 };
416 Self::attach_color_mode(&mut array, color_mode);
417 Ok(array)
418 }
419
420 fn close_file(&mut self) -> ADResult<()> {
421 self.current_path = None;
422 Ok(())
423 }
424
425 fn supports_multiple_arrays(&self) -> bool {
426 false
427 }
428}
429
430pub struct TiffFileProcessor {
432 pub ctrl: FilePluginController<TiffWriter>,
433}
434
435impl TiffFileProcessor {
436 pub fn new() -> Self {
437 Self {
438 ctrl: FilePluginController::new(TiffWriter::new()),
439 }
440 }
441}
442
443impl Default for TiffFileProcessor {
444 fn default() -> Self {
445 Self::new()
446 }
447}
448
449impl NDPluginProcess for TiffFileProcessor {
450 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
451 self.ctrl.process_array(array)
452 }
453
454 fn plugin_type(&self) -> &str {
455 "NDFileTIFF"
456 }
457
458 fn register_params(
459 &mut self,
460 base: &mut asyn_rs::port::PortDriverBase,
461 ) -> asyn_rs::error::AsynResult<()> {
462 self.ctrl.register_params(base)
463 }
464
465 fn on_param_change(
466 &mut self,
467 reason: usize,
468 params: &PluginParamSnapshot,
469 ) -> ParamChangeResult {
470 self.ctrl.on_param_change(reason, params)
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477 use ad_core_rs::ndarray::NDDataBuffer;
478 use ad_core_rs::params::ndarray_driver::NDArrayDriverParams;
479 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
480 use asyn_rs::port::{PortDriverBase, PortFlags};
481 use std::sync::atomic::{AtomicU32, Ordering};
482
483 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
484
485 fn temp_path(prefix: &str) -> PathBuf {
486 let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
487 std::env::temp_dir().join(format!("adcore_test_{}_{}.tif", prefix, n))
488 }
489
490 #[test]
491 fn test_write_u8_mono() {
492 let path = temp_path("tiff_u8");
493 let mut writer = TiffWriter::new();
494
495 let mut arr = NDArray::new(
496 vec![NDDimension::new(4), NDDimension::new(4)],
497 NDDataType::UInt8,
498 );
499 if let NDDataBuffer::U8(v) = &mut arr.data {
500 for i in 0..16 {
501 v[i] = i as u8;
502 }
503 }
504
505 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
506 writer.write_file(&arr).unwrap();
507 writer.close_file().unwrap();
508
509 let data = std::fs::read(&path).unwrap();
510 assert!(data.len() > 16);
511 assert!(
512 &data[0..2] == &[0x49, 0x49] || &data[0..2] == &[0x4D, 0x4D],
513 "Expected TIFF magic bytes"
514 );
515
516 std::fs::remove_file(&path).ok();
517 }
518
519 #[test]
520 fn test_write_u16() {
521 let path = temp_path("tiff_u16");
522 let mut writer = TiffWriter::new();
523
524 let arr = NDArray::new(
525 vec![NDDimension::new(4), NDDimension::new(4)],
526 NDDataType::UInt16,
527 );
528
529 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
530 writer.write_file(&arr).unwrap();
531 writer.close_file().unwrap();
532
533 let data = std::fs::read(&path).unwrap();
534 assert!(data.len() > 32);
535
536 std::fs::remove_file(&path).ok();
537 }
538
539 #[test]
540 fn test_roundtrip_u8() {
541 let path = temp_path("tiff_rt_u8");
542 let mut writer = TiffWriter::new();
543
544 let mut arr = NDArray::new(
545 vec![NDDimension::new(4), NDDimension::new(4)],
546 NDDataType::UInt8,
547 );
548 if let NDDataBuffer::U8(v) = &mut arr.data {
549 for i in 0..16 {
550 v[i] = (i * 10) as u8;
551 }
552 }
553
554 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
555 writer.write_file(&arr).unwrap();
556
557 let read_back = writer.read_file().unwrap();
558 if let (NDDataBuffer::U8(orig), NDDataBuffer::U8(read)) = (&arr.data, &read_back.data) {
559 assert_eq!(orig, read);
560 } else {
561 panic!("data type mismatch on roundtrip");
562 }
563
564 writer.close_file().unwrap();
565 std::fs::remove_file(&path).ok();
566 }
567
568 #[test]
569 fn test_roundtrip_u16() {
570 let path = temp_path("tiff_rt_u16");
571 let mut writer = TiffWriter::new();
572
573 let mut arr = NDArray::new(
574 vec![NDDimension::new(4), NDDimension::new(4)],
575 NDDataType::UInt16,
576 );
577 if let NDDataBuffer::U16(v) = &mut arr.data {
578 for i in 0..16 {
579 v[i] = (i * 1000) as u16;
580 }
581 }
582
583 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
584 writer.write_file(&arr).unwrap();
585
586 let read_back = writer.read_file().unwrap();
587 if let (NDDataBuffer::U16(orig), NDDataBuffer::U16(read)) = (&arr.data, &read_back.data) {
588 assert_eq!(orig, read);
589 } else {
590 panic!("data type mismatch on roundtrip");
591 }
592
593 writer.close_file().unwrap();
594 std::fs::remove_file(&path).ok();
595 }
596
597 #[test]
598 fn test_on_param_change_read_file_emits_array_and_resets_busy() {
599 let path = temp_path("tiff_read_param");
600 let mut writer = TiffWriter::new();
601
602 let mut arr = NDArray::new(
603 vec![NDDimension::new(4), NDDimension::new(3)],
604 NDDataType::UInt8,
605 );
606 arr.unique_id = 77;
607 if let NDDataBuffer::U8(v) = &mut arr.data {
608 for (i, item) in v.iter_mut().enumerate() {
609 *item = i as u8;
610 }
611 }
612
613 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
614 writer.write_file(&arr).unwrap();
615 writer.close_file().unwrap();
616
617 let mut base = PortDriverBase::new("TIFFTEST", 1, PortFlags::default());
618 let _nd_params = NDArrayDriverParams::create(&mut base).unwrap();
619
620 let mut proc = TiffFileProcessor::new();
621 proc.register_params(&mut base).unwrap();
622
623 let reason_path = base.find_param("FILE_PATH").unwrap();
624 let reason_name = base.find_param("FILE_NAME").unwrap();
625 let reason_template = base.find_param("FILE_TEMPLATE").unwrap();
626 let reason_read = base.find_param("READ_FILE").unwrap();
627
628 let _ = proc.on_param_change(
629 reason_path,
630 &PluginParamSnapshot {
631 enable_callbacks: true,
632 reason: reason_path,
633 addr: 0,
634 value: ParamChangeValue::Octet(
635 path.parent().unwrap().to_str().unwrap().to_string(),
636 ),
637 },
638 );
639 let _ = proc.on_param_change(
640 reason_name,
641 &PluginParamSnapshot {
642 enable_callbacks: true,
643 reason: reason_name,
644 addr: 0,
645 value: ParamChangeValue::Octet(
646 path.file_name().unwrap().to_str().unwrap().to_string(),
647 ),
648 },
649 );
650 let _ = proc.on_param_change(
651 reason_template,
652 &PluginParamSnapshot {
653 enable_callbacks: true,
654 reason: reason_template,
655 addr: 0,
656 value: ParamChangeValue::Octet("%s%s".into()),
657 },
658 );
659
660 let result = proc.on_param_change(
661 reason_read,
662 &PluginParamSnapshot {
663 enable_callbacks: true,
664 reason: reason_read,
665 addr: 0,
666 value: ParamChangeValue::Int32(1),
667 },
668 );
669
670 assert_eq!(result.output_arrays.len(), 1);
671 assert!(result.param_updates.iter().any(|u| matches!(
672 u,
673 ParamUpdate::Int32 { reason, value: 0, .. } if *reason == reason_read
674 )));
675 match &result.output_arrays[0].data {
676 NDDataBuffer::U8(v) => assert_eq!(v.len(), 12),
677 other => panic!("unexpected data buffer: {other:?}"),
678 }
679
680 std::fs::remove_file(&path).ok();
681 }
682
683 #[test]
684 fn test_metadata_tags_match_cpp_numbers_and_types() {
685 let path = temp_path("tiff_meta_tags");
686 let mut writer = TiffWriter::new();
687
688 let mut arr = NDArray::new(
689 vec![NDDimension::new(4), NDDimension::new(4)],
690 NDDataType::UInt8,
691 );
692 arr.unique_id = 4242;
693 arr.time_stamp = 1234.5;
694 arr.timestamp.sec = 1_000_000;
695 arr.timestamp.nsec = 500;
696
697 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
698 writer.write_file(&arr).unwrap();
699 writer.close_file().unwrap();
700
701 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
702 assert_eq!(decoder.get_tag_f64(Tag::Unknown(65000)).unwrap(), 1234.5);
704 assert_eq!(decoder.get_tag_u32(Tag::Unknown(65001)).unwrap(), 4242);
706 assert_eq!(decoder.get_tag_u32(Tag::Unknown(65002)).unwrap(), 1_000_000);
708 assert_eq!(decoder.get_tag_u32(Tag::Unknown(65003)).unwrap(), 500);
709 assert_eq!(
711 decoder
712 .get_tag(Tag::Software)
713 .unwrap()
714 .into_string()
715 .unwrap(),
716 "EPICS areaDetector"
717 );
718
719 std::fs::remove_file(&path).ok();
720 }
721
722 #[test]
723 fn test_standard_tags_from_attributes() {
724 let path = temp_path("tiff_std_tags");
725 let mut writer = TiffWriter::new();
726
727 let mut arr = NDArray::new(
728 vec![NDDimension::new(4), NDDimension::new(4)],
729 NDDataType::UInt8,
730 );
731 arr.attributes.add(NDAttribute::new_static(
732 "Model",
733 "",
734 NDAttrSource::Driver,
735 NDAttrValue::String("SimDetector".into()),
736 ));
737 arr.attributes.add(NDAttribute::new_static(
738 "Manufacturer",
739 "",
740 NDAttrSource::Driver,
741 NDAttrValue::String("EPICS".into()),
742 ));
743 arr.attributes.add(NDAttribute::new_static(
744 "TIFFImageDescription",
745 "",
746 NDAttrSource::Driver,
747 NDAttrValue::String("test frame".into()),
748 ));
749
750 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
751 writer.write_file(&arr).unwrap();
752 writer.close_file().unwrap();
753
754 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
755 assert_eq!(
756 decoder.get_tag(Tag::Model).unwrap().into_string().unwrap(),
757 "SimDetector"
758 );
759 assert_eq!(
760 decoder.get_tag(Tag::Make).unwrap().into_string().unwrap(),
761 "EPICS"
762 );
763 assert_eq!(
764 decoder
765 .get_tag(Tag::ImageDescription)
766 .unwrap()
767 .into_string()
768 .unwrap(),
769 "test frame"
770 );
771
772 std::fs::remove_file(&path).ok();
773 }
774
775 #[test]
776 fn test_attribute_tag_format_uses_colon_and_type() {
777 let mut a = NDArray::new(
779 vec![NDDimension::new(2), NDDimension::new(2)],
780 NDDataType::UInt8,
781 );
782 a.attributes.add(NDAttribute::new_static(
783 "Gain",
784 "",
785 NDAttrSource::Driver,
786 NDAttrValue::Int32(-7),
787 ));
788
789 let path = temp_path("tiff_attr_fmt");
790 let mut writer = TiffWriter::new();
791 writer.open_file(&path, NDFileMode::Single, &a).unwrap();
792 writer.write_file(&a).unwrap();
793 writer.close_file().unwrap();
794
795 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
796 let s = decoder
798 .get_tag(Tag::Unknown(65010))
799 .unwrap()
800 .into_string()
801 .unwrap();
802 assert_eq!(s, "Gain:-7");
803
804 std::fs::remove_file(&path).ok();
805 }
806
807 #[test]
808 fn test_signed_rgb_writes_instead_of_erroring() {
809 let path = temp_path("tiff_signed_rgb");
810 let mut writer = TiffWriter::new();
811
812 let mut arr = NDArray::new(
813 vec![
814 NDDimension::new(3),
815 NDDimension::new(2),
816 NDDimension::new(2),
817 ],
818 NDDataType::Int16,
819 );
820 TiffWriter::attach_color_mode(&mut arr, NDColorMode::RGB1);
821 if let NDDataBuffer::I16(v) = &mut arr.data {
822 for (i, item) in v.iter_mut().enumerate() {
823 *item = (i as i16) - 6;
824 }
825 }
826
827 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
828 writer.write_file(&arr).unwrap();
830 writer.close_file().unwrap();
831
832 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
833 let sf = decoder.get_tag_u16_vec(Tag::SampleFormat).unwrap();
834 assert!(sf.iter().all(|&s| s == 2), "expected signed sample format");
836
837 std::fs::remove_file(&path).ok();
838 }
839
840 #[test]
841 fn test_single_mode_requires_auto_save_for_automatic_write() {
842 let path = temp_path("tiff_autosave_single");
843 let full_name = path.to_string_lossy().to_string();
844 let file_path = path.parent().unwrap().to_str().unwrap().to_string();
845 let file_name = path.file_name().unwrap().to_str().unwrap().to_string();
846
847 let mut proc = TiffFileProcessor::new();
848 proc.ctrl.file_base.file_path = file_path.clone() + "/";
849 proc.ctrl.file_base.file_name = file_name;
850 proc.ctrl.file_base.file_template = "%s%s".into();
851 proc.ctrl.file_base.set_mode(NDFileMode::Single);
852
853 let mut arr = NDArray::new(
854 vec![NDDimension::new(4), NDDimension::new(4)],
855 NDDataType::UInt8,
856 );
857 if let NDDataBuffer::U8(v) = &mut arr.data {
858 for (i, item) in v.iter_mut().enumerate() {
859 *item = i as u8;
860 }
861 }
862
863 proc.ctrl.auto_save = false;
864 let _ = proc.process_array(&arr, &NDArrayPool::new(1024));
865 assert!(!std::path::Path::new(&full_name).exists());
866
867 proc.ctrl.auto_save = true;
868 let _ = proc.process_array(&arr, &NDArrayPool::new(1024));
869 assert!(std::path::Path::new(&full_name).exists());
870
871 std::fs::remove_file(&path).ok();
872 }
873}