Can't center characters using Pillow and Python - python

I am trying to write characters in specific locations in an image. I am using Pillow v6, Python 3.6.
Here is my code, I draw char by char, passing the top left point that I calculated.
font = ImageFont.truetype('platechar.tff', 500)
def draw_single_char(img, font, val, tl): #tl = (x,y)
pil_img = Image.fromarray(np.uint8(img))
draw = ImageDraw.Draw(pil_img)
draw.text(tl, val, (0,0,0), font=font)
img = np.array(pil_img)
return img
The output is not centered, I got the character width and height from the font, then with my top left point I draw the rectangle enclosing the character. The character is not centered inside the rectangle.
Font: https://drive.google.com/open?id=1N9rN-AgjK83U9ZDycLKxjeMP3o36vbfg
I want it to be like this (another font)
EDIT
Using Bidu Font editor I was able to remove the horizontal space (blue line). How can I center it vertically?.
Result so far ...

It looks like the font you are using contains non-centered numbers inside originally. So you should choose another font or you can modify your placechar.tff in a special editor for fonts.
Also you can calculate coordinate offsets for each symbol manually, store them into a dictionary and apply it for your text before drawing. It doesn't look like a good idea, but it would work also.

Calculate the width and height of the text to be drawn:
from PIL import Image, ImageDraw, ImageFont
txt='7'
font = ImageFont.truetype('platechar.ttf', 250)
(W, H) = font.getsize(txt)
image = Image.new('RGB', (256, 256), (63, 63, 63, 0))
drawer = ImageDraw.Draw(image)
(offset_w, offset_h) = font.getoffset(txt)
(x, y, W_mask, H_mask) = font.getmask(txt).getbbox()
drawer.text((10, 10 - offset_h), txt, align='center', font=font)
drawer.rectangle((10, 10, W + offset_w, 10 + H - offset_h), outline='black')
drawer.rectangle((x+10, y+10, W_mask+10, H_mask+10), outline='red')
image.show()
image.save('example.png', 'PNG')

After taking the path that #Fomalhaut suggested, using font editor. I found Bidu font editor (link in the question). I was able to fix the horizontal space (also shown in the question). For vertical space, after searching the menus, I found setting option to change the ascent.
I decreased it to 1440, and it worked.

Related

How to draw character with gradient colors using PIL?

I have the function that generates character images from a font file using PIL. For the current example, it generates a white background image and a red character text. What I want now is that instead of pure red or any other color I can generate a gradient color. Is this possible with my current code? I have seen this post but it didn't help me.
Edit 1:
Currently, I am generating English alphabet images from font files using PIL. The fonts variable in my code has N number of ".ttf" files. lets suppose N=3 all in different styles e.g. style1, style2, style3. My current code will always generate these N different styles with fixed white background and fixed red character color. As shown in the below figure.
Instead of red color for the characters, I would like to apply gradients for each style. i.e. all characters in style1 font images should have the same gradient, style 2 font style should have a different gradient from style1 characters but should be the same for all of its characters and so on. As shown below (styles are different from the above images. Its just for demonstration of what I want).
My code so far:
fonts = glob.glob(os.path.join(fonts_dir, '*.ttf'))
for font in fonts:
image = Image.new('RGB', (IMAGE_WIDTH, IMAGE_HEIGHT), color='white')
font = ImageFont.truetype(font, 150)
drawing = ImageDraw.Draw(image)
w, h = drawing.textsize(character, font=font)
drawing.text(
((IMAGE_WIDTH-w)/2, (IMAGE_HEIGHT-h)/2),
character,
fill='red',
font=font
)
image.save(file_path, 'PNG')
One fairly easy way of doing it is to draw the text in white on a black background and then use that as the alpha/transparency channel over a background with a gradient.
Here's a background gradient:
#!/usr/bin/env python3
from PIL import Image, ImageDraw, ImageFont
w, h = 400, 150
image = Image.open('gradient.jpg').rotate(90).resize((w,h))
font = ImageFont.truetype('/System/Library/Fonts/MarkerFelt.ttc', 80)
# Create new alpha channel - solid black
alpha = Image.new('L', (w,h))
draw = ImageDraw.Draw(alpha)
draw.text((20,10),'Some Text',fill='white',font=font)
alpha.save('alpha.png')
# Use text cutout as alpha channel for gradient image
image.putalpha(alpha)
image.save('result.png')
The alpha.png looks like this:
And the result.png looks like this:
Note that the area around the text is transparent. but you can easily paste it onto a white or black background. So, say you wanted the background yellow, add the following to the bottom of the code above:
solid = Image.new('RGB', (w,h), 'yellow')
solid.paste(image,image)
solid.save('result2.png')

Python Pillow text vertical align

I'm trying to position 1 symbol in the top left corner of the given bounding box.
draw = ImageDraw.Draw(img)
font = ImageFont.truetype('LiberationSans-Regular.ttf', 150)
draw.text((x0, y0), "€", "green", font=font)
But when I place text, for example at (0, 0) of the box, it appears with some padding at the top. Also it seems padding size depends on font size.
Is there a way to calculate size of this padding? And maybe move it on that exact amount of pixels upwards.
Basically top pixel of the given symbol must be at y0 of the bounding box, regardless of font and font size.

How do I properly set DPI when saving a pillow image?

I am trying to create images programatically on Python using Pillow library but I'm having problems with the image quality of the text inside the image.
I want to save the Image the I generate to PNG, so I'm setting the DPI when saving according to this, but whether I save with dpi=(72,72) or dpi=(600,600) it visually looks the same.
My code for doing it is the following:
from PIL import Image, ImageDraw, ImageFont
def generate_empty_canvas(width, height, color='white'):
size = (width, height)
return Image.new('RGB', size, color=color)
def draw_text(text, canvas):
font = ImageFont.truetype('Verdana.ttf', 10)
draw = ImageDraw.Draw(canvas)
if '\n' not in text:
draw.text((0, 0), text, font=font, fill='black')
else:
draw.multiline_text((0, 0), text, font=font, fill='black')
def create_sample():
text = 'aaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbbbbbb\nccccccccccccccccccccc'
canvas = generate_empty_canvas(200, 50)
draw_text(text, canvas)
canvas.save('low_quality.png', dpi=(72, 72))
canvas.save('high_quality.png', dpi=(600, 600))
The low_quality.png is:
The high_quality.png is:
As it's visible by the images the quality didn't change.
What am I doing wrong here?
Where do I set the DPI so that the image really has dpi=600?
The DPI values are only metadata on computer images. They give hints on how to display or print an image.
Printing a 360×360 image with 360 dpi will result in a 1×1 inches printout.
A simplified way to explain it: The DPI setting recommends a zoom level for the image.
Saving with other DPIs will not change the content of the image. If you want a larger image create a larger canvas and use a larger font.

ImageFont's getsize() does not get correct text size?

I use the following two methods to to generate text preview image for a .ttf font file
PIL method:
def make_preview(text, fontfile, imagefile, fontsize=30):
try:
font = ImageFont.truetype(fontfile, fontsize)
text_width, text_height = font.getsize(text)
img = Image.new('RGBA', (text_width, text_height))
draw = ImageDraw.Draw(img)
draw.text((0, 0), text, font=font, fill=(0, 0, 0))
return True
except:
return False
ImageMagick method:
def make_preview(text, fontfile, imagefile, fontsize=30):
p = subprocess.Popen(['convert', '-font', fontfile, '-background',
'transparent', '-gravity', 'center', '-pointsize', str(fontsize),
'-trim', '+repage', 'label:%s' % text, image_file])
return p==0
Both methods create correct preview images most of time but in some rare cases (<2%), the font.getsize(text) just cannot get the correct text size which result in text overflowed provided canvas. ImageMagick has same problem.
Sample fonts and previews:
HANFORD.TTF
http://download.appfile.com/HANFORD.png
NEWTOW.TTF
http://download.appfile.com/NEWTOW.png
MILF.TTF
http://download.appfile.com/MILF.png
SWANSE.TTF
http://download.appfile.com/SWANSE.png
I have looked into ImageMagick's documentations and found the explanation of this problem at http://www.imagemagick.org/Usage/text/#overflow.
Is it possible to detect such text overflows and draw text to fit the canvas as we expected?
Not a programming solution, but when I regenerate your problem, its only happens on your fonts (other fonts like Arial is no problem at all), so I have fixed your font files (by changing ascent/decent metrics). you can download here,
And sorry about Hanford Script Font, its not perfect as you see, height seems ok, but left side is not get drawed, its out of my understanding.
UPDATE: Regarding Hanford Font, Here is a work around, pass extra space in text like " Handford Script", and then crop the extra space in image like img=img.crop(img.getbbox())
alt text http://img64.imageshack.us/img64/1903/hanfordfontworkaround.jpg
UPDATE2:I had to pass color=(255,255,255) in Image.New to get Black Text on White background
img = Image.new('RGBA', (text_width, text_height),color=(255,255,255))
I had a similar issue once in PHP and ImageMagick.
In the end, I solved this by drawing the text on a very large canvas, and then trimming it using the trim/auto-crop functions that shave extra space off the image.
If I understand your preview function right, it is actually already doing exactly that: It should be enough to just remove the width and height settings.
In this case, just specify ImageMagick to use a larger canvas size with a fixed font size and it will draw text at specified point size while keeping its integrity.
def make_preview(text, fontfile, imagefile, fontsize=30):
p = subprocess.call(['convert', '-font', fontfile, '-background',
'transparent', '-gravity', 'center', '-size', '1500x300',
'-pointsize', str(fontsize), '-trim', '+repage', 'label:%s' % text, image_file])
return p==0
If you need to fit text into specified canvas rather than using a fixed point size, you may need to resize the output image after it's created.
PIL doesn't do this very well drawing exotic fonts, no matter what point size you specify to load a font, it always overflows text outside output image.

Center-/middle-align text with PIL?

How would I center-align (and middle-vertical-align) text when using PIL?
Deprecation Warning: textsize is deprecated and will be removed in Pillow 10 (2023-07-01). Use textbbox or textlength instead.
Code using textbbox instead of textsize.
from PIL import Image, ImageDraw, ImageFont
def create_image(size, bgColor, message, font, fontColor):
W, H = size
image = Image.new('RGB', size, bgColor)
draw = ImageDraw.Draw(image)
_, _, w, h = draw.textbbox((0, 0), message, font=font)
draw.text(((W-w)/2, (H-h)/2), message, font=font, fill=fontColor)
return image
myFont = ImageFont.truetype('Roboto-Regular.ttf', 16)
myMessage = 'Hello World'
myImage = create_image((300, 200), 'yellow', myMessage, myFont, 'black')
myImage.save('hello_world.png', "PNG")
Result
Use Draw.textsize method to calculate text size and re-calculate position accordingly.
Here is an example:
from PIL import Image, ImageDraw
W, H = (300,200)
msg = "hello"
im = Image.new("RGBA",(W,H),"yellow")
draw = ImageDraw.Draw(im)
w, h = draw.textsize(msg)
draw.text(((W-w)/2,(H-h)/2), msg, fill="black")
im.save("hello.png", "PNG")
and the result:
If your fontsize is different, include the font like this:
myFont = ImageFont.truetype("my-font.ttf", 16)
draw.textsize(msg, font=myFont)
Here is some example code which uses textwrap to split a long line into pieces, and then uses the textsize method to compute the positions.
from PIL import Image, ImageDraw, ImageFont
import textwrap
astr = '''The rain in Spain falls mainly on the plains.'''
para = textwrap.wrap(astr, width=15)
MAX_W, MAX_H = 200, 200
im = Image.new('RGB', (MAX_W, MAX_H), (0, 0, 0, 0))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype(
'/usr/share/fonts/truetype/msttcorefonts/Arial.ttf', 18)
current_h, pad = 50, 10
for line in para:
w, h = draw.textsize(line, font=font)
draw.text(((MAX_W - w) / 2, current_h), line, font=font)
current_h += h + pad
im.save('test.png')
One shall note that the Draw.textsize method is inaccurate. I was working with low pixels images, and after some testing, it turned out that textsize considers every character to be 6 pixel wide, whereas an I takes max. 2 pixels and a W takes min. 8 pixels (in my case). And so, depending on my text, it was or wasn't centered at all. Though, I guess "6" was an average, so if you're working with long texts and big images, it should still be ok.
But now, if you want some real accuracy, you better use the getsize method of the font object you're going to use:
arial = ImageFont.truetype("arial.ttf", 9)
w,h = arial.getsize(msg)
draw.text(((W-w)/2,(H-h)/2), msg, font=arial, fill="black")
As used in Edilio's link.
A simple solution if you're using PIL 8.0.0 or above: text anchors
width, height = # image width and height
draw = ImageDraw.draw(my_image)
draw.text((width/2, height/2), "my text", font=my_font, anchor="mm")
mm means to use the middle of the text as anchor, both horizontally and vertically.
See the anchors page for other kinds of anchoring. For example if you only want to center horizontally you may want to use ma.
The PIL docs for ImageDraw.text are a good place to start, but don't answer your question.
Below is an example of how to center the text in an arbitrary bounding box, as opposed to the center of an image. The bounding box is defined as: (x1, y1) = upper left corner and (x2, y2) = lower right corner.
from PIL import Image, ImageDraw, ImageFont
# Create blank rectangle to write on
image = Image.new('RGB', (300, 300), (63, 63, 63, 0))
draw = ImageDraw.Draw(image)
message = 'Stuck in\nthe middle\nwith you'
bounding_box = [20, 30, 110, 160]
x1, y1, x2, y2 = bounding_box # For easy reading
font = ImageFont.truetype('Consolas.ttf', size=12)
# Calculate the width and height of the text to be drawn, given font size
w, h = draw.textsize(message, font=font)
# Calculate the mid points and offset by the upper left corner of the bounding box
x = (x2 - x1 - w)/2 + x1
y = (y2 - y1 - h)/2 + y1
# Write the text to the image, where (x,y) is the top left corner of the text
draw.text((x, y), message, align='center', font=font)
# Draw the bounding box to show that this works
draw.rectangle([x1, y1, x2, y2])
image.show()
image.save('text_center_multiline.png')
The output shows the text centered vertically and horizontally in the bounding box.
Whether you have a single or multiline message no longer matters, as PIL incorporated the align='center' parameter. However, it is for multiline text only. If the message is a single line, it needs to be manually centered. If the message is multiline, align='center' does the work for you on subsequent lines, but you still have to manually center the text block. Both of these cases are solved at once in the code above.
Use the textsize method (see docs) to figure out the dimensions of your text object before actually drawing it. Then draw it starting at the appropriate coordinates.
All the other answers did NOT take text ascender into consideration.
Here's a backport of ImageDraw.text(..., anchor="mm"). Not sure if it's fully compatible with anchor="mm", cause I haven't tested the other kwargs like spacing, stroke_width yet. But I ensure you this offset fix works for me.
from PIL import ImageDraw
from PIL import __version__ as pil_ver
PILLOW_VERSION = tuple([int(_) for _ in pil_ver.split(".")[:3]])
def draw_anchor_mm_text(
im,
xy,
# args shared by ImageDraw.textsize() and .text()
text,
font=None,
spacing=4,
direction=None,
features=None,
language=None,
stroke_width=0,
# ImageDraw.text() exclusive args
**kwargs,
):
"""
Draw center middle-aligned text. Basically a backport of
ImageDraw.text(..., anchor="mm").
:param PIL.Image.Image im:
:param tuple xy: center of text
:param unicode text:
...
"""
draw = ImageDraw.Draw(im)
# Text anchor is firstly implemented in Pillow 8.0.0.
if PILLOW_VERSION >= (8, 0, 0):
kwargs.update(anchor="mm")
else:
kwargs.pop("anchor", None) # let it defaults to "la"
if font is None:
font = draw.getfont()
# anchor="mm" middle-middle coord xy -> "left-ascender" coord x'y'
# offset_y = ascender - top, https://stackoverflow.com/a/46220683/5101148
# WARN: ImageDraw.textsize() return text size with offset considered.
w, h = draw.textsize(
text,
font=font,
spacing=spacing,
direction=direction,
features=features,
language=language,
stroke_width=stroke_width,
)
offset = font.getoffset(text)
w, h = w - offset[0], h - offset[1]
xy = (xy[0] - w / 2 - offset[0], xy[1] - h / 2 - offset[1])
draw.text(
xy,
text,
font=font,
spacing=spacing,
direction=direction,
features=features,
language=language,
stroke_width=stroke_width,
**kwargs,
)
Refs
https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html
https://github.com/python-pillow/Pillow/issues/4789
https://stackoverflow.com/a/46220683/5101148
https://github.com/python-pillow/Pillow/issues/2486
Using a combination of anchor="mm" and align="center" works wonders. Example
draw.text(
xy=(width / 2, height / 2),
text="centered",
fill="#000000",
font=font,
anchor="mm",
align="center"
)
Note: Tested where font is an ImageFont class object constructed as such:
ImageFont.truetype('path/to/font.ttf', 32)
This is a simple example to add a text in the center of the image
from PIL import Image, ImageDraw, ImageFilter
msg = "hello"
img = Image.open('image.jpg')
W, H = img.size
box_image = img.filter(ImageFilter.BoxBlur(4))
draw = ImageDraw.Draw(box_image)
w, h = draw.textsize(msg)
draw.text(((W - w) / 2, (H - h) / 2), msg, fill="black")
box_image.show()
if you are using the default font then you can use this simple calculation
draw.text((newimage.width/2-len(text)*3, 5), text,fill="black", align ="center",anchor="mm")
the main thing is
you have to divide the image width by 2 then get the length of the string you want and multiply it by 3 and subtract it from the division result
newimage.width/2-len(text)*3 #this is X position
**this answer is an estimation for the default font size used if you use a custom font then the multiplier must be changed accordingly. in the default case it is 3

Categories