Skip to content

Commit c58d281

Browse files
authored
Merge pull request #6056 from radarhere/fits
Added FITS reading
2 parents b803b7c + ee46ef2 commit c58d281

File tree

10 files changed

+204
-123
lines changed

10 files changed

+204
-123
lines changed

Tests/images/hopper.fits

-33.8 KB
Binary file not shown.

Tests/test_file_fits.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from io import BytesIO
2+
3+
import pytest
4+
5+
from PIL import FitsImagePlugin, FitsStubImagePlugin, Image
6+
7+
from .helper import assert_image_equal, hopper
8+
9+
TEST_FILE = "Tests/images/hopper.fits"
10+
11+
12+
def test_open():
13+
# Act
14+
with Image.open(TEST_FILE) as im:
15+
16+
# Assert
17+
assert im.format == "FITS"
18+
assert im.size == (128, 128)
19+
assert im.mode == "L"
20+
21+
assert_image_equal(im, hopper("L"))
22+
23+
24+
def test_invalid_file():
25+
# Arrange
26+
invalid_file = "Tests/images/flower.jpg"
27+
28+
# Act / Assert
29+
with pytest.raises(SyntaxError):
30+
FitsImagePlugin.FitsImageFile(invalid_file)
31+
32+
33+
def test_truncated_fits():
34+
# No END to headers
35+
image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE"
36+
with pytest.raises(OSError):
37+
FitsImagePlugin.FitsImageFile(BytesIO(image_data))
38+
39+
40+
def test_naxis_zero():
41+
# This test image has been manually hexedited
42+
# to set the number of data axes to zero
43+
with pytest.raises(ValueError):
44+
with Image.open("Tests/images/hopper_naxis_zero.fits"):
45+
pass
46+
47+
48+
def test_stub_deprecated():
49+
class Handler:
50+
opened = False
51+
loaded = False
52+
53+
def open(self, im):
54+
self.opened = True
55+
56+
def load(self, im):
57+
self.loaded = True
58+
return Image.new("RGB", (1, 1))
59+
60+
handler = Handler()
61+
with pytest.warns(DeprecationWarning):
62+
FitsStubImagePlugin.register_handler(handler)
63+
64+
with Image.open(TEST_FILE) as im:
65+
assert im.format == "FITS"
66+
assert im.size == (128, 128)
67+
assert im.mode == "L"
68+
69+
assert handler.opened
70+
assert not handler.loaded
71+
72+
im.load()
73+
assert handler.loaded
74+
75+
FitsStubImagePlugin._handler = None
76+
Image.register_open(
77+
FitsImagePlugin.FitsImageFile.format,
78+
FitsImagePlugin.FitsImageFile,
79+
FitsImagePlugin._accept,
80+
)

Tests/test_file_fitsstub.py

Lines changed: 0 additions & 63 deletions
This file was deleted.

docs/deprecations.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,15 @@ Deprecated Use instead
133133
``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER``
134134
===================================================== ============================================================
135135

136+
FitsStubImagePlugin
137+
~~~~~~~~~~~~~~~~~~~
138+
139+
.. deprecated:: 9.1.0
140+
141+
The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in
142+
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
143+
:mod:`~PIL.FitsImagePlugin` instead.
144+
136145
Removed features
137146
----------------
138147

docs/handbook/image-file-formats.rst

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,13 @@ is commonly used in fax applications. The DCX decoder can read files containing
10651065
When the file is opened, only the first image is read. You can use
10661066
:py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images.
10671067

1068+
FITS
1069+
^^^^
1070+
1071+
.. versionadded:: 9.1.0
1072+
1073+
Pillow identifies and reads FITS files, commonly used for astronomy.
1074+
10681075
FLI, FLC
10691076
^^^^^^^^
10701077

@@ -1355,16 +1362,6 @@ Pillow provides a stub driver for BUFR files.
13551362
To add read or write support to your application, use
13561363
:py:func:`PIL.BufrStubImagePlugin.register_handler`.
13571364

1358-
FITS
1359-
^^^^
1360-
1361-
.. versionadded:: 1.1.5
1362-
1363-
Pillow provides a stub driver for FITS files.
1364-
1365-
To add read or write support to your application, use
1366-
:py:func:`PIL.FitsStubImagePlugin.register_handler`.
1367-
13681365
GRIB
13691366
^^^^
13701367

docs/reference/plugins.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ Plugin reference
4141
:undoc-members:
4242
:show-inheritance:
4343

44-
:mod:`~PIL.FitsStubImagePlugin` Module
44+
:mod:`~PIL.FitsImagePlugin` Module
4545
--------------------------------------
4646

47-
.. automodule:: PIL.FitsStubImagePlugin
47+
.. automodule:: PIL.FitsImagePlugin
4848
:members:
4949
:undoc-members:
5050
:show-inheritance:

docs/releasenotes/9.1.0.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
9797
``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest
9898
``viewer.show_file(path="test.jpg")`` instead.
9999

100+
FitsStubImagePlugin
101+
~~~~~~~~~~~~~~~~~~~
102+
103+
.. deprecated:: 9.1.0
104+
105+
The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in
106+
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
107+
:mod:`~PIL.FitsImagePlugin` instead.
108+
100109
API Additions
101110
=============
102111

src/PIL/FitsImagePlugin.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
12+
import math
13+
14+
from . import Image, ImageFile
15+
16+
17+
def _accept(prefix):
18+
return prefix[:6] == b"SIMPLE"
19+
20+
21+
class FitsImageFile(ImageFile.ImageFile):
22+
23+
format = "FITS"
24+
format_description = "FITS"
25+
26+
def _open(self):
27+
headers = {}
28+
while True:
29+
header = self.fp.read(80)
30+
if not header:
31+
raise OSError("Truncated FITS file")
32+
keyword = header[:8].strip()
33+
if keyword == b"END":
34+
break
35+
value = header[8:].strip()
36+
if value.startswith(b"="):
37+
value = value[1:].strip()
38+
if not headers and (not _accept(keyword) or value != b"T"):
39+
raise SyntaxError("Not a FITS file")
40+
headers[keyword] = value
41+
42+
naxis = int(headers[b"NAXIS"])
43+
if naxis == 0:
44+
raise ValueError("No image data")
45+
elif naxis == 1:
46+
self._size = 1, int(headers[b"NAXIS1"])
47+
else:
48+
self._size = int(headers[b"NAXIS1"]), int(headers[b"NAXIS2"])
49+
50+
number_of_bits = int(headers[b"BITPIX"])
51+
if number_of_bits == 8:
52+
self.mode = "L"
53+
elif number_of_bits == 16:
54+
self.mode = "I"
55+
# rawmode = "I;16S"
56+
elif number_of_bits == 32:
57+
self.mode = "I"
58+
elif number_of_bits in (-32, -64):
59+
self.mode = "F"
60+
# rawmode = "F" if number_of_bits == -32 else "F;64F"
61+
62+
offset = math.ceil(self.fp.tell() / 2880) * 2880
63+
self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))]
64+
65+
66+
# --------------------------------------------------------------------
67+
# Registry
68+
69+
Image.register_open(FitsImageFile.format, FitsImageFile, _accept)
70+
71+
Image.register_extensions(FitsImageFile.format, [".fit", ".fits"])

src/PIL/FitsStubImagePlugin.py

Lines changed: 25 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
# See the README file for information on usage and redistribution.
1010
#
1111

12-
from . import Image, ImageFile
12+
import warnings
13+
14+
from . import FitsImagePlugin, Image, ImageFile
1315

1416
_handler = None
1517

@@ -23,57 +25,37 @@ def register_handler(handler):
2325
global _handler
2426
_handler = handler
2527

28+
warnings.warn(
29+
"FitsStubImagePlugin is deprecated and will be removed in Pillow "
30+
"10 (2023-07-01). FITS images can now be read without a handler through "
31+
"FitsImagePlugin instead.",
32+
DeprecationWarning,
33+
)
2634

27-
# --------------------------------------------------------------------
28-
# Image adapter
29-
35+
# Override FitsImagePlugin with this handler
36+
# for backwards compatibility
37+
try:
38+
Image.ID.remove(FITSStubImageFile.format)
39+
except ValueError:
40+
pass
3041

31-
def _accept(prefix):
32-
return prefix[:6] == b"SIMPLE"
42+
Image.register_open(
43+
FITSStubImageFile.format, FITSStubImageFile, FitsImagePlugin._accept
44+
)
3345

3446

3547
class FITSStubImageFile(ImageFile.StubImageFile):
3648

37-
format = "FITS"
38-
format_description = "FITS"
49+
format = FitsImagePlugin.FitsImageFile.format
50+
format_description = FitsImagePlugin.FitsImageFile.format_description
3951

4052
def _open(self):
4153
offset = self.fp.tell()
4254

43-
headers = {}
44-
while True:
45-
header = self.fp.read(80)
46-
if not header:
47-
raise OSError("Truncated FITS file")
48-
keyword = header[:8].strip()
49-
if keyword == b"END":
50-
break
51-
value = header[8:].strip()
52-
if value.startswith(b"="):
53-
value = value[1:].strip()
54-
if not headers and (not _accept(keyword) or value != b"T"):
55-
raise SyntaxError("Not a FITS file")
56-
headers[keyword] = value
57-
58-
naxis = int(headers[b"NAXIS"])
59-
if naxis == 0:
60-
raise ValueError("No image data")
61-
elif naxis == 1:
62-
self._size = 1, int(headers[b"NAXIS1"])
63-
else:
64-
self._size = int(headers[b"NAXIS1"]), int(headers[b"NAXIS2"])
65-
66-
number_of_bits = int(headers[b"BITPIX"])
67-
if number_of_bits == 8:
68-
self.mode = "L"
69-
elif number_of_bits == 16:
70-
self.mode = "I"
71-
# rawmode = "I;16S"
72-
elif number_of_bits == 32:
73-
self.mode = "I"
74-
elif number_of_bits in (-32, -64):
75-
self.mode = "F"
76-
# rawmode = "F" if number_of_bits == -32 else "F;64F"
55+
im = FitsImagePlugin.FitsImageFile(self.fp)
56+
self._size = im.size
57+
self.mode = im.mode
58+
self.tile = []
7759

7860
self.fp.seek(offset)
7961

@@ -86,15 +68,10 @@ def _load(self):
8668

8769

8870
def _save(im, fp, filename):
89-
if _handler is None or not hasattr("_handler", "save"):
90-
raise OSError("FITS save handler not installed")
91-
_handler.save(im, fp, filename)
71+
raise OSError("FITS save handler not installed")
9272

9373

9474
# --------------------------------------------------------------------
9575
# Registry
9676

97-
Image.register_open(FITSStubImageFile.format, FITSStubImageFile, _accept)
9877
Image.register_save(FITSStubImageFile.format, _save)
99-
100-
Image.register_extensions(FITSStubImageFile.format, [".fit", ".fits"])

0 commit comments

Comments
 (0)