jhanarato / uposatha-inky

Display the uposatha on a Pimoroni Inky display
MIT License
0 stars 0 forks source link

Sort out text bounding boxes #21

Closed jhanarato closed 1 year ago

jhanarato commented 1 year ago

We have three different ways of finding the bounding box of text:

I'll write three functions to do this and we can compare the results and benchmark them.

jhanarato commented 1 year ago

Testing shows the Image.getbbox() is equivalent to checking all pixels, except that it adds one pixel to right and bottom.

jhanarato commented 1 year ago

Given this:

>>> font_bbox("H", font)
BBox(left=0, top=7, right=21, bottom=28)
>>> pixel_bbox("H", font)
BBox(left=2, top=7, right=18, bottom=27)
>>> image_bbox("H", font)
BBox(left=2, top=7, right=19, bottom=28)

There's a bit of horizontal padding included in the font bbox.

jhanarato commented 1 year ago

ImageFont.getbbox() will align the bases, so no problem there. ImageFont.length() gives the exact length as a float, so that should do nicely.

jhanarato commented 1 year ago

But wait, no.

def test_should_measure_width_of_text():
    font = Font(size=30)
    expected = pixel_bbox("H", ImageFont.truetype(RobotoBold, 30)).width
    assert font.width("H") == expected

Gives us this:

Expected :17
Actual   :21.21875

getlength() is consistent with getbbox().

jhanarato commented 1 year ago

Benchmarks for our three bbox functions:

--------------------------- benchmark: 3 tests ---------------------------
Name (time in ms)        Min               Max              Mean          
--------------------------------------------------------------------------
test_font_bbox        1.2301 (1.0)      1.6206 (1.0)      1.2585 (1.0)    
test_image_bbox       2.5191 (2.05)     3.2160 (1.98)     2.5854 (2.05)   
test_pixel_bbox       3.1040 (2.52)     3.7406 (2.31)     3.1768 (2.52)   
--------------------------------------------------------------------------
jhanarato commented 1 year ago

This might provide a solution:

https://fonttools.readthedocs.io/en/latest/

Just need to figure out how fonts map to pixels.

jhanarato commented 1 year ago

On the scent...

https://stackoverflow.com/questions/4190667/how-to-get-width-of-a-truetype-font-character-in-1200ths-of-an-inch-with-python#61647653

jhanarato commented 1 year ago

Pimoroni's font packaging system works like this:

>>> from font_roboto import RobotoBold
>>> RobotoBold
'/home/jr/Code/uposatha-inky/venv3-11/lib/python3.11/site-packages/font_roboto/files/Roboto-Bold.ttf'

That's nothing more than finding the TTF fonts and creating a string object called RobotoBold.

We should probably just use filenames for fonts.

jhanarato commented 1 year ago

Here's our font construction again:

ImageFont.truetype(font=RobotoBold, size=size)

The font is just the file name. How about the size? What units are used?

The docs say pixels. Hmm...

jhanarato commented 1 year ago

An introduction to Glyphs:

http://www.fifi.org/doc/libttf2/docs/glyphs.htm

This mentions bounding boxes... perhaps we're getting somewhere.

jhanarato commented 1 year ago

Another interesting post on Stack Overflow:

https://stackoverflow.com/questions/68340553/how-to-convert-font-units-with-certain-font-size-to-pixels

jhanarato commented 1 year ago

This snippet comes from here:

https://fonttools.readthedocs.io/en/latest/ttLib/tables/_g_l_y_f.html

from font_roboto import RobotoBold
from fontTools.ttLib import TTFont
font = TTFont(RobotoBold)
from fontTools.pens.boundsPen import BoundsPen
glyphset = font.getGlyphSet()
bp = BoundsPen(glyphset)
glyphset["H"].draw(bp)
bp.bounds

And gives us: (130, 0, 1316, 1456)

jhanarato commented 1 year ago

Playing with the glyph width calculator I get this, for "H", RobotoBold, 30 point:

Glyph width in points = 21.2109375

from PIL import ImageFont
from font_roboto import RobotoBold
font = ImageFont.truetype(RobotoBold, 30)
font.getlength("H")

Gives 21.21875

Almost exactly the same.

jhanarato commented 1 year ago

Meanwhile, ImageFont.getmask() is different again:

font.getmask("Hello").size

(70, 22)

jhanarato commented 1 year ago

This little script shows that there is a little whitespace around the glyphs:

from PIL import Image, ImageFont, ImageDraw
from font_roboto import RobotoBold

palette = [
    255, 255, 255,  # 0 = WHITE
    0, 0, 0,        # 1 = BLACK
    255, 255, 0     # 2 = YELLOW
]

font = ImageFont.truetype(RobotoBold, 50)
text = "S"
bbox = font.getbbox(text)
image = Image.new(mode="P", size=(50, 50), color=0)
draw = ImageDraw.Draw(image)
draw.rectangle(bbox, fill=1)
draw.text((0, 0), text, 0, font)
image.putpalette(palette)
converted = image.convert(mode="RGB")
converted.show()

We're only working with uppercase S, M, T, W, F.

This looks good:

for c in "SMTWF":
    bbox = BBox(*font.getbbox(c))
    print(f"{c}: {bbox.width}, {bbox.height}")

Gives this:

S: 19, 22
M: 27, 22
T: 20, 22
W: 27, 22
F: 17, 22
jhanarato commented 1 year ago

The left and right padding is called "kerning":

https://graphicdesign.stackexchange.com/questions/131188/adobe-illustrator-fit-type-bounding-box-by-excluding-side-bearing

jhanarato commented 1 year ago

Some notes from yesterday:

jhanarato commented 1 year ago

We should have a module for all of these tests and utilities.

jhanarato commented 1 year ago

Does this give us units?

glyph(ord(c), font).width

If so, this is starting to make sense. We're returning:

glyph width in units x font points x units per em.

There are 2048 units per em in our font. That may be incidental.

If I understand, glyph width x font points = 1em

jhanarato commented 1 year ago

Good article on EM and UPM

https://help.fontlab.com/fontlab-vi/Font-Sizes-and-the-Coordinate-System/

jhanarato commented 1 year ago

I need to calculate the "left side bearing" or bearingX. And I've found another package that might do it, if fonttools doesn't:

https://pypi.org/project/glyphtools/ https://glyphtools.readthedocs.io/en/latest/index.html

jhanarato commented 1 year ago

glyphtools is awesome:

>>> glyphtools.get_glyph_metrics(font, "H")
{'width': 1448, 'lsb': 130, 'xMin': 130, 'xMax': 1316, 'yMin': 0, 'yMax': 1456, 'rise': 0, 'run': 1448, 'rsb': 132, 'fullwidth': 1186}
jhanarato commented 1 year ago

This page - http://www.fifi.org/doc/libttf2/docs/glyphs.htm - tells you what the above values mean.

jhanarato commented 1 year ago

Pretty printed for reference

>>> pprint.pprint(glyphtools.get_glyph_metrics(font, "H"))
{'fullwidth': 1186,
 'lsb': 130,
 'rise': 0,
 'rsb': 132,
 'run': 1448,
 'width': 1448,
 'xMax': 1316,
 'xMin': 130,
 'yMax': 1456,
 'yMin': 0}
jhanarato commented 1 year ago

Glyph metrics work nicely now. xMax - xMin and yMax - yMin seems to give us the best center point. Better even than using a centered anchor.