peterhinch / micropython-font-to-py

A Python 3 utility to convert fonts to Python source capable of being frozen as bytecode
MIT License
368 stars 67 forks source link

Alternative .py writer #53

Closed supcik closed 8 months ago

supcik commented 8 months ago

This change offers an alternative ".py" file writer, where the font is stored in an object. The resulting files look like this :

# Code generated by font_to_py.
# Font: Roboto-Black Char set: ...
# Cmd: font-to-py --alt --xmap -k latin15 fonts/Roboto-Black.ttf 24 TT.py

def get_ch(self, ch):
    def ifb(l):
        return l[0] | (l[1] << 8)

    oc = ord(ch)
    ioff = 2 * (oc - 32 + 1) if oc >= self.min_ch and oc <= self.max_ch else 0
    doff = ifb(self._index[ioff:])
    width = ifb(self._font[doff:])   
    next_offs = doff + 2 + ((width - 1)//8 + 1) * self.height
    return self._font[doff + 2:next_offs], self.height, width

font = type(
    "",
    (object,),
    {
        "height": 24,
        "baseline": 19,
        "max_width": 19,
        "hmap": True,
        "reverse": False,
        "monospaced": False,
        "min_ch": 32,
        "max_ch": 8364,
        "_font": memoryview(
            b"\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3e\x00\x7f\x80"
            b"\xff\x80\xf3\x80\x03\x80\x07\x80\x0f\x00\x0e\x00\x1c\x00\x1c\x00"
            ...
            b"\x7f\x80\x1c\x00\x7f\x80\x7f\x80\x1c\x00\x1f\xe0\x0f\xe0\x07\xe0"
            b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
        ),
        "_index": memoryview(
            b"\x00\x00\x32\x00\x4c\x00\x66\x00\x80\x00\xb2\x00\xe4\x00\x16\x01"
            b"\x48\x01\x62\x01\x7c\x01\x96\x01\xc8\x01\xfa\x01\x14\x02\x46\x02"
            ...
            b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
            b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe8\x21\x1a\x22"
        ),
        "get_ch": get_ch,
    },
)()

The benefit of this solution is that you can easily pass a font as a parameter to a function (for example a print_str() function or a get_bounding_box() function).

The alternative format is selected by the --alt option

peterhinch commented 8 months ago

I don't understand the problem that this seeks to solve. As far as I can see you can already pass a font as a function arg, as in this fragment:

import font10 as font
def foo(f):
    print(f.heigt())
foo(font)  # Prints the height

I am wary of any change to the font file format: a lot of testing was done to prove that, when frozen, the data was not copied to RAM. It's easy to inadvertently create a format where the MicroPython runtime does this.

supcik commented 8 months ago

Thank you for your feedback. I never passed "module objects" as parameter to a function. Actually, I did not know that this was possible.

To me, it feels more natural to pass an object to a function (a bit like a font structure is used in Adafruit GFX).

A small benefits of having an object is that you can use f.height (without the parenthesis) instead of f.height()... but I agree that this might be a detail.

Anyhow, my proposal adds an alternative file format without impacting the current users and without changing the default. I already used it in a small project, and it seems to work well.

peterhinch commented 8 months ago

Passing the font as an arg is routine: it is one of the constructor args for Writer and CWriter classes. From my point of view this extra file format is added complexity with no benefit. Further, it introduces an unknown regarding performance as frozen bytecode. This would require detailed assessment.

I'm afraid this is a definite no from where I stand.

supcik commented 8 months ago

OK, I understand. I will keep this variant in my own repository and continue the development there.

peterhinch commented 8 months ago

Just FYI there is a reason why height is not accessed with f.height. This could easily be achieved by creating a module variable height. However it causes problems when the bytecode resides in Flash. The runtime has no way of knowing that you won't write

import font10 as f
f.height = 42

It may be nonsense but it's valid Python. So the runtime copies the bytecode to RAM. I think it copies the entire module, negating the purpose of freezing it.

A possible solution is to use a property to create a read-only variable, but I'm unsure if the runtime is intelligent enough not to copy to RAM. There is also a general convention against properties in MicroPython on performance grounds. Using a function is therefore preferable and has the merit of making the read-only attribute self-evident.

A final observation. The API for Python font files has been fixed for many years and a great deal of code relies on it.