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.
@@ -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/your-username/react-native-optimized-pdf"
12
+ s.homepage = "https://github.com/Herbeth-LKS/react-native-optimized-pdf"
13
13
  s.license = "MIT"
14
- s.author = { "Your Name" => "your-email@example.com" }
14
+ s.author = { "Herbeth Lucas" => "herbethlucas007@gmail.com" }
15
15
  s.platform = :ios, "12.0"
16
- s.source = { :git => "https://github.com/your-username/react-native-optimized-pdf.git", :tag => "#{s.version}" }
17
- s.source_files = "ios/*.{swift,h,m}"
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.0.0",
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
- }