使用 PIL 时如何居中对齐(和中间垂直对齐)文本?
弃用警告:textsize 已弃用,将在 Pillow 10 (2023-07-01) 中删除。请改用 textbbox 或 textlength。
代码使用
textbbox
而不是 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")
结果
Draw.textsize
方法计算文字大小并相应地重新计算位置。
举个例子:
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")
结果:
如果你的字体大小不同,包括这样的字体:
myFont = ImageFont.truetype("my-font.ttf", 16)
draw.textsize(msg, font=myFont)
这里是一些示例代码,它使用 textwrap 将长行拆分为几段,然后使用
textsize
方法计算位置。
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')
需要注意的是
Draw.textsize
方法是不准确的。我正在处理低像素图像,经过一些测试,结果证明 textsize
认为每个字符都是 6 像素宽,而 I
需要最大值。 2 个像素和一个 W
需要最少。 8 像素(在我的例子中)。因此,根据我的文字,它是否居中。虽然,我猜“6”是一个平均值,所以如果你正在处理长文本和大图像,它应该还可以。
但是现在,如果你想要一些真正的准确性,你最好使用你将要使用的字体对象的
getsize
方法:
arial = ImageFont.truetype("arial.ttf", 9)
w,h = arial.getsize(msg)
draw.text(((W-w)/2,(H-h)/2), msg, font=arial, fill="black")
在 Edilio 的链接中使用。
ImageDraw.text 的PIL 文档是一个很好的起点,但不要回答您的问题。
下面是一个示例,说明如何将文本置于任意边界框的中心,而不是图像的中心。边界框定义为:
(x1, y1)
= 左上角和 (x2, y2)
= 右下角。
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')
无论您有单行消息还是多行消息都不再重要,因为 PIL 包含了
align='center'
参数。但是,它仅适用于多行文本。如果消息是单行,则需要手动居中。如果消息是多行的,align='center'
会在后续行中为您完成工作,但您仍然需要手动将文本块居中。这两种情况都在上面的代码中立即解决了。
所有其他答案都没有考虑 text ascender。
这是
ImageDraw.text(..., anchor="mm")
的反向移植。不确定它是否与anchor="mm"
完全兼容,因为我还没有测试过其他kwargs
像spacing
,stroke_width
。但我向你保证这个 offset 修复对我有用。
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,
)
结合使用
anchor="mm"
和align="center"
会产生奇迹。例子
draw.text(
xy=(width / 2, height / 2),
text="centered",
fill="#000000",
font=font,
anchor="mm",
align="center"
)
注意:经过测试,其中
font
是这样构造的 ImageFont
类对象:
ImageFont.truetype('path/to/font.ttf', 32)
这是一个在图像中心添加文本的简单示例
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()
如果您使用的是默认字体,那么您可以使用这个简单的计算
draw.text((newimage.width/2-len(text)*3, 5), text,fill="black", align ="center",anchor="mm")
主要是 你必须将图像宽度除以 2 然后得到你想要的字符串的长度并将它乘以 3 然后从除法结果中减去它
newimage.width/2-len(text)*3 #this is X position
**此答案是对默认字体大小的估计,如果您使用自定义字体,则必须相应地更改乘数。默认情况下是 3
您可以使用以下算法:
假设主图有白色背景
创建一个空图像(textImg)并在图像的左上角(或任何您想要的地方)绘制文本。
修剪 textImg 中的任何空白
最后,使用渲染文本的尺寸将 textImg 粘贴到主图像上,这等于 textImg 的宽度和高度。
from PIL import Image, ImageFont, ImageDraw
text = "© Lorem Ipsum"
# this is main image we want to draw centered text
mainImg = Image.new(mode='RGB', size=(600, 600), color='white')
# this is image text that will hold trimmed text, create image with any size and draw text in it
textImg = Image.new(mode='RGB', size=(200, 200), color='white')
draw = ImageDraw.Draw(textImg)
font = ImageFont.load_default() # ImageFont.truetype("your_font.ttf", 12)
draw.text((1, 1), text, fill='black', font=font)
# now trim white space from text image
pixels = textImg.load()
xmin, ymin, xmax, ymax = textImg.width, textImg.height, 0, 0
for x in range(textImg.width):
for y in range(textImg.height):
if pixels[x, y] != (255, 255, 255):
xmin, ymin = min(x, xmin), min(y, ymin)
xmax, ymax = max(x, xmax), max(y, ymax)
textImg = textImg.crop((xmin, ymin, xmax+1, ymax+1))
# paste trimmed text image into main image and save
x, y = mainImg.width//2 - textImg.width//2, mainImg.height//2 - textImg.height//2
mainImg.paste(textImg, (x, y, x + textImg.width, y + textImg.height))
mainImg.save('mainImg.png')