8000 extend framebuf.blit method with parameters to select a rectangle from the source · Issue #6365 · micropython/micropython · GitHub
[go: up one dir, main page]

Skip to content

extend framebuf.blit method with parameters to select a rectangle from the source #6365

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
buzzware opened this issue Aug 23, 2020 · 5 comments

Comments

@buzzware
Copy link
buzzware commented Aug 23, 2020

I am trying to implement font rendering. I have loaded a single bitmap of all letters, and now I want to blit each letter to the destination. However, there is currently no framebuf method for blitting a partial framebuf.

Currently its blit(fbuf, x, y, [key])

I suggest blit(fbuf, x, y, [key], [sx,sy,sw,sh]) although I'm not clear on whether width & height are better than right and bottom.

It would bring major performance gains to do partial blitting in C over Python, and I don't want to allocate a tiny framebuf for every letter of the alphabet.

@peterhinch
Copy link
Contributor
peterhinch commented Aug 24, 2020

You could pre-allocate a bytearray big enough for the largest glyph on initialisation, then use uctypes.bytearray_at to create a reference to a bytearray of the correct size at runtime when you render the glyph. Or use a memoryview into the initial array.

FWIW my writer class allocates at render-time: performance is OK rendering to monochrome devices but there is a problem rendering large volumes of text to color devices, which I think you will encounter.

The blit method curently does not behave in a rational manner when blitting between framebufs with different color maps. Fixing this would enable blitting 1-bit glyphs onto multi-bit destinations. At the moment you have to do it in Python (as per my CWriter in the above module). See this issue.

@buzzware
Copy link
Author
buzzware commented Aug 27, 2020

Thanks @peterhinch. Selecting some start byte index into the buffer doesn't solve that I need to select a bit (I'm only using 1 bit monochrome), nor that I only want to stamp X bits in width & height. Thanks for your library, I'll have a look.
For now I can get away with storing a few whole bitmaps and stamping them, rather than full font rendering.
I was attempting to bring BMFont rendering to Micropython, as that seems the most advanced in the space. I've got most of the way, but didn't realise this pixel blitting issue. I want to make it so the font is in the source as one bitmap of bytes(), and each letter is blitted from it. The binary BMFont metadata would just be converted to bytes() as is. I'll have to shelve this effort for now.
Check out BMFont, you can probably use it with your library https://www.angelcode.com/products/bmfont

Incomplete, non-working code follows :

from struct import unpack
from collections import namedtuple

from PngReader import PngReader

is_mpy = False


class BufferCursor:
    def __init__(self, buffer, offset = 0):
        self._buffer = buffer
        self._offset = offset

    @property
    def buffer(self):
        return self._buffer

    @property
    def offset(self):
        return self._offset
    
    @property
    def eof(self):
        return self._offset >= len(self._buffer)

    def read_byte(self):
        result = self._buffer[self._offset]
        self._offset += 1
        return result

    def read_word(self):
        result = self._buffer[self._offset] + (self._buffer[self._offset+1] << 8)
        self._offset += 2
        return result

    def read_long(self):
        result = self._buffer[self._offset] | (self._buffer[self._offset+1] << 8) | (self._buffer[self._offset+2] << 16) | (self._buffer[self._offset+3] << 24)
        self._offset += 4
        return result

    def read_view(self, size):
        if is_mpy:
            result = memoryview(self._buffer, self._offset, size)
            self._offset += size
            return result
        else:
            result = memoryview(self._buffer)[self._offset:self._offset+size]
            self._offset += size
            return result

    def read_str(self):
        length = len(self._buffer)-self._offset
        for i in range(self._offset,len(self._buffer)):
            if self._buffer[i]==0:
                length = i-self._offset
                break
        result = self._buffer[self._offset:self._offset+length]
        result = bytes(result).decode()
        self._offset += length
        return result


class BmFontInfo:
    fontSize = 0

    smooth = 1
    unicode = 1
    italic = 0
    bold = 0

    charset = ""
    stretchH = 100
    aa = 1
    padding = [0, 0, 0, 0]
    spacing = [1, 1]
    outline = 0
    fontName = ""

    def __init__(self,buffer=None):
        if buffer:
            self.unpack_buffer(buffer)

    def unpack_buffer(self,buffer):
        cursor = BufferCursor(buffer)
        self.fontSize, bit_field, charset, self.stretchH, self.aa, \
        self.padding[0], self.padding[1], self.padding[2], self.padding[3], \
        self.spacing[0], self.spacing[1], self.outline \
            = unpack("<h BB H 8B", cursor.read_view(0xe))

        self.smooth = bit_field >> 7
        self.unicode = (bit_field >> 6) & 1
        self.italic = (bit_field >> 5) & 1
        self.bold = (bit_field >> 4) & 1

        self.fontName = cursor.read_str()


class BmFontCommon:

    lineHeight = 0
    base = 0
    scaleW = 0
    scaleH = 0
    pages = 0
    bitField = 0
    alphaChnl = 0
    redChnl = 0
    greenChnl = 0
    blueChnl = 0

    def __init__(self,buffer=None):
        if buffer:
            self.unpack_buffer(buffer)

    def unpack_buffer(self,buffer):
        cursor = BufferCursor(buffer)
        self.lineHeight = cursor.read_word()
        self.base = cursor.read_word()
        self.scaleW = cursor.read_word()
        self.scaleH = cursor.read_word()
        self.pages = cursor.read_word()
        self.bitField = cursor.read_byte()
        self.alphaChnl = cursor.read_byte()
        self.redChnl = cursor.read_byte()
        self.greenChnl = cursor.read_byte()
        self.blueChnl = cursor.read_byte()


class BmFontPages:

    pageNames = None

    def __init__(self,buffer=None):
        if buffer:
            self.unpack_buffer(buffer)

    def unpack_buffer(self,buffer):
        cursor = BufferCursor(buffer)
        self.pageNames = []
        while not cursor.eof:
            name = cursor.read_str()
            if len(name)==0:
                break
            self.pageNames.append(name)


class BmFontChar:
    id = 0
    x = 0
    y = 0
    width = 0
    height = 0
    xoffset = 0
    yoffset = 0
    xadvance = 0
    page = 0
    chnl = 0
    
    @classmethod
    def from_buffer_cursor(cls,cursor):
        result = BmFontChar()
        result.id, \
        result.x, \
        result.y, \
        result.width, \
        result.height, \
        result.xoffset, \
        result.yoffset, \
        result.xadvance, \
        result.page, \
        result.chnl = unpack("<I 4H 3h 2B", cursor.read_view(20))

        # result.id = cursor.read_long()
        # result.x = cursor.read_word()
        # result.y = cursor.read_word()
        # result.width = cursor.read_word()
        # result.height = cursor.read_word()
        # result.xoffset = cursor.read_word()
        # result.yoffset = cursor.read_word()
        # result.xadvance = cursor.read_word()
        # result.page = cursor.read_byte()
        # result.chnl = cursor.read_byte()
        return result


class BmFontChars:

    CHAR_INFO_LEN = 20

    chars = None

    def __init__(self,buffer=None):
        if buffer:
            self.unpack_buffer(buffer)

    def unpack_buffer(self,buffer):
        cursor = BufferCursor(buffer)
        num_chars = len(buffer) // self.CHAR_INFO_LEN
        self.chars = []
        for c in range(num_chars):
            if cursor.eof:
                break
            self.chars.append(BmFontChar.from_buffer_cursor(cursor))


BmFontKerningPair = namedtuple('BmFontKerningPair',['first','second','amount'])


class BmFontKernings:

    pairs = None

    def __init__(self,buffer=None):
        if buffer:
            self.unpack_buffer(buffer)

    def unpack_buffer(self,buffer):
        cursor = BufferCursor(buffer)
        self.pairs = []
        while not cursor.eof:
            first, second, amount = unpack("<I I h",cursor.read_view(10))
            pair = BmFontKerningPair(first,second,amount)
            self.pairs.append(pair)


# Block type 5: kerning pairs
# field	size	type	pos	comment
# first	4	uint	0+c*10	These fields are repeated until all kerning pairs have been described
# second	4	uint	4+c*10
# amount	2	int	8+c*6
# This block is only in the file if there are any kerning pairs with amount differing from 0.


class BmFontBinaryMetadata:

    HEADER = [66, 77, 70]
    INFO = 1
    COMMON = 2
    PAGES = 3
    CHARS = 4
    KERNINGS = 5
    
    def __init__(self,
                 buffer  # a buffer of binary metadata about the chars
                 ):
        self.info = None
        self.common = None
        self.pages = None
        self.chars = None
        self.kernings = None
        self.read_chunks(memoryview(buffer))

    def read_chunks(self,metadata):
        if len(metadata) < 6:
            raise BufferError('invalid buffer length for BMFont')
        if not (metadata[0]==self.HEADER[0] and metadata[1]==self.HEADER[1] and metadata[2]==self.HEADER[2]):
            raise BufferError('BMFont missing BMF byte header')

        cursor = BufferCursor(metadata, 3)
        vers = cursor.read_byte()
        if vers > 3:
            raise RuntimeError('Only supports BMFont Binary v3 (BMFont App v1.10)')

        while not cursor.eof:
            id = cursor.read_byte()
            size = cursor.read_long()
            block = cursor.read_view(size)

            if id==self.INFO:
                self.info = BmFontInfo(block)
            elif id==self.COMMON:
                self.common = BmFontCommon(block)
            elif id==self.PAGES:
                self.pages = BmFontPages(block)
            elif id==self.CHARS:
                self.chars = BmFontChars(block)
            elif id==self.KERNINGS:
                self.kernings = BmFontKernings(block)
            print('done')



class BmFont:

    meta_buffer = None
    metadata = None
    png_reader = None

    def __init__(self):
        pass

    def load_font_png(self,filename):
        self.png_reader = PngReader.from_file(filename)
        return self.png_reader

    def load_fnt_binary(self,filename):
        self.meta_buffer = None
        with open(filename, mode='rb') as file:          # b is important -> binary
            self.meta_buffer = file.read()
        self.metadata = BmFontBinaryMetadata(self.meta_buffer)
        return self.metadata

    def load_files(self,fnt_path,png_path):
        self.load_fnt_binary(fnt_path)
        # pre, ext = os.path.splitext(fnt_path)
        # png_path = pre + '.png'
        self.load_font_png(png_path)

    # fb = framebuf.FrameBuffer(reader.view, reader.width, reader.height, framebuf.MONO_HMSB, reader.stride)




class BmTextWriter:

    def __init__(self,x,y,bmFont):
        self.font = bmFont
        self.x = x
        self.y = y

    def position_char(self, c: BmFontChar, prev_c: BmFontChar = None):
        k = 0
        if prev_c:
            if kerning:
                k = self.kerning_map.get((prev_c.id, c.id), 0)
                dest_location = (k + c.xoffset, c.yoffset)
            else:
                dest_location = (c.xoffset, c.yoffset)
        else:
            dest_location = (0, c.yoffset)
        return GlyphInfo(
            char=c,
            page_index=c.page,
            channel=c.chnl,
            source_bounds=(c.x, c.y, c.x + c.width, c.y + c.height),
            dest_location=dest_location,
            xadvance= c.xadvance + k - (c.xoffset if prev_c is None else 0)
        )

    def write(self,text):
        prev_char = None
        for c in text:
            char = self.font.chars.chars[ord(c)]
            #g = self.position_char(char, prev_char, kerning=self.font.kernings)

            k = 0
            if prev_char:
                k = self.font.kernings.get((prev_char.id, c.id), 0)
                dest_location = (k + c.xoffset, c.yoffset)
            else:
                dest_location = (0, c.yoffset)

            xadvance= c.xadvance + k - (c.xoffset if prev_char is None else 0)
            dest_location = (dest_location[0] + self.x, dest_location[1] + self.y)
            self.x += xadvance
            self.render(char,dest_location[0],dest_location[1])
            prev_char = char

    def render(self, char, x, y):
        source_bounds=(char.x, char.y, char.x + char.width, char.y + char.height)
        # blit

@peterhinch
Copy link
Contributor

The angelcode executables appear to be Windows-only. My font-to-py utility is cross-platform between Windows, Linux and OSX. Many MicroPython users (self included) do not possess Windows machines. I am also concerned about the license: MicroPython and all my own libraries are MIT licensed.

The key purpose of my library is specifically to convert fonts to bitmaps in Python sourcecode. This is crafted so that, when installed as frozen bytecode, it uses almost zero RAM. Glyph access times are short as they are lookups into a ROM-based table.

For these reasons I see little opportunity for convergence between what we are trying to achieve.

@buzzware
Copy link
Author

Yes, its Windows only, and I ran a VM to use it. I don't think its license really matters - it would only apply to the program, not the output. There are many other libraries and tools out there supporting the formats - text, xml and/or binary.
I appreciate what you say about efficiency of the result with your library, but its a chain and I would like a tool like yours to take in a BMFont PNG bitmap and metadata, and produce the same output.
The PNG bitmap is most important because it provides a convenient way to edit pixels before conversion to the binary. Vector to bitmap conversion is rarely ideal at the small bitmap sizes you need for small cheap displays.
I'm glad you're using Freetype like BMFont - I believe that is the highest quality renderer.
BMFont supports kerning.
BMFont provides a convenient way to select which specific characters to export, then does a tetris type algorithm to pack them most efficiently into 1 or more bitmaps of a specific size.
There are probably BMFonts out there already too.
In the game development world, BMFont seems to be the most popular.

@peterhinch
Copy link
Contributor

I take your points about vector to bitmap conversion, bitmap editing, kerning and small bitmap sizes. My fonts are usable on small displays such as the 1.27" OLEDs in the photographs, which look a lot better in practice than in my pictures. However there is no doubt that hand-crafted fonts are best in tiny sizes.

I considered supporting kerning, but avoided it because of the added code complexity. People run the Writer classes on ESP8266 and similar, where RAM is minimal.

I will consider support for these formats, but I have a long "todo" list.

I am more concerned about a solution to the color blitting problem which is a major headache for rendering large quantities of small text. I could fix this, but as a change to the C code it would require a special firmware build which is something I'm not keen to support. My offer of a PR was not taken up.

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

2 participants
0