bool LcmsColorProfileContainer::init() { if (d->profile) cmsCloseProfile(d->profile); d->profile = cmsOpenProfileFromMem((void*)d->data->rawData().constData(), d->data->rawData().size()); #ifndef NDEBUG if (d->data->rawData().size() == 4096) { warnPigment << "Profile has a size of 4096, which is suspicious and indicates a possible misuse of QIODevice::read(int), check your code."; } #endif if (d->profile) { wchar_t buffer[_BUFFER_SIZE_]; d->colorSpaceSignature = cmsGetColorSpace(d->profile); d->deviceClass = cmsGetDeviceClass(d->profile); cmsGetProfileInfo(d->profile, cmsInfoDescription, cmsNoLanguage, cmsNoCountry, buffer, _BUFFER_SIZE_); d->productDescription = QString::fromWCharArray(buffer); d->valid = true; cmsGetProfileInfo(d->profile, cmsInfoModel, cmsNoLanguage, cmsNoCountry, buffer, _BUFFER_SIZE_); d->name = QString::fromWCharArray(buffer); cmsGetProfileInfo(d->profile, cmsInfoManufacturer, cmsNoLanguage, cmsNoCountry, buffer, _BUFFER_SIZE_); d->manufacturer = QString::fromWCharArray(buffer); // Check if the profile can convert (something->this) d->suitableForOutput = cmsIsMatrixShaper(d->profile) || ( cmsIsCLUT(d->profile, INTENT_PERCEPTUAL, LCMS_USED_AS_INPUT) && cmsIsCLUT(d->profile, INTENT_PERCEPTUAL, LCMS_USED_AS_OUTPUT) ); return true; } return false; }
/** * gimp_color_profile_is_linear: * @profile: a #GimpColorProfile * * This function determines is the ICC profile represented by a GimpColorProfile * is a linear RGB profile or not, some profiles that are LUTs though linear * will also return FALSE; * * Return value: %TRUE if the profile is a matrix shaping profile with linear * TRCs, %FALSE otherwise. * * Since: 2.10 **/ gboolean gimp_color_profile_is_linear (GimpColorProfile *profile) { cmsHPROFILE prof; cmsToneCurve *curve; g_return_val_if_fail (GIMP_IS_COLOR_PROFILE (profile), FALSE); prof = profile->priv->lcms_profile; if (! cmsIsMatrixShaper (prof)) return FALSE; if (cmsIsCLUT (prof, INTENT_PERCEPTUAL, LCMS_USED_AS_INPUT)) return FALSE; if (cmsIsCLUT (prof, INTENT_PERCEPTUAL, LCMS_USED_AS_OUTPUT)) return FALSE; curve = cmsReadTag(prof, cmsSigRedTRCTag); if (curve == NULL || ! cmsIsToneCurveLinear (curve)) return FALSE; curve = cmsReadTag (prof, cmsSigGreenTRCTag); if (curve == NULL || ! cmsIsToneCurveLinear (curve)) return FALSE; curve = cmsReadTag (prof, cmsSigBlueTRCTag); if (curve == NULL || ! cmsIsToneCurveLinear (curve)) return FALSE; return TRUE; }
// Return info about supported intents cmsBool CMSEXPORT cmsIsIntentSupported(cmsHPROFILE hProfile, cmsUInt32Number Intent, cmsUInt32Number UsedDirection) { if (cmsIsCLUT(hProfile, Intent, UsedDirection)) return TRUE; // Is there any matrix-shaper? If so, the intent is supported. This is a bit odd, since V2 matrix shaper // does not fully support relative colorimetric because they cannot deal with non-zero black points, but // many profiles claims that, and this is certainly not true for V4 profiles. Lets answer "yes" no matter // the accuracy would be less than optimal in rel.col and v2 case. return cmsIsMatrixShaper(hProfile); }
cmsPipeline* _cmsCreateGamutCheckPipeline(cmsContext ContextID, cmsHPROFILE hProfiles[], cmsBool BPC[], cmsUInt32Number Intents[], cmsFloat64Number AdaptationStates[], cmsUInt32Number nGamutPCSposition, cmsHPROFILE hGamut) { cmsHPROFILE hLab; cmsPipeline* Gamut; cmsStage* CLUT; cmsUInt32Number dwFormat; GAMUTCHAIN Chain; int nChannels, nGridpoints; cmsColorSpaceSignature ColorSpace; cmsUInt32Number i; cmsHPROFILE ProfileList[256]; cmsBool BPCList[256]; cmsFloat64Number AdaptationList[256]; cmsUInt32Number IntentList[256]; memset(&Chain, 0, sizeof(GAMUTCHAIN)); if (nGamutPCSposition <= 0 || nGamutPCSposition > 255) { cmsSignalError(ContextID, cmsERROR_RANGE, "Wrong position of PCS. 1..255 expected, %d found.", nGamutPCSposition); return NULL; } hLab = cmsCreateLab4ProfileTHR(ContextID, NULL); if (hLab == NULL) return NULL; // The figure of merit. On matrix-shaper profiles, should be almost zero as // the conversion is pretty exact. On LUT based profiles, different resolutions // of input and output CLUT may result in differences. if (cmsIsMatrixShaper(hGamut)) { Chain.Thereshold = 1.0; } else { Chain.Thereshold = ERR_THERESHOLD; } // Create a copy of parameters for (i=0; i < nGamutPCSposition; i++) { ProfileList[i] = hProfiles[i]; BPCList[i] = BPC[i]; AdaptationList[i] = AdaptationStates[i]; IntentList[i] = Intents[i]; } // Fill Lab identity ProfileList[nGamutPCSposition] = hLab; BPCList[nGamutPCSposition] = 0; AdaptationList[nGamutPCSposition] = 1.0; IntentList[nGamutPCSposition] = INTENT_RELATIVE_COLORIMETRIC; ColorSpace = cmsGetColorSpace(hGamut); nChannels = cmsChannelsOf(ColorSpace); nGridpoints = _cmsReasonableGridpointsByColorspace(ColorSpace, cmsFLAGS_HIGHRESPRECALC); dwFormat = (CHANNELS_SH(nChannels)|BYTES_SH(2)); // 16 bits to Lab double Chain.hInput = cmsCreateExtendedTransform(ContextID, nGamutPCSposition + 1, ProfileList, BPCList, IntentList, AdaptationList, NULL, 0, dwFormat, TYPE_Lab_DBL, cmsFLAGS_NOCACHE); // Does create the forward step. Lab double to device dwFormat = (CHANNELS_SH(nChannels)|BYTES_SH(2)); Chain.hForward = cmsCreateTransformTHR(ContextID, hLab, TYPE_Lab_DBL, hGamut, dwFormat, INTENT_RELATIVE_COLORIMETRIC, cmsFLAGS_NOCACHE); // Does create the backwards step Chain.hReverse = cmsCreateTransformTHR(ContextID, hGamut, dwFormat, hLab, TYPE_Lab_DBL, INTENT_RELATIVE_COLORIMETRIC, cmsFLAGS_NOCACHE); // All ok? if (Chain.hInput && Chain.hForward && Chain.hReverse) { // Go on, try to compute gamut LUT from PCS. This consist on a single channel containing // dE when doing a transform back and forth on the colorimetric intent. Gamut = cmsPipelineAlloc(ContextID, 3, 1); if (Gamut != NULL) { CLUT = cmsStageAllocCLut16bit(ContextID, nGridpoints, nChannels, 1, NULL); if (!cmsPipelineInsertStage(Gamut, cmsAT_BEGIN, CLUT)) { cmsPipelineFree(Gamut); Gamut = NULL; } else { cmsStageSampleCLut16bit(CLUT, GamutSampler, (void*) &Chain, 0); } } } else Gamut = NULL; // Didn't work... // Free all needed stuff. if (Chain.hInput) cmsDeleteTransform(Chain.hInput); if (Chain.hForward) cmsDeleteTransform(Chain.hForward); if (Chain.hReverse) cmsDeleteTransform(Chain.hReverse); if (hLab) cmsCloseProfile(hLab); // And return computed hull return Gamut; }
static int dt_colorspaces_get_matrix_from_profile (cmsHPROFILE prof, float *matrix, float *lutr, float *lutg, float* lutb, const int lutsize, const int input) { // create an OpenCL processable matrix + tone curves from an cmsHPROFILE: // check this first: if(!cmsIsMatrixShaper(prof)) return 1; cmsToneCurve* red_curve = cmsReadTag(prof, cmsSigRedTRCTag); cmsToneCurve* green_curve = cmsReadTag(prof, cmsSigGreenTRCTag); cmsToneCurve* blue_curve = cmsReadTag(prof, cmsSigBlueTRCTag); cmsCIEXYZ *red_color = cmsReadTag(prof, cmsSigRedColorantTag); cmsCIEXYZ *green_color = cmsReadTag(prof, cmsSigGreenColorantTag); cmsCIEXYZ *blue_color = cmsReadTag(prof, cmsSigBlueColorantTag); if(!red_curve || !green_curve || !blue_curve || !red_color || !green_color || !blue_color) return 2; matrix[0] = red_color->X; matrix[1] = green_color->X; matrix[2] = blue_color->X; matrix[3] = red_color->Y; matrix[4] = green_color->Y; matrix[5] = blue_color->Y; matrix[6] = red_color->Z; matrix[7] = green_color->Z; matrix[8] = blue_color->Z; // some camera ICC profiles claim to have color locations for red, green and blue base colors defined, // but in fact these are all set to zero. we catch this case here. float sum = 0.0f; for(int k=0; k<9; k++) sum += matrix[k]; if(sum == 0.0f) return 3; if(input) { // mark as linear, if they are: if(cmsIsToneCurveLinear(red_curve)) lutr[0] = -1.0f; else for(int k=0; k<lutsize; k++) lutr[k] = cmsEvalToneCurveFloat(red_curve, k/(lutsize-1.0f)); if(cmsIsToneCurveLinear(green_curve)) lutg[0] = -1.0f; else for(int k=0; k<lutsize; k++) lutg[k] = cmsEvalToneCurveFloat(green_curve, k/(lutsize-1.0f)); if(cmsIsToneCurveLinear(blue_curve)) lutb[0] = -1.0f; else for(int k=0; k<lutsize; k++) lutb[k] = cmsEvalToneCurveFloat(blue_curve, k/(lutsize-1.0f)); } else { // invert profile->XYZ matrix for output profiles float tmp[9]; memcpy(tmp, matrix, sizeof(float)*9); if(mat3inv (matrix, tmp)) return 3; // also need to reverse gamma, to apply reverse before matrix multiplication: cmsToneCurve* rev_red = cmsReverseToneCurveEx(0x8000, red_curve); cmsToneCurve* rev_green = cmsReverseToneCurveEx(0x8000, green_curve); cmsToneCurve* rev_blue = cmsReverseToneCurveEx(0x8000, blue_curve); if(!rev_red || !rev_green || !rev_blue) { cmsFreeToneCurve(rev_red); cmsFreeToneCurve(rev_green); cmsFreeToneCurve(rev_blue); return 4; } // pass on tonecurves, in case lutsize > 0: if(cmsIsToneCurveLinear(red_curve)) lutr[0] = -1.0f; else for(int k=0; k<lutsize; k++) lutr[k] = cmsEvalToneCurveFloat(rev_red, k/(lutsize-1.0f)); if(cmsIsToneCurveLinear(green_curve)) lutg[0] = -1.0f; else for(int k=0; k<lutsize; k++) lutg[k] = cmsEvalToneCurveFloat(rev_green, k/(lutsize-1.0f)); if(cmsIsToneCurveLinear(blue_curve)) lutb[0] = -1.0f; else for(int k=0; k<lutsize; k++) lutb[k] = cmsEvalToneCurveFloat(rev_blue, k/(lutsize-1.0f)); cmsFreeToneCurve(rev_red); cmsFreeToneCurve(rev_green); cmsFreeToneCurve(rev_blue); } return 0; }
bool LcmsColorProfileContainer::init() { if (d->profile) { cmsCloseProfile(d->profile); } d->profile = cmsOpenProfileFromMem((void *)d->data->rawData().constData(), d->data->rawData().size()); #ifndef NDEBUG if (d->data->rawData().size() == 4096) { qWarning() << "Profile has a size of 4096, which is suspicious and indicates a possible misuse of QIODevice::read(int), check your code."; } #endif if (d->profile) { wchar_t buffer[_BUFFER_SIZE_]; d->colorSpaceSignature = cmsGetColorSpace(d->profile); d->deviceClass = cmsGetDeviceClass(d->profile); cmsGetProfileInfo(d->profile, cmsInfoDescription, cmsNoLanguage, cmsNoCountry, buffer, _BUFFER_SIZE_); d->name = QString::fromWCharArray(buffer); //apparantly this should give us a localised string??? Not sure about this. cmsGetProfileInfo(d->profile, cmsInfoModel, cmsNoLanguage, cmsNoCountry, buffer, _BUFFER_SIZE_); d->productDescription = QString::fromWCharArray(buffer); cmsGetProfileInfo(d->profile, cmsInfoManufacturer, cmsNoLanguage, cmsNoCountry, buffer, _BUFFER_SIZE_); d->manufacturer = QString::fromWCharArray(buffer); cmsGetProfileInfo(d->profile, cmsInfoCopyright, cmsNoLanguage, cmsNoCountry, buffer, _BUFFER_SIZE_); d->copyright = QString::fromWCharArray(buffer); cmsProfileClassSignature profile_class; profile_class = cmsGetDeviceClass(d->profile); d->valid = (profile_class != cmsSigNamedColorClass); //This is where obtain the whitepoint, and convert it to the actual white point of the profile in the case a Chromatic adaption tag is //present. This is necessary for profiles following the v4 spec. cmsCIEXYZ baseMediaWhitePoint;//dummy to hold copy of mediawhitepoint if this is modified by chromatic adaption. if (cmsIsTag(d->profile, cmsSigMediaWhitePointTag)) { d->mediaWhitePoint = *((cmsCIEXYZ *)cmsReadTag(d->profile, cmsSigMediaWhitePointTag)); baseMediaWhitePoint = d->mediaWhitePoint; cmsXYZ2xyY(&d->whitePoint, &d->mediaWhitePoint); if (cmsIsTag(d->profile, cmsSigChromaticAdaptationTag)) { //the chromatic adaption tag represent a matrix from the actual white point of the profile to D50. cmsCIEXYZ *CAM1 = (cmsCIEXYZ *)cmsReadTag(d->profile, cmsSigChromaticAdaptationTag); //We first put all our data into structures we can manipulate. double d3dummy [3] = {d->mediaWhitePoint.X, d->mediaWhitePoint.Y, d->mediaWhitePoint.Z}; QGenericMatrix<1, 3, double> whitePointMatrix(d3dummy); QTransform invertDummy(CAM1[0].X, CAM1[0].Y, CAM1[0].Z, CAM1[1].X, CAM1[1].Y, CAM1[1].Z, CAM1[2].X, CAM1[2].Y, CAM1[2].Z); //we then abuse QTransform's invert function because it probably does matrix invertion 20 times better than I can program. //if the matrix is uninvertable, invertedDummy will be an identity matrix, which for us means that it won't give any noticeble //effect when we start multiplying. QTransform invertedDummy = invertDummy.inverted(); //we then put the QTransform into a generic 3x3 matrix. double d9dummy [9] = {invertedDummy.m11(), invertedDummy.m12(), invertedDummy.m13(), invertedDummy.m21(), invertedDummy.m22(), invertedDummy.m23(), invertedDummy.m31(), invertedDummy.m32(), invertedDummy.m33() }; QGenericMatrix<3, 3, double> chromaticAdaptionMatrix(d9dummy); //multiplying our inverted adaption matrix with the whitepoint gives us the right whitepoint. QGenericMatrix<1, 3, double> result = chromaticAdaptionMatrix * whitePointMatrix; //and then we pour the matrix into the whitepoint variable. Generic matrix does row/column for indices even though it //uses column/row for initialising. d->mediaWhitePoint.X = result(0, 0); d->mediaWhitePoint.Y = result(1, 0); d->mediaWhitePoint.Z = result(2, 0); cmsXYZ2xyY(&d->whitePoint, &d->mediaWhitePoint); } } //This is for RGB profiles, but it only works for matrix profiles. Need to design it to work with non-matrix profiles. if (cmsIsTag(d->profile, cmsSigRedColorantTag)) { cmsCIEXYZTRIPLE tempColorants; tempColorants.Red = *((cmsCIEXYZ *)cmsReadTag(d->profile, cmsSigRedColorantTag)); tempColorants.Green = *((cmsCIEXYZ *)cmsReadTag(d->profile, cmsSigGreenColorantTag)); tempColorants.Blue = *((cmsCIEXYZ *)cmsReadTag(d->profile, cmsSigBlueColorantTag)); //convert to d65, this is useless. cmsAdaptToIlluminant(&d->colorants.Red, &baseMediaWhitePoint, &d->mediaWhitePoint, &tempColorants.Red); cmsAdaptToIlluminant(&d->colorants.Green, &baseMediaWhitePoint, &d->mediaWhitePoint, &tempColorants.Green); cmsAdaptToIlluminant(&d->colorants.Blue, &baseMediaWhitePoint, &d->mediaWhitePoint, &tempColorants.Blue); //d->colorants = tempColorants; d->hasColorants = true; } else { //qDebug()<<d->name<<": has no colorants"; d->hasColorants = false; } //retrieve TRC. if (cmsIsTag(d->profile, cmsSigRedTRCTag) && cmsIsTag(d->profile, cmsSigBlueTRCTag) && cmsIsTag(d->profile, cmsSigGreenTRCTag)) { d->redTRC = ((cmsToneCurve *)cmsReadTag (d->profile, cmsSigRedTRCTag)); d->greenTRC = ((cmsToneCurve *)cmsReadTag (d->profile, cmsSigGreenTRCTag)); d->blueTRC = ((cmsToneCurve *)cmsReadTag (d->profile, cmsSigBlueTRCTag)); d->redTRCReverse = cmsReverseToneCurve(d->redTRC); d->greenTRCReverse = cmsReverseToneCurve(d->greenTRC); d->blueTRCReverse = cmsReverseToneCurve(d->blueTRC); d->hasTRC = true; } else if (cmsIsTag(d->profile, cmsSigGrayTRCTag)) { d->grayTRC = ((cmsToneCurve *)cmsReadTag (d->profile, cmsSigGrayTRCTag)); d->grayTRCReverse = cmsReverseToneCurve(d->grayTRC); d->hasTRC = true; } else { d->hasTRC = false; } // Check if the profile can convert (something->this) d->suitableForOutput = cmsIsMatrixShaper(d->profile) || (cmsIsCLUT(d->profile, INTENT_PERCEPTUAL, LCMS_USED_AS_INPUT) && cmsIsCLUT(d->profile, INTENT_PERCEPTUAL, LCMS_USED_AS_OUTPUT)); d->version = cmsGetProfileVersion(d->profile); d->defaultIntent = cmsGetHeaderRenderingIntent(d->profile); d->isMatrixShaper = cmsIsMatrixShaper(d->profile); d->isPerceptualCLUT = cmsIsCLUT(d->profile, INTENT_PERCEPTUAL, LCMS_USED_AS_INPUT); d->isSaturationCLUT = cmsIsCLUT(d->profile, INTENT_SATURATION, LCMS_USED_AS_INPUT); d->isAbsoluteCLUT = cmsIsCLUT(d->profile, INTENT_SATURATION, LCMS_USED_AS_INPUT); d->isRelativeCLUT = cmsIsCLUT(d->profile, INTENT_RELATIVE_COLORIMETRIC, LCMS_USED_AS_INPUT); return true; } return false; }
cmsBool CMSEXPORT cmsDetectBlackPoint(cmsCIEXYZ* BlackPoint, cmsHPROFILE hProfile, cmsUInt32Number Intent, cmsUInt32Number dwFlags) { // Zero for black point if (cmsGetDeviceClass(hProfile) == cmsSigLinkClass) { BlackPoint -> X = BlackPoint ->Y = BlackPoint -> Z = 0.0; return FALSE; } // v4 + perceptual & saturation intents does have its own black point, and it is // well specified enough to use it. Black point tag is deprecated in V4. if ((cmsGetEncodedICCversion(hProfile) >= 0x4000000) && (Intent == INTENT_PERCEPTUAL || Intent == INTENT_SATURATION)) { // Matrix shaper share MRC & perceptual intents if (cmsIsMatrixShaper(hProfile)) return BlackPointAsDarkerColorant(hProfile, INTENT_RELATIVE_COLORIMETRIC, BlackPoint, 0); // Get Perceptual black out of v4 profiles. That is fixed for perceptual & saturation intents BlackPoint -> X = cmsPERCEPTUAL_BLACK_X; BlackPoint -> Y = cmsPERCEPTUAL_BLACK_Y; BlackPoint -> Z = cmsPERCEPTUAL_BLACK_Z; return TRUE; } #ifdef CMS_USE_PROFILE_BLACK_POINT_TAG // v2, v4 rel/abs colorimetric if (cmsIsTag(hProfile, cmsSigMediaBlackPointTag) && Intent == INTENT_RELATIVE_COLORIMETRIC) { cmsCIEXYZ *BlackPtr, BlackXYZ, UntrustedBlackPoint, TrustedBlackPoint, MediaWhite; cmsCIELab Lab; // If black point is specified, then use it, BlackPtr = cmsReadTag(hProfile, cmsSigMediaBlackPointTag); if (BlackPtr != NULL) { BlackXYZ = *BlackPtr; _cmsReadMediaWhitePoint(&MediaWhite, hProfile); // Black point is absolute XYZ, so adapt to D50 to get PCS value cmsAdaptToIlluminant(&UntrustedBlackPoint, &MediaWhite, cmsD50_XYZ(), &BlackXYZ); // Force a=b=0 to get rid of any chroma cmsXYZ2Lab(NULL, &Lab, &UntrustedBlackPoint); Lab.a = Lab.b = 0; if (Lab.L > 50) Lab.L = 50; // Clip to L* <= 50 cmsLab2XYZ(NULL, &TrustedBlackPoint, &Lab); if (BlackPoint != NULL) *BlackPoint = TrustedBlackPoint; return TRUE; } } #endif // That is about v2 profiles. // If output profile, discount ink-limiting and that's all if (Intent == INTENT_RELATIVE_COLORIMETRIC && (cmsGetDeviceClass(hProfile) == cmsSigOutputClass) && (cmsGetColorSpace(hProfile) == cmsSigCmykData)) return BlackPointUsingPerceptualBlack(BlackPoint, hProfile); // Nope, compute BP using current intent. return BlackPointAsDarkerColorant(hProfile, Intent, BlackPoint, dwFlags); }
// Calculates the black point of a destination profile. // This algorithm comes from the Adobe paper disclosing its black point compensation method. cmsBool CMSEXPORT cmsDetectDestinationBlackPoint(cmsCIEXYZ* BlackPoint, cmsHPROFILE hProfile, cmsUInt32Number Intent, cmsUInt32Number dwFlags) { cmsColorSpaceSignature ColorSpace; cmsHTRANSFORM hRoundTrip = NULL; cmsCIELab InitialLab, destLab, Lab; cmsFloat64Number inRamp[256], outRamp[256]; cmsFloat64Number MinL, MaxL; cmsBool NearlyStraightMidrange = TRUE; cmsFloat64Number yRamp[256]; cmsFloat64Number x[256], y[256]; cmsFloat64Number lo, hi; int n, l; cmsProfileClassSignature devClass; // Make sure the device class is adequate devClass = cmsGetDeviceClass(hProfile); if (devClass == cmsSigLinkClass || devClass == cmsSigAbstractClass || devClass == cmsSigNamedColorClass) { BlackPoint -> X = BlackPoint ->Y = BlackPoint -> Z = 0.0; return FALSE; } // Make sure intent is adequate if (Intent != INTENT_PERCEPTUAL && Intent != INTENT_RELATIVE_COLORIMETRIC && Intent != INTENT_SATURATION) { BlackPoint -> X = BlackPoint ->Y = BlackPoint -> Z = 0.0; return FALSE; } // v4 + perceptual & saturation intents does have its own black point, and it is // well specified enough to use it. Black point tag is deprecated in V4. if ((cmsGetEncodedICCversion(hProfile) >= 0x4000000) && (Intent == INTENT_PERCEPTUAL || Intent == INTENT_SATURATION)) { // Matrix shaper share MRC & perceptual intents if (cmsIsMatrixShaper(hProfile)) return BlackPointAsDarkerColorant(hProfile, INTENT_RELATIVE_COLORIMETRIC, BlackPoint, 0); // Get Perceptual black out of v4 profiles. That is fixed for perceptual & saturation intents BlackPoint -> X = cmsPERCEPTUAL_BLACK_X; BlackPoint -> Y = cmsPERCEPTUAL_BLACK_Y; BlackPoint -> Z = cmsPERCEPTUAL_BLACK_Z; return TRUE; } // Check if the profile is lut based and gray, rgb or cmyk (7.2 in Adobe's document) ColorSpace = cmsGetColorSpace(hProfile); if (!cmsIsCLUT(hProfile, Intent, LCMS_USED_AS_OUTPUT ) || (ColorSpace != cmsSigGrayData && ColorSpace != cmsSigRgbData && ColorSpace != cmsSigCmykData)) { // In this case, handle as input case return cmsDetectBlackPoint(BlackPoint, hProfile, Intent, dwFlags); } // It is one of the valid cases!, use Adobe algorithm // Set a first guess, that should work on good profiles. if (Intent == INTENT_RELATIVE_COLORIMETRIC) { cmsCIEXYZ IniXYZ; // calculate initial Lab as source black point if (!cmsDetectBlackPoint(&IniXYZ, hProfile, Intent, dwFlags)) { return FALSE; } // convert the XYZ to lab cmsXYZ2Lab(NULL, &InitialLab, &IniXYZ); } else { // set the initial Lab to zero, that should be the black point for perceptual and saturation InitialLab.L = 0; InitialLab.a = 0; InitialLab.b = 0; } // Step 2 // ====== // Create a roundtrip. Define a Transform BT for all x in L*a*b* hRoundTrip = CreateRoundtripXForm(hProfile, Intent); if (hRoundTrip == NULL) return FALSE; // Compute ramps for (l=0; l < 256; l++) { Lab.L = (cmsFloat64Number) (l * 100.0) / 255.0; Lab.a = cmsmin(50, cmsmax(-50, InitialLab.a)); Lab.b = cmsmin(50, cmsmax(-50, InitialLab.b)); cmsDoTransform(hRoundTrip, &Lab, &destLab, 1); inRamp[l] = Lab.L; outRamp[l] = destLab.L; } // Make monotonic for (l = 254; l > 0; --l) { outRamp[l] = cmsmin(outRamp[l], outRamp[l+1]); } // Check if (! (outRamp[0] < outRamp[255])) { cmsDeleteTransform(hRoundTrip); BlackPoint -> X = BlackPoint ->Y = BlackPoint -> Z = 0.0; return FALSE; } // Test for mid range straight (only on relative colorimetric) NearlyStraightMidrange = TRUE; MinL = outRamp[0]; MaxL = outRamp[255]; if (Intent == INTENT_RELATIVE_COLORIMETRIC) { for (l=0; l < 256; l++) { if (! ((inRamp[l] <= MinL + 0.2 * (MaxL - MinL) ) || (fabs(inRamp[l] - outRamp[l]) < 4.0 ))) NearlyStraightMidrange = FALSE; } // If the mid range is straight (as determined above) then the // DestinationBlackPoint shall be the same as initialLab. // Otherwise, the DestinationBlackPoint shall be determined // using curve fitting. if (NearlyStraightMidrange) { cmsLab2XYZ(NULL, BlackPoint, &InitialLab); cmsDeleteTransform(hRoundTrip); return TRUE; } } // curve fitting: The round-trip curve normally looks like a nearly constant section at the black point, // with a corner and a nearly straight line to the white point. for (l=0; l < 256; l++) { yRamp[l] = (outRamp[l] - MinL) / (MaxL - MinL); } // find the black point using the least squares error quadratic curve fitting if (Intent == INTENT_RELATIVE_COLORIMETRIC) { lo = 0.1; hi = 0.5; } else { // Perceptual and saturation lo = 0.03; hi = 0.25; } // Capture shadow points for the fitting. n = 0; for (l=0; l < 256; l++) { cmsFloat64Number ff = yRamp[l]; if (ff >= lo && ff < hi) { x[n] = inRamp[l]; y[n] = yRamp[l]; n++; } } // No suitable points if (n < 3 ) { cmsDeleteTransform(hRoundTrip); BlackPoint -> X = BlackPoint ->Y = BlackPoint -> Z = 0.0; return FALSE; } // fit and get the vertex of quadratic curve Lab.L = RootOfLeastSquaresFitQuadraticCurve(n, x, y); if (Lab.L < 0.0) { // clip to zero L* if the vertex is negative Lab.L = 0; } Lab.a = InitialLab.a; Lab.b = InitialLab.b; cmsLab2XYZ(NULL, BlackPoint, &Lab); cmsDeleteTransform(hRoundTrip); return TRUE; }
// Calculates the black point of a destination profile. // This algorithm comes from the Adobe paper disclosing its black point compensation method. cmsBool CMSEXPORT cmsDetectDestinationBlackPoint(cmsCIEXYZ* BlackPoint, cmsHPROFILE hProfile, cmsUInt32Number Intent, cmsUInt32Number dwFlags) { cmsColorSpaceSignature ColorSpace; cmsHTRANSFORM hRoundTrip = NULL; cmsCIELab InitialLab, destLab, Lab; cmsFloat64Number MinL, MaxL; cmsBool NearlyStraightMidRange = FALSE; cmsFloat64Number L; cmsFloat64Number x[101], y[101]; cmsFloat64Number lo, hi, NonMonoMin; int n, l, i, NonMonoIndx; // Make sure intent is adequate if (Intent != INTENT_PERCEPTUAL && Intent != INTENT_RELATIVE_COLORIMETRIC && Intent != INTENT_SATURATION) { BlackPoint -> X = BlackPoint ->Y = BlackPoint -> Z = 0.0; return FALSE; } // v4 + perceptual & saturation intents does have its own black point, and it is // well specified enough to use it. Black point tag is deprecated in V4. if ((cmsGetEncodedICCversion(hProfile) >= 0x4000000) && (Intent == INTENT_PERCEPTUAL || Intent == INTENT_SATURATION)) { // Matrix shaper share MRC & perceptual intents if (cmsIsMatrixShaper(hProfile)) return BlackPointAsDarkerColorant(hProfile, INTENT_RELATIVE_COLORIMETRIC, BlackPoint, 0); // Get Perceptual black out of v4 profiles. That is fixed for perceptual & saturation intents BlackPoint -> X = cmsPERCEPTUAL_BLACK_X; BlackPoint -> Y = cmsPERCEPTUAL_BLACK_Y; BlackPoint -> Z = cmsPERCEPTUAL_BLACK_Z; return TRUE; } // Check if the profile is lut based and gray, rgb or cmyk (7.2 in Adobe's document) ColorSpace = cmsGetColorSpace(hProfile); if (!cmsIsCLUT(hProfile, Intent, LCMS_USED_AS_OUTPUT ) || (ColorSpace != cmsSigGrayData && ColorSpace != cmsSigRgbData && ColorSpace != cmsSigCmykData)) { // In this case, handle as input case return cmsDetectBlackPoint(BlackPoint, hProfile, Intent, dwFlags); } // It is one of the valid cases!, presto chargo hocus pocus, go for the Adobe magic // Step 1 // ====== // Set a first guess, that should work on good profiles. if (Intent == INTENT_RELATIVE_COLORIMETRIC) { cmsCIEXYZ IniXYZ; // calculate initial Lab as source black point if (!cmsDetectBlackPoint(&IniXYZ, hProfile, Intent, dwFlags)) { return FALSE; } // convert the XYZ to lab cmsXYZ2Lab(NULL, &InitialLab, &IniXYZ); } else { // set the initial Lab to zero, that should be the black point for perceptual and saturation InitialLab.L = 0; InitialLab.a = 0; InitialLab.b = 0; } // Step 2 // ====== // Create a roundtrip. Define a Transform BT for all x in L*a*b* hRoundTrip = CreateRoundtripXForm(hProfile, Intent); if (hRoundTrip == NULL) return FALSE; // Calculate Min L* Lab = InitialLab; Lab.L = 0; cmsDoTransform(hRoundTrip, &Lab, &destLab, 1); MinL = destLab.L; // Calculate Max L* Lab = InitialLab; Lab.L = 100; cmsDoTransform(hRoundTrip, &Lab, &destLab, 1); MaxL = destLab.L; // Step 3 // ====== // check if quadratic estimation needs to be done. if (Intent == INTENT_RELATIVE_COLORIMETRIC) { // Conceptually, this code tests how close the source l and converted L are to one another in the mid-range // of the values. If the converted ramp of L values is close enough to a straight line y=x, then InitialLab // is good enough to be the DestinationBlackPoint, NearlyStraightMidRange = TRUE; for (l=0; l <= 100; l++) { Lab.L = l; Lab.a = InitialLab.a; Lab.b = InitialLab.b; cmsDoTransform(hRoundTrip, &Lab, &destLab, 1); L = destLab.L; // Check the mid range in 20% after MinL if (L > (MinL + 0.2 * (MaxL - MinL))) { // Is close enough? if (fabs(L - l) > 4.0) { // Too far away, profile is buggy! NearlyStraightMidRange = FALSE; break; } } } } else { // Check is always performed for perceptual and saturation intents NearlyStraightMidRange = FALSE; } // If no furter checking is needed, we are done if (NearlyStraightMidRange) { cmsLab2XYZ(NULL, BlackPoint, &InitialLab); cmsDeleteTransform(hRoundTrip); return TRUE; } // The round-trip curve normally looks like a nearly constant section at the black point, // with a corner and a nearly straight line to the white point. // STEP 4 // ======= // find the black point using the least squares error quadratic curve fitting if (Intent == INTENT_RELATIVE_COLORIMETRIC) { lo = 0.1; hi = 0.5; } else { // Perceptual and saturation lo = 0.03; hi = 0.25; } // Capture points for the fitting. n = 0; for (l=0; l <= 100; l++) { cmsFloat64Number ff; Lab.L = (cmsFloat64Number) l; Lab.a = InitialLab.a; Lab.b = InitialLab.b; cmsDoTransform(hRoundTrip, &Lab, &destLab, 1); ff = (destLab.L - MinL)/(MaxL - MinL); if (ff >= lo && ff < hi) { x[n] = Lab.L; y[n] = ff; n++; } } // This part is not on the Adobe paper, but I found is necessary for getting any result. if (IsMonotonic(n, y)) { // Monotonic means lower point is stil valid cmsLab2XYZ(NULL, BlackPoint, &InitialLab); cmsDeleteTransform(hRoundTrip); return TRUE; } // No suitable points, regret and use safer algorithm if (n == 0) { cmsDeleteTransform(hRoundTrip); return cmsDetectBlackPoint(BlackPoint, hProfile, Intent, dwFlags); } NonMonoMin = 100; NonMonoIndx = 0; for (i=0; i < n; i++) { if (y[i] < NonMonoMin) { NonMonoIndx = i; NonMonoMin = y[i]; } } Lab.L = x[NonMonoIndx]; // fit and get the vertex of quadratic curve Lab.L = VertexOfLeastSquaresFitQuadraticCurve(n, x, y); if (Lab.L < 0.0 || Lab.L > 50.0) { // clip to zero L* if the vertex is negative Lab.L = 0; } Lab.a = InitialLab.a; Lab.b = InitialLab.b; cmsLab2XYZ(NULL, BlackPoint, &Lab); cmsDeleteTransform(hRoundTrip); return TRUE; }