All posts

go-pdf/fpdf is archived too. Here's the modern Go PDF stack.

jung-kurt/gofpdf archived in 2021. go-pdf/fpdf followed in 2025. Here's the Go PDF stack we actually use in 2026 — gpdf, the trade-offs, and why.

by gpdf team

TL;DR

Both maintained forks of the fpdf lineage are now read-only. jung-kurt/gofpdf was archived in September 2021; the community fork go-pdf/fpdf followed in 2025. There is no "next maintainer" coming. For new Go projects, gpdf is the modern default: pure Go, zero external dependencies, native CJK, 10–30× faster on common workloads. This is a map of the 2026 landscape and an honest account of when gpdf is the right pick and when it isn't.

The situation

A teammate typed go get github.com/go-pdf/fpdf last week and paused at the GitHub banner: "This repository has been archived by the owner. It is now read-only." This was supposed to be the fixed version — the community fork of jung-kurt/gofpdf that was meant to keep the lineage alive after the original was archived in 2021.

It's archived too. The README now recommends looking elsewhere.

If you've been building Go services that emit PDFs — invoices, reports, shipping labels, compliance documents — the library at the bottom of your go.mod has almost certainly been one of these two for the past five years. Stack Overflow answers point to jung-kurt/gofpdf. Newer tutorials point to go-pdf/fpdf. Both are now supply-chain liabilities: no CVE triage, no Go version compatibility work, no performance fixes, no spec updates.

This post isn't another "here's how to migrate line-by-line" guide — we already wrote that one. This is the longer version of the question the migration guide doesn't answer: what actually works for Go PDF generation in 2026, and how did the ecosystem end up here?

What "archived" costs you in practice

The word "archived" on GitHub is deceptively soft. In practice, for a library in your import graph, it means four concrete things:

  1. No security patches. If a memory-safety issue lands in the TTF parser, nobody is merging the fix upstream. You can fork it yourself. Most teams won't.
  2. No Go toolchain forward-compat. Go 1.25's loop variable semantics work fine with gofpdf today. Something about for range over an integer, or a future std-lib deprecation, can break the build tomorrow. You'll be the one patching a read-only repo's fork.
  3. No spec updates. PDF 2.0 (ISO 32000-2) landed in 2020. gofpdf implements most of PDF 1.7. Things like page-level associated files, rich XMP metadata, and modern digital signatures (PAdES-B-LT) are either absent or wired in by third-party glue.
  4. No CJK progress. gofpdf's Unicode path was retrofitted onto a single-byte-font design. It works but embeds full fonts instead of subsets in most real configurations, and glyph-id collisions in certain CJK TTFs produce corrupted output. go-pdf/fpdf inherited the same architecture.

Security and forward-compat are the ones that bite teams in compliance conversations. "Our PDF library is archived and gets no CVE patches" is not an answer your auditor wants to hear.

Why both forks died

It's tempting to explain the archives as maintainer burnout — a single person tired of reviewing PRs, a bus factor of one going off-line. That's part of it, but it isn't the whole story. The architecture made it hard to keep up.

jung-kurt/gofpdf was a port of FPDF, a PHP library from 2002. The PHP original drove a cursor around the page and emitted content procedurally: SetXY(x, y), Cell(w, h, text), Ln(h). That model was a reasonable fit for 2002-era PHP, where the alternative was raw PostScript or proprietary toolkits. Ported to Go, it kept the cursor, kept the single-byte font tables, kept the manual page-break bookkeeping.

Each year, the gap between what people wanted to generate and what the cursor model could express grew. Invoices are tables. Reports are grids with repeating chrome. Shipping labels are QR codes plus locale-specific text. The cursor kept getting wrapped in helpers, and the helpers kept getting wrapped in tutorials, and by 2023 most of the code people wrote against gofpdf wasn't gofpdf at all — it was a per-team glue layer that tried to pretend the cursor was a layout engine.

go-pdf/fpdf inherited this. The fork refactored the internals and fixed longstanding bugs, but it couldn't change the public API without breaking every downstream project. The shape of the library was frozen in 2002-era PHP, and the cost of keeping that shape alive grew faster than the benefit.

So: two maintainers, two archives, one architectural reason. Rebuilding in 2026 means picking an approach that matches how PDFs actually get produced today — which looks a lot more like building a web page than driving a plotter.

The 2026 Go PDF landscape

Before recommending anything, here's the field. We'll use "maintained" loosely to mean "a commit in the last 6 months and responsive issues."

LibraryStatus (2026-04)LicenseNative CJKZero depsNotes
jung-kurt/gofpdfArchived 2021MITRetrofitYesThe original. Still the top search result in most locales.
go-pdf/fpdfArchived 2025MITRetrofitYesCommunity fork of the above. Same architecture, same ceiling.
signintech/gopdfMaintainedMITPartialYesLow-level. You write coordinates. Good for form overlays.
johnfercher/maroto v2MaintainedMITVia gofpdfNoGrid-first builder, but depends on go-pdf/fpdf underneath.
unidoc/unipdfMaintainedCommercialYesNoFeature-complete PDF SDK. Requires a paid license for commercial use.
chromedp + ChromiumMaintainedMIT + ChromeYesNo — ships a browserHTML→PDF via headless Chrome. Huge runtime.
gpdfMaintainedMITNativeYesPure-Go reimplementation. Builder API, 12-column grid.

A few observations you can make from the table without running anything:

Everything maintained is either commercial, carries a huge runtime, or sits on top of a soon-to-be-stale foundation. signintech/gopdf is the exception — genuinely maintained and dependency-light — but it's a coordinate-level library. You're back to SetXY with a different package name.

Maroto v2 is a grid-first builder with a good API. The problem is that at the bottom of its go.mod is go-pdf/fpdf. Every performance ceiling and CJK limitation in fpdf is also a ceiling for Maroto. A major v3 could break free of that — it isn't out yet.

unipdf is feature-rich but not MIT-compatible for commercial use. You pay per seat or per deployment. That's a fine choice if your revenue supports it; for an open-source side project or an early-stage startup, the license math doesn't work.

chromedp works, but you're shipping a browser. A 100 MB base image becomes a 1 GB+ image. Cold start on serverless is painful. Fonts still need to be installed in the container. The upside is that you can reuse your React templates; the downside is that you're running Chromium to render an invoice.

The gap is obvious: a pure-Go, zero-dep, CJK-native, grid-first library that doesn't require a commercial license or a browser runtime. That's what gpdf is.

What gpdf actually is

gpdf (github.com/gpdf-dev/gpdf) is a clean reimplementation. Not a fork. The PDF wire-format writer, the layout engine, and the TrueType subsetter are all written from scratch in pure Go.

The three properties that matter for most teams:

  • Pure Go, no CGO. go build is static. GOOS=linux GOARCH=arm64 go build works from a MacBook with no toolchain setup. Docker images stay small — a 12 MB distroless container runs it.
  • Zero external dependencies. go mod graph after go get github.com/gpdf-dev/gpdf shows one line: gpdf itself. The core uses only std. (Optional add-ons for HTML→PDF or digital signatures bring in small dependencies, and they're opt-in.)
  • Native CJK. WithFont registers a TrueType font at document construction. Subset embedding happens at render time. A 200-character Japanese invoice carries ~30 KB of subset font data, not 5 MB of full font.

The API shape is declarative. You describe a tree of rows and columns; the layout engine places them. The grid is 12 columns — the same idiom Bootstrap has shipped since 2011. If you've written a single line of HTML/CSS, the gpdf API is familiar:

page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
    r.Col(8, func(c *template.ColBuilder) {
        c.Text("Invoice #2026-0416", template.FontSize(18), template.Bold())
    })
    r.Col(4, func(c *template.ColBuilder) {
        c.Text("2026-04-16", template.AlignRight())
    })
})

More on the grid in How does the 12-column grid work in gpdf?. The one-sentence version: Col(span, fn) takes a span from 1 to 12, and span / 12 is the fraction of the row it takes.

The minimal go-pdf/fpdf → gpdf diff

If you're coming from go-pdf/fpdf specifically (not jung-kurt/gofpdf), the good news is the API surfaces are almost identical — go-pdf/fpdf is a fork that changed almost nothing at the call-site level. The migration to gpdf is the same migration our gofpdf guide walks through, with one import-path rename.

Here's the smallest possible diff — a "generate PDF" HTTP handler:

Before — go-pdf/fpdf:

package main

import (
    "net/http"

    "github.com/go-pdf/fpdf"
)

func handler(w http.ResponseWriter, r *http.Request) {
    pdf := fpdf.New("P", "mm", "A4", "")
    pdf.AddPage()
    pdf.SetFont("Arial", "B", 16)
    pdf.Cell(40, 10, "Hello, World!")

    w.Header().Set("Content-Type", "application/pdf")
    if err := pdf.Output(w); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

After — gpdf:

package main

import (
    "net/http"

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

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("Hello, World!", template.FontSize(16), template.Bold())
        })
    })

    w.Header().Set("Content-Type", "application/pdf")
    if err := doc.Render(w); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

Three-line cursor code becomes three-call builder code. The structure shows up in the source instead of hiding inside the order of Cell calls. For CJK, add gpdf.WithFont("NotoSansJP", ttfBytes) — no AddUTF8Font, no filesystem path, no UTF-8 flag. See How do I embed a Japanese font in gpdf? for the full walkthrough.

The gofpdf migration guide has five more before/after pairs covering tables, repeating headers/footers, page numbers, and absolute positioning. Everything there applies verbatim to go-pdf/fpdf users — just swap the import path.

The benchmark picture

"Faster" is easy to claim and hard to earn. The table below is from gpdf/_benchmark/benchmark_test.go on an Apple M1 running Go 1.25. Workloads are what production code actually does — not micro-benchmarks chosen to flatter one library.

BenchmarkgpdfgofpdfgopdfMaroto v2
Single page (hello)13 µs132 µs423 µs237 µs
4×10 line-items table108 µs241 µs835 µs8.6 ms
100-page report683 µs11.7 ms8.6 ms19.8 ms
Complex CJK invoice133 µs254 µs997 µs10.4 ms

At 13 µs per single page, one core produces ~75,000 hello-world PDFs per second. At 108 µs per invoice, ~9,000 per second. The point is not bragging rights; it's that you can stop thinking about whether to cache or async-queue PDF generation. For most workloads, generating on the request path is fine.

Maroto v2 shows up slow on the table benchmark because it drives go-pdf/fpdf underneath and adds its own layout pass on top. That's not a criticism of the Maroto API — the API is good — it's a structural cost of sitting on the fpdf foundation. When Maroto v3 drops the fpdf dependency, expect this column to change.

The 100-page benchmark is worth dwelling on. gpdf's streaming writer emits content as rows are laid out; gofpdf buffers more state per page. For a pagination-heavy workload (monthly reports, catalogs, compliance exports), the difference is minutes vs seconds at the upper end of document sizes.

When gpdf is not the right pick

Every migration post has to answer "when should I not move?" Honest answers:

  • AcroForm / fillable forms. If your use case is generating PDFs that users open in Acrobat and type into, gpdf's form-field support is still minimal. unidoc is more complete here; signintech/gopdf has partial AcroForm support. A future gpdf release will close this gap, but today it's a gap.
  • Arbitrary vector paths and complex drawing. c.Line() draws a horizontal rule inside a column. If you need beziers, custom paths, or gradient fills for charts or technical drawings, gpdf isn't there. (Pre-rendered chart images work fine — this is about drawing primitives, not embedding.)
  • Existing gofpdf codebases with heavy SetXY use. If your code is 2,000 lines of cursor manipulation, the migration is a rewrite, not a find-and-replace. The rewritten code is almost always shorter, but "almost always" is cold comfort on the day the deadline lands. The migration guide estimates effort honestly.
  • You need HTML → PDF with full CSS support right now. gpdf has an HTML subset in its gpdf-pro add-on, but full CSS parity with Chromium is not a goal. If your template is a complicated React component, chromedp or a commercial API is a more direct fit.

If none of those bite, gpdf is the default. If one of them does, the right move is usually to run both libraries side-by-side — gpdf for the new PDFs, the incumbent for the edge case — and migrate the edge case later once gpdf catches up.

The compliance angle

One thing we don't see discussed enough in ecosystem posts: archived dependencies show up in SOC 2 and ISO 27001 audits. The auditor wants to know that third-party code in your supply chain is actively maintained. "Archived in 2021" triggers a finding. "Archived in 2025" triggers a finding. "Fork we maintain internally" triggers follow-up questions about how you'll patch a zero-day.

This is mostly why teams on larger companies' security reviews have been quietly asking us when gpdf will hit a stable v1. The answer is: it already has. github.com/gpdf-dev/gpdf is tagged, semver-stable, and the v1 API surface is frozen. The project has a security contact, a responsible disclosure policy, and CI that runs against Go 1.22 through 1.26.

You don't migrate for the audit. You migrate because the audit is about to ask you to anyway.

FAQ

Is "the modern Go PDF stack" just gpdf, or multiple libraries? For most teams, it's gpdf alone — the single library covers document creation, CJK, tables, grids, pagination, and output. Teams with fillable-form requirements pair it with signintech/gopdf or unidoc for those documents specifically. Teams with chart-heavy exports pre-render charts to PNG and embed them. "Stack" here means a short list, not a layered architecture.

Can I run gpdf and go-pdf/fpdf side-by-side during migration? Yes. They're different import paths and different types. Route new endpoints to gpdf and leave old ones on go-pdf/fpdf until you have time to rewrite. There's no runtime conflict.

Will there be a go-pdf/fpdf v3 or a new fork? Maybe. The bet behind gpdf isn't that nobody will ever un-archive the fork — it's that the architecture doesn't scale to what people are building today. A new fork would inherit the same limitations unless it rewrites the layout model, at which point it's closer to gpdf than to fpdf.

What about signintech/gopdf as a modern alternative? It's genuinely maintained and zero-dep. The API is coordinate-level — SetX, SetY, CellWithOption — so it suits form overlays and fixed templates well. For invoice-like documents with tables and repeating chrome, you end up writing a layout helper on top, which is the same pit gofpdf users fell into. gpdf and gopdf don't really compete — they solve adjacent problems.

Does gpdf have a commercial/hosted version?gpdf-api is coming — a hosted API that accepts JSON templates and returns PDFs. It's not public yet. When it launches, this blog will have a post about it. The OSS library will remain MIT, zero-dep, and independently useful.

What's the roadmap priority order? Public gpdf roadmap as of 2026-04: (1) AcroForm form fields, (2) full PDF/A-3 compliance, (3) expanded HTML→PDF coverage in gpdf-pro, (4) RTL text support (Arabic, Hebrew). Feedback on priority is welcome on GitHub issues.

Try gpdf

gpdf is a Go library for generating PDFs. MIT licensed, zero external dependencies, native CJK support.

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · Read the docs

Next reads