react-pdf
Version:
Display PDFs in your React app as easily as if they were images.
413 lines (344 loc) • 9.14 kB
JSX
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import mergeClassNames from 'merge-class-names';
import PageCanvas from './Page/PageCanvas';
import PageSVG from './Page/PageSVG';
import TextLayer from './Page/TextLayer';
import AnnotationLayer from './Page/AnnotationLayer';
import {
callIfDefined,
cancelRunningTask,
errorOnDev,
isProvided,
makeCancellable,
makePageCallback,
} from './shared/utils';
import { makeEventProps } from './shared/events';
import { eventsProps, isClassName, isLinkService, isPageIndex, isPageNumber, isPage, isPdf, isRotate } from './shared/propTypes';
export default class Page extends Component {
state = {
page: null,
}
componentDidMount() {
this.loadPage();
}
componentWillReceiveProps(nextProps, nextContext) {
if (
this.getPdf(nextProps, nextContext) !== this.getPdf() ||
this.getPageNumber(nextProps) !== this.getPageNumber()
) {
callIfDefined(
this.context.unregisterPage,
this.pageIndex,
);
if (this.state.page !== null) {
this.setState({ page: null });
}
this.loadPage(nextProps, nextContext);
}
}
componentWillUnmount() {
callIfDefined(
this.context.unregisterPage,
this.pageIndex,
);
cancelRunningTask(this.runningTask);
}
getPdf(props = this.props, context = this.context) {
return props.pdf || context.pdf;
}
getChildContext() {
if (!this.state.page) {
return {};
}
const context = {
page: this.state.page,
pdf: this.getPdf(),
rotate: this.rotate,
scale: this.scale,
};
if (this.props.onRenderError) {
context.onRenderError = this.props.onRenderError;
}
if (this.props.onRenderSuccess) {
context.onRenderSuccess = this.props.onRenderSuccess;
}
if (this.props.onGetAnnotationsError) {
context.onGetAnnotationsError = this.props.onGetAnnotationsError;
}
if (this.props.onGetAnnotationsSuccess) {
context.onGetAnnotationsSuccess = this.props.onGetAnnotationsSuccess;
}
if (this.props.onGetTextError) {
context.onGetTextError = this.props.onGetTextError;
}
if (this.props.onGetTextSuccess) {
context.onGetTextSuccess = this.props.onGetTextSuccess;
}
if (this.props.customTextRenderer) {
context.customTextRenderer = this.props.customTextRenderer;
}
return context;
}
/**
* Called when a page is loaded successfully
*/
onLoadSuccess = (page) => {
this.setState({ page }, () => {
callIfDefined(
this.props.onLoadSuccess,
makePageCallback(page, this.scale),
);
callIfDefined(
this.context.registerPage,
page.pageIndex,
this.ref,
);
});
}
/**
* Called when a page failed to load
*/
onLoadError = (error) => {
if (
error.name === 'RenderingCancelledException' ||
error.name === 'PromiseCancelledException'
) {
return;
}
errorOnDev(error.message, error);
callIfDefined(
this.props.onLoadError,
error,
);
this.setState({ page: false });
}
getPageIndex(props = this.props) {
if (isProvided(props.pageNumber)) {
return props.pageNumber - 1;
}
if (isProvided(props.pageIndex)) {
return props.pageIndex;
}
return null;
}
getPageNumber(props = this.props) {
if (isProvided(props.pageNumber)) {
return props.pageNumber;
}
if (isProvided(props.pageIndex)) {
return props.pageIndex + 1;
}
return null;
}
get pageIndex() {
return this.getPageIndex();
}
get pageNumber() {
return this.getPageNumber();
}
get rotate() {
if (isProvided(this.props.rotate)) {
return this.props.rotate;
}
if (isProvided(this.context.rotate)) {
return this.context.rotate;
}
const { page } = this.state;
return page.rotate;
}
get scale() {
const { scale, width } = this.props;
const { page } = this.state;
const { rotate } = this;
// Be default, we'll render page at 100% * scale width.
let pageScale = 1;
// If width is defined, calculate the scale of the page so it could be of desired width.
if (width) {
const viewport = page.getViewport(scale, rotate);
pageScale = width / viewport.width;
}
return scale * pageScale;
}
get eventProps() {
return makeEventProps(this.props, () => {
const { page } = this.state;
return makePageCallback(page, this.scale);
});
}
get pageKey() {
return `${this.state.page.pageIndex}@${this.scale}/${this.rotate}`;
}
get pageKeyNoScale() {
return `${this.state.page.pageIndex}/${this.rotate}`;
}
get pageProps() {
return {
page: this.state.page,
rotate: this.rotate,
scale: this.scale,
};
}
loadPage(props = this.props, context = this.context) {
const pdf = this.getPdf(props, context);
if (!pdf) {
throw new Error('Attempted to load a page, but no document was specified.');
}
const pageNumber = this.getPageNumber(props);
if (!pageNumber) {
return null;
}
this.runningTask = makeCancellable(pdf.getPage(pageNumber));
return this.runningTask.promise
.then(this.onLoadSuccess)
.catch(this.onLoadError);
}
renderTextLayer() {
const { renderTextLayer } = this.props;
if (!renderTextLayer) {
return null;
}
return (
<TextLayer key={`${this.pageKey}_text`} />
);
}
renderAnnotations() {
const { renderAnnotations } = this.props;
if (!renderAnnotations) {
return null;
}
return (
<AnnotationLayer key={`${this.pageKey}_annotations`} />
);
}
renderSVG() {
return [
<PageSVG key={`${this.pageKeyNoScale}_svg`} />,
/**
* As of now, PDF.js 2.0.120 returns warnings on unimplemented annotations.
* Therefore, as a fallback, we render "traditional" AnnotationLayer component.
*/
this.renderAnnotations(),
];
}
renderCanvas() {
return [
<PageCanvas key={`${this.pageKey}_canvas`} />,
this.renderTextLayer(),
this.renderAnnotations(),
];
}
renderNoData() {
return (
<div className="react-pdf__message react-pdf__message--no-data">{this.props.noData}</div>
);
}
renderError() {
return (
<div className="react-pdf__message react-pdf__message--error">{this.props.error}</div>
);
}
renderLoader() {
return (
<div className="react-pdf__message react-pdf__message--loading">{this.props.loading}</div>
);
}
renderChildren() {
const {
children,
renderMode,
} = this.props;
return [
(
renderMode === 'svg' ?
this.renderSVG() :
this.renderCanvas()
),
children,
];
}
render() {
const { pageNumber } = this;
const pdf = this.getPdf();
const { className } = this.props;
const { page } = this.state;
let content;
if (!pageNumber) {
content = this.renderNoData();
} else if (pdf === null || page === null) {
content = this.renderLoader();
} else if (pdf === false || page === false) {
content = this.renderError();
} else {
content = this.renderChildren();
}
return (
<div
className={mergeClassNames('react-pdf__Page', className)}
ref={(ref) => {
const { inputRef } = this.props;
if (inputRef) {
inputRef(ref);
}
this.ref = ref;
}}
style={{ position: 'relative' }}
data-page-number={pageNumber}
{...this.eventProps}
>
{content}
</div>
);
}
}
Page.defaultProps = {
error: 'Failed to load the page.',
loading: 'Loading page…',
noData: 'No page specified.',
renderAnnotations: true,
renderMode: 'canvas',
renderTextLayer: true,
scale: 1.0,
};
Page.childContextTypes = {
customTextRenderer: PropTypes.func,
onGetTextError: PropTypes.func,
onGetTextSuccess: PropTypes.func,
onRenderError: PropTypes.func,
onRenderSuccess: PropTypes.func,
page: isPage,
pdf: isPdf,
rotate: isRotate,
scale: PropTypes.number,
};
Page.contextTypes = {
linkService: isLinkService,
pdf: isPdf,
registerPage: PropTypes.func,
rotate: isRotate,
unregisterPage: PropTypes.func,
};
Page.propTypes = {
children: PropTypes.node,
className: isClassName,
customTextRenderer: PropTypes.func,
error: PropTypes.string,
inputRef: PropTypes.func,
loading: PropTypes.string,
noData: PropTypes.node,
onGetTextError: PropTypes.func,
onGetTextSuccess: PropTypes.func,
onLoadError: PropTypes.func,
onLoadSuccess: PropTypes.func,
onRenderError: PropTypes.func,
onRenderSuccess: PropTypes.func,
pageIndex: isPageIndex,
pageNumber: isPageNumber,
renderAnnotations: PropTypes.bool,
renderMode: PropTypes.oneOf(['canvas', 'svg']),
renderTextLayer: PropTypes.bool,
rotate: isRotate,
scale: PropTypes.number,
width: PropTypes.number,
...eventsProps(),
};