Zig 0.17.0

QR codes

spider.qrcode turns a text, usually a link, into a QR code: a ticket to scan at the door, a payment, a page to open on a phone. It gives the squares; the app draws them.

The code on this page is from examples/guide (source): a small server kept as tests, for what the example apps do not use.

Turning it on

In build.zig, where the Spider dependency is declared, next to the other parts a project asks for by name:

zig
const std = @import("std");

// Builds nothing to run: `zig build test` is all there is. See README.md.
pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const spider_dep = b.dependency("spider", .{
        .target = target,
        .optimize = optimize,
        // The parts of Spider an app asks for by name.
        .xlsx = true,
        .qrcode = true,
        .r2 = true,
    });
    const spider_mod = spider_dep.module("spider");
    spider_mod.addImport("spider_config", b.createModule(.{
        .root_source_file = b.path("spider.config.zig"),
        .imports = &.{.{ .name = "spider", .module = spider_mod }},
    }));

    const tests = b.addTest(.{ .root_module = b.createModule(.{
        .root_source_file = b.path("src/tests.zig"),
        .target = target,
        .optimize = optimize,
        .imports = &.{.{ .name = "spider", .module = spider_mod }},
    }) });
    b.step("test", "Run the examples").dependOn(&b.addRunArtifact(tests).step);

    // Signing in at Keycloak needs a Keycloak: a step of its own, run by
    // login-test.sh, which starts one for the occasion.
    const login_tests = b.addTest(.{ .root_module = b.createModule(.{
        .root_source_file = b.path("login/tests.zig"),
        .target = target,
        .optimize = optimize,
        .imports = &.{.{ .name = "spider", .module = spider_mod }},
    }) });
    b.step("test-login", "Run the login examples (needs login/docker-compose.yml up)").dependOn(&b.addRunArtifact(login_tests).step);
}

A code as SVG

zig
/// A ticket's link as a QR code, drawn as SVG.
fn ticketQr(c: *spider.Ctx) !spider.Response {
    const code = c.params.get("code") orelse return error.NotFound;
    const link = try std.fmt.allocPrint(c.arena, "https://guide.example/tickets/{s}", .{code});

    const qr = try spider.qrcode.QR.encodeAuto(c.arena, link, .medium);

    // A scanner needs a light margin around the code: four modules.
    const margin = 4;
    const side = qr.size + 2 * margin;
    var svg: std.ArrayList(u8) = .empty;
    try svg.print(c.arena,
        \\<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {d} {d}" shape-rendering="crispEdges">
        \\<rect width="{d}" height="{d}" fill="#fff"/>
        \\
    , .{ side, side, side, side });
    for (0..qr.size) |y| {
        for (0..qr.size) |x| {
            if (!qr.getModule(@intCast(x), @intCast(y))) continue;
            try svg.print(c.arena, "<rect x=\"{d}\" y=\"{d}\" width=\"1\" height=\"1\"/>", .{ x + margin, y + margin });
        }
    }
    try svg.appendSlice(c.arena, "</svg>\n");

    return c.download(svg.items, .{
        .filename = "ticket.svg",
        .content_type = "image/svg+xml",
        .disposition = .@"inline",
    });
}
  • QR.encodeAuto(allocator, text, level) picks the smallest code the text fits in.
  • qr.size is how many squares (modules) a side has, and qr.getModule(x, y) is true for a dark one.
  • The level is how much of a damaged or covered code a scanner can still read: .low (about 7%), .medium (15%), .quartile (25%), .high (30%). A higher level makes a larger code for the same text. .medium suits a screen; go higher for print that gets folded or stained.
  • The light margin around the code is part of it: without four modules of it, many scanners do not find the code.

SVG has no size of its own, so the page decides it:

html
<img src="/tickets/A7K2/qr.svg" alt="Ticket A7K2" width="240" height="240">

The code this handler draws was read back with a scanner: it gives https://guide.example/tickets/A7K2.

zig
test "qrcode: a link becomes a square of modules, drawn as SVG" {
    const app = try spider.testing.start(run);
    var res = try app.get("/tickets/A7K2/qr.svg");
    defer res.deinit();
    try res.expectStatus(200);
    try res.expectHeader("Content-Type", "image/svg+xml");
    // 33 characters at the medium level fit version 3: 29 modules a side,
    // plus the margin of 4 on each.
    try res.expectContains("viewBox=\"0 0 37 37\"");
    // The corner a scanner looks for starts at the margin.
    try res.expectContains("<rect x=\"4\" y=\"4\" width=\"1\" height=\"1\"/>");
}

How much fits

The more text, the more squares, and the harder the code is to scan from a distance: keep links short. A text that does not fit even the largest code is error.DataTooLarge:

zig
test "qrcode: the size follows the text, and a text too long is an error" {
    const a = std.testing.allocator;
    var short = try spider.qrcode.QR.encodeAuto(a, "HELLO", .medium);
    defer short.deinit();
    try std.testing.expectEqual(@as(u16, 21), short.size);

    var longer = try spider.qrcode.QR.encodeAuto(a, "https://guide.example/tickets/A7K2?from=mail&lang=en", .medium);
    defer longer.deinit();
    try std.testing.expect(longer.size > short.size);

    const huge: [4000]u8 = @splat('x');
    try std.testing.expectError(error.DataTooLarge, spider.qrcode.QR.encodeAuto(a, &huge, .high));
}

QR.encode(allocator, text, version, level) takes the size instead of picking it: a version from 1 (21 modules a side) to 40 (177), for when every code of a series has to be the same size.

What goes in a code

Anyone can read a QR code, and anyone can make one. Put a link or an id in it, not a secret, and check on the server what the id is allowed to do, as for any address a visitor can type.