#!/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 if output_file and os.path.isdir(output_file): base_name = os.path.splitext(os.path.basename(input_file))[0] output_dir = 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 if output_file and os.path.isdir(output_file): base_name = os.path.splitext(os.path.basename(input_file))[0] output_dir = 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 if output_file and os.path.isdir(output_file): base_name = os.path.splitext(os.path.basename(input_file))[0] output_dir = 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_file = output_path # 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()