diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 7a2a57151..bde01d9d4 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,4 +1,5 @@ from helper import unittest, PillowTestCase, hopper +import datetime from PIL import Image @@ -254,6 +255,60 @@ class TestImageCms(PillowTestCase): self.assertEqual(ImageCms.getProfileDescription(p), ImageCms.getProfileDescription(p2)) + def test_extended_information(self): + o = ImageCms.getOpenProfile(SRGB) + p = o.profile + + self.assertEqual(p.attributes, 4294967296) + self.assertEqual(p.blue_colorant, ((0.14306640625, 0.06060791015625, 0.7140960693359375), (0.1558847490315394, 0.06603820639433387, 0.06060791015625))) + self.assertEqual(p.blue_primary, ((0.14306641366715667, 0.06060790921083026, 0.7140960805782015), (0.15588475410450106, 0.06603820408959558, 0.06060790921083026))) + self.assertEqual(p.chromatic_adaptation, (((1.04791259765625, 0.0229339599609375, -0.050201416015625), (0.02960205078125, 0.9904632568359375, -0.0170745849609375), (-0.009246826171875, 0.0150604248046875, 0.7517852783203125)), ((1.0267159024652783, 0.022470062342089134, 0.0229339599609375), (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875)))) + self.assertEqual(p.chromaticity, None) + self.assertEqual(p.clut, {0: (False, False, True), 1: (False, False, True), 2: (False, False, True), 3: (False, False, True)}) + self.assertEqual(p.color_space, 'RGB') + self.assertEqual(p.colorant_table, None) + self.assertEqual(p.colorant_table_out, None) + self.assertEqual(p.colorimetric_intent, None) + self.assertEqual(p.connection_space, 'XYZ ') + self.assertEqual(p.copyright, 'Copyright International Color Consortium, 2009') + self.assertEqual(p.creation_date, datetime.datetime(2009, 2, 27, 21, 36, 31)) + self.assertEqual(p.device_class, 'mntr') + self.assertEqual(p.green_colorant, ((0.3851470947265625, 0.7168731689453125, 0.097076416015625), (0.32119769927720654, 0.5978443449048152, 0.7168731689453125))) + self.assertEqual(p.green_primary, ((0.3851470888162112, 0.7168731974161346, 0.09707641738998518), (0.32119768793686687, 0.5978443567149709, 0.7168731974161346))) + self.assertEqual(p.header_flags, 0) + self.assertEqual(p.header_manufacturer, '\x00\x00\x00\x00') + self.assertEqual(p.header_model, '\x00\x00\x00\x00') + self.assertEqual(p.icc_measurement_condition, {'backing': (0.0, 0.0, 0.0), 'flare': 0.0, 'geo': 'unknown', 'observer': 1, 'illuminant_type': 'D65'}) + self.assertEqual(p.icc_version, 33554432) + self.assertEqual(p.icc_viewing_condition, None) + self.assertEqual(p.intent_supported, {0: (True, True, True), 1: (True, True, True), 2: (True, True, True), 3: (True, True, True)}) + self.assertEqual(p.is_matrix_shaper, True) + self.assertEqual(p.luminance, ((0.0, 80.0, 0.0), (0.0, 1.0, 80.0))) + self.assertEqual(p.manufacturer, None) + self.assertEqual(p.media_black_point, ((0.012054443359375, 0.0124969482421875, 0.01031494140625), (0.34573304157549234, 0.35842450765864337, 0.0124969482421875))) + self.assertEqual(p.media_white_point, ((0.964202880859375, 1.0, 0.8249053955078125), (0.3457029219802284, 0.3585375327567059, 1.0))) + self.assertEqual(p.media_white_point_temperature, 5000.722328847392) + self.assertEqual(p.model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB') + self.assertEqual(p.pcs, 'XYZ') + self.assertEqual(p.perceptual_rendering_intent_gamut, None) + self.assertEqual(p.product_copyright, 'Copyright International Color Consortium, 2009') + self.assertEqual(p.product_desc, 'sRGB IEC61966-2-1 black scaled') + self.assertEqual(p.product_description, 'sRGB IEC61966-2-1 black scaled') + self.assertEqual(p.product_manufacturer, '') + self.assertEqual(p.product_model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB') + self.assertEqual(p.profile_description, 'sRGB IEC61966-2-1 black scaled') + self.assertEqual(p.profile_id, b')\xf8=\xde\xaf\xf2U\xaexB\xfa\xe4\xca\x839\r') + self.assertEqual(p.red_colorant, ((0.436065673828125, 0.2224884033203125, 0.013916015625), (0.6484536316398539, 0.3308524880306778, 0.2224884033203125))) + self.assertEqual(p.red_primary, ((0.43606566581047446, 0.22248840582960838, 0.013916015621759925), (0.6484536250319214, 0.3308524944738204, 0.22248840582960838))) + self.assertEqual(p.rendering_intent, 0) + self.assertEqual(p.saturation_rendering_intent_gamut, None) + self.assertEqual(p.screening_description, None) + self.assertEqual(p.target, None) + self.assertEqual(p.technology, 'CRT ') + self.assertEqual(p.version, 2.0) + self.assertEqual(p.viewing_condition, 'Reference Viewing Condition in IEC 61966-2-1') + self.assertEqual(p.xcolor_space, 'RGB ') + if __name__ == '__main__': unittest.main() diff --git a/_imagingcms.c b/_imagingcms.c index 198900c2b..ad5b845e1 100644 --- a/_imagingcms.c +++ b/_imagingcms.c @@ -25,7 +25,11 @@ kevin@cazabon.com\n\ http://www.cazabon.com\n\ " +#include "wchar.h" + #include "Python.h" +#include "datetime.h" + #include "lcms2.h" #include "Imaging.h" #include "py3.h" @@ -521,6 +525,288 @@ cms_get_display_profile_win32(PyObject* self, PyObject* args) } #endif +/* -------------------------------------------------------------------- */ +/* Helper functions. */ + +static PyObject* +_profile_read_mlu(CmsProfileObject* self, cmsTagSignature info) +{ + PyObject *uni; + char *lc = "en"; + char *cc = cmsNoCountry; + cmsMLU *mlu; + cmsUInt32Number len; + wchar_t *buf; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + mlu = cmsReadTag(self->profile, info); + if (!mlu) { + Py_INCREF(Py_None); + return Py_None; + } + + len = cmsMLUgetWide(mlu, lc, cc, NULL, 0); + if (len == 0) { + Py_INCREF(Py_None); + return Py_None; + } + + buf = malloc(len); + if (!buf) { + PyErr_SetString(PyExc_IOError, "Out of Memory"); + return NULL; + } + /* Just in case the next call fails. */ + buf[0] = '\0'; + + cmsMLUgetWide(mlu, lc, cc, buf, len); + // buf contains additional junk after \0 + uni = PyUnicode_FromWideChar(buf, wcslen(buf)); + free(buf); + + return uni; +} + + +static PyObject* +_profile_read_int_as_string(cmsUInt32Number nr) +{ + PyObject* ret; + char buf[5]; + buf[0] = (char) ((nr >> 24) & 0xff); + buf[1] = (char) ((nr >> 16) & 0xff); + buf[2] = (char) ((nr >> 8) & 0xff); + buf[3] = (char) (nr & 0xff); + buf[4] = 0; + +#if PY_VERSION_HEX >= 0x03000000 + ret = PyUnicode_DecodeASCII(buf, 4, NULL); +#else + ret = PyString_FromStringAndSize(buf, 4); +#endif + return ret; +} + + +static PyObject* +_profile_read_signature(CmsProfileObject* self, cmsTagSignature info) +{ + unsigned int *sig; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + sig = (unsigned int *) cmsReadTag(self->profile, info); + if (!sig) { + Py_INCREF(Py_None); + return Py_None; + } + + return _profile_read_int_as_string(*sig); +} + +static PyObject* +_xyz_py(cmsCIEXYZ* XYZ) +{ + cmsCIExyY xyY; + cmsXYZ2xyY(&xyY, XYZ); + return Py_BuildValue("((d,d,d),(d,d,d))", XYZ->X, XYZ->Y, XYZ->Z, xyY.x, xyY.y, xyY.Y); +} + +static PyObject* +_xyz3_py(cmsCIEXYZ* XYZ) +{ + cmsCIExyY xyY[3]; + cmsXYZ2xyY(&xyY[0], &XYZ[0]); + cmsXYZ2xyY(&xyY[1], &XYZ[1]); + cmsXYZ2xyY(&xyY[2], &XYZ[2]); + + return Py_BuildValue("(((d,d,d),(d,d,d),(d,d,d)),((d,d,d),(d,d,d),(d,d,d)))", + XYZ[0].X, XYZ[0].Y, XYZ[0].Z, + XYZ[1].X, XYZ[1].Y, XYZ[1].Z, + XYZ[2].X, XYZ[2].Y, XYZ[2].Z, + xyY[0].x, xyY[0].y, xyY[0].Y, + xyY[1].x, xyY[1].y, xyY[1].Y, + xyY[2].x, xyY[2].y, xyY[2].Y); +} + +static PyObject* +_profile_read_ciexyz(CmsProfileObject* self, cmsTagSignature info, int multi) +{ + cmsCIEXYZ* XYZ; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + XYZ = (cmsCIEXYZ*) cmsReadTag(self->profile, info); + if (!XYZ) { + Py_INCREF(Py_None); + return Py_None; + } + if (multi) + return _xyz3_py(XYZ); + else + return _xyz_py(XYZ); +} + +static PyObject* +_profile_read_ciexyy_triple(CmsProfileObject* self, cmsTagSignature info) +{ + cmsCIExyYTRIPLE* triple; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + triple = (cmsCIExyYTRIPLE*) cmsReadTag(self->profile, info); + if (!triple) { + Py_INCREF(Py_None); + return Py_None; + } + + /* Note: lcms does all the heavy lifting and error checking (nr of + channels == 3). */ + return Py_BuildValue("((d,d,d),(d,d,d),(d,d,d)),", + triple->Red.x, triple->Red.y, triple->Red.Y, + triple->Green.x, triple->Green.y, triple->Green.Y, + triple->Blue.x, triple->Blue.y, triple->Blue.Y); +} + +static PyObject* +_profile_read_named_color_list(CmsProfileObject* self, cmsTagSignature info) +{ + cmsNAMEDCOLORLIST* ncl; + int i, n; + char name[cmsMAX_PATH]; + PyObject* result; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + ncl = (cmsNAMEDCOLORLIST*) cmsReadTag(self->profile, info); + if (ncl == NULL) { + Py_INCREF(Py_None); + return Py_None; + } + + n = cmsNamedColorCount(ncl); + result = PyList_New(n); + if (!result) { + Py_INCREF(Py_None); + return Py_None; + } + + for (i = 0; i < n; i++) { + PyObject* str; + cmsNamedColorInfo(ncl, i, name, NULL, NULL, NULL, NULL); + str = PyUnicode_FromString(name); + if (str == NULL) { + Py_DECREF(result); + Py_INCREF(Py_None); + return Py_None; + } + PyList_SET_ITEM(result, i, str); + } + + return result; +} + +static cmsBool _calculate_rgb_primaries(CmsProfileObject* self, cmsCIEXYZTRIPLE* result) +{ + double input[3][3] = { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } }; + cmsHPROFILE hXYZ; + cmsHTRANSFORM hTransform; + + /* http://littlecms2.blogspot.com/2009/07/less-is-more.html */ + + // double array of RGB values with max on each identitiy + hXYZ = cmsCreateXYZProfile(); + if (hXYZ == NULL) + return 0; + + // transform from our profile to XYZ using doubles for highest precision + hTransform = cmsCreateTransform(self->profile, TYPE_RGB_DBL, + hXYZ, TYPE_XYZ_DBL, + INTENT_RELATIVE_COLORIMETRIC, + cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE); + cmsCloseProfile(hXYZ); + if (hTransform == NULL) + return 0; + + cmsDoTransform(hTransform, (void*) input, result, 3); + cmsDeleteTransform(hTransform); + return 1; +} + +static cmsBool _check_intent(int clut, cmsHPROFILE hProfile, cmsUInt32Number Intent, cmsUInt32Number UsedDirection) +{ + if (clut) { + return cmsIsCLUT(hProfile, Intent, UsedDirection); + } + else { + return cmsIsIntentSupported(hProfile, Intent, UsedDirection); + } +} + +#define INTENTS 200 + +static PyObject* +_is_intent_supported(CmsProfileObject* self, int clut) +{ + PyObject* result; + int n; + int i; + cmsUInt32Number intent_ids[INTENTS]; + char *intent_descs[INTENTS]; + + result = PyDict_New(); + if (result == NULL) { + Py_INCREF(Py_None); + return Py_None; + } + + + n = cmsGetSupportedIntents(INTENTS, + intent_ids, + intent_descs); + for (i = 0; i < n; i++) { + int intent = (int) intent_ids[i]; + PyObject* id; + PyObject* entry; + + /* Only valid for ICC Intents (otherwise we read invalid memory in lcms cmsio1.c). */ + if (!(intent == INTENT_PERCEPTUAL || intent == INTENT_RELATIVE_COLORIMETRIC + || intent == INTENT_SATURATION || intent == INTENT_ABSOLUTE_COLORIMETRIC)) + continue; + + id = PyInt_FromLong((long) intent); + entry = Py_BuildValue("(OOO)", + _check_intent(clut, self->profile, intent, LCMS_USED_AS_INPUT) ? Py_True : Py_False, + _check_intent(clut, self->profile, intent, LCMS_USED_AS_OUTPUT) ? Py_True : Py_False, + _check_intent(clut, self->profile, intent, LCMS_USED_AS_PROOF) ? Py_True : Py_False); + if (id == NULL || entry == NULL) { + Py_XDECREF(id); + Py_XDECREF(entry); + Py_XDECREF(result); + Py_INCREF(Py_None); + return Py_None; + } + PyDict_SetItem(result, id, entry); + } + return result; +} + /* -------------------------------------------------------------------- */ /* Python interface setup */ @@ -621,19 +907,479 @@ cms_profile_getattr_color_space(CmsProfileObject* self, void* closure) return PyUnicode_DecodeFSDefault(findICmode(cmsGetColorSpace(self->profile))); } -/* FIXME: add more properties (creation_datetime etc) */ +/* New-style unicode interfaces. */ +static PyObject* +cms_profile_getattr_copyright(CmsProfileObject* self, void* closure) +{ + return _profile_read_mlu(self, cmsSigCopyrightTag); +} + +static PyObject* +cms_profile_getattr_target(CmsProfileObject* self, void* closure) +{ + return _profile_read_mlu(self, cmsSigCharTargetTag); +} + +static PyObject* +cms_profile_getattr_manufacturer(CmsProfileObject* self, void* closure) +{ + return _profile_read_mlu(self, cmsSigDeviceMfgDescTag); +} + +static PyObject* +cms_profile_getattr_model(CmsProfileObject* self, void* closure) +{ + return _profile_read_mlu(self, cmsSigDeviceModelDescTag); +} + +static PyObject* +cms_profile_getattr_profile_description(CmsProfileObject* self, void* closure) +{ + return _profile_read_mlu(self, cmsSigProfileDescriptionTag); +} + +static PyObject* +cms_profile_getattr_screening_description(CmsProfileObject* self, void* closure) +{ + return _profile_read_mlu(self, cmsSigScreeningDescTag); +} + +static PyObject* +cms_profile_getattr_viewing_condition(CmsProfileObject* self, void* closure) +{ + return _profile_read_mlu(self, cmsSigViewingCondDescTag); +} + +static PyObject* +cms_profile_getattr_creation_date(CmsProfileObject* self, void* closure) +{ + cmsBool result; + struct tm ct; + + result = cmsGetHeaderCreationDateTime(self->profile, &ct); + if (! result) { + Py_INCREF(Py_None); + return Py_None; + } + + return PyDateTime_FromDateAndTime(1900 + ct.tm_year, ct.tm_mon, ct.tm_mday, + ct.tm_hour, ct.tm_min, ct.tm_sec, 0); +} + +static PyObject* +cms_profile_getattr_version(CmsProfileObject* self, void* closure) +{ + cmsFloat64Number version = cmsGetProfileVersion(self->profile); + return PyFloat_FromDouble(version); +} + +static PyObject* +cms_profile_getattr_icc_version(CmsProfileObject* self, void* closure) +{ + return PyInt_FromLong((long) cmsGetEncodedICCversion(self->profile)); +} + +static PyObject* +cms_profile_getattr_attributes(CmsProfileObject* self, void* closure) +{ + cmsUInt64Number attr; + cmsGetHeaderAttributes(self->profile, &attr); +#ifdef _WIN32 + // Windows is weird this way. + return PyLong_FromLongLong((long long) attr); +#else + return PyInt_FromLong((long) attr); +#endif +} + +static PyObject* +cms_profile_getattr_header_flags(CmsProfileObject* self, void* closure) +{ + cmsUInt32Number flags = cmsGetHeaderFlags(self->profile); + return PyInt_FromLong(flags); +} + +static PyObject* +cms_profile_getattr_header_manufacturer(CmsProfileObject* self, void* closure) +{ + return _profile_read_int_as_string(cmsGetHeaderManufacturer(self->profile)); +} + +static PyObject* +cms_profile_getattr_header_model(CmsProfileObject* self, void* closure) +{ + return _profile_read_int_as_string(cmsGetHeaderModel(self->profile)); +} + +static PyObject* +cms_profile_getattr_device_class(CmsProfileObject* self, void* closure) +{ + return _profile_read_int_as_string(cmsGetDeviceClass(self->profile)); +} + +/* Duplicate of pcs, but uninterpreted. */ +static PyObject* +cms_profile_getattr_connection_space(CmsProfileObject* self, void* closure) +{ + return _profile_read_int_as_string(cmsGetPCS(self->profile)); +} + +/* Duplicate of color_space, but uninterpreted. */ +static PyObject* +cms_profile_getattr_xcolor_space(CmsProfileObject* self, void* closure) +{ + return _profile_read_int_as_string(cmsGetColorSpace(self->profile)); +} + +static PyObject* +cms_profile_getattr_profile_id(CmsProfileObject* self, void* closure) +{ + cmsUInt8Number id[16]; + cmsGetHeaderProfileID(self->profile, id); + return PyBytes_FromStringAndSize((char *) id, 16); +} + +static PyObject* +cms_profile_getattr_is_matrix_shaper(CmsProfileObject* self, void* closure) +{ + return PyBool_FromLong((long) cmsIsMatrixShaper(self->profile)); +} + +static PyObject* +cms_profile_getattr_technology(CmsProfileObject* self, void* closure) +{ + return _profile_read_signature(self, cmsSigTechnologyTag); +} + +static PyObject* +cms_profile_getattr_colorimetric_intent(CmsProfileObject* self, void* closure) +{ + return _profile_read_signature(self, cmsSigColorimetricIntentImageStateTag); +} + +static PyObject* +cms_profile_getattr_perceptual_rendering_intent_gamut(CmsProfileObject* self, void* closure) +{ + return _profile_read_signature(self, cmsSigPerceptualRenderingIntentGamutTag); +} + +static PyObject* +cms_profile_getattr_saturation_rendering_intent_gamut(CmsProfileObject* self, void* closure) +{ + return _profile_read_signature(self, cmsSigSaturationRenderingIntentGamutTag); +} + +static PyObject* +cms_profile_getattr_red_colorant(CmsProfileObject* self, void* closure) +{ + if (!cmsIsMatrixShaper(self->profile)) { + Py_INCREF(Py_None); + return Py_None; + } + return _profile_read_ciexyz(self, cmsSigRedColorantTag, 0); +} + + +static PyObject* +cms_profile_getattr_green_colorant(CmsProfileObject* self, void* closure) +{ + if (!cmsIsMatrixShaper(self->profile)) { + Py_INCREF(Py_None); + return Py_None; + } + return _profile_read_ciexyz(self, cmsSigGreenColorantTag, 0); +} + + +static PyObject* +cms_profile_getattr_blue_colorant(CmsProfileObject* self, void* closure) +{ + if (!cmsIsMatrixShaper(self->profile)) { + Py_INCREF(Py_None); + return Py_None; + } + return _profile_read_ciexyz(self, cmsSigBlueColorantTag, 0); +} + +static PyObject* +cms_profile_getattr_media_white_point_temperature(CmsProfileObject *self, void* closure) +{ + cmsCIEXYZ* XYZ; + cmsCIExyY xyY; + cmsFloat64Number tempK; + cmsTagSignature info = cmsSigMediaWhitePointTag; + cmsBool result; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + XYZ = (cmsCIEXYZ*) cmsReadTag(self->profile, info); + if (!XYZ) { + Py_INCREF(Py_None); + return Py_None; + } + if (XYZ == NULL || XYZ->X == 0) { + Py_INCREF(Py_None); + return Py_None; + } + + cmsXYZ2xyY(&xyY, XYZ); + result = cmsTempFromWhitePoint(&tempK, &xyY); + if (!result) { + Py_INCREF(Py_None); + return Py_None; + } + return PyFloat_FromDouble(tempK); +} + +static PyObject* +cms_profile_getattr_media_white_point(CmsProfileObject* self, void* closure) +{ + return _profile_read_ciexyz(self, cmsSigMediaWhitePointTag, 0); +} + + +static PyObject* +cms_profile_getattr_media_black_point(CmsProfileObject* self, void* closure) +{ + return _profile_read_ciexyz(self, cmsSigMediaBlackPointTag, 0); +} + +static PyObject* +cms_profile_getattr_luminance(CmsProfileObject* self, void* closure) +{ + return _profile_read_ciexyz(self, cmsSigLuminanceTag, 0); +} + +static PyObject* +cms_profile_getattr_chromatic_adaptation(CmsProfileObject* self, void* closure) +{ + return _profile_read_ciexyz(self, cmsSigChromaticAdaptationTag, 1); +} + +static PyObject* +cms_profile_getattr_chromaticity(CmsProfileObject* self, void* closure) +{ + return _profile_read_ciexyy_triple(self, cmsSigChromaticityTag); +} + +static PyObject* +cms_profile_getattr_red_primary(CmsProfileObject* self, void* closure) +{ + cmsBool result = 0; + cmsCIEXYZTRIPLE primaries; + + if (cmsIsMatrixShaper(self->profile)) + result = _calculate_rgb_primaries(self, &primaries); + if (! result) { + Py_INCREF(Py_None); + return Py_None; + } + + return _xyz_py(&primaries.Red); +} + +static PyObject* +cms_profile_getattr_green_primary(CmsProfileObject* self, void* closure) +{ + cmsBool result = 0; + cmsCIEXYZTRIPLE primaries; + + if (cmsIsMatrixShaper(self->profile)) + result = _calculate_rgb_primaries(self, &primaries); + if (! result) { + Py_INCREF(Py_None); + return Py_None; + } + + return _xyz_py(&primaries.Green); +} + +static PyObject* +cms_profile_getattr_blue_primary(CmsProfileObject* self, void* closure) +{ + cmsBool result = 0; + cmsCIEXYZTRIPLE primaries; + + if (cmsIsMatrixShaper(self->profile)) + result = _calculate_rgb_primaries(self, &primaries); + if (! result) { + Py_INCREF(Py_None); + return Py_None; + } + + return _xyz_py(&primaries.Blue); +} + +static PyObject* +cms_profile_getattr_colorant_table(CmsProfileObject* self, void* closure) +{ + return _profile_read_named_color_list(self, cmsSigColorantTableTag); +} + +static PyObject* +cms_profile_getattr_colorant_table_out(CmsProfileObject* self, void* closure) +{ + return _profile_read_named_color_list(self, cmsSigColorantTableOutTag); +} + +static PyObject* +cms_profile_getattr_is_intent_supported (CmsProfileObject* self, void* closure) +{ + return _is_intent_supported(self, 0); +} + +static PyObject* +cms_profile_getattr_is_clut (CmsProfileObject* self, void* closure) +{ + return _is_intent_supported(self, 1); +} + +static const char* +_illu_map(int i) +{ + switch(i) { + case 0: + return "unknown"; + case 1: + return "D50"; + case 2: + return "D65"; + case 3: + return "D93"; + case 4: + return "F2"; + case 5: + return "D55"; + case 6: + return "A"; + case 7: + return "E"; + case 8: + return "F8"; + default: + return NULL; + } +} + +static PyObject* +cms_profile_getattr_icc_measurement_condition (CmsProfileObject* self, void* closure) +{ + cmsICCMeasurementConditions* mc; + cmsTagSignature info = cmsSigMeasurementTag; + const char *geo; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + mc = (cmsICCMeasurementConditions*) cmsReadTag(self->profile, info); + if (!mc) { + Py_INCREF(Py_None); + return Py_None; + } + + if (mc->Geometry == 1) + geo = "45/0, 0/45"; + else if (mc->Geometry == 2) + geo = "0d, d/0"; + else + geo = "unknown"; + + return Py_BuildValue("{s:i,s:(ddd),s:s,s:d,s:s}", + "observer", mc->Observer, + "backing", mc->Backing.X, mc->Backing.Y, mc->Backing.Z, + "geo", geo, + "flare", mc->Flare, + "illuminant_type", _illu_map(mc->IlluminantType)); +} + +static PyObject* +cms_profile_getattr_icc_viewing_condition (CmsProfileObject* self, void* closure) +{ + cmsICCViewingConditions* vc; + cmsTagSignature info = cmsSigViewingConditionsTag; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + vc = (cmsICCViewingConditions*) cmsReadTag(self->profile, info); + if (!vc) { + Py_INCREF(Py_None); + return Py_None; + } + + return Py_BuildValue("{s:(ddd),s:(ddd),s:s}", + "illuminant", vc->IlluminantXYZ.X, vc->IlluminantXYZ.Y, vc->IlluminantXYZ.Z, + "surround", vc->SurroundXYZ.X, vc->SurroundXYZ.Y, vc->SurroundXYZ.Z, + "illuminant_type", _illu_map(vc->IlluminantType)); +} + + static struct PyGetSetDef cms_profile_getsetters[] = { + /* Compatibility interfaces. */ { "product_desc", (getter) cms_profile_getattr_product_desc }, { "product_description", (getter) cms_profile_getattr_product_description }, { "product_manufacturer", (getter) cms_profile_getattr_product_manufacturer }, { "product_model", (getter) cms_profile_getattr_product_model }, { "product_copyright", (getter) cms_profile_getattr_product_copyright }, - { "rendering_intent", (getter) cms_profile_getattr_rendering_intent }, { "pcs", (getter) cms_profile_getattr_pcs }, { "color_space", (getter) cms_profile_getattr_color_space }, + + /* New style interfaces. */ + { "rendering_intent", (getter) cms_profile_getattr_rendering_intent }, + { "creation_date", (getter) cms_profile_getattr_creation_date }, + { "copyright", (getter) cms_profile_getattr_copyright }, + { "target", (getter) cms_profile_getattr_target }, + { "manufacturer", (getter) cms_profile_getattr_manufacturer }, + { "model", (getter) cms_profile_getattr_model }, + { "profile_description", (getter) cms_profile_getattr_profile_description }, + { "screening_description", (getter) cms_profile_getattr_screening_description }, + { "viewing_condition", (getter) cms_profile_getattr_viewing_condition }, + { "version", (getter) cms_profile_getattr_version }, + { "icc_version", (getter) cms_profile_getattr_icc_version }, + { "attributes", (getter) cms_profile_getattr_attributes }, + { "header_flags", (getter) cms_profile_getattr_header_flags }, + { "header_manufacturer", (getter) cms_profile_getattr_header_manufacturer }, + { "header_model", (getter) cms_profile_getattr_header_model }, + { "device_class", (getter) cms_profile_getattr_device_class }, + { "connection_space", (getter) cms_profile_getattr_connection_space }, + /* Similar to color_space, but with full 4-letter signature (including trailing whitespace). */ + { "xcolor_space", (getter) cms_profile_getattr_xcolor_space }, + { "profile_id", (getter) cms_profile_getattr_profile_id }, + { "is_matrix_shaper", (getter) cms_profile_getattr_is_matrix_shaper }, + { "technology", (getter) cms_profile_getattr_technology }, + { "colorimetric_intent", (getter) cms_profile_getattr_colorimetric_intent }, + { "perceptual_rendering_intent_gamut", (getter) cms_profile_getattr_perceptual_rendering_intent_gamut }, + { "saturation_rendering_intent_gamut", (getter) cms_profile_getattr_saturation_rendering_intent_gamut }, + { "red_colorant", (getter) cms_profile_getattr_red_colorant }, + { "green_colorant", (getter) cms_profile_getattr_green_colorant }, + { "blue_colorant", (getter) cms_profile_getattr_blue_colorant }, + { "red_primary", (getter) cms_profile_getattr_red_primary }, + { "green_primary", (getter) cms_profile_getattr_green_primary }, + { "blue_primary", (getter) cms_profile_getattr_blue_primary }, + { "media_white_point_temperature", (getter) cms_profile_getattr_media_white_point_temperature }, + { "media_white_point", (getter) cms_profile_getattr_media_white_point }, + { "media_black_point", (getter) cms_profile_getattr_media_black_point }, + { "luminance", (getter) cms_profile_getattr_luminance }, + { "chromatic_adaptation", (getter) cms_profile_getattr_chromatic_adaptation }, + { "chromaticity", (getter) cms_profile_getattr_chromaticity }, + { "colorant_table", (getter) cms_profile_getattr_colorant_table }, + { "colorant_table_out", (getter) cms_profile_getattr_colorant_table_out }, + { "intent_supported", (getter) cms_profile_getattr_is_intent_supported }, + { "clut", (getter) cms_profile_getattr_is_clut }, + { "icc_measurement_condition", (getter) cms_profile_getattr_icc_measurement_condition }, + { "icc_viewing_condition", (getter) cms_profile_getattr_icc_viewing_condition }, + { NULL } }; + static PyTypeObject CmsProfile_Type = { PyVarObject_HEAD_INIT(NULL, 0) "CmsProfile", sizeof(CmsProfileObject), 0, @@ -758,6 +1504,8 @@ PyInit__imagingcms(void) { if (setup_module(m) < 0) return NULL; + PyDateTime_IMPORT; + return m; } #else @@ -766,6 +1514,6 @@ init_imagingcms(void) { PyObject *m = Py_InitModule("_imagingcms", pyCMSdll_methods); setup_module(m); + PyDateTime_IMPORT; } #endif - diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 2d5bb1388..351d3dc2c 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -11,3 +11,412 @@ Cazabon's PyCMS library. .. automodule:: PIL.ImageCms :members: :noindex: + +CmsProfile +---------- + +The ICC color profiles are wrapped in an instance of the class +:py:class:`CmsProfile`. The specification ICC.1:2010 contains more +information about the meaning of the values in ICC profiles. + +For convenience, all XYZ-values are also given as xyY-values (so they +can be easily displayed in a chromaticity diagram, for example). + +.. py:class:: CmsProfile + + .. py:attribute:: creation_date + + Date and time this profile was first created (see 7.2.1 of ICC.1:2010). + + :type: :py:class:`datetime.datetime` or ``None`` + + .. py:attribute:: version + + The version number of the ICC standard that this profile follows + (e.g. `2.0`). + + :type: :py:class:`float` + + .. py:attribute:: icc_version + + Same as `version`, but in encoded format (see 7.2.4 of ICC.1:2010). + + .. py:attribute:: device_class + + 4-character string identifying the profile class. One of + ``scnr``, ``mntr``, ``prtr``, ``link``, ``spac``, ``abst``, + ``nmcl`` (see 7.2.5 of ICC.1:2010 for details). + + :type: :py:class:`string` + + .. py:attribute:: xcolor_space + + 4-character string (padded with whitespace) identifying the color + space, e.g. ``XYZ␣``, ``RGB␣`` or ``CMYK`` (see 7.2.6 of + ICC.1:2010 for details). + + Note that the deprecated attribute ``color_space`` contains an + interpreted (non-padded) variant of this (but can be empty on + unknown input). + + :type: :py:class:`string` + + .. py:attribute:: connection_space + + 4-character string (padded with whitespace) identifying the color + space on the B-side of the transform (see 7.2.7 of ICC.1:2010 for + details). + + Note that the deprecated attribute ``pcs`` contains an interpreted + (non-padded) variant of this (but can be empty on unknown input). + + :type: :py:class:`string` + + .. py:attribute:: header_flags + + The encoded header flags of the profile (see 7.2.11 of ICC.1:2010 + for details). + + :type: :py:class:`int` + + .. py:attribute:: header_manufacturer + + 4-character string (padded with whitespace) identifying the device + manufacturer, which shall match the signature contained in the + appropriate section of the ICC signature registry found at + www.color.org (see 7.2.12 of ICC.1:2010). + + :type: :py:class:`string` + + .. py:attribute:: header_model + + 4-character string (padded with whitespace) identifying the device + model, which shall match the signature contained in the + appropriate section of the ICC signature registry found at + www.color.org (see 7.2.13 of ICC.1:2010). + + :type: :py:class:`string` + + .. py:attribute:: attributes + + Flags used to identify attributes unique to the particular device + setup for which the profile is applicable (see 7.2.14 of + ICC.1:2010 for details). + + :type: :py:class:`int` + + .. py:attribute:: rendering_intent + + The rendering intent to use when combining this profile with + another profile (usually overridden at run-time, but provided here + for DeviceLink and embedded source profiles, see 7.2.15 of ICC.1:2010). + + One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, ``ImageCms.INTENT_PERCEPTUAL``, + ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and ``ImageCms.INTENT_SATURATION``. + + :type: :py:class:`int` + + .. py:attribute:: profile_id + + A sequence of 16 bytes identifying the profile (via a specially + constructed MD5 sum), or 16 binary zeroes if the profile ID has + not been calculated (see 7.2.18 of ICC.1:2010). + + :type: :py:class:`bytes` + + .. py:attribute:: copyright + + The text copyright information for the profile (see 9.2.21 of ICC.1:2010). + + :type: :py:class:`unicode` or ``None`` + + .. py:attribute:: manufacturer + + The (english) display string for the device manufacturer (see + 9.2.22 of ICC.1:2010). + + :type: :py:class:`unicode` or ``None`` + + .. py:attribute:: model + + The (english) display string for the device model of the device + for which this profile is created (see 9.2.23 of ICC.1:2010). + + :type: :py:class:`unicode` or ``None`` + + .. py:attribute:: profile_description + + The (english) display string for the profile description (see + 9.2.41 of ICC.1:2010). + + :type: :py:class:`unicode` or ``None`` + + .. py:attribute:: target + + The name of the registered characterization data set, or the + measurement data for a characterization target (see 9.2.14 of + ICC.1:2010). + + :type: :py:class:`unicode` or ``None`` + + .. py:attribute:: red_colorant + + The first column in the matrix used in matrix/TRC transforms (see 9.2.44 of ICC.1:2010). + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: green_colorant + + The second column in the matrix used in matrix/TRC transforms (see 9.2.30 of ICC.1:2010). + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: blue_colorant + + The third column in the matrix used in matrix/TRC transforms (see 9.2.4 of ICC.1:2010). + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: luminance + + The absolute luminance of emissive devices in candelas per square + metre as described by the Y channel (see 9.2.32 of ICC.1:2010). + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: chromaticity + + The data of the phosphor/colorant chromaticity set used (red, + green and blue channels, see 9.2.16 of ICC.1:2010). + + :type: ``((x, y, Y), (x, y, Y), (x, y, Y))`` or ``None`` + + .. py:attribute:: chromatic_adaption + + The chromatic adaption matrix converts a color measured using the + actual illumination conditions and relative to the actual adopted + white, to an color relative to the PCS adopted white, with + complete adaptation from the actual adopted white chromaticity to + the PCS adopted white chromaticity (see 9.2.15 of ICC.1:2010). + + Two matrices are returned, one in (X, Y, Z) space and one in (x, y, Y) space. + + :type: 2-tuple of 3-tuple, the first with (X, Y, Z) and the second with (x, y, Y) values + + .. py:attribute:: colorant_table + + This tag identifies the colorants used in the profile by a unique + name and set of PCSXYZ or PCSLAB values (see 9.2.19 of + ICC.1:2010). + + :type: list of strings + + .. py:attribute:: colorant_table_out + + This tag identifies the colorants used in the profile by a unique + name and set of PCSLAB values (for DeviceLink profiles only, see + 9.2.19 of ICC.1:2010). + + :type: list of strings + + .. py:attribute:: colorimetric_intent + + 4-character string (padded with whitespace) identifying the image + state of PCS colorimetry produced using the colorimetric intent + transforms (see 9.2.20 of ICC.1:2010 for details). + + :type: :py:class:`string` or ``None`` + + .. py:attribute:: perceptual_rendering_intent_gamut + + 4-character string (padded with whitespace) identifying the (one) + standard reference medium gamut (see 9.2.37 of ICC.1:2010 for + details). + + :type: :py:class:`string` or ``None`` + + .. py:attribute:: saturation_rendering_intent_gamut + + 4-character string (padded with whitespace) identifying the (one) + standard reference medium gamut (see 9.2.37 of ICC.1:2010 for + details). + + :type: :py:class:`string` or ``None`` + + .. py:attribute:: technology + + 4-character string (padded with whitespace) identifying the device + technology (see 9.2.47 of ICC.1:2010 for details). + + :type: :py:class:`string` or ``None`` + + .. py:attribute:: media_black_point + + This tag specifies the media black point and is used for + generating absolute colorimetry. + + This tag was available in ICC 3.2, but it is removed from + version 4. + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: media_white_point_temperature + + Calculates the white point temperature (see the LCMS documentation + for more information). + + :type: :py:class:`float` or `None` + + .. py:attribute:: viewing_condition + + The (english) display string for the viewing conditions (see + 9.2.48 of ICC.1:2010). + + :type: :py:class:`unicode` or ``None`` + + .. py:attribute:: screening_description + + The (english) display string for the screening conditions. + + This tag was available in ICC 3.2, but it is removed from + version 4. + + :type: :py:class:`unicode` or ``None`` + + .. py:attribute:: red_primary + + The XYZ-transformed of the RGB primary color red (1, 0, 0). + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: green_primary + + The XYZ-transformed of the RGB primary color green (0, 1, 0). + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: blue_primary + + The XYZ-transformed of the RGB primary color blue (0, 0, 1). + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: is_matrix_shaper + + True if this profile is implemented as a matrix shaper (see + documentation on LCMS). + + :type: :py:class:`bool` + + .. py:attribute:: clut + + Returns a dictionary of all supported intents and directions for + the CLUT model. + + The dictionary is indexed by intents + (``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, + ``ImageCms.INTENT_PERCEPTUAL``, + ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and + ``ImageCms.INTENT_SATURATION``). + + The values are 3-tuples indexed by directions + (``ImageCms.DIRECTION_INPUT``, ``ImageCms.DIRECTION_OUTPUT``, + ``ImageCms.DIRECTION_PROOF``). + + The elements of the tuple are booleans. If the value is ``True``, + that intent is supported for that direction. + + :type: :py:class:`dict` of boolean 3-tuples + + .. py:attribute:: intent_supported + + Returns a dictionary of all supported intents and directions. + + The dictionary is indexed by intents + (``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, + ``ImageCms.INTENT_PERCEPTUAL``, + ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and + ``ImageCms.INTENT_SATURATION``). + + The values are 3-tuples indexed by directions + (``ImageCms.DIRECTION_INPUT``, ``ImageCms.DIRECTION_OUTPUT``, + ``ImageCms.DIRECTION_PROOF``). + + The elements of the tuple are booleans. If the value is ``True``, + that intent is supported for that direction. + + :type: :py:class:`dict` of boolean 3-tuples + + .. py:attribute:: color_space + + Deprecated but retained for backwards compatibility. + Interpreted value of :py:attr:`.xcolor_space`. May be the + empty string if value could not be decoded. + + :type: :py:class:`string` + + .. py:attribute:: pcs + + Deprecated but retained for backwards compatibility. + Interpreted value of :py:attr:`.connection_space`. May be + the empty string if value could not be decoded. + + :type: :py:class:`string` + + .. py:attribute:: product_model + + Deprecated but retained for backwards compatibility. + ASCII-encoded value of :py:attr:`.model`. + + :type: :py:class:`string` + + .. py:attribute:: product_manufacturer + + Deprecated but retained for backwards compatibility. + ASCII-encoded value of :py:attr:`.manufacturer`. + + :type: :py:class:`string` + + .. py:attribute:: product_copyright + + Deprecated but retained for backwards compatibility. + ASCII-encoded value of :py:attr:`.copyright`. + + :type: :py:class:`string` + + .. py:attribute:: product_description + + Deprecated but retained for backwards compatibility. + ASCII-encoded value of :py:attr:`.profile_description`. + + :type: :py:class:`string` + + .. py:attribute:: product_desc + + Deprecated but retained for backwards compatibility. + ASCII-encoded value of :py:attr:`.profile_description`. + + This alias of :py:attr:`.product_description` used to + contain a derived informative string about the profile, + depending on the value of the description, copyright, + manufacturer and model fields). + + :type: :py:class:`string` + + There is one function defined on the class: + + .. py:method:: is_intent_supported(intent, direction) + + Returns if the intent is supported for the given direction. + + Note that you can also get this information for all intents and directions + with :py:attr:`.intent_supported`. + + :param intent: One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, + ``ImageCms.INTENT_PERCEPTUAL``, + ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` + and ``ImageCms.INTENT_SATURATION``. + :param direction: One of ``ImageCms.DIRECTION_INPUT``, + ``ImageCms.DIRECTION_OUTPUT`` + and ``ImageCms.DIRECTION_PROOF`` + :return: Boolean if the intent and direction is supported.