react-native-optimized-pdf 1.1.0 → 1.3.1
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/CHANGELOG.md +36 -19
- package/CODE_OF_CONDUCT.md +128 -0
- package/LICENSE +21 -0
- package/README.md +50 -35
- package/SECURITY.md +21 -0
- package/android/build.gradle +1 -1
- package/android/src/main/kotlin/com/reactnativeoptimizedpdf/OptimizedPdfView.kt +305 -143
- package/docs/android-setup.md +1 -1
- package/ios/Core/PDFConfiguration.swift +1 -1
- package/ios/Core/PDFDocumentLoader.swift +7 -4
- package/ios/Core/PDFError.swift +0 -9
- package/ios/ReactNative/OptimizedPDFViewManager.swift +0 -28
- package/ios/ReactNative/OptimizedPdfViewManager.m +0 -11
- package/ios/Views/OptimizedPDFView.swift +31 -54
- package/ios/Views/TiledPDFPageView.swift +188 -49
- package/package.json +27 -14
- package/src/OptimizedPdfView.tsx +21 -19
- package/src/components/PdfNavigationControls.tsx +1 -1
- package/src/components/PdfOverlays.tsx +3 -3
- package/src/constants.ts +0 -2
- package/src/services/pdfCache.ts +9 -3
- package/src/types/index.ts +25 -2
- package/tsconfig.json +17 -10
|
@@ -5,7 +5,6 @@ import android.graphics.Bitmap
|
|
|
5
5
|
import android.graphics.Canvas
|
|
6
6
|
import android.graphics.Color
|
|
7
7
|
import android.graphics.Paint
|
|
8
|
-
import android.graphics.pdf.PdfRenderer
|
|
9
8
|
import android.os.ParcelFileDescriptor
|
|
10
9
|
import android.util.AttributeSet
|
|
11
10
|
import android.view.GestureDetector
|
|
@@ -15,18 +14,24 @@ import android.widget.FrameLayout
|
|
|
15
14
|
import com.facebook.react.bridge.Arguments
|
|
16
15
|
import com.facebook.react.bridge.ReactContext
|
|
17
16
|
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
17
|
+
import io.legere.pdfiumandroid.PdfDocument
|
|
18
|
+
import io.legere.pdfiumandroid.PdfPage
|
|
19
|
+
import io.legere.pdfiumandroid.PdfPasswordException
|
|
20
|
+
import io.legere.pdfiumandroid.PdfiumCore
|
|
21
21
|
import java.io.File
|
|
22
|
-
import java.
|
|
22
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
23
|
+
import java.util.concurrent.ExecutorService
|
|
24
|
+
import java.util.concurrent.Executors
|
|
25
|
+
import java.util.concurrent.Future
|
|
26
|
+
import android.graphics.Rect
|
|
23
27
|
import kotlin.math.max
|
|
24
28
|
import kotlin.math.min
|
|
29
|
+
import kotlin.math.sqrt
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
32
|
* View otimizada para renderização de PDF no Android
|
|
28
33
|
*
|
|
29
|
-
* Usa
|
|
34
|
+
* Usa PdfiumCore (pdfiumandroid) para renderização eficiente com suporte a Annots
|
|
30
35
|
* com suporte a zoom via ScaleGestureDetector e pan via GestureDetector
|
|
31
36
|
*/
|
|
32
37
|
class OptimizedPdfView @JvmOverloads constructor(
|
|
@@ -35,10 +40,22 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
35
40
|
defStyleAttr: Int = 0
|
|
36
41
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
companion object {
|
|
44
|
+
private const val TILE_SIZE = 512
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private class Tile(val x: Int, val y: Int, val size: Int, val scale: Float, val bitmap: Bitmap)
|
|
48
|
+
|
|
49
|
+
private val pdfiumCore: PdfiumCore = PdfiumCore()
|
|
50
|
+
private var pdfDocument: PdfDocument? = null
|
|
39
51
|
private var fileDescriptor: ParcelFileDescriptor? = null
|
|
40
|
-
private var currentPage:
|
|
41
|
-
private var
|
|
52
|
+
private var currentPage: PdfPage? = null
|
|
53
|
+
private var baseBitmap: Bitmap? = null
|
|
54
|
+
|
|
55
|
+
private val activeTiles = ConcurrentHashMap<String, Tile>()
|
|
56
|
+
private val tileFutures = ConcurrentHashMap<String, Future<*>>()
|
|
57
|
+
private var pendingBaseRenderFuture: Future<*>? = null
|
|
58
|
+
private val renderExecutor: ExecutorService = Executors.newFixedThreadPool(2)
|
|
42
59
|
|
|
43
60
|
private var source: String = ""
|
|
44
61
|
private var pageIndex: Int = 0
|
|
@@ -56,8 +73,6 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
56
73
|
|
|
57
74
|
private var needsLoad: Boolean = false
|
|
58
75
|
private var pendingPageIndex: Int? = null
|
|
59
|
-
private var decryptedFile: File? = null
|
|
60
|
-
private var pdfBoxInitialized: Boolean = false
|
|
61
76
|
|
|
62
77
|
private val paint = Paint().apply {
|
|
63
78
|
isAntiAlias = true
|
|
@@ -70,7 +85,7 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
70
85
|
|
|
71
86
|
private var lastTouchX: Float = 0f
|
|
72
87
|
private var lastTouchY: Float = 0f
|
|
73
|
-
private var
|
|
88
|
+
private var activePointerId: Int = -1
|
|
74
89
|
|
|
75
90
|
init {
|
|
76
91
|
setBackgroundColor(Color.WHITE)
|
|
@@ -103,6 +118,7 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
103
118
|
translateY = focusY - newContentY - (pdfY * scaleFactor)
|
|
104
119
|
|
|
105
120
|
constrainTranslation()
|
|
121
|
+
updateTiles()
|
|
106
122
|
invalidate()
|
|
107
123
|
}
|
|
108
124
|
return true
|
|
@@ -118,21 +134,6 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
118
134
|
}
|
|
119
135
|
return true
|
|
120
136
|
}
|
|
121
|
-
|
|
122
|
-
override fun onScroll(
|
|
123
|
-
e1: MotionEvent?,
|
|
124
|
-
e2: MotionEvent,
|
|
125
|
-
distanceX: Float,
|
|
126
|
-
distanceY: Float
|
|
127
|
-
): Boolean {
|
|
128
|
-
if (!scaleGestureDetector.isInProgress) {
|
|
129
|
-
translateX -= distanceX
|
|
130
|
-
translateY -= distanceY
|
|
131
|
-
constrainTranslation()
|
|
132
|
-
invalidate()
|
|
133
|
-
}
|
|
134
|
-
return true
|
|
135
|
-
}
|
|
136
137
|
})
|
|
137
138
|
}
|
|
138
139
|
|
|
@@ -168,7 +169,8 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
168
169
|
translateY = focusY - newContentY - (pdfY * currentScale)
|
|
169
170
|
}
|
|
170
171
|
constrainTranslation()
|
|
171
|
-
|
|
172
|
+
updateTiles()
|
|
173
|
+
invalidate()
|
|
172
174
|
}
|
|
173
175
|
start()
|
|
174
176
|
}
|
|
@@ -195,17 +197,76 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
200
|
+
scaleGestureDetector.onTouchEvent(event)
|
|
201
|
+
gestureDetector.onTouchEvent(event)
|
|
202
|
+
|
|
203
|
+
when (event.actionMasked) {
|
|
204
|
+
MotionEvent.ACTION_DOWN -> {
|
|
205
|
+
activePointerId = event.getPointerId(0)
|
|
206
|
+
lastTouchX = event.x
|
|
207
|
+
lastTouchY = event.y
|
|
208
|
+
if (scaleFactor > minScaleFactor + 0.01f) {
|
|
209
|
+
parent?.requestDisallowInterceptTouchEvent(true)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
MotionEvent.ACTION_MOVE -> {
|
|
213
|
+
if (!scaleGestureDetector.isInProgress) {
|
|
214
|
+
val pointerIndex = event.findPointerIndex(activePointerId)
|
|
215
|
+
if (pointerIndex >= 0) {
|
|
216
|
+
val x = event.getX(pointerIndex)
|
|
217
|
+
val y = event.getY(pointerIndex)
|
|
218
|
+
|
|
219
|
+
translateX += (x - lastTouchX)
|
|
220
|
+
translateY += (y - lastTouchY)
|
|
221
|
+
constrainTranslation()
|
|
222
|
+
updateTiles()
|
|
223
|
+
invalidate()
|
|
224
|
+
|
|
225
|
+
lastTouchX = x
|
|
226
|
+
lastTouchY = y
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
val pointerIndex = event.findPointerIndex(activePointerId)
|
|
230
|
+
if (pointerIndex >= 0) {
|
|
231
|
+
lastTouchX = event.getX(pointerIndex)
|
|
232
|
+
lastTouchY = event.getY(pointerIndex)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
MotionEvent.ACTION_POINTER_UP -> {
|
|
237
|
+
val pointerIndex = event.actionIndex
|
|
238
|
+
val pointerId = event.getPointerId(pointerIndex)
|
|
239
|
+
if (pointerId == activePointerId) {
|
|
240
|
+
|
|
241
|
+
val newIndex = if (pointerIndex == 0) 1 else 0
|
|
242
|
+
if (newIndex < event.pointerCount) {
|
|
243
|
+
activePointerId = event.getPointerId(newIndex)
|
|
244
|
+
lastTouchX = event.getX(newIndex)
|
|
245
|
+
lastTouchY = event.getY(newIndex)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (event.pointerCount == 2) {
|
|
250
|
+
val idx = event.findPointerIndex(activePointerId)
|
|
251
|
+
if (idx >= 0) {
|
|
252
|
+
lastTouchX = event.getX(idx)
|
|
253
|
+
lastTouchY = event.getY(idx)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
258
|
+
activePointerId = -1
|
|
259
|
+
parent?.requestDisallowInterceptTouchEvent(false)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return true
|
|
201
263
|
}
|
|
202
264
|
|
|
203
265
|
fun setSource(source: String) {
|
|
204
266
|
if (this.source != source) {
|
|
205
267
|
this.source = source
|
|
206
268
|
needsLoad = true
|
|
207
|
-
|
|
208
|
-
pdfRenderer = null
|
|
269
|
+
closePdfDocument()
|
|
209
270
|
pendingPageIndex = pageIndex
|
|
210
271
|
requestLayout()
|
|
211
272
|
invalidate()
|
|
@@ -215,7 +276,7 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
215
276
|
fun setPage(page: Int) {
|
|
216
277
|
if (this.pageIndex != page) {
|
|
217
278
|
this.pageIndex = page
|
|
218
|
-
if (
|
|
279
|
+
if (pdfDocument != null) {
|
|
219
280
|
displayPage(page)
|
|
220
281
|
} else {
|
|
221
282
|
pendingPageIndex = page
|
|
@@ -241,8 +302,7 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
241
302
|
fun setPassword(pwd: String) {
|
|
242
303
|
if (this.password != pwd) {
|
|
243
304
|
this.password = pwd
|
|
244
|
-
|
|
245
|
-
if (source.isNotEmpty() && pdfRenderer == null) {
|
|
305
|
+
if (source.isNotEmpty() && pdfDocument == null) {
|
|
246
306
|
needsLoad = true
|
|
247
307
|
requestLayout()
|
|
248
308
|
}
|
|
@@ -255,12 +315,28 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
255
315
|
if (width > 0 && height > 0 && needsLoad) {
|
|
256
316
|
needsLoad = false
|
|
257
317
|
loadPdf()
|
|
258
|
-
} else if (changed &&
|
|
318
|
+
} else if (changed && baseBitmap != null) {
|
|
259
319
|
calculateFitScale()
|
|
260
320
|
constrainTranslation()
|
|
261
321
|
}
|
|
262
322
|
}
|
|
263
323
|
|
|
324
|
+
private fun closePdfDocument() {
|
|
325
|
+
pendingBaseRenderFuture?.cancel(true)
|
|
326
|
+
pendingBaseRenderFuture = null
|
|
327
|
+
try { currentPage?.close() } catch (_: Exception) {}
|
|
328
|
+
currentPage = null
|
|
329
|
+
|
|
330
|
+
if (pdfDocument != null) {
|
|
331
|
+
try { pdfDocument?.close() } catch (_: Exception) {}
|
|
332
|
+
pdfDocument = null
|
|
333
|
+
fileDescriptor = null
|
|
334
|
+
} else {
|
|
335
|
+
try { fileDescriptor?.close() } catch (_: Exception) {}
|
|
336
|
+
fileDescriptor = null
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
264
340
|
private fun loadPdf() {
|
|
265
341
|
if (source.isEmpty()) return
|
|
266
342
|
|
|
@@ -277,22 +353,29 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
277
353
|
return
|
|
278
354
|
}
|
|
279
355
|
|
|
280
|
-
|
|
356
|
+
fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
|
|
281
357
|
|
|
282
358
|
try {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
} catch (e:
|
|
359
|
+
val pwd = if (password.isNotEmpty()) password else null
|
|
360
|
+
pdfDocument = pdfiumCore.newDocument(fileDescriptor!!, pwd)
|
|
361
|
+
} catch (e: PdfPasswordException) {
|
|
286
362
|
fileDescriptor?.close()
|
|
287
363
|
fileDescriptor = null
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
364
|
+
if (password.isEmpty()) {
|
|
365
|
+
sendPasswordRequired()
|
|
366
|
+
sendError("PDF is password protected")
|
|
367
|
+
} else {
|
|
368
|
+
sendError("Invalid password for PDF")
|
|
369
|
+
}
|
|
370
|
+
return
|
|
371
|
+
} catch (e: Exception) {
|
|
372
|
+
fileDescriptor?.close()
|
|
373
|
+
fileDescriptor = null
|
|
374
|
+
sendError("Failed to load PDF: ${e.message}")
|
|
375
|
+
return
|
|
293
376
|
}
|
|
294
377
|
|
|
295
|
-
val pageCount =
|
|
378
|
+
val pageCount = pdfDocument!!.getPageCount()
|
|
296
379
|
|
|
297
380
|
sendPageCount(pageCount)
|
|
298
381
|
|
|
@@ -304,81 +387,30 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
304
387
|
sendError("Failed to load PDF: ${e.message}")
|
|
305
388
|
}
|
|
306
389
|
}
|
|
307
|
-
|
|
308
|
-
private fun decryptPdfWithPassword(file: File): File? {
|
|
309
|
-
if (!pdfBoxInitialized) {
|
|
310
|
-
PDFBoxResourceLoader.init(context)
|
|
311
|
-
pdfBoxInitialized = true
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
val document: PDDocument
|
|
315
|
-
try {
|
|
316
|
-
document = if (password.isNotEmpty()) {
|
|
317
|
-
PDDocument.load(file, password)
|
|
318
|
-
} else {
|
|
319
|
-
try {
|
|
320
|
-
PDDocument.load(file, "")
|
|
321
|
-
} catch (e: InvalidPasswordException) {
|
|
322
|
-
sendPasswordRequired()
|
|
323
|
-
sendError("PDF is password protected")
|
|
324
|
-
return null
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
} catch (e: InvalidPasswordException) {
|
|
328
|
-
sendError("Invalid password for PDF")
|
|
329
|
-
return null
|
|
330
|
-
} catch (e: Exception) {
|
|
331
|
-
sendError("Failed to decrypt PDF: ${e.message}")
|
|
332
|
-
return null
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
try {
|
|
336
|
-
if (document.isEncrypted) {
|
|
337
|
-
document.setAllSecurityToBeRemoved(true)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
val tempFile = File(context.cacheDir, "decrypted_${System.currentTimeMillis()}.pdf")
|
|
341
|
-
document.save(FileOutputStream(tempFile))
|
|
342
|
-
document.close()
|
|
343
|
-
|
|
344
|
-
decryptedFile?.delete()
|
|
345
|
-
decryptedFile = tempFile
|
|
346
|
-
|
|
347
|
-
return tempFile
|
|
348
|
-
} catch (e: Exception) {
|
|
349
|
-
document.close()
|
|
350
|
-
sendError("Failed to save decrypted PDF: ${e.message}")
|
|
351
|
-
return null
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
390
|
|
|
355
391
|
private fun displayPage(index: Int) {
|
|
356
|
-
val
|
|
392
|
+
val doc = pdfDocument ?: return
|
|
357
393
|
|
|
358
|
-
|
|
394
|
+
val pageCount = doc.getPageCount()
|
|
395
|
+
if (index < 0 || index >= pageCount) {
|
|
359
396
|
sendError("Invalid page index: $index")
|
|
360
397
|
return
|
|
361
398
|
}
|
|
362
399
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
currentPage = renderer.openPage(index)
|
|
366
|
-
val page = currentPage!!
|
|
400
|
+
pendingBaseRenderFuture?.cancel(true)
|
|
401
|
+
pendingBaseRenderFuture = null
|
|
367
402
|
|
|
368
|
-
|
|
369
|
-
|
|
403
|
+
baseBitmap?.recycle()
|
|
404
|
+
baseBitmap = null
|
|
370
405
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
val bitmapHeight = (pageHeight * scale).toInt()
|
|
406
|
+
currentPage?.close()
|
|
407
|
+
currentPage = null
|
|
374
408
|
|
|
375
|
-
|
|
409
|
+
val page = doc.openPage(index)
|
|
410
|
+
currentPage = page
|
|
376
411
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
bitmap.eraseColor(Color.WHITE)
|
|
380
|
-
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
|
381
|
-
}
|
|
412
|
+
pageWidth = page.getPageWidthPoint()
|
|
413
|
+
pageHeight = page.getPageHeightPoint()
|
|
382
414
|
|
|
383
415
|
pageIndex = index
|
|
384
416
|
calculateFitScale()
|
|
@@ -387,23 +419,150 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
387
419
|
translateX = 0f
|
|
388
420
|
translateY = 0f
|
|
389
421
|
|
|
422
|
+
clearTiles()
|
|
423
|
+
updateTiles()
|
|
390
424
|
invalidate()
|
|
391
425
|
|
|
426
|
+
val scale = calculateBaseScale()
|
|
427
|
+
val bmpWidth = max(1, (pageWidth * scale).toInt())
|
|
428
|
+
val bmpHeight = max(1, (pageHeight * scale).toInt())
|
|
429
|
+
|
|
430
|
+
pendingBaseRenderFuture = renderExecutor.submit {
|
|
431
|
+
try {
|
|
432
|
+
val bitmap = Bitmap.createBitmap(bmpWidth, bmpHeight, Bitmap.Config.ARGB_8888)
|
|
433
|
+
bitmap.eraseColor(Color.WHITE)
|
|
434
|
+
|
|
435
|
+
synchronized(page) {
|
|
436
|
+
if (currentPage == page) {
|
|
437
|
+
page.renderPageBitmap(
|
|
438
|
+
bitmap, 0, 0, bmpWidth, bmpHeight,
|
|
439
|
+
renderAnnot = true
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
post {
|
|
445
|
+
if (currentPage == page) {
|
|
446
|
+
baseBitmap?.recycle()
|
|
447
|
+
baseBitmap = bitmap
|
|
448
|
+
invalidate()
|
|
449
|
+
} else {
|
|
450
|
+
bitmap.recycle()
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} catch (_: Exception) { }
|
|
454
|
+
}
|
|
455
|
+
|
|
392
456
|
sendLoadComplete(index, pageWidth, pageHeight)
|
|
393
457
|
}
|
|
394
458
|
|
|
395
|
-
private fun
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
459
|
+
private fun clearTiles() {
|
|
460
|
+
tileFutures.values.forEach { it.cancel(true) }
|
|
461
|
+
tileFutures.clear()
|
|
462
|
+
activeTiles.values.forEach { it.bitmap.recycle() }
|
|
463
|
+
activeTiles.clear()
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private fun updateTiles() {
|
|
467
|
+
if (width == 0 || height == 0 || pageWidth == 0 || pageHeight == 0) return
|
|
468
|
+
val page = currentPage ?: return
|
|
469
|
+
|
|
470
|
+
val scaledWidth = pageWidth * scaleFactor
|
|
471
|
+
val scaledHeight = pageHeight * scaleFactor
|
|
472
|
+
|
|
473
|
+
val contentX = (width - scaledWidth) / 2 + translateX
|
|
474
|
+
val contentY = (height - scaledHeight) / 2 + translateY
|
|
475
|
+
|
|
476
|
+
val visibleLeft = max(0f, -contentX)
|
|
477
|
+
val visibleTop = max(0f, -contentY)
|
|
478
|
+
val visibleRight = min(scaledWidth, width - contentX)
|
|
479
|
+
val visibleBottom = min(scaledHeight, height - contentY)
|
|
480
|
+
|
|
481
|
+
if (visibleRight <= visibleLeft || visibleBottom <= visibleTop) return
|
|
482
|
+
|
|
483
|
+
val cols = Math.ceil((scaledWidth / TILE_SIZE).toDouble()).toInt()
|
|
484
|
+
val rows = Math.ceil((scaledHeight / TILE_SIZE).toDouble()).toInt()
|
|
485
|
+
|
|
486
|
+
val startCol = Math.floor((visibleLeft / TILE_SIZE).toDouble()).toInt().coerceIn(0, cols - 1)
|
|
487
|
+
val endCol = Math.floor((visibleRight / TILE_SIZE).toDouble()).toInt().coerceIn(0, cols - 1)
|
|
488
|
+
val startRow = Math.floor((visibleTop / TILE_SIZE).toDouble()).toInt().coerceIn(0, rows - 1)
|
|
489
|
+
val endRow = Math.floor((visibleBottom / TILE_SIZE).toDouble()).toInt().coerceIn(0, rows - 1)
|
|
490
|
+
|
|
491
|
+
val neededKeys = mutableSetOf<String>()
|
|
492
|
+
val currentZoomFactor = scaleFactor
|
|
493
|
+
|
|
494
|
+
for (r in startRow..endRow) {
|
|
495
|
+
for (c in startCol..endCol) {
|
|
496
|
+
val key = "${currentZoomFactor}_${c}_${r}"
|
|
497
|
+
neededKeys.add(key)
|
|
498
|
+
|
|
499
|
+
if (!activeTiles.containsKey(key) && !tileFutures.containsKey(key)) {
|
|
500
|
+
val tileX = c * TILE_SIZE
|
|
501
|
+
val tileY = r * TILE_SIZE
|
|
502
|
+
|
|
503
|
+
val future = renderExecutor.submit {
|
|
504
|
+
try {
|
|
505
|
+
val bitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Bitmap.Config.ARGB_8888)
|
|
506
|
+
bitmap.eraseColor(Color.WHITE)
|
|
507
|
+
|
|
508
|
+
synchronized(page) {
|
|
509
|
+
if (currentPage == page) {
|
|
510
|
+
page.renderPageBitmap(
|
|
511
|
+
bitmap,
|
|
512
|
+
-tileX, -tileY,
|
|
513
|
+
scaledWidth.toInt(), scaledHeight.toInt(),
|
|
514
|
+
renderAnnot = true
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
val tile = Tile(tileX, tileY, TILE_SIZE, currentZoomFactor, bitmap)
|
|
520
|
+
if (scaleFactor == currentZoomFactor) {
|
|
521
|
+
activeTiles[key] = tile
|
|
522
|
+
postInvalidate()
|
|
523
|
+
} else {
|
|
524
|
+
bitmap.recycle()
|
|
525
|
+
}
|
|
526
|
+
} catch (e: Exception) {
|
|
527
|
+
e.printStackTrace()
|
|
528
|
+
} finally {
|
|
529
|
+
tileFutures.remove(key)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
tileFutures[key] = future
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
val keysToRemove = activeTiles.keys.filter { it !in neededKeys }
|
|
538
|
+
for (key in keysToRemove) {
|
|
539
|
+
activeTiles.remove(key)?.bitmap?.recycle()
|
|
540
|
+
}
|
|
541
|
+
val futuresToRemove = tileFutures.keys.filter { it !in neededKeys }
|
|
542
|
+
for (key in futuresToRemove) {
|
|
543
|
+
tileFutures.remove(key)?.cancel(true)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private fun calculateBaseScale(): Float {
|
|
548
|
+
if (pageWidth == 0 || pageHeight == 0) return 1.0f
|
|
400
549
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
550
|
+
var scale = if (width > 0 && height > 0) {
|
|
551
|
+
val scaleX = width.toFloat() / pageWidth
|
|
552
|
+
val scaleY = height.toFloat() / pageHeight
|
|
553
|
+
min(scaleX, scaleY)
|
|
404
554
|
} else {
|
|
405
|
-
|
|
555
|
+
1.0f
|
|
406
556
|
}
|
|
557
|
+
|
|
558
|
+
// Base image limit: ~4M pixels (approx. 16MB) to avoid exceeding the Canvas size limit
|
|
559
|
+
val maxPixels = 4000000.0
|
|
560
|
+
val bitmapPixels = (pageWidth.toDouble() * scale) * (pageHeight.toDouble() * scale)
|
|
561
|
+
if (bitmapPixels > maxPixels) {
|
|
562
|
+
scale = Math.sqrt(maxPixels / (pageWidth.toDouble() * pageHeight.toDouble())).toFloat()
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return max(scale, 0.1f)
|
|
407
566
|
}
|
|
408
567
|
|
|
409
568
|
private fun calculateFitScale() {
|
|
@@ -421,28 +580,37 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
421
580
|
override fun onDraw(canvas: Canvas) {
|
|
422
581
|
super.onDraw(canvas)
|
|
423
582
|
|
|
424
|
-
val bitmap = currentBitmap ?: return
|
|
425
|
-
|
|
426
|
-
canvas.save()
|
|
427
|
-
|
|
428
583
|
val scaledWidth = pageWidth * scaleFactor
|
|
429
584
|
val scaledHeight = pageHeight * scaleFactor
|
|
430
585
|
|
|
431
586
|
val centerX = (width - scaledWidth) / 2 + translateX
|
|
432
587
|
val centerY = (height - scaledHeight) / 2 + translateY
|
|
433
588
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
589
|
+
baseBitmap?.let { base ->
|
|
590
|
+
canvas.save()
|
|
591
|
+
canvas.translate(centerX, centerY)
|
|
592
|
+
val baseScaleX = scaledWidth / base.width.toFloat()
|
|
593
|
+
val baseScaleY = scaledHeight / base.height.toFloat()
|
|
594
|
+
canvas.scale(baseScaleX, baseScaleY)
|
|
595
|
+
canvas.drawBitmap(base, 0f, 0f, paint)
|
|
596
|
+
canvas.restore()
|
|
597
|
+
}
|
|
438
598
|
|
|
439
|
-
|
|
599
|
+
for (tile in activeTiles.values) {
|
|
600
|
+
if (tile.scale != scaleFactor) continue
|
|
601
|
+
|
|
602
|
+
canvas.save()
|
|
603
|
+
canvas.translate(centerX + tile.x, centerY + tile.y)
|
|
604
|
+
val drawRect = android.graphics.Rect(0, 0, tile.bitmap.width, tile.bitmap.height)
|
|
605
|
+
canvas.drawBitmap(tile.bitmap, drawRect, drawRect, paint)
|
|
606
|
+
canvas.restore()
|
|
607
|
+
}
|
|
440
608
|
}
|
|
441
609
|
|
|
442
610
|
private fun sendLoadComplete(page: Int, width: Int, height: Int) {
|
|
443
611
|
val reactContext = context as? ReactContext ?: return
|
|
444
612
|
val event = Arguments.createMap().apply {
|
|
445
|
-
putInt("currentPage", page + 1)
|
|
613
|
+
putInt("currentPage", page + 1)
|
|
446
614
|
putInt("width", width)
|
|
447
615
|
putInt("height", height)
|
|
448
616
|
}
|
|
@@ -476,24 +644,18 @@ class OptimizedPdfView @JvmOverloads constructor(
|
|
|
476
644
|
}
|
|
477
645
|
|
|
478
646
|
fun cleanup() {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
pdfRenderer?.close()
|
|
483
|
-
pdfRenderer = null
|
|
484
|
-
|
|
485
|
-
fileDescriptor?.close()
|
|
486
|
-
fileDescriptor = null
|
|
647
|
+
pendingBaseRenderFuture?.cancel(true)
|
|
648
|
+
clearTiles()
|
|
649
|
+
renderExecutor.shutdownNow()
|
|
487
650
|
|
|
488
|
-
|
|
489
|
-
|
|
651
|
+
baseBitmap?.recycle()
|
|
652
|
+
baseBitmap = null
|
|
490
653
|
|
|
491
|
-
|
|
492
|
-
decryptedFile = null
|
|
654
|
+
closePdfDocument()
|
|
493
655
|
}
|
|
494
656
|
|
|
495
657
|
override fun onDetachedFromWindow() {
|
|
496
658
|
super.onDetachedFromWindow()
|
|
497
659
|
cleanup()
|
|
498
660
|
}
|
|
499
|
-
}
|
|
661
|
+
}
|
package/docs/android-setup.md
CHANGED
|
@@ -56,7 +56,7 @@ override fun getPackages(): List<ReactPackage> =
|
|
|
56
56
|
|
|
57
57
|
## Features
|
|
58
58
|
|
|
59
|
-
- High-performance PDF rendering using
|
|
59
|
+
- High-performance PDF rendering using pdfium
|
|
60
60
|
- Smooth zoom and pan with gesture support
|
|
61
61
|
- Double-tap to zoom/reset
|
|
62
62
|
- Memory-efficient rendering with proper bitmap recycling
|
|
@@ -3,7 +3,7 @@ import UIKit
|
|
|
3
3
|
/// Configuration options for PDF rendering.
|
|
4
4
|
struct PDFConfiguration {
|
|
5
5
|
/// Maximum zoom scale allowed.
|
|
6
|
-
var maximumZoom: CGFloat =
|
|
6
|
+
var maximumZoom: CGFloat = 3
|
|
7
7
|
|
|
8
8
|
/// Whether to enable antialiasing for smoother rendering.
|
|
9
9
|
var enableAntialiasing: Bool = true
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import CoreGraphics
|
|
3
3
|
|
|
4
|
+
/// Strips the `file://` scheme prefix from a local PDF source string, if present.
|
|
5
|
+
/// Shared by the document loader and the view so URL normalization stays in one place.
|
|
6
|
+
func normalizedPDFPath(from source: String) -> String {
|
|
7
|
+
source.hasPrefix("file://") ? String(source.dropFirst(7)) : source
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
/// Protocol for PDF document loading operations.
|
|
5
11
|
protocol PDFDocumentLoading {
|
|
6
12
|
func loadDocument(from source: String, password: String?) -> Result<CGPDFDocument, PDFError>
|
|
@@ -40,10 +46,7 @@ final class PDFDocumentLoader: PDFDocumentLoading {
|
|
|
40
46
|
|
|
41
47
|
/// Normalizes the source path to a file URL.
|
|
42
48
|
private func normalizedURL(from source: String) -> URL {
|
|
43
|
-
|
|
44
|
-
? String(source.dropFirst(7))
|
|
45
|
-
: source
|
|
46
|
-
return URL(fileURLWithPath: path)
|
|
49
|
+
URL(fileURLWithPath: normalizedPDFPath(from: source))
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
/// Attempts to unlock an encrypted PDF document.
|