react-native-pdf
Version:
A react native PDF view component, support ios and android platform
476 lines (418 loc) • 17.1 kB
JavaScript
/**
* Copyright (c) 2017-present, Wonday (@wonday.org)
* All rights reserved.
*
* This source code is licensed under the MIT-style license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
View,
Platform,
StyleSheet,
Image,
Text,
requireNativeComponent
} from 'react-native';
import PdfViewNativeComponent, {
Commands as PdfViewCommands,
} from './fabric/RNPDFPdfNativeComponent';
import ReactNativeBlobUtil from 'react-native-blob-util'
import {ViewPropTypes} from 'deprecated-react-native-prop-types';
const SHA1 = require('crypto-js/sha1');
import PdfView from './PdfView';
export default class Pdf extends Component {
static propTypes = {
...ViewPropTypes,
source: PropTypes.oneOfType([
PropTypes.shape({
uri: PropTypes.string,
cache: PropTypes.bool,
cacheFileName: PropTypes.string,
expiration: PropTypes.number,
}),
// Opaque type returned by require('./test.pdf')
PropTypes.number,
]).isRequired,
page: PropTypes.number,
scale: PropTypes.number,
minScale: PropTypes.number,
maxScale: PropTypes.number,
horizontal: PropTypes.bool,
spacing: PropTypes.number,
password: PropTypes.string,
renderActivityIndicator: PropTypes.func,
enableAntialiasing: PropTypes.bool,
enableAnnotationRendering: PropTypes.bool,
showsHorizontalScrollIndicator: PropTypes.bool,
showsVerticalScrollIndicator: PropTypes.bool,
enablePaging: PropTypes.bool,
enableRTL: PropTypes.bool,
fitPolicy: PropTypes.number,
trustAllCerts: PropTypes.bool,
singlePage: PropTypes.bool,
onLoadComplete: PropTypes.func,
onPageChanged: PropTypes.func,
onError: PropTypes.func,
onPageSingleTap: PropTypes.func,
onScaleChanged: PropTypes.func,
onPressLink: PropTypes.func,
// Props that are not available in the earlier react native version, added to prevent crashed on android
accessibilityLabel: PropTypes.string,
importantForAccessibility: PropTypes.string,
renderToHardwareTextureAndroid: PropTypes.string,
testID: PropTypes.string,
onLayout: PropTypes.bool,
accessibilityLiveRegion: PropTypes.string,
accessibilityComponentType: PropTypes.string,
};
static defaultProps = {
password: "",
scale: 1,
minScale: 1,
maxScale: 3,
spacing: 10,
fitPolicy: 2, //fit both
horizontal: false,
page: 1,
enableAntialiasing: true,
enableAnnotationRendering: true,
showsHorizontalScrollIndicator: true,
showsVerticalScrollIndicator: true,
enablePaging: false,
enableRTL: false,
trustAllCerts: true,
usePDFKit: true,
singlePage: false,
onLoadProgress: (percent) => {
},
onLoadComplete: (numberOfPages, path) => {
},
onPageChanged: (page, numberOfPages) => {
},
onError: (error) => {
},
onPageSingleTap: (page, x, y) => {
},
onScaleChanged: (scale) => {
},
onPressLink: (url) => {
},
};
constructor(props) {
super(props);
this.state = {
path: '',
isDownloaded: false,
progress: 0,
};
this.lastRNBFTask = null;
}
componentDidUpdate(prevProps) {
const nextSource = Image.resolveAssetSource(this.props.source);
const curSource = Image.resolveAssetSource(prevProps.source);
if ((nextSource.uri !== curSource.uri)) {
// if has download task, then cancel it.
if (this.lastRNBFTask) {
this.lastRNBFTask.cancel(err => {
this._loadFromSource(this.props.source);
});
this.lastRNBFTask = null;
} else {
this._loadFromSource(this.props.source);
}
}
}
componentDidMount() {
this._mounted = true;
this._loadFromSource(this.props.source);
}
componentWillUnmount() {
this._mounted = false;
if (this.lastRNBFTask) {
// this.lastRNBFTask.cancel(err => {
// });
this.lastRNBFTask = null;
}
}
_loadFromSource = (newSource) => {
const source = Image.resolveAssetSource(newSource) || {};
let uri = source.uri || '';
// first set to initial state
if (this._mounted) {
this.setState({isDownloaded: false, path: '', progress: 0});
}
const filename = source.cacheFileName || SHA1(uri) + '.pdf';
const cacheFile = ReactNativeBlobUtil.fs.dirs.CacheDir + '/' + filename;
if (source.cache) {
ReactNativeBlobUtil.fs
.stat(cacheFile)
.then(stats => {
if (!Boolean(source.expiration) || (source.expiration * 1000 + stats.lastModified) > (new Date().getTime())) {
if (this._mounted) {
this.setState({path: cacheFile, isDownloaded: true});
}
} else {
// cache expirated then reload it
this._prepareFile(source);
}
})
.catch(() => {
this._prepareFile(source);
})
} else {
this._prepareFile(source);
}
};
_prepareFile = async (source) => {
try {
if (source.uri) {
let uri = source.uri || '';
const isNetwork = !!(uri && uri.match(/^https?:\/\//));
const isAsset = !!(uri && uri.match(/^bundle-assets:\/\//));
const isBase64 = !!(uri && uri.match(/^data:application\/pdf;base64/));
const filename = source.cacheFileName || SHA1(uri) + '.pdf';
const cacheFile = ReactNativeBlobUtil.fs.dirs.CacheDir + '/' + filename;
// delete old cache file
this._unlinkFile(cacheFile);
if (isNetwork) {
this._downloadFile(source, cacheFile);
} else if (isAsset) {
ReactNativeBlobUtil.fs
.cp(uri, cacheFile)
.then(() => {
if (this._mounted) {
this.setState({path: cacheFile, isDownloaded: true, progress: 1});
}
})
.catch(async (error) => {
this._unlinkFile(cacheFile);
this._onError(error);
})
} else if (isBase64) {
let data = uri.replace(/data:application\/pdf;base64,/i, '');
ReactNativeBlobUtil.fs
.writeFile(cacheFile, data, 'base64')
.then(() => {
if (this._mounted) {
this.setState({path: cacheFile, isDownloaded: true, progress: 1});
}
})
.catch(async (error) => {
this._unlinkFile(cacheFile);
this._onError(error)
});
} else {
if (this._mounted) {
this.setState({
path: unescape(uri.replace(/file:\/\//i, '')),
isDownloaded: true,
});
}
}
} else {
this._onError(new Error('no pdf source!'));
}
} catch (e) {
this._onError(e)
}
};
_downloadFile = async (source, cacheFile) => {
if (this.lastRNBFTask) {
this.lastRNBFTask.cancel(err => {
});
this.lastRNBFTask = null;
}
const tempCacheFile = cacheFile + '.tmp';
this._unlinkFile(tempCacheFile);
this.lastRNBFTask = ReactNativeBlobUtil.config({
// response data will be saved to this path if it has access right.
path: tempCacheFile,
trusty: this.props.trustAllCerts,
})
.fetch(
source.method ? source.method : 'GET',
source.uri,
source.headers ? source.headers : {},
source.body ? source.body : ""
)
// listen to download progress event
.progress((received, total) => {
this.props.onLoadProgress && this.props.onLoadProgress(received / total);
if (this._mounted) {
this.setState({progress: received / total});
}
})
.catch(async (error) => {
this._onError(error);
});
this.lastRNBFTask
.then(async (res) => {
this.lastRNBFTask = null;
if (res && res.respInfo && res.respInfo.headers && !res.respInfo.headers["Content-Encoding"] && !res.respInfo.headers["Transfer-Encoding"] && res.respInfo.headers["Content-Length"]) {
const expectedContentLength = res.respInfo.headers["Content-Length"];
let actualContentLength;
try {
const fileStats = await ReactNativeBlobUtil.fs.stat(res.path());
if (!fileStats || !fileStats.size) {
throw new Error("FileNotFound:" + source.uri);
}
actualContentLength = fileStats.size;
} catch (error) {
throw new Error("DownloadFailed:" + source.uri);
}
if (expectedContentLength != actualContentLength) {
throw new Error("DownloadFailed:" + source.uri);
}
}
this._unlinkFile(cacheFile);
ReactNativeBlobUtil.fs
.cp(tempCacheFile, cacheFile)
.then(() => {
if (this._mounted) {
this.setState({path: cacheFile, isDownloaded: true, progress: 1});
}
this._unlinkFile(tempCacheFile);
})
.catch(async (error) => {
throw error;
});
})
.catch(async (error) => {
this._unlinkFile(tempCacheFile);
this._unlinkFile(cacheFile);
this._onError(error);
});
};
_unlinkFile = async (file) => {
try {
await ReactNativeBlobUtil.fs.unlink(file);
} catch (e) {
}
}
setNativeProps = nativeProps => {
if (this._root){
this._root.setNativeProps(nativeProps);
}
};
setPage( pageNumber ) {
if ( (pageNumber === null) || (isNaN(pageNumber)) ) {
throw new Error('Specified pageNumber is not a number');
}
if (!!global?.nativeFabricUIManager ) {
if (this._root) {
PdfViewCommands.setNativePage(
this._root,
pageNumber,
);
}
} else {
this.setNativeProps({
page: pageNumber
});
}
}
_onChange = (event) => {
let message = event.nativeEvent.message.split('|');
//__DEV__ && console.log("onChange: " + message);
if (message.length > 0) {
if (message.length > 5) {
message[4] = message.splice(4).join('|');
}
if (message[0] === 'loadComplete') {
let tableContents;
try {
tableContents = message[4]&&JSON.parse(message[4]);
} catch(e) {
tableContents = message[4];
}
this.props.onLoadComplete && this.props.onLoadComplete(Number(message[1]), this.state.path, {
width: Number(message[2]),
height: Number(message[3]),
},
tableContents
);
} else if (message[0] === 'pageChanged') {
this.props.onPageChanged && this.props.onPageChanged(Number(message[1]), Number(message[2]));
} else if (message[0] === 'error') {
this._onError(new Error(message[1]));
} else if (message[0] === 'pageSingleTap') {
this.props.onPageSingleTap && this.props.onPageSingleTap(Number(message[1]), Number(message[2]), Number(message[3]));
} else if (message[0] === 'scaleChanged') {
this.props.onScaleChanged && this.props.onScaleChanged(Number(message[1]));
} else if (message[0] === 'linkPressed') {
this.props.onPressLink && this.props.onPressLink(message[1]);
}
}
};
_onError = (error) => {
this.props.onError && this.props.onError(error);
};
render() {
if (Platform.OS === "android" || Platform.OS === "ios" || Platform.OS === "windows") {
return (
<View style={[this.props.style,{overflow: 'hidden'}]}>
{!this.state.isDownloaded?
(<View
style={[styles.progressContainer, this.props.progressContainerStyle]}
>
{this.props.renderActivityIndicator
? this.props.renderActivityIndicator(this.state.progress)
: <Text>{`${(this.state.progress * 100).toFixed(2)}%`}</Text>}
</View>):(
Platform.OS === "android" || Platform.OS === "windows"?(
<PdfCustom
ref={component => (this._root = component)}
{...this.props}
style={[{flex:1,backgroundColor: '#EEE'}, this.props.style]}
path={this.state.path}
onChange={this._onChange}
/>
):(
this.props.usePDFKit ?(
<PdfCustom
ref={component => (this._root = component)}
{...this.props}
style={[{backgroundColor: '#EEE',overflow: 'hidden'}, this.props.style]}
path={this.state.path}
onChange={this._onChange}
/>
):(<PdfView
{...this.props}
style={[{backgroundColor: '#EEE',overflow: 'hidden'}, this.props.style]}
path={this.state.path}
onLoadComplete={this.props.onLoadComplete}
onPageChanged={this.props.onPageChanged}
onError={this._onError}
onPageSingleTap={this.props.onPageSingleTap}
onScaleChanged={this.props.onScaleChanged}
onPressLink={this.props.onPressLink}
/>)
)
)}
</View>);
} else {
return (null);
}
}
}
if (Platform.OS === "android" || Platform.OS === "ios") {
var PdfCustom = PdfViewNativeComponent;
} else if (Platform.OS === "windows") {
var PdfCustom = requireNativeComponent('RCTPdf', Pdf, {
nativeOnly: {path: true, onChange: true},
})
}
const styles = StyleSheet.create({
progressContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
progressBar: {
width: 200,
height: 2
}
});