Merge v2 codecs (#844)

* wip

* doh, whitespace

* Updating emscripten, restoring export name

* Updating oxipng

* Build wasm

* Fix oxipng; upgrade Rust

* More v2-codec integration

* AVIF now working

* Non-working JXL

* Build hqx with Rust 1.40; refactor build-rust*.sh

* Set web target

* wp2 wip

* wp2 decode options

* Better logo height when loading the logo into squoosh

* Build oxi

* JAKE IS AN IDIOT

* wip oxipng

* Fixing case sensitive imports

* adding log

* another log

* Abort tasks when compress component removed

* Adding progressive option to JXL

* Fix bug going to & from original image

* Exposing epf in jxl

* logs

* Bypass initial CSS plugin

* Revert "logs"

* Adding root

* Fix for finding TSC on Windows

* Use spawn again

* Converting to module paths

* Remove spawnP

* silly

* oops

* logs

* Fixing glob paths in CSS plugin

* Path normalising

* Normalise paths for CSS plugin

* Normalise again

* Use correct func

* Adding lossless mode and near lossless (but hidden in UI)

* Removing useless comments

* Some logging

* Update JXL to v0.1. (#846)

* Rebuild JXL

* Adding slight loss option

Co-authored-by: Ingvar Stepanyan <rreverser@google.com>
Co-authored-by: Luca Versari <veluca93@gmail.com>
This commit is contained in:
Jake Archibald
2020-11-19 10:55:43 +00:00
committed by GitHub
parent 39ca054112
commit 7346511fa1
98 changed files with 20085 additions and 1884 deletions

View File

@@ -1,139 +1,84 @@
CODEC_URL = https://github.com/AOMediaCodec/libavif/archive/v0.8.1.tar.gz
CODEC_URL = https://github.com/AOMediaCodec/libavif/archive/31d7c6d1e32cf467ac24fb8c7a76c4902a4c00db.tar.gz
CODEC_PACKAGE = node_modules/libavif.tar.gz
CODEC_ENC_DIR = node_modules/libavif-enc
CODEC_ENC_BUILD_DIR := $(CODEC_ENC_DIR)/build
CODEC_ENC_OUT := $(CODEC_ENC_BUILD_DIR)/libavif.a
CODEC_DEC_DIR = node_modules/libavif-dec
CODEC_DEC_BUILD_DIR := $(CODEC_DEC_DIR)/build
CODEC_DEC_OUT := $(CODEC_DEC_BUILD_DIR)/libavif.a
LIBAOM_URL = https://aomedia.googlesource.com/aom/+archive/v2.0.0.tar.gz
LIBAOM_PACKAGE = node_modules/libaom.tar.gz
LIBAOM_ENC_DIR := $(CODEC_ENC_DIR)/ext/aom
LIBAOM_ENC_BUILD_DIR := $(LIBAOM_ENC_DIR)/build.libavif
LIBAOM_ENC_OUT := $(LIBAOM_ENC_BUILD_DIR)/libaom.a
LIBAOM_DEC_DIR := $(CODEC_DEC_DIR)/ext/aom
LIBAOM_DEC_BUILD_DIR := $(LIBAOM_DEC_DIR)/build.libavif
LIBAOM_DEC_OUT := $(LIBAOM_DEC_BUILD_DIR)/libaom.a
export CODEC_DIR = node_modules/libavif
BUILD_DIR := node_modules/build
ENC_BUILD_DIR := $(BUILD_DIR)/enc
ENC_MT_BUILD_DIR := $(BUILD_DIR)/enc-mt
DEC_BUILD_DIR := $(BUILD_DIR)/dec
export LIBAOM_DIR = node_modules/libaom
OUT_ENC_JS = enc/avif_enc.js
OUT_ENC_CPP = $(OUT_ENC_JS:.js=.cpp)
OUT_ENC_WASM = $(OUT_ENC_JS:.js=.wasm)
OUT_ENC_MT_JS = enc/avif_enc_mt.js
OUT_DEC_JS = dec/avif_dec.js
OUT_DEC_CPP = $(OUT_DEC_JS:.js=.cpp)
OUT_DEC_WASM = $(OUT_DEC_JS:.js=.wasm)
# ERROR_ON_UNDEFINED_SYMBOLS=0 is needed to seperate the encoder and decoder
EMSCRIPTEN_FLAGS = ${CXXFLAGS} \
${LDFLAGS} \
--bind \
--closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s TEXTDECODER=2 \
-s ENVIRONMENT='worker' \
-s EXPORT_ES6=1 \
-s ERROR_ON_UNDEFINED_SYMBOLS=0
OUT_ENC_CPP = enc/avif_enc.cpp
OUT_DEC_CPP = dec/avif_dec.cpp
CODEC_EMCMAKE = emcmake cmake \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=0 \
-DAVIF_CODEC_AOM=1 \
-DAVIF_LOCAL_AOM=1 \
../
LIBAOM_FLAGS = -DCMAKE_BUILD_TYPE=Release \
-DENABLE_CCACHE=0 \
-DAOM_TARGET_CPU=generic \
-DENABLE_DOCS=0 \
-DENABLE_TESTS=0 \
-DENABLE_EXAMPLES=0 \
-DENABLE_TOOLS=0 \
-DCONFIG_ACCOUNTING=1 \
-DCONFIG_INSPECTION=0 \
-DCONFIG_MULTITHREAD=0 \
-DCONFIG_RUNTIME_CPU_DETECT=0 \
-DCONFIG_WEBM_IO=0
HELPER_MAKEFLAGS := -f helper.Makefile
.PHONY: all clean
all: $(OUT_ENC_JS) $(OUT_DEC_JS)
all: $(OUT_ENC_JS) $(OUT_DEC_JS) $(OUT_ENC_MT_JS)
$(OUT_ENC_JS): $(OUT_ENC_CPP) $(LIBAOM_ENC_OUT) $(CODEC_ENC_OUT)
$(CXX) \
-I $(CODEC_ENC_DIR)/include \
${EMSCRIPTEN_FLAGS} \
-o $@ \
$+
$(OUT_ENC_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
$(MAKE) \
$(HELPER_MAKEFLAGS) \
BUILD_DIR=$(ENC_BUILD_DIR) \
OUT_JS=$@ \
OUT_CPP=$< \
LIBAOM_FLAGS="\
-DCONFIG_AV1_DECODER=0 \
-DCONFIG_MULTITHREAD=0 \
-DCONFIG_AV1_HIGHBITDEPTH=0 \
" \
LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_DECODE=0"
$(OUT_DEC_JS): $(OUT_DEC_CPP) $(LIBAOM_DEC_OUT) $(CODEC_DEC_OUT)
$(CXX) \
-I $(CODEC_DEC_DIR)/include \
${EMSCRIPTEN_FLAGS} \
-o $@ \
$+
$(OUT_ENC_MT_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
$(MAKE) \
$(HELPER_MAKEFLAGS) \
BUILD_DIR=$(ENC_MT_BUILD_DIR) \
OUT_JS=$@ \
OUT_CPP=$< \
LIBAOM_FLAGS="\
-DCONFIG_AV1_DECODER=0 \
-DCONFIG_AV1_HIGHBITDEPTH=0 \
" \
LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_DECODE=0" \
OUT_FLAGS="-pthread"
$(CODEC_ENC_OUT): $(CODEC_ENC_DIR)/CMakeLists.txt $(LIBAOM_ENC_OUT)
mkdir -p $(CODEC_ENC_BUILD_DIR)
cd $(CODEC_ENC_BUILD_DIR) && \
$(CODEC_EMCMAKE) && \
$(MAKE)
$(CODEC_DEC_OUT): $(CODEC_DEC_DIR)/CMakeLists.txt $(LIBAOM_DEC_OUT)
mkdir -p $(CODEC_DEC_BUILD_DIR)
cd $(CODEC_DEC_BUILD_DIR) && \
$(CODEC_EMCMAKE) && \
$(MAKE)
$(LIBAOM_ENC_OUT): $(LIBAOM_ENC_DIR)/CMakeLists.txt
mkdir -p $(LIBAOM_ENC_BUILD_DIR)
cd $(LIBAOM_ENC_BUILD_DIR) && \
emcmake cmake \
$(LIBAOM_FLAGS) \
-DCONFIG_AV1_DECODER=0 \
-DCONFIG_AV1_HIGHBITDEPTH=0 \
../ && \
$(MAKE)
$(LIBAOM_DEC_OUT): $(LIBAOM_DEC_DIR)/CMakeLists.txt
mkdir -p $(LIBAOM_DEC_BUILD_DIR)
cd $(LIBAOM_DEC_BUILD_DIR) && \
emcmake cmake \
$(LIBAOM_FLAGS) \
-DCONFIG_AV1_ENCODER=0 \
../ && \
$(MAKE)
$(CODEC_ENC_DIR)/CMakeLists.txt: $(CODEC_ENC_DIR)
$(CODEC_DEC_DIR)/CMakeLists.txt: $(CODEC_DEC_DIR)
$(LIBAOM_ENC_DIR)/CMakeLists.txt: $(LIBAOM_ENC_DIR)
$(LIBAOM_DEC_DIR)/CMakeLists.txt: $(LIBAOM_DEC_DIR)
$(OUT_DEC_JS): $(OUT_DEC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
$(MAKE) \
$(HELPER_MAKEFLAGS) \
BUILD_DIR=$(DEC_BUILD_DIR) \
OUT_JS=$@ \
OUT_CPP=$< \
LIBAOM_FLAGS="\
-DCONFIG_AV1_ENCODER=0 \
-DCONFIG_MULTITHREAD=0 \
" \
LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_ENCODE=0"
$(CODEC_PACKAGE):
mkdir -p $(dir $@)
mkdir -p $(@D)
curl -sL $(CODEC_URL) -o $@
$(LIBAOM_PACKAGE):
mkdir -p $(dir $@)
mkdir -p $(@D)
curl -sL $(LIBAOM_URL) -o $@
$(CODEC_ENC_DIR) $(CODEC_DEC_DIR): $(CODEC_PACKAGE)
mkdir -p $@
tar xz --strip 1 -C $@ -f $(CODEC_PACKAGE)
$(CODEC_DIR)/CMakeLists.txt: $(CODEC_PACKAGE)
mkdir -p $(@D)
tar xzm --strip 1 -C $(@D) -f $(CODEC_PACKAGE)
$(LIBAOM_ENC_DIR) $(LIBAOM_DEC_DIR): $(LIBAOM_PACKAGE)
mkdir -p $@
tar xz -C $@ -f $(LIBAOM_PACKAGE)
$(LIBAOM_DIR)/CMakeLists.txt: $(LIBAOM_PACKAGE)
mkdir -p $(@D)
tar xzm -C $(@D) -f $(LIBAOM_PACKAGE)
clean:
$(RM) $(LIBAOM_PACKAGE) $(CODEC_PACKAGE) $(OUT_ENC_JS) $(OUT_ENC_WASM) $(OUT_DEC_JS) $(OUT_DEC_WASM)
$(MAKE) -C $(CODEC_ENC_BUILD_DIR) clean
$(MAKE) -C $(CODEC_DEC_BUILD_DIR) clean
$(MAKE) -C $(LIBAOM_ENC_BUILD_DIR) clean
$(MAKE) -C $(LIBAOM_DEC_BUILD_DIR) clean
$(MAKE) $(HELPER_MAKEFLAGS) BUILD_DIR=$(ENC_BUILD_DIR) OUT_JS=$(OUT_ENC_JS) clean
$(MAKE) $(HELPER_MAKEFLAGS) BUILD_DIR=$(ENC_MT_BUILD_DIR) OUT_JS=$(OUT_ENC_MT_JS) clean
$(MAKE) $(HELPER_MAKEFLAGS) BUILD_DIR=$(DEC_BUILD_DIR) OUT_JS=$(OUT_DEC_JS) clean

View File

@@ -8,15 +8,10 @@ thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");
thread_local const val ImageData = val::global("ImageData");
val decode(std::string avifimage) {
// point raw.data and raw.size to the contents of an .avif(s)
avifROData raw = {
.data = (uint8_t*)avifimage.c_str(),
.size = avifimage.length()
};
avifImage* image = avifImageCreateEmpty();
avifDecoder* decoder = avifDecoderCreate();
avifResult decodeResult = avifDecoderRead(decoder, image, &raw);
avifResult decodeResult =
avifDecoderReadMemory(decoder, image, (uint8_t*)avifimage.c_str(), avifimage.length());
// image is an independent copy of decoded data, decoder may be destroyed here
avifDecoderDestroy(decoder);
@@ -25,7 +20,8 @@ val decode(std::string avifimage) {
if (decodeResult == AVIF_RESULT_OK) {
// Convert to interleaved RGB(A)/BGR(A) using a libavif-allocated buffer.
avifRGBImage rgb;
avifRGBImageSetDefaults(&rgb, image); // Defaults to AVIF_RGB_FORMAT_RGBA which is what we want.
avifRGBImageSetDefaults(&rgb,
image); // Defaults to AVIF_RGB_FORMAT_RGBA which is what we want.
rgb.depth = 8; // Does not need to match image->depth. We always want 8-bit pixels.
avifRGBImageAllocatePixels(&rgb);
@@ -33,7 +29,9 @@ val decode(std::string avifimage) {
// We want to create a *copy* of the decoded data to be owned by the JavaScript side.
// For that, we perform `new Uint8Array(wasmMemBuffer, wasmPtr, wasmSize).slice()`:
result = ImageData.new_(Uint8ClampedArray.new_(typed_memory_view(rgb.rowBytes * rgb.height, rgb.pixels)), rgb.width, rgb.height);
result = ImageData.new_(
Uint8ClampedArray.new_(typed_memory_view(rgb.rowBytes * rgb.height, rgb.pixels)), rgb.width,
rgb.height);
// Now we can safely free the RGB pixels:
avifRGBImageFreePixels(&rgb);

View File

@@ -1,4 +1,5 @@
#include <emscripten/bind.h>
#include <emscripten/threading.h>
#include <emscripten/val.h>
#include "avif/avif.h"
@@ -50,13 +51,10 @@ val encode(std::string buffer, int width, int height, AvifOptions options) {
avifImage* image = avifImageCreate(width, height, depth, format);
if (
options.maxQuantizer == AVIF_QUANTIZER_LOSSLESS &&
options.minQuantizer == AVIF_QUANTIZER_LOSSLESS &&
options.minQuantizerAlpha == AVIF_QUANTIZER_LOSSLESS &&
options.maxQuantizerAlpha == AVIF_QUANTIZER_LOSSLESS &&
format == AVIF_PIXEL_FORMAT_YUV444
) {
if (options.maxQuantizer == AVIF_QUANTIZER_LOSSLESS &&
options.minQuantizer == AVIF_QUANTIZER_LOSSLESS &&
options.minQuantizerAlpha == AVIF_QUANTIZER_LOSSLESS &&
options.maxQuantizerAlpha == AVIF_QUANTIZER_LOSSLESS && format == AVIF_PIXEL_FORMAT_YUV444) {
image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY;
} else {
image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT709;
@@ -71,7 +69,7 @@ val encode(std::string buffer, int width, int height, AvifOptions options) {
avifImageRGBToYUV(image, &srcRGB);
avifEncoder* encoder = avifEncoderCreate();
encoder->maxThreads = 1;
encoder->maxThreads = emscripten_num_logical_cores();
encoder->minQuantizer = options.minQuantizer;
encoder->maxQuantizer = options.maxQuantizer;
encoder->minQuantizerAlpha = options.minQuantizerAlpha;

2
codecs/avif/enc/avif_enc_mt.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export * from './avif_enc';
export { default } from './avif_enc';

File diff suppressed because it is too large Load Diff

BIN
codecs/avif/enc/avif_enc_mt.wasm Executable file

Binary file not shown.

View File

@@ -0,0 +1,103 @@
var threadInfoStruct = 0;
var selfThreadId = 0;
var parentThreadId = 0;
var initializedJS = false;
var Module = {};
function threadPrintErr() {
var text = Array.prototype.slice.call(arguments).join(' ');
console.error(text);
}
function threadAlert() {
var text = Array.prototype.slice.call(arguments).join(' ');
postMessage({ cmd: 'alert', text: text, threadId: selfThreadId });
}
var err = threadPrintErr;
this.alert = threadAlert;
Module['instantiateWasm'] = function (info, receiveInstance) {
var instance = new WebAssembly.Instance(Module['wasmModule'], info);
Module['wasmModule'] = null;
receiveInstance(instance);
return instance.exports;
};
this.onmessage = function (e) {
try {
if (e.data.cmd === 'load') {
Module['wasmModule'] = e.data.wasmModule;
Module['wasmMemory'] = e.data.wasmMemory;
Module['buffer'] = Module['wasmMemory'].buffer;
Module['ENVIRONMENT_IS_PTHREAD'] = true;
import(e.data.urlOrBlob)
.then(function (avif_enc_mt) {
return avif_enc_mt.default(Module);
})
.then(function (instance) {
Module = instance;
postMessage({ cmd: 'loaded' });
});
} else if (e.data.cmd === 'objectTransfer') {
Module['PThread'].receiveObjectTransfer(e.data);
} else if (e.data.cmd === 'run') {
Module['__performance_now_clock_drift'] = performance.now() - e.data.time;
threadInfoStruct = e.data.threadInfoStruct;
Module['registerPthreadPtr'](
threadInfoStruct,
/*isMainBrowserThread=*/ 0,
/*isMainRuntimeThread=*/ 0,
);
selfThreadId = e.data.selfThreadId;
parentThreadId = e.data.parentThreadId;
var max = e.data.stackBase;
var top = e.data.stackBase + e.data.stackSize;
Module['establishStackSpace'](top, max);
Module['_emscripten_tls_init']();
Module['PThread'].receiveObjectTransfer(e.data);
Module['PThread'].setThreadStatus(Module['_pthread_self'](), 1);
if (!initializedJS) {
Module['___embind_register_native_and_builtin_types']();
initializedJS = true;
}
try {
var result = Module['dynCall']('ii', e.data.start_routine, [
e.data.arg,
]);
if (!Module['getNoExitRuntime']()) Module['PThread'].threadExit(result);
} catch (ex) {
if (ex === 'Canceled!') {
Module['PThread'].threadCancel();
} else if (ex != 'unwind') {
Atomics.store(
Module['HEAPU32'],
(threadInfoStruct + 4) >> /*C_STRUCTS.pthread.threadExitCode*/ 2,
ex instanceof Module['ExitStatus'] ? ex.status : -2,
);
/*A custom entry specific to Emscripten denoting that the thread crashed.*/ Atomics.store(
Module['HEAPU32'],
(threadInfoStruct + 0) >> /*C_STRUCTS.pthread.threadStatus*/ 2,
1,
);
Module['_emscripten_futex_wake'](
threadInfoStruct + 0,
/*C_STRUCTS.pthread.threadStatus*/ 2147483647,
);
if (!(ex instanceof Module['ExitStatus'])) throw ex;
}
}
} else if (e.data.cmd === 'cancel') {
if (threadInfoStruct) {
Module['PThread'].threadCancel();
}
} else if (e.data.target === 'setimmediate') {
} else if (e.data.cmd === 'processThreadQueue') {
if (threadInfoStruct) {
Module['_emscripten_current_thread_process_queued_calls']();
}
} else {
err('worker.js received unknown command ' + e.data.cmd);
err(e.data);
}
} catch (ex) {
err('worker.js onmessage() captured an uncaught exception: ' + ex);
if (ex && ex.stack) err(ex.stack);
throw ex;
}
};

View File

@@ -0,0 +1,75 @@
# This is a helper Makefile for building LibAVIF + LibAOM with given params.
#
# Params that must be supplied by the caller:
# $(CODEC_DIR)
# $(LIBAOM_DIR)
# $(BUILD_DIR)
# $(OUT_JS)
# $(OUT_CPP)
# $(LIBAOM_FLAGS)
# $(LIBAVIF_FLAGS)
CODEC_BUILD_DIR := $(BUILD_DIR)/libavif
CODEC_OUT := $(CODEC_BUILD_DIR)/libavif.a
LIBAOM_BUILD_DIR := $(BUILD_DIR)/libaom
LIBAOM_OUT := $(LIBAOM_BUILD_DIR)/libaom.a
OUT_WASM = $(OUT_JS:.js=.wasm)
OUT_WORKER=$(OUT_JS:.js=.worker.js)
.PHONY: all clean
all: $(OUT_JS)
$(OUT_JS): $(OUT_CPP) $(LIBAOM_OUT) $(CODEC_OUT)
$(CXX) \
-I $(CODEC_DIR)/include \
$(CXXFLAGS) \
$(LDFLAGS) \
$(OUT_FLAGS) \
--bind \
--closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s TEXTDECODER=2 \
-s ENVIRONMENT='worker' \
-s EXPORT_ES6=1 \
-s EXPORT_NAME="$(basename $(@F))" \
-o $@ \
$+
$(CODEC_OUT): $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_OUT)
emcmake cmake \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=0 \
-DAVIF_CODEC_AOM=1 \
-DAOM_LIBRARY=$(LIBAOM_OUT) \
-DAOM_INCLUDE_DIR=$(LIBAOM_DIR) \
$(LIBAVIF_FLAGS) \
-B $(CODEC_BUILD_DIR) \
$(CODEC_DIR) && \
$(MAKE) -C $(CODEC_BUILD_DIR)
$(LIBAOM_OUT): $(LIBAOM_DIR)/CMakeLists.txt
emcmake cmake \
-DCMAKE_BUILD_TYPE=Release \
-DENABLE_CCACHE=0 \
-DAOM_TARGET_CPU=generic \
-DENABLE_DOCS=0 \
-DENABLE_TESTS=0 \
-DENABLE_EXAMPLES=0 \
-DENABLE_TOOLS=0 \
-DCONFIG_ACCOUNTING=1 \
-DCONFIG_INSPECTION=0 \
-DCONFIG_RUNTIME_CPU_DETECT=0 \
-DCONFIG_WEBM_IO=0 \
$(LIBAOM_FLAGS) \
-B $(LIBAOM_BUILD_DIR) \
$(LIBAOM_DIR) && \
$(MAKE) -C $(LIBAOM_BUILD_DIR)
clean:
$(RM) $(OUT_JS) $(OUT_WASM) $(OUT_WORKER)
$(MAKE) -C $(CODEC_BUILD_DIR) clean
$(MAKE) -C $(LIBAOM_BUILD_DIR) clean

View File

@@ -1,4 +1,10 @@
set -e
docker build -t squoosh-rust - < ../rust.Dockerfile
docker run --rm -v $PWD:/src squoosh-rust "$@"
if [ ! -z "$RUST_IMG" ]
then
# Get part after ":" (https://stackoverflow.com/a/15149278/439965).
IMG_SUFFIX=-${RUST_IMG#*:}
fi
IMG_NAME=squoosh-rust$IMG_SUFFIX
docker build -t $IMG_NAME --build-arg RUST_IMG - < ../rust.Dockerfile
docker run --rm -v $PWD:/src $IMG_NAME "$@"

View File

@@ -1,8 +1,8 @@
FROM emscripten/emsdk:2.0.3
FROM emscripten/emsdk:2.0.8
RUN apt-get update && apt-get install -qqy autoconf libtool pkg-config
ENV CFLAGS "-Os -flto"
ENV CFLAGS "-O3 -flto"
ENV CXXFLAGS "${CFLAGS} -std=c++17"
ENV LDFLAGS "${CFLAGS}"
ENV LDFLAGS "${CFLAGS} -s PTHREAD_POOL_SIZE=navigator.hardwareConcurrency"
# Build and cache standard libraries with these flags
RUN emcc ${CXXFLAGS} --bind -xc++ /dev/null -o /dev/null
WORKDIR /src

View File

@@ -1,11 +0,0 @@
# This is intentionally an old version of Rust. Newer versions
# generate _significantly_ bigger Wasm binaries.
FROM rust:1.37
RUN rustup target add wasm32-unknown-unknown
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
RUN mkdir /opt/binaryen && \
curl -L https://github.com/WebAssembly/binaryen/releases/download/1.38.32/binaryen-1.38.32-x86-linux.tar.gz | tar -xzf - -C /opt/binaryen --strip 1
ENV PATH="/opt/binaryen:${PATH}"
WORKDIR /src

View File

@@ -1,21 +0,0 @@
#!/bin/bash
set -e
echo "============================================="
echo "Compiling wasm"
echo "============================================="
(
wasm-pack build --target web -- --verbose --locked
rm pkg/.gitignore
)
echo "============================================="
echo "Compiling wasm done"
echo "============================================="
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo "Did you update your docker image?"
echo "Run \`docker pull ubuntu\`"
echo "Run \`docker pull rust\`"
echo "Run \`docker build -t squoosh-hqx .\`"
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"

View File

@@ -1,7 +1,6 @@
{
"name": "hqx",
"scripts": {
"build:image": "docker build -t squoosh-hqx - < Dockerfile",
"build": "docker run --rm -v $(pwd):/src squoosh-hqx ./build.sh"
"build": "RUST_IMG=rust:1.40 ../build-rust.sh"
}
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

BIN
codecs/imagequant/imagequant.wasm Normal file → Executable file

Binary file not shown.

59
codecs/jxl/Makefile Normal file
View File

@@ -0,0 +1,59 @@
CODEC_URL = https://gitlab.com/wg1/jpeg-xl.git
CODEC_VERSION = v0.1
CODEC_DIR = node_modules/jxl
CODEC_BUILD_DIR := $(CODEC_DIR)/build
CODEC_OUT := $(CODEC_BUILD_DIR)/lib/libjxl.a
OUT_JS = enc/jxl_enc.js dec/jxl_dec.js
OUT_WASM = $(OUT_JS:.js=.wasm)
.PHONY: all clean
all: $(OUT_JS)
%.js: %.cpp $(LIBAOM_OUT) $(CODEC_OUT)
$(CXX) \
-I $(CODEC_DIR) \
-I $(CODEC_DIR)/lib \
-I $(CODEC_DIR)/lib/include \
-I $(CODEC_BUILD_DIR)/lib/include \
-I $(CODEC_DIR)/third_party/highway \
-I $(CODEC_DIR)/third_party/skcms \
-I $(CODEC_DIR)/third_party/brunsli \
-I $(CODEC_DIR)/third_party/brunsli/c/include \
${CXXFLAGS} \
${LDFLAGS} \
--bind \
--closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s TEXTDECODER=2 \
-s ENVIRONMENT='worker' \
-s EXPORT_ES6=1 \
-s EXPORT_NAME="$(basename $(@F))" \
-o $@ \
$+ \
$(CODEC_BUILD_DIR)/artifacts/libbrunslienc-static.bc \
$(CODEC_BUILD_DIR)/artifacts/libbrunslicommon-static.bc \
$(CODEC_BUILD_DIR)/artifacts/libbrunslidec-static.bc \
$(CODEC_BUILD_DIR)/third_party/brotli/libbrotlidec-static.a \
$(CODEC_BUILD_DIR)/third_party/brotli/libbrotlienc-static.a \
$(CODEC_BUILD_DIR)/third_party/brotli/libbrotlicommon-static.a \
$(CODEC_BUILD_DIR)/third_party/libskcms.a \
$(CODEC_BUILD_DIR)/third_party/highway/libhwy.a
$(CODEC_OUT): $(CODEC_DIR)/CMakeLists.txt
mkdir -p $(CODEC_BUILD_DIR)
cd $(CODEC_BUILD_DIR) && \
emcmake cmake ../ && \
$(MAKE) jxl-static
$(CODEC_DIR)/CMakeLists.txt: $(CODEC_DIR)
$(CODEC_DIR):
mkdir -p $@
git clone $(CODEC_URL) --recursive -j`nproc` --depth 1 --branch $(CODEC_VERSION) $@
clean:
$(RM) $(OUT_JS) $(OUT_WASM)
$(MAKE) -C $(CODEC_BUILD_DIR) clean

View File

@@ -0,0 +1,72 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <jxl/decode.h>
#include "lib/jxl/color_encoding_internal.h"
#include "skcms.h"
using namespace emscripten;
thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");
thread_local const val ImageData = val::global("ImageData");
// R, G, B, A
#define COMPONENTS_PER_PIXEL 4
#define EXPECT_TRUE(a) \
if (!(a)) { \
return val::null(); \
}
#define EXPECT_EQ(a, b) EXPECT_TRUE((a) == (b));
val decode(std::string data) {
std::unique_ptr<JxlDecoder,
std::integral_constant<decltype(&JxlDecoderDestroy), JxlDecoderDestroy>>
dec(JxlDecoderCreate(nullptr));
EXPECT_EQ(JXL_DEC_SUCCESS,
JxlDecoderSubscribeEvents(
dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE));
auto next_in = (const uint8_t*)data.c_str();
auto avail_in = data.size();
EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec.get(), &next_in, &avail_in));
JxlBasicInfo info;
EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec.get(), &info));
size_t pixel_count = info.xsize * info.ysize;
size_t component_count = pixel_count * COMPONENTS_PER_PIXEL;
EXPECT_EQ(JXL_DEC_COLOR_ENCODING, JxlDecoderProcessInput(dec.get(), &next_in, &avail_in));
static const JxlPixelFormat format = {COMPONENTS_PER_PIXEL, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0};
size_t icc_size;
EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetICCProfileSize(dec.get(), &format,
JXL_COLOR_PROFILE_TARGET_DATA, &icc_size));
std::vector<uint8_t> icc_profile(icc_size);
EXPECT_EQ(JXL_DEC_SUCCESS,
JxlDecoderGetColorAsICCProfile(dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA,
icc_profile.data(), icc_profile.size()));
auto float_pixels = std::make_unique<float[]>(component_count);
EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer(dec.get(), &format, float_pixels.get(),
component_count * sizeof(float)));
EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec.get(), &next_in, &avail_in));
auto byte_pixels = std::make_unique<uint8_t[]>(component_count);
// Convert to sRGB.
skcms_ICCProfile jxl_profile;
EXPECT_TRUE(skcms_Parse(icc_profile.data(), icc_profile.size(), &jxl_profile));
EXPECT_TRUE(skcms_Transform(
float_pixels.get(), skcms_PixelFormat_RGBA_ffff,
info.alpha_premultiplied ? skcms_AlphaFormat_PremulAsEncoded : skcms_AlphaFormat_Unpremul,
&jxl_profile, byte_pixels.get(), skcms_PixelFormat_RGBA_8888, skcms_AlphaFormat_Unpremul,
skcms_sRGB_profile(), pixel_count));
return ImageData.new_(
Uint8ClampedArray.new_(typed_memory_view(component_count, byte_pixels.get())), info.xsize,
info.ysize);
}
EMSCRIPTEN_BINDINGS(my_module) {
function("decode", &decode);
}

7
codecs/jxl/dec/jxl_dec.d.ts vendored Normal file
View File

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

1195
codecs/jxl/dec/jxl_dec.js Normal file

File diff suppressed because it is too large Load Diff

BIN
codecs/jxl/dec/jxl_dec.wasm Executable file

Binary file not shown.

115
codecs/jxl/enc/jxl_enc.cpp Normal file
View File

@@ -0,0 +1,115 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include "lib/jxl/enc_file.h"
#include "lib/jxl/external_image.h"
using namespace emscripten;
thread_local const val Uint8Array = val::global("Uint8Array");
struct JXLOptions {
// 1 = slowest
// 7 = fastest
int speed;
float quality;
bool progressive;
int epf;
int nearLossless;
bool lossyPalette;
};
val encode(std::string image, int width, int height, JXLOptions options) {
jxl::CompressParams cparams;
jxl::PassesEncoderState passes_enc_state;
jxl::CodecInOut io;
jxl::PaddedBytes bytes;
jxl::ImageBundle* main = &io.Main();
cparams.epf = options.epf;
cparams.speed_tier = static_cast<jxl::SpeedTier>(options.speed);
cparams.near_lossless = options.nearLossless;
if (options.lossyPalette) {
cparams.lossy_palette = true;
cparams.palette_colors = 0;
cparams.options.predictor = jxl::Predictor::Zero;
}
// Reduce memory usage of tree learning for lossless data.
// TODO(veluca93): this is a mitigation for excessive memory usage in the JXL encoder.
cparams.options.nb_repeats = 0.1;
float quality = options.quality;
// Quality settings roughly match libjpeg qualities.
if (quality < 7 || quality == 100) {
cparams.modular_mode = true;
// Internal modular quality to roughly match VarDCT size.
cparams.quality_pair.first = cparams.quality_pair.second =
std::min(35 + (quality - 7) * 3.0f, 100.0f);
} else {
cparams.modular_mode = false;
if (quality >= 30) {
cparams.butteraugli_distance = 0.1 + (100 - quality) * 0.09;
} else {
cparams.butteraugli_distance = 6.4 + pow(2.5, (30 - quality) / 5.0f) / 6.25f;
}
}
if (options.progressive) {
cparams.qprogressive_mode = true;
cparams.progressive_dc = 1;
cparams.responsive = 1;
}
if (cparams.modular_mode) {
if (cparams.quality_pair.first != 100 || cparams.quality_pair.second != 100) {
cparams.color_transform = jxl::ColorTransform::kXYB;
} else {
cparams.color_transform = jxl::ColorTransform::kNone;
}
}
if (cparams.near_lossless) {
// Near-lossless assumes -R 0
cparams.responsive = 0;
cparams.modular_mode = true;
}
io.metadata.m.SetAlphaBits(8);
if (!io.metadata.size.Set(width, height)) {
return val::null();
}
uint8_t* inBuffer = (uint8_t*)image.c_str();
auto result = jxl::ConvertImage(
jxl::Span<const uint8_t>(reinterpret_cast<const uint8_t*>(image.data()), image.size()), width,
height, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*has_alpha=*/true,
/*alpha_is_premultiplied=*/false, /*bits_per_alpha=*/8, /*bits_per_sample=*/8,
/*big_endian=*/false, /*flipped_y=*/false, /*pool=*/nullptr, main);
if (!result) {
return val::null();
}
auto js_result = val::null();
if (EncodeFile(cparams, &io, &passes_enc_state, &bytes)) {
js_result = Uint8Array.new_(typed_memory_view(bytes.size(), bytes.data()));
}
return js_result;
}
EMSCRIPTEN_BINDINGS(my_module) {
value_object<JXLOptions>("JXLOptions")
.field("speed", &JXLOptions::speed)
.field("quality", &JXLOptions::quality)
.field("progressive", &JXLOptions::progressive)
.field("nearLossless", &JXLOptions::nearLossless)
.field("lossyPalette", &JXLOptions::lossyPalette)
.field("epf", &JXLOptions::epf);
function("encode", &encode);
}

21
codecs/jxl/enc/jxl_enc.d.ts vendored Normal file
View File

@@ -0,0 +1,21 @@
export interface EncodeOptions {
speed: number;
quality: number;
progressive: boolean;
epf: number;
nearLossless: number;
lossyPalette: boolean;
}
export interface JXLModule extends EmscriptenWasm.Module {
encode(
data: BufferSource,
width: number,
height: number,
options: EncodeOptions,
): Uint8Array | null;
}
declare var moduleFactory: EmscriptenWasm.ModuleFactory<JXLModule>;
export default moduleFactory;

1606
codecs/jxl/enc/jxl_enc.js Normal file

File diff suppressed because it is too large Load Diff

BIN
codecs/jxl/enc/jxl_enc.wasm Executable file

Binary file not shown.

6
codecs/jxl/package.json Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "jxl",
"scripts": {
"build": "../build-cpp.sh"
}
}

File diff suppressed because it is too large Load Diff

BIN
codecs/mozjpeg_enc/mozjpeg_enc.wasm Normal file → Executable file

Binary file not shown.

299
codecs/oxipng/Cargo.lock generated
View File

@@ -1,16 +1,22 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "adler32"
version = "1.1.0"
name = "adler"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d"
checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "autocfg"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bit-vec"
@@ -38,9 +44,9 @@ checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
[[package]]
name = "bytemuck"
version = "1.2.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37fa13df2292ecb479ec23aa06f4507928bef07839be9ef15281411076629431"
checksum = "41aa2ec95ca3b5c54cf73c91acf06d24f4495d5f1b1c12506ae3483d646177ac"
[[package]]
name = "byteorder"
@@ -50,9 +56,9 @@ checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
[[package]]
name = "cc"
version = "1.0.58"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518"
checksum = "f1770ced377336a88a67c473594ccc14eca6f4559217c34f64aac8f83d641b40"
[[package]]
name = "cfg-if"
@@ -60,6 +66,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cloudflare-zlib"
version = "0.2.5"
@@ -78,6 +90,18 @@ dependencies = [
"cc",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "const_fn"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c478836e029dcef17fb47c89023448c64f781a046e0300e257ad8225ae59afab"
[[package]]
name = "crc"
version = "1.8.1"
@@ -89,58 +113,57 @@ dependencies = [
[[package]]
name = "crc32fast"
version = "1.2.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.7.3"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285"
checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-epoch",
"crossbeam-utils",
"maybe-uninit",
]
[[package]]
name = "crossbeam-epoch"
version = "0.8.2"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
checksum = "ec0f606a85340376eef0d6d8fec399e6d4a544d648386c6645eb6d0653b27d9f"
dependencies = [
"autocfg",
"cfg-if",
"cfg-if 1.0.0",
"const_fn",
"crossbeam-utils",
"lazy_static",
"maybe-uninit",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-queue"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570"
dependencies = [
"cfg-if",
"crossbeam-utils",
"maybe-uninit",
]
[[package]]
name = "crossbeam-utils"
version = "0.7.2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
checksum = "ec91540d98355f690a86367e566ecad2e9e579f230230eb7c21398372be73ea5"
dependencies = [
"autocfg",
"cfg-if",
"cfg-if 1.0.0",
"const_fn",
"lazy_static",
]
@@ -156,27 +179,34 @@ dependencies = [
[[package]]
name = "either"
version = "1.5.3"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "hashbrown"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
[[package]]
name = "hermit-abi"
version = "0.1.15"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9"
checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
dependencies = [
"libc",
]
[[package]]
name = "image"
version = "0.23.7"
version = "0.23.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2397fc43bd5648b7117aabb3c5e62d0e62c194826ec77b0b4d0c41e62744635"
checksum = "b4f0a8345b33b082aedec2f4d7d4a926b845cee184cbe78b703413066564431b"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"num-iter",
"num-rational",
"num-traits",
@@ -185,11 +215,12 @@ dependencies = [
[[package]]
name = "indexmap"
version = "1.4.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe"
checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2"
dependencies = [
"autocfg",
"hashbrown",
"rayon",
]
@@ -210,39 +241,42 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.72"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9f8082297d534141b30c8d39e9b1773713ab50fdbe4ff30f750d063b3bfd701"
checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
[[package]]
name = "libdeflater"
version = "0.2.0"
name = "libdeflate-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66dca08b13369865b2f6dca1dd05f833985cbe6c12a676b04d55f78b85e80246"
checksum = "21e39efa87b84db3e13ff4e2dfac1e57220abcbd7fe8ec44d238f7f4f787cc1f"
dependencies = [
"cc",
]
[[package]]
name = "log"
version = "0.4.8"
name = "libdeflater"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
checksum = "a4810980d791f26d470e2d7d91a3d4d22aa3a4b709fb7e9c5e43ee54f83a01f2"
dependencies = [
"cfg-if",
"libdeflate-sys",
]
[[package]]
name = "maybe-uninit"
version = "2.0.0"
name = "log"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
dependencies = [
"cfg-if 0.1.10",
]
[[package]]
name = "memoffset"
version = "0.5.5"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c198b026e1bbf08a937e94c6c60f9ec4a2267f5b0d2eec9c1b21b061ce2be55f"
checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa"
dependencies = [
"autocfg",
]
@@ -257,10 +291,20 @@ dependencies = [
]
[[package]]
name = "num-integer"
version = "0.1.43"
name = "miniz_oxide"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b"
checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d"
dependencies = [
"adler",
"autocfg",
]
[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg",
"num-traits",
@@ -268,9 +312,9 @@ dependencies = [
[[package]]
name = "num-iter"
version = "0.1.41"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6e6b7c748f995c4c29c5f5ae0248536e04a5739927c74ec0fa564805094b9f"
checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59"
dependencies = [
"autocfg",
"num-integer",
@@ -279,9 +323,9 @@ dependencies = [
[[package]]
name = "num-rational"
version = "0.3.0"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5b4d7360f362cfb50dde8143501e6940b22f644be75a4cc90b2d81968908138"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
"autocfg",
"num-integer",
@@ -290,9 +334,9 @@ dependencies = [
[[package]]
name = "num-traits"
version = "0.2.12"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg",
]
@@ -308,42 +352,59 @@ dependencies = [
]
[[package]]
name = "oxipng"
version = "3.0.0"
name = "once_cell"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fd695858078338d73862ff3755f820eff0bf4f3304e4b52f22aba53463183a"
checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
[[package]]
name = "oxipng"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea40b366cecfce76ee3b082e7e6567b82cdef75644a22442ca8584bc666ff4eb"
dependencies = [
"bit-vec",
"byteorder",
"cloudflare-zlib",
"crc",
"crossbeam-channel",
"image",
"indexmap",
"itertools",
"libdeflater",
"log",
"miniz_oxide",
"miniz_oxide 0.4.3",
"rayon",
"rgb",
"zopfli",
"rustc_version",
]
[[package]]
name = "pest"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
dependencies = [
"ucd-trie",
]
[[package]]
name = "png"
version = "0.16.6"
version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c150bf7479fafe3dd8740dbe48cc33b2a3efb7b0fe3483aced8bbc39f6d0238d"
checksum = "dfe7f9f1c730833200b134370e1d5098964231af8450bce9b78ee3ab5278b970"
dependencies = [
"bitflags",
"crc32fast",
"deflate",
"miniz_oxide",
"miniz_oxide 0.3.7",
]
[[package]]
name = "proc-macro2"
version = "1.0.18"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa"
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
dependencies = [
"unicode-xid",
]
@@ -359,9 +420,9 @@ dependencies = [
[[package]]
name = "rayon"
version = "1.3.1"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f02856753d04e03e26929f820d0a0a337ebe71f849801eea335d464b349080"
checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674"
dependencies = [
"autocfg",
"crossbeam-deque",
@@ -371,12 +432,12 @@ dependencies = [
[[package]]
name = "rayon-core"
version = "1.7.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e92e15d89083484e11353891f1af602cc661426deb9564c298b270c726973280"
checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-queue",
"crossbeam-utils",
"lazy_static",
"num_cpus",
@@ -384,33 +445,63 @@ dependencies = [
[[package]]
name = "rgb"
version = "0.8.20"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ef54b45ae131327a88597e2463fee4098ad6c88ba7b6af4b3987db8aad4098"
checksum = "287f3c3f8236abb92d8b7e36797f19159df4b58f0a658cc3fb6dd3004b1f3bd3"
dependencies = [
"bytemuck",
]
[[package]]
name = "rustc_version"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65c94201b44764d6d1f7e37c15a8289ed55e546c1762c7f1d57f616966e0c181"
dependencies = [
"semver",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "semver"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ef146c2ad5e5f4b037cd6ce2ebb775401729b19a82040c1beac9d36c7d1428"
dependencies = [
"pest",
]
[[package]]
name = "squoosh-oxipng"
version = "0.1.0"
dependencies = [
"crossbeam-channel",
"log",
"once_cell",
"oxipng",
"rayon",
"wasm-bindgen",
]
[[package]]
name = "syn"
version = "1.0.34"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936cae2873c940d92e697597c5eee105fb570cd5689c695806f672883653349b"
checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac"
dependencies = [
"proc-macro2",
"quote",
@@ -418,10 +509,10 @@ dependencies = [
]
[[package]]
name = "typed-arena"
version = "1.7.0"
name = "ucd-trie"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "unicode-xid"
@@ -431,19 +522,19 @@ checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "wasm-bindgen"
version = "0.2.64"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a634620115e4a229108b71bde263bb4220c483b3f07f5ba514ee8d15064c4c2"
checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42"
dependencies = [
"cfg-if",
"cfg-if 0.1.10",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.64"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e53963b583d18a5aa3aaae4b4c1cb535218246131ba22a71f05b518098571df"
checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68"
dependencies = [
"bumpalo",
"lazy_static",
@@ -456,9 +547,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.64"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fcfd5ef6eec85623b4c6e844293d4516470d8f19cd72d0d12246017eb9060b8"
checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -466,9 +557,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.64"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9adff9ee0e94b926ca81b57f57f86d5545cdcb1d259e21ec9bdd95b901754c75"
checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe"
dependencies = [
"proc-macro2",
"quote",
@@ -479,18 +570,6 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.64"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7b90ea6c632dd06fd765d44542e234d5e63d9bb917ecd64d79778a13bd79ae"
[[package]]
name = "zopfli"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4079b79464426ade2a1b0177fb0ce8396ba6b4084267407e333573c666073964"
dependencies = [
"adler32",
"byteorder",
"crc",
"typed-arena",
]
checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307"

View File

@@ -5,14 +5,23 @@ authors = ["Ingvar Stepanyan <me@rreverser.com>"]
edition = "2018"
publish = false
[package.metadata.wasm-pack.profile.release]
wasm-opt = ["-O", "--no-validation"]
[lib]
crate-type = ["cdylib"]
[dependencies]
oxipng = { version = "3.0.0", default-features = false }
wasm-bindgen = "0.2.64"
log = { version = "0.4", features = ["release_max_level_off"] }
oxipng = { version = "4.0.0", default-features = false, features = ["libdeflater"] }
wasm-bindgen = "0.2.68"
log = { version = "0.4.11", features = ["release_max_level_off"] }
rayon = { version = "1.5.0", optional = true }
once_cell = { version = "1.5.2", optional = true }
crossbeam-channel = { version = "0.5.0", optional = true }
[profile.release]
lto = true
opt-level = "s"
[features]
parallel = ["oxipng/parallel", "rayon", "crossbeam-channel", "once_cell"]

View File

@@ -1,12 +0,0 @@
FROM rust
RUN rustup target add wasm32-unknown-unknown
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
RUN mkdir /opt/wabt && \
curl -L https://github.com/WebAssembly/wabt/releases/download/1.0.17/wabt-1.0.17-ubuntu.tar.gz | tar -xzf - -C /opt/wabt --strip 1
RUN mkdir /opt/wasi-sdk && \
curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-11/wasi-sdk-11.0-linux.tar.gz | tar -xzf - -C /opt/wasi-sdk --strip 1
ENV PATH="/opt/wabt/bin:/opt/wasi-sdk/bin:${PATH}"
WORKDIR /src

8
codecs/oxipng/build.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
set -e
rm -rf pkg,{-parallel}
wasm-pack build --target web
RUSTFLAGS='-C target-feature=+atomics,+bulk-memory' wasm-pack build -t web -d pkg-parallel -- -Z build-std=panic_abort,std --features=parallel
rm pkg{,-parallel}/.gitignore

View File

@@ -1,6 +1,6 @@
{
"name": "oxipng",
"scripts": {
"build": "../build-rust.sh"
"build": "RUST_IMG=rustlang/rust:8bb115b1090d ../build-rust.sh ./build.sh"
}
}

View File

@@ -0,0 +1,5 @@
# OxiPNG
- Source: <https://github.com/shssoichiro/oxipng>
- Version: v3.0.0
- License: MIT

View File

@@ -0,0 +1,15 @@
{
"name": "squoosh-oxipng",
"collaborators": [
"Ingvar Stepanyan <me@rreverser.com>"
],
"version": "0.1.0",
"files": [
"squoosh_oxipng_bg.wasm",
"squoosh_oxipng.js",
"squoosh_oxipng.d.ts"
],
"module": "squoosh_oxipng.js",
"types": "squoosh_oxipng.d.ts",
"sideEffects": false
}

View File

@@ -0,0 +1,53 @@
/* tslint:disable */
/* eslint-disable */
/**
* @param {Uint8Array} data
* @param {number} level
* @returns {Uint8Array}
*/
export function optimise(data: Uint8Array, level: number): Uint8Array;
/**
* @param {number} num
* @returns {any}
*/
export function worker_initializer(num: number): any;
/**
*/
export function start_main_thread(): void;
/**
*/
export function start_worker_thread(): void;
export type InitInput =
| RequestInfo
| URL
| Response
| BufferSource
| WebAssembly.Module;
export interface InitOutput {
readonly optimise: (a: number, b: number, c: number, d: number) => void;
readonly malloc: (a: number) => number;
readonly free: (a: number) => void;
readonly worker_initializer: (a: number) => number;
readonly start_main_thread: () => void;
readonly start_worker_thread: () => void;
readonly __wbindgen_export_0: WebAssembly.Memory;
readonly __wbindgen_malloc: (a: number) => number;
readonly __wbindgen_free: (a: number, b: number) => void;
readonly __wbindgen_start: () => void;
}
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {InitInput | Promise<InitInput>} module_or_path
* @param {WebAssembly.Memory} maybe_memory
*
* @returns {Promise<InitOutput>}
*/
export default function init(
module_or_path?: InitInput | Promise<InitInput>,
maybe_memory?: WebAssembly.Memory,
): Promise<InitOutput>;

View File

@@ -0,0 +1,196 @@
let wasm;
let memory;
const heap = new Array(32).fill(undefined);
heap.push(undefined, null, true, false);
let heap_next = heap.length;
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
let cachedTextDecoder = new TextDecoder('utf-8', {
ignoreBOM: true,
fatal: true,
});
cachedTextDecoder.decode();
let cachegetUint8Memory0 = null;
function getUint8Memory0() {
if (
cachegetUint8Memory0 === null ||
cachegetUint8Memory0.buffer !== wasm.__wbindgen_export_0.buffer
) {
cachegetUint8Memory0 = new Uint8Array(wasm.__wbindgen_export_0.buffer);
}
return cachegetUint8Memory0;
}
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().slice(ptr, ptr + len));
}
let WASM_VECTOR_LEN = 0;
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1);
getUint8Memory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
let cachegetInt32Memory0 = null;
function getInt32Memory0() {
if (
cachegetInt32Memory0 === null ||
cachegetInt32Memory0.buffer !== wasm.__wbindgen_export_0.buffer
) {
cachegetInt32Memory0 = new Int32Array(wasm.__wbindgen_export_0.buffer);
}
return cachegetInt32Memory0;
}
function getArrayU8FromWasm0(ptr, len) {
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
}
/**
* @param {Uint8Array} data
* @param {number} level
* @returns {Uint8Array}
*/
export function optimise(data, level) {
try {
const retptr = wasm.__wbindgen_export_1.value - 16;
wasm.__wbindgen_export_1.value = retptr;
var ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
var len0 = WASM_VECTOR_LEN;
wasm.optimise(retptr, ptr0, len0, level);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v1 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_free(r0, r1 * 1);
return v1;
} finally {
wasm.__wbindgen_export_1.value += 16;
}
}
function getObject(idx) {
return heap[idx];
}
function dropObject(idx) {
if (idx < 36) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
/**
* @param {number} num
* @returns {any}
*/
export function worker_initializer(num) {
var ret = wasm.worker_initializer(num);
return takeObject(ret);
}
/**
*/
export function start_main_thread() {
wasm.start_main_thread();
}
/**
*/
export function start_worker_thread() {
wasm.start_worker_thread();
}
async function load(module, imports, maybe_memory) {
if (typeof Response === 'function' && module instanceof Response) {
memory = imports.wbg.memory = new WebAssembly.Memory({
initial: 17,
maximum: 16384,
shared: true,
});
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn(
'`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n',
e,
);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
memory = imports.wbg.memory = maybe_memory;
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
async function init(input, maybe_memory) {
if (typeof input === 'undefined') {
input = import.meta.url.replace(/\.js$/, '_bg.wasm');
}
const imports = {};
imports.wbg = {};
imports.wbg.__wbindgen_module = function () {
var ret = init.__wbindgen_wasm_module;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_memory = function () {
var ret = wasm.__wbindgen_export_0;
return addHeapObject(ret);
};
imports.wbg.__wbg_of_6510501edc06d65e = function (arg0, arg1) {
var ret = Array.of(takeObject(arg0), takeObject(arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_throw = function (arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
if (
typeof input === 'string' ||
(typeof Request === 'function' && input instanceof Request) ||
(typeof URL === 'function' && input instanceof URL)
) {
input = fetch(input);
}
const { instance, module } = await load(await input, imports, maybe_memory);
wasm = instance.exports;
init.__wbindgen_wasm_module = module;
wasm.__wbindgen_start();
return wasm;
}
export default init;

Binary file not shown.

View File

@@ -0,0 +1,12 @@
/* tslint:disable */
/* eslint-disable */
export function optimise(a: number, b: number, c: number, d: number): void;
export function malloc(a: number): number;
export function free(a: number): void;
export function worker_initializer(a: number): number;
export function start_main_thread(): void;
export function start_worker_thread(): void;
export const __wbindgen_export_0: WebAssembly.Memory;
export function __wbindgen_malloc(a: number): number;
export function __wbindgen_free(a: number, b: number): void;
export function __wbindgen_start(): void;

View File

@@ -51,14 +51,20 @@ function getArrayU8FromWasm0(ptr, len) {
* @returns {Uint8Array}
*/
export function optimise(data, level) {
var ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
var len0 = WASM_VECTOR_LEN;
wasm.optimise(8, ptr0, len0, level);
var r0 = getInt32Memory0()[8 / 4 + 0];
var r1 = getInt32Memory0()[8 / 4 + 1];
var v1 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_free(r0, r1 * 1);
return v1;
try {
const retptr = wasm.__wbindgen_export_0.value - 16;
wasm.__wbindgen_export_0.value = retptr;
var ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
var len0 = WASM_VECTOR_LEN;
wasm.optimise(retptr, ptr0, len0, level);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v1 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_free(r0, r1 * 1);
return v1;
} finally {
wasm.__wbindgen_export_0.value += 16;
}
}
async function load(module, imports) {

View File

@@ -0,0 +1 @@
nightly

View File

@@ -1,9 +1,12 @@
mod malloc_shim;
use wasm_bindgen::prelude::*;
use oxipng::AlphaOptim;
#[wasm_bindgen(catch)]
mod malloc_shim;
#[cfg(feature = "parallel")]
pub mod parallel;
#[wasm_bindgen]
pub fn optimise(data: &[u8], level: u8) -> Vec<u8> {
let mut options = oxipng::Options::from_preset(level);
options.alphas.insert(AlphaOptim::Black);

View File

@@ -0,0 +1,61 @@
use crossbeam_channel::{Sender, Receiver, bounded};
use once_cell::sync::OnceCell;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = Array, js_name = of)]
fn array_of_2(a: JsValue, b: JsValue) -> JsValue;
}
// This is one of the parts that work around Chromium incorrectly implementing postMessage:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1075645
//
// rayon::ThreadPoolBuilder (used below) executes spawn handler to populate the worker pool,
// and then blocks the current thread until each worker unblocks its (opaque) lock.
//
// Normally, we could use postMessage directly inside the spawn handler to
// post module + memory + threadPtr to each worker, and the block the current thread.
//
// However, that bug means that postMessage is currently delayed until the next event loop,
// which will never spin since we block the current thread, and so the other workers will
// never be able to unblock us.
//
// To work around this problem, we:
// 1) Expose `worker_initializer` that returns module + memory pair (without threadPtr)
// that workers can be initialised with to become native threads.
// JavaScript can postMessage this pair in advance, and asynchronously wait for workers
// to acknowledge the receipt.
// 2) Create a global communication channel on the Rust side using crossbeam.
// It will be used to send threadPtr to the pre-initialised workers
// instead of postMessage.
// 3) Provide a separate `start_main_thread` that expects all workers to be ready,
// and just uses the provided channel to send `threadPtr`s using the
// shared memory and blocks the current thread until they're all grabbed.
// 4) Provide a `worker_initializer` that is expected to be invoked from various workers,
// reads one `threadPtr` from the shared channel and starts running it.
static CHANNEL: OnceCell<(Sender<rayon::ThreadBuilder>, Receiver<rayon::ThreadBuilder>)> = OnceCell::new();
#[wasm_bindgen]
pub fn worker_initializer(num: usize) -> JsValue {
CHANNEL.get_or_init(|| bounded(num));
array_of_2(wasm_bindgen::module(), wasm_bindgen::memory())
}
#[wasm_bindgen]
pub fn start_main_thread() {
let (sender, _) = CHANNEL.get().unwrap();
rayon::ThreadPoolBuilder::new()
.num_threads(sender.capacity().unwrap())
.spawn_handler(|thread| Ok(sender.send(thread).unwrap_throw()))
.build_global()
.unwrap_throw()
}
#[wasm_bindgen]
pub fn start_worker_thread() {
let (_, receiver) = CHANNEL.get().unwrap();
receiver.recv().unwrap_throw().run()
}

Binary file not shown.

View File

@@ -1,9 +1,13 @@
FROM emscripten/emsdk:1.39.19 AS wasm-tools
ARG RUST_IMG=rust:1.47
FROM emscripten/emsdk:2.0.8 AS wasm-tools
WORKDIR /opt/wasm-tools
RUN wget -qO- https://github.com/rustwasm/wasm-pack/releases/download/v0.9.1/wasm-pack-v0.9.1-x86_64-unknown-linux-musl.tar.gz | tar -xzf - --strip 1
FROM rust:1.46.0-slim-buster AS rust
FROM $RUST_IMG AS rust
ARG RUST_IMG
RUN rustup target add wasm32-unknown-unknown
RUN if [[ $RUST_IMG = rustlang/rust:* ]] ; then rustup component add rust-src ; fi
COPY --from=wasm-tools /emsdk/upstream/bin/wasm-opt /emsdk/upstream/bin/clang /usr/local/bin/
COPY --from=wasm-tools /emsdk/upstream/lib/ /usr/local/lib/
COPY --from=wasm-tools /emsdk/upstream/emscripten/system/include/libc/ /wasm32/include/

View File

@@ -1,14 +1,12 @@
<!DOCTYPE html>
<script type="module">
import webp_dec from './webp_dec.js';
<!doctype html>
<script src='webp_dec.js'></script>
<script>
async function loadFile(src) {
const resp = await fetch(src);
return await resp.arrayBuffer();
}
async function main() {
const module = await webp_dec();
webp_dec().then(async module => {
console.log('Version:', module.version().toString(16));
const image = await loadFile('../../example.webp');
const imageData = module.decode(image);
@@ -18,7 +16,5 @@
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
ctx.putImageData(imageData, 0, 0);
}
main();
});
</script>

File diff suppressed because it is too large Load Diff

BIN
codecs/webp/dec/webp_dec.wasm Normal file → Executable file

Binary file not shown.

View File

@@ -1,12 +1,11 @@
<!DOCTYPE html>
<script type="module">
import webp_enc from './webp_enc.js';
<!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));
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];
@@ -16,9 +15,7 @@
return ctx.getImageData(0, 0, img.width, img.height);
}
async function main() {
const module = await webp_enc();
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, {
@@ -51,13 +48,11 @@
use_sharp_yuv: 0,
});
console.log('size', result.length);
const blob = new Blob([result], { type: 'image/webp' });
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);
}
main();
});
</script>

File diff suppressed because it is too large Load Diff

BIN
codecs/webp/enc/webp_enc.wasm Normal file → Executable file

Binary file not shown.

50
codecs/wp2/Makefile Normal file
View File

@@ -0,0 +1,50 @@
CODEC_URL = https://chromium.googlesource.com/codecs/libwebp2/+archive/c90b5b476004c9a98731ae1c175cebab5de50fbf.tar.gz
CODEC_DIR = node_modules/wp2
CODEC_BUILD_DIR := $(CODEC_DIR)/.build
CODEC_OUT := $(CODEC_BUILD_DIR)/libwebp2.a
OUT_JS = enc/wp2_enc.js dec/wp2_dec.js
OUT_WASM = $(OUT_JS:.js=.wasm)
.PHONY: all clean
all: $(OUT_JS)
%.js: %.cpp $(CODEC_OUT)
$(CXX) \
-I $(CODEC_DIR) \
${CXXFLAGS} \
${LDFLAGS} \
--bind \
--closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s TEXTDECODER=2 \
-s ENVIRONMENT='worker' \
-s EXPORT_ES6=1 \
-s EXPORT_NAME="$(basename $(@F))" \
-o $@ \
$+
$(CODEC_OUT): $(CODEC_BUILD_DIR)/Makefile
cd $(CODEC_BUILD_DIR) && \
$(MAKE)
$(CODEC_BUILD_DIR)/Makefile: $(CODEC_DIR)/CMakeLists.txt
mkdir -p $(CODEC_BUILD_DIR)
cd $(CODEC_BUILD_DIR) && \
emcmake cmake \
-DWP2_ENABLE_SIMD=0 \
-DWP2_BUILD_TESTS=0 \
-DWP2_ENABLE_TESTS=0 \
-DWP2_BUILD_EXAMPLES=0 \
-DWP2_BUILD_EXTRAS=0 \
../
$(CODEC_DIR)/CMakeLists.txt:
mkdir -p $(CODEC_DIR)
curl -sL $(CODEC_URL) | tar xz -C $(CODEC_DIR)
clean:
$(RM) $(OUT_JS) $(OUT_WASM)
$(MAKE) -C $(CODEC_BUILD_DIR) clean

17
codecs/wp2/dec/README.md Normal file
View File

@@ -0,0 +1,17 @@
# WebP2 decoder
- Source: <https://chromium.googlesource.com/codecs/libwebp2>
## Dependencies
- Docker
## Example
N/A
## API
### `ImageData decode(uint8_t* image_buffer, int image_width, int image_height)`
Decodes the given WebP2 buffer into raw RGBA represented as an `ImageData`.

View File

@@ -0,0 +1,24 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <cstdio>
#include "src/wp2/decode.h"
using namespace emscripten;
thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");
thread_local const val ImageData = val::global("ImageData");
val decode(std::string image_in) {
WP2::ArgbBuffer buffer(WP2_rgbA_32);
WP2Status status = WP2::Decode(image_in, &buffer);
if (status != WP2_STATUS_OK) {
return val::null();
}
return ImageData.new_(
Uint8ClampedArray.new_(typed_memory_view(buffer.stride * buffer.height, buffer.GetRow8(0))),
buffer.width, buffer.height);
}
EMSCRIPTEN_BINDINGS(my_module) {
function("decode", &decode);
}

7
codecs/wp2/dec/wp2_dec.d.ts vendored Normal file
View File

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

1185
codecs/wp2/dec/wp2_dec.js Normal file

File diff suppressed because it is too large Load Diff

BIN
codecs/wp2/dec/wp2_dec.wasm Executable file

Binary file not shown.

17
codecs/wp2/enc/README.md Normal file
View File

@@ -0,0 +1,17 @@
# WebP2 encoder
- Source: <https://chromium.googlesource.com/codecs/libwebp2>
## Dependencies
- Docker
## Example
N/A
## API
### `UInt8Array encode(uint8_t* image_buffer, int image_width, int image_height, WP2::EncoderConfig config)`
Encodes the given image with given dimension to WebP2.

View File

@@ -0,0 +1,38 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <cstdio>
#include "src/wp2/encode.h"
using namespace emscripten;
thread_local const val Uint8Array = val::global("Uint8Array");
val encode(std::string image_in, int image_width, int image_height, WP2::EncoderConfig config) {
uint8_t* image_buffer = (uint8_t*)image_in.c_str();
WP2::ArgbBuffer src = WP2::ArgbBuffer();
WP2Status status =
src.Import(WP2_rgbA_32, // Format. WP2_RGBA_32 is the same but NOT premultiplied alpha
image_width, image_height, image_buffer, 4 * image_width);
if (status != WP2_STATUS_OK) {
return val::null();
}
WP2::MemoryWriter memory_writer;
status = WP2::Encode(src, &memory_writer, config);
if (status != WP2_STATUS_OK) {
return val::null();
}
return Uint8Array.new_(typed_memory_view(memory_writer.size_, memory_writer.mem_));
}
EMSCRIPTEN_BINDINGS(my_module) {
value_object<WP2::EncoderConfig>("WP2EncoderConfig")
.field("quality", &WP2::EncoderConfig::quality)
.field("alpha_quality", &WP2::EncoderConfig::alpha_quality)
.field("speed", &WP2::EncoderConfig::speed)
.field("pass", &WP2::EncoderConfig::pass)
.field("sns", &WP2::EncoderConfig::sns);
function("encode", &encode);
}

20
codecs/wp2/enc/wp2_enc.d.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
export interface EncodeOptions {
quality: number;
alpha_quality: number;
speed: number;
pass: number;
sns: number;
}
export interface WP2Module extends EmscriptenWasm.Module {
encode(
data: BufferSource,
width: number,
height: number,
options: EncodeOptions,
): Uint8Array | null;
}
declare var moduleFactory: EmscriptenWasm.ModuleFactory<WP2Module>;
export default moduleFactory;

1277
codecs/wp2/enc/wp2_enc.js Normal file

File diff suppressed because it is too large Load Diff

BIN
codecs/wp2/enc/wp2_enc.wasm Executable file

Binary file not shown.

6
codecs/wp2/package.json Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "wp2",
"scripts": {
"build": "../build-cpp.sh"
}
}

View File

@@ -10,6 +10,7 @@ declare namespace EmscriptenWasm {
// Options object for modularized Emscripten files. Shoe-horned by @surma.
// FIXME: This an incomplete definition!
interface ModuleOpts {
mainScriptUrlOrBlob?: string;
noInitialRun?: boolean;
locateFile?: (url: string) => string;
onRuntimeInitialized?: () => void;

View File

@@ -13,7 +13,12 @@
import { promises as fsp, readFileSync } from 'fs';
import { createHash } from 'crypto';
import { promisify } from 'util';
import { parse as parsePath, resolve as resolvePath, dirname } from 'path';
import {
parse as parsePath,
resolve as resolvePath,
dirname,
normalize as nomalizePath,
} from 'path';
import postcss from 'postcss';
import postCSSNested from 'postcss-nested';
@@ -55,10 +60,15 @@ export default function (resolveFileUrl) {
hashToId = new Map();
pathToResult = new Map();
const cssPaths = await globP('src/**/*.css', {
nodir: true,
absolute: true,
});
const cssPaths = (
await globP('src/**/*.css', {
nodir: true,
absolute: true,
})
).map((cssPath) =>
// glob() returns windows paths with a forward slash. Normalise it:
nomalizePath(cssPath),
);
await Promise.all(
cssPaths.map(async (path) => {
@@ -73,6 +83,7 @@ export default function (resolveFileUrl) {
getJSON(_, json) {
moduleJSON = json;
},
root: '',
}),
postCSSUrl({
url: ({ relativePath, url }) => {
@@ -146,7 +157,7 @@ export default function (resolveFileUrl) {
async load(id) {
if (id === appendCssModule) return appendCssSource;
if (id.startsWith(sourcePrefix)) {
const path = id.slice(sourcePrefix.length);
const path = nomalizePath(id.slice(sourcePrefix.length));
if (!pathToResult.has(path)) {
throw Error(`Cannot find ${path} in pathToResult`);

View File

@@ -12,9 +12,9 @@
*/
import { promisify } from 'util';
import * as path from 'path';
import { promises as fsp } from 'fs';
import { posix } from 'path';
import glob from 'glob';
import { promises as fsp } from 'fs';
const globP = promisify(glob);
const autoGenComment =
@@ -33,7 +33,7 @@ export default function () {
const workerBasePath = path.join(process.cwd(), 'src', 'features-worker');
const featuresWorkerTsNames = workerImports.map((tsImport) => [
path.relative(workerBasePath, tsImport),
path.relative(workerBasePath, tsImport).split(path.sep).join(posix.sep),
path.basename(tsImport),
]);
@@ -78,7 +78,10 @@ export default function () {
);
const featuresWorkerBridgeTsNames = workerImports.map((tsImport) => [
path.relative(workerBridgeBasePath, tsImport),
path
.relative(workerBridgeBasePath, tsImport)
.split(path.sep)
.join(posix.sep),
path.basename(tsImport),
]);
@@ -165,7 +168,10 @@ export default function () {
previousJoinedMetas = joinedMetas;
const getTsName = (tsImport) => [
path.relative(featureMetaBasePath, tsImport),
path
.relative(featureMetaBasePath, tsImport)
.split(path.sep)
.join(posix.sep),
path.basename(tsImport.slice(0, -'/shared/meta'.length)),
];

View File

@@ -13,6 +13,7 @@
import { promisify } from 'util';
import path from 'path';
import { posix } from 'path';
import glob from 'glob';
const globP = promisify(glob);
@@ -36,9 +37,9 @@ export default function initialCssPlugin() {
// Sort the matches so the parentmost items appear first.
// This is a bit of a hack, but it means the util stuff appears in the cascade first.
const sortedMatches = matches
.map((match) => match.split(path.sep))
.map((match) => path.normalize(match).split(path.sep))
.sort((a, b) => a.length - b.length)
.map((match) => path.join(...match));
.map((match) => posix.join(...match));
const imports = sortedMatches
.map((id, i) => `import css${i} from 'css:${id}';\n`)

View File

@@ -15,7 +15,12 @@
if (!self.<%- amdFunctionName %>) {
const singleRequire = async name => {
if (name === 'require') return require;
let url = name.slice(1) + '.js';
let url;
if (name.startsWith(location.origin)) {
url = name.slice(location.origin.length);
} else {
url = name.slice(1) + '.js';
}
if (!url.startsWith('/c/')) {
url = '/c' + url;
}
@@ -77,7 +82,7 @@ if (!self.<%- amdFunctionName %>) {
})
).then(deps => {
const facValue = factory(...deps);
if(!exports.default) {
if (!exports.default) {
exports.default = facValue;
}
return exports;

View File

@@ -17,9 +17,12 @@ import { promisify } from 'util';
import * as ts from 'typescript';
import glob from 'glob';
import { sync as whichSync } from 'which';
const globP = promisify(glob);
const tscPath = whichSync('tsc');
const extRe = /\.tsx?$/;
function loadConfig(mainPath) {
@@ -59,7 +62,7 @@ export default function simpleTS(mainPath, { noBuild, watch } = {}) {
}
tsBuildDone = Promise.resolve().then(async () => {
await new Promise((resolve) => {
const proc = spawn('tsc', args, {
const proc = spawn(tscPath, args, {
stdio: 'inherit',
});
@@ -75,7 +78,7 @@ export default function simpleTS(mainPath, { noBuild, watch } = {}) {
if (watch) {
tsBuildDone.then(() => {
spawn('tsc', [...args, '--watch', '--preserveWatchOutput'], {
spawn(tscPath, [...args, '--watch', '--preserveWatchOutput'], {
stdio: 'inherit',
});
});
@@ -124,6 +127,15 @@ export default function simpleTS(mainPath, { noBuild, watch } = {}) {
relative(process.cwd(), id),
).replace(extRe, '.js');
console.log(
`simple-ts mapping`,
id,
'to',
newId,
'with outDir',
config.options.outDir,
);
return fsp.readFile(newId, { encoding: 'utf8' });
},
};

8273
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -38,7 +38,8 @@
"rollup": "^2.33.1",
"rollup-plugin-terser": "^7.0.2",
"serve": "^11.3.2",
"typescript": "^4.0.5"
"typescript": "^4.0.5",
"which": "^2.0.2"
},
"husky": {
"hooks": {
@@ -49,5 +50,8 @@
"*.{js,css,json,md,ts,tsx}": [
"prettier --write"
]
},
"dependencies": {
"wasm-feature-detect": "^1.2.9"
}
}

View File

@@ -107,6 +107,7 @@ export default async function ({ watch }) {
{ resolveFileUrl, resolveImportMeta },
clientBundlePlugin(
{
external: ['worker_threads'],
plugins: [
{ resolveFileUrl, resolveImportMeta },
OMT({ loader: await omtLoaderPromise }),
@@ -115,7 +116,7 @@ export default async function ({ watch }) {
commonjs(),
resolve(),
replace({ __PRERENDER__: false, __PRODUCTION__: isProduction }),
terser({ module: true }),
isProduction ? terser({ module: true }) : {},
],
preserveEntrySignatures: false,
},
@@ -124,6 +125,9 @@ export default async function ({ watch }) {
format: 'amd',
chunkFileNames: jsFileName,
entryFileNames: jsFileName,
// This is needed because emscripten's workers use 'this', so they trigger all kinds of interop things,
// such as double-wrapping objects in { default }.
interop: false,
},
resolveFileUrl,
),

View File

@@ -156,11 +156,11 @@ export default class Options extends Component<Props, State> {
<section class={`${style.optionOneCell} ${style.optionsSection}`}>
{supportedEncoderMap ? (
<Select
value={encoderState ? encoderState.type : ''}
value={encoderState ? encoderState.type : 'identity'}
onChange={this.onEncoderTypeChange}
large
>
<option value="">Original Image</option>
<option value="identity">Original Image</option>
{Object.entries(supportedEncoderMap).map(([type, encoder]) => (
<option value={type}>{encoder.meta.label}</option>
))}

View File

@@ -9,7 +9,7 @@ import {
CopyAcrossIconProps,
} from 'client/lazy-app/icons';
import 'shared/initial-app/custom-els/loading-spinner';
import { SourceImage } from '../../compress';
import { SourceImage } from '../';
interface Props {
loading: boolean;

View File

@@ -27,7 +27,7 @@ import Options from './Options';
import ResultCache from './result-cache';
import { cleanMerge, cleanSet } from '../util/clean-modify';
import './custom-els/MultiPanel';
import Results from './results';
import Results from './Results';
import WorkerBridge from '../worker-bridge';
import { resize } from 'features/processors/resize/client';
import type SnackBarElement from 'shared/initial-app/custom-els/snack-bar';
@@ -101,6 +101,12 @@ async function decodeImage(
if (mimeType === 'image/webp') {
return await workerBridge.webpDecode(signal, blob);
}
if (mimeType === 'image/jpegxl') {
return await workerBridge.jxlDecode(signal, blob);
}
if (mimeType === 'image/webp2') {
return await workerBridge.wp2Decode(signal, blob);
}
// If it's not one of those types, fall through and try built-in decoding for a laugh.
}
return await abortable(signal, builtinDecode(blob));
@@ -360,6 +366,10 @@ export default class Compress extends Component<Props, State> {
componentWillUnmount(): void {
updateDocumentTitle();
this.mainAbortController.abort();
for (const controller of this.sideAbortControllers) {
controller.abort();
}
}
componentDidUpdate(prevProps: Props, prevState: State): void {
@@ -495,6 +505,8 @@ export default class Compress extends Component<Props, State> {
const needsProcessing =
needsPreprocessing ||
!latestSideJob.processorState ||
// If we're going to or from 'original image' we should reprocess
!!latestSideJob.encoderState !== !!sideJobStates[i].encoderState ||
!processorStateEquivalent(
latestSideJob.processorState,
sideJobStates[i].processorState,

View File

@@ -148,7 +148,9 @@ const magicNumberToMimeType = new Map<RegExp, string>([
[/^II*/, 'image/tiff'],
[/^MM\x00*/, 'image/tiff'],
[/^RIFF....WEBPVP8[LX ]/, 'image/webp'],
[/^\xF4\xFF\x6F/, 'image/webp2'],
[/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/, 'image/avif'],
[/^\xff\x0a/, 'image/jpegxl'],
]);
export async function sniffMimeType(blob: Blob): Promise<string> {

View File

@@ -3,3 +3,8 @@
/c/*
Cache-Control: max-age=31536000
# COOP+COEP for WebAssembly threads.
/*
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

View File

@@ -0,0 +1,32 @@
/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import jxlDecoder, { JXLModule } from 'codecs/jxl/dec/jxl_dec';
import wasmUrl from 'url:codecs/jxl/dec/jxl_dec.wasm';
import { initEmscriptenModule, blobToArrayBuffer } from 'features/worker-utils';
let emscriptenModule: Promise<JXLModule>;
export default async function decode(blob: Blob): Promise<ImageData> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(jxlDecoder, wasmUrl);
}
const [module, data] = await Promise.all([
emscriptenModule,
blobToArrayBuffer(blob),
]);
const result = module.decode(data);
if (!result) throw new Error('Decoding error');
return result;
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import wp2Decoder, { WP2Module } from 'codecs/wp2/dec/wp2_dec';
import wasmUrl from 'url:codecs/wp2/dec/wp2_dec.wasm';
import { initEmscriptenModule, blobToArrayBuffer } from 'features/worker-utils';
let emscriptenModule: Promise<WP2Module>;
export default async function decode(blob: Blob): Promise<ImageData> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(wp2Decoder, wasmUrl);
}
const [module, data] = await Promise.all([
emscriptenModule,
blobToArrayBuffer(blob),
]);
const result = module.decode(data);
if (!result) throw new Error('Decoding error');
return result;
}

View File

@@ -10,20 +10,34 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import avifEncoder, { AVIFModule } from 'codecs/avif/enc/avif_enc';
import type { AVIFModule } from 'codecs/avif/enc/avif_enc';
import type { EncodeOptions } from '../shared/meta';
import wasmUrl from 'url:codecs/avif/enc/avif_enc.wasm';
import wasmUrlWithoutMT from 'url:codecs/avif/enc/avif_enc.wasm';
import wasmUrlWithMT from 'url:codecs/avif/enc/avif_enc_mt.wasm';
import workerUrl from 'omt:codecs/avif/enc/avif_enc_mt.worker.js';
import { initEmscriptenModule } from 'features/worker-utils';
import { threads } from 'wasm-feature-detect';
let emscriptenModule: Promise<AVIFModule>;
async function init() {
if (await threads()) {
const avifEncoder = await import('codecs/avif/enc/avif_enc_mt');
return initEmscriptenModule<AVIFModule>(
avifEncoder.default,
wasmUrlWithMT,
workerUrl,
);
}
const avifEncoder = await import('codecs/avif/enc/avif_enc.js');
return initEmscriptenModule(avifEncoder.default, wasmUrlWithoutMT);
}
export default async function encode(
data: ImageData,
options: EncodeOptions,
): Promise<ArrayBuffer> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(avifEncoder, wasmUrl);
}
if (!emscriptenModule) emscriptenModule = init();
const module = await emscriptenModule;
const result = module.encode(data.data, data.width, data.height, options);

View File

@@ -0,0 +1,189 @@
import { EncodeOptions } from '../shared/meta';
import type WorkerBridge from 'client/lazy-app/worker-bridge';
import { h, Component } from 'preact';
import { preventDefault, shallowEqual } from 'client/lazy-app/util';
import * as style from 'client/lazy-app/Compress/Options/style.css';
import Range from 'client/lazy-app/Compress/Options/Range';
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
import Expander from 'client/lazy-app/Compress/Options/Expander';
export const encode = (
signal: AbortSignal,
workerBridge: WorkerBridge,
imageData: ImageData,
options: EncodeOptions,
) => workerBridge.jxlEncode(signal, imageData, options);
interface Props {
options: EncodeOptions;
onChange(newOptions: EncodeOptions): void;
}
interface State {
options: EncodeOptions;
effort: number;
quality: number;
progressive: boolean;
edgePreservingFilter: number;
lossless: boolean;
slightLoss: boolean;
}
const maxSpeed = 7;
export class Options extends Component<Props, State> {
static getDerivedStateFromProps(
props: Props,
state: State,
): Partial<State> | null {
if (state.options && shallowEqual(state.options, props.options)) {
return null;
}
const { options } = props;
// Create default form state from options
return {
options,
effort: maxSpeed - options.speed,
quality: options.quality,
progressive: options.progressive,
edgePreservingFilter: options.epf,
lossless: options.quality === 100,
slightLoss: options.lossyPalette,
};
}
// The rest of the defaults are set in getDerivedStateFromProps
state: State = {
lossless: false,
} as State;
private _inputChangeCallbacks = new Map<string, (event: Event) => void>();
private _inputChange = (prop: keyof State, type: 'number' | 'boolean') => {
// Cache the callback for performance
if (!this._inputChangeCallbacks.has(prop)) {
this._inputChangeCallbacks.set(prop, (event: Event) => {
const formEl = event.target as HTMLInputElement | HTMLSelectElement;
const newVal =
type === 'boolean'
? 'checked' in formEl
? formEl.checked
: !!formEl.value
: Number(formEl.value);
const newState: Partial<State> = {
[prop]: newVal,
};
const optionState = {
...this.state,
...newState,
};
const newOptions: EncodeOptions = {
speed: maxSpeed - optionState.effort,
quality: optionState.lossless ? 100 : optionState.quality,
progressive: optionState.progressive,
epf: optionState.edgePreservingFilter,
nearLossless: 0,
lossyPalette: optionState.lossless ? optionState.slightLoss : false,
};
// Updating options, so we don't recalculate in getDerivedStateFromProps.
newState.options = newOptions;
this.setState(newState);
this.props.onChange(newOptions);
});
}
return this._inputChangeCallbacks.get(prop)!;
};
render(
{}: Props,
{
effort,
quality,
progressive,
edgePreservingFilter,
lossless,
slightLoss,
}: State,
) {
// I'm rendering both lossy and lossless forms, as it becomes much easier when
// gathering the data.
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<label class={style.optionInputFirst}>
<Checkbox
name="lossless"
checked={lossless}
onChange={this._inputChange('lossless', 'boolean')}
/>
Lossless
</label>
<Expander>
{lossless && (
<label class={style.optionInputFirst}>
<Checkbox
name="slightLoss"
checked={slightLoss}
onChange={this._inputChange('slightLoss', 'boolean')}
/>
Slight loss
</label>
)}
</Expander>
<Expander>
{!lossless && (
<div>
<div class={style.optionOneCell}>
<Range
min="0"
max="99.9"
step="0.1"
value={quality}
onInput={this._inputChange('quality', 'number')}
>
Quality:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
min="0"
max="3"
value={edgePreservingFilter}
onInput={this._inputChange('edgePreservingFilter', 'number')}
>
Edge preserving filter:
</Range>
</div>
</div>
)}
</Expander>
<label class={style.optionInputFirst}>
<Checkbox
name="progressive"
checked={progressive}
onChange={this._inputChange('progressive', 'boolean')}
/>
Progressive rendering
</label>
<div class={style.optionOneCell}>
<Range
min="0"
max={maxSpeed - 1}
value={effort}
onInput={this._inputChange('effort', 'number')}
>
Effort:
</Range>
</div>
</form>
);
}
}

View File

@@ -10,4 +10,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference path="../../../../../missing-types.d.ts" />
import type { EncodeOptions } from 'codecs/jxl/enc/jxl_enc';
export { EncodeOptions };
export const label = 'JPEG XL (beta)';
export const mimeType = 'image/jpegxl';
export const extension = 'jxl';
export const defaultOptions: EncodeOptions = {
speed: 5,
quality: 50,
progressive: false,
epf: 2,
nearLossless: 0,
lossyPalette: false,
};

View File

@@ -0,0 +1,33 @@
/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import jxlEncoder, { JXLModule } from 'codecs/jxl/enc/jxl_enc';
import wasmUrl from 'url:codecs/jxl/enc/jxl_enc.wasm';
import { initEmscriptenModule } from 'features/worker-utils';
import type { EncodeOptions } from '../shared/meta';
let emscriptenModule: Promise<JXLModule>;
export default async function encode(
data: ImageData,
options: EncodeOptions,
): Promise<ArrayBuffer> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(jxlEncoder, wasmUrl);
}
const module = await emscriptenModule;
const result = module.encode(data.data, data.width, data.height, options);
if (!result) throw new Error('Encoding error.');
// wasm cant run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
return result.buffer as ArrayBuffer;
}

View File

@@ -1,13 +0,0 @@
/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference path="../../../../../missing-types.d.ts" />

View File

@@ -10,17 +10,76 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import initOxiWasm, { optimise } from 'codecs/oxipng/pkg';
import oxiWasmUrl from 'url:codecs/oxipng/pkg/squoosh_oxipng_bg.wasm';
import initOxiWasmST, {
optimise as optimiseST,
} from 'codecs/oxipng/pkg/squoosh_oxipng';
import initOxiWasmMT, {
worker_initializer,
start_main_thread,
optimise as optimiseMT,
} from 'codecs/oxipng/pkg-parallel/squoosh_oxipng';
import oxiWasmUrlST from 'url:codecs/oxipng/pkg/squoosh_oxipng_bg.wasm';
import oxiWasmUrlMT from 'url:codecs/oxipng/pkg-parallel/squoosh_oxipng_bg.wasm';
import { EncodeOptions } from '../shared/meta';
import { threads } from 'wasm-feature-detect';
import workerURL from 'omt:./sub-worker';
import type { WorkerInit } from './sub-worker';
let wasmReady: Promise<unknown>;
function initWorker(worker: Worker, workerInit: WorkerInit) {
return new Promise<void>((resolve) => {
worker.postMessage(workerInit);
worker.addEventListener('message', () => resolve(), { once: true });
});
}
async function initMT() {
const num = navigator.hardwareConcurrency;
// First, let browser fetch and spawn Workers for our pool in the background.
// This is fairly expensive, so we want to start it as early as possible.
const workers = Array.from({ length: num }, () => new Worker(workerURL));
// Meanwhile, asynchronously compile, instantiate and initialise Wasm on our main thread.
await initOxiWasmMT(oxiWasmUrlMT);
// Get module+memory from the Wasm instance.
//
// Ideally we wouldn't go via Wasm bindings here, since both are just JS variables, but memory is
// currently not exposed on the Wasm instance correctly by wasm-bindgen.
const workerInit: WorkerInit = worker_initializer(num);
// Once done, we want to send module+memory to each Worker so that they instantiate Wasm too.
// While doing so, we need to wait for Workers to acknowledge that they have received our message.
// Ideally this shouldn't be necessary, but Chromium currently doesn't conform to the spec:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1075645
//
// If we didn't do this ping-pong game, the `start_main_thread` below would block the current
// thread on an atomic before even *sending* the `postMessage` containing memory,
// so Workers would never be able to unblock us back.
await Promise.all(workers.map((worker) => initWorker(worker, workerInit)));
// Finally, instantiate rayon pool - this will use shared Wasm memory to send tasks to the
// Workers and then block until they're all ready.
start_main_thread();
return optimiseMT;
}
async function initST() {
await initOxiWasmST(oxiWasmUrlST);
return optimiseST;
}
let wasmReady: Promise<typeof optimiseMT | typeof optimiseST>;
export default async function encode(
data: ArrayBuffer,
options: EncodeOptions,
): Promise<ArrayBuffer> {
if (!wasmReady) wasmReady = initOxiWasm(oxiWasmUrl);
await wasmReady;
if (!wasmReady) {
wasmReady = (await threads()) ? initMT() : initST();
}
const optimise = await wasmReady;
return optimise(new Uint8Array(data), options.level).buffer;
}

View File

@@ -0,0 +1,25 @@
import initOxiPNG, {
start_worker_thread,
} from 'codecs/oxipng/pkg-parallel/squoosh_oxipng';
export type WorkerInit = [WebAssembly.Module, WebAssembly.Memory];
addEventListener(
'message',
async (event) => {
// Tell the "main" thread that we've received the message.
//
// At this point, the "main" thread can run Wasm that
// will synchronously block waiting on other atomics.
//
// Note that we don't need to wait for Wasm instantiation here - it's
// better to start main thread as early as possible, and then it blocks
// on a shared atomic anyway until Worker is fully ready.
// @ts-ignore
postMessage(null);
await initOxiPNG(...(event.data as WorkerInit));
start_worker_thread();
},
{ once: true },
);

View File

@@ -1,13 +0,0 @@
/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference path="../../../../../missing-types.d.ts" />

View File

@@ -1,13 +0,0 @@
/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference path="../../../../../missing-types.d.ts" />

View File

@@ -1,13 +0,0 @@
/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference path="../../../../../missing-types.d.ts" />

View File

@@ -0,0 +1,113 @@
import { EncodeOptions } from '../shared/meta';
import type WorkerBridge from 'client/lazy-app/worker-bridge';
import { h, Component } from 'preact';
import { inputFieldValueAsNumber, preventDefault } from 'client/lazy-app/util';
import * as style from 'client/lazy-app/Compress/Options/style.css';
import Range from 'client/lazy-app/Compress/Options/Range';
export const encode = (
signal: AbortSignal,
workerBridge: WorkerBridge,
imageData: ImageData,
options: EncodeOptions,
) => workerBridge.wp2Encode(signal, imageData, options);
interface Props {
options: EncodeOptions;
onChange(newOptions: EncodeOptions): void;
}
interface State {
showAdvanced: boolean;
}
export class Options extends Component<Props, State> {
state: State = {
showAdvanced: false,
};
private onChange = (event: Event) => {
const form = (event.currentTarget as HTMLInputElement).closest(
'form',
) as HTMLFormElement;
const { options } = this.props;
const newOptions: EncodeOptions = {
quality: inputFieldValueAsNumber(form.quality, options.quality),
alpha_quality: inputFieldValueAsNumber(
form.alpha_quality,
options.alpha_quality,
),
speed: inputFieldValueAsNumber(form.speed, options.speed),
pass: inputFieldValueAsNumber(form.pass, options.pass),
sns: inputFieldValueAsNumber(form.sns, options.sns),
};
this.props.onChange(newOptions);
};
render({ options }: Props) {
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<div class={style.optionOneCell}>
<Range
name="quality"
min="0"
max="100"
step="1"
value={options.quality}
onInput={this.onChange}
>
Quality:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="alpha_quality"
min="0"
max="100"
step="1"
value={options.alpha_quality}
onInput={this.onChange}
>
Alpha Quality:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="speed"
min="0"
max="9"
step="1"
value={options.speed}
onInput={this.onChange}
>
Speed:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="pass"
min="1"
max="10"
step="1"
value={options.pass}
onInput={this.onChange}
>
Pass:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="sns"
min="0"
max="100"
step="1"
value={options.sns}
onInput={this.onChange}
>
Spatial noise shaping:
</Range>
</div>
</form>
);
}
}

View File

@@ -10,4 +10,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference path="../../../../../missing-types.d.ts" />
import type { EncodeOptions } from 'codecs/wp2/enc/wp2_enc';
export { EncodeOptions };
export const label = 'WebP v2 (unstable)';
export const mimeType = 'image/webp2';
export const extension = 'wp2';
export const defaultOptions: EncodeOptions = {
quality: 75,
alpha_quality: 100,
speed: 5,
pass: 1,
sns: 50,
};

View File

@@ -0,0 +1,33 @@
/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import wp2Encoder, { WP2Module } from 'codecs/wp2/enc/wp2_enc';
import wasmUrl from 'url:codecs/wp2/enc/wp2_enc.wasm';
import { initEmscriptenModule } from 'features/worker-utils';
import type { EncodeOptions } from '../shared/meta';
let emscriptenModule: Promise<WP2Module>;
export default async function encode(
data: ImageData,
options: EncodeOptions,
): Promise<ArrayBuffer> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(wp2Encoder, wasmUrl);
}
const module = await emscriptenModule;
const result = module.encode(data.data, data.width, data.height, options);
if (!result) throw new Error('Encoding error.');
// wasm cant run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
return result.buffer as ArrayBuffer;
}

View File

@@ -13,11 +13,16 @@
export function initEmscriptenModule<T extends EmscriptenWasm.Module>(
moduleFactory: EmscriptenWasm.ModuleFactory<T>,
wasmUrl: string,
workerUrl?: string,
): Promise<T> {
return moduleFactory({
// Just to be safe, don't automatically invoke any wasm functions
noInitialRun: true,
locateFile: () => wasmUrl,
locateFile: (url: string) => {
if (url.endsWith('.wasm')) return wasmUrl;
if (url.endsWith('.worker.js')) return workerUrl!;
throw Error('Unknown url in locateFile ' + url);
},
});
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -5,6 +5,7 @@
},
"include": [
"src/features/**/worker/**/*",
"src/features/**/sub-worker/**/*",
"src/features/**/shared/**/*",
"src/features/worker-utils/**/*",
"src/features-worker/**/*",