Compare commits

..

2 Commits

Author SHA1 Message Date
Jake Archibald
fd98d67b3e lol this was meant to be 10 seconds. 2018-12-07 14:26:45 +00:00
Jake Archibald
db1db8506e Move early exit for no-rotation. 2018-12-07 14:26:22 +00:00
94 changed files with 6735 additions and 7468 deletions

1
.nvmrc
View File

@@ -1 +0,0 @@
10.16.2

View File

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

View File

@@ -1,2 +0,0 @@
/index.html / 301
/* /index.html 301

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

@@ -8,6 +8,6 @@
"libimagequant": "ImageOptim/libimagequant#2.12.1" "libimagequant": "ImageOptim/libimagequant#2.12.1"
}, },
"devDependencies": { "devDependencies": {
"napa": "3.0.0" "napa": "^3.0.0"
} }
} }

View File

@@ -8,6 +8,6 @@
"mozjpeg": "mozilla/mozjpeg#v3.3.1" "mozjpeg": "mozilla/mozjpeg#v3.3.1"
}, },
"devDependencies": { "devDependencies": {
"napa": "3.0.0" "napa": "^3.0.0"
} }
} }

View File

@@ -1,6 +1,6 @@
# OptiPNG # OptiPNG
- Source: <http://optipng.sourceforge.net/> - Source: <https://sourceforge.net/project/optipng>
- Version: v0.7.7 - Version: v0.7.7
## Dependencies ## Dependencies

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,6 +0,0 @@
**/*.rs.bk
target
Cargo.lock
bin/
pkg/README.md
lut.inc

View File

@@ -1,37 +0,0 @@
[package]
name = "resize"
version = "0.1.0"
authors = ["Surma <surma@surma.link>"]
[lib]
#crate-type = ["cdylib", "rlib"]
crate-type = ["cdylib"]
[features]
default = ["console_error_panic_hook", "wee_alloc"]
[dependencies]
cfg-if = "0.1.2"
wasm-bindgen = "0.2.38"
resize = "0.3.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
# 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,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;
}

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, g: number, h: number, i: number, j: number): void;

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.

View File

@@ -1,7 +1,7 @@
# WebP decoder # WebP decoder
- Source: <https://github.com/webmproject/libwebp> - Source: <https://github.com/webmproject/libwebp>
- Version: v1.0.2 - Version: v0.6.1
## Example ## Example

View File

@@ -6,33 +6,7 @@ export OPTIMIZE="-Os"
export LDFLAGS="${OPTIMIZE}" export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}" export CFLAGS="${OPTIMIZE}"
export CPPFLAGS="${OPTIMIZE}" export CPPFLAGS="${OPTIMIZE}"
apt-get update
apt-get install -qqy autoconf libtool libpng-dev pkg-config
echo "============================================="
echo "Compiling libwebp"
echo "============================================="
test -n "$SKIP_LIBWEBP" || (
cd node_modules/libwebp
autoreconf -fiv
rm -rf build || true
mkdir -p build && cd build
emconfigure ../configure \
--disable-libwebpdemux \
--disable-wic \
--disable-gif \
--disable-tiff \
--disable-jpeg \
--disable-png \
--disable-sdl \
--disable-gl \
--disable-threading \
--disable-neon-rtcd \
--disable-neon \
--disable-sse2 \
--disable-sse4.1
emmake make
)
echo "=============================================" echo "============================================="
echo "Compiling wasm bindings" echo "Compiling wasm bindings"
echo "=============================================" echo "============================================="
@@ -46,9 +20,9 @@ echo "============================================="
--std=c++11 \ --std=c++11 \
-I node_modules/libwebp \ -I node_modules/libwebp \
-o ./webp_dec.js \ -o ./webp_dec.js \
node_modules/libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c \
-x c++ \ -x c++ \
webp_dec.cpp \ webp_dec.cpp
node_modules/libwebp/build/src/.libs/libwebp.a
) )
echo "=============================================" echo "============================================="
echo "Compiling wasm bindings done" echo "Compiling wasm bindings done"

View File

@@ -5,9 +5,9 @@
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh" "build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
}, },
"napa": { "napa": {
"libwebp": "webmproject/libwebp#v1.0.2" "libwebp": "webmproject/libwebp#v1.0.0"
}, },
"devDependencies": { "devDependencies": {
"napa": "3.0.0" "napa": "^3.0.0"
} }
} }

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,7 +1,7 @@
# WebP encoder # WebP encoder
- Source: <https://github.com/webmproject/libwebp> - Source: <https://github.com/webmproject/libwebp>
- Version: v1.0.2 - Version: v0.6.1
## Dependencies ## Dependencies

View File

@@ -7,33 +7,6 @@ export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}" export CFLAGS="${OPTIMIZE}"
export CPPFLAGS="${OPTIMIZE}" export CPPFLAGS="${OPTIMIZE}"
apt-get update
apt-get install -qqy autoconf libtool libpng-dev pkg-config
echo "============================================="
echo "Compiling libwebp"
echo "============================================="
test -n "$SKIP_LIBWEBP" || (
cd node_modules/libwebp
autoreconf -fiv
rm -rf build || true
mkdir -p build && cd build
emconfigure ../configure \
--disable-libwebpdemux \
--disable-wic \
--disable-gif \
--disable-tiff \
--disable-jpeg \
--disable-png \
--disable-sdl \
--disable-gl \
--disable-threading \
--disable-neon-rtcd \
--disable-neon \
--disable-sse2 \
--disable-sse4.1
emmake make
)
echo "=============================================" echo "============================================="
echo "Compiling wasm bindings" echo "Compiling wasm bindings"
echo "=============================================" echo "============================================="
@@ -47,9 +20,9 @@ echo "============================================="
--std=c++11 \ --std=c++11 \
-I node_modules/libwebp \ -I node_modules/libwebp \
-o ./webp_enc.js \ -o ./webp_enc.js \
node_modules/libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c \
-x c++ \ -x c++ \
webp_enc.cpp \ webp_enc.cpp
node_modules/libwebp/build/src/.libs/libwebp.a
) )
echo "=============================================" echo "============================================="
echo "Compiling wasm bindings done" echo "Compiling wasm bindings done"

View File

@@ -5,9 +5,9 @@
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh" "build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
}, },
"napa": { "napa": {
"libwebp": "webmproject/libwebp#v1.0.2" "libwebp": "webmproject/libwebp#v1.0.0"
}, },
"devDependencies": { "devDependencies": {
"napa": "3.0.0" "napa": "^3.0.0"
} }
} }

View File

@@ -26,8 +26,7 @@ val encode(std::string img, int width, int height, WebPConfig config) {
throw std::runtime_error("Unexpected error"); throw std::runtime_error("Unexpected error");
} }
// Only use use_argb if we really need it, as it's slower. pic.use_argb = !!config.lossless;
pic.use_argb = config.lossless || config.use_sharp_yuv || config.preprocessing > 0;
pic.width = width; pic.width = width;
pic.height = height; pic.height = height;
pic.writer = WebPMemoryWrite; pic.writer = WebPMemoryWrite;

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

11276
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,13 @@
{ {
"private": true, "private": true,
"name": "squoosh", "name": "squoosh",
"version": "1.9.1", "version": "1.2.2",
"license": "apache-2.0", "license": "apache-2.0",
"scripts": { "scripts": {
"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",
"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"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@@ -16,61 +15,55 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@types/node": "10.14.15", "@types/node": "^10.12.6",
"@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.1",
"@webcomponents/custom-elements": "1.2.4", "@webcomponents/custom-elements": "^1.2.1",
"@webpack-cli/serve": "0.1.8", "@webpack-cli/serve": "^0.1.2",
"assets-webpack-plugin": "3.9.10", "assets-webpack-plugin": "^3.9.7",
"chalk": "2.4.2", "chokidar": "^2.0.4",
"chokidar": "3.0.2", "classnames": "^2.2.6",
"classnames": "2.2.6", "clean-webpack-plugin": "^1.0.0",
"clean-webpack-plugin": "1.0.1", "comlink": "^3.0.3",
"comlink": "3.1.1", "copy-webpack-plugin": "^4.6.0",
"copy-webpack-plugin": "5.0.4", "critters-webpack-plugin": "^2.0.1",
"critters-webpack-plugin": "2.4.0", "css-loader": "^1.0.1",
"css-loader": "1.0.1", "ejs": "^2.6.1",
"ejs": "2.6.2", "exports-loader": "^0.7.0",
"escape-string-regexp": "2.0.0", "file-drop-element": "^0.0.9",
"exports-loader": "0.7.0", "file-loader": "^2.0.0",
"file-drop-element": "0.2.0", "html-webpack-plugin": "^3.2.0",
"file-loader": "4.2.0", "husky": "^1.1.4",
"gzip-size": "5.1.1", "idb-keyval": "^3.1.0",
"html-webpack-plugin": "3.2.0", "linkstate": "^1.1.1",
"husky": "3.0.4", "loader-utils": "^1.1.0",
"idb-keyval": "3.2.0", "mini-css-extract-plugin": "^0.4.4",
"linkstate": "1.1.1", "minimatch": "^3.0.4",
"loader-utils": "1.2.3", "node-sass": "^4.11.0",
"mini-css-extract-plugin": "0.8.0", "optimize-css-assets-webpack-plugin": "^5.0.1",
"minimatch": "3.0.4", "pointer-tracker": "^2.0.3",
"node-fetch": "2.6.0", "preact": "^8.3.1",
"node-sass": "4.12.0", "prerender-loader": "^1.2.0",
"optimize-css-assets-webpack-plugin": "5.0.1", "pretty-bytes": "^5.1.0",
"pointer-tracker": "2.0.3", "progress-bar-webpack-plugin": "^1.11.0",
"preact": "8.4.2", "raw-loader": "^0.5.1",
"prerender-loader": "1.3.0", "sass-loader": "^7.1.0",
"pretty-bytes": "5.3.0", "script-ext-html-webpack-plugin": "^2.1.3",
"progress-bar-webpack-plugin": "1.12.1", "source-map-loader": "^0.2.4",
"raw-loader": "3.1.0", "style-loader": "^0.23.1",
"readdirp": "3.1.2", "terser-webpack-plugin": "^1.1.0",
"sass-loader": "7.3.1", "ts-loader": "^5.3.0",
"script-ext-html-webpack-plugin": "2.1.4", "tslint": "^5.11.0",
"source-map-loader": "0.2.4", "tslint-config-airbnb": "^5.11.0",
"style-loader": "1.0.0", "tslint-config-semistandard": "^7.0.0",
"terser-webpack-plugin": "1.4.1", "tslint-react": "^3.6.0",
"travis-size-report": "1.1.0", "typed-css-modules": "^0.3.7",
"ts-loader": "6.0.3", "typescript": "^3.1.6",
"tslint": "5.19.0", "url-loader": "^1.1.2",
"tslint-config-airbnb": "5.11.1", "webpack": "^4.25.1",
"tslint-config-semistandard": "8.0.1", "webpack-bundle-analyzer": "^3.0.3",
"tslint-react": "4.0.0", "webpack-cli": "^3.1.2",
"typed-css-modules": "0.4.2", "webpack-dev-server": "^3.1.10",
"typescript": "3.5.3", "worker-plugin": "^1.1.1"
"url-loader": "2.1.0",
"webpack": "4.28.0",
"webpack-bundle-analyzer": "3.4.1",
"webpack-cli": "3.3.4",
"webpack-dev-server": "3.8.0",
"worker-plugin": "3.1.0"
} }
} }

View File

@@ -1,5 +0,0 @@
{
"extends": [
"config:base"
]
}

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

View File

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

View File

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

View File

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

@@ -6,8 +6,8 @@ import { EncodeOptions as OptiPNGEncoderOptions } from './optipng/encoder-meta';
import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta'; import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta';
import { EncodeOptions as BrowserJPEGOptions } from './browser-jpeg/encoder-meta'; import { EncodeOptions as BrowserJPEGOptions } from './browser-jpeg/encoder-meta';
import { EncodeOptions as BrowserWebpEncodeOptions } from './browser-webp/encoder-meta'; import { EncodeOptions as BrowserWebpEncodeOptions } from './browser-webp/encoder-meta';
import { BrowserResizeOptions, VectorResizeOptions } from './resize/processor-meta'; import { BitmapResizeOptions, VectorResizeOptions } from './resize/processor-meta';
import { browserResize, vectorResize } from './resize/processor-sync'; import { resize, vectorResize } from './resize/processor';
import * as browserBMP from './browser-bmp/encoder'; import * as browserBMP from './browser-bmp/encoder';
import * as browserPNG from './browser-png/encoder'; import * as browserPNG from './browser-png/encoder';
import * as browserJPEG from './browser-jpeg/encoder'; import * as browserJPEG from './browser-jpeg/encoder';
@@ -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;
@@ -128,13 +130,6 @@ export default class Processor {
return this._workerApi!.rotate(data, opts); 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 }) @Processor._processingJob({ needsWorker: true })
mozjpegEncode( mozjpegEncode(
data: ImageData, opts: MozJPEGEncoderOptions, data: ImageData, opts: MozJPEGEncoderOptions,
@@ -207,9 +202,9 @@ export default class Processor {
// Synchronous jobs // Synchronous jobs
resize(data: ImageData, opts: BrowserResizeOptions) { resize(data: ImageData, opts: BitmapResizeOptions) {
this.abortCurrent(); this.abortCurrent();
return browserResize(data, opts); return resize(data, opts);
} }
vectorResize(data: HTMLImageElement, opts: VectorResizeOptions) { vectorResize(data: HTMLImageElement, opts: VectorResizeOptions) {

View File

@@ -1,10 +1,8 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import linkState from 'linkstate'; import linkState from 'linkstate';
import { bind, linkRef } from '../../lib/initial-util'; import { bind, linkRef } from '../../lib/initial-util';
import { import { inputFieldValueAsNumber, inputFieldValue, preventDefault } from '../../lib/util';
inputFieldValueAsNumber, inputFieldValue, preventDefault, inputFieldChecked, import { ResizeOptions } from './processor-meta';
} from '../../lib/util';
import { ResizeOptions, isWorkerOptions } from './processor-meta';
import * as style from '../../components/Options/style.scss'; import * as style from '../../components/Options/style.scss';
import Checkbox from '../../components/checkbox'; import Checkbox from '../../components/checkbox';
import Expander from '../../components/expander'; import Expander from '../../components/expander';
@@ -12,9 +10,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 +19,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!;
@@ -50,8 +38,6 @@ export default class ResizerOptions extends Component<Props, State> {
width: inputFieldValueAsNumber(width), width: inputFieldValueAsNumber(width),
height: inputFieldValueAsNumber(height), height: inputFieldValueAsNumber(height),
method: form.resizeMethod.value, method: form.resizeMethod.value,
premultiply: inputFieldChecked(form.premultiply, true),
linearRGB: inputFieldChecked(form.linearRGB, true),
// Casting, as the formfield only returns the correct values. // Casting, as the formfield only returns the correct values.
fitMethod: inputFieldValue(form.fitMethod, options.fitMethod) as ResizeOptions['fitMethod'], fitMethod: inputFieldValue(form.fitMethod, options.fitMethod) as ResizeOptions['fitMethod'],
}; };
@@ -63,31 +49,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 +70,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}>
@@ -146,26 +87,12 @@ export default class ResizerOptions extends Component<Props, State> {
onChange={this.onChange} onChange={this.onChange}
> >
{isVector && <option value="vector">Vector</option>} {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-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
@@ -190,30 +117,6 @@ export default class ResizerOptions extends Component<Props, State> {
onInput={this.onHeightInput} onInput={this.onHeightInput}
/> />
</label> </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}> <label class={style.optionInputFirst}>
<Checkbox <Checkbox
name="maintainAspect" name="maintainAspect"

View File

@@ -1,75 +1,26 @@
type BrowserResizeMethods = type BitmapResizeMethods = 'browser-pixelated' | 'browser-low' | 'browser-medium' | 'browser-high';
| 'browser-pixelated'
| 'browser-low'
| 'browser-medium'
| 'browser-high';
type WorkerResizeMethods =
| 'triangle'
| 'catrom'
| 'mitchell'
| 'lanczos3'
| 'hqx';
const workerResizeMethods: WorkerResizeMethods[] = [
'triangle',
'catrom',
'mitchell',
'lanczos3',
'hqx',
];
export type ResizeOptions = export interface ResizeOptions {
| BrowserResizeOptions
| WorkerResizeOptions
| VectorResizeOptions;
export interface ResizeOptionsCommon {
width: number; width: number;
height: number; height: number;
method: 'vector' | BitmapResizeMethods;
fitMethod: 'stretch' | 'contain'; fitMethod: 'stretch' | 'contain';
} }
export interface BrowserResizeOptions extends ResizeOptionsCommon { export interface BitmapResizeOptions extends ResizeOptions {
method: BrowserResizeMethods; method: BitmapResizeMethods;
} }
export interface WorkerResizeOptions extends ResizeOptionsCommon { export interface VectorResizeOptions extends ResizeOptions {
method: WorkerResizeMethods;
premultiply: boolean;
linearRGB: boolean;
}
export interface VectorResizeOptions extends ResizeOptionsCommon {
method: 'vector'; 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 = { 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.
width: 1, width: 1,
height: 1, height: 1,
// This will be set to 'vector' if the input is SVG. // This will be set to 'vector' if the input is SVG.
method: 'lanczos3', method: 'browser-high',
fitMethod: 'stretch', 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 { nativeResize, NativeResizeMethod, drawableToImageData } from '../../lib/util';
import { getContainOffsets } from './util'; import { BitmapResizeOptions, VectorResizeOptions } from './processor-meta';
import { resize as codecResize } from '../../../codecs/resize/pkg';
function crop(data: ImageData, sx: number, sy: number, sw: number, sh: number): ImageData { function getContainOffsets(sw: number, sh: number, dw: number, dh: number) {
const inputPixels = new Uint32Array(data.data.buffer); const currentAspect = sw / sh;
const endAspect = dw / dh;
// Copy within the same buffer for speed and memory efficiency. if (endAspect > currentAspect) {
for (let y = 0; y < sh; y += 1) { const newSh = sw / endAspect;
const start = ((y + sy) * data.width) + sx; const newSy = (sh - newSh) / 2;
inputPixels.copyWithin(y * sw, start, start + sw); return { sw, sh: newSh, sx: 0, sy: newSy };
} }
return new ImageData( const newSw = sh * endAspect;
new Uint8ClampedArray(inputPixels.buffer.slice(0, sw * sh * 4)), const newSx = (sw - newSw) / 2;
sw, sh, return { sh, sw: newSw, sx: newSx, sy: 0 };
);
} }
/** Resize methods by index */ export function resize(data: ImageData, opts: BitmapResizeOptions): ImageData {
const resizeMethods: WorkerResizeOptions['method'][] = [ let sx = 0;
'triangle', 'catrom', 'mitchell', 'lanczos3', let sy = 0;
]; let sw = data.width;
let sh = data.height;
export async function resize(data: ImageData, opts: WorkerResizeOptions): Promise<ImageData> {
let input = data;
if (opts.fitMethod === 'contain') { if (opts.fitMethod === 'contain') {
const { sx, sy, sw, sh } = getContainOffsets(data.width, data.height, opts.width, opts.height); ({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height));
input = crop(input, Math.round(sx), Math.round(sy), Math.round(sw), Math.round(sh));
} }
const result = codecResize( return nativeResize(
new Uint8Array(input.data.buffer), input.width, input.height, opts.width, opts.height, data, sx, sy, sw, sh, opts.width, opts.height,
resizeMethods.indexOf(opts.method), opts.premultiply, opts.linearRGB, 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 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,76 @@
import wasmUrl from '../../../codecs/rotate/rotate.wasm'; import { RotateOptions } from './processor-meta';
import { RotateOptions, RotateModuleInstance } from './processor-meta';
// We are loading a 500B module here. Loading the code to feature-detect const bpp = 4;
// `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 async function rotate( export function rotate(data: ImageData, opts: RotateOptions): ImageData {
data: ImageData, const { rotate } = opts;
opts: RotateOptions, const flipDimensions = rotate % 180 !== 0;
): Promise<ImageData> { const { width: inputWidth, height: inputHeight } = data;
const { instance } = (await instancePromise) as { const outputWidth = flipDimensions ? inputHeight : inputWidth;
instance: RotateModuleInstance; const outputHeight = flipDimensions ? inputWidth : inputHeight;
}; const out = new ImageData(outputWidth, outputHeight);
let i = 0;
// Number of wasm memory pages (á 64KiB) needed to store the image twice. // In the straight-copy case, d1 is x, d2 is y.
const bytesPerImage = data.width * data.height * 4; // x starts at 0 and increases.
const numPagesNeeded = Math.ceil((bytesPerImage * 2 + 8) / (64 * 1024)); // y starts at 0 and increases.
// Only count full pages, just to be safe. let d1Start = 0;
const numPagesAvailable = Math.floor( let d1Limit = inputWidth;
instance.exports.memory.buffer.byteLength / (64 * 1024), let d1Advance = 1;
); let d1Multiplier = 1;
const additionalPagesToAllocate = numPagesNeeded - numPagesAvailable; let d2Start = 0;
let d2Limit = inputHeight;
let d2Advance = 1;
let d2Multiplier = inputWidth;
if (additionalPagesToAllocate > 0) { if (rotate === 90) {
instance.exports.memory.grow(additionalPagesToAllocate); // 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); for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
// Iterate over channels:
const start = ((d1 * d1Multiplier) + (d2 * d2Multiplier)) * bpp;
for (let j = 0; j < bpp; j += 1) {
out.data[i] = data.data[start + j];
i += 1;
}
}
}
const flipDimensions = opts.rotate % 180 !== 0; return out;
return new ImageData(
view.slice(bytesPerImage + 8, bytesPerImage * 2 + 8),
flipDimensions ? data.height : data.width,
flipDimensions ? data.width : data.height,
);
} }

View File

@@ -2,7 +2,7 @@ type ModuleFactory<M extends EmscriptenWasm.Module> = (
opts: EmscriptenWasm.ModuleOpts, opts: EmscriptenWasm.ModuleOpts,
) => M; ) => M;
export function initEmscriptenModule<T extends EmscriptenWasm.Module>( export function initWasmModule<T extends EmscriptenWasm.Module>(
moduleFactory: ModuleFactory<T>, moduleFactory: ModuleFactory<T>,
wasmUrl: string, wasmUrl: string,
): Promise<T> { ): 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 webp_dec, { WebPModule } from '../../../codecs/webp_dec/webp_dec';
import wasmUrl from '../../../codecs/webp_dec/webp_dec.wasm'; import wasmUrl from '../../../codecs/webp_dec/webp_dec.wasm';
import { initEmscriptenModule } from '../util'; import { initWasmModule } from '../util';
let emscriptenModule: Promise<WebPModule>; let emscriptenModule: Promise<WebPModule>;
export async function decode(data: ArrayBuffer): Promise<ImageData> { 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 module = await emscriptenModule;
const rawImage = module.decode(data); const rawImage = module.decode(data);

View File

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

View File

@@ -9,33 +9,24 @@ import '../../lib/SnackBar';
import Intro from '../intro'; import Intro from '../intro';
import '../custom-els/LoadingSpinner'; import '../custom-els/LoadingSpinner';
const ROUTE_EDITOR = '/editor';
const compressPromise = import( const compressPromise = import(
/* webpackChunkName: "main-app" */ /* webpackChunkName: "main-app" */
'../compress'); '../compress',
);
const swBridgePromise = import( const offlinerPromise = import(
/* webpackChunkName: "sw-bridge" */ /* webpackChunkName: "offliner" */
'../../lib/sw-bridge'); '../../lib/offliner',
);
function back() {
window.history.back();
}
interface Props {} interface Props {}
interface State { interface State {
awaitingShareTarget: boolean;
file?: File | Fileish; file?: File | Fileish;
isEditorOpen: Boolean;
Compress?: typeof import('../compress').default; Compress?: typeof import('../compress').default;
} }
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,
file: undefined, file: undefined,
Compress: undefined, Compress: undefined,
}; };
@@ -51,48 +42,28 @@ 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') {
this.setState(window.STATE); this.setState(window.STATE);
const oldCDU = this.componentDidUpdate; const oldCDU = this.componentDidUpdate;
this.componentDidUpdate = (props, state, prev) => { this.componentDidUpdate = (props, state) => {
if (oldCDU) oldCDU.call(this, props, state, prev); if (oldCDU) oldCDU.call(this, props, state);
window.STATE = this.state; window.STATE = this.state;
}; };
} }
// Since iOS 10, Apple tries to prevent disabling pinch-zoom. This is great in theory, but
// really breaks things on Squoosh, as you can easily end up zooming the UI when you mean to
// zoom the image. Once you've done this, it's really difficult to undo. Anyway, this seems to
// prevent it.
document.body.addEventListener('gesturestart', (event) => {
event.preventDefault();
});
window.addEventListener('popstate', this.onPopState);
} }
@bind @bind
private onFileDrop({ files }: FileDropEvent) { private onFileDrop(event: FileDropEvent) {
if (!files || files.length === 0) return; const { file } = event;
const file = files[0]; if (!file) return;
this.openEditor();
this.setState({ file }); this.setState({ file });
} }
@bind @bind
private onIntroPickFile(file: File | Fileish) { private onIntroPickFile(file: File | Fileish) {
this.openEditor();
this.setState({ file }); this.setState({ file });
} }
@@ -103,32 +74,19 @@ export default class App extends Component<Props, State> {
} }
@bind @bind
private onPopState() { private onBack() {
this.setState({ isEditorOpen: location.pathname === ROUTE_EDITOR }); this.setState({ file: undefined });
} }
@bind render({}: Props, { file, Compress }: State) {
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);
this.setState({ isEditorOpen: true });
}
render({}: Props, { file, isEditorOpen, Compress, awaitingShareTarget }: 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}>
{ {(!file)
showSpinner ? <Intro onFile={this.onIntroPickFile} showSnack={this.showSnack} />
? <loading-spinner class={style.appLoader}/> : (Compress)
: isEditorOpen ? <Compress file={file} showSnack={this.showSnack} onBack={this.onBack} />
? Compress && <Compress file={file!} showSnack={this.showSnack} onBack={back} /> : <loading-spinner class={style.appLoader}/>
: <Intro onFile={this.onIntroPickFile} showSnack={this.showSnack} />
} }
<snack-bar ref={linkRef(this, 'snackbar')} /> <snack-bar ref={linkRef(this, 'snackbar')} />
</file-drop> </file-drop>

View File

@@ -40,9 +40,7 @@ import Checkbox from '../checkbox';
import Expander from '../expander'; import Expander from '../expander';
import Select from '../select'; import Select from '../select';
const encoderOptionsComponentMap: { const encoderOptionsComponentMap = {
[x: string]: (new (...args: any[]) => Component<any, any>) | undefined;
} = {
[identity.type]: undefined, [identity.type]: undefined,
[optiPNG.type]: OptiPNGEncoderOptions, [optiPNG.type]: OptiPNGEncoderOptions,
[mozJPEG.type]: MozJpegEncoderOptions, [mozJPEG.type]: MozJpegEncoderOptions,
@@ -146,14 +144,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 +176,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

@@ -43,7 +43,6 @@ $horizontalPadding: 15px;
.text-field { .text-field {
background: #fff; background: #fff;
color: #000;
font: inherit; font: inherit;
border: none; border: none;
padding: 2px 0 2px 10px; padding: 2px 0 2px 10px;

View File

@@ -18,14 +18,20 @@ import * as browserTIFF from '../../codecs/browser-tiff/encoder-meta';
import * as browserJP2 from '../../codecs/browser-jp2/encoder-meta'; import * as browserJP2 from '../../codecs/browser-jp2/encoder-meta';
import * as browserBMP from '../../codecs/browser-bmp/encoder-meta'; import * as browserBMP from '../../codecs/browser-bmp/encoder-meta';
import * as browserPDF from '../../codecs/browser-pdf/encoder-meta'; import * as browserPDF from '../../codecs/browser-pdf/encoder-meta';
import { EncoderState, EncoderType, EncoderOptions, encoderMap } from '../../codecs/encoders'; import {
import { PreprocessorState, defaultPreprocessorState } from '../../codecs/preprocessors'; EncoderState,
EncoderType,
EncoderOptions,
encoderMap,
} from '../../codecs/encoders';
import {
PreprocessorState,
defaultPreprocessorState,
} from '../../codecs/preprocessors';
import { decodeImage } from '../../codecs/decoders'; 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 { VectorResizeOptions, BitmapResizeOptions } from '../../codecs/resize/processor-meta';
BrowserResizeOptions, isWorkerOptions as isWorkerResizeOptions, isHqx, WorkerResizeOptions,
} from '../../codecs/resize/processor-meta';
import './custom-els/MultiPanel'; import './custom-els/MultiPanel';
import Results from '../results'; import Results from '../results';
import { ExpandIcon, CopyAcrossIconProps } from '../../lib/icons'; import { ExpandIcon, CopyAcrossIconProps } from '../../lib/icons';
@@ -104,24 +110,10 @@ async function preprocessImage(
if (preprocessData.resize.method === 'vector' && source.vectorImage) { if (preprocessData.resize.method === 'vector' && source.vectorImage) {
result = processor.vectorResize( result = processor.vectorResize(
source.vectorImage, 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 { } else {
result = processor.resize(result, preprocessData.resize as BrowserResizeOptions); result = processor.resize(result, preprocessData.resize as BitmapResizeOptions);
} }
} }
if (preprocessData.quantizer.enabled) { if (preprocessData.quantizer.enabled) {
@@ -168,7 +160,7 @@ function stateForNewSourceData(state: State, newSource: SourceImage): State {
for (const i of [0, 1]) { for (const i of [0, 1]) {
// Ditch previous encodings // Ditch previous encodings
const downloadUrl = state.sides[i].downloadUrl; const downloadUrl = state.sides[i].downloadUrl;
if (downloadUrl) URL.revokeObjectURL(downloadUrl); if (downloadUrl) URL.revokeObjectURL(downloadUrl!);
newState = cleanMerge(state, `sides.${i}`, { newState = cleanMerge(state, `sides.${i}`, {
preprocessed: undefined, preprocessed: undefined,
@@ -248,15 +240,13 @@ export default class Compress extends Component<Props, State> {
private readonly encodeCache = new ResultCache(); private readonly encodeCache = new ResultCache();
private readonly leftProcessor = new Processor(); private readonly leftProcessor = new Processor();
private readonly rightProcessor = new Processor(); private readonly rightProcessor = new Processor();
// For debouncing calls to updateImage for each side.
private readonly updateImageTimeoutIds: [number?, number?] = [undefined, undefined];
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
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
@@ -317,8 +307,10 @@ export default class Compress extends Component<Props, State> {
// The image only needs updated if the encoder/preprocessor settings have changed, or the // The image only needs updated if the encoder/preprocessor settings have changed, or the
// source has changed. // source has changed.
if (sourceDataChanged || encoderChanged || preprocessorChanged) { if (sourceDataChanged || encoderChanged || preprocessorChanged) {
this.queueUpdateImage(i, { this.updateImage(i, {
skipPreprocessing: !sourceDataChanged && !preprocessorChanged, skipPreprocessing: !sourceDataChanged && !preprocessorChanged,
}).catch((err) => {
console.error(err);
}); });
} }
} }
@@ -327,14 +319,9 @@ export default class Compress extends Component<Props, State> {
private async onCopyToOtherClick(index: 0 | 1) { private async onCopyToOtherClick(index: 0 | 1) {
const otherIndex = (index + 1) % 2; const otherIndex = (index + 1) % 2;
const oldSettings = this.state.sides[otherIndex]; const oldSettings = this.state.sides[otherIndex];
const newSettings = { ...this.state.sides[index] };
// Create a new object URL for the new settings. This avoids both sides sharing a URL, which
// means it can be safely revoked without impacting the other side.
if (newSettings.file) newSettings.downloadUrl = URL.createObjectURL(newSettings.file);
this.setState({ this.setState({
sides: cleanSet(this.state.sides, otherIndex, newSettings), sides: cleanSet(this.state.sides, otherIndex, this.state.sides[index]),
}); });
const result = await this.props.showSnack('Settings copied across', { const result = await this.props.showSnack('Settings copied across', {
@@ -449,7 +436,7 @@ export default class Compress extends Component<Props, State> {
newState = cleanMerge(newState, `sides.${i}.latestSettings.preprocessorState.resize`, { newState = cleanMerge(newState, `sides.${i}.latestSettings.preprocessorState.resize`, {
width: processed.width, width: processed.width,
height: processed.height, height: processed.height,
method: vectorImage ? 'vector' : 'lanczos3', method: vectorImage ? 'vector' : 'browser-high',
}); });
} }
@@ -465,27 +452,6 @@ export default class Compress extends Component<Props, State> {
} }
} }
/**
* Debounce the heavy lifting of updateImage.
* Otherwise, the thrashing causes jank, and sometimes crashes iOS Safari.
*/
private queueUpdateImage(index: number, options: UpdateImageOptions = {}): void {
// Call updateImage after this delay, unless queueUpdateImage is called again, in which case the
// timeout is reset.
const delay = 100;
clearTimeout(this.updateImageTimeoutIds[index]);
this.updateImageTimeoutIds[index] = self.setTimeout(
() => {
this.updateImage(index, options).catch((err) => {
console.error(err);
});
},
delay,
);
}
private async updateImage(index: number, options: UpdateImageOptions = {}): Promise<void> { private async updateImage(index: number, options: UpdateImageOptions = {}): Promise<void> {
const { const {
skipPreprocessing = false, skipPreprocessing = false,
@@ -579,15 +545,14 @@ 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}
preprocessorState={side.latestSettings.preprocessorState} preprocessorState={side.latestSettings.preprocessorState}
encoderState={side.latestSettings.encoderState} encoderState={side.latestSettings.encoderState}
onEncoderTypeChange={this.onEncoderTypeChange.bind(this, index as 0|1)} onEncoderTypeChange={this.onEncoderTypeChange.bind(this, index)}
onEncoderOptionsChange={this.onEncoderOptionsChange.bind(this, index as 0|1)} onEncoderOptionsChange={this.onEncoderOptionsChange.bind(this, index)}
onPreprocessorOptionsChange={this.onPreprocessorOptionsChange.bind(this, index as 0|1)} onPreprocessorOptionsChange={this.onPreprocessorOptionsChange.bind(this, index)}
/> />
)); ));
@@ -595,14 +560,13 @@ 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}
source={source} source={source}
loading={loading || side.loading} loading={loading || side.loading}
copyDirection={copyDirections[index]} copyDirection={copyDirections[index]}
onCopyToOtherClick={this.onCopyToOtherClick.bind(this, index as 0|1)} onCopyToOtherClick={this.onCopyToOtherClick.bind(this, index)}
buttonPosition={mobileView ? 'stack-right' : buttonPositions[index]} buttonPosition={mobileView ? 'stack-right' : buttonPositions[index]}
> >
{!mobileView ? null : [ {!mobileView ? null : [

View File

@@ -40,7 +40,6 @@
position: relative; position: relative;
display: flex; display: flex;
flex-flow: column; flex-flow: column;
overflow: hidden;
// Reorder so headings appear after content: // Reorder so headings appear after content:
& > :nth-child(1) { & > :nth-child(1) {

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

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

@@ -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
}), }),
@@ -254,11 +251,6 @@ module.exports = async function (_, env) {
filename: '_headers', filename: '_headers',
}), }),
isProd && new AssetTemplatePlugin({
template: path.join(__dirname, '_redirects.ejs'),
filename: '_redirects',
}),
new ScriptExtHtmlPlugin({ new ScriptExtHtmlPlugin({
inline: ['first'] inline: ['first']
}), }),