react-native-pdf-jsi 2.2.8 → 3.0.1

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.
@@ -0,0 +1,769 @@
1
+ package org.wonday.pdf;
2
+
3
+ import android.graphics.Bitmap;
4
+ import android.graphics.Canvas;
5
+ import android.graphics.Color;
6
+ import android.graphics.Paint;
7
+ import android.graphics.Rect;
8
+ import android.graphics.pdf.PdfRenderer;
9
+ import android.os.ParcelFileDescriptor;
10
+ import android.util.Log;
11
+
12
+ import com.facebook.react.bridge.ReactApplicationContext;
13
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
14
+ import com.facebook.react.bridge.ReactMethod;
15
+ import com.facebook.react.bridge.Promise;
16
+ import com.facebook.react.bridge.ReadableArray;
17
+ import com.facebook.react.bridge.ReadableMap;
18
+ import com.facebook.react.bridge.WritableArray;
19
+ import com.facebook.react.bridge.WritableMap;
20
+ import com.facebook.react.bridge.Arguments;
21
+
22
+ import java.io.File;
23
+ import java.io.FileOutputStream;
24
+ import java.io.IOException;
25
+ import java.util.ArrayList;
26
+ import java.util.List;
27
+ import java.util.Date;
28
+ import java.util.Locale;
29
+ import java.text.SimpleDateFormat;
30
+ import android.graphics.pdf.PdfDocument;
31
+
32
+ public class PDFExporter extends ReactContextBaseJavaModule {
33
+ private static final String TAG = "PDFExporter";
34
+ private LicenseVerifier licenseVerifier;
35
+
36
+ public PDFExporter(ReactApplicationContext reactContext) {
37
+ super(reactContext);
38
+ licenseVerifier = new LicenseVerifier(reactContext);
39
+ }
40
+
41
+ @Override
42
+ public String getName() {
43
+ return "PDFExporter";
44
+ }
45
+
46
+ /**
47
+ * Generate a unique filename with timestamp to prevent overwrites
48
+ * @param baseName Base filename without extension
49
+ * @param pageNum Page number (or -1 for PDF operations)
50
+ * @param extension File extension (png, jpeg, pdf)
51
+ * @return Filename with timestamp (e.g., "filename_page_1_20251102_181059.png")
52
+ */
53
+ private String generateTimestampedFileName(String baseName, int pageNum, String extension) {
54
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
55
+ String timestamp = sdf.format(new Date());
56
+
57
+ if (pageNum >= 0) {
58
+ // For image exports: filename_page_1_20251102_181059.png
59
+ return baseName + "_page_" + pageNum + "_" + timestamp + "." + extension;
60
+ } else {
61
+ // For PDF operations: filename_20251102_181059.pdf
62
+ return baseName + "_" + timestamp + "." + extension;
63
+ }
64
+ }
65
+
66
+
67
+ @ReactMethod
68
+ public void exportToImages(String filePath, ReadableMap options, Promise promise) {
69
+ try {
70
+ if (!licenseVerifier.isProActive()) {
71
+ promise.reject("LICENSE_REQUIRED", "Export to Images requires a Pro license");
72
+ return;
73
+ }
74
+
75
+ if (filePath == null || filePath.isEmpty()) {
76
+ promise.reject("INVALID_PATH", "File path is required");
77
+ return;
78
+ }
79
+
80
+ File pdfFile = new File(filePath);
81
+ if (!pdfFile.exists()) {
82
+ promise.reject("FILE_NOT_FOUND", "PDF file not found: " + filePath);
83
+ return;
84
+ }
85
+
86
+ ReadableArray pages = options.hasKey("pages") ? options.getArray("pages") : null;
87
+ int dpi = options.hasKey("dpi") ? options.getInt("dpi") : 150;
88
+ String format = options.hasKey("format") ? options.getString("format") : "png";
89
+ String outputDir = options.hasKey("outputDir") ? options.getString("outputDir") : null;
90
+
91
+ List<String> exportedFiles = exportPagesToImages(pdfFile, pages, dpi, format, outputDir);
92
+
93
+ WritableArray result = Arguments.createArray();
94
+ for (String file : exportedFiles) {
95
+ result.pushString(file);
96
+ }
97
+
98
+ promise.resolve(result);
99
+
100
+ } catch (Exception e) {
101
+ Log.e(TAG, "Error exporting to images", e);
102
+ promise.reject("EXPORT_ERROR", e.getMessage());
103
+ }
104
+ }
105
+
106
+ @ReactMethod
107
+ public void exportPageToImage(String filePath, int pageIndex, ReadableMap options, Promise promise) {
108
+ try {
109
+ String format = options.hasKey("format") ? options.getString("format") : "jpeg";
110
+ double quality = options.hasKey("quality") ? options.getDouble("quality") : 0.9;
111
+ double scale = options.hasKey("scale") ? options.getDouble("scale") : 2.0;
112
+
113
+ Log.i(TAG, "🖼️ [EXPORT] exportPageToImage - START - page: " + pageIndex + ", format: " + format + ", quality: " + quality + ", scale: " + scale);
114
+
115
+ boolean licenseActive = licenseVerifier.isProActive();
116
+ Log.i(TAG, "🔑 [LICENSE] isProActive: " + licenseActive);
117
+
118
+ if (!licenseActive) {
119
+ Log.e(TAG, "❌ [EXPORT] exportPageToImage - FAILED - License required");
120
+ promise.reject("LICENSE_REQUIRED", "Export to Images requires a Pro license");
121
+ return;
122
+ }
123
+
124
+ if (filePath == null || filePath.isEmpty()) {
125
+ Log.e(TAG, "❌ [EXPORT] exportPageToImage - FAILED - Invalid path");
126
+ promise.reject("INVALID_PATH", "File path is required");
127
+ return;
128
+ }
129
+
130
+ File pdfFile = new File(filePath);
131
+ Log.i(TAG, "📁 [FILE] PDF path: " + filePath);
132
+
133
+ if (!pdfFile.exists()) {
134
+ Log.e(TAG, "❌ [FILE] PDF not found: " + filePath);
135
+ promise.reject("FILE_NOT_FOUND", "PDF file not found: " + filePath);
136
+ return;
137
+ }
138
+
139
+ Log.i(TAG, "📁 [FILE] PDF exists, size: " + pdfFile.length() + " bytes");
140
+
141
+ int dpi = (int)(72 * scale);
142
+ Log.i(TAG, "🖼️ [EXPORT] Calculated DPI: " + dpi);
143
+
144
+ String outputPath = exportSinglePageToImage(pdfFile, pageIndex, dpi, format, null);
145
+
146
+ Log.i(TAG, "✅ [EXPORT] exportPageToImage - SUCCESS - output: " + outputPath);
147
+ promise.resolve(outputPath);
148
+
149
+ } catch (Exception e) {
150
+ Log.e(TAG, "❌ [EXPORT] exportPageToImage - ERROR: " + e.getMessage(), e);
151
+ promise.reject("EXPORT_ERROR", e.getMessage());
152
+ }
153
+ }
154
+
155
+ private String exportSinglePageToImage(File pdfFile, int pageIndex, int dpi, String format, String outputDir) throws IOException {
156
+ Log.i(TAG, "🖼️ [EXPORT] exportSinglePageToImage - START - pageIndex: " + pageIndex + ", dpi: " + dpi + ", format: " + format);
157
+
158
+ try (ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY);
159
+ PdfRenderer pdfRenderer = new PdfRenderer(fileDescriptor)) {
160
+
161
+ int totalPages = pdfRenderer.getPageCount();
162
+ Log.i(TAG, "📁 [FILE] PDF opened, total pages: " + totalPages);
163
+
164
+ if (pageIndex < 0 || pageIndex >= totalPages) {
165
+ Log.e(TAG, "❌ [EXPORT] Invalid page index: " + pageIndex + " (total: " + totalPages + ")");
166
+ throw new IOException("Invalid page index: " + pageIndex);
167
+ }
168
+
169
+ PdfRenderer.Page page = pdfRenderer.openPage(pageIndex);
170
+ Log.i(TAG, "📄 [PAGE] Opened page " + (pageIndex + 1));
171
+
172
+ int width = (int)(page.getWidth() * dpi / 72);
173
+ int height = (int)(page.getHeight() * dpi / 72);
174
+
175
+ Log.i(TAG, "🖼️ [BITMAP] Creating bitmap - width: " + width + "px, height: " + height + "px, dpi: " + dpi);
176
+
177
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
178
+ Canvas canvas = new Canvas(bitmap);
179
+ canvas.drawColor(Color.WHITE);
180
+
181
+ Bitmap renderBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
182
+ Log.i(TAG, "🖼️ [RENDER] Rendering page to bitmap...");
183
+ page.render(renderBitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_PRINT);
184
+ canvas.drawBitmap(renderBitmap, 0, 0, null);
185
+ renderBitmap.recycle();
186
+
187
+ page.close();
188
+ Log.i(TAG, "✅ [RENDER] Page rendered successfully");
189
+
190
+ if (outputDir == null) {
191
+ outputDir = getReactApplicationContext().getCacheDir().getAbsolutePath();
192
+ Log.i(TAG, "📁 [FILE] Using cache dir: " + outputDir);
193
+ }
194
+
195
+ // Generate unique filename with timestamp to prevent overwrites
196
+ String baseName = pdfFile.getName().replace(".pdf", "");
197
+ String fileName = generateTimestampedFileName(baseName, pageIndex + 1, format);
198
+ File outputFile = new File(outputDir, fileName);
199
+
200
+ Log.i(TAG, "📁 [FILE] Writing to: " + outputFile.getAbsolutePath());
201
+
202
+ try (FileOutputStream out = new FileOutputStream(outputFile)) {
203
+ Bitmap.CompressFormat compressFormat = format.equalsIgnoreCase("png")
204
+ ? Bitmap.CompressFormat.PNG
205
+ : Bitmap.CompressFormat.JPEG;
206
+ int qualityPercent = format.equalsIgnoreCase("png") ? 100 : 90;
207
+
208
+ Log.i(TAG, "📁 [FILE] Compressing as " + compressFormat + " at " + qualityPercent + "% quality");
209
+ bitmap.compress(compressFormat, qualityPercent, out);
210
+ }
211
+
212
+ bitmap.recycle();
213
+
214
+ long fileSize = outputFile.length();
215
+ Log.i(TAG, "✅ [EXPORT] exportSinglePageToImage - SUCCESS - size: " + fileSize + " bytes, path: " + outputFile.getAbsolutePath());
216
+ return outputFile.getAbsolutePath();
217
+ }
218
+ }
219
+
220
+
221
+ private List<String> exportPagesToImages(File pdfFile, ReadableArray pages, int dpi, String format, String outputDir) throws IOException {
222
+ Log.i(TAG, "🖼️ [EXPORT] exportPagesToImages - START - dpi: " + dpi + ", format: " + format);
223
+
224
+ List<String> exportedFiles = new ArrayList<>();
225
+
226
+ try (ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY);
227
+ PdfRenderer pdfRenderer = new PdfRenderer(fileDescriptor)) {
228
+
229
+ int pageCount = pdfRenderer.getPageCount();
230
+ Log.i(TAG, "📁 [FILE] PDF has " + pageCount + " total pages");
231
+
232
+ List<Integer> pagesToExport = new ArrayList<>();
233
+ if (pages != null && pages.size() > 0) {
234
+ for (int i = 0; i < pages.size(); i++) {
235
+ int pageIndex = pages.getInt(i);
236
+ if (pageIndex >= 0 && pageIndex < pageCount) {
237
+ pagesToExport.add(pageIndex);
238
+ }
239
+ }
240
+ Log.i(TAG, "📊 [PROGRESS] Exporting " + pagesToExport.size() + " specific pages");
241
+ } else {
242
+ for (int i = 0; i < pageCount; i++) {
243
+ pagesToExport.add(i);
244
+ }
245
+ Log.i(TAG, "📊 [PROGRESS] Exporting all " + pageCount + " pages");
246
+ }
247
+
248
+ int current = 0;
249
+ for (int pageIndex : pagesToExport) {
250
+ try {
251
+ current++;
252
+ Log.i(TAG, "📊 [PROGRESS] Exporting page " + current + "/" + pagesToExport.size() + " (page number: " + (pageIndex + 1) + ")");
253
+
254
+ PdfRenderer.Page page = pdfRenderer.openPage(pageIndex);
255
+
256
+ float scale = dpi / 72f; // 72 DPI is default
257
+ int width = (int) (page.getWidth() * scale);
258
+ int height = (int) (page.getHeight() * scale);
259
+
260
+ Log.i(TAG, "🖼️ [BITMAP] Creating " + width + "x" + height + " bitmap");
261
+
262
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
263
+ Canvas canvas = new Canvas(bitmap);
264
+ canvas.drawColor(Color.WHITE);
265
+
266
+ page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
267
+
268
+ String fileName = String.format("page_%d.%s", pageIndex + 1, format);
269
+ String outputPath = saveBitmap(bitmap, fileName, outputDir);
270
+ exportedFiles.add(outputPath);
271
+
272
+ bitmap.recycle();
273
+ page.close();
274
+
275
+ Log.i(TAG, "✅ [PROGRESS] Page " + (pageIndex + 1) + " exported to " + outputPath);
276
+
277
+ } catch (Exception e) {
278
+ Log.e(TAG, "❌ [EXPORT] Error exporting page " + (pageIndex + 1), e);
279
+ }
280
+ }
281
+
282
+ Log.i(TAG, "✅ [EXPORT] exportPagesToImages - SUCCESS - Exported " + exportedFiles.size() + " pages");
283
+ }
284
+
285
+ return exportedFiles;
286
+ }
287
+
288
+
289
+ private String saveBitmap(Bitmap bitmap, String fileName, String outputDir) throws IOException {
290
+ File outputFile;
291
+
292
+ if (outputDir != null && !outputDir.isEmpty()) {
293
+ File dir = new File(outputDir);
294
+ if (!dir.exists()) {
295
+ dir.mkdirs();
296
+ }
297
+ outputFile = new File(dir, fileName);
298
+ } else {
299
+ File cacheDir = getReactApplicationContext().getCacheDir();
300
+ outputFile = new File(cacheDir, fileName);
301
+ }
302
+
303
+ try (FileOutputStream out = new FileOutputStream(outputFile)) {
304
+ if (fileName.toLowerCase().endsWith(".png")) {
305
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
306
+ } else if (fileName.toLowerCase().endsWith(".jpg") || fileName.toLowerCase().endsWith(".jpeg")) {
307
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);
308
+ } else {
309
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
310
+ }
311
+ }
312
+
313
+ return outputFile.getAbsolutePath();
314
+ }
315
+
316
+
317
+ @ReactMethod
318
+ public void mergePDFs(ReadableArray filePaths, String outputPath, Promise promise) {
319
+ try {
320
+ // Check Pro license
321
+ if (!licenseVerifier.isProActive()) {
322
+ promise.reject("LICENSE_REQUIRED", "PDF Operations requires a Pro license");
323
+ return;
324
+ }
325
+
326
+ if (filePaths == null || filePaths.size() < 2) {
327
+ promise.reject("INVALID_INPUT", "At least 2 PDF files are required for merging");
328
+ return;
329
+ }
330
+
331
+ // Validate all files exist
332
+ List<String> validPaths = new ArrayList<>();
333
+ for (int i = 0; i < filePaths.size(); i++) {
334
+ String filePath = filePaths.getString(i);
335
+ File file = new File(filePath);
336
+ if (file.exists()) {
337
+ validPaths.add(filePath);
338
+ } else {
339
+ Log.w(TAG, "File not found: " + filePath);
340
+ }
341
+ }
342
+
343
+ if (validPaths.size() < 2) {
344
+ promise.reject("INVALID_INPUT", "At least 2 valid PDF files are required");
345
+ return;
346
+ }
347
+
348
+ // Determine output path
349
+ if (outputPath == null || outputPath.isEmpty()) {
350
+ File firstFile = new File(validPaths.get(0));
351
+ outputPath = new File(firstFile.getParent(), "merged_" + System.currentTimeMillis() + ".pdf").getAbsolutePath();
352
+ }
353
+
354
+ // Merge PDFs using PdfRenderer and PdfDocument
355
+ PdfDocument mergedDoc = new PdfDocument();
356
+ int pageCount = 0;
357
+
358
+ for (String filePath : validPaths) {
359
+ try (ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(new File(filePath), ParcelFileDescriptor.MODE_READ_ONLY);
360
+ PdfRenderer pdfRenderer = new PdfRenderer(fileDescriptor)) {
361
+
362
+ int srcPageCount = pdfRenderer.getPageCount();
363
+
364
+ for (int i = 0; i < srcPageCount; i++) {
365
+ PdfRenderer.Page page = pdfRenderer.openPage(i);
366
+
367
+ // Create new page in merged document
368
+ PdfDocument.PageInfo pageInfo = new PdfDocument.PageInfo.Builder(
369
+ page.getWidth(),
370
+ page.getHeight(),
371
+ pageCount + 1
372
+ ).create();
373
+
374
+ PdfDocument.Page newPage = mergedDoc.startPage(pageInfo);
375
+ Canvas canvas = newPage.getCanvas();
376
+
377
+ // Render page to bitmap first, then draw on canvas
378
+ Bitmap bitmap = Bitmap.createBitmap(page.getWidth(), page.getHeight(), Bitmap.Config.ARGB_8888);
379
+ page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_PRINT);
380
+ canvas.drawBitmap(bitmap, 0, 0, null);
381
+ bitmap.recycle();
382
+
383
+ mergedDoc.finishPage(newPage);
384
+ page.close();
385
+ pageCount++;
386
+ }
387
+ }
388
+ }
389
+
390
+ // Write merged PDF
391
+ try (FileOutputStream outputStream = new FileOutputStream(outputPath)) {
392
+ mergedDoc.writeTo(outputStream);
393
+ }
394
+
395
+ mergedDoc.close();
396
+
397
+ Log.i(TAG, "Merged " + validPaths.size() + " PDFs with " + pageCount + " pages to: " + outputPath);
398
+ promise.resolve(outputPath);
399
+
400
+ } catch (Exception e) {
401
+ Log.e(TAG, "Error merging PDFs", e);
402
+ promise.reject("MERGE_ERROR", e.getMessage());
403
+ }
404
+ }
405
+
406
+
407
+ @ReactMethod
408
+ public void splitPDF(String filePath, ReadableArray pageRanges, String outputDir, Promise promise) {
409
+ try {
410
+ // 🔍 DEEP DEBUG: Inspect incoming parameters
411
+ Log.i(TAG, "🔍 [DEBUG] ========== SPLIT PDF DEBUG START ==========");
412
+ Log.i(TAG, "🔍 [DEBUG] filePath type: " + (filePath != null ? filePath.getClass().getName() : "null"));
413
+ Log.i(TAG, "🔍 [DEBUG] filePath value: " + filePath);
414
+ Log.i(TAG, "🔍 [DEBUG] pageRanges type: " + (pageRanges != null ? pageRanges.getClass().getName() : "null"));
415
+ Log.i(TAG, "🔍 [DEBUG] pageRanges size: " + (pageRanges != null ? pageRanges.size() : "null"));
416
+ Log.i(TAG, "🔍 [DEBUG] outputDir type: " + (outputDir != null ? outputDir.getClass().getName() : "null"));
417
+ Log.i(TAG, "🔍 [DEBUG] outputDir value: " + outputDir);
418
+
419
+ // Inspect each element in pageRanges
420
+ if (pageRanges != null) {
421
+ for (int i = 0; i < pageRanges.size(); i++) {
422
+ try {
423
+ Log.i(TAG, "🔍 [DEBUG] pageRanges[" + i + "] type: " + pageRanges.getType(i));
424
+
425
+ // Try to get as array
426
+ try {
427
+ ReadableArray range = pageRanges.getArray(i);
428
+ Log.i(TAG, "🔍 [DEBUG] pageRanges[" + i + "] is Array, size: " + range.size());
429
+ if (range.size() >= 2) {
430
+ Log.i(TAG, "🔍 [DEBUG] pageRanges[" + i + "][0] = " + range.getInt(0));
431
+ Log.i(TAG, "🔍 [DEBUG] pageRanges[" + i + "][1] = " + range.getInt(1));
432
+ }
433
+ } catch (Exception e) {
434
+ Log.e(TAG, "🔍 [DEBUG] pageRanges[" + i + "] NOT an array: " + e.getMessage());
435
+ }
436
+
437
+ // Try to get as map
438
+ try {
439
+ ReadableMap rangeMap = pageRanges.getMap(i);
440
+ Log.i(TAG, "🔍 [DEBUG] pageRanges[" + i + "] is Map");
441
+ if (rangeMap.hasKey("start") && rangeMap.hasKey("end")) {
442
+ Log.i(TAG, "🔍 [DEBUG] pageRanges[" + i + "].start = " + rangeMap.getInt("start"));
443
+ Log.i(TAG, "🔍 [DEBUG] pageRanges[" + i + "].end = " + rangeMap.getInt("end"));
444
+ }
445
+ } catch (Exception e) {
446
+ Log.e(TAG, "🔍 [DEBUG] pageRanges[" + i + "] NOT a map: " + e.getMessage());
447
+ }
448
+
449
+ } catch (Exception e) {
450
+ Log.e(TAG, "🔍 [DEBUG] Error inspecting pageRanges[" + i + "]: " + e.getMessage());
451
+ }
452
+ }
453
+ }
454
+
455
+ Log.i(TAG, "🔍 [DEBUG] ========== SPLIT PDF DEBUG END ==========");
456
+
457
+ // Original code continues...
458
+ Log.i(TAG, "✂️ [SPLIT] splitPDF - START - file: " + filePath + ", ranges: " + pageRanges.size());
459
+
460
+ // Check Pro license
461
+ boolean licenseActive = licenseVerifier.isProActive();
462
+ Log.i(TAG, "🔑 [LICENSE] isProActive: " + licenseActive);
463
+
464
+ if (!licenseActive) {
465
+ Log.e(TAG, "❌ [SPLIT] License required");
466
+ promise.reject("LICENSE_REQUIRED", "PDF Operations requires a Pro license");
467
+ return;
468
+ }
469
+
470
+ if (filePath == null || filePath.isEmpty()) {
471
+ Log.e(TAG, "❌ [SPLIT] Invalid path");
472
+ promise.reject("INVALID_PATH", "File path is required");
473
+ return;
474
+ }
475
+
476
+ File pdfFile = new File(filePath);
477
+ if (!pdfFile.exists()) {
478
+ Log.e(TAG, "❌ [FILE] PDF not found: " + filePath);
479
+ promise.reject("FILE_NOT_FOUND", "PDF file not found: " + filePath);
480
+ return;
481
+ }
482
+
483
+ Log.i(TAG, "📁 [FILE] PDF exists, size: " + pdfFile.length() + " bytes");
484
+
485
+ // Use provided outputDir or default to cache directory
486
+ if (outputDir == null || outputDir.isEmpty()) {
487
+ outputDir = getReactApplicationContext().getCacheDir().getAbsolutePath();
488
+ }
489
+ Log.i(TAG, "📁 [FILE] Output directory: " + outputDir);
490
+
491
+ List<String> splitFiles = new ArrayList<>();
492
+
493
+ try (ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY);
494
+ PdfRenderer pdfRenderer = new PdfRenderer(fileDescriptor)) {
495
+
496
+ int totalPages = pdfRenderer.getPageCount();
497
+ Log.i(TAG, "📁 [FILE] PDF opened, total pages: " + totalPages);
498
+
499
+ // Parse page ranges from flat array: [start1, end1, start2, end2, ...]
500
+ // Example: [1, 10, 11, 21] means ranges 1-10 and 11-21
501
+ int rangeCount = pageRanges.size() / 2;
502
+ Log.i(TAG, "📊 [PROGRESS] Processing " + rangeCount + " ranges from flat array");
503
+
504
+ for (int i = 0; i < pageRanges.size(); i += 2) {
505
+ if (i + 1 >= pageRanges.size()) {
506
+ Log.w(TAG, "⚠️ [SPLIT] Skipping incomplete range at index " + i);
507
+ continue;
508
+ }
509
+
510
+ int start = pageRanges.getInt(i) - 1; // Convert to 0-indexed
511
+ int end = pageRanges.getInt(i + 1); // End is inclusive, 1-indexed
512
+
513
+ int rangeNum = (i / 2) + 1;
514
+ Log.i(TAG, "📊 [PROGRESS] Processing range " + rangeNum + "/" + rangeCount + ": pages " + (start + 1) + "-" + end);
515
+
516
+ // Validate range
517
+ if (start < 0 || end > totalPages || start >= end) {
518
+ Log.w(TAG, "⚠️ [SPLIT] Invalid range: [" + (start + 1) + ", " + end + "]");
519
+ continue;
520
+ }
521
+
522
+ // Create split PDF for this range with timestamped filename
523
+ PdfDocument splitDoc = new PdfDocument();
524
+ String baseName = pdfFile.getName().replace(".pdf", "") + "_pages_" + (start + 1) + "-" + end;
525
+ String fileName = generateTimestampedFileName(baseName, -1, "pdf");
526
+ String outputFile = new File(outputDir, fileName).getAbsolutePath();
527
+
528
+ Log.i(TAG, "📁 [FILE] Creating split file: " + outputFile);
529
+
530
+ int pageNum = 1;
531
+ for (int page = start; page < end && page < totalPages; page++) {
532
+ Log.i(TAG, "📊 [PROGRESS] Processing page " + (page + 1) + " (page " + pageNum + " in new PDF)");
533
+
534
+ PdfRenderer.Page srcPage = pdfRenderer.openPage(page);
535
+
536
+ PdfDocument.PageInfo pageInfo = new PdfDocument.PageInfo.Builder(
537
+ srcPage.getWidth(),
538
+ srcPage.getHeight(),
539
+ pageNum++
540
+ ).create();
541
+
542
+ PdfDocument.Page newPage = splitDoc.startPage(pageInfo);
543
+ Canvas canvas = newPage.getCanvas();
544
+
545
+ // Render page to bitmap first, then draw on canvas
546
+ Bitmap bitmap = Bitmap.createBitmap(srcPage.getWidth(), srcPage.getHeight(), Bitmap.Config.ARGB_8888);
547
+ srcPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_PRINT);
548
+ canvas.drawBitmap(bitmap, 0, 0, null);
549
+ bitmap.recycle();
550
+
551
+ splitDoc.finishPage(newPage);
552
+ srcPage.close();
553
+ }
554
+
555
+ // Write split PDF
556
+ try (FileOutputStream outputStream = new FileOutputStream(outputFile)) {
557
+ splitDoc.writeTo(outputStream);
558
+ }
559
+
560
+ splitDoc.close();
561
+ splitFiles.add(outputFile);
562
+
563
+ File createdFile = new File(outputFile);
564
+ Log.i(TAG, "✅ [SPLIT] Created file: " + outputFile + " (size: " + createdFile.length() + " bytes)");
565
+ }
566
+ }
567
+
568
+ Log.i(TAG, "✅ [SPLIT] splitPDF - SUCCESS - Split into " + splitFiles.size() + " files");
569
+
570
+ // Convert ArrayList to WritableArray for React Native bridge
571
+ WritableArray result = Arguments.createArray();
572
+ for (String file : splitFiles) {
573
+ result.pushString(file);
574
+ }
575
+ promise.resolve(result);
576
+
577
+ } catch (Exception e) {
578
+ Log.e(TAG, "❌ [SPLIT] Error: " + e.getMessage(), e);
579
+ promise.reject("SPLIT_ERROR", e.getMessage());
580
+ }
581
+ }
582
+
583
+
584
+ @ReactMethod
585
+ public void extractPages(String filePath, ReadableArray pageNumbers, String outputPath, Promise promise) {
586
+ try {
587
+ Log.i(TAG, "✂️ [EXTRACT] extractPages - START - file: " + filePath + ", pages: " + pageNumbers.size());
588
+
589
+ // Check Pro license
590
+ boolean licenseActive = licenseVerifier.isProActive();
591
+ Log.i(TAG, "🔑 [LICENSE] isProActive: " + licenseActive);
592
+
593
+ if (!licenseActive) {
594
+ Log.e(TAG, "❌ [EXTRACT] License required");
595
+ promise.reject("LICENSE_REQUIRED", "PDF Operations requires a Pro license");
596
+ return;
597
+ }
598
+
599
+ if (filePath == null || filePath.isEmpty()) {
600
+ Log.e(TAG, "❌ [EXTRACT] Invalid path");
601
+ promise.reject("INVALID_PATH", "File path is required");
602
+ return;
603
+ }
604
+
605
+ File pdfFile = new File(filePath);
606
+ if (!pdfFile.exists()) {
607
+ Log.e(TAG, "❌ [FILE] PDF not found: " + filePath);
608
+ promise.reject("FILE_NOT_FOUND", "PDF file not found: " + filePath);
609
+ return;
610
+ }
611
+
612
+ Log.i(TAG, "📁 [FILE] PDF exists, size: " + pdfFile.length() + " bytes");
613
+
614
+ // Determine output path with timestamped filename
615
+ if (outputPath == null || outputPath.isEmpty()) {
616
+ String baseName = pdfFile.getName().replace(".pdf", "") + "_extracted";
617
+ String fileName = generateTimestampedFileName(baseName, -1, "pdf");
618
+ outputPath = new File(pdfFile.getParent(), fileName).getAbsolutePath();
619
+ }
620
+
621
+ Log.i(TAG, "📁 [FILE] Output path: " + outputPath);
622
+
623
+ // Log page numbers to extract
624
+ StringBuilder pageList = new StringBuilder();
625
+ for (int i = 0; i < pageNumbers.size(); i++) {
626
+ if (i > 0) pageList.append(", ");
627
+ pageList.append(pageNumbers.getInt(i));
628
+ }
629
+ Log.i(TAG, "📊 [EXTRACT] Pages to extract: " + pageList.toString());
630
+
631
+ try (ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY);
632
+ PdfRenderer pdfRenderer = new PdfRenderer(fileDescriptor)) {
633
+
634
+ int totalPages = pdfRenderer.getPageCount();
635
+ Log.i(TAG, "📁 [FILE] PDF opened, total pages: " + totalPages);
636
+
637
+ PdfDocument extractDoc = new PdfDocument();
638
+ int pageNum = 1;
639
+ int extractedCount = 0;
640
+
641
+ for (int i = 0; i < pageNumbers.size(); i++) {
642
+ int pageIndex = pageNumbers.getInt(i) - 1; // Convert to 0-indexed
643
+
644
+ Log.i(TAG, "📊 [PROGRESS] Processing page " + (i + 1) + "/" + pageNumbers.size() + " (page number: " + (pageIndex + 1) + ")");
645
+
646
+ if (pageIndex >= 0 && pageIndex < totalPages) {
647
+ PdfRenderer.Page srcPage = pdfRenderer.openPage(pageIndex);
648
+
649
+ PdfDocument.PageInfo pageInfo = new PdfDocument.PageInfo.Builder(
650
+ srcPage.getWidth(),
651
+ srcPage.getHeight(),
652
+ pageNum++
653
+ ).create();
654
+
655
+ PdfDocument.Page newPage = extractDoc.startPage(pageInfo);
656
+ Canvas canvas = newPage.getCanvas();
657
+
658
+ // Render page to bitmap first, then draw on canvas
659
+ Bitmap bitmap = Bitmap.createBitmap(srcPage.getWidth(), srcPage.getHeight(), Bitmap.Config.ARGB_8888);
660
+ srcPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_PRINT);
661
+ canvas.drawBitmap(bitmap, 0, 0, null);
662
+ bitmap.recycle();
663
+
664
+ extractDoc.finishPage(newPage);
665
+ srcPage.close();
666
+ extractedCount++;
667
+
668
+ Log.i(TAG, "✅ [PROGRESS] Extracted page " + (pageIndex + 1));
669
+ } else {
670
+ Log.w(TAG, "⚠️ [EXTRACT] Skipping invalid page index: " + (pageIndex + 1));
671
+ }
672
+ }
673
+
674
+ // Write extracted PDF
675
+ Log.i(TAG, "📁 [FILE] Writing extracted PDF...");
676
+ try (FileOutputStream outputStream = new FileOutputStream(outputPath)) {
677
+ extractDoc.writeTo(outputStream);
678
+ }
679
+
680
+ extractDoc.close();
681
+
682
+ File createdFile = new File(outputPath);
683
+ Log.i(TAG, "✅ [EXTRACT] extractPages - SUCCESS - Extracted " + extractedCount + " pages to: " + outputPath + " (size: " + createdFile.length() + " bytes)");
684
+ }
685
+
686
+ promise.resolve(outputPath);
687
+
688
+ } catch (Exception e) {
689
+ Log.e(TAG, "❌ [EXTRACT] Error: " + e.getMessage(), e);
690
+ promise.reject("EXTRACT_ERROR", e.getMessage());
691
+ }
692
+ }
693
+
694
+
695
+ @ReactMethod
696
+ public void rotatePage(String filePath, int pageNumber, int degrees, Promise promise) {
697
+ try {
698
+ // Check Pro license
699
+ if (!licenseVerifier.isProActive()) {
700
+ promise.reject("LICENSE_REQUIRED", "PDF Operations requires a Pro license");
701
+ return;
702
+ }
703
+
704
+ if (filePath == null || filePath.isEmpty()) {
705
+ promise.reject("INVALID_PATH", "File path is required");
706
+ return;
707
+ }
708
+
709
+ promise.reject("NOT_IMPLEMENTED", "Page rotation not yet implemented on Android");
710
+
711
+ } catch (Exception e) {
712
+ Log.e(TAG, "Error rotating page", e);
713
+ promise.reject("ROTATE_ERROR", e.getMessage());
714
+ }
715
+ }
716
+
717
+
718
+ @ReactMethod
719
+ public void deletePage(String filePath, int pageNumber, Promise promise) {
720
+ try {
721
+ // Check Pro license
722
+ if (!licenseVerifier.isProActive()) {
723
+ promise.reject("LICENSE_REQUIRED", "PDF Operations requires a Pro license");
724
+ return;
725
+ }
726
+
727
+ if (filePath == null || filePath.isEmpty()) {
728
+ promise.reject("INVALID_PATH", "File path is required");
729
+ return;
730
+ }
731
+
732
+ promise.reject("NOT_IMPLEMENTED", "Page deletion not yet implemented on Android");
733
+
734
+ } catch (Exception e) {
735
+ Log.e(TAG, "Error deleting page", e);
736
+ promise.reject("DELETE_ERROR", e.getMessage());
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Get PDF page count
742
+ */
743
+ @ReactMethod
744
+ public void getPageCount(String filePath, Promise promise) {
745
+ try {
746
+ if (filePath == null || filePath.isEmpty()) {
747
+ promise.reject("INVALID_PATH", "File path is required");
748
+ return;
749
+ }
750
+
751
+ File pdfFile = new File(filePath);
752
+ if (!pdfFile.exists()) {
753
+ promise.reject("FILE_NOT_FOUND", "PDF file not found: " + filePath);
754
+ return;
755
+ }
756
+
757
+ try (ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY);
758
+ PdfRenderer pdfRenderer = new PdfRenderer(fileDescriptor)) {
759
+
760
+ int pageCount = pdfRenderer.getPageCount();
761
+ promise.resolve(pageCount);
762
+ }
763
+
764
+ } catch (Exception e) {
765
+ Log.e(TAG, "Error getting page count", e);
766
+ promise.reject("PAGE_COUNT_ERROR", e.getMessage());
767
+ }
768
+ }
769
+ }