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