One engine thread, shared
Each document is an actor, and every document’s actor runs on the same engine thread, because PDFium’s state is global. Calls on any document serialise. async/await throughout.
Docs
PDFCora is PDFium, pinned to a known revision, wrapped so that Swift, Kotlin, TypeScript and Java each get the API their developers expect — with the same types, the same method names and the same core error cases, on a phone, in a browser and on a server.
Modules
The core never depends on an AI runtime — a build check enforces it on iOS, Android and the web — so an app that only renders PDFs ships no model code. On iOS and Android the UI is its own package; on the web the viewer component is part of the core package. On a server there is no UI to take.
| Module | iOS | Android | Web | Node | Java | What it gives you |
|---|---|---|---|---|---|---|
| Core | PDFCora | com.pdfcora:pdfcora | @pdfcora/web | @pdfcora/node | com.pdfcora:pdfcora-java | Render, text, search, forms, annotations, signatures, redaction, tags, save; on Java, also opens DOCX by converting it to PDF |
| AI | PDFCoraAI | com.pdfcora:pdfcora-ai | @pdfcora/web-ai | @pdfcora/web-ai | — | Answers, summaries, redaction and form proposals, alt text, remediation, diff |
| UI | PDFCoraUI | com.pdfcora:pdfcora-ui | PdfViewerElement | — | PdfViewer state only | A viewer component with a configurable toolbar, theme and review bar |
| Agents | — | — | registerWebMcpTools in @pdfcora/web-ai | pdfcora-mcp | — | WebMCP registration for a browser’s agent; an MCP server over stdio for agents such as Claude Desktop |
Requirements: iOS 15+ with Swift 5.9 · Android minSdk 23 with all four ABIs · Web and Node as ESM with the .wasm inside the package, Node 18+ · Java 17+ on macOS and Linux, x86-64 and arm64, with the native engine in the jar.
Node is the web package with file and PNG helpers on top, not a second implementation, so anything written against the web SDK runs there unchanged. Java is its own implementation over the native engine: it carries the document API, annotations, form filling, redaction, tagging, signing, DOCX opening and the viewer state layer — there is no component to draw, and no AI package yet.
Threading
Each document is an actor, and every document’s actor runs on the same engine thread, because PDFium’s state is global. Calls on any document serialise. async/await throughout.
Every document’s suspending functions run on one engine thread for the whole process, so calls on any document serialise. Your coroutine scope stays yours.
In-process calls run on the JavaScript thread, and each document queues them behind one chain. PdfWorker runs the whole SDK in a Worker behind the same API, so a stalled render can be terminated without taking the page down.
Node inherits the web SDK’s promise chain. On Java every call on every document runs on one engine thread for the whole process, so a request pool can share documents safely: the calls serialise, they do not run in parallel. DOCX conversion runs on the calling thread.
A malformed page can stall inside the engine where no cancellation reaches. One engine thread serves every document, so a watchdog makes the calls queued behind a stuck call fail fast with engineUnresponsive — on every document, not only the stuck one — rather than piling up behind it. They succeed again if the stuck call ever returns. Java has no watchdog yet. On the web, PdfWorker isolates a hang in its Worker.
Errors
No native crashes, no untyped throws. The first seven cases below exist in Swift, Kotlin, TypeScript and Java, each in its language’s spelling (wrongPassword, WrongPassword, WRONG_PASSWORD), so error handling ports between platforms. Java has only those seven. iOS, Android and the web add a few more, such as unsupportedSecurity, cancelled, engine and engineUnresponsive.
| Case | When it happens |
|---|---|
passwordRequired | The document is encrypted and no password was given |
wrongPassword | A password was given and the document refused it |
corrupt | The file, or one page of it, cannot be parsed |
pageOutOfRange | A page index outside the document |
closed | The document was used after close() — raised instead of touching freed memory |
invalidOperation | The call cannot apply: tagging a tagged document, a field that is read-only, alt text on a non-figure |
saveFailed | Writing the document failed |
engineUnresponsive | Not on Java. The engine is stuck on an earlier call, so this call fails instead of waiting: through the watchdog on iOS and Android, or when PdfWorker terminates a hung Worker on the web |
Every sealed hierarchy — annotation kinds, form field kinds, structure types — has an unknown case carrying the raw value. An annotation PDFCora does not model is surfaced, not hidden.
Coordinates
PdfRect is always PDF space, with the origin bottom-left, normalised so left <= right and bottom <= top. The viewer’s transform() converts in both directions with toView, toViewRect and toPdf. There is no hidden Y-flip anywhere in the API.
const hits = await page.text.search('invoice')
const box = await page.text.boundingBox(hits[0].start)
const view = viewer.transform().toViewRect(box) // pixels, origin top-left
Viewer
The state layer — page, zoom, scroll, tool, selection — is a plain value you own, so it transfers between platforms. The UI package renders it; you can also render it yourself and keep only the logic.
Zoom, scroll and page travel as data: a session started on a phone resumes in the browser at the same place.
Pick the items and their order, or start from the default and viewing presets. Custom buttons take an icon and an action.
Tokens with the same meaning on all three platforms — accent, backgrounds, corner radius, icon size, toolbar height.
Marks over the page with accept, reject and apply, used by every AI proposal flow. Nothing is written until someone accepts.
Reference
Generated from the source by each language’s own tool. All SDKs on one page →
They are built with Apple’s DocC, which needs a web server. The Android, web, Node and Java references open fine as they are. To read the iOS ones, serve this folder: cd site && python3 -m http.server 8000, then open http://localhost:8000/developers.html#reference.
Each package has its own reference. On the web the viewer component is part of the core package and AI is a separate one, which Node uses unchanged; the Java SDK has no AI package yet. The iOS references load their content at runtime and need the site served over http.
Limits
.doc, XLSX or PPTX. The PDF matches Word’s only as far as the server has the document’s fonts; the Java README lists the packages to install.timestampUrl.PdfWorker. On iOS and Android the watchdog makes every document’s calls fail fast with engineUnresponsive until the stuck call returns; Java has no watchdog, so a hang blocks every document. PdfWorker is re-exported on Node but needs a browser Worker, which Node does not have.