react-native-pdf-jsi 3.4.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +32 -1
  2. package/android/.gradle/5.6.1/fileChanges/last-build.bin +0 -0
  3. package/android/.gradle/5.6.1/fileHashes/fileHashes.lock +0 -0
  4. package/android/.gradle/5.6.1/gc.properties +0 -0
  5. package/android/.gradle/8.5/checksums/checksums.lock +0 -0
  6. package/android/.gradle/8.5/checksums/md5-checksums.bin +0 -0
  7. package/android/.gradle/8.5/checksums/sha1-checksums.bin +0 -0
  8. package/android/.gradle/8.5/dependencies-accessors/dependencies-accessors.lock +0 -0
  9. package/android/.gradle/8.5/dependencies-accessors/gc.properties +0 -0
  10. package/android/.gradle/8.5/executionHistory/executionHistory.lock +0 -0
  11. package/android/.gradle/8.5/fileChanges/last-build.bin +0 -0
  12. package/android/.gradle/8.5/fileHashes/fileHashes.lock +0 -0
  13. package/android/.gradle/8.5/gc.properties +0 -0
  14. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  15. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  16. package/android/.gradle/vcs-1/gc.properties +0 -0
  17. package/android/src/main/java/org/wonday/pdf/PdfManager.java +5 -0
  18. package/fabric/RNPDFPdfNativeComponent.js +4 -3
  19. package/index.d.ts +24 -2
  20. package/ios/PERMISSIONS.md +106 -0
  21. package/ios/RNPDFPdf/FileDownloader.h +15 -0
  22. package/ios/RNPDFPdf/FileDownloader.m +567 -0
  23. package/ios/RNPDFPdf/FileManager.h +12 -0
  24. package/ios/RNPDFPdf/FileManager.m +201 -0
  25. package/ios/RNPDFPdf/ImagePool.h +61 -0
  26. package/ios/RNPDFPdf/ImagePool.m +162 -0
  27. package/ios/RNPDFPdf/LazyMetadataLoader.h +78 -0
  28. package/ios/RNPDFPdf/LazyMetadataLoader.m +184 -0
  29. package/ios/RNPDFPdf/MemoryMappedCache.h +71 -0
  30. package/ios/RNPDFPdf/MemoryMappedCache.m +264 -0
  31. package/ios/RNPDFPdf/PDFExporter.h +1 -1
  32. package/ios/RNPDFPdf/PDFExporter.m +475 -19
  33. package/ios/RNPDFPdf/PDFNativeCacheManager.h +11 -1
  34. package/ios/RNPDFPdf/PDFNativeCacheManager.m +283 -19
  35. package/ios/RNPDFPdf/RNPDFPdfView.h +19 -1
  36. package/ios/RNPDFPdf/RNPDFPdfView.mm +154 -44
  37. package/ios/RNPDFPdf/StreamingPDFProcessor.h +86 -0
  38. package/ios/RNPDFPdf/StreamingPDFProcessor.m +314 -0
  39. package/package.json +5 -3
  40. package/src/managers/ExportManager.js +9 -3
@@ -1,19 +1,53 @@
1
1
  #import "PDFExporter.h"
2
+ #import "ImagePool.h"
2
3
  #import <PDFKit/PDFKit.h>
3
4
  #import <UIKit/UIKit.h>
4
5
 
5
- @implementation PDFExporter
6
+ @implementation PDFExporter {
7
+ ImagePool *_imagePool;
8
+ }
6
9
 
7
10
  RCT_EXPORT_MODULE();
8
11
 
12
+ + (BOOL)requiresMainQueueSetup {
13
+ return NO;
14
+ }
15
+
9
16
  - (instancetype)init {
10
17
  self = [super init];
11
18
  if (self) {
12
19
  // All features are FREE - no license verification needed
20
+ _imagePool = [ImagePool sharedInstance];
13
21
  }
14
22
  return self;
15
23
  }
16
24
 
25
+ - (NSArray<NSString *> *)supportedEvents {
26
+ return @[@"PDFExportProgress", @"PDFExportComplete", @"PDFOperationProgress", @"PDFOperationComplete"];
27
+ }
28
+
29
+ /**
30
+ * Generate a unique filename with timestamp to prevent overwrites
31
+ * @param baseName Base filename without extension
32
+ * @param pageNum Page number (or -1 for PDF operations)
33
+ * @param extension File extension (png, jpeg, pdf)
34
+ * @return Filename with timestamp (e.g., "filename_page_1_20251102_181059.png")
35
+ */
36
+ - (NSString *)generateTimestampedFileName:(NSString *)baseName pageNum:(int)pageNum extension:(NSString *)extension {
37
+ NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
38
+ [formatter setDateFormat:@"yyyyMMdd_HHmmss"];
39
+ [formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
40
+ NSString *timestamp = [formatter stringFromDate:[NSDate date]];
41
+
42
+ if (pageNum >= 0) {
43
+ // For image exports: filename_page_1_20251102_181059.png
44
+ return [NSString stringWithFormat:@"%@_page_%d_%@.%@", baseName, pageNum, timestamp, extension];
45
+ } else {
46
+ // For PDF operations: filename_20251102_181059.pdf
47
+ return [NSString stringWithFormat:@"%@_%@.%@", baseName, timestamp, extension];
48
+ }
49
+ }
50
+
17
51
  //
18
52
  RCT_EXPORT_METHOD(exportToImages:(NSString *)filePath
19
53
  options:(NSDictionary *)options
@@ -33,18 +67,158 @@ RCT_EXPORT_METHOD(exportToImages:(NSString *)filePath
33
67
  return;
34
68
  }
35
69
 
36
-
37
70
  NSArray *pages = options[@"pages"];
38
71
  NSNumber *dpi = options[@"dpi"] ?: @150;
39
72
  NSString *format = options[@"format"] ?: @"png";
40
73
  NSString *outputDir = options[@"outputDir"];
41
74
 
75
+ // Emit start event
76
+ @try {
77
+ [self sendEventWithName:@"PDFExportProgress" body:@{
78
+ @"type": @"exportStart",
79
+ @"operation": @"exportToImages",
80
+ @"filePath": filePath,
81
+ @"totalPages": pages ? @(pages.count) : [NSNull null]
82
+ }];
83
+ } @catch (NSException *exception) {
84
+ // Event emitter not ready, continue without event
85
+ }
42
86
 
43
87
  NSArray *exportedFiles = [self exportPagesToImages:pdfURL pages:pages dpi:dpi.intValue format:format outputDir:outputDir];
44
88
 
89
+ // Still resolve promise for backward compatibility
45
90
  resolve(exportedFiles);
46
91
  }
47
92
 
93
+ /**
94
+ * Export single page to image
95
+ */
96
+ RCT_EXPORT_METHOD(exportPageToImage:(NSString *)filePath
97
+ pageIndex:(int)pageIndex
98
+ options:(NSDictionary *)options
99
+ resolver:(RCTPromiseResolveBlock)resolve
100
+ rejecter:(RCTPromiseRejectBlock)reject) {
101
+
102
+ @try {
103
+ NSString *format = options[@"format"] ?: @"jpeg";
104
+ NSNumber *qualityNum = options[@"quality"] ?: @0.9;
105
+ NSNumber *scaleNum = options[@"scale"] ?: @2.0;
106
+ double quality = qualityNum.doubleValue;
107
+ double scale = scaleNum.doubleValue;
108
+
109
+ NSLog(@"🖼️ [EXPORT] exportPageToImage - START - page: %d, format: %@, quality: %.2f, scale: %.2f",
110
+ pageIndex, format, quality, scale);
111
+
112
+ if (!filePath || filePath.length == 0) {
113
+ NSLog(@"❌ [EXPORT] exportPageToImage - FAILED - Invalid path");
114
+ reject(@"INVALID_PATH", @"File path is required", nil);
115
+ return;
116
+ }
117
+
118
+ NSURL *pdfURL = [NSURL fileURLWithPath:filePath];
119
+ NSLog(@"📁 [FILE] PDF path: %@", filePath);
120
+
121
+ if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
122
+ NSLog(@"❌ [FILE] PDF not found: %@", filePath);
123
+ reject(@"FILE_NOT_FOUND", @"PDF file not found", nil);
124
+ return;
125
+ }
126
+
127
+ NSDictionary *fileAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
128
+ unsigned long long fileSize = [fileAttrs fileSize];
129
+ NSLog(@"📁 [FILE] PDF exists, size: %llu bytes", fileSize);
130
+
131
+ int dpi = (int)(72 * scale);
132
+ NSLog(@"🖼️ [EXPORT] Calculated DPI: %d", dpi);
133
+
134
+ NSString *outputPath = [self exportSinglePageToImage:pdfURL pageIndex:pageIndex dpi:dpi format:format outputDir:nil];
135
+
136
+ NSLog(@"✅ [EXPORT] exportPageToImage - SUCCESS - output: %@", outputPath);
137
+ resolve(outputPath);
138
+
139
+ } @catch (NSException *exception) {
140
+ NSLog(@"❌ [EXPORT] exportPageToImage - ERROR: %@", exception.reason);
141
+ reject(@"EXPORT_ERROR", exception.reason, nil);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Export single page to image (internal method)
147
+ */
148
+ - (NSString *)exportSinglePageToImage:(NSURL *)pdfURL pageIndex:(int)pageIndex dpi:(int)dpi format:(NSString *)format outputDir:(NSString *)outputDir {
149
+ NSLog(@"🖼️ [EXPORT] exportSinglePageToImage - START - pageIndex: %d, dpi: %d, format: %@", pageIndex, dpi, format);
150
+
151
+ PDFDocument *pdfDocument = [[PDFDocument alloc] initWithURL:pdfURL];
152
+ if (!pdfDocument) {
153
+ NSLog(@"❌ [EXPORT] Failed to load PDF document");
154
+ return nil;
155
+ }
156
+
157
+ NSUInteger totalPages = pdfDocument.pageCount;
158
+ NSLog(@"📁 [FILE] PDF opened, total pages: %lu", (unsigned long)totalPages);
159
+
160
+ if (pageIndex < 0 || pageIndex >= totalPages) {
161
+ NSLog(@"❌ [EXPORT] Invalid page index: %d (total: %lu)", pageIndex, (unsigned long)totalPages);
162
+ return nil;
163
+ }
164
+
165
+ PDFPage *page = [pdfDocument pageAtIndex:pageIndex];
166
+ NSLog(@"📄 [PAGE] Opened page %d", pageIndex + 1);
167
+
168
+ CGRect pageRect = [page boundsForBox:kPDFDisplayBoxMediaBox];
169
+ float scale = dpi / 72.0f;
170
+ CGSize imageSize = CGSizeMake(pageRect.size.width * scale, pageRect.size.height * scale);
171
+
172
+ NSLog(@"🖼️ [BITMAP] Creating bitmap - width: %.0fpx, height: %.0fpx, dpi: %d", imageSize.width, imageSize.height, dpi);
173
+
174
+ // OPTIMIZATION: Use ImagePool for reduced allocations
175
+ CGFloat imageScale = [UIScreen mainScreen].scale;
176
+ UIImage *pooledImage = [_imagePool obtainImageWithSize:imageSize scale:imageScale];
177
+
178
+ // Create image context
179
+ UIGraphicsBeginImageContextWithOptions(imageSize, NO, imageScale);
180
+ CGContextRef context = UIGraphicsGetCurrentContext();
181
+
182
+ // Fill background with white
183
+ CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
184
+ CGContextFillRect(context, CGRectMake(0, 0, imageSize.width, imageSize.height));
185
+
186
+ // Scale context
187
+ CGContextScaleCTM(context, scale, scale);
188
+
189
+ // Render page
190
+ NSLog(@"🖼️ [RENDER] Rendering page to bitmap...");
191
+ [page drawWithBox:kPDFDisplayBoxMediaBox toContext:context];
192
+
193
+ // Get rendered image
194
+ UIImage *renderedImage = UIGraphicsGetImageFromCurrentImageContext();
195
+ UIGraphicsEndImageContext();
196
+
197
+ // Recycle pooled image
198
+ [_imagePool recycleImage:pooledImage];
199
+
200
+ NSLog(@"✅ [RENDER] Page rendered successfully");
201
+
202
+ if (!outputDir || outputDir.length == 0) {
203
+ NSArray *cachePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
204
+ outputDir = [cachePaths firstObject];
205
+ NSLog(@"📁 [FILE] Using cache dir: %@", outputDir);
206
+ }
207
+
208
+ // Generate unique filename with timestamp to prevent overwrites
209
+ NSString *baseName = [[pdfURL.lastPathComponent stringByDeletingPathExtension] stringByReplacingOccurrencesOfString:@" " withString:@"_"];
210
+ NSString *fileName = [self generateTimestampedFileName:baseName pageNum:pageIndex + 1 extension:format];
211
+ NSString *outputPath = [self saveImage:renderedImage fileName:fileName outputDir:outputDir];
212
+
213
+ if (outputPath) {
214
+ NSDictionary *fileAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath:outputPath error:nil];
215
+ unsigned long long fileSize = [fileAttrs fileSize];
216
+ NSLog(@"✅ [EXPORT] exportSinglePageToImage - SUCCESS - size: %llu bytes, path: %@", fileSize, outputPath);
217
+ }
218
+
219
+ return outputPath;
220
+ }
221
+
48
222
  /**
49
223
  * Export specific pages to images
50
224
  */
@@ -88,6 +262,21 @@ RCT_EXPORT_METHOD(exportToImages:(NSString *)filePath
88
262
 
89
263
  NSLog(@"📊 [PROGRESS] Exporting page %d/%lu (page number: %d)", current, (unsigned long)pagesToExport.count, pageIndex + 1);
90
264
 
265
+ // Emit progress event
266
+ @try {
267
+ double progress = (double)current / (double)pagesToExport.count;
268
+ [self sendEventWithName:@"PDFExportProgress" body:@{
269
+ @"type": @"exportProgress",
270
+ @"operation": @"exportToImages",
271
+ @"currentPage": @(pageIndex + 1),
272
+ @"totalPages": @(pagesToExport.count),
273
+ @"progress": @(progress),
274
+ @"exportedFiles": [exportedFiles copy]
275
+ }];
276
+ } @catch (NSException *exception) {
277
+ // Event emitter not ready, continue without event
278
+ }
279
+
91
280
  PDFPage *page = [pdfDocument pageAtIndex:pageIndex];
92
281
 
93
282
  if (page) {
@@ -98,8 +287,12 @@ RCT_EXPORT_METHOD(exportToImages:(NSString *)filePath
98
287
 
99
288
  NSLog(@"🖼️ [BITMAP] Creating %.0fx%.0f image", imageSize.width, imageSize.height);
100
289
 
101
- // Create image context
102
- UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0.0);
290
+ // OPTIMIZATION: Use ImagePool for reduced allocations
291
+ CGFloat imageScale = [UIScreen mainScreen].scale;
292
+ UIImage *image = [_imagePool obtainImageWithSize:imageSize scale:imageScale];
293
+
294
+ // Create image context with the pooled image
295
+ UIGraphicsBeginImageContextWithOptions(imageSize, NO, imageScale);
103
296
  CGContextRef context = UIGraphicsGetCurrentContext();
104
297
 
105
298
  // Fill background with white
@@ -114,12 +307,16 @@ RCT_EXPORT_METHOD(exportToImages:(NSString *)filePath
114
307
  [page drawWithBox:kPDFDisplayBoxMediaBox toContext:context];
115
308
 
116
309
  // Get image
117
- UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
310
+ UIImage *renderedImage = UIGraphicsGetImageFromCurrentImageContext();
118
311
  UIGraphicsEndImageContext();
119
312
 
120
- // Save image
121
- NSString *fileName = [NSString stringWithFormat:@"page_%d.%@", pageIndex + 1, format];
122
- NSString *outputPath = [self saveImage:image fileName:fileName outputDir:outputDir];
313
+ // Recycle the pooled image
314
+ [_imagePool recycleImage:image];
315
+
316
+ // Generate unique filename with timestamp to prevent overwrites
317
+ NSString *baseName = [[pdfURL.lastPathComponent stringByDeletingPathExtension] stringByReplacingOccurrencesOfString:@" " withString:@"_"];
318
+ NSString *fileName = [self generateTimestampedFileName:baseName pageNum:pageIndex + 1 extension:format];
319
+ NSString *outputPath = [self saveImage:renderedImage fileName:fileName outputDir:outputDir];
123
320
  if (outputPath) {
124
321
  [exportedFiles addObject:outputPath];
125
322
  NSLog(@"✅ [PROGRESS] Page %d exported to %@", pageIndex + 1, outputPath);
@@ -131,6 +328,19 @@ RCT_EXPORT_METHOD(exportToImages:(NSString *)filePath
131
328
 
132
329
  NSLog(@"✅ [EXPORT] exportPagesToImages - SUCCESS - Exported %lu pages", (unsigned long)exportedFiles.count);
133
330
 
331
+ // Emit complete event
332
+ @try {
333
+ [self sendEventWithName:@"PDFExportComplete" body:@{
334
+ @"type": @"exportComplete",
335
+ @"operation": @"exportToImages",
336
+ @"totalPages": @(exportedFiles.count),
337
+ @"exportedFiles": [exportedFiles copy],
338
+ @"success": @YES
339
+ }];
340
+ } @catch (NSException *exception) {
341
+ // Event emitter not ready, continue without event
342
+ }
343
+
134
344
  return exportedFiles;
135
345
  }
136
346
 
@@ -155,18 +365,22 @@ RCT_EXPORT_METHOD(exportToImages:(NSString *)filePath
155
365
 
156
366
  NSLog(@"📁 [FILE] Writing to: %@", outputURL.path);
157
367
 
158
- // Convert image to data
368
+ // OPTIMIZATION: Smart format selection for better performance
369
+ // JPEG: 5-6x faster than PNG, 90% quality is visually identical
370
+ // PNG: Slowest but lossless (use only when required)
159
371
  NSData *imageData;
160
372
  NSString *format;
161
- if ([fileName.lowercaseString hasSuffix:@".png"]) {
162
- imageData = UIImagePNGRepresentation(image);
163
- format = @"PNG";
164
- } else if ([fileName.lowercaseString hasSuffix:@".jpg"] || [fileName.lowercaseString hasSuffix:@".jpeg"]) {
165
- imageData = UIImageJPEGRepresentation(image, 0.9);
373
+ NSString *lowerFileName = fileName.lowercaseString;
374
+ if ([lowerFileName hasSuffix:@".jpg"] || [lowerFileName hasSuffix:@".jpeg"]) {
375
+ imageData = UIImageJPEGRepresentation(image, 0.9); // High quality, much faster than PNG
166
376
  format = @"JPEG at 90% quality";
377
+ } else if ([lowerFileName hasSuffix:@".png"]) {
378
+ imageData = UIImagePNGRepresentation(image); // PNG: Default for backward compatibility
379
+ format = @"PNG";
167
380
  } else {
168
- imageData = UIImagePNGRepresentation(image);
169
- format = @"PNG (default)";
381
+ // Default to JPEG for better performance (was PNG)
382
+ imageData = UIImageJPEGRepresentation(image, 0.9);
383
+ format = @"JPEG at 90% quality (default)";
170
384
  }
171
385
 
172
386
  NSLog(@"📁 [FILE] Compressing as %@", format);
@@ -201,10 +415,24 @@ RCT_EXPORT_METHOD(mergePDFs:(NSArray *)filePaths
201
415
  return;
202
416
  }
203
417
 
418
+ // Emit start event
419
+ @try {
420
+ [self sendEventWithName:@"PDFOperationProgress" body:@{
421
+ @"type": @"operationStart",
422
+ @"operation": @"mergePDFs",
423
+ @"fileCount": @(filePaths.count),
424
+ @"outputPath": outputPath ?: [NSNull null]
425
+ }];
426
+ } @catch (NSException *exception) {
427
+ // Event emitter not ready, continue without event
428
+ }
429
+
204
430
  // Create merged PDF
205
431
  PDFDocument *mergedPDF = [[PDFDocument alloc] init];
432
+ int currentFile = 0;
206
433
 
207
434
  for (NSString *filePath in filePaths) {
435
+ currentFile++;
208
436
  NSURL *pdfURL = [NSURL fileURLWithPath:filePath];
209
437
  PDFDocument *pdfDoc = [[PDFDocument alloc] initWithURL:pdfURL];
210
438
 
@@ -216,16 +444,68 @@ RCT_EXPORT_METHOD(mergePDFs:(NSArray *)filePaths
216
444
  [mergedPDF insertPage:page atIndex:mergedPDF.pageCount];
217
445
  }
218
446
  }
447
+
448
+ // Emit progress event
449
+ @try {
450
+ double progress = (double)currentFile / (double)filePaths.count;
451
+ [self sendEventWithName:@"PDFOperationProgress" body:@{
452
+ @"type": @"operationProgress",
453
+ @"operation": @"mergePDFs",
454
+ @"currentFile": @(currentFile),
455
+ @"totalFiles": @(filePaths.count),
456
+ @"progress": @(progress)
457
+ }];
458
+ } @catch (NSException *exception) {
459
+ // Event emitter not ready, continue without event
460
+ }
219
461
  }
220
462
  }
221
463
 
464
+ // Handle null/empty outputPath - generate default path
465
+ if (!outputPath || outputPath.length == 0 || [outputPath isKindOfClass:[NSNull class]]) {
466
+ // Get cache directory
467
+ NSArray *cachePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
468
+ NSString *cacheDir = [cachePaths firstObject];
469
+
470
+ // Extract base filename from first input file
471
+ NSString *firstFilePath = [filePaths firstObject];
472
+ NSString *inputFileName = [[firstFilePath lastPathComponent] stringByDeletingPathExtension];
473
+
474
+ // Generate timestamped filename
475
+ NSString *fileName = [self generateTimestampedFileName:inputFileName pageNum:-1 extension:@"pdf"];
476
+ outputPath = [cacheDir stringByAppendingPathComponent:fileName];
477
+ NSLog(@"📁 [FILE] Generated output path: %@", outputPath);
478
+ }
479
+
222
480
  // Save merged PDF
223
481
  NSURL *outputURL = [NSURL fileURLWithPath:outputPath];
224
482
  BOOL success = [mergedPDF writeToURL:outputURL];
225
483
 
226
484
  if (success) {
485
+ // Emit complete event
486
+ @try {
487
+ [self sendEventWithName:@"PDFOperationComplete" body:@{
488
+ @"type": @"operationComplete",
489
+ @"operation": @"mergePDFs",
490
+ @"outputPath": outputPath,
491
+ @"success": @YES
492
+ }];
493
+ } @catch (NSException *exception) {
494
+ // Event emitter not ready, continue without event
495
+ }
227
496
  resolve(outputPath);
228
497
  } else {
498
+ // Emit error event
499
+ @try {
500
+ [self sendEventWithName:@"PDFOperationComplete" body:@{
501
+ @"type": @"operationError",
502
+ @"operation": @"mergePDFs",
503
+ @"error": @"Failed to save merged PDF",
504
+ @"success": @NO
505
+ }];
506
+ } @catch (NSException *exception) {
507
+ // Event emitter not ready, continue without event
508
+ }
229
509
  reject(@"MERGE_ERROR", @"Failed to save merged PDF", nil);
230
510
  }
231
511
  }
@@ -239,8 +519,6 @@ RCT_EXPORT_METHOD(splitPDF:(NSString *)filePath
239
519
  resolver:(RCTPromiseResolveBlock)resolve
240
520
  rejecter:(RCTPromiseRejectBlock)reject) {
241
521
 
242
- NSLog(@"✂️ [SPLIT] splitPDF - START - file: %@, ranges: %lu", filePath, (unsigned long)pageRanges.count);
243
-
244
522
  // All features are FREE - no license verification needed
245
523
 
246
524
  if (!filePath || filePath.length == 0) {
@@ -249,6 +527,36 @@ RCT_EXPORT_METHOD(splitPDF:(NSString *)filePath
249
527
  return;
250
528
  }
251
529
 
530
+ // Handle case where pageRanges might be passed as string instead of array
531
+ if (!pageRanges || ![pageRanges isKindOfClass:[NSArray class]]) {
532
+ if ([pageRanges isKindOfClass:[NSString class]]) {
533
+ NSLog(@"⚠️ [SPLIT] pageRanges passed as string, attempting to parse: %@", pageRanges);
534
+ NSError *jsonError;
535
+ NSData *jsonData = [(NSString *)pageRanges dataUsingEncoding:NSUTF8StringEncoding];
536
+ id parsed = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&jsonError];
537
+ if (!jsonError && [parsed isKindOfClass:[NSArray class]]) {
538
+ pageRanges = (NSArray *)parsed;
539
+ NSLog(@"✅ [SPLIT] Successfully parsed pageRanges from string");
540
+ } else {
541
+ NSLog(@"❌ [SPLIT] Failed to parse pageRanges string: %@", jsonError.localizedDescription);
542
+ reject(@"INVALID_RANGES", @"pageRanges must be an array or valid JSON array string", nil);
543
+ return;
544
+ }
545
+ } else {
546
+ NSLog(@"❌ [SPLIT] Invalid pageRanges type");
547
+ reject(@"INVALID_RANGES", @"pageRanges must be an array", nil);
548
+ return;
549
+ }
550
+ }
551
+
552
+ if (pageRanges.count == 0) {
553
+ NSLog(@"❌ [SPLIT] Empty pageRanges");
554
+ reject(@"INVALID_RANGES", @"At least one page range is required", nil);
555
+ return;
556
+ }
557
+
558
+ NSLog(@"✂️ [SPLIT] splitPDF - START - file: %@, ranges: %lu", filePath, (unsigned long)pageRanges.count);
559
+
252
560
  NSURL *pdfURL = [NSURL fileURLWithPath:filePath];
253
561
  PDFDocument *pdfDocument = [[PDFDocument alloc] initWithURL:pdfURL];
254
562
 
@@ -258,10 +566,61 @@ RCT_EXPORT_METHOD(splitPDF:(NSString *)filePath
258
566
  return;
259
567
  }
260
568
 
569
+ // Emit start event
570
+ @try {
571
+ [self sendEventWithName:@"PDFOperationProgress" body:@{
572
+ @"type": @"operationStart",
573
+ @"operation": @"splitPDF",
574
+ @"filePath": filePath,
575
+ @"rangeCount": @(pageRanges.count)
576
+ }];
577
+ } @catch (NSException *exception) {
578
+ // Event emitter not ready, continue without event
579
+ }
580
+
261
581
  NSMutableArray *splitFiles = [NSMutableArray array];
262
582
  NSUInteger pageCount = pdfDocument.pageCount;
263
583
  NSLog(@"📁 [FILE] PDF opened, total pages: %lu", (unsigned long)pageCount);
264
584
 
585
+ // Convert flat array format [1, 45, 46, 91] to dictionary format [{start: 0, end: 44}, {start: 45, end: 90}]
586
+ NSMutableArray *convertedRanges = [NSMutableArray array];
587
+ if (pageRanges.count > 0) {
588
+ id firstElement = pageRanges[0];
589
+ if ([firstElement isKindOfClass:[NSNumber class]]) {
590
+ // Flat array format - convert to dictionary pairs
591
+ NSLog(@"📊 [SPLIT] Converting flat array to dictionary format");
592
+ for (NSUInteger i = 0; i < pageRanges.count; i += 2) {
593
+ if (i + 1 < pageRanges.count) {
594
+ NSNumber *startNum = pageRanges[i];
595
+ NSNumber *endNum = pageRanges[i + 1];
596
+ // Convert from 1-based (JS) to 0-based (Objective-C)
597
+ int start = startNum.intValue - 1;
598
+ int end = endNum.intValue - 1;
599
+ [convertedRanges addObject:@{
600
+ @"start": @(start),
601
+ @"end": @(end)
602
+ }];
603
+ }
604
+ }
605
+ pageRanges = convertedRanges;
606
+ NSLog(@"📊 [SPLIT] Converted to %lu range(s)", (unsigned long)pageRanges.count);
607
+ } else if ([firstElement isKindOfClass:[NSDictionary class]]) {
608
+ // Already in dictionary format - convert 1-based to 0-based
609
+ NSLog(@"📊 [SPLIT] Converting dictionary ranges from 1-based to 0-based");
610
+ for (NSDictionary *range in pageRanges) {
611
+ NSNumber *startNum = range[@"start"];
612
+ NSNumber *endNum = range[@"end"];
613
+ if (startNum && endNum) {
614
+ [convertedRanges addObject:@{
615
+ @"start": @(startNum.intValue - 1),
616
+ @"end": @(endNum.intValue - 1)
617
+ }];
618
+ }
619
+ }
620
+ pageRanges = convertedRanges;
621
+ }
622
+ }
623
+
265
624
  int rangeIndex = 0;
266
625
  for (NSDictionary *range in pageRanges) {
267
626
  rangeIndex++;
@@ -274,6 +633,20 @@ RCT_EXPORT_METHOD(splitPDF:(NSString *)filePath
274
633
 
275
634
  NSLog(@"📊 [PROGRESS] Processing range %d/%lu: pages %d-%d", rangeIndex, (unsigned long)pageRanges.count, start + 1, end + 1);
276
635
 
636
+ // Emit progress event
637
+ @try {
638
+ double progress = (double)rangeIndex / (double)pageRanges.count;
639
+ [self sendEventWithName:@"PDFOperationProgress" body:@{
640
+ @"type": @"operationProgress",
641
+ @"operation": @"splitPDF",
642
+ @"currentRange": @(rangeIndex),
643
+ @"totalRanges": @(pageRanges.count),
644
+ @"progress": @(progress)
645
+ }];
646
+ } @catch (NSException *exception) {
647
+ // Event emitter not ready, continue without event
648
+ }
649
+
277
650
  if (start >= 0 && end < pageCount && start <= end) {
278
651
  PDFDocument *splitPDF = [[PDFDocument alloc] init];
279
652
 
@@ -308,6 +681,20 @@ RCT_EXPORT_METHOD(splitPDF:(NSString *)filePath
308
681
  }
309
682
 
310
683
  NSLog(@"✅ [SPLIT] splitPDF - SUCCESS - Split into %lu files", (unsigned long)splitFiles.count);
684
+
685
+ // Emit complete event
686
+ @try {
687
+ [self sendEventWithName:@"PDFOperationComplete" body:@{
688
+ @"type": @"operationComplete",
689
+ @"operation": @"splitPDF",
690
+ @"splitFiles": splitFiles,
691
+ @"fileCount": @(splitFiles.count),
692
+ @"success": @YES
693
+ }];
694
+ } @catch (NSException *exception) {
695
+ // Event emitter not ready, continue without event
696
+ }
697
+
311
698
  resolve(splitFiles);
312
699
  }
313
700
 
@@ -331,6 +718,18 @@ RCT_EXPORT_METHOD(extractPages:(NSString *)filePath
331
718
  return;
332
719
  }
333
720
 
721
+ // Emit start event
722
+ @try {
723
+ [self sendEventWithName:@"PDFOperationProgress" body:@{
724
+ @"type": @"operationStart",
725
+ @"operation": @"extractPages",
726
+ @"filePath": filePath,
727
+ @"pageCount": @(pageNumbers.count)
728
+ }];
729
+ } @catch (NSException *exception) {
730
+ // Event emitter not ready, continue without event
731
+ }
732
+
334
733
  NSURL *pdfURL = [NSURL fileURLWithPath:filePath];
335
734
  PDFDocument *pdfDocument = [[PDFDocument alloc] initWithURL:pdfURL];
336
735
 
@@ -343,7 +742,23 @@ RCT_EXPORT_METHOD(extractPages:(NSString *)filePath
343
742
  NSUInteger totalPages = pdfDocument.pageCount;
344
743
  NSLog(@"📁 [FILE] PDF opened, total pages: %lu", (unsigned long)totalPages);
345
744
  NSLog(@"📊 [EXTRACT] Pages to extract: %@", [pageNumbers componentsJoinedByString:@", "]);
346
- NSLog(@"📁 [FILE] Output path: %@", outputPath);
745
+
746
+ // Handle null/empty outputPath - generate default path
747
+ if (!outputPath || outputPath.length == 0 || [outputPath isKindOfClass:[NSNull class]]) {
748
+ // Get cache directory
749
+ NSArray *cachePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
750
+ NSString *cacheDir = [cachePaths firstObject];
751
+
752
+ // Extract base filename from input path
753
+ NSString *inputFileName = [[filePath lastPathComponent] stringByDeletingPathExtension];
754
+
755
+ // Generate timestamped filename
756
+ NSString *fileName = [self generateTimestampedFileName:inputFileName pageNum:-1 extension:@"pdf"];
757
+ outputPath = [cacheDir stringByAppendingPathComponent:fileName];
758
+ NSLog(@"📁 [FILE] Generated output path: %@", outputPath);
759
+ } else {
760
+ NSLog(@"📁 [FILE] Using provided output path: %@", outputPath);
761
+ }
347
762
 
348
763
  // Create extracted PDF
349
764
  PDFDocument *extractedPDF = [[PDFDocument alloc] init];
@@ -356,6 +771,20 @@ RCT_EXPORT_METHOD(extractPages:(NSString *)filePath
356
771
 
357
772
  NSLog(@"📊 [PROGRESS] Processing page %d/%lu (page number: %d)", current, (unsigned long)pageNumbers.count, pageIndex);
358
773
 
774
+ // Emit progress event
775
+ @try {
776
+ double progress = (double)current / (double)pageNumbers.count;
777
+ [self sendEventWithName:@"PDFOperationProgress" body:@{
778
+ @"type": @"operationProgress",
779
+ @"operation": @"extractPages",
780
+ @"currentPage": @(current),
781
+ @"totalPages": @(pageNumbers.count),
782
+ @"progress": @(progress)
783
+ }];
784
+ } @catch (NSException *exception) {
785
+ // Event emitter not ready, continue without event
786
+ }
787
+
359
788
  if (pageIndex >= 0 && pageIndex < totalPages) {
360
789
  PDFPage *page = [pdfDocument pageAtIndex:pageIndex];
361
790
  if (page) {
@@ -377,9 +806,36 @@ RCT_EXPORT_METHOD(extractPages:(NSString *)filePath
377
806
  NSDictionary *fileAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath:outputPath error:nil];
378
807
  unsigned long long fileSize = [fileAttrs fileSize];
379
808
  NSLog(@"✅ [EXTRACT] extractPages - SUCCESS - Extracted %d pages to: %@ (size: %llu bytes)", extractedCount, outputPath, fileSize);
809
+
810
+ // Emit complete event
811
+ @try {
812
+ [self sendEventWithName:@"PDFOperationComplete" body:@{
813
+ @"type": @"operationComplete",
814
+ @"operation": @"extractPages",
815
+ @"outputPath": outputPath,
816
+ @"extractedCount": @(extractedCount),
817
+ @"success": @YES
818
+ }];
819
+ } @catch (NSException *exception) {
820
+ // Event emitter not ready, continue without event
821
+ }
822
+
380
823
  resolve(outputPath);
381
824
  } else {
382
825
  NSLog(@"❌ [EXTRACT] Failed to save extracted PDF");
826
+
827
+ // Emit error event
828
+ @try {
829
+ [self sendEventWithName:@"PDFOperationComplete" body:@{
830
+ @"type": @"operationError",
831
+ @"operation": @"extractPages",
832
+ @"error": @"Failed to save extracted PDF",
833
+ @"success": @NO
834
+ }];
835
+ } @catch (NSException *exception) {
836
+ // Event emitter not ready, continue without event
837
+ }
838
+
383
839
  reject(@"EXTRACT_ERROR", @"Failed to save extracted PDF", nil);
384
840
  }
385
841
  }
@@ -4,15 +4,25 @@
4
4
  */
5
5
 
6
6
  #import <React/RCTBridgeModule.h>
7
+ #import <React/RCTEventEmitter.h>
7
8
  #import <Foundation/Foundation.h>
8
9
 
9
- @interface PDFNativeCacheManager : NSObject <RCTBridgeModule>
10
+ @class LazyMetadataLoader;
11
+ @class MemoryMappedCache;
12
+ @class StreamingPDFProcessor;
13
+
14
+ @interface PDFNativeCacheManager : RCTEventEmitter <RCTBridgeModule>
10
15
 
11
16
  @property (nonatomic, strong) NSString *cacheDir;
12
17
  @property (nonatomic, strong) NSString *metadataFilePath;
13
18
  @property (nonatomic, strong) NSMutableDictionary *cacheMetadata;
14
19
  @property (nonatomic, strong) NSMutableDictionary *cacheStats;
15
20
  @property (nonatomic, strong) NSObject *cacheLock;
21
+ @property (nonatomic, assign) BOOL metadataDirty;
22
+ @property (nonatomic, strong) dispatch_source_t metadataSaveTimer;
23
+ @property (nonatomic, strong) LazyMetadataLoader *lazyLoader;
24
+ @property (nonatomic, strong) MemoryMappedCache *memoryMappedCache;
25
+ @property (nonatomic, strong) StreamingPDFProcessor *streamingProcessor;
16
26
 
17
27
  // Singleton instance for direct access
18
28
  + (instancetype)sharedInstance;