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:
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
/// 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.sizeis how many squares (modules) a side has, andqr.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..mediumsuits 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:
<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.
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:
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.