47 Commits

Author SHA1 Message Date
klemek edeec37c40 coveralls integration 2020-05-02 16:10:23 +02:00
klemek ef4a28f40c coveralls integration 2020-05-02 16:09:35 +02:00
klemek a2ffd83ca1 coveralls integration 2020-05-02 16:05:18 +02:00
klemek 8312589ab4 shit 2020-05-02 16:00:39 +02:00
klemek 84510530ce coveralls integration 2020-05-02 15:59:05 +02:00
klemek f9c78e8892 coveralls integration 2020-05-02 15:55:12 +02:00
klemek 9a45b0a205 moved some tests to integration 2020-05-02 15:55:00 +02:00
klemek 7f1337e412 code quality improvement 2020-05-02 15:42:59 +02:00
klemek 839aff11f7 removed timeout test 2020-05-01 23:59:35 +02:00
klemek e4e61f866d v1.3 2020-05-01 23:57:46 +02:00
klemek d6503b9209 small formating 2020-05-01 23:57:16 +02:00
klemek 801edb3e20 final example for docs 2020-05-01 23:54:26 +02:00
klemek 534837d1d1 docs don't re-generate reaction + fixed tables 2020-05-01 23:46:27 +02:00
Klemek 84b6fd6ad2 first reactions 2020-05-01 23:37:07 +02:00
Klemek 0ef694fbc9 new templates 2020-05-01 23:16:31 +02:00
Klemek 84da593397 fixed db purge 2020-05-01 23:02:33 +02:00
Klemek 2070307c9d making levenstein lib optional 2020-05-01 22:59:40 +02:00
Klemek dca12b5d32 min width of 800 for templates 2020-05-01 22:53:01 +02:00
klemek 79ec20f0a4 Updated README.md 2020-05-01 22:41:05 +02:00
klemek 642338b69d damn you reformat 2020-05-01 22:40:26 +02:00
klemek 1aa26c06ef update of docs 2020-05-01 22:24:41 +02:00
klemek 6444a6e58d new unit tests 2020-05-01 21:02:25 +02:00
klemek 62afce22a6 more unit tests 2020-05-01 13:25:44 +02:00
klemek 6748073048 new docs separating templates / reactions 2020-05-01 13:05:17 +02:00
klemek 6cfd623685 fixed font size on most wide memes 2020-05-01 12:55:40 +02:00
klemek 28cd3f59d9 fixed db purge 2020-05-01 12:48:55 +02:00
klemek b2749898a5 changed some db loading features 2020-05-01 12:10:33 +02:00
klemek 6f38686513 better use of BytesIO 2020-04-30 09:12:05 +02:00
klemek 59a8530cbe checking image file size beforehand 2020-04-30 08:55:12 +02:00
klemek 671f6fd595 handling of errors on read_web 2020-04-30 08:50:38 +02:00
klemek 7c076b896f reading image data from input URL 2020-04-29 13:00:34 +02:00
klemek 7c0e292c91 fixed block on stdin empty 2020-04-29 13:00:14 +02:00
klemek f44e32fbf8 new url validate util + web file reading 2020-04-29 12:49:35 +02:00
klemek 9f46e8b8f9 prevent images from being too big for discord 2020-04-29 12:36:10 +02:00
klemek f99bfff3ed image data from message attachment 2020-04-29 12:31:48 +02:00
klemek f83ae349d6 reworked CLI + accept --input or piping 2020-04-29 12:25:40 +02:00
klemek 01afa3f16d wip meme part + can skip watermark 2020-04-29 12:25:21 +02:00
klemek 8caa7efb94 new stream util + non valued arg -> True/False 2020-04-29 12:24:52 +02:00
klemek b6d063e1e5 image mode from raw bytes 2020-04-29 12:23:49 +02:00
klemek 08c938719a error system: main -> CLI/bot instead of redoing it 2 times 2020-04-29 11:33:19 +02:00
klemek a7610c2f01 text part in meme pipeline 2020-04-28 18:11:09 +02:00
klemek dffdf656dc fixed final height not being accurate 2020-04-28 17:10:19 +02:00
klemek 27b1d422a6 simple memes piping 2020-04-28 17:07:22 +02:00
klemek 208a10c61e new arg util + code cleaning 2020-04-28 16:38:06 +02:00
klemek 462ec21a53 code cleaning 2020-04-28 16:28:29 +02:00
klemek 9e2d2fcce7 more unit tests 2020-04-28 16:17:12 +02:00
klemek 7e269c1ab5 1.3: new dev version 2020-04-28 16:17:05 +02:00
134 changed files with 1154 additions and 233 deletions
+4 -3
View File
@@ -37,6 +37,7 @@ jobs:
- run:
command: |
sudo pip install -r requirements.txt
sudo pip install pytest
python -m pytest ./tests/unit
name: Unit tests
sudo pip install pytest pytest-cov coveralls
pytest ./tests --cov=meme_otron
coveralls
name: Tests
+2
View File
@@ -0,0 +1,2 @@
service_name: circle-ci
parallel: true
+1
View File
@@ -6,3 +6,4 @@ tmp
.key*
*.pyc
.pytest_cache
.coverage
+12
View File
@@ -1,3 +1,7 @@
[![Total alerts](https://img.shields.io/lgtm/alerts/g/Klemek/meme-otron.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Klemek/meme-otron/alerts/)
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/Klemek/meme-otron.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Klemek/meme-otron/context:python)
[![Coverage Status](https://coveralls.io/repos/github/Klemek/meme-otron/badge.svg?branch=master)](https://coveralls.io/github/Klemek/meme-otron?branch=master)
# Meme-Otron
*When making a meme need to be instantaneous*
@@ -45,6 +49,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
+21 -22
View File
@@ -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)
+164
View File
@@ -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
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
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
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
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 <URL>```
* 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
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
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
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
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
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
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
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
<!--LIST1-START-->
<!--LIST1-END-->
### Reactions (no text)
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
<!--LIST2-START-->
<!--LIST2-END-->
## Examples
### Example 1: Simple template
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
<!--EXAMPLE1-START-->
<!--EXAMPLE1-END-->
### Example 2: Use of empty texts
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
<!--EXAMPLE2-START-->
<!--EXAMPLE2-END-->
### Example 3: Text + Template
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
<!--EXAMPLE3-START-->
<!--EXAMPLE3-END-->
### Example 4: Complex composition
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
<!--EXAMPLE4-START-->
<!--EXAMPLE4-END-->
+148 -6
View File
@@ -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
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
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
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
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 <URL>```
* 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
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
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
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
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
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
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
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
<!--START-->
<!--LIST1-START-->
||||
|:---:|:---:|:---:|
|**aliens**<br><a href='https://knowyourmeme.com/memes/ancient-aliens' target='_blank'>more info</a>|**alive**<br>alt: no_brain<br><a href='https://knowyourmeme.com/memes/oh-fuck-i-forgot-to-give-you-a-brain' target='_blank'>more info</a>|**argument**<br>alt: wrestlers<br><a href='https://knowyourmeme.com/memes/american-chopper-argument' target='_blank'>more info</a>|
@@ -88,8 +159,79 @@ Click on an image to enlarge it.
|<a href='./templates/sleeping.jpg' target='_blank'><img alt='enlarge' src='./preview/sleeping.jpg'/></a>|<a href='./templates/spiderman.jpg' target='_blank'><img alt='enlarge' src='./preview/spiderman.jpg'/></a>|<a href='./templates/struggle.jpg' target='_blank'><img alt='enlarge' src='./preview/struggle.jpg'/></a>|
|**t_pose**<br>alt: dominance, monika<br><a href='https://knowyourmeme.com/memes/monika-t-posing-over-sans' target='_blank'>more info</a>|**tom_cousins**<br>alt: cousins, backup, goons<br><a href='https://knowyourmeme.com/memes/tom-and-jerry-hired-goons' target='_blank'>more info</a>|**tough2**<br>alt: tough, fight<br><a href='https://knowyourmeme.com/memes/increasingly-buff-spongebob' target='_blank'>more info</a>|
|<a href='./templates/t_pose.jpg' target='_blank'><img alt='enlarge' src='./preview/t_pose.jpg'/></a>|<a href='./templates/tom_cousins.jpg' target='_blank'><img alt='enlarge' src='./preview/tom_cousins.jpg'/></a>|<a href='./templates/tough2.jpg' target='_blank'><img alt='enlarge' src='./preview/tough2.jpg'/></a>|
|**tough2bis**|**tough3**|**trump**<br>alt: law<br><a href='https://knowyourmeme.com/memes/trumps-first-order-of-business' target='_blank'>more info</a>|
|**tough2bis**<br>alt: soft|**tough3**|**trump**<br>alt: law<br><a href='https://knowyourmeme.com/memes/trumps-first-order-of-business' target='_blank'>more info</a>|
|<a href='./templates/tough2bis.jpg' target='_blank'><img alt='enlarge' src='./preview/tough2bis.jpg'/></a>|<a href='./templates/tough3.jpg' target='_blank'><img alt='enlarge' src='./preview/tough3.jpg'/></a>|<a href='./templates/trump.jpg' target='_blank'><img alt='enlarge' src='./preview/trump.jpg'/></a>|
|**trust_nobody**<br>alt: yourself, gun<br><a href='https://knowyourmeme.com/memes/trust-nobody-not-even-yourself' target='_blank'>more info</a>|**truth**<br>alt: scroll<br><a href='https://knowyourmeme.com/memes/the-scroll-of-truth' target='_blank'>more info</a>|**winnie2**<br>alt: winnie<br><a href='https://knowyourmeme.com/memes/tuxedo-winnie-the-pooh' target='_blank'>more info</a>|
|<a href='./templates/trust_nobody.jpg' target='_blank'><img alt='enlarge' src='./preview/trust_nobody.jpg'/></a>|<a href='./templates/truth.jpg' target='_blank'><img alt='enlarge' src='./preview/truth.jpg'/></a>|<a href='./templates/winnie2.jpg' target='_blank'><img alt='enlarge' src='./preview/winnie2.jpg'/></a>|||
<!--END-->
<!--LIST1-END-->
### Reactions (no text)
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
<!--LIST2-START-->
<!--LIST2-END-->
## Examples
### Example 1: Simple template
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
<!--EXAMPLE1-START-->
>
```
brain3
"Making memes using an image editor"
"Making memes using a Python script"
"Making memes using a Discord bot"
```
![](example1.jpg)
<!--EXAMPLE1-END-->
### Example 2: Use of empty texts
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
<!--EXAMPLE2-START-->
> 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)
<!--EXAMPLE2-END-->
### Example 3: Text + template
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
<!--EXAMPLE3-START-->
> Note how texts make paragraphs
```
text
"*Meme has a 'made with meme-otron' watermark*"
"reddit: ..."
"9gag: ..."
"meme-otron dev:"
-
culture
"meme otron"
```
![](example3.jpg)
<!--EXAMPLE3-END-->
### Example 4: Complex composition
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
<!--EXAMPLE4-START-->
<!--EXAMPLE4-END-->
+115 -44
View File
@@ -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"<br>alt: {', '.join(meme.aliases)}"
if meme.info is not None:
info_line += f"<br><a href='{meme.info}' target='_blank'>more info</a>"
info_line += "|"
if base:
img_line += f"<a href='../templates/{meme.template}' target='_blank'>"
else:
img_line += f"<a href='./templates/{meme.template}' target='_blank'>"
img_line += f"<img alt='enlarge' src='./preview/{meme.template}'/>" \
f"</a>|"
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"<br>alt: {', '.join(meme.aliases)}"
if meme.info is not None:
info_line += f"<br><a href='{meme.info}' target='_blank'>more info</a>"
info_line += "|"
img_line += f"" \
f"<a href='./templates/{meme.template}' target='_blank'>" \
f"<img alt='enlarge' src='./preview/{meme.template}'/>" \
f"</a>|"
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"<!--{tag}-START-->"
end_str = f"<!--{tag}-END-->"
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("<!--START-->")
i1 = content.index("<!--END-->") + len("<!--END-->")
with open(doc_file, mode='w') as f:
f.write(content[:i0])
f.write("<!--START-->\n")
f.write(doc_content)
f.write("\n<!--END-->")
f.write(content[i1:])
if __name__ == '__main__':
main()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 80 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 68 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 70 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 72 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 168 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 202 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 78 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 246 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

+1 -1
View File
@@ -1 +1 @@
VERSION = "1.2"
VERSION = "1.3"
+28 -7
View File
@@ -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:
+55 -6
View File
@@ -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,62 @@ 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)
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 +126,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
+9 -8
View File
@@ -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])
@@ -88,10 +91,8 @@ def load_item(i: int, item: dict):
meme.texts_len = current_text - 1
except TypeError as e:
logger.warning(f"Item '{item_id}'({i + 1}) / Text {j + 1}: {e}")
for text in meme.texts:
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")
for text in meme.texts:
text.update(meme.text_base)
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'")
+90 -41
View File
@@ -1,10 +1,11 @@
import logging
from typing import Optional, Tuple, List
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 +21,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
+5
View File
@@ -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:
+141 -35
View File
@@ -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
+142 -28
View File
@@ -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": []
}]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 71 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Some files were not shown because too many files have changed in this diff Show More