react-native-pdf-jsi 4.3.1 → 4.4.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 (36) hide show
  1. package/PdfView.js +1 -1
  2. package/README.md +30 -1
  3. package/android/src/main/java/org/wonday/pdf/PDFJSIManager.java +138 -3
  4. package/android/src/main/java/org/wonday/pdf/PdfManager.java +10 -0
  5. package/android/src/main/java/org/wonday/pdf/PdfView.java +110 -3
  6. package/android/src/main/java/org/wonday/pdf/SearchRegistry.java +44 -0
  7. package/android/src/main/jniLibs/arm64-v8a/libpdfjsi.so +0 -0
  8. package/android/src/main/jniLibs/armeabi-v7a/libpdfjsi.so +0 -0
  9. package/android/src/main/jniLibs/x86/libpdfjsi.so +0 -0
  10. package/android/src/main/jniLibs/x86_64/libpdfjsi.so +0 -0
  11. package/fabric/RNPDFPdfNativeComponent.js +2 -0
  12. package/index.d.ts +34 -0
  13. package/index.js +29 -2
  14. package/ios/RNPDFPdf/PDFJSIManager.m +89 -4
  15. package/ios/RNPDFPdf/RNPDFPdfView.h +2 -0
  16. package/ios/RNPDFPdf/RNPDFPdfView.mm +251 -13
  17. package/ios/RNPDFPdf/RNPDFPdfViewManager.mm +5 -1
  18. package/ios/RNPDFPdf/SearchRegistry.h +21 -0
  19. package/ios/RNPDFPdf/SearchRegistry.m +71 -0
  20. package/package.json +4 -2
  21. package/src/PDFJSI.js +1 -1
  22. package/android/.gradle/5.6.1/fileChanges/last-build.bin +0 -0
  23. package/android/.gradle/5.6.1/fileHashes/fileHashes.lock +0 -0
  24. package/android/.gradle/5.6.1/gc.properties +0 -0
  25. package/android/.gradle/8.5/checksums/checksums.lock +0 -0
  26. package/android/.gradle/8.5/checksums/md5-checksums.bin +0 -0
  27. package/android/.gradle/8.5/checksums/sha1-checksums.bin +0 -0
  28. package/android/.gradle/8.5/dependencies-accessors/dependencies-accessors.lock +0 -0
  29. package/android/.gradle/8.5/dependencies-accessors/gc.properties +0 -0
  30. package/android/.gradle/8.5/executionHistory/executionHistory.lock +0 -0
  31. package/android/.gradle/8.5/fileChanges/last-build.bin +0 -0
  32. package/android/.gradle/8.5/fileHashes/fileHashes.lock +0 -0
  33. package/android/.gradle/8.5/gc.properties +0 -0
  34. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  35. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  36. package/android/.gradle/vcs-1/gc.properties +0 -0
@@ -8,9 +8,11 @@
8
8
 
9
9
  #import "PDFJSIManager.h"
10
10
  #import "PDFNativeCacheManager.h"
11
+ #import "SearchRegistry.h"
11
12
  #import <React/RCTLog.h>
12
13
  #import <React/RCTUtils.h>
13
14
  #import <React/RCTBridge.h>
15
+ #import <PDFKit/PDFKit.h>
14
16
  #import <dispatch/dispatch.h>
15
17
 
16
18
  @implementation PDFJSIManager {
@@ -248,6 +250,21 @@ RCT_EXPORT_METHOD(optimizeMemory:(NSString *)pdfId
248
250
  });
249
251
  }
250
252
 
253
+ RCT_EXPORT_METHOD(registerPathForSearch:(NSString *)pdfId
254
+ path:(NSString *)path
255
+ resolver:(RCTPromiseResolveBlock)resolve
256
+ rejecter:(RCTPromiseRejectBlock)reject)
257
+ {
258
+ if (pdfId.length && path.length) {
259
+ [SearchRegistry registerPath:pdfId path:path];
260
+ RCTLogInfo(@"✅ [SearchRegistry] Registered path for pdfId: %@ (path length %lu)", pdfId, (unsigned long)path.length);
261
+ resolve(@YES);
262
+ } else {
263
+ RCTLogWarn(@"⚠️ [SearchRegistry] registerPathForSearch skipped: pdfId length=%lu path length=%lu", (unsigned long)pdfId.length, (unsigned long)path.length);
264
+ resolve(@NO);
265
+ }
266
+ }
267
+
251
268
  RCT_EXPORT_METHOD(searchTextDirect:(NSString *)pdfId
252
269
  searchTerm:(NSString *)searchTerm
253
270
  startPage:(NSInteger)startPage
@@ -259,14 +276,82 @@ RCT_EXPORT_METHOD(searchTextDirect:(NSString *)pdfId
259
276
  reject(@"JSI_NOT_INITIALIZED", @"JSI is not initialized", nil);
260
277
  return;
261
278
  }
279
+ if (!searchTerm || searchTerm.length == 0) {
280
+ resolve(@[]);
281
+ return;
282
+ }
262
283
 
263
284
  dispatch_async(_backgroundQueue, ^{
264
285
  @try {
265
286
  RCTLogInfo(@"🔍 Searching text via JSI: '%@' in pages %ld-%ld", searchTerm, (long)startPage, (long)endPage);
266
287
 
267
- // Simulate text search - return empty array for now
268
- NSArray *results = @[];
269
- resolve(results);
288
+ NSString *path = [SearchRegistry pathForPdfId:pdfId];
289
+ if (!path || path.length == 0) {
290
+ RCTLogWarn(@"❌ [Search] No path registered for pdfId: %@ - ensure onLoadComplete ran and pdfId is set on Pdf", pdfId);
291
+ resolve(@[]);
292
+ return;
293
+ }
294
+ if ([path hasPrefix:@"http://"] || [path hasPrefix:@"https://"]) {
295
+ RCTLogWarn(@"❌ [Search] Path for pdfId %@ is a URI (not a local file path) - cannot open for search", pdfId);
296
+ resolve(@[]);
297
+ return;
298
+ }
299
+ RCTLogInfo(@"📂 [Search] Path for pdfId '%@': length %lu", pdfId, (unsigned long)path.length);
300
+ if ([path hasPrefix:@"file://"]) {
301
+ path = [path substringFromIndex:7];
302
+ }
303
+ BOOL readable = [[NSFileManager defaultManager] isReadableFileAtPath:path];
304
+ if (!readable) {
305
+ RCTLogWarn(@"❌ [Search] File not readable at path (length %lu)", (unsigned long)path.length);
306
+ resolve(@[]);
307
+ return;
308
+ }
309
+ NSURL *fileURL = [NSURL fileURLWithPath:path];
310
+ PDFDocument *doc = [[PDFDocument alloc] initWithURL:fileURL];
311
+ if (!doc || doc.pageCount == 0) {
312
+ RCTLogWarn(@"❌ [Search] PDFDocument init failed or empty: doc=%p pageCount=%lu", (__bridge void *)doc, (unsigned long)doc.pageCount);
313
+ resolve(@[]);
314
+ return;
315
+ }
316
+
317
+ NSInteger from = MAX(1, startPage);
318
+ NSInteger to = MIN((NSInteger)doc.pageCount, endPage);
319
+ NSMutableArray *out = [NSMutableArray array];
320
+
321
+ // findString:withOptions: returns selections; each can span multiple pages
322
+ NSArray<PDFSelection *> *selections = [doc findString:searchTerm withOptions:NSCaseInsensitiveSearch];
323
+ RCTLogInfo(@"📄 [Search] findString returned %lu selection(s) for '%@'", (unsigned long)selections.count, searchTerm);
324
+ for (PDFSelection *sel in selections) {
325
+ for (PDFPage *page in sel.pages) {
326
+ NSInteger pageIndex1Based = [doc indexForPage:page] + 1;
327
+ if (pageIndex1Based < from || pageIndex1Based > to) continue;
328
+
329
+ CGRect bounds = [sel boundsForPage:page];
330
+ // PDF page coords: origin bottom-left. Serialize as "left,top,right,bottom" (y-up: top > bottom)
331
+ CGFloat left = bounds.origin.x;
332
+ CGFloat bottom = bounds.origin.y;
333
+ CGFloat right = bounds.origin.x + bounds.size.width;
334
+ CGFloat top = bounds.origin.y + bounds.size.height;
335
+ NSString *rectStr = [NSString stringWithFormat:@"%g,%g,%g,%g", left, top, right, bottom];
336
+
337
+ [out addObject:@{
338
+ @"page": @(pageIndex1Based),
339
+ @"text": sel.string ?: @"",
340
+ @"rect": rectStr
341
+ }];
342
+ }
343
+ }
344
+
345
+ // Register page sizes in points for highlight scaling (use first page of range if we have selections)
346
+ for (NSInteger idx = from; idx <= to; idx++) {
347
+ PDFPage *page = [doc pageAtIndex:(NSUInteger)(idx - 1)];
348
+ if (page) {
349
+ CGRect box = [page boundsForBox:kPDFDisplayBoxMediaBox];
350
+ [SearchRegistry registerPageSizePointsForPdfId:pdfId pageIndex0Based:(idx - 1) widthPt:box.size.width heightPt:box.size.height];
351
+ }
352
+ }
353
+
354
+ resolve([out copy]);
270
355
 
271
356
  } @catch (NSException *exception) {
272
357
  RCTLogError(@"❌ Error searching text via JSI: %@", exception.reason);
@@ -580,7 +665,7 @@ RCT_EXPORT_METHOD(testNativeCache:(RCTPromiseResolveBlock)resolve
580
665
 
581
666
  // iOS doesn't have the same 16KB page size requirements as Android
582
667
  // but we still check for compatibility
583
- BOOL is16KBSupported = [self checkiOS16KBSupport];
668
+ (void)[self checkiOS16KBSupport];
584
669
 
585
670
  NSDictionary *result = @{
586
671
  @"supported": @YES, // iOS is generally compatible
@@ -68,6 +68,8 @@ UIView
68
68
  @property(nonatomic) int spacing;
69
69
  @property(nonatomic, strong) NSString *password;
70
70
  @property(nonatomic) BOOL singlePage;
71
+ @property(nonatomic, strong) NSString *pdfId;
72
+ @property(nonatomic, copy) NSArray *highlightRects;
71
73
 
72
74
  @property(nonatomic, copy) RCTBubblingEventBlock onChange;
73
75
 
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  #import "RNPDFPdfView.h"
10
+ #import "SearchRegistry.h"
10
11
 
11
12
  #import <Foundation/Foundation.h>
12
13
  #import <QuartzCore/QuartzCore.h>
@@ -55,6 +56,37 @@ const float MAX_SCALE = 3.0f;
55
56
  const float MIN_SCALE = 1.0f;
56
57
 
57
58
 
59
+ /** Overlay that draws highlight rects on top of the PDF view. */
60
+ @interface HighlightOverlayView : UIView
61
+ @property (nonatomic, weak) PDFView *pdfView;
62
+ @property (nonatomic, copy) NSArray<NSDictionary *> *highlightRects;
63
+ @end
64
+
65
+ @implementation HighlightOverlayView
66
+ - (void)drawRect:(CGRect)rect {
67
+ PDFView *pv = self.pdfView;
68
+ NSArray *items = self.highlightRects;
69
+ if (!pv || !pv.document || !items.count) return;
70
+ PDFDocument *doc = pv.document;
71
+ [[UIColor colorWithRed:1 green:1 blue:0 alpha:0.35] setFill];
72
+ for (NSDictionary *item in items) {
73
+ NSNumber *pageNum = item[@"page"];
74
+ NSString *rectStr = item[@"rect"];
75
+ if (!pageNum || !rectStr.length) continue;
76
+ int page1 = pageNum.intValue;
77
+ if (page1 < 1) continue;
78
+ PDFPage *page = [doc pageAtIndex:(NSUInteger)(page1 - 1)];
79
+ if (!page) continue;
80
+ NSArray<NSString *> *parts = [rectStr componentsSeparatedByString:@","];
81
+ if (parts.count != 4) continue;
82
+ CGFloat left = parts[0].doubleValue, top = parts[1].doubleValue, right = parts[2].doubleValue, bottom = parts[3].doubleValue;
83
+ CGRect pageRect = CGRectMake(left, bottom, right - left, top - bottom);
84
+ CGRect viewRect = [pv convertRect:pageRect fromPage:page];
85
+ CGContextFillRect(UIGraphicsGetCurrentContext(), viewRect);
86
+ }
87
+ }
88
+ @end
89
+
58
90
  @interface RNPDFScrollViewDelegateProxy : NSObject <UIScrollViewDelegate>
59
91
  - (instancetype)initWithPrimary:(id<UIScrollViewDelegate>)primary secondary:(id<UIScrollViewDelegate>)secondary;
60
92
  @end
@@ -98,10 +130,33 @@ const float MIN_SCALE = 1.0f;
98
130
  }
99
131
 
100
132
  - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
133
+ // First check if primary delegate (PDFView's internal) handles it
101
134
  if (_primary && [_primary respondsToSelector:@selector(viewForZoomingInScrollView:)]) {
102
- return [_primary viewForZoomingInScrollView:scrollView];
135
+ UIView *zoomView = [_primary viewForZoomingInScrollView:scrollView];
136
+ if (zoomView != nil) {
137
+ NSLog(@"🔍 [iOS Zoom Delegate] Primary delegate returned zoom view: %@", NSStringFromClass([zoomView class]));
138
+ return zoomView;
139
+ }
103
140
  }
104
- return nil;
141
+
142
+ // PDFKit's scroll view needs to zoom the PDFDocumentView
143
+ // Search for it in the hierarchy
144
+ for (UIView *subview in scrollView.subviews) {
145
+ NSString *className = NSStringFromClass([subview class]);
146
+ if ([className containsString:@"PDFDocumentView"] || [className containsString:@"PDFPage"]) {
147
+ NSLog(@"🔍 [iOS Zoom Delegate] Found PDF view for zooming: %@", className);
148
+ return subview;
149
+ }
150
+ }
151
+
152
+ // Fallback to first subview if it exists
153
+ UIView *fallback = scrollView.subviews.firstObject;
154
+ if (fallback) {
155
+ NSLog(@"🔍 [iOS Zoom Delegate] Using fallback zoom view: %@", NSStringFromClass([fallback class]));
156
+ } else {
157
+ NSLog(@"⚠️ [iOS Zoom Delegate] WARNING: No view found for zooming! Scroll view has %lu subviews", (unsigned long)scrollView.subviews.count);
158
+ }
159
+ return fallback;
105
160
  }
106
161
 
107
162
  @end
@@ -158,6 +213,13 @@ const float MIN_SCALE = 1.0f;
158
213
  // Track usePageViewController state to prevent unnecessary reconfiguration
159
214
  BOOL _currentUsePageViewController;
160
215
  BOOL _usePageViewControllerStateInitialized;
216
+
217
+ // Search and highlight (iOS parity with Android)
218
+ NSString *_pdfId;
219
+ NSArray *_highlightRects;
220
+ HighlightOverlayView *_highlightOverlay;
221
+ /// Local file path when document loaded (used for SearchRegistry; may differ from _path which can be URI)
222
+ NSString *_lastLoadedPath;
161
223
  }
162
224
 
163
225
  #ifdef RCT_NEW_ARCH_ENABLED
@@ -292,6 +354,30 @@ using namespace facebook::react;
292
354
  _scrollEnabled = newProps.scrollEnabled;
293
355
  [updatedPropNames addObject:@"scrollEnabled"];
294
356
  }
357
+ NSString *newPdfId = RCTNSStringFromStringNilIfEmpty(newProps.pdfId);
358
+ if (_pdfId != newPdfId && ![newPdfId isEqualToString:_pdfId]) {
359
+ if (_pdfId.length) [SearchRegistry unregisterPath:_pdfId];
360
+ _pdfId = [newPdfId copy];
361
+ [updatedPropNames addObject:@"pdfId"];
362
+ // Only register local file paths; never register URIs - onDocumentChanged will register when we have local path
363
+ NSString *pathToRegister = nil;
364
+ if (_lastLoadedPath.length > 0) {
365
+ pathToRegister = _lastLoadedPath;
366
+ } else if (_path.length > 0 && [_path hasPrefix:@"/"]) {
367
+ pathToRegister = _path;
368
+ }
369
+ if (_pdfId.length && pathToRegister.length > 0) {
370
+ [SearchRegistry registerPath:_pdfId path:pathToRegister];
371
+ RCTLogInfo(@"✅ [iOS] SearchRegistry registered path for pdfId: %@ (from updateProps)", _pdfId);
372
+ }
373
+ }
374
+ // Convert codegen vector of {page, rect} to NSArray for setHighlightRects
375
+ NSMutableArray *newHighlightRects = [NSMutableArray array];
376
+ for (const auto &item : newProps.highlightRects) {
377
+ [newHighlightRects addObject:@{ @"page": @(item.page), @"rect": [NSString stringWithUTF8String:item.rect.c_str()] }];
378
+ }
379
+ [self setHighlightRects:[newHighlightRects copy]];
380
+ [updatedPropNames addObject:@"highlightRects"];
295
381
 
296
382
  [super updateProps:props oldProps:oldProps];
297
383
  [self didSetProps:updatedPropNames];
@@ -306,10 +392,11 @@ using namespace facebook::react;
306
392
  - (void)prepareForRecycle
307
393
  {
308
394
  [super prepareForRecycle];
309
-
395
+ if (_pdfId.length) [SearchRegistry unregisterPath:_pdfId];
310
396
  [_pdfView removeFromSuperview];
311
397
  _pdfDocument = Nil;
312
398
  _pdfView = Nil;
399
+ _highlightOverlay = Nil;
313
400
  //Remove notifications
314
401
  [[NSNotificationCenter defaultCenter] removeObserver:self name:@"PDFViewDocumentChangedNotification" object:nil];
315
402
  [[NSNotificationCenter defaultCenter] removeObserver:self name:@"PDFViewPageChangedNotification" object:nil];
@@ -419,6 +506,11 @@ using namespace facebook::react;
419
506
  _preloadQueue.maxConcurrentOperationCount = 3;
420
507
  _preloadQueue.qualityOfService = NSQualityOfServiceBackground;
421
508
 
509
+ _pdfId = nil;
510
+ _highlightRects = nil;
511
+ _lastLoadedPath = nil;
512
+ _highlightOverlay = nil;
513
+
422
514
  // init and config PDFView
423
515
  _pdfView = [[PDFView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)];
424
516
  _pdfView.displayMode = kPDFDisplaySinglePageContinuous;
@@ -443,10 +535,14 @@ using namespace facebook::react;
443
535
  [[_pdfView document] setDelegate: self];
444
536
  [_pdfView setDelegate: self];
445
537
 
446
- // Disable built-in double tap, so as not to conflict with custom recognizers.
538
+ // Only disable double-tap recognizers to avoid conflicts with custom double-tap
539
+ // Leave all other gestures (including pinch) enabled
447
540
  for (UIGestureRecognizer *recognizer in _pdfView.gestureRecognizers) {
448
541
  if ([recognizer isKindOfClass:[UITapGestureRecognizer class]]) {
449
- recognizer.enabled = NO;
542
+ UITapGestureRecognizer *tapGesture = (UITapGestureRecognizer *)recognizer;
543
+ if (tapGesture.numberOfTapsRequired == 2) {
544
+ recognizer.enabled = NO;
545
+ }
450
546
  }
451
547
  }
452
548
 
@@ -661,6 +757,18 @@ using namespace facebook::react;
661
757
  _pdfView.maxScaleFactor = _fixScaleFactor*_maxScale;
662
758
  }
663
759
  }
760
+
761
+ // CRITICAL: Also configure the internal scroll view zoom scales
762
+ // This must be done AFTER _fixScaleFactor is set above
763
+ if (_internalScrollView && _fixScaleFactor > 0) {
764
+ _internalScrollView.minimumZoomScale = _fixScaleFactor * _minScale;
765
+ _internalScrollView.maximumZoomScale = _fixScaleFactor * _maxScale;
766
+ _internalScrollView.zoomScale = _pdfView.scaleFactor;
767
+ RCTLogInfo(@"🔍 [iOS Zoom] Configured internal scroll view zoom scales - min=%f, max=%f, current=%f",
768
+ _internalScrollView.minimumZoomScale,
769
+ _internalScrollView.maximumZoomScale,
770
+ _internalScrollView.zoomScale);
771
+ }
664
772
 
665
773
  }
666
774
 
@@ -668,6 +776,12 @@ using namespace facebook::react;
668
776
  _pdfView.scaleFactor = _scale * _fixScaleFactor;
669
777
  if (_pdfView.scaleFactor>_pdfView.maxScaleFactor) _pdfView.scaleFactor = _pdfView.maxScaleFactor;
670
778
  if (_pdfView.scaleFactor<_pdfView.minScaleFactor) _pdfView.scaleFactor = _pdfView.minScaleFactor;
779
+
780
+ // Also update internal scroll view zoom scale when scale changes
781
+ if (_internalScrollView && _fixScaleFactor > 0) {
782
+ _internalScrollView.zoomScale = _pdfView.scaleFactor;
783
+ RCTLogInfo(@"🔍 [iOS Zoom] Updated internal scroll view zoom scale to %f", _internalScrollView.zoomScale);
784
+ }
671
785
  }
672
786
 
673
787
  if (_pdfDocument && ([effectiveChangedProps containsObject:@"path"] || [changedProps containsObject:@"horizontal"])) {
@@ -952,10 +1066,58 @@ using namespace facebook::react;
952
1066
  RCTLogInfo(@"🔍 [iOS] loadComplete message: %@", message);
953
1067
 
954
1068
  [self notifyOnChangeWithMessage:message];
1069
+
1070
+ // Store local path so we can register when pdfId is set (Fabric may set pdfId after document load)
1071
+ _lastLoadedPath = [pathValue copy];
1072
+ // Register path for searchTextDirect (iOS parity with Android)
1073
+ if (_pdfId.length && pathValue.length) {
1074
+ [SearchRegistry registerPath:_pdfId path:pathValue];
1075
+ RCTLogInfo(@"✅ [iOS] SearchRegistry registered path for pdfId: %@ (from onDocumentChanged)", _pdfId);
1076
+ }
955
1077
  }
956
1078
 
957
1079
  }
958
1080
 
1081
+ - (void)setPdfId:(NSString *)pdfId {
1082
+ if (_pdfId.length && ![pdfId isEqualToString:_pdfId]) {
1083
+ [SearchRegistry unregisterPath:_pdfId];
1084
+ }
1085
+ _pdfId = [pdfId copy];
1086
+ // If document already loaded, register path now (Fabric may set pdfId after path/document load).
1087
+ // Only register local file paths; never register URIs (http/https) - PDFDocument needs file path.
1088
+ NSString *pathToRegister = nil;
1089
+ if (_lastLoadedPath.length > 0) {
1090
+ pathToRegister = _lastLoadedPath;
1091
+ } else if (_path.length > 0 && [_path hasPrefix:@"/"]) {
1092
+ pathToRegister = _path;
1093
+ }
1094
+ if (_pdfId.length && pathToRegister.length > 0) {
1095
+ [SearchRegistry registerPath:_pdfId path:pathToRegister];
1096
+ RCTLogInfo(@"✅ [iOS] SearchRegistry registered path for pdfId: %@ (from setPdfId)", _pdfId);
1097
+ }
1098
+ }
1099
+
1100
+ - (void)setHighlightRects:(NSArray *)highlightRects {
1101
+ _highlightRects = [highlightRects copy];
1102
+ if (!_pdfView) return;
1103
+ if (_highlightRects.count > 0) {
1104
+ if (!_highlightOverlay) {
1105
+ _highlightOverlay = [[HighlightOverlayView alloc] initWithFrame:_pdfView.bounds];
1106
+ _highlightOverlay.backgroundColor = [UIColor clearColor];
1107
+ _highlightOverlay.userInteractionEnabled = NO;
1108
+ _highlightOverlay.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
1109
+ _highlightOverlay.pdfView = _pdfView;
1110
+ [_pdfView addSubview:_highlightOverlay];
1111
+ [_pdfView bringSubviewToFront:_highlightOverlay];
1112
+ }
1113
+ _highlightOverlay.highlightRects = _highlightRects;
1114
+ [_highlightOverlay setNeedsDisplay];
1115
+ } else if (_highlightOverlay) {
1116
+ _highlightOverlay.highlightRects = @[];
1117
+ [_highlightOverlay setNeedsDisplay];
1118
+ }
1119
+ }
1120
+
959
1121
  -(NSString *) getTableContents
960
1122
  {
961
1123
 
@@ -1073,6 +1235,7 @@ using namespace facebook::react;
1073
1235
 
1074
1236
  RLog(@"Enhanced PDF: Navigated to page %d", _page);
1075
1237
  [self notifyOnChangeWithMessage:[[NSString alloc] initWithString:[NSString stringWithFormat:@"pageChanged|%lu|%lu", page+1, numberOfPages]]];
1238
+ if (_highlightOverlay) [_highlightOverlay setNeedsDisplay];
1076
1239
  }
1077
1240
 
1078
1241
  }
@@ -1087,6 +1250,7 @@ using namespace facebook::react;
1087
1250
  [self notifyOnChangeWithMessage:[[NSString alloc] initWithString:[NSString stringWithFormat:@"scaleChanged|%f", _scale]]];
1088
1251
  }
1089
1252
  }
1253
+ if (_highlightOverlay) [_highlightOverlay setNeedsDisplay];
1090
1254
  }
1091
1255
 
1092
1256
  #pragma mark gesture process
@@ -1328,6 +1492,18 @@ using namespace facebook::react;
1328
1492
  // Keep vertical bounce enabled for natural scrolling feel
1329
1493
  scrollView.bounces = YES;
1330
1494
 
1495
+ // Configure scroll view zoom scales to match PDFView's scale factors
1496
+ // This enables native pinch-to-zoom gestures
1497
+ if (_fixScaleFactor > 0) {
1498
+ scrollView.minimumZoomScale = _fixScaleFactor * _minScale;
1499
+ scrollView.maximumZoomScale = _fixScaleFactor * _maxScale;
1500
+ scrollView.zoomScale = _pdfView.scaleFactor;
1501
+ RCTLogInfo(@"🔍 [iOS Zoom] Configured zoom scales - min=%f, max=%f, current=%f",
1502
+ scrollView.minimumZoomScale,
1503
+ scrollView.maximumZoomScale,
1504
+ scrollView.zoomScale);
1505
+ }
1506
+
1331
1507
  RCTLogInfo(@"📊 [iOS Scroll] ScrollView config - scrollEnabled=%d, alwaysBounceHorizontal=%d, bounces=%d, delegate=%@",
1332
1508
  scrollView.scrollEnabled,
1333
1509
  scrollView.alwaysBounceHorizontal,
@@ -1336,23 +1512,30 @@ using namespace facebook::react;
1336
1512
 
1337
1513
  // IMPORTANT: PDFKit relies on the scrollView delegate for pinch-zoom (viewForZoomingInScrollView).
1338
1514
  // Install a proxy delegate that forwards to the original delegate, while still letting us observe scroll events.
1339
- if (!_internalScrollView) {
1340
- RCTLogInfo(@"✅ [iOS Scroll] Setting internal scroll view reference");
1515
+ // CRITICAL FIX: Always set up delegate for new scroll views (PDFView may recreate scroll view on document load)
1516
+ if (!_internalScrollView || _internalScrollView != scrollView) {
1517
+ RCTLogInfo(@"✅ [iOS Scroll] Setting up scroll view delegate (new=%d)", _internalScrollView == nil);
1341
1518
  _internalScrollView = scrollView;
1342
- if (scrollView.delegate && scrollView.delegate != self) {
1343
- _originalScrollDelegate = scrollView.delegate;
1344
- RCTLogInfo(@"📝 [iOS Scroll] Stored original scroll delegate");
1519
+
1520
+ // Get the current delegate (might be PDFView's internal delegate)
1521
+ id<UIScrollViewDelegate> currentDelegate = scrollView.delegate;
1522
+
1523
+ // Only capture original delegate if it's not us or our proxy
1524
+ if (currentDelegate && currentDelegate != self && ![currentDelegate isKindOfClass:[RNPDFScrollViewDelegateProxy class]]) {
1525
+ _originalScrollDelegate = currentDelegate;
1526
+ RCTLogInfo(@"📝 [iOS Scroll] Captured original scroll delegate: %@", NSStringFromClass([currentDelegate class]));
1345
1527
  }
1528
+
1346
1529
  if (_originalScrollDelegate) {
1347
1530
  _scrollDelegateProxy = [[RNPDFScrollViewDelegateProxy alloc] initWithPrimary:_originalScrollDelegate secondary:(id<UIScrollViewDelegate>)self];
1348
1531
  scrollView.delegate = (id<UIScrollViewDelegate>)_scrollDelegateProxy;
1349
1532
  RCTLogInfo(@"🔗 [iOS Scroll] Installed scroll delegate proxy");
1350
1533
  } else {
1351
1534
  scrollView.delegate = self;
1352
- RCTLogInfo(@"🔗 [iOS Scroll] Set self as scroll delegate");
1535
+ RCTLogInfo(@"🔗 [iOS Scroll] Set self as scroll delegate (no original delegate)");
1353
1536
  }
1354
1537
  } else {
1355
- RCTLogInfo(@"⚠️ [iOS Scroll] Internal scroll view already set, skipping delegate setup");
1538
+ RCTLogInfo(@"⚠️ [iOS Scroll] Same scroll view, delegate already configured");
1356
1539
  }
1357
1540
  }
1358
1541
 
@@ -1371,9 +1554,11 @@ using namespace facebook::react;
1371
1554
  #pragma mark - UIScrollViewDelegate
1372
1555
 
1373
1556
  - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
1557
+ // Redraw highlight overlay so rects stay aligned when user scrolls (pan)
1558
+ if (_highlightOverlay) [_highlightOverlay setNeedsDisplay];
1374
1559
  static int scrollEventCount = 0;
1375
1560
  scrollEventCount++;
1376
-
1561
+
1377
1562
  // Log scroll events periodically (every 10th event to avoid spam)
1378
1563
  if (scrollEventCount % 10 == 0) {
1379
1564
  RCTLogInfo(@"📜 [iOS Scroll] scrollViewDidScroll #%d - offset=(%.2f, %.2f), contentSize=(%.2f, %.2f), bounds=(%.2f, %.2f), scrollEnabled=%d",
@@ -1440,6 +1625,59 @@ using namespace facebook::react;
1440
1625
  }
1441
1626
  }
1442
1627
 
1628
+ #pragma mark - UIScrollViewDelegate Zoom Support
1629
+
1630
+ - (void)scrollViewDidZoom:(UIScrollView *)scrollView {
1631
+ // Called during pinch-to-zoom — redraw highlight overlay so rects stay aligned with zoomed content
1632
+ if (_highlightOverlay) [_highlightOverlay setNeedsDisplay];
1633
+ if (_fixScaleFactor > 0 && _pdfView.scaleFactor > 0) {
1634
+ float newScale = _pdfView.scaleFactor / _fixScaleFactor;
1635
+
1636
+ // Only notify if scale changed significantly (prevent spam)
1637
+ if (fabs(_scale - newScale) > 0.01f) {
1638
+ _scale = newScale;
1639
+ RCTLogInfo(@"🔍 [iOS Zoom] Pinch zoom - scale changed to %f", _scale);
1640
+ [self notifyOnChangeWithMessage:[[NSString alloc] initWithString:
1641
+ [NSString stringWithFormat:@"scaleChanged|%f", _scale]]];
1642
+ }
1643
+ }
1644
+ }
1645
+
1646
+ // CRITICAL: Return the view that should be zoomed
1647
+ - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
1648
+ // Search for PDFDocumentView in the scroll view's hierarchy
1649
+ for (UIView *subview in scrollView.subviews) {
1650
+ NSString *className = NSStringFromClass([subview class]);
1651
+ if ([className containsString:@"PDFDocumentView"] || [className containsString:@"PDFPage"]) {
1652
+ RCTLogInfo(@"🔍 [iOS Zoom] viewForZoomingInScrollView returning: %@", className);
1653
+ return subview;
1654
+ }
1655
+ }
1656
+
1657
+ // Fallback to first subview
1658
+ UIView *fallback = scrollView.subviews.firstObject;
1659
+ if (fallback) {
1660
+ RCTLogInfo(@"🔍 [iOS Zoom] viewForZoomingInScrollView using fallback: %@", NSStringFromClass([fallback class]));
1661
+ return fallback;
1662
+ }
1663
+
1664
+ RCTLogInfo(@"⚠️ [iOS Zoom] viewForZoomingInScrollView returning NIL - no view to zoom!");
1665
+ return nil;
1666
+ }
1667
+
1668
+ - (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view {
1669
+ // Optional: Track zoom start
1670
+ RCTLogInfo(@"🔍 [iOS Zoom] Will begin zooming");
1671
+ }
1672
+
1673
+ - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView
1674
+ withView:(UIView *)view
1675
+ atScale:(CGFloat)scale {
1676
+ // Redraw highlight overlay so rects match final zoom level
1677
+ if (_highlightOverlay) [_highlightOverlay setNeedsDisplay];
1678
+ RCTLogInfo(@"🔍 [iOS Zoom] Did end zooming at scale %f", scale);
1679
+ }
1680
+
1443
1681
  // Enhanced progressive loading methods
1444
1682
  - (void)preloadAdjacentPages:(int)currentPage
1445
1683
  {
@@ -52,6 +52,8 @@ RCT_EXPORT_VIEW_PROPERTY(spacing, int);
52
52
  RCT_EXPORT_VIEW_PROPERTY(password, NSString);
53
53
  RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock);
54
54
  RCT_EXPORT_VIEW_PROPERTY(singlePage, BOOL);
55
+ RCT_EXPORT_VIEW_PROPERTY(pdfId, NSString);
56
+ RCT_EXPORT_VIEW_PROPERTY(highlightRects, NSArray);
55
57
 
56
58
  RCT_EXPORT_METHOD(supportPDFKit:(RCTResponseSenderBlock)callback)
57
59
  {
@@ -177,12 +179,14 @@ RCT_EXPORT_METHOD(optimizeMemory:(NSString *)pdfId
177
179
 
178
180
  RCT_EXPORT_METHOD(searchTextDirect:(NSString *)pdfId
179
181
  searchTerm:(NSString *)searchTerm
182
+ startPage:(NSInteger)startPage
183
+ endPage:(NSInteger)endPage
180
184
  resolver:(RCTPromiseResolveBlock)resolve
181
185
  rejecter:(RCTPromiseRejectBlock)reject)
182
186
  {
183
187
  PDFJSIManager *jsiManager = [self.bridge moduleForClass:[PDFJSIManager class]];
184
188
  if (jsiManager) {
185
- [jsiManager searchTextDirect:pdfId searchTerm:searchTerm startPage:1 endPage:999 resolver:resolve rejecter:reject];
189
+ [jsiManager searchTextDirect:pdfId searchTerm:searchTerm startPage:startPage endPage:endPage resolver:resolve rejecter:reject];
186
190
  } else {
187
191
  reject(@"JSI_NOT_AVAILABLE", @"PDFJSIManager not available", nil);
188
192
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Registry mapping pdfId to current PDF file path for programmatic search.
3
+ * RNPDFPdfView registers when a document loads with pdfId; searchTextDirect looks up path by pdfId.
4
+ * Also stores PDF page sizes in points (per pdfId + pageIndex) for highlight coordinate scaling.
5
+ */
6
+ #import <Foundation/Foundation.h>
7
+
8
+ NS_ASSUME_NONNULL_BEGIN
9
+
10
+ @interface SearchRegistry : NSObject
11
+
12
+ + (void)registerPath:(NSString *)pdfId path:(NSString *)path;
13
+ + (void)unregisterPath:(NSString *)pdfId;
14
+ + (nullable NSString *)pathForPdfId:(NSString *)pdfId;
15
+
16
+ + (void)registerPageSizePointsForPdfId:(NSString *)pdfId pageIndex0Based:(NSInteger)pageIndex widthPt:(CGFloat)widthPt heightPt:(CGFloat)heightPt;
17
+ + (void)getPageSizePointsForPdfId:(NSString *)pdfId pageIndex0Based:(NSInteger)pageIndex widthOut:(CGFloat *)widthOut heightOut:(CGFloat *)heightOut;
18
+
19
+ @end
20
+
21
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Registry mapping pdfId to current PDF file path for programmatic search.
3
+ */
4
+ #import "SearchRegistry.h"
5
+
6
+ @implementation SearchRegistry
7
+
8
+ static NSMutableDictionary<NSString *, NSString *> *_pathByPdfId;
9
+ static NSMutableDictionary<NSString *, NSValue *> *_pageSizeByKey; // key = "pdfId_pageIndex", value = NSValue with CGSize
10
+ static dispatch_queue_t _queue;
11
+
12
+ + (void)initialize {
13
+ if (self == [SearchRegistry class]) {
14
+ _pathByPdfId = [NSMutableDictionary new];
15
+ _pageSizeByKey = [NSMutableDictionary new];
16
+ _queue = dispatch_queue_create("com.rnpdf.searchregistry", DISPATCH_QUEUE_SERIAL);
17
+ }
18
+ }
19
+
20
+ + (void)registerPath:(NSString *)pdfId path:(NSString *)path {
21
+ if (!pdfId.length || !path.length) return;
22
+ dispatch_sync(_queue, ^{
23
+ _pathByPdfId[pdfId] = path;
24
+ });
25
+ }
26
+
27
+ + (void)unregisterPath:(NSString *)pdfId {
28
+ if (!pdfId.length) return;
29
+ dispatch_sync(_queue, ^{
30
+ [_pathByPdfId removeObjectForKey:pdfId];
31
+ NSString *prefix = [pdfId stringByAppendingString:@"_"];
32
+ NSArray *keysToRemove = [_pageSizeByKey.allKeys filteredArrayUsingPredicate:
33
+ [NSPredicate predicateWithBlock:^BOOL(NSString *key, id _) { return [key hasPrefix:prefix]; }]];
34
+ [_pageSizeByKey removeObjectsForKeys:keysToRemove];
35
+ });
36
+ }
37
+
38
+ + (NSString *)pathForPdfId:(NSString *)pdfId {
39
+ if (!pdfId.length) return nil;
40
+ __block NSString *path = nil;
41
+ dispatch_sync(_queue, ^{
42
+ path = _pathByPdfId[pdfId];
43
+ });
44
+ return path;
45
+ }
46
+
47
+ + (void)registerPageSizePointsForPdfId:(NSString *)pdfId pageIndex0Based:(NSInteger)pageIndex widthPt:(CGFloat)widthPt heightPt:(CGFloat)heightPt {
48
+ if (!pdfId.length || widthPt <= 0 || heightPt <= 0) return;
49
+ NSString *key = [NSString stringWithFormat:@"%@_%ld", pdfId, (long)pageIndex];
50
+ dispatch_sync(_queue, ^{
51
+ _pageSizeByKey[key] = [NSValue valueWithCGSize:CGSizeMake(widthPt, heightPt)];
52
+ });
53
+ }
54
+
55
+ + (void)getPageSizePointsForPdfId:(NSString *)pdfId pageIndex0Based:(NSInteger)pageIndex widthOut:(CGFloat *)widthOut heightOut:(CGFloat *)heightOut {
56
+ if (!pdfId.length || !widthOut || !heightOut) return;
57
+ *widthOut = 0;
58
+ *heightOut = 0;
59
+ NSString *key = [NSString stringWithFormat:@"%@_%ld", pdfId, (long)pageIndex];
60
+ __block NSValue *val = nil;
61
+ dispatch_sync(_queue, ^{
62
+ val = _pageSizeByKey[key];
63
+ });
64
+ if (val) {
65
+ CGSize s = [val CGSizeValue];
66
+ *widthOut = s.width;
67
+ *heightOut = s.height;
68
+ }
69
+ }
70
+
71
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-pdf-jsi",
3
- "version": "4.3.1",
3
+ "version": "4.4.0",
4
4
  "summary": "High-performance React Native PDF viewer with JSI acceleration - up to 80x faster than traditional bridge",
5
5
  "description": "🚀 Ultra-fast React Native PDF viewer with JSI (JavaScript Interface) integration for maximum performance. Features lazy loading, smart caching, progressive loading, and zero-bridge overhead operations. Perfect for large PDF files with 30-day persistent cache and advanced memory optimization. Google Play 16KB page size compliant for Android 15+. Supports iOS, Android, and Windows platforms.",
6
6
  "main": "index.js",
@@ -80,7 +80,9 @@
80
80
  },
81
81
  "scripts": {
82
82
  "build:plugin": "tsc -p plugin/tsconfig.json",
83
- "prepublishOnly": "npm run build:plugin"
83
+ "prepublishOnly": "npm run build:plugin",
84
+ "clean:for-publish": "node -e \"const fs=require('fs'),path=require('path');['android/.cxx','android/.gradle','android/build'].forEach(p=>{try{fs.rmSync(path.join(__dirname,p),{recursive:true});console.log('Removed',p);}catch(e){}})\"",
85
+ "prepack": "npm run clean:for-publish"
84
86
  },
85
87
  "peerDependencies": {
86
88
  "@react-native-async-storage/async-storage": ">=1.17.0",