Skip to content

external SPI font #304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wxzed opened this issue Feb 25, 2025 · 16 comments
Open

external SPI font #304

wxzed opened this issue Feb 25, 2025 · 16 comments

Comments

@wxzed
Copy link

wxzed commented Feb 25, 2025

I want to extend an external SPI font library. Is there a way to implement this at the Python layer? I found that I can't reassign get_glyph_bitmap at the Python level.

@wxzed
Copy link
Author

wxzed commented Feb 25, 2025

t's not using SPI flash but a font chip.

@kdschlosser
Copy link
Collaborator

OoOoo Hmmm...

You would need to write a driver to access the data on the font chip That is not the hard part. what the hard part is is figuring out how the font is stored on the chip. What is the format of it? I would need to know the make and model of the chip to pull up the datasheet and see what is says....

@wxzed
Copy link
Author

wxzed commented Feb 25, 2025

Not that detailed, I just don’t know how to call my own get_glyph_bitmap. I just want to understand the general structure. With this operation, I can only use get_glyph_dsc.

self.desc.font = lv.font_t()
self.desc.font.get_glyph_dsc = test_get_glyph_dsc

@kdschlosser
Copy link
Collaborator

You are not really explaining what you are trying to do. Unless I know what you are wanting to do I am really not going to be able to offer any advise on how to go about it.

For a custom font loader my understanding of how it works is this.

class FontChip:

    def __init__(self, ...):
        # code needed to access font chip
        ....

    def get_glyph(self, dsc, letter):
        # code needed to collect the data from the font chip.
        dsc.adv_w = ...  # number of pixels between glyphs
        dsc.box_w = ... # height of glyph including extra pixels to normalize the height across the entire font
        dsc.box_h = ...  # width of glyph
        dsc.ofs_x = 0
        dsc.ofs_y = 0
        dsc.format = lv.FONT_GLYPH_FORMAT.IMAGE
        dsc.gid.src = ...  # glyph data for image font
        dsc.entry = ...  # have no clue how to use this

        return True  # return True if the glyph was found False otherwise


def get_glyph_dsc_cb(font, font_glyph_dsc, letter, letter_next):
    font_cls = font.user_data.__cast__()
    
    if font_cls.get_glyph(font_glyph_dsc, letter):
        font_glyph_dsc.resolved_font = font
        return True

    # didn't find the glyph so try the fallback font
    return font.get_glyph_dsc(font.fallback, font_glyph_dsc, letter, letter_next)
    

font_chip = FontChip()

font = lv.font_t()

font.get_glyph_dsc = get_glyph_dsc_cb
font.get_glyph_bitmap = None  # callback function to get the bitmap of the glyph
font.release_glyph = None  # callback function to release memory used by a glyph

font.line_height = ...  # tallest glyph height
font.base_line = ...  # not sure what this does

font.subpx = lv.FONT_SUBPX.NONE
font.kerning = lv.FONT_KERNING.NONE
font.underline_position = 0
font.underline_thickness = 1

font.fallback = lv.font_montserrat_10
font.user_data = font_chip

@wxzed
Copy link
Author

wxzed commented Feb 27, 2025

Thank you for your response. I tried the method you provided, but it didn't work.
Now, I want to try another approach using a different module (mod). Here is a rough outline of my code:

static lv_font_t my_custom_font = {
    .get_glyph_dsc = my_get_glyph_dsc,
    .get_glyph_bitmap = my_get_glyph_bitmap,
    .line_height = 16,  
    .base_line = 0
};

typedef struct {
    mp_obj_base_t base;
    const lv_font_t *font;  
} lv_font_obj_t;


static mp_obj_t lv_font_obj_get_font(mp_obj_t self_in) {
    lv_font_obj_t *self = MP_OBJ_TO_PTR(self_in);
    //return MP_OBJ_FROM_PTR(&self->font); 
    return MP_OBJ_NEW_SMALL_INT((intptr_t)(self->font));
    //return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_1(lv_font_obj_get_font_obj, lv_font_obj_get_font);

static mp_obj_t extern_font_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
    lv_font_obj_t *self = m_new_obj(lv_font_obj_t);
    self->base.type = &machine_exfont_type; 
    self->font = &lv_font_montserrat_16;
    return MP_OBJ_FROM_PTR(self); 
}

static void extern_font_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
    mp_printf(print, "<Exfont object>");
}

static const mp_rom_map_elem_t lv_font_obj_locals_dict_table[] = {
    { MP_ROM_QSTR(MP_QSTR_font), MP_ROM_PTR(&lv_font_obj_get_font_obj) },  
};

static MP_DEFINE_CONST_DICT(lv_font_obj_locals_dict, lv_font_obj_locals_dict_table);


MP_DEFINE_CONST_OBJ_TYPE(
    machine_exfont_type,
    MP_QSTR_Exfont,
    MP_TYPE_FLAG_NONE,
    print, extern_font_print,
    make_new, extern_font_make_new,
    locals_dict, &lv_font_obj_locals_dict
    );

My intention is that the type of myfont.font() should be 'lv_font_t', so that I can use it in the same way as lv.font_montserrat_16. However, I have tried several methods but haven't achieved this yet.
Do you have any suggestions?

>>> from machine import Exfont
>>> myfont = Exfont()
>>> dir(myfont)
['__class__', 'font']
>>> type(myfont.font())
<class 'int'>


>>> import lvgl as lv
>>> type(lv.font_montserrat_16)
<class 'lv_font_t'>
>>> 

@kdschlosser
Copy link
Collaborator

it's not going to work. It is an example of what you need to do. I don't know exactly what you have to do because I don't know what the font chip is or how it stores the data.

@kdschlosser
Copy link
Collaborator

if you get me a link to the data sheet for the font chip I will be able to help more with this.

@wxzed
Copy link
Author

wxzed commented Feb 27, 2025

test.zip
I don’t have the datasheet here, but I do have the previous driver code from LVGL-C.
Here is the part where the font is defined.

// Callback function to get the glyph descriptor
bool myGetGlyphDscCb_16(const lv_font_t * font, lv_font_glyph_dsc_t * dsc_out, uint32_t unicode_letter, uint32_t unicode_letter_next) {
    // Set the glyph descriptor, including information such as glyph width, height, offset, etc.
    //printf("ff\n");
    dsc_out->box_h = 12;   /* Height of the glyph bitmap (in pixels) */
    dsc_out->box_w = 16;   /* Width of the glyph bitmap (in pixels) */
    if(unicode_letter < 128){
      dsc_out->adv_w = ASCII_GetInterval(unicode_letter,ASCII_12_A);   /* Letter spacing */
    }else{
      dsc_out->adv_w = 12;   
    }
    dsc_out->ofs_x = 0;    /* X offset of the glyph bitmap (in pixels) */
    dsc_out->ofs_y = 0;    /* Y offset of the glyph bitmap (in pixels), relative to the baseline */
    dsc_out->bpp = 1;      /* Bits per pixel: 1/2/4/8 */
    dsc_out->is_placeholder = false;
    return true;           /* true: glyph found; false: glyph not found */

}

// Callback function to get the glyph bitmap
const uint8_t * myGetGlyphBitmapCb_16(const lv_font_t * font, uint32_t unicode_letter) {
    // Return pointer to glyph bitmap data
    // Replace the example return value below with actual bitmap data for character "你"
    static uint16_t u2g;
    if (unicode_letter < 128) {
        ASCII_GetData(unicode_letter, ASCII_12_A, _pBits);
    } else {
        u2g = U2G(unicode_letter);
        gt_12_GetData((u2g >> 8) & 0xff, u2g & 0xff, _pBits);
    }
    return (const uint8_t*)_pBits; // Return pointer to array containing bitmap data for character "你"
}

// Callback function to get the glyph descriptor
bool myGetGlyphDscCb_24(const lv_font_t * font, lv_font_glyph_dsc_t * dsc_out, uint32_t unicode_letter, uint32_t unicode_letter_next) {
    // Set glyph descriptor, including glyph width, height, offset, etc.
    dsc_out->box_h = 24;   /* Height of the glyph bitmap (in pixels) */
    dsc_out->box_w = 24;   /* Width of the glyph bitmap (in pixels) */
    if (unicode_letter < 128) {
        dsc_out->adv_w = ASCII_GetInterval(unicode_letter, ASCII_24_B);   /* Letter spacing */
    } else {
        dsc_out->adv_w = 24;   /* Letter spacing */
    }
    dsc_out->ofs_x = 0;    /* X offset of the glyph bitmap (in pixels) */
    dsc_out->ofs_y = 0;    /* Y offset of the glyph bitmap (in pixels), relative to the baseline */
    dsc_out->bpp = 1;      /* Bits per pixel: 1/2/4/8 */
    dsc_out->is_placeholder = false;
    return true;           /* true: glyph found; false: glyph not found */
}


// Callback function to get the glyph bitmap
const uint8_t * myGetGlyphBitmapCb_24(const lv_font_t * font, uint32_t unicode_letter) {
    // Return pointer to glyph bitmap data
    // Replace the example return value below with actual bitmap data for character "你"
    static uint16_t u2g;
    if (unicode_letter < 128) {
        ASCII_GetData(unicode_letter, ASCII_24_B, _pBits);
    } else {
        u2g = U2G(unicode_letter);
        GBK_24_GetData((u2g >> 8) & 0xff, u2g & 0xff, _pBits);
    }
    return (const uint8_t*)_pBits; // Return pointer to array containing bitmap data for character "你"
}


const lv_font_t my_custom_font_16 = {
  .get_glyph_dsc = myGetGlyphDscCb_16,
  .get_glyph_bitmap = myGetGlyphBitmapCb_16,
  .line_height = 15,  // Line height
  .base_line = 3,     // Baseline
  .subpx = LV_FONT_SUBPX_NONE,
};  // Font parsing structure for the font chip

const lv_font_t my_custom_font_24 = {
  .get_glyph_dsc = myGetGlyphDscCb_24,
  .get_glyph_bitmap = myGetGlyphBitmapCb_24,
  .line_height = 27,
  .base_line = 3,
  .subpx = LV_FONT_SUBPX_NONE,
};  // Font parsing structure for the font chip

@kdschlosser
Copy link
Collaborator

kdschlosser commented Feb 27, 2025

Fantastic.

@kdschlosser
Copy link
Collaborator

OK so here you go. Make sure to read the comments near the top.

import lvgl as lv
from micropython import const  # NOQA

import machine


# replace the "..." in the line below with the size of the `_pbits` array
_PBITS_SIZE = const(...)

# set up your SPI bus. If the bus is shared with something else then you only
# need to create the device passing the shared bus to all of the things that 
# use the bus.

spi_bus = machine.SPI.Bus(
    host=1,
    mosi=...,
    miso=...,
    sck=...,
)

spi_device = machine.SPI.Device(
    spi_bus=spi_bus,
    cs=...,
    freq=20000000
)


_ASCII_5X7 = const(1)
_ASCII_7X8 = const(2)
_ASCII_6X12 = const(3)
_ASCII_12_A = const(4)  # 12 * 16
_ASCII_8X16 = const(5)
_ASCII_12X24_A = const(6)
_ASCII_12X24_P = const(7)
_ASCII_16X32 = const(8)
_ASCII_16_A = const(9)
_ASCII_24_B = const(10)
_ASCII_32_B = const(11)


def gt_read_data(sendbuf, _, receivebuf, receivelen):
    spi_device.write(sendbuf)

    buf = bytearray(0x00)

    for i in range(receivelen):
        buf[0] = 0x00
        spi_device.write_readinto(buf, buf)
        receivebuf[i] = buf[0]

    return 1


def r_dat_bat(address, DataLen, pBuff):
    buf = bytearray([0x03])
    spi_device.write(buf)  # <发送读取命令

    buf[0] = (address >> 16) & 0xFF
    spi_device.write(buf)  # <发送24bit地址

    buf[0] = (address >> 8) & 0xFF
    spi_device.write(buf)

    buf[0] = address & 0xFF
    spi_device.write(buf)

    for i in range(DataLen):
        buf[0] = 0XFF
        spi_device.write_readinto(buf, buf)  # <循环读数
        pBuff[i] = buf[0]
  
    return pBuff[0]
  

_GT_UID_MD5_FLAG = const(1)
_GBCODE_5139 = const(0)


def Uncompress(result, a2):
    for i in range(5 + 1):
        result[4 * i] = a2[3 * i]
        result[4 * i + 1] = a2[3 * i + 1] & 0xF0
        result[4 * i + 2] = a2[3 * i + 2]
        result[4 * i + 3] = 16 * a2[3 * i + 1]  
      
    return 1


def check_dot_null(DZ_Data, num):
    for i in range(num):
        if DZ_Data[i]:
            return 1
  
    return 0


def ASCII_GetData(asc, ascii_kind, DZ_Data):
    if _GT_UID_MD5_FLAG == 0:
        return 0
        
    if asc <= 0x1F or asc > 0x7E:
        return 0
        
    if ascii_kind == 1:
        r_dat_bat(8 * (asc + 259932), 8, DZ_Data)
    elif ascii_kind == 2:
        r_dat_bat(8 * (asc + 260028), 8, DZ_Data)
    elif ascii_kind == 3:
        r_dat_bat(12 * asc + 2080864, 0xC, DZ_Data)
    elif ascii_kind == 4:
        r_dat_bat(26 * asc + 2090144 + 2, 0x18, DZ_Data)
    elif ascii_kind == 5:
        r_dat_bat(16 * (asc + 130174), 0x10, DZ_Data)
    elif ascii_kind == 6:
        r_dat_bat(48 * asc + 1543800, 0x30, DZ_Data)
    elif ascii_kind == 7:
        r_dat_bat(48 * asc + 1549944, 0x30, DZ_Data)
    elif ascii_kind == 8:
        r_dat_bat(((asc + 67108832) << 6) + 2084832, 0x40, DZ_Data)
    elif ascii_kind == 9:
        r_dat_bat(34 * asc + 2092384 + 2, 0x20, DZ_Data)
    elif ascii_kind == 0xA:
        r_dat_bat(74 * asc + 1559864 + 2, 0x48, DZ_Data)
    elif ascii_kind == 0xB:
        r_dat_bat(130 * asc + 2092576, 0x82, DZ_Data)
    else:
        return 1
  
    return 1


def ASCII_GetInterval(asc, ascii_kind):
    data = bytearray(2)
  
    if _GT_UID_MD5_FLAG == 0:
        return 0
        
    if asc <= 0x1F or asc > 0x7E:
        return 0
        
    if ascii_kind == 1:
        r_dat_bat(8 * (asc + 259932), 2, data)
    elif ascii_kind == 2:
        r_dat_bat(8 * (asc + 260028), 2, data)
    elif ascii_kind == 3:
        r_dat_bat(12 * asc + 2080864, 2, data)
    elif ascii_kind == 4:
        r_dat_bat(26 * asc + 2090144, 2, data)
    elif ascii_kind == 5:
        r_dat_bat(16 * (asc + 130174), 2, data)
    elif ascii_kind == 6:
        r_dat_bat(48 * asc + 1543800, 2, data)
    elif ascii_kind == 7:
        r_dat_bat(48 * asc + 1549944, 2, data)
    elif ascii_kind == 8:
        r_dat_bat(((asc + 67108832) << 6) + 2084832, 2, data)
    elif ascii_kind == 9:
        r_dat_bat(34 * asc + 2092384, 2, data)
    elif ascii_kind == 0xA:
        r_dat_bat(74 * asc + 1559864, 2, data)
    elif ascii_kind == 0xB:
        r_dat_bat(130 * asc + 2092576, 2, data)

    return data[1]


def gt_12_GetData(MSB, LSB, DZ_Data):
    pBuff = bytearray(20)
    offset = 2109216
    address = 0

    if _GT_UID_MD5_FLAG == 1:
    
        if MSB != 169 or LSB <= 0xA3:
            if MSB <= 0xA0 or MSB > 0xA3 or LSB <= 0xA0:
                if 0xAF < MSB <= 0xF7 and LSB > 0xA0:
                    address = 18 * (94 * MSB + LSB) + offset - 294246
            else:
                address = 18 * (94 * MSB + LSB) + offset - 275310
                
        else:
            address = 18 * LSB + offset + 2124
                
        r_dat_bat(address, 0x12, pBuff)
        Uncompress(DZ_Data, pBuff)


def GBK_24_GetData(c1, c2, DZ_Data):
    temp = c2
    address = 0
    
    if _GT_UID_MD5_FLAG == 0:
        return 0
        
    if c2 == 127:
        address = 0
        
    if c1 <= 0xA0 or c1 > 0xA3 or c2 <= 0xA0:
        if c1 != 166 or c2 <= 0xA0:
            if c1 == 169 and c2 > 0xA0:
                address = 94 * c1 + c2 - 15671
        else:
            address = 94 * c1 + c2 - 15483
    else:
        address = 94 * c1 + c2 - 15295
    
    if c1 <= 0xAF or c1 > 0xF7 or c2 <= 0xA0:
        if c1 > 0xA0 or c1 <= 0x80 or c2 <= 0x3F:
            if c1 > 0xA9 and c2 <= 0xA0:
                if (c2 & 0x80) != 0:
                    temp = c2 - 1
                    
                address = 96 * c1 + temp - 3081
        else:
            if (c2 & 0x80) != 0:
                temp = c2 - 1
            
            address = 190 * c1 + temp - 17351
    else:
        address = 94 * c1 + c2 - 16250
    
    r_dat_bat(72 * address, 0x48, DZ_Data)
    return 72 * address


def U2G(unicode):
    pBuff = bytearray(2)
    address = 0
    offset = 2517590
  
    if _GT_UID_MD5_FLAG == 0:
        return 0

    if unicode > 0x451 or unicode <= 0x9F:
        if unicode > 0x2642 or unicode <= 0x200F:
            if unicode > 0x33D5 or unicode < 0x3000:
                if unicode > 0x9FA5 or unicode < 0x4E00:
                    if unicode > 0xFE6B or unicode <= 0xFE2F:
                        if unicode > 0xFF5E or unicode <= 0xFF00:
                            if unicode > 0xFFE5 or unicode <= 0xFFDF:
                                if unicode > 0xFA29 or unicode <= 0xF92B:
                                    if unicode > 0xE864 or unicode <= 0xE815:
                                        if unicode > 0x2ECA or unicode <= 0x2E80:
                                            if unicode > 0x49B7 or unicode <= 0x4946:
                                                if unicode > 0x4DAE or unicode <= 0x4C76:
                                                    if unicode > 0x3CE0 or unicode <= 0x3446:
                                                        if 0x478D >= unicode > 0x4054:
                                                            address = 2 * (unicode + 2147467178) + 55380
                                                    else:
                                                        address = 2 * (unicode + 2147470265) + 50976
                                                else:
                                                    address = 2 * (unicode + 2147464073) + 50352
                                            else:
                                                address = 2 * (unicode + 2147464889) + 50126
                                        else:
                                            address = 2 * (unicode + 2147471743) + 49978
                                    else:
                                        address = 2 * (unicode + 2147424234) + 49820
                                else:
                                    address = 2 * (unicode + 2147419860) + 49312
                            else:
                                address = 2 * (unicode + 2147418144) + 49142
                        else:
                            address = 2 * (unicode + 2147418367) + 48954
                    else:
                        address = 2 * (unicode + 2147418576) + 48834
                else:
                    address = 2 * (unicode + 2147463680) + 7030
            else:
                address = 2 * (unicode + 2147471360) + 5066
        else:
            address = 2 * (unicode + 2147475440) + 1892
    else:
        address = 2 * (unicode + 2147483488)
    
    address += offset
    r_dat_bat(address, 2, pBuff)
    return (pBuff[0] << 8) + pBuff[1]


# Callback function to get the glyph descriptor
def myGetGlyphDscCb_16(_, dsc_out, unicode_letter, __):
    # Set the glyph descriptor, including information such as glyph width, height, offset, etc.
    # print("ff")
    dsc_out.box_h = 12   # Height of the glyph bitmap (in pixels)
    dsc_out.box_w = 16  # Width of the glyph bitmap (in pixels)
    if unicode_letter < 128:
        dsc_out.adv_w = ASCII_GetInterval(unicode_letter, _ASCII_12_A)   # Letter spacing
    else:
        dsc_out.adv_w = 12

    dsc_out.ofs_x = 0  # X offset of the glyph bitmap (in pixels)
    dsc_out.ofs_y = 0  # Y offset of the glyph bitmap (in pixels), relative to the baseline
    dsc_out.bpp = 1  # Bits per pixel: 1/2/4/8
    dsc_out.is_placeholder = False
    return True  # true: glyph found false: glyph not found


_pBits = bytearray(_PBITS_SIZE)


# Callback function to get the glyph bitmap
def myGetGlyphBitmapCb_16(_, unicode_letter):
    # Return pointer to glyph bitmap data
    # Replace the example return value below with actual bitmap data for character "你"
    if unicode_letter < 128:
        ASCII_GetData(unicode_letter, _ASCII_12_A, _pBits)
    else:
        u2g = U2G(unicode_letter)
        gt_12_GetData((u2g >> 8) & 0xff, u2g & 0xff, _pBits)

    return _pBits  # Return pointer to array containing bitmap data for character "你"


# Callback function to get the glyph descriptor
def myGetGlyphDscCb_24(_, dsc_out, unicode_letter, __):
    # Set glyph descriptor, including glyph width, height, offset, etc.
    dsc_out.box_h = 24  # Height of the glyph bitmap (in pixels)
    dsc_out.box_w = 24  # Width of the glyph bitmap (in pixels)
    if unicode_letter < 128:
        dsc_out.adv_w = ASCII_GetInterval(unicode_letter, _ASCII_24_B)  # Letter spacing
    else:
        dsc_out.adv_w = 24  # Letter spacing

    dsc_out.ofs_x = 0  # X offset of the glyph bitmap (in pixels)
    dsc_out.ofs_y = 0  # Y offset of the glyph bitmap (in pixels), relative to the baseline
    dsc_out.bpp = 1  # Bits per pixel: 1/2/4/8
    dsc_out.is_placeholder = False

    return True  # true: glyph found false: glyph not found


# Callback function to get the glyph bitmap
def myGetGlyphBitmapCb_24(_, unicode_letter):
    # Return pointer to glyph bitmap data
    # Replace the example return value below with actual bitmap data for character "你"
    if unicode_letter < 128:
        ASCII_GetData(unicode_letter, _ASCII_24_B, _pBits)
    else:
        u2g = U2G(unicode_letter)
        GBK_24_GetData((u2g >> 8) & 0xff, u2g & 0xff, _pBits)

    return _pBits  # Return pointer to array containing bitmap data for character "你"


my_custom_font_16 = lv.font_t(dict(
  get_glyph_dsc=myGetGlyphDscCb_16,
  get_glyph_bitmap=myGetGlyphBitmapCb_16,
  line_height=15,  # Line height
  base_line=3,  # Baseline
  subpx=lv.FONT_SUBPX.NONE  # NOQA
))  # Font parsing structure for the font chip


my_custom_font_24 = lv.font_t(dict(
  get_glyph_dsc=myGetGlyphDscCb_24,
  get_glyph_bitmap=myGetGlyphBitmapCb_24,
  line_height=27,
  base_line=3,
  subpx=lv.FONT_SUBPX.NONE,  # NOQA
))  # Font parsing structure for the font chip

@kdschlosser
Copy link
Collaborator

That is only the first version of the code and I am expecting it to be pretty slow. We can always tweak and optimize the code once we know it is working properly.

@wxzed
Copy link
Author

wxzed commented Feb 28, 2025

Thank you very much for your reply, it looks like exactly what I was looking for. I will verify it on my end. Thanks again.

@kdschlosser
Copy link
Collaborator

no worries m8. It may or may not work properly out of the box but at least we have something to work off of.

@tangjie133
Copy link

Hello, now I also encountered the same problem, using the above method to run the error, the error content:

Traceback (most recent call last):
File "", line 348, in
SyntaxError: Cannot convert 'function' to pointer!

@kdschlosser
Copy link
Collaborator

what?

Why are you trying to hijack someone else's issue?

@tangjie133
Copy link

It was provided by a friend of mine who can't handle it right now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants