react-native-pdfrender 0.1.0
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/README.md +306 -0
- package/android/build.gradle +76 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/pdfrender/ComposeRenderer.kt +85 -0
- package/android/src/main/java/com/pdfrender/PdfCacheManager.kt +150 -0
- package/android/src/main/java/com/pdfrender/PdfConstants.kt +63 -0
- package/android/src/main/java/com/pdfrender/PdfIconComponents.kt +275 -0
- package/android/src/main/java/com/pdfrender/PdfRenderingLogic.kt +325 -0
- package/android/src/main/java/com/pdfrender/PdfUIComponents.kt +335 -0
- package/android/src/main/java/com/pdfrender/PdfViewPackage.kt +32 -0
- package/android/src/main/java/com/pdfrender/PdfViewerActivity.kt +3467 -0
- package/android/src/main/java/com/pdfrender/PdfViewerFabricManager.kt +244 -0
- package/android/src/main/java/com/pdfrender/PdfViewerFragment.kt +129 -0
- package/android/src/main/java/com/pdfrender/PdfViewerTurboModule.kt +158 -0
- package/android/src/main/java/com/pdfrender/events/FullScreenChangeEvent.kt +26 -0
- package/android/src/main/java/com/pdfrender/events/LeftScreenChangeEvent.kt +22 -0
- package/android/src/main/java/com/pdfrender/events/RightScreenChangeEvent.kt +22 -0
- package/android/src/main/java/com/pdfrender/events/ZoomChangeEvent.kt +22 -0
- package/ios/PdfCacheManager.swift +44 -0
- package/ios/PdfConstants.swift +38 -0
- package/ios/PdfPageView.swift +121 -0
- package/ios/PdfRenderingLogic.swift +107 -0
- package/ios/PdfToolbarView.swift +158 -0
- package/ios/PdfViewerComponentView.mm +194 -0
- package/ios/PdfViewerTurboModule.mm +186 -0
- package/ios/PdfViewerTurboModuleImpl.swift +141 -0
- package/ios/PdfViewerView.swift +268 -0
- package/ios/PdfViewerViewController.swift +109 -0
- package/lib/commonjs/PdfViewerView.js +105 -0
- package/lib/commonjs/PdfViewerView.js.map +1 -0
- package/lib/commonjs/index.js +28 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/NativePdfViewerComponent.js +27 -0
- package/lib/commonjs/specs/NativePdfViewerComponent.js.map +1 -0
- package/lib/commonjs/specs/NativePdfViewerModule.js +21 -0
- package/lib/commonjs/specs/NativePdfViewerModule.js.map +1 -0
- package/lib/commonjs/usePdfViewer.js +65 -0
- package/lib/commonjs/usePdfViewer.js.map +1 -0
- package/lib/module/PdfViewerView.js +99 -0
- package/lib/module/PdfViewerView.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/specs/NativePdfViewerComponent.js +26 -0
- package/lib/module/specs/NativePdfViewerComponent.js.map +1 -0
- package/lib/module/specs/NativePdfViewerModule.js +18 -0
- package/lib/module/specs/NativePdfViewerModule.js.map +1 -0
- package/lib/module/usePdfViewer.js +60 -0
- package/lib/module/usePdfViewer.js.map +1 -0
- package/lib/typescript/PdfViewerView.d.ts +58 -0
- package/lib/typescript/PdfViewerView.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +7 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/specs/NativePdfViewerComponent.d.ts +59 -0
- package/lib/typescript/specs/NativePdfViewerComponent.d.ts.map +1 -0
- package/lib/typescript/specs/NativePdfViewerModule.d.ts +47 -0
- package/lib/typescript/specs/NativePdfViewerModule.d.ts.map +1 -0
- package/lib/typescript/usePdfViewer.d.ts +45 -0
- package/lib/typescript/usePdfViewer.d.ts.map +1 -0
- package/package.json +109 -0
- package/react-native-pdfrender.podspec +35 -0
- package/react-native.config.js +11 -0
- package/src/PdfViewerView.tsx +159 -0
- package/src/index.tsx +10 -0
- package/src/specs/NativePdfViewerComponent.ts +94 -0
- package/src/specs/NativePdfViewerModule.ts +58 -0
- package/src/usePdfViewer.ts +102 -0
package/README.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# react-native-pdfrender
|
|
2
|
+
|
|
3
|
+
A production-ready React Native PDF rendering library built on the **New Architecture** (TurboModules + Fabric components).
|
|
4
|
+
|
|
5
|
+
- **Embedded viewer** — drop `<PdfViewerView />` anywhere in your render tree
|
|
6
|
+
- **Imperative viewer** — use the `usePdfViewer` hook to open a modal-style PDF viewer
|
|
7
|
+
- **Native rendering** — CGPDFDocument / Core Graphics on iOS, Android PdfRenderer on Android
|
|
8
|
+
- **Jetpack Compose** — modern Android UI, no XML layouts
|
|
9
|
+
- **Codegen-driven** — TypeScript specs generate type-safe native bindings at build time
|
|
10
|
+
- **Zero JS dependencies** — no third-party npm packages required at runtime
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
| Platform | Minimum version |
|
|
17
|
+
|----------|----------------|
|
|
18
|
+
| React Native | 0.70+ |
|
|
19
|
+
| iOS | 13.0+ |
|
|
20
|
+
| Android | SDK 24+ (Android 7.0) |
|
|
21
|
+
| Node | 18+ |
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install react-native-pdfrender
|
|
29
|
+
# or
|
|
30
|
+
yarn add react-native-pdfrender
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### iOS
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
cd ios && pod install
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Android
|
|
40
|
+
|
|
41
|
+
No extra steps — auto-linking handles it.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
### 1. Declarative (embedded viewer)
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { PdfViewerView } from 'react-native-pdfrender';
|
|
51
|
+
|
|
52
|
+
function MyScreen() {
|
|
53
|
+
return (
|
|
54
|
+
<PdfViewerView
|
|
55
|
+
uri="file:///path/to/document.pdf"
|
|
56
|
+
initialPage={0}
|
|
57
|
+
defaultZoom={1.5}
|
|
58
|
+
isFullScreen={false}
|
|
59
|
+
style={{ flex: 1 }}
|
|
60
|
+
onZoomChange={(pct) => console.log('zoom:', pct)}
|
|
61
|
+
onFullScreenChange={(fs) => setFullScreen(fs)}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 2. Imperative (hook — modal viewer)
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { usePdfViewer } from 'react-native-pdfrender';
|
|
71
|
+
|
|
72
|
+
function MyScreen() {
|
|
73
|
+
const { open, dismiss, getPageCount, createPdf } = usePdfViewer({
|
|
74
|
+
onFullScreenChanged: (isFS) => console.log('fullscreen:', isFS),
|
|
75
|
+
onZoomChanged: (zoom) => console.log('zoom:', zoom),
|
|
76
|
+
onDismiss: () => console.log('viewer closed'),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<Button title="Open PDF" onPress={() => open('file:///path/to/doc.pdf')} />
|
|
82
|
+
<Button title="Dismiss" onPress={dismiss} />
|
|
83
|
+
<Button title="Page count" onPress={async () => {
|
|
84
|
+
const n = await getPageCount('file:///path/to/doc.pdf');
|
|
85
|
+
console.log('pages:', n);
|
|
86
|
+
}} />
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## API
|
|
95
|
+
|
|
96
|
+
### `<PdfViewerView />`
|
|
97
|
+
|
|
98
|
+
| Prop | Type | Default | Description |
|
|
99
|
+
|------|------|---------|-------------|
|
|
100
|
+
| `uri` | `string` | **required** | `file://` or `content://` path to the PDF |
|
|
101
|
+
| `initialPage` | `number` | `-1` (first) | 0-based page index to open on |
|
|
102
|
+
| `defaultZoom` | `number` | `1.5` | Initial zoom level (1.0 = 100%) |
|
|
103
|
+
| `isFullScreen` | `boolean` | `false` | Fills the parent container when true |
|
|
104
|
+
| `rightLayout` | `boolean` | `false` | Positions viewer on right side (split-view) |
|
|
105
|
+
| `maxWidth` | `number` | — | Max pixel width (split-view) |
|
|
106
|
+
| `screenWidthPercentage` | `number` | `100` | % of screen width |
|
|
107
|
+
| `icons` | `IconSet` | — | Custom toolbar icon assets |
|
|
108
|
+
| `iconSize` | `number` | `40` | Icon size in logical pixels |
|
|
109
|
+
| `backButtonText` | `string` | — | Toolbar back button label |
|
|
110
|
+
| `headerText` | `string` | — | Toolbar header label |
|
|
111
|
+
| `style` | `ViewStyle` | — | Container style |
|
|
112
|
+
| `onFullScreenChange` | `(isFullScreen: boolean) => void` | — | Fires when fullscreen state changes |
|
|
113
|
+
| `onZoomChange` | `(zoomPercentage: number) => void` | — | Fires when zoom changes |
|
|
114
|
+
| `onLeftScreenChange` | `(isLeftScreen: boolean) => void` | — | Fires in split-view |
|
|
115
|
+
| `onRightScreenChange` | `(isRightScreen: boolean) => void` | — | Fires in split-view |
|
|
116
|
+
|
|
117
|
+
#### `IconSet`
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
interface IconSet {
|
|
121
|
+
zoomIn?: number | string; // require('./zoom_in.png') or URI
|
|
122
|
+
zoomOut?: number | string;
|
|
123
|
+
fullScreen?: number | string;
|
|
124
|
+
minimizeScreen?: number | string;
|
|
125
|
+
leftLayout?: number | string;
|
|
126
|
+
rightLayout?: number | string;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### `usePdfViewer(options?)`
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
interface UsePdfViewerOptions {
|
|
136
|
+
onFullScreenChanged?: (isFullScreen: boolean) => void;
|
|
137
|
+
onZoomChanged?: (zoomPercentage: number) => void;
|
|
138
|
+
onDismiss?: () => void;
|
|
139
|
+
onError?: (error: string) => void;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface UsePdfViewerReturn {
|
|
143
|
+
open: (uri: string, pageIndex?: number, defaultZoom?: number) => void;
|
|
144
|
+
dismiss: () => void;
|
|
145
|
+
getPageCount:(uri: string) => Promise<number>;
|
|
146
|
+
createPdf: (pages: PdfPage[], widthPx: number, heightPx: number) => Promise<PdfCreationResult>;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### `NativePdfViewerModule` (low-level TurboModule)
|
|
153
|
+
|
|
154
|
+
Exposed for advanced use cases. Prefer `usePdfViewer` for most scenarios.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { NativePdfViewerModule } from 'react-native-pdfrender';
|
|
158
|
+
|
|
159
|
+
NativePdfViewerModule.openPdfViewer(uri, pageIndex, defaultZoom);
|
|
160
|
+
NativePdfViewerModule.dismissPdfViewer();
|
|
161
|
+
const count = await NativePdfViewerModule.getPdfPageCount(uri);
|
|
162
|
+
const result = await NativePdfViewerModule.createMultiPagePdfBase64(pages, width, height);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Architecture
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
react-native-pdfrender/
|
|
171
|
+
├── android/
|
|
172
|
+
│ ├── build.gradle ← library module (com.android.library)
|
|
173
|
+
│ └── src/main/java/com/pdfrender/
|
|
174
|
+
│ ├── PdfViewerTurboModule.kt ← TurboModule (imperative API)
|
|
175
|
+
│ ├── PdfViewerFabricManager.kt ← Fabric ViewManager (embedded viewer)
|
|
176
|
+
│ ├── PdfViewerFragment.kt ← modal viewer Fragment
|
|
177
|
+
│ ├── PdfRenderingLogic.kt ← Android PdfRenderer wrapper
|
|
178
|
+
│ ├── ComposeRenderer.kt ← Jetpack Compose UI
|
|
179
|
+
│ └── events/ ← Fabric event classes
|
|
180
|
+
├── ios/
|
|
181
|
+
│ ├── PdfViewerTurboModule.mm ← ObjC++ TurboModule bridge
|
|
182
|
+
│ ├── PdfViewerTurboModuleImpl.swift ← Swift business logic
|
|
183
|
+
│ ├── PdfViewerComponentView.mm ← ObjC++ Fabric ViewManager
|
|
184
|
+
│ ├── PdfViewerView.swift ← main PDF rendering view
|
|
185
|
+
│ ├── PdfRenderingLogic.swift ← CGPDFDocument wrapper
|
|
186
|
+
│ └── ...
|
|
187
|
+
├── src/
|
|
188
|
+
│ ├── index.tsx ← public API exports
|
|
189
|
+
│ ├── PdfViewerView.tsx ← React component
|
|
190
|
+
│ ├── usePdfViewer.ts ← hook
|
|
191
|
+
│ └── specs/
|
|
192
|
+
│ ├── NativePdfViewerModule.ts ← TurboModule codegen spec
|
|
193
|
+
│ └── NativePdfViewerComponent.ts ← Fabric codegen spec
|
|
194
|
+
├── lib/ ← compiled output (generated by bob)
|
|
195
|
+
├── example/ ← example React Native app
|
|
196
|
+
└── react-native-pdfrender.podspec
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### New Architecture (codegen)
|
|
200
|
+
|
|
201
|
+
The `codegenConfig` in `package.json` points to `src/specs/`. At build time:
|
|
202
|
+
|
|
203
|
+
- **iOS** — CocoaPods generates `PdfViewerSpecs.h` and the Fabric C++ headers
|
|
204
|
+
- **Android** — Gradle generates the abstract `NativePdfViewerModuleSpec` Kotlin class
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Building the library
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
# Install deps
|
|
212
|
+
yarn install
|
|
213
|
+
|
|
214
|
+
# Compile TypeScript → lib/
|
|
215
|
+
yarn build
|
|
216
|
+
# runs automatically as `prepare` before npm publish
|
|
217
|
+
|
|
218
|
+
# Type-check only
|
|
219
|
+
yarn typecheck
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Running the example app
|
|
225
|
+
|
|
226
|
+
### Install dependencies
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
# Library deps
|
|
230
|
+
yarn install
|
|
231
|
+
|
|
232
|
+
# Example deps
|
|
233
|
+
cd example && yarn install
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### iOS (one-time Xcode project cleanup required)
|
|
237
|
+
|
|
238
|
+
The example Xcode project was migrated from the original monolithic app and still
|
|
239
|
+
includes the library Swift files in the app target. Because CocoaPods now compiles
|
|
240
|
+
those same files as part of the library pod, you must remove them from the app
|
|
241
|
+
target first to avoid duplicate-symbol errors:
|
|
242
|
+
|
|
243
|
+
1. Open `example/ios/pdfRender.xcodeproj` in Xcode
|
|
244
|
+
2. Select the **pdfRender** app target → **Build Phases** → **Compile Sources**
|
|
245
|
+
3. Remove these files (click each, press `–`):
|
|
246
|
+
- `PdfViewerView.swift`, `PdfViewerViewController.swift`
|
|
247
|
+
- `PdfViewerTurboModuleImpl.swift`, `PdfRenderingLogic.swift`
|
|
248
|
+
- `PdfPageView.swift`, `PdfToolbarView.swift`
|
|
249
|
+
- `PdfCacheManager.swift`, `PdfConstants.swift`
|
|
250
|
+
- `PdfViewerTurboModule.mm`, `PdfViewerComponentView.mm`
|
|
251
|
+
4. Save the Xcode project, then:
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
cd example/ios && pod install && cd ../..
|
|
255
|
+
yarn example:ios
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Android
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
yarn example:android
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Publishing to npm
|
|
267
|
+
|
|
268
|
+
```sh
|
|
269
|
+
# Build the library
|
|
270
|
+
yarn prepare
|
|
271
|
+
|
|
272
|
+
# Verify what will be published
|
|
273
|
+
npm pack --dry-run
|
|
274
|
+
|
|
275
|
+
# Publish publicly
|
|
276
|
+
npm publish --access public
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Common Issues
|
|
282
|
+
|
|
283
|
+
### `PdfViewerSpecs/PdfViewerSpecs.h` not found (iOS build)
|
|
284
|
+
|
|
285
|
+
Run `pod install` inside `example/ios/` (or the consuming app's `ios/`). This triggers codegen which produces the `PdfViewerSpecs` headers.
|
|
286
|
+
|
|
287
|
+
### Duplicate symbol errors on iOS
|
|
288
|
+
|
|
289
|
+
Follow the [Xcode project cleanup](#ios-one-time-xcode-project-cleanup-required) steps above.
|
|
290
|
+
|
|
291
|
+
### `NativePdfViewerModuleSpec` not found (Android)
|
|
292
|
+
|
|
293
|
+
Run the app once — Gradle auto-generates the spec. Or run manually:
|
|
294
|
+
```sh
|
|
295
|
+
cd example/android && ./gradlew generateCodegenArtifactsFromSchema
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Metro can't resolve `react-native-pdfrender`
|
|
299
|
+
|
|
300
|
+
Run `yarn install` inside `example/` (not just the root). The `file:../` entry creates a symlink in `example/node_modules/react-native-pdfrender` that Metro follows.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## License
|
|
305
|
+
|
|
306
|
+
MIT © Shubham Keshari
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
apply plugin: "com.android.library"
|
|
2
|
+
apply plugin: "org.jetbrains.kotlin.android"
|
|
3
|
+
apply plugin: "org.jetbrains.kotlin.plugin.compose"
|
|
4
|
+
apply plugin: "com.facebook.react"
|
|
5
|
+
|
|
6
|
+
def kotlinVersion = "2.1.20"
|
|
7
|
+
|
|
8
|
+
buildscript {
|
|
9
|
+
repositories {
|
|
10
|
+
google()
|
|
11
|
+
mavenCentral()
|
|
12
|
+
}
|
|
13
|
+
dependencies {
|
|
14
|
+
classpath("com.android.tools.build:gradle")
|
|
15
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20")
|
|
16
|
+
classpath("org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.1.20")
|
|
17
|
+
classpath("com.facebook.react:react-native-gradle-plugin")
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
android {
|
|
22
|
+
compileSdk 36
|
|
23
|
+
buildToolsVersion "36.0.0"
|
|
24
|
+
|
|
25
|
+
namespace "com.pdfrender"
|
|
26
|
+
|
|
27
|
+
defaultConfig {
|
|
28
|
+
minSdk 24
|
|
29
|
+
targetSdk 36
|
|
30
|
+
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
buildFeatures {
|
|
34
|
+
buildConfig true
|
|
35
|
+
compose true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
sourceSets {
|
|
39
|
+
main {
|
|
40
|
+
java.srcDirs = ["src/main/java"]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
compileOptions {
|
|
45
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
46
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
kotlinOptions {
|
|
50
|
+
jvmTarget = "17"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
repositories {
|
|
55
|
+
google()
|
|
56
|
+
mavenCentral()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
dependencies {
|
|
60
|
+
implementation("com.facebook.react:react-android")
|
|
61
|
+
|
|
62
|
+
def composeBom = platform("androidx.compose:compose-bom:2024.09.00")
|
|
63
|
+
implementation(composeBom)
|
|
64
|
+
implementation("androidx.compose.ui:ui")
|
|
65
|
+
implementation("androidx.compose.material:material")
|
|
66
|
+
implementation("androidx.compose.ui:ui-tooling-preview")
|
|
67
|
+
debugImplementation("androidx.compose.ui:ui-tooling")
|
|
68
|
+
|
|
69
|
+
implementation("androidx.activity:activity-compose:1.9.2")
|
|
70
|
+
implementation("androidx.fragment:fragment-ktx:1.8.3")
|
|
71
|
+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def isNewArchitectureEnabled() {
|
|
75
|
+
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
|
76
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package com.pdfrender
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.Canvas
|
|
6
|
+
import android.graphics.Paint
|
|
7
|
+
import android.graphics.Typeface
|
|
8
|
+
import android.text.Layout
|
|
9
|
+
import android.text.StaticLayout
|
|
10
|
+
import android.text.TextPaint
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Render PDF page content to a Bitmap using Android Canvas API.
|
|
14
|
+
* This approach doesn't require Compose window attachment.
|
|
15
|
+
*/
|
|
16
|
+
fun renderComposableToBitmap(
|
|
17
|
+
context: Context,
|
|
18
|
+
widthPx: Int,
|
|
19
|
+
heightPx: Int,
|
|
20
|
+
title: String,
|
|
21
|
+
body: String
|
|
22
|
+
): Bitmap {
|
|
23
|
+
val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888)
|
|
24
|
+
val canvas = Canvas(bitmap)
|
|
25
|
+
|
|
26
|
+
// Fill white background
|
|
27
|
+
canvas.drawColor(android.graphics.Color.WHITE)
|
|
28
|
+
|
|
29
|
+
// Setup title paint
|
|
30
|
+
val titlePaint = TextPaint().apply {
|
|
31
|
+
color = android.graphics.Color.BLACK
|
|
32
|
+
textSize = 22f * context.resources.displayMetrics.density
|
|
33
|
+
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
|
34
|
+
isAntiAlias = true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Setup body paint
|
|
38
|
+
val bodyPaint = TextPaint().apply {
|
|
39
|
+
color = android.graphics.Color.BLACK
|
|
40
|
+
textSize = 16f * context.resources.displayMetrics.density
|
|
41
|
+
typeface = Typeface.DEFAULT
|
|
42
|
+
isAntiAlias = true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
val paddingPx = (24 * context.resources.displayMetrics.density).toInt()
|
|
46
|
+
val topPaddingPx = (12 * context.resources.displayMetrics.density).toInt()
|
|
47
|
+
val availableWidth = widthPx - (paddingPx * 2)
|
|
48
|
+
|
|
49
|
+
// Draw title
|
|
50
|
+
val titleLayout = StaticLayout.Builder.obtain(
|
|
51
|
+
title,
|
|
52
|
+
0,
|
|
53
|
+
title.length,
|
|
54
|
+
titlePaint,
|
|
55
|
+
availableWidth
|
|
56
|
+
).setAlignment(Layout.Alignment.ALIGN_NORMAL)
|
|
57
|
+
.setLineSpacing(0f, 1f)
|
|
58
|
+
.setIncludePad(false)
|
|
59
|
+
.build()
|
|
60
|
+
|
|
61
|
+
canvas.save()
|
|
62
|
+
canvas.translate(paddingPx.toFloat(), paddingPx.toFloat())
|
|
63
|
+
titleLayout.draw(canvas)
|
|
64
|
+
canvas.restore()
|
|
65
|
+
|
|
66
|
+
// Draw body
|
|
67
|
+
val bodyYPosition = paddingPx + titleLayout.height + topPaddingPx
|
|
68
|
+
val bodyLayout = StaticLayout.Builder.obtain(
|
|
69
|
+
body,
|
|
70
|
+
0,
|
|
71
|
+
body.length,
|
|
72
|
+
bodyPaint,
|
|
73
|
+
availableWidth
|
|
74
|
+
).setAlignment(Layout.Alignment.ALIGN_NORMAL)
|
|
75
|
+
.setLineSpacing(0f, 1f)
|
|
76
|
+
.setIncludePad(false)
|
|
77
|
+
.build()
|
|
78
|
+
|
|
79
|
+
canvas.save()
|
|
80
|
+
canvas.translate(paddingPx.toFloat(), bodyYPosition.toFloat())
|
|
81
|
+
bodyLayout.draw(canvas)
|
|
82
|
+
canvas.restore()
|
|
83
|
+
|
|
84
|
+
return bitmap
|
|
85
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
package com.pdfrender
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import kotlinx.coroutines.delay
|
|
6
|
+
import kotlinx.coroutines.Dispatchers
|
|
7
|
+
import kotlinx.coroutines.withContext
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* PDF Cache Management Utilities
|
|
11
|
+
*
|
|
12
|
+
* This file contains helper functions for managing PDF page cache,
|
|
13
|
+
* batch calculations, and memory management.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Calculates which batch a page belongs to
|
|
18
|
+
*
|
|
19
|
+
* @param pageIndex The page index (0-based)
|
|
20
|
+
* @param batchSize The size of each batch
|
|
21
|
+
* @return The batch number containing this page
|
|
22
|
+
*/
|
|
23
|
+
fun getBatchNumber(pageIndex: Int, batchSize: Int): Int {
|
|
24
|
+
return pageIndex / batchSize
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Gets the page range for a specific batch
|
|
29
|
+
*
|
|
30
|
+
* @param batchNumber The batch number
|
|
31
|
+
* @param batchSize The size of each batch
|
|
32
|
+
* @param pageCount Total number of pages
|
|
33
|
+
* @return The IntRange of page indices in this batch
|
|
34
|
+
*/
|
|
35
|
+
fun getBatchRange(batchNumber: Int, batchSize: Int, pageCount: Int): IntRange {
|
|
36
|
+
val startPage = batchNumber * batchSize
|
|
37
|
+
val endPage = minOf((batchNumber + 1) * batchSize - 1, pageCount - 1)
|
|
38
|
+
return startPage..endPage
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Calculates memory usage percentage
|
|
43
|
+
*
|
|
44
|
+
* @return Memory usage as a percentage (0-100)
|
|
45
|
+
*/
|
|
46
|
+
fun getMemoryUsagePercent(): Int {
|
|
47
|
+
val runtime = Runtime.getRuntime()
|
|
48
|
+
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
|
|
49
|
+
val maxMemory = runtime.maxMemory()
|
|
50
|
+
return (usedMemory * 100 / maxMemory).toInt()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Calculates effective cache size based on memory usage
|
|
55
|
+
*
|
|
56
|
+
* Reduces cache size when memory is high to prevent OOM errors
|
|
57
|
+
*
|
|
58
|
+
* @param maxCacheSize The maximum cache size
|
|
59
|
+
* @return The effective cache size to use
|
|
60
|
+
*/
|
|
61
|
+
fun calculateEffectiveCacheSize(maxCacheSize: Int): Int {
|
|
62
|
+
val memoryUsagePercent = getMemoryUsagePercent()
|
|
63
|
+
|
|
64
|
+
return when {
|
|
65
|
+
memoryUsagePercent > 60 -> maxCacheSize / 2 // Reduce by half if memory > 60%
|
|
66
|
+
memoryUsagePercent > 50 -> (maxCacheSize * 2 / 3).toInt() // Reduce to 2/3 if memory > 50%
|
|
67
|
+
else -> maxCacheSize
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Determines which batches to keep based on current visible page
|
|
73
|
+
*
|
|
74
|
+
* Keeps the current batch plus one batch before and after for smooth scrolling
|
|
75
|
+
*
|
|
76
|
+
* @param currentVisiblePage The currently visible page index
|
|
77
|
+
* @param batchSize The size of each batch
|
|
78
|
+
* @param pageCount Total number of pages
|
|
79
|
+
* @return Set of batch numbers to keep in cache
|
|
80
|
+
*/
|
|
81
|
+
fun getBatchesToKeep(
|
|
82
|
+
currentVisiblePage: Int,
|
|
83
|
+
batchSize: Int,
|
|
84
|
+
pageCount: Int
|
|
85
|
+
): Set<Int> {
|
|
86
|
+
val currentBatch = getBatchNumber(currentVisiblePage, batchSize)
|
|
87
|
+
|
|
88
|
+
return setOf(
|
|
89
|
+
currentBatch - 1,
|
|
90
|
+
currentBatch,
|
|
91
|
+
currentBatch + 1
|
|
92
|
+
).filter { it >= 0 && it * batchSize < pageCount }.toSet()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Safely recycles a bitmap after ensuring it's not in use
|
|
97
|
+
*
|
|
98
|
+
* This function performs multiple safety checks before recycling:
|
|
99
|
+
* - Checks if bitmap is still in cache
|
|
100
|
+
* - Checks if bitmap is in active use
|
|
101
|
+
* - Validates bitmap is not already recycled
|
|
102
|
+
*
|
|
103
|
+
* @param bitmap The bitmap to recycle
|
|
104
|
+
* @param isStillInCache Function to check if bitmap is still in cache
|
|
105
|
+
* @param isActive Function to check if bitmap is actively being used
|
|
106
|
+
*/
|
|
107
|
+
suspend fun safelyRecycleBitmap(
|
|
108
|
+
bitmap: Bitmap,
|
|
109
|
+
isStillInCache: suspend () -> Boolean,
|
|
110
|
+
isActive: () -> Boolean
|
|
111
|
+
) {
|
|
112
|
+
try {
|
|
113
|
+
// Check if bitmap is still in cache
|
|
114
|
+
val stillInCache = isStillInCache()
|
|
115
|
+
if (stillInCache) {
|
|
116
|
+
Log.w("PdfRenderer", "[CRASH_LOG] Bitmap still in cache, skipping recycle")
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Don't recycle if still in active use
|
|
121
|
+
if (isActive()) {
|
|
122
|
+
Log.d("PdfRenderer", "[CRASH_LOG] Skipping recycle of active bitmap")
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Final check before recycling
|
|
127
|
+
if (!bitmap.isRecycled && bitmap.width > 0 && bitmap.height > 0) {
|
|
128
|
+
bitmap.recycle()
|
|
129
|
+
Log.d("PdfRenderer", "[CRASH_LOG] Recycled bitmap: ${bitmap.width}x${bitmap.height}")
|
|
130
|
+
} else {
|
|
131
|
+
Log.d("PdfRenderer", "[CRASH_LOG] Bitmap already recycled or invalid, skipping")
|
|
132
|
+
}
|
|
133
|
+
} catch (e: Exception) {
|
|
134
|
+
Log.e("PdfRenderer", "[CRASH_LOG] Error recycling bitmap: ${e.message}", e)
|
|
135
|
+
Log.e("PdfRenderer", "[CRASH_LOG] Stack trace: ${e.stackTraceToString()}")
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Checks if a page needs to be re-rendered based on scale difference
|
|
141
|
+
*
|
|
142
|
+
* @param cachedScale The scale at which the page is currently cached
|
|
143
|
+
* @param targetScale The desired scale
|
|
144
|
+
* @return True if the page needs to be re-rendered
|
|
145
|
+
*/
|
|
146
|
+
fun needsRerender(cachedScale: Float?, targetScale: Float): Boolean {
|
|
147
|
+
if (cachedScale == null) return true
|
|
148
|
+
return kotlin.math.abs(cachedScale - targetScale) > 0.08f
|
|
149
|
+
}
|
|
150
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
package com.pdfrender
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Constants and data classes for PDF Viewer
|
|
5
|
+
*
|
|
6
|
+
* This file contains all constants, configuration values, and data classes
|
|
7
|
+
* used throughout the PDF viewer module.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Data class representing a render request for a PDF page
|
|
12
|
+
*
|
|
13
|
+
* @param pageIndex The index of the page to render (0-based)
|
|
14
|
+
* @param targetScale The scale at which to render the page (0.3f to 0.6f)
|
|
15
|
+
* @param priority Priority level: 0 = highest (visible), 1 = medium (near), 2 = low (prefetch)
|
|
16
|
+
*/
|
|
17
|
+
data class RenderRequest(
|
|
18
|
+
val pageIndex: Int,
|
|
19
|
+
val targetScale: Float,
|
|
20
|
+
val priority: Int
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* PDF Viewer Configuration Constants
|
|
25
|
+
*/
|
|
26
|
+
object PdfViewerConfig {
|
|
27
|
+
// Batch loading configuration - OPTIMIZED FOR IMMEDIATE UI DISPLAY
|
|
28
|
+
const val BATCH_SIZE = 3 // Load 3 pages per batch (requirement: show first 3 pages immediately)
|
|
29
|
+
const val MAX_CACHE_SIZE = 100 // Keep max 30 pages in memory (10 batches worth)
|
|
30
|
+
|
|
31
|
+
// Scale configuration
|
|
32
|
+
const val MIN_SCALE = 0.65f // Minimum zoom level (100% - STRICT REQUIREMENT)
|
|
33
|
+
const val MAX_SCALE_SMALL_SCREEN = 2.5f // Maximum zoom for small screens/phones (250%)
|
|
34
|
+
const val MAX_SCALE_LARGE_SCREEN = 3.0f // Maximum zoom for large screens/tablets (300%)
|
|
35
|
+
const val DEFAULT_SCALE = 1.5f // Default zoom level fallback (only used if not provided from JS)
|
|
36
|
+
const val SCALE_STEP = 0.05f // Zoom step size for buttons (5%)
|
|
37
|
+
|
|
38
|
+
// Screen size thresholds for determining small vs large screen
|
|
39
|
+
const val SMALL_SCREEN_WIDTH_DP = 600 // Screens <= 600dp are considered small (phones)
|
|
40
|
+
|
|
41
|
+
// Rendering configuration
|
|
42
|
+
const val RENDERING_TIMEOUT_MS = 5000L // 5 seconds - timeout for stuck renders
|
|
43
|
+
const val WATCHDOG_CHECK_INTERVAL_MS = 2000L // Check for stuck pages every 2 seconds
|
|
44
|
+
|
|
45
|
+
// Gesture configuration
|
|
46
|
+
const val GESTURE_THROTTLE_MS = 16L // 16ms = ~60fps for smooth updates
|
|
47
|
+
const val PINCH_GESTURE_END_DELAY_MS = 500L // Delay before considering gesture ended
|
|
48
|
+
const val MIN_ZOOM_CHANGE_THRESHOLD = 0.005f // 0.5% minimum change to process
|
|
49
|
+
const val SCALE_UPDATE_THRESHOLD = 0.005f // 0.5% threshold for scale updates
|
|
50
|
+
|
|
51
|
+
// Memory management
|
|
52
|
+
const val MEMORY_WARNING_THRESHOLD_PERCENT = 75 // Clean cache if memory > 75%
|
|
53
|
+
const val MEMORY_CRITICAL_THRESHOLD_PERCENT = 80 // Aggressive cleanup if memory > 80%
|
|
54
|
+
|
|
55
|
+
// Initial loading - CRITICAL: Load ONLY first batch (3 pages) for immediate UI display
|
|
56
|
+
const val INITIAL_BATCHES_TO_LOAD = 1 // Load ONLY first 3 pages immediately
|
|
57
|
+
const val INITIAL_LOAD_DELAY_MS = 100L // Delay before showing completion
|
|
58
|
+
|
|
59
|
+
// Background loading configuration - NEW
|
|
60
|
+
const val BACKGROUND_BATCHES_TO_PREFETCH = 2 // Pre-fetch 2 batches (6 pages) in background after UI shows
|
|
61
|
+
const val SCROLL_PREFETCH_THRESHOLD = 2 // Start loading next batch when within 2 pages of unloaded content
|
|
62
|
+
}
|
|
63
|
+
|