@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/android/src/main/java/com/dynlabs/reactnativeimagetowebp/ReactNativeImageToWebpModule.kt
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
package com.dynlabs.reactnativeimagetowebp
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.BitmapFactory
|
|
5
|
+
import android.graphics.ImageDecoder
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import com.facebook.react.bridge.Arguments
|
|
9
|
+
import com.facebook.react.bridge.Promise
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
|
+
import com.facebook.react.bridge.ReadableMap
|
|
12
|
+
import com.facebook.react.bridge.WritableMap
|
|
13
|
+
import java.io.File
|
|
14
|
+
import java.io.FileOutputStream
|
|
15
|
+
import java.util.concurrent.ExecutorService
|
|
16
|
+
import java.util.concurrent.Executors
|
|
17
|
+
|
|
18
|
+
class ReactNativeImageToWebpModule(reactContext: ReactApplicationContext) :
|
|
19
|
+
NativeReactNativeImageToWebpSpec(reactContext) {
|
|
20
|
+
|
|
21
|
+
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
22
|
+
|
|
23
|
+
companion object {
|
|
24
|
+
init {
|
|
25
|
+
System.loadLibrary("react-native-image-to-webp")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const val NAME = NativeReactNativeImageToWebpSpec.NAME
|
|
29
|
+
|
|
30
|
+
private const val ERROR_CODE_INVALID_INPUT = "INVALID_INPUT"
|
|
31
|
+
private const val ERROR_CODE_FILE_NOT_FOUND = "FILE_NOT_FOUND"
|
|
32
|
+
private const val ERROR_CODE_DECODE_FAILED = "DECODE_FAILED"
|
|
33
|
+
private const val ERROR_CODE_ENCODE_FAILED = "ENCODE_FAILED"
|
|
34
|
+
private const val ERROR_CODE_IO_ERROR = "IO_ERROR"
|
|
35
|
+
private const val ERROR_CODE_UNSUPPORTED_FORMAT = "UNSUPPORTED_FORMAT"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Native JNI methods
|
|
39
|
+
private external fun nativeEncodeWebP(
|
|
40
|
+
rgbaData: ByteArray,
|
|
41
|
+
width: Int,
|
|
42
|
+
height: Int,
|
|
43
|
+
quality: Int,
|
|
44
|
+
method: Int,
|
|
45
|
+
lossless: Boolean,
|
|
46
|
+
outputPath: String
|
|
47
|
+
): Boolean
|
|
48
|
+
|
|
49
|
+
private external fun nativeGetLastError(): String
|
|
50
|
+
|
|
51
|
+
override fun convertImageToWebP(
|
|
52
|
+
options: ReadableMap,
|
|
53
|
+
promise: Promise
|
|
54
|
+
) {
|
|
55
|
+
executor.execute {
|
|
56
|
+
try {
|
|
57
|
+
val result = convertImageToWebPInternal(options)
|
|
58
|
+
promise.resolve(result)
|
|
59
|
+
} catch (e: Exception) {
|
|
60
|
+
promise.reject(
|
|
61
|
+
when (e) {
|
|
62
|
+
is FileNotFoundException -> ERROR_CODE_FILE_NOT_FOUND
|
|
63
|
+
is IllegalArgumentException -> ERROR_CODE_INVALID_INPUT
|
|
64
|
+
is UnsupportedOperationException -> ERROR_CODE_UNSUPPORTED_FORMAT
|
|
65
|
+
else -> ERROR_CODE_DECODE_FAILED
|
|
66
|
+
},
|
|
67
|
+
e.message ?: "Unknown error",
|
|
68
|
+
e
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private fun convertImageToWebPInternal(options: ReadableMap): WritableMap {
|
|
75
|
+
// Parse options
|
|
76
|
+
val inputPath = options.getString("inputPath")
|
|
77
|
+
?: throw IllegalArgumentException("inputPath is required")
|
|
78
|
+
|
|
79
|
+
val outputPath = options.getString("outputPath")
|
|
80
|
+
?: deriveOutputPath(inputPath)
|
|
81
|
+
|
|
82
|
+
val maxLongEdge = if (options.hasKey("maxLongEdge")) {
|
|
83
|
+
options.getDouble("maxLongEdge").toInt()
|
|
84
|
+
} else {
|
|
85
|
+
null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
val quality = if (options.hasKey("quality")) {
|
|
89
|
+
options.getInt("quality")
|
|
90
|
+
} else {
|
|
91
|
+
80
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
val method = if (options.hasKey("method")) {
|
|
95
|
+
options.getInt("method")
|
|
96
|
+
} else {
|
|
97
|
+
3
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
val lossless = options.hasKey("lossless") && options.getBoolean("lossless")
|
|
101
|
+
|
|
102
|
+
// Validate
|
|
103
|
+
if (maxLongEdge != null && maxLongEdge <= 0) {
|
|
104
|
+
throw IllegalArgumentException("maxLongEdge must be positive")
|
|
105
|
+
}
|
|
106
|
+
if (quality < 0 || quality > 100) {
|
|
107
|
+
throw IllegalArgumentException("quality must be between 0 and 100")
|
|
108
|
+
}
|
|
109
|
+
if (method < 0 || method > 6) {
|
|
110
|
+
throw IllegalArgumentException("method must be between 0 and 6")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check input file
|
|
114
|
+
val inputFile = File(inputPath)
|
|
115
|
+
if (!inputFile.exists() || !inputFile.canRead()) {
|
|
116
|
+
throw FileNotFoundException("File not found: $inputPath")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Decode image
|
|
120
|
+
val bitmap = decodeImage(inputFile, maxLongEdge)
|
|
121
|
+
?: throw RuntimeException("Failed to decode image")
|
|
122
|
+
|
|
123
|
+
val width = bitmap.width
|
|
124
|
+
val height = bitmap.height
|
|
125
|
+
|
|
126
|
+
// Convert bitmap to RGBA
|
|
127
|
+
val rgbaData = bitmapToRGBA(bitmap)
|
|
128
|
+
bitmap.recycle()
|
|
129
|
+
|
|
130
|
+
// Ensure output directory exists
|
|
131
|
+
val outputFile = File(outputPath)
|
|
132
|
+
outputFile.parentFile?.mkdirs()
|
|
133
|
+
|
|
134
|
+
// Encode to WebP using native code
|
|
135
|
+
val success = nativeEncodeWebP(
|
|
136
|
+
rgbaData,
|
|
137
|
+
width,
|
|
138
|
+
height,
|
|
139
|
+
quality,
|
|
140
|
+
method,
|
|
141
|
+
lossless,
|
|
142
|
+
outputPath
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if (!success) {
|
|
146
|
+
val errorMsg = nativeGetLastError()
|
|
147
|
+
throw RuntimeException("WebP encoding failed: $errorMsg")
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Get file size
|
|
151
|
+
val sizeBytes = outputFile.length()
|
|
152
|
+
|
|
153
|
+
// Return result
|
|
154
|
+
val result = Arguments.createMap()
|
|
155
|
+
result.putString("outputPath", outputPath)
|
|
156
|
+
result.putInt("width", width)
|
|
157
|
+
result.putInt("height", height)
|
|
158
|
+
result.putDouble("sizeBytes", sizeBytes.toDouble())
|
|
159
|
+
|
|
160
|
+
return result
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private fun decodeImage(file: File, maxLongEdge: Int?): Bitmap? {
|
|
164
|
+
return try {
|
|
165
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
166
|
+
// Use ImageDecoder for modern Android
|
|
167
|
+
val source = ImageDecoder.createSource(file)
|
|
168
|
+
val decoder = ImageDecoder.decodeBitmap(source) { decoder, info, source ->
|
|
169
|
+
// Apply sampling if maxLongEdge is specified
|
|
170
|
+
maxLongEdge?.let { maxEdge ->
|
|
171
|
+
val originalWidth = info.size.width
|
|
172
|
+
val originalHeight = info.size.height
|
|
173
|
+
val maxDimension = maxOf(originalWidth, originalHeight)
|
|
174
|
+
if (maxDimension > maxEdge) {
|
|
175
|
+
val sampleSize = (maxDimension / maxEdge).toInt().coerceAtLeast(1)
|
|
176
|
+
decoder.setTargetSampleSize(sampleSize)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
decoder
|
|
181
|
+
} else {
|
|
182
|
+
// Fallback to BitmapFactory
|
|
183
|
+
val options = BitmapFactory.Options().apply {
|
|
184
|
+
inJustDecodeBounds = true
|
|
185
|
+
}
|
|
186
|
+
BitmapFactory.decodeFile(file.absolutePath, options)
|
|
187
|
+
|
|
188
|
+
maxLongEdge?.let { maxEdge ->
|
|
189
|
+
val maxDimension = maxOf(options.outWidth, options.outHeight)
|
|
190
|
+
if (maxDimension > maxEdge) {
|
|
191
|
+
val sampleSize = (maxDimension / maxEdge).toInt().coerceAtLeast(1)
|
|
192
|
+
options.inSampleSize = sampleSize
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
options.inJustDecodeBounds = false
|
|
197
|
+
options.inPreferredConfig = Bitmap.Config.ARGB_8888
|
|
198
|
+
|
|
199
|
+
var bitmap = BitmapFactory.decodeFile(file.absolutePath, options)
|
|
200
|
+
?: return null
|
|
201
|
+
|
|
202
|
+
// Apply EXIF orientation if needed
|
|
203
|
+
bitmap = applyExifOrientation(bitmap, file)
|
|
204
|
+
|
|
205
|
+
// Final resize if still needed (inSampleSize is approximate)
|
|
206
|
+
maxLongEdge?.let { maxEdge ->
|
|
207
|
+
val currentMax = maxOf(bitmap.width, bitmap.height)
|
|
208
|
+
if (currentMax > maxEdge) {
|
|
209
|
+
val scale = maxEdge.toFloat() / currentMax
|
|
210
|
+
val newWidth = (bitmap.width * scale).toInt()
|
|
211
|
+
val newHeight = (bitmap.height * scale).toInt()
|
|
212
|
+
val resized = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
|
|
213
|
+
if (resized != bitmap) {
|
|
214
|
+
bitmap.recycle()
|
|
215
|
+
bitmap = resized
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
bitmap
|
|
221
|
+
}
|
|
222
|
+
} catch (e: Exception) {
|
|
223
|
+
throw RuntimeException("Failed to decode image: ${e.message}", e)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private fun applyExifOrientation(bitmap: Bitmap, file: File): Bitmap {
|
|
228
|
+
// Note: For full EXIF support, use ExifInterface
|
|
229
|
+
// For now, return bitmap as-is
|
|
230
|
+
// TODO: Implement EXIF orientation handling using android.media.ExifInterface
|
|
231
|
+
return bitmap
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private fun bitmapToRGBA(bitmap: Bitmap): ByteArray {
|
|
235
|
+
val width = bitmap.width
|
|
236
|
+
val height = bitmap.height
|
|
237
|
+
val pixels = IntArray(width * height)
|
|
238
|
+
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
|
|
239
|
+
|
|
240
|
+
val rgbaData = ByteArray(width * height * 4)
|
|
241
|
+
var index = 0
|
|
242
|
+
for (pixel in pixels) {
|
|
243
|
+
rgbaData[index++] = ((pixel shr 16) and 0xFF).toByte() // R
|
|
244
|
+
rgbaData[index++] = ((pixel shr 8) and 0xFF).toByte() // G
|
|
245
|
+
rgbaData[index++] = (pixel and 0xFF).toByte() // B
|
|
246
|
+
rgbaData[index++] = ((pixel shr 24) and 0xFF).toByte() // A
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return rgbaData
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private fun deriveOutputPath(inputPath: String): String {
|
|
253
|
+
val inputFile = File(inputPath)
|
|
254
|
+
val directory = inputFile.parent
|
|
255
|
+
val filename = inputFile.nameWithoutExtension
|
|
256
|
+
return File(directory, "$filename.webp").absolutePath
|
|
257
|
+
}
|
|
258
|
+
}
|
package/android/src/main/java/com/dynlabs/reactnativeimagetowebp/ReactNativeImageToWebpPackage.kt
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.dynlabs.reactnativeimagetowebp
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import java.util.HashMap
|
|
9
|
+
|
|
10
|
+
class ReactNativeImageToWebpPackage : BaseReactPackage() {
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
|
+
return if (name == ReactNativeImageToWebpModule.NAME) {
|
|
13
|
+
ReactNativeImageToWebpModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
20
|
+
return ReactModuleInfoProvider {
|
|
21
|
+
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
|
|
22
|
+
moduleInfos[ReactNativeImageToWebpModule.NAME] = ReactModuleInfo(
|
|
23
|
+
ReactNativeImageToWebpModule.NAME,
|
|
24
|
+
ReactNativeImageToWebpModule.NAME,
|
|
25
|
+
false, // canOverrideExistingModule
|
|
26
|
+
false, // needsEagerInit
|
|
27
|
+
false, // isCxxModule
|
|
28
|
+
true // isTurboModule
|
|
29
|
+
)
|
|
30
|
+
moduleInfos
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#include "ImageToWebP.h"
|
|
2
|
+
#include <fstream>
|
|
3
|
+
#include <vector>
|
|
4
|
+
#include <cstring>
|
|
5
|
+
|
|
6
|
+
// libwebp includes (uncomment after vendoring libwebp)
|
|
7
|
+
#ifdef WEBP_AVAILABLE
|
|
8
|
+
#include "webp/encode.h"
|
|
9
|
+
#include "webp/mux.h"
|
|
10
|
+
#endif
|
|
11
|
+
|
|
12
|
+
WebPEncodeResult encodeWebP(
|
|
13
|
+
const uint8_t* rgbaData,
|
|
14
|
+
uint32_t width,
|
|
15
|
+
uint32_t height,
|
|
16
|
+
const WebPEncodeOptions& options,
|
|
17
|
+
const std::string& outputPath) {
|
|
18
|
+
WebPEncodeResult result;
|
|
19
|
+
result.width = width;
|
|
20
|
+
result.height = height;
|
|
21
|
+
|
|
22
|
+
#ifdef WEBP_AVAILABLE
|
|
23
|
+
// Initialize WebP config
|
|
24
|
+
WebPConfig config;
|
|
25
|
+
if (!WebPConfigInit(&config)) {
|
|
26
|
+
result.success = false;
|
|
27
|
+
result.errorMessage = "Failed to initialize WebP config";
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Apply preset
|
|
32
|
+
WebPPreset preset = WEBP_PRESET_DEFAULT;
|
|
33
|
+
if (options.lossless) {
|
|
34
|
+
preset = WEBP_PRESET_DEFAULT; // Will be overridden by lossless flag
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!WebPConfigPreset(&config, preset, options.quality)) {
|
|
38
|
+
result.success = false;
|
|
39
|
+
result.errorMessage = "Failed to configure WebP preset";
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Override with options
|
|
44
|
+
config.quality = static_cast<float>(options.quality);
|
|
45
|
+
config.method = options.method;
|
|
46
|
+
config.lossless = options.lossless ? 1 : 0;
|
|
47
|
+
config.exact = options.exact ? 1 : 0;
|
|
48
|
+
|
|
49
|
+
// Thread level (if supported)
|
|
50
|
+
#ifdef WEBP_USE_THREAD
|
|
51
|
+
config.thread_level = options.threadLevel;
|
|
52
|
+
#endif
|
|
53
|
+
|
|
54
|
+
// Validate config
|
|
55
|
+
if (!WebPValidateConfig(&config)) {
|
|
56
|
+
result.success = false;
|
|
57
|
+
result.errorMessage = "Invalid WebP config";
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Initialize picture
|
|
62
|
+
WebPPicture picture;
|
|
63
|
+
if (!WebPPictureInit(&picture)) {
|
|
64
|
+
result.success = false;
|
|
65
|
+
result.errorMessage = "Failed to initialize WebP picture";
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
picture.width = static_cast<int>(width);
|
|
70
|
+
picture.height = static_cast<int>(height);
|
|
71
|
+
picture.use_argb = options.lossless ? 1 : 0; // Use ARGB for lossless
|
|
72
|
+
|
|
73
|
+
// Import RGBA data
|
|
74
|
+
if (!WebPPictureImportRGBA(&picture, rgbaData, static_cast<int>(width * 4))) {
|
|
75
|
+
WebPPictureFree(&picture);
|
|
76
|
+
result.success = false;
|
|
77
|
+
result.errorMessage = "Failed to import RGBA data";
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Setup memory writer for output
|
|
82
|
+
WebPMemoryWriter writer;
|
|
83
|
+
WebPMemoryWriterInit(&writer);
|
|
84
|
+
picture.writer = WebPMemoryWriterWrite;
|
|
85
|
+
picture.custom_ptr = &writer;
|
|
86
|
+
|
|
87
|
+
// Encode
|
|
88
|
+
if (!WebPEncode(&config, &picture)) {
|
|
89
|
+
WebPMemoryWriterClear(&writer);
|
|
90
|
+
WebPPictureFree(&picture);
|
|
91
|
+
result.success = false;
|
|
92
|
+
result.errorMessage = std::string("WebP encoding failed: ") + std::to_string(picture.error_code);
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Write to file
|
|
97
|
+
std::ofstream outFile(outputPath, std::ios::binary);
|
|
98
|
+
if (!outFile.is_open()) {
|
|
99
|
+
WebPMemoryWriterClear(&writer);
|
|
100
|
+
WebPPictureFree(&picture);
|
|
101
|
+
result.success = false;
|
|
102
|
+
result.errorMessage = "Failed to open output file for writing";
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
outFile.write(reinterpret_cast<const char*>(writer.mem), writer.size);
|
|
107
|
+
if (!outFile.good()) {
|
|
108
|
+
WebPMemoryWriterClear(&writer);
|
|
109
|
+
WebPPictureFree(&picture);
|
|
110
|
+
result.success = false;
|
|
111
|
+
result.errorMessage = "Failed to write WebP data to file";
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
outFile.close();
|
|
115
|
+
|
|
116
|
+
// Set result
|
|
117
|
+
result.success = true;
|
|
118
|
+
result.sizeBytes = writer.size;
|
|
119
|
+
|
|
120
|
+
// Cleanup
|
|
121
|
+
WebPMemoryWriterClear(&writer);
|
|
122
|
+
WebPPictureFree(&picture);
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
#else
|
|
126
|
+
// Placeholder until libwebp is vendored
|
|
127
|
+
result.success = false;
|
|
128
|
+
result.errorMessage = "libwebp not yet integrated. Please vendor libwebp sources to cpp/vendor/libwebp/ and define WEBP_AVAILABLE.";
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
#endif
|
|
132
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#ifndef IMAGE_TO_WEBP_H
|
|
2
|
+
#define IMAGE_TO_WEBP_H
|
|
3
|
+
|
|
4
|
+
#include <cstdint>
|
|
5
|
+
#include <string>
|
|
6
|
+
|
|
7
|
+
struct WebPEncodeOptions {
|
|
8
|
+
int quality = 80;
|
|
9
|
+
int method = 3;
|
|
10
|
+
bool lossless = false;
|
|
11
|
+
bool stripMetadata = true;
|
|
12
|
+
int threadLevel = 1;
|
|
13
|
+
bool exact = false;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
struct WebPEncodeResult {
|
|
17
|
+
bool success = false;
|
|
18
|
+
std::string errorMessage;
|
|
19
|
+
uint32_t width = 0;
|
|
20
|
+
uint32_t height = 0;
|
|
21
|
+
size_t sizeBytes = 0;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Encode RGBA image data to WebP format.
|
|
26
|
+
*
|
|
27
|
+
* @param rgbaData Pointer to RGBA pixel data (width * height * 4 bytes)
|
|
28
|
+
* @param width Image width in pixels
|
|
29
|
+
* @param height Image height in pixels
|
|
30
|
+
* @param options Encoding options
|
|
31
|
+
* @param outputPath Path to write the WebP file
|
|
32
|
+
* @return Result containing success status, dimensions, and file size
|
|
33
|
+
*/
|
|
34
|
+
WebPEncodeResult encodeWebP(
|
|
35
|
+
const uint8_t* rgbaData,
|
|
36
|
+
uint32_t width,
|
|
37
|
+
uint32_t height,
|
|
38
|
+
const WebPEncodeOptions& options,
|
|
39
|
+
const std::string& outputPath);
|
|
40
|
+
|
|
41
|
+
#endif // IMAGE_TO_WEBP_H
|
package/cpp/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# C++ Shared Code
|
|
2
|
+
|
|
3
|
+
This directory contains shared C++ code for WebP encoding using libwebp.
|
|
4
|
+
|
|
5
|
+
## libwebp Vendor Setup
|
|
6
|
+
|
|
7
|
+
To set up libwebp:
|
|
8
|
+
|
|
9
|
+
1. Download libwebp source (>= 1.6.0) from https://github.com/webmproject/libwebp
|
|
10
|
+
2. Extract to `cpp/vendor/libwebp/`
|
|
11
|
+
3. The build systems (CMake for Android, CocoaPods for iOS) will compile libwebp as a static library
|
|
12
|
+
|
|
13
|
+
## Structure
|
|
14
|
+
|
|
15
|
+
- `ImageToWebP.h` / `ImageToWebP.cpp`: C++ wrapper around libwebp encoding API
|
|
16
|
+
- `vendor/libwebp/`: Vendored libwebp source (to be added)
|
|
17
|
+
|
|
18
|
+
## Build Integration
|
|
19
|
+
|
|
20
|
+
- **Android**: CMakeLists.txt includes libwebp sources and links statically
|
|
21
|
+
- **iOS**: Podspec includes libwebp sources and compiles with the module
|
package/cpp/SETUP.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# libwebp Setup Guide
|
|
2
|
+
|
|
3
|
+
This library uses [libwebp](https://github.com/webmproject/libwebp) (>= 1.6.0) for WebP encoding.
|
|
4
|
+
|
|
5
|
+
## Quick Setup
|
|
6
|
+
|
|
7
|
+
**You only need to vendor libwebp sources - everything else is already configured!**
|
|
8
|
+
|
|
9
|
+
### Option 1: Use the Script (Recommended)
|
|
10
|
+
|
|
11
|
+
**Windows (PowerShell):**
|
|
12
|
+
```powershell
|
|
13
|
+
.\scripts\vendor-libwebp.ps1
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**macOS/Linux:**
|
|
17
|
+
```bash
|
|
18
|
+
chmod +x scripts/vendor-libwebp.sh
|
|
19
|
+
./scripts/vendor-libwebp.sh
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Or via yarn:**
|
|
23
|
+
```bash
|
|
24
|
+
yarn vendor:libwebp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Option 2: Manual Setup
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cd cpp
|
|
31
|
+
git clone https://github.com/webmproject/libwebp.git vendor/libwebp
|
|
32
|
+
cd vendor/libwebp
|
|
33
|
+
git checkout v1.6.0
|
|
34
|
+
cd ../../..
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## What Happens Next
|
|
38
|
+
|
|
39
|
+
Once libwebp is vendored:
|
|
40
|
+
|
|
41
|
+
1. **Build files auto-detect libwebp** - Both iOS (`ReactNativeImageToWebp.podspec`) and Android (`CMakeLists.txt`) automatically detect and include libwebp sources if they exist at `cpp/vendor/libwebp/src`
|
|
42
|
+
|
|
43
|
+
2. **C++ code is ready** - The implementation in `cpp/ImageToWebP.cpp` is already complete and will compile once libwebp is present (it's behind `#ifdef WEBP_AVAILABLE`)
|
|
44
|
+
|
|
45
|
+
3. **Build flags are set** - Release flags (`-O3 -DNDEBUG`) and `WEBP_AVAILABLE` macro are automatically configured
|
|
46
|
+
|
|
47
|
+
## Verification
|
|
48
|
+
|
|
49
|
+
After vendoring, verify libwebp is detected:
|
|
50
|
+
|
|
51
|
+
- **iOS**: Run `pod install` in `example/ios` - you should see libwebp sources included
|
|
52
|
+
- **Android**: Build will show "libwebp found, including sources" message
|
|
53
|
+
|
|
54
|
+
## Current Status
|
|
55
|
+
|
|
56
|
+
✅ Build files configured (auto-detect libwebp)
|
|
57
|
+
✅ C++ implementation ready (behind `#ifdef WEBP_AVAILABLE`)
|
|
58
|
+
✅ Compiler flags configured
|
|
59
|
+
⏳ **You need to:** Vendor libwebp sources (run the script above)
|
|
60
|
+
|
|
61
|
+
## Implementation Details
|
|
62
|
+
|
|
63
|
+
The `cpp/ImageToWebP.cpp` file uses libwebp's advanced API:
|
|
64
|
+
|
|
65
|
+
1. `WebPConfigInit()` and `WebPConfigPreset()` for configuration
|
|
66
|
+
2. `WebPPictureInit()` and `WebPPictureImportRGBA()` for image data
|
|
67
|
+
3. `WebPMemoryWriter` for output buffering
|
|
68
|
+
4. `WebPEncode()` for encoding
|
|
69
|
+
5. Proper cleanup of all libwebp structures
|
|
70
|
+
|
|
71
|
+
See libwebp documentation: https://developers.google.com/speed/webp/docs/api
|