react-native-optimized-pdf 1.0.0 → 1.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/ReactNativeOptimizedPdf.podspec +4 -4
- package/ios/Core/PDFConfiguration.swift +28 -0
- package/ios/Core/PDFDocumentLoader.swift +75 -0
- package/ios/Core/PDFError.swift +36 -0
- package/ios/ReactNative/OptimizedPDFViewManager.swift +48 -0
- package/ios/ReactNative/OptimizedPdfViewManager.m +54 -0
- package/ios/Views/OptimizedPDFView.swift +384 -0
- package/ios/Views/TiledPDFPageView.swift +140 -0
- package/package.json +1 -1
- package/ios/OptimizedPdfView.swift +0 -256
- package/ios/OptimizedPdfViewManager.m +0 -22
- package/ios/OptimizedPdfViewManager.swift +0 -24
- package/ios/TiledPdfPageView.swift +0 -76
|
@@ -9,12 +9,12 @@ Pod::Spec.new do |s|
|
|
|
9
9
|
s.description = <<-DESC
|
|
10
10
|
High-performance PDF viewer for React Native
|
|
11
11
|
DESC
|
|
12
|
-
s.homepage = "https://github.com/
|
|
12
|
+
s.homepage = "https://github.com/Herbeth-LKS/react-native-optimized-pdf"
|
|
13
13
|
s.license = "MIT"
|
|
14
|
-
s.author = { "
|
|
14
|
+
s.author = { "Herbeth Lucas" => "herbethlucas007@gmail.com" }
|
|
15
15
|
s.platform = :ios, "12.0"
|
|
16
|
-
s.source = { :git => "https://github.com/
|
|
17
|
-
s.source_files = "ios
|
|
16
|
+
s.source = { :git => "https://github.com/Herbeth-LKS/react-native-optimized-pdf.git", :tag => "#{s.version}" }
|
|
17
|
+
s.source_files = "ios/**/*.{swift,h,m}"
|
|
18
18
|
s.requires_arc = true
|
|
19
19
|
s.swift_version = "5.0"
|
|
20
20
|
s.dependency "React-Core"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Configuration options for PDF rendering.
|
|
4
|
+
struct PDFConfiguration {
|
|
5
|
+
/// Maximum zoom scale allowed.
|
|
6
|
+
var maximumZoom: CGFloat = 5.0
|
|
7
|
+
|
|
8
|
+
/// Whether to enable antialiasing for smoother rendering.
|
|
9
|
+
var enableAntialiasing: Bool = true
|
|
10
|
+
|
|
11
|
+
/// Password for encrypted PDFs.
|
|
12
|
+
var password: String?
|
|
13
|
+
|
|
14
|
+
/// Size of each tile for CATiledLayer rendering.
|
|
15
|
+
var tileSize: CGSize = CGSize(width: 512, height: 512)
|
|
16
|
+
|
|
17
|
+
/// Number of detail levels for zoom.
|
|
18
|
+
var levelsOfDetail: Int = 2
|
|
19
|
+
|
|
20
|
+
/// Bias for levels of detail (allows zoom up to 2^bias resolution).
|
|
21
|
+
var levelsOfDetailBias: Int = 8
|
|
22
|
+
|
|
23
|
+
/// Animation duration for zoom reset.
|
|
24
|
+
var zoomResetAnimationDuration: TimeInterval = 0.25
|
|
25
|
+
|
|
26
|
+
/// Default configuration.
|
|
27
|
+
static let `default` = PDFConfiguration()
|
|
28
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreGraphics
|
|
3
|
+
|
|
4
|
+
/// Protocol for PDF document loading operations.
|
|
5
|
+
protocol PDFDocumentLoading {
|
|
6
|
+
func loadDocument(from source: String, password: String?) -> Result<CGPDFDocument, PDFError>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/// Handles loading and unlocking PDF documents.
|
|
10
|
+
final class PDFDocumentLoader: PDFDocumentLoading {
|
|
11
|
+
|
|
12
|
+
// MARK: - Singleton
|
|
13
|
+
|
|
14
|
+
static let shared = PDFDocumentLoader()
|
|
15
|
+
|
|
16
|
+
private init() {}
|
|
17
|
+
|
|
18
|
+
// MARK: - Public Methods
|
|
19
|
+
|
|
20
|
+
/// Loads a PDF document from the given source path.
|
|
21
|
+
/// - Parameters:
|
|
22
|
+
/// - source: The file path to the PDF document.
|
|
23
|
+
/// - password: Optional password for encrypted PDFs.
|
|
24
|
+
/// - Returns: A Result containing the loaded document or an error.
|
|
25
|
+
func loadDocument(from source: String, password: String?) -> Result<CGPDFDocument, PDFError> {
|
|
26
|
+
guard !source.isEmpty else {
|
|
27
|
+
return .failure(.emptySource)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let url = normalizedURL(from: source)
|
|
31
|
+
|
|
32
|
+
guard let document = CGPDFDocument(url as CFURL) else {
|
|
33
|
+
return .failure(.documentLoadFailed(source))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return unlockIfNeeded(document: document, password: password)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// MARK: - Private Methods
|
|
40
|
+
|
|
41
|
+
/// Normalizes the source path to a file URL.
|
|
42
|
+
private func normalizedURL(from source: String) -> URL {
|
|
43
|
+
let path = source.hasPrefix("file://")
|
|
44
|
+
? String(source.dropFirst(7))
|
|
45
|
+
: source
|
|
46
|
+
return URL(fileURLWithPath: path)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Attempts to unlock an encrypted PDF document.
|
|
50
|
+
private func unlockIfNeeded(
|
|
51
|
+
document: CGPDFDocument,
|
|
52
|
+
password: String?
|
|
53
|
+
) -> Result<CGPDFDocument, PDFError> {
|
|
54
|
+
guard document.isEncrypted else {
|
|
55
|
+
return .success(document)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Try empty password first (some PDFs use empty password for user access)
|
|
59
|
+
if document.unlockWithPassword("") {
|
|
60
|
+
return .success(document)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check if password was provided
|
|
64
|
+
guard let password = password, !password.isEmpty else {
|
|
65
|
+
return .failure(.passwordRequired)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Try provided password
|
|
69
|
+
guard document.unlockWithPassword(password) else {
|
|
70
|
+
return .failure(.invalidPassword)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return .success(document)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Errors that can occur during PDF operations.
|
|
4
|
+
enum PDFError: Error, LocalizedError {
|
|
5
|
+
case emptySource
|
|
6
|
+
case invalidPath(String)
|
|
7
|
+
case documentLoadFailed(String)
|
|
8
|
+
case passwordRequired
|
|
9
|
+
case invalidPassword
|
|
10
|
+
case pageNotFound(Int)
|
|
11
|
+
case renderingFailed
|
|
12
|
+
|
|
13
|
+
var errorDescription: String? {
|
|
14
|
+
switch self {
|
|
15
|
+
case .emptySource:
|
|
16
|
+
return "PDF source path is empty"
|
|
17
|
+
case .invalidPath(let path):
|
|
18
|
+
return "Invalid PDF path: \(path)"
|
|
19
|
+
case .documentLoadFailed(let path):
|
|
20
|
+
return "Failed to open PDF at: \(path)"
|
|
21
|
+
case .passwordRequired:
|
|
22
|
+
return "PDF is password protected"
|
|
23
|
+
case .invalidPassword:
|
|
24
|
+
return "Invalid password for PDF"
|
|
25
|
+
case .pageNotFound(let index):
|
|
26
|
+
return "Page not found at index: \(index)"
|
|
27
|
+
case .renderingFailed:
|
|
28
|
+
return "Failed to render PDF page"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Dictionary representation for React Native events.
|
|
33
|
+
var eventPayload: [String: Any] {
|
|
34
|
+
["message": errorDescription ?? "Unknown error"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
3
|
+
|
|
4
|
+
/// React Native View Manager for OptimizedPDFView.
|
|
5
|
+
///
|
|
6
|
+
/// This class bridges the native OptimizedPDFView with React Native,
|
|
7
|
+
/// exposing properties and methods to the JavaScript layer.
|
|
8
|
+
@objc(OptimizedPdfViewManager)
|
|
9
|
+
final class OptimizedPDFViewManager: RCTViewManager {
|
|
10
|
+
|
|
11
|
+
// MARK: - RCTViewManager
|
|
12
|
+
|
|
13
|
+
override func view() -> UIView! {
|
|
14
|
+
OptimizedPDFView()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override static func requiresMainQueueSetup() -> Bool {
|
|
18
|
+
true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// MARK: - Exported Methods
|
|
22
|
+
|
|
23
|
+
/// Navigates to a specific page in the PDF.
|
|
24
|
+
/// - Parameters:
|
|
25
|
+
/// - node: The React Native view tag.
|
|
26
|
+
/// - page: The page index to navigate to (0-based).
|
|
27
|
+
@objc func goToPage(_ node: NSNumber, page: NSNumber) {
|
|
28
|
+
DispatchQueue.main.async { [weak self] in
|
|
29
|
+
guard let self = self,
|
|
30
|
+
let pdfView = self.bridge?.uiManager.view(forReactTag: node) as? OptimizedPDFView else {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
pdfView.goToPage(page.intValue)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Resets the zoom level to fit the page.
|
|
38
|
+
/// - Parameter node: The React Native view tag.
|
|
39
|
+
@objc func resetZoom(_ node: NSNumber) {
|
|
40
|
+
DispatchQueue.main.async { [weak self] in
|
|
41
|
+
guard let self = self,
|
|
42
|
+
let pdfView = self.bridge?.uiManager.view(forReactTag: node) as? OptimizedPDFView else {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
pdfView.resetZoom()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native bridge for OptimizedPDFViewManager.
|
|
3
|
+
*
|
|
4
|
+
* This file exposes the Swift view manager and its properties/methods
|
|
5
|
+
* to the React Native JavaScript layer using the RCT_EXTERN macros.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
#import <React/RCTViewManager.h>
|
|
9
|
+
#import <React/RCTUIManager.h>
|
|
10
|
+
#import <React/RCTBridgeModule.h>
|
|
11
|
+
|
|
12
|
+
@interface RCT_EXTERN_MODULE(OptimizedPdfViewManager, RCTViewManager)
|
|
13
|
+
|
|
14
|
+
#pragma mark - View Properties
|
|
15
|
+
|
|
16
|
+
/// Path to the PDF file to display.
|
|
17
|
+
RCT_EXPORT_VIEW_PROPERTY(source, NSString)
|
|
18
|
+
|
|
19
|
+
/// Current page index (0-based).
|
|
20
|
+
RCT_EXPORT_VIEW_PROPERTY(page, NSNumber)
|
|
21
|
+
|
|
22
|
+
/// Maximum zoom scale allowed.
|
|
23
|
+
RCT_EXPORT_VIEW_PROPERTY(maximumZoom, NSNumber)
|
|
24
|
+
|
|
25
|
+
/// Whether antialiasing is enabled for smoother rendering.
|
|
26
|
+
RCT_EXPORT_VIEW_PROPERTY(enableAntialiasing, BOOL)
|
|
27
|
+
|
|
28
|
+
/// Password for encrypted PDFs.
|
|
29
|
+
RCT_EXPORT_VIEW_PROPERTY(password, NSString)
|
|
30
|
+
|
|
31
|
+
#pragma mark - Event Callbacks
|
|
32
|
+
|
|
33
|
+
/// Called when an error occurs during PDF loading or rendering.
|
|
34
|
+
RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock)
|
|
35
|
+
|
|
36
|
+
/// Called when a page is fully loaded and displayed.
|
|
37
|
+
RCT_EXPORT_VIEW_PROPERTY(onLoadComplete, RCTDirectEventBlock)
|
|
38
|
+
|
|
39
|
+
/// Called when the document is loaded with the total page count.
|
|
40
|
+
RCT_EXPORT_VIEW_PROPERTY(onPageCount, RCTDirectEventBlock)
|
|
41
|
+
|
|
42
|
+
/// Called when the PDF requires a password to open.
|
|
43
|
+
RCT_EXPORT_VIEW_PROPERTY(onPasswordRequired, RCTDirectEventBlock)
|
|
44
|
+
|
|
45
|
+
#pragma mark - Native Methods
|
|
46
|
+
|
|
47
|
+
/// Navigates to a specific page in the PDF.
|
|
48
|
+
RCT_EXTERN_METHOD(goToPage:(nonnull NSNumber *)node
|
|
49
|
+
page:(nonnull NSNumber *)page)
|
|
50
|
+
|
|
51
|
+
/// Resets the zoom level to fit the page.
|
|
52
|
+
RCT_EXTERN_METHOD(resetZoom:(nonnull NSNumber *)node)
|
|
53
|
+
|
|
54
|
+
@end
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import React
|
|
3
|
+
|
|
4
|
+
/// Protocol for receiving PDF view events.
|
|
5
|
+
@objc protocol OptimizedPDFViewDelegate: AnyObject {
|
|
6
|
+
@objc optional func pdfView(_ pdfView: OptimizedPDFView, didFailWithError error: Error)
|
|
7
|
+
@objc optional func pdfView(_ pdfView: OptimizedPDFView, didLoadWithPageCount pageCount: Int)
|
|
8
|
+
@objc optional func pdfView(_ pdfView: OptimizedPDFView, didDisplayPage page: Int, size: CGSize)
|
|
9
|
+
@objc optional func pdfViewDidRequestPassword(_ pdfView: OptimizedPDFView)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/// A high-performance PDF view using CATiledLayer for smooth zooming and scrolling.
|
|
13
|
+
///
|
|
14
|
+
/// This view is optimized for React Native integration, providing callbacks for
|
|
15
|
+
/// load completion, errors, and page navigation events.
|
|
16
|
+
@objc(OptimizedPdfView)
|
|
17
|
+
final class OptimizedPDFView: UIScrollView {
|
|
18
|
+
|
|
19
|
+
// MARK: - React Native Properties
|
|
20
|
+
|
|
21
|
+
/// Path to the PDF file to display.
|
|
22
|
+
@objc var source: String = "" {
|
|
23
|
+
didSet {
|
|
24
|
+
handleSourceChange()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Current page index (0-based).
|
|
29
|
+
@objc var page: NSNumber = 0 {
|
|
30
|
+
didSet {
|
|
31
|
+
handlePageChange(to: page.intValue)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Maximum zoom scale.
|
|
36
|
+
@objc var maximumZoom: NSNumber = 5.0 {
|
|
37
|
+
didSet {
|
|
38
|
+
configuration.maximumZoom = CGFloat(truncating: maximumZoom)
|
|
39
|
+
applyZoomConfiguration()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Whether antialiasing is enabled.
|
|
44
|
+
@objc var enableAntialiasing: Bool = true {
|
|
45
|
+
didSet {
|
|
46
|
+
configuration.enableAntialiasing = enableAntialiasing
|
|
47
|
+
tiledPageView.enableAntialiasing = enableAntialiasing
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Password for encrypted PDFs.
|
|
52
|
+
@objc var password: String = "" {
|
|
53
|
+
didSet {
|
|
54
|
+
configuration.password = password.isEmpty ? nil : password
|
|
55
|
+
retryLoadIfNeeded()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// MARK: - React Native Event Callbacks
|
|
60
|
+
|
|
61
|
+
@objc var onError: RCTDirectEventBlock?
|
|
62
|
+
@objc var onLoadComplete: RCTDirectEventBlock?
|
|
63
|
+
@objc var onPageCount: RCTDirectEventBlock?
|
|
64
|
+
@objc var onPasswordRequired: RCTDirectEventBlock?
|
|
65
|
+
|
|
66
|
+
// MARK: - Public Properties
|
|
67
|
+
|
|
68
|
+
weak var pdfDelegate: OptimizedPDFViewDelegate?
|
|
69
|
+
|
|
70
|
+
/// Current number of pages in the document.
|
|
71
|
+
private(set) var pageCount: Int = 0
|
|
72
|
+
|
|
73
|
+
// MARK: - Private Properties
|
|
74
|
+
|
|
75
|
+
private var pdfDocument: CGPDFDocument?
|
|
76
|
+
private let tiledPageView: TiledPDFPageView
|
|
77
|
+
private var configuration = PDFConfiguration.default
|
|
78
|
+
private let documentLoader: PDFDocumentLoading
|
|
79
|
+
|
|
80
|
+
private var pendingPageIndex: Int?
|
|
81
|
+
private var needsLoad = false
|
|
82
|
+
private var lastBoundsSize: CGSize = .zero
|
|
83
|
+
private var currentPageRect: CGRect?
|
|
84
|
+
private var initialZoomScale: CGFloat = 1.0
|
|
85
|
+
|
|
86
|
+
// MARK: - Initialization
|
|
87
|
+
|
|
88
|
+
override init(frame: CGRect) {
|
|
89
|
+
self.tiledPageView = TiledPDFPageView(frame: frame)
|
|
90
|
+
self.documentLoader = PDFDocumentLoader.shared
|
|
91
|
+
super.init(frame: frame)
|
|
92
|
+
setupView()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
required init?(coder: NSCoder) {
|
|
96
|
+
self.tiledPageView = TiledPDFPageView(frame: .zero)
|
|
97
|
+
self.documentLoader = PDFDocumentLoader.shared
|
|
98
|
+
super.init(coder: coder)
|
|
99
|
+
setupView()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Initializes with a custom document loader (useful for testing).
|
|
103
|
+
init(frame: CGRect, documentLoader: PDFDocumentLoading) {
|
|
104
|
+
self.tiledPageView = TiledPDFPageView(frame: frame)
|
|
105
|
+
self.documentLoader = documentLoader
|
|
106
|
+
super.init(frame: frame)
|
|
107
|
+
setupView()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// MARK: - Setup
|
|
111
|
+
|
|
112
|
+
private func setupView() {
|
|
113
|
+
setupScrollView()
|
|
114
|
+
setupTiledPageView()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func setupScrollView() {
|
|
118
|
+
delegate = self
|
|
119
|
+
showsHorizontalScrollIndicator = false
|
|
120
|
+
showsVerticalScrollIndicator = false
|
|
121
|
+
bouncesZoom = true
|
|
122
|
+
applyZoomConfiguration()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private func setupTiledPageView() {
|
|
126
|
+
tiledPageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
127
|
+
tiledPageView.configuration = configuration
|
|
128
|
+
addSubview(tiledPageView)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private func applyZoomConfiguration() {
|
|
132
|
+
maximumZoomScale = configuration.maximumZoom
|
|
133
|
+
|
|
134
|
+
guard let pageRect = currentPageRect else { return }
|
|
135
|
+
|
|
136
|
+
let fitScale = calculateFitScale(for: pageRect)
|
|
137
|
+
minimumZoomScale = fitScale
|
|
138
|
+
initialZoomScale = fitScale
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// MARK: - Document Loading
|
|
143
|
+
|
|
144
|
+
private extension OptimizedPDFView {
|
|
145
|
+
|
|
146
|
+
func handleSourceChange() {
|
|
147
|
+
resetDocument()
|
|
148
|
+
pendingPageIndex = page.intValue
|
|
149
|
+
needsLoad = true
|
|
150
|
+
setNeedsLayout()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func handlePageChange(to index: Int) {
|
|
154
|
+
guard let document = pdfDocument else {
|
|
155
|
+
pendingPageIndex = index
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let validIndex = clampPageIndex(index, for: document)
|
|
160
|
+
displayPage(at: validIndex)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func retryLoadIfNeeded() {
|
|
164
|
+
guard !password.isEmpty, pdfDocument == nil, needsLoad else { return }
|
|
165
|
+
setNeedsLayout()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
func resetDocument() {
|
|
169
|
+
pdfDocument = nil
|
|
170
|
+
pageCount = 0
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func loadDocument() {
|
|
174
|
+
guard !source.isEmpty else { return }
|
|
175
|
+
|
|
176
|
+
let result = documentLoader.loadDocument(from: source, password: configuration.password)
|
|
177
|
+
|
|
178
|
+
switch result {
|
|
179
|
+
case .success(let document):
|
|
180
|
+
handleLoadSuccess(document: document)
|
|
181
|
+
|
|
182
|
+
case .failure(let error):
|
|
183
|
+
handleLoadFailure(error: error)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
func handleLoadSuccess(document: CGPDFDocument) {
|
|
188
|
+
pdfDocument = document
|
|
189
|
+
pageCount = document.numberOfPages
|
|
190
|
+
|
|
191
|
+
// Emit page count event on next run loop to ensure listeners are connected
|
|
192
|
+
DispatchQueue.main.async { [weak self] in
|
|
193
|
+
guard let self = self else { return }
|
|
194
|
+
self.onPageCount?(["numberOfPages": NSNumber(value: self.pageCount)])
|
|
195
|
+
self.pdfDelegate?.pdfView?(self, didLoadWithPageCount: self.pageCount)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Display pending or first page
|
|
199
|
+
let targetIndex = pendingPageIndex ?? 0
|
|
200
|
+
pendingPageIndex = nil
|
|
201
|
+
let validIndex = clampPageIndex(targetIndex, for: document)
|
|
202
|
+
displayPage(at: validIndex)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func handleLoadFailure(error: PDFError) {
|
|
206
|
+
if case .passwordRequired = error {
|
|
207
|
+
DispatchQueue.main.async { [weak self] in
|
|
208
|
+
guard let self = self else { return }
|
|
209
|
+
self.onPasswordRequired?([:])
|
|
210
|
+
self.pdfDelegate?.pdfViewDidRequestPassword?(self)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
onError?(error.eventPayload)
|
|
215
|
+
pdfDelegate?.pdfView?(self, didFailWithError: error)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func clampPageIndex(_ index: Int, for document: CGPDFDocument) -> Int {
|
|
219
|
+
max(0, min(index, document.numberOfPages - 1))
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// MARK: - Page Display
|
|
224
|
+
|
|
225
|
+
private extension OptimizedPDFView {
|
|
226
|
+
|
|
227
|
+
func displayPage(at index: Int) {
|
|
228
|
+
guard let document = pdfDocument,
|
|
229
|
+
let page = document.page(at: index + 1) else {
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let pageRect = page.getBoxRect(.cropBox)
|
|
234
|
+
currentPageRect = pageRect
|
|
235
|
+
|
|
236
|
+
prepareForPageDisplay()
|
|
237
|
+
configureTiledView(for: page, rect: pageRect)
|
|
238
|
+
applyFitScale(for: pageRect)
|
|
239
|
+
centerContent()
|
|
240
|
+
notifyPageDisplayed(index: index, size: pageRect.size)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
func prepareForPageDisplay() {
|
|
244
|
+
zoomScale = 1.0
|
|
245
|
+
contentOffset = .zero
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
func configureTiledView(for page: CGPDFPage, rect: CGRect) {
|
|
249
|
+
tiledPageView.updateFrame(for: page)
|
|
250
|
+
contentSize = rect.size
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
func applyFitScale(for pageRect: CGRect) {
|
|
254
|
+
let fitScale = calculateFitScale(for: pageRect)
|
|
255
|
+
minimumZoomScale = fitScale
|
|
256
|
+
initialZoomScale = fitScale
|
|
257
|
+
setZoomScale(fitScale, animated: false)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
func notifyPageDisplayed(index: Int, size: CGSize) {
|
|
261
|
+
let payload: [String: Any] = [
|
|
262
|
+
"currentPage": index + 1,
|
|
263
|
+
"width": size.width,
|
|
264
|
+
"height": size.height
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
DispatchQueue.main.async { [weak self] in
|
|
268
|
+
guard let self = self else { return }
|
|
269
|
+
self.onLoadComplete?(payload)
|
|
270
|
+
self.pdfDelegate?.pdfView?(self, didDisplayPage: index + 1, size: size)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// MARK: - Layout & Scaling
|
|
276
|
+
|
|
277
|
+
private extension OptimizedPDFView {
|
|
278
|
+
|
|
279
|
+
func calculateFitScale(for pageRect: CGRect) -> CGFloat {
|
|
280
|
+
guard bounds.width > 0,
|
|
281
|
+
bounds.height > 0,
|
|
282
|
+
pageRect.width > 0,
|
|
283
|
+
pageRect.height > 0 else {
|
|
284
|
+
return 1.0
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let scaleX = bounds.width / pageRect.width
|
|
288
|
+
let scaleY = bounds.height / pageRect.height
|
|
289
|
+
let scale = min(scaleX, scaleY)
|
|
290
|
+
|
|
291
|
+
guard scale.isFinite, scale > 0 else {
|
|
292
|
+
return 1.0
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return scale
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
func centerContent() {
|
|
299
|
+
let boundsSize = bounds.size
|
|
300
|
+
var frame = tiledPageView.frame
|
|
301
|
+
|
|
302
|
+
frame.origin.x = frame.width < boundsSize.width
|
|
303
|
+
? (boundsSize.width - frame.width) * 0.5
|
|
304
|
+
: 0
|
|
305
|
+
|
|
306
|
+
frame.origin.y = frame.height < boundsSize.height
|
|
307
|
+
? (boundsSize.height - frame.height) * 0.5
|
|
308
|
+
: 0
|
|
309
|
+
|
|
310
|
+
tiledPageView.frame = frame
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// MARK: - UIScrollViewDelegate
|
|
315
|
+
|
|
316
|
+
extension OptimizedPDFView: UIScrollViewDelegate {
|
|
317
|
+
|
|
318
|
+
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
|
319
|
+
tiledPageView
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
|
323
|
+
centerContent()
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// MARK: - Lifecycle
|
|
328
|
+
|
|
329
|
+
extension OptimizedPDFView {
|
|
330
|
+
|
|
331
|
+
override func didMoveToWindow() {
|
|
332
|
+
super.didMoveToWindow()
|
|
333
|
+
if window != nil {
|
|
334
|
+
setNeedsLayout()
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
override func layoutSubviews() {
|
|
339
|
+
super.layoutSubviews()
|
|
340
|
+
|
|
341
|
+
guard bounds.width > 0, bounds.height > 0 else { return }
|
|
342
|
+
|
|
343
|
+
if needsLoad {
|
|
344
|
+
needsLoad = false
|
|
345
|
+
loadDocument()
|
|
346
|
+
lastBoundsSize = bounds.size
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
handleBoundsChange()
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private func handleBoundsChange() {
|
|
354
|
+
guard lastBoundsSize != bounds.size,
|
|
355
|
+
let pageRect = currentPageRect else {
|
|
356
|
+
centerContent()
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
lastBoundsSize = bounds.size
|
|
361
|
+
applyFitScale(for: pageRect)
|
|
362
|
+
centerContent()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// MARK: - Public Methods
|
|
367
|
+
|
|
368
|
+
extension OptimizedPDFView {
|
|
369
|
+
|
|
370
|
+
/// Resets the zoom to fit the page.
|
|
371
|
+
@objc func resetZoom() {
|
|
372
|
+
UIView.animate(withDuration: configuration.zoomResetAnimationDuration) { [weak self] in
|
|
373
|
+
guard let self = self else { return }
|
|
374
|
+
self.setZoomScale(self.initialZoomScale, animated: false)
|
|
375
|
+
self.centerContent()
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/// Navigates to a specific page.
|
|
380
|
+
/// - Parameter index: The page index (0-based).
|
|
381
|
+
@objc func goToPage(_ index: Int) {
|
|
382
|
+
page = NSNumber(value: index)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import CoreGraphics
|
|
3
|
+
|
|
4
|
+
/// A view that renders a PDF page using CATiledLayer for efficient tiled rendering.
|
|
5
|
+
///
|
|
6
|
+
/// CATiledLayer divides the content into tiles, allowing smooth zooming and scrolling
|
|
7
|
+
/// without rendering the entire PDF at once. This is essential for large PDF documents.
|
|
8
|
+
final class TiledPDFPageView: UIView {
|
|
9
|
+
|
|
10
|
+
// MARK: - Properties
|
|
11
|
+
|
|
12
|
+
/// The PDF page to render.
|
|
13
|
+
var pdfPage: CGPDFPage? {
|
|
14
|
+
didSet {
|
|
15
|
+
setNeedsDisplay()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Whether antialiasing is enabled for rendering.
|
|
20
|
+
var enableAntialiasing: Bool = true {
|
|
21
|
+
didSet {
|
|
22
|
+
setNeedsDisplay()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// The configuration for tiled rendering.
|
|
27
|
+
var configuration: PDFConfiguration = .default {
|
|
28
|
+
didSet {
|
|
29
|
+
applyConfiguration()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Returns CATiledLayer as the backing layer.
|
|
34
|
+
override class var layerClass: AnyClass {
|
|
35
|
+
CATiledLayer.self
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Typed accessor for the tiled layer.
|
|
39
|
+
private var tiledLayer: CATiledLayer {
|
|
40
|
+
layer as! CATiledLayer
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// MARK: - Initialization
|
|
44
|
+
|
|
45
|
+
override init(frame: CGRect) {
|
|
46
|
+
super.init(frame: frame)
|
|
47
|
+
commonInit()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
required init?(coder: NSCoder) {
|
|
51
|
+
super.init(coder: coder)
|
|
52
|
+
commonInit()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Initializes with a specific configuration.
|
|
56
|
+
convenience init(frame: CGRect, configuration: PDFConfiguration) {
|
|
57
|
+
self.init(frame: frame)
|
|
58
|
+
self.configuration = configuration
|
|
59
|
+
applyConfiguration()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private func commonInit() {
|
|
63
|
+
applyConfiguration()
|
|
64
|
+
contentScaleFactor = UIScreen.main.scale
|
|
65
|
+
backgroundColor = .white
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// MARK: - Configuration
|
|
69
|
+
|
|
70
|
+
private func applyConfiguration() {
|
|
71
|
+
tiledLayer.tileSize = configuration.tileSize
|
|
72
|
+
tiledLayer.levelsOfDetail = configuration.levelsOfDetail
|
|
73
|
+
tiledLayer.levelsOfDetailBias = configuration.levelsOfDetailBias
|
|
74
|
+
enableAntialiasing = configuration.enableAntialiasing
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// MARK: - Rendering
|
|
78
|
+
|
|
79
|
+
override func draw(_ rect: CGRect) {
|
|
80
|
+
guard let context = UIGraphicsGetCurrentContext(),
|
|
81
|
+
let page = pdfPage else {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
renderPage(page, in: context, rect: rect)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Renders the PDF page in the given graphics context.
|
|
89
|
+
private func renderPage(_ page: CGPDFPage, in context: CGContext, rect: CGRect) {
|
|
90
|
+
context.saveGState()
|
|
91
|
+
defer { context.restoreGState() }
|
|
92
|
+
|
|
93
|
+
// Configure rendering quality
|
|
94
|
+
context.setShouldAntialias(enableAntialiasing)
|
|
95
|
+
context.interpolationQuality = determineInterpolationQuality()
|
|
96
|
+
|
|
97
|
+
// Fill background
|
|
98
|
+
context.setFillColor(UIColor.white.cgColor)
|
|
99
|
+
context.fill(rect)
|
|
100
|
+
|
|
101
|
+
// Transform coordinate system (UIKit uses top-left origin, PDF uses bottom-left)
|
|
102
|
+
context.translateBy(x: 0, y: bounds.height)
|
|
103
|
+
context.scaleBy(x: 1.0, y: -1.0)
|
|
104
|
+
|
|
105
|
+
// Apply PDF transform to fit the page
|
|
106
|
+
let pdfTransform = page.getDrawingTransform(
|
|
107
|
+
.cropBox,
|
|
108
|
+
rect: bounds,
|
|
109
|
+
rotate: 0,
|
|
110
|
+
preserveAspectRatio: true
|
|
111
|
+
)
|
|
112
|
+
context.concatenate(pdfTransform)
|
|
113
|
+
|
|
114
|
+
// Draw the page
|
|
115
|
+
context.drawPDFPage(page)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Determines the interpolation quality based on zoom level.
|
|
119
|
+
private func determineInterpolationQuality() -> CGInterpolationQuality {
|
|
120
|
+
tiledLayer.levelsOfDetail == 1 ? .low : .high
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// MARK: - Public Methods
|
|
125
|
+
|
|
126
|
+
extension TiledPDFPageView {
|
|
127
|
+
|
|
128
|
+
/// Prepares the view for displaying a new page.
|
|
129
|
+
func prepareForReuse() {
|
|
130
|
+
pdfPage = nil
|
|
131
|
+
layer.contents = nil
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Updates the view size to match the page dimensions.
|
|
135
|
+
func updateFrame(for page: CGPDFPage) {
|
|
136
|
+
let pageRect = page.getBoxRect(.cropBox)
|
|
137
|
+
frame = CGRect(origin: .zero, size: pageRect.size)
|
|
138
|
+
pdfPage = page
|
|
139
|
+
}
|
|
140
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-optimized-pdf",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Ultra high-performance PDF viewer for React Native. Renders massive PDFs (1000+ pages) with extreme zoom levels while using minimal memory. 10x faster than native PDFKit on iOS, prevents crashes on large documents. Uses CATiledLayer (iOS) and PdfRenderer (Android) for tile-based rendering.",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import UIKit
|
|
2
|
-
import PDFKit
|
|
3
|
-
import React
|
|
4
|
-
|
|
5
|
-
// Classe que representa a View personalizada do PDF otimizado.
|
|
6
|
-
// É baseada em UIScrollView para permitir zoom e scroll sem travar,
|
|
7
|
-
@objc(OptimizedPdfView)
|
|
8
|
-
class OptimizedPdfView: UIScrollView, UIScrollViewDelegate {
|
|
9
|
-
|
|
10
|
-
// Caminho do arquivo PDF a ser carregado
|
|
11
|
-
@objc var source: String = "" {
|
|
12
|
-
didSet {
|
|
13
|
-
needsLoad = true
|
|
14
|
-
pdfDocument = nil
|
|
15
|
-
pendingPageIndex = Int(truncating: page)
|
|
16
|
-
setNeedsLayout()
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
@objc var page: NSNumber = 0 {
|
|
21
|
-
didSet {
|
|
22
|
-
let idx = page.intValue
|
|
23
|
-
if let doc = pdfDocument {
|
|
24
|
-
if idx >= 0, idx < doc.numberOfPages {
|
|
25
|
-
displayPage(index: idx)
|
|
26
|
-
}
|
|
27
|
-
} else {
|
|
28
|
-
pendingPageIndex = idx
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@objc var maximumZoom: NSNumber = 5.0 {
|
|
34
|
-
didSet {
|
|
35
|
-
self.maximumZoomScale = CGFloat(truncating: maximumZoom)
|
|
36
|
-
if let lastRect = currentPageRect {
|
|
37
|
-
let fit = fitScale(for: lastRect)
|
|
38
|
-
self.minimumZoomScale = fit
|
|
39
|
-
self.initialZoomScale = fit
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
@objc var enableAntialiasing: Bool = true {
|
|
45
|
-
didSet {
|
|
46
|
-
tiledView?.enableAntialiasing = enableAntialiasing
|
|
47
|
-
tiledView?.setNeedsDisplay()
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
@objc var password: String = "" {
|
|
52
|
-
didSet {
|
|
53
|
-
if !password.isEmpty && pdfDocument == nil && needsLoad {
|
|
54
|
-
setNeedsLayout()
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Eventos enviados para o React Native
|
|
60
|
-
@objc var onError: RCTDirectEventBlock?
|
|
61
|
-
@objc var onLoadComplete: RCTDirectEventBlock?
|
|
62
|
-
@objc var onPageCount: RCTDirectEventBlock?
|
|
63
|
-
@objc var onPasswordRequired: RCTDirectEventBlock?
|
|
64
|
-
|
|
65
|
-
// Documento PDF em memória (usando Core Graphics, mais leve que PDFKit para esse caso)
|
|
66
|
-
private var pdfDocument: CGPDFDocument?
|
|
67
|
-
|
|
68
|
-
// View responsável por desenhar a página atual usando CATiledLayer
|
|
69
|
-
private var tiledView: TiledPdfPageView!
|
|
70
|
-
private var initialZoomScale: CGFloat = 1.0
|
|
71
|
-
private var needsLoad = false
|
|
72
|
-
private var pendingPageIndex: Int?
|
|
73
|
-
private var lastBoundsSize: CGSize = .zero
|
|
74
|
-
private var currentPageRect: CGRect?
|
|
75
|
-
|
|
76
|
-
// Inicializadores
|
|
77
|
-
override init(frame: CGRect) {
|
|
78
|
-
super.init(frame: frame)
|
|
79
|
-
setupScrollView()
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
required init?(coder: NSCoder) {
|
|
83
|
-
super.init(coder: coder)
|
|
84
|
-
setupScrollView()
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Configura a ScrollView para suportar zoom suave e sem travamentos
|
|
88
|
-
private func setupScrollView() {
|
|
89
|
-
delegate = self
|
|
90
|
-
minimumZoomScale = 1.0
|
|
91
|
-
maximumZoomScale = CGFloat(truncating: maximumZoom)
|
|
92
|
-
showsHorizontalScrollIndicator = false
|
|
93
|
-
showsVerticalScrollIndicator = false
|
|
94
|
-
bouncesZoom = true // "efeito mola" ao dar zoom
|
|
95
|
-
|
|
96
|
-
// Inicializa o tiledView que renderiza as páginas em blocos (tiles)
|
|
97
|
-
tiledView = TiledPdfPageView(frame: bounds)
|
|
98
|
-
tiledView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
99
|
-
tiledView.enableAntialiasing = enableAntialiasing
|
|
100
|
-
addSubview(tiledView)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Carrega o PDF a partir de um caminho local
|
|
104
|
-
private func loadPdf() {
|
|
105
|
-
guard !source.isEmpty else { return }
|
|
106
|
-
|
|
107
|
-
// Remove prefixo "file://" se existir
|
|
108
|
-
let path = source.hasPrefix("file://") ? String(source.dropFirst(7)) : source
|
|
109
|
-
let url = URL(fileURLWithPath: path)
|
|
110
|
-
|
|
111
|
-
// Tenta abrir o documento PDF
|
|
112
|
-
guard let doc = CGPDFDocument(url as CFURL) else {
|
|
113
|
-
onError?(["message": "Failed to open PDF at \(source)"])
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if doc.isEncrypted {
|
|
118
|
-
if !doc.unlockWithPassword("") {
|
|
119
|
-
if password.isEmpty {
|
|
120
|
-
DispatchQueue.main.async { [weak self] in
|
|
121
|
-
self?.onPasswordRequired?([:])
|
|
122
|
-
}
|
|
123
|
-
onError?(["message": "PDF is password protected"])
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if !doc.unlockWithPassword(password) {
|
|
128
|
-
onError?(["message": "Invalid password for PDF"])
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
pdfDocument = doc
|
|
135
|
-
|
|
136
|
-
// Emite evento na próxima iteração do runloop p/ garantir listeners conectados
|
|
137
|
-
DispatchQueue.main.async { [weak self] in
|
|
138
|
-
guard let self = self else { return }
|
|
139
|
-
self.onPageCount?(["numberOfPages": NSNumber(value: doc.numberOfPages)])
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Página desejada (ou 0 se não tiver)
|
|
143
|
-
let idx = (pendingPageIndex ?? 0)
|
|
144
|
-
pendingPageIndex = nil
|
|
145
|
-
|
|
146
|
-
displayPage(index: max(0, min(idx, doc.numberOfPages - 1)))
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// MARK: Exibir página
|
|
150
|
-
private func displayPage(index: Int) {
|
|
151
|
-
guard let doc = pdfDocument,
|
|
152
|
-
let pageRef = doc.page(at: index + 1) else { return }
|
|
153
|
-
|
|
154
|
-
let pageRect = pageRef.getBoxRect(.cropBox)
|
|
155
|
-
currentPageRect = pageRect
|
|
156
|
-
|
|
157
|
-
// Prepara conteúdo
|
|
158
|
-
zoomScale = 1.0
|
|
159
|
-
contentOffset = .zero
|
|
160
|
-
tiledView.pdfPage = pageRef
|
|
161
|
-
tiledView.frame = CGRect(origin: .zero, size: pageRect.size)
|
|
162
|
-
tiledView.setNeedsDisplay()
|
|
163
|
-
contentSize = pageRect.size
|
|
164
|
-
|
|
165
|
-
// Fit seguro (só com bounds válidos)
|
|
166
|
-
let fit = fitScale(for: pageRect)
|
|
167
|
-
minimumZoomScale = fit
|
|
168
|
-
initialZoomScale = fit
|
|
169
|
-
setZoomScale(fit, animated: false)
|
|
170
|
-
centerContent()
|
|
171
|
-
|
|
172
|
-
// Evento enviado ao RN quando a página carrega
|
|
173
|
-
let w = pageRect.width, h = pageRect.height
|
|
174
|
-
DispatchQueue.main.async { [weak self] in
|
|
175
|
-
self?.onLoadComplete?([ "currentPage": index + 1, "width": w, "height": h])
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Cálculo de escala sempre com guarda
|
|
180
|
-
private func fitScale(for pageRect: CGRect) -> CGFloat {
|
|
181
|
-
guard bounds.width > 0, bounds.height > 0,
|
|
182
|
-
pageRect.width > 0, pageRect.height > 0 else {
|
|
183
|
-
return 1.0
|
|
184
|
-
}
|
|
185
|
-
let sW = bounds.width / pageRect.width
|
|
186
|
-
let sH = bounds.height / pageRect.height
|
|
187
|
-
let s = min(sW, sH)
|
|
188
|
-
// evita zero/NaN/inf
|
|
189
|
-
if s.isNaN || s.isInfinite || s <= 0 { return 1.0 }
|
|
190
|
-
return s
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private func centerContent() {
|
|
194
|
-
let boundsSize = bounds.size
|
|
195
|
-
var frameToCenter = tiledView.frame
|
|
196
|
-
|
|
197
|
-
// Centro horizontal
|
|
198
|
-
frameToCenter.origin.x = (frameToCenter.size.width < boundsSize.width)
|
|
199
|
-
? (boundsSize.width - frameToCenter.size.width) * 0.5
|
|
200
|
-
: 0
|
|
201
|
-
|
|
202
|
-
// Centro vertical
|
|
203
|
-
frameToCenter.origin.y = (frameToCenter.size.height < boundsSize.height)
|
|
204
|
-
? (boundsSize.height - frameToCenter.size.height) * 0.5
|
|
205
|
-
: 0
|
|
206
|
-
|
|
207
|
-
tiledView.frame = frameToCenter
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
|
211
|
-
return tiledView
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
|
215
|
-
centerContent()
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
func resetZoom() {
|
|
219
|
-
UIView.animate(withDuration: 0.25) {
|
|
220
|
-
self.setZoomScale(self.initialZoomScale, animated: false)
|
|
221
|
-
self.centerContent()
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// MARK: Layout ciclo
|
|
226
|
-
override func didMoveToWindow() {
|
|
227
|
-
super.didMoveToWindow()
|
|
228
|
-
if window != nil { setNeedsLayout() }
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
override func layoutSubviews() {
|
|
232
|
-
super.layoutSubviews()
|
|
233
|
-
|
|
234
|
-
guard bounds.width > 0, bounds.height > 0 else { return }
|
|
235
|
-
|
|
236
|
-
// Carrega o PDF somente agora (props já setadas e bounds válidos)
|
|
237
|
-
if needsLoad {
|
|
238
|
-
needsLoad = false
|
|
239
|
-
loadPdf()
|
|
240
|
-
lastBoundsSize = bounds.size
|
|
241
|
-
return
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Se mudou o tamanho (ex.: rotação), refaz o fit na página atual
|
|
245
|
-
if lastBoundsSize != bounds.size, let rect = currentPageRect {
|
|
246
|
-
lastBoundsSize = bounds.size
|
|
247
|
-
let fit = fitScale(for: rect)
|
|
248
|
-
minimumZoomScale = fit
|
|
249
|
-
initialZoomScale = fit
|
|
250
|
-
setZoomScale(fit, animated: false)
|
|
251
|
-
centerContent()
|
|
252
|
-
} else {
|
|
253
|
-
centerContent()
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#import <React/RCTViewManager.h>
|
|
2
|
-
#import <React/RCTUIManager.h>
|
|
3
|
-
#import <React/RCTBridgeModule.h>
|
|
4
|
-
|
|
5
|
-
@interface RCT_EXTERN_MODULE(OptimizedPdfViewManager, RCTViewManager)
|
|
6
|
-
|
|
7
|
-
// Propriedades
|
|
8
|
-
RCT_EXPORT_VIEW_PROPERTY(source, NSString)
|
|
9
|
-
RCT_EXPORT_VIEW_PROPERTY(page, NSNumber)
|
|
10
|
-
RCT_EXPORT_VIEW_PROPERTY(maximumZoom, NSNumber)
|
|
11
|
-
RCT_EXPORT_VIEW_PROPERTY(enableAntialiasing, BOOL)
|
|
12
|
-
RCT_EXPORT_VIEW_PROPERTY(password, NSString)
|
|
13
|
-
RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock)
|
|
14
|
-
RCT_EXPORT_VIEW_PROPERTY(onLoadComplete, RCTDirectEventBlock)
|
|
15
|
-
RCT_EXPORT_VIEW_PROPERTY(onPageCount, RCTDirectEventBlock)
|
|
16
|
-
RCT_EXPORT_VIEW_PROPERTY(onPasswordRequired, RCTDirectEventBlock)
|
|
17
|
-
|
|
18
|
-
// Métodos nativos
|
|
19
|
-
RCT_EXTERN_METHOD(goToPage:(nonnull NSNumber *)node
|
|
20
|
-
page:(nonnull NSNumber *)page)
|
|
21
|
-
|
|
22
|
-
@end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import React
|
|
3
|
-
|
|
4
|
-
@objc(OptimizedPdfViewManager)
|
|
5
|
-
class OptimizedPdfViewManager: RCTViewManager {
|
|
6
|
-
|
|
7
|
-
override func view() -> UIView! {
|
|
8
|
-
return OptimizedPdfView()
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
override static func requiresMainQueueSetup() -> Bool {
|
|
12
|
-
return true
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
@objc func goToPage(_ node: NSNumber, page: NSNumber) {
|
|
16
|
-
DispatchQueue.main.async {
|
|
17
|
-
if let component = self.bridge.uiManager.view(
|
|
18
|
-
forReactTag: node
|
|
19
|
-
) as? OptimizedPdfView {
|
|
20
|
-
component.page = page
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import UIKit
|
|
2
|
-
|
|
3
|
-
// View que desenha um PDF usando CATiledLayer.
|
|
4
|
-
// CATiledLayer divide o PDF em blocos ("tiles"), permitindo zoom e rolagem
|
|
5
|
-
// sem precisar renderizar o PDF inteiro de uma vez.
|
|
6
|
-
class TiledPdfPageView: UIView {
|
|
7
|
-
var pdfPage: CGPDFPage?
|
|
8
|
-
var enableAntialiasing: Bool = true
|
|
9
|
-
|
|
10
|
-
// Define o layer como CATiledLayer em vez do CALayer padrão
|
|
11
|
-
override class var layerClass: AnyClass { CATiledLayer.self }
|
|
12
|
-
private var tiledLayer: CATiledLayer { return layer as! CATiledLayer }
|
|
13
|
-
|
|
14
|
-
override init(frame: CGRect) {
|
|
15
|
-
super.init(frame: frame)
|
|
16
|
-
setupTiledLayer()
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
required init?(coder: NSCoder) {
|
|
20
|
-
super.init(coder: coder)
|
|
21
|
-
setupTiledLayer()
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Configuração do CATiledLayer
|
|
25
|
-
private func setupTiledLayer() {
|
|
26
|
-
// Tamanho de cada tile (ajustado para balancear memória e performance)
|
|
27
|
-
tiledLayer.tileSize = CGSize(width: 512, height: 512)
|
|
28
|
-
|
|
29
|
-
// Número de níveis de detalhe (zoom in/out)
|
|
30
|
-
tiledLayer.levelsOfDetail = 2
|
|
31
|
-
|
|
32
|
-
// permite zoom até 256x acima da resolução base
|
|
33
|
-
tiledLayer.levelsOfDetailBias = 8
|
|
34
|
-
|
|
35
|
-
// Mantém qualidade de acordo com a tela
|
|
36
|
-
contentScaleFactor = UIScreen.main.scale
|
|
37
|
-
|
|
38
|
-
// Fundo branco atrás do PDF
|
|
39
|
-
backgroundColor = .white
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Renderiza o conteúdo do PDF
|
|
43
|
-
override func draw(_ rect: CGRect) {
|
|
44
|
-
guard let ctx = UIGraphicsGetCurrentContext(),
|
|
45
|
-
let page = pdfPage else { return }
|
|
46
|
-
|
|
47
|
-
ctx.saveGState()
|
|
48
|
-
|
|
49
|
-
ctx.setShouldAntialias(enableAntialiasing)
|
|
50
|
-
|
|
51
|
-
// Fundo branco no pedaço atual
|
|
52
|
-
ctx.setFillColor(UIColor.white.cgColor)
|
|
53
|
-
ctx.fill(rect)
|
|
54
|
-
|
|
55
|
-
let pageRect = page.getBoxRect(.cropBox)
|
|
56
|
-
|
|
57
|
-
// Ajusta coordenadas (UIKit e PDF têm sistemas de coordenadas diferentes)
|
|
58
|
-
ctx.translateBy(x: 0, y: bounds.height)
|
|
59
|
-
ctx.scaleBy(x: 1.0, y: -1.0)
|
|
60
|
-
|
|
61
|
-
// Define a transformação para encaixar a página no espaço disponível
|
|
62
|
-
let pdfTransform = page.getDrawingTransform(.cropBox, rect: bounds, rotate: 0, preserveAspectRatio: true)
|
|
63
|
-
ctx.concatenate(pdfTransform)
|
|
64
|
-
|
|
65
|
-
// Qualidade da renderização (melhor em zoom, mais rápida em tiles distantes)
|
|
66
|
-
if tiledLayer.levelsOfDetail == 1 {
|
|
67
|
-
ctx.interpolationQuality = .low
|
|
68
|
-
} else {
|
|
69
|
-
ctx.interpolationQuality = .high
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Desenha a página PDF
|
|
73
|
-
ctx.drawPDFPage(page)
|
|
74
|
-
ctx.restoreGState()
|
|
75
|
-
}
|
|
76
|
-
}
|