DEV Community

Cover image for I Hand-Rolled a QR Code Generator in Pure C#
Saheb Ansari
Saheb Ansari

Posted on

I Hand-Rolled a QR Code Generator in Pure C#

TerraPDF is a zero-dependency, MIT-licensed PDF library for .NET. Here's why "zero dependency" pushed me into implementing Reed-Solomon error correction and BCH codes by hand instead of just calling ZXing.

A few weeks ago I needed to put a QR code on a PDF invoice. Normal problem, normal solution: pull in a barcode library, call .Generate(), move on with your life.

Except the PDF library I was adding it to — TerraPDF, a project I maintain — has one rule that isn't allowed to bend: zero third-party dependencies. No native binaries, no NuGet packages, no "just add this one small transitive dependency." Pure, managed C#, all the way down.

So instead of dotnet add package ZXing.Net, I spent a weekend implementing Reed-Solomon error correction and BCH codes by hand. This post is about why that rule exists, and what it actually costs to hold the line on it.

The problem with "just add a PDF library" in .NET

If you've generated PDFs from .NET before, you've probably hit one of three walls:

  • Native binaries. A lot of the fast, good-looking PDF/rendering tooling in .NET wraps native code — Skia, Cairo, wkhtmltopdf-style headless browsers. That's fine until you're deploying to a slim Docker image, an AWS Lambda, or an Alpine container, and suddenly you're debugging libgdiplus not being found on a machine that's never heard of GDI+.
  • Licensing that changes under you. Several well-known .NET PDF libraries are AGPL, or ship a free tier that stops being free once your company crosses a revenue threshold. Reasonable business decisions, all of them — but not what you want to discover mid-project.
  • Low-level, verbose APIs. Plenty of libraries are technically free and technically pure-managed, but working with them means hand-placing text at X/Y coordinates like it's 2003.

TerraPDF's answer to all three is: pure C#, MIT-licensed, and a fluent API closer to modern UI frameworks than to raw PDF drawing operators.

using TerraPDF.Core;
using TerraPDF.Helpers;

Document.Create(container =>
{
    container.Page(page =>
    {
        page.Size(PageSize.A4);
        page.Margin(2, Unit.Centimetre);
        page.DefaultTextStyle(s => s.FontSize(11));

        page.Header().Text("My Report").Bold().FontSize(24).FontColor(Color.Blue.Darken2);

        page.Content().Column(col =>
        {
            col.Spacing(8);
            col.Item().Text("Hello, TerraPDF!");
            col.Item().Text("This paragraph is italic.").Italic();
            col.Item().Margin(6).Background(Color.Grey.Lighten4).Padding(10)
               .Text("Indented callout box").Bold();
        });

        page.Footer().AlignCenter().Text(t =>
        {
            t.Span("Page ");
            t.CurrentPageNumber().FontSize(9);
            t.Span(" / ");
            t.TotalPages().FontSize(9);
        });
    });
})
.PublishPdf("output.pdf");
Enter fullscreen mode Exit fullscreen mode

No native dependencies to install, no license tier to check, and it reads like the layout code you'd write for a UI, not like PostScript.

Which brings me back to the QR code

TerraPDF already had PNG/JPEG images, tables, rounded borders, vector graphics (lines, béziers, the works), AES-256 encryption, bookmarks, and a table-of-contents generator — all hand-rolled, all dependency-free. Barcodes and QR codes were the obvious next request. And "obvious" is where it stopped being easy.

Code128 barcodes turned out to be the friendly one: a fixed lookup table mapping characters to bar-width patterns, a checksum, done. A few hours of careful, no-room-for-typos table transcription.

QR codes are a different animal entirely. A QR code isn't just "black squares that mean something" — generating one from scratch means implementing:

  • Reed-Solomon error correction over GF(256) — the actual math that lets a QR code still scan with a coffee stain or a corner torn off
  • BCH-encoded format and version information — 15 and 18-bit self-correcting metadata blocks that tell a scanner which mask and error-correction level you used, using a whole separate error-correcting code just for the header
  • 8 different data masking patterns, each scored against 4 penalty rules (too many same-color runs, 2×2 blocks, finder-pattern lookalikes, imbalanced dark/light ratio) — you generate all 8 and keep whichever scores best
  • Version-dependent module placement — a zigzag data-writing algorithm that has to dodge finder patterns, timing patterns, and alignment patterns that themselves move around depending on which of the 40 possible QR "versions" you're encoding into

None of that is exotic — it's all public specification (ISO/IEC 18004) — but there's a real difference between "the spec is public" and "I trust my own transcription of a 160-row error-correction table enough to ship it." I ended up cross-referencing every table against independent public references before committing to it, specifically because a single wrong entry would silently break generation for one specific version-and-error-correction-level combination and nothing would tell you until a scanner failed in someone's hands.

The payoff is an API that looks exactly like everything else in TerraPDF:

container.Barcode("SKU-00042-A", showCaption: true);

container.QrCode("https://example.com/invoice/482",
    level: QrErrorCorrectionLevel.Q,
    hexColor: "#1A3C5E");
Enter fullscreen mode Exit fullscreen mode

Both render as vector-filled rectangles — not a rasterized image dropped onto the page — so they stay perfectly crisp whether someone views the PDF at 100% or zoomed to 800%. And because it's the same IContainer API as everything else, a QR code drops into a table cell, a header, a footer, next to a product row — anywhere.

The rest of what's in there

QR codes were the newest addition, but the library covers the ground you'd expect from a report/invoice/document generator:

  • Rich text — bold, italic, strikethrough, underline, per-span mixed formatting, custom line-height
  • Column, Row, and Table layouts with alignment, spacing, and per-edge borders
  • PNG/JPEG images (including transparency via /SMask), with automatic deduplication across pages
  • A vector graphics canvas — lines, rectangles, circles, béziers, grids — for building your own charts
  • AES-256 encryption by default (with an AES-128 opt-out for ancient viewers), fine-grained permission flags
  • Bookmarks/outlines, automatic table-of-contents generation from headings, clickable hyperlinks and internal links
  • Headers, footers, and page numbers that actually paginate correctly across page breaks

All of it targeting .NET 8 and .NET 9, all of it one dotnet add package TerraPDF away.

What's next

The big remaining item on the roadmap is proper TrueType font embedding with real Unicode shaping — right now text rendering covers the WinAnsi range well, but full Unicode (Arabic, CJK, complex script shaping) is the last major gap between "does the job for reports and invoices" and "does the job for everything."

Try it / poke holes in it

If you try the barcode/QR code feature, I'd love to know if it scans cleanly for you — it's been tested against the specification rigorously (Reed-Solomon syndrome checks, BCH validity, structural invariants, 380+ unit tests), but there's no substitute for a real phone camera pointed at a real printed page. Star the repo if it's useful, and tell me what breaks.

Top comments (0)