Skip to content

Commit

Permalink
Merge pull request python-pillow#7696 from nulano/pfm
Browse files Browse the repository at this point in the history
  • Loading branch information
hugovk authored Jan 8, 2024
2 parents de564ab + 586e774 commit b1f549f
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 11 deletions.
Binary file added Tests/images/hopper.pfm
Binary file not shown.
Binary file added Tests/images/hopper_be.pfm
Binary file not shown.
57 changes: 50 additions & 7 deletions Tests/test_file_ppm.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

from PIL import Image, PpmImagePlugin

from .helper import assert_image_equal_tofile, assert_image_similar, hopper
from .helper import (
assert_image_equal,
assert_image_equal_tofile,
assert_image_similar,
hopper,
)

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

def test_16bit_pgm_write(tmp_path):
with Image.open("Tests/images/16_bit_binary.pgm") as im:
f = str(tmp_path / "temp.pgm")
im.save(f, "PPM")
filename = str(tmp_path / "temp.pgm")
im.save(filename, "PPM")

assert_image_equal_tofile(im, f)
assert_image_equal_tofile(im, filename)


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

f = str(tmp_path / "temp.pnm")
im.save(f)
filename = str(tmp_path / "temp.pnm")
im.save(filename)

assert_image_equal_tofile(im, filename)


def test_pfm(tmp_path):
with Image.open("Tests/images/hopper.pfm") as im:
assert im.info["scale"] == 1.0
assert_image_equal(im, hopper("F"))

filename = str(tmp_path / "tmp.pfm")
im.save(filename)

assert_image_equal_tofile(im, filename)


def test_pfm_big_endian(tmp_path):
with Image.open("Tests/images/hopper_be.pfm") as im:
assert im.info["scale"] == 2.5
assert_image_equal(im, hopper("F"))

assert_image_equal_tofile(im, f)
filename = str(tmp_path / "tmp.pfm")
im.save(filename)

assert_image_equal_tofile(im, filename)


@pytest.mark.parametrize(
"data",
[
b"Pf 1 1 NaN \0\0\0\0",
b"Pf 1 1 inf \0\0\0\0",
b"Pf 1 1 -inf \0\0\0\0",
b"Pf 1 1 0.0 \0\0\0\0",
b"Pf 1 1 -0.0 \0\0\0\0",
],
)
def test_pfm_invalid(data):
with pytest.raises(ValueError):
with Image.open(BytesIO(data)):
pass


@pytest.mark.parametrize(
Expand Down
19 changes: 19 additions & 0 deletions docs/handbook/image-file-formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,25 @@ PCX

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

PFM
^^^

.. versionadded:: 10.3.0

Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files
containing ``F`` data.

Color (PF format) PFM files are not supported.

Opening
~~~~~~~

The :py:func:`~PIL.Image.open` function sets the following
:py:attr:`~PIL.Image.Image.info` properties:

**scale**
The absolute value of the number stored in the *Scale Factor / Endianness* line.

PNG
^^^

Expand Down
49 changes: 49 additions & 0 deletions docs/releasenotes/10.3.0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
10.3.0
------

Backwards Incompatible Changes
==============================

TODO
^^^^

Deprecations
============

TODO
^^^^

TODO

API Changes
===========

TODO
^^^^

TODO

API Additions
=============

TODO
^^^^

TODO

Security
========

TODO
^^^^

TODO

Other Changes
=============

Portable FloatMap (PFM) images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Support has been added for reading and writing grayscale (Pf format)
Portable FloatMap (PFM) files containing ``F`` data.
1 change: 1 addition & 0 deletions docs/releasenotes/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2

10.3.0
10.2.0
10.1.0
10.0.1
Expand Down
26 changes: 22 additions & 4 deletions src/PIL/PpmImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#
from __future__ import annotations

import math

from . import Image, ImageFile
from ._binary import i16be as i16
from ._binary import o8
Expand All @@ -35,6 +37,7 @@
b"P6": "RGB",
# extensions
b"P0CMYK": "CMYK",
b"Pf": "F",
# PIL extensions (for test purposes only)
b"PyP": "P",
b"PyRGBA": "RGBA",
Expand All @@ -43,7 +46,7 @@


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"


##
Expand Down Expand Up @@ -110,6 +113,14 @@ def _open(self):
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
Expand All @@ -136,7 +147,8 @@ def _open(self):
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)]

Expand Down Expand Up @@ -307,6 +319,7 @@ def decode(self, buffer):


def _save(im, fp, filename):
row_order = 1
if im.mode == "1":
rawmode, head = "1;I", b"P4"
elif im.mode == "L":
Expand All @@ -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)
Expand All @@ -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))])


#
Expand All @@ -339,6 +357,6 @@ def _save(im, fp, filename):
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")

0 comments on commit b1f549f

Please sign in to comment.