Skip to content

Instantly share code, notes, and snippets.

@widberg
Last active May 2, 2025 02:02
Show Gist options
  • Select an option

  • Save widberg/2abbbca02b532104bd32cc27743fa9f6 to your computer and use it in GitHub Desktop.

Select an option

Save widberg/2abbbca02b532104bd32cc27743fa9f6 to your computer and use it in GitHub Desktop.
"""
This script converts BMFONT FNT XML files to [bff](https://github.com/widberg/bff) Fonts_Z JSON files.
It was originally posted in the [Zouna Underground Discord](https://discord.gg/CQgMNbYeUR) by Ahmed Khaled.
"""
import json
import xml.etree.ElementTree as ET
import os
import sys
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import ctypes
import webbrowser
def resource_path(relative_path):
try:
base_path = sys._MEIPASS
except Exception:
base_path = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base_path, relative_path)
class RatatouilleConverter:
def __init__(self, root):
self.root = root
self.setup_icon()
root.title("Ratatouille BMFONT to JSON Converter")
root.geometry("460x260")
root.resizable(False, False)
self.style = ttk.Style()
self.style.configure("TButton", font=("Arial", 10))
self.style.configure("TLabel", font=("Arial", 10))
self.style.configure("Header.TLabel", font=("Arial", 12, "bold"))
main_frame = ttk.Frame(root, padding="10 10 10 10")
main_frame.pack(fill=tk.BOTH, expand=True)
header_label = ttk.Label(main_frame, text="BMFONT to JSON Converter for Ratatouille", style="Header.TLabel")
header_label.pack(pady=(0, 10))
fnt_frame = ttk.Frame(main_frame)
fnt_frame.pack(fill=tk.X, pady=5)
fnt_label = ttk.Label(fnt_frame, text="FNT File: ")
fnt_label.pack(side=tk.LEFT, padx=(0, 10))
self.fnt_path = tk.StringVar()
fnt_entry = ttk.Entry(fnt_frame, textvariable=self.fnt_path, width=30)
fnt_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
fnt_button = ttk.Button(fnt_frame, text="Browse", command=self.browse_fnt)
fnt_button.pack(side=tk.RIGHT)
json_frame = ttk.Frame(main_frame)
json_frame.pack(fill=tk.X, pady=5)
json_label = ttk.Label(json_frame, text="JSON File:")
json_label.pack(side=tk.LEFT, padx=(0, 10))
self.json_path = tk.StringVar()
json_entry = ttk.Entry(json_frame, textvariable=self.json_path, width=30)
json_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
json_button = ttk.Button(json_frame, text="Browse", command=self.browse_json)
json_button.pack(side=tk.RIGHT)
buttons_frame = ttk.Frame(main_frame)
buttons_frame.pack(pady=10)
self.convert_button = ttk.Button(buttons_frame, text="Convert", command=self.convert, width=15)
self.convert_button.pack(pady=(0, 5))
self.help_button = ttk.Button(buttons_frame, text="Help", command=self.show_help, width=15)
self.help_button.pack()
self.status_var = tk.StringVar()
self.status_label = ttk.Label(main_frame, textvariable=self.status_var, wraplength=440)
self.status_label.pack(fill=tk.X, pady=5)
footer_frame = ttk.Frame(main_frame)
footer_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 0))
credits_label = ttk.Label(footer_frame, text="by Ahmed Khaled | Discord: hmdkhld.", anchor=tk.CENTER)
credits_label.pack(fill=tk.X)
def setup_icon(self):
try:
icon_filename = "1.ico"
icon_path = resource_path(icon_filename)
if os.path.exists(icon_path):
self.root.iconbitmap(icon_path)
if os.name == 'nt':
try:
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("RatatouilleConverter")
self.root.iconbitmap(default=icon_path)
except Exception:
pass
else:
print(f"Icon file not found at: {icon_path}")
except Exception as e:
print(f"Error setting icon: {e}")
pass
def browse_fnt(self):
filename = filedialog.askopenfilename(
title="Select FNT File",
filetypes=[("FNT Files", "*.fnt"), ("XML Files", "*.xml"), ("All Files", "*.*")]
)
if filename:
self.fnt_path.set(filename)
def browse_json(self):
filename = filedialog.askopenfilename(
title="Select JSON File",
filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")]
)
if filename:
self.json_path.set(filename)
def convert(self):
fnt_path = self.fnt_path.get()
json_path = self.json_path.get()
if not fnt_path or not json_path:
messagebox.showerror("Error", "Please select both FNT and JSON files")
return
output_dir = os.path.dirname(json_path)
json_filename = os.path.basename(json_path)
output_filename = f"NEW_{json_filename}"
output_path = os.path.join(output_dir, output_filename)
self.status_var.set("Please wait...")
self.root.update()
success = self.convert_bmfont_to_json(fnt_path, json_path, output_path)
if success:
self.status_var.set(f"Done! Output saved as {output_filename}")
messagebox.showinfo("Success", f"Done!\nOutput saved as {output_filename}")
else:
self.status_var.set("failed.")
def open_bff_link(self, event):
webbrowser.open_new("https://github.com/widberg/bff")
def show_help(self):
help_text = """
• The FNT file must be in XML format.
• You can extract the FONTES.DPC file using 'bff' tool.
• The converted file will be saved in the same location as the JSON file.
"""
# Create a custom, smaller dialog
help_dialog = tk.Toplevel(self.root)
help_dialog.title("Help")
help_dialog.geometry("350x160")
help_dialog.resizable(False, False)
help_dialog.grab_set()
try:
help_dialog.iconbitmap(resource_path("1.ico"))
except:
pass
frame = ttk.Frame(help_dialog, padding="15 15 15 15")
frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(frame, text="• The FNT file must be in XML format.", anchor="w").pack(fill=tk.X, pady=(0, 5))
link_frame = ttk.Frame(frame)
link_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Label(link_frame, text="• You can extract the FONTES.DPC file using ", anchor="w").pack(side=tk.LEFT)
bff_link = ttk.Label(link_frame, text="bff", foreground="blue", cursor="hand2")
bff_link.pack(side=tk.LEFT)
bff_link.bind("<Button-1>", self.open_bff_link)
ttk.Label(link_frame, text=" tool.", anchor="w").pack(side=tk.LEFT)
ttk.Label(frame, text="• The converted file will be saved in the same location as the JSON file.",
anchor="w", wraplength=300).pack(fill=tk.X, pady=(0, 10))
ttk.Button(frame, text="OK", command=help_dialog.destroy, width=10).pack()
help_dialog.update_idletasks()
x = self.root.winfo_x() + (self.root.winfo_width() - help_dialog.winfo_width()) // 2
y = self.root.winfo_y() + (self.root.winfo_height() - help_dialog.winfo_height()) // 2
help_dialog.geometry(f"+{max(0, x)}+{max(0, y)}")
def convert_bmfont_to_json(self, fnt_path, json_path, output_path):
try:
try:
tree = ET.parse(fnt_path)
root = tree.getroot()
except Exception as e:
messagebox.showerror("Error", f"Error parsing BMFONT XML file: {e}")
return False
try:
with open(json_path, 'r', encoding='utf-8') as f:
json_data = json.load(f)
except Exception as e:
messagebox.showerror("Error", f"Error reading JSON file: {e}")
return False
if "class" not in json_data or "Fonts" not in json_data["class"] or \
"FontsV1_06_63_02PC" not in json_data["class"]["Fonts"] or \
"body" not in json_data["class"]["Fonts"]["FontsV1_06_63_02PC"] or \
"material_names" not in json_data["class"]["Fonts"]["FontsV1_06_63_02PC"]["body"]:
messagebox.showerror("Error", "Invalid JSON structure. Expected keys not found.")
return False
material_names = json_data["class"]["Fonts"]["FontsV1_06_63_02PC"]["body"]["material_names"]
json_data["class"]["Fonts"]["FontsV1_06_63_02PC"]["body"]["characters"] = {}
skipped_chars = 0
processed_chars = 0
for char in root.findall('.//char'):
char_id_str = char.get('id')
if char_id_str is None:
print("Warning: Character element without an 'id', skipping")
skipped_chars += 1
continue
try:
char_id = int(char_id_str)
except ValueError:
print(f"Warning: Invalid character ID '{char_id_str}', skipping")
skipped_chars += 1
continue
try:
if 0 <= char_id <= 0x10FFFF:
char_unicode = chr(char_id)
else:
print(f"Warning: Character ID {char_id} is outside valid Unicode range, skipping")
skipped_chars += 1
continue
except ValueError:
print(f"Warning: Cannot convert character ID {char_id} to Unicode, skipping")
skipped_chars += 1
continue
x = float(char.get('x', 0))
y = float(char.get('y', 0))
width = float(char.get('width', 0))
height = float(char.get('height', 0))
yoffset = float(char.get('yoffset', 0))
xadvance = float(char.get('xadvance', width))
try:
material_index = int(char.get('page', 0))
except ValueError:
print(f"Warning: Invalid page value for character ID {char_id}, using 0")
material_index = 0
json_data["class"]["Fonts"]["FontsV1_06_63_02PC"]["body"]["characters"][char_unicode] = {
"material_index": material_index,
"descent": yoffset,
"top_left_corner": [x, y],
"bottom_right_corner": [x + width, y + height],
"xadvance": xadvance
}
processed_chars += 1
self.status_var.set(f"Processed {processed_chars} characters, skipped {skipped_chars} invalid characters")
try:
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(json_data, f, ensure_ascii=False, indent=2)
return True
except Exception as e:
messagebox.showerror("Error", f"Error writing output JSON: {e}")
return False
except Exception as e:
messagebox.showerror("Error", f"An unexpected error occurred during conversion: {e}")
return False
def main():
root = tk.Tk()
app = RatatouilleConverter(root)
root.protocol("WM_DELETE_WINDOW", root.destroy)
root.mainloop()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment