From dbc0657f469b785519bd3bd39050a8006495f111 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 20:23:19 +1100 Subject: [PATCH] Ensure that optional APP1 EXIF marker is first, followed by APP2 MPF --- Tests/test_file_mpo.py | 2 + src/PIL/JpegImagePlugin.py | 2 +- src/PIL/MpoImagePlugin.py | 75 ++++++++++++++++++++++++++------------ 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 3e5476222..647105ebc 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -82,6 +82,7 @@ def test_app(test_file): def test_exif(test_file): with Image.open(test_file) as im_original: im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) + assert tuple(app[0] for app in im_reloaded.applist[:2]) == ("APP1", "APP2") for im in (im_original, im_reloaded): info = im._getexif() @@ -258,6 +259,7 @@ def test_save_all(): for test_file in test_files: with Image.open(test_file) as im: im_reloaded = roundtrip(im, save_all=True) + assert im_reloaded.applist[0][0] == "APP2" im.seek(0) assert_image_similar(im, im_reloaded, 30) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index eb0db5bb3..84b3c1a92 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -713,7 +713,7 @@ def _save(im, fp, filename): qtables = getattr(im, "quantization", None) qtables = validate_qtables(qtables) - extra = info.get("extra", b"") + extra = b"" icc_profile = info.get("icc_profile") if icc_profile: diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 095cfe7ee..d3bd2c65c 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -18,8 +18,8 @@ # See the README file for information on usage and redistribution. # +import io import itertools -import os import struct from . import ( @@ -31,7 +31,7 @@ from . import ( TiffImagePlugin, ) from ._binary import i16be as i16 -from ._binary import o32le +from ._binary import o16be, o32le # def _accept(prefix): # return JpegImagePlugin._accept(prefix) @@ -52,48 +52,75 @@ def _save_all(im, fp, filename): _save(im, fp, filename) return - mpf_offset = 28 - offsets = [] + data = [] for imSequence in itertools.chain([im], append_images): for im_frame in ImageSequence.Iterator(imSequence): - if not offsets: - # APP2 marker - im_frame.encoderinfo["extra"] = ( - b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 - ) - exif = im_frame.encoderinfo.get("exif") + b = io.BytesIO() + + exif = b"" + if not data: + # Do not pass EXIF to JpegImagePlugin + try: + exif = im_frame.encoderinfo["exif"] + del im_frame.encoderinfo["exif"] + except KeyError: + pass + + JpegImagePlugin._save(im_frame, b, filename) + else: + im_frame.save(b, "JPEG") + + jpeg = b.getvalue() + + # SOI + start = jpeg[:2] + + # APP1 + if exif: if isinstance(exif, Image.Exif): exif = exif.tobytes() - im_frame.encoderinfo["exif"] = exif - if exif: - mpf_offset += 4 + len(exif) + exif = b"\xFF\xE1" + o16be(2 + len(exif)) + exif - JpegImagePlugin._save(im_frame, fp, filename) - offsets.append(fp.tell()) - else: - im_frame.save(fp, "JPEG") - offsets.append(fp.tell() - offsets[-1]) + end = jpeg[2:] + data.append((start, exif, end)) ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd[0xB000] = b"0100" - ifd[0xB001] = len(offsets) + ifd[0xB001] = len(data) mpentries = b"" data_offset = 0 - for i, size in enumerate(offsets): + for i, frame_data in enumerate(data): + size = sum(len(part) for part in frame_data) if i == 0: + # APP2 will be appended later + size += 90 + mptype = 0x030000 # Baseline MP Primary Image else: mptype = 0x000000 # Undefined mpentries += struct.pack("