如何使用PIL居中/中间对齐文本?

117

当使用PIL时,如何使文本居中对齐(并且垂直居中对齐)?

11个回答

218

弃用警告: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")

结果

Result


使用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)

7
draw.textsize: (self, text, font=None) - WeizhongTu
39
如果您的字体大小不同,那么重要的是像这样包括您的字体:draw.textsize(msg, font=myFont),否则它将无法正确居中。 - Coco
1
如何确保文本不会溢出图像? - Trect
1
如何将多行文本居中对齐? - Nagabhushan S N
3
以上代码有误,应该是(W-w/2,H-h/2),而不是((W-w)/2,(H-h)/2)。尝试了上述方法并发现问题所在。 - Ijas
以任意点(x,y)为中心,应该是(x-w/2,y-h/2)。 - xjcl

88

下面是一个使用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')

在此输入图像描述


2
Textwrap很好,但请记住,您为width设置的整数断点是计算字符数,而PIL图像则以像素为单位测量。我使用了上述版本,但在编写行的for循环之前添加了一个while循环。首先,我设置了一个任意高的字符计数来开始,然后使用textwrap来断开行,并使用.textsize来测量文本包装结果列表中第一个输出的像素宽度。如果适合,则继续,否则将我的字符计数减少并再次测量,直到行适合图像。 - Chase
太好了!非常感谢,这不仅解决了对齐问题,还解决了行高问题。我花了两天时间寻找这个问题的解决方案,在这里找到了(变量pad)。 - LaLa

39

请注意,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的链接中使用。


1
不是对楼主问题的答案,但这是一个非常需要的功能。1+ - ddlab
重要提示:此函数“getsize”接受非拉丁字符,如€或德语Umlauts。“textsize”则不接受。拇指向上 :-) - ddlab
这对我非常有效,即使上面的两个答案没有产生想要的结果。谢谢! - PiNaKa30
虽然 getsize 可以接受非拉丁字符,但在某些情况下(特别是使用非 ASCII 字符时),其返回值(尤其是高度)定义不清。对于文本对齐,应使用 getlength 函数(从 Pillow 8.0.0 开始)获取宽度和字体大小获取高度。更好的选择是使用文本锚点,让 Pillow 为您处理居中对齐。 - Nulano

32
如果你使用的是PIL 8.0.0或更高版本,则一个简单的解决方案是:文本锚点。请参见:PIL 8.0.0版本说明文本锚点手册
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 的意思是使用文本的中心点作为锚点,水平和垂直方向都居中。

查看锚点页面了解其他类型的锚定方式。例如,如果您只想水平居中,可以使用ma


8
这是最新版本Pillow的一个更好、更灵活和易于使用的答案。 - chasmani

12

PIL ImageDraw.text 文档 是一个不错的起点,但不能解答您的问题。

下面是一个示例,展示了如何将文本居中在任意边界框中,而不是图像中心。边界框的定义为:(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' 会在后续行上为您处理工作,但您仍需手动居中文本块。以上代码同时解决了这两种情况。


2

使用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)

我找到了相同的解决方案,它非常有效 :) - Christian Weiss

2
使用 textsize 方法(详见 文档),在实际绘制文本之前计算出文本对象的尺寸,然后在适当的坐标位置开始绘制。

2

其他答案没有考虑到文本上伸部分(text ascender)。

这里是ImageDraw.text(..., anchor="mm")的回溯版本。不确定它是否完全兼容anchor="mm",因为我还没有测试其他的kwargs,比如spacingstroke_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://dev59.com/klgQ5IYBdhLWcg3wPxrc#46220683
        # 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


1

您可以使用以下算法:

  1. 假设主图像具有白色背景。

  2. 创建一个空图像(textImg),并在图像的左上角(或任何您希望的位置)绘制文本。

  3. 从textImg中修剪任何空格。

  4. 最后,使用呈现文本的宽度和高度(等于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')

0
如果您正在使用默认字体,则可以使用此简单计算。
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。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接