Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
26e9a848dd | |||
fbaa282f07 | |||
6136ae7411 | |||
f024747299 | |||
66feffcc49 |
5
codecs/hqx/.gitignore
vendored
Normal file
5
codecs/hqx/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
**/*.rs.bk
|
||||
target
|
||||
Cargo.lock
|
||||
bin/
|
||||
pkg/README.md
|
37
codecs/hqx/Cargo.toml
Normal file
37
codecs/hqx/Cargo.toml
Normal file
@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "squooshhqx"
|
||||
version = "0.1.0"
|
||||
authors = ["Surma <surma@surma.link>"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook", "wee_alloc"]
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "0.1.2"
|
||||
wasm-bindgen = "0.2.38"
|
||||
# lazy_static = "1.0.0"
|
||||
hqx = {git = "https://github.com/CryZe/wasmboy-rs", tag="v0.1.2"}
|
||||
|
||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||
# logging them with `console.error`. This is great for development, but requires
|
||||
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||
# code size when deploying.
|
||||
console_error_panic_hook = { version = "0.1.1", optional = true }
|
||||
|
||||
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
|
||||
# compared to the default allocator's ~10K. It is slower than the default
|
||||
# allocator, however.
|
||||
#
|
||||
# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now.
|
||||
wee_alloc = { version = "0.4.2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.2"
|
||||
|
||||
[profile.release]
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
opt-level = "s"
|
||||
lto = true
|
23
codecs/hqx/Dockerfile
Normal file
23
codecs/hqx/Dockerfile
Normal file
@ -0,0 +1,23 @@
|
||||
FROM ubuntu
|
||||
RUN apt-get update && \
|
||||
apt-get install -qqy git build-essential cmake python2.7
|
||||
RUN git clone --recursive https://github.com/WebAssembly/wabt /usr/src/wabt
|
||||
RUN mkdir -p /usr/src/wabt/build
|
||||
WORKDIR /usr/src/wabt/build
|
||||
RUN cmake .. -DCMAKE_INSTALL_PREFIX=/opt/wabt && \
|
||||
make && \
|
||||
make install
|
||||
|
||||
FROM rust
|
||||
RUN rustup install nightly && \
|
||||
rustup target add --toolchain nightly wasm32-unknown-unknown && \
|
||||
cargo install wasm-pack
|
||||
|
||||
COPY --from=0 /opt/wabt /opt/wabt
|
||||
|
||||
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:/opt/wabt/bin:${PATH}"
|
||||
|
||||
WORKDIR /src
|
25
codecs/hqx/build.sh
Executable file
25
codecs/hqx/build.sh
Executable file
@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling wasm"
|
||||
echo "============================================="
|
||||
(
|
||||
rustup run nightly \
|
||||
wasm-pack build --target no-modules
|
||||
mv pkg/squooshhqx_bg.wasm pkg/squooshhqx_bg.unopt.wasm
|
||||
wasm-opt -Os --no-validation pkg/squooshhqx_bg.unopt.wasm -o pkg/squooshhqx_bg.wasm
|
||||
wasm-strip pkg/squooshhqx_bg.wasm
|
||||
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 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
24
codecs/hqx/index.html
Normal file
24
codecs/hqx/index.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!doctype html>
|
||||
<script src ="./pkg/squooshhqx.js"></script>
|
||||
<script type="module">
|
||||
async function run() {
|
||||
await wasm_bindgen("./pkg/squooshhqx_bg.wasm");
|
||||
const bitmap = await createImageBitmap(await fetch("https://i.imgur.com/MNDnBSc.png").then(r => r.blob()));
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = bitmap.width;
|
||||
canvas.height = bitmap.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(bitmap, 0, 0);
|
||||
const imgdata = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
||||
const factor = 4;
|
||||
const r = wasm_bindgen.resize(new Uint32Array(imgdata.data.buffer), bitmap.width, bitmap.height, factor);
|
||||
|
||||
canvas.width = bitmap.width * factor;
|
||||
canvas.height = bitmap.height * factor;
|
||||
const output = new ImageData(new Uint8ClampedArray(r.buffer), canvas.width, canvas.height);
|
||||
ctx.putImageData(output, 0, 0);
|
||||
canvas.style = `width: ${canvas.width}px; height: ${canvas.height}px; image-rendering: pixelated;`;
|
||||
document.body.append(canvas);
|
||||
}
|
||||
run();
|
||||
</script>
|
11
codecs/hqx/loading_benchmark.js
Normal file
11
codecs/hqx/loading_benchmark.js
Normal file
@ -0,0 +1,11 @@
|
||||
// THIS IS NOT A NODE SCRIPT
|
||||
// This is a d8 script. Please install jsvu[1] and install v8.
|
||||
// Then run `npm run --silent benchmark`.
|
||||
// [1]: https://github.com/GoogleChromeLabs/jsvu
|
||||
|
||||
async function init() {
|
||||
const start = Date.now();
|
||||
const module = await WebAssembly.compile(readbuffer("pkg/squooshhqx_bg.wasm"));
|
||||
print(`${Date.now()/1000 - start/1000}`);
|
||||
}
|
||||
init().catch(e => console.error(e.stack));
|
4
codecs/hqx/package-lock.json
generated
Normal file
4
codecs/hqx/package-lock.json
generated
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "hqx",
|
||||
"lockfileVersion": 1
|
||||
}
|
7
codecs/hqx/package.json
Normal file
7
codecs/hqx/package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "hqx",
|
||||
"scripts": {
|
||||
"build:image": "docker build -t squoosh-hqx .",
|
||||
"build": "docker run --rm -v $(pwd):/src squoosh-hqx ./build.sh"
|
||||
}
|
||||
}
|
14
codecs/hqx/pkg/package.json
Normal file
14
codecs/hqx/pkg/package.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "squooshhqx",
|
||||
"collaborators": [
|
||||
"Surma <surma@surma.link>"
|
||||
],
|
||||
"version": "0.1.0",
|
||||
"files": [
|
||||
"squooshhqx_bg.wasm",
|
||||
"squooshhqx.js",
|
||||
"squooshhqx.d.ts"
|
||||
],
|
||||
"browser": "squooshhqx.js",
|
||||
"types": "squooshhqx.d.ts"
|
||||
}
|
20
codecs/hqx/pkg/squooshhqx.d.ts
vendored
Normal file
20
codecs/hqx/pkg/squooshhqx.d.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* @param {Uint32Array} input_image
|
||||
* @param {number} input_width
|
||||
* @param {number} input_height
|
||||
* @param {number} factor
|
||||
* @returns {Uint32Array}
|
||||
*/
|
||||
export function resize(input_image: Uint32Array, input_width: number, input_height: number, factor: number): Uint32Array;
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {RequestInfo | BufferSource | WebAssembly.Module} module_or_path
|
||||
*
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export default function init (module_or_path: RequestInfo | BufferSource | WebAssembly.Module): Promise<any>;
|
||||
|
97
codecs/hqx/pkg/squooshhqx.js
Normal file
97
codecs/hqx/pkg/squooshhqx.js
Normal file
@ -0,0 +1,97 @@
|
||||
(function() {
|
||||
const __exports = {};
|
||||
let wasm;
|
||||
|
||||
let cachegetUint32Memory = null;
|
||||
function getUint32Memory() {
|
||||
if (cachegetUint32Memory === null || cachegetUint32Memory.buffer !== wasm.memory.buffer) {
|
||||
cachegetUint32Memory = new Uint32Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachegetUint32Memory;
|
||||
}
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
function passArray32ToWasm(arg) {
|
||||
const ptr = wasm.__wbindgen_malloc(arg.length * 4);
|
||||
getUint32Memory().set(arg, ptr / 4);
|
||||
WASM_VECTOR_LEN = arg.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function getArrayU32FromWasm(ptr, len) {
|
||||
return getUint32Memory().subarray(ptr / 4, ptr / 4 + len);
|
||||
}
|
||||
|
||||
let cachedGlobalArgumentPtr = null;
|
||||
function globalArgumentPtr() {
|
||||
if (cachedGlobalArgumentPtr === null) {
|
||||
cachedGlobalArgumentPtr = wasm.__wbindgen_global_argument_ptr();
|
||||
}
|
||||
return cachedGlobalArgumentPtr;
|
||||
}
|
||||
/**
|
||||
* @param {Uint32Array} input_image
|
||||
* @param {number} input_width
|
||||
* @param {number} input_height
|
||||
* @param {number} factor
|
||||
* @returns {Uint32Array}
|
||||
*/
|
||||
__exports.resize = function(input_image, input_width, input_height, factor) {
|
||||
const ptr0 = passArray32ToWasm(input_image);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const retptr = globalArgumentPtr();
|
||||
wasm.resize(retptr, ptr0, len0, input_width, input_height, factor);
|
||||
const mem = getUint32Memory();
|
||||
const rustptr = mem[retptr / 4];
|
||||
const rustlen = mem[retptr / 4 + 1];
|
||||
|
||||
const realRet = getArrayU32FromWasm(rustptr, rustlen).slice();
|
||||
wasm.__wbindgen_free(rustptr, rustlen * 4);
|
||||
return realRet;
|
||||
|
||||
};
|
||||
|
||||
function init(module) {
|
||||
|
||||
let result;
|
||||
const imports = {};
|
||||
|
||||
if (module instanceof URL || typeof module === 'string' || module instanceof Request) {
|
||||
|
||||
const response = fetch(module);
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
result = WebAssembly.instantiateStreaming(response, imports)
|
||||
.catch(e => {
|
||||
console.warn("`WebAssembly.instantiateStreaming` failed. Assuming this is because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||
return response
|
||||
.then(r => r.arrayBuffer())
|
||||
.then(bytes => WebAssembly.instantiate(bytes, imports));
|
||||
});
|
||||
} else {
|
||||
result = response
|
||||
.then(r => r.arrayBuffer())
|
||||
.then(bytes => WebAssembly.instantiate(bytes, imports));
|
||||
}
|
||||
} else {
|
||||
|
||||
result = WebAssembly.instantiate(module, imports)
|
||||
.then(result => {
|
||||
if (result instanceof WebAssembly.Instance) {
|
||||
return { instance: result, module };
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
return result.then(({instance, module}) => {
|
||||
wasm = instance.exports;
|
||||
init.__wbindgen_wasm_module = module;
|
||||
|
||||
return wasm;
|
||||
});
|
||||
}
|
||||
|
||||
self.wasm_bindgen = Object.assign(init, __exports);
|
||||
|
||||
})();
|
6
codecs/hqx/pkg/squooshhqx_bg.d.ts
vendored
Normal file
6
codecs/hqx/pkg/squooshhqx_bg.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/* tslint:disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export function resize(a: number, b: number, c: number, d: number, e: number, f: number): void;
|
||||
export function __wbindgen_global_argument_ptr(): number;
|
||||
export function __wbindgen_malloc(a: number): number;
|
||||
export function __wbindgen_free(a: number, b: number): void;
|
BIN
codecs/hqx/pkg/squooshhqx_bg.wasm
Normal file
BIN
codecs/hqx/pkg/squooshhqx_bg.wasm
Normal file
Binary file not shown.
55
codecs/hqx/src/lib.rs
Normal file
55
codecs/hqx/src/lib.rs
Normal file
@ -0,0 +1,55 @@
|
||||
extern crate cfg_if;
|
||||
extern crate hqx;
|
||||
extern crate wasm_bindgen;
|
||||
|
||||
mod utils;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
cfg_if! {
|
||||
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
|
||||
// allocator.
|
||||
if #[cfg(feature = "wee_alloc")] {
|
||||
extern crate wee_alloc;
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[no_mangle]
|
||||
pub fn resize(
|
||||
input_image: Vec<u32>,
|
||||
input_width: usize,
|
||||
input_height: usize,
|
||||
factor: usize,
|
||||
) -> Vec<u32> {
|
||||
let num_output_pixels = input_width * input_height * factor * factor;
|
||||
let mut output_image = Vec::<u32>::with_capacity(num_output_pixels * 4);
|
||||
output_image.resize(num_output_pixels, 0);
|
||||
|
||||
match factor {
|
||||
2 => hqx::hq2x(
|
||||
input_image.as_slice(),
|
||||
output_image.as_mut_slice(),
|
||||
input_width,
|
||||
input_height,
|
||||
),
|
||||
3 => hqx::hq3x(
|
||||
input_image.as_slice(),
|
||||
output_image.as_mut_slice(),
|
||||
input_width,
|
||||
input_height,
|
||||
),
|
||||
4 => hqx::hq4x(
|
||||
input_image.as_slice(),
|
||||
output_image.as_mut_slice(),
|
||||
input_width,
|
||||
input_height,
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
return output_image;
|
||||
}
|
17
codecs/hqx/src/utils.rs
Normal file
17
codecs/hqx/src/utils.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
// When the `console_error_panic_hook` feature is enabled, we can call the
|
||||
// `set_panic_hook` function at least once during initialization, and then
|
||||
// we will get better error messages if our code ever panics.
|
||||
//
|
||||
// For more details see
|
||||
// https://github.com/rustwasm/console_error_panic_hook#readme
|
||||
if #[cfg(feature = "console_error_panic_hook")] {
|
||||
extern crate console_error_panic_hook;
|
||||
pub use self::console_error_panic_hook::set_once as set_panic_hook;
|
||||
} else {
|
||||
#[inline]
|
||||
pub fn set_panic_hook() {}
|
||||
}
|
||||
}
|
49
package-lock.json
generated
49
package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "squoosh",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@ -5465,7 +5465,8 @@
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@ -5486,12 +5487,14 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@ -5506,17 +5509,20 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@ -5633,7 +5639,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@ -5645,6 +5652,7 @@
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@ -5659,6 +5667,7 @@
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@ -5666,12 +5675,14 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@ -5690,6 +5701,7 @@
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@ -5770,7 +5782,8 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@ -5782,6 +5795,7 @@
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@ -5867,7 +5881,8 @@
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@ -5903,6 +5918,7 @@
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@ -5922,6 +5938,7 @@
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@ -5965,12 +5982,14 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -14164,9 +14183,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"ts-loader": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.0.2.tgz",
|
||||
"integrity": "sha512-kkF3sGf3oBUehlvXI9fkbItbFTnNgGkYAz91vtWnsKAU4m+LAmQjuby7uTZNo3As+/zHLuyB052SkQDY6vLXtg==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.0.3.tgz",
|
||||
"integrity": "sha512-iICBD4PryhnGNdtaDva49UGODHFVcuK7p4+G8CP1TVcUhTC4hkcy4MC2dzWwALSnpOzfUfA/4u8B2F64wsMgjQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.3.0",
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "squoosh",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"license": "apache-2.0",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --host 0.0.0.0 --hot",
|
||||
@ -59,7 +59,7 @@
|
||||
"style-loader": "0.23.1",
|
||||
"terser-webpack-plugin": "1.3.0",
|
||||
"travis-size-report": "1.0.1",
|
||||
"ts-loader": "6.0.2",
|
||||
"ts-loader": "6.0.3",
|
||||
"tslint": "5.17.0",
|
||||
"tslint-config-airbnb": "5.11.1",
|
||||
"tslint-config-semistandard": "8.0.1",
|
||||
|
3
src/codecs/hqx/processor-meta.ts
Normal file
3
src/codecs/hqx/processor-meta.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface HqxOptions {
|
||||
factor: 2 | 3 | 4;
|
||||
}
|
32
src/codecs/hqx/processor.ts
Normal file
32
src/codecs/hqx/processor.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import wasmUrl from '../../../codecs/hqx/pkg/squooshhqx_bg.wasm';
|
||||
import '../../../codecs/hqx/pkg/squooshhqx';
|
||||
import { HqxOptions } from './processor-meta';
|
||||
|
||||
interface WasmBindgenExports {
|
||||
resize: typeof import('../../../codecs/hqx/pkg/squooshhqx').resize;
|
||||
}
|
||||
|
||||
type WasmBindgen = ((url: string) => Promise<void>) & WasmBindgenExports;
|
||||
|
||||
declare var wasm_bindgen: WasmBindgen;
|
||||
|
||||
const ready = wasm_bindgen(wasmUrl);
|
||||
|
||||
export async function hqx(
|
||||
data: ImageData,
|
||||
opts: HqxOptions,
|
||||
): Promise<ImageData> {
|
||||
const input = data;
|
||||
await ready;
|
||||
const result = wasm_bindgen.resize(
|
||||
new Uint32Array(input.data.buffer),
|
||||
input.width,
|
||||
input.height,
|
||||
opts.factor,
|
||||
);
|
||||
return new ImageData(
|
||||
new Uint8ClampedArray(result.buffer),
|
||||
data.width * opts.factor,
|
||||
data.height * opts.factor,
|
||||
);
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
import { expose } from 'comlink';
|
||||
import { isHqx } from '../resize/processor-meta';
|
||||
import { clamp } from '../util';
|
||||
|
||||
async function mozjpegEncode(
|
||||
data: ImageData, options: import('../mozjpeg/encoder-meta').EncodeOptions,
|
||||
@ -31,6 +33,18 @@ async function rotate(
|
||||
async function resize(
|
||||
data: ImageData, opts: import('../resize/processor-meta').WorkerResizeOptions,
|
||||
): Promise<ImageData> {
|
||||
if (isHqx(opts)) {
|
||||
const { hqx } = await import(
|
||||
/* webpackChunkName: "process-hqx" */
|
||||
'../hqx/processor');
|
||||
|
||||
const widthRatio = opts.width / data.width;
|
||||
const heightRatio = opts.height / data.height;
|
||||
const ratio = Math.max(widthRatio, heightRatio);
|
||||
if (ratio <= 1) return data;
|
||||
const factor = clamp(Math.ceil(ratio), { min: 2, max: 4 }) as 2|3|4;
|
||||
return hqx(data, { factor });
|
||||
}
|
||||
const { resize } = await import(
|
||||
/* webpackChunkName: "process-resize" */
|
||||
'../resize/processor');
|
||||
@ -63,7 +77,15 @@ async function webpDecode(data: ArrayBuffer): Promise<ImageData> {
|
||||
return decode(data);
|
||||
}
|
||||
|
||||
const exports = { mozjpegEncode, quantize, rotate, resize, optiPngEncode, webpEncode, webpDecode };
|
||||
const exports = {
|
||||
mozjpegEncode,
|
||||
quantize,
|
||||
rotate,
|
||||
resize,
|
||||
optiPngEncode,
|
||||
webpEncode,
|
||||
webpDecode,
|
||||
};
|
||||
export type ProcessorWorkerApi = typeof exports;
|
||||
|
||||
expose(exports, self);
|
||||
|
@ -16,6 +16,7 @@ import * as browserGIF from './browser-gif/encoder';
|
||||
import * as browserTIFF from './browser-tiff/encoder';
|
||||
import * as browserJP2 from './browser-jp2/encoder';
|
||||
import * as browserPDF from './browser-pdf/encoder';
|
||||
import { bind } from '../lib/initial-util';
|
||||
|
||||
type ProcessorWorkerApi = import('./processor-worker').ProcessorWorkerApi;
|
||||
|
||||
@ -94,14 +95,7 @@ export default class Processor {
|
||||
if (!this._worker) return;
|
||||
|
||||
// If the worker is unused for 10 seconds, remove it to save memory.
|
||||
this._workerTimeoutId = self.setTimeout(
|
||||
() => {
|
||||
if (!this._worker) return;
|
||||
this._worker.terminate();
|
||||
this._worker = undefined;
|
||||
},
|
||||
workerTimeout,
|
||||
);
|
||||
this._workerTimeoutId = self.setTimeout(this.terminateWorker, workerTimeout);
|
||||
}
|
||||
|
||||
/** Abort the current job, if any */
|
||||
@ -111,7 +105,11 @@ export default class Processor {
|
||||
this._abortRejector(new DOMException('Aborted', 'AbortError'));
|
||||
this._abortRejector = undefined;
|
||||
this._busy = false;
|
||||
this.terminateWorker();
|
||||
}
|
||||
|
||||
@bind
|
||||
terminateWorker() {
|
||||
if (!this._worker) return;
|
||||
this._worker.terminate();
|
||||
this._worker = undefined;
|
||||
|
@ -12,8 +12,9 @@ import Select from '../../components/select';
|
||||
|
||||
interface Props {
|
||||
isVector: Boolean;
|
||||
inputWidth: number;
|
||||
inputHeight: number;
|
||||
options: ResizeOptions;
|
||||
aspect: number;
|
||||
onChange(newOptions: ResizeOptions): void;
|
||||
}
|
||||
|
||||
@ -21,12 +22,34 @@ interface State {
|
||||
maintainAspect: boolean;
|
||||
}
|
||||
|
||||
const sizePresets = [0.25, 0.3333, 0.5, 1, 2, 3, 4];
|
||||
|
||||
/**
|
||||
* Should we allow the user to select hqx? Chrome currently has a wasm bug, so we currently avoid it
|
||||
* there, unless overridden.
|
||||
* crbug.com/974804
|
||||
*/
|
||||
const allowHqx: boolean = (() => {
|
||||
const url = new URL(location.href);
|
||||
return url.searchParams.has('allow-hqx')
|
||||
// Yep. UA sniffing. Let's hope we can remove this soon.
|
||||
// Block browsers with Chrome/, unless they also have Edge/ (since the Edge UA includes Chrome/)
|
||||
|| !navigator.userAgent.includes('Chrome/') || navigator.userAgent.includes('Edge/');
|
||||
})();
|
||||
|
||||
export default class ResizerOptions extends Component<Props, State> {
|
||||
state: State = {
|
||||
maintainAspect: true,
|
||||
};
|
||||
|
||||
form?: HTMLFormElement;
|
||||
private form?: HTMLFormElement;
|
||||
private presetWidths: { [idx: number]: number } = {};
|
||||
private presetHeights: { [idx: number]: number } = {};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.generatePresetValues(props.inputWidth, props.inputHeight);
|
||||
}
|
||||
|
||||
private reportOptions() {
|
||||
const form = this.form!;
|
||||
@ -53,18 +76,31 @@ export default class ResizerOptions extends Component<Props, State> {
|
||||
this.reportOptions();
|
||||
}
|
||||
|
||||
private getAspect() {
|
||||
return this.props.inputWidth / this.props.inputHeight;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
if (!prevState.maintainAspect && this.state.maintainAspect) {
|
||||
this.form!.height.value = Math.round(Number(this.form!.width.value) / this.props.aspect);
|
||||
this.form!.height.value = Math.round(Number(this.form!.width.value) / this.getAspect());
|
||||
this.reportOptions();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
if (
|
||||
this.props.inputWidth !== nextProps.inputWidth ||
|
||||
this.props.inputHeight !== nextProps.inputHeight
|
||||
) {
|
||||
this.generatePresetValues(nextProps.inputWidth, nextProps.inputHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
private onWidthInput() {
|
||||
if (this.state.maintainAspect) {
|
||||
const width = inputFieldValueAsNumber(this.form!.width);
|
||||
this.form!.height.value = Math.round(width / this.props.aspect);
|
||||
this.form!.height.value = Math.round(width / this.getAspect());
|
||||
}
|
||||
|
||||
this.reportOptions();
|
||||
@ -74,12 +110,44 @@ export default class ResizerOptions extends Component<Props, State> {
|
||||
private onHeightInput() {
|
||||
if (this.state.maintainAspect) {
|
||||
const height = inputFieldValueAsNumber(this.form!.height);
|
||||
this.form!.width.value = Math.round(height * this.props.aspect);
|
||||
this.form!.width.value = Math.round(height * this.getAspect());
|
||||
}
|
||||
|
||||
this.reportOptions();
|
||||
}
|
||||
|
||||
private generatePresetValues(width: number, height: number) {
|
||||
for (const preset of sizePresets) {
|
||||
this.presetWidths[preset] = Math.round(width * preset);
|
||||
this.presetHeights[preset] = Math.round(height * preset);
|
||||
}
|
||||
}
|
||||
|
||||
private getPreset(): number | string {
|
||||
const { width, height } = this.props.options;
|
||||
|
||||
for (const preset of sizePresets) {
|
||||
if (
|
||||
width === this.presetWidths[preset] &&
|
||||
height === this.presetHeights[preset]
|
||||
) return preset;
|
||||
}
|
||||
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
@bind
|
||||
private onPresetChange(event: Event) {
|
||||
const select = event.target as HTMLSelectElement;
|
||||
if (select.value === 'custom') return;
|
||||
const multiplier = Number(select.value);
|
||||
(this.form!.width as HTMLInputElement).value =
|
||||
Math.round(this.props.inputWidth * multiplier) + '';
|
||||
(this.form!.height as HTMLInputElement).value =
|
||||
Math.round(this.props.inputHeight * multiplier) + '';
|
||||
this.reportOptions();
|
||||
}
|
||||
|
||||
render({ options, isVector }: Props, { maintainAspect }: State) {
|
||||
return (
|
||||
<form ref={linkRef(this, 'form')} class={style.optionsSection} onSubmit={preventDefault}>
|
||||
@ -95,12 +163,22 @@ export default class ResizerOptions extends Component<Props, State> {
|
||||
<option value="mitchell">Mitchell</option>
|
||||
<option value="catrom">Catmull-Rom</option>
|
||||
<option value="triangle">Triangle (bilinear)</option>
|
||||
{allowHqx && <option value="hqx">hqx (pixel art)</option>}
|
||||
<option value="browser-pixelated">Browser pixelated</option>
|
||||
<option value="browser-low">Browser low quality</option>
|
||||
<option value="browser-medium">Browser medium quality</option>
|
||||
<option value="browser-high">Browser high quality</option>
|
||||
</Select>
|
||||
</label>
|
||||
<label class={style.optionTextFirst}>
|
||||
Preset:
|
||||
<Select value={this.getPreset()} onChange={this.onPresetChange}>
|
||||
{sizePresets.map(preset =>
|
||||
<option value={preset}>{preset * 100}%</option>,
|
||||
)}
|
||||
<option value="custom">Custom</option>
|
||||
</Select>
|
||||
</label>
|
||||
<label class={style.optionTextFirst}>
|
||||
Width:
|
||||
<input
|
||||
|
@ -1,8 +1,26 @@
|
||||
type BrowserResizeMethods = 'browser-pixelated' | 'browser-low' | 'browser-medium' | 'browser-high';
|
||||
type WorkerResizeMethods = 'triangle' | 'catrom' | 'mitchell' | 'lanczos3';
|
||||
const workerResizeMethods: WorkerResizeMethods[] = ['triangle', 'catrom', 'mitchell', 'lanczos3'];
|
||||
type BrowserResizeMethods =
|
||||
| 'browser-pixelated'
|
||||
| 'browser-low'
|
||||
| 'browser-medium'
|
||||
| 'browser-high';
|
||||
type WorkerResizeMethods =
|
||||
| 'triangle'
|
||||
| 'catrom'
|
||||
| 'mitchell'
|
||||
| 'lanczos3'
|
||||
| 'hqx';
|
||||
const workerResizeMethods: WorkerResizeMethods[] = [
|
||||
'triangle',
|
||||
'catrom',
|
||||
'mitchell',
|
||||
'lanczos3',
|
||||
'hqx',
|
||||
];
|
||||
|
||||
export type ResizeOptions = BrowserResizeOptions | WorkerResizeOptions | VectorResizeOptions;
|
||||
export type ResizeOptions =
|
||||
| BrowserResizeOptions
|
||||
| WorkerResizeOptions
|
||||
| VectorResizeOptions;
|
||||
|
||||
export interface ResizeOptionsCommon {
|
||||
width: number;
|
||||
@ -29,10 +47,21 @@ export interface VectorResizeOptions extends ResizeOptionsCommon {
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
export function isWorkerOptions(opts: ResizeOptions): opts is WorkerResizeOptions {
|
||||
export function isWorkerOptions(
|
||||
opts: ResizeOptions,
|
||||
): opts is WorkerResizeOptions {
|
||||
return (workerResizeMethods as string[]).includes(opts.method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether a set of options are from the HQ<n>X set
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
export function isHqx(opts: ResizeOptions): opts is WorkerResizeOptions {
|
||||
return opts.method === 'hqx';
|
||||
}
|
||||
|
||||
export const defaultOptions: ResizeOptions = {
|
||||
// Width and height will always default to the image size.
|
||||
// This is set elsewhere.
|
||||
|
@ -25,3 +25,12 @@ export function initEmscriptenModule<T extends EmscriptenWasm.Module>(
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
interface ClampOpts {
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export function clamp(x: number, opts: ClampOpts): number {
|
||||
return Math.min(Math.max(x, opts.min || Number.MIN_VALUE), opts.max || Number.MAX_VALUE);
|
||||
}
|
||||
|
@ -110,7 +110,10 @@ export default class App extends Component<Props, State> {
|
||||
@bind
|
||||
private openEditor() {
|
||||
if (this.state.isEditorOpen) return;
|
||||
history.pushState(null, '', ROUTE_EDITOR);
|
||||
// Change path, but preserve query string.
|
||||
const editorURL = new URL(location.href);
|
||||
editorURL.pathname = ROUTE_EDITOR;
|
||||
history.pushState(null, '', editorURL.href);
|
||||
this.setState({ isEditorOpen: true });
|
||||
}
|
||||
|
||||
|
@ -146,12 +146,14 @@ export default class Options extends Component<Props, State> {
|
||||
{preprocessorState.resize.enabled ?
|
||||
<ResizeOptionsComponent
|
||||
isVector={Boolean(source && source.vectorImage)}
|
||||
aspect={source ? source.processed.width / source.processed.height : 1}
|
||||
inputWidth={source ? source.processed.width : 1}
|
||||
inputHeight={source ? source.processed.height : 1}
|
||||
options={preprocessorState.resize}
|
||||
onChange={this.onResizeOptionsChange}
|
||||
/>
|
||||
: null}
|
||||
</Expander>
|
||||
|
||||
<label class={style.sectionEnabler}>
|
||||
<Checkbox
|
||||
name="quantizer.enable"
|
||||
|
@ -24,7 +24,7 @@ import { decodeImage } from '../../codecs/decoders';
|
||||
import { cleanMerge, cleanSet } from '../../lib/clean-modify';
|
||||
import Processor from '../../codecs/processor';
|
||||
import {
|
||||
BrowserResizeOptions, isWorkerOptions as isWorkerResizeOptions,
|
||||
BrowserResizeOptions, isWorkerOptions as isWorkerResizeOptions, isHqx, WorkerResizeOptions,
|
||||
} from '../../codecs/resize/processor-meta';
|
||||
import './custom-els/MultiPanel';
|
||||
import Results from '../results';
|
||||
@ -106,6 +106,18 @@ async function preprocessImage(
|
||||
source.vectorImage,
|
||||
preprocessData.resize,
|
||||
);
|
||||
} else if (isHqx(preprocessData.resize)) {
|
||||
// Hqx can only do x2, x3 or x4.
|
||||
result = await processor.workerResize(result, preprocessData.resize);
|
||||
// Seems like the globals from Rust from hqx and resize are conflicting.
|
||||
// For now we can fix that by terminating the worker.
|
||||
// TODO: Use wasm-bindgen’s new --web target to create a proper ES6 module
|
||||
// and remove this.
|
||||
processor.terminateWorker();
|
||||
// If the target size is not a clean x2, x3 or x4, use Catmull-Rom
|
||||
// for the remaining scaling.
|
||||
const pixelOpts = { ...preprocessData.resize, method: 'catrom' };
|
||||
result = await processor.workerResize(result, pixelOpts as WorkerResizeOptions);
|
||||
} else if (isWorkerResizeOptions(preprocessData.resize)) {
|
||||
result = await processor.workerResize(result, preprocessData.resize);
|
||||
} else {
|
||||
|
@ -12,7 +12,8 @@
|
||||
"variable-name": [true, "check-format", "allow-leading-underscore"],
|
||||
"no-duplicate-imports": false,
|
||||
"prefer-template": [true, "allow-single-concat"],
|
||||
"import-name": false
|
||||
"import-name": false,
|
||||
"jsx-key": false
|
||||
},
|
||||
"linterOptions": {
|
||||
"exclude": [
|
||||
|
Reference in New Issue
Block a user