mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-12 08:47:31 +00:00
Two workers & worker termination (#198)
* Refactoring codecs * Plugging in new processor * Fixing decorator * MozJPEG free issue * Better worker aborting, and terminate workers that aren't used for 10 seconds * Better comment * Ooops, half-typed comment * Uncommenting problematic line * Surma fixed it! * Abstracting WASM initialisation * Better comment * Don't need this. * Adding ticket * noInitalRun * Reverting MozJPEG issue demo * Making a const for worker timeout * Inline docs * Bail early rather than nesting * Addressing nits
This commit is contained in:
2
codecs/mozjpeg_enc/mozjpeg_enc.d.ts
vendored
2
codecs/mozjpeg_enc/mozjpeg_enc.d.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
import { EncodeOptions } from '../../src/codecs/mozjpeg/encoder';
|
import { EncodeOptions } from '../../src/codecs/mozjpeg/encoder-meta';
|
||||||
|
|
||||||
interface MozJPEGModule extends EmscriptenWasm.Module {
|
interface MozJPEGModule extends EmscriptenWasm.Module {
|
||||||
encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array;
|
encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array;
|
||||||
|
|||||||
2
codecs/webp_enc/webp_enc.d.ts
vendored
2
codecs/webp_enc/webp_enc.d.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
import { EncodeOptions } from '../../src/codecs/webp/encoder';
|
import { EncodeOptions } from '../../src/codecs/webp/encoder-meta';
|
||||||
|
|
||||||
interface WebPModule extends EmscriptenWasm.Module {
|
interface WebPModule extends EmscriptenWasm.Module {
|
||||||
encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array;
|
encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array;
|
||||||
|
|||||||
70
package-lock.json
generated
70
package-lock.json
generated
@@ -306,6 +306,7 @@
|
|||||||
"version": "6.5.3",
|
"version": "6.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.3.tgz",
|
||||||
"integrity": "sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg==",
|
"integrity": "sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fast-deep-equal": "^2.0.1",
|
"fast-deep-equal": "^2.0.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -322,7 +323,8 @@
|
|||||||
"ajv-keywords": {
|
"ajv-keywords": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz",
|
"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": {
|
"alphanum-sort": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@@ -1656,7 +1658,8 @@
|
|||||||
"big.js": {
|
"big.js": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
|
"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": {
|
"binary-extensions": {
|
||||||
"version": "1.12.0",
|
"version": "1.12.0",
|
||||||
@@ -2224,7 +2227,8 @@
|
|||||||
"classnames": {
|
"classnames": {
|
||||||
"version": "2.2.6",
|
"version": "2.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
"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": {
|
"clean-css": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
@@ -2456,22 +2460,8 @@
|
|||||||
"comlink": {
|
"comlink": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/comlink/-/comlink-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/comlink/-/comlink-3.0.3.tgz",
|
||||||
"integrity": "sha512-toiZad0dmZIfqkSh4XyD40mRg6/X+8yNvtWCq+f79aIKsJGTf3hY8Ikr4wGx4494h1q9oNHznWMLdorNWsr6dQ=="
|
"integrity": "sha512-toiZad0dmZIfqkSh4XyD40mRg6/X+8yNvtWCq+f79aIKsJGTf3hY8Ikr4wGx4494h1q9oNHznWMLdorNWsr6dQ==",
|
||||||
},
|
"dev": true
|
||||||
"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=="
|
|
||||||
},
|
},
|
||||||
"commander": {
|
"commander": {
|
||||||
"version": "2.17.1",
|
"version": "2.17.1",
|
||||||
@@ -3366,7 +3356,8 @@
|
|||||||
"emojis-list": {
|
"emojis-list": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
|
||||||
"integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k="
|
"integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"encodeurl": {
|
"encodeurl": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@@ -3858,7 +3849,8 @@
|
|||||||
"fast-deep-equal": {
|
"fast-deep-equal": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
"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": {
|
"fast-glob": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
@@ -3877,7 +3869,8 @@
|
|||||||
"fast-json-stable-stringify": {
|
"fast-json-stable-stringify": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
|
"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": {
|
"fastparse": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -6377,7 +6370,8 @@
|
|||||||
"json-schema-traverse": {
|
"json-schema-traverse": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"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": {
|
"json-stringify-safe": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
@@ -6394,7 +6388,8 @@
|
|||||||
"json5": {
|
"json5": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
|
||||||
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE="
|
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"jsonify": {
|
"jsonify": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
@@ -6457,7 +6452,8 @@
|
|||||||
"linkstate": {
|
"linkstate": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/linkstate/-/linkstate-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/linkstate/-/linkstate-1.1.1.tgz",
|
||||||
"integrity": "sha512-5SICdxQG9FpWk44wSEoM2WOCUNuYfClp10t51XAIV5E7vUILF/dTYxf0vJw6bW2dUd2wcQon+LkNtRijpNLrig=="
|
"integrity": "sha512-5SICdxQG9FpWk44wSEoM2WOCUNuYfClp10t51XAIV5E7vUILF/dTYxf0vJw6bW2dUd2wcQon+LkNtRijpNLrig==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"listr": {
|
"listr": {
|
||||||
"version": "0.14.2",
|
"version": "0.14.2",
|
||||||
@@ -6625,6 +6621,7 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz",
|
||||||
"integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=",
|
"integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"big.js": "^3.1.3",
|
"big.js": "^3.1.3",
|
||||||
"emojis-list": "^2.0.0",
|
"emojis-list": "^2.0.0",
|
||||||
@@ -8697,7 +8694,8 @@
|
|||||||
"preact": {
|
"preact": {
|
||||||
"version": "8.3.1",
|
"version": "8.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-8.3.1.tgz",
|
"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": {
|
"prepend-http": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
@@ -8720,7 +8718,8 @@
|
|||||||
"pretty-bytes": {
|
"pretty-bytes": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.1.0.tgz",
|
"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": {
|
"pretty-error": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
@@ -8841,7 +8840,8 @@
|
|||||||
"punycode": {
|
"punycode": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
"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": {
|
"q": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
@@ -9512,6 +9512,7 @@
|
|||||||
"version": "0.4.7",
|
"version": "0.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz",
|
||||||
"integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==",
|
"integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ajv": "^6.1.0",
|
"ajv": "^6.1.0",
|
||||||
"ajv-keywords": "^3.1.0"
|
"ajv-keywords": "^3.1.0"
|
||||||
@@ -11008,6 +11009,7 @@
|
|||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
@@ -12109,13 +12111,13 @@
|
|||||||
"errno": "~0.1.7"
|
"errno": "~0.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"worker-loader": {
|
"worker-plugin": {
|
||||||
"version": "2.0.0",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/worker-plugin/-/worker-plugin-1.1.1.tgz",
|
||||||
"integrity": "sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==",
|
"integrity": "sha512-s5XtToCv/eZdxZHB1t2Ggdl0F6jw+4qm5s/C3lxIp6z4V2aQRMte22x+v2Y1pbJzwTXE+jqGoa+wWn0oqovYyg==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"loader-utils": "^1.0.0",
|
"loader-utils": "^1.1.0"
|
||||||
"schema-utils": "^0.4.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"wrap-ansi": {
|
"wrap-ansi": {
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -59,14 +59,12 @@
|
|||||||
"webpack-bundle-analyzer": "^2.13.1",
|
"webpack-bundle-analyzer": "^2.13.1",
|
||||||
"webpack-cli": "^2.1.5",
|
"webpack-cli": "^2.1.5",
|
||||||
"webpack-dev-server": "^3.1.5",
|
"webpack-dev-server": "^3.1.5",
|
||||||
"webpack-plugin-replace": "^1.1.1"
|
"webpack-plugin-replace": "^1.1.1",
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"comlink": "^3.0.3",
|
"comlink": "^3.0.3",
|
||||||
"comlink-loader": "^1.0.0",
|
|
||||||
"preact": "^8.3.1",
|
|
||||||
"linkstate": "^1.1.1",
|
"linkstate": "^1.1.1",
|
||||||
"pretty-bytes": "^5.1.0"
|
"preact": "^8.3.1",
|
||||||
|
"pretty-bytes": "^5.1.0",
|
||||||
|
"worker-plugin": "^1.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/codecs/browser-bmp/encoder-meta.ts
Normal file
11
src/codecs/browser-bmp/encoder-meta.ts
Normal file
@@ -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);
|
||||||
@@ -1,16 +1,6 @@
|
|||||||
|
import { mimeType } from './encoder-meta';
|
||||||
import { canvasEncode } from '../../lib/util';
|
import { canvasEncode } from '../../lib/util';
|
||||||
import { canvasEncodeTest } from '../generic/util';
|
|
||||||
|
|
||||||
export interface EncodeOptions { }
|
export function encode(data: ImageData) {
|
||||||
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) {
|
|
||||||
return canvasEncode(data, mimeType);
|
return canvasEncode(data, mimeType);
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/codecs/browser-gif/encoder-meta.ts
Normal file
11
src/codecs/browser-gif/encoder-meta.ts
Normal file
@@ -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);
|
||||||
@@ -1,16 +1,6 @@
|
|||||||
|
import { mimeType } from './encoder-meta';
|
||||||
import { canvasEncode } from '../../lib/util';
|
import { canvasEncode } from '../../lib/util';
|
||||||
import { canvasEncodeTest } from '../generic/util';
|
|
||||||
|
|
||||||
export interface EncodeOptions {}
|
export function encode(data: ImageData) {
|
||||||
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) {
|
|
||||||
return canvasEncode(data, mimeType);
|
return canvasEncode(data, mimeType);
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/codecs/browser-jp2/encoder-meta.ts
Normal file
11
src/codecs/browser-jp2/encoder-meta.ts
Normal file
@@ -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);
|
||||||
@@ -1,16 +1,6 @@
|
|||||||
|
import { mimeType } from './encoder-meta';
|
||||||
import { canvasEncode } from '../../lib/util';
|
import { canvasEncode } from '../../lib/util';
|
||||||
import { canvasEncodeTest } from '../generic/util';
|
|
||||||
|
|
||||||
export interface EncodeOptions { }
|
export function encode(data: ImageData) {
|
||||||
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) {
|
|
||||||
return canvasEncode(data, mimeType);
|
return canvasEncode(data, mimeType);
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/codecs/browser-jpeg/encoder-meta.ts
Normal file
8
src/codecs/browser-jpeg/encoder-meta.ts
Normal file
@@ -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 };
|
||||||
@@ -1,14 +1,6 @@
|
|||||||
|
import { EncodeOptions, mimeType } from './encoder-meta';
|
||||||
import { canvasEncode } from '../../lib/util';
|
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) {
|
export function encode(data: ImageData, { quality }: EncodeOptions) {
|
||||||
return canvasEncode(data, mimeType, quality);
|
return canvasEncode(data, mimeType, quality);
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/codecs/browser-pdf/encoder-meta.ts
Normal file
11
src/codecs/browser-pdf/encoder-meta.ts
Normal file
@@ -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);
|
||||||
@@ -1,16 +1,6 @@
|
|||||||
|
import { mimeType } from './encoder-meta';
|
||||||
import { canvasEncode } from '../../lib/util';
|
import { canvasEncode } from '../../lib/util';
|
||||||
import { canvasEncodeTest } from '../generic/util';
|
|
||||||
|
|
||||||
export interface EncodeOptions { }
|
export function encode(data: ImageData) {
|
||||||
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) {
|
|
||||||
return canvasEncode(data, mimeType);
|
return canvasEncode(data, mimeType);
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/codecs/browser-png/encoder-meta.ts
Normal file
8
src/codecs/browser-png/encoder-meta.ts
Normal file
@@ -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 = {};
|
||||||
@@ -1,14 +1,6 @@
|
|||||||
|
import { mimeType } from './encoder-meta';
|
||||||
import { canvasEncode } from '../../lib/util';
|
import { canvasEncode } from '../../lib/util';
|
||||||
|
|
||||||
export interface EncodeOptions {}
|
export function encode(data: ImageData) {
|
||||||
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) {
|
|
||||||
return canvasEncode(data, mimeType);
|
return canvasEncode(data, mimeType);
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/codecs/browser-tiff/encoder-meta.ts
Normal file
11
src/codecs/browser-tiff/encoder-meta.ts
Normal file
@@ -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);
|
||||||
@@ -1,16 +1,6 @@
|
|||||||
|
import { mimeType } from './encoder-meta';
|
||||||
import { canvasEncode } from '../../lib/util';
|
import { canvasEncode } from '../../lib/util';
|
||||||
import { canvasEncodeTest } from '../generic/util';
|
|
||||||
|
|
||||||
export interface EncodeOptions { }
|
export function encode(data: ImageData) {
|
||||||
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) {
|
|
||||||
return canvasEncode(data, mimeType);
|
return canvasEncode(data, mimeType);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { canDecodeImage, nativeDecode } from '../../lib/util';
|
|
||||||
|
|
||||||
export const name = 'Browser WebP Decoder';
|
|
||||||
export async function decode(blob: Blob): Promise<ImageData> {
|
|
||||||
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<boolean> {
|
|
||||||
return canDecodeImage(webpFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
const supportedMimeTypes = ['image/webp'];
|
|
||||||
export function canHandleMimeType(mimeType: string): boolean {
|
|
||||||
return supportedMimeTypes.includes(mimeType);
|
|
||||||
}
|
|
||||||
11
src/codecs/browser-webp/encoder-meta.ts
Normal file
11
src/codecs/browser-webp/encoder-meta.ts
Normal file
@@ -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);
|
||||||
@@ -1,15 +1,5 @@
|
|||||||
|
import { EncodeOptions, mimeType } from './encoder-meta';
|
||||||
import { canvasEncode } from '../../lib/util';
|
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) {
|
export function encode(data: ImageData, { quality }: EncodeOptions) {
|
||||||
return canvasEncode(data, mimeType, quality);
|
return canvasEncode(data, mimeType, quality);
|
||||||
|
|||||||
@@ -1,47 +1,21 @@
|
|||||||
import * as wasmWebp from './webp/decoder';
|
import { nativeDecode, sniffMimeType, canDecodeImage } from '../lib/util';
|
||||||
import * as browserWebp from './webp/decoder';
|
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 {
|
export async function decodeImage(blob: Blob, processor: Processor): Promise<ImageData> {
|
||||||
name: string;
|
|
||||||
decode(blob: Blob): Promise<ImageData>;
|
|
||||||
isSupported(): Promise<boolean>;
|
|
||||||
canHandleMimeType(mimeType: string): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We load all decoders and filter out the unsupported ones.
|
|
||||||
export const decodersPromise: Promise<Decoder[]> = 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<Decoder[]>;
|
|
||||||
|
|
||||||
async function findDecodersByMimeType(mimeType: string): Promise<Decoder[]> {
|
|
||||||
const decoders = await decodersPromise;
|
|
||||||
return decoders.filter(decoder => decoder.canHandleMimeType(mimeType));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function decodeImage(blob: Blob): Promise<ImageData> {
|
|
||||||
const mimeType = await sniffMimeType(blob);
|
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);
|
|
||||||
}
|
|
||||||
for (const decoder of decoders) {
|
|
||||||
try {
|
try {
|
||||||
return await decoder.decode(blob);
|
if (mimeType === 'image/webp' && !(await nativeWebPSupported)) {
|
||||||
} catch { }
|
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");
|
||||||
}
|
}
|
||||||
throw new Error('No decoder could decode image');
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import * as identity from './identity/encoder';
|
import * as identity from './identity/encoder-meta';
|
||||||
import * as optiPNG from './optipng/encoder';
|
import * as optiPNG from './optipng/encoder-meta';
|
||||||
import * as mozJPEG from './mozjpeg/encoder';
|
import * as mozJPEG from './mozjpeg/encoder-meta';
|
||||||
import * as webP from './webp/encoder';
|
import * as webP from './webp/encoder-meta';
|
||||||
import * as browserPNG from './browser-png/encoder';
|
import * as browserPNG from './browser-png/encoder-meta';
|
||||||
import * as browserJPEG from './browser-jpeg/encoder';
|
import * as browserJPEG from './browser-jpeg/encoder-meta';
|
||||||
import * as browserWebP from './browser-webp/encoder';
|
import * as browserWebP from './browser-webp/encoder-meta';
|
||||||
import * as browserGIF from './browser-gif/encoder';
|
import * as browserGIF from './browser-gif/encoder-meta';
|
||||||
import * as browserTIFF from './browser-tiff/encoder';
|
import * as browserTIFF from './browser-tiff/encoder-meta';
|
||||||
import * as browserJP2 from './browser-jp2/encoder';
|
import * as browserJP2 from './browser-jp2/encoder-meta';
|
||||||
import * as browserBMP from './browser-bmp/encoder';
|
import * as browserBMP from './browser-bmp/encoder-meta';
|
||||||
import * as browserPDF from './browser-pdf/encoder';
|
import * as browserPDF from './browser-pdf/encoder-meta';
|
||||||
|
|
||||||
export interface EncoderSupportMap {
|
export interface EncoderSupportMap {
|
||||||
[key: string]: boolean;
|
[key: string]: boolean;
|
||||||
|
|||||||
@@ -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<QuantizerModule>;
|
|
||||||
|
|
||||||
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<ImageData> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
import { bind } from '../../lib/initial-util';
|
import { bind } from '../../lib/initial-util';
|
||||||
import { inputFieldValueAsNumber, konami } from '../../lib/util';
|
import { inputFieldValueAsNumber, konami } from '../../lib/util';
|
||||||
import { QuantizeOptions } from './quantizer';
|
import { QuantizeOptions } from './processor-meta';
|
||||||
|
|
||||||
const konamiPromise = konami();
|
const konamiPromise = konami();
|
||||||
|
|
||||||
|
|||||||
11
src/codecs/imagequant/processor-meta.ts
Normal file
11
src/codecs/imagequant/processor-meta.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface QuantizeOptions {
|
||||||
|
zx: number;
|
||||||
|
maxNumColors: number;
|
||||||
|
dither: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultOptions: QuantizeOptions = {
|
||||||
|
zx: 0,
|
||||||
|
maxNumColors: 256,
|
||||||
|
dither: 1.0,
|
||||||
|
};
|
||||||
21
src/codecs/imagequant/processor.ts
Normal file
21
src/codecs/imagequant/processor.ts
Normal file
@@ -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<QuantizerModule>;
|
||||||
|
|
||||||
|
export async function process(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import QuantizerWorker from './Quantizer.worker';
|
|
||||||
|
|
||||||
export async function quantize(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
@@ -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<MozJPEGModule>;
|
|
||||||
|
|
||||||
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<ArrayBuffer> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
41
src/codecs/mozjpeg/encoder-meta.ts
Normal file
41
src/codecs/mozjpeg/encoder-meta.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
@@ -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 {
|
let emscriptenModule: Promise<MozJPEGModule>;
|
||||||
GRAYSCALE = 1,
|
|
||||||
RGB,
|
export async function encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> {
|
||||||
YCbCr,
|
if (!emscriptenModule) emscriptenModule = initWasmModule(mozjpeg_enc, wasmUrl);
|
||||||
}
|
|
||||||
|
const module = await emscriptenModule;
|
||||||
export interface EncodeOptions {
|
const resultView = module.encode(data.data, data.width, data.height, options);
|
||||||
quality: number;
|
const result = new Uint8Array(resultView);
|
||||||
baseline: boolean;
|
module.free_result();
|
||||||
arithmetic: boolean;
|
|
||||||
progressive: boolean;
|
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||||
optimize_coding: boolean;
|
return result.buffer as ArrayBuffer;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
import { bind } from '../../lib/initial-util';
|
import { bind } from '../../lib/initial-util';
|
||||||
import { inputFieldChecked, inputFieldValueAsNumber } from '../../lib/util';
|
import { inputFieldChecked, inputFieldValueAsNumber } from '../../lib/util';
|
||||||
import { EncodeOptions, MozJpegColorSpace } from './encoder';
|
import { EncodeOptions, MozJpegColorSpace } from './encoder-meta';
|
||||||
import '../../custom-els/RangeInput';
|
import '../../custom-els/RangeInput';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -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<OptiPngModule>;
|
|
||||||
|
|
||||||
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<ArrayBuffer> {
|
|
||||||
const m = await this.emscriptenModule;
|
|
||||||
const result = m.compress(data, opts);
|
|
||||||
const copy = new Uint8Array(result).buffer as ArrayBuffer;
|
|
||||||
m.free_result();
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
src/codecs/optipng/encoder-meta.ts
Normal file
13
src/codecs/optipng/encoder-meta.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
@@ -1,23 +1,18 @@
|
|||||||
import { canvasEncode, blobToArrayBuffer } from '../../lib/util';
|
import optipng, { OptiPngModule } from '../../../codecs/optipng/optipng';
|
||||||
import EncodeWorker from './Encoder.worker';
|
import wasmUrl from '../../../codecs/optipng/optipng.wasm';
|
||||||
|
import { EncodeOptions } from './encoder-meta';
|
||||||
|
import { initWasmModule } from '../util';
|
||||||
|
|
||||||
export interface EncodeOptions {
|
let emscriptenModule: Promise<OptiPngModule>;
|
||||||
level: number;
|
|
||||||
}
|
export async function compress(data: BufferSource, options: EncodeOptions): Promise<ArrayBuffer> {
|
||||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
if (!emscriptenModule) emscriptenModule = initWasmModule(optipng, wasmUrl);
|
||||||
|
|
||||||
export const type = 'png';
|
const module = await emscriptenModule;
|
||||||
export const label = 'OptiPNG';
|
const resultView = module.compress(data, options);
|
||||||
export const mimeType = 'image/png';
|
const result = new Uint8Array(resultView);
|
||||||
export const extension = 'png';
|
module.free_result();
|
||||||
|
|
||||||
export const defaultOptions: EncodeOptions = {
|
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||||
level: 2,
|
return result.buffer as ArrayBuffer;
|
||||||
};
|
|
||||||
|
|
||||||
export async function encode(data: ImageData, opts: EncodeOptions): Promise<ArrayBuffer> {
|
|
||||||
const pngBlob = await canvasEncode(data, mimeType);
|
|
||||||
const pngBuffer = await blobToArrayBuffer(pngBlob);
|
|
||||||
const encodeWorker = await new EncodeWorker();
|
|
||||||
return encodeWorker.compress(pngBuffer, opts);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
import { bind } from '../../lib/initial-util';
|
import { bind } from '../../lib/initial-util';
|
||||||
import { inputFieldValueAsNumber } from '../../lib/util';
|
import { inputFieldValueAsNumber } from '../../lib/util';
|
||||||
import { EncodeOptions } from './encoder';
|
import { EncodeOptions } from './encoder-meta';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
options: EncodeOptions;
|
options: EncodeOptions;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { QuantizeOptions, defaultOptions as quantizerDefaultOptions } from './imagequant/quantizer';
|
import {
|
||||||
import { ResizeOptions, defaultOptions as resizeDefaultOptions } from './resize/resize';
|
QuantizeOptions, defaultOptions as quantizerDefaultOptions,
|
||||||
|
} from './imagequant/processor-meta';
|
||||||
|
import { ResizeOptions, defaultOptions as resizeDefaultOptions } from './resize/processor-meta';
|
||||||
|
|
||||||
interface Enableable {
|
interface Enableable {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|||||||
41
src/codecs/processor-worker.ts
Normal file
41
src/codecs/processor-worker.ts
Normal file
@@ -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<ArrayBuffer> {
|
||||||
|
const { encode } = await import('./mozjpeg/encoder');
|
||||||
|
return encode(data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function quantize(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
|
||||||
|
const { process } = await import('./imagequant/processor');
|
||||||
|
return process(data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function optiPngEncode(
|
||||||
|
data: BufferSource, options: OptiPNGEncoderOptions,
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
const { compress } = await import('./optipng/encoder');
|
||||||
|
return compress(data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function webpEncode(
|
||||||
|
data: ImageData, options: WebPEncoderOptions,
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
const { encode } = await import('./webp/encoder');
|
||||||
|
return encode(data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function webpDecode(data: ArrayBuffer): Promise<ImageData> {
|
||||||
|
const { decode } = await import('./webp/decoder');
|
||||||
|
return decode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exports = { mozjpegEncode, quantize, optiPngEncode, webpEncode, webpDecode };
|
||||||
|
export type ProcessorWorkerApi = typeof exports;
|
||||||
|
|
||||||
|
expose(exports, self);
|
||||||
205
src/codecs/processor.ts
Normal file
205
src/codecs/processor.ts
Normal file
@@ -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<ImageData> {
|
||||||
|
return this._workerApi!.quantize(data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Processor._processingJob({ needsWorker: true })
|
||||||
|
mozjpegEncode(
|
||||||
|
data: ImageData, opts: MozJPEGEncoderOptions,
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
return this._workerApi!.mozjpegEncode(data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Processor._processingJob({ needsWorker: true })
|
||||||
|
async optiPngEncode(
|
||||||
|
data: ImageData, opts: OptiPNGEncoderOptions,
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
// 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<ArrayBuffer> {
|
||||||
|
return this._workerApi!.webpEncode(data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Processor._processingJob({ needsWorker: true })
|
||||||
|
async webpDecode(blob: Blob): Promise<ImageData> {
|
||||||
|
const data = await blobToArrayBuffer(blob);
|
||||||
|
return this._workerApi!.webpDecode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not-worker jobs:
|
||||||
|
|
||||||
|
@Processor._processingJob()
|
||||||
|
browserBmpEncode(data: ImageData): Promise<Blob> {
|
||||||
|
return browserBMP.encode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Processor._processingJob()
|
||||||
|
browserPngEncode(data: ImageData): Promise<Blob> {
|
||||||
|
return browserPNG.encode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Processor._processingJob()
|
||||||
|
browserJpegEncode(data: ImageData, opts: BrowserJPEGOptions): Promise<Blob> {
|
||||||
|
return browserJPEG.encode(data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Processor._processingJob()
|
||||||
|
browserWebpEncode(data: ImageData, opts: BrowserWebpEncodeOptions): Promise<Blob> {
|
||||||
|
return browserWebP.encode(data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Processor._processingJob()
|
||||||
|
browserGifEncode(data: ImageData): Promise<Blob> {
|
||||||
|
return browserGIF.encode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Processor._processingJob()
|
||||||
|
browserTiffEncode(data: ImageData): Promise<Blob> {
|
||||||
|
return browserTIFF.encode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Processor._processingJob()
|
||||||
|
browserJp2Encode(data: ImageData): Promise<Blob> {
|
||||||
|
return browserJP2.encode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Processor._processingJob()
|
||||||
|
browserPdfEncode(data: ImageData): Promise<Blob> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { h, Component } from 'preact';
|
|||||||
import linkState from 'linkstate';
|
import linkState from 'linkstate';
|
||||||
import { bind } from '../../lib/initial-util';
|
import { bind } from '../../lib/initial-util';
|
||||||
import { inputFieldValueAsNumber } from '../../lib/util';
|
import { inputFieldValueAsNumber } from '../../lib/util';
|
||||||
import { ResizeOptions } from './resize';
|
import { ResizeOptions } from './processor-meta';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isVector: Boolean;
|
isVector: Boolean;
|
||||||
|
|||||||
26
src/codecs/resize/processor-meta.ts
Normal file
26
src/codecs/resize/processor-meta.ts
Normal file
@@ -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',
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { nativeResize, NativeResizeMethod, drawableToImageData } from '../../lib/util';
|
import { nativeResize, NativeResizeMethod, drawableToImageData } from '../../lib/util';
|
||||||
|
import { BitmapResizeOptions, VectorResizeOptions } from './processor-meta';
|
||||||
|
|
||||||
function getCoverOffsets(sw: number, sh: number, dw: number, dh: number) {
|
function getCoverOffsets(sw: number, sh: number, dw: number, dh: number) {
|
||||||
const currentAspect = sw / sh;
|
const currentAspect = sw / sh;
|
||||||
@@ -46,30 +47,3 @@ export function vectorResize(data: HTMLImageElement, opts: VectorResizeOptions):
|
|||||||
width: opts.width, height: opts.height,
|
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',
|
|
||||||
};
|
|
||||||
27
src/codecs/util.ts
Normal file
27
src/codecs/util.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
type ModuleFactory<M extends EmscriptenWasm.Module> = (
|
||||||
|
opts: EmscriptenWasm.ModuleOpts,
|
||||||
|
) => M;
|
||||||
|
|
||||||
|
export function initWasmModule<T extends EmscriptenWasm.Module>(
|
||||||
|
moduleFactory: ModuleFactory<T>,
|
||||||
|
wasmUrl: string,
|
||||||
|
): Promise<T> {
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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<WebPModule>;
|
|
||||||
|
|
||||||
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<ImageData> {
|
|
||||||
const m = await this.emscriptenModule;
|
|
||||||
const rawImage = m.decode(data);
|
|
||||||
m.free_result();
|
|
||||||
|
|
||||||
return new ImageData(
|
|
||||||
new Uint8ClampedArray(rawImage.buffer),
|
|
||||||
rawImage.width,
|
|
||||||
rawImage.height,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<WebPModule>;
|
|
||||||
|
|
||||||
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<ArrayBuffer> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
src/codecs/webp/decoder-meta.ts
Normal file
7
src/codecs/webp/decoder-meta.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const name = 'WASM WebP Decoder';
|
||||||
|
|
||||||
|
const supportedMimeTypes = ['image/webp'];
|
||||||
|
|
||||||
|
export function canHandleMimeType(mimeType: string): boolean {
|
||||||
|
return supportedMimeTypes.includes(mimeType);
|
||||||
|
}
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
import { blobToArrayBuffer } from '../../lib/util';
|
import webp_dec, { WebPModule } from '../../../codecs/webp_dec/webp_dec';
|
||||||
import DecoderWorker from './Decoder.worker';
|
import wasmUrl from '../../../codecs/webp_dec/webp_dec.wasm';
|
||||||
|
import { initWasmModule } from '../util';
|
||||||
|
|
||||||
export const name = 'WASM WebP Decoder';
|
let emscriptenModule: Promise<WebPModule>;
|
||||||
export async function decode(blob: Blob): Promise<ImageData> {
|
|
||||||
const decoder = await new DecoderWorker();
|
|
||||||
return decoder.decode(await blobToArrayBuffer(blob));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function isSupported(): Promise<boolean> {
|
export async function decode(data: ArrayBuffer): Promise<ImageData> {
|
||||||
return true;
|
if (!emscriptenModule) emscriptenModule = initWasmModule(webp_dec, wasmUrl);
|
||||||
}
|
|
||||||
|
|
||||||
const supportedMimeTypes = ['image/webp'];
|
const module = await emscriptenModule;
|
||||||
export function canHandleMimeType(mimeType: string): boolean {
|
const rawImage = module.decode(data);
|
||||||
return supportedMimeTypes.includes(mimeType);
|
const result = new ImageData(
|
||||||
|
new Uint8ClampedArray(rawImage.buffer),
|
||||||
|
rawImage.width,
|
||||||
|
rawImage.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
module.free_result();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
72
src/codecs/webp/encoder-meta.ts
Normal file
72
src/codecs/webp/encoder-meta.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
@@ -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 {
|
let emscriptenModule: Promise<WebPModule>;
|
||||||
WEBP_HINT_DEFAULT, // default preset.
|
|
||||||
WEBP_HINT_PICTURE, // digital picture, like portrait, inner shot
|
export async function encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> {
|
||||||
WEBP_HINT_PHOTO, // outdoor photograph, with natural lighting
|
if (!emscriptenModule) emscriptenModule = initWasmModule(webp_enc, wasmUrl);
|
||||||
WEBP_HINT_GRAPH, // Discrete tone image (graph, map-tile etc).
|
|
||||||
}
|
const module = await emscriptenModule;
|
||||||
|
const resultView = module.encode(data.data, data.width, data.height, options);
|
||||||
export interface EncodeOptions {
|
const result = new Uint8Array(resultView);
|
||||||
quality: number;
|
module.free_result();
|
||||||
target_size: number;
|
|
||||||
target_PSNR: number;
|
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||||
method: number;
|
return result.buffer as ArrayBuffer;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
import { bind } from '../../lib/initial-util';
|
import { bind } from '../../lib/initial-util';
|
||||||
import { inputFieldCheckedAsNumber, inputFieldValueAsNumber } from '../../lib/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 * as styles from './styles.scss';
|
||||||
import '../../custom-els/RangeInput';
|
import '../../custom-els/RangeInput';
|
||||||
|
|
||||||
|
|||||||
@@ -12,18 +12,18 @@ import BrowserWebPEncoderOptions from '../../codecs/browser-webp/options';
|
|||||||
import QuantizerOptionsComponent from '../../codecs/imagequant/options';
|
import QuantizerOptionsComponent from '../../codecs/imagequant/options';
|
||||||
import ResizeOptionsComponent from '../../codecs/resize/options';
|
import ResizeOptionsComponent from '../../codecs/resize/options';
|
||||||
|
|
||||||
import * as identity from '../../codecs/identity/encoder';
|
import * as identity from '../../codecs/identity/encoder-meta';
|
||||||
import * as optiPNG from '../../codecs/optipng/encoder';
|
import * as optiPNG from '../../codecs/optipng/encoder-meta';
|
||||||
import * as mozJPEG from '../../codecs/mozjpeg/encoder';
|
import * as mozJPEG from '../../codecs/mozjpeg/encoder-meta';
|
||||||
import * as webP from '../../codecs/webp/encoder';
|
import * as webP from '../../codecs/webp/encoder-meta';
|
||||||
import * as browserPNG from '../../codecs/browser-png/encoder';
|
import * as browserPNG from '../../codecs/browser-png/encoder-meta';
|
||||||
import * as browserJPEG from '../../codecs/browser-jpeg/encoder';
|
import * as browserJPEG from '../../codecs/browser-jpeg/encoder-meta';
|
||||||
import * as browserWebP from '../../codecs/browser-webp/encoder';
|
import * as browserWebP from '../../codecs/browser-webp/encoder-meta';
|
||||||
import * as browserGIF from '../../codecs/browser-gif/encoder';
|
import * as browserGIF from '../../codecs/browser-gif/encoder-meta';
|
||||||
import * as browserTIFF from '../../codecs/browser-tiff/encoder';
|
import * as browserTIFF from '../../codecs/browser-tiff/encoder-meta';
|
||||||
import * as browserJP2 from '../../codecs/browser-jp2/encoder';
|
import * as browserJP2 from '../../codecs/browser-jp2/encoder-meta';
|
||||||
import * as browserBMP from '../../codecs/browser-bmp/encoder';
|
import * as browserBMP from '../../codecs/browser-bmp/encoder-meta';
|
||||||
import * as browserPDF from '../../codecs/browser-pdf/encoder';
|
import * as browserPDF from '../../codecs/browser-pdf/encoder-meta';
|
||||||
import {
|
import {
|
||||||
EncoderState,
|
EncoderState,
|
||||||
EncoderType,
|
EncoderType,
|
||||||
@@ -33,8 +33,8 @@ import {
|
|||||||
EncoderSupportMap,
|
EncoderSupportMap,
|
||||||
encoderMap,
|
encoderMap,
|
||||||
} from '../../codecs/encoders';
|
} from '../../codecs/encoders';
|
||||||
import { QuantizeOptions } from '../../codecs/imagequant/quantizer';
|
import { QuantizeOptions } from '../../codecs/imagequant/processor-meta';
|
||||||
import { ResizeOptions } from '../../codecs/resize/resize';
|
import { ResizeOptions } from '../../codecs/resize/processor-meta';
|
||||||
import { PreprocessorState } from '../../codecs/preprocessors';
|
import { PreprocessorState } from '../../codecs/preprocessors';
|
||||||
import FileSize from '../FileSize';
|
import FileSize from '../FileSize';
|
||||||
import { DownloadIcon } from '../../lib/icons';
|
import { DownloadIcon } from '../../lib/icons';
|
||||||
|
|||||||
@@ -6,21 +6,18 @@ import * as style from './style.scss';
|
|||||||
import Output from '../Output';
|
import Output from '../Output';
|
||||||
import Options from '../Options';
|
import Options from '../Options';
|
||||||
import ResultCache from './result-cache';
|
import ResultCache from './result-cache';
|
||||||
|
import * as identity from '../../codecs/identity/encoder-meta';
|
||||||
import * as quantizer from '../../codecs/imagequant/quantizer';
|
import * as optiPNG from '../../codecs/optipng/encoder-meta';
|
||||||
import * as optiPNG from '../../codecs/optipng/encoder';
|
import * as mozJPEG from '../../codecs/mozjpeg/encoder-meta';
|
||||||
import * as resizer from '../../codecs/resize/resize';
|
import * as webP from '../../codecs/webp/encoder-meta';
|
||||||
import * as mozJPEG from '../../codecs/mozjpeg/encoder';
|
import * as browserPNG from '../../codecs/browser-png/encoder-meta';
|
||||||
import * as webP from '../../codecs/webp/encoder';
|
import * as browserJPEG from '../../codecs/browser-jpeg/encoder-meta';
|
||||||
import * as identity from '../../codecs/identity/encoder';
|
import * as browserWebP from '../../codecs/browser-webp/encoder-meta';
|
||||||
import * as browserPNG from '../../codecs/browser-png/encoder';
|
import * as browserGIF from '../../codecs/browser-gif/encoder-meta';
|
||||||
import * as browserJPEG from '../../codecs/browser-jpeg/encoder';
|
import * as browserTIFF from '../../codecs/browser-tiff/encoder-meta';
|
||||||
import * as browserWebP from '../../codecs/browser-webp/encoder';
|
import * as browserJP2 from '../../codecs/browser-jp2/encoder-meta';
|
||||||
import * as browserGIF from '../../codecs/browser-gif/encoder';
|
import * as browserBMP from '../../codecs/browser-bmp/encoder-meta';
|
||||||
import * as browserTIFF from '../../codecs/browser-tiff/encoder';
|
import * as browserPDF from '../../codecs/browser-pdf/encoder-meta';
|
||||||
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 {
|
import {
|
||||||
EncoderState,
|
EncoderState,
|
||||||
EncoderType,
|
EncoderType,
|
||||||
@@ -35,6 +32,8 @@ import {
|
|||||||
|
|
||||||
import { decodeImage } from '../../codecs/decoders';
|
import { decodeImage } from '../../codecs/decoders';
|
||||||
import { cleanMerge, cleanSet } from '../../lib/clean-modify';
|
import { cleanMerge, cleanSet } from '../../lib/clean-modify';
|
||||||
|
import Processor from '../../codecs/processor';
|
||||||
|
import { VectorResizeOptions, BitmapResizeOptions } from '../../codecs/resize/processor-meta';
|
||||||
|
|
||||||
type Orientation = 'horizontal' | 'vertical';
|
type Orientation = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
@@ -78,20 +77,21 @@ interface UpdateImageOptions {
|
|||||||
async function preprocessImage(
|
async function preprocessImage(
|
||||||
source: SourceImage,
|
source: SourceImage,
|
||||||
preprocessData: PreprocessorState,
|
preprocessData: PreprocessorState,
|
||||||
|
processor: Processor,
|
||||||
): Promise<ImageData> {
|
): Promise<ImageData> {
|
||||||
let result = source.data;
|
let result = source.data;
|
||||||
if (preprocessData.resize.enabled) {
|
if (preprocessData.resize.enabled) {
|
||||||
if (preprocessData.resize.method === 'vector' && source.vectorImage) {
|
if (preprocessData.resize.method === 'vector' && source.vectorImage) {
|
||||||
result = resizer.vectorResize(
|
result = processor.vectorResize(
|
||||||
source.vectorImage,
|
source.vectorImage,
|
||||||
preprocessData.resize as resizer.VectorResizeOptions,
|
preprocessData.resize as VectorResizeOptions,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
result = resizer.resize(result, preprocessData.resize as resizer.BitmapResizeOptions);
|
result = processor.resize(result, preprocessData.resize as BitmapResizeOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (preprocessData.quantizer.enabled) {
|
if (preprocessData.quantizer.enabled) {
|
||||||
result = await quantizer.quantize(result, preprocessData.quantizer);
|
result = await processor.imageQuant(result, preprocessData.quantizer);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -100,20 +100,21 @@ async function compressImage(
|
|||||||
image: ImageData,
|
image: ImageData,
|
||||||
encodeData: EncoderState,
|
encodeData: EncoderState,
|
||||||
sourceFilename: string,
|
sourceFilename: string,
|
||||||
|
processor: Processor,
|
||||||
): Promise<Fileish> {
|
): Promise<Fileish> {
|
||||||
const compressedData = await (() => {
|
const compressedData = await (() => {
|
||||||
switch (encodeData.type) {
|
switch (encodeData.type) {
|
||||||
case optiPNG.type: return optiPNG.encode(image, encodeData.options);
|
case optiPNG.type: return processor.optiPngEncode(image, encodeData.options);
|
||||||
case mozJPEG.type: return mozJPEG.encode(image, encodeData.options);
|
case mozJPEG.type: return processor.mozjpegEncode(image, encodeData.options);
|
||||||
case webP.type: return webP.encode(image, encodeData.options);
|
case webP.type: return processor.webpEncode(image, encodeData.options);
|
||||||
case browserPNG.type: return browserPNG.encode(image, encodeData.options);
|
case browserPNG.type: return processor.browserPngEncode(image);
|
||||||
case browserJPEG.type: return browserJPEG.encode(image, encodeData.options);
|
case browserJPEG.type: return processor.browserJpegEncode(image, encodeData.options);
|
||||||
case browserWebP.type: return browserWebP.encode(image, encodeData.options);
|
case browserWebP.type: return processor.browserWebpEncode(image, encodeData.options);
|
||||||
case browserGIF.type: return browserGIF.encode(image, encodeData.options);
|
case browserGIF.type: return processor.browserGifEncode(image);
|
||||||
case browserTIFF.type: return browserTIFF.encode(image, encodeData.options);
|
case browserTIFF.type: return processor.browserTiffEncode(image);
|
||||||
case browserJP2.type: return browserJP2.encode(image, encodeData.options);
|
case browserJP2.type: return processor.browserJp2Encode(image);
|
||||||
case browserBMP.type: return browserBMP.encode(image, encodeData.options);
|
case browserBMP.type: return processor.browserBmpEncode(image);
|
||||||
case browserPDF.type: return browserPDF.encode(image, encodeData.options);
|
case browserPDF.type: return processor.browserPdfEncode(image);
|
||||||
default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`);
|
default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -177,7 +178,9 @@ export default class Compress extends Component<Props, State> {
|
|||||||
orientation: this.widthQuery.matches ? 'horizontal' : 'vertical',
|
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) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -251,6 +254,10 @@ export default class Compress extends Component<Props, State> {
|
|||||||
private async updateFile(file: File | Fileish) {
|
private async updateFile(file: File | Fileish) {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
|
|
||||||
|
// Abort any current encode jobs, as they're redundant now.
|
||||||
|
this.leftProcessor.abortCurrent();
|
||||||
|
this.rightProcessor.abortCurrent();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let data: ImageData;
|
let data: ImageData;
|
||||||
let vectorImage: HTMLImageElement | undefined;
|
let vectorImage: HTMLImageElement | undefined;
|
||||||
@@ -262,7 +269,8 @@ export default class Compress extends Component<Props, State> {
|
|||||||
vectorImage = await processSvg(file);
|
vectorImage = await processSvg(file);
|
||||||
data = drawableToImageData(vectorImage);
|
data = drawableToImageData(vectorImage);
|
||||||
} else {
|
} else {
|
||||||
data = await decodeImage(file);
|
// Either processor is good enough here.
|
||||||
|
data = await decodeImage(file, this.leftProcessor);
|
||||||
}
|
}
|
||||||
|
|
||||||
let newState: State = {
|
let newState: State = {
|
||||||
@@ -293,6 +301,7 @@ export default class Compress extends Component<Props, State> {
|
|||||||
|
|
||||||
this.setState(newState);
|
this.setState(newState);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err.name === 'AbortError') return;
|
||||||
console.error(err);
|
console.error(err);
|
||||||
this.props.onError('Invalid image');
|
this.props.onError('Invalid image');
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
@@ -320,6 +329,12 @@ export default class Compress extends Component<Props, State> {
|
|||||||
let preprocessed: ImageData | undefined;
|
let preprocessed: ImageData | undefined;
|
||||||
let data: ImageData | undefined;
|
let data: ImageData | undefined;
|
||||||
const cacheResult = this.encodeCache.match(source, image.preprocessorState, image.encoderState);
|
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) {
|
if (cacheResult) {
|
||||||
({ file, preprocessed, data } = cacheResult);
|
({ file, preprocessed, data } = cacheResult);
|
||||||
@@ -331,10 +346,10 @@ export default class Compress extends Component<Props, State> {
|
|||||||
} else {
|
} else {
|
||||||
preprocessed = (skipPreprocessing && image.preprocessed)
|
preprocessed = (skipPreprocessing && image.preprocessed)
|
||||||
? image.preprocessed
|
? image.preprocessed
|
||||||
: await preprocessImage(source, image.preprocessorState);
|
: await preprocessImage(source, image.preprocessorState, processor);
|
||||||
|
|
||||||
file = await compressImage(preprocessed, image.encoderState, source.file.name);
|
file = await compressImage(preprocessed, image.encoderState, source.file.name, processor);
|
||||||
data = await decodeImage(file);
|
data = await decodeImage(file, processor);
|
||||||
|
|
||||||
this.encodeCache.add({
|
this.encodeCache.add({
|
||||||
source,
|
source,
|
||||||
@@ -346,6 +361,7 @@ export default class Compress extends Component<Props, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err.name === 'AbortError') return;
|
||||||
this.props.onError(`Processing error (type=${image.encoderState.type}): ${err}`);
|
this.props.onError(`Processing error (type=${image.encoderState.type}): ${err}`);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { shallowEqual } from '../../lib/util';
|
|||||||
import { SourceImage } from '.';
|
import { SourceImage } from '.';
|
||||||
import { PreprocessorState } from '../../codecs/preprocessors';
|
import { PreprocessorState } from '../../codecs/preprocessors';
|
||||||
|
|
||||||
import * as identity from '../../codecs/identity/encoder';
|
import * as identity from '../../codecs/identity/encoder-meta';
|
||||||
|
|
||||||
interface CacheResult {
|
interface CacheResult {
|
||||||
preprocessed: ImageData;
|
preprocessed: ImageData;
|
||||||
|
|||||||
5
src/missing-types.d.ts
vendored
5
src/missing-types.d.ts
vendored
@@ -22,3 +22,8 @@ declare module '*.svg' {
|
|||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '*.wasm' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const ReplacePlugin = require('webpack-plugin-replace');
|
|||||||
const CopyPlugin = require('copy-webpack-plugin');
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
const WatchTimestampsPlugin = require('./config/watch-timestamps-plugin');
|
const WatchTimestampsPlugin = require('./config/watch-timestamps-plugin');
|
||||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
const WorkerPlugin = require('worker-plugin');
|
||||||
|
|
||||||
function readJson (filename) {
|
function readJson (filename) {
|
||||||
return JSON.parse(fs.readFileSync(filename));
|
return JSON.parse(fs.readFileSync(filename));
|
||||||
@@ -130,10 +131,6 @@ module.exports = function (_, env) {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
test: /\.worker.[tj]sx?$/,
|
|
||||||
loader: 'comlink-loader'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
test: /\.tsx?$/,
|
test: /\.tsx?$/,
|
||||||
exclude: nodeModules,
|
exclude: nodeModules,
|
||||||
@@ -183,6 +180,8 @@ module.exports = function (_, env) {
|
|||||||
beforeEmit: true
|
beforeEmit: true
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
new WorkerPlugin(),
|
||||||
|
|
||||||
// Automatically split code into async chunks.
|
// Automatically split code into async chunks.
|
||||||
// See: https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
|
// See: https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
|
||||||
isProd && new webpack.optimize.SplitChunksPlugin({}),
|
isProd && new webpack.optimize.SplitChunksPlugin({}),
|
||||||
|
|||||||
Reference in New Issue
Block a user