Repository: python-pillow/Pillow

Issue:
Fix #2782
Fixes #2782


Cannot emit sRGB-chunk
### What did you do?
I tried to add an sRGB-chunk via PngInfo to the PNG image. (Script at the bottom).

### What did you expect to happen?
```
File: /mnt/c/Users/stuxc/Desktop/Download_srgb3.png (1045 bytes)
  chunk IHDR at offset 0x0000c, length 13
    640 x 480 image, 24-bit RGB, non-interlaced
  chunk sRGB at offset 0x00025, length 1
    rendering intent = absolute colorimetric
  chunk gAMA at offset 0x00032, length 4: 0.45455
  chunk cHRM at offset 0x00042, length 32
    White x = 0.3127 y = 0.329,  Red x = 0.64 y = 0.33
    Green x = 0.3 y = 0.6,  Blue x = 0.15 y = 0.06
  chunk IDAT at offset 0x0006e, length 915
    zlib: deflated, 32K window, default compression
  chunk IEND at offset 0x0040d, length 0
No errors detected in /mnt/c/Users/stuxc/Desktop/Download_srgb3.png (6 chunks, 99.9% compression).
```
Note the sRGB chunk at offset 0x00025

### What actually happened?
```
File: /mnt/c/Users/stuxc/Desktop/Download_srgb5.png (1032 bytes)
  chunk IHDR at offset 0x0000c, length 13
    640 x 480 image, 24-bit RGB, non-interlaced
  chunk gAMA at offset 0x00025, length 4: 0.45455
  chunk cHRM at offset 0x00035, length 32
    White x = 0.3127 y = 0.329,  Red x = 0.64 y = 0.33
    Green x = 0.3 y = 0.6,  Blue x = 0.15 y = 0.06
  chunk IDAT at offset 0x00061, length 915
    zlib: deflated, 32K window, default compression
  chunk IEND at offset 0x00400, length 0
No errors detected in /mnt/c/Users/stuxc/Desktop/Download_srgb5.png (5 chunks, 99.9% compression).
```

Both outputs were created by `pngcheck -vt <Filename>`

### What versions of Pillow and Python are you using?
`Pillow==4.3.0` affected code is also in the current master branch.
Python 3.6.2 using Windows Subsystem for Linux (Windows 10 Build 15063)

I think the error is located at:
https://github.com/python-pillow/Pillow/blob/master/PIL/PngImagePlugin.py#L732
This removes the sRGB-chunk from the list of valid chunks if (and only if) there is no iCCP-chunk given to the save function.

### Script that replicates the bug
```python
import struct
from PIL import Image
from PIL.PngImagePlugin import PngInfo

GAMA_SRGB = struct.pack('!I', 45455)
CHRM_SRGB = struct.pack("!8I", 31270, 32900, 64000, 33000, 30000, 60000, 15000, 6000)

SRGB_PNGINFO = PngInfo()
SRGB_PNGINFO.add(b"sRGB", b'\3')
SRGB_PNGINFO.add(b"gAMA", GAMA_SRGB)
SRGB_PNGINFO.add(b"cHRM", CHRM_SRGB)

image = Image.new('RGB', (640, 480))
with open("Download_srgb5.png", "wb") as f:
    image.save(f, format="png", compress_level=6, pnginfo=SRGB_PNGINFO)
```

### Significance
Chrome 61 expects images to have a color profile embedded in them. In my case, it is of vital importance that the generated images are being shown using the sRGB color profile.


Discussion:

That was added here: https://github.com/python-pillow/Pillow/pull/2347/files

The spec says: 

> When the sRGB chunk is present, it is recommended that decoders that recognize it and are capable of colour management [ICC] ignore the gAMA and cHRM chunks and use the sRGB chunk instead. Decoders that recognize the sRGB chunk but are not capable of colour management [ICC] are recommended to ignore the gAMA and cHRM chunks, and use the values given above as if they had appeared in gAMA and cHRM chunks.

> It is recommended that the sRGB and iCCP chunks do not both appear in a PNG datastream

So, it appears in the spec that the proper set is (iCCP | sRGB) + gAMA + cHRM. It looks like we're getting that exactly backwards. 


Resolving patch:
diff --git a/PIL/PngImagePlugin.py b/PIL/PngImagePlugin.py
--- a/PIL/PngImagePlugin.py
+++ b/PIL/PngImagePlugin.py
@@ -387,12 +387,31 @@ def chunk_tRNS(self, pos, length):
         return s
 
     def chunk_gAMA(self, pos, length):
-
         # gamma setting
         s = ImageFile._safe_read(self.fp, length)
         self.im_info["gamma"] = i32(s) / 100000.0
         return s
 
+    def chunk_cHRM(self, pos, length):
+        # chromaticity, 8 unsigned ints, actual value is scaled by 100,000
+        # WP x,y, Red x,y, Green x,y Blue x,y
+
+        s = ImageFile._safe_read(self.fp, length)
+        raw_vals = struct.unpack('>%dI' % (len(s) // 4), s)
+        self.im_info['chromaticity'] = tuple(elt/100000.0 for elt in raw_vals)
+        return s
+
+    def chunk_sRGB(self, pos, length):
+        # srgb rendering intent, 1 byte
+        # 0 perceptual
+        # 1 relative colorimetric
+        # 2 saturation
+        # 3 absolute colorimetric
+
+        s = ImageFile._safe_read(self.fp, length)
+        self.im_info['srgb'] = i8(s)
+        return s
+
     def chunk_pHYs(self, pos, length):
 
         # pixels per unit
@@ -731,7 +750,9 @@ def _save(im, fp, filename, chunk=putchunk):
         name = b"ICC Profile"
         data = name + b"\0\0" + zlib.compress(icc)
         chunk(fp, b"iCCP", data)
-    else:
+        
+        # You must either have sRGB or iCCP.
+        # Disallow sRGB chunks when an iCCP-chunk has been emitted.
         chunks.remove(b"sRGB")
 
     info = im.encoderinfo.get("pnginfo")


Test patch:
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -49,6 +49,21 @@ def setUp(self):
         if "zip_encoder" not in codecs or "zip_decoder" not in codecs:
             self.skipTest("zip/deflate support not available")
 
+    def get_chunks(self, filename):
+        chunks = []
+        with open(filename, "rb") as fp:
+            fp.read(8)
+            with PngImagePlugin.PngStream(fp) as png:
+                while True:
+                    cid, pos, length = png.read()
+                    chunks.append(cid)
+                    try:
+                        s = png.call(cid, pos, length)
+                    except EOFError:
+                        break
+                    png.crc(cid, s)
+        return chunks
+
     def test_sanity(self):
 
         # internal version number
@@ -501,18 +516,7 @@ def test_chunk_order(self):
         test_file = self.tempfile("temp.png")
         im.convert("P").save(test_file, dpi=(100, 100))
 
-        chunks = []
-        with open(test_file, "rb") as fp:
-            fp.read(8)
-            with PngImagePlugin.PngStream(fp) as png:
-                while True:
-                    cid, pos, length = png.read()
-                    chunks.append(cid)
-                    try:
-                        s = png.call(cid, pos, length)
-                    except EOFError:
-                        break
-                    png.crc(cid, s)
+        chunks = self.get_chunks(test_file)
 
         # https://www.w3.org/TR/PNG/#5ChunkOrdering
         # IHDR - shall be first