react-native-optimized-pdf 1.0.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/.eslintrc.js +11 -0
- package/.prettierrc.js +10 -0
- package/CHANGELOG.md +35 -0
- package/CONTRIBUTING.md +91 -0
- package/README.md +302 -0
- package/ReactNativeOptimizedPdf.podspec +21 -0
- package/android/build.gradle +57 -0
- package/android/proguard-rules.pro +10 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/kotlin/com/reactnativeoptimizedpdf/OptimizedPdfView.kt +499 -0
- package/android/src/main/kotlin/com/reactnativeoptimizedpdf/OptimizedPdfViewManager.kt +68 -0
- package/android/src/main/kotlin/com/reactnativeoptimizedpdf/OptimizedPdfViewPackage.kt +20 -0
- package/docs/android-setup.md +63 -0
- package/index.d.ts +15 -0
- package/index.ts +13 -0
- package/ios/OptimizedPdfView.swift +256 -0
- package/ios/OptimizedPdfViewManager.m +22 -0
- package/ios/OptimizedPdfViewManager.swift +24 -0
- package/ios/TiledPdfPageView.swift +76 -0
- package/package.json +61 -0
- package/src/OptimizedPdfView.tsx +167 -0
- package/src/components/PdfNavigationControls.tsx +123 -0
- package/src/components/PdfOverlays.tsx +46 -0
- package/src/constants.ts +13 -0
- package/src/index.ts +5 -0
- package/src/services/pdfCache.ts +113 -0
- package/src/types/index.ts +112 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
package com.reactnativeoptimizedpdf
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.Canvas
|
|
6
|
+
import android.graphics.Color
|
|
7
|
+
import android.graphics.Paint
|
|
8
|
+
import android.graphics.pdf.PdfRenderer
|
|
9
|
+
import android.os.ParcelFileDescriptor
|
|
10
|
+
import android.util.AttributeSet
|
|
11
|
+
import android.view.GestureDetector
|
|
12
|
+
import android.view.MotionEvent
|
|
13
|
+
import android.view.ScaleGestureDetector
|
|
14
|
+
import android.widget.FrameLayout
|
|
15
|
+
import com.facebook.react.bridge.Arguments
|
|
16
|
+
import com.facebook.react.bridge.ReactContext
|
|
17
|
+
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
|
|
21
|
+
import java.io.File
|
|
22
|
+
import java.io.FileOutputStream
|
|
23
|
+
import kotlin.math.max
|
|
24
|
+
import kotlin.math.min
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* View otimizada para renderização de PDF no Android
|
|
28
|
+
*
|
|
29
|
+
* Usa PdfRenderer do Android para renderização eficiente em memória
|
|
30
|
+
* com suporte a zoom via ScaleGestureDetector e pan via GestureDetector
|
|
31
|
+
*/
|
|
32
|
+
class OptimizedPdfView @JvmOverloads constructor(
|
|
33
|
+
context: Context,
|
|
34
|
+
attrs: AttributeSet? = null,
|
|
35
|
+
defStyleAttr: Int = 0
|
|
36
|
+
) : FrameLayout(context, attrs, defStyleAttr) {
|
|
37
|
+
|
|
38
|
+
private var pdfRenderer: PdfRenderer? = null
|
|
39
|
+
private var fileDescriptor: ParcelFileDescriptor? = null
|
|
40
|
+
private var currentPage: PdfRenderer.Page? = null
|
|
41
|
+
private var currentBitmap: Bitmap? = null
|
|
42
|
+
|
|
43
|
+
private var source: String = ""
|
|
44
|
+
private var pageIndex: Int = 0
|
|
45
|
+
private var maximumZoom: Float = 5.0f
|
|
46
|
+
private var enableAntialiasing: Boolean = true
|
|
47
|
+
private var password: String = ""
|
|
48
|
+
|
|
49
|
+
private var scaleFactor: Float = 1.0f
|
|
50
|
+
private var minScaleFactor: Float = 1.0f
|
|
51
|
+
private var translateX: Float = 0f
|
|
52
|
+
private var translateY: Float = 0f
|
|
53
|
+
|
|
54
|
+
private var pageWidth: Int = 0
|
|
55
|
+
private var pageHeight: Int = 0
|
|
56
|
+
|
|
57
|
+
private var needsLoad: Boolean = false
|
|
58
|
+
private var pendingPageIndex: Int? = null
|
|
59
|
+
private var decryptedFile: File? = null
|
|
60
|
+
private var pdfBoxInitialized: Boolean = false
|
|
61
|
+
|
|
62
|
+
private val paint = Paint().apply {
|
|
63
|
+
isAntiAlias = true
|
|
64
|
+
isFilterBitmap = true
|
|
65
|
+
isDither = true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private val scaleGestureDetector: ScaleGestureDetector
|
|
69
|
+
private val gestureDetector: GestureDetector
|
|
70
|
+
|
|
71
|
+
private var lastTouchX: Float = 0f
|
|
72
|
+
private var lastTouchY: Float = 0f
|
|
73
|
+
private var isDragging: Boolean = false
|
|
74
|
+
|
|
75
|
+
init {
|
|
76
|
+
setBackgroundColor(Color.WHITE)
|
|
77
|
+
setWillNotDraw(false)
|
|
78
|
+
|
|
79
|
+
scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
|
80
|
+
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
|
81
|
+
val oldScale = scaleFactor
|
|
82
|
+
scaleFactor *= detector.scaleFactor
|
|
83
|
+
scaleFactor = max(minScaleFactor, min(scaleFactor, maximumZoom))
|
|
84
|
+
|
|
85
|
+
if (oldScale != scaleFactor) {
|
|
86
|
+
val focusX = detector.focusX
|
|
87
|
+
val focusY = detector.focusY
|
|
88
|
+
|
|
89
|
+
val oldScaledWidth = pageWidth * oldScale
|
|
90
|
+
val oldScaledHeight = pageHeight * oldScale
|
|
91
|
+
val oldContentX = (width - oldScaledWidth) / 2 + translateX
|
|
92
|
+
val oldContentY = (height - oldScaledHeight) / 2 + translateY
|
|
93
|
+
|
|
94
|
+
val pdfX = (focusX - oldContentX) / oldScale
|
|
95
|
+
val pdfY = (focusY - oldContentY) / oldScale
|
|
96
|
+
|
|
97
|
+
val newScaledWidth = pageWidth * scaleFactor
|
|
98
|
+
val newScaledHeight = pageHeight * scaleFactor
|
|
99
|
+
val newContentX = (width - newScaledWidth) / 2
|
|
100
|
+
val newContentY = (height - newScaledHeight) / 2
|
|
101
|
+
|
|
102
|
+
translateX = focusX - newContentX - (pdfX * scaleFactor)
|
|
103
|
+
translateY = focusY - newContentY - (pdfY * scaleFactor)
|
|
104
|
+
|
|
105
|
+
constrainTranslation()
|
|
106
|
+
invalidate()
|
|
107
|
+
}
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
|
113
|
+
override fun onDoubleTap(e: MotionEvent): Boolean {
|
|
114
|
+
if (scaleFactor > minScaleFactor) {
|
|
115
|
+
animateToScale(minScaleFactor, e.x, e.y)
|
|
116
|
+
} else {
|
|
117
|
+
animateToScale(min(2.5f, maximumZoom), e.x, e.y)
|
|
118
|
+
}
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
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
|
+
}
|
|
138
|
+
|
|
139
|
+
private fun animateToScale(targetScale: Float, focusX: Float, focusY: Float) {
|
|
140
|
+
val startScale = scaleFactor
|
|
141
|
+
val startTranslateX = translateX
|
|
142
|
+
val startTranslateY = translateY
|
|
143
|
+
|
|
144
|
+
val oldScaledWidth = pageWidth * startScale
|
|
145
|
+
val oldScaledHeight = pageHeight * startScale
|
|
146
|
+
val oldContentX = (width - oldScaledWidth) / 2 + startTranslateX
|
|
147
|
+
val oldContentY = (height - oldScaledHeight) / 2 + startTranslateY
|
|
148
|
+
val pdfX = (focusX - oldContentX) / startScale
|
|
149
|
+
val pdfY = (focusY - oldContentY) / startScale
|
|
150
|
+
|
|
151
|
+
android.animation.ValueAnimator.ofFloat(0f, 1f).apply {
|
|
152
|
+
duration = 250
|
|
153
|
+
addUpdateListener { animator ->
|
|
154
|
+
val fraction = animator.animatedValue as Float
|
|
155
|
+
val currentScale = startScale + (targetScale - startScale) * fraction
|
|
156
|
+
scaleFactor = currentScale
|
|
157
|
+
|
|
158
|
+
if (targetScale <= minScaleFactor) {
|
|
159
|
+
translateX = startTranslateX * (1 - fraction)
|
|
160
|
+
translateY = startTranslateY * (1 - fraction)
|
|
161
|
+
} else {
|
|
162
|
+
val newScaledWidth = pageWidth * currentScale
|
|
163
|
+
val newScaledHeight = pageHeight * currentScale
|
|
164
|
+
val newContentX = (width - newScaledWidth) / 2
|
|
165
|
+
val newContentY = (height - newScaledHeight) / 2
|
|
166
|
+
|
|
167
|
+
translateX = focusX - newContentX - (pdfX * currentScale)
|
|
168
|
+
translateY = focusY - newContentY - (pdfY * currentScale)
|
|
169
|
+
}
|
|
170
|
+
constrainTranslation()
|
|
171
|
+
invalidate()
|
|
172
|
+
}
|
|
173
|
+
start()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private fun constrainTranslation() {
|
|
178
|
+
val scaledWidth = pageWidth * scaleFactor
|
|
179
|
+
val scaledHeight = pageHeight * scaleFactor
|
|
180
|
+
|
|
181
|
+
val maxTranslateX = max(0f, (scaledWidth - width) / 2)
|
|
182
|
+
val maxTranslateY = max(0f, (scaledHeight - height) / 2)
|
|
183
|
+
|
|
184
|
+
if (scaledWidth <= width) {
|
|
185
|
+
translateX = 0f
|
|
186
|
+
} else {
|
|
187
|
+
translateX = translateX.coerceIn(-maxTranslateX, maxTranslateX)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (scaledHeight <= height) {
|
|
191
|
+
translateY = 0f
|
|
192
|
+
} else {
|
|
193
|
+
translateY = translateY.coerceIn(-maxTranslateY, maxTranslateY)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
198
|
+
var handled = scaleGestureDetector.onTouchEvent(event)
|
|
199
|
+
handled = gestureDetector.onTouchEvent(event) || handled
|
|
200
|
+
return handled || super.onTouchEvent(event)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fun setSource(source: String) {
|
|
204
|
+
if (this.source != source) {
|
|
205
|
+
this.source = source
|
|
206
|
+
needsLoad = true
|
|
207
|
+
pdfRenderer?.close()
|
|
208
|
+
pdfRenderer = null
|
|
209
|
+
pendingPageIndex = pageIndex
|
|
210
|
+
requestLayout()
|
|
211
|
+
invalidate()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fun setPage(page: Int) {
|
|
216
|
+
if (this.pageIndex != page) {
|
|
217
|
+
this.pageIndex = page
|
|
218
|
+
if (pdfRenderer != null) {
|
|
219
|
+
displayPage(page)
|
|
220
|
+
} else {
|
|
221
|
+
pendingPageIndex = page
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fun setMaximumZoom(zoom: Float) {
|
|
227
|
+
this.maximumZoom = zoom
|
|
228
|
+
if (scaleFactor > zoom) {
|
|
229
|
+
scaleFactor = zoom
|
|
230
|
+
invalidate()
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fun setEnableAntialiasing(enable: Boolean) {
|
|
235
|
+
this.enableAntialiasing = enable
|
|
236
|
+
paint.isAntiAlias = enable
|
|
237
|
+
paint.isFilterBitmap = enable
|
|
238
|
+
invalidate()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fun setPassword(pwd: String) {
|
|
242
|
+
if (this.password != pwd) {
|
|
243
|
+
this.password = pwd
|
|
244
|
+
// Se já temos source mas falhou por senha, tenta recarregar
|
|
245
|
+
if (source.isNotEmpty() && pdfRenderer == null) {
|
|
246
|
+
needsLoad = true
|
|
247
|
+
requestLayout()
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
253
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
254
|
+
|
|
255
|
+
if (width > 0 && height > 0 && needsLoad) {
|
|
256
|
+
needsLoad = false
|
|
257
|
+
loadPdf()
|
|
258
|
+
} else if (changed && currentBitmap != null) {
|
|
259
|
+
calculateFitScale()
|
|
260
|
+
constrainTranslation()
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private fun loadPdf() {
|
|
265
|
+
if (source.isEmpty()) return
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
val path = if (source.startsWith("file://")) {
|
|
269
|
+
source.substring(7)
|
|
270
|
+
} else {
|
|
271
|
+
source
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
val file = File(path)
|
|
275
|
+
if (!file.exists()) {
|
|
276
|
+
sendError("PDF file not found: $path")
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
var pdfFile = file
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
fileDescriptor = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY)
|
|
284
|
+
pdfRenderer = PdfRenderer(fileDescriptor!!)
|
|
285
|
+
} catch (e: SecurityException) {
|
|
286
|
+
fileDescriptor?.close()
|
|
287
|
+
fileDescriptor = null
|
|
288
|
+
|
|
289
|
+
pdfFile = decryptPdfWithPassword(file) ?: return
|
|
290
|
+
|
|
291
|
+
fileDescriptor = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY)
|
|
292
|
+
pdfRenderer = PdfRenderer(fileDescriptor!!)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
val pageCount = pdfRenderer!!.pageCount
|
|
296
|
+
|
|
297
|
+
sendPageCount(pageCount)
|
|
298
|
+
|
|
299
|
+
val targetPage = pendingPageIndex ?: 0
|
|
300
|
+
pendingPageIndex = null
|
|
301
|
+
displayPage(targetPage.coerceIn(0, pageCount - 1))
|
|
302
|
+
|
|
303
|
+
} catch (e: Exception) {
|
|
304
|
+
sendError("Failed to load PDF: ${e.message}")
|
|
305
|
+
}
|
|
306
|
+
}
|
|
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
|
+
|
|
355
|
+
private fun displayPage(index: Int) {
|
|
356
|
+
val renderer = pdfRenderer ?: return
|
|
357
|
+
|
|
358
|
+
if (index < 0 || index >= renderer.pageCount) {
|
|
359
|
+
sendError("Invalid page index: $index")
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
currentPage?.close()
|
|
364
|
+
|
|
365
|
+
currentPage = renderer.openPage(index)
|
|
366
|
+
val page = currentPage!!
|
|
367
|
+
|
|
368
|
+
pageWidth = page.width
|
|
369
|
+
pageHeight = page.height
|
|
370
|
+
|
|
371
|
+
val scale = calculateRenderScale()
|
|
372
|
+
val bitmapWidth = (pageWidth * scale).toInt()
|
|
373
|
+
val bitmapHeight = (pageHeight * scale).toInt()
|
|
374
|
+
|
|
375
|
+
currentBitmap?.recycle()
|
|
376
|
+
|
|
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
|
+
}
|
|
382
|
+
|
|
383
|
+
pageIndex = index
|
|
384
|
+
calculateFitScale()
|
|
385
|
+
|
|
386
|
+
scaleFactor = minScaleFactor
|
|
387
|
+
translateX = 0f
|
|
388
|
+
translateY = 0f
|
|
389
|
+
|
|
390
|
+
invalidate()
|
|
391
|
+
|
|
392
|
+
sendLoadComplete(index, pageWidth, pageHeight)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private fun calculateRenderScale(): Float {
|
|
396
|
+
val displayMetrics = resources.displayMetrics
|
|
397
|
+
val screenDensity = displayMetrics.density
|
|
398
|
+
|
|
399
|
+
val baseScale = 2.0f * screenDensity
|
|
400
|
+
|
|
401
|
+
val maxDimension = max(pageWidth, pageHeight) * baseScale
|
|
402
|
+
return if (maxDimension > 4096) {
|
|
403
|
+
4096f / max(pageWidth, pageHeight)
|
|
404
|
+
} else {
|
|
405
|
+
baseScale
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private fun calculateFitScale() {
|
|
410
|
+
if (pageWidth == 0 || pageHeight == 0 || width == 0 || height == 0) return
|
|
411
|
+
|
|
412
|
+
val scaleX = width.toFloat() / pageWidth
|
|
413
|
+
val scaleY = height.toFloat() / pageHeight
|
|
414
|
+
minScaleFactor = min(scaleX, scaleY)
|
|
415
|
+
|
|
416
|
+
if (scaleFactor < minScaleFactor) {
|
|
417
|
+
scaleFactor = minScaleFactor
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
override fun onDraw(canvas: Canvas) {
|
|
422
|
+
super.onDraw(canvas)
|
|
423
|
+
|
|
424
|
+
val bitmap = currentBitmap ?: return
|
|
425
|
+
|
|
426
|
+
canvas.save()
|
|
427
|
+
|
|
428
|
+
val scaledWidth = pageWidth * scaleFactor
|
|
429
|
+
val scaledHeight = pageHeight * scaleFactor
|
|
430
|
+
|
|
431
|
+
val centerX = (width - scaledWidth) / 2 + translateX
|
|
432
|
+
val centerY = (height - scaledHeight) / 2 + translateY
|
|
433
|
+
|
|
434
|
+
canvas.translate(centerX, centerY)
|
|
435
|
+
canvas.scale(scaleFactor / calculateRenderScale(), scaleFactor / calculateRenderScale())
|
|
436
|
+
|
|
437
|
+
canvas.drawBitmap(bitmap, 0f, 0f, paint)
|
|
438
|
+
|
|
439
|
+
canvas.restore()
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private fun sendLoadComplete(page: Int, width: Int, height: Int) {
|
|
443
|
+
val reactContext = context as? ReactContext ?: return
|
|
444
|
+
val event = Arguments.createMap().apply {
|
|
445
|
+
putInt("currentPage", page + 1) // 1-indexed como no iOS
|
|
446
|
+
putInt("width", width)
|
|
447
|
+
putInt("height", height)
|
|
448
|
+
}
|
|
449
|
+
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
450
|
+
.receiveEvent(id, "onLoadComplete", event)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private fun sendError(message: String) {
|
|
454
|
+
val reactContext = context as? ReactContext ?: return
|
|
455
|
+
val event = Arguments.createMap().apply {
|
|
456
|
+
putString("message", message)
|
|
457
|
+
}
|
|
458
|
+
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
459
|
+
.receiveEvent(id, "onError", event)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private fun sendPageCount(count: Int) {
|
|
463
|
+
val reactContext = context as? ReactContext ?: return
|
|
464
|
+
val event = Arguments.createMap().apply {
|
|
465
|
+
putInt("numberOfPages", count)
|
|
466
|
+
}
|
|
467
|
+
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
468
|
+
.receiveEvent(id, "onPageCount", event)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private fun sendPasswordRequired() {
|
|
472
|
+
val reactContext = context as? ReactContext ?: return
|
|
473
|
+
val event = Arguments.createMap()
|
|
474
|
+
reactContext.getJSModule(RCTEventEmitter::class.java)
|
|
475
|
+
.receiveEvent(id, "onPasswordRequired", event)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
fun cleanup() {
|
|
479
|
+
currentPage?.close()
|
|
480
|
+
currentPage = null
|
|
481
|
+
|
|
482
|
+
pdfRenderer?.close()
|
|
483
|
+
pdfRenderer = null
|
|
484
|
+
|
|
485
|
+
fileDescriptor?.close()
|
|
486
|
+
fileDescriptor = null
|
|
487
|
+
|
|
488
|
+
currentBitmap?.recycle()
|
|
489
|
+
currentBitmap = null
|
|
490
|
+
|
|
491
|
+
decryptedFile?.delete()
|
|
492
|
+
decryptedFile = null
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
override fun onDetachedFromWindow() {
|
|
496
|
+
super.onDetachedFromWindow()
|
|
497
|
+
cleanup()
|
|
498
|
+
}
|
|
499
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
package com.reactnativeoptimizedpdf
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReadableMap
|
|
4
|
+
import com.facebook.react.common.MapBuilder
|
|
5
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
6
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ViewManager que expõe o OptimizedPdfView para o React Native
|
|
11
|
+
*
|
|
12
|
+
* Props suportadas:
|
|
13
|
+
* - source: String (caminho do arquivo PDF)
|
|
14
|
+
* - page: Int (página atual, 0-indexed)
|
|
15
|
+
* - maximumZoom: Float (zoom máximo permitido)
|
|
16
|
+
* - enableAntialiasing: Boolean (habilitar antialiasing)
|
|
17
|
+
*/
|
|
18
|
+
class OptimizedPdfViewManager : SimpleViewManager<OptimizedPdfView>() {
|
|
19
|
+
|
|
20
|
+
companion object {
|
|
21
|
+
const val REACT_CLASS = "OptimizedPdfView"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun getName(): String = REACT_CLASS
|
|
25
|
+
|
|
26
|
+
override fun createViewInstance(reactContext: ThemedReactContext): OptimizedPdfView {
|
|
27
|
+
return OptimizedPdfView(reactContext)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@ReactProp(name = "source")
|
|
31
|
+
fun setSource(view: OptimizedPdfView, source: String?) {
|
|
32
|
+
source?.let { view.setSource(it) }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@ReactProp(name = "page", defaultInt = 0)
|
|
36
|
+
fun setPage(view: OptimizedPdfView, page: Int) {
|
|
37
|
+
view.setPage(page)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@ReactProp(name = "maximumZoom", defaultFloat = 5.0f)
|
|
41
|
+
fun setMaximumZoom(view: OptimizedPdfView, maximumZoom: Float) {
|
|
42
|
+
view.setMaximumZoom(maximumZoom)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@ReactProp(name = "enableAntialiasing", defaultBoolean = true)
|
|
46
|
+
fun setEnableAntialiasing(view: OptimizedPdfView, enableAntialiasing: Boolean) {
|
|
47
|
+
view.setEnableAntialiasing(enableAntialiasing)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@ReactProp(name = "password")
|
|
51
|
+
fun setPassword(view: OptimizedPdfView, password: String?) {
|
|
52
|
+
view.setPassword(password ?: "")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any>? {
|
|
56
|
+
return MapBuilder.builder<String, Any>()
|
|
57
|
+
.put("onLoadComplete", MapBuilder.of("registrationName", "onLoadComplete"))
|
|
58
|
+
.put("onError", MapBuilder.of("registrationName", "onError"))
|
|
59
|
+
.put("onPageCount", MapBuilder.of("registrationName", "onPageCount"))
|
|
60
|
+
.put("onPasswordRequired", MapBuilder.of("registrationName", "onPasswordRequired"))
|
|
61
|
+
.build()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override fun onDropViewInstance(view: OptimizedPdfView) {
|
|
65
|
+
super.onDropViewInstance(view)
|
|
66
|
+
view.cleanup()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package com.reactnativeoptimizedpdf
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* React Native Package para registrar o módulo OptimizedPdf
|
|
10
|
+
*/
|
|
11
|
+
class OptimizedPdfViewPackage : ReactPackage {
|
|
12
|
+
|
|
13
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
14
|
+
return emptyList()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
18
|
+
return listOf(OptimizedPdfViewManager())
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Android Setup for react-native-optimized-pdf
|
|
2
|
+
|
|
3
|
+
## Automatic Linking (React Native 0.60+)
|
|
4
|
+
|
|
5
|
+
The package will be automatically linked when you run your project. No manual linking required.
|
|
6
|
+
|
|
7
|
+
## Manual Setup (if needed)
|
|
8
|
+
|
|
9
|
+
If automatic linking doesn't work, follow these steps:
|
|
10
|
+
|
|
11
|
+
### 1. Add to `settings.gradle`
|
|
12
|
+
|
|
13
|
+
```gradle
|
|
14
|
+
include ':react-native-optimized-pdf'
|
|
15
|
+
project(':react-native-optimized-pdf').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-optimized-pdf/android')
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### 2. Add to `app/build.gradle`
|
|
19
|
+
|
|
20
|
+
```gradle
|
|
21
|
+
dependencies {
|
|
22
|
+
implementation project(':react-native-optimized-pdf')
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 3. Add to `MainApplication.java` or `MainApplication.kt`
|
|
27
|
+
|
|
28
|
+
**Java:**
|
|
29
|
+
|
|
30
|
+
```java
|
|
31
|
+
import com.reactnativeoptimizedpdf.OptimizedPdfPackage;
|
|
32
|
+
|
|
33
|
+
@Override
|
|
34
|
+
protected List<ReactPackage> getPackages() {
|
|
35
|
+
List<ReactPackage> packages = new PackageList(this).getPackages();
|
|
36
|
+
packages.add(new OptimizedPdfPackage());
|
|
37
|
+
return packages;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Kotlin:**
|
|
42
|
+
|
|
43
|
+
```kotlin
|
|
44
|
+
import com.reactnativeoptimizedpdf.OptimizedPdfPackage
|
|
45
|
+
|
|
46
|
+
override fun getPackages(): List<ReactPackage> =
|
|
47
|
+
PackageList(this).packages.apply {
|
|
48
|
+
add(OptimizedPdfPackage())
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Requirements
|
|
53
|
+
|
|
54
|
+
- Android SDK 24+
|
|
55
|
+
- Kotlin 1.9+
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- High-performance PDF rendering using Android's native PdfRenderer
|
|
60
|
+
- Smooth zoom and pan with gesture support
|
|
61
|
+
- Double-tap to zoom/reset
|
|
62
|
+
- Memory-efficient rendering with proper bitmap recycling
|
|
63
|
+
- Antialiasing support for crisp text
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { default } from './src/OptimizedPdfView';
|
|
2
|
+
export { PdfCacheService } from './src/services/pdfCache';
|
|
3
|
+
export { PdfNavigationControls } from './src/components/PdfNavigationControls';
|
|
4
|
+
export { PdfLoadingOverlay, PdfErrorOverlay } from './src/components/PdfOverlays';
|
|
5
|
+
export type {
|
|
6
|
+
PdfSource,
|
|
7
|
+
OptimizedPdfViewProps,
|
|
8
|
+
PdfPageDimensions,
|
|
9
|
+
PdfErrorEvent,
|
|
10
|
+
PdfNavigationControlsProps,
|
|
11
|
+
PdfLoadingOverlayProps,
|
|
12
|
+
PdfErrorOverlayProps,
|
|
13
|
+
NativeLoadCompleteEvent,
|
|
14
|
+
NativePageCountEvent,
|
|
15
|
+
} from './src/types';
|
package/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { default } from './src/OptimizedPdfView';
|
|
2
|
+
export { PdfCacheService } from './src/services/pdfCache';
|
|
3
|
+
export { PdfNavigationControls } from './src/components/PdfNavigationControls';
|
|
4
|
+
export { PdfLoadingOverlay, PdfErrorOverlay } from './src/components/PdfOverlays';
|
|
5
|
+
export type {
|
|
6
|
+
PdfSource,
|
|
7
|
+
OptimizedPdfViewProps,
|
|
8
|
+
PdfPageDimensions,
|
|
9
|
+
PdfErrorEvent,
|
|
10
|
+
PdfNavigationControlsProps,
|
|
11
|
+
PdfLoadingOverlayProps,
|
|
12
|
+
PdfErrorOverlayProps,
|
|
13
|
+
} from './src/types';
|