mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-12 08:47:31 +00:00
WebP encode options (#110)
* Flailing * Holy shit struct binding * Options in the encoder! * Integrating webp options * Addressing feedback * This isn't needed anymore
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<script src='webp_enc.js'></script>
|
<script src='webp_enc.js'></script>
|
||||||
<script>
|
<script>
|
||||||
const Module = webp_enc();
|
const module = webp_enc();
|
||||||
|
|
||||||
async function loadImage(src) {
|
async function loadImage(src) {
|
||||||
// Load image
|
// Load image
|
||||||
@@ -17,27 +17,48 @@
|
|||||||
return ctx.getImageData(0, 0, img.width, img.height);
|
return ctx.getImageData(0, 0, img.width, img.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
Module.onRuntimeInitialized = async _ => {
|
module.onRuntimeInitialized = async _ => {
|
||||||
const api = {
|
console.log('Version:', module.version().toString(16));
|
||||||
version: Module.cwrap('version', 'number', []),
|
|
||||||
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
|
|
||||||
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
|
|
||||||
encode: Module.cwrap('encode', '', ['number', 'number', 'number', 'number']),
|
|
||||||
free_result: Module.cwrap('free_result', '', ['number']),
|
|
||||||
get_result_pointer: Module.cwrap('get_result_pointer', 'number', []),
|
|
||||||
get_result_size: Module.cwrap('get_result_size', 'number', []),
|
|
||||||
};
|
|
||||||
console.log('Version:', api.version().toString(16));
|
|
||||||
const image = await loadImage('../example.png');
|
const image = await loadImage('../example.png');
|
||||||
const p = api.create_buffer(image.width, image.height);
|
const p = module.create_buffer(image.width, image.height);
|
||||||
Module.HEAP8.set(image.data, p);
|
module.HEAP8.set(image.data, p);
|
||||||
api.encode(p, image.width, image.height, 2);
|
module.encode(p, image.width, image.height, {
|
||||||
const resultPointer = api.get_result_pointer();
|
quality: 100,
|
||||||
const resultSize = api.get_result_size();
|
image_hint: 0,
|
||||||
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
|
target_size: 0,
|
||||||
|
target_PSNR: 0,
|
||||||
|
method: 6,
|
||||||
|
sns_strength: 50,
|
||||||
|
filter_strength: 60,
|
||||||
|
filter_sharpness: 0,
|
||||||
|
filter_type: 1,
|
||||||
|
partitions: 0,
|
||||||
|
segments: 4,
|
||||||
|
pass: 1,
|
||||||
|
show_compressed: 0,
|
||||||
|
preprocessing: 0,
|
||||||
|
autofilter: 0,
|
||||||
|
partition_limit: 0,
|
||||||
|
alpha_compression: 1,
|
||||||
|
alpha_filtering: 1,
|
||||||
|
alpha_quality: 100,
|
||||||
|
lossless: 1,
|
||||||
|
exact: 0,
|
||||||
|
image_hint: 0,
|
||||||
|
emulate_jpeg_size: 0,
|
||||||
|
thread_level: 0,
|
||||||
|
low_memory: 0,
|
||||||
|
near_lossless: 100,
|
||||||
|
use_delta_palette: 0,
|
||||||
|
use_sharp_yuv: 0,
|
||||||
|
});
|
||||||
|
const resultPointer = module.get_result_pointer();
|
||||||
|
const resultSize = module.get_result_size();
|
||||||
|
console.log('size', resultSize);
|
||||||
|
const resultView = new Uint8Array(module.HEAP8.buffer, resultPointer, resultSize);
|
||||||
const result = new Uint8Array(resultView);
|
const result = new Uint8Array(resultView);
|
||||||
api.free_result(resultPointer);
|
module.free_result();
|
||||||
api.destroy_buffer(p);
|
module.destroy_buffer(p);
|
||||||
|
|
||||||
const blob = new Blob([result], {type: 'image/webp'});
|
const blob = new Blob([result], {type: 'image/webp'});
|
||||||
const blobURL = URL.createObjectURL(blob);
|
const blobURL = URL.createObjectURL(blob);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "webp_enc",
|
"name": "webp_enc",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install": "napa",
|
"install": "napa",
|
||||||
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"cwrap\"]' -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME=\"webp_enc\"' -I node_modules/libwebp -o ./webp_enc.js webp_enc.c node_modules/libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c"
|
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten emcc --bind -O3 -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME=\"webp_enc\"' -I node_modules/libwebp -o ./webp_enc.js -x c node_modules/libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c -x c++ -std=c++11 webp_enc.cpp "
|
||||||
},
|
},
|
||||||
"napa": {
|
"napa": {
|
||||||
"libwebp": "webmproject/libwebp#v1.0.0"
|
"libwebp": "webmproject/libwebp#v1.0.0"
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
#include "emscripten.h"
|
|
||||||
#include "src/webp/encode.h"
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
int version() {
|
|
||||||
return WebPGetEncoderVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
uint8_t* create_buffer(int width, int height) {
|
|
||||||
return malloc(width * height * 4 * sizeof(uint8_t));
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
void destroy_buffer(uint8_t* p) {
|
|
||||||
free(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
int result[2];
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
void encode(uint8_t* img_in, int width, int height, float quality) {
|
|
||||||
uint8_t* img_out;
|
|
||||||
size_t size;
|
|
||||||
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
|
|
||||||
result[0] = (int)img_out;
|
|
||||||
result[1] = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
void free_result() {
|
|
||||||
WebPFree(result[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
int get_result_pointer() {
|
|
||||||
return result[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
int get_result_size() {
|
|
||||||
return result[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
106
codecs/webp_enc/webp_enc.cpp
Normal file
106
codecs/webp_enc/webp_enc.cpp
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#include "emscripten.h"
|
||||||
|
#include "src/webp/encode.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <emscripten/bind.h>
|
||||||
|
|
||||||
|
using namespace emscripten;
|
||||||
|
|
||||||
|
int version() {
|
||||||
|
return WebPGetEncoderVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
int create_buffer(int width, int height) {
|
||||||
|
return (int) malloc(width * height * 4 * sizeof(uint8_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
void destroy_buffer(int p) {
|
||||||
|
free((uint8_t*) p);
|
||||||
|
}
|
||||||
|
|
||||||
|
int result[2];
|
||||||
|
void encode(int img_in, int width, int height, WebPConfig config) {
|
||||||
|
// A lot of this is duplicated from Encode in picture_enc.c
|
||||||
|
WebPPicture pic;
|
||||||
|
WebPMemoryWriter wrt;
|
||||||
|
int ok;
|
||||||
|
|
||||||
|
if (!WebPPictureInit(&pic)) {
|
||||||
|
return; // shouldn't happen, except if system installation is broken
|
||||||
|
}
|
||||||
|
|
||||||
|
pic.use_argb = !!config.lossless;
|
||||||
|
pic.width = width;
|
||||||
|
pic.height = height;
|
||||||
|
pic.writer = WebPMemoryWrite;
|
||||||
|
pic.custom_ptr = &wrt;
|
||||||
|
|
||||||
|
WebPMemoryWriterInit(&wrt);
|
||||||
|
|
||||||
|
ok = WebPPictureImportRGBA(&pic, (uint8_t*) img_in, width * 4) && WebPEncode(&config, &pic);
|
||||||
|
WebPPictureFree(&pic);
|
||||||
|
if (!ok) {
|
||||||
|
WebPMemoryWriterClear(&wrt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result[0] = (int)wrt.mem;
|
||||||
|
result[1] = wrt.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void free_result() {
|
||||||
|
WebPFree((void*)result[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_result_pointer() {
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_result_size() {
|
||||||
|
return result[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
EMSCRIPTEN_BINDINGS(my_module) {
|
||||||
|
enum_<WebPImageHint>("WebPImageHint")
|
||||||
|
.value("WEBP_HINT_DEFAULT", WebPImageHint::WEBP_HINT_DEFAULT)
|
||||||
|
.value("WEBP_HINT_PICTURE", WebPImageHint::WEBP_HINT_PICTURE)
|
||||||
|
.value("WEBP_HINT_PHOTO", WebPImageHint::WEBP_HINT_PHOTO)
|
||||||
|
.value("WEBP_HINT_GRAPH", WebPImageHint::WEBP_HINT_GRAPH)
|
||||||
|
;
|
||||||
|
|
||||||
|
value_object<WebPConfig>("WebPConfig")
|
||||||
|
.field("lossless", &WebPConfig::lossless)
|
||||||
|
.field("quality", &WebPConfig::quality)
|
||||||
|
.field("method", &WebPConfig::method)
|
||||||
|
.field("image_hint", &WebPConfig::image_hint)
|
||||||
|
.field("target_size", &WebPConfig::target_size)
|
||||||
|
.field("target_PSNR", &WebPConfig::target_PSNR)
|
||||||
|
.field("segments", &WebPConfig::segments)
|
||||||
|
.field("sns_strength", &WebPConfig::sns_strength)
|
||||||
|
.field("filter_strength", &WebPConfig::filter_strength)
|
||||||
|
.field("filter_sharpness", &WebPConfig::filter_sharpness)
|
||||||
|
.field("filter_type", &WebPConfig::filter_type)
|
||||||
|
.field("autofilter", &WebPConfig::autofilter)
|
||||||
|
.field("alpha_compression", &WebPConfig::alpha_compression)
|
||||||
|
.field("alpha_filtering", &WebPConfig::alpha_filtering)
|
||||||
|
.field("alpha_quality", &WebPConfig::alpha_quality)
|
||||||
|
.field("pass", &WebPConfig::pass)
|
||||||
|
.field("show_compressed", &WebPConfig::show_compressed)
|
||||||
|
.field("preprocessing", &WebPConfig::preprocessing)
|
||||||
|
.field("partitions", &WebPConfig::partitions)
|
||||||
|
.field("partition_limit", &WebPConfig::partition_limit)
|
||||||
|
.field("emulate_jpeg_size", &WebPConfig::emulate_jpeg_size)
|
||||||
|
.field("thread_level", &WebPConfig::thread_level)
|
||||||
|
.field("low_memory", &WebPConfig::low_memory)
|
||||||
|
.field("near_lossless", &WebPConfig::near_lossless)
|
||||||
|
.field("exact", &WebPConfig::exact)
|
||||||
|
.field("use_delta_palette", &WebPConfig::use_delta_palette)
|
||||||
|
.field("use_sharp_yuv", &WebPConfig::use_sharp_yuv)
|
||||||
|
;
|
||||||
|
|
||||||
|
function("version", &version);
|
||||||
|
function("create_buffer", &create_buffer, allow_raw_pointers());
|
||||||
|
function("destroy_buffer", &destroy_buffer, allow_raw_pointers());
|
||||||
|
function("encode", &encode, allow_raw_pointers());
|
||||||
|
function("free_result", &free_result);
|
||||||
|
function("get_result_pointer", &get_result_pointer, allow_raw_pointers());
|
||||||
|
function("get_result_size", &get_result_size, allow_raw_pointers());
|
||||||
|
}
|
||||||
14
codecs/webp_enc/webp_enc.d.ts
vendored
14
codecs/webp_enc/webp_enc.d.ts
vendored
@@ -1 +1,13 @@
|
|||||||
export default function(opts: EmscriptenWasm.ModuleOpts): EmscriptenWasm.Module;
|
import { EncodeOptions } from '../../src/codecs/webp/encoder';
|
||||||
|
|
||||||
|
interface WebPModule extends EmscriptenWasm.Module {
|
||||||
|
create_buffer(width: number, height: number): number;
|
||||||
|
encode(pointer: number, width: number, height: number, options: EncodeOptions): void;
|
||||||
|
get_result_pointer(): number;
|
||||||
|
get_result_size(): number;
|
||||||
|
free_result(): void;
|
||||||
|
destroy_buffer(pointer: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function(opts: EmscriptenWasm.ModuleOpts): WebPModule;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,22 +1,10 @@
|
|||||||
import webp_enc from '../../../codecs/webp_enc/webp_enc';
|
import webp_enc, { WebPModule } from '../../../codecs/webp_enc/webp_enc';
|
||||||
// Using require() so TypeScript doesn’t complain about this not being a module.
|
// Using require() so TypeScript doesn’t complain about this not being a module.
|
||||||
import { EncodeOptions } from './encoder';
|
import { EncodeOptions } from './encoder';
|
||||||
const wasmBinaryUrl = require('../../../codecs/webp_enc/webp_enc.wasm');
|
const wasmBinaryUrl = require('../../../codecs/webp_enc/webp_enc.wasm');
|
||||||
|
|
||||||
// API exposed by wasm module. Details in the codec’s README.
|
|
||||||
interface ModuleAPI {
|
|
||||||
version(): number;
|
|
||||||
create_buffer(width: number, height: number): number;
|
|
||||||
destroy_buffer(pointer: number): void;
|
|
||||||
encode(buffer: number, width: number, height: number, quality: number): void;
|
|
||||||
free_result(): void;
|
|
||||||
get_result_pointer(): number;
|
|
||||||
get_result_size(): number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class WebPEncoder {
|
export default class WebPEncoder {
|
||||||
private emscriptenModule: Promise<EmscriptenWasm.Module>;
|
private emscriptenModule: Promise<WebPModule>;
|
||||||
private api: Promise<ModuleAPI>;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.emscriptenModule = new Promise((resolve) => {
|
this.emscriptenModule = new Promise((resolve) => {
|
||||||
@@ -41,38 +29,20 @@ export default class WebPEncoder {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.api = (async () => {
|
|
||||||
// Not sure why, but TypeScript complains that I am using
|
|
||||||
// `emscriptenModule` before it’s getting assigned, which is clearly not
|
|
||||||
// true :shrug: Using `any`
|
|
||||||
const module = await (this as any).emscriptenModule as EmscriptenWasm.Module;
|
|
||||||
|
|
||||||
return {
|
|
||||||
version: module.cwrap('version', 'number', []),
|
|
||||||
create_buffer: module.cwrap('create_buffer', 'number', ['number', 'number']),
|
|
||||||
destroy_buffer: module.cwrap('destroy_buffer', '', ['number']),
|
|
||||||
encode: module.cwrap('encode', '', ['number', 'number', 'number', 'number']),
|
|
||||||
free_result: module.cwrap('free_result', '', []),
|
|
||||||
get_result_pointer: module.cwrap('get_result_pointer', 'number', []),
|
|
||||||
get_result_size: module.cwrap('get_result_size', 'number', []),
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> {
|
async encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> {
|
||||||
const m = await this.emscriptenModule;
|
const module = await this.emscriptenModule;
|
||||||
const api = await this.api;
|
|
||||||
|
|
||||||
const p = api.create_buffer(data.width, data.height);
|
const p = module.create_buffer(data.width, data.height);
|
||||||
m.HEAP8.set(data.data, p);
|
module.HEAP8.set(data.data, p);
|
||||||
api.encode(p, data.width, data.height, options.quality);
|
module.encode(p, data.width, data.height, options);
|
||||||
const resultPointer = api.get_result_pointer();
|
const resultPointer = module.get_result_pointer();
|
||||||
const resultSize = api.get_result_size();
|
const resultSize = module.get_result_size();
|
||||||
const resultView = new Uint8Array(m.HEAP8.buffer, resultPointer, resultSize);
|
const resultView = new Uint8Array(module.HEAP8.buffer, resultPointer, resultSize);
|
||||||
const result = new Uint8Array(resultView);
|
const result = new Uint8Array(resultView);
|
||||||
api.free_result();
|
module.free_result();
|
||||||
api.destroy_buffer(p);
|
module.destroy_buffer(p);
|
||||||
|
|
||||||
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||||
return result.buffer as ArrayBuffer;
|
return result.buffer as ArrayBuffer;
|
||||||
|
|||||||
@@ -1,13 +1,77 @@
|
|||||||
import EncoderWorker from './Encoder.worker';
|
import EncoderWorker from './Encoder.worker';
|
||||||
|
|
||||||
export interface EncodeOptions { quality: number; }
|
export enum WebPImageHint {
|
||||||
|
WEBP_HINT_DEFAULT, // default preset.
|
||||||
|
WEBP_HINT_PICTURE, // digital picture, like portrait, inner shot
|
||||||
|
WEBP_HINT_PHOTO, // outdoor photograph, with natural lighting
|
||||||
|
WEBP_HINT_GRAPH, // Discrete tone image (graph, map-tile etc).
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EncodeOptions {
|
||||||
|
quality: number;
|
||||||
|
target_size: number;
|
||||||
|
target_PSNR: number;
|
||||||
|
method: number;
|
||||||
|
sns_strength: number;
|
||||||
|
filter_strength: number;
|
||||||
|
filter_sharpness: number;
|
||||||
|
filter_type: number;
|
||||||
|
partitions: number;
|
||||||
|
segments: number;
|
||||||
|
pass: number;
|
||||||
|
show_compressed: number;
|
||||||
|
preprocessing: number;
|
||||||
|
autofilter: number;
|
||||||
|
partition_limit: number;
|
||||||
|
alpha_compression: number;
|
||||||
|
alpha_filtering: number;
|
||||||
|
alpha_quality: number;
|
||||||
|
lossless: number;
|
||||||
|
exact: number;
|
||||||
|
image_hint: number;
|
||||||
|
emulate_jpeg_size: number;
|
||||||
|
thread_level: number;
|
||||||
|
low_memory: number;
|
||||||
|
near_lossless: number;
|
||||||
|
use_delta_palette: number;
|
||||||
|
use_sharp_yuv: number;
|
||||||
|
}
|
||||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||||
|
|
||||||
export const type = 'webp';
|
export const type = 'webp';
|
||||||
export const label = 'WebP';
|
export const label = 'WebP';
|
||||||
export const mimeType = 'image/webp';
|
export const mimeType = 'image/webp';
|
||||||
export const extension = 'webp';
|
export const extension = 'webp';
|
||||||
export const defaultOptions: EncodeOptions = { quality: 7 };
|
// These come from struct WebPConfig in encode.h.
|
||||||
|
export const defaultOptions: EncodeOptions = {
|
||||||
|
quality: 75,
|
||||||
|
target_size: 0,
|
||||||
|
target_PSNR: 0,
|
||||||
|
method: 4,
|
||||||
|
sns_strength: 50,
|
||||||
|
filter_strength: 60,
|
||||||
|
filter_sharpness: 0,
|
||||||
|
filter_type: 1,
|
||||||
|
partitions: 0,
|
||||||
|
segments: 4,
|
||||||
|
pass: 1,
|
||||||
|
show_compressed: 0,
|
||||||
|
preprocessing: 0,
|
||||||
|
autofilter: 0,
|
||||||
|
partition_limit: 0,
|
||||||
|
alpha_compression: 1,
|
||||||
|
alpha_filtering: 1,
|
||||||
|
alpha_quality: 100,
|
||||||
|
lossless: 0,
|
||||||
|
exact: 0,
|
||||||
|
image_hint: 0,
|
||||||
|
emulate_jpeg_size: 0,
|
||||||
|
thread_level: 0,
|
||||||
|
low_memory: 0,
|
||||||
|
near_lossless: 100,
|
||||||
|
use_delta_palette: 0,
|
||||||
|
use_sharp_yuv: 0,
|
||||||
|
};
|
||||||
|
|
||||||
export async function encode(data: ImageData, options: EncodeOptions) {
|
export async function encode(data: ImageData, options: EncodeOptions) {
|
||||||
// We need to await this because it's been comlinked.
|
// We need to await this because it's been comlinked.
|
||||||
|
|||||||
@@ -1,3 +1,331 @@
|
|||||||
import qualityOption from '../generic/quality-option';
|
import { h, Component } from 'preact';
|
||||||
|
import { bind } from '../../lib/util';
|
||||||
|
import { EncodeOptions, WebPImageHint } from './encoder';
|
||||||
|
import * as styles from './styles.scss';
|
||||||
|
|
||||||
export default qualityOption();
|
type Props = {
|
||||||
|
options: EncodeOptions,
|
||||||
|
onChange(newOptions: EncodeOptions): void,
|
||||||
|
};
|
||||||
|
|
||||||
|
// From kLosslessPresets in config_enc.c
|
||||||
|
// The format is [method, quality].
|
||||||
|
const losslessPresets:[number, number][] = [
|
||||||
|
[0, 0], [1, 20], [2, 25], [3, 30], [3, 50],
|
||||||
|
[4, 50], [4, 75], [4, 90], [5, 90], [6, 100],
|
||||||
|
];
|
||||||
|
const losslessPresetDefault = 6;
|
||||||
|
|
||||||
|
function determineLosslessQuality(quality: number): number {
|
||||||
|
const index = losslessPresets.findIndex(item => item[1] === quality);
|
||||||
|
if (index !== -1) return index;
|
||||||
|
// Quality doesn't match one of the presets.
|
||||||
|
// This can happen when toggling 'lossless'.
|
||||||
|
return losslessPresetDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||||
|
*/
|
||||||
|
function fieldCheckedAsNumber(field: any): number {
|
||||||
|
return Number((field as HTMLInputElement).checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||||
|
*/
|
||||||
|
function fieldValueAsNumber(field: any): number {
|
||||||
|
return Number((field as HTMLInputElement).value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class WebPEncoderOptions extends Component<Props, {}> {
|
||||||
|
@bind
|
||||||
|
onChange(event: Event) {
|
||||||
|
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
|
||||||
|
const lossless = fieldCheckedAsNumber(form.lossless);
|
||||||
|
const losslessPresetInput = (form.lossless_preset as HTMLInputElement);
|
||||||
|
|
||||||
|
const options: EncodeOptions = {
|
||||||
|
// Copy over options the form doesn't care about, eg emulate_jpeg_size
|
||||||
|
...this.props.options,
|
||||||
|
// And now stuff from the form:
|
||||||
|
lossless,
|
||||||
|
// Special-cased inputs:
|
||||||
|
// In lossless mode, the quality is derived from the preset.
|
||||||
|
quality: lossless ?
|
||||||
|
losslessPresets[Number(losslessPresetInput.value)][1] :
|
||||||
|
fieldValueAsNumber(form.quality),
|
||||||
|
// In lossless mode, the method is derived from the preset.
|
||||||
|
method: lossless ?
|
||||||
|
losslessPresets[Number(losslessPresetInput.value)][0] :
|
||||||
|
fieldValueAsNumber(form.method_input),
|
||||||
|
image_hint: (form.image_hint as HTMLInputElement).checked ?
|
||||||
|
WebPImageHint.WEBP_HINT_GRAPH :
|
||||||
|
WebPImageHint.WEBP_HINT_DEFAULT,
|
||||||
|
// .checked
|
||||||
|
exact: fieldCheckedAsNumber(form.exact),
|
||||||
|
alpha_compression: fieldCheckedAsNumber(form.alpha_compression),
|
||||||
|
autofilter: fieldCheckedAsNumber(form.autofilter),
|
||||||
|
filter_type: fieldCheckedAsNumber(form.filter_type),
|
||||||
|
use_sharp_yuv: fieldCheckedAsNumber(form.use_sharp_yuv),
|
||||||
|
// .value
|
||||||
|
near_lossless: fieldValueAsNumber(form.near_lossless),
|
||||||
|
alpha_quality: fieldValueAsNumber(form.alpha_quality),
|
||||||
|
alpha_filtering: fieldValueAsNumber(form.alpha_filtering),
|
||||||
|
sns_strength: fieldValueAsNumber(form.sns_strength),
|
||||||
|
filter_strength: fieldValueAsNumber(form.filter_strength),
|
||||||
|
filter_sharpness: fieldValueAsNumber(form.filter_sharpness),
|
||||||
|
pass: fieldValueAsNumber(form.pass),
|
||||||
|
preprocessing: fieldValueAsNumber(form.preprocessing),
|
||||||
|
segments: fieldValueAsNumber(form.segments),
|
||||||
|
partitions: fieldValueAsNumber(form.partitions),
|
||||||
|
};
|
||||||
|
this.props.onChange(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _losslessSpecificOptions(options: EncodeOptions) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Effort:
|
||||||
|
<input
|
||||||
|
name="lossless_preset"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="9"
|
||||||
|
value={'' + determineLosslessQuality(options.quality)}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Slight loss:
|
||||||
|
<input
|
||||||
|
class={styles.flipRange}
|
||||||
|
name="near_lossless"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={'' + options.near_lossless}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
{/*
|
||||||
|
Although there are 3 different kinds of image hint, webp only
|
||||||
|
seems to do something with the 'graph' type, and I don't really
|
||||||
|
understand what it does.
|
||||||
|
*/}
|
||||||
|
<input
|
||||||
|
name="image_hint"
|
||||||
|
type="checkbox"
|
||||||
|
checked={options.image_hint === WebPImageHint.WEBP_HINT_GRAPH}
|
||||||
|
value={'' + WebPImageHint.WEBP_HINT_GRAPH}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Discrete tone image (graph, map-tile etc)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _lossySpecificOptions(options: EncodeOptions) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Effort:
|
||||||
|
<input
|
||||||
|
name="method_input"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="6"
|
||||||
|
value={'' + options.method}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Quality:
|
||||||
|
<input
|
||||||
|
name="quality"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="0.01"
|
||||||
|
value={'' + options.quality}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
name="alpha_compression"
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!options.alpha_compression}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Compress alpha
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Alpha quality:
|
||||||
|
<input
|
||||||
|
name="alpha_quality"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={'' + options.alpha_quality}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Alpha filter quality:
|
||||||
|
<input
|
||||||
|
name="alpha_filtering"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="2"
|
||||||
|
value={'' + options.alpha_filtering}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Spacial noise shaping:
|
||||||
|
<input
|
||||||
|
name="sns_strength"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={'' + options.sns_strength}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
name="autofilter"
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!options.autofilter}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Auto adjust filter strength
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Filter strength:
|
||||||
|
<input
|
||||||
|
name="filter_strength"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
disabled={!!options.autofilter}
|
||||||
|
value={'' + options.filter_strength}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
name="filter_type"
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!options.filter_type}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Strong filter
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Filter sharpness:
|
||||||
|
<input
|
||||||
|
class={styles.flipRange}
|
||||||
|
name="filter_sharpness"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="7"
|
||||||
|
value={'' + options.filter_sharpness}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
name="use_sharp_yuv"
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!options.use_sharp_yuv}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Sharp RGB->YUV conversion
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Passes:
|
||||||
|
<input
|
||||||
|
name="pass"
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="10"
|
||||||
|
value={'' + options.pass}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Preprocessing type:
|
||||||
|
<select
|
||||||
|
name="preprocessing"
|
||||||
|
value={'' + options.preprocessing}
|
||||||
|
onChange={this.onChange}
|
||||||
|
>
|
||||||
|
<option value="0">None</option>
|
||||||
|
<option value="1">Segment smooth</option>
|
||||||
|
<option value="2">Pseudo-random dithering</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Segments:
|
||||||
|
<input
|
||||||
|
name="segments"
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="4"
|
||||||
|
value={'' + options.segments}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Partitions:
|
||||||
|
<input
|
||||||
|
name="partitions"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="3"
|
||||||
|
value={'' + options.partitions}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render({ options }: Props) {
|
||||||
|
// I'm rendering both lossy and lossless forms, as it becomes much easier when
|
||||||
|
// gathering the data.
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
name="lossless"
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!options.lossless}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Lossless
|
||||||
|
</label>
|
||||||
|
<div class={options.lossless ? '' : styles.hide}>
|
||||||
|
{this._losslessSpecificOptions(options)}
|
||||||
|
</div>
|
||||||
|
<div class={options.lossless ? styles.hide : ''}>
|
||||||
|
{this._lossySpecificOptions(options)}
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
name="exact"
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!options.exact}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Preserve transparent data. Otherwise, pixels with zero alpha will have RGB also zeroed.
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
6
src/codecs/webp/styles.scss
Normal file
6
src/codecs/webp/styles.scss
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.flip-range {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -21,7 +21,8 @@ module.exports = function (_, env) {
|
|||||||
const isProd = env.mode === 'production';
|
const isProd = env.mode === 'production';
|
||||||
const nodeModules = path.join(__dirname, 'node_modules');
|
const nodeModules = path.join(__dirname, 'node_modules');
|
||||||
const componentStyleDirs = [
|
const componentStyleDirs = [
|
||||||
path.join(__dirname, 'src/components')
|
path.join(__dirname, 'src/components'),
|
||||||
|
path.join(__dirname, 'src/codecs')
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user