xisf_rs/image/
resolution.rs

1use error_stack::{Result, report, Report, ResultExt};
2use libxml::readonly::RoNode;
3
4use crate::error::{ParseNodeError, ParseNodeErrorKind::{self, *}};
5
6fn report(kind: ParseNodeErrorKind) -> Report<ParseNodeError> {
7    report!(context(kind))
8}
9const fn context(kind: ParseNodeErrorKind) -> ParseNodeError {
10    ParseNodeError::new("Resolution", kind)
11}
12
13/// The pixel density of the image, in pixels per inch or pixels per centimeter
14///
15/// Although it would be unusual for horizontal and vertical pixel density to differ,
16/// as this would result in a stretched image for most media, they are specified separately.
17#[derive(Clone, Debug)]
18pub enum Resolution {
19    /// Pixel density is measured in pixels per inch
20    Inch {
21        /// Horizontal pixel density, in pixels per inch
22        ///
23        /// Recall that XISF images are stored in row-major order,
24        /// so this should be applied to dimension 1 (zero-indexed)
25        horizontal: f32,
26        /// Vertical pixel density, in pixels per inch
27        ///
28        /// Recall that XISF images are stored in row-major order,
29        /// so this should be applied to dimension 0 (zero-indexed)
30        vertical: f32,
31    },
32    /// Pixel density is measured in pixels per centimeter
33    Cm {
34        /// Horizontal pixel density, in pixels per centimeter
35        ///
36        /// Recall that XISF images are stored in row-major order,
37        /// so this should be applied to dimension 1 (zero-indexed)
38        horizontal: f32,
39        /// Vertical pixel density, in pixels per centimeter
40        ///
41        /// Recall that XISF images are stored in row-major order,
42        /// so this should be applied to dimension 0 (zero-indexed)
43        vertical: f32,
44    },
45}
46/// 72 PPI
47impl Default for Resolution {
48    fn default() -> Self {
49        Self::Inch {
50            horizontal: 72.0,
51            vertical: 72.0,
52        }
53    }
54}
55impl Resolution {
56    pub(crate) fn parse_node(node: RoNode) -> Result<Self, ParseNodeError> {
57        let _span_guard = tracing::debug_span!("Resolution");
58        let mut attrs = node.get_attributes();
59        let children = node.get_child_nodes();
60
61
62        let horizontal = attrs.remove("horizontal")
63            .ok_or(report(MissingAttr))?
64            .trim()
65            .parse::<f32>()
66            .change_context(context(InvalidAttr))
67            .attach_printable("Invalid horizontal attribute: failed to parse as f32")?;
68
69
70        let vertical = attrs.remove("vertical")
71            .ok_or(report(MissingAttr))?
72            .trim()
73            .parse::<f32>()
74            .change_context(context(InvalidAttr))
75            .attach_printable("Invalid horizontal attribute: failed to parse as f32")?;
76
77        let unit = attrs.remove("unit");
78
79        for remaining in attrs.into_iter() {
80            tracing::warn!("Ignoring unrecognized attribute {}=\"{}\"", remaining.0, remaining.1);
81        }
82        for child in children {
83            tracing::warn!("Ignoring unrecognized child node <{}>", child.get_name());
84        }
85
86        match unit.as_deref() {
87            Some("inch") => Ok(Resolution::Inch { horizontal, vertical }),
88            Some("cm") | None => Ok(Resolution::Cm { horizontal, vertical }),
89            Some(bad) => Err(report(InvalidAttr)).attach_printable(format!("Invalid unit attribute: expected one of [inch, cm]; found {bad}")),
90        }
91    }
92}