qrcode-rs is a Rust crate for generating QR Code and Micro QR Code symbols, rendering them in multiple output formats, and optionally decoding them back through rqrr.
It is designed to cover the common cases well out of the box while still exposing lower-level building blocks for advanced encoding workflows such as fixed versions, mode forcing, GS1/FNC1 payloads, structured payload helpers, and custom rendering.
serde, CLI support, async rendering, streaming, lightweight logging, and rqrr-based decoding via feature flags.no_std + alloc usage.CI checks the complete workspace with Rust 1.88 and locked dependencies. The
crate manifests omit explicit rust-version declarations; removing those
declarations does not lower the compiler requirements of the code or dependencies.
Use the default feature set if you want the most common renderers enabled:
[dependencies]
qrcode-rs = "2.0"
If you only need the core encoder and want to avoid the default rendering stack:
[dependencies]
qrcode-rs = { version = "2.0", default-features = false }
Enable only the pieces you need:
[dependencies]
qrcode-rs = { version = "2.1", default-features = false, features = ["std", "svg", "serde"] }
| Feature | Purpose |
|---|---|
default |
Enables std, image, svg, pic, eps, html, and pdf. |
std |
Links the standard library. Disable for no_std + alloc. |
image |
Raster image rendering, including PNG workflows. |
svg, pic, eps, html, pdf |
Individual renderer backends. |
serde |
Serialize / Deserialize support for core QR data types. |
template-json |
JSON load/export helpers for QrTemplate and QrTemplatePatch. |
log |
Emits encoder diagnostics through the log crate. |
deterministic |
Exposes explicit deterministic construction helpers for audit-sensitive call sites. |
parallel |
Enables Rayon-backed QrCode::par_batch for ordered parallel batch encoding. |
batch-zip-deflate |
Enables DEFLATE compression for library-level ZIP batch packaging. |
async |
Enables Tokio-backed async rendering helpers. |
cli |
Builds the qrencodes command-line tool. |
decode-rqrr |
Enables decoding through rqrr. |
decode-rxing |
Scans Normal/Micro QR images and retains Structured Append headers and candidate failures. |
compat-1x |
Keeps the 1.x facade API available during the 2.0 migration. |
qrcode-rs remains the recommended facade for applications. The 2.0 release
also publishes smaller crates for libraries that need a narrower dependency
surface:
| Crate | Use it when you need |
|---|---|
qrcode-core |
Core encoding types, module views, traits, and plugin contracts. |
qrcode-render |
Shared render traits, text/Unicode/ANSI helpers, and color utilities. |
qrcode-image |
Image-backed pixels, PNG/JPEG encoding, logo overlays, and gradients. |
qrcode-parse |
WiFi, vCard, and GS1 payload parsing without the facade. |
qrcode-decode |
Decoder traits, grayscale views, Structured Append parsing, and the optional rqrr adapter. |
qrcode-svg, qrcode-eps, qrcode-pic, qrcode-html, qrcode-pdf |
Individual renderer backends. |
qrcode-cli |
Split qrencodes workspace package used for release assets and CLI-focused builds. |
For example:
[dependencies]
qrcode-core = "2.0"
qrcode-svg = "2.0"
qrcode-compat is a workspace-local migration harness and is not published.
use image::Luma;
use qrcode_rs::QrCode;
fn main() {
let code = QrCode::new(b"https://example.com").unwrap();
let image = code
.render::<Luma<u8>>()
.min_dimensions(256, 256)
.build();
image.save("/tmp/qrcode.png").unwrap();
}
Generates:

use qrcode_rs::{EcLevel, QrCode};
use qrcode_rs::render::unicode;
fn main() {
let code = QrCode::builder("https://example.com")
.ec_level(EcLevel::H)
.build()
.unwrap();
let terminal = code
.render::<unicode::Dense1x2>()
.quiet_zone(false)
.build();
println!("{terminal}");
}
use qrcode_rs::{EcLevel, QrCode, Version};
use qrcode_rs::render::svg;
fn main() {
let code = QrCode::with_version(b"01234567", Version::Micro(2), EcLevel::L).unwrap();
let image = code
.render::<svg::Color>()
.min_dimensions(200, 200)
.dark_color(svg::Color("#800000"))
.light_color(svg::Color("#ffff80"))
.build();
println!("{image}");
}
Preview:
qrcode-rs includes convenience constructors for common payload types:
QrCode::for_url(...)QrCode::for_text(...)QrCode::for_wifi(ssid, password, auth)QrCode::for_vcard(name, phone, email)QrCode::for_gs1(...)QrCode::new_micro(...)QrCode::batch(inputs, ec_level)QrCode::batch_builder(inputs) with stdExample:
use qrcode_rs::QrCode;
fn main() {
let wifi = QrCode::for_wifi("GuestNetwork", "p\\;ss", "WPA").unwrap();
let contact = QrCode::for_vcard("Ada Lovelace", "+15551234", "ada@example.org").unwrap();
println!("wifi width = {}", wifi.width());
println!("contact width = {}", contact.width());
}
Applications can build named in-memory outputs directly from the library:
use qrcode_rs::QrCode;
let files = QrCode::batch_builder(["alpha", "beta"])
.file_extension("txt")
.render_bytes(|code, _| Ok::<_, qrcode_rs::QrError>(
code.render::<char>().quiet_zone(false).build().into_bytes()
))
.unwrap();
let zip_bytes = files.to_zip().unwrap();
assert!(zip_bytes.starts_with(b"PK\x03\x04"));
With image, BatchOutput<RgbaImage>::to_png_grid(...) builds a PNG contact
sheet; with batch-zip-deflate, to_zip_with(ZipCompression::Deflated) writes
compressed ZIP entries.
files.write_zip(&mut output) and write_zip_with write directly to a
std::io::Write output without allocating the complete archive. Use a buffered
output starting at offset zero. The caller owns flushing and publication;
an error may leave a partial archive in the output.
For lazy inputs, QrCode::batch_builder(inputs).render_zip(&mut output, render)
encodes and renders each input immediately into a Stored ZIP. The
render_zip_with variant selects compression. These methods keep only the
current symbol and payload plus central-directory metadata, and return
BatchZipError for encoding, rendering or packaging failures. They share the
same caller-controlled flushing and partial-output contract as write_zip.
The parse module can turn QR payload text back into typed domain objects:
use qrcode_rs::parse::wifi::WifiConfig;
fn main() {
let payload = r#"WIFI:T:WPA;S:GuestNetwork;P:p\;ss;H:false;;"#;
let cfg = WifiConfig::parse(payload).unwrap();
assert_eq!(cfg.ssid(), "GuestNetwork");
assert_eq!(cfg.security().as_str(), "WPA");
assert!(!cfg.hidden());
}
There are also parsers and examples for:
parse::vcard::VCardparse::gs1::Gs1ResultFor web and document workflows, the crate includes helpers to describe QR codes accessibly:
use qrcode_rs::QrCode;
use qrcode_rs::render::svg;
fn main() {
let payload = "https://example.com";
let code = QrCode::new(payload).unwrap();
let raw_svg = code.render::<svg::Color>().build();
let labeled_svg = svg::aria_label(&raw_svg, &QrCode::alt_text(payload));
println!("{labeled_svg}");
}
rqrrEnable decode-rqrr to bridge generated or external grayscale QR images back into data:
[dependencies]
qrcode-rs = { version = "2.0", features = ["decode-rqrr"] }
use image::Luma;
use qrcode_rs::decode::rqrr::RqrrDecoder;
use qrcode_rs::decode::{GrayPixels, QrDecoder};
use qrcode_rs::QrCode;
fn main() {
let payload = b"https://example.com/decode-bridge";
let code = QrCode::new(payload).unwrap();
let image: image::GrayImage = code.render::<Luma<u8>>().min_dimensions(200, 200).build();
let decoded = RqrrDecoder::new().decode(GrayPixels::from(&image)).unwrap();
assert_eq!(decoded[0].data(), payload);
}
Enable decode-rxing for the optional pure Rust image scanner. It borrows a
checked luma buffer and returns one Result per sampled candidate, retaining
independent failures alongside successful symbols. Payload bytes are not
transcoded; interpret UTF-8 explicitly when text is required.
The historical feature name selects a maintained private QR engine derived
from rxing 0.9.3. The new 2.2 scanner uses the project-owned
qrcode_decode::rxing::BackendError identity. Source provenance and bundled
Apache licensing are documented in the
decoder dependency review.
use qrcode_rs::decode::GrayPixels;
use qrcode_rs::decode::rxing::RxingDecoder;
fn scan_luma(width: u32, height: u32, pixels: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
let image = GrayPixels::try_new(width, height, pixels)?;
for candidate in RxingDecoder::new().scan(image)? {
let symbol = candidate?;
println!("{:?}: {:?}", symbol.decoded().version(), symbol.decoded().data());
if let Some(header) = symbol.structured_append() {
println!("fragment {}/{}", header.position(), header.total());
}
}
Ok(())
}
Use ScanOptions to configure pixel/candidate limits or explicit inverted
polarity. The default limits are 16,777,216 pixels, 32,768 pixels per side,
512 finder patterns, and 256 retained results. Limits return errors rather
than silently dropping candidates. QrDecoder::decode remains a strict
convenience interface, while scan exposes partial failures.
For Structured Append, select the intended fragments and pass their references
to structured_append::reassemble_decoded. It checks completeness, positions,
metadata and the recovered payload XOR. Eight-bit parity cannot uniquely
identify a message; colliding or duplicate groups must be resolved explicitly.
The existing decode-rqrr adapter remains available for normal QR codes.
Enable the cli feature to build the bundled qrencodes binary:
cargo install qrcode-rs --features cli
Basic usage:
qrencodes "https://example.com"
qrencodes -f svg -o out.svg "https://example.com"
qrencodes -f png -o out.png --size 12 --dark '#1a1a2e' --light '#f5f5dc' "Hello"
printf 'piped input' | qrencodes -f unicode
qrencodes --batch ./payloads.txt -f svg -o ./out
qrencodes --batch ./payloads.csv --batch-format csv --batch-column 2 --parallel -f png -o ./out
qrencodes --batch ./payloads.json --batch-format json --batch-key payload -f svg -o ./out
qrencodes --batch ./payloads.txt --batch-pack zip -f svg -o payloads.zip
qrencodes --batch ./payloads.txt --batch-pack grid --grid-columns 3 -f png -o payloads.png
qrencodes validate out.png --expect "Hello"
qrencodes validate out.png --print-payload
qrencodes decode out.png --format json
qrencodes decode out.png --format raw --output payload.bin
qrencodes decode first.png second.png --assemble
qrencodes decode inverted.png --invert
validate and decode scan normal, Micro, and Structured Append QR symbols.
validate retains its summary and lossy UTF-8 display. decode supports text,
JSON and raw bytes: text requires valid UTF-8, JSON preserves byte arrays and
metadata, and raw output requires exactly one logical payload. --assemble
explicitly reconstructs complete Structured Append groups. Missing, duplicate
or conflicting fragments fail before output is published. --allow-partial
permits failed image candidates while reporting them; unreadable images and
invalid assemblies still fail.
Inputs are regular files limited to 64 MiB encoded size and 32,768 pixels per
side, with a default 16,777,216-pixel budget adjustable by --max-pixels on
decode. Decoded and grayscale buffers have a combined 256 MiB budget; these
checks are not a total process-memory or execution-time guarantee. All inputs
and output representations are checked before stdout or atomic file writes.
Library callers can also render an encoded batch into stable, named in-memory outputs and decide how to package them:
use qrcode_rs::{EcLevel, QrCode};
let codes = QrCode::batch(["alpha", "beta"], EcLevel::M)?;
let rendered = QrCode::batch_render(&codes).extension("txt").build::<char>();
assert_eq!(rendered[0].name(), "qr-0001.txt");
# Ok::<(), qrcode_rs::QrError>(())
Supported output formats:
stringunicodeansisvgpngepspichtmlpdfSee the full help with:
cargo run --features cli -- --help
The examples/ directory covers the main workflows in this crate:
encode_image, encode_svg, encode_html, encode_eps, encode_pic, encode_stringencode_eci, encode_kanji, encode_fnc1, const_version, typed_encoding_mode, structured_appendparse_wifi, parse_vcard, parse_gs1, stream_codesplugin_plain_text, plugin_invert_modules, async_renderaccessible_svg, custom_colors, color_spaces, cmyk_print, batch_template, alt_textdecode_roundtrip, error_handlingcli_toolFor the 1.x to 2.0 upgrade path, see MIGRATION-1.x-to-2.0.md. The compat-1x feature keeps the legacy facade available while call sites move to the builder, module-view, streaming, and split-crate APIs. Plugin authors can follow PLUGIN_GUIDE.md.
The repository checks its locked dependency graph with cargo deny and
cargo vet in CI. The minimal feature is an explicit dependency-free
profile; SVG can be added without enabling raster/image dependencies:
cargo check --locked --no-default-features --features minimal
cargo check --locked --no-default-features --features minimal,svg
cargo tree --locked --no-default-features --features minimal --edges normal
Tagged releases include CycloneDX SBOM archives and Sigstore keyless signatures. Install cosign and verify an asset with the certificate emitted alongside it:
cosign verify-blob release-artifact \
--signature release-artifact.sig \
--certificate release-artifact.pem \
--certificate-identity-regexp '^https://github.com/houseme/qrcode-rs/.github/workflows/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
GitHub provenance attestations for crate assets can be verified with:
gh attestation verify qrcode-rs-2.2.0.crate --repo houseme/qrcode-rs
See SECURITY.md for supported versions and the private vulnerability-reporting process. The repository runs a scheduled RustSec dependency advisory check; the badge above reflects that workflow, not a security certification. The encoder does not provide encryption or authentication, so applications must validate untrusted payloads and protect generated files and logs. The current verification boundary and deferred security gates are summarized in docs/security-audit.md.
Licensed under either of:
at your option.
The derived QR engine in crates/qrcode-decode/src/engine is Apache-2.0 only;
its copyright notices, license and NOTICE remain bundled. Project-owned
source outside that directory retains the dual-license terms above.
See CONTRIBUTING.md for development and contribution guidelines, including the local property-test, differential-test, and fuzzing commands used by CI.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Thanks to Kennytm. This crate is based on qrcode-rust.