• All, Gmail is currently rejecting messages from my host. I have a ticket in process, but it may take some time to resolve. Until further notice, do NOT use Gmail for your accounts. You will be unable to receive confirmations and two factor messages to login.

OpenBOR Tools im working on

bWWd

Well-known member
Fontmaker, shadowcoord add for all anims in txt, offset shifter for all levels in folder, manual search , frame path generator for alphamask and regular frame paths and group generator, packer and music converter gui.
all in python so you only need python 3.10.9 for all of them, unless you want music conversion then you need ffmpeg exe in same folder

My fav is probably group generator and fontmaker, they save tons and tons of time



1704544879913.png1704545752050.png1704545601963.png
1704544901235.png
1704544995004.png
1704545045635.png
1704545177401.png
1704545383376.png
1704551402490.png
1704546142649.png
1704551458763.png
 
Last edited:
The group tool is an amazing idea!
About the font, from what I see you choose a TTF font and it output in the OpenBOR format, right?

Do you think its possible to make the opposite? A tool to load a image with the font and output it an with with a custom text
IOW, a bitmap font writer.
 
Fontmaker, shadowcoord add for all anims in txt, offset shifter for all levels in folder, manual search , frame path generator for alphamask and regular frame paths and group generator, packer and music converter gui.
all in python so you only need python for all of them, unless you want music conversion then you need ffmpeg exe in same folder

My fav is probably group generator and fontmaker, they save tons and tons of time



View attachment 6558View attachment 6567View attachment 6566
View attachment 6559
View attachment 6560
View attachment 6561
View attachment 6562
View attachment 6563
View attachment 6569
View attachment 6568
View attachment 6570
This will be so useful for me since I know Jack on fonts...a mixture of STF2, STFA and MVC fonts is what I need to create.
 
so you want to just type with openbor text files? thats easy to make
Inclyuded entity generator gui , it detects animation filenames in folder and if its idle01.png or pain1.gif or really anything with predetermined animation names in the filename then it creates idle anim and pain anims , all anims automatically with paths , also sets loop 1 for idle walk run, i think it will save some time, you can type in header info for anims and for actual entity header.
Ill modifu it so the root will be data folder.
 
Really want if I can just type with specific sprite text, since manually dragging them one by one will consume a lot of time
 
i guess maybe ill try to look for 1st color in the font file and make it transparent

That would be best. The only catch is if someone is still using .pcx, but anyone hanging on to that relic has bigger problems than fonts to worry about.

DC
 
yeah i forgot about pcx, the only reason im not using png is that its visual cue for me -pngs are unconverted and gif are conoverted, if all are pngs i cant tell ands its a mess
BTW if you guys have some ideas what we should automate then post and maybe it can be done cause there are some mundane tasks that can be taken care of by simple guis and buttons.

I think there was an issue with font size where if it was not divisible by 16 it acted weird misaligned ? So maybe i should put normalizer in so it would scale to nearest divisible
 
Last edited:
finished this one for now, checks for image names in folder , when theyre named like some of the anims on the list - it crates animations with them with specified offset when you click on idle frame

yeah i could make different offset per anim but meh, its just for good starting point to get all those anims in, and no attacks cause those require more care even tho it is possible to name frames attack1_01 etc and it will just import them

You can type in all the header ino and everything thats text
root lock for data string in path is not in yet, need more test first... ok just added it ,simply rejects everything before data string in a path.

Hmm, maybe i could display also first frame from walk ,pain and others to set offset for them...

Yoiu can pretty much run this on batch mode and have all cahracters txt files done if you plan this right.Then just adjust attacks and all, i can add code to detect sounds and put them in a frame befre attack, i can add a marker so you specify which frame in attack anim is attack frame , set up so it adds landframe for proper frame in fall, mark 2 opposite corners like i mark offset now to create bbox ... and on and on so in the end you dont have to do much especially if all your frames have same offset.
1704593481104.png
 
Last edited:
Ok hheres the tool for typing with openbor fonts, save the code into bat file and run, but requires python
Tehres still quirks with special symbols not working but mostly works fine , ill be posting code for the guis in this manner cause its also a backup for me
1704633753720.png
Code:
0<0# : ^
'''
@echo off
set script=%~f0
python -x "%script%" %*
exit /b 0
'''
from PIL import Image as PILImage, ImageDraw, ImageFont, ImageTk,ImageGrab
import tkinter as tk
from tkinter import filedialog
import os
from datetime import datetime
import io
import tkinter.messagebox
try:
    from PIL import Image as PILImage, ImageDraw, ImageFont, ImageTk, ImageGrab
    pillow_available = True
except ImportError:
    pillow_available = False

if pillow_available:
    print("Pillow library is available.")
else:
    install_pillow = messagebox.askyesno(
        "Pillow Not Installed",
        "Pillow library is required for this application. Do you want to install it?"
    )
    if install_pillow:
        try:
            import subprocess
            subprocess.run(["pip", "install", "Pillow"], check=True)
            from PIL import Image, ImageDraw, ImageFont, ImageTk, ImageGrab
            pillow_available = True
            print("Pillow library installed successfully.")
        except Exception as install_error:
            messagebox.showerror(
                "Installation Error",
                f"Error installing Pillow: {install_error}\nPlease install Pillow manually using 'pip install Pillow'."
            )
            exit(1)
    else:
        exit(1)
 
class FontParserApp:
    def __init__(self, master):
        self.master = master
        self.master.title("FontTyper")
        self.master.configure(bg="black")
        entry_style = {"fg": "red", "font": ("Helvetica", 10, "bold")}
        entry_styleb = {"fg": "blue", "font": ("Helvetica", 10, "bold")}
        script_dir = os.path.dirname(os.path.realpath(__file__))
        self.first_rgb_value = None
        self.font_image_path = None
        self.font_image = None
        self.canvas_frame = tk.Frame(self.master, bg="black")
        self.canvas_frame.pack()
        self.canvas_scrollbar = tk.Scrollbar(self.canvas_frame, orient=tk.VERTICAL, bg="black", troughcolor="black")
        self.canvas_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.canvas = tk.Canvas(self.canvas_frame, width=600, height=400, yscrollcommand=self.canvas_scrollbar.set, bg="black", bd=0, highlightthickness=0)
        self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.canvas_scrollbar.config(command=self.canvas.yview)
        self.text_entry_label = tk.Label(self.master, text="Enter Text:", fg="black", font=("Helvetica", 10, "bold"))
        self.text_entry_label.pack()
        self.text_entry = tk.Text(self.master, wrap=tk.WORD, height=5, width=40)
        self.text_entry.pack()
        self.load_font_button = tk.Button(self.master, text="Load Font", command=self.load_font, fg="blue", font=("Helvetica", 10, "bold"))
        self.load_font_button.pack()
        self.save_image_button = tk.Button(self.master, text="Save Image", command=self.save, fg="red", font=("Helvetica", 10, "bold"))
        self.save_image_button.pack()
        self.kerning_label = tk.Label(self.master, text="Spacing:", fg="blue", font=("Helvetica", 10, "bold"))
        self.kerning_label.pack()
        self.kerning_entry = tk.Entry(self.master, fg="black", font=("Helvetica", 10, "bold"))
        self.kerning_entry.insert(tk.END, "2")
        self.kerning_entry.pack()
        self.master.bind('<Return>', lambda event: self.generate_text())
        self.init_mapping()
        self.ttf_font_path = None
        self.master.bind('<KeyPress-Return>', lambda event: self.generate_text(event))
        self.master.bind('<KeyRelease>', lambda event: self.generate_text(event))
        self.init_mapping()
        self.transparent_button = tk.Button(self.master, text="Transparent BG Export: OFF", command=self.toggle_transparent,
                                            bg="black", fg="white", font=("Helvetica", 10, "bold"))
        self.transparent_button.pack()
    def init_mapping(self):
        if self.font_image is None:
            print("Font image not loaded.")
            return
        image_width, image_height = self.font_image.size
        num_columns = 16
        num_rows = 16
        cell_width = image_width // num_columns
        cell_height = image_height // num_rows
        self.mapping = {}
        characters = (
            "0123456789ABCDEF",
            "0123456789ABCDEF",
            " !\"#$%&´()*+,-./",
            "0123456789:;{=?",
            "@ABCDEFGHIJKLMNO",
            "PQRSTUVWXYZ[\\]^_",
            "`abcdefghijklmno",
            "pqrstuvwxyz"
        )
        for row, line in enumerate(characters):
            for col, char in enumerate(line):
                char_bbox = self.compute_char_bbox(char, col, row, cell_width, cell_height, 0, 0)
                region = (
                    col * cell_width + char_bbox[0],  
                    row * cell_height,                
                    col * cell_width + char_bbox[2],  
                    (row + 1) * cell_height,        
                )
                char_image = self.font_image.crop(region)
                char_image = char_image.resize((char_image.width, char_image.height), Image.NEAREST)
                char_photo = ImageTk.PhotoImage(char_image)
                self.mapping[char] = char_photo
    def compute_char_bbox(self, char, col, row, cell_width, cell_height, top_padding, bottom_padding):
        region = (
            col * cell_width,
            row * cell_height,
            (col + 1) * cell_width,
            (row + 1) * cell_height
        )
        char_image = self.font_image.crop(region)
        char_image = char_image.resize((char_image.width, char_image.height), Image.NEAREST)
        palette = self.font_image.getpalette()
        first_rgb_value = (palette[0], palette[1], palette[2])
        char_image = char_image.convert("RGBA")
        if not (row == 2 and col == 0):
            for y in range(char_image.height):
                for x in range(char_image.width):
                    pixel = char_image.getpixel((x, y))
                    if pixel[:3] == first_rgb_value:
                        char_image.putpixel((x, y), (0, 0, 0, 0))
            mask = char_image.convert("L")
            bbox = mask.getbbox()
            if bbox is not None:
                bbox = (
                    bbox[0],
                    max(0, bbox[1] - top_padding),
                    bbox[2],
                    min(cell_height, bbox[3] + bottom_padding)
                )
                char_image_cropped = char_image.crop(bbox)
                char_filename = "".join(c for c in char if c.isalnum() or c in ('_', '-'))
                effective_width = bbox[2] - bbox[0] + 1
                print(f"Character: {char}, Effective Width: {effective_width}")
                return bbox
            else:
                print(f"Warning: Bounding box is None for Character: {char}")
                return (0, 0, 0, 0)
        else:
            char_image_cropped = char_image.crop((0, 0, cell_width, cell_height))
            print(f"Character: {char}, Real Width: {cell_width}, Real Height: {cell_height}")
            return (0, 0, cell_width, cell_height)
    def toggle_transparent(self):
        current_state = self.transparent_button.cget("text")
        new_state = "Transparent BG Export: ON" if "OFF" in current_state else "Transparent BG Export: OFF"
        self.transparent_button.config(text=new_state)
    def generate_text(self, event=None):
        text_to_generate = self.text_entry.get("1.0", tk.END).strip()
        try:
            kerning_value = float(self.kerning_entry.get())
        except ValueError:
            kerning_value = 1
        self.show_generated_text(text_to_generate, kerning_value)
    def show_generated_text(self, text, kerning):
        self.canvas.delete("all")
        char_photo = None
        max_canvas_width = 600
        max_canvas_height = 400
        self.canvas.create_rectangle(0, 0, max_canvas_width, max_canvas_height )
        x_position = 0
        y_position = 1
        char_width = 0
        for line in text.split('\n'):
            for char in line:
                if char in self.mapping:
                    char_photo = self.mapping[char]
                    char_width += char_photo.width() + kerning
                    if char_width > max_canvas_width:
                        x_position = 0
                        y_position += char_photo.height()
                        char_width = char_photo.width()
                    if y_position + char_photo.height() > max_canvas_height:
                        self.canvas_scrollbar.config(command=self.canvas.yview)
                        self.canvas.config(yscrollcommand=self.canvas_scrollbar.set)
                    self.canvas.create_image(x_position, y_position, anchor=tk.NW, image=char_photo)
                    x_position += char_photo.width() + kerning
            y_position += char_photo.height()
            x_position = 0
            char_width = 0
        self.canvas.config(width=max_canvas_width, height=min(max_canvas_height, self.canvas.winfo_reqheight()))
        y_position += char_photo.height()
        x_position = 0
        char_width = 0
    def load_font(self):
        file_path = filedialog.askopenfilename(filetypes=[("Image Files", "*.gif;*.png")])
        if file_path:
            try:
                self.font_image = Image.open(file_path)
                self.font_image_path = file_path
                palette = self.font_image.getpalette()
                first_rgb_value = (palette[0], palette[1], palette[2])
                canvas_color = "#{:02x}{:02x}{:02x}".format(*first_rgb_value)
                self.canvas.config(bg=canvas_color)
                self.init_mapping()
                self.canvas.delete("all")
                print(f"Font image '{file_path}' loaded successfully.")
            except Exception as e:
                print(f"Error loading font image: {e}")
    def generate_image(self, text):
        max_line_width = 0
        total_height = 0
        for line in text.split('\n'):
            line_width = sum(self.mapping[char].width() for char in line if char in self.mapping)
            max_line_width = max(max_line_width, line_width)
            total_height += max(self.mapping[char].height() for char in line if char in self.mapping)
        image = Image.new("RGB", (max_line_width, total_height), "white")
        draw = ImageDraw.Draw(image)
        x_position, y_position = 0, 0
        for line in text.split('\n'):
            for char in line:
                if char in self.mapping:
                    char_photo = self.mapping[char]
                    char_width, char_height = char_photo.width(), char_photo.height()
                    image.paste(char_photo, (x_position, y_position))
                    x_position += char_width
        return image              
    def save(self):
        try:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            file_name = f"generated_image_{timestamp}.png"
            file_path = os.path.join(os.path.dirname(__file__), file_name)
            canvas_x, canvas_y, canvas_width, canvas_height = self.canvas.winfo_rootx(), self.canvas.winfo_rooty(), self.canvas.winfo_width(), self.canvas.winfo_height()
            screenshot = ImageGrab.grab(bbox=(canvas_x, canvas_y, canvas_x + canvas_width, canvas_y + canvas_height))
            screenshot = self.process_screenshot(screenshot)
            screenshot.save(file_path)
            print(f"Screenshot saved at: {file_path}")
        except Exception as e:
            print(f"Error saving screenshot: {e}")
    def process_screenshot(self, img):
        if self.transparent_button.cget("text") == "Transparent BG Export: ON":
            img = img.convert("RGBA")
            palette = self.font_image.getpalette()
            first_rgb_value = (palette[0], palette[1], palette[2])
            if first_rgb_value:
                new_data = []
                for item in img.getdata():
                    if item[:3] == first_rgb_value or (item[0] == 0 and item[1] == 0 and item[2] == 0):
                        new_data.append((0, 0, 0, 0))
                    else:
                        new_data.append(item)
                img.putdata(new_data)
                bbox = img.getbbox()
                img = img.crop(bbox)
                return img
            else:
                print("Error obtaining RGB value.")
                return None
        else:
            palette = self.font_image.getpalette()
            first_rgb_value = (palette[0], palette[1], palette[2])
            new_data = []
            for item in img.getdata():
                if item[:3] == first_rgb_value or (item[0] == 0 and item[1] == 0 and item[2] == 0):
                    new_data.append((0, 0, 0, 0))
                else:
                    new_data.append(item)
            img.putdata(new_data)
            bbox = img.getbbox()
            img = img.crop(bbox)
            new_data = []
            for item in img.getdata():
                if item[:3] == (0, 0, 0):
                    new_data.append(first_rgb_value)
                else:
                    new_data.append(item)
            img.putdata(new_data)
            return img
if __name__ == "__main__":
    root = tk.Tk()
    root.tk.call('tk', 'scaling', 2.0)
    app = FontParserApp(root)
    root.mainloop()
 
Last edited:
Fontmaker gui
You can scrolwheel the values so its kinda funny how easy it is to make fonts now , i will add gradient effect to the fonts so top will be different color than bottom

Some fonts have quirks maybe i should add x shift as well but most regular fonts work fine , ok done added x shift

adjust scale and top bottom so each symbol fits and isnt off its cell.
@ symbol is annoying and also brackets , they go way off cell and arent that needed so freel free to just boot them from the pattern, i think i will

Ok added skip symbols, makes it easier to get rid of annoyances

1704635872662.png

Code:
0<0# : ^
''' 
@echo off
set script=%~f0
python -x "%script%" %*
exit /b 0
'''
import tkinter as tk
from tkinter import filedialog, font
from PIL import Image as PILImage, ImageDraw, ImageFont, ImageTk
from PIL import Image
import time
import os
try:
    from PIL import Image as PILImage, ImageFont, ImageTk, ImageGrab
    pillow_available = True
except ImportError:
    pillow_available = False
if pillow_available:
    print("Pillow library is available.")
else:
    install_pillow = messagebox.askyesno(
        "Pillow Not Installed",
        "Pillow library is required for this application. Do you want to install it?"
    )
    if install_pillow:
        try:
            import subprocess
            subprocess.run(["pip", "install", "Pillow"], check=True)
            from PIL import Image, ImageDraw, ImageFont, ImageTk, ImageGrab
            pillow_available = True
            print("Pillow library installed successfully.")
        except Exception as install_error:
            messagebox.showerror(
                "Installation Error",
                f"Error installing Pillow: {install_error}\nPlease install Pillow manually using 'pip install Pillow'."
            )
            exit(1)
    else:
        exit(1)
class FontEditorApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Font Editor")
        self.generated_font_canvas = tk.Canvas(self.root, width=600, height=600, borderwidth=0, highlightthickness=0)
        self.generated_font_canvas.pack(side=tk.RIGHT, padx=10)
        self.font_path = tk.StringVar()
        self.font_size_x = tk.IntVar(value=30)
        self.scaling = tk.DoubleVar(value=100.0)
        scaling_factor = self.scaling.get() / 100.0
        self.font_shift_y = tk.IntVar(value=0)
        self.font_shift_x = tk.IntVar(value=0)
        self.extra_scaling = tk.DoubleVar(value=100.0)
        self.extra_shift_y = tk.IntVar(value=0)
        self.empty_cell_color = tk.StringVar(value="50,0,0")
        self.font_color = tk.StringVar(value="255,150,0")
        self.font_symbol = tk.StringVar(value="A")
        self.skip_symbols = tk.StringVar(value="")  # Add skip_symbols attribute
        self.skip_symbols = tk.StringVar(value="@[]()")
        self.zoomed_canvas = tk.Canvas(self.root, width=4 * self.font_size_x.get(), height=4 * self.font_size_x.get(), borderwidth=0, highlightthickness=0)
        self.zoomed_canvas.pack(side=tk.TOP, padx=10, pady=10)
        self.result_img_rgb = None
        self.auto_apply_changes_on_startup = False
        self.load_default_font()
        self.create_widgets()
        self.load_default_font()
    def create_widgets(self):
        choose_font_button = tk.Button(self.root, text="CHOOSE TRUETYPE FONT", command=self.choose_font, font=("Arial", 14, "bold"), fg="red")
        choose_font_button.pack(pady=10)
        font_size_x_label = tk.Label(self.root, text="Font Size (X):", font=("Arial", 10, "bold"), fg="blue")
        font_size_x_label.pack()
        font_size_x_entry = tk.Entry(self.root, textvariable=self.font_size_x, font=("Arial", 10, "bold"), fg="red")
        font_size_x_entry.pack()
        scaling_label = tk.Label(self.root, text="Scaling Percentage:", font=("Arial", 10, "bold"), fg="blue")
        scaling_label.pack()
        scaling_entry = tk.Entry(self.root, textvariable=self.scaling, font=("Arial", 10, "bold"), fg="red")
        scaling_entry.pack()
        scaling_entry.bind('<KeyRelease>', lambda event: self.generate_full_image())
        font_shift_x_label = tk.Label(self.root, text="Font Shift (X):", font=("Arial", 10, "bold"), fg="blue")
        font_shift_x_label.pack()
        font_shift_x_entry = tk.Entry(self.root, textvariable=self.font_shift_x, font=("Arial", 10, "bold"), fg="red")
        font_shift_x_entry.pack()
        font_shift_y_label = tk.Label(self.root, text="Font Shift (Y):", font=("Arial", 10, "bold"), fg="blue")
        font_shift_y_label.pack()
        font_shift_y_entry = tk.Entry(self.root, textvariable=self.font_shift_y, font=("Arial", 10, "bold"), fg="red")
        font_shift_y_entry.pack()
        empty_cell_color_label = tk.Label(self.root, text="Background Color (RGB):", font=("Arial", 10, "bold"), fg="blue")
        empty_cell_color_label.pack()
        empty_cell_color_entry = tk.Entry(self.root, textvariable=self.empty_cell_color, font=("Arial", 10, "bold"), fg="red")
        empty_cell_color_entry.pack()
        font_color_label = tk.Label(self.root, text="Font Color (RGB):", font=("Arial", 10, "bold"), fg="blue")
        font_color_label.pack()
        font_color_entry = tk.Entry(self.root, textvariable=self.font_color, font=("Arial", 10, "bold"), fg="red")
        font_color_entry.pack()
        skip_symbols_label = tk.Label(self.root, text="Skip Symbols:", font=("Arial", 10, "bold"), fg="blue")
        skip_symbols_label.pack()
        skip_symbols_entry = tk.Entry(self.root, textvariable=self.skip_symbols, font=("Arial", 10, "bold"), fg="red")
        skip_symbols_entry.pack()
        apply_button = tk.Button(self.root, text="GENERATE", command=self.generate_full_image, font=("Arial", 14, "bold"), fg="red")
        apply_button.pack(pady=10)
        save_button = tk.Button(self.root, text="SAVE FONT", command=lambda: self.generate_full_image(save=True), font=("Arial", 14, "bold"), fg="red")
        save_button.pack(pady=10)
        for entry_widget in [font_size_x_entry, scaling_entry, font_shift_y_entry,
                             empty_cell_color_entry, font_color_entry, skip_symbols_entry]:
            entry_widget.bind('<KeyRelease>', self.on_entry_key_release)
        self.root.bind_all("<MouseWheel>", self.on_mousewheel)
    def on_entry_key_release(self, event):
        self.generate_full_image()
    def on_mousewheel(self, event):
        focused_widget = self.root.focus_get()
        self.root.after(500, self.generate_full_image)
        if hasattr(self, 'result_img_rgb') and self.result_img_rgb:
            self.display_zoomed_view(self.result_img_rgb)
        if isinstance(focused_widget, tk.Entry):
            if focused_widget.cget('textvariable') in [self.empty_cell_color, self.font_color]:
                current_value = focused_widget.get()
                rgb_values = [int(val) for val in current_value.split(',')]
                increment = 1
                for i in range(3):
                    rgb_values[i] = max(0, min(rgb_values[i] + increment if event.delta > 0 else rgb_values[i] - increment, 255))
                new_value = ','.join(map(str, rgb_values))
            else:
                try:
                    current_value = float(focused_widget.get())
                    increment = 1
                    new_value = current_value + increment if event.delta > 0 else current_value - increment
                    focused_widget.delete(0, tk.END)
                    focused_widget.insert(0, str(new_value))
                except ValueError:
                    pass
            focused_widget.focus_set()
    def choose_font(self):
        font_path = filedialog.askopenfilename(filetypes=[("Font files", "*.ttf;*.otf")])
        if font_path:
            self.font_path.set(font_path)
            self.display_temporary_message(f"FONT: {os.path.basename(font_path)}", duration=2000)
            self.generate_full_image()
            self.display_zoomed_view(self.result_img_rgb)
    def load_default_font(self):
        script_folder = os.path.dirname(os.path.abspath(__file__))
        files = os.listdir(script_folder)
        font_files = [f for f in files if f.lower().endswith(('.ttf', '.otf'))]
        if font_files:
            default_font_path = os.path.join(script_folder, font_files[0])
            self.font_path.set(default_font_path)
            print(f"Default font loaded: {default_font_path}")
            self.display_temporary_message(f"FONT: {default_font_path}", duration=2000)
            self.generate_full_image()
            self.display_zoomed_view(self.result_img_rgb)
            if not self.auto_apply_changes_on_startup:
                return
            self.apply_changes()
        else:
            print("No TrueType font found in the script's folder. Please choose a font.")
    def display_temporary_message(self, message, duration):
        existing_label = getattr(self, '_temp_label', None)
        if existing_label:
            existing_label.destroy()
        temp_label = tk.Label(self.root, text=message, font=("Arial", 20, "bold"), fg="red")
        temp_label.pack()
        self.root.after(duration, temp_label.destroy)
        self._temp_label = temp_label
    def apply_changes(self):
        self.generated_font_canvas.delete("all")
        font_shift_x_value = self.font_shift_x.get()
        empty_cell_rgb = tuple(map(int, self.empty_cell_color.get().split(',')))
        font_color_rgb = tuple(map(int, self.font_color.get().split(',')))
        font_size = self.font_size_x.get()
        try:
            font_path = self.font_path.get()
            fnt = ImageFont.truetype(font_path, font_size)
            img = Image.new("RGBA", (font_size, font_size), empty_cell_rgb + (0,))
            draw = ImageDraw.Draw(img)
            draw.text((0, 0), text=self.font_symbol.get(), font=fnt, fill=font_color_rgb)
            self.generate_full_image()
        except OSError as e:
            print(f"Error loading font: {e}")
    def generate_full_image(self, save=False):
        self.generated_font_canvas.delete("all")
        if self.scaling.get() == 0 or self.font_size_x.get() == 0:
            return
        pattern = [
            "0123456789ABCDEF",
            "0123456789ABCDEF",
            " !\"#$%&´()*+,-./",
            "0123456789:;{=?",
            "@ABCDEFGHIJKLMNO",
            "PQRSTUVWXYZ[\\]^_",
            "`abcdefghijklmno",
            "pqrstuvwxyz     ",
            "                ",
            "                ",
            "                ",
            "      EMPTY     ",
            "                ",
            "                ",
            "                ",
            "                ",
        ]
        empty_cell_rgb = tuple(map(int, self.empty_cell_color.get().split(',')))
        font_color_rgb = tuple(map(int, self.font_color.get().split(',')))
        font_size = self.font_size_x.get()
        scaling_factor = self.scaling.get() / 100.0
        font_shift_y_value = self.font_shift_y.get()
        font_shift_x_value = self.font_shift_x.get()
        symbol_size = int(font_size * scaling_factor)
        font_path = self.font_path.get()
        full_img = Image.new("RGBA", (len(pattern[0]) * font_size, len(pattern) * font_size), (0, 0, 0, 0))
        skip_symbols = set(self.skip_symbols.get())  # Get skip symbols
        for row, line in enumerate(pattern):
            max_height_in_row = 0  # Track the maximum height in the current row
            for col, char in enumerate(line):
                if char in skip_symbols:
                    continue  # Skip rendering the specified symbol
                additional_space = symbol_size // 2
                img = Image.new("RGBA", (symbol_size, symbol_size + 2 * additional_space), (255, 255, 255, 0))
                char_draw = ImageDraw.Draw(img)
                fnt = ImageFont.truetype(font_path, symbol_size)
                char_draw.text((0, additional_space), text=char, font=fnt, fill=font_color_rgb)
                bbox = img.getbbox()
                center_x = col * font_size + font_size / 2
                center_y = row * font_size + font_size / 2
                paste_x = int(col * font_size + font_shift_x_value)
                paste_y = int(center_y - max_height_in_row / 2 - font_size / 1.5 - additional_space + font_shift_y_value)
                full_img.paste(img, (paste_x, paste_y), img)
        grid_color = (255, 255, 255, 255)
        grid_size_x = len(pattern[0])
        grid_size_y = len(pattern)
        cell_size = font_size
        result_img_rgb = full_img.convert("RGBA")
        draw = ImageDraw.Draw(result_img_rgb)
        for x in range(0, grid_size_x * cell_size, cell_size):
            draw.line([(x, 0), (x, grid_size_y * cell_size)], fill=grid_color, width=1)
        for y in range(0, grid_size_y * cell_size, cell_size):
            draw.line([(0, y), (grid_size_x * cell_size, y)], fill=grid_color, width=1)
        black_img = Image.new("RGBA", full_img.size, empty_cell_rgb + (255,))
        result_img = Image.alpha_composite(black_img, result_img_rgb)
        result_img_rgb = result_img.convert("RGB")
        full_image_tk = ImageTk.PhotoImage(result_img_rgb)
        self.generated_font_canvas.image = full_image_tk
        self.generated_font_canvas.create_image(0, 0, anchor=tk.NW, image=full_image_tk)
        self.result_img_rgb = result_img_rgb
        if save:
            current_time = time.strftime("%Y%m%d%H%M%S")
            file_name = f"font_{current_time}.png"
            result_img_rgb = result_img_rgb.convert("RGBA")
            black_img = Image.new("RGBA", full_img.size, empty_cell_rgb + (255,))
            full_img_with_alpha = Image.alpha_composite(black_img, full_img.convert("RGBA"))
            full_img_with_alpha.convert("RGB").save(file_name)
            print(f"Image saved as: {file_name}")
            message = tk.Label(self.root, text=f"SAVED: {file_name}", font=("Arial", 14, "bold"), fg="red")
            message.pack(pady=10)
            self.root.after(2000, message.destroy)
    def display_zoomed_view(self, full_img_rgb):
        crop_x = 2 * self.font_size_x.get()
        crop_y = 2 * self.font_size_x.get()
        cropped_img = full_img_rgb.crop((0, 2, crop_x, crop_y))
        zoomed_img = cropped_img.resize((2 * crop_x, 2 * crop_y), Image.NEAREST)
        zoomed_img_tk = ImageTk.PhotoImage(zoomed_img)
        self.zoomed_canvas.image = zoomed_img_tk
        self.zoomed_canvas.create_image(0, 0, anchor=tk.NW, image=zoomed_img_tk)
if __name__ == "__main__":
    root = tk.Tk()
    app = FontEditorApp(root)
    root.mainloop()
 
Last edited:
Ok hheres the tool for typing with openbor fonts, save the code into bat file and run, but requires python
I tried to save as .bat and run it like I run the other tool (and I do have Phython) but it returns me this error:

File "path\FontTyper.bat", line 8, in <module>
from PIL import Image, ImageDraw, ImageFont, ImageTk,ImageGrab
ImportError: cannot import name 'Image' from 'PIL' (unknown location)
 
run again i t was autoloading image now it does not and should work, i updated the code
Ok get it again, it was one damn line leftover i used to debug how wide each symbol is to get better spacing.
Also if you dont have pillow in python then install it with pip install pillow in commandline

Ok i added a check if pillow is available, in commandline if its not then you will be asked to install it and gui will run, pillow is for manipulatin image data in python

Ok added check if pillow is installed in both guis and option to install it so it will jsut run with no issues
 
Last edited:
Ok added check if pillow is installed in both guis and option to install it so it will jsut run with no issues
same error:
from PIL import Image, ImageDraw, ImageFont, ImageTk,ImageGrab
ImportError: cannot import name 'Image' from 'PIL' (unknown location)
 
treplace thhe pil line on top with this one :from PIL import Image as PILImage, ImageDraw, ImageFont, ImageTk,ImageGrab

And ideally copy of it few lines below it or just copy the code cause i updated it.
theres lots of version of pillow depending what python you had so... some of them require different names which is why i kinda hate python

my python version is 3.10.9 , you check it in commandline like this: python --version
I i started to build on your version id probably encounter this issue as well and fix it but i have it working fine.
 
Last edited:
So for the final version, can I use this tool to create front sprites for Openbor games?
Is there a download link?
Thank you
 
New tool

This will check for all missing files on disk that are mentioned in txt file so you can fix the path or just delete it, i will probably do batch mode for this in the future so you will go over all entities in folder one by one. I think this process can be automated to autodelete but i still want to have control over it cause maybe you did a typo and the file is there with similar name.
As usual copyu the code to new txt file and rename the file to bat , like : txtcleaner.bat m then run it doubleclick, tested on win10, needs python like most of the tools.
aaas.jpg


Code:
0<0# : ^
'''
@echo off
set script=%~f0
python -x "%script%" %*
exit /b 0/b 0
'''
import tkinter as tk
from tkinter import filedialog, messagebox
import os
import fnmatch
class TextFileScannerApp:
    def __init__(self, master):
        self.master = master
        master.title("TXTCLEANER")
        self.frame = tk.Frame(master)
        self.frame.pack(fill="both", expand=True)
        self.text = tk.Text(self.frame, wrap="word", width=50, height=20)
        self.text.pack(side="left", fill="both", expand=True)
        self.scrollbar = tk.Scrollbar(self.frame, command=self.text.yview)
        self.scrollbar.pack(side="right", fill="y")
        self.text.config(yscrollcommand=self.scrollbar.set)
        self.button_frame = tk.Frame(master)
        self.button_frame.pack(side="right", fill="y")
        self.save_button = tk.Button(self.button_frame, text="Save TXT", command=self.save_file)
        self.save_button.pack(pady=5)
        self.load_button = tk.Button(master, text="Load TXT", command=self.load_file)
        self.load_button.pack(side="left", padx=5, pady=5)
        self.scan_button = tk.Button(master, text="Scan for missing files", command=self.scan_file)
        self.scan_button.pack()
        self.text_file_path = None
    def load_file(self):
        file_path = filedialog.askopenfilename(filetypes=[("Text files", "*.txt")])
        if file_path:
            self.text_file_path = file_path
            with open(file_path, "r") as file:
                self.text.delete("1.0", tk.END)
                self.text.insert(tk.END, file.read())
    def save_file(self):
        if not self.text_file_path:
            messagebox.showwarning("No File Loaded", "Please load a text file first.")
            return
        with open(self.text_file_path, "w") as file:
            file.write(self.text.get("1.0", tk.END))
        self.show_save_dialog()
    def scan_file(self):
        if not self.text_file_path:
            messagebox.showwarning("No File Loaded", "Please load a text file first.")
            return
        data_folder_index = self.text_file_path.rfind("data/")
        if data_folder_index == -1:
            messagebox.showwarning("Invalid File Path", "The loaded file is not inside a 'data/' folder.")
            return
        data_folder_path = self.text_file_path[:data_folder_index]
        content = self.text.get("1.0", tk.END)
        lines = content.split("\n")
        for i, line in enumerate(lines, start=1):
            if "data/" in line:
                path_with_extension = line[line.find("data/"):]
                path, extension = os.path.splitext(path_with_extension.strip())
                if not path.startswith("data/"):
                    continue
                extension = extension[:4]
                full_path = os.path.join(data_folder_path, path) + extension
                print("Checking path:", full_path)
                if not self.file_exists_case_insensitive(full_path):
                    self.show_error_dialog(i, full_path)
                    return
        self.text.tag_remove("highlight", "1.0", tk.END)
        print("ALL PATHS ARE GOOD!")
    def file_exists_case_insensitive(self, path):
        directory, filename = os.path.split(path)
        for file in os.listdir(directory):
            if fnmatch.fnmatch(file, filename):
                return True
        return False
    def show_error_dialog(self, line_number, full_path):
        dialog = tk.Toplevel(self.master)
        dialog.title("File Not Found")
        label = tk.Label(dialog, text=f"File not found on disk: {full_path}")
        label.pack(padx=20, pady=10)
        button_frame = tk.Frame(dialog)
        button_frame.pack(pady=10)
        def delete_line():
            content = self.text.get("1.0", tk.END).split("\n")
            del content[line_number - 1]
            self.text.delete("1.0", tk.END)
            self.text.insert("1.0", "\n".join(content))
            dialog.destroy()
            self.scan_file()  # Continue scanning
        delete_button = tk.Button(button_frame, text="DELETE LINE", command=delete_line)
        delete_button.pack(side="left", padx=10)
        ok_button = tk.Button(button_frame, text="EDIT LINE", command=dialog.destroy)
        ok_button.pack(side="left", padx=10)
        self.text.tag_add("highlight", f"{line_number}.0", f"{line_number}.end")
        self.text.tag_config("highlight", background="yellow")
        self.text.mark_set("insert", f"{line_number}.0")
        self.text.see(f"{line_number}.0")
        self.master.update_idletasks()
        master_width = self.master.winfo_width()
        master_height = self.master.winfo_height()
        master_x = self.master.winfo_x()
        master_y = self.master.winfo_y()
        dialog_width = dialog.winfo_reqwidth()
        dialog_height = dialog.winfo_reqheight()
        position_right = int(master_x + (master_width / 2) - (dialog_width / 2))
        position_down = int(master_y + (master_height / 2) - (dialog_height / 2) - (master_height / 6))  # slightly higher
        dialog.geometry(f"+{position_right}+{position_down}")
    def show_save_dialog(self):
        dialog = tk.Toplevel(self.master)
        dialog.title("File Saved")
        label = tk.Label(dialog, text="The file has been saved successfully.")
        label.pack(padx=20, pady=10)
        ok_button = tk.Button(dialog, text="OK", command=dialog.destroy)
        ok_button.pack(pady=10)
        self.master.update_idletasks()
        master_width = self.master.winfo_width()
        master_height = self.master.winfo_height()
        master_x = self.master.winfo_x()
        master_y = self.master.winfo_y()
        dialog_width = dialog.winfo_reqwidth()
        dialog_height = dialog.winfo_reqheight()
        position_right = int(master_x + (master_width / 2) - (dialog_width / 2))
        position_down = int(master_y + (master_height / 2) - (dialog_height / 2) - (master_height / 3))  # slightly higher
        dialog.geometry(f"+{position_right}+{position_down}")
root = tk.Tk()
root.tk.call('tk', 'scaling', 3.0)
app = TextFileScannerApp(root)
root.mainloop()
 
Last edited:
I updated the fontmaker, its pretty much good to go - you can totally use the output in the game without any edits at all now.
Openbor fonts are aligned to left side of each cell that made it a bit tricky but it works.
Im not sure if it will start without any ttf font in the same folder as the bat... so make sure you have fonts in folder, at least one.

Here is updated manual search, i just changed it so cursos already is on searchbox, it was annoying as hell
So to use it you need openbor manual html in same folder, the code should be inside bat file, you can lookup how manuyal html must be named in the code itself
fsd.jpg

Code:
0<0# : ^
'''
@echo off
set script=%~f0
python -x "%script%" %*
exit /b 0
'''
import subprocess
import sys
def install(package):
    subprocess.check_call([sys.executable, "-m", "pip", "install", package])
try:
    import tkinter
except ImportError:
    install('tkinter')
try:
    from bs4 import BeautifulSoup
except ImportError:
    install('beautifulsoup4')
try:
    from tkinter import ttk
except ImportError:
    install('ttk')
import tkinter as tk
from bs4 import BeautifulSoup
from tkinter import ttk
class ManualSearch:
    def __init__(self, root):
        self.root = root
        self.entry_text = tk.StringVar()
        self.entry = tk.Entry(root, textvariable=self.entry_text, font=('TkDefaultFont', 12, 'bold'), fg='red')
        self.entry.pack()
        self.entry.focus_set()  # Set focus to the search text box
        self.result = tk.Text(root, font=('TkDefaultFont', 12, 'bold'))  # Set the default font to bold
        self.result.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)  # Pack the result Text widget to the left
        self.entry.bind('<KeyRelease>', self.search)
        self.frame = tk.Frame(root)
        self.frame.pack(side=tk.RIGHT, fill=tk.Y)  # Pack the frame to the right
        self.scrollbar = tk.Scrollbar(self.frame)
        self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.listbox = tk.Listbox(self.frame, yscrollcommand=self.scrollbar.set, font=('TkDefaultFont', 12, 'bold'), fg='red')
        self.listbox.pack(side=tk.LEFT, fill=tk.BOTH)
        self.scrollbar.config(command=self.listbox.yview)
        self.listbox.bind('<<ListboxSelect>>', self.on_select)
        self.load_file()
    def load_file(self):
        try:
            with open('OpenBORManual - dreamcast.wiki.html', 'r', encoding='utf-8') as f:
                contents = f.read()
            self.soup = BeautifulSoup(contents, 'html.parser')
            terms = [term.text for term in self.soup.find_all('b') if term.text.strip()]
            terms.sort()  # Sort the terms in alphabetical order
            for term in terms:
                self.listbox.insert(tk.END, term)
        except Exception as e:
            print(f"Error loading file: {e}")
    def search(self, event):
        query = self.entry_text.get().lower()  # Convert query to lowercase
        self.result.delete('1.0', tk.END)
        self.result.tag_config('bold', font=('TkDefaultFont', 12, 'bold'))
        self.result.tag_config('red', foreground='red')  # Create a new tag for red text
        self.result.tag_config('bold_red', foreground='red', font=('TkDefaultFont', 12, 'bold'))  # Create a new tag for bold and red text
        found = False
        for term in self.soup.find_all('b'):
            if term.text.lower().startswith(query):  # Convert term to lowercase
                found = True
                self.display_term(term)
        if not found:
            for term in self.soup.find_all('b'):
                if query in term.text.lower():  # Convert term to lowercase
                    self.display_term(term)
    def display_term(self, term):
        self.result.tag_config('blue', foreground='blue')
        self.result.tag_config('red', foreground='red')
        h2_category = term.find_previous('h2')
        h1_category = term.find_previous('h1')
        if h2_category and (not h1_category or h2_category.find_previous('h1') == h1_category):
            self.result.insert(tk.END, f'{h2_category.text}\n', 'blue')  # Apply the 'blue' tag
        if h1_category:
            self.result.insert(tk.END, f'{h1_category.text}\n', 'blue')  # Apply the 'blue' tag
        self.result.insert(tk.END, f'{term.text}\n', 'red')  # Apply the 'red' tag
        explanation = term.find_next('ul')
        if explanation:
            for li in explanation.find_all('li'):
                self.result.insert(tk.END, f'{li.text}\n\n')
            next_p = explanation.find_next_sibling('p')
            if next_p:
                next_term = next_p.find_next('b')
                if not next_term:
                    self.result.insert(tk.END, f'{next_p.text}\n\n')
        else:
            next_term = term.find_next('b')
            if next_term:
                self.result.insert(tk.END, '\n')
    def on_select(self, event):
        selected_term = self.listbox.get(self.listbox.curselection())
        self.entry_text.set(selected_term)
        self.search(None)
root = tk.Tk()
root.title("OpenBOR Manual reference")
root.tk.call('tk', 'scaling', 2.0)
app = ManualSearch(root)
root.mainloop()






-----------------


Ok it loads txt files so you can feed it your levels.txt file , also visually shows negative values gray cause they are usualy offscreen.
If a value is not set in your loaded txt file - it will use -9999 for x y , if font value is not set it will use default 0.

huuu.png
 
Last edited:
Back
Top Bottom