1use std::{
4 any::TypeId,
5 io::Read,
6 fmt,
7 str::FromStr, ops::Deref, collections::HashMap,
8};
9
10use byteorder::{ReadBytesExt, LE, BE};
11use error_stack::{Report, report, Result, ResultExt};
12use libxml::{readonly::RoNode, xpath::Context as XpathContext};
13use ndarray::{ArrayD, IxDyn};
14use num_complex::Complex;
15use ordered_multimap::ListOrderedMultimap;
16use parse_int::parse as parse_auto_radix;
17use strum::{Display, EnumString, EnumVariantNames, VariantNames};
18use uuid::Uuid;
19use crate::{
20 data_block::{ByteOrder, Context, DataBlock},
21 error::{ReadDataBlockError, ParseValueError, ReadFitsKeyError, ParseNodeErrorKind::*, ReadPropertyError},
22 is_valid_id,
23 MaybeReference,
24 ParseNodeError,
25 property::{Property, PropertyContent, FromProperty},
26 ReadOptions,
27};
28
29mod image_data;
30pub use image_data::*;
31
32mod fits_keyword;
33pub use fits_keyword::*;
34
35mod icc_profile;
36pub use icc_profile::*;
37
38mod rgb_working_space;
39pub use rgb_working_space::*;
40
41mod display_function;
42pub use display_function::*;
43
44mod color_filter_array;
45pub use color_filter_array::*;
46
47mod resolution;
48pub use resolution::*;
49
50mod thumbnail;
51pub use thumbnail::*;
52
53#[derive(Clone, Debug)]
58pub struct ImageBase {
59 data_block: DataBlock,
60 geometry: Vec<usize>,
61 sample_format: SampleFormat,
62
63 image_type: Option<ImageType>,
64 pixel_storage: PixelStorage,
65 color_space: ColorSpace,
66 offset: f64,
67 orientation: Option<Orientation>,
68 id: Option<String>,
69 uuid: Option<Uuid>,
70
71 properties: HashMap<String, PropertyContent>,
72 fits_header: ListOrderedMultimap<String, FitsKeyContent>,
73 icc_profile: Option<ICCProfile>,
74 rgb_working_space: Option<RGBWorkingSpace>,
75 display_function: Option<DisplayFunction>,
76 resolution: Option<Resolution>,
77}
78impl ImageBase {
79 #[inline]
83 pub fn num_dimensions(&self) -> usize {
84 self.geometry.len() - 1
85 }
86
87 #[inline]
92 pub fn dimensions(&self) -> &[usize] {
93 &self.geometry[1..]
94 }
95
96 #[inline]
98 pub fn num_channels(&self) -> usize {
99 self.geometry[0]
100 }
101 #[inline]
104 pub fn num_color_channels(&self) -> usize {
105 self.color_space.num_channels()
106 }
107 #[inline]
109 pub fn num_alpha_channels(&self) -> usize {
110 self.num_channels() - self.color_space.num_channels()
111 }
112
113 #[inline]
115 pub fn sample_format(&self) -> SampleFormat {
116 self.sample_format
117 }
118
119 #[inline]
121 pub fn image_type(&self) -> Option<ImageType> {
122 self.image_type
123 }
124
125 #[inline]
127 pub fn pixel_layout(&self) -> PixelStorage {
128 self.pixel_storage
129 }
130
131 #[inline]
133 pub fn color_space(&self) -> ColorSpace {
134 self.color_space
135 }
136
137 #[inline]
142 pub fn offset(&self) -> f64 {
143 self.offset
144 }
145
146 #[inline]
148 pub fn orientation(&self) -> Option<Orientation> {
149 self.orientation
150 }
151
152 #[inline]
158 pub fn id(&self) -> Option<&str> {
159 self.id.as_deref()
160 }
161
162 #[inline]
164 pub fn uuid(&self) -> Option<Uuid> {
165 self.uuid
166 }
167
168 pub fn read_data(&self, ctx: &Context) -> Result<DynImageData, ReadDataBlockError> {
171 self.data_block.verify_checksum(ctx)?;
172 let reader = &mut *self.data_block.decompressed_bytes(ctx)?;
173
174 macro_rules! read_real {
175 ($func:ident) => {
176 self.read_data_impl(reader,
177 ReadBytesExt::$func::<LE>,
178 ReadBytesExt::$func::<BE>
179 ).map(|buf| buf.into_dyn_img(self.pixel_storage))
180 }
181 }
182 macro_rules! read_complex {
183 ($func:ident, $t:ty) => {
184 {
185 let mut buf;
186 match self.pixel_storage {
187 PixelStorage::Planar => buf = ArrayD::<Complex<$t>>::zeros(IxDyn(&self.geometry[..])),
188 PixelStorage::Normal => {
189 let mut geometry = self.geometry.clone();
190 geometry.rotate_left(1);
191 buf = ArrayD::<Complex<$t>>::zeros(IxDyn(&geometry[..]));
192 },
193 }
194 let buf_slice = buf.as_slice_mut().unwrap();
198 let bytemuck_slice: &mut [$t] = bytemuck::cast_slice_mut(buf_slice);
199
200 match self.data_block.byte_order {
201 ByteOrder::Big => reader.$func::<BE>(bytemuck_slice),
202 ByteOrder::Little => reader.$func::<LE>(bytemuck_slice),
203 }.change_context(ReadDataBlockError::IoError)?;
204 Ok(buf.into_dyn_img(self.pixel_storage))
205 }
206 }
207 }
208
209 match self.sample_format {
210 SampleFormat::UInt8 => self.read_data_impl(reader,
211 Read::read_exact,
212 Read::read_exact
213 ).map(|buf| buf.into_dyn_img(self.pixel_storage)),
214 SampleFormat::UInt16 => read_real!(read_u16_into),
215 SampleFormat::UInt32 => read_real!(read_u32_into),
216 SampleFormat::UInt64 => read_real!(read_u64_into),
217 SampleFormat::Float32 => read_real!(read_f32_into),
218 SampleFormat::Float64 => read_real!(read_f64_into),
219 SampleFormat::Complex32 => read_complex!(read_f32_into, f32),
220 SampleFormat::Complex64 => read_complex!(read_f64_into, f64),
221 }
222 }
223
224 fn read_data_impl<'a, T, F1, F2>(&self, reader: &'a mut dyn Read, read_le: F1, read_be: F2) -> Result<ArrayD<T>, ReadDataBlockError>
228 where F1: Fn(&'a mut dyn Read, &mut [T]) -> std::io::Result<()>,
229 F2: Fn(&'a mut dyn Read, &mut [T]) -> std::io::Result<()>,
230 T: Clone + num_traits::Zero {
231 let mut buf;
232 match self.pixel_storage {
233 PixelStorage::Planar => buf = ArrayD::<T>::zeros(IxDyn(&self.geometry[..])),
234 PixelStorage::Normal => {
235 let mut geometry = self.geometry.clone();
236 geometry.rotate_left(1);
237 buf = ArrayD::<T>::zeros(IxDyn(&geometry[..]));
238 },
239 }
240 let buf_slice = buf.as_slice_mut().unwrap();
244 match self.data_block.byte_order {
245 ByteOrder::Big => read_be(reader, buf_slice),
246 ByteOrder::Little => read_le(reader, buf_slice),
247 }.change_context(ReadDataBlockError::IoError)?;
248 Ok(buf)
249 }
250
251 pub fn has_property(&self, id: impl AsRef<str>) -> bool {
253 self.properties.contains_key(id.as_ref())
254 }
255
256 pub fn parse_property<T: FromProperty>(&self, id: impl AsRef<str>, ctx: &Context) -> Result<T, ReadPropertyError> {
260 let content = self.properties.get(id.as_ref())
261 .ok_or(report!(ReadPropertyError::NotFound))?;
262 T::from_property(&content, ctx)
263 .change_context(ReadPropertyError::InvalidFormat)
264 }
265 pub fn raw_property(&self, id: impl AsRef<str>) -> Option<&PropertyContent> {
267 self.properties.get(id.as_ref())
268 }
269 pub fn all_raw_properties(&self) -> impl Iterator<Item = (&String, &PropertyContent)> {
272 self.properties.iter()
273 }
274
275 pub fn has_fits_key(&self, name: impl AsRef<str>) -> bool {
277 self.fits_header.get(name.as_ref()).is_some()
278 }
279 pub fn parse_fits_key<T: FromFitsKey>(&self, name: impl AsRef<str>) -> Result<T, ReadFitsKeyError> {
283 let content = self.fits_header.get(name.as_ref())
284 .ok_or(report!(ReadFitsKeyError::NotFound))?;
285 T::from_fits_key(content)
286 .change_context(ReadFitsKeyError::InvalidFormat)
287 }
288 pub fn parse_fits_keys<T: FromFitsKey>(&self, name: impl AsRef<str>) -> impl Iterator<Item = Result<T, ParseValueError>> + '_ {
292 self.fits_header.get_all(name.as_ref())
293 .map(|content| T::from_fits_key(content))
294 }
295 pub fn raw_fits_key(&self, name: impl AsRef<str>) -> Option<&FitsKeyContent> {
298 self.fits_header.get(name.as_ref())
299 }
300 pub fn raw_fits_keys(&self, name: impl AsRef<str>) -> impl Iterator<Item = &FitsKeyContent> {
304 self.fits_header.get_all(name.as_ref())
305 }
306 pub fn all_raw_fits_keys(&self) -> impl Iterator<Item = (&String, &FitsKeyContent)> {
309 self.fits_header.iter()
310 }
311
312 pub fn icc_profile(&self) -> Option<&ICCProfile> {
316 self.icc_profile.as_ref()
317 }
318
319 pub fn rgb_working_space(&self) -> Option<&RGBWorkingSpace> {
323 self.rgb_working_space.as_ref()
324 }
325
326 pub fn display_function(&self) -> Option<&DisplayFunction> {
331 self.display_function.as_ref()
332 }
333
334 pub fn pixel_density(&self) -> Option<&Resolution> {
338 self.resolution.as_ref()
339 }
340}
341
342#[derive(Clone, Debug)]
346pub struct Image {
347 base: ImageBase,
348 bounds: Option<SampleBounds>,
349 color_filter_array: Option<CFA>,
350 thumbnail: Option<Thumbnail>,
351}
352
353
354fn parse_image<T: ParseImage + 'static>(node: RoNode, xpath: &XpathContext, opts: &ReadOptions) -> Result<Image, ParseNodeError> {
355 let is_thumbnail = TypeId::of::<T>() == TypeId::of::<Thumbnail>();
356
357 let context = |kind| -> ParseNodeError {
358 ParseNodeError::new(T::TAG_NAME, kind)
359 };
360 let report = |kind| -> Report<ParseNodeError> {
361 report!(ParseNodeError::new(T::TAG_NAME, kind))
362 };
363
364 let mut attrs = node.get_attributes();
369
370 let data_block = DataBlock::parse_node(node, T::TAG_NAME, &mut attrs)?
371 .ok_or(context(MissingAttr))
372 .attach_printable("Missing location attribute: Image elements must have a data block")?;
373
374 let mut geometry: Vec<usize> = vec![];
375 if let Some(dims) = attrs.remove("geometry") {
376 for i in dims.split(":") {
377 let dim = parse_auto_radix::<usize>(i.trim())
378 .change_context(context(InvalidAttr))
379 .attach_printable("Invalid geometry attribute: failed to parse dimension/channel count")
380 .attach_printable_lazy(|| format!("Expected pattern \"{{dim_1}}:...:{{dim_N}}:{{channel_count}}\" (for N>=1 and all values > 0), found \"{i}\""))?;
381 if dim > 0 {
382 geometry.push(dim);
383 } else {
384 return Err(report(InvalidAttr))
385 .attach_printable("Invalid geometry attribute: dimensions and channel count all must be nonzero")
386 }
387 }
388 if geometry.len() < 2 {
389 return Err(report(InvalidAttr))
390 .attach_printable("Invalid geometry attribute: must have at least one dimension and one channel")
391 } else {
392 geometry = geometry.into_iter().rev().collect();
394 }
395 } else {
396 return Err(report(MissingAttr)).attach_printable("Missing geometry attribute")
397 }
398
399 let sample_format = attrs.remove("sampleFormat")
400 .ok_or(report(MissingAttr))
401 .attach_printable("Missing sampleFormat attribute")
402 .and_then(|val| {
403 val.parse::<SampleFormat>()
404 .change_context(context(InvalidAttr))
405 .attach_printable_lazy(||
406 format!("Invalid sampleFormat attribute: expected one of {:?}, found {val}", SampleFormat::VARIANTS))
407 })?;
408
409 let bounds = if let Some(val) = attrs.remove("bounds") {
410 let (low, high) = val.split_once(":")
411 .ok_or(report(InvalidAttr))
412 .attach_printable_lazy(|| "Invalid bounds attribute: expected pattern \"low:high\", found \"{val}\"")?;
413
414 Some(SampleBounds {
415 low: low.trim().parse::<f64>()
416 .change_context(context(InvalidAttr))
417 .attach_printable("Invalid bounds attribute: failed to parse lower bound")?,
418 high: high.trim().parse::<f64>()
419 .change_context(context(InvalidAttr))
420 .attach_printable("Invalid bounds attribute: failed to parse upper bound")?
421 })
422 } else if sample_format.requires_bounds() {
423 return Err(report(MissingAttr))
424 .attach_printable(format!("Missing bounds attribute: required when using using {sample_format} sample format"));
425 } else {
426 None
427 };
428
429 let image_type = if let Some(val) = attrs.remove("imageType") {
430 Some(val.parse::<ImageType>()
431 .change_context(context(InvalidAttr))
432 .attach_printable_lazy(||
433 format!("Invalid imageType attribute: expected one of {:?}, found {val}", ImageType::VARIANTS))?)
434 } else {
435 None
436 };
437
438 let pixel_storage = if let Some(val) = attrs.remove("pixelStorage") {
439 val.parse::<PixelStorage>()
440 .change_context(context(InvalidAttr))
441 .attach_printable_lazy(||
442 format!("Invalid pixelStorage attribute: expected one of {:?}, found {val}", PixelStorage::VARIANTS)
443 )?
444 } else {
445 Default::default()
446 };
447
448 let color_space = if let Some(val) = attrs.remove("colorSpace") {
449 val.parse::<ColorSpace>()
450 .change_context(context(InvalidAttr))
451 .attach_printable_lazy(||
452 format!("Invalid colorSpace attribute: expected one of {:?}, found {val}", ColorSpace::VARIANTS)
453 )?
454 } else {
455 Default::default()
456 };
457
458 let offset = if let Some(val) = attrs.remove("offset") {
459 let maybe_negative = val.parse::<f64>()
460 .change_context(context(InvalidAttr))
461 .attach_printable("Invalid offset attribute")?;
462 if maybe_negative < 0.0 {
463 return Err(report!(context(InvalidAttr))).attach_printable("Invalid offset attribute: must be zero or greater")
464 } else {
465 maybe_negative
466 }
467 } else {
468 0.0
469 };
470
471 let orientation = if let Some(val) = attrs.remove("orientation") {
472 Some(val.parse::<Orientation>()
473 .change_context(context(InvalidAttr))
474 .attach_printable("Invalid orientation attribute")?)
475 } else {
476 None
477 };
478
479 let id = attrs.remove("id");
480 if let Some(id) = &id {
481 if !is_valid_id(id) {
482 return Err(report(InvalidAttr)).attach_printable(
483 format!("Invalid id attribute: must match regex [_a-zA-Z][_a-zA-Z0-9]*, found \"{id}\"")
484 )
485 }
486 }
487
488 let uuid = if let Some(val) = attrs.remove("uuid") {
489 Some(val.parse::<Uuid>()
490 .change_context(context(InvalidAttr))
491 .attach_printable("Invalid uuid attribute")?)
492 } else {
493 None
494 };
495
496 for remaining in attrs.into_iter() {
497 tracing::warn!("Ignoring unrecognized attribute {}=\"{}\"", remaining.0, remaining.1);
498 }
499
500 let mut properties = HashMap::new();
501 let mut fits_header = ListOrderedMultimap::new();
502 let mut icc_profile = None;
503 let mut rgb_working_space = None;
504 let mut display_function = None;
505 let mut color_filter_array = None;
506 let mut resolution = None;
507 let mut thumbnail = None;
508
509 for mut child in node.get_child_nodes() {
511 child = child.follow_reference(xpath).change_context(context(InvalidReference))?;
512
513 macro_rules! parse_optional {
514 ($t:ty, $opt_out:ident) => {
515 {
516 let parsed = <$t>::parse_node(child)?;
517 if $opt_out.replace(parsed).is_some() {
518 tracing::warn!(concat!("Duplicate ", stringify!($t), " element found -- discarding the previous one"));
519 }
520 }
521 };
522 ($t:ty, $opt_out:ident, full) => {
523 {
524 let parsed = <$t>::parse_node(child, xpath, opts)?;
525 if $opt_out.replace(parsed).is_some() {
526 tracing::warn!(concat!("Duplicate ", stringify!($t), " element found -- discarding the previous one"));
527 }
528 }
529 };
530 }
531
532 match child.get_name().as_str() {
533 "Property" => {
534 let prop = Property::parse_node(child)?;
535 if properties.insert(prop.id.clone(), prop.content).is_some() {
536 tracing::warn!("Duplicate property found with id {} -- discarding the previous one", prop.id);
537 }
538 }
539 "FITSKeyword" if opts.import_fits_keywords => {
540 let key = FitsKeyword::parse_node(child)?;
541 fits_header.append(key.name, key.content);
542 },
544 "ICCProfile" => parse_optional!(ICCProfile, icc_profile),
545 "RGBWorkingSpace" => parse_optional!(RGBWorkingSpace, rgb_working_space),
546 "DisplayFunction" => parse_optional!(DisplayFunction, display_function),
547 "ColorFilterArray" if !is_thumbnail => parse_optional!(CFA, color_filter_array),
548 "Resolution" => parse_optional!(Resolution, resolution),
549 "Thumbnail" if !is_thumbnail => parse_optional!(Thumbnail, thumbnail, full),
550 bad => tracing::warn!("Ignoring unrecognized child node <{}>", bad),
551 }
552 }
553
554 if geometry[0] < color_space.num_channels() {
559 return Err(report(InvalidAttr))
560 .attach_printable(format!(
561 "Insufficient color channels: {color_space} color space requires {}; only found {}",
562 color_space.num_channels(),
563 geometry[0]
564 ));
565 }
566
567 if color_filter_array.is_some() && geometry.len() - 1 != 2 {
568 tracing::warn!("ColorFilterArray element only has a defined meaning for 2D images; found one on a {}D image", geometry.len() - 1);
569 }
570
571 Ok(Image {
572 base: ImageBase {
573 data_block,
574 geometry,
575 sample_format,
576
577 image_type,
578 pixel_storage,
579 color_space,
580 offset,
581 orientation,
582 id,
583 uuid,
584
585 properties,
586 fits_header,
587 icc_profile,
588 rgb_working_space,
589 display_function,
590 resolution,
591 },
592 bounds,
593 color_filter_array,
594 thumbnail,
595 })
596}
597
598pub(crate) trait ParseImage: Sized {
599 const TAG_NAME: &'static str;
600}
601
602impl ParseImage for Image {
603 const TAG_NAME: &'static str = "Image";
604}
605
606impl Deref for Image {
607 type Target = ImageBase;
608
609 fn deref(&self) -> &Self::Target {
610 &self.base
611 }
612}
613
614impl Image {
615 pub(crate) fn parse_node(node: RoNode, xpath: &XpathContext, opts: &ReadOptions) -> Result<Self, ParseNodeError> {
616 parse_image::<Self>(node, xpath, opts)
617 }
618
619 pub fn bounds(&self) -> Option<SampleBounds> {
623 self.bounds
624 }
625
626 pub fn cfa(&self) -> Option<&CFA> {
628 self.color_filter_array.as_ref()
629 }
630
631 pub fn thumbnail(&self) -> Option<&Thumbnail> {
633 self.thumbnail.as_ref()
634 }
635}
636
637#[derive(Clone, Copy, Debug, Display, EnumString, EnumVariantNames, PartialEq)]
641pub enum SampleFormat {
642 UInt8,
644 UInt16,
646 UInt32,
648 UInt64,
650 Float32,
652 Float64,
654 Complex32,
656 Complex64,
658}
659impl SampleFormat {
660 pub(crate) fn requires_bounds(&self) -> bool {
661 match self {
662 Self::Float32 | Self::Float64 => true,
663 _ => false,
664 }
665 }
666}
667
668#[derive(Clone, Copy, Debug, PartialEq)]
671pub struct SampleBounds {
672 pub low: f64,
674 pub high: f64,
676}
677
678#[derive(Clone, Copy, Debug, Display, EnumString, EnumVariantNames, PartialEq)]
680pub enum ImageType {
681 Bias,
684 Dark,
687 Flat,
690 Light,
692 MasterBias,
694 MasterDark,
696 MasterFlat,
698 MasterLight,
700 DefectMap,
703 RejectionMapHigh,
711 RejectionMapLow,
719 BinaryRejectionMapHigh,
723 BinaryRejectionMapLow,
727 SlopeMap,
732 WeightMap,
736}
737
738#[derive(Clone, Copy, Debug, Display, Default, EnumString, EnumVariantNames, PartialEq)]
743pub enum PixelStorage {
744 #[default]
748 Planar,
749 Normal,
754}
755
756#[derive(Clone, Copy, Debug, Display, Default, EnumString, EnumVariantNames, PartialEq)]
758pub enum ColorSpace {
759 #[default]
761 Gray,
762 RGB,
766 CIELab,
768}
769impl ColorSpace {
770 pub fn num_channels(&self) -> usize {
772 match self {
773 Self::Gray => 1,
774 Self::RGB | Self::CIELab => 3,
775 }
776 }
777}
778
779#[derive(Clone, Copy, Debug, Default, PartialEq)]
781pub struct Orientation {
782 pub rotation: Rotation,
786 pub hflip: bool,
792}
793impl fmt::Display for Orientation {
794 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
795 if self.hflip && self.rotation == Rotation::None {
797 f.write_str("flip")
798 }
799 else {
800 f.write_fmt(format_args!("{}{}",
801 self.rotation,
802 if self.hflip { ";flip" } else { "" }
803 ))
804 }
805 }
806}
807impl FromStr for Orientation {
808 type Err = Report<ParseValueError>;
809 fn from_str(s: &str) -> Result<Self, ParseValueError> {
810 match s {
811 "0" => Ok(Self { rotation: Rotation::None, hflip: false }),
812 "flip" => Ok(Self { rotation: Rotation::None, hflip: true }),
813 "90" => Ok(Self { rotation: Rotation::Ccw90, hflip: false }),
814 "90;flip" => Ok(Self { rotation: Rotation::Ccw90, hflip: true }),
815 "-90" => Ok(Self { rotation: Rotation::Cw90, hflip: false }),
816 "-90;flip" => Ok(Self { rotation: Rotation::Cw90, hflip: true }),
817 "180" => Ok(Self { rotation: Rotation::_180, hflip: false }),
818 "180;flip" => Ok(Self { rotation: Rotation::_180, hflip: true }),
819 bad => Err(report!(ParseValueError("Orientation")))
820 .attach_printable(format!("Expected one of [0, flip, 90, 90;flip, -90, -90;flip, 180, 180;flip], found {bad}",))
821 }
822 }
823}
824
825#[derive(Clone, Copy, Debug, Default, PartialEq)]
827pub enum Rotation {
828 #[default]
830 None,
831 Cw90,
833 Ccw90,
835 _180,
837}
838impl fmt::Display for Rotation {
839 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
840 f.write_str(match self {
841 Rotation::None => "0",
842 Rotation::Cw90 => "-90",
843 Rotation::Ccw90 => "90",
844 Rotation::_180 => "180",
845 })
846 }
847}