Merge pull request #837 from wiredfool/cms-profile

Return Profile with Transformed Images
This commit is contained in:
Hugo 2014-08-03 09:22:03 +03:00
commit 3fa247fd7c
3 changed files with 102 additions and 8 deletions

View File

@ -150,8 +150,13 @@ for flag in FLAGS.values():
class ImageCmsProfile:
def __init__(self, profile):
# accepts a string (filename), a file-like object, or a low-level
# profile object
"""
:param profile: Either a string representing a filename,
a file like object containing a profile or a
low-level profile object
"""
if isStringType(profile):
self._set(core.profile_open(profile), profile)
elif hasattr(profile, "read"):
@ -169,12 +174,23 @@ class ImageCmsProfile:
self.product_name = None
self.product_info = None
def tobytes(self):
"""
Returns the profile in a format suitable for embedding in
saved images.
:returns: a bytes object containing the ICC profile.
"""
return core.profile_tobytes(self.profile)
class ImageCmsTransform(Image.ImagePointHandler):
"""Transform. This can be used with the procedural API, or with the
standard Image.point() method.
"""
# Transform. This can be used with the procedural API, or with the
# standard Image.point() method.
#
# Will return the output profile in the output.info['icc_profile'].
def __init__(self, input, output, input_mode, output_mode,
intent=INTENT_PERCEPTUAL, proof=None,
@ -197,6 +213,8 @@ class ImageCmsTransform(Image.ImagePointHandler):
self.input_mode = self.inputMode = input_mode
self.output_mode = self.outputMode = output_mode
self.output_profile = output
def point(self, im):
return self.apply(im)
@ -205,6 +223,7 @@ class ImageCmsTransform(Image.ImagePointHandler):
if imOut is None:
imOut = Image.new(self.output_mode, im.size, None)
self.transform.apply(im.im.id, imOut.im.id)
imOut.info['icc_profile'] = self.output_profile.tobytes()
return imOut
def apply_in_place(self, im):
@ -212,6 +231,7 @@ class ImageCmsTransform(Image.ImagePointHandler):
if im.mode != self.output_mode:
raise ValueError("mode mismatch") # wrong output mode
self.transform.apply(im.im.id, im.im.id)
im.info['icc_profile'] = self.output_profile.tobytes()
return im
@ -570,7 +590,7 @@ def applyTransform(im, transform, inPlace=0):
with the transform applied is returned (and im is not changed). The
default is False.
:returns: Either None, or a new PIL Image object, depending on the value of
inPlace
inPlace. The profile will be returned in the image's info['icc_profile'].
:exception PyCMSError:
"""

View File

@ -2,8 +2,11 @@ from helper import unittest, PillowTestCase, lena
from PIL import Image
from io import BytesIO
try:
from PIL import ImageCms
from PIL.ImageCms import ImageCmsProfile
ImageCms.core.profile_open
except ImportError as v:
# Skipped via setUp()
@ -118,7 +121,7 @@ class TestImageCms(PillowTestCase):
def test_extensions(self):
# extensions
from io import BytesIO
i = Image.open("Tests/images/rgb.jpg")
p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"]))
self.assertEqual(
@ -196,6 +199,11 @@ class TestImageCms(PillowTestCase):
# img_srgb.save('temp.srgb.tif') # visually verified vs ps.
self.assert_image_similar(lena(), img_srgb, 30)
self.assertTrue(img_srgb.info['icc_profile'])
profile = ImageCmsProfile(BytesIO(img_srgb.info['icc_profile']))
self.assertTrue('sRGB' in ImageCms.getProfileDescription(profile))
def test_lab_roundtrip(self):
# check to see if we're at least internally consistent.
@ -205,11 +213,32 @@ class TestImageCms(PillowTestCase):
t2 = ImageCms.buildTransform(pLab, SRGB, "LAB", "RGB")
i = ImageCms.applyTransform(lena(), t)
self.assertEqual(i.info['icc_profile'],
ImageCmsProfile(pLab).tobytes())
out = ImageCms.applyTransform(i, t2)
self.assert_image_similar(lena(), out, 2)
def test_profile_tobytes(self):
from io import BytesIO
i = Image.open("Tests/images/rgb.jpg")
p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"]))
p2 = ImageCms.getOpenProfile(BytesIO(p.tobytes()))
# not the same bytes as the original icc_profile,
# but it does roundtrip
self.assertEqual(p.tobytes(),p2.tobytes())
self.assertEqual(ImageCms.getProfileName(p),
ImageCms.getProfileName(p2))
self.assertEqual(ImageCms.getProfileDescription(p),
ImageCms.getProfileDescription(p2))
if __name__ == '__main__':
unittest.main()

View File

@ -49,7 +49,8 @@ http://www.cazabon.com\n\
/* known to-do list with current version:
Verify that PILmode->littleCMStype conversion in findLCMStype is correct for all PIL modes (it probably isn't for the more obscure ones)
Verify that PILmode->littleCMStype conversion in findLCMStype is correct for all
PIL modes (it probably isn't for the more obscure ones)
Add support for creating custom RGB profiles on the fly
Add support for checking presence of a specific tag in a profile
@ -134,6 +135,49 @@ cms_profile_fromstring(PyObject* self, PyObject* args)
return cms_profile_new(hProfile);
}
static PyObject*
cms_profile_tobytes(PyObject* self, PyObject* args)
{
char *pProfile =NULL;
cmsUInt32Number nProfile;
PyObject* CmsProfile;
cmsHPROFILE *profile;
PyObject* ret;
if (!PyArg_ParseTuple(args, "O", &CmsProfile)){
return NULL;
}
profile = ((CmsProfileObject*)CmsProfile)->profile;
if (!cmsSaveProfileToMem(profile, pProfile, &nProfile)) {
PyErr_SetString(PyExc_IOError, "Could not determine profile size");
return NULL;
}
pProfile = (char*)malloc(nProfile);
if (!pProfile) {
PyErr_SetString(PyExc_IOError, "Out of Memory");
return NULL;
}
if (!cmsSaveProfileToMem(profile, pProfile, &nProfile)) {
PyErr_SetString(PyExc_IOError, "Could not get profile");
free(pProfile);
return NULL;
}
#if PY_VERSION_HEX >= 0x03000000
ret = PyBytes_FromStringAndSize(pProfile, (Py_ssize_t)nProfile);
#else
ret = PyString_FromStringAndSize(pProfile, (Py_ssize_t)nProfile);
#endif
free(pProfile);
return ret;
}
static void
cms_profile_dealloc(CmsProfileObject* self)
{
@ -485,6 +529,7 @@ static PyMethodDef pyCMSdll_methods[] = {
{"profile_open", cms_profile_open, 1},
{"profile_frombytes", cms_profile_fromstring, 1},
{"profile_fromstring", cms_profile_fromstring, 1},
{"profile_tobytes", cms_profile_tobytes, 1},
/* profile and transform functions */
{"buildTransform", buildTransform, 1},