← Back to writing

Reading 'native until you need text' as a Pure Go PDF author

A native developer escaped to a WebView at the text layer. Here is why I refused the same escape route while shipping a Pure Go PDF library.

This morning's Hacker News surfaced a post titled "Native all the way, until you need text" — a SwiftUI/AppKit developer's confession that Markdown-rich chat UI on Apple's text APIs eventually became impossible, and they fell back to a WebView for the text-heavy parts. Reading it, I kept seeing the same landscape I see while writing PDFs in Pure Go.

Text is the farthest corner of every runtime

The author's core observation is that native SDKs are strong at rectangles, colors, and animation, but consistently weak the moment arbitrary text layout shows up. That isn't an Apple-specific story — it might be the shape of nearly every graphics runtime in production. Browsers won this corner because they spent two decades occupying the last citadel of typography (CSS, font metrics, line breaking, complex script shaping) and turned themselves into the most general-purpose text engine you can ship inside another app.

When I started writing gpdf as a Pure Go, zero-dependency library, the hardest problem at the end of the project was, predictably, PDF text. TrueType and OpenType font subsetting, CJK glyph tables, glyph variant selection, embedded font compression. PDF object serialization and FlateDecode are spec-and-done; text is spec-plus-everything-else.

That gap matters more in 2026 than it did five years ago. PDFs are read by humans, and increasingly by LLM agents downstream-parsing them as text. A library that emits visually plausible CJK but loses bidirectional or extraction-correct text isn't producing a valid PDF — it's producing a PDF that looks fine in a viewer and silently sheds semantic content the moment another program tries to read it back. I wanted gpdf-generated invoices to survive being re-read by a different agent six months later without losing the customer's name. That dual-audience pressure is what pushed me past the easy compromise.

People who hit this wall usually escape in one of two directions

The article's author chose the first escape. Go native as far as you can, then switch to a WebView at the text layer. It is a defensible call — running CSS on top of WebKit is faster than re-implementing worldwide typography inside a closed runtime.

The other escape is narrowing the problem surface itself. That is the path I took with gpdf. Rendering arbitrary Markdown in arbitrary fonts is unreachable in Pure Go. But "generate a PDF" is a closed problem: the calling code decides the font set and the character set. Once input becomes controllable, text suddenly becomes tractable. That is the deepest layer of the decision I wrote about in why I chose Pure Go for the PDF library.

Half of the work was still text

Around the gpdf v1.0 release, I noticed that the code mass was lopsided: lines, rectangles, image drawing, and the PDF object model combined felt comparable in weight to the font and text-layout code alone. In my own experience, I think I spent roughly twice as long on CJK font embedding as on line drawing.

The skew has a structural reason. Every other PDF primitive maps almost directly onto a numeric instruction stream — the one-letter operators in the content stream are short, regular, and well-specified. Text isn't. Each piece of text requires a font subset to be computed, a CMap to be written, glyph widths to be correct, and the /Encoding and /ToUnicode entries to round-trip cleanly so that downstream text-extraction recovers what the document actually says. That last requirement is the one that drags everything else into a wider scope.

The moment the SwiftUI developer reached for WebView and the moment I refused to reach for CGO and harfbuzz were probably the same temperature of decision. The direction was inverted — he concluded "this isn't a native problem anymore," and I concluded "this is exactly where Pure Go needs to keep walking." Which one is right depends on distribution shape and audience. His app only needs to run on a user's Mac. gpdf's value depends on single-binary distribution, which evaporates the moment CGO drags in an external library.

Common questions

Wouldn't CGO + harfbuzz have solved everything?

It would have. harfbuzz is one of the most complete text-shaping engines in existence — it absorbs CJK ligatures, Indic complex shaping, and the rest. I didn't reach for it because the Pure Go single-binary story collapses the instant CGO enters the dependency graph. go build for linux/amd64, darwin/arm64, and windows/amd64 in one shot stops being free. I traded text quality for distribution simplicity, and for a micro SaaS that runs as a sidecar binary, the trade still feels right.

The trade-off isn't free, and I've audited it twice when users hit specific issues — once on a Korean-language glyph variant, once on tighter line spacing in a benchmark against a CGO-backed competitor. Both times the single-binary advantage outweighed the text-quality gap, but it survived as a deliberate concession rather than as a default. The day a paying customer can't accept that gap, the answer is a gpdf-cgo opt-in build sitting next to the Pure Go one, not an abandonment of the Pure Go path.

Other notable reads today

Next time I open gpdf's text-shaping code, I'll probably remember the morning a SwiftUI developer reached for WebView instead.

© 2026 nadai · JapanSet in Instrument Serif · Inter