| 1 | // Copyright (C) 2017 Pier Luigi Fiorini <pierluigi.fiorini@gmail.com> |
| 2 | // Copyright (C) 2021 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Giuseppe D'Angelo <giuseppe.dangelo@kdab.com> |
| 3 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 4 | |
| 5 | #include <QtCore/QFile> |
| 6 | #include <QtCore/QByteArrayView> |
| 7 | |
| 8 | #include "qedidparser_p.h" |
| 9 | #include "qedidvendortable_p.h" |
| 10 | |
| 11 | #define EDID_DESCRIPTOR_ALPHANUMERIC_STRING 0xfe |
| 12 | #define EDID_DESCRIPTOR_PRODUCT_NAME 0xfc |
| 13 | #define EDID_DESCRIPTOR_SERIAL_NUMBER 0xff |
| 14 | |
| 15 | #define EDID_DATA_BLOCK_COUNT 4 |
| 16 | #define EDID_OFFSET_DATA_BLOCKS 0x36 |
| 17 | #define EDID_OFFSET_LAST_BLOCK 0x6c |
| 18 | #define EDID_OFFSET_PNP_ID 0x08 |
| 19 | #define EDID_OFFSET_SERIAL 0x0c |
| 20 | #define EDID_PHYSICAL_WIDTH 0x15 |
| 21 | #define EDID_OFFSET_PHYSICAL_HEIGHT 0x16 |
| 22 | #define EDID_TRANSFER_FUNCTION 0x17 |
| 23 | #define EDID_FEATURE_SUPPORT 0x18 |
| 24 | #define EDID_CHROMATICITIES_BLOCK 0x19 |
| 25 | |
| 26 | QT_BEGIN_NAMESPACE |
| 27 | |
| 28 | using namespace Qt::StringLiterals; |
| 29 | |
| 30 | static QString lookupVendorIdInSystemDatabase(QByteArrayView id) |
| 31 | { |
| 32 | QString result; |
| 33 | |
| 34 | const QString fileName = "/usr/share/hwdata/pnp.ids"_L1 ; |
| 35 | QFile file(fileName); |
| 36 | if (!file.open(flags: QFile::ReadOnly)) |
| 37 | return result; |
| 38 | |
| 39 | // On Ubuntu 20.04 the longest line in the file is 85 bytes, so this |
| 40 | // leaves plenty of room... |
| 41 | constexpr int MaxLineSize = 512; |
| 42 | char buf[MaxLineSize]; |
| 43 | |
| 44 | while (!file.atEnd()) { |
| 45 | auto read = file.readLine(data: buf, maxlen: MaxLineSize); |
| 46 | if (read < 0 || read == MaxLineSize) // read error |
| 47 | break; |
| 48 | |
| 49 | QByteArrayView line(buf, read - 1); // -1 to remove the trailing newline |
| 50 | if (line.isEmpty()) |
| 51 | continue; |
| 52 | |
| 53 | if (line.startsWith(c: '#')) |
| 54 | continue; |
| 55 | |
| 56 | auto tabPosition = line.indexOf(ch: '\t'); |
| 57 | if (tabPosition <= 0) // no vendor id |
| 58 | continue; |
| 59 | if (tabPosition + 1 == line.size()) // no vendor name |
| 60 | continue; |
| 61 | |
| 62 | if (line.first(n: tabPosition) == id) { |
| 63 | auto vendor = line.sliced(pos: tabPosition + 1); |
| 64 | result = QString::fromUtf8(utf8: vendor.data(), size: vendor.size()); |
| 65 | break; |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | return result; |
| 70 | } |
| 71 | |
| 72 | bool QEdidParser::parse(const QByteArray &blob) |
| 73 | { |
| 74 | const quint8 *data = reinterpret_cast<const quint8 *>(blob.constData()); |
| 75 | const size_t length = blob.size(); |
| 76 | |
| 77 | // Verify header |
| 78 | if (length < 128) |
| 79 | return false; |
| 80 | if (data[0] != 0x00 || data[1] != 0xff) |
| 81 | return false; |
| 82 | |
| 83 | /* Decode the PNP ID from three 5 bit words packed into 2 bytes |
| 84 | * /--08--\/--09--\ |
| 85 | * 7654321076543210 |
| 86 | * |\---/\---/\---/ |
| 87 | * R C1 C2 C3 */ |
| 88 | char pnpId[3]; |
| 89 | pnpId[0] = 'A' + ((data[EDID_OFFSET_PNP_ID] & 0x7c) / 4) - 1; |
| 90 | pnpId[1] = 'A' + ((data[EDID_OFFSET_PNP_ID] & 0x3) * 8) + ((data[EDID_OFFSET_PNP_ID + 1] & 0xe0) / 32) - 1; |
| 91 | pnpId[2] = 'A' + (data[EDID_OFFSET_PNP_ID + 1] & 0x1f) - 1; |
| 92 | |
| 93 | // Clear manufacturer |
| 94 | manufacturer = QString(); |
| 95 | |
| 96 | // Serial number, will be overwritten by an ASCII descriptor |
| 97 | // when and if it will be found |
| 98 | quint32 serial = data[EDID_OFFSET_SERIAL] |
| 99 | + (data[EDID_OFFSET_SERIAL + 1] << 8) |
| 100 | + (data[EDID_OFFSET_SERIAL + 2] << 16) |
| 101 | + (data[EDID_OFFSET_SERIAL + 3] << 24); |
| 102 | if (serial > 0) |
| 103 | serialNumber = QString::number(serial); |
| 104 | else |
| 105 | serialNumber = QString(); |
| 106 | |
| 107 | // Parse EDID data |
| 108 | for (int i = 0; i < EDID_DATA_BLOCK_COUNT; ++i) { |
| 109 | const uint offset = EDID_OFFSET_DATA_BLOCKS + i * 18; |
| 110 | |
| 111 | if (data[offset] != 0 || data[offset + 1] != 0 || data[offset + 2] != 0) |
| 112 | continue; |
| 113 | |
| 114 | if (data[offset + 3] == EDID_DESCRIPTOR_PRODUCT_NAME) |
| 115 | model = parseEdidString(data: &data[offset + 5]); |
| 116 | else if (data[offset + 3] == EDID_DESCRIPTOR_ALPHANUMERIC_STRING) |
| 117 | identifier = parseEdidString(data: &data[offset + 5]); |
| 118 | else if (data[offset + 3] == EDID_DESCRIPTOR_SERIAL_NUMBER) |
| 119 | serialNumber = parseEdidString(data: &data[offset + 5]); |
| 120 | } |
| 121 | |
| 122 | // Try to use cache first because it is potentially more updated |
| 123 | manufacturer = lookupVendorIdInSystemDatabase(id: pnpId); |
| 124 | |
| 125 | if (manufacturer.isEmpty()) { |
| 126 | // Find the manufacturer from the vendor lookup table |
| 127 | const auto compareVendorId = [](const VendorTable &vendor, const char *str) |
| 128 | { |
| 129 | return strncmp(s1: vendor.id, s2: str, n: 3) < 0; |
| 130 | }; |
| 131 | |
| 132 | const auto b = std::begin(arr: q_edidVendorTable); |
| 133 | const auto e = std::end(arr: q_edidVendorTable); |
| 134 | auto it = std::lower_bound(first: b, |
| 135 | last: e, |
| 136 | val: pnpId, |
| 137 | comp: compareVendorId); |
| 138 | |
| 139 | if (it != e && strncmp(s1: it->id, s2: pnpId, n: 3) == 0) |
| 140 | manufacturer = QString::fromUtf8(utf8: it->name); |
| 141 | } |
| 142 | |
| 143 | // If we don't know the manufacturer, fallback to PNP ID |
| 144 | if (manufacturer.isEmpty()) |
| 145 | manufacturer = QString::fromUtf8(utf8: pnpId, size: std::size(pnpId)); |
| 146 | |
| 147 | // Physical size |
| 148 | physicalSize = QSizeF(data[EDID_PHYSICAL_WIDTH], data[EDID_OFFSET_PHYSICAL_HEIGHT]) * 10; |
| 149 | |
| 150 | // Gamma and transfer function |
| 151 | const uint igamma = data[EDID_TRANSFER_FUNCTION]; |
| 152 | if (igamma != 0xff) { |
| 153 | gamma = 1.0 + (igamma / 100.0f); |
| 154 | useTables = false; |
| 155 | } else { |
| 156 | gamma = 0.0; // Defined in DI-EXT |
| 157 | useTables = true; |
| 158 | } |
| 159 | sRgb = data[EDID_FEATURE_SUPPORT] & 0x04; |
| 160 | |
| 161 | // Chromaticities |
| 162 | int rx = (data[EDID_CHROMATICITIES_BLOCK] >> 6) & 0x03; |
| 163 | int ry = (data[EDID_CHROMATICITIES_BLOCK] >> 4) & 0x03; |
| 164 | int gx = (data[EDID_CHROMATICITIES_BLOCK] >> 2) & 0x03; |
| 165 | int gy = (data[EDID_CHROMATICITIES_BLOCK] >> 0) & 0x03; |
| 166 | int bx = (data[EDID_CHROMATICITIES_BLOCK + 1] >> 6) & 0x03; |
| 167 | int by = (data[EDID_CHROMATICITIES_BLOCK + 1] >> 4) & 0x03; |
| 168 | int wx = (data[EDID_CHROMATICITIES_BLOCK + 1] >> 2) & 0x03; |
| 169 | int wy = (data[EDID_CHROMATICITIES_BLOCK + 1] >> 0) & 0x03; |
| 170 | rx |= data[EDID_CHROMATICITIES_BLOCK + 2] << 2; |
| 171 | ry |= data[EDID_CHROMATICITIES_BLOCK + 3] << 2; |
| 172 | gx |= data[EDID_CHROMATICITIES_BLOCK + 4] << 2; |
| 173 | gy |= data[EDID_CHROMATICITIES_BLOCK + 5] << 2; |
| 174 | bx |= data[EDID_CHROMATICITIES_BLOCK + 6] << 2; |
| 175 | by |= data[EDID_CHROMATICITIES_BLOCK + 7] << 2; |
| 176 | wx |= data[EDID_CHROMATICITIES_BLOCK + 8] << 2; |
| 177 | wy |= data[EDID_CHROMATICITIES_BLOCK + 9] << 2; |
| 178 | |
| 179 | redChromaticity.setX(rx * (1.0f / 1024.0f)); |
| 180 | redChromaticity.setY(ry * (1.0f / 1024.0f)); |
| 181 | greenChromaticity.setX(gx * (1.0f / 1024.0f)); |
| 182 | greenChromaticity.setY(gy * (1.0f / 1024.0f)); |
| 183 | blueChromaticity.setX(bx * (1.0f / 1024.0f)); |
| 184 | blueChromaticity.setY(by * (1.0f / 1024.0f)); |
| 185 | whiteChromaticity.setX(wx * (1.0f / 1024.0f)); |
| 186 | whiteChromaticity.setY(wy * (1.0f / 1024.0f)); |
| 187 | |
| 188 | // Find extensions |
| 189 | for (uint i = 1; i < length / 128; ++i) { |
| 190 | uint extensionId = data[i * 128]; |
| 191 | if (extensionId == 0x40) { // DI-EXT |
| 192 | // 0x0E (sub-pixel layout) |
| 193 | // 0x20->0x22 (bits per color) |
| 194 | // 0x51->0x7e Transfer characteristics |
| 195 | const uchar desc = data[i * 128 + 0x51]; |
| 196 | const uchar len = desc & 0x3f; |
| 197 | if ((desc & 0xc0) == 0x40) { |
| 198 | if (len > 45) |
| 199 | return false; |
| 200 | QList<uint16_t> whiteTRC; |
| 201 | whiteTRC.reserve(asize: len + 1); |
| 202 | for (uint j = 0; j < len; ++j) |
| 203 | whiteTRC[j] = data[0x52 + j] * 0x101; |
| 204 | whiteTRC[len] = 0xffff; |
| 205 | tables.append(t: whiteTRC); |
| 206 | } else if ((desc & 0xc0) == 0x80) { |
| 207 | if (len > 15) |
| 208 | return false; |
| 209 | QList<uint16_t> redTRC; |
| 210 | QList<uint16_t> greenTRC; |
| 211 | QList<uint16_t> blueTRC; |
| 212 | blueTRC.reserve(asize: len + 1); |
| 213 | greenTRC.reserve(asize: len + 1); |
| 214 | redTRC.reserve(asize: len + 1); |
| 215 | for (uint j = 0; j < len; ++j) |
| 216 | blueTRC[j] = data[0x52 + j] * 0x101; |
| 217 | blueTRC[len] = 0xffff; |
| 218 | for (uint j = 0; j < len; ++j) |
| 219 | greenTRC[j] = data[0x61 + j] * 0x101; |
| 220 | greenTRC[len] = 0xffff; |
| 221 | for (uint j = 0; j < len; ++j) |
| 222 | redTRC[j] = data[0x70 + j] * 0x101; |
| 223 | redTRC[len] = 0xffff; |
| 224 | tables.append(t: redTRC); |
| 225 | tables.append(t: greenTRC); |
| 226 | tables.append(t: blueTRC); |
| 227 | } |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | return true; |
| 232 | } |
| 233 | |
| 234 | QString QEdidParser::parseEdidString(const quint8 *data) |
| 235 | { |
| 236 | QByteArray buffer(reinterpret_cast<const char *>(data), 13); |
| 237 | |
| 238 | for (int i = 0; i < buffer.size(); ++i) { |
| 239 | // If there are less than 13 characters in the string, the string |
| 240 | // is terminated with the ASCII code ‘0Ah’ (line feed) and padded |
| 241 | // with ASCII code ‘20h’ (space). See EDID 1.4, sections 3.10.3.1, |
| 242 | // 3.10.3.2, and 3.10.3.4. |
| 243 | if (buffer[i] == '\n') { |
| 244 | buffer.truncate(pos: i); |
| 245 | break; |
| 246 | } |
| 247 | |
| 248 | // Replace non-printable characters with dash |
| 249 | if (buffer[i] < '\040' || buffer[i] > '\176') |
| 250 | buffer[i] = '-'; |
| 251 | } |
| 252 | |
| 253 | return QString::fromLatin1(ba: buffer); |
| 254 | } |
| 255 | |
| 256 | QT_END_NAMESPACE |
| 257 | |