Skip to content
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,74 @@ function App() {
- **`style`** : style object to change container css properties
- **`className`** : apply a className to the control

## PDF documents

PDF pages are rendered in the browser with [pdf.js](https://mozilla.github.io/pdf.js/) (the first time a PDF is processed, it's loaded lazily in a Web Worker).
Pages are rendered by batch to accommodate very large documents.

### `usePDFDocument` hook

The simplest way to display a PDF in a React component:

```jsx
import { useState } from 'react';
import { AnnotationViewer, usePDFDocument } from 'react-mindee-js';
Comment thread
Copilot marked this conversation as resolved.

function Document({ file }) {
const { status, numPages, pages, loading, loadPage, error } = usePDFDocument(
file,
{ prefetch: 5, batch: 3 },
);
const [current, setCurrent] = useState(0);

if (status === 'error') return <p>{error.message}</p>;
if (status !== 'ready') return <p>Opening…</p>;

return (
<>
<nav>
{pages.map((image, index) => (
<button
key={index}
onClick={() => {
setCurrent(index);
loadPage(index);
}}
>
{loading.has(index) ? '…' : index + 1}
</button>
))}
</nav>
{pages[current] && (
<AnnotationViewer data={{ image: pages[current], shapes: [] }} />
)}
</>
);
}
```

### `openPDF` (framework-agnostic)

For custom scheduling, use the handle directly. Page numbers are **1-based**, like pdf.js.

```js
import { openPDF } from 'react-mindee-js';

const doc = await openPDF(file, { maxPages: 100 });
doc.numPages; // 5

const first = await doc.getPage(1, { priority: 'high' }); // blob: URL
for await (const { pageNumber, image } of doc.getPages(2, 4)) {
// inclusive range, yielded as soon as each page is ready
}

await doc.destroy(); // revokes every blob: URL handed out by this handle
```

### `getPDFPageCount` / `getImagesFromPDF`

`getPDFPageCount(source)` returns the page count without rendering. `getImagesFromPDF` still works but is **deprecated**: it renders and holds every page in memory at once. Prefer `usePDFDocument` or `openPDF`.

## Browser support

React mindee supports all recent browsers and works where React works. However, you may need check the [SSR](/docs/ssr) section.
Expand Down
2 changes: 2 additions & 0 deletions cypress/integration/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ describe('built library public API', () => {
'getImagesFromPDF',
'getPDFPageCount',
'getZoomScale',
'openPDF',
'setShapeConfig',
'toBase64',
'usePDFDocument',
];

it('exposes exactly the documented runtime exports, all of them functions', () => {
Expand Down
29 changes: 29 additions & 0 deletions cypress/integration/pdf.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,33 @@ describe('built library PDF support', () => {
cy.wrap(getPDFPageCount(multiPage)).should('equal', PAGE_COUNT);
});
});

it('renders pages on demand through openPDF with the inlined worker', () => {
cy.window().then((win) => {
cy.spy(win, 'Worker').as('worker');
});
loadDist({ fresh: true }).then(({ openPDF }) => {
cy.then(async () => {
const handle = await openPDF(multiPage);
const image = await handle.getPage(1);
const size = await imageDimensions(image);
await handle.destroy();
return { numPages: handle.numPages, image, size };
}).should(({ numPages, image, size }) => {
expect(numPages).to.equal(PAGE_COUNT);
expect(image).to.match(/^blob:/);
expect(size.width * size.height).to.be.greaterThan(0);
});
});
cy.get<sinon.SinonSpy>('@worker').should((spy) => {
expect(spy.calledOnce).to.equal(true);
expect(String(spy.firstCall.args[0])).to.match(/^blob:/);
});
});

it('exposes usePDFDocument from the built entry point', () => {
loadDist().then(({ usePDFDocument }) => {
expect(usePDFDocument).to.be.a('function');
});
});
});
8 changes: 4 additions & 4 deletions docs/docs/API/annotation-viewer-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type AnnotationShape = {
}
```

> **image** property should be either an URL object or a base64. If you have an object of type [File](https://developer.mozilla.org/fr/docs/Web/API/File) ( typically generated by file upload libraries) instead you may need to use [URL.createObjectURL()](https://developer.mozilla.org/fr/docs/Web/API/URL/createObjectURL) to generate a valid URL object.
> **image** property should be either an URL (including `blob:` object URLs) or a base64 data URL. If you have an object of type [File](https://developer.mozilla.org/fr/docs/Web/API/File) (typically generated by file upload libraries), you may need to use [URL.createObjectURL()](https://developer.mozilla.org/fr/docs/Web/API/URL/createObjectURL) to generate a valid URL object instead. JPEG, PNG, TIFF (first page) and HEIC/HEIF (primary image) are supported; TIFF and HEIF are converted in the browser, the HEIF decoder being downloaded on first use. For PDF files, see [usePDFDocument](/docs/Utils/use-pdf-document).

:::caution

Expand All @@ -54,9 +54,9 @@ interface AnnotationViewerOptions {
onMouseLeave?: (polygon: Line) => void
onClick?: (polygon: Line) => void
zoom?: {
modifier: number
max: number
defaultZoom: number
modifier: number // multiplier applied per wheel step
max: number // highest magnification, relative to the fitted image (1 = fits the viewer)
defaultZoom: number // accepted for backward compatibility, currently unused
}
}
```
Expand Down
55 changes: 46 additions & 9 deletions docs/docs/Tutorial/pdf-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,54 @@ sidebar_position: 2

# PDF file

> If you got a PDF file in hand, you can use [getImagesFromPDF](/docs/Utils/get-images-form-pdf-api) to extract pages in base64 format and pass them to `<AnnotationViewer>` using a proper state.
> PDF pages are rendered in the browser on demand. Use the [usePDFDocument](/docs/Utils/use-pdf-document) hook to display a document progressively: the first pages are rendered right away, the rest only when the user reaches them.

```tsx
getImagesFromPDF(pdfFile).then((images: string[]) => {
console.log(images) // ===> [base64_1, base64_2, ...]
})
import { useState } from 'react'
import { AnnotationViewer, usePDFDocument } from 'react-mindee-js'

function PdfViewer({ file }: { file: File }) {
const { status, numPages, pages, loading, loadPage } = usePDFDocument(file)
const [current, setCurrent] = useState(0)

const goTo = (index: number) => {
setCurrent(index)
loadPage(index) // renders the page and the rest of its batch if needed
}

if (status !== 'ready') return <p>{status}</p>

return (
<>
<button onClick={() => goTo(current - 1)} disabled={current === 0}>
Previous
</button>
<span>
{current + 1} / {numPages}
</span>
<button onClick={() => goTo(current + 1)} disabled={current === numPages - 1}>
Next
</button>

{pages[current] ? (
<AnnotationViewer data={{ image: pages[current], shapes: [] }} />
) : (
<p>{loading.has(current) ? 'Rendering…' : 'Not loaded'}</p>
)}
</>
)
}
```

### Full example
Outside React, or for custom scheduling, use [openPDF](/docs/Utils/open-pdf) directly:

```ts
const doc = await openPDF(file)
const firstPage = await doc.getPage(1)
// ...
await doc.destroy()
```

<iframe
style={{
width:"100%", height:"80vh", border:0, borderRadius: 4, overflow:"hidden" }}
src="https://codesandbox.io/embed/react-mindee-js-simple-example-with-pdf-sts39?fontsize=14&hidenavigation=1&theme=dark" title="react-mindee-js - Canvas + Basic form Example" allow="accelerometer, ambient-light-sensor, camera, encrypted-media, geolocation, gyroscope, hid, microphone, midi, payment, usb, vr, xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" ></iframe>
:::caution
Page images are `blob:` object URLs that live until the document is destroyed. If you need to send one to a server, read it back with `fetch(url).then((r) => r.blob())` first.
:::
2 changes: 1 addition & 1 deletion docs/docs/Utils/draw-layer.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 4
sidebar_position: 7
---

# drawLayer
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Utils/draw-shape.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 2
sidebar_position: 5
---

# drawShape
Expand Down
18 changes: 13 additions & 5 deletions docs/docs/Utils/get-images-form-pdf-api.mdx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
---
sidebar_position: 1
sidebar_position: 3
---

# getImageFromPDF
# getImagesFromPDF

- Type : `(file: string) => Promise`
:::caution Deprecated
`getImagesFromPDF` renders and holds **every** page in memory at once. Use [openPDF](/docs/Utils/open-pdf) or the [usePDFDocument](/docs/Utils/use-pdf-document) hook to render pages progressively instead.
:::

> The parameter should be a valid Object URL of type string. If you have an object of type [File](https://developer.mozilla.org/fr/docs/Web/API/File) ( typically generated by file upload libraries) in hand you must use [URL.createObjectURL()](https://developer.mozilla.org/fr/docs/Web/API/URL/createObjectURL) to generate a valid URL object.
- Type : `(source: PDFSource, maxPages?: number, onSuccess?: () => void, resolution?: number) => Promise<string[]>`

> Renders every page of a PDF to a PNG data URL. Pages that fail to render are skipped, so the result may be shorter than the page count. `source` accepts a URL string, a `File`/`Blob`, an `ArrayBuffer` or a `Uint8Array`.

```javascript
getImagesFromPDF(file).then((images) => {
// Do something with images pdf pages
// images: ['data:image/png;base64,...', ...]
})
```

- **`maxPages`** : rejects with a `TooManyPagesError` when the document has more pages.
- **`onSuccess`** : called once the document has been accepted, before pages are rendered.
- **`resolution`** : target pixel count of each page, see [PDFPageOptions](/docs/Utils/open-pdf#page-options).
11 changes: 8 additions & 3 deletions docs/docs/Utils/get-pdf-page-count.mdx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
---
sidebar_position: 2
sidebar_position: 4
---

# getPDFPageCount

- Type : `(file: string) => Promise<number>`
- Type : `(source: PDFSource) => Promise<number>`
- `PDFSource` : `string | Blob | ArrayBuffer | Uint8Array`

> The parameter should be a valid Object URL of type string. If you have an object of type [File](https://developer.mozilla.org/fr/docs/Web/API/File) ( typically generated by file upload libraries) in hand you must use [URL.createObjectURL()](https://developer.mozilla.org/fr/docs/Web/API/URL/createObjectURL) to generate a valid URL object.
> Returns the number of pages without rendering anything.

```javascript
getPDFPageCount(file).then((pageCount) => {
// Do something with the page count
})
```

:::tip
If you also need to render pages, call [openPDF](/docs/Utils/open-pdf) once and read `numPages` from the handle so the document is only parsed once.
:::
102 changes: 102 additions & 0 deletions docs/docs/Utils/open-pdf.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
sidebar_position: 1
---

# openPDF

- Type : `(source: PDFSource, options?: OpenPDFOptions) => Promise<PDFDocumentHandle>`

> Parses a PDF once and returns a handle from which pages are rendered **on demand**. Only the pages you ask for are rasterized, the requested one first, and memory is released by `destroy()`. This is the primitive behind [usePDFDocument](/docs/Utils/use-pdf-document) and the recommended way to display documents with more than a handful of pages.

pdf.js and its Web Worker are downloaded lazily, the first time a PDF is processed, and a single worker is shared by every document in the tab.

```ts
import { openPDF } from 'react-mindee-js'

const doc = await openPDF(file, { maxPages: 100 })
doc.numPages // 5

const first = await doc.getPage(1, { priority: 'high' }) // blob: URL
for await (const { pageNumber, image } of doc.getPages(2, 4)) {
// inclusive range, yielded as soon as each page is ready
}

await doc.destroy() // revokes every blob: URL handed out by this handle
```

## Source

`PDFSource = string | Blob | ArrayBuffer | Uint8Array`

A URL or data URL string, a [File](https://developer.mozilla.org/docs/Web/API/File)/`Blob` (typically from a file input or upload library), or raw bytes. You no longer need `URL.createObjectURL()` to pass a `File`.

## Options

```ts
interface OpenPDFOptions {
/** Rejects with a `TooManyPagesError` when the document has more pages. */
maxPages?: number
/** Aborting stops the download/parse and rejects with an `AbortError`. */
signal?: AbortSignal
}
```

## Handle

```ts
interface PDFDocumentHandle {
readonly numPages: number
/** Renders a single page. `pageNumber` is 1-based, like pdf.js. */
getPage: (pageNumber: number, options?: PDFPageOptions) => Promise<string>
/** Renders pages `from` to `to` inclusive (1-based) one after the other, yielding each as soon as it is ready. */
getPages: (from: number, to: number, options?: PDFPageOptions) => AsyncIterable<PDFRenderedPage>
/** Cancels pending renders, revokes every object URL and releases the pdf.js document. */
destroy: () => Promise<void>
}

interface PDFRenderedPage {
pageNumber: number // 1-based
image: string
}
```

:::caution
Page numbers are **1-based**. `getPage(0)`, a page above `numPages` or an inverted range reject with a `RangeError` before anything is rendered.
:::

## Page options

```ts
interface PDFPageOptions {
resolution?: number // default 1 500 000 px (width × height), must be > 0
output?: 'object-url' | 'data-url' // default 'object-url'
priority?: 'high' | 'low' // default 'high' for getPage, 'low' for getPages
signal?: AbortSignal
}
```

- **`resolution`** : target pixel count of the render. The viewer displays 1.5 Mpx; use a much lower value such as `50_000` for thumbnails. Renders at different resolutions are cached independently.
- **`output`** : `object-url` returns a `blob:` URL backed by a PNG blob, which is light to hold in memory and is revoked by `destroy()`. `data-url` returns a self-contained base64 PNG string.
- **`priority`** : `high` puts the request ahead of everything still waiting, so a page the user explicitly asked for is drawn before background prefetches. Requesting an already queued page again with `high` promotes it.
- **`signal`** : aborting removes the request from the queue (or cancels the in-progress render) and rejects **that caller's** promise with an `AbortError`. Other callers waiting on the same page are unaffected; the render itself is only cancelled once nobody waits for it.

## Behaviour

- Pages render one at a time per document, in priority order. Every successful render is cached per page, resolution and output for the lifetime of the handle.
- Several calls for the same page share one render.
- `getPages` requests one page at a time, so breaking out of the `for await` loop leaves nothing pending.
- After `destroy()`, every pending call rejects (even a render that was about to complete) and the handle is unusable.

:::caution
Images are `blob:` object URLs by default: they only exist in the current tab and only until `destroy()`. To upload one, read it back first (`await fetch(url).then((r) => r.blob())`) or request `{ output: 'data-url' }`.
:::

## Errors

| `error.name` | Cause |
| :-------------------- | :------------------------------------------------------ |
| `InvalidPDFException` | The source is not a readable PDF |
| `TooManyPagesError` | The document has more pages than `maxPages` |
| `RangeError` | A page number or range is outside `1..numPages` |
| `AbortError` | The request's `signal` was aborted |
| `Error` | A render was requested after `destroy()` |
2 changes: 1 addition & 1 deletion docs/docs/Utils/set-shape-config.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 3
sidebar_position: 6
---

# setShapeConfig
Expand Down
Loading
Loading