@codefish24/react-pdf-zero
v0.1.0
Published
Dependency-free React PDF viewer — renders, paginates, zooms and captures signatures entirely in the browser with no native modules
Maintainers
Readme
@codefish24/react-pdf-zero
Features
| Feature | |
|---|---|
| Multi-page navigation | ✅ |
| Zoom in / out / reset | ✅ |
| Freehand signature capture | ✅ |
| Click-to-place signature positioning | ✅ |
| Remove / clear signature | ✅ |
| Export signed document — PDF or PNG | ✅ |
| All-pages export with signature baked in | ✅ |
| Custom toolbar via ref imperative API | ✅ |
| Loading & error states | ✅ |
| Responsive / flexible sizing | ✅ |
| Works with URL, File, Blob, ArrayBuffer | ✅ |
PDF engine — what's supported
- Cross-reference tables and streams (PDF 1.0 – 2.0)
- Stream filters: FlateDecode (deflate/zlib + PNG predictor), ASCII85, ASCIIHexDecode, RunLengthDecode
- Graphics: path construction (
m,l,c,re, …), fills, strokes, clipping, transparency groups - Text: character spacing, word spacing, horizontal scaling, text matrix, text rendering modes
- Fonts: Standard Type1 / Type3 / TrueType — mapped to browser equivalents; ToUnicode CMap + WinAnsi + glyph-name decoding for embedded subsets
- Color spaces: DeviceRGB, DeviceGray, DeviceCMYK, Separation, CalRGB
- Images: inline images, XObject images (JPEG, raw), soft-mask (SMask) alpha blending
- Form XObjects: nested content streams with independent resource dictionaries and
/Matrixtransforms
Installation
npm install @codefish24/react-pdf-zeroPeer dependencies:
npm install react react-domQuick start
import { PdfViewer } from '@codefish24/react-pdf-zero';
function App() {
return (
<PdfViewer
src="/documents/report.pdf"
width="100%"
height="700px"
onLoad={({ pageCount }) => console.log('Pages:', pageCount)}
onError={(err) => console.error(err)}
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| src | string \| File \| Blob \| ArrayBuffer | — | Required. URL string, File/Blob from an <input>, or an ArrayBuffer. |
| width | string \| number | '100%' | CSS width of the viewer container. |
| height | string \| number | '100%' | CSS height of the viewer container. |
| onLoad | (meta) => void | — | Called after the PDF is parsed. meta = { pageCount, width, height }. |
| onError | (Error) => void | — | Called when loading or rendering fails. |
| onSigned | (data) => void | — | Enables the ✎ Sign button. Called when the user confirms a signature. See Signature API. |
| onUnsigned | () => void | — | Called when the user removes a previously placed signature. |
| showToolbar | boolean | true | Set to false to hide the built-in toolbar. Use with a ref to build your own. See Custom toolbar. |
| outputFormat | 'png' \| 'pdf' | 'png' | Output format for onSigned. 'png' → per-page PNG base64 array. 'pdf' → all pages merged into one PDF base64 string. |
Signature API
Add onSigned to enable the signature feature.
User flow:
- User clicks ✎ Sign → placement mode starts (crosshair cursor).
- User clicks anywhere on the document → freehand drawing pad appears.
- User draws and clicks Done → signature is stamped onto the page.
- Toolbar shows ✔ Signed and a ✕ Remove button.
After signing, onSigned is called with a payload shaped by outputFormat.
outputFormat="png" (default)
Every page is rendered and returned as a separate PNG. The signed page has the signature baked in; all other pages are clean.
<PdfViewer
src={file}
outputFormat="png"
onSigned={({ page, outputFormat, signedDataUrl, signatureDataUrl, hasSignature, signedPages }) => {
// page — 1-based number of the page that was signed
// outputFormat — 'png'
// signedDataUrl — data:image/png;base64,... of the signed page (convenience shortcut)
// signatureDataUrl — data:image/png;base64,... of the ink only (transparent background)
// hasSignature — always true
// signedPages — array of every page:
// [
// { page: 1, dataUrl: 'data:image/png;base64,...', base64: '<raw base64>' },
// { page: 2, dataUrl: 'data:image/png;base64,...', base64: '<raw base64>' }, // ← signed
// ...
// ]
// .dataUrl — full data URI, use directly as <img src={dataUrl} />
// .base64 — raw base64 string only, use in multipart / API payloads
// Send all pages to your API:
const body = signedPages.map(({ page: p, base64 }) => ({ page: p, data: base64 }));
await fetch('/api/document/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}}
onUnsigned={() => { /* user removed signature */ }}
/>outputFormat="pdf"
All pages are merged into a single PDF file and returned as a base64 data URL.
Important: Do not use the
data:application/pdf;base64,...string directly as an<a href>orwindow.open()target — modern browsers blockdata:URL navigation for security reasons. Always convert to a Blob URL first (see example below).
<PdfViewer
src={file}
outputFormat="pdf"
onSigned={({ page, outputFormat, signedDataUrl, signatureDataUrl, hasSignature, signedPages }) => {
// page — 1-based number of the page that was signed
// outputFormat — 'pdf'
// signedDataUrl — data:application/pdf;base64,... (all pages merged, signature on signed page)
// signatureDataUrl — data:image/png;base64,... of the ink only
// hasSignature — always true
// signedPages — [{ page: 'all', dataUrl }] single entry, same value as signedDataUrl
// ── Helper: data URL → Blob URL (required for download / open) ──────
function toBlob(dataUrl, mime) {
const bytes = Uint8Array.from(atob(dataUrl.split(',')[1]), c => c.charCodeAt(0));
return URL.createObjectURL(new Blob([bytes], { type: mime }));
}
// Download the signed PDF:
const blobUrl = toBlob(signedDataUrl, 'application/pdf');
const a = document.createElement('a');
a.href = blobUrl;
a.download = 'signed-document.pdf';
a.click();
setTimeout(() => URL.revokeObjectURL(blobUrl), 10_000);
// — OR — open in a new browser tab:
// const blobUrl = toBlob(signedDataUrl, 'application/pdf');
// window.open(blobUrl, '_blank');
// — OR — send the raw base64 to your API:
// const pdfBase64 = signedDataUrl.split(',')[1];
// await fetch('/api/document/save', { method: 'POST', body: JSON.stringify({ pdf: pdfBase64 }) });
}}
onUnsigned={() => { /* user removed signature */ }}
/>Remove a signature
The toolbar shows ✕ Remove after signing. Clicking it:
- Clears the signature overlay from the canvas
- Redraws the original clean page
- Calls
onUnsigned()
To remove programmatically via ref: viewerRef.current?.removeSign()
Custom toolbar — ref API
Set showToolbar={false} and attach a ref to drive the viewer entirely from your own UI:
import { useRef } from 'react';
import { PdfViewer } from '@codefish24/react-pdf-zero';
function MyViewer() {
const ref = useRef();
return (
<>
<div className="my-toolbar">
<button onClick={() => ref.current?.prevPage()}>‹</button>
<button onClick={() => ref.current?.nextPage()}>›</button>
<button onClick={() => ref.current?.zoomOut()}>−</button>
<button onClick={() => ref.current?.zoomReset()}>
{ref.current?.scale * 100 ?? 100}%
</button>
<button onClick={() => ref.current?.zoomIn()}>+</button>
<button onClick={() => ref.current?.startSign()}>✎ Sign</button>
<button onClick={() => ref.current?.removeSign()}>✕ Remove</button>
</div>
<PdfViewer
ref={ref}
src="/doc.pdf"
showToolbar={false}
outputFormat="pdf"
onSigned={({ signedDataUrl }) => console.log('signed PDF data URL:', signedDataUrl)}
onUnsigned={() => console.log('signature removed')}
/>
</>
);
}Methods
| Method | Description |
|---|---|
| prevPage() | Go to previous page |
| nextPage() | Go to next page |
| goToPage(n) | Jump to page n (1-based) |
| zoomIn() | Zoom in by one step |
| zoomOut() | Zoom out by one step |
| zoomReset() | Reset zoom to 100% |
| setScale(n) | Set exact zoom level (e.g. 1.5 = 150%) |
| startSign() | Enter signature placement mode |
| cancelSign() | Cancel placement or drawing without saving |
| removeSign() | Clear the current signature and redraw clean page |
Read-only state
ref.current.page // current page number (1-based)
ref.current.pageCount // total number of pages
ref.current.scale // current zoom level (1 = 100%)
ref.current.signed // true if a signature is placed on the current page
ref.current.sigMode // null | 'placing' | 'drawing'Utility exports
The internal PNG-to-PDF assembler is also exported for use in your own code:
import { buildPdf, buildPdfAndDownload } from '@codefish24/react-pdf-zero';buildPdf(pages)
Assembles an array of image data URLs into a single multi-page PDF Blob. Pure JS, no dependencies.
// pages = [{ page: 1, dataUrl: 'data:image/png;base64,...' }, ...]
const blob = await buildPdf(pages);
const blobUrl = URL.createObjectURL(blob);buildPdfAndDownload(pages, filename?)
Convenience wrapper — builds the PDF and immediately triggers a browser download.
await buildPdfAndDownload(signedPages, 'signed-document.pdf');More examples
Load from a <input type="file">
function FileInput() {
const [file, setFile] = React.useState(null);
return (
<>
<input type="file" accept=".pdf" onChange={e => setFile(e.target.files[0])} />
{file && <PdfViewer src={file} width="100%" height="600px" />}
</>
);
}Load from fetch / ArrayBuffer
const res = await fetch('/api/document');
const buffer = await res.arrayBuffer();
<PdfViewer src={buffer} width="100%" height="600px" />Use SignaturePad standalone
import { SignaturePad } from '@codefish24/react-pdf-zero';
<SignaturePad
onDone={(dataUrl) => console.log('Signature PNG:', dataUrl)}
onCancel={() => console.log('cancelled')}
/>Development
# Install dependencies
npm install
# Start demo dev server (http://localhost:3000)
npm run dev
# Build the library bundle (ESM + UMD → dist/)
npm run build:lib
# Build the demo site
npm run buildProject structure
src/
├── index.js ← Package entry (PdfViewer, SignaturePad, buildPdf, buildPdfAndDownload)
├── App.jsx ← Demo application
├── main.jsx ← Vite dev entry
├── styles/
│ ├── viewer.css ← Component styles (rmfv-* prefix)
│ └── app.css ← Demo shell styles (app-* prefix)
├── components/
│ ├── PdfViewer.jsx ← Main PDF viewer component
│ ├── SignaturePad.jsx ← Freehand signature drawing modal
│ └── Controls.jsx ← Built-in toolbar (nav, zoom, sign, remove)
└── lib/
└── pdf/
├── PdfParser.js ← PDF structure parser, XRef, stream decoding
├── PdfRenderer.js ← Canvas 2D PDF graphics engine
├── PngToPdf.js ← PNG array → PDF assembler (zero deps)
└── index.jsBrowser support
Requires a modern browser with:
| API | Minimum version |
|---|---|
| Canvas 2D | All modern browsers |
| DecompressionStream | Chrome 80 · Firefox 113 · Safari 16.4 |
| OffscreenCanvas | Chrome 69 · Firefox 105 · Safari 16.4 |
OffscreenCanvas is used for off-screen page rendering during export. If unavailable, signedPages falls back to null and only signedDataUrl (the signed page) is returned.
License
MIT
