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,38 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
// MARK: - Scroll direction
|
|
4
|
+
|
|
5
|
+
enum PdfScrollDirection: String {
|
|
6
|
+
case vertical
|
|
7
|
+
case horizontal
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// MARK: - Shared constants
|
|
11
|
+
|
|
12
|
+
enum PdfConstants {
|
|
13
|
+
/// Minimum allowed zoom scale (50%)
|
|
14
|
+
static let minZoom: CGFloat = 0.5
|
|
15
|
+
/// Maximum allowed zoom scale (500%)
|
|
16
|
+
static let maxZoom: CGFloat = 5.0
|
|
17
|
+
/// Default initial zoom (150%)
|
|
18
|
+
static let defaultZoom: CGFloat = 1.5
|
|
19
|
+
/// Vertical gap between consecutive pages
|
|
20
|
+
static let pageSpacing: CGFloat = 12.0
|
|
21
|
+
/// Horizontal padding around each page
|
|
22
|
+
static let pageHorizontalPadding: CGFloat = 16.0
|
|
23
|
+
/// Multiplier applied to target width before rendering.
|
|
24
|
+
/// Retina screen (2×) × 1.5 = 3× physical pixels → crisp at maximum zoom.
|
|
25
|
+
static let renderScale: CGFloat = UIScreen.main.scale * 1.5
|
|
26
|
+
/// Max number of page images kept in NSCache
|
|
27
|
+
static let cacheCountLimit = 20
|
|
28
|
+
/// Max total bytes in NSCache (50 MB)
|
|
29
|
+
static let cacheTotalCostLimit = 50 * 1024 * 1024
|
|
30
|
+
/// Fixed height of the toolbar
|
|
31
|
+
static let toolbarHeight: CGFloat = 52.0
|
|
32
|
+
/// Corner radius applied to each page card
|
|
33
|
+
static let cornerRadius: CGFloat = 4.0
|
|
34
|
+
/// Drop-shadow opacity on each page card
|
|
35
|
+
static let pageShadowOpacity: Float = 0.15
|
|
36
|
+
/// Step factor for each tap of + / – zoom button
|
|
37
|
+
static let zoomStep: CGFloat = 1.25
|
|
38
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// UIView that displays a single rendered PDF page.
|
|
4
|
+
///
|
|
5
|
+
/// Rendering is always performed on a background queue; the result is
|
|
6
|
+
/// cached in PdfCacheManager and applied on the main thread.
|
|
7
|
+
/// Cancelling an in-flight render is safe — the DispatchWorkItem is
|
|
8
|
+
/// checked for cancellation before the result is applied.
|
|
9
|
+
final class PdfPageView: UIView {
|
|
10
|
+
|
|
11
|
+
// MARK: - Public
|
|
12
|
+
|
|
13
|
+
/// 0-based index of the page this view represents.
|
|
14
|
+
var pageIndex: Int = 0
|
|
15
|
+
|
|
16
|
+
// MARK: - Private
|
|
17
|
+
|
|
18
|
+
private let imageView = UIImageView()
|
|
19
|
+
private let spinner = UIActivityIndicatorView(style: .medium)
|
|
20
|
+
private var renderTask: DispatchWorkItem?
|
|
21
|
+
|
|
22
|
+
// MARK: - Init
|
|
23
|
+
|
|
24
|
+
override init(frame: CGRect) {
|
|
25
|
+
super.init(frame: frame)
|
|
26
|
+
setup()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
required init?(coder: NSCoder) { fatalError("Use init(frame:)") }
|
|
30
|
+
|
|
31
|
+
// MARK: - Setup
|
|
32
|
+
|
|
33
|
+
private func setup() {
|
|
34
|
+
backgroundColor = .white
|
|
35
|
+
clipsToBounds = true
|
|
36
|
+
|
|
37
|
+
imageView.contentMode = .scaleAspectFit
|
|
38
|
+
imageView.backgroundColor = .white
|
|
39
|
+
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
40
|
+
addSubview(imageView)
|
|
41
|
+
|
|
42
|
+
spinner.translatesAutoresizingMaskIntoConstraints = false
|
|
43
|
+
addSubview(spinner)
|
|
44
|
+
|
|
45
|
+
NSLayoutConstraint.activate([
|
|
46
|
+
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
47
|
+
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
48
|
+
imageView.topAnchor.constraint(equalTo: topAnchor),
|
|
49
|
+
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
50
|
+
|
|
51
|
+
spinner.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
52
|
+
spinner.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
53
|
+
])
|
|
54
|
+
spinner.startAnimating()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// MARK: - Loading
|
|
58
|
+
|
|
59
|
+
/// Loads and renders the page at `pageIndex` using `document`.
|
|
60
|
+
///
|
|
61
|
+
/// - Parameters:
|
|
62
|
+
/// - document: An open CGPDFDocument (kept alive by the caller).
|
|
63
|
+
/// - targetWidth: Logical width of the container; rendering happens
|
|
64
|
+
/// at `targetWidth × PdfConstants.renderScale` for retina.
|
|
65
|
+
func load(document: CGPDFDocument, targetWidth: CGFloat) {
|
|
66
|
+
let cacheKey = PdfCacheManager.key(pageIndex: pageIndex, targetWidth: targetWidth)
|
|
67
|
+
|
|
68
|
+
// Serve from cache immediately if available
|
|
69
|
+
if let cached = PdfCacheManager.shared.image(forKey: cacheKey) {
|
|
70
|
+
imageView.image = cached
|
|
71
|
+
spinner.stopAnimating()
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Cancel any previous render for this view
|
|
76
|
+
renderTask?.cancel()
|
|
77
|
+
renderTask = nil
|
|
78
|
+
|
|
79
|
+
// Capture locals so the closure doesn't hold self strongly during rendering
|
|
80
|
+
let idx = pageIndex
|
|
81
|
+
let renderWidth = targetWidth * PdfConstants.renderScale
|
|
82
|
+
|
|
83
|
+
var item: DispatchWorkItem!
|
|
84
|
+
item = DispatchWorkItem { [weak self] in
|
|
85
|
+
guard !item.isCancelled else { return }
|
|
86
|
+
|
|
87
|
+
let image = PdfRenderingLogic.renderPage(
|
|
88
|
+
document: document,
|
|
89
|
+
pageIndex: idx,
|
|
90
|
+
targetWidth: renderWidth
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if let image {
|
|
94
|
+
PdfCacheManager.shared.setImage(image, forKey: cacheKey)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
DispatchQueue.main.async { [weak self] in
|
|
98
|
+
guard let self, !item.isCancelled else { return }
|
|
99
|
+
self.imageView.image = image
|
|
100
|
+
self.spinner.stopAnimating()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
renderTask = item
|
|
105
|
+
DispatchQueue.global(qos: .userInitiated).async(execute: item)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Cancels any in-flight render task.
|
|
109
|
+
func cancelLoad() {
|
|
110
|
+
renderTask?.cancel()
|
|
111
|
+
renderTask = nil
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// MARK: - Reuse
|
|
115
|
+
|
|
116
|
+
func prepareForReuse() {
|
|
117
|
+
cancelLoad()
|
|
118
|
+
imageView.image = nil
|
|
119
|
+
spinner.startAnimating()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import CoreGraphics
|
|
3
|
+
|
|
4
|
+
/// Stateless PDF rendering utilities using CoreGraphics (CGPDFDocument).
|
|
5
|
+
///
|
|
6
|
+
/// All rendering methods are safe to call from a background thread.
|
|
7
|
+
/// CGPDFDocument is thread-safe for concurrent reads once opened.
|
|
8
|
+
///
|
|
9
|
+
/// Why CoreGraphics instead of PDFKit?
|
|
10
|
+
/// CGPDFDocument gives direct access to the page bitmap, letting us
|
|
11
|
+
/// control output resolution (PdfConstants.renderScale) and feed the
|
|
12
|
+
/// result into a custom UIScrollView. PDFKit's PDFView is great for
|
|
13
|
+
/// simple embed cases but harder to customise for split-view or
|
|
14
|
+
/// controlled-zoom layouts.
|
|
15
|
+
enum PdfRenderingLogic {
|
|
16
|
+
|
|
17
|
+
// MARK: - Document opening
|
|
18
|
+
|
|
19
|
+
/// Opens a CGPDFDocument from a file URL.
|
|
20
|
+
/// Returns nil if the URL is unreachable or the file is not a valid PDF.
|
|
21
|
+
static func openDocument(url: URL) -> CGPDFDocument? {
|
|
22
|
+
CGPDFDocument(url as CFURL)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Convenience: opens a document from a string that may be either a
|
|
26
|
+
/// file:// URI or a plain filesystem path.
|
|
27
|
+
static func openDocument(uri: String) -> CGPDFDocument? {
|
|
28
|
+
openDocument(url: fileURL(from: uri))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// MARK: - Page count
|
|
32
|
+
|
|
33
|
+
/// Synchronously returns total page count without rendering any content.
|
|
34
|
+
/// Safe to call from any thread.
|
|
35
|
+
static func pageCount(url: URL) -> Int {
|
|
36
|
+
CGPDFDocument(url as CFURL)?.numberOfPages ?? 0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static func pageCount(uri: String) -> Int {
|
|
40
|
+
pageCount(url: fileURL(from: uri))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// MARK: - Page rendering
|
|
44
|
+
|
|
45
|
+
/// Renders a single PDF page to a UIImage.
|
|
46
|
+
///
|
|
47
|
+
/// - Parameters:
|
|
48
|
+
/// - document: Open CGPDFDocument.
|
|
49
|
+
/// - pageIndex: 0-based page index.
|
|
50
|
+
/// - targetWidth: Desired pixel width of the output image.
|
|
51
|
+
/// Pass `view.bounds.width * PdfConstants.renderScale`
|
|
52
|
+
/// for retina-quality output.
|
|
53
|
+
/// - Returns: Rendered UIImage, or nil if pageIndex is out of range.
|
|
54
|
+
static func renderPage(
|
|
55
|
+
document: CGPDFDocument,
|
|
56
|
+
pageIndex: Int,
|
|
57
|
+
targetWidth: CGFloat
|
|
58
|
+
) -> UIImage? {
|
|
59
|
+
// CGPDFDocument is 1-based
|
|
60
|
+
guard
|
|
61
|
+
pageIndex >= 0,
|
|
62
|
+
pageIndex < document.numberOfPages,
|
|
63
|
+
let page = document.page(at: pageIndex + 1)
|
|
64
|
+
else { return nil }
|
|
65
|
+
|
|
66
|
+
let mediaBox = page.getBoxRect(.mediaBox)
|
|
67
|
+
guard mediaBox.width > 0 else { return nil }
|
|
68
|
+
|
|
69
|
+
let scale = targetWidth / mediaBox.width
|
|
70
|
+
let size = CGSize(width: targetWidth, height: mediaBox.height * scale)
|
|
71
|
+
|
|
72
|
+
let renderer = UIGraphicsImageRenderer(size: size)
|
|
73
|
+
return renderer.image { ctx in
|
|
74
|
+
let cgCtx = ctx.cgContext
|
|
75
|
+
|
|
76
|
+
// White background — PDFs may have transparent backgrounds
|
|
77
|
+
cgCtx.setFillColor(UIColor.white.cgColor)
|
|
78
|
+
cgCtx.fill(CGRect(origin: .zero, size: size))
|
|
79
|
+
|
|
80
|
+
// CGPDFPage origin is bottom-left; UIKit origin is top-left.
|
|
81
|
+
// Flip the coordinate system vertically before drawing.
|
|
82
|
+
cgCtx.translateBy(x: 0, y: size.height)
|
|
83
|
+
cgCtx.scaleBy(x: scale, y: -scale)
|
|
84
|
+
cgCtx.drawPDFPage(page)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// MARK: - Aspect ratio
|
|
89
|
+
|
|
90
|
+
/// Returns width/height aspect ratio for a given page (for layout).
|
|
91
|
+
/// Falls back to A4 ratio (0.707) if the page cannot be opened.
|
|
92
|
+
static func pageAspectRatio(document: CGPDFDocument, pageIndex: Int) -> CGFloat {
|
|
93
|
+
guard let page = document.page(at: pageIndex + 1) else { return 0.707 }
|
|
94
|
+
let box = page.getBoxRect(.mediaBox)
|
|
95
|
+
return box.height > 0 ? box.width / box.height : 0.707
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// MARK: - URI helpers
|
|
99
|
+
|
|
100
|
+
/// Converts a string that is either a file:// URI or a plain path to URL.
|
|
101
|
+
static func fileURL(from uri: String) -> URL {
|
|
102
|
+
if uri.hasPrefix("file://") {
|
|
103
|
+
return URL(string: uri) ?? URL(fileURLWithPath: uri)
|
|
104
|
+
}
|
|
105
|
+
return URL(fileURLWithPath: uri)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
// MARK: - Delegate
|
|
4
|
+
|
|
5
|
+
protocol PdfToolbarViewDelegate: AnyObject {
|
|
6
|
+
func toolbarDidTapBack(_ toolbar: PdfToolbarView)
|
|
7
|
+
func toolbarDidTapZoomIn(_ toolbar: PdfToolbarView)
|
|
8
|
+
func toolbarDidTapZoomOut(_ toolbar: PdfToolbarView)
|
|
9
|
+
func toolbarDidTapFullScreen(_ toolbar: PdfToolbarView)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// MARK: - PdfToolbarView
|
|
13
|
+
|
|
14
|
+
/// A fixed-height toolbar that mirrors the Android PdfViewerToolbar behaviour.
|
|
15
|
+
///
|
|
16
|
+
/// Layout (left → right):
|
|
17
|
+
/// [← Back] [spacer] [Title] [spacer] [−] [+] [⛶]
|
|
18
|
+
final class PdfToolbarView: UIView {
|
|
19
|
+
|
|
20
|
+
// MARK: Public
|
|
21
|
+
|
|
22
|
+
weak var delegate: PdfToolbarViewDelegate?
|
|
23
|
+
|
|
24
|
+
var isFullScreen: Bool = false {
|
|
25
|
+
didSet {
|
|
26
|
+
fullScreenButton.setTitle(isFullScreen ? "⊡" : "⛶", for: .normal)
|
|
27
|
+
fullScreenButton.backgroundColor = isFullScreen
|
|
28
|
+
? UIColor(red: 1.0, green: 0.93, blue: 0.93, alpha: 1.0)
|
|
29
|
+
: UIColor(red: 0.93, green: 0.96, blue: 1.0, alpha: 1.0)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var headerText: String? {
|
|
34
|
+
didSet { titleLabel.text = headerText }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
var backButtonText: String? {
|
|
38
|
+
didSet { backButton.setTitle(backButtonText ?? "Back", for: .normal) }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Custom icon images (optional — falls back to text glyphs when nil)
|
|
42
|
+
var zoomInImage: UIImage? { didSet { applyIcon(zoomInImage, to: zoomInButton, fallback: "+") } }
|
|
43
|
+
var zoomOutImage: UIImage? { didSet { applyIcon(zoomOutImage, to: zoomOutButton, fallback: "−") } }
|
|
44
|
+
var fullScreenImage: UIImage? { didSet { applyIcon(fullScreenImage, to: fullScreenButton, fallback: "⛶") } }
|
|
45
|
+
|
|
46
|
+
// MARK: - Private UI
|
|
47
|
+
|
|
48
|
+
private let backButton = UIButton(type: .system)
|
|
49
|
+
private let titleLabel = UILabel()
|
|
50
|
+
private let zoomOutButton = UIButton(type: .system)
|
|
51
|
+
private let zoomInButton = UIButton(type: .system)
|
|
52
|
+
private let fullScreenButton = UIButton(type: .system)
|
|
53
|
+
|
|
54
|
+
// MARK: - Init
|
|
55
|
+
|
|
56
|
+
override init(frame: CGRect) {
|
|
57
|
+
super.init(frame: frame)
|
|
58
|
+
setup()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
required init?(coder: NSCoder) { fatalError("Use init(frame:)") }
|
|
62
|
+
|
|
63
|
+
// MARK: - Setup
|
|
64
|
+
|
|
65
|
+
private func setup() {
|
|
66
|
+
backgroundColor = .white
|
|
67
|
+
|
|
68
|
+
// Top border line instead of shadow
|
|
69
|
+
let separator = UIView()
|
|
70
|
+
separator.backgroundColor = UIColor(white: 0.88, alpha: 1)
|
|
71
|
+
separator.translatesAutoresizingMaskIntoConstraints = false
|
|
72
|
+
addSubview(separator)
|
|
73
|
+
NSLayoutConstraint.activate([
|
|
74
|
+
separator.topAnchor.constraint(equalTo: topAnchor),
|
|
75
|
+
separator.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
76
|
+
separator.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
77
|
+
separator.heightAnchor.constraint(equalToConstant: 0.5),
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
// Back button
|
|
81
|
+
backButton.setTitle("Back", for: .normal)
|
|
82
|
+
backButton.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
|
|
83
|
+
backButton.setTitleColor(UIColor.systemBlue, for: .normal)
|
|
84
|
+
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
|
|
85
|
+
backButton.setContentHuggingPriority(.required, for: .horizontal)
|
|
86
|
+
|
|
87
|
+
// Title
|
|
88
|
+
titleLabel.text = ""
|
|
89
|
+
titleLabel.font = .systemFont(ofSize: 17, weight: .semibold)
|
|
90
|
+
titleLabel.textColor = UIColor(red: 0.10, green: 0.10, blue: 0.18, alpha: 1)
|
|
91
|
+
titleLabel.textAlignment = .center
|
|
92
|
+
titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
93
|
+
|
|
94
|
+
// Zoom out — styled icon button
|
|
95
|
+
styleIconButton(zoomOutButton, symbol: "−", fontSize: 26)
|
|
96
|
+
zoomOutButton.addTarget(self, action: #selector(zoomOutTapped), for: .touchUpInside)
|
|
97
|
+
|
|
98
|
+
// Zoom in — styled icon button
|
|
99
|
+
styleIconButton(zoomInButton, symbol: "+", fontSize: 26)
|
|
100
|
+
zoomInButton.addTarget(self, action: #selector(zoomInTapped), for: .touchUpInside)
|
|
101
|
+
|
|
102
|
+
// Full-screen toggle — styled icon button
|
|
103
|
+
styleIconButton(fullScreenButton, symbol: "⛶", fontSize: 22)
|
|
104
|
+
fullScreenButton.addTarget(self, action: #selector(fullScreenTapped), for: .touchUpInside)
|
|
105
|
+
|
|
106
|
+
let stack = UIStackView(arrangedSubviews: [
|
|
107
|
+
backButton,
|
|
108
|
+
UIView(), // flexible spacer
|
|
109
|
+
titleLabel,
|
|
110
|
+
UIView(), // flexible spacer
|
|
111
|
+
zoomOutButton,
|
|
112
|
+
zoomInButton,
|
|
113
|
+
fullScreenButton,
|
|
114
|
+
])
|
|
115
|
+
stack.axis = .horizontal
|
|
116
|
+
stack.spacing = 12
|
|
117
|
+
stack.alignment = .center
|
|
118
|
+
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
119
|
+
addSubview(stack)
|
|
120
|
+
|
|
121
|
+
NSLayoutConstraint.activate([
|
|
122
|
+
stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
|
123
|
+
stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
|
|
124
|
+
stack.topAnchor.constraint(equalTo: topAnchor, constant: 4),
|
|
125
|
+
stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
|
|
126
|
+
])
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private func styleIconButton(_ button: UIButton, symbol: String, fontSize: CGFloat) {
|
|
130
|
+
button.setTitle(symbol, for: .normal)
|
|
131
|
+
button.titleLabel?.font = .systemFont(ofSize: fontSize, weight: .medium)
|
|
132
|
+
button.setTitleColor(.systemBlue, for: .normal)
|
|
133
|
+
button.setTitleColor(UIColor.systemBlue.withAlphaComponent(0.4), for: .disabled)
|
|
134
|
+
button.backgroundColor = UIColor(red: 0.93, green: 0.96, blue: 1.0, alpha: 1.0)
|
|
135
|
+
button.layer.cornerRadius = 8
|
|
136
|
+
button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
|
|
137
|
+
button.setContentHuggingPriority(.required, for: .horizontal)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// MARK: - Icon helpers
|
|
141
|
+
|
|
142
|
+
private func applyIcon(_ image: UIImage?, to button: UIButton, fallback: String) {
|
|
143
|
+
if let image {
|
|
144
|
+
button.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal)
|
|
145
|
+
button.setTitle(nil, for: .normal)
|
|
146
|
+
} else {
|
|
147
|
+
button.setImage(nil, for: .normal)
|
|
148
|
+
button.setTitle(fallback, for: .normal)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// MARK: - Actions
|
|
153
|
+
|
|
154
|
+
@objc private func backTapped() { delegate?.toolbarDidTapBack(self) }
|
|
155
|
+
@objc private func zoomInTapped() { delegate?.toolbarDidTapZoomIn(self) }
|
|
156
|
+
@objc private func zoomOutTapped() { delegate?.toolbarDidTapZoomOut(self) }
|
|
157
|
+
@objc private func fullScreenTapped() { delegate?.toolbarDidTapFullScreen(self) }
|
|
158
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PdfViewerComponentView.mm — Fabric ViewManager for PdfViewerView.
|
|
3
|
+
*
|
|
4
|
+
* Architecture
|
|
5
|
+
* ────────────
|
|
6
|
+
* Swift class (PdfViewerView.swift) ← all rendering logic
|
|
7
|
+
* This ObjC++ file ← Fabric integration layer
|
|
8
|
+
* ├─ Hosts Swift PdfViewerView as contentView
|
|
9
|
+
* ├─ Receives props from codegen-generated PdfViewerViewProps C++ struct
|
|
10
|
+
* └─ Dispatches events via codegen-generated PdfViewerViewEventEmitter
|
|
11
|
+
*
|
|
12
|
+
* Codegen source: src/specs/NativePdfViewerComponent.ts
|
|
13
|
+
* Codegen output: build/generated/ios/PdfViewerSpecs/
|
|
14
|
+
* ├─ ComponentDescriptors.h (PdfViewerViewComponentDescriptor)
|
|
15
|
+
* ├─ Props.h (PdfViewerViewProps)
|
|
16
|
+
* ├─ EventEmitters.h (PdfViewerViewEventEmitter)
|
|
17
|
+
* └─ RCTComponentViewHelpers.h (RCTPdfViewerViewViewProtocol)
|
|
18
|
+
*
|
|
19
|
+
* Why RCTViewComponentView instead of RCTViewManager?
|
|
20
|
+
* RCTViewManager is the old-arch pattern. In Fabric, each component is a
|
|
21
|
+
* self-contained RCTViewComponentView that receives C++ props directly via
|
|
22
|
+
* JSI — no bridge serialization, no main-queue hopping for prop delivery.
|
|
23
|
+
*
|
|
24
|
+
* Guard: compiled only when New Architecture is enabled.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
28
|
+
|
|
29
|
+
#import <React/RCTViewComponentView.h>
|
|
30
|
+
#import <UIKit/UIKit.h>
|
|
31
|
+
|
|
32
|
+
// ── Forward-declare Swift PdfViewerView ───────────────────────────────────────
|
|
33
|
+
// Declare only the properties and block callbacks used in this translation unit.
|
|
34
|
+
// The actual UIView subclass is resolved by the linker from PdfViewerView.swift.
|
|
35
|
+
|
|
36
|
+
@interface PdfViewerView : UIView
|
|
37
|
+
|
|
38
|
+
// Props
|
|
39
|
+
@property (nonatomic, copy, nullable) NSString *pdfUri;
|
|
40
|
+
@property (nonatomic) NSInteger initialPageIndex;
|
|
41
|
+
@property (nonatomic) CGFloat defaultZoom;
|
|
42
|
+
@property (nonatomic) BOOL isFullScreen;
|
|
43
|
+
@property (nonatomic, copy, nullable) NSString *headerText;
|
|
44
|
+
@property (nonatomic, copy, nullable) NSString *backButtonText;
|
|
45
|
+
|
|
46
|
+
// Event callbacks (set by this manager; called by PdfViewerView when events fire)
|
|
47
|
+
@property (nonatomic, copy, nullable) void (^onFullScreenChangeBlock)(BOOL);
|
|
48
|
+
@property (nonatomic, copy, nullable) void (^onZoomChangeBlock)(float);
|
|
49
|
+
@property (nonatomic, copy, nullable) void (^onLeftScreenChangeBlock)(BOOL);
|
|
50
|
+
@property (nonatomic, copy, nullable) void (^onRightScreenChangeBlock)(BOOL);
|
|
51
|
+
|
|
52
|
+
@end
|
|
53
|
+
|
|
54
|
+
// ── Codegen-generated headers ─────────────────────────────────────────────────
|
|
55
|
+
// Generated by `yarn ios` / `pod install` from src/specs/NativePdfViewerComponent.ts
|
|
56
|
+
// The correct include path mirrors the ReactCodegen pod's directory layout:
|
|
57
|
+
// <react/renderer/components/<spec-name>/<header>.h>
|
|
58
|
+
// This matches how react-native-safe-area-context and other Fabric components import them.
|
|
59
|
+
|
|
60
|
+
#import <react/renderer/components/PdfViewerSpecs/ComponentDescriptors.h>
|
|
61
|
+
#import <react/renderer/components/PdfViewerSpecs/EventEmitters.h>
|
|
62
|
+
#import <react/renderer/components/PdfViewerSpecs/Props.h>
|
|
63
|
+
#import <react/renderer/components/PdfViewerSpecs/RCTComponentViewHelpers.h>
|
|
64
|
+
|
|
65
|
+
#import <React/RCTFabricComponentsPlugins.h>
|
|
66
|
+
|
|
67
|
+
using namespace facebook::react;
|
|
68
|
+
|
|
69
|
+
// ── PdfViewerComponentView ────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
@interface PdfViewerComponentView : RCTViewComponentView <RCTPdfViewerViewViewProtocol>
|
|
72
|
+
@end
|
|
73
|
+
|
|
74
|
+
@implementation PdfViewerComponentView {
|
|
75
|
+
PdfViewerView *_pdfView;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Fabric registration ───────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
81
|
+
return concreteComponentDescriptorProvider<PdfViewerViewComponentDescriptor>();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Initialisation ────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
87
|
+
if (self = [super initWithFrame:frame]) {
|
|
88
|
+
_pdfView = [[PdfViewerView alloc] initWithFrame:frame];
|
|
89
|
+
|
|
90
|
+
__weak PdfViewerComponentView *weakSelf = self;
|
|
91
|
+
|
|
92
|
+
// Wire each Swift block to the Fabric event emitter.
|
|
93
|
+
// The event payload field names must match the TS spec interfaces.
|
|
94
|
+
|
|
95
|
+
_pdfView.onFullScreenChangeBlock = ^(BOOL isFS) {
|
|
96
|
+
auto strongSelf = weakSelf;
|
|
97
|
+
if (!strongSelf || !strongSelf->_eventEmitter) return;
|
|
98
|
+
auto emitter = std::dynamic_pointer_cast<const PdfViewerViewEventEmitter>(
|
|
99
|
+
strongSelf->_eventEmitter
|
|
100
|
+
);
|
|
101
|
+
if (emitter) {
|
|
102
|
+
emitter->onFullScreenChange(
|
|
103
|
+
PdfViewerViewEventEmitter::OnFullScreenChange{.isFullScreen = (bool)isFS}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
_pdfView.onZoomChangeBlock = ^(float zoom) {
|
|
109
|
+
auto strongSelf = weakSelf;
|
|
110
|
+
if (!strongSelf || !strongSelf->_eventEmitter) return;
|
|
111
|
+
auto emitter = std::dynamic_pointer_cast<const PdfViewerViewEventEmitter>(
|
|
112
|
+
strongSelf->_eventEmitter
|
|
113
|
+
);
|
|
114
|
+
if (emitter) {
|
|
115
|
+
emitter->onZoomChange(
|
|
116
|
+
PdfViewerViewEventEmitter::OnZoomChange{.zoomPercentage = zoom}
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
_pdfView.onLeftScreenChangeBlock = ^(BOOL isLeft) {
|
|
122
|
+
auto strongSelf = weakSelf;
|
|
123
|
+
if (!strongSelf || !strongSelf->_eventEmitter) return;
|
|
124
|
+
auto emitter = std::dynamic_pointer_cast<const PdfViewerViewEventEmitter>(
|
|
125
|
+
strongSelf->_eventEmitter
|
|
126
|
+
);
|
|
127
|
+
if (emitter) {
|
|
128
|
+
emitter->onLeftScreenChange(
|
|
129
|
+
PdfViewerViewEventEmitter::OnLeftScreenChange{.isLeftScreen = (bool)isLeft}
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
_pdfView.onRightScreenChangeBlock = ^(BOOL isRight) {
|
|
135
|
+
auto strongSelf = weakSelf;
|
|
136
|
+
if (!strongSelf || !strongSelf->_eventEmitter) return;
|
|
137
|
+
auto emitter = std::dynamic_pointer_cast<const PdfViewerViewEventEmitter>(
|
|
138
|
+
strongSelf->_eventEmitter
|
|
139
|
+
);
|
|
140
|
+
if (emitter) {
|
|
141
|
+
emitter->onRightScreenChange(
|
|
142
|
+
PdfViewerViewEventEmitter::OnRightScreenChange{.isRightScreen = (bool)isRight}
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
self.contentView = _pdfView;
|
|
148
|
+
}
|
|
149
|
+
return self;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Prop delivery (called by Fabric on every JS prop update) ──────────────────
|
|
153
|
+
|
|
154
|
+
- (void)updateProps:(const Props::Shared &)props
|
|
155
|
+
oldProps:(const Props::Shared &)oldProps
|
|
156
|
+
{
|
|
157
|
+
const auto &p = *std::static_pointer_cast<const PdfViewerViewProps>(props);
|
|
158
|
+
|
|
159
|
+
// pdfUri
|
|
160
|
+
if (!p.pdfUri.empty()) {
|
|
161
|
+
NSString *uri = [NSString stringWithUTF8String:p.pdfUri.c_str()];
|
|
162
|
+
if (![_pdfView.pdfUri isEqualToString:uri]) {
|
|
163
|
+
_pdfView.pdfUri = uri;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Numeric / bool props
|
|
168
|
+
_pdfView.initialPageIndex = p.initialPageIndex;
|
|
169
|
+
_pdfView.defaultZoom = (CGFloat)p.defaultZoom;
|
|
170
|
+
_pdfView.isFullScreen = p.isFullScreen;
|
|
171
|
+
|
|
172
|
+
// Optional string props
|
|
173
|
+
if (!p.headerText.empty()) {
|
|
174
|
+
_pdfView.headerText = [NSString stringWithUTF8String:p.headerText.c_str()];
|
|
175
|
+
}
|
|
176
|
+
if (!p.backButtonText.empty()) {
|
|
177
|
+
_pdfView.backButtonText = [NSString stringWithUTF8String:p.backButtonText.c_str()];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
[super updateProps:props oldProps:oldProps];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@end
|
|
184
|
+
|
|
185
|
+
// ── Plugin registration ───────────────────────────────────────────────────────
|
|
186
|
+
// Fabric uses this C function to look up the component view class by name.
|
|
187
|
+
// The name "PdfViewerView" must match codegenNativeComponent('PdfViewerView')
|
|
188
|
+
// in NativePdfViewerComponent.ts AND VIEW_NAME returned by the Android manager.
|
|
189
|
+
|
|
190
|
+
Class<RCTComponentViewProtocol> PdfViewerViewCls(void) {
|
|
191
|
+
return PdfViewerComponentView.class;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#endif // RCT_NEW_ARCH_ENABLED
|