Ever needed to generate a QR code without pulling in a massive framework? Let’s build a simple CLI tool in C that takes a URL and spits out a scannable QR code right in your terminal. Pure C, no fluff. 🎯
The Library
We’re using qrcodegen by Project Nayuki. It’s a clean, dependency-free C library (MIT licensed) that handles all the QR encoding magic. Just grab qrcodegen.c and qrcodegen.h and drop them in your project folder.
The library supports:
- All QR Code versions (1-40)
- All 4 error correction levels (L/M/Q/H)
- Automatic encoding mode selection (numeric, alphanumeric, byte)
The Includes
1
2
3
4
| #include <stdio.h>
#include <stdlib.h>
#include <regex.h>
#include "qrcodegen.h"
|
We need regex.h for input validation (POSIX regex) and qrcodegen.h for the QR magic.
First, let’s make sure we get exactly one URL as input and that it looks legit:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| int main(int argc, char *argv[])
{
int sizeOfArgv = 0;
for (int i = 1; i < argc; i++) {
printf("%s\n", argv[i]);
sizeOfArgv++;
}
char *inputUrl = argv[1];
regex_t regex;
int ret;
// Regular expression pattern for basic URL validation:
// ^(https?://) -> URL must start with "http://" or "https://"
// ([a-zA-Z0-9.-]+) -> Domain name with letters, digits, dots, or hyphens
// (:[0-9]+)? -> Optional port number
// (/[a-zA-Z0-9./?=&%-]*)?$ -> Optional path and query parameters
char *pattern = "^(https?://)([a-zA-Z0-9.-]+)(:[0-9]+)?(/[a-zA-Z0-9./?=&%-]*)?$";
// Compile the regular expression with extended syntax
ret = regcomp(®ex, pattern, REG_EXTENDED);
if (ret) {
printf("Could not compile regex\n");
return EXIT_FAILURE;
}
// Execute the regular expression on the URL string
ret = regexec(®ex, inputUrl, 0, NULL, 0);
if (sizeOfArgv > 1 || ret != 0) {
printf("Please only provide a single URL as an input.");
return EXIT_FAILURE;
}
// Free the compiled regular expression
regfree(®ex);
|
The regex is basic but catches the essentials: protocol (http:// or https://), domain, optional port, and path. We also enforce a single argument - no batch mode shenanigans here.
Generating the QR Code
Here’s where qrcodegen does its thing. We need two buffers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Buffers for QR code generation
uint8_t qrcode[qrcodegen_BUFFER_LEN_MAX];
uint8_t tempBuffer[qrcodegen_BUFFER_LEN_MAX];
bool ok = qrcodegen_encodeText(
inputUrl,
tempBuffer,
qrcode,
qrcodegen_Ecc_MEDIUM, // Error correction level
qrcodegen_VERSION_MIN, // Min version (size)
qrcodegen_VERSION_MAX, // Max version
qrcodegen_Mask_AUTO, // Let it pick the best mask
true // Boost ECC if possible
);
if (!ok) {
printf("Failed to generate QR code\n");
return EXIT_FAILURE;
}
|
A few things happening here:
qrcodegen_BUFFER_LEN_MAX - Allocates ~4KB per buffer. Enough for the largest possible QR code (version 40).qrcodegen_Ecc_MEDIUM - ~15% error correction. Good balance between size and reliability. You can also use LOW (7%), QUARTILE (25%), or HIGH (30%).qrcodegen_Mask_AUTO - The library evaluates all 8 mask patterns and picks the one with the lowest “penalty score” (best scannability).boostEcl = true - If there’s room to spare, bump up the error correction level for free. Nice.
Printing to Terminal
Now the fun part - rendering it with Unicode block characters:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| int size = qrcodegen_getSize(qrcode);
// Print with a quiet zone (border)
for (int y = -2; y < size + 2; y++) {
for (int x = -2; x < size + 2; x++) {
if (qrcodegen_getModule(qrcode, x, y)) {
printf("██"); // Dark module
} else {
printf(" "); // Light module
}
}
printf("\n");
}
return EXIT_SUCCESS;
}
|
qrcodegen_getSize() returns the QR code’s side length (21-177 modules depending on version)qrcodegen_getModule(x, y) returns true for dark modules, false for light ones. Out-of-bounds coordinates return false (light), which is perfect for our quiet zone.- The quiet zone (the
-2 / +2 border) is required by the QR spec - scanners need that white space to detect the code boundaries.
We print ██ (two full blocks) for dark modules and two spaces for light ones. This keeps the aspect ratio square-ish in most terminal fonts.
Compile and Run
1
2
| gcc -o qrcode qrcode.c qrcodegen.c
./qrcode "https://example.com"
|
And boom 💥 - a scannable QR code right in your terminal. Try it with your phone’s camera!
The Complete Code
Here’s everything in one place:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
| #include <stdio.h>
#include <stdlib.h>
#include "qrcodegen.h"
#include <regex.h>
int main(int argc, char *argv[])
{
int sizeOfArgv = 0;
for (int i = 1; i < argc; i++) {
printf("%s\n", argv[i]);
sizeOfArgv++;
}
char *inputUrl = argv[1];
regex_t regex;
int ret;
char *pattern = "^(https?://)([a-zA-Z0-9.-]+)(:[0-9]+)?(/[a-zA-Z0-9./?=&%-]*)?$";
ret = regcomp(®ex, pattern, REG_EXTENDED);
if (ret) {
printf("Could not compile regex\n");
return EXIT_FAILURE;
}
ret = regexec(®ex, inputUrl, 0, NULL, 0);
if (sizeOfArgv > 1 || ret != 0) {
printf("Please only provide a single URL as an input.");
return EXIT_FAILURE;
}
regfree(®ex);
uint8_t qrcode[qrcodegen_BUFFER_LEN_MAX];
uint8_t tempBuffer[qrcodegen_BUFFER_LEN_MAX];
bool ok = qrcodegen_encodeText(
inputUrl,
tempBuffer,
qrcode,
qrcodegen_Ecc_MEDIUM,
qrcodegen_VERSION_MIN,
qrcodegen_VERSION_MAX,
qrcodegen_Mask_AUTO,
true
);
if (!ok) {
printf("Failed to generate QR code\n");
return EXIT_FAILURE;
}
int size = qrcodegen_getSize(qrcode);
for (int y = -2; y < size + 2; y++) {
for (int x = -2; x < size + 2; x++) {
if (qrcodegen_getModule(qrcode, x, y)) {
printf("██");
} else {
printf(" ");
}
}
printf("\n");
}
return EXIT_SUCCESS;
}
|
What’s Next?
This is just the basics. You could extend it to:
- Output to PNG/SVG instead of terminal (combine with libpng or just write raw PPM)
- Add ANSI color codes for a fancier terminal output
- Support other content types (WiFi credentials, vCards, etc.)
The qrcodegen library handles all the heavy lifting - Reed-Solomon error correction, mask pattern optimization, version selection. We just feed it text and it handles the rest automagically. And I love it. 🖤