Repository: python-pillow/Pillow

Issue:
PIL doesn't write JPEG comment
Following up on an answer I gave to the following stackoverflow question: https://stackoverflow.com/a/74658310/1358308

OP asked how to write a JPEG comment into the file, i.e. not an EXIF comment, but as a COM segment.  The closest issue I could find, #4450, was about reading JPEG comments.

While looking for a solution I cloned the repo (hash 5bdf63e1d170e1db324c5f805c4e838e4d455e98) to see what was going on, but couldn't see anything relevant.

Would be happy to turn this into a pull-request if you think this is a reasonable idea.  I think I'd fix by modifying `_save()` to pull in the comment (from the `applist`) by appending to `extra` in a similar way as `icc_profile`:

https://github.com/python-pillow/Pillow/blob/948678633924fd62b4ab1cc632acd218ad6796ba/src/PIL/JpegImagePlugin.py#L714-L736

Comments appreciated!


Discussion:

Resolving patch:
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -45,6 +45,7 @@
 from ._binary import i16be as i16
 from ._binary import i32be as i32
 from ._binary import o8
+from ._binary import o16be as o16
 from ._deprecate import deprecate
 from .JpegPresets import presets
 
@@ -724,7 +725,7 @@ def validate_qtables(qtables):
             icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:]
         i = 1
         for marker in markers:
-            size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker))
+            size = o16(2 + ICC_OVERHEAD_LEN + len(marker))
             extra += (
                 b"\xFF\xE2"
                 + size
@@ -735,6 +736,8 @@ def validate_qtables(qtables):
             )
             i += 1
 
+    comment = info.get("comment", im.info.get("comment"))
+
     # "progressive" is the official name, but older documentation
     # says "progression"
     # FIXME: issue a warning if the wrong form is used (post-1.1.7)
@@ -757,6 +760,7 @@ def validate_qtables(qtables):
         dpi[1],
         subsampling,
         qtables,
+        comment,
         extra,
         exif,
     )


Test patch:
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -86,6 +86,33 @@ def test_app(self):
             assert len(im.applist) == 2
 
             assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
+            assert im.app["COM"] == im.info["comment"]
+
+    def test_comment_write(self):
+        with Image.open(TEST_FILE) as im:
+            assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
+
+            # Test that existing comment is saved by default
+            out = BytesIO()
+            im.save(out, format="JPEG")
+            with Image.open(out) as reloaded:
+                assert im.info["comment"] == reloaded.info["comment"]
+
+            # Ensure that a blank comment causes any existing comment to be removed
+            for comment in ("", b"", None):
+                out = BytesIO()
+                im.save(out, format="JPEG", comment=comment)
+                with Image.open(out) as reloaded:
+                    assert "comment" not in reloaded.info
+
+            # Test that a comment argument overrides the default comment
+            for comment in ("Test comment text", b"Text comment text"):
+                out = BytesIO()
+                im.save(out, format="JPEG", comment=comment)
+                with Image.open(out) as reloaded:
+                    if not isinstance(comment, bytes):
+                        comment = comment.encode()
+                    assert reloaded.info["comment"] == comment
 
     def test_cmyk(self):
         # Test CMYK handling.  Thanks to Tim and Charlie for test data,