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.
|
|
|
|
|**t_pose**
alt: dominance, monika
more info|**tom_cousins**
alt: cousins, backup, goons
more info|**tough2**
alt: tough, fight
more info|
|
|
|
|
-|**tough2bis**|**tough3**|**trump**
alt: law
more info|
+|**tough2bis**
alt: soft|**tough3**|**trump**
alt: law
more info|
|
|
|
|
|**trust_nobody**
alt: yourself, gun
more info|**truth**
alt: scroll
more info|**winnie2**
alt: winnie
more info|
|
|
|
|||
-
+
+
+### 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"
+```
+
+
+
+
+### 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"
+```
+
+
+
+
+### 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"
+```
+
+
+
+
+
+### 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"
" \
+ 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""
+ 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"
" \
- 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