1# Licensed under a 3-clause BSD style license - see PYFITS.rst
2
3import gzip
4import io
5
6from astropy.io.fits.file import _File
7from .base import NonstandardExtHDU
8from .hdulist import HDUList
9from astropy.io.fits.header import Header, _pad_length
10from astropy.io.fits.util import fileobj_name
11
12from astropy.utils import lazyproperty
13
14
15class FitsHDU(NonstandardExtHDU):
16    """
17    A non-standard extension HDU for encapsulating entire FITS files within a
18    single HDU of a container FITS file.  These HDUs have an extension (that is
19    an XTENSION keyword) of FITS.
20
21    The FITS file contained in the HDU's data can be accessed by the `hdulist`
22    attribute which returns the contained FITS file as an `HDUList` object.
23    """
24
25    _extension = 'FITS'
26
27    @lazyproperty
28    def hdulist(self):
29        self._file.seek(self._data_offset)
30        fileobj = io.BytesIO()
31        # Read the data into a BytesIO--reading directly from the file
32        # won't work (at least for gzipped files) due to problems deep
33        # within the gzip module that make it difficult to read gzip files
34        # embedded in another file
35        fileobj.write(self._file.read(self.size))
36        fileobj.seek(0)
37        if self._header['COMPRESS']:
38            fileobj = gzip.GzipFile(fileobj=fileobj)
39        return HDUList.fromfile(fileobj, mode='readonly')
40
41    @classmethod
42    def fromfile(cls, filename, compress=False):
43        """
44        Like `FitsHDU.fromhdulist()`, but creates a FitsHDU from a file on
45        disk.
46
47        Parameters
48        ----------
49        filename : str
50            The path to the file to read into a FitsHDU
51        compress : bool, optional
52            Gzip compress the FITS file
53        """
54
55        with HDUList.fromfile(filename) as hdulist:
56            return cls.fromhdulist(hdulist, compress=compress)
57
58    @classmethod
59    def fromhdulist(cls, hdulist, compress=False):
60        """
61        Creates a new FitsHDU from a given HDUList object.
62
63        Parameters
64        ----------
65        hdulist : HDUList
66            A valid Headerlet object.
67        compress : bool, optional
68            Gzip compress the FITS file
69        """
70
71        fileobj = bs = io.BytesIO()
72        if compress:
73            if hasattr(hdulist, '_file'):
74                name = fileobj_name(hdulist._file)
75            else:
76                name = None
77            fileobj = gzip.GzipFile(name, mode='wb', fileobj=bs)
78
79        hdulist.writeto(fileobj)
80
81        if compress:
82            fileobj.close()
83
84        # A proper HDUList should still be padded out to a multiple of 2880
85        # technically speaking
86        padding = (_pad_length(bs.tell()) * cls._padding_byte).encode('ascii')
87        bs.write(padding)
88
89        bs.seek(0)
90
91        cards = [
92            ('XTENSION', cls._extension, 'FITS extension'),
93            ('BITPIX', 8, 'array data type'),
94            ('NAXIS', 1, 'number of array dimensions'),
95            ('NAXIS1', len(bs.getvalue()), 'Axis length'),
96            ('PCOUNT', 0, 'number of parameters'),
97            ('GCOUNT', 1, 'number of groups'),
98        ]
99
100        # Add the XINDn keywords proposed by Perry, though nothing is done with
101        # these at the moment
102        if len(hdulist) > 1:
103            for idx, hdu in enumerate(hdulist[1:]):
104                cards.append(('XIND' + str(idx + 1), hdu._header_offset,
105                              f'byte offset of extension {idx + 1}'))
106
107        cards.append(('COMPRESS', compress, 'Uses gzip compression'))
108        header = Header(cards)
109        return cls._readfrom_internal(_File(bs), header=header)
110
111    @classmethod
112    def match_header(cls, header):
113        card = header.cards[0]
114        if card.keyword != 'XTENSION':
115            return False
116        xtension = card.value
117        if isinstance(xtension, str):
118            xtension = xtension.rstrip()
119        return xtension == cls._extension
120
121    # TODO: Add header verification
122
123    def _summary(self):
124        # TODO: Perhaps make this more descriptive...
125        return (self.name, self.ver, self.__class__.__name__, len(self._header))
126