Compare commits

..

23 Commits
v1.9.1 ... api

Author SHA1 Message Date
Surma
956e064c8c Only allow iframe embedding 2019-03-20 17:51:32 +00:00
Surma
5270553f27 Fix caching headers for sdk 2019-03-20 17:29:09 +00:00
Surma
3005098f67 Remove that stupid async from setFile() 2019-03-20 17:15:20 +00:00
Surma
9b879609f3 Add CORS to sdk file 2019-03-20 17:15:20 +00:00
Surma
9008d1c380 Use microbundle for SDK 2019-03-20 17:15:19 +00:00
Surma
915f4cd4e6 Revert wepack changes 2019-03-20 17:13:13 +00:00
Surma
7dd842a870 Fixup sdk.js deployment 2019-03-20 17:13:12 +00:00
Surma
2e478b323a Trying to bend webpack to my will 2019-03-20 17:13:11 +00:00
Jason Miller
f24d41ba74 fix prerender-loader with multientry 2019-03-20 17:12:13 +00:00
Surma
cb3c66fc5b Rename loading event 2019-03-20 17:12:12 +00:00
Surma
3315188bb3 Jake code review 2019-03-20 17:12:12 +00:00
Surma
efad4f612f Use MAJOR_VERSION from package.json 2019-03-20 17:12:11 +00:00
Surma
7998cf247b Another fix from rebasing 2019-03-20 17:12:11 +00:00
Surma
0041d24aa3 Create a SideEvent 2019-03-20 17:12:10 +00:00
Surma
2fa2e567a6 Address minor nits 2019-03-20 17:12:09 +00:00
Surma
98f61ba60c Add chunk name 2019-03-20 17:12:09 +00:00
Surma
672c57b61f Switch to an event-based architecture 2019-03-20 17:12:08 +00:00
Surma
bfe74b5fb2 Update for API changes 2019-03-20 17:12:08 +00:00
Surma
1507a44141 Lazy-load API 2019-03-20 17:12:07 +00:00
Surma
bb3bd2d46a Restore compressor settings after load via API 2019-03-20 17:12:06 +00:00
Surma
4df3a7df83 Build a demo batch app 2019-03-20 17:12:06 +00:00
Surma
853b305465 Remove raceyness of getter API 2019-03-20 17:12:05 +00:00
Surma
9a230adc03 Simple API test 2019-03-20 17:12:00 +00:00
60 changed files with 10493 additions and 5370 deletions

2
.nvmrc
View File

@@ -1 +1 @@
10.16.2 10.15.3

View File

@@ -1,6 +1,7 @@
# Long-term cache by default. # Long-term cache by default.
/* /*
Cache-Control: max-age=31536000 Cache-Control: max-age=31536000
Access-Control-Allow-Origin: *
# And here are the exceptions: # And here are the exceptions:
/ /
@@ -9,6 +10,9 @@
/serviceworker.js /serviceworker.js
Cache-Control: no-cache Cache-Control: no-cache
/sdk.mjs
Cache-Control: no-cache
/manifest.json /manifest.json
Cache-Control: must-revalidate, max-age=3600 Cache-Control: must-revalidate, max-age=3600

View File

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

View File

@@ -1,37 +0,0 @@
[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

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;
}

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

@@ -4,40 +4,84 @@ set -e
export OPTIMIZE="-Os" export OPTIMIZE="-Os"
export PREFIX="/src/build" export PREFIX="/src/build"
export CFLAGS="${OPTIMIZE} -I${PREFIX}/include/"
export CPPFLAGS="${OPTIMIZE} -I${PREFIX}/include/"
export LDFLAGS="${OPTIMIZE} -L${PREFIX}/lib/"
apt-get update
apt-get install -qqy autoconf libtool
echo "============================================="
echo "Compiling zlib"
echo "============================================="
test -n "$SKIP_ZLIB" || (
cd node_modules/zlib
emconfigure ./configure --prefix=${PREFIX}/
emmake make
emmake make install
)
echo "============================================="
echo "Compiling zlib done"
echo "============================================="
echo "============================================="
echo "Compiling libpng"
echo "============================================="
test -n "$SKIP_LIBPNG" || (
cd node_modules/libpng
autoreconf -i
emconfigure ./configure --with-zlib-prefix=${PREFIX}/ --prefix=${PREFIX}/
emmake make
emmake make install
)
echo "============================================="
echo "Compiling libpng done"
echo "============================================="
echo "=============================================" echo "============================================="
echo "Compiling optipng" echo "Compiling optipng"
echo "=============================================" echo "============================================="
( (
cd node_modules/optipng emcc \
CFLAGS="${OPTIMIZE} -Isrc/zlib" emconfigure ./configure --prefix=${PREFIX} ${OPTIMIZE} \
emmake make -Wno-implicit-function-declaration \
emmake make install -I ${PREFIX}/include \
mkdir -p ${PREFIX}/lib -I node_modules/optipng/src/opngreduc \
mv ${PREFIX}/bin/optipng ${PREFIX}/lib/liboptipng.so -I node_modules/optipng/src/pngxtern \
-I node_modules/optipng/src/cexcept \
-I node_modules/optipng/src/gifread \
-I node_modules/optipng/src/pnmio \
-I node_modules/optipng/src/minitiff \
--std=c99 -c \
node_modules/optipng/src/opngreduc/*.c \
node_modules/optipng/src/pngxtern/*.c \
node_modules/optipng/src/gifread/*.c \
node_modules/optipng/src/minitiff/*.c \
node_modules/optipng/src/pnmio/*.c \
node_modules/optipng/src/optipng/*.c
emcc \
--bind \
${OPTIMIZE} \
-s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME="optipng"' \
-I ${PREFIX}/include \
-I node_modules/optipng/src/opngreduc \
-I node_modules/optipng/src/pngxtern \
-I node_modules/optipng/src/cexcept \
-I node_modules/optipng/src/gifread \
-I node_modules/optipng/src/pnmio \
-I node_modules/optipng/src/minitiff \
-o "optipng.js" \
--std=c++11 \
optipng.cpp \
*.o \
${PREFIX}/lib/libz.so ${PREFIX}/lib/libpng.a
) )
echo "=============================================" echo "============================================="
echo "Compiling optipng done" echo "Compiling optipng done"
echo "=============================================" echo "============================================="
echo "============================================="
echo "Compiling optipng wrapper"
echo "============================================="
(
emcc \
--bind \
${OPTIMIZE} \
-s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME="optipng"' \
-o "optipng.js" \
--std=c++11 \
optipng.cpp \
${PREFIX}/lib/liboptipng.so
)
echo "============================================="
echo "Compiling optipng wrapper done"
echo "============================================="
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo "Did you update your docker image?" echo "Did you update your docker image?"
echo "Run \`docker pull trzeci/emscripten-upstream\`" echo "Run \`docker pull trzeci/emscripten\`"
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
{ {
"name": "optipng", "name": "optipng",
"scripts": { "scripts": {
"install": "tar-dependency install", "install": "tar-dependency install && napa",
"build": "npm run build:wasm", "build": "npm run build:wasm",
"build:wasm": "docker run --rm -v $(pwd):/src trzeci/emscripten-upstream ./build.sh" "build:wasm": "docker run --rm -v $(pwd):/src -e SKIP_ZLIB=\"${SKIP_ZLIB}\" -e SKIP_LIBPNG=\"${SKIP_LIBPNG}\" trzeci/emscripten ./build.sh"
}, },
"tarDependencies": { "tarDependencies": {
"node_modules/optipng": { "node_modules/optipng": {
@@ -11,7 +11,12 @@
"strip": 1 "strip": 1
} }
}, },
"napa": {
"libpng": "emscripten-ports/libpng",
"zlib": "emscripten-ports/zlib"
},
"dependencies": { "dependencies": {
"napa": "3.0.0",
"tar-dependency": "0.0.3" "tar-dependency": "0.0.3"
} }
} }

View File

@@ -1,5 +1,5 @@
[package] [package]
name = "resize" name = "squooshresize"
version = "0.1.0" version = "0.1.0"
authors = ["Surma <surma@surma.link>"] authors = ["Surma <surma@surma.link>"]

View File

@@ -1,9 +1,18 @@
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 FROM rust
RUN rustup target add wasm32-unknown-unknown RUN rustup install nightly && \
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh rustup target add --toolchain nightly wasm32-unknown-unknown && \
cargo install wasm-pack
RUN mkdir /opt/wabt && \ COPY --from=0 /opt/wabt /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/bin:${PATH}"
ENV PATH="/opt/wabt:${PATH}"
WORKDIR /src WORKDIR /src

View File

@@ -0,0 +1,53 @@
// 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
self = global = this;
load("./pkg/resize.js");
async function init() {
// Adjustable constants.
const inputDimensions = 2000;
const outputDimensions = 1500;
const algorithm = 3; // Lanczos
const iterations = new Array(100);
// Constants. Dont change.
const imageByteSize = inputDimensions * inputDimensions * 4;
const imageBuffer = new Uint8ClampedArray(imageByteSize);
const module = await WebAssembly.compile(readbuffer("./pkg/resize_bg.wasm"));
await wasm_bindgen(module);
[[false, false], [true, false], [false, true], [true, true]].forEach(
opts => {
print(`\npremultiplication: ${opts[0]}`);
print(`color space conversion: ${opts[1]}`);
print(`==============================`);
for (let i = 0; i < 100; i++) {
const start = Date.now();
wasm_bindgen.resize(
imageBuffer,
inputDimensions,
inputDimensions,
outputDimensions,
outputDimensions,
algorithm,
...opts
);
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, e.stack));

View File

@@ -6,9 +6,9 @@ echo "============================================="
echo "Compiling wasm" echo "Compiling wasm"
echo "=============================================" echo "============================================="
( (
wasm-pack build rustup run nightly \
wasm-pack build --target no-modules
wasm-strip pkg/resize_bg.wasm wasm-strip pkg/resize_bg.wasm
rm pkg/.gitignore
) )
echo "=============================================" echo "============================================="
echo "Compiling wasm done" echo "Compiling wasm done"

View File

@@ -2,6 +2,7 @@
"name": "resize", "name": "resize",
"scripts": { "scripts": {
"build:image": "docker build -t squoosh-resize .", "build:image": "docker build -t squoosh-resize .",
"build": "docker run --rm -v $(pwd):/src squoosh-resize ./build.sh" "build": "docker run --rm -v $(pwd):/src squoosh-resize ./build.sh",
"benchmark": "v8 --no-liftoff --no-wasm-tier-up ./benchmark.js"
} }
} }

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 +1,13 @@
/* tslint:disable */ /* tslint:disable */
/** /**
* @param {Uint8Array} input_image * @param {Uint8Array} arg0
* @param {number} input_width * @param {number} arg1
* @param {number} input_height * @param {number} arg2
* @param {number} output_width * @param {number} arg3
* @param {number} output_height * @param {number} arg4
* @param {number} typ_idx * @param {number} arg5
* @param {boolean} premultiply * @param {boolean} arg6
* @param {boolean} color_space_conversion * @param {boolean} arg7
* @returns {Uint8Array} * @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; export function resize(arg0: Uint8Array, arg1: number, arg2: number, arg3: number, arg4: number, arg5: number, arg6: boolean, arg7: boolean): Uint8Array;

View File

@@ -1,50 +1,114 @@
import * as wasm from './resize_bg.wasm'; (function() {
var wasm;
const __exports = {};
let cachegetUint8Memory = null;
function getUint8Memory() { let cachegetUint8Memory = null;
if (cachegetUint8Memory === null || cachegetUint8Memory.buffer !== wasm.memory.buffer) { function getUint8Memory() {
cachegetUint8Memory = new Uint8Array(wasm.memory.buffer); if (cachegetUint8Memory === null || cachegetUint8Memory.buffer !== wasm.memory.buffer) {
cachegetUint8Memory = new Uint8Array(wasm.memory.buffer);
}
return cachegetUint8Memory;
} }
return cachegetUint8Memory;
}
let WASM_VECTOR_LEN = 0; let WASM_VECTOR_LEN = 0;
function passArray8ToWasm(arg) { function passArray8ToWasm(arg) {
const ptr = wasm.__wbindgen_malloc(arg.length * 1); const ptr = wasm.__wbindgen_malloc(arg.length * 1);
getUint8Memory().set(arg, ptr / 1); getUint8Memory().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length; WASM_VECTOR_LEN = arg.length;
return ptr; 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) { function getArrayU8FromWasm(ptr, len) {
return getUint8Memory().subarray(ptr / 1, ptr / 1 + 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;
}
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
* @param {number} arg2
* @param {number} arg3
* @param {number} arg4
* @param {number} arg5
* @param {boolean} arg6
* @param {boolean} arg7
* @returns {Uint8Array}
*/
__exports.resize = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) {
const ptr0 = passArray8ToWasm(arg0);
const len0 = WASM_VECTOR_LEN;
const retptr = globalArgumentPtr();
wasm.resize(retptr, ptr0, len0, arg1, arg2, arg3, arg4, arg5, arg6, arg7);
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;
};
const heap = new Array(32);
heap.fill(undefined);
heap.push(undefined, null, true, false);
let heap_next = heap.length;
function dropObject(idx) {
if (idx < 36) return;
heap[idx] = heap_next;
heap_next = idx;
}
__exports.__wbindgen_object_drop_ref = function(i) { dropObject(i); };
function init(path_or_module) {
let instantiation;
const imports = { './resize': __exports };
if (path_or_module instanceof WebAssembly.Module) {
instantiation = WebAssembly.instantiate(path_or_module, imports)
.then(instance => {
return { instance, module: path_or_module }
});
} else {
const data = fetch(path_or_module);
if (typeof WebAssembly.instantiateStreaming === 'function') {
instantiation = WebAssembly.instantiateStreaming(data, 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 data
.then(r => r.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes, imports));
});
} else {
instantiation = data
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.instantiate(buffer, imports));
}
}
return instantiation.then(({instance}) => {
wasm = init.wasm = instance.exports;
});
};
self.wasm_bindgen = Object.assign(init, __exports);
})();

View File

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

Binary file not shown.

View File

@@ -1,8 +1,17 @@
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 FROM rust
RUN rustup target add wasm32-unknown-unknown RUN rustup install nightly && \
rustup target add --toolchain nightly wasm32-unknown-unknown
RUN mkdir /opt/wabt && \ COPY --from=0 /opt/wabt /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/bin:${PATH}"
ENV PATH="/opt/wabt:${PATH}"
WORKDIR /src WORKDIR /src

View File

@@ -6,7 +6,8 @@ echo "============================================="
echo "Compiling wasm" echo "Compiling wasm"
echo "=============================================" echo "============================================="
( (
cargo build \ rustup run nightly \
cargo build \
--target wasm32-unknown-unknown \ --target wasm32-unknown-unknown \
--release --release
cp target/wasm32-unknown-unknown/release/rotate.wasm . cp target/wasm32-unknown-unknown/release/rotate.wasm .

Binary file not shown.

203
config/size-report.js Normal file
View File

@@ -0,0 +1,203 @@
const path = require('path');
const { URL } = require('url');
const gzipSize = require('gzip-size');
const fetch = require('node-fetch');
const prettyBytes = require('pretty-bytes');
const escapeRE = require('escape-string-regexp');
const readdirp = require('readdirp');
const chalk = new require('chalk').constructor({ level: 4 });
function fetchTravis(path, options = {}) {
const url = new URL(path, 'https://api.travis-ci.org');
url.search = new URLSearchParams(options);
return fetch(url, {
headers: { 'Travis-API-Version': '3' },
});
}
function fetchTravisBuildInfo(user, repo, branch) {
return fetchTravis(`/repo/${encodeURIComponent(`${user}/${repo}`)}/builds`, {
'branch.name': branch,
state: 'passed',
limit: 1,
event_type: 'push',
}).then(r => r.json());
}
function fetchTravisText(path) {
return fetchTravis(path).then(r => r.text());
}
/**
* Recursively-read a directory and turn it into an array of { name, size, gzipSize }
*/
async function dirToInfoArray(startPath) {
const results = await new Promise((resolve, reject) => {
readdirp({ root: startPath }, (err, results) => {
if (err) reject(err); else resolve(results);
});
});
return Promise.all(
results.files.map(async (entry) => ({
name: entry.path,
gzipSize: await gzipSize.file(entry.fullPath),
size: entry.stat.size,
})),
);
}
/**
* Try to treat two entries with different file name hashes as the same file.
*/
function findHashedMatch(name, buildInfo) {
const nameParts = /^(.+\.)[a-f0-9]+(\..+)$/.exec(name);
if (!nameParts) return;
const matchRe = new RegExp(`^${escapeRE(nameParts[1])}[a-f0-9]+${escapeRE(nameParts[2])}$`);
const matchingEntry = buildInfo.find(entry => matchRe.test(entry.name));
return matchingEntry;
}
const buildSizePrefix = '=== BUILD SIZES: ';
const buildSizePrefixRe = new RegExp(`^${escapeRE(buildSizePrefix)}(.+)$`, 'm');
async function getPreviousBuildInfo() {
const buildData = await fetchTravisBuildInfo('GoogleChromeLabs', 'squoosh', 'master');
const jobUrl = buildData.builds[0].jobs[0]['@href'];
const log = await fetchTravisText(jobUrl + '/log.txt');
const reResult = buildSizePrefixRe.exec(log);
if (!reResult) return;
return JSON.parse(reResult[1]);
}
/**
* Generate an array that represents the difference between builds.
* Returns an array of { beforeName, afterName, beforeSize, afterSize }.
* Sizes are gzipped size.
* Before/after properties are missing if resource isn't in the previous/new build.
*/
function getChanges(previousBuildInfo, buildInfo) {
const buildChanges = [];
const alsoInPreviousBuild = new Set();
for (const oldEntry of previousBuildInfo) {
const newEntry = buildInfo.find(entry => entry.name === oldEntry.name) ||
findHashedMatch(oldEntry.name, buildInfo);
// Entry is in previous build, but not the new build.
if (!newEntry) {
buildChanges.push({
beforeName: oldEntry.name,
beforeSize: oldEntry.gzipSize,
});
continue;
}
// Mark this entry so we know we've dealt with it.
alsoInPreviousBuild.add(newEntry);
// If they're the same, just ignore.
// Using size rather than gzip size. I've seen different platforms produce different zipped
// sizes.
if (
oldEntry.size === newEntry.size &&
oldEntry.name === newEntry.name
) continue;
// Entry is in both builds (maybe renamed).
buildChanges.push({
beforeName: oldEntry.name,
afterName: newEntry.name,
beforeSize: oldEntry.gzipSize,
afterSize: newEntry.gzipSize,
});
}
// Look for entries that are only in the new build.
for (const newEntry of buildInfo) {
if (alsoInPreviousBuild.has(newEntry)) continue;
buildChanges.push({
afterName: newEntry.name,
afterSize: newEntry.gzipSize,
});
}
return buildChanges;
}
async function main() {
// Output the current build sizes for later retrieval.
const buildInfo = await dirToInfoArray(__dirname + '/../build');
console.log(buildSizePrefix + JSON.stringify(buildInfo));
console.log('\nBuild change report:');
let previousBuildInfo;
try {
previousBuildInfo = await getPreviousBuildInfo();
} catch (err) {
console.log(` Couldn't parse previous build info`);
return;
}
if (!previousBuildInfo) {
console.log(` Couldn't find previous build info`);
return;
}
const buildChanges = getChanges(previousBuildInfo, buildInfo);
if (buildChanges.length === 0) {
console.log(' No changes');
return;
}
// One letter references, so it's easier to get the spacing right.
const y = chalk.yellow;
const g = chalk.green;
const r = chalk.red;
for (const change of buildChanges) {
// New file.
if (!change.beforeSize) {
console.log(` ${g('ADDED')} ${change.afterName} - ${prettyBytes(change.afterSize)}`);
continue;
}
// Removed file.
if (!change.afterSize) {
console.log(` ${r('REMOVED')} ${change.beforeName} - was ${prettyBytes(change.beforeSize)}`);
continue;
}
// Changed file.
let size;
if (change.beforeSize === change.afterSize) {
// Just renamed.
size = `${prettyBytes(change.afterSize)} -> no change`;
} else {
const color = change.afterSize > change.beforeSize ? r : g;
const sizeDiff = prettyBytes(change.afterSize - change.beforeSize, { signed: true });
const relativeDiff = Math.round((change.afterSize / change.beforeSize) * 1000) / 1000;
size = `${prettyBytes(change.beforeSize)} -> ${prettyBytes(change.afterSize)}` +
' (' +
color(`${sizeDiff}, ${relativeDiff}x`) +
')';
}
console.log(` ${y('CHANGED')} ${change.afterName} - ${size}`);
if (change.beforeName !== change.afterName) {
console.log(` Renamed from: ${change.beforeName}`);
}
}
}
main();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

13348
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,15 @@
{ {
"private": true, "private": true,
"name": "squoosh", "name": "squoosh",
"version": "1.9.1", "version": "1.6.0",
"license": "apache-2.0", "license": "apache-2.0",
"scripts": { "scripts": {
"build:sdk": "microbundle --compress -f es -o build/sdk.mjs -i src/sdk.ts",
"start": "webpack-dev-server --host 0.0.0.0 --hot", "start": "webpack-dev-server --host 0.0.0.0 --hot",
"build": "webpack -p", "build": "webpack -p && npm run build:sdk",
"lint": "tslint -c tslint.json -p tsconfig.json -t verbose", "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}'", "lintfix": "tslint -c tslint.json -p tsconfig.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'",
"sizereport": "sizereport --config" "sizereport": "node config/size-report.js"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@@ -16,61 +17,61 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@types/node": "10.14.15", "@types/node": "10.14.1",
"@types/pretty-bytes": "5.1.0", "@types/pretty-bytes": "5.1.0",
"@types/webassembly-js-api": "0.0.3", "@types/webassembly-js-api": "0.0.2",
"@webcomponents/custom-elements": "1.2.4", "@webcomponents/custom-elements": "1.2.1",
"@webpack-cli/serve": "0.1.8", "@webpack-cli/serve": "0.1.3",
"assets-webpack-plugin": "3.9.10", "assets-webpack-plugin": "3.9.10",
"chalk": "2.4.2", "chalk": "2.4.2",
"chokidar": "3.0.2", "chokidar": "2.1.2",
"classnames": "2.2.6", "classnames": "2.2.6",
"clean-webpack-plugin": "1.0.1", "clean-webpack-plugin": "1.0.1",
"comlink": "3.1.1", "copy-webpack-plugin": "5.0.1",
"copy-webpack-plugin": "5.0.4", "comlink": "^3.2.0",
"critters-webpack-plugin": "2.4.0", "critters-webpack-plugin": "2.3.0",
"css-loader": "1.0.1", "css-loader": "1.0.1",
"ejs": "2.6.2", "ejs": "2.6.1",
"escape-string-regexp": "2.0.0", "escape-string-regexp": "1.0.5",
"exports-loader": "0.7.0", "exports-loader": "0.7.0",
"file-drop-element": "0.2.0", "file-drop-element": "0.2.0",
"file-loader": "4.2.0", "file-loader": "3.0.1",
"gzip-size": "5.1.1", "gzip-size": "5.0.0",
"html-webpack-plugin": "3.2.0", "html-webpack-plugin": "3.2.0",
"husky": "3.0.4", "husky": "1.3.1",
"idb-keyval": "3.2.0", "idb-keyval": "3.1.0",
"linkstate": "1.1.1", "linkstate": "1.1.1",
"loader-utils": "1.2.3", "loader-utils": "1.2.3",
"mini-css-extract-plugin": "0.8.0", "microbundle": "^0.10.1",
"mini-css-extract-plugin": "0.5.0",
"minimatch": "3.0.4", "minimatch": "3.0.4",
"node-fetch": "2.6.0", "node-fetch": "2.3.0",
"node-sass": "4.12.0", "node-sass": "4.11.0",
"optimize-css-assets-webpack-plugin": "5.0.1", "optimize-css-assets-webpack-plugin": "5.0.1",
"pointer-tracker": "2.0.3", "pointer-tracker": "2.0.3",
"preact": "8.4.2", "preact": "8.4.2",
"prerender-loader": "1.3.0", "prerender-loader": "1.3.0",
"pretty-bytes": "5.3.0", "pretty-bytes": "5.1.0",
"progress-bar-webpack-plugin": "1.12.1", "progress-bar-webpack-plugin": "1.12.1",
"raw-loader": "3.1.0", "raw-loader": "2.0.0",
"readdirp": "3.1.2", "readdirp": "2.2.1",
"sass-loader": "7.3.1", "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", "source-map-loader": "0.2.4",
"style-loader": "1.0.0", "style-loader": "0.23.1",
"terser-webpack-plugin": "1.4.1", "terser-webpack-plugin": "1.2.3",
"travis-size-report": "1.1.0", "ts-loader": "5.3.3",
"ts-loader": "6.0.3", "tslint": "5.14.0",
"tslint": "5.19.0",
"tslint-config-airbnb": "5.11.1", "tslint-config-airbnb": "5.11.1",
"tslint-config-semistandard": "8.0.1", "tslint-config-semistandard": "7.0.0",
"tslint-react": "4.0.0", "tslint-react": "3.6.0",
"typed-css-modules": "0.4.2", "typed-css-modules": "0.4.2",
"typescript": "3.5.3", "typescript": "3.3.3333",
"url-loader": "2.1.0", "url-loader": "1.1.2",
"webpack": "4.28.0", "webpack": "4.28.0",
"webpack-bundle-analyzer": "3.4.1", "webpack-bundle-analyzer": "3.1.0",
"webpack-cli": "3.3.4", "webpack-cli": "3.3.0",
"webpack-dev-server": "3.8.0", "webpack-dev-server": "3.2.1",
"worker-plugin": "3.1.0" "worker-plugin": "3.1.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,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,6 +1,4 @@
import { expose } from 'comlink'; import { expose } from 'comlink';
import { isHqx } from '../resize/processor-meta';
import { clamp } from '../util';
async function mozjpegEncode( async function mozjpegEncode(
data: ImageData, options: import('../mozjpeg/encoder-meta').EncodeOptions, data: ImageData, options: import('../mozjpeg/encoder-meta').EncodeOptions,
@@ -33,18 +31,6 @@ async function rotate(
async function resize( async function resize(
data: ImageData, opts: import('../resize/processor-meta').WorkerResizeOptions, data: ImageData, opts: import('../resize/processor-meta').WorkerResizeOptions,
): Promise<ImageData> { ): 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( const { resize } = await import(
/* webpackChunkName: "process-resize" */ /* webpackChunkName: "process-resize" */
'../resize/processor'); '../resize/processor');
@@ -77,15 +63,7 @@ async function webpDecode(data: ArrayBuffer): Promise<ImageData> {
return decode(data); return decode(data);
} }
const exports = { const exports = { mozjpegEncode, quantize, rotate, resize, optiPngEncode, webpEncode, webpDecode };
mozjpegEncode,
quantize,
rotate,
resize,
optiPngEncode,
webpEncode,
webpDecode,
};
export type ProcessorWorkerApi = typeof exports; export type ProcessorWorkerApi = typeof exports;
expose(exports, self); expose(exports, self);

View File

@@ -16,7 +16,6 @@ import * as browserGIF from './browser-gif/encoder';
import * as browserTIFF from './browser-tiff/encoder'; import * as browserTIFF from './browser-tiff/encoder';
import * as browserJP2 from './browser-jp2/encoder'; import * as browserJP2 from './browser-jp2/encoder';
import * as browserPDF from './browser-pdf/encoder'; import * as browserPDF from './browser-pdf/encoder';
import { bind } from '../lib/initial-util';
type ProcessorWorkerApi = import('./processor-worker').ProcessorWorkerApi; type ProcessorWorkerApi = import('./processor-worker').ProcessorWorkerApi;
@@ -95,7 +94,14 @@ export default class Processor {
if (!this._worker) return; if (!this._worker) return;
// If the worker is unused for 10 seconds, remove it to save memory. // 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 */ /** Abort the current job, if any */
@@ -105,11 +111,7 @@ export default class Processor {
this._abortRejector(new DOMException('Aborted', 'AbortError')); this._abortRejector(new DOMException('Aborted', 'AbortError'));
this._abortRejector = undefined; this._abortRejector = undefined;
this._busy = false; this._busy = false;
this.terminateWorker();
}
@bind
terminateWorker() {
if (!this._worker) return; if (!this._worker) return;
this._worker.terminate(); this._worker.terminate();
this._worker = undefined; this._worker = undefined;

View File

@@ -12,9 +12,8 @@ import Select from '../../components/select';
interface Props { interface Props {
isVector: Boolean; isVector: Boolean;
inputWidth: number;
inputHeight: number;
options: ResizeOptions; options: ResizeOptions;
aspect: number;
onChange(newOptions: ResizeOptions): void; onChange(newOptions: ResizeOptions): void;
} }
@@ -22,21 +21,12 @@ interface State {
maintainAspect: boolean; maintainAspect: boolean;
} }
const sizePresets = [0.25, 0.3333, 0.5, 1, 2, 3, 4];
export default class ResizerOptions extends Component<Props, State> { export default class ResizerOptions extends Component<Props, State> {
state: State = { state: State = {
maintainAspect: true, maintainAspect: true,
}; };
private form?: HTMLFormElement; 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() { private reportOptions() {
const form = this.form!; const form = this.form!;
@@ -63,31 +53,18 @@ export default class ResizerOptions extends Component<Props, State> {
this.reportOptions(); this.reportOptions();
} }
private getAspect() {
return this.props.inputWidth / this.props.inputHeight;
}
componentDidUpdate(prevProps: Props, prevState: State) { componentDidUpdate(prevProps: Props, prevState: State) {
if (!prevState.maintainAspect && this.state.maintainAspect) { 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(); this.reportOptions();
} }
} }
componentWillReceiveProps(nextProps: Props) {
if (
this.props.inputWidth !== nextProps.inputWidth ||
this.props.inputHeight !== nextProps.inputHeight
) {
this.generatePresetValues(nextProps.inputWidth, nextProps.inputHeight);
}
}
@bind @bind
private onWidthInput() { private onWidthInput() {
if (this.state.maintainAspect) { if (this.state.maintainAspect) {
const width = inputFieldValueAsNumber(this.form!.width); 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(); this.reportOptions();
@@ -97,44 +74,12 @@ export default class ResizerOptions extends Component<Props, State> {
private onHeightInput() { private onHeightInput() {
if (this.state.maintainAspect) { if (this.state.maintainAspect) {
const height = inputFieldValueAsNumber(this.form!.height); 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(); 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) { render({ options, isVector }: Props, { maintainAspect }: State) {
return ( return (
<form ref={linkRef(this, 'form')} class={style.optionsSection} onSubmit={preventDefault}> <form ref={linkRef(this, 'form')} class={style.optionsSection} onSubmit={preventDefault}>
@@ -150,22 +95,12 @@ export default class ResizerOptions extends Component<Props, State> {
<option value="mitchell">Mitchell</option> <option value="mitchell">Mitchell</option>
<option value="catrom">Catmull-Rom</option> <option value="catrom">Catmull-Rom</option>
<option value="triangle">Triangle (bilinear)</option> <option value="triangle">Triangle (bilinear)</option>
<option value="hqx">hqx (pixel art)</option>
<option value="browser-pixelated">Browser pixelated</option> <option value="browser-pixelated">Browser pixelated</option>
<option value="browser-low">Browser low quality</option> <option value="browser-low">Browser low quality</option>
<option value="browser-medium">Browser medium quality</option> <option value="browser-medium">Browser medium quality</option>
<option value="browser-high">Browser high quality</option> <option value="browser-high">Browser high quality</option>
</Select> </Select>
</label> </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}> <label class={style.optionTextFirst}>
Width: Width:
<input <input

View File

@@ -1,26 +1,8 @@
type BrowserResizeMethods = type BrowserResizeMethods = 'browser-pixelated' | 'browser-low' | 'browser-medium' | 'browser-high';
| 'browser-pixelated' type WorkerResizeMethods = 'triangle' | 'catrom' | 'mitchell' | 'lanczos3';
| 'browser-low' const workerResizeMethods: WorkerResizeMethods[] = ['triangle', 'catrom', 'mitchell', 'lanczos3'];
| 'browser-medium'
| 'browser-high';
type WorkerResizeMethods =
| 'triangle'
| 'catrom'
| 'mitchell'
| 'lanczos3'
| 'hqx';
const workerResizeMethods: WorkerResizeMethods[] = [
'triangle',
'catrom',
'mitchell',
'lanczos3',
'hqx',
];
export type ResizeOptions = export type ResizeOptions = BrowserResizeOptions | WorkerResizeOptions | VectorResizeOptions;
| BrowserResizeOptions
| WorkerResizeOptions
| VectorResizeOptions;
export interface ResizeOptionsCommon { export interface ResizeOptionsCommon {
width: number; width: number;
@@ -47,21 +29,10 @@ export interface VectorResizeOptions extends ResizeOptionsCommon {
* *
* @param opts * @param opts
*/ */
export function isWorkerOptions( export function isWorkerOptions(opts: ResizeOptions): opts is WorkerResizeOptions {
opts: ResizeOptions,
): opts is WorkerResizeOptions {
return (workerResizeMethods as string[]).includes(opts.method); 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 = { export const defaultOptions: ResizeOptions = {
// Width and height will always default to the image size. // Width and height will always default to the image size.
// This is set elsewhere. // This is set elsewhere.

View File

@@ -1,6 +1,17 @@
import wasmUrl from '../../../codecs/resize/pkg/resize_bg.wasm';
import '../../../codecs/resize/pkg/resize';
import { WorkerResizeOptions } from './processor-meta'; import { WorkerResizeOptions } from './processor-meta';
import { getContainOffsets } from './util'; import { getContainOffsets } from './util';
import { resize as codecResize } from '../../../codecs/resize/pkg';
interface WasmBindgenExports {
resize: typeof import('../../../codecs/resize/pkg/resize').resize;
}
type WasmBindgen = ((url: string) => Promise<void>) & WasmBindgenExports;
declare var wasm_bindgen: WasmBindgen;
const ready = wasm_bindgen(wasmUrl);
function crop(data: ImageData, sx: number, sy: number, sw: number, sh: number): ImageData { function crop(data: ImageData, sx: number, sy: number, sw: number, sh: number): ImageData {
const inputPixels = new Uint32Array(data.data.buffer); const inputPixels = new Uint32Array(data.data.buffer);
@@ -30,7 +41,9 @@ export async function resize(data: ImageData, opts: WorkerResizeOptions): Promis
input = crop(input, Math.round(sx), Math.round(sy), Math.round(sw), Math.round(sh)); input = crop(input, Math.round(sx), Math.round(sy), Math.round(sw), Math.round(sh));
} }
const result = codecResize( await ready;
const result = wasm_bindgen.resize(
new Uint8Array(input.data.buffer), input.width, input.height, opts.width, opts.height, new Uint8Array(input.data.buffer), input.width, input.height, opts.width, opts.height,
resizeMethods.indexOf(opts.method), opts.premultiply, opts.linearRGB, resizeMethods.indexOf(opts.method), opts.premultiply, opts.linearRGB,
); );

View File

@@ -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

@@ -0,0 +1,106 @@
import App from './index';
import { SquooshStartEventType, SquooshSideEventType } from '../compress/index';
import { expose } from 'comlink';
export interface ReadyMessage {
type: 'READY';
version: string;
}
export function exposeAPI(app: App) {
if (window === top) {
// Someone opened Squoosh in a window rather than an iframe.
// This can be deceiving and we wont allow that.
return;
}
self.parent.postMessage({ type: 'READY', version: MAJOR_VERSION }, '*');
self.addEventListener('message', (event: MessageEvent) => {
if (event.data !== 'READY?') {
return;
}
event.stopImmediatePropagation();
self.parent.postMessage({ type: 'READY', version: MAJOR_VERSION } as ReadyMessage, '*');
});
expose(new API(app), self.parent);
}
function addRemovableGlobalListener<
K extends keyof GlobalEventHandlersEventMap
>(name: K, listener: (ev: GlobalEventHandlersEventMap[K]) => void): () => void {
document.addEventListener(name, listener);
return () => document.removeEventListener(name, listener);
}
/**
* The API class contains the methods that are exposed via Comlink to the
* outside world.
*/
export class API {
/**
* Internal constructor. Do not call.
*/
constructor(private _app: App) {}
/**
* Loads a given file into Squoosh.
* @param blob The `Blob` to load
* @param name The name of the file. The extension of this name will be used
* to deterime which decoder to use.
*/
setFile(blob: Blob, name: string) {
return new Promise((resolve) => {
document.addEventListener(SquooshStartEventType.START, () => resolve(), {
once: true,
});
this._app.openFile(new File([blob], name));
});
}
/**
* Grabs one side from Squoosh as a `File`.
* @param side The side which to grab. 0 = left, 1 = right.
*/
async getBlob(side: 0 | 1) {
if (!this._app.state.file || !this._app.compressInstance) {
throw new Error('No file has been loaded');
}
if (
!this._app.compressInstance!.state.loading &&
!this._app.compressInstance!.state.sides[side].loading
) {
return this._app.compressInstance!.state.sides[side].file;
}
const listeners: ReturnType<typeof addRemovableGlobalListener>[] = [];
const r = new Promise((resolve, reject) => {
listeners.push(
addRemovableGlobalListener(SquooshSideEventType.DONE, (event) => {
if (event.side !== side) {
return;
}
resolve(this._app.compressInstance!.state.sides[side].file);
}),
);
listeners.push(
addRemovableGlobalListener(SquooshSideEventType.ABORT, (event) => {
if (event.side !== side) {
return;
}
reject(new DOMException('Aborted', 'AbortError'));
}),
);
listeners.push(
addRemovableGlobalListener(SquooshSideEventType.ERROR, (event) => {
if (event.side !== side) {
return;
}
reject(event.error);
}),
);
});
r.then(() => listeners.forEach(remove => remove()));
return r;
}
}

View File

@@ -14,10 +14,9 @@ const ROUTE_EDITOR = '/editor';
const compressPromise = import( const compressPromise = import(
/* webpackChunkName: "main-app" */ /* webpackChunkName: "main-app" */
'../compress'); '../compress');
const offlinerPromise = import(
const swBridgePromise = import( /* webpackChunkName: "offliner" */
/* webpackChunkName: "sw-bridge" */ '../../lib/offliner');
'../../lib/sw-bridge');
function back() { function back() {
window.history.back(); window.history.back();
@@ -26,7 +25,6 @@ function back() {
interface Props {} interface Props {}
interface State { interface State {
awaitingShareTarget: boolean;
file?: File | Fileish; file?: File | Fileish;
isEditorOpen: Boolean; isEditorOpen: Boolean;
Compress?: typeof import('../compress').default; Compress?: typeof import('../compress').default;
@@ -34,11 +32,11 @@ interface State {
export default class App extends Component<Props, State> { export default class App extends Component<Props, State> {
state: State = { state: State = {
awaitingShareTarget: new URL(location.href).searchParams.has('share-target'),
isEditorOpen: false, isEditorOpen: false,
file: undefined, file: undefined,
Compress: undefined, Compress: undefined,
}; };
compressInstance?: import('../compress').default;
snackbar?: SnackBarElement; snackbar?: SnackBarElement;
@@ -51,15 +49,7 @@ export default class App extends Component<Props, State> {
this.showSnack('Failed to load app'); this.showSnack('Failed to load app');
}); });
swBridgePromise.then(async ({ offliner, getSharedImage }) => { offlinerPromise.then(({ offliner }) => offliner(this.showSnack));
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 });
});
// In development, persist application state across hot reloads: // In development, persist application state across hot reloads:
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
@@ -80,6 +70,14 @@ export default class App extends Component<Props, State> {
}); });
window.addEventListener('popstate', this.onPopState); window.addEventListener('popstate', this.onPopState);
import(
/* webpackChunkName: "client-api" */
'./client-api').then(m => m.exposeAPI(this));
}
@bind openFile(file: File | Fileish) {
this.openEditor();
this.setState({ file });
} }
@bind @bind
@@ -92,8 +90,7 @@ export default class App extends Component<Props, State> {
@bind @bind
private onIntroPickFile(file: File | Fileish) { private onIntroPickFile(file: File | Fileish) {
this.openEditor(); return this.openFile(file);
this.setState({ file });
} }
@bind @bind
@@ -110,25 +107,24 @@ export default class App extends Component<Props, State> {
@bind @bind
private openEditor() { private openEditor() {
if (this.state.isEditorOpen) return; if (this.state.isEditorOpen) return;
// Change path, but preserve query string. history.pushState(null, '', ROUTE_EDITOR);
const editorURL = new URL(location.href);
editorURL.pathname = ROUTE_EDITOR;
history.pushState(null, '', editorURL.href);
this.setState({ isEditorOpen: true }); this.setState({ isEditorOpen: true });
} }
render({}: Props, { file, isEditorOpen, Compress, awaitingShareTarget }: State) { render({}: Props, { file, isEditorOpen, Compress }: State) {
const showSpinner = awaitingShareTarget || (isEditorOpen && !Compress);
return ( return (
<div id="app" class={style.app}> <div id="app" class={style.app}>
<file-drop accept="image/*" onfiledrop={this.onFileDrop} class={style.drop}> <file-drop accept="image/*" onfiledrop={this.onFileDrop} class={style.drop}>
{ {!isEditorOpen
showSpinner ? <Intro onFile={this.onIntroPickFile} showSnack={this.showSnack} />
? <loading-spinner class={style.appLoader}/> : (Compress)
: isEditorOpen ? <Compress
? Compress && <Compress file={file!} showSnack={this.showSnack} onBack={back} /> ref={i => this.compressInstance = i}
: <Intro onFile={this.onIntroPickFile} showSnack={this.showSnack} /> file={file!}
showSnack={this.showSnack}
onBack={back}
/>
: <loading-spinner class={style.appLoader}/>
} }
<snack-bar ref={linkRef(this, 'snackbar')} /> <snack-bar ref={linkRef(this, 'snackbar')} />
</file-drop> </file-drop>

View File

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

View File

@@ -24,7 +24,7 @@ import { decodeImage } from '../../codecs/decoders';
import { cleanMerge, cleanSet } from '../../lib/clean-modify'; import { cleanMerge, cleanSet } from '../../lib/clean-modify';
import Processor from '../../codecs/processor'; import Processor from '../../codecs/processor';
import { import {
BrowserResizeOptions, isWorkerOptions as isWorkerResizeOptions, isHqx, WorkerResizeOptions, BrowserResizeOptions, isWorkerOptions as isWorkerResizeOptions,
} from '../../codecs/resize/processor-meta'; } from '../../codecs/resize/processor-meta';
import './custom-els/MultiPanel'; import './custom-els/MultiPanel';
import Results from '../results'; import Results from '../results';
@@ -32,6 +32,51 @@ import { ExpandIcon, CopyAcrossIconProps } from '../../lib/icons';
import SnackBarElement from '../../lib/SnackBar'; import SnackBarElement from '../../lib/SnackBar';
import { InputProcessorState, defaultInputProcessorState } from '../../codecs/input-processors'; import { InputProcessorState, defaultInputProcessorState } from '../../codecs/input-processors';
// Safari and Edge don't quite support extending Event, this works around it.
function fixExtendedEvent(instance: Event, type: Function) {
if (!(instance instanceof type)) {
Object.setPrototypeOf(instance, type.prototype);
}
}
export enum SquooshStartEventType {
START = 'squoosh:start',
}
export class SquooshStartEvent extends Event {
constructor(init?: EventInit) {
super(SquooshStartEventType.START, init);
fixExtendedEvent(this, SquooshStartEvent);
}
}
export const enum SquooshSideEventType {
DONE = 'squoosh:done',
ABORT = 'squoosh:abort',
ERROR = 'squoosh:error',
}
export interface SquooshSideEventInit extends EventInit {
side: 0|1;
error?: Error;
}
export class SquooshSideEvent extends Event {
public side: 0|1;
public error?: Error;
constructor(name: SquooshSideEventType, init: SquooshSideEventInit) {
super(name, init);
fixExtendedEvent(this, SquooshSideEvent);
this.side = init.side;
this.error = init.error;
}
}
declare global {
interface GlobalEventHandlersEventMap {
[SquooshStartEventType.START]: SquooshStartEvent;
[SquooshSideEventType.DONE]: SquooshSideEvent;
[SquooshSideEventType.ABORT]: SquooshSideEvent;
[SquooshSideEventType.ERROR]: SquooshSideEvent;
}
}
export interface SourceImage { export interface SourceImage {
file: File | Fileish; file: File | Fileish;
decoded: ImageData; decoded: ImageData;
@@ -106,18 +151,6 @@ async function preprocessImage(
source.vectorImage, source.vectorImage,
preprocessData.resize, 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-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)) { } else if (isWorkerResizeOptions(preprocessData.resize)) {
result = await processor.workerResize(result, preprocessData.resize); result = await processor.workerResize(result, preprocessData.resize);
} else { } else {
@@ -256,7 +289,7 @@ export default class Compress extends Component<Props, State> {
this.widthQuery.addListener(this.onMobileWidthChange); this.widthQuery.addListener(this.onMobileWidthChange);
this.updateFile(props.file); this.updateFile(props.file);
import('../../lib/sw-bridge').then(({ mainAppLoaded }) => mainAppLoaded()); import('../../lib/offliner').then(({ mainAppLoaded }) => mainAppLoaded());
} }
@bind @bind
@@ -407,8 +440,7 @@ export default class Compress extends Component<Props, State> {
// Either processor is good enough here. // Either processor is good enough here.
const processor = this.leftProcessor; const processor = this.leftProcessor;
this.setState({ loadingCounter, loading: true }); this.setState({ loadingCounter, loading: true }, this.signalLoadingStart);
// Abort any current encode jobs, as they're redundant now. // Abort any current encode jobs, as they're redundant now.
this.leftProcessor.abortCurrent(); this.leftProcessor.abortCurrent();
this.rightProcessor.abortCurrent(); this.rightProcessor.abortCurrent();
@@ -486,6 +518,35 @@ export default class Compress extends Component<Props, State> {
); );
} }
@bind
private dispatchSideEvent(type: SquooshSideEventType, init: SquooshSideEventInit) {
document.dispatchEvent(
new SquooshSideEvent(type, init),
);
}
@bind
private signalLoadingStart() {
document.dispatchEvent(
new SquooshStartEvent(),
);
}
@bind
private signalProcessingDone(side: 0|1) {
this.dispatchSideEvent(SquooshSideEventType.DONE, { side });
}
@bind
private signalProcessingAbort(side: 0|1) {
this.dispatchSideEvent(SquooshSideEventType.ABORT, { side });
}
@bind
private signalProcessingError(side: 0|1, msg: string) {
this.dispatchSideEvent(SquooshSideEventType.ERROR, { side, error: new Error(msg) });
}
private async updateImage(index: number, options: UpdateImageOptions = {}): Promise<void> { private async updateImage(index: number, options: UpdateImageOptions = {}): Promise<void> {
const { const {
skipPreprocessing = false, skipPreprocessing = false,
@@ -547,8 +608,13 @@ export default class Compress extends Component<Props, State> {
}); });
} }
} catch (err) { } catch (err) {
if (err.name === 'AbortError') return; if (err.name === 'AbortError') {
this.props.showSnack(`Processing error (type=${settings.encoderState.type}): ${err}`); this.signalProcessingAbort(index as 0|1);
return;
}
const errorMsg = `Processing error (type=${settings.encoderState.type}): ${err}`;
this.signalProcessingError(index as 0|1, errorMsg);
this.props.showSnack(errorMsg);
throw err; throw err;
} }
} }
@@ -571,7 +637,7 @@ export default class Compress extends Component<Props, State> {
encodedSettings: settings, encodedSettings: settings,
}); });
this.setState({ sides }); this.setState({ sides }, () => this.signalProcessingDone(index as 0|1));
} }
render({ onBack }: Props, { loading, sides, source, mobileView }: State) { render({ onBack }: Props, { loading, sides, source, mobileView }: State) {
@@ -579,7 +645,6 @@ export default class Compress extends Component<Props, State> {
const [leftImageData, rightImageData] = sides.map(i => i.data); const [leftImageData, rightImageData] = sides.map(i => i.data);
const options = sides.map((side, index) => ( const options = sides.map((side, index) => (
// tslint:disable-next-line:jsx-key
<Options <Options
source={source} source={source}
mobileView={mobileView} mobileView={mobileView}
@@ -595,7 +660,6 @@ export default class Compress extends Component<Props, State> {
(mobileView ? ['down', 'up'] : ['right', 'left']) as CopyAcrossIconProps['copyDirection'][]; (mobileView ? ['down', 'up'] : ['right', 'left']) as CopyAcrossIconProps['copyDirection'][];
const results = sides.map((side, index) => ( const results = sides.map((side, index) => (
// tslint:disable-next-line:jsx-key
<Results <Results
downloadUrl={side.downloadUrl} downloadUrl={side.downloadUrl}
imageFile={side.file} 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 = {}; state: State = {};
private fileInput?: HTMLInputElement; private fileInput?: HTMLInputElement;
@bind
private resetFileInput() {
this.fileInput!.value = '';
}
@bind @bind
private onFileChange(event: Event): void { private onFileChange(event: Event): void {
const fileInput = event.target as HTMLInputElement; const fileInput = event.target as HTMLInputElement;
const file = fileInput.files && fileInput.files[0]; const file = fileInput.files && fileInput.files[0];
if (!file) return; if (!file) return;
this.resetFileInput();
this.props.onFile(file); this.props.onFile(file);
} }

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 */ /** Set up the service worker and monitor changes */
export async function offliner(showSnack: SnackBarElement['showSnackbar']) { export async function offliner(showSnack: SnackBarElement['showSnackbar']) {
// This needs to be a typeof because Webpack. // This needs to be a typeof because Webpack.

View File

@@ -11,25 +11,6 @@
"src": "/assets/icon-large.png", "src": "/assets/icon-large.png",
"type": "image/png", "type": "image/png",
"sizes": "1024x1024" "sizes": "1024x1024"
},
{
"src": "/assets/icon-large-maskable.png",
"type": "image/png",
"sizes": "1024x1024",
"purpose": "maskable"
} }
], ]
"share_target": {
"action": "/?share-target",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"files": [
{
"name": "file",
"accept": ["image/*"]
}
]
}
}
} }

View File

@@ -34,6 +34,7 @@ declare module 'url-loader!*' {
} }
declare var VERSION: string; declare var VERSION: string;
declare var MAJOR_VERSION: string;
declare var ga: { declare var ga: {
(...args: any[]): void; (...args: any[]): void;

41
src/sdk.ts Normal file
View File

@@ -0,0 +1,41 @@
import { proxy, ProxyResult } from 'comlink';
import { API, ReadyMessage } from './components/App/client-api';
// @ts-ignore
import { version } from '../package.json';
const MAJOR_VERSION = (version.split('.')[0] as string);
/**
* This function will load an iFrame
* @param {HTMLIFrameElement} ifr iFrame that will be used to load squoosh
* @param {string} src URL of squoosh instance to use
*/
export default async function loader(
ifr: HTMLIFrameElement,
src: string = 'https://squoosh.app',
): Promise<ProxyResult<API>> {
ifr.src = src;
await new Promise(resolve => (ifr.onload = resolve));
ifr.contentWindow!.postMessage('READY?', '*');
await new Promise((resolve) => {
window.addEventListener('message', function l(ev) {
const msg = ev.data as ReadyMessage;
if (!msg || msg.type !== 'READY') {
return;
}
if (msg.version !== MAJOR_VERSION) {
throw Error(
`Version mismatch. SDK version ${MAJOR_VERSION}, Squoosh version ${
msg.version
}`,
);
}
ev.stopPropagation();
window.removeEventListener('message', l);
resolve();
});
});
return proxy(ifr.contentWindow!);
}

View File

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

View File

@@ -1,11 +1,8 @@
import webpDataUrl from 'url-loader!../codecs/tiny.webp'; import webpDataUrl from 'url-loader!../codecs/tiny.webp';
// Give TypeScript the correct global.
declare var self: ServiceWorkerGlobalScope;
export function cacheOrNetwork(event: FetchEvent): void { export function cacheOrNetwork(event: FetchEvent): void {
event.respondWith(async function () { 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); 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[]) { export function cleanupCache(event: FetchEvent, cacheName: string, keepAssets: string[]) {
event.waitUntil(async function () { event.waitUntil(async function () {
const cache = await caches.open(cacheName); const cache = await caches.open(cacheName);
@@ -124,26 +104,3 @@ export async function cacheAdditionalProcessors(cacheName: string, buildAssets:
const cache = await caches.open(cacheName); const cache = await caches.open(cacheName);
await cache.addAll(toCache); 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"], "variable-name": [true, "check-format", "allow-leading-underscore"],
"no-duplicate-imports": false, "no-duplicate-imports": false,
"prefer-template": [true, "allow-single-concat"], "prefer-template": [true, "allow-single-concat"],
"import-name": false, "import-name": false
"jsx-key": false
}, },
"linterOptions": { "linterOptions": {
"exclude": [ "exclude": [

View File

@@ -1,3 +1,4 @@
const fs = require('fs');
const path = require('path'); const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const CleanPlugin = require('clean-webpack-plugin'); const CleanPlugin = require('clean-webpack-plugin');
@@ -16,7 +17,11 @@ const CrittersPlugin = require('critters-webpack-plugin');
const AssetTemplatePlugin = require('./config/asset-template-plugin'); const AssetTemplatePlugin = require('./config/asset-template-plugin');
const addCssTypes = require('./config/add-css-types'); const addCssTypes = require('./config/add-css-types');
const VERSION = require('./package.json').version; function readJson (filename) {
return JSON.parse(fs.readFileSync(filename));
}
const VERSION = readJson('./package.json').version;
module.exports = async function (_, env) { module.exports = async function (_, env) {
const isProd = env.mode === 'production'; const isProd = env.mode === 'production';
@@ -142,14 +147,11 @@ module.exports = async function (_, env) {
}, },
{ {
// All the codec files define a global with the same name as their file name. `exports-loader` attaches those to `module.exports`. // All the codec files define a global with the same name as their file name. `exports-loader` attaches those to `module.exports`.
test: /\.js$/, test: /\/codecs\/.*\.js$/,
include: path.join(__dirname, 'src/codecs'),
loader: 'exports-loader' loader: 'exports-loader'
}, },
{ {
// Emscripten modules don't work with Webpack's Wasm loader. test: /\/codecs\/.*\.wasm$/,
test: /\.wasm$/,
exclude: /_bg\.wasm$/,
// This is needed to make webpack NOT process wasm files. // This is needed to make webpack NOT process wasm files.
// See https://github.com/webpack/webpack/issues/6725 // See https://github.com/webpack/webpack/issues/6725
type: 'javascript/auto', type: 'javascript/auto',
@@ -158,11 +160,6 @@ module.exports = async function (_, env) {
name: '[name].[hash:5].[ext]', name: '[name].[hash:5].[ext]',
}, },
}, },
{
// Wasm modules generated by Rust + wasm-pack work great with Webpack.
test: /_bg\.wasm$/,
type: 'webassembly/experimental',
},
{ {
test: /\.(png|svg|jpg|gif)$/, test: /\.(png|svg|jpg|gif)$/,
loader: 'file-loader', loader: 'file-loader',
@@ -175,7 +172,7 @@ module.exports = async function (_, env) {
plugins: [ plugins: [
new webpack.IgnorePlugin( new webpack.IgnorePlugin(
/(fs|crypto|path)/, /(fs|crypto|path)/,
/[/\\]codecs[/\\]/ new RegExp(`${path.sep}codecs${path.sep}`)
), ),
// Pretty progressbar showing build progress: // Pretty progressbar showing build progress:
@@ -242,7 +239,7 @@ module.exports = async function (_, env) {
removeRedundantAttributes: true, removeRedundantAttributes: true,
removeComments: true removeComments: true
}, },
manifest: require('./src/manifest.json'), manifest: readJson('./src/manifest.json'),
inject: 'body', inject: 'body',
compile: true compile: true
}), }),
@@ -266,6 +263,7 @@ module.exports = async function (_, env) {
// Inline constants during build, so they can be folded by UglifyJS. // Inline constants during build, so they can be folded by UglifyJS.
new webpack.DefinePlugin({ new webpack.DefinePlugin({
VERSION: JSON.stringify(VERSION), VERSION: JSON.stringify(VERSION),
MAJOR_VERSION: JSON.stringify(VERSION.split(".")[0]),
// We set node.process=false later in this config. // We set node.process=false later in this config.
// Here we make sure if (process && process.foo) still works: // Here we make sure if (process && process.foo) still works:
process: '{}' process: '{}'