"""Bounds-checked EDID 1.x / CTA parser. Unknown fields retain their bytes.""" import hashlib import re VICS = {1:'640×480p 60 Hz',2:'720×480p 60 Hz 4:3',3:'720×480p 60 Hz 16:9', 4:'1280×720p 60 Hz',5:'1920×1080i 60 Hz',16:'1920×1080p 60 Hz', 17:'720×576p 50 Hz 4:3',18:'720×576p 50 Hz 16:9',19:'1280×720p 50 Hz', 20:'1920×1080i 50 Hz',31:'1920×1080p 50 Hz',32:'1920×1080p 24 Hz', 33:'1920×1080p 25 Hz',34:'1920×1080p 30 Hz',60:'1280×720p 24 Hz', 61:'1280×720p 25 Hz',62:'1280×720p 30 Hz',63:'1920×1080p 120 Hz',64:'1920×1080p 100 Hz', 93:'3840×2160p 24 Hz',94:'3840×2160p 25 Hz',95:'3840×2160p 30 Hz', 96:'3840×2160p 50 Hz',97:'3840×2160p 60 Hz',98:'4096×2160p 24 Hz', 99:'4096×2160p 25 Hz',100:'4096×2160p 30 Hz',101:'4096×2160p 50 Hz',102:'4096×2160p 60 Hz', 117:'3840×2160p 100 Hz',118:'3840×2160p 120 Hz'} EST = ['720×400 70 Hz','720×400 88 Hz','640×480 60 Hz','640×480 67 Hz','640×480 72 Hz', '640×480 75 Hz','800×600 56 Hz','800×600 60 Hz','800×600 72 Hz','800×600 75 Hz', '832×624 75 Hz','1024×768i 87 Hz','1024×768 60 Hz','1024×768 70 Hz','1024×768 75 Hz', '1280×1024 75 Hz','1152×870 75 Hz'] def read_file(path): from pathlib import Path data = Path(path).read_bytes() if len(data) > 32768 * 4: raise ValueError('文件过大:最大 128 KiB') if Path(path).suffix.lower() in ('.hex', '.txt'): s = data.decode('utf-8-sig').strip() s = re.sub(r'0[xX]([0-9a-fA-F]{2})', r'\1', s) s = re.sub(r'[\s,{};]', '', s) if not re.fullmatch(r'(?:[0-9a-fA-F]{2})+', s): raise ValueError('HEX 文件仅接受十六进制字节,不接受偏移地址或 ASCII 栏') return bytes.fromhex(s) return data def hex_dump(raw): return '\n'.join(f'{i:04X} ' + ' '.join(f'{x:02X}' for x in raw[i:i+16]) + ' |' + ''.join(chr(x) if 32 <= x < 127 else '.' for x in raw[i:i+16]) + '|' for i in range(0, len(raw), 16)) def dtd(b, source): clock = int.from_bytes(b[:2], 'little') * 10000 h = b[2] + ((b[4] & 240) << 4); hb = b[3] + ((b[4] & 15) << 8) v = b[5] + ((b[7] & 240) << 4); vb = b[6] + ((b[7] & 15) << 8) hf = b[8] + ((b[11] & 192) << 2); hs = b[9] + ((b[11] & 48) << 4) vf = (b[10] >> 4) + ((b[11] & 12) << 2); vs = (b[10] & 15) + ((b[11] & 3) << 4) interlace = bool(b[17] & 128) total_v = (v + vb) * 2 + 1 if interlace else v + vb rate = clock / ((h + hb) * total_v) if h + hb and total_v else 0 sync = (b[17] >> 3) & 3 polarity = ('H' + ('+' if b[17] & 2 else '-') + ' V' + ('+' if b[17] & 4 else '-')) if sync == 3 else '非数字分离同步' return {'来源':source, '分辨率':f'{h}×{v*2 if interlace else v}' + ('i' if interlace else 'p'), '刷新率':f'{rate*2 if interlace else rate:.3f} Hz' + ('(场率)' if interlace else ''), '像素时钟 MHz':clock / 1e6, '水平有效/前肩/同步/后肩':[h,hf,hs,hb-hf-hs], '垂直有效/前肩/同步/后肩(隔行按场)':[v,vf,vs,vb-vf-vs], '同步':polarity} def parse(raw): if len(raw) < 128: raise ValueError(f'EDID 基础块不足 128 字节,实际 {len(raw)}') r = {'基本信息':{}, '时序':[], '能力':[], '数据块':[], '校验':[], '警告':[]} info, warnings = r['基本信息'], r['警告'] if raw[:8] != bytes.fromhex('00 ff ff ff ff ff ff 00'): warnings.append('EDID 文件头无效;以下解析仅供诊断') if raw[18] != 1: warnings.append('非 EDID 1.x:未解析,仅保留原始数据') return r if len(raw) % 128: warnings.append('末尾存在不足 128 字节的数据') expected = (raw[126] + 1) * 128 if expected != len(raw): warnings.append(f'声明 {expected} 字节,实际 {len(raw)} 字节') for n in range(len(raw)//128): block = raw[n*128:(n+1)*128] r['校验'].append(f'块 {n}: ' + ('通过' if sum(block)%256 == 0 else '失败')) pnp = int.from_bytes(raw[8:10], 'big') manufacturer = ''.join(chr(((pnp >> s) & 31) + 64) for s in (10,5,0)) info.update({'厂家 PNP 代码':manufacturer, '产品编号':f'0x{int.from_bytes(raw[10:12], "little"):04X}', '数字序列号':int.from_bytes(raw[12:16], 'little'), 'EDID 版本':f'{raw[18]}.{raw[19]}', '生产日期':f'{raw[17]+1990} 年 / '+('型号年份' if raw[16]==255 else f'第 {raw[16]} 周' if raw[16] else '周未指定'), '尺寸':f'{raw[21]} × {raw[22]} cm' if raw[21] and raw[22] else '未给出完整物理尺寸(可能编码宽高比)', 'Gamma':(raw[23]+100)/100 if raw[23]!=255 else '未定义', '扩展块数':raw[126], 'SHA-256':hashlib.sha256(raw).hexdigest()}) if raw[20]&128: info['输入类型'] = '数字' if raw[19]>=4: info['接口声明'] = {0:'未定义',1:'DVI',2:'HDMI-A',3:'HDMI-B',4:'MDDI',5:'DisplayPort'}.get(raw[20]&15,'保留') info['色深 bpc'] = {0:'未定义',1:6,2:8,3:10,4:12,5:14,6:16}.get((raw[20]>>4)&7,'保留') else: info['输入类型'] = '模拟' for idx, mode in enumerate(EST): if raw[35+idx//8] & (128>>(idx%8)): r['时序'].append({'来源':'已建立时序','分辨率':mode}) for i in range(38,54,2): a,b = raw[i:i+2] if (a,b) in ((1,1),(0,0)): continue h = (a+31)*8 ratio = (1 if raw[19]<3 else 10/16, 3/4, 4/5, 9/16)[b>>6] r['时序'].append({'来源':'标准时序','分辨率':f'{h}×{round(h*ratio)}','刷新率':f'{(b&63)+60} Hz'}) for offset in range(54,126,18): b=raw[offset:offset+18] if any(b[:2]): r['时序'].append(dtd(b, '基础 DTD'+('(首选)' if offset==54 and (raw[19]>=4 or raw[24]&2) else ''))) elif b[:3]==b'\0\0\0': if b[3] in (252,255,254): info[{252:'显示器名称',255:'文本序列号',254:'描述文本'}[b[3]]] = b[5:18].decode('ascii','replace').strip('\x00\n ') elif b[3]==253: info['频率范围原始值'] = f'垂直 {b[5]}–{b[6]} Hz;水平 {b[7]}–{b[8]} kHz;最大像素时钟 {b[9]*10} MHz' if b[4]: warnings.append('范围描述符有扩展偏移,频率范围显示原始字段,暂未应用偏移') coords=[] low=[(raw[25]>>s)&3 for s in (6,4,2,0)]+[(raw[26]>>s)&3 for s in (6,4,2,0)] for i,name in enumerate(('Rx','Ry','Gx','Gy','Bx','By','Wx','Wy')): coords.append(f'{name}={(raw[27+i]*4+low[i])/1024:.4f}') r['能力'].append('色度坐标:'+', '.join(coords)) for n in range(1,min(len(raw)//128,raw[126]+1)): b=raw[n*128:(n+1)*128] r['数据块'].append({'块':n,'类型':{2:'CTA-861',112:'DisplayID(暂未解码)'}.get(b[0],f'未知 0x{b[0]:02X}'),'原始':b.hex(' ')}) if b[0]!=2: continue r['能力'].append(f'CTA 块 {n} / revision {b[1]}:'+', '.join(label for bit,label in ((64,'基本音频'),(32,'YCbCr 4:4:4'),(16,'YCbCr 4:2:2')) if b[3]&bit)) end=b[2] if end==0: continue if not 4<=end<=127: warnings.append(f'CTA 块 {n} DTD 偏移无效:{end}'); continue p=4 while p>5,b[p]&31 if not b[p]: break if p+1+length>end: warnings.append(f'CTA 块 {n} 数据块越界 @{p}'); break x=b[p+1:p+1+length]; p+=length+1 entry={'块':n,'tag':tag,'原始':x.hex(' ')}; r['数据块'].append(entry) if tag==2: for svd in x: vic=svd&127 if 129<=svd<=192 else svd r['时序'].append({'来源':f'CTA VIC {vic}'+('(原生)' if 129<=svd<=192 else ''),'分辨率':VICS.get(vic,'尚未映射;查看 VIC 编号')}) elif tag==1: if len(x)%3: warnings.append('音频数据块长度不是 3 的倍数') for j in range(0,len(x)-2,3): a,f,c=x[j:j+3]; fmt=(a>>3)&15 rates='/'.join(str(v) for k,v in enumerate((32,44.1,48,88.2,96,176.4,192)) if f&(1<=3: oui=int.from_bytes(x[:3],'little'); entry['OUI']=f'{oui:06X}' if oui==0x000c03: r['能力'].append('HDMI VSDB') if len(x)>=5: r['能力'].append('CEC 物理地址:'+'.'.join(str(v) for v in (x[3]>>4,x[3]&15,x[4]>>4,x[4]&15))) if len(x)>=6: r['能力'].append('HDMI 深色:'+('/'.join(str(v) for k,v in ((4,10),(5,12),(6,16)) if x[5]&(1<=7 and x[6]: r['能力'].append(f'最大 TMDS 时钟:{x[6]*5} MHz') elif oui==0xc45dd8: r['能力'].append('HDMI Forum VSDB(VRR/FRL/DSC 字段暂未解码)') if len(x)>=5 and x[4]: r['能力'].append(f'最大 TMDS 字符速率:{x[4]*5} MHz') if len(x)>=6: r['能力'].append('SCDC:'+('支持' if x[5]&128 else '未声明')) elif tag==7 and x: entry['扩展 tag']=x[0] if x[0]==6 and len(x)>=3: r['能力'].append('HDR EOTF:'+', '.join(name for k,name in enumerate(('传统 SDR','传统 HDR','PQ / ST 2084','HLG')) if x[1]&(1<=3: r['能力'].append('色彩标准:'+', '.join(name for k,name in enumerate(('xvYCC601','xvYCC709','sYCC601','opYCC601','opRGB','BT.2020 cYCC','BT.2020 YCC','BT.2020 RGB')) if x[1]&(1<