retro-go/tools/font_editor.py
Raphael Texier f6fb3be2ca
Added a python tool to create and edit fonts and added diacritics to french translations (#168)
* Update translations.h

Adding diacritics
File encoding is Latin-1 (ISO 8859-1)

* rg_gui: adding missing translations

* rg_gui: adding missing translations

* moving converter to tool folder
Creating basic UI

* font_converter : upgrading UI

adding settings fields for the user

* fonts: adding source fonts

I might delete some of them later...

* font_converter: updated the tool

it's now working properly (sort of) !

* create basic C font renderer

I now have to code the part where the user can edit the C file using the canvas like some sort of Windows Paint

* font_converter: few tweaks

made the code a bit shorter and more readable

* font_converter: reverse change that caused an issue

* C_font_editor: Now work !!!

* font_converter: some tweaks on file output comments

* Font tools: solved issue when exporting char with empty data

(such as space)

* font_converter: Added zoom function

+ made the tools full screen

* font_converter: now can handle 'space' character

* font toolss: added a way to exclude some characters

and updated file header

* font_tool: fixing error with file output

* rg_gui: new font renderer for LVGL type font

* Removing old font and adding "LVGL" verabold

* rg_gui: removing bit per pixel option

* rg_gui: renaming a struct to match the rest of the code

* font_tools: replaced treshold by bit shifting

* fonttool: updated the generator to match the new font format

* Font_converter: now generate decent results

* fonttools: adding some fonts + somes tweaks

* font_editor: works again with the new format !

* font_tools: small tweaks

* rg_gui: added back vertical stretching

* rg_gui: moved output specific data inside the if(output)

* rg_gui: small tweak

* font_tools: small tweaks on advance_width

* font_editor: new gui

* font_editor: improved ergonomics

* Renamed edit_c_font to font_editor

* Add support for old format to font_converter.py

* Missing commas in font_converter.py's output

* Fixed memory usage calculation in font_converter

* Auto generate data when selecting a font file

Automatically generate the canvas when loading a font. Also fixed: Cancelling the file selector will no longer clear the font name

* Draw bounding box after the pixels, so that it's fully visible

* Do not save empty bitmaps

* Added save dialog to font converter

* Replaced the blue dot with a box representing the real footprint of the glyph

* Converted translations.h to UTF-8

* Made font_editor save in UTF-8

* Removed new format support in font_converter

I'm still working on adapting font_editor...

* Add option to ensure that font size is respected

* font_converter can now load C fonts, and font_editor now imports its load/save abilities

* Fixed typo

* Fixed max height and memory calculations in font_editor

* Blue bounding box should show the actual height taken by the glyph (aka the max height)

* Preserve the real padding in the rendered glyph

* Reduce ofs_y when possible

* Regenerated fonts with font_converter.py

I'm still tweaking font_converter.py but the result is pretty close to the previous fonts. In some cases the line height is increased because of diatrics unfortunately...

* Restore upstream fonts to resolve conflict

* Resolving conflicts

* Fixed some glyphs were clipped on the left

* Fix conflicts

* More logic to reduce max_height

* Renamed Original_fonts to just fonts

* Removed the mention of the old tool now that we have our own

---------

Co-authored-by: Raphael Texier <157415568+raphatex@users.noreply.github.com>
Co-authored-by: Alex Duchesne <ducalex007@gmail.com>
2025-07-26 17:22:09 -04:00

391 lines
12 KiB
Python

from tkinter import Tk, Button, Frame, Canvas, filedialog, Checkbutton, IntVar, Label, StringVar, Entry, DISABLED, NORMAL
from font_converter import load_c_font, generate_c_font
import os
font_size = 14
char_code_edit = ord('R')
selected_glyph = 0
list_bbox = [] # ((cc, x0, y0, x1, y1), (cc, x0, y0, x1, y1), ...) used to find the correct glyph on the canva
list_glyph_data = dict() # contain font data for all glyphs
lastrect_xy = (0,0) # used for sliding function
def renderCfont():
canvas.delete("all")
global select_box
select_box = canvas.create_rectangle(0,0,p_size,p_size, width=2, outline="blue")
global list_bbox
list_bbox = []
offset_x_1 = 1
offset_y_1 = 1
max_height_local = 1
global list_glyph_data
# we get the char list to render
if list_char_render.get() != "":
list_char_code_render = [ord(i) for i in (list_char_render.get())]
else:
list_char_code_render = list(list_glyph_data.keys())
for char_code in list_char_code_render:
offset_y = list_glyph_data[char_code]['ofs_y']
width = list_glyph_data[char_code]['box_w']
height = list_glyph_data[char_code]['box_h']
offset_x = list_glyph_data[char_code]['ofs_x']
xDelta = list_glyph_data[char_code]['adv_w']
max_height_local = max(max_height_local, offset_y + height)
offset_x_1 += offset_x
if bounding_box_bool.get():
canvas.create_rectangle((offset_x_1)*p_size, (offset_y_1+offset_y)*p_size, (width+offset_x_1)*p_size, (height+offset_y_1+offset_y)*p_size, width=1, outline="red",fill='') # bounding box
bbox = (
char_code,
offset_x_1,
offset_y_1+offset_y,
width+offset_x_1,
height+offset_y_1+offset_y
)
byte_list = list_glyph_data[char_code]["bitmap"]
if byte_list:
bitmap_index = 0
bit_index = 0
byte = byte_list[bitmap_index]
modulo_8 = (True if width*height%8 == 0 else False)
for y in range(height):
for x in range(width):
if byte & 0b10000000: # Pixel(x,y) = 1
canvas.create_rectangle((x+offset_x_1)*p_size, (y+offset_y_1+offset_y)*p_size, (x+offset_x_1)*p_size+p_size, (y+offset_y_1+offset_y)*p_size+p_size,fill="white")
if bit_index == 7:
bit_index = 0
bitmap_index += 1
if not (modulo_8 and y == height-1):
byte = byte_list[bitmap_index]
else:
byte = byte << 1 # we shift data[n] to get the next pixel on the most significant bit
bit_index += 1
if offset_x_1+3*xDelta <= canva_width:
offset_x_1 += xDelta
else:
offset_x_1 = 1
offset_y_1 += max_height_local
max_height_local = 1
list_bbox.append(bbox)
def render_single_char():
canvas_1.delete("all")
# we also have to clear the matrix
global rect_ids
rect_ids = []
for y in range(40):
line = [-1] * 40
rect_ids.append(line)
global char_code_edit
char_code_edit = ord(char_to_edit.get())
global list_glyph_data
offset_y = list_glyph_data[char_code_edit]['ofs_y']
width = list_glyph_data[char_code_edit]['box_w']
height = list_glyph_data[char_code_edit]['box_h']
offset_x = list_glyph_data[char_code_edit]['ofs_x']
advance_width = list_glyph_data[char_code_edit]['adv_w']
max_size = max(width+offset_x, height+offset_y)
canvas_width = (screen_width // 4)
canvas_height = (screen_height // 2)
global p_size_c
p_size_c = min(canvas_width, canvas_height) // max_size - 1
# ov is the small pixel that stick to the mouse, we want to keep this one
global ov
ov = canvas_1.create_rectangle(0,0,p_size_c,p_size_c,fill="white")
glyph_data_text = (
'width : ' + str(width) + '\n' +
'height : ' + str(height) + '\n' +
'offset_x : ' + str(offset_x) + '\n' +
'offset_y : ' + str(offset_y) + '\n' +
'advance_width : ' + str(advance_width))
output.config(text = glyph_data_text)
global single_bbox
single_bbox = (offset_x, offset_y, offset_x+width, offset_y+height)
bit_index = 0
if bounding_box_bool.get():
canvas_1.create_rectangle((offset_x)*p_size_c, (offset_y)*p_size_c, (width+offset_x)*p_size_c, (height+offset_y)*p_size_c, width=1, outline="red",fill='') # bounding box
byte_list = list_glyph_data[char_code_edit]["bitmap"]
bitmap_index = 0
byte = byte_list[bitmap_index]
modulo_8 = (True if width*height%8 == 0 else False)
for y in range(height):
for x in range(width):
if byte & 0b10000000: # Pixel(x,y) = 1
rect_ids[y+offset_y][x+offset_x] = canvas_1.create_rectangle(
(x+offset_x)*p_size_c,
(y+offset_y)*p_size_c,
(x+offset_x)*p_size_c+p_size_c,
(y+offset_y)*p_size_c+p_size_c,fill="white")
if bit_index == 7:
bit_index = 0
bitmap_index += 1
if not (modulo_8 and y == height-1):
byte = byte_list[bitmap_index]
else:
byte = byte << 1 # we shift data[n] to get the next pixel on the most significant bit
bit_index += 1
def update_glyph_data():
x0, y0, x1, y1 = single_bbox
height = y1 - y0
width = x1 - x0
global char_code_edit
list_glyph_data[char_code_edit]["bitmap"] = []
row = 0
i = 0
for y in range(height):
for x in range(width):
pixel = (1 if rect_ids[y + y0][x + x0] != -1 else 0)
if i == 8:
list_glyph_data[char_code_edit]["bitmap"].append(row)
row = 0
i = 0
row = (row << 1) | pixel
i += 1
row = row << 8-i # to "fill" with zero the remaining empty bits
list_glyph_data[char_code_edit]["bitmap"].append(row)
save_font()
renderCfont()
def save_font():
global list_glyph_data
global font_path
font_name = os.path.splitext(os.path.basename(font_path))[0]
with open(font_path, 'w', encoding='UTF-8') as f:
f.write(generate_c_font(font_name, font_size, list_glyph_data.values()))
def extract_data():
global list_glyph_data
list_glyph_data.clear()
font_name, font_size, font_data = load_c_font(font_path)
for glyph in font_data:
list_glyph_data[glyph["char_code"]] = glyph
b1.config(state=NORMAL)
b3.config(state=NORMAL)
b4.config(state=NORMAL)
renderCfont()
def select_file():
filetypes = (
('c font', '*.c'),
('All files', '*.*')
)
filename = filedialog.askopenfilename(
title='Open a c Font',
initialdir=os.getcwd(),
filetypes=filetypes)
global font_path
font_path = filename
extract_data()
def motion(event):
x = event.x // p_size
y = event.y // p_size
global selected_glyph
for bbox in list_bbox:
cc, x0, y0, x1, y1 = bbox
if x >= x0 and y >= y0 and x <= x1 and y <= y1:
canvas.coords(select_box, x0*p_size-1, y0*p_size-1, x1*p_size+1, y1*p_size+1)
selected_glyph = cc
def click(event):
global char_to_edit
global selected_glyph
char_to_edit.set(chr(selected_glyph))
render_single_char()
def motion_1(event):
global x
global y
x = event.x
y = event.y
if x%p_size_c != 0:
x-= x%p_size_c
if y%p_size_c != 0:
y-= y%p_size_c
canvas_1.itemconfig(ov, fill="White")
canvas_1.coords(ov, x, y, x+p_size_c, y+p_size_c)
def click_1(event):
x_pixel = x//p_size_c
y_pixel = y//p_size_c
if rect_ids[y_pixel][x_pixel] == -1:
rect_ids[y_pixel][x_pixel] = canvas_1.create_rectangle(x, y, x + p_size_c, y + p_size_c, fill="white")
else:
canvas_1.delete(rect_ids[y_pixel][x_pixel])
rect_ids[y_pixel][x_pixel] = -1
canvas_1.itemconfig(ov, fill="Black") # Changes the fill color to black to "hide" it
def slide_1(event):
global x
global y
global lastrect_xy
x = event.x
y = event.y
if x%p_size_c != 0:
x-= x%p_size_c
if y%p_size_c != 0:
y-= y%p_size_c
canvas_1.coords(ov, x, y, x+p_size_c, y+p_size_c)
x_pixel = x//p_size_c
y_pixel = y//p_size_c
if rect_ids[y_pixel][x_pixel] != lastrect_xy:
if rect_ids[y_pixel][x_pixel] == -1:
rect_ids[y_pixel][x_pixel] = canvas_1.create_rectangle(x, y, x + p_size_c, y + p_size_c, fill="white")
else:
canvas_1.delete(rect_ids[y_pixel][x_pixel])
rect_ids[y_pixel][x_pixel] = -1
lastrect_xy = (y_pixel,x_pixel)
window = Tk()
window.title("C font editor")
# TODO : make it dynamic
p_size = 6 # pixel size on the global renderer
p_size_c = 24 # pixel size on the single char renderer
# Get screen width and height
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
# Set the window size to fill the entire screen
window.geometry(f"{screen_width}x{screen_height}")
char_edit_windows_width = (screen_width // 4)//p_size_c
char_edit_windows_height = (screen_height // 2)//p_size_c
canva_width = (screen_width-screen_width // 4)//p_size
canva_height = screen_height//p_size-16
frame = Frame(window)
frame.pack(anchor="center", padx=10, pady=2)
rect_ids = [] # This is gonna be used to store rectangles ids
for y in range(40):
line = [-1] * 40 # Create a new list for each row
rect_ids.append(line)
########## top ##########
# choose font button
choose_font_button = Button(frame, text='Choose C font', width=16, height=2, background="blue", foreground="white", command=select_file)
choose_font_button.pack(side="left", padx=5)
# Variable to hold the state of the checkbox
bounding_box_bool = IntVar() # 0 for unchecked, 1 for checked
checkbox = Checkbutton(frame, text="Bounding box", variable=bounding_box_bool)
checkbox.pack(side="left", padx=5)
# Label and Entry for String to render
Label(frame, text="String to render:").pack(side="left", padx=5)
list_char_render = StringVar(value=str(""))
Entry(frame, textvariable=list_char_render, width=50).pack(side="left", padx=5)
b1 = Button(frame, text="Render", width=12, height=2, background="blue", foreground="white", command=renderCfont)
b1.pack(side="left", padx=5)
########## end of top ##########
########## bottom ##########
frame_bottom = Frame(window)
frame_bottom.pack(anchor="s", padx=2, pady=2)
##### left side #####
frame_left = Frame(frame_bottom)
frame_left.pack(anchor="center", side="left", padx=2, pady=2)
# Label and Entry for Chars to render
Label(frame_left, text="Character to edit").pack(side="top", pady=2)
char_to_edit = StringVar(value=str("R"))
Entry(frame_left, textvariable=char_to_edit, width=4).pack(side="top", pady=2)
b3 = Button(frame_left, text="render", width=12, height=2, background="blue", foreground="white", command=render_single_char)
b3.pack(side="top", padx=5)
# display the glyph data
Label(frame_left, text="Glyph data :").pack(side="top", pady=2)
output = Label(frame_left, height = 5, width = 25, bg = "light cyan")
output.pack(side="top", padx=5)
b4 = Button(frame_left, text="save", width=12, height=2, background="green", foreground="white", command=update_glyph_data)
b4.pack(side="top", padx=5)
# disable buttons until a font is loaded
b1.config(state=DISABLED)
b3.config(state=DISABLED)
b4.config(state=DISABLED)
canvas_1 = Canvas(frame_left, width=char_edit_windows_width*p_size_c, height=char_edit_windows_height*p_size_c, bg="black")
canvas_1.pack(side="left", padx=5)
# ov is the small pixel that 'stick' to the mouse
ov = canvas_1.create_rectangle(0,0,p_size_c,p_size_c,fill="white")
canvas_1.focus_set()
canvas_1.bind('<Motion>', motion_1)
canvas_1.bind("<Button 1>",click_1)
canvas_1.bind("<B1-Motion>",slide_1)
##### end of left side #####
##### right side #####
frame_right = Frame(frame_bottom)
frame_right.pack(side="right", padx=2, pady=2)
canvas = Canvas(frame_right, width=canva_width*p_size, height=canva_height*p_size, bg="black")
canvas.pack(anchor="n", side="left", padx=5)
canvas.focus_set()
canvas.bind('<Motion>', motion)
canvas.bind("<Button 1>",click)
select_box = canvas.create_rectangle(0,0,p_size,p_size, width=2, outline="blue")
##### end of right side #####
########## end of bottom ##########
window.mainloop()