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 does_array_callbacks(&self) -> bool {
461 false
462 }
463
464 fn register_params(
465 &mut self,
466 base: &mut asyn_rs::port::PortDriverBase,
467 ) -> asyn_rs::error::AsynResult<()> {
468 self.ctrl.register_params(base)
469 }
470
471 fn on_param_change(
472 &mut self,
473 reason: usize,
474 params: &PluginParamSnapshot,
475 ) -> ParamChangeResult {
476 self.ctrl.on_param_change(reason, params)
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 use ad_core_rs::ndarray::NDDataBuffer;
484 use ad_core_rs::params::ndarray_driver::NDArrayDriverParams;
485 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
486 use asyn_rs::port::{PortDriverBase, PortFlags};
487 use std::sync::atomic::{AtomicU32, Ordering};
488
489 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
490
491 fn temp_path(prefix: &str) -> PathBuf {
492 let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
493 std::env::temp_dir().join(format!("adcore_test_{}_{}.tif", prefix, n))
494 }
495
496 #[test]
497 fn test_write_u8_mono() {
498 let path = temp_path("tiff_u8");
499 let mut writer = TiffWriter::new();
500
501 let mut arr = NDArray::new(
502 vec![NDDimension::new(4), NDDimension::new(4)],
503 NDDataType::UInt8,
504 );
505 if let NDDataBuffer::U8(v) = &mut arr.data {
506 for i in 0..16 {
507 v[i] = i as u8;
508 }
509 }
510
511 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
512 writer.write_file(&arr).unwrap();
513 writer.close_file().unwrap();
514
515 let data = std::fs::read(&path).unwrap();
516 assert!(data.len() > 16);
517 assert!(
518 &data[0..2] == &[0x49, 0x49] || &data[0..2] == &[0x4D, 0x4D],
519 "Expected TIFF magic bytes"
520 );
521
522 std::fs::remove_file(&path).ok();
523 }
524
525 #[test]
526 fn test_write_u16() {
527 let path = temp_path("tiff_u16");
528 let mut writer = TiffWriter::new();
529
530 let arr = NDArray::new(
531 vec![NDDimension::new(4), NDDimension::new(4)],
532 NDDataType::UInt16,
533 );
534
535 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
536 writer.write_file(&arr).unwrap();
537 writer.close_file().unwrap();
538
539 let data = std::fs::read(&path).unwrap();
540 assert!(data.len() > 32);
541
542 std::fs::remove_file(&path).ok();
543 }
544
545 #[test]
546 fn test_roundtrip_u8() {
547 let path = temp_path("tiff_rt_u8");
548 let mut writer = TiffWriter::new();
549
550 let mut arr = NDArray::new(
551 vec![NDDimension::new(4), NDDimension::new(4)],
552 NDDataType::UInt8,
553 );
554 if let NDDataBuffer::U8(v) = &mut arr.data {
555 for i in 0..16 {
556 v[i] = (i * 10) as u8;
557 }
558 }
559
560 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
561 writer.write_file(&arr).unwrap();
562
563 let read_back = writer.read_file().unwrap();
564 if let (NDDataBuffer::U8(orig), NDDataBuffer::U8(read)) = (&arr.data, &read_back.data) {
565 assert_eq!(orig, read);
566 } else {
567 panic!("data type mismatch on roundtrip");
568 }
569
570 writer.close_file().unwrap();
571 std::fs::remove_file(&path).ok();
572 }
573
574 #[test]
575 fn test_roundtrip_u16() {
576 let path = temp_path("tiff_rt_u16");
577 let mut writer = TiffWriter::new();
578
579 let mut arr = NDArray::new(
580 vec![NDDimension::new(4), NDDimension::new(4)],
581 NDDataType::UInt16,
582 );
583 if let NDDataBuffer::U16(v) = &mut arr.data {
584 for i in 0..16 {
585 v[i] = (i * 1000) as u16;
586 }
587 }
588
589 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
590 writer.write_file(&arr).unwrap();
591
592 let read_back = writer.read_file().unwrap();
593 if let (NDDataBuffer::U16(orig), NDDataBuffer::U16(read)) = (&arr.data, &read_back.data) {
594 assert_eq!(orig, read);
595 } else {
596 panic!("data type mismatch on roundtrip");
597 }
598
599 writer.close_file().unwrap();
600 std::fs::remove_file(&path).ok();
601 }
602
603 #[test]
604 fn test_on_param_change_read_file_emits_array_and_resets_busy() {
605 let path = temp_path("tiff_read_param");
606 let mut writer = TiffWriter::new();
607
608 let mut arr = NDArray::new(
609 vec![NDDimension::new(4), NDDimension::new(3)],
610 NDDataType::UInt8,
611 );
612 arr.unique_id = 77;
613 if let NDDataBuffer::U8(v) = &mut arr.data {
614 for (i, item) in v.iter_mut().enumerate() {
615 *item = i as u8;
616 }
617 }
618
619 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
620 writer.write_file(&arr).unwrap();
621 writer.close_file().unwrap();
622
623 let mut base = PortDriverBase::new("TIFFTEST", 1, PortFlags::default());
624 let _nd_params = NDArrayDriverParams::create(&mut base).unwrap();
625
626 let mut proc = TiffFileProcessor::new();
627 proc.register_params(&mut base).unwrap();
628
629 let reason_path = base.find_param("FILE_PATH").unwrap();
630 let reason_name = base.find_param("FILE_NAME").unwrap();
631 let reason_template = base.find_param("FILE_TEMPLATE").unwrap();
632 let reason_read = base.find_param("READ_FILE").unwrap();
633
634 let _ = proc.on_param_change(
635 reason_path,
636 &PluginParamSnapshot {
637 enable_callbacks: true,
638 reason: reason_path,
639 addr: 0,
640 value: ParamChangeValue::Octet(
641 path.parent().unwrap().to_str().unwrap().to_string(),
642 ),
643 },
644 );
645 let _ = proc.on_param_change(
646 reason_name,
647 &PluginParamSnapshot {
648 enable_callbacks: true,
649 reason: reason_name,
650 addr: 0,
651 value: ParamChangeValue::Octet(
652 path.file_name().unwrap().to_str().unwrap().to_string(),
653 ),
654 },
655 );
656 let _ = proc.on_param_change(
657 reason_template,
658 &PluginParamSnapshot {
659 enable_callbacks: true,
660 reason: reason_template,
661 addr: 0,
662 value: ParamChangeValue::Octet("%s%s".into()),
663 },
664 );
665
666 let result = proc.on_param_change(
667 reason_read,
668 &PluginParamSnapshot {
669 enable_callbacks: true,
670 reason: reason_read,
671 addr: 0,
672 value: ParamChangeValue::Int32(1),
673 },
674 );
675
676 assert_eq!(result.output_arrays.len(), 1);
677 assert!(result.param_updates.iter().any(|u| matches!(
678 u,
679 ParamUpdate::Int32 { reason, value: 0, .. } if *reason == reason_read
680 )));
681 match &result.output_arrays[0].data {
682 NDDataBuffer::U8(v) => assert_eq!(v.len(), 12),
683 other => panic!("unexpected data buffer: {other:?}"),
684 }
685
686 std::fs::remove_file(&path).ok();
687 }
688
689 #[test]
690 fn test_metadata_tags_match_cpp_numbers_and_types() {
691 let path = temp_path("tiff_meta_tags");
692 let mut writer = TiffWriter::new();
693
694 let mut arr = NDArray::new(
695 vec![NDDimension::new(4), NDDimension::new(4)],
696 NDDataType::UInt8,
697 );
698 arr.unique_id = 4242;
699 arr.time_stamp = 1234.5;
700 arr.timestamp.sec = 1_000_000;
701 arr.timestamp.nsec = 500;
702
703 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
704 writer.write_file(&arr).unwrap();
705 writer.close_file().unwrap();
706
707 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
708 assert_eq!(decoder.get_tag_f64(Tag::Unknown(65000)).unwrap(), 1234.5);
710 assert_eq!(decoder.get_tag_u32(Tag::Unknown(65001)).unwrap(), 4242);
712 assert_eq!(decoder.get_tag_u32(Tag::Unknown(65002)).unwrap(), 1_000_000);
714 assert_eq!(decoder.get_tag_u32(Tag::Unknown(65003)).unwrap(), 500);
715 assert_eq!(
717 decoder
718 .get_tag(Tag::Software)
719 .unwrap()
720 .into_string()
721 .unwrap(),
722 "EPICS areaDetector"
723 );
724
725 std::fs::remove_file(&path).ok();
726 }
727
728 #[test]
729 fn test_standard_tags_from_attributes() {
730 let path = temp_path("tiff_std_tags");
731 let mut writer = TiffWriter::new();
732
733 let mut arr = NDArray::new(
734 vec![NDDimension::new(4), NDDimension::new(4)],
735 NDDataType::UInt8,
736 );
737 arr.attributes.add(NDAttribute::new_static(
738 "Model",
739 "",
740 NDAttrSource::Driver,
741 NDAttrValue::String("SimDetector".into()),
742 ));
743 arr.attributes.add(NDAttribute::new_static(
744 "Manufacturer",
745 "",
746 NDAttrSource::Driver,
747 NDAttrValue::String("EPICS".into()),
748 ));
749 arr.attributes.add(NDAttribute::new_static(
750 "TIFFImageDescription",
751 "",
752 NDAttrSource::Driver,
753 NDAttrValue::String("test frame".into()),
754 ));
755
756 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
757 writer.write_file(&arr).unwrap();
758 writer.close_file().unwrap();
759
760 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
761 assert_eq!(
762 decoder.get_tag(Tag::Model).unwrap().into_string().unwrap(),
763 "SimDetector"
764 );
765 assert_eq!(
766 decoder.get_tag(Tag::Make).unwrap().into_string().unwrap(),
767 "EPICS"
768 );
769 assert_eq!(
770 decoder
771 .get_tag(Tag::ImageDescription)
772 .unwrap()
773 .into_string()
774 .unwrap(),
775 "test frame"
776 );
777
778 std::fs::remove_file(&path).ok();
779 }
780
781 #[test]
782 fn test_attribute_tag_format_uses_colon_and_type() {
783 let mut a = NDArray::new(
785 vec![NDDimension::new(2), NDDimension::new(2)],
786 NDDataType::UInt8,
787 );
788 a.attributes.add(NDAttribute::new_static(
789 "Gain",
790 "",
791 NDAttrSource::Driver,
792 NDAttrValue::Int32(-7),
793 ));
794
795 let path = temp_path("tiff_attr_fmt");
796 let mut writer = TiffWriter::new();
797 writer.open_file(&path, NDFileMode::Single, &a).unwrap();
798 writer.write_file(&a).unwrap();
799 writer.close_file().unwrap();
800
801 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
802 let s = decoder
804 .get_tag(Tag::Unknown(65010))
805 .unwrap()
806 .into_string()
807 .unwrap();
808 assert_eq!(s, "Gain:-7");
809
810 std::fs::remove_file(&path).ok();
811 }
812
813 #[test]
814 fn test_signed_rgb_writes_instead_of_erroring() {
815 let path = temp_path("tiff_signed_rgb");
816 let mut writer = TiffWriter::new();
817
818 let mut arr = NDArray::new(
819 vec![
820 NDDimension::new(3),
821 NDDimension::new(2),
822 NDDimension::new(2),
823 ],
824 NDDataType::Int16,
825 );
826 TiffWriter::attach_color_mode(&mut arr, NDColorMode::RGB1);
827 if let NDDataBuffer::I16(v) = &mut arr.data {
828 for (i, item) in v.iter_mut().enumerate() {
829 *item = (i as i16) - 6;
830 }
831 }
832
833 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
834 writer.write_file(&arr).unwrap();
836 writer.close_file().unwrap();
837
838 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
839 let sf = decoder.get_tag_u16_vec(Tag::SampleFormat).unwrap();
840 assert!(sf.iter().all(|&s| s == 2), "expected signed sample format");
842
843 std::fs::remove_file(&path).ok();
844 }
845
846 #[test]
847 fn test_single_mode_requires_auto_save_for_automatic_write() {
848 let path = temp_path("tiff_autosave_single");
849 let full_name = path.to_string_lossy().to_string();
850 let file_path = path.parent().unwrap().to_str().unwrap().to_string();
851 let file_name = path.file_name().unwrap().to_str().unwrap().to_string();
852
853 let mut proc = TiffFileProcessor::new();
854 proc.ctrl.file_base.file_path = file_path.clone() + "/";
855 proc.ctrl.file_base.file_name = file_name;
856 proc.ctrl.file_base.file_template = "%s%s".into();
857 proc.ctrl.file_base.set_mode(NDFileMode::Single);
858
859 let mut arr = NDArray::new(
860 vec![NDDimension::new(4), NDDimension::new(4)],
861 NDDataType::UInt8,
862 );
863 if let NDDataBuffer::U8(v) = &mut arr.data {
864 for (i, item) in v.iter_mut().enumerate() {
865 *item = i as u8;
866 }
867 }
868
869 proc.ctrl.auto_save = false;
870 let _ = proc.process_array(&arr, &NDArrayPool::new(1024));
871 assert!(!std::path::Path::new(&full_name).exists());
872
873 proc.ctrl.auto_save = true;
874 let _ = proc.process_array(&arr, &NDArrayPool::new(1024));
875 assert!(std::path::Path::new(&full_name).exists());
876
877 std::fs::remove_file(&path).ok();
878 }
879}