Skip to content

Commit b1f549f

Browse files
authored
Merge pull request #7696 from nulano/pfm
2 parents de564ab + 586e774 commit b1f549f

File tree

7 files changed

+141
-11
lines changed

7 files changed

+141
-11
lines changed

Tests/images/hopper.pfm

64 KB
Binary file not shown.

Tests/images/hopper_be.pfm

64 KB
Binary file not shown.

Tests/test_file_ppm.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66

77
from PIL import Image, PpmImagePlugin
88

9-
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
9+
from .helper import (
10+
assert_image_equal,
11+
assert_image_equal_tofile,
12+
assert_image_similar,
13+
hopper,
14+
)
1015

1116
# sample ppm stream
1217
TEST_FILE = "Tests/images/hopper.ppm"
@@ -84,20 +89,58 @@ def test_16bit_pgm():
8489

8590
def test_16bit_pgm_write(tmp_path):
8691
with Image.open("Tests/images/16_bit_binary.pgm") as im:
87-
f = str(tmp_path / "temp.pgm")
88-
im.save(f, "PPM")
92+
filename = str(tmp_path / "temp.pgm")
93+
im.save(filename, "PPM")
8994

90-
assert_image_equal_tofile(im, f)
95+
assert_image_equal_tofile(im, filename)
9196

9297

9398
def test_pnm(tmp_path):
9499
with Image.open("Tests/images/hopper.pnm") as im:
95100
assert_image_similar(im, hopper(), 0.0001)
96101

97-
f = str(tmp_path / "temp.pnm")
98-
im.save(f)
102+
filename = str(tmp_path / "temp.pnm")
103+
im.save(filename)
104+
105+
assert_image_equal_tofile(im, filename)
106+
107+
108+
def test_pfm(tmp_path):
109+
with Image.open("Tests/images/hopper.pfm") as im:
110+
assert im.info["scale"] == 1.0
111+
assert_image_equal(im, hopper("F"))
112+
113+
filename = str(tmp_path / "tmp.pfm")
114+
im.save(filename)
115+
116+
assert_image_equal_tofile(im, filename)
117+
118+
119+
def test_pfm_big_endian(tmp_path):
120+
with Image.open("Tests/images/hopper_be.pfm") as im:
121+
assert im.info["scale"] == 2.5
122+
assert_image_equal(im, hopper("F"))
99123

100-
assert_image_equal_tofile(im, f)
124+
filename = str(tmp_path / "tmp.pfm")
125+
im.save(filename)
126+
127+
assert_image_equal_tofile(im, filename)
128+
129+
130+
@pytest.mark.parametrize(
131+
"data",
132+
[
133+
b"Pf 1 1 NaN \0\0\0\0",
134+
b"Pf 1 1 inf \0\0\0\0",
135+
b"Pf 1 1 -inf \0\0\0\0",
136+
b"Pf 1 1 0.0 \0\0\0\0",
137+
b"Pf 1 1 -0.0 \0\0\0\0",
138+
],
139+
)
140+
def test_pfm_invalid(data):
141+
with pytest.raises(ValueError):
142+
with Image.open(BytesIO(data)):
143+
pass
101144

102145

103146
@pytest.mark.parametrize(

docs/handbook/image-file-formats.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,25 @@ PCX
696696

697697
Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data.
698698

699+
PFM
700+
^^^
701+
702+
.. versionadded:: 10.3.0
703+
704+
Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files
705+
containing ``F`` data.
706+
707+
Color (PF format) PFM files are not supported.
708+
709+
Opening
710+
~~~~~~~
711+
712+
The :py:func:`~PIL.Image.open` function sets the following
713+
:py:attr:`~PIL.Image.Image.info` properties:
714+
715+
**scale**
716+
The absolute value of the number stored in the *Scale Factor / Endianness* line.
717+
699718
PNG
700719
^^^
701720

docs/releasenotes/10.3.0.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
10.3.0
2+
------
3+
4+
Backwards Incompatible Changes
5+
==============================
6+
7+
TODO
8+
^^^^
9+
10+
Deprecations
11+
============
12+
13+
TODO
14+
^^^^
15+
16+
TODO
17+
18+
API Changes
19+
===========
20+
21+
TODO
22+
^^^^
23+
24+
TODO
25+
26+
API Additions
27+
=============
28+
29+
TODO
30+
^^^^
31+
32+
TODO
33+
34+
Security
35+
========
36+
37+
TODO
38+
^^^^
39+
40+
TODO
41+
42+
Other Changes
43+
=============
44+
45+
Portable FloatMap (PFM) images
46+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
47+
48+
Support has been added for reading and writing grayscale (Pf format)
49+
Portable FloatMap (PFM) files containing ``F`` data.

docs/releasenotes/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ expected to be backported to earlier versions.
1414
.. toctree::
1515
:maxdepth: 2
1616

17+
10.3.0
1718
10.2.0
1819
10.1.0
1920
10.0.1

src/PIL/PpmImagePlugin.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
#
1616
from __future__ import annotations
1717

18+
import math
19+
1820
from . import Image, ImageFile
1921
from ._binary import i16be as i16
2022
from ._binary import o8
@@ -35,6 +37,7 @@
3537
b"P6": "RGB",
3638
# extensions
3739
b"P0CMYK": "CMYK",
40+
b"Pf": "F",
3841
# PIL extensions (for test purposes only)
3942
b"PyP": "P",
4043
b"PyRGBA": "RGBA",
@@ -43,7 +46,7 @@
4346

4447

4548
def _accept(prefix):
46-
return prefix[0:1] == b"P" and prefix[1] in b"0123456y"
49+
return prefix[0:1] == b"P" and prefix[1] in b"0123456fy"
4750

4851

4952
##
@@ -110,6 +113,14 @@ def _open(self):
110113
if magic_number in (b"P1", b"P2", b"P3"):
111114
decoder_name = "ppm_plain"
112115
for ix in range(3):
116+
if mode == "F" and ix == 2:
117+
scale = float(self._read_token())
118+
if scale == 0.0 or not math.isfinite(scale):
119+
msg = "scale must be finite and non-zero"
120+
raise ValueError(msg)
121+
rawmode = "F;32F" if scale < 0 else "F;32BF"
122+
self.info["scale"] = abs(scale)
123+
continue
113124
token = int(self._read_token())
114125
if ix == 0: # token is the x size
115126
xsize = token
@@ -136,7 +147,8 @@ def _open(self):
136147
elif maxval != 255:
137148
decoder_name = "ppm"
138149

139-
args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval)
150+
row_order = -1 if mode == "F" else 1
151+
args = (rawmode, 0, row_order) if decoder_name == "raw" else (rawmode, maxval)
140152
self._size = xsize, ysize
141153
self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)]
142154

@@ -307,6 +319,7 @@ def decode(self, buffer):
307319

308320

309321
def _save(im, fp, filename):
322+
row_order = 1
310323
if im.mode == "1":
311324
rawmode, head = "1;I", b"P4"
312325
elif im.mode == "L":
@@ -315,6 +328,9 @@ def _save(im, fp, filename):
315328
rawmode, head = "I;16B", b"P5"
316329
elif im.mode in ("RGB", "RGBA"):
317330
rawmode, head = "RGB", b"P6"
331+
elif im.mode == "F":
332+
rawmode, head = "F;32F", b"Pf"
333+
row_order = -1
318334
else:
319335
msg = f"cannot write mode {im.mode} as PPM"
320336
raise OSError(msg)
@@ -326,7 +342,9 @@ def _save(im, fp, filename):
326342
fp.write(b"255\n")
327343
else:
328344
fp.write(b"65535\n")
329-
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
345+
elif head == b"Pf":
346+
fp.write(b"-1.0\n")
347+
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))])
330348

331349

332350
#
@@ -339,6 +357,6 @@ def _save(im, fp, filename):
339357
Image.register_decoder("ppm", PpmDecoder)
340358
Image.register_decoder("ppm_plain", PpmPlainDecoder)
341359

342-
Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"])
360+
Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"])
343361

344362
Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")

0 commit comments

Comments
 (0)