File indexing completed on 2024-04-21 16:17:34

0001 /*
0002  *   SPDX-FileCopyrightText: 2012 Daniel Nicoletti <dantti12@gmail.com>
0003  *   SPDX-FileCopyrightText: 2012-2014 Daniel Vrátil <dvratil@redhat.com>
0004  *
0005  *   SPDX-License-Identifier: LGPL-2.1-or-later
0006  */
0007 
0008 #include "edid.h"
0009 #include "kscreen_debug_edid.h"
0010 
0011 #include <math.h>
0012 
0013 #include <QCryptographicHash>
0014 #include <QFile>
0015 #include <QStringBuilder>
0016 #include <QStringList>
0017 
0018 #define GCM_EDID_OFFSET_PNPID 0x08
0019 #define GCM_EDID_OFFSET_SERIAL 0x0c
0020 #define GCM_EDID_OFFSET_SIZE 0x15
0021 #define GCM_EDID_OFFSET_GAMMA 0x17
0022 #define GCM_EDID_OFFSET_DATA_BLOCKS 0x36
0023 #define GCM_EDID_OFFSET_LAST_BLOCK 0x6c
0024 #define GCM_EDID_OFFSET_EXTENSION_BLOCK_COUNT 0x7e
0025 
0026 #define GCM_DESCRIPTOR_DISPLAY_PRODUCT_NAME 0xfc
0027 #define GCM_DESCRIPTOR_DISPLAY_PRODUCT_SERIAL_NUMBER 0xff
0028 #define GCM_DESCRIPTOR_COLOR_MANAGEMENT_DATA 0xf9
0029 #define GCM_DESCRIPTOR_ALPHANUMERIC_DATA_STRING 0xfe
0030 #define GCM_DESCRIPTOR_COLOR_POINT 0xfb
0031 
0032 #define PNP_IDS "/usr/share/hwdata/pnp.ids"
0033 
0034 using namespace KScreen;
0035 
0036 class Q_DECL_HIDDEN Edid::Private
0037 {
0038 public:
0039     Private()
0040         : valid(false)
0041         , width(0)
0042         , height(0)
0043         , gamma(0)
0044     {
0045     }
0046 
0047     Private(const Private &other)
0048         : valid(other.valid)
0049         , monitorName(other.monitorName)
0050         , vendorName(other.vendorName)
0051         , serialNumber(other.serialNumber)
0052         , eisaId(other.eisaId)
0053         , checksum(other.checksum)
0054         , pnpId(other.pnpId)
0055         , width(other.width)
0056         , height(other.height)
0057         , gamma(other.gamma)
0058         , red(other.red)
0059         , green(other.green)
0060         , blue(other.blue)
0061         , white(other.white)
0062     {
0063     }
0064 
0065     bool parse(const QByteArray &data);
0066     int edidGetBit(int in, int bit) const;
0067     int edidGetBits(int in, int begin, int end) const;
0068     float edidDecodeFraction(int high, int low) const;
0069     QString edidParseString(const quint8 *data) const;
0070 
0071     bool valid;
0072     QString monitorName;
0073     QString vendorName;
0074     QString serialNumber;
0075     QString eisaId;
0076     QString checksum;
0077     QString pnpId;
0078     uint width;
0079     uint height;
0080     qreal gamma;
0081     QQuaternion red;
0082     QQuaternion green;
0083     QQuaternion blue;
0084     QQuaternion white;
0085 };
0086 
0087 Edid::Edid()
0088     : QObject()
0089     , d(new Private())
0090 {
0091 }
0092 
0093 Edid::Edid(const QByteArray &data, QObject *parent)
0094     : QObject(parent)
0095     , d(new Private())
0096 {
0097     d->parse(data);
0098 }
0099 
0100 Edid::Edid(Edid::Private *dd)
0101     : QObject()
0102     , d(dd)
0103 {
0104 }
0105 
0106 Edid::~Edid()
0107 {
0108     delete d;
0109 }
0110 
0111 Edid *Edid::clone() const
0112 {
0113     return new Edid(new Private(*d));
0114 }
0115 
0116 bool Edid::isValid() const
0117 {
0118     return d->valid;
0119 }
0120 
0121 QString Edid::deviceId(const QString &fallbackName) const
0122 {
0123     QString id = QStringLiteral("xrandr");
0124     // if no info was added check if the fallbacName is provided
0125     if (vendor().isNull() && name().isNull() && serial().isNull()) {
0126         if (!fallbackName.isEmpty()) {
0127             id.append(QLatin1Char('-') % fallbackName);
0128         } else {
0129             // all info we have are empty strings
0130             id.append(QLatin1String("-unknown"));
0131         }
0132     } else if (d->valid) {
0133         if (!vendor().isNull()) {
0134             id.append(QLatin1Char('-') % vendor());
0135         }
0136         if (!name().isNull()) {
0137             id.append(QLatin1Char('-') % name());
0138         }
0139         if (!serial().isNull()) {
0140             id.append(QLatin1Char('-') % serial());
0141         }
0142     }
0143 
0144     return id;
0145 }
0146 
0147 QString Edid::name() const
0148 {
0149     if (d->valid) {
0150         return d->monitorName;
0151     }
0152     return QString();
0153 }
0154 
0155 QString Edid::vendor() const
0156 {
0157     if (d->valid) {
0158         return d->vendorName;
0159     }
0160     return QString();
0161 }
0162 
0163 QString Edid::serial() const
0164 {
0165     if (d->valid) {
0166         return d->serialNumber;
0167     }
0168     return QString();
0169 }
0170 
0171 QString Edid::eisaId() const
0172 {
0173     if (d->valid) {
0174         return d->eisaId;
0175     }
0176     return QString();
0177 }
0178 
0179 QString Edid::hash() const
0180 {
0181     if (d->valid) {
0182         return d->checksum;
0183     }
0184     return QString();
0185 }
0186 
0187 QString Edid::pnpId() const
0188 {
0189     if (d->valid) {
0190         return d->pnpId;
0191     }
0192     return QString();
0193 }
0194 
0195 uint Edid::width() const
0196 {
0197     return d->width;
0198 }
0199 
0200 uint Edid::height() const
0201 {
0202     return d->height;
0203 }
0204 
0205 qreal Edid::gamma() const
0206 {
0207     return d->gamma;
0208 }
0209 
0210 QQuaternion Edid::red() const
0211 {
0212     return d->red;
0213 }
0214 
0215 QQuaternion Edid::green() const
0216 {
0217     return d->green;
0218 }
0219 
0220 QQuaternion Edid::blue() const
0221 {
0222     return d->blue;
0223 }
0224 
0225 QQuaternion Edid::white() const
0226 {
0227     return d->white;
0228 }
0229 
0230 bool Edid::Private::parse(const QByteArray &rawData)
0231 {
0232     quint32 serial;
0233     const quint8 *data = reinterpret_cast<const quint8 *>(rawData.constData());
0234     int length = rawData.length();
0235 
0236     /* check header */
0237     if (length < 128) {
0238         if (length > 0) {
0239             qCWarning(KSCREEN_EDID) << "Invalid EDID length (" << length << " bytes)";
0240         }
0241         valid = false;
0242         return valid;
0243     }
0244     if (data[0] != 0x00 || data[1] != 0xff) {
0245         qCWarning(KSCREEN_EDID) << "Failed to parse EDID header";
0246         valid = false;
0247         return valid;
0248     }
0249 
0250     /* decode the PNP ID from three 5 bit words packed into 2 bytes
0251      * /--08--\/--09--\
0252      * 7654321076543210
0253      * |\---/\---/\---/
0254      * R  C1   C2   C3 */
0255     pnpId.resize(3);
0256     pnpId[0] = QLatin1Char('A' + ((data[GCM_EDID_OFFSET_PNPID + 0] & 0x7c) / 4) - 1);
0257     pnpId[1] = QLatin1Char('A' + ((data[GCM_EDID_OFFSET_PNPID + 0] & 0x3) * 8) + ((data[GCM_EDID_OFFSET_PNPID + 1] & 0xe0) / 32) - 1);
0258     pnpId[2] = QLatin1Char('A' + (data[GCM_EDID_OFFSET_PNPID + 1] & 0x1f) - 1);
0259 
0260     // load the PNP_IDS file and load the vendor name
0261     QFile pnpIds(QStringLiteral(PNP_IDS));
0262     if (pnpIds.open(QIODevice::ReadOnly)) {
0263         while (!pnpIds.atEnd()) {
0264             QString line = QString::fromUtf8(pnpIds.readLine());
0265             if (line.startsWith(pnpId)) {
0266                 const QStringList parts = line.split(QLatin1Char('\t'));
0267                 if (parts.size() == 2) {
0268                     vendorName = parts.at(1).simplified();
0269                 }
0270                 break;
0271             }
0272         }
0273     }
0274 
0275     /* maybe there isn't a ASCII serial number descriptor, so use this instead */
0276     serial = static_cast<quint32>(data[GCM_EDID_OFFSET_SERIAL + 0]);
0277     serial += static_cast<quint32>(data[GCM_EDID_OFFSET_SERIAL + 1] * 0x100);
0278     serial += static_cast<quint32>(data[GCM_EDID_OFFSET_SERIAL + 2] * 0x10000);
0279     serial += static_cast<quint32>(data[GCM_EDID_OFFSET_SERIAL + 3] * 0x1000000);
0280     if (serial > 0) {
0281         serialNumber = QString::number(serial);
0282     }
0283 
0284     /* get the size */
0285     width = data[GCM_EDID_OFFSET_SIZE + 0];
0286     height = data[GCM_EDID_OFFSET_SIZE + 1];
0287 
0288     /* we don't care about aspect */
0289     if (width == 0 || height == 0) {
0290         width = 0;
0291         height = 0;
0292     }
0293 
0294     /* get gamma */
0295     if (data[GCM_EDID_OFFSET_GAMMA] == 0xff) {
0296         gamma = 1.0;
0297     } else {
0298         gamma = data[GCM_EDID_OFFSET_GAMMA] / 100.0 + 1.0;
0299     }
0300 
0301     /* get color red */
0302     red.setX(edidDecodeFraction(data[0x1b], edidGetBits(data[0x19], 6, 7)));
0303     red.setY(edidDecodeFraction(data[0x1c], edidGetBits(data[0x19], 5, 4)));
0304 
0305     /* get color green */
0306     green.setX(edidDecodeFraction(data[0x1d], edidGetBits(data[0x19], 2, 3)));
0307     green.setY(edidDecodeFraction(data[0x1e], edidGetBits(data[0x19], 0, 1)));
0308 
0309     /* get color blue */
0310     blue.setX(edidDecodeFraction(data[0x1f], edidGetBits(data[0x1a], 6, 7)));
0311     blue.setY(edidDecodeFraction(data[0x20], edidGetBits(data[0x1a], 4, 5)));
0312 
0313     /* get color white */
0314     white.setX(edidDecodeFraction(data[0x21], edidGetBits(data[0x1a], 2, 3)));
0315     white.setY(edidDecodeFraction(data[0x22], edidGetBits(data[0x1a], 0, 1)));
0316 
0317     /* parse EDID data */
0318     for (uint i = GCM_EDID_OFFSET_DATA_BLOCKS; i <= GCM_EDID_OFFSET_LAST_BLOCK; i += 18) {
0319         /* ignore pixel clock data */
0320         if (data[i] != 0) {
0321             continue;
0322         }
0323         if (data[i + 2] != 0) {
0324             continue;
0325         }
0326 
0327         /* any useful blocks? */
0328         if (data[i + 3] == GCM_DESCRIPTOR_DISPLAY_PRODUCT_NAME) {
0329             QString tmp = edidParseString(&data[i + 5]);
0330             if (!tmp.isEmpty()) {
0331                 monitorName = tmp;
0332             }
0333         } else if (data[i + 3] == GCM_DESCRIPTOR_DISPLAY_PRODUCT_SERIAL_NUMBER) {
0334             QString tmp = edidParseString(&data[i + 5]);
0335             if (!tmp.isEmpty()) {
0336                 serialNumber = tmp;
0337             }
0338         } else if (data[i + 3] == GCM_DESCRIPTOR_COLOR_MANAGEMENT_DATA) {
0339             qCWarning(KSCREEN_EDID) << "failing to parse color management data";
0340         } else if (data[i + 3] == GCM_DESCRIPTOR_ALPHANUMERIC_DATA_STRING) {
0341             QString tmp = edidParseString(&data[i + 5]);
0342             if (!tmp.isEmpty()) {
0343                 eisaId = tmp;
0344             }
0345         } else if (data[i + 3] == GCM_DESCRIPTOR_COLOR_POINT) {
0346             if (data[i + 3 + 9] != 0xff) {
0347                 /* extended EDID block(1) which contains
0348                  * a better gamma value */
0349                 gamma = (data[i + 3 + 9] / 100.0) + 1;
0350             }
0351             if (data[i + 3 + 14] != 0xff) {
0352                 /* extended EDID block(2) which contains
0353                  * a better gamma value */
0354                 gamma = (data[i + 3 + 9] / 100.0) + 1;
0355             }
0356         }
0357     }
0358 
0359     // calculate checksum
0360     QCryptographicHash hash(QCryptographicHash::Md5);
0361     hash.addData(reinterpret_cast<const char *>(data), length);
0362     checksum = QString::fromLatin1(hash.result().toHex());
0363 
0364     valid = true;
0365     return valid;
0366 }
0367 
0368 int Edid::Private::edidGetBit(int in, int bit) const
0369 {
0370     return (in & (1 << bit)) >> bit;
0371 }
0372 
0373 int Edid::Private::edidGetBits(int in, int begin, int end) const
0374 {
0375     int mask = (1 << (end - begin + 1)) - 1;
0376 
0377     return (in >> begin) & mask;
0378 }
0379 
0380 float Edid::Private::edidDecodeFraction(int high, int low) const
0381 {
0382     float result = 0.0;
0383 
0384     high = (high << 2) | low;
0385     for (int i = 0; i < 10; ++i) {
0386         result += edidGetBit(high, i) * pow(2, i - 10);
0387     }
0388     return result;
0389 }
0390 
0391 QString Edid::Private::edidParseString(const quint8 *data) const
0392 {
0393     /* this is always 13 bytes, but we can't guarantee it's null
0394      * terminated or not junk. */
0395     auto text = QString::fromLatin1(reinterpret_cast<const char *>(data), 13).simplified();
0396 
0397     for (int i = 0; i < text.length(); ++i) {
0398         if (!text.at(i).isPrint()) {
0399             text[i] = QLatin1Char('-');
0400         }
0401     }
0402     return text;
0403 }