Commits

Mikhail Korobov committed 15e498a

Greyscale images support. Thanks @a-e-m for the code and test PSD files. Fix #13.

  • Participants
  • Parent commits b378b79

Comments (0)

Files changed (11)

 include README.rst
 include CHANGES.rst
 
-recursive-include src *.c *.pxd *.pyx
+recursive-include src *.c *.pxd *.pyx *.icc
 
     package_dir = {'': 'src'},
     packages = ['psd_tools', 'psd_tools.reader', 'psd_tools.decoder', 'psd_tools.user_api'],
+    package_data = {'psd_tools': ['icc_profiles/*.icc']},
     scripts=['bin/psd-tools.py'],
 
     classifiers=[

src/psd_tools/icc_profiles.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+import os
+
+GRAY_PATH = os.path.join(os.path.dirname(__file__), 'icc_profiles', 'Gray-CIE_L.icc')
+
+try:
+    from PIL import ImageCms
+    gray = ImageCms.ImageCmsProfile(GRAY_PATH)
+    sRGB = ImageCms.createProfile('sRGB')
+except ImportError:
+    gray = None
+    sRGB = None

src/psd_tools/icc_profiles/Gray-CIE_L.icc

Binary file added.

src/psd_tools/icc_profiles/Gray.icc

Binary file added.

src/psd_tools/user_api/pil_support.py

 import warnings
 from psd_tools.utils import be_array_from_bytes
 from psd_tools.constants import Compression, ChannelID, ColorMode, ImageResourceID
+from psd_tools import icc_profiles
 
 try:
     from PIL import Image, ImageCms
         merged_image = Image.merge('CMYK', [bands[key] for key in 'CMYK'])
         # colors are inverted in Photoshop CMYK images; invert them back
         merged_image = frombytes('CMYK', size, merged_image.tobytes(), 'raw', 'CMYK;I')
+    elif color_mode == ColorMode.GRAYSCALE:
+        merged_image = bands['L']
     else:
         raise NotImplementedError()
 
     if icc_profile is not None:
         try:
-            sRGB_profile = ImageCms.createProfile('sRGB')
-            merged_image = ImageCms.profileToProfile(merged_image, icc_profile, sRGB_profile, outputMode='RGB')
+            if color_mode in [ColorMode.RGB, ColorMode.CMYK]:
+                merged_image = ImageCms.profileToProfile(merged_image, icc_profile, icc_profiles.sRGB, outputMode='RGB')
+            elif color_mode == ColorMode.GRAYSCALE:
+                ImageCms.profileToProfile(merged_image, icc_profile, icc_profiles.gray, inPlace=True, outputMode='L')
+
         except ImageCms.PyCMSError as e:
             # PIL/Pillow/(old littlecms?) can't convert some ICC profiles
             warnings.warn(repr(e))
             return 'RGB'[channel_id]
         elif color_mode == ColorMode.CMYK:
             return 'CMYK'[channel_id]
+        elif color_mode == ColorMode.GRAYSCALE:
+            return 'L'[channel_id]
 
     except IndexError:
         # spot channel
 
 
 def _get_header_channel_ids(header):
-    channel_ids = None
+
     if header.color_mode == ColorMode.RGB:
         if header.number_of_channels == 3:
-            channel_ids = [0, 1, 2]
+            return [0, 1, 2]
         elif header.number_of_channels == 4:
-            channel_ids = [0, 1, 2, -1]
+            return [0, 1, 2, ChannelID.TRANSPARENCY_MASK]
 
     elif header.color_mode == ColorMode.CMYK:
         if header.number_of_channels == 4:
-            channel_ids = [0, 1, 2, 3]
+            return [0, 1, 2, 3]
         elif header.number_of_channels == 5:
             # XXX: how to distinguish
             # "4 CMYK + 1 alpha" and "4 CMYK + 1 spot"?
-            channel_ids = [0, 1, 2, 3, -1]
+            return [0, 1, 2, 3, ChannelID.TRANSPARENCY_MASK]
+
+    elif header.color_mode == ColorMode.GRAYSCALE:
+        if header.number_of_channels == 1:
+            return [0]
+        elif header.number_of_channels == 2:
+            return [0, ChannelID.TRANSPARENCY_MASK]
 
     else:
         warnings.warn("Unsupported color mode (%s)" % header.color_mode)
-    return channel_ids
 
 
 def _get_layer_channel_ids(layer):

tests/psd_files/gray0.psd

Added
New image

tests/psd_files/gray1.psd

Added
New image

tests/test_dimensions.py

     ('transparentbg.psd',       (100, 150)),
     ('transparentbg-gimp.psd',  (40, 40)),
     ('vector mask.psd',         (100, 150)),
+    ('gray0.psd',               (400, 359)),
+    ('gray1.psd',               (1800, 1200)),
 )
 
 BBOXES = (
     psd_res = dict((block.resource_id, block.data) for block in psd.image_resource_blocks)
     assert psd_res[ImageResourceID.RESOLUTION_INFO] == resolution
 
+
 @pytest.mark.parametrize(("filename", "size"), DIMENSIONS)
 def test_dimensions_api(filename, size):
     psd = PSDImage(decode_psd(filename))
 def test_bbox(filename, layer_index, bbox):
     psd = PSDImage(decode_psd(filename))
     layer = psd.layers[layer_index]
-    assert layer.bbox == bbox
+    assert layer.bbox == bbox

tests/test_pixels.py

 
 from psd_tools import PSDImage, Layer, Group
 
-from .utils import full_name
+from .utils import full_name, FuzzyInt
 
 PIXEL_COLORS = (
     # filename                  probe point    pixel value
     ('16bit5x5.psd', (1, 3), (46, 196, 104)),
 )
 
+PIXEL_COLORS_GRAYSCALE = (
+    # exact colors depend on Gray ICC profile chosen,
+    # so allow a wide range for some of the values
+    ('gray0.psd', (0, 0), (255, 0)),
+    ('gray0.psd', (70, 57), (FuzzyInt(5, 250), 255)),
+    ('gray0.psd', (322, 65), (FuzzyInt(5, 250), 190)),
+
+    ('gray1.psd', (0, 0), 255),
+    ('gray1.psd', (900, 500), 0),
+    ('gray1.psd', (400, 600), FuzzyInt(5, 250)),
+)
+
+
 LAYER_COLORS = (
     ('1layer.psd',  0,  (5, 5),       (0x27, 0xBA, 0x0F)),
     ('2layers.psd', 1,  (5, 5),       (0x27, 0xBA, 0x0F)),
     ('32bit5x5.psd', 1, (1, 3), (46, 196, 104, 255)),
 )
 
+LAYER_COLORS_GRAYSCALE = (
+    # gray0: layer 0 is shifted 35px to the right
+    ('gray0.psd', 0, (0, 0), (255, 0)),
+    ('gray0.psd', 0, (70-35, 57), (FuzzyInt(5, 250), 255)),
+    ('gray0.psd', 0, (322-35, 65), (FuzzyInt(5, 250), 190)),
+
+    # gray1: black ellipse
+    ('gray1.psd', 0, (0, 0), (0, 0)),
+    ('gray1.psd', 0, (500, 250), (0, 255)),
+
+    # gray1: grey ellipse
+    ('gray1.psd', 1, (0, 0), (FuzzyInt(5, 250), 0)),
+    ('gray1.psd', 1, (700, 500), (FuzzyInt(5, 250), 255)),
+
+    # gray1: background
+    ('gray1.psd', 2, (0, 0), 255),
+    ('gray1.psd', 2, (900, 500), 255),
+    ('gray1.psd', 2, (400, 600), 255),
+)
+
+
 def color_PIL(psd, point):
     im = psd.as_PIL()
     return im.getpixel(point)
 
+
 def color_pymaging(psd, point):
     im = psd.as_pymaging()
     return tuple(im.get_pixel(*point))
     psd = PSDImage.load(full_name(filename))
     assert color == get_color(psd, point)
 
+
 @pytest.mark.parametrize(["filename", "point", "color"], PIXEL_COLORS_32BIT)
 def test_composite_32bit(filename, point, color):
     psd = PSDImage.load(full_name(filename))
     assert color == color_PIL(psd, point)
 
+
 @pytest.mark.parametrize(["filename", "point", "color"], PIXEL_COLORS_16BIT)
 def test_composite_16bit(filename, point, color):
     psd = PSDImage.load(full_name(filename))
     assert color == color_PIL(psd, point)
 
-@pytest.mark.parametrize(["filename", "layer_num", "point", "color"], LAYER_COLORS_MULTIBYTE)
-def test_layer_colors_multibyte(filename, layer_num, point, color):
+
+@pytest.mark.parametrize(["filename", "point", "color"], PIXEL_COLORS_GRAYSCALE)
+def test_composite_grayscale(filename, point, color):
     psd = PSDImage.load(full_name(filename))
-    layer = psd.layers[layer_num]
-    assert color == color_PIL(layer, point)
+    assert color == color_PIL(psd, point)
 
 
 @pytest.mark.parametrize(["get_color"], BACKENDS)
     assert color == get_color(layer, point)
 
 
+@pytest.mark.parametrize(["filename", "layer_num", "point", "color"], LAYER_COLORS_MULTIBYTE)
+def test_layer_colors_multibyte(filename, layer_num, point, color):
+    psd = PSDImage.load(full_name(filename))
+    layer = psd.layers[layer_num]
+    assert color == color_PIL(layer, point)
+
+
+@pytest.mark.parametrize(["filename", "layer_num", "point", "color"], LAYER_COLORS_GRAYSCALE)
+def test_layer_colors_grayscale(filename, layer_num, point, color):
+    psd = PSDImage.load(full_name(filename))
+    layer = psd.layers[layer_num]
+    assert color == color_PIL(layer, point)
+
+
 @pytest.mark.parametrize(["filename", "point", "color"], PIXEL_COLORS + MASK_PIXEL_COLORS + TRANSPARENCY_PIXEL_COLORS)
 def test_layer_merging_size(filename, point, color):
     psd = PSDImage.load(full_name(filename))
     merged_image = psd.as_PIL_merged()
     assert merged_image.size == psd.as_PIL().size
 
+
 @pytest.mark.parametrize(["filename", "point", "color"], PIXEL_COLORS)
 def test_layer_merging_pixels(filename, point, color):
     psd = PSDImage.load(full_name(filename))
     assert color[:3] == merged_image.getpixel(point)[:3]
     assert merged_image.getpixel(point)[3] == 255 # alpha channel
 
+
 @pytest.mark.xfail
 @pytest.mark.parametrize(["filename", "point", "color"], TRANSPARENCY_PIXEL_COLORS)
 def test_layer_merging_pixels_transparency(filename, point, color):
 
 DATA_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'psd_files')
 
+
 def full_name(filename):
     return os.path.join(DATA_PATH, filename)
 
+
 def load_psd(filename):
     with open(full_name(filename), 'rb') as f:
         return psd_tools.reader.parse(f)
 
+
 def decode_psd(filename):
-    return psd_tools.decoder.parse(load_psd(filename))
+    return psd_tools.decoder.parse(load_psd(filename))
+
+
+# see http://lukeplant.me.uk/blog/posts/fuzzy-testing-with-assertnumqueries/
+class FuzzyInt(int):
+    def __new__(cls, lowest, highest):
+        obj = super(FuzzyInt, cls).__new__(cls, highest)
+        obj.lowest = lowest
+        obj.highest = highest
+        return obj
+
+    def __eq__(self, other):
+        return other >= self.lowest and other <= self.highest
+
+    def __repr__(self):
+        return str("[%d..%d]") % (self.lowest, self.highest)