Convert PDF pages into high‑quality images directly in your browser. Private, fast, and fully client‑side. Built with React, TypeScript, Vite, and TanStack Router, and powered by PDF.js for rendering.
PDF to Image converts each page of your PDF into PNG, JPEG, or WEBP images using in‑browser rendering. No server uploads occur; your files never leave your device.
I built this tool because a third‑party app at work that handles payouts for my commute tickets only accepts image uploads, not PDFs. That limitation made submitting receipts frustrating and slow. This project fills that gap by quickly converting PDF pages to images locally, so submissions are fast, private, and compatible with systems that require image formats.
- 100% client‑side: No uploads, no servers, no tracking
- Multiple formats: PNG, JPEG, WEBP (with automatic fallback if WEBP unsupported)
- Page selection: Pick any subset of pages per document
- Quality controls: Scale and quality sliders for crisp outputs
- Batch export: Download images individually or as a single ZIP
- Password‑protected PDFs: Prompted securely in the browser
- Accessible UI: Keyboard‑friendly controls and clear labels
- Pre‑render and SEO: Static head configuration with meta/OG/Twitter tags
-
Try it out at kaiiiiiiiii.github.io/pdf-to-image
-
This app is deployable as a static site (e.g., GitHub Pages, Netlify, Vercel). See Deployment for base‑path configuration.
-
Default base path is configured for GitHub Pages (/pdf-to-image). Adjust via environment if your hosting path differs.
- Node.js 18+
- pnpm
pnpm install
pnpm dev
# open http://localhost:3000pnpm build
pnpm serve- Add PDFs
- Drag and drop PDFs onto the homepage dropzone, or click to browse.
- Configure output
- Format: PNG, JPEG, or WEBP
- Scale: 1.0–4.0 (multiplied by device pixel ratio under the hood)
- Quality: 70–100% (for JPEG/WEBP only)
- Select pages
- For each PDF, use per‑document controls to select all, clear, invert, or click individual page thumbnails to toggle selection.
- Export
- Download individually (multiple files, one per page), or
- Download ZIP (a single archive with files organized per source PDF)
Tip: When working with many pages or large PDFs, prefer ZIP export to avoid multiple browser save prompts.
- Open PDFs using PDF.js with openPdf(). The PDF worker is initialized lazily via initPdfJsWorker() to avoid SSR pitfalls.
- Render a page to a high‑DPI canvas using renderPageToCanvas(). A white background is filled to prevent artifacts in lossy formats (e.g., JPEG).
- Encode the canvas to an image and determine the filename using exportPageAsImage(), which:
- Selects a MIME type mimeForFormat()
- Falls back from WEBP as needed effectiveFormat()
- Encodes via canvasToBlob()
- Generates a predictable filename fileNameFor()
- Batch downloads are handled either one‑by‑one with downloadMany() and downloadBlob(), or compressed into a single archive using zipFiles().
- UI orchestration, import flow, selection logic, and export actions are managed in the main route component createFileRoute("/"), specifically:
- Import and password handling addFiles()
- Download images individually exportIndividually()
- Download a ZIP exportAsZip()
- Conversion settings panel lives in src/components/ControlsPanel.tsx and exposes:
- Image format selector (JPEG/PNG/WEBP). The WEBP slider hints reflect automatic fallback showQuality.
- Scale slider: Controls output resolution; final pixel size ≈ page size × scale × device pixel ratio (DPR).
- Quality slider: Enabled for JPEG/WEBP only.
- Filenames follow baseName-pNN.ext via fileNameFor(), e.g., my‑file-p01.jpg
- Formats:
- PNG: Lossless, supports transparency
- JPEG: Lossy, smallest sizes for photographic content
- WEBP: Modern codec, great compression/quality; app auto‑falls back to PNG if unsupported effectiveFormat()
- Background handling: A white background is prefilled to avoid JPEG artifacts on transparent PDFs renderPageToCanvas()
- 100% local processing within your browser
- No network uploads are performed for PDF rendering or image generation
- Password‑protected PDFs prompt securely via window.prompt within addFiles() and are passed into openPdf()
- Modern evergreen browsers: Chromium, Firefox, and Safari on desktop and mobile
- WEBP support is auto‑detected; fallback behavior ensures compatibility
- Note: Extremely large pages or very high scale on low‑memory devices may cause memory pressure
src/
├─ components/ # UI components
│ ├─ ui/ # Shadcn/Radix-based primitives
│ ├─ ControlsPanel.tsx
│ ├─ Dropzone.tsx
│ ├─ FileList.tsx
│ └─ PageThumbnail.tsx
├─ lib/ # Core utilities
│ ├─ pdf/
│ │ ├─ export.ts # Canvas encoding, filenames, format logic
│ │ ├─ render.ts # PDF.js open/render helpers
│ │ └─ worker.ts # PDF.js worker bootstrap
│ ├─ config.ts # Auto-generated from package.json
│ ├─ download.ts # Client-side download helpers
│ ├─ feature.ts # Feature detection (WEBP, DPR, etc.)
│ ├─ utils.ts
│ └─ zip.ts # ZIP creation with JSZip
├─ routes/
│ ├─ __root.tsx # Document shell & head (SEO/OG/Twitter)
│ └─ index.tsx # Main app route and logic
├─ env.ts # Type-safe environment configuration
└─ styles.css # Tailwind styles
Static hosting
- The app is a fully static SPA. Compatible with GitHub Pages, Netlify, Vercel, AWS S3 + CloudFront, etc.
- Ensure the base path is set correctly when hosting under a subpath.
GitHub Pages
- Base path
- Default is /pdf-to-image via src/env.ts
- Override at build time if needed, e.g.:
VITE_BASE_PATH=/my-subpath pnpm build- Build
pnpm build- Deploy
- Publish the contents of dist/ to your Pages target (gh-pages branch or GitHub Pages config)
Netlify/Vercel
- Usually root “/”; adjust VITE_BASE_PATH if deploying under a non‑root subpath
Prerendering
- TanStack Start prerender enabled with link crawling disabled vite.config.ts. The main route is static and works with static hosting.
Unit Tests (Vitest)
pnpm test- Tests live under src/lib/tests/ and use @testing-library/react/jsdom as needed
pnpm playwright:install
pnpm test:e2e- Specs live under tests/e2e
- Base URL defaults to http://localhost:3000/pdf-to-image playwright.config.ts
- Playwright can auto‑launch the dev server when needed playwright.config.ts
- For custom base paths, set PLAYWRIGHT_BASE_URL or VITE_BASE_PATH
Common scripts from package.json:
- dev: Start Vite dev server on port 3000
- build: Runs update-config then Vite build
- serve: Preview production build locally
- test / test:e2e / playwright:install
- lint / format / check / check-unused
- update-config: Generates a typed config file from package.json
- The script updates src/lib/config.ts from package.json using [scripts/update-config.js](scripts /update-config.js:21). It stores:
- version (from package.json "version")
- homepage (from package.json "homepage", or default)
- Keyboard navigation supported for dropzone and thumbnails (Enter/Space)
- Clear labels and helper text in settings panel src/components/ControlsPanel.tsx
- Live region style footer shows progress and messages src/routes/index.tsx
- Increase Scale for sharper images on HiDPI/Retina displays. Note that memory use grows with page size and scale.
- Prefer WEBP where supported for a good quality/size balance; fallback is automatic effectiveFormat().
- For many pages, use ZIP export to avoid multiple simultaneous download prompts.
- “Failed to open …”
- The PDF may require a password or be corrupted. You will be prompted as needed addFiles(), openPdf().
- “No pages selected.”
- Select at least one page before exporting exportIndividually(), exportAsZip().
- Blank/low‑quality output
- Increase Scale; ensure the original PDF page isn’t a low‑res bitmap.
- Multiple downloads blocked by browser
- Use “Download ZIP” for a single save prompt zipFiles().
- Does my PDF leave my device?
- No. All processing happens in your browser; nothing is uploaded.
- What’s the best format?
- For general use, WEBP (if supported) offers excellent quality with small size; otherwise PNG for graphics or JPEG for photos.
- Why are JPEGs on transparent PDFs white?
- JPEG has no alpha channel; the app fills a white background to avoid visual artifacts renderPageToCanvas().
- Can I convert password‑protected PDFs?
- Yes. You will be prompted for the password when required openPdf().
- Can I host the app in a subfolder?
- Yes. Set VITE_BASE_PATH before building to match your hosting path vite.config.ts.
- Remember last used format/quality/scale between sessions
- Page range presets (e.g., “odd”, “even”, “1-4,7,10‑12” input)
- Drag‑to‑reorder output pages in ZIP
- Dark mode preference toggle
- Optional transparent background for PNG/WEBP when safe for UX
- Fork and clone the repository
- Create a feature branch:
git checkout -b feat/your-feature - Make changes with tests
- Run checks:
pnpm check && pnpm test && pnpm test:e2e - Commit and push
- Open a pull request
- TypeScript‑first
- ESLint and Prettier enforced
- Keep UI accessible and responsive
- Prefer small, pure utilities in src/lib
MIT.
- PDF.js for robust PDF rendering
- TanStack Router/Start for routing and prerender support
- Tailwind CSS and Radix primitives (via shadcn/ui)
- Lucide Icons for SVG icons
- Vite base path and build config:
- vite.config.ts
- vite.config.ts
- Plugin/prerender: vite.config.ts
- Document/SEO:
- Home route and export logic:
- PDF utilities:
- Image export:
- ZIP and download: