diff --git a/codecs/mozjpeg_enc/mozjpeg_enc.d.ts b/codecs/mozjpeg_enc/mozjpeg_enc.d.ts index a4d064df..0c3691f4 100644 --- a/codecs/mozjpeg_enc/mozjpeg_enc.d.ts +++ b/codecs/mozjpeg_enc/mozjpeg_enc.d.ts @@ -1,4 +1,4 @@ -import { EncodeOptions } from '../../src/codecs/mozjpeg/encoder'; +import { EncodeOptions } from '../../src/codecs/mozjpeg/encoder-meta'; interface MozJPEGModule extends EmscriptenWasm.Module { encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array; diff --git a/codecs/webp_enc/webp_enc.d.ts b/codecs/webp_enc/webp_enc.d.ts index 259b2b9e..02e450a9 100644 --- a/codecs/webp_enc/webp_enc.d.ts +++ b/codecs/webp_enc/webp_enc.d.ts @@ -1,4 +1,4 @@ -import { EncodeOptions } from '../../src/codecs/webp/encoder'; +import { EncodeOptions } from '../../src/codecs/webp/encoder-meta'; interface WebPModule extends EmscriptenWasm.Module { encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array; diff --git a/package-lock.json b/package-lock.json index 674f8f63..b369cb84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -306,6 +306,7 @@ "version": "6.5.3", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.3.tgz", "integrity": "sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg==", + "dev": true, "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -322,7 +323,8 @@ "ajv-keywords": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", - "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=" + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "dev": true }, "alphanum-sort": { "version": "1.0.2", @@ -1656,7 +1658,8 @@ "big.js": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==" + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true }, "binary-extensions": { "version": "1.12.0", @@ -2224,7 +2227,8 @@ "classnames": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", + "dev": true }, "clean-css": { "version": "4.2.1", @@ -2456,22 +2460,8 @@ "comlink": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/comlink/-/comlink-3.0.3.tgz", - "integrity": "sha512-toiZad0dmZIfqkSh4XyD40mRg6/X+8yNvtWCq+f79aIKsJGTf3hY8Ikr4wGx4494h1q9oNHznWMLdorNWsr6dQ==" - }, - "comlink-loader": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/comlink-loader/-/comlink-loader-1.1.0.tgz", - "integrity": "sha512-XrHEKyj+BcLdhAu+e6OLUrsOvNgCmmSUQWlx2hKaJzV3dbkSa/KdlNNnMzYX4WTuBQEqggUgYteugHrn+/lC6g==", - "requires": { - "comlinkjs": "^2.4.1", - "loader-utils": "^1.1.0", - "worker-loader": "^2.0.0" - } - }, - "comlinkjs": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/comlinkjs/-/comlinkjs-2.4.1.tgz", - "integrity": "sha512-nifSjuwsqqNg2vq1vcFuKhqclFUA1R0Fal0vjE/TDXqOlaG4h9XRe2MRe2Wy+5aHVzqK/pI+03U327j2hFn1Zg==" + "integrity": "sha512-toiZad0dmZIfqkSh4XyD40mRg6/X+8yNvtWCq+f79aIKsJGTf3hY8Ikr4wGx4494h1q9oNHznWMLdorNWsr6dQ==", + "dev": true }, "commander": { "version": "2.17.1", @@ -3366,7 +3356,8 @@ "emojis-list": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true }, "encodeurl": { "version": "1.0.2", @@ -3858,7 +3849,8 @@ "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true }, "fast-glob": { "version": "2.2.2", @@ -3877,7 +3869,8 @@ "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true }, "fastparse": { "version": "1.1.1", @@ -6377,7 +6370,8 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "json-stringify-safe": { "version": "5.0.1", @@ -6394,7 +6388,8 @@ "json5": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true }, "jsonify": { "version": "0.0.0", @@ -6457,7 +6452,8 @@ "linkstate": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/linkstate/-/linkstate-1.1.1.tgz", - "integrity": "sha512-5SICdxQG9FpWk44wSEoM2WOCUNuYfClp10t51XAIV5E7vUILF/dTYxf0vJw6bW2dUd2wcQon+LkNtRijpNLrig==" + "integrity": "sha512-5SICdxQG9FpWk44wSEoM2WOCUNuYfClp10t51XAIV5E7vUILF/dTYxf0vJw6bW2dUd2wcQon+LkNtRijpNLrig==", + "dev": true }, "listr": { "version": "0.14.2", @@ -6625,6 +6621,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, "requires": { "big.js": "^3.1.3", "emojis-list": "^2.0.0", @@ -8697,7 +8694,8 @@ "preact": { "version": "8.3.1", "resolved": "https://registry.npmjs.org/preact/-/preact-8.3.1.tgz", - "integrity": "sha512-s8H1Y8O9e+mOBo3UP1jvWqArPmjCba2lrrGLlq/0kN1XuIINUbYtf97iiXKxCuG3eYwmppPKnyW2DBrNj/TuTg==" + "integrity": "sha512-s8H1Y8O9e+mOBo3UP1jvWqArPmjCba2lrrGLlq/0kN1XuIINUbYtf97iiXKxCuG3eYwmppPKnyW2DBrNj/TuTg==", + "dev": true }, "prepend-http": { "version": "1.0.4", @@ -8720,7 +8718,8 @@ "pretty-bytes": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.1.0.tgz", - "integrity": "sha512-wa5+qGVg9Yt7PB6rYm3kXlKzgzgivYTLRandezh43jjRqgyDyP+9YxfJpJiLs9yKD1WeU8/OvtToWpW7255FtA==" + "integrity": "sha512-wa5+qGVg9Yt7PB6rYm3kXlKzgzgivYTLRandezh43jjRqgyDyP+9YxfJpJiLs9yKD1WeU8/OvtToWpW7255FtA==", + "dev": true }, "pretty-error": { "version": "2.1.1", @@ -8841,7 +8840,8 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true }, "q": { "version": "1.5.1", @@ -9512,6 +9512,7 @@ "version": "0.4.7", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", + "dev": true, "requires": { "ajv": "^6.1.0", "ajv-keywords": "^3.1.0" @@ -11008,6 +11009,7 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, "requires": { "punycode": "^2.1.0" } @@ -12109,13 +12111,13 @@ "errno": "~0.1.7" } }, - "worker-loader": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-2.0.0.tgz", - "integrity": "sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==", + "worker-plugin": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/worker-plugin/-/worker-plugin-1.1.1.tgz", + "integrity": "sha512-s5XtToCv/eZdxZHB1t2Ggdl0F6jw+4qm5s/C3lxIp6z4V2aQRMte22x+v2Y1pbJzwTXE+jqGoa+wWn0oqovYyg==", + "dev": true, "requires": { - "loader-utils": "^1.0.0", - "schema-utils": "^0.4.0" + "loader-utils": "^1.1.0" } }, "wrap-ansi": { diff --git a/package.json b/package.json index bd668d53..cb01a7b5 100644 --- a/package.json +++ b/package.json @@ -59,14 +59,12 @@ "webpack-bundle-analyzer": "^2.13.1", "webpack-cli": "^2.1.5", "webpack-dev-server": "^3.1.5", - "webpack-plugin-replace": "^1.1.1" - }, - "dependencies": { + "webpack-plugin-replace": "^1.1.1", "classnames": "^2.2.6", "comlink": "^3.0.3", - "comlink-loader": "^1.0.0", - "preact": "^8.3.1", "linkstate": "^1.1.1", - "pretty-bytes": "^5.1.0" + "preact": "^8.3.1", + "pretty-bytes": "^5.1.0", + "worker-plugin": "^1.1.1" } } diff --git a/src/codecs/browser-bmp/encoder-meta.ts b/src/codecs/browser-bmp/encoder-meta.ts new file mode 100644 index 00000000..7a24e5ba --- /dev/null +++ b/src/codecs/browser-bmp/encoder-meta.ts @@ -0,0 +1,11 @@ +import { canvasEncodeTest } from '../generic/util'; + +export interface EncodeOptions { } +export interface EncoderState { type: typeof type; options: EncodeOptions; } + +export const type = 'browser-bmp'; +export const label = 'Browser BMP'; +export const mimeType = 'image/bmp'; +export const extension = 'bmp'; +export const defaultOptions: EncodeOptions = {}; +export const featureTest = () => canvasEncodeTest(mimeType); diff --git a/src/codecs/browser-bmp/encoder.ts b/src/codecs/browser-bmp/encoder.ts index 4b033f2b..7c879d97 100644 --- a/src/codecs/browser-bmp/encoder.ts +++ b/src/codecs/browser-bmp/encoder.ts @@ -1,16 +1,6 @@ +import { mimeType } from './encoder-meta'; import { canvasEncode } from '../../lib/util'; -import { canvasEncodeTest } from '../generic/util'; -export interface EncodeOptions { } -export interface EncoderState { type: typeof type; options: EncodeOptions; } - -export const type = 'browser-bmp'; -export const label = 'Browser BMP'; -export const mimeType = 'image/bmp'; -export const extension = 'bmp'; -export const defaultOptions: EncodeOptions = {}; -export const featureTest = () => canvasEncodeTest(mimeType); - -export function encode(data: ImageData, options: EncodeOptions) { +export function encode(data: ImageData) { return canvasEncode(data, mimeType); } diff --git a/src/codecs/browser-gif/encoder-meta.ts b/src/codecs/browser-gif/encoder-meta.ts new file mode 100644 index 00000000..dd373298 --- /dev/null +++ b/src/codecs/browser-gif/encoder-meta.ts @@ -0,0 +1,11 @@ +import { canvasEncodeTest } from '../generic/util'; + +export interface EncodeOptions {} +export interface EncoderState { type: typeof type; options: EncodeOptions; } + +export const type = 'browser-gif'; +export const label = 'Browser GIF'; +export const mimeType = 'image/gif'; +export const extension = 'gif'; +export const defaultOptions: EncodeOptions = {}; +export const featureTest = () => canvasEncodeTest(mimeType); diff --git a/src/codecs/browser-gif/encoder.ts b/src/codecs/browser-gif/encoder.ts index fea81fa3..7c879d97 100644 --- a/src/codecs/browser-gif/encoder.ts +++ b/src/codecs/browser-gif/encoder.ts @@ -1,16 +1,6 @@ +import { mimeType } from './encoder-meta'; import { canvasEncode } from '../../lib/util'; -import { canvasEncodeTest } from '../generic/util'; -export interface EncodeOptions {} -export interface EncoderState { type: typeof type; options: EncodeOptions; } - -export const type = 'browser-gif'; -export const label = 'Browser GIF'; -export const mimeType = 'image/gif'; -export const extension = 'gif'; -export const defaultOptions: EncodeOptions = {}; -export const featureTest = () => canvasEncodeTest(mimeType); - -export function encode(data: ImageData, options: EncodeOptions) { +export function encode(data: ImageData) { return canvasEncode(data, mimeType); } diff --git a/src/codecs/browser-jp2/encoder-meta.ts b/src/codecs/browser-jp2/encoder-meta.ts new file mode 100644 index 00000000..f6b56413 --- /dev/null +++ b/src/codecs/browser-jp2/encoder-meta.ts @@ -0,0 +1,11 @@ +import { canvasEncodeTest } from '../generic/util'; + +export interface EncodeOptions { } +export interface EncoderState { type: typeof type; options: EncodeOptions; } + +export const type = 'browser-jp2'; +export const label = 'Browser JPEG 2000'; +export const mimeType = 'image/jp2'; +export const extension = 'jp2'; +export const defaultOptions: EncodeOptions = {}; +export const featureTest = () => canvasEncodeTest(mimeType); diff --git a/src/codecs/browser-jp2/encoder.ts b/src/codecs/browser-jp2/encoder.ts index af9a3a92..7c879d97 100644 --- a/src/codecs/browser-jp2/encoder.ts +++ b/src/codecs/browser-jp2/encoder.ts @@ -1,16 +1,6 @@ +import { mimeType } from './encoder-meta'; import { canvasEncode } from '../../lib/util'; -import { canvasEncodeTest } from '../generic/util'; -export interface EncodeOptions { } -export interface EncoderState { type: typeof type; options: EncodeOptions; } - -export const type = 'browser-jp2'; -export const label = 'Browser JPEG 2000'; -export const mimeType = 'image/jp2'; -export const extension = 'jp2'; -export const defaultOptions: EncodeOptions = {}; -export const featureTest = () => canvasEncodeTest(mimeType); - -export function encode(data: ImageData, options: EncodeOptions) { +export function encode(data: ImageData) { return canvasEncode(data, mimeType); } diff --git a/src/codecs/browser-jpeg/encoder-meta.ts b/src/codecs/browser-jpeg/encoder-meta.ts new file mode 100644 index 00000000..0d43204e --- /dev/null +++ b/src/codecs/browser-jpeg/encoder-meta.ts @@ -0,0 +1,8 @@ +export interface EncodeOptions { quality: number; } +export interface EncoderState { type: typeof type; options: EncodeOptions; } + +export const type = 'browser-jpeg'; +export const label = 'Browser JPEG'; +export const mimeType = 'image/jpeg'; +export const extension = 'jpg'; +export const defaultOptions: EncodeOptions = { quality: 0.5 }; diff --git a/src/codecs/browser-jpeg/encoder.ts b/src/codecs/browser-jpeg/encoder.ts index 11ba5558..9e40156f 100644 --- a/src/codecs/browser-jpeg/encoder.ts +++ b/src/codecs/browser-jpeg/encoder.ts @@ -1,14 +1,6 @@ +import { EncodeOptions, mimeType } from './encoder-meta'; import { canvasEncode } from '../../lib/util'; -export interface EncodeOptions { quality: number; } -export interface EncoderState { type: typeof type; options: EncodeOptions; } - -export const type = 'browser-jpeg'; -export const label = 'Browser JPEG'; -export const mimeType = 'image/jpeg'; -export const extension = 'jpg'; -export const defaultOptions: EncodeOptions = { quality: 0.5 }; - export function encode(data: ImageData, { quality }: EncodeOptions) { return canvasEncode(data, mimeType, quality); } diff --git a/src/codecs/browser-pdf/encoder-meta.ts b/src/codecs/browser-pdf/encoder-meta.ts new file mode 100644 index 00000000..6fa0f369 --- /dev/null +++ b/src/codecs/browser-pdf/encoder-meta.ts @@ -0,0 +1,11 @@ +import { canvasEncodeTest } from '../generic/util'; + +export interface EncodeOptions { } +export interface EncoderState { type: typeof type; options: EncodeOptions; } + +export const type = 'browser-pdf'; +export const label = 'Browser PDF'; +export const mimeType = 'application/pdf'; +export const extension = 'pdf'; +export const defaultOptions: EncodeOptions = {}; +export const featureTest = () => canvasEncodeTest(mimeType); diff --git a/src/codecs/browser-pdf/encoder.ts b/src/codecs/browser-pdf/encoder.ts index a3c96193..7c879d97 100644 --- a/src/codecs/browser-pdf/encoder.ts +++ b/src/codecs/browser-pdf/encoder.ts @@ -1,16 +1,6 @@ +import { mimeType } from './encoder-meta'; import { canvasEncode } from '../../lib/util'; -import { canvasEncodeTest } from '../generic/util'; -export interface EncodeOptions { } -export interface EncoderState { type: typeof type; options: EncodeOptions; } - -export const type = 'browser-pdf'; -export const label = 'Browser PDF'; -export const mimeType = 'application/pdf'; -export const extension = 'pdf'; -export const defaultOptions: EncodeOptions = {}; -export const featureTest = () => canvasEncodeTest(mimeType); - -export function encode(data: ImageData, options: EncodeOptions) { +export function encode(data: ImageData) { return canvasEncode(data, mimeType); } diff --git a/src/codecs/browser-png/encoder-meta.ts b/src/codecs/browser-png/encoder-meta.ts new file mode 100644 index 00000000..a3c25aa2 --- /dev/null +++ b/src/codecs/browser-png/encoder-meta.ts @@ -0,0 +1,8 @@ +export interface EncodeOptions {} +export interface EncoderState { type: typeof type; options: EncodeOptions; } + +export const type = 'browser-png'; +export const label = 'Browser PNG'; +export const mimeType = 'image/png'; +export const extension = 'png'; +export const defaultOptions: EncodeOptions = {}; diff --git a/src/codecs/browser-png/encoder.tsx b/src/codecs/browser-png/encoder.tsx index e4294590..7c879d97 100644 --- a/src/codecs/browser-png/encoder.tsx +++ b/src/codecs/browser-png/encoder.tsx @@ -1,14 +1,6 @@ +import { mimeType } from './encoder-meta'; import { canvasEncode } from '../../lib/util'; -export interface EncodeOptions {} -export interface EncoderState { type: typeof type; options: EncodeOptions; } - -export const type = 'browser-png'; -export const label = 'Browser PNG'; -export const mimeType = 'image/png'; -export const extension = 'png'; -export const defaultOptions: EncodeOptions = {}; - -export function encode(data: ImageData, options: EncodeOptions) { +export function encode(data: ImageData) { return canvasEncode(data, mimeType); } diff --git a/src/codecs/browser-tiff/encoder-meta.ts b/src/codecs/browser-tiff/encoder-meta.ts new file mode 100644 index 00000000..f56f0827 --- /dev/null +++ b/src/codecs/browser-tiff/encoder-meta.ts @@ -0,0 +1,11 @@ +import { canvasEncodeTest } from '../generic/util'; + +export interface EncodeOptions { } +export interface EncoderState { type: typeof type; options: EncodeOptions; } + +export const type = 'browser-tiff'; +export const label = 'Browser TIFF'; +export const mimeType = 'image/tiff'; +export const extension = 'tiff'; +export const defaultOptions: EncodeOptions = {}; +export const featureTest = () => canvasEncodeTest(mimeType); diff --git a/src/codecs/browser-tiff/encoder.ts b/src/codecs/browser-tiff/encoder.ts index 6b403390..7c879d97 100644 --- a/src/codecs/browser-tiff/encoder.ts +++ b/src/codecs/browser-tiff/encoder.ts @@ -1,16 +1,6 @@ +import { mimeType } from './encoder-meta'; import { canvasEncode } from '../../lib/util'; -import { canvasEncodeTest } from '../generic/util'; -export interface EncodeOptions { } -export interface EncoderState { type: typeof type; options: EncodeOptions; } - -export const type = 'browser-tiff'; -export const label = 'Browser TIFF'; -export const mimeType = 'image/tiff'; -export const extension = 'tiff'; -export const defaultOptions: EncodeOptions = {}; -export const featureTest = () => canvasEncodeTest(mimeType); - -export function encode(data: ImageData, options: EncodeOptions) { +export function encode(data: ImageData) { return canvasEncode(data, mimeType); } diff --git a/src/codecs/browser-webp/decoder.ts b/src/codecs/browser-webp/decoder.ts deleted file mode 100644 index ff58ec24..00000000 --- a/src/codecs/browser-webp/decoder.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { canDecodeImage, nativeDecode } from '../../lib/util'; - -export const name = 'Browser WebP Decoder'; -export async function decode(blob: Blob): Promise { - return nativeDecode(blob); -} - -// tslint:disable-next-line:max-line-length It’s a data URL. Whatcha gonna do? -const webpFile = 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA='; - -export function isSupported(): Promise { - return canDecodeImage(webpFile); -} - -const supportedMimeTypes = ['image/webp']; -export function canHandleMimeType(mimeType: string): boolean { - return supportedMimeTypes.includes(mimeType); -} diff --git a/src/codecs/browser-webp/encoder-meta.ts b/src/codecs/browser-webp/encoder-meta.ts new file mode 100644 index 00000000..d7780687 --- /dev/null +++ b/src/codecs/browser-webp/encoder-meta.ts @@ -0,0 +1,11 @@ +import { canvasEncodeTest } from '../generic/util'; + +export interface EncodeOptions { quality: number; } +export interface EncoderState { type: typeof type; options: EncodeOptions; } + +export const type = 'browser-webp'; +export const label = 'Browser WebP'; +export const mimeType = 'image/webp'; +export const extension = 'webp'; +export const defaultOptions: EncodeOptions = { quality: 0.5 }; +export const featureTest = () => canvasEncodeTest(mimeType); diff --git a/src/codecs/browser-webp/encoder.ts b/src/codecs/browser-webp/encoder.ts index 55e13bb8..9e40156f 100644 --- a/src/codecs/browser-webp/encoder.ts +++ b/src/codecs/browser-webp/encoder.ts @@ -1,15 +1,5 @@ +import { EncodeOptions, mimeType } from './encoder-meta'; import { canvasEncode } from '../../lib/util'; -import { canvasEncodeTest } from '../generic/util'; - -export interface EncodeOptions { quality: number; } -export interface EncoderState { type: typeof type; options: EncodeOptions; } - -export const type = 'browser-webp'; -export const label = 'Browser WebP'; -export const mimeType = 'image/webp'; -export const extension = 'webp'; -export const defaultOptions: EncodeOptions = { quality: 0.5 }; -export const featureTest = () => canvasEncodeTest(mimeType); export function encode(data: ImageData, { quality }: EncodeOptions) { return canvasEncode(data, mimeType, quality); diff --git a/src/codecs/decoders.ts b/src/codecs/decoders.ts index d621e329..6ffb09c2 100644 --- a/src/codecs/decoders.ts +++ b/src/codecs/decoders.ts @@ -1,47 +1,21 @@ -import * as wasmWebp from './webp/decoder'; -import * as browserWebp from './webp/decoder'; +import { nativeDecode, sniffMimeType, canDecodeImage } from '../lib/util'; +import Processor from './processor'; -import { nativeDecode, sniffMimeType } from '../lib/util'; +// tslint:disable-next-line:max-line-length It’s a data URL. Whatcha gonna do? +const webpFile = 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA='; +const nativeWebPSupported = canDecodeImage(webpFile); -export interface Decoder { - name: string; - decode(blob: Blob): Promise; - isSupported(): Promise; - canHandleMimeType(mimeType: string): boolean; -} - -// We load all decoders and filter out the unsupported ones. -export const decodersPromise: Promise = Promise.all( - [ - browserWebp, - wasmWebp, - ] - .map(async (decoder) => { - if (await decoder.isSupported()) { - return decoder; - } - return null; - }), -// TypeScript is not smart enough to realized that I’m filtering all the falsy -// values here. -).then(list => list.filter(item => !!item)) as any as Promise; - -async function findDecodersByMimeType(mimeType: string): Promise { - const decoders = await decodersPromise; - return decoders.filter(decoder => decoder.canHandleMimeType(mimeType)); -} - -export async function decodeImage(blob: Blob): Promise { +export async function decodeImage(blob: Blob, processor: Processor): Promise { const mimeType = await sniffMimeType(blob); - const decoders = await findDecodersByMimeType(mimeType); - if (decoders.length <= 0) { - // If we can’t find a decoder, hailmary with the browser's decoders - return nativeDecode(blob); + + try { + if (mimeType === 'image/webp' && !(await nativeWebPSupported)) { + return await processor.webpDecode(blob); + } + + // Otherwise, just throw it at the browser's decoder. + return await nativeDecode(blob); + } catch (err) { + throw Error("Couldn't decode image"); } - for (const decoder of decoders) { - try { - return await decoder.decode(blob); - } catch { } - } - throw new Error('No decoder could decode image'); } diff --git a/src/codecs/encoders.ts b/src/codecs/encoders.ts index fae1fcbe..ef053535 100644 --- a/src/codecs/encoders.ts +++ b/src/codecs/encoders.ts @@ -1,15 +1,15 @@ -import * as identity from './identity/encoder'; -import * as optiPNG from './optipng/encoder'; -import * as mozJPEG from './mozjpeg/encoder'; -import * as webP from './webp/encoder'; -import * as browserPNG from './browser-png/encoder'; -import * as browserJPEG from './browser-jpeg/encoder'; -import * as browserWebP from './browser-webp/encoder'; -import * as browserGIF from './browser-gif/encoder'; -import * as browserTIFF from './browser-tiff/encoder'; -import * as browserJP2 from './browser-jp2/encoder'; -import * as browserBMP from './browser-bmp/encoder'; -import * as browserPDF from './browser-pdf/encoder'; +import * as identity from './identity/encoder-meta'; +import * as optiPNG from './optipng/encoder-meta'; +import * as mozJPEG from './mozjpeg/encoder-meta'; +import * as webP from './webp/encoder-meta'; +import * as browserPNG from './browser-png/encoder-meta'; +import * as browserJPEG from './browser-jpeg/encoder-meta'; +import * as browserWebP from './browser-webp/encoder-meta'; +import * as browserGIF from './browser-gif/encoder-meta'; +import * as browserTIFF from './browser-tiff/encoder-meta'; +import * as browserJP2 from './browser-jp2/encoder-meta'; +import * as browserBMP from './browser-bmp/encoder-meta'; +import * as browserPDF from './browser-pdf/encoder-meta'; export interface EncoderSupportMap { [key: string]: boolean; diff --git a/src/codecs/identity/encoder.ts b/src/codecs/identity/encoder-meta.ts similarity index 100% rename from src/codecs/identity/encoder.ts rename to src/codecs/identity/encoder-meta.ts diff --git a/src/codecs/imagequant/Quantizer.worker.ts b/src/codecs/imagequant/Quantizer.worker.ts deleted file mode 100644 index 9a73d8fe..00000000 --- a/src/codecs/imagequant/Quantizer.worker.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { QuantizeOptions } from './quantizer'; -import imagequant, { QuantizerModule } from '../../../codecs/imagequant/imagequant'; -// Using require() so TypeScript doesn’t complain about this not being a module. -const wasmBinaryUrl = require('../../../codecs/imagequant/imagequant.wasm'); - -export default class ImageQuant { - private emscriptenModule: Promise; - - constructor() { - this.emscriptenModule = new Promise((resolve) => { - const m = imagequant({ - // Just to be safe, don’t automatically invoke any wasm functions - noInitialRun: false, - locateFile(url: string): string { - // Redirect the request for the wasm binary to whatever webpack gave us. - if (url.endsWith('.wasm')) { - return wasmBinaryUrl; - } - return url; - }, - onRuntimeInitialized() { - // An Emscripten is a then-able that, for some reason, `then()`s itself, - // causing an infite loop when you wrap it in a real promise. Deleting the `then` - // prop solves this for now. - // See: https://github.com/kripken/emscripten/blob/incoming/src/postamble.js#L129 - // TODO(surma@): File a bug with Emscripten on this. - delete (m as any).then; - resolve(m); - }, - }); - }); - } - - async quantize(data: ImageData, opts: QuantizeOptions): Promise { - const m = await this.emscriptenModule; - - const result = opts.zx ? - m.zx_quantize(data.data, data.width, data.height, opts.dither) - : - m.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither); - - m.free_result(); - - return new ImageData(new Uint8ClampedArray(result.buffer), result.width, result.height); - } -} diff --git a/src/codecs/imagequant/options.tsx b/src/codecs/imagequant/options.tsx index 28c4ff6c..82eb82bd 100644 --- a/src/codecs/imagequant/options.tsx +++ b/src/codecs/imagequant/options.tsx @@ -1,7 +1,7 @@ import { h, Component } from 'preact'; import { bind } from '../../lib/initial-util'; import { inputFieldValueAsNumber, konami } from '../../lib/util'; -import { QuantizeOptions } from './quantizer'; +import { QuantizeOptions } from './processor-meta'; const konamiPromise = konami(); diff --git a/src/codecs/imagequant/processor-meta.ts b/src/codecs/imagequant/processor-meta.ts new file mode 100644 index 00000000..960cf7e1 --- /dev/null +++ b/src/codecs/imagequant/processor-meta.ts @@ -0,0 +1,11 @@ +export interface QuantizeOptions { + zx: number; + maxNumColors: number; + dither: number; +} + +export const defaultOptions: QuantizeOptions = { + zx: 0, + maxNumColors: 256, + dither: 1.0, +}; diff --git a/src/codecs/imagequant/processor.ts b/src/codecs/imagequant/processor.ts new file mode 100644 index 00000000..c9362211 --- /dev/null +++ b/src/codecs/imagequant/processor.ts @@ -0,0 +1,21 @@ +import imagequant, { QuantizerModule } from '../../../codecs/imagequant/imagequant'; +import wasmUrl from '../../../codecs/imagequant/imagequant.wasm'; +import { QuantizeOptions } from './processor-meta'; +import { initWasmModule } from '../util'; + +let emscriptenModule: Promise; + +export async function process(data: ImageData, opts: QuantizeOptions): Promise { + if (!emscriptenModule) emscriptenModule = initWasmModule(imagequant, wasmUrl); + + const module = await emscriptenModule; + + const result = opts.zx ? + module.zx_quantize(data.data, data.width, data.height, opts.dither) + : + module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither); + + module.free_result(); + + return new ImageData(new Uint8ClampedArray(result.buffer), result.width, result.height); +} diff --git a/src/codecs/imagequant/quantizer.ts b/src/codecs/imagequant/quantizer.ts deleted file mode 100644 index a53b060f..00000000 --- a/src/codecs/imagequant/quantizer.ts +++ /dev/null @@ -1,18 +0,0 @@ -import QuantizerWorker from './Quantizer.worker'; - -export async function quantize(data: ImageData, opts: QuantizeOptions): Promise { - const quantizer = await new QuantizerWorker(); - return quantizer.quantize(data, opts); -} - -export interface QuantizeOptions { - zx: number; - maxNumColors: number; - dither: number; -} - -export const defaultOptions: QuantizeOptions = { - zx: 0, - maxNumColors: 256, - dither: 1.0, -}; diff --git a/src/codecs/mozjpeg/Encoder.worker.ts b/src/codecs/mozjpeg/Encoder.worker.ts deleted file mode 100644 index 4839cf00..00000000 --- a/src/codecs/mozjpeg/Encoder.worker.ts +++ /dev/null @@ -1,43 +0,0 @@ -import mozjpeg_enc, { MozJPEGModule } from '../../../codecs/mozjpeg_enc/mozjpeg_enc'; -// Using require() so TypeScript doesn’t complain about this not being a module. -import { EncodeOptions } from './encoder'; -const wasmBinaryUrl = require('../../../codecs/mozjpeg_enc/mozjpeg_enc.wasm'); - -export default class MozJpegEncoder { - private emscriptenModule: Promise; - - constructor() { - this.emscriptenModule = new Promise((resolve) => { - const m = mozjpeg_enc({ - // Just to be safe, don’t automatically invoke any wasm functions - noInitialRun: false, - locateFile(url: string): string { - // Redirect the request for the wasm binary to whatever webpack gave us. - if (url.endsWith('.wasm')) { - return wasmBinaryUrl; - } - return url; - }, - onRuntimeInitialized() { - // An Emscripten is a then-able that, for some reason, `then()`s itself, - // causing an infite loop when you wrap it in a real promise. Deleten the `then` - // prop solves this for now. - // See: https://github.com/kripken/emscripten/blob/incoming/src/postamble.js#L129 - // TODO(surma@): File a bug with Emscripten on this. - delete (m as any).then; - resolve(m); - }, - }); - }); - } - - async encode(data: ImageData, options: EncodeOptions): Promise { - const module = await this.emscriptenModule; - const resultView = module.encode(data.data, data.width, data.height, options); - const result = new Uint8Array(resultView); - module.free_result(); - - // wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer. - return result.buffer as ArrayBuffer; - } -} diff --git a/src/codecs/mozjpeg/encoder-meta.ts b/src/codecs/mozjpeg/encoder-meta.ts new file mode 100644 index 00000000..b8cb8700 --- /dev/null +++ b/src/codecs/mozjpeg/encoder-meta.ts @@ -0,0 +1,41 @@ +export enum MozJpegColorSpace { + GRAYSCALE = 1, + RGB, + YCbCr, +} + +export interface EncodeOptions { + quality: number; + baseline: boolean; + arithmetic: boolean; + progressive: boolean; + optimize_coding: boolean; + smoothing: number; + color_space: MozJpegColorSpace; + quant_table: number; + trellis_multipass: boolean; + trellis_opt_zero: boolean; + trellis_opt_table: boolean; + trellis_loops: number; +} + +export interface EncoderState { type: typeof type; options: EncodeOptions; } + +export const type = 'mozjpeg'; +export const label = 'MozJPEG'; +export const mimeType = 'image/jpeg'; +export const extension = 'jpg'; +export const defaultOptions: EncodeOptions = { + quality: 75, + baseline: false, + arithmetic: false, + progressive: true, + optimize_coding: true, + smoothing: 0, + color_space: MozJpegColorSpace.YCbCr, + quant_table: 3, + trellis_multipass: false, + trellis_opt_zero: false, + trellis_opt_table: false, + trellis_loops: 1, +}; diff --git a/src/codecs/mozjpeg/encoder.ts b/src/codecs/mozjpeg/encoder.ts index 90a976d1..6514a169 100644 --- a/src/codecs/mozjpeg/encoder.ts +++ b/src/codecs/mozjpeg/encoder.ts @@ -1,49 +1,18 @@ -import EncoderWorker from './Encoder.worker'; +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 { initWasmModule } from '../util'; -export enum MozJpegColorSpace { - GRAYSCALE = 1, - RGB, - YCbCr, -} - -export interface EncodeOptions { - quality: number; - baseline: boolean; - arithmetic: boolean; - progressive: boolean; - optimize_coding: boolean; - smoothing: number; - color_space: MozJpegColorSpace; - quant_table: number; - trellis_multipass: boolean; - trellis_opt_zero: boolean; - trellis_opt_table: boolean; - trellis_loops: number; -} - -export interface EncoderState { type: typeof type; options: EncodeOptions; } - -export const type = 'mozjpeg'; -export const label = 'MozJPEG'; -export const mimeType = 'image/jpeg'; -export const extension = 'jpg'; -export const defaultOptions: EncodeOptions = { - quality: 75, - baseline: false, - arithmetic: false, - progressive: true, - optimize_coding: true, - smoothing: 0, - color_space: MozJpegColorSpace.YCbCr, - quant_table: 3, - trellis_multipass: false, - trellis_opt_zero: false, - trellis_opt_table: false, - trellis_loops: 1, -}; - -export async function encode(data: ImageData, options: EncodeOptions) { - // We need to await this because it's been comlinked. - const encoder = await new EncoderWorker(); - return encoder.encode(data, options); +let emscriptenModule: Promise; + +export async function encode(data: ImageData, options: EncodeOptions): Promise { + if (!emscriptenModule) emscriptenModule = initWasmModule(mozjpeg_enc, wasmUrl); + + const module = await emscriptenModule; + const resultView = module.encode(data.data, data.width, data.height, options); + const result = new Uint8Array(resultView); + module.free_result(); + + // wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer. + return result.buffer as ArrayBuffer; } diff --git a/src/codecs/mozjpeg/options.tsx b/src/codecs/mozjpeg/options.tsx index dd0eb9b2..84b86ec5 100644 --- a/src/codecs/mozjpeg/options.tsx +++ b/src/codecs/mozjpeg/options.tsx @@ -1,7 +1,7 @@ import { h, Component } from 'preact'; import { bind } from '../../lib/initial-util'; import { inputFieldChecked, inputFieldValueAsNumber } from '../../lib/util'; -import { EncodeOptions, MozJpegColorSpace } from './encoder'; +import { EncodeOptions, MozJpegColorSpace } from './encoder-meta'; import '../../custom-els/RangeInput'; type Props = { diff --git a/src/codecs/optipng/Encoder.worker.ts b/src/codecs/optipng/Encoder.worker.ts deleted file mode 100644 index 3c56e144..00000000 --- a/src/codecs/optipng/Encoder.worker.ts +++ /dev/null @@ -1,41 +0,0 @@ -import optipng, { OptiPngModule } from '../../../codecs/optipng/optipng'; -// Using require() so TypeScript doesn’t complain about this not being a module. -import { EncodeOptions } from './encoder'; -const wasmBinaryUrl = require('../../../codecs/optipng/optipng.wasm'); - -export default class OptiPng { - private emscriptenModule: Promise; - - constructor() { - this.emscriptenModule = new Promise((resolve) => { - const m = optipng({ - // Just to be safe, don’t automatically invoke any wasm functions - noInitialRun: false, - locateFile(url: string): string { - // Redirect the request for the wasm binary to whatever webpack gave us. - if (url.endsWith('.wasm')) { - return wasmBinaryUrl; - } - return url; - }, - onRuntimeInitialized() { - // An Emscripten is a then-able that, for some reason, `then()`s itself, - // causing an infite loop when you wrap it in a real promise. Deleting the `then` - // prop solves this for now. - // See: https://github.com/kripken/emscripten/blob/incoming/src/postamble.js#L129 - // TODO(surma@): File a bug with Emscripten on this. - delete (m as any).then; - resolve(m); - }, - }); - }); - } - - async compress(data: BufferSource, opts: EncodeOptions): Promise { - const m = await this.emscriptenModule; - const result = m.compress(data, opts); - const copy = new Uint8Array(result).buffer as ArrayBuffer; - m.free_result(); - return copy; - } -} diff --git a/src/codecs/optipng/encoder-meta.ts b/src/codecs/optipng/encoder-meta.ts new file mode 100644 index 00000000..0fa86ea5 --- /dev/null +++ b/src/codecs/optipng/encoder-meta.ts @@ -0,0 +1,13 @@ +export interface EncodeOptions { + level: number; +} +export interface EncoderState { type: typeof type; options: EncodeOptions; } + +export const type = 'png'; +export const label = 'OptiPNG'; +export const mimeType = 'image/png'; +export const extension = 'png'; + +export const defaultOptions: EncodeOptions = { + level: 2, +}; diff --git a/src/codecs/optipng/encoder.ts b/src/codecs/optipng/encoder.ts index 4424135d..0e5b00a1 100644 --- a/src/codecs/optipng/encoder.ts +++ b/src/codecs/optipng/encoder.ts @@ -1,23 +1,18 @@ -import { canvasEncode, blobToArrayBuffer } from '../../lib/util'; -import EncodeWorker from './Encoder.worker'; +import optipng, { OptiPngModule } from '../../../codecs/optipng/optipng'; +import wasmUrl from '../../../codecs/optipng/optipng.wasm'; +import { EncodeOptions } from './encoder-meta'; +import { initWasmModule } from '../util'; -export interface EncodeOptions { - level: number; -} -export interface EncoderState { type: typeof type; options: EncodeOptions; } - -export const type = 'png'; -export const label = 'OptiPNG'; -export const mimeType = 'image/png'; -export const extension = 'png'; - -export const defaultOptions: EncodeOptions = { - level: 2, -}; - -export async function encode(data: ImageData, opts: EncodeOptions): Promise { - const pngBlob = await canvasEncode(data, mimeType); - const pngBuffer = await blobToArrayBuffer(pngBlob); - const encodeWorker = await new EncodeWorker(); - return encodeWorker.compress(pngBuffer, opts); +let emscriptenModule: Promise; + +export async function compress(data: BufferSource, options: EncodeOptions): Promise { + if (!emscriptenModule) emscriptenModule = initWasmModule(optipng, wasmUrl); + + const module = await emscriptenModule; + const resultView = module.compress(data, options); + const result = new Uint8Array(resultView); + module.free_result(); + + // wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer. + return result.buffer as ArrayBuffer; } diff --git a/src/codecs/optipng/options.tsx b/src/codecs/optipng/options.tsx index bc6a9158..28dd9038 100644 --- a/src/codecs/optipng/options.tsx +++ b/src/codecs/optipng/options.tsx @@ -1,7 +1,7 @@ import { h, Component } from 'preact'; import { bind } from '../../lib/initial-util'; import { inputFieldValueAsNumber } from '../../lib/util'; -import { EncodeOptions } from './encoder'; +import { EncodeOptions } from './encoder-meta'; type Props = { options: EncodeOptions; diff --git a/src/codecs/preprocessors.ts b/src/codecs/preprocessors.ts index 878ff7c4..859437e0 100644 --- a/src/codecs/preprocessors.ts +++ b/src/codecs/preprocessors.ts @@ -1,5 +1,7 @@ -import { QuantizeOptions, defaultOptions as quantizerDefaultOptions } from './imagequant/quantizer'; -import { ResizeOptions, defaultOptions as resizeDefaultOptions } from './resize/resize'; +import { + QuantizeOptions, defaultOptions as quantizerDefaultOptions, +} from './imagequant/processor-meta'; +import { ResizeOptions, defaultOptions as resizeDefaultOptions } from './resize/processor-meta'; interface Enableable { enabled: boolean; diff --git a/src/codecs/processor-worker.ts b/src/codecs/processor-worker.ts new file mode 100644 index 00000000..7e6b110e --- /dev/null +++ b/src/codecs/processor-worker.ts @@ -0,0 +1,41 @@ +import { expose } from 'comlink'; +import { EncodeOptions as MozJPEGEncoderOptions } from './mozjpeg/encoder-meta'; +import { QuantizeOptions } from './imagequant/processor-meta'; +import { EncodeOptions as OptiPNGEncoderOptions } from './optipng/encoder-meta'; +import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta'; + +async function mozjpegEncode( + data: ImageData, options: MozJPEGEncoderOptions, +): Promise { + const { encode } = await import('./mozjpeg/encoder'); + return encode(data, options); +} + +async function quantize(data: ImageData, opts: QuantizeOptions): Promise { + const { process } = await import('./imagequant/processor'); + return process(data, opts); +} + +async function optiPngEncode( + data: BufferSource, options: OptiPNGEncoderOptions, +): Promise { + const { compress } = await import('./optipng/encoder'); + return compress(data, options); +} + +async function webpEncode( + data: ImageData, options: WebPEncoderOptions, +): Promise { + const { encode } = await import('./webp/encoder'); + return encode(data, options); +} + +async function webpDecode(data: ArrayBuffer): Promise { + const { decode } = await import('./webp/decoder'); + return decode(data); +} + +const exports = { mozjpegEncode, quantize, optiPngEncode, webpEncode, webpDecode }; +export type ProcessorWorkerApi = typeof exports; + +expose(exports, self); diff --git a/src/codecs/processor.ts b/src/codecs/processor.ts new file mode 100644 index 00000000..994d4d06 --- /dev/null +++ b/src/codecs/processor.ts @@ -0,0 +1,205 @@ +import { proxy } from 'comlink'; +import { QuantizeOptions } from './imagequant/processor-meta'; +import { ProcessorWorkerApi } from './processor-worker'; +import { canvasEncode, blobToArrayBuffer } from '../lib/util'; +import { EncodeOptions as MozJPEGEncoderOptions } from './mozjpeg/encoder-meta'; +import { EncodeOptions as OptiPNGEncoderOptions } from './optipng/encoder-meta'; +import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta'; +import { EncodeOptions as BrowserJPEGOptions } from './browser-jpeg/encoder-meta'; +import { EncodeOptions as BrowserWebpEncodeOptions } from './browser-webp/encoder-meta'; +import { BitmapResizeOptions, VectorResizeOptions } from './resize/processor-meta'; +import { resize, vectorResize } from './resize/processor'; +import * as browserBMP from './browser-bmp/encoder'; +import * as browserPNG from './browser-png/encoder'; +import * as browserJPEG from './browser-jpeg/encoder'; +import * as browserWebP from './browser-webp/encoder'; +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'; + +/** How long the worker should be idle before terminating. */ +const workerTimeout = 1000; + +interface ProcessingJobOptions { + needsWorker?: boolean; +} + +export default class Processor { + /** Worker instance associated with this processor. */ + private _worker?: Worker; + /** Comlinked worker API. */ + private _workerApi?: ProcessorWorkerApi; + /** Rejector for a pending promise. */ + private _abortRejector?: (err: Error) => void; + /** Is work currently happening? */ + private _busy = false; + /** Incementing ID so we can tell if a job has been superseded. */ + private _latestJobId: number = 0; + /** setTimeout ID for killing the worker when idle. */ + private _workerTimeoutId: number = 0; + + /** + * 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(); + + if (needsWorker) self.clearTimeout(this._workerTimeoutId); + + if (!this._worker && needsWorker) { + // worker-loader does magic here. + // @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.ts', { type: 'module' }) as Worker; + // Need to do some TypeScript trickery to make the type match. + this._workerApi = proxy(this._worker) as any as ProcessorWorkerApi; + } + + this._busy = true; + + const returnVal = Promise.race([ + processingFunc.call(this, ...args), + new Promise((_, reject) => { this._abortRejector = reject; }), + ]); + + // Wait for the operation to settle. + await returnVal.catch(() => {}); + + // If no other jobs are happening, cleanup. + if (jobId === this._latestJobId) this._jobCleanup(); + + return returnVal; + }; + }; + } + + private _jobCleanup(): void { + this._busy = false; + + if (!this._worker) return; + + // If the worker is unused for 10 seconds, remove it to save memory. + this._workerTimeoutId = self.setTimeout( + () => { + if (this._busy) throw Error("Worker shouldn't be busy"); + if (!this._worker) return; + this._worker.terminate(); + this._worker = undefined; + }, + workerTimeout, + ); + } + + /** Abort the current job, if any */ + abortCurrent() { + if (!this._busy) return; + if (!this._worker || !this._abortRejector) { + throw Error("There must be a worker/rejector if it's busy"); + } + this._abortRejector(new DOMException('Aborted', 'AbortError')); + this._worker.terminate(); + this._worker = undefined; + this._abortRejector = undefined; + this._busy = false; + } + + // Off main thread jobs: + + @Processor._processingJob({ needsWorker: true }) + imageQuant(data: ImageData, opts: QuantizeOptions): Promise { + return this._workerApi!.quantize(data, opts); + } + + @Processor._processingJob({ needsWorker: true }) + mozjpegEncode( + data: ImageData, opts: MozJPEGEncoderOptions, + ): Promise { + return this._workerApi!.mozjpegEncode(data, opts); + } + + @Processor._processingJob({ needsWorker: true }) + async optiPngEncode( + data: ImageData, opts: OptiPNGEncoderOptions, + ): Promise { + // OptiPNG expects PNG input. + const pngBlob = await canvasEncode(data, 'image/png'); + const pngBuffer = await blobToArrayBuffer(pngBlob); + return this._workerApi!.optiPngEncode(pngBuffer, opts); + } + + @Processor._processingJob({ needsWorker: true }) + webpEncode(data: ImageData, opts: WebPEncoderOptions): Promise { + return this._workerApi!.webpEncode(data, opts); + } + + @Processor._processingJob({ needsWorker: true }) + async webpDecode(blob: Blob): Promise { + const data = await blobToArrayBuffer(blob); + return this._workerApi!.webpDecode(data); + } + + // Not-worker jobs: + + @Processor._processingJob() + browserBmpEncode(data: ImageData): Promise { + return browserBMP.encode(data); + } + + @Processor._processingJob() + browserPngEncode(data: ImageData): Promise { + return browserPNG.encode(data); + } + + @Processor._processingJob() + browserJpegEncode(data: ImageData, opts: BrowserJPEGOptions): Promise { + return browserJPEG.encode(data, opts); + } + + @Processor._processingJob() + browserWebpEncode(data: ImageData, opts: BrowserWebpEncodeOptions): Promise { + return browserWebP.encode(data, opts); + } + + @Processor._processingJob() + browserGifEncode(data: ImageData): Promise { + return browserGIF.encode(data); + } + + @Processor._processingJob() + browserTiffEncode(data: ImageData): Promise { + return browserTIFF.encode(data); + } + + @Processor._processingJob() + browserJp2Encode(data: ImageData): Promise { + return browserJP2.encode(data); + } + + @Processor._processingJob() + browserPdfEncode(data: ImageData): Promise { + return browserPDF.encode(data); + } + + // Synchronous jobs + + resize(data: ImageData, opts: BitmapResizeOptions) { + this.abortCurrent(); + return resize(data, opts); + } + + vectorResize(data: HTMLImageElement, opts: VectorResizeOptions) { + this.abortCurrent(); + return vectorResize(data, opts); + } +} diff --git a/src/codecs/resize/options.tsx b/src/codecs/resize/options.tsx index 11027a6e..c6e461a7 100644 --- a/src/codecs/resize/options.tsx +++ b/src/codecs/resize/options.tsx @@ -2,7 +2,7 @@ import { h, Component } from 'preact'; import linkState from 'linkstate'; import { bind } from '../../lib/initial-util'; import { inputFieldValueAsNumber } from '../../lib/util'; -import { ResizeOptions } from './resize'; +import { ResizeOptions } from './processor-meta'; interface Props { isVector: Boolean; diff --git a/src/codecs/resize/processor-meta.ts b/src/codecs/resize/processor-meta.ts new file mode 100644 index 00000000..bc2c5f1e --- /dev/null +++ b/src/codecs/resize/processor-meta.ts @@ -0,0 +1,26 @@ +type BitmapResizeMethods = 'browser-pixelated' | 'browser-low' | 'browser-medium' | 'browser-high'; + +export interface ResizeOptions { + width: number; + height: number; + method: 'vector' | BitmapResizeMethods; + fitMethod: 'stretch' | 'cover'; +} + +export interface BitmapResizeOptions extends ResizeOptions { + method: BitmapResizeMethods; +} + +export interface VectorResizeOptions extends ResizeOptions { + method: 'vector'; +} + +export const defaultOptions: ResizeOptions = { + // Width and height will always default to the image size. + // This is set elsewhere. + width: 1, + height: 1, + // This will be set to 'vector' if the input is SVG. + method: 'browser-high', + fitMethod: 'stretch', +}; diff --git a/src/codecs/resize/resize.ts b/src/codecs/resize/processor.ts similarity index 65% rename from src/codecs/resize/resize.ts rename to src/codecs/resize/processor.ts index 70bf08f4..55cbe319 100644 --- a/src/codecs/resize/resize.ts +++ b/src/codecs/resize/processor.ts @@ -1,4 +1,5 @@ import { nativeResize, NativeResizeMethod, drawableToImageData } from '../../lib/util'; +import { BitmapResizeOptions, VectorResizeOptions } from './processor-meta'; function getCoverOffsets(sw: number, sh: number, dw: number, dh: number) { const currentAspect = sw / sh; @@ -46,30 +47,3 @@ export function vectorResize(data: HTMLImageElement, opts: VectorResizeOptions): width: opts.width, height: opts.height, }); } - -type BitmapResizeMethods = 'browser-pixelated' | 'browser-low' | 'browser-medium' | 'browser-high'; - -export interface ResizeOptions { - width: number; - height: number; - method: 'vector' | BitmapResizeMethods; - fitMethod: 'stretch' | 'cover'; -} - -export interface BitmapResizeOptions extends ResizeOptions { - method: BitmapResizeMethods; -} - -export interface VectorResizeOptions extends ResizeOptions { - method: 'vector'; -} - -export const defaultOptions: ResizeOptions = { - // Width and height will always default to the image size. - // This is set elsewhere. - width: 1, - height: 1, - // This will be set to 'vector' if the input is SVG. - method: 'browser-high', - fitMethod: 'stretch', -}; diff --git a/src/codecs/util.ts b/src/codecs/util.ts new file mode 100644 index 00000000..7498e816 --- /dev/null +++ b/src/codecs/util.ts @@ -0,0 +1,27 @@ +type ModuleFactory = ( + opts: EmscriptenWasm.ModuleOpts, +) => M; + +export function initWasmModule( + moduleFactory: ModuleFactory, + wasmUrl: string, +): Promise { + return new Promise((resolve) => { + const module = moduleFactory({ + // Just to be safe, don't automatically invoke any wasm functions + noInitialRun: true, + locateFile(url: string): string { + // Redirect the request for the wasm binary to whatever webpack gave us. + if (url.endsWith('.wasm')) return wasmUrl; + return url; + }, + onRuntimeInitialized() { + // An Emscripten is a then-able that resolves with itself, causing an infite loop when you + // wrap it in a real promise. Delete the `then` prop solves this for now. + // https://github.com/kripken/emscripten/issues/5820 + delete (module as any).then; + resolve(module); + }, + }); + }); +} diff --git a/src/codecs/webp/Decoder.worker.ts b/src/codecs/webp/Decoder.worker.ts deleted file mode 100644 index 5def40bf..00000000 --- a/src/codecs/webp/Decoder.worker.ts +++ /dev/null @@ -1,46 +0,0 @@ -import webp_dec, { WebPModule } from '../../../codecs/webp_dec/webp_dec'; -// Using require() so TypeScript doesn’t complain about this not being a module. -const wasmBinaryUrl = require('../../../codecs/webp_dec/webp_dec.wasm'); - -// API exposed by wasm module. Details in the codec’s README. - -export default class WebpDecoder { - private emscriptenModule: Promise; - - constructor() { - this.emscriptenModule = new Promise((resolve) => { - const m = webp_dec({ - // Just to be safe, don’t automatically invoke any wasm functions - noInitialRun: false, - locateFile(url: string): string { - // Redirect the request for the wasm binary to whatever webpack gave us. - if (url.endsWith('.wasm')) { - return wasmBinaryUrl; - } - return url; - }, - onRuntimeInitialized() { - // An Emscripten is a then-able that, for some reason, `then()`s itself, - // causing an infite loop when you wrap it in a real promise. Deleting the `then` - // prop solves this for now. - // See: https://github.com/kripken/emscripten/blob/incoming/src/postamble.js#L129 - // TODO(surma@): File a bug with Emscripten on this. - delete (m as any).then; - resolve(m); - }, - }); - }); - } - - async decode(data: ArrayBuffer): Promise { - const m = await this.emscriptenModule; - const rawImage = m.decode(data); - m.free_result(); - - return new ImageData( - new Uint8ClampedArray(rawImage.buffer), - rawImage.width, - rawImage.height, - ); - } -} diff --git a/src/codecs/webp/Encoder.worker.ts b/src/codecs/webp/Encoder.worker.ts deleted file mode 100644 index 2c2f0c82..00000000 --- a/src/codecs/webp/Encoder.worker.ts +++ /dev/null @@ -1,43 +0,0 @@ -import webp_enc, { WebPModule } from '../../../codecs/webp_enc/webp_enc'; -// Using require() so TypeScript doesn’t complain about this not being a module. -import { EncodeOptions } from './encoder'; -const wasmBinaryUrl = require('../../../codecs/webp_enc/webp_enc.wasm'); - -export default class WebPEncoder { - private emscriptenModule: Promise; - - constructor() { - this.emscriptenModule = new Promise((resolve) => { - const m = webp_enc({ - // Just to be safe, don’t automatically invoke any wasm functions - noInitialRun: false, - locateFile(url: string): string { - // Redirect the request for the wasm binary to whatever webpack gave us. - if (url.endsWith('.wasm')) { - return wasmBinaryUrl; - } - return url; - }, - onRuntimeInitialized() { - // An Emscripten is a then-able that, for some reason, `then()`s itself, - // causing an infite loop when you wrap it in a real promise. Deleten the `then` - // prop solves this for now. - // See: https://github.com/kripken/emscripten/blob/incoming/src/postamble.js#L129 - // TODO(surma@): File a bug with Emscripten on this. - delete (m as any).then; - resolve(m); - }, - }); - }); - } - - async encode(data: ImageData, options: EncodeOptions): Promise { - const module = await this.emscriptenModule; - - const resultView = module.encode(data.data, data.width, data.height, options); - const result = new Uint8Array(resultView); - module.free_result(); - - return result.buffer as ArrayBuffer; - } -} diff --git a/src/codecs/webp/decoder-meta.ts b/src/codecs/webp/decoder-meta.ts new file mode 100644 index 00000000..8edf0dd7 --- /dev/null +++ b/src/codecs/webp/decoder-meta.ts @@ -0,0 +1,7 @@ +export const name = 'WASM WebP Decoder'; + +const supportedMimeTypes = ['image/webp']; + +export function canHandleMimeType(mimeType: string): boolean { + return supportedMimeTypes.includes(mimeType); +} diff --git a/src/codecs/webp/decoder.ts b/src/codecs/webp/decoder.ts index efe67702..6fa6e9e9 100644 --- a/src/codecs/webp/decoder.ts +++ b/src/codecs/webp/decoder.ts @@ -1,17 +1,20 @@ -import { blobToArrayBuffer } from '../../lib/util'; -import DecoderWorker from './Decoder.worker'; +import webp_dec, { WebPModule } from '../../../codecs/webp_dec/webp_dec'; +import wasmUrl from '../../../codecs/webp_dec/webp_dec.wasm'; +import { initWasmModule } from '../util'; -export const name = 'WASM WebP Decoder'; -export async function decode(blob: Blob): Promise { - const decoder = await new DecoderWorker(); - return decoder.decode(await blobToArrayBuffer(blob)); -} +let emscriptenModule: Promise; -export async function isSupported(): Promise { - return true; -} +export async function decode(data: ArrayBuffer): Promise { + if (!emscriptenModule) emscriptenModule = initWasmModule(webp_dec, wasmUrl); -const supportedMimeTypes = ['image/webp']; -export function canHandleMimeType(mimeType: string): boolean { - return supportedMimeTypes.includes(mimeType); + const module = await emscriptenModule; + const rawImage = module.decode(data); + const result = new ImageData( + new Uint8ClampedArray(rawImage.buffer), + rawImage.width, + rawImage.height, + ); + + module.free_result(); + return result; } diff --git a/src/codecs/webp/encoder-meta.ts b/src/codecs/webp/encoder-meta.ts new file mode 100644 index 00000000..330a2825 --- /dev/null +++ b/src/codecs/webp/encoder-meta.ts @@ -0,0 +1,72 @@ +export enum WebPImageHint { + WEBP_HINT_DEFAULT, // default preset. + WEBP_HINT_PICTURE, // digital picture, like portrait, inner shot + WEBP_HINT_PHOTO, // outdoor photograph, with natural lighting + WEBP_HINT_GRAPH, // Discrete tone image (graph, map-tile etc). +} + +export interface EncodeOptions { + quality: number; + target_size: number; + target_PSNR: number; + method: number; + sns_strength: number; + filter_strength: number; + filter_sharpness: number; + filter_type: number; + partitions: number; + segments: number; + pass: number; + show_compressed: number; + preprocessing: number; + autofilter: number; + partition_limit: number; + alpha_compression: number; + alpha_filtering: number; + alpha_quality: number; + lossless: number; + exact: number; + image_hint: number; + emulate_jpeg_size: number; + thread_level: number; + low_memory: number; + near_lossless: number; + use_delta_palette: number; + use_sharp_yuv: number; +} +export interface EncoderState { type: typeof type; options: EncodeOptions; } + +export const type = 'webp'; +export const label = 'WebP'; +export const mimeType = 'image/webp'; +export const extension = 'webp'; +// These come from struct WebPConfig in encode.h. +export const defaultOptions: EncodeOptions = { + quality: 75, + target_size: 0, + target_PSNR: 0, + method: 4, + sns_strength: 50, + filter_strength: 60, + filter_sharpness: 0, + filter_type: 1, + partitions: 0, + segments: 4, + pass: 1, + show_compressed: 0, + preprocessing: 0, + autofilter: 0, + partition_limit: 0, + alpha_compression: 1, + alpha_filtering: 1, + alpha_quality: 100, + lossless: 0, + exact: 0, + image_hint: 0, + emulate_jpeg_size: 0, + thread_level: 0, + low_memory: 0, + near_lossless: 100, + use_delta_palette: 0, + use_sharp_yuv: 0, +}; diff --git a/src/codecs/webp/encoder.ts b/src/codecs/webp/encoder.ts index ce5c0c4a..b308fab4 100644 --- a/src/codecs/webp/encoder.ts +++ b/src/codecs/webp/encoder.ts @@ -1,80 +1,18 @@ -import EncoderWorker from './Encoder.worker'; +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 { initWasmModule } from '../util'; -export enum WebPImageHint { - WEBP_HINT_DEFAULT, // default preset. - WEBP_HINT_PICTURE, // digital picture, like portrait, inner shot - WEBP_HINT_PHOTO, // outdoor photograph, with natural lighting - WEBP_HINT_GRAPH, // Discrete tone image (graph, map-tile etc). -} - -export interface EncodeOptions { - quality: number; - target_size: number; - target_PSNR: number; - method: number; - sns_strength: number; - filter_strength: number; - filter_sharpness: number; - filter_type: number; - partitions: number; - segments: number; - pass: number; - show_compressed: number; - preprocessing: number; - autofilter: number; - partition_limit: number; - alpha_compression: number; - alpha_filtering: number; - alpha_quality: number; - lossless: number; - exact: number; - image_hint: number; - emulate_jpeg_size: number; - thread_level: number; - low_memory: number; - near_lossless: number; - use_delta_palette: number; - use_sharp_yuv: number; -} -export interface EncoderState { type: typeof type; options: EncodeOptions; } - -export const type = 'webp'; -export const label = 'WebP'; -export const mimeType = 'image/webp'; -export const extension = 'webp'; -// These come from struct WebPConfig in encode.h. -export const defaultOptions: EncodeOptions = { - quality: 75, - target_size: 0, - target_PSNR: 0, - method: 4, - sns_strength: 50, - filter_strength: 60, - filter_sharpness: 0, - filter_type: 1, - partitions: 0, - segments: 4, - pass: 1, - show_compressed: 0, - preprocessing: 0, - autofilter: 0, - partition_limit: 0, - alpha_compression: 1, - alpha_filtering: 1, - alpha_quality: 100, - lossless: 0, - exact: 0, - image_hint: 0, - emulate_jpeg_size: 0, - thread_level: 0, - low_memory: 0, - near_lossless: 100, - use_delta_palette: 0, - use_sharp_yuv: 0, -}; - -export async function encode(data: ImageData, options: EncodeOptions) { - // We need to await this because it's been comlinked. - const encoder = await new EncoderWorker(); - return encoder.encode(data, options); +let emscriptenModule: Promise; + +export async function encode(data: ImageData, options: EncodeOptions): Promise { + if (!emscriptenModule) emscriptenModule = initWasmModule(webp_enc, wasmUrl); + + const module = await emscriptenModule; + const resultView = module.encode(data.data, data.width, data.height, options); + const result = new Uint8Array(resultView); + module.free_result(); + + // wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer. + return result.buffer as ArrayBuffer; } diff --git a/src/codecs/webp/options.tsx b/src/codecs/webp/options.tsx index 81a1a901..0025051d 100644 --- a/src/codecs/webp/options.tsx +++ b/src/codecs/webp/options.tsx @@ -1,7 +1,7 @@ import { h, Component } from 'preact'; import { bind } from '../../lib/initial-util'; import { inputFieldCheckedAsNumber, inputFieldValueAsNumber } from '../../lib/util'; -import { EncodeOptions, WebPImageHint } from './encoder'; +import { EncodeOptions, WebPImageHint } from './encoder-meta'; import * as styles from './styles.scss'; import '../../custom-els/RangeInput'; diff --git a/src/components/Options/index.tsx b/src/components/Options/index.tsx index abe37179..77feeeaf 100644 --- a/src/components/Options/index.tsx +++ b/src/components/Options/index.tsx @@ -12,18 +12,18 @@ import BrowserWebPEncoderOptions from '../../codecs/browser-webp/options'; import QuantizerOptionsComponent from '../../codecs/imagequant/options'; import ResizeOptionsComponent from '../../codecs/resize/options'; -import * as identity from '../../codecs/identity/encoder'; -import * as optiPNG from '../../codecs/optipng/encoder'; -import * as mozJPEG from '../../codecs/mozjpeg/encoder'; -import * as webP from '../../codecs/webp/encoder'; -import * as browserPNG from '../../codecs/browser-png/encoder'; -import * as browserJPEG from '../../codecs/browser-jpeg/encoder'; -import * as browserWebP from '../../codecs/browser-webp/encoder'; -import * as browserGIF from '../../codecs/browser-gif/encoder'; -import * as browserTIFF from '../../codecs/browser-tiff/encoder'; -import * as browserJP2 from '../../codecs/browser-jp2/encoder'; -import * as browserBMP from '../../codecs/browser-bmp/encoder'; -import * as browserPDF from '../../codecs/browser-pdf/encoder'; +import * as identity from '../../codecs/identity/encoder-meta'; +import * as optiPNG from '../../codecs/optipng/encoder-meta'; +import * as mozJPEG from '../../codecs/mozjpeg/encoder-meta'; +import * as webP from '../../codecs/webp/encoder-meta'; +import * as browserPNG from '../../codecs/browser-png/encoder-meta'; +import * as browserJPEG from '../../codecs/browser-jpeg/encoder-meta'; +import * as browserWebP from '../../codecs/browser-webp/encoder-meta'; +import * as browserGIF from '../../codecs/browser-gif/encoder-meta'; +import * as browserTIFF from '../../codecs/browser-tiff/encoder-meta'; +import * as browserJP2 from '../../codecs/browser-jp2/encoder-meta'; +import * as browserBMP from '../../codecs/browser-bmp/encoder-meta'; +import * as browserPDF from '../../codecs/browser-pdf/encoder-meta'; import { EncoderState, EncoderType, @@ -33,8 +33,8 @@ import { EncoderSupportMap, encoderMap, } from '../../codecs/encoders'; -import { QuantizeOptions } from '../../codecs/imagequant/quantizer'; -import { ResizeOptions } from '../../codecs/resize/resize'; +import { QuantizeOptions } from '../../codecs/imagequant/processor-meta'; +import { ResizeOptions } from '../../codecs/resize/processor-meta'; import { PreprocessorState } from '../../codecs/preprocessors'; import FileSize from '../FileSize'; import { DownloadIcon } from '../../lib/icons'; diff --git a/src/components/compress/index.tsx b/src/components/compress/index.tsx index 05177b9f..0568ff41 100644 --- a/src/components/compress/index.tsx +++ b/src/components/compress/index.tsx @@ -6,21 +6,18 @@ import * as style from './style.scss'; import Output from '../Output'; import Options from '../Options'; import ResultCache from './result-cache'; - -import * as quantizer from '../../codecs/imagequant/quantizer'; -import * as optiPNG from '../../codecs/optipng/encoder'; -import * as resizer from '../../codecs/resize/resize'; -import * as mozJPEG from '../../codecs/mozjpeg/encoder'; -import * as webP from '../../codecs/webp/encoder'; -import * as identity from '../../codecs/identity/encoder'; -import * as browserPNG from '../../codecs/browser-png/encoder'; -import * as browserJPEG from '../../codecs/browser-jpeg/encoder'; -import * as browserWebP from '../../codecs/browser-webp/encoder'; -import * as browserGIF from '../../codecs/browser-gif/encoder'; -import * as browserTIFF from '../../codecs/browser-tiff/encoder'; -import * as browserJP2 from '../../codecs/browser-jp2/encoder'; -import * as browserBMP from '../../codecs/browser-bmp/encoder'; -import * as browserPDF from '../../codecs/browser-pdf/encoder'; +import * as identity from '../../codecs/identity/encoder-meta'; +import * as optiPNG from '../../codecs/optipng/encoder-meta'; +import * as mozJPEG from '../../codecs/mozjpeg/encoder-meta'; +import * as webP from '../../codecs/webp/encoder-meta'; +import * as browserPNG from '../../codecs/browser-png/encoder-meta'; +import * as browserJPEG from '../../codecs/browser-jpeg/encoder-meta'; +import * as browserWebP from '../../codecs/browser-webp/encoder-meta'; +import * as browserGIF from '../../codecs/browser-gif/encoder-meta'; +import * as browserTIFF from '../../codecs/browser-tiff/encoder-meta'; +import * as browserJP2 from '../../codecs/browser-jp2/encoder-meta'; +import * as browserBMP from '../../codecs/browser-bmp/encoder-meta'; +import * as browserPDF from '../../codecs/browser-pdf/encoder-meta'; import { EncoderState, EncoderType, @@ -35,6 +32,8 @@ import { import { decodeImage } from '../../codecs/decoders'; import { cleanMerge, cleanSet } from '../../lib/clean-modify'; +import Processor from '../../codecs/processor'; +import { VectorResizeOptions, BitmapResizeOptions } from '../../codecs/resize/processor-meta'; type Orientation = 'horizontal' | 'vertical'; @@ -78,20 +77,21 @@ interface UpdateImageOptions { async function preprocessImage( source: SourceImage, preprocessData: PreprocessorState, + processor: Processor, ): Promise { let result = source.data; if (preprocessData.resize.enabled) { if (preprocessData.resize.method === 'vector' && source.vectorImage) { - result = resizer.vectorResize( + result = processor.vectorResize( source.vectorImage, - preprocessData.resize as resizer.VectorResizeOptions, + preprocessData.resize as VectorResizeOptions, ); } else { - result = resizer.resize(result, preprocessData.resize as resizer.BitmapResizeOptions); + result = processor.resize(result, preprocessData.resize as BitmapResizeOptions); } } if (preprocessData.quantizer.enabled) { - result = await quantizer.quantize(result, preprocessData.quantizer); + result = await processor.imageQuant(result, preprocessData.quantizer); } return result; } @@ -100,20 +100,21 @@ async function compressImage( image: ImageData, encodeData: EncoderState, sourceFilename: string, + processor: Processor, ): Promise { const compressedData = await (() => { switch (encodeData.type) { - case optiPNG.type: return optiPNG.encode(image, encodeData.options); - case mozJPEG.type: return mozJPEG.encode(image, encodeData.options); - case webP.type: return webP.encode(image, encodeData.options); - case browserPNG.type: return browserPNG.encode(image, encodeData.options); - case browserJPEG.type: return browserJPEG.encode(image, encodeData.options); - case browserWebP.type: return browserWebP.encode(image, encodeData.options); - case browserGIF.type: return browserGIF.encode(image, encodeData.options); - case browserTIFF.type: return browserTIFF.encode(image, encodeData.options); - case browserJP2.type: return browserJP2.encode(image, encodeData.options); - case browserBMP.type: return browserBMP.encode(image, encodeData.options); - case browserPDF.type: return browserPDF.encode(image, encodeData.options); + case optiPNG.type: return processor.optiPngEncode(image, encodeData.options); + case mozJPEG.type: return processor.mozjpegEncode(image, encodeData.options); + case webP.type: return processor.webpEncode(image, encodeData.options); + case browserPNG.type: return processor.browserPngEncode(image); + case browserJPEG.type: return processor.browserJpegEncode(image, encodeData.options); + case browserWebP.type: return processor.browserWebpEncode(image, encodeData.options); + case browserGIF.type: return processor.browserGifEncode(image); + case browserTIFF.type: return processor.browserTiffEncode(image); + case browserJP2.type: return processor.browserJp2Encode(image); + case browserBMP.type: return processor.browserBmpEncode(image); + case browserPDF.type: return processor.browserPdfEncode(image); default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`); } })(); @@ -177,7 +178,9 @@ export default class Compress extends Component { orientation: this.widthQuery.matches ? 'horizontal' : 'vertical', }; - readonly encodeCache = new ResultCache(); + private readonly encodeCache = new ResultCache(); + private readonly leftProcessor = new Processor(); + private readonly rightProcessor = new Processor(); constructor(props: Props) { super(props); @@ -251,6 +254,10 @@ export default class Compress extends Component { private async updateFile(file: File | Fileish) { this.setState({ loading: true }); + // Abort any current encode jobs, as they're redundant now. + this.leftProcessor.abortCurrent(); + this.rightProcessor.abortCurrent(); + try { let data: ImageData; let vectorImage: HTMLImageElement | undefined; @@ -262,7 +269,8 @@ export default class Compress extends Component { vectorImage = await processSvg(file); data = drawableToImageData(vectorImage); } else { - data = await decodeImage(file); + // Either processor is good enough here. + data = await decodeImage(file, this.leftProcessor); } let newState: State = { @@ -293,6 +301,7 @@ export default class Compress extends Component { this.setState(newState); } catch (err) { + if (err.name === 'AbortError') return; console.error(err); this.props.onError('Invalid image'); this.setState({ loading: false }); @@ -320,6 +329,12 @@ export default class Compress extends Component { let preprocessed: ImageData | undefined; let data: ImageData | undefined; const cacheResult = this.encodeCache.match(source, image.preprocessorState, image.encoderState); + const processor = (index === 0) ? this.leftProcessor : this.rightProcessor; + + // Abort anything the processor is currently doing. + // Although the processor will abandon current tasks when a new one is called, + // we might not call another task here. Eg, we might get the result from the cache. + processor.abortCurrent(); if (cacheResult) { ({ file, preprocessed, data } = cacheResult); @@ -331,10 +346,10 @@ export default class Compress extends Component { } else { preprocessed = (skipPreprocessing && image.preprocessed) ? image.preprocessed - : await preprocessImage(source, image.preprocessorState); + : await preprocessImage(source, image.preprocessorState, processor); - file = await compressImage(preprocessed, image.encoderState, source.file.name); - data = await decodeImage(file); + file = await compressImage(preprocessed, image.encoderState, source.file.name, processor); + data = await decodeImage(file, processor); this.encodeCache.add({ source, @@ -346,6 +361,7 @@ export default class Compress extends Component { }); } } catch (err) { + if (err.name === 'AbortError') return; this.props.onError(`Processing error (type=${image.encoderState.type}): ${err}`); throw err; } diff --git a/src/components/compress/result-cache.ts b/src/components/compress/result-cache.ts index c01f290d..1f06e6c6 100644 --- a/src/components/compress/result-cache.ts +++ b/src/components/compress/result-cache.ts @@ -4,7 +4,7 @@ import { shallowEqual } from '../../lib/util'; import { SourceImage } from '.'; import { PreprocessorState } from '../../codecs/preprocessors'; -import * as identity from '../../codecs/identity/encoder'; +import * as identity from '../../codecs/identity/encoder-meta'; interface CacheResult { preprocessed: ImageData; diff --git a/src/missing-types.d.ts b/src/missing-types.d.ts index b553e665..cbc5021a 100644 --- a/src/missing-types.d.ts +++ b/src/missing-types.d.ts @@ -22,3 +22,8 @@ declare module '*.svg' { const content: string; export default content; } + +declare module '*.wasm' { + const content: string; + export default content; +} diff --git a/webpack.config.js b/webpack.config.js index 0e2017bb..dbbf146d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,6 +12,7 @@ const ReplacePlugin = require('webpack-plugin-replace'); const CopyPlugin = require('copy-webpack-plugin'); const WatchTimestampsPlugin = require('./config/watch-timestamps-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const WorkerPlugin = require('worker-plugin'); function readJson (filename) { return JSON.parse(fs.readFileSync(filename)); @@ -130,10 +131,6 @@ module.exports = function (_, env) { } ] }, - { - test: /\.worker.[tj]sx?$/, - loader: 'comlink-loader' - }, { test: /\.tsx?$/, exclude: nodeModules, @@ -183,6 +180,8 @@ module.exports = function (_, env) { beforeEmit: true }), + new WorkerPlugin(), + // Automatically split code into async chunks. // See: https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366 isProd && new webpack.optimize.SplitChunksPlugin({}),