react-native-pdf-jsi 4.2.0 → 4.2.2
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/README.md +15 -0
- package/android/src/main/java/org/wonday/pdf/PDFExporter.java +74 -0
- package/index.d.ts +244 -0
- package/index.js +5 -1
- package/ios/RNPDFPdf/PDFExporter.m +83 -0
- package/ios/RNPDFPdf/RNPDFPdfView.mm +0 -309
- package/package.json +1 -1
- package/src/PDFCompressor.js +476 -0
- package/src/index.js +15 -1
package/README.md
CHANGED
|
@@ -410,6 +410,21 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
410
410
|
|
|
411
411
|
## Recent Fixes
|
|
412
412
|
|
|
413
|
+
### PDFCompressor Module Fix (v4.2.2)
|
|
414
|
+
Fixed "Unable to resolve module react-native-pdf-jsi/src/PDFCompressor" error ([#17](https://github.com/126punith/react-native-pdf-jsi/issues/17)). The PDFCompressor module is now properly exported and accessible. Also fixed iOS compilation error for missing `RCTLogInfo` import. The compression feature now works correctly with accurate size estimates (~15-18% compression using native zlib deflate).
|
|
415
|
+
|
|
416
|
+
**Usage:**
|
|
417
|
+
```jsx
|
|
418
|
+
import { PDFCompressor, CompressionPreset } from 'react-native-pdf-jsi';
|
|
419
|
+
|
|
420
|
+
// Compress a PDF
|
|
421
|
+
const result = await PDFCompressor.compressWithPreset(pdfPath, CompressionPreset.WEB);
|
|
422
|
+
console.log(`Compressed: ${result.originalSizeMB}MB → ${result.compressedSizeMB}MB`);
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### iOS Performance - Unnecessary Path Handlers (v4.2.1)
|
|
426
|
+
Use v4.2.1 it contains stable fixes for IOS with unwanted debug logs removed
|
|
427
|
+
|
|
413
428
|
### iOS Performance - Unnecessary Path Handlers (v4.2.0)
|
|
414
429
|
Fixed performance issue where path-related handlers were running unnecessarily when the path value hadn't actually changed. The fix filters out "path" from effectiveChangedProps when pathActuallyChanged=NO, preventing unnecessary reconfigurations of spacing, display direction, scroll views, usePageViewController, and other path-dependent handlers. This reduces unnecessary rerenders and improves performance, especially when navigating between pages. Addresses issue #7 (Page Prop Causes Full Rerender).
|
|
415
430
|
|
|
@@ -734,4 +734,78 @@ public class PDFExporter extends ReactContextBaseJavaModule {
|
|
|
734
734
|
promise.reject("PAGE_COUNT_ERROR", e.getMessage());
|
|
735
735
|
}
|
|
736
736
|
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Compress PDF using streaming processor
|
|
740
|
+
* Uses O(1) constant memory regardless of file size
|
|
741
|
+
* @param inputPath Input PDF file path
|
|
742
|
+
* @param outputPath Output compressed PDF file path
|
|
743
|
+
* @param compressionLevel Compression level (0-9, 9 is maximum compression)
|
|
744
|
+
* @param promise Promise to resolve with compression result
|
|
745
|
+
*/
|
|
746
|
+
@ReactMethod
|
|
747
|
+
public void compressPDF(String inputPath, String outputPath, int compressionLevel, Promise promise) {
|
|
748
|
+
try {
|
|
749
|
+
Log.d(TAG, "compressPDF called with inputPath: " + inputPath + ", outputPath: " + outputPath + ", level: " + compressionLevel);
|
|
750
|
+
|
|
751
|
+
if (inputPath == null || inputPath.isEmpty()) {
|
|
752
|
+
promise.reject("INVALID_PATH", "Input file path is required");
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
File inputFile = new File(inputPath);
|
|
757
|
+
if (!inputFile.exists()) {
|
|
758
|
+
promise.reject("FILE_NOT_FOUND", "Input PDF file not found: " + inputPath);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Generate output path if not provided
|
|
763
|
+
File outputFile;
|
|
764
|
+
if (outputPath == null || outputPath.isEmpty()) {
|
|
765
|
+
String baseName = inputFile.getName().replaceAll("\\.pdf$", "");
|
|
766
|
+
String outputFileName = generateTimestampedFileName(baseName + "_compressed", -1, "pdf");
|
|
767
|
+
outputFile = new File(inputFile.getParent(), outputFileName);
|
|
768
|
+
} else {
|
|
769
|
+
outputFile = new File(outputPath);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Ensure output directory exists
|
|
773
|
+
File outputDir = outputFile.getParentFile();
|
|
774
|
+
if (outputDir != null && !outputDir.exists()) {
|
|
775
|
+
outputDir.mkdirs();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
Log.d(TAG, "Starting compression: " + inputFile.getAbsolutePath() + " -> " + outputFile.getAbsolutePath());
|
|
779
|
+
|
|
780
|
+
// Use StreamingPDFProcessor for O(1) memory compression
|
|
781
|
+
StreamingPDFProcessor processor = new StreamingPDFProcessor();
|
|
782
|
+
StreamingPDFProcessor.CompressionResult result = processor.compressPDFStreaming(
|
|
783
|
+
inputFile,
|
|
784
|
+
outputFile,
|
|
785
|
+
compressionLevel
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
// Build response
|
|
789
|
+
WritableMap response = Arguments.createMap();
|
|
790
|
+
response.putDouble("originalSize", result.originalSize);
|
|
791
|
+
response.putDouble("compressedSize", result.compressedSize);
|
|
792
|
+
response.putDouble("durationMs", result.durationMs);
|
|
793
|
+
response.putDouble("compressionRatio", result.compressionRatio);
|
|
794
|
+
response.putDouble("spaceSavedPercent", result.spaceSavedPercent);
|
|
795
|
+
response.putString("outputPath", outputFile.getAbsolutePath());
|
|
796
|
+
response.putBoolean("success", true);
|
|
797
|
+
|
|
798
|
+
Log.d(TAG, String.format("Compression complete: %.2f MB -> %.2f MB (%.1f%% saved) in %dms",
|
|
799
|
+
result.originalSize / (1024.0 * 1024.0),
|
|
800
|
+
result.compressedSize / (1024.0 * 1024.0),
|
|
801
|
+
result.spaceSavedPercent,
|
|
802
|
+
result.durationMs));
|
|
803
|
+
|
|
804
|
+
promise.resolve(response);
|
|
805
|
+
|
|
806
|
+
} catch (Exception e) {
|
|
807
|
+
Log.e(TAG, "Error compressing PDF", e);
|
|
808
|
+
promise.reject("COMPRESSION_ERROR", e.getMessage(), e);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
737
811
|
}
|
package/index.d.ts
CHANGED
|
@@ -219,3 +219,247 @@ export const PDFCache: PDFCacheManager;
|
|
|
219
219
|
* CacheManager (alias for PDFCache)
|
|
220
220
|
*/
|
|
221
221
|
export const CacheManager: PDFCacheManager;
|
|
222
|
+
|
|
223
|
+
// ========================================
|
|
224
|
+
// PDFCompressor (PDF Compression)
|
|
225
|
+
// ========================================
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Compression presets for different use cases
|
|
229
|
+
*/
|
|
230
|
+
export enum CompressionPreset {
|
|
231
|
+
/** Optimized for email attachments (high compression, smaller file) */
|
|
232
|
+
EMAIL = 'email',
|
|
233
|
+
/** Optimized for web viewing (balanced compression) */
|
|
234
|
+
WEB = 'web',
|
|
235
|
+
/** Optimized for mobile devices (good compression, fast decompression) */
|
|
236
|
+
MOBILE = 'mobile',
|
|
237
|
+
/** Optimized for printing (low compression, high quality) */
|
|
238
|
+
PRINT = 'print',
|
|
239
|
+
/** Optimized for long-term archival (maximum compression) */
|
|
240
|
+
ARCHIVE = 'archive',
|
|
241
|
+
/** Custom compression settings */
|
|
242
|
+
CUSTOM = 'custom'
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Compression levels (0-9, higher = more compression but slower)
|
|
247
|
+
*/
|
|
248
|
+
export enum CompressionLevel {
|
|
249
|
+
NONE = 0,
|
|
250
|
+
FASTEST = 1,
|
|
251
|
+
FAST = 3,
|
|
252
|
+
BALANCED = 5,
|
|
253
|
+
DEFAULT = 6,
|
|
254
|
+
GOOD = 7,
|
|
255
|
+
BETTER = 8,
|
|
256
|
+
BEST = 9
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Compression options
|
|
261
|
+
*/
|
|
262
|
+
export interface CompressionOptions {
|
|
263
|
+
/** Compression preset (from CompressionPreset) */
|
|
264
|
+
preset?: CompressionPreset;
|
|
265
|
+
/** Compression level (0-9), overrides preset */
|
|
266
|
+
level?: number;
|
|
267
|
+
/** Output file path (optional, auto-generated if not provided) */
|
|
268
|
+
outputPath?: string;
|
|
269
|
+
/** Progress callback function */
|
|
270
|
+
onProgress?: (progress: { progress: number; bytesProcessed: number; totalBytes: number }) => void;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Compression result
|
|
275
|
+
*/
|
|
276
|
+
export interface CompressionResult {
|
|
277
|
+
/** Whether compression was successful */
|
|
278
|
+
success: boolean;
|
|
279
|
+
/** Input file path */
|
|
280
|
+
inputPath: string;
|
|
281
|
+
/** Output file path */
|
|
282
|
+
outputPath: string;
|
|
283
|
+
/** Original file size in bytes */
|
|
284
|
+
originalSize: number;
|
|
285
|
+
/** Compressed file size in bytes */
|
|
286
|
+
compressedSize: number;
|
|
287
|
+
/** Original file size in MB */
|
|
288
|
+
originalSizeMB: number;
|
|
289
|
+
/** Compressed file size in MB */
|
|
290
|
+
compressedSizeMB: number;
|
|
291
|
+
/** Compression ratio (0-1, lower is better) */
|
|
292
|
+
compressionRatio: number;
|
|
293
|
+
/** Space saved percentage */
|
|
294
|
+
spaceSavedPercent: number;
|
|
295
|
+
/** Duration in milliseconds */
|
|
296
|
+
durationMs: number;
|
|
297
|
+
/** Throughput in MB/s */
|
|
298
|
+
throughputMBps: number;
|
|
299
|
+
/** Preset used */
|
|
300
|
+
preset: CompressionPreset;
|
|
301
|
+
/** Compression level used */
|
|
302
|
+
compressionLevel: number;
|
|
303
|
+
/** Method used (native_streaming or fallback) */
|
|
304
|
+
method: string;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Compression estimate result
|
|
309
|
+
*/
|
|
310
|
+
export interface CompressionEstimate {
|
|
311
|
+
/** Input file path */
|
|
312
|
+
inputPath: string;
|
|
313
|
+
/** Original file size in bytes */
|
|
314
|
+
originalSize: number;
|
|
315
|
+
/** Original file size in MB */
|
|
316
|
+
originalSizeMB: number;
|
|
317
|
+
/** Estimated compressed size in bytes */
|
|
318
|
+
estimatedCompressedSize: number;
|
|
319
|
+
/** Estimated compressed size in MB */
|
|
320
|
+
estimatedCompressedSizeMB: number;
|
|
321
|
+
/** Estimated compression ratio */
|
|
322
|
+
estimatedCompressionRatio: number;
|
|
323
|
+
/** Estimated space savings percentage */
|
|
324
|
+
estimatedSavingsPercent: number;
|
|
325
|
+
/** Estimated duration in milliseconds */
|
|
326
|
+
estimatedDurationMs: number;
|
|
327
|
+
/** Preset used for estimate */
|
|
328
|
+
preset: CompressionPreset;
|
|
329
|
+
/** Description of the preset */
|
|
330
|
+
presetDescription: string;
|
|
331
|
+
/** Confidence level of the estimate */
|
|
332
|
+
confidence: 'low' | 'medium' | 'high';
|
|
333
|
+
/** Additional notes */
|
|
334
|
+
note: string;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* PDFCompressor capabilities
|
|
339
|
+
*/
|
|
340
|
+
export interface CompressionCapabilities {
|
|
341
|
+
/** Whether streaming compression is available */
|
|
342
|
+
streamingCompression: boolean;
|
|
343
|
+
/** Available presets */
|
|
344
|
+
presets: CompressionPreset[];
|
|
345
|
+
/** Maximum file size in MB */
|
|
346
|
+
maxFileSizeMB: number;
|
|
347
|
+
/** Supported platforms */
|
|
348
|
+
supportedPlatforms: string[];
|
|
349
|
+
/** Current platform */
|
|
350
|
+
currentPlatform: string;
|
|
351
|
+
/** Whether native module is available */
|
|
352
|
+
nativeModuleAvailable: boolean;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* PDFCompressor Manager for PDF compression
|
|
357
|
+
* Uses native streaming for O(1) memory operations on large files (1GB+)
|
|
358
|
+
*/
|
|
359
|
+
export interface PDFCompressorManager {
|
|
360
|
+
/**
|
|
361
|
+
* Check if compression functionality is available
|
|
362
|
+
* @returns True if compression is available
|
|
363
|
+
*/
|
|
364
|
+
isAvailable(): boolean;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Get compression capabilities
|
|
368
|
+
* @returns Capabilities object
|
|
369
|
+
*/
|
|
370
|
+
getCapabilities(): CompressionCapabilities;
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Compress a PDF file
|
|
374
|
+
* @param inputPath Path to input PDF file
|
|
375
|
+
* @param options Compression options
|
|
376
|
+
* @returns Promise resolving to compression result
|
|
377
|
+
*/
|
|
378
|
+
compress(inputPath: string, options?: CompressionOptions): Promise<CompressionResult>;
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Compress PDF with a specific preset
|
|
382
|
+
* @param inputPath Path to input PDF file
|
|
383
|
+
* @param preset Compression preset
|
|
384
|
+
* @param outputPath Output file path (optional)
|
|
385
|
+
* @returns Promise resolving to compression result
|
|
386
|
+
*/
|
|
387
|
+
compressWithPreset(inputPath: string, preset: CompressionPreset, outputPath?: string): Promise<CompressionResult>;
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Compress PDF for email (maximum compression)
|
|
391
|
+
* @param inputPath Path to input PDF file
|
|
392
|
+
* @param outputPath Output file path (optional)
|
|
393
|
+
* @returns Promise resolving to compression result
|
|
394
|
+
*/
|
|
395
|
+
compressForEmail(inputPath: string, outputPath?: string): Promise<CompressionResult>;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Compress PDF for web viewing
|
|
399
|
+
* @param inputPath Path to input PDF file
|
|
400
|
+
* @param outputPath Output file path (optional)
|
|
401
|
+
* @returns Promise resolving to compression result
|
|
402
|
+
*/
|
|
403
|
+
compressForWeb(inputPath: string, outputPath?: string): Promise<CompressionResult>;
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Compress PDF for mobile viewing
|
|
407
|
+
* @param inputPath Path to input PDF file
|
|
408
|
+
* @param outputPath Output file path (optional)
|
|
409
|
+
* @returns Promise resolving to compression result
|
|
410
|
+
*/
|
|
411
|
+
compressForMobile(inputPath: string, outputPath?: string): Promise<CompressionResult>;
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Compress PDF for archival (maximum compression)
|
|
415
|
+
* @param inputPath Path to input PDF file
|
|
416
|
+
* @param outputPath Output file path (optional)
|
|
417
|
+
* @returns Promise resolving to compression result
|
|
418
|
+
*/
|
|
419
|
+
compressForArchive(inputPath: string, outputPath?: string): Promise<CompressionResult>;
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Estimate compression result without actually compressing
|
|
423
|
+
* @param inputPath Path to input PDF file
|
|
424
|
+
* @param preset Compression preset
|
|
425
|
+
* @returns Promise resolving to estimated compression result
|
|
426
|
+
*/
|
|
427
|
+
estimateCompression(inputPath: string, preset?: CompressionPreset): Promise<CompressionEstimate>;
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Delete a compressed file
|
|
431
|
+
* @param filePath Path to file to delete
|
|
432
|
+
* @returns Promise resolving to true if deleted successfully
|
|
433
|
+
*/
|
|
434
|
+
deleteCompressedFile(filePath: string): Promise<boolean>;
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get preset configuration
|
|
438
|
+
* @param preset Preset name
|
|
439
|
+
* @returns Preset configuration
|
|
440
|
+
*/
|
|
441
|
+
getPresetConfig(preset: CompressionPreset): {
|
|
442
|
+
level: CompressionLevel;
|
|
443
|
+
targetSizeKB: number | null;
|
|
444
|
+
description: string;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Get module information
|
|
449
|
+
* @returns Module info object
|
|
450
|
+
*/
|
|
451
|
+
getModuleInfo(): {
|
|
452
|
+
name: string;
|
|
453
|
+
version: string;
|
|
454
|
+
platform: string;
|
|
455
|
+
nativeAvailable: boolean;
|
|
456
|
+
presets: string[];
|
|
457
|
+
compressionLevels: string[];
|
|
458
|
+
capabilities: CompressionCapabilities;
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* PDFCompressor singleton instance
|
|
464
|
+
*/
|
|
465
|
+
export const PDFCompressor: PDFCompressorManager;
|
package/index.js
CHANGED
|
@@ -742,6 +742,7 @@ import BookmarkManager from './src/managers/BookmarkManager';
|
|
|
742
742
|
import AnalyticsManager from './src/managers/AnalyticsManager';
|
|
743
743
|
import FileManager from './src/managers/FileManager';
|
|
744
744
|
import CacheManager from './src/managers/CacheManager';
|
|
745
|
+
import PDFCompressor, { CompressionPreset, CompressionLevel } from './src/PDFCompressor';
|
|
745
746
|
|
|
746
747
|
// Alias for backward compatibility and intuitive naming
|
|
747
748
|
export const PDFCache = CacheManager;
|
|
@@ -751,7 +752,10 @@ export {
|
|
|
751
752
|
BookmarkManager,
|
|
752
753
|
AnalyticsManager,
|
|
753
754
|
FileManager,
|
|
754
|
-
CacheManager
|
|
755
|
+
CacheManager,
|
|
756
|
+
PDFCompressor,
|
|
757
|
+
CompressionPreset,
|
|
758
|
+
CompressionLevel
|
|
755
759
|
};
|
|
756
760
|
|
|
757
761
|
// ========================================
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#import "PDFExporter.h"
|
|
2
2
|
#import "ImagePool.h"
|
|
3
|
+
#import "StreamingPDFProcessor.h"
|
|
4
|
+
#import <React/RCTLog.h>
|
|
3
5
|
#import <PDFKit/PDFKit.h>
|
|
4
6
|
#import <UIKit/UIKit.h>
|
|
5
7
|
|
|
@@ -956,4 +958,85 @@ RCT_EXPORT_METHOD(getPageCount:(NSString *)filePath
|
|
|
956
958
|
resolve(@(pageCount));
|
|
957
959
|
}
|
|
958
960
|
|
|
961
|
+
/**
|
|
962
|
+
* Compress PDF using streaming processor
|
|
963
|
+
* Uses O(1) constant memory regardless of file size
|
|
964
|
+
* @param inputPath Input PDF file path
|
|
965
|
+
* @param outputPath Output compressed PDF file path
|
|
966
|
+
* @param compressionLevel Compression level (0-9, 9 is maximum compression)
|
|
967
|
+
*/
|
|
968
|
+
RCT_EXPORT_METHOD(compressPDF:(NSString *)inputPath
|
|
969
|
+
outputPath:(NSString *)outputPath
|
|
970
|
+
compressionLevel:(int)compressionLevel
|
|
971
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
972
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
973
|
+
|
|
974
|
+
RCTLogInfo(@"compressPDF called with inputPath: %@, outputPath: %@, level: %d", inputPath, outputPath, compressionLevel);
|
|
975
|
+
|
|
976
|
+
if (!inputPath || inputPath.length == 0) {
|
|
977
|
+
reject(@"INVALID_PATH", @"Input file path is required", nil);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
982
|
+
if (![fileManager fileExistsAtPath:inputPath]) {
|
|
983
|
+
reject(@"FILE_NOT_FOUND", [NSString stringWithFormat:@"Input PDF file not found: %@", inputPath], nil);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Generate output path if not provided
|
|
988
|
+
NSString *finalOutputPath = outputPath;
|
|
989
|
+
if (!finalOutputPath || finalOutputPath.length == 0) {
|
|
990
|
+
NSString *directory = [inputPath stringByDeletingLastPathComponent];
|
|
991
|
+
NSString *baseName = [[inputPath lastPathComponent] stringByDeletingPathExtension];
|
|
992
|
+
NSString *outputFileName = [self generateTimestampedFileName:[baseName stringByAppendingString:@"_compressed"] pageNum:-1 extension:@"pdf"];
|
|
993
|
+
finalOutputPath = [directory stringByAppendingPathComponent:outputFileName];
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Ensure output directory exists
|
|
997
|
+
NSString *outputDir = [finalOutputPath stringByDeletingLastPathComponent];
|
|
998
|
+
if (![fileManager fileExistsAtPath:outputDir]) {
|
|
999
|
+
NSError *error;
|
|
1000
|
+
[fileManager createDirectoryAtPath:outputDir withIntermediateDirectories:YES attributes:nil error:&error];
|
|
1001
|
+
if (error) {
|
|
1002
|
+
reject(@"DIR_CREATE_ERROR", @"Failed to create output directory", error);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
RCTLogInfo(@"Starting compression: %@ -> %@", inputPath, finalOutputPath);
|
|
1008
|
+
|
|
1009
|
+
// Use StreamingPDFProcessor for O(1) memory compression
|
|
1010
|
+
StreamingPDFProcessor *processor = [StreamingPDFProcessor sharedInstance];
|
|
1011
|
+
NSError *error;
|
|
1012
|
+
CompressionResult *result = [processor compressPDFStreaming:inputPath
|
|
1013
|
+
outputPath:finalOutputPath
|
|
1014
|
+
compressionLevel:compressionLevel
|
|
1015
|
+
error:&error];
|
|
1016
|
+
|
|
1017
|
+
if (error || !result) {
|
|
1018
|
+
reject(@"COMPRESSION_ERROR", error ? error.localizedDescription : @"Compression failed", error);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Build response
|
|
1023
|
+
NSDictionary *response = @{
|
|
1024
|
+
@"originalSize": @(result.originalSize),
|
|
1025
|
+
@"compressedSize": @(result.compressedSize),
|
|
1026
|
+
@"durationMs": @(result.durationMs),
|
|
1027
|
+
@"compressionRatio": @(result.compressionRatio),
|
|
1028
|
+
@"spaceSavedPercent": @(result.spaceSavedPercent),
|
|
1029
|
+
@"outputPath": finalOutputPath,
|
|
1030
|
+
@"success": @YES
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
RCTLogInfo(@"Compression complete: %.2f MB -> %.2f MB (%.1f%% saved) in %.0fms",
|
|
1034
|
+
result.originalSize / (1024.0 * 1024.0),
|
|
1035
|
+
result.compressedSize / (1024.0 * 1024.0),
|
|
1036
|
+
result.spaceSavedPercent,
|
|
1037
|
+
result.durationMs);
|
|
1038
|
+
|
|
1039
|
+
resolve(response);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
959
1042
|
@end
|
|
@@ -399,29 +399,6 @@ using namespace facebook::react;
|
|
|
399
399
|
_currentUsePageViewController = NO;
|
|
400
400
|
_usePageViewControllerStateInitialized = NO;
|
|
401
401
|
|
|
402
|
-
// #region agent log
|
|
403
|
-
{
|
|
404
|
-
NSString *logPath0 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
|
|
405
|
-
NSDictionary *logEntry0 = @{
|
|
406
|
-
@"sessionId": @"debug-session",
|
|
407
|
-
@"runId": @"init",
|
|
408
|
-
@"hypothesisId": @"F",
|
|
409
|
-
@"location": @"RNPDFPdfView.mm:393",
|
|
410
|
-
@"message": @"initCommonProps completed",
|
|
411
|
-
@"data": @{
|
|
412
|
-
@"horizontal": @(_horizontal),
|
|
413
|
-
@"enablePaging": @(_enablePaging),
|
|
414
|
-
@"scrollEnabled": @(_scrollEnabled),
|
|
415
|
-
@"singlePage": @(_singlePage)
|
|
416
|
-
},
|
|
417
|
-
@"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
|
|
418
|
-
};
|
|
419
|
-
NSData *logData0 = [NSJSONSerialization dataWithJSONObject:logEntry0 options:0 error:nil];
|
|
420
|
-
NSString *logLine0 = [[NSString alloc] initWithData:logData0 encoding:NSUTF8StringEncoding];
|
|
421
|
-
[[logLine0 stringByAppendingString:@"\n"] writeToFile:logPath0 atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
422
|
-
}
|
|
423
|
-
// #endregion
|
|
424
|
-
|
|
425
402
|
// Enhanced properties
|
|
426
403
|
_enableCaching = YES;
|
|
427
404
|
_enablePreloading = YES;
|
|
@@ -802,38 +779,6 @@ using namespace facebook::react;
|
|
|
802
779
|
_page > 0 &&
|
|
803
780
|
_page <= (int)_pdfDocument.pageCount;
|
|
804
781
|
|
|
805
|
-
// #region agent log
|
|
806
|
-
if ([changedProps containsObject:@"page"]) {
|
|
807
|
-
NSString *logPath14 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
|
|
808
|
-
NSDictionary *logEntry14 = @{
|
|
809
|
-
@"sessionId": @"debug-session",
|
|
810
|
-
@"runId": @"init",
|
|
811
|
-
@"hypothesisId": @"A,C,D",
|
|
812
|
-
@"location": @"RNPDFPdfView.mm:803",
|
|
813
|
-
@"message": @"updateProps: page prop changed - checking shouldNavigateToPage",
|
|
814
|
-
@"data": @{
|
|
815
|
-
@"_page": @(_page),
|
|
816
|
-
@"_previousPage": @(_previousPage),
|
|
817
|
-
@"documentLoaded": @(_documentLoaded),
|
|
818
|
-
@"isNavigating": @(_isNavigating),
|
|
819
|
-
@"shouldNavigateToPage": @(shouldNavigateToPage),
|
|
820
|
-
@"contentOffsetBeforeNav": _internalScrollView ? @{@"x": @(_internalScrollView.contentOffset.x), @"y": @(_internalScrollView.contentOffset.y)} : @"noScrollView"
|
|
821
|
-
},
|
|
822
|
-
@"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
|
|
823
|
-
};
|
|
824
|
-
NSData *logData14 = [NSJSONSerialization dataWithJSONObject:logEntry14 options:0 error:nil];
|
|
825
|
-
NSString *logLine14 = [[NSString alloc] initWithData:logData14 encoding:NSUTF8StringEncoding];
|
|
826
|
-
NSFileHandle *fileHandle14 = [NSFileHandle fileHandleForWritingAtPath:logPath14];
|
|
827
|
-
if (fileHandle14) {
|
|
828
|
-
[fileHandle14 seekToEndOfFile];
|
|
829
|
-
[fileHandle14 writeData:[[logLine14 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
830
|
-
[fileHandle14 closeFile];
|
|
831
|
-
} else {
|
|
832
|
-
[[logLine14 stringByAppendingString:@"\n"] writeToFile:logPath14 atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
// #endregion
|
|
836
|
-
|
|
837
782
|
if (shouldNavigateToPage) {
|
|
838
783
|
_isNavigating = YES;
|
|
839
784
|
PDFPage *pdfPage = [_pdfDocument pageAtIndex:_page-1];
|
|
@@ -841,35 +786,6 @@ using namespace facebook::react;
|
|
|
841
786
|
if (pdfPage) {
|
|
842
787
|
// Use smooth navigation instead of instant jump to prevent full rerender
|
|
843
788
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
844
|
-
// #region agent log
|
|
845
|
-
CGPoint contentOffsetBefore = self->_internalScrollView ? self->_internalScrollView.contentOffset : CGPointMake(0, 0);
|
|
846
|
-
NSString *logPath15 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
|
|
847
|
-
NSDictionary *logEntry15 = @{
|
|
848
|
-
@"sessionId": @"debug-session",
|
|
849
|
-
@"runId": @"init",
|
|
850
|
-
@"hypothesisId": @"B,C",
|
|
851
|
-
@"location": @"RNPDFPdfView.mm:812",
|
|
852
|
-
@"message": @"goToDestination: BEFORE navigation call",
|
|
853
|
-
@"data": @{
|
|
854
|
-
@"targetPage": @(self->_page),
|
|
855
|
-
@"enablePaging": @(self->_enablePaging),
|
|
856
|
-
@"contentOffsetBefore": @{@"x": @(contentOffsetBefore.x), @"y": @(contentOffsetBefore.y)},
|
|
857
|
-
@"isNavigating": @(self->_isNavigating)
|
|
858
|
-
},
|
|
859
|
-
@"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
|
|
860
|
-
};
|
|
861
|
-
NSData *logData15 = [NSJSONSerialization dataWithJSONObject:logEntry15 options:0 error:nil];
|
|
862
|
-
NSString *logLine15 = [[NSString alloc] initWithData:logData15 encoding:NSUTF8StringEncoding];
|
|
863
|
-
NSFileHandle *fileHandle15 = [NSFileHandle fileHandleForWritingAtPath:logPath15];
|
|
864
|
-
if (fileHandle15) {
|
|
865
|
-
[fileHandle15 seekToEndOfFile];
|
|
866
|
-
[fileHandle15 writeData:[[logLine15 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
867
|
-
[fileHandle15 closeFile];
|
|
868
|
-
} else {
|
|
869
|
-
[[logLine15 stringByAppendingString:@"\n"] writeToFile:logPath15 atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
870
|
-
}
|
|
871
|
-
// #endregion
|
|
872
|
-
|
|
873
789
|
if (!self->_enablePaging) {
|
|
874
790
|
// For non-paging mode, use animated navigation
|
|
875
791
|
CGRect pdfPageRect = [pdfPage boundsForBox:kPDFDisplayBoxCropBox];
|
|
@@ -904,38 +820,6 @@ using namespace facebook::react;
|
|
|
904
820
|
|
|
905
821
|
self->_previousPage = self->_page;
|
|
906
822
|
self->_isNavigating = NO;
|
|
907
|
-
|
|
908
|
-
// #region agent log
|
|
909
|
-
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
910
|
-
CGPoint contentOffsetAfter = self->_internalScrollView ? self->_internalScrollView.contentOffset : CGPointMake(0, 0);
|
|
911
|
-
NSString *logPath16 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
|
|
912
|
-
NSDictionary *logEntry16 = @{
|
|
913
|
-
@"sessionId": @"debug-session",
|
|
914
|
-
@"runId": @"init",
|
|
915
|
-
@"hypothesisId": @"B,C",
|
|
916
|
-
@"location": @"RNPDFPdfView.mm:845",
|
|
917
|
-
@"message": @"goToDestination: AFTER navigation call (100ms delay)",
|
|
918
|
-
@"data": @{
|
|
919
|
-
@"targetPage": @(self->_page),
|
|
920
|
-
@"previousPage": @(self->_previousPage),
|
|
921
|
-
@"contentOffsetBefore": @{@"x": @(contentOffsetBefore.x), @"y": @(contentOffsetBefore.y)},
|
|
922
|
-
@"contentOffsetAfter": @{@"x": @(contentOffsetAfter.x), @"y": @(contentOffsetAfter.y)},
|
|
923
|
-
@"offsetChanged": @(fabs(contentOffsetBefore.x - contentOffsetAfter.x) > 1 || fabs(contentOffsetBefore.y - contentOffsetAfter.y) > 1)
|
|
924
|
-
},
|
|
925
|
-
@"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
|
|
926
|
-
};
|
|
927
|
-
NSData *logData16 = [NSJSONSerialization dataWithJSONObject:logEntry16 options:0 error:nil];
|
|
928
|
-
NSString *logLine16 = [[NSString alloc] initWithData:logData16 encoding:NSUTF8StringEncoding];
|
|
929
|
-
NSFileHandle *fileHandle16 = [NSFileHandle fileHandleForWritingAtPath:logPath16];
|
|
930
|
-
if (fileHandle16) {
|
|
931
|
-
[fileHandle16 seekToEndOfFile];
|
|
932
|
-
[fileHandle16 writeData:[[logLine16 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
933
|
-
[fileHandle16 closeFile];
|
|
934
|
-
} else {
|
|
935
|
-
[[logLine16 stringByAppendingString:@"\n"] writeToFile:logPath16 atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
936
|
-
}
|
|
937
|
-
});
|
|
938
|
-
// #endregion
|
|
939
823
|
});
|
|
940
824
|
} else {
|
|
941
825
|
_isNavigating = NO;
|
|
@@ -1415,40 +1299,6 @@ using namespace facebook::react;
|
|
|
1415
1299
|
NSStringFromCGSize(scrollView.contentSize),
|
|
1416
1300
|
enabled);
|
|
1417
1301
|
|
|
1418
|
-
// #region agent log
|
|
1419
|
-
{
|
|
1420
|
-
NSString *logPath1 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
|
|
1421
|
-
NSDictionary *logEntry1 = @{
|
|
1422
|
-
@"sessionId": @"debug-session",
|
|
1423
|
-
@"runId": @"init",
|
|
1424
|
-
@"hypothesisId": @"D,F",
|
|
1425
|
-
@"location": @"RNPDFPdfView.mm:1307",
|
|
1426
|
-
@"message": @"Found UIScrollView in hierarchy",
|
|
1427
|
-
@"data": @{
|
|
1428
|
-
@"depth": @(depth),
|
|
1429
|
-
@"contentSize": @{@"width": @(scrollView.contentSize.width), @"height": @(scrollView.contentSize.height)},
|
|
1430
|
-
@"frame": @{@"x": @(scrollView.frame.origin.x), @"y": @(scrollView.frame.origin.y), @"width": @(scrollView.frame.size.width), @"height": @(scrollView.frame.size.height)},
|
|
1431
|
-
@"scrollEnabled": @(scrollView.scrollEnabled),
|
|
1432
|
-
@"alwaysBounceHorizontal": @(scrollView.alwaysBounceHorizontal),
|
|
1433
|
-
@"userInteractionEnabled": @(scrollView.userInteractionEnabled),
|
|
1434
|
-
@"horizontal": @(_horizontal),
|
|
1435
|
-
@"enablePaging": @(_enablePaging)
|
|
1436
|
-
},
|
|
1437
|
-
@"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
|
|
1438
|
-
};
|
|
1439
|
-
NSData *logData1 = [NSJSONSerialization dataWithJSONObject:logEntry1 options:0 error:nil];
|
|
1440
|
-
NSString *logLine1 = [[NSString alloc] initWithData:logData1 encoding:NSUTF8StringEncoding];
|
|
1441
|
-
NSFileHandle *fileHandle1 = [NSFileHandle fileHandleForWritingAtPath:logPath1];
|
|
1442
|
-
if (fileHandle1) {
|
|
1443
|
-
[fileHandle1 seekToEndOfFile];
|
|
1444
|
-
[fileHandle1 writeData:[[logLine1 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
1445
|
-
[fileHandle1 closeFile];
|
|
1446
|
-
} else {
|
|
1447
|
-
[[logLine1 stringByAppendingString:@"\n"] writeToFile:logPath1 atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
// #endregion
|
|
1451
|
-
|
|
1452
1302
|
// Since we're starting the recursion from _pdfView, all scroll views found are within its hierarchy
|
|
1453
1303
|
// Configure scroll properties
|
|
1454
1304
|
BOOL previousScrollEnabled = scrollView.scrollEnabled;
|
|
@@ -1484,40 +1334,6 @@ using namespace facebook::react;
|
|
|
1484
1334
|
scrollView.bounces,
|
|
1485
1335
|
scrollView.delegate != nil ? @"set" : @"nil");
|
|
1486
1336
|
|
|
1487
|
-
// #region agent log
|
|
1488
|
-
{
|
|
1489
|
-
NSString *logPath3 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
|
|
1490
|
-
NSDictionary *logEntry3 = @{
|
|
1491
|
-
@"sessionId": @"debug-session",
|
|
1492
|
-
@"runId": @"init",
|
|
1493
|
-
@"hypothesisId": @"A,B,C,D,E",
|
|
1494
|
-
@"location": @"RNPDFPdfView.mm:1374",
|
|
1495
|
-
@"message": @"ScrollView configuration completed",
|
|
1496
|
-
@"data": @{
|
|
1497
|
-
@"scrollEnabled": @(scrollView.scrollEnabled),
|
|
1498
|
-
@"alwaysBounceHorizontal": @(scrollView.alwaysBounceHorizontal),
|
|
1499
|
-
@"bounces": @(scrollView.bounces),
|
|
1500
|
-
@"contentSize": @{@"width": @(scrollView.contentSize.width), @"height": @(scrollView.contentSize.height)},
|
|
1501
|
-
@"userInteractionEnabled": @(scrollView.userInteractionEnabled),
|
|
1502
|
-
@"delegate": scrollView.delegate != nil ? @"set" : @"nil",
|
|
1503
|
-
@"horizontal": @(_horizontal),
|
|
1504
|
-
@"enablePaging": @(_enablePaging)
|
|
1505
|
-
},
|
|
1506
|
-
@"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
|
|
1507
|
-
};
|
|
1508
|
-
NSData *logData3 = [NSJSONSerialization dataWithJSONObject:logEntry3 options:0 error:nil];
|
|
1509
|
-
NSString *logLine3 = [[NSString alloc] initWithData:logData3 encoding:NSUTF8StringEncoding];
|
|
1510
|
-
NSFileHandle *fileHandle3 = [NSFileHandle fileHandleForWritingAtPath:logPath3];
|
|
1511
|
-
if (fileHandle3) {
|
|
1512
|
-
[fileHandle3 seekToEndOfFile];
|
|
1513
|
-
[fileHandle3 writeData:[[logLine3 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
1514
|
-
[fileHandle3 closeFile];
|
|
1515
|
-
} else {
|
|
1516
|
-
[[logLine3 stringByAppendingString:@"\n"] writeToFile:logPath3 atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
// #endregion
|
|
1520
|
-
|
|
1521
1337
|
// IMPORTANT: PDFKit relies on the scrollView delegate for pinch-zoom (viewForZoomingInScrollView).
|
|
1522
1338
|
// Install a proxy delegate that forwards to the original delegate, while still letting us observe scroll events.
|
|
1523
1339
|
if (!_internalScrollView) {
|
|
@@ -1549,41 +1365,6 @@ using namespace facebook::react;
|
|
|
1549
1365
|
RCTLogWarn(@"⚠️ [iOS Scroll] No UIScrollView found in view hierarchy (view=%@, subviewCount=%lu)",
|
|
1550
1366
|
NSStringFromClass([view class]),
|
|
1551
1367
|
(unsigned long)[view.subviews count]);
|
|
1552
|
-
|
|
1553
|
-
// #region agent log
|
|
1554
|
-
{
|
|
1555
|
-
NSString *logPath6 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
|
|
1556
|
-
NSMutableArray *subviewClasses = [NSMutableArray array];
|
|
1557
|
-
for (UIView *subview in view.subviews) {
|
|
1558
|
-
[subviewClasses addObject:NSStringFromClass([subview class])];
|
|
1559
|
-
}
|
|
1560
|
-
NSDictionary *logEntry6 = @{
|
|
1561
|
-
@"sessionId": @"debug-session",
|
|
1562
|
-
@"runId": @"init",
|
|
1563
|
-
@"hypothesisId": @"F",
|
|
1564
|
-
@"location": @"RNPDFPdfView.mm:1446",
|
|
1565
|
-
@"message": @"No UIScrollView found in hierarchy",
|
|
1566
|
-
@"data": @{
|
|
1567
|
-
@"viewClass": NSStringFromClass([view class]),
|
|
1568
|
-
@"subviewCount": @([view.subviews count]),
|
|
1569
|
-
@"subviewClasses": subviewClasses,
|
|
1570
|
-
@"horizontal": @(_horizontal),
|
|
1571
|
-
@"enablePaging": @(_enablePaging)
|
|
1572
|
-
},
|
|
1573
|
-
@"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
|
|
1574
|
-
};
|
|
1575
|
-
NSData *logData6 = [NSJSONSerialization dataWithJSONObject:logEntry6 options:0 error:nil];
|
|
1576
|
-
NSString *logLine6 = [[NSString alloc] initWithData:logData6 encoding:NSUTF8StringEncoding];
|
|
1577
|
-
NSFileHandle *fileHandle6 = [NSFileHandle fileHandleForWritingAtPath:logPath6];
|
|
1578
|
-
if (fileHandle6) {
|
|
1579
|
-
[fileHandle6 seekToEndOfFile];
|
|
1580
|
-
[fileHandle6 writeData:[[logLine6 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
1581
|
-
[fileHandle6 closeFile];
|
|
1582
|
-
} else {
|
|
1583
|
-
[[logLine6 stringByAppendingString:@"\n"] writeToFile:logPath6 atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
// #endregion
|
|
1587
1368
|
}
|
|
1588
1369
|
}
|
|
1589
1370
|
|
|
@@ -1604,38 +1385,6 @@ using namespace facebook::react;
|
|
|
1604
1385
|
scrollView.bounds.size.width,
|
|
1605
1386
|
scrollView.bounds.size.height,
|
|
1606
1387
|
scrollView.scrollEnabled);
|
|
1607
|
-
|
|
1608
|
-
// #region agent log
|
|
1609
|
-
{
|
|
1610
|
-
NSString *logPath2 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
|
|
1611
|
-
NSDictionary *logEntry2 = @{
|
|
1612
|
-
@"sessionId": @"debug-session",
|
|
1613
|
-
@"runId": @"init",
|
|
1614
|
-
@"hypothesisId": @"B",
|
|
1615
|
-
@"location": @"RNPDFPdfView.mm:1300",
|
|
1616
|
-
@"message": @"scrollViewDidScroll called",
|
|
1617
|
-
@"data": @{
|
|
1618
|
-
@"eventCount": @(scrollEventCount),
|
|
1619
|
-
@"contentOffset": @{@"x": @(scrollView.contentOffset.x), @"y": @(scrollView.contentOffset.y)},
|
|
1620
|
-
@"contentSize": @{@"width": @(scrollView.contentSize.width), @"height": @(scrollView.contentSize.height)},
|
|
1621
|
-
@"bounds": @{@"width": @(scrollView.bounds.size.width), @"height": @(scrollView.bounds.size.height)},
|
|
1622
|
-
@"scrollEnabled": @(scrollView.scrollEnabled),
|
|
1623
|
-
@"alwaysBounceHorizontal": @(scrollView.alwaysBounceHorizontal)
|
|
1624
|
-
},
|
|
1625
|
-
@"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
|
|
1626
|
-
};
|
|
1627
|
-
NSData *logData2 = [NSJSONSerialization dataWithJSONObject:logEntry2 options:0 error:nil];
|
|
1628
|
-
NSString *logLine2 = [[NSString alloc] initWithData:logData2 encoding:NSUTF8StringEncoding];
|
|
1629
|
-
NSFileHandle *fileHandle2 = [NSFileHandle fileHandleForWritingAtPath:logPath2];
|
|
1630
|
-
if (fileHandle2) {
|
|
1631
|
-
[fileHandle2 seekToEndOfFile];
|
|
1632
|
-
[fileHandle2 writeData:[[logLine2 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
1633
|
-
[fileHandle2 closeFile];
|
|
1634
|
-
} else {
|
|
1635
|
-
[[logLine2 stringByAppendingString:@"\n"] writeToFile:logPath2 atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
// #endregion
|
|
1639
1388
|
}
|
|
1640
1389
|
|
|
1641
1390
|
if (!_pdfDocument || _singlePage) {
|
|
@@ -1664,36 +1413,6 @@ using namespace facebook::react;
|
|
|
1664
1413
|
// Only update if page actually changed and is valid
|
|
1665
1414
|
if (newPage != _page && newPage > 0 && newPage <= (int)_pdfDocument.pageCount) {
|
|
1666
1415
|
RCTLogInfo(@"📄 [iOS Scroll] Page changed: %d -> %d (from scroll position)", _page, newPage);
|
|
1667
|
-
// #region agent log
|
|
1668
|
-
{
|
|
1669
|
-
NSString *logPath12 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
|
|
1670
|
-
NSDictionary *logEntry12 = @{
|
|
1671
|
-
@"sessionId": @"debug-session",
|
|
1672
|
-
@"runId": @"init",
|
|
1673
|
-
@"hypothesisId": @"A,C,D",
|
|
1674
|
-
@"location": @"RNPDFPdfView.mm:1558",
|
|
1675
|
-
@"message": @"scrollViewDidScroll detected page change - BEFORE updating _page",
|
|
1676
|
-
@"data": @{
|
|
1677
|
-
@"oldPage": @(_page),
|
|
1678
|
-
@"newPage": @(newPage),
|
|
1679
|
-
@"previousPage": @(_previousPage),
|
|
1680
|
-
@"contentOffset": @{@"x": @(scrollView.contentOffset.x), @"y": @(scrollView.contentOffset.y)},
|
|
1681
|
-
@"isNavigating": @(_isNavigating)
|
|
1682
|
-
},
|
|
1683
|
-
@"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
|
|
1684
|
-
};
|
|
1685
|
-
NSData *logData12 = [NSJSONSerialization dataWithJSONObject:logEntry12 options:0 error:nil];
|
|
1686
|
-
NSString *logLine12 = [[NSString alloc] initWithData:logData12 encoding:NSUTF8StringEncoding];
|
|
1687
|
-
NSFileHandle *fileHandle12 = [NSFileHandle fileHandleForWritingAtPath:logPath12];
|
|
1688
|
-
if (fileHandle12) {
|
|
1689
|
-
[fileHandle12 seekToEndOfFile];
|
|
1690
|
-
[fileHandle12 writeData:[[logLine12 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
1691
|
-
[fileHandle12 closeFile];
|
|
1692
|
-
} else {
|
|
1693
|
-
[[logLine12 stringByAppendingString:@"\n"] writeToFile:logPath12 atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
// #endregion
|
|
1697
1416
|
|
|
1698
1417
|
// CRITICAL FIX: Update _previousPage to the new page value when page changes from user scrolling
|
|
1699
1418
|
// This prevents updateProps from triggering programmatic navigation when React Native
|
|
@@ -1712,34 +1431,6 @@ using namespace facebook::react;
|
|
|
1712
1431
|
|
|
1713
1432
|
// Notify about page change
|
|
1714
1433
|
[self notifyOnChangeWithMessage:[[NSString alloc] initWithString:[NSString stringWithFormat:@"pageChanged|%d|%lu", newPage, _pdfDocument.pageCount]]];
|
|
1715
|
-
// #region agent log
|
|
1716
|
-
{
|
|
1717
|
-
NSString *logPath13 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
|
|
1718
|
-
NSDictionary *logEntry13 = @{
|
|
1719
|
-
@"sessionId": @"debug-session",
|
|
1720
|
-
@"runId": @"init",
|
|
1721
|
-
@"hypothesisId": @"A,C,D",
|
|
1722
|
-
@"location": @"RNPDFPdfView.mm:1570",
|
|
1723
|
-
@"message": @"scrollViewDidScroll detected page change - AFTER updating _page and _previousPage (to prevent navigation loop)",
|
|
1724
|
-
@"data": @{
|
|
1725
|
-
@"_page": @(_page),
|
|
1726
|
-
@"_previousPage": @(_previousPage),
|
|
1727
|
-
@"notificationSent": @YES
|
|
1728
|
-
},
|
|
1729
|
-
@"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
|
|
1730
|
-
};
|
|
1731
|
-
NSData *logData13 = [NSJSONSerialization dataWithJSONObject:logEntry13 options:0 error:nil];
|
|
1732
|
-
NSString *logLine13 = [[NSString alloc] initWithData:logData13 encoding:NSUTF8StringEncoding];
|
|
1733
|
-
NSFileHandle *fileHandle13 = [NSFileHandle fileHandleForWritingAtPath:logPath13];
|
|
1734
|
-
if (fileHandle13) {
|
|
1735
|
-
[fileHandle13 seekToEndOfFile];
|
|
1736
|
-
[fileHandle13 writeData:[[logLine13 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
1737
|
-
[fileHandle13 closeFile];
|
|
1738
|
-
} else {
|
|
1739
|
-
[[logLine13 stringByAppendingString:@"\n"] writeToFile:logPath13 atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
// #endregion
|
|
1743
1434
|
}
|
|
1744
1435
|
} else {
|
|
1745
1436
|
if (scrollEventCount % 50 == 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-pdf-jsi",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.2",
|
|
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,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDFCompressor - PDF Compression Manager
|
|
3
|
+
* Handles PDF file compression with streaming support for large files
|
|
4
|
+
*
|
|
5
|
+
* Uses native StreamingPDFProcessor for O(1) memory operations
|
|
6
|
+
* Can compress 1GB+ PDFs without memory issues
|
|
7
|
+
*
|
|
8
|
+
* @author Punith M
|
|
9
|
+
* @version 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { NativeModules, Platform } from 'react-native';
|
|
13
|
+
import ReactNativeBlobUtil from 'react-native-blob-util';
|
|
14
|
+
|
|
15
|
+
const { PDFExporter, StreamingPDFProcessor } = NativeModules;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compression presets for different use cases
|
|
19
|
+
*/
|
|
20
|
+
export const CompressionPreset = {
|
|
21
|
+
/** Optimized for email attachments (high compression, smaller file) */
|
|
22
|
+
EMAIL: 'email',
|
|
23
|
+
/** Optimized for web viewing (balanced compression) */
|
|
24
|
+
WEB: 'web',
|
|
25
|
+
/** Optimized for mobile devices (good compression, fast decompression) */
|
|
26
|
+
MOBILE: 'mobile',
|
|
27
|
+
/** Optimized for printing (low compression, high quality) */
|
|
28
|
+
PRINT: 'print',
|
|
29
|
+
/** Optimized for long-term archival (maximum compression) */
|
|
30
|
+
ARCHIVE: 'archive',
|
|
31
|
+
/** Custom compression settings */
|
|
32
|
+
CUSTOM: 'custom'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compression levels (0-9, higher = more compression but slower)
|
|
37
|
+
*/
|
|
38
|
+
export const CompressionLevel = {
|
|
39
|
+
NONE: 0,
|
|
40
|
+
FASTEST: 1,
|
|
41
|
+
FAST: 3,
|
|
42
|
+
BALANCED: 5,
|
|
43
|
+
DEFAULT: 6,
|
|
44
|
+
GOOD: 7,
|
|
45
|
+
BETTER: 8,
|
|
46
|
+
BEST: 9
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Preset configurations
|
|
51
|
+
*/
|
|
52
|
+
const PRESET_CONFIGS = {
|
|
53
|
+
[CompressionPreset.EMAIL]: {
|
|
54
|
+
level: CompressionLevel.BEST,
|
|
55
|
+
targetSizeKB: 10000, // 10MB target
|
|
56
|
+
description: 'Maximum compression for email attachments'
|
|
57
|
+
},
|
|
58
|
+
[CompressionPreset.WEB]: {
|
|
59
|
+
level: CompressionLevel.GOOD,
|
|
60
|
+
targetSizeKB: 50000, // 50MB target
|
|
61
|
+
description: 'Balanced compression for web delivery'
|
|
62
|
+
},
|
|
63
|
+
[CompressionPreset.MOBILE]: {
|
|
64
|
+
level: CompressionLevel.BALANCED,
|
|
65
|
+
targetSizeKB: 25000, // 25MB target
|
|
66
|
+
description: 'Optimized for mobile viewing'
|
|
67
|
+
},
|
|
68
|
+
[CompressionPreset.PRINT]: {
|
|
69
|
+
level: CompressionLevel.FAST,
|
|
70
|
+
targetSizeKB: null, // No size limit
|
|
71
|
+
description: 'Low compression for print quality'
|
|
72
|
+
},
|
|
73
|
+
[CompressionPreset.ARCHIVE]: {
|
|
74
|
+
level: CompressionLevel.BEST,
|
|
75
|
+
targetSizeKB: null, // No size limit, just maximum compression
|
|
76
|
+
description: 'Maximum compression for archival'
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* PDFCompressor Class
|
|
82
|
+
* Provides PDF compression functionality with streaming support
|
|
83
|
+
*/
|
|
84
|
+
export class PDFCompressor {
|
|
85
|
+
constructor() {
|
|
86
|
+
this.isNativeAvailable = this._checkNativeAvailability();
|
|
87
|
+
|
|
88
|
+
if (this.isNativeAvailable) {
|
|
89
|
+
console.log('📦 PDFCompressor: Native streaming compression available');
|
|
90
|
+
} else {
|
|
91
|
+
console.warn('📦 PDFCompressor: Native module not available - compression may be limited');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if native compression module is available
|
|
97
|
+
* @private
|
|
98
|
+
* @returns {boolean} True if native module is available
|
|
99
|
+
*/
|
|
100
|
+
_checkNativeAvailability() {
|
|
101
|
+
// Check if PDFExporter module exists and has the compressPDF method
|
|
102
|
+
const hasCompressPDF = PDFExporter && typeof PDFExporter.compressPDF === 'function';
|
|
103
|
+
console.log('📦 PDFCompressor: Checking native availability...');
|
|
104
|
+
console.log('📦 PDFCompressor: PDFExporter exists:', !!PDFExporter);
|
|
105
|
+
console.log('📦 PDFCompressor: PDFExporter.compressPDF exists:', hasCompressPDF);
|
|
106
|
+
return hasCompressPDF;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if compression is available
|
|
111
|
+
* @returns {boolean} True if compression functionality is available
|
|
112
|
+
*/
|
|
113
|
+
isAvailable() {
|
|
114
|
+
return this.isNativeAvailable;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get compression capabilities
|
|
119
|
+
* @returns {Object} Capabilities object
|
|
120
|
+
*/
|
|
121
|
+
getCapabilities() {
|
|
122
|
+
return {
|
|
123
|
+
streamingCompression: this.isNativeAvailable,
|
|
124
|
+
presets: Object.values(CompressionPreset),
|
|
125
|
+
maxFileSizeMB: this.isNativeAvailable ? 1024 : 100, // 1GB+ with native, 100MB without
|
|
126
|
+
supportedPlatforms: ['ios', 'android'],
|
|
127
|
+
currentPlatform: Platform.OS,
|
|
128
|
+
nativeModuleAvailable: this.isNativeAvailable
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get preset configuration
|
|
134
|
+
* @param {string} preset - Preset name from CompressionPreset
|
|
135
|
+
* @returns {Object} Preset configuration
|
|
136
|
+
*/
|
|
137
|
+
getPresetConfig(preset) {
|
|
138
|
+
return PRESET_CONFIGS[preset] || PRESET_CONFIGS[CompressionPreset.WEB];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Compress a PDF file
|
|
143
|
+
* @param {string} inputPath - Path to input PDF file
|
|
144
|
+
* @param {Object} options - Compression options
|
|
145
|
+
* @param {string} options.preset - Compression preset (from CompressionPreset)
|
|
146
|
+
* @param {number} options.level - Compression level (0-9), overrides preset
|
|
147
|
+
* @param {string} options.outputPath - Output file path (optional, auto-generated if not provided)
|
|
148
|
+
* @param {Function} options.onProgress - Progress callback function
|
|
149
|
+
* @returns {Promise<Object>} Compression result
|
|
150
|
+
*/
|
|
151
|
+
async compress(inputPath, options = {}) {
|
|
152
|
+
const {
|
|
153
|
+
preset = CompressionPreset.WEB,
|
|
154
|
+
level = null,
|
|
155
|
+
outputPath = null,
|
|
156
|
+
onProgress = null
|
|
157
|
+
} = options;
|
|
158
|
+
|
|
159
|
+
console.log(`📦 PDFCompressor: Starting compression with preset '${preset}'`);
|
|
160
|
+
|
|
161
|
+
// Validate input file exists
|
|
162
|
+
const fileExists = await this._fileExists(inputPath);
|
|
163
|
+
if (!fileExists) {
|
|
164
|
+
throw new Error(`Input file not found: ${inputPath}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Get file size
|
|
168
|
+
const inputStats = await this._getFileStats(inputPath);
|
|
169
|
+
const inputSizeMB = inputStats.size / (1024 * 1024);
|
|
170
|
+
|
|
171
|
+
console.log(`📦 PDFCompressor: Input file size: ${inputSizeMB.toFixed(2)} MB`);
|
|
172
|
+
|
|
173
|
+
// Determine compression level
|
|
174
|
+
const presetConfig = this.getPresetConfig(preset);
|
|
175
|
+
const compressionLevel = level !== null ? level : presetConfig.level;
|
|
176
|
+
|
|
177
|
+
// Generate output path if not provided
|
|
178
|
+
const finalOutputPath = outputPath || this._generateOutputPath(inputPath);
|
|
179
|
+
|
|
180
|
+
// Perform compression
|
|
181
|
+
const startTime = Date.now();
|
|
182
|
+
let result;
|
|
183
|
+
|
|
184
|
+
if (this.isNativeAvailable) {
|
|
185
|
+
result = await this._compressNative(inputPath, finalOutputPath, compressionLevel, onProgress);
|
|
186
|
+
} else {
|
|
187
|
+
result = await this._compressFallback(inputPath, finalOutputPath, compressionLevel, onProgress);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const duration = Date.now() - startTime;
|
|
191
|
+
|
|
192
|
+
// Get output file stats
|
|
193
|
+
const outputStats = await this._getFileStats(finalOutputPath);
|
|
194
|
+
const outputSizeMB = outputStats.size / (1024 * 1024);
|
|
195
|
+
const compressionRatio = inputStats.size > 0 ? outputStats.size / inputStats.size : 1;
|
|
196
|
+
const spaceSavedPercent = (1 - compressionRatio) * 100;
|
|
197
|
+
const throughputMBps = duration > 0 ? (inputSizeMB / (duration / 1000)) : 0;
|
|
198
|
+
|
|
199
|
+
const compressionResult = {
|
|
200
|
+
success: true,
|
|
201
|
+
inputPath,
|
|
202
|
+
outputPath: finalOutputPath,
|
|
203
|
+
originalSize: inputStats.size,
|
|
204
|
+
compressedSize: outputStats.size,
|
|
205
|
+
originalSizeMB: inputSizeMB,
|
|
206
|
+
compressedSizeMB: outputSizeMB,
|
|
207
|
+
compressionRatio,
|
|
208
|
+
spaceSavedPercent,
|
|
209
|
+
durationMs: duration,
|
|
210
|
+
throughputMBps,
|
|
211
|
+
preset,
|
|
212
|
+
compressionLevel,
|
|
213
|
+
method: this.isNativeAvailable ? 'native_streaming' : 'fallback'
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
console.log(`📦 PDFCompressor: Compression complete!`);
|
|
217
|
+
console.log(` 📊 ${inputSizeMB.toFixed(2)} MB → ${outputSizeMB.toFixed(2)} MB (${spaceSavedPercent.toFixed(1)}% saved)`);
|
|
218
|
+
console.log(` ⏱️ ${duration}ms (${throughputMBps.toFixed(1)} MB/s)`);
|
|
219
|
+
|
|
220
|
+
return compressionResult;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Compress PDF with a specific preset
|
|
225
|
+
* @param {string} inputPath - Path to input PDF file
|
|
226
|
+
* @param {string} preset - Compression preset
|
|
227
|
+
* @param {string} outputPath - Output file path (optional)
|
|
228
|
+
* @returns {Promise<Object>} Compression result
|
|
229
|
+
*/
|
|
230
|
+
async compressWithPreset(inputPath, preset, outputPath = null) {
|
|
231
|
+
return this.compress(inputPath, { preset, outputPath });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Compress PDF for email (maximum compression)
|
|
236
|
+
* @param {string} inputPath - Path to input PDF file
|
|
237
|
+
* @param {string} outputPath - Output file path (optional)
|
|
238
|
+
* @returns {Promise<Object>} Compression result
|
|
239
|
+
*/
|
|
240
|
+
async compressForEmail(inputPath, outputPath = null) {
|
|
241
|
+
return this.compress(inputPath, {
|
|
242
|
+
preset: CompressionPreset.EMAIL,
|
|
243
|
+
outputPath
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Compress PDF for web viewing
|
|
249
|
+
* @param {string} inputPath - Path to input PDF file
|
|
250
|
+
* @param {string} outputPath - Output file path (optional)
|
|
251
|
+
* @returns {Promise<Object>} Compression result
|
|
252
|
+
*/
|
|
253
|
+
async compressForWeb(inputPath, outputPath = null) {
|
|
254
|
+
return this.compress(inputPath, {
|
|
255
|
+
preset: CompressionPreset.WEB,
|
|
256
|
+
outputPath
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Compress PDF for mobile viewing
|
|
262
|
+
* @param {string} inputPath - Path to input PDF file
|
|
263
|
+
* @param {string} outputPath - Output file path (optional)
|
|
264
|
+
* @returns {Promise<Object>} Compression result
|
|
265
|
+
*/
|
|
266
|
+
async compressForMobile(inputPath, outputPath = null) {
|
|
267
|
+
return this.compress(inputPath, {
|
|
268
|
+
preset: CompressionPreset.MOBILE,
|
|
269
|
+
outputPath
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Compress PDF for archival (maximum compression)
|
|
275
|
+
* @param {string} inputPath - Path to input PDF file
|
|
276
|
+
* @param {string} outputPath - Output file path (optional)
|
|
277
|
+
* @returns {Promise<Object>} Compression result
|
|
278
|
+
*/
|
|
279
|
+
async compressForArchive(inputPath, outputPath = null) {
|
|
280
|
+
return this.compress(inputPath, {
|
|
281
|
+
preset: CompressionPreset.ARCHIVE,
|
|
282
|
+
outputPath
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Estimate compression result without actually compressing
|
|
288
|
+
* @param {string} inputPath - Path to input PDF file
|
|
289
|
+
* @param {string} preset - Compression preset
|
|
290
|
+
* @returns {Promise<Object>} Estimated compression result
|
|
291
|
+
*/
|
|
292
|
+
async estimateCompression(inputPath, preset = CompressionPreset.WEB) {
|
|
293
|
+
const fileExists = await this._fileExists(inputPath);
|
|
294
|
+
if (!fileExists) {
|
|
295
|
+
throw new Error(`Input file not found: ${inputPath}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const inputStats = await this._getFileStats(inputPath);
|
|
299
|
+
const inputSizeMB = inputStats.size / (1024 * 1024);
|
|
300
|
+
const presetConfig = this.getPresetConfig(preset);
|
|
301
|
+
|
|
302
|
+
// IMPORTANT: Native module uses zlib deflate which produces ~15-18% compression
|
|
303
|
+
// on PDFs regardless of compression level, because PDFs already contain
|
|
304
|
+
// compressed content (JPEG images, embedded fonts, compressed streams).
|
|
305
|
+
// All presets produce approximately the same result.
|
|
306
|
+
const estimatedRatio = 0.84; // ~16% reduction - same for all presets
|
|
307
|
+
const estimatedSize = inputStats.size * estimatedRatio;
|
|
308
|
+
const estimatedSizeMB = estimatedSize / (1024 * 1024);
|
|
309
|
+
const estimatedSavingsPercent = (1 - estimatedRatio) * 100;
|
|
310
|
+
|
|
311
|
+
// Estimate time based on file size (actual ~25ms/MB with native streaming)
|
|
312
|
+
const msPerMB = this.isNativeAvailable ? 25 : 100;
|
|
313
|
+
const estimatedTimeMs = inputSizeMB * msPerMB;
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
inputPath,
|
|
317
|
+
originalSize: inputStats.size,
|
|
318
|
+
originalSizeMB: inputSizeMB,
|
|
319
|
+
estimatedCompressedSize: estimatedSize,
|
|
320
|
+
estimatedCompressedSizeMB: estimatedSizeMB,
|
|
321
|
+
estimatedCompressionRatio: estimatedRatio,
|
|
322
|
+
estimatedSavingsPercent,
|
|
323
|
+
estimatedDurationMs: estimatedTimeMs,
|
|
324
|
+
preset,
|
|
325
|
+
presetDescription: presetConfig.description,
|
|
326
|
+
confidence: 'low',
|
|
327
|
+
note: 'All presets produce ~15-18% compression. Native uses zlib deflate which has minimal effect on already-compressed PDF content.'
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Native compression using PDFExporter.compressPDF
|
|
333
|
+
* @private
|
|
334
|
+
*/
|
|
335
|
+
async _compressNative(inputPath, outputPath, compressionLevel, onProgress) {
|
|
336
|
+
console.log(`📦 PDFCompressor: Using native streaming compression (level: ${compressionLevel})`);
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
// Use PDFExporter.compressPDF - the main native compression method
|
|
340
|
+
if (PDFExporter && typeof PDFExporter.compressPDF === 'function') {
|
|
341
|
+
console.log('📦 PDFCompressor: Calling PDFExporter.compressPDF...');
|
|
342
|
+
const result = await PDFExporter.compressPDF(
|
|
343
|
+
inputPath,
|
|
344
|
+
outputPath,
|
|
345
|
+
compressionLevel
|
|
346
|
+
);
|
|
347
|
+
console.log('📦 PDFCompressor: Native compression result:', result);
|
|
348
|
+
return result;
|
|
349
|
+
} else {
|
|
350
|
+
// Native module not available - use fallback
|
|
351
|
+
console.warn('📦 PDFCompressor: PDFExporter.compressPDF not available, using fallback');
|
|
352
|
+
console.warn('📦 PDFCompressor: PDFExporter available:', !!PDFExporter);
|
|
353
|
+
console.warn('📦 PDFCompressor: PDFExporter.compressPDF:', PDFExporter ? typeof PDFExporter.compressPDF : 'N/A');
|
|
354
|
+
return this._compressFallback(inputPath, outputPath, compressionLevel, onProgress);
|
|
355
|
+
}
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error('📦 PDFCompressor: Native compression failed:', error);
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Fallback compression (file copy with potential optimization)
|
|
364
|
+
* @private
|
|
365
|
+
*/
|
|
366
|
+
async _compressFallback(inputPath, outputPath, compressionLevel, onProgress) {
|
|
367
|
+
console.log('📦 PDFCompressor: Using fallback compression (limited functionality)');
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
// For fallback, we simply copy the file
|
|
371
|
+
// Real compression requires native code
|
|
372
|
+
await ReactNativeBlobUtil.fs.cp(inputPath, outputPath);
|
|
373
|
+
|
|
374
|
+
if (onProgress) {
|
|
375
|
+
onProgress({
|
|
376
|
+
progress: 1.0,
|
|
377
|
+
bytesProcessed: 0,
|
|
378
|
+
totalBytes: 0
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.warn('📦 PDFCompressor: Fallback mode - file copied without compression');
|
|
383
|
+
console.warn('📦 PDFCompressor: For actual compression, ensure native module is properly linked');
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
success: true,
|
|
387
|
+
method: 'fallback_copy',
|
|
388
|
+
note: 'Native compression not available - file was copied without compression'
|
|
389
|
+
};
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.error('📦 PDFCompressor: Fallback compression failed:', error);
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Check if file exists
|
|
398
|
+
* @private
|
|
399
|
+
*/
|
|
400
|
+
async _fileExists(filePath) {
|
|
401
|
+
try {
|
|
402
|
+
return await ReactNativeBlobUtil.fs.exists(filePath);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get file statistics
|
|
410
|
+
* @private
|
|
411
|
+
*/
|
|
412
|
+
async _getFileStats(filePath) {
|
|
413
|
+
try {
|
|
414
|
+
const stats = await ReactNativeBlobUtil.fs.stat(filePath);
|
|
415
|
+
return {
|
|
416
|
+
size: parseInt(stats.size, 10),
|
|
417
|
+
lastModified: stats.lastModified,
|
|
418
|
+
path: stats.path
|
|
419
|
+
};
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.error('📦 PDFCompressor: Failed to get file stats:', error);
|
|
422
|
+
return { size: 0, lastModified: 0, path: filePath };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Generate output path based on input path
|
|
428
|
+
* @private
|
|
429
|
+
*/
|
|
430
|
+
_generateOutputPath(inputPath) {
|
|
431
|
+
const timestamp = Date.now();
|
|
432
|
+
const baseName = inputPath.replace(/\.pdf$/i, '');
|
|
433
|
+
return `${baseName}_compressed_${timestamp}.pdf`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Delete a compressed file
|
|
438
|
+
* @param {string} filePath - Path to file to delete
|
|
439
|
+
* @returns {Promise<boolean>} Success status
|
|
440
|
+
*/
|
|
441
|
+
async deleteCompressedFile(filePath) {
|
|
442
|
+
try {
|
|
443
|
+
const exists = await this._fileExists(filePath);
|
|
444
|
+
if (exists) {
|
|
445
|
+
await ReactNativeBlobUtil.fs.unlink(filePath);
|
|
446
|
+
console.log(`📦 PDFCompressor: Deleted file: ${filePath}`);
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
return false;
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.error('📦 PDFCompressor: Failed to delete file:', error);
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Get module information
|
|
458
|
+
* @returns {Object} Module info
|
|
459
|
+
*/
|
|
460
|
+
getModuleInfo() {
|
|
461
|
+
return {
|
|
462
|
+
name: 'PDFCompressor',
|
|
463
|
+
version: '1.0.0',
|
|
464
|
+
platform: Platform.OS,
|
|
465
|
+
nativeAvailable: this.isNativeAvailable,
|
|
466
|
+
presets: Object.keys(CompressionPreset),
|
|
467
|
+
compressionLevels: Object.keys(CompressionLevel),
|
|
468
|
+
capabilities: this.getCapabilities()
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Create singleton instance
|
|
474
|
+
const pdfCompressor = new PDFCompressor();
|
|
475
|
+
|
|
476
|
+
export default pdfCompressor;
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
// Core JSI functionality
|
|
10
10
|
export { default as PDFJSI } from './PDFJSI';
|
|
11
11
|
|
|
12
|
+
// PDF Compression
|
|
13
|
+
export {
|
|
14
|
+
default as PDFCompressor,
|
|
15
|
+
PDFCompressor as PDFCompressorClass,
|
|
16
|
+
CompressionPreset,
|
|
17
|
+
CompressionLevel
|
|
18
|
+
} from './PDFCompressor';
|
|
19
|
+
|
|
12
20
|
// Enhanced PDF View component
|
|
13
21
|
export { default as EnhancedPdfView, EnhancedPdfUtils } from './EnhancedPdfView';
|
|
14
22
|
|
|
@@ -29,4 +37,10 @@ export {
|
|
|
29
37
|
getJSIStats,
|
|
30
38
|
getPerformanceHistory,
|
|
31
39
|
clearPerformanceHistory
|
|
32
|
-
} from './PDFJSI';
|
|
40
|
+
} from './PDFJSI';
|
|
41
|
+
|
|
42
|
+
// Re-export compression utilities
|
|
43
|
+
export {
|
|
44
|
+
CompressionPreset,
|
|
45
|
+
CompressionLevel
|
|
46
|
+
} from './PDFCompressor';
|