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