Outline text on image in Python - python

I have been using PIL Image
I am trying to draw text on an image. I want this text to have a black outline like most memes. I've attempted to do this by drawing a shadow letter of a bigger font behind the letter in front. I've adjusted the x and y postions of the shadow accordingly. The shadow is slightly off though. The letter in front should be exactly in the middle of the shadow letter, but this isn't the case. The question mark certainly isn't centered horizontally, and all the letters are too low vertically. The outline also just doesn't look good.
Below is a minimum reproducible example to produce the image above.
Link to the font
Link to original image
from PIL import Image, ImageDraw, ImageFont
caption = "Why is the text slightly off?"
img = Image.open('./example-img.jpg')
d = ImageDraw.Draw(img)
x, y = 10, 400
font = ImageFont.truetype(font='./impact.ttf', size=50)
shadowFont = ImageFont.truetype(font='./impact.ttf', size=60)
for idx in range(0, len(caption)):
char = caption[idx]
w, h = font.getsize(char)
sw, sh = shadowFont.getsize(char) # shadow width, shadow height
sx = x - ((sw - w) / 2) # Shadow x
sy = y - ((sh - h) / 2) # Shadow y
# print(x,y,sx,sy,w,h,sw,sh)
d.text((sx, sy), char, fill="black", font=shadowFont) # Drawing the text
d.text((x, y), char, fill=(255,255,255), font=font) # Drawing the text
x += w + 5
img.save('example-output.jpg')
Another approach includes drawing the text 4 times in black behind the main text at positions slightly higher, slightly lower, slightly left, and slightly right, but these have also not been optimal as shown below
Code to produce the image above
from PIL import Image, ImageDraw, ImageFont
caption = "Why does the Y and i look weird?"
x, y = 10, 400
font = ImageFont.truetype(font='./impact.ttf', size=60)
img = Image.open('./example-img.jpg')
d = ImageDraw.Draw(img)
shadowColor = (0, 0, 0)
thickness = 4
d.text((x - thickness, y - thickness), caption, font=font, fill=shadowColor, thick=thickness)
d.text((x + thickness, y - thickness), caption, font=font, fill=shadowColor, thick=thickness)
d.text((x - thickness, y + thickness), caption, font=font, fill=shadowColor, thick=thickness)
d.text((x + thickness, y + thickness), caption, font=font, fill=shadowColor, thick=thickness)
d.text((x, y), caption, spacing=4, fill=(255, 255, 255), font=font) # Drawing the text
img.save('example-output.jpg')

I don't know since what version, but about a year ago Pillow added text stroking. You probably need to update it if you haven't do so lately. Example usage with stroke_width of 2:
from PIL import Image, ImageDraw, ImageFont
caption = 'I need to update my Pillow'
img = Image.open('./example-img.jpg')
d = ImageDraw.Draw(img)
font = ImageFont.truetype('impact.ttf', size=50)
d.text((10, 400), caption, fill='white', font=font,
stroke_width=2, stroke_fill='black')
img.save('example-output.jpg')

You can use mathlibplot text Stroke effect which uses PIL.
Example:
import matplotlib.pyplot as plt
import matplotlib.patheffects as path_effects
import matplotlib.image as mpimg
fig = plt.figure(figsize=(7, 5))
fig.figimage(mpimg.imread('seal.jpg'))
text = fig.text(0.5, 0.1, 'This text stands out because of\n'
'its black border.', color='white',
ha='center', va='center', size=30)
text.set_path_effects([path_effects.Stroke(linewidth=3, foreground='black'),
path_effects.Normal()])
plt.savefig('meme.png')
Result:

As #Abang pointed out, use stroke_width and stroke_fill.
Link for more details
Code:
from PIL import Image, ImageDraw, ImageFont
caption = 'Ans: stroke_width & stroke_fill'
img = Image.open('./example-img.jpg')
d = ImageDraw.Draw(img)
font = ImageFont.truetype('impact.ttf', size=50)
d.text((60, 400), caption, fill='white', font=font, spacing = 4, align = 'center',
stroke_width=4, stroke_fill='black')
img.save('example-output.jpg')

Related

Kerning text in Python PIL

I have some code to produce an image with text on it, using PIL:
from PIL import Image, ImageDraw, ImageFont
width = 2480
height = 3071
message = "Hello"
font = ImageFont.truetype("Arial.ttf", size=900)
img = Image.new('RGB', (width, height), color='black')
imgDraw = ImageDraw.Draw(img)
textWidth, textHeight = imgDraw.textsize(message, font=font)
xText = (width - textWidth) / 2
yText = (height - textHeight) / 2
imgDraw.text((152, 2100), message, font=font, fill=(255, 255, 255))
img.save('result.png')
I get a result like this: Example
But I need the text to have less space between letters. I intend to use this code for a batch of 40+ words located in a CSV file. How can I automatically adjust the kerning? I've seen other articles pointing to a related problem but they haven't helped.

Pillow, how to put the text in the center of the image

I use the Pillow (PIL) 6.0 and add text in the image. And I want to put the text in the center of the image. Here is my code,
import os
import string
from PIL import Image
from PIL import ImageFont, ImageDraw, ImageOps
width, height = 100, 100
text = 'H'
font_size = 100
os.makedirs('./{}'.format(text), exist_ok=True)
img = Image.new("L", (width, height), color=0) # "L": (8-bit pixels, black and white)
font = ImageFont.truetype("arial.ttf", font_size)
draw = ImageDraw.Draw(img)
w, h = draw.textsize(text, font=font)
draw.text(((width-w)/2, (height-h)/2), text=text, fill='white', font=font)
img.save('H.png')
Here is the output:
Question:
The text is in the center horizontally, but not in the center vertically. How can I put it in the center horizontally and vertically?
Text always have some added space around characters, e.g. if we create a box that is the exact size reported for your 'H'
img = Image.new("L", (width, height), color=0) # "L": (8-bit pixels, black and white)
font = ImageFont.truetype("arial.ttf", font_size)
draw = ImageDraw.Draw(img)
w, h = draw.textsize(text, font=font)
# draw.text(((width-w)/2, (height-h)/2), text=text, fill='white', font=font)
# img.save('H.png')
img2 = Image.new("L", (w, h), color=0) # "L": (8-bit pixels, black and white)
draw2 = ImageDraw.Draw(img2)
draw2.text((0, 0)), text=text, fill='white', font=font)
img2.save('H.png')
gives the bounding box:
Knowing that line height is normally ~20% larger than the glyphs/characters (+ some trial and error), and we can figure out the extent of the extra space. (The extra space for width is equally distributed so not interesting for centering).
draw2.text((0, 0 - int(h*0.21)), text=text, fill='white', font=font)
which moves the 'H' to the top:
Plugging this back into your original code:
img = Image.new("L", (width, height), color=0) # "L": (8-bit pixels, black and white)
font = ImageFont.truetype("arial.ttf", font_size)
draw = ImageDraw.Draw(img)
w, h = draw.textsize(text, font=font)
h += int(h*0.21)
draw.text(((width-w)/2, (height-h)/2), text=text, fill='white', font=font)
img.save('H.png')
gives:
The 0.21 factor usually works well for a large range of font sizes for the same font. E.g. just plugging in font size 30:
Use of anchors can help with this
import os
import string
from PIL import Image
from PIL import ImageFont, ImageDraw, ImageOps
width, height = 100, 100
text = 'H'
font_size = 100
os.makedirs('./{}'.format(text), exist_ok=True)
img = Image.new("L", (width, height), color=0) # "L": (8-bit pixels, black and white)
font = ImageFont.truetype("arial.ttf", font_size)
draw = ImageDraw.Draw(img)
draw.text(((width)/2, (height)/2), text=text, fill='white', font=font, anchor="mm", align='center')
img.save('H.png')
It works fine without w and h
P.S.: I've tested it, and it can work well with non-English characters also

Correctly centring text (PIL/Pillow)

I'm drawing some text on a black strip, and then pasting the result on top of a base image, using PIL. One criticality is having the text position perfectly in the center of the black strip.
I cater for that via the following code:
from PIL import Image, ImageFont, ImageDraw
background = Image.new('RGB', (strip_width, strip_height)) #creating the black strip
draw = ImageDraw.Draw(background)
font = ImageFont.truetype("/usr/share/fonts/truetype/freefont/FreeSansBold.ttf", 16)
text_width, text_height = draw.textsize("Foooo Barrrr!")
position = ((strip_width-text_width)/2,(strip_height-text_height)/2)
draw.text(position,"Foooo Barrrr!",(255,255,255),font=font)
offset = (0,base_image_height/2)
base_image.paste(background,offset)
Notice how I'm setting position.
Now with all said and done, the result looks like so:
The text isn't precisely middled. It's slightly to the right and down. How do I improve my algorithm?
Remember to pass your font to draw.textsize as a second parameter (and also make sure you are really using the same text and font arguments to draw.textsize and draw.text).
Here's what worked for me:
from PIL import Image, ImageFont, ImageDraw
def center_text(img, font, text, color=(255, 255, 255)):
draw = ImageDraw.Draw(img)
text_width, text_height = draw.textsize(text, font)
position = ((strip_width-text_width)/2,(strip_height-text_height)/2)
draw.text(position, text, color, font=font)
return img
Usage:
strip_width, strip_height = 300, 50
text = "Foooo Barrrr!!"
background = Image.new('RGB', (strip_width, strip_height)) #creating the black strip
font = ImageFont.truetype("times", 24)
center_text(background, font, "Foooo Barrrr!")
Result:

python pil draw text offset

I tried to draw a character on an image with Python PIL. With the function ImageDraw.Draw.text(), the xy parameter points to the left-top corner of text. However I set xy to (0,0), the character haven't been draw the the left-top of images.
from PIL import ImageFont, ImageDraw, Image
imageSize=(40,40)
mage = Image.new("RGB", imageSize, (0,0,0))
draw = ImageDraw.Draw(image)
txt = "J"
font = ImageFont.truetype("ANTQUAB.ttf",35)
draw.text((0,0), txt, font=font)
why?
The xy parameter of draw.text() is the top left corner of the text (http://pillow.readthedocs.io/en/3.1.x/reference/ImageDraw.html), however the font might have some padding around the text, especially vertically. What I did was set the y part of the tuple to a negative number (maybe somewhere around -5?) and it worked for me.
This code:
from PIL import ImageFont, ImageDraw, Image
imageSize=(100,100)
image = Image.new("RGB", imageSize, (0,0,0))
draw = ImageDraw.Draw(image)
txt = "J"
font = ImageFont.truetype("ARIAL.ttf",35)
draw.text((0,0), txt, font=font)
image.show()
generates this:
Is this not what you were expecting?
Looks like there are font-specific offsets; you can get the vertical offset from the "top" value returned from FreeTypeFont.getbbox(). Subtracting this offset from your y-coordinate on the draw.text call will align top of text with top of image.
from PIL import ImageFont, ImageDraw, Image
text = 'J'
font = "arial.ttf"
fontsize = 12
img_w = 100
img_h = 100
canvas = Image.new('RGBA', size=(img_w,img_h), color='white')
draw = ImageDraw.Draw(canvas)
y_text = 0
font_obj = ImageFont.truetype(font, fontsize)
(left, top, right, bottom) = font_obj.getbbox(text)
x_pos = 0
y_pos = 0
# offset the y coordinate in the draw call by the "top" parameter fromgetbbox; this is the font-specific text padding
draw.text(xy=(x_pos, y_pos-top),
text=text,
font=font_obj,
align='left',
fill='black')
canvas.show()

A watermark inside a rectangle which filled the certain color

I want to add a watermark at a picture. But just a text, but a rectangle filled with the black color and a white text inside it.
For now, I only can put a text:
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
img = Image.open("in.jpg")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("/usr/share/fonts/truetype/ubuntu-font-family/Ubuntu-C.ttf", 66)
#font = ImageFont.truetype("Arialbd.ttf", 66)
draw.text((width - 510, height-100),"copyright",(209,239,8), font=font)
img.save('out.jpg')
This will draw the text on a black rectangular background:
from PIL import Image, ImageFont, ImageDraw
img = Image.open("in.jpg")
width, height = img.width, img.height
draw = ImageDraw.Draw(img)
font = ImageFont.truetype(
"/usr/share/fonts/truetype/ubuntu-font-family/Ubuntu-C.ttf", 66)
x, y = (width - 510, height-100)
# x, y = 10, 10
text = "copyright"
w, h = font.getsize(text)
draw.rectangle((x, y, x + w, y + h), fill='black')
draw.text((x, y), text, fill=(209, 239, 8), font=font)
img.save('out.jpg')
Using imagemagick, a better looking watermark could be made with
from PIL import Image, ImageFont, ImageDraw
font = ImageFont.truetype(
"/usr/share/fonts/truetype/ubuntu-font-family/Ubuntu-C.ttf", 66)
text = "copyright"
size = font.getsize(text)
img = Image.new('RGBA', size=size, color=(0, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.text((0, 0), text, fill=(209, 239, 8), font=font)
img.save('label.jpg')
and then calling (through subprocess if you wish) something like
composite -dissolve 25% -gravity south label.jpg in.jpg out.jpg
or if you make label with a white background,
composite -compose bumpmap -gravity southeast label.jpg in.jpg out.jpg
To run these commands from within the Python script, you could use subprocess like this:
import subprocess
import shlex
from PIL import Image, ImageFont, ImageDraw
font = ImageFont.truetype(
"/usr/share/fonts/truetype/ubuntu-font-family/Ubuntu-C.ttf", 66)
text = "copyright"
size = font.getsize(text)
img = Image.new('RGBA', size=size, color='white')
draw = ImageDraw.Draw(img)
draw.text((0, 0), text, fill=(209, 239, 8), font=font)
img.save('label.jpg')
cmd = 'composite -compose bumpmap -gravity southeast label.jpg in.jpg out.jpg'
proc = subprocess.Popen(shlex.split(cmd))
proc.communicate()

Categories

Resources