LSST Applications g00d0e8bbd7+edbf708997,g03191d30f7+9ce8016dbd,g1955dfad08+0bd186d245,g199a45376c+5137f08352,g1fd858c14a+a888a50aa2,g262e1987ae+45f9aba685,g29ae962dfc+1c7d47a24f,g2cef7863aa+73c82f25e4,g35bb328faa+edbf708997,g3fd5ace14f+eed17d2c67,g47891489e3+6dc8069a4c,g53246c7159+edbf708997,g64539dfbff+c4107e45b5,g67b6fd64d1+6dc8069a4c,g74acd417e5+f452e9c21a,g786e29fd12+af89c03590,g7ae74a0b1c+a25e60b391,g7aefaa3e3d+2025e9ce17,g7cc15d900a+2d158402f9,g87389fa792+a4172ec7da,g89139ef638+6dc8069a4c,g8d4809ba88+c4107e45b5,g8d7436a09f+e96c132b44,g8ea07a8fe4+db21c37724,g98df359435+aae6d409c1,ga2180abaac+edbf708997,gac66b60396+966efe6077,gb632fb1845+88945a90f8,gbaa8f7a6c5+38b34f4976,gbf99507273+edbf708997,gca7fc764a6+6dc8069a4c,gd7ef33dd92+6dc8069a4c,gda68eeecaf+7d1e613a8d,gdab6d2f7ff+f452e9c21a,gdbb4c4dda9+c4107e45b5,ge410e46f29+6dc8069a4c,ge41e95a9f2+c4107e45b5,geaed405ab2+e194be0d2b,w.2025.47
LSST Data Management Base Package
Loading...
Searching...
No Matches
fits.cc
Go to the documentation of this file.
1// -*- lsst-c++ -*-
2
3#include <cstdint>
4#include <cstdio>
5#include <complex>
6#include <cmath>
7#include <sstream>
8#include <unordered_set>
9#include <unordered_map>
10#include <filesystem>
11#include <regex>
12#include <cctype>
13#include <type_traits>
14
15#include "fitsio.h"
16extern "C" {
17#include "fitsio2.h"
18}
19
20#include "boost/algorithm/string.hpp"
21#include "boost/preprocessor/seq/for_each.hpp"
22#include "boost/format.hpp"
23
24#include "lsst/pex/exceptions.h"
25#include "lsst/log/Log.h"
26#include "lsst/afw/fits.h"
27#include "lsst/geom/Angle.h"
30
31namespace lsst {
32namespace afw {
33namespace fits {
34
35// ----------------------------------------------------------------------------------------------------------
36// ---- Miscellaneous utilities -----------------------------------------------------------------------------
37// ----------------------------------------------------------------------------------------------------------
38
39namespace {
40
41// FITS BITPIX header constants and the special CFITSIO values for variants
42// with different signedness from the FITS defaults. By using partial
43// specialization we can handle all of (int, long, long long) without
44// knowing which of those is left out of (int32_t, int64_t).
45
46template <typename T>
47constexpr int header_bitpix = static_cast<int>(sizeof(T)) * 8 * (std::is_floating_point_v<T> ? -1 : 1);
48
49template <typename T, bool is_signed = std::is_signed_v<T>, int size = sizeof(T)>
50constexpr int cfitsio_bitpix = header_bitpix<T>;
51
52template <typename T>
53constexpr int cfitsio_bitpix<T, true, 1> = SBYTE_IMG;
54
55template <typename T>
56constexpr int cfitsio_bitpix<T, false, 2> = USHORT_IMG;
57
58template <typename T>
59constexpr int cfitsio_bitpix<T, false, 4> = ULONG_IMG;
60
61template <typename T>
62constexpr int cfitsio_bitpix<T, false, 8> = ULONGLONG_IMG;
63
65
66/*
67 * Format a PropertySet into a FITS header string using simplifying assumptions.
68 *
69 * See @ref makeLimitedFitsHeader for details.
70 *
71 * @param[in] paramNames Names of properties to format
72 * @param[in] metadata Metadata to format
73 * @return a FITS header string (exactly 80 characters per entry, no line terminators)
74 */
75std::string makeLimitedFitsHeaderImpl(std::vector<std::string> const &paramNames,
76 daf::base::PropertySet const &metadata) {
77 std::ostringstream result;
78 for (auto const &fullName : paramNames) {
79 std::size_t lastPeriod = fullName.rfind(char('.'));
80 auto name = (lastPeriod == std::string::npos) ? fullName : fullName.substr(lastPeriod + 1);
81 std::type_info const &type = metadata.typeOf(name);
82
83 std::string out = "";
84 out.reserve(80);
85 if (name.size() > 8) {
86 continue; // The name is too long for a FITS keyword; skip this item
87 }
88 out = (boost::format("%-8s= ") % name).str();
89
90 if (type == typeid(bool)) {
91 out += metadata.get<bool>(name) ? "T" : "F";
92 } else if (type == typeid(std::uint8_t)) {
93 out += (boost::format("%20d") % static_cast<int>(metadata.get<std::uint8_t>(name))).str();
94 } else if (type == typeid(int)) {
95 out += (boost::format("%20d") % metadata.get<int>(name)).str();
96 } else if (type == typeid(double)) {
97 double value = metadata.get<double>(name);
98 if (!std::isnan(value)) {
99 // use G because FITS wants uppercase E for exponents
100 out += (boost::format("%#20.17G") % value).str();
101 } else {
102 LOGLS_WARN("lsst.afw.fits",
103 boost::format("In %s, found NaN in metadata item '%s'") %
104 BOOST_CURRENT_FUNCTION % name);
105 // Convert it to FITS undefined
106 out += " ";
107 }
108 } else if (type == typeid(float)) {
109 float value = metadata.get<float>(name);
110 if (!std::isnan(value)) {
111 out += (boost::format("%#20.15G") % value).str();
112 } else {
113 LOGLS_WARN("lsst.afw.fits",
114 boost::format("In %s, found NaN in metadata item '%s'") %
115 BOOST_CURRENT_FUNCTION % name);
116 // Convert it to FITS undefined
117 out += " ";
118 }
119 } else if (type == typeid(std::nullptr_t)) {
120 out += " ";
121 } else if (type == typeid(std::string)) {
122 out += "'" + metadata.get<std::string>(name) + "'";
123 if (out.size() > 80) {
124 continue; // Formatted data is too long; skip this item
125 }
126 }
127
128 int const len = out.size();
129 if (len < 80) {
130 out += std::string(80 - len, ' ');
131 } else if (len > 80) {
132 // non-string item has a formatted value that is too long; this should never happen
133 throw LSST_EXCEPT(pex::exceptions::LogicError,
134 "Formatted data too long: " + std::to_string(len) + " > 80: \"" + out + "\"");
135 }
136
137 result << out;
138 }
139
140 return result.str();
141}
142
151class StringStartSet {
152public:
154 StringStartSet(std::initializer_list<std::string> const &input) : _minSize(-1) {
155 for (auto const &word : input) {
156 std::size_t const size = word.size();
157 if (size < _minSize) {
158 _minSize = size;
159 }
160 }
161 for (auto const &word : input) {
162 std::string const start = startString(word);
163 assert(_words.count(start) == 0); // This should be the only word that starts this way
164 _words[start] = word;
165 }
166 }
167
169 bool matches(std::string const &key) const {
170 auto const iter = _words.find(startString(key));
171 if (iter == _words.end()) {
172 return false;
173 }
174 // Check that the full word matches too
175 std::string const &word = iter->second;
176 return key.compare(0, word.size(), word) == 0;
177 }
178
179private:
180 using Map = std::unordered_map<std::string, std::string>;
181
183 std::string startString(std::string const &word) const { return word.substr(0, _minSize); }
184
185 std::size_t _minSize; // Minimum length of provided words
186 Map _words; // Start of words --> full word
187};
188
194static std::unordered_set<std::string> const ignoreKeys = {
195 // FITS core keywords
196 "SIMPLE", "BITPIX", "NAXIS", "EXTEND", "GCOUNT", "PCOUNT", "XTENSION", "TFIELDS", "BSCALE", "BZERO",
197 // FITS compression keywords
198 "ZBITPIX", "ZIMAGE", "ZCMPTYPE", "ZSIMPLE", "ZEXTEND", "ZBLANK", "ZDATASUM", "ZHECKSUM", "ZQUANTIZ",
199 "ZDITHER0",
200 // Not essential, but will prevent fitsverify warnings
201 "DATASUM", "CHECKSUM"};
202
208StringStartSet const ignoreKeyStarts{// FITS core keywords
209 "NAXIS", "TZERO", "TSCAL",
210 // FITS compression keywords
211 "ZNAXIS", "ZTILE", "ZNAME", "ZVAL"};
212
218StringStartSet const ignoreKeyStartsWrite{"TFORM", "TTYPE"};
219
220// Strip leading and trailing single quotes and whitespace from a string.
221std::string strip(std::string const &s) {
222 if (s.empty()) return s;
223 std::size_t i1 = s.find_first_not_of(" '");
224 if (i1 == std::string::npos) {
225 return std::string();
226 }
227 std::size_t i2 = s.find_last_not_of(" '");
228 // if there's an i1, there must be an i2
229 return s.substr(i1, 1 + i2 - i1);
230}
231
232// ---- FITS binary table format codes for various C++ types. -----------------------------------------------
233
234char getFormatCode(bool *) { return 'X'; }
235char getFormatCode(std::string *) { return 'A'; }
236char getFormatCode(std::int8_t *) { return 'S'; }
237char getFormatCode(std::uint8_t *) { return 'B'; }
238char getFormatCode(std::int16_t *) { return 'I'; }
239char getFormatCode(std::uint16_t *) { return 'U'; }
240char getFormatCode(std::int32_t *) { return 'J'; }
241char getFormatCode(std::uint32_t *) { return 'V'; }
242char getFormatCode(std::int64_t *) { return 'K'; }
243char getFormatCode(float *) { return 'E'; }
244char getFormatCode(double *) { return 'D'; }
245char getFormatCode(std::complex<float> *) { return 'C'; }
246char getFormatCode(std::complex<double> *) { return 'M'; }
247char getFormatCode(lsst::geom::Angle *) { return 'D'; }
248
249// ---- Create a TFORM value for the given type and size ----------------------------------------------------
250
251template <typename T>
252std::string makeColumnFormat(int size = 1) {
253 if (size > 0) {
254 return (boost::format("%d%c") % size % getFormatCode((T *)nullptr)).str();
255 } else if (size < 0) {
256 // variable length, max size given as -size
257 return (boost::format("1Q%c(%d)") % getFormatCode((T *)nullptr) % (-size)).str();
258 } else {
259 // variable length, max size unknown
260 return (boost::format("1Q%c") % getFormatCode((T *)nullptr)).str();
261 }
262}
263
264// ---- Traits class to get cfitsio type constants from templates -------------------------------------------
265
266template <typename T>
267struct FitsType;
268
269template <>
270struct FitsType<bool> {
271 static int const CONSTANT = TLOGICAL;
272};
273template <>
274struct FitsType<char> {
275 static int const CONSTANT = TSTRING;
276};
277template <>
278struct FitsType<signed char> {
279 static int const CONSTANT = TSBYTE;
280};
281template <>
282struct FitsType<unsigned char> {
283 static int const CONSTANT = TBYTE;
284};
285template <>
286struct FitsType<short> {
287 static int const CONSTANT = TSHORT;
288};
289template <>
290struct FitsType<unsigned short> {
291 static int const CONSTANT = TUSHORT;
292};
293template <>
294struct FitsType<int> {
295 static int const CONSTANT = TINT;
296};
297template <>
298struct FitsType<unsigned int> {
299 static int const CONSTANT = TUINT;
300};
301template <>
302struct FitsType<long> {
303 static int const CONSTANT = TLONG;
304};
305template <>
306struct FitsType<unsigned long> {
307 static int const CONSTANT = TULONG;
308};
309template <>
310struct FitsType<long long> {
311 static int const CONSTANT = TLONGLONG;
312};
313template <>
314struct FitsType<unsigned long long> {
315 static int const CONSTANT = TULONGLONG;
316};
317template <>
318struct FitsType<float> {
319 static int const CONSTANT = TFLOAT;
320};
321template <>
322struct FitsType<double> {
323 static int const CONSTANT = TDOUBLE;
324};
325template <>
326struct FitsType<lsst::geom::Angle> {
327 static int const CONSTANT = TDOUBLE;
328};
329template <>
330struct FitsType<std::complex<float> > {
331 static int const CONSTANT = TCOMPLEX;
332};
333template <>
334struct FitsType<std::complex<double> > {
335 static int const CONSTANT = TDBLCOMPLEX;
336};
337
338// We use TBIT when writing booleans to table cells, but TLOGICAL in headers.
339template <typename T>
340struct FitsTableType : public FitsType<T> {};
341template <>
342struct FitsTableType<bool> {
343 static int const CONSTANT = TBIT;
344};
345
346bool isFitsImageTypeSigned(int constant) {
347 switch (constant) {
348 case BYTE_IMG: return false;
349 case SHORT_IMG: return true;
350 case USHORT_IMG: return false;
351 case LONG_IMG: return true;
352 case ULONG_IMG: return false;
353 case LONGLONG_IMG: return true;
354 case ULONGLONG_IMG: return false;
355 case FLOAT_IMG: return true;
356 case DOUBLE_IMG: return true;
357 }
358 throw LSST_EXCEPT(pex::exceptions::InvalidParameterError, "Invalid constant.");
359}
360
361/*
362 * Information about one item of metadata: is a comment? is valid?
363 *
364 * See isCommentIsValid for more information.
365 */
366struct ItemInfo {
367 ItemInfo(bool isComment, bool isValid) : isComment(isComment), isValid(isValid) {}
368 bool isComment;
369 bool isValid;
370};
371
372/*
373 * Is an item a commnt (or history) and is it usable in a FITS header?
374 *
375 * For an item to be valid:
376 * - If name is COMMENT or HISTORY then the item must be of type std::string
377 * - All other items are always valid
378 */
379ItemInfo isCommentIsValid(daf::base::PropertyList const &pl, std::string const &name) {
380 if (!pl.exists(name)) {
381 return ItemInfo(false, false);
382 }
383 std::type_info const &type = pl.typeOf(name);
384 if ((name == "COMMENT") || (name == "HISTORY")) {
385 return ItemInfo(true, type == typeid(std::string));
386 }
387 return ItemInfo(false, true);
388}
389
390} // namespace
391
392// ----------------------------------------------------------------------------------------------------------
393// ---- Implementations for stuff in fits.h -----------------------------------------------------------------
394// ----------------------------------------------------------------------------------------------------------
395
396std::string makeErrorMessage(std::string const &fileName, int status, std::string const &msg) {
398 os << "cfitsio error";
399 if (fileName != "") {
400 os << " (" << fileName << ")";
401 }
402 if (status != 0) {
403 char fitsErrMsg[FLEN_ERRMSG];
404 fits_get_errstatus(status, fitsErrMsg);
405 os << ": " << fitsErrMsg << " (" << status << ")";
406 }
407 if (msg != "") {
408 os << " : " << msg;
409 }
410 os << "\ncfitsio error stack:\n";
411 char cfitsioMsg[FLEN_ERRMSG];
412 // fits_read_errmsg can return a junk string with non printable characters
413 // creating problem with python exception bindings
414 while (fits_read_errmsg(cfitsioMsg) != 0) {
415 cfitsioMsg[FLEN_ERRMSG-1] = char(0); // ensure termination
416 std::size_t len=strlen(cfitsioMsg);
417 for(std::size_t i = 0; i < len; i++)
418 if( !isprint(cfitsioMsg[i]) ) cfitsioMsg[i] = '.';
419 os << " " << cfitsioMsg << "\n";
420 }
421 return os.str();
422}
423
424std::string makeErrorMessage(void *fptr, int status, std::string const &msg) {
425 std::string fileName = "";
426 fitsfile *fd = reinterpret_cast<fitsfile *>(fptr);
427 if (fd != nullptr && fd->Fptr != nullptr && fd->Fptr->filename != nullptr) {
428 fileName = fd->Fptr->filename;
429 }
430 return makeErrorMessage(fileName, status, msg);
431}
432
434 std::set<std::string> const &excludeNames) {
435 daf::base::PropertyList const *pl = dynamic_cast<daf::base::PropertyList const *>(&metadata);
436 std::vector<std::string> allParamNames;
437 if (pl) {
438 allParamNames = pl->getOrderedNames();
439 } else {
440 allParamNames = metadata.paramNames(false);
441 }
442 std::vector<std::string> desiredParamNames;
443 for (auto const &name : allParamNames) {
444 if (excludeNames.count(name) == 0) {
445 desiredParamNames.push_back(name);
446 }
447 }
448 return makeLimitedFitsHeaderImpl(desiredParamNames, metadata);
449}
450
452 if (_managed) std::free(_ptr);
453 _ptr = nullptr;
454 _len = 0;
455 _managed = true;
456}
457
459 reset();
460 _ptr = std::malloc(len);
461 _len = len;
462 _managed = true;
463}
464
465template <typename T>
467 return cfitsio_bitpix<T>;
468}
469
470// ----------------------------------------------------------------------------------------------------------
471// ---- Implementations for Fits class ----------------------------------------------------------------------
472// ----------------------------------------------------------------------------------------------------------
473
475 std::string fileName = "<unknown>";
476 fitsfile *fd = reinterpret_cast<fitsfile *>(fptr);
477 if (fd != nullptr && fd->Fptr != nullptr && fd->Fptr->filename != nullptr) {
478 fileName = fd->Fptr->filename;
479 }
480 return fileName;
481}
482
484 int n = 1;
485 fits_get_hdu_num(reinterpret_cast<fitsfile *>(fptr), &n);
486 return n - 1;
487}
488
489void Fits::setHdu(int hdu, bool relative) {
490 if (relative) {
491 fits_movrel_hdu(reinterpret_cast<fitsfile *>(fptr), hdu, nullptr, &status);
492 if (behavior & AUTO_CHECK) {
493 LSST_FITS_CHECK_STATUS(*this, boost::format("Incrementing HDU by %d") % hdu);
494 }
495 } else {
496 if (hdu != DEFAULT_HDU) {
497 fits_movabs_hdu(reinterpret_cast<fitsfile *>(fptr), hdu + 1, nullptr, &status);
498 }
499 if (hdu == DEFAULT_HDU && getHdu() == 0 && getImageDim() == 0) {
500 // want a silent failure here
501 int tmpStatus = status;
502 fits_movrel_hdu(reinterpret_cast<fitsfile *>(fptr), 1, nullptr, &tmpStatus);
503 }
504 if (behavior & AUTO_CHECK) {
505 LSST_FITS_CHECK_STATUS(*this, boost::format("Moving to HDU %d") % hdu);
506 }
507 }
508}
509
510void Fits::setHdu(std::string const &name, HduType hdutype, int hduver) {
511 fits_movnam_hdu(reinterpret_cast<fitsfile *>(fptr), static_cast<int>(hdutype),
512 const_cast<char *>(name.c_str()), hduver, &status);
513 if (behavior & AUTO_CHECK)
514 LSST_FITS_CHECK_STATUS(*this, boost::format("Moving to named HDU %s, type %d, hduver %d") % name %
515 static_cast<int>(hdutype) % hduver);
516}
517
519 int n = 0;
520 fits_get_num_hdus(reinterpret_cast<fitsfile *>(fptr), &n, &status);
521 if (behavior & AUTO_CHECK) {
522 LSST_FITS_CHECK_STATUS(*this, "Getting number of HDUs in file.");
523 }
524 return n;
525}
526
527// ---- Writing and updating header keys --------------------------------------------------------------------
528
529namespace {
530
531// Impl functions in the anonymous namespace do special handling for strings, bools, and IEEE fp values.
532
539std::string nonFiniteDoubleToString(double value) {
540 if (std::isfinite(value)) {
541 return "";
542 }
543 if (std::isnan(value)) {
544 return "NAN";
545 }
546 if (value < 0) {
547 return "-INFINITY";
548 }
549 return "+INFINITY";
550}
551
557double stringToNonFiniteDouble(std::string const &value) {
558 if (value == "NAN") {
560 }
561 if (value == "+INFINITY") {
563 }
564 if (value == "-INFINITY") {
566 }
567 return 0;
568}
569
570template <typename T>
571void updateKeyImpl(Fits &fits, char const *key, T const &value, char const *comment) {
572 fits_update_key(reinterpret_cast<fitsfile *>(fits.fptr), FitsType<T>::CONSTANT, const_cast<char *>(key),
573 const_cast<T *>(&value), const_cast<char *>(comment), &fits.status);
574}
575
576void updateKeyImpl(Fits &fits, char const *key, std::string const &value, char const *comment) {
577 fits_update_key_longstr(reinterpret_cast<fitsfile *>(fits.fptr), const_cast<char *>(key),
578 const_cast<char *>(value.c_str()), const_cast<char *>(comment), &fits.status);
579}
580
581void updateKeyImpl(Fits &fits, char const *key, bool const &value, char const *comment) {
582 int v = value;
583 fits_update_key(reinterpret_cast<fitsfile *>(fits.fptr), TLOGICAL, const_cast<char *>(key), &v,
584 const_cast<char *>(comment), &fits.status);
585}
586
587void updateKeyImpl(Fits &fits, char const *key, double const &value, char const *comment) {
588 std::string strValue = nonFiniteDoubleToString(value);
589 if (!strValue.empty()) {
590 updateKeyImpl(fits, key, strValue, comment);
591 } else {
592 fits_update_key(reinterpret_cast<fitsfile *>(fits.fptr), FitsType<double>::CONSTANT,
593 const_cast<char *>(key), const_cast<double *>(&value), const_cast<char *>(comment),
594 &fits.status);
595 }
596}
597
598template <typename T>
599void writeKeyImpl(Fits &fits, char const *key, T const &value, char const *comment) {
600 fits_write_key(reinterpret_cast<fitsfile *>(fits.fptr), FitsType<T>::CONSTANT, const_cast<char *>(key),
601 const_cast<T *>(&value), const_cast<char *>(comment), &fits.status);
602}
603
604void writeKeyImpl(Fits &fits, char const *key, char const *comment) {
605 // Write a key with an undefined value
606 fits_write_key_null(reinterpret_cast<fitsfile *>(fits.fptr), const_cast<char *>(key),
607 const_cast<char *>(comment), &fits.status);
608}
609
610void writeKeyImpl(Fits &fits, char const *key, std::string const &value, char const *comment) {
611 if (strncmp(key, "COMMENT", 7) == 0) {
612 fits_write_comment(reinterpret_cast<fitsfile *>(fits.fptr), const_cast<char *>(value.c_str()),
613 &fits.status);
614 } else if (strncmp(key, "HISTORY", 7) == 0) {
615 fits_write_history(reinterpret_cast<fitsfile *>(fits.fptr), const_cast<char *>(value.c_str()),
616 &fits.status);
617 } else {
618 fits_write_key_longstr(reinterpret_cast<fitsfile *>(fits.fptr), const_cast<char *>(key),
619 const_cast<char *>(value.c_str()), const_cast<char *>(comment), &fits.status);
620 }
621}
622
623void writeKeyImpl(Fits &fits, char const *key, bool const &value, char const *comment) {
624 int v = value;
625 fits_write_key(reinterpret_cast<fitsfile *>(fits.fptr), TLOGICAL, const_cast<char *>(key), &v,
626 const_cast<char *>(comment), &fits.status);
627}
628
629void writeKeyImpl(Fits &fits, char const *key, double const &value, char const *comment) {
630 std::string strValue = nonFiniteDoubleToString(value);
631 if (!strValue.empty()) {
632 writeKeyImpl(fits, key, strValue, comment);
633 } else {
634 fits_write_key(reinterpret_cast<fitsfile *>(fits.fptr), FitsType<double>::CONSTANT,
635 const_cast<char *>(key), const_cast<double *>(&value), const_cast<char *>(comment),
636 &fits.status);
637 }
638}
639
640} // namespace
641
642template <typename T>
643void Fits::updateKey(std::string const &key, T const &value, std::string const &comment) {
644 updateKeyImpl(*this, key.c_str(), value, comment.c_str());
645 if (behavior & AUTO_CHECK) {
646 LSST_FITS_CHECK_STATUS(*this, boost::format("Updating key '%s': '%s'") % key % value);
647 }
648}
649
650template <typename T>
651void Fits::writeKey(std::string const &key, T const &value, std::string const &comment) {
652 writeKeyImpl(*this, key.c_str(), value, comment.c_str());
653 if (behavior & AUTO_CHECK) {
654 LSST_FITS_CHECK_STATUS(*this, boost::format("Writing key '%s': '%s'") % key % value);
655 }
656}
657
658template <typename T>
659void Fits::updateKey(std::string const &key, T const &value) {
660 updateKeyImpl(*this, key.c_str(), value, nullptr);
661 if (behavior & AUTO_CHECK) {
662 LSST_FITS_CHECK_STATUS(*this, boost::format("Updating key '%s': '%s'") % key % value);
663 }
664}
665
666template <typename T>
667void Fits::writeKey(std::string const &key, T const &value) {
668 writeKeyImpl(*this, key.c_str(), value, nullptr);
669 if (behavior & AUTO_CHECK) {
670 LSST_FITS_CHECK_STATUS(*this, boost::format("Writing key '%s': '%s'") % key % value);
671 }
672}
673
674template <typename T>
675void Fits::updateColumnKey(std::string const &prefix, int n, T const &value, std::string const &comment) {
676 updateKey((boost::format("%s%d") % prefix % (n + 1)).str(), value, comment);
677 if (behavior & AUTO_CHECK) {
678 LSST_FITS_CHECK_STATUS(*this, boost::format("Updating key '%s%d': '%s'") % prefix % (n + 1) % value);
679 }
680}
681
682template <typename T>
683void Fits::writeColumnKey(std::string const &prefix, int n, T const &value, std::string const &comment) {
684 writeKey((boost::format("%s%d") % prefix % (n + 1)).str(), value, comment);
685 if (behavior & AUTO_CHECK) {
686 LSST_FITS_CHECK_STATUS(*this, boost::format("Writing key '%s%d': '%s'") % prefix % (n + 1) % value);
687 }
688}
689
690template <typename T>
691void Fits::updateColumnKey(std::string const &prefix, int n, T const &value) {
692 updateKey((boost::format("%s%d") % prefix % (n + 1)).str(), value);
693 if (behavior & AUTO_CHECK) {
694 LSST_FITS_CHECK_STATUS(*this, boost::format("Updating key '%s%d': '%s'") % prefix % (n + 1) % value);
695 }
696}
697
698template <typename T>
699void Fits::writeColumnKey(std::string const &prefix, int n, T const &value) {
700 writeKey((boost::format("%s%d") % prefix % (n + 1)).str(), value);
701 if (behavior & AUTO_CHECK) {
702 LSST_FITS_CHECK_STATUS(*this, boost::format("Writing key '%s%d': '%s'") % prefix % (n + 1) % value);
703 }
704}
705
706// ---- Reading header keys ---------------------------------------------------------------------------------
707
708namespace {
709
710template <typename T>
711void readKeyImpl(Fits &fits, char const *key, T &value) {
712 fits_read_key(reinterpret_cast<fitsfile *>(fits.fptr), FitsType<T>::CONSTANT, const_cast<char *>(key),
713 &value, nullptr, &fits.status);
714}
715
716void readKeyImpl(Fits &fits, char const *key, std::string &value) {
717 char *buf = nullptr;
718 fits_read_key_longstr(reinterpret_cast<fitsfile *>(fits.fptr), const_cast<char *>(key), &buf, nullptr,
719 &fits.status);
720 if (buf) {
721 value = strip(buf);
722 free(buf);
723 }
724}
725
726void readKeyImpl(Fits &fits, char const *key, double &value) {
727 // We need to check for the possibility that the value is a special string (for NAN, +/-Inf).
728 // If a quote mark (') is present then it's a string.
729
730 char buf[FLEN_VALUE];
731 fits_read_keyword(reinterpret_cast<fitsfile *>(fits.fptr), const_cast<char *>(key), buf, nullptr, &fits.status);
732 if (fits.status != 0) {
733 return;
734 }
735 if (std::string(buf).find('\'') != std::string::npos) {
736 std::string unquoted;
737 readKeyImpl(fits, key, unquoted); // Let someone else remove quotes and whitespace
738 if (fits.status != 0) {
739 return;
740 }
741 value = stringToNonFiniteDouble(unquoted);
742 if (value == 0) {
743 throw LSST_EXCEPT(
744 afw::fits::FitsError,
745 (boost::format("Unrecognised string value for keyword '%s' when parsing as double: %s") %
746 key % unquoted)
747 .str());
748 }
749 } else {
750 fits_read_key(reinterpret_cast<fitsfile *>(fits.fptr), FitsType<double>::CONSTANT,
751 const_cast<char *>(key), &value, nullptr, &fits.status);
752 }
753}
754
755} // namespace
756
757template <typename T>
758void Fits::readKey(std::string const &key, T &value) {
759 readKeyImpl(*this, key.c_str(), value);
760 if (behavior & AUTO_CHECK) {
761 LSST_FITS_CHECK_STATUS(*this, boost::format("Reading key '%s'") % key);
762 }
763}
764
766 char key[81]; // allow for terminating NUL
767 char value[81];
768 char comment[81];
769 int nKeys = 0;
770 fits_get_hdrspace(reinterpret_cast<fitsfile *>(fptr), &nKeys, nullptr, &status);
771 std::string keyStr;
772 std::string valueStr;
773 std::string commentStr;
774 int i = 1;
775 while (i <= nKeys) {
776 fits_read_keyn(reinterpret_cast<fitsfile *>(fptr), i, key, value, comment, &status);
777 // fits_read_keyn does not convert the key case on read, like other fits methods in cfitsio>=3.38
778 // We uppercase to try to be more consistent.
779 std::string upperKey(key);
780 boost::to_upper(upperKey);
781 if (upperKey.compare(key) != 0){
782 LOGLS_DEBUG("lsst.afw.fits",
783 boost::format("In %s, standardizing key '%s' to uppercase '%s' on read.") %
784 BOOST_CURRENT_FUNCTION % key % upperKey);
785 }
786 keyStr = upperKey;
787 valueStr = value;
788 commentStr = comment;
789 ++i;
790 while (valueStr.size() > 2 && valueStr[valueStr.size() - 2] == '&' && i <= nKeys) {
791 // we're using key to hold the entire record here; the actual key is safe in keyStr
792 fits_read_record(reinterpret_cast<fitsfile *>(fptr), i, key, &status);
793 if (strncmp(key, "CONTINUE", 8) != 0) {
794 // require both trailing '&' and CONTINUE to invoke long-string handling
795 break;
796 }
797 std::string card = key;
798 valueStr.erase(valueStr.size() - 2);
799 std::size_t firstQuote = card.find('\'');
800 if (firstQuote == std::string::npos) {
801 throw LSST_EXCEPT(
802 FitsError,
804 fptr, status,
805 boost::format("Invalid CONTINUE at header key %d: \"%s\".") % i % card));
806 }
807 std::size_t lastQuote = card.find('\'', firstQuote + 1);
808 if (lastQuote == std::string::npos) {
809 throw LSST_EXCEPT(
810 FitsError,
812 fptr, status,
813 boost::format("Invalid CONTINUE at header key %d: \"%s\".") % i % card));
814 }
815 valueStr += card.substr(firstQuote + 1, lastQuote - firstQuote);
816 std::size_t slash = card.find('/', lastQuote + 1);
817 if (slash != std::string::npos) {
818 commentStr += strip(card.substr(slash + 1));
819 }
820 ++i;
821 }
822 if (behavior & AUTO_CHECK) {
823 LSST_FITS_CHECK_STATUS(*this, boost::format("Reading key '%s'") % keyStr);
824 }
825 functor(keyStr, valueStr, commentStr);
826 }
827}
828
829// ---- Reading and writing PropertySet/PropertyList --------------------------------------------------------
830
831namespace {
832
833bool isKeyIgnored(std::string const &key, bool write = false) {
834 return ((ignoreKeys.find(key) != ignoreKeys.end()) || ignoreKeyStarts.matches(key) ||
835 (write && ignoreKeyStartsWrite.matches(key)));
836}
837
838class MetadataIterationFunctor : public HeaderIterationFunctor {
839public:
840 void operator()(std::string const &key, std::string const &value, std::string const &comment) override;
841
842 template <typename T>
843 void add(std::string const &key, T value, std::string const &comment) {
844 // PropertyList/Set can not support array items where some elements are
845 // defined and some undefined. If we are adding defined value where
846 // previously we have an undefined value we must use set instead.
847 if (list) {
848 if (list->exists(key) && list->isUndefined(key)) {
849 LOGLS_WARN("lsst.afw.fits",
850 boost::format("In %s, replacing undefined value for key '%s'.") %
851 BOOST_CURRENT_FUNCTION % key);
852 list->set(key, value, comment);
853 } else {
854 list->add(key, value, comment);
855 }
856 } else {
857 if (set->exists(key) && set->isUndefined(key)) {
858 LOGLS_WARN("lsst.afw.fits",
859 boost::format("In %s, replacing undefined value for key '%s'.") %
860 BOOST_CURRENT_FUNCTION % key);
861 set->set(key, value);
862 } else {
863 set->add(key, value);
864 }
865 }
866 }
867
868 void add(std::string const &key, std::string const &comment) {
869 // If this undefined value is adding to a pre-existing key that has
870 // a defined value we must skip the add so as not to break
871 // PropertyList/Set.
872 if (list) {
873 if (list->exists(key) && !list->isUndefined(key)) {
874 // Do nothing. Assume the previously defined value takes
875 // precedence.
876 LOGLS_WARN("lsst.afw.fits",
877 boost::format("In %s, dropping undefined value for key '%s'.") %
878 BOOST_CURRENT_FUNCTION % key);
879 } else {
880 list->add(key, nullptr, comment);
881 }
882 } else {
883 if (set->exists(key) && !set->isUndefined(key)) {
884 // Do nothing. Assume the previously defined value takes
885 // precedence.
886 LOGLS_WARN("lsst.afw.fits",
887 boost::format("In %s, dropping undefined value for key '%s'.") %
888 BOOST_CURRENT_FUNCTION % key);
889 } else {
890 set->add(key, nullptr);
891 }
892 }
893 }
894
895 bool strip;
896 daf::base::PropertySet *set;
897 daf::base::PropertyList *list;
898};
899
900void MetadataIterationFunctor::operator()(std::string const &key, std::string const &value,
901 std::string const &comment) {
902 static std::regex const boolRegex("[tTfF]");
903 static std::regex const intRegex("[+-]?[0-9]+");
904 static std::regex const doubleRegex("[+-]?([0-9]*\\.?[0-9]+|[0-9]+\\.?[0-9]*)([eE][+-]?[0-9]+)?");
905 static std::regex const fitsStringRegex("'(.*?) *'");
906 // regex for two-line comment added to all FITS headers by CFITSIO
907 static std::regex const fitsDefinitionCommentRegex(
908 " *(FITS \\(Flexible Image Transport System\\)|and Astrophysics', volume 376, page 359).*");
909 std::smatch matchStrings;
910
911 if (strip && isKeyIgnored(key)) {
912 return;
913 }
914
915 std::istringstream converter(value);
916 if (std::regex_match(value, boolRegex)) {
917 // convert the string to an bool
918 add(key, bool(value == "T" || value == "t"), comment);
919 } else if (std::regex_match(value, intRegex)) {
920 // convert the string to an int
921 std::int64_t val;
922 converter >> val;
923 if (val < (1LL << 31) && val > -(1LL << 31)) {
924 add(key, static_cast<int>(val), comment);
925 } else {
926 add(key, val, comment);
927 }
928 } else if (std::regex_match(value, doubleRegex)) {
929 // convert the string to a double
930 double val;
931 converter >> val;
932 add(key, val, comment);
933 } else if (std::regex_match(value, matchStrings, fitsStringRegex)) {
934 std::string const str = matchStrings[1].str(); // strip off the enclosing single quotes
935 double val = stringToNonFiniteDouble(str);
936 if (val != 0.0) {
937 add(key, val, comment);
938 } else {
939 add(key, str, comment);
940 }
941 } else if (key == "HISTORY") {
942 add(key, comment, "");
943 } else if (key == "COMMENT" && !(strip && std::regex_match(comment, fitsDefinitionCommentRegex))) {
944 add(key, comment, "");
945 } else if (key.empty() && value.empty()) {
946 // This is a blank keyword comment. Since comments do not retain
947 // their position on read there is nothing to be gained by storing
948 // this in the PropertyList as a blank keyword. Therefore store
949 // them with the other comments.
950 add("COMMENT", comment, "");
951 } else if (value.empty()) {
952 // do nothing for empty values that are comments
953 // Otherwise write null value to PropertySet
954 if (key != "COMMENT") {
955 add(key, comment);
956 }
957 } else {
958 throw LSST_EXCEPT(
959 afw::fits::FitsError,
960 (boost::format("Could not parse header value for key '%s': '%s'") % key % value).str());
961 }
962}
963
964void writeKeyFromProperty(Fits &fits, daf::base::PropertySet const &metadata, std::string const &key,
965 char const *comment = nullptr) {
966 std::string upperKey(key);
967 boost::to_upper(upperKey);
968 if (upperKey.compare(key) != 0){
969 LOGLS_WARN("lsst.afw.fits",
970 boost::format("In %s, key '%s' may be standardized to uppercase '%s' on write.") %
971 BOOST_CURRENT_FUNCTION % key % upperKey);
972 }
973 std::type_info const &valueType = metadata.typeOf(key);
974
975 // Ensure long keywords have "HIERARCH " prepended; otherwise, cfitsio doesn't treat them right.
976 std::string keyName = key;
977 if (keyName.size() > 8 && keyName.rfind("HIERARCH ", 0) != 0) {
978 keyName = "HIERARCH " + keyName;
979 }
980
981 if (valueType == typeid(bool)) {
982 if (metadata.isArray(key)) {
983 std::vector<bool> tmp = metadata.getArray<bool>(key);
984 // work around unfortunate specialness of std::vector<bool>
985 for (std::size_t i = 0; i != tmp.size(); ++i) {
986 writeKeyImpl(fits, keyName.c_str(), static_cast<bool>(tmp[i]), comment);
987 }
988 } else {
989 writeKeyImpl(fits, keyName.c_str(), metadata.get<bool>(key), comment);
990 }
991 } else if (valueType == typeid(std::uint8_t)) {
992 if (metadata.isArray(key)) {
993 std::vector<std::uint8_t> tmp = metadata.getArray<std::uint8_t>(key);
994 for (std::size_t i = 0; i != tmp.size(); ++i) {
995 writeKeyImpl(fits, keyName.c_str(), tmp[i], comment);
996 }
997 } else {
998 writeKeyImpl(fits, keyName.c_str(), metadata.get<std::uint8_t>(key), comment);
999 }
1000 } else if (valueType == typeid(int)) {
1001 if (metadata.isArray(key)) {
1002 std::vector<int> tmp = metadata.getArray<int>(key);
1003 for (std::size_t i = 0; i != tmp.size(); ++i) {
1004 writeKeyImpl(fits, keyName.c_str(), tmp[i], comment);
1005 }
1006 } else {
1007 writeKeyImpl(fits, keyName.c_str(), metadata.get<int>(key), comment);
1008 }
1009 } else if (valueType == typeid(long)) {
1010 if (metadata.isArray(key)) {
1011 std::vector<long> tmp = metadata.getArray<long>(key);
1012 for (std::size_t i = 0; i != tmp.size(); ++i) {
1013 writeKeyImpl(fits, keyName.c_str(), tmp[i], comment);
1014 }
1015 } else {
1016 writeKeyImpl(fits, keyName.c_str(), metadata.get<long>(key), comment);
1017 }
1018 } else if (valueType == typeid(long long)) {
1019 if (metadata.isArray(key)) {
1020 std::vector<long long> tmp = metadata.getArray<long long>(key);
1021 for (std::size_t i = 0; i != tmp.size(); ++i) {
1022 writeKeyImpl(fits, keyName.c_str(), tmp[i], comment);
1023 }
1024 } else {
1025 writeKeyImpl(fits, keyName.c_str(), metadata.get<long long>(key), comment);
1026 }
1027 } else if (valueType == typeid(std::int64_t)) {
1028 if (metadata.isArray(key)) {
1029 std::vector<std::int64_t> tmp = metadata.getArray<std::int64_t>(key);
1030 for (std::size_t i = 0; i != tmp.size(); ++i) {
1031 writeKeyImpl(fits, keyName.c_str(), tmp[i], comment);
1032 }
1033 } else {
1034 writeKeyImpl(fits, keyName.c_str(), metadata.get<std::int64_t>(key), comment);
1035 }
1036 } else if (valueType == typeid(double)) {
1037 if (metadata.isArray(key)) {
1038 std::vector<double> tmp = metadata.getArray<double>(key);
1039 for (std::size_t i = 0; i != tmp.size(); ++i) {
1040 writeKeyImpl(fits, keyName.c_str(), tmp[i], comment);
1041 }
1042 } else {
1043 writeKeyImpl(fits, keyName.c_str(), metadata.get<double>(key), comment);
1044 }
1045 } else if (valueType == typeid(std::string)) {
1046 if (metadata.isArray(key)) {
1047 std::vector<std::string> tmp = metadata.getArray<std::string>(key);
1048 for (std::size_t i = 0; i != tmp.size(); ++i) {
1049 writeKeyImpl(fits, keyName.c_str(), tmp[i], comment);
1050 }
1051 } else {
1052 writeKeyImpl(fits, keyName.c_str(), metadata.get<std::string>(key), comment);
1053 }
1054 } else if (valueType == typeid(std::nullptr_t)) {
1055 if (metadata.isArray(key)) {
1056 // Write multiple undefined values for the same key
1057 auto tmp = metadata.getArray<std::nullptr_t>(key);
1058 for (std::size_t i = 0; i != tmp.size(); ++i) {
1059 writeKeyImpl(fits, keyName.c_str(), comment);
1060 }
1061 } else {
1062 writeKeyImpl(fits, keyName.c_str(), comment);
1063 }
1064 } else {
1065 // FIXME: inherited this error handling from fitsIo.cc; need a better option.
1066 LOGLS_WARN("lsst.afw.fits.writeKeyFromProperty",
1067 makeErrorMessage(fits.fptr, fits.status,
1068 boost::format("In %s, unknown type '%s' for key '%s'.") %
1069 BOOST_CURRENT_FUNCTION % valueType.name() % key));
1070 }
1071 if (fits.behavior & Fits::AUTO_CHECK) {
1072 LSST_FITS_CHECK_STATUS(fits, boost::format("Writing key '%s'") % key);
1073 }
1074}
1075
1076} // namespace
1077
1078void Fits::readMetadata(daf::base::PropertySet &metadata, bool strip) {
1079 MetadataIterationFunctor f;
1080 f.strip = strip;
1081 f.set = &metadata;
1082 f.list = dynamic_cast<daf::base::PropertyList *>(&metadata);
1083 forEachKey(f);
1084}
1085
1087 using NameList = std::vector<std::string>;
1088 daf::base::PropertyList const *pl = dynamic_cast<daf::base::PropertyList const *>(&metadata);
1089 NameList paramNames;
1090 if (pl) {
1091 paramNames = pl->getOrderedNames();
1092 } else {
1093 paramNames = metadata.paramNames(false);
1094 }
1095 for (auto const &paramName : paramNames) {
1096 if (!isKeyIgnored(paramName, true)) {
1097 if (pl) {
1098 writeKeyFromProperty(*this, metadata, paramName, pl->getComment(paramName).c_str());
1099 } else {
1100 writeKeyFromProperty(*this, metadata, paramName);
1101 }
1102 }
1103 }
1104}
1105
1106// ---- Manipulating tables ---------------------------------------------------------------------------------
1107
1109 char *ttype = nullptr;
1110 char *tform = nullptr;
1111 fits_create_tbl(reinterpret_cast<fitsfile *>(fptr), BINARY_TBL, 0, 0, &ttype, &tform, nullptr, nullptr, &status);
1112 if (behavior & AUTO_CHECK) {
1113 LSST_FITS_CHECK_STATUS(*this, "Creating binary table");
1114 }
1115}
1116
1117template <typename T>
1118int Fits::addColumn(std::string const &ttype, int size) {
1119 int nCols = 0;
1120 fits_get_num_cols(reinterpret_cast<fitsfile *>(fptr), &nCols, &status);
1121 std::string tform = makeColumnFormat<T>(size);
1122 fits_insert_col(reinterpret_cast<fitsfile *>(fptr), nCols + 1, const_cast<char *>(ttype.c_str()),
1123 const_cast<char *>(tform.c_str()), &status);
1124 if (behavior & AUTO_CHECK) {
1125 LSST_FITS_CHECK_STATUS(*this, boost::format("Adding column '%s' with size %d") % ttype % size);
1126 }
1127 return nCols;
1128}
1129
1130template <typename T>
1131int Fits::addColumn(std::string const &ttype, int size, std::string const &comment) {
1132 int nCols = addColumn<T>(ttype, size);
1133 updateColumnKey("TTYPE", nCols, ttype, comment);
1134 if (behavior & AUTO_CHECK) {
1135 LSST_FITS_CHECK_STATUS(*this, boost::format("Adding column '%s' with size %d") % ttype % size);
1136 }
1137 return nCols;
1138}
1139
1141 long first = 0;
1142 fits_get_num_rows(reinterpret_cast<fitsfile *>(fptr), &first, &status);
1143 fits_insert_rows(reinterpret_cast<fitsfile *>(fptr), first, nRows, &status);
1144 if (behavior & AUTO_CHECK) {
1145 LSST_FITS_CHECK_STATUS(*this, boost::format("Adding %d rows to binary table") % nRows);
1146 }
1147 return first;
1148}
1149
1151 long r = 0;
1152 fits_get_num_rows(reinterpret_cast<fitsfile *>(fptr), &r, &status);
1153 if (behavior & AUTO_CHECK) {
1154 LSST_FITS_CHECK_STATUS(*this, "Checking how many rows are in table");
1155 }
1156 return r;
1157}
1158
1159template <typename T>
1160void Fits::writeTableArray(std::size_t row, int col, int nElements, T const *value) {
1161 fits_write_col(reinterpret_cast<fitsfile *>(fptr), FitsTableType<T>::CONSTANT, col + 1, row + 1, 1,
1162 nElements, const_cast<T *>(value), &status);
1163 if (behavior & AUTO_CHECK) {
1164 LSST_FITS_CHECK_STATUS(*this, boost::format("Writing %d-element array at table cell (%d, %d)") %
1165 nElements % row % col);
1166 }
1167}
1168
1169void Fits::writeTableScalar(std::size_t row, int col, std::string const &value) {
1170 // cfitsio doesn't let us specify the size of a string, it just looks for null terminator.
1171 // Using std::string::c_str() guarantees that we have one. But we can't store arbitrary
1172 // data in a string field because cfitsio will also chop off anything after the first null
1173 // terminator.
1174 char const *tmp = value.c_str();
1175 fits_write_col(reinterpret_cast<fitsfile *>(fptr), TSTRING, col + 1, row + 1, 1, 1,
1176 const_cast<char const **>(&tmp), &status);
1177 if (behavior & AUTO_CHECK) {
1178 LSST_FITS_CHECK_STATUS(*this, boost::format("Writing value at table cell (%d, %d)") % row % col);
1179 }
1180}
1181
1182template <typename T>
1183void Fits::readTableArray(std::size_t row, int col, int nElements, T *value) {
1184 int anynul = false;
1185 fits_read_col(reinterpret_cast<fitsfile *>(fptr), FitsTableType<T>::CONSTANT, col + 1, row + 1, 1,
1186 nElements, nullptr, value, &anynul, &status);
1187 if (behavior & AUTO_CHECK) {
1188 LSST_FITS_CHECK_STATUS(*this, boost::format("Reading value at table cell (%d, %d)") % row % col);
1189 }
1190}
1191
1192void Fits::readTableScalar(std::size_t row, int col, std::string &value, bool isVariableLength) {
1193 int anynul = false;
1194 long size = isVariableLength ? getTableArraySize(row, col) : getTableArraySize(col);
1195 // We can't directly write into a std::string until C++17.
1196 std::vector<char> buf(size + 1, 0);
1197 // cfitsio wants a char** because they imagine we might want an array of strings,
1198 // but we only want one element.
1199 char *tmp = &buf.front();
1200 fits_read_col(reinterpret_cast<fitsfile *>(fptr), TSTRING, col + 1, row + 1, 1, 1, nullptr, &tmp, &anynul,
1201 &status);
1202 if (behavior & AUTO_CHECK) {
1203 LSST_FITS_CHECK_STATUS(*this, boost::format("Reading value at table cell (%d, %d)") % row % col);
1204 }
1205 value = std::string(tmp);
1206}
1207
1209 int typecode = 0;
1210 long result = 0;
1211 long width = 0;
1212 fits_get_coltype(reinterpret_cast<fitsfile *>(fptr), col + 1, &typecode, &result, &width, &status);
1213 if (behavior & AUTO_CHECK) {
1214 LSST_FITS_CHECK_STATUS(*this, boost::format("Looking up array size for column %d") % col);
1215 }
1216 return result;
1217}
1218
1220 long result = 0;
1221 long offset = 0;
1222 fits_read_descript(reinterpret_cast<fitsfile *>(fptr), col + 1, row + 1, &result, &offset, &status);
1223 if (behavior & AUTO_CHECK) {
1224 LSST_FITS_CHECK_STATUS(*this, boost::format("Looking up array size for cell (%d, %d)") % row % col);
1225 }
1226 return result;
1227}
1228
1229// ---- Manipulating images ---------------------------------------------------------------------------------
1230
1231namespace {
1232
1233int get_actual_cfitsio_bitpix(Fits & fits) {
1234 int result = 0;
1235 fits_get_img_equivtype(reinterpret_cast<fitsfile *>(fits.fptr), &result, &fits.status);
1236 if (fits.behavior & Fits::AUTO_CHECK) LSST_FITS_CHECK_STATUS(fits, "Getting image type");
1237 if (result == cfitsio_bitpix<std::int64_t>) {
1238 // Baffingly, fits_get_img_equivtype handles all of the special
1239 // special-BZERO unsigned variants *except* uint64, even though CFITSIO
1240 // does handle uint64 on write. So we have to special-case it here.
1241 std::uint64_t bzero = 0;
1242 int tmp_status = 0;
1243 fits_read_key(reinterpret_cast<fitsfile *>(fits.fptr), FitsType<std::uint64_t>::CONSTANT, "BZERO",
1244 &bzero, nullptr, &tmp_status);
1245 if (tmp_status == 0 && bzero == 9223372036854775808u) {
1246 result = cfitsio_bitpix<std::uint64_t>;
1247 }
1248 }
1249 return result;
1250}
1251
1253template <typename T>
1254std::pair<T, T> calculate_min_max(ndarray::Array<T const, 1, 1> const& image,
1255 ndarray::Array<bool, 1, 1> const& mask) {
1257 auto mm = mask.begin();
1258 for (auto ii = image.begin(); ii != image.end(); ++ii, ++mm) {
1259 if (*mm) continue;
1260 if (!std::isfinite(*ii)) continue;
1261 if (*ii > max) max = *ii;
1262 if (*ii < min) min = *ii;
1263 }
1264 return std::make_pair(min, max);
1265}
1266
1267// Return range of values for quantized values. We assume bitpix=32 because
1268// that's all CFITSIO supports.
1269constexpr std::uint64_t quantized_range() {
1270 // Number of reserved values for float --> bitpix=32 conversions (copied out of cfitsio)
1271 int constexpr N_RESERVED_VALUES = 10;
1272 std::uint64_t range = (static_cast<std::uint64_t>(1) << 32) - 1;
1273 range -= N_RESERVED_VALUES; // CFITSIO wants to reserve special values, for e.g. NULL/NaN.
1274 range -= 2; // To allow for rounding and fuzz at either end
1275 return range;
1276}
1277
1278template <typename T>
1279float measure_range_scaling(ndarray::Array<T const, 1, 1> const& image,
1280 ndarray::Array<bool, 1, 1> const& mask) {
1281 auto minMax = calculate_min_max(image, mask);
1282 T const min = minMax.first;
1283 T const max = minMax.second;
1284 if (min == max) return -1.0;
1285 auto range = quantized_range();
1286 return (max - min) / range;
1287}
1288
1289template <typename T>
1290float measure_stdev_scaling(ndarray::Array<T const, 1, 1> const& image,
1291 ndarray::Array<bool, 1, 1> const& mask, float level) {
1292 std::vector<T> array;
1293 array.reserve(image.size());
1294 auto mm = mask.begin();
1295 for (auto ii = image.begin(); ii != image.end(); ++ii, ++mm) {
1296 if (!*mm) {
1297 array.push_back(*ii);
1298 }
1299 }
1300 // Quartiles; from https://stackoverflow.com/a/11965377/834250
1301 auto const q1 = array.size() / 4;
1302 auto const q2 = array.size() / 2;
1303 auto const q3 = q1 + q2;
1304 std::nth_element(array.begin(), array.begin() + q1, array.end());
1305 std::nth_element(array.begin() + q1 + 1, array.begin() + q2, array.end());
1306 std::nth_element(array.begin() + q2 + 1, array.begin() + q3, array.end());
1307 // No, we're not doing any interpolation for the lower and upper quartiles.
1308 // We're estimating the noise, so it doesn't need to be super precise.
1309 double const lq = array[q1];
1310 double const uq = array[q3];
1311 double const stdev = 0.741 * (uq - lq);
1312 return stdev / level;
1313}
1314
1315template <typename T>
1316float measure_qlevel(
1317 QuantizationOptions const & options,
1318 ndarray::Array<T const, 1, 1> const& image,
1319 ndarray::Array<bool, 1, 1> const& mask
1320) {
1321 switch (options.scaling) {
1322 // We return a negative number when we want to tell CFITSIO that the
1323 // quantization level is actually ZSCALE itself, not a target
1324 // quantization level.
1326 return -measure_range_scaling(image, mask);
1328 return -measure_stdev_scaling(image, mask, options.level);
1330 return options.level;
1332 return -options.level;
1333 }
1334 throw LSST_EXCEPT(pex::exceptions::LogicError, "Invalid scaling algorithm.");
1335}
1336
1337int compression_algorithm_to_cfitsio(CompressionAlgorithm algorithm) {
1338 switch (algorithm) {
1340 return GZIP_1;
1342 return GZIP_2;
1344 return RICE_1;
1345 }
1346 throw LSST_EXCEPT(pex::exceptions::LogicError, "Invalid compression algorithm.");
1347}
1348
1349int dither_algorithm_to_cfitsio(DitherAlgorithm algorithm) {
1350 switch (algorithm) {
1352 return NO_DITHER;
1354 return SUBTRACTIVE_DITHER_1;
1356 return SUBTRACTIVE_DITHER_2;
1357 }
1358 throw LSST_EXCEPT(pex::exceptions::LogicError, "Invalid dither algorithm.");
1359}
1360
1361class CompressionContext {
1362public:
1363
1364 template <typename T>
1365 CompressionContext(
1366 Fits & fits,
1367 CompressionOptions const * options,
1368 afw::image::ImageBase<T> const& image,
1369 afw::image::Mask<> const * mask
1370 ) : CompressionContext(fits) {
1371 // If the image is already fully contiguous, get a flat 1-d view into
1372 // it. If it isn't, copy to amke a flat 1-d array. We need to
1373 // remember the non-const copy if we make one because we might want it
1374 // later.
1375 ndarray::Array<T, 1, 1> image_flat_mutable;
1376 ndarray::Array<T const, 1, 1> image_flat;
1377 ndarray::Array<T const, 2, 2> image_array = ndarray::dynamic_dimension_cast<2>(image.getArray());
1378 if (image_array.isEmpty()) {
1379 ndarray::Array<T, 2, 2> image_array_copy = ndarray::copy(image.getArray());
1380 image_flat_mutable = ndarray::flatten<1>(image_array_copy);
1381 image_flat = image_flat_mutable;
1382 } else {
1383 image_flat = ndarray::flatten<1>(image_array);
1384 }
1385 _pixel_data = image_flat.begin();
1386 _pixel_data_mgr = image_flat.getManager();
1387 _n_pixels = image_flat.size();
1388 if (options) {
1389 float qlevel = 0.0;
1390 if constexpr(std::is_floating_point_v<T>) {
1391 if (options->quantization) {
1392 if (mask && image.getDimensions() != mask->getDimensions()) {
1393 std::ostringstream os;
1394 os << "Size mismatch between image and mask: ";
1395 os << image.getWidth() << "x" << image.getHeight();
1396 os << " vs ";
1397 os << mask->getWidth() << "x" << mask->getHeight();
1398 throw LSST_EXCEPT(pex::exceptions::InvalidParameterError, os.str());
1399 }
1400 ndarray::Array<bool, 2, 2> mask_array = ndarray::allocate(image_array.getShape());
1401 ndarray::Array<bool, 1, 1> mask_flat = ndarray::flatten<1>(mask_array);
1402 if (mask && options->uses_mask()) {
1403 mask_array.deep() = (
1404 mask->getArray() & mask->getPlaneBitMask(options->quantization.value().mask_planes)
1405 );
1406 }
1408 image_flat.begin(), image_flat.end(), mask_flat.begin(), mask_flat.begin(),
1409 [](T const & img, bool const & msk) { return msk || !std::isfinite(img); }
1410 );
1411 qlevel = measure_qlevel(options->quantization.value(), image_flat, mask_flat);
1412 // Quantized floats is also the only context in which NaN and inf don't round-trip
1413 // automatically.
1414 if (image_flat_mutable.isEmpty()) {
1415 // We didn't make a copy that we can modify before; have to do it now.
1416 image_flat_mutable = ndarray::copy(image_flat);
1417 }
1419 image_flat_mutable.begin(), image_flat_mutable.end(), image_flat_mutable.begin(),
1420 [](T const & value) {
1421 return (std::isnan(value)) ? -std::numeric_limits<T>::infinity() : value;
1422 }
1423 );
1424 _pixel_data = image_flat_mutable.begin();
1425 _pixel_data_mgr = image_flat_mutable.getManager();
1426 }
1427 }
1428 _apply(*options, qlevel, std::is_floating_point_v<T>, image.getDimensions());
1429 }
1430 }
1431
1432 template <typename T>
1433 T const * get_pixel_data() const { return static_cast<T const*>(_pixel_data); }
1434
1435 std::size_t get_n_pixels() const { return _n_pixels; }
1436
1437 // Disable copies (moves are fine).
1438 CompressionContext(CompressionContext const&) = delete;
1439 CompressionContext& operator=(CompressionContext const&) = delete;
1440
1441 ~CompressionContext(){
1442 auto fptr = reinterpret_cast<fitsfile *>(_fits->fptr);
1443 fits_set_compression_type(fptr, _algorithm, &_fits->status);
1444 fits_set_tile_dim(fptr, _tile_dim.size(), _tile_dim.data(), &_fits->status);
1445 fits_set_quantize_level(fptr, _qlevel, &_fits->status);
1446 fits_set_dither_seed(fptr, _dither_seed, &_fits->status);
1447 }
1448
1449private:
1450
1451 explicit CompressionContext(Fits & fits)
1452 : _fits(&fits), _algorithm(0), _dither_seed(0), _qlevel(0.0), _n_pixels(0),
1453 _pixel_data(nullptr), _pixel_data_mgr()
1454 {
1455 auto fptr = reinterpret_cast<fitsfile *>(fits.fptr);
1456 fits_get_compression_type(fptr, &_algorithm, &fits.status);
1457 fits_get_tile_dim(fptr, _tile_dim.size(), _tile_dim.data(), &fits.status);
1458 fits_get_quantize_level(fptr, &_qlevel, &fits.status);
1459 // There is weirdly no way to get the dither method from CFITSIO. That
1460 // makes this code less defensive than it would ideally be, but since the
1461 // dither method is ignored when compression is off and overridden whenever
1462 // compression is enabled, it shouldn't actually be a problem.
1463 fits_get_dither_seed(fptr, &_dither_seed, &fits.status);
1464 }
1465
1466 void _apply(
1467 CompressionOptions options,
1468 float qlevel,
1469 bool is_float,
1470 lsst::geom::Extent2I const & dimensions
1471 ) {
1472 if (is_float && !options.quantization && options.algorithm == CompressionAlgorithm::RICE_1_) {
1473 throw LSST_EXCEPT(
1474 pex::exceptions::InvalidParameterError,
1475 "RICE_1 compression of floating point images requires quantization."
1476 );
1477 }
1478 if (is_float && qlevel == 0.0 && options.algorithm == CompressionAlgorithm::RICE_1_) {
1479 // User asked to quantize but the scaling algorithm came back with
1480 // a scale factor of zero, which pretty much only happens when the
1481 // image is a constant. CFITSIO will choke on this, so we switch to
1482 // lossless compression (which should be great because the image is
1483 // a constant). We don't warn because this is not always something
1484 // the user can control.
1485 options = CompressionOptions();
1486 }
1487 if (!is_float && options.quantization) {
1488 throw LSST_EXCEPT(
1489 pex::exceptions::InvalidParameterError,
1490 "Quantization cannot be used on integer images."
1491 );
1492 }
1493 auto fptr = reinterpret_cast<fitsfile *>(_fits->fptr);
1494 if (!_n_pixels) {
1495 fits_set_compression_type(fptr, 0, &_fits->status);
1496 return;
1497 }
1498 fits_set_compression_type(fptr, compression_algorithm_to_cfitsio(options.algorithm), &_fits->status);
1499 // CFITSIO wants the shape in (X, Y) order, which is a reverse of what we
1500 // have for the shape and the requested tile.
1501 std::array<long, 2> tile_dim = {
1502 (options.tile_width == 0) ? dimensions.getX() : static_cast<long>(options.tile_width),
1503 (options.tile_height == 0) ? dimensions.getY() : static_cast<long>(options.tile_height)
1504 };
1505 fits_set_tile_dim(fptr, tile_dim.size(), tile_dim.data(), &_fits->status);
1506 if (options.quantization) {
1507 auto q = options.quantization.value();
1508 fits_set_quantize_method(fptr, dither_algorithm_to_cfitsio(q.dither), &_fits->status);
1509 fits_set_dither_seed(fptr, q.seed, &_fits->status);
1510 fits_set_quantize_level(fptr, qlevel, &_fits->status);
1511 } else {
1512 fits_set_quantize_level(fptr, 0.0, &_fits->status);
1513 }
1514 }
1515
1516 // FITS object to restore to its previous state upon destruction.
1517 Fits * _fits;
1518 // These data members are the *old* CFITSIO settings we'll restore in the
1519 // destructor.
1520 int _algorithm;
1521 int _dither_seed;
1522 float _qlevel;
1523 std::array<long, 2> _tile_dim;
1524 // A pointer to a contiguous image pixel array, either extracted from the
1525 // image (if it was already contiguous) or copied, its size, and a
1526 // reference to its lifetime.
1527 std::size_t _n_pixels;
1528 void const * _pixel_data;
1529 ndarray::Manager::Ptr _pixel_data_mgr;
1530};
1531
1532} // anonymous
1533
1535 long naxes = 0;
1536 fits_create_img(reinterpret_cast<fitsfile *>(fptr), 8, 0, &naxes, &status);
1537 if (behavior & AUTO_CHECK) {
1538 LSST_FITS_CHECK_STATUS(*this, "Creating empty image HDU");
1539 }
1540}
1541
1542void Fits::createImageImpl(int bitpix, int naxis, long const *naxes) {
1543 fits_create_img(reinterpret_cast<fitsfile *>(fptr), bitpix, naxis, const_cast<long *>(naxes), &status);
1544 if (behavior & AUTO_CHECK) {
1545 LSST_FITS_CHECK_STATUS(*this, "Creating new image HDU");
1546 }
1547}
1548
1549template <typename T>
1550void Fits::writeImageImpl(T const *data, int nElements, std::optional<T> explicit_null) {
1551 if (explicit_null) {
1552 fits_write_imgnull(reinterpret_cast<fitsfile *>(fptr), FitsType<T>::CONSTANT, 1, nElements,
1553 const_cast<T *>(data), const_cast<T*>(&explicit_null.value()), &status);
1554 } else {
1555 fits_write_img(reinterpret_cast<fitsfile *>(fptr), FitsType<T>::CONSTANT, 1, nElements,
1556 const_cast<T *>(data), &status);
1557 }
1558 if (behavior & AUTO_CHECK) {
1559 LSST_FITS_CHECK_STATUS(*this, "Writing image");
1560 }
1561}
1562
1563template <typename T>
1565 daf::base::PropertySet const * header,
1566 image::Mask<image::MaskPixel> const * mask) {
1567 // Context will restore the original settings when it is destroyed.
1568 CompressionContext context(*this, compression, image, mask);
1569 if (behavior & AUTO_CHECK) {
1570 LSST_FITS_CHECK_STATUS(*this, "Activating compression for write image");
1571 }
1572 // We need a place to put the image+header, and CFITSIO needs to know the
1573 // dimensions.
1574 ndarray::Vector<long, 2> dims(image.getArray().getShape().reverse());
1575 createImageImpl(cfitsio_bitpix<T>, 2, dims.elems);
1576 // Write the header
1578 geom::createTrivialWcsMetadata(image::detail::wcsNameForXY0, image.getXY0());
1580 if (header) {
1581 fullMetadata = header->deepCopy();
1582 fullMetadata->combine(*wcsMetadata);
1583 } else {
1584 fullMetadata = wcsMetadata;
1585 }
1586 writeMetadata(*fullMetadata);
1587 std::optional<T> explicit_null = std::nullopt;
1588 if constexpr(std::is_floating_point_v<T>) {
1589 if (compression && compression->quantization) {
1590 explicit_null = -std::numeric_limits<T>::infinity();
1591 }
1592 }
1593 // Write the image itself.
1594 writeImageImpl(context.get_pixel_data<T>(), context.get_n_pixels(), explicit_null);
1595}
1596
1597
1598namespace {
1599
1601template <typename T, class Enable = void>
1602struct NullValue {
1603 static T constexpr value = 0;
1604};
1605
1607template <typename T>
1608struct NullValue<T, typename std::enable_if<std::numeric_limits<T>::has_quiet_NaN>::type> {
1609 static T constexpr value = std::numeric_limits<T>::quiet_NaN();
1610};
1611
1612} // namespace
1613
1614template <typename T>
1615void Fits::readImageImpl(int nAxis, T *data, long *begin, long *end, long *increment) {
1616 fitsfile * fits = reinterpret_cast<fitsfile *>(fptr);
1617 int is_compressed = fits_is_compressed_image(fits, &status);
1618 if (behavior & AUTO_CHECK) LSST_FITS_CHECK_STATUS(*this, "Checking image compressed state");
1619 if (
1620 is_compressed && fits->Fptr->quantize_level == 9999
1621 && (fits->Fptr->cn_zscale != 0 || fits->Fptr->cn_zzero != 0)
1622 ) {
1623 // CFITSIO bug can make it leave quantize_level=9999 (NO_QUANTIZE) from
1624 // a previously-inspected different HDU with lossless compression,
1625 // preventing application of ZSCALE and ZZERO, even if it has seen a
1626 // ZSCALE or ZZERO header or column on this HDU. This has been
1627 // reported upstream, so if this workaround (which involves poking at
1628 // the innards of private structs) ever stops working, hopefully the
1629 // bug will have been fixed upstream anyway.
1630 fits->Fptr->quantize_level = 0;
1631 }
1632 T null = NullValue<T>::value;
1633 int anyNulls = 0;
1634 fits_read_subset(fits, FitsType<T>::CONSTANT, begin, end, increment,
1635 reinterpret_cast<void *>(&null), data, &anyNulls, &status);
1636 if (behavior & AUTO_CHECK) LSST_FITS_CHECK_STATUS(*this, "Reading image");
1637}
1638
1640 int nAxis = 0;
1641 fits_get_img_dim(reinterpret_cast<fitsfile *>(fptr), &nAxis, &status);
1642 if (behavior & AUTO_CHECK) LSST_FITS_CHECK_STATUS(*this, "Getting NAXIS");
1643 return nAxis;
1644}
1645
1646void Fits::getImageShapeImpl(int maxDim, long *nAxes) {
1647 fits_get_img_size(reinterpret_cast<fitsfile *>(fptr), maxDim, nAxes, &status);
1648 if (behavior & AUTO_CHECK) LSST_FITS_CHECK_STATUS(*this, "Getting NAXES");
1649}
1650
1651template <typename T>
1653 int imageType = get_actual_cfitsio_bitpix(*this);
1655 if (imageType < 0) {
1656 return false; // can't represent floating-point with integer
1657 }
1658 bool is_compressed = fits_is_compressed_image(reinterpret_cast<fitsfile*>(fptr), &status);
1659 if (behavior & AUTO_CHECK) LSST_FITS_CHECK_STATUS(*this, "Checking pixel type compatibility");
1660 if (is_compressed && sizeof(T) == 8) {
1661 // CFITSIO can't decompress into [u]int64, at least on some
1662 // platforms, so we don't support it.
1663 return false;
1664 }
1666 if (isFitsImageTypeSigned(imageType)) {
1667 return cfitsio_bitpix<T> >= imageType;
1668 } else {
1669 // need extra bits to safely convert unsigned to signed
1670 return cfitsio_bitpix<T> > imageType;
1671 }
1672 } else {
1673 if (!isFitsImageTypeSigned(imageType)) {
1674 return cfitsio_bitpix<T> >= imageType;
1675 } else {
1676 return false;
1677 }
1678 }
1679 }
1680 // we allow all conversions to float and double, even if they lose precision
1681 return true;
1682}
1683
1685 int bitpix = get_actual_cfitsio_bitpix(*this);
1686 if (bitpix < 0) {
1687 return "float" + std::to_string(-bitpix);
1688 }
1689 switch (bitpix) {
1690 case BYTE_IMG: return "uint8";
1691 case SBYTE_IMG: return "int8";
1692 case SHORT_IMG: return "int16";
1693 case USHORT_IMG: return "uint16";
1694 case LONG_IMG: return "int32";
1695 case ULONG_IMG: return "uint32";
1696 case LONGLONG_IMG: return "int64";
1697 case ULONGLONG_IMG: return "uint64";
1698 }
1699 throw LSST_EXCEPT(
1700 FitsError,
1701 (boost::format("Unrecognized BITPIX value: %d") % bitpix).str()
1702 );
1703}
1704
1705// ---- Manipulating files ----------------------------------------------------------------------------------
1706
1707Fits::Fits(std::string const &filename, std::string const &mode, int behavior_)
1708 : fptr(nullptr), status(0), behavior(behavior_) {
1709 if (mode == "r" || mode == "rb") {
1710 fits_open_file(reinterpret_cast<fitsfile **>(&fptr), const_cast<char *>(filename.c_str()), READONLY,
1711 &status);
1712 } else if (mode == "w" || mode == "wb") {
1713 std::filesystem::remove(filename); // cfitsio doesn't like over-writing files
1714 fits_create_file(reinterpret_cast<fitsfile **>(&fptr), const_cast<char *>(filename.c_str()), &status);
1715 } else if (mode == "a" || mode == "ab") {
1716 fits_open_file(reinterpret_cast<fitsfile **>(&fptr), const_cast<char *>(filename.c_str()), READWRITE,
1717 &status);
1718 int nHdu = 0;
1719 fits_get_num_hdus(reinterpret_cast<fitsfile *>(fptr), &nHdu, &status);
1720 fits_movabs_hdu(reinterpret_cast<fitsfile *>(fptr), nHdu, nullptr, &status);
1721 if ((behavior & AUTO_CHECK) && (behavior & AUTO_CLOSE) && (status) && (fptr)) {
1722 // We're about to throw an exception, and the destructor won't get called
1723 // because we're in the constructor, so cleanup here first.
1724 int tmpStatus = 0;
1725 fits_close_file(reinterpret_cast<fitsfile *>(fptr), &tmpStatus);
1726 }
1727 } else {
1728 throw LSST_EXCEPT(
1729 FitsError,
1730 (boost::format("Invalid mode '%s' given when opening file '%s'") % mode % filename).str());
1731 }
1732 if (behavior & AUTO_CHECK) {
1733 LSST_FITS_CHECK_STATUS(*this, boost::format("Opening file '%s' with mode '%s'") % filename % mode);
1734 }
1735}
1736
1737Fits::Fits(MemFileManager &manager, std::string const &mode, int behavior_)
1738 : fptr(nullptr), status(0), behavior(behavior_) {
1739 using Reallocator = void *(*)(void *, std::size_t);
1740 // It's a shame this logic is essentially a duplicate of above, but the innards are different enough
1741 // we can't really reuse it.
1742 if (mode == "r" || mode == "rb") {
1743 fits_open_memfile(reinterpret_cast<fitsfile **>(&fptr), "unused", READONLY, &manager._ptr,
1744 &manager._len, 0, nullptr, // no reallocator or deltasize necessary for READONLY
1745 &status);
1746 } else if (mode == "w" || mode == "wb") {
1747 Reallocator reallocator = nullptr;
1748 if (manager._managed) reallocator = &std::realloc;
1749 fits_create_memfile(reinterpret_cast<fitsfile **>(&fptr), &manager._ptr, &manager._len, 0,
1750 reallocator, // use default deltasize
1751 &status);
1752 } else if (mode == "a" || mode == "ab") {
1753 Reallocator reallocator = nullptr;
1754 if (manager._managed) reallocator = &std::realloc;
1755 fits_open_memfile(reinterpret_cast<fitsfile **>(&fptr), "unused", READWRITE, &manager._ptr,
1756 &manager._len, 0, reallocator, &status);
1757 int nHdu = 0;
1758 fits_get_num_hdus(reinterpret_cast<fitsfile *>(fptr), &nHdu, &status);
1759 fits_movabs_hdu(reinterpret_cast<fitsfile *>(fptr), nHdu, nullptr, &status);
1760 if ((behavior & AUTO_CHECK) && (behavior & AUTO_CLOSE) && (status) && (fptr)) {
1761 // We're about to throw an exception, and the destructor won't get called
1762 // because we're in the constructor, so cleanup here first.
1763 int tmpStatus = 0;
1764 fits_close_file(reinterpret_cast<fitsfile *>(fptr), &tmpStatus);
1765 }
1766 } else {
1767 throw LSST_EXCEPT(FitsError,
1768 (boost::format("Invalid mode '%s' given when opening memory file at '%s'") % mode %
1769 manager._ptr)
1770 .str());
1771 }
1772 if (behavior & AUTO_CHECK) {
1774 *this, boost::format("Opening memory file at '%s' with mode '%s'") % manager._ptr % mode);
1775 }
1776}
1777
1779 fits_close_file(reinterpret_cast<fitsfile *>(fptr), &status);
1780 fptr = nullptr;
1781}
1782
1784 daf::base::PropertyList const & first,
1785 daf::base::PropertyList const & second) {
1787 bool const asScalar = true;
1788 for (auto const &name : first.getOrderedNames()) {
1789 auto const iscv = isCommentIsValid(first, name);
1790 if (iscv.isComment) {
1791 if (iscv.isValid) {
1792 combined->add<std::string>(name, first.getArray<std::string>(name));
1793 }
1794 } else {
1795 combined->copy(name, first, name, asScalar);
1796 }
1797 }
1798 for (auto const &name : second.getOrderedNames()) {
1799 auto const iscv = isCommentIsValid(second, name);
1800 if (iscv.isComment) {
1801 if (iscv.isValid) {
1802 combined->add<std::string>(name, second.getArray<std::string>(name));
1803 }
1804 } else {
1805 // `copy` will replace an item, even if has a different type, so no need to call `remove`
1806 combined->copy(name, second, name, asScalar);
1807 }
1808 }
1809 return combined;
1810}
1811
1813
1814namespace detail {
1815template <typename T, typename... Args>
1816dafPlistPtr _readMetadata(T &&fitsparm, bool strip, Args... args) {
1818 fp.setHdu(args...);
1819 return readMetadata(fp, strip);
1820}
1821
1822} // namespace detail
1823
1824dafPlistPtr readMetadata(std::string const &fileName, int hdu, bool strip) {
1825 return detail::_readMetadata(fileName, strip, hdu);
1826}
1827
1828dafPlistPtr readMetadata(std::string const &fileName, std::string const &hduname, HduType type, int hduver,
1829 bool strip) {
1830 return detail::_readMetadata(fileName, strip, hduname, type, hduver);
1831}
1832
1833dafPlistPtr readMetadata(fits::MemFileManager &manager, int hdu, bool strip) {
1834 return detail::_readMetadata(manager, strip, hdu);
1835}
1836
1837dafPlistPtr readMetadata(MemFileManager &manager, std::string const &hduname, HduType type, int hduver,
1838 bool strip) {
1839 return detail::_readMetadata(manager, strip, hduname, type, hduver);
1840}
1841
1844 fitsfile.readMetadata(*metadata, strip);
1845 // if INHERIT=T, we want to also include header entries from the primary HDU
1846 int oldHdu = fitsfile.getHdu();
1847 if (oldHdu != 0 && metadata->exists("INHERIT")) {
1848 bool inherit = false;
1849 if (metadata->typeOf("INHERIT") == typeid(std::nullptr_t)) {
1850 // Assume false if INHERIT exists but is undefined.
1851 inherit = false;
1852 } else if (metadata->typeOf("INHERIT") == typeid(std::string)) {
1853 inherit = (metadata->get<std::string>("INHERIT") == "T");
1854 } else {
1855 inherit = metadata->get<bool>("INHERIT");
1856 }
1857 if (strip) metadata->remove("INHERIT");
1858 if (inherit) {
1859 HduMoveGuard guard(fitsfile, 0);
1860 // Combine the metadata from the primary HDU with the metadata from the specified HDU,
1861 // with non-comment values from the specified HDU superseding those in the primary HDU
1862 // and comments from the specified HDU appended to comments from the primary HDU
1863 auto primaryHduMetadata = std::make_shared<daf::base::PropertyList>();
1864 fitsfile.readMetadata(*primaryHduMetadata, strip);
1865 metadata = combineMetadata(*primaryHduMetadata, *metadata);
1866 } else {
1867 // Purge invalid values
1868 auto const emptyMetadata = std::make_shared<lsst::daf::base::PropertyList>();
1869 metadata = combineMetadata(*metadata, *emptyMetadata);
1870 }
1871 }
1872 return metadata;
1873}
1874
1875
1876HduMoveGuard::HduMoveGuard(Fits & fits, int hdu, bool relative) :
1877 _fits(fits),
1878 _oldHdu(_fits.getHdu()),
1879 _enabled(true)
1880{
1881 _fits.setHdu(hdu, relative);
1882}
1883
1885 if (!_enabled) {
1886 return;
1887 }
1888 int status = 0;
1889 std::swap(status, _fits.status); // unset error indicator, but remember the old status
1890 try {
1891 _fits.setHdu(_oldHdu);
1892 } catch (...) {
1893 LOGL_WARN(
1894 "afw.fits",
1895 makeErrorMessage(_fits.fptr, _fits.status, "Failed to move back to HDU %d").c_str(),
1896 _oldHdu
1897 );
1898 }
1899 std::swap(status, _fits.status); // reset the old status
1900}
1901
1903 auto fits = reinterpret_cast<fitsfile *>(fptr);
1904 if (getHdu() != 0 || countHdus() == 1) {
1905 return false; // Can't possibly be the PHU leading a compressed image
1906 }
1907 // Check NAXIS = 0
1908 int naxis;
1909 fits_get_img_dim(fits, &naxis, &status);
1910 if (behavior & AUTO_CHECK) {
1911 LSST_FITS_CHECK_STATUS(*this, "Checking NAXIS of PHU");
1912 }
1913 if (naxis != 0) {
1914 return false;
1915 }
1916 // Check first extension (and move back there when we're done if we're not compressed)
1917 HduMoveGuard move(*this, 1);
1918 bool isCompressed = fits_is_compressed_image(fits, &status);
1919 if (behavior & AUTO_CHECK) {
1920 LSST_FITS_CHECK_STATUS(*this, "Checking compression");
1921 }
1922 if (isCompressed) {
1923 move.disable();
1924 }
1925 return isCompressed;
1926}
1927
1928#define INSTANTIATE_KEY_OPS(r, data, T) \
1929 template void Fits::updateKey(std::string const &, T const &, std::string const &); \
1930 template void Fits::writeKey(std::string const &, T const &, std::string const &); \
1931 template void Fits::updateKey(std::string const &, T const &); \
1932 template void Fits::writeKey(std::string const &, T const &); \
1933 template void Fits::updateColumnKey(std::string const &, int, T const &, std::string const &); \
1934 template void Fits::writeColumnKey(std::string const &, int, T const &, std::string const &); \
1935 template void Fits::updateColumnKey(std::string const &, int, T const &); \
1936 template void Fits::writeColumnKey(std::string const &, int, T const &); \
1937 template void Fits::readKey(std::string const &, T &);
1938
1939#define INSTANTIATE_IMAGE_OPS(r, data, T) \
1940 template void Fits::writeImageImpl(T const *, int, std::optional<T>); \
1941 template void Fits::writeImage(image::ImageBase<T> const &, CompressionOptions const *, \
1942 daf::base::PropertySet const *, \
1943 image::Mask<image::MaskPixel> const *); \
1944 template void Fits::readImageImpl(int, T *, long *, long *, long *); \
1945 template bool Fits::checkImageType<T>(); \
1946 template int getBitPix<T>();
1947
1948#define INSTANTIATE_TABLE_OPS(r, data, T) \
1949 template int Fits::addColumn<T>(std::string const &ttype, int size); \
1950 template int Fits::addColumn<T>(std::string const &ttype, int size, std::string const &comment);
1951#define INSTANTIATE_TABLE_ARRAY_OPS(r, data, T) \
1952 template void Fits::writeTableArray(std::size_t row, int col, int nElements, T const *value); \
1953 template void Fits::readTableArray(std::size_t row, int col, int nElements, T *value);
1954
1955// ----------------------------------------------------------------------------------------------------------
1956// ---- Explicit instantiation ------------------------------------------------------------------------------
1957// ----------------------------------------------------------------------------------------------------------
1958
1959#define KEY_TYPES \
1960 (bool)(unsigned char)(short)(unsigned short)(int)(unsigned int)(long)(unsigned long)(LONGLONG)( \
1961 float)(double)(std::complex<float>)(std::complex<double>)(std::string)
1962
1963#define COLUMN_TYPES \
1964 (bool)(std::string)(std::int8_t)(std::uint8_t)(std::int16_t)(std::uint16_t)(std::int32_t)(std::uint32_t) \
1965 (std::int64_t)(float)(double)(lsst::geom::Angle)(std::complex<float>)(std::complex<double>)
1966
1967#define COLUMN_ARRAY_TYPES \
1968 (bool)(char)(std::uint8_t)(std::int16_t)(std::uint16_t)(std::int32_t)(std::uint32_t)(std::int64_t)( \
1969 float)(double)(lsst::geom::Angle)(std::complex<float>)(std::complex<double>)
1970
1971#define IMAGE_TYPES \
1972 (unsigned char)(short)(unsigned short)(int)(unsigned int)(std::int64_t)(std::uint64_t)(float)(double)
1973
1974BOOST_PP_SEQ_FOR_EACH(INSTANTIATE_KEY_OPS, _, KEY_TYPES)
1975BOOST_PP_SEQ_FOR_EACH(INSTANTIATE_TABLE_OPS, _, COLUMN_TYPES)
1976BOOST_PP_SEQ_FOR_EACH(INSTANTIATE_TABLE_ARRAY_OPS, _, COLUMN_ARRAY_TYPES)
1977BOOST_PP_SEQ_FOR_EACH(INSTANTIATE_IMAGE_OPS, _, IMAGE_TYPES)
1978} // namespace fits
1979} // namespace afw
1980} // namespace lsst
#define LSST_EXCEPT(type,...)
Create an exception with a given type.
Definition Exception.h:48
LSST DM logging module built on log4cxx.
#define LOGLS_WARN(logger, message)
Log a warn-level message using an iostream-based interface.
Definition Log.h:659
#define LOGL_WARN(logger, message...)
Log a warn-level message using a varargs/printf style interface.
Definition Log.h:547
#define LOGLS_DEBUG(logger, message)
Log a debug-level message using an iostream-based interface.
Definition Log.h:619
T begin(T... args)
T c_str(T... args)
An exception thrown when problems are found when reading or writing FITS files.
Definition fits.h:37
A simple struct that combines the two arguments that must be passed to most cfitsio routines and cont...
Definition fits.h:242
void writeKey(std::string const &key, T const &value, std::string const &comment)
Add a FITS header key to the bottom of the header.
Definition fits.cc:651
void writeColumnKey(std::string const &prefix, int n, T const &value, std::string const &comment)
Write a key of the form XXXXXnnn, where XXXXX is the prefix and nnn is a column number.
Definition fits.cc:683
void closeFile()
Close a FITS file.
Definition fits.cc:1778
void writeImage(ndarray::Array< T const, N, C > const &array)
Write an ndarray::Array to a FITS image HDU.
Definition fits.h:434
int getImageDim()
Return the number of dimensions in the current HDU.
Definition fits.cc:1639
std::size_t countRows()
Return the number of row in a table.
Definition fits.cc:1150
void createEmpty()
Create an empty image HDU with NAXIS=0 at the end of the file.
Definition fits.cc:1534
void readTableArray(std::size_t row, int col, int nElements, T *value)
Read an array value from a binary table.
Definition fits.cc:1183
void updateColumnKey(std::string const &prefix, int n, T const &value, std::string const &comment)
Update a key of the form XXXXXnnn, where XXXXX is the prefix and nnn is a column number.
Definition fits.cc:675
void setHdu(int hdu, bool relative=false)
Set the current HDU.
Definition fits.cc:489
void createTable()
Create a new binary table extension.
Definition fits.cc:1108
Fits()
Default constructor; set all data members to 0.
Definition fits.h:567
void readTableScalar(std::size_t row, int col, T &value)
Read an array scalar from a binary table.
Definition fits.h:553
bool checkCompressedImagePhu()
Go to the first image header in the FITS file.
Definition fits.cc:1902
int countHdus()
Return the number of HDUs in the file.
Definition fits.cc:518
void writeTableScalar(std::size_t row, int col, T value)
Write a scalar value to a binary table.
Definition fits.h:541
void forEachKey(HeaderIterationFunctor &functor)
Call a polymorphic functor for every key in the header.
Definition fits.cc:765
std::string getFileName() const
Return the file name associated with the FITS object or "<unknown>" if there is none.
Definition fits.cc:474
int addColumn(std::string const &ttype, int size, std::string const &comment)
Add a column to a table.
Definition fits.cc:1131
void updateKey(std::string const &key, T const &value, std::string const &comment)
Set a FITS header key, editing if it already exists and appending it if not.
Definition fits.cc:643
void readMetadata(daf::base::PropertySet &metadata, bool strip=false)
Read a FITS header into a PropertySet or PropertyList.
Definition fits.cc:1078
void writeTableArray(std::size_t row, int col, int nElements, T const *value)
Write an array value to a binary table.
Definition fits.cc:1160
void readKey(std::string const &key, T &value)
Read a FITS header key into the given reference.
Definition fits.cc:758
std::string getImageDType()
Return the numpy dtype equivalent of the image pixel type (e.g.
Definition fits.cc:1684
int getHdu()
Return the current HDU (0-indexed; 0 is the Primary HDU).
Definition fits.cc:483
void writeMetadata(daf::base::PropertySet const &metadata)
Read a FITS header into a PropertySet or PropertyList.
Definition fits.cc:1086
long getTableArraySize(int col)
Return the size of an array column.
Definition fits.cc:1208
std::size_t addRows(std::size_t nRows)
Append rows to a table, and return the index of the first new row.
Definition fits.cc:1140
bool checkImageType()
Return true if the current HDU is compatible with the given pixel type.
Definition fits.cc:1652
RAII scoped guard for moving the HDU in a Fits object.
Definition fits.h:706
Base class for polymorphic functors used to iterator over FITS key headers.
Definition fits.h:50
Lifetime-management for memory that goes into FITS memory files.
Definition fits.h:126
void reset()
Return the manager to the same state it would be if default-constructed.
Definition fits.cc:451
The base class for all image classed (Image, Mask, MaskedImage, ...)
Definition ImageBase.h:102
Represent a 2-dimensional array of bitmask pixels.
Definition Mask.h:82
Class for storing ordered metadata with comments.
std::string const & getComment(std::string const &name) const
Get the comment for a string property name (possibly hierarchical).
std::vector< std::string > getOrderedNames() const
Get the list of property names, in the order they were added.
Class for storing generic metadata.
Definition PropertySet.h:67
virtual std::shared_ptr< PropertySet > deepCopy() const
Make a deep copy of the PropertySet and all of its contents.
std::vector< std::string > paramNames(bool topLevelOnly=true) const
A variant of names that excludes the names of subproperties.
T compare(T... args)
T count(T... args)
T data(T... args)
T empty(T... args)
T end(T... args)
T erase(T... args)
T find_first_not_of(T... args)
T find(T... args)
T find_last_not_of(T... args)
#define INSTANTIATE_TABLE_OPS(r, data, T)
Definition fits.cc:1948
#define INSTANTIATE_IMAGE_OPS(r, data, T)
Definition fits.cc:1939
#define COLUMN_ARRAY_TYPES
Definition fits.cc:1967
#define INSTANTIATE_KEY_OPS(r, data, T)
Definition fits.cc:1928
#define KEY_TYPES
Definition fits.cc:1959
#define COLUMN_TYPES
Definition fits.cc:1963
#define INSTANTIATE_TABLE_ARRAY_OPS(r, data, T)
Definition fits.cc:1951
#define IMAGE_TYPES
Definition fits.cc:1971
#define LSST_FITS_CHECK_STATUS(fitsObj,...)
Throw a FitsError exception if the status of the given Fits object is nonzero.
Definition fits.h:112
T free(T... args)
T front(T... args)
T infinity(T... args)
T isfinite(T... args)
T isnan(T... args)
T make_pair(T... args)
T make_shared(T... args)
T malloc(T... args)
T max(T... args)
T min(T... args)
T name(T... args)
dafPlistPtr _readMetadata(T &&fitsparm, bool strip, Args... args)
Definition fits.cc:1816
std::shared_ptr< daf::base::PropertyList > combineMetadata(daf::base::PropertyList const &first, daf::base::PropertyList const &second)
Combine two sets of metadata in a FITS-appropriate fashion.
Definition fits.cc:1783
const int DEFAULT_HDU
Specify that the default HDU should be read.
std::shared_ptr< daf::base::PropertyList > readMetadata(std::string const &fileName, int hdu=DEFAULT_HDU, bool strip=false)
Read FITS header.
Definition fits.cc:1824
@ RANGE
Scale to preserve dynamic range with bad pixels msasked out.
@ STDEV_MASKED
Scale based on the standard deviation with bad pixels masked out.
@ STDEV_CFITSIO
Let CFITSIO work out the scaling (per-tile; does not respect mask planes)
CompressionAlgorithm
FITS compression algorithms.
int getBitPix()
Return the cfitsio integer BITPIX code for the given data type.
Definition fits.cc:466
std::string makeErrorMessage(std::string const &fileName="", int status=0, std::string const &msg="")
Return an error message reflecting FITS I/O errors.
Definition fits.cc:396
HduType
an enum representing the various types of FITS HDU that are available in cfitsio library
Definition fits.h:224
std::string makeLimitedFitsHeader(lsst::daf::base::PropertySet const &metadata, std::set< std::string > const &excludeNames={})
Format a PropertySet into an FITS header string in a simplistic fashion.
Definition fits.cc:433
std::shared_ptr< daf::base::PropertyList > dafPlistPtr
Definition fits.cc:1812
DitherAlgorithm
FITS quantization algorithms.
std::string const wcsNameForXY0
Definition ImageBase.h:70
Extent< int, 2 > Extent2I
Definition Extent.h:397
STL namespace.
decltype(nullptr) nullptr_t
Definition doctest.h:523
decltype(sizeof(void *)) size_t
Definition doctest.h:524
T nth_element(T... args)
T push_back(T... args)
T quiet_NaN(T... args)
T realloc(T... args)
T regex_match(T... args)
T reserve(T... args)
T rfind(T... args)
T size(T... args)
T str(T... args)
T strncmp(T... args)
Options controlling image compression with FITS.
std::optional< QuantizationOptions > quantization
Options for quantizing a floating point image (i.e. lossy compression).
T substr(T... args)
T swap(T... args)
py::scoped_interpreter guard
Definition test_image.cc:19
T to_string(T... args)
T transform(T... args)