@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.
Files changed (95) hide show
  1. package/LICENSE +20 -0
  2. package/PdfViewer.podspec +28 -0
  3. package/README.md +290 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +121 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  8. package/android/src/main/java/com/margelo/nitro/pdfviewer/HybridPdfViewer.kt +169 -0
  9. package/android/src/main/java/com/margelo/nitro/pdfviewer/PdfViewer.kt +996 -0
  10. package/android/src/main/java/com/margelo/nitro/pdfviewer/PdfViewerPackage.kt +26 -0
  11. package/ios/PdfViewer.swift +696 -0
  12. package/lib/module/PdfViewer.nitro.js +4 -0
  13. package/lib/module/PdfViewer.nitro.js.map +1 -0
  14. package/lib/module/index.js +13 -0
  15. package/lib/module/index.js.map +1 -0
  16. package/lib/module/package.json +1 -0
  17. package/lib/typescript/package.json +1 -0
  18. package/lib/typescript/src/PdfViewer.nitro.d.ts +67 -0
  19. package/lib/typescript/src/PdfViewer.nitro.d.ts.map +1 -0
  20. package/lib/typescript/src/index.d.ts +8 -0
  21. package/lib/typescript/src/index.d.ts.map +1 -0
  22. package/nitro.json +17 -0
  23. package/nitrogen/generated/android/c++/JErrorEvent.hpp +57 -0
  24. package/nitrogen/generated/android/c++/JFunc_void_ErrorEvent.hpp +77 -0
  25. package/nitrogen/generated/android/c++/JFunc_void_LoadCompleteEvent.hpp +76 -0
  26. package/nitrogen/generated/android/c++/JFunc_void_LoadingChangeEvent.hpp +76 -0
  27. package/nitrogen/generated/android/c++/JFunc_void_PageChangeEvent.hpp +76 -0
  28. package/nitrogen/generated/android/c++/JFunc_void_ScaleChangeEvent.hpp +76 -0
  29. package/nitrogen/generated/android/c++/JFunc_void_ThumbnailGeneratedEvent.hpp +77 -0
  30. package/nitrogen/generated/android/c++/JHybridPdfViewerSpec.cpp +273 -0
  31. package/nitrogen/generated/android/c++/JHybridPdfViewerSpec.hpp +94 -0
  32. package/nitrogen/generated/android/c++/JLoadCompleteEvent.hpp +61 -0
  33. package/nitrogen/generated/android/c++/JLoadingChangeEvent.hpp +53 -0
  34. package/nitrogen/generated/android/c++/JPageChangeEvent.hpp +57 -0
  35. package/nitrogen/generated/android/c++/JScaleChangeEvent.hpp +53 -0
  36. package/nitrogen/generated/android/c++/JThumbnailGeneratedEvent.hpp +57 -0
  37. package/nitrogen/generated/android/c++/views/JHybridPdfViewerStateUpdater.cpp +108 -0
  38. package/nitrogen/generated/android/c++/views/JHybridPdfViewerStateUpdater.hpp +49 -0
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/ErrorEvent.kt +32 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/Func_void_ErrorEvent.kt +81 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/Func_void_LoadCompleteEvent.kt +81 -0
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/Func_void_LoadingChangeEvent.kt +81 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/Func_void_PageChangeEvent.kt +81 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/Func_void_ScaleChangeEvent.kt +81 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/Func_void_ThumbnailGeneratedEvent.kt +81 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/HybridPdfViewerSpec.kt +195 -0
  47. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/LoadCompleteEvent.kt +35 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/LoadingChangeEvent.kt +29 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/PageChangeEvent.kt +32 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/ScaleChangeEvent.kt +29 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/ThumbnailGeneratedEvent.kt +32 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/pdfviewerOnLoad.kt +35 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/views/HybridPdfViewerManager.kt +50 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pdfviewer/views/HybridPdfViewerStateUpdater.kt +23 -0
  55. package/nitrogen/generated/android/pdfviewer+autolinking.cmake +83 -0
  56. package/nitrogen/generated/android/pdfviewer+autolinking.gradle +27 -0
  57. package/nitrogen/generated/android/pdfviewerOnLoad.cpp +58 -0
  58. package/nitrogen/generated/android/pdfviewerOnLoad.hpp +25 -0
  59. package/nitrogen/generated/ios/PdfViewer+autolinking.rb +60 -0
  60. package/nitrogen/generated/ios/PdfViewer-Swift-Cxx-Bridge.cpp +80 -0
  61. package/nitrogen/generated/ios/PdfViewer-Swift-Cxx-Bridge.hpp +339 -0
  62. package/nitrogen/generated/ios/PdfViewer-Swift-Cxx-Umbrella.hpp +64 -0
  63. package/nitrogen/generated/ios/PdfViewerAutolinking.mm +33 -0
  64. package/nitrogen/generated/ios/PdfViewerAutolinking.swift +25 -0
  65. package/nitrogen/generated/ios/c++/HybridPdfViewerSpecSwift.cpp +11 -0
  66. package/nitrogen/generated/ios/c++/HybridPdfViewerSpecSwift.hpp +205 -0
  67. package/nitrogen/generated/ios/c++/views/HybridPdfViewerComponent.mm +161 -0
  68. package/nitrogen/generated/ios/swift/ErrorEvent.swift +46 -0
  69. package/nitrogen/generated/ios/swift/Func_void_ErrorEvent.swift +47 -0
  70. package/nitrogen/generated/ios/swift/Func_void_LoadCompleteEvent.swift +47 -0
  71. package/nitrogen/generated/ios/swift/Func_void_LoadingChangeEvent.swift +47 -0
  72. package/nitrogen/generated/ios/swift/Func_void_PageChangeEvent.swift +47 -0
  73. package/nitrogen/generated/ios/swift/Func_void_ScaleChangeEvent.swift +47 -0
  74. package/nitrogen/generated/ios/swift/Func_void_ThumbnailGeneratedEvent.swift +47 -0
  75. package/nitrogen/generated/ios/swift/HybridPdfViewerSpec.swift +65 -0
  76. package/nitrogen/generated/ios/swift/HybridPdfViewerSpec_cxx.swift +500 -0
  77. package/nitrogen/generated/ios/swift/LoadCompleteEvent.swift +57 -0
  78. package/nitrogen/generated/ios/swift/LoadingChangeEvent.swift +35 -0
  79. package/nitrogen/generated/ios/swift/PageChangeEvent.swift +46 -0
  80. package/nitrogen/generated/ios/swift/ScaleChangeEvent.swift +35 -0
  81. package/nitrogen/generated/ios/swift/ThumbnailGeneratedEvent.swift +46 -0
  82. package/nitrogen/generated/shared/c++/ErrorEvent.hpp +71 -0
  83. package/nitrogen/generated/shared/c++/HybridPdfViewerSpec.cpp +52 -0
  84. package/nitrogen/generated/shared/c++/HybridPdfViewerSpec.hpp +111 -0
  85. package/nitrogen/generated/shared/c++/LoadCompleteEvent.hpp +75 -0
  86. package/nitrogen/generated/shared/c++/LoadingChangeEvent.hpp +67 -0
  87. package/nitrogen/generated/shared/c++/PageChangeEvent.hpp +71 -0
  88. package/nitrogen/generated/shared/c++/ScaleChangeEvent.hpp +67 -0
  89. package/nitrogen/generated/shared/c++/ThumbnailGeneratedEvent.hpp +71 -0
  90. package/nitrogen/generated/shared/c++/views/HybridPdfViewerComponent.cpp +243 -0
  91. package/nitrogen/generated/shared/c++/views/HybridPdfViewerComponent.hpp +127 -0
  92. package/nitrogen/generated/shared/json/PdfViewerConfig.json +23 -0
  93. package/package.json +175 -0
  94. package/src/PdfViewer.nitro.ts +97 -0
  95. 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,4 @@
1
+ "use strict";
2
+
3
+ export {};
4
+ //# sourceMappingURL=PdfViewer.nitro.js.map
@@ -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