Skip to content

Commit

Permalink
Merge pull request python-pillow#2355 from gunjambi/imagecms-preserve…
Browse files Browse the repository at this point in the history
…-aux-channels

Imagecms preserve aux channels
  • Loading branch information
wiredfool committed Feb 7, 2017
2 parents 2d271e2 + 4a1ad89 commit 3ea2599
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 1 deletion.
93 changes: 92 additions & 1 deletion Tests/test_imagecms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from helper import unittest, PillowTestCase, hopper
import datetime

from PIL import Image
from PIL import Image, ImageMode

from io import BytesIO
import os
Expand Down Expand Up @@ -332,6 +332,97 @@ def test_profile_typesafety(self):
with self.assertRaises(TypeError):
ImageCms.ImageCmsProfile(1).tobytes()

def assert_aux_channel_preserved(self, mode, transform_in_place, preserved_channel):
def create_test_image():
# set up test image with something interesting in the tested aux
# channel.
nine_grid_deltas = [
(-1, -1), (-1, 0), (-1, 1),
( 0, -1), ( 0, 0), ( 0, 1),
( 1, -1), ( 1, 0), ( 1, 1),
]
chans = []
bands = ImageMode.getmode(mode).bands
for band_ndx, band in enumerate(bands):
channel_type = 'L' # 8-bit unorm
channel_pattern = hopper(channel_type)

# paste pattern with varying offsets to avoid correlation
# potentially hiding some bugs (like channels getting mixed).
paste_offset = (
int(band_ndx / float(len(bands)) * channel_pattern.size[0]),
int(band_ndx / float(len(bands) * 2) * channel_pattern.size[1])
)
channel_data = Image.new(channel_type, channel_pattern.size)
for delta in nine_grid_deltas:
channel_data.paste(channel_pattern, tuple(paste_offset[c] + delta[c]*channel_pattern.size[c] for c in range(2)))
chans.append(channel_data)
return Image.merge(mode, chans)

source_image = create_test_image()
preserved_channel_ndx = source_image.getbands().index(preserved_channel)
source_image_aux = source_image.split()[preserved_channel_ndx]

# create some transform, it doesn't matter which one
source_profile = ImageCms.createProfile("sRGB")
destination_profile = ImageCms.createProfile("sRGB")
t = ImageCms.buildTransform(source_profile, destination_profile, inMode=mode, outMode=mode)

# apply transform
if transform_in_place:
ImageCms.applyTransform(source_image, t, inPlace=True)
result_image = source_image
else:
result_image = ImageCms.applyTransform(source_image, t, inPlace=False)
result_image_aux = result_image.split()[preserved_channel_ndx]

self.assert_image_equal(source_image_aux, result_image_aux)

def test_preserve_auxiliary_channels_rgba(self):
self.assert_aux_channel_preserved(mode='RGBA', transform_in_place=False, preserved_channel='A')

def test_preserve_auxiliary_channels_rgba_in_place(self):
self.assert_aux_channel_preserved(mode='RGBA', transform_in_place=True, preserved_channel='A')

def test_preserve_auxiliary_channels_rgbx(self):
self.assert_aux_channel_preserved(mode='RGBX', transform_in_place=False, preserved_channel='X')

def test_preserve_auxiliary_channels_rgbx_in_place(self):
self.assert_aux_channel_preserved(mode='RGBX', transform_in_place=True, preserved_channel='X')

def test_auxiliary_channels_isolated(self):
# test data in aux channels does not affect non-aux channels
aux_channel_formats = [
# format, profile, color-only format, source test image
('RGBA', 'sRGB', 'RGB', hopper('RGBA')),
('RGBX', 'sRGB', 'RGB', hopper('RGBX')),
('LAB', 'LAB', 'LAB', Image.open('Tests/images/hopper.Lab.tif')),
]
for src_format in aux_channel_formats:
for dst_format in aux_channel_formats:
for transform_in_place in [True, False]:
# inplace only if format doesn't change
if transform_in_place and src_format[0] != dst_format[0]:
continue

# convert with and without AUX data, test colors are equal
source_profile = ImageCms.createProfile(src_format[1])
destination_profile = ImageCms.createProfile(dst_format[1])
source_image = src_format[3]
test_transform = ImageCms.buildTransform(source_profile, destination_profile, inMode=src_format[0], outMode=dst_format[0])

# test conversion from aux-ful source
if transform_in_place:
test_image = source_image.copy()
ImageCms.applyTransform(test_image, test_transform, inPlace=True)
else:
test_image = ImageCms.applyTransform(source_image, test_transform, inPlace=False)

# reference conversion from aux-less source
reference_transform = ImageCms.buildTransform(source_profile, destination_profile, inMode=src_format[2], outMode=dst_format[2])
reference_image = ImageCms.applyTransform(source_image.convert(src_format[2]), reference_transform)

self.assert_image_equal(test_image.convert(dst_format[2]), reference_image)

if __name__ == '__main__':
unittest.main()
101 changes: 101 additions & 0 deletions _imagingcms.c
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,97 @@ findLCMStype(char* PILmode)
}
}

#define Cms_Min(a, b) ((a) < (b) ? (a) : (b))

static int
pyCMSgetAuxChannelChannel (cmsUInt32Number format, int auxChannelNdx)
{
int numColors = T_CHANNELS(format);
int numExtras = T_EXTRA(format);

if (T_SWAPFIRST(format) && T_DOSWAP(format)) {
// reverse order, before anything but last extra is shifted last
if (auxChannelNdx == numExtras - 1)
return numColors + numExtras - 1;
else
return numExtras - 2 - auxChannelNdx;
}
else if (T_SWAPFIRST(format)) {
// in order, after color channels, but last extra is shifted to first
if (auxChannelNdx == numExtras - 1)
return 0;
else
return numColors + 1 + auxChannelNdx;
}
else if (T_DOSWAP(format)) {
// reverse order, before anything
return numExtras - 1 - auxChannelNdx;
}
else {
// in order, after color channels
return numColors + auxChannelNdx;
}
}

static void
pyCMScopyAux (cmsHTRANSFORM hTransform, Imaging imDst, const Imaging imSrc)
{
cmsUInt32Number dstLCMSFormat;
cmsUInt32Number srcLCMSFormat;
int numSrcExtras;
int numDstExtras;
int numExtras;
int ySize;
int xSize;
int channelSize;
int srcChunkSize;
int dstChunkSize;
int e;

// trivially copied
if (imDst == imSrc)
return;

dstLCMSFormat = cmsGetTransformOutputFormat(hTransform);
srcLCMSFormat = cmsGetTransformInputFormat(hTransform);

// currently, all Pillow formats are chunky formats, but check it anyway
if (T_PLANAR(dstLCMSFormat) || T_PLANAR(srcLCMSFormat))
return;

// copy only if channel format is identical, except OPTIMIZED is ignored as it
// does not affect the aux channel
if (T_FLOAT(dstLCMSFormat) != T_FLOAT(srcLCMSFormat)
|| T_FLAVOR(dstLCMSFormat) != T_FLAVOR(srcLCMSFormat)
|| T_ENDIAN16(dstLCMSFormat) != T_ENDIAN16(srcLCMSFormat)
|| T_BYTES(dstLCMSFormat) != T_BYTES(srcLCMSFormat))
return;

numSrcExtras = T_EXTRA(srcLCMSFormat);
numDstExtras = T_EXTRA(dstLCMSFormat);
numExtras = Cms_Min(numSrcExtras, numDstExtras);
ySize = Cms_Min(imSrc->ysize, imDst->ysize);
xSize = Cms_Min(imSrc->xsize, imDst->xsize);
channelSize = T_BYTES(dstLCMSFormat);
srcChunkSize = (T_CHANNELS(srcLCMSFormat) + T_EXTRA(srcLCMSFormat)) * channelSize;
dstChunkSize = (T_CHANNELS(dstLCMSFormat) + T_EXTRA(dstLCMSFormat)) * channelSize;

for (e = 0; e < numExtras; ++e) {
int y;
int dstChannel = pyCMSgetAuxChannelChannel(dstLCMSFormat, e);
int srcChannel = pyCMSgetAuxChannelChannel(srcLCMSFormat, e);

for (y = 0; y < ySize; y++) {
int x;
char* pDstExtras = imDst->image[y] + dstChannel * channelSize;
const char* pSrcExtras = imSrc->image[y] + srcChannel * channelSize;

for (x = 0; x < xSize; x++)
memcpy(pDstExtras + x * dstChunkSize, pSrcExtras + x * srcChunkSize, channelSize);
}
}
}

static int
pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform)
{
Expand All @@ -301,9 +392,19 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform)

Py_BEGIN_ALLOW_THREADS

// transform color channels only
for (i = 0; i < im->ysize; i++)
cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize);

// lcms by default does nothing to the auxiliary channels leaving those
// unchanged. To do "the right thing" here, i.e. maintain identical results
// with and without inPlace, we replicate those channels to the output.
//
// As of lcms 2.8, a new cmsFLAGS_COPY_ALPHA flag is introduced which would
// do the same thing automagically. Unfortunately, lcms2.8 is not yet widely
// enough available on all platforms, so we polyfill it here for now.
pyCMScopyAux(hTransform, imOut, im);

Py_END_ALLOW_THREADS

return 0;
Expand Down

0 comments on commit 3ea2599

Please sign in to comment.