react-fast-scroll-pdf
v1.3.1
Published
Image-based PDF viewer using pdfjs
Downloads
429
Readme
react-fast-scroll-pdf
A small, super-fast javascript PDF renderer
We needed a way to very quickly render a complete PDF document but without using a lot of canvas elements (iPads etc limit the amount of canvas memory). This uses a single canvas element and uses pdfjs-dist to render each pdf page before converting it to a PNG image. PDF annotations are then placed on top of the image so that links etc still work.
[screenshot)
Getting Started
To get a local copy up and running follow these simple steps.
Prerequisites
Install via npm
npm install --save react-fast-scroll-pdf
Or clone locally
git clone https://github.com/wearemothership/react-fast-scroll-pdf
Demo
We have provided some demos of how this can be used in your project.
Online demo
Or build and run the demos locally
git clone https://github.com/wearemothership/react-fast-scroll-pdf
cd react-fast-scroll-pdf
npm install
npm run example
Usage
Some usage examples of how this can be used in you project.
Simple
import the FastScrollPDF component and pass in the required props
import React, { SyntheticEvent, useState } from "react";
import { FastScrollPDF } from "react-fast-scroll-pdf";
const App = (): JSX.Element => {
const [file, setFile] = useState<Uint8Array>();
const sourceOptions = {
data: file
};
const fileChanged = (ev: SyntheticEvent<HTMLInputElement>) => {
const target = ev.target as HTMLInputElement;
const newFile = target.files?.[0];
if (newFile) {
const fileReader = new FileReader();
fileReader.onload = (e) => {
const { result } = e.target as FileReader;
const arr = new Uint8Array(result as ArrayBuffer);
setFile(arr);
};
fileReader.readAsArrayBuffer(newFile);
}
};
return (
<div className="App">
<input type="file" onChange={fileChanged} />
{ file ? <FastScrollPDF source={sourceOptions} /> : null }
</div>
);
};
export default App;
Medium
This is something that you may wish to do if you want to apply your own styles or get access to the individual components to sort layout out in your own way. Here you're going to use the components along with the hook to give you lots of control over how you display the PDF.
Note: For reloading the PDF efficiently you should pass in refs to the PDFDocument component and then use those refs for changeZoom. If you choose not to use the refs, it will all still work but instead of reloading from the currently active page, it will reload from page 1.
import React, { SyntheticEvent, useState } from "react";
import { ZoomButtons, PDFDocument, usePDF } from "react-fast-scroll-pdf";
const App = (): JSX.Element => {
const [file, setFile] = useState<Uint8Array>();
const sourceOptions = {
data: file
};
const scrollContainerRef = useRef();
const viewerRef = useRef();
const {
pages,
changeZoomStart,
changeZoomEnd,
renderCurrentPage
} = usePDF({
source,
scrollContainer: scrollContainerRef.current,
viewer: viewerRef.current
});
// Recommend debouncing this code, e.g. lodash debounce.
const scrollDocument = () => renderCurrentPage();
const fileChanged = (ev: SyntheticEvent<HTMLInputElement>) => {
const target = ev.target as HTMLInputElement;
const newFile = target.files?.[0];
if (newFile) {
const fileReader = new FileReader();
fileReader.onload = (e) => {
const { result } = e.target as FileReader;
const arr = new Uint8Array(result as ArrayBuffer);
setFile(arr);
};
fileReader.readAsArrayBuffer(newFile);
}
};
const zoomChangeStart = useCallback((newZoom: number) => {
// This causes the pages to zoom in, but in low quality.
changeZoomStart(newZoom);
}, [changeZoomStart]);
const zoomChangeEnd = useCallback(() => {
// This tells the renderer to recreate all of the images.
changeZoomEnd();
}, [changeZoomEnd])
// Allows the page to be zoomed to fit to the page width
// This is OPTIONAL
const fitPage = useCallback(() => {
if (viewportWidth && scrollContainerRef.current) {
const initial = viewportWidth / zoom;
const scale = (scrollContainerRef.current.offsetWidth / initial) * 0.95;
doZoom(scale);
changeZoomEnd();
return scale;
}
return undefined;
}, [viewportWidth, doZoom, changeZoomEnd, zoom]);
useEffect(() => {
const oldRef = scrollContainerRef.current;
scrollContainerRef.current?.addEventListener("scroll", scrollDocument);
return () => oldRef?.removeEventListener("scroll", scrollDocument);
}, [scrollDocument]);
return (
<div className="App">
<input type="file" onChange={fileChanged} />
<ZoomButtons zoomChangeStart={zoomChangeStart} zoomChangeEnd={zoomChangeEnd} zoomFit={fitPage} />
<PDFDocument scrollContainerRef={scrollContainerRef} viewerRef={viewerRef} pages={pages} />
</div>
);
};
export default App;
Hard
You can import the usePDF hook element into React as below (see Example above to see this running)
import React, { SyntheticEvent, useRef, useState } from 'react';
import { usePDF } from "react-fast-scroll-pdf";
const App = (): JSX.Element => {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const viewerRef = useRef<HTMLDivElement>(null);
const [zoom, setZoom] = useState<number>(1);
const [file, setFile] = useState<Uint8Array>();
const sourceOptions = {
data: file
}
const {
pages,
changeZoomStart,
changeZoomEnd,
renderCurrentPage
} = usePDF({
source: sourceOptions,
scrollContainer: scrollContainerRef.current,
viewer: viewerRef.current
})
const ZOOM_CHANGE: number = 0.1;
// Recommend debouncing this, e.g. lodash debounce.
const scrollDocument = () => renderCurrentPage();
const zoomIn = useCallback(() => {
const newZoom = zoom + ZOOM_CHANGE;
if (newZoom > 0) {
setZoom(newZoom);
changeZoomStart(newStart);
}
}, [changeZoomStart]);
const zoomOut = useCallback(() => {
const newZoom = zoom - ZOOM_CHANGE;
if (newZoom < 2) {
setZoom(newZoom);
changeZoomStart(newZoom);
}
}, [changeZoomStart]);
const zoomEnd = useCallback(() => {
changeZoomEnd();
}, [changeZoomEnd]);
const fileChanged = useCallback((ev: SyntheticEvent<HTMLInputElement>) => {
const target = ev.target as HTMLInputElement;
const file = target.files?.[0]
if (file) {
var fileReader = new FileReader();
fileReader.onload = (e) => {
const { result } = e.target as FileReader;
const arr = new Uint8Array(result as ArrayBuffer);
setFile(arr);
};
fileReader.readAsArrayBuffer(file);
}
}, [])
return (
<div className="App">
<input type="file" onChange={fileChanged} />
<div id="scrollContainer" ref={scrollContainerRef} style={{ overflow: "scroll", height: "800px" }}>
<div id="viewer" ref={viewerRef}>
{pages}
</div>
</div>
<button type="button" onMouseDown={zoomIn} onMouseUp={zoomEnd}>+</button>
<button type="button" onMouseDown={zoomOut} onMouseUp={zoomEnd}>-</button>
</div>
);
}
PDFPage and PlaceholderPage
These two components can be accessed via an import and may be useful if you wish to add additional pages to the page list returned by the usePDF hook.
Props
FastScrollPDF
- source: (required) a PDFJS source object (see below)
- loadingImage: (optional) an image that will be spun in the middle of loading pages (default: spinner gif - courtesy of icons8).
- spinLoadingImage: (optional) Whether to spin the loading image (default: false)
- enableAnnotations: (optional) whether to create an annotations layer. (default: true),
- className: (optional) a CSS class to apply to the main window. (default: none),
- hideZoom: (optional) if true, the zoom buttons are hidden. (default: false),
- showFitPage: (optional) if true, the Fit to Page button is shown. (default: false)
PDFDocument
- pages: (required) The list of pages returned from the usePDF hook.
- scrollContainerRef: (optional) a ref object to store a ref to the div set to be used for scrolling.
- viewerRef: (optional) a ref object to store a ref to the div which contains all the pages (usually a child of scrollContainer).
- className: (optional) a class to apply to the document div.
- rowGap: (optional) the gap between pages as a css string (default: "16px").
ZoomButtons
- zoomChange: (required) a function that accepts a zoom number and carried out the required zoom action.
- zoomStep: (optional) the increment to change the zoom amount by per second (plus or minus). (default: 1)
- zoomStart: (optional) the starting zoom. (default: 1)
- zoomFit: (optional) the function called that should calculate and return the new scaling factor.
- minZoom: (optional) the minimum amount of zoom (default: 0.1)
- maxZoom: (optional) the maximum amount of zoom (default: 5)
- buttonClasses: (optional) a CSS class to apply to the buttons.
- groupClasses: (optional) a CSS class to apply to the div that groups the buttons.
- selectedClass: (optional) a CSS class to apply to selected buttons.
- icons: (optional) an object of icons/text to display on each button: { zoomIn: ReactNode, zoomOut: ReactNode, fitPage: ReactNode }
PDFPage
- width: (required) the width of the page in px.
- height: (required) the height of the page in px.
- pageNum: (required) the page number represented by the component.
- imageSrc: (required) a data URL or image location to be displayed.
- type: (optional) one of "place" or "canvas" for placeholder or image canvas.
- children: (optional) child nodes to display over the top of the page (e.g. annotation layer - use styling to position correctly)
PlaceholderPage
- width: (required) the width of the page in px.
- height: (required) the height of the page in px.
- type: (optional) one of "place" or "canvas" for placeholder or image canvas.
- loadingImage: (optional) an image src string to display in the centre of the loading page (png, jpeg, gif etc);
- spin: (optional) a boolean whether to spin the image (default: false)
usePDF
- source: (required) a PDFJS source object (see below)
- loadingImage: (optional) an image that will be spun in the middle of loading pages (default: Font Awesome Spinner).
- spinLoadingImage: (optional) Whether to spin the loading image (default: false)
- enableAnnotations: (optional) whether to create an annotations layer. (default: true)
- scrollContainer: (optional) Reference to the HTMLDivElement used as a scroll container for the viewer
- viewerContainer: (optional) Reference to the HTMLDivElement used as the viewer.
usePDF return
- pages: a fragment of Placeholder pages or PDFPages (as images) as div & img elements
- changeZoom: call this function to zoom in on the pdf and recreate the images. (scale: number) => void
- changeZoomStart: Call this function when zooming first starts. (scale: number) => void
- changeZoomEnd: call this function when zooming ends. () => void
- renderCurrentPage: Render the page currently in view (and the next page). If the page is already rendered nothing happens unless 'force' is true. (force: boolean) => void
- viewportWidth: (number) The current width of the viewport (a page)
- viewportHeight: (number) The current height of the viewport (a page)
PDF.js source object
You can find details of the options available on the source object here.
Roadmap
See the open issues for a list of proposed features (and known issues).
Contributing
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated.
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature
) - Commit your Changes (
git commit -m 'Add some AmazingFeature'
) - Push to the Branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
License
Distributed under the MIT License. https://github.com/wearemothership/react-fast-scroll-pdf/blob/main/LICENSE.md
Copyright (c) 2021 Mothership Software Ltd.
react-fast-scroll-pdf is used in…
Please let us know if you wish us to add your project to this list.