1#![doc = include_str!("../README.md")]
33#![warn(
34 trivial_casts,
35 trivial_numeric_casts,
36 clippy::uninlined_format_args,
37 clippy::match_same_arms
38)]
39
40extern crate async_trait;
41#[macro_use]
43extern crate quick_error;
44
45pub mod async_fits;
46pub mod card;
47pub mod error;
48pub mod file;
49pub mod fits;
50pub mod wcs;
51
52pub mod gz;
53pub mod hdu;
54
55pub use async_fits::AsyncFits;
56pub use file::FITSFile;
57pub use fits::Fits;
58pub use hdu::data::bintable::{DataValue, TableData, TableRowData};
59pub use hdu::data::image::{ImageData, Pixels};
60pub use hdu::data::iter::It;
61pub use hdu::{AsyncHDU, HDU};
62pub use wcs::{ImgXY, LonLat, WCSParams, WCS};
63
64#[cfg(test)]
65mod tests {
66 use crate::async_fits::AsyncFits;
67 use crate::fits::Fits;
68 use crate::hdu::data::image::Pixels;
69 use crate::hdu::data::DataStream;
70 use crate::hdu::AsyncHDU;
71 use crate::wcs::ImgXY;
72 use crate::FITSFile;
73
74 use crate::hdu::data::bintable::ColumnId;
75 use crate::hdu::header::extension::Xtension;
76 use crate::hdu::header::Bitpix;
77
78 use std::fs::File;
79 use std::io::Cursor;
80 use std::io::{BufReader, Read};
81
82 use futures::StreamExt;
83 use image::DynamicImage;
84 use test_case::test_case;
85
86 #[test]
87 fn test_fits_image_mandatory_kw() {
88 let f = BufReader::new(File::open("samples/hipsgen/Npix208.fits").unwrap());
89 let bytes: Result<Vec<_>, _> = f.bytes().collect();
90 let buf = bytes.unwrap();
91
92 let reader = Cursor::new(&buf[..]);
93 let mut hdu_list = Fits::from_reader(reader);
94 let hdu = hdu_list.next().unwrap().unwrap();
95 assert!(matches!(hdu, HDU::Primary(_)));
96
97 if let HDU::Primary(hdu) = hdu {
98 let header = hdu.get_header();
99 assert_eq!(header.get_xtension().get_naxis(), &[64, 64]);
100 assert_eq!(header.get_xtension().get_bitpix(), Bitpix::F32);
101 }
102
103 assert!(hdu_list.next().is_none());
104 }
105
106 #[test_case("samples/fits.gsfc.nasa.gov/Astro_UIT.fits",1,0,0,&[11520],&[524288])]
107 #[test_case("samples/fits.gsfc.nasa.gov/EUVE.fits",5,0,4,&[5760, 14400, 550080, 1788480, 3026880, 4262400, 4271040, 4279680, 4288320],&[0, 524288, 1228800, 1228800, 1228800, 48, 40, 40, 40])]
108 #[test_case("samples/fits.gsfc.nasa.gov/HST_FGS.fits",1,1,0,&[20160, 2537280],&[2511264, 693])]
109 #[test_case("samples/fits.gsfc.nasa.gov/IUE_LWP.fits",1,0,1,&[28800, 34560],&[0, 11535])]
110 #[test_case("samples/misc/ngc5457K.fits",1,0,0,&[14400],&[65116872])]
111 #[test_case("samples/fits.gsfc.nasa.gov/HST_FOC.fits",1,1,0,&[11520, 4216320],&[4194304, 312])]
112 #[test_case("samples/fits.gsfc.nasa.gov/HST_FOS.fits",1,1,0,&[14400, 40320],&[16512, 672])]
113 #[test_case("samples/fits.gsfc.nasa.gov/HST_HRS.fits",1,1,0,&[20160, 66240],&[32000, 1648])]
114 #[test_case("samples/fits.gsfc.nasa.gov/HST_NICMOS.fits",6,0,0,&[20160, 31680, 322560, 613440, 763200, 912960],&[0, 284040, 284040, 142020, 142020, 284040])]
115 #[test_case("samples/fits.gsfc.nasa.gov/HST_WFPC_II.fits",1,1,0,&[23040, 694080],&[640000, 3184])]
116 #[test_case("samples/fits.gsfc.nasa.gov/HST_WFPC_II_bis.fits",1,0,0,&[23040],&[40000])]
117 fn test_fits_count_hdu(
118 filename: &str,
119 num_image_ext: usize,
120 num_asciitable_ext: usize,
121 num_bintable_ext: usize,
122 byte_offsets: &[u64],
123 byte_lengths: &[u64],
124 ) {
125 let mut hdu_list = FITSFile::open(filename).unwrap();
126
127 let mut n_image_ext = 0; let mut n_bintable_ext = 0;
129 let mut n_asciitable_ext = 0;
130 let mut seen_byte_offsets = vec![];
131 let mut seen_byte_lengths = vec![];
132
133 while let Some(Ok(hdu)) = hdu_list.next() {
134 match &hdu {
135 HDU::Primary(_) | HDU::XImage(_) => {
136 n_image_ext += 1;
137 }
138 HDU::XBinaryTable(_) => {
139 n_bintable_ext += 1;
140 }
141 HDU::XASCIITable(_) => {
142 n_asciitable_ext += 1;
143 }
144 };
145 seen_byte_lengths.push(hdu.get_data_unit_byte_size());
146 seen_byte_offsets.push(hdu.get_data_unit_byte_offset())
147 }
148
149 assert_eq!(n_image_ext, num_image_ext);
150 assert_eq!(n_bintable_ext, num_bintable_ext);
151 assert_eq!(n_asciitable_ext, num_asciitable_ext);
152 assert_eq!(seen_byte_offsets, byte_offsets);
153 assert_eq!(seen_byte_lengths, byte_lengths);
154 }
155
156 #[test]
157 fn test_fits_image_f32() {
158 let f = BufReader::new(File::open("samples/hipsgen/Npix208.fits").unwrap());
159 let bytes: Result<Vec<_>, _> = f.bytes().collect();
160 let buf = bytes.unwrap();
161
162 let reader = Cursor::new(&buf[..]);
163 let mut hdu_list = Fits::from_reader(reader);
164
165 let hdu = hdu_list.next().unwrap().unwrap();
166 assert!(matches!(hdu, HDU::Primary(_)));
167 if let HDU::Primary(hdu) = hdu {
168 let header = hdu.get_header();
169 let num_pixels = header.get_xtension().get_num_pixels();
170 let image = hdu_list.get_data(&hdu);
171 match image.pixels() {
172 Pixels::F32(it) => {
173 assert!(it.count() as u64 == num_pixels);
174 }
175 _ => unreachable!(),
176 }
177 } else {
178 unreachable!();
179 }
180 }
181
182 #[test]
183 fn test_fits_i16() {
184 let mut f = File::open("samples/hipsgen/Npix4906.fits").unwrap();
185 let mut raw_bytes = Vec::<u8>::new();
186 f.read_to_end(&mut raw_bytes).unwrap();
187
188 let reader = Cursor::new(&raw_bytes[..]);
189 let mut hdu_list = Fits::from_reader(reader);
190
191 let hdu = hdu_list.next().unwrap().unwrap();
192 assert!(matches!(hdu, HDU::Primary(_)));
193 if let HDU::Primary(hdu) = hdu {
194 let header = hdu.get_header();
195 let num_pixels = header.get_xtension().get_num_pixels();
196 let image = hdu_list.get_data(&hdu);
197 match image.pixels() {
198 Pixels::I16(data) => {
199 assert!(data.count() as u64 == num_pixels)
200 }
201 _ => unreachable!(),
202 }
203 } else {
204 unreachable!();
205 }
206 }
207
208 #[test_case("samples/fits.gsfc.nasa.gov/Astro_UIT.fits", true)]
209 #[test_case("samples/hipsgen/Npix8.fits", false)]
210 #[test_case("samples/hipsgen/Npix9.fits", false)]
211 #[test_case("samples/hipsgen/Npix132.fits", false)]
212 #[test_case("samples/hipsgen/Npix133.fits", false)]
213 #[test_case("samples/hipsgen/Npix134.fits", false)]
214 #[test_case("samples/hipsgen/Npix140.fits", false)]
215 #[test_case("samples/hipsgen/Npix208.fits", false)]
216 #[test_case("samples/hipsgen/Npix282.fits", false)]
217 #[test_case("samples/hipsgen/Npix4906.fits", false)]
218 #[test_case("samples/hipsgen/Npix691539.fits", false)]
219 #[test_case("samples/hips2fits/allsky_panstarrs.fits", false)]
220 #[test_case("samples/hips2fits/cutout-CDS_P_HST_PHAT_F475W.fits", false)]
221 #[test_case("samples/fits.gsfc.nasa.gov/EUVE.fits", false)]
222 #[test_case("samples/fits.gsfc.nasa.gov/HST_FGS.fits", false)]
223 #[test_case("samples/fits.gsfc.nasa.gov/HST_FOC.fits", false)]
224 #[test_case("samples/fits.gsfc.nasa.gov/HST_FOS.fits", false)]
225 #[test_case("samples/fits.gsfc.nasa.gov/HST_HRS.fits", false)]
226 #[test_case("samples/fits.gsfc.nasa.gov/HST_NICMOS.fits", false)]
227 #[test_case("samples/fits.gsfc.nasa.gov/HST_WFPC_II.fits", false)]
228 #[test_case("samples/fits.gsfc.nasa.gov/HST_WFPC_II_bis.fits", false)]
229 #[test_case("samples/vizier/NVSSJ235137-362632r.fits", false)]
230 #[test_case("samples/vizier/VAR.358.R.fits", false)]
231 #[test_case("samples/fits.gsfc.nasa.gov/IUE_LWP.fits", false)]
232 #[test_case("samples/misc/bonn.fits", false)]
233 #[test_case("samples/misc/EUC_MER_MOSAIC-VIS-FLAG_TILE100158585-1EC1C5_20221211T132329.822037Z_00.00.fits", false)]
234 #[test_case("samples/misc/P122_49.fits", false)]
235 #[test_case("samples/misc/skv1678175163788.fits", false)]
236 #[test_case("samples/misc/SN2923fxjA.fits", false)]
237 fn test_fits_opening(filename: &str, ground_truth: bool) {
238 let hdu_list = FITSFile::open(filename).expect("Can find fits file");
239
240 let mut corrupted = false;
241 for hdu in hdu_list {
242 if hdu.is_err() {
243 corrupted = true;
244 }
245 }
246
247 assert_eq!(ground_truth, corrupted);
248 }
249
250 #[test]
251 fn test_fits_not_fitting_in_memory() {
252 use std::fs::File;
253 use std::io::BufReader;
254 let f = File::open("samples/fits.gsfc.nasa.gov/EUVE.fits").unwrap();
255 let reader = BufReader::new(f);
256 let mut hdu_list = Fits::from_reader(reader);
257
258 while let Some(Ok(HDU::XImage(hdu))) = hdu_list.next() {
259 let num_pixels = hdu.get_header().get_xtension().get_num_pixels();
260
261 if let Ok(wcs) = hdu.wcs() {
263 let xy = ImgXY::new(0.0, 0.0);
265 let _lonlat = wcs.unproj_lonlat(&xy).unwrap();
266 }
267
268 let image = hdu_list.get_data(&hdu);
269 assert_eq!(
270 num_pixels as usize,
271 match image.pixels() {
272 Pixels::I16(it) => it.count(),
273 Pixels::U8(it) => it.count(),
274 Pixels::I32(it) => it.count(),
275 Pixels::I64(it) => it.count(),
276 Pixels::F32(it) => it.count(),
277 Pixels::F64(it) => it.count(),
278 }
279 );
280 }
281 }
282
283 #[test]
284 fn test_fits_image_borrowed() {
285 use std::fs::File;
286
287 let mut f = File::open("samples/fits.gsfc.nasa.gov/HST_FOC.fits").unwrap();
288 let mut buf = Vec::new();
289 f.read_to_end(&mut buf).unwrap();
290
291 let reader = Cursor::new(&buf[..]);
292 let mut hdu_list = Fits::from_reader(reader);
293
294 if let Some(Ok(HDU::Primary(hdu))) = hdu_list.next() {
295 let num_pixels = hdu.get_header().get_xtension().get_num_pixels();
296 let image = hdu_list.get_data(&hdu);
297 match image.pixels() {
298 Pixels::F32(data) => {
299 assert_eq!(data.count(), num_pixels as usize);
300 }
301 _ => unreachable!(),
302 }
303 }
304 }
305
306 #[test]
307 fn test_fits_bintable() {
308 use std::fs::File;
309
310 let f = File::open("samples/vizier/II_278_transit.fits").unwrap();
311
312 let reader = BufReader::new(f);
313 let mut hdu_list = Fits::from_reader(reader);
314 let mut data_len = 0;
315 while let Some(Ok(hdu)) = hdu_list.next() {
316 if let HDU::XBinaryTable(hdu) = hdu {
317 let _ = hdu.get_header().get_xtension();
318 data_len = hdu_list.get_data(&hdu).table_data().count();
319 }
320 }
321
322 assert_eq!(177 * 23, data_len);
323 }
324
325 #[test]
326 fn test_fits_bintable_corr() {
327 use std::fs::File;
328
329 let f = File::open("samples/astrometry.net/corr.fits").unwrap();
330
331 let reader = BufReader::new(f);
332 let mut hdu_list = Fits::from_reader(reader);
333 while let Some(Ok(hdu)) = hdu_list.next() {
334 if let HDU::XBinaryTable(hdu) = hdu {
335 let data_len = hdu_list
336 .get_data(&hdu)
337 .table_data()
338 .select_fields(&[
339 ColumnId::Name("phot_bp_mean_mag"),
342 ColumnId::Name("phot_rp_mean_mag"),
343 ColumnId::Name("mag"),
344 ])
345 .count();
346
347 assert_eq!(data_len, 3 * 52);
348 }
349 }
350 }
351
352 #[test_case("samples/misc/SN2923fxjA.fits.gz", 5415.0, 6386.0)]
353 #[test_case("samples/misc/SN2923fxjA.fits", 5415.0, 6386.0)]
354 fn test_fits_open_external_gzipped_file(filename: &str, min: f32, max: f32) {
355 let mut hdu_list = FITSFile::open(filename).unwrap();
356 use std::iter::Iterator;
357
358 while let Some(Ok(hdu)) = hdu_list.next() {
359 match hdu {
360 HDU::Primary(hdu) | HDU::XImage(hdu) => {
361 let xtension = hdu.get_header().get_xtension();
362 let [naxis1, naxis2] = *xtension.get_naxis() else {
363 panic!("Wrong number of axis in the header")
364 };
365
366 let image = hdu_list.get_data(&hdu);
367 if let Pixels::F32(it) = image.pixels() {
368 let c = it
369 .map(|v| (((v - min) / (max - min)) * 255.0) as u8)
370 .collect::<Vec<_>>();
371
372 let imgbuf = DynamicImage::ImageLuma8(
373 image::ImageBuffer::from_raw(naxis1 as u32, naxis2 as u32, c).unwrap(),
374 );
375 imgbuf.save(format!("{filename}.jpg")).unwrap();
376 };
377 }
378 _ => (),
379 }
380 }
381 }
382
383 use super::hdu::HDU;
384 #[test]
385 fn test_fits_images_data_block() {
386 use std::fs::File;
387
388 let mut f = File::open("samples/fits.gsfc.nasa.gov/EUVE.fits").unwrap();
389 let mut buf = Vec::new();
390 f.read_to_end(&mut buf).unwrap();
391 let reader = Cursor::new(&buf[..]);
392
393 let mut hdu_list = Fits::from_reader(reader);
394
395 while let Some(Ok(hdu)) = hdu_list.next() {
396 match hdu {
397 HDU::XImage(hdu) | HDU::Primary(hdu) => {
398 let num_pixels = hdu.get_header().get_xtension().get_num_pixels();
399
400 let image = hdu_list.get_data(&hdu);
401 assert_eq!(
402 num_pixels as usize,
403 match image.pixels() {
404 Pixels::U8(it) => it.count(),
405 Pixels::I16(it) => it.count(),
406 Pixels::I32(it) => it.count(),
407 Pixels::I64(it) => it.count(),
408 Pixels::F32(it) => it.count(),
409 Pixels::F64(it) => it.count(),
410 }
411 );
412 }
413 HDU::XBinaryTable(hdu) => {
414 let _num_bytes = hdu.get_header().get_xtension().get_num_bytes_data_block();
415 let _data = hdu_list.get_data(&hdu);
416 }
421 HDU::XASCIITable(hdu) => {
422 let num_bytes = hdu.get_header().get_xtension().get_num_bytes_data_block();
423 let bytes = hdu_list.get_data(&hdu);
424
425 assert_eq!(num_bytes as usize, bytes.bytes().count());
426 }
427 }
428 }
429 }
430
431 #[test]
432 fn test_fits_images_data_block_bufreader() {
433 use std::fs::File;
434
435 let f = File::open("samples/fits.gsfc.nasa.gov/EUVE.fits").unwrap();
436 let reader = BufReader::new(f);
437
438 let mut hdu_list = Fits::from_reader(reader);
439
440 while let Some(Ok(hdu)) = hdu_list.next() {
441 match hdu {
442 HDU::XImage(hdu) => {
443 let num_pixels = hdu.get_header().get_xtension().get_num_pixels();
444
445 let image = hdu_list.get_data(&hdu);
446 assert_eq!(
447 num_pixels as usize,
448 match image.pixels() {
449 Pixels::U8(it) => it.count(),
450 Pixels::I16(it) => it.count(),
451 Pixels::I32(it) => it.count(),
452 Pixels::I64(it) => it.count(),
453 Pixels::F32(it) => it.count(),
454 Pixels::F64(it) => it.count(),
455 }
456 );
457 }
458 HDU::XBinaryTable(_) => {
459 }
465 HDU::XASCIITable(hdu) => {
466 let num_bytes = hdu.get_header().get_xtension().get_num_bytes_data_block();
467
468 assert_eq!(num_bytes as usize, hdu_list.get_data(&hdu).bytes().count());
469 }
470 _ => (),
471 }
472 }
473 }
474
475 #[test]
476 fn test_bad_bytes() {
477 let bytes: &[u8] = &[
478 60, 33, 68, 79, 67, 84, 89, 80, 69, 32, 72, 84, 77, 76, 32, 80, 85, 66, 76, 73, 67, 32,
479 34, 45, 47, 47, 73, 69, 84, 70, 47, 47, 68, 84, 68, 32, 72, 84, 77, 76, 32, 50, 46, 48,
480 47, 47, 69, 78, 34, 62, 10, 60, 104, 116, 109, 108, 62, 60, 104, 101, 97, 100, 62, 10,
481 60, 116, 105, 116, 108, 101, 62, 52, 48, 52, 32, 78, 111, 116, 32, 70, 111, 117, 110,
482 100, 60, 47, 116, 105, 116, 108, 101, 62, 10, 60, 47, 104, 101, 97, 100, 62, 60, 98,
483 111, 100, 121, 62, 10, 60, 104, 49, 62, 78, 111, 116, 32, 70, 111, 117, 110, 100, 60,
484 47, 104, 49, 62, 10, 60, 112, 62, 84, 104, 101, 32, 114, 101, 113, 117, 101, 115, 116,
485 101, 100, 32, 85, 82, 76, 32, 47, 97, 108, 108, 115, 107, 121, 47, 80, 78, 82, 101,
486 100, 47, 78, 111, 114, 100, 101, 114, 55, 47, 68, 105, 114, 52, 48, 48, 48, 48, 47, 78,
487 112, 105, 120, 52, 52, 49, 49, 49, 46, 102, 105, 116, 115, 32, 119, 97, 115, 32, 110,
488 111, 116, 32, 102, 111, 117, 110, 100, 32, 111, 110, 32, 116, 104, 105, 115, 32, 115,
489 101, 114, 118, 101, 114, 46, 60, 47, 112, 62, 10, 60, 47, 98, 111, 100, 121, 62, 60,
490 47, 104, 116, 109, 108, 62, 10,
491 ];
492 let reader = Cursor::new(bytes);
493 let mut hdu_list = Fits::from_reader(reader);
494 assert!(hdu_list.next().unwrap().is_err());
495 }
496
497 #[test_case("samples/fits.gsfc.nasa.gov/EUVE.fits")]
500 #[test_case("samples/fits.gsfc.nasa.gov/HST_FOC.fits")]
501 #[test_case("samples/vizier/new_url.fits")]
502 #[tokio::test]
503 async fn test_fits_images_data_block_bufreader_async(filename: &str) {
504 let mut f = File::open(filename).unwrap();
508 let mut buf = Vec::new();
509 f.read_to_end(&mut buf).unwrap();
510
511 let reader = futures::io::BufReader::new(&buf[..]);
512
513 let mut hdu_list = AsyncFits::from_reader(reader);
514
515 while let Some(Ok(hdu)) = hdu_list.next().await {
516 match hdu {
517 AsyncHDU::XImage(hdu) | AsyncHDU::Primary(hdu) => {
518 let num_pixels = hdu.get_header().get_xtension().get_num_pixels();
519
520 assert_eq!(
521 num_pixels as usize,
522 match hdu_list.get_data(&hdu) {
523 DataStream::U8(st) => st.count().await,
524 DataStream::I16(st) => st.count().await,
525 DataStream::I32(st) => st.count().await,
526 DataStream::I64(st) => st.count().await,
527 DataStream::F32(st) => st.count().await,
528 DataStream::F64(st) => st.count().await,
529 }
530 );
531 }
532 AsyncHDU::XBinaryTable(hdu) => {
533 let num_bytes = hdu.get_header().get_xtension().get_num_bytes_data_block();
534
535 assert_eq!(num_bytes as usize, hdu_list.get_data(&hdu).count().await);
536 }
537 AsyncHDU::XASCIITable(hdu) => {
538 let num_bytes = hdu.get_header().get_xtension().get_num_bytes_data_block();
539
540 assert_eq!(num_bytes as usize, hdu_list.get_data(&hdu).count().await);
541 }
542 }
543 }
544 }
545
546 #[test]
547 fn test_fits_euve() {
548 use std::fs::File;
549 use std::io::BufReader;
550
551 let f = File::open("samples/fits.gsfc.nasa.gov/EUVE.fits").unwrap();
552 let reader = BufReader::new(f);
553
554 let mut hdu_list = Fits::from_reader(reader);
555
556 while let Some(Ok(hdu)) = hdu_list.next() {
557 match hdu {
558 HDU::Primary(_) => (),
560 HDU::XImage(hdu) => {
561 let num_pixels = hdu.get_header().get_xtension().get_num_pixels();
562
563 let data = hdu_list.get_data(&hdu);
564 assert_eq!(
565 num_pixels as usize,
566 match data.pixels() {
567 Pixels::U8(it) => it.count(),
568 Pixels::I16(it) => it.count(),
569 Pixels::I32(it) => it.count(),
570 Pixels::I64(it) => it.count(),
571 Pixels::F32(it) => it.count(),
572 Pixels::F64(it) => it.count(),
573 }
574 );
575 }
576 HDU::XBinaryTable(hdu) => {
577 let num_rows = hdu.get_header().get_xtension().get_num_rows();
578
579 assert_eq!(num_rows, hdu_list.get_data(&hdu).row_iter().count());
580 }
581 HDU::XASCIITable(hdu) => {
582 let num_bytes = hdu.get_header().get_xtension().get_num_bytes_data_block();
583
584 assert_eq!(num_bytes as usize, hdu_list.get_data(&hdu).bytes().count());
585 }
586 }
587 }
588 }
589}