Replace dependencies with system apis (#8)

* pdf dep rem

* remove stale package

* replace with native share

* rename share module

* revert: restore react-native-share (native replacement had worse perf, negligible size gain)

* cosmetic
This commit is contained in:
2026-04-21 04:46:15 +05:30
committed by GitHub
parent 46b421acf2
commit 77d4b4bd87
11 changed files with 173 additions and 189 deletions

View File

@@ -17,9 +17,9 @@ class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> {
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
return PackageList(this).packages
val packages = PackageList(this).packages.toMutableList()
packages.add(PdfGeneratorPackage())
return packages
}
override fun getJSMainModuleName(): String = "index"

View File

@@ -0,0 +1,55 @@
package com.lensapp
import android.graphics.BitmapFactory
import android.graphics.RectF
import android.graphics.pdf.PdfDocument
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableArray
import java.io.File
import java.io.FileOutputStream
class PdfGeneratorModule(private val reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
override fun getName() = "PdfGenerator"
@ReactMethod
fun generate(imagePaths: ReadableArray, fileName: String, promise: Promise) {
try {
val outDir = File(reactContext.filesDir, "pdfs").also { it.mkdirs() }
val outFile = File(outDir, "$fileName.pdf")
val doc = PdfDocument()
for (i in 0 until imagePaths.size()) {
val path = imagePaths.getString(i)!!.removePrefix("file://")
val bitmap = BitmapFactory.decodeFile(path)
?: throw Exception("Failed to decode image: $path")
// A4 at 72 DPI: 595 x 842 points
val pageWidth = 595
val pageHeight = 842
val pageInfo = PdfDocument.PageInfo.Builder(pageWidth, pageHeight, i + 1).create()
val page = doc.startPage(pageInfo)
val scale = minOf(pageWidth.toFloat() / bitmap.width, pageHeight.toFloat() / bitmap.height)
val scaledW = bitmap.width * scale
val scaledH = bitmap.height * scale
val left = (pageWidth - scaledW) / 2f
val top = (pageHeight - scaledH) / 2f
page.canvas.drawBitmap(bitmap, null, RectF(left, top, left + scaledW, top + scaledH), null)
bitmap.recycle()
doc.finishPage(page)
}
FileOutputStream(outFile).use { doc.writeTo(it) }
doc.close()
promise.resolve("file://${outFile.absolutePath}")
} catch (e: Exception) {
promise.reject("PDF_ERROR", e.message, e)
}
}
}

View File

@@ -0,0 +1,14 @@
package com.lensapp
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class PdfGeneratorPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
listOf(PdfGeneratorModule(reactContext))
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
emptyList()
}

View File

@@ -0,0 +1,4 @@
#import <React/RCTBridgeModule.h>
@interface PdfGenerator : NSObject <RCTBridgeModule>
@end

View File

@@ -0,0 +1,58 @@
#import "PdfGenerator.h"
#import <UIKit/UIKit.h>
@implementation PdfGenerator
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(generate:(NSArray *)imagePaths
fileName:(NSString *)fileName
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray *docPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *pdfsDir = [docPaths[0] stringByAppendingPathComponent:@"pdfs"];
[[NSFileManager defaultManager] createDirectoryAtPath:pdfsDir
withIntermediateDirectories:YES
attributes:nil
error:nil];
NSString *outPath = [[pdfsDir stringByAppendingPathComponent:fileName]
stringByAppendingPathExtension:@"pdf"];
// A4 at 72 DPI: 595 x 842 points
CGRect pageRect = CGRectMake(0, 0, 595, 842);
UIGraphicsPDFRendererFormat *format = [UIGraphicsPDFRendererFormat defaultFormat];
UIGraphicsPDFRenderer *renderer = [[UIGraphicsPDFRenderer alloc] initWithBounds:pageRect format:format];
NSError *error;
BOOL success = [renderer writePDFToURL:[NSURL fileURLWithPath:outPath]
withActions:^(UIGraphicsPDFRendererContext *ctx) {
for (NSString *rawPath in imagePaths) {
NSString *path = [rawPath hasPrefix:@"file://"] ? [rawPath substringFromIndex:7] : rawPath;
UIImage *image = [UIImage imageWithContentsOfFile:path];
if (!image) continue;
[ctx beginPage];
CGFloat scale = MIN(pageRect.size.width / image.size.width,
pageRect.size.height / image.size.height);
CGFloat w = image.size.width * scale;
CGFloat h = image.size.height * scale;
CGFloat x = (pageRect.size.width - w) / 2;
CGFloat y = (pageRect.size.height - h) / 2;
[image drawInRect:CGRectMake(x, y, w, h)];
}
} error:&error];
if (success) {
resolve([@"file://" stringByAppendingString:outPath]);
} else {
reject(@"PDF_ERROR", error.localizedDescription, error);
}
});
}
@end

14
package-lock.json generated
View File

@@ -13,7 +13,6 @@
"react-native": "0.73.0",
"react-native-document-scanner-plugin": "^2.0.4",
"react-native-fs": "^2.20.0",
"react-native-html-to-pdf": "^1.3.0",
"react-native-share": "^12.2.6"
},
"devDependencies": {
@@ -12418,19 +12417,6 @@
}
}
},
"node_modules/react-native-html-to-pdf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/react-native-html-to-pdf/-/react-native-html-to-pdf-1.3.0.tgz",
"integrity": "sha512-UCbQO1k5yeajaJrEF7Wqq8ojdgpxsTbICQvtorXmuO1LkU/a6B3/tZyLes4zXCiq/5nTMTNA/k9m5bYStg5S0Q==",
"license": "MIT",
"workspaces": [
"example"
],
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-share": {
"version": "12.2.6",
"resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-12.2.6.tgz",

View File

@@ -15,7 +15,6 @@
"react-native": "0.73.0",
"react-native-document-scanner-plugin": "^2.0.4",
"react-native-fs": "^2.20.0",
"react-native-html-to-pdf": "^1.3.0",
"react-native-share": "^12.2.6"
},
"devDependencies": {

View File

@@ -1,45 +0,0 @@
diff --git a/node_modules/react-native-html-to-pdf/android/src/main/java/com/htmltopdf/HtmlToPdfPackage.kt b/node_modules/react-native-html-to-pdf/android/src/main/java/com/htmltopdf/HtmlToPdfPackage.kt
index 5fe5a8d..d2586e0 100644
--- a/node_modules/react-native-html-to-pdf/android/src/main/java/com/htmltopdf/HtmlToPdfPackage.kt
+++ b/node_modules/react-native-html-to-pdf/android/src/main/java/com/htmltopdf/HtmlToPdfPackage.kt
@@ -1,33 +1,16 @@
package com.htmltopdf
-import com.facebook.react.BaseReactPackage
+import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
-import com.facebook.react.module.model.ReactModuleInfo
-import com.facebook.react.module.model.ReactModuleInfoProvider
-import java.util.HashMap
+import com.facebook.react.uimanager.ViewManager
-class HtmlToPdfPackage : BaseReactPackage() {
- override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
- return if (name == HtmlToPdfModule.NAME) {
- HtmlToPdfModule(reactContext)
- } else {
- null
- }
+class HtmlToPdfPackage : ReactPackage {
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
+ return listOf(HtmlToPdfModule(reactContext))
}
- override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
- return ReactModuleInfoProvider {
- val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
- moduleInfos[HtmlToPdfModule.NAME] = ReactModuleInfo(
- HtmlToPdfModule.NAME,
- HtmlToPdfModule.NAME,
- false, // canOverrideExistingModule
- false, // needsEagerInit
- false, // isCxxModule
- true // isTurboModule
- )
- moduleInfos
- }
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
+ return emptyList()
}
}

View File

@@ -1,19 +1,17 @@
// Mock native modules before importing the module under test
jest.mock('react-native-fs', () => ({
DocumentDirectoryPath: '/mock/documents',
readFile: jest.fn().mockResolvedValue('base64data'),
mkdir: jest.fn().mockResolvedValue(undefined),
jest.mock('react-native', () => ({
NativeModules: {
PdfGenerator: {
generate: jest.fn(),
},
},
}));
jest.mock('react-native-html-to-pdf', () => ({
generatePDF: jest.fn(),
}));
import RNFS from 'react-native-fs';
import {generatePDF as mockGeneratePDF} from 'react-native-html-to-pdf';
import {NativeModules} from 'react-native';
import {generatePdf} from '../utils/generatePdf';
import {ScannedPage} from '../types';
const mockGenerate = NativeModules.PdfGenerator.generate as jest.Mock;
const mockPages: ScannedPage[] = [
{id: 'p1', uri: 'file:///mock/scans/scan1.jpg', width: 1240, height: 1754},
{id: 'p2', uri: '/mock/scans/scan2.jpg', width: 1240, height: 1754},
@@ -24,73 +22,35 @@ beforeEach(() => {
});
describe('generatePdf', () => {
it('strips file:// before reading and passes base64 to HTML', async () => {
(mockGeneratePDF as jest.Mock).mockResolvedValue({
filePath: '/mock/documents/pdfs/test.pdf',
});
it('passes all image URIs to the native module', async () => {
mockGenerate.mockResolvedValue('file:///mock/pdfs/test.pdf');
await generatePdf(mockPages, 'test doc');
await generatePdf(mockPages, 'test');
// Should have been called for each page
expect(RNFS.readFile).toHaveBeenCalledTimes(2);
// file:// prefix stripped for first page
expect(RNFS.readFile).toHaveBeenCalledWith('/mock/scans/scan1.jpg', 'base64');
// path without prefix used as-is for second page
expect(RNFS.readFile).toHaveBeenCalledWith('/mock/scans/scan2.jpg', 'base64');
});
it('passes a relative directory name, not an absolute path', async () => {
(mockGeneratePDF as jest.Mock).mockResolvedValue({
filePath: '/mock/documents/pdfs/test.pdf',
});
await generatePdf(mockPages, 'My Doc');
const call = (mockGeneratePDF as jest.Mock).mock.calls[0][0];
expect(call.directory).toBe('pdfs');
expect(call.directory).not.toContain('/');
});
it('sanitises the file name', async () => {
(mockGeneratePDF as jest.Mock).mockResolvedValue({
filePath: '/mock/documents/pdfs/test.pdf',
});
await generatePdf(mockPages, 'Scan 2024/06/15 — page 1');
const call = (mockGeneratePDF as jest.Mock).mock.calls[0][0];
expect(call.fileName).not.toMatch(/[^a-z0-9_\-]/i);
});
it('returns a file:// URI', async () => {
(mockGeneratePDF as jest.Mock).mockResolvedValue({
filePath: '/mock/documents/pdfs/test.pdf',
});
const result = await generatePdf(mockPages, 'test');
expect(result).toBe('file:///mock/documents/pdfs/test.pdf');
});
it('does not double-prefix an already file:// path', async () => {
(mockGeneratePDF as jest.Mock).mockResolvedValue({
filePath: 'file:///mock/documents/pdfs/test.pdf',
});
const result = await generatePdf(mockPages, 'test');
expect(result).toBe('file:///mock/documents/pdfs/test.pdf');
expect(result).not.toContain('file://file://');
});
it('throws when the library returns no filePath', async () => {
(mockGeneratePDF as jest.Mock).mockResolvedValue({filePath: null});
await expect(generatePdf(mockPages, 'test')).rejects.toThrow(
'PDF generation failed',
expect(mockGenerate).toHaveBeenCalledWith(
['file:///mock/scans/scan1.jpg', '/mock/scans/scan2.jpg'],
'test',
);
});
it('throws when the library rejects', async () => {
(mockGeneratePDF as jest.Mock).mockRejectedValue(new Error('native crash'));
it('sanitises the file name before passing to native module', async () => {
mockGenerate.mockResolvedValue('file:///mock/pdfs/test.pdf');
await generatePdf(mockPages, 'Scan 2024/06/15 — page 1');
const fileName = mockGenerate.mock.calls[0][1];
expect(fileName).not.toMatch(/[^a-z0-9_\-]/i);
});
it('returns the path resolved by the native module', async () => {
mockGenerate.mockResolvedValue('file:///mock/pdfs/test.pdf');
const result = await generatePdf(mockPages, 'test');
expect(result).toBe('file:///mock/pdfs/test.pdf');
});
it('throws when the native module rejects', async () => {
mockGenerate.mockRejectedValue(new Error('native crash'));
await expect(generatePdf(mockPages, 'test')).rejects.toThrow('native crash');
});

View File

@@ -36,6 +36,7 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
const sharePdf = async (pageIndices?: number[]) => {
setGenerating(true);
await new Promise(resolve => setTimeout(resolve, 0));
try {
const pages = pageIndices
? pageIndices.map(i => document.pages[i]).filter(Boolean)

View File

@@ -1,58 +1,10 @@
import {generatePDF as rnGeneratePDF} from 'react-native-html-to-pdf';
import RNFS from 'react-native-fs';
import {NativeModules} from 'react-native';
import {ScannedPage} from '../types';
// On Android, `directory` is a relative subdirectory name appended to
// getExternalFilesDir()/filesDir — NOT an absolute path.
const PDF_SUBDIR = 'pdfs';
async function pageToBase64(uri: string): Promise<string> {
const path = uri.startsWith('file://') ? uri.slice(7) : uri;
return RNFS.readFile(path, 'base64');
}
function toFileUri(p: string): string {
return p.startsWith('file://') ? p : `file://${p}`;
}
export async function generatePdf(
pages: ScannedPage[],
name: string,
): Promise<string> {
const imageHtml = await Promise.all(
pages.map(async p => {
const b64 = await pageToBase64(p.uri);
return `<div class="page"><img src="data:image/jpeg;base64,${b64}" /></div>`;
}),
);
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #fff; }
.page { width: 100%; page-break-after: always; }
.page:last-child { page-break-after: avoid; }
img { width: 100%; height: auto; display: block; }
</style>
</head>
<body>${imageHtml.join('\n')}</body>
</html>`;
const {PdfGenerator} = NativeModules;
export async function generatePdf(pages: ScannedPage[], name: string): Promise<string> {
const imagePaths = pages.map(p => p.uri);
const fileName = name.replace(/[^a-z0-9_\-]/gi, '_');
const result = await rnGeneratePDF({
html,
fileName,
directory: PDF_SUBDIR,
base64: false,
});
if (!result.filePath) {
throw new Error('PDF generation failed: library returned no file path');
}
return toFileUri(result.filePath);
return PdfGenerator.generate(imagePaths, fileName);
}