QR Codes Explained: From Factory Labels to Generation and Decoding in Code
Point a phone at a small square on a machine and its maintenance page opens. Scan another square and the phone offers to join a Wi-Fi network. The same visual format can carry a product identifier, a ticket, or a short message.
A QR code is a way to store data in a picture that software can locate, read, and recover even when some information is damaged. The application decides what to do with the recovered data.
This guide explains the original idea, the structure of the symbol, the encoding and decoding pipelines, and practical implementation choices. It includes generation examples in Python, JavaScript, and Go, a Python decoder, and libraries for other languages.
Where QR Codes Came From
QR stands for Quick Response. In 1994, a team led by Masahiro Hara at DENSO, whose QR business later became part of DENSO WAVE, introduced the code. Industrial users needed more information than their existing barcodes conveniently held, including Japanese characters, while keeping scanning fast.
The design used two dimensions for capacity and distinctive corner patterns for rapid detection. Automotive production adopted it for tracking and workflow management. DENSO WAVE’s decision not to exercise its patent rights over standardized QR codes helped wider adoption; international standardization followed in 2000. The company’s history of QR Code describes that progression.
The underlying idea remains useful: move a small piece of machine-readable information across a visual interface, without typing, a cable, or an already established network connection.
What Does a QR Code Actually Store?
A QR symbol contains encoded data. It does not inherently contain a website, a payment system, or a command to execute.
| Payload | Example | Possible scanner behavior |
|---|---|---|
| Plain text | Machine A17: inspect filter | Display the message |
| HTTPS URL | https://example.com/assets/A17 | Offer to open a browser |
| Asset identifier | ASSET:A17 | Let a custom application look up the asset |
| Wi-Fi configuration | WIFI:T:WPA;S:Workshop;P:demo-password;; | Offer to join a network |
| Contact data | A vCard record | Offer to create a contact |
| Application token | A short opaque ticket identifier | Submit it to an application for validation |
Wi-Fi and contact handling are payload conventions, separate from the QR symbol format. Support varies between scanners. The example Wi-Fi password is illustrative; special characters in real credentials need escaping according to the convention. See ZXing’s barcode content formats.
Decoding an image can happen offline. Opening its URL or validating a ticket against a server may require connectivity. A generic phone camera may show a custom payload as text even though your own application understands it.
Static and Dynamic QR Codes
A static QR code stores a fixed payload. Once printed, those bits do not change.
A service marketed as a dynamic QR code usually stores a fixed URL whose server-side destination can change:
Printed QR → https://example.com/q/A17 → redirect → current asset page
You update the redirect mapping, not the printed symbol. This supports changing destinations and recording redirect requests, but it adds a dependency on the domain and service. Request counts are not necessarily unique human scans: previews, retries, and automated requests can affect them.
For a product label, a short URL on a domain you control is often a practical design. A QR code has no inherent expiry date; a service, token, or destination may expire.
Inside the Black-and-White Square
This article focuses on QR Code Model 2, the familiar square format. Other QR-family formats have different structures and limits.
Each small square is a module. A module is a logical cell, not necessarily one screen pixel. When rendering, one module might occupy an 8 × 8 pixel block.
| Component | Purpose |
|---|---|
| Three finder patterns | Locate the symbol and establish its orientation |
| Separators | Keep the finder patterns visually distinct from adjacent content |
| Timing patterns | Alternating modules that help establish the grid spacing |
| Alignment patterns | Help account for distortion; present from version 2 onward |
| Format information | Records the error-correction level and selected mask |
| Version information | Explicit version metadata for versions 7 and above |
| Data region | Holds encoded payload and error-correction codewords |
| Quiet zone | Clear surrounding margin that separates the symbol from its background |
The positions and reserved regions are visible in Nayuki’s QR encoder implementation.
Model 2 versions range from 1 to 40. Version 1 is 21 × 21 modules; each increment adds four modules per side, ending at 177 × 177. The formula is:
modules per side = 17 + 4 × version
Capacity depends on version, encoding mode, and error correction. A larger version adds room but also needs more physical space to preserve module size. See DENSO WAVE’s version and capacity guide.
Allow at least four clear modules on every side for a standard QR symbol. A version 1 symbol rendered at eight pixels per module therefore occupies (21 + 4 + 4) × 8 = 232 pixels per side, including the margin. See DENSO WAVE’s code-area guidance.
How Encoding Turns Data into a QR Code
An encoder follows this pipeline:
Payload → segments → data bits → error-correction blocks
→ module placement → mask selection → rendered image
- Select modes. Numeric, alphanumeric, byte, and Kanji modes encode different character sets efficiently. Mixed content can use multiple segments.
- Choose a version. Find a symbol with enough capacity at the requested error-correction level.
- Build the bitstream. Add mode and length fields, content bits, a terminator, and padding as required.
- Add redundancy. Split codewords into blocks, calculate Reed–Solomon parity, and interleave the blocks.
- Place bits. Fill available modules in a prescribed two-column zigzag, starting at the lower right and skipping reserved patterns.
- Choose a mask. Evaluate eight patterns that flip selected data-region modules; choose one with a favorable visual penalty score.
- Write metadata and render. Store the mask and correction level, then draw the modules and quiet zone.
Masking reduces difficult visual patterns. It is reversible and is not encryption. Nayuki’s library provides readable implementations across six languages.
A Small Encoding Example
For a deliberately chosen byte-mode, version 1 encoding of Hi, the beginning of the bitstream is:
Mode: 0100 byte mode
Byte count: 00000010 2 bytes for versions 1–9
H (0x48): 01001000
i (0x69): 01101001
This is only the beginning: termination, padding, correction data, placement, and masking still follow. The mode and length parsing can also be inspected in ZXing’s bitstream decoder.
For international text, distinguish characters from bytes. UTF-8 characters can occupy multiple bytes. Extended Channel Interpretation (ECI) can identify a character encoding, but library and scanner support must be checked. Test your actual languages and emojis across intended readers rather than assuming every byte-mode payload is interpreted identically. The node-qrcode documentation describes its encoding support and limitations.
Error Correction: Why Damage Does Not Always Break a Scan
QR codes use Reed–Solomon error correction to reconstruct damaged codewords within the correction budget.
| Level | Approximate codeword restoration capability | Practical starting point |
|---|---|---|
| L | 7% | Clean, controlled presentation |
| M | 15% | General-purpose labels and screens |
| Q | 25% | Labels exposed to some wear |
| H | 30% | Greater redundancy where space permits |
These figures are approximate codeword-based capabilities, not permission to cover that percentage of the picture. Damage location, block distribution, and whether the scanner can still locate the symbol matter. A destroyed finder pattern can prevent decoding before error correction helps.
Higher correction consumes capacity and can increase symbol size. At a fixed printed size, that can make modules smaller and harder to capture. Choose the level together with payload length and physical dimensions. See DENSO WAVE’s error-correction explanation.
How a Scanner Decodes the Image
A typical camera-based reader performs two related jobs: finding a usable module grid and recovering its data.
Camera image → locate symbol → correct perspective → sample grid
→ read metadata → unmask → recover codewords
→ correct errors → parse payload → application action
The image-processing stage distinguishes dark and light regions, finds candidate corner patterns, estimates orientation and perspective, and samples module centers. Blur, reflections, low resolution, or a cropped margin can prevent a reliable grid from being formed. OpenCV exposes detection and decoding together or separately through QRCodeDetector.
With a sampled grid, the decoder reads format/version information, reverses the mask, follows the placement order, and separates the interleaved blocks. It applies Reed–Solomon correction and parses the reconstructed data segments. ZXing’s decoder implementation shows the block correction and payload-decoding stages.
The application then interprets the result. Successfully recovering a URL does not establish that its destination is trustworthy. Likewise, recovering a ticket token does not establish that the ticket is valid or unused.
Generate and Decode a QR Code with Python
Create an isolated environment and install the generator and decoder dependencies:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install "qrcode[pil]" opencv-python-headless
Save this as generate_qr.py:
import qrcode
payload = "https://example.com/assets/A17"
qr = qrcode.QRCode(
version=None,
error_correction=qrcode.constants.ERROR_CORRECT_M,
box_size=8,
border=4,
)
qr.add_data(payload)
qr.make(fit=True)
qr.make_image(fill_color="black", back_color="white").save("asset-qr.png")
print(f"Generated version {qr.version}: {payload}")
version=None with fit=True lets the library choose the size. box_size is pixels per module; border is measured in modules. These options come from python-qrcode.
Save this as decode_qr.py:
import cv2
image = cv2.imread("asset-qr.png")
if image is None:
raise SystemExit("Could not load asset-qr.png")
text, points, straight = cv2.QRCodeDetector().detectAndDecode(image)
if points is None:
raise SystemExit("No QR code detected")
if not text:
raise SystemExit("A QR code was detected, but no nonempty text was decoded")
print(text)
assert text == "https://example.com/assets/A17"
Run the round trip:
python generate_qr.py
python decode_qr.py
The decoder prints the original URL. This example handles one text payload; it does not open the destination. For multiple symbols, OpenCV provides detectAndDecodeMulti. For arbitrary binary content, choose an API that exposes raw bytes rather than assuming a text result is sufficient.
Generate a QR Code with JavaScript and Node.js
Install the encoder in your project:
npm install qrcode
Save as generate-qr.cjs:
const QRCode = require('qrcode');
async function main() {
const payload = 'https://example.com/assets/A17';
const options = { errorCorrectionLevel: 'M', margin: 4, scale: 8 };
await QRCode.toFile('asset-qr.png', payload, options);
await QRCode.toFile('asset-qr.svg', payload, { ...options, type: 'svg' });
console.log('Saved PNG and SVG QR codes');
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
node generate-qr.cjs
PNG suits raster displays; SVG scales cleanly for print layouts. node-qrcode also supports browser canvas rendering.
For browser decoding, jsQR accepts RGBA pixel data, such as ImageData obtained from a canvas:
// In a bundled browser application after: npm install jsqr
import jsQR from 'jsqr';
export function decodeCanvas(canvas) {
const context = canvas.getContext('2d');
if (!context) throw new Error('A 2D canvas context is required');
const frame = context.getImageData(0, 0, canvas.width, canvas.height);
const result = jsQR(frame.data, frame.width, frame.height);
return result ? result.data : null;
}
Draw the image or video frame onto the canvas first. Camera capture and permission handling are separate application work. Cross-origin images also need appropriate CORS access before their pixels can be read.
Generate a QR Code with Go
In a new example directory:
go mod init example.com/qr-demo
go get github.com/skip2/go-qrcode
Save as main.go:
package main
import (
"log"
"github.com/skip2/go-qrcode"
)
func main() {
err := qrcode.WriteFile(
"https://example.com/assets/A17",
qrcode.Medium,
256,
"asset-qr.png",
)
if err != nil {
log.Fatal(err)
}
}
go run .
The size argument requests the output image dimension in pixels. go-qrcode is an encoder; choose a separate reader when your application also scans images.
QR Libraries for Different Languages
Generation and decoding are separate capabilities. A library that creates a module matrix may still require your own PNG, SVG, or display renderer.
| Language or environment | Library | Useful capability |
|---|---|---|
| Python | python-qrcode | Generate PNG and SVG symbols |
| Python / C++ | OpenCV QRCodeDetector | Detect and decode images in a vision pipeline |
| JavaScript / TypeScript | node-qrcode | Generate images and browser canvas output |
| JavaScript / TypeScript | jsQR | Decode a symbol from RGBA image data |
| C / C++ | Nayuki QR Code generator | Generate module matrices, including for embedded rendering |
| C++ | ZXing-C++ | Read and write multiple barcode formats |
| Java / Android | ZXing | Encode and decode QR and other barcode formats |
| Go | go-qrcode | Generate QR images |
| C# / .NET | QRCoder | Generate QR codes with several rendering options |
| Rust | qrcode | Encode QR data and render through supported output paths |
Choose based on required formats, raw-byte access, Unicode behavior, license, platform dependencies, and project maintenance. Pin dependencies for a shipped application. A barcode library’s decoding API is not necessarily a complete camera interface.
Embedded Implementation Considerations
On a small device, generating a QR matrix and drawing it on a display can be much simpler than decoding a camera image. With Nayuki’s C implementation, the application supplies buffers and renders the resulting modules; the library does not require an image-file workflow. Its C source and examples are useful starting points.
For a setup screen, encode a short device identifier or onboarding URL. Reserve enough display area for complete modules and the quiet zone. Camera decoding adds frame-buffer, image-processing, and compute requirements, so check those against the target hardware early.
Practical Use Cases and Design Choices
| Use case | Payload choice | What the application must handle |
|---|---|---|
| Asset maintenance | Stable asset URL or identifier | Access control and current maintenance records |
| Device onboarding | Device identity plus a short-lived pairing token | Device binding, expiry, and pairing confirmation |
| Event entry | Opaque ticket token | Validity and atomic redemption to prevent reuse |
| Offline inspection | Compact, versioned record | Schema validation and authenticity if needed |
| Guest Wi-Fi | Wi-Fi configuration string | Reader compatibility and credential rotation |
| Menus and documentation | Stable HTTPS URL | Long-lived hosting and readable mobile content |
| Inventory | Item, batch, or serial identifier | Integration with stock and manufacturing records |
A QR label is easy to copy. For tickets, checking that the QR text has the right shape is insufficient: the system must decide whether that token is authorized and still usable. Offline signatures can establish who issued a payload, but they do not by themselves prevent copying or solve synchronized redemption.
For an embedded product, treat scanning as one step in the overall workflow: scan, identify, authenticate when needed, confirm, and recover from failure. The broader systems engineering guide explains how to define and validate such end-to-end behavior.
Make the Printed Code Easy to Scan
Start with a short payload, dark modules on a plain light background, and a complete quiet zone. Render raster output at an integer number of pixels per module and avoid resizing that blurs module edges. Use vector output when the print workflow supports it.
Do not assume maximum error correction compensates for tiny printing, glare, or a logo covering important patterns. Decide the physical size using the intended camera, distance, lighting, and print process.
Before shipping a label or screen:
- Decode the generated file and compare the exact result with the intended payload.
- Scan the final printed or displayed version with representative readers.
- Check realistic distance, angle, lighting, surface reflections, and wear.
- Test international text and special characters if they are part of your payload.
- Exercise missing connectivity, unavailable destinations, and invalid or expired tokens.
| Symptom | Things to check |
|---|---|
| No symbol detected | Quiet zone, contrast, focus, module size, cropped finder patterns |
| Detected but not decoded | Blur, damage, excessive density, perspective, reader limitations |
| Correct symbol but unexpected text | Character encoding, ECI support, byte-versus-text handling |
| URL decodes but page fails | Domain, connectivity, redirect, and server availability |
| Looks good on screen but fails on paper | Actual printed dimensions, ink spread, finish, and scaling |
A good first implementation is a short payload generated by an established library, decoded by an independent reader, and tested on the actual surface where people will scan it. That closes the loop between the bits you intended to encode and the action the user expects.