PAYATHON 2026

如何通过 Epson TM-T88V POS printer 打印 Arecibo message

数据小许

结论

不必把每个 dot 当成独立数字发送。先将 23×73 的 0/1 矩阵按比例放大,再把每行的黑白点按照 8 个 dots 打包为 1 byte,最后通过 ESC/POS 光栅位图命令发送。

使用 GS v 0(Print raster bit image)时,命令格式如下:

GS v 0 m xL xH yL yH d1...dk

参数对应的图像尺寸和数据格式如下:

  • 图像宽度为 23 × scale dots
  • 图像高度为 73 × scale dots
  • scale=8 时,图像尺寸为 184×584 dots
  • scale=16 时,图像尺寸为 368×1168 dots
  • 每个 byte 的最高位对应最左侧 dot
  • 1 表示黑点,0 表示白点

需要注意,GS v 0 命令中的 xL/xH 表示每行的 byte 数,而不是 dot 数。不同 ESC/POS 图像命令对参数的定义并不完全相同,不能因为都使用了 nL + nH × 256,就把它们混用。

nL + nH × 256 是什么意思

这是将大于 255 的整数拆成两个 byte 的通用方式:

value = nL + nH × 256
nL = value % 256
nH = value // 256

例如,某条命令明确要求用两个 byte 表示水平方向的 dot 数时,可以这样计算。

放大 8 倍

width = 23 × 8 = 184 dots

nL = 184 % 256 = 184
nH = 184 // 256 = 0

需要发送:

184, 0

放大 16 倍

width = 23 × 16 = 368 dots

nL = 368 % 256 = 112
nH = 368 // 256 = 1

因为:

112 + 1 × 256 = 368

所以需要发送:

112, 1

本文使用的 GS v 0 命令有所不同,它的水平参数是每行的 byte 数:

bytes_per_row = ceil(width_dots / 8)

实际参数如下:

比例宽度高度每行 bytesxL,xHyL,yH
8×8184 dots584 dots2323,072,2
16×16368 dots1168 dots4646,0144,4

其中:

584 = 72 + 2 × 256
1168 = 144 + 4 × 256

位图数据如何打包

假设放大后,某一行开头的 8 个 dots 是:

00101000

将其视为一个二进制 byte:

00101000₂ = 40

此时应向打印机发送一个值为 40 的 byte。

各位的顺序如下:

bit 7  bit 6  bit 5  bit 4  bit 3  bit 2  bit 1  bit 0
左侧                                                    右侧

对应的 Python 写法是:

byte_value |= 1 << (7 - bit_position)

将每个原始元素放大为 8×8 时:

  • 水平方向将该元素重复 8 次
  • 垂直方向将整行重复 8 次

放大为 16×16 时采用同样的方法。

可直接使用的 Python 示例

下面的代码使用 GS v 0,命令字节为:

1D 76 30 m xL xH yL yH

其中,m=0 表示按正常尺寸打印。图像已经由程序完成放大,因此不再使用打印机内置的倍宽或倍高模式。

# -*- coding: utf-8 -*-
import socket


def split_word(value):
    """将非负整数拆成低位 byte 和高位 byte。"""
    if not 0 <= value <= 0xFFFF:
        raise ValueError("value must fit in two bytes")
    return value & 0xFF, (value >> 8) & 0xFF


def parse_arecibo(text):
    rows = [
        line.strip()
        for line in text.splitlines()
        if line.strip()
    ]

    if not rows:
        raise ValueError("No bitmap data found")

    width = len(rows[0])

    if any(len(row) != width for row in rows):
        raise ValueError("All rows must have the same width")

    if any(ch not in "01" for row in rows for ch in row):
        raise ValueError("Bitmap may contain only '0' and '1'")

    return rows


def expand_and_pack(rows, scale=8):
    """
    将 0/1 矩阵放大,并按 GS v 0 所需格式打包。

    每个输出 byte 包含从左到右的 8 个 dots:
    bit 7 是最左侧 dot,bit 0 是最右侧 dot。
    """
    if scale <= 0:
        raise ValueError("scale must be greater than zero")

    source_width = len(rows[0])
    source_height = len(rows)

    width_dots = source_width * scale
    height_dots = source_height * scale
    bytes_per_row = (width_dots + 7) // 8

    image_data = bytearray()

    for source_row in rows:
        expanded_bits = []

        for pixel in source_row:
            bit = 1 if pixel == "1" else 0
            expanded_bits.extend([bit] * scale)

        packed_row = bytearray(bytes_per_row)

        for x, bit in enumerate(expanded_bits):
            if bit:
                byte_index = x // 8
                bit_index = 7 - (x % 8)
                packed_row[byte_index] |= 1 << bit_index

        # 垂直方向重复 scale 次
        for _ in range(scale):
            image_data.extend(packed_row)

    return width_dots, height_dots, bytes_per_row, bytes(image_data)


def make_raster_command(rows, scale=8):
    width_dots, height_dots, bytes_per_row, image_data = (
        expand_and_pack(rows, scale)
    )

    xL, xH = split_word(bytes_per_row)
    yL, yH = split_word(height_dots)

    # GS v 0 m xL xH yL yH
    header = bytes([
        0x1D, 0x76, 0x30, 0x00,
        xL, xH,
        yL, yH,
    ])

    expected_size = bytes_per_row * height_dots
    if len(image_data) != expected_size:
        raise RuntimeError("Unexpected raster data size")

    return header + image_data


def print_arecibo(host, bitmap_text, scale=8, port=9100):
    rows = parse_arecibo(bitmap_text)

    if len(rows) != 73 or len(rows[0]) != 23:
        raise ValueError(
            f"Expected a 23x73 bitmap, got "
            f"{len(rows[0])}x{len(rows)}"
        )

    raster = make_raster_command(rows, scale)

    with socket.create_connection((host, port), timeout=10) as sock:
        # ESC @:初始化打印机
        sock.sendall(b"\x1b\x40")

        # ESC a 1:居中,可按需要删除
        sock.sendall(b"\x1b\x61\x01")

        # 发送完整的 GS v 0 命令及图像数据
        sock.sendall(raster)

        # 恢复左对齐,并走纸
        sock.sendall(b"\x1b\x61\x00")
        sock.sendall(b"\n\n\n")

        # GS V 0:切纸
        sock.sendall(b"\x1d\x56\x00")


ARECIBO = """\
00000010101010000000000
00101000001010000000100
10001000100010010110010
10101010101010100100100
00000000000000000000000
00000000000011000000000
00000000001101000000000
00000000001101000000000
00000000010101000000000
00000000011111000000000
00000000000000000000000
11000011100011000011000
10000000000000110010000
11010001100011000011010
11111011111011111011111
"""

# 使用时必须将 ARECIBO 补全为全部 73 行:
# print_arecibo("192.168.1.15", ARECIBO, scale=8)

题目只展示了部分数据,因此示例中的 ARECIBO 并不完整。实际打印前必须补全全部 73 行,否则尺寸校验会报错。

8×8 模式的实际数据量

原始尺寸为:

23 × 73 elements

放大后:

width_dots  = 23 × 8 = 184
height_dots = 73 × 8 = 584

每行的 byte 数为:

184 / 8 = 23 bytes

图像数据总长度为:

23 × 584 = 13432 bytes

命令头为:

1D 76 30 00 17 00 48 02

对应的十进制值为:

29, 118, 48, 0, 23, 0, 72, 2

其中:

  • 0x17 0x00 表示每行 23 bytes
  • 0x48 0x02 表示高度为 584 dots
  • 命令头之后紧跟 13432 bytes 的图像数据

16×16 模式的实际数据量

放大后:

width_dots  = 23 × 16 = 368
height_dots = 73 × 16 = 1168

每行的 byte 数为:

368 / 8 = 46 bytes

图像数据总长度为:

46 × 1168 = 53728 bytes

命令头为:

1D 76 30 00 2E 00 90 04

对应的十进制值为:

29, 118, 48, 0, 46, 0, 144, 4

368 dots 通常能够容纳在 TM-T88V 的打印宽度内,但仍需以当前纸宽、打印区域设置及对应固件的 ESC/POS 技术手册为准。

原程序中需要修正的问题

原发送函数使用了:

struct.pack('h', x)

h 表示一个通常占 2 bytes 的有符号短整数,并且默认采用本机字节序。发送 ESC/POS 指令时,每个指令值通常必须是一个准确的 byte。例如,27 应发送为:

1B

而不是:

1B 00

原程序还使用了:

mySocket.send(data, 1)

这里的第二个参数是 socket flags,并不表示“发送一个 byte”。同时,send() 也不能保证一次发送完整的缓冲区。

应直接使用:

sock.sendall(bytes([27, 33, 48]))

例如:

def send_instructions(sock, values):
    sock.sendall(bytes(values))

发送文本时可以继续使用:

def send_text(sock, text):
    sock.sendall(text.encode("utf-8"))

打印机能否直接解释 UTF-8,取决于它的字符集配置,但这不影响前面的二进制位图发送。

注意事项

  • 确认当前 TM-T88V 固件支持所使用的 GS v 0 命令。如果手册描述的是 ESC * 或 GS ( L,参数含义和数据排列方式会有所不同。
  • 使用 sendall() 发送完整的命令和数据,避免 TCP 只发送部分缓冲区。
  • 图像数据不能经过 UTF-8 编码,也不能转换为十进制数字字符串,必须保持为原始 bytes。
  • 如果位图每行不足 8 dots,应在右侧补白点。本例放大后的宽度 184 和 368 都能被 8 整除,不需要填充。
  • 如果打印结果出现黑白反转,可以将打包条件从 if bit: 改为对白点置位,也可以先确认所用命令是否规定 1 代表打印 dot。
  • 如果一次发送较大的 16×16 图像时发生中断,应先检查打印机接收缓冲区、接口超时设置和命令允许的最大光栅高度。除非能在各段之间正确处理走纸和连续打印位置,否则不要擅自拆分图像。

备注:内容仅供参考。