From 2944ff18d6ac79b881e3a3ec8d656fe08a41d1a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 16 Jul 2022 20:02:58 +1000 Subject: [PATCH 1/2] Support saving multiple MPO frames --- Tests/test_file_mpo.py | 41 +++++++++++++++++--- docs/handbook/image-file-formats.rst | 11 ++++++ src/PIL/JpegImagePlugin.py | 2 +- src/PIL/MpoImagePlugin.py | 57 ++++++++++++++++++++++++++-- 4 files changed, 101 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index d093f26ccd8..849857d31d6 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -5,15 +5,19 @@ from PIL import Image -from .helper import assert_image_similar, is_pypy, skip_unless_feature +from .helper import ( + assert_image_equal, + assert_image_similar, + is_pypy, + skip_unless_feature, +) test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] pytestmark = skip_unless_feature("jpg") -def frame_roundtrip(im, **options): - # Note that for now, there is no MPO saving functionality +def roundtrip(im, **options): out = BytesIO() im.save(out, "MPO", **options) test_bytes = out.tell() @@ -237,13 +241,38 @@ def test_image_grab(): def test_save(): - # Note that only individual frames can be saved at present for test_file in test_files: with Image.open(test_file) as im: assert im.tell() == 0 - jpg0 = frame_roundtrip(im) + jpg0 = roundtrip(im) assert_image_similar(im, jpg0, 30) im.seek(1) assert im.tell() == 1 - jpg1 = frame_roundtrip(im) + jpg1 = roundtrip(im) assert_image_similar(im, jpg1, 30) + + +def test_save_all(): + for test_file in test_files: + with Image.open(test_file) as im: + im_reloaded = roundtrip(im, save_all=True) + + im.seek(0) + assert_image_similar(im, im_reloaded, 30) + + im.seek(1) + im_reloaded.seek(1) + assert_image_similar(im, im_reloaded, 30) + + im = Image.new("RGB", (1, 1)) + im2 = Image.new("RGB", (1, 1), "#f00") + im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) + + assert_image_equal(im, im_reloaded) + + im_reloaded.seek(1) + assert_image_similar(im2, im_reloaded, 1) + + # Test that a single frame image will not be saved as an MPO + jpg = roundtrip(im, save_all=True) + assert "mp" not in jpg.info diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 30452c4a6c2..1728c8e0579 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1209,6 +1209,17 @@ image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL methods may be used to read other pictures from the file. The pictures are zero-indexed and random access is supported. +When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default +only the first frame of a multiframe image will be saved. If the ``save_all`` +argument is present and true, then all frames will be saved, and the following +option will also be available. + +**append_images** + A list of images to append as additional pictures. Each of the + images in the list can be single or multiframe images. + + .. versionadded:: 9.3.0 + PCD ^^^ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 4efe6281ad6..a6ed223bc6f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -711,7 +711,7 @@ def validate_qtables(qtables): qtables = getattr(im, "quantization", None) qtables = validate_qtables(qtables) - extra = b"" + extra = info.get("extra", b"") icc_profile = info.get("icc_profile") if icc_profile: diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 27c30958c9c..5bfd8efc1a6 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -18,16 +18,66 @@ # See the README file for information on usage and redistribution. # -from . import Image, ImageFile, JpegImagePlugin +import itertools +import os +import struct + +from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin from ._binary import i16be as i16 +from ._binary import o32le # def _accept(prefix): # return JpegImagePlugin._accept(prefix) def _save(im, fp, filename): - # Note that we can only save the current frame at present - return JpegImagePlugin._save(im, fp, filename) + JpegImagePlugin._save(im, fp, filename) + + +def _save_all(im, fp, filename): + append_images = im.encoderinfo.get("append_images", []) + if not append_images: + try: + animated = im.is_animated + except AttributeError: + animated = False + if not animated: + _save(im, fp, filename) + return + + offsets = [] + for imSequence in itertools.chain([im], append_images): + for im_frame in ImageSequence.Iterator(imSequence): + if not offsets: + # APP2 marker + im.encoderinfo["extra"] = ( + b"\xFF\xE2" + struct.pack(">H", 6 + 70) + b"MPF\0" + b" " * 70 + ) + JpegImagePlugin._save(im_frame, fp, filename) + offsets.append(fp.tell()) + else: + im_frame.save(fp, "JPEG") + offsets.append(fp.tell() - offsets[-1]) + + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[0xB001] = len(offsets) + + mpentries = b"" + data_offset = 0 + for i, size in enumerate(offsets): + if i == 0: + mptype = 0x030000 # Baseline MP Primary Image + else: + mptype = 0x000000 # Undefined + mpentries += struct.pack(" Date: Tue, 19 Jul 2022 07:40:25 +1000 Subject: [PATCH 2/2] Added release notes --- docs/releasenotes/9.3.0.rst | 59 +++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 60 insertions(+) create mode 100644 docs/releasenotes/9.3.0.rst diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst new file mode 100644 index 00000000000..da045a50a98 --- /dev/null +++ b/docs/releasenotes/9.3.0.rst @@ -0,0 +1,59 @@ +9.3.0 +----- + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Saving multiple MPO frames +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Multiple MPO frames can now be saved. Using the ``save_all`` argument, all of +an image's frames will be saved to file:: + + from PIL import Image + im = Image.open("frozenpond.mpo") + im.save(out, save_all=True) + +Additional images can also be appended when saving, by combining the +``save_all`` argument with the ``append_images`` argument:: + + im.save(out, save_all=True, append_images=[im1, im2, ...]) + + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 597c804f861..8c436be3bd8 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 9.3.0 9.2.0 9.1.1 9.1.0