@webority-technologies/mobile-ui 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RNMobileUiPdfRasterizer.podspec +25 -0
- package/android/build.gradle +64 -0
- package/android/src/newarch/java/com/webority/mobileui/pdfrasterizer/PdfRasterizerModule.kt +211 -0
- package/android/src/newarch/java/com/webority/mobileui/pdfrasterizer/PdfRasterizerPackage.kt +33 -0
- package/ios/PdfRasterizer.h +13 -0
- package/ios/PdfRasterizer.mm +292 -0
- package/lib/commonjs/components/DocumentViewer/DocumentViewer.js +199 -24
- package/lib/commonjs/specs/NativePdfRasterizer.js +9 -0
- package/lib/module/components/DocumentViewer/DocumentViewer.js +201 -26
- package/lib/module/specs/NativePdfRasterizer.js +5 -0
- package/lib/typescript/commonjs/components/DocumentViewer/DocumentViewer.d.ts +12 -4
- package/lib/typescript/commonjs/specs/NativePdfRasterizer.d.ts +40 -0
- package/lib/typescript/module/components/DocumentViewer/DocumentViewer.d.ts +12 -4
- package/lib/typescript/module/specs/NativePdfRasterizer.d.ts +40 -0
- package/package.json +13 -4
|
@@ -5,21 +5,57 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = exports.DocumentViewer = void 0;
|
|
7
7
|
var _mobileCore = require("@webority-technologies/mobile-core");
|
|
8
|
+
var _download = require("@webority-technologies/mobile-core/download");
|
|
8
9
|
var _react = require("react");
|
|
9
10
|
var _reactNative = require("react-native");
|
|
10
|
-
var
|
|
11
|
+
var _NativePdfRasterizer = _interopRequireDefault(require("../../specs/NativePdfRasterizer.js"));
|
|
11
12
|
var _index = require("../Spinner/index.js");
|
|
12
13
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
13
14
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
15
|
+
const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
16
|
+
const MAX_RENDER_SCALE = 3;
|
|
17
|
+
/** How many pages beyond the current one stay mounted, each direction. */
|
|
18
|
+
const RENDER_WINDOW = 1;
|
|
14
19
|
const toError = value => value instanceof Error ? value : new Error(String(value));
|
|
15
20
|
|
|
21
|
+
/** The renderPage scale is capped so a very wide view never asks the native
|
|
22
|
+
* rasterizer for an unbounded bitmap (see react-native.md's ARGB_8888 4-bytes/px
|
|
23
|
+
* guidance) — both native sides additionally clamp their own pixel ceiling. */
|
|
24
|
+
const scaleForWidth = (viewWidthPx, pageWidthPt) => {
|
|
25
|
+
if (pageWidthPt <= 0) {
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const raw = viewWidthPx / pageWidthPt * _reactNative.PixelRatio.get();
|
|
29
|
+
return Math.min(Math.max(raw, 1), MAX_RENDER_SCALE);
|
|
30
|
+
};
|
|
31
|
+
const localPathFromSource = async source => {
|
|
32
|
+
if (!HAS_SCHEME.test(source.uri) || source.uri.startsWith('file://')) {
|
|
33
|
+
return source.uri.replace(/^file:\/\//, '');
|
|
34
|
+
}
|
|
35
|
+
const result = await (0, _download.downloadFile)({
|
|
36
|
+
url: source.uri,
|
|
37
|
+
headers: source.headers,
|
|
38
|
+
authenticated: false,
|
|
39
|
+
overwrite: source.cache === false
|
|
40
|
+
});
|
|
41
|
+
return result.path;
|
|
42
|
+
};
|
|
43
|
+
|
|
16
44
|
/**
|
|
17
|
-
* Renders a PDF
|
|
18
|
-
*
|
|
45
|
+
* Renders a PDF via our own native rasterizer (PDFKit on iOS, PdfRenderer on
|
|
46
|
+
* Android) — no third-party PDF dependency. Pages are paged through a plain
|
|
47
|
+
* `ScrollView` with manual windowed mounting (render the visible page ±
|
|
48
|
+
* `RENDER_WINDOW`), not a `FlatList` — a `VirtualizedList`-based component
|
|
49
|
+
* would trip React Native's "VirtualizedLists should never be nested inside
|
|
50
|
+
* plain ScrollViews" the moment a consumer embeds DocumentViewer in their own
|
|
51
|
+
* scrolling screen, which is a completely ordinary way to use it. Avoiding the
|
|
52
|
+
* VirtualizedList primitive entirely removes that whole class of warning
|
|
53
|
+
* rather than working around one instance of it. The WebView fallback
|
|
54
|
+
* consumer apps use for non-PDF office documents is a distinct rendering
|
|
55
|
+
* strategy and stays app-side.
|
|
19
56
|
*/
|
|
20
57
|
const DocumentViewer = ({
|
|
21
58
|
source,
|
|
22
|
-
trustAllCerts = false,
|
|
23
59
|
onLoadComplete,
|
|
24
60
|
onPageChanged,
|
|
25
61
|
onError,
|
|
@@ -28,29 +64,162 @@ const DocumentViewer = ({
|
|
|
28
64
|
testID,
|
|
29
65
|
accessibilityLabel
|
|
30
66
|
}) => {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
const
|
|
67
|
+
const {
|
|
68
|
+
width: windowWidth
|
|
69
|
+
} = (0, _reactNative.useWindowDimensions)();
|
|
70
|
+
const [pageCount, setPageCount] = (0, _react.useState)(0);
|
|
71
|
+
const [pageSizePt, setPageSizePt] = (0, _react.useState)({
|
|
72
|
+
width: 612,
|
|
73
|
+
height: 792
|
|
74
|
+
}); // US Letter fallback
|
|
75
|
+
const pageAspect = pageSizePt.height / pageSizePt.width;
|
|
76
|
+
const [renderedPages, setRenderedPages] = (0, _react.useState)({});
|
|
77
|
+
const [currentPage, setCurrentPage] = (0, _react.useState)(0);
|
|
78
|
+
const handleRef = (0, _react.useRef)(null);
|
|
79
|
+
const onErrorRef = (0, _react.useRef)(onError);
|
|
80
|
+
onErrorRef.current = onError;
|
|
81
|
+
const onLoadCompleteRef = (0, _react.useRef)(onLoadComplete);
|
|
82
|
+
onLoadCompleteRef.current = onLoadComplete;
|
|
83
|
+
const onPageChangedRef = (0, _react.useRef)(onPageChanged);
|
|
84
|
+
onPageChangedRef.current = onPageChanged;
|
|
85
|
+
const documentSource = (0, _react.useMemo)(() => ({
|
|
86
|
+
uri: source.uri,
|
|
87
|
+
headers: source.headers,
|
|
88
|
+
cache: source.cache
|
|
89
|
+
}), [source.uri, source.headers, source.cache]);
|
|
90
|
+
(0, _react.useEffect)(() => {
|
|
91
|
+
let cancelled = false;
|
|
92
|
+
const load = async () => {
|
|
93
|
+
if (!_NativePdfRasterizer.default) {
|
|
94
|
+
onErrorRef.current?.(new Error('[@webority-technologies/mobile-ui] PdfRasterizer native module is not linked. ' + 'Run pod install / rebuild the app after adding @webority-technologies/mobile-ui.'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const path = await localPathFromSource(documentSource);
|
|
99
|
+
if (cancelled) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const opened = await _NativePdfRasterizer.default.open(path);
|
|
103
|
+
if (cancelled) {
|
|
104
|
+
await _NativePdfRasterizer.default.close(opened.handle).catch(() => undefined);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
handleRef.current = opened.handle;
|
|
108
|
+
setPageCount(opened.pageCount);
|
|
109
|
+
setCurrentPage(0);
|
|
110
|
+
setRenderedPages({});
|
|
111
|
+
if (opened.pageWidth > 0 && opened.pageHeight > 0) {
|
|
112
|
+
setPageSizePt({
|
|
113
|
+
width: opened.pageWidth,
|
|
114
|
+
height: opened.pageHeight
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
onLoadCompleteRef.current?.(opened.pageCount);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (!cancelled) {
|
|
120
|
+
const err = toError(error);
|
|
121
|
+
_mobileCore.Logger.error('[@webority-technologies/mobile-ui] DocumentViewer failed to load.', err);
|
|
122
|
+
onErrorRef.current?.(err);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
void load();
|
|
127
|
+
return () => {
|
|
128
|
+
cancelled = true;
|
|
129
|
+
};
|
|
130
|
+
}, [documentSource]);
|
|
131
|
+
(0, _react.useEffect)(() => () => {
|
|
132
|
+
const openHandle = handleRef.current;
|
|
133
|
+
if (openHandle) {
|
|
134
|
+
_NativePdfRasterizer.default?.close(openHandle).catch(() => undefined);
|
|
135
|
+
}
|
|
136
|
+
}, []);
|
|
137
|
+
const renderPageImage = (0, _react.useCallback)(async pageIndex => {
|
|
138
|
+
const openHandle = handleRef.current;
|
|
139
|
+
if (!openHandle || !_NativePdfRasterizer.default) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const scale = scaleForWidth(windowWidth, pageSizePt.width);
|
|
144
|
+
const uri = await _NativePdfRasterizer.default.renderPage(openHandle, pageIndex, scale);
|
|
145
|
+
setRenderedPages(prev => prev[pageIndex] ? prev : {
|
|
146
|
+
...prev,
|
|
147
|
+
[pageIndex]: uri
|
|
148
|
+
});
|
|
149
|
+
} catch (error) {
|
|
150
|
+
_mobileCore.Logger.error(`[@webority-technologies/mobile-ui] DocumentViewer failed to render page ${pageIndex}.`, toError(error));
|
|
151
|
+
}
|
|
152
|
+
}, [pageSizePt.width, windowWidth]);
|
|
153
|
+
const pageHeight = windowWidth * pageAspect;
|
|
154
|
+
|
|
155
|
+
// Mount and rasterize the current page plus a small window around it;
|
|
156
|
+
// everything else stays an empty placeholder slot until scrolled near.
|
|
157
|
+
(0, _react.useEffect)(() => {
|
|
158
|
+
if (pageCount === 0) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const start = Math.max(0, currentPage - RENDER_WINDOW);
|
|
162
|
+
const end = Math.min(pageCount - 1, currentPage + RENDER_WINDOW);
|
|
163
|
+
for (let index = start; index <= end; index += 1) {
|
|
164
|
+
void renderPageImage(index);
|
|
165
|
+
}
|
|
166
|
+
}, [currentPage, pageCount, renderPageImage]);
|
|
167
|
+
const handleScroll = (0, _react.useCallback)(event => {
|
|
168
|
+
if (pageHeight <= 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const page = Math.min(pageCount - 1, Math.max(0, Math.round(event.nativeEvent.contentOffset.y / pageHeight)));
|
|
172
|
+
setCurrentPage(prev => {
|
|
173
|
+
if (prev !== page) {
|
|
174
|
+
onPageChangedRef.current?.(page + 1, pageCount);
|
|
175
|
+
}
|
|
176
|
+
return page;
|
|
177
|
+
});
|
|
178
|
+
}, [pageCount, pageHeight]);
|
|
179
|
+
const pages = (0, _react.useMemo)(() => Array.from({
|
|
180
|
+
length: pageCount
|
|
181
|
+
}, (_unused, i) => i), [pageCount]);
|
|
182
|
+
const renderWindowStart = Math.max(0, currentPage - RENDER_WINDOW);
|
|
183
|
+
const renderWindowEnd = Math.min(pageCount - 1, currentPage + RENDER_WINDOW);
|
|
40
184
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
41
185
|
style: [styles.root, style],
|
|
42
186
|
testID: testID,
|
|
43
187
|
accessible: accessibilityLabel !== undefined,
|
|
44
188
|
accessibilityLabel: accessibilityLabel,
|
|
45
189
|
accessibilityRole: "none",
|
|
46
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
190
|
+
children: pageCount === 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
191
|
+
style: styles.pagePlaceholder,
|
|
192
|
+
children: loadingIndicator ? loadingIndicator() : /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Spinner, {
|
|
193
|
+
size: "lg"
|
|
194
|
+
})
|
|
195
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
|
|
196
|
+
onScroll: handleScroll,
|
|
197
|
+
scrollEventThrottle: 32,
|
|
198
|
+
showsVerticalScrollIndicator: true,
|
|
199
|
+
removeClippedSubviews: true,
|
|
200
|
+
children: pages.map(index => {
|
|
201
|
+
const withinWindow = index >= renderWindowStart && index <= renderWindowEnd;
|
|
202
|
+
const uri = withinWindow ? renderedPages[index] : undefined;
|
|
203
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
204
|
+
style: [styles.page, {
|
|
205
|
+
width: windowWidth,
|
|
206
|
+
height: pageHeight
|
|
207
|
+
}],
|
|
208
|
+
children: uri ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
|
|
209
|
+
source: {
|
|
210
|
+
uri
|
|
211
|
+
},
|
|
212
|
+
style: styles.pageImage,
|
|
213
|
+
resizeMode: "contain",
|
|
214
|
+
accessible: false
|
|
215
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
216
|
+
style: styles.pagePlaceholder,
|
|
217
|
+
children: withinWindow ? loadingIndicator?.() ?? /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Spinner, {
|
|
218
|
+
size: "lg"
|
|
219
|
+
}) : null
|
|
220
|
+
})
|
|
221
|
+
}, index);
|
|
222
|
+
})
|
|
54
223
|
})
|
|
55
224
|
});
|
|
56
225
|
};
|
|
@@ -60,10 +229,16 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
60
229
|
root: {
|
|
61
230
|
flex: 1
|
|
62
231
|
},
|
|
63
|
-
|
|
232
|
+
page: {
|
|
233
|
+
alignSelf: 'center'
|
|
234
|
+
},
|
|
235
|
+
pageImage: {
|
|
236
|
+
flex: 1
|
|
237
|
+
},
|
|
238
|
+
pagePlaceholder: {
|
|
64
239
|
flex: 1,
|
|
65
|
-
|
|
66
|
-
|
|
240
|
+
alignItems: 'center',
|
|
241
|
+
justifyContent: 'center'
|
|
67
242
|
}
|
|
68
243
|
});
|
|
69
244
|
var _default = exports.default = DocumentViewer;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _reactNative = require("react-native");
|
|
8
|
+
var _default = exports.default = _reactNative.TurboModuleRegistry.get('PdfRasterizer');
|
|
9
|
+
//# sourceMappingURL=NativePdfRasterizer.js.map
|
|
@@ -1,20 +1,56 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { Logger } from '@webority-technologies/mobile-core';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
4
|
+
import { downloadFile } from '@webority-technologies/mobile-core/download';
|
|
5
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { Image, PixelRatio, ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native';
|
|
7
|
+
import PdfRasterizer from "../../specs/NativePdfRasterizer.js";
|
|
7
8
|
import { Spinner } from "../Spinner/index.js";
|
|
8
9
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
11
|
+
const MAX_RENDER_SCALE = 3;
|
|
12
|
+
/** How many pages beyond the current one stay mounted, each direction. */
|
|
13
|
+
const RENDER_WINDOW = 1;
|
|
9
14
|
const toError = value => value instanceof Error ? value : new Error(String(value));
|
|
10
15
|
|
|
16
|
+
/** The renderPage scale is capped so a very wide view never asks the native
|
|
17
|
+
* rasterizer for an unbounded bitmap (see react-native.md's ARGB_8888 4-bytes/px
|
|
18
|
+
* guidance) — both native sides additionally clamp their own pixel ceiling. */
|
|
19
|
+
const scaleForWidth = (viewWidthPx, pageWidthPt) => {
|
|
20
|
+
if (pageWidthPt <= 0) {
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const raw = viewWidthPx / pageWidthPt * PixelRatio.get();
|
|
24
|
+
return Math.min(Math.max(raw, 1), MAX_RENDER_SCALE);
|
|
25
|
+
};
|
|
26
|
+
const localPathFromSource = async source => {
|
|
27
|
+
if (!HAS_SCHEME.test(source.uri) || source.uri.startsWith('file://')) {
|
|
28
|
+
return source.uri.replace(/^file:\/\//, '');
|
|
29
|
+
}
|
|
30
|
+
const result = await downloadFile({
|
|
31
|
+
url: source.uri,
|
|
32
|
+
headers: source.headers,
|
|
33
|
+
authenticated: false,
|
|
34
|
+
overwrite: source.cache === false
|
|
35
|
+
});
|
|
36
|
+
return result.path;
|
|
37
|
+
};
|
|
38
|
+
|
|
11
39
|
/**
|
|
12
|
-
* Renders a PDF
|
|
13
|
-
*
|
|
40
|
+
* Renders a PDF via our own native rasterizer (PDFKit on iOS, PdfRenderer on
|
|
41
|
+
* Android) — no third-party PDF dependency. Pages are paged through a plain
|
|
42
|
+
* `ScrollView` with manual windowed mounting (render the visible page ±
|
|
43
|
+
* `RENDER_WINDOW`), not a `FlatList` — a `VirtualizedList`-based component
|
|
44
|
+
* would trip React Native's "VirtualizedLists should never be nested inside
|
|
45
|
+
* plain ScrollViews" the moment a consumer embeds DocumentViewer in their own
|
|
46
|
+
* scrolling screen, which is a completely ordinary way to use it. Avoiding the
|
|
47
|
+
* VirtualizedList primitive entirely removes that whole class of warning
|
|
48
|
+
* rather than working around one instance of it. The WebView fallback
|
|
49
|
+
* consumer apps use for non-PDF office documents is a distinct rendering
|
|
50
|
+
* strategy and stays app-side.
|
|
14
51
|
*/
|
|
15
52
|
export const DocumentViewer = ({
|
|
16
53
|
source,
|
|
17
|
-
trustAllCerts = false,
|
|
18
54
|
onLoadComplete,
|
|
19
55
|
onPageChanged,
|
|
20
56
|
onError,
|
|
@@ -23,29 +59,162 @@ export const DocumentViewer = ({
|
|
|
23
59
|
testID,
|
|
24
60
|
accessibilityLabel
|
|
25
61
|
}) => {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
const
|
|
62
|
+
const {
|
|
63
|
+
width: windowWidth
|
|
64
|
+
} = useWindowDimensions();
|
|
65
|
+
const [pageCount, setPageCount] = useState(0);
|
|
66
|
+
const [pageSizePt, setPageSizePt] = useState({
|
|
67
|
+
width: 612,
|
|
68
|
+
height: 792
|
|
69
|
+
}); // US Letter fallback
|
|
70
|
+
const pageAspect = pageSizePt.height / pageSizePt.width;
|
|
71
|
+
const [renderedPages, setRenderedPages] = useState({});
|
|
72
|
+
const [currentPage, setCurrentPage] = useState(0);
|
|
73
|
+
const handleRef = useRef(null);
|
|
74
|
+
const onErrorRef = useRef(onError);
|
|
75
|
+
onErrorRef.current = onError;
|
|
76
|
+
const onLoadCompleteRef = useRef(onLoadComplete);
|
|
77
|
+
onLoadCompleteRef.current = onLoadComplete;
|
|
78
|
+
const onPageChangedRef = useRef(onPageChanged);
|
|
79
|
+
onPageChangedRef.current = onPageChanged;
|
|
80
|
+
const documentSource = useMemo(() => ({
|
|
81
|
+
uri: source.uri,
|
|
82
|
+
headers: source.headers,
|
|
83
|
+
cache: source.cache
|
|
84
|
+
}), [source.uri, source.headers, source.cache]);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
let cancelled = false;
|
|
87
|
+
const load = async () => {
|
|
88
|
+
if (!PdfRasterizer) {
|
|
89
|
+
onErrorRef.current?.(new Error('[@webority-technologies/mobile-ui] PdfRasterizer native module is not linked. ' + 'Run pod install / rebuild the app after adding @webority-technologies/mobile-ui.'));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const path = await localPathFromSource(documentSource);
|
|
94
|
+
if (cancelled) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const opened = await PdfRasterizer.open(path);
|
|
98
|
+
if (cancelled) {
|
|
99
|
+
await PdfRasterizer.close(opened.handle).catch(() => undefined);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
handleRef.current = opened.handle;
|
|
103
|
+
setPageCount(opened.pageCount);
|
|
104
|
+
setCurrentPage(0);
|
|
105
|
+
setRenderedPages({});
|
|
106
|
+
if (opened.pageWidth > 0 && opened.pageHeight > 0) {
|
|
107
|
+
setPageSizePt({
|
|
108
|
+
width: opened.pageWidth,
|
|
109
|
+
height: opened.pageHeight
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
onLoadCompleteRef.current?.(opened.pageCount);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (!cancelled) {
|
|
115
|
+
const err = toError(error);
|
|
116
|
+
Logger.error('[@webority-technologies/mobile-ui] DocumentViewer failed to load.', err);
|
|
117
|
+
onErrorRef.current?.(err);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
void load();
|
|
122
|
+
return () => {
|
|
123
|
+
cancelled = true;
|
|
124
|
+
};
|
|
125
|
+
}, [documentSource]);
|
|
126
|
+
useEffect(() => () => {
|
|
127
|
+
const openHandle = handleRef.current;
|
|
128
|
+
if (openHandle) {
|
|
129
|
+
PdfRasterizer?.close(openHandle).catch(() => undefined);
|
|
130
|
+
}
|
|
131
|
+
}, []);
|
|
132
|
+
const renderPageImage = useCallback(async pageIndex => {
|
|
133
|
+
const openHandle = handleRef.current;
|
|
134
|
+
if (!openHandle || !PdfRasterizer) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const scale = scaleForWidth(windowWidth, pageSizePt.width);
|
|
139
|
+
const uri = await PdfRasterizer.renderPage(openHandle, pageIndex, scale);
|
|
140
|
+
setRenderedPages(prev => prev[pageIndex] ? prev : {
|
|
141
|
+
...prev,
|
|
142
|
+
[pageIndex]: uri
|
|
143
|
+
});
|
|
144
|
+
} catch (error) {
|
|
145
|
+
Logger.error(`[@webority-technologies/mobile-ui] DocumentViewer failed to render page ${pageIndex}.`, toError(error));
|
|
146
|
+
}
|
|
147
|
+
}, [pageSizePt.width, windowWidth]);
|
|
148
|
+
const pageHeight = windowWidth * pageAspect;
|
|
149
|
+
|
|
150
|
+
// Mount and rasterize the current page plus a small window around it;
|
|
151
|
+
// everything else stays an empty placeholder slot until scrolled near.
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (pageCount === 0) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const start = Math.max(0, currentPage - RENDER_WINDOW);
|
|
157
|
+
const end = Math.min(pageCount - 1, currentPage + RENDER_WINDOW);
|
|
158
|
+
for (let index = start; index <= end; index += 1) {
|
|
159
|
+
void renderPageImage(index);
|
|
160
|
+
}
|
|
161
|
+
}, [currentPage, pageCount, renderPageImage]);
|
|
162
|
+
const handleScroll = useCallback(event => {
|
|
163
|
+
if (pageHeight <= 0) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const page = Math.min(pageCount - 1, Math.max(0, Math.round(event.nativeEvent.contentOffset.y / pageHeight)));
|
|
167
|
+
setCurrentPage(prev => {
|
|
168
|
+
if (prev !== page) {
|
|
169
|
+
onPageChangedRef.current?.(page + 1, pageCount);
|
|
170
|
+
}
|
|
171
|
+
return page;
|
|
172
|
+
});
|
|
173
|
+
}, [pageCount, pageHeight]);
|
|
174
|
+
const pages = useMemo(() => Array.from({
|
|
175
|
+
length: pageCount
|
|
176
|
+
}, (_unused, i) => i), [pageCount]);
|
|
177
|
+
const renderWindowStart = Math.max(0, currentPage - RENDER_WINDOW);
|
|
178
|
+
const renderWindowEnd = Math.min(pageCount - 1, currentPage + RENDER_WINDOW);
|
|
35
179
|
return /*#__PURE__*/_jsx(View, {
|
|
36
180
|
style: [styles.root, style],
|
|
37
181
|
testID: testID,
|
|
38
182
|
accessible: accessibilityLabel !== undefined,
|
|
39
183
|
accessibilityLabel: accessibilityLabel,
|
|
40
184
|
accessibilityRole: "none",
|
|
41
|
-
children: /*#__PURE__*/_jsx(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
185
|
+
children: pageCount === 0 ? /*#__PURE__*/_jsx(View, {
|
|
186
|
+
style: styles.pagePlaceholder,
|
|
187
|
+
children: loadingIndicator ? loadingIndicator() : /*#__PURE__*/_jsx(Spinner, {
|
|
188
|
+
size: "lg"
|
|
189
|
+
})
|
|
190
|
+
}) : /*#__PURE__*/_jsx(ScrollView, {
|
|
191
|
+
onScroll: handleScroll,
|
|
192
|
+
scrollEventThrottle: 32,
|
|
193
|
+
showsVerticalScrollIndicator: true,
|
|
194
|
+
removeClippedSubviews: true,
|
|
195
|
+
children: pages.map(index => {
|
|
196
|
+
const withinWindow = index >= renderWindowStart && index <= renderWindowEnd;
|
|
197
|
+
const uri = withinWindow ? renderedPages[index] : undefined;
|
|
198
|
+
return /*#__PURE__*/_jsx(View, {
|
|
199
|
+
style: [styles.page, {
|
|
200
|
+
width: windowWidth,
|
|
201
|
+
height: pageHeight
|
|
202
|
+
}],
|
|
203
|
+
children: uri ? /*#__PURE__*/_jsx(Image, {
|
|
204
|
+
source: {
|
|
205
|
+
uri
|
|
206
|
+
},
|
|
207
|
+
style: styles.pageImage,
|
|
208
|
+
resizeMode: "contain",
|
|
209
|
+
accessible: false
|
|
210
|
+
}) : /*#__PURE__*/_jsx(View, {
|
|
211
|
+
style: styles.pagePlaceholder,
|
|
212
|
+
children: withinWindow ? loadingIndicator?.() ?? /*#__PURE__*/_jsx(Spinner, {
|
|
213
|
+
size: "lg"
|
|
214
|
+
}) : null
|
|
215
|
+
})
|
|
216
|
+
}, index);
|
|
217
|
+
})
|
|
49
218
|
})
|
|
50
219
|
});
|
|
51
220
|
};
|
|
@@ -54,10 +223,16 @@ const styles = StyleSheet.create({
|
|
|
54
223
|
root: {
|
|
55
224
|
flex: 1
|
|
56
225
|
},
|
|
57
|
-
|
|
226
|
+
page: {
|
|
227
|
+
alignSelf: 'center'
|
|
228
|
+
},
|
|
229
|
+
pageImage: {
|
|
230
|
+
flex: 1
|
|
231
|
+
},
|
|
232
|
+
pagePlaceholder: {
|
|
58
233
|
flex: 1,
|
|
59
|
-
|
|
60
|
-
|
|
234
|
+
alignItems: 'center',
|
|
235
|
+
justifyContent: 'center'
|
|
61
236
|
}
|
|
62
237
|
});
|
|
63
238
|
export default DocumentViewer;
|
|
@@ -3,12 +3,11 @@ import type { StyleProp, ViewStyle } from 'react-native';
|
|
|
3
3
|
export interface DocumentViewerSource {
|
|
4
4
|
uri: string;
|
|
5
5
|
headers?: Record<string, string>;
|
|
6
|
+
/** Reuse a previously downloaded copy of this URL instead of re-fetching. Default true. */
|
|
6
7
|
cache?: boolean;
|
|
7
8
|
}
|
|
8
9
|
export interface DocumentViewerProps {
|
|
9
10
|
source: DocumentViewerSource;
|
|
10
|
-
/** Trust self-signed / invalid certs on the document request. Default false. */
|
|
11
|
-
trustAllCerts?: boolean;
|
|
12
11
|
onLoadComplete?: (numberOfPages: number) => void;
|
|
13
12
|
onPageChanged?: (page: number, numberOfPages: number) => void;
|
|
14
13
|
onError?: (error: Error) => void;
|
|
@@ -19,8 +18,17 @@ export interface DocumentViewerProps {
|
|
|
19
18
|
accessibilityLabel?: string;
|
|
20
19
|
}
|
|
21
20
|
/**
|
|
22
|
-
* Renders a PDF
|
|
23
|
-
*
|
|
21
|
+
* Renders a PDF via our own native rasterizer (PDFKit on iOS, PdfRenderer on
|
|
22
|
+
* Android) — no third-party PDF dependency. Pages are paged through a plain
|
|
23
|
+
* `ScrollView` with manual windowed mounting (render the visible page ±
|
|
24
|
+
* `RENDER_WINDOW`), not a `FlatList` — a `VirtualizedList`-based component
|
|
25
|
+
* would trip React Native's "VirtualizedLists should never be nested inside
|
|
26
|
+
* plain ScrollViews" the moment a consumer embeds DocumentViewer in their own
|
|
27
|
+
* scrolling screen, which is a completely ordinary way to use it. Avoiding the
|
|
28
|
+
* VirtualizedList primitive entirely removes that whole class of warning
|
|
29
|
+
* rather than working around one instance of it. The WebView fallback
|
|
30
|
+
* consumer apps use for non-PDF office documents is a distinct rendering
|
|
31
|
+
* strategy and stays app-side.
|
|
24
32
|
*/
|
|
25
33
|
export declare const DocumentViewer: React.FC<DocumentViewerProps>;
|
|
26
34
|
export default DocumentViewer;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
export interface PdfDocumentInfo {
|
|
3
|
+
/** Opaque handle for every subsequent call (getPageInfo/renderPage/close) — NOT the file path. */
|
|
4
|
+
handle: string;
|
|
5
|
+
pageCount: number;
|
|
6
|
+
/** Points (1/72 in), the unrotated page size of page 0 — a per-page size read happens in renderPage. */
|
|
7
|
+
pageWidth: number;
|
|
8
|
+
pageHeight: number;
|
|
9
|
+
}
|
|
10
|
+
export interface PdfPageInfo {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
export interface Spec extends TurboModule {
|
|
15
|
+
/**
|
|
16
|
+
* Opens a LOCAL pdf file (a file:// path or bare absolute path — no http(s), no
|
|
17
|
+
* headers, no auth: callers download remote documents with mobile-core's
|
|
18
|
+
* `downloadFile` first). Returns a handle used by every other call. Throws
|
|
19
|
+
* (rejects) on a missing file, a corrupt PDF, or a password-protected PDF.
|
|
20
|
+
*/
|
|
21
|
+
open(path: string): Promise<PdfDocumentInfo>;
|
|
22
|
+
/**
|
|
23
|
+
* Reads one page's own size without rasterizing it (pages in a PDF can each
|
|
24
|
+
* have a different size/rotation).
|
|
25
|
+
*/
|
|
26
|
+
getPageInfo(handle: string, pageIndex: number): Promise<PdfPageInfo>;
|
|
27
|
+
/**
|
|
28
|
+
* Rasterizes one page to a PNG file in the cache directory and returns its
|
|
29
|
+
* absolute path. `scale` is a multiplier on the page's own point size (pass
|
|
30
|
+
* the view width / page width ratio, capped by the caller — see
|
|
31
|
+
* react-native.md's "RGBA_8888 is 4 bytes/px" guidance). Renders are
|
|
32
|
+
* serialized per handle on both platforms; call sequentially per document.
|
|
33
|
+
*/
|
|
34
|
+
renderPage(handle: string, pageIndex: number, scale: number): Promise<string>;
|
|
35
|
+
/** Releases the native document and evicts any cached page renders for it. */
|
|
36
|
+
close(handle: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
declare const _default: Spec | null;
|
|
39
|
+
export default _default;
|
|
40
|
+
//# sourceMappingURL=NativePdfRasterizer.d.ts.map
|
|
@@ -3,12 +3,11 @@ import type { StyleProp, ViewStyle } from 'react-native';
|
|
|
3
3
|
export interface DocumentViewerSource {
|
|
4
4
|
uri: string;
|
|
5
5
|
headers?: Record<string, string>;
|
|
6
|
+
/** Reuse a previously downloaded copy of this URL instead of re-fetching. Default true. */
|
|
6
7
|
cache?: boolean;
|
|
7
8
|
}
|
|
8
9
|
export interface DocumentViewerProps {
|
|
9
10
|
source: DocumentViewerSource;
|
|
10
|
-
/** Trust self-signed / invalid certs on the document request. Default false. */
|
|
11
|
-
trustAllCerts?: boolean;
|
|
12
11
|
onLoadComplete?: (numberOfPages: number) => void;
|
|
13
12
|
onPageChanged?: (page: number, numberOfPages: number) => void;
|
|
14
13
|
onError?: (error: Error) => void;
|
|
@@ -19,8 +18,17 @@ export interface DocumentViewerProps {
|
|
|
19
18
|
accessibilityLabel?: string;
|
|
20
19
|
}
|
|
21
20
|
/**
|
|
22
|
-
* Renders a PDF
|
|
23
|
-
*
|
|
21
|
+
* Renders a PDF via our own native rasterizer (PDFKit on iOS, PdfRenderer on
|
|
22
|
+
* Android) — no third-party PDF dependency. Pages are paged through a plain
|
|
23
|
+
* `ScrollView` with manual windowed mounting (render the visible page ±
|
|
24
|
+
* `RENDER_WINDOW`), not a `FlatList` — a `VirtualizedList`-based component
|
|
25
|
+
* would trip React Native's "VirtualizedLists should never be nested inside
|
|
26
|
+
* plain ScrollViews" the moment a consumer embeds DocumentViewer in their own
|
|
27
|
+
* scrolling screen, which is a completely ordinary way to use it. Avoiding the
|
|
28
|
+
* VirtualizedList primitive entirely removes that whole class of warning
|
|
29
|
+
* rather than working around one instance of it. The WebView fallback
|
|
30
|
+
* consumer apps use for non-PDF office documents is a distinct rendering
|
|
31
|
+
* strategy and stays app-side.
|
|
24
32
|
*/
|
|
25
33
|
export declare const DocumentViewer: React.FC<DocumentViewerProps>;
|
|
26
34
|
export default DocumentViewer;
|