Repository: python-pillow/Pillow

Issue:
Sanitize TGA Image.info
Currently, the TGA image plugin sets `Image.info` "compression" to "tga_rle" on `load()`, while `save()` takes "rle" (bool) option. Both methods should use the same option for consistency. It probably makes sense to keep "rle" as TGA format doesn't support other compression methods, but "compression" may be more consistent with BMP and TIFF plugins. Neither of the options is documented, so there is no danger of breaking backward compatibility.

Also, it's not very clear whether `save()`  method should "inherit" info like TIFF and PNG plugins do:

https://github.com/python-pillow/Pillow/blob/4407cb65079a7d1150277e3b9a144996f56357c9/src/PIL/TiffImagePlugin.py#L1399-L1400

Currently, TGA plugin only inherits "orientation" but doesn't allow to specify it as a keyword to `save()`, and "id_section" is ignored altogether.


Discussion:

Resolving patch:
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -20,6 +20,8 @@
 from . import Image, ImageFile, ImagePalette
 from ._binary import i8, i16le as i16, o8, o16le as o16
 
+import warnings
+
 __version__ = "0.3"
 
 
@@ -53,7 +55,7 @@ def _open(self):
         # process header
         s = self.fp.read(18)
 
-        idlen = i8(s[0])
+        id_len = i8(s[0])
 
         colormaptype = i8(s[1])
         imagetype = i8(s[2])
@@ -100,8 +102,8 @@ def _open(self):
         if imagetype & 8:
             self.info["compression"] = "tga_rle"
 
-        if idlen:
-            self.info["id_section"] = self.fp.read(idlen)
+        if id_len:
+            self.info["id_section"] = self.fp.read(id_len)
 
         if colormaptype:
             # read palette
@@ -151,11 +153,23 @@ def _save(im, fp, filename):
     except KeyError:
         raise IOError("cannot write mode %s as TGA" % im.mode)
 
-    rle = im.encoderinfo.get("rle", False)
-
+    if "rle" in im.encoderinfo:
+        rle = im.encoderinfo["rle"]
+    else:
+        compression = im.encoderinfo.get("compression",
+                                         im.info.get("compression"))
+        rle = compression == "tga_rle"
     if rle:
         imagetype += 8
 
+    id_section = im.encoderinfo.get("id_section",
+                                    im.info.get("id_section", ""))
+    id_len = len(id_section)
+    if id_len > 255:
+        id_len = 255
+        id_section = id_section[:255]
+        warnings.warn("id_section has been trimmed to 255 characters")
+
     if colormaptype:
         colormapfirst, colormaplength, colormapentry = 0, 256, 24
     else:
@@ -166,11 +180,12 @@ def _save(im, fp, filename):
     else:
         flags = 0
 
-    orientation = im.info.get("orientation", -1)
+    orientation = im.encoderinfo.get("orientation",
+                                     im.info.get("orientation", -1))
     if orientation > 0:
         flags = flags | 0x20
 
-    fp.write(b"\000" +
+    fp.write(o8(id_len) +
              o8(colormaptype) +
              o8(imagetype) +
              o16(colormapfirst) +
@@ -183,6 +198,9 @@ def _save(im, fp, filename):
              o8(bits) +
              o8(flags))
 
+    if id_section:
+        fp.write(id_section)
+
     if colormaptype:
         fp.write(im.im.getpalette("RGB", "BGR"))
 


Test patch:
diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py
--- a/Tests/test_file_tga.py
+++ b/Tests/test_file_tga.py
@@ -53,10 +53,10 @@ def test_sanity(self):
                     # Generate a new test name every time so the
                     # test will not fail with permission error
                     # on Windows.
-                    test_file = self.tempfile("temp.tga")
+                    out = self.tempfile("temp.tga")
 
-                    original_im.save(test_file, rle=rle)
-                    saved_im = Image.open(test_file)
+                    original_im.save(out, rle=rle)
+                    saved_im = Image.open(out)
                     if rle:
                         self.assertEqual(
                             saved_im.info["compression"],
@@ -95,34 +95,93 @@ def test_save(self):
         test_file = "Tests/images/tga_id_field.tga"
         im = Image.open(test_file)
 
-        test_file = self.tempfile("temp.tga")
+        out = self.tempfile("temp.tga")
 
         # Save
-        im.save(test_file)
-        test_im = Image.open(test_file)
+        im.save(out)
+        test_im = Image.open(out)
         self.assertEqual(test_im.size, (100, 100))
+        self.assertEqual(test_im.info["id_section"], im.info["id_section"])
 
         # RGBA save
-        im.convert("RGBA").save(test_file)
-        test_im = Image.open(test_file)
+        im.convert("RGBA").save(out)
+        test_im = Image.open(out)
         self.assertEqual(test_im.size, (100, 100))
 
+    def test_save_id_section(self):
+        test_file = "Tests/images/rgb32rle.tga"
+        im = Image.open(test_file)
+
+        out = self.tempfile("temp.tga")
+
+        # Check there is no id section
+        im.save(out)
+        test_im = Image.open(out)
+        self.assertNotIn("id_section", test_im.info)
+
+        # Save with custom id section
+        im.save(out, id_section=b"Test content")
+        test_im = Image.open(out)
+        self.assertEqual(test_im.info["id_section"], b"Test content")
+
+        # Save with custom id section greater than 255 characters
+        id_section = b"Test content" * 25
+        self.assert_warning(UserWarning,
+                            lambda: im.save(out, id_section=id_section))
+        test_im = Image.open(out)
+        self.assertEqual(test_im.info["id_section"], id_section[:255])
+
+        test_file = "Tests/images/tga_id_field.tga"
+        im = Image.open(test_file)
+
+        # Save with no id section
+        im.save(out, id_section="")
+        test_im = Image.open(out)
+        self.assertNotIn("id_section", test_im.info)
+
+    def test_save_orientation(self):
+        test_file = "Tests/images/rgb32rle.tga"
+        im = Image.open(test_file)
+        self.assertEqual(im.info["orientation"], -1)
+
+        out = self.tempfile("temp.tga")
+
+        im.save(out, orientation=1)
+        test_im = Image.open(out)
+        self.assertEqual(test_im.info["orientation"], 1)
+
     def test_save_rle(self):
         test_file = "Tests/images/rgb32rle.tga"
         im = Image.open(test_file)
+        self.assertEqual(im.info["compression"], "tga_rle")
 
-        test_file = self.tempfile("temp.tga")
+        out = self.tempfile("temp.tga")
 
         # Save
-        im.save(test_file)
-        test_im = Image.open(test_file)
+        im.save(out)
+        test_im = Image.open(out)
         self.assertEqual(test_im.size, (199, 199))
+        self.assertEqual(test_im.info["compression"], "tga_rle")
+
+        # Save without compression
+        im.save(out, compression=None)
+        test_im = Image.open(out)
+        self.assertNotIn("compression", test_im.info)
 
         # RGBA save
-        im.convert("RGBA").save(test_file)
-        test_im = Image.open(test_file)
+        im.convert("RGBA").save(out)
+        test_im = Image.open(out)
         self.assertEqual(test_im.size, (199, 199))
 
+        test_file = "Tests/images/tga_id_field.tga"
+        im = Image.open(test_file)
+        self.assertNotIn("compression", im.info)
+
+        # Save with compression
+        im.save(out, compression="tga_rle")
+        test_im = Image.open(out)
+        self.assertEqual(test_im.info["compression"], "tga_rle")
+
     def test_save_l_transparency(self):
         # There are 559 transparent pixels in la.tga.
         num_transparent = 559
@@ -133,10 +192,10 @@ def test_save_l_transparency(self):
         self.assertEqual(
             im.getchannel("A").getcolors()[0][0], num_transparent)
 
-        test_file = self.tempfile("temp.tga")
-        im.save(test_file)
+        out = self.tempfile("temp.tga")
+        im.save(out)
 
-        test_im = Image.open(test_file)
+        test_im = Image.open(out)
         self.assertEqual(test_im.mode, "LA")
         self.assertEqual(
             test_im.getchannel("A").getcolors()[0][0], num_transparent)