npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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 /Matrix transforms

Installation

npm install @codefish24/react-pdf-zero

Peer dependencies:

npm install react react-dom

Quick 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:

  1. User clicks ✎ Sign → placement mode starts (crosshair cursor).
  2. User clicks anywhere on the document → freehand drawing pad appears.
  3. User draws and clicks Done → signature is stamped onto the page.
  4. 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> or window.open() target — modern browsers block data: 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 build

Project 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.js

Browser 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