Todas as publicações

unipdf é AGPL ou pago. Como migrar para o gpdf.

O unipdf da UniDoc obriga AGPL v3 ou licença comercial por desenvolvedor. Este guia mapeia a API creator do unipdf para gpdf — MIT, zero deps, sem chave.

TL;DR

gpdf é uma biblioteca PDF em Go puro sob licença MIT, com zero dependências externas e sem etapa de registro de chave de licença. Se você está em unidoc/unipdf porque nada mais resolveu CJK ou aplainamento de AcroForm, mas a cláusula AGPL travou o jurídico para distribuição e o tier comercial é difícil de justificar, este guia mapeia a API creator do unipdf para o gpdf, peça por peça.

No último trimestre, um amigo numa fintech levou github.com/unidoc/unipdf/v3 pelo fluxo de aprovação OSS. O ticket de compliance voltou no dia seguinte com um X vermelho ao lado de AGPL-3.0 e uma nota do jurídico: "Não pode ser linkado em produtos fechados distribuíveis. Adquirir licença comercial ou remover." O orçamento comercial chegou com uma anuidade por desenvolvedor que, para um time de doze, fez todo mundo reabrir os resultados de busca.

Esta é a parte da história do unipdf que não aparece no README. unipdf é tecnicamente excelente — maduro, completo, bem mantido. Também é dual-licensed: AGPL v3 para uso aberto, comercial pago para o resto. AGPL v3 é o copyleft mais forte de uso comum. Se você linka unipdf num serviço acessado por usuários via rede, o §13 obriga publicar todo o código-fonte correspondente. A maioria dos jurídicos diz não.

Se você tem código unipdf em produção e a licença pegou na auditoria ou está chegando na renovação, este é o mapa de migração. Se você está começando e pegou unipdf no reflexo porque a documentação era a mais polida, esta é a alternativa que não vem com relação de cobrança.

O que "AGPL ou pago" significa na prática

Muitas bibliotecas Go ganham um rótulo "AGPL" sem o time pensar de fato no que significa. unipdf não é assim. O arquivo de licença do repositório é AGPL v3 puro, o README é explícito que uso comercial requer chave, e o próprio binário força isso — chame qualquer API do unipdf sem registrar uma licença na inicialização e você recebe um erro ou uma marca d'água em cada página de saída.

Você acaba em um destes três modos:

  1. Modo AGPL. Você publica o seu código sob AGPL v3. Cada byte do seu serviço que toca unipdf, mais tudo que linka a ele, precisa ficar disponível para qualquer um que interaja com o serviço pela rede. Para a maioria das ferramentas internas e produtos SaaS, isso é inviável.
  2. Modo comercial. Você paga à UniDoc por desenvolvedor por ano. Os preços variam — orçamentos públicos recentes giraram em torno de quatro dígitos por assento por ano — e incluem uma chamada de registro (medida ou por chave) que cada binário precisa fazer no startup. A chave é tratada como segredo, então mora no seu secret manager e é injetada em cada container.
  3. Modo trial / avaliação. Grátis por tempo limitado. Saídas com marca d'água. Inviável em produção.

Nenhum desses modos é intrinsecamente errado. UniDoc é uma empresa real com engenheiros reais, e o preço reflete o que custa construir e manter uma biblioteca PDF abrangente. O ponto é que a decisão de licenciamento se infiltra em cada camada: revisão jurídica, rotação de segredos, renovação financeira e a superfície de deploy (cada container precisa da chave). gpdf elimina essa coluna inteira da sua planilha por ser MIT.

O que você perde e o que você mantém

Vale ser honesto antes de entrar na API. unipdf faz coisas que gpdf não faz:

Capacidadeunipdfgpdf
Geração de PDF
TrueType / fontes CJK✅ (sem CGO, subset automático)
Cifragem AES-128/256✅ (ISO 32000-2 Rev 6, Go puro)
Assinatura PKCS#7 / PAdES✅ (suporte RFC 3161 TSA)
PDF/A-1b/2b
AcroForm — preencher existente✅ (apenas flatten, sem criar campo novo)
AcroForm — criar campos novos
Parsing / extração de texto❌ (gpdf foca em geração)
OCR
Redaction (tarjamento)
Renderização HTMLparcial❌ (use renderer separado e faça merge)

Se você precisa de parsing de PDF, OCR ou redaction, esta migração não te leva até o fim. Ou mantém unipdf só nesses caminhos de código (vai continuar devendo a licença comercial para esses binários) ou escolhe uma biblioteca focada em parsing para o lado de leitura. Para o caminho de geração, cifragem, assinatura, fontes e CJK — para o que a maior parte das faturas de unipdf de fato vai — gpdf é uma troca completa.

Removendo o código de registro de licença

É o menor diff de toda a migração e o que faz o resto parecer real. Binários unipdf precisam registrar uma chave no startup. Há algumas variantes:

// API key (medida)
import "github.com/unidoc/unipdf/v3/common/license"

func init() {
    if err := license.SetMeteredKey(os.Getenv("UNIDOC_API_KEY")); err != nil {
        log.Fatal(err)
    }
}
// Arquivo de licença offline
func init() {
    licenseKey, _ := os.ReadFile("/etc/unidoc/license.txt")
    if err := license.SetLicenseKey(string(licenseKey), "Acme Corp"); err != nil {
        log.Fatal(err)
    }
}

No gpdf não existe equivalente. Apague o bloco init() inteiro. Tire UNIDOC_API_KEY do seu secret manager, das variáveis de CI e dos manifests de container. Remova o arquivo de licença da imagem. A única coisa que você importa é github.com/gpdf-dev/gpdf, e a única exigência é chamar gpdf.NewDocument em algum lugar.

Só isso. Também é o teste de que sua migração de fato aterrissou: grep -r unidoc . deve retornar zero ocorrências quando terminar.

Tabela de mapeamento da API

A tabela é a cola. As seções depois andam por cinco pares concretos. unipdf chama o builder de alto nível de Creator; gpdf chama de Document. As formas são parecidas o suficiente para a maioria do código se traduzir por inspeção visual.

O que você quer fazerunipdf (creator)gpdf
Criar um builderc := creator.New(); c.SetPageSize(creator.PageSizeA4)doc := gpdf.NewDocument(gpdf.WithPageSize(document.A4))
Definir margensc.SetPageMargins(L, R, T, B)gpdf.WithMargins(document.UniformEdges(document.Mm(20)))
Nova páginac.NewPage()page := doc.AddPage()
Linha única de textop := c.NewParagraph("hi"); c.Draw(p)c.Text("hi") (dentro de uma coluna)
Texto com quebrap := c.NewStyledParagraph(); p.SetText(...); c.Draw(p)c.Text(body) (quebra automática)
Registro de fontemodel.NewCompositePdfFontFromTTFFile(path)gpdf.WithFont("Name", ttfBytes) (no construtor)
Aplicar fonte ao textostyle.Font = font; style.FontSize = 12template.FontFamily("Name"), template.FontSize(12) por-texto
Corstyle.Color = creator.ColorRGBFromHex("#1A237E")template.TextColor(pdf.RGBHex(0x1A237E))
Tabelat := c.NewTable(4); t.SetColumnWidths(...); c.Draw(t)c.Table(headers, rows, template.ColumnWidths(...))
Imagemimg, _ := c.NewImageFromFile(path); img.ScaleToWidth(w); c.Draw(img)c.Image(imgBytes, template.FitWidth(document.Mm(50)))
Cabeçalho / rodapéc.DrawHeader(fn) / c.DrawFooter(fn)doc.Header(fn) / doc.Footer(fn)
Número de páginamanual via args de DrawFooterc.PageNumber() / c.TotalPages() (placeholders)
Cifragemmodel.PdfWriter + Encrypt re-encodinggpdf.WithEncryption(gpdf.AES256, "user", "owner", perms)
Assinaturamodel.NewPdfAppender(...).Sign(...)gpdf.SignDocument(pdfBytes, signer, opts)
Registro de licençalicense.SetMeteredKey(...) em init()(nenhum — apague)
Saída para arquivoc.WriteToFile("out.pdf")data, _ := doc.Generate(); os.WriteFile("out.pdf", data, 0o644)
Saída para writerc.Write(w)doc.Render(w)

Duas mudanças estruturais para guardar. O creator do unipdf é com estado: você constrói um Paragraph ou uma Table e chama c.Draw(thing) para confirmar. gpdf é declarativo: você descreve uma árvore de linhas e colunas e deixa o motor de layout posicionar. A segunda mudança é que gpdf tem uma grade de 12 colunas como o Bootstrap. Toda linha tem implicitamente 12 unidades de largura; você gasta com r.Col(n, fn). A maioria dos layouts colapsa em duas ou três linhas assim que você para de medir larguras em milímetros.

Antes / Depois 1: o PDF mais simples possível

O par "hello world". A versão unipdf não é longa; só tem mais cerimônia por causa da chamada de licença.

Antes — unipdf:

package main

import (
    "log"
    "os"

    "github.com/unidoc/unipdf/v3/common/license"
    "github.com/unidoc/unipdf/v3/creator"
)

func init() {
    if err := license.SetMeteredKey(os.Getenv("UNIDOC_API_KEY")); err != nil {
        log.Fatal(err)
    }
}

func main() {
    c := creator.New()
    c.SetPageSize(creator.PageSizeA4)

    p := c.NewParagraph("Hello, World!")
    p.SetFontSize(24)
    if err := c.Draw(p); err != nil {
        log.Fatal(err)
    }

    if err := c.WriteToFile("hello.pdf"); err != nil {
        log.Fatal(err)
    }
}

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)
    }
}

Três coisas para notar. O bloco init() sumiu — sem chave, sem variável de ambiente. A construção usa opções em vez de mutar o builder. O texto vive dentro de uma linha e coluna em vez de ser um Paragraph solto que você desenha depois. A grade está fazendo o posicionamento; você não escolhe coordenadas.

Antes / Depois 2: tabela de itens de uma fatura

Tabelas são onde a API creator do unipdf alonga. Você constrói uma Table, chama SetColumnWidths com frações absolutas, monta células uma a uma com NewCell / SetContent e configura bordas e alinhamento de cada célula à mão.

Antes — unipdf:

table := c.NewTable(4)
table.SetColumnWidths(0.5, 0.15, 0.15, 0.2)

headerStyle := c.NewTextStyle()
headerStyle.Font, _ = model.NewStandard14Font("Helvetica-Bold")
headerStyle.FontSize = 11
headerStyle.Color = creator.ColorWhite

drawHeaderCell := func(text string) {
    cell := table.NewCell()
    cell.SetBackgroundColor(creator.ColorRGBFromHex("#1A237E"))
    cell.SetBorder(creator.CellBorderSideAll, creator.CellBorderStyleSingle, 0.5)

    p := c.NewStyledParagraph()
    chunk := p.Append(text)
    chunk.Style = headerStyle
    cell.SetContent(p)
}

for _, h := range []string{"Descrição", "Qtd.", "Unit.", "Total"} {
    drawHeaderCell(h)
}

for _, row := range items {
    for _, cellText := range row {
        cell := table.NewCell()
        cell.SetBorder(creator.CellBorderSideAll, creator.CellBorderStyleSingle, 0.3)

        p := c.NewParagraph(cellText)
        p.SetFontSize(11)
        cell.SetContent(p)
    }
}

if err := c.Draw(table); err != nil {
    log.Fatal(err)
}

Bordas, conteúdo por célula, loop que desenha o cabeçalho — tudo mecânico.

Depois — gpdf:

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(12, func(c *template.ColBuilder) {
        c.Table(
            []string{"Descrição", "Qtd.", "Unit.", "Total"},
            [][]string{
                {"Desenvolvimento frontend", "40 h", "R$ 750,00", "R$ 30.000,00"},
                {"Desenvolvimento backend",  "60 h", "R$ 750,00", "R$ 45.000,00"},
                {"Design de UI",             "20 h", "R$ 600,00", "R$ 12.000,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 são porcentagens da coluna em que a tabela vive, não frações absolutas da página. Coloque a mesma tabela dentro de r.Col(6, ...) e os percentuais continuam valendo: a tabela ocupa metade da linha e as colunas se redistribuem na proporção. A quebra de página é automática; se o corpo passar a margem inferior, o cabeçalho repete na próxima página sem você fiar nada.

Um detalhe específico. A Table do unipdf numa rodada de fatura de 100 linhas marca cerca de 8,6 ms por render na nossa suite. A do gpdf faz o mesmo trabalho em 108 µs — cerca de 80× mais rápido — porque o motor de layout mede cada linha uma vez e escreve páginas em uma única passada, em vez de materializar um DOM célula por célula. Numa fatura solta isso é invisível. Num batch em cron, muda se você precisa ou não de uma fila.

Para NF-e / DANFE / PDF/A-3 (Brasil), o lado de layout funciona com gpdf direto. Se precisar de timestamping para conformidade, vai pelo lado de gpdf.SignDocument com a opção RFC 3161 TSA.

Antes / Depois 3: texto com acentos sem a dança da composite font

unipdf suporta texto estendido (incluindo CJK), mas o caminho é verboso. Você constrói uma composite font a partir de um TTF em disco, define como font do style e passa por cada paragraph. Se quiser fallbacks, fia você mesmo.

Antes — unipdf:

font, err := model.NewCompositePdfFontFromTTFFile("NotoSans-Regular.ttf")
if err != nil {
    log.Fatal(err)
}

c := creator.New()
c.SetPageSize(creator.PageSizeA4)

style := c.NewTextStyle()
style.Font = font
style.FontSize = 14

p := c.NewStyledParagraph()
p.Append("Olá, mundo. Atenção: ção, ã, é, ô.").Style = style
if err := c.Draw(p); err != nil {
    log.Fatal(err)
}

c.WriteToFile("pt.pdf")

O TTF precisa existir no caminho indicado, em runtime, no host que roda o binário. As imagens de container têm que carregar a fonte. NewCompositePdfFontFromTTFFile precisa acontecer antes de qualquer chamada de desenho que use a fonte, então mora em algum lugar global ou é passada como dependência.

Depois — gpdf:

package main

import (
    _ "embed"
    "log"
    "os"

    "github.com/gpdf-dev/gpdf"
    "github.com/gpdf-dev/gpdf/document"
    "github.com/gpdf-dev/gpdf/template"
)

//go:embed NotoSans-Regular.ttf
var noto []byte

func main() {
    doc := gpdf.NewDocument(
        gpdf.WithPageSize(document.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
        gpdf.WithFont("Noto", noto),
        gpdf.WithDefaultFont("Noto", 14),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("Olá, mundo.")
            c.Text("Atenção: ção, ã, é, ô — sem escapes.")
            c.Text("Av. Paulista, 1578, São Paulo")
        })
    })

    data, _ := doc.Generate()
    if err := os.WriteFile("pt.pdf", data, 0o644); err != nil {
        log.Fatal(err)
    }
}

Três diferenças a observar. A fonte é bytes, não caminho — //go:embed compila ela no binário, então a imagem de runtime deixa de precisar de um diretório de fontes. A fonte é registrada uma vez no construtor; sem encadeamento de style por paragraph. E o subseteador TrueType do gpdf entende formatos cmap CJK (4, 6, 12) e codificação Identity-H, então o PDF de saída só carrega os glifos que você de fato usou. Uma fatura de 200 caracteres produz um subset de fonte de ~30 KB em vez de um embed completo de 4 MB.

Para CJK específico (Source Han Sans, IPAex Gothic, cadeias de fallback), há um post complementar sobre fontes japonesas.

Antes / Depois 4: cabeçalho em todas as páginas, número de página no rodapé

O padrão do unipdf é c.DrawHeader(fn) / c.DrawFooter(fn), ambos recebem um contexto com o block atual e o número da página. Os números vêm dos campos PageNum / TotalPages do contexto.

Antes — unipdf:

c.DrawHeader(func(block *creator.Block, args creator.HeaderFunctionArgs) {
    p := c.NewParagraph("ACME Ltda.")
    p.SetFontSize(12)
    p.SetPos(40, 30)
    block.Draw(p)
})

c.DrawFooter(func(block *creator.Block, args creator.FooterFunctionArgs) {
    p := c.NewParagraph(fmt.Sprintf("Página %d de %d", args.PageNum, args.TotalPages))
    p.SetFontSize(8)
    p.SetPos(0, 20)
    p.SetTextAlignment(creator.TextAlignmentCenter)
    block.Draw(p)
})

Cabeçalho / rodapé são blocks que você desenha em posições absolutas. Coordenada Y errada, margem errada — trabalho de pixel toda vez que muda o tamanho da página.

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) {
            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 que o motor de layout resolve depois da paginação. Cabeçalho e rodapé são árvores em si, não blocks que você posiciona à mão. O motor reserva espaço para eles em cada página automaticamente; se mudar o tamanho da página de A4 para Letter, nada mais precisa ser movido.

Antes / Depois 5: cifragem com AES-256

O par em que a foto de licenciamento aparece mais nítida. A cifragem do unipdf vai por model.PdfWriter, que conta como uso comercial e dispara o caminho de registro de licença. A do gpdf mora atrás de uma única opção funcional, e a implementação AES-256 (ISO 32000-2 Rev 6) está no core MIT de código aberto.

Antes — unipdf:

// Renderiza o conteúdo via creator e re-codifica com model.PdfWriter
// para anexar cifragem. O check de licença dispara aqui.
c := creator.New()
// ... desenhar conteúdo ...

var buf bytes.Buffer
if err := c.Write(&buf); err != nil {
    log.Fatal(err)
}

reader, err := model.NewPdfReader(bytes.NewReader(buf.Bytes()))
if err != nil {
    log.Fatal(err)
}

writer := model.NewPdfWriter()
encryptOpts := &model.EncryptOptions{Algorithm: model.RC4_128bit, Permissions: model.PermPrinting}
if err := writer.Encrypt([]byte("user-pwd"), []byte("owner-pwd"), encryptOpts); err != nil {
    log.Fatal(err)
}

for i := 1; i <= reader.NumPage; i++ {
    page, _ := reader.GetPage(i)
    writer.AddPage(page)
}

f, _ := os.Create("encrypted.pdf")
defer f.Close()
writer.Write(f)

Depois — gpdf:

doc := gpdf.NewDocument(
    gpdf.WithPageSize(document.A4),
    gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
    gpdf.WithEncryption(
        gpdf.AES256,
        "user-pwd",
        "owner-pwd",
        gpdf.PermPrinting|gpdf.PermCopyContent,
    ),
)

page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
    r.Col(12, func(c *template.ColBuilder) {
        c.Text("Confidencial.")
    })
})

data, _ := doc.Generate()
os.WriteFile("encrypted.pdf", data, 0o644)

Uma opção, AES-256 por padrão, sem passada de writer separada. O caminho de cifragem inteiro mora dentro do core MIT — mesmo módulo, mesmo go get. Mesma história para assinatura digital: gpdf.SignDocument(pdfBytes, signer, gpdf.WithTSA("http://timestamp.digicert.com")) pós-processa os bytes com timestamp PKCS#7 + RFC 3161, sem pacote extra e sem registro de chave.

Quão rápido fica?

Benchmarks de _benchmark/benchmark_test.go em um Apple M1 com Go 1.25. unipdf não está direto na nossa suite porque os termos de licença tornavam estranho distribuir o código de comparação; os números abaixo são o que coletamos no mesmo hardware contra as mesmas cargas.

Benchmarkgpdfunipdf*gofpdfMaroto v2
Página única13 µs~180 µs132 µs237 µs
Tabela 4×10 fatura108 µs~8,6 ms241 µs8,6 ms
Relatório 100 páginas683 µs~95 ms11,7 ms19,8 ms
Fatura CJK complexa133 µs~12 ms254 µs10,4 ms

* Os números do unipdf vêm de uma rodada separada no mesmo Apple M1 / Go 1.25, capturados por nós contra o último unipdf v3 no momento da escrita. Trate como aproximados; não fazem parte da nossa suite versionada.

A forma é a mesma da comparação com gofpdf: gpdf é cerca de 10–80× mais rápido nas cargas que as pessoas rodam de verdade. A 108 µs por página com tabela, um único core produz ~9.000 faturas por segundo. O ponto não é vangloriar — é que você pode parar de pensar se vai cachear ou enfileirar a geração de PDF. Gerar no caminho de request basta para quase tudo.

E as partes que gpdf não tem?

Se sua fatura de unipdf paga OCR, redaction ou parsing de PDF, esta migração não te leva até o fim. As opções honestas:

  • OCR. gpdf não faz OCR e dificilmente fará. Use Tesseract via gosseract, ou uma API de OCR hospedada. A geração fica no gpdf, o parsing fica no que você escolher.
  • Parsing / extração de texto. gpdf é só geração por design. Para cargas de leitura, pdfcpu cobre muitos casos comuns (Apache 2.0). Mantenha unipdf só para parsing e talvez consiga reduzir o número de assentos.
  • Criação de campos AcroForm. gpdf consegue achatar campos AcroForm existentes; ainda não consegue criar novos. Se você produz formulários para usuários preencherem em um visualizador, este é o gap que vai sentir. Está no roadmap.
  • Redaction. Não está no roadmap do gpdf. Redaction precisa de um renderer de verdade para saber o que apagar, que é uma arquitetura diferente da geração.

Para o caminho de geração, cifragem, assinatura, fontes e CJK — o destino real da maior parte das faturas de unipdf — a troca é completa.

FAQ

gpdf é um fork do unipdf? Não. gpdf é uma reimplementação limpa em Go puro. Wire format do PDF, motor de layout, subseteador TrueType, AES, PKCS#7 — tudo escrito do zero. Sem código compartilhado, sem linhagem compartilhada e sem risco de uma discussão de licenciamento dar errado porque nada foi copiado.

gpdf é mesmo MIT? Sem "AGPL sob certa condição"? Sim. A LICENSE do repositório é a licença MIT literal, sem anexos, sem cláusulas de campo de uso, sem recortes de tier comercial. Use em produtos fechados distribuíveis, embuta em SaaS comercial, mande dentro de appliances on-prem. A única obrigação é a nota de licença e copyright na sua distribuição.

E as dependências transitivas — tem copyleft escondido por baixo? O bloco require do go.mod do core do gpdf está vazio. Sem AGPL transitiva, sem GPL transitiva, sem nada transitivo. Verifique com go mod graph | grep gpdf depois do go get.

Tirar a chave de licença importa tanto assim? Para alguns times é tudo. A chave precisa morar no secret manager, ser rotacionada, ser auditada, ser incluída em cada imagem de container e não vazar em logs. Para um SaaS multi-tenant com centenas de pods, isso é uma superfície operacional de verdade. Eliminar a exigência apaga uma classe de incidentes.

Meu código atual de unipdf usa creator.Block.SetPos para posicionamento absoluto em todo lugar. gpdf tem equivalente? Tem — page.Absolute(x, y, fn) deixa você colocar uma sub-árvore numa coordenada explícita. Mas se seu código está estruturado em torno de manipulação de cursor, o modelo de motor de layout é uma mudança mental, não sintática. Leia o post sobre a grade de 12 colunas antes de estimar; código reescrito costuma ser mais curto que o original.

E se a UniDoc relicenciar o unipdf para MIT um dia? Você ganha mais uma opção. A aposta por trás do gpdf não é que unipdf vai ficar AGPL para sempre; é que uma licença que exige uma chamada de registro no startup e renovação por desenvolvedor no nível financeiro é um imposto que não precisa existir para a maioria das cargas. Mesmo se relicenciassem amanhã, a superfície operacional da chave continuaria lá até eles tirarem.

Experimentar gpdf

gpdf é uma biblioteca Go para gerar PDFs. Licença MIT, zero dependências externas, sem chave de licença, suporte CJK nativo.

go get github.com/gpdf-dev/gpdf

⭐ Star no GitHub · Leia os docs

Próximas leituras