在文档、测试报告和新人培训材料里,z/VM 会话截图是说明操作步骤的重要素材。但并非每次都能随时登录主机执行命令,尤其在自动化流水线中,环境准备和权限申请会拖慢节奏。Node.js 具备成熟的画布与字体处理生态,可以模拟 3270 终端的字符缓冲区,并把每一屏内容转换成 PNG 图片。这个 Mock2Image 工具的思路并不复杂,但需要在字符宽度、字段属性和颜色映射上做足够细致的处理,否则输出会显得不像真实终端。下面从建模开始,逐步实现一个可用的生成器。

一、z/VM 终端模型与 3270 缓冲区设计
z/VM 的交互会话通常基于 IBM 3270 终端协议。屏幕被划分为固定行列的字符单元,常见规格为 24 行 80 列,也有 32 行、43 行等变体。每个单元不仅保存字符编码,还携带字段属性,用于控制高亮、反色、下划线和保护状态。在 Node.js 中模拟时,最简单的方式是使用二维数组,外层数组表示行,内层数组表示列,每个元素存储字符和样式对象。这样既方便遍历渲染,也能灵活扩展颜色和字体。与真实 3270 不同,我们不需要处理完整数据流,只需根据业务需要填充缓冲区,适合生成静态截图或测试序列。
字段属性在 3270 中位于字段起始位置,占用一个屏幕位置,不显示字符但对后续字符生效。例如保护字段用于展示只读文本,高亮字段常用于提示用户输入。在 Mock2Image 中可以通过对象属性 fg、bg、bold、underline 等标记,渲染时逐单元判断。对于连续文本,设计一个 putText 辅助函数可以减少重复劳动,它会按顺序写入字符,并支持指定前景色和背景色。这种缓冲区模型不仅是渲染基础,也是后续模拟命令响应的核心数据结构。
二、构建渲染管线与字体选择
从缓冲区到 PNG 图片的渲染管线包含三个步骤:确定单元格尺寸和边距、创建画布、逐单元绘制字符。Node.js 生态中常用的 node-canvas 提供了类浏览器的 Canvas API,支持 TrueType 字体和 PNG 输出。安装依赖后,创建一个宽为左右边距加上列数乘以单元格宽度、高为上下边距加上行数乘以单元格高度的画布。背景默认为黑色,前景默认为绿色,模仿经典 3270 终端。使用 ctx.fillText 绘制字符时,需要设置 textBaseline 为 middle 并手动计算垂直位置,否则字符会偏上。
字体选择对还原度影响很大。理想情况下使用 IBM 3270 字体,这是一款开源等宽字体,字形接近真实终端。如果环境未安装该字体,可以回退到常用等宽字体如 Consolas 或 Courier New,但注意单元格宽度要根据字体实际字形宽度调整。一般 9 像素宽、18 像素高配合 14px 字号比较合适。如果字体渲染出现模糊,可以在创建画布时按两倍尺寸绘制然后缩放,但会增加内存占用。考虑到输出用于文档,直接按最终尺寸生成即可。
三、实现核心 bufferToPng 函数
核心函数接收一个屏幕缓冲区数组和可选配置,返回 PNG Buffer。先根据行列数、单元格宽高和边距计算画布总尺寸,然后填充背景色。遍历每个单元格时,如果该位置为 null 就跳过,保持背景。否则先判断是否有背景色,有则填充该单元格矩形,再设置前景色绘制字符。字符绘制使用 fillText,传入字符和坐标。为了让字符在单元格内水平方向也居中,可以再加上半个单元格宽度减去字符宽度的一半,但等宽字体中简单使用左对齐加 1 像素偏移通常即可。
为了提高扩展性,可以将颜色映射和字体设置提取为配置项。例如默认前景色 #00ff00、背景色 #000000,用户也可以传入高亮黄色、输入字段白色等。更重要的是,如果后续要支持 3270 扩展属性,可以在单元格对象中加入 reverse 字段,渲染时交换前景色和背景色。这个函数保持纯同步,方便在脚本或测试中直接调用。代码结构清晰后,生成一张图片的耗时在几十毫秒内,完全满足批量生成需求。
const { createCanvas } = require('canvas');
const COLS = 80;
const ROWS = 24;
const CELL_WIDTH = 9;
const CELL_HEIGHT = 18;
const MARGIN = 10;
function bufferToPng(screen, options = {}) {
const width = MARGIN * 2 + COLS * CELL_WIDTH;
const height = MARGIN * 2 + ROWS * CELL_HEIGHT;
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
ctx.fillStyle = options.background || '#000000';
ctx.fillRect(0, 0, width, height);
ctx.font = options.font || '14px "IBM 3270", monospace';
ctx.textBaseline = 'middle';
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const cell = screen[row] && screen[row][col];
if (!cell) continue;
const x = MARGIN + col * CELL_WIDTH;
const y = MARGIN + row * CELL_HEIGHT + CELL_HEIGHT / 2;
if (cell.bg) {
ctx.fillStyle = cell.bg;
ctx.fillRect(MARGIN + col * CELL_WIDTH, MARGIN + row * CELL_HEIGHT, CELL_WIDTH, CELL_HEIGHT);
}
ctx.fillStyle = cell.fg || options.foreground || '#00ff00';
ctx.fillText(cell.char || ' ', x, y);
}
}
return canvas.toBuffer('image/png');
}
module.exports = { bufferToPng };
四、模拟 z/VM 登录画面并生成图片
以最常见的 z/VM 登录屏为例,顶部通常显示 VM READ 或 z/VM ONLINE,中间有 USERID 和 PASSWORD 输入字段,底部显示版本信息。我们可以先创建一个 24 行 80 列的空屏,然后使用 putText 填充文本。对于输入字段,使用下划线字符占位,并设置浅色背景表示可编辑区域。调用 bufferToPng 后写入文件,即可得到一张与真实会话高度相似的 PNG 图片。整个过程不需要连接任何主机,也不依赖外部服务。
这种模拟不仅用于静态截图,还可以扩展为命令驱动。例如定义一个 sendCommand 函数,根据命令名在缓冲区中追加输出行,模拟 CP 命令或 CMS 命令的响应。每次响应后重新生成图片,就能得到一系列操作步骤截图。对于自动化测试,可以将这些图片与基线图片做像素级对比,验证界面渲染是否发生变化。相比真实主机截图,Mock2Image 的输出完全确定,不会受网络延迟或用户输入影响。
const { bufferToPng } = require('./mock2image');
const fs = require('fs');
const COLS = 80;
const ROWS = 24;
const screen = Array.from({ length: ROWS }, () => Array(COLS).fill(null));
function putText(row, col, text, fg = '#00ff00', bg = null) {
for (let i = 0; i < text.length; i++) {
screen[row][col + i] = { char: text[i], fg, bg };
}
}
putText(1, 26, 'z/VM ONLINE', '#ffffff', '#003300');
putText(3, 10, 'VM READ', '#00ffff');
putText(5, 2, 'LOGON', '#ffff00');
putText(5, 10, 'USERID', '#00ff00', '#111111');
putText(7, 2, 'PASSWORD', '#ffff00');
putText(7, 10, '________', '#00ff00', '#111111');
putText(12, 2, 'COMMAND ==>', '#00ff00');
putText(20, 2, 'z/VM V7 R2.0', '#888888');
const png = bufferToPng(screen, { font: '14px "IBM 3270", monospace' });
fs.writeFileSync('zvm-login.png', png);
console.log('PNG generated');
五、常见问题与优化建议
一个常见问题是字体缺失导致字符间距不一致,尤其在某些 Linux 服务器上。解决办法是在部署时安装 IBM 3270 字体,或使用 node-canvas 的 registerFont 方法注册本地字体文件。另一个问题是对齐偏移,因为不同字符的字形宽度可能不同,建议固定单元格宽度并忽略字符实际宽度,这样整体更整齐。如果发现生成图片过大,可以调整单元格尺寸或使用 JPEG 格式降低体积,但 PNG 更适合文本截图。
对于需要多页输出的场景,可以维护一个屏幕数组列表,每个元素代表一屏,循环生成多张图片并打包。颜色映射也可以从简单的十六进制字符串升级为调色板对象,方便统一修改。未来还可以结合 SVG 输出,通过 sharp 库转换为 WebP 或 AVIF,进一步减小体积。只要缓冲区模型保持稳定,后续扩展都不会影响核心渲染逻辑。
Node.jsz/VMMock2Image修改时间:2026-10-03 07:22:09