@papyrus-sdk/engine-native 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/LICENSE +21 -0
  2. package/android/build.gradle +40 -40
  3. package/android/src/main/AndroidManifest.xml +3 -3
  4. package/android/src/main/java/com/papyrus/engine/PapyrusEngineStore.java +71 -71
  5. package/android/src/main/java/com/papyrus/engine/PapyrusNativeEngineModule.java +529 -529
  6. package/android/src/main/java/com/papyrus/engine/PapyrusNativeEngineModule.kt +540 -0
  7. package/android/src/main/java/com/papyrus/engine/PapyrusOutline.java +20 -20
  8. package/android/src/main/java/com/papyrus/engine/PapyrusOutlineItem.java +13 -13
  9. package/android/src/main/java/com/papyrus/engine/PapyrusPackage.java +24 -24
  10. package/android/src/main/java/com/papyrus/engine/PapyrusPageView.java +86 -86
  11. package/android/src/main/java/com/papyrus/engine/PapyrusPageViewManager.java +16 -16
  12. package/android/src/main/java/com/papyrus/engine/PapyrusPageViewModule.kt +12 -0
  13. package/android/src/main/java/com/papyrus/engine/PapyrusTextHit.java +15 -15
  14. package/android/src/main/java/com/papyrus/engine/PapyrusTextSearch.java +20 -20
  15. package/android/src/main/java/com/papyrus/engine/PapyrusTextSelect.java +20 -20
  16. package/android/src/main/java/com/papyrus/engine/PapyrusTextSelection.java +11 -11
  17. package/dist/index.d.mts +8 -7
  18. package/dist/index.d.ts +8 -7
  19. package/dist/index.js +200 -8
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +201 -9
  22. package/dist/index.mjs.map +1 -1
  23. package/ios/PapyrusNativeEngine.podspec +1 -1
  24. package/ios/PapyrusPageViewManager.m +19 -19
  25. package/package.json +30 -30
  26. package/react-native.config.js +10 -10
@@ -1,529 +1,529 @@
1
- package com.papyrus.engine;
2
-
3
- import android.content.ContentResolver;
4
- import android.content.Context;
5
- import android.net.Uri;
6
- import android.os.ParcelFileDescriptor;
7
- import android.view.View;
8
-
9
- import com.facebook.react.bridge.Arguments;
10
- import com.facebook.react.bridge.Promise;
11
- import com.facebook.react.bridge.ReactApplicationContext;
12
- import com.facebook.react.bridge.ReactContextBaseJavaModule;
13
- import com.facebook.react.bridge.ReactMethod;
14
- import com.facebook.react.bridge.ReadableArray;
15
- import com.facebook.react.bridge.ReadableMap;
16
- import com.facebook.react.bridge.WritableArray;
17
- import com.facebook.react.bridge.WritableMap;
18
- import com.facebook.react.bridge.ReadableType;
19
- import com.facebook.react.uimanager.UIBlock;
20
- import com.facebook.react.uimanager.UIManagerModule;
21
-
22
- import com.shockwave.pdfium.PdfDocument;
23
-
24
- import java.io.File;
25
- import java.io.FileOutputStream;
26
- import java.io.IOException;
27
- import java.io.InputStream;
28
- import java.net.HttpURLConnection;
29
- import java.net.URL;
30
- import java.util.concurrent.ExecutorService;
31
- import java.util.concurrent.Executors;
32
-
33
- public class PapyrusNativeEngineModule extends ReactContextBaseJavaModule {
34
- private final ReactApplicationContext reactContext;
35
- private final ExecutorService executor = Executors.newSingleThreadExecutor();
36
-
37
- public PapyrusNativeEngineModule(ReactApplicationContext reactContext) {
38
- super(reactContext);
39
- this.reactContext = reactContext;
40
- }
41
-
42
- @Override
43
- public String getName() {
44
- return "PapyrusNativeEngine";
45
- }
46
-
47
- @ReactMethod(isBlockingSynchronousMethod = true)
48
- public String createEngine() {
49
- return PapyrusEngineStore.createEngine(reactContext);
50
- }
51
-
52
- @ReactMethod
53
- public void destroyEngine(String engineId) {
54
- PapyrusEngineStore.destroyEngine(engineId);
55
- }
56
-
57
- @ReactMethod
58
- public void load(final String engineId, final ReadableMap source, final Promise promise) {
59
- executor.execute(() -> {
60
- try {
61
- PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
62
- if (state == null) {
63
- promise.reject("papyrus_no_engine", "Engine not found");
64
- return;
65
- }
66
-
67
- File file = materializeSource(source, reactContext);
68
- if (file == null) {
69
- promise.reject("papyrus_invalid_source", "Unsupported PDF source");
70
- return;
71
- }
72
-
73
- ParcelFileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
74
- PdfDocument document = state.pdfium.newDocument(fd);
75
- PapyrusEngineStore.setDocument(state, document, fd, file.getAbsolutePath());
76
-
77
- int pageCount = state.pdfium.getPageCount(document);
78
- WritableMap result = Arguments.createMap();
79
- result.putInt("pageCount", pageCount);
80
- promise.resolve(result);
81
- } catch (Throwable error) {
82
- promise.reject("papyrus_load_failed", error);
83
- }
84
- });
85
- }
86
-
87
- @ReactMethod(isBlockingSynchronousMethod = true)
88
- public int getPageCount(String engineId) {
89
- PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
90
- if (state == null || state.document == null) return 0;
91
- return state.pdfium.getPageCount(state.document);
92
- }
93
-
94
- @ReactMethod
95
- public void renderPage(final String engineId, final int pageIndex, final int target, final float scale, final float zoom, final int rotation) {
96
- final PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
97
- if (state == null) return;
98
- UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class);
99
- if (uiManager == null) return;
100
-
101
- uiManager.addUIBlock(new UIBlock() {
102
- @Override
103
- public void execute(com.facebook.react.uimanager.NativeViewHierarchyManager nativeViewHierarchyManager) {
104
- View view = nativeViewHierarchyManager.resolveView(target);
105
- if (view instanceof PapyrusPageView) {
106
- ((PapyrusPageView) view).render(state, pageIndex, scale, zoom, rotation);
107
- }
108
- }
109
- });
110
- }
111
-
112
- @ReactMethod
113
- public void renderTextLayer(String engineId, int pageIndex, int target, float scale, float zoom, int rotation) {
114
- }
115
-
116
- @ReactMethod
117
- public void getTextContent(String engineId, int pageIndex, Promise promise) {
118
- PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
119
- if (state == null || state.document == null) {
120
- promise.resolve(Arguments.createArray());
121
- return;
122
- }
123
- String text;
124
- synchronized (state.pdfiumLock) {
125
- text = extractPageText(state, pageIndex);
126
- }
127
- WritableArray items = Arguments.createArray();
128
- if (text != null && !text.isEmpty()) {
129
- WritableMap item = Arguments.createMap();
130
- item.putString("str", text);
131
- item.putString("dir", "ltr");
132
- item.putDouble("width", 0);
133
- item.putDouble("height", 0);
134
- WritableArray transform = Arguments.createArray();
135
- transform.pushDouble(1);
136
- transform.pushDouble(0);
137
- transform.pushDouble(0);
138
- transform.pushDouble(1);
139
- transform.pushDouble(0);
140
- transform.pushDouble(0);
141
- item.putArray("transform", transform);
142
- item.putString("fontName", "");
143
- items.pushMap(item);
144
- }
145
- promise.resolve(items);
146
- }
147
-
148
- @ReactMethod
149
- public void getPageDimensions(String engineId, int pageIndex, Promise promise) {
150
- PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
151
- if (state == null || state.document == null) {
152
- WritableMap result = Arguments.createMap();
153
- result.putInt("width", 0);
154
- result.putInt("height", 0);
155
- promise.resolve(result);
156
- return;
157
- }
158
- int width;
159
- int height;
160
- synchronized (state.pdfiumLock) {
161
- width = state.pdfium.getPageWidthPoint(state.document, pageIndex);
162
- height = state.pdfium.getPageHeightPoint(state.document, pageIndex);
163
- }
164
- WritableMap result = Arguments.createMap();
165
- result.putInt("width", width);
166
- result.putInt("height", height);
167
- promise.resolve(result);
168
- }
169
-
170
- @ReactMethod
171
- public void getOutline(String engineId, Promise promise) {
172
- executor.execute(() -> {
173
- PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
174
- if (state == null || state.document == null) {
175
- promise.resolve(Arguments.createArray());
176
- return;
177
- }
178
-
179
- PapyrusOutlineItem[] items = null;
180
- try {
181
- if (PapyrusOutline.AVAILABLE) {
182
- if (state.sourcePath != null && !state.sourcePath.isEmpty()) {
183
- synchronized (state.pdfiumLock) {
184
- items = PapyrusOutline.nativeGetOutlineFile(state.sourcePath);
185
- }
186
- } else {
187
- long docPtr;
188
- synchronized (state.pdfiumLock) {
189
- docPtr = extractNativeDocPointer(state.document);
190
- }
191
- if (docPtr != 0) {
192
- synchronized (state.pdfiumLock) {
193
- items = PapyrusOutline.nativeGetOutline(docPtr);
194
- }
195
- }
196
- }
197
- }
198
- } catch (Throwable ignored) {
199
- items = null;
200
- }
201
-
202
- WritableArray result = Arguments.createArray();
203
- if (items != null) {
204
- for (PapyrusOutlineItem item : items) {
205
- result.pushMap(serializeOutlineItem(item));
206
- }
207
- }
208
- promise.resolve(result);
209
- });
210
- }
211
-
212
- @ReactMethod
213
- public void getPageIndex(String engineId, Object dest, Promise promise) {
214
- promise.resolve(null);
215
- }
216
-
217
- @ReactMethod
218
- public void searchText(String engineId, String query, Promise promise) {
219
- executor.execute(() -> {
220
- PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
221
- if (state == null || state.document == null || query == null || query.length() < 2) {
222
- promise.resolve(Arguments.createArray());
223
- return;
224
- }
225
-
226
- int pageCount = state.pdfium.getPageCount(state.document);
227
- state.isSearching = true;
228
- try {
229
- try {
230
- if (PapyrusTextSearch.AVAILABLE) {
231
- PapyrusTextHit[] hits = null;
232
- if (state.sourcePath != null && !state.sourcePath.isEmpty()) {
233
- synchronized (state.pdfiumLock) {
234
- hits = PapyrusTextSearch.nativeSearchFile(state.sourcePath, query);
235
- }
236
- } else {
237
- long docPtr;
238
- synchronized (state.pdfiumLock) {
239
- docPtr = extractNativeDocPointer(state.document);
240
- }
241
- if (docPtr != 0) {
242
- synchronized (state.pdfiumLock) {
243
- hits = PapyrusTextSearch.nativeSearch(docPtr, pageCount, query);
244
- }
245
- }
246
- }
247
-
248
- if (hits != null && hits.length > 0) {
249
- WritableArray results = Arguments.createArray();
250
- for (PapyrusTextHit hit : hits) {
251
- WritableMap result = Arguments.createMap();
252
- result.putInt("pageIndex", hit.pageIndex);
253
- result.putString("text", hit.text != null ? hit.text : query);
254
- result.putInt("matchIndex", hit.matchIndex);
255
- if (hit.rects != null && hit.rects.length >= 4) {
256
- WritableArray rects = Arguments.createArray();
257
- for (int i = 0; i + 3 < hit.rects.length; i += 4) {
258
- WritableMap rect = Arguments.createMap();
259
- rect.putDouble("x", hit.rects[i]);
260
- rect.putDouble("y", hit.rects[i + 1]);
261
- rect.putDouble("width", hit.rects[i + 2]);
262
- rect.putDouble("height", hit.rects[i + 3]);
263
- rects.pushMap(rect);
264
- }
265
- result.putArray("rects", rects);
266
- }
267
- results.pushMap(result);
268
- }
269
- promise.resolve(results);
270
- return;
271
- }
272
- }
273
- } catch (Throwable ignored) {
274
- }
275
-
276
- String normalizedQuery = query.toLowerCase();
277
- WritableArray results = Arguments.createArray();
278
-
279
- for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
280
- String text;
281
- synchronized (state.pdfiumLock) {
282
- text = extractPageText(state, pageIndex);
283
- }
284
- if (text == null || text.isEmpty()) continue;
285
-
286
- String lower = text.toLowerCase();
287
- int pos = lower.indexOf(normalizedQuery);
288
- int matchIndex = 0;
289
- while (pos != -1) {
290
- int start = Math.max(0, pos - 20);
291
- int end = Math.min(text.length(), pos + normalizedQuery.length() + 20);
292
- String preview = text.substring(start, end);
293
-
294
- WritableMap result = Arguments.createMap();
295
- result.putInt("pageIndex", pageIndex);
296
- result.putString("text", preview);
297
- result.putInt("matchIndex", matchIndex++);
298
- results.pushMap(result);
299
-
300
- pos = lower.indexOf(normalizedQuery, pos + 1);
301
- }
302
- }
303
-
304
- promise.resolve(results);
305
- } finally {
306
- state.isSearching = false;
307
- }
308
- });
309
- }
310
-
311
- @ReactMethod
312
- public void selectText(String engineId, int pageIndex, double x, double y, double width, double height, Promise promise) {
313
- executor.execute(() -> {
314
- PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
315
- if (state == null || state.document == null || pageIndex < 0) {
316
- promise.resolve(null);
317
- return;
318
- }
319
-
320
- if (!PapyrusTextSelect.AVAILABLE) {
321
- promise.resolve(null);
322
- return;
323
- }
324
-
325
- PapyrusTextSelection selection = null;
326
- try {
327
- if (state.sourcePath != null && !state.sourcePath.isEmpty()) {
328
- synchronized (state.pdfiumLock) {
329
- selection = PapyrusTextSelect.nativeSelectTextFile(state.sourcePath, pageIndex, (float) x, (float) y, (float) width, (float) height);
330
- }
331
- } else {
332
- long docPtr;
333
- synchronized (state.pdfiumLock) {
334
- docPtr = extractNativeDocPointer(state.document);
335
- }
336
- if (docPtr != 0) {
337
- synchronized (state.pdfiumLock) {
338
- selection = PapyrusTextSelect.nativeSelectText(docPtr, pageIndex, (float) x, (float) y, (float) width, (float) height);
339
- }
340
- }
341
- }
342
- } catch (Throwable ignored) {
343
- selection = null;
344
- }
345
-
346
- if (selection == null || selection.rects == null || selection.rects.length == 0) {
347
- promise.resolve(null);
348
- return;
349
- }
350
-
351
- WritableMap result = Arguments.createMap();
352
- result.putString("text", selection.text != null ? selection.text : "");
353
- WritableArray rects = Arguments.createArray();
354
- for (int i = 0; i + 3 < selection.rects.length; i += 4) {
355
- WritableMap rect = Arguments.createMap();
356
- rect.putDouble("x", selection.rects[i]);
357
- rect.putDouble("y", selection.rects[i + 1]);
358
- rect.putDouble("width", selection.rects[i + 2]);
359
- rect.putDouble("height", selection.rects[i + 3]);
360
- rects.pushMap(rect);
361
- }
362
- result.putArray("rects", rects);
363
- promise.resolve(result);
364
- });
365
- }
366
-
367
- private String extractPageText(PapyrusEngineStore.EngineState state, int pageIndex) {
368
- try {
369
- state.pdfium.openPage(state.document, pageIndex);
370
- } catch (Throwable ignored) {
371
- }
372
-
373
- try {
374
- java.lang.reflect.Method method = null;
375
- try {
376
- method = state.pdfium.getClass().getDeclaredMethod("getPageText", PdfDocument.class, int.class);
377
- } catch (NoSuchMethodException ignored) {
378
- }
379
-
380
- if (method == null) {
381
- try {
382
- method = state.pdfium.getClass().getDeclaredMethod("nativeGetPageText", long.class, int.class);
383
- } catch (NoSuchMethodException ignored) {
384
- }
385
- }
386
-
387
- if (method != null) {
388
- method.setAccessible(true);
389
- Object result;
390
- if (method.getParameterTypes().length == 2 && method.getParameterTypes()[0] == PdfDocument.class) {
391
- result = method.invoke(state.pdfium, state.document, pageIndex);
392
- } else if (method.getParameterTypes().length == 2 && method.getParameterTypes()[0] == long.class) {
393
- long docPtr = extractNativeDocPointer(state.document);
394
- result = method.invoke(state.pdfium, docPtr, pageIndex);
395
- } else {
396
- result = null;
397
- }
398
- return result != null ? result.toString() : "";
399
- }
400
- } catch (Throwable ignored) {
401
- }
402
-
403
- return "";
404
- }
405
-
406
- private long extractNativeDocPointer(PdfDocument document) {
407
- try {
408
- java.lang.reflect.Field field = PdfDocument.class.getDeclaredField("mNativeDocPtr");
409
- field.setAccessible(true);
410
- Object value = field.get(document);
411
- if (value instanceof Long) {
412
- return (Long) value;
413
- }
414
- } catch (Throwable ignored) {
415
- }
416
- return 0;
417
- }
418
-
419
- private WritableMap serializeOutlineItem(PapyrusOutlineItem item) {
420
- WritableMap map = Arguments.createMap();
421
- map.putString("title", item.title != null ? item.title : "");
422
- map.putInt("pageIndex", item.pageIndex);
423
- if (item.children != null && item.children.length > 0) {
424
- WritableArray children = Arguments.createArray();
425
- for (PapyrusOutlineItem child : item.children) {
426
- children.pushMap(serializeOutlineItem(child));
427
- }
428
- map.putArray("children", children);
429
- }
430
- return map;
431
- }
432
-
433
- private static File materializeSource(ReadableMap source, Context context) throws IOException {
434
- if (source.hasKey("uri") && source.getType("uri") == ReadableType.String) {
435
- String uriString = source.getString("uri");
436
- if (uriString == null) return null;
437
-
438
- if (uriString.startsWith("http://") || uriString.startsWith("https://")) {
439
- return downloadToCache(uriString, context);
440
- }
441
-
442
- if (uriString.startsWith("asset:/")) {
443
- return copyFromAsset(uriString.substring("asset:/".length()), context);
444
- }
445
-
446
- if (uriString.startsWith("file:///android_asset/")) {
447
- return copyFromAsset(uriString.substring("file:///android_asset/".length()), context);
448
- }
449
-
450
- if (uriString.startsWith("content://")) {
451
- return copyFromContentUri(Uri.parse(uriString), context);
452
- }
453
-
454
- if (uriString.startsWith("file://")) {
455
- return new File(Uri.parse(uriString).getPath());
456
- }
457
-
458
- return new File(uriString);
459
- }
460
-
461
- if (source.hasKey("data") && source.getType("data") == ReadableType.Array) {
462
- ReadableArray array = source.getArray("data");
463
- if (array == null) return null;
464
- byte[] bytes = new byte[array.size()];
465
- for (int i = 0; i < array.size(); i++) {
466
- bytes[i] = (byte) array.getInt(i);
467
- }
468
- return writeBytesToCache(bytes, context);
469
- }
470
-
471
- return null;
472
- }
473
-
474
- private static File downloadToCache(String uri, Context context) throws IOException {
475
- URL url = new URL(uri);
476
- HttpURLConnection connection = (HttpURLConnection) url.openConnection();
477
- connection.connect();
478
- if (connection.getResponseCode() >= 400) {
479
- throw new IOException("Failed to download PDF");
480
- }
481
- InputStream inputStream = connection.getInputStream();
482
- File out = createTempFile(context);
483
- writeStreamToFile(inputStream, out);
484
- connection.disconnect();
485
- return out;
486
- }
487
-
488
- private static File copyFromContentUri(Uri uri, Context context) throws IOException {
489
- ContentResolver resolver = context.getContentResolver();
490
- InputStream inputStream = resolver.openInputStream(uri);
491
- if (inputStream == null) throw new IOException("Unable to read content URI");
492
- File out = createTempFile(context);
493
- writeStreamToFile(inputStream, out);
494
- return out;
495
- }
496
-
497
- private static File copyFromAsset(String assetPath, Context context) throws IOException {
498
- InputStream inputStream = context.getAssets().open(assetPath);
499
- File out = createTempFile(context);
500
- writeStreamToFile(inputStream, out);
501
- return out;
502
- }
503
-
504
- private static File writeBytesToCache(byte[] bytes, Context context) throws IOException {
505
- File out = createTempFile(context);
506
- FileOutputStream fos = new FileOutputStream(out);
507
- fos.write(bytes);
508
- fos.flush();
509
- fos.close();
510
- return out;
511
- }
512
-
513
- private static File createTempFile(Context context) throws IOException {
514
- File cacheDir = context.getCacheDir();
515
- return File.createTempFile("papyrus", ".pdf", cacheDir);
516
- }
517
-
518
- private static void writeStreamToFile(InputStream inputStream, File out) throws IOException {
519
- FileOutputStream fos = new FileOutputStream(out);
520
- byte[] buffer = new byte[8192];
521
- int read;
522
- while ((read = inputStream.read(buffer)) != -1) {
523
- fos.write(buffer, 0, read);
524
- }
525
- fos.flush();
526
- fos.close();
527
- inputStream.close();
528
- }
529
- }
1
+ package com.papyrus.engine;
2
+
3
+ import android.content.ContentResolver;
4
+ import android.content.Context;
5
+ import android.net.Uri;
6
+ import android.os.ParcelFileDescriptor;
7
+ import android.view.View;
8
+
9
+ import com.facebook.react.bridge.Arguments;
10
+ import com.facebook.react.bridge.Promise;
11
+ import com.facebook.react.bridge.ReactApplicationContext;
12
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
13
+ import com.facebook.react.bridge.ReactMethod;
14
+ import com.facebook.react.bridge.ReadableArray;
15
+ import com.facebook.react.bridge.ReadableMap;
16
+ import com.facebook.react.bridge.WritableArray;
17
+ import com.facebook.react.bridge.WritableMap;
18
+ import com.facebook.react.bridge.ReadableType;
19
+ import com.facebook.react.uimanager.UIBlock;
20
+ import com.facebook.react.uimanager.UIManagerModule;
21
+
22
+ import com.shockwave.pdfium.PdfDocument;
23
+
24
+ import java.io.File;
25
+ import java.io.FileOutputStream;
26
+ import java.io.IOException;
27
+ import java.io.InputStream;
28
+ import java.net.HttpURLConnection;
29
+ import java.net.URL;
30
+ import java.util.concurrent.ExecutorService;
31
+ import java.util.concurrent.Executors;
32
+
33
+ public class PapyrusNativeEngineModule extends ReactContextBaseJavaModule {
34
+ private final ReactApplicationContext reactContext;
35
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
36
+
37
+ public PapyrusNativeEngineModule(ReactApplicationContext reactContext) {
38
+ super(reactContext);
39
+ this.reactContext = reactContext;
40
+ }
41
+
42
+ @Override
43
+ public String getName() {
44
+ return "PapyrusNativeEngine";
45
+ }
46
+
47
+ @ReactMethod(isBlockingSynchronousMethod = true)
48
+ public String createEngine() {
49
+ return PapyrusEngineStore.createEngine(reactContext);
50
+ }
51
+
52
+ @ReactMethod
53
+ public void destroyEngine(String engineId) {
54
+ PapyrusEngineStore.destroyEngine(engineId);
55
+ }
56
+
57
+ @ReactMethod
58
+ public void load(final String engineId, final ReadableMap source, final Promise promise) {
59
+ executor.execute(() -> {
60
+ try {
61
+ PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
62
+ if (state == null) {
63
+ promise.reject("papyrus_no_engine", "Engine not found");
64
+ return;
65
+ }
66
+
67
+ File file = materializeSource(source, reactContext);
68
+ if (file == null) {
69
+ promise.reject("papyrus_invalid_source", "Unsupported PDF source");
70
+ return;
71
+ }
72
+
73
+ ParcelFileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
74
+ PdfDocument document = state.pdfium.newDocument(fd);
75
+ PapyrusEngineStore.setDocument(state, document, fd, file.getAbsolutePath());
76
+
77
+ int pageCount = state.pdfium.getPageCount(document);
78
+ WritableMap result = Arguments.createMap();
79
+ result.putInt("pageCount", pageCount);
80
+ promise.resolve(result);
81
+ } catch (Throwable error) {
82
+ promise.reject("papyrus_load_failed", error);
83
+ }
84
+ });
85
+ }
86
+
87
+ @ReactMethod(isBlockingSynchronousMethod = true)
88
+ public int getPageCount(String engineId) {
89
+ PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
90
+ if (state == null || state.document == null) return 0;
91
+ return state.pdfium.getPageCount(state.document);
92
+ }
93
+
94
+ @ReactMethod
95
+ public void renderPage(final String engineId, final int pageIndex, final int target, final float scale, final float zoom, final int rotation) {
96
+ final PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
97
+ if (state == null) return;
98
+ UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class);
99
+ if (uiManager == null) return;
100
+
101
+ uiManager.addUIBlock(new UIBlock() {
102
+ @Override
103
+ public void execute(com.facebook.react.uimanager.NativeViewHierarchyManager nativeViewHierarchyManager) {
104
+ View view = nativeViewHierarchyManager.resolveView(target);
105
+ if (view instanceof PapyrusPageView) {
106
+ ((PapyrusPageView) view).render(state, pageIndex, scale, zoom, rotation);
107
+ }
108
+ }
109
+ });
110
+ }
111
+
112
+ @ReactMethod
113
+ public void renderTextLayer(String engineId, int pageIndex, int target, float scale, float zoom, int rotation) {
114
+ }
115
+
116
+ @ReactMethod
117
+ public void getTextContent(String engineId, int pageIndex, Promise promise) {
118
+ PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
119
+ if (state == null || state.document == null) {
120
+ promise.resolve(Arguments.createArray());
121
+ return;
122
+ }
123
+ String text;
124
+ synchronized (state.pdfiumLock) {
125
+ text = extractPageText(state, pageIndex);
126
+ }
127
+ WritableArray items = Arguments.createArray();
128
+ if (text != null && !text.isEmpty()) {
129
+ WritableMap item = Arguments.createMap();
130
+ item.putString("str", text);
131
+ item.putString("dir", "ltr");
132
+ item.putDouble("width", 0);
133
+ item.putDouble("height", 0);
134
+ WritableArray transform = Arguments.createArray();
135
+ transform.pushDouble(1);
136
+ transform.pushDouble(0);
137
+ transform.pushDouble(0);
138
+ transform.pushDouble(1);
139
+ transform.pushDouble(0);
140
+ transform.pushDouble(0);
141
+ item.putArray("transform", transform);
142
+ item.putString("fontName", "");
143
+ items.pushMap(item);
144
+ }
145
+ promise.resolve(items);
146
+ }
147
+
148
+ @ReactMethod
149
+ public void getPageDimensions(String engineId, int pageIndex, Promise promise) {
150
+ PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
151
+ if (state == null || state.document == null) {
152
+ WritableMap result = Arguments.createMap();
153
+ result.putInt("width", 0);
154
+ result.putInt("height", 0);
155
+ promise.resolve(result);
156
+ return;
157
+ }
158
+ int width;
159
+ int height;
160
+ synchronized (state.pdfiumLock) {
161
+ width = state.pdfium.getPageWidthPoint(state.document, pageIndex);
162
+ height = state.pdfium.getPageHeightPoint(state.document, pageIndex);
163
+ }
164
+ WritableMap result = Arguments.createMap();
165
+ result.putInt("width", width);
166
+ result.putInt("height", height);
167
+ promise.resolve(result);
168
+ }
169
+
170
+ @ReactMethod
171
+ public void getOutline(String engineId, Promise promise) {
172
+ executor.execute(() -> {
173
+ PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
174
+ if (state == null || state.document == null) {
175
+ promise.resolve(Arguments.createArray());
176
+ return;
177
+ }
178
+
179
+ PapyrusOutlineItem[] items = null;
180
+ try {
181
+ if (PapyrusOutline.AVAILABLE) {
182
+ if (state.sourcePath != null && !state.sourcePath.isEmpty()) {
183
+ synchronized (state.pdfiumLock) {
184
+ items = PapyrusOutline.nativeGetOutlineFile(state.sourcePath);
185
+ }
186
+ } else {
187
+ long docPtr;
188
+ synchronized (state.pdfiumLock) {
189
+ docPtr = extractNativeDocPointer(state.document);
190
+ }
191
+ if (docPtr != 0) {
192
+ synchronized (state.pdfiumLock) {
193
+ items = PapyrusOutline.nativeGetOutline(docPtr);
194
+ }
195
+ }
196
+ }
197
+ }
198
+ } catch (Throwable ignored) {
199
+ items = null;
200
+ }
201
+
202
+ WritableArray result = Arguments.createArray();
203
+ if (items != null) {
204
+ for (PapyrusOutlineItem item : items) {
205
+ result.pushMap(serializeOutlineItem(item));
206
+ }
207
+ }
208
+ promise.resolve(result);
209
+ });
210
+ }
211
+
212
+ @ReactMethod
213
+ public void getPageIndex(String engineId, Object dest, Promise promise) {
214
+ promise.resolve(null);
215
+ }
216
+
217
+ @ReactMethod
218
+ public void searchText(String engineId, String query, Promise promise) {
219
+ executor.execute(() -> {
220
+ PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
221
+ if (state == null || state.document == null || query == null || query.length() < 2) {
222
+ promise.resolve(Arguments.createArray());
223
+ return;
224
+ }
225
+
226
+ int pageCount = state.pdfium.getPageCount(state.document);
227
+ state.isSearching = true;
228
+ try {
229
+ try {
230
+ if (PapyrusTextSearch.AVAILABLE) {
231
+ PapyrusTextHit[] hits = null;
232
+ if (state.sourcePath != null && !state.sourcePath.isEmpty()) {
233
+ synchronized (state.pdfiumLock) {
234
+ hits = PapyrusTextSearch.nativeSearchFile(state.sourcePath, query);
235
+ }
236
+ } else {
237
+ long docPtr;
238
+ synchronized (state.pdfiumLock) {
239
+ docPtr = extractNativeDocPointer(state.document);
240
+ }
241
+ if (docPtr != 0) {
242
+ synchronized (state.pdfiumLock) {
243
+ hits = PapyrusTextSearch.nativeSearch(docPtr, pageCount, query);
244
+ }
245
+ }
246
+ }
247
+
248
+ if (hits != null && hits.length > 0) {
249
+ WritableArray results = Arguments.createArray();
250
+ for (PapyrusTextHit hit : hits) {
251
+ WritableMap result = Arguments.createMap();
252
+ result.putInt("pageIndex", hit.pageIndex);
253
+ result.putString("text", hit.text != null ? hit.text : query);
254
+ result.putInt("matchIndex", hit.matchIndex);
255
+ if (hit.rects != null && hit.rects.length >= 4) {
256
+ WritableArray rects = Arguments.createArray();
257
+ for (int i = 0; i + 3 < hit.rects.length; i += 4) {
258
+ WritableMap rect = Arguments.createMap();
259
+ rect.putDouble("x", hit.rects[i]);
260
+ rect.putDouble("y", hit.rects[i + 1]);
261
+ rect.putDouble("width", hit.rects[i + 2]);
262
+ rect.putDouble("height", hit.rects[i + 3]);
263
+ rects.pushMap(rect);
264
+ }
265
+ result.putArray("rects", rects);
266
+ }
267
+ results.pushMap(result);
268
+ }
269
+ promise.resolve(results);
270
+ return;
271
+ }
272
+ }
273
+ } catch (Throwable ignored) {
274
+ }
275
+
276
+ String normalizedQuery = query.toLowerCase();
277
+ WritableArray results = Arguments.createArray();
278
+
279
+ for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
280
+ String text;
281
+ synchronized (state.pdfiumLock) {
282
+ text = extractPageText(state, pageIndex);
283
+ }
284
+ if (text == null || text.isEmpty()) continue;
285
+
286
+ String lower = text.toLowerCase();
287
+ int pos = lower.indexOf(normalizedQuery);
288
+ int matchIndex = 0;
289
+ while (pos != -1) {
290
+ int start = Math.max(0, pos - 20);
291
+ int end = Math.min(text.length(), pos + normalizedQuery.length() + 20);
292
+ String preview = text.substring(start, end);
293
+
294
+ WritableMap result = Arguments.createMap();
295
+ result.putInt("pageIndex", pageIndex);
296
+ result.putString("text", preview);
297
+ result.putInt("matchIndex", matchIndex++);
298
+ results.pushMap(result);
299
+
300
+ pos = lower.indexOf(normalizedQuery, pos + 1);
301
+ }
302
+ }
303
+
304
+ promise.resolve(results);
305
+ } finally {
306
+ state.isSearching = false;
307
+ }
308
+ });
309
+ }
310
+
311
+ @ReactMethod
312
+ public void selectText(String engineId, int pageIndex, double x, double y, double width, double height, Promise promise) {
313
+ executor.execute(() -> {
314
+ PapyrusEngineStore.EngineState state = PapyrusEngineStore.getEngine(engineId);
315
+ if (state == null || state.document == null || pageIndex < 0) {
316
+ promise.resolve(null);
317
+ return;
318
+ }
319
+
320
+ if (!PapyrusTextSelect.AVAILABLE) {
321
+ promise.resolve(null);
322
+ return;
323
+ }
324
+
325
+ PapyrusTextSelection selection = null;
326
+ try {
327
+ if (state.sourcePath != null && !state.sourcePath.isEmpty()) {
328
+ synchronized (state.pdfiumLock) {
329
+ selection = PapyrusTextSelect.nativeSelectTextFile(state.sourcePath, pageIndex, (float) x, (float) y, (float) width, (float) height);
330
+ }
331
+ } else {
332
+ long docPtr;
333
+ synchronized (state.pdfiumLock) {
334
+ docPtr = extractNativeDocPointer(state.document);
335
+ }
336
+ if (docPtr != 0) {
337
+ synchronized (state.pdfiumLock) {
338
+ selection = PapyrusTextSelect.nativeSelectText(docPtr, pageIndex, (float) x, (float) y, (float) width, (float) height);
339
+ }
340
+ }
341
+ }
342
+ } catch (Throwable ignored) {
343
+ selection = null;
344
+ }
345
+
346
+ if (selection == null || selection.rects == null || selection.rects.length == 0) {
347
+ promise.resolve(null);
348
+ return;
349
+ }
350
+
351
+ WritableMap result = Arguments.createMap();
352
+ result.putString("text", selection.text != null ? selection.text : "");
353
+ WritableArray rects = Arguments.createArray();
354
+ for (int i = 0; i + 3 < selection.rects.length; i += 4) {
355
+ WritableMap rect = Arguments.createMap();
356
+ rect.putDouble("x", selection.rects[i]);
357
+ rect.putDouble("y", selection.rects[i + 1]);
358
+ rect.putDouble("width", selection.rects[i + 2]);
359
+ rect.putDouble("height", selection.rects[i + 3]);
360
+ rects.pushMap(rect);
361
+ }
362
+ result.putArray("rects", rects);
363
+ promise.resolve(result);
364
+ });
365
+ }
366
+
367
+ private String extractPageText(PapyrusEngineStore.EngineState state, int pageIndex) {
368
+ try {
369
+ state.pdfium.openPage(state.document, pageIndex);
370
+ } catch (Throwable ignored) {
371
+ }
372
+
373
+ try {
374
+ java.lang.reflect.Method method = null;
375
+ try {
376
+ method = state.pdfium.getClass().getDeclaredMethod("getPageText", PdfDocument.class, int.class);
377
+ } catch (NoSuchMethodException ignored) {
378
+ }
379
+
380
+ if (method == null) {
381
+ try {
382
+ method = state.pdfium.getClass().getDeclaredMethod("nativeGetPageText", long.class, int.class);
383
+ } catch (NoSuchMethodException ignored) {
384
+ }
385
+ }
386
+
387
+ if (method != null) {
388
+ method.setAccessible(true);
389
+ Object result;
390
+ if (method.getParameterTypes().length == 2 && method.getParameterTypes()[0] == PdfDocument.class) {
391
+ result = method.invoke(state.pdfium, state.document, pageIndex);
392
+ } else if (method.getParameterTypes().length == 2 && method.getParameterTypes()[0] == long.class) {
393
+ long docPtr = extractNativeDocPointer(state.document);
394
+ result = method.invoke(state.pdfium, docPtr, pageIndex);
395
+ } else {
396
+ result = null;
397
+ }
398
+ return result != null ? result.toString() : "";
399
+ }
400
+ } catch (Throwable ignored) {
401
+ }
402
+
403
+ return "";
404
+ }
405
+
406
+ private long extractNativeDocPointer(PdfDocument document) {
407
+ try {
408
+ java.lang.reflect.Field field = PdfDocument.class.getDeclaredField("mNativeDocPtr");
409
+ field.setAccessible(true);
410
+ Object value = field.get(document);
411
+ if (value instanceof Long) {
412
+ return (Long) value;
413
+ }
414
+ } catch (Throwable ignored) {
415
+ }
416
+ return 0;
417
+ }
418
+
419
+ private WritableMap serializeOutlineItem(PapyrusOutlineItem item) {
420
+ WritableMap map = Arguments.createMap();
421
+ map.putString("title", item.title != null ? item.title : "");
422
+ map.putInt("pageIndex", item.pageIndex);
423
+ if (item.children != null && item.children.length > 0) {
424
+ WritableArray children = Arguments.createArray();
425
+ for (PapyrusOutlineItem child : item.children) {
426
+ children.pushMap(serializeOutlineItem(child));
427
+ }
428
+ map.putArray("children", children);
429
+ }
430
+ return map;
431
+ }
432
+
433
+ private static File materializeSource(ReadableMap source, Context context) throws IOException {
434
+ if (source.hasKey("uri") && source.getType("uri") == ReadableType.String) {
435
+ String uriString = source.getString("uri");
436
+ if (uriString == null) return null;
437
+
438
+ if (uriString.startsWith("http://") || uriString.startsWith("https://")) {
439
+ return downloadToCache(uriString, context);
440
+ }
441
+
442
+ if (uriString.startsWith("asset:/")) {
443
+ return copyFromAsset(uriString.substring("asset:/".length()), context);
444
+ }
445
+
446
+ if (uriString.startsWith("file:///android_asset/")) {
447
+ return copyFromAsset(uriString.substring("file:///android_asset/".length()), context);
448
+ }
449
+
450
+ if (uriString.startsWith("content://")) {
451
+ return copyFromContentUri(Uri.parse(uriString), context);
452
+ }
453
+
454
+ if (uriString.startsWith("file://")) {
455
+ return new File(Uri.parse(uriString).getPath());
456
+ }
457
+
458
+ return new File(uriString);
459
+ }
460
+
461
+ if (source.hasKey("data") && source.getType("data") == ReadableType.Array) {
462
+ ReadableArray array = source.getArray("data");
463
+ if (array == null) return null;
464
+ byte[] bytes = new byte[array.size()];
465
+ for (int i = 0; i < array.size(); i++) {
466
+ bytes[i] = (byte) array.getInt(i);
467
+ }
468
+ return writeBytesToCache(bytes, context);
469
+ }
470
+
471
+ return null;
472
+ }
473
+
474
+ private static File downloadToCache(String uri, Context context) throws IOException {
475
+ URL url = new URL(uri);
476
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
477
+ connection.connect();
478
+ if (connection.getResponseCode() >= 400) {
479
+ throw new IOException("Failed to download PDF");
480
+ }
481
+ InputStream inputStream = connection.getInputStream();
482
+ File out = createTempFile(context);
483
+ writeStreamToFile(inputStream, out);
484
+ connection.disconnect();
485
+ return out;
486
+ }
487
+
488
+ private static File copyFromContentUri(Uri uri, Context context) throws IOException {
489
+ ContentResolver resolver = context.getContentResolver();
490
+ InputStream inputStream = resolver.openInputStream(uri);
491
+ if (inputStream == null) throw new IOException("Unable to read content URI");
492
+ File out = createTempFile(context);
493
+ writeStreamToFile(inputStream, out);
494
+ return out;
495
+ }
496
+
497
+ private static File copyFromAsset(String assetPath, Context context) throws IOException {
498
+ InputStream inputStream = context.getAssets().open(assetPath);
499
+ File out = createTempFile(context);
500
+ writeStreamToFile(inputStream, out);
501
+ return out;
502
+ }
503
+
504
+ private static File writeBytesToCache(byte[] bytes, Context context) throws IOException {
505
+ File out = createTempFile(context);
506
+ FileOutputStream fos = new FileOutputStream(out);
507
+ fos.write(bytes);
508
+ fos.flush();
509
+ fos.close();
510
+ return out;
511
+ }
512
+
513
+ private static File createTempFile(Context context) throws IOException {
514
+ File cacheDir = context.getCacheDir();
515
+ return File.createTempFile("papyrus", ".pdf", cacheDir);
516
+ }
517
+
518
+ private static void writeStreamToFile(InputStream inputStream, File out) throws IOException {
519
+ FileOutputStream fos = new FileOutputStream(out);
520
+ byte[] buffer = new byte[8192];
521
+ int read;
522
+ while ((read = inputStream.read(buffer)) != -1) {
523
+ fos.write(buffer, 0, read);
524
+ }
525
+ fos.flush();
526
+ fos.close();
527
+ inputStream.close();
528
+ }
529
+ }