report-to-pdf — Relatório Markdown para PDF polido
Renderizador determinístico de Markdown para PDF, controlado por script. Ele analisa um subconjunto restrito de Markdown de "relatório" com um pequeno analisador de blocos feito à mão (nenhuma biblioteca Markdown externa) e o distribui através do fpdf2 com as mesmas convenções visuais do gerador build-user-guide.py do módulo ColeteOnline Magento — página de título, cabeçalho/rodapé, sumário (TOC), cabeçalhos de seção com barra de destaque, blocos de código sombreados, tabelas com bordas e listras zebradas. A lógica de layout nunca muda entre execuções na mesma entrada: mesmo relatório de entrada, mesmo PDF de saída, todas as vezes — sem LLM no loop de renderização.
Quando usar
- "Transforme esta revisão em markdown em um PDF para o mentor."
- "Exporte meu relatório como um PDF polido."
- "Quero que isso tenha sempre a mesma aparência, não reformatado por um LLM a cada execução."
- Qualquer relatório/escrita
.mdque tenha cabeçalhos, listas, blocos de código ou tabelas e precise de uma versão em PDF de qualidade para cliente/mentor/marca.
Como executar
python3 <diretório-da-skill>/scripts/build_report_pdf.py <entrada.md> [opções]
(<diretório-da-skill> é o diretório base desta skill, a pasta que contém este SKILL.md.)
Opções:
-o / --output <caminho>— caminho do PDF de saída (padrão:<entrada>.pdfao lado do arquivo de origem).--logo <caminho>— imagem de logotipo opcional (PNG/WebP/JPEG, qualquer coisa que o Pillow possa abrir) na página de título. Repita a flag para vários logotipos, desenhados lado a lado em uma linha (também configurável via frontmatterlogo: a.png, b.png, caminhos relativos ao arquivo.md).--title,--subtitle,--version,--vendor,--author— substituem os valores correspondentes do frontmatter (veja abaixo); as flags da CLI vencem.--cover center|left— layout da página de capa. Todo o bloco de título (logotipo, título, subtítulo, autor, versão) é centralizado verticalmente em ambos os modos;center(padrão) também o centraliza horizontalmente,lefto alinha à esquerda. Também configurável via frontmattercover:.--no-header— omite o cabeçalho corrente e o espaço que ele reserva, para que o conteúdo de cada página comece na mesma margem superior (também configurável via frontmatterheader: false).--accent "R,G,B"— substitui a cor de destaque usada para a barra superior da página de título e as barras de destaque dos cabeçalhos de seção (padrão220,50,50, vermelho ColeteOnline).--dark— renderiza com o tema de cores escuro em vez do tema claro padrão (também configurável via frontmattertheme: dark;--darkvence se ambos estiverem presentes). Veja "Tema escuro" abaixo.
O script instala automaticamente fpdf2, Pillow e Pygments via pip install --user se alguma importação falhar, espelhando a auto-instalação do Pillow do script de referência. Todos os três foram verificados como importáveis e o script foi executado de ponta a ponta durante o desenvolvimento (veja Testes abaixo).
Convenções Markdown que ele espera
Bloco de frontmatter opcional no topo do arquivo:
---
title: Título do Meu Relatório
subtitle: Um subtítulo de uma linha
version: 1.0
vendor: ColeteOnline SRL
accent: 220,50,50
---
Todas as chaves são opcionais e têm fallback sensato:
title— recai para o primeiro cabeçalho# H1encontrado no corpo, depois para o nome do arquivo de entrada.subtitle— recai para vazio (omitido da página de título).version— recai para a data de hoje (YYYY-MM-DD); definaversion: nonepara ocultar a linha de versão.vendor— recai para vazio (linha de copyright omitida do rodapé da página de título se não definido).author— nome(s) do(s) autor(es), mostrado(s) na capa abaixo do subtítulo (ex.:author: João Silva, Maria Souza). Omitido se não definido.cover—center(padrão) ouleft; veja--coveracima.header—falseomite o cabeçalho corrente; veja--no-header.justify—truejustifica o texto do corpo, cabeçalhos e células de tabela; o padrão é alinhado à esquerda (texto justificado estica os espaços entre palavras em linhas curtas quebradas, ex.: um cabeçalho de duas linhas).accent— recai para o vermelho da marca ColeteOnline220,50,50.
Markdown do corpo e como cada elemento é renderizado:
| Markdown | Renderiza como |
|---|---|
# Título | O primeiro # no arquivo é o título da página de título apenas (consumido, não repetido no corpo). Qualquer # posterior é um cabeçalho principal real (veja abaixo). |
# Seção Principal (após o primeiro #) | Cabeçalho principal. Sempre começa sua própria página nova (a menos que o cursor já esteja no topo de uma página em branco), grande barra colorida à esquerda, negrito 16pt, texto preto — e ganha uma entrada no Sumário. Use apenas para divisões genuínas de nível superior; cada uma custa uma página. |
## Subseção | Subcabeçalho. Negrito 13pt, texto na cor de destaque com um sublinhado curto na cor de destaque (visualmente distinto dos dois níveis acima e abaixo) — não força uma quebra de página, apenas continua o fluxo. Ainda ganha uma entrada no Sumário. Tem sua própria verificação de "não deixar um cabeçalho preso no final de uma página". |
### Sub-subseção (e ####+) | Cabeçalho terciário. Negrito 12pt, cinza neutro, linha fina de largura total, sem quebra de página, sem entrada no Sumário (mantém o Sumário escaneável). |
| Texto de parágrafo | Parágrafo de corpo justificado à esquerda, 10pt. |
- item / * item | Lista com marcadores (•). |
1. item | Lista numerada. |
```linguagem ... ``` | Bloco de código monoespaçado sombreado e com borda; a tag de linguagem (se presente) é impressa como uma pequena etiqueta em itálico acima do bloco e aciona o destaque de sintaxe Pygments real — veja abaixo. |
| ` | a |
--- / *** / ___ em sua própria linha | Espaçador vertical (não uma quebra de página). |
<br> em sua própria linha | Uma linha de corpo em branco explícita (5,5mm). Empilhe várias para mais espaço. O renderizador não adiciona espaços próprios no topo de uma página, então use isso quando quiser espaço extra. |
<!-- pagebreak --> em sua própria linha | Iniciar uma nova página (sem efeito se já estiver no topo de uma). |
 em sua própria linha | Imagem centralizada, com borda, redimensionada para caber; caminhos relativos são resolvidos em relação à pasta do arquivo .md. PNG/JPEG/WebP e SVG (veja "Figuras, caixas e matemática"). Uma imagem raster nua com menos de 900 px de largura é limitada a ~130x105mm; uma figura (imagem seguida por uma legenda), qualquer SVG e qualquer imagem com 900 px ou mais de largura pode usar a largura total do texto (altura máxima 120mm). Uma figura que não cabe no espaço restante na página é reduzida para caber (até 70% de sua altura) em vez de pular para a próxima página e deixar um espaço. |
*Figura: ...* (uma linha inteira em itálico logo após uma imagem) | A legenda da imagem: pequena, centralizada, em itálico, mantida na mesma página que a imagem. O texto alternativo nunca é impresso. |
> texto | Caixa de definição: uma caixa com borda e levemente sombreada por blockquote (termo em negrito e math inline funcionam dentro; uma linha > sem nada após inicia um novo parágrafo na mesma caixa). |
$...$ | Math inline, convertido para texto Unicode (letras gregas, ≤ ≥ × ·, sub/sobrescritos, \frac{a}{b} -> (a)/b, \sqrt, \text{}, acentos). Um preço como $5 não é math. |
$$...$$ (uma linha, ou em suas próprias linhas) | Math de exibição, uma imagem centralizada do primeiro backend de math que funcionar (veja "Figuras, caixas e matemática"). |
| Bloco indentado com 4 espaços após uma linha em branco | Bloco de código sem tag de linguagem (exportações de notebook imprimem a saída de célula dessa forma). Não aplicado logo após uma lista. |
**negrito** | Texto em negrito real, através do renderizador markdown=True embutido do fpdf2. |
[texto](url) | Uma anotação de link PDF real e clicável — apenas o texto de exibição é mostrado, não a URL bruta — estilizado em uma cor de link distinta. |
*itálico* / _itálico_ | Itálico renderizado (convertido internamente para a forma nativa __italic__ do fpdf2). |
`código` | As crases de código inline são removidas — sem suporte a monoespaçado inline, renderiza como texto simples. |
Cabeçalhos, parágrafos, marcadores, itens numerados e células de tabela são todos renderizados através do caminho markdown=True do fpdf2, então negrito e links [texto](url) funcionam em todo lugar onde o texto aparece, incluindo dentro de cabeçalhos e células de tabela. Blocos de código com cerca são a única exceção — seu conteúdo é renderizado completamente literalmente (sem análise de markdown), então uma amostra de código contendo **, __ ou []() literais não será mal interpretada.
Figuras, caixas e matemática
Escrito para que o companheiro markdown de um notebook (saída do nbconvert) renderize diretamente: report-to-pdf notebook.md substitui uma etapa separada de PDF baseada em Chrome.
Imagens SVG.  funciona. O desenho SVG próprio do fpdf2 pula pontas de seta <marker>, filtros e máscaras, desenha texto em uma fonte principal (sem grego) e colore mal o texto sem fill, então todo SVG é rasterizado a 2,5x e incorporado como PNG, usando, nesta ordem: um Chrome/Edge headless (encontrado automaticamente; defina REPORT_PDF_BROWSER para o caminho de um navegador para substituir), depois cairosvg se importável. Com nenhum dos dois, o SVG é desenhado nativamente pelo fpdf2 e um aviso diz que pode estar incompleto. A rasterização é armazenada em cache por execução.
Backends de math de exibição (--math auto|latex|mathtext|unicode, ou frontmatter math:; padrão auto). O script imprime qual backend foi usado.
latex: LaTeX real quandopdflatex,xelatexoutectonicestá no PATH (maispdftocairo, PyMuPDF oudvisvgmpara fazer a imagem), oulatex+dvipng. Cada fórmula é compilada comstandalone,amsmath,amssymbexcolora 12 pt, então cortada para um PNG transparente de 300 dpi na cor do texto. Instalação: MiKTeX (Windows), TeX Live (apt install texlive-latex-base texlive-latex-extra poppler-utils, macOSbrew install --cask mactex-no-gui), outectonic(binário único, baixa pacotes sob demanda;conda install -c conda-forge tectonicouscoop install tectonic; testado de ponta a ponta no Windows com PyMuPDF como conversor de imagem).mathtext: subconjunto LaTeX do matplotlib (frações, somas, grego,\text{}é mapeado para\mathrm). Nenhum TeX necessário, apenas matplotlib. Ambientes comoalignednão são suportados e falham.unicode: a mesma conversão de texto que o math inline, centralizado.
Um backend que está faltando ou falha (por exemplo, uma fórmula que o TeX rejeita) cai silenciosamente para o próximo; --math latex ainda degrada da mesma forma.
Teste: python tests/run_checks.py (conversão de math, analisador, roteamento SVG, fallback de backend com PATH simulado, construção de ponta a ponta de tests/fixture.md). Renderize um companheiro: python scripts/build_report_pdf.py notebook.md -o notebook.pdf.
Destaque de sintaxe
Blocos de código com cerca são tokenizados com lexers Pygments reais (não um realçador de regex feito à mão — código é exatamente o tipo de coisa que um pequeno analisador ad hoc erra sutilmente: escapes de string, comentários aninhados, literais de template). A tag de linguagem na cerca (```typescript, ```sql, ```python, ...) seleciona o lexer; um punhado de aliases comuns são normalizados (ts/ tsx → typescript, js/jsx → javascript, sh/shell/zsh → bash, yml → yaml, prisma → graphql — o lexer embutido mais próximo e uma boa correspondência na prática, já que a sintaxe @attribute(...) do Prisma e a sintaxe @directive(...) do GraphQL tokenizam quase de forma idêntica).
O destaque é um bônus, nunca um requisito difícil para renderizar um bloco de código — uma tag de linguagem ausente, uma linguagem que o Pygments não reconhece (mesmo após a tabela de aliases), ou o próprio Pygments não sendo importável, todos recaem para o comportamento exato anterior ao destaque: o bloco inteiro em uma cor plana, sem falha, sem conteúdo ausente.
Paleta de cores (escolhida para legibilidade contra o fundo cinza claro do bloco de código, não vinculada a nenhum tema Pygments específico):
| Categoria de token | Cor |
|---|---|
Palavras-chave (interface, async, const, NOT NULL, palavras-chave de tipo como string/number) | Magenta |
| Strings | Verde |
| Números | Marrom/laranja |
| Comentários | Cinza |
Nomes de função/classe, decoradores/atributos (@map, @relation) | Azul / marrom |
Builtins (Promise, etc.) | Verde-azulado |
| Todo o resto (identificadores, pontuação, operadores) | A cor base do código |
Um tipo de token sem uma entrada exata na paleta resolve percorrendo a hierarquia de tipos de token do Pygments (ex.: Keyword.Declaration não tem entrada própria, mas seu pai Keyword tem) até que uma correspondência seja encontrada — a raiz Token nua sempre corresponde, então isso nunca falha em resolver para alguma cor.
A lógica de ajuste de tamanho monoespaçado (uma linha de código muito larga para a página reduz o tamanho da fonte de todo o bloco uniformemente em vez de transbordar) não é afetada pelo destaque — ela mede larguras de caracteres simples, que não mudam com base na cor em que um token é desenhado.
Tema escuro
Passe --dark, ou defina theme: dark no frontmatter, para renderizar o documento inteiro em um tema escuro em vez do tema claro padrão. Cada cor no documento muda para uma contraparte apropriada para o escuro — não apenas os blocos de código:
- O fundo da página em si é preenchido escuro (fpdf2 não tem configuração nativa de "cor de fundo da página", então isso é feito preenchendo a página inteira no hook
header(), antes que qualquer outra coisa desenhe sobre ele — incluindo na página de título). - O texto do corpo, cabeçalhos (todos os três níveis), texto de rodapé/atenuado, linhas de régua, listras zebradas de tabela e o cabeçalho/rodapé corrente usam um conjunto separado de cores do modo escuro, não apenas um claro invertido.
- O destaque de sintaxe usa sua própria paleta de modo escuro (rosas/verdes/azuis mais brilhantes escolhidos para contraste contra o fundo escuro do código), não as cores do modo claro escurecidas.
- A cor do link (tanto links markdown
[texto](url)quanto os links de salto para página do Sumário) muda para um azul mais claro que permanece legível em um fundo escuro.
A cor --accent (a barra superior, barra esquerda do cabeçalho principal e texto/sublinhado do subcabeçalho) é usada como está em ambos os temas — escolha uma que funcione contra um fundo branco e um quase preto se você planeja gerar o mesmo relatório em ambos os temas, ou substitua --accent por tema se não.
Sumário
O Sumário preenchido posteriormente (veja "Como isso renderiza" acima
...
[truncado para tradução]