react-native-optimized-pdf 1.1.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -19
- package/CODE_OF_CONDUCT.md +128 -0
- package/LICENSE +21 -0
- package/README.md +50 -35
- package/SECURITY.md +21 -0
- package/android/build.gradle +1 -1
- package/android/src/main/kotlin/com/reactnativeoptimizedpdf/OptimizedPdfView.kt +305 -143
- package/docs/android-setup.md +1 -1
- package/ios/Core/PDFConfiguration.swift +1 -1
- package/ios/Core/PDFDocumentLoader.swift +7 -4
- package/ios/Core/PDFError.swift +0 -9
- package/ios/ReactNative/OptimizedPDFViewManager.swift +0 -28
- package/ios/ReactNative/OptimizedPdfViewManager.m +0 -11
- package/ios/Views/OptimizedPDFView.swift +31 -54
- package/ios/Views/TiledPDFPageView.swift +188 -49
- package/package.json +27 -14
- package/src/OptimizedPdfView.tsx +21 -19
- package/src/components/PdfNavigationControls.tsx +1 -1
- package/src/components/PdfOverlays.tsx +3 -3
- package/src/constants.ts +0 -2
- package/src/services/pdfCache.ts +9 -3
- package/src/types/index.ts +25 -2
- package/tsconfig.json +17 -10
package/ios/Core/PDFError.swift
CHANGED
|
@@ -3,29 +3,20 @@ import Foundation
|
|
|
3
3
|
/// Errors that can occur during PDF operations.
|
|
4
4
|
enum PDFError: Error, LocalizedError {
|
|
5
5
|
case emptySource
|
|
6
|
-
case invalidPath(String)
|
|
7
6
|
case documentLoadFailed(String)
|
|
8
7
|
case passwordRequired
|
|
9
8
|
case invalidPassword
|
|
10
|
-
case pageNotFound(Int)
|
|
11
|
-
case renderingFailed
|
|
12
9
|
|
|
13
10
|
var errorDescription: String? {
|
|
14
11
|
switch self {
|
|
15
12
|
case .emptySource:
|
|
16
13
|
return "PDF source path is empty"
|
|
17
|
-
case .invalidPath(let path):
|
|
18
|
-
return "Invalid PDF path: \(path)"
|
|
19
14
|
case .documentLoadFailed(let path):
|
|
20
15
|
return "Failed to open PDF at: \(path)"
|
|
21
16
|
case .passwordRequired:
|
|
22
17
|
return "PDF is password protected"
|
|
23
18
|
case .invalidPassword:
|
|
24
19
|
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
20
|
}
|
|
30
21
|
}
|
|
31
22
|
|
|
@@ -17,32 +17,4 @@ final class OptimizedPDFViewManager: RCTViewManager {
|
|
|
17
17
|
override static func requiresMainQueueSetup() -> Bool {
|
|
18
18
|
true
|
|
19
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
20
|
}
|
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
#import <React/RCTViewManager.h>
|
|
9
|
-
#import <React/RCTUIManager.h>
|
|
10
|
-
#import <React/RCTBridgeModule.h>
|
|
11
9
|
|
|
12
10
|
@interface RCT_EXTERN_MODULE(OptimizedPdfViewManager, RCTViewManager)
|
|
13
11
|
|
|
@@ -42,13 +40,4 @@ RCT_EXPORT_VIEW_PROPERTY(onPageCount, RCTDirectEventBlock)
|
|
|
42
40
|
/// Called when the PDF requires a password to open.
|
|
43
41
|
RCT_EXPORT_VIEW_PROPERTY(onPasswordRequired, RCTDirectEventBlock)
|
|
44
42
|
|
|
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
43
|
@end
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import UIKit
|
|
2
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
|
-
}
|
|
3
|
+
import PDFKit
|
|
11
4
|
|
|
12
5
|
/// A high-performance PDF view using CATiledLayer for smooth zooming and scrolling.
|
|
13
6
|
///
|
|
14
7
|
/// This view is optimized for React Native integration, providing callbacks for
|
|
15
8
|
/// load completion, errors, and page navigation events.
|
|
9
|
+
///
|
|
10
|
+
/// Naming note: the Swift type is `OptimizedPDFView` while its `@objc` export is
|
|
11
|
+
/// `OptimizedPdfView`, and the Objective-C manager/registration is
|
|
12
|
+
/// `OptimizedPdfViewManager`. The `Pdf` spellings are the native bridge contract
|
|
13
|
+
/// with JavaScript and must not be renamed.
|
|
16
14
|
@objc(OptimizedPdfView)
|
|
17
15
|
final class OptimizedPDFView: UIScrollView {
|
|
18
16
|
|
|
@@ -33,7 +31,7 @@ final class OptimizedPDFView: UIScrollView {
|
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
/// Maximum zoom scale.
|
|
36
|
-
@objc var maximumZoom: NSNumber =
|
|
34
|
+
@objc var maximumZoom: NSNumber = 3 {
|
|
37
35
|
didSet {
|
|
38
36
|
configuration.maximumZoom = CGFloat(truncating: maximumZoom)
|
|
39
37
|
applyZoomConfiguration()
|
|
@@ -44,7 +42,7 @@ final class OptimizedPDFView: UIScrollView {
|
|
|
44
42
|
@objc var enableAntialiasing: Bool = true {
|
|
45
43
|
didSet {
|
|
46
44
|
configuration.enableAntialiasing = enableAntialiasing
|
|
47
|
-
tiledPageView
|
|
45
|
+
tiledPageView?.enableAntialiasing = enableAntialiasing
|
|
48
46
|
}
|
|
49
47
|
}
|
|
50
48
|
|
|
@@ -65,15 +63,14 @@ final class OptimizedPDFView: UIScrollView {
|
|
|
65
63
|
|
|
66
64
|
// MARK: - Public Properties
|
|
67
65
|
|
|
68
|
-
weak var pdfDelegate: OptimizedPDFViewDelegate?
|
|
69
|
-
|
|
70
66
|
/// Current number of pages in the document.
|
|
71
67
|
private(set) var pageCount: Int = 0
|
|
72
68
|
|
|
73
69
|
// MARK: - Private Properties
|
|
74
70
|
|
|
75
71
|
private var pdfDocument: CGPDFDocument?
|
|
76
|
-
private
|
|
72
|
+
private var tiledPageView: TiledPDFPageView!
|
|
73
|
+
private var pdfURL: URL?
|
|
77
74
|
private var configuration = PDFConfiguration.default
|
|
78
75
|
private let documentLoader: PDFDocumentLoading
|
|
79
76
|
|
|
@@ -81,7 +78,6 @@ final class OptimizedPDFView: UIScrollView {
|
|
|
81
78
|
private var needsLoad = false
|
|
82
79
|
private var lastBoundsSize: CGSize = .zero
|
|
83
80
|
private var currentPageRect: CGRect?
|
|
84
|
-
private var initialZoomScale: CGFloat = 1.0
|
|
85
81
|
|
|
86
82
|
// MARK: - Initialization
|
|
87
83
|
|
|
@@ -99,14 +95,6 @@ final class OptimizedPDFView: UIScrollView {
|
|
|
99
95
|
setupView()
|
|
100
96
|
}
|
|
101
97
|
|
|
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
98
|
// MARK: - Setup
|
|
111
99
|
|
|
112
100
|
private func setupView() {
|
|
@@ -135,7 +123,6 @@ final class OptimizedPDFView: UIScrollView {
|
|
|
135
123
|
|
|
136
124
|
let fitScale = calculateFitScale(for: pageRect)
|
|
137
125
|
minimumZoomScale = fitScale
|
|
138
|
-
initialZoomScale = fitScale
|
|
139
126
|
}
|
|
140
127
|
}
|
|
141
128
|
|
|
@@ -145,6 +132,8 @@ private extension OptimizedPDFView {
|
|
|
145
132
|
|
|
146
133
|
func handleSourceChange() {
|
|
147
134
|
resetDocument()
|
|
135
|
+
pdfURL = nil
|
|
136
|
+
FlattenedPageCache.shared.removeAll()
|
|
148
137
|
pendingPageIndex = page.intValue
|
|
149
138
|
needsLoad = true
|
|
150
139
|
setNeedsLayout()
|
|
@@ -173,6 +162,9 @@ private extension OptimizedPDFView {
|
|
|
173
162
|
func loadDocument() {
|
|
174
163
|
guard !source.isEmpty else { return }
|
|
175
164
|
|
|
165
|
+
let path = normalizedPDFPath(from: source)
|
|
166
|
+
pdfURL = URL(fileURLWithPath: path)
|
|
167
|
+
|
|
176
168
|
let result = documentLoader.loadDocument(from: source, password: configuration.password)
|
|
177
169
|
|
|
178
170
|
switch result {
|
|
@@ -192,7 +184,6 @@ private extension OptimizedPDFView {
|
|
|
192
184
|
DispatchQueue.main.async { [weak self] in
|
|
193
185
|
guard let self = self else { return }
|
|
194
186
|
self.onPageCount?(["numberOfPages": NSNumber(value: self.pageCount)])
|
|
195
|
-
self.pdfDelegate?.pdfView?(self, didLoadWithPageCount: self.pageCount)
|
|
196
187
|
}
|
|
197
188
|
|
|
198
189
|
// Display pending or first page
|
|
@@ -207,12 +198,10 @@ private extension OptimizedPDFView {
|
|
|
207
198
|
DispatchQueue.main.async { [weak self] in
|
|
208
199
|
guard let self = self else { return }
|
|
209
200
|
self.onPasswordRequired?([:])
|
|
210
|
-
self.pdfDelegate?.pdfViewDidRequestPassword?(self)
|
|
211
201
|
}
|
|
212
202
|
}
|
|
213
203
|
|
|
214
204
|
onError?(error.eventPayload)
|
|
215
|
-
pdfDelegate?.pdfView?(self, didFailWithError: error)
|
|
216
205
|
}
|
|
217
206
|
|
|
218
207
|
func clampPageIndex(_ index: Int, for document: CGPDFDocument) -> Int {
|
|
@@ -234,7 +223,22 @@ private extension OptimizedPDFView {
|
|
|
234
223
|
currentPageRect = pageRect
|
|
235
224
|
|
|
236
225
|
prepareForPageDisplay()
|
|
237
|
-
|
|
226
|
+
|
|
227
|
+
tiledPageView?.cancel()
|
|
228
|
+
tiledPageView?.removeFromSuperview()
|
|
229
|
+
|
|
230
|
+
let newView = TiledPDFPageView(frame: CGRect(origin: .zero, size: pageRect.size))
|
|
231
|
+
newView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
232
|
+
newView.configuration = configuration
|
|
233
|
+
newView.pdfURL = pdfURL
|
|
234
|
+
newView.pageIndex = index
|
|
235
|
+
newView.pdfPage = page
|
|
236
|
+
addSubview(newView)
|
|
237
|
+
tiledPageView = newView
|
|
238
|
+
|
|
239
|
+
contentSize = pageRect.size
|
|
240
|
+
tiledPageView.preparePage()
|
|
241
|
+
|
|
238
242
|
applyFitScale(for: pageRect)
|
|
239
243
|
centerContent()
|
|
240
244
|
notifyPageDisplayed(index: index, size: pageRect.size)
|
|
@@ -245,15 +249,9 @@ private extension OptimizedPDFView {
|
|
|
245
249
|
contentOffset = .zero
|
|
246
250
|
}
|
|
247
251
|
|
|
248
|
-
func configureTiledView(for page: CGPDFPage, rect: CGRect) {
|
|
249
|
-
tiledPageView.updateFrame(for: page)
|
|
250
|
-
contentSize = rect.size
|
|
251
|
-
}
|
|
252
|
-
|
|
253
252
|
func applyFitScale(for pageRect: CGRect) {
|
|
254
253
|
let fitScale = calculateFitScale(for: pageRect)
|
|
255
254
|
minimumZoomScale = fitScale
|
|
256
|
-
initialZoomScale = fitScale
|
|
257
255
|
setZoomScale(fitScale, animated: false)
|
|
258
256
|
}
|
|
259
257
|
|
|
@@ -267,7 +265,6 @@ private extension OptimizedPDFView {
|
|
|
267
265
|
DispatchQueue.main.async { [weak self] in
|
|
268
266
|
guard let self = self else { return }
|
|
269
267
|
self.onLoadComplete?(payload)
|
|
270
|
-
self.pdfDelegate?.pdfView?(self, didDisplayPage: index + 1, size: size)
|
|
271
268
|
}
|
|
272
269
|
}
|
|
273
270
|
}
|
|
@@ -362,23 +359,3 @@ extension OptimizedPDFView {
|
|
|
362
359
|
centerContent()
|
|
363
360
|
}
|
|
364
361
|
}
|
|
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
|
-
}
|
|
@@ -1,44 +1,100 @@
|
|
|
1
1
|
import UIKit
|
|
2
2
|
import CoreGraphics
|
|
3
|
+
import PDFKit
|
|
3
4
|
|
|
4
5
|
/// A view that renders a PDF page using CATiledLayer for efficient tiled rendering.
|
|
5
6
|
///
|
|
6
7
|
/// CATiledLayer divides the content into tiles, allowing smooth zooming and scrolling
|
|
7
8
|
/// without rendering the entire PDF at once. This is essential for large PDF documents.
|
|
9
|
+
|
|
10
|
+
// MARK: - Global cache of flattened pages
|
|
11
|
+
final class FlattenedPageCache {
|
|
12
|
+
static let shared = FlattenedPageCache()
|
|
13
|
+
|
|
14
|
+
private let cache = NSCache<NSString, FlattenedPageEntry>()
|
|
15
|
+
|
|
16
|
+
private init() {
|
|
17
|
+
cache.countLimit = 10
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func entry(forKey key: String) -> FlattenedPageEntry? {
|
|
21
|
+
cache.object(forKey: key as NSString)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func setEntry(_ entry: FlattenedPageEntry, forKey key: String) {
|
|
25
|
+
cache.setObject(entry, forKey: key as NSString)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func removeAll() {
|
|
29
|
+
cache.removeAllObjects()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
final class FlattenedPageEntry: NSObject {
|
|
34
|
+
let document: CGPDFDocument
|
|
35
|
+
let page: CGPDFPage
|
|
36
|
+
|
|
37
|
+
init(document: CGPDFDocument, page: CGPDFPage) {
|
|
38
|
+
self.document = document
|
|
39
|
+
self.page = page
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// MARK: - Atomic flag for thread-safe cancellation
|
|
44
|
+
|
|
45
|
+
final class OSAtomicFlag {
|
|
46
|
+
private var _value: Bool = false
|
|
47
|
+
private var _lock = os_unfair_lock()
|
|
48
|
+
|
|
49
|
+
func set() {
|
|
50
|
+
os_unfair_lock_lock(&_lock)
|
|
51
|
+
_value = true
|
|
52
|
+
os_unfair_lock_unlock(&_lock)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func reset() {
|
|
56
|
+
os_unfair_lock_lock(&_lock)
|
|
57
|
+
_value = false
|
|
58
|
+
os_unfair_lock_unlock(&_lock)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func isSet() -> Bool {
|
|
62
|
+
os_unfair_lock_lock(&_lock)
|
|
63
|
+
let v = _value
|
|
64
|
+
os_unfair_lock_unlock(&_lock)
|
|
65
|
+
return v
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// MARK: - TiledPDFPageView
|
|
70
|
+
|
|
8
71
|
final class TiledPDFPageView: UIView {
|
|
9
72
|
|
|
10
73
|
// MARK: - Properties
|
|
11
74
|
|
|
12
|
-
/// The PDF page to render.
|
|
13
75
|
var pdfPage: CGPDFPage? {
|
|
14
|
-
didSet {
|
|
15
|
-
setNeedsDisplay()
|
|
16
|
-
}
|
|
76
|
+
didSet { setNeedsDisplay() }
|
|
17
77
|
}
|
|
18
78
|
|
|
19
|
-
|
|
79
|
+
var pdfURL: URL?
|
|
80
|
+
var pageIndex: Int = 0
|
|
81
|
+
|
|
20
82
|
var enableAntialiasing: Bool = true {
|
|
21
|
-
didSet {
|
|
22
|
-
setNeedsDisplay()
|
|
23
|
-
}
|
|
83
|
+
didSet { setNeedsDisplay() }
|
|
24
84
|
}
|
|
25
85
|
|
|
26
|
-
/// The configuration for tiled rendering.
|
|
27
86
|
var configuration: PDFConfiguration = .default {
|
|
28
|
-
didSet {
|
|
29
|
-
applyConfiguration()
|
|
30
|
-
}
|
|
87
|
+
didSet { applyConfiguration() }
|
|
31
88
|
}
|
|
32
89
|
|
|
33
|
-
|
|
34
|
-
override class var layerClass: AnyClass {
|
|
35
|
-
CATiledLayer.self
|
|
36
|
-
}
|
|
90
|
+
override class var layerClass: AnyClass { CATiledLayer.self }
|
|
37
91
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
92
|
+
private var tiledLayer: CATiledLayer { layer as! CATiledLayer }
|
|
93
|
+
|
|
94
|
+
private var _activePage: CGPDFPage?
|
|
95
|
+
private var _pdfKitDocument: PDFDocument?
|
|
96
|
+
private var _generation: UInt = 0
|
|
97
|
+
private let _cancelled = OSAtomicFlag()
|
|
42
98
|
|
|
43
99
|
// MARK: - Initialization
|
|
44
100
|
|
|
@@ -52,7 +108,6 @@ final class TiledPDFPageView: UIView {
|
|
|
52
108
|
commonInit()
|
|
53
109
|
}
|
|
54
110
|
|
|
55
|
-
/// Initializes with a specific configuration.
|
|
56
111
|
convenience init(frame: CGRect, configuration: PDFConfiguration) {
|
|
57
112
|
self.init(frame: frame)
|
|
58
113
|
self.configuration = configuration
|
|
@@ -74,35 +129,139 @@ final class TiledPDFPageView: UIView {
|
|
|
74
129
|
enableAntialiasing = configuration.enableAntialiasing
|
|
75
130
|
}
|
|
76
131
|
|
|
132
|
+
// MARK: - Cancellation
|
|
133
|
+
|
|
134
|
+
func cancel() {
|
|
135
|
+
_cancelled.set()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private var currentCacheKey: String {
|
|
139
|
+
guard let url = pdfURL else { return "" }
|
|
140
|
+
return "\(url.absoluteString)_page\(pageIndex)"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// MARK: - Prepare Page (Flatten Pipeline)
|
|
144
|
+
|
|
145
|
+
func preparePage() {
|
|
146
|
+
_cancelled.reset()
|
|
147
|
+
_generation &+= 1
|
|
148
|
+
let currentGen = _generation
|
|
149
|
+
|
|
150
|
+
let cacheKey = currentCacheKey
|
|
151
|
+
if let cached = FlattenedPageCache.shared.entry(forKey: cacheKey) {
|
|
152
|
+
_activePage = cached.page
|
|
153
|
+
setNeedsDisplay()
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_activePage = pdfPage
|
|
158
|
+
|
|
159
|
+
guard let url = pdfURL,
|
|
160
|
+
let originalPage = pdfPage else { return }
|
|
161
|
+
|
|
162
|
+
let idx = pageIndex
|
|
163
|
+
let originalCropBox = originalPage.getBoxRect(.cropBox)
|
|
164
|
+
let rotation = originalPage.rotationAngle
|
|
165
|
+
|
|
166
|
+
let pdfKitDoc: PDFDocument
|
|
167
|
+
if let existing = _pdfKitDocument {
|
|
168
|
+
pdfKitDoc = existing
|
|
169
|
+
} else if let newDoc = PDFDocument(url: url) {
|
|
170
|
+
_pdfKitDocument = newDoc
|
|
171
|
+
pdfKitDoc = newDoc
|
|
172
|
+
} else {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
guard let pdfKitPage = pdfKitDoc.page(at: idx) else { return }
|
|
177
|
+
let annotations = pdfKitPage.annotations
|
|
178
|
+
if annotations.isEmpty { return }
|
|
179
|
+
|
|
180
|
+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
181
|
+
guard let self = self else { return }
|
|
182
|
+
|
|
183
|
+
let entry = Self.createFlattenedEntry(
|
|
184
|
+
pdfKitPage: pdfKitPage,
|
|
185
|
+
originalCropBox: originalCropBox,
|
|
186
|
+
rotation: Int(rotation)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
guard let entry = entry else { return }
|
|
190
|
+
FlattenedPageCache.shared.setEntry(entry, forKey: cacheKey)
|
|
191
|
+
|
|
192
|
+
DispatchQueue.main.async { [weak self] in
|
|
193
|
+
guard let self = self, self._generation == currentGen else { return }
|
|
194
|
+
self._activePage = entry.page
|
|
195
|
+
self.setNeedsDisplay()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// MARK: - Flattening Logic
|
|
201
|
+
private static func createFlattenedEntry(
|
|
202
|
+
pdfKitPage: PDFPage,
|
|
203
|
+
originalCropBox: CGRect,
|
|
204
|
+
rotation: Int
|
|
205
|
+
) -> FlattenedPageEntry? {
|
|
206
|
+
let finalSize: CGSize
|
|
207
|
+
if rotation == 90 || rotation == 270 {
|
|
208
|
+
finalSize = CGSize(width: originalCropBox.height, height: originalCropBox.width)
|
|
209
|
+
} else {
|
|
210
|
+
finalSize = originalCropBox.size
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
var mediaBox = CGRect(origin: .zero, size: finalSize)
|
|
214
|
+
|
|
215
|
+
let pdfData = NSMutableData()
|
|
216
|
+
guard let consumer = CGDataConsumer(data: pdfData as CFMutableData),
|
|
217
|
+
let pdfContext = CGContext(consumer: consumer, mediaBox: &mediaBox, nil) else {
|
|
218
|
+
return nil
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
pdfContext.beginPage(mediaBox: &mediaBox)
|
|
222
|
+
|
|
223
|
+
if originalCropBox.origin.x != 0 || originalCropBox.origin.y != 0 {
|
|
224
|
+
pdfContext.translateBy(x: -originalCropBox.origin.x, y: -originalCropBox.origin.y)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
pdfKitPage.draw(with: .cropBox, to: pdfContext)
|
|
228
|
+
|
|
229
|
+
pdfContext.endPage()
|
|
230
|
+
pdfContext.closePDF()
|
|
231
|
+
|
|
232
|
+
guard let provider = CGDataProvider(data: pdfData as CFData),
|
|
233
|
+
let flattenedDoc = CGPDFDocument(provider),
|
|
234
|
+
let flattenedCGPage = flattenedDoc.page(at: 1) else {
|
|
235
|
+
return nil
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return FlattenedPageEntry(document: flattenedDoc, page: flattenedCGPage)
|
|
239
|
+
}
|
|
240
|
+
|
|
77
241
|
// MARK: - Rendering
|
|
78
242
|
|
|
79
243
|
override func draw(_ rect: CGRect) {
|
|
244
|
+
guard !_cancelled.isSet() else { return }
|
|
245
|
+
|
|
80
246
|
guard let context = UIGraphicsGetCurrentContext(),
|
|
81
|
-
let page =
|
|
82
|
-
return
|
|
83
|
-
}
|
|
247
|
+
let page = _activePage else { return }
|
|
84
248
|
|
|
85
249
|
renderPage(page, in: context, rect: rect)
|
|
86
250
|
}
|
|
87
251
|
|
|
88
|
-
/// Renders the PDF page in the given graphics context.
|
|
89
252
|
private func renderPage(_ page: CGPDFPage, in context: CGContext, rect: CGRect) {
|
|
90
253
|
context.saveGState()
|
|
91
254
|
defer { context.restoreGState() }
|
|
92
255
|
|
|
93
|
-
// Configure rendering quality
|
|
94
256
|
context.setShouldAntialias(enableAntialiasing)
|
|
95
257
|
context.interpolationQuality = determineInterpolationQuality()
|
|
96
258
|
|
|
97
|
-
// Fill background
|
|
98
259
|
context.setFillColor(UIColor.white.cgColor)
|
|
99
260
|
context.fill(rect)
|
|
100
261
|
|
|
101
|
-
// Transform coordinate system (UIKit uses top-left origin, PDF uses bottom-left)
|
|
102
262
|
context.translateBy(x: 0, y: bounds.height)
|
|
103
263
|
context.scaleBy(x: 1.0, y: -1.0)
|
|
104
264
|
|
|
105
|
-
// Apply PDF transform to fit the page
|
|
106
265
|
let pdfTransform = page.getDrawingTransform(
|
|
107
266
|
.cropBox,
|
|
108
267
|
rect: bounds,
|
|
@@ -110,31 +269,11 @@ final class TiledPDFPageView: UIView {
|
|
|
110
269
|
preserveAspectRatio: true
|
|
111
270
|
)
|
|
112
271
|
context.concatenate(pdfTransform)
|
|
113
|
-
|
|
114
272
|
// Draw the page
|
|
115
273
|
context.drawPDFPage(page)
|
|
116
274
|
}
|
|
117
275
|
|
|
118
|
-
/// Determines the interpolation quality based on zoom level.
|
|
119
276
|
private func determineInterpolationQuality() -> CGInterpolationQuality {
|
|
120
277
|
tiledLayer.levelsOfDetail == 1 ? .low : .high
|
|
121
278
|
}
|
|
122
279
|
}
|
|
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,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-optimized-pdf",
|
|
3
|
-
"version": "1.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "React Native PDF viewer with high performance, zoom, and support for large PDF files. Fast, memory-efficient, and supports annotations (annots) on iOS and Android.",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "
|
|
8
|
+
"test": "jest",
|
|
9
|
+
"test:coverage": "jest --coverage --coverageReporters=text",
|
|
9
10
|
"lint": "eslint . --ext .ts,.tsx",
|
|
10
11
|
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
|
11
12
|
"format": "prettier --write \"**/*.{ts,tsx,js,json,md}\"",
|
|
@@ -14,21 +15,27 @@
|
|
|
14
15
|
},
|
|
15
16
|
"keywords": [
|
|
16
17
|
"react-native",
|
|
18
|
+
"react-native-pdf",
|
|
17
19
|
"pdf",
|
|
18
20
|
"pdf-viewer",
|
|
19
|
-
"viewer",
|
|
21
|
+
"react-native-pdf-viewer",
|
|
22
|
+
"pdf-reader",
|
|
23
|
+
"pdf-renderer",
|
|
24
|
+
"pdf-display",
|
|
20
25
|
"ios",
|
|
21
26
|
"android",
|
|
22
|
-
"performance",
|
|
23
|
-
"high-performance",
|
|
24
27
|
"large-pdf",
|
|
28
|
+
"big-pdf",
|
|
29
|
+
"pdf-performance",
|
|
30
|
+
"fast-pdf",
|
|
25
31
|
"memory-efficient",
|
|
26
32
|
"tiled-rendering",
|
|
27
33
|
"zoom",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
34
|
+
"annotations",
|
|
35
|
+
"annots",
|
|
36
|
+
"pdfkit",
|
|
37
|
+
"pdfium",
|
|
38
|
+
"catiledlayer"
|
|
32
39
|
],
|
|
33
40
|
"author": "Your Name",
|
|
34
41
|
"license": "MIT",
|
|
@@ -38,16 +45,22 @@
|
|
|
38
45
|
},
|
|
39
46
|
"devDependencies": {
|
|
40
47
|
"@react-native-community/eslint-config": "^3.2.0",
|
|
48
|
+
"@testing-library/react-native": "^13.3.1",
|
|
41
49
|
"@types/crypto-js": "^4.2.2",
|
|
42
|
-
"@types/
|
|
43
|
-
"@types/react
|
|
50
|
+
"@types/jest": "^29.5.14",
|
|
51
|
+
"@types/react": "^18.2.6",
|
|
52
|
+
"@types/react-native": "^0.64.19",
|
|
44
53
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
45
54
|
"@typescript-eslint/parser": "^6.0.0",
|
|
46
55
|
"eslint": "^8.0.0",
|
|
47
56
|
"eslint-config-prettier": "^10.1.8",
|
|
48
57
|
"eslint-plugin-jest": "^29.11.0",
|
|
49
58
|
"eslint-plugin-prettier": "^5.5.4",
|
|
59
|
+
"jest": "^29.7.0",
|
|
50
60
|
"prettier": "2.8.8",
|
|
61
|
+
"react": "18.2.0",
|
|
62
|
+
"react-native": "0.73.11",
|
|
63
|
+
"react-test-renderer": "18.2.0",
|
|
51
64
|
"typescript": "^5.0.0"
|
|
52
65
|
},
|
|
53
66
|
"repository": {
|
|
@@ -55,7 +68,7 @@
|
|
|
55
68
|
"url": "https://github.com/Herbeth-LKS/react-native-optimized-pdf"
|
|
56
69
|
},
|
|
57
70
|
"dependencies": {
|
|
58
|
-
"
|
|
59
|
-
"
|
|
71
|
+
"@dr.pogodin/react-native-fs": "^2.41.0",
|
|
72
|
+
"crypto-js": "^4.2.0"
|
|
60
73
|
}
|
|
61
74
|
}
|