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 Tidelift - Newest PyPI version - Number of PyPI downloads `_ 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")