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
|
@@ -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
|
+
|