十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

嵌入式汉字字模提取:GBK点阵字库索引算法与工具实战

嵌入式汉字字模提取:GBK点阵字库索引算法与工具实战 简介一套面向嵌入式显示与打印场景的GBK汉字点阵字库资源覆盖16×16与24×24两种常用点阵规格收入GBK1.0中的22046个汉字字模专为打印机开发、LCD汉字显示等实际需求准备。压缩包内共包含21个文件主要类型有bin字库文件、txt编码表、exe字模提取工具、c/h源码、xls编码表等整体大小为3.41MB结构清晰便于快速定位与使用。字库附带了按分区排列和按编码顺序排列的两份GBK编码表同时给出GBK转BIG5与GBK转Unicode的函数实现索引算法也已直接提供免去手工对照规范的繁琐方便在PC端快速生成所需字模数据。此外还集成了PCtoLCD2002、牧码字模等提取软件可帮助开发者快速生成自定义字模并调试输出效果。目前该资源已有1661人学习适合单片机LCD显示、嵌入式打印及需要处理汉字点阵字库的开发者参考。1. 一次被逼出来的字模工具开发做嵌入式设备开发的朋友应该都遇到过这种情况——项目做到一半屏幕要显示汉字打印机要输出汉字结果字库这块卡了你整整两天。我这次做的项目是一台工业仪表要求带一块 2.4 寸 TFT 屏幕显示参数和菜单同时外接一台热敏打印机打印测量结果。屏幕分辨率有限字库不能太大打印精度要求相对高字太小会糊。两个场景对字的需求不同最终确定用 GBK 编码的 16×16 点阵字模做屏幕显示24×24 点阵字模做打印输出。GBK 是中文 Windows 和大量嵌入式设备默认的编码方式它的字库文件在嵌入式领域特别成熟像 UCDOS 的 HZK16、HZK24 系列至今还在各种单片机项目里服役。整套方案包含两块字库、一个索引算法、几个配套小工具覆盖了从字模提取到显示打印的完整链路。如果你的项目也和汉字显示、打印有关或者你正在纠结怎么给单片机配字库这篇文章正好能用上。我会把字模的结构、索引算法推导、工具实现思路、以及我踩过的坑全部拆开讲清楚。2. 整体设计思路为什么选 GBK 而不是 UTF-82.1 编码选择的博弈现在互联网上 UTF-8 是绝对的主流但嵌入式设备屏幕显示和打印场景GBK 依然是不二之选。核心原因是索引效率。GBK 是双字节定长编码汉字内码的两个字节和区位码有严格的数学对应关系可以直接通过公式计算字模在文件中的偏移量。UTF-8 是变长编码汉字三个字节虽然也能算但兼容性和历史积累远不如 GBK 字库成熟。UCDOS、HZK 系列字库文件格式公开、资料齐全在 8 位和 32 位单片机上跑了几十年稳定性早已验证。再考虑到用户交互场景很多工控设备的上位机软件是老一代 Delphi、VB 写的默认编码就是 GBK。如果你用 UTF-8 存字模上位机下发中文字符串后还得先转码再查字库多一道工序就多一个出错点。直接用 GBK 内码做索引从串口或者网络拿到数据就能直接查字模效率最高。2.2 16×16 和 24×24 的分工逻辑两种尺寸不是随便选的背后是显示和打印两种场景的物理限制16×16 字模每字占32 字节适合屏幕显示。TFT 屏像素点距小16 像素高的汉字在小字号下还能保持辨识度。字库文件只有 1MB 左右GBK 全字符集约 2 万汉字单片机 Flash 完全放得下。24×24 字模每字占72 字节适合打印。热敏打印机打印头分辨率一般是 203 DPI24×24 的点阵打印出来大约 3mm 见方清晰锐利如果拿 16×16 去打笔画会挤成一团小字勉强能看稍微大一点就露馅。另外还有一个细节GBK 编码区分不了全角标点和半角标点但字库文件里标点符号是单独存放的。16×16 和 24×24 字库的标点符号都排在汉字区之后索引公式要兼容这一部分后面细说。3. 索引算法推导两字节内码背后的数学关系这是整个项目最核心的部分。搞懂了索引算法字库就是一个随机访问的文件想取哪个字的字模直接算偏移量跳过去就行。3.1 GBK 编码规律GBK 编码的汉字由两个字节组成每个字节的取值范围有讲究第一字节区码0x81 ~ 0xFE共 126 个区第二字节位码0x40 ~ 0xFE其中去除0x7F共 190 个位一个区的位码从0x40到0xFE共 191 个编码位置但0x7FDEL 字符被剔除所以实际一个区有 190 个字符位。重点来了每个区固定有 190 个字符位。这意味着只要你在文件里按区、位顺序存储字模偏移量的计算就是一个纯数学问题。3.2 偏移量公式的来源以 HZK16 为例文件按如下顺序存放先按区号从小到大排列0x81 区、0x82 区...0xFE 区每个区内按位号从小到大排列0x40 位、0x41 位...0xFE 位每个字符的 16×16 点阵占 32 字节设汉字内码为hi_byte高字节和lo_byte低字节# 计算区号和位号 zone hi_byte - 0x81 # 区号从 0 开始 pos lo_byte - 0x40 # 位号从 0 开始 # 如果位号大于等于 0x7F需要减 1因为 0x7F 被剔除 if lo_byte 0x7f: pos - 1 # 计算偏移量 offset (zone * 190 pos) * 32等等这里有个关键问题。刚才说每个区有 190 个位但从0x40到0xFE一共有0xFE - 0x40 1 - 1 190个有效位这个没错。但是实际字库文件到底怎么排不同的字库实现方式可能不一样。我去翻了 HZK16 的文件头说明和网上多个版本的行列排布发现一个坑有的版本宣称按区位排有的宣称按 GB2312 内码排但实际文件字节数相同大约 267KB也就是 8176 个字符 × 32 字节排布顺序却不完全一致。最稳妥的办法是拿到一个字库先用已知汉字验证比如中的 GBK 编码是0xD6D0反推偏移量应该是多少然后读出来用工具可视化比对。我当时为了避免这个坑直接用了 Python 脚本把 HZK16 里从偏移 0 开始的前 32 字节打印成 16×16 点阵图确认第一个字符是什么再反推排序方式。这一步花了我半小时但换来的是后面所有代码一次通过。3.3 24×24 的索引推导HZK24 的排布逻辑和 HZK16 类似但有几个差异要特别注意字节数每个字 24 行 × 24 列按行存储每行 3 字节24 位所以每字72 字节文件组织HZK24 通常按字体分文件如 HZK24F 仿宋、HZK24H 黑体、HZK24K 楷体但每个文件的内部结构相同索引公式和 16×16 一样只是最后的乘数变成 72def get_gbk_offset(hi: int, lo: int, bytes_per_char: int) - int: zone hi - 0x81 pos lo - 0x40 if lo 0x7F: pos - 1 return (zone * 190 pos) * bytes_per_char这个算法对 HZK16bytes_per_char32和 HZK24bytes_per_char72都通用只是乘数不同。3.4 全角空格和标点的处理GBK 编码里全角空格0xA1A1在全角符号区和汉字区是连续的算法可以直接覆盖。但半角空格0x20不是双字节编码不适用上面的公式需要单独判断。很多网上的代码没处理这个边界打印含空格的字符串时会取到错误偏移导致打印出乱码甚至越界。我实际测过字符串里混了半角空格如果不去拦截轻则打出一个奇怪符号重则直接跳过字节导致后面所有汉字对不上。我的处理方式是在遍历字符串时如果是 ASCII 字符小于0x80直接用字库里的 ASCII 字模区如果有或者忽略只有双字节字符才走索引公式。def process_text(text: str, font16, font24): result [] i 0 while i len(text): char text[i] code ord(char) if code 0x80: # ASCII 字符单独处理 result.append(render_ascii(char, font16, font24)) i 1 else: # 双字节字符走 GBK 索引 gbk_bytes char.encode(gbk) hi, lo gbk_bytes[0], gbk_bytes[1] off16 get_gbk_offset(hi, lo, 32) off24 get_gbk_offset(hi, lo, 72) # 读取字模并渲染 i 1 return result4. 实操环节从零搭一个字模提取工具4.1 数据准备字库文件从哪来网上下载的 HZK16、HZK24 很多版本有坑有的文件头多几个字节有的缺少 ASCII 字符区有的是老式 UCDOS 格式。我的建议是下载后第一件事不是急着接代码而是验证文件大小和已知字符偏移。HZK16 的完整大小应该是126 × 190 × 32 766,080字节。如果你下载的文件大小对不上说明排序方式或者字符范围不对趁早换。我最终用的是自己从 Windows 系统字库simsun.ttc里抽出来的字模生成的字库。具体方法用 Python 的PIL库加载 TTF 字体把每个字符渲染成 16×16 或 24×24 的位图然后按 GBK 区位顺序写入二进制文件。这样生成的字体笔画更饱满比 UCDOS 的宋体好看不少。代价是生成步骤多一层但一劳永逸。from PIL import Image, ImageDraw, ImageFont def generate_hzk16(font_path, output_path): font ImageFont.truetype(font_path, 16) with open(output_path, wb) as f: for zone in range(0x81, 0xFF): for pos in range(0x40, 0xFF): if pos 0x7F: continue try: char bytes([zone, pos]).decode(gbk) except UnicodeDecodeError: f.write(bytes(32)) continue img Image.new(1, (16, 16), 255) draw ImageDraw.Draw(img) draw.text((0, 0), char, fontfont, fill0) for row in range(16): byte 0 for col in range(16): pixel img.getpixel((col, row)) if pixel 0: byte | 1 (7 - col) f.write(bytes([byte]))注意这里ImageFont.truetype(font_path, 16)的第二个参数是字体像素高度不是字号。TTF 里的字号是磅值和像素高度之间有个换算关系通常需要乘一个系数。我实测下来要得到 16 像素高的汉字磅值要设成 20 左右不然字模会被截断。这个系数和字体有关需要微调。4.2 可视化验证别用眼睛瞎猜写完字库生成脚本后必须做可视化验证。我是写了个 Python 脚本把字库里的每个汉字渲染成 ASCII 艺术字#和空格组成的格子输出到终端里人工核对。这一步能发现绝大多数问题字模错位、字节序反了、行序反了、字符偏位等等。def show_bmp(data: bytes, width: int 16): for row in range(width): line for col in range(width): byte_idx row * (width // 8) col // 8 bit_idx 7 - (col % 8) if data[byte_idx] (1 bit_idx): line # else: line print(line)4.3 索引算法在 Python 里的完整实现这是我在项目里实际使用的核心模块支持 16×16 和 24×24 两种字库输入字符串输出字模数据列表class GBKFont: def __init__(self, hzk16_path, hzk24_path): with open(hzk16_path, rb) as f: self.font16 f.read() with open(hzk24_path, rb) as f: self.font24 f.read() def _offset(self, hi: int, lo: int, bpc: int) - int: zone hi - 0x81 pos lo - 0x40 if lo 0x7F: pos - 1 return (zone * 190 pos) * bpc def get_char_data(self, char: str, size: int 16): 获取单个字符的字模size16 或 24 code ord(char) if code 0x80: raise ValueError(ASCII 字符请走 ASCII 字模通道) try: gbk char.encode(gbk) except UnicodeEncodeError: # 生僻字 GBK 无法编码用替代字符 gbk ?.encode(gbk) hi, lo gbk[0], gbk[1] if size 16: offset self._offset(hi, lo, 32) return self.font16[offset:offset 32] else: offset self._offset(hi, lo, 72) return self.font24[offset:offset 72] def get_string_data(self, text: str, size: int 16): 获取字符串的字模列表自动处理半角字符 result [] for char in text: if ord(char) 0x80: # 半角字符按空格处理或用 ASCII 字模 result.append(None) else: result.append(self.get_char_data(char, size)) return result用的时候屏幕显示直接拿 32 字节喂给 LCD 的绘图函数打印则拿 72 字节转成位图数据发给打印机。5. 常见问题与排查技巧实录5.1 典型问题速查表问题现象可能原因排查方法所有汉字都偏了几个像素字模字节序反了行内位序检查1 (7 - col)的方向汉字上下颠倒行序错误从最后一行开始存储检查写入顺序是否需要反转某些汉字显示为乱码GBK 编码失败生僻字超出范围用try/except捕获统一替换字符串错位第一个字正常后面全乱半角字符没处理走错索引通道判断ord(char) 0x80单独处理24×24 字模显示异常每行字节数算错应为 3 字节确认字节下标计算row * 3 col // 8打印出来字体有锯齿打印头分辨率不够或字模用了 16×16换成 24×24 字模字库文件读取越界文件大小不对或字符超出 GBK 范围校验文件大小为 766080 或 1723680 字节全角空格正常但半角空格消失ASCII 通道没有对应字模单独给半角空格分配一个空白字模5.2 困惑我半天的字节序问题第一次把字模显示到 TFT 屏幕上汉字全部左右镜像。排查了半天问题出在字模数据的位序上。我的 TFT 屏幕驱动要求每字节的 bit0 对应最左边的像素而字库文件是 bit7 对应最左边。这俩顺序反了显示出来自然反的。解决办法是在驱动层做位反转或者显示函数里反转解析。def reverse_byte(b: int) - int: b (b 0xF0) 4 | (b 0x0F) 4 b (b 0xCC) 2 | (b 0x33) 2 b (b 0xAA) 1 | (b 0x55) 1 return b如果你也在做屏幕显示拿到字模后先别急着接驱动用 Python 先可视化确认字节序风格再决定适配层放哪里。别像我一样第一次直接怼硬件结果查了半天发现是软件顺序问题。5.3 生僻字的一点现实妥协GBK 不是 Unicode很多生僻字、异体字根本不在编码范围内。比如商家名字里的生僻字GBK 查不到字库自然也没有。我的处理方式是遇到无法编码的字符统一替换成一个□GBK 能编码同时在日志里记录位置方便事后核对。别试图用 GBK 编码所有 Unicode 字符不现实。如果你的项目确实需要覆盖 GBK 以外的生僻字方案是换 UTF-8 编码 全字库但那样字库体积会翻好几倍嵌入式环境通常扛不住。取舍是看业务需求面向大众的消费类产品可以考虑降低成本面向专业领域的工业设备就用 GBK 足够了。5.4 打印对齐的一个隐藏坑用 24×24 字模打印中文字符中英文混排时对齐容易出现问题。因为英文字符宽度只有 12 像素而中文字符宽度是 24 像素如果驱动层没有做宽度自适应打印出来的英文和中文会错落不齐。我的解决方案打印驱动里所有字符统一按 24 像素宽度处理英文左对齐后右补空白。这样牺牲了一点英文间距的紧凑度但保证了整体对齐效果。对于工控设备打单据这种场景对齐比紧凑重要得多。6. 这个方案做到什么程度了目前整套方案已经跑在我那台工业仪表上屏幕显示和打印输出的汉字清晰稳定GBK 全字符集的 index 算法响应时间在微秒级别完全是够用的。回头总结一下做汉字字模这件事本质上不复杂就是一个买对原料字库文件、算对公式索引算法、处理好边界半角字符、生僻字、字节序的组合拳。没有哪个环节是高精尖但每个环节都有暗坑踩一个就够折腾半天。我个人实操中的体会是千万不要跳步。字库文件先验证大小索引算法先用已知汉字反推字模生成后先可视化确认再往硬件上接。省掉的每一步最后都会变成排查问题的时间补回来。这个项目后续还可以扩展的方向包括增加 12×12 小字库用于状态栏显示、支持斜体和粗体变体、给 24×24 字模做抗锯齿处理虽然点阵字库抗锯齿意义不大但可以缓解打印发虚的问题。如果你也正在搞嵌入式显示或者打印相关的开发希望这篇记录能帮你少踩几个坑。有问题的话欢迎顺着这个思路自己动手验证一遍收获会比直接抄代码大得多。本文还有配套的精品资源点击获取
返回列表