Skip to content

Commit c70a619

Browse files
authored
Merge pull request #22760 from rezkiy37/fix/19918-preview-large-pdf
Fix freezes when previewing a large PDF file
2 parents 316a545 + 717f296 commit c70a619

File tree

4 files changed

+146
-24
lines changed

4 files changed

+146
-24
lines changed

package-lock.json

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
"react-pdf": "^6.2.2",
142142
"react-plaid-link": "3.3.2",
143143
"react-web-config": "^1.0.0",
144+
"react-window": "^1.8.9",
144145
"save": "^2.4.0",
145146
"semver": "^7.3.8",
146147
"shim-keyboard-event-key": "^1.0.3",

src/components/PDFView/index.js

Lines changed: 115 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import _ from 'underscore';
22
import React, {Component} from 'react';
3-
import {View, Dimensions} from 'react-native';
3+
import {View} from 'react-native';
44
import 'core-js/features/array/at';
55
import {Document, Page, pdfjs} from 'react-pdf/dist/esm/entry.webpack';
66
import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker';
7+
import {VariableSizeList as List} from 'react-window';
78
import FullScreenLoadingIndicator from '../FullscreenLoadingIndicator';
89
import styles from '../../styles/styles';
910
import variables from '../../styles/variables';
@@ -15,13 +16,27 @@ import withLocalize from '../withLocalize';
1516
import Text from '../Text';
1617
import compose from '../../libs/compose';
1718
import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback';
19+
import Log from '../../libs/Log';
20+
21+
/**
22+
* Each page has a default border. The app should take this size into account
23+
* when calculates the page width and height.
24+
*/
25+
const PAGE_BORDER = 9;
26+
/**
27+
* Pages should be more narrow than the container on large screens. The app should take this size into account
28+
* when calculates the page width.
29+
*/
30+
const LARGE_SCREEN_SIDE_SPACING = 40;
1831

1932
class PDFView extends Component {
2033
constructor(props) {
2134
super(props);
2235
this.state = {
2336
numPages: null,
24-
windowWidth: Dimensions.get('window').width,
37+
pageViewports: [],
38+
containerWidth: props.windowWidth,
39+
containerHeight: props.windowHeight,
2540
shouldRequestPassword: false,
2641
isPasswordInvalid: false,
2742
isKeyboardOpen: false,
@@ -30,6 +45,9 @@ class PDFView extends Component {
3045
this.initiatePasswordChallenge = this.initiatePasswordChallenge.bind(this);
3146
this.attemptPDFLoad = this.attemptPDFLoad.bind(this);
3247
this.toggleKeyboardOnSmallScreens = this.toggleKeyboardOnSmallScreens.bind(this);
48+
this.calculatePageHeight = this.calculatePageHeight.bind(this);
49+
this.calculatePageWidth = this.calculatePageWidth.bind(this);
50+
this.renderPage = this.renderPage.bind(this);
3351

3452
const workerBlob = new Blob([pdfWorkerSource], {type: 'text/javascript'});
3553
pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(workerBlob);
@@ -50,21 +68,70 @@ class PDFView extends Component {
5068
}
5169

5270
/**
53-
* Upon successful document load, set the number of pages on PDF,
71+
* Upon successful document load, combine an array of page viewports,
72+
* set the number of pages on PDF,
5473
* hide/reset PDF password form, and notify parent component that
5574
* user input is no longer required.
5675
*
57-
* @param {*} {numPages} No of pages in the rendered PDF
76+
* @param {Object} pdf - The PDF file instance
77+
* @param {Number} pdf.numPages - Number of pages of the PDF file
78+
* @param {Function} pdf.getPage - A method to get page by its number. It requires to have the context. It should be the pdf itself.
5879
* @memberof PDFView
5980
*/
60-
onDocumentLoadSuccess({numPages}) {
61-
this.setState({
62-
numPages,
63-
shouldRequestPassword: false,
64-
isPasswordInvalid: false,
81+
onDocumentLoadSuccess(pdf) {
82+
const {numPages} = pdf;
83+
84+
Promise.all(
85+
_.times(numPages, (index) => {
86+
const pageNumber = index + 1;
87+
88+
return pdf.getPage(pageNumber).then((page) => page.getViewport({scale: 1}));
89+
}),
90+
).then((pageViewports) => {
91+
this.setState({
92+
pageViewports,
93+
numPages,
94+
shouldRequestPassword: false,
95+
isPasswordInvalid: false,
96+
});
6597
});
6698
}
6799

100+
/**
101+
* Calculates a proper page height. The method should be called only when there are page viewports.
102+
* It is based on a ratio between the specific page viewport width and provided page width.
103+
* Also, the app should take into account the page borders.
104+
* @param {*} pageIndex
105+
* @returns {Number}
106+
*/
107+
calculatePageHeight(pageIndex) {
108+
if (this.state.pageViewports.length === 0) {
109+
Log.warn('Dev error: calculatePageHeight() in PDFView called too early');
110+
111+
return 0;
112+
}
113+
114+
const pageViewport = this.state.pageViewports[pageIndex];
115+
const pageWidth = this.calculatePageWidth();
116+
const scale = pageWidth / pageViewport.width;
117+
const actualHeight = pageViewport.height * scale + PAGE_BORDER * 2;
118+
119+
return actualHeight;
120+
}
121+
122+
/**
123+
* Calculates a proper page width.
124+
* It depends on a screen size. Also, the app should take into account the page borders.
125+
* @returns {Number}
126+
*/
127+
calculatePageWidth() {
128+
const pdfContainerWidth = this.state.containerWidth;
129+
const pageWidthOnLargeScreen = Math.min(pdfContainerWidth - LARGE_SCREEN_SIDE_SPACING * 2, variables.pdfPageMaxWidth);
130+
const pageWidth = this.props.isSmallScreenWidth ? this.state.containerWidth : pageWidthOnLargeScreen;
131+
132+
return pageWidth + PAGE_BORDER * 2;
133+
}
134+
68135
/**
69136
* Initiate password challenge process. The react-pdf/Document
70137
* component calls this handler to indicate that a PDF requires a
@@ -110,10 +177,29 @@ class PDFView extends Component {
110177
this.props.onToggleKeyboard(isKeyboardOpen);
111178
}
112179

180+
/**
181+
* It is a currying method that returns a function that renders a specific page based on its index.
182+
* The function includes a wrapper to apply virtualized styles.
183+
* @param {Number} pageWidth
184+
* @returns {JSX.Element}
185+
*/
186+
renderPage(pageWidth) {
187+
return ({index, style}) => (
188+
<View style={style}>
189+
<Page
190+
key={`page_${index}`}
191+
width={pageWidth}
192+
pageIndex={index}
193+
// This needs to be empty to avoid multiple loading texts which show per page and look ugly
194+
// See https://github.com/Expensify/App/issues/14358 for more details
195+
loading=""
196+
/>
197+
</View>
198+
);
199+
}
200+
113201
renderPDFView() {
114-
const pdfContainerWidth = this.state.windowWidth - 100;
115-
const pageWidthOnLargeScreen = pdfContainerWidth <= variables.pdfPageMaxWidth ? pdfContainerWidth : variables.pdfPageMaxWidth;
116-
const pageWidth = this.props.isSmallScreenWidth ? this.state.windowWidth : pageWidthOnLargeScreen;
202+
const pageWidth = this.calculatePageWidth();
117203
const outerContainerStyle = [styles.w100, styles.h100, styles.justifyContentCenter, styles.alignItemsCenter];
118204

119205
// If we're requesting a password then we need to hide - but still render -
@@ -127,7 +213,11 @@ class PDFView extends Component {
127213
<View
128214
focusable
129215
style={pdfContainerStyle}
130-
onLayout={(event) => this.setState({windowWidth: event.nativeEvent.layout.width})}
216+
onLayout={({
217+
nativeEvent: {
218+
layout: {width, height},
219+
},
220+
}) => this.setState({containerWidth: width, containerHeight: height})}
131221
>
132222
<Document
133223
error={<Text style={[styles.textLabel, styles.textLarge]}>{this.props.translate('attachmentView.failedToLoadPDF')}</Text>}
@@ -141,16 +231,18 @@ class PDFView extends Component {
141231
onLoadSuccess={this.onDocumentLoadSuccess}
142232
onPassword={this.initiatePasswordChallenge}
143233
>
144-
{_.map(_.range(this.state.numPages), (v, index) => (
145-
<Page
146-
width={pageWidth}
147-
key={`page_${index + 1}`}
148-
pageNumber={index + 1}
149-
// This needs to be empty to avoid multiple loading texts which show per page and look ugly
150-
// See https://github.com/Expensify/App/issues/14358 for more details
151-
loading=""
152-
/>
153-
))}
234+
{this.state.pageViewports.length > 0 && (
235+
<List
236+
style={styles.PDFViewList}
237+
width={this.props.isSmallScreenWidth ? pageWidth : this.state.containerWidth}
238+
height={this.state.containerHeight}
239+
estimatedItemSize={this.calculatePageHeight(0)}
240+
itemCount={this.state.numPages}
241+
itemSize={this.calculatePageHeight}
242+
>
243+
{this.renderPage(pageWidth)}
244+
</List>
245+
)}
154246
</Document>
155247
</View>
156248
{this.state.shouldRequestPassword && (

src/styles/styles.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2104,10 +2104,13 @@ const styles = {
21042104
height: '100%',
21052105
justifyContent: 'center',
21062106
overflow: 'hidden',
2107-
overflowY: 'auto',
21082107
alignItems: 'center',
21092108
},
21102109

2110+
PDFViewList: {
2111+
overflowX: 'hidden',
2112+
},
2113+
21112114
pdfPasswordForm: {
21122115
wideScreenWidth: {
21132116
width: 350,

0 commit comments

Comments
 (0)