react-native-pdf-jsi 3.2.0 → 3.3.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.
@@ -475,3 +475,4 @@ public class FileDownloader extends ReactContextBaseJavaModule {
475
475
  }).start();
476
476
  }
477
477
  }
478
+
@@ -214,3 +214,4 @@ public class FileManager extends ReactContextBaseJavaModule {
214
214
  }
215
215
  }
216
216
 
217
+
@@ -153,7 +153,7 @@ public class PdfManager extends SimpleViewManager<PdfView> implements RNPDFPdfVi
153
153
  public void setSinglePage(PdfView pdfView, boolean singlePage) {
154
154
  pdfView.setSinglePage(singlePage);
155
155
  }
156
-
156
+
157
157
  // It seems funny, but this method is called through delegate on Paper, but on Fabric we need to
158
158
  // use `receiveCommand` method and call this one there
159
159
  @Override
@@ -174,7 +174,8 @@ public class PdfManager extends SimpleViewManager<PdfView> implements RNPDFPdfVi
174
174
  @Override
175
175
  public void onAfterUpdateTransaction(PdfView pdfView) {
176
176
  super.onAfterUpdateTransaction(pdfView);
177
- pdfView.drawPdf();
177
+ // Removed pdfView.drawPdf() - PdfView now manages reload internally
178
+ // to prevent unnecessary document recreation on scroll/prop updates
178
179
  }
179
180
 
180
181
  }
@@ -75,9 +75,15 @@ public class PdfView extends PDFView implements OnPageChangeListener,OnLoadCompl
75
75
  private FitPolicy fitPolicy = FitPolicy.WIDTH;
76
76
  private boolean singlePage = false;
77
77
  private boolean scrollEnabled = true;
78
+
79
+ private String decelerationRate = "normal"; // "normal", "fast", "slow"
78
80
 
79
81
  private float originalWidth = 0;
80
82
  private float lastPageWidth = 0;
83
+
84
+ // Track if document needs reload (FIX: Prevent recreation on prop changes)
85
+ private boolean needsReload = true;
86
+ private String lastLoadedPath = null;
81
87
  private float lastPageHeight = 0;
82
88
 
83
89
  // used to store the parameters for `super.onSizeChanged`
@@ -279,6 +285,17 @@ public class PdfView extends PDFView implements OnPageChangeListener,OnLoadCompl
279
285
  }
280
286
 
281
287
  public void drawPdf() {
288
+
289
+ // FIX: Check if we actually need to reload the document
290
+ // Only reload if path changed or this is first load
291
+ if (!needsReload && this.path != null && this.path.equals(lastLoadedPath)) {
292
+ showLog(format("drawPdf: Skipping reload, path unchanged: %s", this.path));
293
+ // Just jump to the page if needed, dont reload entire document
294
+ if (this.page > 0 && !this.isRecycled()) {
295
+ this.jumpTo(this.page - 1, false);
296
+ }
297
+ return;
298
+ }
282
299
  showLog(format("drawPdf path:%s %s", this.path, this.page));
283
300
 
284
301
  if (this.path != null){
@@ -333,6 +350,10 @@ public class PdfView extends PDFView implements OnPageChangeListener,OnLoadCompl
333
350
  }
334
351
 
335
352
  configurator.load();
353
+
354
+ // Mark as loaded, clear reload flag
355
+ lastLoadedPath = this.path;
356
+ needsReload = false;
336
357
  }
337
358
  }
338
359
 
@@ -341,6 +362,8 @@ public class PdfView extends PDFView implements OnPageChangeListener,OnLoadCompl
341
362
  }
342
363
 
343
364
  public void setPath(String path) {
365
+ // Path changed - need to reload document
366
+ needsReload = true;
344
367
  this.path = path;
345
368
  }
346
369
 
@@ -419,6 +442,12 @@ public class PdfView extends PDFView implements OnPageChangeListener,OnLoadCompl
419
442
  public void setSinglePage(boolean singlePage) {
420
443
  this.singlePage = singlePage;
421
444
  }
445
+
446
+ /**
447
+
448
+ /**
449
+
450
+ /**
422
451
 
423
452
  /**
424
453
  * @see https://github.com/barteksc/AndroidPdfViewer/blob/master/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/link/DefaultLinkHandler.java
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Streaming Base64 Decoder for Large PDFs
3
+ * Eliminates OOM crashes by processing base64 data in chunks
4
+ *
5
+ * Features:
6
+ * - O(1) constant memory usage (8KB chunks)
7
+ * - Progress callback support
8
+ * - PDF header validation
9
+ * - Error handling and recovery
10
+ *
11
+ * Usage:
12
+ * StreamingBase64Decoder.decodeToFile(base64String, outputFile);
13
+ * StreamingBase64Decoder.decodeToFileWithProgress(base64String, outputFile, progress -> {
14
+ * Log.i("Decode", "Progress: " + (progress * 100) + "%");
15
+ * });
16
+ */
17
+
18
+ package org.wonday.pdf;
19
+
20
+ import android.util.Base64;
21
+ import android.util.Log;
22
+ import java.io.*;
23
+ import java.nio.charset.StandardCharsets;
24
+
25
+ public class StreamingBase64Decoder {
26
+ private static final String TAG = "StreamingBase64Decoder";
27
+
28
+ // Chunk size for processing (8KB = optimal for memory/performance balance)
29
+ private static final int CHUNK_SIZE = 8192;
30
+
31
+ // PDF file signature (magic bytes)
32
+ private static final byte[] PDF_HEADER = {0x25, 0x50, 0x44, 0x46}; // "%PDF"
33
+
34
+ /**
35
+ * Progress callback interface
36
+ */
37
+ public interface ProgressCallback {
38
+ void onProgress(float progress);
39
+ }
40
+
41
+ /**
42
+ * Decode base64 string to file using streaming (O(1) memory)
43
+ *
44
+ * @param base64Data Base64 encoded PDF data
45
+ * @param outputFile Output file to write decoded data
46
+ * @throws IOException If I/O error occurs
47
+ * @throws IllegalArgumentException If invalid base64 or not a PDF
48
+ */
49
+ public static void decodeToFile(String base64Data, File outputFile) throws IOException {
50
+ decodeToFileWithProgress(base64Data, outputFile, null);
51
+ }
52
+
53
+ /**
54
+ * Decode base64 string to file with progress callback
55
+ *
56
+ * @param base64Data Base64 encoded PDF data
57
+ * @param outputFile Output file to write decoded data
58
+ * @param callback Progress callback (0.0 to 1.0)
59
+ * @throws IOException If I/O error occurs
60
+ * @throws IllegalArgumentException If invalid base64 or not a PDF
61
+ */
62
+ public static void decodeToFileWithProgress(
63
+ String base64Data,
64
+ File outputFile,
65
+ ProgressCallback callback) throws IOException {
66
+
67
+ long startTime = System.currentTimeMillis();
68
+ Log.i(TAG, "[PERF] [decodeToFileWithProgress] 🔵 ENTER - Input size: " + base64Data.length() + " chars");
69
+
70
+ if (base64Data == null || base64Data.trim().isEmpty()) {
71
+ throw new IllegalArgumentException("Base64 data is null or empty");
72
+ }
73
+
74
+ // Clean base64 data - remove data URI prefix if present
75
+ long cleanStart = System.currentTimeMillis();
76
+ String cleanBase64 = cleanBase64Data(base64Data);
77
+ long cleanTime = System.currentTimeMillis() - cleanStart;
78
+ Log.i(TAG, "[PERF] [decodeToFileWithProgress] Clean data: " + cleanTime + "ms");
79
+
80
+ int totalLength = cleanBase64.length();
81
+ int offset = 0;
82
+ boolean isFirstChunk = true;
83
+
84
+ try (FileOutputStream fos = new FileOutputStream(outputFile);
85
+ BufferedOutputStream bos = new BufferedOutputStream(fos, 16384)) {
86
+
87
+ long decodeStart = System.currentTimeMillis();
88
+ int chunksProcessed = 0;
89
+
90
+ // Process base64 data in chunks
91
+ while (offset < totalLength) {
92
+ // Calculate chunk boundaries (must be multiple of 4 for base64)
93
+ int chunkEnd = Math.min(offset + CHUNK_SIZE, totalLength);
94
+
95
+ // Adjust chunk end to be on a base64 boundary (multiple of 4)
96
+ // Exception: last chunk can be any size
97
+ if (chunkEnd < totalLength) {
98
+ int remainder = (chunkEnd - offset) % 4;
99
+ if (remainder != 0) {
100
+ chunkEnd -= remainder;
101
+ }
102
+ }
103
+
104
+ // Extract and decode chunk
105
+ String chunk = cleanBase64.substring(offset, chunkEnd);
106
+ byte[] decodedChunk;
107
+
108
+ try {
109
+ decodedChunk = Base64.decode(chunk, Base64.DEFAULT);
110
+ } catch (IllegalArgumentException e) {
111
+ Log.e(TAG, "Invalid base64 data at offset " + offset, e);
112
+ throw new IllegalArgumentException("Invalid base64 encoding at position " + offset, e);
113
+ }
114
+
115
+ // Validate PDF header on first chunk
116
+ if (isFirstChunk) {
117
+ if (!validatePDFHeader(decodedChunk)) {
118
+ throw new IllegalArgumentException("Invalid PDF data - missing or corrupt PDF header");
119
+ }
120
+ isFirstChunk = false;
121
+ Log.i(TAG, "[PERF] [decodeToFileWithProgress] PDF header validated ✓");
122
+ }
123
+
124
+ // Write decoded chunk to file
125
+ bos.write(decodedChunk);
126
+
127
+ offset = chunkEnd;
128
+ chunksProcessed++;
129
+
130
+ // Report progress
131
+ if (callback != null && chunksProcessed % 10 == 0) {
132
+ float progress = (float) offset / totalLength;
133
+ callback.onProgress(progress);
134
+ }
135
+ }
136
+
137
+ bos.flush();
138
+ fos.getFD().sync(); // Force sync to disk for durability
139
+
140
+ long decodeTime = System.currentTimeMillis() - decodeStart;
141
+ long totalTime = System.currentTimeMillis() - startTime;
142
+
143
+ Log.i(TAG, "[PERF] [decodeToFileWithProgress] Decode time: " + decodeTime + "ms");
144
+ Log.i(TAG, "[PERF] [decodeToFileWithProgress] Chunks processed: " + chunksProcessed);
145
+ Log.i(TAG, "[PERF] [decodeToFileWithProgress] Output size: " + outputFile.length() + " bytes");
146
+ Log.i(TAG, "[PERF] [decodeToFileWithProgress] 🔴 EXIT - Total: " + totalTime + "ms");
147
+
148
+ // Final progress callback
149
+ if (callback != null) {
150
+ callback.onProgress(1.0f);
151
+ }
152
+
153
+ } catch (IOException e) {
154
+ // Clean up partial file on error
155
+ if (outputFile.exists()) {
156
+ outputFile.delete();
157
+ }
158
+ Log.e(TAG, "Failed to decode base64 to file", e);
159
+ throw e;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Clean base64 data - remove whitespace, newlines, and data URI prefix
165
+ */
166
+ private static String cleanBase64Data(String base64Data) {
167
+ String cleaned = base64Data.trim();
168
+
169
+ // Remove data URI prefix if present (e.g., "data:application/pdf;base64,")
170
+ int commaIndex = cleaned.indexOf(',');
171
+ if (commaIndex != -1 && cleaned.substring(0, commaIndex).contains("base64")) {
172
+ cleaned = cleaned.substring(commaIndex + 1);
173
+ }
174
+
175
+ // Remove all whitespace and newlines (some base64 strings have line breaks)
176
+ cleaned = cleaned.replaceAll("\\s+", "");
177
+
178
+ return cleaned;
179
+ }
180
+
181
+ /**
182
+ * Validate PDF header (magic bytes: %PDF)
183
+ */
184
+ private static boolean validatePDFHeader(byte[] data) {
185
+ if (data == null || data.length < PDF_HEADER.length) {
186
+ return false;
187
+ }
188
+
189
+ // Check for PDF magic bytes at start
190
+ for (int i = 0; i < PDF_HEADER.length; i++) {
191
+ if (data[i] != PDF_HEADER[i]) {
192
+ return false;
193
+ }
194
+ }
195
+
196
+ return true;
197
+ }
198
+
199
+ /**
200
+ * Validate PDF file header
201
+ */
202
+ public static boolean validatePDFFile(File file) throws IOException {
203
+ if (file == null || !file.exists() || file.length() < PDF_HEADER.length) {
204
+ return false;
205
+ }
206
+
207
+ try (FileInputStream fis = new FileInputStream(file)) {
208
+ byte[] header = new byte[PDF_HEADER.length];
209
+ int bytesRead = fis.read(header);
210
+
211
+ if (bytesRead < PDF_HEADER.length) {
212
+ return false;
213
+ }
214
+
215
+ return validatePDFHeader(header);
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Estimate decoded size from base64 length
221
+ * Base64 encoding increases size by ~33%, so decoded size is ~75% of encoded
222
+ */
223
+ public static long estimateDecodedSize(int base64Length) {
224
+ return (long) (base64Length * 0.75);
225
+ }
226
+
227
+ /**
228
+ * Calculate number of chunks for given base64 data
229
+ */
230
+ public static int calculateChunkCount(int base64Length) {
231
+ return (int) Math.ceil((double) base64Length / CHUNK_SIZE);
232
+ }
233
+ }
234
+
235
+
236
+
package/index.d.ts CHANGED
@@ -39,6 +39,9 @@ export interface PdfProps {
39
39
  showsHorizontalScrollIndicator?: boolean,
40
40
  showsVerticalScrollIndicator?: boolean,
41
41
  scrollEnabled?: boolean,
42
+ /**
43
+ /**
44
+ /**
42
45
  spacing?: number,
43
46
  password?: string,
44
47
  renderActivityIndicator?: (progress: number) => React.ReactElement,
@@ -70,3 +73,127 @@ declare class Pdf extends React.Component<PdfProps, any> {
70
73
  }
71
74
 
72
75
  export default Pdf;
76
+
77
+ // ========================================
78
+ // PDFCache (Streaming Base64 Decoder)
79
+ // ========================================
80
+
81
+ /**
82
+ * Cache info returned from storage operations
83
+ */
84
+ export interface CacheInfo {
85
+ cacheId: string;
86
+ filePath: string;
87
+ fileSize: number;
88
+ createdAt: number;
89
+ lastAccessed?: number;
90
+ expired?: boolean;
91
+ }
92
+
93
+ /**
94
+ * Options for storing base64 PDF with streaming decoder
95
+ */
96
+ export interface StoreBase64Options {
97
+ /**
98
+ * Base64 PDF data (with or without data URI prefix)
99
+ */
100
+ base64: string;
101
+ /**
102
+ * Custom cache identifier (optional, auto-generated if not provided)
103
+ */
104
+ identifier?: string;
105
+ /**
106
+ * Cache TTL in milliseconds
107
+ * @default 2592000000 (30 days)
108
+ */
109
+ maxAge?: number;
110
+ /**
111
+ * Maximum cache size in bytes
112
+ * @default 524288000 (500MB)
113
+ */
114
+ maxSize?: number;
115
+ /**
116
+ * Progress callback (0.0 to 1.0)
117
+ */
118
+ onProgress?: (progress: number) => void;
119
+ }
120
+
121
+ /**
122
+ * Cache statistics
123
+ */
124
+ export interface CacheStats {
125
+ totalSize: number;
126
+ fileCount: number;
127
+ hitRate: number;
128
+ cacheHits: number;
129
+ cacheMisses: number;
130
+ averageLoadTime: number;
131
+ }
132
+
133
+ /**
134
+ * PDFCache Manager for streaming base64 decoding
135
+ * Eliminates OOM crashes with large PDFs (60MB-200MB+)
136
+ */
137
+ export interface PDFCacheManager {
138
+ /**
139
+ * Store base64 PDF with streaming decoder (O(1) constant memory)
140
+ * @param options Storage options
141
+ * @returns Promise resolving to cache info
142
+ */
143
+ storeBase64(options: StoreBase64Options): Promise<CacheInfo>;
144
+
145
+ /**
146
+ * Get cached PDF by identifier
147
+ * @param identifier Cache identifier
148
+ * @returns Promise resolving to cache info or null if not found
149
+ */
150
+ get(identifier: string): Promise<CacheInfo | null>;
151
+
152
+ /**
153
+ * Check if PDF is cached and not expired
154
+ * @param identifier Cache identifier
155
+ * @returns Promise resolving to true if cached and valid
156
+ */
157
+ has(identifier: string): Promise<boolean>;
158
+
159
+ /**
160
+ * Remove cached PDF
161
+ * @param identifier Cache identifier
162
+ * @returns Promise resolving to true if removed successfully
163
+ */
164
+ remove(identifier: string): Promise<boolean>;
165
+
166
+ /**
167
+ * Clear all cached PDFs
168
+ */
169
+ clear(): Promise<void>;
170
+
171
+ /**
172
+ * Clear expired PDFs only
173
+ * @returns Promise resolving to number of entries removed
174
+ */
175
+ clearExpired(): Promise<number>;
176
+
177
+ /**
178
+ * Get cache statistics
179
+ * @returns Promise resolving to cache stats
180
+ */
181
+ getStats(): Promise<CacheStats>;
182
+
183
+ /**
184
+ * Estimate decoded size from base64 length
185
+ * @param base64Length Length of base64 string
186
+ * @returns Estimated decoded size in bytes
187
+ */
188
+ estimateDecodedSize(base64Length: number): number;
189
+ }
190
+
191
+ /**
192
+ * PDFCache singleton instance
193
+ */
194
+ export const PDFCache: PDFCacheManager;
195
+
196
+ /**
197
+ * CacheManager (alias for PDFCache)
198
+ */
199
+ export const CacheManager: PDFCacheManager;
package/index.js CHANGED
@@ -610,12 +610,17 @@ import ExportManager from './src/managers/ExportManager';
610
610
  import BookmarkManager from './src/managers/BookmarkManager';
611
611
  import AnalyticsManager from './src/managers/AnalyticsManager';
612
612
  import FileManager from './src/managers/FileManager';
613
+ import CacheManager from './src/managers/CacheManager';
614
+
615
+ // Alias for backward compatibility and intuitive naming
616
+ export const PDFCache = CacheManager;
613
617
 
614
618
  export {
615
619
  ExportManager,
616
620
  BookmarkManager,
617
621
  AnalyticsManager,
618
- FileManager
622
+ FileManager,
623
+ CacheManager
619
624
  };
620
625
 
621
626
  // ========================================
@@ -2,16 +2,14 @@
2
2
  #import <PDFKit/PDFKit.h>
3
3
  #import <UIKit/UIKit.h>
4
4
 
5
- @implementation PDFExporter {
6
- LicenseVerifier *_licenseVerifier;
7
- }
5
+ @implementation PDFExporter
8
6
 
9
7
  RCT_EXPORT_MODULE();
10
8
 
11
9
  - (instancetype)init {
12
10
  self = [super init];
13
11
  if (self) {
14
- _licenseVerifier = [[LicenseVerifier alloc] init];
12
+ // All features are FREE - no license verification needed
15
13
  }
16
14
  return self;
17
15
  }
@@ -22,11 +20,7 @@ RCT_EXPORT_METHOD(exportToImages:(NSString *)filePath
22
20
  resolver:(RCTPromiseResolveBlock)resolve
23
21
  rejecter:(RCTPromiseRejectBlock)reject) {
24
22
 
25
-
26
- if (![_licenseVerifier isProActive]) {
27
- reject(@"LICENSE_REQUIRED", @"Export to Images requires a Pro license", nil);
28
- return;
29
- }
23
+ // All features are FREE - no license verification needed
30
24
 
31
25
  if (!filePath || filePath.length == 0) {
32
26
  reject(@"INVALID_PATH", @"File path is required", nil);
@@ -200,11 +194,7 @@ RCT_EXPORT_METHOD(mergePDFs:(NSArray *)filePaths
200
194
  resolver:(RCTPromiseResolveBlock)resolve
201
195
  rejecter:(RCTPromiseRejectBlock)reject) {
202
196
 
203
- // Check Pro license
204
- if (![_licenseVerifier isProActive]) {
205
- reject(@"LICENSE_REQUIRED", @"PDF Operations requires a Pro license", nil);
206
- return;
207
- }
197
+ // All features are FREE - no license verification needed
208
198
 
209
199
  if (!filePaths || filePaths.count < 2) {
210
200
  reject(@"INVALID_INPUT", @"At least 2 PDF files are required for merging", nil);
@@ -251,15 +241,7 @@ RCT_EXPORT_METHOD(splitPDF:(NSString *)filePath
251
241
 
252
242
  NSLog(@"✂️ [SPLIT] splitPDF - START - file: %@, ranges: %lu", filePath, (unsigned long)pageRanges.count);
253
243
 
254
- // Check Pro license
255
- BOOL licenseActive = [_licenseVerifier isProActive];
256
- NSLog(@"🔑 [LICENSE] isProActive: %d", licenseActive);
257
-
258
- if (!licenseActive) {
259
- NSLog(@"❌ [SPLIT] License required");
260
- reject(@"LICENSE_REQUIRED", @"PDF Operations requires a Pro license", nil);
261
- return;
262
- }
244
+ // All features are FREE - no license verification needed
263
245
 
264
246
  if (!filePath || filePath.length == 0) {
265
247
  NSLog(@"❌ [SPLIT] Invalid path");
@@ -341,15 +323,7 @@ RCT_EXPORT_METHOD(extractPages:(NSString *)filePath
341
323
 
342
324
  NSLog(@"✂️ [EXTRACT] extractPages - START - file: %@, pages: %lu", filePath, (unsigned long)pageNumbers.count);
343
325
 
344
- // Check Pro license
345
- BOOL licenseActive = [_licenseVerifier isProActive];
346
- NSLog(@"🔑 [LICENSE] isProActive: %d", licenseActive);
347
-
348
- if (!licenseActive) {
349
- NSLog(@"❌ [EXTRACT] License required");
350
- reject(@"LICENSE_REQUIRED", @"PDF Operations requires a Pro license", nil);
351
- return;
352
- }
326
+ // All features are FREE - no license verification needed
353
327
 
354
328
  if (!filePath || filePath.length == 0) {
355
329
  NSLog(@"❌ [EXTRACT] Invalid path");
@@ -420,11 +394,7 @@ RCT_EXPORT_METHOD(rotatePage:(NSString *)filePath
420
394
  resolver:(RCTPromiseResolveBlock)resolve
421
395
  rejecter:(RCTPromiseRejectBlock)reject) {
422
396
 
423
- // Check Pro license
424
- if (![_licenseVerifier isProActive]) {
425
- reject(@"LICENSE_REQUIRED", @"PDF Operations requires a Pro license", nil);
426
- return;
427
- }
397
+ // All features are FREE - no license verification needed
428
398
 
429
399
  if (!filePath || filePath.length == 0) {
430
400
  reject(@"INVALID_PATH", @"File path is required", nil);
@@ -473,11 +443,7 @@ RCT_EXPORT_METHOD(deletePage:(NSString *)filePath
473
443
  resolver:(RCTPromiseResolveBlock)resolve
474
444
  rejecter:(RCTPromiseRejectBlock)reject) {
475
445
 
476
- // Check Pro license
477
- if (![_licenseVerifier isProActive]) {
478
- reject(@"LICENSE_REQUIRED", @"PDF Operations requires a Pro license", nil);
479
- return;
480
- }
446
+ // All features are FREE - no license verification needed
481
447
 
482
448
  if (!filePath || filePath.length == 0) {
483
449
  reject(@"INVALID_PATH", @"File path is required", nil);
@@ -42,6 +42,9 @@ UIView
42
42
  @property(nonatomic) BOOL showsHorizontalScrollIndicator;
43
43
  @property(nonatomic) BOOL scrollEnabled;
44
44
  @property(nonatomic) BOOL enablePaging;
45
+ @property(nonatomic) float scrollVelocity;
46
+ @property(nonatomic) BOOL enableMomentum;
47
+ @property(nonatomic, strong) NSString *decelerationRate;
45
48
  @property(nonatomic) BOOL enableRTL;
46
49
  @property(nonatomic) BOOL enableAnnotationRendering;
47
50
  @property(nonatomic) BOOL enableDoubleTapZoom;
@@ -195,6 +195,20 @@ using namespace facebook::react;
195
195
  _scrollEnabled = newProps.scrollEnabled;
196
196
  [updatedPropNames addObject:@"scrollEnabled"];
197
197
  }
198
+
199
+ // Scroll control properties (Issue #607)
200
+ if (_scrollVelocity != newProps.scrollVelocity) {
201
+ _scrollVelocity = newProps.scrollVelocity;
202
+ [updatedPropNames addObject:@"scrollVelocity"];
203
+ }
204
+ if (_enableMomentum != newProps.enableMomentum) {
205
+ _enableMomentum = newProps.enableMomentum;
206
+ [updatedPropNames addObject:@"enableMomentum"];
207
+ }
208
+ if (_decelerationRate != RCTNSStringFromStringNilIfEmpty(newProps.decelerationRate)) {
209
+ _decelerationRate = RCTNSStringFromStringNilIfEmpty(newProps.decelerationRate);
210
+ [updatedPropNames addObject:@"decelerationRate"];
211
+ }
198
212
 
199
213
  [super updateProps:props oldProps:oldProps];
200
214
  [self didSetProps:updatedPropNames];
@@ -284,6 +298,11 @@ using namespace facebook::react;
284
298
  _showsHorizontalScrollIndicator = YES;
285
299
  _showsVerticalScrollIndicator = YES;
286
300
  _scrollEnabled = YES;
301
+
302
+ // Scroll control properties (Issue #607)
303
+ _scrollVelocity = 1.0f;
304
+ _enableMomentum = YES;
305
+ _decelerationRate = @"normal";
287
306
 
288
307
  // Enhanced properties
289
308
  _enableCaching = YES;
@@ -540,6 +559,33 @@ using namespace facebook::react;
540
559
  }
541
560
  }
542
561
  }
562
+
563
+ // Apply scroll control properties (Issue #607)
564
+ if (_pdfDocument && ([changedProps containsObject:@"path"] ||
565
+ [changedProps containsObject:@"enableMomentum"] ||
566
+ [changedProps containsObject:@"decelerationRate"])) {
567
+ for (UIView *subview in _pdfView.subviews) {
568
+ if ([subview isKindOfClass:[UIScrollView class]]) {
569
+ UIScrollView *scrollView = (UIScrollView *)subview;
570
+
571
+ // Apply momentum (bounces)
572
+ scrollView.bounces = _enableMomentum;
573
+ scrollView.alwaysBounceVertical = _enableMomentum;
574
+ scrollView.alwaysBounceHorizontal = _enableMomentum;
575
+
576
+ // Apply deceleration rate
577
+ if ([_decelerationRate isEqualToString:@"fast"]) {
578
+ scrollView.decelerationRate = UIScrollViewDecelerationRateFast;
579
+ } else if ([_decelerationRate isEqualToString:@"slow"]) {
580
+ scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
581
+ } else {
582
+ scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
583
+ }
584
+
585
+ NSLog(@"[RNPDFPdf] Applied scroll controls: momentum=%d, deceleration=%@", _enableMomentum, _decelerationRate);
586
+ }
587
+ }
588
+ }
543
589
 
544
590
  if (_pdfDocument && ([changedProps containsObject:@"path"] || [changedProps containsObject:@"enablePaging"] || [changedProps containsObject:@"horizontal"] || [changedProps containsObject:@"page"])) {
545
591
 
@@ -43,6 +43,9 @@ RCT_EXPORT_VIEW_PROPERTY(showsHorizontalScrollIndicator, BOOL);
43
43
  RCT_EXPORT_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL);
44
44
  RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL);
45
45
  RCT_EXPORT_VIEW_PROPERTY(enablePaging, BOOL);
46
+ RCT_EXPORT_VIEW_PROPERTY(scrollVelocity, float);
47
+ RCT_EXPORT_VIEW_PROPERTY(enableMomentum, BOOL);
48
+ RCT_EXPORT_VIEW_PROPERTY(decelerationRate, NSString);
46
49
  RCT_EXPORT_VIEW_PROPERTY(enableRTL, BOOL);
47
50
  RCT_EXPORT_VIEW_PROPERTY(enableAnnotationRendering, BOOL);
48
51
  RCT_EXPORT_VIEW_PROPERTY(enableDoubleTapZoom, BOOL);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-pdf-jsi",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "summary": "High-performance React Native PDF viewer with JSI acceleration - up to 80x faster than traditional bridge",
5
5
  "description": "🚀 Ultra-fast React Native PDF viewer with JSI (JavaScript Interface) integration for maximum performance. Features lazy loading, smart caching, progressive loading, and zero-bridge overhead operations. Perfect for large PDF files with 30-day persistent cache and advanced memory optimization. Google Play 16KB page size compliant for Android 15+. Supports iOS, Android, and Windows platforms.",
6
6
  "main": "index.js",
@@ -0,0 +1,271 @@
1
+ /**
2
+ * PDFCache Manager
3
+ * Handles base64 PDF caching with streaming decoder
4
+ * Eliminates OOM crashes for large PDFs (60MB-200MB+)
5
+ *
6
+ * Features:
7
+ * - Streaming base64 decoder (O(1) memory)
8
+ * - Progress callbacks
9
+ * - Native persistent cache
10
+ * - 30-day TTL
11
+ * - LRU eviction
12
+ */
13
+
14
+ import { NativeModules, NativeEventEmitter, Platform } from 'react-native';
15
+
16
+ const { PDFJSIManager } = NativeModules;
17
+
18
+ // Event emitter for progress updates
19
+ const eventEmitter = PDFJSIManager ? new NativeEventEmitter(PDFJSIManager) : null;
20
+
21
+ class CacheManager {
22
+ constructor() {
23
+ this.progressListeners = new Map();
24
+ }
25
+
26
+ /**
27
+ * Store base64 PDF with streaming decoder
28
+ * Eliminates OOM crashes - uses O(1) constant memory
29
+ *
30
+ * @param {Object} options - Storage options
31
+ * @param {string} options.base64 - Base64 PDF data (with or without data URI prefix)
32
+ * @param {string} [options.identifier] - Custom cache identifier (optional)
33
+ * @param {number} [options.maxAge] - Cache TTL in milliseconds (default: 30 days)
34
+ * @param {number} [options.maxSize] - Max cache size in bytes (default: 500MB)
35
+ * @param {function} [options.onProgress] - Progress callback (0.0 to 1.0)
36
+ * @returns {Promise<Object>} Cache info { cacheId, filePath, fileSize }
37
+ */
38
+ async storeBase64(options) {
39
+ if (!options || !options.base64) {
40
+ throw new Error('base64 data is required');
41
+ }
42
+
43
+ const {
44
+ base64,
45
+ identifier,
46
+ maxAge = 30 * 24 * 60 * 60 * 1000, // 30 days default
47
+ maxSize = 500 * 1024 * 1024, // 500MB default
48
+ onProgress
49
+ } = options;
50
+
51
+ // Register progress listener if provided
52
+ let progressSubscription;
53
+ if (onProgress && eventEmitter) {
54
+ progressSubscription = eventEmitter.addListener(
55
+ 'PDFCacheProgress',
56
+ (event) => {
57
+ if (event.identifier === identifier) {
58
+ onProgress(event.progress);
59
+ }
60
+ }
61
+ );
62
+ }
63
+
64
+ try {
65
+ // Platform-specific implementation
66
+ if (Platform.OS === 'android') {
67
+ // Android: Use native PDFNativeCacheManager with streaming decoder
68
+ const result = await PDFJSIManager.storePDFBase64({
69
+ base64: base64,
70
+ identifier: identifier || this._generateIdentifier(base64),
71
+ maxAge: maxAge,
72
+ maxSize: maxSize,
73
+ withProgress: !!onProgress
74
+ });
75
+
76
+ return {
77
+ cacheId: result.cacheId,
78
+ filePath: result.filePath,
79
+ fileSize: result.fileSize,
80
+ createdAt: result.createdAt || Date.now()
81
+ };
82
+ } else if (Platform.OS === 'ios') {
83
+ // iOS: Similar implementation (to be added)
84
+ const result = await PDFJSIManager.storePDFBase64({
85
+ base64: base64,
86
+ identifier: identifier || this._generateIdentifier(base64),
87
+ maxAge: maxAge,
88
+ maxSize: maxSize
89
+ });
90
+
91
+ return {
92
+ cacheId: result.cacheId,
93
+ filePath: result.filePath,
94
+ fileSize: result.fileSize,
95
+ createdAt: result.createdAt || Date.now()
96
+ };
97
+ } else {
98
+ throw new Error(`Platform ${Platform.OS} not supported`);
99
+ }
100
+ } catch (error) {
101
+ console.error('[CacheManager] Failed to store base64 PDF:', error);
102
+ throw error;
103
+ } finally {
104
+ // Clean up progress listener
105
+ if (progressSubscription) {
106
+ progressSubscription.remove();
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Get cached PDF by identifier
113
+ *
114
+ * @param {string} identifier - Cache identifier
115
+ * @returns {Promise<Object|null>} Cache info or null if not found/expired
116
+ */
117
+ async get(identifier) {
118
+ if (!identifier) {
119
+ throw new Error('identifier is required');
120
+ }
121
+
122
+ try {
123
+ const result = await PDFJSIManager.getCachedPDF(identifier);
124
+
125
+ if (!result || result.expired) {
126
+ return null;
127
+ }
128
+
129
+ return {
130
+ cacheId: result.cacheId,
131
+ filePath: result.filePath,
132
+ fileSize: result.fileSize,
133
+ createdAt: result.createdAt,
134
+ lastAccessed: result.lastAccessed,
135
+ expired: result.expired
136
+ };
137
+ } catch (error) {
138
+ console.warn('[CacheManager] Failed to get cached PDF:', error);
139
+ return null;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Check if PDF is cached and not expired
145
+ *
146
+ * @param {string} identifier - Cache identifier
147
+ * @returns {Promise<boolean>} True if cached and valid
148
+ */
149
+ async has(identifier) {
150
+ const cached = await this.get(identifier);
151
+ return cached !== null && !cached.expired;
152
+ }
153
+
154
+ /**
155
+ * Remove cached PDF
156
+ *
157
+ * @param {string} identifier - Cache identifier
158
+ * @returns {Promise<boolean>} True if removed successfully
159
+ */
160
+ async remove(identifier) {
161
+ if (!identifier) {
162
+ throw new Error('identifier is required');
163
+ }
164
+
165
+ try {
166
+ await PDFJSIManager.removeCachedPDF(identifier);
167
+ return true;
168
+ } catch (error) {
169
+ console.warn('[CacheManager] Failed to remove cached PDF:', error);
170
+ return false;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Clear all cached PDFs
176
+ *
177
+ * @returns {Promise<void>}
178
+ */
179
+ async clear() {
180
+ try {
181
+ await PDFJSIManager.clearPDFCache();
182
+ } catch (error) {
183
+ console.error('[CacheManager] Failed to clear cache:', error);
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Clear expired PDFs only
190
+ *
191
+ * @returns {Promise<number>} Number of entries removed
192
+ */
193
+ async clearExpired() {
194
+ try {
195
+ const result = await PDFJSIManager.clearExpiredPDFs();
196
+ return result.removedCount || 0;
197
+ } catch (error) {
198
+ console.warn('[CacheManager] Failed to clear expired:', error);
199
+ return 0;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Get cache statistics
205
+ *
206
+ * @returns {Promise<Object>} Cache stats { totalSize, fileCount, hitRate }
207
+ */
208
+ async getStats() {
209
+ try {
210
+ const stats = await PDFJSIManager.getPDFCacheStats();
211
+ return {
212
+ totalSize: stats.totalSize || 0,
213
+ fileCount: stats.fileCount || 0,
214
+ hitRate: stats.hitRate || 0,
215
+ cacheHits: stats.cacheHits || 0,
216
+ cacheMisses: stats.cacheMisses || 0,
217
+ averageLoadTime: stats.averageLoadTimeMs || 0
218
+ };
219
+ } catch (error) {
220
+ console.warn('[CacheManager] Failed to get stats:', error);
221
+ return {
222
+ totalSize: 0,
223
+ fileCount: 0,
224
+ hitRate: 0,
225
+ cacheHits: 0,
226
+ cacheMisses: 0,
227
+ averageLoadTime: 0
228
+ };
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Estimate decoded size from base64 length
234
+ * Base64 encoding increases size by ~33%, so decoded is ~75%
235
+ *
236
+ * @param {number} base64Length - Length of base64 string
237
+ * @returns {number} Estimated decoded size in bytes
238
+ */
239
+ estimateDecodedSize(base64Length) {
240
+ return Math.floor(base64Length * 0.75);
241
+ }
242
+
243
+ /**
244
+ * Generate identifier from base64 fingerprint
245
+ * Uses first 100 chars + last 100 chars + length
246
+ *
247
+ * @private
248
+ */
249
+ _generateIdentifier(base64) {
250
+ const length = base64.length;
251
+ const fingerprint = base64.substring(0, Math.min(100, length)) +
252
+ base64.substring(Math.max(0, length - 100)) +
253
+ length;
254
+
255
+ // Simple hash (can be improved with crypto-js)
256
+ let hash = 0;
257
+ for (let i = 0; i < fingerprint.length; i++) {
258
+ const char = fingerprint.charCodeAt(i);
259
+ hash = ((hash << 5) - hash) + char;
260
+ hash = hash & hash; // Convert to 32bit integer
261
+ }
262
+
263
+ return `pdf_${Math.abs(hash).toString(36)}_${Date.now()}`;
264
+ }
265
+ }
266
+
267
+ // Export singleton instance
268
+ export default new CacheManager();
269
+
270
+
271
+
@@ -87,3 +87,4 @@ export default FileManager;
87
87
 
88
88
 
89
89
 
90
+
@@ -282,3 +282,6 @@ export default performanceLogger;
282
282
 
283
283
 
284
284
 
285
+
286
+
287
+