add font support
This commit is contained in:
191
docs/jpgbin.md
Normal file
191
docs/jpgbin.md
Normal 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())
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
268
utils/hershey_to_pyfont.py
Normal 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)
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
176
utils/png_to_jpg.py
Normal 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
495
utils/pyfont_to_bin.py
Normal 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()
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user