Todas as publicações

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.

por gpdf team

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:

  1. Notas fiscais, recibos, DANFEs — cabeçalho, bloco do cliente, tabela de itens, totais, rodapé.
  2. Relatórios — documentos multipágina com cabeçalhos repetidos, números de página, gráficos inseridos como imagens.
  3. Formulários e certificados — texto em posições fixas sobre um template.
  4. 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 fazergofpdfgpdf
Criar um documentogofpdf.New("P", "mm", "A4", "")gpdf.NewDocument(gpdf.WithPageSize(document.A4))
Adicionar uma páginapdf.AddPage()doc.AddPage() (retorna um *PageBuilder)
Definir uma fontepdf.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 linhapdf.Cell(40, 10, "hi")c.Text("hi")
Escrever texto com quebrapdf.MultiCell(0, 10, body, "", "L", false)c.Text(body) (quebra automaticamente)
Cor do textopdf.SetTextColor(255, 0, 0)template.TextColor(pdf.Red) (opção por texto)
Desenhar uma linha horizontalpdf.Line(x1, y1, x2, y2)c.Line(template.LineThickness(document.Pt(1)))
Inserir uma imagempdf.ImageOptions("logo.png", x, y, w, h, ...)c.Image(imgBytes, template.FitWidth(document.Mm(50)))
Definir cursor XYpdf.SetXY(x, y)(sem equivalente — use linhas/colunas ou page.Absolute(x, y, fn))
Cabeçalho repetidopdf.SetHeaderFunc(fn)doc.Header(fn)
Rodapé repetidopdf.SetFooterFunc(fn)doc.Footer(fn)
Número de páginamanual: pdf.PageNo()c.PageNumber() / c.TotalPages()
Saída para arquivopdf.OutputFileAndClose("out.pdf")data, _ := doc.Generate(); os.WriteFile("out.pdf", data, 0o644)
Saída para writerpdf.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.

BenchmarkgpdfgofpdfgopdfMaroto v2
Página única13 µs132 µs423 µs237 µs
Tabela 4×10108 µs241 µs835 µs8,6 ms
Documento de 100 págs.683 µs11,7 ms8,6 ms19,8 ms
Nota fiscal CJK complexa133 µs254 µs997 µs10,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.)
  • SetXY e trabalho com cursor absoluto. Dá pra posicionar absoluto com page.Absolute(x, y, fn), mas se seu código existente são 2.000 linhas de SetXY seguidas de Cell, 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