$npx -y skills add rcosteira79/android-skills --skill pdf-annotationsUse 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 1
| 1 | # Platform PDF annotation & page-object editing |
| 2 | |
| 3 | Android can natively edit PDF annotations since API 36.1 — before that the |
| 4 | platform story was render-only and the docs still say so: the Jetpack viewer |
| 5 | guide's PDF surface is `PdfViewerFragment`, which explicitly does **not** |
| 6 | support edit mode and hands annotation off to other apps via an |
| 7 | `android.intent.action.ANNOTATE` intent |
| 8 | [kb://android/develop/ui/views/layout/pdf/implement-pdf-viewer]. Do not |
| 9 | conclude "use a third-party library" for annotation work before applying the |
| 10 | version gate below. |
| 11 | |
| 12 | ## The version gate (decide the entry point first) |
| 13 | |
| 14 | Two parallel renderers expose the same editing surface |
| 15 | (`android.graphics.pdf.component`): |
| 16 | |
| 17 | | Entry point | Editing available | Device requirement | |
| 18 | |---|---|---| |
| 19 | | `PdfRenderer.Page` | API 36.1+ | platform release | |
| 20 | | `PdfRendererPreV.Page` | SDK extension 18 | API 31+ with the PDF module updated (PreV itself needs extension 13) | |
| 21 | |
| 22 | Gate at runtime with `SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S)` |
| 23 | [kb://android/guide/sdk-extensions/index]. Extension 18 reaches back to API 31 |
| 24 | via mainline updates, so `PdfRendererPreV` is the branch that covers most |
| 25 | devices. |
| 26 | |
| 27 | ## The workflow: open → edit → write |
| 28 | |
| 29 | ```kotlin |
| 30 | // The fd must be seekable — pipes/sockets are rejected at construction. |
| 31 | ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).use { fd -> |
| 32 | PdfRenderer(fd).use { renderer -> // or PdfRendererPreV on the ext-18 branch |
| 33 | renderer.openPage(0).use { page -> |
| 34 | // Only SUPPORTED annotation types are listed. The pairs are |
| 35 | // android.util.Pair — NO Kotlin destructuring (no component1/2); |
| 36 | // read them with .first / .second. |
| 37 | val existing = page.getPageAnnotations() |
| 38 | val staleId: Int? = existing.firstOrNull { it.second is FreeTextAnnotation }?.first |
| 39 | |
| 40 | val highlight = HighlightAnnotation(boundsList) // List<RectF> quads |
| 41 | val id = page.addPageAnnotation(highlight) |
| 42 | check(id != -1) { "annotation rejected" } // add returns -1, it does NOT throw |
| 43 | |
| 44 | highlight.color = newColor |
| 45 | page.updatePageAnnotation(id, highlight) // by id -> Boolean success |
| 46 | staleId?.let { page.removePageAnnotation(it) } // by id, not list position |
| 47 | } |
| 48 | // Write BEFORE close(), to a separate writable fd; the boolean is |
| 49 | // removePasswordProtection (IOException if true and the doc stays encrypted). |
| 50 | ParcelFileDescriptor.open( |
| 51 | dest, |
| 52 | ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE, |
| 53 | ).use { out -> |
| 54 | renderer.write(out, false) |
| 55 | } |
| 56 | } |
| 57 | } |
| 58 | ``` |
| 59 | |
| 60 | Contracts: |
| 61 | |
| 62 | - **`addPageAnnotation` returns the annotation id, or `-1` when the annotation |
| 63 | cannot be added — check the return; only null/`UNKNOWN`-type/already-attached |
| 64 | arguments throw** (`IllegalArgumentException`). |
| 65 | - **An annotation instance is single-attach document-wide**: adding one that is |
| 66 | "already added in this page or some other page" throws. Construct a fresh |
| 67 | component object per add; don't reuse a removed one across pages. |
| 68 | - **Ids, not list positions, drive `update`/`remove`.** `getPageAnnotations()` |
| 69 | returns only annotation types the API models, so a page with unsupported |
| 70 | annotations has gaps — the `Pair.first` id is the stable handle. |
| 71 | - **One page open at a time** — the pre-existing `PdfRenderer` invariant |
| 72 | ("you can have only a single page opened at any given time") still applies; |
| 73 | close the page before opening the next. |
| 74 | - **`write()` before `close()`**, destination must be a distinct writable |
| 75 | seekable fd; `IllegalStateException` after close. |
| 76 | - **The pairs are `android.util.Pair`, not `kotlin.Pair`** — Kotlin |
| 77 | destructuring (`for ((id, a) in …)`) does not compile against them; use |
| 78 | `.first`/`.second`. |
| 79 | |
| 80 | ## Rendering what you edited |
| 81 | |
| 82 | Annotation types render only when `RenderParams.renderFlags` includes them: |
| 83 | `FLAG_RENDER_TEXT_ANNOTATIONS` / `FLAG_RENDER_HIGHLIGHT_ANNOTATIONS` (and the |
| 84 | 36.1 additions `FLAG_RENDER_FREETEXT_ANNOTATIONS` / |
| 85 | `FLAG_RENDER_STAMP_ANNOTATIONS`). An added stamp or free-text that "doesn't |
| 86 | show up" in your preview bitmap is usually a missing render flag, not a failed |
| 87 | add — re-render with the matching flags set via |
| 88 | `RenderParams.Builder.setRenderFlag |