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
@@ -0,0 +1,275 @@
1
+ package com.pdfrender
2
+
3
+ import android.graphics.BitmapFactory
4
+ import androidx.compose.foundation.Image
5
+ import androidx.compose.runtime.*
6
+ import androidx.compose.ui.Modifier
7
+ import androidx.compose.ui.graphics.Color
8
+ import androidx.compose.ui.graphics.ColorFilter
9
+ import androidx.compose.ui.graphics.asImageBitmap
10
+ import androidx.compose.ui.platform.LocalContext
11
+ import kotlinx.coroutines.Dispatchers
12
+ import kotlinx.coroutines.withContext
13
+ import java.net.URL
14
+
15
+ /**
16
+ * PDF Icon Components
17
+ *
18
+ * Handles loading and displaying icons from various sources:
19
+ * - URLs (http/https)
20
+ * - Local files
21
+ * - React Native resources
22
+ * - Android drawables
23
+ */
24
+
25
+ /**
26
+ * Load and display an icon from URL, file, or drawable
27
+ *
28
+ * @param imageUrl URL or path to the icon
29
+ * @param drawableName Fallback drawable name if URL fails
30
+ * @param modifier Modifier for the image
31
+ * @param tint Color tint to apply
32
+ * @param contentDescription Accessibility description
33
+ */
34
+ @Composable
35
+ fun LoadIconImage(
36
+ imageUrl: String?,
37
+ drawableName: String,
38
+ modifier: Modifier,
39
+ tint: Color,
40
+ contentDescription: String
41
+ ) {
42
+ val context = LocalContext.current
43
+ var bitmap by remember { mutableStateOf<android.graphics.Bitmap?>(null) }
44
+
45
+ LaunchedEffect(imageUrl) {
46
+ if (!imageUrl.isNullOrEmpty()) {
47
+ // Load from URL or local file
48
+ bitmap = withContext(Dispatchers.IO) {
49
+ try {
50
+ when {
51
+ // Handle React Native resource URIs (res://drawable/... or res:/drawable/...)
52
+ imageUrl.startsWith("res://") || imageUrl.startsWith("res:/") -> {
53
+ val resourceName = imageUrl.substringAfterLast("/").substringBefore(".")
54
+ val resourceId = context.resources.getIdentifier(resourceName, "drawable", context.packageName)
55
+ if (resourceId != 0) {
56
+ BitmapFactory.decodeResource(context.resources, resourceId)
57
+ } else null
58
+ }
59
+
60
+ // Handle asset URIs (file:///android_asset/...)
61
+ imageUrl.startsWith("file:///android_asset/") -> {
62
+ val assetPath = imageUrl.removePrefix("file:///android_asset/")
63
+ context.assets.open(assetPath).use { inputStream ->
64
+ BitmapFactory.decodeStream(inputStream)
65
+ }
66
+ }
67
+
68
+ // Handle local file URIs (file://...)
69
+ imageUrl.startsWith("file://") -> {
70
+ val filePath = imageUrl.removePrefix("file://")
71
+ BitmapFactory.decodeFile(filePath)
72
+ }
73
+
74
+ // Handle content:// URIs
75
+ imageUrl.startsWith("content://") -> {
76
+ context.contentResolver.openInputStream(android.net.Uri.parse(imageUrl))?.use { inputStream ->
77
+ BitmapFactory.decodeStream(inputStream)
78
+ }
79
+ }
80
+
81
+ // Handle http/https URLs
82
+ imageUrl.startsWith("http://") || imageUrl.startsWith("https://") -> {
83
+ val url = URL(imageUrl)
84
+ val connection = url.openConnection()
85
+ connection.connectTimeout = 5000
86
+ connection.readTimeout = 5000
87
+ connection.connect()
88
+ val inputStream = connection.getInputStream()
89
+ BitmapFactory.decodeStream(inputStream)
90
+ }
91
+
92
+ // Try as numeric resource ID
93
+ imageUrl.matches(Regex("^\\d+$")) -> {
94
+ val resourceId = imageUrl.toInt()
95
+ BitmapFactory.decodeResource(context.resources, resourceId)
96
+ }
97
+
98
+ // Try as drawable resource or asset
99
+ else -> {
100
+ val resourceId = context.resources.getIdentifier(imageUrl, "drawable", context.packageName)
101
+ if (resourceId != 0) {
102
+ BitmapFactory.decodeResource(context.resources, resourceId)
103
+ } else {
104
+ // Try as asset in various paths
105
+ val assetPaths = listOf(
106
+ imageUrl,
107
+ "drawable-mdpi/$imageUrl",
108
+ "drawable-hdpi/$imageUrl",
109
+ "drawable-xhdpi/$imageUrl",
110
+ "drawable-xxhdpi/$imageUrl"
111
+ )
112
+
113
+ var loadedBitmap: android.graphics.Bitmap? = null
114
+ for (path in assetPaths) {
115
+ try {
116
+ val inputStream = context.assets.open(path)
117
+ loadedBitmap = BitmapFactory.decodeStream(inputStream)
118
+ inputStream.close()
119
+ if (loadedBitmap != null) break
120
+ } catch (e: Exception) {
121
+ // Try next path
122
+ }
123
+ }
124
+ loadedBitmap ?: BitmapFactory.decodeFile(imageUrl)
125
+ }
126
+ }
127
+ }
128
+ } catch (e: Exception) {
129
+ null
130
+ }
131
+ }
132
+ } else {
133
+ // Fallback to drawable
134
+ val resourceId = context.resources.getIdentifier(drawableName, "drawable", context.packageName)
135
+ if (resourceId != 0) {
136
+ bitmap = BitmapFactory.decodeResource(context.resources, resourceId)
137
+ }
138
+ }
139
+ }
140
+
141
+ bitmap?.let {
142
+ Image(
143
+ bitmap = it.asImageBitmap(),
144
+ contentDescription = contentDescription,
145
+ modifier = modifier,
146
+ colorFilter = ColorFilter.tint(tint)
147
+ )
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Zoom In Icon Component
153
+ */
154
+ @Composable
155
+ fun ZoomInIcon(
156
+ tint: Color,
157
+ modifier: Modifier,
158
+ imageUrl: String? = null
159
+ ) {
160
+ LoadIconImage(
161
+ imageUrl = imageUrl,
162
+ drawableName = "ic_zoom_in",
163
+ modifier = modifier,
164
+ tint = tint,
165
+ contentDescription = "Zoom In"
166
+ )
167
+ }
168
+
169
+ /**
170
+ * Zoom Out Icon Component
171
+ */
172
+ @Composable
173
+ fun ZoomOutIcon(
174
+ tint: Color,
175
+ modifier: Modifier,
176
+ imageUrl: String? = null
177
+ ) {
178
+ LoadIconImage(
179
+ imageUrl = imageUrl,
180
+ drawableName = "ic_zoom_out",
181
+ modifier = modifier,
182
+ tint = tint,
183
+ contentDescription = "Zoom Out"
184
+ )
185
+ }
186
+
187
+ /**
188
+ * Fullscreen Icon Component
189
+ */
190
+ @Composable
191
+ fun FullscreenIcon(
192
+ tint: Color,
193
+ modifier: Modifier,
194
+ imageUrl: String? = null
195
+ ) {
196
+ LoadIconImage(
197
+ imageUrl = imageUrl,
198
+ drawableName = "ic_fullscreen",
199
+ modifier = modifier,
200
+ tint = tint,
201
+ contentDescription = "Fullscreen"
202
+ )
203
+ }
204
+
205
+ /**
206
+ * Minimize Icon Component
207
+ */
208
+ @Composable
209
+ fun MinimizeIcon(
210
+ tint: Color,
211
+ modifier: Modifier,
212
+ imageUrl: String? = null
213
+ ) {
214
+ LoadIconImage(
215
+ imageUrl = imageUrl,
216
+ drawableName = "ic_minimize",
217
+ modifier = modifier,
218
+ tint = tint,
219
+ contentDescription = "Minimize"
220
+ )
221
+ }
222
+
223
+ /**
224
+ * Left Layout Icon Component
225
+ */
226
+ @Composable
227
+ fun LeftLayoutSidebarIcon(
228
+ tint: Color,
229
+ modifier: Modifier,
230
+ imageUrl: String? = null
231
+ ) {
232
+ LoadIconImage(
233
+ imageUrl = imageUrl,
234
+ drawableName = "ic_left_layout",
235
+ modifier = modifier,
236
+ tint = tint,
237
+ contentDescription = "Left Layout"
238
+ )
239
+ }
240
+
241
+ /**
242
+ * Right Layout Icon Component
243
+ */
244
+ @Composable
245
+ fun RightLayoutSidebarIcon(
246
+ tint: Color,
247
+ modifier: Modifier,
248
+ imageUrl: String? = null
249
+ ) {
250
+ LoadIconImage(
251
+ imageUrl = imageUrl,
252
+ drawableName = "ic_right_layout",
253
+ modifier = modifier,
254
+ tint = tint,
255
+ contentDescription = "Right Layout"
256
+ )
257
+ }
258
+
259
+ /**
260
+ * Reset Icon Component
261
+ */
262
+ @Composable
263
+ fun ResetIcon(
264
+ tint: Color,
265
+ modifier: Modifier,
266
+ imageUrl: String? = null
267
+ ) {
268
+ LoadIconImage(
269
+ imageUrl = imageUrl,
270
+ drawableName = "ic_reset",
271
+ modifier = modifier,
272
+ tint = tint,
273
+ contentDescription = "Reset"
274
+ )
275
+ }
@@ -0,0 +1,325 @@
1
+ package com.pdfrender
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Canvas
5
+ import android.graphics.Color
6
+ import android.graphics.pdf.PdfRenderer
7
+ import android.util.Log
8
+ import kotlinx.coroutines.Dispatchers
9
+ import kotlinx.coroutines.withContext
10
+ import kotlinx.coroutines.sync.Mutex
11
+ import kotlinx.coroutines.sync.withLock
12
+ import kotlin.system.measureTimeMillis
13
+
14
+ /**
15
+ * PDF Page Rendering Logic
16
+ *
17
+ * This file contains all functions related to rendering PDF pages to bitmaps.
18
+ * Handles thread safety, memory management, and error handling.
19
+ */
20
+
21
+ /**
22
+ * Data class to hold rendering timing information
23
+ */
24
+ data class RenderTimings(
25
+ val totalTime: Long,
26
+ val pageOpenTime: Long,
27
+ val bitmapCreateTime: Long,
28
+ val renderTime: Long
29
+ )
30
+
31
+ /**
32
+ * Renders a single PDF page to a Bitmap at the specified scale
33
+ *
34
+ * This function is thread-safe and handles:
35
+ * - Page validation
36
+ * - Memory management
37
+ * - Error handling and cleanup
38
+ * - Bitmap creation with appropriate quality settings
39
+ * - Performance timing measurements
40
+ *
41
+ * @param pageIndex The index of the page to render (0-based)
42
+ * @param targetScale The scale at which to render (0.3f to 0.6f)
43
+ * @param pageCount Total number of pages in the PDF
44
+ * @param pdfRenderer The PdfRenderer instance (can be null)
45
+ * @param renderMutex Mutex for thread-safe access to PdfRenderer
46
+ * @param density Screen density for proper scaling
47
+ * @param activeBitmaps Set to track active bitmaps (prevents premature recycling)
48
+ * @param logTimings Whether to log detailed timing information (default: true)
49
+ *
50
+ * @return Pair of rendered Bitmap and RenderTimings, or null if rendering failed
51
+ */
52
+ suspend fun renderPageToBitmap(
53
+ pageIndex: Int,
54
+ targetScale: Float,
55
+ pageCount: Int,
56
+ pdfRenderer: PdfRenderer?,
57
+ renderMutex: Mutex,
58
+ density: Float,
59
+ activeBitmaps: MutableSet<Bitmap>,
60
+ logTimings: Boolean = true
61
+ ): Pair<Bitmap, RenderTimings>? = withContext(Dispatchers.Default) {
62
+
63
+ // Validate page index
64
+ if (pageIndex < 0 || pageIndex >= pageCount) {
65
+ Log.e("PdfRenderer", "Invalid page index: $pageIndex (pageCount: $pageCount)")
66
+ return@withContext null
67
+ }
68
+
69
+ try {
70
+ val renderer = pdfRenderer
71
+ if (renderer == null) {
72
+ Log.e("PdfRenderer", "Renderer is null for page $pageIndex")
73
+ return@withContext null
74
+ }
75
+
76
+ var page: PdfRenderer.Page? = null
77
+ var bitmap: Bitmap? = null
78
+ var pageOpenTime = 0L
79
+ var bitmapCreateTime = 0L
80
+ var renderTime = 0L
81
+
82
+ try {
83
+ // Measure page opening time
84
+ pageOpenTime = measureTimeMillis {
85
+ // Use mutex to safely open page (PdfRenderer is not thread-safe)
86
+ page = renderMutex.withLock {
87
+ try {
88
+ // Double-check renderer is still valid
89
+ val currentRenderer = pdfRenderer
90
+ if (currentRenderer == null) {
91
+ Log.e("PdfRenderer", "Renderer became null during lock")
92
+ return@withLock null
93
+ }
94
+
95
+ // Validate page index again
96
+ if (pageIndex < 0 || pageIndex >= pageCount) {
97
+ Log.e("PdfRenderer", "Invalid page index in lock: $pageIndex")
98
+ return@withLock null
99
+ }
100
+
101
+ currentRenderer.openPage(pageIndex)
102
+ } catch (e: IllegalStateException) {
103
+ Log.e("PdfRenderer", "IllegalState opening page $pageIndex: ${e.message}")
104
+ null
105
+ } catch (e: IllegalArgumentException) {
106
+ Log.e("PdfRenderer", "IllegalArgument opening page $pageIndex: ${e.message}")
107
+ null
108
+ } catch (e: Exception) {
109
+ Log.e("PdfRenderer", "Error opening page $pageIndex: ${e.message}")
110
+ e.printStackTrace()
111
+ null
112
+ }
113
+ }
114
+ }
115
+
116
+ val openedPage = page
117
+ if (openedPage == null) {
118
+ return@withContext null
119
+ }
120
+
121
+ // Calculate bitmap dimensions based on scale (optimized calculation)
122
+ val scaleFactor = (targetScale * density * 4f).coerceIn(1.5f, 4f)
123
+ val width = (openedPage.width * scaleFactor).toInt()
124
+ val height = (openedPage.height * scaleFactor).toInt()
125
+
126
+ // Check memory availability before creating bitmap
127
+ val runtime = Runtime.getRuntime()
128
+ val maxMemory = runtime.maxMemory()
129
+ val usedMemory = runtime.totalMemory() - runtime.freeMemory()
130
+ val availableMemory = maxMemory - usedMemory
131
+ val bitmapSize = (width * height * 4).toLong()
132
+
133
+ // Measure bitmap creation time
134
+ bitmapCreateTime = measureTimeMillis {
135
+ bitmap = try {
136
+ // Optimize bitmap config based on memory pressure
137
+ val config = when {
138
+ bitmapSize > availableMemory * 0.8 -> {
139
+ // Use lower quality if memory is very tight
140
+ val reducedScale = scaleFactor * 0.7f
141
+ val reducedWidth = (openedPage.width * reducedScale).toInt()
142
+ val reducedHeight = (openedPage.height * reducedScale).toInt()
143
+ Log.w("PdfRenderer", "Low memory: Using RGB_565 for page $pageIndex")
144
+ Bitmap.createBitmap(reducedWidth, reducedHeight, Bitmap.Config.RGB_565)
145
+ }
146
+ bitmapSize > availableMemory * 0.6 -> {
147
+ // Use RGB_565 for better memory efficiency
148
+ Log.d("PdfRenderer", "Memory pressure: Using RGB_565 for page $pageIndex")
149
+ Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
150
+ }
151
+ else -> {
152
+ // Use high quality ARGB_8888 when memory is available
153
+ Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
154
+ }
155
+ }
156
+
157
+ // Pre-fill with white background for better rendering performance
158
+ config.apply {
159
+ Canvas(this).drawColor(Color.WHITE)
160
+ }
161
+ } catch (e: OutOfMemoryError) {
162
+ Log.e("PdfRenderer", "OOM creating bitmap for page $pageIndex")
163
+ System.gc()
164
+ null
165
+ }
166
+ }
167
+
168
+ val createdBitmap = bitmap
169
+ if (createdBitmap == null) {
170
+ return@withContext null
171
+ }
172
+
173
+ // Track this bitmap as active (prevents premature recycling)
174
+ activeBitmaps.add(createdBitmap)
175
+
176
+ // Measure actual PDF rendering time
177
+ renderTime = measureTimeMillis {
178
+ try {
179
+ // Use RENDER_MODE_FOR_DISPLAY for optimized rendering
180
+ openedPage.render(createdBitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
181
+ } catch (e: IllegalStateException) {
182
+ Log.e("PdfRenderer", "IllegalState rendering page $pageIndex: ${e.message}")
183
+ throw e
184
+ } catch (e: Exception) {
185
+ Log.e("PdfRenderer", "Error rendering page $pageIndex to bitmap: ${e.message}")
186
+ e.printStackTrace()
187
+ throw e
188
+ }
189
+ }
190
+
191
+ val timings = RenderTimings(
192
+ totalTime = pageOpenTime + bitmapCreateTime + renderTime,
193
+ pageOpenTime = pageOpenTime,
194
+ bitmapCreateTime = bitmapCreateTime,
195
+ renderTime = renderTime
196
+ )
197
+
198
+ if (logTimings) {
199
+ Log.i("PdfRenderer", """
200
+ 📊 Page $pageIndex Render Complete:
201
+ ├─ Total: ${timings.totalTime}ms
202
+ ├─ Page Open: ${pageOpenTime}ms
203
+ ├─ Bitmap Create: ${bitmapCreateTime}ms
204
+ ├─ PDF Render: ${renderTime}ms
205
+ └─ Size: ${width}x${height} (${createdBitmap.config})
206
+ """.trimIndent())
207
+ }
208
+
209
+ return@withContext Pair(createdBitmap, timings)
210
+ } catch (e: Exception) {
211
+ Log.e("PdfRenderer", "Error rendering page $pageIndex: ${e.message}")
212
+ e.printStackTrace()
213
+
214
+ // Clean up bitmap if created
215
+ bitmap?.let {
216
+ cleanupBitmap(it, activeBitmaps)
217
+ }
218
+ return@withContext null
219
+ } finally {
220
+ // Always close the page
221
+ try {
222
+ page?.close()
223
+ } catch (e: Exception) {
224
+ e.printStackTrace()
225
+ }
226
+ }
227
+ } catch (e: Exception) {
228
+ Log.e("PdfRenderer", "Unexpected error in renderPageToBitmap: ${e.message}")
229
+ e.printStackTrace()
230
+ return@withContext null
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Simplified version that returns only the Bitmap (for backward compatibility)
236
+ *
237
+ * @return The rendered Bitmap, or null if rendering failed
238
+ */
239
+ suspend fun renderPageToBitmapSimple(
240
+ pageIndex: Int,
241
+ targetScale: Float,
242
+ pageCount: Int,
243
+ pdfRenderer: PdfRenderer?,
244
+ renderMutex: Mutex,
245
+ density: Float,
246
+ activeBitmaps: MutableSet<Bitmap>
247
+ ): Bitmap? {
248
+ return renderPageToBitmap(
249
+ pageIndex = pageIndex,
250
+ targetScale = targetScale,
251
+ pageCount = pageCount,
252
+ pdfRenderer = pdfRenderer,
253
+ renderMutex = renderMutex,
254
+ density = density,
255
+ activeBitmaps = activeBitmaps,
256
+ logTimings = false
257
+ )?.first
258
+ }
259
+
260
+ /**
261
+ * Performance statistics tracker for bitmap rendering
262
+ */
263
+ class RenderingStats {
264
+ private val timings = mutableListOf<RenderTimings>()
265
+
266
+ fun addTiming(timing: RenderTimings) {
267
+ synchronized(timings) {
268
+ timings.add(timing)
269
+ // Keep only last 100 timings to prevent memory bloat
270
+ if (timings.size > 100) {
271
+ timings.removeAt(0)
272
+ }
273
+ }
274
+ }
275
+
276
+ fun getStats(): String {
277
+ synchronized(timings) {
278
+ if (timings.isEmpty()) return "No render statistics available"
279
+
280
+ val avgTotal = timings.map { it.totalTime }.average()
281
+ val avgPageOpen = timings.map { it.pageOpenTime }.average()
282
+ val avgBitmapCreate = timings.map { it.bitmapCreateTime }.average()
283
+ val avgRender = timings.map { it.renderTime }.average()
284
+
285
+ val minTotal = timings.minOf { it.totalTime }
286
+ val maxTotal = timings.maxOf { it.totalTime }
287
+
288
+ return """
289
+ 📈 Rendering Statistics (last ${timings.size} pages):
290
+ ├─ Average Total: ${"%.2f".format(avgTotal)}ms
291
+ ├─ Average Page Open: ${"%.2f".format(avgPageOpen)}ms
292
+ ├─ Average Bitmap Create: ${"%.2f".format(avgBitmapCreate)}ms
293
+ ├─ Average PDF Render: ${"%.2f".format(avgRender)}ms
294
+ ├─ Fastest: ${minTotal}ms
295
+ └─ Slowest: ${maxTotal}ms
296
+ """.trimIndent()
297
+ }
298
+ }
299
+
300
+ fun reset() {
301
+ synchronized(timings) {
302
+ timings.clear()
303
+ }
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Safely cleans up a bitmap by removing it from active set and recycling it
309
+ *
310
+ * @param bitmap The bitmap to clean up
311
+ * @param activeBitmaps The set of active bitmaps
312
+ */
313
+ private fun cleanupBitmap(bitmap: Bitmap?, activeBitmaps: MutableSet<Bitmap>) {
314
+ if (bitmap == null) return
315
+
316
+ try {
317
+ activeBitmaps.remove(bitmap)
318
+ if (!bitmap.isRecycled && bitmap.width > 0 && bitmap.height > 0) {
319
+ bitmap.recycle()
320
+ }
321
+ } catch (e: Exception) {
322
+ Log.e("PdfRenderer", "Error cleaning up bitmap: ${e.message}")
323
+ }
324
+ }
325
+