Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added FITS reading, deprecate FitsStubImagePlugin #6056

Merged
merged 6 commits into from Feb 20, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
radarhere marked this conversation as resolved.
Show resolved Hide resolved
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
FitsImagePlugin instead.
radarhere marked this conversation as resolved.
Show resolved Hide resolved

Removed features
----------------

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

radarhere marked this conversation as resolved.
Show resolved Hide resolved
.. versionadded:: 9.1.0

Pillow identifies and reads FITS files, commonly used for astronomy.

FLI, FLC
^^^^^^^^

Expand Down Expand Up @@ -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
^^^^

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
radarhere marked this conversation as resolved.
Show resolved Hide resolved
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
FitsImagePlugin instead.
radarhere marked this conversation as resolved.
Show resolved Hide resolved

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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation mentioned that the offset for the image data was a multiple of 2880.

https://fits.gsfc.nasa.gov/fits_primer.html

Each header or data unit is a multiple of 2880 bytes long. If necessary, the header or data unit is padded out to the required length with ASCII blanks or NULLs depending on the type of unit.

The data unit, if present, immediately follows the last 2880-byte block in the header unit.

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"])