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.
Files changed (68) hide show
  1. package/README.md +306 -0
  2. package/android/build.gradle +76 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/java/com/pdfrender/ComposeRenderer.kt +85 -0
  5. package/android/src/main/java/com/pdfrender/PdfCacheManager.kt +150 -0
  6. package/android/src/main/java/com/pdfrender/PdfConstants.kt +63 -0
  7. package/android/src/main/java/com/pdfrender/PdfIconComponents.kt +275 -0
  8. package/android/src/main/java/com/pdfrender/PdfRenderingLogic.kt +325 -0
  9. package/android/src/main/java/com/pdfrender/PdfUIComponents.kt +335 -0
  10. package/android/src/main/java/com/pdfrender/PdfViewPackage.kt +32 -0
  11. package/android/src/main/java/com/pdfrender/PdfViewerActivity.kt +3467 -0
  12. package/android/src/main/java/com/pdfrender/PdfViewerFabricManager.kt +244 -0
  13. package/android/src/main/java/com/pdfrender/PdfViewerFragment.kt +129 -0
  14. package/android/src/main/java/com/pdfrender/PdfViewerTurboModule.kt +158 -0
  15. package/android/src/main/java/com/pdfrender/events/FullScreenChangeEvent.kt +26 -0
  16. package/android/src/main/java/com/pdfrender/events/LeftScreenChangeEvent.kt +22 -0
  17. package/android/src/main/java/com/pdfrender/events/RightScreenChangeEvent.kt +22 -0
  18. package/android/src/main/java/com/pdfrender/events/ZoomChangeEvent.kt +22 -0
  19. package/ios/PdfCacheManager.swift +44 -0
  20. package/ios/PdfConstants.swift +38 -0
  21. package/ios/PdfPageView.swift +121 -0
  22. package/ios/PdfRenderingLogic.swift +107 -0
  23. package/ios/PdfToolbarView.swift +158 -0
  24. package/ios/PdfViewerComponentView.mm +194 -0
  25. package/ios/PdfViewerTurboModule.mm +186 -0
  26. package/ios/PdfViewerTurboModuleImpl.swift +141 -0
  27. package/ios/PdfViewerView.swift +268 -0
  28. package/ios/PdfViewerViewController.swift +109 -0
  29. package/lib/commonjs/PdfViewerView.js +105 -0
  30. package/lib/commonjs/PdfViewerView.js.map +1 -0
  31. package/lib/commonjs/index.js +28 -0
  32. package/lib/commonjs/index.js.map +1 -0
  33. package/lib/commonjs/package.json +1 -0
  34. package/lib/commonjs/specs/NativePdfViewerComponent.js +27 -0
  35. package/lib/commonjs/specs/NativePdfViewerComponent.js.map +1 -0
  36. package/lib/commonjs/specs/NativePdfViewerModule.js +21 -0
  37. package/lib/commonjs/specs/NativePdfViewerModule.js.map +1 -0
  38. package/lib/commonjs/usePdfViewer.js +65 -0
  39. package/lib/commonjs/usePdfViewer.js.map +1 -0
  40. package/lib/module/PdfViewerView.js +99 -0
  41. package/lib/module/PdfViewerView.js.map +1 -0
  42. package/lib/module/index.js +8 -0
  43. package/lib/module/index.js.map +1 -0
  44. package/lib/module/package.json +1 -0
  45. package/lib/module/specs/NativePdfViewerComponent.js +26 -0
  46. package/lib/module/specs/NativePdfViewerComponent.js.map +1 -0
  47. package/lib/module/specs/NativePdfViewerModule.js +18 -0
  48. package/lib/module/specs/NativePdfViewerModule.js.map +1 -0
  49. package/lib/module/usePdfViewer.js +60 -0
  50. package/lib/module/usePdfViewer.js.map +1 -0
  51. package/lib/typescript/PdfViewerView.d.ts +58 -0
  52. package/lib/typescript/PdfViewerView.d.ts.map +1 -0
  53. package/lib/typescript/index.d.ts +7 -0
  54. package/lib/typescript/index.d.ts.map +1 -0
  55. package/lib/typescript/specs/NativePdfViewerComponent.d.ts +59 -0
  56. package/lib/typescript/specs/NativePdfViewerComponent.d.ts.map +1 -0
  57. package/lib/typescript/specs/NativePdfViewerModule.d.ts +47 -0
  58. package/lib/typescript/specs/NativePdfViewerModule.d.ts.map +1 -0
  59. package/lib/typescript/usePdfViewer.d.ts +45 -0
  60. package/lib/typescript/usePdfViewer.d.ts.map +1 -0
  61. package/package.json +109 -0
  62. package/react-native-pdfrender.podspec +35 -0
  63. package/react-native.config.js +11 -0
  64. package/src/PdfViewerView.tsx +159 -0
  65. package/src/index.tsx +10 -0
  66. package/src/specs/NativePdfViewerComponent.ts +94 -0
  67. package/src/specs/NativePdfViewerModule.ts +58 -0
  68. 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,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -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
+