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

Вопрос лишь в том, какую модель выбрать и как правильно поставить ей задачу, чтобы получить готовый стикерпак, а не набор случайных картинок. Одна из них — ChatGPT 5 Image — умеет создавать изображения сразу в формате PNG и с прозрачностью, что делает ее идеальным инструментом.

В этой статье я расскажу, как создать стикерпак в ChatGPT 5 Image, лучшей нейросети для создания стикеров в 2025 году, всего за пять минут.

В какой нейросети лучше генерировать стикеры

Если коротко: сегодня абсолютным фаворитом считается GPT 5 Image. Почему? Всё просто — взгляните на эту таблицу:

Как видите, ChatGPT 5 Image выделяется ключевой особенностью — он создаёт изображения сразу с прозрачностью, то есть с готовым альфа‑каналом.

А это, между прочим, экономит вам уйму времени: не нужно вручную вырезать фон (а ведь удаление фона с нечеткими краями — один из тех кошмаров, о которых дизайнеры предпочитают не вспоминать).

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

Ну а в этом гайде я б��ду работать именно с ChatGPT 5 Image.

Кстати, с нейросетью всё не так просто: у неё несколько имён. В марте 2025 года она появилась как GPT Image 1, но пользователи чаще называют её то ChatGPT 4o, то ChatGPT 5 Image — по названию версии чат‑бота, в котором она живёт. Официальное имя — GPT Image 1, но давайте не будем теряться в терминологии и остановимся на привычном «ChatGPT 5 Image».

Запускать ChatGPT 5 Image, как мне кажется, удобнее всего через сервис BotHub. Из России он работает стабильно и без VPN, интерфейс дружелюбный, и самое приятное — там можно переключаться между разными нейросетями буквально в два клика.

Если решите зарегистрироваться, загляните по этой ссылке: получите 100 000 бонусных капсов.

Генерируем промпт для ChatGPT 5 Image в ChatGPT 5

Вы наверняка слышали о метапромптинге — это когда промпт создаётся… с помощью другого промпта. Да‑да, звучит необычно, но работает потрясающе: ChatGPT помогает подготовить правильный запрос.

Вот какая структура промпта, на мой взгляд, получается наиболее удобной и универсальной — своего рода скелет для будущих идей:

Создай промпт для нейросети GPT 5 Image на генерацию стикеров (понимает описания на естественном русском языке), который будет генерировать одним изображением набор стикеров 3×3. Формат изображения — квадратный (1:1), с прозрачностью (стикеры должен быть на прозрачном фоне).

Персонаж(и) промпта: <...>

Визуальный стиль: <...>

Эмоции персонажей: <...>

Надписи на стикерах: <...>

Дополнительные элементы и эффекты: <...>

Общие эффекты стикеров: разноцветная обводка или свечение небольшой толщины, вокруг неё — тень, в сторону 22,5°.

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

Добавь к промпту информацию о том, что если приложена картинка, то ее нужно использовать как образец.

Такой шаблон вы можете закидывать практически в любую нейросеть — будь то ChatGPT 5, Grok 4, DeepSeek v3.2 или Claude Sonnet 4.5. Кстати, если отправить несколько раз одно и то же, получатся разные результаты.

Тут я включить автопилот и доверил всё ChatGPT 5. Вот что модель выдала:

Затем я переключился с ChatGPT 5 на ChatGPT 5 Image.

Причём всё происходило в рамках одного диалога. Просто я снял галочку «Учитывать контекст», ведь запросы были независимыми: промпт создавался отдельно, а генерация изображения — уже по готовому тексту.

Если сравнить с другими нейросетями в этом интерфейсе (например, Midjoureny), возможно вас удивит, что у ChatGPT 5 Image нет визуальных меню, выпадающих списков или настроек «по клику». Все параметры, включая прозрачность фона, соотношение сторон или размер, задаются обычным текстом. Например: «пусть фон будет прозрачным», «создай квадратное изображение 1:1» и так далее

Но есть одна очень удобная фича, которую нельзя не упомянуть: возможность добавлять картинку‑референс. Я специально включил фразу об этом в шаблон. Зачем? Чтобы можно было прикрепить, скажем, своё фото или любую другую картинку. Это особенно полезно, если хочется, чтобы персонажи в стикерах выглядели узнаваемо.

Я сгенерировал две версии стикеров на одном и том же промпте. Как видите, стиль и черты получились очень схожи.

Выглядит здорово, правда? Теперь осталось совсем немного — взять виртуальные ножницы под названием Photoshop и разрезать общее полотно на отдельные стикеры.

Да, отмечу, что черный фон на самом деле тут не черный — просто в браузере так отобразилась прозрачность. Если же скачать картинку (кнопка ↓ появится в правом верхнем углу картинки при наведении мыши), то это будет.png с прозрачностью.

В этом эксперименте ChatGPT полностью подбирал стиль и параметры. Но если хочется большего контроля, вы можете задать персонажа, стиль, эмоции и так далее. Например, популярные стили, которые встречаются в соцсетях:

  • Disney в 3D — если хочется кинематографичности;

  • чиби (милый японский стиль, где всё миниатюрное и очаровательно);

  • Ghibli — мягкий акварельный вайб;

  • 3D kawaii — когда милота просто зашкаливает.

Лайфхак: необязательно генерировать сразу 9 стикеров сеткой 3×3. Можно сделать 2×2 или вообще по одному — тогда каждый будет в большем разрешении (1024×1024 пикселя), детали и надписи станут ещё чётче.

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

Как разделить изображение на отдельные стикеры

Если вы тоже сгенерировали весь стикерпак за один заход, следующим шагом будет разделить картинку на отдельные стикеры.

Казалось бы, простая задача, правда? Но нет — автоматическое распиливание изображения всё ещё остаётся квестом. Пока не существует онлайн‑сервиса, который справлялся бы с этим идеально с первого раза (а жаль!).

Более того, большинство онлайн‑сервисов (например, Aspose) просто «разрубают картинку топором» на равные куски по горизонтали и вертикали, и в них нет никакой функции обнаружения элементов.

Но даже те сервисы, что предлагают ИИ‑функции для автоматического обнаружения объектов (Komiko, который больше специализируется на разделении слоёв, чем объектов, но всё‑таки), делают это неточно. Взгляните сами, что получается:

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

Пробуем разделить по слоям в Photoshop

Что ж, выход здесь только один: разделять по слоям в графическом редакторе. Для этого подойдёт практически любой графический редактор — Photoshop, Gimp, Inkscape, Figma (да, даже Figma). Задача простая: аккуратно вырезать каждый стикер в отдельный слой.

Возьмём одну из наших ранее сгенерированных картинок и пройдём процесс пошагово.

1. Открываем изображение

Начнём с открытия файла в Photoshop:

Видите шахматное полотно? Это значит, что фон действительно прозрачный, — то, что нам и нужно.

2. Добавляем белый фон для удобства

Чтобы тени и обводки было видно чётче, я бы рекомендовал подложить под стикеры белый слой. Делается это в три шага:

1) Создайте новый слой кнопкой .

2) Перетащите этот слой вниз под основной.

3) Залейте его белым цветом. Это можно сделать через меню Edit → Fill, выбрав белый цвет. Или превратить слой в фон через Layer → New → Background from Layer — Photoshop сам закрасит его текущим фоновым цветом.

Зачем именно белый? Он лучше всего подчеркивает любые тени и блики. А если у вас стикеры без теней и с белыми обводками — поэкспериментируйте с другими фонами.

3. Переходим к работе с объектами

Переключаемся обратно на слой с самими стикерами — Layer 1 — и берём в руки один из самых полезных инструментов: полигональное лассо.

4. Навигация по полотну

Кстати, масштаб можно крутить колесиком мыши (если это не так, проверьте настройки Photoshop), а перетаскивать полотно — левой кнопкой, пока другой рукой на клавиатуре зажат пробел. И то и другое очень пригодится в процессе разделения изображения на части.

5. Обводим стикер вручную

С помощью щелчков мыши обводим стикер вокруг так, чтобы не попасть контуром ни на его область, ни на соседние стикеры. Если понадобится всё отменить и начать сначала — нажмите Esc, а если случайно поставили точку не туда — Backspace или Del.

Кстати, пока зажата левая кнопка мыши, можно временно активировать режим обычного лассо, зажав на клавиатуре Alt. Тогда вы будете отмечать контур не точками, а непрерывной линией. Хотя смысла в этом нет — быстро утомляет.

Чтобы замкнуть контур, достаточно либо щёлкнуть в начальной точке, либо дважды — в последней.

6. Вырезаем слой

Когда вы уже отметили область, щелкните правой кнопкой где‑нибудь внутри изображения и выберите Layer via cut. Тогда отмеченный фрагмент будет вырезан в новый слой.

7. Повторяем для всех стикеров

Тут самое интересное... Шаги 3–7 (не забываем сначала выбирать основной слой) нужно повторить для каждого стикера. Уже отделенные слои можно скрывать, нажав на значок глаза.

8. Последний стикер — не забываем!

Даже последний стикер лучше вырезать в отдельный слой — иногда остаются едва заметные полупрозрачные пиксели, и лучше подстраховаться.

9. Сохраняем стикеры

Теперь — время экспортировать.

Выделяем все получившиеся слои (щелчок по первому → Shift + щелчок по последнему), кликаем правой кнопкой и выбираем Quick export as PNG. Photoshop предложит выбрать папку для сохранения слоев.

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

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

10. Проверяем размер изображений

Но прежде чем нести их в Telegram, нужно сделать ещё один штрих. По стандарту у телеграм‑стикеров одна из сторон должна быть не меньше 512 пикселей. Так что давайте немного увеличим наши картинки.

11. Массовое открытие файлов

Закрываем исходный документ, открываем все экспортированные PNG. Самый быстрый способ — просто перетащить их одновременно в окно Photoshop. Он откроет каждое изображение в отдельной вкладке.

12. Выравниваем размеры холста

Сейчас наши изображения имеют слегка разный размер: где‑то 324×336 пкс, где‑то 279×327 и так далее. Чтобы удобнее масштабировать картинки, сначала приведем их к одному размеру.

Для этого в каждом изображении откорректируйте размер полотна: выберите Image → Canvas size, поставьте ширину и высоту, скажем, 350×350 и нажмите OK.

Почему 350×350? Просто потому, что ChatGPT 5 Image генерирует квадрат в размере 1024×1024 пкс (кстати, еще умеет 1536×1024 и 1024×1536). Если разделить на 3, получим ~341,33. Добавим немного запаса — 350 кажется оптимальным вариантом. Но если в процессе где‑то что‑то обрежется, то поставьте чуть ��ольше, например 355–360 пкс (для всех стикеров нужен одинаковый размер).

Переключаться между открытыми вкладками, кстати, удобно комбинацией Ctrl+Tab.

Как вы уже заметили, Canvas size, в отличие от Image size, не меняет масштаба изображения, то есть пиксели остаются нетронутыми.

13. Центрируем изображение

Если вдруг стикер выглядит чуть сдвинутым — возьмите инструмент Move tool и аккуратно подвиньте слой туда, где он смотрится гармонично.

Например, на этом скриншоте я сдвинул слой влево‑вверх, так как эффекты в виде полосок слегка «перевесили» визуальный центр картинки.

14. Масштабируем до стандарта Telegram

И наконец... выбираем Image → Image size, чтобы поставить размер 512×512 пикселей.

Обратите внимание на алгоритм увеличения, выбранный в списке Resample. Чтобы сгладить шероховатости, особенно для мультяшных изображений, имеет смысл выбрать алгоритм Preserve details 2.0, а также подрегулировать значение Reduce noise.

15. Финальный экспорт

После масштабирования каждое изображение сразу же можно сохранять и закрывать.

Загрузка стикеров в Telegram

Как и многое в этом мессенджере, нужная функция спрятана не в настройках и не в меню, а в… отдельном боте. Конечно, можно добавить стикеры и через мобильное приложение (на Android или iOS), но удобнее и функциональнее — именно через бота. К тому же там доступно большее количество команд.

Адрес его прост до безобразия: ? https://t.me/Stickers. Или просто вбейте в поиске Telegram — @Stickers. Добавьте его в избранное, закрепите вверху, поставьте звёздочку — не пожалеете. Это ваш проводник в мир создания мемов и эмоций.

Я покажу процесс на примере Telegram Desktop (https://desktop.telegram.org/), но всё это можно осуществить и в веб‑версии (https://web.telegram.org/).

Создаём набор стикеров

1. Начало

Находясь в боте, для начала вводим /newpack.

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

2. Придумываем название

Далее бот попросит вас указать название набора. Просто напишите текстом.

Это имя будет видно всем пользователям, которые добавят ваш стикерпак.

3. Загружаем изображения

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

  • Тип файла: .png или .webp.

  • Фон: прозрачный.

  • Одна из сторон холста = 512 пикселей.

  • Другая сторона холста ≤ 512 пикселей.

Если фон вдруг непрозрачный — нестрашно. Telegram просто оставит ваш фон как есть (квадратный или прямоугольный).

А наличие белой обводки и тени — скорее дело вкуса, чем строгого стандарта. Честно говоря, представить модерацию, которая проверяет наличие «тени толщиной 2 пикселя под нужным углом», просто невозможно.

Бот принимает только одно изображение за раз. Даже если вы перетащите несколько файлов — он просто возьмёт одно из них.

4. Отправляем первый стикер

Отправьте изображение так же, как обычно загружаете файл в Telegram — просто перетащите его в окно чата.

И вот тут есть два момента, которые важно не пропустить:

  1. Обязательно отправляйте без сжатия (выбирайте вариант «как файл»).

  2. Порядок загрузки имеет значение — именно в нём стикеры появятся в паке. Однако позже можно всё переупорядочить с помощью команды /ordersticker.

Кстати, на этом этапе можно добавлять не только свои изображения, но и стикеры из других наборов. Telegram не ограничивает использование чужих — авторских прав на стикеры здесь просто нет. Так что при желании можно собрать личный «сборник лучших стикеров».

5. Добавляем смайлики-ассоциации

Вы, наверное, замечали: когда вводите смайлик в чате, Telegram предлагает ленту похожих стикеров. Эти ассоциации тоже задаёт сам автор стикерпака.

После загрузки каждого стикера просто отправьте в чат сообщение с одним или несколькими смайликами — именно они станут его триггерами.

Если ввести один эмодзи — стикер будет ассоциироваться только с ним. А если несколько — каждый из них тоже привяжется к вашему изображению.

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

6. Повторяем шаги и следим за прогрессом

Дальше всё просто: повторяйте шаги 3–5 для каждого стикера, пока не загрузите весь набор. Бот после каждого добавления выводит, сколько стикеров уже в паке. Это удобно, особенно если вы работаете с большим сетом.

7. Публикуем стикерпак

Когда загрузили последний стикер, самое время «закрыть сделку» — отправьте команду /publish.

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

8. Добавляем обложку

Теперь нужно загрузить иконку‑обложку стикерпака. Формат прежний (.png или.webp), размер изображения — 100×100 пкс (включая отступы и тени, при наличии таковых).

Как вариант — пропустить этот шаг, тем самым создав иконку из самого пер��ого стикера, который вы загрузили в набор.

Здесь я снова открыл один из стикеров в Photoshop, уменьшил его размер до 100×100 пкс и сохранил под новым именем (cover.png).

Кстати, для уменьшения картинок чаще применяют алгоритм Bicubiс sharper.

9. Придумываем ссылку

Придумайте завершающий компонент веб‑адреса (так называмеый слаг):

https://t.me/addstickers/nazvaniye_vashego_stikerpaka

Правила просты: используйте латиницу, цифры и подчёркивания. Всё как в обычных URL.

10. Ура! Ваш стикерпак готов ?

Готово, стикерпак создан! Теперь можно спамить этой ссылкой по всем известным контактам и чатам: https://t.me/addstickers/funnyfoxes13.

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

Как отредактировать стикерпак в Telegram

Хорошая новость в том, что бот @Stickers позволяет не только создавать новые наборы, но и редактировать уже существующие.

Вот набор полезных команд, которые точно стоит запомнить:

  • /addsticker — добавить стикер в стикерпак, который вы создали ранее. Если вдруг хотите убрать лишний — для этого есть зеркальная команда /delsticker.

  • /editsticker — дает возможность отредактировать стикер. В принципе, настройка здесь только одна: изменить привязанный смайлик. После выбора /editsticker отправьте боту сам стикер, который хотите изменить.

  • /replacesticker — позволяет заменить изображение уже существующего стикера. На случай, если вы можете захотеть обновить изображение одного из них.

  • /ordersticker — изменить порядок расположения стикеров в наборе.

  • /setpackicon — обновить или добавить обложку вашего набора.

  • /cancel — отменить предыдущее действие. Аналог компьютерного Ctrl+Z.

Другие команды тоже достаточно интуитивны.

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

Просмотр статистики популярности

Создали шедевр и хотите узнать, насколько он зашёл публике? Telegram предусмотрел и это. Вот три команды, которые помогут вам следить за успехом вашего стикерпака:

  • /packusagetop;

  • /packstats;

  • /stats.

Например, /packusagetop, позволяет отобразить статистику, сколько человек добавили ваш набор сегодня, вчера, за определенный день или за всё время.

Где ещё можно применить стикеры

На этом этапе вы уже, скорее всего, захотите использовать свои творения не только в Telegram — и это отличная идея. Ваши прозрачные PNG — универсальный актив. Вот лишь несколько способов их применения:

  • Отправляйте их в другие мессенджеры, на форумах или в чаты — в виде мемов, мини‑комиксов или просто забавных р��акций.

  • Используйте в качестве аватарок для соцсетей, профилей на форумах или даже в рабочих чатах (ну а почему бы и нет?).

Комментарии (2)


  1. vybo
    03.11.2025 21:27

    DIY или Сделай сам

    Читать "ничего сам не делай, публикуй голый нейрослоп и гордись собой", ну ведь хотя бы подпись "серьēзно" не могла же не резануть по глазам, фш уже открыт и куда уж проще стереть черточку и тыкнуть пару круглых точек, ан нет — антилехту виднее


  1. pol_pot
    03.11.2025 21:27

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

    Первая версия делает нарезку сеткой по 9шт, автоматически детектит расположение и центрирует, и потом еще растягивает всех до одного требуемого телеграмом размера.

    Вторая версия позволяет вручную аккуратно выделить тех кто плохо получился + такая же постобработка и подготовка к телеграму.

    Получилось меньше 1млн токенов, если бы пришлось платить перепродавцам типа ботхаба за токены бесплатного по сути джемини то сожрало бы рублей 300.

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

    Скрытый текст
    <!DOCTYPE html>
    <html lang="ru">
    <head>
        <meta charset="UTF-8">
        <title>Авто-подготовка стикеров для Telegram</title>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
        <style>
            body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; padding: 20px; max-width: 900px; margin: auto; background-color: #f9f9f9; }
            .container { background-color: #fff; padding: 25px; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
            h2 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
            .controls { margin: 20px 0; padding: 15px; background-color: #f0f3f5; border-radius: 8px; }
            .controls label { margin-right: 10px; vertical-align: middle; }
            .controls input[type="range"] { vertical-align: middle; width: 120px; }
            .controls input[type="text"] { vertical-align: middle; padding: 4px; border: 1px solid #ccc; border-radius: 4px; }
            #status { margin-top: 15px; font-weight: bold; color: #007bff; }
            #output-area { margin-top: 20px; }
            #output { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 15px; }
            .slice-container { text-align: center; background-color: #f0f3f5; padding: 10px; border-radius: 8px; }
            .slice-container img { max-width: 100%; border: 1px solid #ddd; background-image: linear-gradient(45deg, #eee 25%, transparent 25%), linear-gradient(-45deg, #eee 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #eee 75%), linear-gradient(-45deg, transparent 75%, #eee 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px; }
            .slice-container a { display: block; margin-top: 8px; text-decoration: none; background-color: #007bff; color: white; padding: 5px 10px; border-radius: 5px; font-size: 14px; }
            button { background-color: #28a745; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer; font-size: 16px; }
            #downloadZipBtn { background-color: #17a2b8; }
        </style>
    </head>
    <body>
    
    <div class="container">
        <h2>Автоматическая подготовка стикеров для Telegram</h2>
        <p>Инструмент сам найдет контуры стикеров и отформатирует их в нужный размер 512x512 пикселей.</p>
        
        <input type="file" id="imageLoader" accept="image/png"/>
        
        <div class="controls" style="display: none;">
            <div>
                <label for="padding">Отступ (px):</label>
                <input type="range" id="padding" min="0" max="100" value="0">
                <span id="paddingValue">0</span>
            </div>
            <div style="margin-top: 10px;">
                 <label for="baseFilename">Базовое имя файла:</label>
                 <input type="text" id="baseFilename" value="sticker_auto_tg">
            </div>
        </div>
    
        <button id="processBtn" style="display: none;">Анализировать и вырезать</button>
        <div id="status"></div>
        
        <div id="output-area">
            <button id="downloadZipBtn" style="display: none;">Скачать все (.zip)</button>
            <div id="output"></div>
        </div>
    </div>
    
    <script>
        const imageLoader = document.getElementById('imageLoader');
        const controls = document.querySelector('.controls');
        const processBtn = document.getElementById('processBtn');
        const downloadZipBtn = document.getElementById('downloadZipBtn');
        const baseFilenameInput = document.getElementById('baseFilename');
        const paddingSlider = document.getElementById('padding');
        const paddingValueSpan = document.getElementById('paddingValue');
        const statusDiv = document.getElementById('status');
        const outputDiv = document.getElementById('output');
    
        let originalImage = null;
        let imageData = null;
    
        imageLoader.addEventListener('change', e => {
            const reader = new FileReader();
            reader.onload = event => {
                originalImage = new Image();
                originalImage.onload = () => {
                    const mainCanvas = document.createElement('canvas');
                    mainCanvas.width = originalImage.width;
                    mainCanvas.height = originalImage.height;
                    const mainCtx = mainCanvas.getContext('2d');
                    mainCtx.drawImage(originalImage, 0, 0);
                    imageData = mainCtx.getImageData(0, 0, mainCanvas.width, mainCanvas.height);
                    
                    controls.style.display = 'block';
                    processBtn.style.display = 'inline-block';
                    outputDiv.innerHTML = '';
                    downloadZipBtn.style.display = 'none';
                    statusDiv.innerText = '';
                };
                originalImage.src = event.target.result;
            };
            reader.readAsDataURL(e.target.files[0]);
        });
        
        paddingSlider.addEventListener('input', e => {
            paddingValueSpan.innerText = e.target.value;
        });
    
        processBtn.addEventListener('click', () => {
            if (!originalImage) return;
            
            outputDiv.innerHTML = '';
            downloadZipBtn.style.display = 'none';
            statusDiv.innerText = 'Анализ изображения... Это может занять несколько секунд.';
    
            setTimeout(processImage, 50);
        });
    
        function processImage() {
            const cols = 3, rows = 3;
            const cellWidth = imageData.width / cols;
            const cellHeight = imageData.height / rows;
            let stickerCount = 0;
            const padding = parseInt(paddingSlider.value, 10);
            const visited = new Set();
    
            for (let r = 0; r < rows; r++) {
                for (let c = 0; c < cols; c++) {
                    const cellX = c * cellWidth;
                    const cellY = r * cellHeight;
                    
                    let sumX = 0, sumY = 0, count = 0;
                    for (let y = 0; y < cellHeight; y++) {
                        for (let x = 0; x < cellWidth; x++) {
                            const globalX = Math.floor(cellX + x);
                            const globalY = Math.floor(cellY + y);
                            const alpha = imageData.data[(globalY * imageData.width + globalX) * 4 + 3];
                            if (alpha > 50) {
                                sumX += globalX;
                                sumY += globalY;
                                count++;
                            }
                        }
                    }
    
                    if (count > 0) {
                        const startX = Math.floor(sumX / count);
                        const startY = Math.floor(sumY / count);
                        
                        if (visited.has(startY * imageData.width + startX)) continue;
    
                        const bounds = findStickerBoundsBFS(startX, startY, visited);
    
                        if (bounds) {
                            stickerCount++;
                            const cropX = bounds.minX - padding;
                            const cropY = bounds.minY - padding;
                            const cropWidth = (bounds.maxX - bounds.minX) + 1 + (padding * 2);
                            const cropHeight = (bounds.maxY - bounds.minY) + 1 + (padding * 2);
    
                            const tempCanvas = document.createElement('canvas');
                            tempCanvas.width = cropWidth;
                            tempCanvas.height = cropHeight;
                            const tempCtx = tempCanvas.getContext('2d');
                            
                            tempCtx.drawImage(originalImage, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
                            
                            // --- НОВЫЙ БЛОК: ФОРМАТИРОВАНИЕ ДЛЯ TELEGRAM ---
                            const finalCanvas = resizeAndCenterForTelegram(tempCanvas);
                            displayResult(finalCanvas, stickerCount);
                        }
                    }
                }
            }
            
            statusDiv.innerText = `Готово! Найдено ${stickerCount} стикеров.`;
            if (stickerCount > 0) downloadZipBtn.style.display = 'inline-block';
        }
    
        function resizeAndCenterForTelegram(sourceCanvas) {
            const TELEGRAM_SIZE = 512;
            const finalCanvas = document.createElement('canvas');
            finalCanvas.width = TELEGRAM_SIZE;
            finalCanvas.height = TELEGRAM_SIZE;
            const finalCtx = finalCanvas.getContext('2d');
    
            const sourceWidth = sourceCanvas.width;
            const sourceHeight = sourceCanvas.height;
    
            const scaleFactor = Math.min(TELEGRAM_SIZE / sourceWidth, TELEGRAM_SIZE / sourceHeight);
            
            const newWidth = sourceWidth * scaleFactor;
            const newHeight = sourceHeight * scaleFactor;
            
            const drawX = (TELEGRAM_SIZE - newWidth) / 2;
            const drawY = (TELEGRAM_SIZE - newHeight) / 2;
    
            finalCtx.drawImage(sourceCanvas, drawX, drawY, newWidth, newHeight);
            
            return finalCanvas;
        }
    
        function findStickerBoundsBFS(startX, startY, visited) {
            const queue = [[startX, startY]];
            const bounds = { minX: startX, minY: startY, maxX: startX, maxY: startY };
            const startIndex = startY * imageData.width + startX;
            if (visited.has(startIndex)) return null;
            visited.add(startIndex);
    
            while (queue.length > 0) {
                const [x, y] = queue.shift();
                bounds.minX = Math.min(bounds.minX, x);
                bounds.maxX = Math.max(bounds.maxX, x);
                bounds.minY = Math.min(bounds.minY, y);
                bounds.maxY = Math.max(bounds.maxY, y);
                
                const neighbors = [[x, y - 1], [x, y + 1], [x - 1, y], [x + 1, y]];
                for (const [nx, ny] of neighbors) {
                    if (nx >= 0 && nx < imageData.width && ny >= 0 && ny < imageData.height) {
                        const nIndex = ny * imageData.width + nx;
                        if (!visited.has(nIndex)) {
                            visited.add(nIndex);
                            const alpha = imageData.data[nIndex * 4 + 3];
                            if (alpha > 50) queue.push([nx, ny]);
                        }
                    }
                }
            }
            return bounds;
        }
        
        function displayResult(canvas, count) {
            const container = document.createElement('div');
            container.className = 'slice-container';
            const imgElement = document.createElement('img');
            const dataUrl = canvas.toDataURL('image/png');
            imgElement.src = dataUrl;
            
            const link = document.createElement('a');
            link.href = dataUrl;
            const baseName = baseFilenameInput.value.trim() || 'sticker_auto_tg';
            link.download = `${baseName}_${count}.png`;
            link.innerText = 'Скачать';
            
            container.appendChild(imgElement);
            container.appendChild(link);
            outputDiv.appendChild(container);
        }
        
        downloadZipBtn.addEventListener('click', async () => {
            const zip = new JSZip();
            downloadZipBtn.innerText = 'Архивация...';
            
            document.querySelectorAll('#output .slice-container').forEach(slice => {
                const img = slice.querySelector('img');
                const link = slice.querySelector('a');
                const base64Data = img.src.split(',')[1];
                zip.file(link.download, base64Data, {base64: true});
            });
            
            const baseName = baseFilenameInput.value.trim() || 'sticker_auto_tg';
            const content = await zip.generateAsync({type:"blob"});
            const link = document.createElement('a');
            link.href = URL.createObjectURL(content);
            link.download = `${baseName}_archive.zip`;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            downloadZipBtn.innerText = 'Скачать все (.zip)';
        });
    </script>
    
    </body>
    </html>
    Скрытый текст
    <!DOCTYPE html>
    <html lang="ru">
    <head>
        <meta charset="UTF-8">
        <title>Обводка и подготовка стикеров для Telegram</title>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
        <style>
            body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; padding: 20px; max-width: 900px; margin: auto; background-color: #f9f9f9; }
            .container { background-color: #fff; padding: 25px; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
            h2 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
            .controls { margin: 20px 0; padding: 15px; background-color: #f0f3f5; border-radius: 8px; display: none; }
            .controls label { margin-right: 10px; vertical-align: middle; }
            .controls input[type="text"] { vertical-align: middle; padding: 4px; border: 1px solid #ccc; border-radius: 4px; }
            canvas { border: 2px dashed #ccc; cursor: crosshair; max-width: 100%; display: block; margin-top: 15px; }
            #output-area { margin-top: 20px; }
            #output { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 15px; }
            .slice-container { text-align: center; background-color: #f0f3f5; padding: 10px; border-radius: 8px; }
            .slice-container img { max-width: 100%; border: 1px solid #ddd; background-image: linear-gradient(45deg, #eee 25%, transparent 25%), linear-gradient(-45deg, #eee 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #eee 75%), linear-gradient(-45deg, transparent 75%, #eee 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px; }
            .slice-container a { display: block; margin-top: 8px; text-decoration: none; background-color: #007bff; color: white; padding: 5px 10px; border-radius: 5px; font-size: 14px; }
            button { background-color: #6c757d; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer; font-size: 16px; margin-right: 10px; }
            #downloadZipBtn { background-color: #17a2b8; }
        </style>
    </head>
    <body>
    
    <div class="container">
        <h2>Подготовка стикеров для Telegram</h2>
        <p>Обведите стикер и замкните контур. Результат автоматически форматируется в 512x512 пикселей для Telegram.</p>
        
        <input type="file" id="imageLoader" accept="image/png"/>
        
        <div class="controls">
            <button id="undoBtn">Отменить точку</button>
            <button id="clearBtn">Очистить выделение</button>
            <label for="baseFilename">Базовое имя файла:</label>
            <input type="text" id="baseFilename" value="sticker_tg">
        </div>
    
        <canvas id="canvas"></canvas>
        
        <div id="output-area">
            <button id="downloadZipBtn" style="display: none;">Скачать все (.zip)</button>
            <div id="output"></div>
        </div>
    </div>
    
    <script>
        const imageLoader = document.getElementById('imageLoader');
        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');
        const controls = document.querySelector('.controls');
        const undoBtn = document.getElementById('undoBtn');
        const clearBtn = document.getElementById('clearBtn');
        const downloadZipBtn = document.getElementById('downloadZipBtn');
        const baseFilenameInput = document.getElementById('baseFilename');
    
        let originalImage = null;
        let points = [];
        let isDrawing = false;
        let stickerCount = 0;
        const SNAP_DISTANCE = 15;
    
        imageLoader.addEventListener('change', e => {
            const reader = new FileReader();
            reader.onload = event => {
                originalImage = new Image();
                originalImage.onload = () => {
                    canvas.width = originalImage.width;
                    canvas.height = originalImage.height;
                    controls.style.display = 'block';
                    clearSelection();
                    document.getElementById('output').innerHTML = '';
                    downloadZipBtn.style.display = 'none';
                    stickerCount = 0;
                    draw();
                };
                originalImage.src = event.target.result;
            };
            reader.readAsDataURL(e.target.files[0]);
        });
    
        function draw() {
            if (!originalImage) return;
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            ctx.drawImage(originalImage, 0, 0);
    
            if (points.length === 0) return;
    
            ctx.strokeStyle = 'rgba(0, 255, 255, 0.9)';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(points[0].x, points[0].y);
            for (let i = 1; i < points.length; i++) {
                ctx.lineTo(points[i].x, points[i].y);
            }
            ctx.stroke();
    
            ctx.fillStyle = 'rgba(0, 255, 255, 0.9)';
            points.forEach(p => {
                ctx.beginPath();
                ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
                ctx.fill();
            });
    
            const mousePos = getCurrentMousePos(event);
            if (mousePos && points.length > 2) {
                const dist = Math.hypot(mousePos.x - points[0].x, mousePos.y - points[0].y);
                if (dist < SNAP_DISTANCE) {
                    ctx.fillStyle = 'rgba(255, 0, 0, 1)';
                    ctx.beginPath();
                    ctx.arc(points[0].x, points[0].y, 8, 0, Math.PI * 2);
                    ctx.fill();
                }
            }
        }
    
        let currentMousePos = null;
        function getCurrentMousePos(evt) {
            if (!evt) return currentMousePos;
            const rect = canvas.getBoundingClientRect();
            const scaleX = canvas.width / rect.width;
            const scaleY = canvas.height / rect.height;
            currentMousePos = {
                x: (evt.clientX - rect.left) * scaleX,
                y: (evt.clientY - rect.top) * scaleY
            };
            return currentMousePos;
        }
    
        canvas.addEventListener('mousemove', e => {
            getCurrentMousePos(e);
            if (isDrawing) {
                points.push(getCurrentMousePos(e));
            }
            draw();
        });
    
        canvas.addEventListener('mousedown', e => {
            const pos = getCurrentMousePos(e);
            if (points.length > 2) {
                const dist = Math.hypot(pos.x - points[0].x, pos.y - points[0].y);
                if (dist < SNAP_DISTANCE) {
                    cutSelection();
                    return;
                }
            }
            isDrawing = true;
            points.push(pos);
            draw();
        });
    
        canvas.addEventListener('mouseup', () => { isDrawing = false; });
        undoBtn.addEventListener('click', () => { points.pop(); draw(); });
        clearBtn.addEventListener('click', clearSelection);
    
        function clearSelection() {
            points = [];
            draw();
        }
    
        function cutSelection() {
            if (points.length < 3) return;
            
            const bounds = {
                minX: Math.min(...points.map(p => p.x)),
                minY: Math.min(...points.map(p => p.y)),
                maxX: Math.max(...points.map(p => p.x)),
                maxY: Math.max(...points.map(p => p.y))
            };
    
            const width = bounds.maxX - bounds.minX;
            const height = bounds.maxY - bounds.minY;
    
            const tempCanvas = document.createElement('canvas');
            tempCanvas.width = width;
            tempCanvas.height = height;
            const tempCtx = tempCanvas.getContext('2d');
    
            tempCtx.save();
            tempCtx.beginPath();
            tempCtx.moveTo(points[0].x - bounds.minX, points[0].y - bounds.minY);
            for (let i = 1; i < points.length; i++) {
                tempCtx.lineTo(points[i].x - bounds.minX, points[i].y - bounds.minY);
            }
            tempCtx.closePath();
            tempCtx.clip();
            
            tempCtx.drawImage(originalImage, -bounds.minX, -bounds.minY);
            tempCtx.restore();
            
            // --- НОВЫЙ БЛОК: ФОРМАТИРОВАНИЕ ДЛЯ TELEGRAM ---
            const finalCanvas = resizeAndCenterForTelegram(tempCanvas);
            
            stickerCount++;
            displayResult(finalCanvas, stickerCount);
            clearSelection();
        }
        
        function resizeAndCenterForTelegram(sourceCanvas) {
            const TELEGRAM_SIZE = 512;
            const finalCanvas = document.createElement('canvas');
            finalCanvas.width = TELEGRAM_SIZE;
            finalCanvas.height = TELEGRAM_SIZE;
            const finalCtx = finalCanvas.getContext('2d');
    
            const sourceWidth = sourceCanvas.width;
            const sourceHeight = sourceCanvas.height;
    
            const scaleFactor = Math.min(TELEGRAM_SIZE / sourceWidth, TELEGRAM_SIZE / sourceHeight);
            
            const newWidth = sourceWidth * scaleFactor;
            const newHeight = sourceHeight * scaleFactor;
    
            // Центрируем изображение
            const drawX = (TELEGRAM_SIZE - newWidth) / 2;
            const drawY = (TELEGRAM_SIZE - newHeight) / 2;
    
            finalCtx.drawImage(sourceCanvas, drawX, drawY, newWidth, newHeight);
            
            return finalCanvas;
        }
    
        function displayResult(resCanvas, count) {
            const output = document.getElementById('output');
            const container = document.createElement('div');
            container.className = 'slice-container';
            const imgElement = document.createElement('img');
            const dataUrl = resCanvas.toDataURL('image/png');
            imgElement.src = dataUrl;
            
            const link = document.createElement('a');
            link.href = dataUrl;
            const baseName = baseFilenameInput.value.trim() || 'sticker_tg';
            link.download = `${baseName}_${count}.png`;
            link.innerText = 'Скачать';
            
            container.appendChild(imgElement);
            container.appendChild(link);
            output.appendChild(container);
            downloadZipBtn.style.display = 'inline-block';
        }
    
        downloadZipBtn.addEventListener('click', async () => {
            const zip = new JSZip();
            downloadZipBtn.innerText = 'Архивация...';
            
            document.querySelectorAll('#output .slice-container').forEach(slice => {
                const img = slice.querySelector('img');
                const link = slice.querySelector('a');
                const base64Data = img.src.split(',')[1];
                zip.file(link.download, base64Data, {base64: true});
            });
            
            const baseName = baseFilenameInput.value.trim() || 'sticker_tg';
            const content = await zip.generateAsync({type:"blob"});
            const link = document.createElement('a');
            link.href = URL.createObjectURL(content);
            link.download = `${baseName}_archive.zip`;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            downloadZipBtn.innerText = 'Скачать все (.zip)';
        });
    
    </script>
    
    </body>
    </html>