5

Portei o dart_pdf (a melhor lib de geração de PDF) para JavaScript puro

O começo de tudo

Nos anos 80, gerar um relatório formatado parecia mais um desafio de geometria analítica do que de programação. Em Clipper, você posicionava um texto com @ 10, 20 SAY "Report Title". Em Fortran, a mesma ideia vinha do descritor de coluna do FORMAT: FORMAT(T20, 'Report Title'). Em Cobol, o Report Writer usava cláusulas como LINE 10 COLUMN 20 na definição do grupo de relatório. Linguagens diferentes, mesmo raciocínio: você dizia exatamente em que linha e coluna cada pedaço de texto deveria cair.

Em 2026, gerar um PDF ainda segue o mesmo padrão (x,y) na maioria das libs.
Por anos, gerei PDFs usando várias linguagens e bibliotecas diferentes — iText, PDFKit, pdfmake, jsPDF, React-pdf, e várias outras. Hoje em dia, mesmo com apoio de agentes LLMs, posicionar e formatar coisas dentro de um PDF ainda é muito difícil e leva muitos ciclos de ajustes finos.

Entendo que o problema esbarra diretamente no PDF, pois ele é um formato de baixo nível, descreve a saída final e não um formato de layout inteligente. Para uma biblioteca de PDF, isso vira centenas de decisões geométricas: coordenadas, fontes, alturas, quebras de página, margens, tabelas, imagens e assim por diante. Diferentemente de HTML/CSS ou Word, o PDF não foi concebido para você dizer "coloque este parágrafo e deixe o restante fluir naturalmente". Essencialmente o PDF é uma lista de instruções de layout e conteúdo, depois que suas posições já foram calculadas.

Mesmo para as LLMs, a paginação é surpreendentemente difícil. A IA precisa saber quanto espaço cada elemento ocupará antes de colocá-lo. Um parágrafo pode ter 3 linhas ou 7 dependendo da fonte, largura, idioma etc. Se ele não couber, é preciso criar outra página, mover o conteúdo, repetir cabeçalhos e talvez evitar uma linha órfã. Texto depende de métricas reais da fonte. Não basta saber que a fonte é tamanho 12, para calcular corretamente o layout são necessárias informações como largura de cada glyph, ascent, descent, line height e fallback de caracteres.

Para os humanos e para LLMs, um código de geração de PDF (ex: setText('Lorem Ipsum', 38, 92)) não significa nada semanticamente. Nós humanos, ainda utilizamos o ciclo de gerar o PDF, ver se o texto ficou na posição correta, e fazer os ajustes. Mas o formato do PDF é especialmente ruim para LLMs, o agente de IA fica praticamente diagramando às cegas.

É inegável que LLMs são muito boas em coisas simbólicas:

  • "gere uma tabela com estes dados"
  • "escreva este relatório"
  • "calcule essas métricas"

Mas diagramação exige raciocínio espacial + iterativo + preciso em pixels/pontos.

E há uma ironia: o PDF em si, não é (nem de longe) o maior problema. O difícil é o layout. Se a IA pudesse produzir um modelo intermediário realmente bom, algo como um "DOM para PDF", com símbolos semânticos, gerar o arquivo PDF no final seria quase trivial.

A inovação

Em 2015 entra em cena o Eric Seidel. Ele junto com sua equipe, inspirados pelo Flexbox do HTML/CSS, criaram o sistema de layout do Sky (que depois se tornaria o Flutter), que extremamente resumido, define uma estrutura de layout declarativo em formato de árvore, onde cada pai passa ao filho restrições de layout, e o filho informa ao pai, o espaço que ele precisa para ser desenhado na tela depois. Foi uma inovação muito grande que influenciou o Jetpack Compose e o SwiftUI.

Este sistema de layout fez David PHAM-VAN (DavBfr) pensar fora da caixa e criar o dart_pdf ("A Widgets system similar to Flutter's, for easy high-level Pdf creation").

Vamos recapitular uma premissa:

Para gerar um PDF, alguém precisa resolver o layout antes.

Neste quesito que o dart_pdf inovou. Ele trouxe um sistema de layout declarativo no estilo Flutter pra um formato que nunca teve isso. Foi uma sacada nova, inspirada no Flutter, mas aplicada de forma muito original ao PDF. Por isso tanta gente (inclusive eu) acha ela superior às libs tradicionais, e uma inovação necessária para que a geração de relatórios formatados consiga sair dos anos 80.

No dart_pdf, o "layout do flutter" foi reimplementado para um domínio completamente diferente: geração de PDF. Esse é um dos motivos pelos quais o dart_pdf ficou tão elegante. Em vez de gerar PDF por coordenadas absolutas, ele trouxe o paradigma declarativo do Flutter para um formato que tradicionalmente é muito mais baixo nível.

A motivação

Desde que conheci o dart_pdf (há uns 6 ou 7 anos), usei ele em muitos projetos. Não porque seja a lib mais rápida ou a mais leve (só quem já rodou Dart em servidor sabe do que estou falando), mas por causa da forma como ela permite pensar o layout. Você compõe um documento, exatamente como faria com uma UI no Flutter ou na web (Column, Row, Container, Table), e a paginação, o layout e o fluxo de texto simplesmente funcionam.

Já usei essa biblioteca em apps mobile, em servidores, no desktop e na web, e a facilidade de criar documentos sempre superou os desafios específicos de cada tecnologia.

Ao longo desses anos, também precisei examinar o código-fonte (mais de uma vez) para depurar problemas reais em produção. Eu não estava apenas usando a biblioteca de fora (isso é importante para o que vem a seguir); em certa medida, eu precisei entender seu funcionamento interno e com isso, sabia que quase nada nela realmente dependia do Dart.

Mas ela tem um grande problema: não dá para usá-la em JavaScript puro.

Só para constar: já desenvolvi projetos em html que usavam o flutter web + dart_pdf dentro de iframe, apenas para gerar PDF.

Existem libs em JavaScript como a React-pdf, que é o que mais se aproxima dessa forma declarativa no ecossistema JS, mas arrasta o React junto.

Eu queria algo sem framework, que rodasse num navegador só com um importmap, no Node ou dentro de um host V8 puro, embutido numa aplicação via ClearScript.

Então eu portei a biblioteca. O resultado é o js_pdf, que agora está em seu primeiro release candidate.

Antes de mais nada, deixe-me ser inequívoco quanto aos créditos: toda boa ideia nesta biblioteca é do David PHAM-VAN (DavBfr). O modelo de widgets, o protocolo de layout, o algoritmo de paginação, a superfície da API. Tudo isso é design dele, refinado ao longo de anos de uso real.

Eu apenas traduzi o código e onde eu divergi, foi para atender a uma restrição que ele nunca teve. Sem o dart_pdf, o js_pdf simplesmente não existiria. É um port, e a licença e os cabeçalhos de atribuição deixam isso claro em cada arquivo.

A experiência

Eu não queria uma lib para funcionar no Node. Eu queria uma api 100% JavaScript para rodar no Node, no Browser, no ClearScript, no Deno, etc.

Sem DOM. Sem Canvas. Sem Buffer. Sem fetch. Sem sistema de arquivos. Sem timers. O único trabalho da biblioteca é transformar uma árvore de widgets em um Uint8Array.

Parece uma preferência estética mas não é. Isso é o que permite que o mesmo arquivo, exatamente igual, rode num importmap de navegador, no Node e dentro do ClearScript sem shim, sem bundler e sem polyfill.

Fazer isso valer de verdade acabou sendo uma parte interessante. Eu compilo para ES2020 com types: [], de modo que document, window, fetch, setTimeout, Buffer e process viram erros de compilação e não avisos de lint. Além disso, existe um script de verificação (source gate) que varre o bundle compilado em busca de nomes proibidos. A regra só vale se for verificada deterministicamente. Se estivesse apenas escrita no AGENTS.md ou CLAUDE.md, algum ponto teria passado em quase 40 mil linhas de código.

Pontos que foram mais difíceis do que eu esperava

1. Memória

No início, meu decodificador de PNG consumia meio gigabyte de heap para uma única imagem grande. A causa era um problema de port errado: acumular bytes num array number[] comum. O V8 gasta cerca de oito bytes por elemento num array de números, então um buffer de pixels de 60 MB acabava ocupando 480 MB.

Hoje, todo caminho binário do port acumula dados num Uint8Array que cresce conforme necessário. Essa é agora uma das regras que o source gate verifica, porque é exatamente o tipo de coisa que parece inofensiva numa revisão de código.

2. Divergência de arquitetura: "Layout/Paint"

A mágica do Flutter: Constraints go down. Sizes go up. Parents set positions.

Lá atrás eu falei do modelo de layout que o Flutter introduziu. layout() e paint() são duas fases do mesmo ciclo de desenho: o widget mede a si mesmo e guarda o resultado no campo size do próprio RenderObject, paint() lê esse valor logo em seguida, ainda dentro do mesmo ciclo. Refazer essa medição a cada um dos 60 quadros por segundo seria caro, então o valor fica em cache até alguém marcar markNeedsLayout().

O dart_pdf importa esse contrato: layout() escreve no "cache" , e cada paint() é sempre chamado logo depois do layout() que acabou de preencher aquele box dentro do mesmo ciclo. Em multi_page.dart a fase de paginação e a fase de desenho são separadas, e cada uma refaz seu próprio layout() imediatamente antes de usar o box, ou seja a leitura sempre acontece pareada na escrita que a gerou.

O problema é que essa garantia mora na ordem em que o código chama layout() e paint() em centenas de pontos ao longo de 37 mil linhas. No port, algumas dessas chamadas aconteciam fora de ordem, e o resultado final eram PDFs com conteúdo cortado, tabelas quebradas e imagens desalinhadas.

Como opção de arquitetura (já que não precisamos do cache), o box na js_pdf deixou de ser uma propriedade mutável e virou um valor retornado por layout(), passado como parâmetro para paint().

Como eu realmente fiz isso

É óbvio: agentes de LLM foram uma parte grande de como isso foi possível. Não vou fingir o contrário. Mas quero ser preciso sobre o que eles me deram.

Eu já tinha anos de uso do dart_pdf e muitas sessões de debug dentro do seu código-fonte. Eu sabia o que ele fazia internamente, sabia que pouco estava realmente atrelado ao Dart, e estava convencido de que uma versão em JavaScript puro era possível. Essa convicção é antiga. O que eu nunca tive foi tempo. Uma tradução desse tamanho representa milhares de horas que uma pessoa simplesmente não encontra. Tanto que até hoje não existia um port em JavaScript.

A referência de verdade são os exemplos. O dart_pdf traz um conjunto deles que mantive como benchmark, e a definição de "pronto", para qualquer fase, significava que esses exemplos geravam o documento do início ao fim, no Node, Browser e ClearScript. Ao redor disso existe uma suíte de testes: 447 testes em cerca de 7.300 linhas, contra aproximadamente 36.900 linhas de TypeScript em 148 arquivos-fonte. O resultado final é um único módulo ES de ~400 KB minificado, com declarações de tipos incluídas.

Em relação às horas de trabalho, foi isso que mudou. Os agentes não me disseram o que construir; eles me deram as horas para construir aquilo que eu já sabia que existia. E um agente que traduz 37.000 linhas também vai traduzir um bug com total confiança, inventar um comportamento que nunca existiu no original, gerar uma coisa que não funciona no ambiente de destino. E é exatamente por isso que os exemplos e a suíte de testes são o que tornam o resultado confiável, não o modelo.

Saber o que fazer e como verificar se o que foi feito está correto: esse é o trabalho.

Saindo da década de 80

Lá no começo eu descrevi uma ironia: o PDF em si não é o problema, o layout é. E que, se existisse um modelo intermediário de verdade — uma espécie de "DOM para PDF", com símbolos semânticos em vez de coordenadas — gerar o arquivo final seria quase trivial, principalmente para uma IA.

Esse modelo já existe. Chama-se dart_pdf. Quando você escreve Column, Row, Container, Table, Text, você não está dizendo "desenhe em x, y" — está descrevendo intenção, e é o próprio motor de layout que resolve constraints, tamanhos e posições por baixo. É o "DOM para PDF" que eu descrevi lá em cima, só que o David já tinha construído há anos, emprestando a árvore de widgets do Flutter para um formato que nunca teve isso.

Pedir para um agente de LLM escrever pw.Column({ children: [...] }) é uma tarefa simbólica. Pedir para o mesmo agente calcular em que pixel cada glyph cai não é.

Portar o dart_pdf para JavaScript não resolveu o problema do layout, o David já tinha resolvido isso. O port resolveu o problema desse modelo semântico só existir em Dart.

Projeto original, que merece sua estrela muito mais do que o meu: https://github.com/DavBfr/dart_pdf

js_pdf: https://github.com/romulocrj/js_pdf

Apache-2.0, igual ao original. Não oficial e sem afiliação. Este é o meu port do trabalho do David, não um projeto dele, e qualquer bug que você encontrar aqui é meu, não dele.

Obrigado, David. Espero que este port leve ainda mais gente até você.


Carregando publicação patrocinada...
1