QR code

How to data merge QR codes with InDesign

So how did I end up mail merging my QR codes? My solution turned out to be typographic after all, but it did not involve contextual glyph substitutions.

The key to mail merging QR codes is to realize that while we cannot control formatting in InDesign’s data merge, square shapes exist in Unicode, so if there is a programmatic way to generate the dot patterns, we can pick a monospaced font, vary the glyphs used, and end up with a scannable QR code.

One way to generate the dot patterns is to use the qrencode tool by Kentarō Fukuchi. Its “-t ASCII” option is especially useful, as this means we don’t need to interface with the C library; the ASCII output can easily be parsed using a Perl script or similar and turned into a simple pattern of zeroes and ones.

So two things came out of this exercise: First, InDesign cannot handle ideographic spaces (U+3000) in data files, so CJK fonts are out; and second, qrencode -t ASCII doubles up the ASCII characters so that the dots come out visually more or less correct, so to convert this output into a dot pattern we need to deduplicate the ASCII characters.

The key portion of my code looks is this:

sub get_qrcode ($$) {
    # Note that qrencode uses ## for a dot (and 2 spaces for no dot), not just #, so we need to deduplicate first
    my($s, $quality) = @_;
    my $h = open(INPUT, '-|');
    die "$0: exec: fork: $!\n" unless defined $h;
    my @qr = ();
    if (!$h) {
        my @cmdline = ('qrencode', '-o', '-', '-s', '1', '-l', $quality, '-m', ' 0',  '-t', 'ASCII', $s);
        exec { $cmdline[0] } @cmdline;
        die "$0: exec: $!\n";
    } else {
        for (;;) {
            my $s = scalar <INPUT>;
        last unless defined $s;
            chomp $s;
            $s =~ s/([# ])\1/\1/g; # deduplicate
            my @spec = map { ($_ eq '#') + 0 } split('', $s);
            push @qr, [@spec];
    return @qr;

Provided that qrencode is installed, this code will convert the input string into a QR code bit pattern with the specified quality.

To form the dots it is necessary to choose a perfectly square glyph. Since I was dealing with print, I can also pick a rectangular glyph and scale it horizontally to make it square. One obvious candidate is U+2588, which fills up the entire glyph space and is naturally square in most CJK fonts. Unfortunately if we use a CJK font we will need to use the CJK space, and it turned out that putting CJK spaces in the data file will cause InDesign to not recognize a UCS-2 file as UCS-2. So there are two options: Use U+2588 with a normal monospaced font and scale the glyphs horizontally, or use something else such as U+25A0, which is a smaller square dot.

It turns out that if we use U+2588, we will need to typeset the dots in a really small point size, like 2pt. If we then generate a PDF and view it in Apple’s Preview tool, the QR code will get greeked, so this is undesirable. Using U+25A0 allows a larger point size to be used which decreases the probability of the QR code being greeked; the only problem is that with U+25A0 spacing cannot be as exact.

It’s also worth noting that InDesign turns out to be incapable of handling UTF-8 in data files. Although you can specify Unicode, Macintosh platform (and Unicode on the Macintosh platform invariably means UTF-8), InDesign will silently ignore the file. You have to use little-endian UCS-2, which InDesign will interpret as “Unicode, PC platform” even if you don’t specify any options.

Lastly, with a programmatic way to generate QR codes, we also don’t need to worry about choosing a quality. We can just let our code search for an optimal quality. For example, my code (clearly not the best code as I noted in the comments) currently has the following:

sub qrcode ($) {
    # Compute the best QR code by sucessively going from the highest to lowest quality.
    # Stop when we get something of the correct size.
    # Don't bother returning an error value because only 1 line uses this function and that line uses its own error checking.
    my($s) = @_;
    my @qr = ();
    for my $quality (qw(H Q M L)) {
        @qr = get_qrcode($s, $quality);
    last if @qr == $qr_code_size;
    return @qr;

Just define some constraints (such as a fixed size for the dot pattern, $qr_code_size in my case) and let your code search for a quality that is adequate for your requirements.

Syndicate content