I\'m using python/PIL to write text on a set of PNG
images. I was able to get the font I wanted, but I\'d like to now outline the text in black.
This is how I handled the problem when I needed to do it for frame counters. Just a heads up if you start to push this too far for the thickness, then you will need more draws to cover your areas you're missing.
from PIL import Image,ImageDraw,ImageFont
import os
#setting varibles
imgFile = "frame_0.jpg"
output = "frame_edit_0.jpg"
font = ImageFont.truetype("arial.ttf", 30)
text = "SEQ_00100_SHOT_0004_FR_0001"
textColor = 'white'
shadowColor = 'black'
outlineAmount = 3
#open image
img = Image.open(imgFile)
draw = ImageDraw.Draw(img)
#get the size of the image
imgWidth,imgHeight = img.size
#get text size
txtWidth, txtHeight = draw.textsize(text, font=font)
#get location to place text
x = imgWidth - txtWidth - 100
y = imgHeight - txtHeight - 100
#create outline text
for adj in range(outlineAmount):
#move right
draw.text((x-adj, y), text, font=font, fill=shadowColor)
#move left
draw.text((x+adj, y), text, font=font, fill=shadowColor)
#move up
draw.text((x, y+adj), text, font=font, fill=shadowColor)
#move down
draw.text((x, y-adj), text, font=font, fill=shadowColor)
#diagnal left up
draw.text((x-adj, y+adj), text, font=font, fill=shadowColor)
#diagnal right up
draw.text((x+adj, y+adj), text, font=font, fill=shadowColor)
#diagnal left down
draw.text((x-adj, y-adj), text, font=font, fill=shadowColor)
#diagnal right down
draw.text((x+adj, y-adj), text, font=font, fill=shadowColor)
#create normal text on image
draw.text((x,y), text, font=font, fill=textColor)
img.save(output)
print 'Finished'
os.startfile(output)
Check the stroke_width option, which implements stroke/border/outline
effect. For shadow
effect, you can draw with a small offset. Bellow is an example to add subtitle to image.
#!/usr/bin/env python
from PIL import Image, ImageDraw, ImageFont
def add_subtitle(
bg,
text="nice",
xy=("center", 20),
font="font/SourceHanSansSC-Regular.otf",
font_size=53,
font_color=(255, 255, 255),
stroke=2,
stroke_color=(0, 0, 0),
shadow=(4, 4),
shadow_color=(0, 0, 0),
):
"""draw subtitle on image by pillow
Args:
bg(PIL image): image to add subtitle
text(str): subtitle
xy(tuple): absolute top left location of subtitle
...: extra style of subtitle
Returns:
bg(PIL image): image with subtitle
"""
stroke_width = stroke
xy = list(xy)
W, H = bg.width, bg.height
font = ImageFont.truetype(str(font), font_size)
w, h = font.getsize(text, stroke_width=stroke_width)
if xy[0] == "center":
xy[0] = (W - w) // 2
if xy[1] == "center":
xy[1] = (H - h) // 2
draw = ImageDraw.Draw(bg)
if shadow:
draw.text(
(xy[0] + shadow[0], xy[1] + shadow[1]), text, font=font, fill=shadow_color
)
draw.text(
(xy[0], xy[1]),
text,
font=font,
fill=font_color,
stroke_width=stroke_width,
stroke_fill=stroke_color,
)
return bg
if __name__ == "__main__":
bg = Image.open("./Screenshot_20200626-131218.png")
bg = add_subtitle(bg)
bg.save("out.png")
I was looking for that on the internet and I saw that PIL doesn't have native support to add text border. In that case, I create this proposal method based on Alec Bennett solution.
The problem that I found with that solution is about how to create a smooth border on larger border size. Problem Example
If we walk over 360 degrees or 2pi radians, adding the same text, we could create a better implementation of the text border functionality.
Here's the example function
def add_text(image, text, location, font, fontsize=14, fontcolor=(0, 0, 0),
border=0, border_color=(0, 0, 0), points=15):
font_format = ImageFont.truetype(font, fontsize)
drawer = ImageDraw.Draw(image)
if border:
(x, y) = location
for step in range(0, math.floor(border * points), 1):
angle = step * 2 * math.pi / math.floor(border * points)
drawer.text((x - border * math.cos(angle), y - border * math.sin(angle)), text, border_color, font=font_format)
drawer.text(location, text, fontcolor, font=font_format)
return image
The same image, text and config generate the next Final Result
You can take a look at this Text Outline Using PIL:
import Image, ImageFont, ImageDraw
import win32api, os
x, y = 10, 10
fname1 = "c:/test.jpg"
im = Image.open(fname1)
pointsize = 30
fillcolor = "red"
shadowcolor = "yellow"
text = "hi there"
font = win32api.GetWindowsDirectory() + "\\Fonts\\ARIALBD.TTF"
draw = ImageDraw.Draw(im)
font = ImageFont.truetype(font, pointsize)
# thin border
draw.text((x-1, y), text, font=font, fill=shadowcolor)
draw.text((x+1, y), text, font=font, fill=shadowcolor)
draw.text((x, y-1), text, font=font, fill=shadowcolor)
draw.text((x, y+1), text, font=font, fill=shadowcolor)
# thicker border
draw.text((x-1, y-1), text, font=font, fill=shadowcolor)
draw.text((x+1, y-1), text, font=font, fill=shadowcolor)
draw.text((x-1, y+1), text, font=font, fill=shadowcolor)
draw.text((x+1, y+1), text, font=font, fill=shadowcolor)
# now draw the text over it
draw.text((x, y), text, font=font, fill=fillcolor)
fname2 = "c:/test2.jpg"
im.save(fname2)
os.startfile(fname2)
import Image, ImageFont, ImageDraw
import win32api, os
x, y = 10, 10
fname1 = "c:/test.jpg"
im = Image.open(fname1)
pointsize = 30
fillcolor = "red"
shadowcolor = "yellow"
text = "hi there"
font = win32api.GetWindowsDirectory() + "\\Fonts\\ARIALBD.TTF"
draw = ImageDraw.Draw(im)
font = ImageFont.truetype(font, pointsize)
# thin border
draw.text((x-1, y), text, font=font, fill=shadowcolor)
draw.text((x+1, y), text, font=font, fill=shadowcolor)
draw.text((x, y-1), text, font=font, fill=shadowcolor)
draw.text((x, y+1), text, font=font, fill=shadowcolor)
# thicker border
draw.text((x-1, y-1), text, font=font, fill=shadowcolor)
draw.text((x+1, y-1), text, font=font, fill=shadowcolor)
draw.text((x-1, y+1), text, font=font, fill=shadowcolor)
draw.text((x+1, y+1), text, font=font, fill=shadowcolor)
# now draw the text over it
draw.text((x, y), text, font=font, fill=fillcolor)
fname2 = "c:/test2.jpg"
im.save(fname2)
os.startfile(fname2)