Merge branch 'main' into jpeg-app-segments

This commit is contained in:
Hugo van Kemenade 2024-01-09 16:10:41 +02:00 committed by GitHub
commit 520bf6397f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 81 additions and 51 deletions

View File

@ -10,18 +10,25 @@ from .helper import assert_image_equal, assert_image_similar, hopper
class TestImageTransform: class TestImageTransform:
def test_sanity(self): def test_sanity(self):
im = Image.new("L", (100, 100)) im = hopper()
seq = tuple(range(10)) for transform in (
ImageTransform.AffineTransform((1, 0, 0, 0, 1, 0)),
transform = ImageTransform.AffineTransform(seq[:6]) ImageTransform.PerspectiveTransform((1, 0, 0, 0, 1, 0, 0, 0)),
im.transform((100, 100), transform) ImageTransform.ExtentTransform((0, 0) + im.size),
transform = ImageTransform.ExtentTransform(seq[:4]) ImageTransform.QuadTransform(
im.transform((100, 100), transform) (0, 0, 0, im.height, im.width, im.height, im.width, 0)
transform = ImageTransform.QuadTransform(seq[:8]) ),
im.transform((100, 100), transform) ImageTransform.MeshTransform(
transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])]) [
im.transform((100, 100), transform) (
(0, 0) + im.size,
(0, 0, 0, im.height, im.width, im.height, im.width, 0),
)
]
),
):
assert_image_equal(im, im.transform(im.size, transform))
def test_info(self): def test_info(self):
comment = b"File written by Adobe Photoshop\xa8 4.0" comment = b"File written by Adobe Photoshop\xa8 4.0"

View File

@ -19,6 +19,11 @@ The :py:mod:`~PIL.ImageTransform` module contains implementations of
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
.. autoclass:: PerspectiveTransform
:members:
:undoc-members:
:show-inheritance:
.. autoclass:: ExtentTransform .. autoclass:: ExtentTransform
:members: :members:
:undoc-members: :undoc-members:

View File

@ -33,6 +33,13 @@ When saving JPEG files, ``no_default_app_segments`` can now be set to ``True`` t
the image without default JFIF and Adobe application segments. The JFIF segment will the image without default JFIF and Adobe application segments. The JFIF segment will
still be stored if ``dpi`` is also specified. still be stored if ``dpi`` is also specified.
Added PerspectiveTransform
^^^^^^^^^^^^^^^^^^^^^^^^^^
:py:class:`~PIL.ImageTransform.PerspectiveTransform` has been added, meaning
that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding
subclass of :py:class:`~PIL.ImageTransform.Transform`.
Security Security
======== ========

View File

@ -63,6 +63,26 @@ class AffineTransform(Transform):
method = Image.Transform.AFFINE method = Image.Transform.AFFINE
class PerspectiveTransform(Transform):
"""
Define a perspective image transform.
This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel
(x, y) in the output image, the new value is taken from a position
((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in
the input image, rounded to nearest pixel.
This function can be used to scale, translate, rotate, and shear the
original image.
See :py:meth:`.Image.transform`
:param matrix: An 8-tuple (a, b, c, d, e, f, g, h).
"""
method = Image.Transform.PERSPECTIVE
class ExtentTransform(Transform): class ExtentTransform(Transform):
""" """
Define a transform to extract a subregion from an image. Define a transform to extract a subregion from an image.

View File

@ -100,6 +100,7 @@ class PpmImageFile(ImageFile.ImageFile):
except KeyError: except KeyError:
msg = "not a PPM file" msg = "not a PPM file"
raise SyntaxError(msg) raise SyntaxError(msg)
self._mode = mode
if magic_number in (b"P1", b"P4"): if magic_number in (b"P1", b"P4"):
self.custom_mimetype = "image/x-portable-bitmap" self.custom_mimetype = "image/x-portable-bitmap"
@ -108,38 +109,31 @@ class PpmImageFile(ImageFile.ImageFile):
elif magic_number in (b"P3", b"P6"): elif magic_number in (b"P3", b"P6"):
self.custom_mimetype = "image/x-portable-pixmap" self.custom_mimetype = "image/x-portable-pixmap"
maxval = None self._size = int(self._read_token()), int(self._read_token())
decoder_name = "raw" decoder_name = "raw"
if magic_number in (b"P1", b"P2", b"P3"): if magic_number in (b"P1", b"P2", b"P3"):
decoder_name = "ppm_plain" decoder_name = "ppm_plain"
for ix in range(3): if mode == "1":
if mode == "F" and ix == 2: args = "1;I"
elif mode == "F":
scale = float(self._read_token()) scale = float(self._read_token())
if scale == 0.0 or not math.isfinite(scale): if scale == 0.0 or not math.isfinite(scale):
msg = "scale must be finite and non-zero" msg = "scale must be finite and non-zero"
raise ValueError(msg) raise ValueError(msg)
rawmode = "F;32F" if scale < 0 else "F;32BF"
self.info["scale"] = abs(scale) self.info["scale"] = abs(scale)
continue
token = int(self._read_token()) rawmode = "F;32F" if scale < 0 else "F;32BF"
if ix == 0: # token is the x size args = (rawmode, 0, -1)
xsize = token
elif ix == 1: # token is the y size
ysize = token
if mode == "1":
self._mode = "1"
rawmode = "1;I"
break
else: else:
self._mode = rawmode = mode maxval = int(self._read_token())
elif ix == 2: # token is maxval
maxval = token
if not 0 < maxval < 65536: if not 0 < maxval < 65536:
msg = "maxval must be greater than 0 and less than 65536" msg = "maxval must be greater than 0 and less than 65536"
raise ValueError(msg) raise ValueError(msg)
if maxval > 255 and mode == "L": if maxval > 255 and mode == "L":
self._mode = "I" self._mode = "I"
rawmode = mode
if decoder_name != "ppm_plain": if decoder_name != "ppm_plain":
# If maxval matches a bit depth, use the raw decoder directly # If maxval matches a bit depth, use the raw decoder directly
if maxval == 65535 and mode == "L": if maxval == 65535 and mode == "L":
@ -147,10 +141,8 @@ class PpmImageFile(ImageFile.ImageFile):
elif maxval != 255: elif maxval != 255:
decoder_name = "ppm" decoder_name = "ppm"
row_order = -1 if mode == "F" else 1 args = rawmode if decoder_name == "raw" else (rawmode, maxval)
args = (rawmode, 0, row_order) if decoder_name == "raw" else (rawmode, maxval) self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)]
self._size = xsize, ysize
self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)]
# #
@ -319,7 +311,6 @@ class PpmDecoder(ImageFile.PyDecoder):
def _save(im, fp, filename): def _save(im, fp, filename):
row_order = 1
if im.mode == "1": if im.mode == "1":
rawmode, head = "1;I", b"P4" rawmode, head = "1;I", b"P4"
elif im.mode == "L": elif im.mode == "L":
@ -330,7 +321,6 @@ def _save(im, fp, filename):
rawmode, head = "RGB", b"P6" rawmode, head = "RGB", b"P6"
elif im.mode == "F": elif im.mode == "F":
rawmode, head = "F;32F", b"Pf" rawmode, head = "F;32F", b"Pf"
row_order = -1
else: else:
msg = f"cannot write mode {im.mode} as PPM" msg = f"cannot write mode {im.mode} as PPM"
raise OSError(msg) raise OSError(msg)
@ -344,6 +334,7 @@ def _save(im, fp, filename):
fp.write(b"65535\n") fp.write(b"65535\n")
elif head == b"Pf": elif head == b"Pf":
fp.write(b"-1.0\n") fp.write(b"-1.0\n")
row_order = -1 if im.mode == "F" else 1
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]) ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))])