One Does Not Simply Build Memes with Java: Unless It’s Quarkus
Markus

Markus @myfear

About: Markus is a Java Champion, former Java EE Expert Group member, founder of JavaLand, reputed speaker at Java conferences around the world, and a very well known figure in the Enterprise Java world.

Location:
Munich, Germany
Joined:
Mar 26, 2024

One Does Not Simply Build Memes with Java: Unless It’s Quarkus

Publish Date: Jul 11
0 0

Meme

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
Enter fullscreen mode Exit fullscreen mode

…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
Enter fullscreen mode Exit fullscreen mode

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

  1. Download the Impact font

    Grab the TTF file from FontMeme or any other site that offers Impact.ttf.

  2. Find the Boromir image

    You’ll want a version of the classic meme. Save it as boromir.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.

  3. Organize your resource files

    Quarkus makes anything inside src/main/resources/META-INF/resources automatically available on the classpath. So we’ll put the image there. Fonts go directly under resources.

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then open your browser and go to:

http://localhost:8080/meme?top=One%20does%20not%20simply&bottom=refactor%20a%20quickstart
Enter fullscreen mode Exit fullscreen mode

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.

Meme

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.

Comments 0 total

    Add comment