Compare commits

...

6 Commits

Author SHA1 Message Date
3541ce7492 1.8.1 2019-06-19 12:25:18 +01:00
b6fae0eb4c Hqx fix (#629)
* Optimising wasm so it doesn't break Chrome

* Simpler dockerfile (thanks surma!)
2019-06-19 12:24:24 +01:00
fbaa282f07 Update dependency ts-loader to v6.0.3 (#627)
Update dependency ts-loader to v6.0.3
2019-06-18 12:56:24 +01:00
6136ae7411 Update dependency ts-loader to v6.0.3 2019-06-18 11:30:57 +00:00
f024747299 1.8.0 2019-06-18 12:24:41 +01:00
66feffcc49 Add Hq{2,3,4}x (#624)
* Scaling works on native

* It works in wasm

* Integrate with UI

* Remove benchmark

* Integrate Hqx into Resizer module

* Link against repo for hqx

* Remove unused defaultOpts

* Re-add test file

* Adding size dropdown

* Chrome: go and sit on the naughty step

* Better docs

* Review

* Add link to crbug

* Update src/codecs/processor-worker/index.ts

Co-Authored-By: Jake Archibald <jaffathecake@gmail.com>

* Terminate worker inbetween resize jobs
2019-06-18 12:23:22 +01:00
27 changed files with 568 additions and 40 deletions

5
codecs/hqx/.gitignore vendored Normal file
View File

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

37
codecs/hqx/Cargo.toml Normal file
View File

@ -0,0 +1,37 @@
[package]
name = "squooshhqx"
version = "0.1.0"
authors = ["Surma <surma@surma.link>"]
[lib]
crate-type = ["cdylib"]
[features]
default = ["console_error_panic_hook", "wee_alloc"]
[dependencies]
cfg-if = "0.1.2"
wasm-bindgen = "0.2.38"
# lazy_static = "1.0.0"
hqx = {git = "https://github.com/CryZe/wasmboy-rs", tag="v0.1.2"}
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.1", optional = true }
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. It is slower than the default
# allocator, however.
#
# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now.
wee_alloc = { version = "0.4.2", optional = true }
[dev-dependencies]
wasm-bindgen-test = "0.2"
[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"
lto = true

21
codecs/hqx/Dockerfile Normal file
View File

@ -0,0 +1,21 @@
FROM ubuntu
RUN apt-get update && \
apt-get install -qqy git build-essential cmake python2.7
RUN git clone --depth 1 --recursive https://github.com/WebAssembly/wabt /usr/src/wabt
RUN mkdir -p /usr/src/wabt/build
WORKDIR /usr/src/wabt/build
RUN cmake .. -DCMAKE_INSTALL_PREFIX=/opt/wabt && \
make && \
make install
FROM rust
RUN rustup install nightly && \
rustup target add --toolchain nightly wasm32-unknown-unknown && \
cargo install wasm-pack
COPY --from=0 /opt/wabt /opt/wabt
RUN mkdir /opt/binaryen && \
curl -L https://github.com/WebAssembly/binaryen/releases/download/1.38.32/binaryen-1.38.32-x86-linux.tar.gz | tar -xzf - -C /opt/binaryen --strip 1
ENV PATH="/opt/binaryen:/opt/wabt/bin:${PATH}"
WORKDIR /src

26
codecs/hqx/build.sh Executable file
View File

@ -0,0 +1,26 @@
#!/bin/bash
set -e
echo "============================================="
echo "Compiling wasm"
echo "============================================="
(
rustup run nightly \
wasm-pack build --target no-modules
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://bugs.chromium.org/p/chromium/issues/detail?id=97480 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 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"

24
codecs/hqx/index.html Normal file
View File

@ -0,0 +1,24 @@
<!doctype html>
<script src ="./pkg/squooshhqx.js"></script>
<script type="module">
async function run() {
await wasm_bindgen("./pkg/squooshhqx_bg.wasm");
const bitmap = await createImageBitmap(await fetch("https://i.imgur.com/MNDnBSc.png").then(r => r.blob()));
const canvas = document.createElement("canvas");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(bitmap, 0, 0);
const imgdata = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
const factor = 4;
const r = wasm_bindgen.resize(new Uint32Array(imgdata.data.buffer), bitmap.width, bitmap.height, factor);
canvas.width = bitmap.width * factor;
canvas.height = bitmap.height * factor;
const output = new ImageData(new Uint8ClampedArray(r.buffer), canvas.width, canvas.height);
ctx.putImageData(output, 0, 0);
canvas.style = `width: ${canvas.width}px; height: ${canvas.height}px; image-rendering: pixelated;`;
document.body.append(canvas);
}
run();
</script>

4
codecs/hqx/package-lock.json generated Normal file
View File

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

7
codecs/hqx/package.json Normal file
View File

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

View File

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

20
codecs/hqx/pkg/squooshhqx.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
/* tslint:disable */
/**
* @param {Uint32Array} input_image
* @param {number} input_width
* @param {number} input_height
* @param {number} factor
* @returns {Uint32Array}
*/
export function resize(input_image: Uint32Array, input_width: number, input_height: number, factor: number): Uint32Array;
/**
* If `module_or_path` is {RequestInfo}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {RequestInfo | BufferSource | WebAssembly.Module} module_or_path
*
* @returns {Promise<any>}
*/
export default function init (module_or_path: RequestInfo | BufferSource | WebAssembly.Module): Promise<any>;

View File

@ -0,0 +1,97 @@
(function() {
const __exports = {};
let wasm;
let cachegetUint32Memory = null;
function getUint32Memory() {
if (cachegetUint32Memory === null || cachegetUint32Memory.buffer !== wasm.memory.buffer) {
cachegetUint32Memory = new Uint32Array(wasm.memory.buffer);
}
return cachegetUint32Memory;
}
let WASM_VECTOR_LEN = 0;
function passArray32ToWasm(arg) {
const ptr = wasm.__wbindgen_malloc(arg.length * 4);
getUint32Memory().set(arg, ptr / 4);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function getArrayU32FromWasm(ptr, len) {
return getUint32Memory().subarray(ptr / 4, ptr / 4 + len);
}
let cachedGlobalArgumentPtr = null;
function globalArgumentPtr() {
if (cachedGlobalArgumentPtr === null) {
cachedGlobalArgumentPtr = wasm.__wbindgen_global_argument_ptr();
}
return cachedGlobalArgumentPtr;
}
/**
* @param {Uint32Array} input_image
* @param {number} input_width
* @param {number} input_height
* @param {number} factor
* @returns {Uint32Array}
*/
__exports.resize = function(input_image, input_width, input_height, factor) {
const ptr0 = passArray32ToWasm(input_image);
const len0 = WASM_VECTOR_LEN;
const retptr = globalArgumentPtr();
wasm.resize(retptr, ptr0, len0, input_width, input_height, factor);
const mem = getUint32Memory();
const rustptr = mem[retptr / 4];
const rustlen = mem[retptr / 4 + 1];
const realRet = getArrayU32FromWasm(rustptr, rustlen).slice();
wasm.__wbindgen_free(rustptr, rustlen * 4);
return realRet;
};
function init(module) {
let result;
const imports = {};
if (module instanceof URL || typeof module === 'string' || module instanceof Request) {
const response = fetch(module);
if (typeof WebAssembly.instantiateStreaming === 'function') {
result = WebAssembly.instantiateStreaming(response, imports)
.catch(e => {
console.warn("`WebAssembly.instantiateStreaming` failed. Assuming this is because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
return response
.then(r => r.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes, imports));
});
} else {
result = response
.then(r => r.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes, imports));
}
} else {
result = WebAssembly.instantiate(module, imports)
.then(result => {
if (result instanceof WebAssembly.Instance) {
return { instance: result, module };
} else {
return result;
}
});
}
return result.then(({instance, module}) => {
wasm = instance.exports;
init.__wbindgen_wasm_module = module;
return wasm;
});
}
self.wasm_bindgen = Object.assign(init, __exports);
})();

6
codecs/hqx/pkg/squooshhqx_bg.d.ts vendored Normal file
View File

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

Binary file not shown.

55
codecs/hqx/src/lib.rs Normal file
View File

@ -0,0 +1,55 @@
extern crate cfg_if;
extern crate hqx;
extern crate wasm_bindgen;
mod utils;
use cfg_if::cfg_if;
use wasm_bindgen::prelude::*;
cfg_if! {
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
if #[cfg(feature = "wee_alloc")] {
extern crate wee_alloc;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
}
}
#[wasm_bindgen]
#[no_mangle]
pub fn resize(
input_image: Vec<u32>,
input_width: usize,
input_height: usize,
factor: usize,
) -> Vec<u32> {
let num_output_pixels = input_width * input_height * factor * factor;
let mut output_image = Vec::<u32>::with_capacity(num_output_pixels * 4);
output_image.resize(num_output_pixels, 0);
match factor {
2 => hqx::hq2x(
input_image.as_slice(),
output_image.as_mut_slice(),
input_width,
input_height,
),
3 => hqx::hq3x(
input_image.as_slice(),
output_image.as_mut_slice(),
input_width,
input_height,
),
4 => hqx::hq4x(
input_image.as_slice(),
output_image.as_mut_slice(),
input_width,
input_height,
),
_ => unreachable!(),
};
return output_image;
}

17
codecs/hqx/src/utils.rs Normal file
View File

@ -0,0 +1,17 @@
use cfg_if::cfg_if;
cfg_if! {
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function at least once during initialization, and then
// we will get better error messages if our code ever panics.
//
// For more details see
// https://github.com/rustwasm/console_error_panic_hook#readme
if #[cfg(feature = "console_error_panic_hook")] {
extern crate console_error_panic_hook;
pub use self::console_error_panic_hook::set_once as set_panic_hook;
} else {
#[inline]
pub fn set_panic_hook() {}
}
}

49
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "squoosh",
"version": "1.7.0",
"version": "1.8.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -5465,7 +5465,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -5486,12 +5487,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -5506,17 +5509,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -5633,7 +5639,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -5645,6 +5652,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -5659,6 +5667,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -5666,12 +5675,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -5690,6 +5701,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -5770,7 +5782,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -5782,6 +5795,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -5867,7 +5881,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -5903,6 +5918,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -5922,6 +5938,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -5965,12 +5982,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},
@ -14164,9 +14183,9 @@
"dev": true
},
"ts-loader": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.0.2.tgz",
"integrity": "sha512-kkF3sGf3oBUehlvXI9fkbItbFTnNgGkYAz91vtWnsKAU4m+LAmQjuby7uTZNo3As+/zHLuyB052SkQDY6vLXtg==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.0.3.tgz",
"integrity": "sha512-iICBD4PryhnGNdtaDva49UGODHFVcuK7p4+G8CP1TVcUhTC4hkcy4MC2dzWwALSnpOzfUfA/4u8B2F64wsMgjQ==",
"dev": true,
"requires": {
"chalk": "^2.3.0",

View File

@ -1,7 +1,7 @@
{
"private": true,
"name": "squoosh",
"version": "1.7.0",
"version": "1.8.1",
"license": "apache-2.0",
"scripts": {
"start": "webpack-dev-server --host 0.0.0.0 --hot",
@ -59,7 +59,7 @@
"style-loader": "0.23.1",
"terser-webpack-plugin": "1.3.0",
"travis-size-report": "1.0.1",
"ts-loader": "6.0.2",
"ts-loader": "6.0.3",
"tslint": "5.17.0",
"tslint-config-airbnb": "5.11.1",
"tslint-config-semistandard": "8.0.1",

View File

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

View File

@ -0,0 +1,32 @@
import wasmUrl from '../../../codecs/hqx/pkg/squooshhqx_bg.wasm';
import '../../../codecs/hqx/pkg/squooshhqx';
import { HqxOptions } from './processor-meta';
interface WasmBindgenExports {
resize: typeof import('../../../codecs/hqx/pkg/squooshhqx').resize;
}
type WasmBindgen = ((url: string) => Promise<void>) & WasmBindgenExports;
declare var wasm_bindgen: WasmBindgen;
const ready = wasm_bindgen(wasmUrl);
export async function hqx(
data: ImageData,
opts: HqxOptions,
): Promise<ImageData> {
const input = data;
await ready;
const result = wasm_bindgen.resize(
new Uint32Array(input.data.buffer),
input.width,
input.height,
opts.factor,
);
return new ImageData(
new Uint8ClampedArray(result.buffer),
data.width * opts.factor,
data.height * opts.factor,
);
}

View File

@ -1,4 +1,6 @@
import { expose } from 'comlink';
import { isHqx } from '../resize/processor-meta';
import { clamp } from '../util';
async function mozjpegEncode(
data: ImageData, options: import('../mozjpeg/encoder-meta').EncodeOptions,
@ -31,6 +33,18 @@ async function rotate(
async function resize(
data: ImageData, opts: import('../resize/processor-meta').WorkerResizeOptions,
): Promise<ImageData> {
if (isHqx(opts)) {
const { hqx } = await import(
/* webpackChunkName: "process-hqx" */
'../hqx/processor');
const widthRatio = opts.width / data.width;
const heightRatio = opts.height / data.height;
const ratio = Math.max(widthRatio, heightRatio);
if (ratio <= 1) return data;
const factor = clamp(Math.ceil(ratio), { min: 2, max: 4 }) as 2|3|4;
return hqx(data, { factor });
}
const { resize } = await import(
/* webpackChunkName: "process-resize" */
'../resize/processor');
@ -63,7 +77,15 @@ async function webpDecode(data: ArrayBuffer): Promise<ImageData> {
return decode(data);
}
const exports = { mozjpegEncode, quantize, rotate, resize, optiPngEncode, webpEncode, webpDecode };
const exports = {
mozjpegEncode,
quantize,
rotate,
resize,
optiPngEncode,
webpEncode,
webpDecode,
};
export type ProcessorWorkerApi = typeof exports;
expose(exports, self);

View File

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

View File

@ -12,8 +12,9 @@ import Select from '../../components/select';
interface Props {
isVector: Boolean;
inputWidth: number;
inputHeight: number;
options: ResizeOptions;
aspect: number;
onChange(newOptions: ResizeOptions): void;
}
@ -21,12 +22,21 @@ interface State {
maintainAspect: boolean;
}
const sizePresets = [0.25, 0.3333, 0.5, 1, 2, 3, 4];
export default class ResizerOptions extends Component<Props, State> {
state: State = {
maintainAspect: true,
};
form?: HTMLFormElement;
private form?: HTMLFormElement;
private presetWidths: { [idx: number]: number } = {};
private presetHeights: { [idx: number]: number } = {};
constructor(props: Props) {
super(props);
this.generatePresetValues(props.inputWidth, props.inputHeight);
}
private reportOptions() {
const form = this.form!;
@ -53,18 +63,31 @@ export default class ResizerOptions extends Component<Props, State> {
this.reportOptions();
}
private getAspect() {
return this.props.inputWidth / this.props.inputHeight;
}
componentDidUpdate(prevProps: Props, prevState: State) {
if (!prevState.maintainAspect && this.state.maintainAspect) {
this.form!.height.value = Math.round(Number(this.form!.width.value) / this.props.aspect);
this.form!.height.value = Math.round(Number(this.form!.width.value) / this.getAspect());
this.reportOptions();
}
}
componentWillReceiveProps(nextProps: Props) {
if (
this.props.inputWidth !== nextProps.inputWidth ||
this.props.inputHeight !== nextProps.inputHeight
) {
this.generatePresetValues(nextProps.inputWidth, nextProps.inputHeight);
}
}
@bind
private onWidthInput() {
if (this.state.maintainAspect) {
const width = inputFieldValueAsNumber(this.form!.width);
this.form!.height.value = Math.round(width / this.props.aspect);
this.form!.height.value = Math.round(width / this.getAspect());
}
this.reportOptions();
@ -74,12 +97,44 @@ export default class ResizerOptions extends Component<Props, State> {
private onHeightInput() {
if (this.state.maintainAspect) {
const height = inputFieldValueAsNumber(this.form!.height);
this.form!.width.value = Math.round(height * this.props.aspect);
this.form!.width.value = Math.round(height * this.getAspect());
}
this.reportOptions();
}
private generatePresetValues(width: number, height: number) {
for (const preset of sizePresets) {
this.presetWidths[preset] = Math.round(width * preset);
this.presetHeights[preset] = Math.round(height * preset);
}
}
private getPreset(): number | string {
const { width, height } = this.props.options;
for (const preset of sizePresets) {
if (
width === this.presetWidths[preset] &&
height === this.presetHeights[preset]
) return preset;
}
return 'custom';
}
@bind
private onPresetChange(event: Event) {
const select = event.target as HTMLSelectElement;
if (select.value === 'custom') return;
const multiplier = Number(select.value);
(this.form!.width as HTMLInputElement).value =
Math.round(this.props.inputWidth * multiplier) + '';
(this.form!.height as HTMLInputElement).value =
Math.round(this.props.inputHeight * multiplier) + '';
this.reportOptions();
}
render({ options, isVector }: Props, { maintainAspect }: State) {
return (
<form ref={linkRef(this, 'form')} class={style.optionsSection} onSubmit={preventDefault}>
@ -95,12 +150,22 @@ export default class ResizerOptions extends Component<Props, State> {
<option value="mitchell">Mitchell</option>
<option value="catrom">Catmull-Rom</option>
<option value="triangle">Triangle (bilinear)</option>
<option value="hqx">hqx (pixel art)</option>
<option value="browser-pixelated">Browser pixelated</option>
<option value="browser-low">Browser low quality</option>
<option value="browser-medium">Browser medium quality</option>
<option value="browser-high">Browser high quality</option>
</Select>
</label>
<label class={style.optionTextFirst}>
Preset:
<Select value={this.getPreset()} onChange={this.onPresetChange}>
{sizePresets.map(preset =>
<option value={preset}>{preset * 100}%</option>,
)}
<option value="custom">Custom</option>
</Select>
</label>
<label class={style.optionTextFirst}>
Width:
<input

View File

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

View File

@ -25,3 +25,12 @@ export function initEmscriptenModule<T extends EmscriptenWasm.Module>(
});
});
}
interface ClampOpts {
min?: number;
max?: number;
}
export function clamp(x: number, opts: ClampOpts): number {
return Math.min(Math.max(x, opts.min || Number.MIN_VALUE), opts.max || Number.MAX_VALUE);
}

View File

@ -110,7 +110,10 @@ export default class App extends Component<Props, State> {
@bind
private openEditor() {
if (this.state.isEditorOpen) return;
history.pushState(null, '', ROUTE_EDITOR);
// Change path, but preserve query string.
const editorURL = new URL(location.href);
editorURL.pathname = ROUTE_EDITOR;
history.pushState(null, '', editorURL.href);
this.setState({ isEditorOpen: true });
}

View File

@ -146,12 +146,14 @@ export default class Options extends Component<Props, State> {
{preprocessorState.resize.enabled ?
<ResizeOptionsComponent
isVector={Boolean(source && source.vectorImage)}
aspect={source ? source.processed.width / source.processed.height : 1}
inputWidth={source ? source.processed.width : 1}
inputHeight={source ? source.processed.height : 1}
options={preprocessorState.resize}
onChange={this.onResizeOptionsChange}
/>
: null}
</Expander>
<label class={style.sectionEnabler}>
<Checkbox
name="quantizer.enable"

View File

@ -24,7 +24,7 @@ import { decodeImage } from '../../codecs/decoders';
import { cleanMerge, cleanSet } from '../../lib/clean-modify';
import Processor from '../../codecs/processor';
import {
BrowserResizeOptions, isWorkerOptions as isWorkerResizeOptions,
BrowserResizeOptions, isWorkerOptions as isWorkerResizeOptions, isHqx, WorkerResizeOptions,
} from '../../codecs/resize/processor-meta';
import './custom-els/MultiPanel';
import Results from '../results';
@ -106,6 +106,18 @@ async function preprocessImage(
source.vectorImage,
preprocessData.resize,
);
} else if (isHqx(preprocessData.resize)) {
// Hqx can only do x2, x3 or x4.
result = await processor.workerResize(result, preprocessData.resize);
// Seems like the globals from Rust from hqx and resize are conflicting.
// For now we can fix that by terminating the worker.
// TODO: Use wasm-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 {

View File

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