我之前已经有一个 truecolor half-block 版本:把图片压到终端宽度后,用上半块和下半块分别保存两行像素的颜色。这个方法稳定、兼容性好,但每个字符只能表达上下两个采样点。想让图片在命令行里更细,就要把一个字符格继续拆开,改用 Unicode 的四象限块字符。
这篇文章说明新版转换器的思路。使用入口在 实用功能 页面:把图片拖进去,生成预览,下载 .ans 文件,然后在支持 24-bit truecolor 的终端里运行 cat 文件名.ans。
一、为什么不是简单地把四个像素直接塞进一个字符
ANSI 终端的一个字符格通常只能同时设置一种前景色和一种背景色。Unicode 四象限字符能决定这个字符格里哪些象限显示前景色,哪些象限显示背景色,但它不能让左上、右上、左下、右下四个象限分别拥有四种独立颜色。
所以“四象限 full color”实际面对一个约束:每个 2 x 2 像素块最多只能压缩成两组颜色。转换器要做的不是假装没有损失,而是在这个约束下找到误差最小的两色表示。
二、四象限字符怎样编码 2 x 2 像素
一个字符格被看作四个位置:左上、右上、左下、右下。每个位置要么属于前景色,要么属于背景色,因此一共有 16 种组合。常见字符包括空格、上半块、下半块、左半块、右半块、对角块和整块,例如 ▘ ▝ ▀ ▖ ▌ ▞ ▛ ▗ ▚ ▐ ▜ ▄ ▙ ▟ █。
转换器先把原图按目标终端宽度缩放到一个采样网格。假设输出宽度是 180 个终端字符,那么采样网格的宽度就是 360 个像素;每两个横向采样点和两个纵向采样点合成一个终端字符。高度会按终端字体比例自动计算,默认把一个终端字符当作高宽比约 2:1。
三、怎样选择前景色、背景色和字符
对每一个 2 x 2 像素块,转换器枚举 16 种象限遮罩。某一种遮罩会把四个像素分成两组:前景组和背景组。前景组取平均 RGB 作为前景色,背景组取平均 RGB 作为背景色,然后计算四个原始像素与两组代表色之间的平方误差。
最终选择误差最小的一组:
- 枚举 16 种象限遮罩。
- 根据遮罩把四个采样像素分成前景组和背景组。
- 分别计算两组 RGB 平均值。
- 计算四个像素的重建误差。
- 输出误差最小的 Unicode 字符和 ANSI truecolor 前景/背景色。
它增加了左右方向的形状选择,但不保证任何图片的观感都更好或文件一定更大。字节数还取决于相邻颜色重复、字形 UTF-8 长度和重置次数;后面的数值对照固定同一个采样网格。
四、生成的 .ans 文件里是什么
.ans 文件本质上是普通 UTF-8 文本,只是里面包含 ANSI 转义序列。每个字符前会按需要写入类似 ESC[38;2;R;G;B;48;2;R;G;Bm 的控制码,分别设置前景色和背景色,然后写入一个四象限 Unicode 字符。每行结束时再用 ESC[0m 重置样式,避免背景色污染后面的命令行。
先确认实际终端配置支持 24-bit 颜色和对应块字形,再执行下面的命令。终端名称或操作系统名称本身不是兼容性验证:
cat image-quadrant-180x120-truecolor.ans
如果图片太宽,终端自动换行会破坏画面;这时要么把终端窗口拉宽,要么在转换时降低输出宽度。
五、为什么工具页不把图片上传到服务器
图片转换不需要服务器参与。浏览器已经能解码常见图片格式,并能通过 Canvas 读取缩放后的像素。把转换放在前端有三个好处:
- 隐私更简单:图片不离开用户设备。
- 存储压力更小:服务器不保存上传文件,也不需要定时清理原图。
- 反馈更快:拖入图片后可以直接预览、调整宽度并重新生成。
转换成功后安排约 30 分钟的清理计时器,撤销本页 Blob 链接并清空预览引用。后台标签页计时器可能延迟,这不是安全擦除保证;已下载文件不受影响。转换模块不上传图片,不等于整个站点没有日志、广告或其他网络请求。
六、实现中的几个边界
第一,浏览器只能处理它自己能解码的格式。PNG、JPEG、WebP、GIF、AVIF 和 SVG 通常没有问题,但 HEIC、损坏文件或引用外部资源的 SVG 可能失败。
第二,四象限字符并不等于四倍无损分辨率。每个字符依然只有前景和背景两种颜色;复杂纹理、噪声和高频细节会被压缩成两组代表色。
第三,终端字体会影响观感。理想字体应该让块元素紧密贴合,没有明显缝隙;如果看到格线或字符宽度不齐,需要换等宽字体或调整终端行距。
七、为什么这版比 half-block 更适合细节图
Half-block 的优势是每个字符保存上下两行颜色,色彩表达稳定;四象限版本的优势是同一个字符里可以出现左右方向的结构,因此头发、轮廓线、小面积高光和斜边会更清楚。对于动漫头像、图标和边缘明显的插画,四象限通常能在相同终端宽度下保留更多形状信息。
如果目标是最大兼容性,half-block 仍然是很好的选择;如果目标是在支持 Unicode 和 truecolor 的现代终端里尽可能精细,四象限加最小误差两色聚类更合适。
八、ANSI 转换策略对照表
| 策略 | 每个字符表达的信息 | 适合图片 | 主要限制 |
|---|---|---|---|
| Plain ASCII | 只用字符密度近似明暗,不保留真实颜色。 | 高对比黑白图、终端兼容性要求极高的场景。 | 色彩和细小边缘损失最大。 |
| Half-block truecolor | 一个字符保存上下两个采样点,各自一组颜色。 | 照片、渐变、对兼容性要求较高的彩色图。 | 横向结构弱,斜线和小轮廓容易变粗。 |
| Quadrant truecolor | 一个字符覆盖 2 x 2 采样点,用两色最小误差近似。 | 头像、图标、插画、轮廓明显的图。 | 每格仍只有前景/背景两色,不是四象限四种独立颜色。 |
| 完整图片输出 | 保留原始像素,由图片查看器渲染。 | 需要精确保真、打印或后期处理的场景。 | 不是通用字符加 SGR 的显示方式,依赖图像查看器或终端图形协议。 |
九、终端兼容性验证清单
生成 .ans 后,不建议只看浏览器预览就下结论。浏览器使用的是 Canvas 和页面字体,真实终端还会受字体、行高、色彩配置和自动换行影响。一个可复查的测试记录至少应包含终端名称、字体、字号、输出列宽、图片合成背景和实际运行命令。
| 检查项 | 通过标准 | 失败时优先调整 |
|---|---|---|
| Truecolor 支持 | 渐变色和照片色块没有退化成 256 色近似。 | 检查实际色块与配置;COLORTERM 只是提示,手工设置不会增加显示能力。 |
| Unicode block elements | 四象限字符无缺字、无明显缝隙。 | 更换等宽字体,降低行距或改用 half-block。 |
| 输出宽度 | 一行 ANSI 内容不会被终端自动折行。 | 降低转换宽度,或把终端窗口拉宽后重新测试。 |
| 透明背景 | 透明区域与终端背景接近,不产生奇怪边框。 | 选择接近主题背景的合成色后重新导出。 |
十、字符格比例如何进入采样公式
设字符格宽 w、高 h,高宽比 a=h/w。每个象限是 w/2 × h/2,高宽比仍是 a,不是 2a。a=2 时象限高是宽的两倍,旧文写成四倍有误。
显示图像的高宽比是 (rows/cols) × a。匹配源图 src_h/src_w,便得到下面的采样关系:
CELL_RATIO = 2.0
cols = target_cols
src_w, src_h = img.size
rows = max(1, int(cols * src_h / (src_w * CELL_RATIO) + 0.5))
img = img.resize((cols * 2, rows * 2))
800×600 图片、80 列、a=2,对应 80×30 字符、160×60 采样像素,显示高宽比为 30×2/80=0.75。不除以 a 才会产生两倍纵向拉伸。至少保留 1 行,避免超宽全景图得到 0 高度;极端比例仍受整数格子限制。网页另有 260 行、62000 格上限,触发限制时实际列数会减小。
用已知正方形网格校准实际终端的比例,比观察人像更可靠。2.0 是默认值,不是所有终端和字体的实测参数。
十一、256 色量化的两个反例
网页和本文离线版本都输出 truecolor,没有自动 256 色降级。不支持 24-bit SGR 的接收方可能忽略、映射到调色板或显示异常,行为取决于实现。COLORTERM、TERM 只是提示,手工设置它们不会增加终端能力。
下面是独立的调色板练习,查找默认 xterm 16–255 中 RGB 平方误差最小的颜色。0–15 系统色可能由主题配置;如果其余项也被重定义,默认表同样不适用。
LEVELS = (0, 95, 135, 175, 215, 255)
PALETTE = {
16 + 36*r + 6*g + b: (LEVELS[r], LEVELS[g], LEVELS[b])
for r in range(6) for g in range(6) for b in range(6)
}
PALETTE.update({232+i: (8+10*i,) * 3 for i in range(24)})
def rgb_to_256(r, g, b):
rgb = (r, g, b)
return min(PALETTE, key=lambda i:
sum((a-b)**2 for a, b in zip(rgb, PALETTE[i])))
旧版灰阶公式把 (17,17,17) 映射到索引 232,即 (8,8,8),SSE=243;实际最近项是 233,即 (18,18,18),SSE=3。(95,95,95) 在色立方索引 59 上精确匹配,不能只因它是灰色就强制走灰阶段。穷举 256 个灰度输入,新搜索在 67 个输入上严格降低误差,其余误差相同。
另一个旧文错误:v*5//256 在 v=255 时得到 4,不是 6。问题是它永远达不到第 5 档,不是上界越界。纯红、绿、蓝、白、黑的默认索引分别应是 196、46、21、231、16。
十二、可复核的四像素实验
位序固定为左上、右上、左下、右下,对应 bit 0、1、2、3。网页从 mask=0 到 15 枚举,按四个像素、三个通道的编码 RGB 平方误差求和。每组均值按 JavaScript Math.round 取整数;这不是线性光空间或感知色差。误差相同时保留较小 mask,因此互补遮罩交换前景/背景后虽等价,导出的字符仍有确定的选择。
| 2×2 输入(逐行) | 选择与四象限 SSE | 固定上下半块 SSE |
|---|---|---|
| 全红 | mask 0,空格,背景为红;0 | 0 |
| 红蓝 / 红蓝 | mask 5,▌;0 | 130052 |
| 红红 / 蓝蓝 | mask 3,▀;0 | 0 |
| 红蓝 / 蓝红 | mask 6,▞,前景为蓝;0 | 130052 |
| 红绿 / 蓝白 | mask 1,▘;130050 | 130052 |

上下半块这里特指同一个 2×2 网格上的 mask=3,不代表某个图片缩放器的质量。因为它本来就在 16 个候选里,最佳遮罩误差不可能比它更高;这不是对真实字体观感的保证。四种明显不同颜色仍留下 130050 的残差,展示了两色约束的失败边界。全红选空格不是丢图:有颜色的背景仍覆盖整个字符格。
下载并离线复现
下载 ANSI 四象限实验包,包含完整转换器、检查脚本、参考颜色与 .ans 文件、网页核心源码快照和运行记录。
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -r requirements.txt
python ansi_lab.py input.png output.ans --columns 80 --aspect 2 --background '#050505'
python audit_ansi.py --output my-audit
转换仅需 Python 和 Pillow;完整对照检查还需要 Node.js,可通过 –node 指定路径。默认不覆盖已有输出,确需替换时加 –force,输入与输出不得为同一文件。校验通过应显示 ANSI_AUDIT_PASSED;不要用 Python -O 关闭断言。
实测环境为 Python 3.13.9、Pillow 12.2.0。528 组像素、104 组尺寸与当前网页 JavaScript 核心一致;另对 13 组像素穷举每组每通道 0–255 的中心值,独立复核最小误差。检查还包括互补遮罩、全灰度输入、透明背景、EXIF 方向、坏文件、覆盖保护和每行颜色重置。
下载中的 reference/audit.json 记录源码哈希和结果。8 个纯黑格子分两行时只需 2 次颜色设置,而非 8 次;因此不能根据格子数精确推算文件大小。代码只输出本实验允许的 SGR、块字形和换行,不应把这个结论推广到来源未知的 .ans 文件。
离线版本用 Pillow 解码和 Lanczos 缩放,网页使用 Canvas;同一原图不承诺生成逐字节相同文件。这里只验证相同采样像素的核心算法,不包含真实终端字体、浏览器缩放器或所有图片格式的验证。离线版仅取静态首帧,不自带 SVG/HEIC 解码或 256 色文件输出。
协议与字形参考:XTerm 控制序列、Unicode Block Elements。图片处理边界见 Pillow Image,不要关闭大图解压保护来掩盖输入失败。
十三、使用建议
- 按目标终端实际列宽设置输出,并留出边框或复用器的空间。
- 如果终端会自动换行,就降低宽度,而不是继续提高精度。
- 透明 PNG 建议选择接近终端背景的合成色。
- 复杂照片会生成很大的 ANSI 文件,插画、图标和人物头像通常效果更好。
完整使用入口在 实用功能页面。转换器本身不依赖账号,也不需要后端保存文件;它只是把图片采样、两色近似和 ANSI 输出这三步放到了同一个浏览器界面里。
本文讲的整套遮罩搜索和真彩色编码,做成了可以直接用的网页工具:图片转 ANSI 字符画。拖入图片即可生成,转换全程在你自己的浏览器里完成,文件不上传服务器。