File indexing completed on 2024-04-21 15:08:51

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 }