Compare commits

..

4 Commits

Author SHA1 Message Date
Surma
b6f69c57cf moar stuff 2019-02-07 17:35:48 +00:00
Surma
9e19c36b42 Add Xargo and config and stuff 2019-02-07 00:29:11 +00:00
Surma
cf45520be3 Cleanup and add pkg 2019-02-06 10:25:56 +00:00
Surma
8faf8a5b48 First attempt at Oxipng 2019-02-05 19:40:22 +00:00
101 changed files with 7971 additions and 6938 deletions

1
.nvmrc
View File

@@ -1 +0,0 @@
10.16.1

View File

@@ -1,4 +1,7 @@
language: node_js
node_js:
- node
- 10
- 8
cache: npm
script: npm run build
after_success: npm run sizereport

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/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
RUN mkdir /opt/wabt && \
curl -L https://github.com/WebAssembly/wabt/releases/download/1.0.11/wabt-1.0.11-linux.tar.gz | tar -xzf - -C /opt/wabt --strip 1
ENV PATH="/opt/binaryen:/opt/wabt:${PATH}"
WORKDIR /src

View File

@@ -1,25 +0,0 @@
#!/bin/bash
set -e
echo "============================================="
echo "Compiling wasm"
echo "============================================="
(
wasm-pack build
wasm-strip pkg/squooshhqx_bg.wasm
echo "Optimising Wasm so it doesn't break Chrome (this takes like 10-15mins. get a cup of tea)"
echo "Once https://crbug.com/974804 is fixed, we can remove this step"
wasm-opt -Os --no-validation -o pkg/squooshhqx_bg.wasm 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 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"

View File

@@ -1,4 +0,0 @@
{
"name": "hqx",
"lockfileVersion": 1
}

View File

@@ -1,7 +0,0 @@
{
"name": "hqx",
"scripts": {
"build:image": "docker build -t squoosh-hqx .",
"build": "docker run --rm -v $(pwd):/src squoosh-hqx ./build.sh"
}
}

View File

@@ -1,15 +0,0 @@
{
"name": "squooshhqx",
"collaborators": [
"Surma <surma@surma.link>"
],
"version": "0.1.0",
"files": [
"squooshhqx_bg.wasm",
"squooshhqx.js",
"squooshhqx.d.ts"
],
"module": "squooshhqx.js",
"types": "squooshhqx.d.ts",
"sideEffects": "false"
}

View File

@@ -1,9 +0,0 @@
/* 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;

View File

@@ -1,46 +0,0 @@
import * as wasm from './squooshhqx_bg.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;
}
let cachegetInt32Memory = null;
function getInt32Memory() {
if (cachegetInt32Memory === null || cachegetInt32Memory.buffer !== wasm.memory.buffer) {
cachegetInt32Memory = new Int32Array(wasm.memory.buffer);
}
return cachegetInt32Memory;
}
function getArrayU32FromWasm(ptr, len) {
return getUint32Memory().subarray(ptr / 4, ptr / 4 + len);
}
/**
* @param {Uint32Array} input_image
* @param {number} input_width
* @param {number} input_height
* @param {number} factor
* @returns {Uint32Array}
*/
export function resize(input_image, input_width, input_height, factor) {
const retptr = 8;
const ret = wasm.resize(retptr, passArray32ToWasm(input_image), WASM_VECTOR_LEN, input_width, input_height, factor);
const memi32 = getInt32Memory();
const v0 = getArrayU32FromWasm(memi32[retptr / 4 + 0], memi32[retptr / 4 + 1]).slice();
wasm.__wbindgen_free(memi32[retptr / 4 + 0], memi32[retptr / 4 + 1] * 4);
return v0;
}

View File

@@ -1,5 +0,0 @@
/* tslint:disable */
export const memory: WebAssembly.Memory;
export function __wbindgen_malloc(a: number): number;
export function __wbindgen_free(a: number, b: number): void;
export function resize(a: number, b: number, c: number, d: number, e: number, f: number): void;

Binary file not shown.

View File

@@ -1,55 +0,0 @@
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;
}

0
codecs/oxipng/.cargo-ok Normal file
View File

View File

@@ -1,5 +1,5 @@
/target
**/*.rs.bk
target
Cargo.lock
bin/
pkg/README.md
wasm-pack.log

View File

@@ -1,19 +1,19 @@
[package]
name = "resize"
name = "test"
version = "0.1.0"
authors = ["Surma <surma@surma.link>"]
[lib]
#crate-type = ["cdylib", "rlib"]
crate-type = ["cdylib"]
path = "lib.rs"
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook", "wee_alloc"]
default = ["console_error_panic_hook"]
[dependencies]
cfg-if = "0.1.2"
wasm-bindgen = "0.2.38"
resize = "0.3.0"
wasm-bindgen = "0.2"
oxipng = "2.2.0"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
@@ -34,4 +34,3 @@ wasm-bindgen-test = "0.2"
[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"
lto = true

52
codecs/oxipng/README.md Normal file
View File

@@ -0,0 +1,52 @@
# 🦀🕸️ `wasm-pack-template`
A template for kick starting a Rust and WebAssembly project using
[`wasm-pack`](https://github.com/rustwasm/wasm-pack).
This template is designed for compiling Rust libraries into WebAssembly and
publishing the resulting package to NPM.
* Want to use the published NPM package in a Website? [Check out
`create-wasm-app`.](https://github.com/rustwasm/create-wasm-app)
* Want to make a monorepo-style Website without publishing to NPM? Check out
[`rust-webpack-template`](https://github.com/rustwasm/rust-webpack-template)
and/or
[`rust-parcel-template`](https://github.com/rustwasm/rust-parcel-template).
## 🔋 Batteries Included
* [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating
between WebAssembly and JavaScript.
* [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook)
for logging panic messages to the developer console.
* [`wee_alloc`](https://github.com/rustwasm/wee_alloc), an allocator optimized
for small code size.
## 🚴 Usage
### 🐑 Use `cargo generate` to Clone this Template
[Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate)
```
cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project
cd my-project
```
### 🛠️ Build with `wasm-pack build`
```
wasm-pack build
```
### 🔬 Test in Headless Browsers with `wasm-pack test`
```
wasm-pack test --headless --firefox
```
### 🎁 Publish to NPM with `wasm-pack publish`
```
wasm-pack publish
```

5
codecs/oxipng/Xargo.toml Normal file
View File

@@ -0,0 +1,5 @@
[target.wasm32-unknown-unknown.dependencies]
time = {}
[target.wasm32-unknown-unknown.dependencies.std]
features = ["wasm_syscall"]

View File

@@ -0,0 +1,52 @@
# 🦀🕸️ `wasm-pack-template`
A template for kick starting a Rust and WebAssembly project using
[`wasm-pack`](https://github.com/rustwasm/wasm-pack).
This template is designed for compiling Rust libraries into WebAssembly and
publishing the resulting package to NPM.
* Want to use the published NPM package in a Website? [Check out
`create-wasm-app`.](https://github.com/rustwasm/create-wasm-app)
* Want to make a monorepo-style Website without publishing to NPM? Check out
[`rust-webpack-template`](https://github.com/rustwasm/rust-webpack-template)
and/or
[`rust-parcel-template`](https://github.com/rustwasm/rust-parcel-template).
## 🔋 Batteries Included
* [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating
between WebAssembly and JavaScript.
* [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook)
for logging panic messages to the developer console.
* [`wee_alloc`](https://github.com/rustwasm/wee_alloc), an allocator optimized
for small code size.
## 🚴 Usage
### 🐑 Use `cargo generate` to Clone this Template
[Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate)
```
cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project
cd my-project
```
### 🛠️ Build with `wasm-pack build`
```
wasm-pack build
```
### 🔬 Test in Headless Browsers with `wasm-pack test`
```
wasm-pack test --headless --firefox
```
### 🎁 Publish to NPM with `wasm-pack publish`
```
wasm-pack publish
```

BIN
codecs/oxipng/pkg/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

14
codecs/oxipng/pkg/lol.js Normal file
View File

@@ -0,0 +1,14 @@
const oxipng = require("./oxipng_wasm");
const repl = require("repl");
const fs = require("fs");
async function init() {
// const img = fs.readFileSync("img.png")
// const output = oxipng.compress(img, 0);
// fs.writeFileSync("output.png", output);
console.log(">>>", oxipng.doit());
const r = repl.start("node> ");
r.context.i = oxipng;
}
init();

3
codecs/oxipng/pkg/oxipng.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/* tslint:disable */
export function compress(arg0: Uint8Array, arg1: number): Uint8Array;

View File

@@ -0,0 +1,73 @@
/* tslint:disable */
var wasm;
const TextDecoder = require('util').TextDecoder;
let cachedTextDecoder = new TextDecoder('utf-8');
let cachegetUint8Memory = null;
function getUint8Memory() {
if (cachegetUint8Memory === null || cachegetUint8Memory.buffer !== wasm.memory.buffer) {
cachegetUint8Memory = new Uint8Array(wasm.memory.buffer);
}
return cachegetUint8Memory;
}
function getStringFromWasm(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory().subarray(ptr, ptr + len));
}
module.exports.__wbg_log_64e6f53d8e6d5db5 = function(arg0, arg1) {
let varg0 = getStringFromWasm(arg0, arg1);
console.log(varg0);
};
let WASM_VECTOR_LEN = 0;
function passArray8ToWasm(arg) {
const ptr = wasm.__wbindgen_malloc(arg.length * 1);
getUint8Memory().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function getArrayU8FromWasm(ptr, len) {
return getUint8Memory().subarray(ptr / 1, ptr / 1 + len);
}
let cachedGlobalArgumentPtr = null;
function globalArgumentPtr() {
if (cachedGlobalArgumentPtr === null) {
cachedGlobalArgumentPtr = wasm.__wbindgen_global_argument_ptr();
}
return cachedGlobalArgumentPtr;
}
let cachegetUint32Memory = null;
function getUint32Memory() {
if (cachegetUint32Memory === null || cachegetUint32Memory.buffer !== wasm.memory.buffer) {
cachegetUint32Memory = new Uint32Array(wasm.memory.buffer);
}
return cachegetUint32Memory;
}
/**
* @param {Uint8Array} arg0
* @param {number} arg1
* @returns {Uint8Array}
*/
module.exports.compress = function(arg0, arg1) {
const ptr0 = passArray8ToWasm(arg0);
const len0 = WASM_VECTOR_LEN;
const retptr = globalArgumentPtr();
wasm.compress(retptr, ptr0, len0, arg1);
const mem = getUint32Memory();
const rustptr = mem[retptr / 4];
const rustlen = mem[retptr / 4 + 1];
const realRet = getArrayU8FromWasm(rustptr, rustlen).slice();
wasm.__wbindgen_free(rustptr, rustlen * 1);
return realRet;
};
wasm = require('./oxipng_bg');

View File

@@ -1,5 +1,6 @@
/* tslint:disable */
export const memory: WebAssembly.Memory;
export function __wbindgen_global_argument_ptr(): number;
export function compress(a: number, b: number, c: number, d: number): void;
export function __wbindgen_malloc(a: number): number;
export function __wbindgen_free(a: number, b: number): void;
export function resize(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number): void;

View File

@@ -0,0 +1,9 @@
const path = require('path').join(__dirname, 'oxipng_bg.wasm');
const bytes = require('fs').readFileSync(path);
let imports = {};
imports['./oxipng'] = require('./oxipng');
const wasmModule = new WebAssembly.Module(bytes);
const wasmInstance = new WebAssembly.Instance(wasmModule, imports);
module.exports = wasmInstance.exports;

Binary file not shown.

3
codecs/oxipng/pkg/oxipng_manual.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/* tslint:disable */
export function compress(arg0: Uint8Array, arg1: number): Promise<Uint8Array>;

View File

@@ -0,0 +1,77 @@
/* tslint:disable */
import wasmUrl from './oxipng_bg.wasm';
let wasm;
const instancePromise = WebAssembly.instantiateStreaming(fetch(wasmUrl), {
"./oxipng": {__wbg_log_64e6f53d8e6d5db5}
});
let cachedTextDecoder = new TextDecoder('utf-8');
let cachegetUint8Memory = null;
function getUint8Memory() {
if (cachegetUint8Memory === null || cachegetUint8Memory.buffer !== wasm.memory.buffer) {
cachegetUint8Memory = new Uint8Array(wasm.memory.buffer);
}
return cachegetUint8Memory;
}
function getStringFromWasm(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory().subarray(ptr, ptr + len));
}
export function __wbg_log_64e6f53d8e6d5db5(arg0, arg1) {
let varg0 = getStringFromWasm(arg0, arg1);
console.log(varg0);
}
let WASM_VECTOR_LEN = 0;
function passArray8ToWasm(arg) {
const ptr = wasm.__wbindgen_malloc(arg.length * 1);
getUint8Memory().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function getArrayU8FromWasm(ptr, len) {
return getUint8Memory().subarray(ptr / 1, ptr / 1 + len);
}
let cachedGlobalArgumentPtr = null;
function globalArgumentPtr() {
if (cachedGlobalArgumentPtr === null) {
cachedGlobalArgumentPtr = wasm.__wbindgen_global_argument_ptr();
}
return cachedGlobalArgumentPtr;
}
let cachegetUint32Memory = null;
function getUint32Memory() {
if (cachegetUint32Memory === null || cachegetUint32Memory.buffer !== wasm.memory.buffer) {
cachegetUint32Memory = new Uint32Array(wasm.memory.buffer);
}
return cachegetUint32Memory;
}
/**
* @param {Uint8Array} arg0
* @param {number} arg1
* @returns {Uint8Array}
*/
export async function compress(arg0, arg1) {
wasm = (await instancePromise).instance.exports;
debugger;
const ptr0 = passArray8ToWasm(arg0);
const len0 = WASM_VECTOR_LEN;
const retptr = globalArgumentPtr();
wasm.compress(retptr, ptr0, len0, arg1);
const mem = getUint32Memory();
const rustptr = mem[retptr / 4];
const rustlen = mem[retptr / 4 + 1];
const realRet = getArrayU8FromWasm(rustptr, rustlen).slice();
wasm.__wbindgen_free(rustptr, rustlen * 1);
return realRet;
}

3
codecs/oxipng/pkg/oxipng_wasm.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/* tslint:disable */
export function doit(): number;

View File

@@ -0,0 +1,11 @@
/* tslint:disable */
var wasm;
/**
* @returns {number}
*/
module.exports.doit = function() {
return wasm.doit();
};
wasm = require('./oxipng_wasm_bg');

3
codecs/oxipng/pkg/oxipng_wasm_bg.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/* tslint:disable */
export const memory: WebAssembly.Memory;
export function doit(): number;

View File

@@ -0,0 +1,8 @@
const path = require('path').join(__dirname, 'oxipng_wasm_bg.wasm');
const bytes = require('fs').readFileSync(path);
let imports = {};
const wasmModule = new WebAssembly.Module(bytes);
const wasmInstance = new WebAssembly.Instance(wasmModule, imports);
module.exports = wasmInstance.exports;

Binary file not shown.

View File

@@ -0,0 +1,15 @@
{
"name": "oxipng-wasm",
"collaborators": [
"Surma <surma@surma.link>"
],
"version": "0.1.0",
"files": [
"oxipng_wasm_bg.wasm",
"oxipng_wasm.js",
"oxipng_wasm_bg.js",
"oxipng_wasm.d.ts"
],
"main": "oxipng_wasm.js",
"types": "oxipng_wasm.d.ts"
}

View File

42
codecs/oxipng/src/lib.rs Normal file
View File

@@ -0,0 +1,42 @@
extern crate cfg_if;
extern crate wasm_bindgen;
// extern crate oxipng;
mod utils;
use cfg_if::cfg_if;
use wasm_bindgen::prelude::*;
use std::time::{Instant};
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]
// extern {
// #[wasm_bindgen(js_namespace = console)]
// fn log(s: &str);
// }
// #[wasm_bindgen]
// pub fn compress(img: Vec<u8>, level: u8) -> Vec<u8> {
// log(&format!("len: {}, level: {}", img.len(), level));
// let mut options = oxipng::Options::from_preset(level);
// options.threads = 0;
// let result = oxipng::optimize_from_memory(img.as_slice(), &options);
// match result {
// Ok(v) => v,
// Err(e) => e.to_string().as_bytes().to_vec()
// }
// }
#[wasm_bindgen]
pub fn doit() -> u32 {
let start = Instant::now();
start.elapsed().as_secs() as u32
}

1
codecs/oxipng/tmp/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target

View File

@@ -1,19 +1,17 @@
[package]
name = "squooshhqx"
name = "loltest"
version = "0.1.0"
authors = ["Surma <surma@surma.link>"]
[lib]
crate-type = ["cdylib"]
path = "lib.rs"
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook", "wee_alloc"]
default = ["console_error_panic_hook"]
[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"}
wasm-bindgen = "0.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
@@ -21,17 +19,9 @@ hqx = {git = "https://github.com/CryZe/wasmboy-rs", tag="v0.1.2"}
# 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

View File

@@ -0,0 +1,2 @@
[dependencies.std]
features = ["wasm_syscall"]

View File

@@ -0,0 +1,20 @@
Im trying to activate [the `wasm_syscall` feature][1] in Rusts stdlib for WebAssembly.
Here is my `Cargo.toml` and my `Xargo.toml`. But even with this setup the generated wasm file is still hard-coded to panic.
**HELP?**
My current command to compile is:
```
xargo build --target wasm32-unknown-unknown --release
```
If you have [`wasm2wat`][2] installed, you can verify the generate code via
```
wasm2wat target/wasm32-unknown-unknown/release/loltest.wasm | grep -A5 perform::
```
[1]: https://github.com/rust-lang/rust/blob/b139669f374eb5024a50eb13f116ff763b1c5935/src/libstd/sys/wasm/mod.rs#L309
[2]: https://github.com/WebAssembly/wabt

14
codecs/oxipng/tmp/lib.rs Normal file
View File

@@ -0,0 +1,14 @@
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
use std::thread::spawn;
#[wasm_bindgen]
pub fn doit() {
// let child = spawn(move || -> u32 {
// 5
// });
// let result = child.join().unwrap();
let result = spawn();
println!("Result: {}", result);
}

47
codecs/oxipng/tmp/lol.js Normal file
View File

@@ -0,0 +1,47 @@
// const oxipng = require("./oxipng_wasm");
const repl = require("repl");
const fs = require("fs");
const dec = new TextDecoder();
let buffer = '';
async function init() {
const { instance } = await WebAssembly.instantiate(
fs.readFileSync("./target/wasm32-unknown-unknown/release/loltest.wasm"),
{
__wbindgen_placeholder__: {
__wbindgen_describe(v) {
console.log(`__wbindgen_desribe(${v})`);
}
},
env: {
// See https://github.com/rust-lang/rust/blob/master/src/libstd/sys/wasm/mod.rs
rust_wasm_syscall(syscall, ptr) {
switch(syscall) {
case 1: // Write
const [fd, dataPtr, len] = new Uint32Array(instance.exports.memory.buffer, ptr, 3 * 4);
const fragment = new Uint8Array(instance.exports.memory.buffer, dataPtr, len);
buffer += dec.decode(fragment);
const idx = buffer.indexOf('\n');
if(idx !== -1) {
console.log(buffer.slice(0, idx));
buffer = buffer.slice(idx);
}
return 1;
case 6: // Time
return 1;
default:
return 0; // False, unimplemented
}
}
}
}
);
try {
instance.exports.doit();
} catch{}
const r = repl.start("node> ");
r.context.i = instance;
}
init();

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
**/*.rs.bk
target
Cargo.lock
bin/
pkg/README.md
lut.inc

View File

@@ -1,9 +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.11/wabt-1.0.11-linux.tar.gz | tar -xzf - -C /opt/wabt --strip 1
ENV PATH="/opt/wabt:${PATH}"
WORKDIR /src

View File

@@ -1,23 +0,0 @@
include!("./src/srgb.rs");
use std::io::Write;
fn main() -> std::io::Result<()> {
let mut srgb_to_linear_lut = String::from("static SRGB_TO_LINEAR_LUT: [f32; 256] = [");
let mut linear_to_srgb_lut = String::from("static LINEAR_TO_SRGB_LUT: [f32; 256] = [");
for i in 0..256 {
srgb_to_linear_lut.push_str(&format!("{0:.7}", srgb_to_linear((i as f32) / 255.0)));
srgb_to_linear_lut.push_str(",");
linear_to_srgb_lut.push_str(&format!("{0:.7}", linear_to_srgb((i as f32) / 255.0)));
linear_to_srgb_lut.push_str(",");
}
srgb_to_linear_lut.pop().unwrap();
linear_to_srgb_lut.pop().unwrap();
srgb_to_linear_lut.push_str("];");
linear_to_srgb_lut.push_str("];");
let mut file = std::fs::File::create("src/lut.inc")?;
file.write_all(srgb_to_linear_lut.as_bytes())?;
file.write_all(linear_to_srgb_lut.as_bytes())?;
Ok(())
}

View File

@@ -1,22 +0,0 @@
#!/bin/bash
set -e
echo "============================================="
echo "Compiling wasm"
echo "============================================="
(
wasm-pack build
wasm-strip pkg/resize_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-resize .\`"
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"

View File

@@ -1,4 +0,0 @@
{
"name": "resize",
"lockfileVersion": 1
}

View File

@@ -1,7 +0,0 @@
{
"name": "resize",
"scripts": {
"build:image": "docker build -t squoosh-resize .",
"build": "docker run --rm -v $(pwd):/src squoosh-resize ./build.sh"
}
}

View File

@@ -1,15 +0,0 @@
{
"name": "resize",
"collaborators": [
"Surma <surma@surma.link>"
],
"version": "0.1.0",
"files": [
"resize_bg.wasm",
"resize.js",
"resize.d.ts"
],
"module": "resize.js",
"types": "resize.d.ts",
"sideEffects": "false"
}

View File

@@ -1,13 +0,0 @@
/* tslint:disable */
/**
* @param {Uint8Array} input_image
* @param {number} input_width
* @param {number} input_height
* @param {number} output_width
* @param {number} output_height
* @param {number} typ_idx
* @param {boolean} premultiply
* @param {boolean} color_space_conversion
* @returns {Uint8Array}
*/
export function resize(input_image: Uint8Array, input_width: number, input_height: number, output_width: number, output_height: number, typ_idx: number, premultiply: boolean, color_space_conversion: boolean): Uint8Array;

View File

@@ -1,50 +0,0 @@
import * as wasm from './resize_bg.wasm';
let cachegetUint8Memory = null;
function getUint8Memory() {
if (cachegetUint8Memory === null || cachegetUint8Memory.buffer !== wasm.memory.buffer) {
cachegetUint8Memory = new Uint8Array(wasm.memory.buffer);
}
return cachegetUint8Memory;
}
let WASM_VECTOR_LEN = 0;
function passArray8ToWasm(arg) {
const ptr = wasm.__wbindgen_malloc(arg.length * 1);
getUint8Memory().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
let cachegetInt32Memory = null;
function getInt32Memory() {
if (cachegetInt32Memory === null || cachegetInt32Memory.buffer !== wasm.memory.buffer) {
cachegetInt32Memory = new Int32Array(wasm.memory.buffer);
}
return cachegetInt32Memory;
}
function getArrayU8FromWasm(ptr, len) {
return getUint8Memory().subarray(ptr / 1, ptr / 1 + len);
}
/**
* @param {Uint8Array} input_image
* @param {number} input_width
* @param {number} input_height
* @param {number} output_width
* @param {number} output_height
* @param {number} typ_idx
* @param {boolean} premultiply
* @param {boolean} color_space_conversion
* @returns {Uint8Array}
*/
export function resize(input_image, input_width, input_height, output_width, output_height, typ_idx, premultiply, color_space_conversion) {
const retptr = 8;
const ret = wasm.resize(retptr, passArray8ToWasm(input_image), WASM_VECTOR_LEN, input_width, input_height, output_width, output_height, typ_idx, premultiply, color_space_conversion);
const memi32 = getInt32Memory();
const v0 = getArrayU8FromWasm(memi32[retptr / 4 + 0], memi32[retptr / 4 + 1]).slice();
wasm.__wbindgen_free(memi32[retptr / 4 + 0], memi32[retptr / 4 + 1] * 1);
return v0;
}

Binary file not shown.

View File

@@ -1,121 +0,0 @@
extern crate cfg_if;
extern crate resize;
extern crate wasm_bindgen;
mod utils;
use cfg_if::cfg_if;
use resize::Pixel::RGBA;
use resize::Type;
use wasm_bindgen::prelude::*;
mod srgb;
use srgb::Clamp;
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;
}
}
include!("./lut.inc");
// If `with_space_conversion` is true, this function returns 2 functions that
// convert from sRGB to linear RGB and vice versa. If `with_space_conversion` is
// false, the 2 functions returned do nothing.
fn converter_funcs(with_space_conversion: bool) -> ((fn(u8) -> f32), (fn(f32) -> u8)) {
if with_space_conversion {
(
|v| SRGB_TO_LINEAR_LUT[v as usize] * 255.0,
|v| (LINEAR_TO_SRGB_LUT[v as usize] * 255.0) as u8,
)
} else {
(|v| v as f32, |v| v as u8)
}
}
// If `with_alpha_premultiplication` is true, this function returns a function
// that premultiply the alpha channel with the given channel value and another
// function that reverses that process. If `with_alpha_premultiplication` is
// false, the functions just return the channel value.
fn alpha_multiplier_funcs(
with_alpha_premultiplication: bool,
) -> ((fn(f32, u8) -> u8), (fn(u8, u8) -> f32)) {
if with_alpha_premultiplication {
(
|v, a| (v * (a as f32) / 255.0) as u8,
|v, a| (v as f32) * 255.0 / (a as f32).clamp(0.0, 255.0),
)
} else {
(|v, _a| v as u8, |v, _a| v as f32)
}
}
#[wasm_bindgen]
#[no_mangle]
pub fn resize(
mut input_image: Vec<u8>,
input_width: usize,
input_height: usize,
output_width: usize,
output_height: usize,
typ_idx: usize,
premultiply: bool,
color_space_conversion: bool,
) -> Vec<u8> {
let typ = match typ_idx {
0 => Type::Triangle,
1 => Type::Catrom,
2 => Type::Mitchell,
3 => Type::Lanczos3,
_ => panic!("Nope"),
};
let num_input_pixels = input_width * input_height;
let num_output_pixels = output_width * output_height;
let (to_linear, to_color_space) = converter_funcs(color_space_conversion);
let (premultiplier, demultiplier) = alpha_multiplier_funcs(premultiply);
// If both options are false, there is no preprocessing on the pixel valus
// and we can skip the loop.
if premultiply || color_space_conversion {
for i in 0..num_input_pixels {
for j in 0..3 {
input_image[4 * i + j] =
premultiplier(to_linear(input_image[4 * i + j]), input_image[4 * i + 3]);
}
}
}
let mut resizer = resize::new(
input_width,
input_height,
output_width,
output_height,
RGBA,
typ,
);
let mut output_image = Vec::<u8>::with_capacity(num_output_pixels * 4);
output_image.resize(num_output_pixels * 4, 0);
resizer.resize(input_image.as_slice(), output_image.as_mut_slice());
if premultiply || color_space_conversion {
for i in 0..num_output_pixels {
for j in 0..3 {
// We dont need to worry about division by zero, as division by zero
// is well-defined on floats to return ±Inf. ±Inf is converted to 0
// when casting to integers.
output_image[4 * i + j] = to_color_space(demultiplier(
output_image[4 * i + j],
output_image[4 * i + 3],
));
}
}
}
return output_image;
}

View File

@@ -1,29 +0,0 @@
pub trait Clamp: std::cmp::PartialOrd + Sized {
fn clamp(self, min: Self, max: Self) -> Self {
if self.lt(&min) {
min
} else if self.gt(&max) {
max
} else {
self
}
}
}
impl Clamp for f32 {}
pub fn srgb_to_linear(v: f32) -> f32 {
if v < 0.04045 {
v / 12.92
} else {
((v + 0.055) / 1.055).powf(2.4).clamp(0.0, 1.0)
}
}
pub fn linear_to_srgb(v: f32) -> f32 {
if v < 0.0031308 {
v * 12.92
} else {
(1.055 * v.powf(1.0 / 2.4) - 0.055).clamp(0.0, 1.0)
}
}

View File

@@ -1,17 +0,0 @@
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() {}
}
}

View File

@@ -1,2 +0,0 @@
target
Cargo.lock

View File

@@ -1,14 +0,0 @@
[package]
name = "rotate"
version = "0.1.0"
authors = ["Surma <surma@google.com>"]
edition = "2018"
[lib]
name = "rotate"
path = "rotate.rs"
crate-type = ["cdylib", "rlib"]
[profile.release]
lto = true
opt-level = "s"

View File

@@ -1,8 +0,0 @@
FROM rust
RUN rustup target add wasm32-unknown-unknown
RUN mkdir /opt/wabt && \
curl -L https://github.com/WebAssembly/wabt/releases/download/1.0.11/wabt-1.0.11-linux.tar.gz | tar -xzf - -C /opt/wabt --strip 1
ENV PATH="/opt/wabt:${PATH}"
WORKDIR /src

View File

@@ -1,45 +0,0 @@
// 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() {
// Adjustable constants.
const imageDimensions = 4096;
const iterations = new Array(100);
// Constants. Dont change.
const imageByteSize = imageDimensions * imageDimensions * 4;
const wasmPageSize = 64 * 1024;
const buffer = readbuffer("rotate.wasm");
const { instance } = await WebAssembly.instantiate(buffer);
const pagesAvailable = Math.floor(
instance.exports.memory.buffer.byteLength / wasmPageSize
);
const pagesNeeded = Math.floor((imageByteSize * 2 + 4) / wasmPageSize) + 1;
const additionalPagesNeeded = pagesNeeded - pagesAvailable;
if (additionalPagesNeeded > 0) {
instance.exports.memory.grow(additionalPagesNeeded);
}
[0, 90, 180, 270].forEach(rotation => {
print(`\n${rotation} degrees`);
print(`==============================`);
for (let i = 0; i < 100; i++) {
const start = Date.now();
instance.exports.rotate(imageDimensions, imageDimensions, rotation);
iterations[i] = Date.now() - start;
}
const average = iterations.reduce((sum, c) => sum + c) / iterations.length;
const stddev = Math.sqrt(
iterations
.map(i => Math.pow(i - average, 2))
.reduce((sum, c) => sum + c) / iterations.length
);
print(`n = ${iterations.length}`);
print(`Average: ${average}`);
print(`StdDev: ${stddev}`);
});
}
init().catch(e => console.error(e.stack));

View File

@@ -1,24 +0,0 @@
#!/bin/bash
set -e
echo "============================================="
echo "Compiling wasm"
echo "============================================="
(
cargo build \
--target wasm32-unknown-unknown \
--release
cp target/wasm32-unknown-unknown/release/rotate.wasm .
wasm-strip rotate.wasm
)
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-rotate .\`"
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"

View File

@@ -1,11 +0,0 @@
{
"name": "rotate",
"scripts": {
"build:image": "docker build -t squoosh-rotate .",
"build": "docker run --rm -v $(pwd):/src squoosh-rotate ./build.sh",
"benchmark": "echo File size after gzip && npm run benchmark:filesize && echo Optimizing && npm run -s benchmark:optimizing",
"benchmark:baseline": "v8 --liftoff --no-wasm-tier-up --no-opt ./benchmark.js",
"benchmark:optimizing": "v8 --no-liftoff --no-wasm-tier-up ./benchmark.js",
"benchmark:filesize": "cat rotate.wasm | gzip -c9n | wc -c"
}
}

View File

@@ -1,113 +0,0 @@
use std::slice::{from_raw_parts, from_raw_parts_mut};
// This function is taken from Zachary Dremann
// https://github.com/GoogleChromeLabs/squoosh/pull/462
trait HardUnwrap<T> {
fn unwrap_hard(self) -> T;
}
impl<T> HardUnwrap<T> for Option<T> {
#[cfg(not(debug_assertions))]
#[inline]
fn unwrap_hard(self) -> T {
match self {
Some(t) => t,
None => std::process::abort(),
}
}
#[cfg(debug_assertions)]
fn unwrap_hard(self) -> T {
self.unwrap()
}
}
const TILE_SIZE: usize = 16;
fn get_buffers<'a>(width: usize, height: usize) -> (&'a [u32], &'a mut [u32]) {
let num_pixels = width * height;
let in_b: &[u32];
let out_b: &mut [u32];
unsafe {
in_b = from_raw_parts::<u32>(8 as *const u32, num_pixels);
out_b = from_raw_parts_mut::<u32>((num_pixels * 4 + 8) as *mut u32, num_pixels);
}
return (in_b, out_b);
}
#[inline(never)]
fn rotate_0(width: usize, height: usize) {
let (in_b, out_b) = get_buffers(width, height);
for (in_p, out_p) in in_b.iter().zip(out_b.iter_mut()) {
*out_p = *in_p;
}
}
#[inline(never)]
fn rotate_90(width: usize, height: usize) {
let (in_b, out_b) = get_buffers(width, height);
let new_width = height;
let _new_height = width;
for y_start in (0..height).step_by(TILE_SIZE) {
for x_start in (0..width).step_by(TILE_SIZE) {
for y in y_start..(y_start + TILE_SIZE).min(height) {
let in_offset = y * width;
let in_bounds = if x_start + TILE_SIZE < width {
(in_offset + x_start)..(in_offset + x_start + TILE_SIZE)
} else {
(in_offset + x_start)..(in_offset + width)
};
let in_chunk = in_b.get(in_bounds).unwrap_hard();
for (x, in_p) in in_chunk.iter().enumerate() {
let new_x = (new_width - 1) - y;
let new_y = x + x_start;
*out_b.get_mut(new_y * new_width + new_x).unwrap_hard() = *in_p;
}
}
}
}
}
#[inline(never)]
fn rotate_180(width: usize, height: usize) {
let (in_b, out_b) = get_buffers(width, height);
for (in_p, out_p) in in_b.iter().zip(out_b.iter_mut().rev()) {
*out_p = *in_p;
}
}
#[inline(never)]
fn rotate_270(width: usize, height: usize) {
let (in_b, out_b) = get_buffers(width, height);
let new_width = height;
let new_height = width;
for y_start in (0..height).step_by(TILE_SIZE) {
for x_start in (0..width).step_by(TILE_SIZE) {
for y in y_start..(y_start + TILE_SIZE).min(height) {
let in_offset = y * width;
let in_bounds = if x_start + TILE_SIZE < width {
(in_offset + x_start)..(in_offset + x_start + TILE_SIZE)
} else {
(in_offset + x_start)..(in_offset + width)
};
let in_chunk = in_b.get(in_bounds).unwrap_hard();
for (x, in_p) in in_chunk.iter().enumerate() {
let new_x = y;
let new_y = new_height - 1 - (x_start + x);
*out_b.get_mut(new_y * new_width + new_x).unwrap_hard() = *in_p;
}
}
}
}
}
#[no_mangle]
fn rotate(width: usize, height: usize, rotate: usize) {
match rotate {
0 => rotate_0(width, height),
90 => rotate_90(width, height),
180 => rotate_180(width, height),
270 => rotate_270(width, height),
_ => std::process::abort(),
}
}

Binary file not shown.

10986
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,13 @@
{
"private": true,
"name": "squoosh",
"version": "1.8.1",
"version": "1.3.1",
"license": "apache-2.0",
"scripts": {
"start": "webpack-dev-server --host 0.0.0.0 --hot",
"build": "webpack -p",
"lint": "tslint -c tslint.json -p tsconfig.json -t verbose",
"lintfix": "tslint -c tslint.json -p tsconfig.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'",
"sizereport": "sizereport --config"
"lint": "tslint -c tslint.json -p tsconfig.json -t verbose 'src/**/*.{ts,tsx,js,jsx}'",
"lintfix": "tslint -c tslint.json -p tsconfig.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'"
},
"husky": {
"hooks": {
@@ -16,61 +15,55 @@
}
},
"devDependencies": {
"@types/node": "10.14.14",
"@types/node": "10.12.21",
"@types/pretty-bytes": "5.1.0",
"@types/webassembly-js-api": "0.0.3",
"@webcomponents/custom-elements": "1.2.4",
"@webpack-cli/serve": "0.1.8",
"assets-webpack-plugin": "3.9.10",
"chalk": "2.4.2",
"chokidar": "3.0.2",
"@types/webassembly-js-api": "0.0.2",
"@webcomponents/custom-elements": "1.2.1",
"@webpack-cli/serve": "0.1.3",
"assets-webpack-plugin": "3.9.7",
"chokidar": "2.0.4",
"classnames": "2.2.6",
"clean-webpack-plugin": "1.0.1",
"comlink": "3.1.1",
"copy-webpack-plugin": "5.0.4",
"critters-webpack-plugin": "2.4.0",
"copy-webpack-plugin": "4.6.0",
"critters-webpack-plugin": "2.2.0",
"css-loader": "1.0.1",
"ejs": "2.6.2",
"escape-string-regexp": "2.0.0",
"ejs": "2.6.1",
"exports-loader": "0.7.0",
"file-drop-element": "0.2.0",
"file-loader": "4.1.0",
"gzip-size": "5.1.1",
"file-drop-element": "0.0.9",
"file-loader": "3.0.1",
"html-webpack-plugin": "3.2.0",
"husky": "3.0.2",
"idb-keyval": "3.2.0",
"husky": "1.3.1",
"idb-keyval": "3.1.0",
"linkstate": "1.1.1",
"loader-utils": "1.2.3",
"mini-css-extract-plugin": "0.8.0",
"loader-utils": "1.2.0",
"mini-css-extract-plugin": "0.5.0",
"minimatch": "3.0.4",
"node-fetch": "2.6.0",
"node-sass": "4.12.0",
"node-sass": "4.11.0",
"optimize-css-assets-webpack-plugin": "5.0.1",
"pointer-tracker": "2.0.3",
"preact": "8.4.2",
"prerender-loader": "1.3.0",
"pretty-bytes": "5.3.0",
"prerender-loader": "1.2.0",
"pretty-bytes": "5.1.0",
"progress-bar-webpack-plugin": "1.12.1",
"raw-loader": "3.1.0",
"readdirp": "3.1.1",
"raw-loader": "1.0.0",
"sass-loader": "7.1.0",
"script-ext-html-webpack-plugin": "2.1.4",
"script-ext-html-webpack-plugin": "2.1.3",
"source-map-loader": "0.2.4",
"style-loader": "0.23.1",
"terser-webpack-plugin": "1.4.1",
"travis-size-report": "1.1.0",
"ts-loader": "6.0.3",
"tslint": "5.18.0",
"terser-webpack-plugin": "1.2.2",
"ts-loader": "5.3.3",
"tslint": "5.12.1",
"tslint-config-airbnb": "5.11.1",
"tslint-config-semistandard": "8.0.1",
"tslint-react": "4.0.0",
"typed-css-modules": "0.4.2",
"typescript": "3.5.3",
"url-loader": "2.1.0",
"tslint-config-semistandard": "7.0.0",
"tslint-react": "3.6.0",
"typed-css-modules": "0.3.7",
"typescript": "3.2.4",
"url-loader": "1.1.2",
"webpack": "4.28.0",
"webpack-bundle-analyzer": "3.4.1",
"webpack-cli": "3.3.4",
"webpack-dev-server": "3.7.2",
"worker-plugin": "3.1.0"
"webpack-bundle-analyzer": "3.0.3",
"webpack-cli": "3.2.3",
"webpack-dev-server": "3.1.14",
"worker-plugin": "3.0.0"
}
}

View File

@@ -1,14 +0,0 @@
const escapeRE = require("escape-string-regexp");
module.exports = {
repo: "GoogleChromeLabs/squoosh",
path: "build/**/!(*.map)",
branch: "master",
findRenamed(path, newPaths) {
const nameParts = /^(.+\.)[a-f0-9]+(\..+)$/.exec(path);
if (!nameParts) return;
const matchRe = new RegExp(`^${escapeRE(nameParts[1])}[a-f0-9]+${escapeRE(nameParts[2])}$`);
return newPaths.find(newPath => matchRe.test(newPath));
}
};

View File

@@ -1,5 +1,6 @@
import * as identity from './identity/encoder-meta';
import * as optiPNG from './optipng/encoder-meta';
import * as oxiPNG from './oxipng/encoder-meta';
import * as mozJPEG from './mozjpeg/encoder-meta';
import * as webP from './webp/encoder-meta';
import * as browserPNG from './browser-png/encoder-meta';
@@ -18,6 +19,7 @@ export interface EncoderSupportMap {
export type EncoderState =
identity.EncoderState |
optiPNG.EncoderState |
oxiPNG.EncoderState |
mozJPEG.EncoderState |
webP.EncoderState |
browserPNG.EncoderState |
@@ -32,6 +34,7 @@ export type EncoderState =
export type EncoderOptions =
identity.EncodeOptions |
optiPNG.EncodeOptions |
oxiPNG.EncodeOptions |
mozJPEG.EncodeOptions |
webP.EncodeOptions |
browserPNG.EncodeOptions |
@@ -48,6 +51,7 @@ export type EncoderType = keyof typeof encoderMap;
export const encoderMap = {
[identity.type]: identity,
[optiPNG.type]: optiPNG,
[oxiPNG.type]: oxiPNG,
[mozJPEG.type]: mozJPEG,
[webP.type]: webP,
[browserPNG.type]: browserPNG,

View File

@@ -1,3 +0,0 @@
export interface HqxOptions {
factor: 2 | 3 | 4;
}

View File

@@ -1,20 +0,0 @@
import { resize } from '../../../codecs/hqx/pkg';
import { HqxOptions } from './processor-meta';
export async function hqx(
data: ImageData,
opts: HqxOptions,
): Promise<ImageData> {
const input = data;
const result = 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,
);
}

View File

@@ -1,12 +1,12 @@
import imagequant, { QuantizerModule } from '../../../codecs/imagequant/imagequant';
import wasmUrl from '../../../codecs/imagequant/imagequant.wasm';
import { QuantizeOptions } from './processor-meta';
import { initEmscriptenModule } from '../util';
import { initWasmModule } from '../util';
let emscriptenModule: Promise<QuantizerModule>;
export async function process(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
if (!emscriptenModule) emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
if (!emscriptenModule) emscriptenModule = initWasmModule(imagequant, wasmUrl);
const module = await emscriptenModule;

View File

@@ -1,12 +1,12 @@
import mozjpeg_enc, { MozJPEGModule } from '../../../codecs/mozjpeg_enc/mozjpeg_enc';
import wasmUrl from '../../../codecs/mozjpeg_enc/mozjpeg_enc.wasm';
import { EncodeOptions } from './encoder-meta';
import { initEmscriptenModule } from '../util';
import { initWasmModule } from '../util';
let emscriptenModule: Promise<MozJPEGModule>;
export async function encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> {
if (!emscriptenModule) emscriptenModule = initEmscriptenModule(mozjpeg_enc, wasmUrl);
if (!emscriptenModule) emscriptenModule = initWasmModule(mozjpeg_enc, wasmUrl);
const module = await emscriptenModule;
const resultView = module.encode(data.data, data.width, data.height, options);

View File

@@ -3,7 +3,7 @@ export interface EncodeOptions {
}
export interface EncoderState { type: typeof type; options: EncodeOptions; }
export const type = 'png';
export const type = 'optipng';
export const label = 'OptiPNG';
export const mimeType = 'image/png';
export const extension = 'png';

View File

@@ -1,12 +1,12 @@
import optipng, { OptiPngModule } from '../../../codecs/optipng/optipng';
import wasmUrl from '../../../codecs/optipng/optipng.wasm';
import { EncodeOptions } from './encoder-meta';
import { initEmscriptenModule } from '../util';
import { initWasmModule } from '../util';
let emscriptenModule: Promise<OptiPngModule>;
export async function compress(data: BufferSource, options: EncodeOptions): Promise<ArrayBuffer> {
if (!emscriptenModule) emscriptenModule = initEmscriptenModule(optipng, wasmUrl);
if (!emscriptenModule) emscriptenModule = initWasmModule(optipng, wasmUrl);
const module = await emscriptenModule;
const resultView = module.compress(data, options);

View File

@@ -0,0 +1,13 @@
export interface EncodeOptions {
level: number;
}
export interface EncoderState { type: typeof type; options: EncodeOptions; }
export const type = 'oxipng';
export const label = 'OxiPNG';
export const mimeType = 'image/png';
export const extension = 'png';
export const defaultOptions: EncodeOptions = {
level: 6,
};

View File

@@ -0,0 +1,18 @@
import * as oxipng from '../../../codecs/oxipng/pkg/oxipng_manual';
import { EncodeOptions } from './encoder-meta';
export async function compress(data: BufferSource, { level }: EncodeOptions): Promise<ArrayBuffer> {
let buffer: ArrayBuffer;
if (ArrayBuffer.isView(data)) {
buffer = data.buffer;
} else {
buffer = data;
}
debugger;
const resultView = await oxipng.compress(new Uint8Array(buffer), level);
const result = new Uint8Array(resultView);
// wasm cant run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
return result.buffer as ArrayBuffer;
}

View File

@@ -0,0 +1,42 @@
import { h, Component } from 'preact';
import { bind } from '../../lib/initial-util';
import { inputFieldValueAsNumber, preventDefault } from '../../lib/util';
import { EncodeOptions } from './encoder-meta';
import Range from '../../components/range';
import * as style from '../../components/Options/style.scss';
type Props = {
options: EncodeOptions;
onChange(newOptions: EncodeOptions): void;
};
export default class OxiPNGEncoderOptions extends Component<Props, {}> {
@bind
onChange(event: Event) {
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
const options: EncodeOptions = {
level: inputFieldValueAsNumber(form.level),
};
this.props.onChange(options);
}
render({ options }: Props) {
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<div class={style.optionOneCell}>
<Range
name="level"
min="0"
max="9"
step="1"
value={options.level}
onInput={this.onChange}
>
Effort:
</Range>
</div>
</form>
);
}
}

View File

@@ -1,13 +1,12 @@
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,
): Promise<ArrayBuffer> {
const { encode } = await import(
/* webpackChunkName: "process-mozjpeg-enc" */
'../mozjpeg/encoder');
'../mozjpeg/encoder',
);
return encode(data, options);
}
@@ -16,7 +15,8 @@ async function quantize(
): Promise<ImageData> {
const { process } = await import(
/* webpackChunkName: "process-imagequant" */
'../imagequant/processor');
'../imagequant/processor',
);
return process(data, opts);
}
@@ -25,39 +25,29 @@ async function rotate(
): Promise<ImageData> {
const { rotate } = await import(
/* webpackChunkName: "process-rotate" */
'../rotate/processor');
'../rotate/processor',
);
return rotate(data, opts);
}
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');
return resize(data, opts);
}
async function optiPngEncode(
data: BufferSource, options: import('../optipng/encoder-meta').EncodeOptions,
): Promise<ArrayBuffer> {
const { compress } = await import(
/* webpackChunkName: "process-optipng" */
'../optipng/encoder');
'../optipng/encoder',
);
return compress(data, options);
}
async function oxiPngEncode(
data: BufferSource, options: import('../oxipng/encoder-meta').EncodeOptions,
): Promise<ArrayBuffer> {
const { compress } = await import(
/* webpackChunkName: "process-optipng" */
'../oxipng/encoder',
);
return compress(data, options);
}
@@ -66,14 +56,16 @@ async function webpEncode(
): Promise<ArrayBuffer> {
const { encode } = await import(
/* webpackChunkName: "process-webp-enc" */
'../webp/encoder');
'../webp/encoder',
);
return encode(data, options);
}
async function webpDecode(data: ArrayBuffer): Promise<ImageData> {
const { decode } = await import(
/* webpackChunkName: "process-webp-dec" */
'../webp/decoder');
'../webp/decoder',
);
return decode(data);
}
@@ -81,8 +73,8 @@ const exports = {
mozjpegEncode,
quantize,
rotate,
resize,
optiPngEncode,
oxiPngEncode,
webpEncode,
webpDecode,
};

View File

@@ -3,11 +3,12 @@ import { QuantizeOptions } from './imagequant/processor-meta';
import { canvasEncode, blobToArrayBuffer } from '../lib/util';
import { EncodeOptions as MozJPEGEncoderOptions } from './mozjpeg/encoder-meta';
import { EncodeOptions as OptiPNGEncoderOptions } from './optipng/encoder-meta';
import { EncodeOptions as OxiPNGEncoderOptions } from './oxipng/encoder-meta';
import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta';
import { EncodeOptions as BrowserJPEGOptions } from './browser-jpeg/encoder-meta';
import { EncodeOptions as BrowserWebpEncodeOptions } from './browser-webp/encoder-meta';
import { BrowserResizeOptions, VectorResizeOptions } from './resize/processor-meta';
import { browserResize, vectorResize } from './resize/processor-sync';
import { BitmapResizeOptions, VectorResizeOptions } from './resize/processor-meta';
import { resize, vectorResize } from './resize/processor';
import * as browserBMP from './browser-bmp/encoder';
import * as browserPNG from './browser-png/encoder';
import * as browserJPEG from './browser-jpeg/encoder';
@@ -16,7 +17,6 @@ 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;
@@ -95,7 +95,14 @@ 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(this.terminateWorker, workerTimeout);
this._workerTimeoutId = self.setTimeout(
() => {
if (!this._worker) return;
this._worker.terminate();
this._worker = undefined;
},
workerTimeout,
);
}
/** Abort the current job, if any */
@@ -105,11 +112,7 @@ 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;
@@ -128,13 +131,6 @@ export default class Processor {
return this._workerApi!.rotate(data, opts);
}
@Processor._processingJob({ needsWorker: true })
workerResize(
data: ImageData, opts: import('./resize/processor-meta').WorkerResizeOptions,
): Promise<ImageData> {
return this._workerApi!.resize(data, opts);
}
@Processor._processingJob({ needsWorker: true })
mozjpegEncode(
data: ImageData, opts: MozJPEGEncoderOptions,
@@ -152,6 +148,16 @@ export default class Processor {
return this._workerApi!.optiPngEncode(pngBuffer, opts);
}
@Processor._processingJob({ needsWorker: true })
async oxiPngEncode(
data: ImageData, opts: OxiPNGEncoderOptions,
): Promise<ArrayBuffer> {
// OptiPNG expects PNG input.
const pngBlob = await canvasEncode(data, 'image/png');
const pngBuffer = await blobToArrayBuffer(pngBlob);
return this._workerApi!.oxiPngEncode(pngBuffer, opts);
}
@Processor._processingJob({ needsWorker: true })
webpEncode(data: ImageData, opts: WebPEncoderOptions): Promise<ArrayBuffer> {
return this._workerApi!.webpEncode(data, opts);
@@ -207,9 +213,9 @@ export default class Processor {
// Synchronous jobs
resize(data: ImageData, opts: BrowserResizeOptions) {
resize(data: ImageData, opts: BitmapResizeOptions) {
this.abortCurrent();
return browserResize(data, opts);
return resize(data, opts);
}
vectorResize(data: HTMLImageElement, opts: VectorResizeOptions) {

View File

@@ -1,10 +1,8 @@
import { h, Component } from 'preact';
import linkState from 'linkstate';
import { bind, linkRef } from '../../lib/initial-util';
import {
inputFieldValueAsNumber, inputFieldValue, preventDefault, inputFieldChecked,
} from '../../lib/util';
import { ResizeOptions, isWorkerOptions } from './processor-meta';
import { inputFieldValueAsNumber, inputFieldValue, preventDefault } from '../../lib/util';
import { ResizeOptions } from './processor-meta';
import * as style from '../../components/Options/style.scss';
import Checkbox from '../../components/checkbox';
import Expander from '../../components/expander';
@@ -12,9 +10,8 @@ import Select from '../../components/select';
interface Props {
isVector: Boolean;
inputWidth: number;
inputHeight: number;
options: ResizeOptions;
aspect: number;
onChange(newOptions: ResizeOptions): void;
}
@@ -22,21 +19,12 @@ interface State {
maintainAspect: boolean;
}
const sizePresets = [0.25, 0.3333, 0.5, 1, 2, 3, 4];
export default class ResizerOptions extends Component<Props, State> {
state: State = {
maintainAspect: true,
};
private form?: HTMLFormElement;
private presetWidths: { [idx: number]: number } = {};
private presetHeights: { [idx: number]: number } = {};
constructor(props: Props) {
super(props);
this.generatePresetValues(props.inputWidth, props.inputHeight);
}
form?: HTMLFormElement;
private reportOptions() {
const form = this.form!;
@@ -50,8 +38,6 @@ export default class ResizerOptions extends Component<Props, State> {
width: inputFieldValueAsNumber(width),
height: inputFieldValueAsNumber(height),
method: form.resizeMethod.value,
premultiply: inputFieldChecked(form.premultiply, true),
linearRGB: inputFieldChecked(form.linearRGB, true),
// Casting, as the formfield only returns the correct values.
fitMethod: inputFieldValue(form.fitMethod, options.fitMethod) as ResizeOptions['fitMethod'],
};
@@ -63,31 +49,18 @@ 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.getAspect());
this.form!.height.value = Math.round(Number(this.form!.width.value) / this.props.aspect);
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.getAspect());
this.form!.height.value = Math.round(width / this.props.aspect);
}
this.reportOptions();
@@ -97,44 +70,12 @@ 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.getAspect());
this.form!.width.value = Math.round(height * this.props.aspect);
}
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}>
@@ -146,26 +87,12 @@ export default class ResizerOptions extends Component<Props, State> {
onChange={this.onChange}
>
{isVector && <option value="vector">Vector</option>}
<option value="lanczos3">Lanczos3</option>
<option value="mitchell">Mitchell</option>
<option value="catrom">Catmull-Rom</option>
<option value="triangle">Triangle (bilinear)</option>
<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
@@ -190,30 +117,6 @@ export default class ResizerOptions extends Component<Props, State> {
onInput={this.onHeightInput}
/>
</label>
<Expander>
{isWorkerOptions(options) ?
<label class={style.optionInputFirst}>
<Checkbox
name="premultiply"
checked={options.premultiply}
onChange={this.onChange}
/>
Premultiply alpha channel
</label>
: null
}
{isWorkerOptions(options) ?
<label class={style.optionInputFirst}>
<Checkbox
name="linearRGB"
checked={options.linearRGB}
onChange={this.onChange}
/>
Linear RGB
</label>
: null
}
</Expander>
<label class={style.optionInputFirst}>
<Checkbox
name="maintainAspect"

View File

@@ -1,75 +1,26 @@
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',
];
type BitmapResizeMethods = 'browser-pixelated' | 'browser-low' | 'browser-medium' | 'browser-high';
export type ResizeOptions =
| BrowserResizeOptions
| WorkerResizeOptions
| VectorResizeOptions;
export interface ResizeOptionsCommon {
export interface ResizeOptions {
width: number;
height: number;
method: 'vector' | BitmapResizeMethods;
fitMethod: 'stretch' | 'contain';
}
export interface BrowserResizeOptions extends ResizeOptionsCommon {
method: BrowserResizeMethods;
export interface BitmapResizeOptions extends ResizeOptions {
method: BitmapResizeMethods;
}
export interface WorkerResizeOptions extends ResizeOptionsCommon {
method: WorkerResizeMethods;
premultiply: boolean;
linearRGB: boolean;
}
export interface VectorResizeOptions extends ResizeOptionsCommon {
export interface VectorResizeOptions extends ResizeOptions {
method: 'vector';
}
/**
* Return whether a set of options are worker resize options.
*
* @param opts
*/
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.
width: 1,
height: 1,
// This will be set to 'vector' if the input is SVG.
method: 'lanczos3',
method: 'browser-high',
fitMethod: 'stretch',
premultiply: true,
linearRGB: true,
};

View File

@@ -1,35 +0,0 @@
import { nativeResize, NativeResizeMethod, drawableToImageData } from '../../lib/util';
import { BrowserResizeOptions, VectorResizeOptions } from './processor-meta';
import { getContainOffsets } from './util';
export function browserResize(data: ImageData, opts: BrowserResizeOptions): ImageData {
let sx = 0;
let sy = 0;
let sw = data.width;
let sh = data.height;
if (opts.fitMethod === 'contain') {
({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height));
}
return nativeResize(
data, sx, sy, sw, sh, opts.width, opts.height,
opts.method.slice('browser-'.length) as NativeResizeMethod,
);
}
export function vectorResize(data: HTMLImageElement, opts: VectorResizeOptions): ImageData {
let sx = 0;
let sy = 0;
let sw = data.width;
let sh = data.height;
if (opts.fitMethod === 'contain') {
({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height));
}
return drawableToImageData(data, {
sx, sy, sw, sh,
width: opts.width, height: opts.height,
});
}

View File

@@ -1,39 +1,49 @@
import { WorkerResizeOptions } from './processor-meta';
import { getContainOffsets } from './util';
import { resize as codecResize } from '../../../codecs/resize/pkg';
import { nativeResize, NativeResizeMethod, drawableToImageData } from '../../lib/util';
import { BitmapResizeOptions, VectorResizeOptions } from './processor-meta';
function crop(data: ImageData, sx: number, sy: number, sw: number, sh: number): ImageData {
const inputPixels = new Uint32Array(data.data.buffer);
function getContainOffsets(sw: number, sh: number, dw: number, dh: number) {
const currentAspect = sw / sh;
const endAspect = dw / dh;
// Copy within the same buffer for speed and memory efficiency.
for (let y = 0; y < sh; y += 1) {
const start = ((y + sy) * data.width) + sx;
inputPixels.copyWithin(y * sw, start, start + sw);
if (endAspect > currentAspect) {
const newSh = sw / endAspect;
const newSy = (sh - newSh) / 2;
return { sw, sh: newSh, sx: 0, sy: newSy };
}
return new ImageData(
new Uint8ClampedArray(inputPixels.buffer.slice(0, sw * sh * 4)),
sw, sh,
);
const newSw = sh * endAspect;
const newSx = (sw - newSw) / 2;
return { sh, sw: newSw, sx: newSx, sy: 0 };
}
/** Resize methods by index */
const resizeMethods: WorkerResizeOptions['method'][] = [
'triangle', 'catrom', 'mitchell', 'lanczos3',
];
export async function resize(data: ImageData, opts: WorkerResizeOptions): Promise<ImageData> {
let input = data;
export function resize(data: ImageData, opts: BitmapResizeOptions): ImageData {
let sx = 0;
let sy = 0;
let sw = data.width;
let sh = data.height;
if (opts.fitMethod === 'contain') {
const { sx, sy, sw, sh } = getContainOffsets(data.width, data.height, opts.width, opts.height);
input = crop(input, Math.round(sx), Math.round(sy), Math.round(sw), Math.round(sh));
({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height));
}
const result = codecResize(
new Uint8Array(input.data.buffer), input.width, input.height, opts.width, opts.height,
resizeMethods.indexOf(opts.method), opts.premultiply, opts.linearRGB,
return nativeResize(
data, sx, sy, sw, sh, opts.width, opts.height,
opts.method.slice('browser-'.length) as NativeResizeMethod,
);
return new ImageData(new Uint8ClampedArray(result.buffer), opts.width, opts.height);
}
export function vectorResize(data: HTMLImageElement, opts: VectorResizeOptions): ImageData {
let sx = 0;
let sy = 0;
let sw = data.width;
let sh = data.height;
if (opts.fitMethod === 'contain') {
({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height));
}
return drawableToImageData(data, {
sx, sy, sw, sh,
width: opts.width, height: opts.height,
});
}

View File

@@ -1,14 +0,0 @@
export function getContainOffsets(sw: number, sh: number, dw: number, dh: number) {
const currentAspect = sw / sh;
const endAspect = dw / dh;
if (endAspect > currentAspect) {
const newSh = sw / endAspect;
const newSy = (sh - newSh) / 2;
return { sw, sh: newSh, sx: 0, sy: newSy };
}
const newSw = sh * endAspect;
const newSx = (sw - newSw) / 2;
return { sh, sw: newSw, sx: newSx, sy: 0 };
}

View File

@@ -3,10 +3,3 @@ export interface RotateOptions {
}
export const defaultOptions: RotateOptions = { rotate: 0 };
export interface RotateModuleInstance {
exports: {
memory: WebAssembly.Memory;
rotate(width: number, height: number, rotate: 0 | 90 | 180 | 270): void;
};
}

View File

@@ -1,43 +1,73 @@
import wasmUrl from '../../../codecs/rotate/rotate.wasm';
import { RotateOptions, RotateModuleInstance } from './processor-meta';
import { RotateOptions } from './processor-meta';
// We are loading a 500B module here. Loading the code to feature-detect
// `instantiateStreaming` probably takes longer to load than the time we save by
// using `instantiateStreaming` in the first place. So lets just use
// `ArrayBuffer`s here.
const instancePromise = fetch(wasmUrl)
.then(r => r.arrayBuffer())
.then(buf => WebAssembly.instantiate(buf));
export function rotate(data: ImageData, opts: RotateOptions): ImageData {
const { rotate } = opts;
const flipDimensions = rotate % 180 !== 0;
const { width: inputWidth, height: inputHeight } = data;
const outputWidth = flipDimensions ? inputHeight : inputWidth;
const outputHeight = flipDimensions ? inputWidth : inputHeight;
const out = new ImageData(outputWidth, outputHeight);
let i = 0;
export async function rotate(
data: ImageData,
opts: RotateOptions,
): Promise<ImageData> {
const { instance } = (await instancePromise) as {
instance: RotateModuleInstance;
};
// In the straight-copy case, d1 is x, d2 is y.
// x starts at 0 and increases.
// y starts at 0 and increases.
let d1Start = 0;
let d1Limit = inputWidth;
let d1Advance = 1;
let d1Multiplier = 1;
let d2Start = 0;
let d2Limit = inputHeight;
let d2Advance = 1;
let d2Multiplier = inputWidth;
// Number of wasm memory pages (á 64KiB) needed to store the image twice.
const bytesPerImage = data.width * data.height * 4;
const numPagesNeeded = Math.ceil((bytesPerImage * 2 + 8) / (64 * 1024));
// Only count full pages, just to be safe.
const numPagesAvailable = Math.floor(
instance.exports.memory.buffer.byteLength / (64 * 1024),
);
const additionalPagesToAllocate = numPagesNeeded - numPagesAvailable;
if (additionalPagesToAllocate > 0) {
instance.exports.memory.grow(additionalPagesToAllocate);
if (rotate === 90) {
// d1 is y, d2 is x.
// y starts at its max value and decreases.
// x starts at 0 and increases.
d1Start = inputHeight - 1;
d1Limit = inputHeight;
d1Advance = -1;
d1Multiplier = inputWidth;
d2Start = 0;
d2Limit = inputWidth;
d2Advance = 1;
d2Multiplier = 1;
} else if (rotate === 180) {
// d1 is x, d2 is y.
// x starts at its max and decreases.
// y starts at its max and decreases.
d1Start = inputWidth - 1;
d1Limit = inputWidth;
d1Advance = -1;
d1Multiplier = 1;
d2Start = inputHeight - 1;
d2Limit = inputHeight;
d2Advance = -1;
d2Multiplier = inputWidth;
} else if (rotate === 270) {
// d1 is y, d2 is x.
// y starts at 0 and increases.
// x starts at its max and decreases.
d1Start = 0;
d1Limit = inputHeight;
d1Advance = 1;
d1Multiplier = inputWidth;
d2Start = inputWidth - 1;
d2Limit = inputWidth;
d2Advance = -1;
d2Multiplier = 1;
}
const view = new Uint8ClampedArray(instance.exports.memory.buffer);
view.set(data.data, 8);
instance.exports.rotate(data.width, data.height, opts.rotate);
const flipDimensions = opts.rotate % 180 !== 0;
return new ImageData(
view.slice(bytesPerImage + 8, bytesPerImage * 2 + 8),
flipDimensions ? data.height : data.width,
flipDimensions ? data.width : data.height,
);
const inB = new Uint32Array(data.data.buffer);
const outB = new Uint32Array(out.data.buffer);
for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
const start = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
outB[i] = inB[start];
i += 1;
}
}
return out;
}

View File

@@ -2,7 +2,7 @@ type ModuleFactory<M extends EmscriptenWasm.Module> = (
opts: EmscriptenWasm.ModuleOpts,
) => M;
export function initEmscriptenModule<T extends EmscriptenWasm.Module>(
export function initWasmModule<T extends EmscriptenWasm.Module>(
moduleFactory: ModuleFactory<T>,
wasmUrl: string,
): Promise<T> {
@@ -25,12 +25,3 @@ 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);
}

View File

@@ -1,11 +1,11 @@
import webp_dec, { WebPModule } from '../../../codecs/webp_dec/webp_dec';
import wasmUrl from '../../../codecs/webp_dec/webp_dec.wasm';
import { initEmscriptenModule } from '../util';
import { initWasmModule } from '../util';
let emscriptenModule: Promise<WebPModule>;
export async function decode(data: ArrayBuffer): Promise<ImageData> {
if (!emscriptenModule) emscriptenModule = initEmscriptenModule(webp_dec, wasmUrl);
if (!emscriptenModule) emscriptenModule = initWasmModule(webp_dec, wasmUrl);
const module = await emscriptenModule;
const rawImage = module.decode(data);

View File

@@ -1,12 +1,12 @@
import webp_enc, { WebPModule } from '../../../codecs/webp_enc/webp_enc';
import wasmUrl from '../../../codecs/webp_enc/webp_enc.wasm';
import { EncodeOptions } from './encoder-meta';
import { initEmscriptenModule } from '../util';
import { initWasmModule } from '../util';
let emscriptenModule: Promise<WebPModule>;
export async function encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> {
if (!emscriptenModule) emscriptenModule = initEmscriptenModule(webp_enc, wasmUrl);
if (!emscriptenModule) emscriptenModule = initWasmModule(webp_enc, wasmUrl);
const module = await emscriptenModule;
const resultView = module.encode(data.data, data.width, data.height, options);

View File

@@ -13,11 +13,12 @@ const ROUTE_EDITOR = '/editor';
const compressPromise = import(
/* webpackChunkName: "main-app" */
'../compress');
const swBridgePromise = import(
/* webpackChunkName: "sw-bridge" */
'../../lib/sw-bridge');
'../compress',
);
const offlinerPromise = import(
/* webpackChunkName: "offliner" */
'../../lib/offliner',
);
function back() {
window.history.back();
@@ -26,7 +27,6 @@ function back() {
interface Props {}
interface State {
awaitingShareTarget: boolean;
file?: File | Fileish;
isEditorOpen: Boolean;
Compress?: typeof import('../compress').default;
@@ -34,7 +34,6 @@ interface State {
export default class App extends Component<Props, State> {
state: State = {
awaitingShareTarget: new URL(location.href).searchParams.has('share-target'),
isEditorOpen: false,
file: undefined,
Compress: undefined,
@@ -51,15 +50,7 @@ export default class App extends Component<Props, State> {
this.showSnack('Failed to load app');
});
swBridgePromise.then(async ({ offliner, getSharedImage }) => {
offliner(this.showSnack);
if (!this.state.awaitingShareTarget) return;
const file = await getSharedImage();
// Remove the ?share-target from the URL
history.replaceState('', '', '/');
this.openEditor();
this.setState({ file, awaitingShareTarget: false });
});
offlinerPromise.then(({ offliner }) => offliner(this.showSnack));
// In development, persist application state across hot reloads:
if (process.env.NODE_ENV === 'development') {
@@ -83,9 +74,8 @@ export default class App extends Component<Props, State> {
}
@bind
private onFileDrop({ files }: FileDropEvent) {
if (!files || files.length === 0) return;
const file = files[0];
private onFileDrop({ file }: FileDropEvent) {
if (!file) return;
this.openEditor();
this.setState({ file });
}
@@ -110,25 +100,19 @@ export default class App extends Component<Props, State> {
@bind
private openEditor() {
if (this.state.isEditorOpen) return;
// Change path, but preserve query string.
const editorURL = new URL(location.href);
editorURL.pathname = ROUTE_EDITOR;
history.pushState(null, '', editorURL.href);
history.pushState(null, '', ROUTE_EDITOR);
this.setState({ isEditorOpen: true });
}
render({}: Props, { file, isEditorOpen, Compress, awaitingShareTarget }: State) {
const showSpinner = awaitingShareTarget || (isEditorOpen && !Compress);
render({}: Props, { file, isEditorOpen, Compress }: State) {
return (
<div id="app" class={style.app}>
<file-drop accept="image/*" onfiledrop={this.onFileDrop} class={style.drop}>
{
showSpinner
? <loading-spinner class={style.appLoader}/>
: isEditorOpen
? Compress && <Compress file={file!} showSnack={this.showSnack} onBack={back} />
: <Intro onFile={this.onIntroPickFile} showSnack={this.showSnack} />
{!isEditorOpen
? <Intro onFile={this.onIntroPickFile} showSnack={this.showSnack} />
: (Compress)
? <Compress file={file!} showSnack={this.showSnack} onBack={back} />
: <loading-spinner class={style.appLoader}/>
}
<snack-bar ref={linkRef(this, 'snackbar')} />
</file-drop>

View File

@@ -146,14 +146,12 @@ export default class Options extends Component<Props, State> {
{preprocessorState.resize.enabled ?
<ResizeOptionsComponent
isVector={Boolean(source && source.vectorImage)}
inputWidth={source ? source.processed.width : 1}
inputHeight={source ? source.processed.height : 1}
aspect={source ? source.processed.width / source.processed.height : 1}
options={preprocessorState.resize}
onChange={this.onResizeOptionsChange}
/>
: null}
</Expander>
<label class={style.sectionEnabler}>
<Checkbox
name="quantizer.enable"
@@ -180,7 +178,6 @@ export default class Options extends Component<Props, State> {
{encoderSupportMap ?
<Select value={encoderState.type} onChange={this.onEncoderTypeChange} large>
{encoders.filter(encoder => encoderSupportMap[encoder.type]).map(encoder => (
// tslint:disable-next-line:jsx-key
<option value={encoder.type}>{encoder.label}</option>
))}
</Select>

View File

@@ -8,6 +8,7 @@ import Options from '../Options';
import ResultCache from './result-cache';
import * as identity from '../../codecs/identity/encoder-meta';
import * as optiPNG from '../../codecs/optipng/encoder-meta';
import * as oxiPNG from '../../codecs/oxipng/encoder-meta';
import * as mozJPEG from '../../codecs/mozjpeg/encoder-meta';
import * as webP from '../../codecs/webp/encoder-meta';
import * as browserPNG from '../../codecs/browser-png/encoder-meta';
@@ -18,14 +19,20 @@ import * as browserTIFF from '../../codecs/browser-tiff/encoder-meta';
import * as browserJP2 from '../../codecs/browser-jp2/encoder-meta';
import * as browserBMP from '../../codecs/browser-bmp/encoder-meta';
import * as browserPDF from '../../codecs/browser-pdf/encoder-meta';
import { EncoderState, EncoderType, EncoderOptions, encoderMap } from '../../codecs/encoders';
import { PreprocessorState, defaultPreprocessorState } from '../../codecs/preprocessors';
import {
EncoderState,
EncoderType,
EncoderOptions,
encoderMap,
} from '../../codecs/encoders';
import {
PreprocessorState,
defaultPreprocessorState,
} from '../../codecs/preprocessors';
import { decodeImage } from '../../codecs/decoders';
import { cleanMerge, cleanSet } from '../../lib/clean-modify';
import Processor from '../../codecs/processor';
import {
BrowserResizeOptions, isWorkerOptions as isWorkerResizeOptions, isHqx, WorkerResizeOptions,
} from '../../codecs/resize/processor-meta';
import { VectorResizeOptions, BitmapResizeOptions } from '../../codecs/resize/processor-meta';
import './custom-els/MultiPanel';
import Results from '../results';
import { ExpandIcon, CopyAcrossIconProps } from '../../lib/icons';
@@ -104,24 +111,10 @@ async function preprocessImage(
if (preprocessData.resize.method === 'vector' && source.vectorImage) {
result = processor.vectorResize(
source.vectorImage,
preprocessData.resize,
preprocessData.resize as VectorResizeOptions,
);
} 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-bindgens 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 {
result = processor.resize(result, preprocessData.resize as BrowserResizeOptions);
result = processor.resize(result, preprocessData.resize as BitmapResizeOptions);
}
}
if (preprocessData.quantizer.enabled) {
@@ -139,6 +132,7 @@ async function compressImage(
const compressedData = await (() => {
switch (encodeData.type) {
case optiPNG.type: return processor.optiPngEncode(image, encodeData.options);
case oxiPNG.type: return processor.oxiPngEncode(image, encodeData.options);
case mozJPEG.type: return processor.mozjpegEncode(image, encodeData.options);
case webP.type: return processor.webpEncode(image, encodeData.options);
case browserPNG.type: return processor.browserPngEncode(image);
@@ -256,7 +250,7 @@ export default class Compress extends Component<Props, State> {
this.widthQuery.addListener(this.onMobileWidthChange);
this.updateFile(props.file);
import('../../lib/sw-bridge').then(({ mainAppLoaded }) => mainAppLoaded());
import('../../lib/offliner').then(({ mainAppLoaded }) => mainAppLoaded());
}
@bind
@@ -449,7 +443,7 @@ export default class Compress extends Component<Props, State> {
newState = cleanMerge(newState, `sides.${i}.latestSettings.preprocessorState.resize`, {
width: processed.width,
height: processed.height,
method: vectorImage ? 'vector' : 'lanczos3',
method: vectorImage ? 'vector' : 'browser-high',
});
}
@@ -579,7 +573,6 @@ export default class Compress extends Component<Props, State> {
const [leftImageData, rightImageData] = sides.map(i => i.data);
const options = sides.map((side, index) => (
// tslint:disable-next-line:jsx-key
<Options
source={source}
mobileView={mobileView}
@@ -595,7 +588,6 @@ export default class Compress extends Component<Props, State> {
(mobileView ? ['down', 'up'] : ['right', 'left']) as CopyAcrossIconProps['copyDirection'][];
const results = sides.map((side, index) => (
// tslint:disable-next-line:jsx-key
<Results
downloadUrl={side.downloadUrl}
imageFile={side.file}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -53,17 +53,11 @@ export default class Intro extends Component<Props, State> {
state: State = {};
private fileInput?: HTMLInputElement;
@bind
private resetFileInput() {
this.fileInput!.value = '';
}
@bind
private onFileChange(event: Event): void {
const fileInput = event.target as HTMLInputElement;
const file = fileInput.files && fileInput.files[0];
if (!file) return;
this.resetFileInput();
this.props.onFile(file);
}

View File

@@ -7,7 +7,8 @@ function init() {
if (!('customElements' in self)) {
import(
/* webpackChunkName: "wc-polyfill" */
'@webcomponents/custom-elements').then(init);
'@webcomponents/custom-elements',
).then(init);
} else {
init();
}

View File

@@ -40,23 +40,6 @@ async function updateReady(reg: ServiceWorkerRegistration): Promise<void> {
});
}
/** Wait for a shared image */
export function getSharedImage(): Promise<File> {
return new Promise((resolve) => {
const onmessage = (event: MessageEvent) => {
if (event.data.action !== 'load-image') return;
resolve(event.data.file);
navigator.serviceWorker.removeEventListener('message', onmessage);
};
navigator.serviceWorker.addEventListener('message', onmessage);
// This message is picked up by the service worker - it's how it knows we're ready to receive
// the file.
navigator.serviceWorker.controller!.postMessage('share-ready');
});
}
/** Set up the service worker and monitor changes */
export async function offliner(showSnack: SnackBarElement['showSnackbar']) {
// This needs to be a typeof because Webpack.

View File

@@ -12,18 +12,5 @@
"type": "image/png",
"sizes": "1024x1024"
}
],
"share_target": {
"action": "/?share-target",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"files": [
{
"name": "file",
"accept": ["image/*"]
}
]
}
}
}

View File

@@ -1,6 +1,5 @@
import {
cacheOrNetworkAndCache, cleanupCache, cacheOrNetwork, cacheBasics, cacheAdditionalProcessors,
serveShareTarget,
} from './util';
import { get } from 'idb-keyval';
@@ -41,23 +40,14 @@ self.addEventListener('activate', (event) => {
});
self.addEventListener('fetch', (event) => {
// We only care about GET.
if (event.request.method !== 'GET') return;
const url = new URL(event.request.url);
// Don't care about other-origin URLs
if (url.origin !== location.origin) return;
if (
url.pathname === '/' &&
url.searchParams.has('share-target') &&
event.request.method === 'POST'
) {
serveShareTarget(event);
return;
}
// We only care about GET from here on in.
if (event.request.method !== 'GET') return;
if (url.pathname.startsWith('/demo-') || url.pathname.startsWith('/wc-polyfill')) {
cacheOrNetworkAndCache(event, dynamicCache);
cleanupCache(event, dynamicCache, BUILD_ASSETS);

View File

@@ -1,11 +1,8 @@
import webpDataUrl from 'url-loader!../codecs/tiny.webp';
// Give TypeScript the correct global.
declare var self: ServiceWorkerGlobalScope;
export function cacheOrNetwork(event: FetchEvent): void {
event.respondWith(async function () {
const cachedResponse = await caches.match(event.request, { ignoreSearch: true });
const cachedResponse = await caches.match(event.request);
return cachedResponse || fetch(event.request);
}());
}
@@ -32,23 +29,6 @@ export function cacheOrNetworkAndCache(event: FetchEvent, cacheName: string): vo
}());
}
export function serveShareTarget(event: FetchEvent): void {
const dataPromise = event.request.formData();
// Redirect so the user can refresh the page without resending data.
// @ts-ignore It doesn't like me giving a response to respondWith, although it's allowed.
event.respondWith(Response.redirect('/?share-target'));
event.waitUntil(async function () {
// The page sends this message to tell the service worker it's ready to receive the file.
await nextMessage('share-ready');
const client = await self.clients.get(event.resultingClientId);
const data = await dataPromise;
const file = data.get('file');
client.postMessage({ file, action: 'load-image' });
}());
}
export function cleanupCache(event: FetchEvent, cacheName: string, keepAssets: string[]) {
event.waitUntil(async function () {
const cache = await caches.open(cacheName);
@@ -124,26 +104,3 @@ export async function cacheAdditionalProcessors(cacheName: string, buildAssets:
const cache = await caches.open(cacheName);
await cache.addAll(toCache);
}
const nextMessageResolveMap = new Map<string, (() => void)[]>();
/**
* Wait on a message with a particular event.data value.
*
* @param dataVal The event.data value.
*/
function nextMessage(dataVal: string): Promise<void> {
return new Promise((resolve) => {
if (!nextMessageResolveMap.has(dataVal)) {
nextMessageResolveMap.set(dataVal, []);
}
nextMessageResolveMap.get(dataVal)!.push(resolve);
});
}
self.addEventListener('message', (event) => {
const resolvers = nextMessageResolveMap.get(event.data);
if (!resolvers) return;
nextMessageResolveMap.delete(event.data);
for (const resolve of resolvers) resolve();
});

View File

@@ -12,8 +12,7 @@
"variable-name": [true, "check-format", "allow-leading-underscore"],
"no-duplicate-imports": false,
"prefer-template": [true, "allow-single-concat"],
"import-name": false,
"jsx-key": false
"import-name": false
},
"linterOptions": {
"exclude": [

Some files were not shown because too many files have changed in this diff Show More