react-native-optimized-pdf 1.0.1 → 1.2.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.
@@ -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 com.tom_roush.pdfbox.android.PDFBoxResourceLoader
19
- import com.tom_roush.pdfbox.pdmodel.PDDocument
20
- import com.tom_roush.pdfbox.pdmodel.encryption.InvalidPasswordException
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.io.FileOutputStream
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 PdfRenderer do Android para renderização eficiente em memória
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
- private var pdfRenderer: PdfRenderer? = null
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: PdfRenderer.Page? = null
41
- private var currentBitmap: Bitmap? = null
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 isDragging: Boolean = false
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
- invalidate()
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
- var handled = scaleGestureDetector.onTouchEvent(event)
199
- handled = gestureDetector.onTouchEvent(event) || handled
200
- return handled || super.onTouchEvent(event)
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
- pdfRenderer?.close()
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 (pdfRenderer != null) {
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
- // Se já temos source mas falhou por senha, tenta recarregar
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 && currentBitmap != null) {
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
- var pdfFile = file
356
+ fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
281
357
 
282
358
  try {
283
- fileDescriptor = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY)
284
- pdfRenderer = PdfRenderer(fileDescriptor!!)
285
- } catch (e: SecurityException) {
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
- pdfFile = decryptPdfWithPassword(file) ?: return
290
-
291
- fileDescriptor = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY)
292
- pdfRenderer = PdfRenderer(fileDescriptor!!)
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 = pdfRenderer!!.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 renderer = pdfRenderer ?: return
392
+ val doc = pdfDocument ?: return
357
393
 
358
- if (index < 0 || index >= renderer.pageCount) {
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
- currentPage?.close()
364
-
365
- currentPage = renderer.openPage(index)
366
- val page = currentPage!!
400
+ pendingBaseRenderFuture?.cancel(true)
401
+ pendingBaseRenderFuture = null
367
402
 
368
- pageWidth = page.width
369
- pageHeight = page.height
403
+ baseBitmap?.recycle()
404
+ baseBitmap = null
370
405
 
371
- val scale = calculateRenderScale()
372
- val bitmapWidth = (pageWidth * scale).toInt()
373
- val bitmapHeight = (pageHeight * scale).toInt()
406
+ currentPage?.close()
407
+ currentPage = null
374
408
 
375
- currentBitmap?.recycle()
409
+ val page = doc.openPage(index)
410
+ currentPage = page
376
411
 
377
- currentBitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888)
378
- currentBitmap?.let { bitmap ->
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 calculateRenderScale(): Float {
396
- val displayMetrics = resources.displayMetrics
397
- val screenDensity = displayMetrics.density
398
-
399
- val baseScale = 2.0f * screenDensity
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
- val maxDimension = max(pageWidth, pageHeight) * baseScale
402
- return if (maxDimension > 4096) {
403
- 4096f / max(pageWidth, pageHeight)
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
- baseScale
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
- canvas.translate(centerX, centerY)
435
- canvas.scale(scaleFactor / calculateRenderScale(), scaleFactor / calculateRenderScale())
436
-
437
- canvas.drawBitmap(bitmap, 0f, 0f, paint)
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
- canvas.restore()
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) // 1-indexed como no iOS
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
- currentPage?.close()
480
- currentPage = null
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
- currentBitmap?.recycle()
489
- currentBitmap = null
651
+ baseBitmap?.recycle()
652
+ baseBitmap = null
490
653
 
491
- decryptedFile?.delete()
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
+ }
@@ -56,7 +56,7 @@ override fun getPackages(): List<ReactPackage> =
56
56
 
57
57
  ## Features
58
58
 
59
- - High-performance PDF rendering using Android's native PdfRenderer
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
@@ -0,0 +1,28 @@
1
+ import UIKit
2
+
3
+ /// Configuration options for PDF rendering.
4
+ struct PDFConfiguration {
5
+ /// Maximum zoom scale allowed.
6
+ var maximumZoom: CGFloat = 5.0
7
+
8
+ /// Whether to enable antialiasing for smoother rendering.
9
+ var enableAntialiasing: Bool = true
10
+
11
+ /// Password for encrypted PDFs.
12
+ var password: String?
13
+
14
+ /// Size of each tile for CATiledLayer rendering.
15
+ var tileSize: CGSize = CGSize(width: 512, height: 512)
16
+
17
+ /// Number of detail levels for zoom.
18
+ var levelsOfDetail: Int = 2
19
+
20
+ /// Bias for levels of detail (allows zoom up to 2^bias resolution).
21
+ var levelsOfDetailBias: Int = 8
22
+
23
+ /// Animation duration for zoom reset.
24
+ var zoomResetAnimationDuration: TimeInterval = 0.25
25
+
26
+ /// Default configuration.
27
+ static let `default` = PDFConfiguration()
28
+ }