11 — Renderização (Estratégia e Tabela de Decisão)¶
A renderização do boleto é tratada por backends plugáveis atrás de uma interface única. O domínio (cálculo de linha digitável, código de barras, PIX, valores) nunca depende do backend; o backend apenas recebe dados já validados e produz o PDF. Isso permite trocar/coexistir motores de renderização sem tocar nas regras bancárias.
Modelos avaliados¶
| Modelo | Como funciona | Dependências |
|---|---|---|
| ReportLab | Desenho programático em Python puro, posicionamento absoluto (pt a pt). | Nenhuma dependência de sistema (Python puro). |
| HTML/CSS + WeasyPrint | Template Jinja2 → HTML/CSS print → PDF. |
Bibliotecas nativas (Pango, cairo, GDK-PixBuf). |
| HTML/CSS + Playwright | Template Jinja2 → HTML → Chromium headless imprime PDF. | Navegador Chromium no ambiente. |
Tabela de decisão¶
Notas de 1 (pior) a 5 (melhor) nos quatro critérios pedidos. A linha Dependências de sistema não é um dos quatro critérios, mas é a restrição operacional decisiva (quanto maior a nota, menos dependências) — detalhada mais abaixo.
| Critério | ReportLab | HTML/CSS + WeasyPrint | HTML/CSS + Playwright |
|---|---|---|---|
| Simples de Implementar | 3 — API de baixo nível, verbosa, mas com farto histórico em geração de boletos. | 4 — HTML/CSS + Jinja2 é familiar e produtivo. | 2 — exige orquestrar navegador headless e seu ciclo de vida. |
| Melhorias Visuais | 2 — posicionamento absoluto; tema e logo já suportados, mas ajustes finos de layout custam caro. | 4 — CSS torna temas, cores e fontes simples. | 5 — CSS moderno completo, máxima fidelidade de navegador. |
| Fácil de Manutenção | 2 — mudança visual mexe em coordenadas; frágil a ajustes. | 4 — layout declarativo, fácil de evoluir. | 4 — layout declarativo, porém com peso operacional do Chromium. |
| Velocidade de Renderizar | 5 — Python puro, sem navegador; baixo uso de CPU/memória, ideal para alto volume. | 3 — mais lento que ReportLab; suporta subconjunto de CSS. | 2 — navegador é pesado; maior latência e consumo de memória. |
| Média dos 4 critérios | 3,00 | 3,75 | 3,25 |
| Dependências de sistema | 5 — nenhuma | 2 — libs nativas | 1 — Chromium |
Leitura por perfil de uso (média ponderada)¶
Nenhum modelo domina os quatro critérios — a escolha depende do perfil do deployment. Abaixo, duas ponderações típicas (soma dos pesos = 100%).
Perfil A — boleto padrão em produção (alto volume, serverless/Docker leve, layout regulado): pesos Velocidade 30%, Manutenção 25%, Visual 20%, Implementar 15%, Dependências 10%.
| Modelo | Cálculo | Total |
|---|---|---|
| ReportLab | 5·0,30 + 2·0,25 + 2·0,20 + 3·0,15 + 5·0,10 | 3,35 |
| WeasyPrint | 3·0,30 + 4·0,25 + 4·0,20 + 4·0,15 + 2·0,10 | 3,50 |
| Playwright | 2·0,30 + 4·0,25 + 5·0,20 + 2·0,15 + 1·0,10 | 3,00 |
Perfil B — boleto white-label (marca/temas frequentes, prévia web, volume moderado): pesos Visual 30%, Manutenção 25%, Implementar 20%, Velocidade 15%, Dependências 10%.
| Modelo | Cálculo | Total |
|---|---|---|
| ReportLab | 2·0,30 + 2·0,25 + 3·0,20 + 5·0,15 + 5·0,10 | 2,95 |
| WeasyPrint | 4·0,30 + 4·0,25 + 4·0,20 + 3·0,15 + 2·0,10 | 3,65 |
| Playwright | 5·0,30 + 4·0,25 + 2·0,20 + 2·0,15 + 1·0,10 | 3,30 |
A restrição decisiva: dependências de sistema¶
Para rodar em ambientes constrangidos (ex.: 512 MB no Render) e manter a imagem Docker leve, o gerador de PDF precisa ser puro Python, sem bibliotecas de sistema. Isso aponta para o ReportLab (zero dependências de sistema) — e não WeasyPrint, que traz Pango/cairo.
Essa é a contrapartida do WeasyPrint: ele traz Pango/cairo. Não muda o padrão do projeto (a prioridade é visual + manutenção — ver "Decisão do projeto" abaixo), mas é o motivo pelo qual o ReportLab permanece como alternativa de alto volume/serverless, a um passo de config quando o ambiente for muito constrangido.
Decisão do projeto — REVISADA com paridade visual comprovada + benchmark¶
A decisão inicial (WeasyPrint padrão) apoiava-se na vantagem visual/manutenção do HTML/CSS. Dois fatos supervenientes mudaram o veredito:
- Paridade visual comprovada. O backend ReportLab (
modelo="moderno") foi validado lado a lado contra imagens de referência e reproduz o layout por completo — chips, célula PIX teal, paleta cinza, carnê e tema (faixa de marca, marca d'água, rodapé). A vantagem visual do WeasyPrint deixou de existir para os layouts do projeto. - Benchmark em lote (100–200 boletos, mesmos contextos, mesma máquina):
| Lote | ReportLab | WeasyPrint | Fator |
|---|---|---|---|
| 100 | 1,2 s (11,9 ms/boleto) | 132,1 s (1.321 ms/boleto) | ~111× |
| 200 | 2,1 s (10,7 ms/boleto) | 265,2 s (1.326 ms/boleto) | ~124× |
| Tamanho do PDF | 7,8 KB | 23,7 KB | 3× |
Para emissão em lote (100–200 no fluxo assíncrono do doc 12), o WeasyPrint levaria ~4,4 minutos num lote de 200 — enquanto o ReportLab entrega em ~2 segundos, viabilizando até resposta síncrona.
Padrão promovido a 1º:
ReportLabBackend(modelo="moderno"). Visual idêntico à referência, ~120× mais rápido, PDFs 3× menores e zero dependências de sistema.
Backend único (decisão final)¶
Render somente pelo ReportLab (decisão de projeto): a via HTML/CSS + WeasyPrint foi removida do escopo — os templates Jinja2 saíram do pacote. A análise comparativa acima permanece como registro histórico da decisão. Se um dia for necessário white-label além dos modelos atuais, um backend HTML poderá ser reavaliado em projeto separado.
Implementação (backend ReportLab)¶
O backend único vive no pacote pycobranca/render/ e é alimentado pelo
domínio via BancoBase.contexto_render(). O pacote é dividido por responsabilidade:
| Módulo | Papel |
|---|---|
comum.py |
constantes, paleta e primitivas (canvas, texto, código de barras, QR, logo) |
tela.py |
a Tela — canvas + cursor + coordenadas, texto e a célula rotulada |
dados.py |
DadosBoleto/extrai_dados — o preenchimento dos dados do boleto |
blocos.py |
blocos comuns aos modelos (rótulo, demonstrativo, corte) |
modelos/ |
catálogo dos documentos: boleto_classico, boleto_moderno, carne, fatura |
Para adicionar um documento novo, basta criar um módulo em modelos/ — as camadas de baixo são
compartilhadas (ver o contrato de modelo na docstring de modelos/__init__.py).
from datetime import date
from pycobranca.bancos import Bancos
from pycobranca.render import render_boleto_pdf, render_carne_pdf
Banco = Bancos.find("341")
boleto = Banco(
valor="127.50",
cedente="Empresa Exemplo LTDA",
cedente_documento="11.222.333/0001-81",
agencia="0057",
conta="12345",
carteira="109",
nosso_numero="12345678",
data_vencimento=date(2026, 8, 15),
sacado="Cliente Final da Silva",
sacado_documento="529.982.247-25",
)
pdf = render_boleto_pdf(boleto.contexto_render(), modelo="moderno")
modelo="moderno"(padrão recomendado): recibo com chips, célula PIX e TEMA.modelo="classico": layout tradicional.render_carne_pdf({"parcelas": [...]}): carnê 3×A4.render_fatura_pdf(contexto): fatura — corpo livre + boleto na mesma página (ver abaixo).desenha_boleto(canvas, contexto, modelo): desenha o boleto num canvas existente, para compor o boleto dentro de outro documento (é o que a fatura usa).
Fatura — corpo livre em 3 níveis¶
A fatura desenha um corpo no topo e o boleto logo abaixo. O corpo tem três níveis de liberdade, do mais simples ao mais aberto — todos em Python puro, sem engine de HTML.
Por que não HTML? Renderizar HTML/CSS fielmente exige uma engine de layout (WeasyPrint carrega Pango via FFI; wkhtmltopdf é um binário). Isso reintroduziria dependências de sistema e contrariaria a decisão de escopo do projeto. Quem precisa de HTML/CSS completo usa os dados (
contexto_render()/to_dict()) e renderiza na engine que preferir — a engine segue leve.
Nível 1 — itens (tabela pronta)¶
contexto["itens"] = [
{"descricao": "Mensalidade — agosto/2026", "quantidade": 1, "valor": 99.90},
{"descricao": "Serviço adicional", "quantidade": 2, "valor_unitario": 13.80},
]
Quando só há valor, ele é o total da linha; com valor_unitario, o total é
quantidade × valor_unitario. O total da fatura é somado automaticamente.
Nível 2 — fatura.blocos (corpo declarativo)¶
Serve a qualquer modalidade (mensalidade, condomínio, consumo, escola, serviços):
contexto["fatura"] = {
"titulo": "FATURA DE CONSUMO",
"blocos": [
{"tipo": "campos", "itens": [("Período", "01/08 a 31/08"), ("Contrato", "4471")]},
{
"tipo": "tabela",
"colunas": ["Descrição", "Qtd.", "Unitário", "Total"],
"larguras": [110, 18, 28, 34],
"alinhamento": "lrrr",
"linhas": [["Consumo de água (m³)", "18", "3,50", "63,00"]],
},
{"tipo": "texto", "conteudo": "Leitura em <b>18/08/2026</b>."},
{"tipo": "separador"},
{"tipo": "total", "rotulo": "Total da fatura", "valor": 127.50},
],
}
| Bloco | Campos |
|---|---|
campos |
itens (lista de (rótulo, valor)), colunas (por linha, padrão 3) |
tabela |
colunas, linhas, larguras (mm), alinhamento (l/r por coluna) |
texto |
conteudo, tamanho |
total |
rotulo, valor |
separador |
— |
espaco |
altura (mm) |
O bloco texto aceita a marcação inline do ReportLab (<b>, <i>, <font color="#...">,
<br/>) — mini-HTML sem dependência nova.
Nível 3 — fatura.desenhar (liberdade total)¶
def minha_arte(tela, info):
tela.texto(tela.x_(0), tela.y_() - 6 * tela.mm, "ARTE LIVRE", fonte="Helvetica-Bold", tam=16)
tela.avanca(12)
contexto["fatura"] = {"desenhar": minha_arte}
O callable recebe a Tela e os dados preenchidos, desenha o que
quiser, e o boleto é composto abaixo.
Precedência: desenhar > blocos > itens. Sem nenhum dos três, a saída é o boleto puro.
Contrato REST: os níveis 1 e 2 são serializáveis —
BoletoDataganhouitens(ItemFatura) efatura(FaturaCorpocomBlocoFatura) emcontrato_rest.json, então um serviço HTTP pode expô-los. O nível 3 é umcallablePython — não atravessa REST por natureza. - Código de barras desenhado vetorialmente a partir dos 44 dígitos (sequencia_i2of5); QR do Bolepix viapix.qrcode_matrix(matriz de módulos 0/1). - Requisitos de validação: leitura do código de barras/QR no PDF gerado, regressão visual contra as imagens de referência e benchmark de lote (100/1.000/10.000).
Logo opt-in no cabeçalho¶
O cabeçalho aceita um logo opcional, fornecido pelo chamador. Quando presente, ele é desenhado no lugar do nome do banco em texto (recibo, ficha e ambos os lados do carnê), preservando a proporção. É um mecanismo: a biblioteca desenha o asset entregue e não embute marcas registradas de bancos — a origem e a licença do arquivo são responsabilidade de quem emite.
# via domínio: o logo flui para banco.logo no contexto
logo = Path("marca_empresa.png").read_bytes() # bytes de PNG/JPEG…
boleto = Banco(..., logo=logo) # …ou um caminho de arquivo (str)
pdf = render_boleto_pdf(boleto.contexto_render(), modelo="moderno")
# ou direto no contexto de render
ctx = boleto.contexto_render()
ctx["banco"]["logo"] = "marca_empresa.png"
pdf = render_boleto_pdf(ctx)
Fontes aceitas: bytes de PNG/JPEG, caminho de arquivo (str/os.PathLike) ou um
reportlab.lib.utils.ImageReader. Sem logo (None), o cabeçalho mostra o nome do banco —
comportamento padrão inalterado (saída byte a byte idêntica).
Logos de bancos empacotados¶
Como conveniência, a biblioteca inclui logos de 17 bancos (nomeados pelo código FEBRABAN) em
pycobranca/render/logos/, expostos por logo_do_banco:
from pycobranca.render import logo_do_banco, bancos_com_logo
bancos_com_logo() # ('001', '004', '033', '041', '085', '104', '136', '237', '336', '341', '748', '756')
ctx["banco"]["logo"] = logo_do_banco("237") # bytes do PNG, ou None se não houver
Marcas registradas. Os logos pertencem aos respectivos bancos e servem apenas para identificar o emissor — uso nominativo. Atribuição e origem em
logos/NOTICE.md. Use um logo somente quando tiver o direito de exibir a marca (cobrança legítima pela instituição). Para marca própria/white-label, forneça o seu arquivo embanco.logo.