Skip to main content

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}