Repository: python-pillow/Pillow

Issue:
[SgiImagePlugin] Save uncompressed SGI/BW/RGB/RGBA files
**Issue:**
SGI files are only readable

**Changes proposed in this pull request:**
-  Add a _save method for uncompressed version of SGI File (not RLE)
-  Able to save Image as .sgi, .bw, .rgb, .rgba
- Support "L", "RGB" and "RGBA" modes (like read method)

**Note:**
A future PR could be possible to add RLE feature



Discussion:

Resolving patch:
diff --git a/PIL/SgiImagePlugin.py b/PIL/SgiImagePlugin.py
--- a/PIL/SgiImagePlugin.py
+++ b/PIL/SgiImagePlugin.py
@@ -7,9 +7,12 @@
 # See "The SGI Image File Format (Draft version 0.97)", Paul Haeberli.
 # <ftp://ftp.sgi.com/graphics/SGIIMAGESPEC>
 #
+#
 # History:
+# 2016-16-10 mb   Add save method without compression
 # 1995-09-10 fl   Created
 #
+# Copyright (c) 2016 by Mickael Bonfill.
 # Copyright (c) 2008 by Karsten Hiddemann.
 # Copyright (c) 1997 by Secret Labs AB.
 # Copyright (c) 1995 by Fredrik Lundh.
@@ -19,10 +22,13 @@
 
 
 from PIL import Image, ImageFile, _binary
+import struct
+import os
 
-__version__ = "0.2"
+__version__ = "0.3"
 
 i8 = _binary.i8
+o8 = _binary.o8
 i16 = _binary.i16be
 
 
@@ -76,12 +82,79 @@ def _open(self):
         elif compression == 1:
             raise ValueError("SGI RLE encoding not supported")
 
+
+def _save(im, fp, filename):
+    if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L":
+        raise ValueError("Unsupported SGI image mode")
+
+    # Flip the image, since the origin of SGI file is the bottom-left corner
+    im = im.transpose(Image.FLIP_TOP_BOTTOM)
+    # Define the file as SGI File Format
+    magicNumber = 474
+    # Run-Length Encoding Compression - Unsupported at this time
+    rle = 0
+    # Byte-per-pixel precision, 1 = 8bits per pixel
+    bpc = 1
+    # Number of dimensions (x,y,z)
+    dim = 3
+    # X Dimension = width / Y Dimension = height
+    x, y = im.size
+    if im.mode == "L" and y == 1:
+        dim = 1
+    elif im.mode == "L":
+        dim = 2
+    # Z Dimension: Number of channels
+    z = len(im.mode)
+    if dim == 1 or dim == 2:
+        z = 1
+    # Minimum Byte value
+    pinmin = 0
+    # Maximum Byte value (255 = 8bits per pixel)
+    pinmax = 255
+    # Image name (79 characters max, truncated below in write)
+    imgName = os.path.splitext(os.path.basename(filename))[0]
+    if str is not bytes:
+        imgName = imgName.encode('ascii', 'ignore')
+    # Standard representation of pixel in the file
+    colormap = 0
+    fp.write(struct.pack('>h', magicNumber))
+    fp.write(o8(rle))
+    fp.write(o8(bpc))
+    fp.write(struct.pack('>H', dim))
+    fp.write(struct.pack('>H', x))
+    fp.write(struct.pack('>H', y))
+    fp.write(struct.pack('>H', z))
+    fp.write(struct.pack('>l', pinmin))
+    fp.write(struct.pack('>l', pinmax))
+
+    fp.write(struct.pack('4s', b'')) # dummy
+    fp.write(struct.pack('79s', imgName)) # truncates to 79 chars
+    fp.write(struct.pack('s', b'')) # force null byte after imgname
+    fp.write(struct.pack('>l', colormap))
+
+    fp.write(struct.pack('404s', b'')) # dummy
+
+    #assert we've got the right number of bands. 
+    if len(im.getbands()) != z:
+        raise ValueError("incorrect number of bands in SGI write: %s vs %s" %
+                         (z, len(im.getbands())))
+
+    for channel in im.split():
+        fp.write(channel.tobytes())
+
+    fp.close()
+
+
 #
 # registry
 
 Image.register_open(SgiImageFile.format, SgiImageFile, _accept)
-
+Image.register_save(SgiImageFile.format, _save)
+Image.register_mime(SgiImageFile.format, "image/sgi")
+Image.register_mime(SgiImageFile.format, "image/rgb")
 Image.register_extension(SgiImageFile.format, ".bw")
 Image.register_extension(SgiImageFile.format, ".rgb")
 Image.register_extension(SgiImageFile.format, ".rgba")
 Image.register_extension(SgiImageFile.format, ".sgi")
+
+# End of file


Test patch:
diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py
--- a/Tests/test_file_sgi.py
+++ b/Tests/test_file_sgi.py
@@ -45,6 +45,17 @@ def test_invalid_file(self):
                           lambda:
                           SgiImagePlugin.SgiImageFile(invalid_file))
 
+    def test_write(self):
+        def roundtrip(img):
+            out = self.tempfile('temp.sgi')
+            img.save(out, format='sgi')
+            reloaded = Image.open(out)
+            self.assert_image_equal(img, reloaded)
+
+        for mode in ('L', 'RGB', 'RGBA'):
+            roundtrip(hopper(mode))
+
+
 
 if __name__ == '__main__':
     unittest.main()