diff --git a/docs/build.py b/docs/build.py index e4a597d..3e1651e 100644 --- a/docs/build.py +++ b/docs/build.py @@ -42,7 +42,7 @@ img_line = None i = None for i, meme_id in enumerate(id_list): meme = meme_db.get_meme(meme_id) - img = img_factory.build_image(meme.template, meme.texts, debug=True) + img = img_factory.build_from_template(meme.template, meme.texts, debug=True) if img is not None: img.save(path.join(templates_dir, meme.template)) size = (round(img.size[0] * IMG_HEIGHT / img.size[1]), IMG_HEIGHT) diff --git a/meme_otron/img_factory.py b/meme_otron/img_factory.py index 30f7289..a480a5b 100644 --- a/meme_otron/img_factory.py +++ b/meme_otron/img_factory.py @@ -3,6 +3,7 @@ from PIL import Image, ImageFont, ImageDraw import os import os.path as path import logging +import sys from . import utils from .types import Text @@ -12,6 +13,8 @@ TEMPLATES_DIR = utils.relative_path(__file__, "..", "templates") FONTS = {} +TEXT_IMAGE_WIDTH = 800 + logger = logging.getLogger("img_factory") @@ -27,6 +30,8 @@ def load_fonts(): def compose_image(images: List[Image.Image]) -> Image.Image: + if len(images) == 1: + return images[0] width = min([img.size[0] for img in images]) for i, img in enumerate(images): if img.size[0] != width: @@ -40,7 +45,7 @@ def compose_image(images: List[Image.Image]) -> Image.Image: return output_image -def build_image(template: str, texts: List[Text], debug: bool = False) -> Optional[Image.Image]: +def build_from_template(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: @@ -50,8 +55,26 @@ def build_image(template: str, texts: List[Text], debug: bool = False) -> Option return img +def build_text_only(texts: List[Text], debug: bool = False) -> Image.Image: + heights = [] + for text in texts: + text.init() + text.text, font = fit_text((TEXT_IMAGE_WIDTH, sys.maxsize), text) + text_size = font.getsize_multiline(text.text, stroke_width=text.stroke_width * font.size) + heights += [round(text_size[1] / (text.y_range[1] - text.y_range[0]))] + max_height = sum(heights) + for i, text in enumerate(texts): + range_factor = heights[i] / max_height + start = sum(heights[:i]) / max_height + text.y_range = (start + text.y_range[0] * range_factor, start + text.y_range[1] * range_factor) + pass + txt_img = Image.new('RGBA', (TEXT_IMAGE_WIDTH, max_height), (255, 255, 255)) + return apply_texts(txt_img, texts, debug=debug) + + def apply_texts(img: Image.Image, texts: List[Text], debug: bool = False) -> Image.Image: - img = img.convert(mode='RGBA') + if img.mode != 'RGBA': + img = img.convert(mode='RGBA') draw = ImageDraw.Draw(img) for text in texts: draw_text(draw, img, text, debug=debug) @@ -95,10 +118,10 @@ def fit_text(size: Tuple[int, int], text: Text) -> Tuple[str, ImageFont.FreeType 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_size = round(text.font_size * size[0]) + 1 font = FONTS[text.font] text_content = "" - while (text_size is None or text_size[0] >= max_width or text_size[1] >= max_height) and font_size > 1: + 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) n_lines = 0 diff --git a/meme_otron/meme_otron.py b/meme_otron/meme_otron.py index fedc8e1..1291cd3 100644 --- a/meme_otron/meme_otron.py +++ b/meme_otron/meme_otron.py @@ -24,42 +24,62 @@ left_wmark.font_size = 0.02 left_wmark.x_range = [0.005, 0.995] left_wmark.y_range = [0.005, 0.995] +simple_text = Text() +simple_text.align = "left" +simple_text.position = Pos.W +simple_text.font_size = 0.04 +simple_text.x_range = [0.01, 0.99] +simple_text.y_range = [0.2, 0.8] -def compute(*args: str, left_wmark_text: Optional[Text] = None, debug: bool = False) -> Optional[Image.Image]: + +def compute(*args: str, left_wmark_text: Optional[str] = None, debug: bool = False) -> Optional[Image.Image]: if len(args) < 1: return None parts = utils.split_arguments(args, "-") images = [] for part in parts: - images += [compute_part(*part, debug=debug)] + img = compute_part(*part, debug=debug) + if img is not None: + images += [img] + + if len(images) == 0: + return None output_image = img_factory.compose_image(images) watermarks = [right_wmark] if left_wmark_text is not None: - left_wmark.text = left_wmark_text - watermarks += [left_wmark] + watermarks += [left_wmark.variant(left_wmark_text)] output_image = img_factory.apply_texts(output_image, watermarks, debug=debug) return output_image def compute_part(*args: str, debug: bool = False) -> Optional[Image.Image]: - meme_id = args[0] - meme = meme_db.get_meme(meme_id) - if meme is None: - logger.warning(f"Meme template '{meme_id}' not found") + meme_id = args[0].lower().strip() + + if meme_id == "text": + if len(args) < 2: + return None + texts = [simple_text.variant(arg) for arg in args[1:]] + return img_factory.build_text_only(texts, debug=debug) + elif meme_id == "image": return None - if len(args) > 1: - c = 0 - for i in range(len(meme.texts)): - if meme.texts[i].text_ref is None: - if c < len(args) - 1: - meme.texts[i].text = args[c + 1].replace("\\n", "\n") + else: + meme = 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: + c = 0 + for i in range(len(meme.texts)): + if meme.texts[i].text_ref is None: + if c < len(args) - 1: + meme.texts[i].text = args[c + 1].replace("\\n", "\n") + else: + meme.texts[i].text = "" + c += 1 else: - meme.texts[i].text = "" - c += 1 - else: - meme.texts[i].text = meme.texts[meme.texts[i].text_ref].text - return img_factory.build_image(meme.template, meme.texts, debug=debug) + meme.texts[i].text = meme.texts[meme.texts[i].text_ref].text + return img_factory.build_from_template(meme.template, meme.texts, debug=debug) diff --git a/meme_otron/types.py b/meme_otron/types.py index 887fd94..5751a39 100644 --- a/meme_otron/types.py +++ b/meme_otron/types.py @@ -57,6 +57,11 @@ class Text: self.align = None self.position = None + def variant(self, text: str) -> 'Text': + new_text = copy.deepcopy(self) + new_text.text = text + return new_text + def update(self, base: 'Text'): for prop in Text.base_properties: if getattr(self, prop) is None: diff --git a/tests/unit/meme_otron/test_types.py b/tests/unit/meme_otron/test_types.py index a200b23..7de8e77 100644 --- a/tests/unit/meme_otron/test_types.py +++ b/tests/unit/meme_otron/test_types.py @@ -20,13 +20,22 @@ class TestText(TestCase): txt2.fill = [0, 1, 0] txt2.stroke_width = 5 txt1.update(txt2) - self.assertEqual("txt1", txt1.text, "text keeped") - self.assertIsNone(txt1.angle, "angle keeped") - self.assertEqual((0, 1), txt1.x_range, "position keeped") + self.assertEqual("txt1", txt1.text, "text kept") + self.assertIsNone(txt1.angle, "angle kept") + self.assertEqual((0, 1), txt1.x_range, "position kept") self.assertEqual(txt2.fill, txt1.fill, "fill changed") - self.assertNotEqual(txt2.stroke_width, txt1.stroke_width, "stroke_width keeped") + self.assertNotEqual(txt2.stroke_width, txt1.stroke_width, "stroke_width kept") self.assertEqual(6, txt1.stroke_width) + def test_variant(self): + txt1 = types.Text("txt1") + txt1.stroke_width = 6 + txt1.x_range = (0.5, 0.8) + txt2 = txt1.variant("txt2") + self.assertEqual("txt2", txt2.text, "text changed") + self.assertIsNone(txt2.angle, "angle kept") + self.assertEqual((0.5, 0.8), txt2.x_range, "position kept") + def test_init(self): txt1 = types.Text("txt1") txt1.fill = [0, 1, 0] diff --git a/tools/live_edit.py b/tools/live_edit.py index d459214..430428e 100644 --- a/tools/live_edit.py +++ b/tools/live_edit.py @@ -30,7 +30,7 @@ while True: count = 0 for meme_id in meme_db.LIST: meme = meme_db.get_meme(meme_id) - img = img_factory.build_image(meme.template, meme.texts, debug=True) + img = img_factory.build_from_template(meme.template, meme.texts, debug=True) if img is not None: img.save(path.join(dst_dir, meme.template)) count += 1