react-native-optimized-pdf 1.1.0 → 1.2.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/CHANGELOG.md +12 -18
- package/CODE_OF_CONDUCT.md +128 -0
- package/LICENSE +21 -0
- package/README.md +23 -32
- 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/Views/OptimizedPDFView.swift +25 -8
- package/ios/Views/TiledPDFPageView.swift +204 -32
- package/package.json +19 -11
- package/src/OptimizedPdfView.tsx +20 -14
- package/src/components/PdfNavigationControls.tsx +1 -1
- package/src/components/PdfOverlays.tsx +3 -3
- package/src/types/index.ts +25 -0
- package/tsconfig.json +17 -10
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import UIKit
|
|
2
2
|
import React
|
|
3
|
+
import PDFKit
|
|
3
4
|
|
|
4
5
|
/// Protocol for receiving PDF view events.
|
|
5
6
|
@objc protocol OptimizedPDFViewDelegate: AnyObject {
|
|
@@ -44,7 +45,7 @@ final class OptimizedPDFView: UIScrollView {
|
|
|
44
45
|
@objc var enableAntialiasing: Bool = true {
|
|
45
46
|
didSet {
|
|
46
47
|
configuration.enableAntialiasing = enableAntialiasing
|
|
47
|
-
tiledPageView
|
|
48
|
+
tiledPageView?.enableAntialiasing = enableAntialiasing
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -73,7 +74,8 @@ final class OptimizedPDFView: UIScrollView {
|
|
|
73
74
|
// MARK: - Private Properties
|
|
74
75
|
|
|
75
76
|
private var pdfDocument: CGPDFDocument?
|
|
76
|
-
private
|
|
77
|
+
private var tiledPageView: TiledPDFPageView!
|
|
78
|
+
private var pdfURL: URL?
|
|
77
79
|
private var configuration = PDFConfiguration.default
|
|
78
80
|
private let documentLoader: PDFDocumentLoading
|
|
79
81
|
|
|
@@ -145,6 +147,8 @@ private extension OptimizedPDFView {
|
|
|
145
147
|
|
|
146
148
|
func handleSourceChange() {
|
|
147
149
|
resetDocument()
|
|
150
|
+
pdfURL = nil
|
|
151
|
+
FlattenedPageCache.shared.removeAll()
|
|
148
152
|
pendingPageIndex = page.intValue
|
|
149
153
|
needsLoad = true
|
|
150
154
|
setNeedsLayout()
|
|
@@ -173,6 +177,9 @@ private extension OptimizedPDFView {
|
|
|
173
177
|
func loadDocument() {
|
|
174
178
|
guard !source.isEmpty else { return }
|
|
175
179
|
|
|
180
|
+
let path = source.hasPrefix("file://") ? String(source.dropFirst(7)) : source
|
|
181
|
+
pdfURL = URL(fileURLWithPath: path)
|
|
182
|
+
|
|
176
183
|
let result = documentLoader.loadDocument(from: source, password: configuration.password)
|
|
177
184
|
|
|
178
185
|
switch result {
|
|
@@ -234,7 +241,22 @@ private extension OptimizedPDFView {
|
|
|
234
241
|
currentPageRect = pageRect
|
|
235
242
|
|
|
236
243
|
prepareForPageDisplay()
|
|
237
|
-
|
|
244
|
+
|
|
245
|
+
tiledPageView?.cancel()
|
|
246
|
+
tiledPageView?.removeFromSuperview()
|
|
247
|
+
|
|
248
|
+
let newView = TiledPDFPageView(frame: CGRect(origin: .zero, size: pageRect.size))
|
|
249
|
+
newView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
250
|
+
newView.configuration = configuration
|
|
251
|
+
newView.pdfURL = pdfURL
|
|
252
|
+
newView.pageIndex = index
|
|
253
|
+
newView.pdfPage = page
|
|
254
|
+
addSubview(newView)
|
|
255
|
+
tiledPageView = newView
|
|
256
|
+
|
|
257
|
+
contentSize = pageRect.size
|
|
258
|
+
tiledPageView.preparePage()
|
|
259
|
+
|
|
238
260
|
applyFitScale(for: pageRect)
|
|
239
261
|
centerContent()
|
|
240
262
|
notifyPageDisplayed(index: index, size: pageRect.size)
|
|
@@ -245,11 +267,6 @@ private extension OptimizedPDFView {
|
|
|
245
267
|
contentOffset = .zero
|
|
246
268
|
}
|
|
247
269
|
|
|
248
|
-
func configureTiledView(for page: CGPDFPage, rect: CGRect) {
|
|
249
|
-
tiledPageView.updateFrame(for: page)
|
|
250
|
-
contentSize = rect.size
|
|
251
|
-
}
|
|
252
|
-
|
|
253
270
|
func applyFitScale(for pageRect: CGRect) {
|
|
254
271
|
let fitScale = calculateFitScale(for: pageRect)
|
|
255
272
|
minimumZoomScale = fitScale
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import CoreGraphics
|
|
3
|
+
import PDFKit
|
|
4
|
+
|
|
5
|
+
|
|
1
6
|
import UIKit
|
|
2
7
|
import CoreGraphics
|
|
3
8
|
|
|
@@ -5,40 +10,93 @@ import CoreGraphics
|
|
|
5
10
|
///
|
|
6
11
|
/// CATiledLayer divides the content into tiles, allowing smooth zooming and scrolling
|
|
7
12
|
/// without rendering the entire PDF at once. This is essential for large PDF documents.
|
|
13
|
+
|
|
14
|
+
// MARK: - Global cache of flattened pages
|
|
15
|
+
final class FlattenedPageCache {
|
|
16
|
+
static let shared = FlattenedPageCache()
|
|
17
|
+
|
|
18
|
+
private let cache = NSCache<NSString, FlattenedPageEntry>()
|
|
19
|
+
|
|
20
|
+
private init() {
|
|
21
|
+
cache.countLimit = 10
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func entry(forKey key: String) -> FlattenedPageEntry? {
|
|
25
|
+
cache.object(forKey: key as NSString)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func setEntry(_ entry: FlattenedPageEntry, forKey key: String) {
|
|
29
|
+
cache.setObject(entry, forKey: key as NSString)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func removeEntry(forKey key: String) {
|
|
33
|
+
cache.removeObject(forKey: key as NSString)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func removeAll() {
|
|
37
|
+
cache.removeAllObjects()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
final class FlattenedPageEntry: NSObject {
|
|
42
|
+
let document: CGPDFDocument
|
|
43
|
+
let page: CGPDFPage
|
|
44
|
+
|
|
45
|
+
init(document: CGPDFDocument, page: CGPDFPage) {
|
|
46
|
+
self.document = document
|
|
47
|
+
self.page = page
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// MARK: - Atomic flag for thread-safe cancellation
|
|
52
|
+
|
|
53
|
+
final class OSAtomicFlag {
|
|
54
|
+
private var _value: Bool = false
|
|
55
|
+
private var _lock = os_unfair_lock()
|
|
56
|
+
|
|
57
|
+
func set() {
|
|
58
|
+
os_unfair_lock_lock(&_lock)
|
|
59
|
+
_value = true
|
|
60
|
+
os_unfair_lock_unlock(&_lock)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func isSet() -> Bool {
|
|
64
|
+
os_unfair_lock_lock(&_lock)
|
|
65
|
+
let v = _value
|
|
66
|
+
os_unfair_lock_unlock(&_lock)
|
|
67
|
+
return v
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// MARK: - TiledPDFPageView
|
|
72
|
+
|
|
8
73
|
final class TiledPDFPageView: UIView {
|
|
9
74
|
|
|
10
75
|
// MARK: - Properties
|
|
11
76
|
|
|
12
|
-
/// The PDF page to render.
|
|
13
77
|
var pdfPage: CGPDFPage? {
|
|
14
|
-
didSet {
|
|
15
|
-
setNeedsDisplay()
|
|
16
|
-
}
|
|
78
|
+
didSet { setNeedsDisplay() }
|
|
17
79
|
}
|
|
18
80
|
|
|
19
|
-
|
|
81
|
+
var pdfURL: URL?
|
|
82
|
+
var pageIndex: Int = 0
|
|
83
|
+
|
|
20
84
|
var enableAntialiasing: Bool = true {
|
|
21
|
-
didSet {
|
|
22
|
-
setNeedsDisplay()
|
|
23
|
-
}
|
|
85
|
+
didSet { setNeedsDisplay() }
|
|
24
86
|
}
|
|
25
87
|
|
|
26
|
-
/// The configuration for tiled rendering.
|
|
27
88
|
var configuration: PDFConfiguration = .default {
|
|
28
|
-
didSet {
|
|
29
|
-
applyConfiguration()
|
|
30
|
-
}
|
|
89
|
+
didSet { applyConfiguration() }
|
|
31
90
|
}
|
|
32
91
|
|
|
33
|
-
|
|
34
|
-
override class var layerClass: AnyClass {
|
|
35
|
-
CATiledLayer.self
|
|
36
|
-
}
|
|
92
|
+
override class var layerClass: AnyClass { CATiledLayer.self }
|
|
37
93
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
94
|
+
private var tiledLayer: CATiledLayer { layer as! CATiledLayer }
|
|
95
|
+
|
|
96
|
+
private var _activePage: CGPDFPage?
|
|
97
|
+
private var _pdfKitDocument: PDFDocument?
|
|
98
|
+
private var _generation: UInt = 0
|
|
99
|
+
private let _cancelled = OSAtomicFlag()
|
|
42
100
|
|
|
43
101
|
// MARK: - Initialization
|
|
44
102
|
|
|
@@ -52,7 +110,6 @@ final class TiledPDFPageView: UIView {
|
|
|
52
110
|
commonInit()
|
|
53
111
|
}
|
|
54
112
|
|
|
55
|
-
/// Initializes with a specific configuration.
|
|
56
113
|
convenience init(frame: CGRect, configuration: PDFConfiguration) {
|
|
57
114
|
self.init(frame: frame)
|
|
58
115
|
self.configuration = configuration
|
|
@@ -74,35 +131,138 @@ final class TiledPDFPageView: UIView {
|
|
|
74
131
|
enableAntialiasing = configuration.enableAntialiasing
|
|
75
132
|
}
|
|
76
133
|
|
|
134
|
+
// MARK: - Cancellation
|
|
135
|
+
|
|
136
|
+
func cancel() {
|
|
137
|
+
_cancelled.set()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private var currentCacheKey: String {
|
|
141
|
+
guard let url = pdfURL else { return "" }
|
|
142
|
+
return "\(url.absoluteString)_page\(pageIndex)"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// MARK: - Prepare Page (Flatten Pipeline)
|
|
146
|
+
|
|
147
|
+
func preparePage() {
|
|
148
|
+
_generation &+= 1
|
|
149
|
+
let currentGen = _generation
|
|
150
|
+
|
|
151
|
+
let cacheKey = currentCacheKey
|
|
152
|
+
if let cached = FlattenedPageCache.shared.entry(forKey: cacheKey) {
|
|
153
|
+
_activePage = cached.page
|
|
154
|
+
setNeedsDisplay()
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
_activePage = pdfPage
|
|
159
|
+
|
|
160
|
+
guard let url = pdfURL,
|
|
161
|
+
let originalPage = pdfPage else { return }
|
|
162
|
+
|
|
163
|
+
let idx = pageIndex
|
|
164
|
+
let originalCropBox = originalPage.getBoxRect(.cropBox)
|
|
165
|
+
let rotation = originalPage.rotationAngle
|
|
166
|
+
|
|
167
|
+
let pdfKitDoc: PDFDocument
|
|
168
|
+
if let existing = _pdfKitDocument {
|
|
169
|
+
pdfKitDoc = existing
|
|
170
|
+
} else if let newDoc = PDFDocument(url: url) {
|
|
171
|
+
_pdfKitDocument = newDoc
|
|
172
|
+
pdfKitDoc = newDoc
|
|
173
|
+
} else {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
guard let pdfKitPage = pdfKitDoc.page(at: idx) else { return }
|
|
178
|
+
let annotations = pdfKitPage.annotations
|
|
179
|
+
if annotations.isEmpty { return }
|
|
180
|
+
|
|
181
|
+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
182
|
+
guard let self = self else { return }
|
|
183
|
+
|
|
184
|
+
let entry = Self.createFlattenedEntry(
|
|
185
|
+
pdfKitPage: pdfKitPage,
|
|
186
|
+
originalCropBox: originalCropBox,
|
|
187
|
+
rotation: Int(rotation)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
guard let entry = entry else { return }
|
|
191
|
+
FlattenedPageCache.shared.setEntry(entry, forKey: cacheKey)
|
|
192
|
+
|
|
193
|
+
DispatchQueue.main.async { [weak self] in
|
|
194
|
+
guard let self = self, self._generation == currentGen else { return }
|
|
195
|
+
self._activePage = entry.page
|
|
196
|
+
self.setNeedsDisplay()
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// MARK: - Flattening Logic
|
|
202
|
+
private static func createFlattenedEntry(
|
|
203
|
+
pdfKitPage: PDFPage,
|
|
204
|
+
originalCropBox: CGRect,
|
|
205
|
+
rotation: Int
|
|
206
|
+
) -> FlattenedPageEntry? {
|
|
207
|
+
let finalSize: CGSize
|
|
208
|
+
if rotation == 90 || rotation == 270 {
|
|
209
|
+
finalSize = CGSize(width: originalCropBox.height, height: originalCropBox.width)
|
|
210
|
+
} else {
|
|
211
|
+
finalSize = originalCropBox.size
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
var mediaBox = CGRect(origin: .zero, size: finalSize)
|
|
215
|
+
|
|
216
|
+
let pdfData = NSMutableData()
|
|
217
|
+
guard let consumer = CGDataConsumer(data: pdfData as CFMutableData),
|
|
218
|
+
let pdfContext = CGContext(consumer: consumer, mediaBox: &mediaBox, nil) else {
|
|
219
|
+
return nil
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
pdfContext.beginPage(mediaBox: &mediaBox)
|
|
223
|
+
|
|
224
|
+
if originalCropBox.origin.x != 0 || originalCropBox.origin.y != 0 {
|
|
225
|
+
pdfContext.translateBy(x: -originalCropBox.origin.x, y: -originalCropBox.origin.y)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
pdfKitPage.draw(with: .cropBox, to: pdfContext)
|
|
229
|
+
|
|
230
|
+
pdfContext.endPage()
|
|
231
|
+
pdfContext.closePDF()
|
|
232
|
+
|
|
233
|
+
guard let provider = CGDataProvider(data: pdfData as CFData),
|
|
234
|
+
let flattenedDoc = CGPDFDocument(provider),
|
|
235
|
+
let flattenedCGPage = flattenedDoc.page(at: 1) else {
|
|
236
|
+
return nil
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return FlattenedPageEntry(document: flattenedDoc, page: flattenedCGPage)
|
|
240
|
+
}
|
|
241
|
+
|
|
77
242
|
// MARK: - Rendering
|
|
78
243
|
|
|
79
244
|
override func draw(_ rect: CGRect) {
|
|
245
|
+
guard !_cancelled.isSet() else { return }
|
|
246
|
+
|
|
80
247
|
guard let context = UIGraphicsGetCurrentContext(),
|
|
81
|
-
let page =
|
|
82
|
-
return
|
|
83
|
-
}
|
|
248
|
+
let page = _activePage else { return }
|
|
84
249
|
|
|
85
250
|
renderPage(page, in: context, rect: rect)
|
|
86
251
|
}
|
|
87
252
|
|
|
88
|
-
/// Renders the PDF page in the given graphics context.
|
|
89
253
|
private func renderPage(_ page: CGPDFPage, in context: CGContext, rect: CGRect) {
|
|
90
254
|
context.saveGState()
|
|
91
255
|
defer { context.restoreGState() }
|
|
92
256
|
|
|
93
|
-
// Configure rendering quality
|
|
94
257
|
context.setShouldAntialias(enableAntialiasing)
|
|
95
258
|
context.interpolationQuality = determineInterpolationQuality()
|
|
96
259
|
|
|
97
|
-
// Fill background
|
|
98
260
|
context.setFillColor(UIColor.white.cgColor)
|
|
99
261
|
context.fill(rect)
|
|
100
262
|
|
|
101
|
-
// Transform coordinate system (UIKit uses top-left origin, PDF uses bottom-left)
|
|
102
263
|
context.translateBy(x: 0, y: bounds.height)
|
|
103
264
|
context.scaleBy(x: 1.0, y: -1.0)
|
|
104
265
|
|
|
105
|
-
// Apply PDF transform to fit the page
|
|
106
266
|
let pdfTransform = page.getDrawingTransform(
|
|
107
267
|
.cropBox,
|
|
108
268
|
rect: bounds,
|
|
@@ -110,12 +270,10 @@ final class TiledPDFPageView: UIView {
|
|
|
110
270
|
preserveAspectRatio: true
|
|
111
271
|
)
|
|
112
272
|
context.concatenate(pdfTransform)
|
|
113
|
-
|
|
114
273
|
// Draw the page
|
|
115
274
|
context.drawPDFPage(page)
|
|
116
275
|
}
|
|
117
276
|
|
|
118
|
-
/// Determines the interpolation quality based on zoom level.
|
|
119
277
|
private func determineInterpolationQuality() -> CGInterpolationQuality {
|
|
120
278
|
tiledLayer.levelsOfDetail == 1 ? .low : .high
|
|
121
279
|
}
|
|
@@ -125,9 +283,9 @@ final class TiledPDFPageView: UIView {
|
|
|
125
283
|
|
|
126
284
|
extension TiledPDFPageView {
|
|
127
285
|
|
|
128
|
-
/// Prepares the view for displaying a new page.
|
|
129
286
|
func prepareForReuse() {
|
|
130
287
|
pdfPage = nil
|
|
288
|
+
_activePage = nil
|
|
131
289
|
layer.contents = nil
|
|
132
290
|
}
|
|
133
291
|
|
|
@@ -137,4 +295,18 @@ extension TiledPDFPageView {
|
|
|
137
295
|
frame = CGRect(origin: .zero, size: pageRect.size)
|
|
138
296
|
pdfPage = page
|
|
139
297
|
}
|
|
298
|
+
|
|
299
|
+
func invalidateCache() {
|
|
300
|
+
FlattenedPageCache.shared.removeEntry(forKey: currentCacheKey)
|
|
301
|
+
_activePage = pdfPage
|
|
302
|
+
_pdfKitDocument = nil
|
|
303
|
+
setNeedsDisplay()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
func invalidateAllCache() {
|
|
307
|
+
FlattenedPageCache.shared.removeAll()
|
|
308
|
+
_pdfKitDocument = nil
|
|
309
|
+
_activePage = pdfPage
|
|
310
|
+
setNeedsDisplay()
|
|
311
|
+
}
|
|
140
312
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-optimized-pdf",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.2.0",
|
|
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": {
|
|
@@ -14,21 +14,27 @@
|
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
16
16
|
"react-native",
|
|
17
|
+
"react-native-pdf",
|
|
17
18
|
"pdf",
|
|
18
19
|
"pdf-viewer",
|
|
19
|
-
"viewer",
|
|
20
|
+
"react-native-pdf-viewer",
|
|
21
|
+
"pdf-reader",
|
|
22
|
+
"pdf-renderer",
|
|
23
|
+
"pdf-display",
|
|
20
24
|
"ios",
|
|
21
25
|
"android",
|
|
22
|
-
"performance",
|
|
23
|
-
"high-performance",
|
|
24
26
|
"large-pdf",
|
|
27
|
+
"big-pdf",
|
|
28
|
+
"pdf-performance",
|
|
29
|
+
"fast-pdf",
|
|
25
30
|
"memory-efficient",
|
|
26
31
|
"tiled-rendering",
|
|
27
32
|
"zoom",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
33
|
+
"annotations",
|
|
34
|
+
"annots",
|
|
35
|
+
"pdfkit",
|
|
36
|
+
"pdfium",
|
|
37
|
+
"catiledlayer"
|
|
32
38
|
],
|
|
33
39
|
"author": "Your Name",
|
|
34
40
|
"license": "MIT",
|
|
@@ -39,8 +45,8 @@
|
|
|
39
45
|
"devDependencies": {
|
|
40
46
|
"@react-native-community/eslint-config": "^3.2.0",
|
|
41
47
|
"@types/crypto-js": "^4.2.2",
|
|
42
|
-
"@types/react": "^18.
|
|
43
|
-
"@types/react-native": "^0.
|
|
48
|
+
"@types/react": "^18.2.6",
|
|
49
|
+
"@types/react-native": "^0.64.19",
|
|
44
50
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
45
51
|
"@typescript-eslint/parser": "^6.0.0",
|
|
46
52
|
"eslint": "^8.0.0",
|
|
@@ -48,6 +54,8 @@
|
|
|
48
54
|
"eslint-plugin-jest": "^29.11.0",
|
|
49
55
|
"eslint-plugin-prettier": "^5.5.4",
|
|
50
56
|
"prettier": "2.8.8",
|
|
57
|
+
"react": "18.2.0",
|
|
58
|
+
"react-native": "0.73.11",
|
|
51
59
|
"typescript": "^5.0.0"
|
|
52
60
|
},
|
|
53
61
|
"repository": {
|
package/src/OptimizedPdfView.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
-
|
|
2
|
+
import { View, Platform, requireNativeComponent } from 'react-native';
|
|
3
3
|
import { PdfCacheService } from './services/pdfCache';
|
|
4
4
|
import { PdfNavigationControls } from './components/PdfNavigationControls';
|
|
5
5
|
import { PdfLoadingOverlay, PdfErrorOverlay } from './components/PdfOverlays';
|
|
@@ -9,11 +9,16 @@ import {
|
|
|
9
9
|
DEFAULT_SHOW_NAVIGATION_CONTROLS,
|
|
10
10
|
ERROR_MESSAGES,
|
|
11
11
|
} from './constants';
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
OptimizedPdfViewProps,
|
|
14
|
+
NativeLoadCompleteEvent,
|
|
15
|
+
NativePageCountEvent,
|
|
16
|
+
NativeOptimizedPdfViewProps,
|
|
17
|
+
} from './types';
|
|
13
18
|
|
|
14
19
|
const NativeOptimizedPdfView =
|
|
15
20
|
Platform.OS === 'ios' || Platform.OS === 'android'
|
|
16
|
-
? requireNativeComponent('OptimizedPdfView')
|
|
21
|
+
? requireNativeComponent<NativeOptimizedPdfViewProps>('OptimizedPdfView')
|
|
17
22
|
: () => null;
|
|
18
23
|
|
|
19
24
|
/**
|
|
@@ -37,6 +42,7 @@ const NativeOptimizedPdfView =
|
|
|
37
42
|
*/
|
|
38
43
|
export default function OptimizedPdfView({
|
|
39
44
|
source,
|
|
45
|
+
page,
|
|
40
46
|
password,
|
|
41
47
|
maximumZoom = DEFAULT_MAXIMUM_ZOOM,
|
|
42
48
|
enableAntialiasing = DEFAULT_ENABLE_ANTIALIASING,
|
|
@@ -52,7 +58,7 @@ export default function OptimizedPdfView({
|
|
|
52
58
|
const [loading, setLoading] = useState(true);
|
|
53
59
|
const [progress, setProgress] = useState(0);
|
|
54
60
|
const [error, setError] = useState<string | null>(null);
|
|
55
|
-
const [
|
|
61
|
+
const [internalPage, setInternalPage] = useState(0);
|
|
56
62
|
const [totalPages, setTotalPages] = useState(1);
|
|
57
63
|
const lastSource = useRef<string | null>(null);
|
|
58
64
|
|
|
@@ -64,7 +70,7 @@ export default function OptimizedPdfView({
|
|
|
64
70
|
setProgress(0);
|
|
65
71
|
setError(null);
|
|
66
72
|
setLocalPath(null);
|
|
67
|
-
|
|
73
|
+
setInternalPage(0);
|
|
68
74
|
setTotalPages(1);
|
|
69
75
|
lastSource.current = source.uri;
|
|
70
76
|
|
|
@@ -96,23 +102,23 @@ export default function OptimizedPdfView({
|
|
|
96
102
|
}, [source.uri, source.cache, source.cacheFileName, source.expiration, onError, source]);
|
|
97
103
|
|
|
98
104
|
const handleNextPage = () => {
|
|
99
|
-
if (
|
|
100
|
-
const newPage =
|
|
101
|
-
|
|
105
|
+
if (internalPage < totalPages - 1) {
|
|
106
|
+
const newPage = internalPage + 1;
|
|
107
|
+
setInternalPage(newPage);
|
|
102
108
|
onPageChange?.(newPage);
|
|
103
109
|
}
|
|
104
110
|
};
|
|
105
111
|
|
|
106
112
|
const handlePrevPage = () => {
|
|
107
|
-
if (
|
|
108
|
-
const newPage =
|
|
109
|
-
|
|
113
|
+
if (internalPage > 0) {
|
|
114
|
+
const newPage = internalPage - 1;
|
|
115
|
+
setInternalPage(newPage);
|
|
110
116
|
onPageChange?.(newPage);
|
|
111
117
|
}
|
|
112
118
|
};
|
|
113
119
|
|
|
114
120
|
const handlePageChange = (newPage: number) => {
|
|
115
|
-
|
|
121
|
+
setInternalPage(newPage);
|
|
116
122
|
onPageChange?.(newPage);
|
|
117
123
|
};
|
|
118
124
|
|
|
@@ -143,7 +149,7 @@ export default function OptimizedPdfView({
|
|
|
143
149
|
<View style={[{ flex: 1 }, style]}>
|
|
144
150
|
<NativeOptimizedPdfView
|
|
145
151
|
source={localPath}
|
|
146
|
-
page={page}
|
|
152
|
+
page={showNavigationControls ? internalPage : page || 0}
|
|
147
153
|
enableAntialiasing={enableAntialiasing}
|
|
148
154
|
maximumZoom={maximumZoom}
|
|
149
155
|
password={password || ''}
|
|
@@ -155,7 +161,7 @@ export default function OptimizedPdfView({
|
|
|
155
161
|
/>
|
|
156
162
|
{showNavigationControls && (
|
|
157
163
|
<PdfNavigationControls
|
|
158
|
-
currentPage={
|
|
164
|
+
currentPage={internalPage}
|
|
159
165
|
totalPages={totalPages}
|
|
160
166
|
onNextPage={handleNextPage}
|
|
161
167
|
onPrevPage={handlePrevPage}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
|
-
|
|
2
|
+
import { View, Text, TouchableOpacity, TextInput, StyleSheet } from 'react-native';
|
|
3
3
|
import type { PdfNavigationControlsProps } from '../types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
2
|
+
import { View, Text, ActivityIndicator, StyleSheet } from 'react-native';
|
|
3
3
|
import type { PdfLoadingOverlayProps, PdfErrorOverlayProps } from '../types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -8,7 +8,7 @@ import type { PdfLoadingOverlayProps, PdfErrorOverlayProps } from '../types';
|
|
|
8
8
|
export const PdfLoadingOverlay: React.FC<PdfLoadingOverlayProps> = ({ progress, style }) => {
|
|
9
9
|
return (
|
|
10
10
|
<View style={[styles.container, style]}>
|
|
11
|
-
<ActivityIndicator size="large" color="#
|
|
11
|
+
<ActivityIndicator size="large" color="#000000" />
|
|
12
12
|
<Text style={styles.progressText}>{progress}%</Text>
|
|
13
13
|
</View>
|
|
14
14
|
);
|
|
@@ -33,7 +33,7 @@ const styles = StyleSheet.create({
|
|
|
33
33
|
padding: 20,
|
|
34
34
|
},
|
|
35
35
|
progressText: {
|
|
36
|
-
color: '#
|
|
36
|
+
color: '#000000',
|
|
37
37
|
marginTop: 10,
|
|
38
38
|
fontSize: 16,
|
|
39
39
|
fontWeight: '600',
|
package/src/types/index.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface PdfErrorEvent {
|
|
|
41
41
|
export interface OptimizedPdfViewProps {
|
|
42
42
|
/** PDF source configuration */
|
|
43
43
|
source: PdfSource;
|
|
44
|
+
/** Current page index (0-based) */
|
|
45
|
+
page?: number;
|
|
44
46
|
/** Password for encrypted PDF files */
|
|
45
47
|
password?: string;
|
|
46
48
|
/** Maximum zoom level. Default: 3 */
|
|
@@ -110,3 +112,26 @@ export interface PdfErrorOverlayProps {
|
|
|
110
112
|
error: string;
|
|
111
113
|
style?: ViewStyle;
|
|
112
114
|
}
|
|
115
|
+
|
|
116
|
+
export interface NativeOptimizedPdfViewProps {
|
|
117
|
+
/** PDF source configuration */
|
|
118
|
+
source: string;
|
|
119
|
+
/** Current page index (0-based) */
|
|
120
|
+
page: number;
|
|
121
|
+
/** Password for encrypted PDF files */
|
|
122
|
+
password?: string;
|
|
123
|
+
/** Maximum zoom level. Default: 3 */
|
|
124
|
+
maximumZoom?: number;
|
|
125
|
+
/** Enable antialiasing for better rendering quality. Default: true */
|
|
126
|
+
enableAntialiasing?: boolean;
|
|
127
|
+
/** Callback when PDF is loaded successfully */
|
|
128
|
+
onLoadComplete?: (event: NativeLoadCompleteEvent) => void;
|
|
129
|
+
/** Callback when an error occurs */
|
|
130
|
+
onError?: (error: PdfErrorEvent) => void;
|
|
131
|
+
/** Callback when page count is available */
|
|
132
|
+
onPageCount?: (event: NativePageCountEvent) => void;
|
|
133
|
+
/** Callback when page changes */
|
|
134
|
+
onPasswordRequired?: () => void;
|
|
135
|
+
/** Custom style for the container */
|
|
136
|
+
style?: ViewStyle;
|
|
137
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
+
// "extends": "@react-native/typescript-config/tsconfig.json",
|
|
2
3
|
"compilerOptions": {
|
|
3
|
-
"target": "
|
|
4
|
-
"module": "
|
|
5
|
-
"lib": ["es6"],
|
|
6
|
-
"allowJs": true,
|
|
7
|
-
"jsx": "react-native",
|
|
8
|
-
"noEmit": true,
|
|
9
|
-
"isolatedModules": true,
|
|
10
|
-
"strict": true,
|
|
11
|
-
"moduleResolution": "node",
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "ESNext",
|
|
12
6
|
"esModuleInterop": true,
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"jsx": "react-native",
|
|
13
9
|
"skipLibCheck": true,
|
|
14
|
-
"
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noUnusedLocals": true,
|
|
12
|
+
"noUnusedParameters": true,
|
|
13
|
+
"noEmit": false,
|
|
14
|
+
"checkJs": false,
|
|
15
|
+
"allowJs": false,
|
|
16
|
+
"noImplicitAny": false,
|
|
17
|
+
"declaration": true,
|
|
18
|
+
"paths": {
|
|
19
|
+
"react-native-calendars": ["./src/index.ts"]
|
|
20
|
+
},
|
|
15
21
|
},
|
|
22
|
+
"include": ["src/**/*", "example/App.tsx"],
|
|
16
23
|
"exclude": ["node_modules", "**/__tests__/**"]
|
|
17
24
|
}
|