Last active
May 2, 2025 02:02
-
-
Save widberg/2abbbca02b532104bd32cc27743fa9f6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| 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