react-native-pdfrender 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +306 -0
- package/android/build.gradle +76 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/pdfrender/ComposeRenderer.kt +85 -0
- package/android/src/main/java/com/pdfrender/PdfCacheManager.kt +150 -0
- package/android/src/main/java/com/pdfrender/PdfConstants.kt +63 -0
- package/android/src/main/java/com/pdfrender/PdfIconComponents.kt +275 -0
- package/android/src/main/java/com/pdfrender/PdfRenderingLogic.kt +325 -0
- package/android/src/main/java/com/pdfrender/PdfUIComponents.kt +335 -0
- package/android/src/main/java/com/pdfrender/PdfViewPackage.kt +32 -0
- package/android/src/main/java/com/pdfrender/PdfViewerActivity.kt +3467 -0
- package/android/src/main/java/com/pdfrender/PdfViewerFabricManager.kt +244 -0
- package/android/src/main/java/com/pdfrender/PdfViewerFragment.kt +129 -0
- package/android/src/main/java/com/pdfrender/PdfViewerTurboModule.kt +158 -0
- package/android/src/main/java/com/pdfrender/events/FullScreenChangeEvent.kt +26 -0
- package/android/src/main/java/com/pdfrender/events/LeftScreenChangeEvent.kt +22 -0
- package/android/src/main/java/com/pdfrender/events/RightScreenChangeEvent.kt +22 -0
- package/android/src/main/java/com/pdfrender/events/ZoomChangeEvent.kt +22 -0
- package/ios/PdfCacheManager.swift +44 -0
- package/ios/PdfConstants.swift +38 -0
- package/ios/PdfPageView.swift +121 -0
- package/ios/PdfRenderingLogic.swift +107 -0
- package/ios/PdfToolbarView.swift +158 -0
- package/ios/PdfViewerComponentView.mm +194 -0
- package/ios/PdfViewerTurboModule.mm +186 -0
- package/ios/PdfViewerTurboModuleImpl.swift +141 -0
- package/ios/PdfViewerView.swift +268 -0
- package/ios/PdfViewerViewController.swift +109 -0
- package/lib/commonjs/PdfViewerView.js +105 -0
- package/lib/commonjs/PdfViewerView.js.map +1 -0
- package/lib/commonjs/index.js +28 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/NativePdfViewerComponent.js +27 -0
- package/lib/commonjs/specs/NativePdfViewerComponent.js.map +1 -0
- package/lib/commonjs/specs/NativePdfViewerModule.js +21 -0
- package/lib/commonjs/specs/NativePdfViewerModule.js.map +1 -0
- package/lib/commonjs/usePdfViewer.js +65 -0
- package/lib/commonjs/usePdfViewer.js.map +1 -0
- package/lib/module/PdfViewerView.js +99 -0
- package/lib/module/PdfViewerView.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/specs/NativePdfViewerComponent.js +26 -0
- package/lib/module/specs/NativePdfViewerComponent.js.map +1 -0
- package/lib/module/specs/NativePdfViewerModule.js +18 -0
- package/lib/module/specs/NativePdfViewerModule.js.map +1 -0
- package/lib/module/usePdfViewer.js +60 -0
- package/lib/module/usePdfViewer.js.map +1 -0
- package/lib/typescript/PdfViewerView.d.ts +58 -0
- package/lib/typescript/PdfViewerView.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +7 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/specs/NativePdfViewerComponent.d.ts +59 -0
- package/lib/typescript/specs/NativePdfViewerComponent.d.ts.map +1 -0
- package/lib/typescript/specs/NativePdfViewerModule.d.ts +47 -0
- package/lib/typescript/specs/NativePdfViewerModule.d.ts.map +1 -0
- package/lib/typescript/usePdfViewer.d.ts +45 -0
- package/lib/typescript/usePdfViewer.d.ts.map +1 -0
- package/package.json +109 -0
- package/react-native-pdfrender.podspec +35 -0
- package/react-native.config.js +11 -0
- package/src/PdfViewerView.tsx +159 -0
- package/src/index.tsx +10 -0
- package/src/specs/NativePdfViewerComponent.ts +94 -0
- package/src/specs/NativePdfViewerModule.ts +58 -0
- package/src/usePdfViewer.ts +102 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PdfViewerTurboModule.mm — ObjC++ bridge for the PDF viewer TurboModule.
|
|
3
|
+
*
|
|
4
|
+
* Architecture
|
|
5
|
+
* ────────────
|
|
6
|
+
* Swift class (PdfViewerTurboModuleImpl.swift)
|
|
7
|
+
* └─ holds ALL business logic (present VC, render pages, create PDF)
|
|
8
|
+
* This ObjC++ file
|
|
9
|
+
* └─ bridges Swift ↔ React Native New Architecture (JSI / TurboModules)
|
|
10
|
+
* └─ wraps promise-based methods (RCTPromiseResolveBlock / RejectBlock)
|
|
11
|
+
* └─ emits native events to JS via RCTEventEmitter
|
|
12
|
+
*
|
|
13
|
+
* Why .mm (ObjC++)?
|
|
14
|
+
* The New Architecture TurboModule system uses C++ (JSI) internally.
|
|
15
|
+
* `getTurboModule:` returns a std::shared_ptr — a C++ type — which
|
|
16
|
+
* cannot appear in a plain .m (ObjC) file.
|
|
17
|
+
*
|
|
18
|
+
* Why forward-declare instead of importing pdfRender-Swift.h?
|
|
19
|
+
* The auto-generated Swift bridging header pulls in every @objc class,
|
|
20
|
+
* including AppDelegate's ReactNativeDelegate which depends on
|
|
21
|
+
* RCTDefaultReactNativeFactoryDelegate — a transitive header chain that
|
|
22
|
+
* causes "file not found" errors in this translation unit. A targeted
|
|
23
|
+
* forward declaration breaks the dependency and the linker resolves the
|
|
24
|
+
* Swift symbol at link time.
|
|
25
|
+
*
|
|
26
|
+
* Module name
|
|
27
|
+
* RCT_EXPORT_MODULE(NativePdfViewerModule) must match the name returned by
|
|
28
|
+
* TurboModuleRegistry.getEnforcing('NativePdfViewerModule') in the TS spec.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
#import <React/RCTBridgeModule.h>
|
|
32
|
+
#import <React/RCTEventEmitter.h>
|
|
33
|
+
|
|
34
|
+
// ── Forward-declare Swift class ───────────────────────────────────────────────
|
|
35
|
+
// Only declare the exact selectors used in this file; the linker resolves the
|
|
36
|
+
// real Swift symbols at link time. Adding new Swift methods requires adding
|
|
37
|
+
// the corresponding declaration here.
|
|
38
|
+
|
|
39
|
+
@interface PdfViewerTurboModuleImpl : NSObject
|
|
40
|
+
|
|
41
|
+
+ (BOOL)requiresMainQueueSetup;
|
|
42
|
+
|
|
43
|
+
- (void)openViewerWithUri:(NSString *)uri
|
|
44
|
+
pageIndex:(double)pageIndex
|
|
45
|
+
defaultZoom:(double)defaultZoom
|
|
46
|
+
onFullScreenChange:(void (^)(BOOL))onFullScreenChange
|
|
47
|
+
onZoomChange:(void (^)(float))onZoomChange
|
|
48
|
+
onDismiss:(void (^)(void))onDismiss;
|
|
49
|
+
|
|
50
|
+
- (void)dismissViewer;
|
|
51
|
+
|
|
52
|
+
- (NSInteger)getPageCountWithUri:(NSString *)uri;
|
|
53
|
+
|
|
54
|
+
- (NSDictionary * _Nullable)createPdfWithPages:(NSArray<NSDictionary *> *)pages
|
|
55
|
+
widthPx:(double)widthPx
|
|
56
|
+
heightPx:(double)heightPx;
|
|
57
|
+
|
|
58
|
+
@end
|
|
59
|
+
|
|
60
|
+
// ── RCT_NEW_ARCH_ENABLED guard ────────────────────────────────────────────────
|
|
61
|
+
// Allows the same file to compile for both old and new architecture.
|
|
62
|
+
// RN 0.74+ enables new arch by default; the guard keeps migration safe.
|
|
63
|
+
|
|
64
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
65
|
+
#import <PdfViewerSpecs/PdfViewerSpecs.h>
|
|
66
|
+
|
|
67
|
+
@interface PdfViewerTurboModule : RCTEventEmitter <NativePdfViewerModuleSpec>
|
|
68
|
+
@end
|
|
69
|
+
|
|
70
|
+
#else
|
|
71
|
+
|
|
72
|
+
@interface PdfViewerTurboModule : RCTEventEmitter <RCTBridgeModule>
|
|
73
|
+
@end
|
|
74
|
+
|
|
75
|
+
#endif
|
|
76
|
+
|
|
77
|
+
// ── Implementation ────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
@implementation PdfViewerTurboModule {
|
|
80
|
+
PdfViewerTurboModuleImpl *_impl;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
RCT_EXPORT_MODULE(NativePdfViewerModule)
|
|
84
|
+
|
|
85
|
+
- (instancetype)init {
|
|
86
|
+
if (self = [super init]) {
|
|
87
|
+
_impl = [[PdfViewerTurboModuleImpl alloc] init];
|
|
88
|
+
}
|
|
89
|
+
return self;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
+ (BOOL)requiresMainQueueSetup { return NO; }
|
|
93
|
+
|
|
94
|
+
// Events emitted to JS (matches usePdfViewer.ts subscriptions)
|
|
95
|
+
- (NSArray<NSString *> *)supportedEvents {
|
|
96
|
+
return @[
|
|
97
|
+
@"PdfFullScreenChanged",
|
|
98
|
+
@"PdfZoomChanged",
|
|
99
|
+
@"PdfViewerDismissed",
|
|
100
|
+
@"PdfViewerError",
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── openPdfViewer ─────────────────────────────────────────────────────────────
|
|
105
|
+
// Dispatches to the main thread; Swift impl presents PdfViewerViewController.
|
|
106
|
+
|
|
107
|
+
RCT_EXPORT_METHOD(openPdfViewer:(NSString *)uri
|
|
108
|
+
pageIndex:(double)pageIndex
|
|
109
|
+
defaultZoom:(double)defaultZoom)
|
|
110
|
+
{
|
|
111
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
112
|
+
[self->_impl openViewerWithUri:uri
|
|
113
|
+
pageIndex:pageIndex
|
|
114
|
+
defaultZoom:defaultZoom
|
|
115
|
+
onFullScreenChange:^(BOOL isFS) {
|
|
116
|
+
[self sendEventWithName:@"PdfFullScreenChanged"
|
|
117
|
+
body:@{@"isFullScreen": @(isFS)}];
|
|
118
|
+
}
|
|
119
|
+
onZoomChange:^(float zoom) {
|
|
120
|
+
[self sendEventWithName:@"PdfZoomChanged"
|
|
121
|
+
body:@{@"zoomPercentage": @(zoom)}];
|
|
122
|
+
}
|
|
123
|
+
onDismiss:^{
|
|
124
|
+
[self sendEventWithName:@"PdfViewerDismissed" body:@{}];
|
|
125
|
+
}];
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── dismissPdfViewer ──────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
RCT_EXPORT_METHOD(dismissPdfViewer)
|
|
132
|
+
{
|
|
133
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
134
|
+
[self->_impl dismissViewer];
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── getPdfPageCount ───────────────────────────────────────────────────────────
|
|
139
|
+
// File I/O happens on a background queue; promise is resolved/rejected there.
|
|
140
|
+
|
|
141
|
+
RCT_EXPORT_METHOD(getPdfPageCount:(NSString *)uri
|
|
142
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
143
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
144
|
+
{
|
|
145
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
146
|
+
NSInteger count = [self->_impl getPageCountWithUri:uri];
|
|
147
|
+
if (count > 0) {
|
|
148
|
+
resolve(@(count));
|
|
149
|
+
} else {
|
|
150
|
+
reject(@"PDF_ERROR", @"Could not open PDF or file has 0 pages", nil);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── createMultiPagePdfBase64 ──────────────────────────────────────────────────
|
|
156
|
+
// PDF generation is CPU-bound; run on a background queue.
|
|
157
|
+
|
|
158
|
+
RCT_EXPORT_METHOD(createMultiPagePdfBase64:(NSArray<NSDictionary *> *)pages
|
|
159
|
+
widthPx:(double)widthPx
|
|
160
|
+
heightPx:(double)heightPx
|
|
161
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
162
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
163
|
+
{
|
|
164
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
165
|
+
NSDictionary *result = [self->_impl createPdfWithPages:pages
|
|
166
|
+
widthPx:widthPx
|
|
167
|
+
heightPx:heightPx];
|
|
168
|
+
if (result) {
|
|
169
|
+
resolve(result);
|
|
170
|
+
} else {
|
|
171
|
+
reject(@"PDF_CREATE_ERROR", @"Failed to generate PDF", nil);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── New Architecture JSI bridge ───────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
179
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
180
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
181
|
+
{
|
|
182
|
+
return std::make_shared<facebook::react::NativePdfViewerModuleSpecJSI>(params);
|
|
183
|
+
}
|
|
184
|
+
#endif
|
|
185
|
+
|
|
186
|
+
@end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Pure Swift business logic for the PDF viewer TurboModule.
|
|
4
|
+
///
|
|
5
|
+
/// The ObjC++ bridge (PdfViewerTurboModule.mm) forward-declares this class
|
|
6
|
+
/// and calls into it. All @objc annotations use explicit ObjC selector names
|
|
7
|
+
/// to guarantee the bridge calls the correct method regardless of Swift's
|
|
8
|
+
/// automatic ObjC name generation rules.
|
|
9
|
+
///
|
|
10
|
+
/// Threading contract:
|
|
11
|
+
/// - openViewer / dismissViewer must be called on the MAIN thread by the bridge.
|
|
12
|
+
/// - getPageCount / createPdf are synchronous and must be called on a
|
|
13
|
+
/// BACKGROUND thread by the bridge, which wraps them in promise blocks.
|
|
14
|
+
@objc(PdfViewerTurboModuleImpl)
|
|
15
|
+
final class PdfViewerTurboModuleImpl: NSObject {
|
|
16
|
+
|
|
17
|
+
// Weak reference so ARC cleans up when the user dismisses
|
|
18
|
+
private weak var visibleVC: PdfViewerViewController?
|
|
19
|
+
|
|
20
|
+
@objc static func requiresMainQueueSetup() -> Bool { false }
|
|
21
|
+
|
|
22
|
+
// MARK: - openViewer (main thread)
|
|
23
|
+
|
|
24
|
+
/// Present PdfViewerViewController over the current key window root VC.
|
|
25
|
+
/// Selector: openViewerWithUri:pageIndex:defaultZoom:onFullScreenChange:onZoomChange:onDismiss:
|
|
26
|
+
@objc(openViewerWithUri:pageIndex:defaultZoom:onFullScreenChange:onZoomChange:onDismiss:)
|
|
27
|
+
func openViewer(
|
|
28
|
+
uri: String,
|
|
29
|
+
pageIndex: Double,
|
|
30
|
+
defaultZoom: Double,
|
|
31
|
+
onFullScreenChange: @escaping (Bool) -> Void,
|
|
32
|
+
onZoomChange: @escaping (Float) -> Void,
|
|
33
|
+
onDismiss: @escaping () -> Void
|
|
34
|
+
) {
|
|
35
|
+
guard let presenter = topPresentedViewController() else { return }
|
|
36
|
+
|
|
37
|
+
let vc = PdfViewerViewController.make(
|
|
38
|
+
uri: uri,
|
|
39
|
+
pageIndex: Int(pageIndex),
|
|
40
|
+
defaultZoom: Float(defaultZoom)
|
|
41
|
+
)
|
|
42
|
+
vc.onFullScreenChange = { fs in onFullScreenChange(fs) }
|
|
43
|
+
vc.onZoomChange = { pct in onZoomChange(pct) }
|
|
44
|
+
vc.onDismiss = { onDismiss() }
|
|
45
|
+
vc.modalPresentationStyle = .overFullScreen
|
|
46
|
+
|
|
47
|
+
presenter.present(vc, animated: true)
|
|
48
|
+
visibleVC = vc
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// MARK: - dismissViewer (main thread)
|
|
52
|
+
|
|
53
|
+
/// Selector: dismissViewer
|
|
54
|
+
@objc(dismissViewer)
|
|
55
|
+
func dismissViewer() {
|
|
56
|
+
visibleVC?.dismiss(animated: true)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// MARK: - getPageCount (background thread, synchronous)
|
|
60
|
+
|
|
61
|
+
/// Returns page count for the given URI.
|
|
62
|
+
/// Selector: getPageCountWithUri:
|
|
63
|
+
@objc(getPageCountWithUri:)
|
|
64
|
+
func getPageCount(uri: String) -> Int {
|
|
65
|
+
PdfRenderingLogic.pageCount(uri: uri)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// MARK: - createPdf (background thread, synchronous)
|
|
69
|
+
|
|
70
|
+
/// Renders structured pages into a PDF and returns base64 + temp file path.
|
|
71
|
+
/// Returns nil on failure (caller should reject the promise).
|
|
72
|
+
/// Selector: createPdfWithPages:widthPx:heightPx:
|
|
73
|
+
@objc(createPdfWithPages:widthPx:heightPx:)
|
|
74
|
+
func createPdf(
|
|
75
|
+
pages: [[String: Any]],
|
|
76
|
+
widthPx: Double,
|
|
77
|
+
heightPx: Double
|
|
78
|
+
) -> [String: Any]? {
|
|
79
|
+
let pageSize = CGSize(width: widthPx, height: heightPx)
|
|
80
|
+
let data = NSMutableData()
|
|
81
|
+
|
|
82
|
+
UIGraphicsBeginPDFContextToData(data, CGRect(origin: .zero, size: pageSize), nil)
|
|
83
|
+
|
|
84
|
+
for page in pages {
|
|
85
|
+
let title = page["title"] as? String ?? ""
|
|
86
|
+
let body = page["body"] as? String ?? ""
|
|
87
|
+
|
|
88
|
+
UIGraphicsBeginPDFPage()
|
|
89
|
+
guard let ctx = UIGraphicsGetCurrentContext() else { continue }
|
|
90
|
+
|
|
91
|
+
// White background
|
|
92
|
+
ctx.setFillColor(UIColor.white.cgColor)
|
|
93
|
+
ctx.fill(CGRect(origin: .zero, size: pageSize))
|
|
94
|
+
|
|
95
|
+
// Title
|
|
96
|
+
let titleAttrs: [NSAttributedString.Key: Any] = [
|
|
97
|
+
.font: UIFont.boldSystemFont(ofSize: 24),
|
|
98
|
+
.foregroundColor: UIColor.black,
|
|
99
|
+
]
|
|
100
|
+
title.draw(at: CGPoint(x: 40, y: 40), withAttributes: titleAttrs)
|
|
101
|
+
|
|
102
|
+
// Body
|
|
103
|
+
let bodyAttrs: [NSAttributedString.Key: Any] = [
|
|
104
|
+
.font: UIFont.systemFont(ofSize: 16),
|
|
105
|
+
.foregroundColor: UIColor.darkGray,
|
|
106
|
+
]
|
|
107
|
+
let bodyRect = CGRect(x: 40, y: 90, width: pageSize.width - 80, height: pageSize.height - 130)
|
|
108
|
+
body.draw(in: bodyRect, withAttributes: bodyAttrs)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
UIGraphicsEndPDFContext()
|
|
112
|
+
|
|
113
|
+
let base64 = data.base64EncodedString(options: [])
|
|
114
|
+
let filename = "generated_\(Int(Date().timeIntervalSince1970)).pdf"
|
|
115
|
+
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
|
|
116
|
+
|
|
117
|
+
do {
|
|
118
|
+
try data.write(to: tempURL, options: .atomic)
|
|
119
|
+
return ["base64": base64, "path": tempURL.path]
|
|
120
|
+
} catch {
|
|
121
|
+
return nil
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// MARK: - Helpers
|
|
126
|
+
|
|
127
|
+
/// Walks the presentation chain to find the topmost presented VC.
|
|
128
|
+
private func topPresentedViewController() -> UIViewController? {
|
|
129
|
+
guard let scene = UIApplication.shared.connectedScenes
|
|
130
|
+
.compactMap({ $0 as? UIWindowScene })
|
|
131
|
+
.first,
|
|
132
|
+
let root = scene.windows.first(where: \.isKeyWindow)?.rootViewController
|
|
133
|
+
else { return nil }
|
|
134
|
+
|
|
135
|
+
var top = root
|
|
136
|
+
while let presented = top.presentedViewController {
|
|
137
|
+
top = presented
|
|
138
|
+
}
|
|
139
|
+
return top
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
// MARK: - Delegate (used by PdfViewerViewController)
|
|
4
|
+
|
|
5
|
+
@objc protocol PdfViewerViewDelegate: AnyObject {
|
|
6
|
+
@objc optional func pdfViewerView(_ view: PdfViewerView, didChangeFullScreen isFullScreen: Bool)
|
|
7
|
+
@objc optional func pdfViewerView(_ view: PdfViewerView, didChangeZoom zoomPercentage: Float)
|
|
8
|
+
@objc optional func pdfViewerViewDidRequestDismiss(_ view: PdfViewerView)
|
|
9
|
+
@objc optional func pdfViewerView(_ view: PdfViewerView, didChangeToRightScreen isRight: Bool)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// MARK: - PdfViewerView
|
|
13
|
+
|
|
14
|
+
/// Main PDF rendering view.
|
|
15
|
+
///
|
|
16
|
+
/// Equivalent to ComposeRenderer.kt on Android.
|
|
17
|
+
/// Hosts a UIScrollView with pinch-to-zoom, a vertical stack of PdfPageViews,
|
|
18
|
+
/// and a PdfToolbarView. All page rendering is performed on background threads.
|
|
19
|
+
///
|
|
20
|
+
/// The @objc block callbacks (`onFullScreenChangeBlock`, etc.) are used by the
|
|
21
|
+
/// Fabric ViewManager (PdfViewerComponentView.mm) to dispatch events to JS.
|
|
22
|
+
/// The delegate protocol is used by PdfViewerViewController for VC-level handling.
|
|
23
|
+
@objc(PdfViewerView)
|
|
24
|
+
final class PdfViewerView: UIView {
|
|
25
|
+
|
|
26
|
+
// MARK: - @objc props (read by Fabric ObjC++ bridge via forward declaration)
|
|
27
|
+
|
|
28
|
+
@objc var pdfUri: String? {
|
|
29
|
+
didSet { guard pdfUri != oldValue else { return }; reloadDocument() }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@objc var initialPageIndex: Int = 0 {
|
|
33
|
+
didSet { if document != nil { scrollToPage(initialPageIndex) } }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@objc var defaultZoom: CGFloat = PdfConstants.defaultZoom {
|
|
37
|
+
didSet { scrollView.zoomScale = defaultZoom }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@objc var isFullScreen: Bool = false {
|
|
41
|
+
didSet {
|
|
42
|
+
toolbar.isHidden = isFullScreen
|
|
43
|
+
toolbarHeightConstraint?.constant = isFullScreen ? 0 : PdfConstants.toolbarHeight
|
|
44
|
+
UIView.animate(withDuration: 0.2) { self.layoutIfNeeded() }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@objc var headerText: String? {
|
|
49
|
+
didSet { toolbar.headerText = headerText }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@objc var backButtonText: String? {
|
|
53
|
+
didSet { toolbar.backButtonText = backButtonText }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// MARK: - @objc block callbacks (wired by Fabric manager and ViewController)
|
|
57
|
+
|
|
58
|
+
@objc var onFullScreenChangeBlock: ((Bool) -> Void)?
|
|
59
|
+
@objc var onZoomChangeBlock: ((Float) -> Void)?
|
|
60
|
+
@objc var onLeftScreenChangeBlock: ((Bool) -> Void)?
|
|
61
|
+
@objc var onRightScreenChangeBlock: ((Bool) -> Void)?
|
|
62
|
+
|
|
63
|
+
// MARK: - Delegate (optional, for ViewController usage)
|
|
64
|
+
|
|
65
|
+
@objc weak var delegate: PdfViewerViewDelegate?
|
|
66
|
+
|
|
67
|
+
// MARK: - Private state
|
|
68
|
+
|
|
69
|
+
private var document: CGPDFDocument?
|
|
70
|
+
private var pageViews: [PdfPageView] = []
|
|
71
|
+
|
|
72
|
+
private let scrollView = UIScrollView()
|
|
73
|
+
private let pageStack = UIStackView()
|
|
74
|
+
private let toolbar = PdfToolbarView()
|
|
75
|
+
private var toolbarHeightConstraint: NSLayoutConstraint?
|
|
76
|
+
|
|
77
|
+
// MARK: - Init
|
|
78
|
+
|
|
79
|
+
override init(frame: CGRect) {
|
|
80
|
+
super.init(frame: frame)
|
|
81
|
+
setupUI()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
required init?(coder: NSCoder) { fatalError("Use init(frame:)") }
|
|
85
|
+
|
|
86
|
+
// MARK: - Layout
|
|
87
|
+
|
|
88
|
+
override func layoutSubviews() {
|
|
89
|
+
super.layoutSubviews()
|
|
90
|
+
// Rebuild pages when first laid out with a real size
|
|
91
|
+
if !pageViews.isEmpty { return }
|
|
92
|
+
if document != nil { buildPageViews() }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// MARK: - UI Setup
|
|
96
|
+
|
|
97
|
+
private func setupUI() {
|
|
98
|
+
backgroundColor = .white
|
|
99
|
+
|
|
100
|
+
// ── Toolbar ──────────────────────────────────────────────────────
|
|
101
|
+
toolbar.delegate = self
|
|
102
|
+
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
|
103
|
+
addSubview(toolbar)
|
|
104
|
+
|
|
105
|
+
let toolbarH = toolbar.heightAnchor.constraint(equalToConstant: PdfConstants.toolbarHeight)
|
|
106
|
+
toolbarHeightConstraint = toolbarH
|
|
107
|
+
|
|
108
|
+
// ── ScrollView ───────────────────────────────────────────────────
|
|
109
|
+
scrollView.delegate = self
|
|
110
|
+
scrollView.minimumZoomScale = PdfConstants.minZoom
|
|
111
|
+
scrollView.maximumZoomScale = PdfConstants.maxZoom
|
|
112
|
+
scrollView.showsVerticalScrollIndicator = true
|
|
113
|
+
scrollView.showsHorizontalScrollIndicator = false
|
|
114
|
+
scrollView.contentInsetAdjustmentBehavior = .never
|
|
115
|
+
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
116
|
+
addSubview(scrollView)
|
|
117
|
+
|
|
118
|
+
// ── Page stack ───────────────────────────────────────────────────
|
|
119
|
+
pageStack.axis = .vertical
|
|
120
|
+
pageStack.spacing = PdfConstants.pageSpacing
|
|
121
|
+
pageStack.alignment = .center
|
|
122
|
+
pageStack.translatesAutoresizingMaskIntoConstraints = false
|
|
123
|
+
scrollView.addSubview(pageStack)
|
|
124
|
+
|
|
125
|
+
// ── Constraints ──────────────────────────────────────────────────
|
|
126
|
+
NSLayoutConstraint.activate([
|
|
127
|
+
scrollView.topAnchor.constraint(equalTo: topAnchor),
|
|
128
|
+
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
129
|
+
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
130
|
+
scrollView.bottomAnchor.constraint(equalTo: toolbar.topAnchor),
|
|
131
|
+
|
|
132
|
+
toolbar.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
133
|
+
toolbar.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
134
|
+
toolbar.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
|
|
135
|
+
toolbarH,
|
|
136
|
+
|
|
137
|
+
pageStack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 16),
|
|
138
|
+
pageStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
|
|
139
|
+
pageStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
|
|
140
|
+
pageStack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -16),
|
|
141
|
+
// The stack width is pinned to the scroll view's frame (not content) width,
|
|
142
|
+
// so horizontal overflow is impossible — only vertical scrolling occurs.
|
|
143
|
+
pageStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
|
|
144
|
+
])
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// MARK: - Document loading
|
|
148
|
+
|
|
149
|
+
private func reloadDocument() {
|
|
150
|
+
cancelAllPageLoads()
|
|
151
|
+
pageViews.removeAll()
|
|
152
|
+
pageStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
|
153
|
+
document = nil
|
|
154
|
+
|
|
155
|
+
guard let uri = pdfUri, !uri.isEmpty else { return }
|
|
156
|
+
|
|
157
|
+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
158
|
+
let doc = PdfRenderingLogic.openDocument(uri: uri)
|
|
159
|
+
DispatchQueue.main.async { [weak self] in
|
|
160
|
+
guard let self, let doc else { return }
|
|
161
|
+
self.document = doc
|
|
162
|
+
// Only build page views once we have a real layout size.
|
|
163
|
+
// layoutSubviews will trigger buildPageViews if bounds == .zero here.
|
|
164
|
+
if self.bounds.width > 0 {
|
|
165
|
+
self.buildPageViews()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private func buildPageViews() {
|
|
172
|
+
guard let document else { return }
|
|
173
|
+
|
|
174
|
+
let containerWidth = bounds.width > 0 ? bounds.width : UIScreen.main.bounds.width
|
|
175
|
+
let pageWidth = containerWidth - PdfConstants.pageHorizontalPadding * 2
|
|
176
|
+
|
|
177
|
+
for i in 0..<document.numberOfPages {
|
|
178
|
+
let pageView = PdfPageView()
|
|
179
|
+
pageView.pageIndex = i
|
|
180
|
+
let ratio = PdfRenderingLogic.pageAspectRatio(document: document, pageIndex: i)
|
|
181
|
+
let pageHeight = ratio > 0 ? pageWidth / ratio : pageWidth * 1.414
|
|
182
|
+
|
|
183
|
+
pageView.translatesAutoresizingMaskIntoConstraints = false
|
|
184
|
+
pageStack.addArrangedSubview(pageView)
|
|
185
|
+
NSLayoutConstraint.activate([
|
|
186
|
+
pageView.widthAnchor.constraint(equalToConstant: pageWidth),
|
|
187
|
+
pageView.heightAnchor.constraint(equalToConstant: pageHeight),
|
|
188
|
+
])
|
|
189
|
+
pageViews.append(pageView)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
renderVisiblePages()
|
|
193
|
+
scrollToPage(initialPageIndex)
|
|
194
|
+
scrollView.zoomScale = defaultZoom
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// MARK: - Rendering
|
|
198
|
+
|
|
199
|
+
private func renderVisiblePages() {
|
|
200
|
+
guard let document else { return }
|
|
201
|
+
let targetWidth = bounds.width > 0 ? bounds.width : UIScreen.main.bounds.width
|
|
202
|
+
pageViews.forEach { $0.load(document: document, targetWidth: targetWidth) }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private func cancelAllPageLoads() {
|
|
206
|
+
pageViews.forEach { $0.cancelLoad() }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// MARK: - Scroll
|
|
210
|
+
|
|
211
|
+
func scrollToPage(_ index: Int) {
|
|
212
|
+
guard index >= 0, index < pageViews.count else { return }
|
|
213
|
+
let pv = pageViews[index]
|
|
214
|
+
let frame = pv.convert(pv.bounds, to: scrollView)
|
|
215
|
+
scrollView.scrollRectToVisible(frame, animated: false)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// MARK: - UIScrollViewDelegate
|
|
220
|
+
|
|
221
|
+
extension PdfViewerView: UIScrollViewDelegate {
|
|
222
|
+
|
|
223
|
+
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
|
224
|
+
pageStack
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
|
228
|
+
let pct = Float(scrollView.zoomScale * 100)
|
|
229
|
+
onZoomChangeBlock?(pct)
|
|
230
|
+
delegate?.pdfViewerView?(self, didChangeZoom: pct)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
234
|
+
renderVisiblePages()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
238
|
+
if !decelerate { renderVisiblePages() }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// MARK: - PdfToolbarViewDelegate
|
|
243
|
+
|
|
244
|
+
extension PdfViewerView: PdfToolbarViewDelegate {
|
|
245
|
+
|
|
246
|
+
func toolbarDidTapBack(_ toolbar: PdfToolbarView) {
|
|
247
|
+
onLeftScreenChangeBlock?(true)
|
|
248
|
+
delegate?.pdfViewerViewDidRequestDismiss?(self)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
func toolbarDidTapZoomIn(_ toolbar: PdfToolbarView) {
|
|
252
|
+
let next = min(scrollView.zoomScale * PdfConstants.zoomStep, PdfConstants.maxZoom)
|
|
253
|
+
scrollView.setZoomScale(next, animated: true)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
func toolbarDidTapZoomOut(_ toolbar: PdfToolbarView) {
|
|
257
|
+
let next = max(scrollView.zoomScale / PdfConstants.zoomStep, PdfConstants.minZoom)
|
|
258
|
+
scrollView.setZoomScale(next, animated: true)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
func toolbarDidTapFullScreen(_ toolbar: PdfToolbarView) {
|
|
262
|
+
let next = !isFullScreen
|
|
263
|
+
isFullScreen = next
|
|
264
|
+
toolbar.isFullScreen = next
|
|
265
|
+
onFullScreenChangeBlock?(next)
|
|
266
|
+
delegate?.pdfViewerView?(self, didChangeFullScreen: next)
|
|
267
|
+
}
|
|
268
|
+
}
|