mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-02-04 13:40:54 +03:00
Merge pull request #2355 from gunjambi/imagecms-preserve-aux-channels
Imagecms preserve aux channels
This commit is contained in:
commit
3ea2599984
|
@ -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()
|
||||||
|
|
101
_imagingcms.c
101
_imagingcms.c
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user