Cannot align text with a line drawn on an image - python

I'm trying to do some image manipulation with the python library Pillow (fork of PIL) and am coming across a weird problem. For some reason, when I try to draw a line and draw some text at the same y coordinate, they're not matching up. The text is a bit below the line, yet I have both graphics starting at the same point. Has anyone had this problem before and/or know how to solve it? Here's the code I'm using:
image = Image.open("../path_to_image/image.jpg")
draw = ImageDraw.Draw(image)
font = ImageFont.truetype("../fonts/Arial Bold.ttf", 180)
draw.line((0,2400, 500,2400), fill="#FFF", width=1)
draw.text((0, 2400), "Test Text", font=font)
image.save(os.path.join(root, "test1.jpg"), "JPEG", quality=100)
return

I get something similar (with sizes 10 times smaller):
This is happening because the (x,y) coordinates given to ImageDraw.text() are the top left corner of the text:
PIL.ImageDraw.Draw.text(xy, text, fill=None, font=None, anchor=None)
Draws the string at the given position.
Parameters:
xy – Top left corner of the text.
text – Text to be drawn.
font – An ImageFont instance.
fill – Color to use for the text.
This is confirmed in the code: the text is turned into a bitmap and then drawn at xy.

For those with a similar problem, I ended up creating a helper function that manually adjusts the font size until font.getsize(text)[1] returns the correctly sized text. Here's a snippet:
def adjust_font_size_to_line_height(font_location, desired_point_size, text):
adjusted_points = 1
while True:
font = ImageFont.truetype(font_location, adjusted_points)
height = font.getsize(text)[1]
if height != desired_point_size:
adjusted_points += 1
else:
break
return adjusted_points

Related

How can I draw a curved text using python? Converting text to curved image?

I want to write a python program that converts given text into an image. There are tutorials for it online, but I want to curve the text over a circle.
Let's say I have the text "YOUR CURVED TEXT".
I want to draw this on an image and end up with something like following:
I checked Pillow and OpenCV. I might be making a mistake but I don't think they have any functionality for curving the given text?
What would be the best way to do this?
Thanks in advance.
You can do something like that in ImageMagick using -distort arc 360.
convert -font Arial -pointsize 20 label:' Your Curved Text Your Curved Text ' -virtual-pixel Background -background white -distort Arc 360 -rotate -90 arc_circle_text.jpg
You can do that also in Python Wand, which uses ImageMagick as follows:
from wand.image import Image
from wand.font import Font
from wand.display import display
with Image() as img:
img.background_color = 'white'
img.font = Font('Arial', 20)
img.read(filename='label: Your Curved Text Your Curved Text ')
img.virtual_pixel = 'white'
# 360 degree arc, rotated -90 degrees
img.distort('arc', (360,-90))
img.save(filename='arc_text.png')
img.format = 'png'
display(img)
Thanks to Eric McConville for helping with the label: code.
thanks to fmw42 for the answer above. I slightly modified this to allow for a transparent background and to do negative angles as well. My first submission to give back to this wonderful community :)
import wand
def curved_text_to_image(
text: str,
font_filepath: str,
font_size: int,
color: str, #assumes hex string
curve_degree: int):
"""
Uses ImageMagik / wand - so have to ensure its installed.
"""
with wand.image.Image(width=1, height=1, resolution=(600, 600)) as img: # open an image
with wand.drawing.Drawing() as draw: # open a drawing objetc
# assign font details
draw.font = font_filepath
draw.font_size = font_size
draw.fill_color = wand.color.Color(color)
# get size of text
metrics = draw.get_font_metrics(img, text)
height, width = int(metrics.text_height), int(metrics.text_width)
# resize the image
img.resize(width=width, height=height)
# draw the text
draw.text(0, height, text)
draw(img)
img.virtual_pixel = 'transparent'
# curve_degree arc, rotated 0 degrees - ie at the top
if curve_degree >= 0:
img.distort('arc', (curve_degree, 0))
else:
# rotate it 180 degrees, then distory and rotate back 180 degrees
img.rotate(180)
img.distort('arc', (abs(curve_degree), 180))
img.format = 'png'
wand.display.display(img)
return img

How to choose multiple text coordinate such that it doesn't overlap with multiple image coordinate in a same background image using python?

I'm trying to copy text and images on a background image using python PIL library. I want to achieve this such that new images and text don't overlap each other or touch on bordeers, they have to be separately placed on the single background image. Image size and text size doesn't concern it can be any but its need to fit inside the background image, minimum one image and text must be there. the script I have written to achieve this as below
img = Image.open('imagefolder').resize((600,200))
b,l = img.size
width=20
height=20
while height<l-10:
hmax=0
width=10
select = random.choice([True,False])
ran_height = random.choice(range(5,55))
if select:
while width<b:
image_folder=random.choice(image_folders)
imagetemp=img.open(image_folder)
size_w,size_h=imagetemp.size
if size_h>hmax:
hmax=size_h
select_word = random.choice([True,False])
d.text([10,30], "hello", helvetica)
d.text([90,60], "python", helvetica)
if select_word:
if (width+size_w)<b-10 and (height+size_h)<l-10 :
(x, y) = (width, height)
image.paste(imagetemp,(x, y))
width = width+size_w+10
else:
height = height+hmax+10
break
else:
width =width+size_w+10
height = height+hmax+10
else:
height = height+ran_height+10
In this code ,
d.text([10,30], "hello", helvetica)
d.text([90,60], "python",helvetica)
prints text 'hello' and 'python' in the coordinate (10,30) and (90,60) respectively. and
image.paste(imagetemp,(x, y))
prints image in the coordinate of (x,y)
I want to pass a coordinate value for text like this such that it doesn't fall under the image coordinates or texts don't overlap with an image . for example
image
If I pass image coordinate value
d.text([x,y], "hello", helvetica)
texts will be printed inside the image this is not what I'm looking for, I want to print outside the image. Let me know if u have any questions. I searched online I didn't find even a single example of doing this, there are many examples for copying the text inside the image using PIL, not outside the image. Any suggestion will be helpful thanks.
**Fixed it by finding the size of a text
draw_txt.textsize(random_text, font=font)**
**Fixed it by finding the size of a text
draw_txt.textsize(random_text, font=font)**

Adding Kurdish text (right to left) to image is returned separated strings

I'm using PIT to put Kurdish(sorani, arabic-like) text on an image, but the text on the image is separated from each other. Does anyone knows how to solve this problem?
font_size = 100
img = Image.open("cute cat.jpg")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("./KGoran.ttf", font_size, encoding="unic")
text = "ڕۆژتان باش ئازیزان"
reversed_text = reversed(text)
fill = (0,0,0)
font = font
string_text = ""
for i in reversed_text:
string_text += i
draw.text((0, 0), string_text, fill, font)
img.save("added text to image.jpg")
PIL can't handle shaping Arabic-like fonts, (unless they fixed it recently). I've seen one library, like this reshaper library, which will combine characters correctly. Other solutions include using pango or pycairo to deal with the font, see this discussion.

PIL how to scale text size in relation to the size of the image

I'm trying to dynamically scale text to be placed on images of varying but known dimensions. The text will be applied as a watermark. Is there any way to scale the text in relation to the image dimensions? I don't require that the text take up the whole surface area, just to be visible enough so its easily identifiable and difficult to remove. I'm using Python Imaging Library version 1.1.7. on Linux.
I would like to be able to set the ratio of the text size to the image dimensions, say like 1/10 the size or something.
I have been looking at the font size attribute to change the size but I have had no luck in creating an algorithm to scale it. I'm wondering if there is a better way.
Any ideas on how I could achieve this?
Thanks
You could just increment the font size until you find a fit. font.getsize() is the function that tells you how large the rendered text is.
from PIL import ImageFont, ImageDraw, Image
image = Image.open('hsvwheel.png')
draw = ImageDraw.Draw(image)
txt = "Hello World"
fontsize = 1 # starting font size
# portion of image width you want text width to be
img_fraction = 0.50
font = ImageFont.truetype("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("arial.ttf", fontsize)
# optionally de-increment to be sure it is less than criteria
fontsize -= 1
font = ImageFont.truetype("arial.ttf", fontsize)
print('final font size',fontsize)
draw.text((10, 25), txt, font=font) # put the text on the image
image.save('hsvwheel_txt.png') # save it
If this is not efficient enough for you, you can implement a root-finding scheme, but I'm guessing that the font.getsize() function is small potatoes compared to the rest of your image editing processes.
I know this is an old question that has already been answered with a solution that I too have used. Thanks, #Paul!
Though with increasing the font size by one for each iteration can be time-consuming (at least for me on my poor little server). So eg. small text (like "Foo") would take around 1 - 2 seconds, depending on the image size.
To solve that I adjusted Pauls code so that it searches for the number somewhat like a binary search.
breakpoint = img_fraction * photo.size[0]
jumpsize = 75
while True:
if font.getsize(text)[0] < breakpoint:
fontsize += jumpsize
else:
jumpsize = jumpsize // 2
fontsize -= jumpsize
font = ImageFont.truetype(font_path, fontsize)
if jumpsize <= 1:
break
Like this, it increases the font size until it's above the breakpoint and from there on out it goes up and down with (cutting the jump size in half with each down) until it has the right size.
With that, I could reduce the steps from around 200+ to about 10 and so from around 1-2 sec to 0.04 to 0.08 sec.
This is a drop-in replacement for Pauls code (for the while statement and the 2 lines after it because you already get the font correct font size in the while)
This was done in a few mins so any improvements are appreciated! I hope this can help some who are looking for a bit more performant friendly solution.
In general when you change the font sizing its not going to be a linear change in size of the font.
Now this often depends on the software, fonts, etc... This example was taken from Typophile and uses LaTex + Computer Modern font. As you can see its not exactly a linear scaling. So if you are having trouble with non-linear font scaling then I'm not sure how to resolve it, but one suggestion maybe is to.
Render the font as closely to the size that you want, then scale that up/down via regular image scaling algorithm...
Just accept that it won't exactly be linear scaling and try to create some sort of table/algorithm that will select the closest point size for the font to match up with the image size.
Despite other answers saying that font size do not scale linearly, in all the examples that I tested they did scale linearly (within 1-2%).
So if you need a simpler and more efficient version that works within a few percent, you can copy/paste the following:
from PIL import ImageFont, ImageDraw, Image
def find_font_size(text, font, image, target_width_ratio):
tested_font_size = 100
tested_font = ImageFont.truetype(font, tested_font_size)
observed_width, observed_height = get_text_size(text, image, tested_font)
estimated_font_size = tested_font_size / (observed_width / image.width) * target_width_ratio
return round(estimated_font_size)
def get_text_size(text, image, font):
im = Image.new('RGB', (image.width, image.height))
draw = ImageDraw.Draw(im)
return draw.textsize(text, font)
The function find_font_size() can then be used like that (full example):
width_ratio = 0.5 # Portion of the image the text width should be (between 0 and 1)
font_family = "arial.ttf"
text = "Hello World"
image = Image.open('image.jpg')
editable_image = ImageDraw.Draw(image)
font_size = find_font_size(text, font_family, image, width_ratio)
font = ImageFont.truetype(font_family, font_size)
print(f"Font size found = {font_size} - Target ratio = {width_ratio} - Measured ratio = {get_text_size(text, image, font)[0] / image.width}")
editable_image.text((10, 10), text, font=font)
image.save('output.png')
Which for a 225x225 image would print:
>> Font size found = 22 - Target ratio = 0.5 - Measured ratio = 0.502
I tested find_font_size() with various fonts and picture sizes, and it worked in all cases.
If you want to know how this function works, basically tested_font_size is used to find out which ratio will be obtained if we use this specific font size to generate the text. Then, we use a cross-multiplication rule to get the targeted font size.
I tested different values for tested_font_size and found that as long as it's not too small, it does not make any difference.

fonts clipping with PIL

This image was created with PIL. See how the g's and the y's are cut off in this image? How can I prevent this?
http://img109.imageshack.us/img109/8874/screenshotep.png
The code that created this image is pretty straight forward (abbreviated):
import Image, ImageDraw, ImageFont
im = Image.new("RGBA", (200, 200), 'white')
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("VeraSe.ttf", 12)
draw.text(
(1, 1),
" %s: " % "ggjyfFwe__",
font=font,
fill='black'
)
draw.text(
(1, 30),
" %s" % 15,
font=font,
fill='black'
)
im.show()
I tried it with a few different fonts, and it always gets clipped. Surprising;y, googleing "PIL font clipping" returns very few useful hits... I'm using python 2.6.4 and PIL 1.1.6 on Ubuntu 9.10
Here's a late answer for this older question.
The problem appears to be that PIL and Pillow will clip the edges of rendered text. This most often shows on trailing wide characters and decenders (like 'y's). This can also appear on the top of some fonts. This has been a problem for at least ten years. It happens regardless of the size of the image on which text() is called. The conflict appears to choosing the bounding rectangle as "font.size * number_chars" instead of "whatever I actually need to render" and this occurs deep in the stack (_imagingft.c). Fixing this causes other problems, like lining up text rendered letter by letter.
Some solutions include:
Append a space to the end of your string. im.text(xy, my_text + ' ', ...)
For height issues, get the width of your text (font.getsize()), second render the text plus a good ascender and descender, chop the rendered text to the first reported width and the second actual height.
Use a different library such as AggDraw or pyvips.
This is referenced in various questions fonts clipping with PIL, PIL cuts off top of letters, Properly render text with a given font in Python and accurately detect its boundaries. These questions reference the same underlying issue but are not duplicates
I couldn't solve this problem for some fonts using the approaches mentioned so far, so I ended up using aggdraw as a transparent replacement for PIL's text drawig methods.
Your code rewritten to aggdraw would look like:
import Image
import aggdraw
im = Image.new("RGBA", (200, 200), 'white')
draw = aggdraw.Draw(im)
# note that the color is specified in the font constructor in aggdraw
font = aggdraw.Font((0,0,0), "VeraSe.ttf", size=12, opacity=255)
draw.text((1, 1), " %s: " % "ggjyfFwe__", font) # no color here
draw.text((1, 30), " %s" % 15, font)
draw.flush() # don't forget this to update the underlying PIL image!
im.show()
The "bug" still exists in 2012, with Ubuntu 11.10. Fontsize 11, 12, 13 and 15 clip the underscore completely.
#!/usr/bin/env python
""" demonstrates clipping of descenders for certain font sizes """
import Image, ImageDraw, ImageFont
fontPath = "/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans-Bold.ttf"
im = Image.new('L', (256, 256))
ys=15
for i in range(10,21):
fh = ImageFont.truetype(fontPath, i)
sometext="%dgt_}" % (i)
ImageDraw.Draw(im).text((10, ys ),sometext , 254, fh)
ys+=i+5
im.show()
My suggestion is, before you create the image object, to get the required size for the text.
This is done using font.getsize("text") (documentation).
In a image generating script I made, I first found the maximum height of one line of text, by calling the equvalient of font.getsize("Åj") (If you only need US-ASCII, you could find the height of "Aj" instead). Then I calculated the required image height and line offsets, including margins and line-spacing.
Here is an kludge that works well for me. It is a variant on gnud's answer. (Different enough to deserve a separate answer vs. comment I hope.) I have tested a lot of word placements and this has performed consistently.
When a text is drawn without fully reaching the full height of the font, clipping can occur. As gnud noted, by using characters such as "Aj" (I use "Fj") you avoid this bug.
Whenever a word is placed:
1) Do a draw.textsize(text, font=font) with your desired word. Store the height/width.
2) Add ' Fj' (spaceFJ) to the end of the word, and redo the textsize and store tis third height/width.
4) You will do the actual text draw with the word from item 2 (with the ' Fj' at the end). Having this addendum will keep the font from being clipped.
4) Before you do the actual text draw, crop the image where the ' Fj' will land (crop.load() is required to avoid a lazy copy). Then draw the text, and past the cropped image back over the ' Fj'.
This process avoids clipping, seems reasonably performant, and yields the full, unclipped text. Below is a copy/paste of a section of Python code I use for this. Partial example, but hopefully it adds some insight.
# note: xpos & ypos were previous set = coordinates for text draw
# the hard-coded addition of 4 to c_x likely will vary by font
# (I only use one font in this process, so kludged it.)
width, height = draw.textsize(word, font=font)
word2 = word + ' Fj'
width2, height2 = draw.textsize(word2, font=font)
# crop to overwrite ' Fj' with previous image bits
c_w = width2 - width
c_h = height2
c_x = xpos + width + 4
c_y = ypos
box = (c_x, c_y, c_x + c_w, c_y + c_h)
region = img.crop(box)
region.load()
draw.text((xpos, ypos), word2, (0,0,0), font=font)
img.paste(region, box)

Categories

Resources