The JSON file is called workingspaces.json, it can be either in the global iccprofiles directory, or in the user's ICC profiles dir (set in preferences). The format is the following: {"working_spaces": [ { "name" : "ACES", "file" : "/path/to/ACES.icc" }, { "name" : "ACEScg", "matrix" : [0.7184354, 0.16578523, 0.09882643, 0.29728935, 0.66958117, 0.03571544, -0.00647622, 0.01469771, 0.66732561] } ]} if "matrix" is present, "file" is ignored. If only "file" is present, the matrix is extracted from the ICC profile. For this, we look only at the R, G, and B matrix columns and the white point set in the profile. Bradford adaptation is used to convert the matrix to D50. Anything else (LUT, TRC, ...) in the profile is ignored. It is the user's responsibility to ensure that the profile is suitable to be used as a working space.
1509 lines
47 KiB
C++
1509 lines
47 KiB
C++
/*
|
|
* This file is part of RawTherapee.
|
|
*
|
|
* Copyright (c) 2004-2010 Gabor Horvath <hgabor@rawtherapee.com>
|
|
*
|
|
* RawTherapee is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* RawTherapee is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with RawTherapee. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include <cstring>
|
|
|
|
#include <glibmm.h>
|
|
#include <glib/gstdio.h>
|
|
|
|
#ifdef WIN32
|
|
#include <winsock2.h>
|
|
#else
|
|
#include <netinet/in.h>
|
|
#endif
|
|
|
|
#include <iostream>
|
|
|
|
#include "iccstore.h"
|
|
|
|
#include "iccmatrices.h"
|
|
#include "procparams.h"
|
|
|
|
#include "../rtgui/options.h"
|
|
#include "../rtgui/threadutils.h"
|
|
|
|
#include "cJSON.h"
|
|
|
|
namespace rtengine
|
|
{
|
|
|
|
extern const Settings* settings;
|
|
|
|
}
|
|
|
|
namespace
|
|
{
|
|
|
|
// Not recursive
|
|
void loadProfiles(
|
|
const Glib::ustring& dirName,
|
|
std::map<Glib::ustring, cmsHPROFILE>* profiles,
|
|
std::map<Glib::ustring, rtengine::ProfileContent>* profileContents,
|
|
std::map<Glib::ustring, Glib::ustring>* profileNames,
|
|
bool nameUpper
|
|
)
|
|
{
|
|
if (dirName.empty()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
Glib::Dir dir(dirName);
|
|
|
|
for (Glib::DirIterator entry = dir.begin(); entry != dir.end(); ++entry) {
|
|
const Glib::ustring fileName = *entry;
|
|
|
|
if (fileName.size() < 4) {
|
|
continue;
|
|
}
|
|
|
|
const Glib::ustring extension = rtengine::getFileExtension(fileName);
|
|
|
|
if (extension != "icc" && extension != "icm") {
|
|
continue;
|
|
}
|
|
|
|
const Glib::ustring filePath = Glib::build_filename(dirName, fileName);
|
|
|
|
if (!Glib::file_test(filePath, Glib::FILE_TEST_IS_REGULAR)) {
|
|
continue;
|
|
}
|
|
|
|
Glib::ustring name = fileName.substr(0, fileName.size() - 4);
|
|
|
|
if (nameUpper) {
|
|
name = name.uppercase();
|
|
}
|
|
|
|
if (profiles) {
|
|
const rtengine::ProfileContent content(filePath);
|
|
const cmsHPROFILE profile = content.toProfile();
|
|
|
|
if (profile) {
|
|
profiles->emplace(name, profile);
|
|
|
|
if (profileContents) {
|
|
profileContents->emplace(name, content);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (profileNames) {
|
|
profileNames->emplace(name, filePath);
|
|
}
|
|
}
|
|
} catch (Glib::Exception&) {
|
|
}
|
|
}
|
|
|
|
// Version dedicated to single profile load when loadAll==false (cli version "-q" mode)
|
|
bool loadProfile(
|
|
const Glib::ustring& profile,
|
|
const Glib::ustring& dirName,
|
|
std::map<Glib::ustring, cmsHPROFILE>* profiles,
|
|
std::map<Glib::ustring, rtengine::ProfileContent>* profileContents
|
|
)
|
|
{
|
|
if (dirName.empty() || profiles == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
Glib::Dir dir(dirName);
|
|
|
|
for (Glib::DirIterator entry = dir.begin(); entry != dir.end(); ++entry) {
|
|
const Glib::ustring fileName = *entry;
|
|
|
|
if (fileName.size() < 4) {
|
|
continue;
|
|
}
|
|
|
|
const Glib::ustring extension = rtengine::getFileExtension(fileName);
|
|
|
|
if (extension != "icc" && extension != "icm") {
|
|
continue;
|
|
}
|
|
|
|
const Glib::ustring filePath = Glib::build_filename(dirName, fileName);
|
|
|
|
if (!Glib::file_test(filePath, Glib::FILE_TEST_IS_REGULAR)) {
|
|
continue;
|
|
}
|
|
|
|
const Glib::ustring name = fileName.substr(0, fileName.size() - 4);
|
|
|
|
if (name == profile) {
|
|
const rtengine::ProfileContent content(filePath);
|
|
const cmsHPROFILE profile = content.toProfile();
|
|
|
|
if (profile) {
|
|
profiles->emplace(name, profile);
|
|
|
|
if (profileContents) {
|
|
profileContents->emplace(name, content);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
} catch (Glib::Exception&) {
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void getSupportedIntent(cmsHPROFILE profile, cmsUInt32Number intent, cmsUInt32Number direction, uint8_t& result)
|
|
{
|
|
if (cmsIsIntentSupported(profile, intent, direction)) {
|
|
result |= 1 << intent;
|
|
}
|
|
}
|
|
|
|
uint8_t getSupportedIntents(cmsHPROFILE profile, cmsUInt32Number direction)
|
|
{
|
|
if (!profile) {
|
|
return 0;
|
|
}
|
|
|
|
uint8_t result = 0;
|
|
|
|
getSupportedIntent(profile, INTENT_PERCEPTUAL, direction, result);
|
|
getSupportedIntent(profile, INTENT_RELATIVE_COLORIMETRIC, direction, result);
|
|
getSupportedIntent(profile, INTENT_SATURATION, direction, result);
|
|
getSupportedIntent(profile, INTENT_ABSOLUTE_COLORIMETRIC, direction, result);
|
|
|
|
return result;
|
|
}
|
|
|
|
cmsHPROFILE createXYZProfile()
|
|
{
|
|
double mat[3][3] = { {1.0, 0, 0}, {0, 1.0, 0}, {0, 0, 1.0} };
|
|
return rtengine::ICCStore::createFromMatrix(mat, false, "XYZ");
|
|
}
|
|
|
|
const double(*wprofiles[])[3] = {xyz_sRGB, xyz_adobe, xyz_prophoto, xyz_widegamut, xyz_bruce, xyz_beta, xyz_best, xyz_rec2020};
|
|
const double(*iwprofiles[])[3] = {sRGB_xyz, adobe_xyz, prophoto_xyz, widegamut_xyz, bruce_xyz, beta_xyz, best_xyz, rec2020_xyz};
|
|
const char* wpnames[] = {"sRGB", "Adobe RGB", "ProPhoto", "WideGamut", "BruceRGB", "Beta RGB", "BestRGB", "Rec2020"};
|
|
const char* wpgamma[] = {"default", "BT709_g2.2_s4.5", "sRGB_g2.4_s12.92", "linear_g1.0", "standard_g2.2", "standard_g1.8", "High_g1.3_s3.35", "Low_g2.6_s6.9"}; //gamma free
|
|
//default = gamma inside profile
|
|
//BT709 g=2.22 s=4.5 sRGB g=2.4 s=12.92
|
|
//linear g=1.0
|
|
//std22 g=2.2 std18 g=1.8
|
|
// high g=1.3 s=3.35 for high dynamic images
|
|
//low g=2.6 s=6.9 for low contrast images
|
|
|
|
}
|
|
|
|
rtengine::ProfileContent::ProfileContent() = default;
|
|
|
|
rtengine::ProfileContent::ProfileContent(const Glib::ustring& fileName)
|
|
{
|
|
FILE* const f = g_fopen(fileName.c_str(), "rb");
|
|
|
|
if (!f) {
|
|
return;
|
|
}
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
long length = ftell(f);
|
|
if(length > 0) {
|
|
char* d = new char[length + 1];
|
|
fseek(f, 0, SEEK_SET);
|
|
length = fread(d, 1, length, f);
|
|
d[length] = 0;
|
|
data.assign(d, length);
|
|
delete[] d;
|
|
} else {
|
|
data.clear();
|
|
}
|
|
fclose(f);
|
|
|
|
}
|
|
|
|
rtengine::ProfileContent::ProfileContent(cmsHPROFILE hProfile)
|
|
{
|
|
if (hProfile != nullptr) {
|
|
cmsUInt32Number bytesNeeded = 0;
|
|
cmsSaveProfileToMem(hProfile, nullptr, &bytesNeeded);
|
|
|
|
if (bytesNeeded > 0) {
|
|
char* d = new char[bytesNeeded + 1];
|
|
cmsSaveProfileToMem(hProfile, d, &bytesNeeded);
|
|
data.assign(d, bytesNeeded);
|
|
delete[] d;
|
|
}
|
|
}
|
|
}
|
|
|
|
cmsHPROFILE rtengine::ProfileContent::toProfile() const
|
|
{
|
|
|
|
return
|
|
!data.empty()
|
|
? cmsOpenProfileFromMem(data.c_str(), data.size())
|
|
: nullptr;
|
|
}
|
|
|
|
const std::string& rtengine::ProfileContent::getData() const
|
|
{
|
|
return data;
|
|
}
|
|
|
|
class rtengine::ICCStore::Implementation
|
|
{
|
|
public:
|
|
Implementation() :
|
|
loadAll(true),
|
|
xyz(createXYZProfile()),
|
|
srgb(cmsCreate_sRGBProfile())
|
|
{
|
|
//cmsErrorAction(LCMS_ERROR_SHOW);
|
|
|
|
constexpr int N = sizeof(wpnames) / sizeof(wpnames[0]);
|
|
|
|
for (int i = 0; i < N; ++i) {
|
|
wProfiles[wpnames[i]] = createFromMatrix(wprofiles[i]);
|
|
// wProfilesGamma[wpnames[i]] = createFromMatrix(wprofiles[i], true);
|
|
wMatrices[wpnames[i]] = wprofiles[i];
|
|
iwMatrices[wpnames[i]] = iwprofiles[i];
|
|
}
|
|
}
|
|
|
|
~Implementation()
|
|
{
|
|
for (auto &p : wProfiles) {
|
|
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);
|
|
}
|
|
}
|
|
if(srgb) {
|
|
cmsCloseProfile(srgb);
|
|
}
|
|
if(xyz) {
|
|
cmsCloseProfile(xyz);
|
|
}
|
|
}
|
|
|
|
void init(const Glib::ustring& usrICCDir, const Glib::ustring& rtICCDir, bool loadAll)
|
|
{
|
|
// Reads all profiles from the given profiles dir
|
|
|
|
MyMutex::MyLock lock(mutex);
|
|
|
|
this->loadAll = loadAll;
|
|
|
|
// RawTherapee's profiles take precedence if a user's profile of the same name exists
|
|
profilesDir = Glib::build_filename(rtICCDir, "output");
|
|
userICCDir = usrICCDir;
|
|
fileProfiles.clear();
|
|
fileProfileContents.clear();
|
|
if (loadAll) {
|
|
loadProfiles(profilesDir, &fileProfiles, &fileProfileContents, nullptr, false);
|
|
loadProfiles(userICCDir, &fileProfiles, &fileProfileContents, nullptr, false);
|
|
}
|
|
|
|
// Input profiles
|
|
// Load these to different areas, since the short name(e.g. "NIKON D700" may overlap between system/user and RT dir)
|
|
stdProfilesDir = Glib::build_filename(rtICCDir, "input");
|
|
fileStdProfiles.clear();
|
|
fileStdProfilesFileNames.clear();
|
|
if (loadAll) {
|
|
loadProfiles(stdProfilesDir, nullptr, nullptr, &fileStdProfilesFileNames, true);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
cmsHPROFILE workingSpace(const Glib::ustring& name) const
|
|
{
|
|
const ProfileMap::const_iterator r = wProfiles.find(name);
|
|
|
|
return
|
|
r != wProfiles.end()
|
|
? r->second
|
|
: wProfiles.find("sRGB")->second;
|
|
}
|
|
|
|
// cmsHPROFILE workingSpaceGamma(const Glib::ustring& name) const
|
|
// {
|
|
|
|
// const ProfileMap::const_iterator r = wProfilesGamma.find(name);
|
|
|
|
// return
|
|
// r != wProfilesGamma.end()
|
|
// ? r->second
|
|
// : wProfilesGamma.find("sRGB")->second;
|
|
// }
|
|
|
|
TMatrix workingSpaceMatrix(const Glib::ustring& name) const
|
|
{
|
|
const MatrixMap::const_iterator r = wMatrices.find(name);
|
|
|
|
return
|
|
r != wMatrices.end()
|
|
? r->second
|
|
: wMatrices.find("sRGB")->second;
|
|
}
|
|
|
|
TMatrix workingSpaceInverseMatrix(const Glib::ustring& name) const
|
|
{
|
|
|
|
const MatrixMap::const_iterator r = iwMatrices.find(name);
|
|
|
|
return
|
|
r != iwMatrices.end()
|
|
? r->second
|
|
: iwMatrices.find("sRGB")->second;
|
|
}
|
|
|
|
bool outputProfileExist(const Glib::ustring& name) const
|
|
{
|
|
MyMutex::MyLock lock(mutex);
|
|
return fileProfiles.find(name) != fileProfiles.end();
|
|
}
|
|
|
|
cmsHPROFILE getProfile(const Glib::ustring& name)
|
|
{
|
|
MyMutex::MyLock lock(mutex);
|
|
|
|
const ProfileMap::const_iterator r = fileProfiles.find(name);
|
|
|
|
if (r != fileProfiles.end()) {
|
|
return r->second;
|
|
}
|
|
|
|
if (!name.compare(0, 5, "file:")) {
|
|
const ProfileContent content(name.substr(5));
|
|
const cmsHPROFILE profile = content.toProfile();
|
|
|
|
if (profile) {
|
|
fileProfiles.emplace(name, profile);
|
|
fileProfileContents.emplace(name, content);
|
|
|
|
return profile;
|
|
}
|
|
} else if (!loadAll) {
|
|
// Look for a standard profile
|
|
if (!loadProfile(name, profilesDir, &fileProfiles, &fileProfileContents)) {
|
|
loadProfile(name, userICCDir, &fileProfiles, &fileProfileContents);
|
|
}
|
|
const ProfileMap::const_iterator r = fileProfiles.find(name);
|
|
if (r != fileProfiles.end()) {
|
|
return r->second;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
cmsHPROFILE getStdProfile(const Glib::ustring& name)
|
|
{
|
|
const Glib::ustring nameUpper = name.uppercase();
|
|
|
|
MyMutex::MyLock lock(mutex);
|
|
|
|
const ProfileMap::const_iterator r = fileStdProfiles.find(nameUpper);
|
|
|
|
// Return profile from store
|
|
if (r != fileStdProfiles.end()) {
|
|
return r->second;
|
|
} else if (!loadAll) {
|
|
// Directory not scanned, so looking and adding now...
|
|
if (!loadProfile(name, profilesDir, &fileProfiles, &fileProfileContents)) {
|
|
loadProfile(name, userICCDir, &fileProfiles, &fileProfileContents);
|
|
}
|
|
const ProfileMap::const_iterator r = fileProfiles.find(name);
|
|
if (r != fileProfiles.end()) {
|
|
return r->second;
|
|
}
|
|
}
|
|
|
|
// Profile is not yet in store
|
|
const NameMap::const_iterator f = fileStdProfilesFileNames.find(nameUpper);
|
|
|
|
// Profile does not exist
|
|
if (f == fileStdProfilesFileNames.end()) {
|
|
return nullptr;
|
|
}
|
|
|
|
// But there exists one --> load it
|
|
const ProfileContent content(f->second);
|
|
const cmsHPROFILE profile = content.toProfile();
|
|
|
|
if (profile) {
|
|
fileStdProfiles.emplace(f->first, profile);
|
|
}
|
|
|
|
// Profile invalid or stored now --> remove entry from fileStdProfilesFileNames
|
|
fileStdProfilesFileNames.erase(f);
|
|
return profile;
|
|
}
|
|
|
|
ProfileContent getContent(const Glib::ustring& name) const
|
|
{
|
|
MyMutex::MyLock lock(mutex);
|
|
|
|
const ContentMap::const_iterator r = fileProfileContents.find(name);
|
|
|
|
return
|
|
r != fileProfileContents.end()
|
|
? r->second
|
|
: ProfileContent();
|
|
}
|
|
|
|
cmsHPROFILE getXYZProfile() const
|
|
{
|
|
return xyz;
|
|
}
|
|
|
|
cmsHPROFILE getsRGBProfile() const
|
|
{
|
|
return srgb;
|
|
}
|
|
|
|
std::vector<Glib::ustring> getProfiles(ProfileType type) const
|
|
{
|
|
std::vector<Glib::ustring> res;
|
|
|
|
MyMutex::MyLock lock(mutex);
|
|
|
|
for (const auto profile : fileProfiles) {
|
|
if (
|
|
(
|
|
type==ICCStore::ProfileType::MONITOR
|
|
&& cmsGetDeviceClass(profile.second) == cmsSigDisplayClass
|
|
&& cmsGetColorSpace(profile.second) == cmsSigRgbData
|
|
)
|
|
||(
|
|
type==ICCStore::ProfileType::PRINTER
|
|
&& cmsGetDeviceClass(profile.second) == cmsSigOutputClass
|
|
)
|
|
||(
|
|
type==ICCStore::ProfileType::OUTPUT
|
|
&& (cmsGetDeviceClass(profile.second) == cmsSigDisplayClass
|
|
|| cmsGetDeviceClass(profile.second) == cmsSigInputClass
|
|
|| cmsGetDeviceClass(profile.second) == cmsSigOutputClass)
|
|
&& cmsGetColorSpace(profile.second) == cmsSigRgbData
|
|
)
|
|
) {
|
|
res.push_back(profile.first);
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
std::vector<Glib::ustring> getProfilesFromDir(const Glib::ustring& dirName) const
|
|
{
|
|
std::vector<Glib::ustring> res;
|
|
ProfileMap profiles;
|
|
|
|
MyMutex::MyLock lock(mutex);
|
|
|
|
loadProfiles(profilesDir, &profiles, nullptr, nullptr, false);
|
|
loadProfiles(dirName, &profiles, nullptr, nullptr, false);
|
|
|
|
for (const auto& profile : profiles) {
|
|
res.push_back(profile.first);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
std::uint8_t getInputIntents(cmsHPROFILE profile)
|
|
{
|
|
MyMutex::MyLock lock(mutex);
|
|
|
|
return getSupportedIntents(profile, LCMS_USED_AS_INPUT);
|
|
}
|
|
|
|
std::uint8_t getOutputIntents(cmsHPROFILE profile)
|
|
{
|
|
MyMutex::MyLock lock(mutex);
|
|
|
|
return getSupportedIntents(profile, LCMS_USED_AS_OUTPUT);
|
|
}
|
|
|
|
std::uint8_t getProofIntents(cmsHPROFILE profile)
|
|
{
|
|
MyMutex::MyLock lock(mutex);
|
|
|
|
return getSupportedIntents(profile, LCMS_USED_AS_PROOF);
|
|
}
|
|
|
|
std::uint8_t getInputIntents(const Glib::ustring &name)
|
|
{
|
|
return getInputIntents(getProfile(name));
|
|
}
|
|
|
|
std::uint8_t getOutputIntents(const Glib::ustring &name)
|
|
{
|
|
return getOutputIntents(getProfile(name));
|
|
}
|
|
|
|
std::uint8_t getProofIntents(const Glib::ustring &name)
|
|
{
|
|
return getProofIntents(getProfile(name));
|
|
}
|
|
|
|
Glib::ustring getDefaultMonitorProfileName() const
|
|
{
|
|
return defaultMonitorProfile;
|
|
}
|
|
|
|
void setDefaultMonitorProfileName(const Glib::ustring &name)
|
|
{
|
|
MyMutex::MyLock lock(mutex);
|
|
defaultMonitorProfile = name;
|
|
}
|
|
|
|
std::vector<Glib::ustring> getWorkingProfiles()
|
|
{
|
|
std::vector<Glib::ustring> 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<double, 3>;
|
|
using CMatrix = std::array<CVector, 3>;
|
|
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<cmsCIEXYZ *>(cmsReadTag(prof, cmsSigMediaWhitePointTag));
|
|
cmsCIEXYZ *red = static_cast<cmsCIEXYZ *>(cmsReadTag(prof, cmsSigRedMatrixColumnTag));
|
|
cmsCIEXYZ *green = static_cast<cmsCIEXYZ *>(cmsReadTag(prof, cmsSigGreenMatrixColumnTag));
|
|
cmsCIEXYZ *blue = static_cast<cmsCIEXYZ *>(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<PMatrix> 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<Glib::ustring, cmsHPROFILE>;
|
|
using MatrixMap = std::map<Glib::ustring, TMatrix>;
|
|
using ContentMap = std::map<Glib::ustring, ProfileContent>;
|
|
using NameMap = std::map<Glib::ustring, Glib::ustring>;
|
|
|
|
ProfileMap wProfiles;
|
|
// ProfileMap wProfilesGamma;
|
|
MatrixMap wMatrices;
|
|
MatrixMap iwMatrices;
|
|
|
|
std::vector<std::unique_ptr<PMatrix>> pMatrices;
|
|
|
|
// These contain profiles from user/system directory(supplied on init)
|
|
Glib::ustring profilesDir;
|
|
Glib::ustring userICCDir;
|
|
ProfileMap fileProfiles;
|
|
ContentMap fileProfileContents;
|
|
|
|
//These contain standard profiles from RT. Keys are all in uppercase.
|
|
Glib::ustring stdProfilesDir;
|
|
NameMap fileStdProfilesFileNames;
|
|
ProfileMap fileStdProfiles;
|
|
|
|
Glib::ustring defaultMonitorProfile;
|
|
|
|
bool loadAll;
|
|
|
|
const cmsHPROFILE xyz;
|
|
const cmsHPROFILE srgb;
|
|
|
|
mutable MyMutex mutex;
|
|
};
|
|
|
|
rtengine::ICCStore* rtengine::ICCStore::getInstance()
|
|
{
|
|
static rtengine::ICCStore instance;
|
|
return &instance;
|
|
}
|
|
|
|
void rtengine::ICCStore::init(const Glib::ustring& usrICCDir, const Glib::ustring& stdICCDir, bool loadAll)
|
|
{
|
|
implementation->init(usrICCDir, stdICCDir, loadAll);
|
|
}
|
|
|
|
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);
|
|
// }
|
|
|
|
rtengine::TMatrix rtengine::ICCStore::workingSpaceMatrix(const Glib::ustring& name) const
|
|
{
|
|
return implementation->workingSpaceMatrix(name);
|
|
}
|
|
|
|
rtengine::TMatrix rtengine::ICCStore::workingSpaceInverseMatrix(const Glib::ustring& name) const
|
|
{
|
|
return implementation->workingSpaceInverseMatrix(name);
|
|
}
|
|
|
|
bool rtengine::ICCStore::outputProfileExist(const Glib::ustring& name) const
|
|
{
|
|
return implementation->outputProfileExist(name);
|
|
}
|
|
|
|
cmsHPROFILE rtengine::ICCStore::getProfile(const Glib::ustring& name) const
|
|
{
|
|
return implementation->getProfile(name);
|
|
}
|
|
|
|
cmsHPROFILE rtengine::ICCStore::getStdProfile(const Glib::ustring& name) const
|
|
{
|
|
return implementation->getStdProfile(name);
|
|
}
|
|
|
|
rtengine::ProfileContent rtengine::ICCStore::getContent(const Glib::ustring& name) const
|
|
{
|
|
return implementation->getContent(name);
|
|
}
|
|
|
|
|
|
Glib::ustring rtengine::ICCStore::getDefaultMonitorProfileName() const
|
|
{
|
|
return implementation->getDefaultMonitorProfileName();
|
|
}
|
|
|
|
|
|
void rtengine::ICCStore::setDefaultMonitorProfileName(const Glib::ustring &name)
|
|
{
|
|
implementation->setDefaultMonitorProfileName(name);
|
|
}
|
|
|
|
cmsHPROFILE rtengine::ICCStore::getXYZProfile() const
|
|
{
|
|
return implementation->getXYZProfile();
|
|
}
|
|
|
|
cmsHPROFILE rtengine::ICCStore::getsRGBProfile() const
|
|
{
|
|
return implementation->getsRGBProfile();
|
|
}
|
|
|
|
std::vector<Glib::ustring> rtengine::ICCStore::getProfiles(ProfileType type) const
|
|
{
|
|
return implementation->getProfiles(type);
|
|
}
|
|
|
|
std::vector<Glib::ustring> rtengine::ICCStore::getProfilesFromDir(const Glib::ustring& dirName) const
|
|
{
|
|
return implementation->getProfilesFromDir(dirName);
|
|
}
|
|
|
|
std::uint8_t rtengine::ICCStore::getInputIntents(cmsHPROFILE profile) const
|
|
{
|
|
return implementation->getInputIntents(profile);
|
|
}
|
|
|
|
std::uint8_t rtengine::ICCStore::getOutputIntents(cmsHPROFILE profile) const
|
|
{
|
|
return implementation->getOutputIntents(profile);
|
|
}
|
|
|
|
std::uint8_t rtengine::ICCStore::getProofIntents(cmsHPROFILE profile) const
|
|
{
|
|
return implementation->getProofIntents(profile);
|
|
}
|
|
|
|
std::uint8_t rtengine::ICCStore::getInputIntents(const Glib::ustring& name) const
|
|
{
|
|
return implementation->getInputIntents(name);
|
|
}
|
|
|
|
std::uint8_t rtengine::ICCStore::getOutputIntents(const Glib::ustring& name) const
|
|
{
|
|
return implementation->getOutputIntents(name);
|
|
}
|
|
|
|
std::uint8_t rtengine::ICCStore::getProofIntents(const Glib::ustring& name) const
|
|
{
|
|
return implementation->getProofIntents(name);
|
|
}
|
|
|
|
rtengine::ICCStore::ICCStore() :
|
|
implementation(new Implementation)
|
|
{
|
|
}
|
|
|
|
rtengine::ICCStore::~ICCStore() = default;
|
|
|
|
std::vector<Glib::ustring> rtengine::ICCStore::getWorkingProfiles()
|
|
{
|
|
return implementation->getWorkingProfiles();
|
|
}
|
|
|
|
std::vector<Glib::ustring> rtengine::ICCStore::getGamma()
|
|
{
|
|
|
|
std::vector<Glib::ustring> res;
|
|
|
|
for (unsigned int i = 0; i < sizeof(wpgamma) / sizeof(wpgamma[0]); i++) {
|
|
res.push_back(wpgamma[i]);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
void rtengine::ICCStore::getGammaArray(const procparams::ColorManagementParams &icm, GammaValues &ga)
|
|
{
|
|
const double eps = 0.000000001; // not divide by zero
|
|
if (!icm.freegamma) {//if Free gamma not selected
|
|
// gamma : ga[0],ga[1],ga[2],ga[3],ga[4],ga[5] by calcul
|
|
if(icm.gamma == "BT709_g2.2_s4.5") {
|
|
ga[0] = 2.22; //BT709 2.2 4.5 - my preferred as D.Coffin
|
|
ga[1] = 0.909995;
|
|
ga[2] = 0.090005;
|
|
ga[3] = 0.222222;
|
|
ga[4] = 0.081071;
|
|
} else if (icm.gamma == "sRGB_g2.4_s12.92") {
|
|
ga[0] = 2.40; //sRGB 2.4 12.92 - RT default as Lightroom
|
|
ga[1] = 0.947858;
|
|
ga[2] = 0.052142;
|
|
ga[3] = 0.077399;
|
|
ga[4] = 0.039293;
|
|
} else if (icm.gamma == "High_g1.3_s3.35") {
|
|
ga[0] = 1.3 ; //for high dynamic images
|
|
ga[1] = 0.998279;
|
|
ga[2] = 0.001721;
|
|
ga[3] = 0.298507;
|
|
ga[4] = 0.005746;
|
|
} else if (icm.gamma == "Low_g2.6_s6.9") {
|
|
ga[0] = 2.6 ; //gamma 2.6 variable : for low contrast images
|
|
ga[1] = 0.891161;
|
|
ga[2] = 0.108839;
|
|
ga[3] = 0.144928;
|
|
ga[4] = 0.076332;
|
|
} else if (icm.gamma == "standard_g2.2") {
|
|
ga[0] = 2.2; //gamma=2.2(as gamma of Adobe, Widegamut...)
|
|
ga[1] = 1.;
|
|
ga[2] = 0.;
|
|
ga[3] = 1. / eps;
|
|
ga[4] = 0.;
|
|
} else if (icm.gamma == "standard_g1.8") {
|
|
ga[0] = 1.8; //gamma=1.8(as gamma of Prophoto)
|
|
ga[1] = 1.;
|
|
ga[2] = 0.;
|
|
ga[3] = 1. / eps;
|
|
ga[4] = 0.;
|
|
} else /* if (icm.gamma == "linear_g1.0") */ {
|
|
ga[0] = 1.0; //gamma=1 linear : for high dynamic images(cf : D.Coffin...)
|
|
ga[1] = 1.;
|
|
ga[2] = 0.;
|
|
ga[3] = 1. / eps;
|
|
ga[4] = 0.;
|
|
}
|
|
ga[5] = 0.0;
|
|
ga[6] = 0.0;
|
|
} else { //free gamma selected
|
|
GammaValues g_a; //gamma parameters
|
|
double pwr = 1.0 / icm.gampos;
|
|
double ts = icm.slpos;
|
|
double slope = icm.slpos == 0 ? eps : icm.slpos;
|
|
|
|
int mode = 0;
|
|
Color::calcGamma(pwr, ts, mode, g_a); // call to calcGamma with selected gamma and slope : return parameters for LCMS2
|
|
ga[4] = g_a[3] * ts;
|
|
//printf("g_a.gamma0=%f g_a.gamma1=%f g_a.gamma2=%f g_a.gamma3=%f g_a.gamma4=%f\n", g_a.gamma0,g_a.gamma1,g_a.gamma2,g_a.gamma3,g_a.gamma4);
|
|
ga[0] = icm.gampos;
|
|
ga[1] = 1. /(1.0 + g_a[4]);
|
|
ga[2] = g_a[4] /(1.0 + g_a[4]);
|
|
ga[3] = 1. / slope;
|
|
ga[5] = 0.0;
|
|
ga[6] = 0.0;
|
|
//printf("ga[0]=%f ga[1]=%f ga[2]=%f ga[3]=%f ga[4]=%f\n", ga[0],ga[1],ga[2],ga[3],ga[4]);
|
|
}
|
|
}
|
|
|
|
// WARNING: the caller must lock lcmsMutex
|
|
cmsHPROFILE rtengine::ICCStore::makeStdGammaProfile(cmsHPROFILE iprof)
|
|
{
|
|
// forgive me for the messy code, quick hack to change gamma of an ICC profile to the RT standard gamma
|
|
if (!iprof) {
|
|
return nullptr;
|
|
}
|
|
|
|
cmsUInt32Number bytesNeeded = 0;
|
|
cmsSaveProfileToMem(iprof, nullptr, &bytesNeeded);
|
|
|
|
if (bytesNeeded == 0) {
|
|
return nullptr;
|
|
}
|
|
|
|
uint8_t *data = new uint8_t[bytesNeeded + 1];
|
|
cmsSaveProfileToMem(iprof, data, &bytesNeeded);
|
|
const uint8_t *p = &data[128]; // skip 128 byte header
|
|
uint32_t tag_count;
|
|
memcpy(&tag_count, p, 4);
|
|
p += 4;
|
|
tag_count = ntohl(tag_count);
|
|
|
|
struct icctag {
|
|
uint32_t sig;
|
|
uint32_t offset;
|
|
uint32_t size;
|
|
} tags[tag_count];
|
|
|
|
const uint32_t gamma = 0x239;
|
|
int gamma_size = 14;
|
|
int data_size =(gamma_size + 3) & ~3;
|
|
|
|
for (uint32_t i = 0; i < tag_count; i++) {
|
|
memcpy(&tags[i], p, 12);
|
|
tags[i].sig = ntohl(tags[i].sig);
|
|
tags[i].offset = ntohl(tags[i].offset);
|
|
tags[i].size = ntohl(tags[i].size);
|
|
p += 12;
|
|
|
|
if (tags[i].sig != 0x62545243 && // bTRC
|
|
tags[i].sig != 0x67545243 && // gTRC
|
|
tags[i].sig != 0x72545243 && // rTRC
|
|
tags[i].sig != 0x6B545243) { // kTRC
|
|
data_size +=(tags[i].size + 3) & ~3;
|
|
}
|
|
}
|
|
|
|
uint32_t sz = 128 + 4 + tag_count * 12 + data_size;
|
|
uint8_t *nd = new uint8_t[sz];
|
|
memset(nd, 0, sz);
|
|
memcpy(nd, data, 128 + 4);
|
|
sz = htonl(sz);
|
|
memcpy(nd, &sz, 4);
|
|
uint32_t offset = 128 + 4 + tag_count * 12;
|
|
uint32_t gamma_offset = 0;
|
|
|
|
for (uint32_t i = 0; i < tag_count; i++) {
|
|
struct icctag tag;
|
|
tag.sig = htonl(tags[i].sig);
|
|
|
|
if (tags[i].sig == 0x62545243 || // bTRC
|
|
tags[i].sig == 0x67545243 || // gTRC
|
|
tags[i].sig == 0x72545243 || // rTRC
|
|
tags[i].sig == 0x6B545243) { // kTRC
|
|
if (gamma_offset == 0) {
|
|
gamma_offset = offset;
|
|
uint32_t pcurve[] = { htonl(0x63757276), htonl(0), htonl(gamma_size == 12 ? 0U : 1U) };
|
|
memcpy(&nd[offset], pcurve, 12);
|
|
|
|
if (gamma_size == 14) {
|
|
uint16_t gm = htons(gamma);
|
|
memcpy(&nd[offset + 12], &gm, 2);
|
|
}
|
|
|
|
offset +=(gamma_size + 3) & ~3;
|
|
}
|
|
|
|
tag.offset = htonl(gamma_offset);
|
|
tag.size = htonl(gamma_size);
|
|
} else {
|
|
tag.offset = htonl(offset);
|
|
tag.size = htonl(tags[i].size);
|
|
memcpy(&nd[offset], &data[tags[i].offset], tags[i].size);
|
|
offset +=(tags[i].size + 3) & ~3;
|
|
}
|
|
|
|
memcpy(&nd[128 + 4 + i * 12], &tag, 12);
|
|
}
|
|
|
|
cmsHPROFILE oprof = cmsOpenProfileFromMem(nd, ntohl(sz));
|
|
delete [] nd;
|
|
delete [] data;
|
|
return oprof;
|
|
}
|
|
|
|
cmsHPROFILE rtengine::ICCStore::createFromMatrix(const double matrix[3][3], bool gamma, const Glib::ustring& name)
|
|
{
|
|
|
|
static const unsigned phead[] = {
|
|
1024, 0, 0x2100000, 0x6d6e7472, 0x52474220, 0x58595a20, 0, 0, 0,
|
|
0x61637370, 0, 0, 0, 0, 0, 0, 0, 0xf6d6, 0x10000, 0xd32d
|
|
};
|
|
unsigned pbody[] = {
|
|
10, 0x63707274, 0, 36, /* cprt */
|
|
0x64657363, 0, 40, /* desc */
|
|
0x77747074, 0, 20, /* wtpt */
|
|
0x626b7074, 0, 20, /* bkpt */
|
|
0x72545243, 0, 14, /* rTRC */
|
|
0x67545243, 0, 14, /* gTRC */
|
|
0x62545243, 0, 14, /* bTRC */
|
|
0x7258595a, 0, 20, /* rXYZ */
|
|
0x6758595a, 0, 20, /* gXYZ */
|
|
0x6258595a, 0, 20
|
|
}; /* bXYZ */
|
|
static const unsigned pwhite[] = { 0xf351, 0x10000, 0x116cc };//D65
|
|
//static const unsigned pwhite[] = { 0xf6d6, 0x10000, 0xd340 };//D50
|
|
|
|
// 0x63757276 : curveType, 0 : reserved, 1 : entries(1=gamma, 0=identity), 0x1000000=1.0
|
|
unsigned pcurve[] = { 0x63757276, 0, 0, 0x1000000 };
|
|
// unsigned pcurve[] = { 0x63757276, 0, 1, 0x1000000 };
|
|
|
|
if (gamma) {
|
|
pcurve[2] = 1;
|
|
// pcurve[3] = 0x1f00000;// pcurve for gamma BT709 : g=2.22 s=4.5
|
|
// normalize gamma in RT, default(Emil's choice = sRGB)
|
|
pcurve[3] = 0x2390000;//pcurve for gamma sRGB : g:2.4 s=12.92
|
|
|
|
} else {
|
|
// lcms2 up to 2.4 has a bug with linear gamma causing precision loss(banding)
|
|
// of floating point data when a normal icc encoding of linear gamma is used
|
|
//(i e 0 table entries), but by encoding a gamma curve which is 1.0 the
|
|
// floating point path is taken within lcms2 so no precision loss occurs and
|
|
// gamma is still 1.0.
|
|
pcurve[2] = 1;
|
|
pcurve[3] = 0x1000000; //pcurve for gamma 1
|
|
}
|
|
|
|
// constructing profile header
|
|
unsigned* oprof = new unsigned [phead[0] / sizeof(unsigned)];
|
|
memset(oprof, 0, phead[0]);
|
|
memcpy(oprof, phead, sizeof(phead));
|
|
|
|
oprof[0] = 132 + 12 * pbody[0];
|
|
|
|
// constructing tag directory(pointers inside the file), and types
|
|
// 0x74657874 : text
|
|
// 0x64657363 : description tag
|
|
for (unsigned int i = 0; i < pbody[0]; i++) {
|
|
oprof[oprof[0] / 4] = i ?(i > 1 ? 0x58595a20 : 0x64657363) : 0x74657874;
|
|
pbody[i * 3 + 2] = oprof[0];
|
|
oprof[0] +=(pbody[i * 3 + 3] + 3) & -4;
|
|
}
|
|
|
|
memcpy(oprof + 32, pbody, sizeof(pbody));
|
|
|
|
// wtpt
|
|
memcpy((char *)oprof + pbody[8] + 8, pwhite, sizeof(pwhite));
|
|
|
|
// r/g/b TRC
|
|
for (int i = 4; i < 7; i++) {
|
|
memcpy((char *)oprof + pbody[i * 3 + 2], pcurve, sizeof(pcurve));
|
|
}
|
|
|
|
// r/g/b XYZ
|
|
// pseudoinverse((double(*)[3]) out_rgb[output_color-1], inverse, 3);
|
|
for (int i = 0; i < 3; i++)
|
|
for (int j = 0; j < 3; j++) {
|
|
oprof[pbody[j * 3 + 23] / 4 + i + 2] = matrix[i][j] * 0x10000 + 0.5;
|
|
// for (num = k=0; k < 3; k++)
|
|
// num += xyzd50_srgb[i][k] * inverse[j][k];
|
|
}
|
|
|
|
// convert to network byte order
|
|
for (unsigned int i = 0; i < phead[0] / 4; i++) {
|
|
oprof[i] = htonl(oprof[i]);
|
|
}
|
|
|
|
// cprt
|
|
strcpy((char *)oprof + pbody[2] + 8, "--rawtherapee profile--");
|
|
|
|
// desc
|
|
oprof[pbody[5] / 4 + 2] = name.size() + 1;
|
|
strcpy((char *)oprof + pbody[5] + 12, name.c_str());
|
|
|
|
|
|
cmsHPROFILE p = cmsOpenProfileFromMem(oprof, ntohl(oprof[0]));
|
|
delete [] oprof;
|
|
return p;
|
|
}
|
|
|
|
cmsHPROFILE rtengine::ICCStore::createGammaProfile(const procparams::ColorManagementParams &icm, GammaValues &ga)
|
|
{
|
|
float p[6]; //primaries
|
|
ga[6] = 0.0;
|
|
|
|
enum class ColorTemp {
|
|
D50 = 5003, // for Widegamut, Prophoto Best, Beta -> D50
|
|
D65 = 6504 // for sRGB, AdobeRGB, Bruce Rec2020 -> D65
|
|
};
|
|
ColorTemp temp = ColorTemp::D50;
|
|
|
|
//primaries for 7 working profiles ==> output profiles
|
|
// eventually to adapt primaries if RT used special profiles !
|
|
if (icm.output == "WideGamut") {
|
|
p[0] = 0.7350; //Widegamut primaries
|
|
p[1] = 0.2650;
|
|
p[2] = 0.1150;
|
|
p[3] = 0.8260;
|
|
p[4] = 0.1570;
|
|
p[5] = 0.0180;
|
|
} else if (icm.output == "Adobe RGB") {
|
|
p[0] = 0.6400; //Adobe primaries
|
|
p[1] = 0.3300;
|
|
p[2] = 0.2100;
|
|
p[3] = 0.7100;
|
|
p[4] = 0.1500;
|
|
p[5] = 0.0600;
|
|
temp = ColorTemp::D65;
|
|
} else if (icm.output == "sRGB") {
|
|
p[0] = 0.6400; // sRGB primaries
|
|
p[1] = 0.3300;
|
|
p[2] = 0.3000;
|
|
p[3] = 0.6000;
|
|
p[4] = 0.1500;
|
|
p[5] = 0.0600;
|
|
temp = ColorTemp::D65;
|
|
} else if (icm.output == "BruceRGB") {
|
|
p[0] = 0.6400; // Bruce primaries
|
|
p[1] = 0.3300;
|
|
p[2] = 0.2800;
|
|
p[3] = 0.6500;
|
|
p[4] = 0.1500;
|
|
p[5] = 0.0600;
|
|
temp = ColorTemp::D65;
|
|
} else if (icm.output == "Beta RGB") {
|
|
p[0] = 0.6888; // Beta primaries
|
|
p[1] = 0.3112;
|
|
p[2] = 0.1986;
|
|
p[3] = 0.7551;
|
|
p[4] = 0.1265;
|
|
p[5] = 0.0352;
|
|
} else if (icm.output == "BestRGB") {
|
|
p[0] = 0.7347; // Best primaries
|
|
p[1] = 0.2653;
|
|
p[2] = 0.2150;
|
|
p[3] = 0.7750;
|
|
p[4] = 0.1300;
|
|
p[5] = 0.0350;
|
|
} else if (icm.output == "Rec2020") {
|
|
p[0] = 0.7080; // Rec2020 primaries
|
|
p[1] = 0.2920;
|
|
p[2] = 0.1700;
|
|
p[3] = 0.7970;
|
|
p[4] = 0.1310;
|
|
p[5] = 0.0460;
|
|
temp = ColorTemp::D65;
|
|
} else {
|
|
p[0] = 0.7347; //ProPhoto and default primaries
|
|
p[1] = 0.2653;
|
|
p[2] = 0.1596;
|
|
p[3] = 0.8404;
|
|
p[4] = 0.0366;
|
|
p[5] = 0.0001;
|
|
}
|
|
|
|
cmsCIExyY xyD;
|
|
cmsCIExyYTRIPLE Primaries = {
|
|
{p[0], p[1], 1.0}, // red
|
|
{p[2], p[3], 1.0}, // green
|
|
{p[4], p[5], 1.0} // blue
|
|
};
|
|
cmsToneCurve* GammaTRC[3];
|
|
|
|
// 7 parameters for smoother curves
|
|
cmsFloat64Number Parameters[7] = { ga[0], ga[1], ga[2], ga[3], ga[4], ga[5], ga[6] } ;
|
|
|
|
//lcmsMutex->lock(); Mutex acquired by the caller
|
|
cmsWhitePointFromTemp(&xyD,(double)temp);
|
|
GammaTRC[0] = GammaTRC[1] = GammaTRC[2] = cmsBuildParametricToneCurve(nullptr, 5, Parameters); //5 = smoother than 4
|
|
cmsHPROFILE oprofdef = cmsCreateRGBProfile(&xyD, &Primaries, GammaTRC); //oprofdef become Outputprofile
|
|
cmsFreeToneCurve(GammaTRC[0]);
|
|
//lcmsMutex->unlock();
|
|
|
|
return oprofdef;
|
|
}
|
|
|
|
// WARNING: the caller must lock lcmsMutex
|
|
cmsHPROFILE rtengine::ICCStore::createCustomGammaOutputProfile(const procparams::ColorManagementParams &icm, GammaValues &ga)
|
|
{
|
|
bool pro = false;
|
|
Glib::ustring outProfile;
|
|
cmsHPROFILE outputProfile = nullptr;
|
|
|
|
if (icm.freegamma && icm.gampos < 1.35) {
|
|
pro = true; //select profil with gammaTRC modified :
|
|
} else if (icm.gamma == "linear_g1.0" ||(icm.gamma == "High_g1.3_s3.35")) {
|
|
pro = true; //pro=0 RT_sRGB || Prophoto
|
|
}
|
|
|
|
// Check that output profiles exist, otherwise use LCMS2
|
|
// Use the icc/icm profiles associated to possible working profiles, set in "options"
|
|
if (icm.working == "ProPhoto" && rtengine::ICCStore::getInstance()->outputProfileExist(options.rtSettings.prophoto) && !pro) {
|
|
outProfile = options.rtSettings.prophoto;
|
|
} else if (icm.working == "Adobe RGB" && rtengine::ICCStore::getInstance()->outputProfileExist(options.rtSettings.adobe) ) {
|
|
outProfile = options.rtSettings.adobe;
|
|
} else if (icm.working == "WideGamut" && rtengine::ICCStore::getInstance()->outputProfileExist(options.rtSettings.widegamut) ) {
|
|
outProfile = options.rtSettings.widegamut;
|
|
} else if (icm.working == "Beta RGB" && rtengine::ICCStore::getInstance()->outputProfileExist(options.rtSettings.beta) ) {
|
|
outProfile = options.rtSettings.beta;
|
|
} else if (icm.working == "BestRGB" && rtengine::ICCStore::getInstance()->outputProfileExist(options.rtSettings.best) ) {
|
|
outProfile = options.rtSettings.best;
|
|
} else if (icm.working == "BruceRGB" && rtengine::ICCStore::getInstance()->outputProfileExist(options.rtSettings.bruce) ) {
|
|
outProfile = options.rtSettings.bruce;
|
|
} else if (icm.working == "sRGB" && rtengine::ICCStore::getInstance()->outputProfileExist(options.rtSettings.srgb) && !pro) {
|
|
outProfile = options.rtSettings.srgb;
|
|
} else if (icm.working == "sRGB" && rtengine::ICCStore::getInstance()->outputProfileExist(options.rtSettings.srgb10) && pro) {
|
|
outProfile = options.rtSettings.srgb10;
|
|
} else if (icm.working == "ProPhoto" && rtengine::ICCStore::getInstance()->outputProfileExist(options.rtSettings.prophoto10) && pro) {
|
|
outProfile = options.rtSettings.prophoto10;
|
|
} else if (icm.working == "Rec2020" && rtengine::ICCStore::getInstance()->outputProfileExist(options.rtSettings.rec2020) ) {
|
|
outProfile = options.rtSettings.rec2020;
|
|
} else {
|
|
// Should not occurs
|
|
if (settings->verbose) {
|
|
printf("\"%s\": unknown working profile! - use LCMS2 substitution\n", icm.working.c_str() );
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
//begin adaptation rTRC gTRC bTRC
|
|
//"outputProfile" profile has the same characteristics than RGB values, but TRC are adapted... for applying profile
|
|
if (settings->verbose) {
|
|
printf("Output Gamma - profile: \"%s\"\n", outProfile.c_str() ); //c_str()
|
|
}
|
|
|
|
outputProfile = ICCStore::getInstance()->getProfile(outProfile); //get output profile
|
|
|
|
if (outputProfile == nullptr) {
|
|
|
|
if (settings->verbose) {
|
|
printf("\"%s\" ICC output profile not found!\n", outProfile.c_str());
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// 7 parameters for smoother curves
|
|
cmsFloat64Number Parameters[7] = { ga[0], ga[1], ga[2], ga[3], ga[4], ga[5], ga[6] };
|
|
|
|
//change desc Tag , to "free gamma", or "BT709", etc.
|
|
cmsMLU *mlu;
|
|
cmsContext ContextID = cmsGetProfileContextID(outputProfile); // create context to modify some TAGs
|
|
mlu = cmsMLUalloc(ContextID, 1);
|
|
|
|
// instruction with //ICC are used to generate ICC profile
|
|
if (mlu == nullptr) {
|
|
printf("Description error\n");
|
|
} else {
|
|
|
|
// Description TAG : selection of gamma and Primaries
|
|
if (!icm.freegamma) {
|
|
std::wstring gammaStr;
|
|
|
|
if(icm.gamma == "High_g1.3_s3.35") {
|
|
gammaStr = std::wstring(L"GammaTRC: High g=1.3 s=3.35");
|
|
} else if (icm.gamma == "Low_g2.6_s6.9") {
|
|
gammaStr = std::wstring(L"GammaTRC: Low g=2.6 s=6.9");
|
|
} else if (icm.gamma == "sRGB_g2.4_s12.92") {
|
|
gammaStr = std::wstring(L"GammaTRC: sRGB g=2.4 s=12.92");
|
|
} else if (icm.gamma == "BT709_g2.2_s4.5") {
|
|
gammaStr = std::wstring(L"GammaTRC: BT709 g=2.2 s=4.5");
|
|
} else if (icm.gamma == "linear_g1.0") {
|
|
gammaStr = std::wstring(L"GammaTRC: Linear g=1.0");
|
|
} else if (icm.gamma == "standard_g2.2") {
|
|
gammaStr = std::wstring(L"GammaTRC: g=2.2");
|
|
} else if (icm.gamma == "standard_g1.8") {
|
|
gammaStr = std::wstring(L"GammaTRC: g=1.8");
|
|
}
|
|
|
|
cmsMLUsetWide(mlu, "en", "US", gammaStr.c_str());
|
|
} else {
|
|
// create description with gamma + slope + primaries
|
|
std::wostringstream gammaWs;
|
|
gammaWs.precision(2);
|
|
gammaWs << "Manual GammaTRC: g=" <<(float)icm.gampos << " s=" <<(float)icm.slpos;
|
|
|
|
cmsMLUsetWide(mlu, "en", "US", gammaWs.str().c_str());
|
|
}
|
|
|
|
cmsWriteTag(outputProfile, cmsSigProfileDescriptionTag, mlu);//desc changed
|
|
|
|
/*
|
|
cmsMLUsetWide(mlu, "en", "US", L"General Public License - AdobeRGB compatible");//adapt to profil
|
|
cmsWriteTag(outputProfile, cmsSigCopyrightTag, mlu);
|
|
|
|
cmsMLUsetWide(mlu, "en", "US", L"RawTherapee");
|
|
cmsWriteTag(outputProfile, cmsSigDeviceMfgDescTag, mlu);
|
|
|
|
cmsMLUsetWide(mlu, "en", "US", L"RTMedium"); //adapt to profil
|
|
cmsWriteTag(outputProfile, cmsSigDeviceModelDescTag, mlu);
|
|
|
|
*/
|
|
|
|
cmsMLUfree(mlu);
|
|
}
|
|
|
|
// Calculate output profile's rTRC gTRC bTRC
|
|
cmsToneCurve* GammaTRC = cmsBuildParametricToneCurve(nullptr, 5, Parameters);
|
|
cmsWriteTag(outputProfile, cmsSigRedTRCTag,(void*)GammaTRC );
|
|
cmsWriteTag(outputProfile, cmsSigGreenTRCTag,(void*)GammaTRC );
|
|
cmsWriteTag(outputProfile, cmsSigBlueTRCTag,(void*)GammaTRC );
|
|
|
|
if (GammaTRC) {
|
|
cmsFreeToneCurve(GammaTRC);
|
|
}
|
|
|
|
return outputProfile;
|
|
}
|