How to get the font pixel height using PIL's ImageFont class? - python

I am using PIL' ImageFont module to load fonts to generate text images.
I want the text to tightly bound to the edge, however, when using the ImageFont to get the font height, It seems that it includes the character's padding. As the red rectangle indicates.
c = 'A'
font = ImageFont.truetype(font_path, font_size)
width = font.getsize(c)[0]
height = font.getsize(c)[1]
im = Image.new("RGBA", (width, height), (0, 0, 0))
draw = ImageDraw.Draw(im)
draw.text((0, 0), 'A', (255, 255, 255), font=font)
im.show('charimg')
If I can get the actual height of the character, then I could skip the bounding rows in the bottom rectangle, could this info got from the font?
Thank you.

Exact size depends on many factors. I'll just show you how to calculate different metrics of font.
font = ImageFont.truetype('arial.ttf', font_size)
ascent, descent = font.getmetrics()
(width, baseline), (offset_x, offset_y) = font.font.getsize(text)
Height of red area: offset_y
Height of green area: ascent - offset_y
Height of blue area: descent
Black rectangle: font.getmask(text).getbbox()
Hope it helps.

The top voted answer is outdated. There is a new function in Pillow 8.0.0: ImageDraw.textbbox.
See the release notes for other text-related functions added in Pillow 8.0.0.
Note that ImageDraw.textsize, ImageFont.getsize and ImageFont.getoffset are broken, and should not be used for new code. These have been effectively replaced by the new functions with a cleaner API. See the documentaion for details.
To get a tight bounding box for a whole string you can use the following code:
from PIL import Image, ImageDraw, ImageFont
image = Image.new("RGB", (200, 80))
draw = ImageDraw.Draw(image)
font = ImageFont.truetype("arial.ttf", 30)
draw.text((20, 20), "Hello World", font=font)
bbox = draw.textbbox((20, 20), "Hello World", font=font)
draw.rectangle(bbox, outline="red")
print(bbox)
# (20, 26, 175, 48)
image.show()
You can combine it with the new ImageDraw.textlength to get individual bounding boxes per letter:
from PIL import Image, ImageDraw, ImageFont
image = Image.new("RGB", (200, 80))
draw = ImageDraw.Draw(image)
font = ImageFont.truetype("arial.ttf", 30)
xy = (20, 20)
text = "Example"
draw.text(xy, text, font=font)
x, y = xy
for c in text:
bbox = draw.textbbox((x, y), c, font=font)
draw.rectangle(bbox, outline="red")
x += draw.textlength(c, font=font)
image.show()
Note that this ignores the effect of kerning. Kering is currently broken with basic text layout, but could introduce a slight inaccuracy with Raqm layout. To fix it you would add the text length of pairs of letters instead:
for a, b in zip(text, text[1:] + " "):
bbox = draw.textbbox((x, y), a, font=font)
draw.rectangle(bbox, outline="red")
x += draw.textlength(a + b, font=font) - draw.textlength(b, font=font)

from PIL import Image, ImageDraw, ImageFont
im = Image.new('RGB', (400, 300), (200, 200, 200))
text = 'AQj'
font = ImageFont.truetype('arial.ttf', size=220)
ascent, descent = font.getmetrics()
(width, height), (offset_x, offset_y) = font.font.getsize(text)
draw = ImageDraw.Draw(im)
draw.rectangle([(0, 0), (width, offset_y)], fill=(237, 127, 130)) # Red
draw.rectangle([(0, offset_y), (width, ascent)], fill=(202, 229, 134)) # Green
draw.rectangle([(0, ascent), (width, ascent + descent)], fill=(134, 190, 229)) # Blue
draw.rectangle(font.getmask(text).getbbox(), outline=(0, 0, 0)) # Black
draw.text((0, 0), text, font=font, fill=(0, 0, 0))
im.save('result.jpg')
print(width, height)
print(offset_x, offset_y)
print('Red height', offset_y)
print('Green height', ascent - offset_y)
print('Blue height', descent)
print('Black', font.getmask(text).getbbox())
result
Calculate area pixel
from PIL import Image, ImageDraw, ImageFont
im = Image.new('RGB', (400, 300), (200, 200, 200))
text = 'AQj'
font = ImageFont.truetype('arial.ttf', size=220)
ascent, descent = font.getmetrics()
(width, height), (offset_x, offset_y) = font.font.getsize(text)
draw = ImageDraw.Draw(im)
draw.rectangle([(0, offset_y), (font.getmask(text).getbbox()[2], ascent + descent)], fill=(202, 229, 134))
draw.text((0, 0), text, font=font, fill=(0, 0, 0))
im.save('result.jpg')
print('Font pixel', (ascent + descent - offset_y) * (font.getmask(text).getbbox()[2]))
result

Related

Draw text with background color

I want to know how can I draw text like this check image
As you can see text is on a green image and text has pink color background
My code, this is part of my code I'm using PIL
draw = ImageDraw.Draw(background)
font = ImageFont.truetype("assets/font2.ttf", 40)
font2 = ImageFont.truetype("assets/font2.ttf", 70)
arial = ImageFont.truetype("assets/font2.ttf", 30)
name_font = ImageFont.truetype("assets/font.ttf", 30)
para = textwrap.wrap(title, width=32)
j = 0
draw.text(
(10, 10), f"{h}", fill="red", font=name_font
)
draw.text(
(600, 150),
"NOW PLAYING",
fill="white",
stroke_width=2,
stroke_fill="white",
font=font2,
)
Thanks in advance :-)
You can use the draw.textbbox method to get a bounding box for your text string and fill it using the draw.rectangle method.
from PIL import Image, ImageDraw, ImageFont
image = Image.new("RGB", (500, 100), "white")
font = ImageFont.truetype("segoeui.ttf", 40)
draw = ImageDraw.Draw(image)
position = (10, 10)
text = "Hello world"
bbox = draw.textbbox(position, text, font=font)
draw.rectangle(bbox, fill="red")
draw.text(position, text, font=font, fill="black")
image.show()
If you want a larger margin for the background rectangle, you can adjust the returned bounding box like so:
left, top, right, bottom = draw.textbbox(position, text, font=font)
draw.rectangle((left-5, top-5, right+5, bottom+5), fill="red")
draw.text(position, text, font=font, fill="black")

How to set anchor to fit the text at center of image with PIL.ImageDraw.Draw.text?

Drawn text on an image with Pillow library, tried to fit the text anchored at center of image by option anchor='mm', but it looks not exactly as center of image.
Demo code
from PIL import Image, ImageDraw, ImageFont
im = Image.new("RGBA", (500, 500), (255, 255, 255, 255))
font = ImageFont.truetype(font='arial.ttf', size=320)
draw = ImageDraw.Draw(im)
draw.text((250, 250), "123", font=font, fill='black', anchor='mm')
im.show()
result:
expectation:
The reason why the text looks misaligned is because there is a little margin at the left of the 1 and pillow will align the text including this margin.
Adding 0 at both ends will visualize that it is correctly centered and that there is a large margin at 1.
If you want to align the text excluding margins, ImageFont.getmask is helpful.
def get_offset_for_true_mm(text, draw, font):
anchor_bbox = draw.textbbox((0, 0), text, font=font, anchor='lt')
anchor_center = (anchor_bbox[0] + anchor_bbox[2]) // 2, (anchor_bbox[1] + anchor_bbox[3]) // 2
mask_bbox = font.getmask(text).getbbox()
mask_center = (mask_bbox[0] + mask_bbox[2]) // 2, (mask_bbox[1] + mask_bbox[3]) // 2
return anchor_center[0] - mask_center[0], anchor_center[1] - mask_center[1]
im = Image.new("RGBA", (500, 500), (255, 255, 255, 255))
font = ImageFont.truetype(font='arial.ttf', size=320)
draw = ImageDraw.Draw(im)
text = "123"
offset = get_offset_for_true_mm(text, draw, font)
draw.text((250 + offset[0], 250 + offset[1]), text, font=font, fill='black', anchor='mm')
im.show()
Result:

How do I reduce the number of colors used in ImageDraw.text

I have a folder with .ttf and .otf fonts and would like to write them on my ImageDraw object but with NO shading. A single RGB only. I have tried bitmap fonts but they A) don't look nice and B) use more than one color anyway.
I have read that there is a library for converting .bdf to .pil. If I convert arial.ttf to arial.bdf and then to arial.pil, will this be what I'm looking for? The text will almost always be dropped onto a background--so should I consider writing the text first on a blank canvas, do a color reduction, and then paste that canvas onto my background?
I have previously made this program using Java and it writes text very nicely on my bitmaps. One color, symmetrical, etc. Image below.
Below are the two attempts with python. The blockier one is a bitmap font, the other is regular arial.ttf.
Here is my code:
def personalize(self):
names = self.personalize_entry.get("1.0", 'end-1c').split('\n')
num_names = len(names)
num_grids = math.ceil(num_names/20)
answer = ask_grid_background()
separator = Image.new('RGB', (473, 1), color_dict['P'])
background = Image.new('RGB', (473, 821), color_dict['.'])
if answer:
showinfo("Bitmap", "Give me the design.")
file_path = filedialog.askopenfilename()
filename = path_leaf(file_path)
filename = filename[:-4]
__, __, center = read(file_path)
if center == 0:
messagebox.showinfo("Hmmm", f"I couldn't find a center...are you sure this is a basic set up?")
return False
img = Image.open(file_path)
size_num = img.size
section = img.crop((5, (size_num[1] - 55 - center), 478, (size_num[1] - center - 15)))
background.paste(separator, (0, 0))
for i in range(20):
background.paste(section, (0, (41 * i + 1)))
background.paste(separator, (0, (41 * i) + 41))
else:
background.paste(separator, (0, 0))
for i in range(20):
# background.paste(section,(0,(41*i+1)))
background.paste(separator, (0, (41 * i) + 41))
draw = ImageDraw.Draw(background)
fnt = ImageFont.truetype("Fonts/PIXEAB__.ttf",36)
draw.text((10, 10), names[0], font=fnt, fill=(0, 0, 0))
background.show()
ImageDraw has an undocumented member fontmode, which can be set to '1' (cf. Pillow's image modes) to turn off the anti-aliasing of the rendered text.
Let's compare common rendered text, draw.fontmode is implicitly set to 'L':
from PIL import Image, ImageDraw, ImageFont
image = Image.new('RGB', (800, 200), (255, 255, 255))
draw = ImageDraw.Draw(image)
font = ImageFont.truetype('arial.ttf', 150)
draw.text((10, 10), 'Hello World', font=font, fill=(0, 0, 0))
image.save('image.png')
Now, let's explicitly set draw.fontmode = '1':
from PIL import Image, ImageDraw, ImageFont
image = Image.new('RGB', (800, 200), (255, 255, 255))
draw = ImageDraw.Draw(image)
draw.fontmode = '1'
font = ImageFont.truetype('arial.ttf', 150)
draw.text((10, 10), 'Hello World', font=font, fill=(0, 0, 0))
image.save('image.png')
Et voilà – no anti-aliasing, all pixels are solid black.
----------------------------------------
System information
----------------------------------------
Platform: Windows-10-10.0.16299-SP0
Python: 3.9.1
PyCharm: 2021.1
Pillow: 8.2.0
----------------------------------------

Draw underline text with PIL

There is a post related to bold/italic:
Draw bold/italic text with PIL?
However, how to draw underline text with PIL?
Looks like there is no standard way of doing this, but you always can implement it.
Possible solution:
import Image
import ImageDraw
import ImageFont
def draw_underlined_text(draw, pos, text, font, **options):
twidth, theight = draw.textsize(text, font=font)
lx, ly = pos[0], pos[1] + theight
draw.text(pos, text, font=font, **options)
draw.line((lx, ly, lx + twidth, ly), **options)
im = Image.new('RGB', (400, 400), (255,)*3)
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("arial.ttf", 50)
draw_underlined_text(draw, (50, 150), 'Hello PIL!', font, fill=0)
draw_underlined_text(draw, (50, 300), 'Test', font, fill=128)
im.show()

Draw text image without crop need by PIL

I would like to draw a text by using PIL. But my problem is I need to crop the text image again after run the program. The thing i need is only text, no border. Any one can suggest?
Thank you.
This is my code:
import Image, ImageDraw, ImageFont
def draw (text, size, color) :
fontPath = '/home/FreeSansBold.ttf'
font = ImageFont.truetype(fontPath, size)
size2 = font.getsize(text)
im = Image.new('RGBA', size2, (0, 0, 0, 0))
draw = ImageDraw.Draw(im)
draw.text((0, 0), text, font=font, fill=color)
im.save(text +'.png')
drawA = draw('A', 200, 'green')
drawC = draw('C', 200, 'blue')
drawG = draw('G', 200, 'yellow')
drawT = draw('T', 200, 'red')
Could you clarify what you mean by no border? Are you wanting text tight against edge of the image? If so this should work:
import Image, ImageDraw, ImageFont
def draw (text, size, color) :
fontPath = '/home/FreeSansBold.ttf'
font = ImageFont.truetype(fontPath, size)
size2 = font.getsize(text)
im = Image.new('RGBA', size2, (0, 0, 0, 0))
draw = ImageDraw.Draw(im)
draw.text((0, 0), text, font=font, fill=color)
pixels = im.load()
width, height = im.size
max_x = max_y = 0
min_y = height
min_x = width
# find the corners that bound the letter by looking for
# non-transparent pixels
transparent = (0, 0, 0, 0)
for x in xrange(width):
for y in xrange(height):
p = pixels[x,y]
if p != transparent:
min_x = min(x, min_x)
min_y = min(y, min_y)
max_x = max(x, max_x)
max_y = max(y, max_y)
cropped = im.crop((min_x, min_y, max_x, max_y))
cropped.save(text +'.png')
drawA = draw('A', 200, 'green')
drawC = draw('C', 200, 'blue')
drawG = draw('G', 200, 'yellow')
drawT = draw('T', 200, 'red')
It produces an image like this (I filled in the transparent pixels with red to show the bounds of the image better: http://img43.imageshack.us/img43/3066/awithbg.png

Categories