From 2722cfd72b6b458f709d71c2ef06e9949cf3bbe2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Aug 2024 20:20:24 +1000 Subject: [PATCH 1/4] Added writing XMP bytes to JPEG --- Tests/test_file_jpeg.py | 7 +++++++ src/PIL/JpegImagePlugin.py | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index df589e24a..8feae2edd 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -991,6 +991,13 @@ class TestFileJpeg: else: assert im.getxmp() == {"xmpmeta": None} + def test_save_xmp(self, tmp_path: Path) -> None: + f = str(tmp_path / "temp.jpg") + hopper().save(f, xmp=b"XMP test") + + with Image.open(f) as reloaded: + assert reloaded.info["xmp"] == b"XMP test" + @pytest.mark.timeout(timeout=1) def test_eof(self) -> None: # Even though this decoder never says that it is finished diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index fc897e2b9..f15b62f0f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -746,6 +746,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: extra = info.get("extra", b"") + xmp = info.get("xmp") + if xmp: + size = o16(31 + len(xmp)) + extra += b"\xFF\xE1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp + MAX_BYTES_IN_MARKER = 65533 icc_profile = info.get("icc_profile") if icc_profile: From d49884e40c204db7ce89b621823b2ac37ad51a10 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Aug 2024 21:48:36 +1000 Subject: [PATCH 2/4] Raise ValueError is XMP data is too long --- Tests/test_file_jpeg.py | 8 +++++++- src/PIL/JpegImagePlugin.py | 19 ++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 8feae2edd..f9c5fbc0b 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -993,11 +993,17 @@ class TestFileJpeg: def test_save_xmp(self, tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") - hopper().save(f, xmp=b"XMP test") + im = hopper() + im.save(f, xmp=b"XMP test") with Image.open(f) as reloaded: assert reloaded.info["xmp"] == b"XMP test" + im.save(f, xmp=b"1" * 65504) + + with pytest.raises(ValueError): + im.save(f, xmp=b"1" * 65505) + @pytest.mark.timeout(timeout=1) def test_eof(self) -> None: # Even though this decoder never says that it is finished diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index f15b62f0f..75c0ae384 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -746,23 +746,28 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: extra = info.get("extra", b"") + MAX_BYTES_IN_MARKER = 65533 xmp = info.get("xmp") if xmp: - size = o16(31 + len(xmp)) + overhead_len = 29 + max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len + if len(xmp) > max_data_bytes_in_marker: + msg = "XMP data is too long" + raise ValueError(msg) + size = o16(2 + overhead_len + len(xmp)) extra += b"\xFF\xE1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp - MAX_BYTES_IN_MARKER = 65533 icc_profile = info.get("icc_profile") if icc_profile: - ICC_OVERHEAD_LEN = 14 - MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN + overhead_len = 14 + max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len markers = [] while icc_profile: - markers.append(icc_profile[:MAX_DATA_BYTES_IN_MARKER]) - icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:] + markers.append(icc_profile[:max_data_bytes_in_marker]) + icc_profile = icc_profile[max_data_bytes_in_marker:] i = 1 for marker in markers: - size = o16(2 + ICC_OVERHEAD_LEN + len(marker)) + size = o16(2 + overhead_len + len(marker)) extra += ( b"\xFF\xE2" + size From c056406f21e2a5aa87a8e4560fc33e079bed59e8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Aug 2024 06:19:15 +1000 Subject: [PATCH 3/4] Added comments to explain overhead_len --- src/PIL/JpegImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 75c0ae384..0cb50e0bf 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -749,7 +749,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: MAX_BYTES_IN_MARKER = 65533 xmp = info.get("xmp") if xmp: - overhead_len = 29 + overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len if len(xmp) > max_data_bytes_in_marker: msg = "XMP data is too long" @@ -759,7 +759,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: icc_profile = info.get("icc_profile") if icc_profile: - overhead_len = 14 + overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len markers = [] while icc_profile: From be34a7da4bb25a660d83abee3495626f221e949d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Sep 2024 19:19:39 +1000 Subject: [PATCH 4/4] Save xmp from info --- Tests/test_file_jpeg.py | 6 ++++-- src/PIL/JpegImagePlugin.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index f9c5fbc0b..c8981ce85 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -995,11 +995,13 @@ class TestFileJpeg: f = str(tmp_path / "temp.jpg") im = hopper() im.save(f, xmp=b"XMP test") - with Image.open(f) as reloaded: assert reloaded.info["xmp"] == b"XMP test" - im.save(f, xmp=b"1" * 65504) + im.info["xmp"] = b"1" * 65504 + im.save(f) + with Image.open(f) as reloaded: + assert reloaded.info["xmp"] == b"1" * 65504 with pytest.raises(ValueError): im.save(f, xmp=b"1" * 65505) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 0cb50e0bf..723165ec4 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -747,7 +747,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: extra = info.get("extra", b"") MAX_BYTES_IN_MARKER = 65533 - xmp = info.get("xmp") + xmp = info.get("xmp", im.info.get("xmp")) if xmp: overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len