code cleaning
This commit is contained in:
+14
-15
@@ -1,15 +1,14 @@
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
|
||||
from . import img_factory as imgf
|
||||
from . import meme_db as db
|
||||
from . import img_factory
|
||||
from . import meme_db
|
||||
from . import meme_otron
|
||||
from . import VERSION
|
||||
|
||||
if __name__ == "__main__":
|
||||
db.load_memes()
|
||||
imgf.load_fonts()
|
||||
meme_db.load_memes()
|
||||
img_factory.load_fonts()
|
||||
|
||||
# TODO better arguments reading (-h, -o, -v)
|
||||
|
||||
@@ -21,29 +20,29 @@ if __name__ == "__main__":
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
output_f = None
|
||||
output_file = None
|
||||
if "-o" in sys.argv:
|
||||
i = sys.argv.index("-o")
|
||||
if len(sys.argv) >= i:
|
||||
output_f = sys.argv[i + 1]
|
||||
output_file = sys.argv[i + 1]
|
||||
del sys.argv[i + 1]
|
||||
del sys.argv[i]
|
||||
img = meme_otron.compute(*sys.argv[1:])
|
||||
if img is None:
|
||||
hint = db.find_nearest(sys.argv[1])
|
||||
if hint is not None:
|
||||
print(f"Did you mean '{hint}'?", file=sys.stderr)
|
||||
proposal = meme_db.find_nearest(sys.argv[1])
|
||||
if proposal is not None:
|
||||
print(f"Did you mean '{proposal}'?", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if output_f is None:
|
||||
if output_file is None:
|
||||
with os.fdopen(os.dup(sys.stdout.fileno())) as output:
|
||||
img.save(output, format="jpeg")
|
||||
else:
|
||||
try:
|
||||
img.save(output_f)
|
||||
print(f"Wrote '{output_f}'")
|
||||
img.save(output_file)
|
||||
print(f"Wrote '{output_file}'")
|
||||
except OSError as e:
|
||||
print(f"Cannot write '{output_f}': {e}", file=sys.stderr)
|
||||
print(f"Cannot write '{output_file}': {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except ValueError as e:
|
||||
print(f"Cannot write '{output_f}': {e}", file=sys.stderr)
|
||||
print(f"Cannot write '{output_file}': {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
+19
-43
@@ -1,9 +1,11 @@
|
||||
from typing import List, Optional, Tuple
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
import os
|
||||
import os.path as path
|
||||
import logging
|
||||
|
||||
from . import utils
|
||||
from .types import Text
|
||||
|
||||
FONT_DIR = utils.relative_path(__file__, "..", "fonts")
|
||||
TEMPLATES_DIR = utils.relative_path(__file__, "..", "templates")
|
||||
@@ -24,14 +26,7 @@ def load_fonts():
|
||||
logger.error(f"Could not load font '{split[0]}'")
|
||||
|
||||
|
||||
def make(template, texts, debug=False):
|
||||
"""
|
||||
:param (str) template:
|
||||
:param (list of Text) texts:
|
||||
:param (bool) debug:
|
||||
:rtype: PIL.Image.Image
|
||||
:return:
|
||||
"""
|
||||
def build_image(template: str, texts: List[Text], debug: bool = False) -> Optional[Image.Image]:
|
||||
try:
|
||||
img = Image.open(path.join(TEMPLATES_DIR, template)).convert(mode='RGBA')
|
||||
except OSError as e:
|
||||
@@ -45,19 +40,13 @@ def make(template, texts, debug=False):
|
||||
return img.convert(mode='RGB')
|
||||
|
||||
|
||||
def draw_text(draw, img, text, debug=False):
|
||||
"""
|
||||
:param (PIL.ImageDraw.ImageDraw) draw: source image canvas
|
||||
:param (PIL.Image.Image) img: source image
|
||||
:param (Text) text:
|
||||
:param (bool) debug:
|
||||
"""
|
||||
def draw_text(draw: ImageDraw.ImageDraw, img: Image.Image, text: Text, debug: bool = False):
|
||||
if text.text is not None and len(text.text.strip()) > 0:
|
||||
text.init() # load default values
|
||||
if text.font in FONTS:
|
||||
text.text, font = fit_text(img.size, text)
|
||||
if text.angle == 0:
|
||||
draw.text(get_pos(img.size, text, font), text.text, fill=text.fill, align=text.align, font=font,
|
||||
draw.text(get_text_pos(img.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] * img.size[0], text.y_range[0] * img.size[1]),
|
||||
@@ -70,7 +59,7 @@ def draw_text(draw, img, text, debug=False):
|
||||
center_y = (text.y_range[0] + text.y_range[1]) * img.size[1] / 2
|
||||
txt_img = Image.new('RGBA', (width, height))
|
||||
txt_draw = ImageDraw.Draw(txt_img)
|
||||
txt_draw.text(get_pos(img.size, text, font, relative=True), text.text, fill=text.fill,
|
||||
txt_draw.text(get_text_pos(img.size, text, font, relative=True), 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:
|
||||
@@ -84,43 +73,30 @@ def draw_text(draw, img, text, debug=False):
|
||||
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:
|
||||
"""
|
||||
# TODO rework this function
|
||||
def fit_text(size: Tuple[int, int], text: Text) -> Tuple[str, ImageFont.FreeTypeFont]:
|
||||
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
|
||||
font_size = round(text.font_size * min(size)) + 1
|
||||
font = FONTS[text.font]
|
||||
t = ""
|
||||
text_content = ""
|
||||
while (text_size is None or text_size[0] >= max_width or text_size[1] >= max_height) and font_size > 1:
|
||||
font_size -= 1
|
||||
font = font.font_variant(size=font_size)
|
||||
k = 0 # number of lines
|
||||
while k == 0 or (t is not None and text_size[0] >= max_width):
|
||||
k += 1
|
||||
t = utils.justify_text(text.text, k)
|
||||
if t is not None:
|
||||
text_size = font.getsize_multiline(t, stroke_width=text.stroke_width * font_size)
|
||||
if t is None:
|
||||
n_lines = 0
|
||||
while n_lines == 0 or (text_content is not None and text_size[0] >= max_width):
|
||||
n_lines += 1
|
||||
text_content = utils.justify_text(text.text, n_lines)
|
||||
if text_content is not None:
|
||||
text_size = font.getsize_multiline(text_content, stroke_width=text.stroke_width * font_size)
|
||||
if text_content is None:
|
||||
# max break attained
|
||||
text_size = None # restart
|
||||
return t, font
|
||||
text_size = None # retry
|
||||
return text_content, font
|
||||
|
||||
|
||||
def get_pos(size, text, font, relative=False):
|
||||
"""
|
||||
:param (int,int) size: source image size
|
||||
:param (Text) text:
|
||||
:param (PIL.ImageFont.FreeTypeFont) font:
|
||||
:rtype (int,int)
|
||||
:return:
|
||||
"""
|
||||
def get_text_pos(size: Tuple[int, int], text: Text,
|
||||
font: ImageFont.FreeTypeFont, relative: bool = False) -> Tuple[int, int]:
|
||||
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])
|
||||
|
||||
+15
-33
@@ -1,3 +1,4 @@
|
||||
from typing import Optional
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -13,17 +14,14 @@ LIST = []
|
||||
logger = logging.getLogger("meme_db")
|
||||
|
||||
|
||||
def load_memes(purge=False):
|
||||
"""
|
||||
:param (bool) purge:
|
||||
"""
|
||||
def load_memes(purge: bool = False):
|
||||
global DATA, ALIASES
|
||||
if purge:
|
||||
DATA = {}
|
||||
ALIASES = {}
|
||||
try:
|
||||
with open(DATA_FILE) as f:
|
||||
content = "".join(f.readlines())
|
||||
with open(DATA_FILE) as input_file:
|
||||
content = "".join(input_file.readlines())
|
||||
raw_data = json.loads(content)
|
||||
if not (isinstance(raw_data, list)):
|
||||
raise TypeError(f"Root is not a list")
|
||||
@@ -37,12 +35,9 @@ def load_memes(purge=False):
|
||||
logger.error(f"Invalid data file: {e}")
|
||||
|
||||
|
||||
def load_item(i, item):
|
||||
"""
|
||||
:param (int) i:
|
||||
:param (dict) item:
|
||||
"""
|
||||
def load_item(i: int, item: dict):
|
||||
global LIST
|
||||
# TODO reduce complexity
|
||||
item_id = ""
|
||||
try:
|
||||
if not (isinstance(item, dict)):
|
||||
@@ -68,13 +63,13 @@ def load_item(i, item):
|
||||
raw_texts = utils.read_key(item, "texts", meme.texts, types=[dict], is_list=True)
|
||||
if "texts" in item:
|
||||
meme.texts = []
|
||||
c = 1
|
||||
current_text = 1
|
||||
for j in range(len(raw_texts)):
|
||||
raw_text = raw_texts[j]
|
||||
try:
|
||||
text = load_text(c, raw_text)
|
||||
text = load_text(current_text, raw_text)
|
||||
if text.text_ref is None:
|
||||
c += 1
|
||||
current_text += 1
|
||||
elif text.text_ref < 1 or text.text_ref > len(meme.texts):
|
||||
logger.warning(
|
||||
f"Item '{item_id}'({i + 1}) / Text {j + 1}: invalid text reference {text.text_ref}")
|
||||
@@ -90,7 +85,7 @@ def load_item(i, item):
|
||||
text.style_ref -= 1
|
||||
text.update(meme.texts[text.style_ref])
|
||||
meme.texts += [text]
|
||||
meme.texts_len = c - 1
|
||||
meme.texts_len = current_text - 1
|
||||
except TypeError as e:
|
||||
logger.warning(f"Item '{item_id}'({i + 1}) / Text {j + 1}: {e}")
|
||||
for text in meme.texts:
|
||||
@@ -117,17 +112,9 @@ def load_item(i, item):
|
||||
logger.warning(f"Item '{item_id}'({i + 1}): {e}")
|
||||
|
||||
|
||||
def load_text(c, raw_text, text=None):
|
||||
"""
|
||||
:param (int) c:
|
||||
:param (dict) raw_text:
|
||||
:param (Text|None) text:
|
||||
:raises TypeError:
|
||||
:rtype: Text
|
||||
:return:
|
||||
"""
|
||||
def load_text(current_text: int, raw_text: dict, text: Optional[Text] = None) -> Text:
|
||||
if text is None:
|
||||
text = Text(f"text {c}")
|
||||
text = Text(f"text {current_text}")
|
||||
text.font = utils.read_key_safe(raw_text, "font", 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)
|
||||
@@ -150,12 +137,7 @@ def load_text(c, raw_text, text=None):
|
||||
return text
|
||||
|
||||
|
||||
def get_meme(name):
|
||||
"""
|
||||
:param (str) name:
|
||||
:rtype: Meme|None
|
||||
:return:
|
||||
"""
|
||||
def get_meme(name: str) -> Optional[Meme]:
|
||||
name = name.lower().strip().replace(" ", "_")
|
||||
if name in ALIASES:
|
||||
return DATA[ALIASES[name]].clone()
|
||||
@@ -163,6 +145,6 @@ def get_meme(name):
|
||||
return None
|
||||
|
||||
|
||||
def find_nearest(word):
|
||||
def find_nearest(word: str) -> str:
|
||||
word = word.lower().strip().replace(" ", "_")
|
||||
return utils.find_nearest(word, ALIASES.keys())
|
||||
return utils.find_nearest(word, list(ALIASES))
|
||||
|
||||
@@ -59,4 +59,4 @@ def compute(*args, left_wmark_text=None, debug=False):
|
||||
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)
|
||||
return imgf.build_image(meme.template, meme.texts, debug=debug)
|
||||
|
||||
+5
-7
@@ -1,3 +1,4 @@
|
||||
from typing import Optional
|
||||
from enum import IntEnum
|
||||
import copy
|
||||
|
||||
@@ -18,7 +19,7 @@ class Pos(IntEnum):
|
||||
|
||||
|
||||
class Meme:
|
||||
def __init__(self, meme_id):
|
||||
def __init__(self, meme_id: str):
|
||||
self.id = meme_id
|
||||
self.aliases = []
|
||||
self.abstract = None
|
||||
@@ -28,7 +29,7 @@ class Meme:
|
||||
self.texts = None
|
||||
self.texts_len = 0
|
||||
|
||||
def clone(self):
|
||||
def clone(self) -> 'Meme':
|
||||
return copy.deepcopy(self)
|
||||
|
||||
|
||||
@@ -36,7 +37,7 @@ class Text:
|
||||
base_properties = ["font", "font_size", "fill", "stroke_width",
|
||||
"stroke_fill", "align", "position"]
|
||||
|
||||
def __init__(self, text=None):
|
||||
def __init__(self, text: Optional[str] = None):
|
||||
self.text = text
|
||||
self.text_ref = None
|
||||
|
||||
@@ -56,10 +57,7 @@ class Text:
|
||||
self.align = None
|
||||
self.position = None
|
||||
|
||||
def update(self, base):
|
||||
"""
|
||||
:param (Text) base:
|
||||
"""
|
||||
def update(self, base: 'Text'):
|
||||
for prop in Text.base_properties:
|
||||
if getattr(self, prop) is None:
|
||||
setattr(self, prop, getattr(base, prop))
|
||||
|
||||
Reference in New Issue
Block a user