Neste projeto vamos construir um gerador de QR Code completo em Rust. QR Codes (Quick Response Codes) são códigos de barras bidimensionais capazes de armazenar texto, URLs, informações de contato e muito mais. Nosso gerador vai codificar dados de entrada e produzir saída em três formatos: renderização direta no terminal usando blocos Unicode, arquivos SVG vetoriais e imagens PNG rasterizadas.
Este projeto demonstra como utilizar crates do ecossistema Rust para resolver problemas práticos. Vamos trabalhar com a crate qrcode para a codificação do QR Code, a crate image para gerar imagens PNG e manipulação de strings para produzir SVG. Ao final, você terá uma ferramenta de linha de comando versátil e pronta para uso no dia a dia.
O Que Vamos Construir
Um gerador de QR Code com as seguintes funcionalidades:
- Codificação de texto e URLs em QR Codes
- Renderização no terminal com caracteres Unicode (blocos ██)
- Exportação para SVG (vetorial, escalável sem perda de qualidade)
- Exportação para PNG com tamanho configurável
- Seleção do nível de correção de erros (Low, Medium, Quartile, High)
- Interface de linha de comando com múltiplas opções de saída
- Suporte a margem (quiet zone) configurável
Estrutura do Projeto
qrcode-generator/
├── Cargo.toml
└── src/
└── main.rs
Configurando o Projeto
Crie o projeto com o Cargo:
cargo new qrcode-generator
cd qrcode-generator
Edite o Cargo.toml com as dependências necessárias:
[package]
name = "qrcode-generator"
version = "0.1.0"
edition = "2021"
[dependencies]
qrcode = "0.14"
image = "0.25"
A crate qrcode implementa a codificação QR Code segundo a norma ISO 18004. A crate image fornece funcionalidades para criar e salvar imagens em diversos formatos, incluindo PNG.
Passo 1: Gerando o QR Code e Renderizando no Terminal
Vamos começar com a parte mais visual: gerar um QR Code a partir de texto e exibi-lo diretamente no terminal usando caracteres Unicode. Usamos os caracteres de bloco (█, espaço e meios blocos) para representar os módulos preto e branco.
use image::{GrayImage, Luma};
use qrcode::render::unicode;
use qrcode::{EcLevel, QrCode, Version};
use std::env;
use std::fs::File;
use std::io::{self, Write};
/// Configuração do gerador de QR Code
struct Configuracao {
/// Dados a serem codificados (texto ou URL)
dados: String,
/// Formato de saída desejado
formato: FormatoSaida,
/// Nível de correção de erros
nivel_ec: EcLevel,
/// Tamanho de cada módulo em pixels (para PNG)
tamanho_modulo: u32,
/// Largura da margem em módulos (quiet zone)
margem: u32,
/// Caminho do arquivo de saída (quando aplicável)
caminho_saida: Option<String>,
}
/// Formatos de saída suportados
#[derive(Debug, Clone, PartialEq)]
enum FormatoSaida {
Terminal,
Svg,
Png,
}
/// Renderiza o QR Code no terminal usando caracteres Unicode
fn renderizar_terminal(codigo: &QrCode) {
// A crate qrcode tem suporte nativo para renderização Unicode
let texto = codigo
.render::<unicode::Dense1x2>()
.dark_color(unicode::Dense1x2::Light)
.light_color(unicode::Dense1x2::Dark)
.quiet_zone(true)
.build();
println!("{}", texto);
}
A renderização Unicode usa Dense1x2, que combina dois módulos verticais em um caractere Unicode (blocos superior e inferior), resultando em uma representação compacta. A inversão de cores (dark como Light e light como Dark) é necessária porque a maioria dos terminais tem fundo escuro, e o QR Code precisa de contraste correto para leitura.
Passo 2: Exportando para SVG
O formato SVG é ideal para QR Codes porque é vetorial – pode ser escalado para qualquer tamanho sem perda de qualidade. Vamos gerar o SVG manualmente construindo a string XML, o que nos dá controle total sobre o resultado.
/// Gera um arquivo SVG a partir do QR Code
fn gerar_svg(codigo: &QrCode, caminho: &str, tamanho_modulo: u32, margem: u32) -> io::Result<()> {
let matriz = codigo.to_colors();
let largura_qr = codigo.width() as u32;
// Dimensões totais do SVG incluindo margens
let tamanho_total = (largura_qr + margem * 2) * tamanho_modulo;
let mut svg = String::new();
// Cabeçalho SVG com namespace
svg.push_str(&format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="{t}" height="{t}" viewBox="0 0 {t} {t}">
"#,
t = tamanho_total
));
// Fundo branco
svg.push_str(&format!(
r#" <rect width="{}" height="{}" fill="white"/>
"#,
tamanho_total, tamanho_total
));
// Desenha cada módulo escuro como um retângulo preto
for y in 0..largura_qr {
for x in 0..largura_qr {
let indice = (y * largura_qr + x) as usize;
if matriz[indice] == qrcode::Color::Dark {
let px = (x + margem) * tamanho_modulo;
let py = (y + margem) * tamanho_modulo;
svg.push_str(&format!(
r#" <rect x="{}" y="{}" width="{}" height="{}" fill="black"/>
"#,
px, py, tamanho_modulo, tamanho_modulo
));
}
}
}
svg.push_str("</svg>\n");
// Escreve o arquivo SVG
let mut arquivo = File::create(caminho)?;
arquivo.write_all(svg.as_bytes())?;
println!("SVG salvo em '{}' ({}x{} pixels)", caminho, tamanho_total, tamanho_total);
Ok(())
}
Cada módulo escuro do QR Code se torna um elemento <rect> no SVG. A margem (quiet zone) é adicionada ao redor do código, conforme exigido pela especificação – sem essa margem, leitores de QR Code podem ter dificuldade em detectar as bordas do código. O tamanho_modulo controla a escala final: um módulo de 10 pixels em um QR de 25x25 módulos com margem 4 resulta em uma imagem de 330x330 pixels.
Passo 3: Exportando para PNG
Para a exportação PNG, usamos a crate image para criar uma imagem em escala de cinza e preencher os pixels correspondentes a cada módulo do QR Code.
/// Gera um arquivo PNG a partir do QR Code
fn gerar_png(
codigo: &QrCode,
caminho: &str,
tamanho_modulo: u32,
margem: u32,
) -> io::Result<()> {
let matriz = codigo.to_colors();
let largura_qr = codigo.width() as u32;
// Dimensões totais da imagem incluindo margens
let tamanho_total = (largura_qr + margem * 2) * tamanho_modulo;
// Cria a imagem em escala de cinza, toda branca
let mut imagem = GrayImage::from_pixel(tamanho_total, tamanho_total, Luma([255u8]));
// Preenche os módulos escuros com preto
for y in 0..largura_qr {
for x in 0..largura_qr {
let indice = (y * largura_qr + x) as usize;
if matriz[indice] == qrcode::Color::Dark {
// Preenche o bloco de pixels correspondente ao módulo
let px_inicio = (x + margem) * tamanho_modulo;
let py_inicio = (y + margem) * tamanho_modulo;
for dy in 0..tamanho_modulo {
for dx in 0..tamanho_modulo {
imagem.put_pixel(
px_inicio + dx,
py_inicio + dy,
Luma([0u8]), // Preto
);
}
}
}
}
}
// Salva a imagem como PNG
imagem.save(caminho).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Erro ao salvar PNG: {}", e),
)
})?;
println!(
"PNG salvo em '{}' ({}x{} pixels)",
caminho, tamanho_total, tamanho_total
);
Ok(())
}
/// Exibe informações sobre o QR Code gerado
fn exibir_info(codigo: &QrCode, dados: &str, nivel_ec: EcLevel) {
let versao = match codigo.version() {
Version::Normal(v) => format!("{}", v),
Version::Micro(v) => format!("M{}", v),
};
let nivel_str = match nivel_ec {
EcLevel::L => "Low (7%)",
EcLevel::M => "Medium (15%)",
EcLevel::Q => "Quartile (25%)",
EcLevel::H => "High (30%)",
};
println!("--- Informações do QR Code ---");
println!(" Dados: {} ({} caracteres)", truncar(dados, 50), dados.len());
println!(" Versão: {}", versao);
println!(" Módulos: {}x{}", codigo.width(), codigo.width());
println!(" Correção de erros: {}", nivel_str);
println!();
}
/// Trunca uma string adicionando reticências se exceder o limite
fn truncar(texto: &str, limite: usize) -> String {
if texto.len() <= limite {
texto.to_string()
} else {
format!("{}...", &texto[..limite])
}
}
A crate image lida com toda a complexidade da codificação PNG (compressão DEFLATE, chunks, CRC). Usamos GrayImage (escala de cinza) em vez de RGB porque o QR Code é monocromático, resultando em arquivos menores. Cada módulo do QR é escalado para um bloco de tamanho_modulo x tamanho_modulo pixels.
Passo 4: Interface de Linha de Comando e main
Agora juntamos todas as peças em uma interface de linha de comando completa que permite ao usuário escolher o formato de saída, o nível de correção de erros e outros parâmetros.
/// Analisa os argumentos da linha de comando
fn analisar_argumentos() -> Result<Configuracao, String> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
return Err(exibir_uso());
}
let mut dados = String::new();
let mut formato = FormatoSaida::Terminal;
let mut nivel_ec = EcLevel::M;
let mut tamanho_modulo = 10;
let mut margem = 4;
let mut caminho_saida = None;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--formato" | "-f" => {
i += 1;
if i >= args.len() {
return Err("Falta o valor para --formato".to_string());
}
formato = match args[i].as_str() {
"terminal" | "t" => FormatoSaida::Terminal,
"svg" | "s" => FormatoSaida::Svg,
"png" | "p" => FormatoSaida::Png,
outro => return Err(format!("Formato desconhecido: '{}'", outro)),
};
}
"--ec" | "-e" => {
i += 1;
if i >= args.len() {
return Err("Falta o valor para --ec".to_string());
}
nivel_ec = match args[i].as_str() {
"L" | "low" => EcLevel::L,
"M" | "medium" => EcLevel::M,
"Q" | "quartile" => EcLevel::Q,
"H" | "high" => EcLevel::H,
outro => return Err(format!("Nível EC desconhecido: '{}'", outro)),
};
}
"--tamanho" | "-t" => {
i += 1;
if i >= args.len() {
return Err("Falta o valor para --tamanho".to_string());
}
tamanho_modulo = args[i].parse::<u32>().map_err(|_| {
"O valor de --tamanho deve ser um número inteiro positivo".to_string()
})?;
}
"--margem" | "-m" => {
i += 1;
if i >= args.len() {
return Err("Falta o valor para --margem".to_string());
}
margem = args[i].parse::<u32>().map_err(|_| {
"O valor de --margem deve ser um número inteiro positivo".to_string()
})?;
}
"--saida" | "-o" => {
i += 1;
if i >= args.len() {
return Err("Falta o valor para --saida".to_string());
}
caminho_saida = Some(args[i].clone());
}
outro => {
if outro.starts_with('-') {
return Err(format!("Opção desconhecida: '{}'", outro));
}
dados = outro.to_string();
}
}
i += 1;
}
if dados.is_empty() {
return Err("Nenhum dado fornecido para codificar".to_string());
}
// Define caminho padrão de saída se necessário
if caminho_saida.is_none() && formato != FormatoSaida::Terminal {
caminho_saida = Some(match formato {
FormatoSaida::Svg => "qrcode.svg".to_string(),
FormatoSaida::Png => "qrcode.png".to_string(),
FormatoSaida::Terminal => unreachable!(),
});
}
Ok(Configuracao {
dados,
formato,
nivel_ec,
tamanho_modulo,
margem,
caminho_saida,
})
}
/// Exibe as instruções de uso e retorna a mensagem de erro
fn exibir_uso() -> String {
let uso = r#"Gerador de QR Code em Rust
Uso:
qrcode-generator <texto> [opções]
Opções:
-f, --formato <tipo> Formato de saída: terminal, svg, png (padrão: terminal)
-e, --ec <nível> Correção de erros: L, M, Q, H (padrão: M)
-t, --tamanho <pixels> Tamanho de cada módulo em pixels (padrão: 10)
-m, --margem <módulos> Largura da margem/quiet zone (padrão: 4)
-o, --saida <arquivo> Caminho do arquivo de saída
Exemplos:
qrcode-generator "Olá, mundo!"
qrcode-generator "https://rust-lang.org" -f png -o rust.png
qrcode-generator "Contato" -f svg -e H -t 8
qrcode-generator "dados" -f png -t 20 -m 2 -o grande.png"#;
eprintln!("{}", uso);
"Argumentos insuficientes".to_string()
}
fn main() {
let config = match analisar_argumentos() {
Ok(c) => c,
Err(e) => {
eprintln!("Erro: {}", e);
std::process::exit(1);
}
};
// Gera o QR Code
let codigo = match QrCode::with_error_correction_level(&config.dados, config.nivel_ec) {
Ok(c) => c,
Err(e) => {
eprintln!("Erro ao gerar QR Code: {}", e);
eprintln!("Os dados podem ser muito longos para o nível de correção escolhido.");
std::process::exit(1);
}
};
// Exibe informações sobre o QR Code gerado
exibir_info(&codigo, &config.dados, config.nivel_ec);
// Gera a saída no formato solicitado
let resultado = match config.formato {
FormatoSaida::Terminal => {
renderizar_terminal(&codigo);
Ok(())
}
FormatoSaida::Svg => {
let caminho = config.caminho_saida.as_deref().unwrap_or("qrcode.svg");
gerar_svg(&codigo, caminho, config.tamanho_modulo, config.margem)
}
FormatoSaida::Png => {
let caminho = config.caminho_saida.as_deref().unwrap_or("qrcode.png");
gerar_png(&codigo, caminho, config.tamanho_modulo, config.margem)
}
};
if let Err(e) = resultado {
eprintln!("Erro ao gerar saída: {}", e);
std::process::exit(1);
}
}
A interface de linha de comando suporta tanto opções curtas (-f) quanto longas (--formato). Os valores padrão são sensatos: correção de erros nível M (15% de redundância, bom equilíbrio), módulos de 10 pixels e margem de 4 módulos (o mínimo recomendado pela especificação). Se nenhum caminho de saída for especificado para SVG ou PNG, usa nomes padrão.
Como Executar
Compile o projeto:
cargo build --release
Exemplos de uso:
# QR Code no terminal (mais rápido para testar)
cargo run -- "Olá, Rust Brasil!"
# Saída esperada:
# --- Informações do QR Code ---
# Dados: Olá, Rust Brasil! (19 caracteres)
# Versão: 2
# Módulos: 25x25
# Correção de erros: Medium (15%)
#
# [QR Code renderizado com caracteres Unicode]
# Gerar PNG de uma URL
cargo run -- "https://www.rust-lang.org" -f png -o rust_lang.png
# Gerar SVG com alta correção de erros
cargo run -- "Texto importante" -f svg -e H -o importante.svg
# Gerar PNG grande com módulos de 20 pixels
cargo run -- "https://github.com" -f png -t 20 -o github.png
# QR Code com margem mínima
cargo run -- "compacto" -f png -m 1 -o compacto.png
Para verificar se o QR Code funciona, abra a imagem PNG ou SVG gerada e aponte a câmera do celular ou use um leitor de QR Code online.
Desafios para Expandir
QR Codes coloridos – Use a crate
imagecomRgbImagepara gerar QR Codes com cores personalizadas (módulos escuros em azul, fundo em amarelo claro, etc.) e adicione gradientes para um visual moderno.Logo no centro – Implemente a funcionalidade de inserir uma imagem (logo) no centro do QR Code, aproveitando a correção de erros nível H (30%) para garantir que o código continue legível mesmo com a área central ocultada.
Leitura de stdin – Adicione suporte para ler dados de
stdincomcat arquivo.txt | qrcode-generator -f png, permitindo integração com pipes e scripts shell.Geração em lote – Implemente um modo que lê uma lista de dados de um arquivo CSV e gera um QR Code para cada linha, salvando todos em um diretório com nomes sequenciais.
Servidor HTTP – Use a crate
axumouactix-webpara criar um endpoint que recebe texto via query parameter e retorna o QR Code como imagem PNG ou SVG diretamente no navegador.
Veja Também
- Trabalhando com Strings – Manipulação de texto para codificação
- Módulo IO – Leitura e escrita de arquivos
- Result e Tratamento de Erros – Propagação de erros entre as camadas
- Escrevendo Arquivos – Padrões para escrita de arquivos em Rust
- Formatação de Strings – Construção de strings SVG com format!