How can I decrease the letter spacing of this text? I want to make the text more squished together by a few pixels.
I'm trying to make a transparent image, with text on it, that I want pushed together. Like this, but transparent:
from PIL import Image, ImageDraw, ImageFont
(W, H) = (140, 40)
#create transparent image
image = Image.new("RGBA", (140, 40), (0,0,0,0))
#load font
font = ImageFont.truetype("Arial.ttf", 30)
draw = ImageDraw.Draw(image)
text = "kpy7n"
w,h = font.getsize(text)
draw.text(((W-w)/2,(H-h)/2), text, font=font, fill=0)
image.save("transparent-image.png")
This function will automate all the pain for you. It was written to emulate Photoshop values and can render leading (the space between lines) as well as tracking (the space between characters).
def draw_text_psd_style(draw, xy, text, font, tracking=0, leading=None, **kwargs):
"""
usage: draw_text_psd_style(draw, (0, 0), "Test",
tracking=-0.1, leading=32, fill="Blue")
Leading is measured from the baseline of one line of text to the
baseline of the line above it. Baseline is the invisible line on which most
letters—that is, those without descenders—sit. The default auto-leading
option sets the leading at 120% of the type size (for example, 12‑point
leading for 10‑point type).
Tracking is measured in 1/1000 em, a unit of measure that is relative to
the current type size. In a 6 point font, 1 em equals 6 points;
in a 10 point font, 1 em equals 10 points. Tracking
is strictly proportional to the current type size.
"""
def stutter_chunk(lst, size, overlap=0, default=None):
for i in range(0, len(lst), size - overlap):
r = list(lst[i:i + size])
while len(r) < size:
r.append(default)
yield r
x, y = xy
font_size = font.size
lines = text.splitlines()
if leading is None:
leading = font.size * 1.2
for line in lines:
for a, b in stutter_chunk(line, 2, 1, ' '):
w = font.getlength(a + b) - font.getlength(b)
# dprint("[debug] kwargs")
print("[debug] kwargs:{}".format(kwargs))
draw.text((x, y), a, font=font, **kwargs)
x += w + (tracking / 1000) * font_size
y += leading
x = xy[0]
It takes a font and a draw object, which can be obtained via:
font = ImageFont.truetype("Arial.ttf", 30)
draw = ImageDraw.Draw(image)
You have to draw the text character by character and then change the x coordinate when drawing the next
Example of code:
w,h = font.getsize("k")
draw.text(((W,H),"K", font=font, fill=0)
draw.text(((W+w)*0.7,H),"p", font=font, fill=0)
draw.text(((W+w*2)*0.7,H),"y", font=font, fill=0)
draw.text(((W+w*3)*1,H),"7", font=font, fill=0)
draw.text(((W+w*4)*0.8,H),"n", font=font, fill=0)
You can do this by changing the kerning - I am not sure how to do that with PIL at the moment, but it is possible with ImageMagick in the Terminal and with Python using wand which is a Python binding to ImageMagick.
First, in the Terminal - look at the parameter -kerning which is first minus three then plus three:
magick -size 200x80 xc:black -gravity center -font "Arial Bold.ttf" -pointsize 50 -kerning -3 -fill white -draw "text 0,0 'kpy7n'" k-3.png
magick -size 200x80 xc:black -gravity center -font "Arial Bold.ttf" -pointsize 50 -kerning 3 -fill white -draw "text 0,0 'kpy7n'" k+3.png
And, somewhat similarly in Python:
#!/usr/bin/env python3
# Needed this on macOS Monterey:
# export WAND_MAGICK_LIBRARY_SUFFIX="-7.Q16HDRI"
# export MAGICK_HOME=/opt/homebrew
from wand.image import Image
from wand.drawing import Drawing
from wand.font import Font
text = "kpy7n"
# Create a black canvas 400x120
with Image(width=400, height=120, pseudo='xc:black') as image:
with Drawing() as draw:
# Draw once in yellow with positive kerning
draw.font_size = 50
draw.font = 'Arial Bold.ttf'
draw.fill_color = 'yellow'
draw.text_kerning = 3.0
draw.text(10, 80, text)
draw(image)
# Draw again in magenta with negative kerning
draw.fill_color = 'magenta'
draw.text_kerning = -3.0
draw.text(200, 80, text)
draw(image)
image.save(filename='result.png')
Related
I have created a set of images in python utilizing PIL. In addition to this, I've implemented textwrap in order to put text onto the images I've created, however, they're not quite perfect. First, below are three examples of images I've created.
These three images have different widths, but I'd like them all to have the same width, whereas height isn't of concern and can be taller or smaller than each other; the width is the only thing that must remain consistent. In addition to this, I've used utf-8 encoding in order to get this text on the images, but I would like the font to look something more like the following
Also shown in the above image is how those boxes are stacked--That is how I'd like to have my final product. Rather than three separate images of bordered text, I'd like to have one single image containing those bordered boxes of text. Here is my current code for what I've output
for match in find_matches(text=fullText):
ct += 1
match_words = match.split(" ")
match = " ".join(match_words[:-1])
print(match)
W, H = 300, 300
base = Image.new("RGB", (W, H), (255, 255, 255))
draw = ImageDraw.Draw(base)
font = ImageFont.load_default()
current_h, pad = 50, 5
for key in textwrap.wrap(match, width=50):
line = key.encode("ascii")
w, h = draw.textsize(line, font=font)
draw.text(((W - w) / 2, current_h), line, (0, 0, 0), font=font)
current_h += h + pad
draw.text((W / 2, current_h), str(ct).encode("utf-8"), (0, 0, 0), font=font)
for count, matches in enumerate(match):
base.save(f"{ct}C.png")
bbox = ImageOps.invert(base).getbbox()
trim = base.crop(bbox)
patent = ImageOps.expand(trim, border=5, fill=(255, 255, 255))
patent = ImageOps.expand(patent, border=3, fill=(0, 0, 0))
patent.save(f"{ct}C.png")
p_w, p_h = patent.size
Image.open(result_fpath, "r")
result.paste(patent)
result.save(result_fpath)
Finally, this has to be an automated process. What I was thinking that could be done for the stacked boxes into a single image would be a for-loop that takes in the created images and then pastes them into an image of the same size as the first pasted image which resizes appropriately for each subsequent bordered box of text. I'd appreciate any help on this greatly.
I find this sort of thing much easier with ImageMagick, for which there are decent bindings available with wand.
Here's how you can do one image, just at the command-line in Terminal, showing the various parts in different colours so you can see what affects what:
magick -background yellow -gravity center -pointsize 24 -size 400x caption:"Detecting, by the component, that a replacement component has been added in the transport\n246C" -bordercolor magenta -border 10 -bordercolor cyan -border 5 result.png
And here's how you can do a few in one go:
magick -background white -gravity center -pointsize 24 -size 400x -bordercolor black \
\( caption:"Detecting, by the component, that a replacement component has been added in the transport\n246C" -bordercolor black -border 5 -bordercolor white -border 5 \) \
\( caption:"Detecting, by the component, that another component has been removed\n246D" -bordercolor black -border 5 -bordercolor white -border 5 \) \
\( caption:"Detecting, by any means, that another component has been replaced\n247K" -bordercolor black -border 5 -bordercolor white -border 5 \) \
-append result.png
Of course you can change the fonts, change the colours, read the captions from a file, use Unicode, space differently and/or do it all in Python with very similar-looking code - here is a link to an answer showing the approximate technique in wand in Python.
How would I center-align (and middle-vertical-align) text when using PIL?
Deprecation Warning: textsize is deprecated and will be removed in Pillow 10 (2023-07-01). Use textbbox or textlength instead.
Code using textbbox instead of textsize.
from PIL import Image, ImageDraw, ImageFont
def create_image(size, bgColor, message, font, fontColor):
W, H = size
image = Image.new('RGB', size, bgColor)
draw = ImageDraw.Draw(image)
_, _, w, h = draw.textbbox((0, 0), message, font=font)
draw.text(((W-w)/2, (H-h)/2), message, font=font, fill=fontColor)
return image
myFont = ImageFont.truetype('Roboto-Regular.ttf', 16)
myMessage = 'Hello World'
myImage = create_image((300, 200), 'yellow', myMessage, myFont, 'black')
myImage.save('hello_world.png', "PNG")
Result
Use Draw.textsize method to calculate text size and re-calculate position accordingly.
Here is an example:
from PIL import Image, ImageDraw
W, H = (300,200)
msg = "hello"
im = Image.new("RGBA",(W,H),"yellow")
draw = ImageDraw.Draw(im)
w, h = draw.textsize(msg)
draw.text(((W-w)/2,(H-h)/2), msg, fill="black")
im.save("hello.png", "PNG")
and the result:
If your fontsize is different, include the font like this:
myFont = ImageFont.truetype("my-font.ttf", 16)
draw.textsize(msg, font=myFont)
Here is some example code which uses textwrap to split a long line into pieces, and then uses the textsize method to compute the positions.
from PIL import Image, ImageDraw, ImageFont
import textwrap
astr = '''The rain in Spain falls mainly on the plains.'''
para = textwrap.wrap(astr, width=15)
MAX_W, MAX_H = 200, 200
im = Image.new('RGB', (MAX_W, MAX_H), (0, 0, 0, 0))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype(
'/usr/share/fonts/truetype/msttcorefonts/Arial.ttf', 18)
current_h, pad = 50, 10
for line in para:
w, h = draw.textsize(line, font=font)
draw.text(((MAX_W - w) / 2, current_h), line, font=font)
current_h += h + pad
im.save('test.png')
One shall note that the Draw.textsize method is inaccurate. I was working with low pixels images, and after some testing, it turned out that textsize considers every character to be 6 pixel wide, whereas an I takes max. 2 pixels and a W takes min. 8 pixels (in my case). And so, depending on my text, it was or wasn't centered at all. Though, I guess "6" was an average, so if you're working with long texts and big images, it should still be ok.
But now, if you want some real accuracy, you better use the getsize method of the font object you're going to use:
arial = ImageFont.truetype("arial.ttf", 9)
w,h = arial.getsize(msg)
draw.text(((W-w)/2,(H-h)/2), msg, font=arial, fill="black")
As used in Edilio's link.
A simple solution if you're using PIL 8.0.0 or above: text anchors
width, height = # image width and height
draw = ImageDraw.draw(my_image)
draw.text((width/2, height/2), "my text", font=my_font, anchor="mm")
mm means to use the middle of the text as anchor, both horizontally and vertically.
See the anchors page for other kinds of anchoring. For example if you only want to center horizontally you may want to use ma.
The PIL docs for ImageDraw.text are a good place to start, but don't answer your question.
Below is an example of how to center the text in an arbitrary bounding box, as opposed to the center of an image. The bounding box is defined as: (x1, y1) = upper left corner and (x2, y2) = lower right corner.
from PIL import Image, ImageDraw, ImageFont
# Create blank rectangle to write on
image = Image.new('RGB', (300, 300), (63, 63, 63, 0))
draw = ImageDraw.Draw(image)
message = 'Stuck in\nthe middle\nwith you'
bounding_box = [20, 30, 110, 160]
x1, y1, x2, y2 = bounding_box # For easy reading
font = ImageFont.truetype('Consolas.ttf', size=12)
# Calculate the width and height of the text to be drawn, given font size
w, h = draw.textsize(message, font=font)
# Calculate the mid points and offset by the upper left corner of the bounding box
x = (x2 - x1 - w)/2 + x1
y = (y2 - y1 - h)/2 + y1
# Write the text to the image, where (x,y) is the top left corner of the text
draw.text((x, y), message, align='center', font=font)
# Draw the bounding box to show that this works
draw.rectangle([x1, y1, x2, y2])
image.show()
image.save('text_center_multiline.png')
The output shows the text centered vertically and horizontally in the bounding box.
Whether you have a single or multiline message no longer matters, as PIL incorporated the align='center' parameter. However, it is for multiline text only. If the message is a single line, it needs to be manually centered. If the message is multiline, align='center' does the work for you on subsequent lines, but you still have to manually center the text block. Both of these cases are solved at once in the code above.
Use the textsize method (see docs) to figure out the dimensions of your text object before actually drawing it. Then draw it starting at the appropriate coordinates.
All the other answers did NOT take text ascender into consideration.
Here's a backport of ImageDraw.text(..., anchor="mm"). Not sure if it's fully compatible with anchor="mm", cause I haven't tested the other kwargs like spacing, stroke_width yet. But I ensure you this offset fix works for me.
from PIL import ImageDraw
from PIL import __version__ as pil_ver
PILLOW_VERSION = tuple([int(_) for _ in pil_ver.split(".")[:3]])
def draw_anchor_mm_text(
im,
xy,
# args shared by ImageDraw.textsize() and .text()
text,
font=None,
spacing=4,
direction=None,
features=None,
language=None,
stroke_width=0,
# ImageDraw.text() exclusive args
**kwargs,
):
"""
Draw center middle-aligned text. Basically a backport of
ImageDraw.text(..., anchor="mm").
:param PIL.Image.Image im:
:param tuple xy: center of text
:param unicode text:
...
"""
draw = ImageDraw.Draw(im)
# Text anchor is firstly implemented in Pillow 8.0.0.
if PILLOW_VERSION >= (8, 0, 0):
kwargs.update(anchor="mm")
else:
kwargs.pop("anchor", None) # let it defaults to "la"
if font is None:
font = draw.getfont()
# anchor="mm" middle-middle coord xy -> "left-ascender" coord x'y'
# offset_y = ascender - top, https://stackoverflow.com/a/46220683/5101148
# WARN: ImageDraw.textsize() return text size with offset considered.
w, h = draw.textsize(
text,
font=font,
spacing=spacing,
direction=direction,
features=features,
language=language,
stroke_width=stroke_width,
)
offset = font.getoffset(text)
w, h = w - offset[0], h - offset[1]
xy = (xy[0] - w / 2 - offset[0], xy[1] - h / 2 - offset[1])
draw.text(
xy,
text,
font=font,
spacing=spacing,
direction=direction,
features=features,
language=language,
stroke_width=stroke_width,
**kwargs,
)
Refs
https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html
https://github.com/python-pillow/Pillow/issues/4789
https://stackoverflow.com/a/46220683/5101148
https://github.com/python-pillow/Pillow/issues/2486
Using a combination of anchor="mm" and align="center" works wonders. Example
draw.text(
xy=(width / 2, height / 2),
text="centered",
fill="#000000",
font=font,
anchor="mm",
align="center"
)
Note: Tested where font is an ImageFont class object constructed as such:
ImageFont.truetype('path/to/font.ttf', 32)
This is a simple example to add a text in the center of the image
from PIL import Image, ImageDraw, ImageFilter
msg = "hello"
img = Image.open('image.jpg')
W, H = img.size
box_image = img.filter(ImageFilter.BoxBlur(4))
draw = ImageDraw.Draw(box_image)
w, h = draw.textsize(msg)
draw.text(((W - w) / 2, (H - h) / 2), msg, fill="black")
box_image.show()
if you are using the default font then you can use this simple calculation
draw.text((newimage.width/2-len(text)*3, 5), text,fill="black", align ="center",anchor="mm")
the main thing is
you have to divide the image width by 2 then get the length of the string you want and multiply it by 3 and subtract it from the division result
newimage.width/2-len(text)*3 #this is X position
**this answer is an estimation for the default font size used if you use a custom font then the multiplier must be changed accordingly. in the default case it is 3
I have tested the calculations and the math is correct (and takes into account the height and width of the font), but after Python creates the image and I put it into Photoshop, the vertical and horizontal centering of the text is not correct. Should I be doing something else with my code?
from PIL import Image, ImageDraw, ImageFont
# base = Image.open("Images/Phones/KK17018_Navy_KH10089.jpg").convert("RGBA")
base = Image.open('Images/Tablets/KK17076_Hunter_KH235.jpg').convert('RGB')
# Create blank rectangle to write on
draw = ImageDraw.Draw(base)
message = 'GGS'
num1, num2 = base.size
bounding_box = [0, 0, num1, num2]
x1, y1, x2, y2 = bounding_box # For easy reading
# font = ImageFont.truetype('Fonts/Circle/Circle Monograms Three White.ttf', size=413)
font = ImageFont.truetype('Fonts/Modern/Vhiena_Monoline.otf', size=800)
font2 = ImageFont.truetype('Fonts/Modern/Vhiena_Base.otf', size=800)
font3 = ImageFont.truetype('Fonts/Modern/Vhiena_Extrude A.otf', size=800)
# Calculate the width and height of the text to be drawn, given font size
w, h = draw.textsize(message, font=font3)
# Calculate the mid points and offset by the upper left corner of the bounding box
x = (x2 - x1 - w) / 2 + x1
y = (y2 - y1 - h) / 2 + y1
# Write the text to the image, where (x,y) is the top left corner of the text
draw.text((x, y), message, align='center', font=font3, fill='orange')
draw.text((x, y), message, align='center', font=font2, fill='black')
draw.text((x, y), message, align='center', font=font, fill='white')
# Draw the bounding box to show that this works
# draw.rectangle([x1, y1, x2, y2])
base.show()
base.save('test_image.jpg')
The text's upper left x,y coordinates should be (874,1399.5), but in Photoshop, they show as (875,1586). The Python code above does calculate (874,1399.5) correctly, but something is placing the font lower than it should be.
Also, I'm stacking fonts like this because it gives a regular font, a shadow font and a font that makes it look beveled in the middle of the font. Would there be a better method or is stacking fonts an ok practice?
EDIT: Upon further testing, something is adding a 22% top margin to the font as the font size increases. I could account for this, but this seems rather odd.
I don't have your exact font handy to test, but this is probably because Pillow's textsize and text methods, by default, anchor to the ascender height instead of the actual rendered top; see this note in the docs. Try using textbbox instead of textsize, and specifying the top anchor in both that and the text method, and see if that behaves more intuitively.
Note that you could probably just anchor the text to 'mm', middle/middle, to center it based on the coordinates of the image's midpoint. This anchors vertically to halfway between the ascenders and descenders, though, so it may not actually look centered, depending on the font and what glyphs you render.
I want to create a simple Python script that let's me create an image file and place a word dead-center on that canvas. However, while I can get the word to align horizontally, I just can't get it to the center vertically. I am on MacOS btw. I have tried this so far:
import os
from PIL import Image, ImageDraw, ImageFont
font_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'fonts')
font = ImageFont.truetype(os.path.join(font_path, "Verdana.ttf"), 80)
W, H = (1200,200)
msg = "hola"
im = Image.new("RGB",(W,H),(255,255,255))
draw = ImageDraw.Draw(im)
w, h = draw.textsize(msg, font)
draw.text(((W-w)/2,(H-h)/2), msg, font=font, fill="black")
im.save("output_script.png", "PNG")
I already considered the font in the textsize calculation. But the word still appears roughly 5-10% below the middle. Any ideas?
textsize seems to return the size of the actual text, not that of the line. So it's smaller for ooo than for XXX!
I think you should just use 80 — the size you gave PIL — for the height, although I can't guarantee that it's correct.
ImageFont.getmask(txt) returns the alpha mask bitmap with which text can be centered in the image.
import Image, ImageFont
img = Image.new('L', (32, 32), color=0)
img_w, img_h = img.size
font = ImageFont.truetype('/path/to/font.ttf', 16)
mask = font.getmask('hola') # your text here
mask_w, mask_h = mask.size
d = Image.core.draw(img.im, 0)
d.draw_bitmap(((img_w - mask_w)/2, (img_h - mask_h)/2), mask, 255) # last arg is pixel intensity of text
img.save('test.png')
I want to get 26 files (for starters): A.ico, B.ico, ... Z.ico, where they are composed of 16x16 256-color image, and a 32x32 256-color image, where the color of the text is black, and the font is ... say Calibri, and the size - whatever fits best into the square. I would like to do this using Python Image Library if possible.
I know that I can probably get my icons through other means, but I would like to learn to use the PIL better, and would like to use it for the task at hand.
Start with a large blank image and draw the character on the center of it. Find the edges of the character and extract a square from the image that includes all of the character. Use the thumbnail function with the ANTIALIAS option to reduce it to the 16x16 or 32x32 size required. Then reduce the number of colors to 256: How to reduce color palette with PIL
This is based on the answer by #Mark Ransom. Thank you, Mark!
This worked for me, though the 'blackify' function is imperfect still.
I still need to figure out how to create an .ico file without using icotool for Linux.
# This script generates icon files from the two images.
# Uses Python 2.6.5, uses the Python Imaging Library
import Image
import ImageDraw
import ImageFont
letters = [chr(i + ord('A')) for i in range(26)]
default_huge = ImageFont.load_default()
large_size = 1000
lim = large_size + 1
# Apparently I can use the same size for the font.
calibri_huge = ImageFont.truetype("calibri.ttf", large_size)
def crop_letter(img):
minx, maxx, miny, maxy = lim, -lim, lim, -lim
for x in range(large_size):
for y in range(large_size):
if sum(img.getpixel((x, y))) == 3 * 255: continue
# Else, found a black pixel
minx = min(minx, x)
maxx = max(maxx, x)
miny = min(miny, y)
maxy = max(maxy, y)
return img.crop(box = (minx, miny, maxx, maxy))
# This works for me 95% of the time
def blackify(color):
return 255 * (color > 240)
for letter in letters:
# A bit wasteful, but I have plenty of RAM.
img = Image.new("RGB", (large_size, large_size), "white")
draw = ImageDraw.Draw(img)
draw.text((0,0), letter, font = calibri_huge, fill = "black")
img32 = crop_letter(img)
img16 = img32.copy()
img32.thumbnail((32, 32), Image.ANTIALIAS)
img16.thumbnail((16, 16), Image.ANTIALIAS)
img32 = Image.eval(img32, blackify)
img16 = Image.eval(img16, blackify)
## Not needed
## # Apparently this is all it takes to get 256 colors.
## img32 = img32.convert('P')
## img16 = img16.convert('P')
img32.save('icons3/{0}32x32.bmp'.format(letter))
img16.save('icons3/{0}16x16.bmp'.format(letter))
# break
print('DONE!')