Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9edff02dc0 |
@@ -37,7 +37,6 @@ jobs:
|
||||
- run:
|
||||
command: |
|
||||
sudo pip install -r requirements.txt
|
||||
sudo pip install pytest pytest-cov coveralls
|
||||
pytest ./tests --cov=meme_otron
|
||||
coveralls
|
||||
name: Tests
|
||||
sudo pip install pytest
|
||||
python -m pytest ./tests/unit
|
||||
name: Unit tests
|
||||
@@ -1,2 +0,0 @@
|
||||
service_name: circle-ci
|
||||
parallel: true
|
||||
@@ -6,4 +6,3 @@ tmp
|
||||
.key*
|
||||
*.pyc
|
||||
.pytest_cache
|
||||
.coverage
|
||||
@@ -1,7 +1,3 @@
|
||||
[](https://lgtm.com/projects/g/Klemek/meme-otron/alerts/)
|
||||
[](https://lgtm.com/projects/g/Klemek/meme-otron/context:python)
|
||||
[](https://coveralls.io/github/Klemek/meme-otron?branch=master)
|
||||
|
||||
# Meme-Otron
|
||||
|
||||
*When making a meme need to be instantaneous*
|
||||
@@ -49,14 +45,6 @@ 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
|
||||
|
||||
@@ -3,7 +3,7 @@ import traceback
|
||||
import logging
|
||||
import discord
|
||||
import re
|
||||
from io import BytesIO
|
||||
import tempfile
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
@@ -116,32 +116,28 @@ 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])
|
||||
|
||||
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" \
|
||||
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" \
|
||||
f"<{DOC_URL}>"
|
||||
if len(response) >= 2000:
|
||||
await message.channel.send(f"{message.author.mention} ... really?")
|
||||
else:
|
||||
await message.channel.send(response)
|
||||
else:
|
||||
with BytesIO() as output_file:
|
||||
img.save(output_file, format="JPEG")
|
||||
output_file.flush()
|
||||
output_file.seek(0)
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False) as output:
|
||||
img.save(output, format="JPEG")
|
||||
response = None
|
||||
meme_id = utils.sanitize_input(args[0])
|
||||
if len(args) == 1 and meme_id not in ["image", "text"]:
|
||||
if len(args) == 1:
|
||||
meme = meme_db.get_meme(meme_id)
|
||||
response = f"Template `{meme.id}`:"
|
||||
if len(meme.aliases) > 0:
|
||||
@@ -156,8 +152,13 @@ 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(output_file, "meme.jpg"))
|
||||
response = await message.channel.send(response,
|
||||
file=discord.File(filename="meme.jpg", fp=output.name))
|
||||
SENT[message_id] += [response]
|
||||
try:
|
||||
os.remove(output.name)
|
||||
except PermissionError:
|
||||
pass
|
||||
if not is_direct:
|
||||
await delete(message)
|
||||
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
# 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-->
|
||||
@@ -1,25 +1,5 @@
|
||||
# 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:
|
||||
|
||||
```
|
||||
@@ -35,36 +15,7 @@ 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
|
||||
|
||||
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>
|
||||
## Discord features
|
||||
|
||||
Tag the bot and use the above syntax to get started. In addition, you can use the following commands:
|
||||
|
||||
@@ -78,8 +29,7 @@ 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
|
||||
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
|
||||
## CLI features
|
||||
|
||||
In this project directory, you can simply call:
|
||||
```
|
||||
@@ -90,37 +40,16 @@ 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
|
||||
|
||||
> Note: with `-o`, you are free to choose the output format
|
||||
|
||||
## 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-->
|
||||
<!--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>|
|
||||
@@ -159,79 +88,8 @@ 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**<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>|
|
||||
|**tough2bis**|**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>|||
|
||||
<!--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-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-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-END-->
|
||||
|
||||
|
||||
### Example 4: Complex composition
|
||||
<sub><sup>[↑ back to top](#meme-otron-guide)</sup></sub>
|
||||
|
||||
<!--EXAMPLE4-START-->
|
||||
|
||||
<!--EXAMPLE4-END-->
|
||||
<!--END-->
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
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)
|
||||
|
||||
@@ -15,66 +13,12 @@ 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):
|
||||
@@ -84,69 +28,54 @@ def make_empty(target_dir: str):
|
||||
os.mkdir(target_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)
|
||||
make_empty(templates_dir)
|
||||
make_empty(preview_dir)
|
||||
|
||||
id_list = sorted(meme_db.LIST)
|
||||
|
||||
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)
|
||||
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)
|
||||
if img is not None:
|
||||
img.save(utils.relative_path(__file__, file_name))
|
||||
return inject_content(doc_content, content, tag)
|
||||
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)
|
||||
|
||||
doc_content += "|" * (COLUMNS - (i % COLUMNS))
|
||||
|
||||
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:]
|
||||
with open(doc_file, mode='r') as f:
|
||||
content = "".join(f.readlines())
|
||||
|
||||
i0 = content.index("<!--START-->")
|
||||
i1 = content.index("<!--END-->") + len("<!--END-->")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
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:])
|
||||
|
||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
@@ -1 +1 @@
|
||||
VERSION = "1.3"
|
||||
VERSION = "1.2"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
|
||||
from . import img_factory
|
||||
from . import meme_db
|
||||
@@ -9,22 +8,11 @@ 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"
|
||||
@@ -33,21 +21,12 @@ if __name__ == "__main__":
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
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)
|
||||
output_file = utils.read_argument(sys.argv, "-o", "--output", valued=True, delete=True)
|
||||
img = meme_otron.compute(*sys.argv[1:])
|
||||
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:
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from typing import List, Optional, Tuple
|
||||
from PIL import Image, ImageFont, ImageDraw, UnidentifiedImageError
|
||||
import io
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
import os
|
||||
import os.path as path
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from . import utils
|
||||
from .types import Text
|
||||
@@ -14,8 +12,6 @@ TEMPLATES_DIR = utils.relative_path(__file__, "..", "templates")
|
||||
|
||||
FONTS = {}
|
||||
|
||||
TEXT_IMAGE_WIDTH = 800
|
||||
|
||||
logger = logging.getLogger("img_factory")
|
||||
|
||||
|
||||
@@ -30,62 +26,17 @@ def load_fonts():
|
||||
logger.error(f"Could not load font '{split[0]}'")
|
||||
|
||||
|
||||
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]:
|
||||
def build_image(template: str, texts: List[Text], debug: bool = False) -> Optional[Image.Image]:
|
||||
try:
|
||||
img = Image.open(path.join(TEMPLATES_DIR, template)).convert(mode='RGBA')
|
||||
except OSError as e:
|
||||
logger.error(f"Could not read template file '{template}': {e}")
|
||||
return None
|
||||
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')
|
||||
|
||||
|
||||
@@ -126,10 +77,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 * size[0]) + 1
|
||||
font_size = round(text.font_size * min(size)) + 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
|
||||
|
||||
@@ -15,11 +15,10 @@ logger = logging.getLogger("meme_db")
|
||||
|
||||
|
||||
def load_memes(purge: bool = False):
|
||||
global DATA, ALIASES, LIST
|
||||
global DATA, ALIASES
|
||||
if purge:
|
||||
DATA.clear()
|
||||
ALIASES.clear()
|
||||
LIST = []
|
||||
DATA = {}
|
||||
ALIASES = {}
|
||||
try:
|
||||
with open(DATA_FILE) as input_file:
|
||||
content = "".join(input_file.readlines())
|
||||
@@ -44,8 +43,6 @@ 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])
|
||||
@@ -91,8 +88,10 @@ 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)
|
||||
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:
|
||||
@@ -130,7 +129,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 = getattr(Pos, raw_text["position"])
|
||||
text.position = [p for p in Pos if p.name == raw_text["position"]][0]
|
||||
if "align" in raw_text:
|
||||
if raw_text["align"] not in ["left", "center", "right"]:
|
||||
raise TypeError(f"'align' is not 'left', 'center' or 'right'")
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from typing import Optional, Tuple, List
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
import logging
|
||||
|
||||
from .types import Text, Pos
|
||||
from . import img_factory
|
||||
from . import meme_db
|
||||
from . import utils
|
||||
from . import img_factory as imgf
|
||||
from . import meme_db as db
|
||||
|
||||
logger = logging.getLogger("meme_otron")
|
||||
|
||||
right_wmark = Text("Made with meme-otron")
|
||||
right_wmark.position = Pos.SE
|
||||
@@ -21,91 +20,43 @@ left_wmark.font_size = 0.02
|
||||
left_wmark.x_range = [0.005, 0.995]
|
||||
left_wmark.y_range = [0.005, 0.995]
|
||||
|
||||
simple_text = Text()
|
||||
simple_text.align = "left"
|
||||
simple_text.position = Pos.W
|
||||
simple_text.font_size = 0.04
|
||||
simple_text.x_range = [0.01, 0.99]
|
||||
simple_text.y_range = [0.2, 0.8]
|
||||
|
||||
def parse_text(s):
|
||||
"""
|
||||
:param (str) s:
|
||||
:rtype: str
|
||||
"""
|
||||
return s.replace("\\n", "\n")
|
||||
|
||||
|
||||
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]]:
|
||||
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:
|
||||
"""
|
||||
if len(args) < 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:
|
||||
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
|
||||
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 = meme.texts[meme.texts[i].text_ref].text
|
||||
return img_factory.build_from_template(meme.template, meme.texts, debug=debug), None
|
||||
meme.texts[i].text = ""
|
||||
c += 1
|
||||
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)
|
||||
|
||||
@@ -57,11 +57,6 @@ 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:
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
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, Tuple, BinaryIO
|
||||
|
||||
try:
|
||||
from Levenshtein import distance
|
||||
except ModuleNotFoundError:
|
||||
distance = None
|
||||
|
||||
|
||||
# region path utils
|
||||
from typing import List, Optional, Union
|
||||
from Levenshtein import distance
|
||||
|
||||
|
||||
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,
|
||||
@@ -53,11 +37,6 @@ 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):
|
||||
@@ -86,11 +65,6 @@ def is_list_of(obj, types: List[type], length: Optional[int] = None) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region args utils
|
||||
|
||||
|
||||
args_regex = re.compile('"([^"]*)"|\'([^\']*)\'|([^ ]+)')
|
||||
|
||||
|
||||
@@ -104,45 +78,7 @@ 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
|
||||
@@ -155,14 +91,6 @@ 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):
|
||||
@@ -174,28 +102,6 @@ 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)
|
||||
@@ -215,6 +121,23 @@ 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)
|
||||
@@ -222,48 +145,19 @@ def safe_index(src: Union[str, list], pattern, start: int = 0):
|
||||
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
|
||||
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
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"id": "2_texts",
|
||||
"abstract": true,
|
||||
"based_on": "white_text",
|
||||
"font_size": 0.08,
|
||||
"font_size": 0.12,
|
||||
"texts": [{
|
||||
"x_range": [0.02, 0.98],
|
||||
"y_range": [0.02, 0.50],
|
||||
@@ -199,6 +199,7 @@
|
||||
"id": "tough2bis",
|
||||
"aliases": ["soft"],
|
||||
"template": "tough2bis.jpg",
|
||||
"aliases": [],
|
||||
"based_on": "2_panel_right"
|
||||
},{
|
||||
"id": "tough3",
|
||||
@@ -246,7 +247,7 @@
|
||||
"based_on": "white_text",
|
||||
"aliases": [],
|
||||
"info": "https://knowyourmeme.com/memes/ancient-aliens",
|
||||
"font_size": 0.16,
|
||||
"font_size": 0.15,
|
||||
"texts": [{
|
||||
"x_range": [0.02, 0.98],
|
||||
"y_range": [0.8, 0.98],
|
||||
@@ -344,11 +345,11 @@
|
||||
"aliases": ["tom", "jerry"],
|
||||
"info": "https://knowyourmeme.com/memes/buff-tom",
|
||||
"based_on": "white_text",
|
||||
"font_size": 0.06,
|
||||
"font_size": 0.07,
|
||||
"texts": [{
|
||||
"x_range": [0.07, 0.36],
|
||||
"y_range": [0.69, 0.98],
|
||||
"font_size": 0.04
|
||||
"font_size": 0.05
|
||||
},{
|
||||
"x_range": [0.44, 0.84],
|
||||
"y_range": [0.27, 0.59]
|
||||
@@ -359,7 +360,7 @@
|
||||
"based_on": "white_text",
|
||||
"aliases": ["nut"],
|
||||
"info": "https://knowyourmeme.com/memes/nut-button",
|
||||
"font_size": 0.07,
|
||||
"font_size": 0.08,
|
||||
"texts": [{
|
||||
"x_range": [0.10, 0.46],
|
||||
"y_range": [0.46, 0.80],
|
||||
@@ -408,7 +409,6 @@
|
||||
"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.04,
|
||||
"font_size": 0.06,
|
||||
"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.05,
|
||||
"font_size": 0.06,
|
||||
"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.04,
|
||||
"font_size": 0.08,
|
||||
"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.08
|
||||
"font_size": 0.15
|
||||
},{
|
||||
"x_range": [0.03, 0.16],
|
||||
"y_range": [0.65, 0.98],
|
||||
"font_size": 0.03,
|
||||
"font_size": 0.04,
|
||||
"position": "W"
|
||||
},{
|
||||
"x_range": [0.33, 0.48],
|
||||
"y_range": [0.78, 0.97],
|
||||
"font_size": 0.03,
|
||||
"font_size": 0.04,
|
||||
"position": "SE"
|
||||
},{
|
||||
"x_range": [0.34, 0.48],
|
||||
"y_range": [0.35, 0.61],
|
||||
"font_size": 0.03,
|
||||
"font_size": 0.04,
|
||||
"position": "E"
|
||||
}]
|
||||
},{
|
||||
@@ -595,7 +595,7 @@
|
||||
"aliases": [],
|
||||
"info": "https://knowyourmeme.com/memes/woman-yelling-at-a-cat",
|
||||
"position": "S",
|
||||
"font_size": 0.08,
|
||||
"font_size": 0.12,
|
||||
"texts": [{
|
||||
"x_range": [0.01, 0.49],
|
||||
"y_range": [0.50, 0.99]
|
||||
@@ -608,7 +608,6 @@
|
||||
"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],
|
||||
@@ -632,7 +631,7 @@
|
||||
"based_on": "white_text",
|
||||
"aliases": [],
|
||||
"info": "https://knowyourmeme.com/memes/epic-handshake",
|
||||
"font_size": 0.09,
|
||||
"font_size": 0.12,
|
||||
"texts": [{
|
||||
"x_range": [-0.05, 0.45],
|
||||
"y_range": [0.48, 0.78],
|
||||
@@ -706,7 +705,7 @@
|
||||
"based_on": "white_text",
|
||||
"aliases": ["butterfly"],
|
||||
"info": "https://knowyourmeme.com/memes/is-this-a-pigeon",
|
||||
"font_size": 0.07,
|
||||
"font_size": 0.08,
|
||||
"texts": [{
|
||||
"x_range": [0.02, 0.98],
|
||||
"y_range": [0.86, 0.99],
|
||||
@@ -762,12 +761,12 @@
|
||||
"info": "https://knowyourmeme.com/memes/nobody-is-born-cool",
|
||||
"based_on": "white_text",
|
||||
"aliases": [],
|
||||
"font_size": 0.05,
|
||||
"font_size": 0.07,
|
||||
"texts": [{
|
||||
"x_range": [0.07, 0.39],
|
||||
"y_range": [0.62, 0.85],
|
||||
"stroke_width": 0,
|
||||
"font_size": 0.04,
|
||||
"font_size": 0.05,
|
||||
"angle": -15
|
||||
},{
|
||||
"x_range": [0.18, 0.46],
|
||||
@@ -787,14 +786,14 @@
|
||||
"based_on": "white_text",
|
||||
"aliases": ["joker"],
|
||||
"info": "https://knowyourmeme.com/memes/mini-joker",
|
||||
"font_size": 0.06,
|
||||
"font_size": 0.07,
|
||||
"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.04
|
||||
"font_size": 0.05
|
||||
}]
|
||||
},{
|
||||
"id": "nobody_cares",
|
||||
@@ -817,7 +816,6 @@
|
||||
"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],
|
||||
@@ -909,8 +907,7 @@
|
||||
"y_range": [0.32, 0.47]
|
||||
},{
|
||||
"x_range": [0.70, 0.95],
|
||||
"y_range": [0.80, 0.95],
|
||||
"text_ref": 2
|
||||
"y_range": [0.80, 0.95]
|
||||
}]
|
||||
},{
|
||||
"id": "spiderman",
|
||||
@@ -918,7 +915,7 @@
|
||||
"based_on": "white_text",
|
||||
"aliases": ["same"],
|
||||
"info": "https://knowyourmeme.com/memes/spider-man-pointing-at-spider-man",
|
||||
"font_size": 0.05,
|
||||
"font_size": 0.07,
|
||||
"texts": [{
|
||||
"x_range": [0.15, 0.39],
|
||||
"y_range": [0.26, 0.52]
|
||||
@@ -955,7 +952,6 @@
|
||||
"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],
|
||||
@@ -968,7 +964,7 @@
|
||||
},{
|
||||
"x_range": [0.05, 0.38],
|
||||
"y_range": [0.42, 0.81],
|
||||
"font_size": 0.06
|
||||
"font_size": 0.07
|
||||
}]
|
||||
},{
|
||||
"id": "trust_nobody",
|
||||
@@ -976,7 +972,7 @@
|
||||
"based_on": "white_text",
|
||||
"aliases": ["yourself","gun"],
|
||||
"info": "https://knowyourmeme.com/memes/trust-nobody-not-even-yourself",
|
||||
"font_size": 0.05,
|
||||
"font_size": 0.07,
|
||||
"texts": [{
|
||||
"x_range": [0.16, 0.50],
|
||||
"y_range": [0.32, 0.85]
|
||||
@@ -1027,7 +1023,7 @@
|
||||
"based_on": "white_text",
|
||||
"aliases": ["cousins","backup", "goons"],
|
||||
"info": "https://knowyourmeme.com/memes/tom-and-jerry-hired-goons",
|
||||
"font_size": 0.06,
|
||||
"font_size": 0.07,
|
||||
"texts": [{
|
||||
"x_range": [0.14, 0.39],
|
||||
"y_range": [0.21, 0.69]
|
||||
@@ -1113,7 +1109,7 @@
|
||||
"id": "seagull4",
|
||||
"template": "seagull4.jpg",
|
||||
"based_on": "white_text",
|
||||
"font_size": 0.04,
|
||||
"font_size": 0.07,
|
||||
"position": "S",
|
||||
"texts": [{
|
||||
"x_range": [0.01, 0.49],
|
||||
@@ -1127,11 +1123,11 @@
|
||||
},{
|
||||
"x_range": [0.51, 0.99],
|
||||
"y_range": [0.80, 0.99],
|
||||
"font_size": 0.05
|
||||
"font_size": 0.09
|
||||
},{
|
||||
"x_range": [0.16, 0.38],
|
||||
"y_range": [0.03, 0.17],
|
||||
"font_size": 0.03
|
||||
"font_size": 0.04
|
||||
},{
|
||||
"x_range": [0.72, 0.94],
|
||||
"y_range": [0.00, 0.13],
|
||||
@@ -1365,114 +1361,4 @@
|
||||
"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": []
|
||||
}]
|
||||
|
||||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 47 KiB |