Gerador de QR Code em Rust

Construa um gerador de QR Code em Rust com saída para terminal Unicode, SVG e PNG usando as crates qrcode e image para codificar texto e URLs.

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

  1. QR Codes coloridos – Use a crate image com RgbImage para gerar QR Codes com cores personalizadas (módulos escuros em azul, fundo em amarelo claro, etc.) e adicione gradientes para um visual moderno.

  2. 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.

  3. Leitura de stdin – Adicione suporte para ler dados de stdin com cat arquivo.txt | qrcode-generator -f png, permitindo integração com pipes e scripts shell.

  4. 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.

  5. Servidor HTTP – Use a crate axum ou actix-web para 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