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