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 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.0",
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';