react-native-optimized-pdf 1.0.1 → 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.
@@ -0,0 +1,312 @@
1
+ import UIKit
2
+ import CoreGraphics
3
+ import PDFKit
4
+
5
+
6
+ import UIKit
7
+ import CoreGraphics
8
+
9
+ /// A view that renders a PDF page using CATiledLayer for efficient tiled rendering.
10
+ ///
11
+ /// CATiledLayer divides the content into tiles, allowing smooth zooming and scrolling
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
+
73
+ final class TiledPDFPageView: UIView {
74
+
75
+ // MARK: - Properties
76
+
77
+ var pdfPage: CGPDFPage? {
78
+ didSet { setNeedsDisplay() }
79
+ }
80
+
81
+ var pdfURL: URL?
82
+ var pageIndex: Int = 0
83
+
84
+ var enableAntialiasing: Bool = true {
85
+ didSet { setNeedsDisplay() }
86
+ }
87
+
88
+ var configuration: PDFConfiguration = .default {
89
+ didSet { applyConfiguration() }
90
+ }
91
+
92
+ override class var layerClass: AnyClass { CATiledLayer.self }
93
+
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()
100
+
101
+ // MARK: - Initialization
102
+
103
+ override init(frame: CGRect) {
104
+ super.init(frame: frame)
105
+ commonInit()
106
+ }
107
+
108
+ required init?(coder: NSCoder) {
109
+ super.init(coder: coder)
110
+ commonInit()
111
+ }
112
+
113
+ convenience init(frame: CGRect, configuration: PDFConfiguration) {
114
+ self.init(frame: frame)
115
+ self.configuration = configuration
116
+ applyConfiguration()
117
+ }
118
+
119
+ private func commonInit() {
120
+ applyConfiguration()
121
+ contentScaleFactor = UIScreen.main.scale
122
+ backgroundColor = .white
123
+ }
124
+
125
+ // MARK: - Configuration
126
+
127
+ private func applyConfiguration() {
128
+ tiledLayer.tileSize = configuration.tileSize
129
+ tiledLayer.levelsOfDetail = configuration.levelsOfDetail
130
+ tiledLayer.levelsOfDetailBias = configuration.levelsOfDetailBias
131
+ enableAntialiasing = configuration.enableAntialiasing
132
+ }
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
+
242
+ // MARK: - Rendering
243
+
244
+ override func draw(_ rect: CGRect) {
245
+ guard !_cancelled.isSet() else { return }
246
+
247
+ guard let context = UIGraphicsGetCurrentContext(),
248
+ let page = _activePage else { return }
249
+
250
+ renderPage(page, in: context, rect: rect)
251
+ }
252
+
253
+ private func renderPage(_ page: CGPDFPage, in context: CGContext, rect: CGRect) {
254
+ context.saveGState()
255
+ defer { context.restoreGState() }
256
+
257
+ context.setShouldAntialias(enableAntialiasing)
258
+ context.interpolationQuality = determineInterpolationQuality()
259
+
260
+ context.setFillColor(UIColor.white.cgColor)
261
+ context.fill(rect)
262
+
263
+ context.translateBy(x: 0, y: bounds.height)
264
+ context.scaleBy(x: 1.0, y: -1.0)
265
+
266
+ let pdfTransform = page.getDrawingTransform(
267
+ .cropBox,
268
+ rect: bounds,
269
+ rotate: 0,
270
+ preserveAspectRatio: true
271
+ )
272
+ context.concatenate(pdfTransform)
273
+ // Draw the page
274
+ context.drawPDFPage(page)
275
+ }
276
+
277
+ private func determineInterpolationQuality() -> CGInterpolationQuality {
278
+ tiledLayer.levelsOfDetail == 1 ? .low : .high
279
+ }
280
+ }
281
+
282
+ // MARK: - Public Methods
283
+
284
+ extension TiledPDFPageView {
285
+
286
+ func prepareForReuse() {
287
+ pdfPage = nil
288
+ _activePage = nil
289
+ layer.contents = nil
290
+ }
291
+
292
+ /// Updates the view size to match the page dimensions.
293
+ func updateFrame(for page: CGPDFPage) {
294
+ let pageRect = page.getBoxRect(.cropBox)
295
+ frame = CGRect(origin: .zero, size: pageRect.size)
296
+ pdfPage = page
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
+ }
312
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-native-optimized-pdf",
3
- "version": "1.0.1",
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.",
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
- "catiled-layer",
29
- "pdfrenderer",
30
- "no-crash",
31
- "optimized"
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.0.0",
43
- "@types/react-native": "^0.72.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": {
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
- const { View, Platform, requireNativeComponent } = require('react-native');
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 { OptimizedPdfViewProps, NativeLoadCompleteEvent, NativePageCountEvent } from './types';
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 [page, setPage] = useState(0);
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
- setPage(0);
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 (page < totalPages - 1) {
100
- const newPage = page + 1;
101
- setPage(newPage);
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 (page > 0) {
108
- const newPage = page - 1;
109
- setPage(newPage);
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
- setPage(newPage);
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={page}
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
- const { View, Text, TouchableOpacity, TextInput, StyleSheet } = require('react-native');
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
- const { View, Text, ActivityIndicator, StyleSheet } = require('react-native');
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="#fff" />
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: '#fff',
36
+ color: '#000000',
37
37
  marginTop: 10,
38
38
  fontSize: 16,
39
39
  fontWeight: '600',
@@ -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": "es2017",
4
- "module": "commonjs",
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
- "forceConsistentCasingInFileNames": true
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
  }