@kishannareshpal/expo-pdf 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/.editorconfig +12 -0
- package/.eslintrc.js +2 -0
- package/.node-version +1 -0
- package/.prettierignore +31 -0
- package/.prettierrc +8 -0
- package/CHANGELOG.md +14 -0
- package/CONTRIBUTING.md +271 -0
- package/LICENSE +21 -0
- package/README.md +243 -0
- package/android/build.gradle +55 -0
- package/android/src/main/AndroidManifest.xml +9 -0
- package/android/src/main/java/com/kishannareshpal/expopdf/.editorconfig +12 -0
- package/android/src/main/java/com/kishannareshpal/expopdf/KJExpoPdfModule.kt +60 -0
- package/android/src/main/java/com/kishannareshpal/expopdf/KJExpoPdfView.kt +222 -0
- package/android/src/main/java/com/kishannareshpal/expopdf/lib/ContentPadding.kt +23 -0
- package/android/src/main/java/com/kishannareshpal/expopdf/lib/FitMode.kt +18 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/build/pdf-module.d.ts +7 -0
- package/build/pdf-module.d.ts.map +1 -0
- package/build/pdf-module.js +4 -0
- package/build/pdf-module.js.map +1 -0
- package/build/pdf-view.d.ts +25 -0
- package/build/pdf-view.d.ts.map +1 -0
- package/build/pdf-view.js +14 -0
- package/build/pdf-view.js.map +1 -0
- package/build/types.d.ts +20 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils.d.ts +3 -0
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +10 -0
- package/build/utils.js.map +1 -0
- package/bun.lock +2278 -0
- package/eslint.config.js +5 -0
- package/expo-module.config.json +16 -0
- package/ios/KJExpoPdf.podspec +29 -0
- package/ios/KJExpoPdfModule.swift +51 -0
- package/ios/KJExpoPdfView.swift +242 -0
- package/ios/extensions/PdfViewExtensions.swift +94 -0
- package/ios/lib/ContentPadding.swift +26 -0
- package/ios/lib/FitMode.swift +14 -0
- package/package.json +60 -0
- package/src/index.ts +4 -0
- package/src/pdf-module.ts +8 -0
- package/src/pdf-view.tsx +68 -0
- package/src/types.ts +16 -0
- package/src/utils.ts +12 -0
- package/tsconfig.json +9 -0
package/eslint.config.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'KJExpoPdf'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platforms = {
|
|
14
|
+
:ios => '15.1',
|
|
15
|
+
:tvos => '15.1'
|
|
16
|
+
}
|
|
17
|
+
s.swift_version = '5.9'
|
|
18
|
+
s.source = { git: 'https://github.com/kishannareshpal/expo-pdf' }
|
|
19
|
+
s.static_framework = true
|
|
20
|
+
|
|
21
|
+
s.dependency 'ExpoModulesCore'
|
|
22
|
+
|
|
23
|
+
# Swift/Objective-C compatibility
|
|
24
|
+
s.pod_target_xcconfig = {
|
|
25
|
+
'DEFINES_MODULE' => 'YES',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
29
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
public class KJExpoPdfModule: Module {
|
|
4
|
+
// Each module class must implement the definition function. The definition consists of components
|
|
5
|
+
// that describes the module's functionality and behavior.
|
|
6
|
+
// See https://docs.expo.dev/modules/module-api for more details about available components.
|
|
7
|
+
public func definition() -> ModuleDefinition {
|
|
8
|
+
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
|
|
9
|
+
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
|
|
10
|
+
// The module will be accessible from `requireNativeModule('ExpoPdf')` in JavaScript.
|
|
11
|
+
Name("KJExpoPdf")
|
|
12
|
+
|
|
13
|
+
// Enables the module to be used as a native view. Definition components that are accepted as part of the
|
|
14
|
+
// view definition: Prop, Events.
|
|
15
|
+
View(KJExpoPdfView.self) {
|
|
16
|
+
Events("onLoadComplete", "onPageChanged", "onError")
|
|
17
|
+
|
|
18
|
+
Prop("uri") { (view: KJExpoPdfView, uri: String) in
|
|
19
|
+
view.setUri(uri)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Prop("password") { (view: KJExpoPdfView, password: String?) in
|
|
23
|
+
view.setPassword(password)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Prop("pagingEnabled") { (view: KJExpoPdfView, enabled: Bool?) in
|
|
27
|
+
view.setPagingEnabled(enabled)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Prop("disableDoubleTapToZoom") { (view: KJExpoPdfView, disabled: Bool?) in
|
|
31
|
+
view.setDoubleTapZoomEnabled(disabled != true)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Prop("horizontal") { (view: KJExpoPdfView, enabled: Bool?) in
|
|
35
|
+
view.setHorizontalModeEnabled(enabled)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Prop("pageGap") { (view: KJExpoPdfView, gapPx: Int?) in
|
|
39
|
+
view.setPageGap(gapPx)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Prop("contentPadding") { (view: KJExpoPdfView, contentPadding: ContentPadding?) in
|
|
43
|
+
view.setContentPadding(contentPadding?.toEdgeInset())
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Prop("fitMode") { (view: KJExpoPdfView, fitMode: FitMode?) in
|
|
47
|
+
view.setFitMode(fitMode)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import PDFKit
|
|
3
|
+
import SwiftUI
|
|
4
|
+
|
|
5
|
+
class KJExpoPdfView: ExpoView {
|
|
6
|
+
// MARK: - Defaults
|
|
7
|
+
static let DEFAULT_PAGING_ENABLED = false
|
|
8
|
+
static let DEFAULT_DOUBLE_TAP_ZOOM_ENABLED = true
|
|
9
|
+
static let DEFAULT_HORIZONTAL_MODE_ENABLED = false
|
|
10
|
+
static let DEFAULT_PAGE_GAP = 0
|
|
11
|
+
static let DEFAULT_CONTENT_PADDING = UIEdgeInsets.zero
|
|
12
|
+
static let DEFAULT_FIT_MODE = FitMode.both
|
|
13
|
+
|
|
14
|
+
let onLoadComplete = EventDispatcher()
|
|
15
|
+
let onPageChanged = EventDispatcher()
|
|
16
|
+
let onError = EventDispatcher()
|
|
17
|
+
|
|
18
|
+
enum ErrorCode: String, Codable {
|
|
19
|
+
case invalidUri = "invalid_uri"
|
|
20
|
+
case invalidDocument = "invalid_document"
|
|
21
|
+
case passwordRequired = "password_required"
|
|
22
|
+
case passwordIncorrect = "password_incorrect"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private let pdfView = PDFView()
|
|
26
|
+
|
|
27
|
+
private var documentURL: URL? = nil
|
|
28
|
+
private var password: String? = nil
|
|
29
|
+
private var isPagingEnabled: Bool = DEFAULT_PAGING_ENABLED
|
|
30
|
+
private var isDoubleTapZoomEnabled: Bool = DEFAULT_DOUBLE_TAP_ZOOM_ENABLED
|
|
31
|
+
private var isHorizontalModeEnabled: Bool = DEFAULT_HORIZONTAL_MODE_ENABLED
|
|
32
|
+
private var pageGap: Int = DEFAULT_PAGE_GAP
|
|
33
|
+
private var contentPadding: UIEdgeInsets = DEFAULT_CONTENT_PADDING
|
|
34
|
+
private var fitMode: FitMode = DEFAULT_FIT_MODE
|
|
35
|
+
|
|
36
|
+
required init(appContext: AppContext? = nil) {
|
|
37
|
+
super.init(appContext: appContext)
|
|
38
|
+
|
|
39
|
+
clipsToBounds = true
|
|
40
|
+
|
|
41
|
+
// Set the primitive PdfView's content background to transparent so that it inherits
|
|
42
|
+
// the color from the React Native view (ExpoView), as defined by the
|
|
43
|
+
// style prop in the component (`style={{ backgroundColor: '#eee' }}`).
|
|
44
|
+
self.pdfView.backgroundColor = .clear
|
|
45
|
+
|
|
46
|
+
// We calculate the scaling manually via PDFView.scaleToFit(contentPadding:)
|
|
47
|
+
self.pdfView.autoScales = false
|
|
48
|
+
|
|
49
|
+
addSubview(pdfView)
|
|
50
|
+
|
|
51
|
+
setupListeners()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override func layoutSubviews() {
|
|
55
|
+
super.layoutSubviews()
|
|
56
|
+
pdfView.frame = bounds
|
|
57
|
+
|
|
58
|
+
// Maintain insets on rotation, but don't reset reading position
|
|
59
|
+
self.pdfView.scaleToFit(
|
|
60
|
+
contentPadding: self.contentPadding, fitMode: self.fitMode, resetOffset: false)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
deinit {
|
|
64
|
+
NotificationCenter.default.removeObserver(self)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func setUri(_ uri: String) {
|
|
68
|
+
guard let parsedURL = URL(string: uri) else {
|
|
69
|
+
reportError(.invalidUri, "Invalid URI provided: \(uri)")
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
self.documentURL = parsedURL
|
|
74
|
+
self.reloadPdf()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func setPassword(_ password: String?) {
|
|
78
|
+
self.password = password
|
|
79
|
+
|
|
80
|
+
// Reload the PDF as it needs to perform the unlock attempt
|
|
81
|
+
// if password has been set, or lock if password's been removed
|
|
82
|
+
if self.pdfView.document?.isLocked == true {
|
|
83
|
+
self.reloadPdf()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func setPagingEnabled(_ enabled: Bool?) {
|
|
88
|
+
self.isPagingEnabled = enabled ?? Self.DEFAULT_PAGING_ENABLED
|
|
89
|
+
self.pdfView.displayMode = self.isPagingEnabled ? .singlePage : .singlePageContinuous
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func setDoubleTapZoomEnabled(_ enabled: Bool?) {
|
|
93
|
+
self.isDoubleTapZoomEnabled = enabled ?? Self.DEFAULT_DOUBLE_TAP_ZOOM_ENABLED
|
|
94
|
+
self.pdfView.toggleDoubleTapToZoom(self.isDoubleTapZoomEnabled)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func setHorizontalModeEnabled(_ enabled: Bool?) {
|
|
98
|
+
self.isHorizontalModeEnabled = enabled ?? Self.DEFAULT_HORIZONTAL_MODE_ENABLED
|
|
99
|
+
self.pdfView.displayDirection = self.isHorizontalModeEnabled ? .horizontal : .vertical
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func setPageGap(_ gap: Int?) {
|
|
103
|
+
self.pageGap = gap ?? Self.DEFAULT_PAGE_GAP
|
|
104
|
+
self.pdfView.pageBreakMargins = UIEdgeInsets(
|
|
105
|
+
top: 0,
|
|
106
|
+
left: 0,
|
|
107
|
+
bottom: isHorizontalModeEnabled ? 0 : CGFloat(pageGap),
|
|
108
|
+
right: isHorizontalModeEnabled ? CGFloat(pageGap) : 0
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// PDFView pageBreakMargins not only apply insets between the pages, but also around the pages
|
|
112
|
+
// which is not what we always want - expo-pdf only uses pageBreakMargins for inter page spacing
|
|
113
|
+
// and we use our contentPadding for the spacing around the document.
|
|
114
|
+
// - Subtract the PDFView pageBreakMargins to prevent double spacing
|
|
115
|
+
var padding = self.contentPadding
|
|
116
|
+
let margins = self.pdfView.pageBreakMargins
|
|
117
|
+
|
|
118
|
+
padding = UIEdgeInsets(
|
|
119
|
+
top: padding.top - margins.top,
|
|
120
|
+
left: padding.left - margins.left,
|
|
121
|
+
bottom: padding.bottom - margins.bottom,
|
|
122
|
+
right: padding.right - margins.right
|
|
123
|
+
)
|
|
124
|
+
self.pdfView.scaleToFit(
|
|
125
|
+
contentPadding: self.contentPadding, fitMode: self.fitMode, resetOffset: false)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
func setContentPadding(_ padding: UIEdgeInsets?) {
|
|
129
|
+
self.contentPadding = padding ?? Self.DEFAULT_CONTENT_PADDING
|
|
130
|
+
|
|
131
|
+
// PDFView pageBreakMargins not only apply insets between the pages, but also around the pages
|
|
132
|
+
// which is not what we always want - expo-pdf only uses pageBreakMargins for inter page spacing
|
|
133
|
+
// and we use our contentPadding for the spacing around the document.
|
|
134
|
+
// - Subtract the PDFView pageBreakMargins to prevent double spacing
|
|
135
|
+
var padding = self.contentPadding
|
|
136
|
+
let margins = self.pdfView.pageBreakMargins
|
|
137
|
+
|
|
138
|
+
padding = UIEdgeInsets(
|
|
139
|
+
top: padding.top - margins.top,
|
|
140
|
+
left: padding.left - margins.left,
|
|
141
|
+
bottom: padding.bottom - margins.bottom,
|
|
142
|
+
right: padding.right - margins.right
|
|
143
|
+
)
|
|
144
|
+
self.pdfView.scaleToFit(
|
|
145
|
+
contentPadding: self.contentPadding, fitMode: self.fitMode, resetOffset: false)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func setFitMode(_ mode: FitMode?) {
|
|
149
|
+
self.fitMode = mode ?? Self.DEFAULT_FIT_MODE
|
|
150
|
+
|
|
151
|
+
self.pdfView.scaleToFit(
|
|
152
|
+
contentPadding: self.contentPadding, fitMode: self.fitMode, resetOffset: false)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@objc private func handlePageChange() {
|
|
156
|
+
guard
|
|
157
|
+
let page = pdfView.currentPage,
|
|
158
|
+
let document = pdfView.document
|
|
159
|
+
else { return }
|
|
160
|
+
|
|
161
|
+
onPageChanged([
|
|
162
|
+
"pageIndex": document.index(for: page),
|
|
163
|
+
"pageCount": document.pageCount,
|
|
164
|
+
])
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private func reloadPdf() {
|
|
168
|
+
guard let document = self.loadDocument() else {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if document.isLocked {
|
|
173
|
+
if let password = self.password {
|
|
174
|
+
let unlocked = document.unlock(withPassword: password)
|
|
175
|
+
if !unlocked {
|
|
176
|
+
reportError(
|
|
177
|
+
.passwordIncorrect,
|
|
178
|
+
"The provided password was incorrect"
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
reportError(
|
|
183
|
+
.passwordRequired,
|
|
184
|
+
"PDF requires a password, but no password was provided"
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
self.pdfView.document = document
|
|
190
|
+
|
|
191
|
+
// Dispatch async to allow PDFView to finish its initial layout
|
|
192
|
+
DispatchQueue.main.async {
|
|
193
|
+
self.pdfView.scaleToFit(
|
|
194
|
+
contentPadding: self.contentPadding, fitMode: self.fitMode, resetOffset: true)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
self.onLoadComplete([
|
|
198
|
+
"pageCount": document.pageCount
|
|
199
|
+
])
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private func loadDocument() -> PDFDocument? {
|
|
203
|
+
guard let documentURL else {
|
|
204
|
+
return nil
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// TODO: Apple PDFView supports "http", "https" - but I've not implemented it on Android so for consistency I'm explicitly not allowing it here until then.
|
|
208
|
+
guard ["file"].contains(self.documentURL?.scheme?.lowercased()) else {
|
|
209
|
+
reportError(
|
|
210
|
+
.invalidUri,
|
|
211
|
+
"URL scheme '\(self.documentURL?.scheme ?? "unknown")' is not supported"
|
|
212
|
+
)
|
|
213
|
+
return nil
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
guard let document: PDFDocument = PDFDocument(url: documentURL) else {
|
|
217
|
+
self.reportError(
|
|
218
|
+
.invalidDocument,
|
|
219
|
+
"Failed to load the PDF document"
|
|
220
|
+
)
|
|
221
|
+
return nil
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return document
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private func setupListeners() {
|
|
228
|
+
NotificationCenter.default.addObserver(
|
|
229
|
+
self,
|
|
230
|
+
selector: #selector(handlePageChange),
|
|
231
|
+
name: .PDFViewPageChanged,
|
|
232
|
+
object: pdfView
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private func reportError(_ code: ErrorCode, _ message: String) {
|
|
237
|
+
onError([
|
|
238
|
+
"code": code.rawValue,
|
|
239
|
+
"message": message,
|
|
240
|
+
])
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//
|
|
2
|
+
// PdfViewExtensions.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kishan Jadav on 05/01/2026.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import PDFKit
|
|
9
|
+
|
|
10
|
+
extension PDFView {
|
|
11
|
+
func scaleToFit(contentPadding: UIEdgeInsets, fitMode: FitMode, resetOffset: Bool = false) {
|
|
12
|
+
guard let page = self.currentPage else {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let viewSize = self.bounds.size
|
|
17
|
+
let pageSize = page.bounds(for: self.displayBox).size
|
|
18
|
+
|
|
19
|
+
// Ensure we have valid dimensions to avoid division by zero
|
|
20
|
+
guard viewSize.width > 0, pageSize.width > 0, pageSize.height > 0 else {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Calculate the available space (View size minus Padding)
|
|
25
|
+
let availableWidth = viewSize.width - contentPadding.left - contentPadding.right
|
|
26
|
+
let availableHeight = viewSize.height - contentPadding.top - contentPadding.bottom
|
|
27
|
+
|
|
28
|
+
// Calculate potential scale factors
|
|
29
|
+
let widthScale = availableWidth / pageSize.width
|
|
30
|
+
let heightScale = availableHeight / pageSize.height
|
|
31
|
+
|
|
32
|
+
// Determine the target scale based on the requested FitMode
|
|
33
|
+
let targetScale: CGFloat
|
|
34
|
+
switch fitMode {
|
|
35
|
+
case .width:
|
|
36
|
+
targetScale = widthScale
|
|
37
|
+
case .height:
|
|
38
|
+
targetScale = heightScale
|
|
39
|
+
case .both:
|
|
40
|
+
// "Aspect Fit": Choose the smaller scale to ensure the whole page is visible
|
|
41
|
+
targetScale = min(widthScale, heightScale)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Apply new scale factor
|
|
45
|
+
if abs(self.scaleFactor - targetScale) > 0.001 {
|
|
46
|
+
self.minScaleFactor = targetScale // Prevent zooming out further than the fit
|
|
47
|
+
self.scaleFactor = targetScale
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
self.applyContentPadding(contentPadding, resetOffset: resetOffset)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func applyContentPadding(_ contentPadding: UIEdgeInsets, resetOffset: Bool = false) {
|
|
54
|
+
// Iterate through the PDFView's subviews to find the scroll view
|
|
55
|
+
if let scrollView = self.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView {
|
|
56
|
+
scrollView.contentInset = contentPadding
|
|
57
|
+
|
|
58
|
+
if resetOffset {
|
|
59
|
+
var offset = scrollView.contentOffset
|
|
60
|
+
if self.displayDirection == .horizontal {
|
|
61
|
+
offset.x = -contentPadding.left
|
|
62
|
+
} else {
|
|
63
|
+
offset.y = -contentPadding.top
|
|
64
|
+
}
|
|
65
|
+
scrollView.contentOffset = offset
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func toggleDoubleTapToZoom(_ enabled: Bool) {
|
|
71
|
+
// Iterate through the PDFView's subviews to find the scroll view
|
|
72
|
+
for subview in self.subviews {
|
|
73
|
+
if let gestureRecognizers = subview.gestureRecognizers {
|
|
74
|
+
for gesture in gestureRecognizers {
|
|
75
|
+
if let tapGesture = gesture as? UITapGestureRecognizer, tapGesture.numberOfTapsRequired == 2 {
|
|
76
|
+
// Disable the double-tap recognizer
|
|
77
|
+
tapGesture.isEnabled = enabled
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Sometimes the gesture is deeper, so we check sub-subviews (like the document view)
|
|
83
|
+
for internalSubview in subview.subviews {
|
|
84
|
+
if let gestureRecognizers = internalSubview.gestureRecognizers {
|
|
85
|
+
for gesture in gestureRecognizers {
|
|
86
|
+
if let tapGesture = gesture as? UITapGestureRecognizer, tapGesture.numberOfTapsRequired == 2 {
|
|
87
|
+
tapGesture.isEnabled = enabled
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//
|
|
2
|
+
// ContentPadding.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kishan Jadav on 05/01/2026.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import ExpoModulesCore
|
|
9
|
+
|
|
10
|
+
struct ContentPadding: Record {
|
|
11
|
+
@Field
|
|
12
|
+
var left: Int = 0
|
|
13
|
+
|
|
14
|
+
@Field
|
|
15
|
+
var top: Int = 0
|
|
16
|
+
|
|
17
|
+
@Field
|
|
18
|
+
var right: Int = 0
|
|
19
|
+
|
|
20
|
+
@Field
|
|
21
|
+
var bottom: Int = 0
|
|
22
|
+
|
|
23
|
+
func toEdgeInset() -> UIEdgeInsets {
|
|
24
|
+
UIEdgeInsets(top: CGFloat(self.top), left: CGFloat(self.left), bottom: CGFloat(self.bottom), right: CGFloat(self.right))
|
|
25
|
+
}
|
|
26
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kishannareshpal/expo-pdf",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A cross-platform, performant PDF viewer component for React Native and Expo",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "expo-module build",
|
|
9
|
+
"clean": "expo-module clean",
|
|
10
|
+
"lint": "expo-module lint",
|
|
11
|
+
"test": "expo-module test",
|
|
12
|
+
"prepublishOnly": "expo-module prepublishOnly",
|
|
13
|
+
"expo-module": "expo-module",
|
|
14
|
+
"open:ios": "xed example/ios",
|
|
15
|
+
"open:android": "open -a \"Android Studio\" example/android",
|
|
16
|
+
"format": "prettier --write .",
|
|
17
|
+
"format:check": "prettier --check .",
|
|
18
|
+
"prepare": "husky; expo-module prepare"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"react-native",
|
|
22
|
+
"expo",
|
|
23
|
+
"@kishannareshpal/expo-pdf",
|
|
24
|
+
"rn-pdf",
|
|
25
|
+
"react-native-pdf",
|
|
26
|
+
"expo-pdf",
|
|
27
|
+
"pdf",
|
|
28
|
+
"expopdf"
|
|
29
|
+
],
|
|
30
|
+
"repository": "https://github.com/kishannareshpal/expo-pdf",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/kishannareshpal/expo-pdf/issues"
|
|
33
|
+
},
|
|
34
|
+
"author": "Kishan Jadav <kishan_jadav@hotmail.com> (https://github.com/kishannareshpal)",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"homepage": "https://github.com/kishannareshpal/expo-pdf#readme",
|
|
37
|
+
"dependencies": {},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/react": "~19.1.0",
|
|
40
|
+
"expo": "^54.0.27",
|
|
41
|
+
"expo-module-scripts": "^5.0.8",
|
|
42
|
+
"husky": "^9.1.7",
|
|
43
|
+
"lint-staged": "^16.2.7",
|
|
44
|
+
"prettier": "^3.7.4",
|
|
45
|
+
"react-native": "0.81.5"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"expo": "*",
|
|
49
|
+
"react": "*",
|
|
50
|
+
"react-native": "*"
|
|
51
|
+
},
|
|
52
|
+
"lint-staged": {
|
|
53
|
+
"*.{js,jsx,ts,tsx,json,css,md,yml,yaml}": [
|
|
54
|
+
"prettier --write"
|
|
55
|
+
],
|
|
56
|
+
"*.{java,kt,swift,h,m,mm}": [
|
|
57
|
+
"prettier --write"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { NativeModule, requireNativeModule } from 'expo';
|
|
2
|
+
|
|
3
|
+
type PdfModuleEvents = {}
|
|
4
|
+
|
|
5
|
+
declare class PdfModule extends NativeModule<PdfModuleEvents> { }
|
|
6
|
+
|
|
7
|
+
// This call loads the native module object from the JSI.
|
|
8
|
+
export default requireNativeModule<PdfModule>('KJExpoPdf');
|
package/src/pdf-view.tsx
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { requireNativeView } from 'expo';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
import { ContentPadding, FitMode, OnErrorEventPayload, OnLoadCompleteEventPayload, OnPageChangedEventPayload } from './types';
|
|
5
|
+
import { NativeSyntheticEvent, StyleProp, StyleSheet, ViewStyle } from 'react-native';
|
|
6
|
+
import { forwardNativeEventTo } from './utils';
|
|
7
|
+
|
|
8
|
+
type BaseProps = {
|
|
9
|
+
style?: StyleProp<ViewStyle>;
|
|
10
|
+
/**
|
|
11
|
+
* The file URI. Accepts a remote resource (e.g. via HTTPs) or a local file path (e.g. file:///)
|
|
12
|
+
*/
|
|
13
|
+
uri: string;
|
|
14
|
+
password?: string;
|
|
15
|
+
pagingEnabled?: boolean
|
|
16
|
+
disableDoubleTapToZoom?: boolean
|
|
17
|
+
horizontal?: boolean
|
|
18
|
+
pageGap?: number
|
|
19
|
+
contentPadding?: ContentPadding
|
|
20
|
+
fitMode?: FitMode
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type NativePdfViewProps = BaseProps & {
|
|
24
|
+
onLoadComplete?: (event: NativeSyntheticEvent<OnLoadCompleteEventPayload>) => void;
|
|
25
|
+
onPageChanged?: (event: NativeSyntheticEvent<OnPageChangedEventPayload>) => void;
|
|
26
|
+
onError?: (event: NativeSyntheticEvent<OnErrorEventPayload>) => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const NativePdfView: React.ComponentType<NativePdfViewProps> = requireNativeView('KJExpoPdf');
|
|
30
|
+
|
|
31
|
+
// -----------
|
|
32
|
+
|
|
33
|
+
export type PdfViewProps = BaseProps & {
|
|
34
|
+
onLoadComplete?: (params: OnLoadCompleteEventPayload) => void,
|
|
35
|
+
onPageChanged?: (params: OnPageChangedEventPayload) => void,
|
|
36
|
+
onError?: (params: OnErrorEventPayload) => void
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const PdfView = ({
|
|
40
|
+
style,
|
|
41
|
+
onLoadComplete,
|
|
42
|
+
onError,
|
|
43
|
+
onPageChanged,
|
|
44
|
+
...props
|
|
45
|
+
}: PdfViewProps) => {
|
|
46
|
+
return (
|
|
47
|
+
<NativePdfView
|
|
48
|
+
style={[styles.container, style]}
|
|
49
|
+
uri={props.uri}
|
|
50
|
+
disableDoubleTapToZoom={props.disableDoubleTapToZoom}
|
|
51
|
+
horizontal={props.horizontal}
|
|
52
|
+
pageGap={props.pageGap}
|
|
53
|
+
pagingEnabled={props.pagingEnabled}
|
|
54
|
+
password={props.password}
|
|
55
|
+
contentPadding={props.contentPadding}
|
|
56
|
+
fitMode={props.fitMode}
|
|
57
|
+
onLoadComplete={forwardNativeEventTo(onLoadComplete)}
|
|
58
|
+
onPageChanged={forwardNativeEventTo(onPageChanged)}
|
|
59
|
+
onError={forwardNativeEventTo(onError)}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const styles = StyleSheet.create({
|
|
65
|
+
container: {
|
|
66
|
+
backgroundColor: '#eeeeee'
|
|
67
|
+
}
|
|
68
|
+
})
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type OnLoadCompleteEventPayload = { pageCount: number }
|
|
2
|
+
|
|
3
|
+
export type OnPageChangedEventPayload = { pageIndex: number, pageCount: number }
|
|
4
|
+
|
|
5
|
+
export type ErrorCode = 'no_url' | 'invalid_url' | 'invalid_document'
|
|
6
|
+
|
|
7
|
+
export type OnErrorEventPayload = { code: ErrorCode, message: string }
|
|
8
|
+
|
|
9
|
+
export type ContentPadding = {
|
|
10
|
+
top?: number;
|
|
11
|
+
right?: number;
|
|
12
|
+
bottom?: number;
|
|
13
|
+
left?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type FitMode = 'width' | 'height' | 'both';
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NativeSyntheticEvent } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export const forwardNativeEventTo = <T,>(handler?: (payload: T) => void) => {
|
|
4
|
+
if (!handler) return undefined;
|
|
5
|
+
|
|
6
|
+
return (event: NativeSyntheticEvent<T>) => {
|
|
7
|
+
// Native event includes a "target" property that I think corresponds to the view tag, but this is not needed in my lib
|
|
8
|
+
delete (event.nativeEvent as any).target;
|
|
9
|
+
|
|
10
|
+
handler(event.nativeEvent);
|
|
11
|
+
};
|
|
12
|
+
}
|
package/tsconfig.json
ADDED