1use std::collections::BTreeMap;
2use std::ops::Range;
3use std::path::{Path, PathBuf};
4
5use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
6use ad_core_rs::color::NDColorMode;
7use ad_core_rs::error::{ADError, ADResult};
8use ad_core_rs::ndarray::{NDArray, NDDataType, NDDimension};
9use ad_core_rs::ndarray_pool::NDArrayPool;
10use ad_core_rs::plugin::file_base::{NDFileMode, NDFileWriter};
11use ad_core_rs::plugin::file_controller::FilePluginController;
12use ad_core_rs::plugin::runtime::{
13 NDPluginProcess, ParamChangeResult, PluginParamSnapshot, ProcessResult,
14};
15
16const TAG_IMAGE_WIDTH: u16 = 256;
20const TAG_IMAGE_LENGTH: u16 = 257;
21const TAG_BITS_PER_SAMPLE: u16 = 258;
22const TAG_COMPRESSION: u16 = 259;
23const TAG_PHOTOMETRIC: u16 = 262;
24const TAG_IMAGE_DESCRIPTION: u16 = 270;
25const TAG_MAKE: u16 = 271;
26const TAG_MODEL: u16 = 272;
27const TAG_STRIP_OFFSETS: u16 = 273;
28const TAG_SAMPLES_PER_PIXEL: u16 = 277;
29const TAG_ROWS_PER_STRIP: u16 = 278;
30const TAG_STRIP_BYTE_COUNTS: u16 = 279;
31const TAG_PLANAR_CONFIG: u16 = 284;
32const TAG_SOFTWARE: u16 = 305;
33const TAG_SAMPLE_FORMAT: u16 = 339;
34const TIFFTAG_NDTIMESTAMP: u16 = 65000;
35const TIFFTAG_UNIQUEID: u16 = 65001;
36const TIFFTAG_EPICSTSSEC: u16 = 65002;
37const TIFFTAG_EPICSTSNSEC: u16 = 65003;
38const TIFFTAG_FIRST_ATTRIBUTE: u16 = 65010;
39
40const TYPE_ASCII: u16 = 2;
42const TYPE_SHORT: u16 = 3;
43const TYPE_LONG: u16 = 4;
44const TYPE_DOUBLE: u16 = 12;
45
46const COMPRESSION_NONE: u16 = 1;
48const PHOTOMETRIC_MINISBLACK: u16 = 1;
49const PHOTOMETRIC_RGB: u16 = 2;
50const PLANARCONFIG_CONTIG: u16 = 1;
51const PLANARCONFIG_SEPARATE: u16 = 2;
52const SAMPLEFORMAT_UINT: u16 = 1;
53const SAMPLEFORMAT_INT: u16 = 2;
54const SAMPLEFORMAT_IEEEFP: u16 = 3;
55
56struct TiffLayout {
64 width: usize,
65 height: usize,
66 samples_per_pixel: u16,
67 photometric: u16,
68 planar_config: u16,
69 rows_per_strip: usize,
70 color_mode: NDColorMode,
71}
72
73struct IfdEntry {
75 tag: u16,
76 field_type: u16,
77 count: u32,
78 data: Vec<u8>,
81}
82
83impl IfdEntry {
84 fn ascii(tag: u16, value: &str) -> Self {
85 let mut data = value.as_bytes().to_vec();
86 data.push(0); Self {
88 tag,
89 field_type: TYPE_ASCII,
90 count: data.len() as u32,
91 data,
92 }
93 }
94
95 fn short(tag: u16, values: &[u16]) -> Self {
96 Self {
97 tag,
98 field_type: TYPE_SHORT,
99 count: values.len() as u32,
100 data: values.iter().flat_map(|v| v.to_le_bytes()).collect(),
101 }
102 }
103
104 fn long(tag: u16, values: &[u32]) -> Self {
105 Self {
106 tag,
107 field_type: TYPE_LONG,
108 count: values.len() as u32,
109 data: values.iter().flat_map(|v| v.to_le_bytes()).collect(),
110 }
111 }
112
113 fn short_or_long(tag: u16, value: usize) -> ADResult<Self> {
117 let value = u32::try_from(value)
118 .map_err(|_| ADError::InvalidDimensions("TIFF dimension exceeds 2^32".into()))?;
119 Ok(if value <= u32::from(u16::MAX) {
120 Self::short(tag, &[value as u16])
121 } else {
122 Self::long(tag, &[value])
123 })
124 }
125
126 fn double(tag: u16, value: f64) -> Self {
127 Self {
128 tag,
129 field_type: TYPE_DOUBLE,
130 count: 1,
131 data: value.to_le_bytes().to_vec(),
132 }
133 }
134}
135
136fn attribute_tag_string(attr: &NDAttribute) -> String {
140 let value = match &attr.value {
141 NDAttrValue::Int8(v) => format!("{}", v),
142 NDAttrValue::Int16(v) => format!("{}", v),
143 NDAttrValue::Int32(v) => format!("{}", v),
144 NDAttrValue::Int64(v) => format!("{}", v),
145 NDAttrValue::UInt8(v) => format!("{}", v),
146 NDAttrValue::UInt16(v) => format!("{}", v),
147 NDAttrValue::UInt32(v) => format!("{}", v),
148 NDAttrValue::UInt64(v) => format!("{}", v),
149 NDAttrValue::Float32(v) => format!("{:.6}", v),
151 NDAttrValue::Float64(v) => format!("{:.6}", v),
152 NDAttrValue::String(s) => s.clone(),
153 NDAttrValue::Undefined => String::new(),
154 };
155 format!("{}:{}", attr.name, value)
156}
157
158pub struct TiffWriter {
160 current_path: Option<PathBuf>,
161}
162
163impl TiffWriter {
164 pub fn new() -> Self {
165 Self { current_path: None }
166 }
167
168 fn layout(array: &NDArray) -> ADResult<TiffLayout> {
173 let color_mode = array.info().color_mode;
182 let mono = |width: usize, height: usize| TiffLayout {
183 width,
184 height,
185 samples_per_pixel: 1,
186 photometric: PHOTOMETRIC_MINISBLACK,
187 planar_config: PLANARCONFIG_CONTIG,
188 rows_per_strip: height,
189 color_mode: NDColorMode::Mono,
190 };
191
192 Ok(match array.dims.as_slice() {
193 [x] => mono(x.size, 1),
194 [x, y] => mono(x.size, y.size),
195 [c, x, y] if c.size == 3 && color_mode == NDColorMode::RGB1 => TiffLayout {
196 width: x.size,
197 height: y.size,
198 samples_per_pixel: 3,
199 photometric: PHOTOMETRIC_RGB,
200 planar_config: PLANARCONFIG_CONTIG,
201 rows_per_strip: y.size,
202 color_mode: NDColorMode::RGB1,
203 },
204 [x, c, y] if c.size == 3 && color_mode == NDColorMode::RGB2 => TiffLayout {
205 width: x.size,
206 height: y.size,
207 samples_per_pixel: 3,
208 photometric: PHOTOMETRIC_RGB,
209 planar_config: PLANARCONFIG_SEPARATE,
210 rows_per_strip: 1,
211 color_mode: NDColorMode::RGB2,
212 },
213 [x, y, c] if c.size == 3 && color_mode == NDColorMode::RGB3 => TiffLayout {
214 width: x.size,
215 height: y.size,
216 samples_per_pixel: 3,
217 photometric: PHOTOMETRIC_RGB,
218 planar_config: PLANARCONFIG_SEPARATE,
219 rows_per_strip: y.size,
220 color_mode: NDColorMode::RGB3,
221 },
222 _ => {
223 return Err(ADError::InvalidDimensions(
224 "unsupported array structure".into(),
225 ));
226 }
227 })
228 }
229
230 fn sample_format_and_bits(data_type: NDDataType) -> (u16, u16) {
232 match data_type {
233 NDDataType::Int8 => (SAMPLEFORMAT_INT, 8),
234 NDDataType::UInt8 => (SAMPLEFORMAT_UINT, 8),
235 NDDataType::Int16 => (SAMPLEFORMAT_INT, 16),
236 NDDataType::UInt16 => (SAMPLEFORMAT_UINT, 16),
237 NDDataType::Int32 => (SAMPLEFORMAT_INT, 32),
238 NDDataType::UInt32 => (SAMPLEFORMAT_UINT, 32),
239 NDDataType::Int64 => (SAMPLEFORMAT_INT, 64),
240 NDDataType::UInt64 => (SAMPLEFORMAT_UINT, 64),
241 NDDataType::Float32 => (SAMPLEFORMAT_IEEEFP, 32),
242 NDDataType::Float64 => (SAMPLEFORMAT_IEEEFP, 64),
243 }
244 }
245
246 fn strip_writes(layout: &TiffLayout, bytes_per_sample: usize) -> Vec<(usize, Range<usize>)> {
256 let TiffLayout { width, height, .. } = *layout;
257 match layout.color_mode {
258 NDColorMode::RGB2 => {
259 let strip = width * bytes_per_sample; let mut writes = Vec::with_capacity(3 * height);
261 for row in 0..height {
262 let red = 3 * row * strip;
263 writes.push((row, red..red + strip));
264 writes.push((height + row, red + strip..red + 2 * strip));
265 writes.push((2 * height + row, red + 2 * strip..red + 3 * strip));
266 }
267 writes
268 }
269 NDColorMode::RGB3 => {
270 let plane = width * height * bytes_per_sample;
271 (0..3).map(|p| (p, p * plane..(p + 1) * plane)).collect()
272 }
273 _ => {
275 let total =
276 width * height * usize::from(layout.samples_per_pixel) * bytes_per_sample;
277 vec![(0, 0..total)]
278 }
279 }
280 }
281
282 fn encode(array: &NDArray, layout: &TiffLayout) -> ADResult<Vec<u8>> {
292 let raw = array.data.as_u8_slice();
293 let (sample_format, bits_per_sample) = Self::sample_format_and_bits(array.data.data_type());
294 let bytes_per_sample = usize::from(bits_per_sample) / 8;
295
296 let writes = Self::strip_writes(layout, bytes_per_sample);
297 let expected: usize = writes.iter().map(|(_, r)| r.len()).sum();
298 if raw.len() < expected {
299 return Err(ADError::InvalidDimensions(format!(
300 "TIFF: array holds {} bytes, the {:?} layout needs {}",
301 raw.len(),
302 layout.color_mode,
303 expected
304 )));
305 }
306
307 let mut out: Vec<u8> = Vec::with_capacity(expected + 1024);
308 out.extend_from_slice(b"II"); out.extend_from_slice(&42u16.to_le_bytes());
310 out.extend_from_slice(&0u32.to_le_bytes()); let strip_count = writes.len();
313 let mut strip_offsets = vec![0u32; strip_count];
314 let mut strip_byte_counts = vec![0u32; strip_count];
315 for (index, range) in writes {
316 strip_offsets[index] = out.len() as u32;
317 strip_byte_counts[index] = range.len() as u32;
318 out.extend_from_slice(&raw[range]);
319 }
320 if out.len() % 2 != 0 {
321 out.push(0); }
323
324 let samples = usize::from(layout.samples_per_pixel);
325 let mut entries = vec![
326 IfdEntry::short_or_long(TAG_IMAGE_WIDTH, layout.width)?,
327 IfdEntry::short_or_long(TAG_IMAGE_LENGTH, layout.height)?,
328 IfdEntry::short(TAG_BITS_PER_SAMPLE, &vec![bits_per_sample; samples]),
329 IfdEntry::short(TAG_COMPRESSION, &[COMPRESSION_NONE]),
330 IfdEntry::short(TAG_PHOTOMETRIC, &[layout.photometric]),
331 IfdEntry::long(TAG_STRIP_OFFSETS, &strip_offsets),
332 IfdEntry::short(TAG_SAMPLES_PER_PIXEL, &[layout.samples_per_pixel]),
333 IfdEntry::short_or_long(TAG_ROWS_PER_STRIP, layout.rows_per_strip)?,
334 IfdEntry::long(TAG_STRIP_BYTE_COUNTS, &strip_byte_counts),
335 IfdEntry::short(TAG_PLANAR_CONFIG, &[layout.planar_config]),
336 IfdEntry::short(TAG_SAMPLE_FORMAT, &vec![sample_format; samples]),
337 ];
338
339 let model = array
341 .attributes
342 .get("Model")
343 .map(|a| a.value.as_string())
344 .unwrap_or_else(|| "Unknown".to_string());
345 let make = array
346 .attributes
347 .get("Manufacturer")
348 .map(|a| a.value.as_string())
349 .unwrap_or_else(|| "Unknown".to_string());
350 entries.push(IfdEntry::ascii(TAG_MODEL, &model));
351 entries.push(IfdEntry::ascii(TAG_MAKE, &make));
352 entries.push(IfdEntry::ascii(TAG_SOFTWARE, "EPICS areaDetector"));
353 if let Some(desc) = array.attributes.get("TIFFImageDescription") {
354 entries.push(IfdEntry::ascii(
355 TAG_IMAGE_DESCRIPTION,
356 &desc.value.as_string(),
357 ));
358 }
359
360 entries.push(IfdEntry::double(TIFFTAG_NDTIMESTAMP, array.time_stamp));
362 entries.push(IfdEntry::long(TIFFTAG_UNIQUEID, &[array.unique_id as u32]));
363 entries.push(IfdEntry::long(TIFFTAG_EPICSTSSEC, &[array.timestamp.sec]));
364 entries.push(IfdEntry::long(TIFFTAG_EPICSTSNSEC, &[array.timestamp.nsec]));
365
366 for (i, attr) in array.attributes.iter().enumerate() {
368 let Some(tag) = TIFFTAG_FIRST_ATTRIBUTE.checked_add(i as u16) else {
369 break; };
371 entries.push(IfdEntry::ascii(tag, &attribute_tag_string(attr)));
372 }
373
374 entries.sort_by_key(|e| e.tag); let ifd_offset = out.len();
377 let ifd_size = 2 + 12 * entries.len() + 4;
378 let mut values_offset = ifd_offset + ifd_size;
379 let mut ifd: Vec<u8> = Vec::with_capacity(ifd_size);
380 let mut values: Vec<u8> = Vec::new();
381
382 ifd.extend_from_slice(&(entries.len() as u16).to_le_bytes());
383 for entry in &entries {
384 ifd.extend_from_slice(&entry.tag.to_le_bytes());
385 ifd.extend_from_slice(&entry.field_type.to_le_bytes());
386 ifd.extend_from_slice(&entry.count.to_le_bytes());
387 if entry.data.len() <= 4 {
388 let mut inline = entry.data.clone();
389 inline.resize(4, 0);
390 ifd.extend_from_slice(&inline);
391 } else {
392 ifd.extend_from_slice(&(values_offset as u32).to_le_bytes());
393 values.extend_from_slice(&entry.data);
394 if values.len() % 2 != 0 {
395 values.push(0);
396 }
397 values_offset = ifd_offset + ifd_size + values.len();
398 }
399 }
400 ifd.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&ifd);
403 out.extend_from_slice(&values);
404 out[4..8].copy_from_slice(&(ifd_offset as u32).to_le_bytes());
405 Ok(out)
406 }
407
408 fn attach_color_mode(array: &mut NDArray, color_mode: NDColorMode) {
409 array.attributes.add(NDAttribute::new_static(
410 "ColorMode",
411 "Color mode",
412 NDAttrSource::Driver,
413 NDAttrValue::Int32(color_mode as i32),
414 ));
415 }
416}
417
418impl NDFileWriter for TiffWriter {
419 fn open_file(&mut self, path: &Path, _mode: NDFileMode, _array: &NDArray) -> ADResult<()> {
420 self.current_path = Some(path.to_path_buf());
421 Ok(())
422 }
423
424 fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
425 let path = self
426 .current_path
427 .as_ref()
428 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
429 let layout = Self::layout(array)?;
430 let bytes = Self::encode(array, &layout)?;
431 std::fs::write(path, bytes)?;
432 Ok(())
433 }
434
435 fn read_file(&mut self) -> ADResult<NDArray> {
436 let path = self
437 .current_path
438 .as_ref()
439 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
440 let bytes = std::fs::read(path)?;
441 decode(&bytes)
442 }
443
444 fn close_file(&mut self) -> ADResult<()> {
445 self.current_path = None;
446 Ok(())
447 }
448
449 fn supports_multiple_arrays(&self) -> bool {
450 false
451 }
452}
453
454fn decode(bytes: &[u8]) -> ADResult<NDArray> {
464 let err = |m: &str| ADError::UnsupportedConversion(format!("TIFF decode error: {m}"));
465
466 let read_u16 = |off: usize| -> ADResult<u16> {
467 bytes
468 .get(off..off + 2)
469 .map(|b| u16::from_le_bytes([b[0], b[1]]))
470 .ok_or_else(|| err("truncated"))
471 };
472 let read_u32 = |off: usize| -> ADResult<u32> {
473 bytes
474 .get(off..off + 4)
475 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
476 .ok_or_else(|| err("truncated"))
477 };
478
479 if bytes.len() < 8 || &bytes[0..2] != b"II" || read_u16(2)? != 42 {
480 return Err(err("not a little-endian classic TIFF"));
481 }
482
483 let ifd = read_u32(4)? as usize;
485 let entry_count = read_u16(ifd)? as usize;
486 let mut tags: BTreeMap<u16, Vec<u64>> = BTreeMap::new();
487 for i in 0..entry_count {
488 let entry = ifd + 2 + 12 * i;
489 let tag = read_u16(entry)?;
490 let size = match read_u16(entry + 2)? {
491 TYPE_SHORT => 2usize,
492 TYPE_LONG => 4,
493 _ => continue,
494 };
495 let n = read_u32(entry + 4)? as usize;
496 let base = if n * size <= 4 {
497 entry + 8
498 } else {
499 read_u32(entry + 8)? as usize
500 };
501 let mut values = Vec::with_capacity(n);
502 for k in 0..n {
503 values.push(match size {
504 2 => u64::from(read_u16(base + 2 * k)?),
505 _ => u64::from(read_u32(base + 4 * k)?),
506 });
507 }
508 tags.insert(tag, values);
509 }
510
511 let scalar = |tag: u16| -> Option<u64> { tags.get(&tag).and_then(|v| v.first().copied()) };
512 let width = scalar(TAG_IMAGE_WIDTH).ok_or_else(|| err("no ImageWidth"))? as usize;
513 let height = scalar(TAG_IMAGE_LENGTH).ok_or_else(|| err("no ImageLength"))? as usize;
514 let bits = scalar(TAG_BITS_PER_SAMPLE).ok_or_else(|| err("no BitsPerSample"))?;
515 let samples_per_pixel = scalar(TAG_SAMPLES_PER_PIXEL).unwrap_or(1);
516 let photometric = scalar(TAG_PHOTOMETRIC).ok_or_else(|| err("no PhotometricInterpretation"))?;
517 let planar_config = scalar(TAG_PLANAR_CONFIG).unwrap_or(u64::from(PLANARCONFIG_CONTIG));
518 let sample_format = match scalar(TAG_SAMPLE_FORMAT) {
520 Some(0) | None => u64::from(SAMPLEFORMAT_UINT),
521 Some(v) => v,
522 };
523
524 let data_type = match (bits, sample_format as u16) {
525 (8, SAMPLEFORMAT_INT) => NDDataType::Int8,
526 (8, SAMPLEFORMAT_UINT) => NDDataType::UInt8,
527 (16, SAMPLEFORMAT_INT) => NDDataType::Int16,
528 (16, SAMPLEFORMAT_UINT) => NDDataType::UInt16,
529 (32, SAMPLEFORMAT_INT) => NDDataType::Int32,
530 (32, SAMPLEFORMAT_UINT) => NDDataType::UInt32,
531 (64, SAMPLEFORMAT_INT) => NDDataType::Int64,
532 (64, SAMPLEFORMAT_UINT) => NDDataType::UInt64,
533 (32, SAMPLEFORMAT_IEEEFP) => NDDataType::Float32,
534 (64, SAMPLEFORMAT_IEEEFP) => NDDataType::Float64,
535 _ => {
536 return Err(err(&format!(
537 "unsupported bitsPerSample={bits} sampleFormat={sample_format}"
538 )));
539 }
540 };
541
542 let (dims, color_mode) = match (photometric as u16, planar_config as u16, samples_per_pixel) {
543 (PHOTOMETRIC_MINISBLACK, PLANARCONFIG_CONTIG, 1) => (
544 vec![NDDimension::new(width), NDDimension::new(height)],
545 NDColorMode::Mono,
546 ),
547 (PHOTOMETRIC_RGB, PLANARCONFIG_CONTIG, 3) => (
548 vec![
549 NDDimension::new(3),
550 NDDimension::new(width),
551 NDDimension::new(height),
552 ],
553 NDColorMode::RGB1,
554 ),
555 (PHOTOMETRIC_RGB, PLANARCONFIG_SEPARATE, 3) => (
556 vec![
557 NDDimension::new(width),
558 NDDimension::new(height),
559 NDDimension::new(3),
560 ],
561 NDColorMode::RGB3,
562 ),
563 _ => {
564 return Err(err(&format!(
565 "unsupported photoMetric={photometric}, planarConfig={planar_config}, \
566 samplesPerPixel={samples_per_pixel}"
567 )));
568 }
569 };
570
571 let offsets = tags
572 .get(&TAG_STRIP_OFFSETS)
573 .ok_or_else(|| err("no StripOffsets"))?;
574 let counts = tags
575 .get(&TAG_STRIP_BYTE_COUNTS)
576 .ok_or_else(|| err("no StripByteCounts"))?;
577 if offsets.len() != counts.len() {
578 return Err(err("StripOffsets/StripByteCounts length mismatch"));
579 }
580 let mut raw: Vec<u8> = Vec::new();
581 for (offset, byte_count) in offsets.iter().zip(counts) {
582 let (start, len) = (*offset as usize, *byte_count as usize);
583 let strip = bytes
584 .get(start..start + len)
585 .ok_or_else(|| err("strip extends past the end of the file"))?;
586 raw.extend_from_slice(strip);
587 }
588
589 let mut array = NDArray::new(dims, data_type);
590 array.data = crate::codec::buffer_from_bytes(&raw, data_type)
591 .ok_or_else(|| err("strip data is not a whole number of samples"))?;
592 TiffWriter::attach_color_mode(&mut array, color_mode);
593 Ok(array)
594}
595
596pub struct TiffFileProcessor {
598 pub ctrl: FilePluginController<TiffWriter>,
599}
600
601impl TiffFileProcessor {
602 pub fn new() -> Self {
603 Self {
604 ctrl: FilePluginController::new(TiffWriter::new()),
605 }
606 }
607}
608
609impl Default for TiffFileProcessor {
610 fn default() -> Self {
611 Self::new()
612 }
613}
614
615impl NDPluginProcess for TiffFileProcessor {
616 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
617 self.ctrl.process_array(array)
618 }
619
620 fn plugin_type(&self) -> &str {
621 "NDFileTIFF"
622 }
623
624 fn does_array_callbacks(&self) -> bool {
627 false
628 }
629
630 fn register_params(
631 &mut self,
632 base: &mut asyn_rs::port::PortDriverBase,
633 ) -> asyn_rs::error::AsynResult<()> {
634 self.ctrl.register_params(base)
635 }
636
637 fn on_param_change(
638 &mut self,
639 reason: usize,
640 params: &PluginParamSnapshot,
641 ) -> ParamChangeResult {
642 self.ctrl.on_param_change(reason, params)
643 }
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649 use ad_core_rs::ndarray::NDDataBuffer;
650 use ad_core_rs::params::ndarray_driver::NDArrayDriverParams;
653 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
654 use asyn_rs::port::{PortDriverBase, PortFlags};
655 use std::sync::atomic::{AtomicU32, Ordering};
656 use tiff::decoder::Decoder;
657 use tiff::tags::Tag;
658
659 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
660
661 fn temp_path(prefix: &str) -> PathBuf {
662 let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
663 std::env::temp_dir().join(format!("adcore_test_{}_{}.tif", prefix, n))
664 }
665
666 #[test]
674 fn test_r8_75_3d_without_colormode_attribute_is_an_error() {
675 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
676 use ad_core_rs::color::NDColorMode;
677
678 let rgb1_dims = || {
679 vec![
680 NDDimension::new(3),
681 NDDimension::new(4),
682 NDDimension::new(4),
683 ]
684 };
685
686 let arr = NDArray::new(rgb1_dims(), NDDataType::UInt8);
688 let path = temp_path("tiff_3d_no_colormode");
689 let mut writer = TiffWriter::new();
690 writer
691 .open_file(&path, NDFileMode::Single, &arr)
692 .expect("open");
693 let err = writer.write_file(&arr).unwrap_err();
694 assert!(
695 matches!(err, ADError::InvalidDimensions(_)),
696 "3-D without ColorMode must be rejected, got {err:?}"
697 );
698 std::fs::remove_file(&path).ok();
699
700 let mut arr = NDArray::new(rgb1_dims(), NDDataType::UInt8);
703 arr.attributes.add(NDAttribute::new_static(
704 "ColorMode",
705 "",
706 NDAttrSource::Driver,
707 NDAttrValue::Int32(NDColorMode::RGB1 as i32),
708 ));
709 let path = temp_path("tiff_3d_rgb1");
710 let mut writer = TiffWriter::new();
711 writer
712 .open_file(&path, NDFileMode::Single, &arr)
713 .expect("open");
714 writer
715 .write_file(&arr)
716 .expect("3-D WITH ColorMode=RGB1 must still write");
717 writer.close_file().ok();
718 assert!(path.exists());
719 std::fs::remove_file(&path).ok();
720 }
721
722 #[test]
723 fn test_write_u8_mono() {
724 let path = temp_path("tiff_u8");
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 if let NDDataBuffer::U8(v) = &mut arr.data {
732 for i in 0..16 {
733 v[i] = i as u8;
734 }
735 }
736
737 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
738 writer.write_file(&arr).unwrap();
739 writer.close_file().unwrap();
740
741 let data = std::fs::read(&path).unwrap();
742 assert!(data.len() > 16);
743 assert!(
744 &data[0..2] == &[0x49, 0x49] || &data[0..2] == &[0x4D, 0x4D],
745 "Expected TIFF magic bytes"
746 );
747
748 std::fs::remove_file(&path).ok();
749 }
750
751 #[test]
752 fn test_write_u16() {
753 let path = temp_path("tiff_u16");
754 let mut writer = TiffWriter::new();
755
756 let arr = NDArray::new(
757 vec![NDDimension::new(4), NDDimension::new(4)],
758 NDDataType::UInt16,
759 );
760
761 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
762 writer.write_file(&arr).unwrap();
763 writer.close_file().unwrap();
764
765 let data = std::fs::read(&path).unwrap();
766 assert!(data.len() > 32);
767
768 std::fs::remove_file(&path).ok();
769 }
770
771 #[test]
772 fn test_roundtrip_u8() {
773 let path = temp_path("tiff_rt_u8");
774 let mut writer = TiffWriter::new();
775
776 let mut arr = NDArray::new(
777 vec![NDDimension::new(4), NDDimension::new(4)],
778 NDDataType::UInt8,
779 );
780 if let NDDataBuffer::U8(v) = &mut arr.data {
781 for i in 0..16 {
782 v[i] = (i * 10) as u8;
783 }
784 }
785
786 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
787 writer.write_file(&arr).unwrap();
788
789 let read_back = writer.read_file().unwrap();
790 if let (NDDataBuffer::U8(orig), NDDataBuffer::U8(read)) = (&arr.data, &read_back.data) {
791 assert_eq!(orig, read);
792 } else {
793 panic!("data type mismatch on roundtrip");
794 }
795
796 writer.close_file().unwrap();
797 std::fs::remove_file(&path).ok();
798 }
799
800 #[test]
801 fn test_roundtrip_u16() {
802 let path = temp_path("tiff_rt_u16");
803 let mut writer = TiffWriter::new();
804
805 let mut arr = NDArray::new(
806 vec![NDDimension::new(4), NDDimension::new(4)],
807 NDDataType::UInt16,
808 );
809 if let NDDataBuffer::U16(v) = &mut arr.data {
810 for i in 0..16 {
811 v[i] = (i * 1000) as u16;
812 }
813 }
814
815 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
816 writer.write_file(&arr).unwrap();
817
818 let read_back = writer.read_file().unwrap();
819 if let (NDDataBuffer::U16(orig), NDDataBuffer::U16(read)) = (&arr.data, &read_back.data) {
820 assert_eq!(orig, read);
821 } else {
822 panic!("data type mismatch on roundtrip");
823 }
824
825 writer.close_file().unwrap();
826 std::fs::remove_file(&path).ok();
827 }
828
829 #[test]
830 fn test_on_param_change_read_file_emits_array_and_resets_busy() {
831 let path = temp_path("tiff_read_param");
832 let mut writer = TiffWriter::new();
833
834 let mut arr = NDArray::new(
835 vec![NDDimension::new(4), NDDimension::new(3)],
836 NDDataType::UInt8,
837 );
838 arr.unique_id = 77;
839 if let NDDataBuffer::U8(v) = &mut arr.data {
840 for (i, item) in v.iter_mut().enumerate() {
841 *item = i as u8;
842 }
843 }
844
845 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
846 writer.write_file(&arr).unwrap();
847 writer.close_file().unwrap();
848
849 let mut base = PortDriverBase::new("TIFFTEST", 1, PortFlags::default());
850 let _nd_params = NDArrayDriverParams::create(&mut base).unwrap();
851
852 let mut proc = TiffFileProcessor::new();
853 proc.register_params(&mut base).unwrap();
854
855 let reason_path = base.find_param("FILE_PATH").unwrap();
856 let reason_name = base.find_param("FILE_NAME").unwrap();
857 let reason_template = base.find_param("FILE_TEMPLATE").unwrap();
858 let reason_read = base.find_param("READ_FILE").unwrap();
859
860 let _ = proc.on_param_change(
861 reason_path,
862 &PluginParamSnapshot {
863 enable_callbacks: true,
864 reason: reason_path,
865 addr: 0,
866 value: ParamChangeValue::Octet(
867 path.parent().unwrap().to_str().unwrap().to_string(),
868 ),
869 },
870 );
871 let _ = proc.on_param_change(
872 reason_name,
873 &PluginParamSnapshot {
874 enable_callbacks: true,
875 reason: reason_name,
876 addr: 0,
877 value: ParamChangeValue::Octet(
878 path.file_name().unwrap().to_str().unwrap().to_string(),
879 ),
880 },
881 );
882 let _ = proc.on_param_change(
883 reason_template,
884 &PluginParamSnapshot {
885 enable_callbacks: true,
886 reason: reason_template,
887 addr: 0,
888 value: ParamChangeValue::Octet("%s%s".into()),
889 },
890 );
891
892 let result = proc.on_param_change(
893 reason_read,
894 &PluginParamSnapshot {
895 enable_callbacks: true,
896 reason: reason_read,
897 addr: 0,
898 value: ParamChangeValue::Int32(1),
899 },
900 );
901
902 assert_eq!(result.output_arrays.len(), 1);
903 assert!(result.param_updates.iter().any(|u| matches!(
904 u,
905 ParamUpdate::Int32 { reason, value: 0, .. } if *reason == reason_read
906 )));
907 match &result.output_arrays[0].data {
908 NDDataBuffer::U8(v) => assert_eq!(v.len(), 12),
909 other => panic!("unexpected data buffer: {other:?}"),
910 }
911
912 std::fs::remove_file(&path).ok();
913 }
914
915 #[test]
916 fn test_metadata_tags_match_cpp_numbers_and_types() {
917 let path = temp_path("tiff_meta_tags");
918 let mut writer = TiffWriter::new();
919
920 let mut arr = NDArray::new(
921 vec![NDDimension::new(4), NDDimension::new(4)],
922 NDDataType::UInt8,
923 );
924 arr.unique_id = 4242;
925 arr.time_stamp = 1234.5;
926 arr.timestamp.sec = 1_000_000;
927 arr.timestamp.nsec = 500;
928
929 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
930 writer.write_file(&arr).unwrap();
931 writer.close_file().unwrap();
932
933 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
934 assert_eq!(decoder.get_tag_f64(Tag::Unknown(65000)).unwrap(), 1234.5);
936 assert_eq!(decoder.get_tag_u32(Tag::Unknown(65001)).unwrap(), 4242);
938 assert_eq!(decoder.get_tag_u32(Tag::Unknown(65002)).unwrap(), 1_000_000);
940 assert_eq!(decoder.get_tag_u32(Tag::Unknown(65003)).unwrap(), 500);
941 assert_eq!(
943 decoder
944 .get_tag(Tag::Software)
945 .unwrap()
946 .into_string()
947 .unwrap(),
948 "EPICS areaDetector"
949 );
950
951 std::fs::remove_file(&path).ok();
952 }
953
954 #[test]
955 fn test_standard_tags_from_attributes() {
956 let path = temp_path("tiff_std_tags");
957 let mut writer = TiffWriter::new();
958
959 let mut arr = NDArray::new(
960 vec![NDDimension::new(4), NDDimension::new(4)],
961 NDDataType::UInt8,
962 );
963 arr.attributes.add(NDAttribute::new_static(
964 "Model",
965 "",
966 NDAttrSource::Driver,
967 NDAttrValue::String("SimDetector".into()),
968 ));
969 arr.attributes.add(NDAttribute::new_static(
970 "Manufacturer",
971 "",
972 NDAttrSource::Driver,
973 NDAttrValue::String("EPICS".into()),
974 ));
975 arr.attributes.add(NDAttribute::new_static(
976 "TIFFImageDescription",
977 "",
978 NDAttrSource::Driver,
979 NDAttrValue::String("test frame".into()),
980 ));
981
982 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
983 writer.write_file(&arr).unwrap();
984 writer.close_file().unwrap();
985
986 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
987 assert_eq!(
988 decoder.get_tag(Tag::Model).unwrap().into_string().unwrap(),
989 "SimDetector"
990 );
991 assert_eq!(
992 decoder.get_tag(Tag::Make).unwrap().into_string().unwrap(),
993 "EPICS"
994 );
995 assert_eq!(
996 decoder
997 .get_tag(Tag::ImageDescription)
998 .unwrap()
999 .into_string()
1000 .unwrap(),
1001 "test frame"
1002 );
1003
1004 std::fs::remove_file(&path).ok();
1005 }
1006
1007 #[test]
1008 fn test_attribute_tag_format_uses_colon_and_type() {
1009 let mut a = NDArray::new(
1011 vec![NDDimension::new(2), NDDimension::new(2)],
1012 NDDataType::UInt8,
1013 );
1014 a.attributes.add(NDAttribute::new_static(
1015 "Gain",
1016 "",
1017 NDAttrSource::Driver,
1018 NDAttrValue::Int32(-7),
1019 ));
1020
1021 let path = temp_path("tiff_attr_fmt");
1022 let mut writer = TiffWriter::new();
1023 writer.open_file(&path, NDFileMode::Single, &a).unwrap();
1024 writer.write_file(&a).unwrap();
1025 writer.close_file().unwrap();
1026
1027 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
1028 let s = decoder
1030 .get_tag(Tag::Unknown(65010))
1031 .unwrap()
1032 .into_string()
1033 .unwrap();
1034 assert_eq!(s, "Gain:-7");
1035
1036 std::fs::remove_file(&path).ok();
1037 }
1038
1039 #[test]
1040 fn test_signed_rgb_writes_instead_of_erroring() {
1041 let path = temp_path("tiff_signed_rgb");
1042 let mut writer = TiffWriter::new();
1043
1044 let mut arr = NDArray::new(
1045 vec![
1046 NDDimension::new(3),
1047 NDDimension::new(2),
1048 NDDimension::new(2),
1049 ],
1050 NDDataType::Int16,
1051 );
1052 TiffWriter::attach_color_mode(&mut arr, NDColorMode::RGB1);
1053 if let NDDataBuffer::I16(v) = &mut arr.data {
1054 for (i, item) in v.iter_mut().enumerate() {
1055 *item = (i as i16) - 6;
1056 }
1057 }
1058
1059 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
1060 writer.write_file(&arr).unwrap();
1062 writer.close_file().unwrap();
1063
1064 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
1065 let sf = decoder.get_tag_u16_vec(Tag::SampleFormat).unwrap();
1066 assert!(sf.iter().all(|&s| s == 2), "expected signed sample format");
1068
1069 std::fs::remove_file(&path).ok();
1070 }
1071
1072 fn rgb_array(mode: NDColorMode, w: usize, h: usize) -> NDArray {
1077 let dims = match mode {
1078 NDColorMode::RGB1 => vec![3, w, h],
1079 NDColorMode::RGB2 => vec![w, 3, h],
1080 NDColorMode::RGB3 => vec![w, h, 3],
1081 other => panic!("not an RGB layout: {other:?}"),
1082 };
1083 let mut arr = NDArray::new(
1084 dims.into_iter().map(NDDimension::new).collect(),
1085 NDDataType::UInt8,
1086 );
1087 TiffWriter::attach_color_mode(&mut arr, mode);
1088 if let NDDataBuffer::U8(v) = &mut arr.data {
1089 for y in 0..h {
1090 for x in 0..w {
1091 for c in 0..3 {
1092 let idx = match mode {
1093 NDColorMode::RGB1 => c + x * 3 + y * w * 3,
1094 NDColorMode::RGB2 => x + c * w + y * w * 3,
1095 NDColorMode::RGB3 => x + y * w + c * w * h,
1096 _ => unreachable!(),
1097 };
1098 v[idx] = pixel(x, y, c);
1099 }
1100 }
1101 }
1102 }
1103 arr
1104 }
1105
1106 fn pixel(x: usize, y: usize, c: usize) -> u8 {
1107 ((x * 7 + y * 13 + c * 61) % 256) as u8
1108 }
1109
1110 fn mono_array(w: usize, h: usize) -> NDArray {
1111 let mut arr = NDArray::new(
1112 vec![NDDimension::new(w), NDDimension::new(h)],
1113 NDDataType::UInt8,
1114 );
1115 if let NDDataBuffer::U8(v) = &mut arr.data {
1116 v.iter_mut().enumerate().for_each(|(i, x)| *x = i as u8);
1117 }
1118 arr
1119 }
1120
1121 fn strips_in_index_order(path: &Path) -> Vec<u8> {
1126 let bytes = std::fs::read(path).unwrap();
1127 let mut decoder = Decoder::new(std::fs::File::open(path).unwrap()).unwrap();
1128 let offsets = decoder.get_tag_u32_vec(Tag::StripOffsets).unwrap();
1129 let counts = decoder.get_tag_u32_vec(Tag::StripByteCounts).unwrap();
1130 assert_eq!(offsets.len(), counts.len());
1131 let mut out = Vec::new();
1132 for (off, count) in offsets.iter().zip(&counts) {
1133 out.extend_from_slice(&bytes[*off as usize..(*off + *count) as usize]);
1134 }
1135 out
1136 }
1137
1138 fn write_tiff(prefix: &str, array: &NDArray) -> PathBuf {
1139 let path = temp_path(prefix);
1140 let mut writer = TiffWriter::new();
1141 writer.open_file(&path, NDFileMode::Single, array).unwrap();
1142 writer.write_file(array).unwrap();
1143 writer.close_file().unwrap();
1144 path
1145 }
1146
1147 #[test]
1148 fn test_r8_67_every_image_carries_planarconfiguration() {
1149 for (name, array, expected) in [
1153 ("planar_mono", mono_array(4, 3), PLANARCONFIG_CONTIG),
1154 (
1155 "planar_rgb1",
1156 rgb_array(NDColorMode::RGB1, 4, 3),
1157 PLANARCONFIG_CONTIG,
1158 ),
1159 (
1160 "planar_rgb2",
1161 rgb_array(NDColorMode::RGB2, 4, 3),
1162 PLANARCONFIG_SEPARATE,
1163 ),
1164 (
1165 "planar_rgb3",
1166 rgb_array(NDColorMode::RGB3, 4, 3),
1167 PLANARCONFIG_SEPARATE,
1168 ),
1169 ] {
1170 let path = write_tiff(name, &array);
1171 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
1172 assert_eq!(
1173 decoder.get_tag_u32(Tag::PlanarConfiguration).unwrap() as u16,
1174 expected,
1175 "{name}: PlanarConfiguration (tag 284) must be on disk with C's value"
1176 );
1177 std::fs::remove_file(&path).ok();
1178 }
1179 }
1180
1181 #[test]
1182 fn test_r8_67_rgb2_and_rgb3_are_written_as_three_separate_planes() {
1183 let (w, h) = (4usize, 3usize);
1187 let mut expected_planes: Vec<u8> = Vec::new();
1188 for c in 0..3 {
1189 for y in 0..h {
1190 for x in 0..w {
1191 expected_planes.push(pixel(x, y, c));
1192 }
1193 }
1194 }
1195
1196 for (name, mode) in [
1197 ("sep_rgb2", NDColorMode::RGB2),
1198 ("sep_rgb3", NDColorMode::RGB3),
1199 ] {
1200 let path = write_tiff(name, &rgb_array(mode, w, h));
1201 assert_eq!(
1202 strips_in_index_order(&path),
1203 expected_planes,
1204 "{mode:?}: on-disk strips must be the R, G and B planes in order"
1205 );
1206 std::fs::remove_file(&path).ok();
1207 }
1208
1209 let mut expected_chunky: Vec<u8> = Vec::new();
1211 for y in 0..h {
1212 for x in 0..w {
1213 for c in 0..3 {
1214 expected_chunky.push(pixel(x, y, c));
1215 }
1216 }
1217 }
1218 let path = write_tiff("sep_rgb1", &rgb_array(NDColorMode::RGB1, w, h));
1219 assert_eq!(strips_in_index_order(&path), expected_chunky);
1220 std::fs::remove_file(&path).ok();
1221 }
1222
1223 #[test]
1224 fn test_r8_67_rows_per_strip_matches_c() {
1225 let (w, h) = (4usize, 3usize);
1230 for (name, array, rows, strips) in [
1231 ("rps_mono", mono_array(w, h), h as u32, 1),
1232 ("rps_rgb1", rgb_array(NDColorMode::RGB1, w, h), h as u32, 1),
1233 ("rps_rgb2", rgb_array(NDColorMode::RGB2, w, h), 1, 3 * h),
1234 ("rps_rgb3", rgb_array(NDColorMode::RGB3, w, h), h as u32, 3),
1235 ] {
1236 let path = write_tiff(name, &array);
1237 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
1238 assert_eq!(
1239 decoder.get_tag_u32(Tag::RowsPerStrip).unwrap(),
1240 rows,
1241 "{name}: RowsPerStrip"
1242 );
1243 assert_eq!(
1244 decoder.get_tag_u32_vec(Tag::StripOffsets).unwrap().len(),
1245 strips,
1246 "{name}: strip count"
1247 );
1248 std::fs::remove_file(&path).ok();
1249 }
1250 }
1251
1252 #[test]
1253 fn test_r8_67_no_resolution_tags() {
1254 let path = write_tiff("no_resolution", &rgb_array(NDColorMode::RGB1, 4, 3));
1258 let mut decoder = Decoder::new(std::fs::File::open(&path).unwrap()).unwrap();
1259 for tag in [Tag::XResolution, Tag::YResolution, Tag::ResolutionUnit] {
1260 assert!(
1261 decoder.get_tag(tag).is_err(),
1262 "{tag:?} must not be written — C sets no resolution tags"
1263 );
1264 }
1265 std::fs::remove_file(&path).ok();
1266 }
1267
1268 #[test]
1269 fn test_r8_67_planar_file_reads_back_as_rgb3() {
1270 let (w, h) = (4usize, 3usize);
1275 let path = write_tiff("planar_read", &rgb_array(NDColorMode::RGB2, w, h));
1276
1277 let mut writer = TiffWriter::new();
1278 writer
1279 .open_file(
1280 &path,
1281 NDFileMode::Single,
1282 &NDArray::new(vec![], NDDataType::UInt8),
1283 )
1284 .unwrap();
1285 let read_back = writer.read_file().unwrap();
1286 writer.close_file().unwrap();
1287
1288 assert_eq!(
1289 read_back.dims.iter().map(|d| d.size).collect::<Vec<_>>(),
1290 vec![w, h, 3]
1291 );
1292 assert_eq!(
1293 read_back
1294 .attributes
1295 .get("ColorMode")
1296 .unwrap()
1297 .value
1298 .as_i64(),
1299 Some(NDColorMode::RGB3 as i64)
1300 );
1301 assert_eq!(
1302 read_back.data.as_u8_slice(),
1303 rgb_array(NDColorMode::RGB3, w, h).data.as_u8_slice(),
1304 "the RGB2 pixels must come back as the same image in RGB3 layout"
1305 );
1306 std::fs::remove_file(&path).ok();
1307 }
1308
1309 #[test]
1310 fn test_single_mode_requires_auto_save_for_automatic_write() {
1311 let path = temp_path("tiff_autosave_single");
1312 let full_name = path.to_string_lossy().to_string();
1313 let file_path = path.parent().unwrap().to_str().unwrap().to_string();
1314 let file_name = path.file_name().unwrap().to_str().unwrap().to_string();
1315
1316 let mut proc = TiffFileProcessor::new();
1317 proc.ctrl.file_base.file_path = file_path.clone() + "/";
1318 proc.ctrl.file_base.file_name = file_name;
1319 proc.ctrl.file_base.file_template = "%s%s".into();
1320 proc.ctrl.file_base.set_mode(NDFileMode::Single);
1321
1322 let mut arr = NDArray::new(
1323 vec![NDDimension::new(4), NDDimension::new(4)],
1324 NDDataType::UInt8,
1325 );
1326 if let NDDataBuffer::U8(v) = &mut arr.data {
1327 for (i, item) in v.iter_mut().enumerate() {
1328 *item = i as u8;
1329 }
1330 }
1331
1332 proc.ctrl.auto_save = false;
1333 let _ = proc.process_array(&arr, &NDArrayPool::new(1024));
1334 assert!(!std::path::Path::new(&full_name).exists());
1335
1336 proc.ctrl.auto_save = true;
1337 let _ = proc.process_array(&arr, &NDArrayPool::new(1024));
1338 assert!(std::path::Path::new(&full_name).exists());
1339
1340 std::fs::remove_file(&path).ok();
1341 }
1342}