Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/FitsImagePlugin.py: 58%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# The Python Imaging Library
3# $Id$
4#
5# FITS file handling
6#
7# Copyright (c) 1998-2003 by Fredrik Lundh
8#
9# See the README file for information on usage and redistribution.
10#
11from __future__ import annotations
13__lazy_modules__ = {"gzip", "math", "zlib"}
15import gzip
16import math
17import zlib
19from . import Image, ImageFile
22def _accept(prefix: bytes) -> bool:
23 return prefix.startswith(b"SIMPLE")
26class FitsImageFile(ImageFile.ImageFile):
27 format = "FITS"
28 format_description = "FITS"
30 def _open(self) -> None:
31 assert self.fp is not None
33 headers: dict[bytes, bytes] = {}
34 header_in_progress = False
35 decoder_name = ""
36 while True:
37 header = self.fp.read(80)
38 if not header:
39 msg = "Truncated FITS file"
40 raise OSError(msg)
41 keyword = header[:8].strip()
42 if keyword in (b"SIMPLE", b"XTENSION"):
43 header_in_progress = True
44 elif headers and not header_in_progress:
45 # This is now a data unit
46 break
47 elif keyword == b"END":
48 # Seek to the end of the header unit
49 self.fp.seek(math.ceil(self.fp.tell() / 2880) * 2880)
50 if not decoder_name:
51 decoder_name, offset, args = self._parse_headers(headers)
53 header_in_progress = False
54 continue
56 if decoder_name:
57 # Keep going to read past the headers
58 continue
60 value = header[8:].split(b"/")[0].strip()
61 if value.startswith(b"="):
62 value = value[1:].strip()
63 if not headers and (not _accept(keyword) or value != b"T"):
64 msg = "Not a FITS file"
65 raise SyntaxError(msg)
66 headers[keyword] = value
68 if not decoder_name:
69 msg = "No image data"
70 raise ValueError(msg)
72 offset += self.fp.tell() - 80
73 self.tile = [ImageFile._Tile(decoder_name, None, offset, args)]
75 def _get_size(
76 self, headers: dict[bytes, bytes], prefix: bytes
77 ) -> tuple[int, int] | None:
78 naxis = int(headers[prefix + b"NAXIS"])
79 if naxis == 0:
80 return None
82 if naxis == 1:
83 return 1, int(headers[prefix + b"NAXIS1"])
84 else:
85 return int(headers[prefix + b"NAXIS1"]), int(headers[prefix + b"NAXIS2"])
87 def _parse_headers(
88 self, headers: dict[bytes, bytes]
89 ) -> tuple[str, int, tuple[str | int, ...]]:
90 prefix = b""
91 decoder_name = "raw"
92 offset = 0
93 if (
94 headers.get(b"XTENSION") == b"'BINTABLE'"
95 and headers.get(b"ZIMAGE") == b"T"
96 and headers[b"ZCMPTYPE"] == b"'GZIP_1 '"
97 ):
98 no_prefix_size = self._get_size(headers, prefix) or (0, 0)
99 number_of_bits = int(headers[b"BITPIX"])
100 offset = no_prefix_size[0] * no_prefix_size[1] * (number_of_bits // 8)
102 prefix = b"Z"
103 decoder_name = "fits_gzip"
105 size = self._get_size(headers, prefix)
106 if not size:
107 return "", 0, ()
109 self._size = size
111 number_of_bits = int(headers[prefix + b"BITPIX"])
112 if number_of_bits == 8:
113 self._mode = rawmode = "L"
114 elif number_of_bits == 16:
115 self._mode = "I;16"
116 rawmode = "I;16B"
117 elif number_of_bits == 32:
118 self._mode = "I"
119 rawmode = "I;32B"
120 elif number_of_bits == -32 or (decoder_name == "raw" and number_of_bits == -64):
121 self._mode = "F"
122 rawmode = f"F;{number_of_bits * -1}BF"
123 else:
124 msg = "Unsupported number of bits"
125 raise OSError(msg)
127 args: tuple[str | int, ...]
128 if decoder_name == "raw":
129 args = (rawmode, 0, -1)
130 else:
131 args = (number_of_bits,)
132 return decoder_name, offset, args
135class FitsGzipDecoder(ImageFile.PyDecoder):
136 _pulls_fd = True
138 def decode(self, buffer: Image.DecoderInput) -> tuple[int, int]:
139 assert self.fd is not None
140 with gzip.open(self.fd) as fp:
141 try:
142 value = fp.read(self.state.xsize * self.state.ysize * 4)
143 except EOFError as e:
144 msg = "Truncated FITS gzip data"
145 raise OSError(msg) from e
146 except zlib.error as e:
147 msg = "Corrupt FITS gzip data"
148 raise OSError(msg) from e
150 rows = []
151 offset = 0
152 number_of_bytes = abs(self.args[0]) // 8
153 for y in range(self.state.ysize):
154 row = bytearray()
155 for x in range(self.state.xsize):
156 row += value[
157 offset + 4 - 1 : offset + (4 - number_of_bytes) - 1 : -1
158 ]
159 offset += 4
160 rows.append(row)
161 self.set_as_raw(bytes([pixel for row in rows[::-1] for pixel in row]))
162 return -1, 0
165# --------------------------------------------------------------------
166# Registry
168Image.register_open(FitsImageFile.format, FitsImageFile, _accept)
169Image.register_decoder("fits_gzip", FitsGzipDecoder)
171Image.register_extensions(FitsImageFile.format, [".fit", ".fits"])