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}