Right-align text with PIL? - python

I've an Arabic text and want to move the text to the upper right corner in the image.
I did try using align='right' in the code also tried direction=rtl but it shows an empty area of the image on the right top corner.
If you want to try the code make sure to Install libraqm & raqm
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import textwrap
#configuration
font_size=36
width=3840
height=390
back_ground_color=(255,255,255)
font_size=80
font_color=(0,0,0)
text = " وأيضا هذا النصح محبوب جدا جدا لذلك انا أفضلههذا النص مكتوب باللغةمكتوب باللغةمكتوب باللغةمكتوب باللغةمكتوب باللغة مكتوب باللغة العربية وهو واضح بشكل جميل"
wrapped = textwrap.fill(text, 100)
im = Image.new ( "RGB", (width,height), back_ground_color )
draw = ImageDraw.Draw ( im )
unicode_font = ImageFont.truetype("Sahel.ttf", font_size)
draw.text ( (0,0), wrapped, font=unicode_font, fill=font_color,spacing=30,direction='rtl',align='right',features='rtla')
im.save('text.png')

Summary
The align parameter can be used for the smaller lines (usually last one) in order to align them according to the text box
In order to right align the text box according to the image you need to use the draw.textsize function to get the text box size and calculate its position according to your image size.
Working example with some comments
from PIL import Image, ImageDraw, ImageFont
import textwrap
# configuration
font_size = 36
width = 3840
height = 390
back_ground_color = (255, 255, 255)
font_size = 80
font_color = (0, 0, 0)
text = " وأيضا هذا النصح محبوب جدا جدا لذلك انا أفضلههذا النص مكتوب باللغةمكتوب باللغةمكتوب باللغةمكتوب باللغةمكتوب باللغة مكتوب باللغة العربية وهو واضح بشكل جميل"
wrapped = textwrap.fill(text, 100)
im = Image.new("RGB", (width, height), back_ground_color)
draw = ImageDraw.Draw(im)
unicode_font = ImageFont.truetype("Sahel Regular.ttf", font_size)
# get text box size
text_w, text_h = draw.textsize(wrapped, font=unicode_font, direction="rtl", language="ar")
draw.text(
# (0, 0), # top left corner
# (width - text_w, 0), # top right corner
# (width - text_w, height - text_h), # bottom right corner
# (0, height - text_h), # bottom left corner
# ((width - text_w) // 2, (height - text_h) // 2), # center
(width - text_w, (height - text_h) // 2), # center vertical + start from right
wrapped,
font=unicode_font,
fill=font_color,
direction='rtl',
language='ar',
align='left', # affect the second line of the text, set to right to see the last line aligned to the right
)
im.show()
Result

Related

How to crop and paste an image into an ellipse in pillow

I have a drawn ellipse on an image, I want to crop and place into this ellipse an image thumbnail. Here is how the ellipse is placed in the image:
What I tried to use is paste but that will just cover the ellipse as shown here:
Here is my pillow code so far:
from PIL import ImageFont, ImageDraw, Image
from IPython.display import Image as jpImg
import textwrap
# Start new Image and draw it
image = Image.new('RGB', (1200, 630), color = 'white')
draw = ImageDraw.Draw(image)
# Text content to go into image
txt = "This is the title of this entry This is the title of this entry"
list_sections = ['Some section text will go here', 'Some section text will go here 1', 'Some section text will go here 2','Some section text will go here 3', 'Some section text will go here 4']
category_list = ['category 1', 'category 2', 'category 3', 'category 4']
# Failed attempt to draw a right angle triangle to divide the image rectangle
#draw.polygon([(1200,630), (200, 200), (150,50)], fill = 'yellow')
# Ending up faking it with a [line]
#draw.line((0, 0) + image.size, fill=128)
draw.line((10,960,1700, 75), fill='red', width=600)
# portion of image width you want text width to be
# matching fontsize to width
img_fraction = 0.50
fontsize = 1 # starting font size
font = ImageFont.truetype("/Library/Fonts/Arial.ttf", fontsize)
while font.getsize(txt)[0] < img_fraction*image.size[0]:
# iterate until the text size is just larger than the criteria
fontsize += 1
font = ImageFont.truetype("/Library/Fonts/Arial.ttf", fontsize)
# optionally de-increment to be sure it is less than criteria
fontsize -= 1
font = ImageFont.truetype("/Library/Fonts/Arial.ttf", fontsize)
# Font for title text
font_title = ImageFont.truetype("/Library/Fonts/Arial.ttf", 50)
# Draw sections from list
top = 100
for section in list_sections:
draw.text((35, top), section, font=font, fill="red")
top = top + 40
continue
# Draw Category from list
top = image.size[1] /2
side = image.size[0]-400
for cat in category_list:
draw.text((side, top), cat, font=font, fill="white")
top = top + 40
continue
# Draw parenthesis to surround category list
font_title_parent_0 = ImageFont.truetype("/Library/Fonts/Arial.ttf", 350)
draw.text((side - 100, top-300), '{', font=font_title_parent_0, fill=(255, 255, 255, 128))
draw.text((side + 240, top-300), '}', font=font_title_parent_0, fill="white")
# Draw Ellipse for photo
draw.ellipse((300-20,300-20,30+500,30+500), fill='white',outline="red", width=25)
# Paste an image on the eclipse
# offset = ((bg_w - img_w) // 2, (bg_h - img_h) // 2)
offset = ((300-20) // 2, (30+500) // 2)
avatar = Image.open('data/images/avatar.jpeg', 'r')
#image.paste(avatar, offset)
draw.text((10, 15), textwrap.shorten(txt, width=55, placeholder='..'), font=font_title, fill="red") # put the text on the image
image.save('data/images/final_card.png') # save it
jpImg(filename='data/images/final_card.png')
Is it possible to crop the avatar thumbnail and place it within the ellipse? Thanks.
yes, one way is to load the avatar and mask it then plop it in, as such:
image = Image.new('RGB', (1200, 630), color = 'white')
draw = ImageDraw.Draw(image)
# draw the border
avatar_size = (200, 200)
x_offset, y_offset = 300, 300
border_bounding = [x_offset, y_offset, x_offset+avatar_size[0], y_offset+avatar_size[1]]
draw.ellipse(border_bounding, fill="red")
# make it into a mask, but scale it down slightly so we have a border
border_size = 25
mask = Image.new('L', [ x-border_size for x in avatar_size ], 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.ellipse([0,0,*mask.size], fill=255)
# crop the avatar into the smaller circle
avatar = Image.open('jake.jfif').convert('RGB')
output = ImageOps.fit(avatar, mask.size, centering=(0.5, 0.5))
output.putalpha(mask)
# now drop that in the center of where our cicle is
image.paste(output, (x_offset+(border_size//2), y_offset+(border_size//2)), output)
display(image)
And you get:

How to better crop and paste an image in PIL

I am trying to crop an avatar and place it in a given location in another image using python pil.
Here is the output of what I have so far:
And here is the code:
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
import textwrap
text = "Creating Twitter Cards dynamically with Python"
background_image = "data/obi-pvc.png" # this is the background
avatar = Image.open("data/avatar.png")
font = "data/fonts/AllertaStencil-Regular.ttf"
background = Image.open(background_image)
background_width, background_height = background.size
avatar.convert('RGBA')
## DO NOT change below this line!!
save_name = f"{text.lower().replace(' ', '_')}.png"
#textwrapped = textwrap.wrap(text, width=text_wrap_width)
# crop avatar
width, height = avatar.size
x = (width - height)//2
avatar_cropped = avatar.crop((x, 0, x+height, height))
width_cr, height_cr = avatar_cropped.size
# create grayscale image with white circle (255) on black background (0)
mask = Image.new('L', avatar_cropped.size)
mask_draw = ImageDraw.Draw(mask)
width, height = avatar_cropped.size
mask_draw.ellipse((0, 0, width, height), fill=255)
# add mask as alpha channel
avatar_cropped.putalpha(mask)
draw = ImageDraw.Draw(background)
font = ImageFont.truetype(font, font_size)
draw.text((offset,margin), '\n'.join(textwrapped), font=font, fill=color)
x, y = avatar_cropped.size
margin = 40
# left top
position_tl = (0 + margin, 0 + margin)
position_tr = (x - margin - width_cr, 0 + margin)
position_bl = (0 + margin, y - margin - height_cr)
position_br = (x - margin - width_cr, y - margin - height_cr)
background.paste(avatar_cropped, position)
background.save(f"data/output/{save_name}")
display(background)
The avatar should fit within the circle. I can't seem to really figure out how to apply the positioning. Thanks
Here is the avatar:

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 to move the circular image to the exact center position of the bigger image?

I want to move the circular image to the exact center position of the bigger image. How to do accomplish that task accurately?
from IPython.display import display
import numpy as np
from PIL import Image, ImageDraw, ImageFilter
def show_saved_image(str):
img = Image.open(str)
display(img)
im1 = Image.open('rocket.jpg')
im2 = Image.open('lena.jpg')
#height, width, channels = im1.shape
im1_width, im1_height = im1.size
im1_centreX, im1_centreY = int(im1_width/2), int(im1_height/2)
im2_width, im2_height = im2.size
im2_centreX, im2_centreY = int(im2_width/2), int(im2_height/2)
print(im1_width, im1_height)
print(im2_width, im2_height)
radius = int(min(im2_width/2, im2_height/2))
ulX, ulY = im2_centreX-radius, im2_centreY-radius
lrX, lrY = im2_centreX+radius, im2_centreY+radius
desired_pointX, desired_pointY = im1_centreX-radius, im1_centreY-radius
# ![rocket_pillow_paste_out](data/dst/rocket_pillow_paste_out.jpg)
mask_im = Image.new("L", im2.size, 0)
draw = ImageDraw.Draw(mask_im)
draw.ellipse((ulX, ulY, lrX, lrY), fill=255)
# mask_im_blur = mask_im.filter(ImageFilter.GaussianBlur(10))
# mask_im_blur.save('mask_circle_blur.jpg', quality=95)
back_im = im1.copy()
back_im.paste(im2, (desired_pointX, desired_pointY), mask_im)
#back_im.paste(im2, (desired_pointX, desired_pointY), mask_im_blur)
back_im.save('output.jpg', quality=95)
im = Image.open('output.jpg')
draw = ImageDraw.Draw(im)
draw.ellipse((im1_centreX-4, im1_centreY-4, im1_centreX+4, im1_centreY+4 ), fill=(0, 255, 0), outline=(0, 0, 0))
draw.ellipse((desired_pointX-4, desired_pointY-4, desired_pointX+4, desired_pointY+4 ), fill=(255, 0, 0), outline=(0, 0, 0))
im.save('imagedraw.jpg', quality=95)
show_saved_image("imagedraw.jpg")
Images:
rocket.jpg
lena.jpg
If there is another way, then please help me with that, too.
You just need to modify this line
desired_pointX, desired_pointY = im1_centreX - radius, im1_centreY - radius
to
desired_pointX, desired_pointY = im1_centreX - int(im2_width/2), im1_centreY - int(im2_height/2)
Your mask_im has shape im2.size, so you need to adapt to that, not just the radius of the circle. Since radius is int(im2_height/2), the vertical alignment is fine, but radius is smaller than int(im2_width/2), that's why the insufficient shift leftwards.

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

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

Categories