arduino编程时,需要把资源处理成c数组的方式包含进去,图片、音乐、文字等都要经过处理才能使用。本文说明了图片和中文文字的处理方法。

图片资源

epaper有两种显示模式,一种是黑白二色图片,一种是黑白加四灰度图片。这里只说黑白二色图片的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#!/usr/bin/env python3
"""
把图片转成 800x480 1位黑白数据(墨水屏用)
输出:
image_800x480.bin 原始字节(1位/像素,行扫描,每字节8像素)
gImage_800x480.h C 数组
preview_800x480.png 可选预览
用法:
python img2epd.py input.jpg
python img2epd.py input.jpg --preview
python img2epd.py input.png --threshold 100 --invert --preview
"""

import argparse
import os
from PIL import Image

WIDTH, HEIGHT = 800, 480
MSB_FIRST = True # True: 高位对应左侧像素;若显示左右镜像改为 False
BLACK_IS_1 = False # True: 1=黑, 0=白;若显示反色改为 False

def load_and_fit(path, w, h):
img = Image.open(path).convert("L") # 灰度
# 等比缩放并居中填充白底
scale = min(w / img.width, h / img.height)
nw, nh = int(img.width * scale), int(img.height * scale)
img = img.resize((nw, nh), Image.LANCZOS)
canvas = Image.new("L", (w, h), 255) # 白底
canvas.paste(img, ((w - nw) // 2, (h - nh) // 2))
return canvas

def to_1bit_bytes(img, threshold=128, msb_first=True, black_is_1=True):
"""返回 bytes: 每字节8像素,行扫描"""
pixels = img.load()
row_bytes = (WIDTH + 7) // 8
out = bytearray(row_bytes * HEIGHT)
for y in range(HEIGHT):
for x in range(WIDTH):
gray = pixels[x, y]
is_black = gray < threshold
bit = 1 if (is_black == black_is_1) else 0
byte_index = y * row_bytes + (x // 8)
bit_pos = (7 - (x % 8)) if msb_first else (x % 8)
if bit:
out[byte_index] |= (1 << bit_pos)
return bytes(out)

def write_header(data, path, name="gImage_800x480"):
with open(path, "w", encoding="utf-8") as f:
f.write(f"// Auto-generated {WIDTH}x{HEIGHT} 1-bit image\n")
f.write(f"#define {name.upper()}_WIDTH {WIDTH}\n")
f.write(f"#define {name.upper()}_HEIGHT {HEIGHT}\n")
f.write(f"const unsigned char {name}[{len(data)}] = {{\n")
for i in range(0, len(data), 16):
chunk = data[i:i+16]
f.write(" " + ", ".join(f"0x{b:02X}" for b in chunk) + ",\n")
f.write("};\n")

def main():
ap = argparse.ArgumentParser()
ap.add_argument("input")
ap.add_argument("--threshold", type=int, default=128)
ap.add_argument("--invert", action="store_true", help="反转黑白")
ap.add_argument("--preview", action="store_true")
ap.add_argument("--outdir", default=".")
args = ap.parse_args()

img = load_and_fit(args.input, WIDTH, HEIGHT)
data = to_1bit_bytes(img, args.threshold,
msb_first=MSB_FIRST,
black_is_1=(not args.invert) if BLACK_IS_1 else args.invert)

bin_path = os.path.join(args.outdir, "image.bin")
h_path = os.path.join(args.outdir, "gImage.h")
with open(bin_path, "wb") as f:
f.write(data)
write_header(data, h_path)

print(f"bin: {bin_path} ({len(data)} bytes, 期望 {((WIDTH+7)//8)*HEIGHT})")
print(f"h : {h_path}")

if args.preview:
# 用同样打包方式反解回图,确认效果
row_bytes = (WIDTH + 7) // 8
prev = Image.new("1", (WIDTH, HEIGHT))
px = prev.load()
for y in range(HEIGHT):
for x in range(WIDTH):
b = data[y * row_bytes + (x // 8)]
bit_pos = (7 - (x % 8)) if MSB_FIRST else (x % 8)
bit = (b >> bit_pos) & 1
px[x, y] = 0 if (bit == (1 if BLACK_IS_1 else 0)) else 1
prev.save(os.path.join(args.outdir, "preview_800x480.png"))
print("preview saved")

if __name__ == "__main__":
main()

保存为 img2epd.py,使用方法:
python img2epd.py input.jpg --preview
生成:
1 image_800x480.bin 原始字节(1位/像素,行扫描,每字节8像素)
2 gImage_800x480.h C 数组
3 preview_800x480.png 可选预览
1可用于网络下载的资源供程序动态下载使用,2可用于包含进ino直接使用,3预览效果。

中文文字

esp32编程中使用的文字都必须经过特别的处理才能正常使用,英文字库可显示字符比较少,容易处理,中文字库大,把整个字库转换之后体积太大,一般只把所需的字符包含进去,这样占资源较少。

英文字库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
from PIL import Image, ImageDraw, ImageFont, Image
from numpy import asarray, ceil, array, sum, concatenate, pad

filename = 'consolab36' #<----- select new font name
fontname = 'consolab.ttf' #<----- specify the font the you intend to use. Place any font into the fonts folder
height = 36 #<----- new font height
width = 22 #<----- new font width
THRESHOLD = 120 #<----- image intensity threshold for binary conversion. It changes the contrast of the final font


font_offset = 4 # recommended to be at least 4
binary_byte = array([128, 64, 32, 16, 8, 4, 2, 1])

def createTMPimage(ASCII):

image = Image.new('RGB', (width, height), color=(0, 0, 0))
draw = ImageDraw.Draw(image)
font = ImageFont.truetype(f"./fonts/{fontname}", height - font_offset)
if font.getlength(chr(ASCII)) > width:
temp_image = Image.new('RGB', (int(font.getlength(chr(ASCII))), height), color=(0, 0, 0))
temp_draw = ImageDraw.Draw(temp_image)
temp_draw.text((0, 0), chr(ASCII), font=font)
squeezed_image = temp_image.resize((width, height), Image.HAMMING)
image.paste(squeezed_image, (0, 0))
else:
draw.text((0, 0), chr(ASCII), font=font)
image.save(f'./tmp/{ASCII}.bmp')


def readImage2Binary(ASCII):

image = Image.open(f'./tmp/{ASCII}.bmp')
data = asarray(image)
binary_map = data[:, :, 0]
return binary_map


def convertMap2Hex(binary_map):

hex_map = []
for line in range(binary_map.shape[0]):
for bit_chunks in range(int(ceil(width/8))):
tmp = binary_map[line][bit_chunks*8:(min((bit_chunks+1)*8, width))]
tmp = array(list(map(lambda x: int(x > THRESHOLD), tmp)))
tmp = concatenate((tmp, array([0] * (8 - len(tmp))))) # padding with zeros
binary_value = int(sum(tmp * binary_byte))
hex_map.append(f"{binary_value:#0{4}x}")

return hex_map


def write_file_intro(f):

f.write('/* Includes ------------------------------------------------------------------*/\n')
f.write('#include "fonts.h"\n')
f.write(f'const uint8_t Font{height}_Table [] = \n')
f.write('{\n')


def write_file_closure(f):

f.write('};\n\n')
f.write(f'sFONT Font{height} = {{\n')
f.write(f'\tFont{height}_Table,\n')
f.write(f'\t{width}, /* Width */\n')
f.write(f'\t{height}, /* Height */\n')
f.write('};\n\n')


def write_letter(hex_map):

f.write(f'\t// ASCII: {ASCII} "{chr(ASCII)}" ({width} pixels wide)\n')

count = 0
f.write('\t')

for item in hex_map:
f.write(f'{item}, ')
count += 1
if count == 3:
count = 0
f.write('\n\t')

f.write('\n')

if __name__ == "__main__":

with open(f'./output/{filename}.cpp', 'w') as f:
write_file_intro(f)

for ASCII in range(32, 127):
print(f'working on ASCII: {ASCII}: {chr(ASCII)}')

createTMPimage(ASCII)
binary_map = readImage2Binary(ASCII)
hex_map = convertMap2Hex(binary_map)
write_letter(hex_map)

write_file_closure(f)

这是 https://github.com/theHEXstyle/font2bytes 脚本,可以把英文可显示字符转化成可用的c数组格式,和官方示例程序包里的font24.cpp这些字体一致。
同一目录下先建立fonts,output,tmp目录,字体放在fonts里,结果输出在output里。
注意要等宽字体,不然排版不齐,英文courier New,consola字体都可以。
36号比较适合,示例里的24号有点小。

python要安装PIL和numpy库。
使用方式见脚本的开始部分。

中文字库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
生成微雪风格的 CH_CN 字库 C 文件。
默认:微软雅黑 24 号,32x41 点阵,横向取模,MSB first,自动居中。

用法示例:
python gen_font.py --text "中华人民共和国"
python gen_font.py --text "你好微雪" --size 24 --w 32 --h 41 --out Font24CN.c
"""

import argparse
import os
import sys
from PIL import Image, ImageDraw, ImageFont

# ---------------- 默认参数 ----------------
DEFAULT_TTF = r"C:\Windows\Fonts\msyh.ttc" # 微软雅黑
DEFAULT_SIZE = 24
DEFAULT_W = 32
DEFAULT_H = 41
DEFAULT_OUT = "Font24CN.c"
DEFAULT_STRUCT = "Font24CN"
DEFAULT_TABLE = "Font24CN_Table"
DEFAULT_ASCII_W = 24
DEFAULT_COMMENT = "微软雅黑"


# ---------------- 点阵渲染(裁剪 + 居中) ----------------

def render_glyph(ch, font, w, h, x_off=None, y_off=None, threshold=128):
"""
渲染字符到 w x h 二值位图。
默认:在大画布上画字,按包围盒裁剪后居中贴到目标画布。
x_off/y_off 不为 None 时用固定偏移(相对目标画布左上角)。
返回 list[list[int]](0/1),行优先,每行 w 个像素。
"""
# 1. 大画布上画字,测量真实包围盒
pad = 16
tmp = Image.new("L", (w + pad * 2, h + pad * 2), 0)
d = ImageDraw.Draw(tmp)
d.text((pad, pad), ch, font=font, fill=255)
bbox = tmp.getbbox()

if bbox is None:
# 字形为空,返回全 0
return [[0] * w for _ in range(h)]

# 2. 裁剪出字形
glyph = tmp.crop(bbox)
gw, gh = glyph.size

# 3. 贴到目标画布
img = Image.new("L", (w, h), 0)
if x_off is None and y_off is None:
dx = (w - gw) // 2
dy = (h - gh) // 2
else:
dx = x_off or 0
dy = y_off or 0
img.paste(glyph, (dx, dy))

# 4. 二值化
pixels = img.load()
rows = []
for y in range(h):
row = []
for x in range(w):
row.append(1 if pixels[x, y] >= threshold else 0)
rows.append(row)
return rows


def rows_to_bytes(rows, w, h):
"""
横向取模:每行按 8 位一组打包,高位在前(MSB first),不足补 0。
"""
data = []
for y in range(h):
row = rows[y]
for byte_start in range(0, w, 8):
byte_val = 0
for bit in range(8):
col = byte_start + bit
if col < w and row[col]:
byte_val |= (0x80 >> bit)
data.append(byte_val)
return data


# ---------------- C 文件输出 ----------------

def format_bytes(data, per_line=16):
lines = []
for i in range(0, len(data), per_line):
chunk = data[i:i + per_line]
lines.append(",".join(f"0x{b:02X}" for b in chunk) + ",")
return "\n".join(lines)


def c_escape(s):
return s.replace("\\", "\\\\").replace('"', '\\"')


def build_c_content(chars, font, w, h, x_off, y_off,
table_name, struct_name, ascii_w, comment, font_size):
entries = []
for ch in chars:
rows = render_glyph(ch, font, w, h, x_off, y_off)
data = rows_to_bytes(rows, w, h)
entries.append((ch, data))

lines = []
lines.append("/*")
lines.append(" Auto-generated by gen_font.py")
lines.append(f" Font: msyh size {font_size}")
lines.append(f" Glyph: {w}x{h}, horizontal scan, MSB first, auto-centered")
lines.append("*/")
lines.append("")
lines.append('#include "fonts.h"')
lines.append("")
lines.append(f"const CH_CN {table_name}[] = ")
lines.append("{")

for ch, data in entries:
lines.append(f"/*-- 文字: {ch} --*/")
lines.append(f"/*-- {comment}{font_size}; 此字体下对应的点阵为:宽x高={w}x{h} --*/")
lines.append("{")
lines.append(f'"{c_escape(ch)}",')
lines.append(format_bytes(data))
lines.append("},")
lines.append("")

lines.append("};")
lines.append("")
lines.append(f"cFONT {struct_name} = {{")
lines.append(f" {table_name},")
lines.append(f" sizeof({table_name})/sizeof(CH_CN), /*size of table*/")
lines.append(f" {ascii_w}, /* ASCII Width */")
lines.append(f" {w}, /* Width */")
lines.append(f" {h}, /* Height */")
lines.append("};")
lines.append("")
return "\n".join(lines)


def generate_c(text, ttf_path, font_size, w, h, out_path,
x_off=None, y_off=None,
struct_name=DEFAULT_STRUCT, table_name=DEFAULT_TABLE,
ascii_w=DEFAULT_ASCII_W, comment=DEFAULT_COMMENT):
font = ImageFont.truetype(ttf_path, font_size)

# 去重保序
seen, chars = set(), []
for ch in text:
if ch not in seen:
seen.add(ch)
chars.append(ch)

content = build_c_content(
chars, font, w, h, x_off, y_off,
table_name, struct_name, ascii_w, comment, font_size)

# 优先 UTF-8

with open(out_path, "w", encoding="utf-8", newline="\n") as f:
f.write(content)
enc = "UTF-8"

print(f"已写出 {out_path}({enc} 编码),共 {len(chars)} 个字符")
for ch in chars:
rows = render_glyph(ch, font, w, h, x_off, y_off)
dots = sum(sum(r) for r in rows)
if dots == 0:
print(f" [警告] 字符 '{ch}' 渲染为空!")
else:
print(f" '{ch}': {dots} 个非零点")


# ---------------- 命令行入口 ----------------

def main():
parser = argparse.ArgumentParser(
description="生成微雪风格 CH_CN 字库 C 文件(默认微软雅黑 24,自动居中)")
parser.add_argument("--text", required=True,
help='要生成的字符,如 "中华人民共和国"')
parser.add_argument("--ttf", default=DEFAULT_TTF,
help=f"字体文件路径(默认 {DEFAULT_TTF})")
parser.add_argument("--size", type=int, default=DEFAULT_SIZE,
help=f"字号(默认 {DEFAULT_SIZE})")
parser.add_argument("--w", type=int, default=DEFAULT_W,
help=f"点阵宽(默认 {DEFAULT_W})")
parser.add_argument("--h", type=int, default=DEFAULT_H,
help=f"点阵高(默认 {DEFAULT_H})")
parser.add_argument("--out", default=DEFAULT_OUT,
help=f"输出 C 文件路径(默认 {DEFAULT_OUT})")
parser.add_argument("--x-off", type=int, default=None,
help="固定 X 偏移(不设则自动居中)")
parser.add_argument("--y-off", type=int, default=None,
help="固定 Y 偏移(不设则自动居中)")
parser.add_argument("--ascii-w", type=int, default=DEFAULT_ASCII_W,
help=f"结构体 ASCII Width 字段(默认 {DEFAULT_ASCII_W})")
parser.add_argument("--struct", default=DEFAULT_STRUCT,
help=f"cFONT 变量名(默认 {DEFAULT_STRUCT})")
parser.add_argument("--table", default=DEFAULT_TABLE,
help=f"字模数组名(默认 {DEFAULT_TABLE})")
parser.add_argument("--comment", default=DEFAULT_COMMENT,
help=f"注释里的字体名(默认 {DEFAULT_COMMENT})")
args = parser.parse_args()

if not os.path.exists(args.ttf):
print(f"错误:找不到字体文件 {args.ttf}")
sys.exit(1)

generate_c(
text=args.text,
ttf_path=args.ttf,
font_size=args.size,
w=args.w,
h=args.h,
out_path=args.out,
x_off=args.x_off,
y_off=args.y_off,
struct_name=args.struct,
table_name=args.table,
ascii_w=args.ascii_w,
comment=args.comment,
)


if __name__ == "__main__":
main()

保存为gen_font.py。
生成24号字体:

1
python gen_font.py --text "中华人民共和国" --ttf E:\desktop\NotoSansSC-6.ttf

生成36号字体:

1
python gen_font.py --text "你好微雪" --size 36 --w 36 --h 36

text部分包含项目中输出到界面的所有中文字符,没有的话会输出空白。
没有–ttf部分是缺省的雅黑字体。
生成的Font24CN.c复制到项目目录,使用方法:

1
2
//白底黑字输出中文字符串,字体名称Fon24CN
Paint_DrawString_CN(10, 10, "中国", &Font24CN, BLACK, WHITE);

字体名称没有相应改变。注意在fonts.h中包含此字体,字体名称改变要修改Font24CN.c中的数组名。
微雪 Paint_DrawString_CN 驱动是纯二值点阵绘制,没有抗锯齿,Noto Sans SC字体24和36号字体效果比雅黑和黑体要好一点。

CrossMux上用的Noto Sans SC字体因为有完整的字库渲染引擎、灰度抗锯齿、以及针对墨水屏优化的排版逻辑,效果更好。