Repository: python-pillow/Pillow

Issue:
Support RGBA→PA conversion
I would expect this to generate a 256-color PNG image with `tRNS` chunk with up to 256 levels of alpha transparency:

``` python
Image.open("in.png").convert("RGBA").convert("PA").save("out.png")
```

Unfortunately, I get an error, and it's oddly implying that the input is RGB and not RGBA:

> ValueError: conversion from RGB to PA not supported

I'm surprised it doesn't work, because [there is code to support RGBA quantization](https://github.com/python-imaging/Pillow/blob/master/libImaging/QuantOctree.c).

Did I miss something, or is that a bug?

---

Pillow 2.3.0, Mac OS X 10.9.2, Python 2.7.5

Support RGBA→PA conversion
I would expect this to generate a 256-color PNG image with `tRNS` chunk with up to 256 levels of alpha transparency:

``` python
Image.open("in.png").convert("RGBA").convert("PA").save("out.png")
```

Unfortunately, I get an error, and it's oddly implying that the input is RGB and not RGBA:

> ValueError: conversion from RGB to PA not supported

I'm surprised it doesn't work, because [there is code to support RGBA quantization](https://github.com/python-imaging/Pillow/blob/master/libImaging/QuantOctree.c).

Did I miss something, or is that a bug?

---

Pillow 2.3.0, Mac OS X 10.9.2, Python 2.7.5



Discussion:
From a quick look, it appears that the paths through Image.py:convert to Image.py:quantize are RGB:P, not with alpha channels. So, it falls through to the C level in Convert.c, P and PA go through the frompalette, but only P goes through topalette, and topalette looks like it only does RGB:P.  

It might work if you run quantize on the RGBA, instead of convert. 

Ok further information. PA images (which are 2 channel, P + A, each 8 bits) are hard to create:
- You can't create one by adding an alpha channel to a P image. 
- Fast Octree quantization on an RGBA image returns a P image where the palette is RGBA items, not a PA image. I was getting errors trying to save that, so I'm not sure that that way is roses either. It appears that some parts of the palette code assume a max of 3 bands. 
- Internally, you can't setmode from P to PA, since that is an inplace operation. 
- There's no convert mapping from P to PA.
- Finally, I think that the only format that actually saves PA images is the internal ImageMath format, which is essentially raw data. Tiff is the only other one that reads them. 
- You can make a PA image from Image.new. 

So, PA probably isn't the answer. 

If you haven an RGBA palette, that's stored in bands (which is what the python layer uses -- the C layer seems to use interleaved palettes), then the fitst 768 bytes will be the RGB palette, and the last 256 would be the tRNS header. I'll take a look and see if that's a plausible way to handle it. And hey, it should work: PngImagePlugin.py:586. As long as info['transparency'] and encoderinfo['transparency'] are none, that should work. It's tested here: Tests/test_file_png.py:164. 

Sorry, I've mistaken `PA` for a palette with RGBA entries. I've actually meant the latter — `P` with alpha suitable for PNG with the `tRNS` chunk.

``` python
Image.open("in.png").convert("RGBA").convert("P").save("out.png")
```

doesn't preserve the alpha either. I get the desired result with:

``` python
Image.open("in.png").convert("RGBA").quantize(method=2).save("out.png")
```

so maybe `convert("P")` could behave like `quantize(method=2)` when source is RGBA?

I think it's reasonable for RGBA converting to P to use quantize with sane defaults. I think it's also a good idea for an RGBA image to default to quantizing with FastOctree, since there's no other current option. 

@wiredfool Do you have any code for this issue, assuming code is needed? I'm not sure I fully follow the discussion.

In my head. I think it's just a quick couple of bits, but that impression was from a week ago. I should have done it before it swapped out of my head. 

From a quick look, it appears that the paths through Image.py:convert to Image.py:quantize are RGB:P, not with alpha channels. So, it falls through to the C level in Convert.c, P and PA go through the frompalette, but only P goes through topalette, and topalette looks like it only does RGB:P.  

It might work if you run quantize on the RGBA, instead of convert. 

Ok further information. PA images (which are 2 channel, P + A, each 8 bits) are hard to create:
- You can't create one by adding an alpha channel to a P image. 
- Fast Octree quantization on an RGBA image returns a P image where the palette is RGBA items, not a PA image. I was getting errors trying to save that, so I'm not sure that that way is roses either. It appears that some parts of the palette code assume a max of 3 bands. 
- Internally, you can't setmode from P to PA, since that is an inplace operation. 
- There's no convert mapping from P to PA.
- Finally, I think that the only format that actually saves PA images is the internal ImageMath format, which is essentially raw data. Tiff is the only other one that reads them. 
- You can make a PA image from Image.new. 

So, PA probably isn't the answer. 

If you haven an RGBA palette, that's stored in bands (which is what the python layer uses -- the C layer seems to use interleaved palettes), then the fitst 768 bytes will be the RGB palette, and the last 256 would be the tRNS header. I'll take a look and see if that's a plausible way to handle it. And hey, it should work: PngImagePlugin.py:586. As long as info['transparency'] and encoderinfo['transparency'] are none, that should work. It's tested here: Tests/test_file_png.py:164. 

Sorry, I've mistaken `PA` for a palette with RGBA entries. I've actually meant the latter — `P` with alpha suitable for PNG with the `tRNS` chunk.

``` python
Image.open("in.png").convert("RGBA").convert("P").save("out.png")
```

doesn't preserve the alpha either. I get the desired result with:

``` python
Image.open("in.png").convert("RGBA").quantize(method=2).save("out.png")
```

so maybe `convert("P")` could behave like `quantize(method=2)` when source is RGBA?

I think it's reasonable for RGBA converting to P to use quantize with sane defaults. I think it's also a good idea for an RGBA image to default to quantizing with FastOctree, since there's no other current option. 

@wiredfool Do you have any code for this issue, assuming code is needed? I'm not sure I fully follow the discussion.

In my head. I think it's just a quick couple of bits, but that impression was from a week ago. I should have done it before it swapped out of my head. 


Resolving patch:
diff --git a/PIL/Image.py b/PIL/Image.py
--- a/PIL/Image.py
+++ b/PIL/Image.py
@@ -735,6 +735,9 @@ 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)
+
         if mode == "P" and palette == ADAPTIVE:
             im = self.im.quantize(colors)
             new = self._new(im)
@@ -762,7 +765,7 @@ def convert(self, mode=None, matrix=None, dither=None,
 
         return self._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 +776,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,13 @@ 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)
+               
     
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))