Install Emscripten
Emscripten is a compiler tool to compile C and C++ code into WebAssembly. We could install Emscripten by following its installation guide, but I recommend using its docker image to avoid some installation hassles. All you need to do now is to install Docker environment on your computer.
Prepare C Code
In order to use the C library, we must first define some interfaces for calls between js and C code. So let's we download C library code first.
git clone https://github.com/webmproject/libwebp
Then we need to write our webp.c
code which contains all apis we need.
#include "emscripten.h"
#include <stdlib.h>
// include this header to use webp encoder function
#include "src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE
int version()
{
return WebPGetEncoderVersion();
}
// Allocate memory for image data based on width and height
EMSCRIPTEN_KEEPALIVE
uint8_t *create_buffer(int width, int height)
{
return malloc(width * height * 4 * sizeof(uint8_t));
}
// free this memory
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t *p)
{
free(p);
}
int result[2];
// this is the function to encode png into webp
EMSCRIPTEN_KEEPALIVE
void encode(uint8_t *img_in, int width, int height, float quality)
{
uint8_t *img_out;
size_t size;
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
// we can see here, the output image data saved in result variable
result[0] = (int)img_out;
result[1] = size;
}
// free memory
EMSCRIPTEN_KEEPALIVE
void free_result(uint8_t *result)
{
WebPFree(result);
}
// get pointer for resule image data
EMSCRIPTEN_KEEPALIVE
int get_result_pointer()
{
return result[0];
}
// get pointer for result image size
EMSCRIPTEN_KEEPALIVE
int get_result_size()
{
return result[1];
}
The api is simple, create_buffer
is used to allocate memory for input image data, encode
is used to convert png into webp format, get_result_pointer
and get_result_size
are used to retrieve result data.
As you can see, we should use macro EMSCRIPTEN_KEEPALIVE
to export functions.
Compile C into WebAssembly
Now we have docker environment, C library code and C api code, it's time to compile C code into WebAssembly. The compiling command is as follows.
docker run \
--rm \
-v $(pwd):/src \
emscripten/emsdk \
emcc -O1 \
-s WASM=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
webp.c libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c \
-o webp.js
Next, let's look at each option one by one to see what it means.
docker run
: to run a docker image--rm
: remove container after running-v $(pwd):/src
: bind current directory from the host in the container into the/src
directoryemscripten/emsdk
: get the latest tag of this containeremcc
: begin to use emscripten to compile-O1
: stands for simple optimizations-s WASM=1
: to output wasm file-s ALLOW_MEMORY_GROWTH=1
: allows the total amount of memory used to change depending on the demands of the application-s EXPORTED_RUNTIME_METHODS='["cwrap"]'
: to exportcwrap
function-I libwebp
: to specify C header fileswebp.c libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c
: this is C code to compile-o webp.js
: to outputwebp.js
file
If everything is ok, we should see two files generated: webp.js
and webp.wasm
.
Use WebAssembly
Now it's time that we call the api defined above to do the real job.
The process is simple.
- load
webp.js
file, in which it will try to loadwebp.wasm
file - wrap the C apis
- load png image
- allocate memory and set image data in it
- convert image and get the result
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./webp.js"></script>
<script>
let api;
Module.onRuntimeInitialized = async _ => {
// wrap c functions
api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
encode: Module.cwrap("encode", "", ["number", "number", "number", "number",]),
free_result: Module.cwrap("free_result", "", ["number"]),
get_result_pointer: Module.cwrap("get_result_pointer", "number", []),
get_result_size: Module.cwrap("get_result_size", "number", []),
};
// test version api
console.log(api.version());
(async () => {
// load png image
const image = await loadImage('./image.png');
// allocate memory
const p = api.create_buffer(image.width, image.height);
// set image data into memory
Module.HEAP8.set(image.data, p);
// convert
api.encode(p, image.width, image.height, 100);
// get result
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
// free memory
api.free_result(resultPointer);
api.destroy_buffer(p);
// show result
const blob = new Blob([result], { type: 'image/webp' });
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img)
})()
};
async function loadImage(src) {
// Load image
const imgBlob = await fetch(src).then(resp => resp.blob());
const img = await createImageBitmap(imgBlob);
// Make canvas same size as image
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// Draw image onto canvas
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
</script>
</body>
</html>
Now open the html file and we can see image file size is reduced from to 323KB to 201KB.