File indexing completed on 2024-04-28 11:50:50
0001 /*************************************************************************** 0002 * Copyright (C) 2012-2016 by Daniel Nicoletti <dantti12@gmail.com> * 0003 * * 0004 * This program is free software; you can redistribute it and/or modify * 0005 * it under the terms of the GNU General Public License as published by * 0006 * the Free Software Foundation; either version 2 of the License, or * 0007 * (at your option) any later version. * 0008 * * 0009 * This program is distributed in the hope that it will be useful, * 0010 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0012 * GNU General Public License for more details. * 0013 * * 0014 * You should have received a copy of the GNU General Public License * 0015 * along with this program; see the file COPYING. If not, write to * 0016 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * 0017 * Boston, MA 02110-1301, USA. * 0018 ***************************************************************************/ 0019 0020 #include "ProfileUtils.h" 0021 0022 #include "DmiUtils.h" 0023 #include "Edid.h" 0024 0025 #include <QCryptographicHash> 0026 #include <QFileInfo> 0027 0028 #include <QLoggingCategory> 0029 0030 #include <version.h> 0031 0032 #define PACKAGE_NAME PROJECT_NAME 0033 #define PACKAGE_VERSION COLORD_KDE_VERSION_STRING 0034 0035 /* defined in metadata-spec.txt */ 0036 #define CD_PROFILE_METADATA_STANDARD_SPACE "STANDARD_space" 0037 #define CD_PROFILE_METADATA_EDID_MD5 "EDID_md5" 0038 #define CD_PROFILE_METADATA_EDID_MODEL "EDID_model" 0039 #define CD_PROFILE_METADATA_EDID_SERIAL "EDID_serial" 0040 #define CD_PROFILE_METADATA_EDID_MNFT "EDID_mnft" 0041 #define CD_PROFILE_METADATA_EDID_VENDOR "EDID_manufacturer" 0042 #define CD_PROFILE_METADATA_FILE_CHECKSUM "FILE_checksum" 0043 #define CD_PROFILE_METADATA_CMF_PRODUCT "CMF_product" 0044 #define CD_PROFILE_METADATA_CMF_BINARY "CMF_binary" 0045 #define CD_PROFILE_METADATA_CMF_VERSION "CMF_version" 0046 #define CD_PROFILE_METADATA_DATA_SOURCE "DATA_source" 0047 #define CD_PROFILE_METADATA_DATA_SOURCE_EDID "edid" 0048 #define CD_PROFILE_METADATA_DATA_SOURCE_CALIB "calib" 0049 #define CD_PROFILE_METADATA_MAPPING_FORMAT "MAPPING_format" 0050 #define CD_PROFILE_METADATA_MAPPING_QUALIFIER "MAPPING_qualifier" 0051 0052 Q_DECLARE_LOGGING_CATEGORY(COLORD) 0053 0054 QString ProfileUtils::profileHash(QFile &profile) 0055 { 0056 QString checksum; 0057 cmsHPROFILE lcms_profile = nullptr; 0058 0059 /* get the internal profile id, if it exists */ 0060 lcms_profile = cmsOpenProfileFromFile(profile.fileName().toUtf8(), "r"); 0061 if (lcms_profile == nullptr) { 0062 // Compute the hash from the whole file.. 0063 return QCryptographicHash::hash(profile.readAll(), QCryptographicHash::Md5).toHex(); 0064 } else { 0065 checksum = getPrecookedMd5(lcms_profile); 0066 if (lcms_profile != nullptr) { 0067 cmsCloseProfile(lcms_profile); 0068 } 0069 0070 if (checksum.isNull()) { 0071 // Compute the hash from the whole file.. 0072 return QCryptographicHash::hash(profile.readAll(), QCryptographicHash::Md5).toHex(); 0073 } else { 0074 return checksum; 0075 } 0076 } 0077 } 0078 0079 QString ProfileUtils::getPrecookedMd5(cmsHPROFILE lcms_profile) 0080 { 0081 cmsUInt8Number profile_id[16]; 0082 bool md5_precooked = false; 0083 QByteArray md5; 0084 0085 /* check to see if we have a pre-cooked MD5 */ 0086 cmsGetHeaderProfileID(lcms_profile, profile_id); 0087 for (int i = 0; i < 16; ++i) { 0088 if (profile_id[i] != 0) { 0089 md5_precooked = true; 0090 break; 0091 } 0092 } 0093 if (!md5_precooked) { 0094 return QString(); 0095 } 0096 0097 /* convert to a hex string */ 0098 for (int i = 0; i < 16; ++i) { 0099 md5.append(profile_id[i]); 0100 } 0101 0102 return md5.toHex(); 0103 } 0104 0105 bool ProfileUtils::createIccProfile(bool isLaptop, const Edid &edid, const QString &filename) 0106 { 0107 cmsCIExyYTRIPLE chroma; 0108 cmsCIExyY white_point; 0109 cmsHPROFILE lcms_profile = nullptr; 0110 cmsToneCurve *transfer_curve[3] = {nullptr, nullptr, nullptr}; 0111 bool ret = false; 0112 cmsHANDLE dict = nullptr; 0113 0114 /* ensure the per-user directory exists */ 0115 // Create dir path if not available 0116 // check if the file doesn't already exist 0117 QFileInfo fileInfo(filename); 0118 if (fileInfo.exists()) { 0119 qCWarning(COLORD) << "EDID ICC Profile already exists" << filename; 0120 if (*transfer_curve != nullptr) 0121 cmsFreeToneCurve(*transfer_curve); 0122 return false; 0123 } 0124 0125 // copy color data from our structures 0126 // Red 0127 chroma.Red.x = edid.red().x(); 0128 chroma.Red.y = edid.red().y(); 0129 // Green 0130 chroma.Green.x = edid.green().x(); 0131 chroma.Green.y = edid.green().y(); 0132 // Blue 0133 chroma.Blue.x = edid.blue().x(); 0134 chroma.Blue.y = edid.blue().y(); 0135 // White 0136 white_point.x = edid.white().x(); 0137 white_point.y = edid.white().y(); 0138 white_point.Y = 1.0; 0139 0140 // estimate the transfer function for the gamma 0141 transfer_curve[0] = transfer_curve[1] = transfer_curve[2] = cmsBuildGamma(nullptr, edid.gamma()); 0142 0143 // create our generated profile 0144 lcms_profile = cmsCreateRGBProfile(&white_point, &chroma, transfer_curve); 0145 if (lcms_profile == nullptr) { 0146 qCWarning(COLORD) << "Failed to create ICC profile on cmsCreateRGBProfile"; 0147 if (*transfer_curve != nullptr) 0148 cmsFreeToneCurve(*transfer_curve); 0149 return false; 0150 } 0151 0152 cmsSetColorSpace(lcms_profile, cmsSigRgbData); 0153 cmsSetPCS(lcms_profile, cmsSigXYZData); 0154 cmsSetHeaderRenderingIntent(lcms_profile, INTENT_RELATIVE_COLORIMETRIC); 0155 cmsSetDeviceClass(lcms_profile, cmsSigDisplayClass); 0156 0157 // copyright 0158 ret = cmsWriteTagTextAscii(lcms_profile, cmsSigCopyrightTag, QStringLiteral("No copyright")); 0159 if (!ret) { 0160 qCWarning(COLORD) << "Failed to write copyright"; 0161 if (*transfer_curve != nullptr) 0162 cmsFreeToneCurve(*transfer_curve); 0163 return false; 0164 } 0165 0166 // set model 0167 QString model; 0168 if (isLaptop) { 0169 model = DmiUtils::deviceModel(); 0170 } else { 0171 model = edid.name(); 0172 } 0173 0174 if (model.isEmpty()) { 0175 model = QStringLiteral("Unknown monitor"); 0176 } 0177 ret = cmsWriteTagTextAscii(lcms_profile, cmsSigDeviceModelDescTag, model); 0178 if (!ret) { 0179 qCWarning(COLORD) << "Failed to write model"; 0180 if (*transfer_curve != nullptr) { 0181 cmsFreeToneCurve(*transfer_curve); 0182 } 0183 return false; 0184 } 0185 0186 // write title 0187 ret = cmsWriteTagTextAscii(lcms_profile, cmsSigProfileDescriptionTag, model); 0188 if (!ret) { 0189 qCWarning(COLORD) << "Failed to write description"; 0190 if (*transfer_curve != nullptr) 0191 cmsFreeToneCurve(*transfer_curve); 0192 return false; 0193 } 0194 0195 // get manufacturer 0196 QString vendor; 0197 if (isLaptop) { 0198 vendor = DmiUtils::deviceVendor(); 0199 } else { 0200 vendor = edid.vendor(); 0201 } 0202 0203 if (vendor.isEmpty()) { 0204 vendor = QStringLiteral("Unknown vendor"); 0205 } 0206 ret = cmsWriteTagTextAscii(lcms_profile, cmsSigDeviceMfgDescTag, vendor); 0207 if (!ret) { 0208 qCWarning(COLORD) << "Failed to write manufacturer"; 0209 if (*transfer_curve != nullptr) 0210 cmsFreeToneCurve(*transfer_curve); 0211 return false; 0212 } 0213 0214 // just create a new dict 0215 dict = cmsDictAlloc(nullptr); 0216 0217 // set the framework creator metadata 0218 cmsDictAddEntryAscii(dict, CD_PROFILE_METADATA_CMF_PRODUCT, PACKAGE_NAME); 0219 cmsDictAddEntryAscii(dict, CD_PROFILE_METADATA_CMF_BINARY, PACKAGE_NAME); 0220 cmsDictAddEntryAscii(dict, CD_PROFILE_METADATA_CMF_VERSION, PACKAGE_VERSION); 0221 0222 /* set the data source so we don't ever prompt the user to 0223 * recalibrate (as the EDID data won't have changed) */ 0224 cmsDictAddEntryAscii(dict, CD_PROFILE_METADATA_DATA_SOURCE, CD_PROFILE_METADATA_DATA_SOURCE_EDID); 0225 0226 // set 'ICC meta Tag for Monitor Profiles' data 0227 cmsDictAddEntryAscii(dict, QStringLiteral("EDID_md5"), edid.hash()); 0228 0229 if (!model.isEmpty()) 0230 cmsDictAddEntryAscii(dict, QStringLiteral("EDID_model"), model); 0231 0232 if (!edid.serial().isEmpty()) { 0233 cmsDictAddEntryAscii(dict, QStringLiteral("EDID_serial"), edid.serial()); 0234 } 0235 0236 if (!edid.pnpId().isEmpty()) { 0237 cmsDictAddEntryAscii(dict, QStringLiteral("EDID_mnft"), edid.pnpId()); 0238 } 0239 0240 if (!vendor.isEmpty()) { 0241 cmsDictAddEntryAscii(dict, QStringLiteral("EDID_manufacturer"), vendor); 0242 } 0243 0244 /* write new tag */ 0245 ret = cmsWriteTag(lcms_profile, cmsSigMetaTag, dict); 0246 if (!ret) { 0247 qCWarning(COLORD) << "Failed to write profile metadata"; 0248 if (*transfer_curve != nullptr) 0249 cmsFreeToneCurve(*transfer_curve); 0250 return false; 0251 } 0252 0253 /* write profile id */ 0254 ret = cmsMD5computeID(lcms_profile); 0255 if (!ret) { 0256 qCWarning(COLORD) << "Failed to write profile id"; 0257 if (dict != nullptr) 0258 cmsDictFree(dict); 0259 if (*transfer_curve != nullptr) 0260 cmsFreeToneCurve(*transfer_curve); 0261 return false; 0262 } 0263 0264 /* save, TODO: get error */ 0265 ret = cmsSaveProfileToFile(lcms_profile, filename.toUtf8()); 0266 0267 if (dict != nullptr) { 0268 cmsDictFree(dict); 0269 } 0270 if (*transfer_curve != nullptr) { 0271 cmsFreeToneCurve(*transfer_curve); 0272 } 0273 0274 return ret; 0275 } 0276 0277 cmsBool ProfileUtils::cmsWriteTagTextAscii(cmsHPROFILE lcms_profile, cmsTagSignature sig, const QString &text) 0278 { 0279 cmsBool ret; 0280 cmsMLU *mlu = cmsMLUalloc(nullptr, 1); 0281 cmsMLUsetASCII(mlu, "EN", "us", text.toLatin1().constData()); 0282 ret = cmsWriteTag(lcms_profile, sig, mlu); 0283 cmsMLUfree(mlu); 0284 return ret; 0285 } 0286 0287 cmsBool ProfileUtils::cmsDictAddEntryAscii(cmsHANDLE dict, const QString &key, const QString &value) 0288 { 0289 qCDebug(COLORD) << key << value; 0290 cmsBool ret; 0291 0292 wchar_t *mb_key = new wchar_t[key.length() + 1]; 0293 if (key.toWCharArray(mb_key) != key.length()) { 0294 delete[] mb_key; 0295 return false; 0296 } 0297 mb_key[key.length()] = 0; 0298 0299 wchar_t *mb_value = new wchar_t[value.length() + 1]; 0300 if (value.toWCharArray(mb_value) != value.length()) { 0301 delete[] mb_key; 0302 delete[] mb_value; 0303 return false; 0304 } 0305 mb_value[value.length()] = 0; 0306 0307 ret = cmsDictAddEntry(dict, mb_key, mb_value, nullptr, nullptr); 0308 delete[] mb_key; 0309 delete[] mb_value; 0310 return ret; 0311 }