ser_io/
lib.rs

1// MIT License
2//
3// Copyright (c) 2021 Andy Grove
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in all
13// copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21// SOFTWARE.
22
23#![doc = include_str!("../README.md")]
24
25use std::fs::{self, File};
26use std::io::{Error, ErrorKind, Read, Result, Seek, SeekFrom, Write};
27use std::path::PathBuf;
28use std::str;
29
30use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
31
32const HEADER_SIZE: usize = 178;
33
34const FILE_ID: &str = "LUCAM-RECORDER";
35
36/// SER file
37pub struct SerFile {
38    /// File
39    file: File,
40    /// SER header
41    pub header: SerHeader,
42    /// Timestamp in UTC of each frame
43    pub timestamps: Vec<u64>,
44}
45
46#[derive(Debug, Clone)]
47pub struct SerHeader {
48    /// Image height, in pixels
49    pub image_height: u32,
50    /// Image width, in pixels
51    pub image_width: u32,
52    /// Number of frames
53    pub frame_count: usize,
54    /// Pixel depth per plane
55    pub pixel_depth_per_plane: u32,
56    /// Number of bytes per pixel (1 or 2)
57    /// The endianness of encoded image data. This is only relevant if the image data is 16-bit
58    pub endianness: Endianness,
59    /// Bayer encoding
60    pub color_id: ColorId,
61    /// Name of observer
62    pub observer: String,
63    /// Name of telescope
64    pub telescope: String,
65    /// Name of instrument
66    pub instrument: String,
67    /// File timestamp
68    pub date_time: u64,
69    /// File timestamp in UTC
70    pub date_time_utc: u64,
71}
72
73impl SerHeader {
74    /// Total number of image bytes in the file
75    pub fn image_data_bytes(&self) -> usize {
76        self.image_frame_size() * self.frame_count
77    }
78
79    /// Number of bytes per image frame
80    pub fn image_frame_size(&self) -> usize {
81        (self.bytes_per_pixel() as u32 * self.image_width * self.image_height) as usize
82    }
83
84    /// Number of bytes per pixel, either 1 or 2 (bit depth) times 1 or 3 (mono or rgb)
85    pub fn bytes_per_pixel(&self) -> usize {
86        let is_rgb = self.color_id == ColorId::RGB || self.color_id == ColorId::BGR;
87        ((self.pixel_depth_per_plane as usize - 1) / 8 + 1) * (1 + 2 * is_rgb as usize)
88    }
89}
90
91impl SerFile {
92    /// Open a SER file
93    pub fn open(filename: &str) -> Result<Self> {
94        let mut file = File::open(&filename)?;
95        let metadata = fs::metadata(&filename)?;
96        let len = metadata.len() as usize;
97        if len < HEADER_SIZE {
98            return Err(Error::new(
99                ErrorKind::InvalidData,
100                "file shorter than header length of 178 bytes",
101            ));
102        }
103
104        let mut header_bytes = [0u8; HEADER_SIZE];
105        file.read_exact(&mut header_bytes)?;
106
107        let header_id= parse_string(&header_bytes[0..14]);
108        if header_id != FILE_ID {
109            return Err(Error::new(ErrorKind::InvalidData, "bad header"));
110        }
111
112        // unused
113        let _lu_id = parse_u32(&header_bytes[14..18]);
114
115        let color_id = parse_u32(&header_bytes[18..22]);
116
117        let color_id = match color_id {
118            0 => ColorId::Mono,
119            8 => ColorId::BayerRGGB,
120            9 => ColorId::BayerGRBG,
121            10 => ColorId::BayerGBRG,
122            11 => ColorId::BayerBGGR,
123            16 => ColorId::BayerCYYM,
124            17 => ColorId::BayerYCMY,
125            18 => ColorId::BayerYMCY,
126            19 => ColorId::BayerMYYC,
127            100 => ColorId::RGB,
128            101 => ColorId::BGR,
129            _ => return Err(Error::new(ErrorKind::InvalidData, "invalid color id")),
130        };
131
132        let endianness = match parse_u32(&header_bytes[22..26]) {
133            0 => Endianness::LittleEndian,
134            _ => Endianness::BigEndian,
135        };
136
137        let image_width = parse_u32(&header_bytes[26..30]);
138        let image_height = parse_u32(&header_bytes[30..34]);
139        let pixel_depth_per_plane = parse_u32(&header_bytes[34..38]);
140        let frame_count = parse_u32(&header_bytes[38..42]) as usize;
141        let observer = parse_string(&header_bytes[42..82]);
142        let instrument = parse_string(&header_bytes[82..122]);
143        let telescope = parse_string(&header_bytes[122..162]);
144        let date_time = parse_u64(&header_bytes[162..170]);
145        let date_time_utc = parse_u64(&header_bytes[170..HEADER_SIZE]);
146
147        let header = SerHeader {
148            image_height,
149            image_width,
150            frame_count,
151            pixel_depth_per_plane,
152            endianness,
153            color_id,
154            observer,
155            telescope,
156            instrument,
157            date_time,
158            date_time_utc,
159        };
160
161        if len < HEADER_SIZE + header.image_data_bytes() {
162            // TODO could add an option to be able to read valid frames that were
163            // saved in the case of the file being truncated
164            return Err(Error::new(
165                ErrorKind::InvalidData,
166                "not enough bytes for images",
167            ));
168        }
169
170        // read optional trailer with timestamp per frame
171        let trailer_offset = HEADER_SIZE + header.image_data_bytes() as usize;
172        let trailer_size = 8 * frame_count as usize;
173        let timestamps: Vec<u64> = if len >= trailer_offset + trailer_size {
174            let mut trailer = vec![0u8; trailer_size];
175            file.seek(SeekFrom::Start(trailer_offset as u64))?;
176            file.read_exact(&mut trailer)?;
177            file.seek(SeekFrom::End(0))?;
178            (0..frame_count as usize)
179                .map(|i| parse_u64(&trailer[i..i + 8]))
180                .collect::<Vec<_>>()
181        } else {
182            vec![]
183        };
184
185        Ok(Self {
186            file,
187            header,
188            timestamps,
189        })
190    }
191
192    /// Read the frame at the given offset
193    pub fn read_frame(&mut self, i: usize) -> Result<Vec<u8>> {
194        if i < self.header.frame_count as usize {
195            let mut buffer = vec![0u8; self.header.image_frame_size()];
196            let offset = HEADER_SIZE + i * self.header.image_frame_size();
197            self.file.seek(SeekFrom::Start(offset as u64))?;
198            self.file.read_exact(&mut buffer)?;
199            self.file.seek(SeekFrom::End(0))?;
200            Ok(buffer)
201        } else {
202            Err(Error::new(ErrorKind::InvalidData, "invalid frame index"))
203        }
204    }
205}
206
207pub struct SerWriter {
208    header: SerHeader,
209    file: File,
210}
211
212impl SerWriter {
213    pub fn new<T: Into<PathBuf>>(path: T, mut header: SerHeader) -> Result<Self> {
214        let mut file = File::create(path.into())?;
215        header.frame_count = 0;
216
217        let mut header_bytes: Vec<u8> = Vec::with_capacity(HEADER_SIZE);
218        header_bytes.append(&mut FILE_ID.as_bytes().to_vec());
219        header_bytes.write_u32::<LittleEndian>(0)?; // lu_id unused
220        let bayer_n: u32 = header.color_id as u32;
221        header_bytes.write_u32::<LittleEndian>(bayer_n)?;
222        header_bytes.write_u32::<LittleEndian>(match header.endianness {
223            Endianness::LittleEndian => 0,
224            Endianness::BigEndian => 1,
225        })?;
226        header_bytes.write_u32::<LittleEndian>(header.image_width)?;
227        header_bytes.write_u32::<LittleEndian>(header.image_height)?;
228        header_bytes.write_u32::<LittleEndian>(header.pixel_depth_per_plane)?;
229        header_bytes.write_u32::<LittleEndian>(header.frame_count as u32)?;
230
231        let mut string_buffer = [0u8; 40];
232        override_buffer(header.observer.as_bytes(), &mut string_buffer);
233        header_bytes.write_all(&string_buffer)?;
234
235        string_buffer = [0u8; 40];
236        override_buffer(header.instrument.as_bytes(), &mut string_buffer);
237        header_bytes.write_all(&string_buffer)?;
238
239        string_buffer = [0u8; 40];
240        override_buffer(header.telescope.as_bytes(), &mut string_buffer);
241        header_bytes.write_all(&string_buffer)?;
242
243        header_bytes.write_u64::<LittleEndian>(header.date_time)?;
244        header_bytes.write_u64::<LittleEndian>(header.date_time_utc)?;
245
246        assert!(header_bytes.len() == HEADER_SIZE);
247
248        file.write_all(&header_bytes)?;
249
250        Ok(Self { header, file })
251    }
252
253    pub fn write_frame(&mut self, frame: &[u8]) -> Result<()> {
254        if self.header.image_frame_size() == frame.len() {
255            self.update_frame_count()?;
256            self.file.write_all(frame)
257        } else {
258            Err(Error::new(
259                ErrorKind::InvalidData,
260                format!(
261                    "Cannot write image with {} bytes when header specifies image size as {} bytes",
262                    frame.len(),
263                    self.header.image_frame_size()
264                ),
265            ))
266        }
267    }
268
269    pub fn write_timestamps(&mut self, timestamps: &[u64]) -> Result<()> {
270        let mut header_bytes = Vec::with_capacity(4 * timestamps.len());
271        for ts in timestamps {
272            header_bytes.write_u64::<LittleEndian>(*ts)?;
273        }
274        self.file.write_all(&header_bytes)
275    }
276
277    pub fn update_frame_count(&mut self) -> Result<()> {
278        self.header.frame_count += 1;
279        self.file.seek(SeekFrom::Start(38))?;
280        self.file.write_all(&self.header.frame_count.to_le_bytes())?;
281        self.file.seek(SeekFrom::End(0))?;
282        Ok(())
283    }
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287pub enum ColorId {
288    Mono = 0,
289    BayerRGGB = 8,
290    BayerGRBG,
291    BayerGBRG,
292    BayerBGGR,
293    BayerCYYM = 16,
294    BayerYCMY,
295    BayerYMCY,
296    BayerMYYC,
297    RGB = 100,
298    BGR,
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302pub enum Endianness {
303    LittleEndian,
304    BigEndian,
305}
306
307/// Parse a little-endian u32
308fn parse_u32(buf: &[u8]) -> u32 {
309    let mut buf = buf;
310    buf.read_u32::<LittleEndian>().unwrap()
311}
312
313/// Parse a little-endian u64
314fn parse_u64(buf: &[u8]) -> u64 {
315    let mut buf = buf;
316    buf.read_u64::<LittleEndian>().unwrap()
317}
318
319/// Parse a string
320fn parse_string(x: &[u8]) -> String {
321    String::from_utf8_lossy(x).to_string()
322}
323
324fn override_buffer(new_buf: &[u8], buf: &mut [u8]) {
325    let iter = new_buf.len().min(buf.len());
326    for i in 0..iter {
327        buf[i] = new_buf[i];
328    }
329}