npm package: @kishannareshpal/expo-pdf
Description: A cross-platform, performant PDF viewer component for React Native and Expo
Author: Kishan Jadav
Latest version: 0.3.2

![expo-pdf Banner](./docs/banner.png)

# `@kishannareshpal/expo-pdf`

[![NPM Version](https://img.shields.io/npm/v/%40kishannareshpal%2Fexpo-pdf?style=flat&logo=npm&label=%40kishannareshpal%2Fexpo-pdf)](https://www.npmjs.com/package/@kishannareshpal/expo-pdf)
[![GitHub Repo stars](https://img.shields.io/github/stars/kishannareshpal/expo-pdf?style=flat)](https://github.com/kishannareshpal/expo-pdf/stargazers)

A cross-platform, high-performance PDF viewer for React Native and Expo, built on top of native* PDF rendering engines.

| [iOS](./docs/preview-ios.mp4)          | [Android](./docs/preview-android.mp4)          |
| -------------------------------------- | ---------------------------------------------- |
| ![iOS preview](./docs/preview-ios.gif) | ![Android preview](./docs/preview-android.gif) |

## Features

- Supports Android and iOS
  - Uses Apple's [`PDFKit`](https://developer.apple.com/documentation/pdfkit/pdfview) on iOS
  - *Uses [kishannareshpal/AndroidPdfViewer](https://github.com/kishannareshpal/AndroidPdfViewer) on Android which is a maintained
    fork of [barteksc/AndroidPdfViewerV2](https://github.com/kishannareshpal/AndroidPdfViewer) which uses the open-source [PDFium](https://pdfium.googlesource.com/pdfium/+/HEAD/docs/getting-started.md) PDF rendering engine.
    - Note: We'll be looking to switch to [`androidx.pdf`](https://developer.android.com/jetpack/androidx/releases/pdf) on Android once that becomes stable.
- Load PDFs from local file paths
  - Supports local file URIs on android and iOS and ContentResolver URIs on android
- Pinch-to-zoom / double-tap-to-zoom and drag gestures
- Support for password-protected PDFs
- Horizontal and vertical reading modes
- Support for content-insets
- Color inversion

## Installation

This package works with both Expo and framework-less React Native projects but Expo provides a more streamlined experience.

### Expo

```bash
npx expo install @kishannareshpal/expo-pdf
```

### Bare React Native

1. Install this package

   ```bash
   npm add @kishannareshpal/expo-pdf

   # bun add @kishannareshpal/expo-pdf
   # pnpm add @kishannareshpal/expo-pdf
   # yarn add @kishannareshpal/expo-pdf
   ```

2. Install CocoaPods dependencies
   ```bash
   npx pod-install
   ```

## Usage

### Use a locally bundled PDF

```jsx
import { PdfView } from '@kishannareshpal/expo-pdf';

export const App = () => {
  const [assets] = useAssets([require('./assets/sample.pdf')]);
  const [uri, setUri] = (useState < string) | (null > null);

  useEffect(() => {
    if (!assets?.length) {
      return;
    }

    assets[0]
      .downloadAsync()
      .then((asset) => setUri(asset.localUri))
      .catch(console.error);
  }, [assets]);

  if (!uri) {
    return null;
  }

  return <PdfView style={{ flex: 1 }} uri={uri} />;
};
```

### Load a file picked using a system picker

```jsx
import { File } from 'expo-file-system';
import * as DocumentPicker from 'expo-document-picker';
import { PdfView } from '@kishannareshpal/expo-pdf'

export const App = () => {
  const [uri, setUri] = useState<string | null>(null)

  const pickFile = () => {
    try {
      const result = await DocumentPicker.getDocumentAsync({ copyToCacheDirectory: true });
      const file = new File(result.assets[0]);
      setUri(file.localUri)
    } catch (error) {
      console.error(error);
    }
  }

  if (!uri) {
    return null;
  }

  return (
    <View>
      <Button onPress={pickFile}>Pick a file</Button>

      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        {uri ? (
          <PdfView
            style={{ flex: 1 }}
            uri={uri}
          />
        ) : (
          <Text>Please pick a file<Text>
        )}
      </View>
    </View>
  )
}
```

### Use remote URLs

```jsx
import { File } from 'expo-file-system';
import * as DocumentPicker from 'expo-document-picker';
import { PdfView } from '@kishannareshpal/expo-pdf'

export const App = () => {
  const [uri, setUri] = useState<string | null>(null)

  const loadFromUrl = (url: string) => {
    const destination = new Directory(Paths.cache, 'pdfs');

    try {
      destination.create();
      const output = await File.downloadFileAsync(url, destination);
      setUri(output.uri)
    } catch (error) {
      console.error(error);
    }
  }

  if (!uri) {
    return null;
  }

  return (
    <View>
      <Button onPress={() => loadFromUrl("https://pdfobject.com/pdf/sample.pdf")}>
        Download and load from URL
      </Button>

      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        {uri ? (
          <PdfView
            style={{ flex: 1 }}
            uri={uri}
          />
        ) : (
          <Text>Please download the file to preview<Text>
        )}
      </View>
    </View>
  )
}
```

## API

<table>
  <thead>
    <tr>
      <th>Props</th>
      <th>Required</th>
      <th>Type</th>
      <th>Description</th>
      <th>Default</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>uri</code></td>
      <td>Required</td>
      <td><code>string</code></td>
      <td>PDF document local file URI.</td>
      <td>-</td>
    </tr>
    <tr>
      <td><code>password</code></td>
      <td>No</td>
      <td><code>string</code></td>
      <td>Password to apply for password-protected PDFs by on load.</td>
      <td><code>undefined</code></td>
    </tr>
    <tr>
      <td><code>pagingEnabled</code></td>
      <td>No</td>
      <td><code>boolean</code></td>
      <td>Enables page-by-page snapping instead of free scrolling in vertical direction by default, unless the <code>horizontal</code> prop is provided to page horizontally instead.</td>
      <td><code>false</code></td>
    </tr>
    <tr>
      <td><code>doubleTapToZoom</code></td>
      <td>No</td>
      <td><code>boolean</code></td>
      <td>Allows the user to zoom in/out by double-tapping the document.</td>
      <td><code>true</code></td>
    </tr>
    <tr>
      <td><code>horizontal</code></td>
      <td>No</td>
      <td><code>boolean</code></td>
      <td>Renders and scrolls pages horizontally instead of vertically. If paging is enabled, then it changes horizontally.</td>
      <td><code>false</code></td>
    </tr>
    <tr>
      <td><code>pageGap</code></td>
      <td>No</td>
      <td><code>number</code></td>
      <td>Space between adjacent pages.</td>
      <td><code>0</code></td>
    </tr>
    <tr>
      <td><code>contentPadding</code></td>
      <td>No</td>
      <td><a href="#contentpadding"><code>ContentPadding</code></a></td>
      <td>Padding applied around the document inside the viewer container.</td>
      <td><code>{ top: 0, left: 0, right: 0, bottom: 0 }</code></td>
    </tr>
    <tr>
      <td><code>fitMode</code></td>
      <td>No</td>
      <td><a href="#fitmode"><code>FitMode</code></a></td>
      <td>How the document is scaled to fit within the viewer.</td>
      <td><code>"width"</code></td>
    </tr>
    <tr>
      <td><code>pageColorInverted</code></td>
      <td>No</td>
      <td><code>boolean</code></td>
      <td>Whether the rendered page should have its color inverted (useful to simulate dark mode)</td>
      <td><code>false</code></td>
    </tr>
    <tr>
      <td><code>autoScale</code></td>
      <td>No</td>
      <td><code>boolean</code></td>
      <td>Automatically rescales the document when its or its parent’s layout changes after initial render.</td>
      <td><code>true</code></td>
    </tr>
    <tr>
      <td><code>onLoadComplete</code></td>
      <td>No</td>
      <td><a href="#onloadcompleteeventpayload"><code>(OnLoadCompleteEventPayload) =&gt; void</code></a></td>
      <td>Triggered once the document and its metadata have finished loading.</td>
      <td>-</td>
    </tr>
    <tr>
      <td><code>onPageChanged</code></td>
      <td>No</td>
      <td><a href="#onpagechangedeventpayload"><code>(OnPageChangedEventPayload) =&gt; void</code></a></td>
      <td>Triggered when the currently visible page index changes.</td>
      <td>-</td>
    </tr>
    <tr>
      <td><code>onError</code></td>
      <td>No</td>
      <td><a href="#onerroreventpayload"><code>(OnErrorEventPayload) =&gt; void</code></a></td>
      <td>Triggered when the PDF fails to load, decrypt, or render.</td>
      <td>-</td>
    </tr>
  </tbody>
</table>

### API Reference

#### `ContentPadding`

```ts
{ top?: number, left?: number, right?: number, bottom?: number }
```

#### `FitMode`

```ts
'width' | 'height' | 'both';
```

#### `OnLoadCompleteEventPayload`

```ts
{
  pageCount: number;
}
```

#### `OnPageChangedEventPayload`

```ts
{ pageIndex: number, pageCount: number }
```

#### `OnErrorEventPayload`

```ts
{
    code: 'invalid_url' | 'invalid_document' | 'password_required' | 'password_incorrect',
    message: string
}
```

## Support for CSS using `uniwind` (TailwindCSS)

To use `uniwind` class names, you must wrap the component with the [`withUniwind`](https://docs.uniwind.dev/api/with-uniwind#withuniwind) Higher-Order Component (HOC).
This allows the component to process the `className` prop and convert it into native styles.

```tsx
// src/lib/styled.tsx
import { PdfView as PdfViewPrimitive } from '@kishannareshpal/expo-pdf';
import { ComponentProps } from 'react';
import { withUniwind } from 'uniwind';

export const StyledPdfViewPrimitive = withUniwind(PdfViewPrimitive);
export type StyledPdfViewPrimitiveProps = ComponentProps<
  typeof StyledPdfViewPrimitive
>;
```

```tsx
// src/app.tsx
import { StyledPdfView } from '@/lib/styled';

const App = () => {
  return (
    <StyledPdfView
      className="flex-1 rounded-4xl bg-red-500"
      uri="..."
      ...
    />
  )
}
```

## References

### Prior art

This project is inspired by [`react-native-pdf`](https://github.com/wonday/react-native-pdf) and maintained to the more modern React Native and Expo ecosystem.

## Contributing

Contributions are welcome!

### How to contribute

Please read [CONTRIBUTING.md](./CONTRIBUTING.md)

### How to publish a new version to the registry

> **NOTE**
>
> This package follows semantic versioning with the format: `major.minor.patch(-beta.n|next)`.
>
> - Major version: Increment when making incompatible API changes.
> - Minor version: Increment when adding new functionality in a backward-compatible way.
> - Patch version: Increment when fixing bugs in a backward-compatible manner.
>
> **Only approved repository members with access to our NPM account can publish new versions.**

1. Ensure all changes ready for release are merged into `main`.
2. Go to the [`✳️ Release Package`](https://github.com/kishannareshpal/expo-pdf/actions/workflows/release.yml) workflow
3. Click the `Run workflow` dropdown button and configure the inputs:
   - `Dry run`: Uncheck this to perform the actual release (Default is `true` to test the build/versioning without publishing - only useful when updating the `release.yml` workflow during maintenance)
   - `NPM tag`: Leave as `latest` for standard releases, or use `beta` / `next` for pre-releases.
   - `Version override`: Leave as `auto` to let it automatically increment the patch version (or beta version if `beta` was specified above).
     if you to bump the minor or major version, you must specify that here (e.g. `1.2.0` or `2.0.0`)
4. Click the `Run workflow` button to publish 🚀
   - Monitor the status at [**kishannareshpal/expo-pdf/actions**](https://github.com/kishannareshpal/expo-pdf/actions)

## License

MIT