Python 带PIL的多色文本

Python 带PIL的多色文本,python,fonts,python-imaging-library,Python,Fonts,Python Imaging Library,我正在创建一个web应用程序,它提供动态图像和文本。 绘制的每个字符串可以有多种颜色 到目前为止,我已经创建了一个parse方法和一个render方法。解析方法只需要获取字符串,并解析其中的颜色,它们的格式如下:“§aThis是绿色的§rthis是白色的”(是的,它是Minecraft)。 这就是我的字体模块的外观: # Imports from pillow from PIL import Image, ImageDraw, ImageFont # Load the fonts font_r

我正在创建一个web应用程序,它提供动态图像和文本。 绘制的每个字符串可以有多种颜色

到目前为止,我已经创建了一个parse方法和一个render方法。解析方法只需要获取字符串,并解析其中的颜色,它们的格式如下:“§aThis是绿色的§rthis是白色的”(是的,它是Minecraft)。 这就是我的字体模块的外观:

# Imports from pillow
from PIL import Image, ImageDraw, ImageFont

# Load the fonts
font_regular = ImageFont.truetype("static/font/regular.ttf", 24)
font_bold = ImageFont.truetype("static/font/bold.ttf", 24)
font_italics = ImageFont.truetype("static/font/italics.ttf", 24)
font_bold_italics = ImageFont.truetype("static/font/bold-italics.ttf", 24)

max_height = 21 # 9, from FONT_HEIGHT in FontRederer in MC source, multiplied by
                # 3, because each virtual pixel in the font is 3 real pixels
                # This number is also returned by:
                # font_regular.getsize("ABCDEFGHIJKLMNOPQRSTUVWXYZ")[1] 

# Create the color codes
colorCodes = [0] * 32 # Empty array, 32 slots
# This is ported from the original MC java source:
for i in range(0, 32):
    j = int((i >> 3 & 1) * 85)
    k = int((i >> 2 & 1) * 170 + j)
    l = int((i >> 1 & 1) * 170 + j)
    i1 = int((i >> 0 & 1) * 170 + j)
    if i == 6:
        k += 85
    if i >= 16:
        k = int(k/4)
        l = int(l/4)
        i1 = int(i1/4)
    colorCodes[i] = (k & 255) << 16 | (l & 255) << 8 | i1 & 255

def _get_colour(c):
    ''' Get the RGB-tuple for the color
    Color can be a string, one of the chars in: 0123456789abcdef
    or an int in range 0 to 15, including 15
    '''
    if type(c) == str:
        if c == 'r':
            c = int('f', 16)
        else:
            c = int(c, 16)
    c = colorCodes[c]
    return ( c >> 16 , c >> 8 & 255 , c & 255 )

def _get_shadow(c):
    ''' Get the shadow RGB-tuple for the color
    Color can be a string, one of the chars in: 0123456789abcdefr
    or an int in range 0 to 15, including 15
    '''
    if type(c) == str:
        if c == 'r':
            c = int('f', 16)
        else:
            c = int(c, 16)
    return _get_colour(c+16)

def _get_font(bold, italics):
    font = font_regular
    if bold and italics:
        font = font_bold_italics
    elif bold:
        font = font_bold
    elif italics:
        font = font_italics
    return font

def parse(message):
    ''' Parse the message in a format readable by render
    this will return a touple like this:
    [((int,int),str,str)]
    so if you where to send it directly to the rederer you have to do this:
    render(pos, parse(message), drawer)
    '''
    result = []
    lastColour = 'r'
    total_width = 0
    bold = False
    italics = False
    for i in range(0,len(message)):
        if message[i] == '§':
            continue
        elif message[i-1] == '§':
            if message[i] in "01234567890abcdef":
                lastColour = message[i]
            if message[i] == 'l':
                bold = True
            if message[i] == 'o':
                italics = True
            if message[i] == 'r':
                bold = False
                italics = False
                lastColour = message[i]  
            continue
        width, height = _get_font(bold, italics).getsize(message[i])
        total_width += width
        result.append(((width, height), lastColour, bold, italics, message[i]))
    return result

def get_width(message):
    ''' Calculate the width of the message
    The message has to be in the format returned by the parse function
    '''
    return sum([i[0][0] for i in message])


def render(pos, message, drawer):
    ''' Render the message to the drawer
    The message has to be in the format returned by the parse function
    '''
    x = pos[0]
    y = pos[1]
    for i in message:
        (width, height), colour, bold, italics, char = i
        font = _get_font(bold, italics)
        drawer.text((x+3, y+3+(max_height-height)), char, fill=_get_shadow(colour), font=font)
        drawer.text((x, y+(max_height-height)), char, fill=_get_colour(colour), font=font)
        x += width
#从枕头导入
从PIL导入图像、ImageDraw、ImageFont
#加载字体
font\u regular=ImageFont.truetype(“static/font/regular.ttf”,24)
font\u bold=ImageFont.truetype(“static/font/bold.ttf”,24)
font\u italics=ImageFont.truetype(“static/font/italics.ttf”,24)
font\u bold\u italics=ImageFont.truetype(“static/font/bold italics.ttf”,24)
最大字体高度=21#9,从MC源中FontRederer中的字体高度乘以
#3,因为字体中的每个虚拟像素是3个真实像素
#此号码也由以下人员返回:
#font_regular.getsize(“ABCDEFGHIJKLMNOPQRSTUVWXYZ”)[1]
#创建颜色代码
颜色代码=[0]*32#空数组,32个插槽
#这是从原始MC java源代码移植的:
对于范围(0,32)内的i:
j=int((i>>3和1)*85)
k=int((i>>2&1)*170+j)
l=int((i>>1和1)*170+j)
i1=int((i>>0&1)*170+j)
如果i==6:
k+=85
如果i>=16:
k=int(k/4)
l=int(l/4)
i1=int(i1/4)
颜色代码[i]=(k&255)16,c>>8&255,c&255)
def_get_shadow(c):
''获取颜色的阴影RGB元组
颜色可以是字符串,即:0123456789abcdefr中的字符之一
或介于0到15之间的整数,包括15
'''
如果类型(c)==str:
如果c=='r':
c=int('f',16)
其他:
c=int(c,16)
返回颜色(c+16)
def_get_字体(粗体、斜体):
font=font\U常规
如果为粗体和斜体:
字体=字体\u粗体\u斜体
elif粗体:
font=font\u粗体
elif斜体:
font=font\u斜体
返回字体
def解析(消息):
''以render可读的格式解析消息
这将返回如下图:
[((int,int),str,str)]
因此,如果您要将其直接发送到重新发货人,您必须执行以下操作:
呈现(pos、解析(消息)、抽屉)
'''
结果=[]
lastColour='r'
总宽度=0
粗体=假
斜体=假
对于范围(0,len(消息))中的i:
如果消息[i]='§':
持续
elif消息[i-1]=='§':
如果“01234567890abcdef”中的消息[i]:
lastColour=消息[i]
如果消息[i]=“l”:
粗体=真
如果消息[i]=“o”:
斜体=真
如果消息[i]=“r”:
粗体=假
斜体=假
lastColour=消息[i]
持续
宽度,高度=\u获取字体(粗体,斜体)。获取大小(消息[i])
总宽度+=宽度
结果。追加(((宽度、高度)、LastColor、粗体、斜体、消息[i]))
返回结果
def get_宽度(消息):
''计算消息的宽度
消息必须采用解析函数返回的格式
'''
返回和([i[0][0]表示消息中的i])
def渲染(位置、消息、抽屉):
''把信息交给抽屉
消息必须采用解析函数返回的格式
'''
x=位置[0]
y=位置[1]
对于我的留言:
(宽度、高度)、颜色、粗体、斜体、字符=i
字体=\u获取\u字体(粗体、斜体)
drawer.text((x+3,y+3+(最大高度)),char,fill=\u get\u shadow(颜色),font=font)
抽屉。文本((x,y+(最大高度)),字符,填充=\u获取颜色(颜色),字体=字体)
x+=宽度
它确实可以工作,但是应该在字体基线下的字符,如g、y和q,都是在基线上渲染的,所以看起来很奇怪,下面是一个例子:


关于如何使它们正确显示,有什么想法吗?或者我必须制作我自己的偏移表,在那里我手动放置它们吗?

鉴于无法从PIL获得偏移量,您可以通过切片图像来实现这一点,因为PIL适当地组合了多个字符。这里我有两种方法,但我认为第一种方法更好,尽管两种方法都只是几行。第一种方法给出了这个结果(它也是对小字体的放大,这就是它被像素化的原因):

为了解释这个想法,假设我想要字母“j”,而不是仅仅制作一个“j”的图像,我制作了一个“oj”的图像,因为这样可以保持“j”正确对齐。然后我裁剪我不想要的部分,只保留“j”(在“o”和“oj”上使用
textsize

顺便说一句,我使用“o”,而不仅仅是“o”,因为相邻的字母似乎互相有点腐蚀

第二种方法是制作整个字母表的图像,将其切片,然后将其重新粘贴在一起。这比听起来容易。下面是一个示例,构建字典和连接到图像中都只需几行代码:

import Image, ImageDraw
import string

A = " ".join(string.printable)

image = Image.new("RGB", (1200,20), (0,0,0))
draw = ImageDraw.Draw(image)

# make a dictionary of character images
xcuts = [draw.textsize(A[:i+1])[0] for i in range(len(A))]
xcuts = [0]+xcuts
ycut = draw.textsize(A)[1]
draw.text((0,0), A, (255,255,255))
# ichars is like {"a":(width,image), "b":(width,image), ...}
ichars = dict([(A[i], (xcuts[i+1]-xcuts[i]+1, image.crop((xcuts[i]-1, 0, xcuts[i+1], ycut)))) for i in range(len(xcuts)-1)])

# Test it...
image2 = Image.new("RGB", (400,20), (0,0,0))
x = 0
for c in "This is just a nifty text string":
    w, char_image = ichars[c]
    image2.paste(char_image, (x, 0))
    x += w
下面是结果字符串的(放大)图像:

下面是整个字母表的图像:

这里的一个技巧是,我必须在原始字母表图像中的每个字符之间留出一个空格,否则我会让相邻的字符相互影响

我想如果你需要在有限的字体和字符范围内这样做,最好预先计算一个字母图像字典

或者,对于另一种方法,使用诸如numpy之类的工具,您可以轻松确定上面
ichar
字典中每个字符的yoffset(例如,沿每个水平行取最大值,然后在非零索引上找到最大值和最小值)。

这不是重复的。据我所知,那个问题的海报只是有语法错误的问题。我担心你会有
import Image, ImageDraw
import string

A = " ".join(string.printable)

image = Image.new("RGB", (1200,20), (0,0,0))
draw = ImageDraw.Draw(image)

# make a dictionary of character images
xcuts = [draw.textsize(A[:i+1])[0] for i in range(len(A))]
xcuts = [0]+xcuts
ycut = draw.textsize(A)[1]
draw.text((0,0), A, (255,255,255))
# ichars is like {"a":(width,image), "b":(width,image), ...}
ichars = dict([(A[i], (xcuts[i+1]-xcuts[i]+1, image.crop((xcuts[i]-1, 0, xcuts[i+1], ycut)))) for i in range(len(xcuts)-1)])

# Test it...
image2 = Image.new("RGB", (400,20), (0,0,0))
x = 0
for c in "This is just a nifty text string":
    w, char_image = ichars[c]
    image2.paste(char_image, (x, 0))
    x += w