A Journey Down the QRCode Rabbit Hole with 12 Monkeys

Max Rydahl Andersen

Holly Cummins recently shared a blog post about a QR code generator that integrates images into the QR code, like this one:

myqr

I was intrigued by the concept and decided to explore it further. Holly’s script is already executable using JBang, like this:

jbang https://hollycummins.com/5c25661ae4594167a9c41570824f955d/QrCode.java \
      https://xam.dk myoverlay.png qrcode.png

You can also install it locally using jbang install https://hollycummins.com/5c25661ae4594167a9c41570824f955d/QrCode.java and then run it using qrcode with remote file arguments like this:

qrcode https://xam.dk %{https://placedog.net/64/64} qrcode.png

This will generate the following:

qrcode dog

I thought it would be interesting to simplify the initial setup. This led me down a fascinating rabbit hole. Now, you can do this:

jbang install qrcode@xam.dk
qrcode https://quarkus.io -od 200
       -i %{https://design.jboss.org/quarkus/logo/final/SVG/quarkus_icon_rgb_default.svg}

This will generate a QR code for https://quarkus.io with the Quarkus SVG logo embedded in it, scaled to 200x200.

So, how did I achieve this? Let’s start with the remote file arguments.

Remote file arguments

This feature doesn’t require any additional code to work. It even works with Holly’s original script.

JBang has a magic feature that interprets arguments enclosed with %{} as remote files. It downloads the argument and replaces it with the path to the downloaded file.

This means you can use a local file or any URL from the internet without making code changes. All the following are valid arguments, assuming you have internet access:

qrcode https://xam.dk -i xam.png

qrcode https://xam.dk -i %{https://placekitten.com/128/128}

qrcode https://xam.dk -i %{https://placedog.net/128/128}

Since JBang does the heavy lifting, you can use this feature with anything you run with JBang. It just needs to accept a file as input.

Java ImageIO actually supports loading directly from a URI so it is trivial to add support for this to Holly’s script directly. I’ll leave that as an exercise for the reader.

Now, let’s simplify the name of the command.

Simpler names

In Holly’s original script, she used QrCode.java as the name of the file and hosted it at https://hollycummins.com/5c25661ae4594167a9c41570824f955d/QrCode.java.

That’s a bit long to type.

There are several ways to simplify this.

First, you can use JBang’s aliases feature. This lets you create a short name for a script by adding a file called jbang-catalog.json in either your github repository or even on your website.

Simply do:

Now if that jbang-catalog was placed at https://hollycummins.com/jbang-catalog.json you could refer to it as:

Holly could also add a https://github.com/hollycummins/jbang-catalog repository with a jbang-catalog.json file in it and then you could refer to it as:

jbang qrcode@hollycummins

In my case, I’m going to make it available on https://xam.dk using two combined features. main.java and jbang-catalog.

main.java

I place my qrcode.java at https://xam.dk/qrcode/main.java - this lets you refer to it using jbang https://xam.dk/qrcode/main.java, but you can also just let it be jbang https://xam.dk/qrcode.

Notice how main.java is missing here. JBang automatically looks for a /main.java file if nothing else found at the URL. This simplifies the naming.

jbang-catalog on a site

Even https://xam.dk/qrcode is a bit long to type. So I can add a jbang-catalog.json file to my site at https://xam.dk/jbang-catalog.json and that is how you can refer to it:

jbang qrcode@xam.dk

Flags and Defaults

I utilize Picocli for managing flags and arguments. It’s a library that simplifies the creation of command line tools with Java.

Firstly, include the dependency in your qrcode.java file:

//DEPS info.picocli:picocli:4.5.0
//DEPS info.picocli:picocli-codegen:4.5.0

The codegen dependency is optional but it enables Picocli’s annotation processor, providing more comprehensive error messages for the required annotations during the build process.

Next, add fields to your class for the flags you need:

@Parameters(index = "0", description = "Text to encode")
String text;

@Option(names = {"-i", "--image"}, description = "Image to overlay", required = true)
Path imagePath;

@Option(names = {"-o", "--output"}, description = "Output file", defaultValue = "qrcode.png")
Path outPath;

There are also modifications to transition from a static method to a picocli annotated class, but that’s not crucial for this post. You can view the complete code at https://xam.dk/qrcode/main.java and learn more about these details at Picocli’s website.

With these changes, the qrcode command now includes a help text:

jbang qrcode@xam.dk
Missing required parameter: '<text>'
Usage: qrcode [-hV] -i=<imagePath> [-o=<outPath>] <text>
Make a QR code with an overlay image. Inspired by https://hollycummins.
com/creating-QR-codes/
      <text>                Text to encode
  -h, --help                Show this help message and exit.
  -i, --image=<imagePath>   Image to overlay
  -o, --output=<outPath>    Output file
  -V, --version             Print version information and exit.

Additionally:

  • Only two arguments are required as the default output file is now qrcode.png.

  • The flags can be placed in any order, qrcode -i xam.png https://xam.dk works just as well as qrcode https://xam.dk -i xam.png.

Next, I want to be able to load SVG files from simpleicons.org and scale them.

Monkey SVG and Scaling

Holly’s script employs the excellent ImageIO library provided by the standard JDK. It’s a great (though unmaintained) library for processing images in Java, but it doesn’t directly support SVG files.

Fortunately, there’s a library named TwelveMonkeys that, via Apache Batik, easily adds SVG (and other image) support to ImageIO.

So, I include the dependency in my qrcode.java file:

//DEPS org.apache.xmlgraphics:batik-transcoder:1.17
//DEPS com.twelvemonkeys.imageio:imageio-batik:3.9.4

With this, the code can now load .svg files.

However, SVG files are vector graphics and often have a size that’s not suitable for the "embedding image in QRCode" trick. So, we need to add a flag to control the image dimensions. I add an overlay-dimensions flag along with a basic converter for it:

@Option(names = { "-od",
            "--overlay-dimensions" }, description = "Dimension to apply to overlay", converter = DimensionsConverter.class)
    Dimension overlayDimensions;

class DimensionsConverter implements ITypeConverter<Dimension> {
    public Dimension convert(String value) throws Exception {
        String[] dim = value.split("[x,:]");

        if (dim.length < 1 && dim.length > 2) {
            throw new IllegalArgumentException("Invalid dimensions " + value);
        }

        int width = Integer.parseInt(dim[0]);
        int height = dim.length == 2 ? Integer.parseInt(dim[1]) : width;

        return new Dimension(width, height);
    }
}

I add the scaling code to the readImage method to scale on read when reading SVG files but interpolate when reading other image types:

ImageReadParam param = reader.getDefaultReadParam();

// scale svg when reading
if (dimensions != null && "svg".equals(reader.getFormatName())) {
    param.setSourceRenderSize(dimensions);
}

BufferedImage image = reader.read(0, param);

// scale non-svg by resampling
if (dimensions != null && !"svg".equals(reader.getFormatName())) {
    BufferedImageOp resampler = new ResampleOp(
                                    dimensions.width, dimensions.height,
                                    ResampleOp.FILTER_LANCZOS);
    image = resampler.filter(image, null);
}

return image;

There are a few more lines to this as we can no longer rely on the static method to return the image, but you can view the complete code at https://xam.dk/qrcode/main.java.

End of the Rabbit Hole

I also want to generate SVG files instead of PNG, but I must stop here or this rabbit hole will never end. It’s enough that I found 12 monkeys along the way.