add font utils

This commit is contained in:
2026-01-25 15:14:15 +08:00
parent 863c0c784c
commit c6f70c47fd
8 changed files with 1105 additions and 0 deletions

18
utils/download.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/sh
# 连接并打印文件列表
mpremote connect /dev/tty.wchusbserial14310 ls
# 生成romfs文件系统并上传
mpremote romfs deploy src/rom
# 复制文件
for file in src/*.py; do
if [ -f "$file" ]; then
mpremote fs cp "$file" :
fi
done
# 打印文件列表
mpremote ls /
mpremote ls /rom

145
utils/font2bin.py Normal file
View 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
View 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()

View 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
View 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
View 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()