diff --git a/README.md b/README.md index e022ccf..b9b4b19 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,14 @@ You can invite the bot on your server with [this link](https://discordapp.com/ap ## History +* 1.3 + * **Complex memes syntax** + * Examples in docs + * "Reaction" templates + * Reworked CLI arguments + * More unit testing + * More docs + * Bug fix * 1.2 * Reworked text fitting * Unit testing diff --git a/discord_bot/__main__.py b/discord_bot/__main__.py index 4264e13..d602e45 100644 --- a/discord_bot/__main__.py +++ b/discord_bot/__main__.py @@ -3,7 +3,7 @@ import traceback import logging import discord import re -import tempfile +from io import BytesIO import sys from datetime import datetime from dotenv import load_dotenv @@ -116,28 +116,32 @@ async def on_message(message: discord.Message): if len(args) > 1 and message.author.display_name is not None: left_wmark_text = f"By {message.author.display_name}" logging.info(args[0]) - meme_id = re.sub(r'[^A-Za-z0-9 _]', "", args[0]).strip() - args[0] = meme_id - img = meme_otron.compute(*args, left_wmark_text=left_wmark_text) - if img is None: - if len(meme_id) == 0: - response = f":warning: Template not found\n" - else: - hint = meme_db.find_nearest(meme_id) - response = f":warning: Template `{meme_id}` not found\n" - if hint is not None: - response += f"Did you mean `{hint}`?\n" - response += f"You can find a more detailed help and a list of templates at:\n" \ + + input_data = None + if len(message.attachments) > 0: + input_data = await message.attachments[0].read() + + img, errors = meme_otron.compute(*args, left_wmark_text=left_wmark_text, + input_data=input_data, max_file_size=8 * 1024 * 1024) + if len(errors) > 0: + response = ":warning:" + for err in errors: + response += "\n" + err.replace("'", "`").replace("`` ", "") + response += f"\nYou can find a more detailed help and a list of templates at:\n" \ f"<{DOC_URL}>" if len(response) >= 2000: await message.channel.send(f"{message.author.mention} ... really?") else: await message.channel.send(response) else: - with tempfile.NamedTemporaryFile(delete=False) as output: - img.save(output, format="JPEG") + with BytesIO() as output_file: + img.save(output_file, format="JPEG") + output_file.flush() + output_file.seek(0) + response = None - if len(args) == 1: + meme_id = utils.sanitize_input(args[0]) + if len(args) == 1 and meme_id not in ["image", "text"]: meme = meme_db.get_meme(meme_id) response = f"Template `{meme.id}`:" if len(meme.aliases) > 0: @@ -152,13 +156,8 @@ async def on_message(message: discord.Message): response = f"A meme by {message.author.mention}:" if message_id not in SENT: SENT[message_id] = [] - response = await message.channel.send(response, - file=discord.File(filename="meme.jpg", fp=output.name)) + response = await message.channel.send(response, file=discord.File(output_file, "meme.jpg")) SENT[message_id] += [response] - try: - os.remove(output.name) - except PermissionError: - pass if not is_direct: await delete(message) diff --git a/docs/README-template.md b/docs/README-template.md new file mode 100644 index 0000000..b1f2d32 --- /dev/null +++ b/docs/README-template.md @@ -0,0 +1,164 @@ +# Meme-Otron guide + +* [Commands](#commands) + * [Simple use](#simple-use) + * [Advanced use](#advanced-use) + * [Discord features](#discord-features) + * [CLI features](#cli-features) +* [List of templates](#list-of-templates) + * [Standard Templates](#standard-templates) + * [Reactions (no text)](#reactions-no-text) +* [Examples](#examples) + * [Example 1: Simple template](#example-1-simple-template) + * [Example 2: Use of empty texts](#example-2-use-of-empty-texts) + * [Example 3: Text + Template](#example-3-text--template) + * [Example 4: Complex composition](#example-4-complex-composition) + + +## Commands + +### Simple use +[↑ back to top](#meme-otron-guide) + +You can generate memes by using the following arguments: + +``` +[meme id] "text1" "text2" ... +``` + +Depending of the number of `"text"` arguments, several behavior occurs: +* **None**: you get the template that gives you the locations of texts. (see below) +* **Less than the template's**: the remaining texts are blank on the output +* **More than the template's**: the extra arguments are ignored + +> Notes +> * You don't have to use all texts shown on the templates +> * You can use an empty text argument ( `""` ) to skip a text and keep it blank + +See [Examples](#examples) to get an idea of how to use it. + +### Advanced use +[↑ back to top](#meme-otron-guide) + +Since version 1.3, Meme-Otron allows you to "pipe" parts in order to compose more advanced memes. The syntax is as follows: + +``` +[part1] - [part2] - ... +``` + +Each `part` can be one of the following: + +* A template: as described in [Simple use](#simple-use) +* Texts: ```text "text 1" "text 2" ...``` + * Black Arial texts on white background + * Each text is it's own paragraph +* Images: ```image ``` + * Takes an image from input or an URL (optional) + * Input depends on the system: + * the Discord bot takes the attachment + * the CLI takes stdin or `--input` argument. + +> Notes +> * Input of `image` is always the same, don't expect multiple instances of `image` to get different results if you don't indicate an URL + +See [Examples](#examples) to get an idea of how to use it. + +### Discord features +[↑ back to top](#meme-otron-guide) + +Tag the bot and use the above syntax to get started. In addition, you can use the following commands: + +* Use `help` to get a simple help message +* Use `list` to get a list of all meme ids +* Use `delete` to delete the last message sent by the bot (directed to you) + +To get the template info, just send the meme id without texts. + +> Tip : You can use `\\n` in your texts to add a line break + +Enjoy the full experience of this bot by using direct messages to keep your server free of spam. + +### CLI features +[↑ back to top](#meme-otron-guide) + +In this project directory, you can simply call: +``` +python -m meme_otron [meme id] "text1" "text2" ... > output.jpg +``` +Without pipe redirection with `-o [output]`: +``` +python -m meme_otron -o output.png [meme id] "text1" "text2" ... +``` + +You can even pipe input images like this: +``` +python -m meme_otron [arguments] < input.jpg > output.jpg +``` + +Available arguments: +* `--help` / `-h` + * Show a simple guide +* `--output [file]` / `-o [file]` + * Output file, you are free to choose the format +* `--input [file]` / `-i [file]` + * Input file used for `image` +* `-nw` / `--no-watermark` + * Removes the watermark +* `-d` / `--debug` + * Add more info to output like a box show the texts boundaries +* `-v` / `--verbose` + * Add more logging + + +## List of templates +[↑ back to top](#meme-otron-guide) + +You can find here the full list of templates. +Each one has extra info and an image showing how texts are placed. +Click on an image to enlarge it. + +### Standard Templates +[↑ back to top](#meme-otron-guide) + + + + + +### Reactions (no text) +[↑ back to top](#meme-otron-guide) + + + + + + +## Examples + +### Example 1: Simple template +[↑ back to top](#meme-otron-guide) + + + + + +### Example 2: Use of empty texts +[↑ back to top](#meme-otron-guide) + + + + + +### Example 3: Text + Template +[↑ back to top](#meme-otron-guide) + + + + + + +### Example 4: Complex composition +[↑ back to top](#meme-otron-guide) + + + + \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 0d596e0..fde809e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,25 @@ +# Meme-Otron guide + +* [Commands](#commands) + * [Simple use](#simple-use) + * [Advanced use](#advanced-use) + * [Discord features](#discord-features) + * [CLI features](#cli-features) +* [List of templates](#list-of-templates) + * [Standard Templates](#standard-templates) + * [Reactions (no text)](#reactions-no-text) +* [Examples](#examples) + * [Example 1: Simple template](#example-1-simple-template) + * [Example 2: Use of empty texts](#example-2-use-of-empty-texts) + * [Example 3: Text + Template](#example-3-text--template) + * [Example 4: Complex composition](#example-4-complex-composition) + + ## Commands +### Simple use +[↑ back to top](#meme-otron-guide) + You can generate memes by using the following arguments: ``` @@ -15,7 +35,36 @@ Depending of the number of `"text"` arguments, several behavior occurs: > * You don't have to use all texts shown on the templates > * You can use an empty text argument ( `""` ) to skip a text and keep it blank -## Discord features +See [Examples](#examples) to get an idea of how to use it. + +### Advanced use +[↑ back to top](#meme-otron-guide) + +Since version 1.3, Meme-Otron allows you to "pipe" parts in order to compose more advanced memes. The syntax is as follows: + +``` +[part1] - [part2] - ... +``` + +Each part can be one of the following: + +* A template: as described in [Simple use](#simple-use) +* Texts: ```text "text 1" "text 2" ...``` + * Black Arial texts on white background + * Each text is it's own paragraph +* Images: ```image ``` + * Takes an image from input or an URL (optional) + * Input depends on the system: + * the Discord bot takes the attachment + * the CLI takes stdin or `--input` argument. + +> Notes +> * Input of `image` is always the same, don't expect multiple instances of `image` to get different results if you don't indicate an URL + +See [Examples](#examples) to get an idea of how to use it. + +### Discord features +[↑ back to top](#meme-otron-guide) Tag the bot and use the above syntax to get started. In addition, you can use the following commands: @@ -29,7 +78,8 @@ To get the template info, just send the meme id without texts. Enjoy the full experience of this bot by using direct messages to keep your server free of spam. -## CLI features +### CLI features +[↑ back to top](#meme-otron-guide) In this project directory, you can simply call: ``` @@ -40,16 +90,37 @@ Without pipe redirection with `-o [output]`: python -m meme_otron -o output.png [meme id] "text1" "text2" ... ``` -> Note: with `-o`, you are free to choose the output format +You can even pipe input images like this: +``` +python -m meme_otron [arguments] < input.jpg > output.jpg +``` + +Available arguments: +* `--help` / `-h` + * Show a simple guide +* `--output [file]` / `-o [file]` + * Output file, you are free to choose the format +* `--input [file]` / `-i [file]` + * Input file used for `image` +* `-nw` / `--no-watermark` + * Removes the watermark +* `-d` / `--debug` + * Add more info to output like a box show the texts boundaries +* `-v` / `--verbose` + * Add more logging + ## List of templates +[↑ back to top](#meme-otron-guide) You can find here the full list of templates. Each one has extra info and an image showing how texts are placed. Click on an image to enlarge it. +### Standard Templates +[↑ back to top](#meme-otron-guide) - + |||| |:---:|:---:|:---:| |**aliens**
more info|**alive**
alt: no_brain
more info|**argument**
alt: wrestlers
more info| @@ -88,8 +159,79 @@ Click on an image to enlarge it. |enlarge|enlarge|enlarge| |**t_pose**
alt: dominance, monika
more info|**tom_cousins**
alt: cousins, backup, goons
more info|**tough2**
alt: tough, fight
more info| |enlarge|enlarge|enlarge| -|**tough2bis**|**tough3**|**trump**
alt: law
more info| +|**tough2bis**
alt: soft|**tough3**|**trump**
alt: law
more info| |enlarge|enlarge|enlarge| |**trust_nobody**
alt: yourself, gun
more info|**truth**
alt: scroll
more info|**winnie2**
alt: winnie
more info| |enlarge|enlarge|enlarge||| - + + +### Reactions (no text) +[↑ back to top](#meme-otron-guide) + + + + + + +## Examples + +### Example 1: Simple template +[↑ back to top](#meme-otron-guide) + + +> + +``` +brain3 +"Making memes using an image editor" +"Making memes using a Python script" +"Making memes using a Discord bot" +``` + +![](example1.jpg) + + +### Example 2: Use of empty texts +[↑ back to top](#meme-otron-guide) + + +> The 5th text is not set and the 3rd is explicitly set to empty + +``` +see_that_guy +"See that guy over there?" +"He uses an image editor to make memes" +"" +"meme-otron dev" +``` + +![](example2.jpg) + + +### Example 3: Text + template +[↑ back to top](#meme-otron-guide) + + +> Note how texts make paragraphs + +``` +text +"*Meme has a 'made with meme-otron' watermark*" +"reddit: ..." +"9gag: ..." +"meme-otron dev:" +- +culture +"meme otron" +``` + +![](example3.jpg) + + + +### Example 4: Complex composition +[↑ back to top](#meme-otron-guide) + + + + \ No newline at end of file diff --git a/docs/build.py b/docs/build.py index e4a597d..e46dac5 100644 --- a/docs/build.py +++ b/docs/build.py @@ -1,10 +1,12 @@ import os import logging +from typing import List import PIL from os import path from meme_otron import img_factory from meme_otron import meme_db from meme_otron import utils +from meme_otron import meme_otron logging.basicConfig(format="[%(asctime)s][%(levelname)s][%(module)s] %(message)s", level=logging.WARNING) @@ -13,12 +15,66 @@ meme_db.load_memes() templates_dir = utils.relative_path(__file__, "templates") preview_dir = utils.relative_path(__file__, "preview") +doc_template_file = utils.relative_path(__file__, "README-template.md") doc_file = utils.relative_path(__file__, "README.md") COLUMNS = 3 IMG_HEIGHT = 400 +def main(): + make_empty(templates_dir) + make_empty(preview_dir) + + with open(doc_template_file, mode='r') as f: + content = "".join(f.readlines()) + + full_list = sorted(meme_db.LIST) + template_list = [meme_id for meme_id in full_list if len(meme_db.get_meme(meme_id).texts) > 0] + reaction_list = [meme_id for meme_id in full_list if meme_id not in template_list] + + content = produce_template_list(content, "LIST1", template_list) + content = produce_template_list(content, "LIST2", reaction_list) + + content = produce_example(content, "EXAMPLE1", "example1.jpg", "", + "brain3", + "Making memes using an image editor", + "Making memes using a Python script", + "Making memes using a Discord bot") + + content = produce_example(content, "EXAMPLE2", "example2.jpg", + "The 5th text is not set and the 3rd is explicitly set to empty", + "see_that_guy", + "See that guy over there?", + "He uses an image editor to make memes", + "", + "meme-otron's dev") + + content = produce_example(content, "EXAMPLE3", "example3.jpg", + "Note how texts make paragraphs", + "text", + "*Meme has a 'made with meme-otron' watermark*", + "reddit: ...", + "9gag: ...", + "meme-otron's dev:", + "-", + "culture", + "meme otron") + + content = produce_example(content, "EXAMPLE4", "example4.jpg", + "", + "image", + "https://i.imgur.com/DNLFUuK.png", + "-", + "text", + "meme-otron's dev close to finishing the idea", + "-", + "holup") + + with open(doc_file, mode='w') as f: + f.write(content) + + def make_empty(target_dir: str): if path.exists(target_dir): for file in os.listdir(target_dir): @@ -28,54 +84,69 @@ def make_empty(target_dir: str): os.mkdir(target_dir) -make_empty(templates_dir) -make_empty(preview_dir) +def produce_template_list(content: str, tag: str, id_list: List[str]): + if len(id_list) == 0: + return content + doc_content = "|" * (COLUMNS + 1) \ + + "\n|" + ":---:|" * COLUMNS + info_line = None + img_line = None + i = None + for i, meme_id in enumerate(id_list): + meme = meme_db.get_meme(meme_id) + img = img_factory.build_from_template(meme.template, meme.texts, debug=True) + if img is not None: + base = True + if len(meme.texts) > 0: + base = False + image_path = path.join(templates_dir, meme.template) + img.save(image_path) + size = (round(img.size[0] * IMG_HEIGHT / img.size[1]), IMG_HEIGHT) + img2 = img.resize(size, resample=PIL.Image.LANCZOS) + img2.save(path.join(preview_dir, meme.template)) + if i % COLUMNS == 0: + if info_line is not None and img_line is not None: + doc_content += info_line + img_line + info_line = "\n|" + img_line = "\n|" + info_line += f"**{meme_id}**" + if len(meme.aliases) > 0: + info_line += f"
alt: {', '.join(meme.aliases)}" + if meme.info is not None: + info_line += f"
more info" + info_line += "|" + if base: + img_line += f"" + else: + img_line += f"" + img_line += f"enlarge" \ + f"|" + print(i, meme_id) + info_line += "|" * (COLUMNS - (i % COLUMNS)) + img_line += "|" * (COLUMNS - (i % COLUMNS)) + doc_content += info_line + img_line + return inject_content(doc_content, content, tag) -id_list = sorted(meme_db.LIST) -doc_content = "|" * (COLUMNS + 1) \ - + "\n|" + ":---:|" * COLUMNS - -info_line = None -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) +def produce_example(content: str, tag: str, file_name: str, note: str, *args: str): + doc_content = f"> {note}\n\n" \ + "```\n" + \ + " \n".join(['"' + a + '"' if ' ' in a or len(a) == 0 else a for a in args]) + \ + "\n```\n\n" \ + f"![]({file_name})" + img, err = meme_otron.compute(*args) 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) - img2 = img.resize(size, resample=PIL.Image.LANCZOS) - img2.save(path.join(preview_dir, meme.template)) - if i % COLUMNS == 0: - if info_line is not None and img_line is not None: - doc_content += info_line + img_line - info_line = "\n|" - img_line = "\n|" - info_line += f"**{meme_id}**" - if len(meme.aliases) > 0: - info_line += f"
alt: {', '.join(meme.aliases)}" - if meme.info is not None: - info_line += f"
more info" - info_line += "|" - img_line += f"" \ - f"" \ - f"enlarge" \ - f"|" - print(i, meme_id) + img.save(utils.relative_path(__file__, file_name)) + return inject_content(doc_content, content, tag) -doc_content += "|" * (COLUMNS - (i % COLUMNS)) -with open(doc_file, mode='r') as f: - content = "".join(f.readlines()) +def inject_content(new_content, content, tag): + start_str = f"" + end_str = f"" + i0 = content.index(start_str) + i1 = content.index(end_str) + len(end_str) + return content[:i0] + start_str + "\n" + new_content + "\n" + end_str + content[i1:] -i0 = content.index("") -i1 = content.index("") + len("") -with open(doc_file, mode='w') as f: - f.write(content[:i0]) - f.write("\n") - f.write(doc_content) - f.write("\n") - f.write(content[i1:]) +if __name__ == '__main__': + main() diff --git a/docs/preview/bender.jpg b/docs/preview/bender.jpg index 4e05aa4..5e12ff8 100644 Binary files a/docs/preview/bender.jpg and b/docs/preview/bender.jpg differ diff --git a/docs/preview/born_cool.jpg b/docs/preview/born_cool.jpg index 8db7263..d7d68fa 100644 Binary files a/docs/preview/born_cool.jpg and b/docs/preview/born_cool.jpg differ diff --git a/docs/preview/buff.jpg b/docs/preview/buff.jpg index d5b9053..4f1851f 100644 Binary files a/docs/preview/buff.jpg and b/docs/preview/buff.jpg differ diff --git a/docs/preview/button.jpg b/docs/preview/button.jpg index 503b846..82ff049 100644 Binary files a/docs/preview/button.jpg and b/docs/preview/button.jpg differ diff --git a/docs/preview/bye_mom.jpg b/docs/preview/bye_mom.jpg index f7ad0bc..51b587a 100644 Binary files a/docs/preview/bye_mom.jpg and b/docs/preview/bye_mom.jpg differ diff --git a/docs/preview/culture.jpg b/docs/preview/culture.jpg index 2834bb6..af5e546 100644 Binary files a/docs/preview/culture.jpg and b/docs/preview/culture.jpg differ diff --git a/docs/preview/disappointed.jpg b/docs/preview/disappointed.jpg index 4c8d2d5..60b2d33 100644 Binary files a/docs/preview/disappointed.jpg and b/docs/preview/disappointed.jpg differ diff --git a/docs/preview/drift.jpg b/docs/preview/drift.jpg index f8c07e0..7a1ee8b 100644 Binary files a/docs/preview/drift.jpg and b/docs/preview/drift.jpg differ diff --git a/docs/preview/everywhere.jpg b/docs/preview/everywhere.jpg index ce2da02..3c38217 100644 Binary files a/docs/preview/everywhere.jpg and b/docs/preview/everywhere.jpg differ diff --git a/docs/preview/fight.jpg b/docs/preview/fight.jpg index 08e7bf7..afb0808 100644 Binary files a/docs/preview/fight.jpg and b/docs/preview/fight.jpg differ diff --git a/docs/preview/fine.jpg b/docs/preview/fine.jpg index a2dd592..b1e6e81 100644 Binary files a/docs/preview/fine.jpg and b/docs/preview/fine.jpg differ diff --git a/docs/preview/gate.jpg b/docs/preview/gate.jpg index a1bf287..a62eeec 100644 Binary files a/docs/preview/gate.jpg and b/docs/preview/gate.jpg differ diff --git a/docs/preview/girl_cat.jpg b/docs/preview/girl_cat.jpg index af17341..17e8c07 100644 Binary files a/docs/preview/girl_cat.jpg and b/docs/preview/girl_cat.jpg differ diff --git a/docs/preview/grandma.jpg b/docs/preview/grandma.jpg index afc38a6..89794d1 100644 Binary files a/docs/preview/grandma.jpg and b/docs/preview/grandma.jpg differ diff --git a/docs/preview/gru.jpg b/docs/preview/gru.jpg index 587a50e..da92ced 100644 Binary files a/docs/preview/gru.jpg and b/docs/preview/gru.jpg differ diff --git a/docs/preview/handshake.jpg b/docs/preview/handshake.jpg index 0089303..6fca066 100644 Binary files a/docs/preview/handshake.jpg and b/docs/preview/handshake.jpg differ diff --git a/docs/preview/handshake2.jpg b/docs/preview/handshake2.jpg index ae110eb..22cf0e9 100644 Binary files a/docs/preview/handshake2.jpg and b/docs/preview/handshake2.jpg differ diff --git a/docs/preview/mini.jpg b/docs/preview/mini.jpg index 93d4603..14f1906 100644 Binary files a/docs/preview/mini.jpg and b/docs/preview/mini.jpg differ diff --git a/docs/preview/overconfident.jpg b/docs/preview/overconfident.jpg index 5af506e..888489d 100644 Binary files a/docs/preview/overconfident.jpg and b/docs/preview/overconfident.jpg differ diff --git a/docs/preview/pigeon.jpg b/docs/preview/pigeon.jpg index 7683cd7..63d57d2 100644 Binary files a/docs/preview/pigeon.jpg and b/docs/preview/pigeon.jpg differ diff --git a/docs/preview/seagull4.jpg b/docs/preview/seagull4.jpg index 9503542..e431d58 100644 Binary files a/docs/preview/seagull4.jpg and b/docs/preview/seagull4.jpg differ diff --git a/docs/preview/spiderman.jpg b/docs/preview/spiderman.jpg index 496f23d..4b4e4f3 100644 Binary files a/docs/preview/spiderman.jpg and b/docs/preview/spiderman.jpg differ diff --git a/docs/preview/tom_cousins.jpg b/docs/preview/tom_cousins.jpg index ad81c87..89f2675 100644 Binary files a/docs/preview/tom_cousins.jpg and b/docs/preview/tom_cousins.jpg differ diff --git a/docs/preview/tough2.jpg b/docs/preview/tough2.jpg index 0df3511..b4a75f0 100644 Binary files a/docs/preview/tough2.jpg and b/docs/preview/tough2.jpg differ diff --git a/docs/preview/tough2bis.jpg b/docs/preview/tough2bis.jpg index b46875e..b34b1ee 100644 Binary files a/docs/preview/tough2bis.jpg and b/docs/preview/tough2bis.jpg differ diff --git a/docs/preview/trump.jpg b/docs/preview/trump.jpg index 1ede05e..7cff7aa 100644 Binary files a/docs/preview/trump.jpg and b/docs/preview/trump.jpg differ diff --git a/docs/preview/trust_nobody.jpg b/docs/preview/trust_nobody.jpg index 6b3f83e..e0fbf69 100644 Binary files a/docs/preview/trust_nobody.jpg and b/docs/preview/trust_nobody.jpg differ diff --git a/docs/preview/truth.jpg b/docs/preview/truth.jpg index a086343..4b1452c 100644 Binary files a/docs/preview/truth.jpg and b/docs/preview/truth.jpg differ diff --git a/docs/preview/winnie2.jpg b/docs/preview/winnie2.jpg index fbb3a85..b626cbb 100644 Binary files a/docs/preview/winnie2.jpg and b/docs/preview/winnie2.jpg differ diff --git a/docs/templates/bender.jpg b/docs/templates/bender.jpg index 6a54832..f3f72f7 100644 Binary files a/docs/templates/bender.jpg and b/docs/templates/bender.jpg differ diff --git a/docs/templates/born_cool.jpg b/docs/templates/born_cool.jpg index 1827dd2..deb4698 100644 Binary files a/docs/templates/born_cool.jpg and b/docs/templates/born_cool.jpg differ diff --git a/docs/templates/buff.jpg b/docs/templates/buff.jpg index 3a98cfc..875b9fc 100644 Binary files a/docs/templates/buff.jpg and b/docs/templates/buff.jpg differ diff --git a/docs/templates/button.jpg b/docs/templates/button.jpg index 3731601..a64fa26 100644 Binary files a/docs/templates/button.jpg and b/docs/templates/button.jpg differ diff --git a/docs/templates/bye_mom.jpg b/docs/templates/bye_mom.jpg index cfec0d4..91533a5 100644 Binary files a/docs/templates/bye_mom.jpg and b/docs/templates/bye_mom.jpg differ diff --git a/docs/templates/culture.jpg b/docs/templates/culture.jpg index 4f872c6..1bd99d8 100644 Binary files a/docs/templates/culture.jpg and b/docs/templates/culture.jpg differ diff --git a/docs/templates/disappointed.jpg b/docs/templates/disappointed.jpg index d51959a..b4c1df4 100644 Binary files a/docs/templates/disappointed.jpg and b/docs/templates/disappointed.jpg differ diff --git a/docs/templates/drift.jpg b/docs/templates/drift.jpg index 5ff4a6b..bbcf064 100644 Binary files a/docs/templates/drift.jpg and b/docs/templates/drift.jpg differ diff --git a/docs/templates/everywhere.jpg b/docs/templates/everywhere.jpg index 3509903..eb72d9f 100644 Binary files a/docs/templates/everywhere.jpg and b/docs/templates/everywhere.jpg differ diff --git a/docs/templates/fight.jpg b/docs/templates/fight.jpg index 622187b..70de25d 100644 Binary files a/docs/templates/fight.jpg and b/docs/templates/fight.jpg differ diff --git a/docs/templates/fine.jpg b/docs/templates/fine.jpg index 5de37fe..db1c11e 100644 Binary files a/docs/templates/fine.jpg and b/docs/templates/fine.jpg differ diff --git a/docs/templates/gate.jpg b/docs/templates/gate.jpg index 4563c3d..57068b0 100644 Binary files a/docs/templates/gate.jpg and b/docs/templates/gate.jpg differ diff --git a/docs/templates/girl_cat.jpg b/docs/templates/girl_cat.jpg index ab23119..6d6bc8d 100644 Binary files a/docs/templates/girl_cat.jpg and b/docs/templates/girl_cat.jpg differ diff --git a/docs/templates/grandma.jpg b/docs/templates/grandma.jpg index b68ecd6..f623030 100644 Binary files a/docs/templates/grandma.jpg and b/docs/templates/grandma.jpg differ diff --git a/docs/templates/gru.jpg b/docs/templates/gru.jpg index 1ebe33b..37cc74d 100644 Binary files a/docs/templates/gru.jpg and b/docs/templates/gru.jpg differ diff --git a/docs/templates/handshake.jpg b/docs/templates/handshake.jpg index 774483a..a84ec69 100644 Binary files a/docs/templates/handshake.jpg and b/docs/templates/handshake.jpg differ diff --git a/docs/templates/handshake2.jpg b/docs/templates/handshake2.jpg index 4038a6a..210779d 100644 Binary files a/docs/templates/handshake2.jpg and b/docs/templates/handshake2.jpg differ diff --git a/docs/templates/mini.jpg b/docs/templates/mini.jpg index 3cfb638..47c0d04 100644 Binary files a/docs/templates/mini.jpg and b/docs/templates/mini.jpg differ diff --git a/docs/templates/overconfident.jpg b/docs/templates/overconfident.jpg index f00172e..6b9afef 100644 Binary files a/docs/templates/overconfident.jpg and b/docs/templates/overconfident.jpg differ diff --git a/docs/templates/pigeon.jpg b/docs/templates/pigeon.jpg index bb8181e..328e601 100644 Binary files a/docs/templates/pigeon.jpg and b/docs/templates/pigeon.jpg differ diff --git a/docs/templates/seagull4.jpg b/docs/templates/seagull4.jpg index 9a9c674..f4c28d9 100644 Binary files a/docs/templates/seagull4.jpg and b/docs/templates/seagull4.jpg differ diff --git a/docs/templates/spiderman.jpg b/docs/templates/spiderman.jpg index 7c319c8..c71a051 100644 Binary files a/docs/templates/spiderman.jpg and b/docs/templates/spiderman.jpg differ diff --git a/docs/templates/tom_cousins.jpg b/docs/templates/tom_cousins.jpg index eb3ceee..6931166 100644 Binary files a/docs/templates/tom_cousins.jpg and b/docs/templates/tom_cousins.jpg differ diff --git a/docs/templates/tough2.jpg b/docs/templates/tough2.jpg index 87352c4..f75960b 100644 Binary files a/docs/templates/tough2.jpg and b/docs/templates/tough2.jpg differ diff --git a/docs/templates/tough2bis.jpg b/docs/templates/tough2bis.jpg index fe1193e..6a1c459 100644 Binary files a/docs/templates/tough2bis.jpg and b/docs/templates/tough2bis.jpg differ diff --git a/docs/templates/trump.jpg b/docs/templates/trump.jpg index 141c704..ab313e6 100644 Binary files a/docs/templates/trump.jpg and b/docs/templates/trump.jpg differ diff --git a/docs/templates/trust_nobody.jpg b/docs/templates/trust_nobody.jpg index bf43783..fd4269e 100644 Binary files a/docs/templates/trust_nobody.jpg and b/docs/templates/trust_nobody.jpg differ diff --git a/docs/templates/truth.jpg b/docs/templates/truth.jpg index 0fa1d3e..4994dd5 100644 Binary files a/docs/templates/truth.jpg and b/docs/templates/truth.jpg differ diff --git a/docs/templates/winnie2.jpg b/docs/templates/winnie2.jpg index 983f73c..3224c90 100644 Binary files a/docs/templates/winnie2.jpg and b/docs/templates/winnie2.jpg differ diff --git a/meme_otron/__init__.py b/meme_otron/__init__.py index 5ce0602..1ea4d17 100644 --- a/meme_otron/__init__.py +++ b/meme_otron/__init__.py @@ -1 +1 @@ -VERSION = "1.2" +VERSION = "1.3" diff --git a/meme_otron/__main__.py b/meme_otron/__main__.py index d0070d2..7884d43 100644 --- a/meme_otron/__main__.py +++ b/meme_otron/__main__.py @@ -1,5 +1,6 @@ import sys import os +import logging from . import img_factory from . import meme_db @@ -8,11 +9,22 @@ from . import utils from . import VERSION if __name__ == "__main__": + wmark = not utils.read_argument(sys.argv, "-nw", "--no-watermark", delete=True) + debug = utils.read_argument(sys.argv, "-d", "--debug", delete=True) + verbose = utils.read_argument(sys.argv, "-v", "--verbose", delete=True) + output_file = utils.read_argument(sys.argv, "-o", "--output", valued=True, delete=True) + input_file = utils.read_argument(sys.argv, "-i", "--input", valued=True, delete=True) + + if verbose and debug: + logging.basicConfig(format="[%(asctime)s][%(levelname)s][%(module)s] %(message)s", level=logging.DEBUG) + elif verbose: + logging.basicConfig(format="[%(asctime)s][%(levelname)s][%(module)s] %(message)s", level=logging.INFO) + else: + logging.basicConfig(format="%(message)s", level=logging.WARNING) + meme_db.load_memes() img_factory.load_fonts() - # TODO better arguments reading (-h, -o, -v) - if len(sys.argv) <= 1 or utils.read_argument(sys.argv, "help", "--help", "-h"): print(f"Meme-Otron v{VERSION}" "python -m meme_otron -h\n" @@ -21,12 +33,21 @@ if __name__ == "__main__": file=sys.stderr) sys.exit(1) else: - output_file = utils.read_argument(sys.argv, "-o", "--output", valued=True, delete=True) - img = meme_otron.compute(*sys.argv[1:]) + input_data = None + if input_file is not None: + try: + with open(input_file, "rb") as f: + input_data = f.read() + except IOError as e: + print(f"Cannot read '{input_file}': {e}", file=sys.stderr) + sys.exit(1) + elif utils.is_stdin_ready(): + input_data = utils.read_stream(sys.stdin.buffer) + + img, errors = meme_otron.compute(*sys.argv[1:], input_data=input_data, wmark=wmark, debug=debug) + for err in errors: + print(err, file=sys.stderr) if img is None: - 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_file is None: with os.fdopen(os.dup(sys.stdout.fileno())) as output: diff --git a/meme_otron/img_factory.py b/meme_otron/img_factory.py index 409154a..d533b06 100644 --- a/meme_otron/img_factory.py +++ b/meme_otron/img_factory.py @@ -1,8 +1,10 @@ from typing import List, Optional, Tuple -from PIL import Image, ImageFont, ImageDraw +from PIL import Image, ImageFont, ImageDraw, UnidentifiedImageError +import io import os import os.path as path import logging +import sys from . import utils from .types import Text @@ -12,6 +14,8 @@ TEMPLATES_DIR = utils.relative_path(__file__, "..", "templates") FONTS = {} +TEXT_IMAGE_WIDTH = 800 + logger = logging.getLogger("img_factory") @@ -26,17 +30,63 @@ def load_fonts(): logger.error(f"Could not load font '{split[0]}'") -def build_image(template: str, texts: List[Text], debug: bool = False) -> Optional[Image.Image]: +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: + images[i] = img.resize((width, round(img.size[1] * width / img.size[0])), resample=Image.LANCZOS) + height = sum([img.size[1] for img in images]) + output_image = Image.new('RGB', (width, height)) + current_height = 0 + for img in images: + output_image.paste(img, (0, current_height)) + current_height += img.size[1] + return output_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: logger.error(f"Could not read template file '{template}': {e}") return None - draw = ImageDraw.Draw(img) + img = apply_texts(img, texts, debug=debug) + 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 build_image_only(image_data: bytes) -> Optional[Image.Image]: + try: + image = Image.open(io.BytesIO(image_data)) + except UnidentifiedImageError: + return None + return image.convert(mode='RGB') + + +def apply_texts(img: Image.Image, texts: List[Text], debug: bool = False) -> Image.Image: + if img.mode != 'RGBA': + img = img.convert(mode='RGBA') + draw = ImageDraw.Draw(img) for text in texts: draw_text(draw, img, text, debug=debug) - return img.convert(mode='RGB') @@ -77,10 +127,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_db.py b/meme_otron/meme_db.py index ae8c849..5f9a4f9 100644 --- a/meme_otron/meme_db.py +++ b/meme_otron/meme_db.py @@ -15,10 +15,11 @@ logger = logging.getLogger("meme_db") def load_memes(purge: bool = False): - global DATA, ALIASES + global DATA, ALIASES, LIST if purge: - DATA = {} - ALIASES = {} + DATA.clear() + ALIASES.clear() + LIST = [] try: with open(DATA_FILE) as input_file: content = "".join(input_file.readlines()) @@ -43,6 +44,8 @@ def load_item(i: int, item: dict): if not (isinstance(item, dict)): raise TypeError(f"root is not a dict") item_id = utils.read_key(item, "id", types=[str]) + if len(item_id.strip()) == 0: + return if item_id in DATA: raise NameError(f"id '{item_id}' already existing") based_on = utils.read_key_safe(item, "based_on", types=[str]) @@ -90,8 +93,6 @@ def load_item(i: int, item: dict): logger.warning(f"Item '{item_id}'({i + 1}) / Text {j + 1}: {e}") for text in meme.texts: text.update(meme.text_base) - if not meme.abstract and len(meme.texts) == 0: - logger.warning(f"Item '{item_id}'({i + 1}): no texts loaded") else: DATA[item_id] = meme if not meme.abstract: @@ -129,7 +130,7 @@ def load_text(current_text: int, raw_text: dict, text: Optional[Text] = None) -> 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] + text.position = getattr(Pos, raw_text["position"]) 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/meme_otron.py b/meme_otron/meme_otron.py index 2f42cf5..261dd06 100644 --- a/meme_otron/meme_otron.py +++ b/meme_otron/meme_otron.py @@ -1,10 +1,12 @@ -import logging +from typing import Optional, Tuple, List +import re +from PIL import Image +from io import BytesIO from .types import Text, Pos -from . import img_factory as imgf -from . import meme_db as db - -logger = logging.getLogger("meme_otron") +from . import img_factory +from . import meme_db +from . import utils right_wmark = Text("Made with meme-otron") right_wmark.position = Pos.SE @@ -20,43 +22,91 @@ left_wmark.font_size = 0.02 left_wmark.x_range = [0.005, 0.995] left_wmark.y_range = [0.005, 0.995] - -def parse_text(s): - """ - :param (str) s: - :rtype: str - """ - return s.replace("\\n", "\n") +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, left_wmark_text=None, debug=False): - """ - :param (str) left_wmark_text: - :param (bool) debug: - :param (str) args: - :rtype: PIL.Image.Image - :return: - """ +def compute(*args: str, input_data: Optional[bytes] = None, + wmark: bool = True, left_wmark_text: Optional[str] = None, + max_file_size: Optional[int] = None, + debug: bool = False) -> Tuple[Optional[Image.Image], List[str]]: if len(args) < 1: - 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: - 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 = parse_text(args[c + 1]) - else: - meme.texts[i].text = "" - c += 1 + return None, ['Not enough arguments'] + + parts = utils.split_arguments(args, "-") + images = [] + errors = [] + for part in parts: + img, err = compute_part(*part, input_data=input_data, max_file_size=max_file_size, debug=debug) + if img is not None: + images += [img] + else: + errors += [err] + + if len(images) == 0: + return None, errors + + output_image = img_factory.compose_image(images) + + if wmark: + watermarks = [right_wmark] + if left_wmark_text is not None: + watermarks += [left_wmark.variant(left_wmark_text)] + output_image = img_factory.apply_texts(output_image, watermarks, debug=debug) + + if max_file_size is not None: + with BytesIO() as img_file: + output_image.save(img_file, 'JPEG') + if img_file.tell() > max_file_size: + return None, ['Output image too big'] + + return output_image, errors + + +def compute_part(*args: str, input_data: Optional[bytes] = None, + max_file_size: Optional[int] = None, + debug: bool = False) -> Tuple[Optional[Image.Image], Optional[str]]: + meme_id = utils.sanitize_input(args[0]) + + if meme_id == "text": + if len(args) <= 1: + return None, 'Text: not enough arguments' + texts = [simple_text.variant(arg) for arg in args[1:]] + return img_factory.build_text_only(texts, debug=debug), None + elif meme_id == "image": + if input_data is None or len(input_data) == 0: + if len(args) <= 1: + return None, 'Image: received no input data nor URL' else: - meme.texts[i].text = meme.texts[meme.texts[i].text_ref].text - meme.texts += [right_wmark] - if left_wmark_text is not None: - left_wmark.text = left_wmark_text - meme.texts += [left_wmark] - return imgf.build_image(meme.template, meme.texts, debug=debug) + input_data, err = utils.read_web_file(args[1], max_file_size=max_file_size) + if input_data is None: + return None, 'Image: ' + err + img = img_factory.build_image_only(input_data) + if img is None: + return None, 'Image: invalid image format' + else: + return img, None + else: + meme = meme_db.get_meme(meme_id) + if meme is None: + error = f"Template: '{meme_id}' not found." + proposal = meme_db.find_nearest(meme_id) + if proposal is not None: + error += f" Did you mean '{proposal}'?" + return None, error + 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 = meme.texts[meme.texts[i].text_ref].text + return img_factory.build_from_template(meme.template, meme.texts, debug=debug), None 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/meme_otron/utils.py b/meme_otron/utils.py index 5eb8bd7..417a663 100644 --- a/meme_otron/utils.py +++ b/meme_otron/utils.py @@ -1,14 +1,30 @@ import re +import select import sys +from urllib.request import urlopen +from urllib.error import URLError, HTTPError +from urllib.parse import urlparse import os.path as path -from typing import List, Optional, Union -from Levenshtein import distance +from typing import List, Optional, Union, Tuple, BinaryIO + +try: + from Levenshtein import distance +except ModuleNotFoundError: + distance = None + + +# region path utils def relative_path(file: str, *args: str) -> str: return path.realpath(path.join(path.dirname(path.realpath(file)), *args)) +# endregion + +# region dict utils + + def read_key_safe(d: dict, k: str, default=None, *, types: Optional[List[type]] = None, is_list: bool = False, @@ -37,6 +53,11 @@ def read_key(d: dict, k: str, default=None, *, raise KeyError(k) +# endregion + +# region type utils + + def check_type(obj, types: List[type], is_list: bool = False, is_list_size: Optional[int] = None): if is_list: if not is_list_of(obj, types, is_list_size): @@ -65,6 +86,11 @@ def is_list_of(obj, types: List[type], length: Optional[int] = None) -> bool: return True +# endregion + +# region args utils + + args_regex = re.compile('"([^"]*)"|\'([^\']*)\'|([^ ]+)') @@ -78,7 +104,45 @@ def parse_arguments(src: str) -> List[str]: return [get_found_match(m) for m in args_regex.findall(src)] +def read_argument(args: List[str], *names: str, valued: bool = False, delete: bool = False): + for i, arg in enumerate(args): + if arg.lower() in names: + if delete: + del args[i] + i -= 1 + if not valued: + return True + else: + v = None + if i + 1 < len(args): + v = args[i + 1] + if delete: + del args[i + 1] + return v + if valued: + return None + else: + return False + + +def split_arguments(args: Union[List[str], Tuple[str]], separator: str) -> List[List[str]]: + output = [[]] + for argument in args: + if argument == separator: + output += [[]] + else: + output[-1] += [argument] + return [part for part in output if len(part) > 0] + + +# endregion + +# region lang utils + + def find_nearest(word: str, wlist: List[str], threshold: int = 5) -> Optional[str]: + if distance is None: + return None distances = [ (distance(word, w), # distance abs(len(w) - len(word)), # length diff @@ -91,6 +155,14 @@ def find_nearest(word: str, wlist: List[str], threshold: int = 5) -> Optional[st return found[2] +def sanitize_input(src: str) -> str: + return re.sub(r'[^A-Za-z0-9 _]', "", src.lower().strip()) + + +# endregion + +# region format utils + def justify_text(src: str, n_lines: int) -> Optional[str]: spaces_indexes = find_all(src, " ") if n_lines - 1 > len(spaces_indexes): @@ -102,6 +174,28 @@ def justify_text(src: str, n_lines: int) -> Optional[str]: return replace_at(src, "\n", break_indexes, 1) +def place_line_breaks(breaks_positions: List[float], spaces_indexes: List[int]) -> List[int]: + breaks_positions = breaks_positions[:] + breaks_indexes = [] + dist = sys.maxsize + for i, value in enumerate(spaces_indexes): + if not len(breaks_positions): + break + if dist < abs(value - breaks_positions[0]): + breaks_indexes += [spaces_indexes[i - 1]] + breaks_positions.pop(0) + else: + dist = abs(value - breaks_positions[0]) + if len(breaks_positions): + breaks_indexes += [spaces_indexes[-1]] + return breaks_indexes + + +# endregion + +# region string utils + + def find_all(src: str, pattern: str) -> List[int]: indexes = [] i = safe_index(src, pattern) @@ -121,23 +215,6 @@ def replace_at(src: str, pattern: str, indexes: List[int], remove: int) -> str: return output -def place_line_breaks(breaks_positions: List[float], spaces_indexes: List[int]) -> List[int]: - breaks_positions = breaks_positions[:] - breaks_indexes = [] - dist = sys.maxsize - for i, value in enumerate(spaces_indexes): - if not len(breaks_positions): - break - if dist < abs(value - breaks_positions[0]): - breaks_indexes += [spaces_indexes[i - 1]] - breaks_positions.pop(0) - else: - dist = abs(value - breaks_positions[0]) - if len(breaks_positions): - breaks_indexes += [spaces_indexes[-1]] - return breaks_indexes - - def safe_index(src: Union[str, list], pattern, start: int = 0): try: return src.index(pattern, start) @@ -145,19 +222,48 @@ def safe_index(src: Union[str, list], pattern, start: int = 0): return None -def read_argument(args: List[str], *names: str, valued: bool = False, delete: bool = False): - for i, arg in enumerate(args): - if arg.lower() in names: - if delete: - del args[i] - i -= 1 - if not valued: - return True - else: - v = None - if i + 1 < len(args): - v = args[i + 1] - if delete: - del args[i + 1] - return v - return None +# endregion + +# region stream utils + + +def is_stdin_ready() -> bool: + """ + https://stackoverflow.com/questions/3762881/how-do-i-check-if-stdin-has-some-data + """ + return sys.stdin.isatty() and select.select([sys.stdin, ], [], [], 0.0)[0] + + +def read_stream(stream: BinaryIO) -> bytes: + output_data = bytearray() + for line in stream: + output_data += line + return output_data + + +# endregion + + +# region web utils + + +def read_web_file(url: str, *, timeout: float = 5, + max_file_size: Optional[int] = None) -> Tuple[Optional[bytes], Optional[str]]: + if not validate_url(url): + return None, 'Invalid URL' + try: + with urlopen(url, None, timeout) as web_file: + if max_file_size is not None and int(web_file.info()['Content-Length']) > max_file_size: + return None, 'File too big' + return web_file.read(), None + except HTTPError as e: + return None, f'Could not connect: {e}' + except URLError: + return None, f'Could not connect to server' + + +def validate_url(url: str) -> bool: + parsed = urlparse(url) + return parsed.scheme != "" and parsed.netloc != "" + +# endregion diff --git a/memes.json b/memes.json index 03d09de..a766d47 100644 --- a/memes.json +++ b/memes.json @@ -134,7 +134,7 @@ "id": "2_texts", "abstract": true, "based_on": "white_text", - "font_size": 0.12, + "font_size": 0.08, "texts": [{ "x_range": [0.02, 0.98], "y_range": [0.02, 0.50], @@ -199,7 +199,6 @@ "id": "tough2bis", "aliases": ["soft"], "template": "tough2bis.jpg", - "aliases": [], "based_on": "2_panel_right" },{ "id": "tough3", @@ -247,7 +246,7 @@ "based_on": "white_text", "aliases": [], "info": "https://knowyourmeme.com/memes/ancient-aliens", - "font_size": 0.15, + "font_size": 0.16, "texts": [{ "x_range": [0.02, 0.98], "y_range": [0.8, 0.98], @@ -345,11 +344,11 @@ "aliases": ["tom", "jerry"], "info": "https://knowyourmeme.com/memes/buff-tom", "based_on": "white_text", - "font_size": 0.07, + "font_size": 0.06, "texts": [{ "x_range": [0.07, 0.36], "y_range": [0.69, 0.98], - "font_size": 0.05 + "font_size": 0.04 },{ "x_range": [0.44, 0.84], "y_range": [0.27, 0.59] @@ -360,7 +359,7 @@ "based_on": "white_text", "aliases": ["nut"], "info": "https://knowyourmeme.com/memes/nut-button", - "font_size": 0.08, + "font_size": 0.07, "texts": [{ "x_range": [0.10, 0.46], "y_range": [0.46, 0.80], @@ -409,6 +408,7 @@ "info": "https://knowyourmeme.com/memes/ah-i-see-youre-a-man-of-culture-as-well", "stroke_fill": [0, 0, 0], "stroke_width": 0.05, + "font_size": 0.03, "texts": [{ "x_range": [0.57, 0.68], "y_range": [0.87, 0.945] @@ -419,7 +419,7 @@ "aliases": ["boyfried", "cheating", "girlfriend"], "info": "https://knowyourmeme.com/memes/distracted-boyfriend", "based_on": "white_text", - "font_size": 0.06, + "font_size": 0.04, "texts": [{ "x_range": [0.49, 0.75], "y_range": [0.40, 0.62] @@ -454,7 +454,7 @@ "info": "https://knowyourmeme.com/memes/left-exit-12-off-ramp", "font": "impact", "fill": [255, 255, 255], - "font_size": 0.06, + "font_size": 0.05, "position": "N", "texts": [{ "x_range": [0.21, 0.37], @@ -541,7 +541,7 @@ "aliases": ["fire", "dog"], "info": "https://knowyourmeme.com/memes/this-is-fine", "based_on": "white_text", - "font_size": 0.08, + "font_size": 0.04, "texts": [{ "x_range": [0.14, 0.34], "y_range": [0.55, 0.79] @@ -555,21 +555,21 @@ "font": "grafo", "fill": [0, 0, 0], "stroke_width": 0, - "font_size": 0.15 + "font_size": 0.08 },{ "x_range": [0.03, 0.16], "y_range": [0.65, 0.98], - "font_size": 0.04, + "font_size": 0.03, "position": "W" },{ "x_range": [0.33, 0.48], "y_range": [0.78, 0.97], - "font_size": 0.04, + "font_size": 0.03, "position": "SE" },{ "x_range": [0.34, 0.48], "y_range": [0.35, 0.61], - "font_size": 0.04, + "font_size": 0.03, "position": "E" }] },{ @@ -595,7 +595,7 @@ "aliases": [], "info": "https://knowyourmeme.com/memes/woman-yelling-at-a-cat", "position": "S", - "font_size": 0.12, + "font_size": 0.08, "texts": [{ "x_range": [0.01, 0.49], "y_range": [0.50, 0.99] @@ -608,6 +608,7 @@ "template": "gru.jpg", "aliases": ["plan"], "info": "https://knowyourmeme.com/memes/grus-plan", + "font_size": 0.04, "texts": [{ "x_range": [0.28, 0.48], "y_range": [0.08, 0.50], @@ -631,7 +632,7 @@ "based_on": "white_text", "aliases": [], "info": "https://knowyourmeme.com/memes/epic-handshake", - "font_size": 0.12, + "font_size": 0.09, "texts": [{ "x_range": [-0.05, 0.45], "y_range": [0.48, 0.78], @@ -705,7 +706,7 @@ "based_on": "white_text", "aliases": ["butterfly"], "info": "https://knowyourmeme.com/memes/is-this-a-pigeon", - "font_size": 0.08, + "font_size": 0.07, "texts": [{ "x_range": [0.02, 0.98], "y_range": [0.86, 0.99], @@ -761,12 +762,12 @@ "info": "https://knowyourmeme.com/memes/nobody-is-born-cool", "based_on": "white_text", "aliases": [], - "font_size": 0.07, + "font_size": 0.05, "texts": [{ "x_range": [0.07, 0.39], "y_range": [0.62, 0.85], "stroke_width": 0, - "font_size": 0.05, + "font_size": 0.04, "angle": -15 },{ "x_range": [0.18, 0.46], @@ -786,14 +787,14 @@ "based_on": "white_text", "aliases": ["joker"], "info": "https://knowyourmeme.com/memes/mini-joker", - "font_size": 0.07, + "font_size": 0.06, "texts": [{ "x_range": [0.42, 0.77], "y_range": [0.19, 0.55] },{ "x_range": [0.12, 0.42], "y_range": [0.36, 0.66], - "font_size": 0.05 + "font_size": 0.04 }] },{ "id": "nobody_cares", @@ -816,6 +817,7 @@ "aliases": ["alcohol","depressed"], "info": "https://knowyourmeme.com/memes/overconfident-alcoholic", "based_on": "white_text", + "font_size": 0.04, "texts": [{ "x_range": [0.04, 0.30], "y_range": [0.03, 0.19], @@ -907,7 +909,8 @@ "y_range": [0.32, 0.47] },{ "x_range": [0.70, 0.95], - "y_range": [0.80, 0.95] + "y_range": [0.80, 0.95], + "text_ref": 2 }] },{ "id": "spiderman", @@ -915,7 +918,7 @@ "based_on": "white_text", "aliases": ["same"], "info": "https://knowyourmeme.com/memes/spider-man-pointing-at-spider-man", - "font_size": 0.07, + "font_size": 0.05, "texts": [{ "x_range": [0.15, 0.39], "y_range": [0.26, 0.52] @@ -952,6 +955,7 @@ "aliases": ["law"], "info": "https://knowyourmeme.com/memes/trumps-first-order-of-business", "based_on": "white_text", + "font_size": 0.04, "texts": [{ "x_range": [0.68, 0.92], "y_range": [0.37, 0.66], @@ -964,7 +968,7 @@ },{ "x_range": [0.05, 0.38], "y_range": [0.42, 0.81], - "font_size": 0.07 + "font_size": 0.06 }] },{ "id": "trust_nobody", @@ -972,7 +976,7 @@ "based_on": "white_text", "aliases": ["yourself","gun"], "info": "https://knowyourmeme.com/memes/trust-nobody-not-even-yourself", - "font_size": 0.07, + "font_size": 0.05, "texts": [{ "x_range": [0.16, 0.50], "y_range": [0.32, 0.85] @@ -1023,7 +1027,7 @@ "based_on": "white_text", "aliases": ["cousins","backup", "goons"], "info": "https://knowyourmeme.com/memes/tom-and-jerry-hired-goons", - "font_size": 0.07, + "font_size": 0.06, "texts": [{ "x_range": [0.14, 0.39], "y_range": [0.21, 0.69] @@ -1109,7 +1113,7 @@ "id": "seagull4", "template": "seagull4.jpg", "based_on": "white_text", - "font_size": 0.07, + "font_size": 0.04, "position": "S", "texts": [{ "x_range": [0.01, 0.49], @@ -1123,11 +1127,11 @@ },{ "x_range": [0.51, 0.99], "y_range": [0.80, 0.99], - "font_size": 0.09 + "font_size": 0.05 },{ "x_range": [0.16, 0.38], "y_range": [0.03, 0.17], - "font_size": 0.04 + "font_size": 0.03 },{ "x_range": [0.72, 0.94], "y_range": [0.00, 0.13], @@ -1361,4 +1365,114 @@ "text_ref": 13, "angle": 38 }] +},{ + "id": "favorite", + "template": "favorite.jpg", + "aliases": [], + "info": "https://knowyourmeme.com/memes/this-is-my-favorite-subreddit", + "font_size": 0.06, + "font": "grafo", + "texts": [{ + "x_range": [0.28, 0.55], + "y_range": [0.10, 0.15], + "font_size": 0.10, + "fill": [8, 9, 129], + "stroke_width": 0.02, + "stroke_fill": [8, 9, 129], + "position": "N" + },{ + "x_range": [0.61, 0.96], + "y_range": [0.06, 0.14], + "font_size": 0.08, + "fill": [249, 26, 10], + "stroke_width": 0.02, + "stroke_fill": [249, 26, 10] + },{ + "x_range": [0.65, 0.97], + "y_range": [0.60, 0.75], + "fill": [135, 55, 48] + }] +},{ + "id": "quality", + "template": "quality.jpg", + "aliases": ["competition"], + "based_on": "white_text", + "info": "https://knowyourmeme.com/memes/king-neptune-vs-spongebob-squarepants", + "font_size": 0.05, + "texts": [{ + "x_range": [0.11, 0.54], + "y_range": [0.18, 0.41], + "font_size": 0.04 + },{ + "x_range": [0.13, 0.40], + "y_range": [0.84, 0.96], + "font_size": 0.06 + },{ + "x_range": [0.56, 0.80], + "y_range": [0.30, 0.40] + },{ + "x_range": [0.40, 0.82], + "y_range": [0.74, 0.89], + "font_size": 0.07 + }] +},{ + "id": "pasta", + "template": "pasta.jpg", + "aliases": ["vista", "italian"], + "info": "https://knowyourmeme.com/photos/1272835-how-italians-do-things", + "texts": [] +},{ + "id": "holup", + "template": "holup.jpg", + "aliases": ["hold_up"], + "info": "https://knowyourmeme.com/memes/vault-boy-hold-up", + "texts": [] +},{ + "id": "wtf", + "template": "wtf.jpg", + "aliases": ["excuse_me"], + "info": "https://knowyourmeme.com/memes/excuse-me-what-the-fuck", + "texts": [] +},{ + "id": "stonks", + "template": "stonks.jpg", + "aliases": [], + "info": "https://knowyourmeme.com/memes/stonks", + "texts": [] +},{ + "id": "doubt", + "template": "doubt.jpg", + "aliases": ["press_x"], + "info": "https://knowyourmeme.com/memes/la-noire-doubt-press-x-to-doubt", + "texts": [] +},{ + "id": "listen", + "template": "listen.jpg", + "aliases": ["chicken","little_shit"], + "info": "https://knowyourmeme.com/memes/listen-here-you-little-shit", + "texts": [] +},{ + "id": "head_out", + "template": "head_out.jpg", + "aliases": ["ight"], + "info": "https://knowyourmeme.com/memes/ight-imma-head-out", + "texts": [] +},{ + "id": "money", + "template": "money.jpg", + "aliases": ["fry"], + "info": "https://knowyourmeme.com/memes/shut-up-and-take-my-money", + "texts": [] +},{ + "id": "white", + "template": "white.jpg", + "aliases": ["magazine"], + "info": "https://knowyourmeme.com/memes/dave-chappelle-reading-white-people-magazine", + "texts": [] +},{ + "id": "", + "template": "", + "aliases": [], + "info": "", + "texts": [] }] diff --git a/templates/aliens.jpg b/templates/aliens.jpg index 502f1b8..5e8a12d 100644 Binary files a/templates/aliens.jpg and b/templates/aliens.jpg differ diff --git a/templates/argument.jpg b/templates/argument.jpg index 2d10cea..1f7328a 100644 Binary files a/templates/argument.jpg and b/templates/argument.jpg differ diff --git a/templates/brain3.jpg b/templates/brain3.jpg index ee7f04e..869b8b0 100644 Binary files a/templates/brain3.jpg and b/templates/brain3.jpg differ diff --git a/templates/brain4.jpg b/templates/brain4.jpg index 4bf8cb4..5b28e1d 100644 Binary files a/templates/brain4.jpg and b/templates/brain4.jpg differ diff --git a/templates/brain5.jpg b/templates/brain5.jpg index 567cd0a..798a02e 100644 Binary files a/templates/brain5.jpg and b/templates/brain5.jpg differ diff --git a/templates/buff.jpg b/templates/buff.jpg index 74950e2..7c35fca 100644 Binary files a/templates/buff.jpg and b/templates/buff.jpg differ diff --git a/templates/burn.jpg b/templates/burn.jpg index 5fb133f..25d608b 100644 Binary files a/templates/burn.jpg and b/templates/burn.jpg differ diff --git a/templates/button.jpg b/templates/button.jpg index 2e88096..be09e9b 100644 Binary files a/templates/button.jpg and b/templates/button.jpg differ diff --git a/templates/bye_mom.jpg b/templates/bye_mom.jpg index adafc95..e10d8c2 100644 Binary files a/templates/bye_mom.jpg and b/templates/bye_mom.jpg differ diff --git a/templates/clock.jpg b/templates/clock.jpg index 4619043..32fa2d1 100644 Binary files a/templates/clock.jpg and b/templates/clock.jpg differ diff --git a/templates/culture.jpg b/templates/culture.jpg index fa04313..915d0aa 100644 Binary files a/templates/culture.jpg and b/templates/culture.jpg differ diff --git a/templates/dont_look.jpg b/templates/dont_look.jpg index 697c42c..836e1c1 100644 Binary files a/templates/dont_look.jpg and b/templates/dont_look.jpg differ diff --git a/templates/doubt.jpg b/templates/doubt.jpg new file mode 100644 index 0000000..db5a9df Binary files /dev/null and b/templates/doubt.jpg differ diff --git a/templates/everywhere.jpg b/templates/everywhere.jpg index e4db63a..4be4491 100644 Binary files a/templates/everywhere.jpg and b/templates/everywhere.jpg differ diff --git a/templates/favorite.jpg b/templates/favorite.jpg new file mode 100644 index 0000000..99f9dcd Binary files /dev/null and b/templates/favorite.jpg differ diff --git a/templates/fight.jpg b/templates/fight.jpg index 5ab1aff..01469b3 100644 Binary files a/templates/fight.jpg and b/templates/fight.jpg differ diff --git a/templates/fine.jpg b/templates/fine.jpg index e67bd51..8a7bd53 100644 Binary files a/templates/fine.jpg and b/templates/fine.jpg differ diff --git a/templates/flex_tape.jpg b/templates/flex_tape.jpg index 7ad1ebe..5f25146 100644 Binary files a/templates/flex_tape.jpg and b/templates/flex_tape.jpg differ diff --git a/templates/gate.jpg b/templates/gate.jpg index 40ac098..023d106 100644 Binary files a/templates/gate.jpg and b/templates/gate.jpg differ diff --git a/templates/grandma.jpg b/templates/grandma.jpg index a626273..465b12e 100644 Binary files a/templates/grandma.jpg and b/templates/grandma.jpg differ diff --git a/templates/gru.jpg b/templates/gru.jpg index 6ca77e4..8e89160 100644 Binary files a/templates/gru.jpg and b/templates/gru.jpg differ diff --git a/templates/handshake.jpg b/templates/handshake.jpg index c125e19..c2f488f 100644 Binary files a/templates/handshake.jpg and b/templates/handshake.jpg differ diff --git a/templates/handshake2.jpg b/templates/handshake2.jpg index 39d3b84..a90bc49 100644 Binary files a/templates/handshake2.jpg and b/templates/handshake2.jpg differ diff --git a/templates/head_out.jpg b/templates/head_out.jpg new file mode 100644 index 0000000..63bcdcc Binary files /dev/null and b/templates/head_out.jpg differ diff --git a/templates/holup.jpg b/templates/holup.jpg new file mode 100644 index 0000000..41ecad7 Binary files /dev/null and b/templates/holup.jpg differ diff --git a/templates/idea.jpg b/templates/idea.jpg index 56469c2..7a53b0e 100644 Binary files a/templates/idea.jpg and b/templates/idea.jpg differ diff --git a/templates/listen.jpg b/templates/listen.jpg new file mode 100644 index 0000000..ca7d9ae Binary files /dev/null and b/templates/listen.jpg differ diff --git a/templates/money.jpg b/templates/money.jpg new file mode 100644 index 0000000..09d411d Binary files /dev/null and b/templates/money.jpg differ diff --git a/templates/nobody_cares.jpg b/templates/nobody_cares.jpg index 2ae0b83..20ae877 100644 Binary files a/templates/nobody_cares.jpg and b/templates/nobody_cares.jpg differ diff --git a/templates/overconfident.jpg b/templates/overconfident.jpg index 531f2f2..a3af3e2 100644 Binary files a/templates/overconfident.jpg and b/templates/overconfident.jpg differ diff --git a/templates/pasta.jpg b/templates/pasta.jpg new file mode 100644 index 0000000..5e6c3b9 Binary files /dev/null and b/templates/pasta.jpg differ diff --git a/templates/patrick.jpg b/templates/patrick.jpg index ed86583..3d893d0 100644 Binary files a/templates/patrick.jpg and b/templates/patrick.jpg differ diff --git a/templates/pigeon.jpg b/templates/pigeon.jpg index 1c7dc37..b3b6002 100644 Binary files a/templates/pigeon.jpg and b/templates/pigeon.jpg differ diff --git a/templates/pills.jpg b/templates/pills.jpg index 3995aff..afe1f01 100644 Binary files a/templates/pills.jpg and b/templates/pills.jpg differ diff --git a/templates/quality.jpg b/templates/quality.jpg new file mode 100644 index 0000000..d7ac7df Binary files /dev/null and b/templates/quality.jpg differ diff --git a/templates/salt_bae.jpg b/templates/salt_bae.jpg index 0b778d3..a9f5a96 100644 Binary files a/templates/salt_bae.jpg and b/templates/salt_bae.jpg differ diff --git a/templates/scary.jpg b/templates/scary.jpg index 1c90ced..412b0f9 100644 Binary files a/templates/scary.jpg and b/templates/scary.jpg differ diff --git a/templates/seagull4.jpg b/templates/seagull4.jpg index ccf13c3..768bb1b 100644 Binary files a/templates/seagull4.jpg and b/templates/seagull4.jpg differ diff --git a/templates/see_that_guy.jpg b/templates/see_that_guy.jpg index 991bb32..b29337e 100644 Binary files a/templates/see_that_guy.jpg and b/templates/see_that_guy.jpg differ diff --git a/templates/sleeping.jpg b/templates/sleeping.jpg index a94b95c..0e59ff5 100644 Binary files a/templates/sleeping.jpg and b/templates/sleeping.jpg differ diff --git a/templates/spiderman.jpg b/templates/spiderman.jpg index 11d4f51..bcc9277 100644 Binary files a/templates/spiderman.jpg and b/templates/spiderman.jpg differ diff --git a/templates/stonks.jpg b/templates/stonks.jpg new file mode 100644 index 0000000..48a536c Binary files /dev/null and b/templates/stonks.jpg differ diff --git a/templates/struggle.jpg b/templates/struggle.jpg index d5434f1..1275fb8 100644 Binary files a/templates/struggle.jpg and b/templates/struggle.jpg differ diff --git a/templates/t_pose.jpg b/templates/t_pose.jpg index d152c89..a201eab 100644 Binary files a/templates/t_pose.jpg and b/templates/t_pose.jpg differ diff --git a/templates/trump.jpg b/templates/trump.jpg index 07536a9..42cb464 100644 Binary files a/templates/trump.jpg and b/templates/trump.jpg differ diff --git a/templates/trust_nobody.jpg b/templates/trust_nobody.jpg index bdf8304..2add981 100644 Binary files a/templates/trust_nobody.jpg and b/templates/trust_nobody.jpg differ diff --git a/templates/truth.jpg b/templates/truth.jpg index 22510bf..d75fe7e 100644 Binary files a/templates/truth.jpg and b/templates/truth.jpg differ diff --git a/templates/white.jpg b/templates/white.jpg new file mode 100644 index 0000000..3491365 Binary files /dev/null and b/templates/white.jpg differ diff --git a/templates/winnie2.jpg b/templates/winnie2.jpg index 9ecf644..4b9f7c0 100644 Binary files a/templates/winnie2.jpg and b/templates/winnie2.jpg differ diff --git a/templates/winnie3.jpg b/templates/winnie3.jpg index 7bfca3d..2b0308e 100644 Binary files a/templates/winnie3.jpg and b/templates/winnie3.jpg differ diff --git a/templates/worthless.jpg b/templates/worthless.jpg index 80a3415..ea1263f 100644 Binary files a/templates/worthless.jpg and b/templates/worthless.jpg differ diff --git a/templates/wtf.jpg b/templates/wtf.jpg new file mode 100644 index 0000000..b93c28a Binary files /dev/null and b/templates/wtf.jpg differ diff --git a/tests/unit/meme_otron/test_meme_db.py b/tests/unit/meme_otron/test_meme_db.py new file mode 100644 index 0000000..adbd552 --- /dev/null +++ b/tests/unit/meme_otron/test_meme_db.py @@ -0,0 +1,67 @@ +from unittest import TestCase +from meme_otron import meme_db +from meme_otron.types import Pos, Text +from random import randint, randrange, choice + + +def get_rand_raw_text(): + return { + "font": str(randint(0, pow(10, 10))), + "x_range": [randrange(-10, 10), randrange(-10, 10)], + "y_range": [randrange(-10, 10), randrange(-10, 10)], + "text_ref": randint(-10, 10), + "style_ref": randint(-10, 10), + "angle": randrange(-10, 10), + "font_size": randrange(-10, 10), + "fill": [randint(-10, 10), randint(-10, 10), randint(-10, 10)], + "stroke_width": randrange(-10, 10), + "stroke_fill": [randint(-10, 10), randint(-10, 10), randint(-10, 10)], + "align": choice(["left", "center", "right"]), + "position": choice([k.name for k in Pos]) + } + + +class TestMemeDbLoadText(TestCase): + object_keys = ["text_ref", "style_ref", "angle", "font_size", "fill", + "stroke_width", "stroke_fill", "position", "align"] + + def test_load_text_minimal(self): + try: + text = meme_db.load_text(0, {}) + self.assertEqual(f"text 0", text.text) + for key in self.object_keys: + self.assertIsNone(getattr(text, key)) + except TypeError as e: + self.fail(e) + + def test_load_text_normal(self): + try: + raw_text = get_rand_raw_text() + i = randint(-10, 10) + text = meme_db.load_text(i, raw_text) + self.assertEqual(f"text {i}", text.text) + for key in self.object_keys: + if key == "position": + self.assertEqual(getattr(Pos, raw_text[key]), text.position) + else: + self.assertEqual(raw_text[key], getattr(text, key)) + except TypeError as e: + self.fail(e) + + def test_load_text_base(self): + try: + base_text = meme_db.load_text(0, get_rand_raw_text()) + raw_text = { + "font": str(randint(0, pow(10, 10))) + } + text = meme_db.load_text(0, raw_text, base_text) + self.assertEqual(f"text 0", text.text) + for key in self.object_keys: + if key in ["font_size", "fill", "stroke_width", "stroke_fill", "position", "align"]: + self.assertEqual(getattr(base_text, key), getattr(text, key)) + elif key == "font": + self.assertEqual(raw_text["font"], getattr(text, key)) + else: + self.assertIsNone(getattr(text, key)) + except TypeError as e: + self.fail(e) diff --git a/tests/unit/meme_otron/test_types.py b/tests/unit/meme_otron/test_types.py new file mode 100644 index 0000000..7de8e77 --- /dev/null +++ b/tests/unit/meme_otron/test_types.py @@ -0,0 +1,59 @@ +from unittest import TestCase +from meme_otron import types + + +class TestText(TestCase): + def test_declare(self): + txt1 = types.Text("txt1") + self.assertEqual("txt1", txt1.text) + self.assertIsNone(txt1.angle) + self.assertEqual((0, 1), txt1.x_range) + self.assertIsNone(txt1.fill) + self.assertIsNone(txt1.stroke_width) + + def test_update(self): + txt1 = types.Text("txt1") + txt1.stroke_width = 6 + txt2 = types.Text("txt2") + txt2.angle = 5 + txt2.x_range = (0.5, 0.8) + txt2.fill = [0, 1, 0] + txt2.stroke_width = 5 + txt1.update(txt2) + 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 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] + txt1.init() + self.assertIsNotNone((0, 1, 0), txt1.fill) + self.assertIsNotNone(txt1.stroke_width) + + +class TestMeme(TestCase): + def test_declare(self): + meme1 = types.Meme("meme1") + self.assertEqual("meme1", meme1.id) + self.assertIsNone(meme1.template) + + def test_clone(self): + meme1 = types.Meme("meme1") + meme1.template = "test1" + meme2 = meme1.clone() + meme1.template = "test2" + self.assertEqual("meme1", meme2.id) + self.assertEqual("test1", meme2.template) diff --git a/tests/unit/meme_otron/test_utils.py b/tests/unit/meme_otron/test_utils.py index f5696b5..7f8d287 100644 --- a/tests/unit/meme_otron/test_utils.py +++ b/tests/unit/meme_otron/test_utils.py @@ -2,10 +2,12 @@ from unittest import TestCase from meme_otron import utils -class Test(TestCase): +class TestUtilsPath(TestCase): def test_relative_path(self): self.assertEqual(__file__, utils.relative_path(__file__, ".", "test_utils.py")) + +class TestUtilsType(TestCase): def test_is_list_of(self): self.assertFalse(utils.is_list_of(None, [str])) self.assertFalse(utils.is_list_of("", [int])) @@ -48,6 +50,8 @@ class Test(TestCase): except TypeError as e: self.assertEqual("not a list of 3 float", str(e)) + +class TestUtilsDict(TestCase): def test_read_key(self): d = { "test1": 5, @@ -82,11 +86,19 @@ class Test(TestCase): self.assertEqual("default", utils.read_key_safe(d, "test3", "default")) self.assertIsNone(utils.read_key_safe(d, "test3")) + +class TestUtilsLang(TestCase): def test_find_nearest(self): self.assertEqual("test", utils.find_nearest("tost", ["test", "example", "what"])) self.assertIsNone(utils.find_nearest("unknown", ["test", "example", "what"], threshold=2)) self.assertEqual("test", utils.find_nearest("unknown", ["test", "example", "what"], threshold=200)) + def test_sanitize_input(self): + self.assertEqual("", utils.sanitize_input("")) + self.assertEqual("a b_c", utils.sanitize_input(" A+=¤$ bé_cè:* ")) + + +class TestUtilsArgs(TestCase): def test_parse_arguments(self): self.assertEqual([], utils.parse_arguments("")) self.assertEqual(["test"], utils.parse_arguments("test")) @@ -94,6 +106,29 @@ class Test(TestCase): self.assertEqual(["test1", "test 2", "test 3"], utils.parse_arguments("test1 'test 2' \"test 3\"")) self.assertEqual(["test1", "", ""], utils.parse_arguments("test1 '' \"\"")) + def test_read_argument(self): + self.assertFalse(utils.read_argument(["test", "-o", "test"], "--output")) + self.assertTrue(utils.read_argument(["test", "-O", "test"], "--output", "-o")) + self.assertIsNone(utils.read_argument(["test", "-o"], "-o", valued=True)) + self.assertEqual("test1", utils.read_argument(["test", "-o", "test1", "-o", "test2"], "-o", valued=True)) + args = ["test", "-o", "test1"] + self.assertTrue(utils.read_argument(args, "-o", delete=True)) + self.assertEqual(["test", "test1"], args) + args = ["test", "-o", "test1"] + self.assertEqual("test1", utils.read_argument(args, "-o", valued=True, delete=True)) + self.assertEqual(["test"], args) + args = ["test", "-o"] + self.assertIsNone(utils.read_argument(args, "-o", valued=True, delete=True)) + self.assertEqual(["test"], args) + + def test_split_arguments(self): + self.assertEqual([["test", "-o", "test"]], utils.split_arguments(["test", "-o", "test"], "-")) + self.assertEqual([["test1"], ["test2"]], utils.split_arguments(["test1", "-", "test2"], "-")) + self.assertEqual([["test1"], ["test2"]], utils.split_arguments(["-", "test1", "-", "-", "test2", "-"], "-")) + self.assertEqual([], utils.split_arguments([], "-")) + + +class TestUtilsString(TestCase): def test_safe_index(self): self.assertEqual(0, utils.safe_index("a", "a")) self.assertEqual(0, utils.safe_index([0], 0)) @@ -116,6 +151,8 @@ class Test(TestCase): self.assertEqual("ddd", utils.replace_at("abc", "d", [0, 1, 2], 1)) self.assertEqual("a nice_plac_", utils.replace_at("a nice place", "_", [6, 11], 1)) + +class TestUtilsFormat(TestCase): def test_break_text(self): self.assertIsNone(utils.justify_text("abcd", 2)) self.assertIsNone(utils.justify_text("abcd efgh", 3)) @@ -124,22 +161,35 @@ class Test(TestCase): self.assertEqual("ab cd\nef gh", utils.justify_text("ab cd ef gh", 2)) self.assertEqual("ab\ncd ef\ngh", utils.justify_text("ab cd ef gh", 3)) - def test_best_fit(self): + def test_place_line_breaks(self): self.assertEqual([5, 9, 15], utils.place_line_breaks([5.2, 14.3, 15.2], [3, 5, 9, 15, 18])) self.assertEqual([5, 9, 15, 18], utils.place_line_breaks([5.2, 14.3, 14.5, 15.2], [3, 5, 9, 15, 18])) self.assertEqual([5, 9, 15, 18], utils.place_line_breaks([5.2, 14.3, 14.5, 15.2], [3, 5, 9, 15, 18, 20])) - def test_read_argument(self): - self.assertIsNone(utils.read_argument(["test", "-o", "test"], "--output")) - self.assertTrue(utils.read_argument(["test", "-O", "test"], "--output", "-o")) - self.assertIsNone(utils.read_argument(["test", "-o"], "-o", valued=True)) - self.assertEqual("test1", utils.read_argument(["test", "-o", "test1", "-o", "test2"], "-o", valued=True)) - args = ["test", "-o", "test1"] - self.assertTrue(utils.read_argument(args, "-o", delete=True)) - self.assertEqual(["test", "test1"], args) - args = ["test", "-o", "test1"] - self.assertEqual("test1", utils.read_argument(args, "-o", valued=True, delete=True)) - self.assertEqual(["test"], args) - args = ["test", "-o"] - self.assertIsNone(utils.read_argument(args, "-o", valued=True, delete=True)) - self.assertEqual(["test"], args) + +class TestUtilsWeb(TestCase): + def test_validate_url(self): + self.assertTrue(utils.validate_url("https://google.com/page#anchor?key=value&query")) + self.assertFalse(utils.validate_url("https:google.com/page#anchor?key=value&query")) + self.assertFalse(utils.validate_url("")) + self.assertFalse(utils.validate_url("google.com")) + + def test_read_web_file(self): + out, err = utils.read_web_file("http:invalid.url") + self.assertIsNone(out) + self.assertEqual('Invalid URL', err) + out, err = utils.read_web_file("http://unknown.domain/") + self.assertIsNone(out) + self.assertEqual('Could not connect to server', err) + out, err = utils.read_web_file("http://httpbin.org/status/418") + self.assertIsNone(out) + self.assertEqual('Could not connect: HTTP Error 418: I\'M A TEAPOT', err) + out, err = utils.read_web_file("http://httpbin.org/bytes/1024", max_file_size=1000) + self.assertIsNone(out) + self.assertEqual('File too big', err) + # out, err = utils.read_web_file("http://httpbin.org/delay/1", timeout=0.1) + # self.assertIsNone(out) + # self.assertEqual('Could not connect to server', err) + out, err = utils.read_web_file("http://httpbin.org/base64/dGVzdA==") + self.assertIsNone(err) + self.assertEqual('test', out.decode("utf-8")) 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