gofpdf foi arquivado. Guia de migração para gpdf.
jung-kurt/gofpdf foi arquivado em 2021 e go-pdf/fpdf em 2025. Este guia mapeia toda a API do gofpdf para o gpdf — Go puro, zero dependências, CJK nativo.
TL;DR
gpdf é uma biblioteca Go pura, sem dependências externas, com suporte CJK nativo (sem a dança do AddUTF8Font), que usa uma grade de 12 colunas em vez de empurrar pixels com SetXY e roda aproximadamente 10× mais rápido que o gofpdf nas mesmas cargas. A migração consiste basicamente em substituir chamadas imperativas ao cursor por builders declarativos. Este guia percorre o mapeamento com cinco pares antes/depois.
Semana passada um colega abriu um projeto Go novo, rodou go get github.com/jung-kurt/gofpdf e dez minutos depois me mandou o screenshot do banner do GitHub: "This repository has been archived by the owner. It is now read-only." Seguido por: "Espera, o fork também está arquivado?"
Sim. Os dois.
jung-kurt/gofpdf foi arquivado em 8 de setembro de 2021. O fork comunitário go-pdf/fpdf publicou seu último release em 2023 e foi arquivado em 2025. A biblioteca Go que dois terços das respostas no Stack Overflow ainda recomendam está há mais de quatro anos em modo somente-leitura, e o fork que deveria substituí-la também já se foi.
Se você tem uma base gofpdf em produção, este post é um mapa de migração. Se está começando um projeto novo e foi puxar gofpdf por reflexo, porque era o que os resultados de busca mostravam, esta é a alternativa.
Por que o gofpdf realmente não volta
Bibliotecas open source nem sempre morrem. Às vezes o mantenedor se afasta e alguém assume. Era o que todo mundo assumiu que ia acontecer com o gofpdf — e por um tempo aconteceu. O fork go-pdf/fpdf reorganizou o código, resolveu alguns bugs antigos, aceitou PRs, parecia uma continuação legítima.
Aí, no início de 2025, o fork também foi arquivado. O README agora diz, em parte: "Este projeto não é mais mantido ativamente. Considere usar outra biblioteca."
A razão importa menos que a consequência: todo projeto Go que depende do gofpdf agora está em cima de duas camadas de código sem manutenção. Questões de segurança não vão ser corrigidas. A spec PDF 2.0 saiu em 2020 e o gofpdf não cobre a maior parte do que mudou. A semântica de variáveis de loop do Go 1.25 ainda funciona com gofpdf hoje, mas o que quebrar amanhã é você que conserta em um fork privado.
Não é um problema de "a biblioteca tem bugs". É um problema de cadeia de suprimentos.
Para times no Brasil há um agravante: exigências fiscais (NFe, DANFE, PDF/A para arquivamento) já são difíceis de justificar com uma biblioteca abandonada em auditoria.
Para que o pessoal realmente usa gofpdf
Antes de entrar no mapeamento, vale ser específico sobre as cargas que migram. Olhando os trackers de issues e as perguntas de Stack Overflow e dev.to sobre jung-kurt/gofpdf e go-pdf/fpdf, os usos dominantes são:
- Notas fiscais, recibos, DANFEs — cabeçalho, bloco do cliente, tabela de itens, totais, rodapé.
- Relatórios — documentos multipágina com cabeçalhos repetidos, números de página, gráficos inseridos como imagens.
- Formulários e certificados — texto em posições fixas sobre um template.
- Documentos CJK — notas e etiquetas de envio em japonês, chinês, coreano.
Os três primeiros a API de builder do gpdf cobre tranquilamente. O quarto — CJK — é onde o gpdf abre a maior distância em relação ao gofpdf. O gofpdf obrigava você a chamar AddUTF8Font, gerenciar um caminho para um TTF, e torcer para o texto não sair do plano básico. O gpdf trata CJK como cidadão de primeira classe: registra uma fonte TrueType, escreve em japonês, sai um PDF.
A tabela de mapeamento da API
A tabela a seguir é a colinha. As seções seguintes percorrem cinco pares antes/depois concretos.
| O que você quer fazer | gofpdf | gpdf |
|---|---|---|
| Criar um documento | gofpdf.New("P", "mm", "A4", "") | gpdf.NewDocument(gpdf.WithPageSize(document.A4)) |
| Adicionar uma página | pdf.AddPage() | doc.AddPage() (retorna um *PageBuilder) |
| Definir uma fonte | pdf.SetFont("Arial", "B", 16) | template.FontFamily(...), template.Bold(), template.FontSize(16) como opções de texto |
| Registrar um TTF (CJK) | pdf.AddUTF8Font("noto", "", "NotoSansJP-Regular.ttf") | gpdf.WithFont("NotoSansJP", ttfBytes) (na construção) |
| Escrever uma linha | pdf.Cell(40, 10, "hi") | c.Text("hi") |
| Escrever texto com quebra | pdf.MultiCell(0, 10, body, "", "L", false) | c.Text(body) (quebra automaticamente) |
| Cor do texto | pdf.SetTextColor(255, 0, 0) | template.TextColor(pdf.Red) (opção por texto) |
| Desenhar uma linha horizontal | pdf.Line(x1, y1, x2, y2) | c.Line(template.LineThickness(document.Pt(1))) |
| Inserir uma imagem | pdf.ImageOptions("logo.png", x, y, w, h, ...) | c.Image(imgBytes, template.FitWidth(document.Mm(50))) |
| Definir cursor XY | pdf.SetXY(x, y) | (sem equivalente — use linhas/colunas ou page.Absolute(x, y, fn)) |
| Cabeçalho repetido | pdf.SetHeaderFunc(fn) | doc.Header(fn) |
| Rodapé repetido | pdf.SetFooterFunc(fn) | doc.Footer(fn) |
| Número de página | manual: pdf.PageNo() | c.PageNumber() / c.TotalPages() |
| Saída para arquivo | pdf.OutputFileAndClose("out.pdf") | data, _ := doc.Generate(); os.WriteFile("out.pdf", data, 0o644) |
| Saída para writer | pdf.Output(w) | doc.Render(w) |
A mudança de forma é o que pesa mais: gofpdf é imperativo, gpdf é declarativo. No gofpdf você empurra um cursor pela página e escreve onde ele cai. No gpdf você descreve uma árvore de linhas e colunas e deixa o motor de layout posicionar as coisas. Os primeiros trechos parecem mais longos em gpdf. No terceiro você para de sentir falta do SetXY.
Uma nota sobre unidades. O gofpdf te faz escolher uma unidade base na construção ("mm", "pt", "in"). O gpdf usa pontos internamente e oferece helpers — document.Mm(20), document.Pt(12), document.Cm(1), document.In(0.5) — para a unidade que você preferir no ponto de chamada. É mais próximo de CSS do que do gofpdf, e depois que você coloca um cabeçalho em cada página com margens em document.Mm(15), para de pensar em unidade.
Antes / Depois 1: o PDF mais simples possível
O par "hello world". A brevidade do gofpdf foi o que o deixou tão citável. A versão gpdf tem algumas linhas a mais porque está construindo uma árvore, não dirigindo um cursor.
Antes — gofpdf:
package main
import "github.com/jung-kurt/gofpdf"
func main() {
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.AddPage()
pdf.SetFont("Arial", "B", 24)
pdf.Cell(40, 10, "Hello, World!")
pdf.OutputFileAndClose("hello.pdf")
}
Depois — gpdf:
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
doc := gpdf.NewDocument(
gpdf.WithPageSize(document.A4),
gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("Hello, World!", template.FontSize(24), template.Bold())
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("hello.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
A grade faz o trabalho. AutoRow adiciona uma linha cuja altura vem do conteúdo; r.Col(12, ...) diz "esta coluna ocupa as 12 colunas da grade". Mesma ideia do Bootstrap aplicada a uma página PDF.
Generate() devolve os bytes; Render(w) transmite para um io.Writer se você preferir não alocar. Não existe o passo de "fechar o arquivo" porque o gpdf não segura file handle nenhum.
Antes / Depois 2: uma tabela de itens de nota
Tabelas é onde o gofpdf fica verboso. Não tem tabela embutida; você chama Cell em loops aninhados, gerencia largura de colunas na mão e usa Ln(-1) para pular linha. Metade dos tutoriais de nota fiscal com gofpdf na internet é, literalmente, boilerplate de tabela.
Antes — gofpdf:
pdf.SetFont("Arial", "B", 11)
pdf.SetFillColor(220, 220, 220)
pdf.CellFormat(80, 8, "Descrição", "1", 0, "L", true, 0, "")
pdf.CellFormat(20, 8, "Qtd.", "1", 0, "C", true, 0, "")
pdf.CellFormat(30, 8, "Preço Unit.", "1", 0, "R", true, 0, "")
pdf.CellFormat(30, 8, "Total", "1", 1, "R", true, 0, "")
pdf.SetFont("Arial", "", 11)
items := [][]string{
{"Desenvolvimento frontend", "40 h", "R$ 150,00", "R$ 6.000,00"},
{"Desenvolvimento backend", "60 h", "R$ 150,00", "R$ 9.000,00"},
{"Design de UI", "20 h", "R$ 120,00", "R$ 2.400,00"},
}
for _, row := range items {
pdf.CellFormat(80, 8, row[0], "1", 0, "L", false, 0, "")
pdf.CellFormat(20, 8, row[1], "1", 0, "C", false, 0, "")
pdf.CellFormat(30, 8, row[2], "1", 0, "R", false, 0, "")
pdf.CellFormat(30, 8, row[3], "1", 1, "R", false, 0, "")
}
Você calcula as larguras de cabeça, e boa sorte se alguma descrição quebrar.
Depois — gpdf:
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Table(
[]string{"Descrição", "Qtd.", "Preço Unit.", "Total"},
[][]string{
{"Desenvolvimento frontend", "40 h", "R$ 150,00", "R$ 6.000,00"},
{"Desenvolvimento backend", "60 h", "R$ 150,00", "R$ 9.000,00"},
{"Design de UI", "20 h", "R$ 120,00", "R$ 2.400,00"},
},
template.ColumnWidths(50, 15, 15, 20),
template.TableHeaderStyle(
template.Bold(),
template.TextColor(pdf.White),
template.BgColor(pdf.RGBHex(0x1A237E)),
),
template.TableStripe(pdf.RGBHex(0xF5F5F5)),
)
})
})
ColumnWidths(50, 15, 15, 20) são porcentagens da coluna em que a tabela vive, não milímetros absolutos. Jogue a mesma tabela dentro de um r.Col(6, ...) e as mesmas porcentagens continuam funcionando. Esse é o tipo de coisa que não sai do CellFormat sem um wrapper.
Quebra de linha automática. Quebra de página automática — se a tabela passar da margem inferior, o cabeçalho se repete na página seguinte.
Antes / Depois 3: texto CJK sem a dança ritual
Esse é o que me fez largar o gofpdf. Para renderizar japonês em gofpdf você chama AddUTF8Font, aponta para um TTF no disco, define a fonte e reza. O subsetting funciona na maior parte do tempo. Alguns TTFs disparam colisão de glyph-id e produzem caracteres corrompidos. As mensagens de erro não ajudam.
Antes — gofpdf:
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.AddUTF8Font("notosansjp", "", "NotoSansJP-Regular.ttf")
pdf.AddPage()
pdf.SetFont("notosansjp", "", 14)
pdf.Cell(0, 10, "こんにちは、世界。")
pdf.OutputFileAndClose("ja.pdf")
Duas minas. O TTF precisa existir no caminho dado em tempo de execução (então sua imagem Docker tem que carregar a fonte). E Cell com largura 0 significa "até a margem direita", o que em CJK frequentemente corta porque o estimador de largura não conta corretamente os glifos de largura total.
Depois — gpdf:
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
fontData, err := os.ReadFile("NotoSansJP-Regular.ttf")
if err != nil {
log.Fatal(err)
}
doc := gpdf.NewDocument(
gpdf.WithPageSize(document.A4),
gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
gpdf.WithFont("NotoSansJP", fontData),
gpdf.WithDefaultFont("NotoSansJP", 14),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("こんにちは、世界。")
c.Text("吾輩は猫である。名前はまだ無い。")
c.Text("東京都渋谷区神宮前1-2-3")
})
})
data, _ := doc.Generate()
os.WriteFile("ja.pdf", data, 0o644)
}
Duas diferenças.
Primeira: você passa bytes, não um caminho. Embute o TTF com //go:embed e seu binário fica autocontido. Nada de "font not found" em produção porque alguém esqueceu de montar um volume.
Segunda: o subsetter TrueType do gpdf entende os formatos cmap de CJK (4, 6, 12) e a codificação Identity-H. O PDF de saída carrega apenas os glifos que você realmente usou — embutir NotoSansJP para uma nota de 200 caracteres dá um subset de ~30 KB, não um embed cheio de 4 MB. Se você já viu o gofpdf produzir um PDF de 5 MB para uma página em japonês, essa é a primeira coisa que você nota.
Para um tour mais profundo de opções específicas de CJK — IPAex Gothic, Source Han Sans, cadeias de fallback — veja o post complementar que vem.
Antes / Depois 4: cabeçalho em cada página, número no rodapé
O padrão do gofpdf para cromo repetido é SetHeaderFunc e SetFooterFunc — ambos recebem um func() que roda contra o cursor atual. Números de página vêm de pdf.PageNo() e pdf.AliasNbPages().
Antes — gofpdf:
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.SetHeaderFunc(func() {
pdf.SetFont("Arial", "B", 12)
pdf.Cell(0, 10, "ACME Ltda.")
pdf.Ln(15)
})
pdf.SetFooterFunc(func() {
pdf.SetY(-15)
pdf.SetFont("Arial", "I", 8)
pdf.CellFormat(0, 10,
fmt.Sprintf("Página %d/{nb}", pdf.PageNo()),
"", 0, "C", false, 0, "")
})
pdf.AliasNbPages("")
pdf.AddPage()
// ... corpo ...
{nb} é uma sentinela que o gofpdf reescreve na saída com o número total de páginas. Funciona, só faz parte daquelas coisas que você precisa saber.
Depois — gpdf:
doc := gpdf.NewDocument(
gpdf.WithPageSize(document.A4),
gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
)
doc.Header(func(p *template.PageBuilder) {
p.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("ACME Ltda.", template.Bold(), template.FontSize(12))
c.Line(template.LineColor(pdf.Gray(0.7)))
c.Spacer(document.Mm(4))
})
})
})
doc.Footer(func(p *template.PageBuilder) {
p.AutoRow(func(r *template.RowBuilder) {
r.Col(6, func(c *template.ColBuilder) {
c.Text("ACME Ltda.",
template.FontSize(8), template.TextColor(pdf.Gray(0.5)))
})
r.Col(6, func(c *template.ColBuilder) {
// "Página X / Y" — ambas partes são placeholders
// resolvidos pelo motor de layout após a paginação.
c.PageNumber(template.AlignRight(),
template.FontSize(8), template.TextColor(pdf.Gray(0.5)))
})
})
})
for i := 0; i < 10; i++ {
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text(fmt.Sprintf("Conteúdo da página %d.", i+1))
})
})
}
PageNumber e TotalPages são placeholders. Eles se expandem após a paginação, quando o motor de layout já sabe quantas páginas existem. Sem sentinela {nb}, sem SetY(-15) para cravar o rodapé embaixo — o rodapé é só uma árvore, e o motor reserva espaço para ele em cada página automaticamente.
Antes / Depois 5: produzir bytes para um handler HTTP
A maior parte do código gofpdf em produção não escreve em arquivo. Escreve num io.Writer — geralmente um http.ResponseWriter retornando application/pdf para o navegador. Este é o par onde a API do gpdf mais se parece com a do gofpdf.
Antes — gofpdf:
func handler(w http.ResponseWriter, r *http.Request) {
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.AddPage()
pdf.SetFont("Arial", "", 12)
pdf.Cell(0, 10, "Gerado em "+time.Now().Format(time.RFC3339))
w.Header().Set("Content-Type", "application/pdf")
if err := pdf.Output(w); err != nil {
http.Error(w, err.Error(), 500)
}
}
Depois — gpdf:
func handler(w http.ResponseWriter, r *http.Request) {
doc := gpdf.NewDocument(
gpdf.WithPageSize(document.A4),
gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("Gerado em " + time.Now().Format(time.RFC3339))
})
})
w.Header().Set("Content-Type", "application/pdf")
if err := doc.Render(w); err != nil {
http.Error(w, err.Error(), 500)
}
}
Mesma forma. doc.Render(w) transmite o PDF direto na resposta. Se quiser setar Content-Length, chame Generate() primeiro para pegar o slice de bytes e tire len().
Quão rápido é "rápido o suficiente"?
O gpdf é aproximadamente 10× mais rápido que o gofpdf nas cargas que a galera realmente roda. Os números abaixo vêm de _benchmark/benchmark_test.go rodando num Apple M1 com Go 1.25.
| Benchmark | gpdf | gofpdf | gopdf | Maroto v2 |
|---|---|---|---|---|
| Página única | 13 µs | 132 µs | 423 µs | 237 µs |
| Tabela 4×10 | 108 µs | 241 µs | 835 µs | 8,6 ms |
| Documento de 100 págs. | 683 µs | 11,7 ms | 8,6 ms | 19,8 ms |
| Nota fiscal CJK complexa | 133 µs | 254 µs | 997 µs | 10,4 ms |
Não são sintéticos — o benchmark de tabela é uma de itens de nota com 4 colunas e 10 linhas; o de 100 páginas é um relatório paginado com cabeçalho repetido e números de página. A forma combina com o que o código de produção faz de verdade.
Sobre o que esses números significam. A 13 µs por página única, um único core produz ~75.000 PDFs "hello world" por segundo. A 108 µs por página com tabela, ~9.000 notas por segundo. O ponto não é ostentar — é que você pode parar de se perguntar se precisa cachear ou empurrar geração de PDF para uma fila assíncrona. Para a maioria das cargas, gerar no caminho da requisição basta.
O que você perde na migração
Nada neste guia tem valor se esconder lacunas reais. Aqui está o que o gpdf ainda não faz e o gofpdf fazia:
- Linhas em ângulos arbitrários, curvas Bézier e caminhos complexos.
c.Line()desenha uma régua horizontal cruzando uma coluna. Se você produz desenhos técnicos ou gráficos com geometria própria, o gpdf ainda não chegou lá. (Gráficos como imagens pré-renderizadas: funciona sem problema.) SetXYe trabalho com cursor absoluto. Dá pra posicionar absoluto compage.Absolute(x, y, fn), mas se seu código existente são 2.000 linhas deSetXYseguidas deCell, a migração é mais uma reescrita. A boa notícia é que o código reescrito costuma ser metade do tamanho do original.- Campos de formulário (AcroForm). O gpdf ainda não gera campos preenchíveis. Se seus PDFs são templates que o usuário preenche num visualizador, fique com uma biblioteca que suporte AcroForm — por enquanto.
- Anotações e marcadores. Suporte básico de outline existe; anotações ricas não.
Se nada disso morde você, a migração é direta. Se morde, abra uma issue — o roadmap é movido pelo que as pessoas pedem.
FAQ
O gpdf é um fork do gofpdf? Não. O gpdf é uma reimplementação limpa. O trabalho de formato de cabo PDF, o motor de layout, o subsetter TrueType — tudo escrito do zero em Go puro. Não há linhagem compartilhada com gofpdf ou seus forks. Tem que ser reescrita porque a arquitetura do gofpdf é construída em torno de um único cursor mutável; não dá para extrair uma grade declarativa daquilo sem quebrar todos os sites de chamada existentes.
O gpdf tem dependências externas?
A biblioteca core tem zero. Rode go mod graph | grep gpdf depois de go get github.com/gpdf-dev/gpdf e verá uma única linha. O add-on gpdf-pro (HTML→PDF, criptografia AES, assinaturas, PDF/A) puxa golang.org/x/net para parsing HTML, mas é opt-in e não é necessário para migrar.
E CGO? O gofpdf era CGO-free, e o gpdf?
Igual. Go puro, sem CGO. Faça GOOS=linux GOARCH=arm64 go build e entregue um binário estático. Isso importa em imagens distroless e Alpine, onde arrastar uma toolchain CGO dobra o tamanho do container.
Meu código gofpdf existente usa SetXY para posicionamento absoluto em todo lugar. Dá para migrar sem reescrever?
Você consegue envolver page.Absolute(x, y, fn) e ter algo com sensação parecida. Mas se seu código está estruturado em torno de manipulação de cursor, o modelo do motor de layout é uma mudança mental, não sintática. A maior parte dos times descobre que a reescrita sai mais curta do que o original.
NFe, DANFE e PDF/A para arquivamento fiscal?
Carimbo de tempo e assinatura digital estão sendo implementados em gpdf-pro. Se você tem um requisito concreto, abra uma issue para subir a prioridade.
E se o go-pdf/fpdf for desarquivado? Aí você tem mais uma opção. A aposta por trás do gpdf não é que o gofpdf fique arquivado para sempre — é que a arquitetura (baseada em cursor, fontes de um byte, sem CJK nativo) é beco sem saída independente de quem mantém. Gerar PDF em 2026 se parece mais com montar uma página web do que com dirigir um plotter, e a API deveria refletir isso.
Experimente o gpdf
gpdf é uma biblioteca Go para gerar PDFs. Licença MIT, zero dependências externas, suporte CJK nativo.
go get github.com/gpdf-dev/gpdf
⭐ Star no GitHub · Leia a documentação
Próximas leituras
- Como funciona a grade de 12 colunas no gpdf? (em breve)
- Como embutir uma fonte japonesa no gpdf? (em breve)
- Quickstart — setup em cinco minutos, incluindo
go.mod