diff --git a/rtengine/clutstore.cc b/rtengine/clutstore.cc index 565db8faa..4124e38bb 100644 --- a/rtengine/clutstore.cc +++ b/rtengine/clutstore.cc @@ -286,7 +286,7 @@ void rtengine::HaldCLUT::splitClutFilename( profile_name = "sRGB"; if (!name.empty()) { - for (const auto& working_profile : rtengine::ICCStore::getWorkingProfiles()) { + for (const auto& working_profile : rtengine::ICCStore::getInstance()->getWorkingProfiles()) { if ( !working_profile.empty() // This isn't strictly needed, but an empty wp name should be skipped anyway && std::search(name.rbegin(), name.rend(), working_profile.rbegin(), working_profile.rend()) == name.rbegin() diff --git a/rtengine/dcp.cc b/rtengine/dcp.cc index 82ea35f0f..d00827d73 100644 --- a/rtengine/dcp.cc +++ b/rtengine/dcp.cc @@ -37,30 +37,36 @@ namespace DCPProfile::Matrix invert3x3(const DCPProfile::Matrix& a) { - const double res00 = a[1][1] * a[2][2] - a[2][1] * a[1][2]; - const double res10 = a[2][0] * a[1][2] - a[1][0] * a[2][2]; - const double res20 = a[1][0] * a[2][1] - a[2][0] * a[1][1]; - - const double det = a[0][0] * res00 + a[0][1] * res10 + a[0][2] * res20; - - if (std::fabs(det) < 1.0e-10) { + DCPProfile::Matrix res = a; + if (!invertMatrix(a, res)) { std::cerr << "DCP matrix cannot be inverted! Expect weird output." << std::endl; - return a; } - - DCPProfile::Matrix res; - - res[0][0] = res00 / det; - res[0][1] = (a[2][1] * a[0][2] - a[0][1] * a[2][2]) / det; - res[0][2] = (a[0][1] * a[1][2] - a[1][1] * a[0][2]) / det; - res[1][0] = res10 / det; - res[1][1] = (a[0][0] * a[2][2] - a[2][0] * a[0][2]) / det; - res[1][2] = (a[1][0] * a[0][2] - a[0][0] * a[1][2]) / det; - res[2][0] = res20 / det; - res[2][1] = (a[2][0] * a[0][1] - a[0][0] * a[2][1]) / det; - res[2][2] = (a[0][0] * a[1][1] - a[1][0] * a[0][1]) / det; - return res; + + // const double res00 = a[1][1] * a[2][2] - a[2][1] * a[1][2]; + // const double res10 = a[2][0] * a[1][2] - a[1][0] * a[2][2]; + // const double res20 = a[1][0] * a[2][1] - a[2][0] * a[1][1]; + + // const double det = a[0][0] * res00 + a[0][1] * res10 + a[0][2] * res20; + + // if (std::fabs(det) < 1.0e-10) { + // std::cerr << "DCP matrix cannot be inverted! Expect weird output." << std::endl; + // return a; + // } + + // DCPProfile::Matrix res; + + // res[0][0] = res00 / det; + // res[0][1] = (a[2][1] * a[0][2] - a[0][1] * a[2][2]) / det; + // res[0][2] = (a[0][1] * a[1][2] - a[1][1] * a[0][2]) / det; + // res[1][0] = res10 / det; + // res[1][1] = (a[0][0] * a[2][2] - a[2][0] * a[0][2]) / det; + // res[1][2] = (a[1][0] * a[0][2] - a[0][0] * a[1][2]) / det; + // res[2][0] = res20 / det; + // res[2][1] = (a[2][0] * a[0][1] - a[0][0] * a[2][1]) / det; + // res[2][2] = (a[0][0] * a[1][1] - a[1][0] * a[0][1]) / det; + + // return res; } DCPProfile::Matrix multiply3x3(const DCPProfile::Matrix& a, const DCPProfile::Matrix& b) diff --git a/rtengine/iccstore.cc b/rtengine/iccstore.cc index e77ea7f0f..50ff58878 100644 --- a/rtengine/iccstore.cc +++ b/rtengine/iccstore.cc @@ -27,6 +27,8 @@ #include #endif +#include + #include "iccstore.h" #include "iccmatrices.h" @@ -35,6 +37,8 @@ #include "../rtgui/options.h" #include "../rtgui/threadutils.h" +#include "cJSON.h" + namespace rtengine { @@ -274,7 +278,7 @@ public: for (int i = 0; i < N; ++i) { wProfiles[wpnames[i]] = createFromMatrix(wprofiles[i]); - wProfilesGamma[wpnames[i]] = createFromMatrix(wprofiles[i], true); + // wProfilesGamma[wpnames[i]] = createFromMatrix(wprofiles[i], true); wMatrices[wpnames[i]] = wprofiles[i]; iwMatrices[wpnames[i]] = iwprofiles[i]; } @@ -287,11 +291,11 @@ public: cmsCloseProfile(p.second); } } - for (auto &p : wProfilesGamma) { - if (p.second) { - cmsCloseProfile(p.second); - } - } + // for (auto &p : wProfilesGamma) { + // if (p.second) { + // cmsCloseProfile(p.second); + // } + // } for (auto &p : fileProfiles) { if(p.second) { cmsCloseProfile(p.second); @@ -334,6 +338,9 @@ public: defaultMonitorProfile = settings->monitorProfile; + loadWorkingSpaces(rtICCDir); + loadWorkingSpaces(userICCDir); + // initialize the alarm colours for lcms gamut checking -- we use bright green cmsUInt16Number cms_alarm_codes[cmsMAXCHANNELS] = { 0, 65535, 65535 }; cmsSetAlarmCodes(cms_alarm_codes); @@ -349,16 +356,16 @@ public: : wProfiles.find("sRGB")->second; } - cmsHPROFILE workingSpaceGamma(const Glib::ustring& name) const - { + // cmsHPROFILE workingSpaceGamma(const Glib::ustring& name) const + // { - const ProfileMap::const_iterator r = wProfilesGamma.find(name); + // const ProfileMap::const_iterator r = wProfilesGamma.find(name); - return - r != wProfilesGamma.end() - ? r->second - : wProfilesGamma.find("sRGB")->second; - } + // return + // r != wProfilesGamma.end() + // ? r->second + // : wProfilesGamma.find("sRGB")->second; + // } TMatrix workingSpaceMatrix(const Glib::ustring& name) const { @@ -582,17 +589,276 @@ public: defaultMonitorProfile = name; } + std::vector getWorkingProfiles() + { + std::vector res; + + // for (unsigned int i = 0; i < sizeof(wpnames) / sizeof(wpnames[0]); i++) { + // res.push_back(wpnames[i]); + // } + for (const auto &p : wProfiles) { + res.push_back(p.first); + } + + return res; + } + private: + using CVector = std::array; + using CMatrix = std::array; + struct PMatrix { + double matrix[3][3]; + PMatrix(): matrix{} {} + PMatrix(const CMatrix &m) + { + set(m); + } + + CMatrix toMatrix() const + { + CMatrix ret; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + ret[i][j] = matrix[i][j]; + } + } + return ret; + } + + void set(const CMatrix &m) + { + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + matrix[i][j] = m[i][j]; + } + } + } + }; + + bool computeWorkingSpaceMatrix(const Glib::ustring &path, const Glib::ustring &filename, PMatrix &out) + { + Glib::ustring fullpath = filename; + if (!Glib::path_is_absolute(fullpath)) { + fullpath = Glib::build_filename(path, filename); + } + ProfileContent content(fullpath); + cmsHPROFILE prof = content.toProfile(); + if (!prof) { + return false; + } + if (cmsGetColorSpace(prof) != cmsSigRgbData) { + cmsCloseProfile(prof); + return false; + } + if (!cmsIsMatrixShaper(prof)) { + cmsCloseProfile(prof); + return false; + } + cmsCIEXYZ *white = static_cast(cmsReadTag(prof, cmsSigMediaWhitePointTag)); + cmsCIEXYZ *red = static_cast(cmsReadTag(prof, cmsSigRedMatrixColumnTag)); + cmsCIEXYZ *green = static_cast(cmsReadTag(prof, cmsSigGreenMatrixColumnTag)); + cmsCIEXYZ *blue = static_cast(cmsReadTag(prof, cmsSigBlueMatrixColumnTag)); + + if (!white || !red || !green || !blue) { + cmsCloseProfile(prof); + return false; + } + + // do the Bradford adaptation to D50 + // matrices from Bruce Lindbloom's webpage + static constexpr CMatrix bradford_MA = { + CVector({0.8951000, 0.2664000, -0.1614000}), + CVector({-0.7502000, 1.7135000, 0.0367000}), + CVector({0.0389000, -0.0685000, 1.0296000}) + }; + static constexpr CMatrix bradford_MA_inv = { + CVector({0.9869929, -0.1470543, 0.1599627}), + CVector({0.4323053, 0.5183603, 0.0492912}), + CVector({-0.0085287, 0.0400428, 0.9684867}) + }; + static constexpr CVector bradford_MA_dot_D50 = { + 0.99628443, 1.02042736, 0.81864437 + }; + + CVector srcw = dotProduct(bradford_MA, CVector({ white->X, white->Y, white->Z })); + CMatrix m = { + CVector({ bradford_MA_dot_D50[0]/srcw[0], 0.0, 0.0 }), + CVector({ 0.0, bradford_MA_dot_D50[1]/srcw[1], 0.0 }), + CVector({ 0.0, 0.0, bradford_MA_dot_D50[2]/srcw[2] }) + }; + CMatrix adapt = dotProduct(dotProduct(bradford_MA_inv, m), bradford_MA); + + m[0][0] = red->X; m[0][1] = green->X; m[0][2] = blue->X; + m[1][0] = red->Y; m[1][1] = green->Y; m[1][2] = blue->Y; + m[2][0] = red->Z; m[2][1] = green->Z; m[2][2] = blue->Z; + + m = dotProduct(adapt, m); + out.set(m); + + cmsCloseProfile(prof); + return true; + } + + bool loadWorkingSpaces(const Glib::ustring &path) + { + Glib::ustring fileName = Glib::build_filename(path, "workingspaces.json"); + FILE* const f = g_fopen(fileName.c_str(), "r"); + + if (settings->verbose) { + std::cout << "trying to load extra working spaces from " << fileName << std::flush; + } + + if (!f) { + if (settings->verbose) { + std::cout << " FAIL" << std::endl; + } + return false; + } + + fseek(f, 0, SEEK_END); + long length = ftell(f); + if (length <= 0) { + if (settings->verbose) { + std::cout << " FAIL" << std::endl; + } + fclose(f); + return false; + } + + char *buf = new char[length + 1]; + fseek(f, 0, SEEK_SET); + length = fread(buf, 1, length, f); + buf[length] = 0; + + fclose(f); + + cJSON_Minify(buf); + cJSON *root = cJSON_Parse(buf); + + if (!root) { + if (settings->verbose) { + std::cout << " FAIL" << std::endl; + } + return false; + } + + delete[] buf; + + cJSON *js = cJSON_GetObjectItem(root, "working_spaces"); + if (!js) { + goto parse_error; + } + + for (js = js->child; js != nullptr; js = js->next) { + cJSON *ji = cJSON_GetObjectItem(js, "name"); + std::unique_ptr m(new PMatrix); + std::string name; + + if (!ji || ji->type != cJSON_String) { + goto parse_error; + } + + name = ji->valuestring; + + if (wProfiles.find(name) != wProfiles.end()) { + continue; // already there -- ignore + } + + bool found_matrix = false; + + ji = cJSON_GetObjectItem(js, "matrix"); + if (ji) { + if (ji->type != cJSON_Array) { + goto parse_error; + } + + ji = ji->child; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j, ji = ji->next) { + if (!ji || ji->type != cJSON_Number) { + goto parse_error; + } + m->matrix[i][j] = ji->valuedouble; + } + } + + if (ji) { + goto parse_error; + } + found_matrix = true; + } else { + ji = cJSON_GetObjectItem(js, "file"); + if (!ji || ji->type != cJSON_String) { + goto parse_error; + } + found_matrix = computeWorkingSpaceMatrix(path, ji->valuestring, *m); + } + + if (!found_matrix) { + if (settings->verbose) { + std::cout << "Could not find suitable matrix for working space: " << name << std::endl; + } + continue; + } + + pMatrices.emplace_back(std::move(m)); + TMatrix w = pMatrices.back()->matrix; + + CMatrix b = {}; + if (!rtengine::invertMatrix(pMatrices.back()->toMatrix(), b)) { + if (settings->verbose) { + std::cout << "Matrix for working space: " << name << " is not invertible, skipping" << std::endl; + } + pMatrices.pop_back(); + } else { + wMatrices[name] = w; + pMatrices.emplace_back(new PMatrix(b)); + TMatrix iw = pMatrices.back()->matrix; + iwMatrices[name] = iw; + wProfiles[name] = createFromMatrix(w); + + if (settings->verbose) { + std::cout << "Added working space: " << name << std::endl; + std::cout << " matrix: ["; + for (int i = 0; i < 3; ++i) { + std::cout << " ["; + for (int j = 0; j < 3; ++j) { + std::cout << " " << w[i][j]; + } + std::cout << "]"; + } + std::cout << " ]" << std::endl; + } + } + } + + cJSON_Delete(root); + if (settings->verbose) { + std::cout << " OK" << std::endl; + } + return true; + + parse_error: + if (settings->verbose) { + std::cout << " ERROR in parsing " << fileName << std::endl; + } + + cJSON_Delete(root); + return false; + } + using ProfileMap = std::map; using MatrixMap = std::map; using ContentMap = std::map; using NameMap = std::map; ProfileMap wProfiles; - ProfileMap wProfilesGamma; + // ProfileMap wProfilesGamma; MatrixMap wMatrices; MatrixMap iwMatrices; + std::vector> pMatrices; + // These contain profiles from user/system directory(supplied on init) Glib::ustring profilesDir; Glib::ustring userICCDir; @@ -630,10 +896,10 @@ cmsHPROFILE rtengine::ICCStore::workingSpace(const Glib::ustring& name) const return implementation->workingSpace(name); } -cmsHPROFILE rtengine::ICCStore::workingSpaceGamma(const Glib::ustring& name) const -{ - return implementation->workingSpaceGamma(name); -} +// cmsHPROFILE rtengine::ICCStore::workingSpaceGamma(const Glib::ustring& name) const +// { +// return implementation->workingSpaceGamma(name); +// } rtengine::TMatrix rtengine::ICCStore::workingSpaceMatrix(const Glib::ustring& name) const { @@ -736,14 +1002,7 @@ rtengine::ICCStore::~ICCStore() = default; std::vector rtengine::ICCStore::getWorkingProfiles() { - - std::vector res; - - for (unsigned int i = 0; i < sizeof(wpnames) / sizeof(wpnames[0]); i++) { - res.push_back(wpnames[i]); - } - - return res; + return implementation->getWorkingProfiles(); } std::vector rtengine::ICCStore::getGamma() diff --git a/rtengine/iccstore.h b/rtengine/iccstore.h index 4604404fb..c23ecd112 100644 --- a/rtengine/iccstore.h +++ b/rtengine/iccstore.h @@ -69,7 +69,7 @@ public: void init(const Glib::ustring& usrICCDir, const Glib::ustring& stdICCDir, bool loadAll); cmsHPROFILE workingSpace(const Glib::ustring& name) const; - cmsHPROFILE workingSpaceGamma(const Glib::ustring& name) const; + // cmsHPROFILE workingSpaceGamma(const Glib::ustring& name) const; TMatrix workingSpaceMatrix(const Glib::ustring& name) const; TMatrix workingSpaceInverseMatrix(const Glib::ustring& name) const; @@ -95,7 +95,7 @@ public: std::uint8_t getOutputIntents(const Glib::ustring& name) const; std::uint8_t getProofIntents(const Glib::ustring& name) const; - static std::vector getWorkingProfiles(); + /*static*/ std::vector getWorkingProfiles(); static std::vector getGamma(); static void getGammaArray(const procparams::ColorManagementParams& icm, GammaValues& ga); diff --git a/rtengine/rawimagesource.cc b/rtengine/rawimagesource.cc index 081ac3661..7fe347797 100644 --- a/rtengine/rawimagesource.cc +++ b/rtengine/rawimagesource.cc @@ -3997,7 +3997,7 @@ void RawImageSource::colorSpaceConversion_ (Imagefloat* im, const ColorManagemen } } else { - const bool working_space_is_prophoto = (cmp.working == "ProPhoto"); + bool working_space_is_prophoto = (cmp.working == "ProPhoto"); // use supplied input profile @@ -4066,6 +4066,33 @@ void RawImageSource::colorSpaceConversion_ (Imagefloat* im, const ColorManagemen cmsHPROFILE prophoto = ICCStore::getInstance()->workingSpace("ProPhoto"); // We always use Prophoto to apply the ICC profile to minimize problems with clipping in LUT conversion. bool transform_via_pcs_lab = false; bool separate_pcs_lab_highlights = false; + + // check if the working space is fully contained in prophoto + if (!working_space_is_prophoto) { + TMatrix toxyz = ICCStore::getInstance()->workingSpaceMatrix(cmp.working); + TMatrix torgb = ICCStore::getInstance()->workingSpaceInverseMatrix("ProPhoto"); + float rgb[3] = {0.f, 0.f, 0.f}; + for (int i = 0; i < 2 && !working_space_is_prophoto; ++i) { + rgb[i] = 1.f; + float x, y, z; + + Color::rgbxyz(rgb[0], rgb[1], rgb[2], x, y, z, toxyz); + Color::xyz2rgb(x, y, z, rgb[0], rgb[1], rgb[2], torgb); + + for (int j = 0; j < 2; ++j) { + if (rgb[j] < 0.f || rgb[j] > 1.f) { + working_space_is_prophoto = true; + prophoto = ICCStore::getInstance()->workingSpace(cmp.working); + if (settings->verbose) { + std::cout << "colorSpaceConversion_: converting directly to " << cmp.working << " instead of passing through ProPhoto" << std::endl; + } + break; + } + rgb[j] = 0.f; + } + } + } + lcmsMutex->lock (); switch (camera_icc_type) { diff --git a/rtengine/rt_math.h b/rtengine/rt_math.h index ca93619ee..a1dc11d44 100644 --- a/rtengine/rt_math.h +++ b/rtengine/rt_math.h @@ -4,6 +4,7 @@ #include #include #include +#include namespace rtengine { @@ -137,4 +138,67 @@ constexpr std::uint8_t uint16ToUint8Rounded(std::uint16_t i) return ((i + 128) - ((i + 128) >> 8)) >> 8; } + +template +bool invertMatrix(const std::array, 3> &in, std::array, 3> &out) +{ + const T res00 = in[1][1] * in[2][2] - in[2][1] * in[1][2]; + const T res10 = in[2][0] * in[1][2] - in[1][0] * in[2][2]; + const T res20 = in[1][0] * in[2][1] - in[2][0] * in[1][1]; + + const T det = in[0][0] * res00 + in[0][1] * res10 + in[0][2] * res20; + + if (std::abs(det) < 1.0e-10) { + return false; + } + + out[0][0] = res00 / det; + out[0][1] = (in[2][1] * in[0][2] - in[0][1] * in[2][2]) / det; + out[0][2] = (in[0][1] * in[1][2] - in[1][1] * in[0][2]) / det; + out[1][0] = res10 / det; + out[1][1] = (in[0][0] * in[2][2] - in[2][0] * in[0][2]) / det; + out[1][2] = (in[1][0] * in[0][2] - in[0][0] * in[1][2]) / det; + out[2][0] = res20 / det; + out[2][1] = (in[2][0] * in[0][1] - in[0][0] * in[2][1]) / det; + out[2][2] = (in[0][0] * in[1][1] - in[1][0] * in[0][1]) / det; + + return true; } + +template +std::array, 3> dotProduct(const std::array, 3> &a, const std::array, 3> &b) +{ + std::array, 3> res; + + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + res[i][j] = 0; + + for (int k = 0; k < 3; ++k) { + res[i][j] += a[i][k] * b[k][j]; + } + } + } + + return res; +} + + +template +std::array dotProduct(const std::array, 3> &a, const std::array &b) +{ + std::array res; + + for (int i = 0; i < 3; ++i) { + res[i] = 0; + for (int k = 0; k < 3; ++k) { + res[i] += a[i][k] * b[k]; + } + } + + return res; +} + + +} + diff --git a/rtgui/icmpanel.cc b/rtgui/icmpanel.cc index e026243be..bb9677d1c 100644 --- a/rtgui/icmpanel.cc +++ b/rtgui/icmpanel.cc @@ -160,7 +160,7 @@ ICMPanel::ICMPanel () : FoldableToolPanel(this, "icm", M("TP_ICM_LABEL")), iunch wnames = Gtk::manage (new MyComboBoxText ()); wVBox->pack_start (*wnames, Gtk::PACK_SHRINK); - std::vector wpnames = rtengine::ICCStore::getWorkingProfiles(); + std::vector wpnames = rtengine::ICCStore::getInstance()->getWorkingProfiles(); for (size_t i = 0; i < wpnames.size(); i++) { wnames->append (wpnames[i]);