Closed jhanarato closed 1 year ago
Testing shows the Image.getbbox()
is equivalent to checking all pixels, except that it adds one pixel to right and bottom.
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.
ImageFont.getbbox()
will align the bases, so no problem there. ImageFont.length()
gives the exact length as a float, so that should do nicely.
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()
.
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)
--------------------------------------------------------------------------
This might provide a solution:
https://fonttools.readthedocs.io/en/latest/
Just need to figure out how fonts map to pixels.
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.
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...
An introduction to Glyphs:
http://www.fifi.org/doc/libttf2/docs/glyphs.htm
This mentions bounding boxes... perhaps we're getting somewhere.
Another interesting post on Stack Overflow:
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)
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.
Meanwhile, ImageFont.getmask()
is different again:
font.getmask("Hello").size
(70, 22)
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
The left and right padding is called "kerning":
Some notes from yesterday:
fonttools
width function I came across on Stack Overflow.We should have a module for all of these tests and utilities.
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
Good article on EM and UPM
https://help.fontlab.com/fontlab-vi/Font-Sizes-and-the-Coordinate-System/
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
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}
This page - http://www.fifi.org/doc/libttf2/docs/glyphs.htm - tells you what the above values mean.
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}
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.
We have three different ways of finding the bounding box of text:
ImageFont.getbbox()
Image.getbbox()
Image.load()
to find all the black pixels.I'll write three functions to do this and we can compare the results and benchmark them.