fits_io/header/header.rs
1use crate::ascii_table::AsciiColumnFormat;
2use crate::header::card::Card;
3use crate::header::card_keys;
4use crate::header::extension_type::ExtensionType;
5use crate::header::value::Value;
6use crate::header::{BayerPattern, Bitpix, ImageType, TableColumnFormat, TableNullValue};
7use crate::util::ReadSeek;
8use chrono::{DateTime, Utc};
9use std::error::Error;
10use std::fmt::Formatter;
11use std::io::Read;
12use std::{fmt, vec};
13
14pub(crate) const CARD_NUM_BYTES: usize = 80;
15
16/// FITS files are laid out in blocks of this many bytes; headers and data
17/// sections are both padded up to a whole number of them.
18pub(crate) const BLOCK_NUM_BYTES: usize = 2880;
19
20/// Joins each value that was split across CONTINUE cards back into one card.
21///
22/// The convention marks a value as continuing by ending it with `&`, which the
23/// next CONTINUE card carries on from. Left unjoined, a long value reads as
24/// whatever fitted on its first card, `&` and all.
25fn join_continuations(cards: Vec<Card>) -> Vec<Card> {
26 let mut joined: Vec<Card> = Vec::with_capacity(cards.len());
27
28 for card in cards {
29 let Card::Continuation { string, comment } = &card else {
30 joined.push(card);
31 continue;
32 };
33
34 // A continuation only belongs to a card whose value said it was coming.
35 let continues = joined
36 .last_mut()
37 .and_then(Card::string_value_mut)
38 .filter(|value| value.ends_with('&'));
39
40 let Some(value) = continues else {
41 joined.push(card);
42 continue;
43 };
44
45 value.pop();
46 value.push_str(string.as_deref().unwrap_or_default());
47
48 // The comment for the whole value rides on the last continuation.
49 if let Some(comment) = comment
50 && let Some(last) = joined.last_mut()
51 {
52 last.set_comment(comment.clone());
53 }
54 }
55
56 joined
57}
58
59/// Whether a keyword describes the table a compressed image is stored in, rather
60/// than the image itself.
61///
62/// The `Z` keywords describe the image and are translated; the table's own
63/// structural keywords, and the column definitions, have no meaning once the
64/// image is unpacked.
65fn describes_the_table(key: &str) -> bool {
66 const STRUCTURAL: [&str; 6] = [
67 card_keys::XTENSION,
68 card_keys::BITPIX,
69 card_keys::NAXIS,
70 card_keys::PCOUNT,
71 card_keys::GCOUNT,
72 card_keys::TFIELDS,
73 ];
74
75 const COLUMN_PREFIXES: [&str; 9] = [
76 card_keys::PREFIX_TFORM_N,
77 card_keys::PREFIX_TTYPE_N,
78 card_keys::PREFIX_TSCAL_N,
79 card_keys::PREFIX_TZERO_N,
80 card_keys::PREFIX_TNULL_N,
81 card_keys::PREFIX_TDIM_N,
82 card_keys::PREFIX_TUNIT_N,
83 card_keys::PREFIX_TDISP_N,
84 card_keys::PREFIX_TBCOL_N,
85 ];
86
87 if STRUCTURAL.contains(&key) {
88 return true;
89 }
90
91 // Every `Z` keyword either describes the compression or restates a card that
92 // `uncompressed` writes fresh, so none of them belong to the image.
93 if key.starts_with('Z') {
94 return true;
95 }
96
97 COLUMN_PREFIXES.iter().any(|prefix| {
98 key.strip_prefix(prefix)
99 .is_some_and(|index| !index.is_empty() && index.chars().all(|c| c.is_ascii_digit()))
100 })
101}
102
103/// What a CHECKSUM card holds while the checksum that will replace it is being
104/// computed.
105///
106/// ASCII zeros, not spaces: the encoded value carries an ASCII-zero offset in
107/// every one of its sixteen characters, so a placeholder of zeros is what makes
108/// swapping it for the real value change the sum by exactly that value. Spaces
109/// would leave the result short by the difference.
110const BLANK_CHECKSUM: &str = "0000000000000000";
111
112/// Splits free text into the pieces a run of COMMENT or HISTORY cards holds.
113///
114/// The keyword takes the first eight columns, so seventy-two are left for the
115/// text. A line of its own in the text stays a line of its own, and anything
116/// longer than a card runs on to the next one.
117fn comment_lines(text: &str) -> Vec<String> {
118 const ROOM: usize = CARD_NUM_BYTES - 8;
119
120 let mut lines = Vec::new();
121
122 for line in text.lines() {
123 let mut rest = line;
124
125 loop {
126 if rest.len() <= ROOM {
127 lines.push(rest.to_string());
128 break;
129 }
130
131 // Breaking on a space keeps words whole; a word longer than a card
132 // is broken wherever it has to be.
133 let mut at = rest[..=ROOM]
134 .rfind(char::is_whitespace)
135 .unwrap_or(ROOM)
136 .max(1);
137 while !rest.is_char_boundary(at) {
138 at -= 1;
139 }
140
141 lines.push(rest[..at].trim_end().to_string());
142 rest = rest[at..].trim_start();
143 }
144 }
145
146 if lines.is_empty() {
147 lines.push(String::new());
148 }
149
150 lines
151}
152
153/// A FITS header: the cards that describe an HDU and its data.
154#[derive(Clone, Default)]
155pub struct Header {
156 cards: Vec<Card>,
157 /// How many bytes this header occupied in the file it was read from.
158 ///
159 /// Joining a continued value takes several cards down to one, so the card
160 /// count no longer says how far the data section is from the start of the
161 /// header. A header built in memory has no such history and is measured
162 /// from its cards.
163 bytes_in_file: Option<usize>,
164}
165
166impl Header {
167 /// How many cards this header writes out as, which is more than it holds
168 /// when a value is long enough to need continuing.
169 fn written_card_count(&self) -> usize {
170 let cards: usize = self
171 .cards
172 .iter()
173 .filter(|card| **card != Card::End)
174 .map(|card| card.to_cards().len())
175 .sum();
176
177 // `to_bytes` always writes an END card, whether or not one is held.
178 cards + 1
179 }
180
181 pub(crate) fn bytes_len(&self) -> usize {
182 if let Some(bytes) = self.bytes_in_file {
183 return bytes;
184 }
185
186 let num_bytes = self.written_card_count() * CARD_NUM_BYTES;
187 let num_off_bytes = BLOCK_NUM_BYTES - (num_bytes % BLOCK_NUM_BYTES);
188 if num_off_bytes == BLOCK_NUM_BYTES {
189 num_bytes
190 } else {
191 num_bytes + num_off_bytes
192 }
193 }
194
195 /// The AUTHOR card: who prepared the data.
196 pub fn author(&self) -> Option<&str> {
197 self.cards.iter().find_map(|card| {
198 if let Card::Author { value, .. } = card {
199 Some(value.as_str())
200 } else {
201 None
202 }
203 })
204 }
205
206 /// The BITPIX card: the type of the values in the data section.
207 ///
208 /// BITPIX is mandatory, and a header lacking it is rejected when the file is
209 /// opened, so this returns `Some` for any header read from a file. It is
210 /// `None` only for a header built by hand and left incomplete.
211 pub fn bitpix(&self) -> Option<Bitpix> {
212 self.cards.iter().find_map(|card| {
213 if let Card::Bitpix { value, .. } = card {
214 Some(*value)
215 } else {
216 None
217 }
218 })
219 }
220
221 /// The BLANK card: the raw value that stands for an undefined pixel.
222 ///
223 /// The standard defines it only for the integer BITPIX types; a floating point
224 /// array says the same thing with a NaN.
225 pub fn blank(&self) -> Option<i64> {
226 self.cards.iter().find_map(|card| {
227 if let Card::Blank { value, .. } = card {
228 Some(*value)
229 } else {
230 None
231 }
232 })
233 }
234
235 /// The BLOCKED card, a deprecated hint about the file's block size.
236 pub fn blocked(&self) -> Option<bool> {
237 self.cards.iter().find_map(|card| {
238 if let Card::Blocked { value, .. } = card {
239 Some(*value)
240 } else {
241 None
242 }
243 })
244 }
245
246 /// The BSCALE card: the factor a raw array value is multiplied by.
247 ///
248 /// See [`Header::bscale_or_default`] for the standard's default of 1.
249 pub fn bscale(&self) -> Option<f64> {
250 self.cards.iter().find_map(|card| {
251 if let Card::BScale { value, .. } = card {
252 Some(*value)
253 } else {
254 None
255 }
256 })
257 }
258
259 /// BSCALE, defaulting to 1.0 when the card is absent.
260 ///
261 /// BSCALE is optional; the FITS standard defines its default as 1.0, so a
262 /// missing card means unscaled data rather than unknown data.
263 pub fn bscale_or_default(&self) -> f64 {
264 self.bscale().unwrap_or(1.0)
265 }
266
267 /// The BUNIT card: the physical unit the array's values are in.
268 pub fn bunit(&self) -> Option<&str> {
269 self.cards.iter().find_map(|card| {
270 if let Card::BUnit { value, .. } = card {
271 Some(value.as_str())
272 } else {
273 None
274 }
275 })
276 }
277
278 /// The BZERO card: the offset added to a scaled array value.
279 ///
280 /// See [`Header::bzero_or_default`] for the standard's default of 0.
281 pub fn bzero(&self) -> Option<f64> {
282 self.cards.iter().find_map(|card| {
283 if let Card::BZero { value, .. } = card {
284 Some(*value)
285 } else {
286 None
287 }
288 })
289 }
290
291 /// BZERO, defaulting to 0.0 when the card is absent.
292 ///
293 /// BZERO is optional; the FITS standard defines its default as 0.0, so a
294 /// missing card means unshifted data rather than unknown data.
295 pub fn bzero_or_default(&self) -> f64 {
296 self.bzero().unwrap_or(0.0)
297 }
298
299 /// The DATAMAX card: the largest physical value in the array.
300 pub fn data_max(&self) -> Option<f64> {
301 self.cards.iter().find_map(|card| {
302 if let Card::DataMax { value, .. } = card {
303 Some(*value)
304 } else {
305 None
306 }
307 })
308 }
309
310 /// The DATAMIN card: the smallest physical value in the array.
311 pub fn data_min(&self) -> Option<f64> {
312 self.cards.iter().find_map(|card| {
313 if let Card::DataMin { value, .. } = card {
314 Some(*value)
315 } else {
316 None
317 }
318 })
319 }
320
321 /// The DATE card: when the file was written.
322 pub fn date(&self) -> Option<&DateTime<Utc>> {
323 self.cards.iter().find_map(|card| {
324 if let Card::Date { value, .. } = card {
325 Some(value)
326 } else {
327 None
328 }
329 })
330 }
331
332 /// The DATE-OBS card: when the observation was made.
333 pub fn date_observed(&self) -> Option<&DateTime<Utc>> {
334 self.cards.iter().find_map(|card| {
335 if let Card::DateObserved { value, .. } = card {
336 Some(value)
337 } else {
338 None
339 }
340 })
341 }
342
343 /// The EPOCH card, which EQUINOX supersedes.
344 pub fn epoch(&self) -> Option<f64> {
345 self.cards.iter().find_map(|card| {
346 if let Card::Epoch { value, .. } = card {
347 Some(*value)
348 } else {
349 None
350 }
351 })
352 }
353
354 /// The EQUINOX card: the epoch of the coordinate system, in years.
355 pub fn equinox(&self) -> Option<f64> {
356 self.cards.iter().find_map(|card| {
357 if let Card::Equinox { value, .. } = card {
358 Some(*value)
359 } else {
360 None
361 }
362 })
363 }
364
365 /// The EXTEND card: whether extensions may follow the primary HDU.
366 pub fn extend(&self) -> Option<bool> {
367 self.cards.iter().find_map(|card| {
368 if let Card::Extend { value, .. } = card {
369 Some(*value)
370 } else {
371 None
372 }
373 })
374 }
375
376 /// The EXTLEVEL card: this extension's level in a hierarchy of them.
377 pub fn extension_level(&self) -> Option<i64> {
378 self.cards.iter().find_map(|card| {
379 if let Card::ExtensionLevel { value, .. } = card {
380 Some(*value)
381 } else {
382 None
383 }
384 })
385 }
386
387 /// The EXTNAME card: this extension's name.
388 pub fn extension_name(&self) -> Option<&str> {
389 self.cards.iter().find_map(|card| {
390 if let Card::ExtensionName { value, .. } = card {
391 Some(value.as_str())
392 } else {
393 None
394 }
395 })
396 }
397
398 /// The EXTVER card: this extension's version.
399 pub fn extension_version(&self) -> Option<i64> {
400 self.cards.iter().find_map(|card| {
401 if let Card::ExtensionVersion { value, .. } = card {
402 Some(*value)
403 } else {
404 None
405 }
406 })
407 }
408
409 /// The GCOUNT card: how many groups the data section holds.
410 ///
411 /// One for everything but a random-groups HDU.
412 pub fn group_count(&self) -> Option<i64> {
413 self.cards.iter().find_map(|card| {
414 if let Card::GroupCount { value, .. } = card {
415 Some(*value)
416 } else {
417 None
418 }
419 })
420 }
421
422 /// The GROUPS card: whether this HDU uses the random-groups convention.
423 ///
424 /// See [`Header::is_random_groups`], which also checks the axis that marks it.
425 pub fn groups(&self) -> Option<bool> {
426 self.cards.iter().find_map(|card| {
427 if let Card::Groups { value, .. } = card {
428 Some(*value)
429 } else {
430 None
431 }
432 })
433 }
434
435 /// The INSTRUME card: the instrument the data came from.
436 pub fn instrument(&self) -> Option<&str> {
437 self.cards.iter().find_map(|card| {
438 if let Card::Instrument { value, .. } = card {
439 Some(value.as_str())
440 } else {
441 None
442 }
443 })
444 }
445
446 /// The NAXIS card: how many axes the data section has.
447 ///
448 /// NAXIS is mandatory, and a header lacking it is rejected when the file is
449 /// opened, so this returns `Some` for any header read from a file. It is
450 /// `None` only for a header built by hand and left incomplete.
451 pub fn naxis(&self) -> Option<i64> {
452 self.cards.iter().find_map(|card| {
453 if let Card::NAxis { value, .. } = card {
454 Some(*value)
455 } else {
456 None
457 }
458 })
459 }
460
461 /// The OBJECT card: what was observed.
462 pub fn object(&self) -> Option<&str> {
463 self.cards.iter().find_map(|card| {
464 if let Card::Object { value, .. } = card {
465 Some(value.as_str())
466 } else {
467 None
468 }
469 })
470 }
471
472 /// The OBSERVER card: who made the observation.
473 pub fn observer(&self) -> Option<&str> {
474 self.cards.iter().find_map(|card| {
475 if let Card::Observer { value, .. } = card {
476 Some(value.as_str())
477 } else {
478 None
479 }
480 })
481 }
482
483 /// The ORIGIN card: the organisation that wrote the file.
484 pub fn origin(&self) -> Option<&str> {
485 self.cards.iter().find_map(|card| {
486 if let Card::Origin { value, .. } = card {
487 Some(value.as_str())
488 } else {
489 None
490 }
491 })
492 }
493
494 /// The PCOUNT card: how many extra values follow the array.
495 ///
496 /// This is a binary table's heap, or the parameters of a random-groups HDU.
497 pub fn pcount(&self) -> Option<i64> {
498 self.cards.iter().find_map(|card| {
499 if let Card::ParameterCount { value, .. } = card {
500 Some(*value)
501 } else {
502 None
503 }
504 })
505 }
506
507 /// The REFERENC card: a publication describing the data.
508 pub fn reference(&self) -> Option<&str> {
509 self.cards.iter().find_map(|card| {
510 if let Card::Reference { value, .. } = card {
511 Some(value.as_str())
512 } else {
513 None
514 }
515 })
516 }
517
518 /// The SIMPLE card: whether the file conforms to the FITS standard.
519 ///
520 /// Only a primary header carries it.
521 pub fn simple(&self) -> Option<bool> {
522 self.cards.iter().find_map(|card| {
523 if let Card::Simple { value, .. } = card {
524 Some(*value)
525 } else {
526 None
527 }
528 })
529 }
530
531 /// The TELESCOP card: the telescope the data came from.
532 pub fn telescope(&self) -> Option<&str> {
533 self.cards.iter().find_map(|card| {
534 if let Card::Telescope { value, .. } = card {
535 Some(value.as_str())
536 } else {
537 None
538 }
539 })
540 }
541
542 /// The TFIELDS card: how many columns the table has.
543 pub fn table_fields(&self) -> Option<i64> {
544 self.cards.iter().find_map(|card| {
545 if let Card::TableFields { value, .. } = card {
546 Some(*value)
547 } else {
548 None
549 }
550 })
551 }
552
553 /// The THEAP card: where a binary table's heap starts, as a byte offset
554 /// into the data section.
555 pub fn table_heap(&self) -> Option<i64> {
556 self.cards.iter().find_map(|card| {
557 if let Card::TableHeap { value, .. } = card {
558 Some(*value)
559 } else {
560 None
561 }
562 })
563 }
564
565 /// The XTENSION card: which kind of extension this is.
566 ///
567 /// `None` for a primary header, which is not an extension.
568 pub fn extension(&self) -> Option<ExtensionType> {
569 self.cards.iter().find_map(|card| {
570 if let Card::Xtension { value, .. } = card {
571 Some(*value)
572 } else {
573 None
574 }
575 })
576 }
577
578 /// The FOCALLEN card: the telescope's focal length.
579 ///
580 /// A widespread convention among astrophotography software rather than part of
581 /// the standard, as are the other camera keywords near it.
582 pub fn focal_length(&self) -> Option<f64> {
583 self.cards.iter().find_map(|card| {
584 if let Card::FocalLength { value, .. } = card {
585 Some(*value)
586 } else {
587 None
588 }
589 })
590 }
591
592 /// The EXPTIME card: how long the exposure lasted.
593 pub fn exposure_time(&self) -> Option<std::time::Duration> {
594 self.cards.iter().find_map(|card| {
595 if let Card::ExposureTime { value, .. } = card {
596 Some(*value)
597 } else {
598 None
599 }
600 })
601 }
602
603 /// The CCD-TEMP card: the sensor's temperature, in degrees Celsius.
604 pub fn ccd_temperature(&self) -> Option<f64> {
605 self.cards.iter().find_map(|card| {
606 if let Card::CCDTemperature { value, .. } = card {
607 Some(*value)
608 } else {
609 None
610 }
611 })
612 }
613
614 /// The BAYERPAT card: the colour filter layout over the sensor.
615 ///
616 /// `None` for a monochrome sensor, or one that did not record the pattern.
617 pub fn bayer_pattern(&self) -> Option<BayerPattern> {
618 self.cards.iter().find_map(|card| {
619 if let Card::BayerPattern { value, .. } = card {
620 Some(*value)
621 } else {
622 None
623 }
624 })
625 }
626
627 /// The CREATOR card: the software that wrote the file.
628 pub fn creator(&self) -> Option<&str> {
629 self.cards.iter().find_map(|card| {
630 if let Card::Creator { value, .. } = card {
631 Some(value.as_str())
632 } else {
633 None
634 }
635 })
636 }
637
638 /// The XORGSUBF card: where a subframe starts on the sensor, horizontally.
639 pub fn subframe_x_position_in_binned_pixels(&self) -> Option<i64> {
640 self.cards.iter().find_map(|card| {
641 if let Card::SubframeXPositionInBinnedPixels { value, .. } = card {
642 Some(*value)
643 } else {
644 None
645 }
646 })
647 }
648
649 /// The YORGSUBF card: where a subframe starts on the sensor, vertically.
650 pub fn subframe_y_position_in_binned_pixels(&self) -> Option<i64> {
651 self.cards.iter().find_map(|card| {
652 if let Card::SubframeYPositionInBinnedPixels { value, .. } = card {
653 Some(*value)
654 } else {
655 None
656 }
657 })
658 }
659
660 /// The XBINNING card: how many sensor pixels were binned into one, horizontally.
661 pub fn binned_pixels_x(&self) -> Option<i64> {
662 self.cards.iter().find_map(|card| {
663 if let Card::BinnedPixelsX { value, .. } = card {
664 Some(*value)
665 } else {
666 None
667 }
668 })
669 }
670
671 /// The YBINNING card: how many sensor pixels were binned into one, vertically.
672 pub fn binned_pixels_y(&self) -> Option<i64> {
673 self.cards.iter().find_map(|card| {
674 if let Card::BinnedPixelsY { value, .. } = card {
675 Some(*value)
676 } else {
677 None
678 }
679 })
680 }
681
682 /// The CCDXBIN card, another spelling of XBINNING.
683 pub fn ccd_binned_pixels_x(&self) -> Option<i64> {
684 self.cards.iter().find_map(|card| {
685 if let Card::CCDBinnedPixelsX { value, .. } = card {
686 Some(*value)
687 } else {
688 None
689 }
690 })
691 }
692
693 /// The CCDYBIN card, another spelling of YBINNING.
694 pub fn ccd_binned_pixels_y(&self) -> Option<i64> {
695 self.cards.iter().find_map(|card| {
696 if let Card::CCDBinnedPixelsY { value, .. } = card {
697 Some(*value)
698 } else {
699 None
700 }
701 })
702 }
703
704 /// The XPIXSZ card: the width of a pixel in microns, binning included.
705 pub fn pixel_size_x_with_binning_in_microns(&self) -> Option<f64> {
706 self.cards.iter().find_map(|card| {
707 if let Card::PixelSizeXWithBinningInMicrons { value, .. } = card {
708 Some(*value)
709 } else {
710 None
711 }
712 })
713 }
714
715 /// The YPIXSZ card: the height of a pixel in microns, binning included.
716 pub fn pixel_size_y_with_binning_in_microns(&self) -> Option<f64> {
717 self.cards.iter().find_map(|card| {
718 if let Card::PixelSizeYWithBinningInMicrons { value, .. } = card {
719 Some(*value)
720 } else {
721 None
722 }
723 })
724 }
725
726 /// The IMAGETYP card: whether this is a light, dark, flat or bias frame.
727 pub fn image_type(&self) -> Option<&ImageType> {
728 self.cards.iter().find_map(|card| {
729 if let Card::ImageType { value, .. } = card {
730 Some(value)
731 } else {
732 None
733 }
734 })
735 }
736
737 /// The EXPOSURE card, another spelling of EXPTIME.
738 pub fn exposure(&self) -> Option<std::time::Duration> {
739 self.cards.iter().find_map(|card| {
740 if let Card::Exposure { value, .. } = card {
741 Some(*value)
742 } else {
743 None
744 }
745 })
746 }
747
748 /// The RA card: the right ascension the telescope was pointed at.
749 pub fn ra(&self) -> Option<f64> {
750 self.cards.iter().find_map(|card| {
751 if let Card::Ra { value, .. } = card {
752 Some(*value)
753 } else {
754 None
755 }
756 })
757 }
758
759 /// The DEC card: the declination the telescope was pointed at.
760 pub fn dec(&self) -> Option<f64> {
761 self.cards.iter().find_map(|card| {
762 if let Card::Dec { value, .. } = card {
763 Some(*value)
764 } else {
765 None
766 }
767 })
768 }
769
770 /// The GUIDECAM card: the guide camera in use.
771 pub fn guide_cam(&self) -> Option<&str> {
772 self.cards.iter().find_map(|card| {
773 if let Card::GuideCam { value, .. } = card {
774 Some(value.as_str())
775 } else {
776 None
777 }
778 })
779 }
780
781 /// The FOCUSPOS card: where the focuser was.
782 pub fn focus_position(&self) -> Option<i64> {
783 self.cards.iter().find_map(|card| {
784 if let Card::FocusPosition { value, .. } = card {
785 Some(*value)
786 } else {
787 None
788 }
789 })
790 }
791
792 /// The SITELONG card: the observing site's longitude.
793 pub fn site_longitude(&self) -> Option<f64> {
794 self.cards.iter().find_map(|card| {
795 if let Card::SiteLongitude { value, .. } = card {
796 Some(*value)
797 } else {
798 None
799 }
800 })
801 }
802
803 /// The SITELAT card: the observing site's latitude.
804 pub fn site_latitude(&self) -> Option<f64> {
805 self.cards.iter().find_map(|card| {
806 if let Card::SiteLatitude { value, .. } = card {
807 Some(*value)
808 } else {
809 None
810 }
811 })
812 }
813
814 /// The IMAGEW card: the image's width, as the writing software recorded it.
815 pub fn image_width(&self) -> Option<i64> {
816 self.cards.iter().find_map(|card| {
817 if let Card::ImageWidth { value, .. } = card {
818 Some(*value)
819 } else {
820 None
821 }
822 })
823 }
824
825 /// The IMAGEH card: the image's height, as the writing software recorded it.
826 pub fn image_height(&self) -> Option<i64> {
827 self.cards.iter().find_map(|card| {
828 if let Card::ImageHeight { value, .. } = card {
829 Some(*value)
830 } else {
831 None
832 }
833 })
834 }
835
836 /// The CDELTn card for axis `index`: how far the world coordinate moves
837 /// per pixel.
838 pub fn coordinate_delta(&self, index: usize) -> Option<f64> {
839 self.cards.iter().find_map(|card| {
840 if let Card::CoordinateDeltaN {
841 value, index: idx, ..
842 } = card
843 && index == *idx
844 {
845 return Some(*value);
846 };
847 None
848 })
849 }
850
851 /// The CROTAn card for axis `index`: the rotation between the pixel and
852 /// world axes, in degrees.
853 pub fn coordinate_rotation(&self, index: usize) -> Option<f64> {
854 self.cards.iter().find_map(|card| {
855 if let Card::CoordinateRotationN {
856 value, index: idx, ..
857 } = card
858 && index == *idx
859 {
860 return Some(*value);
861 };
862 None
863 })
864 }
865
866 /// The CRPIXn card for axis `index`: the pixel that the reference value
867 /// sits at, counting from 1.
868 pub fn coordinate_reference_pixel(&self, index: usize) -> Option<f64> {
869 self.cards.iter().find_map(|card| {
870 if let Card::CoordinateReferencePixelN {
871 value, index: idx, ..
872 } = card
873 && index == *idx
874 {
875 return Some(*value);
876 };
877 None
878 })
879 }
880
881 /// The CRVALn card for axis `index`: the world coordinate at the
882 /// reference pixel.
883 pub fn coordinate_value_at_pixel(&self, index: usize) -> Option<f64> {
884 self.cards.iter().find_map(|card| {
885 if let Card::CoordinateValueAtPixelN {
886 value, index: idx, ..
887 } = card
888 && index == *idx
889 {
890 return Some(*value);
891 };
892 None
893 })
894 }
895
896 /// The CDi_j card: one element of the matrix taking pixel offsets to
897 /// intermediate world coordinates.
898 ///
899 /// `row` and `column` count from 0, so CD1_1 is `coordinate_transform(0, 0)`.
900 /// This matrix carries the scale as well as the rotation, which is why a
901 /// header using it has no CDELTn cards.
902 pub fn coordinate_transform(&self, row: usize, column: usize) -> Option<f64> {
903 self.matrix_element("CD", row, column)
904 }
905
906 /// The PCi_j card: one element of the dimensionless matrix that rotates and
907 /// skews pixel offsets, before CDELTn scales them.
908 ///
909 /// `row` and `column` count from 0, so PC1_1 is
910 /// `coordinate_rotation_matrix(0, 0)`.
911 pub fn coordinate_rotation_matrix(&self, row: usize, column: usize) -> Option<f64> {
912 self.matrix_element("PC", row, column)
913 }
914
915 /// Reads one element of a two-index keyword family such as CDi_j.
916 ///
917 /// These are not among the keywords this crate models individually, so they
918 /// arrive as plain value cards and are looked up by name.
919 fn matrix_element(&self, prefix: &str, row: usize, column: usize) -> Option<f64> {
920 let key = format!("{}{}_{}", prefix, row + 1, column + 1);
921
922 self.raw_card(&key)
923 .into_iter()
924 .find_map(|value| match value {
925 Value::Float { value, .. } => Some(value),
926 // A whole number is commonly written without a decimal point.
927 Value::Integer { value, .. } => Some(value as f64),
928 _ => None,
929 })
930 }
931
932 /// The CTYPEn card for axis `index`: what the axis measures, and the
933 /// projection it uses.
934 pub fn coordinate_axis_name(&self, index: usize) -> Option<&str> {
935 self.cards.iter().find_map(|card| {
936 if let Card::CoordinateAxisNameN {
937 value, index: idx, ..
938 } = card
939 && index == *idx
940 {
941 return Some(value.as_str());
942 };
943 None
944 })
945 }
946
947 /// The NAXISn card for axis `index`: how long that axis is.
948 ///
949 /// `index` counts from 0, so NAXIS1 is `naxis_n(0)`.
950 pub fn naxis_n(&self, index: usize) -> Option<i64> {
951 self.cards.iter().find_map(|card| {
952 if let Card::NAxisN {
953 value, index: idx, ..
954 } = card
955 && index == *idx
956 {
957 return Some(*value);
958 };
959 None
960 })
961 }
962
963 /// The PSCALn card for group parameter `index`.
964 pub fn parameter_scaling_factor(&self, index: usize) -> Option<f64> {
965 self.cards.iter().find_map(|card| {
966 if let Card::ParameterScalingFactorN {
967 value, index: idx, ..
968 } = card
969 && index == *idx
970 {
971 return Some(*value);
972 };
973 None
974 })
975 }
976
977 /// The PTYPEn card for group parameter `index`: what it measures.
978 pub fn parameter_type(&self, index: usize) -> Option<&str> {
979 self.cards.iter().find_map(|card| {
980 if let Card::ParameterTypeN {
981 value, index: idx, ..
982 } = card
983 && index == *idx
984 {
985 return Some(value.as_str());
986 };
987 None
988 })
989 }
990
991 /// The PZEROn card for group parameter `index`.
992 pub fn parameter_scaling_zero_point(&self, index: usize) -> Option<f64> {
993 self.cards.iter().find_map(|card| {
994 if let Card::ParameterScalingZeroPointN {
995 value, index: idx, ..
996 } = card
997 && index == *idx
998 {
999 return Some(*value);
1000 };
1001 None
1002 })
1003 }
1004
1005 /// The TBCOLn card for column `index`: where the column starts within an
1006 /// ASCII table's row, counting from 1.
1007 pub fn table_column(&self, index: usize) -> Option<i64> {
1008 self.cards.iter().find_map(|card| {
1009 if let Card::TableColumnN {
1010 value, index: idx, ..
1011 } = card
1012 && index == *idx
1013 {
1014 return Some(*value);
1015 };
1016 None
1017 })
1018 }
1019
1020 /// The TDIMn card for column `index`: the shape of a multidimensional
1021 /// column, as written.
1022 pub fn table_dimensions(&self, index: usize) -> Option<&str> {
1023 self.cards.iter().find_map(|card| {
1024 if let Card::TableDimensionsN {
1025 value, index: idx, ..
1026 } = card
1027 && index == *idx
1028 {
1029 return Some(value.as_str());
1030 };
1031 None
1032 })
1033 }
1034
1035 /// The TDISPn card for column `index`: how the column is best displayed.
1036 pub fn table_display_format(&self, index: usize) -> Option<&str> {
1037 self.cards.iter().find_map(|card| {
1038 if let Card::TableDisplayFormatN {
1039 value, index: idx, ..
1040 } = card
1041 && index == *idx
1042 {
1043 return Some(value.as_str());
1044 };
1045 None
1046 })
1047 }
1048
1049 /// The TNULLn card for column `index`: the value that marks an undefined
1050 /// entry in that column.
1051 pub fn table_null_value(&self, index: usize) -> Option<&TableNullValue> {
1052 self.cards.iter().find_map(|card| {
1053 if let Card::TableNullValueN {
1054 value, index: idx, ..
1055 } = card
1056 && index == *idx
1057 {
1058 return Some(value);
1059 };
1060 None
1061 })
1062 }
1063
1064 /// The TSCALn card for column `index`: the factor a stored entry is
1065 /// multiplied by.
1066 pub fn table_scaling_factor(&self, index: usize) -> Option<f64> {
1067 self.cards.iter().find_map(|card| {
1068 if let Card::TableScalingFactorN {
1069 value, index: idx, ..
1070 } = card
1071 && index == *idx
1072 {
1073 return Some(*value);
1074 };
1075 None
1076 })
1077 }
1078
1079 /// The TTYPEn card for column `index`: the column's name.
1080 pub fn table_column_type(&self, index: usize) -> Option<&str> {
1081 self.cards.iter().find_map(|card| {
1082 if let Card::TableTypeN {
1083 value, index: idx, ..
1084 } = card
1085 && index == *idx
1086 {
1087 return Some(value.as_str());
1088 };
1089 None
1090 })
1091 }
1092
1093 /// The TFORMn card for column `index`, exactly as written.
1094 pub fn table_format(&self, index: usize) -> Option<&str> {
1095 self.cards.iter().find_map(|card| {
1096 if let Card::TableFormatN {
1097 value, index: idx, ..
1098 } = card
1099 && index == *idx
1100 {
1101 return Some(value.as_str());
1102 };
1103 None
1104 })
1105 }
1106
1107 /// The TFORMn card for column `index`, read as a binary table format.
1108 ///
1109 /// `None` when the card is absent or does not name a binary table format,
1110 /// which is the case for every ASCII table; use
1111 /// [`Header::ascii_column_format`] for those.
1112 pub fn table_column_format(&self, index: usize) -> Option<TableColumnFormat> {
1113 TableColumnFormat::try_from(self.table_format(index)?.to_string()).ok()
1114 }
1115
1116 /// The TFORMn card for column `index`, read as an ASCII table format.
1117 ///
1118 /// `None` when the card is absent or does not name an ASCII table format.
1119 pub fn ascii_column_format(&self, index: usize) -> Option<AsciiColumnFormat> {
1120 AsciiColumnFormat::try_from(self.table_format(index)?.to_string()).ok()
1121 }
1122
1123 /// The TUNITn card for column `index`: the column's physical unit.
1124 pub fn table_unit(&self, index: usize) -> Option<&str> {
1125 self.cards.iter().find_map(|card| {
1126 if let Card::TableUnitN {
1127 value, index: idx, ..
1128 } = card
1129 && index == *idx
1130 {
1131 return Some(value.as_str());
1132 };
1133 None
1134 })
1135 }
1136
1137 /// The TZEROn card for column `index`: the offset added after scaling.
1138 pub fn table_scaling_zero_point(&self, index: usize) -> Option<f64> {
1139 self.cards.iter().find_map(|card| {
1140 if let Card::TableScalingZeroPointN {
1141 value, index: idx, ..
1142 } = card
1143 && index == *idx
1144 {
1145 return Some(*value);
1146 };
1147 None
1148 })
1149 }
1150
1151 pub(crate) fn data_block_len(&self) -> usize {
1152 let data_size = self.data_bytes_len();
1153
1154 let num_off_bytes = BLOCK_NUM_BYTES - (data_size % BLOCK_NUM_BYTES);
1155 if num_off_bytes == BLOCK_NUM_BYTES {
1156 data_size
1157 } else {
1158 data_size + num_off_bytes
1159 }
1160 }
1161
1162 /// Size of this HDU's data section in bytes, excluding block padding.
1163 ///
1164 /// This is the standard's
1165 /// `BITPIX/8 * GCOUNT * (PCOUNT + NAXIS1 * ... * NAXISn)`. PCOUNT matters
1166 /// for binary tables: it is the size of the heap that follows the rows, and
1167 /// leaving it out puts the *next* HDU at the wrong offset in every file
1168 /// whose table has variable length array columns.
1169 ///
1170 /// Returns 0 for a header that declares no data, and also for an incomplete
1171 /// header — a header missing BITPIX, NAXIS or one of its NAXISn cards cannot
1172 /// describe a data section. [`Header::validate_primary`] and
1173 /// [`Header::validate_extension`] reject such headers up front, so this
1174 /// fallback is only reachable for hand-built headers.
1175 pub(crate) fn data_bytes_len(&self) -> usize {
1176 let (Some(bitpix), Some(number_of_axis)) = (self.bitpix(), self.naxis()) else {
1177 return 0;
1178 };
1179
1180 if number_of_axis <= 0 {
1181 return 0;
1182 }
1183
1184 let mut elements: usize = 1;
1185 for axis in 0..number_of_axis {
1186 // NAXISn is untrusted input: a negative or absurd length must not
1187 // overflow the running product.
1188 let Some(length) = self.naxis_n(axis as usize) else {
1189 return 0;
1190 };
1191 let Ok(length) = usize::try_from(length) else {
1192 return 0;
1193 };
1194 let Some(product) = elements.checked_mul(length) else {
1195 return 0;
1196 };
1197 elements = product;
1198 }
1199
1200 // PCOUNT and GCOUNT are mandatory on extensions and absent from a
1201 // conforming primary header, where they are 0 and 1.
1202 let pcount = self.pcount().unwrap_or(0).max(0) as usize;
1203 let gcount = self.group_count().unwrap_or(1).max(0) as usize;
1204
1205 let Some(bytes) = elements.checked_add(pcount) else {
1206 return 0;
1207 };
1208 let Some(bytes) = bytes.checked_mul(gcount) else {
1209 return 0;
1210 };
1211 let Some(bytes) = bytes.checked_mul(bitpix.byte_size()) else {
1212 return 0;
1213 };
1214
1215 bytes
1216 }
1217
1218 /// Whether this HDU is an image stored compressed inside a table.
1219 ///
1220 /// The tiled image convention keeps a compressed image in a binary table,
1221 /// one tile per row, and describes the image it stands for with keywords
1222 /// beginning `Z`. Such an HDU reads as a table unless it is decompressed;
1223 /// see [`BinTableHDU::read_compressed_image`](crate::hdu::BinTableHDU::read_compressed_image).
1224 pub fn is_compressed_image(&self) -> bool {
1225 matches!(
1226 self.raw_card(card_keys::ZIMAGE).first(),
1227 Some(Value::Logical { value: true, .. })
1228 )
1229 }
1230
1231 /// The ZBITPIX card: the type of the values in the image once decompressed.
1232 pub fn compressed_bitpix(&self) -> Option<Bitpix> {
1233 Bitpix::try_from(self.z_integer(card_keys::ZBITPIX)?).ok()
1234 }
1235
1236 /// The ZNAXIS card: how many axes the decompressed image has.
1237 pub fn compressed_naxis(&self) -> Option<i64> {
1238 self.z_integer(card_keys::ZNAXIS)
1239 }
1240
1241 /// The ZNAXISn card for axis `index`: the decompressed image's length along
1242 /// it. `index` counts from 0.
1243 pub fn compressed_naxis_n(&self, index: usize) -> Option<i64> {
1244 self.z_integer(&format!("{}{}", card_keys::PREFIX_ZNAXIS_N, index + 1))
1245 }
1246
1247 /// The ZTILEn card for axis `index`: how far a tile reaches along it.
1248 ///
1249 /// The convention's default is a tile one row of the image wide, which is
1250 /// what a header that leaves the card out means.
1251 pub fn compressed_tile_size(&self, index: usize) -> i64 {
1252 if let Some(size) = self.z_integer(&format!("{}{}", card_keys::PREFIX_ZTILE_N, index + 1)) {
1253 return size;
1254 }
1255
1256 match index {
1257 0 => self.compressed_naxis_n(0).unwrap_or(1),
1258 _ => 1,
1259 }
1260 }
1261
1262 /// The ZCMPTYPE card: which algorithm the tiles were compressed with.
1263 pub fn compression_type(&self) -> Option<&str> {
1264 self.cards.iter().find_map(|card| match card {
1265 Card::Value {
1266 name,
1267 value: Value::String { value, .. },
1268 } if name == card_keys::ZCMPTYPE => Some(value.as_str()),
1269 _ => None,
1270 })
1271 }
1272
1273 /// A compression parameter, looked up by the name a ZNAMEn card gives it.
1274 ///
1275 /// The algorithms take their settings as name and value pairs rather than as
1276 /// keywords of their own, so Rice's block size arrives as `ZNAME1 =
1277 /// 'BLOCKSIZE'` with the value in `ZVAL1`.
1278 pub fn compression_parameter(&self, name: &str) -> Option<i64> {
1279 for index in 1.. {
1280 let key = format!("{}{}", card_keys::PREFIX_ZNAME_N, index);
1281 let found = self.cards.iter().find_map(|card| match card {
1282 Card::Value {
1283 name: key_name,
1284 value: Value::String { value, .. },
1285 } if *key_name == key => Some(value.clone()),
1286 _ => None,
1287 });
1288
1289 let found = found?;
1290
1291 if found.trim() == name {
1292 return self.z_integer(&format!("{}{}", card_keys::PREFIX_ZVAL_N, index));
1293 }
1294 }
1295
1296 None
1297 }
1298
1299 /// The ZQUANTIZ card: how a floating point image's values were turned into
1300 /// the integers the compressor works on.
1301 ///
1302 /// `NO_DITHER` quantises plainly; the two `SUBTRACTIVE_DITHER` methods add a
1303 /// known pseudo-random number to each value before rounding it and take the
1304 /// same number off again on the way back, which keeps quantisation from
1305 /// laying a pattern over a smooth background.
1306 pub fn quantization_method(&self) -> Option<&str> {
1307 self.z_string(card_keys::ZQUANTIZ)
1308 }
1309
1310 /// The ZDITHER0 card: which entry of the dithering sequence the first tile
1311 /// starts at.
1312 pub fn dither_seed(&self) -> Option<i64> {
1313 self.z_integer(card_keys::ZDITHER0)
1314 }
1315
1316 /// The ZBLANK card: the quantised value that stands for a pixel the image
1317 /// does not define.
1318 ///
1319 /// A tile may carry its own ZBLANK column instead, which takes precedence
1320 /// over this for that tile.
1321 pub fn compressed_blank(&self) -> Option<i64> {
1322 self.z_integer(card_keys::ZBLANK)
1323 }
1324
1325 /// One of the `Z` keywords as a string.
1326 fn z_string(&self, key: &str) -> Option<&str> {
1327 self.cards.iter().find_map(|card| match card {
1328 Card::Value {
1329 name,
1330 value: Value::String { value, .. },
1331 } if name == key => Some(value.as_str()),
1332 _ => None,
1333 })
1334 }
1335
1336 /// One of the `Z` keywords as an integer.
1337 ///
1338 /// None of them are among the keywords this crate models individually, so
1339 /// they arrive as plain value cards and are looked up by name.
1340 fn z_integer(&self, key: &str) -> Option<i64> {
1341 self.raw_card(key)
1342 .into_iter()
1343 .find_map(|value| match value {
1344 Value::Integer { value, .. } => Some(value),
1345 Value::Float { value, .. } => Some(value as i64),
1346 _ => None,
1347 })
1348 }
1349
1350 /// How many two-dimensional planes the image inside a compressed table
1351 /// holds.
1352 pub(crate) fn compressed_plane_count(&self) -> usize {
1353 let Some(axes) = self.compressed_naxis() else {
1354 return 0;
1355 };
1356
1357 if axes < 2 {
1358 return 0;
1359 }
1360
1361 let mut planes = 1_usize;
1362 for axis in 2..axes as usize {
1363 let length = self.compressed_naxis_n(axis).unwrap_or(0).max(0) as usize;
1364 let Some(product) = planes.checked_mul(length) else {
1365 return 0;
1366 };
1367 planes = product;
1368 }
1369
1370 planes
1371 }
1372
1373 /// The header the decompressed image would have.
1374 ///
1375 /// Every card that describes the table rather than the image is dropped, and
1376 /// BITPIX and the NAXISn cards are taken from their `Z` counterparts, so
1377 /// that the result describes the image the HDU stands for. Anything else the
1378 /// header carried — WCS keywords especially — comes across untouched.
1379 pub fn uncompressed(&self) -> Self {
1380 let mut header = Self {
1381 cards: self
1382 .cards
1383 .iter()
1384 .filter(|card| !describes_the_table(&card.key()))
1385 .cloned()
1386 .collect(),
1387 bytes_in_file: None,
1388 };
1389
1390 header.remove_prefixed(card_keys::PREFIX_NAXIS_N);
1391
1392 if let Some(bitpix) = self.compressed_bitpix() {
1393 header.set(Card::Bitpix {
1394 value: bitpix,
1395 comment: None,
1396 });
1397 }
1398
1399 let axes = self.compressed_naxis().unwrap_or(0).max(0);
1400 header.set(Card::NAxis {
1401 value: axes,
1402 comment: None,
1403 });
1404 for axis in 0..axes as usize {
1405 header.set(Card::NAxisN {
1406 index: axis,
1407 value: self.compressed_naxis_n(axis).unwrap_or(0),
1408 comment: None,
1409 });
1410 }
1411
1412 header
1413 }
1414
1415 /// Whether this HDU uses the random-groups convention.
1416 ///
1417 /// Such an HDU's data section is not an image but GCOUNT groups, each one a
1418 /// run of PCOUNT parameters followed by an array. The convention is marked
1419 /// by `GROUPS = T`, and by a first axis of length zero standing in for the
1420 /// axis the groups occupy.
1421 pub fn is_random_groups(&self) -> bool {
1422 self.groups() == Some(true) && self.naxis_n(0) == Some(0)
1423 }
1424
1425 /// How many values each group's array holds, for a random-groups HDU.
1426 ///
1427 /// The first axis is the placeholder that marks the convention, so the array
1428 /// is the axes after it.
1429 pub(crate) fn group_array_len(&self) -> usize {
1430 let Some(axes) = self.naxis() else {
1431 return 0;
1432 };
1433
1434 let mut elements = 1_usize;
1435 for axis in 1..axes.max(0) as usize {
1436 let length = self.naxis_n(axis).unwrap_or(0).max(0) as usize;
1437 let Some(product) = elements.checked_mul(length) else {
1438 return 0;
1439 };
1440 elements = product;
1441 }
1442
1443 elements
1444 }
1445
1446 /// How many two-dimensional planes an image HDU's data section holds.
1447 ///
1448 /// The first two axes are the image; every axis beyond them multiplies the
1449 /// number of images, so a NAXIS = 4 array with NAXIS3 = 2 and NAXIS4 = 3
1450 /// holds six planes, not two. An HDU with fewer than two axes holds no
1451 /// image at all.
1452 pub(crate) fn image_plane_count(&self) -> usize {
1453 let Some(axes) = self.naxis() else {
1454 return 0;
1455 };
1456
1457 if axes < 2 {
1458 return 0;
1459 }
1460
1461 let mut planes = 1_usize;
1462 for axis in 2..axes as usize {
1463 let length = self.naxis_n(axis).unwrap_or(0).max(0) as usize;
1464
1465 // A zero-length axis means no data at all, not "ignore this axis".
1466 let Some(product) = planes.checked_mul(length) else {
1467 return 0;
1468 };
1469 planes = product;
1470 }
1471
1472 planes
1473 }
1474
1475 /// Byte offset of a binary table's heap from the start of its data section.
1476 ///
1477 /// THEAP names it explicitly; a table without that card puts the heap
1478 /// directly after the last row.
1479 pub(crate) fn table_heap_offset(&self) -> usize {
1480 if let Some(offset) = self.table_heap()
1481 && let Ok(offset) = usize::try_from(offset)
1482 {
1483 return offset;
1484 }
1485
1486 let rows = |axis| self.naxis_n(axis).unwrap_or(0).max(0) as usize;
1487 rows(0).saturating_mul(rows(1))
1488 }
1489
1490 /// Renders this header as the bytes it occupies in a file.
1491 ///
1492 /// The result is always a whole number of 2880-byte blocks, padded with
1493 /// spaces, and always ends with an END card — a header without one is not a
1494 /// header a reader can find the end of.
1495 pub fn to_bytes(&self) -> Vec<u8> {
1496 let mut bytes = Vec::with_capacity(self.bytes_len());
1497
1498 for card in &self.cards {
1499 if card == &Card::End {
1500 break;
1501 }
1502 for written in card.to_cards() {
1503 bytes.extend_from_slice(&written);
1504 }
1505 }
1506
1507 bytes.extend_from_slice(&Card::End.to_bytes());
1508
1509 let padding = (BLOCK_NUM_BYTES - bytes.len() % BLOCK_NUM_BYTES) % BLOCK_NUM_BYTES;
1510 bytes.resize(bytes.len() + padding, b' ');
1511
1512 bytes
1513 }
1514
1515 /// Writes the DATASUM and CHECKSUM cards for an HDU whose data section is
1516 /// `data`.
1517 ///
1518 /// CHECKSUM covers the whole HDU including its own card, so it cannot be
1519 /// known until the header has been rendered. It is set to blanks here and
1520 /// filled in by [`Header::checksummed_bytes`] once there is a header to sum.
1521 pub(crate) fn set_checksum_placeholders(&mut self, data: &[u8]) {
1522 self.set(Card::Value {
1523 name: card_keys::DATASUM.to_string(),
1524 value: Value::String {
1525 value: crate::checksum::sum32(data, 0).to_string(),
1526 comment: Some("checksum of the data section".into()),
1527 },
1528 });
1529 self.set(Card::Value {
1530 name: card_keys::CHECKSUM.to_string(),
1531 value: Value::String {
1532 value: BLANK_CHECKSUM.to_string(),
1533 comment: Some("checksum of the whole HDU".into()),
1534 },
1535 });
1536 }
1537
1538 /// This header rendered with a CHECKSUM that is correct for it and `data`.
1539 ///
1540 /// The card is written blank, the whole HDU is summed, and the card is then
1541 /// filled in with the complement of that sum — so that summing the finished
1542 /// HDU gives all ones. The blank value and the final one are the same width,
1543 /// so filling it in does not move anything.
1544 pub(crate) fn checksummed_bytes(&self, data: &[u8]) -> Vec<u8> {
1545 let mut header = self.clone();
1546 header.set_checksum_placeholders(data);
1547
1548 let blank = header.to_bytes();
1549
1550 let sum = crate::checksum::sum32(data, crate::checksum::sum32(&blank, 0));
1551 let checksum = crate::checksum::encode(crate::checksum::complement(sum));
1552
1553 header.set(Card::Value {
1554 name: card_keys::CHECKSUM.to_string(),
1555 value: Value::String {
1556 value: checksum,
1557 comment: Some("checksum of the whole HDU".into()),
1558 },
1559 });
1560
1561 let bytes = header.to_bytes();
1562 debug_assert_eq!(
1563 bytes.len(),
1564 blank.len(),
1565 "filling in the checksum must not change the header's length"
1566 );
1567
1568 bytes
1569 }
1570
1571 /// Sets the NAXISn card for axis `index`, which counts from 0.
1572 ///
1573 /// # Errors
1574 ///
1575 /// Returns an error for a negative length, which no axis can have.
1576 pub fn set_naxis_n(
1577 &mut self,
1578 index: usize,
1579 length: i64,
1580 ) -> Result<(), Box<dyn Error + Send + Sync>> {
1581 if length < 0 {
1582 return Err(format!("An axis cannot be {} long", length).into());
1583 }
1584
1585 self.set(Card::NAxisN {
1586 index,
1587 value: length,
1588 comment: None,
1589 });
1590
1591 Ok(())
1592 }
1593
1594 /// Checks that this header describes a data section of `actual` bytes.
1595 ///
1596 /// The header is the only thing that says how to read the data after it, so
1597 /// one that disagrees with what follows produces a file nothing can read:
1598 /// the next HDU is looked for at the wrong offset, and the array comes back
1599 /// the wrong shape. Setting an image or a table keeps the two in step, but a
1600 /// caller who edits NAXISn through [`Header::header_mut`] can put them out
1601 /// of step again, and this is where that is caught.
1602 ///
1603 /// This can only catch an HDU that carries its own data. Where the data is
1604 /// still in a file, the header is what says how much of it to read, so the
1605 /// two cannot disagree — a header edited to describe more than the file
1606 /// holds fails when the read runs off the end instead.
1607 ///
1608 /// [`Header::header_mut`]: crate::hdu::HDU::header_mut
1609 pub(crate) fn validate_against_data(
1610 &self,
1611 actual: usize,
1612 ) -> Result<(), Box<dyn Error + Send + Sync>> {
1613 let declared = self.data_bytes_len();
1614
1615 // The data section is padded out to whole blocks, so anything from the
1616 // declared length up to the end of its last block is consistent.
1617 let padded = declared.div_ceil(BLOCK_NUM_BYTES) * BLOCK_NUM_BYTES;
1618
1619 if actual < declared || actual > padded {
1620 return Err(format!(
1621 "This header describes {} bytes of data, but the HDU holds {}. A header that \
1622 disagrees with its data produces a file that cannot be read back.",
1623 declared, actual
1624 )
1625 .into());
1626 }
1627
1628 Ok(())
1629 }
1630
1631 /// This header with its mandatory cards present and in the order the FITS
1632 /// standard requires.
1633 ///
1634 /// The standard is strict about the front of a header: a primary header
1635 /// opens with SIMPLE, BITPIX, NAXIS and then one NAXISn per axis, and an
1636 /// extension header opens with XTENSION and continues through PCOUNT and
1637 /// GCOUNT. A reader is entitled to reject anything else, so a header that is
1638 /// being written out is put in that order here rather than left however it
1639 /// was assembled.
1640 ///
1641 /// Missing mandatory cards are filled in with the values the standard
1642 /// defines: a header built from nothing has no SIMPLE card at all, and a
1643 /// file written from one would not be readable.
1644 ///
1645 /// `extension` names the kind of extension this header belongs to, or
1646 /// `None` for the primary header.
1647 pub(crate) fn conformed(&self, extension: Option<ExtensionType>) -> Self {
1648 let mut mandatory = Vec::new();
1649
1650 match extension {
1651 None => mandatory.push(Card::Simple {
1652 // A file this crate wrote conforms to the standard, so SIMPLE is
1653 // true even if the header it came from said otherwise.
1654 value: true,
1655 comment: self.comment_for(card_keys::SIMPLE),
1656 }),
1657 Some(extension) => mandatory.push(Card::Xtension {
1658 value: extension,
1659 comment: self.comment_for(card_keys::XTENSION),
1660 }),
1661 }
1662
1663 mandatory.push(Card::Bitpix {
1664 value: self.bitpix().unwrap_or(Bitpix::U8),
1665 comment: self.comment_for(card_keys::BITPIX),
1666 });
1667
1668 let axes = self.naxis().unwrap_or(0).max(0);
1669 mandatory.push(Card::NAxis {
1670 value: axes,
1671 comment: self.comment_for(card_keys::NAXIS),
1672 });
1673
1674 for axis in 0..axes as usize {
1675 mandatory.push(Card::NAxisN {
1676 index: axis,
1677 value: self.naxis_n(axis).unwrap_or(0),
1678 comment: self.comment_for(&format!("{}{}", card_keys::PREFIX_NAXIS_N, axis + 1)),
1679 });
1680 }
1681
1682 // PCOUNT and GCOUNT are mandatory on every extension and are not written
1683 // in a conforming primary header.
1684 if extension.is_some() {
1685 mandatory.push(Card::ParameterCount {
1686 value: self.pcount().unwrap_or(0),
1687 comment: self.comment_for(card_keys::PCOUNT),
1688 });
1689 mandatory.push(Card::GroupCount {
1690 value: self.group_count().unwrap_or(1),
1691 comment: self.comment_for(card_keys::GCOUNT),
1692 });
1693 }
1694
1695 // A table's TFIELDS belongs immediately after GCOUNT.
1696 if matches!(
1697 extension,
1698 Some(ExtensionType::BinTable | ExtensionType::AsciiTable)
1699 ) {
1700 mandatory.push(Card::TableFields {
1701 value: self.table_fields().unwrap_or(0),
1702 comment: self.comment_for(card_keys::TFIELDS),
1703 });
1704 }
1705
1706 let placed: Vec<String> = mandatory.iter().map(Card::key).collect();
1707
1708 // Everything else keeps the order it already had, minus the cards that
1709 // have just been placed at the front and any END, which `to_bytes` adds.
1710 let rest = self
1711 .cards
1712 .iter()
1713 .filter(|card| **card != Card::End && !placed.contains(&card.key()));
1714
1715 Self {
1716 cards: mandatory.iter().cloned().chain(rest.cloned()).collect(),
1717 // A header being written out is measured by what it writes.
1718 bytes_in_file: None,
1719 }
1720 }
1721
1722 /// The comment on the existing card for `key`, so that rewriting a header
1723 /// does not throw away what its cards said about themselves.
1724 fn comment_for(&self, key: &str) -> Option<String> {
1725 self.cards
1726 .iter()
1727 .find(|card| card.key() == key)
1728 .and_then(|card| match Value::from(card) {
1729 Value::Integer { comment, .. }
1730 | Value::Float { comment, .. }
1731 | Value::Logical { comment, .. }
1732 | Value::String { comment, .. } => comment,
1733 _ => None,
1734 })
1735 }
1736
1737 /// Replaces the card for `key`, or adds it before the END card.
1738 ///
1739 /// Writing an image means bringing BITPIX and the NAXISn cards into line
1740 /// with the data, and those cards are already there in a header that was
1741 /// read from a file.
1742 pub(crate) fn set(&mut self, card: Card) {
1743 let key = card.key();
1744
1745 if let Some(existing) = self.cards.iter_mut().find(|existing| existing.key() == key) {
1746 *existing = card;
1747 return;
1748 }
1749
1750 match self.cards.iter().position(|card| card == &Card::End) {
1751 Some(end) => self.cards.insert(end, card),
1752 None => self.cards.push(card),
1753 }
1754 }
1755
1756 /// Removes every indexed card whose keyword starts with `prefix`, such as
1757 /// every TFORMn.
1758 ///
1759 /// The index has to be there: `NAXIS` is not one of the `NAXISn` cards, and
1760 /// removing it along with them would leave a header that no longer says how
1761 /// many axes it has.
1762 pub(crate) fn remove_prefixed(&mut self, prefix: &str) {
1763 self.cards.retain(|card| {
1764 let key = card.key();
1765 let Some(index) = key.strip_prefix(prefix) else {
1766 return true;
1767 };
1768
1769 !(!index.is_empty() && index.chars().all(|c| c.is_ascii_digit()))
1770 });
1771 }
1772
1773 /// Every card with the keyword `key`, as raw values.
1774 ///
1775 /// Most keywords appear once, but COMMENT and HISTORY may repeat, and an
1776 /// unrecognised keyword can appear as often as the writer liked.
1777 pub fn raw_card(&self, key: &str) -> Vec<Value> {
1778 self.cards
1779 .iter()
1780 .filter_map(|card| {
1781 if key == card.key() {
1782 Some(Value::from(card))
1783 } else {
1784 None
1785 }
1786 })
1787 .collect()
1788 }
1789
1790 /// The value of the card with the keyword `key`, if there is one.
1791 ///
1792 /// Where a keyword repeats — COMMENT and HISTORY do, and an unrecognised one
1793 /// may — this is the first of them; [`Header::raw_card`] returns them all.
1794 ///
1795 /// ```
1796 /// # use fits_io::header::Header;
1797 /// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1798 /// let mut header = Header::default();
1799 /// header.set_card("OBJECT", "M31")?;
1800 ///
1801 /// assert_eq!(header.card("OBJECT").map(|v| v.value_to_string()), Some("M31".into()));
1802 /// assert!(header.card("MISSING").is_none());
1803 /// # Ok(())
1804 /// # }
1805 /// ```
1806 pub fn card(&self, key: &str) -> Option<Value> {
1807 self.cards
1808 .iter()
1809 .find(|card| card.key() == key)
1810 .map(Value::from)
1811 }
1812
1813 /// Whether this header carries a card with the keyword `key`.
1814 pub fn contains_card(&self, key: &str) -> bool {
1815 self.cards.iter().any(|card| card.key() == key)
1816 }
1817
1818 /// Every keyword this header holds, in the order the cards are in.
1819 ///
1820 /// A repeated keyword appears once per card, and the blank keyword of a
1821 /// COMMENT-style card with no keyword appears as an empty string.
1822 pub fn card_keys(&self) -> impl Iterator<Item = String> + '_ {
1823 self.cards
1824 .iter()
1825 .filter(|card| **card != Card::End)
1826 .map(Card::key)
1827 }
1828
1829 /// Sets the card with the keyword `key` to `value`, adding it if the header
1830 /// does not already have one.
1831 ///
1832 /// The value may be any of the types a FITS card can hold — an integer, a
1833 /// float, a bool, a string, or `None` for a keyword written with no value —
1834 /// and [`Value::with_comment`] puts a comment beside it.
1835 ///
1836 /// A keyword of up to eight characters drawn from `A`–`Z`, `0`–`9`, `-` and
1837 /// `_` is written as an ordinary card, upper-cased on the way in. A longer
1838 /// or otherwise unconventional keyword is written with the `HIERARCH`
1839 /// convention, which keeps its case. Setting a keyword this crate reads
1840 /// through one of its typed accessors — `OBJECT`, `DATE-OBS`, `BUNIT` and
1841 /// the rest — leaves that accessor returning the new value.
1842 ///
1843 /// # Errors
1844 ///
1845 /// Returns an error for a keyword that is empty, holds characters a card
1846 /// cannot carry, or is one of the repeatable and structural keywords that
1847 /// have setters of their own: use [`Header::add_comment`],
1848 /// [`Header::add_history`] and [`Header::set_naxis_n`] for those. Also
1849 /// returns an error when the value is too long for the one card it has to
1850 /// fit on — a long *string* is written across CONTINUE cards instead and is
1851 /// never too long.
1852 ///
1853 /// ```
1854 /// # use fits_io::header::{Header, Value};
1855 /// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1856 /// let mut header = Header::default();
1857 ///
1858 /// header.set_card("OBJECT", "NGC 7000")?;
1859 /// header.set_card("EXPTIME", Value::from(120.0).with_comment("seconds"))?;
1860 /// header.set_card("MOONLIT", true)?;
1861 ///
1862 /// // A keyword too long for the eight columns a card gives it becomes a
1863 /// // HIERARCH card.
1864 /// header.set_card("ESO INS FILT1 NAME", "Halpha")?;
1865 ///
1866 /// assert_eq!(header.object(), Some("NGC 7000"));
1867 /// # Ok(())
1868 /// # }
1869 /// ```
1870 pub fn set_card(
1871 &mut self,
1872 key: &str,
1873 value: impl Into<Value>,
1874 ) -> Result<(), Box<dyn Error + Send + Sync>> {
1875 self.set(Self::card_for(key, value.into())?);
1876 Ok(())
1877 }
1878
1879 /// Removes every card with the keyword `key`, and says how many went.
1880 ///
1881 /// A mandatory card removed this way comes back when the header is written:
1882 /// SIMPLE, BITPIX, NAXIS and the rest are filled in from the data whatever
1883 /// the cards say.
1884 pub fn remove_card(&mut self, key: &str) -> usize {
1885 let before = self.cards.len();
1886 self.cards.retain(|card| card.key() != key);
1887 before - self.cards.len()
1888 }
1889
1890 /// Adds a COMMENT card carrying `text`.
1891 ///
1892 /// COMMENT cards repeat, so this always adds one rather than replacing what
1893 /// is there. Text too long for a card is split across as many as it needs.
1894 pub fn add_comment(&mut self, text: impl AsRef<str>) {
1895 for line in comment_lines(text.as_ref()) {
1896 self.push(Card::Comment(line));
1897 }
1898 }
1899
1900 /// Adds a HISTORY card carrying `text`.
1901 ///
1902 /// As with [`Header::add_comment`], this adds rather than replaces, and text
1903 /// too long for one card is split across several.
1904 pub fn add_history(&mut self, text: impl AsRef<str>) {
1905 for line in comment_lines(text.as_ref()) {
1906 self.push(Card::History(line));
1907 }
1908 }
1909
1910 /// The text of every COMMENT card, in order.
1911 pub fn comments(&self) -> impl Iterator<Item = &str> {
1912 self.cards.iter().filter_map(|card| match card {
1913 Card::Comment(text) => Some(text.as_str()),
1914 _ => None,
1915 })
1916 }
1917
1918 /// The text of every HISTORY card, in order.
1919 pub fn history(&self) -> impl Iterator<Item = &str> {
1920 self.cards.iter().filter_map(|card| match card {
1921 Card::History(text) => Some(text.as_str()),
1922 _ => None,
1923 })
1924 }
1925
1926 /// The card `key` and `value` should be written as.
1927 ///
1928 /// A keyword the fixed format can hold becomes an ordinary card, and one it
1929 /// cannot becomes a HIERARCH card.
1930 fn card_for(key: &str, value: Value) -> Result<Card, Box<dyn Error + Send + Sync>> {
1931 const RESERVED: [&str; 5] = [
1932 card_keys::COMMENT,
1933 card_keys::HISTORY,
1934 card_keys::END,
1935 "CONTINUE",
1936 "HIERARCH",
1937 ];
1938
1939 let key = key.trim();
1940
1941 if key.is_empty() {
1942 return Err("A card needs a keyword, and this one is empty".into());
1943 }
1944
1945 let upper = key.to_ascii_uppercase();
1946
1947 if RESERVED.contains(&upper.as_str()) {
1948 return Err(format!(
1949 "{} cards are not set by keyword: use add_comment, add_history, or let the \
1950 header write END and CONTINUE itself",
1951 upper
1952 )
1953 .into());
1954 }
1955
1956 if upper
1957 .strip_prefix(card_keys::PREFIX_NAXIS_N)
1958 .is_some_and(|index| !index.is_empty() && index.chars().all(|c| c.is_ascii_digit()))
1959 {
1960 return Err(format!(
1961 "{} says how much data follows the header, so it is set with set_naxis_n and \
1962 checked against the data",
1963 upper
1964 )
1965 .into());
1966 }
1967
1968 if !key.chars().all(|c| (' '..='~').contains(&c)) {
1969 return Err(format!(
1970 "A FITS keyword holds only printable ASCII, but {:?} does not",
1971 key
1972 )
1973 .into());
1974 }
1975
1976 let conventional = upper.len() <= 8
1977 && upper
1978 .chars()
1979 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '-' || c == '_');
1980
1981 let card = if conventional {
1982 Self::specialised(Card::Value { name: upper, value })
1983 } else {
1984 if key.contains('=') || key.contains('\'') {
1985 return Err(format!(
1986 "A HIERARCH keyword cannot hold a quote or an equals sign, but {:?} does",
1987 key
1988 )
1989 .into());
1990 }
1991
1992 Card::Hierarch {
1993 name: key.to_string(),
1994 value,
1995 }
1996 };
1997
1998 // A string is written across CONTINUE cards when it does not fit, so
1999 // only the cards that have to fit on one are measured. A HIERARCH card
2000 // has no continuation convention and always has to fit.
2001 let continues = matches!(Value::from(&card), Value::String { .. })
2002 && !matches!(card, Card::Hierarch { .. });
2003
2004 if !continues && card.text_len() > CARD_NUM_BYTES {
2005 return Err(format!(
2006 "{} = {} needs {} bytes, and a card holds {}",
2007 card.key(),
2008 Value::from(&card).value_to_string(),
2009 card.text_len(),
2010 CARD_NUM_BYTES
2011 )
2012 .into());
2013 }
2014
2015 Ok(card)
2016 }
2017
2018 /// The typed card for a keyword this crate knows, or the generic card it was
2019 /// given.
2020 ///
2021 /// The typed accessors match on their own variants, so a card set by keyword
2022 /// has to become one of those variants for `header.object()` to see an
2023 /// OBJECT that was set by name. Rendering the card and reading it back is
2024 /// what the file itself would do, so it produces exactly the variant a
2025 /// round trip through a file would. It is only adopted when the value
2026 /// survives that trip — a value the fixed format cannot hold keeps the
2027 /// generic card, which writes it as given.
2028 fn specialised(card: Card) -> Card {
2029 match Card::try_from(&card.to_bytes()) {
2030 Ok(parsed)
2031 if parsed.key() == card.key() && Value::from(&parsed) == Value::from(&card) =>
2032 {
2033 parsed
2034 }
2035 _ => card,
2036 }
2037 }
2038
2039 /// Adds `card` before the END card, keeping any card with the same keyword.
2040 fn push(&mut self, card: Card) {
2041 match self.cards.iter().position(|card| card == &Card::End) {
2042 Some(end) => self.cards.insert(end, card),
2043 None => self.cards.push(card),
2044 }
2045 }
2046
2047 pub(crate) fn from_reader(
2048 reader: &mut Box<dyn ReadSeek>,
2049 ) -> Result<Option<Self>, Box<dyn Error + Send + Sync>> {
2050 let cards = Self::read_all_cards(reader)?;
2051
2052 if let Some(Card::End) = cards.last() {
2053 let bytes_in_file = {
2054 let bytes = cards.len() * CARD_NUM_BYTES;
2055 let over = bytes % BLOCK_NUM_BYTES;
2056 if over == 0 {
2057 bytes
2058 } else {
2059 bytes + BLOCK_NUM_BYTES - over
2060 }
2061 };
2062
2063 Ok(Some(Self {
2064 cards: join_continuations(cards),
2065 bytes_in_file: Some(bytes_in_file),
2066 }))
2067 } else {
2068 Ok(None)
2069 }
2070 }
2071
2072 pub(crate) fn validate_primary(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
2073 if self.simple().is_none() {
2074 return Err("This is not a valid fits file. Card SIMPLE is missing".into());
2075 }
2076 if let Some(false) = self.simple() {
2077 return Err(
2078 "This is not a valid fits file. It must contain card simple with value true".into(),
2079 );
2080 }
2081 self.validate_structure("fits file")?;
2082
2083 Ok(())
2084 }
2085
2086 pub(crate) fn validate_extension(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
2087 if self.extension().is_none() {
2088 return Err("This is not a valid fits extension. Card XTENSION is missing".into());
2089 }
2090 self.validate_structure("fits extension")?;
2091
2092 Ok(())
2093 }
2094
2095 /// Checks the structural cards every HDU must carry: BITPIX, NAXIS and one
2096 /// NAXISn per axis.
2097 ///
2098 /// Callers rely on this: once a header has been validated, [`Header::bitpix`],
2099 /// [`Header::naxis`] and [`Header::naxis_n`] are known to return `Some`, and
2100 /// [`Header::data_bytes_len`] is known to describe the real data section.
2101 fn validate_structure(&self, kind: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
2102 if self.bitpix().is_none() {
2103 return Err(format!("This is not a valid {}. Card BITPIX is missing", kind).into());
2104 }
2105
2106 let Some(number_of_axis) = self.naxis() else {
2107 return Err(format!("This is not a valid {}. Card NAXIS is missing", kind).into());
2108 };
2109
2110 if number_of_axis < 0 {
2111 return Err(format!(
2112 "This is not a valid {}. Card NAXIS must not be negative, but was {}",
2113 kind, number_of_axis
2114 )
2115 .into());
2116 }
2117
2118 for axis in 0..number_of_axis {
2119 let Some(length) = self.naxis_n(axis as usize) else {
2120 return Err(format!(
2121 "This is not a valid {}. NAXIS is {} but card NAXIS{} is missing",
2122 kind,
2123 number_of_axis,
2124 axis + 1
2125 )
2126 .into());
2127 };
2128
2129 if length < 0 {
2130 return Err(format!(
2131 "This is not a valid {}. Card NAXIS{} must not be negative, but was {}",
2132 kind,
2133 axis + 1,
2134 length
2135 )
2136 .into());
2137 }
2138 }
2139
2140 Ok(())
2141 }
2142
2143 fn read_all_cards(
2144 reader: &mut Box<dyn ReadSeek>,
2145 ) -> Result<Vec<Card>, Box<dyn Error + Send + Sync>> {
2146 let mut block = [0_u8; BLOCK_NUM_BYTES];
2147 let mut cards = vec![];
2148
2149 while Self::read_block(reader, &mut block)? {
2150 // `as_chunks` hands back fixed-size arrays, so there is nothing to
2151 // convert and no length to assert.
2152 for card in block.as_chunks::<CARD_NUM_BYTES>().0 {
2153 let card = Card::try_from(card)?;
2154
2155 let is_end = card == Card::End;
2156 cards.push(card);
2157
2158 // Everything after END is padding.
2159 if is_end {
2160 return Ok(cards);
2161 }
2162 }
2163 }
2164
2165 Ok(cards)
2166 }
2167
2168 /// Fills `block` with exactly one 2880-byte FITS block.
2169 ///
2170 /// Returns `false` at a clean end of file. `Read::read` is free to return
2171 /// fewer bytes than asked for even mid-file, so the read is repeated until
2172 /// the block is full; stopping early would misalign every following card.
2173 fn read_block(
2174 reader: &mut Box<dyn ReadSeek>,
2175 block: &mut [u8; BLOCK_NUM_BYTES],
2176 ) -> Result<bool, Box<dyn Error + Send + Sync>> {
2177 let mut filled = 0;
2178
2179 while filled < BLOCK_NUM_BYTES {
2180 match reader.read(&mut block[filled..])? {
2181 0 if filled == 0 => return Ok(false),
2182 0 => {
2183 return Err(format!(
2184 "Truncated FITS header: a block is {} bytes but only {} were left",
2185 BLOCK_NUM_BYTES, filled
2186 )
2187 .into());
2188 }
2189 bytes => filled += bytes,
2190 }
2191 }
2192
2193 Ok(true)
2194 }
2195}
2196
2197impl fmt::Debug for Header {
2198 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2199 writeln!(
2200 f,
2201 "Flexible Image Transport System (FITS) Data Unit Header:"
2202 )?;
2203 for card in &self.cards {
2204 if card != &Card::End {
2205 let value = Value::from(card);
2206 writeln!(
2207 f,
2208 "{: <8} = {: >72} / {}",
2209 card.key(),
2210 value.value_to_string(),
2211 value.comment_to_string()
2212 )?;
2213 } else {
2214 write!(f, "END")?;
2215 }
2216 }
2217
2218 Ok(())
2219 }
2220}