Работа с Pillow

Приступаем к самому интересному: сейчас будем загружать изображение, рисовать поверх него текст и сохранять полученный мем в отдельный файл.

План действий

Создание мема будет состоять из следующих этапов:

  1. Загрузить изображение и создать слой для рисования на нём;
  2. Рассчитать размер шрифта для каждой надписи, чтобы не получилось так, что надпись вылезла за пределы изображения;
  3. Если x is None, то есть надпись нужно выровнять по центру, то рассчитать соответствующую стартовую координату;
  4. Поместить все надписи на слой для рисования и сохранить полученное изображение в новый файл.

Внешние зависимости

Для реализации замысла нам понадобится не только Pillow, но и несколько пакетов из стандартной библиотеки. Итоговый блок импорта будет выглядеть так:

import uuid
from dataclasses import dataclass
from typing import Optional

from PIL import Image, ImageDraw, ImageFont

Главная функция

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

def fill_meme(meme, labels):
    # Проверяем, что пользователь передал
    # столько надписей, сколько должно быть на меме
    slots_count = len(meme.slots)
    labels_count = len(labels)
    if slots_count != labels_count:
        raise ValueError(
            'Неверное количество надписей: '
            f'ожидалось {slots_count}, '
            f'получено {labels_count}'
        )

    # Загружаем изображение
    with Image.open(meme.filename) as img:
        # Создаём слой, на котором можно рисовать текст
        canvas = ImageDraw.Draw(img)

        # И перебираем все пары "слот - надпись"
        for slot, label in zip(meme.slots, labels):
            label = label.upper()
            default_font_size = img.height // 6
            # Это первая вспомогательная функция.
            # Она создаёт шрифт такого размера,
            # чтобы надпись не вылезла за границы изображения
            font = create_font(img, label, 'ariblk.ttf', default_font_size)

            x, y = slot.x, slot.y
            if x is None:
                # Это вторая вспомогательная функция.
                # Она рассчитывает начальную координату таким образом,
                # чтобы надпись в итоге оказалась отцентрированной
                x = calc_left_padding(img, label, font)
            canvas.text((x, y), label, font=font, fill='white')

        # А это третья вспомогательная функция.
        # Она прицепляет к названию исходного файла
        # уникальный идентификатор
        filename = make_unique_filename(meme.filename)
        img.save(filename)

Расчёт размера шрифта

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

А мы пока что реализуем простой алгоритм:

  • создадим надпись с дефолтным шрифтом;
  • рассчитаем размеры прямоугольника, который она занимает;
  • если окажется, что ширина надписи превышает 80% от ширины изображения, то рассчитаем новый размер шрифта.
def create_font(img, label, name, default_size):
    font = ImageFont.truetype(name, default_size)
    # font.getlength(label) - это длина надписи в пикселях.
    # В нашем случае это ширина, т.к. надпись всегда горизонтальна.
    label_width = font.getlength(label)
    ratio = label_width / img.width
    max_ratio = 0.8

    # Если надпись с дефолтным размером шрифта
    # занимает более 80% ширины изображения,
    # то рассчитываем новый размер шрифта
    if ratio > max_ratio:
        new_size = int(default_size / (1 + ratio - max_ratio))
        return ImageFont.truetype(name, new_size)

    # Если надпись занимала менее 80% изображения,
    # то возвращаем шрифт дефолтного размера
    return font

Надпись по центру

Здесь всё предельно просто:

  • рассчитываем свободное пространство (ширина изображения минус ширина надписи);
  • делим пополам;
  • получаем размер отступа, который нужно добавить слева, чтобы надпись в итоге оказалась ровно посередине изображения.
def calc_left_padding(img, label, font):
    label_width = font.getlength(label)
    return int((img.width - label_width) / 2)

Генерируем имя файла

Разделяем имя исходного файла на имя и расширение, вставляем между ними уникальный идентификатор и собираем обратно.

def make_unique_filename(source):
    filename, _, extension = source.rpartition('.')
    return f'{filename}_{uuid.uuid4()}.{extension}'

Проверяем, что получилось

Давайте сделаем то, ради чего всё затевалось: создадим пару мемов.

fill_meme(MEMES['Coding cat'], ['Компилируем HTML', 'В CSS'])
fill_meme(MEMES['Coding cat'], ['2 минуты пишу код', '3 часа отлаживаю'])
fill_meme(MEMES['Hackerman'], ['from PIL import *'])
fill_meme(MEMES['Hackerman'], ['print("Hello, world")'])

Запускайте код, открывайте директорию с исходными изображениями и наслаждайтесь свежесозданными мемасами :)

Coding cat 1

Coding cat 21

Hackerman 1

Hackerman 2