react-native-pdf-jsi 4.3.2 → 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 (35) hide show
  1. package/README.md +27 -1
  2. package/android/src/main/java/org/wonday/pdf/PDFJSIManager.java +138 -3
  3. package/android/src/main/java/org/wonday/pdf/PdfManager.java +10 -0
  4. package/android/src/main/java/org/wonday/pdf/PdfView.java +110 -3
  5. package/android/src/main/java/org/wonday/pdf/SearchRegistry.java +44 -0
  6. package/android/src/main/jniLibs/arm64-v8a/libpdfjsi.so +0 -0
  7. package/android/src/main/jniLibs/armeabi-v7a/libpdfjsi.so +0 -0
  8. package/android/src/main/jniLibs/x86/libpdfjsi.so +0 -0
  9. package/android/src/main/jniLibs/x86_64/libpdfjsi.so +0 -0
  10. package/fabric/RNPDFPdfNativeComponent.js +2 -0
  11. package/index.d.ts +34 -0
  12. package/index.js +29 -2
  13. package/ios/RNPDFPdf/PDFJSIManager.m +89 -4
  14. package/ios/RNPDFPdf/RNPDFPdfView.h +2 -0
  15. package/ios/RNPDFPdf/RNPDFPdfView.mm +127 -4
  16. package/ios/RNPDFPdf/RNPDFPdfViewManager.mm +5 -1
  17. package/ios/RNPDFPdf/SearchRegistry.h +21 -0
  18. package/ios/RNPDFPdf/SearchRegistry.m +71 -0
  19. package/package.json +4 -2
  20. package/src/PDFJSI.js +1 -1
  21. package/android/.gradle/5.6.1/fileChanges/last-build.bin +0 -0
  22. package/android/.gradle/5.6.1/fileHashes/fileHashes.lock +0 -0
  23. package/android/.gradle/5.6.1/gc.properties +0 -0
  24. package/android/.gradle/8.5/checksums/checksums.lock +0 -0
  25. package/android/.gradle/8.5/checksums/md5-checksums.bin +0 -0
  26. package/android/.gradle/8.5/checksums/sha1-checksums.bin +0 -0
  27. package/android/.gradle/8.5/dependencies-accessors/dependencies-accessors.lock +0 -0
  28. package/android/.gradle/8.5/dependencies-accessors/gc.properties +0 -0
  29. package/android/.gradle/8.5/executionHistory/executionHistory.lock +0 -0
  30. package/android/.gradle/8.5/fileChanges/last-build.bin +0 -0
  31. package/android/.gradle/8.5/fileHashes/fileHashes.lock +0 -0
  32. package/android/.gradle/8.5/gc.properties +0 -0
  33. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  34. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  35. package/android/.gradle/vcs-1/gc.properties +0 -0
package/README.md CHANGED
@@ -41,7 +41,7 @@ High-performance React Native PDF viewer with JSI (JavaScript Interface) acceler
41
41
  - **Export Operations**: Export pages to PNG/JPEG with quality control
42
42
  - **PDF Operations**: Split, merge, extract, rotate, and delete pages
43
43
  - **PDF Compression**: Reduce file sizes with 5 smart presets (EMAIL, WEB, MOBILE, PRINT, ARCHIVE)
44
- - **Text Extraction**: Extract and search text with statistics and context
44
+ - **Text Extraction & Search**: Extract and search text with statistics and context; **programmatic search** via `searchTextDirect(pdfId, term, startPage, endPage)` with bounding rects, and **highlight rendering** via `pdfId` + `highlightRects` props (Android & iOS)
45
45
  - **File Management** (Android): Download to public storage, open folders with MediaStore API
46
46
 
47
47
  ### Compliance & Compatibility
@@ -261,6 +261,8 @@ The documentation includes:
261
261
  | `enableAnnotationRendering` | boolean | true | Enable annotation rendering |
262
262
  | `enableDoubleTapZoom` | boolean | true | Enable double tap to zoom |
263
263
  | `singlePage` | boolean | false | Show only first page (thumbnail mode) |
264
+ | `pdfId` | string | undefined | Stable ID for this PDF (e.g. `"main-pdf"`); required for `searchTextDirect()` so native code can resolve the document path |
265
+ | `highlightRects` | array | undefined | Array of `{ page: number, rect: string }` (rect: `"left,top,right,bottom"` in PDF points) to draw yellow highlights; use with `searchTextDirect()` results |
264
266
  | `trustAllCerts` | boolean | true | Allow self-signed certificates |
265
267
  | `onLoadProgress` | function(percent) | null | Loading progress callback (0-1) |
266
268
  | `onLoadComplete` | function(pages, path, size, tableContents) | null | Called when PDF loads |
@@ -304,6 +306,30 @@ const pdfRef = useRef(null);
304
306
  pdfRef.current?.setPage(42);
305
307
  ```
306
308
 
309
+ #### searchTextDirect(pdfId, searchTerm, startPage, endPage)
310
+
311
+ Programmatic PDF text search. Returns a promise that resolves to an array of `{ page, text, rect }` (rect is `"left,top,right,bottom"` in PDF coordinates). Use with `pdfId` and `highlightRects` to show highlights.
312
+
313
+ ```jsx
314
+ import Pdf, { searchTextDirect } from 'react-native-pdf-jsi';
315
+
316
+ const PDF_ID = 'main-pdf';
317
+ const [highlights, setHighlights] = useState([]);
318
+
319
+ <Pdf
320
+ pdfId={PDF_ID}
321
+ source={source}
322
+ highlightRects={highlights.filter(r => r.rect).map(r => ({ page: r.page, rect: r.rect }))}
323
+ onLoadComplete={(pages, path) => { /* path is registered for search */ }}
324
+ />
325
+
326
+ // After PDF has loaded, e.g. on button press:
327
+ const results = await searchTextDirect(PDF_ID, 'Lorem', 1, 999);
328
+ setHighlights(results);
329
+ ```
330
+
331
+ On iOS, the path is registered when the document loads (local file only); you can also call `NativeModules.PDFJSIManager.registerPathForSearch(pdfId, path)` after `onLoadComplete` if needed. Highlights stay aligned when zooming and scrolling on both Android and iOS.
332
+
307
333
  ## ProGuard / R8 Configuration (Android Release Builds)
308
334
 
309
335
  **IMPORTANT:** If you're using ProGuard or R8 code shrinking in your release builds, you must add the following rules to prevent crashes. These rules preserve JSI classes and native module interfaces that are required at runtime.
@@ -18,15 +18,27 @@ import com.facebook.react.bridge.ReactMethod;
18
18
  import com.facebook.react.bridge.Promise;
19
19
  import com.facebook.react.bridge.ReadableMap;
20
20
  import com.facebook.react.bridge.ReadableArray;
21
+ import com.facebook.react.bridge.WritableArray;
21
22
  import com.facebook.react.bridge.WritableMap;
22
23
  import com.facebook.react.bridge.Arguments;
23
24
 
24
25
  // import com.facebook.react.turbomodule.core.CallInvokerHolder; // Not available in this RN version
25
26
  import com.facebook.soloader.SoLoader;
26
27
 
28
+ import java.io.File;
29
+ import java.io.IOException;
27
30
  import java.util.concurrent.ExecutorService;
28
31
  import java.util.concurrent.Executors;
29
32
 
33
+ import android.graphics.RectF;
34
+ import android.net.Uri;
35
+ import android.os.ParcelFileDescriptor;
36
+
37
+ import io.legere.pdfiumandroid.PdfiumCore;
38
+ import io.legere.pdfiumandroid.PdfDocument;
39
+ import io.legere.pdfiumandroid.PdfPage;
40
+ import io.legere.pdfiumandroid.PdfTextPage;
41
+
30
42
  public class PDFJSIManager extends ReactContextBaseJavaModule {
31
43
  private static final String MODULE_NAME = "PDFJSIManager";
32
44
  private static final String TAG = "PDFJSI";
@@ -220,9 +232,24 @@ public class PDFJSIManager extends ReactContextBaseJavaModule {
220
232
  }
221
233
  });
222
234
  }
235
+
236
+ /**
237
+ * Register a path for search by pdfId. Called from JS when loadComplete fires so search works
238
+ * even if the native view has not received pdfId yet. On Android the view also registers; this is for parity with iOS.
239
+ */
240
+ @ReactMethod
241
+ public void registerPathForSearch(String pdfId, String path, Promise promise) {
242
+ if (pdfId != null && !pdfId.isEmpty() && path != null && !path.isEmpty()) {
243
+ SearchRegistry.registerPath(pdfId, path);
244
+ promise.resolve(true);
245
+ } else {
246
+ promise.resolve(false);
247
+ }
248
+ }
223
249
 
224
250
  /**
225
- * Search text directly via JSI
251
+ * Search text directly via JSI.
252
+ * Uses SearchRegistry to get path for pdfId, then io.legere PdfiumCore to extract text and find matches.
226
253
  */
227
254
  @ReactMethod
228
255
  public void searchTextDirect(String pdfId, String searchTerm, int startPage, int endPage, Promise promise) {
@@ -230,11 +257,20 @@ public class PDFJSIManager extends ReactContextBaseJavaModule {
230
257
  promise.reject("JSI_NOT_INITIALIZED", "JSI is not initialized");
231
258
  return;
232
259
  }
233
-
260
+ if (searchTerm == null || searchTerm.isEmpty()) {
261
+ promise.resolve(Arguments.createArray());
262
+ return;
263
+ }
234
264
  backgroundExecutor.execute(() -> {
235
265
  try {
236
266
  Log.d(TAG, "Searching text via JSI: '" + searchTerm + "' in pages " + startPage + "-" + endPage);
237
- ReadableArray results = nativeSearchTextDirect(pdfId, searchTerm, startPage, endPage);
267
+ String path = SearchRegistry.getPath(pdfId);
268
+ if (path == null || path.isEmpty()) {
269
+ Log.w(TAG, "No path registered for pdfId: " + pdfId + " - pass pdfId to Pdf view to enable search");
270
+ promise.resolve(Arguments.createArray());
271
+ return;
272
+ }
273
+ WritableArray results = searchInPdf(pdfId, path, searchTerm, startPage, endPage);
238
274
  promise.resolve(results);
239
275
  } catch (Exception e) {
240
276
  Log.e(TAG, "Error searching text via JSI", e);
@@ -242,6 +278,105 @@ public class PDFJSIManager extends ReactContextBaseJavaModule {
242
278
  }
243
279
  });
244
280
  }
281
+
282
+ private WritableArray searchInPdf(String pdfId, String path, String searchTerm, int startPage, int endPage) {
283
+ WritableArray out = Arguments.createArray();
284
+ ParcelFileDescriptor pfd = null;
285
+ PdfDocument doc = null;
286
+ try {
287
+ if (path.startsWith("content://")) {
288
+ pfd = getReactApplicationContext().getContentResolver()
289
+ .openFileDescriptor(Uri.parse(path), "r");
290
+ } else {
291
+ File file = new File(path);
292
+ if (!file.exists() || !file.canRead()) {
293
+ return out;
294
+ }
295
+ pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
296
+ }
297
+ if (pfd == null) return out;
298
+ PdfiumCore core = new PdfiumCore();
299
+ doc = core.newDocument(pfd);
300
+ int pageCount = doc.getPageCount();
301
+ int from = Math.max(1, startPage);
302
+ int to = Math.min(endPage, pageCount);
303
+ String termLower = searchTerm.toLowerCase();
304
+ for (int pageIndex = from; pageIndex <= to; pageIndex++) {
305
+ int zeroBased = pageIndex - 1;
306
+ PdfPage page = doc.openPage(zeroBased);
307
+ if (page == null) continue;
308
+ try {
309
+ // Store page size in PDF points for highlight scaling in PdfView
310
+ try {
311
+ int wPt = page.getPageWidthPoint();
312
+ int hPt = page.getPageHeightPoint();
313
+ if (wPt > 0 && hPt > 0) {
314
+ SearchRegistry.registerPageSizePoints(pdfId, zeroBased, (float) wPt, (float) hPt);
315
+ }
316
+ } catch (Exception ignored) {}
317
+ PdfTextPage textPage = page.openTextPage();
318
+ if (textPage == null) continue;
319
+ try {
320
+ int chars = textPage.textPageCountChars();
321
+ if (chars <= 0) continue;
322
+ String text = textPage.textPageGetText(0, chars);
323
+ if (text == null) continue;
324
+ String textLower = text.toLowerCase();
325
+ int idx = 0;
326
+ while ((idx = textLower.indexOf(termLower, idx)) >= 0) {
327
+ int end = Math.min(idx + searchTerm.length(), text.length());
328
+ int len = end - idx;
329
+ String snippet = text.substring(idx, end);
330
+ WritableMap item = Arguments.createMap();
331
+ item.putInt("page", pageIndex);
332
+ item.putString("text", snippet);
333
+ String rectStr = "{}";
334
+ try {
335
+ int rectCount = textPage.textPageCountRects(idx, len);
336
+ if (rectCount > 0) {
337
+ RectF first = textPage.textPageGetRect(0);
338
+ if (first != null) {
339
+ rectStr = first.left + "," + first.top + "," + first.right + "," + first.bottom;
340
+ }
341
+ }
342
+ if ("{}".equals(rectStr)) {
343
+ RectF charBox = textPage.textPageGetCharBox(idx);
344
+ if (charBox != null) {
345
+ rectStr = charBox.left + "," + charBox.top + "," + charBox.right + "," + charBox.bottom;
346
+ }
347
+ }
348
+ } catch (Exception e) {
349
+ Log.d(TAG, "Rect lookup for match at " + idx + ": " + e.getMessage());
350
+ }
351
+ item.putString("rect", rectStr);
352
+ out.pushMap(item);
353
+ idx = end;
354
+ }
355
+ } finally {
356
+ textPage.close();
357
+ }
358
+ } finally {
359
+ page.close();
360
+ }
361
+ }
362
+ } catch (IOException e) {
363
+ Log.e(TAG, "Search IO error", e);
364
+ } catch (Exception e) {
365
+ Log.e(TAG, "Search error", e);
366
+ } finally {
367
+ if (doc != null) {
368
+ try {
369
+ doc.close();
370
+ } catch (Exception ignored) {}
371
+ }
372
+ if (pfd != null) {
373
+ try {
374
+ pfd.close();
375
+ } catch (IOException ignored) {}
376
+ }
377
+ }
378
+ return out;
379
+ }
245
380
 
246
381
  /**
247
382
  * Get performance metrics via JSI
@@ -69,6 +69,16 @@ public class PdfManager extends SimpleViewManager<PdfView> implements RNPDFPdfVi
69
69
  pdfView.setPath(path);
70
70
  }
71
71
 
72
+ @ReactProp(name = "pdfId")
73
+ public void setPdfId(PdfView pdfView, String pdfId) {
74
+ pdfView.setPdfId(pdfId);
75
+ }
76
+
77
+ @ReactProp(name = "highlightRects")
78
+ public void setHighlightRects(PdfView pdfView, ReadableArray highlightRects) {
79
+ pdfView.setHighlightRects(highlightRects);
80
+ }
81
+
72
82
  // page start from 1
73
83
  @ReactProp(name = "page")
74
84
  public void setPage(PdfView pdfView, int page) {
@@ -20,7 +20,12 @@ import android.net.Uri;
20
20
  import android.util.AttributeSet;
21
21
  import android.view.MotionEvent;
22
22
  import android.graphics.Canvas;
23
+ import android.graphics.Color;
24
+ import android.graphics.Paint;
23
25
  import android.os.Handler;
26
+
27
+ import com.facebook.react.bridge.ReadableArray;
28
+ import com.facebook.react.bridge.ReadableMap;
24
29
  import android.os.Looper;
25
30
 
26
31
 
@@ -52,6 +57,8 @@ import static java.lang.String.format;
52
57
 
53
58
  import java.io.FileNotFoundException;
54
59
  import java.io.InputStream;
60
+ import java.util.ArrayList;
61
+ import java.util.List;
55
62
 
56
63
  import com.google.gson.Gson;
57
64
 
@@ -77,7 +84,8 @@ public class PdfView extends PDFView implements OnPageChangeListener,OnLoadCompl
77
84
  private FitPolicy fitPolicy = FitPolicy.WIDTH;
78
85
  private boolean singlePage = false;
79
86
  private boolean scrollEnabled = true;
80
-
87
+ private String pdfId = null;
88
+
81
89
  private String decelerationRate = "normal"; // "normal", "fast", "slow"
82
90
 
83
91
  private float originalWidth = 0;
@@ -94,8 +102,29 @@ public class PdfView extends PDFView implements OnPageChangeListener,OnLoadCompl
94
102
  private int oldW = 0;
95
103
  private int oldH = 0;
96
104
 
105
+ /** Search highlight rects: list of { page (1-based), rect "left,top,right,bottom" in PDF points } */
106
+ private List<HighlightRect> highlightRects = new ArrayList<>();
107
+ private static final int HIGHLIGHT_COLOR = Color.argb(80, 255, 255, 0);
108
+ private final Paint highlightPaint = new Paint();
109
+
97
110
  public PdfView(Context context, AttributeSet set){
98
111
  super(context, set);
112
+ highlightPaint.setColor(HIGHLIGHT_COLOR);
113
+ highlightPaint.setStyle(Paint.Style.FILL);
114
+ }
115
+
116
+ /** Entry for one highlight: page (1-based) and rect in PDF points "left,top,right,bottom". */
117
+ private static class HighlightRect {
118
+ final int page;
119
+ final float left, top, right, bottom;
120
+
121
+ HighlightRect(int page, float left, float top, float right, float bottom) {
122
+ this.page = page;
123
+ this.left = left;
124
+ this.top = top;
125
+ this.right = right;
126
+ this.bottom = bottom;
127
+ }
99
128
  }
100
129
 
101
130
  @Override
@@ -225,6 +254,9 @@ public class PdfView extends PDFView implements OnPageChangeListener,OnLoadCompl
225
254
  self.loadCompleteDispatched = true;
226
255
  showLog("loadComplete: Event dispatched successfully (delayed)");
227
256
  }
257
+ if (self.pdfId != null && self.path != null) {
258
+ SearchRegistry.registerPath(self.pdfId, self.path);
259
+ }
228
260
  }
229
261
  });
230
262
  } else {
@@ -340,6 +372,42 @@ public class PdfView extends PDFView implements OnPageChangeListener,OnLoadCompl
340
372
 
341
373
  lastPageWidth = pageWidth;
342
374
  lastPageHeight = pageHeight;
375
+
376
+ if (!highlightRects.isEmpty() && pdfId != null) {
377
+ int pageOneBased = displayedPage + 1;
378
+ try {
379
+ float pdfW = 0, pdfH = 0;
380
+ float[] sizePt = SearchRegistry.getPageSizePoints(pdfId, displayedPage);
381
+ if (sizePt != null && sizePt.length >= 2 && sizePt[0] > 0 && sizePt[1] > 0) {
382
+ pdfW = sizePt[0];
383
+ pdfH = sizePt[1];
384
+ }
385
+ if (pdfW <= 0 || pdfH <= 0) {
386
+ SizeF fallback = getPageSize(displayedPage);
387
+ if (fallback != null) {
388
+ pdfW = fallback.getWidth();
389
+ pdfH = fallback.getHeight();
390
+ }
391
+ }
392
+ if (pdfW > 0 && pdfH > 0) {
393
+ float scaleX = pageWidth / pdfW;
394
+ float scaleY = pageHeight / pdfH;
395
+ for (HighlightRect hr : highlightRects) {
396
+ if (hr.page != pageOneBased) continue;
397
+ float left = hr.left * scaleX;
398
+ float right = hr.right * scaleX;
399
+ float top = hr.top * scaleY;
400
+ float bottom = hr.bottom * scaleY;
401
+ // PDF coords: origin bottom-left, so top > bottom. Canvas: origin top-left.
402
+ float canvasTop = pageHeight - top;
403
+ float canvasBottom = pageHeight - bottom;
404
+ canvas.drawRect(left, canvasTop, right, canvasBottom, highlightPaint);
405
+ }
406
+ }
407
+ } catch (Exception e) {
408
+ showLog("Highlight draw error: " + e.getMessage());
409
+ }
410
+ }
343
411
  }
344
412
 
345
413
  @Override
@@ -361,6 +429,14 @@ public class PdfView extends PDFView implements OnPageChangeListener,OnLoadCompl
361
429
  // the PDF on navigation. We only recycle when PdfManager.onDropViewInstance is called.
362
430
  }
363
431
 
432
+ @Override
433
+ public void recycle() {
434
+ if (pdfId != null) {
435
+ SearchRegistry.unregisterPath(pdfId);
436
+ }
437
+ super.recycle();
438
+ }
439
+
364
440
  public void drawPdf() {
365
441
 
366
442
  // FIX: Check if we actually need to reload the document
@@ -444,14 +520,45 @@ public class PdfView extends PDFView implements OnPageChangeListener,OnLoadCompl
444
520
  }
445
521
 
446
522
  public void setPath(String path) {
447
- // Path changed - need to reload document
523
+ if (pdfId != null) {
524
+ SearchRegistry.unregisterPath(pdfId);
525
+ }
448
526
  needsReload = true;
449
527
  this.path = path;
450
- // Reset flags when path changes
451
528
  loadCompleteDispatched = false;
452
529
  lastKnownPageCount = 0;
453
530
  }
454
531
 
532
+ public void setPdfId(String pdfId) {
533
+ if (this.pdfId != null && !this.pdfId.equals(pdfId)) {
534
+ SearchRegistry.unregisterPath(this.pdfId);
535
+ }
536
+ this.pdfId = pdfId;
537
+ }
538
+
539
+ public void setHighlightRects(ReadableArray arr) {
540
+ highlightRects.clear();
541
+ if (arr == null) return;
542
+ for (int i = 0; i < arr.size(); i++) {
543
+ ReadableMap map = arr.getMap(i);
544
+ if (map == null || !map.hasKey("page") || !map.hasKey("rect")) continue;
545
+ int page = map.getInt("page");
546
+ String rectStr = map.getString("rect");
547
+ if (rectStr == null || rectStr.equals("{}")) continue;
548
+ String[] parts = rectStr.split(",");
549
+ if (parts.length != 4) continue;
550
+ try {
551
+ float left = Float.parseFloat(parts[0].trim());
552
+ float top = Float.parseFloat(parts[1].trim());
553
+ float right = Float.parseFloat(parts[2].trim());
554
+ float bottom = Float.parseFloat(parts[3].trim());
555
+ highlightRects.add(new HighlightRect(page, left, top, right, bottom));
556
+ } catch (NumberFormatException ignored) {}
557
+ }
558
+ invalidate();
559
+ postInvalidate();
560
+ }
561
+
455
562
  // page start from 1
456
563
  public void setPage(int page) {
457
564
  int newPage = page>1?page:1;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Registry mapping pdfId to current PDF file path for programmatic search.
3
+ * PdfView 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
+ package org.wonday.pdf;
7
+
8
+ import java.util.concurrent.ConcurrentHashMap;
9
+
10
+ public final class SearchRegistry {
11
+ private static final ConcurrentHashMap<String, String> pdfIdToPath = new ConcurrentHashMap<>();
12
+ /** Key: pdfId + "_" + pageIndex0Based, value: float[2] = { widthPt, heightPt } */
13
+ private static final ConcurrentHashMap<String, float[]> pdfIdPageSizePoints = new ConcurrentHashMap<>();
14
+
15
+ public static void registerPath(String pdfId, String path) {
16
+ if (pdfId != null && !pdfId.isEmpty() && path != null && !path.isEmpty()) {
17
+ pdfIdToPath.put(pdfId, path);
18
+ }
19
+ }
20
+
21
+ public static void unregisterPath(String pdfId) {
22
+ if (pdfId != null && !pdfId.isEmpty()) {
23
+ pdfIdToPath.remove(pdfId);
24
+ // Clear page sizes for this pdfId
25
+ pdfIdPageSizePoints.keySet().removeIf(k -> k != null && k.startsWith(pdfId + "_"));
26
+ }
27
+ }
28
+
29
+ public static String getPath(String pdfId) {
30
+ return pdfId == null ? null : pdfIdToPath.get(pdfId);
31
+ }
32
+
33
+ /** Register page size in PDF points (for highlight scaling). */
34
+ public static void registerPageSizePoints(String pdfId, int pageIndex0Based, float widthPt, float heightPt) {
35
+ if (pdfId != null && !pdfId.isEmpty() && widthPt > 0 && heightPt > 0) {
36
+ pdfIdPageSizePoints.put(pdfId + "_" + pageIndex0Based, new float[] { widthPt, heightPt });
37
+ }
38
+ }
39
+
40
+ /** Get page size in PDF points; returns float[2] = { widthPt, heightPt } or null. */
41
+ public static float[] getPageSizePoints(String pdfId, int pageIndex0Based) {
42
+ return pdfId == null ? null : pdfIdPageSizePoints.get(pdfId + "_" + pageIndex0Based);
43
+ }
44
+ }
@@ -37,6 +37,8 @@
37
37
  password: ?string,
38
38
  onChange: ?BubblingEventHandler<ChangeEvent>,
39
39
  singlePage: ?boolean,
40
+ pdfId: ?string,
41
+ highlightRects: ?$ReadOnlyArray<$ReadOnly<{|page: Int32, rect: string|}>>,
40
42
  |}>;
41
43
 
42
44
  interface NativeCommands {
package/index.d.ts CHANGED
@@ -88,6 +88,17 @@ export interface PdfProps {
88
88
  onPageSingleTap?: (page: number, x: number, y: number) => void,
89
89
  onScaleChanged?: (scale: number) => void,
90
90
  onPressLink?: (url: string) => void,
91
+ /**
92
+ * Optional. When set, use this id with searchTextDirect(pdfId, ...) for programmatic text search.
93
+ * On iOS, the same id must be passed to the Pdf view and to searchTextDirect. One view per pdfId.
94
+ */
95
+ pdfId?: string,
96
+ /**
97
+ * Optional. Array of rects to highlight on the PDF (e.g. from searchTextDirect results).
98
+ * Each item: { page: number, rect: string } where rect is "left,top,right,bottom" in PDF page coordinates.
99
+ * Supported on Android; iOS can be added later.
100
+ */
101
+ highlightRects?: Array<{ page: number; rect: string }>,
91
102
  }
92
103
 
93
104
  declare class Pdf extends React.Component<PdfProps, any> {
@@ -463,3 +474,26 @@ export interface PDFCompressorManager {
463
474
  * PDFCompressor singleton instance
464
475
  */
465
476
  export const PDFCompressor: PDFCompressorManager;
477
+
478
+ // ========================================
479
+ // JSI / Programmatic search
480
+ // ========================================
481
+
482
+ export interface PDFSearchResultItem {
483
+ page: number;
484
+ text: string;
485
+ /** Bounds of the match (format is platform-specific; on iOS a CGRect string). */
486
+ rect: string;
487
+ }
488
+
489
+ /**
490
+ * Search PDF text programmatically.
491
+ * On iOS: pass the same pdfId to the Pdf view (pdfId prop) so the view is registered for search.
492
+ * On Android: returns empty array until a native text-extraction implementation is added.
493
+ */
494
+ export function searchTextDirect(
495
+ pdfId: string,
496
+ searchTerm: string,
497
+ startPage: number,
498
+ endPage: number
499
+ ): Promise<PDFSearchResultItem[]>;
package/index.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  StyleSheet,
19
19
  Image,
20
20
  Text,
21
+ NativeModules,
21
22
  requireNativeComponent
22
23
  } from 'react-native';
23
24
  // Codegen component variables - will be loaded lazily to prevent hooks errors
@@ -27,7 +28,7 @@ import ReactNativeBlobUtil from 'react-native-blob-util'
27
28
  import {ViewPropTypes} from 'deprecated-react-native-prop-types';
28
29
  const SHA1 = require('crypto-js/sha1');
29
30
  import PdfView from './PdfView';
30
- import PDFJSI from './src/PDFJSI';
31
+ import PDFJSI, { searchTextDirect } from './src/PDFJSI';
31
32
 
32
33
  export default class Pdf extends Component {
33
34
 
@@ -67,6 +68,8 @@ export default class Pdf extends Component {
67
68
  onPageSingleTap: PropTypes.func,
68
69
  onScaleChanged: PropTypes.func,
69
70
  onPressLink: PropTypes.func,
71
+ pdfId: PropTypes.string,
72
+ highlightRects: PropTypes.arrayOf(PropTypes.shape({ page: PropTypes.number.isRequired, rect: PropTypes.string.isRequired })),
70
73
 
71
74
  // Props that are not available in the earlier react native version, added to prevent crashed on android
72
75
  accessibilityLabel: PropTypes.string,
@@ -111,6 +114,8 @@ export default class Pdf extends Component {
111
114
  },
112
115
  onPressLink: (url) => {
113
116
  },
117
+ pdfId: undefined,
118
+ highlightRects: undefined,
114
119
  };
115
120
 
116
121
  constructor(props) {
@@ -594,7 +599,24 @@ export default class Pdf extends Component {
594
599
  if (!filePath || filePath.trim() === '') {
595
600
  filePath = this.downloadedFilePath || this.state.path || '';
596
601
  }
597
-
602
+ // Register path for search (iOS: ensures SearchRegistry has path when pdfId may not reach native view)
603
+ if (this.props.pdfId && filePath) {
604
+ const PDFJSIManager = NativeModules.PDFJSIManager;
605
+ if (PDFJSIManager && typeof PDFJSIManager.registerPathForSearch === 'function') {
606
+ if (__DEV__) {
607
+ console.log('📌 [Pdf] Registering path for search:', this.props.pdfId, 'pathLength:', filePath.length);
608
+ }
609
+ PDFJSIManager.registerPathForSearch(this.props.pdfId, filePath).then(() => {
610
+ if (__DEV__) console.log('✅ [Pdf] Path registered for search:', this.props.pdfId);
611
+ }).catch((err) => {
612
+ if (__DEV__) console.warn('⚠️ [Pdf] registerPathForSearch failed:', err);
613
+ });
614
+ } else if (__DEV__) {
615
+ console.warn('⚠️ [Pdf] PDFJSIManager.registerPathForSearch not available');
616
+ }
617
+ } else if (__DEV__) {
618
+ console.log('📌 [Pdf] Skip path registration: pdfId=', this.props.pdfId, 'hasPath=', !!filePath);
619
+ }
598
620
  // Log path extraction for debugging
599
621
  if (__DEV__) {
600
622
  console.log('📁 [Pdf] loadComplete - Path extraction:', {
@@ -758,6 +780,11 @@ export {
758
780
  CompressionLevel
759
781
  };
760
782
 
783
+ // ========================================
784
+ // Programmatic search and JSI API (fix #24)
785
+ // ========================================
786
+ export { searchTextDirect, PDFJSI };
787
+
761
788
  // ========================================
762
789
  // TIER 3: Pre-built UI Components
763
790
  // ========================================
@@ -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
@@ -181,6 +213,13 @@ const float MIN_SCALE = 1.0f;
181
213
  // Track usePageViewController state to prevent unnecessary reconfiguration
182
214
  BOOL _currentUsePageViewController;
183
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;
184
223
  }
185
224
 
186
225
  #ifdef RCT_NEW_ARCH_ENABLED
@@ -315,6 +354,30 @@ using namespace facebook::react;
315
354
  _scrollEnabled = newProps.scrollEnabled;
316
355
  [updatedPropNames addObject:@"scrollEnabled"];
317
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"];
318
381
 
319
382
  [super updateProps:props oldProps:oldProps];
320
383
  [self didSetProps:updatedPropNames];
@@ -329,10 +392,11 @@ using namespace facebook::react;
329
392
  - (void)prepareForRecycle
330
393
  {
331
394
  [super prepareForRecycle];
332
-
395
+ if (_pdfId.length) [SearchRegistry unregisterPath:_pdfId];
333
396
  [_pdfView removeFromSuperview];
334
397
  _pdfDocument = Nil;
335
398
  _pdfView = Nil;
399
+ _highlightOverlay = Nil;
336
400
  //Remove notifications
337
401
  [[NSNotificationCenter defaultCenter] removeObserver:self name:@"PDFViewDocumentChangedNotification" object:nil];
338
402
  [[NSNotificationCenter defaultCenter] removeObserver:self name:@"PDFViewPageChangedNotification" object:nil];
@@ -442,6 +506,11 @@ using namespace facebook::react;
442
506
  _preloadQueue.maxConcurrentOperationCount = 3;
443
507
  _preloadQueue.qualityOfService = NSQualityOfServiceBackground;
444
508
 
509
+ _pdfId = nil;
510
+ _highlightRects = nil;
511
+ _lastLoadedPath = nil;
512
+ _highlightOverlay = nil;
513
+
445
514
  // init and config PDFView
446
515
  _pdfView = [[PDFView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)];
447
516
  _pdfView.displayMode = kPDFDisplaySinglePageContinuous;
@@ -997,10 +1066,58 @@ using namespace facebook::react;
997
1066
  RCTLogInfo(@"🔍 [iOS] loadComplete message: %@", message);
998
1067
 
999
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
+ }
1000
1077
  }
1001
1078
 
1002
1079
  }
1003
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
+
1004
1121
  -(NSString *) getTableContents
1005
1122
  {
1006
1123
 
@@ -1118,6 +1235,7 @@ using namespace facebook::react;
1118
1235
 
1119
1236
  RLog(@"Enhanced PDF: Navigated to page %d", _page);
1120
1237
  [self notifyOnChangeWithMessage:[[NSString alloc] initWithString:[NSString stringWithFormat:@"pageChanged|%lu|%lu", page+1, numberOfPages]]];
1238
+ if (_highlightOverlay) [_highlightOverlay setNeedsDisplay];
1121
1239
  }
1122
1240
 
1123
1241
  }
@@ -1132,6 +1250,7 @@ using namespace facebook::react;
1132
1250
  [self notifyOnChangeWithMessage:[[NSString alloc] initWithString:[NSString stringWithFormat:@"scaleChanged|%f", _scale]]];
1133
1251
  }
1134
1252
  }
1253
+ if (_highlightOverlay) [_highlightOverlay setNeedsDisplay];
1135
1254
  }
1136
1255
 
1137
1256
  #pragma mark gesture process
@@ -1435,9 +1554,11 @@ using namespace facebook::react;
1435
1554
  #pragma mark - UIScrollViewDelegate
1436
1555
 
1437
1556
  - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
1557
+ // Redraw highlight overlay so rects stay aligned when user scrolls (pan)
1558
+ if (_highlightOverlay) [_highlightOverlay setNeedsDisplay];
1438
1559
  static int scrollEventCount = 0;
1439
1560
  scrollEventCount++;
1440
-
1561
+
1441
1562
  // Log scroll events periodically (every 10th event to avoid spam)
1442
1563
  if (scrollEventCount % 10 == 0) {
1443
1564
  RCTLogInfo(@"📜 [iOS Scroll] scrollViewDidScroll #%d - offset=(%.2f, %.2f), contentSize=(%.2f, %.2f), bounds=(%.2f, %.2f), scrollEnabled=%d",
@@ -1507,7 +1628,8 @@ using namespace facebook::react;
1507
1628
  #pragma mark - UIScrollViewDelegate Zoom Support
1508
1629
 
1509
1630
  - (void)scrollViewDidZoom:(UIScrollView *)scrollView {
1510
- // Called during pinch-to-zoom
1631
+ // Called during pinch-to-zoom — redraw highlight overlay so rects stay aligned with zoomed content
1632
+ if (_highlightOverlay) [_highlightOverlay setNeedsDisplay];
1511
1633
  if (_fixScaleFactor > 0 && _pdfView.scaleFactor > 0) {
1512
1634
  float newScale = _pdfView.scaleFactor / _fixScaleFactor;
1513
1635
 
@@ -1551,7 +1673,8 @@ using namespace facebook::react;
1551
1673
  - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView
1552
1674
  withView:(UIView *)view
1553
1675
  atScale:(CGFloat)scale {
1554
- // Optional: Track zoom end
1676
+ // Redraw highlight overlay so rects match final zoom level
1677
+ if (_highlightOverlay) [_highlightOverlay setNeedsDisplay];
1555
1678
  RCTLogInfo(@"🔍 [iOS Zoom] Did end zooming at scale %f", scale);
1556
1679
  }
1557
1680
 
@@ -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.2",
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",
package/src/PDFJSI.js CHANGED
@@ -392,7 +392,7 @@ class PDFJSIManager {
392
392
  if (Platform.OS === 'android') {
393
393
  results = await PDFJSIManagerNative.searchTextDirect(pdfId, searchTerm, startPage, endPage);
394
394
  } else if (Platform.OS === 'ios') {
395
- results = await RNPDFPdfViewManager.searchTextDirect(pdfId, searchTerm);
395
+ results = await RNPDFPdfViewManager.searchTextDirect(pdfId, searchTerm, startPage, endPage);
396
396
  } else {
397
397
  throw new Error(`Platform ${Platform.OS} not supported`);
398
398
  }
File without changes
File without changes
@@ -1,2 +0,0 @@
1
- #Thu Oct 02 20:04:14 IST 2025
2
- gradle.version=8.5
File without changes