One Does Not Simply Build Memes with Java: Unless It’s Quarkus
Harness the speed of Quarkus and the power of Java 2D to generate memes dynamically through a RESTful API. Because image manipulation isn’t just for Python.
What do you get when you combine a legendary Java framework with a legendary meme? A REST API that lets you generate memes on the fly with custom text. Yes, we’re going full Boromir.
In this tutorial, we’ll build a meme generator API using Quarkus. It will overlay top and bottom text onto a template image using Java’s Graphics2D
and serve it via a blazing-fast REST endpoint. You can call it like:
/meme?top=Cannot%20simply%20walk%20into%20mordor&bottom=One%20does%20not
…and get back a JPEG image. No frontends, no nonsense. Just fast, fun Java.
Let’s start.
Step 1: Bootstrap Your Quarkus Project
We’ll start by generating a fresh Quarkus project that includes the RESTEasy Reactive extension, which makes building HTTP endpoints delightfully efficient.
Run this in your terminal:
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=quarkus-meme-generator \
-Dextensions="quarkus-rest,quarkus-awt" \
-DnoCode
cd quarkus-meme-generator
This gives you a clean base to work with, no starter code, just the essentials.
Step 2: Add Your Meme Assets
Our meme generator needs two things:
A template image (we’ll use the famous “One Does Not Simply” Boromir meme)
A meme-friendly font (we’ll use Impact, the undisputed king of meme typography)
Prepare Your Resources
Download the Impact font
Grab the TTF file from FontMeme or any other site that offersImpact.ttf
.Find the Boromir image
You’ll want a version of the classic meme. Save it asboromir.png
.
I should mention that it would be great to use a picture that you have rights to use. I have created a comic style version for this tutorial. Feel free to use it, it’s part of the github repository.Organize your resource files
Quarkus makes anything insidesrc/main/resources/META-INF/resources
automatically available on the classpath. So we’ll put the image there. Fonts go directly underresources
.
We’re now ready to start coding.
Step 3: Create the MemeService
This is where the magic happens. We’ll load the font and image, draw the user’s custom text, and output a fresh JPEG byte stream.
If you don’t feel like c&p all the steps, feel free to look at the complete repository on my Github account.
Create a new Java file at src/main/java/org/acme/MemeService.java
:
package org.acme;
import jakarta.enterprise.context.ApplicationScoped;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.FontMetrics;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import javax.imageio.ImageIO;
@ApplicationScoped
public class MemeService {
private Font memeFont;
// Load the font from resources
// Ensure the font file is placed in src/main/resources/Impact.ttf
public MemeService() {
try (InputStream is = getClass().getClassLoader().getResourceAsStream("Impact.ttf")) {
if (is == null)
throw new IllegalStateException("Font not found");
memeFont = Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(120f);
} catch (Exception e) {
throw new RuntimeException("Failed to load font", e);
}
}
// Generates a meme image with the specified top and bottom text
// This method reads a base image (boromir.png) from resources,
// draws the specified text on it, and returns the image as a byte array.
public byte[] generateMeme(String topText, String bottomText) {
try {
InputStream is = getClass().getClassLoader().getResourceAsStream("META-INF/resources/boromir.png");
if (is == null)
throw new IllegalStateException("Image not found");
BufferedImage image = ImageIO.read(is);
Graphics2D g = image.createGraphics();
g.setFont(memeFont);
g.setColor(Color.WHITE);
drawText(g, topText, image.getWidth(), image.getHeight(), true);
drawText(g, bottomText, image.getWidth(), image.getHeight(), false);
g.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", baos);
return baos.toByteArray();
} catch (Exception e) {
throw new RuntimeException("Failed to generate meme", e);
}
}
// Draws the specified text on the image at the top or bottom
private void drawText(Graphics2D g, String text, int width, int height, boolean isTop) {
if (text == null || text.isEmpty())
return;
FontMetrics metrics = g.getFontMetrics();
int x = (width - metrics.stringWidth(text)) / 2;
int y = isTop ? metrics.getHeight() : height - metrics.getHeight() / 4;
// Outline
g.setColor(Color.BLACK);
g.drawString(text.toUpperCase(), x - 2, y - 2);
g.drawString(text.toUpperCase(), x + 2, y - 2);
g.drawString(text.toUpperCase(), x - 2, y + 2);
g.drawString(text.toUpperCase(), x + 2, y + 2);
// Main text
g.setColor(Color.WHITE);
g.drawString(text.toUpperCase(), x, y);
}
}
This service encapsulates everything: font loading, image drawing, and byte array generation. The result? A clean separation of responsibilities, Quarkus-style.
Step 4: Expose the REST Endpoint
Time to let the outside world access your memes.
Create the REST controller at src/main/java/org/acme/MemeResource.java
:
package org.acme;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Response;
@Path("/meme")
public class MemeResource {
@Inject
MemeService memeService;
@GET
@Produces("image/jpeg")
public Response createMeme(@QueryParam("top") String top,
@QueryParam("bottom") String bottom) {
byte[] meme = memeService.generateMeme(top, bottom);
return Response.ok(meme).build();
}
}
This is a classic Quarkus resource class. It listens for GET requests on /meme
, grabs the top
and bottom
query parameters, passes them to the service, and returns the result as a JPEG image.
Step 5: Run and Meme
Let’s see it in action.
Start your app in Dev Mode:
./mvnw quarkus:dev
Then open your browser and go to:
http://localhost:8080/meme?top=One%20does%20not%20simply&bottom=refactor%20a%20quickstart
You should see Boromir glaring back at you with your custom message in glorious meme font.
Try tweaking the text or wrapping this into a frontend for a full-blown meme studio.
Why This Works So Well
This isn’t just a gimmick project. It shows how Quarkus handles:
Static asset loading (images, fonts)
Image processing on the fly using AWT
Byte-level media responses via REST
Hot Reload during dev mode
This is a fun way to explore real backend techniques: serving media, using configuration, and deploying portable APIs.
What’s Next?
Want to turn this into a meme microservice for your app? Add file upload support for templates. Connect it to a Slack bot. Store memes in a PostgreSQL database. Or pair it with a local LLM to auto-generate captions.
Because one does not simply stop at the first tutorial.