PDF layers,
for PDFium
PDF has had layers since 2001 — optional content groups, in the specification's words. CAD exports, multilingual artwork, maps with switchable overlays and watermarked drafts all use them. Almost no viewer outside Acrobat lets you touch them.
PDFium is the reason why, and it is also the fix. It renders PDFs in Chrome, Edge and most of the embedded viewers on the web, and it has understood optional content internally for years — it just never exposed it. There is no fpdf_ocg.h. pdfium-ocg adds that API and demonstrates it end to end: an eight-patch series against upstream PDFium, a port onto EmbedPDF's WebAssembly build, and a working Layers panel you can click through in a browser.
Labels (French) switches English off by itself — the document declares the two a /RBGroups radio pair, and the panel honours it. The headings are the document's own /Order tree, and nothing here writes to the file: visibility is view state.Try it
No install and no clone: the WebAssembly build runs in your browser.
The live demo opens a ten-layer site plan in the EmbedPDF viewer, compiled against the patched PDFium. Open the layers tab in the sidebar, then:
- Uncheck a layer. The drawing repaints without it. Nothing is written to the file, so this works on a read-only PDF.
- Tick
Labels (French).Labels (English)switches itself off, because the document declares the two a radio pair. - Draw something, then hide the layer you drew it on. Pick a layer, switch to Shapes and draw a rectangle: it lands on that layer and hides with it. This is the part that matters most — almost nobody hand-authors optional content, so an annotation is usually the only thing that will ever be on a layer.
- Select an annotation and press the layers button on its toolbar for a dropdown of every layer — pick one to move it there, or
No layerto take it off layers entirely. - Delete a layer. It asks what should happen to what is drawn on it. There is no safe default: content still naming a removed layer renders visible, so silently keeping it would reveal the very thing you meant to delete.
- Drag a row. Reordering rewrites the document's
/Order; drag a heading and its children move with it.

What the patches do
Eight patches, each a self-contained change with its own tests, written to be reviewed in order.
| # | Patch | What it adds |
|---|---|---|
| 1 | Honour /OC on annotations | A spec-compliance fix with no API change |
| 2 | Enumerate and query | FPDFDoc_GetOCGCount, FPDFDoc_GetOCG, FPDFOCG_GetName, FPDFOCG_GetVisibility |
| 3 | Runtime toggle | FPDFOCG_SetVisibility, FPDFOCG_ResetVisibility — the headline |
| 4 | The /Order tree | FPDFDoc_GetOCGOrder, FPDFOCGNode_* — what a Layers panel needs to draw itself |
| 5 | /Locked and /RBGroups | FPDFOCG_IsLocked, FPDFDoc_GetOCGRadioGroup* |
| 6 | Authoring | FPDFDoc_AddOCG, FPDFAnnot_GetOCG / FPDFAnnot_SetOCG |
| 7 | Editing | FPDFDoc_RemoveOCG, FPDFOCGNode_MoveChild, FPDFPageObj_GetOCG / FPDFPageObj_SetOCG |
| 8 | Tree shaping | FPDFOCGNode_AddGroup, FPDFOCGNode_MoveChildTo — headings and re-parenting |
Patch 1 is the one to read first if you only read one. It is not part of the API work at all: PDFium filters annotations on the Hidden, Print and NoView flags and nothing else, so an annotation on a hidden layer draws anyway, although ISO 32000-1 §12.5.2 says /OC shall be evaluated before drawing. That is a plain bug that predates this work, and it is fixed on its own with its own test.
The state of it
At the tip of the series, 934 embedder tests and 1097 unit tests pass, and PDFium's api_check.py and git cl format are clean.
Nothing has been submitted upstream yet. A Google CLA comes first, and there is one open design question worth settling on the mailing list before review: whether FPDFOCG_SetVisibility should refuse to change a /Locked group, or report the lock and let the embedder decide. This series takes the second position; the repository also holds a working implementation of the first, so the discussion can be about code.
Prior art: PDFium bug 1136, “Public API for optional contents groups”, filed in 2018 and still open. A maintainer called it feasible; the reporter narrowed the request to getting each group's count, name and state, and setting the state. That is patches 2 and 3.
Building it
scripts/bootstrap.sh clones the upstream trees at their pinned base commits and replays the repository's patches onto each. It fetches and patches; it deliberately does not build.
./scripts/bootstrap.sh # everything
./scripts/bootstrap.sh pdfium # just the upstream series
./scripts/bootstrap.sh v2 # just the v2 viewer (the complete one)
The patches cover two EmbedPDF lines, because they are different products with the same name. v2, the @embedpdf/snippet package people ship today, is where the layers work is finished, and the demo is a build of it. v3 is the ground-up rewrite, still catching up with v2; the layers work there is equivalent but sits on a different architecture.
The README has the full build instructions for PDFium, the WebAssembly runtime and the viewer, and PLAN.md has the design notes, including the alternatives that were rejected and why.