Austin Greear
Austin Greear

Reputation: 21

A PDF viewer for Expo with React Native

Does Expo have a way to view a PDF and jump to a specific page in said PDF. I've looked into https://www.npmjs.com/package/react-native-pdf but i found it to be incompatible with expo given its use of native React-Native libraries. I've also tried https://github.com/xcarpentier/rn-pdf-reader-js, but there appears to be a couple of issues with it and it doesn't look like the creator updates it anymore.

Upvotes: 2

Views: 4639

Answers (3)

SS Technology
SS Technology

Reputation: 1

Installed the following library: 1 - Expo constant 2 - Expo File system

Use the below component:

import React, { useState } from 'react';
import { useEffect } from 'react';
import { 
    View,
    Platform, 
    StyleSheet,
    Alert
} from 'react-native';
import * as FileSystem from 'expo-file-system';
import { WebView } from 'react-native-webview';
import Loader from '../Loader/Loader';

const {
    cacheDirectory,
    writeAsStringAsync,
    deleteAsync,
    getInfoAsync,
    EncodingType,
} = FileSystem

const PdfReader = (props) => {

  const isAndroid = Platform.OS == 'android' ? true : false;

  const [renderType, setRenderType] = useState(undefined);
  const [ready, setReady] = useState(false);
  const [renderedOnce, setRenderedOnce] = useState(undefined);

  const bundleJsPath = `${cacheDirectory}bundle.js`
  const htmlPath = `${cacheDirectory}index.html`
  const pdfPath = `${cacheDirectory}file.pdf`

    const getRenderType = () => {
        const {
            useGoogleReader,
            useGoogleDriveViewer,
            source: { uri, base64 },
        } = props

        if (useGoogleReader) {
            return 'GOOGLE_READER'
        }

        if (useGoogleDriveViewer) {
            return 'GOOGLE_DRIVE_VIEWER';
        }

        if (Platform.OS === 'ios') {
            if (uri !== undefined) {
                return 'DIRECT_URL'
            }
            if (base64 !== undefined) {
                return 'BASE64_TO_LOCAL_PDF'
            }
        }

        if (base64 !== undefined) {
            return 'DIRECT_BASE64'
        }

        if (uri !== undefined) {
            return 'URL_TO_BASE64'
        }

        return undefined
    }

    const validate = () => {
    
        let renderType_n = renderType == undefined ? getRenderType() : renderType;
        const {source } = props;
       
        if (!renderType_n || !source) {
            showError('source is undefined')
        } else if (
            (renderType_n === 'DIRECT_URL' ||
                renderType_n === 'GOOGLE_READER' ||
                renderType_n === 'GOOGLE_DRIVE_VIEWER' ||
                renderType_n === 'URL_TO_BASE64') &&
            (!source.uri ||
                !(
                    source.uri.startsWith('http') ||
                    source.uri.startsWith('file') ||
                    source.uri.startsWith('content')
                ))
        ) {
            showError(
                `source.uri is undefined or not started with http, file or content source.uri = ${source.uri}`,
            )
        } else if (
            (renderType_n === 'BASE64_TO_LOCAL_PDF' ||
                renderType_n === 'DIRECT_BASE64') &&
            (!source.base64 ||
                !source.base64.startsWith('data:application/pdf;base64,'))
        ) {
            showError(
                'Base64 is not correct (ie. start with data:application/pdf;base64,)',
            )
        }
    }

    async function fetchPdfAsync(source){
        const mediaBlob = await urlToBlob(source)
        if (mediaBlob) {
            return readAsTextAsync(mediaBlob)
        }
        return undefined
    }

    async function urlToBlob(source){
        if (!source.uri) {
            return undefined
        }
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest()
            xhr.onerror = reject
            xhr.onreadystatechange = () => {
                if (xhr.readyState === 4) {
                    resolve(xhr.response)
                }
            }

            xhr.open('GET', source.uri)

            if (source.headers && Object.keys(source.headers).length > 0) {
                Object.keys(source.headers).forEach((key) => {
                    xhr.setRequestHeader(key, source.headers[key])
                })
            }

            xhr.responseType = 'blob'
            xhr.send()
        })
    }

    function readAsTextAsync(mediaBlob) {
        return new Promise((resolve, reject) => {
            try {
                const reader = new FileReader()
                reader.onloadend = (_e) => {
                    if (typeof reader.result === 'string') {
                        return resolve(reader.result)
                    }
                    return reject(
                        `Unable to get result of file due to bad type, waiting string and getting ${typeof reader.result}.`,
                    )
                }
                reader.readAsDataURL(mediaBlob)
            } catch (error) {
                reject(error)
            }
        })
    }

    async function writeWebViewReaderFileAsync(data,customStyle,withScroll,withPinchZoom,maximumPinchZoomScale) {
       
        const { exists, md5 } = await getInfoAsync(bundleJsPath, { md5: true })
        const bundleContainer = require('./bundleContainer')
        if (__DEV__ || !exists || bundleContainer.getBundleMd5() !== md5) {
            await writeAsStringAsync(bundleJsPath, bundleContainer.getBundle())
        }
        await writeAsStringAsync(
            htmlPath,
            viewerHtml(
                data,
                customStyle,
                withScroll,
                withPinchZoom,
                maximumPinchZoomScale,
            ),
        )
    }

    async function writePDFAsync(base64) {
        await writeAsStringAsync(
            pdfPath,
            base64.replace('data:application/pdf;base64,', ''),
            { encoding: EncodingType.Base64 },
        )
    }

    const getGoogleReaderUrl = (url) =>
        `https://docs.google.com/viewer?url=${url}`
    const getGoogleDriveUrl = (url) =>
        `https://drive.google.com/viewerng/viewer?embedded=true&url=${url}`

    const init = async () => {
        let renderType_n = renderType == undefined ? getRenderType() : renderType;

        try {
            const {
                source,
                customStyle,
                withScroll,
                withPinchZoom,
                maximumPinchZoomScale,
            } = props
        
            switch (renderType_n) {
                case 'GOOGLE_DRIVE_VIEWER': {
                    break;
                }

                case 'URL_TO_BASE64': {
                    const data = await fetchPdfAsync(source)
                    await writeWebViewReaderFileAsync(
                        data,
                        customStyle,
                        withScroll,
                        withPinchZoom,
                        maximumPinchZoomScale,
                    )
                    break
                }

                case 'DIRECT_BASE64': {
                    await writeWebViewReaderFileAsync(
                        source.base64,
                        customStyle,
                        withScroll,
                        withPinchZoom,
                        maximumPinchZoomScale,
                    )
                    break
                }

                case 'BASE64_TO_LOCAL_PDF': {
                    await writePDFAsync(source.base64)
                    break
                }

                default:
                    break
            }
            setReady(true);
        } catch (error) {
            alert(`Sorry, an error occurred. ${error.message}`)
            console.error(error)
        }
    }

    async function removeFilesAsync() {
        const { exists: htmlPathExist } = await getInfoAsync(htmlPath)
        if (htmlPathExist) {
            await deleteAsync(htmlPath, { idempotent: true })
        }

        const { exists: pdfPathExist } = await getInfoAsync(pdfPath)
        if (pdfPathExist) {
            await deleteAsync(pdfPath, { idempotent: true })
        }
    }

    const originWhitelist = [
        'http://*',
        'https://*',
        'file://*',
        'data:*',
        'content:*',
    ]

    const style = [styles.pdfview, props.webviewStyle];

    const getWebviewSource = () => {
        const { source: { uri, headers } } = props;
        let renderType_n = renderType == undefined ? getRenderType() : renderType;

        switch (renderType_n) {
            case 'GOOGLE_READER':
                return { uri: getGoogleReaderUrl(uri) }
            case 'GOOGLE_DRIVE_VIEWER':
                return { uri: getGoogleDriveUrl(uri) };
            case 'DIRECT_BASE64':
            case 'URL_TO_BASE64':
                return { uri: htmlPath }
            case 'DIRECT_URL':
                return { uri: uri, headers }
            case 'BASE64_TO_LOCAL_PDF':
                return { uri: pdfPath }
            default: {
                showError('Unknown RenderType')
                return undefined
            }
        }
    }

   useEffect(()=>{
       setRenderType(getRenderType());
       validate();
       init();
 
       return () => {
          // componentWillUnmount
           if (renderType === 'DIRECT_BASE64' || renderType === 'URL_TO_BASE64' || renderType === 'BASE64_TO_LOCAL_PDF'
           ) {
               try {
                   removeFilesAsync()
               } catch (error) {
                   alert(`Error on removing file. ${error.message}`)
                   console.error(error)
               }
           }
       }

   },[]);

  const source = ready ? getWebviewSource() : undefined;

  return (
  <View style={styles.constainer}>
     
        {ready && source != null ?
          <WebView
           {...{
              originWhitelist,
              style,
             }}
             source={source}
            //   source = {{uri:undefined}}
              onLoad = {()=>{
                  setReady(true);
              }}

              onError = {(e)=>{
                  setReady(false);
                  Alert.alert('Alert message',e);
              }}

              allowFileAccess={isAndroid}
              allowFileAccessFromFileURLs={isAndroid}
              allowUniversalAccessFromFileURLs={isAndroid}
              scalesPageToFit={Platform.select({ android: false })}
              mixedContentMode={isAndroid ? 'always' : undefined}
              sharedCookiesEnabled={false}
              startInLoadingState={true}
           />
           :
              <Loader loading={true} />
           }
  </View>
  );
}

const styles = StyleSheet.create({
    constainer:{
        flex:1,
        backgroundColor:'white',
    },

    pdfview:{
      flex:1,
    }
});

 const showError = (e) => {
    Alert.alert('Alert message', e);
 }

function viewerHtml(base64, customStyle, withScroll = false, withPinchZoom = false,maximumPinchZoomScale = 5) {
    return `
<!DOCTYPE html>
<html>
  <head>
    <title>PDF reader</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, maximum-scale=${withPinchZoom ? `${maximumPinchZoomScale}.0` : '1.0'
        }, user-scalable=${withPinchZoom ? 'yes' : 'no'}" />
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/web/pdf_viewer.min.js"></script>
    <script
      crossorigin
      src="https://unpkg.com/react@16/umd/react.production.min.js"
    ></script>
    <script
      crossorigin
      src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"
    ></script>
    <script>
      pdfjsLib.GlobalWorkerOptions.workerSrc =
        'https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.worker.min.js'
    </script>
    <script type="application/javascript">
      try {
        window.CUSTOM_STYLE = JSON.parse('${JSON.stringify(
            customStyle ?? {},
        )}');
      } catch (error) {
        window.CUSTOM_STYLE = {}
      }
      try {
        window.WITH_SCROLL = JSON.parse('${JSON.stringify(withScroll)}');
      } catch (error) {
        window.WITH_SCROLL = {}
      }
    </script>
  </head>
  <body>
     <div id="file" data-file="${base64}"></div>
     <div id="react-container"></div>
     <script type="text/javascript" src="bundle.js"></script>
   </body>
</html>
`
}




export default PdfReader;


=========== Use ===============
<PdfReader
              style={{ width:'100%',height: '100%' }}
              source={{
                cache:true ,
                uri: 'https://www.clickdimensions.com/links/TestPDFfile.pdf',
              }}
            />

I hope this will help

Upvotes: 0

Morta
Morta

Reputation: 399

"rn-pdf-reader-js": "^4.1.1"
"expo": "^40.0.0"

The library work just fine for me in their latest version the only issue comes around that the base64 contain octet-stream so i replace it with pdf like that:

   setBase64(reader.result.replace("octet-stream", "pdf"))

and pass it to the source like that:

 <PdfReader
   source={{
     base64: base64,
     }}
  />

I hope this helps you. otherwise, please provide us more details about so we can help.

Upvotes: 0

Hikmert
Hikmert

Reputation: 64

rn-pdf-reader-js seems to be not working for the expo 38+. You can try this fork instead:

https://github.com/stratoss/rn-pdf-reader-js

Just import it as import PDFReader from '@bildau/rn-pdf-reader' and you are good to go.

Upvotes: 3

Related Questions