1#![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
36pub struct SerFile {
38 file: File,
40 pub header: SerHeader,
42 pub timestamps: Vec<u64>,
44}
45
46#[derive(Debug, Clone)]
47pub struct SerHeader {
48 pub image_height: u32,
50 pub image_width: u32,
52 pub frame_count: usize,
54 pub pixel_depth_per_plane: u32,
56 pub endianness: Endianness,
59 pub color_id: ColorId,
61 pub observer: String,
63 pub telescope: String,
65 pub instrument: String,
67 pub date_time: u64,
69 pub date_time_utc: u64,
71}
72
73impl SerHeader {
74 pub fn image_data_bytes(&self) -> usize {
76 self.image_frame_size() * self.frame_count
77 }
78
79 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 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 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 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 return Err(Error::new(
165 ErrorKind::InvalidData,
166 "not enough bytes for images",
167 ));
168 }
169
170 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 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)?; 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
307fn parse_u32(buf: &[u8]) -> u32 {
309 let mut buf = buf;
310 buf.read_u32::<LittleEndian>().unwrap()
311}
312
313fn parse_u64(buf: &[u8]) -> u64 {
315 let mut buf = buf;
316 buf.read_u64::<LittleEndian>().unwrap()
317}
318
319fn 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}