fonts clipping with PIL - python

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)

Related

How to kern text in PIL

I'm working on a problem that requires good precision pixel by pixel, so I need to have the ability to manipulate text in more ways than what PIL provides. Especially with regard to kerning. There is a feature that allows you to disable kerning, but not control the amount.
This problem was made more challenging because of bugs in PIL that relate to accurately measuring the size of text. There are many posts about this problem but the most useful information is a SO post here, How to get the font pixel height using PIL's ImageFont class? and a blog article, How to properly calculate text size in PIL images
My code is for my own use but if someone is having similar issues, I'm sure that it can easily be adapted for your own needs.
My key functions are:
def get_text_width(text_string, font):
return font.getmask(text_string).getbbox()[2]
def kern(name, draw_object, y, space, font, fill):
chars = [char for char in name]
total_width = 0
for char in chars:
width_text = get_text_width(char, font)
total_width += (width_text + int(space))
__, height_text = draw_object.textsize(name, font)
__, offset_y = font.getoffset(name)
height_text += offset_y
width_adjuster = 0
for char in chars:
width_text = get_text_width(char, font)
top_left_x = (473 / 2 - total_width / 2) + width_adjuster
top_left_y = (40 / 2 - height_text / 2) + y
xy = top_left_x, top_left_y
width_adjuster += width_text + int(space)
print(f"char:{char},width_text:{width_text},xy:{xy},width_adjuster:{width_adjuster}")
draw_object.text(xy, char, font=font, fill=fill)
It gives a nice output below. However, it's not entirely precise. The number of pixels between letters will vary slightly with different fonts. I have not found a way to standardize this, so I've just accepted the fact when I enter a value for kerning into my GUI, it is just a scalar relative to the font, not the number of pixels

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.

Cannot align text with a line drawn on an image

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

Mirror Image but wrong size

I am trying to input an image (image1) and flip it horizontally and then save to a file (image2). This works but not the way I want it to
currently this code gives me a flipped image but it just shows the bottom right quarter of the image, so it is the wrong size. Am I overwriting something somewhere? I just want the code to flip the image horizontally and show the whole picture flipped. Where did I go wrong?
and I cannot just use a mirror function or reverse function, I need to write an algorithm
I get the correct window size but the incorrect image size
def Flip(image1, image2):
img = graphics.Image(graphics.Point(0, 0), image1)
X, Y = img.getWidth(), img.getHeight()
for y in range(Y):
for x in range(X):
r, g, b = img.getPixel(x,y)
color = graphics.color_rgb(r, g, b)
img.setPixel(X-x, y, color)
win = graphics.GraphWin(img, img.getWidth(), img.getHeight())
img.draw(win)
img.save(image2)
I think your problem is in this line:
win = graphics.GraphWin(img, img.getWidth(), img.getHeight())
The first argument to the GraphWin constructor is supposed to be the title, but you are instead giving it an Image object. It makes me believe that maybe the width and height you are supplying are then being ignored. The default width and height for GraphWin is 200 x 200, so depending on the size of your image, that may be why only part of it is being drawn.
Try something like this:
win = graphics.GraphWin("Flipping an Image", img.getWidth(), img.getHeight())
Another problem is that your anchor point for the image is wrong. According to the docs, the anchor point is where the center of the image will be rendered (thus at 0,0 you are only seeing the bottom right quadrant of the picture). Here is a possible solution if you don't know what the size of the image is at the time of creation:
img = graphics.Image(graphics.Point(0, 0), image1)
img.move(img.getWidth() / 2, img.getHeight() / 2)
You are editing your source image. It would be
better to create an image copy and set those pixels instead:
create a new image for editing:
img_new = img
Assign the pixel values to that:
img_new.setPixel(X-x, y, color)
And draw that instead:
win = graphics.GraphWin(img_new, img_new.getWidth(), img_new.getHeight())
img_new.draw(win)
img_new.save(image2)
This will also check that your ranges are correct. if they are not, you will see both flipped and unflipped portions in the final image, showing which portions are outside of your ranges.
If you're not opposed to using an external library, I'd recommend the Python Imaging Library. In particular, the ImageOps module has a mirror function that should do exactly what you want.

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.

Categories

Resources