[AB-70] moving image generation functionality to separate image generation module

removing doge image generation from default base
split rest-api into separate modules (base and extensions)
This commit is contained in:
Sheldan
2023-12-23 20:41:25 +01:00
parent e9d14ac417
commit 980ca9380c
39 changed files with 1896 additions and 41 deletions

View File

@@ -9,9 +9,4 @@
<artifactId>entertainment-int</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>

View File

@@ -23,9 +23,4 @@
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>image-generation</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.15-SNAPSHOT</version>
</parent>
<artifactId>image-generation-impl</artifactId>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>src/main/assembly/liquibase.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>image-generation-int</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,18 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
<id>liquibase</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<outputDirectory>.</outputDirectory>
<directory>${project.basedir}/src/main/resources/migrations</directory>
<includes>
<include>**/*</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,147 @@
package dev.sheldan.abstracto.imagegeneration.command;
import dev.sheldan.abstracto.core.command.UtilityModuleDefinition;
import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand;
import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
import dev.sheldan.abstracto.core.command.config.HelpInfo;
import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.templating.model.AttachedFile;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.FileService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.imagegeneration.config.ImageGenerationFeatureDefinition;
import dev.sheldan.abstracto.imagegeneration.config.ImageGenerationSlashCommandNames;
import dev.sheldan.abstracto.imagegeneration.service.ImageGenerationService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class Triggered extends AbstractConditionableCommand {
public static final String MEMBER_PARAMETER_KEY = "member";
@Autowired
private ImageGenerationService imageGenerationService;
@Autowired
private TemplateService templateService;
@Autowired
private ChannelService channelService;
@Autowired
private FileService fileService;
@Autowired
private InteractionService interactionService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
private static final String TRIGGERED_EMBED_TEMPLATE_KEY = "triggered_response";
@Value("${abstracto.feature.imagegeneration.triggered.imagesize}")
private Integer imageSize;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
Member member;
List<Object> parameters = commandContext.getParameters().getParameters();
if(parameters.isEmpty()) {
member = commandContext.getAuthor();
} else {
member = (Member) parameters.get(0);
}
File triggeredGifFile = imageGenerationService.getTriggeredGif(member.getEffectiveAvatar().getUrl(imageSize));
MessageToSend messageToSend = templateService.renderEmbedTemplate(TRIGGERED_EMBED_TEMPLATE_KEY, new Object());
// template support does not support binary files
AttachedFile file = AttachedFile
.builder()
.file(triggeredGifFile)
.fileName("avatar.gif")
.build();
messageToSend.getAttachedFiles().add(file);
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenAccept(unused -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
.thenApply(unused -> CommandResult.fromIgnored());
}
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
event.deferReply().queue();
Member targetMember;
if(slashCommandParameterService.hasCommandOption(MEMBER_PARAMETER_KEY, event)) {
targetMember = slashCommandParameterService.getCommandOption(MEMBER_PARAMETER_KEY, event, Member.class);
} else {
targetMember = event.getMember();
}
File triggeredGifFile = imageGenerationService.getTriggeredGif(targetMember.getEffectiveAvatar().getUrl(imageSize));
MessageToSend messageToSend = templateService.renderEmbedTemplate(TRIGGERED_EMBED_TEMPLATE_KEY, new Object());
// template support does not support binary files
AttachedFile file = AttachedFile
.builder()
.file(triggeredGifFile)
.fileName("avatar.gif")
.build();
messageToSend.getAttachedFiles().add(file);
return FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(messageToSend, event.getHook()))
.thenAccept(unused -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
.thenApply(unused -> CommandResult.fromIgnored());
}
@Override
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
Parameter memberParameter = Parameter
.builder()
.name(MEMBER_PARAMETER_KEY)
.type(Member.class)
.templated(true)
.optional(true)
.build();
parameters.add(memberParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(ImageGenerationSlashCommandNames.IMAGE_GENERATION)
.groupName("memes")
.commandName("triggered")
.build();
return CommandConfiguration.builder()
.name("triggered")
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.supportsEmbedException(true)
.async(true)
.slashCommandConfig(slashCommandConfig)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return ImageGenerationFeatureDefinition.IMAGE_GENERATION;
}
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.imagegeneration.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@PropertySource("classpath:image-generation-config.properties")
public class ImageGenerationConfig {
}

View File

@@ -0,0 +1,32 @@
package dev.sheldan.abstracto.imagegeneration.service;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.service.HttpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
@Component
public class ImageGenerationServiceBean implements ImageGenerationService {
@Value("${abstracto.feature.imagegeneration.triggered.url}")
private String triggeredUrl;
@Autowired
private HttpService httpService;
@Override
public File getTriggeredGif(String imageUrl) {
try {
return httpService.downloadFileToTempFile(triggeredUrl.replace("{1}", imageUrl));
} catch (IOException e) {
throw new AbstractoRunTimeException(String.format("Failed to download triggered gif for url %s with error %s", imageUrl, e.getMessage()));
}
}
}

View File

@@ -0,0 +1,5 @@
abstracto.featureFlags.imageGeneration.featureName=imageGeneration
abstracto.featureFlags.imageGeneration.enabled=false
abstracto.feature.imagegeneration.triggered.url=http://${PRIVATE_REST_API_HOST}:${PRIVATE_REST_API_PORT}/memes/triggered/file.gif?url={1}
abstracto.feature.imagegeneration.triggered.imagesize=128

View File

@@ -0,0 +1,10 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="seedData/data.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,20 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<property name="utilityModule" value="(SELECT id FROM module WHERE name = 'utility')"/>
<property name="imageGenerationFeature" value="(SELECT id FROM feature WHERE key = 'imageGeneration')"/>
<changeSet author="Sheldan" id="triggered-command">
<insert tableName="command">
<column name="name" value="triggered"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${imageGenerationFeature}"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,11 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="feature.xml" relativeToChangelogFile="true"/>
<include file="command.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,14 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="imageGeneration_feature-insertion">
<insert tableName="feature">
<column name="key" value="imageGeneration"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,10 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="1.5.15/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>image-generation</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.15-SNAPSHOT</version>
</parent>
<artifactId>image-generation-int</artifactId>
</project>

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.imagegeneration.config;
import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import org.springframework.stereotype.Component;
@Component
public class ImageGenerationFeatureConfig implements FeatureConfig {
@Override
public FeatureDefinition getFeature() {
return ImageGenerationFeatureDefinition.IMAGE_GENERATION;
}
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.imagegeneration.config;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import lombok.Getter;
@Getter
public enum ImageGenerationFeatureDefinition implements FeatureDefinition {
IMAGE_GENERATION("imageGeneration");
private String key;
ImageGenerationFeatureDefinition(String key) {
this.key = key;
}
}

View File

@@ -0,0 +1,5 @@
package dev.sheldan.abstracto.imagegeneration.config;
public class ImageGenerationSlashCommandNames {
public static final String IMAGE_GENERATION = "imagegeneration";
}

View File

@@ -0,0 +1,7 @@
package dev.sheldan.abstracto.imagegeneration.service;
import java.io.File;
public interface ImageGenerationService {
File getTriggeredGif(String imageUrl);
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>abstracto-modules</artifactId>
<version>1.5.15-SNAPSHOT</version>
</parent>
<artifactId>image-generation</artifactId>
<packaging>pom</packaging>
<modules>
<module>image-generation-int</module>
<module>image-generation-impl</module>
</modules>
<dependencies>
<dependency>
<groupId>dev.sheldan.abstracto.core</groupId>
<artifactId>core-int</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -9,11 +9,6 @@
<artifactId>invite-filter-int</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>dev.sheldan.abstracto.core</groupId>

View File

@@ -33,6 +33,7 @@
<module>custom-command</module>
<module>twitch</module>
<module>giveaway</module>
<module>image-generation</module>
</modules>
</project>

View File

@@ -294,6 +294,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>image-generation-impl</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>giveaway-impl</artifactId>

View File

@@ -6,6 +6,6 @@ import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
public interface CommandAlternative extends Prioritized {
boolean shouldExecute(UnParsedCommandParameter parameterm, Guild guild);
boolean shouldExecute(UnParsedCommandParameter parameter, Guild guild);
void execute(UnParsedCommandParameter parameter, Message message);
}

View File

@@ -7,8 +7,13 @@ services:
template_deployment:
build: python/deployment/installer/template-deployment
image: ${REGISTRY_PREFIX}abstracto-template-deployment:${VERSION:-latest}
rest_api:
rest_api_base:
build:
context: python/components/rest-api
context: python/components/rest-api-base
dockerfile: docker/Dockerfile
image: ${REGISTRY_PREFIX}abstracto-rest-api:${VERSION:-latest}
rest_api_image_gen:
build:
context: python/components/image-gen
dockerfile: docker/Dockerfile
image: ${REGISTRY_PREFIX}abstracto-rest-api-image-gen:${VERSION:-latest}

View File

@@ -0,0 +1,3 @@
FROM alpine:3.19.0
ADD resources /python/resources
ADD python /python

View File

@@ -0,0 +1,76 @@
from __main__ import app
from PIL import Image, ImageOps
from flask import request
import requests
import validators
import logging
import io
from utils import flask_utils
allowed_content_length = 4_000_000
allowed_image_formats = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif']
max_off_set = 5
gif_speedup_factor = 4
@app.route('/memes/triggered/file.gif') # to directly embed, discord _requires_ this file ending, it seems
def triggered_animated():
url = request.args.get('url')
if not validators.url(url):
return 'no valid url', 400
session = requests.Session()
response = session.head(url)
content_type = response.headers['content-type']
if content_type not in allowed_image_formats:
return f'Incorrect image type {content_type}', 400
actual_content_length = int(response.headers['content-length'])
if actual_content_length > allowed_content_length:
return f'Image too large {actual_content_length}', 400
image_file = requests.get(url, stream=True)
input_image = Image.open(io.BytesIO(image_file.content))
original_input_image = input_image
old_width, old_height = input_image.size
with Image.open('resources/img/triggered_footer.jpg') as footer_image:
footer_width, footer_height = footer_image.size
footer_ratio = old_width / footer_width
desired_new_footer_width = int(footer_width * footer_ratio)
if footer_ratio > 1:
footer_image = footer_image.resize((desired_new_footer_width, footer_height))
else:
footer_image = ImageOps.contain(footer_image, (desired_new_footer_width, footer_height))
new_footer_width, new_footer_height = footer_image.size
new_total_height = old_height + new_footer_height
points = [[0, -max_off_set], [max_off_set, 0], [0, max_off_set], [-max_off_set, 0]]
if content_type == 'image/gif':
logging.info(f'Rendering triggered for gif.')
frame_count = original_input_image.n_frames
old_frames = []
for frame_index in range(frame_count):
input_image.seek(frame_index)
frame = input_image.convert('RGBA')
old_frames.append(frame)
frames = []
for index, old_frame in enumerate(old_frames):
off_set = points[index % len(points)]
frame = Image.new('RGBA', (old_width, new_total_height), (0, 0, 0, 0))
old_frame = ImageOps.contain(old_frame, (old_width + max_off_set * 2, old_height + max_off_set * 2))
frame.paste(old_frame, (-max_off_set + off_set[0], -max_off_set + off_set[1]))
frame.paste(footer_image, (0, old_height))
frames.append(frame)
return flask_utils.serve_pil_gif_image(frames, (int(original_input_image.info['duration']) / gif_speedup_factor))
else:
input_image = ImageOps.contain(input_image, (old_width + max_off_set * 2, old_height + max_off_set * 2))
frames = []
logging.info(f'Rendering triggered for static image.')
for off_set in points:
frame = Image.new('RGBA', (old_width, new_total_height), (0, 0, 0, 0))
frame.paste(input_image, (-max_off_set + off_set[0], -max_off_set + off_set[1]))
frame.paste(footer_image, (0, old_height))
frames.append(frame)
return flask_utils.serve_pil_gif_image(frames)

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -5,6 +5,5 @@ RUN apk --no-cache add msttcorefonts-installer fontconfig && \
ADD wrapper /
ADD python/requirements.txt requirements.txt
RUN pip install -r requirements.txt
ADD resources /python/resources
ADD python /python
CMD ["/run.sh"]

View File

@@ -11,8 +11,6 @@ app = Flask(__name__, template_folder=template_dir)
import sys
sys.path.append("..")
# loads the api end points
from base import image_gen
# This code was only done, because "from custom import *" did not work, it seems it did not execute the code in it
# while the code was valid
@@ -48,7 +46,7 @@ def dynamic_import_from_src(src, star_import=False):
if __name__ == "__main__":
dynamic_import_from_src("custom", star_import=False)
dynamic_import_from_src("endpoints", star_import=False)
@app.route('/')
def hello():

View File

@@ -14,4 +14,5 @@ urllib3==2.0.7
waitress==2.1.2
Werkzeug==3.0.1
zipp==3.17.0
pytz==2023.3.post1
pytz==2023.3.post1
validators==0.22.0

View File

@@ -10,6 +10,13 @@ def serve_pil_image(pil_img):
return send_file(img_io, mimetype='image/png')
def serve_pil_gif_image(frames, duration=25):
animated_gif = BytesIO()
frames[0].save(animated_gif, format='GIF', save_all=True, append_images=frames[1:], duration=duration, loop=0, disposal=2)
animated_gif.seek(0)
return send_file(animated_gif, mimetype='image/gif')
class ValidationException(Exception):
def __init__(self, provided_value, message):
self.provided_value = provided_value

View File

@@ -1,18 +0,0 @@
from __main__ import app
from PIL import Image, ImageDraw, ImageFont
from flask import request
from utils import flask_utils
@app.route('/memes/doge/orangeSun/')
def image_gen():
text = request.args.get('text')
with Image.open('resources/img/semf_template.jpg') as im:
d1 = ImageDraw.Draw(im)
text_box_size = (300, 240)
W, H = text_box_size
font = ImageFont.truetype(f'Impact.ttf', 60)
_, _, w, h = d1.textbbox((0, 0), text, font=font)
d1.text(((W-w)/2 + 320, (H-h)/2 + 120), text, font=font, fill=(255, 255, 255))
return flask_utils.serve_pil_image(im)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB