xisf_rs/image/
display_function.rs

1use std::collections::HashMap;
2
3use error_stack::{Report, report, Result, ResultExt};
4use libxml::readonly::RoNode;
5
6use crate::error::{ParseNodeError, ParseNodeErrorKind::{self, *}};
7
8/// A single-channel screen transfer function
9#[derive(Clone, Debug, PartialEq)]
10pub struct STF {
11    midtones_balance: f64,
12    shadows_clip: f64,
13    highlights_clip: f64,
14    shadows_expansion: f64,
15    highlights_expansion: f64,
16
17    clip_delta: f64,
18    expand_delta: f64,
19}
20/// [Identity transformation](https://pixinsight.com/doc/docs/XISF-1.0-spec/XISF-1.0-spec.html#__equation_23__)
21impl Default for STF {
22    fn default() -> Self {
23        Self {
24            midtones_balance: 0.5,
25            shadows_clip: 0.0,
26            highlights_clip: 1.0,
27            shadows_expansion: 0.0,
28            highlights_expansion: 1.0,
29            clip_delta: 1.0,
30            expand_delta: 1.0,
31        }
32    }
33}
34impl STF {
35    /// Parameters:
36    /// - `m`: midtones balance, clamped to the range [0, 1]
37    /// - `s`: shadows clipping point, clamped to the range [0, 1]
38    /// - `h`: highlights clipping point, clamped to the range [0, 1]
39    /// - `l`: shadows dynamic range expansion, clamped to 0 or below
40    /// - `r`: highlights dynamic range expansion, clamped to 1 or above
41    ///
42    /// If `s` is greater than `h`, the two are swapped
43    pub fn new(m: f64, mut s: f64, mut h: f64, l: f64, r: f64) -> Self {
44        if s > h {
45            std::mem::swap(&mut s, &mut h);
46        }
47        let shadows_clip = s.clamp(0.0, 1.0);
48        let highlights_clip = h.clamp(0.0, 1.0);
49        let shadows_expansion = l.min(0.0);
50        let highlights_expansion = r.max(1.0);
51        Self {
52            midtones_balance: m.clamp(0.0, 1.0),
53            shadows_clip,
54            highlights_clip,
55            shadows_expansion,
56            highlights_expansion,
57            clip_delta: highlights_clip - shadows_clip,
58            expand_delta: highlights_expansion - shadows_expansion,
59        }
60    }
61
62    /// Midtones balance parameter
63    #[inline]
64    pub fn mb(&self) -> f64 {
65        self.midtones_balance
66    }
67
68    /// Shadows clipping parameter
69    #[inline]
70    pub fn sc(&self) -> f64 {
71        self.shadows_clip
72    }
73
74    /// Highlights clipping parameter
75    #[inline]
76    pub fn hc(&self) -> f64 {
77        self.highlights_clip
78    }
79
80    /// Shadows expansion parameter
81    #[inline]
82    pub fn se(&self) -> f64 {
83        self.shadows_expansion
84    }
85
86    /// Highlights expansion parameter
87    #[inline]
88    pub fn he(&self) -> f64 {
89        self.highlights_expansion
90    }
91
92    // pub fn make_8bit_lut(&self)
93    // pub fn make_16bit_lut(&self)
94    // pub fn make_20bit_lut(&self)
95    // pub fn make_24bit_lut(&self)
96
97    /// [Midtones transfer function](https://pixinsight.com/doc/docs/XISF-1.0-spec/XISF-1.0-spec.html#__equation_19__)
98    #[inline]
99    fn mtf(&self, x: f64) -> f64 {
100        if x > 0.0 {
101            if x < 1.0 {
102                let m1 = self.mb() - 1.0;
103                m1 * x / ((self.mb() + m1) * x - self.mb())
104            } else {
105                1.0
106            }
107        } else {
108            0.0
109        }
110    }
111
112    /// [Clipping function](https://pixinsight.com/doc/docs/XISF-1.0-spec/XISF-1.0-spec.html#__equation_20__)
113    #[inline]
114    fn clip(&self, x: f64) -> f64 {
115        if x < self.sc() {
116            0.0
117        } else if x > self.hc() {
118            1.0
119        } else if self.clip_delta == 0.0 {
120            self.sc()
121        } else {
122            (x - self.sc()) / self.clip_delta
123        }
124    }
125
126    /// [Expansion function](https://pixinsight.com/doc/docs/XISF-1.0-spec/XISF-1.0-spec.html#__equation_21__)
127    #[inline]
128    fn expand(&self, x: f64) -> f64 {
129        (x - self.se()) / self.expand_delta
130    }
131
132    /// [Display function](https://pixinsight.com/doc/docs/XISF-1.0-spec/XISF-1.0-spec.html#__equation_22__)
133    #[inline]
134    pub fn apply(&self, x: f64) -> f64 {
135        self.expand(self.mtf(self.clip(x)))
136    }
137}
138
139fn report(kind: ParseNodeErrorKind) -> Report<ParseNodeError> {
140    report!(context(kind))
141}
142const fn context(kind: ParseNodeErrorKind) -> ParseNodeError {
143    ParseNodeError::new("DisplayFunction", kind)
144}
145
146/// A multi-channel screen transfer function
147#[derive(Clone, Debug)]
148pub struct DisplayFunction {
149    name: Option<String>,
150    rgbl: [STF; 4],
151}
152/// No name and identity STFs for each channel
153impl Default for DisplayFunction {
154    fn default() -> Self {
155        Self {
156            name: Default::default(),
157            rgbl: Default::default(),
158        }
159    }
160}
161/// Ignores the name, just compares the single-channel STFs
162impl PartialEq for DisplayFunction {
163    fn eq(&self, other: &Self) -> bool {
164        self.rgbl == other.rgbl
165    }
166    fn ne(&self, other: &Self) -> bool {
167        self.rgbl != other.rgbl
168    }
169}
170impl DisplayFunction {
171    pub(crate) fn parse_node(node: RoNode) -> Result<Self, ParseNodeError> {
172        let _span_guard = tracing::debug_span!("DisplayFunction");
173        let mut attrs = node.get_attributes();
174        let children = node.get_child_nodes();
175
176        let name = attrs.remove("name");
177
178        fn parse_rkgbl(attr: &str, attrs: &mut HashMap<String, String>) -> Result<[f64; 4], ParseNodeError> {
179            if let Some(val) = attrs.remove(attr) {
180                match val.split(":").collect::<Vec<_>>().as_slice() {
181                    &[rk, g, b, l] => Ok([
182                        rk.trim().parse::<f64>()
183                            .change_context(context(InvalidAttr))
184                            .attach_printable_lazy(|| format!("Invalid {attr} attribute: failed to parse red/grayscale value"))?,
185                        g.trim().parse::<f64>()
186                            .change_context(context(InvalidAttr))
187                            .attach_printable_lazy(|| format!("Invalid {attr} attribute: failed to parse green value"))?,
188                        b.trim().parse::<f64>()
189                            .change_context(context(InvalidAttr))
190                            .attach_printable_lazy(|| format!("Invalid {attr} attribute: failed to parse blue value"))?,
191                        l.trim().parse::<f64>()
192                            .change_context(context(InvalidAttr))
193                            .attach_printable_lazy(|| format!("Invalid {attr} attribute: failed to parse luminance value"))?,
194                    ]),
195                    _bad => Err(report(InvalidAttr)).attach_printable(format!("Invalid {attr} attribute: expected pattern rk:g:b:l, found {val}")),
196                }
197            } else {
198                Err(report(MissingAttr)).attach_printable(format!("Missing {attr} attribute"))
199            }
200        }
201
202        let m = parse_rkgbl("m", &mut attrs)?;
203        let s = parse_rkgbl("s", &mut attrs)?;
204        let h = parse_rkgbl("h", &mut attrs)?;
205        let l = parse_rkgbl("l", &mut attrs)?;
206        let r = parse_rkgbl("r", &mut attrs)?;
207
208        let rk = STF::new(m[0], s[0], h[0], l[0], r[0]);
209        let g  = STF::new(m[1], s[1], h[1], l[1], r[1]);
210        let b  = STF::new(m[2], s[2], h[2], l[2], r[2]);
211        let l  = STF::new(m[3], s[3], h[3], l[3], r[3]);
212
213        for remaining in attrs.into_iter() {
214            tracing::warn!("Ignoring unrecognized attribute {}=\"{}\"", remaining.0, remaining.1);
215        }
216        for child in children {
217            tracing::warn!("Ignoring unrecognized child node <{}>", child.get_name());
218        }
219
220        Ok(Self {
221            name,
222            rgbl: [rk, g, b, l],
223        })
224    }
225
226    /// A common name used to distinguish this display function from others or describe its behavior.
227    /// May contain any valid Unicode.
228    #[inline]
229    pub fn name(&self) -> Option<&str> {
230        self.name.as_deref()
231    }
232
233    /// Screen transfer functions for the red channel of an RGB image
234    #[inline]
235    pub fn r(&self) -> &STF {
236        &self.rgbl[0]
237    }
238    /// Screen transfer function for the single channel of a grayscale image -- just an alias for [`Self::r()`]
239    #[inline]
240    pub fn k(&self) -> &STF {
241        &self.rgbl[0]
242    }
243    /// Screen transfer functions for the red channel of an RGB image
244    #[inline]
245    pub fn g(&self) -> &STF {
246        &self.rgbl[1]
247    }
248    /// Screen transfer functions for the red channel of an RGB image
249    #[inline]
250    pub fn b(&self) -> &STF {
251        &self.rgbl[2]
252    }
253    /// Screen transfer function for grayscale/luminance previews of an RGB image
254    ///
255    /// <div class="warning">
256    ///
257    /// Do not use this for grayscale images! This STF is only meant to be used
258    /// for grayscale previews of color images. For grayscale images, [`Self::k()`] should be used instead.
259    ///
260    /// </div>
261    #[inline]
262    pub fn l(&self) -> &STF {
263        &self.rgbl[3]
264    }
265}
266
267/// Allows transparent access to the per-channel [`STF`]
268impl std::ops::Index<usize> for DisplayFunction {
269    type Output = STF;
270
271    fn index(&self, index: usize) -> &Self::Output {
272        &self.rgbl[index]
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use libxml::parser::Parser;
280
281    #[test]
282    fn parse_node() {
283        // example from the XISF spec
284        let auto_stretch = r#"<DisplayFunction m="0.000735:0.000735:0.000735:0.5"
285            s="0.003758:0.003758:0.003758:0"
286            h="1:1:1:1"
287            l="0:0:0:0"
288            r="1:1:1:1"
289            name="AutoStretch" />"#;
290
291        let xml = Parser::default().parse_string(auto_stretch.as_bytes()).unwrap();
292        let df = DisplayFunction::parse_node(xml.get_root_readonly().unwrap());
293        let df = df.unwrap();
294        assert_eq!(df.name(), Some("AutoStretch"));
295        let rgb = STF::new(0.000735, 0.003758, 1.0, 0.0, 1.0);
296        assert_eq!(df.r(), &rgb);
297        assert_eq!(df.g(), &rgb);
298        assert_eq!(df.b(), &rgb);
299        assert_eq!(df.l(), &Default::default());
300
301        // should clamp midpoint balance to 1.0, swap high clip and low clip,
302        // clamp shadows_expansion to 0, and clamp highlights_expansion to 1
303        let malformed = r#"<DisplayFunction m="1.000735:1.000735:1.000735:0.5"
304            s="1:1:1:0"
305            h="0.003758:0.003758:0.003758:1"
306            l="1:1:1:0"
307            r="0:0:0:1" />"#;
308        let xml = Parser::default().parse_string(malformed.as_bytes()).unwrap();
309        let df = DisplayFunction::parse_node(xml.get_root_readonly().unwrap());
310        let df = df.unwrap();
311        assert_eq!(df.name(), None);
312        let rgb = STF::new(1.0, 0.003758, 1.0, 0.0, 1.0);
313        assert_eq!(df.r(), &rgb);
314        assert_eq!(df.g(), &rgb);
315        assert_eq!(df.b(), &rgb);
316        assert_eq!(df.l(), &Default::default());
317    }
318}