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
package/PdfView.js CHANGED
@@ -393,7 +393,7 @@ export default class PdfView extends Component {
393
393
  renderScrollComponent={(props) => <ScrollView
394
394
  {...props}
395
395
  centerContent={this.state.centerContent}
396
- pinchGestureEnabled={false}
396
+ pinchGestureEnabled={true}
397
397
  />}
398
398
  initialScrollIndex={this.props.page < 1 ? 0 : this.props.page - 1}
399
399
  onViewableItemsChanged={this._onViewableItemsChanged}
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.
@@ -457,6 +483,9 @@ MIT License - see [LICENSE](LICENSE) file for details.
457
483
 
458
484
  ## Recent Fixes
459
485
 
486
+ ### iOS Pinch-to-Zoom (v4.3.2)
487
+ Fixed iOS pinch-to-zoom not working when the scroll view delegate was set to the view itself or when the delegate proxy's primary didn't implement `viewForZoomingInScrollView`. Implemented the missing `viewForZoomingInScrollView:` in RNPDFPdfView so the scroll view receives the correct zoomable view. Fixes [#23](https://github.com/126punith/react-native-pdf-jsi/issues/23) (PR [#22](https://github.com/126punith/react-native-pdf-jsi/pull/22)).
488
+
460
489
  ### Android PDF Preserved on Navigation (v4.3.1)
461
490
  Fixed issue where the PDF instance was destroyed on Android when navigating away and returning to the screen ([#20](https://github.com/126punith/react-native-pdf-jsi/issues/20)). The PDF is now preserved in memory during navigation (matching iOS behavior) and only recycled when the component unmounts.
462
491
 
@@ -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
  // ========================================