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.
- package/android/src/main/java/org/wonday/pdf/FileDownloader.java +1 -0
- package/android/src/main/java/org/wonday/pdf/FileManager.java +1 -0
- package/android/src/main/java/org/wonday/pdf/PdfManager.java +3 -2
- package/android/src/main/java/org/wonday/pdf/PdfView.java +29 -0
- package/android/src/main/java/org/wonday/pdf/StreamingBase64Decoder.java +236 -0
- package/index.d.ts +127 -0
- package/index.js +6 -1
- package/ios/RNPDFPdf/PDFExporter.m +8 -42
- package/ios/RNPDFPdf/RNPDFPdfView.h +3 -0
- package/ios/RNPDFPdf/RNPDFPdfView.mm +46 -0
- package/ios/RNPDFPdf/RNPDFPdfViewManager.mm +3 -0
- package/package.json +1 -1
- package/src/managers/CacheManager.js +271 -0
- package/src/managers/FileManager.js +1 -0
- package/src/utils/PerformanceLogger.js +3 -0
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
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
|
+
|