use super::config::{DitherSeed, Method, Quantize, Tiling};
use super::hdu::{ImageView, Tile};
use super::{gzip, hcompress, quantize, rice, FitsError, FitsResult};
pub(super) struct Compressed {
pub zcmptype: &'static str,
pub zbitpix: i64,
pub zaxes: Vec<usize>,
pub ztiles: Vec<usize>,
pub bytepix: usize,
pub is_rice: bool,
pub is_hcompress: bool,
pub hscale: i64,
pub hsmooth: bool,
pub quant: Option<u32>,
pub naxis2: usize,
pub pcount: usize,
pub row_bytes: usize,
pub data: Vec<u8>,
}
enum Algo {
Gzip {
level: u8,
},
Rice(Quantize),
Hcompress {
scale: i32,
smooth: bool,
quantize: Quantize,
},
}
impl Algo {
fn is_hcompress(&self) -> bool {
matches!(self, Algo::Hcompress { .. })
}
fn hscale(&self) -> i32 {
match self {
Algo::Hcompress { scale, .. } => *scale,
_ => 0,
}
}
fn quantize(&self) -> Option<Quantize> {
match self {
Algo::Gzip { .. } => None,
Algo::Rice(q) => Some(*q),
Algo::Hcompress { quantize, .. } => Some(*quantize),
}
}
}
pub(super) fn build(view: &ImageView<'_>, method: &Method) -> FitsResult<Compressed> {
let (tiling, algo) = match method {
Method::None => unreachable!("caller handles None"),
Method::Gzip { tiling, level } => (tiling, Algo::Gzip { level: *level }),
Method::Rice { tiling, quantize } => (tiling, Algo::Rice(*quantize)),
Method::Hcompress {
tiling,
scale,
smooth,
quantize,
} => (
tiling,
Algo::Hcompress {
scale: *scale,
smooth: *smooth,
quantize: *quantize,
},
),
};
build_tiled(view, tiling, &algo)
}
struct Grid {
tx: usize,
ty: usize,
ntx: usize,
nty: usize,
ztile: Vec<usize>,
}
fn resolve_grid(view: &ImageView<'_>, tiling: &Tiling, hcompress: bool) -> FitsResult<Grid> {
let axes = view.axes();
let (mut tx, mut ty) = match tiling {
Tiling::Default => {
if hcompress {
(view.w, view.h)
} else {
(view.w, 1)
}
}
Tiling::Rows(n) => (view.w, (*n).max(1)),
Tiling::Dims(d) => {
if d.is_empty() || d.iter().take(2).any(|&v| v == 0) {
return Err(FitsError::InvalidTiling(
"tile dimensions must be non-zero".into(),
));
}
if d.len() > axes.len() || d.iter().skip(2).any(|&v| v != 1) {
return Err(FitsError::InvalidTiling(
"only the first two tile dimensions may exceed 1".into(),
));
}
(d[0], d.get(1).copied().unwrap_or(1))
}
};
tx = tx.min(view.w).max(1);
ty = ty.min(view.h).max(1);
let ntx = view.w.div_ceil(tx);
let nty = view.h.div_ceil(ty);
if hcompress {
let last_w = view.w - (ntx - 1) * tx;
let last_h = view.h - (nty - 1) * ty;
if tx < 4 || ty < 4 || last_w < 4 || last_h < 4 {
return Err(FitsError::HcompressTooSmall);
}
}
let mut ztile = vec![1usize; axes.len()];
ztile[0] = tx;
ztile[1] = ty;
Ok(Grid {
tx,
ty,
ntx,
nty,
ztile,
})
}
fn build_tiled(view: &ImageView<'_>, tiling: &Tiling, algo: &Algo) -> FitsResult<Compressed> {
let grid = resolve_grid(view, tiling, algo.is_hcompress())?;
let quant = match algo.quantize() {
Some(q) if view.is_float() => {
let planar = view.planar_f32();
let delta = quantize::global_delta(&planar, view.w, view.h * view.ch, q.level);
let seed = match q.seed {
DitherSeed::Auto => {
let tw = grid.tx.min(view.w);
let th = grid.ty.min(view.h);
quantize::dither_seed(&view.rect_f32(0, 0, 0, tw, th))
}
DitherSeed::Fixed(n) => n.clamp(1, 10_000),
};
Some((delta, seed))
}
_ => None,
};
let quantized = quant.is_some();
let row_bytes = if quantized { 24 } else { 8 };
let n_tiles = grid.ntx * grid.nty * view.ch;
let mut heap = Vec::new();
let mut table = Vec::with_capacity(n_tiles * row_bytes);
let mut tile_index = 0usize;
for plane in 0..view.ch {
for iy in 0..grid.nty {
for ix in 0..grid.ntx {
let x0 = ix * grid.tx;
let y0 = iy * grid.ty;
let tw = grid.tx.min(view.w - x0);
let th = grid.ty.min(view.h - y0);
let (compressed, zzero) = if let Some((delta, seed)) = quant {
let f = view.rect_f32(plane, x0, y0, tw, th);
let q = quantize::quantize_tile(&f, delta, tile_index, seed);
let bytes = if algo.is_hcompress() {
let mut d = q.idata;
hcompress::compress(&mut d, tw, th, algo.hscale())
} else {
rice::encode_int(&q.idata)
};
(bytes, Some(q.zzero))
} else {
let bytes = match algo {
Algo::Gzip { level } => {
gzip::gzip(&view.rect_tile(plane, x0, y0, tw, th).to_be_bytes(), *level)
}
Algo::Rice(_) => match view.rect_tile(plane, x0, y0, tw, th) {
Tile::I16(v) => rice::encode_short(&v),
Tile::I8(v) => rice::encode_byte(&v),
Tile::F32(_) => unreachable!("float is quantized"),
},
Algo::Hcompress { scale, .. } => {
let mut d = view.rect_i32(plane, x0, y0, tw, th);
hcompress::compress(&mut d, tw, th, *scale)
}
};
(bytes, None)
};
let offset = heap.len() as i32;
let nbytes = compressed.len() as i32;
table.extend_from_slice(&nbytes.to_be_bytes());
table.extend_from_slice(&offset.to_be_bytes());
if let Some((delta, _)) = quant {
table.extend_from_slice(&delta.to_be_bytes());
table.extend_from_slice(&zzero.unwrap_or(0.0).to_be_bytes());
}
heap.extend_from_slice(&compressed);
tile_index += 1;
}
}
}
let pcount = heap.len();
let mut data = table;
data.extend_from_slice(&heap);
let (zcmptype, is_rice, is_hcompress, hscale, hsmooth) = match algo {
Algo::Gzip { .. } => ("GZIP_1", false, false, 0i64, false),
Algo::Rice(_) => ("RICE_1", true, false, 0, false),
Algo::Hcompress { scale, smooth, .. } => {
("HCOMPRESS_1", false, true, *scale as i64, *smooth)
}
};
let bytepix = if quantized { 4 } else { view.bytepix() };
Ok(Compressed {
zcmptype,
zbitpix: view.bitpix(),
zaxes: view.axes(),
ztiles: grid.ztile,
bytepix,
is_rice,
is_hcompress,
hscale,
hsmooth,
quant: quant.map(|(_, seed)| seed),
naxis2: n_tiles,
pcount,
row_bytes,
data,
})
}