View Javadoc
1   /*
2    * Copyright 2016-2025 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.util;
6   
7   import java.nio.ByteBuffer;
8   import java.nio.ByteOrder;
9   import java.nio.charset.StandardCharsets;
10  import java.util.Arrays;
11  import java.util.Locale;
12  
13  import org.slf4j.Logger;
14  import org.slf4j.LoggerFactory;
15  
16  import oshi.annotation.SuppressForbidden;
17  import oshi.annotation.concurrent.ThreadSafe;
18  
19  /**
20   * EDID parsing utility.
21   */
22  @ThreadSafe
23  public final class EdidUtil {
24  
25      private static final Logger LOG = LoggerFactory.getLogger(EdidUtil.class);
26  
27      private EdidUtil() {
28      }
29  
30      /**
31       * Gets the Manufacturer ID from (up to) 3 5-bit characters in bytes 8 and 9
32       *
33       * @param edid The EDID byte array
34       * @return The manufacturer ID
35       */
36      @SuppressForbidden(reason = "customized base 2 parsing not in Util class")
37      public static String getManufacturerID(byte[] edid) {
38          // Bytes 8-9 are manufacturer ID in 3 5-bit characters.
39          String temp = String.format(Locale.ROOT, "%8s%8s", Integer.toBinaryString(edid[8] & 0xFF),
40                  Integer.toBinaryString(edid[9] & 0xFF)).replace(' ', '0');
41          LOG.debug("Manufacurer ID: {}", temp);
42          return String.format(Locale.ROOT, "%s%s%s", (char) (64 + Integer.parseInt(temp.substring(1, 6), 2)),
43                  (char) (64 + Integer.parseInt(temp.substring(6, 11), 2)),
44                  (char) (64 + Integer.parseInt(temp.substring(11, 16), 2))).replace("@", "");
45      }
46  
47      /**
48       * Gets the Product ID, bytes 10 and 11
49       *
50       * @param edid The EDID byte array
51       * @return The product ID
52       */
53      public static String getProductID(byte[] edid) {
54          // Bytes 10-11 are product ID expressed in hex characters
55          return Integer.toHexString(
56                  ByteBuffer.wrap(Arrays.copyOfRange(edid, 10, 12)).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xffff);
57      }
58  
59      /**
60       * Gets the Serial number, bytes 12-15
61       *
62       * @param edid The EDID byte array
63       * @return If all 4 bytes represent alphanumeric characters, a 4-character string, otherwise a hex string.
64       */
65      public static String getSerialNo(byte[] edid) {
66          // Bytes 12-15 are Serial number (last 4 characters)
67          if (LOG.isDebugEnabled()) {
68              LOG.debug("Serial number: {}", Arrays.toString(Arrays.copyOfRange(edid, 12, 16)));
69          }
70          return String.format(Locale.ROOT, "%s%s%s%s", getAlphaNumericOrHex(edid[15]), getAlphaNumericOrHex(edid[14]),
71                  getAlphaNumericOrHex(edid[13]), getAlphaNumericOrHex(edid[12]));
72      }
73  
74      private static String getAlphaNumericOrHex(byte b) {
75          return Character.isLetterOrDigit((char) b) ? String.format(Locale.ROOT, "%s", (char) b)
76                  : String.format(Locale.ROOT, "%02X", b);
77      }
78  
79      /**
80       * Return the week of year of manufacture
81       *
82       * @param edid The EDID byte array
83       * @return The week of year
84       */
85      public static byte getWeek(byte[] edid) {
86          // Byte 16 is manufacture week
87          return edid[16];
88      }
89  
90      /**
91       * Return the year of manufacture
92       *
93       * @param edid The EDID byte array
94       * @return The year of manufacture
95       */
96      public static int getYear(byte[] edid) {
97          // Byte 17 is manufacture year-1990
98          byte temp = edid[17];
99          LOG.debug("Year-1990: {}", temp);
100         return temp + 1990;
101     }
102 
103     /**
104      * Return the EDID version
105      *
106      * @param edid The EDID byte array
107      * @return The EDID version
108      */
109     public static String getVersion(byte[] edid) {
110         // Bytes 18-19 are EDID version
111         return edid[18] + "." + edid[19];
112     }
113 
114     /**
115      * Test if this EDID is a digital monitor based on byte 20
116      *
117      * @param edid The EDID byte array
118      * @return True if the EDID represents a digital monitor, false otherwise
119      */
120     public static boolean isDigital(byte[] edid) {
121         // Byte 20 is Video input params
122         return 1 == (edid[20] & 0xff) >> 7;
123     }
124 
125     /**
126      * Get monitor width in cm
127      *
128      * @param edid The EDID byte array
129      * @return Monitor width in cm
130      */
131     public static int getHcm(byte[] edid) {
132         // Byte 21 is horizontal size in cm
133         return edid[21];
134     }
135 
136     /**
137      * Get monitor height in cm
138      *
139      * @param edid The EDID byte array
140      * @return Monitor height in cm
141      */
142     public static int getVcm(byte[] edid) {
143         // Byte 22 is vertical size in cm
144         return edid[22];
145     }
146 
147     /**
148      * Get the VESA descriptors
149      *
150      * @param edid The EDID byte array
151      * @return A 2D array with four 18-byte elements representing VESA descriptors
152      */
153     public static byte[][] getDescriptors(byte[] edid) {
154         byte[][] desc = new byte[4][18];
155         for (int i = 0; i < desc.length; i++) {
156             System.arraycopy(edid, 54 + 18 * i, desc[i], 0, 18);
157         }
158         return desc;
159     }
160 
161     /**
162      * Get the VESA descriptor type
163      *
164      * @param desc An 18-byte VESA descriptor
165      * @return An integer representing the first four bytes of the VESA descriptor
166      */
167     public static int getDescriptorType(byte[] desc) {
168         return ByteBuffer.wrap(Arrays.copyOfRange(desc, 0, 4)).getInt();
169     }
170 
171     /**
172      * Parse a detailed timing descriptor
173      *
174      * @param desc An 18-byte VESA descriptor
175      * @return A string describing part of the detailed timing descriptor
176      */
177     public static String getTimingDescriptor(byte[] desc) {
178         int clock = ByteBuffer.wrap(Arrays.copyOfRange(desc, 0, 2)).order(ByteOrder.LITTLE_ENDIAN).getShort() / 100;
179         int hActive = (desc[2] & 0xff) + ((desc[4] & 0xf0) << 4);
180         int vActive = (desc[5] & 0xff) + ((desc[7] & 0xf0) << 4);
181         return String.format(Locale.ROOT, "Clock %dMHz, Active Pixels %dx%d ", clock, hActive, vActive);
182     }
183 
184     /**
185      * Parse descriptor range limits
186      *
187      * @param desc An 18-byte VESA descriptor
188      * @return A string describing some of the range limits
189      */
190     public static String getDescriptorRangeLimits(byte[] desc) {
191         return String.format(Locale.ROOT, "Field Rate %d-%d Hz vertical, %d-%d Hz horizontal, Max clock: %d MHz",
192                 desc[5], desc[6], desc[7], desc[8], desc[9] * 10);
193     }
194 
195     /**
196      * Parse descriptor text
197      *
198      * @param desc An 18-byte VESA descriptor
199      * @return Plain text starting at the 4th byte
200      */
201     public static String getDescriptorText(byte[] desc) {
202         return new String(Arrays.copyOfRange(desc, 4, 18), StandardCharsets.US_ASCII).trim();
203     }
204 
205     /**
206      * Get the preferred resolution for the monitor (Eg: 1920x1080)
207      *
208      * @param edid The edid Byte array
209      * @return Plain text preferred resolution
210      */
211 
212     public static String getPreferredResolution(byte[] edid) {
213         int dtd = 54;
214         int horizontalRes = (edid[dtd + 4] & 0xF0) << 4 | edid[dtd + 2] & 0xFF;
215         int verticalRes = (edid[dtd + 7] & 0xF0) << 4 | edid[dtd + 5] & 0xFF;
216         return horizontalRes + "x" + verticalRes;
217     }
218 
219     /**
220      * Get the monitor model from the EDID
221      *
222      * @param edid The edid Byte array
223      * @return Plain text monitor model
224      */
225 
226     public static String getModel(byte[] edid) {
227 
228         byte[][] desc = EdidUtil.getDescriptors(edid);
229         String model = null;
230 
231         for (byte[] b : desc) {
232 
233             if (EdidUtil.getDescriptorType(b) == 0xfc) {
234                 model = EdidUtil.getDescriptorText(b);
235                 break;
236             }
237         }
238 
239         assert model != null;
240         String[] tokens = model.split("\\s+");
241         if (tokens.length >= 1) {
242             model = tokens[tokens.length - 1];
243         }
244         return model.trim();
245     }
246 
247     /**
248      * Parse an EDID byte array into user-readable information
249      *
250      * @param edid An EDID byte array
251      * @return User-readable text represented by the EDID
252      */
253     public static String toString(byte[] edid) {
254         StringBuilder sb = new StringBuilder();
255         sb.append("  Manuf. ID=").append(EdidUtil.getManufacturerID(edid));
256         sb.append(", Product ID=").append(EdidUtil.getProductID(edid));
257         sb.append(", ").append(EdidUtil.isDigital(edid) ? "Digital" : "Analog");
258         sb.append(", Serial=").append(EdidUtil.getSerialNo(edid));
259         sb.append(", ManufDate=").append(EdidUtil.getWeek(edid) * 12 / 52 + 1).append('/')
260                 .append(EdidUtil.getYear(edid));
261         sb.append(", EDID v").append(EdidUtil.getVersion(edid));
262         int hSize = EdidUtil.getHcm(edid);
263         int vSize = EdidUtil.getVcm(edid);
264         sb.append(String.format(Locale.ROOT, "%n  %d x %d cm (%.1f x %.1f in)", hSize, vSize, hSize / 2.54,
265                 vSize / 2.54));
266         byte[][] desc = EdidUtil.getDescriptors(edid);
267         for (byte[] b : desc) {
268             switch (EdidUtil.getDescriptorType(b)) {
269             case 0xff:
270                 sb.append("\n  Serial Number: ").append(EdidUtil.getDescriptorText(b));
271                 break;
272             case 0xfe:
273                 sb.append("\n  Unspecified Text: ").append(EdidUtil.getDescriptorText(b));
274                 break;
275             case 0xfd:
276                 sb.append("\n  Range Limits: ").append(EdidUtil.getDescriptorRangeLimits(b));
277                 break;
278             case 0xfc:
279                 sb.append("\n  Monitor Name: ").append(EdidUtil.getDescriptorText(b));
280                 break;
281             case 0xfb:
282                 sb.append("\n  White Point Data: ").append(ParseUtil.byteArrayToHexString(b));
283                 break;
284             case 0xfa:
285                 sb.append("\n  Standard Timing ID: ").append(ParseUtil.byteArrayToHexString(b));
286                 break;
287             default:
288                 if (EdidUtil.getDescriptorType(b) <= 0x0f && EdidUtil.getDescriptorType(b) >= 0x00) {
289                     sb.append("\n  Manufacturer Data: ").append(ParseUtil.byteArrayToHexString(b));
290                 } else {
291                     sb.append("\n  Preferred Timing: ").append(EdidUtil.getTimingDescriptor(b));
292                 }
293                 break;
294             }
295         }
296         return sb.toString();
297     }
298 }