Skip to content

Commit

Permalink
Merge pull request #6056 from radarhere/fits
Browse files Browse the repository at this point in the history
Added FITS reading
  • Loading branch information
mergify[bot] committed Feb 20, 2022
2 parents b803b7c + ee46ef2 commit c58d281
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 123 deletions.
Binary file modified Tests/images/hopper.fits
Binary file not shown.
80 changes: 80 additions & 0 deletions 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,
)
63 changes: 0 additions & 63 deletions Tests/test_file_fitsstub.py

This file was deleted.

9 changes: 9 additions & 0 deletions docs/deprecations.rst
Expand Up @@ -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
----------------

Expand Down
17 changes: 7 additions & 10 deletions docs/handbook/image-file-formats.rst
Expand Up @@ -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
: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
^^^^^^^^

Expand Down Expand Up @@ -1355,16 +1362,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
^^^^

Expand Down
4 changes: 2 additions & 2 deletions docs/reference/plugins.rst
Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions docs/releasenotes/9.1.0.rst
Expand Up @@ -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
=============

Expand Down
71 changes: 71 additions & 0 deletions 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"])
73 changes: 25 additions & 48 deletions src/PIL/FitsStubImagePlugin.py
Expand Up @@ -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

Expand All @@ -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)

Expand All @@ -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"])

0 comments on commit c58d281

Please sign in to comment.