react-native-pdf-jsi 4.2.1 → 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
@@ -409,6 +409,19 @@ MIT License - see [LICENSE](LICENSE) file for details.
409
409
  - **Author**: Punith M ([@126punith](https://github.com/126punith))
410
410
 
411
411
  ## Recent Fixes
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
+
412
425
  ### iOS Performance - Unnecessary Path Handlers (v4.2.1)
413
426
  Use v4.2.1 it contains stable fixes for IOS with unwanted debug logs removed
414
427
 
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-pdf-jsi",
3
- "version": "4.2.1",
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';