Composables

PDF Annotations

Community

rcosteira79

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

Overview

Repository
rcosteira79/android-skills
Publisher
rcosteira79
Stars
174
Forks
21
Bundled files
0
License
MIT
Updated
Nov 10, 2026

Installation

tmp=$(mktemp -d)
git clone --depth 1 https://github.com/rcosteira79/android-skills.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/plugins/android-skills/skills/pdf-annotations" ~/.claude/skills/pdf-annotations
rm -rf "$tmp"

Instructions

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.

FAQ

What does PDF Annotations do?

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

Who made PDF Annotations?

rcosteira79 (a community member) publishes it in the rcosteira79/android-skills repository on GitHub.

Is PDF Annotations free?

Yes. PDF Annotations is open source, published by rcosteira79 under the MIT license.

What comes with PDF Annotations?

Just its instructions. Everything the agent needs is in one SKILL.md file.

Which agents can use PDF Annotations?

Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.

What are agent skills?

An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.

How are agent skills different from MCP servers?

An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.

How can my agent test my Android app?

Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.

You're subscribed

The next lesson lands in on Wednesday.