Bean & Bark: the bag tells its story

View .md

The orders are flowing and the reports are honest. Now the physical product: at Thursday's tasting, a buyer picks up a bag of single-origin and asks "where's this from?" Nadia wants the answer on the bag — a QR that opens the roast batch's provenance page. Easy enough to generate a QR. The hard part, the part that quietly ruins a print run, is making sure it actually scans — and that's the part Kanject.Core.Qr refuses to let you get wrong.

You'll learn
  • Declare a bag label as a [QrTemplate] whose payload is the batch's provenance URL
  • Understand why a centre logo needs error-correction level H
  • Watch the scannability verifier reject an on-brand label that's too faint to scan (KANQR052)
  • Serve a verified label PNG straight from an API endpoint — no System.Drawing, no native binaries

The label is a template

A bag label is just a payload plus styling. Mark a partial class with [QrTemplate], and the source generator emits the payload builder and render helpers — no reflection, AOT-clean, so it drops straight into a Lambda with no native image library in the bundle:

csharp
using Kanject.Core.Qr.Attributes;using Kanject.Core.Qr.Enums;// Each roast batch gets a bag label. The template is the provenance URL a// buyer's phone opens; {BatchId} is URL-escaped into it. Ecc = H because a// centre logo will occlude modules — H recovers ~30% of the symbol, the// headroom a logo eats into.[QrTemplate("https://beanandbark.coffee/batch/{BatchId}", Ecc = QrErrorCorrection.H)]public sealed partial class BagLabel{    public required string BatchId { get; init; }}

The {BatchId} placeholder binds to the property and is URL-escaped on the way in. Ecc = QrErrorCorrection.H sets error correction to its highest level — and that isn't arbitrary. A QR carries redundant data so a scanner can recover a damaged symbol; a centre logo is damage, deliberately. Level H recovers roughly 30% of the symbol, which is the headroom a logo occupies. Register the encoder and renderers once at startup with services.AddQrCodes() and you're ready.

The label that looked perfect and didn't scan

The brand kit says the QR should be "latte" — a warm tan — on cream. It looks beautiful. Turn on VerifyBeforeReturn and try to render it:

csharp
// The brand palette: "latte" modules on cream. It looks gorgeous in Figma.var brandStyle = new QrRenderOptions{    DarkColor  = "#b08d57",   // latte    LightColor = "#f4ecd8",   // cream    ModuleShape = QrModuleShape.Rounded,    Logo = barkLogo,    VerifyBeforeReturn = true,};var matrix = encoder.Encode(label.ToQrPayload(),    new QrEncodeOptions { ErrorCorrection = QrErrorCorrection.H });var png = pngRenderer.Render(matrix, brandStyle);   // 💥 throws

It throws. The verifier runs pure arithmetic over the matrix and the colours — luminance of the light colour minus the dark — and this pairing doesn't clear the threshold a real scanner needs. A tan-on-cream code is a Rorschach test to a phone camera in daylight:

csharp
try{    var png = pngRenderer.Render(matrix, brandStyle);}catch (QrUnscannableException ex) when (ex.Code == "KANQR052"){    // ex.Message:    //   [KANQR052] Foreground/background contrast is too low; dark modules    //   must be substantially darker than the background.    //    // The label WOULD print. A phone at a market stall, in daylight, on a    // matte kraft bag, would not read it. The verifier caught it here — at    // build time on a designer's laptop — instead of at the tasting.}

The fix keeps the brand — cream stays, the logo stays, the rounded modules stay — and swaps the tan for espresso. Same coffee family, real contrast:

csharp
// Keep the cream. Swap the modules to espresso — same brand family, real// contrast. Nothing else changes.var labelStyle = new QrRenderOptions{    DarkColor  = "#2b1d0e",   // espresso    LightColor = "#f4ecd8",   // cream    ModuleShape = QrModuleShape.Rounded,    EyeStyle = new QrFinderEyeStyle { Color = "#2b1d0e", Rounded = true },    Logo = barkLogo,    VerifyBeforeReturn = true,};var png = pngRenderer.Render(matrix, labelStyle);   // ✓ verified, scannable

The output, live

This is the actual bag-label QR that Kanject.Core.Qr produced for the fixed styling above — rendered by the library, verified, and checked into these docs verbatim:

Version 5, 37×37 modules, error correction H — espresso on cream, verified scannable. Scan a real bag and it opens that batch's provenance page.

One endpoint per batch

The roastery prints labels from a little service — one endpoint that turns a batch id into a verified PNG. Because the generated ToQrPng verifies by default, an unscannable label can never leave this endpoint quietly:

csharp
// The label service: one endpoint per batch, returning a verified PNG.// ToQrPng defaults its render options to QrRenderOptions.VerifiedDefault,// so passing labelStyle (VerifyBeforeReturn = true) keeps every served// label checked — a batch id that somehow produced an unscannable symbol// would 500 loudly here, never ship a dead bag.labels.MapGet("/batch/{batchId}/label.png",    (string batchId, IQrEncoder enc, IQrPngRenderer png) =>    {        var bytes = new BagLabel { BatchId = batchId }            .ToQrPng(enc, png, renderOptions: labelStyle);        return Results.File(bytes, "image/png");    });
Recap
  • A [QrTemplate] on a partial class generates the payload builder and ToQrPayload/ToQrSvg/ToQrPng helpers; the generated render helpers verify by default (QrRenderOptions.VerifiedDefault).
  • A centre logo is deliberate damage — use Ecc = QrErrorCorrection.H for the ~30% recovery headroom it needs.
  • With VerifyBeforeReturn = true, a too-faint colour pairing throws QrUnscannableException with Code == "KANQR052" — caught at build time, not at the market stall.
  • The verifier is pure arithmetic on the matrix + colours: contrast (KANQR052), logo error budget (KANQR051), reserved patterns (KANQR053) — no camera, no decode loop.
  • It's BCL-only and AOT-clean, so a label endpoint ships in a Lambda with no System.Drawing and no native image binaries.
Check yourself2
Why does the bag label use error-correction level H specifically?
The "latte on cream" label render throws QrUnscannableException with code KANQR052. What would happen if you removed VerifyBeforeReturn = true and rendered the same colours?
Try it yourself 2
1 Make the verifier talk
Before shipping a new palette, you want to know why it passes or fails without throwing. Call the verifier directly and read its verdict.
Inject IQrScannabilityVerifier and call Verify(matrix, options). It returns a result, not an exception.
Show solution
verifier.Verify(matrix, options) returns a QrScannabilityResult with IsScannable, and — when it fails — Code (e.g. "KANQR052") and Reason. That's the same check VerifyBeforeReturn runs, but as data you can log or surface in a palette-picker UI instead of a thrown exception.
csharp
var verdict = verifier.Verify(matrix, brandStyle);if (!verdict.IsScannable)    logger.LogWarning("Label rejected: {Code} — {Reason}", verdict.Code, verdict.Reason);
2 Predict the SVG twin
The print shop wants crisp vector labels, not raster PNGs. Without changing the styling or the verification, how do you hand them an SVG of the same label?
There's a sibling generated method to ToQrPng. It takes the SVG renderer instead.
Show solution
Call label.ToQrSvg(enc, svgRenderer, renderOptions: labelStyle) — it returns the SVG string, runs the same scannability verification (the helpers verify by default), and takes the identical QrRenderOptions. One template, one styling, both output formats — the SVG scales to any bag size without pixelation.
Was this page helpful?