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