Repository: python-pillow/Pillow

Issue:
The PNG loader crashes trying to save palette images with tRNS color type 3 (indexed color) value after convert to RGB
![defaktologo_bad](https://f.cloud.github.com/assets/4199178/2030235/e2a6f2ea-88f3-11e3-9d40-b86f41857491.png)

ValueError: too many values to unpack

![valueerror](https://f.cloud.github.com/assets/4199178/2030315/05d21a00-88f5-11e3-932c-6ab1a5d0ea62.png)

4.2.1.1. tRNS Transparency (PNG Specification)
http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.tRNS

The PNG loader crashes trying to save palette images with tRNS color type 3 (indexed color) value after convert to RGB
![defaktologo_bad](https://f.cloud.github.com/assets/4199178/2030235/e2a6f2ea-88f3-11e3-9d40-b86f41857491.png)

ValueError: too many values to unpack

![valueerror](https://f.cloud.github.com/assets/4199178/2030315/05d21a00-88f5-11e3-932c-6ab1a5d0ea62.png)

4.2.1.1. tRNS Transparency (PNG Specification)
http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.tRNS



Discussion:

Resolving patch:
diff --git a/PIL/Image.py b/PIL/Image.py
--- a/PIL/Image.py
+++ b/PIL/Image.py
@@ -735,21 +735,65 @@ def convert(self, mode=None, matrix=None, dither=None,
             im = self.im.convert_matrix(mode, matrix)
             return self._new(im)
 
+        if mode == "P" and self.mode == "RGBA":
+            return self.quantize(colors)
+
+        trns = None
+        delete_trns = False
+        # transparency handling
+        if "transparency" in self.info and self.info['transparency'] is not None:
+            if self.mode in ('L', 'RGB') and mode == 'RGBA':
+                # Use transparent conversion to promote from transparent
+                # color to an alpha channel.         
+                return self._new(self.im.convert_transparent(
+                                           mode, self.info['transparency']))
+            elif self.mode in ('L', 'RGB', 'P') and mode in ('L', 'RGB', 'P'):
+                t = self.info['transparency']
+                if isinstance(t, bytes):
+                    # Dragons. This can't be represented by a single color
+                    warnings.warn('Palette images with Transparency expressed '+
+                                  ' in bytes should be converted to RGBA images')
+                    delete_trns = True
+                else:
+                    # get the new transparency color.
+                    # use existing conversions
+                    trns_im = Image()._new(core.new(self.mode, (1,1)))
+                    if self.mode == 'P':
+                        trns_im.putpalette(self.palette)
+                    trns_im.putpixel((0,0), t)
+
+                    if mode in ('L','RGB'):
+                        trns_im = trns_im.convert(mode)
+                    else:
+                        # can't just retrieve the palette number, got to do it
+                        # after quantization. 
+                        trns_im = trns_im.convert('RGB')
+                    trns = trns_im.getpixel((0,0))
+                        
+                    
         if mode == "P" and palette == ADAPTIVE:
             im = self.im.quantize(colors)
             new = self._new(im)
             from PIL import ImagePalette
             new.palette = ImagePalette.raw("RGB", new.im.getpalette("RGB"))
+            if delete_trns:
+                # This could possibly happen if we requantize to fewer colors.
+                # The transparency would be totally off in that case. 
+                del(new.info['transparency'])
+            if trns is not None:
+                try:
+                    new.info['transparency'] = new.palette.getcolor(trns)
+                except:
+                    # if we can't make a transparent color, don't leave the old
+                    # transparency hanging around to mess us up.
+                    del(new.info['transparency'])
+                    warnings.warn("Couldn't allocate palette entry for transparency")
             return new
 
         # colorspace conversion
         if dither is None:
             dither = FLOYDSTEINBERG
-
-        # Use transparent conversion to promote from transparent color to an alpha channel.
-        if self.mode in ("L", "RGB") and mode == "RGBA" and "transparency" in self.info:
-            return self._new(self.im.convert_transparent(mode, self.info['transparency']))
-            
+                
         try:
             im = self.im.convert(mode, dither)
         except ValueError:
@@ -760,9 +804,22 @@ def convert(self, mode=None, matrix=None, dither=None,
             except KeyError:
                 raise ValueError("illegal conversion")
 
-        return self._new(im)
+        new_im = self._new(im)
+        if delete_trns:
+            #crash fail if we leave a bytes transparency in an rgb/l mode.
+            del(new.info['transparency'])
+        if trns is not None:
+            if new_im.mode == 'P':
+                try:
+                    new_im.info['transparency'] = new_im.palette.getcolor(trns)
+                except:
+                    del(new_im.info['transparency'])
+                    warnings.warn("Couldn't allocate palette entry for transparency")
+            else:
+                new_im.info['transparency'] = trns
+        return new_im
 
-    def quantize(self, colors=256, method=0, kmeans=0, palette=None):
+    def quantize(self, colors=256, method=None, kmeans=0, palette=None):
 
         # methods:
         #    0 = median cut
@@ -773,7 +830,18 @@ def quantize(self, colors=256, method=0, kmeans=0, palette=None):
         # quantizer interface in a later version of PIL.
 
         self.load()
-
+        
+        if method is None:
+            # defaults:
+            method = 0
+            if self.mode == 'RGBA':
+                method = 2
+
+        if self.mode == 'RGBA' and method != 2:
+            # Caller specified an invalid mode. 
+            raise ValueError('Fast Octree (method == 2) is the ' +
+                             ' only valid method for quantizing RGBA images')
+        
         if palette:
             # use palette from reference image
             palette.load()


Test patch:
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -46,5 +46,67 @@ def test_16bit_workaround():
     im = Image.open('Tests/images/16bit.cropped.tif')
     _test_float_conversion(im.convert('I'))
     
+def test_rgba_p():
+    im = lena('RGBA')
+    im.putalpha(lena('L'))
 
+    converted = im.convert('P')
+    comparable = converted.convert('RGBA')
+
+    assert_image_similar(im, comparable, 20)
+               
+def test_trns_p():    
+    im = lena('P')
+    im.info['transparency']=0
+
+    f = tempfile('temp.png')
+
+    l = im.convert('L')
+    assert_equal(l.info['transparency'], 0) # undone
+    assert_no_exception(lambda: l.save(f))
+
+
+    rgb = im.convert('RGB')
+    assert_equal(rgb.info['transparency'], (0,0,0)) # undone
+    assert_no_exception(lambda: rgb.save(f))
+    
+def test_trns_l():
+    im = lena('L')
+    im.info['transparency'] = 128
+
+    f = tempfile('temp.png')
+    
+    rgb = im.convert('RGB')
+    assert_equal(rgb.info['transparency'], (128,128,128)) # undone
+    assert_no_exception(lambda: rgb.save(f))
+
+    p = im.convert('P')
+    assert_true('transparency' in p.info)
+    assert_no_exception(lambda: p.save(f))
+
+    p = assert_warning(UserWarning,
+                       lambda: im.convert('P', palette = Image.ADAPTIVE))
+    assert_false('transparency' in p.info)
+    assert_no_exception(lambda: p.save(f))
+
+    
+def test_trns_RGB():
+    im = lena('RGB')
+    im.info['transparency'] = im.getpixel((0,0))
+
+    f = tempfile('temp.png')
+        
+    l = im.convert('L')
+    assert_equal(l.info['transparency'], l.getpixel((0,0))) # undone
+    assert_no_exception(lambda: l.save(f))
+
+    p = im.convert('P')
+    assert_true('transparency' in p.info)
+    assert_no_exception(lambda: p.save(f))
     
+    p = assert_warning(UserWarning,
+                       lambda: im.convert('P', palette = Image.ADAPTIVE))
+    assert_false('transparency' in p.info)
+    assert_no_exception(lambda: p.save(f))
+
+
diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py
--- a/Tests/test_image_quantize.py
+++ b/Tests/test_image_quantize.py
@@ -19,4 +19,9 @@ def test_octree_quantize():
     im = im.quantize(100, Image.FASTOCTREE)
     assert_image(im, "P", im.size)
 
-    assert len(im.getcolors()) == 100
\ No newline at end of file
+    assert len(im.getcolors()) == 100
+
+def test_rgba_quantize():
+    im = lena('RGBA')
+    assert_no_exception(lambda: im.quantize())
+    assert_exception(Exception, lambda: im.quantize(method=0))
diff --git a/Tests/tester.py b/Tests/tester.py
--- a/Tests/tester.py
+++ b/Tests/tester.py
@@ -121,9 +121,9 @@ def assert_no_exception(func):
 def assert_warning(warn_class, func):
     # note: this assert calls func three times!
     import warnings
-    def warn_error(message, category, **options):
+    def warn_error(message, category=UserWarning, **options):
         raise category(message)
-    def warn_ignore(message, category, **options):
+    def warn_ignore(message, category=UserWarning, **options):
         pass
     warn = warnings.warn
     result = None