diff --git a/README.md b/README.md
index 6982676f5..6ca870166 100644
--- a/README.md
+++ b/README.md
@@ -65,10 +65,10 @@ As of 2019, Pillow development is
-
-
`_ and by direct URL access
-eg. https://pypi.org/project/Pillow/1.0/.
+`_ and by direct URL access
+eg. https://pypi.org/project/pillow/1.0/.
diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst
index 8dfe34d95..8ce6f4b9c 100644
--- a/docs/releasenotes/10.3.0.rst
+++ b/docs/releasenotes/10.3.0.rst
@@ -45,11 +45,6 @@ Deprecated Use instead
:py:data:`sys.version_info`, and ``PIL.__version__``
===================================================== ============================================================
-TODO
-^^^^
-
-TODO
-
API Changes
===========
@@ -77,7 +72,8 @@ TODO
Other Changes
=============
-TODO
-^^^^
+Portable FloatMap (PFM) images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-TODO
+Support has been added for reading and writing grayscale (Pf format)
+Portable FloatMap (PFM) files containing ``F`` data.
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index 25dbfa5b0..d43e21e14 100644
--- a/src/PIL/PpmImagePlugin.py
+++ b/src/PIL/PpmImagePlugin.py
@@ -15,6 +15,8 @@
#
from __future__ import annotations
+import math
+
from . import Image, ImageFile
from ._binary import i16be as i16
from ._binary import o8
@@ -35,6 +37,7 @@ MODES = {
b"P6": "RGB",
# extensions
b"P0CMYK": "CMYK",
+ b"Pf": "F",
# PIL extensions (for test purposes only)
b"PyP": "P",
b"PyRGBA": "RGBA",
@@ -43,7 +46,7 @@ MODES = {
def _accept(prefix):
- return prefix[0:1] == b"P" and prefix[1] in b"0123456y"
+ return prefix[0:1] == b"P" and prefix[1] in b"0123456fy"
##
@@ -110,6 +113,14 @@ class PpmImageFile(ImageFile.ImageFile):
if magic_number in (b"P1", b"P2", b"P3"):
decoder_name = "ppm_plain"
for ix in range(3):
+ if mode == "F" and ix == 2:
+ scale = float(self._read_token())
+ if scale == 0.0 or not math.isfinite(scale):
+ msg = "scale must be finite and non-zero"
+ raise ValueError(msg)
+ rawmode = "F;32F" if scale < 0 else "F;32BF"
+ self.info["scale"] = abs(scale)
+ continue
token = int(self._read_token())
if ix == 0: # token is the x size
xsize = token
@@ -136,7 +147,8 @@ class PpmImageFile(ImageFile.ImageFile):
elif maxval != 255:
decoder_name = "ppm"
- args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval)
+ row_order = -1 if mode == "F" else 1
+ args = (rawmode, 0, row_order) if decoder_name == "raw" else (rawmode, maxval)
self._size = xsize, ysize
self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)]
@@ -307,6 +319,7 @@ class PpmDecoder(ImageFile.PyDecoder):
def _save(im, fp, filename):
+ row_order = 1
if im.mode == "1":
rawmode, head = "1;I", b"P4"
elif im.mode == "L":
@@ -315,6 +328,9 @@ def _save(im, fp, filename):
rawmode, head = "I;16B", b"P5"
elif im.mode in ("RGB", "RGBA"):
rawmode, head = "RGB", b"P6"
+ elif im.mode == "F":
+ rawmode, head = "F;32F", b"Pf"
+ row_order = -1
else:
msg = f"cannot write mode {im.mode} as PPM"
raise OSError(msg)
@@ -326,7 +342,9 @@ def _save(im, fp, filename):
fp.write(b"255\n")
else:
fp.write(b"65535\n")
- ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
+ elif head == b"Pf":
+ fp.write(b"-1.0\n")
+ ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))])
#
@@ -339,6 +357,6 @@ Image.register_save(PpmImageFile.format, _save)
Image.register_decoder("ppm", PpmDecoder)
Image.register_decoder("ppm_plain", PpmPlainDecoder)
-Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"])
+Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"])
Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")