initial commit
@@ -0,0 +1,2 @@
|
|||||||
|
.env
|
||||||
|
.idea
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
import logging
|
||||||
|
import discord
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
from datetime import datetime
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from meme_otron import img_factory as imgf
|
||||||
|
from meme_otron import meme_db as db
|
||||||
|
from meme_otron import utils
|
||||||
|
from meme_otron import main
|
||||||
|
|
||||||
|
VERSION = "1.0-dev"
|
||||||
|
t0 = datetime.now()
|
||||||
|
logging.basicConfig(format="[%(asctime)s][%(levelname)s][%(module)s] %(message)s", level=logging.INFO)
|
||||||
|
|
||||||
|
imgf.load_fonts()
|
||||||
|
db.load_memes()
|
||||||
|
|
||||||
|
# Loading token
|
||||||
|
load_dotenv()
|
||||||
|
token = os.getenv('DISCORD_TOKEN')
|
||||||
|
|
||||||
|
client = discord.Client()
|
||||||
|
|
||||||
|
|
||||||
|
def debug(message, txt):
|
||||||
|
"""
|
||||||
|
Print a log with the context of the current event
|
||||||
|
|
||||||
|
:param (discord.Message) message: message that triggered the event
|
||||||
|
:param (str) txt: text of the log
|
||||||
|
"""
|
||||||
|
logging.info(f"{message.guild} > #{message.channel}: {txt}")
|
||||||
|
|
||||||
|
|
||||||
|
@client.event
|
||||||
|
async def on_ready():
|
||||||
|
"""
|
||||||
|
Called when client is connected
|
||||||
|
"""
|
||||||
|
# Change status
|
||||||
|
await client.change_presence(
|
||||||
|
activity=discord.Game(f"v{VERSION}"),
|
||||||
|
status=discord.Status.online
|
||||||
|
)
|
||||||
|
# Debug connected guilds
|
||||||
|
logging.info(f'{client.user} v{VERSION} has connected to Discord\nto the following guilds:')
|
||||||
|
for guild in client.guilds:
|
||||||
|
logging.info(f'- {guild.name}(id: {guild.id})')
|
||||||
|
|
||||||
|
|
||||||
|
@client.event
|
||||||
|
async def on_message(message):
|
||||||
|
"""
|
||||||
|
Called when a message is sent to any channel on any guild
|
||||||
|
|
||||||
|
:param (discord.Message) message: message sent
|
||||||
|
"""
|
||||||
|
# Ignore self messages
|
||||||
|
if message.author == client.user:
|
||||||
|
return
|
||||||
|
direct = message.channel.type == discord.ChannelType.private
|
||||||
|
if direct or client.user in message.mentions:
|
||||||
|
message.content = re.sub(r'<@[^>]+>', '', message.content).strip()
|
||||||
|
args = utils.parse_arguments(message.content)
|
||||||
|
debug(message, str(args))
|
||||||
|
if len(args) == 0 or args[0].lower().strip() == "help":
|
||||||
|
await message.channel.send(f"Hey {message.author.mention},\n"
|
||||||
|
f"You can generate a meme with the syntax\n"
|
||||||
|
f"```\n"
|
||||||
|
f"[template] \"text 1\" \"text 2\" ...\n"
|
||||||
|
f"```"
|
||||||
|
f"You can find a more detailed help and a list of templates at:\n"
|
||||||
|
f"<https://github.com/klemek/meme-otron/tree/master/discord>")
|
||||||
|
return
|
||||||
|
async with message.channel.typing():
|
||||||
|
left_wmark_text = None
|
||||||
|
if not direct and len(args) > 1:
|
||||||
|
f"By {message.author.display_name}"
|
||||||
|
img = main.compute(*args, left_wmark_text=left_wmark_text)
|
||||||
|
if img is None:
|
||||||
|
await message.channel.send(f"Template `{args[0]}` not found\n"
|
||||||
|
f"You can find a more detailed help and a list of templates at:\n"
|
||||||
|
f"<https://github.com/klemek/meme-otron/tree/master/discord>")
|
||||||
|
return
|
||||||
|
with tempfile.NamedTemporaryFile() as output:
|
||||||
|
img.save(output, format="JPEG")
|
||||||
|
response = None
|
||||||
|
if len(args) == 1:
|
||||||
|
response = f"Template `{args[0]}`:"
|
||||||
|
elif not direct:
|
||||||
|
response = f"A meme by {message.author.mention}:"
|
||||||
|
await message.channel.send(response,
|
||||||
|
file=discord.File(filename="meme.jpg", fp=output.name))
|
||||||
|
if not direct:
|
||||||
|
try:
|
||||||
|
await message.delete()
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
except discord.NotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Launch client and rerun on errors
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
client.run(token)
|
||||||
|
break # clean kill
|
||||||
|
except Exception as e:
|
||||||
|
t = datetime.now()
|
||||||
|
logging.error(f"Exception raised at {t:%Y-%m-%d %H:%M} : {repr(e)}")
|
||||||
|
fileName = f"error_{t:%Y-%m-%d_%H-%M-%S}.txt"
|
||||||
|
if os.path.exists(fileName):
|
||||||
|
logging.error("Two many errors, killing")
|
||||||
|
break
|
||||||
|
with open(fileName, 'w') as f:
|
||||||
|
f.write(f"Discord AI Dungeon 2 v{VERSION} started at {t0:%Y-%m-%d %H:%M}\r\n"
|
||||||
|
f"Exception raised at {t:%Y-%m-%d %H:%M}\r\n"
|
||||||
|
f"\r\n"
|
||||||
|
f"{traceback.format_exc()}")
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
discord
|
||||||
|
python-dotenv
|
||||||
|
requests
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from os import path
|
||||||
|
from meme_otron import img_factory as imgf
|
||||||
|
from meme_otron import meme_db
|
||||||
|
|
||||||
|
logging.basicConfig(format="[%(asctime)s][%(levelname)s][%(module)s] %(message)s", level=logging.DEBUG)
|
||||||
|
|
||||||
|
imgf.load_fonts()
|
||||||
|
meme_db.load_memes()
|
||||||
|
|
||||||
|
dst_dir = path.abspath("templates")
|
||||||
|
|
||||||
|
for f in os.listdir(dst_dir):
|
||||||
|
if path.isfile(path.join(dst_dir, f)):
|
||||||
|
os.unlink(path.join(dst_dir, f))
|
||||||
|
|
||||||
|
|
||||||
|
for meme_id in meme_db.DATA:
|
||||||
|
meme = meme_db.get_meme(meme_id)
|
||||||
|
if meme is not None:
|
||||||
|
img = imgf.make(meme.template, meme.texts, debug=True)
|
||||||
|
img.save(path.join(dst_dir, meme.template))
|
||||||
|
print(meme_id)
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,146 @@
|
|||||||
|
from PIL import Image, ImageFont, ImageDraw
|
||||||
|
import os
|
||||||
|
import os.path as path
|
||||||
|
import logging
|
||||||
|
|
||||||
|
DEFAULT_FONT = "arial"
|
||||||
|
DEFAULT_FONT_SIZE = 0.05
|
||||||
|
|
||||||
|
FONT_DIR = "../fonts"
|
||||||
|
TEMPLATES_DIR = "../templates"
|
||||||
|
|
||||||
|
FONTS = {}
|
||||||
|
|
||||||
|
logger = logging.getLogger("img_factory")
|
||||||
|
|
||||||
|
|
||||||
|
def load_fonts():
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
"""
|
||||||
|
for file in [f for f in os.listdir(FONT_DIR) if path.isfile(path.join(FONT_DIR, f))]:
|
||||||
|
split = path.splitext(file)
|
||||||
|
if split[-1] == ".ttf":
|
||||||
|
try:
|
||||||
|
FONTS[split[0]] = ImageFont.truetype(path.join(FONT_DIR, file))
|
||||||
|
logger.info(f"Loaded font '{split[0]}'")
|
||||||
|
except OSError:
|
||||||
|
logger.error(f"Could not load font '{split[0]}'")
|
||||||
|
|
||||||
|
|
||||||
|
def make(template, texts, debug=False):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param (str) template:
|
||||||
|
:param (list of Text) texts:
|
||||||
|
:param (bool) debug:
|
||||||
|
:rtype: PIL.Image.Image
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
img = Image.open(path.join(TEMPLATES_DIR, template))
|
||||||
|
except OSError as e:
|
||||||
|
logger.error(f"Could not read template file '{template}': {e}")
|
||||||
|
return None
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
for text in texts:
|
||||||
|
draw_text(draw, img.size, text, debug=debug)
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def draw_text(draw, size, text, debug=False):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param (PIL.ImageDraw.ImageDraw) draw: source image canvas
|
||||||
|
:param (int,int) size: source image size
|
||||||
|
:param (Text) text:
|
||||||
|
:param (bool) debug:
|
||||||
|
"""
|
||||||
|
# TODO rotation
|
||||||
|
# https://stackoverflow.com/questions/245447/how-do-i-draw-text-at-an-angle-using-pythons-pil
|
||||||
|
if text.text is not None and len(text.text.strip()) > 0:
|
||||||
|
if text.font is None:
|
||||||
|
text.font = DEFAULT_FONT
|
||||||
|
if text.font in FONTS:
|
||||||
|
text.text, font = fit_text(size, text)
|
||||||
|
draw.text(get_pos(size, text, font), text.text, fill=text.fill, align=text.align, font=font,
|
||||||
|
stroke_width=round(text.stroke_width * font.size), stroke_fill=text.stroke_fill)
|
||||||
|
if debug:
|
||||||
|
draw.rectangle([(text.x_range[0] * size[0], text.y_range[0] * size[1]),
|
||||||
|
(text.x_range[1] * size[0], text.y_range[1] * size[1])],
|
||||||
|
None,
|
||||||
|
(128, 128, 128))
|
||||||
|
else:
|
||||||
|
logger.warning(f"Invalid font '{text.font}'")
|
||||||
|
|
||||||
|
|
||||||
|
def fit_text(size, text):
|
||||||
|
"""
|
||||||
|
:param (int,int) size: source image size
|
||||||
|
:param (Text) text:
|
||||||
|
:rtype: (str, PIL.ImageFont.FreeTypeFont)
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
max_width = round(size[0] * (text.x_range[1] - text.x_range[0]))
|
||||||
|
max_height = round(size[1] * (text.y_range[1] - text.y_range[0]))
|
||||||
|
text_size = None
|
||||||
|
if text.font_size is None:
|
||||||
|
text.font_size = DEFAULT_FONT_SIZE
|
||||||
|
font_size = round(text.font_size * min(size)) + 1
|
||||||
|
font = FONTS[text.font]
|
||||||
|
t = ""
|
||||||
|
while (text_size is None or text_size[1] >= max_height) and font_size > 1:
|
||||||
|
font_size -= 1
|
||||||
|
font = font.font_variant(size=font_size)
|
||||||
|
words = text.text.split(" ")
|
||||||
|
t = ""
|
||||||
|
for word in words:
|
||||||
|
spacer = " "
|
||||||
|
if len(t) == 0:
|
||||||
|
spacer = ""
|
||||||
|
text_size = font.getsize_multiline(t + spacer + word, stroke_width=text.stroke_width * font_size)
|
||||||
|
if text_size[0] >= max_width:
|
||||||
|
t += "\n" + word
|
||||||
|
else:
|
||||||
|
t += spacer + word
|
||||||
|
text_size = font.getsize_multiline(t, stroke_width=text.stroke_width * font_size)
|
||||||
|
return t, font
|
||||||
|
|
||||||
|
|
||||||
|
def get_pos(size, text, font):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param (int,int) size: source image size
|
||||||
|
:param (Text) text:
|
||||||
|
:param (PIL.ImageFont.FreeTypeFont) font:
|
||||||
|
:rtype (int,int)
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
min_x = round(text.x_range[0] * size[0])
|
||||||
|
max_x = round(text.x_range[1] * size[0])
|
||||||
|
min_y = round(text.y_range[0] * size[1])
|
||||||
|
max_y = round(text.y_range[1] * size[1])
|
||||||
|
pos_x = 0
|
||||||
|
pos_y = 0
|
||||||
|
text_size = font.getsize_multiline(text.text, stroke_width=text.stroke_width * font.size)
|
||||||
|
|
||||||
|
if text.position.value[0] == "S":
|
||||||
|
pos_y = min_y
|
||||||
|
elif text.position.value[0] == "C":
|
||||||
|
pos_y = round((min_y + max_y) / 2 - text_size[1] / 2)
|
||||||
|
elif text.position.value[0] == "N":
|
||||||
|
pos_y = max_y - text_size[1]
|
||||||
|
|
||||||
|
if text.position.value[1] == "W":
|
||||||
|
pos_x = min_x
|
||||||
|
elif text.position.value[1] == "C":
|
||||||
|
pos_x = round((min_x + max_x) / 2 - text_size[0] / 2)
|
||||||
|
elif text.position.value[1] == "E":
|
||||||
|
pos_x = max_x - text_size[0]
|
||||||
|
|
||||||
|
return pos_x, pos_y
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .types import Text, Pos
|
||||||
|
from . import img_factory as imgf
|
||||||
|
from . import meme_db as db
|
||||||
|
|
||||||
|
logger = logging.getLogger("meme_otron")
|
||||||
|
|
||||||
|
right_wmark = Text("Made with meme-otron")
|
||||||
|
right_wmark.position = Pos.SE
|
||||||
|
right_wmark.fill = (128, 128, 128, 128)
|
||||||
|
right_wmark.font_size = 0.02
|
||||||
|
right_wmark.x_range = [0.005, 0.995]
|
||||||
|
right_wmark.y_range = [0.005, 0.995]
|
||||||
|
|
||||||
|
left_wmark = Text()
|
||||||
|
left_wmark.position = Pos.SW
|
||||||
|
left_wmark.fill = (128, 128, 128, 128)
|
||||||
|
left_wmark.font_size = 0.02
|
||||||
|
left_wmark.x_range = [0.005, 0.995]
|
||||||
|
left_wmark.y_range = [0.005, 0.995]
|
||||||
|
|
||||||
|
|
||||||
|
def compute(*args, left_wmark_text=None, debug=False):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param (str) left_wmark_text:
|
||||||
|
:param (bool) debug:
|
||||||
|
:param (str) args:
|
||||||
|
:rtype: PIL.Image.Image
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if len(args) < 1:
|
||||||
|
logger.warning("python3 meme_otron.py (meme_id) \"[text 1]\" \"[text 2]\" ... > file.jpg")
|
||||||
|
return None
|
||||||
|
meme_id = args[0]
|
||||||
|
meme = db.get_meme(meme_id)
|
||||||
|
if meme is None:
|
||||||
|
logger.warning(f"Meme template '{meme_id}' not found")
|
||||||
|
return None
|
||||||
|
if len(args) > 1:
|
||||||
|
for i in range(len(meme.texts)):
|
||||||
|
if i < len(args) - 1:
|
||||||
|
meme.texts[i].text = args[i + 1]
|
||||||
|
else:
|
||||||
|
meme.texts[i].text = ""
|
||||||
|
meme.texts += [right_wmark]
|
||||||
|
if left_wmark_text is not None:
|
||||||
|
left_wmark.text = left_wmark_text
|
||||||
|
meme.texts += [left_wmark]
|
||||||
|
return imgf.make(meme.template, meme.texts, debug=debug)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(format="[%(asctime)s][%(levelname)s][%(module)s] %(message)s", level=logging.DEBUG)
|
||||||
|
db.load_memes()
|
||||||
|
imgf.load_fonts()
|
||||||
|
img = compute(*sys.argv[1:])
|
||||||
|
with os.fdopen(os.dup(sys.stdout.fileno())) as output:
|
||||||
|
img.save(output, format="jpeg")
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .types import Pos, Text, Meme
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
DATA_FILE = "../memes.json"
|
||||||
|
|
||||||
|
DATA = {}
|
||||||
|
ALIASES = {}
|
||||||
|
|
||||||
|
logger = logging.getLogger("meme_db")
|
||||||
|
|
||||||
|
|
||||||
|
def load_memes():
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(DATA_FILE) as f:
|
||||||
|
content = "".join(f.readlines())
|
||||||
|
raw_data = json.loads(content)
|
||||||
|
if not (isinstance(raw_data, list)):
|
||||||
|
raise TypeError(f"Root is not a list")
|
||||||
|
for i in range(len(raw_data)):
|
||||||
|
load_item(i, raw_data[i])
|
||||||
|
except OSError as e:
|
||||||
|
logger.error(f"Could not read data file: {e}")
|
||||||
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
logger.error(f"Wrong JSON syntax '{DATA_FILE}': {e}")
|
||||||
|
except TypeError as e:
|
||||||
|
logger.error(f"Invalid data file: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def load_item(i, item):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param (int) i:
|
||||||
|
:param (dict) item:
|
||||||
|
"""
|
||||||
|
item_id = ""
|
||||||
|
try:
|
||||||
|
if not (isinstance(item, dict)):
|
||||||
|
raise TypeError(f"root is not a dict")
|
||||||
|
item_id = utils.read_key(item, "id")
|
||||||
|
if item_id in DATA:
|
||||||
|
raise NameError(f"id '{item_id}' already existing")
|
||||||
|
based_on = utils.read_key_safe(item, "based_on")
|
||||||
|
abstract = utils.read_key_safe(item, "abstract", False)
|
||||||
|
aliases = utils.read_key_safe(item, "aliases", [])
|
||||||
|
if not utils.is_list_of(aliases, [str]):
|
||||||
|
raise TypeError(f"'aliases' is not a list of str")
|
||||||
|
template = None
|
||||||
|
font = None
|
||||||
|
font_size = None
|
||||||
|
texts = None
|
||||||
|
if based_on is not None:
|
||||||
|
if based_on in DATA:
|
||||||
|
template = DATA[based_on].template
|
||||||
|
font = DATA[based_on].font
|
||||||
|
font_size = DATA[based_on].font_size
|
||||||
|
texts = DATA[based_on].clone_texts()
|
||||||
|
else:
|
||||||
|
raise NameError(f"Reference '{based_on}' not found in data, make sur it's placed before this one")
|
||||||
|
if not abstract:
|
||||||
|
template = utils.read_key(item, "template", template)
|
||||||
|
font = utils.read_key_safe(item, "font", font)
|
||||||
|
font_size = utils.read_key_safe(item, "font_size", font_size)
|
||||||
|
raw_texts = utils.read_key(item, "texts", texts)
|
||||||
|
if texts is None:
|
||||||
|
if not (isinstance(raw_texts, list)):
|
||||||
|
raise TypeError(f"'texts' is not a list")
|
||||||
|
texts = []
|
||||||
|
for j in range(len(raw_texts)):
|
||||||
|
raw_text = raw_texts[j]
|
||||||
|
try:
|
||||||
|
texts += [load_text(j, raw_text)]
|
||||||
|
except TypeError as e:
|
||||||
|
logger.warning(f"Item '{item_id}'({i}) / Text {j}: {e}")
|
||||||
|
if font is not None:
|
||||||
|
if not (isinstance(font, str)):
|
||||||
|
raise TypeError(f"'font' is not a str")
|
||||||
|
for text in texts:
|
||||||
|
if text.font is None:
|
||||||
|
text.font = font
|
||||||
|
if font_size is not None:
|
||||||
|
if not (isinstance(font_size, float)):
|
||||||
|
raise TypeError(f"'font_size' is not a float")
|
||||||
|
for text in texts:
|
||||||
|
if text.font_size is None:
|
||||||
|
text.font_size = font_size
|
||||||
|
if len(texts) == 0:
|
||||||
|
logger.warning(f"Item '{item_id}'({i}): no texts loaded")
|
||||||
|
else:
|
||||||
|
DATA[item_id] = Meme(item_id, aliases, abstract, template, font, font_size, texts)
|
||||||
|
for alias in aliases:
|
||||||
|
ALIASES[alias] = item_id
|
||||||
|
logger.info(f"Loaded meme '{item_id}' with {len(texts)} texts")
|
||||||
|
except KeyError as e:
|
||||||
|
logger.warning(f"Item '{item_id}'({i}): key {e} not found")
|
||||||
|
except TypeError as e:
|
||||||
|
logger.warning(f"Item '{item_id}'({i}): {e}")
|
||||||
|
except NameError as e:
|
||||||
|
logger.warning(f"Item '{item_id}'({i}): {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def load_text(j, raw_text):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param (int) j:
|
||||||
|
:param (dict) raw_text:
|
||||||
|
:raises TypeError:
|
||||||
|
:rtype: Text
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not (isinstance(raw_text, dict)):
|
||||||
|
raise TypeError(f"root is not a dict")
|
||||||
|
text = Text(f"text {j+1}")
|
||||||
|
if "font" in raw_text:
|
||||||
|
if not (isinstance(raw_text["font"], str)):
|
||||||
|
raise TypeError(f"'font' is not a str")
|
||||||
|
text.font = raw_text["font"]
|
||||||
|
if "x_range" in raw_text:
|
||||||
|
if not (utils.is_list_of(raw_text["x_range"], [int, float], 2)):
|
||||||
|
raise TypeError(f"'x_range' is not a list of 2 float")
|
||||||
|
text.x_range = raw_text["x_range"]
|
||||||
|
if "y_range" in raw_text:
|
||||||
|
if not (utils.is_list_of(raw_text["y_range"], [int, float], 2)):
|
||||||
|
raise TypeError(f"'y_range' is not a list of 2 float")
|
||||||
|
text.y_range = raw_text["y_range"]
|
||||||
|
if "position" in raw_text:
|
||||||
|
if raw_text["position"] not in [p.name for p in Pos]:
|
||||||
|
raise TypeError(f"'position' is not a valid position (ex: NW, E, SE, ...)")
|
||||||
|
text.position = [p for p in Pos if p.name == raw_text["position"]][0]
|
||||||
|
if "font_size" in raw_text:
|
||||||
|
if not (isinstance(raw_text["font_size"], float)):
|
||||||
|
raise TypeError(f"'font_size' is not a float")
|
||||||
|
text.font_size = raw_text["font_size"]
|
||||||
|
if "fill" in raw_text:
|
||||||
|
if not (utils.is_list_of(raw_text["fill"], [int], 3)):
|
||||||
|
raise TypeError(f"'fill' is not a list of 3 int")
|
||||||
|
text.fill = raw_text["fill"]
|
||||||
|
if "stroke_width" in raw_text:
|
||||||
|
if not (isinstance(raw_text["stroke_width"], float)):
|
||||||
|
raise TypeError(f"'stroke_width' is not a float")
|
||||||
|
text.stroke_width = raw_text["stroke_width"]
|
||||||
|
if "stroke_fill" in raw_text:
|
||||||
|
if not (utils.is_list_of(raw_text["stroke_fill"], [int], 3)):
|
||||||
|
raise TypeError(f"'stroke_fill' is not a list of 3 int")
|
||||||
|
text.stroke_fill = raw_text["stroke_fill"]
|
||||||
|
if "align" in raw_text:
|
||||||
|
if raw_text["align"] not in ["left", "center", "right"]:
|
||||||
|
raise TypeError(f"'align' is not 'left', 'center' or 'right'")
|
||||||
|
text.align = raw_text["align"]
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def get_meme(name):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param (str) name:
|
||||||
|
:rtype: Meme|None
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if name in DATA and not DATA[name].abstract:
|
||||||
|
return DATA[name].clone()
|
||||||
|
elif name in ALIASES:
|
||||||
|
return DATA[ALIASES[name]].clone()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from enum import Enum
|
||||||
|
import copy
|
||||||
|
|
||||||
|
|
||||||
|
class Pos(Enum):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
"""
|
||||||
|
NW = "NW"
|
||||||
|
N = "NC"
|
||||||
|
NE = "NE"
|
||||||
|
W = "CW"
|
||||||
|
CENTER = "CC"
|
||||||
|
E = "CE"
|
||||||
|
SW = "NW"
|
||||||
|
S = "NC"
|
||||||
|
SE = "NE"
|
||||||
|
|
||||||
|
|
||||||
|
class Meme:
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, meme_id, aliases, abstract, template, font, font_size, texts):
|
||||||
|
self.id = meme_id
|
||||||
|
self.aliases = aliases
|
||||||
|
self.abstract = abstract
|
||||||
|
self.template = template
|
||||||
|
self.font = font
|
||||||
|
self.font_size = font_size
|
||||||
|
self.texts = texts
|
||||||
|
|
||||||
|
def clone_texts(self):
|
||||||
|
return copy.deepcopy(self.texts)
|
||||||
|
|
||||||
|
def clone(self):
|
||||||
|
return Meme(self.id,
|
||||||
|
self.aliases,
|
||||||
|
self.abstract,
|
||||||
|
self.template,
|
||||||
|
self.font,
|
||||||
|
self.font_size,
|
||||||
|
self.clone_texts())
|
||||||
|
|
||||||
|
|
||||||
|
class Text:
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, text=None):
|
||||||
|
self.text = text
|
||||||
|
self.x_range = (0, 1)
|
||||||
|
self.y_range = (0, 1)
|
||||||
|
self.position = Pos.CENTER
|
||||||
|
self.font_size = None
|
||||||
|
self.fill = (0, 0, 0)
|
||||||
|
self.stroke_width = 0
|
||||||
|
self.stroke_fill = (0, 0, 0)
|
||||||
|
self.font = None
|
||||||
|
self.align = "center"
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def read_key_safe(d, k, default=None):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param (dict) d: source dict
|
||||||
|
:param (str) k: key to read
|
||||||
|
:param default: default value
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if k in d:
|
||||||
|
return d[k]
|
||||||
|
else:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def read_key(d, k, default=None):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param (dict) d: source dict
|
||||||
|
:param (str) k: key to read
|
||||||
|
:param default: default value
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if k in d:
|
||||||
|
return d[k]
|
||||||
|
elif default is not None:
|
||||||
|
return default
|
||||||
|
else:
|
||||||
|
raise KeyError(k)
|
||||||
|
|
||||||
|
|
||||||
|
def is_list_of(obj, types, length=None):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param obj:
|
||||||
|
:param (list of type) types:
|
||||||
|
:param (int) length:
|
||||||
|
:rtype: bool
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not (isinstance(obj, list)):
|
||||||
|
return False
|
||||||
|
for item in obj:
|
||||||
|
found = False
|
||||||
|
for t in types:
|
||||||
|
if isinstance(item, t):
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
return False
|
||||||
|
if length is not None and len(obj) != length:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_url(s):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param (str) s:
|
||||||
|
:rtype: bool
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return False # TODO
|
||||||
|
|
||||||
|
|
||||||
|
args_regex = re.compile('"([^"]*)"|\'([^\']*)\'|([^ ]+)')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_arguments(s):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param (str) s:
|
||||||
|
:rtype: list of str
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return [[g for g in m if len(g) > 0][0] for m in args_regex.findall(s)]
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
[{
|
||||||
|
"id": "2_panel_right",
|
||||||
|
"abstract": true,
|
||||||
|
"font": "arial",
|
||||||
|
"texts": [{
|
||||||
|
"x_range": [0.52, 0.98],
|
||||||
|
"y_range": [0.00, 0.50]
|
||||||
|
}, {
|
||||||
|
"x_range": [0.52, 0.98],
|
||||||
|
"y_range": [0.50, 1.00]
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
"id": "3_panel_right",
|
||||||
|
"abstract": true,
|
||||||
|
"font": "arial",
|
||||||
|
"texts": [{
|
||||||
|
"x_range": [0.52, 0.98],
|
||||||
|
"y_range": [0.00, 0.33]
|
||||||
|
},{
|
||||||
|
"x_range": [0.52, 0.98],
|
||||||
|
"y_range": [0.33, 0.67]
|
||||||
|
}, {
|
||||||
|
"x_range": [0.52, 0.98],
|
||||||
|
"y_range": [0.67, 1.00]
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
"id": "4_panel_right",
|
||||||
|
"abstract": true,
|
||||||
|
"font": "arial",
|
||||||
|
"texts": [{
|
||||||
|
"x_range": [0.52, 0.98],
|
||||||
|
"y_range": [0.00, 0.25]
|
||||||
|
},{
|
||||||
|
"x_range": [0.52, 0.98],
|
||||||
|
"y_range": [0.25, 0.50]
|
||||||
|
},{
|
||||||
|
"x_range": [0.52, 0.98],
|
||||||
|
"y_range": [0.50, 0.75]
|
||||||
|
},{
|
||||||
|
"x_range": [0.52, 0.98],
|
||||||
|
"y_range": [0.75, 1.00]
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
"id": "5_panel_right",
|
||||||
|
"abstract": true,
|
||||||
|
"font": "arial",
|
||||||
|
"texts": [{
|
||||||
|
"x_range": [0.52, 0.98],
|
||||||
|
"y_range": [0.00, 0.20]
|
||||||
|
},{
|
||||||
|
"x_range": [0.52, 0.98],
|
||||||
|
"y_range": [0.20, 0.40]
|
||||||
|
},{
|
||||||
|
"x_range": [0.52, 0.98],
|
||||||
|
"y_range": [0.40, 0.60]
|
||||||
|
},{
|
||||||
|
"x_range": [0.52, 0.98],
|
||||||
|
"y_range": [0.60, 0.80]
|
||||||
|
},{
|
||||||
|
"x_range": [0.52, 0.98],
|
||||||
|
"y_range": [0.80, 1.00]
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
"id": "2_panel_left",
|
||||||
|
"abstract": true,
|
||||||
|
"font": "arial",
|
||||||
|
"texts": [{
|
||||||
|
"x_range": [0.02, 0.48],
|
||||||
|
"y_range": [0.00, 0.50]
|
||||||
|
}, {
|
||||||
|
"x_range": [0.02, 0.48],
|
||||||
|
"y_range": [0.50, 1.00]
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
"id": "3_panel_left",
|
||||||
|
"abstract": true,
|
||||||
|
"font": "arial",
|
||||||
|
"texts": [{
|
||||||
|
"x_range": [0.02, 0.48],
|
||||||
|
"y_range": [0.00, 0.33]
|
||||||
|
},{
|
||||||
|
"x_range": [0.02, 0.48],
|
||||||
|
"y_range": [0.33, 0.67]
|
||||||
|
}, {
|
||||||
|
"x_range": [0.02, 0.48],
|
||||||
|
"y_range": [0.67, 1.00]
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
"id": "4_panel_left",
|
||||||
|
"abstract": true,
|
||||||
|
"font": "arial",
|
||||||
|
"texts": [{
|
||||||
|
"x_range": [0.02, 0.48],
|
||||||
|
"y_range": [0.00, 0.25]
|
||||||
|
},{
|
||||||
|
"x_range": [0.02, 0.48],
|
||||||
|
"y_range": [0.25, 0.50]
|
||||||
|
},{
|
||||||
|
"x_range": [0.02, 0.48],
|
||||||
|
"y_range": [0.50, 0.75]
|
||||||
|
},{
|
||||||
|
"x_range": [0.02, 0.48],
|
||||||
|
"y_range": [0.75, 1.00]
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
"id": "5_panel_left",
|
||||||
|
"abstract": true,
|
||||||
|
"font": "arial",
|
||||||
|
"texts": [{
|
||||||
|
"x_range": [0.02, 0.48],
|
||||||
|
"y_range": [0.00, 0.20]
|
||||||
|
},{
|
||||||
|
"x_range": [0.02, 0.48],
|
||||||
|
"y_range": [0.20, 0.40]
|
||||||
|
},{
|
||||||
|
"x_range": [0.02, 0.48],
|
||||||
|
"y_range": [0.40, 0.60]
|
||||||
|
},{
|
||||||
|
"x_range": [0.02, 0.48],
|
||||||
|
"y_range": [0.60, 0.80]
|
||||||
|
},{
|
||||||
|
"x_range": [0.02, 0.48],
|
||||||
|
"y_range": [0.80, 1.00]
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
"id": "drake",
|
||||||
|
"template": "drake.jpg",
|
||||||
|
"based_on": "2_panel_right"
|
||||||
|
},{
|
||||||
|
"id": "brain3",
|
||||||
|
"template": "brain3.jpg",
|
||||||
|
"based_on": "3_panel_left"
|
||||||
|
},{
|
||||||
|
"id": "brain4",
|
||||||
|
"aliases": ["brains"],
|
||||||
|
"template": "brain4.jpg",
|
||||||
|
"based_on": "4_panel_left"
|
||||||
|
},{
|
||||||
|
"id": "brain5",
|
||||||
|
"template": "brain5.jpg",
|
||||||
|
"based_on": "5_panel_left"
|
||||||
|
},{
|
||||||
|
"id": "disappointed",
|
||||||
|
"template": "disappointed.jpg",
|
||||||
|
"based_on": "2_panel_left"
|
||||||
|
},{
|
||||||
|
"id": "nope",
|
||||||
|
"template": "nope.jpg",
|
||||||
|
"based_on": "2_panel_right"
|
||||||
|
},{
|
||||||
|
"id": "pleasure3",
|
||||||
|
"aliases": ["satisfied3"],
|
||||||
|
"template": "pleasure3.jpg",
|
||||||
|
"based_on": "3_panel_left"
|
||||||
|
},{
|
||||||
|
"id": "pleasure4",
|
||||||
|
"aliases": ["pleasure","satisfied","satisfied4"],
|
||||||
|
"template": "pleasure4.jpg",
|
||||||
|
"based_on": "4_panel_left"
|
||||||
|
},{
|
||||||
|
"id": "tough2",
|
||||||
|
"aliases": ["tough", "fight"],
|
||||||
|
"template": "tough2.jpg",
|
||||||
|
"based_on": "2_panel_right"
|
||||||
|
},{
|
||||||
|
"id": "tough2bis",
|
||||||
|
"aliases": ["soft"],
|
||||||
|
"template": "tough2bis.jpg",
|
||||||
|
"based_on": "2_panel_right"
|
||||||
|
},{
|
||||||
|
"id": "tough3",
|
||||||
|
"template": "tough3.jpg",
|
||||||
|
"based_on": "3_panel_right"
|
||||||
|
},{
|
||||||
|
"id": "winnie2",
|
||||||
|
"aliases": ["winnie"],
|
||||||
|
"template": "winnie2.jpg",
|
||||||
|
"based_on": "2_panel_right"
|
||||||
|
},{
|
||||||
|
"id": "winnie3",
|
||||||
|
"template": "winnie3.jpg",
|
||||||
|
"based_on": "3_panel_right"
|
||||||
|
}]
|
||||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 230 KiB |