use std::cell::RefCell;
use std::path::{Path, PathBuf};
use super::errors::{Error, Result};
use super::hdu::FitsHdu;
use super::images::ImageDescription;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileOpenMode {
ReadOnly,
ReadWrite,
}
pub struct FitsFile {
data: Vec<u8>,
filename: PathBuf,
mode: FileOpenMode,
cached_parse: RefCell<Option<crate::hdu::FitsData>>,
}
pub struct NewFitsFile {
path: PathBuf,
overwrite: bool,
}
pub trait DescribesHdu {
fn get_hdu<'a>(
&self,
fits_data: &'a crate::hdu::FitsData,
) -> Option<(usize, &'a crate::hdu::Hdu)>;
}
impl DescribesHdu for usize {
fn get_hdu<'a>(
&self,
fits_data: &'a crate::hdu::FitsData,
) -> Option<(usize, &'a crate::hdu::Hdu)> {
fits_data.get(*self).map(|hdu| (*self, hdu))
}
}
impl DescribesHdu for &str {
fn get_hdu<'a>(
&self,
fits_data: &'a crate::hdu::FitsData,
) -> Option<(usize, &'a crate::hdu::Hdu)> {
for (i, hdu) in fits_data.iter().enumerate() {
for card in &hdu.cards {
if card.keyword_str() == "EXTNAME" {
if let Some(crate::value::Value::String(ref s)) = card.value {
if s.trim() == *self {
return Some((i, hdu));
}
}
}
}
}
None
}
}
impl DescribesHdu for String {
fn get_hdu<'a>(
&self,
fits_data: &'a crate::hdu::FitsData,
) -> Option<(usize, &'a crate::hdu::Hdu)> {
self.as_str().get_hdu(fits_data)
}
}
impl FitsFile {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let data = std::fs::read(path.as_ref())?;
Ok(FitsFile {
data,
filename: path.as_ref().to_path_buf(),
mode: FileOpenMode::ReadOnly,
cached_parse: RefCell::new(None),
})
}
pub fn edit<P: AsRef<Path>>(path: P) -> Result<Self> {
let data = std::fs::read(path.as_ref())?;
Ok(FitsFile {
data,
filename: path.as_ref().to_path_buf(),
mode: FileOpenMode::ReadWrite,
cached_parse: RefCell::new(None),
})
}
pub fn parsed(&self) -> Result<std::cell::Ref<'_, crate::hdu::FitsData>> {
{
let cache = self.cached_parse.borrow();
if cache.is_some() {
return Ok(std::cell::Ref::map(cache, |c| c.as_ref().unwrap()));
}
}
let parsed = crate::hdu::parse_fits(&self.data)?;
*self.cached_parse.borrow_mut() = Some(parsed);
Ok(std::cell::Ref::map(self.cached_parse.borrow(), |c| {
c.as_ref().unwrap()
}))
}
fn invalidate_cache(&self) {
*self.cached_parse.borrow_mut() = None;
}
pub fn create<P: AsRef<Path>>(path: P) -> NewFitsFile {
NewFitsFile {
path: path.as_ref().to_path_buf(),
overwrite: false,
}
}
pub fn primary_hdu(&self) -> Result<FitsHdu> {
Ok(FitsHdu { hdu_index: 0 })
}
pub fn hdu<D: DescribesHdu>(&self, desc: D) -> Result<FitsHdu> {
let fits_data = self.parsed()?;
let (idx, _) = desc
.get_hdu(&fits_data)
.ok_or(Error::Message("HDU not found".to_string()))?;
Ok(FitsHdu { hdu_index: idx })
}
pub fn num_hdus(&self) -> Result<usize> {
let fits_data = self.parsed()?;
Ok(fits_data.len())
}
pub fn iter(&self) -> Result<Vec<FitsHdu>> {
let fits_data = self.parsed()?;
Ok((0..fits_data.len())
.map(|i| FitsHdu { hdu_index: i })
.collect())
}
pub fn create_image(&mut self, extname: &str, desc: &ImageDescription) -> Result<FitsHdu> {
let bitpix = desc.data_type.to_bitpix();
let naxes = &desc.dimensions;
let mut cards = crate::extension::build_extension_header(
crate::extension::ExtensionType::Image,
bitpix,
naxes,
0,
1,
)?;
if let Some(bzero) = desc.data_type.unsigned_bzero() {
cards.push(crate::header::Card {
keyword: make_keyword("BZERO"),
value: Some(bzero),
comment: None,
});
cards.push(crate::header::Card {
keyword: make_keyword("BSCALE"),
value: Some(crate::value::Value::Integer(1)),
comment: None,
});
}
let extname_card = crate::header::Card {
keyword: make_keyword("EXTNAME"),
value: Some(crate::value::Value::String(extname.to_string())),
comment: None,
};
cards.push(extname_card);
let header_bytes = crate::header::serialize_header(&cards)?;
let data_bytes = desc.dimensions.iter().copied().product::<usize>()
* ((bitpix.unsigned_abs() as usize) / 8);
let padded_data = crate::block::padded_byte_len(data_bytes);
self.data.extend_from_slice(&header_bytes);
self.data.resize(self.data.len() + padded_data, 0u8);
self.invalidate_cache();
let fits_data = self.parsed()?;
let idx = fits_data.len() - 1;
Ok(FitsHdu { hdu_index: idx })
}
pub fn create_table(
&mut self,
extname: &str,
columns: &[crate::bintable::BinaryColumnDescriptor],
) -> Result<FitsHdu> {
let mut cards = crate::bintable::build_binary_table_cards(columns, 0, 0)?;
let extname_card = crate::header::Card {
keyword: make_keyword("EXTNAME"),
value: Some(crate::value::Value::String(extname.to_string())),
comment: None,
};
cards.push(extname_card);
let header_bytes = crate::header::serialize_header(&cards)?;
self.data.extend_from_slice(&header_bytes);
self.invalidate_cache();
let fits_data = self.parsed()?;
let idx = fits_data.len() - 1;
Ok(FitsHdu { hdu_index: idx })
}
pub fn create_ascii_table(
&mut self,
extname: &str,
columns: &[crate::table::AsciiColumnDescriptor],
) -> Result<FitsHdu> {
let mut cards = crate::table::build_ascii_table_cards(columns, 0)?;
let extname_card = crate::header::Card {
keyword: make_keyword("EXTNAME"),
value: Some(crate::value::Value::String(extname.to_string())),
comment: None,
};
cards.push(extname_card);
let header_bytes = crate::header::serialize_header(&cards)?;
self.data.extend_from_slice(&header_bytes);
self.invalidate_cache();
let fits_data = self.parsed()?;
let idx = fits_data.len() - 1;
Ok(FitsHdu { hdu_index: idx })
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn set_data(&mut self, data: Vec<u8>) {
self.data = data;
self.invalidate_cache();
}
pub fn flush(&self) -> Result<()> {
if self.mode == FileOpenMode::ReadWrite {
std::fs::write(&self.filename, &self.data)?;
}
Ok(())
}
pub fn filename(&self) -> &Path {
&self.filename
}
pub fn mode(&self) -> FileOpenMode {
self.mode
}
}
impl Drop for FitsFile {
fn drop(&mut self) {
if self.mode == FileOpenMode::ReadWrite {
let _ = std::fs::write(&self.filename, &self.data);
}
}
}
impl NewFitsFile {
pub fn overwrite(mut self) -> Self {
self.overwrite = true;
self
}
pub fn open(self) -> Result<FitsFile> {
if !self.overwrite && self.path.exists() {
return Err(Error::Message(format!(
"file already exists: {}",
self.path.display()
)));
}
let cards = crate::primary::build_primary_header(8, &[])?;
let header_bytes = crate::header::serialize_header(&cards)?;
std::fs::write(&self.path, &header_bytes)?;
Ok(FitsFile {
data: header_bytes,
filename: self.path,
mode: FileOpenMode::ReadWrite,
cached_parse: RefCell::new(None),
})
}
}
fn make_keyword(name: &str) -> [u8; 8] {
let mut kw = [b' '; 8];
let bytes = name.as_bytes();
let len = bytes.len().min(8);
kw[..len].copy_from_slice(&bytes[..len]);
kw
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::images::ImageType;
#[test]
fn create_and_open() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.fits");
let f = FitsFile::create(&path).open().unwrap();
assert_eq!(f.mode(), FileOpenMode::ReadWrite);
assert!(f.data().len() >= 2880);
}
#[test]
fn create_exists_without_overwrite() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.fits");
FitsFile::create(&path).open().unwrap();
assert!(FitsFile::create(&path).open().is_err());
}
#[test]
fn create_with_overwrite() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.fits");
FitsFile::create(&path).open().unwrap();
FitsFile::create(&path).overwrite().open().unwrap();
}
#[test]
fn open_readonly() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.fits");
FitsFile::create(&path).open().unwrap();
let f = FitsFile::open(&path).unwrap();
assert_eq!(f.mode(), FileOpenMode::ReadOnly);
}
#[test]
fn edit_mode() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.fits");
FitsFile::create(&path).open().unwrap();
let f = FitsFile::edit(&path).unwrap();
assert_eq!(f.mode(), FileOpenMode::ReadWrite);
}
#[test]
fn primary_hdu() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.fits");
let f = FitsFile::create(&path).open().unwrap();
let hdu = f.primary_hdu().unwrap();
assert_eq!(hdu.hdu_index, 0);
}
#[test]
fn num_hdus() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.fits");
let f = FitsFile::create(&path).open().unwrap();
assert_eq!(f.num_hdus().unwrap(), 1);
}
#[test]
fn create_image_extension() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.fits");
let mut f = FitsFile::create(&path).open().unwrap();
let desc = ImageDescription {
data_type: ImageType::Float,
dimensions: vec![10, 10],
};
let hdu = f.create_image("SCI", &desc).unwrap();
assert_eq!(hdu.hdu_index, 1);
assert_eq!(f.num_hdus().unwrap(), 2);
}
#[test]
fn hdu_by_name() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.fits");
let mut f = FitsFile::create(&path).open().unwrap();
let desc = ImageDescription {
data_type: ImageType::Float,
dimensions: vec![10],
};
f.create_image("SCI", &desc).unwrap();
let hdu = f.hdu("SCI").unwrap();
assert_eq!(hdu.hdu_index, 1);
}
#[test]
fn hdu_by_index() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.fits");
let f = FitsFile::create(&path).open().unwrap();
let hdu = f.hdu(0usize).unwrap();
assert_eq!(hdu.hdu_index, 0);
}
#[test]
fn hdu_not_found() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.fits");
let f = FitsFile::create(&path).open().unwrap();
assert!(f.hdu("MISSING").is_err());
}
#[test]
fn iter_hdus() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.fits");
let mut f = FitsFile::create(&path).open().unwrap();
let desc = ImageDescription {
data_type: ImageType::Short,
dimensions: vec![5],
};
f.create_image("EXT1", &desc).unwrap();
f.create_image("EXT2", &desc).unwrap();
let hdus = f.iter().unwrap();
assert_eq!(hdus.len(), 3);
}
}