Skip to main content

ser_file/
ser.rs

1use binrw::binrw;
2use getset::{Getters, Setters};
3use std::fmt::{Display, Formatter};
4
5use crate::{FixedString, Frame, FrameFormat, PixelDepth, Timestamp};
6
7/// SER format description version 3
8///
9/// Authors
10/// Heiko Wilkens (version 2)
11/// Grischa Hahn (red = extensions of version 3)
12///
13/// 2014 Feb 06
14///
15/// Source: <https://grischa-hahn.hier-im-netz.de/astro/ser/SER%20Doc%20V3b.pdf>
16///
17#[binrw]
18#[brw(little)]
19#[derive(Getters, Setters)]
20pub struct Ser {
21    /// 1_FileID
22    ///
23    /// Format: String
24    ///
25    /// Length: 14 Byte (14 ASCII characters)
26    ///
27    /// Content: "LUCAM-RECORDER" (fix)
28    #[brw(magic = b"LUCAM-RECORDER")]
29    file_id: (),
30
31    /// 2_LuID
32    ///
33    /// Format: Integer_32 (little-endian)
34    ///
35    /// Length: 4 Byte
36    ///
37    /// Content: Lumenera camera series ID (currently unused; default = 0)
38    #[getset(get = "pub", set = "pub")]
39    lu_id: i32,
40
41    /// 3_ColorID
42    ///
43    /// Format: Integer_32 (little-endian)
44    ///
45    /// Length: 4 Byte
46    color_id: ColorId,
47
48    /// 4_LittleEndian
49    ///
50    /// Format: Integer_32 (little-endian)
51    ///
52    /// Length: 4 Byte
53    ///
54    /// Content: 0 (FALSE) for big-endian byte order in 16 bit image data
55    /// 1 (TRUE) for little-endian byte order in 16 bit image data
56    ///
57    /// NOTE: See comment on [PixelEndian]
58    little_endian: PixelEndian,
59
60    /// 5_ImageWidth
61    ///
62    /// Format: Integer_32 (little-endian)
63    ///
64    /// Length: 4 Byte
65    ///
66    /// Content: Width of every image in pixel
67    image_width: i32,
68
69    /// 6_ImageHeight
70    ///
71    /// Format: Integer_32 (little-endian)
72    ///
73    /// Length: 4 Byte
74    ///
75    /// Content: Height of every image in pixel
76    image_height: i32,
77
78    /// 7_PixelDepthPerPlane
79    ///
80    /// Format: Integer_32 (little-endian)
81    ///
82    /// Length: 4 Byte
83    ///
84    /// Content: True bit depth per pixel per plane
85    pixel_depth_per_plane: PixelDepth,
86
87    /// 8_FrameCount
88    ///
89    /// Format: Integer_32 (little-endian)
90    ///
91    /// Length: 4 Byte
92    ///
93    /// Content: Number of image frames in SER file
94    #[br(temp)]
95    #[bw(calc(image_data.len() as _))]
96    frame_count: i32,
97
98    /// 9_Observer
99    ///
100    /// Format: String
101    ///
102    /// Length: 40 Byte (40 ASCII characters {32…126 dec.}, fill unused characters with 0 dec.)
103    ///
104    /// Content: Name of observer
105    #[getset(get = "pub", set = "pub")]
106    observer: FixedString<40>,
107
108    /// 10_Instrument
109    ///
110    /// Format: String
111    ///
112    /// Length: 40 Byte (40 ASCII characters {32…126 dec.}, fill unused characters with 0 dec.)
113    ///
114    /// Content: Name of used camera
115    #[getset(get = "pub", set = "pub")]
116    instrument: FixedString<40>,
117
118    /// 11_Telescope
119    ///
120    /// Format: String
121    ///
122    /// Length: 40 Byte (40 ASCII characters {32…126 dec.}, fill unused characters with 0 dec.)
123    ///
124    /// Content: Name of used telescope
125    #[getset(get = "pub", set = "pub")]
126    telescope: FixedString<40>,
127
128    /// 12_DateTime
129    ///
130    /// Format: Date / Integer_64 (little-endian)
131    ///
132    /// Length: 8 Byte
133    ///
134    /// Content: Start time of image stream (local time)
135    ///
136    /// If 12_DateTime <= 0 then 12_DateTime is invalid and the SER file does not contain a
137    /// Time stamp trailer.
138    #[getset(get = "pub")]
139    datetime: Timestamp,
140
141    /// 13_DateTime_UTC
142    ///
143    /// Format: Date / Integer_64 (little-endian)
144    ///
145    /// Length: 8 Byte
146    ///
147    /// Content: Start time of image stream in UTC
148    #[getset(get = "pub")]
149    datetime_utc: Timestamp,
150
151    /// Image Data
152    ///
153    /// Image data starts at File start offset decimal 178
154    ///
155    /// Size of every image frame in byte is: 5_ImageWidth x 6_ImageHeigth x BytePerPixel
156    #[br(args {
157        count: frame_count as usize,
158        inner: FrameFormat::new(
159            color_id.clone(),
160            pixel_depth_per_plane.clone(),
161            little_endian.clone(),
162            image_width as _,
163            image_height as _,
164        )
165    })]
166    #[bw(args_raw = self.frame_format())]
167    image_data: Vec<Frame>,
168
169    /// Trailer
170    ///
171    /// Trailer starts at byte offset: 178 + 8_FrameCount x 5_ImageWidth x 6_ImageHeigth x
172    /// BytePerPixel.
173    ///
174    /// Trailer contains Date / Integer_64 (little-endian) time stamps in UTC for every image frame.
175    /// According to Microsoft documentation the used time stamp has the following format:
176    /// “Holds IEEE 64-bit (8-byte) values that represent dates ranging from January 1 of the year 0001
177    /// through December 31 of the year 9999, and times from 12:00:00 AM (midnight) through
178    /// 11:59:59.9999999 PM. Each increment represents 100 nanoseconds of elapsed time since the
179    /// beginning of January 1 of the year 1 in the Gregorian calendar. The maximum value represents
180    /// 100 nanoseconds before the beginning of January 1 of the year 10000.”
181    ///
182    /// According to the findings of Raoul Behrend, Université de Genève, the date record is not a 64 bits
183    /// unsigned integer as stated, but a 62 bits unsigned integer. He got no information about the use of
184    /// the two MSB.
185    #[br(count = match frame_count { f if datetime.is_valid() => f as usize, _ => 0})]
186    trailer: Vec<Timestamp>,
187}
188
189/// Mutate SER file datetimes and frame timestamps
190pub struct DatesMut<'a> {
191    datetime: &'a mut Timestamp,
192    datetime_utc: &'a mut Timestamp,
193    frame_count: usize,
194    frame_times: &'a mut Vec<Timestamp>,
195}
196
197#[derive(Debug)]
198pub enum DateErrors {
199    InvalidDatetime,
200    IncorrectTimestamps,
201}
202
203/// Mutate SER file frames
204pub struct FramesMut<'a> {
205    format: FrameFormat,
206    has_trailer: bool,
207    frames: &'a mut Vec<Frame>,
208    frame_times: &'a mut Vec<Timestamp>,
209}
210
211#[derive(Debug)]
212pub enum FramePushErrors {
213    Incompatible,
214    TimestampExpected,
215    TimestampUnexpected,
216}
217
218/// Describes the color format of SER frames
219#[binrw]
220#[allow(non_camel_case_types)]
221#[derive(Debug, Clone)]
222pub enum ColorId {
223    #[brw(magic = 0i32)]
224    MONO,
225    #[brw(magic = 8i32)]
226    BAYER_RGGB,
227    #[brw(magic = 9i32)]
228    BAYER_GRBG,
229    #[brw(magic = 10i32)]
230    BAYER_GBRG,
231    #[brw(magic = 11i32)]
232    BAYER_BGGR,
233    #[brw(magic = 16i32)]
234    BAYER_CYYM,
235    #[brw(magic = 17i32)]
236    BAYER_YCMY,
237    #[brw(magic = 18i32)]
238    BAYER_YMCY,
239    #[brw(magic = 19i32)]
240    BAYER_MYYC,
241    #[brw(magic = 100i32)]
242    RGB,
243    #[brw(magic = 101i32)]
244    BGR,
245}
246
247/// Describes the endianness of pixel data in SER frames
248///
249/// Developers Note
250/// ===================
251///
252/// Astonishingly, by convention most SER implementations invert this flag's
253/// value from the official specification.
254///
255/// For example, Siril's [source
256/// code](https://gitlab.com/free-astro/siril/-/blob/master/src/io/ser.h#L73)
257/// contains the following comment:
258///
259/// > "For an unknown reason, several of the first programs to support SER
260/// > disrespect the specification regarding the endianness flag. The
261/// > specification states that a boolean value is used for the LittleEndian
262/// > header, and they use it as a BigEndian header, with 0 for little-endian
263/// > and 1 for big-endian. Consequently, to not break compatibility with these
264/// > first implementations, later programs, like Siril and GoQat, have also
265/// > decided to implement this header in opposite meaning to the specification."
266///
267/// Other rust crates seem to follow this convention, and this crate does as
268/// well.
269#[binrw]
270#[derive(Clone, Debug, PartialEq)]
271pub enum PixelEndian {
272    #[brw(magic = 1i32)] // see above
273    Big,
274    #[brw(magic = 0i32)] // see above
275    Little,
276}
277
278impl Ser {
279    pub fn with_format(format: FrameFormat) -> Self {
280        Self {
281            file_id: (),
282            lu_id: 0,
283            color_id: format.color().clone(),
284            little_endian: format.endian().clone(),
285            image_width: *format.width() as _,
286            image_height: *format.height() as _,
287            pixel_depth_per_plane: format.depth().clone(),
288            observer: FixedString::default(),
289            instrument: FixedString::default(),
290            telescope: FixedString::default(),
291            datetime: Timestamp::default(),
292            datetime_utc: Timestamp::default(),
293            image_data: Vec::new(),
294            trailer: Vec::new(),
295        }
296    }
297
298    /// Creates a new copy of this SER's frame format
299    pub fn frame_format(&self) -> FrameFormat {
300        FrameFormat::new(
301            self.color_id.clone(),
302            self.pixel_depth_per_plane.clone(),
303            self.little_endian.clone(),
304            self.image_width as _,
305            self.image_height as _,
306        )
307    }
308
309    /// SER files include timestamps for each frame only if the `datetime` field
310    /// is valid.
311    pub fn has_frame_timestamps(&self) -> bool {
312        self.datetime.is_valid()
313    }
314
315    /// The number of frames
316    pub fn frame_count(&self) -> usize {
317        self.image_data.len()
318    }
319
320    /// Returns an iterator for [Frame] references and their associated
321    /// [Timestamp] if provided.
322    pub fn iter(&self) -> impl Iterator<Item = (&Frame, Option<&Timestamp>)> {
323        let mut times = self.trailer.iter();
324        self.image_data
325            .iter()
326            .zip(std::iter::from_fn(move || Some(times.next())))
327    }
328
329    /// Moves into an iterator for [Frame]s and their associated [Timestamp] if
330    /// provided.
331    pub fn into_iter(self) -> impl Iterator<Item = (Frame, Option<Timestamp>)> {
332        let mut times = self.trailer.into_iter();
333        self.image_data
334            .into_iter()
335            .zip(std::iter::from_fn(move || Some(times.next())))
336    }
337
338    /// Returns a [FramesMut] object for mutating frames
339    pub fn frames_mut<'a>(&'a mut self) -> FramesMut<'a> {
340        FramesMut {
341            format: self.frame_format(),
342            has_trailer: self.has_frame_timestamps(),
343            frames: &mut self.image_data,
344            frame_times: &mut self.trailer,
345        }
346    }
347
348    /// Returns a [DatesMut] object for mutating dates and frame timestamps
349    pub fn dates_mut<'a>(&'a mut self) -> DatesMut<'a> {
350        DatesMut {
351            datetime: &mut self.datetime,
352            datetime_utc: &mut self.datetime_utc,
353            frame_count: self.image_data.len(),
354            frame_times: &mut self.trailer,
355        }
356    }
357}
358
359impl<'a> FramesMut<'a> {
360    /// Returns a reference to the current [FrameFormat].
361    pub fn format(&self) -> &FrameFormat {
362        &self.format
363    }
364
365    /// Push a frame onto the frames [Vec].
366    ///
367    /// [Frame]s must have a pixel format compatible with self's [FrameFormat].
368    ///
369    /// Frame [Timestamp]s MUST be specified if the SER's datetime is set.
370    /// Otherwise, they MAY NOT be specified.
371    ///
372    /// Frame timestamps are in UTC
373    pub fn try_push(
374        &mut self,
375        frame: Frame,
376        timestamp: Option<Timestamp>,
377    ) -> Result<(), FramePushErrors> {
378        if self.format != frame {
379            return Err(FramePushErrors::Incompatible);
380        }
381
382        if self.has_trailer {
383            match timestamp {
384                Some(ts) => self.frame_times.push(ts),
385                None => return Err(FramePushErrors::TimestampExpected),
386            }
387        } else {
388            match timestamp {
389                Some(_) => return Err(FramePushErrors::TimestampUnexpected),
390                None => (),
391            }
392        };
393
394        self.frames.push(frame);
395
396        Ok(())
397    }
398}
399
400impl<'a> DatesMut<'a> {
401    /// Clear the file's datetimes and frame timestamps
402    pub fn clear(&mut self) {
403        *self.datetime = Timestamp::default();
404        *self.datetime_utc = Timestamp::default();
405        self.frame_times.clear();
406    }
407
408    /// Sets the file's datetimes and frame timestamps.
409    ///
410    /// The provided datetimes MUST be valid [Timestamp]s.
411    ///
412    /// The [Vec] of timestamps MUST have the same length as frames in the file.
413    pub fn try_set_dates(
414        &mut self,
415        datetime: Timestamp,
416        datetime_utc: Timestamp,
417        frame_times: Vec<Timestamp>,
418    ) -> Result<(), DateErrors> {
419        if !datetime.is_valid() || !datetime_utc.is_valid() {
420            return Err(DateErrors::InvalidDatetime);
421        }
422
423        if frame_times.len() != self.frame_count {
424            return Err(DateErrors::IncorrectTimestamps);
425        }
426
427        *self.datetime = datetime;
428        *self.datetime_utc = datetime_utc;
429        *self.frame_times = frame_times;
430
431        Ok(())
432    }
433}
434
435impl Default for PixelEndian {
436    fn default() -> Self {
437        PixelEndian::Little
438    }
439}
440
441impl From<&PixelEndian> for binrw::Endian {
442    fn from(value: &PixelEndian) -> Self {
443        match value {
444            PixelEndian::Big => binrw::Endian::Big,
445            PixelEndian::Little => binrw::Endian::Little,
446        }
447    }
448}
449
450impl std::error::Error for FramePushErrors {}
451
452impl Display for FramePushErrors {
453    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
454        match self {
455            FramePushErrors::Incompatible => {
456                f.write_str("Frame incompatible. All frames must have the same format.")
457            }
458            FramePushErrors::TimestampExpected => f.write_str(
459                "Timestamps MUST be added for each frame when the SER's datetime is valid.",
460            ),
461            FramePushErrors::TimestampUnexpected => {
462                f.write_str("Timestamps MAY NOT be added when the SER's datetime is invalid.")
463            }
464        }
465    }
466}
467
468impl std::error::Error for DateErrors {}
469
470impl Display for DateErrors {
471    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
472        match self {
473            DateErrors::InvalidDatetime => f.write_str("Cannot set datetime to invalid timestamp. To clear the datetimes, use the `.clear()` method."),
474            DateErrors::IncorrectTimestamps => f.write_str("Frame timestamps do not match the number of the frames."),
475        }
476    }
477}
478
479impl From<FrameFormat> for Ser {
480    fn from(value: FrameFormat) -> Self {
481        Self::with_format(value)
482    }
483}