React-native 에서 .pdf 파일 화면에 보여주기(react-native-webview, react-native-pdf) + react-native-pdf npm 설치 오류

dev.horang🐯·2023년 3월 14일
4

React-Native

목록 보기
15/15
post-thumbnail

프로젝트 중 url에 pdf를 화면에 보여줘야 하는 로직이 필요했고 가장 간단하게 react-native-webview를 사용하려 했다. 코드는

import { WebView } from 'react-native-webview';
...
return(
   <WebView
              source={{ uri: `url.pdf` }}
              onFileDownload={false}
              onError={(err) => console.log(err)}
            />
)

ios에서는 정상적으로 pdf 를 웹뷰형태로 띄워줬다. 하지만 android에서는 해당 파일을 계속 기기로 다운받기만 할 뿐 화면에 웹뷰형태로 보여주지는 못했다.
그래서 react-native-pdf 를 다운받아 사용해 보기로 했다.

설치부터 오류가 발생했다....
자체 내장 라이브러리인 react-native-blob-util과 내가 사용하고 있는 rn-fetch-blob에 충돌이 발생해서 생긴 오류였다.. 그래서 결국node-modules의 react-native-pdf파일 자체의 코드를 건들여 해결했다.react-native-blob-util관련 코드를 모두 rn-fetch-blob으로 바꿔서 해결했다. 해결한 코드는 마지막에 첨부했다.

그래서 해당 충돌 오류는 해결했다. 그런데 이번엔 ios 에서 문제가 발생하는 것이였다. 아마도 react-native-blob-util을 수정해서 발생한 문제 같았는데 이미 rn-fetch-blob을 사용한 곳이 많아 수정이 힘들었고 결국은 ios는 기존대로 webview로 android는 react-native-pdf로 적용시켜 해결했다.

  {Platform.OS == 'ios' && (
            <WebView

              source={{ uri: `url.pdf` }}
              // allowingReadAccessToURL={true}
              onFileDownload={false}
              onError={(err) => console.log(err)}
            />
          )}

          {Platform.OS == 'android' && (
            <Pdf
              trustAllCerts={false}
              source={{ uri: `url.pdf`, cache: true }}
              style={{ flex: 1 }}
              onError={(error) => {
                console.log(error);
              }}
              onLoadProgress={(e) => console.log(e)}
            />
          )}

라이브러리 수정

react-native-pdf/index.js를

/**
 * 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 {
    requireNativeComponent,
    View,
    Platform,
    StyleSheet,
    Image,
    Text
} from 'react-native';

import RNFetchBlob from 'rn-fetch-blob'
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,
        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,
        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,
            isSupportPDFKit: -1
        };

        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;
        if (Platform.OS === "ios") {
            const PdfViewManagerNative = require('react-native').NativeModules.PdfViewManager;
            PdfViewManagerNative.supportPDFKit((isSupportPDFKit) => {
                if (this._mounted) {
                    this.setState({isSupportPDFKit: isSupportPDFKit ? 1 : 0});
                }
            });
        }
        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 = RNFetchBlob.fs.dirs.CacheDir + '/' + filename;

        if (source.cache) {
            RNFetchBlob.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 = RNFetchBlob.fs.dirs.CacheDir + '/' + filename;

                // delete old cache file
                this._unlinkFile(cacheFile);

                if (isNetwork) {
                    this._downloadFile(source, cacheFile);
                } else if (isAsset) {
                    RNFetchBlob.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, '');
                    RNFetchBlob.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: 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 = RNFetchBlob.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});
                }
            });

        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 RNFetchBlob.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);
                RNFetchBlob.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 RNFetchBlob.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');
        }
        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') {
                this.props.onLoadComplete && this.props.onLoadComplete(Number(message[1]), this.state.path, {
                    width: Number(message[2]),
                    height: Number(message[3]),
                },
                message[4]&&JSON.parse(message[4]));
            } 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.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 && this.state.isSupportPDFKit === 1?(
                                                <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") {
    var PdfCustom = requireNativeComponent('RCTPdf', Pdf, {
        nativeOnly: {path: true, onChange: true},
    })
} else if (Platform.OS === "ios") {
    var PdfCustom = requireNativeComponent('RCTPdfView', Pdf, {
        nativeOnly: {path: true, onChange: true},
    })
} 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
    }
});
profile
좋아하는걸 배우는건 신나🎵

0개의 댓글