add font utils
This commit is contained in:
23
README.md
23
README.md
@@ -3,5 +3,28 @@
|
||||
esp8266版天气信息显示设备
|
||||
|
||||
|
||||
## API接口
|
||||
|
||||
1. /ping
|
||||
|
||||
2. /status
|
||||
|
||||
3. /weather
|
||||
/weather/?city=xxx&force=1
|
||||
|
||||
4. /lcd
|
||||
|
||||
5. /lcd/set, POST
|
||||
|
||||
6. /exec, POST
|
||||
|
||||
```sh
|
||||
# read memory free
|
||||
> *curl -H "Content-Type: application/json" -X POST -d '{"cmd":"import gc;gc.collect();R=gc.mem_free()", "token":"c6b74200"}' http://192.168.99.194/exec*
|
||||
|
||||
# reset
|
||||
> *curl -H "Content-Type: application/json" -X POST -d '{"cmd":"import machine; machine.reset()", "token":"c6b74200"}' http://192.168.99.194/exec*
|
||||
|
||||
|
||||
## 参考资料
|
||||
[MicroPython remote control: mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html)
|
||||
|
||||
84
docs/streaming.md
Normal file
84
docs/streaming.md
Normal file
@@ -0,0 +1,84 @@
|
||||
### 各工具的区别与用途
|
||||
|
||||
1. **font2bitmap.py**
|
||||
- 用途:将TrueType字体转换为等宽(或比例)位图字体模块
|
||||
- 输入:TrueType字体文件(.ttf/.otf)
|
||||
- 输出:Python字体模块(如proverbs_font.py)
|
||||
- 特点:
|
||||
- 支持比例字体(字符宽度可变)
|
||||
- 支持CJK字符和Unicode
|
||||
- 生成4个数据结构:MAP、_WIDTHS、_OFFSETS、_BITMAPS
|
||||
- 使用方式:`display.write(font_module, "Hello", x, y, color)`
|
||||
|
||||
2. **monofont2bitmap.py**
|
||||
- 用途:将等宽TrueType字体转换为位图字体模块
|
||||
- 输入:TrueType字体文件(.ttf/.otf)
|
||||
- 输出:Python字体模块(如inconsolata_32.py)
|
||||
- 特点:
|
||||
- 固定宽度字符
|
||||
- 简单数据结构:HEIGHT、WIDTH、_BITMAP
|
||||
- 支持多色字体
|
||||
- 使用方式:`display.bitmap(font_module, x, y, char_index)`
|
||||
|
||||
3. **font_from_romfont.py**
|
||||
- 用途:将ROM字体二进制文件转换为Python模块
|
||||
- 输入:老旧计算机/游戏机的ROM字体数据
|
||||
- 输出:Python字体模块
|
||||
- 特点:
|
||||
- 保留复古计算机字体风格
|
||||
- 通常8x8固定大小
|
||||
- 使用方式:`display.bitmap(font_module, x, y, char_index)`
|
||||
|
||||
4. **imgtobitmap.py**
|
||||
- 用途:将图像文件转换为位图模块
|
||||
- 输入:图像文件(PNG、JPG等)
|
||||
- 输出:Python图像模块
|
||||
- 特点:
|
||||
- 支持任何图像到位图的转换
|
||||
- 支持1-8位/像素的颜色深度
|
||||
- 生成调色板
|
||||
- 使用方式:`display.bitmap(image_module, x, y, 0)`
|
||||
|
||||
### 哪个可以转成流式格式?
|
||||
|
||||
**所有这些工具的输出都可以转换为流式格式!**
|
||||
|
||||
我创建了相应的转换工具:
|
||||
|
||||
1. **font2bin.py**:将font2bitmap.py生成的字体模块转换为二进制流式格式
|
||||
2. **image2bin.py**:将imgtobitmap.py生成的图像模块转换为二进制流式格式
|
||||
|
||||
**原理相同**:
|
||||
- Python模块在导入时加载全部数据到RAM
|
||||
- 二进制文件可以按需读取,只加载需要的数据
|
||||
- 针对ESP8266等内存受限设备特别有效
|
||||
|
||||
### 流式格式的优势
|
||||
|
||||
1. **font2bitmap.py → 流式格式**:
|
||||
- 节省93%内存(6.4KB → ~400字节)
|
||||
- 适合大型字体,特别是包含CJK字符的字体
|
||||
- 保持与原有`st7789.write()`接口的兼容性
|
||||
|
||||
2. **imgtobitmap.py → 流式格式**:
|
||||
- 适合大型图像或动画
|
||||
- 支持动态帧加载(StreamingAnimation类)
|
||||
- 可以存储在文件系统而不是RAM中
|
||||
|
||||
3. **monofont2bitmap.py → 流式格式**:
|
||||
- 简单结构使流式加载更容易
|
||||
- 虽然已经很紧凑,但在极小内存环境中仍有价值
|
||||
|
||||
4. **font_from_romfont.py → 流式格式**:
|
||||
- 超简单结构,流式实现非常直接
|
||||
- 对于大批量老旧字体集合有意义
|
||||
|
||||
### 实际应用建议
|
||||
|
||||
对于ESP8266等内存受限设备:
|
||||
1. 使用`font2bin.py`将大型字体转换为流式格式
|
||||
2. 使用`image2bin.py`将大型图像或动画帧转换为流式格式
|
||||
3. 使用`streaming_font.py`和`streaming_image.py`加载和显示
|
||||
4. 这样可以在保持功能的同时,最大限度地减少内存使用
|
||||
|
||||
这种流式方式使得ESP8266等设备可以显示大型字体和图像,而不会因内存不足而失效。
|
||||
145
utils/font2bin.py
Normal file
145
utils/font2bin.py
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/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.bin
|
||||
"""
|
||||
|
||||
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"""
|
||||
info_path = output_path.replace(".bin", ".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.bin")
|
||||
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())
|
||||
407
utils/font2bitmap.py
Normal file
407
utils/font2bitmap.py
Normal file
@@ -0,0 +1,407 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Needs freetype-py>=1.0
|
||||
|
||||
# Font handling classes are from Dan Bader blog post on using freetype
|
||||
# http://dbader.org/blog/monochrome-font-rendering-with-freetype-and-python
|
||||
#
|
||||
# Modified by Russ Hughes, Mar 2021 to write bitmap modules compatible with
|
||||
# the MicroPython ili9342c driver at https://github.com/russhughes/ili9342c_mpy
|
||||
# and the st7789 driver at https://github.com/russhughes/st7789_mpy.
|
||||
#
|
||||
# The Negative glyph.left fix is from peterhinch's font conversion program
|
||||
# https://github.com/peterhinch/micropython-font-to-py
|
||||
# https://github.com/peterhinch/micropython-font-to-py/issues/21
|
||||
# Handle negative glyph.left correctly (capital J),
|
||||
# also glyph.width > advance (capital K and R).
|
||||
#
|
||||
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2013 Daniel Bader (http://dbader.org)
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
|
||||
import sys
|
||||
import shlex
|
||||
import argparse
|
||||
import bisect
|
||||
import freetype
|
||||
|
||||
|
||||
def to_int(string):
|
||||
""" Return integer value from a hex or decimal string"""
|
||||
return int(string, base=16) if string.startswith("0x") else int(string)
|
||||
|
||||
|
||||
def get_chars(string):
|
||||
""" Return string comprised of given characters or range(s) of characters"""
|
||||
return ''.join(chr(b) for a in [
|
||||
(lambda sub: range(sub[0], sub[-1] + 1))
|
||||
(list(map(to_int, ele.split('-'))))
|
||||
for ele in string.split(',')] for b in a)
|
||||
|
||||
|
||||
def wrap_str(string, items_per_line=32):
|
||||
""" Return a string wrapped to items_per_line with special care for escape characters"""
|
||||
length = len(string)
|
||||
lines = []
|
||||
i = 0
|
||||
end = 0
|
||||
while length > end:
|
||||
end = min(i + items_per_line, length)
|
||||
if string[end - 1] == '\\':
|
||||
end -= 2
|
||||
lines.append(string[i : end])
|
||||
i = end
|
||||
return "(\n '" + "'\n '".join(lines) + "'\n)"
|
||||
|
||||
|
||||
def wrap_bytes(lst, items_per_line=16):
|
||||
"""Return a string of items wrapped to items_per_line"""
|
||||
lines = [
|
||||
"".join(f'\\x{x:02x}' for x in lst[i : i + items_per_line])
|
||||
for i in range(0, len(lst), items_per_line)
|
||||
]
|
||||
return " b'" + "'\\\n b'".join(lines) + "'"
|
||||
|
||||
|
||||
def wrap_longs(lst, items_per_line=16):
|
||||
"""Return a string of longs wrapped to items_per_line"""
|
||||
lines = [
|
||||
"".join(f'\\x{x:02x}' for x in lst[i : i + items_per_line])
|
||||
for i in range(0, len(lst), items_per_line)
|
||||
]
|
||||
return " b'" + "'\\\n b'".join(lines) + "'"
|
||||
|
||||
|
||||
class Bitmap():
|
||||
"""
|
||||
A 2D bitmap image represented as a list of byte values. Each byte indicates
|
||||
the state of a single pixel in the bitmap. A value of 0 indicates that the
|
||||
pixel is `off` and any other value indicates that it is `on`.
|
||||
"""
|
||||
def __init__(self, width, height, pixels=None):
|
||||
self.width = int(width)
|
||||
self.height = int(height)
|
||||
self.pixels = pixels or bytearray(width * height)
|
||||
|
||||
def __repr__(self):
|
||||
"""Return a string representation of the bitmap's pixels."""
|
||||
rows = ''
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
rows += '#' if self.pixels[y * self.width + x] else '.'
|
||||
rows += '\n'
|
||||
return rows
|
||||
|
||||
def bit_string(self):
|
||||
"""Return a binary string representation of the bitmap's pixels."""
|
||||
bits = ''
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
bits += '1' if self.pixels[y * self.width + x] else '0'
|
||||
return bits
|
||||
|
||||
def bitblt(self, src, x, y):
|
||||
"""Copy all pixels from `src` into this bitmap"""
|
||||
srcpixel = 0
|
||||
dstpixel = y * self.width + x
|
||||
row_offset = self.width - src.width
|
||||
|
||||
for _ in range(src.height):
|
||||
for _ in range(src.width):
|
||||
# Perform an OR operation on the destination pixel and the
|
||||
# source pixel because glyph bitmaps may overlap if character
|
||||
# kerning is applied, e.g. in the string "AVA", the "A" and "V"
|
||||
# glyphs must be rendered with overlapping bounding boxes.
|
||||
self.pixels[dstpixel] = (
|
||||
self.pixels[dstpixel] or src.pixels[srcpixel])
|
||||
srcpixel += 1
|
||||
dstpixel += 1
|
||||
dstpixel += row_offset
|
||||
|
||||
|
||||
class Glyph():
|
||||
def __init__(self, pixels, width, height, top, left, advance_width):
|
||||
self.bitmap = Bitmap(width, height, pixels)
|
||||
|
||||
# The glyph bitmap's top-side bearing, i.e. the vertical distance from
|
||||
# the baseline to the bitmap's top-most scanline.
|
||||
self.top = top
|
||||
self.left = left
|
||||
# Ascent and descent determine how many pixels the glyph extends
|
||||
# above or below the baseline.
|
||||
self.descent = max(0, self.height - self.top)
|
||||
self.ascent = max(0, max(self.top, self.height) - self.descent)
|
||||
|
||||
# The advance width determines where to place the next character
|
||||
# horizontally, that is, how many pixels we move to the right to draw
|
||||
# the next glyph.
|
||||
self.advance_width = advance_width
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self.bitmap.width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self.bitmap.height
|
||||
|
||||
@staticmethod
|
||||
def from_glyphslot(slot):
|
||||
"""Construct and return a Glyph object from a FreeType GlyphSlot."""
|
||||
pixels = Glyph.unpack_mono_bitmap(slot.bitmap)
|
||||
width, height = slot.bitmap.width, slot.bitmap.rows
|
||||
top = slot.bitmap_top
|
||||
left = slot.bitmap_left
|
||||
|
||||
# The advance width is given in FreeType's 26.6 fixed point format,
|
||||
# which means that the pixel values are multiples of 64.
|
||||
advance_width = slot.advance.x // 64
|
||||
|
||||
return Glyph(pixels, width, height, top, left, advance_width)
|
||||
|
||||
@staticmethod
|
||||
def unpack_mono_bitmap(bitmap):
|
||||
"""
|
||||
Unpack a freetype FT_LOAD_TARGET_MONO glyph bitmap into a bytearray
|
||||
where each pixel is represented by a single byte.
|
||||
"""
|
||||
# Allocate a bytearray of sufficient size to hold the glyph bitmap.
|
||||
data = bytearray(bitmap.rows * bitmap.width)
|
||||
|
||||
# Iterate over every byte in the glyph bitmap. Note that we're not
|
||||
# iterating over every pixel in the resulting unpacked bitmap --
|
||||
# we're iterating over the packed bytes in the input bitmap.
|
||||
for y in range(bitmap.rows):
|
||||
for byte_index in range(bitmap.pitch):
|
||||
|
||||
# Read the byte that contains the packed pixel data.
|
||||
byte_value = bitmap.buffer[y * bitmap.pitch + byte_index]
|
||||
|
||||
# We've processed this many bits (=pixels) so far. This
|
||||
# determines where we'll read the next batch of pixels from.
|
||||
num_bits_done = byte_index * 8
|
||||
|
||||
# Pre-compute where to write the pixels that we're going
|
||||
# to unpack from the current byte in the glyph bitmap.
|
||||
rowstart = y * bitmap.width + byte_index * 8
|
||||
|
||||
# Iterate over every bit (=pixel) that's still a part of the
|
||||
# output bitmap. Sometimes we're only unpacking a fraction of a
|
||||
# byte because glyphs may not always fit on a byte boundary. So
|
||||
# we make sure to stop if we unpack past the current row of
|
||||
# pixels.
|
||||
for bit_index in range(min(8, bitmap.width - num_bits_done)):
|
||||
|
||||
# Unpack the next pixel from the current glyph byte.
|
||||
bit = byte_value & (1 << (7 - bit_index))
|
||||
|
||||
# Write the pixel to the output bytearray. We ensure that
|
||||
# `off` pixels have a value of 0 and `on` pixels have a
|
||||
# value of 1.
|
||||
data[rowstart + bit_index] = 1 if bit else 0
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class Font():
|
||||
def __init__(self, filename, width, height):
|
||||
self.face = freetype.Face(filename)
|
||||
self.face.set_pixel_sizes(width, height)
|
||||
|
||||
def glyph_for_character(self, char):
|
||||
# Let FreeType load the glyph for the given character and tell it to
|
||||
# render a monochromatic bitmap representation.
|
||||
self.face.load_char(
|
||||
char, freetype.FT_LOAD_RENDER | freetype.FT_LOAD_TARGET_MONO)
|
||||
|
||||
return Glyph.from_glyphslot(self.face.glyph)
|
||||
|
||||
def render_character(self, char):
|
||||
glyph = self.glyph_for_character(char)
|
||||
return glyph.bitmap
|
||||
|
||||
def text_dimensions(self, text):
|
||||
"""
|
||||
Return (width, height, baseline) of `text` rendered in the current
|
||||
font.
|
||||
"""
|
||||
width = 0
|
||||
max_ascent = 0
|
||||
max_descent = 0
|
||||
|
||||
# For each character in the text string we get the glyph
|
||||
# and update the overall dimensions of the resulting bitmap.
|
||||
for char in text:
|
||||
glyph = self.glyph_for_character(char)
|
||||
max_ascent = max(max_ascent, glyph.ascent)
|
||||
max_descent = max(max_descent, glyph.descent)
|
||||
|
||||
if glyph.left >= 0:
|
||||
char_width = int(
|
||||
max(glyph.advance_width, glyph.width + glyph.left))
|
||||
else:
|
||||
char_width = int(
|
||||
max(glyph.advance_width - glyph.left, glyph.width))
|
||||
|
||||
width += char_width
|
||||
|
||||
height = max_ascent + max_descent
|
||||
return (width, height, max_descent)
|
||||
|
||||
def write_python(self, text, font_file):
|
||||
"""
|
||||
Render the given `text` into a python bitmap module.
|
||||
"""
|
||||
_, height, baseline = self.text_dimensions(text)
|
||||
|
||||
bits = []
|
||||
widths = []
|
||||
offsets = []
|
||||
offset = 0
|
||||
|
||||
for char in text:
|
||||
glyph = self.glyph_for_character(char)
|
||||
|
||||
# Negative glyph.left fix from peterhinch
|
||||
# https://github.com/peterhinch/micropython-font-to-py
|
||||
#
|
||||
# https://github.com/peterhinch/micropython-font-to-py/issues/21
|
||||
# Handle negative glyph.left correctly (capital J),
|
||||
# also glyph.width > advance (capital K and R).
|
||||
|
||||
if glyph.left >= 0:
|
||||
char_width = int(
|
||||
max(glyph.advance_width, glyph.width + glyph.left))
|
||||
left = glyph.left
|
||||
else:
|
||||
char_width = int(
|
||||
max(glyph.advance_width - glyph.left, glyph.width))
|
||||
left = 0
|
||||
|
||||
# save the bit offset and width of the current glyph
|
||||
offsets.append(offset)
|
||||
widths.append(char_width)
|
||||
outbuffer = Bitmap(char_width, height)
|
||||
|
||||
# The vertical drawing position should place the glyph
|
||||
# on the baseline as intended.
|
||||
y = height - glyph.ascent - baseline
|
||||
outbuffer.bitblt(glyph.bitmap, left, y)
|
||||
|
||||
# convert bitmap to ascii bitmap string
|
||||
bit_string = outbuffer.bit_string()
|
||||
bits.append(bit_string)
|
||||
offset += len(bit_string)
|
||||
|
||||
# join all the bitmap strings together
|
||||
bit_string = ''.join(bits)
|
||||
|
||||
# escape backslash, quote and single quote characters for char_map
|
||||
text_escaped = text.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'")
|
||||
char_map = wrap_str(text_escaped)
|
||||
|
||||
cmd_line = " ".join(map(shlex.quote, sys.argv))
|
||||
max_width = max(widths)
|
||||
|
||||
# write python module source
|
||||
print('# -*- coding: utf-8 -*-')
|
||||
print(f'# Converted from {font_file} using:')
|
||||
print(f'# {cmd_line}')
|
||||
print()
|
||||
|
||||
print(f'MAP = {char_map}\n')
|
||||
print('BPP = 1')
|
||||
print(f'HEIGHT = {height}')
|
||||
print(f'MAX_WIDTH = {max_width}')
|
||||
print('_WIDTHS = \\')
|
||||
print(wrap_bytes(widths))
|
||||
print()
|
||||
|
||||
byte_offsets = bytearray()
|
||||
bytes_table = [0xff, 0xffff, 0xffffff, 0xffffffff]
|
||||
bytes_required = bisect.bisect_left(bytes_table, offset, 0, 3) + 1
|
||||
for offset in offsets:
|
||||
byte_offsets.extend(offset.to_bytes(bytes_required, 'big'))
|
||||
|
||||
print(f'OFFSET_WIDTH = {bytes_required}')
|
||||
print('_OFFSETS = \\')
|
||||
print(wrap_longs(byte_offsets))
|
||||
print()
|
||||
|
||||
print('_BITMAPS =\\')
|
||||
byte_values = [int(bit_string[i:i+8], 2) for i in range(0, len(bit_string), 8)]
|
||||
print(wrap_bytes(byte_values))
|
||||
print("\nWIDTHS = memoryview(_WIDTHS)")
|
||||
print("OFFSETS = memoryview(_OFFSETS)")
|
||||
print("BITMAPS = memoryview(_BITMAPS)")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='font2bitmap',
|
||||
description=('''
|
||||
Convert characters from a truetype font 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_height',
|
||||
type=int,
|
||||
default=8,
|
||||
help='size of font to create bitmaps from.')
|
||||
|
||||
parser.add_argument(
|
||||
'-width', '--font_width',
|
||||
type=int,
|
||||
default=None,
|
||||
help='width of font to create bitmaps from.')
|
||||
|
||||
group = parser.add_argument_group(
|
||||
'character selection',
|
||||
'characters from the font to include in the bitmap.')
|
||||
|
||||
excl = group.add_mutually_exclusive_group(required=True)
|
||||
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()
|
||||
font_file = args.font_file
|
||||
height = args.font_height
|
||||
width = args.font_height if args.font_width is None else args.font_width
|
||||
characters = get_chars(args.characters) if args.string is None else args.string
|
||||
|
||||
fnt = Font(font_file, width, height)
|
||||
fnt.write_python(characters, font_file)
|
||||
|
||||
|
||||
main()
|
||||
87
utils/font_from_romfont.py
Normal file
87
utils/font_from_romfont.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Convert fonts from the font-bin directory of spacerace's
|
||||
https://github.com/spacerace/romfont repo.
|
||||
|
||||
Reads all romfont bin files from the specified -input-directory (-i) and writes
|
||||
python font files to the specified -output-directory (-o). Optionally limiting
|
||||
characters included to -first-char (-f) thru -last-char (-l).
|
||||
|
||||
Example:
|
||||
|
||||
font_from_romfont -i font-bin -o pyfont -f 32 -l 127
|
||||
|
||||
requires argparse
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import argparse
|
||||
|
||||
def convert_font(file_in, file_out, width, height, first=0x0, last=0xff):
|
||||
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'WIDTH = {width}', file=font_file)
|
||||
print(f'HEIGHT = {height}', file=font_file)
|
||||
print(f'FIRST = 0x{first:02x}', file=font_file)
|
||||
print(f'LAST = 0x{last:02x}', file=font_file)
|
||||
print('_FONT =\\\n', sep='', end='', file=font_file)
|
||||
for chunk in iter(lambda: bin_file.read(chunk_size), b''):
|
||||
print('b\'', sep='', end='', file=font_file)
|
||||
for data in chunk:
|
||||
print(f'\\x{data:02x}', end='', file=font_file)
|
||||
print('\'\\', file=font_file)
|
||||
current += 1
|
||||
if current > last:
|
||||
break
|
||||
|
||||
print('', file=font_file)
|
||||
print('FONT = memoryview(_FONT)', file=font_file)
|
||||
|
||||
def auto_int(x):
|
||||
return int(x, 0)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Convert Romfont.bin font files in input to python in font_directory.')
|
||||
parser.add_argument('input', help='file or directory containing binary font file(s).')
|
||||
parser.add_argument('output', help='file or directory to contain python font file(s).')
|
||||
parser.add_argument('-f', '--first-char', type=auto_int, default=0x20)
|
||||
parser.add_argument('-l', '--last-char', type=auto_int, default=0x7f)
|
||||
args = parser.parse_args()
|
||||
|
||||
file_re = re.compile(r'^(.*)(\d+)x(\d+)\.bin$')
|
||||
|
||||
is_dir = os.path.isdir(args.input)
|
||||
bin_files = os.listdir(args.input) if is_dir else [args.input]
|
||||
for bin_file_name in bin_files:
|
||||
match = file_re.match(bin_file_name)
|
||||
if match:
|
||||
font_width = int(match.group(2))
|
||||
font_height = int(match.group(3))
|
||||
|
||||
if is_dir:
|
||||
bin_file_name = args.input+'/'+bin_file_name
|
||||
|
||||
if is_dir:
|
||||
font_file_name = (
|
||||
args.font_directory + '/' +
|
||||
match.group(1).rstrip('_').lower()+
|
||||
f'_{font_width}x{font_height}.py')
|
||||
else:
|
||||
font_file_name = args.output
|
||||
|
||||
print("converting", bin_file_name, 'to', font_file_name)
|
||||
|
||||
convert_font(
|
||||
bin_file_name,
|
||||
font_file_name,
|
||||
font_width,
|
||||
font_height,
|
||||
args.first_char,
|
||||
args.last_char)
|
||||
|
||||
main()
|
||||
139
utils/image2bin.py
Normal file
139
utils/image2bin.py
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/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.bin
|
||||
"""
|
||||
|
||||
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
|
||||
image_data = {
|
||||
"width": image_module.WIDTH,
|
||||
"height": image_module.HEIGHT,
|
||||
"colors": image_module.COLORS,
|
||||
"bpp": image_module.BPP,
|
||||
"palette": getattr(image_module, "PALETTE", []),
|
||||
"bitmap": bytes(image_module._bitmap),
|
||||
}
|
||||
|
||||
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
|
||||
f.write(struct.pack("B", image_data["colors"])) # Number of colors
|
||||
f.write(struct.pack("B", image_data["bpp"])) # Bits per pixel
|
||||
|
||||
# Write palette
|
||||
if image_data["palette"]:
|
||||
f.write(struct.pack("B", len(image_data["palette"]))) # Palette length
|
||||
for color in image_data["palette"]:
|
||||
f.write(struct.pack("H", color)) # 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"""
|
||||
info_path = output_path.replace(".bin", ".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', '.bin'))} 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.bin")
|
||||
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())
|
||||
220
utils/monofont2bitmap.py
Normal file
220
utils/monofont2bitmap.py
Normal file
@@ -0,0 +1,220 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user