FitsImagePlugin.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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. #
  11. from __future__ import annotations
  12. import gzip
  13. import math
  14. from . import Image, ImageFile
  15. def _accept(prefix: bytes) -> bool:
  16. return prefix[:6] == b"SIMPLE"
  17. class FitsImageFile(ImageFile.ImageFile):
  18. format = "FITS"
  19. format_description = "FITS"
  20. def _open(self) -> None:
  21. assert self.fp is not None
  22. headers: dict[bytes, bytes] = {}
  23. header_in_progress = False
  24. decoder_name = ""
  25. while True:
  26. header = self.fp.read(80)
  27. if not header:
  28. msg = "Truncated FITS file"
  29. raise OSError(msg)
  30. keyword = header[:8].strip()
  31. if keyword in (b"SIMPLE", b"XTENSION"):
  32. header_in_progress = True
  33. elif headers and not header_in_progress:
  34. # This is now a data unit
  35. break
  36. elif keyword == b"END":
  37. # Seek to the end of the header unit
  38. self.fp.seek(math.ceil(self.fp.tell() / 2880) * 2880)
  39. if not decoder_name:
  40. decoder_name, offset, args = self._parse_headers(headers)
  41. header_in_progress = False
  42. continue
  43. if decoder_name:
  44. # Keep going to read past the headers
  45. continue
  46. value = header[8:].split(b"/")[0].strip()
  47. if value.startswith(b"="):
  48. value = value[1:].strip()
  49. if not headers and (not _accept(keyword) or value != b"T"):
  50. msg = "Not a FITS file"
  51. raise SyntaxError(msg)
  52. headers[keyword] = value
  53. if not decoder_name:
  54. msg = "No image data"
  55. raise ValueError(msg)
  56. offset += self.fp.tell() - 80
  57. self.tile = [ImageFile._Tile(decoder_name, (0, 0) + self.size, offset, args)]
  58. def _get_size(
  59. self, headers: dict[bytes, bytes], prefix: bytes
  60. ) -> tuple[int, int] | None:
  61. naxis = int(headers[prefix + b"NAXIS"])
  62. if naxis == 0:
  63. return None
  64. if naxis == 1:
  65. return 1, int(headers[prefix + b"NAXIS1"])
  66. else:
  67. return int(headers[prefix + b"NAXIS1"]), int(headers[prefix + b"NAXIS2"])
  68. def _parse_headers(
  69. self, headers: dict[bytes, bytes]
  70. ) -> tuple[str, int, tuple[str | int, ...]]:
  71. prefix = b""
  72. decoder_name = "raw"
  73. offset = 0
  74. if (
  75. headers.get(b"XTENSION") == b"'BINTABLE'"
  76. and headers.get(b"ZIMAGE") == b"T"
  77. and headers[b"ZCMPTYPE"] == b"'GZIP_1 '"
  78. ):
  79. no_prefix_size = self._get_size(headers, prefix) or (0, 0)
  80. number_of_bits = int(headers[b"BITPIX"])
  81. offset = no_prefix_size[0] * no_prefix_size[1] * (number_of_bits // 8)
  82. prefix = b"Z"
  83. decoder_name = "fits_gzip"
  84. size = self._get_size(headers, prefix)
  85. if not size:
  86. return "", 0, ()
  87. self._size = size
  88. number_of_bits = int(headers[prefix + b"BITPIX"])
  89. if number_of_bits == 8:
  90. self._mode = "L"
  91. elif number_of_bits == 16:
  92. self._mode = "I;16"
  93. elif number_of_bits == 32:
  94. self._mode = "I"
  95. elif number_of_bits in (-32, -64):
  96. self._mode = "F"
  97. args: tuple[str | int, ...]
  98. if decoder_name == "raw":
  99. args = (self.mode, 0, -1)
  100. else:
  101. args = (number_of_bits,)
  102. return decoder_name, offset, args
  103. class FitsGzipDecoder(ImageFile.PyDecoder):
  104. _pulls_fd = True
  105. def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
  106. assert self.fd is not None
  107. value = gzip.decompress(self.fd.read())
  108. rows = []
  109. offset = 0
  110. number_of_bits = min(self.args[0] // 8, 4)
  111. for y in range(self.state.ysize):
  112. row = bytearray()
  113. for x in range(self.state.xsize):
  114. row += value[offset + (4 - number_of_bits) : offset + 4]
  115. offset += 4
  116. rows.append(row)
  117. self.set_as_raw(bytes([pixel for row in rows[::-1] for pixel in row]))
  118. return -1, 0
  119. # --------------------------------------------------------------------
  120. # Registry
  121. Image.register_open(FitsImageFile.format, FitsImageFile, _accept)
  122. Image.register_decoder("fits_gzip", FitsGzipDecoder)
  123. Image.register_extensions(FitsImageFile.format, [".fit", ".fits"])