diff --git a/docs/build_docs.py b/docs/build_docs.py index f83ffe1..a32ac66 100644 --- a/docs/build_docs.py +++ b/docs/build_docs.py @@ -14,9 +14,12 @@ dst_dir = utils.relative_path(__file__, "templates") templates_dir = utils.relative_path(__file__, "..", "templates") -for f in os.listdir(dst_dir): - if path.isfile(path.join(dst_dir, f)): - os.unlink(path.join(dst_dir, f)) +if path.exists(dst_dir): + for f in os.listdir(dst_dir): + if path.isfile(path.join(dst_dir, f)): + os.unlink(path.join(dst_dir, f)) +else: + os.mkdir(dst_dir) count = 0 diff --git a/meme_otron/img_factory.py b/meme_otron/img_factory.py index 6082177..348d213 100644 --- a/meme_otron/img_factory.py +++ b/meme_otron/img_factory.py @@ -5,9 +5,6 @@ import logging from . import utils -DEFAULT_FONT = "arial" -DEFAULT_FONT_SIZE = 0.05 - FONT_DIR = utils.relative_path(__file__, "..", "fonts") TEMPLATES_DIR = utils.relative_path(__file__, "..", "templates") @@ -65,8 +62,7 @@ def draw_text(draw, size, text, debug=False): # 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 + text.init() # load default values 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, @@ -90,8 +86,6 @@ def fit_text(size, text): 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 = "" @@ -127,8 +121,6 @@ def get_pos(size, text, font): 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 int(text.position.value) // 3 == 0: diff --git a/meme_otron/meme_db.py b/meme_otron/meme_db.py index 4d85cd4..4c307e7 100644 --- a/meme_otron/meme_db.py +++ b/meme_otron/meme_db.py @@ -1,6 +1,5 @@ import json import logging -import os.path as path from .types import Pos, Text, Meme from . import utils @@ -44,63 +43,44 @@ def load_item(i, item): try: if not (isinstance(item, dict)): raise TypeError(f"root is not a dict") - item_id = utils.read_key(item, "id") + item_id = utils.read_key(item, "id", types=[str]) 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 + based_on = utils.read_key_safe(item, "based_on", types=[str]) 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() + meme = DATA[based_on].clone() + meme.id = item_id 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 = [] + else: + meme = Meme(item_id) + meme.abstract = utils.read_key_safe(item, "abstract", False, types=[bool]) + meme.aliases = utils.read_key_safe(item, "aliases", [], types=[str], is_list=True) + meme.text_base = load_text(0, item, meme.text_base) + if not meme.abstract: + meme.template = utils.read_key(item, "template", meme.template, types=[str]) + raw_texts = utils.read_key(item, "texts", meme.texts, types=[dict], is_list=True) + if meme.texts is None: + meme.texts = [] for j in range(len(raw_texts)): raw_text = raw_texts[j] try: - texts += [load_text(j, raw_text)] + meme.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: + for text in meme.texts: + text.update(meme.text_base) + if len(meme.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: + DATA[item_id] = meme + for alias in meme.aliases: if alias in ALIASES: logger.warning(f"Item '{item_id}'({i}): alias '{alias}' already registered by '{ALIASES[alias]}'") else: ALIASES[alias] = item_id - logger.info(f"Loaded meme '{item_id}' with {len(texts)} texts") + logger.info(f"Loaded meme '{item_id}' with {len(meme.texts)} texts") except KeyError as e: logger.warning(f"Item '{item_id}'({i}): key {e} not found") except TypeError as e: @@ -109,51 +89,30 @@ def load_item(i, item): logger.warning(f"Item '{item_id}'({i}): {e}") -def load_text(j, raw_text): +def load_text(j, raw_text, text=None): """ TODO :param (int) j: :param (dict) raw_text: + :param (Text|None) 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 text is None: + text = Text(f"text {j + 1}") + text.font = utils.read_key_safe(raw_text, "font", types=[str]) + text.x_range = utils.read_key_safe(raw_text, "x_range", types=[float, int], is_list=True, is_list_size=2) + text.y_range = utils.read_key_safe(raw_text, "y_range", types=[float, int], is_list=True, is_list_size=2) + text.font_size = utils.read_key_safe(raw_text, "font_size", types=[float]) + text.fill = utils.read_key_safe(raw_text, "fill", types=[int], is_list=True, is_list_size=3) + text.stroke_width = utils.read_key_safe(raw_text, "stroke_width", types=[float]) + text.stroke_fill = utils.read_key_safe(raw_text, "stroke_fill", types=[int], is_list=True, is_list_size=3) 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 = tuple(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 = tuple(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'") diff --git a/meme_otron/types.py b/meme_otron/types.py index f7768af..c6a1970 100644 --- a/meme_otron/types.py +++ b/meme_otron/types.py @@ -1,6 +1,9 @@ from enum import IntEnum import copy +DEFAULT_FONT = "arial" +DEFAULT_FONT_SIZE = 0.05 + class Pos(IntEnum): """ @@ -22,51 +25,79 @@ class Meme: TODO """ - def __init__(self, meme_id, aliases, abstract, template, font, font_size, texts): + def __init__(self, meme_id, aliases=None, abstract=False, template=None, text_base=None, texts=None): self.id = meme_id - self.aliases = aliases + if aliases is None: + self.aliases = [] + else: + 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) + if text_base is None: + self.text_base = Text() + else: + self.text_base = text_base + if texts is None: + self.texts = None + else: + self.texts = copy.deepcopy(texts) def clone(self): - return Meme(self.id, - self.aliases, - self.abstract, - self.template, - self.font, - self.font_size, - self.clone_texts()) + return copy.deepcopy(self) class Text: """ TODO """ + base_properties = ["font", "font_size", "fill", "stroke_width", + "stroke_fill", "align", "position"] def __init__(self, text=None): self.text = text - + self.x_range = (0, 1) self.y_range = (0, 1) - + self.font = None self.font_size = None - - self.fill = (0, 0, 0) - self.stroke_width = 0 - self.stroke_fill = (0, 0, 0) - - self.align = "center" - self.position = Pos.CENTER + + self.fill = None + self.stroke_width = None + self.stroke_fill = None + + self.align = None + self.position = None def update(self, base): - for prop in ["font", "font_size", "fill", "stroke_width", - "stroke_fill", "align", "position"]: + """ + TODO + + :param (Text) base: + """ + for prop in Text.base_properties: if getattr(self, prop) is None: - setattr(self,prop, getattr(base, prop)) + setattr(self, prop, getattr(base, prop)) + + def init(self): + """ + TODO + """ + if self.font is None: + self.font = DEFAULT_FONT + if self.font_size is None: + self.font_size = DEFAULT_FONT_SIZE + if self.align is None: + self.align = "center" + if self.fill is None: + self.fill = (0, 0, 0) + else: + self.fill = tuple(self.fill) + if self.stroke_fill is None: + self.stroke_fill = (0, 0, 0) + else: + self.stroke_fill = tuple(self.stroke_fill) + if self.stroke_width is None: + self.stroke_width = 0 + if self.position is None: + self.position = Pos.CENTER diff --git a/meme_otron/utils.py b/meme_otron/utils.py index 3003bcc..d6d445b 100644 --- a/meme_otron/utils.py +++ b/meme_otron/utils.py @@ -3,41 +3,86 @@ import os.path as path def relative_path(file, *args): + """ + TODO + + :param (str) file: + :param (str) args: + :rtype str + :return: + """ return path.realpath(path.join(path.dirname(path.realpath(file)), *args)) -def read_key_safe(d, k, default=None): +def read_key_safe(d, k, default=None, *, types=None, is_list=False, is_list_size=None): """ TODO :param (dict) d: source dict :param (str) k: key to read :param default: default value + :param (list of type|None) types: types to check + :param (bool) is_list: if the type is a list of types + :param (int|None) is_list_size: size of the list to enforce or None + :raises TypeError: :return: """ - if k in d: - return d[k] - else: + try: + return read_key(d, k, default, types=types, is_list=is_list, is_list_size=is_list_size) + except KeyError: return default -def read_key(d, k, default=None): +def read_key(d, k, default=None, *, types=None, is_list=False, is_list_size=None): """ TODO :param (dict) d: source dict :param (str) k: key to read :param default: default value + :param (list of type|None) types: types to check + :param (bool) is_list: if the type is a list of types + :param (int|None) is_list_size: size of the list to enforce or None + :raises TypeError: + :raises KeyError: :return: """ if k in d: - return d[k] + v = d[k] + if types is not None: + try: + check_type(v, types, is_list, is_list_size) + except TypeError as e: + raise TypeError(f"'{k}' is {e}") + return v elif default is not None: return default else: raise KeyError(k) +def check_type(obj, types, is_list=False, is_list_size=None): + """ + TODO + + :param obj: + :param (list of type|None) types: types to check + :param (bool) is_list: if the type is a list of types + :param (int|None) is_list_size: size of the list to enforce or None + :raises TypeError: + :return: + """ + if is_list: + if not is_list_of(obj, types, is_list_size): + if is_list_size is None: + raise TypeError(f"not a list of {is_list_size} {types[0].__name__}") + else: + raise TypeError(f"not a list of {types[0].__name__}") + else: + if not is_list_of([obj], types): + raise TypeError(f"not a {types[0].__name__}") + + def is_list_of(obj, types, length=None): """ TODO @@ -63,17 +108,6 @@ def is_list_of(obj, types, length=None): return True -def is_url(s): - """ - TODO - - :param (str) s: - :rtype: bool - :return: - """ - return False # TODO - - args_regex = re.compile('"([^"]*)"|\'([^\']*)\'|([^ ]+)')