forked from external-repos/squoosh
Compare commits
2 Commits
normalize-
...
jpeg-simd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
173d1ea1e5 | ||
|
|
0e90f805a4 |
39
.babelrc
39
.babelrc
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-typescript",
|
||||
{
|
||||
"jsxPragma": "h"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@parcel/babel-preset-env",
|
||||
{
|
||||
"loose": true,
|
||||
"bugfixes": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"@babel/plugin-transform-react-jsx",
|
||||
{
|
||||
"loose": true,
|
||||
"pragma": "h",
|
||||
"pragmaFrag": "Fragment"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@babel/plugin-proposal-decorators",
|
||||
{
|
||||
"legacy": true
|
||||
}
|
||||
],
|
||||
[
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
{
|
||||
"loose": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,6 +4,3 @@ node_modules
|
||||
*.scss.d.ts
|
||||
*.css.d.ts
|
||||
*.o
|
||||
.parcel-cache
|
||||
.cache
|
||||
/public
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "@parcel/config-default",
|
||||
"transformers": {
|
||||
"*.wasm": ["@parcel/transformer-raw"]
|
||||
},
|
||||
"packagers": {
|
||||
"*.wasm": "@parcel/packager-raw"
|
||||
}
|
||||
}
|
||||
15
.proxyrc.js
15
.proxyrc.js
@@ -1,15 +0,0 @@
|
||||
module.exports = function(app) {
|
||||
// `app` is an Express instance
|
||||
app.use(function(req, res, next) {
|
||||
const sh = res.setHeader;
|
||||
res.setHeader = function(key, value) {
|
||||
// remove pointless/incorrect charset from binary responses:
|
||||
if (/^content-type$/i.test(key)) {
|
||||
const m = value && value.match(/^(image\/|application\/wasm); charset=.+$/);
|
||||
if (m) value = m[1];
|
||||
}
|
||||
return sh.call(this, key, value);
|
||||
}
|
||||
next();
|
||||
});
|
||||
};
|
||||
@@ -5,5 +5,7 @@ ENV CXXFLAGS "${CFLAGS} -std=c++17"
|
||||
ENV LDFLAGS "${CFLAGS}"
|
||||
# Build and cache standard libraries with these flags
|
||||
RUN emcc ${CXXFLAGS} --bind -xc++ /dev/null -o /dev/null
|
||||
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
|
||||
RUN /bin/bash -ic 'nvm install 14.7.0'
|
||||
WORKDIR /src
|
||||
CMD ["sh", "-c", "emmake make -j`nproc`"]
|
||||
|
||||
13
codecs/mozjpeg_enc/.emscripten
Normal file
13
codecs/mozjpeg_enc/.emscripten
Normal file
@@ -0,0 +1,13 @@
|
||||
import os
|
||||
emsdk_path = "/emsdk/"
|
||||
#NODE_JS = emsdk_path + '/node/12.18.1_64bit/bin/node'
|
||||
# We need a newer version of Node that supports the most recent iteration of Wasm SIMD
|
||||
# and need to enable that experimental support via flag so that autoconf can successfully
|
||||
# detect SIMD support.
|
||||
NODE_JS=["/root/.nvm/versions/node/v14.7.0/bin/node", "--experimental-wasm-simd"]
|
||||
LLVM_ROOT = emsdk_path + '/upstream/bin'
|
||||
BINARYEN_ROOT = emsdk_path + '/upstream'
|
||||
EMSCRIPTEN_ROOT = emsdk_path + '/upstream/emscripten'
|
||||
TEMP_DIR = emsdk_path + '/tmp'
|
||||
COMPILER_ENGINE = NODE_JS
|
||||
JS_ENGINES = [NODE_JS]
|
||||
@@ -2,14 +2,24 @@ CODEC_URL := https://github.com/mozilla/mozjpeg/archive/v3.3.1.tar.gz
|
||||
CODEC_DIR := node_modules/mozjpeg
|
||||
CODEC_OUT_RELATIVE := .libs/libjpeg.a rdswitch.o
|
||||
CODEC_OUT := $(addprefix $(CODEC_DIR)/, $(CODEC_OUT_RELATIVE))
|
||||
OUT_JS := mozjpeg_enc.js
|
||||
OUT_JS := mozjpeg_enc_simd.js
|
||||
OUT_WASM := $(OUT_JS:.js=.wasm)
|
||||
|
||||
CFLAGS += -msimd128 -msse --em-config /src/.emscripten
|
||||
CXXFLAGS += -msimd128 -msse --em-config /src/.emscripten
|
||||
|
||||
#CFLAGS += --em-config /src/.emscripten
|
||||
#CXXFLAGS += --em-config /src/.emscripten
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(OUT_JS)
|
||||
|
||||
%.js: %.cpp $(CODEC_OUT)
|
||||
# We just to disable the overridden NODE_JS binary because
|
||||
# `--closure 1` can’t handle arrays in `.emscripten` it seems.
|
||||
# https://github.com/emscripten-core/emsdk/issues/583
|
||||
export EM_NODE_JS='/root/.nvm/versions/node/v14.7.0/bin/node' && \
|
||||
$(CXX) \
|
||||
-I $(CODEC_DIR) \
|
||||
${CXXFLAGS} \
|
||||
@@ -18,6 +28,7 @@ all: $(OUT_JS)
|
||||
--closure 1 \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MODULARIZE=1 \
|
||||
-s EXPORT_ES6=1 \
|
||||
-s 'EXPORT_NAME="$(basename $(@F))"' \
|
||||
-o $@ \
|
||||
$+
|
||||
|
||||
62
codecs/mozjpeg_enc/bench.d8.js
Normal file
62
codecs/mozjpeg_enc/bench.d8.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import mozjpeg from "./mozjpeg_enc.js";
|
||||
import mozjpegSimd from "./mozjpeg_enc_simd.js";
|
||||
|
||||
const width = 2048;
|
||||
const height = 2048;
|
||||
const size = width * height * 4;
|
||||
const data = new Uint8ClampedArray(size);
|
||||
for(let i = 0; i < size; i++) {
|
||||
data[i] = Math.random() * 256;
|
||||
}
|
||||
|
||||
mozjpeg({
|
||||
onRuntimeInitialized() {
|
||||
const start = performance.now();
|
||||
const result = this.encode(data, width, height, {
|
||||
quality: 75,
|
||||
baseline: false,
|
||||
arithmetic: false,
|
||||
progressive: true,
|
||||
optimize_coding: true,
|
||||
smoothing: 0,
|
||||
color_space: 3,
|
||||
quant_table: 3,
|
||||
trellis_multipass: false,
|
||||
trellis_opt_zero: false,
|
||||
trellis_opt_table: false,
|
||||
trellis_loops: 1,
|
||||
auto_subsample: true,
|
||||
chroma_subsample: 2,
|
||||
separate_chroma_quality: false,
|
||||
chroma_quality: 75,
|
||||
});
|
||||
const end = performance.now();
|
||||
console.log("No SIMD", end - start);
|
||||
}
|
||||
})
|
||||
|
||||
mozjpegSimd({
|
||||
onRuntimeInitialized() {
|
||||
const start = performance.now();
|
||||
const result = this.encode(data, width, height, {
|
||||
quality: 75,
|
||||
baseline: false,
|
||||
arithmetic: false,
|
||||
progressive: true,
|
||||
optimize_coding: true,
|
||||
smoothing: 0,
|
||||
color_space: 3,
|
||||
quant_table: 3,
|
||||
trellis_multipass: false,
|
||||
trellis_opt_zero: false,
|
||||
trellis_opt_table: false,
|
||||
trellis_loops: 1,
|
||||
auto_subsample: true,
|
||||
chroma_subsample: 2,
|
||||
separate_chroma_quality: false,
|
||||
chroma_quality: 75,
|
||||
});
|
||||
const end = performance.now();
|
||||
console.log("SIMD", end - start);
|
||||
}
|
||||
})
|
||||
@@ -1,26 +1,14 @@
|
||||
<!doctype html>
|
||||
<script src='mozjpeg_enc.js'></script>
|
||||
<script>
|
||||
const module = mozjpeg_enc();
|
||||
|
||||
async function loadImage(src) {
|
||||
// Load image
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
await new Promise(resolve => img.onload = resolve);
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
[canvas.width, canvas.height] = [img.width, img.height];
|
||||
// Draw image onto canvas
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
return ctx.getImageData(0, 0, img.width, img.height);
|
||||
}
|
||||
|
||||
module.onRuntimeInitialized = async _ => {
|
||||
console.log('Version:', module.version().toString(16));
|
||||
const module = mozjpeg_enc({
|
||||
loadFile() {
|
||||
return "./mozjpeg_enc.wasm";
|
||||
},
|
||||
async onRuntimeInitialized() {
|
||||
//console.log('Version:', module.version().toString(16));
|
||||
const image = await loadImage('../example.png');
|
||||
const result = module.encode(image.data, image.width, image.height, {
|
||||
const result = this.encode(image.data, image.width, image.height, {
|
||||
quality: 75,
|
||||
baseline: false,
|
||||
arithmetic: false,
|
||||
@@ -44,5 +32,21 @@
|
||||
const img = document.createElement('img');
|
||||
img.src = blobURL;
|
||||
document.body.appendChild(img);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function loadImage(src) {
|
||||
// Load image
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
await new Promise(resolve => img.onload = resolve);
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
[canvas.width, canvas.height] = [img.width, img.height];
|
||||
// Draw image onto canvas
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
return ctx.getImageData(0, 0, img.width, img.height);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
var mozjpeg_enc = (function() {
|
||||
var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined;
|
||||
if (typeof __filename !== 'undefined') _scriptDir = _scriptDir || __filename;
|
||||
var _scriptDir = import.meta.url;
|
||||
|
||||
return (
|
||||
function(mozjpeg_enc) {
|
||||
mozjpeg_enc = mozjpeg_enc || {};
|
||||
@@ -63,10 +63,4 @@ d.run=Cb;if(d.preInit)for("function"==typeof d.preInit&&(d.preInit=[d.preInit]);
|
||||
}
|
||||
);
|
||||
})();
|
||||
if (typeof exports === 'object' && typeof module === 'object')
|
||||
module.exports = mozjpeg_enc;
|
||||
else if (typeof define === 'function' && define['amd'])
|
||||
define([], function() { return mozjpeg_enc; });
|
||||
else if (typeof exports === 'object')
|
||||
exports["mozjpeg_enc"] = mozjpeg_enc;
|
||||
|
||||
export default mozjpeg_enc;
|
||||
Binary file not shown.
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"name": "codecs",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
3
global.d.ts
vendored
3
global.d.ts
vendored
@@ -1,3 +1,6 @@
|
||||
declare const __webpack_public_path__: string;
|
||||
declare const PRERENDER: boolean;
|
||||
|
||||
declare interface NodeModule {
|
||||
hot: any;
|
||||
}
|
||||
|
||||
14014
package-lock.json
generated
14014
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@@ -4,8 +4,8 @@
|
||||
"version": "1.11.4",
|
||||
"license": "apache-2.0",
|
||||
"scripts": {
|
||||
"start": "parcel serve src/index.html --no-autoinstall --host 0.0.0.0 --dist-dir build",
|
||||
"build": "parcel build src/index.html --no-autoinstall --dist-dir build",
|
||||
"start": "webpack-dev-server --host 0.0.0.0 --hot",
|
||||
"build": "webpack -p",
|
||||
"lint": "tslint -c tslint.json -p tsconfig.json -t verbose",
|
||||
"lintfix": "tslint -c tslint.json -p tsconfig.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'",
|
||||
"sizereport": "sizereport --config"
|
||||
@@ -15,37 +15,63 @@
|
||||
"pre-commit": "npm run lint"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"codecs": "0.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "10.14.15",
|
||||
"@types/pretty-bytes": "5.1.0",
|
||||
"@types/webassembly-js-api": "0.0.3",
|
||||
"@webcomponents/custom-elements": "1.2.4",
|
||||
"@webpack-cli/serve": "0.1.8",
|
||||
"assets-webpack-plugin": "3.9.10",
|
||||
"chalk": "2.4.2",
|
||||
"chokidar": "3.0.2",
|
||||
"classnames": "2.2.6",
|
||||
"clean-webpack-plugin": "1.0.1",
|
||||
"comlink": "3.1.1",
|
||||
"copy-webpack-plugin": "5.0.4",
|
||||
"critters-webpack-plugin": "2.4.0",
|
||||
"css-loader": "1.0.1",
|
||||
"ejs": "2.6.2",
|
||||
"escape-string-regexp": "2.0.0",
|
||||
"exports-loader": "0.7.0",
|
||||
"file-drop-element": "0.2.0",
|
||||
"file-loader": "4.2.0",
|
||||
"gzip-size": "5.1.1",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"husky": "3.0.4",
|
||||
"idb-keyval": "3.2.0",
|
||||
"linkstate": "1.1.1",
|
||||
"loader-utils": "1.2.3",
|
||||
"mini-css-extract-plugin": "0.8.0",
|
||||
"minimatch": "3.0.4",
|
||||
"node-fetch": "2.6.0",
|
||||
"node-sass": "4.13.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"pointer-tracker": "^2.4.0",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.1",
|
||||
"pointer-tracker": "2.0.3",
|
||||
"preact": "8.4.2",
|
||||
"prerender-loader": "1.3.0",
|
||||
"pretty-bytes": "5.3.0",
|
||||
"progress-bar-webpack-plugin": "1.12.1",
|
||||
"raw-loader": "3.1.0",
|
||||
"readdirp": "3.1.2",
|
||||
"sass": "^1.26.10",
|
||||
"sass-loader": "7.3.1",
|
||||
"script-ext-html-webpack-plugin": "2.1.4",
|
||||
"source-map-loader": "0.2.4",
|
||||
"style-loader": "1.0.0",
|
||||
"terser-webpack-plugin": "1.4.1",
|
||||
"travis-size-report": "1.1.0",
|
||||
"ts-loader": "6.0.3",
|
||||
"tslint": "5.19.0",
|
||||
"tslint-config-airbnb": "5.11.1",
|
||||
"tslint-config-semistandard": "8.0.1",
|
||||
"tslint-react": "4.0.0",
|
||||
"typescript": "3.5.3"
|
||||
"typed-css-modules": "0.4.2",
|
||||
"typescript": "3.5.3",
|
||||
"url-loader": "2.1.0",
|
||||
"webpack": "4.39.3",
|
||||
"webpack-bundle-analyzer": "3.4.1",
|
||||
"webpack-cli": "3.3.4",
|
||||
"webpack-dev-server": "3.8.0",
|
||||
"worker-plugin": "3.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../codecs
|
||||
@@ -1,6 +1,6 @@
|
||||
import { builtinDecode, sniffMimeType, canDecodeImage } from '../lib/util';
|
||||
import Processor from './processor';
|
||||
import webpDataUrl from 'url:./tiny.webp';
|
||||
import webpDataUrl from 'url-loader!./tiny.webp';
|
||||
|
||||
const webPSupported = canDecodeImage(webpDataUrl);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { h, Component } from 'preact';
|
||||
import * as style from '../../components/Options/style.module.scss';
|
||||
import { bind } from '../../lib/initial-util';
|
||||
import * as style from '../../components/Options/style.scss';
|
||||
import Range from '../../components/range';
|
||||
|
||||
interface EncodeOptions {
|
||||
@@ -25,7 +26,8 @@ export default function qualityOption(opts: QualityOptionArg = {}) {
|
||||
} = opts;
|
||||
|
||||
class QualityOptions extends Component<Props, {}> {
|
||||
onChange = (event: Event) => {
|
||||
@bind
|
||||
onChange(event: Event) {
|
||||
const el = event.currentTarget as HTMLInputElement;
|
||||
this.props.onChange({ quality: Number(el.value) });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { resize } from '../../codec-impl/hqx/pkg';
|
||||
import { resize } from '../../../codecs/hqx/pkg';
|
||||
import { HqxOptions } from './processor-meta';
|
||||
|
||||
export async function hqx(
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { bind } from '../../lib/initial-util';
|
||||
import { inputFieldValueAsNumber, konami, preventDefault } from '../../lib/util';
|
||||
import { QuantizeOptions } from './processor-meta';
|
||||
import * as style from '../../components/Options/style.module.scss';
|
||||
import * as style from '../../components/Options/style.scss';
|
||||
import Expander from '../../components/expander';
|
||||
import Select from '../../components/select';
|
||||
import Range from '../../components/range';
|
||||
@@ -26,7 +27,8 @@ export default class QuantizerOptions extends Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
onChange = (event: Event) => {
|
||||
@bind
|
||||
onChange(event: Event) {
|
||||
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
|
||||
const { options } = this.props;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import imagequant, { QuantizerModule } from '../../codec-impl/imagequant/imagequant';
|
||||
import wasmUrl from 'url:../../codec-impl/imagequant/imagequant.wasm';
|
||||
import imagequant, { QuantizerModule } from '../../../codecs/imagequant/imagequant';
|
||||
import wasmUrl from '../../../codecs/imagequant/imagequant.wasm';
|
||||
import { QuantizeOptions } from './processor-meta';
|
||||
import { initEmscriptenModule } from '../util';
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import mozjpeg_enc, { MozJPEGModule } from '../../codec-impl/mozjpeg_enc/mozjpeg_enc';
|
||||
// import wasmUrl from 'url:../../codecs/mozjpeg_enc/mozjpeg_enc.wasm';
|
||||
import wasmUrl from 'url:../../codec-impl/mozjpeg_enc/mozjpeg_enc.wasm';
|
||||
import mozjpeg_enc, { MozJPEGModule } from '../../../codecs/mozjpeg_enc/mozjpeg_enc';
|
||||
import wasmUrl from '../../../codecs/mozjpeg_enc/mozjpeg_enc.wasm';
|
||||
import { EncodeOptions } from './encoder-meta';
|
||||
import { initEmscriptenModule } from '../util';
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { bind } from '../../lib/initial-util';
|
||||
import { inputFieldChecked, inputFieldValueAsNumber, preventDefault } from '../../lib/util';
|
||||
import { EncodeOptions, MozJpegColorSpace } from './encoder-meta';
|
||||
import * as style from '../../components/Options/style.module.scss';
|
||||
import * as style from '../../components/Options/style.scss';
|
||||
import Checkbox from '../../components/checkbox';
|
||||
import Expander from '../../components/expander';
|
||||
import Select from '../../components/select';
|
||||
@@ -22,7 +23,8 @@ export default class MozJPEGEncoderOptions extends Component<Props, State> {
|
||||
showAdvanced: false,
|
||||
};
|
||||
|
||||
onChange = (event: Event) => {
|
||||
@bind
|
||||
onChange(event: Event) {
|
||||
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
|
||||
const { options } = this.props;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { optimise } from '../../codec-impl/oxipng/pkg';
|
||||
import { optimise } from '../../../codecs/oxipng/pkg';
|
||||
import { EncodeOptions } from './encoder-meta';
|
||||
|
||||
export async function compress(data: ArrayBuffer, options: EncodeOptions): Promise<ArrayBuffer> {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { bind } from '../../lib/initial-util';
|
||||
import { inputFieldValueAsNumber, preventDefault } from '../../lib/util';
|
||||
import { EncodeOptions } from './encoder-meta';
|
||||
import Range from '../../components/range';
|
||||
import * as style from '../../components/Options/style.module.scss';
|
||||
import * as style from '../../components/Options/style.scss';
|
||||
|
||||
type Props = {
|
||||
options: EncodeOptions;
|
||||
@@ -10,7 +11,8 @@ type Props = {
|
||||
};
|
||||
|
||||
export default class OxiPNGEncoderOptions extends Component<Props, {}> {
|
||||
onChange = (event: Event) => {
|
||||
@bind
|
||||
onChange(event: Event) {
|
||||
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
|
||||
|
||||
const options: EncodeOptions = {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { proxy } from 'comlink';
|
||||
import workerURL from 'bundle:./processor-worker/index.ts';
|
||||
import { QuantizeOptions } from './imagequant/processor-meta';
|
||||
import { canvasEncode, blobToArrayBuffer } from '../lib/util';
|
||||
import { EncodeOptions as MozJPEGEncoderOptions } from './mozjpeg/encoder-meta';
|
||||
@@ -17,25 +16,13 @@ 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;
|
||||
|
||||
/** How long the worker should be idle before terminating. */
|
||||
const workerTimeout = 10000;
|
||||
|
||||
/**
|
||||
* Decorator that manages the (re)starting of the worker and aborting existing jobs. Not all
|
||||
* processing jobs require a worker (e.g. the main thread canvas encodes), use the needsWorker
|
||||
* option to control this.
|
||||
*/
|
||||
function processingJob(options: ProcessingJobOptions = {}) {
|
||||
return (target: Processor, propertyKey: string, descriptor: PropertyDescriptor): void => {
|
||||
const processingFunc = descriptor.value;
|
||||
|
||||
descriptor.value = target.runProcessingJob.bind(target, options, processingFunc);
|
||||
};
|
||||
}
|
||||
|
||||
interface ProcessingJobOptions {
|
||||
needsWorker?: boolean;
|
||||
}
|
||||
@@ -54,10 +41,18 @@ export default class Processor {
|
||||
/** setTimeout ID for killing the worker when idle. */
|
||||
private _workerTimeoutId: number = 0;
|
||||
|
||||
/** @private */
|
||||
async _runProcessingJob(options: ProcessingJobOptions, job: Function) {
|
||||
/**
|
||||
* Decorator that manages the (re)starting of the worker and aborting existing jobs. Not all
|
||||
* processing jobs require a worker (e.g. the main thread canvas encodes), use the needsWorker
|
||||
* option to control this.
|
||||
*/
|
||||
private static _processingJob(options: ProcessingJobOptions = {}) {
|
||||
const { needsWorker = false } = options;
|
||||
|
||||
return (target: Processor, propertyKey: string, descriptor: PropertyDescriptor): void => {
|
||||
const processingFunc = descriptor.value;
|
||||
|
||||
descriptor.value = async function (this: Processor, ...args: any[]) {
|
||||
this._latestJobId += 1;
|
||||
const jobId = this._latestJobId;
|
||||
this.abortCurrent();
|
||||
@@ -69,13 +64,9 @@ export default class Processor {
|
||||
// @ts-ignore - Typescript doesn't know about the 2nd param to new Worker, and the
|
||||
// definition can't be overwritten.
|
||||
this._worker = new Worker(
|
||||
// './processor-worker',
|
||||
// new URL('./processor-worker/index.ts', import.meta.url),
|
||||
workerURL,
|
||||
'./processor-worker',
|
||||
{ name: 'processor-worker', type: 'module' },
|
||||
// { name: 'processor-worker', type: 'module' },
|
||||
) as Worker;
|
||||
console.log(this._worker);
|
||||
// Need to do some TypeScript trickery to make the type match.
|
||||
this._workerApi = proxy(this._worker) as any as ProcessorWorkerApi;
|
||||
}
|
||||
@@ -83,7 +74,7 @@ export default class Processor {
|
||||
this._busy = true;
|
||||
|
||||
const returnVal = Promise.race([
|
||||
job(),
|
||||
processingFunc.call(this, ...args),
|
||||
new Promise((_, reject) => { this._abortRejector = reject; }),
|
||||
]);
|
||||
|
||||
@@ -94,6 +85,8 @@ export default class Processor {
|
||||
if (jobId === this._latestJobId) this._jobCleanup();
|
||||
|
||||
return returnVal;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
private _jobCleanup(): void {
|
||||
@@ -102,7 +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(this.terminateWorker.bind(this), workerTimeout);
|
||||
this._workerTimeoutId = self.setTimeout(this.terminateWorker, workerTimeout);
|
||||
}
|
||||
|
||||
/** Abort the current job, if any */
|
||||
@@ -115,6 +108,7 @@ export default class Processor {
|
||||
this.terminateWorker();
|
||||
}
|
||||
|
||||
@bind
|
||||
terminateWorker() {
|
||||
if (!this._worker) return;
|
||||
this._worker.terminate();
|
||||
@@ -122,108 +116,93 @@ export default class Processor {
|
||||
}
|
||||
|
||||
// Off main thread jobs:
|
||||
@Processor._processingJob({ needsWorker: true })
|
||||
imageQuant(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
|
||||
return this._runProcessingJob({ needsWorker: true }, () => {
|
||||
return this._workerApi!.quantize(data, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Processor._processingJob({ needsWorker: true })
|
||||
rotate(
|
||||
data: ImageData, opts: import('./rotate/processor-meta').RotateOptions,
|
||||
): Promise<ImageData> {
|
||||
return this._runProcessingJob({ needsWorker: true }, () => {
|
||||
return this._workerApi!.rotate(data, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Processor._processingJob({ needsWorker: true })
|
||||
workerResize(
|
||||
data: ImageData, opts: import('./resize/processor-meta').WorkerResizeOptions,
|
||||
): Promise<ImageData> {
|
||||
return this._runProcessingJob({ needsWorker: true }, () => {
|
||||
return this._workerApi!.resize(data, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Processor._processingJob({ needsWorker: true })
|
||||
mozjpegEncode(
|
||||
data: ImageData, opts: MozJPEGEncoderOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
return this._runProcessingJob({ needsWorker: true }, () => {
|
||||
return this._workerApi!.mozjpegEncode(data, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Processor._processingJob({ needsWorker: true })
|
||||
async oxiPngEncode(
|
||||
data: ImageData, opts: OxiPNGEncoderOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
return this._runProcessingJob({ needsWorker: true }, async () => {
|
||||
// OxiPNG expects PNG input.
|
||||
const pngBlob = await canvasEncode(data, 'image/png');
|
||||
const pngBuffer = await blobToArrayBuffer(pngBlob);
|
||||
return this._workerApi!.oxiPngEncode(pngBuffer, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Processor._processingJob({ needsWorker: true })
|
||||
webpEncode(data: ImageData, opts: WebPEncoderOptions): Promise<ArrayBuffer> {
|
||||
return this._runProcessingJob({ needsWorker: true }, () => {
|
||||
return this._workerApi!.webpEncode(data, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Processor._processingJob({ needsWorker: true })
|
||||
async webpDecode(blob: Blob): Promise<ImageData> {
|
||||
return this._runProcessingJob({ needsWorker: true }, async () => {
|
||||
const data = await blobToArrayBuffer(blob);
|
||||
return this._workerApi!.webpDecode(data);
|
||||
});
|
||||
}
|
||||
|
||||
// Not-worker jobs:
|
||||
|
||||
@Processor._processingJob()
|
||||
browserBmpEncode(data: ImageData): Promise<Blob> {
|
||||
return this._runProcessingJob({}, () => {
|
||||
return browserBMP.encode(data);
|
||||
});
|
||||
}
|
||||
|
||||
@Processor._processingJob()
|
||||
browserPngEncode(data: ImageData): Promise<Blob> {
|
||||
return this._runProcessingJob({}, () => {
|
||||
return browserPNG.encode(data);
|
||||
});
|
||||
}
|
||||
|
||||
@Processor._processingJob()
|
||||
browserJpegEncode(data: ImageData, opts: BrowserJPEGOptions): Promise<Blob> {
|
||||
return this._runProcessingJob({}, () => {
|
||||
return browserJPEG.encode(data, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Processor._processingJob()
|
||||
browserWebpEncode(data: ImageData, opts: BrowserWebpEncodeOptions): Promise<Blob> {
|
||||
return this._runProcessingJob({}, () => {
|
||||
return browserWebP.encode(data, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@Processor._processingJob()
|
||||
browserGifEncode(data: ImageData): Promise<Blob> {
|
||||
return this._runProcessingJob({}, () => {
|
||||
return browserGIF.encode(data);
|
||||
});
|
||||
}
|
||||
|
||||
@Processor._processingJob()
|
||||
browserTiffEncode(data: ImageData): Promise<Blob> {
|
||||
return this._runProcessingJob({}, () => {
|
||||
return browserTIFF.encode(data);
|
||||
});
|
||||
}
|
||||
|
||||
@Processor._processingJob()
|
||||
browserJp2Encode(data: ImageData): Promise<Blob> {
|
||||
return this._runProcessingJob({}, () => {
|
||||
return browserJP2.encode(data);
|
||||
});
|
||||
}
|
||||
|
||||
@Processor._processingJob()
|
||||
browserPdfEncode(data: ImageData): Promise<Blob> {
|
||||
return this._runProcessingJob({}, () => {
|
||||
return browserPDF.encode(data);
|
||||
});
|
||||
}
|
||||
|
||||
// Synchronous jobs
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { h, Component } from 'preact';
|
||||
import linkState from 'linkstate';
|
||||
import { linkRef } from '../../lib/initial-util';
|
||||
import { bind, linkRef } from '../../lib/initial-util';
|
||||
import {
|
||||
inputFieldValueAsNumber, inputFieldValue, preventDefault, inputFieldChecked,
|
||||
} from '../../lib/util';
|
||||
import { ResizeOptions, isWorkerOptions } from './processor-meta';
|
||||
import * as style from '../../components/Options/style.module.scss';
|
||||
import * as style from '../../components/Options/style.scss';
|
||||
import Checkbox from '../../components/checkbox';
|
||||
import Expander from '../../components/expander';
|
||||
import Select from '../../components/select';
|
||||
@@ -58,7 +58,8 @@ export default class ResizerOptions extends Component<Props, State> {
|
||||
this.props.onChange(newOptions);
|
||||
}
|
||||
|
||||
private onChange = () => {
|
||||
@bind
|
||||
private onChange() {
|
||||
this.reportOptions();
|
||||
}
|
||||
|
||||
@@ -82,7 +83,8 @@ export default class ResizerOptions extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
private onWidthInput = () => {
|
||||
@bind
|
||||
private onWidthInput() {
|
||||
if (this.state.maintainAspect) {
|
||||
const width = inputFieldValueAsNumber(this.form!.width);
|
||||
this.form!.height.value = Math.round(width / this.getAspect());
|
||||
@@ -91,7 +93,8 @@ export default class ResizerOptions extends Component<Props, State> {
|
||||
this.reportOptions();
|
||||
}
|
||||
|
||||
private onHeightInput = () => {
|
||||
@bind
|
||||
private onHeightInput() {
|
||||
if (this.state.maintainAspect) {
|
||||
const height = inputFieldValueAsNumber(this.form!.height);
|
||||
this.form!.width.value = Math.round(height * this.getAspect());
|
||||
@@ -120,7 +123,8 @@ export default class ResizerOptions extends Component<Props, State> {
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
private onPresetChange = (event: Event) => {
|
||||
@bind
|
||||
private onPresetChange(event: Event) {
|
||||
const select = event.target as HTMLSelectElement;
|
||||
if (select.value === 'custom') return;
|
||||
const multiplier = Number(select.value);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import wasmUrl from 'url:../../codec-impl/rotate/rotate.wasm';
|
||||
import wasmUrl from '../../../codecs/rotate/rotate.wasm';
|
||||
import { RotateOptions, RotateModuleInstance } from './processor-meta';
|
||||
|
||||
// We are loading a 500B module here. Loading the code to feature-detect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import webp_dec, { WebPModule } from '../../codec-impl/webp/dec/webp_dec';
|
||||
import wasmUrl from 'url:../../codec-impl/webp/dec/webp_dec.wasm';
|
||||
import webp_dec, { WebPModule } from '../../../codecs/webp/dec/webp_dec';
|
||||
import wasmUrl from '../../../codecs/webp/dec/webp_dec.wasm';
|
||||
import { initEmscriptenModule } from '../util';
|
||||
|
||||
let emscriptenModule: Promise<WebPModule>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import webp_enc, { WebPModule } from '../../codec-impl/webp/enc/webp_enc';
|
||||
import wasmUrl from 'url:../../codec-impl/webp/enc/webp_enc.wasm';
|
||||
import webp_enc, { WebPModule } from '../../../codecs/webp/enc/webp_enc';
|
||||
import wasmUrl from '../../../codecs/webp/enc/webp_enc.wasm';
|
||||
import { EncodeOptions } from './encoder-meta';
|
||||
import { initEmscriptenModule } from '../util';
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { bind } from '../../lib/initial-util';
|
||||
import { inputFieldCheckedAsNumber, inputFieldValueAsNumber, preventDefault } from '../../lib/util';
|
||||
import { EncodeOptions, WebPImageHint } from './encoder-meta';
|
||||
import * as style from '../../components/Options/style.module.scss';
|
||||
import * as style from '../../components/Options/style.scss';
|
||||
import Checkbox from '../../components/checkbox';
|
||||
import Expander from '../../components/expander';
|
||||
import Select from '../../components/select';
|
||||
@@ -40,7 +41,8 @@ export default class WebPEncoderOptions extends Component<Props, State> {
|
||||
showAdvanced: false,
|
||||
};
|
||||
|
||||
onChange = (event: Event) => {
|
||||
@bind
|
||||
onChange(event: Event) {
|
||||
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
|
||||
const lossless = inputFieldCheckedAsNumber(form.lossless);
|
||||
const { options } = this.props;
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { linkRef, Fileish } from '../../lib/initial-util';
|
||||
import * as style from './style.module.scss';
|
||||
import { bind, linkRef, Fileish } from '../../lib/initial-util';
|
||||
import * as style from './style.scss';
|
||||
import { FileDropEvent } from 'file-drop-element';
|
||||
import 'file-drop-element';
|
||||
import SnackBarElement, { SnackOptions } from '../../lib/SnackBar/index';
|
||||
import '../../lib/SnackBar/index';
|
||||
import Intro from '../intro/index';
|
||||
import '../custom-els/LoadingSpinner/index';
|
||||
import SnackBarElement, { SnackOptions } from '../../lib/SnackBar';
|
||||
import '../../lib/SnackBar';
|
||||
import Intro from '../intro';
|
||||
import '../custom-els/LoadingSpinner';
|
||||
|
||||
const ROUTE_EDITOR = '/editor';
|
||||
|
||||
const compressPromise = import(
|
||||
/* webpackChunkName: "main-app" */
|
||||
'../compress');
|
||||
|
||||
const swBridgePromise = import(
|
||||
/* webpackChunkName: "sw-bridge" */
|
||||
'../../lib/sw-bridge');
|
||||
|
||||
function back() {
|
||||
@@ -43,17 +45,10 @@ export default class App extends Component<Props, State> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onFileDrop = this.onFileDrop.bind(this);
|
||||
this.onIntroPickFile = this.onIntroPickFile.bind(this);
|
||||
this.showSnack = this.showSnack.bind(this);
|
||||
this.onPopState = this.onPopState.bind(this);
|
||||
this.openEditor = this.openEditor.bind(this);
|
||||
|
||||
compressPromise.then((module) => {
|
||||
this.setState({ Compress: module.default });
|
||||
}).catch((e) => {
|
||||
}).catch(() => {
|
||||
this.showSnack('Failed to load app');
|
||||
throw e;
|
||||
});
|
||||
|
||||
swBridgePromise.then(async ({ offliner, getSharedImage }) => {
|
||||
@@ -67,8 +62,7 @@ export default class App extends Component<Props, State> {
|
||||
});
|
||||
|
||||
// In development, persist application state across hot reloads:
|
||||
// if (process.env.NODE_ENV === 'development') {
|
||||
if (module.hot) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
this.setState(window.STATE);
|
||||
const oldCDU = this.componentDidUpdate;
|
||||
this.componentDidUpdate = (props, state, prev) => {
|
||||
@@ -88,6 +82,7 @@ export default class App extends Component<Props, State> {
|
||||
window.addEventListener('popstate', this.onPopState);
|
||||
}
|
||||
|
||||
@bind
|
||||
private onFileDrop({ files }: FileDropEvent) {
|
||||
if (!files || files.length === 0) return;
|
||||
const file = files[0];
|
||||
@@ -95,20 +90,24 @@ export default class App extends Component<Props, State> {
|
||||
this.setState({ file });
|
||||
}
|
||||
|
||||
@bind
|
||||
private onIntroPickFile(file: File | Fileish) {
|
||||
this.openEditor();
|
||||
this.setState({ file });
|
||||
}
|
||||
|
||||
@bind
|
||||
private showSnack(message: string, options: SnackOptions = {}): Promise<string> {
|
||||
if (!this.snackbar) throw Error('Snackbar missing');
|
||||
return this.snackbar.showSnackbar(message, options);
|
||||
}
|
||||
|
||||
@bind
|
||||
private onPopState() {
|
||||
this.setState({ isEditorOpen: location.pathname === ROUTE_EDITOR });
|
||||
}
|
||||
|
||||
@bind
|
||||
private openEditor() {
|
||||
if (this.state.isEditorOpen) return;
|
||||
// Change path, but preserve query string.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import * as style from './style.module.scss';
|
||||
import * as style from './style.scss';
|
||||
import { bind } from '../../lib/initial-util';
|
||||
import { cleanSet, cleanMerge } from '../../lib/clean-modify';
|
||||
import OxiPNGEncoderOptions from '../../codecs/oxipng/options';
|
||||
import MozJpegEncoderOptions from '../../codecs/mozjpeg/options';
|
||||
@@ -81,7 +82,8 @@ export default class Options extends Component<Props, State> {
|
||||
encodersSupported.then(encoderSupportMap => this.setState({ encoderSupportMap }));
|
||||
}
|
||||
|
||||
private onEncoderTypeChange = (event: Event) => {
|
||||
@bind
|
||||
private onEncoderTypeChange(event: Event) {
|
||||
const el = event.currentTarget as HTMLSelectElement;
|
||||
|
||||
// The select element only has values matching encoder types,
|
||||
@@ -90,7 +92,8 @@ export default class Options extends Component<Props, State> {
|
||||
this.props.onEncoderTypeChange(type);
|
||||
}
|
||||
|
||||
private onPreprocessorEnabledChange = (event: Event) => {
|
||||
@bind
|
||||
private onPreprocessorEnabledChange(event: Event) {
|
||||
const el = event.currentTarget as HTMLInputElement;
|
||||
const preprocessor = el.name.split('.')[0] as keyof PreprocessorState;
|
||||
|
||||
@@ -99,13 +102,15 @@ export default class Options extends Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
private onQuantizerOptionsChange = (opts: QuantizeOptions) => {
|
||||
@bind
|
||||
private onQuantizerOptionsChange(opts: QuantizeOptions) {
|
||||
this.props.onPreprocessorOptionsChange(
|
||||
cleanMerge(this.props.preprocessorState, 'quantizer', opts),
|
||||
);
|
||||
}
|
||||
|
||||
private onResizeOptionsChange = (opts: ResizeOptions) => {
|
||||
@bind
|
||||
private onResizeOptionsChange(opts: ResizeOptions) {
|
||||
this.props.onPreprocessorOptionsChange(
|
||||
cleanMerge(this.props.preprocessorState, 'resize', opts),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import PointerTracker, { Pointer } from 'pointer-tracker';
|
||||
import * as styles from './styles.module.css';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const legacyClipCompatAttr = 'legacy-clip-compat';
|
||||
const orientationAttr = 'orientation';
|
||||
|
||||
@@ -2,8 +2,8 @@ import { h, Component } from 'preact';
|
||||
import PinchZoom, { ScaleToOpts } from './custom-els/PinchZoom';
|
||||
import './custom-els/PinchZoom';
|
||||
import './custom-els/TwoUp';
|
||||
import * as style from './style.module.scss';
|
||||
import { linkRef } from '../../lib/initial-util';
|
||||
import * as style from './style.scss';
|
||||
import { bind, linkRef } from '../../lib/initial-util';
|
||||
import { shallowEqual, drawDataToCanvas } from '../../lib/util';
|
||||
import {
|
||||
ToggleBackgroundIcon,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
ToggleBackgroundActiveIcon,
|
||||
RotateIcon,
|
||||
} from '../../lib/icons';
|
||||
import { twoUpHandle } from './custom-els/TwoUp/styles.module.css';
|
||||
import { twoUpHandle } from './custom-els/TwoUp/styles.css';
|
||||
import { InputProcessorState } from '../../codecs/input-processors';
|
||||
import { cleanSet } from '../../lib/clean-modify';
|
||||
import { SourceImage } from '../compress';
|
||||
@@ -135,25 +135,29 @@ export default class Output extends Component<Props, State> {
|
||||
return props.rightCompressed || (props.source && props.source.processed);
|
||||
}
|
||||
|
||||
private toggleBackground = () => {
|
||||
@bind
|
||||
private toggleBackground() {
|
||||
this.setState({
|
||||
altBackground: !this.state.altBackground,
|
||||
});
|
||||
}
|
||||
|
||||
private zoomIn = () => {
|
||||
@bind
|
||||
private zoomIn() {
|
||||
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
|
||||
|
||||
this.pinchZoomLeft.scaleTo(this.state.scale * 1.25, scaleToOpts);
|
||||
}
|
||||
|
||||
private zoomOut = () => {
|
||||
@bind
|
||||
private zoomOut() {
|
||||
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
|
||||
|
||||
this.pinchZoomLeft.scaleTo(this.state.scale / 1.25, scaleToOpts);
|
||||
}
|
||||
|
||||
private onRotateClick = () => {
|
||||
@bind
|
||||
private onRotateClick() {
|
||||
const { inputProcessorState } = this.props;
|
||||
if (!inputProcessorState) return;
|
||||
|
||||
@@ -166,7 +170,8 @@ export default class Output extends Component<Props, State> {
|
||||
this.props.onInputProcessorChange(newState);
|
||||
}
|
||||
|
||||
private onScaleValueFocus = () => {
|
||||
@bind
|
||||
private onScaleValueFocus() {
|
||||
this.setState({ editingScale: true }, () => {
|
||||
if (this.scaleInput) {
|
||||
// Firefox unfocuses the input straight away unless I force a style calculation here. I have
|
||||
@@ -177,11 +182,13 @@ export default class Output extends Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
private onScaleInputBlur = () => {
|
||||
@bind
|
||||
private onScaleInputBlur() {
|
||||
this.setState({ editingScale: false });
|
||||
}
|
||||
|
||||
private onScaleInputChanged = (event: Event) => {
|
||||
@bind
|
||||
private onScaleInputChanged(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const percent = parseFloat(target.value);
|
||||
if (isNaN(percent)) return;
|
||||
@@ -190,7 +197,8 @@ export default class Output extends Component<Props, State> {
|
||||
this.pinchZoomLeft.scaleTo(percent / 100, scaleToOpts);
|
||||
}
|
||||
|
||||
private onPinchZoomLeftChange = (event: Event) => {
|
||||
@bind
|
||||
private onPinchZoomLeftChange(event: Event) {
|
||||
if (!this.pinchZoomRight || !this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
|
||||
this.setState({
|
||||
scale: this.pinchZoomLeft.scale,
|
||||
@@ -210,7 +218,8 @@ export default class Output extends Component<Props, State> {
|
||||
*
|
||||
* @param event Event to redirect
|
||||
*/
|
||||
private onRetargetableEvent = (event: Event) => {
|
||||
@bind
|
||||
private onRetargetableEvent(event: Event) {
|
||||
const targetEl = event.target as HTMLElement;
|
||||
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
|
||||
// If the event is on the handle of the two-up, let it through,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { h, Component } from 'preact';
|
||||
import * as style from './style.module.scss';
|
||||
import * as style from './style.scss';
|
||||
import { UncheckedIcon, CheckedIcon } from '../../lib/icons';
|
||||
|
||||
interface Props extends JSX.HTMLAttributes {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as style from './styles.module.css';
|
||||
import * as style from './styles.css';
|
||||
import { transitionHeight } from '../../../../lib/util';
|
||||
|
||||
interface CloseAllOptions {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { Fileish } from '../../lib/initial-util';
|
||||
import { bind, Fileish } from '../../lib/initial-util';
|
||||
import { blobToImg, drawableToImageData, blobToText } from '../../lib/util';
|
||||
import * as style from './style.module.scss';
|
||||
import * as style from './style.scss';
|
||||
import Output from '../Output';
|
||||
import Options from '../Options';
|
||||
import ResultCache from './result-cache';
|
||||
@@ -254,7 +254,8 @@ export default class Compress extends Component<Props, State> {
|
||||
import('../../lib/sw-bridge').then(({ mainAppLoaded }) => mainAppLoaded());
|
||||
}
|
||||
|
||||
private onMobileWidthChange = () => {
|
||||
@bind
|
||||
private onMobileWidthChange() {
|
||||
this.setState({ mobileView: this.widthQuery.matches });
|
||||
}
|
||||
|
||||
@@ -343,7 +344,8 @@ export default class Compress extends Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
private onInputProcessorChange = async (options: InputProcessorState): Promise<void> => {
|
||||
@bind
|
||||
private async onInputProcessorChange(options: InputProcessorState): Promise<void> {
|
||||
const source = this.state.source;
|
||||
if (!source) return;
|
||||
|
||||
@@ -394,7 +396,8 @@ export default class Compress extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
private updateFile = async (file: File | Fileish) => {
|
||||
@bind
|
||||
private async updateFile(file: File | Fileish) {
|
||||
const loadingCounter = this.state.loadingCounter + 1;
|
||||
// Either processor is good enough here.
|
||||
const processor = this.leftProcessor;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as styles from './styles.module.css';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
/**
|
||||
* A simple spinner. This custom element has no JS API. Just put it in the document, and it'll
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { h, Component, ComponentChild, ComponentChildren } from 'preact';
|
||||
import * as style from './style.module.scss';
|
||||
import * as style from './style.scss';
|
||||
import { transitionHeight } from '../../lib/util';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { linkRef, Fileish } from '../../lib/initial-util';
|
||||
import { bind, linkRef, Fileish } from '../../lib/initial-util';
|
||||
import '../custom-els/LoadingSpinner';
|
||||
|
||||
import logo from 'url:./imgs/logo.svg';
|
||||
import largePhoto from 'url:./imgs/demos/demo-large-photo.jpg';
|
||||
import artwork from 'url:./imgs/demos/demo-artwork.jpg';
|
||||
import deviceScreen from 'url:./imgs/demos/demo-device-screen.png';
|
||||
import largePhotoIcon from 'url:./imgs/demos/icon-demo-large-photo.jpg';
|
||||
import artworkIcon from 'url:./imgs/demos/icon-demo-artwork.jpg';
|
||||
import deviceScreenIcon from 'url:./imgs/demos/icon-demo-device-screen.jpg';
|
||||
import logoIcon from 'url:./imgs/demos/icon-demo-logo.png';
|
||||
import * as style from './style.module.scss';
|
||||
import logo from './imgs/logo.svg';
|
||||
import largePhoto from './imgs/demos/demo-large-photo.jpg';
|
||||
import artwork from './imgs/demos/demo-artwork.jpg';
|
||||
import deviceScreen from './imgs/demos/demo-device-screen.png';
|
||||
import largePhotoIcon from './imgs/demos/icon-demo-large-photo.jpg';
|
||||
import artworkIcon from './imgs/demos/icon-demo-artwork.jpg';
|
||||
import deviceScreenIcon from './imgs/demos/icon-demo-device-screen.jpg';
|
||||
import logoIcon from './imgs/demos/icon-demo-logo.png';
|
||||
import * as style from './style.scss';
|
||||
import SnackBarElement from '../../lib/SnackBar';
|
||||
|
||||
const demos = [
|
||||
@@ -67,11 +67,13 @@ export default class Intro extends Component<Props, State> {
|
||||
window.addEventListener('appinstalled', this.onAppInstalled);
|
||||
}
|
||||
|
||||
private resetFileInput = () => {
|
||||
@bind
|
||||
private resetFileInput() {
|
||||
this.fileInput!.value = '';
|
||||
}
|
||||
|
||||
private onFileChange = (event: Event) => {
|
||||
@bind
|
||||
private onFileChange(event: Event): void {
|
||||
const fileInput = event.target as HTMLInputElement;
|
||||
const file = fileInput.files && fileInput.files[0];
|
||||
if (!file) return;
|
||||
@@ -79,11 +81,13 @@ export default class Intro extends Component<Props, State> {
|
||||
this.props.onFile(file);
|
||||
}
|
||||
|
||||
private onButtonClick = () => {
|
||||
@bind
|
||||
private onButtonClick() {
|
||||
this.fileInput!.click();
|
||||
}
|
||||
|
||||
private onDemoClick = async (index: number, event: Event) => {
|
||||
@bind
|
||||
private async onDemoClick(index: number, event: Event) {
|
||||
try {
|
||||
this.setState({ fetchingDemoIndex: index });
|
||||
const demo = demos[index];
|
||||
@@ -100,7 +104,8 @@ export default class Intro extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
private onBeforeInstallPromptEvent = (event: BeforeInstallPromptEvent) => {
|
||||
@bind
|
||||
private onBeforeInstallPromptEvent(event: BeforeInstallPromptEvent) {
|
||||
// Don't show the mini-infobar on mobile
|
||||
event.preventDefault();
|
||||
|
||||
@@ -116,7 +121,8 @@ export default class Intro extends Component<Props, State> {
|
||||
ga('send', 'event', gaEventInfo);
|
||||
}
|
||||
|
||||
private onInstallClick = async (event: Event) => {
|
||||
@bind
|
||||
private async onInstallClick(event: Event) {
|
||||
// Get the deferred beforeinstallprompt event
|
||||
const beforeInstallEvent = this.state.beforeInstallEvent;
|
||||
// If there's no deferred prompt, bail.
|
||||
@@ -144,7 +150,8 @@ export default class Intro extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
private onAppInstalled = () => {
|
||||
@bind
|
||||
private onAppInstalled() {
|
||||
// We don't need the install button, if it's shown
|
||||
this.setState({ beforeInstallEvent: undefined });
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { h, Component } from 'preact';
|
||||
import * as style from './style.module.scss';
|
||||
import * as style from './style.scss';
|
||||
import RangeInputElement from '../../custom-els/RangeInput';
|
||||
import '../../custom-els/RangeInput';
|
||||
import { linkRef } from '../../lib/initial-util';
|
||||
import { linkRef, bind } from '../../lib/initial-util';
|
||||
|
||||
interface Props extends JSX.HTMLAttributes {}
|
||||
interface State {}
|
||||
@@ -10,7 +10,8 @@ interface State {}
|
||||
export default class Range extends Component<Props, State> {
|
||||
rangeWc?: RangeInputElement;
|
||||
|
||||
private onTextInput = (event: Event) => {
|
||||
@bind
|
||||
private onTextInput(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const value = input.value.trim();
|
||||
if (!value) return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { h, Component } from 'preact';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import * as style from './style.module.scss';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import * as style from './style.scss';
|
||||
|
||||
interface Props {
|
||||
blob: Blob;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { h, Component, ComponentChildren, ComponentChild } from 'preact';
|
||||
|
||||
import * as style from './style.module.scss';
|
||||
import * as style from './style.scss';
|
||||
import FileSize from './FileSize';
|
||||
import { DownloadIcon, CopyAcrossIcon, CopyAcrossIconProps } from '../../lib/icons';
|
||||
import '../custom-els/LoadingSpinner';
|
||||
import { SourceImage } from '../compress';
|
||||
import { Fileish } from '../../lib/initial-util';
|
||||
import { Fileish, bind } from '../../lib/initial-util';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
@@ -52,12 +52,14 @@ export default class Results extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
private onCopyToOtherClick = (event: Event) => {
|
||||
@bind
|
||||
private onCopyToOtherClick(event: Event) {
|
||||
event.preventDefault();
|
||||
this.props.onCopyToOtherClick();
|
||||
}
|
||||
|
||||
onDownload = () => {
|
||||
@bind
|
||||
onDownload() {
|
||||
// GA can’t do floats. So we round to ints. We're deliberately rounding to nearest kilobyte to
|
||||
// avoid cases where exact image sizes leak something interesting about the user.
|
||||
const before = Math.round(this.props.source!.file.size / 1024);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { h, Component } from 'preact';
|
||||
import * as style from './style.module.scss';
|
||||
import * as style from './style.scss';
|
||||
|
||||
interface Props extends JSX.HTMLAttributes {
|
||||
large?: boolean;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PointerTracker from 'pointer-tracker';
|
||||
import * as style from './styles.module.css';
|
||||
import { bind } from '../../lib/initial-util';
|
||||
import * as style from './styles.css';
|
||||
|
||||
const RETARGETED_EVENTS = ['focus', 'blur'];
|
||||
const UPDATE_EVENTS = ['input', 'change'];
|
||||
@@ -22,7 +23,6 @@ class RangeInputElement extends HTMLElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._input = document.createElement('input');
|
||||
this._input.type = 'range';
|
||||
this._input.className = style.input;
|
||||
@@ -38,12 +38,10 @@ class RangeInputElement extends HTMLElement {
|
||||
},
|
||||
});
|
||||
|
||||
this._retargetEvent = this._retargetEvent.bind(this);
|
||||
for (const event of RETARGETED_EVENTS) {
|
||||
this._input.addEventListener(event, this._retargetEvent, true);
|
||||
}
|
||||
|
||||
this._update = this._update.bind(this);
|
||||
for (const event of UPDATE_EVENTS) {
|
||||
this._input.addEventListener(event, this._update, true);
|
||||
}
|
||||
@@ -82,12 +80,14 @@ class RangeInputElement extends HTMLElement {
|
||||
this._update();
|
||||
}
|
||||
|
||||
@bind
|
||||
private _retargetEvent(event: Event) {
|
||||
event.stopImmediatePropagation();
|
||||
const retargetted = new Event(event.type, event);
|
||||
this.dispatchEvent(retargetted);
|
||||
}
|
||||
|
||||
@bind
|
||||
private _update() {
|
||||
const value = Number(this.value) || 0;
|
||||
const min = Number(this.min) || 0;
|
||||
|
||||
@@ -12,6 +12,5 @@
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="./index.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
declare module '@webcomponents/custom-elements';
|
||||
|
||||
function init() {
|
||||
import('./init-app.tsx');
|
||||
require('./init-app.tsx');
|
||||
}
|
||||
|
||||
if (!('customElements' in self)) {
|
||||
import('@webcomponents/custom-elements').then(init);
|
||||
import(
|
||||
/* webpackChunkName: "wc-polyfill" */
|
||||
'@webcomponents/custom-elements').then(init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof PRERENDER === 'undefined') {
|
||||
// Determine the current display mode.
|
||||
let displayMode = 'browser';
|
||||
const mqStandAlone = '(display-mode: standalone)';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { h, render } from 'preact';
|
||||
import './lib/fix-pmc.mjs';
|
||||
import './style/index.scss';
|
||||
import App from './components/App/index.tsx';
|
||||
import './lib/fix-pmc';
|
||||
import './style';
|
||||
import App from './components/App';
|
||||
|
||||
// Find the outermost Element in our server-rendered HTML structure.
|
||||
let root = document.getElementById('app_root') as Element;
|
||||
@@ -10,7 +10,16 @@ let root = document.getElementById('app_root') as Element;
|
||||
root = render(<App />, document.body, root);
|
||||
root.setAttribute('id', 'app_root');
|
||||
|
||||
if (module.hot) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Enable support for React DevTools and some helpful console warnings:
|
||||
import('preact/debug');
|
||||
require('preact/debug');
|
||||
|
||||
// When an update to any module is received, re-import the app and trigger a full re-render:
|
||||
module.hot.accept('./components/App', () => {
|
||||
// The linter doesn't like the capital A in App. It is wrong.
|
||||
// tslint:disable-next-line variable-name
|
||||
import('./components/App').then(({ default: App }) => {
|
||||
root = render(<App />, document.body, root);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as style from './styles.module.css';
|
||||
import * as style from './styles.css';
|
||||
|
||||
export interface SnackOptions {
|
||||
timeout?: number;
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
// This file contains the utils that are needed for the very first rendering of the page. They're
|
||||
// here because WebPack isn't quite smart enough to split things in the same file.
|
||||
|
||||
/**
|
||||
* A decorator that binds values to their class instance.
|
||||
* @example
|
||||
* class C {
|
||||
* @bind
|
||||
* foo () {
|
||||
* return this;
|
||||
* }
|
||||
* }
|
||||
* let f = new C().foo;
|
||||
* f() instanceof C; // true
|
||||
*/
|
||||
export function bind(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
return {
|
||||
// the first time the prototype property is accessed for an instance,
|
||||
// define an instance property pointing to the bound function.
|
||||
// This effectively "caches" the bound prototype method as an instance property.
|
||||
get() {
|
||||
const bound = descriptor.value.bind(this);
|
||||
Object.defineProperty(this, propertyKey, {
|
||||
value: bound,
|
||||
});
|
||||
return bound;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Creates a function ref that assigns its value to a given property of an object.
|
||||
* @example
|
||||
* // element is stored as `this.foo` when rendered.
|
||||
|
||||
@@ -60,11 +60,9 @@ export function getSharedImage(): Promise<File> {
|
||||
/** Set up the service worker and monitor changes */
|
||||
export async function offliner(showSnack: SnackBarElement['showSnackbar']) {
|
||||
// This needs to be a typeof because Webpack.
|
||||
if (typeof window === 'undefined') return;
|
||||
if (typeof PRERENDER === 'boolean') return;
|
||||
|
||||
if (!navigator.serviceWorker) return;
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && !module.hot) {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
navigator.serviceWorker.register('../sw');
|
||||
}
|
||||
|
||||
|
||||
19
src/missing-types.d.ts
vendored
19
src/missing-types.d.ts
vendored
@@ -2,13 +2,6 @@ interface CanvasRenderingContext2D {
|
||||
filter: string;
|
||||
}
|
||||
|
||||
declare module '*.module.scss' {
|
||||
const classNameMapping: Record<string, string> & {
|
||||
default: Record<string, string>
|
||||
};
|
||||
export = classNameMapping;
|
||||
}
|
||||
|
||||
// Handling file-loader imports:
|
||||
declare module '*.png' {
|
||||
const content: string;
|
||||
@@ -35,17 +28,7 @@ declare module '*.wasm' {
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module 'url:*' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module 'data-url:*' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module 'bundle:*' {
|
||||
declare module 'url-loader!*' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import webpDataUrl from 'url:../codecs/tiny.webp';
|
||||
import webpDataUrl from 'url-loader!../codecs/tiny.webp';
|
||||
|
||||
// Give TypeScript the correct global.
|
||||
declare var self: ServiceWorkerGlobalScope;
|
||||
|
||||
@@ -6,24 +6,15 @@
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noUnusedLocals": true,
|
||||
"sourceMap": true,
|
||||
"jsx": "react",
|
||||
"jsxFactory": "h",
|
||||
"allowJs": false,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"codecs/*": [
|
||||
"codecs/*"
|
||||
]
|
||||
}
|
||||
"baseUrl": "."
|
||||
},
|
||||
"exclude": [
|
||||
"src/sw/**/*",
|
||||
"src/codecs/processor-worker/**/*",
|
||||
".cache",
|
||||
"public",
|
||||
"node_modules"
|
||||
"src/codecs/processor-worker/**/*"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user