GBK点阵字模索引算法与嵌入式显示打印实战指南
简介一套面向嵌入式显示与打印开发场景的GBK汉字点阵字模库覆盖GBK1.0标准中全部22046个汉字提供16×16与24×24两种规格的BIN字库文件可直接用于LCD液晶显示、微型打印机、POS机等设备。资源内整理了按分区与按编码两种顺序排列的完整GBK编码表并附带牧码字模、PCtoLCD2002等多款字模提取与转换工具方便生成或调整字模数据。另有GBK与BIG5、Unicode互转的C语言函数及头文件配合索引获取函数可帮助开发者将字库无缝集成到打印机或显示项目里。整包共21个文件以txt编码表、bin字库、exe工具为主辅以C/H源码、配置和说明文档压缩后仅3.41MB内容紧凑且实用。目前已有1667人学习下载适合单片机工程师、打印机固件开发者以及需要处理汉字字模的嵌入式爱好者使用省去长时间寻找专业字库的精力。1. GBK点阵字模在显示与打印里到底是干什么的做嵌入式设备或打印固件的人大概率接过这种活儿没有操作系统可调用字体渲染引擎屏幕要显示汉字、打印头要出字只能从点阵字库文件里按编码把字形抠出来。GBK汉字16x16和24x24点阵字模就是这么用的每个汉字是一组固定长度的字节显示时推给屏幕打印时推给打印头取模一次永远不变快且确定。开发者第一次上手常在偏移计算上翻车——换字库规格就错位遇扩展区汉字直接乱码。下面把GBK编码到字模偏移的索引算法讲透给一套能直接抄的提取工具再排一遍常见坑适合正在做嵌入式显示、打印机驱动、工控屏或老系统汉字移植的人。2. 从GBK编码到字模偏移索引算法拆到公式级2.1 先看懂GBK双字节结构和区位码GBK是双字节编码方案每个汉字对应两个字节。第一字节范围0x81到0xFE第二字节范围0x40到0xFE其中0x7F被剔除不在任何汉字编码里出现。这个规则意味着拿到汉字的内存字节之后第一字节减0xA0可以得到区号第二字节减0xA0可以得到位号——前提是这个字落在GB2312规定的汉字区内。GB2312把6763个常用汉字放在16区到87区每区固定94个位。区号加上0xA0得到第一字节位号加上0xA0得到第二字节。比如“啊”字在16区第1位GBK编码就是0xB0A1这是整套索引算法里最常用的一个基准点。反过来说遇到任意一个汉字把两个编码字节分别减0xA0就得到它所在的区和位后续算偏移全靠这两个数字。这里有个容易忽略的点GB2312汉字区的区号从16开始不是1。前15区是符号和西文字母点阵字库文件未必把它们包含在内。所以后续公式里出现的起始区号不是拍脑袋定的而是字库文件设计时决定的排布起点动手前必须确认。提示判断一个汉字是否属于GB2312汉字区只看两个字节的数值区间即可第一字节0xB0到0xF7第二字节0xA1到0xFE两个条件同时满足才算。还要明确一件事GBK和GB2312不是一回事。GBK完全兼容GB2312又向后新增了大量汉字和符号总量超过两万个。索引算法要分两层看GB2312汉字区有现成的线性公式扩展区需要单独想办法。写代码的第一步不是急着算偏移而是判断当前汉字在不在GB2312汉字区里。2.2 16x16字模的偏移公式32字节对齐的线性定位16x16点阵字模的存储规则很直白一个汉字16行每行16个点每8个点打包成一个字节每行2字节总共32字节。绝大多数按GB2312区位码排列的字库文件就是把这32字节按照区号、位号的顺序连续写进文件。偏移计算可以写成一行offset ((区号 - 16) * 94 (位号 - 1)) * 32把区号位号换成原始编码字节等价形式是offset ((b1 - 0xB0) * 94 (b2 - 0xA1)) * 32公式里94是每区固定字位数32是单个汉字占用的字节数。举例验证“啊”字的b1等于0xB0b2等于0xA1括号里全是0偏移为0它正好是16区第1位应该排在汉字区第一个字。如果从文件偏移0读32字节并还原成16行点阵能看到完整的“啊”字字形说明文件起点和公式假设一致。设计这个公式时有一个默认前提字库文件从16区开始存放不包含前15区。很多HZK16这类按GB2312区位码排列的字库文件确实如此但也有从01区开始包含符号区的版本。判断方法很简单看文件大小从16区开始的版本按16区到87区共72区、每区94字位计算大约是72乘94乘32等于216576字节包含符号区的版本会多出15个区的数据。拿到文件先算一遍大小再用“啊”字验证能避开后面整片错位的尴尬。常用参数如下表动手前先把这些值确认好再进代码。参数16x16取值说明单字字节数3216行乘2字节每行字节数2每行16个点总行数16纵向16行每区字数94GB2312固定值起始区号16常见汉字区排布16x16通常是显示场景的首选占空间小、读取快一个汉字只有32字节一次读入刚好填一行缓存。缺点是放大后锯齿明显打印场景如果对字形要求稍高一般直接用24x24。2.3 24x24字模的偏移差异与GBK扩展区的索引边界24x24的索引本质和16x16一样只是每个汉字变成24行、每行24个点每行3字节单字72字节。偏移公式的结构不变末尾乘数从32变成72offset ((b1 - 0xB0) * 94 (b2 - 0xA1)) * 72这里藏着一个常见变体部分字库为了实现内存对齐把24x24的每行存成4字节高24位用低8位补零单字变成96字节。如果按72字节去读这种字库读出来的结果会逐行错位第一行正确从第二行开始混入下一行的数据。判断方法同样靠文件大小复核或者用第3章的预览工具直接看。GBK扩展区是另一个坑。GBK编码空间里除了GB2312原有的6763个汉字还塞进了大量生僻字和异体字。这些扩展汉字的第一字节常落在0x81到0xA0或0xF8到0xFE区间第二字节也可能落在0xA1之外。这时候区位公式彻底失灵减出来的区号可能是负数也可能是无效区读出来的数据会指到别的字或者越界。处理扩展区汉字常见做法有三条。第一条换用完整GBK顺序排列的点阵字库这类文件内部不按区号位号排而是按GBK编码从小到大排索引时用二分查找或直接建立编码到偏移的映射表第二条只承诺支持GB2312的6763个常用字扩展汉字取模时返回全零或打印占位黑框这在不少工控屏项目里可接受第三条程序启动时扫描整个字库文件把每个汉字编码和偏移建一张哈希表启动增加几十毫秒换来任意GBK汉字的O(1)定位。我实际做项目时会先写一个最基础的区域判断函数把不在GB2312汉字区的字符单独拦截再决定走哪条路def is_gb2312_hanzi(b1, b2): # 判断两个字节是否落在GB2312汉字区16区到87区每区94位 return 0xB0 b1 0xF7 and 0xA1 b2 0xFE这个函数的逻辑很直接b1和b2分别是GBK编码的第一、第二字节。返回值是True说明能用线性公式算偏移是False说明落在扩展区必须走降级分支。很多翻车现场就是因为跳过了这一步硬把扩展区的字节套区位公式取出了一段不属于它的数据。打印场景建议直接用24x24或更大规格。扩展区的处理逻辑和显示一致但要额外确认一件事字库文件是否包含对应规格的完整GBK版本否则打印到生僻字时会变成空白格。3. 点阵字模的字节排布取到偏移后怎样还原出字形3.1 行优先、高位在前一个汉字的点阵在文件里的真实样子偏移算对只是第一步。接下来要把32字节或72字节还原成可见字形。规则是行优先从上到下逐行存放每行内部的点从左到右先拼装成字节。以16x16为例第1、2字节是第1行第3、4字节是第2行依此类推第31、32字节是第16行。高位在前是另一个关键约定。一行16个点第一个字节的最高位代表左边第1个点最低位代表左边第8个点第二个字节的最高位代表左边第9个点最低位代表左边第16个点。如果字库采用低位在左的取模方向所有字形都会左右镜像这个放到避坑章节细讲。写代码时不建议把行列计算散落在各处。我一般会定义一个取像素的函数所有渲染逻辑都走它def get_pixel(buf, offset, row, col, bpl): 从字模字节中取第row行第col列的像素返回0或1 offset: 当前汉字的起始字节偏移 bpl: 每行字节数16x16为224x24为3 byte_pos offset row * bpl col // 8 bit_pos 7 - (col % 8) return (buf[byte_pos] bit_pos) 1这段代码的核心是位定位col // 8算出本行第几个字节col % 8算出字节内第几位7减模数是因为字节最高位对应col等于0的位置。拿一个笔画特征明显的字比如“口”或“十”做预览比对着字节数值猜要快得多第4章的工具就是做这个的。3.2 16x16与24x24的逐行读取差异读取一行像素时需要两个参数每行字节数bpl和单字字节数bpc。16x16分别是2和3224x24分别是3和72。24x24如果遇到每行4字节对齐的字库bpl是4单字字节数是96读取时每行取前3字节但换行要按4字节跳。24x24比16x16多出来的复杂度主要在打印端。24行乘3字节刚好72字节没有跨字节对齐问题。但按4字节对齐的版本每一行实际占4字节绘制时只取前3字节跳行时不能漏掉补位的第4字节。很多人在这里出错是因为看到字库标注“24x24”默认了72字节没先核对文件大小。判断字库属于哪种排布有一个土办法先取一个“口”字预览如果第一行对、第二行开始越来越乱基本就是对齐参数错了。逐行错乱和整体错位的区别很明显前者是行与行之间数据串位形状呈阶梯状斜线后者是整字偏移。这个经验能省下大量瞎试参数的时间。3.3 显示与打印场景下的方向与分辨率兼容显示场景下字模按行优先直接写入显存通常就是正确方向。打印场景则要小心针式打印机的打印头从纸的一端扫到另一端直接送显示用的位图打出来往往左右颠倒。常见做法是在驱动里做一次水平镜像或者准备一套打印专用字模。写固件时我会把方向做成配置项不在代码里写死。分辨率是另一个容易被忽略的算术题。16x16字模在300DPI打印分辨率下物理尺寸约等于16除300乘25.4只有1.35毫米看小标签勉强够用单据正文就偏小了。24x24对应约2.03毫米适合打印小票和铭牌。这段换算值得记下来毫米等于点阵边除以DPI再乘25.4。如果目标设备分辨率不同用同样公式估算显示效果能避免打完一批样品才发现字太小。方向兼容还牵涉取模方向。有的字库横向取模有的是纵向取模同一组字节按不同方式解释得到的是完全不同的字形。做镜像时下面这个函数可以按整行翻转位序def reverse_bits(byte): rev 0 for j in range(8): rev | ((byte j) 1) (7 - j) return rev def mirror_horizontal(row_bytes, bpl): 整行左右镜像字节顺序反转字节内部位序也反转 row_bytes: 当前行的原始字节列表 bpl: 每行字节数 mirrored bytearray(bpl) for i in range(bpl): mirrored[i] reverse_bits(row_bytes[bpl - 1 - i]) return mirrored这个函数的原理是一行里的点从左到右占多个字节镜像后原本最右边的字节跑到最左边同时字节内的位序也要倒过来。只反转字节顺序或者只反转字节内位序都会得到错误的镜像结果。4. 手写一套字模提取与预览工具从编码到点阵图全流程4.1 最小提取脚本定位编码、计算偏移、按位还原先给一个最简可运行的Python脚本接收字库文件路径、单个汉字和规格控制台直接输出点阵文本预览。有了它换任何一种字库都可以在几秒内验证索引假设。# -*- coding: utf-8 -*- import sys def load_font(path): 读取整个字库文件到内存后续只通过偏移取字节 with open(path, rb) as f: return f.read() def offset_gb2312(b1, b2, bytes_per_char, start_qu16): 按GB2312区位码计算字模偏移 b1: GBK编码第一字节 b2: GBK编码第二字节 bytes_per_char: 单字字节数16x16为3224x24为72 start_qu: 字库起始区号多数汉字字库从16区开始 qu b1 - 0xA0 # 区号从1开始 wei b2 - 0xA0 # 位号从1开始 if qu start_qu: raise ValueError(字符在起始区之前可能不是汉字) if wei 1 or wei 94: raise ValueError(位号越界) return ((qu - start_qu) * 94 (wei - 1)) * bytes_per_char def get_pixel(buf, offset, row, col, bpl): 取第row行第col列的像素值返回0或1 bpl: 每行字节数16x16为224x24为3 byte_pos offset row * bpl col // 8 bit_pos 7 - (col % 8) return (buf[byte_pos] bit_pos) 1 def render_text(buf, ch, size): raw ch.encode(gbk) if len(raw) ! 2: raise ValueError(字符的GBK编码不是双字节) b1, b2 raw[0], raw[1] bpl size // 8 bytes_per_char bpl * size off offset_gb2312(b1, b2, bytes_per_char) lines [] for row in range(size): line for col in range(size): line # if get_pixel(buf, off, row, col, bpl) else . lines.append(line) return \n.join(lines), off if __name__ __main__: if len(sys.argv) ! 4: print(用法: python font_preview.py 字库文件 汉字 16|24) sys.exit(1) font_path, ch, size_str sys.argv[1], sys.argv[2], sys.argv[3] size int(size_str) data load_font(font_path) try: preview, off render_text(data, ch, size) print(f字符: {ch} 偏移: {off}) print(preview) except ValueError as e: print(f取模失败: {e})代码的关键点是把偏移、行、列、每行字节数全部参数化16x16和24x24共用同一套逻辑。bpl等于size整除8对16得到2对24得到3bytes_per_char等于bpl乘以size分别得到32和72。预览输出用#和.表示笔画与空白字形明显不对时优先怀疑偏移或方向不用等到烧进设备才发现。几个重要参数要说明start_qu默认16对应不含符号区的字库排布如果文件包含符号区要改成1否则所有字符偏移会差15个区的数据。另一个是sys.argv的第三个参数16或24传错规格会读到错误长度的字模。4.2 不依赖第三方库的BMP输出让字形直接可见控制台预览够用但信息有限。我习惯再补一个BMP导出直接把点阵渲染成位图文件方便在像素级别检查字形边缘。用标准库手写24位BMP文件头不依赖Pillow在内网开发机或嵌入式工具链里都能跑。import struct def write_bmp_24(path, width, height, bitmap): bitmap: 二维列表每行是0到255的灰度值0为黑 写的是24位真彩BMP文件头完全手写 row_bytes (width * 3 3) // 4 * 4 # BMP每行按4字节对齐 data_size row_bytes * height file_size 54 data_size with open(path, wb) as fp: fp.write(bBM) fp.write(struct.pack(I, file_size)) fp.write(struct.pack(I, 0)) fp.write(struct.pack(I, 54)) fp.write(struct.pack(I, 40)) fp.write(struct.pack(i, width)) fp.write(struct.pack(i, height)) fp.write(struct.pack(H, 1)) fp.write(struct.pack(H, 24)) fp.write(struct.pack(I, 0)) fp.write(struct.pack(I, data_size)) fp.write(struct.pack(i, 2835)) fp.write(struct.pack(i, 2835)) fp.write(struct.pack(I, 0)) fp.write(struct.pack(I, 0)) # BMP数据从最后一行往上写所以这里把二维列表倒序 for row in reversed(bitmap): for pixel in row: fp.write(bytes([pixel, pixel, pixel])) fp.write(b\x00 * (row_bytes - width * 3))BMP头由文件头和信息头组成关键字段是文件总大小、位深24、每行4字节对齐、行序自底向上。把点阵转成灰度二维列表的逻辑在前面基础上加一层映射def font_to_bitmap(data, ch, size): raw ch.encode(gbk) if len(raw) ! 2: raise ValueError(非GBK双字节字符) b1, b2 raw[0], raw[1] bpl size // 8 off offset_gb2312(b1, b2, bpl * size) bitmap [] for row in range(size): line [] for col in range(size): pixel 0 if get_pixel(data, off, row, col, bpl) else 255 line.append(pixel) bitmap.append(line) return bitmap这里做了一个颜色映射get_pixel返回1表示笔画点写入BMP时置0黑色背景返回255白色。如果导出的BMP里字形反白把0和255的映射对调就行。2835这个DPI值只是头部字段不影响像素数据。4.3 整段文本批量导出字距行距与换行处理实际项目里不会一个字一个字取模通常是一整段文本。常见做法是按行切分逐字取模字与字之间留固定字距行与行之间留行距最后合成一张大位图。下面脚本把多行文本渲染到一张24位BMP里def render_text_bitmap(data, text, size, spacing1, line_gap2): 渲染整段文本到灰度位图 spacing: 字间距像素为单位 line_gap: 行间距像素为单位 lines text.split(\n) bpl size // 8 line_h size line_gap width max(len(l) for l in lines) * (size spacing) spacing height len(lines) * line_h line_gap bitmap [[255] * width for _ in range(height)] for li, line in enumerate(lines): for ci, ch in enumerate(line): raw ch.encode(gbk) if len(raw) ! 2: continue off offset_gb2312(raw[0], raw[1], bpl * size) base_x spacing ci * (size spacing) base_y line_gap li * line_h for r in range(size): for c in range(size): if get_pixel(data, off, r, c, bpl): bitmap[base_y r][base_x c] 0 return bitmap参数spacing和line_gap分别控制字间距和行间距单位是像素。width按最长一行字符数计算避免右侧截断。遇到非汉字比如ASCII字母或标点这里直接跳过字符位置会留白。如果字库文件包含符号区可以把start_qu调整为对应值再把符号编码映射一并做进去否则需要单独加载一套ASCII字模。5. 避坑指南扩展区错位、镜像、打印发虚四类典型翻车记录5.1 坑扩展区汉字取出乱码或黑块现象代码在GB2312常用字上一切正常遇到生僻字比如人名字输出全是乱码、黑块或形状完全错乱的图形。原因GBK扩展区汉字不在GB2312的16到87区范围内区位公式算出的偏移指向了别的字符或文件范围外。有些字的b1落在0x81到0xA0区间减0xA0直接得到负数Python里会通过负索引读到错误数据。解决取模前先用is_gb2312_hanzi判断不在区间内的走独立分支。如果是完整GBK字库按编码顺序二分或者启动时建映射表如果不是直接输出占位黑框同时在日志里记录未覆盖字符方便后期补字库。5.2 坑字模左右镜像或上下颠倒现象预览时字形左右颠倒或者头脚倒置。单独看一个字节里的位是正常的但整行组合起来方向不对。原因字库取模方向和代码解释方向不一致。高位在左、每行从左到右是最常见约定但有些历史字库是低位在左有些按纵向取模。这类问题最难发现因为偏移公式完全正确数据也是自己的只有字形方向不对。解决先拿“口”或“十”这种对称特征明显的字预览确认是水平镜像还是垂直翻转。水平镜像用3.3节的mirror_horizontal逐行翻转垂直翻转就反转行的遍历顺序。改完重新取模对比不要靠猜。5.3 坑24x24字模逐行错位越往下越乱现象16x16完全正常切换到24x24后字形错乱第一行看起来对第二行开始每行都偏移一点整体呈阶梯状斜线。原因字库每行按4字节对齐存储单字96字节代码仍按72字节计算。每退一行就多读或少读一个字节误差逐行累积所以越往下越乱。解决先核对文件大小。按72字节每字排布的字库覆盖72个汉字区时大约是72乘94乘72等于487296字节按96字节排布的会明显更大。计算时把bpl设成4单字字节数设成96。修改后重新取“口”字验证第一行是否完整。5.4 坑所有汉字整体平移一个固定字节数现象预览字形是完整的但位置偏了有时上下两个字的数据混在一起看起来像整个字形被平移了一段。原因字库文件带文件头。部分字库会在数据区前面加描述信息或版本信息代码从0开始读自然错位。这个偏移量往往固定所有字符都差同样字节数。解决先读偏移0的数据正常应该看到首个汉字的字模如果乱码尝试把文件头长度作为基础偏移。可以写一个小循环枚举前几百字节作为基础偏移找到让“啊”字能正确显示的那个值。这个值和文件格式强相关确认一次记下来后续换字库再复核一遍。6. 进阶缓存替代计算批量渲染前先跑一遍自检批量渲染场景里同一个字可能反复出现与其每次都做编码判断和偏移运算不如启动时把所有GB2312汉字扫一遍建立GBK编码到偏移的字典。这样运行时的取模操作只剩一次查表def build_cache(size, start_qu16): bpl size // 8 bpc bpl * size cache {} for qu in range(start_qu, 88): for wei in range(1, 95): b1 qu 0xA0 b2 wei 0xA0 cache[(b1, b2)] ((qu - start_qu) * 94 (wei - 1)) * bpc return cache这套缓存在线性公式之外多了一层保护查表时同时检查偏移加上单字字节数是否超过文件长度越界就返回错误码避免把越界数据当真字模用。如果换到资源更紧的单片机环境可以用静态数组替代字典索引是编码减去基址后的序号访问时间O(1)内存占用也固定。拿到一个新字库时我的习惯是先跑一遍全量自检把GB2312的6763个汉字全部取模拼成一张大图用眼睛扫一遍找异常字形同时统计每个偏移是否在文件范围内。这个自检脚本只需把第4章的render_text_bitmap放进一个循环不加额外逻辑但能挡住大部分换库引发的低级错误。最后补一个习惯性动作把“啊”和“口”两个字的预览结果固定存一份。每次改代码、换字库、调参数都先跑这两个字对比。它们一个是基准位验证一个是方向验证只要这两个字形态正确索引和排布基本不会错。希望这些经验能帮到你少踩几个坑。本文还有配套的精品资源点击获取