use super::HduParts;
use super::gzip;
use super::map_tiles;
use super::rice;
use crate::error::FitsError;
use crate::error::Result;
use crate::header::Header;
use crate::keyword::key;
use crate::table::BinTable;
use crate::table::ColumnData;
use crate::table::Tform;
use crate::table::TformKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Algo {
Gzip1,
Gzip2,
Rice1,
}
impl Algo {
fn name(self) -> &'static str {
match self {
Algo::Gzip1 => "GZIP_1",
Algo::Gzip2 => "GZIP_2",
Algo::Rice1 => "RICE_1",
}
}
fn parse(s: &str) -> Result<Algo> {
match s {
"GZIP_1" => Ok(Algo::Gzip1),
"GZIP_2" => Ok(Algo::Gzip2),
"RICE_1" => Ok(Algo::Rice1),
other => Err(FitsError::UnsupportedCompression {
name: format!("table column codec {other}"),
}),
}
}
}
struct ColMeta {
kind: TformKind,
elem_size: usize,
repeat: usize,
width: usize,
offset: usize,
algo: Algo,
}
impl ColMeta {
fn shuffle_width(&self) -> usize {
match self.kind {
TformKind::I16 | TformKind::I32 | TformKind::F32 | TformKind::I64 | TformKind::F64 => {
self.elem_size
}
_ => 1,
}
}
fn rice_bytepix(&self) -> Option<usize> {
match self.kind {
TformKind::Byte => Some(1),
TformKind::I16 => Some(2),
TformKind::I32 => Some(4),
_ => None,
}
}
}
fn pick_algo(kind: TformKind, requested: Algo) -> Algo {
match kind {
TformKind::Logical
| TformKind::Bit
| TformKind::Char
| TformKind::ComplexF32
| TformKind::ComplexF64 => {
if requested == Algo::Gzip2 {
Algo::Gzip2
} else {
Algo::Gzip1
}
}
TformKind::F32 | TformKind::F64 | TformKind::I64 => {
if requested == Algo::Gzip1 {
Algo::Gzip1
} else {
Algo::Gzip2
}
}
TformKind::I16 | TformKind::I32 | TformKind::Byte => requested,
TformKind::ArrayDesc32 | TformKind::ArrayDesc64 => requested,
}
}
fn col_meta(tform: &Tform, offset: usize, algo: Algo) -> Result<ColMeta> {
if matches!(tform.kind, TformKind::ArrayDesc32 | TformKind::ArrayDesc64) {
return Err(FitsError::UnsupportedCompression {
name: "variable-length column in a compressed table".to_string(),
});
}
let elem_size = tform.kind.elem_size();
let width = tform.byte_width();
let repeat = if width == 0 { 0 } else { width / elem_size };
Ok(ColMeta {
kind: tform.kind,
elem_size,
repeat,
width,
offset,
algo: pick_algo(tform.kind, algo),
})
}
pub(crate) fn compress_table(
header: &Header,
table: &BinTable,
rows_per_tile: usize,
default_algo: &str,
out: &mut Vec<u8>,
) -> Result<Header> {
let default_algo = Algo::parse(default_algo)?;
let ncols = table.columns.len();
let nrows = table.nrows;
let naxis1 = table.row_len;
let raw = table.raw_rows();
let metas: Vec<ColMeta> = table
.columns
.iter()
.map(|c| col_meta(&c.tform, c.byte_offset, default_algo))
.collect::<Result<_>>()?;
let rpt = rows_per_tile.clamp(1, nrows.max(1));
let nchunks = nrows.div_ceil(rpt);
let comps = map_tiles(
nchunks * ncols,
Vec::<u8>::new,
|cm, i| -> Result<Vec<u8>> {
let chunk = i / ncols;
let m = &metas[i % ncols];
let r0 = chunk * rpt;
let rows = rpt.min(nrows - r0);
cm.clear();
cm.reserve(rows * m.width);
for r in 0..rows {
let off = (r0 + r) * naxis1 + m.offset;
cm.extend_from_slice(&raw[off..off + m.width]);
}
compress_column(cm, m)
},
)?;
let mut descriptors = vec![(0u64, 0u64); nchunks * ncols];
let mut heap: Vec<u8> = Vec::new();
for (i, comp) in comps.iter().enumerate() {
descriptors[i] = (comp.len() as u64, heap.len() as u64);
heap.extend_from_slice(comp);
}
out.clear();
out.reserve(nchunks * ncols * 16 + heap.len());
for &(nelem, off) in &descriptors {
out.extend_from_slice(&(nelem as i64).to_be_bytes());
out.extend_from_slice(&(off as i64).to_be_bytes());
}
out.extend_from_slice(&heap);
let mut h = header.clone();
let orig_pcount = header.get_integer("PCOUNT").unwrap_or(0);
h.set("ZTABLE", true)
.comment("ZTABLE", "this is a compressed table");
h.set("ZTILELEN", rpt as i64);
h.set("ZNAXIS1", naxis1 as i64);
h.set("ZNAXIS2", nrows as i64);
h.set("ZPCOUNT", orig_pcount);
for (ci, m) in metas.iter().enumerate() {
let n = ci + 1;
let zform = header
.get_text(key!("TFORM{n}").as_str())
.unwrap_or("")
.to_string();
h.set(key!("ZFORM{n}").as_str(), zform);
h.set(key!("TFORM{n}").as_str(), "1QB");
h.set(key!("ZCTYP{n}").as_str(), m.algo.name());
}
h.set("NAXIS1", (ncols * 16) as i64);
h.set("NAXIS2", nchunks as i64);
h.set("PCOUNT", heap.len() as i64);
h.set("GCOUNT", 1);
Ok(h)
}
pub(crate) fn uncompress_table(header: &Header, table: &BinTable) -> Result<HduParts> {
if header.get_logical("ZTABLE") != Some(true) {
return Err(FitsError::NotCompressedTable);
}
let naxis1 = req_int(header, "ZNAXIS1")? as usize;
let nrows = req_int(header, "ZNAXIS2")? as usize;
let zpcount = header.get_integer("ZPCOUNT").unwrap_or(0);
let mut rpt = req_int(header, "ZTILELEN")?.max(1) as usize;
if rpt > nrows {
rpt = nrows.max(1);
}
let ncols = req_int(header, "TFIELDS")? as usize;
let mut metas = Vec::with_capacity(ncols);
let mut zforms = Vec::with_capacity(ncols);
let mut offset = 0;
for n in 1..=ncols {
let zform = header
.get_text(key!("ZFORM{n}").as_str())
.ok_or(FitsError::MissingKeyword { name: "ZFORMn" })?
.to_string();
let tform = Tform::parse(&zform)?;
let algo = match header.get_text(key!("ZCTYP{n}").as_str()) {
Some(s) => Algo::parse(s)?,
None => Algo::Gzip2, };
let m = col_meta(&tform, offset, algo)?;
offset += m.width;
zforms.push(zform);
metas.push(m);
}
if offset != naxis1 {
return Err(FitsError::RowWidthMismatch {
computed: offset,
declared: naxis1,
});
}
let total = nrows
.checked_mul(naxis1)
.ok_or(FitsError::DataUnitOverflow)?;
let nchunks = nrows.div_ceil(rpt.max(1));
let cells: Vec<Vec<ColumnData>> = (0..ncols)
.map(|ci| table.column_by_idx(ci)?.vla())
.collect::<Result<_>>()?;
let decompressed = map_tiles(
nchunks * ncols,
|| (),
|_unit, i| -> Result<Vec<u8>> {
let chunk = i / ncols;
let m = &metas[i % ncols];
let rows = rpt.min(nrows - chunk * rpt);
let cell = cells[i % ncols]
.get(chunk)
.ok_or(FitsError::UnexpectedEof)?;
let bytes = match cell {
ColumnData::Bytes(b) => b.as_slice(),
_ => {
return Err(FitsError::UnsupportedCompression {
name: "compressed table cell is not a byte array".to_string(),
});
}
};
decompress_column(bytes, m, rows)
},
)?;
let mut out = vec![0u8; total];
for (i, cm) in decompressed.iter().enumerate() {
let chunk = i / ncols;
let m = &metas[i % ncols];
let r0 = chunk * rpt;
let rows = rpt.min(nrows - r0);
for r in 0..rows {
let dst = (r0 + r) * naxis1 + m.offset;
out[dst..dst + m.width].copy_from_slice(&cm[r * m.width..(r + 1) * m.width]);
}
}
let mut h = header.clone();
h.set("NAXIS1", naxis1 as i64);
h.set("NAXIS2", nrows as i64);
h.set("PCOUNT", zpcount);
for (n, zform) in zforms.iter().enumerate() {
h.set(key!("TFORM{}", n + 1).as_str(), zform.clone());
h.remove(key!("ZFORM{}", n + 1).as_str());
h.remove(key!("ZCTYP{}", n + 1).as_str());
}
for key in [
"ZTABLE", "ZTILELEN", "ZNAXIS1", "ZNAXIS2", "ZPCOUNT", "ZHEAPPTR",
] {
h.remove(key);
}
Ok(HduParts {
header: h,
data: out,
})
}
fn compress_column(cm: &[u8], m: &ColMeta) -> Result<Vec<u8>> {
Ok(match m.algo {
Algo::Gzip1 => gzip::gzip_encode(cm, gzip::DEFAULT_GZIP_LEVEL),
Algo::Gzip2 => gzip::gzip_encode(
&gzip::shuffle_bytes(cm, m.shuffle_width()),
gzip::DEFAULT_GZIP_LEVEL,
),
Algo::Rice1 => {
let bytepix = m.rice_bytepix().ok_or(FitsError::UnsupportedCompression {
name: format!("RICE_1 on a {} column", m.kind.code()),
})?;
rice::rice_encode(&be_to_i64(cm, bytepix), bytepix, 32)
}
})
}
fn decompress_column(bytes: &[u8], m: &ColMeta, rows: usize) -> Result<Vec<u8>> {
let cm = match m.algo {
Algo::Gzip1 => gzip::gunzip(bytes)?,
Algo::Gzip2 => gzip::unshuffle_bytes(&gzip::gunzip(bytes)?, m.shuffle_width()),
Algo::Rice1 => {
let bytepix = m.rice_bytepix().ok_or(FitsError::UnsupportedCompression {
name: format!("RICE_1 on a {} column", m.kind.code()),
})?;
let nelem = rows * m.repeat;
let mut ints = Vec::new();
rice::rice_decode_into(bytes, nelem, bytepix, 32, &mut ints);
i64_to_be(&ints, bytepix)
}
};
if cm.len() != rows * m.width {
return Err(FitsError::UnsupportedCompression {
name: "decompressed column size mismatch".to_string(),
});
}
Ok(cm)
}
fn be_to_i64(bytes: &[u8], bytepix: usize) -> Vec<i64> {
match bytepix {
1 => bytes.iter().map(|&b| b as i64).collect(),
2 => bytes
.chunks_exact(2)
.map(|c| i16::from_be_bytes(c.try_into().unwrap()) as i64)
.collect(),
_ => bytes
.chunks_exact(4)
.map(|c| i32::from_be_bytes(c.try_into().unwrap()) as i64)
.collect(),
}
}
fn i64_to_be(vals: &[i64], bytepix: usize) -> Vec<u8> {
let mut out = vec![0u8; vals.len() * bytepix];
match bytepix {
1 => {
for (slot, &v) in out.iter_mut().zip(vals) {
*slot = v as u8;
}
}
2 => {
for (slot, &v) in out.chunks_exact_mut(2).zip(vals) {
slot.copy_from_slice(&(v as i16).to_be_bytes());
}
}
_ => {
for (slot, &v) in out.chunks_exact_mut(4).zip(vals) {
slot.copy_from_slice(&(v as i32).to_be_bytes());
}
}
}
out
}
fn req_int(header: &Header, key: &'static str) -> Result<i64> {
header
.get_integer(key)
.ok_or(FitsError::MissingKeyword { name: key })
}