@thatkid02/react-native-pdf-viewer 0.0.1
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/LICENSE +20 -0
- package/PdfViewer.podspec +28 -0
- package/README.md +290 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +121 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/pdfviewer/HybridPdfViewer.kt +169 -0
- package/android/src/main/java/com/margelo/nitro/pdfviewer/PdfViewer.kt +996 -0
- package/android/src/main/java/com/margelo/nitro/pdfviewer/PdfViewerPackage.kt +26 -0
- package/ios/PdfViewer.swift +696 -0
- package/lib/module/PdfViewer.nitro.js +4 -0
- package/lib/module/PdfViewer.nitro.js.map +1 -0
- package/lib/module/index.js +13 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/PdfViewer.nitro.d.ts +67 -0
- package/lib/typescript/src/PdfViewer.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +8 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/android/c++/JErrorEvent.hpp +57 -0
- package/nitrogen/generated/android/c++/JFunc_void_ErrorEvent.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_LoadCompleteEvent.hpp +76 -0
- package/nitrogen/generated/android/c++/JFunc_void_LoadingChangeEvent.hpp +76 -0
- package/nitrogen/generated/android/c++/JFunc_void_PageChangeEvent.hpp +76 -0
- package/nitrogen/generated/android/c++/JFunc_void_ScaleChangeEvent.hpp +76 -0
- package/nitrogen/generated/android/c++/JFunc_void_ThumbnailGeneratedEvent.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridPdfViewerSpec.cpp +273 -0
- package/nitrogen/generated/android/c++/JHybridPdfViewerSpec.hpp +94 -0
- package/nitrogen/generated/android/c++/JLoadCompleteEvent.hpp +61 -0
- package/nitrogen/generated/android/c++/JLoadingChangeEvent.hpp +53 -0
- package/nitrogen/generated/android/c++/JPageChangeEvent.hpp +57 -0
- package/nitrogen/generated/android/c++/JScaleChangeEvent.hpp +53 -0
- package/nitrogen/generated/android/c++/JThumbnailGeneratedEvent.hpp +57 -0
- package/nitrogen/generated/android/c++/views/JHybridPdfViewerStateUpdater.cpp +108 -0
- package/nitrogen/generated/android/c++/views/JHybridPdfViewerStateUpdater.hpp +49 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/ErrorEvent.kt +32 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/Func_void_ErrorEvent.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/Func_void_LoadCompleteEvent.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/Func_void_LoadingChangeEvent.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/Func_void_PageChangeEvent.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/Func_void_ScaleChangeEvent.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/Func_void_ThumbnailGeneratedEvent.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/HybridPdfViewerSpec.kt +195 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/LoadCompleteEvent.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/LoadingChangeEvent.kt +29 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/PageChangeEvent.kt +32 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/ScaleChangeEvent.kt +29 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/ThumbnailGeneratedEvent.kt +32 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/pdfviewerOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/views/HybridPdfViewerManager.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/views/HybridPdfViewerStateUpdater.kt +23 -0
- package/nitrogen/generated/android/pdfviewer+autolinking.cmake +83 -0
- package/nitrogen/generated/android/pdfviewer+autolinking.gradle +27 -0
- package/nitrogen/generated/android/pdfviewerOnLoad.cpp +58 -0
- package/nitrogen/generated/android/pdfviewerOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/PdfViewer+autolinking.rb +60 -0
- package/nitrogen/generated/ios/PdfViewer-Swift-Cxx-Bridge.cpp +80 -0
- package/nitrogen/generated/ios/PdfViewer-Swift-Cxx-Bridge.hpp +339 -0
- package/nitrogen/generated/ios/PdfViewer-Swift-Cxx-Umbrella.hpp +64 -0
- package/nitrogen/generated/ios/PdfViewerAutolinking.mm +33 -0
- package/nitrogen/generated/ios/PdfViewerAutolinking.swift +25 -0
- package/nitrogen/generated/ios/c++/HybridPdfViewerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridPdfViewerSpecSwift.hpp +205 -0
- package/nitrogen/generated/ios/c++/views/HybridPdfViewerComponent.mm +161 -0
- package/nitrogen/generated/ios/swift/ErrorEvent.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_ErrorEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_LoadCompleteEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_LoadingChangeEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_PageChangeEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_ScaleChangeEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_ThumbnailGeneratedEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridPdfViewerSpec.swift +65 -0
- package/nitrogen/generated/ios/swift/HybridPdfViewerSpec_cxx.swift +500 -0
- package/nitrogen/generated/ios/swift/LoadCompleteEvent.swift +57 -0
- package/nitrogen/generated/ios/swift/LoadingChangeEvent.swift +35 -0
- package/nitrogen/generated/ios/swift/PageChangeEvent.swift +46 -0
- package/nitrogen/generated/ios/swift/ScaleChangeEvent.swift +35 -0
- package/nitrogen/generated/ios/swift/ThumbnailGeneratedEvent.swift +46 -0
- package/nitrogen/generated/shared/c++/ErrorEvent.hpp +71 -0
- package/nitrogen/generated/shared/c++/HybridPdfViewerSpec.cpp +52 -0
- package/nitrogen/generated/shared/c++/HybridPdfViewerSpec.hpp +111 -0
- package/nitrogen/generated/shared/c++/LoadCompleteEvent.hpp +75 -0
- package/nitrogen/generated/shared/c++/LoadingChangeEvent.hpp +67 -0
- package/nitrogen/generated/shared/c++/PageChangeEvent.hpp +71 -0
- package/nitrogen/generated/shared/c++/ScaleChangeEvent.hpp +67 -0
- package/nitrogen/generated/shared/c++/ThumbnailGeneratedEvent.hpp +71 -0
- package/nitrogen/generated/shared/c++/views/HybridPdfViewerComponent.cpp +243 -0
- package/nitrogen/generated/shared/c++/views/HybridPdfViewerComponent.hpp +127 -0
- package/nitrogen/generated/shared/json/PdfViewerConfig.json +23 -0
- package/package.json +175 -0
- package/src/PdfViewer.nitro.ts +97 -0
- package/src/index.tsx +27 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import PDFKit
|
|
3
|
+
import NitroModules
|
|
4
|
+
|
|
5
|
+
// Error Types
|
|
6
|
+
enum PdfViewerError: LocalizedError {
|
|
7
|
+
case documentNotLoaded
|
|
8
|
+
case invalidPageIndex(page: Int, pageCount: Int)
|
|
9
|
+
case zoomDisabled
|
|
10
|
+
case invalidSource
|
|
11
|
+
case invalidUri
|
|
12
|
+
case unsupportedScheme(String)
|
|
13
|
+
case fileNotFound(String)
|
|
14
|
+
case fileNotReadable(String)
|
|
15
|
+
case parseFailed
|
|
16
|
+
case passwordProtected
|
|
17
|
+
case emptyPdf
|
|
18
|
+
case networkError(Error)
|
|
19
|
+
case httpError(Int)
|
|
20
|
+
case emptyResponse
|
|
21
|
+
case thumbnailSaveFailed
|
|
22
|
+
|
|
23
|
+
var errorDescription: String? {
|
|
24
|
+
switch self {
|
|
25
|
+
case .documentNotLoaded: return "Document not loaded"
|
|
26
|
+
case .invalidPageIndex(let page, let count): return "Invalid page index \(page), document has \(count) pages"
|
|
27
|
+
case .zoomDisabled: return "Zoom is disabled"
|
|
28
|
+
case .invalidSource: return "Invalid source URI"
|
|
29
|
+
case .invalidUri: return "Could not parse source URI"
|
|
30
|
+
case .unsupportedScheme(let scheme): return "Unsupported URI scheme: \(scheme)"
|
|
31
|
+
case .fileNotFound(let path): return "File not found: \(path)"
|
|
32
|
+
case .fileNotReadable(let path): return "File is not readable: \(path)"
|
|
33
|
+
case .parseFailed: return "Failed to parse PDF"
|
|
34
|
+
case .passwordProtected: return "PDF is password protected"
|
|
35
|
+
case .emptyPdf: return "PDF has no pages"
|
|
36
|
+
case .networkError(let error): return "Network error: \(error.localizedDescription)"
|
|
37
|
+
case .httpError(let code): return "HTTP error: \(code)"
|
|
38
|
+
case .emptyResponse: return "Empty response"
|
|
39
|
+
case .thumbnailSaveFailed: return "Failed to save thumbnail"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
var errorCode: String {
|
|
44
|
+
switch self {
|
|
45
|
+
case .documentNotLoaded: return "DOCUMENT_NOT_LOADED"
|
|
46
|
+
case .invalidPageIndex: return "INVALID_PAGE_INDEX"
|
|
47
|
+
case .zoomDisabled: return "ZOOM_DISABLED"
|
|
48
|
+
case .invalidSource: return "INVALID_SOURCE"
|
|
49
|
+
case .invalidUri: return "INVALID_URI"
|
|
50
|
+
case .unsupportedScheme: return "UNSUPPORTED_SCHEME"
|
|
51
|
+
case .fileNotFound: return "FILE_NOT_FOUND"
|
|
52
|
+
case .fileNotReadable: return "FILE_NOT_READABLE"
|
|
53
|
+
case .parseFailed: return "PARSE_FAILED"
|
|
54
|
+
case .passwordProtected: return "PASSWORD_PROTECTED"
|
|
55
|
+
case .emptyPdf: return "EMPTY_PDF"
|
|
56
|
+
case .networkError: return "NETWORK_ERROR"
|
|
57
|
+
case .httpError: return "HTTP_ERROR"
|
|
58
|
+
case .emptyResponse: return "EMPTY_RESPONSE"
|
|
59
|
+
case .thumbnailSaveFailed: return "THUMBNAIL_SAVE_ERROR"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class HybridPdfViewer: HybridPdfViewerSpec {
|
|
65
|
+
// Properties
|
|
66
|
+
private var containerView: UIView!
|
|
67
|
+
private var pdfView: PDFView!
|
|
68
|
+
private var activityIndicator: UIActivityIndicatorView!
|
|
69
|
+
private var document: PDFDocument?
|
|
70
|
+
private var sourceUri: String?
|
|
71
|
+
private var urlSession: URLSession!
|
|
72
|
+
private var downloadTask: URLSessionDataTask?
|
|
73
|
+
private var loadToken: Int = 0
|
|
74
|
+
private var boundsObservation: NSKeyValueObservation?
|
|
75
|
+
private var isLoading: Bool = false {
|
|
76
|
+
didSet {
|
|
77
|
+
if isLoading != oldValue {
|
|
78
|
+
updateActivityIndicator()
|
|
79
|
+
onLoadingChange?(LoadingChangeEvent(isLoading: isLoading))
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Optimized caching with limits
|
|
85
|
+
private lazy var thumbnailCache: NSCache<NSNumber, NSString> = {
|
|
86
|
+
let cache = NSCache<NSNumber, NSString>()
|
|
87
|
+
cache.countLimit = 50 // Max 50 thumbnails in memory
|
|
88
|
+
cache.totalCostLimit = 10 * 1024 * 1024 // 10MB limit
|
|
89
|
+
cache.evictsObjectsWithDiscardedContent = true
|
|
90
|
+
return cache
|
|
91
|
+
}()
|
|
92
|
+
|
|
93
|
+
// Dedicated queue for thumbnail generation with limited concurrency
|
|
94
|
+
private let thumbnailQueue = DispatchQueue(label: "com.pdfviewer.thumbnails", qos: .utility, attributes: .concurrent)
|
|
95
|
+
private let thumbnailSemaphore = DispatchSemaphore(value: 4) // Max 4 concurrent thumbnail generations
|
|
96
|
+
|
|
97
|
+
// Track if we're currently generating thumbnails to avoid duplicate work
|
|
98
|
+
private var pendingThumbnails = Set<Int>()
|
|
99
|
+
private let pendingLock = NSLock()
|
|
100
|
+
|
|
101
|
+
// Nitro View
|
|
102
|
+
var view: UIView {
|
|
103
|
+
return containerView
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Nitro Properties
|
|
107
|
+
var source: String? {
|
|
108
|
+
didSet {
|
|
109
|
+
if source != oldValue {
|
|
110
|
+
loadDocument(source)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
var horizontal: Bool? {
|
|
116
|
+
didSet {
|
|
117
|
+
guard let horizontal = horizontal else { return }
|
|
118
|
+
ensureMainThread {
|
|
119
|
+
self.pdfView.displayDirection = horizontal ? .horizontal : .vertical
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
var enablePaging: Bool? {
|
|
125
|
+
didSet {
|
|
126
|
+
guard let enablePaging = enablePaging else { return }
|
|
127
|
+
ensureMainThread {
|
|
128
|
+
self.pdfView.displayMode = enablePaging ? .singlePage : .singlePageContinuous
|
|
129
|
+
self.pdfView.usePageViewController(enablePaging, withViewOptions: nil)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
var spacing: Double? {
|
|
135
|
+
didSet {
|
|
136
|
+
guard let spacing = spacing else { return }
|
|
137
|
+
ensureMainThread {
|
|
138
|
+
if spacing > 0 {
|
|
139
|
+
self.pdfView.displaysPageBreaks = true
|
|
140
|
+
self.pdfView.pageBreakMargins = UIEdgeInsets(top: CGFloat(spacing), left: 0, bottom: CGFloat(spacing), right: 0)
|
|
141
|
+
} else {
|
|
142
|
+
self.pdfView.displaysPageBreaks = false
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
var enableZoom: Bool? {
|
|
149
|
+
didSet {
|
|
150
|
+
guard let enableZoom = enableZoom else { return }
|
|
151
|
+
ensureMainThread {
|
|
152
|
+
if !enableZoom {
|
|
153
|
+
self.pdfView.minScaleFactor = 1.0
|
|
154
|
+
self.pdfView.maxScaleFactor = 1.0
|
|
155
|
+
self.pdfView.scaleFactor = 1.0
|
|
156
|
+
} else {
|
|
157
|
+
self.pdfView.minScaleFactor = CGFloat(self.minScale ?? 0.5)
|
|
158
|
+
self.pdfView.maxScaleFactor = CGFloat(self.maxScale ?? 4.0)
|
|
159
|
+
}
|
|
160
|
+
// PDFView's built-in gestures handle zoom when scale factors allow it
|
|
161
|
+
// No need to disable isUserInteractionEnabled as it affects scrolling too
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
var minScale: Double? {
|
|
167
|
+
didSet {
|
|
168
|
+
if let minScale = minScale, enableZoom ?? true {
|
|
169
|
+
ensureMainThread {
|
|
170
|
+
self.pdfView.minScaleFactor = CGFloat(minScale)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
var maxScale: Double? {
|
|
177
|
+
didSet {
|
|
178
|
+
if let maxScale = maxScale, enableZoom ?? true {
|
|
179
|
+
ensureMainThread {
|
|
180
|
+
self.pdfView.maxScaleFactor = CGFloat(maxScale)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Event callbacks
|
|
187
|
+
var onLoadComplete: ((LoadCompleteEvent) -> Void)?
|
|
188
|
+
var onPageChange: ((PageChangeEvent) -> Void)?
|
|
189
|
+
var onScaleChange: ((ScaleChangeEvent) -> Void)?
|
|
190
|
+
var onError: ((ErrorEvent) -> Void)?
|
|
191
|
+
var onThumbnailGenerated: ((ThumbnailGeneratedEvent) -> Void)?
|
|
192
|
+
var onLoadingChange: ((LoadingChangeEvent) -> Void)?
|
|
193
|
+
|
|
194
|
+
// Show/hide activity indicator
|
|
195
|
+
var showsActivityIndicator: Bool? {
|
|
196
|
+
didSet {
|
|
197
|
+
ensureMainThread {
|
|
198
|
+
self.updateActivityIndicator()
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Activity Indicator
|
|
204
|
+
private func updateActivityIndicator() {
|
|
205
|
+
// Ensure we're on main thread for UI updates
|
|
206
|
+
guard Thread.isMainThread else {
|
|
207
|
+
DispatchQueue.main.async { [weak self] in
|
|
208
|
+
self?.updateActivityIndicator()
|
|
209
|
+
}
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let shouldShow = isLoading && (showsActivityIndicator ?? true)
|
|
214
|
+
if shouldShow {
|
|
215
|
+
activityIndicator.startAnimating()
|
|
216
|
+
} else {
|
|
217
|
+
activityIndicator.stopAnimating()
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Initialization
|
|
222
|
+
override init() {
|
|
223
|
+
// Create container view
|
|
224
|
+
containerView = UIView()
|
|
225
|
+
containerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
226
|
+
containerView.backgroundColor = .white
|
|
227
|
+
|
|
228
|
+
// Create PDF view
|
|
229
|
+
pdfView = PDFView()
|
|
230
|
+
pdfView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
231
|
+
pdfView.autoScales = false
|
|
232
|
+
pdfView.displayDirection = .vertical
|
|
233
|
+
pdfView.displayMode = .singlePageContinuous
|
|
234
|
+
pdfView.usePageViewController(false, withViewOptions: nil)
|
|
235
|
+
pdfView.displaysPageBreaks = false
|
|
236
|
+
pdfView.minScaleFactor = 0.5
|
|
237
|
+
pdfView.maxScaleFactor = 4.0
|
|
238
|
+
pdfView.backgroundColor = .white
|
|
239
|
+
|
|
240
|
+
// Enable user interaction for gestures (pinch zoom, pan, etc.)
|
|
241
|
+
pdfView.isUserInteractionEnabled = true
|
|
242
|
+
|
|
243
|
+
// PDFView has built-in gesture recognizers for pinch zoom
|
|
244
|
+
// No need to add custom gesture recognizers
|
|
245
|
+
containerView.addSubview(pdfView)
|
|
246
|
+
|
|
247
|
+
// Create activity indicator
|
|
248
|
+
activityIndicator = UIActivityIndicatorView(style: .large)
|
|
249
|
+
activityIndicator.hidesWhenStopped = true
|
|
250
|
+
activityIndicator.color = .gray
|
|
251
|
+
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
|
252
|
+
containerView.addSubview(activityIndicator)
|
|
253
|
+
|
|
254
|
+
// Center activity indicator
|
|
255
|
+
NSLayoutConstraint.activate([
|
|
256
|
+
activityIndicator.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
|
|
257
|
+
activityIndicator.centerYAnchor.constraint(equalTo: containerView.centerYAnchor)
|
|
258
|
+
])
|
|
259
|
+
|
|
260
|
+
let config = URLSessionConfiguration.ephemeral
|
|
261
|
+
config.timeoutIntervalForRequest = 30.0
|
|
262
|
+
urlSession = URLSession(configuration: config)
|
|
263
|
+
|
|
264
|
+
super.init()
|
|
265
|
+
|
|
266
|
+
NotificationCenter.default.addObserver(
|
|
267
|
+
self,
|
|
268
|
+
selector: #selector(pageChanged),
|
|
269
|
+
name: .PDFViewPageChanged,
|
|
270
|
+
object: pdfView
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
NotificationCenter.default.addObserver(
|
|
274
|
+
self,
|
|
275
|
+
selector: #selector(scaleChanged),
|
|
276
|
+
name: .PDFViewScaleChanged,
|
|
277
|
+
object: pdfView
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
// Observe memory warnings to clear caches
|
|
281
|
+
NotificationCenter.default.addObserver(
|
|
282
|
+
self,
|
|
283
|
+
selector: #selector(didReceiveMemoryWarning),
|
|
284
|
+
name: UIApplication.didReceiveMemoryWarningNotification,
|
|
285
|
+
object: nil
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
// Observe bounds changes to update scale factor when view is resized
|
|
289
|
+
boundsObservation = pdfView.observe(\.bounds, options: [.new]) { [weak self] _, _ in
|
|
290
|
+
self?.updateScaleToFitWidthIfNeeded()
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
deinit {
|
|
295
|
+
NotificationCenter.default.removeObserver(self)
|
|
296
|
+
boundsObservation?.invalidate()
|
|
297
|
+
downloadTask?.cancel()
|
|
298
|
+
urlSession.invalidateAndCancel()
|
|
299
|
+
thumbnailCache.removeAllObjects()
|
|
300
|
+
cleanupThumbnailDirectory()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Document Loading
|
|
304
|
+
private func loadDocument(_ source: String?) {
|
|
305
|
+
loadToken += 1
|
|
306
|
+
let currentToken = loadToken
|
|
307
|
+
downloadTask?.cancel()
|
|
308
|
+
downloadTask = nil
|
|
309
|
+
document = nil
|
|
310
|
+
pdfView.document = nil
|
|
311
|
+
|
|
312
|
+
// Clear pending thumbnail operations
|
|
313
|
+
pendingLock.lock()
|
|
314
|
+
pendingThumbnails.removeAll()
|
|
315
|
+
pendingLock.unlock()
|
|
316
|
+
|
|
317
|
+
// Set loading state
|
|
318
|
+
ensureMainThread {
|
|
319
|
+
self.isLoading = true
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
guard let source = source, !source.isEmpty else {
|
|
323
|
+
ensureMainThread { self.isLoading = false }
|
|
324
|
+
emitError(.invalidSource)
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Store the source URI for thumbnail caching
|
|
329
|
+
sourceUri = source
|
|
330
|
+
|
|
331
|
+
guard let url = resolveURL(from: source) else {
|
|
332
|
+
ensureMainThread { self.isLoading = false }
|
|
333
|
+
emitError(.invalidUri)
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let scheme = url.scheme ?? ""
|
|
338
|
+
guard url.isFileURL || scheme == "http" || scheme == "https" else {
|
|
339
|
+
ensureMainThread { self.isLoading = false }
|
|
340
|
+
emitError(.unsupportedScheme(scheme))
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if url.isFileURL {
|
|
345
|
+
loadLocalDocument(url: url, token: currentToken)
|
|
346
|
+
} else {
|
|
347
|
+
loadRemoteDocument(url: url, token: currentToken)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private func loadLocalDocument(url: URL, token: Int) {
|
|
352
|
+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
353
|
+
guard let self = self, token == self.loadToken else { return }
|
|
354
|
+
|
|
355
|
+
let fileManager = FileManager.default
|
|
356
|
+
let path = url.path
|
|
357
|
+
|
|
358
|
+
guard fileManager.fileExists(atPath: path) else {
|
|
359
|
+
self.emitErrorOnMain(.fileNotFound(path))
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
guard fileManager.isReadableFile(atPath: path) else {
|
|
364
|
+
self.emitErrorOnMain(.fileNotReadable(path))
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
guard let document = PDFDocument(url: url) else {
|
|
369
|
+
self.emitErrorOnMain(.parseFailed)
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
DispatchQueue.main.async { [weak self] in
|
|
374
|
+
guard let self = self, token == self.loadToken else { return }
|
|
375
|
+
self.applyLoadedDocument(document)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private func loadRemoteDocument(url: URL, token: Int) {
|
|
381
|
+
let request = URLRequest(url: url, timeoutInterval: 30.0)
|
|
382
|
+
|
|
383
|
+
downloadTask = urlSession.dataTask(with: request) { [weak self] data, response, error in
|
|
384
|
+
guard let self = self, token == self.loadToken else { return }
|
|
385
|
+
|
|
386
|
+
if let error = error as NSError? {
|
|
387
|
+
// Check if cancelled
|
|
388
|
+
if error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled {
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
self.emitErrorOnMain(.networkError(error))
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if let httpResponse = response as? HTTPURLResponse,
|
|
396
|
+
!(200...299).contains(httpResponse.statusCode) {
|
|
397
|
+
self.emitErrorOnMain(.httpError(httpResponse.statusCode))
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
guard let data = data, !data.isEmpty else {
|
|
402
|
+
self.emitErrorOnMain(.emptyResponse)
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
guard let document = PDFDocument(data: data) else {
|
|
407
|
+
self.emitErrorOnMain(.parseFailed)
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
DispatchQueue.main.async { [weak self] in
|
|
412
|
+
guard let self = self, token == self.loadToken else { return }
|
|
413
|
+
self.applyLoadedDocument(document)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
downloadTask?.resume()
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private func applyLoadedDocument(_ document: PDFDocument) {
|
|
420
|
+
guard !document.isLocked else {
|
|
421
|
+
isLoading = false
|
|
422
|
+
emitError(.passwordProtected)
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
guard document.pageCount > 0 else {
|
|
427
|
+
isLoading = false
|
|
428
|
+
emitError(.emptyPdf)
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
isLoading = false
|
|
433
|
+
self.document = document
|
|
434
|
+
pdfView.document = document
|
|
435
|
+
thumbnailCache.removeAllObjects()
|
|
436
|
+
|
|
437
|
+
// Update scale after document is loaded
|
|
438
|
+
updateScaleToFitWidthIfNeeded()
|
|
439
|
+
|
|
440
|
+
if let firstPage = document.page(at: 0) {
|
|
441
|
+
let pageRect = firstPage.bounds(for: .mediaBox)
|
|
442
|
+
onLoadComplete?(LoadCompleteEvent(
|
|
443
|
+
pageCount: Double(document.pageCount),
|
|
444
|
+
pageWidth: Double(pageRect.width),
|
|
445
|
+
pageHeight: Double(pageRect.height)
|
|
446
|
+
))
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private func updateScaleToFitWidthIfNeeded() {
|
|
451
|
+
guard let document = document, document.pageCount > 0,
|
|
452
|
+
let firstPage = document.page(at: 0) else { return }
|
|
453
|
+
|
|
454
|
+
let pageRect = firstPage.bounds(for: .mediaBox)
|
|
455
|
+
let viewWidth = pdfView.bounds.width
|
|
456
|
+
|
|
457
|
+
// Only update scale if view has been laid out (width > 0) and zoom is enabled
|
|
458
|
+
guard viewWidth > 0 && pageRect.width > 0 else { return }
|
|
459
|
+
|
|
460
|
+
let scale = viewWidth / pageRect.width
|
|
461
|
+
let minScale = CGFloat(self.minScale ?? 0.5)
|
|
462
|
+
let maxScale = CGFloat(self.maxScale ?? 4.0)
|
|
463
|
+
|
|
464
|
+
// Clamp the scale between min and max
|
|
465
|
+
let clampedScale = max(minScale, min(maxScale, scale))
|
|
466
|
+
pdfView.scaleFactor = clampedScale
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private func resolveURL(from source: String) -> URL? {
|
|
470
|
+
if let url = URL(string: source), url.scheme != nil {
|
|
471
|
+
return url
|
|
472
|
+
}
|
|
473
|
+
return URL(fileURLWithPath: source)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Notifications
|
|
477
|
+
@objc private func pageChanged() {
|
|
478
|
+
guard let document = document, let currentPage = pdfView.currentPage else { return }
|
|
479
|
+
let pageIndex = document.index(for: currentPage)
|
|
480
|
+
|
|
481
|
+
onPageChange?(PageChangeEvent(
|
|
482
|
+
page: Double(pageIndex),
|
|
483
|
+
pageCount: Double(document.pageCount)
|
|
484
|
+
))
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
@objc private func scaleChanged() {
|
|
488
|
+
onScaleChange?(ScaleChangeEvent(scale: Double(pdfView.scaleFactor)))
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
@objc private func didReceiveMemoryWarning() {
|
|
492
|
+
// Clear thumbnail cache to free up memory
|
|
493
|
+
thumbnailCache.removeAllObjects()
|
|
494
|
+
|
|
495
|
+
// Clear pending thumbnails set
|
|
496
|
+
pendingLock.lock()
|
|
497
|
+
pendingThumbnails.removeAll()
|
|
498
|
+
pendingLock.unlock()
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Nitro Methods
|
|
502
|
+
func goToPage(page: Double) throws {
|
|
503
|
+
guard let document = document else {
|
|
504
|
+
throw PdfViewerError.documentNotLoaded
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
let pageIndex = Int(page)
|
|
508
|
+
guard pageIndex >= 0 && pageIndex < document.pageCount else {
|
|
509
|
+
throw PdfViewerError.invalidPageIndex(page: pageIndex, pageCount: document.pageCount)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
guard let pdfPage = document.page(at: pageIndex) else {
|
|
513
|
+
throw PdfViewerError.invalidPageIndex(page: pageIndex, pageCount: document.pageCount)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Ensure UI update happens on main thread
|
|
517
|
+
if Thread.isMainThread {
|
|
518
|
+
pdfView.go(to: pdfPage)
|
|
519
|
+
} else {
|
|
520
|
+
DispatchQueue.main.async { [weak self] in
|
|
521
|
+
self?.pdfView.go(to: pdfPage)
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
func setScale(scale: Double) throws {
|
|
527
|
+
guard enableZoom ?? true else {
|
|
528
|
+
throw PdfViewerError.zoomDisabled
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
let minScale = CGFloat(self.minScale ?? 0.5)
|
|
532
|
+
let maxScale = CGFloat(self.maxScale ?? 4.0)
|
|
533
|
+
let clampedScale = max(minScale, min(maxScale, CGFloat(scale)))
|
|
534
|
+
|
|
535
|
+
// Ensure UI update happens on main thread
|
|
536
|
+
if Thread.isMainThread {
|
|
537
|
+
pdfView.scaleFactor = clampedScale
|
|
538
|
+
} else {
|
|
539
|
+
DispatchQueue.main.async { [weak self] in
|
|
540
|
+
self?.pdfView.scaleFactor = clampedScale
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
func generateThumbnail(page: Double) throws {
|
|
546
|
+
guard let document = document else {
|
|
547
|
+
throw PdfViewerError.documentNotLoaded
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
let pageIndex = Int(page)
|
|
551
|
+
guard pageIndex >= 0 && pageIndex < document.pageCount else {
|
|
552
|
+
throw PdfViewerError.invalidPageIndex(page: pageIndex, pageCount: document.pageCount)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
let pageKey = NSNumber(value: pageIndex)
|
|
556
|
+
|
|
557
|
+
// Return cached thumbnail immediately if available
|
|
558
|
+
if let cached = thumbnailCache.object(forKey: pageKey) {
|
|
559
|
+
onThumbnailGenerated?(ThumbnailGeneratedEvent(page: Double(pageIndex), uri: cached as String))
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Check if already being generated
|
|
564
|
+
pendingLock.lock()
|
|
565
|
+
if pendingThumbnails.contains(pageIndex) {
|
|
566
|
+
pendingLock.unlock()
|
|
567
|
+
return // Already in progress
|
|
568
|
+
}
|
|
569
|
+
pendingThumbnails.insert(pageIndex)
|
|
570
|
+
pendingLock.unlock()
|
|
571
|
+
|
|
572
|
+
// Generate thumbnail asynchronously with semaphore for concurrency control
|
|
573
|
+
thumbnailQueue.async { [weak self] in
|
|
574
|
+
guard let self = self else { return }
|
|
575
|
+
|
|
576
|
+
defer {
|
|
577
|
+
self.pendingLock.lock()
|
|
578
|
+
self.pendingThumbnails.remove(pageIndex)
|
|
579
|
+
self.pendingLock.unlock()
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
self.thumbnailSemaphore.wait()
|
|
583
|
+
defer { self.thumbnailSemaphore.signal() }
|
|
584
|
+
|
|
585
|
+
// Double-check cache after acquiring semaphore
|
|
586
|
+
if let cached = self.thumbnailCache.object(forKey: pageKey) {
|
|
587
|
+
DispatchQueue.main.async { [weak self] in
|
|
588
|
+
self?.onThumbnailGenerated?(ThumbnailGeneratedEvent(page: Double(pageIndex), uri: cached as String))
|
|
589
|
+
}
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
self.generateThumbnailSync(document: document, pageIndex: pageIndex, pageKey: pageKey)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
func generateAllThumbnails() throws {
|
|
598
|
+
guard let document = document else {
|
|
599
|
+
throw PdfViewerError.documentNotLoaded
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
let pageCount = document.pageCount
|
|
603
|
+
|
|
604
|
+
// Process thumbnails in batches for better memory management
|
|
605
|
+
thumbnailQueue.async { [weak self] in
|
|
606
|
+
guard let self = self else { return }
|
|
607
|
+
|
|
608
|
+
for pageIndex in 0..<pageCount {
|
|
609
|
+
autoreleasepool {
|
|
610
|
+
let pageKey = NSNumber(value: pageIndex)
|
|
611
|
+
|
|
612
|
+
// Return cached thumbnail immediately
|
|
613
|
+
if let cached = self.thumbnailCache.object(forKey: pageKey) {
|
|
614
|
+
DispatchQueue.main.async { [weak self] in
|
|
615
|
+
self?.onThumbnailGenerated?(ThumbnailGeneratedEvent(page: Double(pageIndex), uri: cached as String))
|
|
616
|
+
}
|
|
617
|
+
return
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
self.thumbnailSemaphore.wait()
|
|
621
|
+
defer { self.thumbnailSemaphore.signal() }
|
|
622
|
+
|
|
623
|
+
self.generateThumbnailSync(document: document, pageIndex: pageIndex, pageKey: pageKey)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Private Thumbnail Generation
|
|
630
|
+
private func generateThumbnailSync(document: PDFDocument, pageIndex: Int, pageKey: NSNumber) {
|
|
631
|
+
guard let pdfPage = document.page(at: pageIndex) else { return }
|
|
632
|
+
|
|
633
|
+
let pageRect = pdfPage.bounds(for: .mediaBox)
|
|
634
|
+
guard pageRect.width > 0 && pageRect.height > 0 else { return }
|
|
635
|
+
|
|
636
|
+
let aspectRatio = pageRect.height / pageRect.width
|
|
637
|
+
let thumbWidth: CGFloat = 120.0
|
|
638
|
+
let thumbHeight = thumbWidth * aspectRatio
|
|
639
|
+
|
|
640
|
+
let thumbnail = pdfPage.thumbnail(of: CGSize(width: thumbWidth, height: thumbHeight), for: .mediaBox)
|
|
641
|
+
|
|
642
|
+
if let uri = saveThumbnailToCache(thumbnail, page: pageIndex) {
|
|
643
|
+
// Calculate approximate memory cost for cache
|
|
644
|
+
let cost = Int(thumbWidth * thumbHeight * 4) // Approximate bytes
|
|
645
|
+
thumbnailCache.setObject(uri as NSString, forKey: pageKey, cost: cost)
|
|
646
|
+
|
|
647
|
+
DispatchQueue.main.async { [weak self] in
|
|
648
|
+
self?.onThumbnailGenerated?(ThumbnailGeneratedEvent(page: Double(pageIndex), uri: uri))
|
|
649
|
+
}
|
|
650
|
+
} else {
|
|
651
|
+
emitErrorOnMain(.thumbnailSaveFailed)
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Helper Methods
|
|
656
|
+
private func saveThumbnailToCache(_ image: UIImage, page: Int) -> String? {
|
|
657
|
+
guard let data = image.jpegData(compressionQuality: 0.8) else { return nil }
|
|
658
|
+
|
|
659
|
+
let hash = abs(sourceUri?.hashValue ?? 0)
|
|
660
|
+
let fileName = "thumb_\(page)_\(hash).jpg"
|
|
661
|
+
let cacheDir = FileManager.default.temporaryDirectory.appendingPathComponent("PDFThumbnails")
|
|
662
|
+
|
|
663
|
+
do {
|
|
664
|
+
try FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true)
|
|
665
|
+
let fileURL = cacheDir.appendingPathComponent(fileName)
|
|
666
|
+
try data.write(to: fileURL, options: .atomic)
|
|
667
|
+
return fileURL.absoluteString
|
|
668
|
+
} catch {
|
|
669
|
+
return nil
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
private func emitError(_ error: PdfViewerError) {
|
|
674
|
+
onError?(ErrorEvent(message: error.errorDescription ?? "Unknown error", code: error.errorCode))
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private func emitErrorOnMain(_ error: PdfViewerError) {
|
|
678
|
+
DispatchQueue.main.async { [weak self] in
|
|
679
|
+
self?.isLoading = false
|
|
680
|
+
self?.emitError(error)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private func ensureMainThread(_ block: @escaping () -> Void) {
|
|
685
|
+
if Thread.isMainThread {
|
|
686
|
+
block()
|
|
687
|
+
} else {
|
|
688
|
+
DispatchQueue.main.async(execute: block)
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
private func cleanupThumbnailDirectory() {
|
|
693
|
+
let cacheDir = FileManager.default.temporaryDirectory.appendingPathComponent("PDFThumbnails")
|
|
694
|
+
try? FileManager.default.removeItem(at: cacheDir)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["PdfViewer.nitro.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { getHostComponent, callback } from 'react-native-nitro-modules';
|
|
4
|
+
const PdfViewerConfig = require('../nitrogen/generated/shared/json/PdfViewerConfig.json');
|
|
5
|
+
export const PdfViewerView = getHostComponent('PdfViewer', () => PdfViewerConfig);
|
|
6
|
+
|
|
7
|
+
// HybridRef type for ref prop
|
|
8
|
+
|
|
9
|
+
// Re-export types
|
|
10
|
+
|
|
11
|
+
// Re-export callback utility
|
|
12
|
+
export { callback };
|
|
13
|
+
//# sourceMappingURL=index.js.map
|