rustronomy_fits/
header_data_unit.rs

1/*
2    Copyright (C) 2022 Raúl Wolters
3
4    This file is part of rustronomy-fits.
5
6    rustronomy is free software: you can redistribute it and/or modify
7    it under the terms of the GNU General Public License as published by
8    the Free Software Foundation, either version 3 of the License, or
9    (at your option) any later version.
10
11    rustronomy is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14    GNU General Public License for more details.
15
16    You should have received a copy of the GNU General Public License
17    along with rustronomy.  If not, see <http://www.gnu.org/licenses/>.
18*/
19
20use core::fmt;
21use std::{borrow::Cow, error::Error, fmt::Display};
22
23use crate::{
24  bitpix::Bitpix,
25  extensions::{image::ImgParser, table::AsciiTblParser, Extension},
26  hdu_err::*,
27  header::Header,
28  raw::{
29    raw_io::{RawFitsReader, RawFitsWriter},
30    BlockSized,
31  },
32};
33
34const VALID_EXTENSION_NAMES: [&'static str; 3] = ["'IMAGE   '", "'TABLE   '", "'BINTABLE'"];
35
36#[derive(Debug, Clone)]
37pub struct HeaderDataUnit {
38  header: Header,
39  data: Option<Extension>,
40}
41
42impl HeaderDataUnit {
43  /*
44      INTERNAL CODE
45  */
46
47  pub(crate) fn decode_hdu(raw: &mut RawFitsReader) -> Result<Self, Box<dyn Error>> {
48    //(1) Read the header
49    let header = Header::decode_header(raw)?;
50
51    //(2) Read data, if there is any
52    let extension = match &header.get_value("XTENSION") {
53      None => {
54        /*  (2a)
55            This is the primary header (or there is simply no data in
56            this hdu). This means that this HDU may contain random
57            groups. Random groups and emtpy arrays have the NAXIS
58            keyword set to zero.
59        */
60        if header.get_value_as::<usize>("NAXIS")? == 0 {
61          //For now I'll just return None rather than implement random
62          //groups
63          None
64        } else {
65          //Image
66          Some(Self::read_img(raw, &header)?)
67        }
68      }
69      Some(extension_type) => {
70        /*  (2b)
71            This is not a primary header, but the header of an extension
72            hdu.
73        */
74        match extension_type.as_str() {
75          "'IMAGE   '" => Some(Self::read_img(raw, &header)?),
76          _kw @ "'TABLE   '" => Some(Self::read_table(raw, &header)?),
77          kw @ "'BINTABLE'" => Err(Self::not_impl(kw))?,
78          kw => Err(InvalidRecordValueError::new("XTENSION", kw, &VALID_EXTENSION_NAMES))?,
79        }
80      }
81    };
82
83    //(3) return complete HDU
84    Ok(HeaderDataUnit { header: header, data: extension })
85  }
86
87  fn read_table(raw: &mut RawFitsReader, header: &Header) -> Result<Extension, Box<dyn Error>> {
88    /*
89        To parse a table we need to know the following keywords:
90            TFIELDS => #fields in a row
91            NAXIS1 => #characters in a row
92            NAXIS2 => #rows in the table
93            TBCOL{i} => starting index of field i
94            TFORM{i} => data format of field i
95            TTYPE{i} => name of field i (not required)
96        In addition, we require the following keywords to have been set to:
97            NAXIS == 2
98            BITPIX == 8
99            PCOUNT == 0
100            GCOUNT == 1
101        We obtain these values from the header
102    */
103
104    //(1) check that the mandatory keywords have been set properly
105    let naxis: usize = header.get_value_as("NAXIS")?;
106    let bitpix: isize = header.get_value_as("BITPIX")?;
107    let pcount: usize = header.get_value_as("PCOUNT")?;
108    let gcount: usize = header.get_value_as("GCOUNT")?;
109    //Here come the if statements :c
110    if naxis != 2 {
111      Err(InvalidRecordValueError::new("NAXIS", &format!("{naxis}"), &["2"]))?
112    }
113    if bitpix != 8 {
114      Err(InvalidRecordValueError::new("BITPIX", &format!("{bitpix}"), &["8"]))?
115    }
116    if pcount != 0 {
117      Err(InvalidRecordValueError::new("PCOUNT", &format!("{pcount}"), &["0"]))?
118    }
119    if gcount != 1 {
120      Err(InvalidRecordValueError::new("GCOUNT", &format!("{gcount}"), &["1"]))?
121    }
122
123    //(2) Obtain the keywords required for decoding the header
124    let nfields: usize = header.get_value_as("TFIELDS")?;
125    let row_len: usize = header.get_value_as("NAXIS1")?;
126    let nrows: usize = header.get_value_as("NAXIS2")?;
127
128    let mut row_index_col_start: Vec<usize> = Vec::new();
129    for i in 1..=nfields {
130      row_index_col_start.push(
131        //We have to substract 1 since FITS indices start at 1 rather
132        //than 0
133        header.get_value_as::<usize>(&format!("TBCOL{i}"))? - 1,
134      );
135    }
136
137    let mut field_format: Vec<String> = Vec::new();
138    for i in 1..=nfields {
139      field_format.push(header.get_value_as(&format!("TFORM{i}"))?)
140    }
141
142    let labels = match header.get_value("TTYPE1") {
143      None => None,
144      Some(_) => {
145        /*
146            This header contains descriptive keywords for the entries
147            in the table. Note that the TTYPE{i} keywords are themselves
148            keywords, and the actual desciptions of the columns are
149            stored in the header behind these keywords.
150        */
151        let mut tmp: Vec<String> = Vec::new();
152        for i in 1..=nfields {
153          tmp.push(header.get_value_as(&format!("TTYPE{i}"))?);
154        }
155        Some(
156          //Before we return, we query keywords we've found so far
157          tmp
158            .into_iter()
159            .map(|mut ttype_keyword| {
160              //We still have to strip the keyword of its annoying
161              //{'keyword   '} syntax
162              ttype_keyword.remove(0);
163              ttype_keyword.pop();
164              header.get_value_as(ttype_keyword.trim())
165            })
166            .collect::<Result<Vec<String>, Box<dyn Error>>>()?,
167        )
168      }
169    };
170
171    //(3) Decode the image using the table parser
172    let tbl = AsciiTblParser::decode_tbl(
173      raw,
174      row_len,
175      nrows,
176      nfields,
177      row_index_col_start,
178      field_format,
179      labels,
180    )?;
181
182    //(R) return the completed table
183    Ok(tbl)
184  }
185
186  fn read_img(raw: &mut RawFitsReader, header: &Header) -> Result<Extension, Box<dyn Error>> {
187    //Let's start by getting the number of axes from the NAXIS keyword
188    let naxis: usize = header.get_value_as("NAXIS")?;
189
190    //Axis sizes are encoded in the NAXIS{i} keywords
191    let mut axes: Vec<usize> = Vec::new();
192    for i in 1..=naxis {
193      axes.push(header.get_value_as(&format!("NAXIS{i}"))?);
194    }
195
196    //Datatype is encoded in the BITPIX keyword
197    let bitpix = Bitpix::from_code(&header.get_value_as("BITPIX")?)?;
198
199    //Now do the actual decoding of the image:
200    Ok(ImgParser::decode_img(raw, &axes, bitpix)?)
201  }
202
203  pub(crate) fn encode_hdu(self, writer: &mut RawFitsWriter) -> Result<(), Box<dyn Error>> {
204    //(1) Write header
205    self.header.encode_header(writer)?;
206
207    //(2) If we have data, write the data
208    match self.data {
209      Some(data) => data.write_to_buffer(writer)?,
210      _ => {} //no data, do nothing
211    }
212
213    //(R) ok
214    Ok(())
215  }
216
217  fn not_impl(keyword: &str) -> Box<NotImplementedErr> {
218    Box::new(NotImplementedErr::new(keyword.to_string()))
219  }
220
221  /*
222      USER-FACING API STARTS HERE
223  */
224
225  //Some simple getters
226  pub fn get_header(&self) -> &Header {
227    &self.header
228  }
229  pub fn get_data(&self) -> Option<&Extension> {
230    self.data.as_ref()
231  }
232
233  //Destructs HDU into parts
234  pub fn to_parts(self) -> (Header, Option<Extension>) {
235    (self.header, self.data)
236  }
237
238  pub fn pretty_print_header(&self) -> String {
239    format!(
240      "[Header] - #records: {}, size: {}",
241      self.header.get_block_len(),
242      self.header.get_num_records()
243    )
244  }
245
246  pub fn pretty_print_data(&self) -> String {
247    let data_string: Cow<str> = match &self.data {
248      None => "(NO_DATA)".into(),
249      Some(data) => format!("{data}").into(),
250    };
251    format!("[Data] {data_string}")
252  }
253}
254
255impl BlockSized for HeaderDataUnit {
256  fn get_block_len(&self) -> usize {
257    self.header.get_block_len()
258      + match &self.data {
259        None => 0,
260        Some(data) => data.get_block_len(),
261      }
262  }
263}
264
265impl Display for HeaderDataUnit {
266  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
267    write!(f, "{}", self.pretty_print_header())?;
268    write!(f, "{}", self.pretty_print_data())?;
269    Ok(())
270  }
271}