#░░░███░███░███░███░███ #░░░░░█░█░░░░█░░█░░░█░█ #░░░░█░░███░░█░░█░█░█░█ #░░░█░░░█░░░░█░░█░█░█░█ #░░░███░███░░█░░███░███ # meta developer: @ZetGo # scope: hikka_min 2.0.0 # requires: Pillow git+https://github.com/ZetGoHack/TStickers.git __version__ = (1, 3, 2) import io import math import re # from tstickers import convert from PIL import Image, ImageDraw from herokutl.tl.custom import Message from herokutl.tl.functions.messages import GetCustomEmojiDocumentsRequest from herokutl.tl.functions.payments import GetUniqueStarGiftRequest from herokutl.tl.functions.help import ( GetPeerProfileColorsRequest, ) from herokutl.tl.types import ( Channel, EmojiStatusCollectible, MessageMediaWebPage, StarGiftAttributeBackdrop, ) from herokutl.tl.types.payments import ( UniqueStarGift, ) from .. import loader, utils SHAPES = { # TODO: фигуры для создания масок на авы } BBOX_TGA = ( 2894 / 8268, 1260 / 8268, 2504 / 8268, 2504 / 8268, ) BBOX_IOS = ( 2590 / 8268, 629 / 8268, 3120 / 8268, 3120 / 8268, ) # BBOX_TGD = ( # 4779 / 16000, # 4779 / 16000, # 7728 / 16000, # 7728 / 16000, # ) DEFAULT_PP_SIZE = 2048 # no need to use a bigger size since Telegram will compress it anyway # better than overloading the script with large images RE_ONLY_ONE_EMOJI = re.compile(r"^[^<]+$|^[^<]+$") def resize_image(image: Image.Image, max_size: int = DEFAULT_PP_SIZE) -> Image.Image: w, h = image.size if max(w, h) <= max_size: return image else: scale = max_size / max(w, h) new_w = int(w * scale) new_h = int(h * scale) return image.resize((new_w, new_h), Image.LANCZOS) # Source: https://gist.github.com/weihanglo/1e754ec47fdd683a42fdf6a272904535#file-draw_gradient_pillow-py def get_gradient(size: tuple, color1: tuple, color2: tuple, gradient_type: str = "linear") -> Image.Image: def interpolate(f_co, t_co, interval): if interval <= 1: yield list(t_co) return det_co = [(t - f) / (interval - 1) for f, t in zip(f_co, t_co)] for i in range(interval): yield [round(f + det * i) for f, det in zip(f_co, det_co)] gradient = Image.new('RGB', size, color=(0, 0, 0)) draw = ImageDraw.Draw(gradient) if gradient_type == "linear": bottom_color, top_color = color1, color2 for y, color in enumerate(interpolate(top_color, bottom_color, max(1, size[1]))): draw.line([(0, y), (size[0], y)], fill=tuple(color), width=1) elif gradient_type == "radial": center_color, edge_color = color1, color2 max_radius = math.hypot(size[0], size[1]) / 2.0 interval = max(1, int(math.ceil(max_radius)) + 1) colors = list(interpolate(center_color, edge_color, interval)) cx = size[0] / 2 cy = size[1] / 2 for r_index, color in enumerate(colors): r = interval - 1 - r_index if r < 0: continue bbox = [ int(round(cx - r)), int(round(cy - r)), int(round(cx + r)), int(round(cy + r)) ] draw.ellipse(bbox, fill=tuple(color)) return gradient # Source: https://github.com/TelegramMessenger/Telegram-iOS/blob/master/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift#L68-L615 def _add_glow(image: Image.Image, bbox: tuple) -> Image.Image: img = image.convert("RGBA") size = img.size[0] bx, by, bw, bh = bbox cx = bx * size + bw * size * 0.5 cy = by * size + bh * size * 0.5 radius = (bh * size / 2) * (300 / 104) mask = Image.new("L", (size, size), 0) draw = ImageDraw.Draw(mask) for i in range(100): step = 1.0 - i / (100 - 1) alpha = 0.4 * pow(1.0 - step, 2) alpha_value = int(round(max(0, min(1, alpha)) * 255)) _radius = radius * step bbox_pix = [ int(round(cx - _radius)), int(round(cy - _radius)), int(round(cx + _radius)), int(round(cy + _radius)) ] if alpha_value > 0: draw.ellipse(bbox_pix, fill=alpha_value) white = Image.new("RGBA", (size, size), (255, 255, 255, 0)) white.putalpha(mask) result = Image.alpha_composite(img, white) return result.convert("RGBA") def set_gradient(img: Image.Image, gradient: Image.Image, scale: int = 100) -> io.BytesIO: grad_size = DEFAULT_PP_SIZE gradient = gradient.resize((grad_size,) * 2, Image.LANCZOS).convert('RGBA') target_size = grad_size * scale / 100 img_w, img_h = img.size img_max = max(img_w, img_h) fit_size = min(target_size, img_max * 4) scale_factor = fit_size / img_max new_w = max(1, int(round(img_w * scale_factor))) new_h = max(1, int(round(img_h * scale_factor))) resampling = Image.LANCZOS if scale_factor <= 1 else Image.BICUBIC img = img.resize((new_w, new_h), resampling) left = (grad_size - new_w) // 2 top = (grad_size - new_h) // 2 gradient.paste(img, (left, top), img) buffer = io.BytesIO() gradient.save(buffer, format='PNG') buffer.seek(0) return buffer def crop_by_bbox(img: Image.Image, bbox: tuple): img_w, img_h = img.size x, y, w, h = bbox left = int(round(x * img_w)) top = int(round(y * img_h)) right = int(round((x + w) * img_w)) bottom = int(round((y + h) * img_h)) return img.crop((left, top, right, bottom)) def hex_to_rgb(value: int): return ((value >> 16) & 255, (value >> 8) & 255, value & 255) def hexes_to_rgbs(value: list): if len(value) > 1: res = list() for i in value: res.append(hex_to_rgb(i)) return tuple(res) else: res = hex_to_rgb(value[0]) return (res, res) @loader.tds class Gradientor(loader.Module): strings = { "name": "Gradientor", "_cls_doc": "A module to create your profile picture with a background from your profile\n\nALSO TRY: @gradientorbot!!!", "gradient_creating": "🔁 Creating gradient...", "gradient_created": "✅ Gradient created!", "nft_done": ( "✅ Gradient created from " "gift background!" ), "noargs": "❌ No arguments provided!", "nft_error": ( "❌ Failed to get gift info." "Make sure the link/slug is correct" ), } strings_ru = { "_cls_doc": "Модуль для создания вашей аватарки на фоне из вашего профиля\n\nТАК ЖЕ ПОПРОБУЙТЕ: @gradientorbot!!!", "gradient_creating": "🔁 Создание градиента...", "gradient_created": "✅ Градиент создан!", "nft_done": ( "✅ Градиент создан из фона " "подарка!" ), "noargs": "❌ Не указаны аргументы!", "nft_error": ( "❌ Не удалось получить информацию о подарке." "Убедитесь, что ссылка/slug правильные" ), } async def client_ready(self): self.colors = self.get("PROFILE_COLORS", None) if not self.colors or not self.colors.get("light", None): raw_colors = (await self.client(GetPeerProfileColorsRequest(0))).colors self.colors = { "dark": { str(col.color_id): hexes_to_rgbs(col.dark_colors.bg_colors) for col in raw_colors }, "light": { str(col.color_id): hexes_to_rgbs(col.colors.bg_colors) for col in raw_colors }, } self.set("PROFILE_COLORS", self.colors) async def make_gradient( self, photo: bytes | None, bbox: tuple, color1: int, color2: int, force_linear: bool = False, add_glow: bool = False, _full: bool = False, background_only: bool = True, resize_percent: int = 100, ) -> io.BytesIO: gradient = get_gradient((DEFAULT_PP_SIZE,)*2, color1, color2, "linear" if force_linear else "radial") if add_glow: pass # NO FURTHER DEVELOPMENT if not _full: gradient = crop_by_bbox(gradient, bbox) if not background_only and not _full: p_b_io = io.BytesIO(photo) p_b_io.seek(0) img = Image.open(p_b_io).convert('RGBA') result = set_gradient(img, gradient, resize_percent) else: result = io.BytesIO() gradient.save(result, format='PNG') result.seek(0) result.name = "grad @nullmod.png" return result async def _get_photo(self, m: Message): if m.document and "image/" in getattr(m.document, "mime_type", "") and not isinstance(m.media, MessageMediaWebPage): photo = await m.download_media(bytes) elif not m.document and m.message: match = RE_ONLY_ONE_EMOJI.match(m.text.strip()) if match: emoji_id = match.group(1) or match.group(2) try: doc = (await self.client(GetCustomEmojiDocumentsRequest([int(emoji_id)])))[0] if "image/" in getattr(doc, "mime_type", ""): photo = await self.client.download_media(doc, bytes) else: photo = None except Exception: photo = None else: photo = None else: photo = None return photo async def get_photo(self, m: Message, r: Message): photo = None if r: photo = await self._get_photo(r) if not photo: photo = await self._get_photo(m) return photo @loader.command( ru_doc="[фотография/reply/emoji] - создать аватарку с градиентом из цвета профиля\n" "--update-cache - обновить кеш профиля, если вы только что сменили фон профиля\n" "--linear - использовать линейный градиент\n" "--scale [масштаб в процентах] - изменить размер накладываемого фото (по умолчанию 100)\n" "--light - использовать светлую тему\n" "--ios - создать аватарку для iOS-клиентов" ) async def makepp(self, message: Message): """[photo/reply/emoji] - create a profile picture with a gradient from profile color --update-cache - update profile cache if you just changed profile background --linear - use linear gradient --scale [scale in percents] - change the size of the overlaid photo (default 100) --light - use light theme --ios - create a profile picture for iOS clients""" reply: Message = await message.get_reply_message() args = utils.get_args(message) if "--ios" in args: bbox = BBOX_IOS _type = "ios" args.remove("--ios") else: bbox = BBOX_TGA _type = "android" if "--update-cache" in args: upd_cache = True args.remove("--update-cache") else: upd_cache = False if "--linear" in args: force_linear = True args.remove("--linear") else: force_linear = False if "--scale" in args: _scale_indx = args.index("--scale") try: scale = int(args[_scale_indx + 1]) args.pop(_scale_indx + 1) args.pop(_scale_indx) except ValueError: args.remove("--scale") scale = 100 del _scale_indx else: scale = 100 if "--light" in args: theme = "light" args.remove("--light") else: theme = "dark" if "--full" in args: _full = True args.remove("--full") else: _full = False user = None background_only = False add_glow = False if args: user = await self.client.get_entity(int(args[0]) if args[0].isdigit() else args[0]) if not (photo := await self.get_photo(message, reply)): background_only = True if not user: if upd_cache: user = self.client.hikka_me = await self.client.get_me() elif reply: user = reply.sender else: user = self.client.hikka_me if not isinstance(user, Channel) and not user.premium: color1, color2 = (28, 28, 28), (28, 28, 28) elif user.emoji_status and isinstance(user.emoji_status, EmojiStatusCollectible): color1, color2 = ( user.emoji_status.edge_color, user.emoji_status.center_color ) color1 = hex_to_rgb(color1) color2 = hex_to_rgb(color2) elif user.profile_color: color_variant = user.profile_color.color color1, color2 = self.colors.get(theme).get( str(color_variant), ((28, 28, 28), (28, 28, 28)) ) if _type == "ios": add_glow = True force_linear = True else: color1, color2 = (28, 28, 28), (28, 28, 28) await utils.answer(message, self.strings["gradient_creating"]) result = await self.make_gradient( photo, bbox, color1, color2, force_linear, add_glow, _full, background_only, scale, ) await utils.answer(message, self.strings["gradient_created"], file=result, force_document=True) @loader.command(ru_doc="[gift link/slug] - создать аватарку с градиентом из фона nft-подарка") async def nftbg(self, message: Message): """[gift link/slug] - create a profile picture with a gradient from nft gift background""" reply: Message = await message.get_reply_message() args = utils.get_args(message) if "--ios" in args: bbox = BBOX_IOS args.remove("--ios") else: bbox = BBOX_TGA if "--linear" in args: force_linear = True args.remove("--linear") else: force_linear = False if "--scale" in args: _scale_indx = args.index("--scale") try: scale = int(args[_scale_indx + 1]) args.pop(_scale_indx + 1) args.pop(_scale_indx) except ValueError: args.remove("--scale") scale = 100 del _scale_indx else: scale = 100 if "--full" in args: _full = True args.remove("--full") else: _full = False if not args: return await utils.answer(message, self.strings["noargs"]) args = args[0].split("/")[-1] background_only = False try: gift: UniqueStarGift = await self.client(GetUniqueStarGiftRequest(args)) except Exception as e: return await utils.answer(message, self.strings["nft_error"] + "\n" + str(e)) backdrop = next(attr for attr in gift.gift.attributes if isinstance(attr, StarGiftAttributeBackdrop)) color1, color2 = ( backdrop.edge_color, backdrop.center_color ) color1 = hex_to_rgb(color1) color2 = hex_to_rgb(color2) if not (photo := await self.get_photo(message, reply)): background_only = True await utils.answer(message, self.strings["gradient_creating"]) result = await self.make_gradient( photo, bbox, color1, color2, force_linear, False, _full, background_only, scale, ) await utils.answer(message, self.strings["nft_done"].format(args), file=result, force_document=True)