Merge pull request #6056 from radarhere/fits

Added FITS reading
This commit is contained in:
mergify[bot] 2022-02-20 21:49:01 +00:00 committed by GitHub
commit c58d2817bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 204 additions and 123 deletions

Binary file not shown.

80
Tests/test_file_fits.py Normal file
View File

@ -0,0 +1,80 @@
from io import BytesIO
import pytest
from PIL import FitsImagePlugin, FitsStubImagePlugin, Image
from .helper import assert_image_equal, hopper
TEST_FILE = "Tests/images/hopper.fits"
def test_open():
# Act
with Image.open(TEST_FILE) as im:
# Assert
assert im.format == "FITS"
assert im.size == (128, 128)
assert im.mode == "L"
assert_image_equal(im, hopper("L"))
def test_invalid_file():
# Arrange
invalid_file = "Tests/images/flower.jpg"
# Act / Assert
with pytest.raises(SyntaxError):
FitsImagePlugin.FitsImageFile(invalid_file)
def test_truncated_fits():
# No END to headers
image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE"
with pytest.raises(OSError):
FitsImagePlugin.FitsImageFile(BytesIO(image_data))
def test_naxis_zero():
# This test image has been manually hexedited
# to set the number of data axes to zero
with pytest.raises(ValueError):
with Image.open("Tests/images/hopper_naxis_zero.fits"):
pass
def test_stub_deprecated():
class Handler:
opened = False
loaded = False
def open(self, im):
self.opened = True
def load(self, im):
self.loaded = True
return Image.new("RGB", (1, 1))
handler = Handler()
with pytest.warns(DeprecationWarning):
FitsStubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im:
assert im.format == "FITS"
assert im.size == (128, 128)
assert im.mode == "L"
assert handler.opened
assert not handler.loaded
im.load()
assert handler.loaded
FitsStubImagePlugin._handler = None
Image.register_open(
FitsImagePlugin.FitsImageFile.format,
FitsImagePlugin.FitsImageFile,
FitsImagePlugin._accept,
)

View File

@ -1,63 +0,0 @@
from io import BytesIO
import pytest
from PIL import FitsStubImagePlugin, Image
TEST_FILE = "Tests/images/hopper.fits"
def test_open():
# Act
with Image.open(TEST_FILE) as im:
# Assert
assert im.format == "FITS"
assert im.size == (128, 128)
assert im.mode == "L"
def test_invalid_file():
# Arrange
invalid_file = "Tests/images/flower.jpg"
# Act / Assert
with pytest.raises(SyntaxError):
FitsStubImagePlugin.FITSStubImageFile(invalid_file)
def test_load():
# Arrange
with Image.open(TEST_FILE) as im:
# Act / Assert: stub cannot load without an implemented handler
with pytest.raises(OSError):
im.load()
def test_truncated_fits():
# No END to headers
image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE"
with pytest.raises(OSError):
FitsStubImagePlugin.FITSStubImageFile(BytesIO(image_data))
def test_naxis_zero():
# This test image has been manually hexedited
# to set the number of data axes to zero
with pytest.raises(ValueError):
with Image.open("Tests/images/hopper_naxis_zero.fits"):
pass
def test_save():
# Arrange
with Image.open(TEST_FILE) as im:
dummy_fp = None
dummy_filename = "dummy.filename"
# Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError):
im.save(dummy_filename)
with pytest.raises(OSError):
FitsStubImagePlugin._save(im, dummy_fp, dummy_filename)

View File

@ -133,6 +133,15 @@ Deprecated Use instead
``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` ``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER``
===================================================== ============================================================ ===================================================== ============================================================
FitsStubImagePlugin
~~~~~~~~~~~~~~~~~~~
.. deprecated:: 9.1.0
The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
:mod:`~PIL.FitsImagePlugin` instead.
Removed features Removed features
---------------- ----------------

View File

@ -1065,6 +1065,13 @@ is commonly used in fax applications. The DCX decoder can read files containing
When the file is opened, only the first image is read. You can use When the file is opened, only the first image is read. You can use
:py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images. :py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images.
FITS
^^^^
.. versionadded:: 9.1.0
Pillow identifies and reads FITS files, commonly used for astronomy.
FLI, FLC FLI, FLC
^^^^^^^^ ^^^^^^^^
@ -1355,16 +1362,6 @@ Pillow provides a stub driver for BUFR files.
To add read or write support to your application, use To add read or write support to your application, use
:py:func:`PIL.BufrStubImagePlugin.register_handler`. :py:func:`PIL.BufrStubImagePlugin.register_handler`.
FITS
^^^^
.. versionadded:: 1.1.5
Pillow provides a stub driver for FITS files.
To add read or write support to your application, use
:py:func:`PIL.FitsStubImagePlugin.register_handler`.
GRIB GRIB
^^^^ ^^^^

View File

@ -41,10 +41,10 @@ Plugin reference
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:mod:`~PIL.FitsStubImagePlugin` Module :mod:`~PIL.FitsImagePlugin` Module
-------------------------------------- --------------------------------------
.. automodule:: PIL.FitsStubImagePlugin .. automodule:: PIL.FitsImagePlugin
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View File

@ -97,6 +97,15 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest ``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest
``viewer.show_file(path="test.jpg")`` instead. ``viewer.show_file(path="test.jpg")`` instead.
FitsStubImagePlugin
~~~~~~~~~~~~~~~~~~~
.. deprecated:: 9.1.0
The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
:mod:`~PIL.FitsImagePlugin` instead.
API Additions API Additions
============= =============

View File

@ -0,0 +1,71 @@
#
# The Python Imaging Library
# $Id$
#
# FITS file handling
#
# Copyright (c) 1998-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
import math
from . import Image, ImageFile
def _accept(prefix):
return prefix[:6] == b"SIMPLE"
class FitsImageFile(ImageFile.ImageFile):
format = "FITS"
format_description = "FITS"
def _open(self):
headers = {}
while True:
header = self.fp.read(80)
if not header:
raise OSError("Truncated FITS file")
keyword = header[:8].strip()
if keyword == b"END":
break
value = header[8:].strip()
if value.startswith(b"="):
value = value[1:].strip()
if not headers and (not _accept(keyword) or value != b"T"):
raise SyntaxError("Not a FITS file")
headers[keyword] = value
naxis = int(headers[b"NAXIS"])
if naxis == 0:
raise ValueError("No image data")
elif naxis == 1:
self._size = 1, int(headers[b"NAXIS1"])
else:
self._size = int(headers[b"NAXIS1"]), int(headers[b"NAXIS2"])
number_of_bits = int(headers[b"BITPIX"])
if number_of_bits == 8:
self.mode = "L"
elif number_of_bits == 16:
self.mode = "I"
# rawmode = "I;16S"
elif number_of_bits == 32:
self.mode = "I"
elif number_of_bits in (-32, -64):
self.mode = "F"
# rawmode = "F" if number_of_bits == -32 else "F;64F"
offset = math.ceil(self.fp.tell() / 2880) * 2880
self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))]
# --------------------------------------------------------------------
# Registry
Image.register_open(FitsImageFile.format, FitsImageFile, _accept)
Image.register_extensions(FitsImageFile.format, [".fit", ".fits"])

View File

@ -9,7 +9,9 @@
# See the README file for information on usage and redistribution. # See the README file for information on usage and redistribution.
# #
from . import Image, ImageFile import warnings
from . import FitsImagePlugin, Image, ImageFile
_handler = None _handler = None
@ -23,57 +25,37 @@ def register_handler(handler):
global _handler global _handler
_handler = handler _handler = handler
warnings.warn(
"FitsStubImagePlugin is deprecated and will be removed in Pillow "
"10 (2023-07-01). FITS images can now be read without a handler through "
"FitsImagePlugin instead.",
DeprecationWarning,
)
# -------------------------------------------------------------------- # Override FitsImagePlugin with this handler
# Image adapter # for backwards compatibility
try:
Image.ID.remove(FITSStubImageFile.format)
except ValueError:
pass
Image.register_open(
def _accept(prefix): FITSStubImageFile.format, FITSStubImageFile, FitsImagePlugin._accept
return prefix[:6] == b"SIMPLE" )
class FITSStubImageFile(ImageFile.StubImageFile): class FITSStubImageFile(ImageFile.StubImageFile):
format = "FITS" format = FitsImagePlugin.FitsImageFile.format
format_description = "FITS" format_description = FitsImagePlugin.FitsImageFile.format_description
def _open(self): def _open(self):
offset = self.fp.tell() offset = self.fp.tell()
headers = {} im = FitsImagePlugin.FitsImageFile(self.fp)
while True: self._size = im.size
header = self.fp.read(80) self.mode = im.mode
if not header: self.tile = []
raise OSError("Truncated FITS file")
keyword = header[:8].strip()
if keyword == b"END":
break
value = header[8:].strip()
if value.startswith(b"="):
value = value[1:].strip()
if not headers and (not _accept(keyword) or value != b"T"):
raise SyntaxError("Not a FITS file")
headers[keyword] = value
naxis = int(headers[b"NAXIS"])
if naxis == 0:
raise ValueError("No image data")
elif naxis == 1:
self._size = 1, int(headers[b"NAXIS1"])
else:
self._size = int(headers[b"NAXIS1"]), int(headers[b"NAXIS2"])
number_of_bits = int(headers[b"BITPIX"])
if number_of_bits == 8:
self.mode = "L"
elif number_of_bits == 16:
self.mode = "I"
# rawmode = "I;16S"
elif number_of_bits == 32:
self.mode = "I"
elif number_of_bits in (-32, -64):
self.mode = "F"
# rawmode = "F" if number_of_bits == -32 else "F;64F"
self.fp.seek(offset) self.fp.seek(offset)
@ -86,15 +68,10 @@ class FITSStubImageFile(ImageFile.StubImageFile):
def _save(im, fp, filename): def _save(im, fp, filename):
if _handler is None or not hasattr("_handler", "save"): raise OSError("FITS save handler not installed")
raise OSError("FITS save handler not installed")
_handler.save(im, fp, filename)
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Registry # Registry
Image.register_open(FITSStubImageFile.format, FITSStubImageFile, _accept)
Image.register_save(FITSStubImageFile.format, _save) Image.register_save(FITSStubImageFile.format, _save)
Image.register_extensions(FITSStubImageFile.format, [".fit", ".fits"])

View File

@ -30,6 +30,7 @@ _plugins = [
"DcxImagePlugin", "DcxImagePlugin",
"DdsImagePlugin", "DdsImagePlugin",
"EpsImagePlugin", "EpsImagePlugin",
"FitsImagePlugin",
"FitsStubImagePlugin", "FitsStubImagePlugin",
"FliImagePlugin", "FliImagePlugin",
"FpxImagePlugin", "FpxImagePlugin",