retro-go 1.45 编译及显示中文

        最近做了个使用 retro-go 的开源掌机 基于ESP32-S3的C19掌机(适配GBC外壳) - 立创开源硬件平台 ,做完后用提供的固件发现屏幕反显了,估计是屏幕型号不太对,随即自己拉 retro-go 官方库来编译,拉取的最新的 1.45 ,记录下适配的过程和解决的一些问题。

  1. 安装 esp-idf 5.2 ,retro-go 介绍可以用idf5.3 但我用5.3报错
  2. 拉取 retro-go ,在/components/retro-go/targets 下增加 一个target ,参考已经项目与自己硬件最接近版本,注意 key map 的设置格式
  3. 在/components/retro-go 下的 config.h 中 参考其它的target 将自己增加的target 引入进来

支持中文:

  1. 找一个中文的 ttf 可以用这个 https://github.com/TakWolf/fusion-pixel-font
  2. 用 font_converter.py 设置路径与字符范围及字体大小后生成c源文件
  3. 在 fonts.h 中增加生成的源文件内的变量名和设置枚举与 fonts 数组
  4. sdkconfig 中设置 CONFIG_FATFS_API_ENCODING_UTF_8=y 开启 fatfs 使用unicode 编码读取文件名

如果不开启 utf8 会导致 rg_utf8_get_codepoint 方法后台报 invalid utf-8 prefix,也就是无法正确得到文件名 。

参考:

Configuration Options Reference - ESP32-S3 - — ESP-IDF 编程指南 v5.5 文档

https://elm-chan.org/fsw/ff/doc/config.html#lfn_unicode

解决开启中文浏览列表会很卡的问题

        增加中文字体后且能成功显示中文字体后,会发现列表变得很卡,查看源码后知道其查询字库是一个个遍历的,字符数变多后肯定会卡,解决方法是字符数据定长或再加一个索引数组。不管哪种方法其本质都是希望能通过字符号+偏移量定位到具体字符数据,下面是主要代码:



typedef struct
{
    char name[16];
    uint8_t type;   // 0=monospace, 1=proportional ,2= location by map
    uint8_t width;  // median width of glyphs
    uint8_t height; // height of tallest glyph
    size_t  chars;  // glyph count
    const uint32_t *map; //索引数组
    uint32_t map_len;//索引数组长度
    uint32_t map_start_code;//索引的第一个字符码
    uint8_t data[]; // stream of rg_font_glyph_t (end of list indicated by an entry with 0x0000 codepoint)
} rg_font_t;


//rg_gui.c  get_glyph  增加 font->type == 2  的逻辑 小于等于255 直接查询,从map_start_code 开始使用索引
//   这只是方法的一部分
static size_t get_glyph(uint32_t *output, const rg_font_t *font, int points, int c)
{
    // Some glyphs are always zero width
    if (!font || c == '\r' || c == '\n' || c == 0) // || c < 8 || c > 0xFFFF)
        return 0;

    if (points <= 0)
        points = font->height;


    const uint8_t *ptr = font->data;
    const rg_font_glyph_t *glyph = (rg_font_glyph_t *)ptr;
    if(font->type == 2){

        if (c <= 255)
        {
            int times =0;
            while (glyph->code && glyph->code != c && times++ <=255)
            {
                if (glyph->width != 0)
                    ptr += (((glyph->width * glyph->height) - 1) / 8) + 1;
                ptr += sizeof(rg_font_glyph_t);
                glyph = (rg_font_glyph_t *)ptr;
            }
        }else if(c >= font->map_start_code){
            uint32_t map_index =  c - font->map_start_code;
            if (map_index < font->map_len)
            {
                uint32_t data_index = font->map[map_index];
                glyph = (rg_font_glyph_t *)(ptr + data_index);
            }
        }
    }
    else
    {
        // for (size_t i = 0; i < font->chars && glyph->code && glyph->code != c; ++i)
        while (glyph->code && glyph->code != c)
        {
            if (glyph->width != 0)
                ptr += (((glyph->width * glyph->height) - 1) / 8) + 1;
            ptr += sizeof(rg_font_glyph_t);
            glyph = (rg_font_glyph_t *)ptr;
        }
    }

 

        修改后的库:https://github.com/longxiangam/retro-go

       tools 下生成字库的 font_converter.py 脚本增加对索引的支持,使用这个脚本生成字库时选择生成 map 并设置 start code 生成的代码就会生成 索引数组。

from PIL import Image, ImageDraw, ImageFont
from tkinter import Tk, Label, Entry, StringVar, Button, Frame, Canvas, filedialog, ttk, Checkbutton, IntVar
import os
import re
import uuid

################################ - Font format - ################################
#
# font:
# |
# ├── glyph_bitmap[] -> 8 bit array containing the bitmap data for all glyph
# |
# └── glyph_data[] -> struct that contains all the data to correctly draw the glyph
#
######################## - Explanation of glyph_bitmap[] - #######################
# First, let's see an example : '!'
#
# we are going to convert glyph_bitmap[] bytes to binary :
# 11111111,
# 11111111,
# 11000111,
# 11100000,
#
# then we rearrange them :
#  [3 bits wide]
#       111
#       111
#       111
# [9    111   We clearly reconize '!' character
# bits  111
# tall] 111
#       000
#       111
#       111
#       (000000)
#
# Second example with '0' :
# 0x30,0x04,0x07,0x09,0x00,0x07,
# 0x7D,0xFB,0xBF,0x7E,0xFD,0xFB,0xFF,0x7C,
#
# - width = 0x07 = 7
# - height = 0x09 = 9
# - data[n] = 0x7D,0xFB,0xBF,0x7E,0xFD,0xFB,0xFF,0x7C
#
# in binary :
# 1111101
# 11111011
# 10111111
# 1111110
# 11111101
# 11111011
# 11111111
# 1111100
#
# We see that everything is not aligned so we add zeros ON THE LEFT :
# ->01111101
#   11111011
#   10111111
# ->01111110
#   11111101
#   11111011
#   11111111
# ->01111100
#
# Next, we rearrange the bits :
#    [ 7 bits wide]
#       0111110
#       1111110
#       1110111
# [9    1110111
# bits  1110111     we can reconize '0' (if you squint a loooot)
# tall] 1110111
#       1110111
#       1111111
#       0111110
#       (0)
#
# And that's basically how characters are encoded using this tool

# Example usage (defaults parameters)
list_char_ranges_init = "32-126, 160-255,19968-40959"
font_size_init = 12
map_start_code_init = "19968"  # Default map start code

font_path = ("arial.ttf")  # Replace with your TTF font path

# Variables to track panning
start_x = 0
start_y = 0

def get_char_list():
    list_char = []
    for intervals in list_char_ranges.get().split(','):
        first = intervals.split('-')[0]
        # we check if the user input is a single char or an interval
        try:
            second = intervals.split('-')[1]
        except IndexError:
            list_char.append(int(first))
        else:
            for char in range(int(first), int(second) + 1):
                list_char.append(char)
    return list_char

def find_bounding_box(image):
    pixels = image.load()
    width, height = image.size
    x_min, y_min = width, height
    x_max, y_max = 0, 0

    for y in range(height):
        for x in range(width):
            if pixels[x, y] >= 1:  # Looking for 'on' pixels
                x_min = min(x_min, x)
                y_min = min(y_min, y)
                x_max = max(x_max, x)
                y_max = max(y_max, y)

    if x_min > x_max or y_min > y_max:  # No target pixels found
        return None
    return (x_min, y_min, x_max+1, y_max+1)

def load_ttf_font(font_path, font_size):
    # Load the TTF font
    enforce_font_size = enforce_font_size_bool.get()
    pil_font = ImageFont.truetype(font_path, font_size)

    font_name = ' '.join(pil_font.getname())
    font_data = []

    for char_code in get_char_list():
        char = chr(char_code)

        image = Image.new("1", (font_size * 2, font_size * 2), 0) # generate mono bmp, 0 = black color
        draw = ImageDraw.Draw(image)
        # Draw at pos 1 otherwise some glyphs are clipped. we remove the added offset below
        draw.text((1, 0), char, font=pil_font, fill=255)

        bbox = find_bounding_box(image)  # Get bounding box

        if bbox is None: # control character / space
            width, height = 0, 0
            offset_x, offset_y = 0, 0
        else:
            x0, y0, x1, y1 = bbox
            width, height = x1 - x0, y1 - y0
            offset_x, offset_y = x0, y0
            if offset_x:
                offset_x -= 1

        try: # Get the real glyph width including padding on the right that the box will remove
            adv_w = int(draw.textlength(char, font=pil_font))
            adv_w = max(adv_w, width + offset_x)
        except:
            adv_w = width + offset_x

        # Shift or crop glyphs that would be drawn beyond font_size. Most glyphs are not affected by this.
        # If enforce_font_size is false, then max_height will be calculated at the end and the font might
        # be taller than requested.
        if enforce_font_size and offset_y + height > font_size:
            print(f"    font_size exceeded: {offset_y+height}")
            if font_size - height >= 0:
                offset_y = font_size - height
            else:
                offset_y = 0
                height = font_size

        # Extract bitmap data
        cropped_image = image.crop(bbox)
        bitmap = []
        row = 0
        i = 0
        for y in range(height):
            for x in range(width):
                if i == 8:
                    bitmap.append(row)
                    row = 0
                    i = 0
                pixel = 1 if cropped_image.getpixel((x, y)) else 0
                row = (row << 1) | pixel
                i += 1
        bitmap.append(row << 8-i) # to "fill" with zero the remaining empty bits
        bitmap = bitmap[0:int((width * height + 7) / 8)]

        # Create glyph entry
        glyph_data = {
            "char_code": char_code,
            "ofs_y": int(offset_y),
            "box_w": int(width),
            "box_h": int(height),
            "ofs_x": int(offset_x),
            "adv_w": int(adv_w),
            "bitmap": bitmap,
        }
        font_data.append(glyph_data)

    # The font render glyphs at font_size but they can shift them up or down which will cause the max_height
    # to exceed font_size. It's not desirable to remove the padding entirely (the "enforce" option above), 
    # but there are some things we can do to reduce the discrepency without affecting the look.
    max_height = max(g["ofs_y"] + g["box_h"] for g in font_data)
    if max_height > font_size:
        min_ofs_y = min((g["ofs_y"] if g["box_h"] > 0 else 1000) for g in font_data)
        for key, glyph in enumerate(font_data):
            offset = glyph["ofs_y"]
            # If there's a consistent excess of top padding across all glyphs, we can remove it
            if min_ofs_y > 0 and offset >= min_ofs_y:
                offset -= min_ofs_y
            # In some fonts like Vera and DejaVu we can shift _ and | to gain an extra pixel
            if chr(glyph["char_code"]) in ["_", "|"] and offset + glyph["box_h"] > font_size and offset > 0:
                offset -= 1
            font_data[key]["ofs_y"] = offset

        max_height = max(g["ofs_y"] + g["box_h"] for g in font_data)

    print(f"Glyphs: {len(font_data)}, font_size: {font_size}, max_height: {max_height}")

    return (font_name, font_size, font_data)

def load_c_font(file_path):
    # Load the C font
    font_name = "Unknown"
    font_size = 0
    font_data = []

    with open(file_path, 'r', encoding='UTF-8') as file:
        text = file.read()
        text = re.sub('//.*?$|/\*.*?\*/', '', text, flags=re.S|re.MULTILINE)
        text = re.sub('[\n\r\t\s]+', ' ', text)
        # FIXME: Handle parse errors...
        if m := re.search('\.name\s*=\s*"(.+)",', text):
            font_name = m.group(1)
        if m := re.search('\.height\s*=\s*(\d+),', text):
            font_size = int(m.group(1))
        if m := re.search('\.data\s*=\s*\{(.+?)\}', text):
            hexdata = [int(h, base=16) for h in re.findall('0x[0-9A-Fa-f]{2}', text)]

    while len(hexdata):
        char_code = hexdata[0] | (hexdata[1] << 8)
        if not char_code:
            break
        ofs_y = hexdata[2]
        box_w = hexdata[3]
        box_h = hexdata[4]
        ofs_x = hexdata[5]
        adv_w = hexdata[6]
        bitmap = hexdata[7:int((box_w * box_h + 7) / 8) + 7]

        glyph_data = {
            "char_code": char_code,
            "ofs_y": ofs_y,
            "box_w": box_w,
            "box_h": box_h,
            "ofs_x": ofs_x,
            "adv_w": adv_w,
            "bitmap": bitmap,
        }
        font_data.append(glyph_data)

        hexdata = hexdata[7 + len(bitmap):]

    return (font_name, font_size, font_data)

def generate_font_data():
    if font_path.endswith(".c"):
        font_name, font_size, font_data = load_c_font(font_path)
    else:
        font_name, font_size, font_data = load_ttf_font(font_path, int(font_height_input.get()))

    window.title(f"Font preview: {font_name} {font_size}")
    font_height_input.set(font_size)

    max_height = max(font_size, max(g["ofs_y"] + g["box_h"] for g in font_data))
    bounding_box = bounding_box_bool.get()

    canvas.delete("all")
    offset_x_1 = 1
    offset_y_1 = 1

    for glyph_data in font_data:
        offset_y = glyph_data["ofs_y"]
        width = glyph_data["box_w"]
        height = glyph_data["box_h"]
        offset_x = glyph_data["ofs_x"]
        adv_w = glyph_data["adv_w"]

        if offset_x_1+adv_w+1 > canva_width:
            offset_x_1 = 1
            offset_y_1 += max_height + 1

        byte_index = 0
        byte_value = 0
        bit_index = 0
        for y in range(height):
            for x in range(width):
                if bit_index == 0:
                    byte_value = glyph_data["bitmap"][byte_index]
                    byte_index += 1
                if byte_value & (1 << 7-bit_index):
                    canvas.create_rectangle((x+offset_x_1+offset_x)*p_size, (y+offset_y_1+offset_y)*p_size, (x+offset_x_1+offset_x)*p_size+p_size, (y+offset_y_1+offset_y)*p_size+p_size,fill="white")
                bit_index += 1
                bit_index %= 8

        if bounding_box:
            canvas.create_rectangle((offset_x_1+offset_x)*p_size, (offset_y_1+offset_y)*p_size, (width+offset_x_1+offset_x)*p_size, (height+offset_y_1+offset_y)*p_size, width=1, outline="red", fill='')
            canvas.create_rectangle((offset_x_1)*p_size, (offset_y_1)*p_size, (offset_x_1+adv_w)*p_size, (offset_y_1+max_height)*p_size, width=1, outline='blue', fill='')

        offset_x_1 += adv_w + 1

    return (font_name, font_size, font_data)

def save_font_data():
    font_name, font_size, font_data = generate_font_data()

    filename = filedialog.asksaveasfilename(
        title='Save Font',
        initialdir=os.getcwd(),
        initialfile=f"{font_name.replace('-', '_').replace(' ', '')}{font_size}",
        defaultextension=".c",
        filetypes=(('Retro-Go Font', '*.c'), ('All files', '*.*')))

    if filename:
        with open(filename, 'w', encoding='UTF-8') as f:
            f.write(generate_c_font(font_name, font_size, font_data))

def generate_c_font(font_name, font_size, font_data):
    normalized_name = f"{font_name.replace('-', '_').replace(' ', '')}{font_size}"
    max_height = max(font_size, max(g["ofs_y"] + g["box_h"] for g in font_data))
    memory_usage = sum(len(g["bitmap"]) + 7 for g in font_data)  # 7 bytes for header

    # Calculate map data if enabled
    generate_map = generate_map_bool.get()
    map_start_code = int(map_start_code_input.get()) if generate_map else 0
    map_data = []
    if generate_map:
        # Find the range for the map
        char_codes = [g["char_code"] for g in font_data]
        max_char = max(char_codes)
        map_size = max_char - map_start_code + 1
        map_data = [0] * map_size  # Initialize with zeros
        data_index = 0
        for glyph in font_data:
            map_index = glyph["char_code"] - map_start_code
            if 0 <= map_index < map_size:
                map_data[map_index] = data_index
            data_index += 7 + len(glyph["bitmap"])  # 7 bytes header + bitmap size
        memory_usage += map_size * 4  # Each map entry is 4 bytes (uint32_t)

    file_data = "#include \"../rg_gui.h\"\n\n"
    file_data += "// File generated with font_converter.py (https://github.com/ducalex/retro-go/tree/dev/tools)\n\n"
    file_data += f"// Font           : {font_name}\n"
    file_data += f"// Point Size     : {font_size}\n"
    file_data += f"// Memory usage   : {memory_usage} bytes\n"
    file_data += f"// # characters   : {len(font_data)}\n"
    if generate_map:
        file_data += f"// Map start code : {map_start_code}\n"
        file_data += f"// Map size       : {len(map_data)} entries\n"
    file_data += "\n"
    
    font_type = 1;
    if generate_map:
        file_data += f"static const uint32_t font_{normalized_name}_map[] = {{\n"
        for i in range(0, len(map_data), 8):
            line = map_data[i:i+8]
            file_data += "    " + ", ".join([f"0x{val:04X}" for val in line]) + ",\n"
        file_data += "};\n\n"
        font_type = 2;

    file_data += f"const rg_font_t font_{normalized_name} = {{\n"
    file_data += f"    .name = \"{font_name}\",\n"
    file_data += f"    .type = {font_type},\n"
    file_data += f"    .width = 0,\n"
    file_data += f"    .height = {max_height},\n"
    file_data += f"    .chars = {len(font_data)},\n"
    if generate_map:
        file_data += f"    .map_start_code = {map_start_code},\n"
        file_data += f"    .map = font_{normalized_name}_map,\n"
        file_data += f"    .map_len = sizeof(font_{normalized_name}_map) / 4,\n"
    file_data += f"    .data = {{\n"
    for glyph in font_data:
        char_code = glyph['char_code']
        header_data = [char_code & 0xFF, char_code >> 8, glyph['ofs_y'], glyph['box_w'],
                       glyph['box_h'], glyph['ofs_x'], glyph['adv_w']]
        file_data += f"        /* U+{char_code:04X} '{chr(char_code)}' */\n        "
        file_data += ", ".join([f"0x{byte:02X}" for byte in header_data])
        file_data += f",\n        "
        if len(glyph["bitmap"]) > 0:
            file_data += ", ".join([f"0x{byte:02X}" for byte in glyph["bitmap"]])
            file_data += f","
        file_data += "\n"
    file_data += "\n"
    file_data += "        // Terminator\n"
    file_data += "        0x00, 0x00,\n"
    file_data += "    },\n"
    file_data += "};\n"

    return file_data

def select_file():
    filename = filedialog.askopenfilename(
        title='Load Font',
        initialdir=os.getcwd(),
        filetypes=(('True Type Font', '*.ttf'), ('Retro-Go Font', '*.c'), ('All files', '*.*')))

    if filename:
        global font_path
        font_path = filename
        generate_font_data()

# Function to zoom in and out on the canvas
def zoom(event):
    scale = 1.0
    if event.delta > 0:  # Scroll up to zoom in
        scale = 1.2
    elif event.delta < 0:  # Scroll down to zoom out
        scale = 0.8

    # Get the canvas size and adjust scale based on cursor position
    canvas.scale("all", event.x, event.y, scale, scale)

    # Update the scroll region to reflect the new scale
    canvas.configure(scrollregion=canvas.bbox("all"))

def start_pan(event):
    global start_x, start_y
    # Record the current mouse position
    start_x = event.x
    start_y = event.y

def pan_canvas(event):
    global start_x, start_y
    # Calculate the distance moved
    dx = start_x - event.x
    dy = start_y - event.y

    # Scroll the canvas
    canvas.move("all", -dx, -dy)

    # Update the starting position
    start_x = event.x
    start_y = event.y

if __name__ == "__main__":
    window = Tk()
    window.title("Retro-Go Font Converter")

    # 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}")

    p_size = 8 # pixel size on the renderer

    canva_width = screen_width//p_size
    canva_height = screen_height//p_size-16

    frame = Frame(window)
    frame.pack(anchor="center", padx=10, pady=2)

    # choose font button (file picker)
    choose_font_button = ttk.Button(frame, text='Choose font', command=select_file)
    choose_font_button.pack(side="left", padx=5)

    # Label and Entry for Font height
    Label(frame, text="Font height").pack(side="left", padx=5)
    font_height_input = StringVar(value=str(font_size_init))
    Entry(frame, textvariable=font_height_input, width=4).pack(side="left", padx=5)

    # Variable to hold the state of the checkbox
    enforce_font_size_bool = IntVar()  # 0 for unchecked, 1 for checked
    Checkbutton(frame, text="Enforce size", variable=enforce_font_size_bool).pack(side="left", padx=5)

    # Label and Entry for Char ranges to include
    Label(frame, text="Ranges to include").pack(side="left", padx=5)
    list_char_ranges = StringVar(value=str(list_char_ranges_init))
    Entry(frame, textvariable=list_char_ranges, width=30).pack(side="left", padx=5)

    # Variable to hold the state of the checkbox
    bounding_box_bool = IntVar(value=1)  # 0 for unchecked, 1 for checked
    Checkbutton(frame, text="Bounding box", variable=bounding_box_bool).pack(side="left", padx=10)

    # Variable to hold the state of the map generation checkbox
    generate_map_bool = IntVar()  # 0 for unchecked, 1 for checked
    Checkbutton(frame, text="Generate map", variable=generate_map_bool).pack(side="left", padx=5)

    # Label and Entry for Map start code
    Label(frame, text="Map start code").pack(side="left", padx=5)
    map_start_code_input = StringVar(value=str(map_start_code_init))
    Entry(frame, textvariable=map_start_code_input, width=6).pack(side="left", padx=5)

    # Button to launch the font generation function
    b1 = Button(frame, text="Preview", width=14, height=2, background="blue", foreground="white", command=generate_font_data)
    b1.pack(side="left", padx=5)

    # Button to launch the font exporting function
    b1 = Button(frame, text="Save", width=14, height=2, background="blue", foreground="white", command=save_font_data)
    b1.pack(side="left", padx=5)

    frame = Frame(window).pack(anchor="w", padx=2, pady=2)
    canvas = Canvas(frame, width=canva_width*p_size, height=canva_height*p_size, bg="black")
    canvas.configure(scrollregion=(0, 0, canva_width*p_size, canva_height*p_size))
    canvas.bind("<MouseWheel>", zoom)
    canvas.bind("<ButtonPress-1>", start_pan)  # Start panning
    canvas.bind("<B1-Motion>",pan_canvas)
    canvas.focus_set()
    canvas.pack(fill="both", expand=True)

    window.mainloop()

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值