react-native-pdfrender 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +306 -0
- package/android/build.gradle +76 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/pdfrender/ComposeRenderer.kt +85 -0
- package/android/src/main/java/com/pdfrender/PdfCacheManager.kt +150 -0
- package/android/src/main/java/com/pdfrender/PdfConstants.kt +63 -0
- package/android/src/main/java/com/pdfrender/PdfIconComponents.kt +275 -0
- package/android/src/main/java/com/pdfrender/PdfRenderingLogic.kt +325 -0
- package/android/src/main/java/com/pdfrender/PdfUIComponents.kt +335 -0
- package/android/src/main/java/com/pdfrender/PdfViewPackage.kt +32 -0
- package/android/src/main/java/com/pdfrender/PdfViewerActivity.kt +3467 -0
- package/android/src/main/java/com/pdfrender/PdfViewerFabricManager.kt +244 -0
- package/android/src/main/java/com/pdfrender/PdfViewerFragment.kt +129 -0
- package/android/src/main/java/com/pdfrender/PdfViewerTurboModule.kt +158 -0
- package/android/src/main/java/com/pdfrender/events/FullScreenChangeEvent.kt +26 -0
- package/android/src/main/java/com/pdfrender/events/LeftScreenChangeEvent.kt +22 -0
- package/android/src/main/java/com/pdfrender/events/RightScreenChangeEvent.kt +22 -0
- package/android/src/main/java/com/pdfrender/events/ZoomChangeEvent.kt +22 -0
- package/ios/PdfCacheManager.swift +44 -0
- package/ios/PdfConstants.swift +38 -0
- package/ios/PdfPageView.swift +121 -0
- package/ios/PdfRenderingLogic.swift +107 -0
- package/ios/PdfToolbarView.swift +158 -0
- package/ios/PdfViewerComponentView.mm +194 -0
- package/ios/PdfViewerTurboModule.mm +186 -0
- package/ios/PdfViewerTurboModuleImpl.swift +141 -0
- package/ios/PdfViewerView.swift +268 -0
- package/ios/PdfViewerViewController.swift +109 -0
- package/lib/commonjs/PdfViewerView.js +105 -0
- package/lib/commonjs/PdfViewerView.js.map +1 -0
- package/lib/commonjs/index.js +28 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/NativePdfViewerComponent.js +27 -0
- package/lib/commonjs/specs/NativePdfViewerComponent.js.map +1 -0
- package/lib/commonjs/specs/NativePdfViewerModule.js +21 -0
- package/lib/commonjs/specs/NativePdfViewerModule.js.map +1 -0
- package/lib/commonjs/usePdfViewer.js +65 -0
- package/lib/commonjs/usePdfViewer.js.map +1 -0
- package/lib/module/PdfViewerView.js +99 -0
- package/lib/module/PdfViewerView.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/specs/NativePdfViewerComponent.js +26 -0
- package/lib/module/specs/NativePdfViewerComponent.js.map +1 -0
- package/lib/module/specs/NativePdfViewerModule.js +18 -0
- package/lib/module/specs/NativePdfViewerModule.js.map +1 -0
- package/lib/module/usePdfViewer.js +60 -0
- package/lib/module/usePdfViewer.js.map +1 -0
- package/lib/typescript/PdfViewerView.d.ts +58 -0
- package/lib/typescript/PdfViewerView.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +7 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/specs/NativePdfViewerComponent.d.ts +59 -0
- package/lib/typescript/specs/NativePdfViewerComponent.d.ts.map +1 -0
- package/lib/typescript/specs/NativePdfViewerModule.d.ts +47 -0
- package/lib/typescript/specs/NativePdfViewerModule.d.ts.map +1 -0
- package/lib/typescript/usePdfViewer.d.ts +45 -0
- package/lib/typescript/usePdfViewer.d.ts.map +1 -0
- package/package.json +109 -0
- package/react-native-pdfrender.podspec +35 -0
- package/react-native.config.js +11 -0
- package/src/PdfViewerView.tsx +159 -0
- package/src/index.tsx +10 -0
- package/src/specs/NativePdfViewerComponent.ts +94 -0
- package/src/specs/NativePdfViewerModule.ts +58 -0
- package/src/usePdfViewer.ts +102 -0
|
@@ -0,0 +1,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
|
+
}
|