1mod convert;
17mod geometry;
18mod gzip;
19mod hcompress;
20mod plio;
21mod quantize;
22mod rice;
23mod table;
24
25use convert::as_bytes;
26use convert::as_i16;
27use convert::be_floats_into;
28use convert::be_to_i64_into;
29use convert::bytepix_to_bitpix;
30use convert::cell_len;
31use convert::cell_to_f64_into;
32use convert::cell_to_i64_into;
33use convert::float_to_be;
34use convert::gather_f64;
35use convert::gather_i64;
36use convert::i64_to_be;
37use convert::i64_to_be_into;
38use convert::zeroed_samples;
39use geometry::TileGeometry;
40use geometry::TileScratch;
41
42pub(crate) use table::compress_table;
43pub(crate) use table::uncompress_table;
44
45use crate::bitpix::Bitpix;
46use crate::data::Image;
47use crate::data::ImageData;
48use crate::data::Scaling;
49use crate::endian::push_pq_descriptor;
50use crate::error::FitsError;
51use crate::error::Result;
52use crate::header::Header;
53use crate::keyword::key;
54use crate::table::BinTable;
55use crate::table::ColumnData;
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
61pub enum DitherMethod {
62 None,
64 #[default]
67 Subtractive1,
68 Subtractive2,
71}
72
73impl DitherMethod {
74 pub(crate) fn dithered(self) -> bool {
76 !matches!(self, DitherMethod::None)
77 }
78}
79
80#[derive(Debug, Clone)]
85pub struct CompressOptions {
86 pub tile_shape: Vec<usize>,
89 pub gzip_level: u32,
92 pub hcompress_scale: i32,
94 pub quantize_level: f64,
98 pub dither: DitherMethod,
101}
102
103impl Default for CompressOptions {
104 fn default() -> CompressOptions {
105 CompressOptions {
106 tile_shape: Vec::new(),
107 gzip_level: gzip::DEFAULT_GZIP_LEVEL,
108 hcompress_scale: 0,
109 quantize_level: 0.0,
110 dither: DitherMethod::Subtractive1,
111 }
112 }
113}
114
115impl CompressOptions {
116 pub fn tiled(tile_shape: impl Into<Vec<usize>>) -> CompressOptions {
120 CompressOptions {
121 tile_shape: tile_shape.into(),
122 ..CompressOptions::default()
123 }
124 }
125}
126
127#[derive(Debug)]
130pub(crate) struct HduParts {
131 pub header: Header,
132 pub data: Vec<u8>,
133}
134
135#[cfg(feature = "parallel")]
146pub(crate) fn map_tiles<S, T, I, F>(ntiles: usize, init: I, f: F) -> Result<Vec<T>>
147where
148 S: Send,
149 T: Send,
150 I: Fn() -> S + Sync + Send,
151 F: Fn(&mut S, usize) -> Result<T> + Sync + Send,
152{
153 use rayon::prelude::*;
154 (0..ntiles)
155 .into_par_iter()
156 .map_init(init, |scratch, t| f(scratch, t))
157 .collect()
158}
159
160#[cfg(not(feature = "parallel"))]
161pub(crate) fn map_tiles<S, T, I, F>(ntiles: usize, init: I, f: F) -> Result<Vec<T>>
162where
163 I: FnOnce() -> S,
164 F: Fn(&mut S, usize) -> Result<T>,
165{
166 let mut scratch = init();
167 (0..ntiles).map(|t| f(&mut scratch, t)).collect()
168}
169
170pub(crate) fn decompress_image(header: &Header, table: &BinTable) -> Result<Image> {
172 if header.get_logical("ZIMAGE") != Some(true) {
173 return Err(FitsError::NotCompressedImage);
174 }
175 let zbitpix = Bitpix::from_code(
176 header
177 .get_integer("ZBITPIX")
178 .ok_or(FitsError::MissingKeyword { name: "ZBITPIX" })?,
179 )?;
180 let is_float = zbitpix.is_float();
181 let cmptype = header
182 .get_text("ZCMPTYPE")
183 .ok_or(FitsError::MissingKeyword { name: "ZCMPTYPE" })?
184 .to_string();
185
186 let znaxis = header
187 .get_integer("ZNAXIS")
188 .ok_or(FitsError::MissingKeyword { name: "ZNAXIS" })?;
189 if !(0..=999).contains(&znaxis) {
193 return Err(FitsError::KeywordOutOfRange { name: "ZNAXIS" });
194 }
195 let znaxis = znaxis as usize;
196 let dims = read_axes(header, znaxis)?;
197 if dims.is_empty() {
201 return Ok(Image {
202 shape: dims,
203 samples: zeroed_samples(zbitpix, 0)?,
204 scaling: Scaling::from_header(header),
205 });
206 }
207 let total = dims
211 .iter()
212 .try_fold(1usize, |acc, &n| acc.checked_mul(n))
213 .ok_or(FitsError::DataUnitOverflow)?;
214 let tiles: Vec<usize> = (1..=znaxis)
215 .map(|i| {
216 header
217 .get_integer(key!("ZTILE{i}").as_str())
218 .map(|v| v.max(1) as usize)
219 .unwrap_or(if i == 1 { dims[0] } else { 1 })
220 })
221 .collect();
222
223 let rice = rice::rice_params(header, zbitpix);
224 let int_bitpix = if is_float {
227 bytepix_to_bitpix(rice.bytepix)
228 } else {
229 zbitpix
230 };
231
232 let zquantiz = header
234 .get_text("ZQUANTIZ")
235 .unwrap_or("NO_DITHER")
236 .to_string();
237 let method = match zquantiz.as_str() {
238 "NO_DITHER" => DitherMethod::None,
239 "SUBTRACTIVE_DITHER_1" => DitherMethod::Subtractive1,
240 "SUBTRACTIVE_DITHER_2" => DitherMethod::Subtractive2,
241 other => {
242 if is_float {
243 return Err(FitsError::UnsupportedCompression {
244 name: format!("float quantization {other}"),
245 });
246 }
247 DitherMethod::None
248 }
249 };
250 let zdither0 = header.get_integer("ZDITHER0").unwrap_or(1);
251 let zblank_keyword = header.get_integer("ZBLANK");
254 let zblank_column = read_i64_column(table, "ZBLANK");
255 let smooth = hcompress_smooth(header);
256 let params = CodecParams {
257 blocksize: rice.blocksize,
258 bytepix: rice.bytepix,
259 smooth,
260 };
261
262 let primary = read_tiles(table, "COMPRESSED_DATA")?;
264 let gzip_fallback = read_tiles(table, "GZIP_COMPRESSED_DATA")?;
265 let uncompressed = read_tiles(table, "UNCOMPRESSED_DATA")?;
266 let zscale = read_f64_column(table, "ZSCALE");
268 let zzero = read_f64_column(table, "ZZERO");
269
270 let geom = TileGeometry::new(&dims, &tiles);
271 let ntiles = geom.ntiles();
272 let mut samples = zeroed_samples(zbitpix, total)?;
273
274 let ctx = DecodeCtx {
279 codec: ImageCodec::parse(&cmptype)?,
280 zbitpix,
281 int_bitpix,
282 params,
283 };
284 if is_float {
285 let decode = |t: usize, s: &TileScratch, out: &mut Vec<f64>, ints: &mut Vec<i64>| {
286 let cols = TileColumns {
287 primary: primary.get(t),
288 gzip: gzip_fallback.get(t),
289 uncompressed: uncompressed.get(t),
290 };
291 let dq = Dequant {
292 scale: column_at(&zscale, t).unwrap_or(1.0),
293 zero: column_at(&zzero, t).unwrap_or(0.0),
294 method,
295 irow: t as i64 + zdither0,
296 zblank: column_at(&zblank_column, t).or(zblank_keyword),
297 };
298 decode_float_tile_into(&ctx, cols, s.nelem(), dq, out, ints)
299 };
300 match &mut samples {
301 ImageData::F32(o) => run_decode_scatter(ntiles, &geom, o, decode, |v| v as f32)?,
302 ImageData::F64(o) => run_decode_scatter(ntiles, &geom, o, decode, |v| v)?,
303 _ => unreachable!("a float ZBITPIX yields a float sample buffer"),
304 }
305 } else {
306 let decode = |t: usize, s: &TileScratch, out: &mut Vec<i64>, _ints: &mut Vec<i64>| {
307 let cols = TileColumns {
308 primary: primary.get(t),
309 gzip: gzip_fallback.get(t),
310 uncompressed: uncompressed.get(t),
311 };
312 decode_one_tile_into(&ctx, cols, s.nelem(), out)
313 };
314 match &mut samples {
315 ImageData::U8(o) => run_decode_scatter(ntiles, &geom, o, decode, |v| v as u8)?,
316 ImageData::I16(o) => run_decode_scatter(ntiles, &geom, o, decode, |v| v as i16)?,
317 ImageData::I32(o) => run_decode_scatter(ntiles, &geom, o, decode, |v| v as i32)?,
318 ImageData::I64(o) => run_decode_scatter(ntiles, &geom, o, decode, |v| v)?,
319 _ => unreachable!("an integer ZBITPIX yields an integer sample buffer"),
320 }
321 }
322 Ok(Image {
323 shape: dims,
324 samples,
325 scaling: Scaling::from_header(header),
326 })
327}
328
329fn run_decode_scatter<S, D>(
334 ntiles: usize,
335 geom: &TileGeometry,
336 out: &mut [D],
337 decode: impl Fn(usize, &TileScratch, &mut Vec<S>, &mut Vec<i64>) -> Result<()> + Sync + Send,
338 convert: impl Fn(S) -> D + Sync + Send,
339) -> Result<()>
340where
341 S: Copy + Send,
342{
343 #[cfg(feature = "parallel")]
349 {
350 let sink = DisjointOut::new(out);
351 let init = || (TileScratch::default(), Vec::<S>::new(), Vec::<i64>::new());
352 map_tiles(ntiles, init, |(scratch, vals, ints), t| -> Result<()> {
353 geom.tile_into(t, scratch);
354 decode(t, scratch, vals, ints)?;
355 unsafe { sink.scatter_rows(&scratch.row_bases, scratch.row_len, vals, &convert) };
360 Ok(())
361 })?;
362 Ok(())
363 }
364 #[cfg(not(feature = "parallel"))]
365 {
366 let mut scratch = TileScratch::default();
367 let mut vals: Vec<S> = Vec::new();
368 let mut ints: Vec<i64> = Vec::new();
369 for t in 0..ntiles {
370 geom.tile_into(t, &mut scratch);
371 decode(t, &scratch, &mut vals, &mut ints)?;
372 scatter_rows(out, &scratch.row_bases, scratch.row_len, &vals, &convert);
373 }
374 Ok(())
375 }
376}
377
378#[cfg(not(feature = "parallel"))]
383fn scatter_rows<S: Copy, D>(
384 out: &mut [D],
385 row_bases: &[usize],
386 row_len: usize,
387 vals: &[S],
388 convert: &impl Fn(S) -> D,
389) {
390 let mut off = 0;
391 for &base in row_bases {
392 if off >= vals.len() {
393 break;
394 }
395 let rl = row_len.min(vals.len() - off);
396 for (d, &v) in out[base..base + rl].iter_mut().zip(&vals[off..off + rl]) {
397 *d = convert(v);
398 }
399 off += row_len;
400 }
401}
402
403#[cfg(feature = "parallel")]
408#[derive(Debug)]
409struct DisjointOut<D> {
410 ptr: *mut D,
411 len: usize,
412}
413
414#[cfg(feature = "parallel")]
416unsafe impl<D> Sync for DisjointOut<D> {}
417
418#[cfg(feature = "parallel")]
419impl<D> DisjointOut<D> {
420 fn new(out: &mut [D]) -> DisjointOut<D> {
421 DisjointOut {
422 ptr: out.as_mut_ptr(),
423 len: out.len(),
424 }
425 }
426
427 unsafe fn scatter_rows<S: Copy>(
435 &self,
436 row_bases: &[usize],
437 row_len: usize,
438 vals: &[S],
439 convert: &impl Fn(S) -> D,
440 ) {
441 let mut off = 0;
442 for &base in row_bases {
443 if off >= vals.len() {
444 break;
445 }
446 let rl = row_len.min(vals.len() - off);
447 debug_assert!(base + rl <= self.len, "tile row out of bounds {}", self.len);
448 let dst = unsafe { std::slice::from_raw_parts_mut(self.ptr.add(base), rl) };
452 for (d, &v) in dst.iter_mut().zip(&vals[off..off + rl]) {
453 *d = convert(v);
454 }
455 off += row_len;
456 }
457 }
458}
459
460#[derive(Debug, Default)]
465struct EncodeScratch {
466 tile: TileScratch,
467 ints: Vec<i64>,
470 floats: Vec<f64>,
472 be: Vec<u8>,
475}
476
477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
480enum ImageCodec {
481 Gzip1,
482 Gzip2,
483 Rice1,
484 Plio1,
485 Hcompress1,
486 NoCompress,
487}
488
489impl ImageCodec {
490 fn parse(s: &str) -> Result<ImageCodec> {
491 Ok(match s {
492 "GZIP_1" => ImageCodec::Gzip1,
493 "GZIP_2" => ImageCodec::Gzip2,
494 "RICE_1" => ImageCodec::Rice1,
495 "PLIO_1" => ImageCodec::Plio1,
496 "HCOMPRESS_1" => ImageCodec::Hcompress1,
497 "NOCOMPRESS" => ImageCodec::NoCompress,
498 other => {
499 return Err(FitsError::UnsupportedCompression {
500 name: other.to_string(),
501 });
502 }
503 })
504 }
505}
506
507pub(crate) fn compress_image(
511 image: &Image,
512 cmptype: &str,
513 options: &CompressOptions,
514 out: &mut Vec<u8>,
515) -> Result<Header> {
516 let bitpix = image.samples.bitpix();
517 if bitpix.is_float() {
518 return compress_float_image(image, cmptype, options, out);
519 }
520 let codec = ImageCodec::parse(cmptype)?;
521 if codec == ImageCodec::Rice1 && bitpix.elem_size() > 4 {
525 return Err(FitsError::UnsupportedCompression {
526 name: "RICE_1 with BYTEPIX > 4 (64-bit pixels)".to_string(),
527 });
528 }
529 if codec == ImageCodec::Hcompress1 && bitpix.elem_size() > 4 {
533 return Err(FitsError::UnsupportedCompression {
534 name: "HCOMPRESS_1 with 64-bit pixels".to_string(),
535 });
536 }
537 let dims = &image.shape;
538 let tiles = resolve_tile_shape(dims, &options.tile_shape)?;
539
540 let geom = TileGeometry::new(dims, &tiles);
541 let ntiles = if dims.is_empty() { 0 } else { geom.ntiles() };
546 let bytepix = bitpix.elem_size();
547 let (gzip_level, scale) = (options.gzip_level, options.hcompress_scale);
548
549 let cells = map_tiles(ntiles, EncodeScratch::default, |s, t| -> Result<TileCell> {
553 geom.tile_into(t, &mut s.tile);
554 gather_i64(
557 &image.samples,
558 &s.tile.row_bases,
559 s.tile.row_len,
560 &mut s.ints,
561 );
562 let vals = &s.ints;
563 Ok(match codec {
564 ImageCodec::Gzip1 => {
565 i64_to_be_into(vals, bitpix, &mut s.be);
566 TileCell::Bytes(gzip::gzip_encode(&s.be, gzip_level))
567 }
568 ImageCodec::Gzip2 => {
569 i64_to_be_into(vals, bitpix, &mut s.be);
570 TileCell::Bytes(gzip::gzip2_encode(&s.be, bytepix, gzip_level))
571 }
572 ImageCodec::Rice1 => TileCell::Bytes(rice::rice_encode(vals, bytepix, 32)),
573 ImageCodec::Plio1 => TileCell::I16(plio::plio_encode(vals, vals.len())),
574 ImageCodec::Hcompress1 => TileCell::Bytes(hcompress::hcompress_tile_encode(
575 vals,
576 &s.tile.tdims,
577 scale,
578 )?),
579 ImageCodec::NoCompress => TileCell::Bytes(i64_to_be(vals, bitpix)),
581 })
582 })?;
583
584 let mut descriptors: Vec<(usize, usize)> = Vec::with_capacity(ntiles);
585 let mut heap: Vec<u8> = Vec::new();
586 for cell in &cells {
587 descriptors.push((cell.nelem(), heap.len()));
588 cell.extend_heap(&mut heap);
589 }
590
591 let maxnelem = descriptors.iter().map(|&(n, _)| n).max().unwrap_or(0);
592 let wide = heap.len() > i32::MAX as usize || maxnelem > i32::MAX as usize;
595
596 out.clear();
599 out.reserve(ntiles * if wide { 16 } else { 8 } + heap.len());
600 for &(nelem, offset) in &descriptors {
601 push_pq_descriptor(out, wide, nelem as u64, offset as u64);
602 }
603 out.extend_from_slice(&heap);
604
605 let tform_letter = if codec == ImageCodec::Plio1 { 'I' } else { 'B' };
606 let desc = if wide { 'Q' } else { 'P' };
607 let mut h = Header::new();
608 h.set("XTENSION", "BINTABLE")
609 .comment("XTENSION", "binary table extension");
610 h.set("BITPIX", 8).set("NAXIS", 2);
611 h.set("NAXIS1", if wide { 16 } else { 8 })
612 .set("NAXIS2", ntiles as i64);
613 h.set("PCOUNT", heap.len() as i64).set("GCOUNT", 1);
614 h.set("TFIELDS", 1);
615 h.set("TTYPE1", "COMPRESSED_DATA");
616 h.set("TFORM1", format!("1{desc}{tform_letter}({maxnelem})"));
617 set_zimage_axes(&mut h, cmptype, bitpix, dims, &tiles);
618 match codec {
619 ImageCodec::Rice1 => {
620 h.set("ZNAME1", "BLOCKSIZE").set("ZVAL1", 32);
621 h.set("ZNAME2", "BYTEPIX").set("ZVAL2", bytepix as i64);
622 }
623 ImageCodec::Hcompress1 => {
624 h.set("ZNAME1", "SCALE").set("ZVAL1", scale as i64);
625 h.set("ZNAME2", "SMOOTH").set("ZVAL2", 0);
626 }
627 _ => {}
628 }
629 if !image.scaling.is_identity() {
633 h.set("BZERO", image.scaling.bzero);
634 h.set("BSCALE", image.scaling.bscale);
635 }
636 if let Some(blank) = image.scaling.blank {
637 h.set("BLANK", blank);
638 }
639 Ok(h)
640}
641
642#[derive(Debug)]
647struct FloatTile {
648 bytes: Vec<u8>,
649 zscale: f64,
650 zzero: f64,
651 quantized: bool,
652 has_null: bool,
653}
654
655fn compress_float_image(
662 image: &Image,
663 cmptype: &str,
664 options: &CompressOptions,
665 out: &mut Vec<u8>,
666) -> Result<Header> {
667 let codec = ImageCodec::parse(cmptype)?;
668 if !matches!(
669 codec,
670 ImageCodec::Gzip1 | ImageCodec::Gzip2 | ImageCodec::Rice1
671 ) {
672 return Err(FitsError::UnsupportedCompression {
673 name: format!("{cmptype} for float images (write)"),
674 });
675 }
676 let zbitpix = image.samples.bitpix();
677 let dims = &image.shape;
678 let tiles = resolve_tile_shape(dims, &options.tile_shape)?;
679
680 let geom = TileGeometry::new(dims, &tiles);
681 let ntiles = if dims.is_empty() { 0 } else { geom.ntiles() };
684
685 let zdither0 = 1i64; let int_bitpix = Bitpix::I32; let method = options.dither;
688 let (gzip_level, qlevel) = (options.gzip_level, options.quantize_level);
689
690 let tiles_out = map_tiles(
694 ntiles,
695 EncodeScratch::default,
696 |s, t| -> Result<FloatTile> {
697 geom.tile_into(t, &mut s.tile);
698 let nx = s.tile.tdims[0];
699 let ny = s.tile.row_bases.len();
700 gather_f64(
702 &image.samples,
703 &s.tile.row_bases,
704 s.tile.row_len,
705 &mut s.floats,
706 );
707 let irow = t as i64 + zdither0; Ok(
709 match quantize::quantize_tile(&s.floats, nx, ny, qlevel, method, irow) {
710 Some(q) => {
711 s.ints.clear();
712 s.ints.extend(q.idata.iter().map(|&v| v as i64));
713 let bytes = match codec {
714 ImageCodec::Gzip1 => {
715 i64_to_be_into(&s.ints, int_bitpix, &mut s.be);
716 gzip::gzip_encode(&s.be, gzip_level)
717 }
718 ImageCodec::Gzip2 => {
719 i64_to_be_into(&s.ints, int_bitpix, &mut s.be);
720 gzip::gzip2_encode(&s.be, 4, gzip_level)
721 }
722 ImageCodec::Rice1 => rice::rice_encode(&s.ints, 4, 32),
723 _ => unreachable!(),
724 };
725 FloatTile {
726 bytes,
727 zscale: q.bscale,
728 zzero: q.bzero,
729 quantized: true,
730 has_null: q.has_null,
731 }
732 }
733 None => FloatTile {
735 bytes: gzip::gzip_encode(&float_to_be(&s.floats, zbitpix), gzip_level),
736 zscale: 0.0,
737 zzero: 0.0,
738 quantized: false,
739 has_null: false,
740 },
741 },
742 )
743 },
744 )?;
745
746 let mut cd_desc: Vec<(usize, usize)> = Vec::with_capacity(ntiles);
747 let mut gz_desc: Vec<(usize, usize)> = Vec::with_capacity(ntiles);
748 let mut zscale = vec![0f64; ntiles];
749 let mut zzero = vec![0f64; ntiles];
750 let mut heap: Vec<u8> = Vec::new();
751 let mut any_null = false;
752 for (t, tile) in tiles_out.iter().enumerate() {
753 zscale[t] = tile.zscale;
754 zzero[t] = tile.zzero;
755 any_null |= tile.has_null;
756 let (cd, gz) = if tile.quantized {
757 ((tile.bytes.len(), heap.len()), (0, heap.len()))
758 } else {
759 ((0, heap.len()), (tile.bytes.len(), heap.len()))
760 };
761 cd_desc.push(cd);
762 gz_desc.push(gz);
763 heap.extend_from_slice(&tile.bytes);
764 }
765
766 out.clear();
769 out.reserve(ntiles * 32 + heap.len());
770 for t in 0..ntiles {
771 push_pq_descriptor(out, false, cd_desc[t].0 as u64, cd_desc[t].1 as u64);
774 push_pq_descriptor(out, false, gz_desc[t].0 as u64, gz_desc[t].1 as u64);
775 out.extend_from_slice(&zscale[t].to_be_bytes());
776 out.extend_from_slice(&zzero[t].to_be_bytes());
777 }
778 out.extend_from_slice(&heap);
779
780 let max_cd = cd_desc.iter().map(|&(n, _)| n).max().unwrap_or(0);
781 let max_gz = gz_desc.iter().map(|&(n, _)| n).max().unwrap_or(0);
782 let mut h = Header::new();
783 h.set("XTENSION", "BINTABLE")
784 .comment("XTENSION", "binary table extension");
785 h.set("BITPIX", 8).set("NAXIS", 2);
786 h.set("NAXIS1", 32).set("NAXIS2", ntiles as i64);
787 h.set("PCOUNT", heap.len() as i64).set("GCOUNT", 1);
788 h.set("TFIELDS", 4);
789 h.set("TTYPE1", "COMPRESSED_DATA")
790 .set("TFORM1", format!("1PB({max_cd})"));
791 h.set("TTYPE2", "GZIP_COMPRESSED_DATA")
792 .set("TFORM2", format!("1PB({max_gz})"));
793 h.set("TTYPE3", "ZSCALE").set("TFORM3", "1D");
794 h.set("TTYPE4", "ZZERO").set("TFORM4", "1D");
795 set_zimage_axes(&mut h, cmptype, zbitpix, dims, &tiles);
796 if codec == ImageCodec::Rice1 {
797 h.set("ZNAME1", "BLOCKSIZE").set("ZVAL1", 32);
798 h.set("ZNAME2", "BYTEPIX").set("ZVAL2", 4);
799 } else {
800 h.set("ZNAME1", "BYTEPIX").set("ZVAL1", 4);
802 }
803 h.set("ZQUANTIZ", dither_name(method));
804 h.set("ZDITHER0", zdither0);
805 if any_null {
806 h.set("ZBLANK", quantize::NULL_VALUE as i64);
809 }
810 Ok(h)
811}
812
813fn dither_name(method: DitherMethod) -> &'static str {
815 match method {
816 DitherMethod::None => "NO_DITHER",
817 DitherMethod::Subtractive1 => "SUBTRACTIVE_DITHER_1",
818 DitherMethod::Subtractive2 => "SUBTRACTIVE_DITHER_2",
819 }
820}
821
822#[derive(Debug)]
825enum TileCell {
826 Bytes(Vec<u8>),
827 I16(Vec<i16>),
828}
829
830impl TileCell {
831 fn nelem(&self) -> usize {
833 match self {
834 TileCell::Bytes(b) => b.len(),
835 TileCell::I16(v) => v.len(),
836 }
837 }
838
839 fn extend_heap(&self, heap: &mut Vec<u8>) {
841 match self {
842 TileCell::Bytes(b) => heap.extend_from_slice(b),
843 TileCell::I16(v) => {
844 for &x in v {
845 heap.extend_from_slice(&x.to_be_bytes());
846 }
847 }
848 }
849 }
850}
851
852fn resolve_tile_shape(dims: &[usize], tile_shape: &[usize]) -> Result<Vec<usize>> {
856 if tile_shape.is_empty() {
857 Ok((0..dims.len())
858 .map(|i| if i == 0 { dims[i].max(1) } else { 1 })
859 .collect())
860 } else if tile_shape.len() == dims.len() {
861 Ok(tile_shape.iter().map(|&t| t.max(1)).collect())
862 } else {
863 Err(FitsError::TileShapeRankMismatch {
864 tile_rank: tile_shape.len(),
865 image_rank: dims.len(),
866 })
867 }
868}
869
870fn set_zimage_axes(
874 h: &mut Header,
875 cmptype: &str,
876 zbitpix: Bitpix,
877 dims: &[usize],
878 tiles: &[usize],
879) {
880 h.set("ZIMAGE", true)
881 .comment("ZIMAGE", "this is a tiled-compressed image");
882 h.set("ZCMPTYPE", cmptype);
883 h.set("ZBITPIX", zbitpix.code());
884 h.set("ZNAXIS", dims.len() as i64);
885 for (i, &n) in dims.iter().enumerate() {
886 h.set(key!("ZNAXIS{}", i + 1).as_str(), n as i64);
887 }
888 for (i, &t) in tiles.iter().enumerate() {
889 h.set(key!("ZTILE{}", i + 1).as_str(), t as i64);
890 }
891}
892
893fn read_tiles(table: &BinTable, name: &str) -> Result<Vec<ColumnData>> {
895 match table.column_index(name) {
896 Some(c) => table.column_by_idx(c)?.vla(),
897 None => Ok(Vec::new()),
898 }
899}
900
901fn read_f64_column(table: &BinTable, name: &str) -> Option<Vec<f64>> {
903 let c = table.column_index(name)?;
904 match table.column_by_idx(c).and_then(|col| col.raw()) {
905 Ok(ColumnData::F64(v)) => Some(v),
906 _ => None,
907 }
908}
909
910fn read_i64_column(table: &BinTable, name: &str) -> Option<Vec<i64>> {
913 let c = table.column_index(name)?;
914 match table.column_by_idx(c).and_then(|col| col.raw()) {
915 Ok(ColumnData::Bytes(v)) => Some(v.iter().map(|&x| x as i64).collect()),
916 Ok(ColumnData::I16(v)) => Some(v.iter().map(|&x| x as i64).collect()),
917 Ok(ColumnData::I32(v)) => Some(v.iter().map(|&x| x as i64).collect()),
918 Ok(ColumnData::I64(v)) => Some(v),
919 _ => None,
920 }
921}
922
923fn column_at<T: Copy>(col: &Option<Vec<T>>, t: usize) -> Option<T> {
924 col.as_ref().and_then(|v| v.get(t).copied())
925}
926
927#[derive(Debug, Clone, Copy)]
932struct TileColumns<'a> {
933 primary: Option<&'a ColumnData>,
934 gzip: Option<&'a ColumnData>,
935 uncompressed: Option<&'a ColumnData>,
936}
937
938#[derive(Debug)]
940enum TileSource<'a> {
941 Compressed(&'a ColumnData),
942 Gzip(&'a ColumnData),
943 Uncompressed(&'a ColumnData),
944}
945
946impl<'a> TileColumns<'a> {
947 fn resolve(&self) -> Result<TileSource<'a>> {
950 if let Some(c) = self.primary.filter(|c| cell_len(c) > 0) {
951 Ok(TileSource::Compressed(c))
952 } else if let Some(c) = self.gzip.filter(|c| cell_len(c) > 0) {
953 Ok(TileSource::Gzip(c))
954 } else if let Some(c) = self.uncompressed.filter(|c| cell_len(c) > 0) {
955 Ok(TileSource::Uncompressed(c))
956 } else {
957 Err(FitsError::UnsupportedCompression {
958 name: "empty tile (no compressed or uncompressed data)".to_string(),
959 })
960 }
961 }
962}
963
964#[derive(Debug, Clone, Copy)]
967struct CodecParams {
968 blocksize: usize,
969 bytepix: usize,
970 smooth: bool,
971}
972
973#[derive(Debug)]
976struct Dequant {
977 scale: f64,
978 zero: f64,
979 method: DitherMethod,
980 irow: i64,
981 zblank: Option<i64>,
982}
983
984#[derive(Debug)]
989struct DecodeCtx {
990 codec: ImageCodec,
991 zbitpix: Bitpix,
992 int_bitpix: Bitpix,
993 params: CodecParams,
994}
995
996fn decode_one_tile_into(
997 ctx: &DecodeCtx,
998 cols: TileColumns,
999 tile_elems: usize,
1000 out: &mut Vec<i64>,
1001) -> Result<()> {
1002 match cols.resolve()? {
1003 TileSource::Compressed(cell) => decode_tile_cell_into(ctx, cell, tile_elems, out),
1004 TileSource::Gzip(cell) => {
1005 gzip::gzip_tile_into(as_bytes(cell)?, ctx.int_bitpix, tile_elems, out)
1006 }
1007 TileSource::Uncompressed(cell) => {
1008 cell_to_i64_into(cell, out);
1009 Ok(())
1010 }
1011 }
1012}
1013
1014fn decode_float_tile_into(
1019 ctx: &DecodeCtx,
1020 cols: TileColumns,
1021 tile_elems: usize,
1022 dq: Dequant,
1023 out: &mut Vec<f64>,
1024 ints: &mut Vec<i64>,
1025) -> Result<()> {
1026 match cols.resolve()? {
1027 TileSource::Compressed(cell) => {
1028 decode_tile_cell_into(ctx, cell, tile_elems, ints)?;
1030 quantize::dequantize_into(ints, dq.scale, dq.zero, dq.method, dq.irow, dq.zblank, out);
1031 Ok(())
1032 }
1033 TileSource::Gzip(cell) => {
1034 let max = tile_elems.saturating_mul(ctx.zbitpix.elem_size());
1036 be_floats_into(&gzip::gunzip(as_bytes(cell)?, max)?, ctx.zbitpix, out);
1037 Ok(())
1038 }
1039 TileSource::Uncompressed(cell) => {
1040 cell_to_f64_into(cell, ctx.zbitpix, out);
1041 Ok(())
1042 }
1043 }
1044}
1045
1046fn decode_tile_cell_into(
1049 ctx: &DecodeCtx,
1050 cell: &ColumnData,
1051 tile_elems: usize,
1052 out: &mut Vec<i64>,
1053) -> Result<()> {
1054 let params = ctx.params;
1055 match ctx.codec {
1056 ImageCodec::Gzip1 => gzip::gzip_tile_into(as_bytes(cell)?, ctx.int_bitpix, tile_elems, out),
1057 ImageCodec::Gzip2 => {
1058 gzip::gzip2_tile_into(as_bytes(cell)?, ctx.int_bitpix, tile_elems, out)
1059 }
1060 ImageCodec::Rice1 => {
1061 if !matches!(params.bytepix, 1 | 2 | 4) {
1065 return Err(FitsError::UnsupportedCompression {
1066 name: format!("RICE_1 with BYTEPIX = {} (only 1/2/4)", params.bytepix),
1067 });
1068 }
1069 rice::rice_decode_into(
1070 as_bytes(cell)?,
1071 tile_elems,
1072 params.bytepix,
1073 params.blocksize,
1074 out,
1075 );
1076 Ok(())
1077 }
1078 ImageCodec::Plio1 => {
1079 plio::plio_decode_into(as_i16(cell)?, tile_elems, out);
1080 Ok(())
1081 }
1082 ImageCodec::Hcompress1 => {
1083 hcompress::hcompress_tile_into(as_bytes(cell)?, params.smooth, tile_elems, out)
1084 }
1085 ImageCodec::NoCompress => {
1087 be_to_i64_into(as_bytes(cell)?, ctx.int_bitpix, out);
1088 Ok(())
1089 }
1090 }
1091}
1092
1093fn hcompress_smooth(header: &Header) -> bool {
1096 let mut i = 1;
1097 while let Some(name) = header.get_text(key!("ZNAME{i}").as_str()) {
1098 if name == "SMOOTH" {
1099 return header.get_integer(key!("ZVAL{i}").as_str()).unwrap_or(0) != 0;
1100 }
1101 i += 1;
1102 }
1103 false
1104}
1105
1106fn read_axes(header: &Header, n: usize) -> Result<Vec<usize>> {
1108 (1..=n)
1109 .map(|i| match header.get_integer(key!("ZNAXIS{i}").as_str()) {
1110 Some(v) if v >= 0 => Ok(v as usize),
1111 Some(_) => Err(FitsError::KeywordOutOfRange { name: "ZNAXISn" }),
1112 None => Err(FitsError::MissingKeyword { name: "ZNAXISn" }),
1113 })
1114 .collect()
1115}
1116
1117#[cfg(test)]
1118mod tests;