Build webp

This commit is contained in:
Surma
2024-08-07 18:48:23 +01:00
parent a0f9fea679
commit b5bd766a4e
37 changed files with 473 additions and 6253 deletions

View File

@@ -18,6 +18,14 @@ LIBWEBP_FLAGS = -I${WEBP}/include -L${WEBP}/lib -lwebp
$(OUT_JS):
$(LD) \
-O3 \
-flto \
-std=c++17 \
-s FILESYSTEM=0 \
-s PTHREAD_POOL_SIZE=navigator.hardwareConcurrency \
-s ALLOW_MEMORY_GROWTH=1 \
-s TEXTDECODER=2 \
-s NODEJS_CATCH_EXIT=0 -s NODEJS_CATCH_REJECTION=0 \
$(LIBWEBP_FLAGS) \
$(LDFLAGS) \
-lembind \
@@ -28,6 +36,14 @@ $(OUT_JS):
%.o: %.cpp
$(CXX) -c \
-O3 \
-flto \
-std=c++17 \
-s FILESYSTEM=0 \
-s PTHREAD_POOL_SIZE=navigator.hardwareConcurrency \
-s ALLOW_MEMORY_GROWTH=1 \
-s TEXTDECODER=2 \
-s NODEJS_CATCH_EXIT=0 -s NODEJS_CATCH_REJECTION=0 \
$(LIBWEBP_FLAGS) \
$(CXXFLAGS) \
-o $@ \

View File

@@ -14,17 +14,27 @@
flake-utils,
webp-src,
}:
let
optionSets = {
base = {
simd = false;
};
simd = {
simd = true;
};
};
in
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
packageBuilder =
with pkgs;
rec {
packages = rec {
default = webp-squoosh;
webp-squoosh = stdenv.mkDerivation {
name = "mozjpeg-squoosh";
name:
{ simd }:
{
"webp-squoosh-${name}" = stdenv.mkDerivation {
name = "webp-squoosh-${name}";
# Only copy files that are actually relevant to avoid unnecessary
# cache invalidations.
src = runCommand "src" { } ''
@@ -35,9 +45,9 @@
'';
nativeBuildInputs = [
emscripten
webp
self.packages.${system}."webp-${name}"
];
WEBP = webp;
WEBP = self.packages.${system}."webp-${name}";
dontConfigure = true;
buildPhase = ''
export HOME=$TMPDIR
@@ -48,15 +58,11 @@
cp -r enc dec $out
'';
};
webp = stdenv.mkDerivation {
name = "webp";
"webp-${name}" = stdenv.mkDerivation {
name = "webp-${name}";
src = webp-src;
nativeBuildInputs = [
# autoconf
# automake
# libtool
emscripten
# pkg-config
cmake
];
configurePhase = ''
@@ -76,6 +82,7 @@
-DWEBP_BUILD_WEBPINFO=0 \
-DWEBP_BUILD_WEBPMUX=0 \
-DWEBP_BUILD_EXTRAS=0 \
${if simd then "-DWEBP_ENABLE_SIMD=1" else ""} \
-B $TMPDIR/build \
.
'';
@@ -90,16 +97,29 @@
'';
dontFixup = true;
};
"install-${name}" = writeShellScriptBin "install.sh" ''
${pkgs.coreutils}/bin/mkdir -p wasm_build/${name}
${pkgs.rsync}/bin/rsync --chmod=u+w -r ${
self.packages.${system}."webp-squoosh-${name}"
}/* wasm_build/${name}
'';
};
forEachOption = pkgs.callPackage (import ../../nix/for-each-option.nix) { };
packageVariants = forEachOption packageBuilder optionSets;
in
with pkgs;
{
packages = packageVariants // {
installScript = writeShellScriptBin "install.sh" ''
${pkgs.coreutils}/bin/rm -rf wasm_build
${pkgs.coreutils}/bin/mkdir -p wasm_build
${pkgs.rsync}/bin/rsync --chmod=u+w -r ${webp-squoosh}/* wasm_build/
${self.packages.${system}.install-base}/bin/install.sh
${self.packages.${system}.install-simd}/bin/install.sh
'';
};
apps = {
install = {
type = "app";
program = "${packages.installScript}/bin/install.sh";
program = "${self.packages.${system}.installScript}/bin/install.sh";
};
};
}

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
# WebP decoder
- Source: <https://github.com/webmproject/libwebp>
- Version: v1.0.2
## Example
See `example.html`
## API
### `int version()`
Returns the version of libwebp as a number. va.b.c is encoded as 0x0a0b0c
### `RawImage decode(std::string buffer)`
Decodes the given webp buffer into raw RGBA. `RawImage` is a class with 3 fields: `buffer`, `width`, and `height`.

View File

@@ -0,0 +1,20 @@
<!doctype html>
<script src='webp_dec.js'></script>
<script>
async function loadFile(src) {
const resp = await fetch(src);
return await resp.arrayBuffer();
}
webp_dec().then(async module => {
console.log('Version:', module.version().toString(16));
const image = await loadFile('../../example.webp');
const imageData = module.decode(image);
const canvas = document.createElement('canvas');
canvas.width = imageData.width;
canvas.height = imageData.height;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
ctx.putImageData(imageData, 0, 0);
});
</script>

View File

@@ -0,0 +1,29 @@
#include <string>
#include "emscripten/bind.h"
#include "emscripten/val.h"
#include "webp/decode.h"
#include "webp/demux.h"
using namespace emscripten;
int version() {
return WebPGetDecoderVersion();
}
thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");
thread_local const val ImageData = val::global("ImageData");
val decode(std::string buffer) {
int width, height;
std::unique_ptr<uint8_t[]> rgba(
WebPDecodeRGBA((const uint8_t*)buffer.c_str(), buffer.size(), &width, &height));
return rgba ? ImageData.new_(
Uint8ClampedArray.new_(typed_memory_view(width * height * 4, rgba.get())),
width, height)
: val::null();
}
EMSCRIPTEN_BINDINGS(my_module) {
function("decode", &decode);
function("version", &version);
}

View File

@@ -0,0 +1,7 @@
export interface WebPModule extends EmscriptenWasm.Module {
decode(data: BufferSource): ImageData | null;
}
declare var moduleFactory: EmscriptenWasm.ModuleFactory<WebPModule>;
export default moduleFactory;

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -0,0 +1,22 @@
# WebP encoder
- Source: <https://github.com/webmproject/libwebp>
- Version: v1.0.2
## Dependencies
- Docker
## Example
See `example.html`
## API
### `int version()`
Returns the version of libwebp as a number. va.b.c is encoded as 0x0a0b0c
### `UInt8Array encode(uint8_t* image_buffer, int image_width, int image_height, WebPConfig config)`
Encodes the given image with given dimension to WebP.

View File

@@ -0,0 +1,58 @@
<!doctype html>
<script src='webp_enc.js'></script>
<script>
async function loadImage(src) {
// Load image
const img = document.createElement('img');
img.src = src;
await new Promise(resolve => img.onload = resolve);
// Make canvas same size as image
const canvas = document.createElement('canvas');
[canvas.width, canvas.height] = [img.width, 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);
}
webp_enc().then(async module => {
console.log('Version:', module.version().toString(16));
const image = await loadImage('../../example.png');
const result = module.encode(image.data, image.width, image.height, {
quality: 75,
target_size: 0,
target_PSNR: 0,
method: 4,
sns_strength: 50,
filter_strength: 60,
filter_sharpness: 0,
filter_type: 1,
partitions: 0,
segments: 4,
pass: 1,
show_compressed: 0,
preprocessing: 0,
autofilter: 0,
partition_limit: 0,
alpha_compression: 1,
alpha_filtering: 1,
alpha_quality: 100,
lossless: 0,
exact: 0,
image_hint: 0,
emulate_jpeg_size: 0,
thread_level: 0,
low_memory: 0,
near_lossless: 100,
use_delta_palette: 0,
use_sharp_yuv: 0,
});
console.log('size', result.length);
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);
});
</script>

View File

@@ -0,0 +1,85 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <stdlib.h>
#include <string.h>
#include <stdexcept>
#include "webp/encode.h"
using namespace emscripten;
int version() {
return WebPGetEncoderVersion();
}
thread_local const val Uint8Array = val::global("Uint8Array");
val encode(std::string img, int width, int height, WebPConfig config) {
auto img_in = (uint8_t*)img.c_str();
// A lot of this is duplicated from Encode in picture_enc.c
WebPPicture pic;
WebPMemoryWriter wrt;
int ok;
if (!WebPPictureInit(&pic)) {
// shouldn't happen, except if system installation is broken
return val::null();
}
// Allow quality to go higher than 0.
config.qmax = 100;
// Only use use_argb if we really need it, as it's slower.
pic.use_argb = config.lossless || config.use_sharp_yuv || config.preprocessing > 0;
pic.width = width;
pic.height = height;
pic.writer = WebPMemoryWrite;
pic.custom_ptr = &wrt;
WebPMemoryWriterInit(&wrt);
ok = WebPPictureImportRGBA(&pic, img_in, width * 4) && WebPEncode(&config, &pic);
WebPPictureFree(&pic);
val js_result = ok ? Uint8Array.new_(typed_memory_view(wrt.size, wrt.mem)) : val::null();
WebPMemoryWriterClear(&wrt);
return js_result;
}
EMSCRIPTEN_BINDINGS(my_module) {
enum_<WebPImageHint>("WebPImageHint")
.value("WEBP_HINT_DEFAULT", WebPImageHint::WEBP_HINT_DEFAULT)
.value("WEBP_HINT_PICTURE", WebPImageHint::WEBP_HINT_PICTURE)
.value("WEBP_HINT_PHOTO", WebPImageHint::WEBP_HINT_PHOTO)
.value("WEBP_HINT_GRAPH", WebPImageHint::WEBP_HINT_GRAPH);
value_object<WebPConfig>("WebPConfig")
.field("lossless", &WebPConfig::lossless)
.field("quality", &WebPConfig::quality)
.field("method", &WebPConfig::method)
.field("image_hint", &WebPConfig::image_hint)
.field("target_size", &WebPConfig::target_size)
.field("target_PSNR", &WebPConfig::target_PSNR)
.field("segments", &WebPConfig::segments)
.field("sns_strength", &WebPConfig::sns_strength)
.field("filter_strength", &WebPConfig::filter_strength)
.field("filter_sharpness", &WebPConfig::filter_sharpness)
.field("filter_type", &WebPConfig::filter_type)
.field("autofilter", &WebPConfig::autofilter)
.field("alpha_compression", &WebPConfig::alpha_compression)
.field("alpha_filtering", &WebPConfig::alpha_filtering)
.field("alpha_quality", &WebPConfig::alpha_quality)
.field("pass", &WebPConfig::pass)
.field("show_compressed", &WebPConfig::show_compressed)
.field("preprocessing", &WebPConfig::preprocessing)
.field("partitions", &WebPConfig::partitions)
.field("partition_limit", &WebPConfig::partition_limit)
.field("emulate_jpeg_size", &WebPConfig::emulate_jpeg_size)
.field("low_memory", &WebPConfig::low_memory)
.field("near_lossless", &WebPConfig::near_lossless)
.field("exact", &WebPConfig::exact)
.field("use_delta_palette", &WebPConfig::use_delta_palette)
.field("use_sharp_yuv", &WebPConfig::use_sharp_yuv);
function("version", &version);
function("encode", &encode);
}

View File

@@ -0,0 +1,42 @@
export interface EncodeOptions {
quality: number;
target_size: number;
target_PSNR: number;
method: number;
sns_strength: number;
filter_strength: number;
filter_sharpness: number;
filter_type: number;
partitions: number;
segments: number;
pass: number;
show_compressed: number;
preprocessing: number;
autofilter: number;
partition_limit: number;
alpha_compression: number;
alpha_filtering: number;
alpha_quality: number;
lossless: number;
exact: number;
image_hint: number;
emulate_jpeg_size: number;
thread_level: number;
low_memory: number;
near_lossless: number;
use_delta_palette: number;
use_sharp_yuv: number;
}
export interface WebPModule extends EmscriptenWasm.Module {
encode(
data: BufferSource,
width: number,
height: number,
options: EncodeOptions,
): Uint8Array | null;
}
declare var moduleFactory: EmscriptenWasm.ModuleFactory<WebPModule>;
export default moduleFactory;

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -0,0 +1 @@
export { default } from './webp_enc.js';

6
nix/for-each-option.nix Normal file
View File

@@ -0,0 +1,6 @@
{ lib }:
f: opts:
let
derivations = (lib.attrsets.mapAttrsToList f opts);
in
lib.lists.fold (a: b: a // b) { } derivations

View File

@@ -10,14 +10,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { WebPModule } from 'codecs/webp/dec/webp_dec';
import type { WebPModule } from 'codecs/webp/wasm_build/base/dec/webp_dec';
import { initEmscriptenModule, blobToArrayBuffer } from 'features/worker-utils';
let emscriptenModule: Promise<WebPModule>;
export default async function decode(blob: Blob): Promise<ImageData> {
if (!emscriptenModule) {
const decoder = await import('codecs/webp/dec/webp_dec');
const decoder = await import('codecs/webp/wasm_build/base/dec/webp_dec');
emscriptenModule = initEmscriptenModule(decoder.default);
}

View File

@@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { EncodeOptions } from 'codecs/webp/enc/webp_enc';
import type { EncodeOptions } from 'codecs/webp/wasm_build/base/enc/webp_enc';
export { EncodeOptions };

View File

@@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { WebPModule } from 'codecs/webp/enc/webp_enc';
import type { WebPModule } from 'codecs/webp/wasm_build/base/enc/webp_enc';
import type { EncodeOptions } from '../shared/meta';
import { initEmscriptenModule } from 'features/worker-utils';
@@ -20,10 +20,12 @@ let emscriptenModule: Promise<WebPModule>;
async function init() {
if (await simd()) {
const webpEncoder = await import('codecs/webp/enc/webp_enc_simd');
const webpEncoder = await import(
'codecs/webp/wasm_build/simd/enc/webp_enc'
);
return initEmscriptenModule(webpEncoder.default);
}
const webpEncoder = await import('codecs/webp/enc/webp_enc');
const webpEncoder = await import('codecs/webp/wasm_build/base/enc/webp_enc');
return initEmscriptenModule(webpEncoder.default);
}

View File

@@ -25,7 +25,7 @@ import * as featuresWorker from 'entry-data:../features-worker';
// Decoders (some are feature detected)
import * as avifDec from 'entry-data:codecs/avif/dec/avif_dec';
import * as webpDec from 'entry-data:codecs/webp/dec/webp_dec';
import * as webpDec from 'entry-data:codecs/webp/wasm_build/base/dec/webp_dec';
// AVIF
import * as avifEncMt from 'entry-data:codecs/avif/enc/avif_enc_mt';
@@ -41,8 +41,8 @@ import * as oxiMt from 'entry-data:codecs/oxipng/pkg-parallel/squoosh_oxipng';
import * as oxi from 'entry-data:codecs/oxipng/pkg/squoosh_oxipng';
// WebP
import * as webpEncSimd from 'entry-data:codecs/webp/enc/webp_enc_simd';
import * as webpEnc from 'entry-data:codecs/webp/enc/webp_enc';
import * as webpEncSimd from 'entry-data:codecs/webp/wasm_build/simd/enc/webp_enc';
import * as webpEnc from 'entry-data:codecs/webp/wasm_build/base/enc/webp_enc';
// WP2
import * as wp2EncMtSimd from 'entry-data:codecs/wp2/enc/wp2_enc_mt_simd';