Skip to content

Commit df34f0e

Browse files
authored
Merge pull request #53 from callstack-internal/feature/rotation
feat: add rotation
2 parents 77a9f96 + 03d749d commit df34f0e

File tree

5 files changed

+75
-21
lines changed

5 files changed

+75
-21
lines changed

example/src/App.tsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,54 @@ import React, {useState} from 'react';
22
import pdfWorkerSource from 'pdfjs-dist/build/pdf.worker.min.mjs';
33
import * as pdfjs from 'pdfjs-dist';
44
import ReactFastPDF, {PDFPreviewer} from 'react-fast-pdf';
5+
import type {RotationDegrees} from 'react-fast-pdf';
56
import './index.css';
67

78
pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(new Blob([pdfWorkerSource], {type: 'text/javascript'}));
89

910
function App() {
1011
const [file, setFile] = useState<string | null>(null);
12+
const [rotation, setRotation] = useState<RotationDegrees>(0);
1113

1214
// `.default` is required when referencing the legacy CJS package.
1315
const packageName = ('default' in ReactFastPDF ? (ReactFastPDF.default as {PackageName: string}) : ReactFastPDF).PackageName;
1416

17+
const handleRotate = () => {
18+
setRotation((prev) => ((prev + 90) % 360) as RotationDegrees);
19+
};
20+
1521
return (
1622
<main className="container">
1723
<h1 className="title">Hello, I am {packageName}!</h1>
1824

1925
{file ? (
2026
<>
21-
<button
22-
className="button button_back"
23-
type="button"
24-
onClick={() => setFile(null)}
25-
>
26-
Back
27-
</button>
27+
<div style={{display: 'flex', gap: '10px', marginBottom: '10px', flexWrap: 'wrap'}}>
28+
<button
29+
className="button button_back"
30+
type="button"
31+
onClick={() => {
32+
setFile(null);
33+
setRotation(0);
34+
}}
35+
>
36+
Back
37+
</button>
38+
39+
<button
40+
className="button"
41+
type="button"
42+
onClick={handleRotate}
43+
>
44+
Rotate 90° (Current: {rotation}°)
45+
</button>
46+
</div>
2847

2948
<PDFPreviewer
3049
file={file}
3150
pageMaxWidth={1000}
3251
isSmallScreen={false}
52+
rotation={rotation}
3353
/>
3454
</>
3555
) : (

src/PDFPreviewer.tsx

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {Document} from 'react-pdf';
66
import 'react-pdf/dist/Page/AnnotationLayer.css';
77
import 'react-pdf/dist/Page/TextLayer.css';
88

9-
import type {PDFDocument, PageViewport} from './types.js';
9+
import type {PDFDocument, PageViewport, RotationDegrees} from './types.js';
1010
import {pdfPreviewerStyles as styles} from './styles.js';
1111
import PDFPasswordForm, {type PDFPasswordFormProps} from './PDFPasswordForm.js';
1212
import PageRenderer from './PageRenderer.js';
@@ -27,6 +27,7 @@ type Props = {
2727
onLoadError?: () => void;
2828
containerStyle?: CSSProperties;
2929
contentContainerStyle?: CSSProperties;
30+
rotation?: RotationDegrees;
3031
};
3132

3233
type OnPasswordCallback = (password: string | null) => void;
@@ -48,7 +49,8 @@ function PDFPreviewer({
4849
contentContainerStyle,
4950
shouldShowErrorComponent = true,
5051
onLoadError,
51-
}: Props) {
52+
rotation = 0,
53+
}: Props): JSX.Element {
5254
const [pageViewports, setPageViewports] = useState<PageViewport[]>([]);
5355
const [numPages, setNumPages] = useState(0);
5456
const [containerWidth, setContainerWidth] = useState(0);
@@ -100,6 +102,7 @@ function PDFPreviewer({
100102
* Calculates a proper page height. The method should be called only when there are page viewports.
101103
* It is based on a ratio between the specific page viewport width and provided page width.
102104
* Also, the app should take into account the page borders.
105+
* When rotation is 90 or 270 degrees, width and height are swapped.
103106
*/
104107
const calculatePageHeight = useCallback(
105108
(pageIndex: number) => {
@@ -109,12 +112,18 @@ function PDFPreviewer({
109112

110113
const pageWidth = calculatePageWidth();
111114

112-
const {width: pageViewportWidth, height: pageViewportHeight} = pageViewports[pageIndex];
115+
const {width: originalWidth, height: originalHeight} = pageViewports[pageIndex];
116+
117+
// Swap dimensions when rotated 90 or 270 degrees
118+
const isRotated90or270 = rotation === 90 || rotation === 270;
119+
const pageViewportWidth = isRotated90or270 ? originalHeight : originalWidth;
120+
const pageViewportHeight = isRotated90or270 ? originalWidth : originalHeight;
121+
113122
const scale = pageWidth / pageViewportWidth;
114123

115124
return pageViewportHeight * scale + PAGE_BORDER * 2;
116125
},
117-
[pageViewports, calculatePageWidth],
126+
[pageViewports, calculatePageWidth, rotation],
118127
);
119128

120129
const estimatedPageHeight = calculatePageHeight(0);
@@ -204,7 +213,15 @@ function PDFPreviewer({
204213
if (containerWidth > 0 && containerHeight > 0) {
205214
listRef.current?.resetAfterIndex(0);
206215
}
207-
}, [containerWidth, containerHeight]);
216+
}, [containerWidth, containerHeight, rotation]);
217+
218+
/**
219+
* Scroll back to the top whenever rotation changes so the list offset
220+
* is consistent regardless of how page dimensions change.
221+
*/
222+
useLayoutEffect(() => {
223+
listRef.current?.scrollTo(0);
224+
}, [rotation]);
208225

209226
useLayoutEffect(() => {
210227
if (!containerRef.current) {
@@ -227,14 +244,20 @@ function PDFPreviewer({
227244
ref={containerRef}
228245
style={{...styles.container, ...containerStyle}}
229246
>
230-
<div style={{...styles.innerContainer, ...(shouldRequestPassword ? styles.invisibleContainer : {})}}>
247+
<div
248+
style={{
249+
...styles.innerContainer,
250+
...(shouldRequestPassword ? styles.invisibleContainer : {}),
251+
}}
252+
>
231253
<Document
232254
file={file}
233255
options={DEFAULT_DOCUMENT_OPTIONS}
234256
externalLinkTarget={DEFAULT_EXTERNAL_LINK_TARGET}
235257
error={shouldShowErrorComponent ? ErrorComponent : null}
236258
onLoadError={onLoadError}
237259
loading={LoadingComponent}
260+
rotate={rotation}
238261
onLoadSuccess={onDocumentLoadSuccess}
239262
onPassword={initiatePasswordChallenge}
240263
>
@@ -244,11 +267,17 @@ function PDFPreviewer({
244267
style={{...styles.list, ...contentContainerStyle}}
245268
outerRef={setListAttributes}
246269
width={isSmallScreen ? pageWidth : containerWidth}
247-
height={containerHeight}
270+
height={numPages === 1 && estimatedPageHeight < containerHeight ? estimatedPageHeight : containerHeight}
248271
itemCount={numPages}
249272
itemSize={calculatePageHeight}
250273
estimatedItemSize={calculatePageHeight(0)}
251-
itemData={{pageWidth, estimatedPageHeight, calculatePageHeight, getDevicePixelRatio, containerHeight, numPages}}
274+
itemData={{
275+
pageWidth,
276+
estimatedPageHeight,
277+
calculatePageHeight,
278+
getDevicePixelRatio,
279+
numPages,
280+
}}
252281
>
253282
{PageRenderer}
254283
</List>
@@ -261,6 +290,6 @@ function PDFPreviewer({
261290
);
262291
}
263292

264-
PDFPasswordForm.displayName = 'PDFPreviewer';
293+
PDFPreviewer.displayName = 'PDFPreviewer';
265294

266295
export default memo(PDFPreviewer);

src/PageRenderer.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,19 @@ type Props = {
1212
calculatePageHeight: (pageIndex: number) => number;
1313
getDevicePixelRatio: (width: number, height: number) => number | undefined;
1414
numPages: number;
15-
containerHeight: number;
1615
};
1716
};
1817

1918
function PageRenderer({index, style, data}: Props) {
20-
const {pageWidth, estimatedPageHeight, calculatePageHeight, getDevicePixelRatio, numPages, containerHeight} = data;
19+
const {pageWidth, estimatedPageHeight, calculatePageHeight, getDevicePixelRatio, numPages} = data;
2120
/**
2221
* Render a specific page based on its index.
2322
* The method includes a wrapper to apply virtualized styles.
2423
*/
2524
const pageHeight = calculatePageHeight(index);
2625
const devicePixelRatio = getDevicePixelRatio(pageWidth, pageHeight);
27-
const parsedHeight = parseFloat(style.height as unknown as string);
2826
const parsedTop = parseFloat(style.top as unknown as string);
29-
const topPadding = numPages > 1 || parsedHeight > containerHeight ? parsedTop + PAGE_BORDER : (containerHeight - parsedHeight) / 2;
27+
const topPadding = numPages === 1 ? parsedTop : parsedTop + PAGE_BORDER;
3028
return (
3129
<div style={{...styles.pageWrapper, ...style, top: `${topPadding}px`}}>
3230
<Page

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {pdfjs} from 'react-pdf';
22
import PDFPreviewer from './PDFPreviewer.js';
3+
import type {RotationDegrees} from './types.js';
34

45
const PACKAGE_NAME = 'react-fast-pdf';
56

67
export {PDFPreviewer, pdfjs};
8+
export type {RotationDegrees};
79

810
export default {
911
PackageName: PACKAGE_NAME,

src/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,9 @@ type ComponentStyles = {
1818
[key: string]: CSSProperties;
1919
};
2020

21-
export type {PDFDocument, PageViewport, ComponentStyles};
21+
/**
22+
* Valid rotation angles for PDF pages (in degrees clockwise)
23+
*/
24+
type RotationDegrees = 0 | 90 | 180 | 270;
25+
26+
export type {PDFDocument, PageViewport, ComponentStyles, RotationDegrees};

0 commit comments

Comments
 (0)