diff --git a/Tests/images/hopper.fits b/Tests/images/hopper.fits index 85afa4ac167..7f28f75e5a8 100644 Binary files a/Tests/images/hopper.fits and b/Tests/images/hopper.fits differ diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py new file mode 100644 index 00000000000..447888acd8d --- /dev/null +++ b/Tests/test_file_fits.py @@ -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, + ) diff --git a/Tests/test_file_fitsstub.py b/Tests/test_file_fitsstub.py deleted file mode 100644 index c77457947ef..00000000000 --- a/Tests/test_file_fitsstub.py +++ /dev/null @@ -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) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 2349300563b..0b82e41851c 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -133,6 +133,15 @@ Deprecated Use instead ``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 ---------------- diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 04966d5dfad..d3053a34c95 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1064,6 +1064,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 :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 ^^^^^^^^ @@ -1354,16 +1361,6 @@ Pillow provides a stub driver for BUFR files. To add read or write support to your application, use :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 ^^^^ diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 5c833a35f22..fcf4514a84d 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -41,10 +41,10 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.FitsStubImagePlugin` Module +:mod:`~PIL.FitsImagePlugin` Module -------------------------------------- -.. automodule:: PIL.FitsStubImagePlugin +.. automodule:: PIL.FitsImagePlugin :members: :undoc-members: :show-inheritance: diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 5389aae1707..17574fdca24 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -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(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 ============= diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py new file mode 100644 index 00000000000..c16300efa89 --- /dev/null +++ b/src/PIL/FitsImagePlugin.py @@ -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"]) diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py index a3a94cf4b21..9eed02999e2 100644 --- a/src/PIL/FitsStubImagePlugin.py +++ b/src/PIL/FitsStubImagePlugin.py @@ -9,7 +9,9 @@ # See the README file for information on usage and redistribution. # -from . import Image, ImageFile +import warnings + +from . import FitsImagePlugin, Image, ImageFile _handler = None @@ -23,57 +25,37 @@ def register_handler(handler): global _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, + ) -# -------------------------------------------------------------------- -# Image adapter - + # Override FitsImagePlugin with this handler + # for backwards compatibility + try: + Image.ID.remove(FITSStubImageFile.format) + except ValueError: + pass -def _accept(prefix): - return prefix[:6] == b"SIMPLE" + Image.register_open( + FITSStubImageFile.format, FITSStubImageFile, FitsImagePlugin._accept + ) class FITSStubImageFile(ImageFile.StubImageFile): - format = "FITS" - format_description = "FITS" + format = FitsImagePlugin.FitsImageFile.format + format_description = FitsImagePlugin.FitsImageFile.format_description def _open(self): offset = self.fp.tell() - 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" + im = FitsImagePlugin.FitsImageFile(self.fp) + self._size = im.size + self.mode = im.mode + self.tile = [] self.fp.seek(offset) @@ -86,15 +68,10 @@ def _load(self): def _save(im, fp, filename): - if _handler is None or not hasattr("_handler", "save"): - raise OSError("FITS save handler not installed") - _handler.save(im, fp, filename) + raise OSError("FITS save handler not installed") # -------------------------------------------------------------------- # Registry -Image.register_open(FITSStubImageFile.format, FITSStubImageFile, _accept) Image.register_save(FITSStubImageFile.format, _save) - -Image.register_extensions(FITSStubImageFile.format, [".fit", ".fits"]) diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 45fef241ee0..e65b155b2dc 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -30,6 +30,7 @@ "DcxImagePlugin", "DdsImagePlugin", "EpsImagePlugin", + "FitsImagePlugin", "FitsStubImagePlugin", "FliImagePlugin", "FpxImagePlugin",