Сегодня мы развлечемся и напишем бота в Telegram, который будет делать мемы и стикеры из изображений, используя библиотеку Pillow.

Наш бот будет накладывать текст на фото или трансформировать фото в подобие стикера. Да, функционал самый простой, если нужно что-то более продвинутое, можно воспользоваться фотошопом или нейронками. Или разобраться глубже в том, как написать дополнительную функциональность.
Лично я использую бота, создающего стикеры и мемы, когда надо отправить что-то в чате как реакцию на происходящее с юмором, не тратя время на поиск подходящего мема.
В завершении статьи, мы развернем проект в облаке Amvera, используя git push, буквально за четыре команды в IDE.
Исходный код бота на GitHub.
Ссылка на демо работающего бота.
Создание бота, создающего стикеры и мемы
Для создания бота нам нужно перейти в BotFather и получить токен для API. Он нам потребуется позже.
Написание бота с использованием библиотеки aiogram 3
Перед тем как приступить к написанию бота, мы установим необходимые библиотеки для работы бота.
Переходим в командную строку (cmd) и прописываем команду ниже.
pip install aiogram pillowПриступим к написанию кода.
Создаём и открываем файл main.py.
Импортируем библиотеки.
import logging
import asyncio
import random
import osfrom aiogram import Bot, Dispatcher, Router, types
from aiogram.filters import CommandStart, StateFilter
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.state import StatesGroup, State
from aiogram.fsm.context import FSMContext
from stickers import create_sticker
from mem import create_memeЭкземпляр бота выглядит следующим образом.
logging.basicConfig(level=logging.INFO)
bot = Bot(token=os.environ["TOKEN"]) # токен, который будет в качестве переменной на хостинге
dp = Dispatcher() # переменная диспетчера
router = Router() # переменная роутера
async def main():
dp.include_router(router) # инициализация роутера
await dp.start_polling(bot) # запуск бота
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO) # инициализация логгера
try:
asyncio.run(main()) # запускаем функцию main в асинхронном режиме
except KeyboardInterrupt:
print('Бот выключен')Дополнительно создадим файлы mem.py, stickers.py а также шрифт для мемов, который можно будет скачать в исходнике бота на Github в конце статьи.
Мы сделали основу бота, теперь создадим команды
Переходим в файл mem.py и вставляем код ниже.
import io
from PIL import Image, ImageDraw, ImageFont
from aiogram.types import BufferedInputFile
async def create_meme(photo_bytes: bytes, text: str) -> BufferedInputFile:
try:
image = Image.open(io.BytesIO(photo_bytes)).convert("RGB") # открываем файл изображения в байтах
except Exception as e:
print(f"Ошибка открытия изображения: {e}")
return None
draw = ImageDraw.Draw(image)
# размер шрифта
font_size = int(image.width / 10)
try:
font = ImageFont.truetype("fonts/font.ttf", size=font_size) # выбираем шрифт из папки fonts, присваиваем размер шрифта
except IOError: # если шрифт не был найден
print("Шрифт не найден. Используем шрифт по умолчанию.")
font = ImageFont.load_default() # используем стандартный шрифт
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# проверка, чтобы текст не выходил за пределы изображения
if text_width > image.width:
font_size = int(font_size * (image.width / text_width))
font = ImageFont.truetype("fonts/font.ttf", size=font_size)
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = (image.width - text_width) // 2
y = image.height - text_height - 20
# проверка, чтобы текст не выходил за нижнюю границу изображения
if y < 0:
y = 10 # отступ сверху, если текст слишком большой
draw.text((x, y), text, fill="white", font=font) # "пишем" текст на картинке в белом цвете
meme_bytes = io.BytesIO()
image.save(meme_bytes, format='JPEG') # сохарняем фото
meme_bytes.seek(0)
# конвертируем фото в байты
meme_bytes_data = meme_bytes.getvalue()
return BufferedInputFile(meme_bytes_data, filename='meme.jpg')Переходим в файл stickers.py и вставляем код ниже.
from aiogram import types
from aiogram.types import BufferedInputFile
from PIL import Image
import io
async def create_sticker(photo: types.PhotoSize, bot) -> BufferedInputFile:
photo_file = await bot.download(photo.file_id) # скачиваем фото
image = Image.open(photo_file) # открываем фото
image = image.resize((512, 512)) # размер стикера Telegram
sticker_bytes = io.BytesIO()
image.save(sticker_bytes, format='PNG') # сохраняем фото
sticker_bytes.seek(0)
sticker = BufferedInputFile(sticker_bytes.getvalue(), filename='sticker.png') # создаем стикер
return stickerНапишем состояния FSM
from stickers import create_sticker
from mem import create_meme
class Form(StatesGroup): # Состояния
waiting_memphoto = State()
waiting_stickphoto = State()
waiting_text_for_mem = State()Напишем переменные
choicestart = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="1️⃣ Мем из фотографии", callback_data="mem")],
[InlineKeyboardButton(text="2️⃣ Стикер из фотографии", callback_data="sticker")],
]) # Кнопки
cancel = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="◀️ Назад", callback_data="cancel")]
])
errors_in_text = {
"❌ Я не умею добавить текст в виде ФАЙЛА 