Работа с изображениями
Учим «Цыплёнка-помощника» брать присланное фото, что-нибудь с ним делать через Pillow и возвращать готовую картинку обратно в чат.
Pillow — это библиотека Python для работы с картинками: открыть файл, уменьшить, повернуть, наложить текст, сохранить заново. С её помощью бот превращается в маленький фоторедактор, который живёт прямо в Telegram.
В уроке про фото, файлы и стикеры наш «Цыплёнок-помощник» уже научился принимать картинки от пользователя и отправлять свои. А в уроке про запросы к API через aiohttp он ходил за данными во внешний мир. Сегодня соединяем оба навыка и добавляем третий: бот будет не просто пересылать фото, а менять его — и присылать обратно уже обработанным.
Зачем боту уметь редактировать картинки
Представь: ты ведёшь бота для своего класса. Все шлют фотки с прогулки, а бот автоматически шлёпает на каждую водяной знак «9Б, весна 2026» — и получается готовый альбом в едином стиле. Или бот для игрового клана: кидаешь свой скрин, а он делает из него квадратную аватарку нужного размера. Или мем-бот: присылаешь картинку, он добавляет рамку и подпись снизу — классический формат мема готов за секунду.
Всё это — одна и та же схема: взять фото от пользователя → что-то с ним сделать → вернуть результат. Звучит как магия, но на деле это четыре простых шага, и сегодня мы пройдём каждый. Вот к чему придём — бот, который принимает любое фото и возвращает его уменьшенным до аккуратной превьюшки с подписью:
@chick_helper_bot
[ты прислал большое фото 4000×3000 на 5 МБ]
🐤 Готово! Уменьшил твоё фото до превью и подписал. Держи 👇
[бот присылает картинку 512×384 с подписью «🐤 codechick» в углу]
Результат: пользователь шлёт большое фото, а бот возвращает маленькую обработанную версию. Тяжёлое фото на несколько мегабайт превращается в лёгкую превьюшку, на которую ещё и нанесена подпись.
Картинка как сетка пикселей, а Pillow — её редактор
Чтобы не бояться обработки фото, держи в голове простую картину. Любое изображение — это сетка крошечных квадратиков-пикселей, как клеточки в тетради. У каждой клеточки есть свой цвет. Картинка 100×100 — это сто клеточек в ширину и сто в высоту, всего десять тысяч цветных точек. Когда мы «уменьшаем» фото, мы просто делаем эту сетку мельче; когда «поворачиваем» — раскладываем те же клеточки по-другому.
Самому возиться с миллионами пикселей вручную — это кошмар. Поэтому существует Pillow — библиотека, которая берёт всю грязную работу на себя. Ты говоришь ей человеческими словами: «открой этот файл», «сделай в два раза меньше», «поверни на 90 градусов», «напиши вот этот текст вот тут», «сохрани» — а она сама пересчитывает все пиксели. Это как разница между «нарисовать каждую клеточку карандашом» и «нажать пару кнопок в фоторедакторе».
Кстати, теперь становится понятно, почему большое фото «весит» много мегабайт, а превью — считанные килобайты. Чем больше клеточек в сетке, тем больше цветов надо где-то записать. Фото 4000×3000 — это двенадцать миллионов пикселей, и каждый хранит свой цвет; уменьшив сетку до 512×384, мы оставляем меньше двухсот тысяч точек — отсюда и резкое падение веса. Для превьюшки в чате огромное разрешение всё равно избыточно: глаз разницы не заметит, а пересылается такая картинка почти мгновенно. Именно поэтому уменьшение — самая частая операция в фото-ботах, с неё мы и начнём.
Pillow не входит в Python по умолчанию, её надо поставить отдельно. Имя пакета при установке — Pillow (с большой буквы), а вот в коде модуль называется PIL — это историческое имя, к нему привыкаешь быстро:
pip install PillowРезультат: в твоё виртуальное окружение установится Pillow, и в коде станет доступен импорт from PIL import Image.
Главный объект Pillow — это Image. Открыл файл — получил объект Image, поработал с ним методами, сохранил обратно в файл. Давай посмотрим на самый простой пример без всякого Telegram, чтобы прочувствовать логику.
Знакомимся с Pillow на простом примере
Представь, что у нас уже есть файл photo.jpg на диске. Откроем его, узнаем размер, уменьшим вдвое и сохраним результат в новый файл:
from PIL import Image
# 1. Открываем файл — получаем объект Image
img = Image.open("photo.jpg")
print("Размер оригинала:", img.size) # (4000, 3000)
# 2. Считаем новый размер — в два раза меньше по каждой стороне
new_size = (img.width // 2, img.height // 2)
# 3. Уменьшаем
small = img.resize(new_size)
# 4. Сохраняем в новый файл
small.save("photo_small.jpg")
Результат: рядом с photo.jpg появится файл photo_small.jpg вдвое меньшего размера по каждой стороне. Оригинал при этом не меняется — мы создали новую картинку.
Разберём по шагам:
Image.open("photo.jpg")— открывает файл и возвращает объектImage. Это как «загрузить фото в редактор».img.size— кортеж(ширина, высота)в пикселях. Есть и отдельныеimg.widthиimg.height.img.resize((w, h))— создаёт новую картинку нужного размера. Самimgне меняется — Pillow почти всегда возвращает новый объект, а не правит старый.small.save("photo_small.jpg")— записывает картинку в файл. По расширению.jpgPillow сам поймёт, в каком формате сохранять.
Чтобы окончательно подружиться с размерами, посчитаем новый размер на чистом Python — без картинок, просто арифметика, которую можно проверить прямо в браузере:
width, height = 4000, 3000
# хотим, чтобы большая сторона стала не больше 512
max_side = 512
ratio = max_side / max(width, height)
new_width = int(width * ratio)
new_height = int(height * ratio)
print("Было:", (width, height))
print("Стало:", (new_width, new_height))
Вывод:
Было: (4000, 3000) Стало: (512, 384)
Видишь приём: мы считаем ratio — во сколько раз надо сжать, чтобы большая сторона стала ровно 512, — и умножаем на него обе стороны. Так картинка ужимается пропорционально, не сплющиваясь. Этот расчёт пригодится в боте.
Шаг 1–2: скачиваем фото пользователя
Теперь к боту. Первое, что нужно, — заполучить файл, который прислал пользователь, к себе на диск. В уроке про фото мы уже видели, что у фотографии есть file_id. Telegram не присылает сам файл сразу — он присылает только этот идентификатор, а скачивание мы запускаем сами. У объекта bot для этого есть удобный метод download.
Важная деталь про фото в Telegram: одно отправленное фото приходит в нескольких размерах (мелкое превью, среднее, крупное). Они лежат в списке message.photo по возрастанию размера, поэтому message.photo[-1] — это всегда самый большой, самый качественный вариант. Его и берём.
from aiogram import F
from aiogram.types import Message
@dp.message(F.photo)
async def handle_photo(message: Message):
# берём самый крупный размер фото
photo = message.photo[-1]
# скачиваем файл на диск рядом с ботом
await bot.download(photo, destination="input.jpg")
await message.answer("🐤 Поймал твоё фото, сейчас поколдую над ним...")
Результат: как только пользователь пришлёт фото, бот скачает его в файл input.jpg рядом со своим кодом и ответит, что принял картинку в работу.
Разберём:
@dp.message(F.photo)— этот хэндлер срабатывает только на сообщения, в которых есть фото.F.photo— это «волшебный фильтр» aiogram: «у сообщения есть полеphoto».message.photo[-1]— самый большой из присланных размеров.await bot.download(photo, destination="input.jpg")— скачивает файл и сохраняет под именемinput.jpg. Это асинхронная операция (бот ходит в сеть за файлом), поэтомуawaitобязателен.
Шаг 3: обрабатываем картинку через Pillow
Файл на диске — дальше работаем чистым Pillow, как в примере выше. Сделаем то, что обещали в начале: уменьшим фото до превью и нанесём подпись в углу. Для текста понадобится ещё один помощник из Pillow — ImageDraw, это как «карандаш», которым можно рисовать поверх картинки.
from PIL import Image, ImageDraw
def make_preview(src_path: str, dst_path: str):
img = Image.open(src_path)
# 1. Пропорционально ужимаем так, чтобы большая сторона была 512
max_side = 512
ratio = max_side / max(img.width, img.height)
new_size = (int(img.width * ratio), int(img.height * ratio))
img = img.resize(new_size)
# 2. Берём «карандаш» и пишем подпись в левом верхнем углу
draw = ImageDraw.Draw(img)
draw.text((10, 10), "\ud83d\udc24 codechick", fill="white")
# 3. Сохраняем результат
img.save(dst_path)
Результат: функция возьмёт картинку из src_path, уменьшит её до превью и сохранит в dst_path с белой подписью «🐤 codechick» в левом верхнем углу.
Что здесь нового:
ImageDraw.Draw(img)— создаёт «карандаш», привязанный к нашей картинке. Всё, что мы им рисуем, ложится поверх пикселейimg.draw.text((10, 10), "текст", fill="white")— пишет текст. Первый аргумент — координаты(x, y), отступ от левого верхнего угла в пикселях.fill="white"— цвет букв.
Координаты в Pillow считаются от левого верхнего угла: x растёт вправо, y — вниз. То есть (10, 10) — это чуть-чуть отступить от верхнего левого угла. Если захочешь подпись внизу, надо взять y побольше, ближе к высоте картинки.
Шаг 4: отправляем результат обратно
Готовый файл лежит на диске — осталось отдать его пользователю. Отправка фото нам знакома по уроку про фото: используем message.answer_photo, а сам файл оборачиваем в FSInputFile — это способ сказать aiogram «возьми файл вот по этому пути на диске».
from aiogram.types import FSInputFile
await message.answer_photo(
FSInputFile("output.jpg"),
caption="🐤 Готово! Уменьшил и подписал твоё фото.",
)
Результат: бот пришлёт в чат картинку из файла output.jpg с подписью под ней.
FSInputFile расшифровывается как «File System Input File» — файл из файловой системы. Если бы картинка у нас была в памяти, а не на диске, мы бы взяли другой класс, но для нашего случая, когда мы сохранили файл через Pillow, FSInputFile — самое то.
Собираем всё в один хэндлер
А теперь — обещанный бот целиком. Соединяем все четыре шага в один хэндлер «Цыплёнка-помощника». Функцию make_preview из шага 3 считаем уже объявленной выше в файле bot.py:
from aiogram import F
from aiogram.types import Message, FSInputFile
@dp.message(F.photo)
async def handle_photo(message: Message):
# Шаг 1–2: скачиваем самый крупный размер фото
photo = message.photo[-1]
await bot.download(photo, destination="input.jpg")
await message.answer("\ud83d\udc24 Поймал фото, обрабатываю...")
# Шаг 3: обрабатываем через Pillow
make_preview("input.jpg", "output.jpg")
# Шаг 4: отправляем результат обратно
await message.answer_photo(
FSInputFile("output.jpg"),
caption="\ud83d\udc24 Готово! Уменьшил и подписал твоё фото.",
)
Результат: пользователь присылает любое фото — бот скачивает его, пишет «обрабатываю...», уменьшает картинку до превью с подписью и присылает результат обратно в чат отдельным сообщением.
Вот и вся схема из начала урока, целиком на четырёх шагах:
- Скачали присланный файл через
bot.download. - Открыли его в Pillow через
Image.open. - Обработали:
resize+ подпись черезImageDraw, иsave. - Отправили результат обратно через
answer_photo+FSInputFile.
Запомни эту четвёрку — по ней строится любой фото-бот, что бы ты с картинкой ни делал. Хочешь поворачивать? Замени тело make_preview на img.rotate(90). Хочешь делать чёрно-белым? img.convert("L"). Каркас остаётся тем же.
Обрати внимание на маленькую, но важную деталь: между скачиванием и отправкой результата бот пишет промежуточное «обрабатываю...». Это не для красоты. Скачивание файла и его обработка занимают время — пусть доли секунды, но пользователь в этот момент не понимает, увидел ли бот его фото вообще. Короткое сообщение-заглушка снимает это напряжение: человек видит, что бот не завис, а работает. В уроках про большие задачи мы ещё вернёмся к этой идее и научимся показывать статус «бот печатает...», но даже простой текст уже делает бота куда приятнее в общении.
Частые ошибки и подводные камни
1. Обрабатываешь не тот размер фото
Очень частая ловушка — взять message.photo[0] вместо message.photo[-1]. Нулевой элемент — это самое мелкое превью (часто всего 90 пикселей), и пользователь получит обратно мутный квадратик, недоумевая, куда делось его красивое фото. Помни: message.photo отсортирован от мелкого к крупному, и тебе почти всегда нужен последний — [-1].
2. Pillow не меняет картинку на месте
Новички пишут img.resize((512, 512)) отдельной строкой и ждут, что img станет меньше. Но Pillow возвращает новую картинку, а старую не трогает. Если не присвоить результат (img = img.resize(...)) или не сохранить его в новую переменную, изменение просто потеряется. Правило: результат resize, rotate, convert и подобных методов надо обязательно куда-то записать.
3. Забыл сохранить файл перед отправкой
Бывает, что всю обработку сделали в памяти, а save() вызвать забыли — и в output.jpg остаётся либо старое содержимое, либо вообще пусто. А если файл существует со вчерашнего запуска, бот вышлет позавчерашнюю картинку и будет «глючить» загадочным образом. Всегда сохраняй результат в файл перед тем, как заворачивать его в FSInputFile.
4. Pillow не установлена или импорт перепутан
Ошибка ModuleNotFoundError: No module named 'PIL' пугает новичков: «я же ставил Pillow!». Тут хитрость: ставится пакет с именем Pillow (pip install Pillow), а импортируется он как PIL — from PIL import Image. Это не опечатка, а историческая особенность. Если ошибка осталась — проверь, что ставил пакет в то же виртуальное окружение, где запускаешь бота.
5. Файлы перетирают друг друга при нескольких пользователях
Пока ты тестируешь бота в одиночку, имена input.jpg и output.jpg работают. Но если двое пришлют фото одновременно, они начнут писать в один и тот же файл и получат чужие картинки. Для надёжности делай имена уникальными — например, подставляй id пользователя: f"input_{message.from_user.id}.jpg". Тогда у каждого свой файл, и они не мешают друг другу.
Мини-практика: бот-«поворачиватель»
Теперь твой ход. Сделай «Цыплёнку-помощнику» команду-фишку: пользователь присылает фото — бот возвращает его повёрнутым на 90 градусов и подписанным «🐤 повернул для тебя».
- Скачай присланное фото (шаг 1–2 ты уже знаешь).
- Открой его в Pillow и вызови
img.rotate(90, expand=True). Аргументexpand=Trueважен: без него углы картинки обрежутся, а с ним Pillow расширит холст, чтобы повёрнутое фото влезло целиком. - Нанеси подпись через
ImageDrawи сохрани результат. - Отправь обратно через
answer_photo+FSInputFile.
Когда заработает — попробуй усложнить. Добавь команду на выбор поворота: пусть бот сначала покажет inline-кнопки «90°», «180°», «270°» (их мы разбирали в модуле про кнопки), а после нажатия повернёт сохранённое фото на нужный угол. Подсказка: угол можно временно запомнить в FSM-состоянии пользователя, как мы делали в анкете.
Итоги
Сегодня наш «Цыплёнок-помощник» стал настоящим карманным фоторедактором. Теперь он умеет всю классическую четвёрку:
- скачать присланное фото —
bot.download(message.photo[-1], destination=...); - открыть и обработать через Pillow —
Image.open,resize,rotate, текст черезImageDraw; - сохранить результат в файл —
img.save(...); - отправить обратно —
answer_photo(FSInputFile(...)).
Главная мысль урока: картинка — это просто сетка пикселей, а Pillow берёт на себя всю грязную работу с ними. Тебе остаётся выстроить четыре шага в правильном порядке — и бот превращается в фабрику мемов, аватарок или альбомов, какую только придумаешь.
В следующем уроке мы продолжим тему интеграций и научим бота работать с документами и файлами разных форматов — принимать PDF и таблицы, отдавать сгенерированные отчёты. Картинками наш «Цыплёнок» уже жонглирует, пора браться за файлы посерьёзнее. До встречи!