@tobyt/expo-pdf-markup
v0.6.1
Published
An Expo module for displaying and annotating PDFs
Maintainers
Readme
@tobyt/expo-pdf-markup
An Expo module for displaying and annotating PDFs, with support for iOS, Android, and Web.
📖 API Reference · 🚀 Example App
Status: Early development — actively tested in the Choir app.
Platform support
| Platform | PDF rendering | Annotations | | -------- | ------------- | ----------- | | iOS | ✅ | ✅ | | Android | ✅ | ✅ | | Web | ✅ | ✅ |
Installation
npm install @tobyt/expo-pdf-markupiOS
npx pod-installWeb
Web rendering uses pdfjs-dist. Install it as a dependency:
npm install pdfjs-distThen add the Metro config plugin to your metro.config.js:
const { getDefaultConfig } = require('expo/metro-config');
const { withPdfMarkup } = require('@tobyt/expo-pdf-markup/metro');
const config = getDefaultConfig(__dirname);
module.exports = withPdfMarkup(config);withPdfMarkup does two things automatically:
- Patches
import.metain pdfjs-dist so Metro can bundle it (pdfjs-dist v4 uses ESM syntax in a Node.js-only code path that is otherwise unreachable in a browser). - Copies the pdfjs worker to
public/pdf.worker.min.mjsin your project root so it is served alongside your app (same-origin, no CORS issues). The default worker URL is./pdf.worker.min.mjs(relative to the page), so it works whether your app is hosted at the root or a sub-path.
The public/pdf.worker.min.mjs file is regenerated on each Metro start if missing, so you can add it to .gitignore:
public/pdf.worker.min.mjsUsing a different worker URL
If you are not using Metro (e.g. Webpack or a custom CDN), set the worker URL before mounting the view:
import { setPdfJsWorkerSrc } from '@tobyt/expo-pdf-markup';
setPdfJsWorkerSrc('https://your-cdn.example.com/pdf.worker.min.mjs');Asset loading on web
expo-asset returns a full URL on web (https://… or http://…), not a local path. Pass it directly to source — pdfjs accepts URLs:
// asset.localUri on web is already a URL; the .replace() is a no-op
setPdfPath(asset.localUri.replace('file://', ''));Usage
Full API reference is available at tobyt42.github.io/expo-pdf-markup.
import { ExpoPdfMarkupView } from '@tobyt/expo-pdf-markup';
import type { AnnotationMode } from '@tobyt/expo-pdf-markup';
import { Asset } from 'expo-asset';
import { useEffect, useState } from 'react';
import { StyleSheet } from 'react-native';
export default function App() {
const [pdfPath, setPdfPath] = useState<string | null>(null);
const [annotations, setAnnotations] = useState(JSON.stringify({ version: 1, annotations: [] }));
useEffect(() => {
async function preparePdf() {
const asset = Asset.fromModule(require('./assets/document.pdf'));
await asset.downloadAsync();
if (asset.localUri) setPdfPath(asset.localUri.replace('file://', ''));
}
preparePdf();
}, []);
if (!pdfPath) return null;
return (
<ExpoPdfMarkupView
source={pdfPath}
style={StyleSheet.absoluteFill}
annotationMode="ink"
annotationColor="#FF0000"
annotationLineWidth={3}
annotations={annotations}
onLoadComplete={({ nativeEvent: { pageCount } }) => console.log(`Loaded ${pageCount} pages`)}
onPageChanged={({ nativeEvent: { page, pageCount, pageWidth, pageHeight } }) =>
console.log(`Page ${page + 1} of ${pageCount} (${pageWidth}×${pageHeight}pt)`)
}
onAnnotationsChanged={({ nativeEvent }) => setAnnotations(nativeEvent.annotations)}
onError={({ nativeEvent: { message } }) => console.error(message)}
/>
);
}Custom text input UI
If you provide onTextInputRequested, the built-in native prompt is skipped and your callback is
used instead. The callback receives a request object with mode, page, point, and
currentText for edit flows, so you can prefill your own UI when the user taps an existing text
annotation while the text tool is active.
Development
This module uses the Expo Modules API. The example directory contains a test app.
# Build the module
npm run build
# Run the example app
cd example
npx expo startCore Team
License
MIT
