Merge pull request #2355 from gunjambi/imagecms-preserve-aux-channels

Imagecms preserve aux channels
This commit is contained in:
wiredfool 2017-02-07 19:38:05 +00:00 committed by GitHub
commit 3ea2599984
2 changed files with 193 additions and 1 deletions

View File

@ -1,7 +1,7 @@
from helper import unittest, PillowTestCase, hopper from helper import unittest, PillowTestCase, hopper
import datetime import datetime
from PIL import Image from PIL import Image, ImageMode
from io import BytesIO from io import BytesIO
import os import os
@ -332,6 +332,97 @@ class TestImageCms(PillowTestCase):
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
ImageCms.ImageCmsProfile(1).tobytes() 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__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -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 static int
pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform)
{ {
@ -301,9 +392,19 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform)
Py_BEGIN_ALLOW_THREADS Py_BEGIN_ALLOW_THREADS
// transform color channels only
for (i = 0; i < im->ysize; i++) for (i = 0; i < im->ysize; i++)
cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize); 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 Py_END_ALLOW_THREADS
return 0; return 0;