mod convert;
mod geometry;
mod gzip;
mod hcompress;
mod plio;
mod quantize;
mod rice;
mod table;
use convert::as_bytes;
use convert::as_i16;
use convert::be_floats_into;
use convert::be_to_i64_into;
use convert::bytepix_to_bitpix;
use convert::cell_len;
use convert::cell_to_f64_into;
use convert::cell_to_i64_into;
use convert::float_to_be;
use convert::gather_f64;
use convert::gather_i64;
use convert::i64_to_be;
use convert::i64_to_be_into;
use convert::zeroed_samples;
use geometry::TileGeometry;
use geometry::TileScratch;
pub(crate) use table::compress_table;
pub(crate) use table::uncompress_table;
use crate::bitpix::Bitpix;
use crate::data::Image;
use crate::data::ImageData;
use crate::data::Scaling;
use crate::endian::push_pq_descriptor;
use crate::error::FitsError;
use crate::error::Result;
use crate::header::Header;
use crate::keyword::key;
use crate::table::BinTable;
use crate::table::ColumnData;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DitherMethod {
None,
#[default]
Subtractive1,
Subtractive2,
}
impl DitherMethod {
pub(crate) fn dithered(self) -> bool {
!matches!(self, DitherMethod::None)
}
}
#[derive(Debug, Clone)]
pub struct CompressOptions {
pub tile_shape: Vec<usize>,
pub gzip_level: u32,
pub hcompress_scale: i32,
pub quantize_level: f64,
pub dither: DitherMethod,
}
impl Default for CompressOptions {
fn default() -> CompressOptions {
CompressOptions {
tile_shape: Vec::new(),
gzip_level: gzip::DEFAULT_GZIP_LEVEL,
hcompress_scale: 0,
quantize_level: 0.0,
dither: DitherMethod::Subtractive1,
}
}
}
impl CompressOptions {
pub fn tiled(tile_shape: impl Into<Vec<usize>>) -> CompressOptions {
CompressOptions {
tile_shape: tile_shape.into(),
..CompressOptions::default()
}
}
}
#[derive(Debug)]
pub(crate) struct HduParts {
pub header: Header,
pub data: Vec<u8>,
}
#[cfg(feature = "parallel")]
pub(crate) fn map_tiles<S, T, I, F>(ntiles: usize, init: I, f: F) -> Result<Vec<T>>
where
S: Send,
T: Send,
I: Fn() -> S + Sync + Send,
F: Fn(&mut S, usize) -> Result<T> + Sync + Send,
{
use rayon::prelude::*;
(0..ntiles)
.into_par_iter()
.map_init(init, |scratch, t| f(scratch, t))
.collect()
}
#[cfg(not(feature = "parallel"))]
pub(crate) fn map_tiles<S, T, I, F>(ntiles: usize, init: I, f: F) -> Result<Vec<T>>
where
I: FnOnce() -> S,
F: Fn(&mut S, usize) -> Result<T>,
{
let mut scratch = init();
(0..ntiles).map(|t| f(&mut scratch, t)).collect()
}
pub(crate) fn decompress_image(header: &Header, table: &BinTable) -> Result<Image> {
if header.get_logical("ZIMAGE") != Some(true) {
return Err(FitsError::NotCompressedImage);
}
let zbitpix = Bitpix::from_code(
header
.get_integer("ZBITPIX")
.ok_or(FitsError::MissingKeyword { name: "ZBITPIX" })?,
)?;
let is_float = zbitpix.is_float();
let cmptype = header
.get_text("ZCMPTYPE")
.ok_or(FitsError::MissingKeyword { name: "ZCMPTYPE" })?
.to_string();
let znaxis = header
.get_integer("ZNAXIS")
.ok_or(FitsError::MissingKeyword { name: "ZNAXIS" })?;
if !(0..=999).contains(&znaxis) {
return Err(FitsError::KeywordOutOfRange { name: "ZNAXIS" });
}
let znaxis = znaxis as usize;
let dims = read_axes(header, znaxis)?;
if dims.is_empty() {
return Ok(Image {
shape: dims,
samples: zeroed_samples(zbitpix, 0)?,
scaling: Scaling::from_header(header),
});
}
let total = dims
.iter()
.try_fold(1usize, |acc, &n| acc.checked_mul(n))
.ok_or(FitsError::DataUnitOverflow)?;
let tiles: Vec<usize> = (1..=znaxis)
.map(|i| {
header
.get_integer(key!("ZTILE{i}").as_str())
.map(|v| v.max(1) as usize)
.unwrap_or(if i == 1 { dims[0] } else { 1 })
})
.collect();
let rice = rice::rice_params(header, zbitpix);
let int_bitpix = if is_float {
bytepix_to_bitpix(rice.bytepix)
} else {
zbitpix
};
let zquantiz = header
.get_text("ZQUANTIZ")
.unwrap_or("NO_DITHER")
.to_string();
let method = match zquantiz.as_str() {
"NO_DITHER" => DitherMethod::None,
"SUBTRACTIVE_DITHER_1" => DitherMethod::Subtractive1,
"SUBTRACTIVE_DITHER_2" => DitherMethod::Subtractive2,
other => {
if is_float {
return Err(FitsError::UnsupportedCompression {
name: format!("float quantization {other}"),
});
}
DitherMethod::None
}
};
let zdither0 = header.get_integer("ZDITHER0").unwrap_or(1);
let zblank_keyword = header.get_integer("ZBLANK");
let zblank_column = read_i64_column(table, "ZBLANK");
let smooth = hcompress_smooth(header);
let params = CodecParams {
blocksize: rice.blocksize,
bytepix: rice.bytepix,
smooth,
};
let primary = read_tiles(table, "COMPRESSED_DATA")?;
let gzip_fallback = read_tiles(table, "GZIP_COMPRESSED_DATA")?;
let uncompressed = read_tiles(table, "UNCOMPRESSED_DATA")?;
let zscale = read_f64_column(table, "ZSCALE");
let zzero = read_f64_column(table, "ZZERO");
let geom = TileGeometry::new(&dims, &tiles);
let ntiles = geom.ntiles();
let mut samples = zeroed_samples(zbitpix, total)?;
let ctx = DecodeCtx {
codec: ImageCodec::parse(&cmptype)?,
zbitpix,
int_bitpix,
params,
};
if is_float {
let decode = |t: usize, s: &TileScratch, out: &mut Vec<f64>, ints: &mut Vec<i64>| {
let cols = TileColumns {
primary: primary.get(t),
gzip: gzip_fallback.get(t),
uncompressed: uncompressed.get(t),
};
let dq = Dequant {
scale: column_at(&zscale, t).unwrap_or(1.0),
zero: column_at(&zzero, t).unwrap_or(0.0),
method,
irow: t as i64 + zdither0,
zblank: column_at(&zblank_column, t).or(zblank_keyword),
};
decode_float_tile_into(&ctx, cols, s.nelem(), dq, out, ints)
};
match &mut samples {
ImageData::F32(o) => run_decode_scatter(ntiles, &geom, o, decode, |v| v as f32)?,
ImageData::F64(o) => run_decode_scatter(ntiles, &geom, o, decode, |v| v)?,
_ => unreachable!("a float ZBITPIX yields a float sample buffer"),
}
} else {
let decode = |t: usize, s: &TileScratch, out: &mut Vec<i64>, _ints: &mut Vec<i64>| {
let cols = TileColumns {
primary: primary.get(t),
gzip: gzip_fallback.get(t),
uncompressed: uncompressed.get(t),
};
decode_one_tile_into(&ctx, cols, s.nelem(), out)
};
match &mut samples {
ImageData::U8(o) => run_decode_scatter(ntiles, &geom, o, decode, |v| v as u8)?,
ImageData::I16(o) => run_decode_scatter(ntiles, &geom, o, decode, |v| v as i16)?,
ImageData::I32(o) => run_decode_scatter(ntiles, &geom, o, decode, |v| v as i32)?,
ImageData::I64(o) => run_decode_scatter(ntiles, &geom, o, decode, |v| v)?,
_ => unreachable!("an integer ZBITPIX yields an integer sample buffer"),
}
}
Ok(Image {
shape: dims,
samples,
scaling: Scaling::from_header(header),
})
}
fn run_decode_scatter<S, D>(
ntiles: usize,
geom: &TileGeometry,
out: &mut [D],
decode: impl Fn(usize, &TileScratch, &mut Vec<S>, &mut Vec<i64>) -> Result<()> + Sync + Send,
convert: impl Fn(S) -> D + Sync + Send,
) -> Result<()>
where
S: Copy + Send,
{
#[cfg(feature = "parallel")]
{
let sink = DisjointOut::new(out);
let init = || (TileScratch::default(), Vec::<S>::new(), Vec::<i64>::new());
map_tiles(ntiles, init, |(scratch, vals, ints), t| -> Result<()> {
geom.tile_into(t, scratch);
decode(t, scratch, vals, ints)?;
unsafe { sink.scatter_rows(&scratch.row_bases, scratch.row_len, vals, &convert) };
Ok(())
})?;
Ok(())
}
#[cfg(not(feature = "parallel"))]
{
let mut scratch = TileScratch::default();
let mut vals: Vec<S> = Vec::new();
let mut ints: Vec<i64> = Vec::new();
for t in 0..ntiles {
geom.tile_into(t, &mut scratch);
decode(t, &scratch, &mut vals, &mut ints)?;
scatter_rows(out, &scratch.row_bases, scratch.row_len, &vals, &convert);
}
Ok(())
}
}
#[cfg(not(feature = "parallel"))]
fn scatter_rows<S: Copy, D>(
out: &mut [D],
row_bases: &[usize],
row_len: usize,
vals: &[S],
convert: &impl Fn(S) -> D,
) {
let mut off = 0;
for &base in row_bases {
if off >= vals.len() {
break;
}
let rl = row_len.min(vals.len() - off);
for (d, &v) in out[base..base + rl].iter_mut().zip(&vals[off..off + rl]) {
*d = convert(v);
}
off += row_len;
}
}
#[cfg(feature = "parallel")]
#[derive(Debug)]
struct DisjointOut<D> {
ptr: *mut D,
len: usize,
}
#[cfg(feature = "parallel")]
unsafe impl<D> Sync for DisjointOut<D> {}
#[cfg(feature = "parallel")]
impl<D> DisjointOut<D> {
fn new(out: &mut [D]) -> DisjointOut<D> {
DisjointOut {
ptr: out.as_mut_ptr(),
len: out.len(),
}
}
unsafe fn scatter_rows<S: Copy>(
&self,
row_bases: &[usize],
row_len: usize,
vals: &[S],
convert: &impl Fn(S) -> D,
) {
let mut off = 0;
for &base in row_bases {
if off >= vals.len() {
break;
}
let rl = row_len.min(vals.len() - off);
debug_assert!(base + rl <= self.len, "tile row out of bounds {}", self.len);
let dst = unsafe { std::slice::from_raw_parts_mut(self.ptr.add(base), rl) };
for (d, &v) in dst.iter_mut().zip(&vals[off..off + rl]) {
*d = convert(v);
}
off += row_len;
}
}
}
#[derive(Debug, Default)]
struct EncodeScratch {
tile: TileScratch,
ints: Vec<i64>,
floats: Vec<f64>,
be: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ImageCodec {
Gzip1,
Gzip2,
Rice1,
Plio1,
Hcompress1,
NoCompress,
}
impl ImageCodec {
fn parse(s: &str) -> Result<ImageCodec> {
Ok(match s {
"GZIP_1" => ImageCodec::Gzip1,
"GZIP_2" => ImageCodec::Gzip2,
"RICE_1" => ImageCodec::Rice1,
"PLIO_1" => ImageCodec::Plio1,
"HCOMPRESS_1" => ImageCodec::Hcompress1,
"NOCOMPRESS" => ImageCodec::NoCompress,
other => {
return Err(FitsError::UnsupportedCompression {
name: other.to_string(),
});
}
})
}
}
pub(crate) fn compress_image(
image: &Image,
cmptype: &str,
options: &CompressOptions,
out: &mut Vec<u8>,
) -> Result<Header> {
let bitpix = image.samples.bitpix();
if bitpix.is_float() {
return compress_float_image(image, cmptype, options, out);
}
let codec = ImageCodec::parse(cmptype)?;
if codec == ImageCodec::Rice1 && bitpix.elem_size() > 4 {
return Err(FitsError::UnsupportedCompression {
name: "RICE_1 with BYTEPIX > 4 (64-bit pixels)".to_string(),
});
}
if codec == ImageCodec::Hcompress1 && bitpix.elem_size() > 4 {
return Err(FitsError::UnsupportedCompression {
name: "HCOMPRESS_1 with 64-bit pixels".to_string(),
});
}
let dims = &image.shape;
let tiles = resolve_tile_shape(dims, &options.tile_shape)?;
let geom = TileGeometry::new(dims, &tiles);
let ntiles = if dims.is_empty() { 0 } else { geom.ntiles() };
let bytepix = bitpix.elem_size();
let (gzip_level, scale) = (options.gzip_level, options.hcompress_scale);
let cells = map_tiles(ntiles, EncodeScratch::default, |s, t| -> Result<TileCell> {
geom.tile_into(t, &mut s.tile);
gather_i64(
&image.samples,
&s.tile.row_bases,
s.tile.row_len,
&mut s.ints,
);
let vals = &s.ints;
Ok(match codec {
ImageCodec::Gzip1 => {
i64_to_be_into(vals, bitpix, &mut s.be);
TileCell::Bytes(gzip::gzip_encode(&s.be, gzip_level))
}
ImageCodec::Gzip2 => {
i64_to_be_into(vals, bitpix, &mut s.be);
TileCell::Bytes(gzip::gzip2_encode(&s.be, bytepix, gzip_level))
}
ImageCodec::Rice1 => TileCell::Bytes(rice::rice_encode(vals, bytepix, 32)),
ImageCodec::Plio1 => TileCell::I16(plio::plio_encode(vals, vals.len())),
ImageCodec::Hcompress1 => TileCell::Bytes(hcompress::hcompress_tile_encode(
vals,
&s.tile.tdims,
scale,
)?),
ImageCodec::NoCompress => TileCell::Bytes(i64_to_be(vals, bitpix)),
})
})?;
let mut descriptors: Vec<(usize, usize)> = Vec::with_capacity(ntiles);
let mut heap: Vec<u8> = Vec::new();
for cell in &cells {
descriptors.push((cell.nelem(), heap.len()));
cell.extend_heap(&mut heap);
}
let maxnelem = descriptors.iter().map(|&(n, _)| n).max().unwrap_or(0);
let wide = heap.len() > i32::MAX as usize || maxnelem > i32::MAX as usize;
out.clear();
out.reserve(ntiles * if wide { 16 } else { 8 } + heap.len());
for &(nelem, offset) in &descriptors {
push_pq_descriptor(out, wide, nelem as u64, offset as u64);
}
out.extend_from_slice(&heap);
let tform_letter = if codec == ImageCodec::Plio1 { 'I' } else { 'B' };
let desc = if wide { 'Q' } else { 'P' };
let mut h = Header::new();
h.set("XTENSION", "BINTABLE")
.comment("XTENSION", "binary table extension");
h.set("BITPIX", 8).set("NAXIS", 2);
h.set("NAXIS1", if wide { 16 } else { 8 })
.set("NAXIS2", ntiles as i64);
h.set("PCOUNT", heap.len() as i64).set("GCOUNT", 1);
h.set("TFIELDS", 1);
h.set("TTYPE1", "COMPRESSED_DATA");
h.set("TFORM1", format!("1{desc}{tform_letter}({maxnelem})"));
set_zimage_axes(&mut h, cmptype, bitpix, dims, &tiles);
match codec {
ImageCodec::Rice1 => {
h.set("ZNAME1", "BLOCKSIZE").set("ZVAL1", 32);
h.set("ZNAME2", "BYTEPIX").set("ZVAL2", bytepix as i64);
}
ImageCodec::Hcompress1 => {
h.set("ZNAME1", "SCALE").set("ZVAL1", scale as i64);
h.set("ZNAME2", "SMOOTH").set("ZVAL2", 0);
}
_ => {}
}
if !image.scaling.is_identity() {
h.set("BZERO", image.scaling.bzero);
h.set("BSCALE", image.scaling.bscale);
}
if let Some(blank) = image.scaling.blank {
h.set("BLANK", blank);
}
Ok(h)
}
#[derive(Debug)]
struct FloatTile {
bytes: Vec<u8>,
zscale: f64,
zzero: f64,
quantized: bool,
has_null: bool,
}
fn compress_float_image(
image: &Image,
cmptype: &str,
options: &CompressOptions,
out: &mut Vec<u8>,
) -> Result<Header> {
let codec = ImageCodec::parse(cmptype)?;
if !matches!(
codec,
ImageCodec::Gzip1 | ImageCodec::Gzip2 | ImageCodec::Rice1
) {
return Err(FitsError::UnsupportedCompression {
name: format!("{cmptype} for float images (write)"),
});
}
let zbitpix = image.samples.bitpix();
let dims = &image.shape;
let tiles = resolve_tile_shape(dims, &options.tile_shape)?;
let geom = TileGeometry::new(dims, &tiles);
let ntiles = if dims.is_empty() { 0 } else { geom.ntiles() };
let zdither0 = 1i64; let int_bitpix = Bitpix::I32; let method = options.dither;
let (gzip_level, qlevel) = (options.gzip_level, options.quantize_level);
let tiles_out = map_tiles(
ntiles,
EncodeScratch::default,
|s, t| -> Result<FloatTile> {
geom.tile_into(t, &mut s.tile);
let nx = s.tile.tdims[0];
let ny = s.tile.row_bases.len();
gather_f64(
&image.samples,
&s.tile.row_bases,
s.tile.row_len,
&mut s.floats,
);
let irow = t as i64 + zdither0; Ok(
match quantize::quantize_tile(&s.floats, nx, ny, qlevel, method, irow) {
Some(q) => {
s.ints.clear();
s.ints.extend(q.idata.iter().map(|&v| v as i64));
let bytes = match codec {
ImageCodec::Gzip1 => {
i64_to_be_into(&s.ints, int_bitpix, &mut s.be);
gzip::gzip_encode(&s.be, gzip_level)
}
ImageCodec::Gzip2 => {
i64_to_be_into(&s.ints, int_bitpix, &mut s.be);
gzip::gzip2_encode(&s.be, 4, gzip_level)
}
ImageCodec::Rice1 => rice::rice_encode(&s.ints, 4, 32),
_ => unreachable!(),
};
FloatTile {
bytes,
zscale: q.bscale,
zzero: q.bzero,
quantized: true,
has_null: q.has_null,
}
}
None => FloatTile {
bytes: gzip::gzip_encode(&float_to_be(&s.floats, zbitpix), gzip_level),
zscale: 0.0,
zzero: 0.0,
quantized: false,
has_null: false,
},
},
)
},
)?;
let mut cd_desc: Vec<(usize, usize)> = Vec::with_capacity(ntiles);
let mut gz_desc: Vec<(usize, usize)> = Vec::with_capacity(ntiles);
let mut zscale = vec![0f64; ntiles];
let mut zzero = vec![0f64; ntiles];
let mut heap: Vec<u8> = Vec::new();
let mut any_null = false;
for (t, tile) in tiles_out.iter().enumerate() {
zscale[t] = tile.zscale;
zzero[t] = tile.zzero;
any_null |= tile.has_null;
let (cd, gz) = if tile.quantized {
((tile.bytes.len(), heap.len()), (0, heap.len()))
} else {
((0, heap.len()), (tile.bytes.len(), heap.len()))
};
cd_desc.push(cd);
gz_desc.push(gz);
heap.extend_from_slice(&tile.bytes);
}
out.clear();
out.reserve(ntiles * 32 + heap.len());
for t in 0..ntiles {
push_pq_descriptor(out, false, cd_desc[t].0 as u64, cd_desc[t].1 as u64);
push_pq_descriptor(out, false, gz_desc[t].0 as u64, gz_desc[t].1 as u64);
out.extend_from_slice(&zscale[t].to_be_bytes());
out.extend_from_slice(&zzero[t].to_be_bytes());
}
out.extend_from_slice(&heap);
let max_cd = cd_desc.iter().map(|&(n, _)| n).max().unwrap_or(0);
let max_gz = gz_desc.iter().map(|&(n, _)| n).max().unwrap_or(0);
let mut h = Header::new();
h.set("XTENSION", "BINTABLE")
.comment("XTENSION", "binary table extension");
h.set("BITPIX", 8).set("NAXIS", 2);
h.set("NAXIS1", 32).set("NAXIS2", ntiles as i64);
h.set("PCOUNT", heap.len() as i64).set("GCOUNT", 1);
h.set("TFIELDS", 4);
h.set("TTYPE1", "COMPRESSED_DATA")
.set("TFORM1", format!("1PB({max_cd})"));
h.set("TTYPE2", "GZIP_COMPRESSED_DATA")
.set("TFORM2", format!("1PB({max_gz})"));
h.set("TTYPE3", "ZSCALE").set("TFORM3", "1D");
h.set("TTYPE4", "ZZERO").set("TFORM4", "1D");
set_zimage_axes(&mut h, cmptype, zbitpix, dims, &tiles);
if codec == ImageCodec::Rice1 {
h.set("ZNAME1", "BLOCKSIZE").set("ZVAL1", 32);
h.set("ZNAME2", "BYTEPIX").set("ZVAL2", 4);
} else {
h.set("ZNAME1", "BYTEPIX").set("ZVAL1", 4);
}
h.set("ZQUANTIZ", dither_name(method));
h.set("ZDITHER0", zdither0);
if any_null {
h.set("ZBLANK", quantize::NULL_VALUE as i64);
}
Ok(h)
}
fn dither_name(method: DitherMethod) -> &'static str {
match method {
DitherMethod::None => "NO_DITHER",
DitherMethod::Subtractive1 => "SUBTRACTIVE_DITHER_1",
DitherMethod::Subtractive2 => "SUBTRACTIVE_DITHER_2",
}
}
#[derive(Debug)]
enum TileCell {
Bytes(Vec<u8>),
I16(Vec<i16>),
}
impl TileCell {
fn nelem(&self) -> usize {
match self {
TileCell::Bytes(b) => b.len(),
TileCell::I16(v) => v.len(),
}
}
fn extend_heap(&self, heap: &mut Vec<u8>) {
match self {
TileCell::Bytes(b) => heap.extend_from_slice(b),
TileCell::I16(v) => {
for &x in v {
heap.extend_from_slice(&x.to_be_bytes());
}
}
}
}
}
fn resolve_tile_shape(dims: &[usize], tile_shape: &[usize]) -> Result<Vec<usize>> {
if tile_shape.is_empty() {
Ok((0..dims.len())
.map(|i| if i == 0 { dims[i].max(1) } else { 1 })
.collect())
} else if tile_shape.len() == dims.len() {
Ok(tile_shape.iter().map(|&t| t.max(1)).collect())
} else {
Err(FitsError::TileShapeRankMismatch {
tile_rank: tile_shape.len(),
image_rank: dims.len(),
})
}
}
fn set_zimage_axes(
h: &mut Header,
cmptype: &str,
zbitpix: Bitpix,
dims: &[usize],
tiles: &[usize],
) {
h.set("ZIMAGE", true)
.comment("ZIMAGE", "this is a tiled-compressed image");
h.set("ZCMPTYPE", cmptype);
h.set("ZBITPIX", zbitpix.code());
h.set("ZNAXIS", dims.len() as i64);
for (i, &n) in dims.iter().enumerate() {
h.set(key!("ZNAXIS{}", i + 1).as_str(), n as i64);
}
for (i, &t) in tiles.iter().enumerate() {
h.set(key!("ZTILE{}", i + 1).as_str(), t as i64);
}
}
fn read_tiles(table: &BinTable, name: &str) -> Result<Vec<ColumnData>> {
match table.column_index(name) {
Some(c) => table.column_by_idx(c)?.vla(),
None => Ok(Vec::new()),
}
}
fn read_f64_column(table: &BinTable, name: &str) -> Option<Vec<f64>> {
let c = table.column_index(name)?;
match table.column_by_idx(c).and_then(|col| col.raw()) {
Ok(ColumnData::F64(v)) => Some(v),
_ => None,
}
}
fn read_i64_column(table: &BinTable, name: &str) -> Option<Vec<i64>> {
let c = table.column_index(name)?;
match table.column_by_idx(c).and_then(|col| col.raw()) {
Ok(ColumnData::Bytes(v)) => Some(v.iter().map(|&x| x as i64).collect()),
Ok(ColumnData::I16(v)) => Some(v.iter().map(|&x| x as i64).collect()),
Ok(ColumnData::I32(v)) => Some(v.iter().map(|&x| x as i64).collect()),
Ok(ColumnData::I64(v)) => Some(v),
_ => None,
}
}
fn column_at<T: Copy>(col: &Option<Vec<T>>, t: usize) -> Option<T> {
col.as_ref().and_then(|v| v.get(t).copied())
}
#[derive(Debug, Clone, Copy)]
struct TileColumns<'a> {
primary: Option<&'a ColumnData>,
gzip: Option<&'a ColumnData>,
uncompressed: Option<&'a ColumnData>,
}
#[derive(Debug)]
enum TileSource<'a> {
Compressed(&'a ColumnData),
Gzip(&'a ColumnData),
Uncompressed(&'a ColumnData),
}
impl<'a> TileColumns<'a> {
fn resolve(&self) -> Result<TileSource<'a>> {
if let Some(c) = self.primary.filter(|c| cell_len(c) > 0) {
Ok(TileSource::Compressed(c))
} else if let Some(c) = self.gzip.filter(|c| cell_len(c) > 0) {
Ok(TileSource::Gzip(c))
} else if let Some(c) = self.uncompressed.filter(|c| cell_len(c) > 0) {
Ok(TileSource::Uncompressed(c))
} else {
Err(FitsError::UnsupportedCompression {
name: "empty tile (no compressed or uncompressed data)".to_string(),
})
}
}
}
#[derive(Debug, Clone, Copy)]
struct CodecParams {
blocksize: usize,
bytepix: usize,
smooth: bool,
}
#[derive(Debug)]
struct Dequant {
scale: f64,
zero: f64,
method: DitherMethod,
irow: i64,
zblank: Option<i64>,
}
#[derive(Debug)]
struct DecodeCtx {
codec: ImageCodec,
zbitpix: Bitpix,
int_bitpix: Bitpix,
params: CodecParams,
}
fn decode_one_tile_into(
ctx: &DecodeCtx,
cols: TileColumns,
tile_elems: usize,
out: &mut Vec<i64>,
) -> Result<()> {
match cols.resolve()? {
TileSource::Compressed(cell) => decode_tile_cell_into(ctx, cell, tile_elems, out),
TileSource::Gzip(cell) => {
gzip::gzip_tile_into(as_bytes(cell)?, ctx.int_bitpix, tile_elems, out)
}
TileSource::Uncompressed(cell) => {
cell_to_i64_into(cell, out);
Ok(())
}
}
}
fn decode_float_tile_into(
ctx: &DecodeCtx,
cols: TileColumns,
tile_elems: usize,
dq: Dequant,
out: &mut Vec<f64>,
ints: &mut Vec<i64>,
) -> Result<()> {
match cols.resolve()? {
TileSource::Compressed(cell) => {
decode_tile_cell_into(ctx, cell, tile_elems, ints)?;
quantize::dequantize_into(ints, dq.scale, dq.zero, dq.method, dq.irow, dq.zblank, out);
Ok(())
}
TileSource::Gzip(cell) => {
let max = tile_elems.saturating_mul(ctx.zbitpix.elem_size());
be_floats_into(&gzip::gunzip(as_bytes(cell)?, max)?, ctx.zbitpix, out);
Ok(())
}
TileSource::Uncompressed(cell) => {
cell_to_f64_into(cell, ctx.zbitpix, out);
Ok(())
}
}
}
fn decode_tile_cell_into(
ctx: &DecodeCtx,
cell: &ColumnData,
tile_elems: usize,
out: &mut Vec<i64>,
) -> Result<()> {
let params = ctx.params;
match ctx.codec {
ImageCodec::Gzip1 => gzip::gzip_tile_into(as_bytes(cell)?, ctx.int_bitpix, tile_elems, out),
ImageCodec::Gzip2 => {
gzip::gzip2_tile_into(as_bytes(cell)?, ctx.int_bitpix, tile_elems, out)
}
ImageCodec::Rice1 => {
if !matches!(params.bytepix, 1 | 2 | 4) {
return Err(FitsError::UnsupportedCompression {
name: format!("RICE_1 with BYTEPIX = {} (only 1/2/4)", params.bytepix),
});
}
rice::rice_decode_into(
as_bytes(cell)?,
tile_elems,
params.bytepix,
params.blocksize,
out,
);
Ok(())
}
ImageCodec::Plio1 => {
plio::plio_decode_into(as_i16(cell)?, tile_elems, out);
Ok(())
}
ImageCodec::Hcompress1 => {
hcompress::hcompress_tile_into(as_bytes(cell)?, params.smooth, tile_elems, out)
}
ImageCodec::NoCompress => {
be_to_i64_into(as_bytes(cell)?, ctx.int_bitpix, out);
Ok(())
}
}
}
fn hcompress_smooth(header: &Header) -> bool {
let mut i = 1;
while let Some(name) = header.get_text(key!("ZNAME{i}").as_str()) {
if name == "SMOOTH" {
return header.get_integer(key!("ZVAL{i}").as_str()).unwrap_or(0) != 0;
}
i += 1;
}
false
}
fn read_axes(header: &Header, n: usize) -> Result<Vec<usize>> {
(1..=n)
.map(|i| match header.get_integer(key!("ZNAXIS{i}").as_str()) {
Some(v) if v >= 0 => Ok(v as usize),
Some(_) => Err(FitsError::KeywordOutOfRange { name: "ZNAXISn" }),
None => Err(FitsError::MissingKeyword { name: "ZNAXISn" }),
})
.collect()
}
#[cfg(test)]
mod tests;