react-native-pdfrender 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +306 -0
  2. package/android/build.gradle +76 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/java/com/pdfrender/ComposeRenderer.kt +85 -0
  5. package/android/src/main/java/com/pdfrender/PdfCacheManager.kt +150 -0
  6. package/android/src/main/java/com/pdfrender/PdfConstants.kt +63 -0
  7. package/android/src/main/java/com/pdfrender/PdfIconComponents.kt +275 -0
  8. package/android/src/main/java/com/pdfrender/PdfRenderingLogic.kt +325 -0
  9. package/android/src/main/java/com/pdfrender/PdfUIComponents.kt +335 -0
  10. package/android/src/main/java/com/pdfrender/PdfViewPackage.kt +32 -0
  11. package/android/src/main/java/com/pdfrender/PdfViewerActivity.kt +3467 -0
  12. package/android/src/main/java/com/pdfrender/PdfViewerFabricManager.kt +244 -0
  13. package/android/src/main/java/com/pdfrender/PdfViewerFragment.kt +129 -0
  14. package/android/src/main/java/com/pdfrender/PdfViewerTurboModule.kt +158 -0
  15. package/android/src/main/java/com/pdfrender/events/FullScreenChangeEvent.kt +26 -0
  16. package/android/src/main/java/com/pdfrender/events/LeftScreenChangeEvent.kt +22 -0
  17. package/android/src/main/java/com/pdfrender/events/RightScreenChangeEvent.kt +22 -0
  18. package/android/src/main/java/com/pdfrender/events/ZoomChangeEvent.kt +22 -0
  19. package/ios/PdfCacheManager.swift +44 -0
  20. package/ios/PdfConstants.swift +38 -0
  21. package/ios/PdfPageView.swift +121 -0
  22. package/ios/PdfRenderingLogic.swift +107 -0
  23. package/ios/PdfToolbarView.swift +158 -0
  24. package/ios/PdfViewerComponentView.mm +194 -0
  25. package/ios/PdfViewerTurboModule.mm +186 -0
  26. package/ios/PdfViewerTurboModuleImpl.swift +141 -0
  27. package/ios/PdfViewerView.swift +268 -0
  28. package/ios/PdfViewerViewController.swift +109 -0
  29. package/lib/commonjs/PdfViewerView.js +105 -0
  30. package/lib/commonjs/PdfViewerView.js.map +1 -0
  31. package/lib/commonjs/index.js +28 -0
  32. package/lib/commonjs/index.js.map +1 -0
  33. package/lib/commonjs/package.json +1 -0
  34. package/lib/commonjs/specs/NativePdfViewerComponent.js +27 -0
  35. package/lib/commonjs/specs/NativePdfViewerComponent.js.map +1 -0
  36. package/lib/commonjs/specs/NativePdfViewerModule.js +21 -0
  37. package/lib/commonjs/specs/NativePdfViewerModule.js.map +1 -0
  38. package/lib/commonjs/usePdfViewer.js +65 -0
  39. package/lib/commonjs/usePdfViewer.js.map +1 -0
  40. package/lib/module/PdfViewerView.js +99 -0
  41. package/lib/module/PdfViewerView.js.map +1 -0
  42. package/lib/module/index.js +8 -0
  43. package/lib/module/index.js.map +1 -0
  44. package/lib/module/package.json +1 -0
  45. package/lib/module/specs/NativePdfViewerComponent.js +26 -0
  46. package/lib/module/specs/NativePdfViewerComponent.js.map +1 -0
  47. package/lib/module/specs/NativePdfViewerModule.js +18 -0
  48. package/lib/module/specs/NativePdfViewerModule.js.map +1 -0
  49. package/lib/module/usePdfViewer.js +60 -0
  50. package/lib/module/usePdfViewer.js.map +1 -0
  51. package/lib/typescript/PdfViewerView.d.ts +58 -0
  52. package/lib/typescript/PdfViewerView.d.ts.map +1 -0
  53. package/lib/typescript/index.d.ts +7 -0
  54. package/lib/typescript/index.d.ts.map +1 -0
  55. package/lib/typescript/specs/NativePdfViewerComponent.d.ts +59 -0
  56. package/lib/typescript/specs/NativePdfViewerComponent.d.ts.map +1 -0
  57. package/lib/typescript/specs/NativePdfViewerModule.d.ts +47 -0
  58. package/lib/typescript/specs/NativePdfViewerModule.d.ts.map +1 -0
  59. package/lib/typescript/usePdfViewer.d.ts +45 -0
  60. package/lib/typescript/usePdfViewer.d.ts.map +1 -0
  61. package/package.json +109 -0
  62. package/react-native-pdfrender.podspec +35 -0
  63. package/react-native.config.js +11 -0
  64. package/src/PdfViewerView.tsx +159 -0
  65. package/src/index.tsx +10 -0
  66. package/src/specs/NativePdfViewerComponent.ts +94 -0
  67. package/src/specs/NativePdfViewerModule.ts +58 -0
  68. package/src/usePdfViewer.ts +102 -0
@@ -0,0 +1,244 @@
1
+ package com.pdfrender
2
+
3
+ import android.net.Uri
4
+ import androidx.compose.foundation.background
5
+ import androidx.compose.foundation.layout.Box
6
+ import androidx.compose.foundation.layout.fillMaxSize
7
+ import androidx.compose.material.MaterialTheme
8
+ import androidx.compose.ui.Modifier
9
+ import androidx.compose.ui.graphics.Color
10
+ import androidx.compose.ui.platform.ComposeView
11
+ import androidx.compose.ui.platform.LocalDensity
12
+ import androidx.compose.ui.platform.ViewCompositionStrategy
13
+ import androidx.compose.ui.unit.dp
14
+ import com.facebook.react.bridge.ReadableMap
15
+ import com.facebook.react.uimanager.SimpleViewManager
16
+ import com.facebook.react.uimanager.ThemedReactContext
17
+ import com.facebook.react.uimanager.UIManagerHelper
18
+ import com.facebook.react.uimanager.annotations.ReactProp
19
+ import com.pdfrender.events.FullScreenChangeEvent
20
+ import com.pdfrender.events.LeftScreenChangeEvent
21
+ import com.pdfrender.events.RightScreenChangeEvent
22
+ import com.pdfrender.events.ZoomChangeEvent
23
+
24
+ /**
25
+ * PdfViewerFabricManager — New Architecture ViewManager
26
+ *
27
+ * Replaces the old PdfViewerViewManager (which used RCTEventEmitter).
28
+ * Now uses UIManagerHelper.getEventDispatcherForReactTag() — the correct
29
+ * event dispatch path for both Fabric and the interop bridge.
30
+ *
31
+ * Each @ReactProp setter stores the value in the view's tag map so that
32
+ * updateViewContent() can compose a complete snapshot even when props
33
+ * arrive in separate calls.
34
+ */
35
+ class PdfViewerFabricManager : SimpleViewManager<ComposeView>() {
36
+
37
+ override fun getName(): String = VIEW_NAME
38
+
39
+ // ── Tag keys for storing per-view prop state ─────────────────────────────
40
+ // Using string-keyed tags via setTag(key, value) on ComposeView.
41
+ // Each view instance carries its own snapshot — no shared state.
42
+
43
+ override fun createViewInstance(reactContext: ThemedReactContext): ComposeView =
44
+ ComposeView(reactContext).apply {
45
+ clipToOutline = true
46
+ clipToPadding = true
47
+ clipChildren = true
48
+ // Dispose Compose when the view is detached from the window tree.
49
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
50
+ }
51
+
52
+ // ── Prop setters ─────────────────────────────────────────────────────────
53
+
54
+ @ReactProp(name = "pdfUri")
55
+ fun setPdfUri(view: ComposeView, uri: String?) {
56
+ view.setTag(R.id.tag_pdf_uri, uri)
57
+ updateViewContent(view)
58
+ }
59
+
60
+ @ReactProp(name = "initialPageIndex", defaultInt = -1)
61
+ fun setInitialPageIndex(view: ComposeView, index: Int) {
62
+ view.setTag(R.id.tag_page_index, index)
63
+ updateViewContent(view)
64
+ }
65
+
66
+ @ReactProp(name = "maxWidth")
67
+ fun setMaxWidth(view: ComposeView, maxWidth: Float) {
68
+ view.setTag(R.id.tag_max_width, maxWidth)
69
+ updateViewContent(view)
70
+ }
71
+
72
+ @ReactProp(name = "screenWidthPercentage", defaultFloat = 100f)
73
+ fun setScreenWidthPercentage(view: ComposeView, percentage: Float) {
74
+ view.setTag(R.id.tag_screen_pct, percentage)
75
+ updateViewContent(view)
76
+ }
77
+
78
+ @ReactProp(name = "defaultZoom", defaultFloat = 1.5f)
79
+ fun setDefaultZoom(view: ComposeView, zoom: Float) {
80
+ view.setTag(R.id.tag_default_zoom, zoom)
81
+ updateViewContent(view)
82
+ }
83
+
84
+ @ReactProp(name = "isFullScreen", defaultBoolean = false)
85
+ fun setIsFullScreen(view: ComposeView, isFullScreen: Boolean) {
86
+ view.setTag(R.id.tag_fullscreen, isFullScreen)
87
+ updateViewContent(view)
88
+ }
89
+
90
+ @ReactProp(name = "rightLayout", defaultBoolean = false)
91
+ fun setRightLayout(view: ComposeView, rightLayout: Boolean) {
92
+ view.setTag(R.id.tag_right_layout, rightLayout)
93
+ updateViewContent(view)
94
+ }
95
+
96
+ @ReactProp(name = "backButtonText")
97
+ fun setBackButtonText(view: ComposeView, text: String?) {
98
+ view.setTag(R.id.tag_back_text, text)
99
+ updateViewContent(view)
100
+ }
101
+
102
+ @ReactProp(name = "headerText")
103
+ fun setHeaderText(view: ComposeView, text: String?) {
104
+ view.setTag(R.id.tag_header_text, text)
105
+ updateViewContent(view)
106
+ }
107
+
108
+ @ReactProp(name = "iconUrls")
109
+ fun setIconUrls(view: ComposeView, iconUrls: ReadableMap?) {
110
+ view.setTag(R.id.tag_icon_urls, iconUrls)
111
+ updateViewContent(view)
112
+ }
113
+
114
+ @ReactProp(name = "iconStyle")
115
+ fun setIconStyle(view: ComposeView, iconStyle: ReadableMap?) {
116
+ view.setTag(R.id.tag_icon_style, iconStyle)
117
+ updateViewContent(view)
118
+ }
119
+
120
+ // ── Compose content update ───────────────────────────────────────────────
121
+
122
+ private fun updateViewContent(view: ComposeView) {
123
+ val uriString = view.getTag(R.id.tag_pdf_uri) as? String
124
+ if (uriString.isNullOrEmpty()) return
125
+
126
+ val uri = Uri.parse(uriString)
127
+ val pageIndex = (view.getTag(R.id.tag_page_index) as? Int) ?: -1
128
+ val maxWidthPx = view.getTag(R.id.tag_max_width) as? Float
129
+ val screenPct = (view.getTag(R.id.tag_screen_pct) as? Float) ?: 100f
130
+ val defaultZoom = (view.getTag(R.id.tag_default_zoom) as? Float) ?: 1.5f
131
+ val isFullScreen = (view.getTag(R.id.tag_fullscreen) as? Boolean) ?: false
132
+ val rightLayout = (view.getTag(R.id.tag_right_layout) as? Boolean) ?: false
133
+ val backButtonText = view.getTag(R.id.tag_back_text) as? String
134
+ val headerText = view.getTag(R.id.tag_header_text) as? String
135
+ val iconUrlsMap = view.getTag(R.id.tag_icon_urls) as? ReadableMap
136
+ val iconStyleMap = view.getTag(R.id.tag_icon_style) as? ReadableMap
137
+
138
+ val iconUrls: Map<String, String?>? = iconUrlsMap?.let { map ->
139
+ mapOf(
140
+ "zoomIn" to map.takeIf { it.hasKey("zoomIn") }?.getString("zoomIn"),
141
+ "zoomOut" to map.takeIf { it.hasKey("zoomOut") }?.getString("zoomOut"),
142
+ "fullScreen" to map.takeIf { it.hasKey("fullScreen") }?.getString("fullScreen"),
143
+ "minimizeScreen" to map.takeIf { it.hasKey("minimizeScreen") }?.getString("minimizeScreen"),
144
+ "leftLayout" to map.takeIf { it.hasKey("leftLayout") }?.getString("leftLayout"),
145
+ "rightLayout" to map.takeIf { it.hasKey("rightLayout") }?.getString("rightLayout")
146
+ )
147
+ }
148
+
149
+ val iconSizeDp: androidx.compose.ui.unit.Dp = iconStyleMap
150
+ ?.takeIf { it.hasKey("fontSize") }
151
+ ?.let { (it.getDouble("fontSize") * 2.5).dp }
152
+ ?: 40.dp
153
+
154
+ val maxWidthDp: androidx.compose.ui.unit.Dp? = maxWidthPx?.let {
155
+ // Will be converted inside the composable using LocalDensity
156
+ it.dp
157
+ }
158
+
159
+ view.setContent {
160
+ MaterialTheme {
161
+ Box(
162
+ modifier = Modifier
163
+ .fillMaxSize()
164
+ .background(Color.Transparent),
165
+ propagateMinConstraints = false
166
+ ) {
167
+ val density = LocalDensity.current.density
168
+ val resolvedMaxWidthDp = maxWidthPx?.let { (it / density).dp }
169
+
170
+ PdfRendererView(
171
+ uri = uri,
172
+ initialPageIndex = pageIndex,
173
+ maxWidthDp = resolvedMaxWidthDp,
174
+ maxZoom = 2.5f,
175
+ screenWidthPercentage = screenPct,
176
+ iconUrls = iconUrls,
177
+ iconSize = iconSizeDp,
178
+ defaultZoom = defaultZoom,
179
+ isFullScreen = isFullScreen,
180
+ rightLayout = rightLayout,
181
+ backButtonText = backButtonText,
182
+ headerText = headerText,
183
+ onFullScreenChange = { fs -> dispatchFullScreenEvent(view, fs) },
184
+ onLeftScreenChange = { ls -> dispatchLeftScreenEvent(view, ls) },
185
+ onRightScreenChange = { rs -> dispatchRightScreenEvent(view, rs) },
186
+ onZoomChange = { pct -> dispatchZoomEvent(view, pct) }
187
+ )
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ // ── New-arch event dispatch ──────────────────────────────────────────────
194
+ // UIManagerHelper.getEventDispatcherForReactTag replaces the deprecated
195
+ // RCTEventEmitter pattern from the old bridge architecture.
196
+
197
+ private fun dispatchFullScreenEvent(view: ComposeView, isFullScreen: Boolean) {
198
+ val ctx = view.context as? ThemedReactContext ?: return
199
+ UIManagerHelper.getEventDispatcherForReactTag(ctx, view.id)
200
+ ?.dispatchEvent(
201
+ FullScreenChangeEvent(UIManagerHelper.getSurfaceId(view), view.id, isFullScreen)
202
+ )
203
+ }
204
+
205
+ private fun dispatchLeftScreenEvent(view: ComposeView, isLeftScreen: Boolean) {
206
+ val ctx = view.context as? ThemedReactContext ?: return
207
+ UIManagerHelper.getEventDispatcherForReactTag(ctx, view.id)
208
+ ?.dispatchEvent(
209
+ LeftScreenChangeEvent(UIManagerHelper.getSurfaceId(view), view.id, isLeftScreen)
210
+ )
211
+ }
212
+
213
+ private fun dispatchRightScreenEvent(view: ComposeView, isRightScreen: Boolean) {
214
+ val ctx = view.context as? ThemedReactContext ?: return
215
+ UIManagerHelper.getEventDispatcherForReactTag(ctx, view.id)
216
+ ?.dispatchEvent(
217
+ RightScreenChangeEvent(UIManagerHelper.getSurfaceId(view), view.id, isRightScreen)
218
+ )
219
+ }
220
+
221
+ private fun dispatchZoomEvent(view: ComposeView, zoomPercentage: Int) {
222
+ val ctx = view.context as? ThemedReactContext ?: return
223
+ UIManagerHelper.getEventDispatcherForReactTag(ctx, view.id)
224
+ ?.dispatchEvent(
225
+ ZoomChangeEvent(UIManagerHelper.getSurfaceId(view), view.id, zoomPercentage)
226
+ )
227
+ }
228
+
229
+ // ── Event registration map for React Native JS layer ────────────────────
230
+
231
+ override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> = mapOf(
232
+ FullScreenChangeEvent.EVENT_NAME to mapOf("registrationName" to "onFullScreenChange"),
233
+ ZoomChangeEvent.EVENT_NAME to mapOf("registrationName" to "onZoomChange"),
234
+ LeftScreenChangeEvent.EVENT_NAME to mapOf("registrationName" to "onLeftScreenChange"),
235
+ RightScreenChangeEvent.EVENT_NAME to mapOf("registrationName" to "onRightScreenChange")
236
+ )
237
+
238
+ companion object {
239
+ const val VIEW_NAME = "PdfViewerView"
240
+ }
241
+ }
242
+
243
+ private val Number.dp: androidx.compose.ui.unit.Dp
244
+ get() = androidx.compose.ui.unit.Dp(this.toFloat())
@@ -0,0 +1,129 @@
1
+ package com.pdfrender
2
+
3
+ import android.net.Uri
4
+ import android.os.Bundle
5
+ import android.view.LayoutInflater
6
+ import android.view.View
7
+ import android.view.ViewGroup
8
+ import androidx.compose.material.MaterialTheme
9
+ import androidx.compose.ui.platform.ComposeView
10
+ import androidx.compose.ui.platform.ViewCompositionStrategy
11
+ import androidx.fragment.app.Fragment
12
+ import androidx.fragment.app.FragmentActivity
13
+
14
+ /**
15
+ * PdfViewerFragment
16
+ *
17
+ * Activity-independent PDF viewer. Drop it into any FragmentActivity —
18
+ * no PdfViewerActivity is spawned, no Intent required.
19
+ *
20
+ * Usage (from any FragmentActivity or from the TurboModule):
21
+ * PdfViewerFragment.show(activity, "file:///path/to/file.pdf", pageIndex = 0)
22
+ *
23
+ * Dismiss:
24
+ * PdfViewerFragment.dismiss(activity)
25
+ * // or the user presses the system back button (addToBackStack handles it)
26
+ */
27
+ class PdfViewerFragment : Fragment() {
28
+
29
+ // ── Public callback interface ────────────────────────────────────────────
30
+ interface Listener {
31
+ fun onPageChanged(pageIndex: Int, totalPages: Int) = Unit
32
+ fun onFullScreenChanged(isFullScreen: Boolean) = Unit
33
+ fun onZoomChanged(zoomPercentage: Int) = Unit
34
+ fun onDismiss() = Unit
35
+ }
36
+
37
+ var listener: Listener? = null
38
+
39
+ // ── Arguments ────────────────────────────────────────────────────────────
40
+ private val pdfUri: String get() = requireArguments().getString(ARG_URI)!!
41
+ private val initialPage: Int get() = requireArguments().getInt(ARG_PAGE, -1)
42
+ private val defaultZoom: Float get() = requireArguments().getFloat(ARG_ZOOM, 1.5f)
43
+ private val isFullScreen: Boolean get() = requireArguments().getBoolean(ARG_FULLSCREEN, false)
44
+
45
+ // ── Fragment lifecycle ───────────────────────────────────────────────────
46
+ override fun onCreateView(
47
+ inflater: LayoutInflater,
48
+ container: ViewGroup?,
49
+ savedInstanceState: Bundle?
50
+ ): View = ComposeView(requireContext()).apply {
51
+ // Dispose Compose tree when the view lifecycle is destroyed.
52
+ // This covers Fragment being removed AND detached in a ViewPager.
53
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
54
+
55
+ setContent {
56
+ MaterialTheme {
57
+ PdfRendererView(
58
+ uri = Uri.parse(pdfUri),
59
+ initialPageIndex = initialPage,
60
+ defaultZoom = defaultZoom,
61
+ isFullScreen = isFullScreen,
62
+ onFullScreenChange = { fs ->
63
+ listener?.onFullScreenChanged(fs)
64
+ },
65
+ onZoomChange = { pct ->
66
+ listener?.onZoomChanged(pct)
67
+ }
68
+ )
69
+ }
70
+ }
71
+ }
72
+
73
+ // ── Companion (static factory) ───────────────────────────────────────────
74
+ companion object {
75
+ private const val TAG = "PdfViewerFragment"
76
+ private const val ARG_URI = "pdf_uri"
77
+ private const val ARG_PAGE = "initial_page"
78
+ private const val ARG_ZOOM = "default_zoom"
79
+ private const val ARG_FULLSCREEN = "is_full_screen"
80
+
81
+ /**
82
+ * Show the PDF viewer overlaid on top of the host Activity's content.
83
+ * The fragment is added to the back-stack so the system back button
84
+ * dismisses it automatically.
85
+ *
86
+ * @param activity Any FragmentActivity — your own app's Activity.
87
+ * @param uri PDF URI (file:// or content://)
88
+ * @param pageIndex 0-based page to scroll to on open (-1 = first page)
89
+ * @param defaultZoom Initial zoom level (1.0 = 100%, 1.5 = 150%)
90
+ * @param isFullScreen Start in full-screen mode
91
+ * @param listener Optional callback for page/zoom/dismiss events
92
+ */
93
+ fun show(
94
+ activity: FragmentActivity,
95
+ uri: String,
96
+ pageIndex: Int = -1,
97
+ defaultZoom: Float = 1.5f,
98
+ isFullScreen: Boolean = false,
99
+ listener: Listener? = null
100
+ ): PdfViewerFragment {
101
+ val fragment = PdfViewerFragment().also { f ->
102
+ f.listener = listener
103
+ f.arguments = Bundle().apply {
104
+ putString(ARG_URI, uri)
105
+ putInt(ARG_PAGE, pageIndex)
106
+ putFloat(ARG_ZOOM, defaultZoom)
107
+ putBoolean(ARG_FULLSCREEN, isFullScreen)
108
+ }
109
+ }
110
+ activity.supportFragmentManager
111
+ .beginTransaction()
112
+ .add(android.R.id.content, fragment, TAG)
113
+ .addToBackStack(TAG)
114
+ .commit()
115
+ return fragment
116
+ }
117
+
118
+ /**
119
+ * Dismiss the currently visible viewer (if any).
120
+ */
121
+ fun dismiss(activity: FragmentActivity) {
122
+ val fm = activity.supportFragmentManager
123
+ val f = fm.findFragmentByTag(TAG) ?: return
124
+ fm.beginTransaction().remove(f).commit()
125
+ try { fm.popBackStack(TAG, androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE) }
126
+ catch (_: Exception) {}
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,158 @@
1
+ package com.pdfrender
2
+
3
+ import android.graphics.pdf.PdfDocument
4
+ import android.os.Handler
5
+ import android.os.Looper
6
+ import android.util.Base64
7
+ import androidx.fragment.app.FragmentActivity
8
+ import com.facebook.react.bridge.*
9
+ import com.facebook.react.module.annotations.ReactModule
10
+ import java.io.File
11
+ import java.io.FileOutputStream
12
+
13
+ /**
14
+ * PdfViewerTurboModule — New Architecture TurboModule
15
+ *
16
+ * Replaces the old PdfModule (which extended ReactContextBaseJavaModule).
17
+ * Now extends NativePdfViewerModuleSpec — the codegen-generated abstract class
18
+ * that gives compile-time type safety between the JS spec and this implementation.
19
+ *
20
+ * Key changes vs old PdfModule:
21
+ * 1. openPdfInNativeViewer → openPdfViewer: shows PdfViewerFragment inside the
22
+ * host Activity instead of starting a separate PdfViewerActivity.
23
+ * 2. All method signatures match NativePdfViewerModule.ts exactly.
24
+ * 3. @ReactModule annotation enables lazy TurboModule loading.
25
+ */
26
+ @ReactModule(name = PdfViewerTurboModule.NAME)
27
+ class PdfViewerTurboModule(reactContext: ReactApplicationContext) :
28
+ NativePdfViewerModuleSpec(reactContext) {
29
+
30
+ override fun getName(): String = NAME
31
+
32
+ // ── Open PDF in host Activity (Fragment-based, no new Activity) ──────────
33
+ override fun openPdfViewer(uri: String, pageIndex: Double, defaultZoom: Double) {
34
+ val activity = reactApplicationContext.currentActivity as? FragmentActivity
35
+ ?: run {
36
+ // Emit an error event the JS layer can subscribe to
37
+ reactApplicationContext.emitDeviceEvent(
38
+ "PdfViewerError",
39
+ Arguments.createMap().apply {
40
+ putString("error", "Host Activity is not a FragmentActivity — cannot show PdfViewerFragment")
41
+ }
42
+ )
43
+ return
44
+ }
45
+
46
+ Handler(Looper.getMainLooper()).post {
47
+ PdfViewerFragment.show(
48
+ activity = activity,
49
+ uri = uri,
50
+ pageIndex = pageIndex.toInt(),
51
+ defaultZoom = defaultZoom.toFloat(),
52
+ listener = object : PdfViewerFragment.Listener {
53
+ override fun onFullScreenChanged(isFullScreen: Boolean) {
54
+ reactApplicationContext.emitDeviceEvent(
55
+ "PdfFullScreenChanged",
56
+ Arguments.createMap().apply { putBoolean("isFullScreen", isFullScreen) }
57
+ )
58
+ }
59
+ override fun onZoomChanged(zoomPercentage: Int) {
60
+ reactApplicationContext.emitDeviceEvent(
61
+ "PdfZoomChanged",
62
+ Arguments.createMap().apply { putInt("zoomPercentage", zoomPercentage) }
63
+ )
64
+ }
65
+ override fun onDismiss() {
66
+ reactApplicationContext.emitDeviceEvent(
67
+ "PdfViewerDismissed",
68
+ Arguments.createMap()
69
+ )
70
+ }
71
+ }
72
+ )
73
+ }
74
+ }
75
+
76
+ // ── Dismiss the Fragment viewer ──────────────────────────────────────────
77
+ override fun dismissPdfViewer() {
78
+ val activity = reactApplicationContext.currentActivity as? FragmentActivity ?: return
79
+ Handler(Looper.getMainLooper()).post {
80
+ PdfViewerFragment.dismiss(activity)
81
+ }
82
+ }
83
+
84
+ // ── Get page count without rendering ────────────────────────────────────
85
+ override fun getPdfPageCount(uri: String, promise: Promise) {
86
+ Thread {
87
+ try {
88
+ val pfd = reactApplicationContext.contentResolver.openFileDescriptor(
89
+ android.net.Uri.parse(uri), "r"
90
+ )
91
+ if (pfd == null) {
92
+ promise.reject("PDF_ERROR", "Cannot open PDF file: $uri")
93
+ return@Thread
94
+ }
95
+ val renderer = android.graphics.pdf.PdfRenderer(pfd)
96
+ val count = renderer.pageCount
97
+ renderer.close()
98
+ pfd.close()
99
+ promise.resolve(count)
100
+ } catch (e: Exception) {
101
+ promise.reject("PDF_PAGE_COUNT_ERROR", e.message, e)
102
+ }
103
+ }.start()
104
+ }
105
+
106
+ // ── Create multi-page PDF as Base64 ─────────────────────────────────────
107
+ // Preserved from old PdfModule — logic unchanged.
108
+ override fun createMultiPagePdfBase64(
109
+ pages: ReadableArray,
110
+ widthPx: Double,
111
+ heightPx: Double,
112
+ promise: Promise
113
+ ) {
114
+ Thread {
115
+ try {
116
+ val pdfDocument = PdfDocument()
117
+
118
+ for (i in 0 until pages.size()) {
119
+ val pageObj = pages.getMap(i) ?: continue
120
+ val title = pageObj.getString("title") ?: "Untitled"
121
+ val body = pageObj.getString("body") ?: ""
122
+
123
+ val bitmap = renderComposableToBitmap(
124
+ context = reactApplicationContext,
125
+ widthPx = widthPx.toInt(),
126
+ heightPx = heightPx.toInt(),
127
+ title = title,
128
+ body = body
129
+ )
130
+
131
+ val pageInfo = PdfDocument.PageInfo.Builder(bitmap.width, bitmap.height, i + 1).create()
132
+ val page = pdfDocument.startPage(pageInfo)
133
+ page.canvas.drawBitmap(bitmap, 0f, 0f, null)
134
+ pdfDocument.finishPage(page)
135
+ bitmap.recycle()
136
+ }
137
+
138
+ val outFile = File(reactApplicationContext.cacheDir, "pdf_${System.currentTimeMillis()}.pdf")
139
+ FileOutputStream(outFile).use { fos ->
140
+ pdfDocument.writeTo(fos)
141
+ }
142
+ pdfDocument.close()
143
+
144
+ val base64 = Base64.encodeToString(outFile.readBytes(), Base64.DEFAULT)
145
+ promise.resolve(Arguments.createMap().apply {
146
+ putString("base64", base64)
147
+ putString("path", outFile.absolutePath)
148
+ })
149
+ } catch (e: Exception) {
150
+ promise.reject("PDF_CREATE_ERROR", e.message, e)
151
+ }
152
+ }.start()
153
+ }
154
+
155
+ companion object {
156
+ const val NAME = "NativePdfViewerModule"
157
+ }
158
+ }
@@ -0,0 +1,26 @@
1
+ package com.pdfrender.events
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+
7
+ /**
8
+ * Fabric event emitted when the PDF viewer enters or exits full-screen mode.
9
+ * Dispatched via UIManagerHelper.getEventDispatcherForReactTag() — the new-arch way.
10
+ */
11
+ class FullScreenChangeEvent(
12
+ surfaceId: Int,
13
+ viewTag: Int,
14
+ private val isFullScreen: Boolean
15
+ ) : Event<FullScreenChangeEvent>(surfaceId, viewTag) {
16
+
17
+ override fun getEventName(): String = EVENT_NAME
18
+
19
+ override fun getEventData(): WritableMap = Arguments.createMap().apply {
20
+ putBoolean("isFullScreen", isFullScreen)
21
+ }
22
+
23
+ companion object {
24
+ const val EVENT_NAME = "topFullScreenChange"
25
+ }
26
+ }
@@ -0,0 +1,22 @@
1
+ package com.pdfrender.events
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+
7
+ class LeftScreenChangeEvent(
8
+ surfaceId: Int,
9
+ viewTag: Int,
10
+ private val isLeftScreen: Boolean
11
+ ) : Event<LeftScreenChangeEvent>(surfaceId, viewTag) {
12
+
13
+ override fun getEventName(): String = EVENT_NAME
14
+
15
+ override fun getEventData(): WritableMap = Arguments.createMap().apply {
16
+ putBoolean("isLeftScreen", isLeftScreen)
17
+ }
18
+
19
+ companion object {
20
+ const val EVENT_NAME = "topLeftScreenChange"
21
+ }
22
+ }
@@ -0,0 +1,22 @@
1
+ package com.pdfrender.events
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+
7
+ class RightScreenChangeEvent(
8
+ surfaceId: Int,
9
+ viewTag: Int,
10
+ private val isRightScreen: Boolean
11
+ ) : Event<RightScreenChangeEvent>(surfaceId, viewTag) {
12
+
13
+ override fun getEventName(): String = EVENT_NAME
14
+
15
+ override fun getEventData(): WritableMap = Arguments.createMap().apply {
16
+ putBoolean("isRightScreen", isRightScreen)
17
+ }
18
+
19
+ companion object {
20
+ const val EVENT_NAME = "topRightScreenChange"
21
+ }
22
+ }
@@ -0,0 +1,22 @@
1
+ package com.pdfrender.events
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+
7
+ class ZoomChangeEvent(
8
+ surfaceId: Int,
9
+ viewTag: Int,
10
+ private val zoomPercentage: Int
11
+ ) : Event<ZoomChangeEvent>(surfaceId, viewTag) {
12
+
13
+ override fun getEventName(): String = EVENT_NAME
14
+
15
+ override fun getEventData(): WritableMap = Arguments.createMap().apply {
16
+ putInt("zoomPercentage", zoomPercentage)
17
+ }
18
+
19
+ companion object {
20
+ const val EVENT_NAME = "topZoomChange"
21
+ }
22
+ }
@@ -0,0 +1,44 @@
1
+ import UIKit
2
+
3
+ /// Thread-safe LRU page-image cache backed by NSCache.
4
+ ///
5
+ /// NSCache is already thread-safe and evicts entries automatically under
6
+ /// memory pressure — no manual locking is required.
7
+ final class PdfCacheManager {
8
+
9
+ static let shared = PdfCacheManager()
10
+
11
+ private let cache = NSCache<NSString, UIImage>()
12
+
13
+ private init() {
14
+ cache.countLimit = PdfConstants.cacheCountLimit
15
+ cache.totalCostLimit = PdfConstants.cacheTotalCostLimit
16
+ }
17
+
18
+ // MARK: - Access
19
+
20
+ func image(forKey key: String) -> UIImage? {
21
+ cache.object(forKey: key as NSString)
22
+ }
23
+
24
+ /// Cost is approximated as width × height × 4 bytes (RGBA).
25
+ func setImage(_ image: UIImage, forKey key: String) {
26
+ let cost = Int(image.size.width * image.size.height * 4)
27
+ cache.setObject(image, forKey: key as NSString, cost: cost)
28
+ }
29
+
30
+ func remove(forKey key: String) {
31
+ cache.removeObject(forKey: key as NSString)
32
+ }
33
+
34
+ func removeAll() {
35
+ cache.removeAllObjects()
36
+ }
37
+
38
+ // MARK: - Cache key helpers
39
+
40
+ /// Stable cache key: "<pageIndex>_<Int(targetWidth)>"
41
+ static func key(pageIndex: Int, targetWidth: CGFloat) -> String {
42
+ "page_\(pageIndex)_\(Int(targetWidth))"
43
+ }
44
+ }