App Features
PDF → PNG images
Single-file Expo screen: PDF.js in a hidden WebView, PNG pages in a ZIP, system share sheet. Native iOS and Android only—the sections below walk through what the file does and how data moves.
Demo video
Screen recording of the flow: pick a PDF, progress while pages render, preview, then share or save the ZIP (native app).
Single-file reference (download)
Place the file in your Expo Router app (`src/app/pdf-to-images-simple.tsx` or similar), vendor PDF.js UMD builds, register the route, and install the packages below.
Packages: expo-asset, expo-document-picker, expo-file-system, expo-keep-awake, expo-router, expo-sharing, jszip, react-native-webview, react-native-safe-area-context.
What this file does
One React Native screen drives the full pipeline: pick a PDF, stage it in a cache folder next to bundled PDF.js UMD scripts, load a small HTML document in a hidden WebView, run Mozilla PDF.js to draw each page on a canvas, encode PNGs, stream pixel data to native code through the WebView bridge in chunks, assemble a ZIP with JSZip, write it under the app documents directory, and optionally open the system share sheet. Processing stays on the device; nothing is uploaded.
The browser build is intentionally unsupported (`Platform.OS === 'web'`) because this flow relies on local `file://` staging and native WebView file access, not a typical web deployment.
End-to-end flow
- User taps “Choose PDF”. `DocumentPicker` returns the asset. On Android, `content://` URIs are copied to a readable cache path via `resolveReadableUri`.
- A session folder is created under `cacheDirectory/pdf_to_images_session/` with a unique id in the path. The PDF is copied to `input.pdf`. `copyBundledPdfJsToWorkDir` copies `pdf.min.js` and `pdf.worker.min.js` from Expo assets next to it so everything shares one `file://` origin.
- `webSession` is set with generated HTML, `baseUrl` pointing at that folder, and on iOS `allowingReadAccessToURL` set to the session directory path without a trailing slash—required for WKWebView to read `input.pdf` and the scripts.
- `START_PDF_PIPELINE_JS` runs on load (and is reinjected after `onLoadEnd`, with extra delays on Android) until `window.pdfjsLib` exists, then invokes `window.startPdfToPngPipeline` defined inside the HTML string.
- Inside the WebView, PDF.js opens the PDF from an `ArrayBuffer`, caps pages at `MAX_PAGES`, scales each page using `MAX_PAGE_WIDTH_PX`, renders to a 2D canvas, reads PNG as base64, splits it into ~450k-character chunks, and posts `pageStart`, `pagePart`, and `pageDone` messages to React Native.
- `onMessage` parses JSON: it accumulates base64 parts per page, writes `page-001.png`, etc. into a `JSZip` instance, updates progress, and on `done` calls `finishZip`. Errors tear down the session and show an alert.
- `finishZip` builds the archive as base64, writes a zip under `documentDirectory` in `PdfImageExports/` named like `pdf_pages_*.zip` where the middle is a timestamp, deletes the session folder, shows a preview of the first page, and the user can call `shareAsync` with MIME `application/zip` (and iOS UTI `public.zip-archive`).
Main blocks in the code
Constants and bootstrap script
`MAX_PAGES`, `MAX_PAGE_WIDTH_PX`, `KEEP_AWAKE_TAG`, `SESSION_DIR`, and `START_PDF_PIPELINE_JS`: a small polling script that waits for `pdfjsLib` and `startPdfToPngPipeline`, starts conversion, or posts a timeout error after many retries.
Filesystem helpers
`ensureFileUrl` adds `file://` where sharing needs it. `copyBundledPdfJsToWorkDir` uses `Asset.fromModule`, `downloadAsync`, and `copyAsync` so the WebView sees real files on disk. `resolveReadableUri` copies Android `content://` picks into the cache when needed.
Generated HTML (`buildPdfToPngHtml`)
A template string builds a minimal page that loads `pdf.min.js`, defines `startPdfToPngPipeline` to configure the worker path, fetch `input.pdf` (XHR with fetch fallback), call `getDocument`, iterate pages, `render` to canvas with a timeout guard, then `postMessage` JSON payloads—chunked base64 for bridge limits.
React Native UI and WebView
State tracks `busy`, progress text, `webSession` (HTML + baseUrl + readAccessUrl), ZIP path, preview URI, and page count. A near-invisible WebView mounts when a session exists; `activateKeepAwakeAsync` keeps the device awake during long renders. Cleanup on unmount or error deletes the temp directory and stops keep-awake.
The `onMessage` handler
Handles `stage` (loading vs opening), `meta` (page counts), `pageStart` / `pagePart` / `pageDone` for assembly, `done` to finalize the ZIP, and `error` to reset UI and alert. A ref tracks which page’s chunks are being merged.
ZIP creation and teardown
`finishZip` reads PNG entries from the zip object, builds a preview data URI from the first image, writes the ZIP file with base64 encoding, then `teardown` removes the session directory and clears WebView state.
Sharing
`shareZip` checks `Sharing.isAvailableAsync`, then `shareAsync` on the ZIP path with the correct MIME type so users can save to Files or send via another app.
On web, the primary button is disabled and a short message explains that this demo targets native iOS and Android only.