Platform PDF annotation & page-object editing
Android can natively edit PDF annotations since API 36.1 — before that the
platform story was render-only and the docs still say so: the Jetpack viewer
guide's PDF surface is PdfViewerFragment, which explicitly does not
support edit mode and hands annotation off to other apps via an
android.intent.action.ANNOTATE intent
[kb://android/develop/ui/views/layout/pdf/implement-pdf-viewer]. Do not
conclude "use a third-party library" for annotation work before applying the
version gate below.
The version gate (decide the entry point first)
Two parallel renderers expose the same editing surface
(android.graphics.pdf.component):
| Entry point |
Editing available |
Device requirement |
PdfRenderer.Page |
API 36.1+ |
platform release |
PdfRendererPreV.Page |
SDK extension 18 |
API 31+ with the PDF module updated (PreV itself needs extension 13) |
Gate at runtime with SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S)
[kb://android/guide/sdk-extensions/index]. Extension 18 reaches back to API 31
via mainline updates, so PdfRendererPreV is the branch that covers most
devices.
The workflow: open → edit → write
// The fd must be seekable — pipes/sockets are rejected at construction.
ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).use { fd ->
PdfRenderer(fd).use { renderer -> // or PdfRendererPreV on the ext-18 branch
renderer.openPage(0).use { page ->
// Only SUPPORTED annotation types are listed. The pairs are
// android.util.Pair — NO Kotlin destructuring (no component1/2);
// read them with .first / .second.
val existing = page.getPageAnnotations()
val staleId: Int? = existing.firstOrNull { it.second is FreeTextAnnotation }?.first
val highlight = HighlightAnnotation(boundsList) // List<RectF> quads
val id = page.addPageAnnotation(highlight)
check(id != -1) { "annotation rejected" } // add returns -1, it does NOT throw
highlight.color = newColor
page.updatePageAnnotation(id, highlight) // by id -> Boolean success
staleId?.let { page.removePageAnnotation(it) } // by id, not list position
}
// Write BEFORE close(), to a separate writable fd; the boolean is
// removePasswordProtection (IOException if true and the doc stays encrypted).
ParcelFileDescriptor.open(
dest,
ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE,
).use { out ->
renderer.write(out, false)
}
}
}
Contracts:
addPageAnnotation returns the annotation id, or -1 when the annotation
cannot be added — check the return; only null/UNKNOWN-type/already-attached
arguments throw (IllegalArgumentException).
- An annotation instance is single-attach document-wide: adding one that is
"already added in this page or some other page" throws. Construct a fresh
component object per add; don't reuse a removed one across pages.
- Ids, not list positions, drive
update/remove. getPageAnnotations()
returns only annotation types the API models, so a page with unsupported
annotations has gaps — the Pair.first id is the stable handle.
- One page open at a time — the pre-existing
PdfRenderer invariant
("you can have only a single page opened at any given time") still applies;
close the page before opening the next.
write() before close(), destination must be a distinct writable
seekable fd; IllegalStateException after close.
- The pairs are
android.util.Pair, not kotlin.Pair — Kotlin
destructuring (for ((id, a) in …)) does not compile against them; use
.first/.second.
Rendering what you edited
Annotation types render only when RenderParams.renderFlags includes them:
FLAG_RENDER_TEXT_ANNOTATIONS / FLAG_RENDER_HIGHLIGHT_ANNOTATIONS (and the
36.1 additions FLAG_RENDER_FREETEXT_ANNOTATIONS /
FLAG_RENDER_STAMP_ANNOTATIONS). An added stamp or free-text that "doesn't
show up" in your preview bitmap is usually a missing render flag, not a failed
add — re-render with the matching flags set via
RenderParams.Builder.setRenderFlags(...) and the render(bitmap, clip, transform, params) overload. The render-mode constants live on
RenderParams (and legacy PdfRenderer.Page) — PdfRendererPreV.Page
has none, so on the PreV branch write
RenderParams.Builder(RenderParams.RENDER_MODE_FOR_DISPLAY); qualifying it
via PdfRendererPreV.Page does not compile.
Component vocabulary (android.graphics.pdf.component)
HighlightAnnotation(List<RectF>) — quad list, one rect per highlighted run.
FreeTextAnnotation(RectF, String) — bounds + text; text/background colors
settable.
StampAnnotation(RectF) — a container: only PdfPagePathObject,
PdfPageTextObject, or PdfPageImageObject children (addObject), per the
class contract.
- Page objects also attach directly to the page (
addPageObject /
getPageObjects / updatePageObject / removePageObject — same id-based
contract as annotations), and carry a transform matrix
(PdfPageObject.setMatrix/transform). PdfPagePathObject stroke defaults
to opaque black in sRGB.
Page.getTopPageObjectAtPosition(PointF, …) hit-tests the topmost object —
use it for tap-to-select before update/remove.
Out of scope (route elsewhere)
Creating PDFs from scratch (PdfDocument canvas API), form filling
(applyEdit, API 35), text extraction/search, and embedding a viewer UI
(PdfViewerFragment — display-only
[kb://android/develop/ui/views/layout/pdf/implement-pdf-viewer]) are separate
surfaces; third-party SDKs remain the answer only below the version gate or
for annotation types the component API doesn't model.
1---2name: pdf-annotations3description: Use when reading, adding, updating, or removing PDF annotations (highlight, free-text, stamp) or page objects (path/text/image) with Android's platform PDF APIs, or when saving those edits back to disk — the editing surface added in API 36.1 (PdfRenderer.Page) and SDK extension 18 (PdfRendererPreV.Page on API 31+), android.graphics.pdf.component. Covers the version gate, the open-edit-write workflow, and the id/render-flag/save contracts. Not for display-only PdfViewerFragment work, PdfDocument creation-from-scratch, form filling, or third-party PDF SDKs (iText, PDFBox, PSPDFKit).4---56# Platform PDF annotation & page-object editing78Android can natively edit PDF annotations since API 36.1 — before that the9platform story was render-only and the docs still say so: the Jetpack viewer10guide's PDF surface is `PdfViewerFragment`, which explicitly does **not**11support edit mode and hands annotation off to other apps via an12`android.intent.action.ANNOTATE` intent13[kb://android/develop/ui/views/layout/pdf/implement-pdf-viewer]. Do not14conclude "use a third-party library" for annotation work before applying the15version gate below.1617## The version gate (decide the entry point first)1819Two parallel renderers expose the same editing surface20(`android.graphics.pdf.component`):2122| Entry point | Editing available | Device requirement |23|---|---|---|24| `PdfRenderer.Page` | API 36.1+ | platform release |25| `PdfRendererPreV.Page` | SDK extension 18 | API 31+ with the PDF module updated (PreV itself needs extension 13) |2627Gate at runtime with `SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S)`28[kb://android/guide/sdk-extensions/index]. Extension 18 reaches back to API 3129via mainline updates, so `PdfRendererPreV` is the branch that covers most30devices.3132## The workflow: open → edit → write3334```kotlin35// The fd must be seekable — pipes/sockets are rejected at construction.36ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).use { fd ->37 PdfRenderer(fd).use { renderer -> // or PdfRendererPreV on the ext-18 branch38 renderer.openPage(0).use { page ->39 // Only SUPPORTED annotation types are listed. The pairs are40 // android.util.Pair — NO Kotlin destructuring (no component1/2);41 // read them with .first / .second.42 val existing = page.getPageAnnotations()43 val staleId: Int? = existing.firstOrNull { it.second is FreeTextAnnotation }?.first4445 val highlight = HighlightAnnotation(boundsList) // List<RectF> quads46 val id = page.addPageAnnotation(highlight)47 check(id != -1) { "annotation rejected" } // add returns -1, it does NOT throw4849 highlight.color = newColor50 page.updatePageAnnotation(id, highlight) // by id -> Boolean success51 staleId?.let { page.removePageAnnotation(it) } // by id, not list position52 }53 // Write BEFORE close(), to a separate writable fd; the boolean is54 // removePasswordProtection (IOException if true and the doc stays encrypted).55 ParcelFileDescriptor.open(56 dest,57 ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE,58 ).use { out ->59 renderer.write(out, false)60 }61 }62}63```6465Contracts:6667- **`addPageAnnotation` returns the annotation id, or `-1` when the annotation68 cannot be added — check the return; only null/`UNKNOWN`-type/already-attached69 arguments throw** (`IllegalArgumentException`).70- **An annotation instance is single-attach document-wide**: adding one that is71 "already added in this page or some other page" throws. Construct a fresh72 component object per add; don't reuse a removed one across pages.73- **Ids, not list positions, drive `update`/`remove`.** `getPageAnnotations()`74 returns only annotation types the API models, so a page with unsupported75 annotations has gaps — the `Pair.first` id is the stable handle.76- **One page open at a time** — the pre-existing `PdfRenderer` invariant77 ("you can have only a single page opened at any given time") still applies;78 close the page before opening the next.79- **`write()` before `close()`**, destination must be a distinct writable80 seekable fd; `IllegalStateException` after close.81- **The pairs are `android.util.Pair`, not `kotlin.Pair`** — Kotlin82 destructuring (`for ((id, a) in …)`) does not compile against them; use83 `.first`/`.second`.8485## Rendering what you edited8687Annotation types render only when `RenderParams.renderFlags` includes them:88`FLAG_RENDER_TEXT_ANNOTATIONS` / `FLAG_RENDER_HIGHLIGHT_ANNOTATIONS` (and the8936.1 additions `FLAG_RENDER_FREETEXT_ANNOTATIONS` /90`FLAG_RENDER_STAMP_ANNOTATIONS`). An added stamp or free-text that "doesn't91show up" in your preview bitmap is usually a missing render flag, not a failed92add — re-render with the matching flags set via93`RenderParams.Builder.setRenderFlags(...)` and the `render(bitmap, clip,94transform, params)` overload. The render-mode constants live on95**`RenderParams`** (and legacy `PdfRenderer.Page`) — `PdfRendererPreV.Page`96has none, so on the PreV branch write97`RenderParams.Builder(RenderParams.RENDER_MODE_FOR_DISPLAY)`; qualifying it98via `PdfRendererPreV.Page` does not compile.99100## Component vocabulary (android.graphics.pdf.component)101102- `HighlightAnnotation(List<RectF>)` — quad list, one rect per highlighted run.103- `FreeTextAnnotation(RectF, String)` — bounds + text; text/background colors104 settable.105- `StampAnnotation(RectF)` — a container: **only** `PdfPagePathObject`,106 `PdfPageTextObject`, or `PdfPageImageObject` children (`addObject`), per the107 class contract.108- Page objects also attach directly to the page (`addPageObject` /109 `getPageObjects` / `updatePageObject` / `removePageObject` — same id-based110 contract as annotations), and carry a transform matrix111 (`PdfPageObject.setMatrix`/`transform`). `PdfPagePathObject` stroke defaults112 to opaque black in sRGB.113- `Page.getTopPageObjectAtPosition(PointF, …)` hit-tests the topmost object —114 use it for tap-to-select before update/remove.115116## Out of scope (route elsewhere)117118Creating PDFs from scratch (`PdfDocument` canvas API), form filling119(`applyEdit`, API 35), text extraction/search, and embedding a viewer UI120(`PdfViewerFragment` — display-only121[kb://android/develop/ui/views/layout/pdf/implement-pdf-viewer]) are separate122surfaces; third-party SDKs remain the answer only below the version gate or123for annotation types the component API doesn't model.