feat: HDMI-Tool 与 I2C-Inject 首版工具集
@@ -0,0 +1,76 @@
|
||||
# HDMI-Tool — HDMI 源端测试工具(LT8619C/DLPC3421 转接板调试)
|
||||
|
||||
> 更新:2026-09-03。单文件 Python GUI(`hdmi_gui.py`),另附命令行分辨率/图案工具
|
||||
> `hdmi_source.bat`。已打包版直接双击 `HDMI-Tool.exe`,无需安装 Python。
|
||||
|
||||
---
|
||||
|
||||
## 1. 功能总览
|
||||
|
||||
| 功能 | 说明 |
|
||||
|---|---|
|
||||
| EDID 查看 | 枚举各显示输出口,从注册表读取 EDID 原始数据,按 EDID 1.4/CEA-861 解算:厂商/型号/尺寸/支持模式(DTD/已建立/标准/CEA-VIC)/首选时序完整参数/校验和 |
|
||||
| 分辨率切换 | 第二行"分辨率"下拉列出显卡按该屏 EDID 提供的全部模式;**应用**=临时切换(不写注册表,重启还原),**恢复**=回到本次会话切改前的模式 |
|
||||
| 测试图案 | 全屏投到所选显示器,测试卡区域可选 `640x360 / 800x600 / full 铺满`;内置 纯红/纯绿/纯蓝/棋盘格/网格线 + R/G/B 三行纯色值按钮(55/AA/FF,与 dist 下 solid_*.png 对应)+ 3 个自定义图片栏位 |
|
||||
| 命令行模式 | 自动化测试脚本可直接调用(见 §4) |
|
||||
|
||||
## 2. 快速上手(GUI)
|
||||
|
||||
1. 双击 `HDMI-Tool.exe`(或 `python hdmi_gui.py`,需 Python ≥3.10 + pyserial 不需要,纯标准库;自定义图片 JPG/BMP 需 Pillow)
|
||||
2. 顶部显示器下拉选择目标屏(多屏时注意别选成主屏),"刷新"重新枚举
|
||||
3. **切分辨率**:第二行选模式 → 点"应用";测完点"恢复"
|
||||
4. **投测试图**:
|
||||
- 先在图案按钮行右侧选**测试卡区域**(640x360 / 800x600 / full)
|
||||
- 点图案按钮即全屏投放;按 **ESC** 退出
|
||||
- R/G/B 三行纯色按钮(55/AA/FF)用于位交换类测试:55 与 AA 互为字节镜像对,FF 为全高基准
|
||||
- 自定义图片:点"⚙"配置名称与图片文件(图片会复制到本目录),点栏位名投放
|
||||
5. 区域与自定义栏位配置自动保存在本目录 `pattern_slots.json`
|
||||
|
||||
## 3. 典型场景(LT8619C demo 板测试)
|
||||
|
||||
```
|
||||
1. HDMI 接 demo 板 → 启动 HDMI-Tool → 刷新 → 选中 demo 板显示器
|
||||
2. 分辨率切 800x600@60(demo 板 shadow EDID 首选)→ 测试卡区域选 800x600
|
||||
3. 点 R/G/B 行的纯色按钮投图 → 配合逻辑分析仪/万用表测 RGB 输出
|
||||
4. 测完"恢复"分辨率
|
||||
```
|
||||
|
||||
## 4. 命令行模式
|
||||
|
||||
```
|
||||
python hdmi_gui.py --dump 控制台输出 EDID 解算(exe 版不支持)
|
||||
python hdmi_gui.py --test-pattern Blue --area 800x600 --seconds 5
|
||||
出图 5 秒自动退出
|
||||
python hdmi_gui.py --test-pattern Custom1 投自定义栏位 1
|
||||
--area {640x360,800x600,full} --monitor N --seconds S
|
||||
```
|
||||
|
||||
**hdmi_source.bat**(PowerShell 源端工具,功能与 GUI 互补):
|
||||
|
||||
```
|
||||
tools\hdmi_source.bat -List 枚举显示器与支持分辨率(验证 EDID)
|
||||
tools\hdmi_source.bat -Monitor 1 -Mode 800x600 切模式
|
||||
tools\hdmi_source.bat -Mode 800x600 -Pattern -Restore
|
||||
切模式→出图案→退出后自动恢复
|
||||
```
|
||||
|
||||
## 5. 目录内容
|
||||
|
||||
| 文件 | 用途 |
|
||||
|---|---|
|
||||
| `HDMI-Tool.exe` | 打包好的单文件版(--noconsole;`--dump` 请用源码方式跑) |
|
||||
| `hdmi_gui.py` | 主程序源码 |
|
||||
| `build_exe.py` | 打包脚本(`python build_exe.py`,自动装 PyInstaller;`--debug` 额外出带控制台版) |
|
||||
| `hdmi_source.ps1 / .bat` | 源端分辨率/图案命令行工具 |
|
||||
| `gen_bw_patterns.py` / `gen_solid_colors.py` | 测试图生成脚本(bw 黑白系列 640x360 / solid 纯色系列 800x600) |
|
||||
| `solid_*.png` | 9 张 800x600 纯色图(R/G/B 各 55/AA/FF,供自定义栏位或脚本使用) |
|
||||
| `bw_*.png` | 5 张 640x360 黑白测试图(西门子星/频率条纹/渐变棋盘/波带/综合卡) |
|
||||
| `logos/` | 图标源文件与 `make_ico.py`(改 logo 后重跑再打包) |
|
||||
| `pattern_slots.json` | 自定义栏位 + 测试卡区域配置(运行后自动生成/更新) |
|
||||
|
||||
## 6. 注意事项
|
||||
|
||||
- 测试卡区域与自定义栏位配置按 exe/脚本所在目录各自保存,两份安装互不影响
|
||||
- 重打包后 Windows 图标缓存可能仍显示旧图标:`ie4uinit -show` 或改名即可
|
||||
- 多屏环境切分辨率前先确认显示器序号(下拉框中名称与 `-List` 输出一致)
|
||||
- 修改源码后重新打包前,先结束残留的 HDMI-Tool 进程(build_exe.py 会自动处理)
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
把 hdmi_gui.py 打包成单文件 exe(工具名: HDMI-Tool)
|
||||
================================
|
||||
使用 PyInstaller --onefile --noconsole: Python 解释器 + 标准库 + tkinter
|
||||
全部打进一个 exe, 目标机器无需安装任何依赖。
|
||||
|
||||
用法:
|
||||
python build_exe.py 自动安装 PyInstaller(如缺)并打包
|
||||
python build_exe.py --debug 额外打一个带控制台的变体(--dump 可用)
|
||||
|
||||
产物: tools/dist/HDMI-Tool.exe(及 --debug 时的 HDMI-Tool-console.exe)
|
||||
首次运行 onefile exe 会先解压到临时目录, 启动比脚本慢一两秒, 属正常现象。
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
ICO = os.path.join(BASE, 'logos', 'logo_ph_orange.ico')
|
||||
|
||||
|
||||
def ensure_pyinstaller():
|
||||
try:
|
||||
import PyInstaller # noqa: F401
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
print('PyInstaller 未安装, 正在安装...')
|
||||
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'pyinstaller'])
|
||||
|
||||
|
||||
def remove_stale(exe_path):
|
||||
"""删除旧 exe; 若被占用(上次验证的进程还在), 先结束同名进程再重试"""
|
||||
if not os.path.exists(exe_path):
|
||||
return
|
||||
try:
|
||||
os.remove(exe_path)
|
||||
return
|
||||
except PermissionError:
|
||||
pass
|
||||
import time
|
||||
print(f'{exe_path} 被占用, 结束残留进程后重试...')
|
||||
subprocess.run(['taskkill', '/F', '/IM', os.path.basename(exe_path)],
|
||||
capture_output=True)
|
||||
time.sleep(1.0)
|
||||
os.remove(exe_path)
|
||||
|
||||
|
||||
def build(name, extra_args):
|
||||
dist = os.path.join(BASE, 'dist')
|
||||
work = os.path.join(BASE, 'build')
|
||||
remove_stale(os.path.join(dist, name + '.exe'))
|
||||
cmd = [sys.executable, '-m', 'PyInstaller',
|
||||
'--onefile', '--clean', '--name', name,
|
||||
'--distpath', dist, '--workpath', work, '--specpath', work,
|
||||
] + extra_args + [os.path.join(BASE, 'hdmi_gui.py')]
|
||||
if os.path.exists(ICO):
|
||||
# exe 文件图标 + 运行时窗口图标资源(从 _MEIPASS 读取)
|
||||
cmd += ['--icon', ICO, '--add-data', f'{ICO};.']
|
||||
else:
|
||||
print('警告: 未找到 logos/logo_ph_orange.ico(先运行 logos/make_ico.py), 本次打包不带图标')
|
||||
print('\n>>', ' '.join(cmd))
|
||||
subprocess.check_call(cmd)
|
||||
out = os.path.join(dist, name + '.exe')
|
||||
print(f'完成: {out} ({os.path.getsize(out) / 1024 / 1024:.1f} MB)')
|
||||
|
||||
|
||||
def main():
|
||||
build_console = '--debug' in sys.argv
|
||||
ensure_pyinstaller()
|
||||
# 常规版: 双击即用, 无黑框(GUI/图案/自定义栏位全部可用, --dump 除外)
|
||||
build('HDMI-Tool', ['--noconsole'])
|
||||
if build_console:
|
||||
# 调试版: 带控制台, --dump / 异常栈可见
|
||||
build('HDMI-Tool-console', [])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
生成 640x360 黑白测试图, 供 HDMI 源端工具的自定义栏位使用
|
||||
================================================================
|
||||
输出到 exe 目录(tools/dist/), 并把前 3 张写入 pattern_slots.json,
|
||||
启动 hdmi_gui 后自定义栏位即可直接点用; 其余两张可用 "⚙" 换上。
|
||||
|
||||
图案清单(均为 640x360 黑白, 1:1 投屏无缩放):
|
||||
bw_star.png 西门子星: 60 楔放射星, 看极限分辨率与中心偏移
|
||||
bw_sweep.png 频率扫描条纹: 周期 24px 渐减到 3px(上竖线/下横线)
|
||||
bw_checker_sweep.png 渐变棋盘: 格子尺寸 2px 递增到 32px 共 9 段
|
||||
bw_zoneplate.png 同心波带片: 环距向中心递减, 看各向混叠
|
||||
bw_testcard.png 综合测试卡: 灰阶/刻度/细棋盘/粗细线/标记
|
||||
|
||||
用法: python gen_bw_patterns.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
W, H = 640, 360
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT = os.path.join(BASE, 'dist')
|
||||
|
||||
|
||||
def save(img, name):
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
path = os.path.join(OUT, name)
|
||||
img.save(path)
|
||||
print(f'写出 {path}')
|
||||
|
||||
|
||||
def gen_star():
|
||||
"""西门子星: 60 个黑白交替楔形, 边缘楔宽约 17px, 向中心无限加密"""
|
||||
img = Image.new('L', (W, H), 255)
|
||||
d = ImageDraw.Draw(img)
|
||||
cx, cy, R = W // 2, H // 2, 165
|
||||
n = 60
|
||||
for i in range(0, n, 2): # 黑楔在偶数位, 底为白
|
||||
d.pieslice([cx - R, cy - R, cx + R, cy + R],
|
||||
i * 360.0 / n, (i + 1) * 360.0 / n, fill=0)
|
||||
d.ellipse([cx - R - 6, cy - R - 6, cx + R + 6, cy + R + 6],
|
||||
outline=0, width=3)
|
||||
d.rectangle([0, 0, W - 1, H - 1], outline=0, width=2)
|
||||
save(img, 'bw_star.png')
|
||||
|
||||
|
||||
def gen_sweep():
|
||||
"""频率扫描条纹: 左到右黑白条纹周期 24px 渐减到 3px; 上半竖线, 下半横线"""
|
||||
img = Image.new('L', (W, H), 255)
|
||||
px = img.load()
|
||||
# 上半: 竖条纹
|
||||
ph = 0.0
|
||||
for x in range(W):
|
||||
t = x / (W - 1)
|
||||
ph += 1.0 / (24 - 21 * t)
|
||||
if (ph % 1.0) < 0.5:
|
||||
for y in range(H // 2):
|
||||
px[x, y] = 0
|
||||
# 下半: 横条纹
|
||||
ph = 0.0
|
||||
for y in range(H // 2, H):
|
||||
t = (y - H // 2) / (H // 2 - 1)
|
||||
ph += 1.0 / (24 - 21 * t)
|
||||
if (ph % 1.0) < 0.5:
|
||||
for x in range(W):
|
||||
px[x, y] = 0
|
||||
d = ImageDraw.Draw(img)
|
||||
d.line([0, H // 2, W, H // 2], fill=0, width=2)
|
||||
d.rectangle([0, 0, W - 1, H - 1], outline=0, width=2)
|
||||
d.text((8, 6), 'H sweep 24px->3px', fill=0)
|
||||
d.text((8, H // 2 + 6), 'V sweep 24px->3px', fill=0)
|
||||
save(img, 'bw_sweep.png')
|
||||
|
||||
|
||||
def gen_checker_sweep():
|
||||
"""渐变棋盘: 格子尺寸从 2px 逐段加倍到 32px, 看多大格子开始可分辨"""
|
||||
img = Image.new('L', (W, H), 0)
|
||||
d = ImageDraw.Draw(img)
|
||||
sizes = [2, 3, 4, 6, 8, 12, 16, 24, 32]
|
||||
seg = W / len(sizes)
|
||||
for i, s in enumerate(sizes):
|
||||
x0, x1 = int(round(i * seg)), int(round((i + 1) * seg))
|
||||
for yy in range(0, H, s):
|
||||
for xx in range(x0, x1, s):
|
||||
if ((xx // s) + (yy // s)) % 2 == 0:
|
||||
d.rectangle([xx, yy, min(xx + s, x1) - 1, min(yy + s, H) - 1],
|
||||
fill=255)
|
||||
d.line([x1, 0, x1, H], fill=255, width=2)
|
||||
d.text((4, 4), 'checker 2..32px', fill=255)
|
||||
save(img, 'bw_checker_sweep.png')
|
||||
|
||||
|
||||
def gen_zoneplate():
|
||||
"""同心波带片: 环距中心处约 31px、边缘约 3.5px, 观察各方向混叠"""
|
||||
img = Image.new('L', (W, H), 255)
|
||||
px = img.load()
|
||||
cx, cy, k = W / 2, H / 2, 0.0008
|
||||
for y in range(H):
|
||||
dy = y - cy
|
||||
for x in range(W):
|
||||
dx = x - cx
|
||||
if math.cos(k * (dx * dx + dy * dy)) > 0:
|
||||
px[x, y] = 0
|
||||
d = ImageDraw.Draw(img)
|
||||
d.rectangle([0, 0, W - 1, H - 1], outline=0, width=2)
|
||||
save(img, 'bw_zoneplate.png')
|
||||
|
||||
|
||||
def gen_testcard():
|
||||
"""综合测试卡: 灰阶条 + 刻度 + 细棋盘 + 粗细线 + 角标"""
|
||||
img = Image.new('L', (W, H), 0)
|
||||
d = ImageDraw.Draw(img)
|
||||
|
||||
# 外框与 40px 刻度(边缘短刻线)
|
||||
d.rectangle([0, 0, W - 1, H - 1], outline=255, width=2)
|
||||
for x in range(40, W, 40):
|
||||
d.line([x, 0, x, 10], fill=255, width=1)
|
||||
d.line([x, H, x, H - 10], fill=255, width=1)
|
||||
for y in range(40, H, 40):
|
||||
d.line([0, y, 10, y], fill=255, width=1)
|
||||
d.line([W, y, W - 10, y], fill=255, width=1)
|
||||
|
||||
# 顶部 16 级灰阶条
|
||||
gw = W // 16
|
||||
for i in range(16):
|
||||
v = round(i * 255 / 15)
|
||||
d.rectangle([i * gw, 20, (i + 1) * gw - 1, 44], fill=v, outline=255)
|
||||
|
||||
# 左下: 1px 棋盘块 (640/4=160 宽 x 72 高)
|
||||
bx, by, bs = 16, H - 96, 2
|
||||
for yy in range(by, by + 72, bs):
|
||||
for xx in range(bx, bx + 160, bs):
|
||||
if ((xx // bs) + (yy // bs)) % 2 == 0:
|
||||
d.rectangle([xx, yy, xx + bs - 1, yy + bs - 1], fill=255)
|
||||
d.text((bx, by - 14), '2px checker', fill=255)
|
||||
|
||||
# 右下: 1/2/4px 白线组(横线), 看线条再现
|
||||
lx = W - 210
|
||||
for row, lw in enumerate((1, 2, 4)):
|
||||
y0 = H - 100 + row * 26
|
||||
for off in range(0, 13, lw * 2):
|
||||
d.line([lx, y0 + off, lx + 190, y0 + off], fill=255, width=lw)
|
||||
d.text((lx, H - 114), '1/2/4px lines', fill=255)
|
||||
|
||||
# 中心十字(1px)
|
||||
cx, cy = W // 2, H // 2
|
||||
d.line([cx, 60, cx, H - 60], fill=255, width=1)
|
||||
d.line([W // 4, cy, W - W // 4, cy], fill=255, width=1)
|
||||
|
||||
# 四角 L 标记
|
||||
m, L = 16, 28
|
||||
for sx, sy in ((m, m), (W - 1 - m, m), (m, H - 1 - m), (W - 1 - m, H - 1 - m)):
|
||||
dx = -1 if sx > cx else 1
|
||||
dy = -1 if sy > cy else 1
|
||||
d.line([sx, sy, sx + dx * L, sy], fill=255, width=2)
|
||||
d.line([sx, sy, sx, sy + dy * L], fill=255, width=2)
|
||||
|
||||
d.text((W // 2 - 60, 52), '640x360 TEST', fill=255)
|
||||
save(img, 'bw_testcard.png')
|
||||
|
||||
|
||||
def write_slots():
|
||||
slots = [
|
||||
{'name': '西门子星', 'file': 'bw_star.png'},
|
||||
{'name': '频率条纹', 'file': 'bw_sweep.png'},
|
||||
{'name': '渐变棋盘', 'file': 'bw_checker_sweep.png'},
|
||||
]
|
||||
path = os.path.join(OUT, 'pattern_slots.json')
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(slots, f, ensure_ascii=False, indent=2)
|
||||
print(f'写出 {path}(前 3 张已配入自定义栏位, 其余两张可用 "⚙" 换上)')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
gen_star()
|
||||
gen_sweep()
|
||||
gen_checker_sweep()
|
||||
gen_zoneplate()
|
||||
gen_testcard()
|
||||
write_slots()
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
生成 800x600 纯色测试图, 供 RGB 位交换(0x606E)验证使用
|
||||
================================================================
|
||||
配合 demo 板当前 EDID 首选时序 800x600@60, 全屏纯色输出。
|
||||
用法: python gen_solid_colors.py → 输出到 tools/dist/solid_*.png
|
||||
在 HDMI-Tool 中通过自定义栏位(⚙ 换图)加载。
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from PIL import Image
|
||||
|
||||
W, H = 800, 600 # 与 demo 板 EDID 首选时序一致, 全屏铺满
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT = os.path.join(BASE, 'dist')
|
||||
|
||||
# (文件名后缀, RGB 值, 用途)
|
||||
COLORS = [
|
||||
('FF0000', (0xFF, 0x00, 0x00), '通道映射: R=D[23:16]'),
|
||||
('00FF00', (0x00, 0xFF, 0x00), '通道映射: G=D[15:8]'),
|
||||
('0000FF', (0x00, 0x00, 0xFF), '通道映射: B=D[7:0]'),
|
||||
('AA0000', (0xAA, 0x00, 0x00), 'R 交换检测: 0xAA(10101010)'),
|
||||
('550000', (0x55, 0x00, 0x00), 'R 交换检测: 0x55(01010101)'),
|
||||
('00AA00', (0x00, 0xAA, 0x00), 'G 交换检测'),
|
||||
('005500', (0x00, 0x55, 0x00), 'G 交换检测'),
|
||||
('0000AA', (0x00, 0x00, 0xAA), 'B 交换检测'),
|
||||
('000055', (0x00, 0x00, 0x55), 'B 交换检测'),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
for suffix, rgb, note in COLORS:
|
||||
img = Image.new('RGB', (W, H), rgb)
|
||||
path = os.path.join(OUT, f'solid_{suffix}.png')
|
||||
img.save(path)
|
||||
print(f'写出 {path} # {note}')
|
||||
print(f'\n共 {len(COLORS)} 张, 分辨率 {W}x{H}(匹配 demo 板 800x600 EDID)。')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
rem HDMI source test tool launcher (bypasses PowerShell execution policy)
|
||||
rem Examples:
|
||||
rem hdmi_source.bat -List
|
||||
rem hdmi_source.bat -Mode 640x360
|
||||
rem hdmi_source.bat -Mode 640x360 -Pattern -Restore
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0hdmi_source.ps1" %*
|
||||
@@ -0,0 +1,487 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
HDMI 源端测试工具 —— 枚举/切换输出分辨率 + 全屏测试图案
|
||||
|
||||
.DESCRIPTION
|
||||
配合 LT8619C(HDMI 转 RGB888)→ DLPC3421 转接板调试使用。
|
||||
|
||||
原理:电脑显卡通过 HDMI DDC 读取转接板 EDID 来枚举可用分辨率,
|
||||
本脚本调用 Win32 API 完成模式枚举与切换,再用 WinForms 全屏窗口
|
||||
输出测试图案。板载 EDID 只声明了 640x360@60(25.2MHz 像素时钟),
|
||||
插上 HDMI 后显卡应只能发这一个模式;-List 可直接验证 EDID 是否生效。
|
||||
|
||||
测试图案用途:
|
||||
Bars 100% 彩条 —— 肉眼/示波器验证 RGB 通道顺序与极性
|
||||
GraySteps 16 级灰阶 —— 验证 RGB888 各 bit 连通性
|
||||
Gradient R/G/B 三段渐变 —— 验证每通道 8bit 线性度
|
||||
Checker 黑白棋盘格 —— 验证像素时钟 PCLK 与 DE 对位
|
||||
Grid 网格+中心十字 —— 验证像素映射/偏移/缩放
|
||||
Red/Green/Blue/White/Black/Gray50 纯色 —— 万用表/示波器测单通道电平
|
||||
|
||||
全屏窗口操作:空格或→下一图案,←上一图案,ESC 退出。
|
||||
|
||||
.EXAMPLE
|
||||
tools\hdmi_source.bat -List
|
||||
枚举所有活动显示器与显卡支持的分辨率(验证 EDID)
|
||||
|
||||
.EXAMPLE
|
||||
tools\hdmi_source.bat -Monitor 1 -Mode 640x360
|
||||
把显示器 1 切到 640x360(缺省 60Hz,可写 640x360@60)
|
||||
|
||||
.EXAMPLE
|
||||
tools\hdmi_source.bat -Mode 640x360 -Pattern -Restore
|
||||
切到 640x360 并全屏出图案,退出图案后恢复原分辨率
|
||||
|
||||
.EXAMPLE
|
||||
tools\hdmi_source.bat -Pattern -PatternName Checker -Seconds 5
|
||||
当前分辨率下全屏棋盘格,5 秒后自动退出(自动化测试用)
|
||||
#>
|
||||
|
||||
param(
|
||||
[switch]$List,
|
||||
[int]$Monitor = 1,
|
||||
[string]$Mode = '',
|
||||
[switch]$Pattern,
|
||||
[ValidateSet('', 'Bars', 'GraySteps', 'Gradient', 'Checker', 'Grid',
|
||||
'Red', 'Green', 'Blue', 'White', 'Black', 'Gray50')]
|
||||
[string]$PatternName = '',
|
||||
[int]$Seconds = 0,
|
||||
[switch]$Restore
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# ---------------- Win32 显示 API ----------------
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace HdmiSrc
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||
public struct DISPLAY_DEVICE
|
||||
{
|
||||
public int cb;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string DeviceName;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceString;
|
||||
public int StateFlags;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceID;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceKey;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||
public struct DEVMODE
|
||||
{
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string dmDeviceName;
|
||||
public short dmSpecVersion;
|
||||
public short dmDriverVersion;
|
||||
public short dmSize;
|
||||
public short dmDriverExtra;
|
||||
public int dmFields;
|
||||
public int dmPositionX;
|
||||
public int dmPositionY;
|
||||
public int dmDisplayOrientation;
|
||||
public int dmDisplayFixedOutput;
|
||||
public short dmColor;
|
||||
public short dmDuplex;
|
||||
public short dmYResolution;
|
||||
public short dmTTOption;
|
||||
public short dmCollate;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string dmFormName;
|
||||
public short dmLogPixels;
|
||||
public int dmBitsPerPel;
|
||||
public int dmPelsWidth;
|
||||
public int dmPelsHeight;
|
||||
public int dmDisplayFlags;
|
||||
public int dmDisplayFrequency;
|
||||
public int dmICMMethod;
|
||||
public int dmICMIntent;
|
||||
public int dmMediaType;
|
||||
public int dmDitherType;
|
||||
public int dmReserved1;
|
||||
public int dmReserved2;
|
||||
public int dmPanningWidth;
|
||||
public int dmPanningHeight;
|
||||
}
|
||||
|
||||
public static class Native
|
||||
{
|
||||
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
|
||||
public static extern bool EnumDisplayDevices(string lpDevice, uint iDevNum,
|
||||
ref DISPLAY_DEVICE lpDisplayDevice, uint dwFlags);
|
||||
|
||||
// PowerShell 会把 $null marshal 成空串而非 NULL,由 C# 侧保证传 NULL
|
||||
public static bool EnumDisplayAdapters(uint iDevNum, ref DISPLAY_DEVICE lpDisplayDevice)
|
||||
{
|
||||
return EnumDisplayDevices(null, iDevNum, ref lpDisplayDevice, 0);
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
|
||||
public static extern bool EnumDisplaySettings(string lpszDeviceName, int iModeNum,
|
||||
ref DEVMODE lpDevMode);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
|
||||
public static extern int ChangeDisplaySettingsEx(string lpszDeviceName,
|
||||
ref DEVMODE lpDevMode, IntPtr hwnd, int dwflags, IntPtr lParam);
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
# 板载 EDID 声明的目标模式,-List 时用于自检
|
||||
$TargetW = 640
|
||||
$TargetH = 360
|
||||
|
||||
function New-DevMode {
|
||||
$dm = New-Object HdmiSrc.DEVMODE
|
||||
$dm.dmDeviceName = ''
|
||||
$dm.dmFormName = ''
|
||||
$dm.dmSize = [System.Runtime.InteropServices.Marshal]::SizeOf($dm)
|
||||
return $dm
|
||||
}
|
||||
|
||||
function New-DisplayDevice {
|
||||
$dd = New-Object HdmiSrc.DISPLAY_DEVICE
|
||||
$dd.DeviceName = ''
|
||||
$dd.DeviceString = ''
|
||||
$dd.DeviceID = ''
|
||||
$dd.DeviceKey = ''
|
||||
$dd.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($dd)
|
||||
return $dd
|
||||
}
|
||||
|
||||
# 枚举所有"已连接到桌面"的显示器(即插着 HDMI 的输出口)
|
||||
function Get-Displays {
|
||||
$list = @()
|
||||
$i = 0
|
||||
while ($true) {
|
||||
$dd = New-DisplayDevice
|
||||
if (-not [HdmiSrc.Native]::EnumDisplayAdapters($i, [ref]$dd)) { break }
|
||||
# StateFlags bit0 = ATTACHED_TO_DESKTOP
|
||||
if ($dd.StateFlags -band 0x1) {
|
||||
$mon = New-DisplayDevice
|
||||
[void][HdmiSrc.Native]::EnumDisplayDevices($dd.DeviceName, 0, [ref]$mon, 0)
|
||||
$list += [pscustomobject]@{
|
||||
Index = $list.Count + 1
|
||||
Device = $dd.DeviceName
|
||||
Adapter = $dd.DeviceString
|
||||
MonName = $mon.DeviceString
|
||||
}
|
||||
}
|
||||
$i++
|
||||
}
|
||||
return $list
|
||||
}
|
||||
|
||||
# 显卡枚举到的全部显示模式(去重)
|
||||
function Get-Modes {
|
||||
param([string]$Device)
|
||||
$modes = @()
|
||||
$i = 0
|
||||
while ($true) {
|
||||
$dm = New-DevMode
|
||||
if (-not [HdmiSrc.Native]::EnumDisplaySettings($Device, $i, [ref]$dm)) { break }
|
||||
$modes += [pscustomobject]@{
|
||||
W = $dm.dmPelsWidth
|
||||
H = $dm.dmPelsHeight
|
||||
Hz = $dm.dmDisplayFrequency
|
||||
Bpp = $dm.dmBitsPerPel
|
||||
}
|
||||
$i++
|
||||
}
|
||||
return ($modes | Sort-Object W, H, Hz, Bpp -Unique)
|
||||
}
|
||||
|
||||
function Get-CurrentDevMode {
|
||||
param([string]$Device)
|
||||
$dm = New-DevMode
|
||||
[void][HdmiSrc.Native]::EnumDisplaySettings($Device, -1, [ref]$dm) # ENUM_CURRENT_SETTINGS
|
||||
return $dm
|
||||
}
|
||||
|
||||
# 应用一个 DEVMODE(先 CDS_TEST 再真正生效),返回 $true/$false
|
||||
function Set-DevMode {
|
||||
param([string]$Device, $dm)
|
||||
$dm.dmFields = 0x00040000 -bor 0x00080000 -bor 0x00100000 -bor 0x00400000
|
||||
# DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY
|
||||
$rc = [HdmiSrc.Native]::ChangeDisplaySettingsEx($Device, [ref]$dm, [IntPtr]::Zero, 2, [IntPtr]::Zero)
|
||||
if ($rc -ne 0) {
|
||||
Write-Host (" 错误: 显卡拒绝该模式 (CDS_TEST 返回 {0})" -f $rc) -ForegroundColor Red
|
||||
return $false
|
||||
}
|
||||
$rc = [HdmiSrc.Native]::ChangeDisplaySettingsEx($Device, [ref]$dm, [IntPtr]::Zero, 0, [IntPtr]::Zero)
|
||||
if ($rc -ne 0) {
|
||||
Write-Host (" 错误: 模式应用失败 (返回 {0})" -f $rc) -ForegroundColor Red
|
||||
return $false
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
# 在显卡枚举的模式里找 W x H [@Hz] 并切换
|
||||
function Switch-Mode {
|
||||
param([string]$Device, [int]$W, [int]$H, [int]$Hz)
|
||||
$all = @(Get-Modes $Device)
|
||||
$hit = @($all | Where-Object { $_.W -eq $W -and $_.H -eq $H -and ($Hz -le 0 -or $_.Hz -eq $Hz) })
|
||||
if (-not $hit) {
|
||||
Write-Host (" 错误: 显卡未枚举到 {0}x{1} 模式" -f $W, $H) -ForegroundColor Red
|
||||
Write-Host " 可能原因:" -ForegroundColor Yellow
|
||||
Write-Host " 1. 转接板 EDID 未被读取(检查 HPD / DDC / LT8619C 供电)"
|
||||
Write-Host " 2. EDID 中未包含该分辨率(当前板载 EDID 只有 640x360@60)"
|
||||
Write-Host " 3. 显卡驱动拒收该 DTD(可用 CRU 手动添加自定义分辨率)"
|
||||
Write-Host " 先运行 -List 查看显卡实际枚举到了哪些模式。"
|
||||
return $false
|
||||
}
|
||||
$dm = New-DevMode
|
||||
$dm.dmPelsWidth = $W
|
||||
$dm.dmPelsHeight = $H
|
||||
$dm.dmDisplayFrequency = $hit[0].Hz
|
||||
$dm.dmBitsPerPel = 32
|
||||
$ok = Set-DevMode $Device $dm
|
||||
if ($ok) {
|
||||
Write-Host (" 已切换 {0} -> {1}x{2}@{3}" -f $Device, $W, $H, $hit[0].Hz) -ForegroundColor Green
|
||||
}
|
||||
return $ok
|
||||
}
|
||||
|
||||
# ---------------- 全屏测试图案 ----------------
|
||||
function Show-PatternWindow {
|
||||
param(
|
||||
[string]$Device = '',
|
||||
[string]$StartPattern = '',
|
||||
[int]$AutoSeconds = 0
|
||||
)
|
||||
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
# 窗口放到目标设备名对应的屏幕上(如 \\.\DISPLAY2),找不到则回退主屏
|
||||
$screen = [System.Windows.Forms.Screen]::AllScreens |
|
||||
Where-Object { $_.DeviceName -eq $Device } |
|
||||
Select-Object -First 1
|
||||
if (-not $screen) { $screen = [System.Windows.Forms.Screen]::PrimaryScreen }
|
||||
|
||||
$script:patterns = @('Bars', 'GraySteps', 'Gradient', 'Checker', 'Grid',
|
||||
'Red', 'Green', 'Blue', 'White', 'Black', 'Gray50')
|
||||
$script:pi = 0
|
||||
if ($StartPattern) {
|
||||
$idx = [array]::IndexOf($script:patterns, $StartPattern)
|
||||
if ($idx -ge 0) { $script:pi = $idx }
|
||||
}
|
||||
|
||||
$script:form = New-Object System.Windows.Forms.Form
|
||||
$script:form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None
|
||||
$script:form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
|
||||
$script:form.Bounds = $screen.Bounds
|
||||
$script:form.TopMost = $true
|
||||
$script:form.KeyPreview = $true
|
||||
# 双缓冲避免棋盘格/网格闪烁
|
||||
$script:form.GetType().GetProperty('DoubleBuffered',
|
||||
[System.Reflection.BindingFlags]'Instance,NonPublic').SetValue($script:form, $true, $null)
|
||||
|
||||
$osdFont = New-Object System.Drawing.Font('Consolas', 11)
|
||||
|
||||
$paintHandler = {
|
||||
param($s, $e)
|
||||
$g = $e.Graphics
|
||||
$w = $e.ClipRectangle.Width
|
||||
$h = $e.ClipRectangle.Height
|
||||
$name = $script:patterns[$script:pi]
|
||||
|
||||
switch ($name) {
|
||||
'Bars' {
|
||||
# 100% 饱和度竖彩条:白黄青绿品红红蓝黑
|
||||
$c = @(@(255,255,255), @(255,255,0), @(0,255,255), @(0,255,0),
|
||||
@(255,0,255), @(255,0,0), @(0,0,255), @(0,0,0))
|
||||
$n = $c.Count
|
||||
for ($k = 0; $k -lt $n; $k++) {
|
||||
$bw = [Math]::Ceiling($w / $n)
|
||||
$brush = New-Object System.Drawing.SolidBrush (
|
||||
[System.Drawing.Color]::FromArgb($c[$k][0], $c[$k][1], $c[$k][2]))
|
||||
$g.FillRectangle($brush, [int]($k * $bw), 0, [int]$bw + 1, $h)
|
||||
$brush.Dispose()
|
||||
}
|
||||
}
|
||||
'GraySteps' {
|
||||
# 16 级灰阶,验证 RGB888 各 bit
|
||||
$n = 16
|
||||
for ($k = 0; $k -lt $n; $k++) {
|
||||
$v = [int]($k * 255 / ($n - 1))
|
||||
$bw = [Math]::Ceiling($w / $n)
|
||||
$brush = New-Object System.Drawing.SolidBrush (
|
||||
[System.Drawing.Color]::FromArgb($v, $v, $v))
|
||||
$g.FillRectangle($brush, [int]($k * $bw), 0, [int]$bw + 1, $h)
|
||||
$brush.Dispose()
|
||||
}
|
||||
}
|
||||
'Gradient' {
|
||||
# R/G/B 三段水平渐变,验证每通道 8bit 线性度
|
||||
$seg = [Math]::Ceiling($w / 3.0)
|
||||
$rgbIdx = @(0, 1, 2)
|
||||
foreach ($ch in $rgbIdx) {
|
||||
for ($x = 0; $x -lt $seg; $x++) {
|
||||
$v = [int](255 * $x / [Math]::Max(1.0, ($seg - 1)))
|
||||
$r = 0; $gg = 0; $b = 0
|
||||
if ($ch -eq 0) { $r = $v } elseif ($ch -eq 1) { $gg = $v } else { $b = $v }
|
||||
$brush = New-Object System.Drawing.SolidBrush (
|
||||
[System.Drawing.Color]::FromArgb($r, $gg, $b))
|
||||
$g.FillRectangle($brush, [int]($ch * $seg + $x), 0, 1, $h)
|
||||
$brush.Dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
'Checker' {
|
||||
# 黑白棋盘格,验证 PCLK/DE 对位
|
||||
$g.FillRectangle([System.Drawing.Brushes]::Black, 0, 0, $w, $h)
|
||||
$cell = 16
|
||||
for ($y = 0; $y -lt $h; $y += $cell) {
|
||||
for ($x = 0; $x -lt $w; $x += $cell) {
|
||||
if ((([int]($x / $cell) + [int]($y / $cell)) % 2) -eq 0) {
|
||||
$g.FillRectangle([System.Drawing.Brushes]::White, $x, $y, $cell, $cell)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'Grid' {
|
||||
# 黑底 + 40px 网格 + 中心十字 + 白色边框,验证像素映射
|
||||
$g.FillRectangle([System.Drawing.Brushes]::Black, 0, 0, $w, $h)
|
||||
$pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(0, 128, 0), 1)
|
||||
for ($x = 40; $x -lt $w; $x += 40) { $g.DrawLine($pen, $x, 0, $x, $h) }
|
||||
for ($y = 40; $y -lt $h; $y += 40) { $g.DrawLine($pen, 0, $y, $w, $y) }
|
||||
$pen.Dispose()
|
||||
$wp = New-Object System.Drawing.Pen ([System.Drawing.Color]::White, 2)
|
||||
$cx = [int]($w / 2); $cy = [int]($h / 2)
|
||||
$g.DrawLine($wp, $cx, 0, $cx, $h)
|
||||
$g.DrawLine($wp, 0, $cy, $w, $cy)
|
||||
$g.DrawRectangle($wp, 1, 1, $w - 3, $h - 3)
|
||||
$wp.Dispose()
|
||||
}
|
||||
'Red' { $g.FillRectangle([System.Drawing.Brushes]::Red, 0, 0, $w, $h) }
|
||||
'Green' { $g.FillRectangle([System.Drawing.Brushes]::Green, 0, 0, $w, $h) }
|
||||
'Blue' { $g.FillRectangle([System.Drawing.Brushes]::Blue, 0, 0, $w, $h) }
|
||||
'White' { $g.FillRectangle([System.Drawing.Brushes]::White, 0, 0, $w, $h) }
|
||||
'Black' { $g.FillRectangle([System.Drawing.Brushes]::Black, 0, 0, $w, $h) }
|
||||
'Gray50' {
|
||||
$brush = New-Object System.Drawing.SolidBrush (
|
||||
[System.Drawing.Color]::FromArgb(128, 128, 128))
|
||||
$g.FillRectangle($brush, 0, 0, $w, $h)
|
||||
$brush.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
# 左上角 OSD:图案名 + 分辨率 + 按键提示
|
||||
$text = "[{0}] {1}x{2} 空格/→:下一图案 ←:上一图案 ESC:退出" -f $name, $w, $h
|
||||
$sz = $g.MeasureString($text, $osdFont)
|
||||
$bg = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(150, 0, 0, 0))
|
||||
$g.FillRectangle($bg, 6, 6, $sz.Width + 12, $sz.Height + 8)
|
||||
$bg.Dispose()
|
||||
$g.DrawString($text, $osdFont, [System.Drawing.Brushes]::White, 12, 10)
|
||||
}
|
||||
$script:form.Add_Paint($paintHandler)
|
||||
|
||||
$script:form.Add_KeyDown({
|
||||
param($s, $e)
|
||||
switch ($e.KeyCode) {
|
||||
'Escape' { $s.Close() }
|
||||
'Space' { $script:pi = ($script:pi + 1) % $script:patterns.Count; $s.Invalidate() }
|
||||
'Right' { $script:pi = ($script:pi + 1) % $script:patterns.Count; $s.Invalidate() }
|
||||
'Left' { $script:pi = ($script:pi - 1 + $script:patterns.Count) % $script:patterns.Count; $s.Invalidate() }
|
||||
}
|
||||
})
|
||||
|
||||
$script:form.Add_Activated({ [System.Windows.Forms.Cursor]::Hide() })
|
||||
$script:form.Add_FormClosed({ [System.Windows.Forms.Cursor]::Show() })
|
||||
|
||||
if ($AutoSeconds -gt 0) {
|
||||
$timer = New-Object System.Windows.Forms.Timer
|
||||
$timer.Interval = $AutoSeconds * 1000
|
||||
$timer.Add_Tick({ $script:form.Close() })
|
||||
$timer.Start()
|
||||
}
|
||||
|
||||
Write-Host ("全屏图案已启动于 {0} ({1}x{2})" -f $screen.DeviceName, $screen.Bounds.Width, $screen.Bounds.Height)
|
||||
Write-Host "空格/→:下一图案 ←:上一图案 ESC:退出"
|
||||
[void]$script:form.ShowDialog()
|
||||
}
|
||||
|
||||
function Show-Usage {
|
||||
Write-Host @"
|
||||
用法:
|
||||
hdmi_source.bat -List 枚举显示器与可用分辨率(验证 EDID)
|
||||
hdmi_source.bat -Monitor <n> -Mode <WxH[@Hz]> 切换分辨率, 如 -Mode 640x360 或 640x360@60
|
||||
hdmi_source.bat [-Monitor <n>] -Pattern [-PatternName <名>] [-Seconds <n>]
|
||||
全屏测试图案(Bar/GraySteps/Gradient/Checker/Grid/纯色)
|
||||
可选: -Restore 退出图案窗口后恢复切换前的分辨率
|
||||
-Seconds <n> 图案 n 秒后自动退出(自动化测试)
|
||||
|
||||
典型调试流程:
|
||||
1. HDMI 插上转接板 -> -List 确认出现 640x360(EDID 生效)
|
||||
2. -Mode 640x360 -Pattern -> 全屏图案, 逐项验证 RGB 通道/位序/像素映射
|
||||
"@
|
||||
}
|
||||
|
||||
# ---------------- 主流程 ----------------
|
||||
$displays = @(Get-Displays) # @() 保证单显示器时不退化为标量
|
||||
if ($displays.Count -eq 0) {
|
||||
Write-Host "错误: 未找到活动显示器" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($List) {
|
||||
foreach ($d in $displays) {
|
||||
Write-Host ""
|
||||
Write-Host ("== 显示器 {0} {1} 显卡[{2}] 监视器[{3}]" -f $d.Index, $d.Device, $d.Adapter, $d.MonName) -ForegroundColor Cyan
|
||||
$cur = Get-CurrentDevMode $d.Device
|
||||
Write-Host (" 当前模式: {0}x{1}@{2} {3}bpp" -f $cur.dmPelsWidth, $cur.dmPelsHeight, $cur.dmDisplayFrequency, $cur.dmBitsPerPel)
|
||||
$modes = Get-Modes $d.Device
|
||||
$modes | Format-Table W, H, Hz, Bpp -AutoSize
|
||||
$hasTarget = $modes | Where-Object { $_.W -eq $TargetW -and $_.H -eq $TargetH }
|
||||
if ($hasTarget) {
|
||||
Write-Host (" [OK] 找到板载 EDID 目标模式 {0}x{1} -> EDID 已被显卡读取" -f $TargetW, $TargetH) -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host (" [--] 未找到 {0}x{1} (若此口接的是转接板, 说明 EDID/HPD 未生效)" -f $TargetW, $TargetH) -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not $Mode -and -not $Pattern) {
|
||||
Show-Usage
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($Monitor -gt $displays.Count) {
|
||||
Write-Host ("错误: -Monitor 超出范围(共 {0} 个活动显示器, 运行 -List 查看)" -f $displays.Count) -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$disp = $displays[$Monitor - 1]
|
||||
Write-Host ("目标: 显示器 {0} = {1}" -f $disp.Index, $disp.Device)
|
||||
|
||||
$prevMode = $null
|
||||
if ($Mode) {
|
||||
if ($Mode -notmatch '^\s*(\d+)\s*[xX]\s*(\d+)\s*(?:@\s*(\d+)\s*)?$') {
|
||||
Write-Host "错误: -Mode 格式应为 WxH 或 WxH@Hz, 例如 640x360 或 640x360@60" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$w = [int]$Matches[1]; $h = [int]$Matches[2]
|
||||
$hz = 0
|
||||
if ($Matches[3]) { $hz = [int]$Matches[3] }
|
||||
|
||||
$prevMode = Get-CurrentDevMode $disp.Device
|
||||
$ok = Switch-Mode $disp.Device $w $h $hz
|
||||
if (-not $ok) { exit 1 }
|
||||
Start-Sleep -Milliseconds 300 # 等显卡/显示器完成模式切换
|
||||
}
|
||||
|
||||
if ($Pattern) {
|
||||
if ($Restore -and -not $prevMode) {
|
||||
Write-Host "提示: -Restore 仅在与 -Mode 同用时生效" -ForegroundColor Yellow
|
||||
}
|
||||
try {
|
||||
Show-PatternWindow -Device $disp.Device -StartPattern $PatternName -AutoSeconds $Seconds
|
||||
}
|
||||
finally {
|
||||
if ($Restore -and $prevMode) {
|
||||
Write-Host ("恢复原分辨率 {0}x{1}@{2} ..." -f $prevMode.dmPelsWidth, $prevMode.dmPelsHeight, $prevMode.dmDisplayFrequency)
|
||||
[void](Set-DevMode $disp.Device $prevMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" fill="#000000"/>
|
||||
<rect x="38" y="64" width="436" height="186" rx="34" fill="#FF9900"/>
|
||||
<text x="256" y="210" font-family="Arial, 'Microsoft YaHei', sans-serif" font-size="146" font-weight="900" fill="#000000" text-anchor="middle">HDMI</text>
|
||||
<text x="256" y="421" font-family="Arial, 'Microsoft YaHei', sans-serif" font-size="146" font-weight="900" fill="#FFFFFF" text-anchor="middle">Tool</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 506 B |
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
把 logo_ph_orange.svg 同款设计栅格化为多尺寸 .ico(供 exe 图标)
|
||||
================================================================
|
||||
设计参数与 SVG 严格一致: 黑底 512, 橙色圆角矩形 x38 y64 w436 h186 rx34,
|
||||
HDMI 146px Arial Bold 黑字(框内居中), Tool 146px 白字(基线 421)。
|
||||
|
||||
输出: logos/logo_ph_orange.png(512, 预览/复用)
|
||||
logos/logo_ph_orange.ico(16~256 七档, exe 图标用)
|
||||
|
||||
用法: python make_ico.py
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
FONT = r'C:\Windows\Fonts\arialbd.ttf' # Arial Bold, Windows 标配
|
||||
|
||||
|
||||
def render(size=512):
|
||||
s = size / 512.0
|
||||
img = Image.new('RGB', (size, size), (0, 0, 0))
|
||||
d = ImageDraw.Draw(img)
|
||||
d.rounded_rectangle([38 * s, 64 * s, (38 + 436) * s, (64 + 186) * s],
|
||||
radius=34 * s, fill=(255, 153, 0))
|
||||
if not os.path.exists(FONT):
|
||||
raise SystemExit(f'未找到字体 {FONT}, 无法栅格化')
|
||||
f = ImageFont.truetype(FONT, max(8, round(146 * s)))
|
||||
cx = size / 2
|
||||
# HDMI: 居中于橙色框
|
||||
d.text((cx, 64 * s + 186 * s / 2), 'HDMI', font=f, fill=(0, 0, 0), anchor='mm')
|
||||
# Tool: 与 SVG 基线 421 对齐(无下伸字母, 中心 = 421 - cap/2 ~= 368)
|
||||
d.text((cx, 368 * s), 'Tool', font=f, fill=(255, 255, 255), anchor='mm')
|
||||
return img
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
img = render(512)
|
||||
png = os.path.join(BASE, 'logo_ph_orange.png')
|
||||
ico = os.path.join(BASE, 'logo_ph_orange.ico')
|
||||
img.save(png)
|
||||
img.save(ico, sizes=[(16, 16), (24, 24), (32, 32), (48, 48),
|
||||
(64, 64), (128, 128), (256, 256)])
|
||||
print(f'写出 {png}')
|
||||
print(f'写出 {ico}')
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"area": "800x600",
|
||||
"slots": [
|
||||
{
|
||||
"name": "西门子星",
|
||||
"file": "bw_star.png"
|
||||
},
|
||||
{
|
||||
"name": "频率条纹",
|
||||
"file": "bw_sweep.png"
|
||||
},
|
||||
{
|
||||
"name": "渐变棋盘",
|
||||
"file": "bw_checker_sweep.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |