@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.
- package/LICENSE +20 -0
- package/README.md +247 -0
- package/ReactNativeImageToWebp.podspec +35 -0
- package/android/build.gradle +85 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/CMakeLists.txt +67 -0
- package/android/src/main/cpp/ImageToWebPJNI.cpp +73 -0
- package/android/src/main/java/com/dynlabs/reactnativeimagetowebp/ReactNativeImageToWebpModule.kt +258 -0
- package/android/src/main/java/com/dynlabs/reactnativeimagetowebp/ReactNativeImageToWebpPackage.kt +33 -0
- package/cpp/ImageToWebP.cpp +132 -0
- package/cpp/ImageToWebP.h +41 -0
- package/cpp/README.md +21 -0
- package/cpp/SETUP.md +71 -0
- package/ios/ReactNativeImageToWebp.h +5 -0
- package/ios/ReactNativeImageToWebp.mm +342 -0
- package/lib/module/NativeReactNativeImageToWebp.js +5 -0
- package/lib/module/NativeReactNativeImageToWebp.js.map +1 -0
- package/lib/module/index.js +78 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/presets.js +64 -0
- package/lib/module/presets.js.map +1 -0
- package/lib/module/validation.js +36 -0
- package/lib/module/validation.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeReactNativeImageToWebp.d.ts +24 -0
- package/lib/typescript/src/NativeReactNativeImageToWebp.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +35 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/presets.d.ts +3 -0
- package/lib/typescript/src/presets.d.ts.map +1 -0
- package/lib/typescript/src/validation.d.ts +9 -0
- package/lib/typescript/src/validation.d.ts.map +1 -0
- package/package.json +139 -0
- package/src/NativeReactNativeImageToWebp.ts +32 -0
- package/src/index.tsx +109 -0
- package/src/presets.ts +80 -0
- 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
|
+
}
|