Хобрук: Ваш путь к мастерству в программировании

Разноцветный текст с PIL

Я создаю веб-приложение, которое обслуживает динамическое изображение с текстом. Каждая нарисованная строка может быть нескольких цветов.

До сих пор я создал метод синтаксического анализа и метод рендеринга. Метод parse просто берет строку и анализирует из нее цвета в следующем формате: «§aЭто зеленый§rэто белый» (Да, это 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

И это работает, но символы, которые должны быть ниже основной линии шрифта, такие как g, y и q, отображаются на базовой линии, поэтому это выглядит странно. Вот пример:

Любые идеи о том, как я могу заставить их отображаться правильно? Или мне надо сделать свою таблицу смещений, куда я их вручную положу?


  • Это не дубликат this . Насколько я мог понять, у автора этого вопроса были проблемы с синтаксической ошибкой. 06.10.2013
  • Боюсь, вам придется делать смещения вручную; PIL просто не предоставляет метрики шрифта, необходимые для этого автоматически. 06.10.2013
  • Я надеюсь, что это не так, я подожду и увижу. 06.10.2013

Ответы:


1

Учитывая, что вы не можете получить смещения из PIL, вы можете сделать это, нарезая изображения, поскольку PIL соответствующим образом объединяет несколько символов. Здесь у меня есть два подхода, но я думаю, что первый из представленных лучше, хотя оба они состоят всего из нескольких строк. Первый подход дает такой результат (это также увеличение мелкого шрифта, поэтому он пикселизирован):

введите здесь описание изображения

Чтобы объяснить идею здесь, скажем, я хочу букву «j», и вместо того, чтобы просто создать изображение только «j», я создаю изображение «o j», так как это будет поддерживать правильное выравнивание «j». Затем я вырезаю часть, которая мне не нужна, и просто сохраняю «j» (используя textsize как для «o», так и для «o j»).

import Image, ImageDraw
from random import randint
make_color = lambda : (randint(50, 255), randint(50, 255), randint(50,255))

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

fill = " o "
x = 0
w_fill, y = draw.textsize(fill)
x_draw, x_paste = 0, 0
for c in "The quick brown fox jumps over the lazy dog.":
    w_full = draw.textsize(fill+c)[0]
    w = w_full - w_fill     # the width of the character on its own
    draw.text((x_draw,0), fill+c, make_color())
    iletter = image.crop((x_draw+w_fill, 0, x_draw+w_full, y))
    image2.paste(iletter, (x_paste, 0))
    x_draw += w_full
    x_paste += w
image2.show()

Кстати, я использую «о», а не просто «о», так как соседние буквы, кажется, немного портят друг друга.

Второй способ — сделать изображение всего алфавита, разрезать его на части, а затем снова склеить. Это проще, чем кажется. Вот пример, и как создание словаря, так и объединение изображений — это всего несколько строк кода:

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, вы можете легко определить yoffset каждого символа в приведенном выше словаре ichar (например, взять максимум вдоль каждой горизонтальной строки, а затем найти максимум и минимум в ненулевых индексах).

06.10.2013
  • Это сработало отлично! Единственная проблема, по крайней мере для моего шрифта, заключалась в том, что буква «о» не была самой старшей после символа, поэтому я заменил ее на «О» в верхнем регистре. 07.10.2013
  • @totokaka: Интересно. С шрифтом по умолчанию, который я использовал, textsize возвращал одинаковую высоту для всех символов. Похоже, что это не относится к вашему шрифту? В этой ситуации также может быть полезно использовать комбинацию символов для заливки, которая максимально простирается вверх и вниз, например `Oj`. 08.10.2013
  • Новые материалы

    Введение в контекст React
    В этом посте мы поговорим о Context API, который был представлен в React 16, и о том, как вы можете их использовать. Что такое контекст? Глядя на определение из react docs , оно..

    Шлюз с лицензией OSS, совместимый с Apollo Federation v2, появится в WunderGraph
    Сегодня мы рады сообщить, что мы сотрудничаем с поддерживаемой YC Tailor Technologies, Inc. для внедрения Apollo Federation v2. Реализация будет лицензирована MIT (Engine) и Apache 2.0..

    Это оно
    Ну, я официально уволился с работы! На этой неделе я буду лихорадочно выполнять последние требования Думающего , чтобы я мог сосредоточиться на поиске работы. Что именно это значит?..

    7 полезных библиотек JavaScript, которые вы должны использовать в своем следующем проекте
    Усильте свою разработку JavaScript Есть поговорка «Не нужно изобретать велосипед». Библиотеки — лучший тому пример. Это поможет вам написать сложные и трудоемкие функции простым способом...

    Базовое руководство по переносу концепций обучения в глубокое обучение
    Обзор По мере того, как машинное обучение становится все более мощным и продвинутым, модели, обеспечивающие эту расширенную возможность, становятся все больше и начинают требовать огромного..

    C в C.R.U.D с использованием React-Redux
    Если вы использовали React, возможно, вы знакомы с головной болью, связанной с обратным потоком данных. Передача состояния реквизитам от родительских компонентов к дочерним компонентам может..

    5 обязательных элементов современного инструмента конвейера данных
    В цифровом мире предприятия используют конвейеры данных для перемещения, преобразования и хранения огромных объемов данных. Эти конвейеры составляют основу бизнес-аналитики и играют..