From 36b4044a59f822027bbcbabe0e53a4a395ae5a8b Mon Sep 17 00:00:00 2001 From: kicer Date: Thu, 29 Jan 2026 15:32:26 +0800 Subject: [PATCH] add font support --- docs/jpgbin.md | 191 +++++++ src/rom/app.py | 15 +- src/rom/display.py | 45 +- utils/convert_assets.sh | 397 +------------- utils/font2bin.py | 151 ------ utils/hershey_to_pyfont.py | 268 ++++++++++ utils/image2bin.py | 159 ------ utils/imgtobitmap.py | 156 ------ utils/monofont2bitmap.py | 220 -------- utils/png_to_jpg.py | 176 +++++++ utils/pyfont_to_bin.py | 495 ++++++++++++++++++ ...t_from_romfont.py => romfont_to_pyfont.py} | 3 +- 12 files changed, 1187 insertions(+), 1089 deletions(-) create mode 100644 docs/jpgbin.md delete mode 100644 utils/font2bin.py create mode 100644 utils/hershey_to_pyfont.py delete mode 100644 utils/image2bin.py delete mode 100644 utils/imgtobitmap.py delete mode 100644 utils/monofont2bitmap.py create mode 100644 utils/png_to_jpg.py create mode 100644 utils/pyfont_to_bin.py rename utils/{font_from_romfont.py => romfont_to_pyfont.py} (97%) diff --git a/docs/jpgbin.md b/docs/jpgbin.md new file mode 100644 index 0000000..c0ec501 --- /dev/null +++ b/docs/jpgbin.md @@ -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(" {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(" {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()) diff --git a/src/rom/app.py b/src/rom/app.py index 3115500..85551ef 100644 --- a/src/rom/app.py +++ b/src/rom/app.py @@ -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() diff --git a/src/rom/display.py b/src/rom/display.py index 1afd4cb..172f8d0 100644 --- a/src/rom/display.py +++ b/src/rom/display.py @@ -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() diff --git a/utils/convert_assets.sh b/utils/convert_assets.sh index 9b9755a..839e77c 100755 --- a/utils/convert_assets.sh +++ b/utils/convert_assets.sh @@ -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 diff --git a/utils/font2bin.py b/utils/font2bin.py deleted file mode 100644 index 7d147ee..0000000 --- a/utils/font2bin.py +++ /dev/null @@ -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 - -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 ") - 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()) diff --git a/utils/hershey_to_pyfont.py b/utils/hershey_to_pyfont.py new file mode 100644 index 0000000..0945028 --- /dev/null +++ b/utils/hershey_to_pyfont.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 + +""" +Convert Hershey font data to python module. + +Usage: hershey_to_py.py [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[0-9 ]{5})(?P[0-9 ]{3})(?P.*)$'), + 'width': re.compile(r'^# WIDTH = (?P\d+)$'), + 'height': re.compile(r'^# HEIGHT = (?P\d+)$'), + 'first': re.compile(r'^# FIRST = (?P\d+)$'), + 'last': re.compile(r'^# LAST = (?P\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\d+)\s+(?P\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) diff --git a/utils/image2bin.py b/utils/image2bin.py deleted file mode 100644 index c6c6db4..0000000 --- a/utils/image2bin.py +++ /dev/null @@ -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 - -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 ") - 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()) diff --git a/utils/imgtobitmap.py b/utils/imgtobitmap.py deleted file mode 100644 index cfc643f..0000000 --- a/utils/imgtobitmap.py +++ /dev/null @@ -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 - -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()) diff --git a/utils/monofont2bitmap.py b/utils/monofont2bitmap.py deleted file mode 100644 index 11bbbfa..0000000 --- a/utils/monofont2bitmap.py +++ /dev/null @@ -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() diff --git a/utils/png_to_jpg.py b/utils/png_to_jpg.py new file mode 100644 index 0000000..bc1e0ac --- /dev/null +++ b/utils/png_to_jpg.py @@ -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()) diff --git a/utils/pyfont_to_bin.py b/utils/pyfont_to_bin.py new file mode 100644 index 0000000..f07561d --- /dev/null +++ b/utils/pyfont_to_bin.py @@ -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 [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() diff --git a/utils/font_from_romfont.py b/utils/romfont_to_pyfont.py similarity index 97% rename from utils/font_from_romfont.py rename to utils/romfont_to_pyfont.py index dfed01b..ecf448b 100644 --- a/utils/font_from_romfont.py +++ b/utils/romfont_to_pyfont.py @@ -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)