@dynlabs/react-native-image-to-webp 0.1.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 (38) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +247 -0
  3. package/ReactNativeImageToWebp.podspec +35 -0
  4. package/android/build.gradle +85 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/cpp/CMakeLists.txt +67 -0
  7. package/android/src/main/cpp/ImageToWebPJNI.cpp +73 -0
  8. package/android/src/main/java/com/dynlabs/reactnativeimagetowebp/ReactNativeImageToWebpModule.kt +258 -0
  9. package/android/src/main/java/com/dynlabs/reactnativeimagetowebp/ReactNativeImageToWebpPackage.kt +33 -0
  10. package/cpp/ImageToWebP.cpp +132 -0
  11. package/cpp/ImageToWebP.h +41 -0
  12. package/cpp/README.md +21 -0
  13. package/cpp/SETUP.md +71 -0
  14. package/ios/ReactNativeImageToWebp.h +5 -0
  15. package/ios/ReactNativeImageToWebp.mm +342 -0
  16. package/lib/module/NativeReactNativeImageToWebp.js +5 -0
  17. package/lib/module/NativeReactNativeImageToWebp.js.map +1 -0
  18. package/lib/module/index.js +78 -0
  19. package/lib/module/index.js.map +1 -0
  20. package/lib/module/package.json +1 -0
  21. package/lib/module/presets.js +64 -0
  22. package/lib/module/presets.js.map +1 -0
  23. package/lib/module/validation.js +36 -0
  24. package/lib/module/validation.js.map +1 -0
  25. package/lib/typescript/package.json +1 -0
  26. package/lib/typescript/src/NativeReactNativeImageToWebp.d.ts +24 -0
  27. package/lib/typescript/src/NativeReactNativeImageToWebp.d.ts.map +1 -0
  28. package/lib/typescript/src/index.d.ts +35 -0
  29. package/lib/typescript/src/index.d.ts.map +1 -0
  30. package/lib/typescript/src/presets.d.ts +3 -0
  31. package/lib/typescript/src/presets.d.ts.map +1 -0
  32. package/lib/typescript/src/validation.d.ts +9 -0
  33. package/lib/typescript/src/validation.d.ts.map +1 -0
  34. package/package.json +139 -0
  35. package/src/NativeReactNativeImageToWebp.ts +32 -0
  36. package/src/index.tsx +109 -0
  37. package/src/presets.ts +80 -0
  38. package/src/validation.ts +55 -0
package/package.json ADDED
@@ -0,0 +1,139 @@
1
+ {
2
+ "name": "@dynlabs/react-native-image-to-webp",
3
+ "version": "0.1.0",
4
+ "description": "Convert images to WebP format with presets and resizing options",
5
+ "main": "./lib/module/index.js",
6
+ "types": "./lib/typescript/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "source": "./src/index.tsx",
10
+ "types": "./lib/typescript/src/index.d.ts",
11
+ "default": "./lib/module/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "lib",
18
+ "android",
19
+ "ios",
20
+ "cpp",
21
+ "*.podspec",
22
+ "react-native.config.js",
23
+ "!ios/build",
24
+ "!android/build",
25
+ "!android/gradle",
26
+ "!android/gradlew",
27
+ "!android/gradlew.bat",
28
+ "!android/local.properties",
29
+ "!**/__tests__",
30
+ "!**/__fixtures__",
31
+ "!**/__mocks__",
32
+ "!**/.*"
33
+ ],
34
+ "scripts": {
35
+ "example": "yarn workspace @dynlabs/react-native-image-to-webp-example",
36
+ "example:ios": "yarn workspace @dynlabs/react-native-image-to-webp-example ios",
37
+ "example:android": "yarn workspace @dynlabs/react-native-image-to-webp-example android",
38
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
39
+ "prepare": "bob build",
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
42
+ "format": "prettier --write \"**/*.{js,ts,tsx,json,md}\"",
43
+ "format:check": "prettier --check \"**/*.{js,ts,tsx,json,md}\"",
44
+ "test": "echo \"No tests specified\" && exit 0",
45
+ "build": "bob build",
46
+ "vendor:libwebp": "node -e \"const {execSync} = require('child_process'); const os = require('os'); const script = os.platform() === 'win32' ? 'scripts/vendor-libwebp.ps1' : 'scripts/vendor-libwebp.sh'; execSync(script, {stdio: 'inherit'})\""
47
+ },
48
+ "keywords": [
49
+ "react-native",
50
+ "ios",
51
+ "android"
52
+ ],
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+.git"
56
+ },
57
+ "author": " <> ()",
58
+ "license": "MIT",
59
+ "bugs": {
60
+ "url": "/issues"
61
+ },
62
+ "homepage": "#readme",
63
+ "publishConfig": {
64
+ "registry": "https://registry.npmjs.org/",
65
+ "access": "restricted"
66
+ },
67
+ "devDependencies": {
68
+ "@changesets/cli": "^2.27.1",
69
+ "@commitlint/cli": "^19.5.0",
70
+ "@commitlint/config-conventional": "^19.5.0",
71
+ "@eslint/compat": "^1.3.2",
72
+ "@eslint/eslintrc": "^3.3.1",
73
+ "@eslint/js": "^9.35.0",
74
+ "@react-native/babel-preset": "0.83.0",
75
+ "@react-native/eslint-config": "0.83.0",
76
+ "@types/react": "^19.2.0",
77
+ "del-cli": "^6.0.0",
78
+ "eslint": "^9.35.0",
79
+ "eslint-config-prettier": "^10.1.8",
80
+ "eslint-plugin-prettier": "^5.5.4",
81
+ "husky": "^9.0.11",
82
+ "lint-staged": "^15.2.2",
83
+ "prettier": "^2.8.8",
84
+ "react": "19.2.0",
85
+ "react-native": "0.83.0",
86
+ "react-native-builder-bob": "^0.40.17",
87
+ "turbo": "^2.5.6",
88
+ "typescript": "^5.9.2"
89
+ },
90
+ "peerDependencies": {
91
+ "react": "*",
92
+ "react-native": "*"
93
+ },
94
+ "workspaces": [
95
+ "example"
96
+ ],
97
+ "packageManager": "yarn@4.11.0",
98
+ "react-native-builder-bob": {
99
+ "source": "src",
100
+ "output": "lib",
101
+ "targets": [
102
+ [
103
+ "module",
104
+ {
105
+ "esm": true
106
+ }
107
+ ],
108
+ [
109
+ "typescript",
110
+ {
111
+ "project": "tsconfig.build.json"
112
+ }
113
+ ]
114
+ ]
115
+ },
116
+ "codegenConfig": {
117
+ "name": "ReactNativeImageToWebpSpec",
118
+ "type": "modules",
119
+ "jsSrcsDir": "src",
120
+ "android": {
121
+ "javaPackageName": "com.dynlabs.reactnativeimagetowebp"
122
+ }
123
+ },
124
+ "prettier": {
125
+ "quoteProps": "consistent",
126
+ "singleQuote": true,
127
+ "tabWidth": 2,
128
+ "trailingComma": "es5",
129
+ "useTabs": false
130
+ },
131
+ "create-react-native-library": {
132
+ "type": "turbo-module",
133
+ "languages": "kotlin-objc",
134
+ "tools": [
135
+ "eslint"
136
+ ],
137
+ "version": "0.57.0"
138
+ }
139
+ }
@@ -0,0 +1,32 @@
1
+ import { TurboModuleRegistry, type TurboModule } from 'react-native';
2
+
3
+ export type ConvertPreset =
4
+ | 'balanced'
5
+ | 'small'
6
+ | 'fast'
7
+ | 'lossless'
8
+ | 'document';
9
+
10
+ export interface ConvertOptions {
11
+ inputPath: string;
12
+ outputPath?: string;
13
+ preset?: ConvertPreset;
14
+ maxLongEdge?: number;
15
+ quality?: number;
16
+ method?: number;
17
+ lossless?: boolean;
18
+ stripMetadata?: boolean;
19
+ }
20
+
21
+ export interface ConvertResult {
22
+ outputPath: string;
23
+ width: number;
24
+ height: number;
25
+ sizeBytes: number;
26
+ }
27
+
28
+ export interface Spec extends TurboModule {
29
+ convertImageToWebP(options: ConvertOptions): Promise<ConvertResult>;
30
+ }
31
+
32
+ export default TurboModuleRegistry.getEnforcing<Spec>('ReactNativeImageToWebp');
package/src/index.tsx ADDED
@@ -0,0 +1,109 @@
1
+ import NativeReactNativeImageToWebp, {
2
+ type ConvertOptions,
3
+ type ConvertResult,
4
+ type ConvertPreset,
5
+ } from './NativeReactNativeImageToWebp';
6
+ import { validateOptions } from './validation';
7
+ import { applyPreset } from './presets';
8
+
9
+ export type { ConvertOptions, ConvertResult, ConvertPreset };
10
+
11
+ const ERROR_CODES = {
12
+ INVALID_INPUT: 'INVALID_INPUT',
13
+ FILE_NOT_FOUND: 'FILE_NOT_FOUND',
14
+ DECODE_FAILED: 'DECODE_FAILED',
15
+ ENCODE_FAILED: 'ENCODE_FAILED',
16
+ IO_ERROR: 'IO_ERROR',
17
+ UNSUPPORTED_FORMAT: 'UNSUPPORTED_FORMAT',
18
+ } as const;
19
+
20
+ export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
21
+
22
+ export class ImageToWebPError extends Error {
23
+ code: ErrorCode;
24
+
25
+ constructor(code: ErrorCode, message: string) {
26
+ super(message);
27
+ this.name = 'ImageToWebPError';
28
+ this.code = code;
29
+ Object.setPrototypeOf(this, ImageToWebPError.prototype);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Convert an image file to WebP format.
35
+ *
36
+ * @param options - Conversion options
37
+ * @returns Promise resolving to conversion result with output path and metadata
38
+ * @throws {ImageToWebPError} If conversion fails
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * const result = await convertImageToWebP({
43
+ * inputPath: '/path/to/image.jpg',
44
+ * preset: 'balanced',
45
+ * maxLongEdge: 2048,
46
+ * });
47
+ * console.log(`Output: ${result.outputPath}, Size: ${result.sizeBytes} bytes`);
48
+ * ```
49
+ */
50
+ export async function convertImageToWebP(
51
+ options: ConvertOptions
52
+ ): Promise<ConvertResult> {
53
+ // Validate input
54
+ const validationError = validateOptions(options);
55
+ if (validationError) {
56
+ throw new ImageToWebPError(validationError.code, validationError.message);
57
+ }
58
+
59
+ // Apply preset defaults
60
+ const finalOptions = applyPreset(options);
61
+
62
+ try {
63
+ return await NativeReactNativeImageToWebp.convertImageToWebP(finalOptions);
64
+ } catch (error) {
65
+ // Map native errors to our error types
66
+ if (error instanceof Error) {
67
+ const message = error.message;
68
+ if (message.includes('FILE_NOT_FOUND')) {
69
+ throw new ImageToWebPError(
70
+ ERROR_CODES.FILE_NOT_FOUND,
71
+ `File not found: ${options.inputPath}`
72
+ );
73
+ }
74
+ if (message.includes('DECODE_FAILED')) {
75
+ throw new ImageToWebPError(
76
+ ERROR_CODES.DECODE_FAILED,
77
+ `Failed to decode image: ${message}`
78
+ );
79
+ }
80
+ if (message.includes('ENCODE_FAILED')) {
81
+ throw new ImageToWebPError(
82
+ ERROR_CODES.ENCODE_FAILED,
83
+ `Failed to encode WebP: ${message}`
84
+ );
85
+ }
86
+ if (message.includes('IO_ERROR')) {
87
+ throw new ImageToWebPError(
88
+ ERROR_CODES.IO_ERROR,
89
+ `I/O error: ${message}`
90
+ );
91
+ }
92
+ if (message.includes('UNSUPPORTED_FORMAT')) {
93
+ throw new ImageToWebPError(
94
+ ERROR_CODES.UNSUPPORTED_FORMAT,
95
+ `Unsupported image format: ${message}`
96
+ );
97
+ }
98
+ if (message.includes('INVALID_INPUT')) {
99
+ throw new ImageToWebPError(
100
+ ERROR_CODES.INVALID_INPUT,
101
+ `Invalid input: ${message}`
102
+ );
103
+ }
104
+ }
105
+ throw error;
106
+ }
107
+ }
108
+
109
+ export { ERROR_CODES };
package/src/presets.ts ADDED
@@ -0,0 +1,80 @@
1
+ import type {
2
+ ConvertOptions,
3
+ ConvertPreset,
4
+ } from './NativeReactNativeImageToWebp';
5
+
6
+ interface PresetConfig {
7
+ quality?: number;
8
+ method?: number;
9
+ lossless?: boolean;
10
+ stripMetadata?: boolean;
11
+ threadLevel?: number;
12
+ exact?: boolean;
13
+ }
14
+
15
+ const PRESETS: Record<ConvertPreset, PresetConfig> = {
16
+ balanced: {
17
+ quality: 80,
18
+ method: 3,
19
+ lossless: false,
20
+ stripMetadata: true,
21
+ threadLevel: 1,
22
+ },
23
+ small: {
24
+ quality: 74,
25
+ method: 5,
26
+ lossless: false,
27
+ stripMetadata: true,
28
+ threadLevel: 1,
29
+ },
30
+ fast: {
31
+ quality: 78,
32
+ method: 1,
33
+ lossless: false,
34
+ stripMetadata: true,
35
+ threadLevel: 1,
36
+ },
37
+ lossless: {
38
+ lossless: true,
39
+ method: 4,
40
+ stripMetadata: true,
41
+ },
42
+ document: {
43
+ quality: 82,
44
+ method: 4,
45
+ lossless: false,
46
+ stripMetadata: true,
47
+ exact: true, // Will be set conditionally if alpha present
48
+ },
49
+ };
50
+
51
+ export function applyPreset(options: ConvertOptions): ConvertOptions {
52
+ const preset: ConvertPreset = options.preset || 'balanced';
53
+ const presetConfig = PRESETS[preset];
54
+
55
+ const result: ConvertOptions = {
56
+ ...options,
57
+ };
58
+
59
+ // Apply preset values only if not explicitly overridden
60
+ if (result.quality === undefined && presetConfig.quality !== undefined) {
61
+ result.quality = presetConfig.quality;
62
+ }
63
+ if (result.method === undefined && presetConfig.method !== undefined) {
64
+ result.method = presetConfig.method;
65
+ }
66
+ if (result.lossless === undefined && presetConfig.lossless !== undefined) {
67
+ result.lossless = presetConfig.lossless;
68
+ }
69
+ if (
70
+ result.stripMetadata === undefined &&
71
+ presetConfig.stripMetadata !== undefined
72
+ ) {
73
+ result.stripMetadata = presetConfig.stripMetadata;
74
+ }
75
+
76
+ // Note: threadLevel and exact are handled natively, not passed through JS API
77
+ // They are documented here for reference but applied in native code
78
+
79
+ return result;
80
+ }
@@ -0,0 +1,55 @@
1
+ import type { ConvertOptions } from './NativeReactNativeImageToWebp';
2
+ import type { ErrorCode } from './index';
3
+
4
+ interface ValidationError {
5
+ code: ErrorCode;
6
+ message: string;
7
+ }
8
+
9
+ export function validateOptions(
10
+ options: ConvertOptions
11
+ ): ValidationError | null {
12
+ if (!options.inputPath || typeof options.inputPath !== 'string') {
13
+ return {
14
+ code: 'INVALID_INPUT',
15
+ message: 'inputPath is required and must be a string',
16
+ };
17
+ }
18
+
19
+ if (options.maxLongEdge !== undefined) {
20
+ if (typeof options.maxLongEdge !== 'number' || options.maxLongEdge <= 0) {
21
+ return {
22
+ code: 'INVALID_INPUT',
23
+ message: 'maxLongEdge must be a positive number',
24
+ };
25
+ }
26
+ }
27
+
28
+ if (options.quality !== undefined) {
29
+ if (
30
+ typeof options.quality !== 'number' ||
31
+ options.quality < 0 ||
32
+ options.quality > 100
33
+ ) {
34
+ return {
35
+ code: 'INVALID_INPUT',
36
+ message: 'quality must be a number between 0 and 100',
37
+ };
38
+ }
39
+ }
40
+
41
+ if (options.method !== undefined) {
42
+ if (
43
+ typeof options.method !== 'number' ||
44
+ options.method < 0 ||
45
+ options.method > 6
46
+ ) {
47
+ return {
48
+ code: 'INVALID_INPUT',
49
+ message: 'method must be a number between 0 and 6',
50
+ };
51
+ }
52
+ }
53
+
54
+ return null;
55
+ }