mod gzip;
mod hcompress;
mod plio;
mod quantize;
mod rice;
mod table;
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::encode_be;
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)]
pub struct CompressOptions {
pub tile_shape: Vec<usize>,
pub gzip_level: u32,
pub hcompress_scale: i32,
pub quantize_level: f64,
}
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,
}
}
}
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", 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" => quantize::DitherMethod::None,
"SUBTRACTIVE_DITHER_1" => quantize::DitherMethod::Subtractive1,
"SUBTRACTIVE_DITHER_2" => quantize::DitherMethod::Subtractive2,
other => {
if is_float {
return Err(FitsError::UnsupportedCompression {
name: format!("float quantization {other}"),
});
}
quantize::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 codec = 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 {
cmptype: &cmptype,
zbitpix,
int_bitpix,
codec,
};
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")]
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>,
}
pub(crate) fn encode_image(
image: &Image,
cmptype: &str,
options: &CompressOptions,
out: &mut Vec<u8>,
) -> Result<Header> {
let bitpix = image.samples.bitpix();
if bitpix.is_float() {
return encode_float_image(image, cmptype, options, out);
}
if cmptype == "RICE_1" && bitpix.elem_size() > 4 {
return Err(FitsError::UnsupportedCompression {
name: "RICE_1 with BYTEPIX > 4 (64-bit pixels)".to_string(),
});
}
if cmptype == "HCOMPRESS_1" && 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 = 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 cmptype {
"GZIP_1" => {
i64_to_be_into(vals, bitpix, &mut s.be);
TileCell::Bytes(gzip::gzip_encode(&s.be, gzip_level))
}
"GZIP_2" => {
i64_to_be_into(vals, bitpix, &mut s.be);
TileCell::Bytes(gzip::gzip2_encode(&s.be, bytepix, gzip_level))
}
"RICE_1" => TileCell::Bytes(rice::rice_encode(vals, bytepix, 32)),
"PLIO_1" => TileCell::I16(plio::plio_encode(vals, vals.len())),
"HCOMPRESS_1" => TileCell::Bytes(hcompress::hcompress_tile_encode(
vals,
&s.tile.tdims,
scale,
)?),
"NOCOMPRESS" => TileCell::Bytes(i64_to_be(vals, bitpix)),
other => {
return Err(FitsError::UnsupportedCompression {
name: format!("{other} (write)"),
});
}
})
})?;
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 cmptype == "PLIO_1" { '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 cmptype {
"RICE_1" => {
h.set("ZNAME1", "BLOCKSIZE").set("ZVAL1", 32);
h.set("ZNAME2", "BYTEPIX").set("ZVAL2", bytepix as i64);
}
"HCOMPRESS_1" => {
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 encode_float_image(
image: &Image,
cmptype: &str,
options: &CompressOptions,
out: &mut Vec<u8>,
) -> Result<Header> {
if !matches!(cmptype, "GZIP_1" | "GZIP_2" | "RICE_1") {
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 = geom.ntiles();
let zdither0 = 1i64; let int_bitpix = Bitpix::I32; let method = quantize::DitherMethod::Subtractive1; 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 cmptype {
"GZIP_1" => {
i64_to_be_into(&s.ints, int_bitpix, &mut s.be);
gzip::gzip_encode(&s.be, gzip_level)
}
"GZIP_2" => {
i64_to_be_into(&s.ints, int_bitpix, &mut s.be);
gzip::gzip2_encode(&s.be, 4, gzip_level)
}
"RICE_1" => 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 cmptype == "RICE_1" {
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: quantize::DitherMethod) -> &'static str {
match method {
quantize::DitherMethod::None => "NO_DITHER",
quantize::DitherMethod::Subtractive1 => "SUBTRACTIVE_DITHER_1",
quantize::DitherMethod::Subtractive2 => "SUBTRACTIVE_DITHER_2",
}
}
fn gather_f64(samples: &ImageData, row_bases: &[usize], row_len: usize, out: &mut Vec<f64>) {
out.clear();
match samples {
ImageData::F32(v) => {
for &b in row_bases {
out.extend(v[b..b + row_len].iter().map(|&x| x as f64));
}
}
ImageData::F64(v) => {
for &b in row_bases {
out.extend_from_slice(&v[b..b + row_len]);
}
}
_ => {}
}
}
fn float_to_be(vals: &[f64], bitpix: Bitpix) -> Vec<u8> {
match bitpix {
Bitpix::F32 => encode_be(
&vals.iter().map(|&v| v as f32).collect::<Vec<_>>(),
f32::to_be_bytes,
),
_ => encode_be(vals, f64::to_be_bytes),
}
}
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]) -> Vec<usize> {
if tile_shape.len() == dims.len() {
tile_shape.iter().map(|&t| t.max(1)).collect()
} else {
(0..dims.len())
.map(|i| if i == 0 { dims[i].max(1) } else { 1 })
.collect()
}
}
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 gather_i64(samples: &ImageData, row_bases: &[usize], row_len: usize, out: &mut Vec<i64>) {
out.clear();
match samples {
ImageData::U8(v) => {
for &b in row_bases {
out.extend(v[b..b + row_len].iter().map(|&x| x as i64));
}
}
ImageData::I16(v) => {
for &b in row_bases {
out.extend(v[b..b + row_len].iter().map(|&x| x as i64));
}
}
ImageData::I32(v) => {
for &b in row_bases {
out.extend(v[b..b + row_len].iter().map(|&x| x as i64));
}
}
ImageData::I64(v) => {
for &b in row_bases {
out.extend_from_slice(&v[b..b + row_len]);
}
}
_ => {}
}
}
fn i64_to_be_into(vals: &[i64], bitpix: Bitpix, out: &mut Vec<u8>) {
out.clear();
out.resize(vals.len() * bitpix.elem_size(), 0);
match bitpix {
Bitpix::U8 => {
for (slot, &v) in out.iter_mut().zip(vals) {
*slot = v as u8;
}
}
Bitpix::I16 => {
for (slot, &v) in out.chunks_exact_mut(2).zip(vals) {
slot.copy_from_slice(&(v as i16).to_be_bytes());
}
}
Bitpix::I32 => {
for (slot, &v) in out.chunks_exact_mut(4).zip(vals) {
slot.copy_from_slice(&(v as i32).to_be_bytes());
}
}
Bitpix::I64 => {
for (slot, &v) in out.chunks_exact_mut(8).zip(vals) {
slot.copy_from_slice(&v.to_be_bytes());
}
}
_ => {}
}
}
fn i64_to_be(vals: &[i64], bitpix: Bitpix) -> Vec<u8> {
let mut out = Vec::new();
i64_to_be_into(vals, bitpix, &mut out);
out
}
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>,
}
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: quantize::DitherMethod,
irow: i64,
zblank: Option<i64>,
}
struct DecodeCtx<'a> {
cmptype: &'a str,
zbitpix: Bitpix,
int_bitpix: Bitpix,
codec: 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, 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) => {
be_floats_into(&gzip::gunzip(as_bytes(cell)?)?, 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 codec = ctx.codec;
match ctx.cmptype {
"GZIP_1" => gzip::gzip_tile_into(as_bytes(cell)?, ctx.int_bitpix, out),
"GZIP_2" => gzip::gzip2_tile_into(as_bytes(cell)?, ctx.int_bitpix, out),
"RICE_1" => {
if !matches!(codec.bytepix, 1 | 2 | 4) {
return Err(FitsError::UnsupportedCompression {
name: format!("RICE_1 with BYTEPIX = {} (only 1/2/4)", codec.bytepix),
});
}
rice::rice_decode_into(
as_bytes(cell)?,
tile_elems,
codec.bytepix,
codec.blocksize,
out,
);
Ok(())
}
"PLIO_1" => {
plio::plio_decode_into(as_i16(cell)?, tile_elems, out);
Ok(())
}
"HCOMPRESS_1" => {
hcompress::hcompress_tile_into(as_bytes(cell)?, codec.smooth, tile_elems, out)
}
"NOCOMPRESS" => {
be_to_i64_into(as_bytes(cell)?, ctx.int_bitpix, out);
Ok(())
}
other => Err(FitsError::UnsupportedCompression {
name: other.to_string(),
}),
}
}
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
}
#[derive(Debug)]
struct TileGeometry {
dims: Vec<usize>,
tiles: Vec<usize>,
stride: Vec<usize>,
ntiles_axis: Vec<usize>,
}
#[derive(Debug, Default)]
struct TileScratch {
origin: Vec<usize>,
tdims: Vec<usize>,
row_bases: Vec<usize>,
row_len: usize,
coord: Vec<usize>,
}
impl TileScratch {
fn nelem(&self) -> usize {
self.row_len * self.row_bases.len()
}
}
impl TileGeometry {
fn new(dims: &[usize], tiles: &[usize]) -> TileGeometry {
let n = dims.len();
let ntiles_axis = dims
.iter()
.zip(tiles)
.map(|(&d, &t)| d.div_ceil(t))
.collect();
let mut stride = vec![1usize; n];
for i in 1..n {
stride[i] = stride[i - 1] * dims[i - 1];
}
TileGeometry {
dims: dims.to_vec(),
tiles: tiles.to_vec(),
stride,
ntiles_axis,
}
}
fn ntiles(&self) -> usize {
self.ntiles_axis.iter().product()
}
fn tile_into(&self, t: usize, s: &mut TileScratch) {
let n = self.dims.len();
s.origin.clear();
s.tdims.clear();
let mut rem = t;
for i in 0..n {
let ti = rem % self.ntiles_axis[i];
rem /= self.ntiles_axis[i];
let origin = ti * self.tiles[i];
s.origin.push(origin);
s.tdims.push(self.tiles[i].min(self.dims[i] - origin));
}
s.row_len = if n == 0 { 1 } else { s.tdims[0] };
let nrows: usize = if n <= 1 {
1
} else {
s.tdims[1..].iter().product()
};
let mut flat: usize = (0..n).map(|i| s.origin[i] * self.stride[i]).sum();
s.row_bases.clear();
s.row_bases.reserve(nrows);
s.coord.clear();
s.coord.resize(n, 0);
for _ in 0..nrows {
s.row_bases.push(flat);
for i in 1..n {
s.coord[i] += 1;
flat += self.stride[i];
if s.coord[i] < s.tdims[i] {
break;
}
s.coord[i] = 0;
flat -= s.tdims[i] * self.stride[i];
}
}
}
}
fn read_axes(header: &Header, prefix: &str, n: usize) -> Result<Vec<usize>> {
(1..=n)
.map(|i| match header.get_integer(key!("{prefix}{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()
}
fn cell_len(cell: &ColumnData) -> usize {
match cell {
ColumnData::Bytes(v) => v.len(),
ColumnData::I16(v) => v.len(),
ColumnData::I32(v) => v.len(),
ColumnData::I64(v) => v.len(),
_ => 0,
}
}
fn as_bytes(cell: &ColumnData) -> Result<&[u8]> {
match cell {
ColumnData::Bytes(b) => Ok(b),
_ => Err(FitsError::UnsupportedCompression {
name: "compressed cell is not a byte array".to_string(),
}),
}
}
fn as_i16(cell: &ColumnData) -> Result<&[i16]> {
match cell {
ColumnData::I16(v) => Ok(v),
_ => Err(FitsError::UnsupportedCompression {
name: "PLIO_1 data is not an i16 list".to_string(),
}),
}
}
fn cell_to_i64_into(cell: &ColumnData, out: &mut Vec<i64>) {
out.clear();
match cell {
ColumnData::Bytes(v) => out.extend(v.iter().map(|&b| b as i64)),
ColumnData::I16(v) => out.extend(v.iter().map(|&x| x as i64)),
ColumnData::I32(v) => out.extend(v.iter().map(|&x| x as i64)),
ColumnData::I64(v) => out.extend_from_slice(v),
_ => {}
}
}
fn cell_to_f64_into(cell: &ColumnData, zbitpix: Bitpix, out: &mut Vec<f64>) {
out.clear();
match cell {
ColumnData::F32(v) => out.extend(v.iter().map(|&x| x as f64)),
ColumnData::F64(v) => out.extend_from_slice(v),
ColumnData::Bytes(b) => be_floats_into(b, zbitpix, out),
_ => {}
}
}
fn be_to_i64_into(bytes: &[u8], bitpix: Bitpix, out: &mut Vec<i64>) {
out.clear();
match bitpix {
Bitpix::U8 => out.extend(bytes.iter().map(|&b| b as i64)),
Bitpix::I16 => out.extend(
bytes
.chunks_exact(2)
.map(|c| i16::from_be_bytes(c.try_into().unwrap()) as i64),
),
Bitpix::I32 => out.extend(
bytes
.chunks_exact(4)
.map(|c| i32::from_be_bytes(c.try_into().unwrap()) as i64),
),
Bitpix::I64 => out.extend(
bytes
.chunks_exact(8)
.map(|c| i64::from_be_bytes(c.try_into().unwrap())),
),
Bitpix::F32 | Bitpix::F64 => {} }
}
fn be_floats_into(bytes: &[u8], bitpix: Bitpix, out: &mut Vec<f64>) {
out.clear();
match bitpix {
Bitpix::F32 => out.extend(
bytes
.chunks_exact(4)
.map(|c| f32::from_be_bytes(c.try_into().unwrap()) as f64),
),
Bitpix::F64 => out.extend(
bytes
.chunks_exact(8)
.map(|c| f64::from_be_bytes(c.try_into().unwrap())),
),
_ => {}
}
}
fn bytepix_to_bitpix(bytepix: usize) -> Bitpix {
match bytepix {
1 => Bitpix::U8,
2 => Bitpix::I16,
8 => Bitpix::I64,
_ => Bitpix::I32,
}
}
fn zeroed_samples(bitpix: Bitpix, len: usize) -> ImageData {
match bitpix {
Bitpix::U8 => ImageData::U8(vec![0; len]),
Bitpix::I16 => ImageData::I16(vec![0; len]),
Bitpix::I32 => ImageData::I32(vec![0; len]),
Bitpix::I64 => ImageData::I64(vec![0; len]),
Bitpix::F32 => ImageData::F32(vec![0.0; len]),
Bitpix::F64 => ImageData::F64(vec![0.0; len]),
}
}
#[cfg(test)]
mod tests;