File indexing completed on 2024-04-14 14:32:49

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 "Edid.h"
0021 
0022 #include <math.h>
0023 
0024 #include <QCryptographicHash>
0025 #include <QFile>
0026 #include <QStringList>
0027 
0028 #include <QLoggingCategory>
0029 
0030 #define GCM_EDID_OFFSET_PNPID 0x08
0031 #define GCM_EDID_OFFSET_SERIAL 0x0c
0032 #define GCM_EDID_OFFSET_SIZE 0x15
0033 #define GCM_EDID_OFFSET_GAMMA 0x17
0034 #define GCM_EDID_OFFSET_DATA_BLOCKS 0x36
0035 #define GCM_EDID_OFFSET_LAST_BLOCK 0x6c
0036 #define GCM_EDID_OFFSET_EXTENSION_BLOCK_COUNT 0x7e
0037 
0038 #define GCM_DESCRIPTOR_DISPLAY_PRODUCT_NAME 0xfc
0039 #define GCM_DESCRIPTOR_DISPLAY_PRODUCT_SERIAL_NUMBER 0xff
0040 #define GCM_DESCRIPTOR_COLOR_MANAGEMENT_DATA 0xf9
0041 #define GCM_DESCRIPTOR_ALPHANUMERIC_DATA_STRING 0xfe
0042 #define GCM_DESCRIPTOR_COLOR_POINT 0xfb
0043 
0044 #define PNP_IDS "/usr/share/hwdata/pnp.ids"
0045 
0046 Q_DECLARE_LOGGING_CATEGORY(COLORD)
0047 
0048 Edid::Edid()
0049 {
0050 }
0051 
0052 Edid::Edid(const quint8 *data, size_t length)
0053 {
0054     parse(data, length);
0055 }
0056 
0057 bool Edid::isValid() const
0058 {
0059     return m_valid;
0060 }
0061 
0062 QString Edid::deviceId(const QString &fallbackName) const
0063 {
0064     QString id = QStringLiteral("xrandr");
0065     // if no info was added check if the fallbacName is provided
0066     if (vendor().isNull() && name().isNull() && serial().isNull()) {
0067         if (!fallbackName.isEmpty()) {
0068             id.append(QLatin1Char('-') % fallbackName);
0069         } else {
0070             // all info we have are empty strings
0071             id.append(QLatin1String("-unknown"));
0072         }
0073     } else if (m_valid) {
0074         if (!vendor().isNull()) {
0075             id.append(QLatin1Char('-') % vendor());
0076         }
0077         if (!name().isNull()) {
0078             id.append(QLatin1Char('-') % name());
0079         }
0080         if (!serial().isNull()) {
0081             id.append(QLatin1Char('-') % serial());
0082         }
0083     }
0084 
0085     return id;
0086 }
0087 
0088 QString Edid::name() const
0089 {
0090     if (m_valid) {
0091         return m_monitorName;
0092     }
0093     return QString();
0094 }
0095 
0096 QString Edid::vendor() const
0097 {
0098     if (m_valid) {
0099         return m_vendorName;
0100     }
0101     return QString();
0102 }
0103 
0104 QString Edid::serial() const
0105 {
0106     if (m_valid) {
0107         return m_serialNumber;
0108     }
0109     return QString();
0110 }
0111 
0112 QString Edid::eisaId() const
0113 {
0114     if (m_valid) {
0115         return m_eisaId;
0116     }
0117     return QString();
0118 }
0119 
0120 QString Edid::hash() const
0121 {
0122     if (m_valid) {
0123         return m_checksum;
0124     }
0125     return QString();
0126 }
0127 
0128 QString Edid::pnpId() const
0129 {
0130     if (m_valid) {
0131         return m_pnpId;
0132     }
0133     return QString();
0134 }
0135 
0136 uint Edid::width() const
0137 {
0138     return m_width;
0139 }
0140 
0141 uint Edid::height() const
0142 {
0143     return m_height;
0144 }
0145 
0146 qreal Edid::gamma() const
0147 {
0148     return m_gamma;
0149 }
0150 
0151 QQuaternion Edid::red() const
0152 {
0153     return m_red;
0154 }
0155 
0156 QQuaternion Edid::green() const
0157 {
0158     return m_green;
0159 }
0160 
0161 QQuaternion Edid::blue() const
0162 {
0163     return m_blue;
0164 }
0165 
0166 QQuaternion Edid::white() const
0167 {
0168     return m_white;
0169 }
0170 
0171 bool Edid::parse(const quint8 *data, size_t length)
0172 {
0173     quint32 serial;
0174 
0175     /* check header */
0176     if (length < 128) {
0177         qCWarning(COLORD) << "EDID length is too small";
0178         m_valid = false;
0179         return m_valid;
0180     }
0181     if (data[0] != 0x00 || data[1] != 0xff) {
0182         qCWarning(COLORD) << "Failed to parse EDID header";
0183         m_valid = false;
0184         return m_valid;
0185     }
0186 
0187     /* decode the PNP ID from three 5 bit words packed into 2 bytes
0188      * /--08--\/--09--\
0189      * 7654321076543210
0190      * |\---/\---/\---/
0191      * R  C1   C2   C3 */
0192     m_pnpId.resize(3);
0193     m_pnpId[0] = QChar::fromLatin1('A' + ((data[GCM_EDID_OFFSET_PNPID + 0] & 0x7c) / 4) - 1);
0194     m_pnpId[1] = QChar::fromLatin1('A' + ((data[GCM_EDID_OFFSET_PNPID + 0] & 0x3) * 8) + ((data[GCM_EDID_OFFSET_PNPID + 1] & 0xe0) / 32) - 1);
0195     m_pnpId[2] = QChar::fromLatin1('A' + (data[GCM_EDID_OFFSET_PNPID + 1] & 0x1f) - 1);
0196 
0197     // load the PNP_IDS file and load the vendor name
0198     if (!m_pnpId.isEmpty()) {
0199         QFile pnpIds(QLatin1String(PNP_IDS));
0200         if (pnpIds.open(QIODevice::ReadOnly)) {
0201             while (!pnpIds.atEnd()) {
0202                 QString line = pnpIds.readLine();
0203                 if (line.startsWith(m_pnpId)) {
0204                     QStringList parts = line.split(QLatin1Char('\t'));
0205                     if (parts.size() == 2) {
0206                         m_vendorName = line.split(QLatin1Char('\t')).at(1).simplified();
0207                     }
0208                     break;
0209                 }
0210             }
0211             qCDebug(COLORD) << "PNP ID" << m_pnpId << "Vendor Name" << m_vendorName;
0212         }
0213     }
0214 
0215     /* maybe there isn't a ASCII serial number descriptor, so use this instead */
0216     serial = static_cast<quint32>(data[GCM_EDID_OFFSET_SERIAL + 0]);
0217     serial += static_cast<quint32>(data[GCM_EDID_OFFSET_SERIAL + 1] * 0x100);
0218     serial += static_cast<quint32>(data[GCM_EDID_OFFSET_SERIAL + 2] * 0x10000);
0219     serial += static_cast<quint32>(data[GCM_EDID_OFFSET_SERIAL + 3] * 0x1000000);
0220     if (serial > 0) {
0221         m_serialNumber = QString::number(serial);
0222     }
0223 
0224     /* get the size */
0225     m_width = data[GCM_EDID_OFFSET_SIZE + 0];
0226     m_height = data[GCM_EDID_OFFSET_SIZE + 1];
0227 
0228     /* we don't care about aspect */
0229     if (m_width == 0 || m_height == 0) {
0230         m_width = 0;
0231         m_height = 0;
0232     }
0233 
0234     /* get gamma */
0235     if (data[GCM_EDID_OFFSET_GAMMA] == 0xff) {
0236         m_gamma = 1.0f;
0237     } else {
0238         m_gamma = (static_cast<float>(data[GCM_EDID_OFFSET_GAMMA] / 100) + 1);
0239     }
0240 
0241     /* get color red */
0242     m_red.setX(edidDecodeFraction(data[0x1b], edidGetBits(data[0x19], 6, 7)));
0243     m_red.setY(edidDecodeFraction(data[0x1c], edidGetBits(data[0x19], 5, 4)));
0244 
0245     /* get color green */
0246     m_green.setX(edidDecodeFraction(data[0x1d], edidGetBits(data[0x19], 2, 3)));
0247     m_green.setY(edidDecodeFraction(data[0x1e], edidGetBits(data[0x19], 0, 1)));
0248 
0249     /* get color blue */
0250     m_blue.setX(edidDecodeFraction(data[0x1f], edidGetBits(data[0x1a], 6, 7)));
0251     m_blue.setY(edidDecodeFraction(data[0x20], edidGetBits(data[0x1a], 4, 5)));
0252 
0253     /* get color white */
0254     m_white.setX(edidDecodeFraction(data[0x21], edidGetBits(data[0x1a], 2, 3)));
0255     m_white.setY(edidDecodeFraction(data[0x22], edidGetBits(data[0x1a], 0, 1)));
0256 
0257     /* parse EDID data */
0258     for (uint i = GCM_EDID_OFFSET_DATA_BLOCKS; i <= GCM_EDID_OFFSET_LAST_BLOCK; i += 18) {
0259         /* ignore pixel clock data */
0260         if (data[i] != 0) {
0261             continue;
0262         }
0263         if (data[i + 2] != 0) {
0264             continue;
0265         }
0266 
0267         /* any useful blocks? */
0268         if (data[i + 3] == GCM_DESCRIPTOR_DISPLAY_PRODUCT_NAME) {
0269             QString tmp = edidParseString(&data[i + 5]);
0270             if (!tmp.isEmpty()) {
0271                 m_monitorName = tmp;
0272             }
0273         } else if (data[i + 3] == GCM_DESCRIPTOR_DISPLAY_PRODUCT_SERIAL_NUMBER) {
0274             QString tmp = edidParseString(&data[i + 5]);
0275             if (!tmp.isEmpty()) {
0276                 m_serialNumber = tmp;
0277             }
0278         } else if (data[i + 3] == GCM_DESCRIPTOR_COLOR_MANAGEMENT_DATA) {
0279             qCWarning(COLORD) << "failing to parse color management data";
0280         } else if (data[i + 3] == GCM_DESCRIPTOR_ALPHANUMERIC_DATA_STRING) {
0281             QString tmp = edidParseString(&data[i + 5]);
0282             if (!tmp.isEmpty()) {
0283                 m_eisaId = tmp;
0284             }
0285         } else if (data[i + 3] == GCM_DESCRIPTOR_COLOR_POINT) {
0286             if (data[i + 3 + 9] != 0xff) {
0287                 /* extended EDID block(1) which contains
0288                  * a better gamma value */
0289                 m_gamma = ((float)data[i + 3 + 9] / 100) + 1;
0290             }
0291             if (data[i + 3 + 14] != 0xff) {
0292                 /* extended EDID block(2) which contains
0293                  * a better gamma value */
0294                 m_gamma = ((float)data[i + 3 + 9] / 100) + 1;
0295             }
0296         }
0297     }
0298 
0299     // calculate checksum
0300     QCryptographicHash hash(QCryptographicHash::Md5);
0301     hash.addData(reinterpret_cast<const char *>(data), length);
0302     m_checksum = hash.result().toHex();
0303 
0304     m_valid = true;
0305     return m_valid;
0306 }
0307 
0308 int Edid::edidGetBit(int in, int bit) const
0309 {
0310     return (in & (1 << bit)) >> bit;
0311 }
0312 
0313 int Edid::edidGetBits(int in, int begin, int end) const
0314 {
0315     int mask = (1 << (end - begin + 1)) - 1;
0316 
0317     return (in >> begin) & mask;
0318 }
0319 
0320 double Edid::edidDecodeFraction(int high, int low) const
0321 {
0322     double result = 0.0;
0323     int i;
0324 
0325     high = (high << 2) | low;
0326     for (i = 0; i < 10; ++i) {
0327         result += edidGetBit(high, i) * pow(2, i - 10);
0328     }
0329     return result;
0330 }
0331 
0332 QString Edid::edidParseString(const quint8 *data) const
0333 {
0334     QString text;
0335 
0336     /* this is always 13 bytes, but we can't guarantee it's null
0337      * terminated or not junk. */
0338     text = QString::fromLatin1((const char *)data, 13);
0339 
0340     // Remove newlines, extra spaces and stuff
0341     text = text.simplified();
0342 
0343     return text;
0344 }