add font support

This commit is contained in:
2026-01-29 15:32:26 +08:00
parent 816a54a112
commit 36b4044a59
12 changed files with 1187 additions and 1089 deletions

191
docs/jpgbin.md Normal file
View File

@@ -0,0 +1,191 @@
#!/usr/bin/env python3
"""
图像转JPG二进制文件工具
将图像文件转换为ST7789显示所需的JPG二进制格式(.jbin文件)
使用方法:
python image2jpgbin.py input.jpg output.jbin
python image2jpgbin.py input.png output.jbin
支持格式:
- JPEG (.jpg, .jpeg)
- PNG (.png)
- BMP (.bmp)
注意:
对于PNG/BMP等非JPEG格式会先转换为JPEG格式quality参数控制最终JPEG质量
"""
import argparse
import os
import struct
import sys
from pathlib import Path
try:
from PIL import Image
except ImportError:
print("错误: 需要安装Pillow库")
print("请运行: pip install Pillow")
sys.exit(1)
def convert_image_to_jbin(input_path, output_path, quality=85):
"""
将图像文件转换为JBIN格式
Args:
input_path: 输入图像文件路径
output_path: 输出JBIN文件路径
quality: JPEG质量(1-100)
Returns:
True: 转换成功
False: 转换失败
"""
try:
# 打开图像文件
with Image.open(input_path) as img:
# 转换为RGB模式
if img.mode != "RGB":
img = img.convert("RGB")
# 获取图像尺寸
width, height = img.size
# 创建临时JPEG文件内存中
import io
jpeg_buffer = io.BytesIO()
img.save(jpeg_buffer, format="JPEG", quality=quality, optimize=True)
jpeg_data = jpeg_buffer.getvalue()
# 打开输出文件
with open(output_path, "wb") as f:
# 写入头信息(宽度和高度,小端序)
f.write(struct.pack("<HH", width, height))
# 写入JPEG数据
f.write(jpeg_data)
print(f"成功转换: {input_path} ({width}x{height}) -> {output_path}")
print(f" 输出文件大小: {os.path.getsize(output_path)} 字节")
return True
except Exception as e:
print(f"转换失败: {e}")
return False
def convert_image_to_jbin(input_path, output_path, quality=85):
"""
将图像文件转换为JBIN格式
Args:
input_path: 输入图像文件路径
output_path: 输出JBIN文件路径
quality: JPEG质量(1-100)
- 对于JPEG文件: 重新压缩时使用此质量
- 对于PNG/BMP文件: 转换为JPEG时使用此质量
Returns:
True: 转换成功
False: 转换失败
"""
try:
# 打开图像文件
with Image.open(input_path) as img:
# 转换为RGB模式
if img.mode != "RGB":
img = img.convert("RGB")
# 获取图像尺寸
width, height = img.size
# 创建临时JPEG文件内存中
import io
jpeg_buffer = io.BytesIO()
img.save(jpeg_buffer, format="JPEG", quality=quality, optimize=True)
jpeg_data = jpeg_buffer.getvalue()
# 打开输出文件
with open(output_path, "wb") as f:
# 写入头信息(宽度和高度,小端序)
f.write(struct.pack("<HH", width, height))
# 写入JPEG数据
f.write(jpeg_data)
print(f"成功转换: {input_path} ({width}x{height}) -> {output_path}")
print(f" 输出文件大小: {os.path.getsize(output_path)} 字节")
print(f" JPEG质量: {quality}")
return True
except Exception as e:
print(f"转换失败: {e}")
return False
def main():
parse = argparse.ArgumentParser(
description="将图像文件转换为ST7789显示所需的JPG二进制格式(.jbin文件)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
# 单文件转换
python image2jpgbin.py input.jpg output.jbin
# 转换非JPEG文件(PNG/BMP)
python image2jpgbin.py input.png output.jbin
# 指定JPEG质量
python image2jpgbin.py --quality 95 input.jpg output.jbin
输出格式(.jbin):
偏移 大小 描述
0x00 2 图像宽度(小端序)
0x02 2 图像高度(小端序)
0x04 N JPEG图像数据
注意:
对于PNG/BMP等非JPEG格式会先转换为JPEG格式quality参数控制转换质量
""",
)
parse.add_argument("input", help="输入图像文件路径")
parse.add_argument("output", help="输出JBIN文件路径")
parse.add_argument(
"--quality",
"-q",
type=int,
default=85,
choices=range(1, 101),
help="JPEG压缩质量(1-100),默认85",
)
args = parse.parse_args()
# 检查输入文件是否存在
if not os.path.isfile(args.input):
print(f"错误: 输入文件不存在: {args.input}")
return 1
# 创建输出目录(如果需要)
output_dir = os.path.dirname(args.output)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir, exist_ok=True)
# 转换文件
if convert_image_to_jbin(args.input, args.output, args.quality):
return 0
else:
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -177,7 +177,7 @@ async def animation_task():
try:
# 计算当前帧号(1-20)
current_frame = (frame % frame_count) + 1
filename = f"/rom/www/images/T{current_frame}.jpg"
filename = f"/rom/images/T{current_frame}.jpg"
# 显示当前帧,右下角
display.show_jpg(filename, 160, 160)
@@ -203,15 +203,17 @@ def start():
# 初始化液晶屏
display.init_display(config.get("bl_mode") != "gpio")
display.brightness(int(config.get("brightness", 10)))
display.show_jpg("/rom/www/images/T1.jpg", 80, 80)
display.show_jpg("/rom/images/T1.jpg", 80, 80)
gc.collect()
display.message("WiFi connect ...")
if not wifi_manager.connect():
print("Failed to connect to WiFi, starting CaptivePortal for configuration")
gc.collect()
from captive_portal import CaptivePortal
portal = CaptivePortal()
display.portal_win(portal.essid.decode('ascii'))
return portal.start()
gc.collect()
# init web server
@@ -368,7 +370,8 @@ def start():
# 启动动画显示任务
loop.create_task(animation_task())
# run!
gc.collect()
print(f"App Memory Free: {gc.mem_free()}")
display.message(f"success: {gc.mem_free()}...")
# run!
loop.run_forever()

View File

@@ -29,9 +29,14 @@ class Display:
self._backlight = None
self._brightness = 80 # 默认亮度80%
self._initialized = True
self.en_font = '/rom/fonts/en-8x16.rfont'
self.cn_font = '/rom/fonts/cn-22x24.bfont'
self.vector_font = '/rom/fonts/en-32x32.hfont'
# 前景色、背景色、提示框背景色
self._COLORS = (0xFE19, 0x0000, 0x7800)
def init_display(self, bl_pwm=True):
"""初始化液晶屏"""
def init_display(self, bl_pwm=True, buffer_size=2048):
"""初始化液晶屏默认2048够用且不易有内存碎片"""
try:
from machine import PWM, SPI, Pin
@@ -42,7 +47,7 @@ class Display:
240,
dc=Pin(0, Pin.OUT),
reset=Pin(2, Pin.OUT),
buffer_size=0,
buffer_size=buffer_size,
)
# 初始化PWM背光控制
@@ -69,15 +74,11 @@ class Display:
"""检查液晶屏是否已初始化"""
return self.tft is not None
def driver(self):
"""获取液晶屏对象"""
return self.tft
def clear(self, color=st7789.BLACK):
"""清屏"""
self.tft.fill(color)
def show_jpg(self, filename, x=0, y=0, mode=st7789.FAST):
def show_jpg(self, filename, x=0, y=0, mode=st7789.SLOW):
"""显示JPG图片"""
self.tft.jpg(filename, x, y, mode)
@@ -97,6 +98,34 @@ class Display:
self._brightness = _brightness
return self._brightness
def message(self, msg, x=10, y=10, fg=st7789.WHITE, bg=st7789.BLACK):
self.tft.text(self.en_font, msg, x, y, fg, bg)
def window(self, title=None, content=None, info=None):
C_FG,C_BG,C_BT = self._COLORS
self.tft.fill(C_BG)
self.tft.rect(8, 8, 224, 224, C_FG)
self.tft.fill_rect(9, 9, 222, 40, C_BT)
self.tft.hline(9, 48, 222, C_FG)
self.tft.fill_rect(9, 192, 222, 39, C_BT)
self.tft.hline(9, 192, 222, C_FG)
if title:
self.tft.write(self.cn_font, title, 19, 17, C_FG, C_BT)
if info:
self.tft.text(self.en_font, info, 19, 204, C_FG, C_BT)
for i, line in enumerate(content):
if line:
self.tft.write(self.cn_font, line, 19, 68+i*28, C_FG, C_BG)
def portal_win(self, ssid):
tips = [
"> 连接热点:",
f"> {ssid}",
"",
"> 自动进入配置页面",
]
self.window("配置设备网络连接", tips, "portal ip: 192.168.4.1")
# 全局液晶屏实例
display = Display()

View File

@@ -1,394 +1,15 @@
#!/bin/bash
# -*- coding: utf-8 -*-
# 资源转换脚本 - 将字体和图片转换为二进制文件供流式加载使用
# 使用方法: ./convert_assets.sh
set -e # 遇到错误时退出
CN_TEXT='配置设备网络连接热点自动进入页面0123456789 abcdefghijklmnopqrstuvwxyz:ABCDEFGHIJKLMNOPQRSTUVWXYZ!%*+,-./()[]\"<>=?℃'
# 项目根目录
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ASSETS_DIR="$PROJECT_ROOT/utils/assets"
FONTS_DIR="$PROJECT_ROOT/utils/assets"
FONT_CONFIG_PATH="$ASSETS_DIR/fonts.json"
OUTPUT_DIR="$PROJECT_ROOT/utils/output"
SRC_DIR=./assets
DST_DIR=../src/rom/fonts
OTF_FONT=$SRC_DIR/NotoSansSC-Regular.otf
# 创建输出目录
mkdir -p "$OUTPUT_DIR/py"
mkdir -p "$OUTPUT_DIR/bin"
python3 pyfont_to_bin.py $SRC_DIR/romand.py $DST_DIR/en-32x32.hfont
python3 pyfont_to_bin.py $SRC_DIR/vga1_8x16.py $DST_DIR/en-8x16.rfont
python3 pyfont_to_bin.py $SRC_DIR/vga1_16x32.py $DST_DIR/en-16x32.rfont
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 打印带颜色的信息
print_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_blue() {
echo -e "${BLUE}[CONFIG]${NC} $1"
}
# ==============================================
# 图片转换配置表 - 调整这里来自定义图片设置
# ==============================================
# 默认图片颜色深度
DEFAULT_IMAGE_COLORS=4
# 支持的图片文件扩展名
IMAGE_EXTENSIONS=("png" "jpg" "jpeg" "bmp" "gif")
# ==============================================
# 函数定义
# ==============================================
# 检查依赖
check_dependencies() {
print_info "检查依赖..."
# 检查Python
if ! command -v python3 &> /dev/null; then
print_error "Python3 未安装,请先安装 Python3"
exit 1
fi
# 检查freetype-py
if ! python3 -c "import freetype" &> /dev/null; then
print_warn "freetype-py 未安装,尝试安装..."
pip3 install freetype-py
fi
# 检查Pillow
if ! python3 -c "import PIL" &> /dev/null; then
print_warn "Pillow 未安装,尝试安装..."
pip3 install Pillow
fi
print_info "依赖检查完成"
}
# 读取字体配置文件
load_font_configs() {
local fonts_file="$FONT_CONFIG_PATH"
if [ ! -f "$fonts_file" ]; then
print_warn "字体配置文件不存在: $fonts_file"
print_warn "将跳过字体转换"
return
fi
print_info "加载字体配置文件: $fonts_file"
print_blue "字体配置:"
echo "----------------------------------------"
printf "%-12s %-30s %-8s %-15s %s\n" "字体名称" "字符集预览" "字号" "输出名称" "描述"
echo "----------------------------------------"
# 检查是否有Python可用来解析JSON
if ! command -v python3 &> /dev/null; then
print_warn "Python3 不可用无法解析JSON配置文件"
print_warn "将跳过字体转换"
return
fi
# 使用Python解析JSON并生成临时配置文件
python3 -c "
import json
import sys
try:
with open('$fonts_file', 'r', encoding='utf-8') as f:
config = json.load(f)
line_num = 0
for font in config.get('fonts', []):
name = font.get('name', '')
file = font.get('file', '')
size = font.get('size', '')
chars = font.get('chars', '')
output = font.get('output', '')
description = font.get('description', '')
if not name or not file or not size or not output:
continue
# 如果是相对路径加上assets目录
if not file.startswith('/'):
file = '$ASSETS_DIR/' + file
# 显示配置信息
preview = chars[:25] + ('...' if len(chars) > 25 else '')
print(f'{name:<12} {preview:<30} {size:<8} {output:<15} {description}')
# 存储配置到临时文件
with open('$TEMP_FONT_CONFIG_FILE', 'a') as f_out:
f_out.write(f'{name}|{chars}|{file}|{size}|{output}|{description}\n')
line_num += 1
print(f'\n加载了 {line_num} 个字体配置')
except Exception as e:
print(f'解析JSON配置文件时出错: {e}', file=sys.stderr)
sys.exit(1)
"
}
# 显示图片配置
show_image_configs() {
print_blue "图片转换:"
echo "----------------------------------------"
echo "支持格式: ${IMAGE_EXTENSIONS[*]}"
echo "默认颜色深度: $DEFAULT_IMAGE_COLORS"
echo "特殊配置: bx_开头的图片文件名中的x值将覆盖默认颜色深度"
echo
}
# 检查图片文件
is_image_file() {
local file_name="$1"
local ext="${file_name##*.}"
for supported_ext in "${IMAGE_EXTENSIONS[@]}"; do
if [[ "$ext" == "$supported_ext" ]]; then
return 0
fi
done
return 1
}
# 获取图片颜色深度
get_image_colors() {
local file_name="$1"
# 检查是否是b数字_或bx数字_开头的文件
if [[ "$file_name" =~ ^b[0-9]+_ ]] || [[ "$file_name" =~ ^bx[0-9]+_ ]]; then
# 提取数字部分
local colors=$(echo "$file_name" | sed -E 's/^b(x?)([0-9]+)_.*/\2/')
print_info "图片 $file_name 使用特殊颜色深度: $colors" >&2
echo "$colors"
return
fi
# 使用默认颜色深度
echo "$DEFAULT_IMAGE_COLORS"
}
# 转换字体
convert_font() {
local font_file="$1"
local font_size="$2"
local characters="$3"
local output_name="$4"
local output_py_dir="$5"
local output_bin_dir="$6"
print_info "转换字体: $(basename "$font_file") (字号: $font_size)"
local font_name=$(basename "$font_file" | cut -d'.' -f1)
local temp_py="$output_py_dir/${output_name}.py"
local bin_file="$output_bin_dir/${output_name}.font"
# 步骤1: 转换为Python模块
print_info " 步骤1: 将字体转换为Python模块..."
python3 "$PROJECT_ROOT/utils/font2bitmap.py" "$font_file" "$font_size" -s "$characters" > "$temp_py"
# 步骤2: 转换为二进制文件
print_info " 步骤2: 将Python模块转换为二进制文件..."
python3 "$PROJECT_ROOT/utils/font2bin.py" "$temp_py" "$bin_file"
print_info " 字体转换完成: $bin_file"
}
# 转换图片
convert_image() {
local image_file="$1"
local colors="$2"
local output_name="$3"
local output_py_dir="$4"
local output_bin_dir="$5"
print_info "转换图片: $(basename "$image_file") (颜色深度: $colors)"
local temp_py="$output_py_dir/${output_name}.py"
local bin_file="$output_bin_dir/${output_name}.img"
# 检查imgtobitmap.py是否存在
if [ ! -f "$PROJECT_ROOT/utils/imgtobitmap.py" ]; then
print_warn "imgtobitmap.py 不存在,跳过图片转换"
return
fi
# 步骤1: 转换为Python模块
print_info " 步骤1: 将图片转换为Python模块..."
python3 "$PROJECT_ROOT/utils/imgtobitmap.py" "$image_file" "$colors" > "$temp_py"
# 步骤2: 转换为二进制文件
print_info " 步骤2: 将Python模块转换为二进制文件..."
python3 "$PROJECT_ROOT/utils/image2bin.py" "$temp_py" "$bin_file"
print_info " 图片转换完成: $bin_file"
}
# 主转换函数
main() {
print_info "开始转换资源..."
print_info "输出目录: $OUTPUT_DIR"
# 初始化字体配置文件列表
TEMP_FONT_CONFIG_FILE="/tmp/font_configs.$$"
# 加载字体配置
load_font_configs
# 显示图片配置
show_image_configs
# 转换字体
if [ ! -f "$TEMP_FONT_CONFIG_FILE" ] || [ ! -s "$TEMP_FONT_CONFIG_FILE" ]; then
print_warn "没有找到任何字体配置,跳过字体转换"
else
while IFS='|' read -r font_name characters font_file font_size output_name description; do
# 只在字体文件存在时转换
if [ -f "$font_file" ]; then
convert_font "$font_file" "$font_size" "$characters" "$output_name" "$OUTPUT_DIR/py" "$OUTPUT_DIR/bin"
else
print_warn "字体文件不存在: $font_file"
fi
done < "$TEMP_FONT_CONFIG_FILE"
fi
# 检查并转换图片
local image_files_exist=false
# 遍历assets目录下的所有图片文件
for image_file in "$ASSETS_DIR"/*; do
if [ -f "$image_file" ]; then
local file_name=$(basename "$image_file")
if is_image_file "$file_name"; then
image_files_exist=true
break
fi
fi
done
if [ "$image_files_exist" = false ]; then
print_warn "assets目录下没有找到支持的图片文件跳过图片转换"
else
print_info "开始转换图片文件..."
# 遍历assets目录下的所有图片文件
for image_file in "$ASSETS_DIR"/*; do
if [ -f "$image_file" ]; then
local file_name=$(basename "$image_file")
if is_image_file "$file_name"; then
local image_name="${file_name%.*}"
local colors=$(get_image_colors "$file_name")
convert_image "$image_file" "$colors" "$image_name" "$OUTPUT_DIR/py" "$OUTPUT_DIR/bin"
fi
fi
done
fi
# 显示输出文件信息
print_info "转换完成! 生成的文件:"
echo -e "\n${GREEN}=== 二进制文件 ===${NC}"
ls -lh "$OUTPUT_DIR/bin"/*.img "$OUTPUT_DIR/bin"/*.font 2>/dev/null || print_warn "没有生成二进制文件"
echo -e "\n${GREEN}=== 信息文件 ===${NC}"
ls -lh "$OUTPUT_DIR/py"/*.info 2>/dev/null || print_warn "没有生成信息文件"
echo -e "\n${GREEN}=== Python模块 ===${NC}"
ls -lh "$OUTPUT_DIR/py"/*.py 2>/dev/null || print_warn "没有生成Python模块"
print_info "转换脚本执行完成!"
}
# 显示帮助信息
show_help() {
echo "用法: $0 [选项]"
echo ""
echo "选项:"
echo " -h, --help 显示此帮助信息"
echo ""
echo "此脚本将转换以下资源:"
echo " 1. 字体 - 根据assets/fonts/fonts.txt配置转换为二进制字体文件"
echo " 2. 图片 - 遍历assets目录下所有支持的图片文件转换为二进制图片文件"
echo ""
echo "输入文件应位于: $ASSETS_DIR"
echo "字体配置文件应位于: $FONTS_DIR/fonts.txt"
echo "输出文件将保存到:"
echo " - Python模块: $OUTPUT_DIR/py"
echo " - 二进制文件: $OUTPUT_DIR/bin"
echo ""
echo "图片特殊命名规则:"
echo " - 默认使用 $DEFAULT_IMAGE_COLORS 色深度"
echo " - 如果图片名以 bx_N_ 开头,则使用 N 色深度"
echo " 例如: bx_2_icon.png 将使用 2 色深度"
echo ""
echo "fonts.json 配置格式 (JSON):"
echo " {"
echo " \"fonts\": ["
echo " {"
echo " \"name\": \"小字体\","
echo " \"file\": \"NotoSansSC-Regular.otf\","
echo " \"size\": 14,"
echo " \"chars\": \"你好世界...ABCD...\","
echo " \"output\": \"small_font\","
echo " \"description\": \"用于基本UI元素的小号字体\""
echo " },"
echo " {"
echo " \"name\": \"中字体\","
echo " \"file\": \"NotoSansSC-Regular.otf\","
echo " \"size\": 18,"
echo " \"chars\": \"你好今天天气...ABCD...\","
echo " \"output\": \"medium_font\","
echo " \"description\": \"用于主要文本显示的中号字体\""
echo " }"
echo " ]"
echo " }"
}
# 解析命令行参数
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
exit 0
;;
*)
print_error "未知选项: $1"
show_help
exit 1
;;
esac
shift
done
# 清理临时文件
cleanup() {
if [ -f "$TEMP_FONT_CONFIG_FILE" ]; then
rm -f "$TEMP_FONT_CONFIG_FILE"
fi
}
# 注册清理函数
trap cleanup EXIT
# 检查依赖并执行转换
check_dependencies
main
python3 font2bitmap.py -s "$CN_TEXT" $OTF_FONT 22 > $SRC_DIR/cn-22x24.py
python3 pyfont_to_bin.py $SRC_DIR/cn-22x24.py $DST_DIR/cn-22x24.bfont

View File

@@ -1,151 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
font2bin.py - Convert font modules to binary files for streaming
This script converts Python font modules (like those created by font2bitmap)
to binary files that can be streamed from the file system. This significantly
reduces RAM usage on memory-constrained devices like ESP8266.
Usage:
python font2bin.py <font_module> <output_binary>
Example:
python font2bin.py proverbs_font font_data.font
"""
import importlib.util
import os
import struct
import sys
from pathlib import Path
def read_font_module(module_path):
"""Load and extract font data from a Python font module"""
try:
# Load the module
spec = importlib.util.spec_from_file_location("font_module", module_path)
font_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(font_module)
# Extract font metadata and data
font_data = {
"map": font_module.MAP,
"bpp": getattr(font_module, "BPP", 1),
"height": font_module.HEIGHT,
"max_width": font_module.MAX_WIDTH,
"widths": bytes(font_module._WIDTHS),
"offsets": bytes(font_module._OFFSETS),
"bitmaps": bytes(font_module._BITMAPS),
"offset_width": getattr(font_module, "OFFSET_WIDTH", 2),
}
return font_data
except Exception as e:
print(f"Error loading font module: {e}")
return None
def write_binary_file(font_data, output_path):
"""Write font data to a binary file with a specific format"""
try:
with open(output_path, "wb") as f:
# Write header information
f.write(b"FONT") # Magic number
f.write(struct.pack("B", font_data["bpp"])) # Bits per pixel
f.write(struct.pack("H", font_data["height"])) # Font height
f.write(struct.pack("H", font_data["max_width"])) # Maximum width
f.write(
struct.pack("B", font_data["offset_width"])
) # Offset width in bytes
# Write character map
map_bytes = font_data["map"].encode("utf-8")
f.write(struct.pack("H", len(map_bytes))) # Map length
f.write(map_bytes)
# Write widths
f.write(struct.pack("H", len(font_data["widths"]))) # Widths length
f.write(font_data["widths"])
# Write offsets
f.write(struct.pack("I", len(font_data["offsets"]))) # Offsets length
f.write(font_data["offsets"])
# Write bitmaps
f.write(struct.pack("I", len(font_data["bitmaps"]))) # Bitmaps length
f.write(font_data["bitmaps"])
print(f"Successfully wrote font data to {output_path}")
print(f"File size: {os.path.getsize(output_path)} bytes")
return True
except Exception as e:
print(f"Error writing binary file: {e}")
return False
def create_info_file(font_data, output_path):
"""Create a text file with font information"""
# Extract the base filename without extension
base_name = os.path.splitext(os.path.basename(output_path))[0]
# Get the py directory path (assuming output_path is in bin directory)
bin_dir = os.path.dirname(output_path)
py_dir = os.path.join(os.path.dirname(bin_dir), "py")
info_path = os.path.join(py_dir, base_name + ".info")
try:
with open(info_path, "w") as f:
f.write(f"Font Information\n")
f.write(f"==============\n")
f.write(f"BPP: {font_data['bpp']}\n")
f.write(f"Height: {font_data['height']}\n")
f.write(f"Max Width: {font_data['max_width']}\n")
f.write(f"Offset Width: {font_data['offset_width']}\n")
f.write(f"Character Count: {len(font_data['map'])}\n")
f.write(f"Widths Size: {len(font_data['widths'])} bytes\n")
f.write(f"Offsets Size: {len(font_data['offsets'])} bytes\n")
f.write(f"Bitmaps Size: {len(font_data['bitmaps'])} bytes\n")
f.write(
f"Total Size: {len(font_data['widths']) + len(font_data['offsets']) + len(font_data['bitmaps'])} bytes\n"
)
print(f"Font information written to {info_path}")
return True
except Exception as e:
print(f"Error writing info file: {e}")
return False
def main():
if len(sys.argv) != 3:
print("Usage: python font2bin.py <font_module> <output_binary>")
print("Example: python font2bin.py proverbs_font font_data.font")
return 1
font_module_path = sys.argv[1]
output_path = sys.argv[2]
# Check if input file exists
if not os.path.exists(font_module_path):
print(f"Error: Font module not found at {font_module_path}")
return 1
# Load font data
font_data = read_font_module(font_module_path)
if font_data is None:
return 1
# Write binary file
if not write_binary_file(font_data, output_path):
return 1
# Create info file
if not create_info_file(font_data, output_path):
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

268
utils/hershey_to_pyfont.py Normal file
View File

@@ -0,0 +1,268 @@
#!/usr/bin/env python3
"""
Convert Hershey font data to python module.
Usage: hershey_to_py.py <glyph_file> [map_file]
The glyph_file (hf) is the Hershey font data file. The map_file (hmp) is an optional file that maps
the Hershey font data to a character set. The hershey_to_py.py script is compatible with the output
from my fork of LingDong's ttf2hershey python2 program available from my github repository at
https://github.com/russhughes/ttf2hershey. Not all TrueType fonts can be converted. Some may
result in a font with out-of-order or missing characters.
A Hershey font file is a text file with the following format:
Optional header lines:
# WIDTH = 40 width of the font
# HEIGHT = 45 height of the font
# FIRST = 32 first character in the font
# LAST = 127 last character in the font
Comment lines start with a # and are ignored with the exception of the optional header lines.
Glyph data lines have the following format:
Bytes 1-5: The character number
Bytes 6-8: The number of vector pairs in the glyph
Bytes 9: left hand position
Bytes 10: right hand position
Bytes 11+: The vector data as a string of characters, 2 characters per vector.
Vector values are relative to the ascii value of 'R'. A value of " R" non-drawing move to operation.
Example:
45 6JZLBXBXFLFLB
Character number: 45 (ASCII '-')
Number of vectors: 6
Left hand position: J (ascii value 74 - 82 = -8)
Right hand position: Z (ascii value 90 - 82 = 8)
Vector data: LBXBXFLFLB
The vector data is interpreted as follows:
LB - Line to (-6, -16)
XB - Line to (6, -16)
XF - Line to (6, -12)
LF - Line to (-6, -12)
LB - Line to (-6, -16)
A Hershey Map file is a text file with the following format:
Comment lines start with a # and are ignored.
Map data lines have the following format:
Number of the first glyph to include in the font followed by space and the number of the last glyph
in the font. If the last glyph is 0 then only the first glyph is included.
Example:
32 64
65 127
"""
import argparse
import re
from struct import pack
class HersheyFont:
"""
Hershey font data
"""
def __init__(self, width=40, height=45, first=32, last=127, glyphs=None):
self.width = width
self.height = height
self.first = first
self.last = last
self.glyphs = glyphs or {}
class Glyph:
"""
Glyph data
"""
def __init__(self, num, vectors, count):
self.num = num
self.vectors = vectors
self.count = count
def parse_line(keyword_dict, line):
"""
Perform regex search against all defined regexes and
return the key and match result from the first match.
"""
for key, rx in keyword_dict.items():
match = rx.search(line)
if match:
return key, match
return None, None
HF_KEYWORDS = {
'glyph': re.compile(r'^(?P<num>[0-9 ]{5})(?P<length>[0-9 ]{3})(?P<vectors>.*)$'),
'width': re.compile(r'^# WIDTH = (?P<width>\d+)$'),
'height': re.compile(r'^# HEIGHT = (?P<height>\d+)$'),
'first': re.compile(r'^# FIRST = (?P<first>\d+)$'),
'last': re.compile(r'^# LAST = (?P<last>\d+)$')}
def hershey_load(glyph_file_name, map_file_name=None):
"""
Load Hershey font, optionally using a map file.
"""
glyphs = {}
font = []
width = 40
height = 45
first = 32
last = 127
# Read the glyphs file
with open(glyph_file_name, "r") as file:
for line in file:
key, glyph_data = parse_line(HF_KEYWORDS, line.rstrip())
if key == 'glyph':
num = int(glyph_data['num'])
if map_file_name is None:
font.append(
Glyph(num, glyph_data['vectors'], int(glyph_data['length'])-1))
else:
glyphs[num] = Glyph(
num, glyph_data['vectors'], int(glyph_data['length'])-1)
elif key == 'width':
width = int(glyph_data['width'])
elif key == 'height':
height = int(glyph_data['height'])
elif key == 'first':
first = int(glyph_data['first'])
elif key == 'last':
last = int(glyph_data['last'])
# Read the map file if one was specified
if map_file_name is not None:
map_line = re.compile(r'(?P<begin>\d+)\s+(?P<end>\d+)$')
with open(map_file_name, "r") as file:
for line in file:
if line[0] == '#':
continue
match = map_line.search(line.rstrip())
if match:
begin = int(match['begin'])
end = int(match['end'])
if end > 0:
font.extend(glyphs[glyph_num] for glyph_num in range(begin, end + 1))
else:
font.append(glyphs[begin])
return HersheyFont(width, height, first, last, font)
def write_font(font):
"""
Write _fronts.
"""
font_data = bytes()
for glyph in font.glyphs:
count = glyph.count
f_c = bytearray(count.to_bytes(1, byteorder='little'))
f_v = bytearray(glyph.vectors, 'utf-8')
font_data += f_c + f_v
print("_font =\\")
print("b'", sep='', end='')
count = 0
for byte in (font_data):
print(f'\\x{byte:02x}', sep='', end='')
count += 1
if count == 15:
print("'\\\nb'", sep='', end='')
count = 0
print("'")
def write_offsets(offsets):
"""
Write the 16 bit integer table to the start of the vector data for each
glyph in the font.
"""
index_data = bytes()
for offset in offsets:
index_data += bytearray(pack('H', offset))
print("\n_index =\\")
print("b'", sep='', end='')
for count, byte in enumerate(index_data):
if count > 0 and count % 15 == 0:
print("'\\\nb'", sep='', end='')
print(f'\\x{byte:02x}', sep='', end='')
print("'")
def create_module(font, font_type="vector"):
"""
Create python module from Hershey glyphs, optionally using a map file.
"""
print(f"FONT_TYPE = {font_type}")
print(f"FIRST = {font.first}")
print(f"LAST = {font.last}")
print(f"WIDTH = {font.width}")
print(f"HEIGHT = {font.height}\n")
write_font(font)
offsets = []
offset = 0
for glyph in font.glyphs:
offsets.append(offset)
offset += len(glyph.vectors) + 1
write_offsets(offsets)
print("\nFONT = memoryview(_font)")
print("INDEX = memoryview(_index)\n")
parser = argparse.ArgumentParser(
prog='hershey2py',
description=('''
Convert hershey format font to python module for use
with the draw method in the st7789 and ili9342 drivers.'''))
parser.add_argument(
'hershey_file',
type=str,
help='name of hershey font file to convert.')
parser.add_argument(
'map_file',
type=str,
nargs='?',
default=None,
help='Hershey glyph map file.')
args = parser.parse_args()
font = hershey_load(args.hershey_file, args.map_file)
create_module(font)

View File

@@ -1,159 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
image2bin.py - Convert image bitmap modules to binary streaming format
This script converts Python image modules (created by imgtobitmap.py) to
binary files that can be streamed from the file system. This is especially
useful for large images or animations on memory-constrained devices.
Usage:
python image2bin.py <image_module> <output_binary>
Example:
python image2bin.py logo.py logo.img
"""
import importlib.util
import os
import struct
import sys
def read_image_module(module_path):
"""Load and extract image data from a Python image module"""
try:
# Load the module
spec = importlib.util.spec_from_file_location("image_module", module_path)
image_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(image_module)
# Extract image metadata and data
bitmap_obj = image_module._bitmap
try:
bitmap_bytes = bytes(bitmap_obj)
except Exception as e:
print(f"Error converting bitmap to bytes: {e}", file=sys.stderr)
bitmap_bytes = b"\x00" # Fallback
image_data = {
"width": image_module.WIDTH,
"height": image_module.HEIGHT,
"colors": image_module.COLORS,
"bpp": image_module.BPP,
"palette": getattr(image_module, "PALETTE", []),
"bitmap": bitmap_bytes,
}
return image_data
except Exception as e:
print(f"Error loading image module: {e}")
return None
def write_binary_file(image_data, output_path):
"""Write image data to a binary file with a specific format"""
try:
with open(output_path, "wb") as f:
# Write header information
f.write(b"IMG ") # Magic number ("IMG " with trailing space)
f.write(struct.pack("H", image_data["width"])) # Image width
f.write(struct.pack("H", image_data["height"])) # Image height
# Limit colors to ubyte range (0-255)
colors_val = min(image_data["colors"], 255)
f.write(struct.pack("B", colors_val)) # Number of colors
f.write(struct.pack("B", image_data["bpp"])) # Bits per pixel
# Write palette
if image_data["palette"]:
palette_length = min(
len(image_data["palette"]), 255
) # Limit to ubyte range
f.write(struct.pack("B", palette_length)) # Palette length
for i in range(palette_length):
f.write(
struct.pack("H", image_data["palette"][i])
) # RGB565 color value
else:
f.write(struct.pack("B", 0)) # Zero palette length
# Write bitmap data
f.write(struct.pack("I", len(image_data["bitmap"]))) # Bitmap length
f.write(image_data["bitmap"])
print(f"Successfully wrote image data to {output_path}")
print(f"File size: {os.path.getsize(output_path)} bytes")
return True
except Exception as e:
print(f"Error writing binary file: {e}")
return False
def create_info_file(image_data, output_path):
"""Create a text file with image information"""
# Extract the base filename without extension
base_name = os.path.splitext(os.path.basename(output_path))[0]
# Get the py directory path (assuming output_path is in bin directory)
bin_dir = os.path.dirname(output_path)
py_dir = os.path.join(os.path.dirname(bin_dir), "py")
info_path = os.path.join(py_dir, base_name + ".info")
try:
with open(info_path, "w") as f:
f.write(f"Image Information\n")
f.write(f"================\n")
f.write(f"Width: {image_data['width']} pixels\n")
f.write(f"Height: {image_data['height']} pixels\n")
f.write(f"Colors: {image_data['colors']}\n")
f.write(f"BPP: {image_data['bpp']}\n")
if image_data["palette"]:
f.write(f"Palette: {len(image_data['palette'])} colors\n")
else:
f.write(f"Palette: None\n")
f.write(f"Bitmap Size: {len(image_data['bitmap'])} bytes\n")
f.write(
f"Total File Size: {os.path.getsize(output_path.replace('.info', '.img'))} bytes\n"
)
print(f"Image information written to {info_path}")
return True
except Exception as e:
print(f"Error writing info file: {e}")
return False
def main():
if len(sys.argv) != 3:
print("Usage: python image2bin.py <image_module> <output_binary>")
print("Example: python image2bin.py logo.py logo.img")
print(
"\nThis converts Python image modules (from imgtobitmap.py) to streaming format."
)
return 1
image_module_path = sys.argv[1]
output_path = sys.argv[2]
# Check if input file exists
if not os.path.exists(image_module_path):
print(f"Error: Image module not found at {image_module_path}")
return 1
# Load image data
image_data = read_image_module(image_module_path)
if image_data is None:
return 1
# Write binary file
if not write_binary_file(image_data, output_path):
return 1
# Create info file
if not create_info_file(image_data, output_path):
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,156 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
imgtobitmap.py - Convert image files to Python bitmap modules for st7789_mpy
This script converts image files (PNG, JPG, etc.) to Python bitmap modules
compatible with the st7789_mpy library and the image2bin.py conversion script.
The output modules can be used with st7789.bitmap() method.
Usage:
python imgtobitmap.py <image_file> <colors>
Example:
python imgtobitmap.py logo.png 4 > logo.py
"""
import argparse
import sys
from PIL import Image
def convert_image_to_bitmap(image_path, colors):
"""
Convert an image file to a Python bitmap module
Args:
image_path (str): Path to the input image file
colors (int): Number of colors in the output (must be a power of 2, max 256)
"""
try:
# Validate colors
if colors <= 0 or (colors & (colors - 1)) != 0 or colors > 256:
print(f"Error: Colors must be a power of 2 between 1 and 256, got {colors}")
return False
# Calculate bits per pixel
bpp = 0
while (1 << bpp) < colors:
bpp += 1
# Load image
img = Image.open(image_path)
# Convert to palette image with specified number of colors
img = img.convert("P", palette=Image.Palette.ADAPTIVE, colors=colors)
palette = img.getpalette()
# Get actual number of colors
palette_colors = len(palette) // 3
bits_required = palette_colors.bit_length()
if bits_required < bpp:
print(
f"\nNOTE: Quantization reduced colors to {palette_colors} from the {colors} "
f"requested, reconverting using {bits_required} bit per pixel could save memory.\n",
file=sys.stderr,
)
# For all the colors in the palette
colors_hex = []
for color in range(palette_colors):
# Get RGB values and convert to 565
color565 = (
((palette[color * 3] & 0xF8) << 8)
| ((palette[color * 3 + 1] & 0xFC) << 3)
| (palette[color * 3 + 2] & 0xF8) >> 3
)
# Swap bytes in 565
color_val = ((color565 & 0xFF) << 8) + ((color565 & 0xFF00) >> 8)
# Append byte swapped 565 color to colors
colors_hex.append(f"{color_val:04x}")
# Generate bitmap data
image_bitstring = ""
# Run through the image and create a string with the ascii binary
# representation of the color of each pixel.
for y in range(img.height):
for x in range(img.width):
pixel = img.getpixel((x, y))
bstring = "".join(
"1" if (pixel & (1 << bit - 1)) else "0"
for bit in range(bpp, 0, -1)
)
image_bitstring += bstring
bitmap_bits = len(image_bitstring)
max_colors = 1 << bpp
# Create python source with image parameters
print(f"HEIGHT = {img.height}")
print(f"WIDTH = {img.width}")
print(f"COLORS = {max_colors}")
print(f"BPP = {bpp}")
print("PALETTE = [", sep="", end="")
for color, rgb in enumerate(colors_hex):
if color:
print(",", sep="", end="")
print(f"0x{rgb}", sep="", end="")
print("]")
# Run though image bit string 8 bits at a time
# and create python array source for memoryview
print("_bitmap =\\", sep="")
print("b'", sep="", end="")
for i in range(0, bitmap_bits, 8):
# Limit line length for readability
if i and i % (16 * 8) == 0:
print("'\\\nb'", end="", sep="")
value = image_bitstring[i : i + 8]
color = int(value, 2)
print(f"\\x{color:02x}", sep="", end="")
print("'")
return True
except Exception as e:
print(f"Error processing image: {e}", file=sys.stderr)
return False
def main():
parser = argparse.ArgumentParser(
prog="imgtobitmap",
description="Convert image file to python module for use with bitmap method.",
)
parser.add_argument("image_file", help="Name of file containing image to convert")
parser.add_argument(
"bits_per_pixel",
type=int,
choices=range(1, 9),
default=1,
metavar="bits_per_pixel",
help="The number of bits to use per pixel (1..8)",
)
args = parser.parse_args()
bits = args.bits_per_pixel
colors = 1 << bits
return 0 if convert_image_to_bitmap(args.image_file, colors) else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,220 +0,0 @@
#!/usr/bin/env python3
'''
monofont2bitmap.py
Convert characters from monospace truetype fonts to a python bitmap
for use with the bitmap method in the st7789 and ili9342 drivers.
positional arguments:
font_file Name of font file to convert.
font_size Size of font to create bitmaps from.
bits_per_pixel The number of bits (1..8) to use per pixel.
optional arguments:
-h, --help show this help message and exit
-f FOREGROUND, --foreground FOREGROUND
Foreground color of characters.
-b BACKGROUND, --background BACKGROUND
Background color of characters.
character selection:
Characters from the font to include in the bitmap.
-c CHARACTERS, --characters CHARACTERS
integer or hex character values and/or ranges to
include.
For example: "65, 66, 67" or "32-127" or
"0x30-0x39, 0x41-0x5a"
-s STRING, --string STRING
String of characters to include For example:
"1234567890-."
'''
import sys
import shlex
from PIL import Image, ImageFont, ImageDraw
import argparse
image_bitstring = ''
def to_int(str):
return int(str, base=16) if str.startswith("0x") else int(str)
def get_characters(str):
return ''.join(chr(b) for a in [
(lambda sub: range(sub[0], sub[-1] + 1))
(list(map(to_int, ele.split('-'))))
for ele in str.split(',')] for b in a)
def process_char(img, bits):
global image_bitstring
# Run through the image and create a string with the ascii binary
# representation of the color of each pixel.
for y in range(img.height):
for x in range(img.width):
pixel = img.getpixel((x, y))
color = pixel
bit_string = ''.join(
'1' if (color & (1 << bit - 1)) else '0'
for bit in range(bits, 0, -1)
)
image_bitstring += bit_string
def main():
parser = argparse.ArgumentParser(
prog='font2bitmap',
description=('''
Convert characters from monospace truetype fonts to a
python bitmap for use with the bitmap method in the
st7789 and ili9342 drivers.'''))
parser.add_argument(
'font_file',
help='Name of font file to convert.')
parser.add_argument(
'font_size',
type=int,
default=8,
help='Size of font to create bitmaps from.')
parser.add_argument(
'bits_per_pixel',
type=int,
choices=range(1, 9),
default=1,
metavar='bits_per_pixel',
help='The number of bits (1..8) to use per pixel.')
parser.add_argument(
'-f', '--foreground',
default='white',
help='Foreground color of characters.')
parser.add_argument(
'-b', '--background',
default='black',
help='Background color of characters.')
group = parser.add_argument_group(
'character selection',
'Characters from the font to include in the bitmap.')
excl = group.add_mutually_exclusive_group()
excl.add_argument(
'-c', '--characters',
help='''integer or hex character values and/or ranges to include.
For example: "65, 66, 67" or "32-127" or "0x30-0x39, 0x41-0x5a"''')
excl.add_argument(
'-s', '--string',
help='''String of characters to include
For example: "1234567890-."''')
args = parser.parse_args()
bpp = args.bits_per_pixel
font_file = args.font_file
font_size = args.font_size
if args.string is None:
characters = get_characters(args.characters)
else:
characters = args.string
foreground = args.foreground
background = args.background
# load font and get size of characters string in pixels
font = ImageFont.truetype(font_file, font_size)
size = font.getsize(characters)
# create image large enough to all characters
im = Image.new('RGB', size, color=background)
# draw all specified characters in the image
draw = ImageDraw.Draw(im)
draw.text((0, 0), characters, font=font, color=foreground)
# convert image to a palletized image with the requested color depth
bpp_im = im.convert(mode='P', palette=Image.ADAPTIVE, colors=1 << bpp)
palette = bpp_im.getpalette()
# convert all characters into a ascii bit string
ofs = 0
for char in characters:
char_size = font.getsize(char)
crop = (ofs, 0, ofs + char_size[0], size[1])
char_im = bpp_im.crop(crop)
process_char(char_im, bpp)
ofs += char_size[0]
bitmap_bits = len(image_bitstring)
char_map = characters.replace('\\', '\\\\').replace('"', '\\"')
cmdline = " ".join(map(shlex.quote, sys.argv))
# Create python source
print('# coding=UTF8')
print(f'# Converted from {font_file} using:')
print(f'# {cmdline}')
print()
print(f'HEIGHT = {char_im.height}')
print(f'WIDTH = {char_im.width}')
print(f'COLORS = {1 << bpp}')
print(f'BITMAPS = {len(characters)}')
print(f'MAP = "{char_map}"')
print(f'BPP = {bpp}')
print('PALETTE = [', sep='', end='')
# For all the colors in the palette
colors = []
for color in range(1 << bpp):
# get rgb values and convert to 565
color565 = (
((palette[color*3] & 0xF8) << 8) |
((palette[color*3+1] & 0xFC) << 3) |
((palette[color*3+2] & 0xF8) >> 3))
# swap bytes in 565
color = ((color565 & 0xff) << 8) + ((color565 & 0xff00) >> 8)
# append byte swapped 565 color to colors
colors.append(f'{color:04x}')
# write color palette colors
for color, rgb in enumerate(colors):
if color:
print(', ', sep='', end='')
print(f'0x{rgb}', sep='', end='')
print(']')
# Run though image bit string 8 bits at a time
# and create python array source for memoryview
print('_BITMAP =\\', sep='')
print(' b\'', sep='', end='')
for i in range(0, bitmap_bits, 8):
if i and i % (16 * 8) == 0:
print("'\\\n b'", end='', sep='')
value = image_bitstring[i:i+8]
color = int(value, 2)
print(f'\\x{color:02x}', sep='', end='')
print("'\\\n b'", end='', sep='')
print("'\n\nBITMAP = memoryview(_BITMAP)")
main()

176
utils/png_to_jpg.py Normal file
View File

@@ -0,0 +1,176 @@
import os
import sys
from PIL import Image
def png_to_jpg(input_dir=None, bg_color=(0, 0, 0), quality=100):
"""
将指定目录下的所有PNG图片转换为JPG格式
Args:
input_dir: 输入目录,默认为当前目录
bg_color: 背景颜色,默认为黑色(0, 0, 0)
quality: JPG质量默认为100
"""
# 如果未指定输入目录,使用当前目录
if input_dir is None:
input_dir = os.getcwd()
# 确保目录存在
if not os.path.exists(input_dir):
print(f"错误: 目录 '{input_dir}' 不存在")
return False
# 获取所有PNG文件
png_files = [f for f in os.listdir(input_dir) if f.lower().endswith('.png')]
if not png_files:
print(f"在目录 '{input_dir}' 中未找到PNG文件")
return True
print(f"找到 {len(png_files)} 个PNG文件")
# 转换每个PNG文件
successful = 0
failed = 0
for png_file in png_files:
try:
# 构建完整的文件路径
png_path = os.path.join(input_dir, png_file)
# 打开PNG图像
img = Image.open(png_path)
# 如果图像有alpha通道透明通道处理透明部分
if img.mode in ('RGBA', 'LA', 'P'):
# 创建新的RGB图像背景为指定颜色
rgb_img = Image.new('RGB', img.size, bg_color)
# 如果是P模式调色板先转换为RGBA
if img.mode == 'P':
img = img.convert('RGBA')
# 将原图像粘贴到新背景上
if img.mode == 'RGBA':
# RGBA图像需要分离alpha通道
rgb_img.paste(img, mask=img.split()[3]) # 使用alpha通道作为蒙版
elif img.mode == 'LA':
# LA模式L是亮度A是alpha
rgb_img.paste(img.convert('RGBA'), mask=img.split()[1])
else:
# 如果没有alpha通道直接转换为RGB
rgb_img = img.convert('RGB')
# 生成输出文件名(将.png替换为.jpg
jpg_file = os.path.splitext(png_file)[0] + '.jpg'
jpg_path = os.path.join(input_dir, jpg_file)
# 保存为JPG
rgb_img.save(jpg_path, 'JPEG', quality=quality, optimize=True)
print(f"✓ 转换成功: {png_file} -> {jpg_file}")
successful += 1
except Exception as e:
print(f"✗ 转换失败 {png_file}: {str(e)}")
failed += 1
print(f"\n转换完成: 成功 {successful} 个, 失败 {failed}")
return failed == 0
def main():
"""主函数,处理命令行参数"""
import argparse
parser = argparse.ArgumentParser(
description='将PNG图片转换为JPG格式透明背景转为指定颜色',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
%(prog)s # 转换当前目录所有PNG黑色背景质量100
%(prog)s -d /path/to/images # 转换指定目录
%(prog)s -c white -q 90 # 白色背景质量90
%(prog)s -c 255,255,255 # 使用RGB值指定白色背景
"""
)
parser.add_argument(
'-d', '--directory',
default='.',
help='输入目录,默认为当前目录'
)
parser.add_argument(
'-c', '--color',
default='black',
help='背景颜色支持颜色名或RGB值"white""255,255,255"默认为black'
)
parser.add_argument(
'-q', '--quality',
type=int,
default=100,
choices=range(1, 101),
metavar='[1-100]',
help='JPG质量1-100默认为100'
)
parser.add_argument(
'-r', '--recursive',
action='store_true',
help='递归处理子目录(注意:本脚本当前版本不支持,已预留接口)'
)
args = parser.parse_args()
# 解析颜色参数
bg_color = (0, 0, 0) # 默认黑色
if args.color:
color_str = args.color.lower().strip()
# 预定义颜色
color_map = {
'black': (0, 0, 0),
'white': (255, 255, 255),
'red': (255, 0, 0),
'green': (0, 255, 0),
'blue': (0, 0, 255),
'transparent': None,
}
if color_str in color_map:
bg_color = color_map[color_str]
elif ',' in color_str:
# 尝试解析RGB值
try:
rgb = [int(x.strip()) for x in color_str.split(',')]
if len(rgb) == 3 and all(0 <= x <= 255 for x in rgb):
bg_color = tuple(rgb)
else:
print(f"警告: 颜色值无效,使用默认黑色")
except ValueError:
print(f"警告: 颜色格式无效,使用默认黑色")
else:
print(f"警告: 颜色 '{args.color}' 不被识别,使用默认黑色")
print(f"设置:")
print(f" 目录: {args.directory}")
print(f" 背景色: RGB{bg_color}")
print(f" 质量: {args.quality}")
print(f" 递归: {'' if args.recursive else ''}")
print("-" * 40)
# 执行转换
try:
success = png_to_jpg(args.directory, bg_color, args.quality)
return 0 if success else 1
except KeyboardInterrupt:
print("\n操作被用户中断")
return 130
except Exception as e:
print(f"错误: {str(e)}")
return 1
if __name__ == "__main__":
sys.exit(main())

495
utils/pyfont_to_bin.py Normal file
View File

@@ -0,0 +1,495 @@
#!/usr/bin/env python3
"""
Convert pyfont files to binary font formats (BFont, HFont, or RFont).
This script automatically detects the font type based on the input file characteristics
and converts it to the appropriate binary format:
- BFont (BitmapFont): For bitmap fonts with character maps and offsets
- HFont (HersheyFont): For vector fonts with stroke data
- RFont (RomFont): For fixed-width bitmap fonts with sequential character ranges
Usage: pyfont_to_bin.py <input_pyfont_file> [output_file_or_directory]
output_file_or_directory can be:
- A file path to specify the output file name and location
- A directory path to place the output file in that directory with auto-generated name
Binary Formats:
1. BFont (BitmapFont) format:
- 3 bytes: Magic number (0x42 0x46 0x54 = "BFT")
- 1 byte: Version (0x01T
- 1 byte: max_Width
- 1 byte: HEIGHT of the font
- 1 byte: bpp
- 1 byte: OFFSET_WIDTH (number of bytes for each offset)
- 2 bytes: Character count (big endian)
- N*2 bytes: Character map (character count bytes, 2 bytes per Unicode char)
- N*(1+OFFSET_WIDTH) bytes: WIDTH+Offset data
- M bytes: Bitmap data
2. HFont (HersheyFont) format:
- 3 bytes: Magic number (0x48 0x46 0x54 = "HFT")
- 1 byte: Version (0x01)
- 1 byte: WIDTH of the font
- 1 byte: HEIGHT of the font
- 1 byte: FIRST character code
- 1 byte: LAST character code
- 2 bytes: Maximum size of any character in bytes (big endian)
- N bytes: Index data
- M bytes: Font data (stroke vectors)
3. RFont (RomFont) format:
- 8 bytes: RFont header
- 3 bytes: Magic number (0x52 0x46 0x54 = "RFT")
- 1 byte: Version (0x01)
- 1 byte: WIDTH of the font
- 1 byte: HEIGHT of the font
- 1 byte: FIRST character code
- 1 byte: LAST character code
- N bytes: Character bitmap data
"""
import argparse
import os
import struct
import sys
# Magic numbers for different font formats
MAGIC_BFONT = b"BFT" # Bitmap Font Binary
MAGIC_HFONT = b"HFT" # Hershey Font Binary
MAGIC_RFONT = b"RFT" # Rom Font Binary
VERSION = 0x01
def detect_font_type(namespace, input_file):
"""
Detect the type of font based on file characteristics.
Returns one of: 'bfont', 'hfont', 'rfont'
"""
# Check for bitmap font format (proverbs/clock style)
if (
"MAP" in namespace
and "WIDTHS" in namespace
and "MAX_WIDTH" in namespace
and "OFFSETS" in namespace
and "BITMAPS" in namespace
):
return "bfont"
# Check for standard bitmap font format
if (
"WIDTH" in namespace
and "HEIGHT" in namespace
and "FIRST" in namespace
and "LAST" in namespace
and "FONT" in namespace
):
# Check if it has sequential bitmap data (_FONT)
if "INDEX" in namespace:
return "hfont"
else:
return "rfont"
# Unknown format
return None
def convert_to_bfont(namespace, input_file, output_file=None):
"""
Convert pyfont to BFont format (bitmap font with variable width characters)
"""
# Extract font parameters
char_map_str = namespace.get("MAP")
height = namespace.get("HEIGHT")
max_width = namespace.get("MAX_WIDTH")
width = max_width if max_width else height
bpp = namespace.get("BPP", 1) # Default to 1 BPP if not specified
widths = namespace.get("_WIDTHS")
offset_width = namespace.get("OFFSET_WIDTH", 2) # Default to 2 bytes
offsets = namespace.get("_OFFSETS")
bitmaps = namespace.get("_BITMAPS")
if None in (char_map_str, height, max_width, offsets, bitmaps):
return None
# Convert character map to bytes - use UTF-8 encoding for Unicode characters
# Since we need to store Unicode code points, we'll use 2 bytes per character
char_map = b""
for c in char_map_str:
# Pack each Unicode code point as 2 bytes (big endian)
char_map += struct.pack(">H", ord(c))
char_count = len(char_map_str)
# Handle memoryview objects
if offsets and hasattr(offsets, "tobytes"):
offsets = offsets.tobytes()
if bitmaps and hasattr(bitmaps, "tobytes"):
bitmaps = bitmaps.tobytes()
if widths and hasattr(widths, "tobytes"):
widths = widths.tobytes()
# Calculate the actual max width from widths data if available
if widths and len(widths) > 0:
actual_max_width = max(widths)
else:
actual_max_width = width
font_info = {
"width": actual_max_width, # Use actual max width for the header
"height": height,
"char_count": char_count,
"char_map": char_map,
"offset_width": offset_width,
"offsets": offsets,
"font_data": bitmaps,
"widths": widths, # Store WIDTH data
"bpp": bpp, # Store BPP value
}
# Update output filename if needed (only if it doesn't already include dimensions)
if output_file and not output_file.endswith(f"-{width}x{height}.bfont"):
base_name = os.path.splitext(os.path.basename(input_file))[0]
output_dir = os.path.dirname(output_file)
output_name = f"{base_name}-{width}x{height}.bfont"
output_file = os.path.join(output_dir, output_name)
# Write BFont file
write_bfont_file(font_info, output_file)
# Calculate merged data size
merged_data_size = font_info["char_count"] * (1 + font_info["offset_width"])
# Print font information
print(f"Bitmap Font Detected")
print(f" Source: {input_file}")
print(f" Output format: BFont ({output_file})")
print(f" Character count: {font_info['char_count']}")
print(f" Font dimensions: {font_info['width']}x{font_info['height']}")
print(f" BPP: {bpp}")
print(f" Font data size: {len(font_info['font_data'])} bytes")
print(f" Width+Offset data size: {merged_data_size} bytes")
print(f" Offset width: {font_info['offset_width']} bytes")
return True
def convert_to_hfont(namespace, input_file, output_file=None):
"""
Convert pyfont to HFont format (Hershey vector font)
"""
# Extract font parameters
first = namespace.get("FIRST")
last = namespace.get("LAST")
width = namespace.get("WIDTH", 0)
height = namespace.get("HEIGHT", 0)
# Check for Hershey-specific data
indices = namespace.get("_index")
font_data = namespace.get("_font")
# Handle memoryview objects
if indices and hasattr(indices, "tobytes"):
indices = indices.tobytes()
if font_data and hasattr(font_data, "tobytes"):
font_data = font_data.tobytes()
# Check if we have the required data
if indices is None or font_data is None:
print("Error: HFont requires _index and _font data")
return False
# Calculate character count
char_count = last - first + 1 if first is not None and last is not None else 0
# Calculate maximum character size
max_char_size = 0
if indices and font_data:
# Parse the index data to get offsets (2 bytes per entry)
offsets = []
for i in range(0, len(indices), 2):
offset = indices[i] + (indices[i + 1] << 8)
offsets.append(offset)
# Add the end offset
offsets.append(len(font_data))
# Calculate the size of each character
for i in range(len(offsets) - 1):
size = offsets[i + 1] - offsets[i]
if size > max_char_size:
max_char_size = size
# Update output filename if needed (only if it doesn't already include dimensions)
if output_file and not output_file.endswith(f"-{width}x{height}.hfont"):
base_name = os.path.splitext(os.path.basename(input_file))[0]
output_dir = os.path.dirname(output_file)
output_name = f"{base_name}-{width}x{height}.hfont"
output_file = os.path.join(output_dir, output_name)
# Write HFont file
with open(output_file, "wb") as f:
# Write magic number
f.write(MAGIC_HFONT)
# Write version
f.write(bytes([VERSION]))
# Write font metadata (each as a single byte)
f.write(bytes([width if width < 256 else 255])) # 1 byte: WIDTH
f.write(bytes([height if height < 256 else 255])) # 1 byte: HEIGHT
f.write(
bytes([first if first is not None else 0])
) # 1 byte: FIRST character code
f.write(bytes([last if last is not None else 0])) # 1 byte: LAST character code
# Write maximum character size (2 bytes, big endian)
f.write(struct.pack(">H", max_char_size))
# Write index data
if indices:
f.write(indices)
# Write font data
if font_data:
f.write(font_data)
# Print font information
print(f"Vector Font (Hershey Style) Detected")
print(f" Source: {input_file}")
print(f" Output format: HFont ({output_file})")
print(f" Character range: {first}-{last} ({char_count} chars)")
print(f" Font dimensions: {width}x{height}")
print(f" Font data size: {len(font_data) if font_data else 0} bytes")
print(f" Index data size: {len(indices) if indices else 0} bytes")
print(f" Maximum character size: {max_char_size} bytes")
return True
def convert_to_rfont(namespace, input_file, output_file=None):
"""
Convert pyfont to RFont format (RomFont, fixed-width bitmap font)
"""
# Extract font parameters
first = namespace.get("FIRST")
last = namespace.get("LAST")
width = namespace.get("WIDTH")
height = namespace.get("HEIGHT")
font_data = namespace.get("_FONT")
# Handle case where font_data is a memoryview
if font_data is not None and hasattr(font_data, "tobytes"):
font_data = font_data.tobytes()
if None in (first, last, width, height, font_data):
return False
# Calculate character count
char_count = last - first + 1
# Calculate bytes per character
bytes_per_line = (width + 7) // 8
bytes_per_char = bytes_per_line * height
# Update output filename if needed (only if it doesn't already include dimensions)
if output_file and not output_file.endswith(f"-{width}x{height}.rfont"):
base_name = os.path.splitext(os.path.basename(input_file))[0]
output_dir = os.path.dirname(output_file)
output_name = f"{base_name}-{width}x{height}.rfont"
output_file = os.path.join(output_dir, output_name)
# Write RFont file
with open(output_file, "wb") as f:
# Write header
f.write(MAGIC_RFONT) # 3 bytes: Magic number
f.write(bytes([VERSION])) # 1 byte: Version
# Verify that all values fit in a single byte
if max(first, last, width, height) > 255:
print(f"Warning: Font metadata values > 255 will be truncated to 255")
first = min(first, 255)
last = min(last, 255)
width = min(width, 255)
height = min(height, 255)
f.write(bytes([width])) # 1 byte: WIDTH
f.write(bytes([height])) # 1 byte: HEIGHT
f.write(bytes([first])) # 1 byte: FIRST character code
f.write(bytes([last])) # 1 byte: LAST character code
# Write font data
f.write(font_data)
# Print font information
print(f"Fixed-Width Bitmap Font Detected")
print(f" Source: {input_file}")
print(f" Output format: RFont ({output_file})")
print(f" Character range: {first}-{last} ({char_count} chars)")
print(f" Font dimensions: {width}x{height}")
print(f" Fixed character size: {bytes_per_char} bytes")
print(f" Total font data size: {len(font_data)} bytes")
return True
def write_bfont_file(font_info, output_file):
"""Write the BFont binary file"""
# Make sure the output directory exists
output_dir = os.path.dirname(output_file)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir)
with open(output_file, "wb") as f:
# Write magic number
f.write(MAGIC_BFONT)
# Write version
f.write(bytes([VERSION]))
# Write max_Width
f.write(bytes([font_info["width"]])) # 1 byte: max_Width
# Write HEIGHT of the font
f.write(bytes([font_info["height"]])) # 1 byte: HEIGHT
# Write bpp
f.write(bytes([font_info.get("bpp", 1)])) # 1 byte: bpp
# Write OFFSET_WIDTH (1 byte)
f.write(bytes([font_info["offset_width"]]))
# Write character count (2 bytes, big endian)
f.write(struct.pack(">H", font_info["char_count"]))
# Write character map (as packed Unicode code points)
f.write(font_info["char_map"])
# Write merged WIDTH+Offset data
widths = font_info.get("widths")
offsets = font_info["offsets"]
offset_width = font_info["offset_width"]
# If no widths data provided, use max_width for each character
if not widths:
widths = bytes([font_info["width"]] * font_info["char_count"])
# Merge width and offset data for each character
# Each character has 1 byte width followed by offset_width bytes offset
for i in range(font_info["char_count"]):
# Write width (1 byte)
f.write(bytes([widths[i]]))
# Write offset (offset_width bytes) directly from the offsets byte array
offset_start = i * offset_width
offset_end = offset_start + offset_width
f.write(offsets[offset_start:offset_end])
# Write bitmap data
f.write(font_info["font_data"])
return True
def convert_to_binary(input_file, output_file=None):
"""Convert a pyfont file to binary format, auto-detecting the format"""
try:
# Check if output_file is a directory
if output_file and os.path.isdir(output_file):
output_dir = output_file
output_file = None
else:
output_dir = None
# Generate default output file name if not provided or directory specified
if output_file is None:
base_name = os.path.splitext(os.path.basename(input_file))[0]
if output_dir:
output_path = output_dir
else:
output_path = os.path.dirname(input_file)
output_name = "" # Will be set by each conversion function
# Parse the pyfont file using exec()
namespace = {}
with open(input_file, "r") as f:
exec(f.read(), namespace)
# Detect the font type
font_type = detect_font_type(namespace, input_file)
# Generate default output file name if not provided or directory specified
if output_file is None:
base_name = os.path.splitext(os.path.basename(input_file))[0]
if output_dir:
output_path = output_dir
else:
output_path = os.path.dirname(input_file)
# Create output file name based on detected format
if font_type == "bfont":
# Get width and height for filename
if "MAX_WIDTH" in namespace and "HEIGHT" in namespace:
width = namespace["MAX_WIDTH"]
height = namespace["HEIGHT"]
else:
width = namespace.get("WIDTH", 0)
height = namespace.get("HEIGHT", 0)
output_name = f"{base_name}-{width}x{height}.bfont"
elif font_type == "hfont":
width = namespace.get("WIDTH", 0)
height = namespace.get("HEIGHT", 0)
output_name = f"{base_name}-{width}x{height}.hfont"
elif font_type == "rfont":
width = namespace.get("WIDTH", 0)
height = namespace.get("HEIGHT", 0)
output_name = f"{base_name}-{width}x{height}.rfont"
else:
output_name = f"{base_name}.bin"
output_file = os.path.join(output_path, output_name)
# Convert based on the detected format
if font_type == "bfont":
return convert_to_bfont(namespace, input_file, output_file)
elif font_type == "hfont":
return convert_to_hfont(namespace, input_file, output_file)
elif font_type == "rfont":
return convert_to_rfont(namespace, input_file, output_file)
else:
print(f"Error: Unable to detect font type for {input_file}")
print(
"Available variables:",
[k for k in namespace.keys() if not k.startswith("__")],
)
return False
except Exception as e:
print(f"Error converting {input_file}: {str(e)}")
import traceback
traceback.print_exc()
return False
def main():
parser = argparse.ArgumentParser(
description="Convert pyfont files to binary font formats (BFont, HFont, or RFont)"
)
parser.add_argument("input_file", help="Input pyfont file path")
parser.add_argument(
"output", nargs="?", help="Output file or directory path (optional)"
)
args = parser.parse_args()
if not convert_to_binary(args.input_file, args.output):
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -17,13 +17,14 @@ import os
import re
import argparse
def convert_font(file_in, file_out, width, height, first=0x0, last=0xff):
def convert_font(file_in, file_out, width, height, first=0x0, last=0xff, font_type="rom"):
chunk_size = height
with open(file_in, "rb") as bin_file:
bin_file.seek(first * height)
current = first
with open(file_out, 'wt') as font_file:
print(f'"""converted from {file_in} """', file=font_file)
print(f"FONT_TYPE = {font_type}", file=font_file)
print(f'WIDTH = {width}', file=font_file)
print(f'HEIGHT = {height}', file=font_file)
print(f'FIRST = 0x{first:02x}', file=font_file)