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 | |