#
# @@@@@@ @@@@@@ @@@@@@@ @@@@@@@ @@@@@@ @@@@@@@@@@ @@@@@@ @@@@@@@ @@@ @@@ @@@ @@@@@@@@ @@@@@@
# @@@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@@ @@@@@@@@ @@@@@@@@@@@ @@@@@@@@ @@@@@@@@ @@@ @@@ @@@ @@@@@@@@ @@@@@@@
# @@! @@@ !@@ @@! @@! @@@ @@! @@@ @@! @@! @@! @@! @@@ @@! @@@ @@! @@@ @@! @@! !@@
# !@! @!@ !@! !@! !@! @!@ !@! @!@ !@! !@! !@! !@! @!@ !@! @!@ !@! @!@ !@! !@! !@!
# @!@!@!@! !!@@!! @!! @!@!!@! @!@ !@! @!! !!@ @!@ @!@ !@! @!@ !@! @!@ !@! @!! @!!!:! !!@@!!
# !!!@!!!! !!@!!! !!! !!@!@! !@! !!! !@! ! !@! !@! !!! !@! !!! !@! !!! !!! !!!!!: !!@!!!
# !!: !!! !:! !!: !!: :!! !!: !!! !!: !!: !!: !!! !!: !!! !!: !!! !!: !!: !:!
# :!: !:! !:! :!: :!: !:! :!: !:! :!: :!: :!: !:! :!: !:! :!: !:! :!: :!: !:!
# :: ::: :::: :: :: :: ::: ::::: :: ::: :: ::::: :: :::: :: ::::: :: :: :::: :: :::: :::: ::
# : : : :: : : : : : : : : : : : : : : :: : : : : : : :: : : : :: :: :: : :
#
# © Copyright 2024
#
# https://t.me/Den4ikSuperOstryyPer4ik
# and
# https://t.me/ToXicUse
#
# 🔒 Licensed under the GNU AGPLv3
# https://www.gnu.org/licenses/agpl-3.0.html
#
# meta developer: @AstroModules
# meta banner: https://raw.githubusercontent.com/Den4ikSuperOstryyPer4ik/Astro-modules/main/Banners/Demotivator.jpg
# requires: pillow
import io
import os
import re
from typing import Optional
import requests
from hikkatl.tl.types import MessageMediaDocument, MessageMediaPhoto
from PIL import Image, ImageColor, ImageDraw, ImageFont, ImageOps
from .. import loader, utils
MIME_TYPES = ["webp", "png", "jpeg", "jpg", "bmp", "dds", "dib", "eps", "ico", "tiff"]
class FontValidator(loader.validators.Validator):
"""Valid link to file with font"""
def __init__(self):
super().__init__(
self._validate,
{
"en": "link to file with font",
"ru": "ссылкой на файл со шрифтом"
}
)
@staticmethod
def _validate(value) -> str:
if not isinstance(value, str):
raise loader.validators.ValidationError("Value must be a string - URL(Link) to file with font.")
try:
if not value.endswith(".ttf") or not utils.check_url(value):
raise Exception("Invalid URL")
except Exception:
raise loader.validators.ValidationError(f"Passed value ({value}) is not a valid URL to file with font.")
return value
class ColorValidator(loader.validators.Validator):
"""Valid string color (for PIL.ImageColor)"""
def __init__(self):
super().__init__(
self._validate,
{
"en": "color (red/blue/green/...)",
"ru": "цветом (red/blue/green/...)",
}
)
@staticmethod
def _validate(value) -> str:
if not isinstance(value, str):
raise loader.validators.ValidationError("Value must be a string - valid color")
try:
_ = ImageColor.getcolor(value, "RGBA")
except Exception:
raise loader.validators.ValidationError(f"Passed value ({value}) is not a valid color")
return value
@loader.tds
class DemotivatorMod(loader.Module):
'''Demotivate picture with text, arguments and config.'''
strings = {
"name": "Demotivator",
"require_photo": "Reply with a photo, attach it to the team.",
"require_text": "Text required!",
"require_args": "Args required!",
"error": "An error occurred...",
"success": "Result:",
"demotivation": "Demotivation, please wait...",
"watermark_cfg": "Default watermark.",
"font_color_cfg": "Default text font color.",
"fill_color_cfg": "Default background color",
"font_name_cfg": "Link to font file (.ttf, not .zip)",
"top_size_cfg": "Default top text size.",
"bottom_size_cfg": "Default additional (bottom) text size.",
"arrange_cfg": "Adjust photo frames or not",
}
strings_ru = {
"require_photo": "Ответьте на фото, приложите его к команде.",
"require_text": "Необходим текст!",
"require_args": "Необходимы аргументы!",
"error": "Произошла ошибка...",
"success": "Результат:",
"demotivation": "Демотивация, подождите, пожалуйста...",
"watermark_cfg": "Водяной знак по умолчанию.",
"font_color_cfg": "Цвет шрифта текста по умолчанию.",
"fill_color_cfg": "Цвет фона по умолчанию",
"font_name_cfg": "Ссылка на файл со шрифтом (.ttf, не .zip)",
"top_size_cfg": "Размер главного текста по умолчанию.",
"bottom_size_cfg": "Размер дополнительного (нижнего) текста по умолчанию.",
"arrange_cfg": "Регулировать рамки под фотографию или нет",
"_cls_doc": "Демотивировает картинку по параметрам(текст, аргументы и конфиг)."
}
def __init__(self) -> None:
self.config = loader.ModuleConfig(
loader.ConfigValue(
"watermark",
"@AstroModules",
lambda: self.strings("watermark_cfg"),
validator=loader.validators.String(),
),
loader.ConfigValue(
"font_color",
"white",
lambda: self.strings("font_color_cfg"),
validator=ColorValidator(),
),
loader.ConfigValue(
"fill_color",
"black",
lambda: self.strings("fill_color_cfg"),
validator=ColorValidator(),
),
loader.ConfigValue(
"font_name_link",
"https://0x0.st/HHyo.ttf",
lambda: self.strings("font_name_cfg"),
validator=FontValidator(),
),
loader.ConfigValue(
"top_size",
80,
lambda: self.strings("top_size_cfg"),
validator=loader.validators.Integer(minimum=10),
),
loader.ConfigValue(
"bottom_size",
60,
lambda: self.strings("bottom_size_cfg"),
validator=loader.validators.Integer(minimum=10),
),
loader.ConfigValue(
"arrange",
True,
lambda: self.strings("arrange_cfg"),
validator=loader.validators.Boolean(),
),
)
def parse_args(self, text: str):
args = text.replace("\n", " ").split(" ")
text = " " + text
parsed = {}
for arg in args:
try:
if arg in ["-bottom-text", "-btm-text", "-bottom"]:
parsed["bottom_text"] = args[args.index(arg)+1]
elif arg in ["-wt", "-watermark"]:
parsed["watermark"] = args[args.index(arg)+1]
elif arg in ["-font-color", "-ftc"]:
parsed["font_color"] = args[args.index(arg)+1]
elif arg in ["-fill-color", "-flc"]:
parsed["fill_color"] = args[args.index(arg)+1]
elif arg in ["-font-name", "-font", "-font-link"]:
parsed["font_name"] = args[args.index(arg)+1]
elif arg in ["-top-size", "-tpsz", "-topsize"]:
parsed["top_size"] = args[args.index(arg)+1]
elif arg in ["-bottom-size", "-btmsz"]:
parsed["bottom_size"] = args[args.index(arg)+1]
elif arg in ["-arrange", "-arr"]:
parsed["arrange"] = True
text = text.replace(f" {arg}", "")
continue
else:
continue
text = text.replace(f" {arg} {args[args.index(arg)+1]}", "")
except IndexError:
pass
parsed["top_text"] = text
return parsed
async def download_media(self, message):
media = None
msg = None
if message.media:
media, msg = message.media, message
elif (reply := await message.get_reply_message()) and reply.media:
media, msg = reply.media, reply
if not (media and msg) or not isinstance(media, (MessageMediaDocument, MessageMediaPhoto)):
return False
if (isinstance(media, MessageMediaDocument) and media.document) and (not (image := re.match(r"image/(.*)", media.document.mime_type)) or image.group(1) not in MIME_TYPES):
return False
return await msg.download_media()
def create_demot(self,
top_text: str = "",
bottom_text: str = "",
*,
file: str,
watermark: Optional[str] = None,
result_filename: str,
font_color: str = 'white',
fill_color: str = 'black',
font_name,
top_size: int = 80,
bottom_size: int = 60,
arrange: bool = False
):
"""
This method in https://github.com/Infqq/simpledemotivators/blob/main/simpledemotivators/Demotivator.py
Author: Infqq
GitHub Repo: https://github.com/Infqq/simpledemotivators/
"""
if arrange:
user_img = Image.open(file).convert("RGBA")
(width, height) = user_img.size
img = Image.new('RGB', (width + 250, height + 260), color=fill_color)
img_border = Image.new('RGB', (width + 10, height + 10), color='#000000')
border = ImageOps.expand(img_border, border=2, fill='#ffffff')
img.paste(border, (111, 96))
img.paste(user_img, (118, 103))
drawer = ImageDraw.Draw(img)
else:
img = Image.new('RGB', (1280, 1024), color=fill_color)
img_border = Image.new('RGB', (1060, 720), color='#000000')
border = ImageOps.expand(img_border, border=2, fill='#ffffff')
user_img = Image.open(file).convert("RGBA").resize((1050, 710))
(width, height) = user_img.size
img.paste(border, (111, 96))
img.paste(user_img, (118, 103))
drawer = ImageDraw.Draw(img)
font_1 = ImageFont.truetype(font=font_name(), size=top_size, encoding='UTF-8')
text_width = font_1.getsize(top_text)[0]
while text_width >= (width + 250) - 20:
font_1 = ImageFont.truetype(font=font_name(), size=top_size, encoding='UTF-8')
text_width = font_1.getsize(top_text)[0]
top_size -= 1
font_2 = ImageFont.truetype(font=font_name(), size=bottom_size, encoding='UTF-8')
text_width = font_2.getsize(bottom_text)[0]
while text_width >= (width + 250) - 20:
font_2 = ImageFont.truetype(font=font_name(), size=bottom_size, encoding='UTF-8')
text_width = font_2.getsize(bottom_text)[0]
bottom_size -= 1
size_1 = drawer.textsize(top_text, font=font_1)
size_2 = drawer.textsize(bottom_text, font=font_2)
if arrange:
drawer.text((((width + 250) - size_1[0]) / 2, ((height + 190) - size_1[1])), top_text, fill=font_color, font=font_1)
drawer.text((((width + 250) - size_2[0]) / 2, ((height + 235) - size_2[1])), bottom_text, fill=font_color, font=font_2)
else:
drawer.text(((1280 - size_1[0]) / 2, 840), top_text, fill=font_color, font=font_1)
drawer.text(((1280 - size_2[0]) / 2, 930), bottom_text, fill=font_color, font=font_2)
if watermark:
(width, height) = img.size
idraw = ImageDraw.Draw(img)
idraw.line((1000 - len(watermark) * 5, 817, 1008 + len(watermark) * 5, 817), fill=0, width=4)
font_2 = ImageFont.truetype(font=font_name(), size=20, encoding='UTF-8')
size_2 = idraw.textsize(watermark.lower(), font=font_2)
idraw.text((((width + 729) - size_2[0]) / 2, ((height - 192) - size_2[1])),
watermark.lower(), font=font_2)
img.save(result_filename)
os.remove(file)
return result_filename
async def demotivate_pic(self, args: dict):
result_path = "/tmp/_demoted_" + args["file"]
font_name = args.get("font_name", self.config["font_name_link"])
font_resp = await utils.run_sync(requests.get, font_name)
def _get_font():
font = io.BytesIO(font_resp.content)
font.name = font_name.split("/")[-1]
return font
result = self.create_demot(
top_text=args["top_text"],
bottom_text=args.get("bottom_text", ''),
file=args["file"],
watermark=args.get("watermark", self.config["watermark"]) if not args.get("arrange", self.config["arrange"]) else None,
result_filename=result_path,
font_color=args.get("font_color", self.config["font_color"]),
font_name=_get_font,
fill_color=args.get("fill_color", self.config["fill_color"]),
top_size=int(args.get("top_size", self.config["top_size"])),
bottom_size=int(args.get("bottom_size", self.config["bottom_size"])),
arrange=args.get("arrange", self.config["arrange"]),
)
return result_path if result else ""
@loader.command(
ru_doc="""<текст>
[-bottom/-btm-text/-bottom-text <текст> - доп. текст внизу]
[-wt/-watermark <текст> - добавить водяной знак]
[-font-color/-ftc <цвет> (red/while/blue/yellow/...) - цвет шрифта (по дефолту white)]
[-fill-color/-flc <цвет> (red/while/blue/yellow/...) - цвет заднего фона (по дефолту black)]
[-font/-font-name/-font-link <ссылка на файл со шрифтами> (не zip, а ttf) - шрифт для текста]
[-top-size/-topsize/-tpsz <размер> (по дефолту 80) - размер главного текста]
[-bottom-size/-btmsz <размер> (по дефолту 60) - размер доп.(нижнего) текста]
[-arrange - регулировать рамки под фотографию]
- демотивировать картинку по заданному тексту и аргументам
""",
alias="demot",
)
async def demotivate(self, message):
"""
[-bottom/-btm-text/-bottom-text - add. text below]
[-wt/-watermark - add watermark]
[-font-color/-ftc (red/while/blue/yellow/...) - font color (white by default)]
[-fill-color/-flc (red/while/blue/yellow/...) - background color (black by default)]
[-font/-font-name/-font-link (not zip, but ttf) - font for text]
[-top-size/-topsize/-tpsz (default 80) - main text size]
[-bottom-size/-btmsz (default 60) - extra size text]
[-arrange - adjust photo frames]
- demotivate a picture according to the given text and arguments
"""
if not (args := utils.get_args_raw(message)):
return await utils.answer(message, self.strings("require_args"))
m = await utils.answer(message, self.strings("demotivation"))
args = self.parse_args(args)
media = ''
if not args:
return await utils.answer(m, self.strings("require_args"))
elif not args.get("top_text", None):
return await utils.answer(m, self.strings("require_text"))
if not (media := await self.download_media(message)):
return await utils.answer(m, self.strings("require_photo"))
args["file"] = media
demoted = await self.demotivate_pic(args)
if not demoted:
return await utils.answer(m, self.strings("error"))
await utils.answer_file(m, demoted, self.strings("success"), reply_to=(await message.get_reply_message()))
os.remove(demoted)