Compare commits

..

44 Commits

Author SHA1 Message Date
Jason Miller
6ed26ec70c a couple more fixes from CR 2021-06-07 15:04:51 -04:00
Jason Miller
73b3fd0ef3 Merge branch 'dev' into preprocessor-transformations-rebased 2021-06-07 15:02:13 -04:00
Jason Miller
ad6f91692f Reuse glyphs for multiple icons, fix up typings 2021-06-07 15:01:24 -04:00
Jason Miller
7918938d8a Suggestions from code review 2021-06-07 14:59:26 -04:00
Jason Miller
543c2e73fb Avoid mutating input buffer in crop preprocessor 2021-06-03 11:16:26 -04:00
Jason Miller
8c5b4f33bf remove missing-types.d.ts 2021-06-03 11:13:53 -04:00
Jason Miller
066e9acf93 Merge branch 'dev' into preprocessor-transformations-rebased 2021-06-03 11:10:43 -04:00
Jake Archibald
44de57a92a Merge branch 'dev' into preprocessor-transformations-rebased 2021-02-09 13:40:02 +00:00
Jason Miller
33347951d7 Merge branch 'dev' into preprocessor-transformations-rebased 2020-12-15 22:50:01 -05:00
Jason Miller
c459319b21 Fix rotation transforms 2020-12-14 17:32:29 -05:00
Jason Miller
588ec61543 fix rotate crop adjustment 2020-12-14 16:45:15 -05:00
Jason Miller
82914f9cde Fix Firefox, remove dead code. 2020-12-11 12:55:14 -05:00
Jason Miller
37f4d753f9 Merge pull request #897 from GoogleChromeLabs/cli-invoc-2
Flyouts
2020-12-10 23:46:54 -05:00
Jason Miller
ce3d94297d Close Options flyout when clicking an item 2020-12-10 23:44:48 -05:00
Jason Miller
76ef6294f3 Fix merge issue 2020-12-10 23:37:55 -05:00
Jason Miller
50bc8e4106 Merge branch 'preprocessor-transformations-rebased' into cli-invoc-2 2020-12-10 23:34:10 -05:00
Jason Miller
1b8f051438 Merge branch 'dev' of github.com:GoogleChromeLabs/squoosh into preprocessor-transformations-rebased 2020-12-10 23:32:15 -05:00
Jason Miller
cf894b7d19 fix 2020-12-10 12:34:50 -05:00
Jason Miller
d1d181fccd Merge upstream 2020-12-10 10:24:56 -05:00
Jason Miller
a65bbdf811 Hoist flyouts to <body> 2020-12-09 23:47:43 -05:00
Jason Miller
7de8fa9da3 Initial Options Flyout 2020-12-09 18:11:44 -05:00
Jason Miller
646747b039 Merge branch 'preprocessor-transformations-rebased' into cli-invoc-2 2020-12-09 12:21:24 -05:00
Jason Miller
a2fb7a38cd Fix mobile fly-out 2020-12-09 12:20:55 -05:00
Jason Miller
81890e972b Remove development image load 2020-12-09 12:14:11 -05:00
Jason Miller
c2aa35aa02 Hide options on mobile 2020-12-09 12:12:32 -05:00
Jason Miller
c6d936cd49 Fix cropbox UI in Firefox 2020-12-09 11:58:27 -05:00
Jason Miller
60b79da936 Keep resize enabled when cropping 2020-12-09 11:58:10 -05:00
Jason Miller
dbd80f15eb Fix cropbox background when zoomed 2020-12-09 11:49:03 -05:00
Jason Miller
ed3c79894d Remove "Retina" zoom preset 2020-12-09 11:48:28 -05:00
Jason Miller
213028cfdd Changing crop updates and disables resize processor 2020-12-09 11:48:10 -05:00
Jason Miller
952aea049d Merge branch 'cli-invoc' of github.com:GoogleChromeLabs/squoosh into cli-invoc-2 2020-12-09 11:28:24 -05:00
Jason Miller
e3b053db12 Fix crop preprocessor 2020-12-09 11:22:43 -05:00
Jason Miller
b8574b228a fix ordering of preprocessors 2020-12-09 11:22:33 -05:00
Jason Miller
ee8ea539e7 fix crop aspect and presets 2020-12-09 11:22:22 -05:00
Jake Archibald
a7a991ae45 Removing redundant imports, restoring auto-load 2020-12-09 12:38:32 +00:00
Jason Miller
32232c7f0b Add yet another gray 2020-12-09 12:33:59 +00:00
Jason Miller
bb78632cf5 Preact TS tweak 2020-12-09 12:33:59 +00:00
Jason Miller
68cd15bd14 crop and flip preprocessor 2020-12-09 12:33:59 +00:00
Jason Miller
bde3a93b6e Add Transform modal 2020-12-09 12:33:57 +00:00
Jason Miller
7aeef5ff37 Add Flyout, hoist altBackground to Compress 2020-12-09 12:29:59 +00:00
Jason Miller
0ee234f03b Preact 10.5.7 2020-12-09 12:29:06 +00:00
Surma
8105633ca6 Update src/client/lazy-app/util/cli-invocation-generator.ts 2020-12-07 11:17:38 +00:00
Surma
46764f3375 Show snack on error 2020-12-01 13:14:35 +00:00
Surma
0371cfd292 Add CLI button 2020-12-01 13:12:28 +00:00
142 changed files with 4001 additions and 10496 deletions

View File

@@ -1,42 +1,36 @@
# [Squoosh]! # [Squoosh]!
[Squoosh] is an image compression web app that reduces image sizes through numerous formats. [Squoosh] is an image compression web app that allows you to dive into the advanced options provided
by various image compressors.
# API & CLI # API & CLI
Squoosh has [an API](https://github.com/GoogleChromeLabs/squoosh/tree/dev/libsquoosh) and [a CLI](https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli) to compress many images at once. Squoosh now has [an API](https://github.com/GoogleChromeLabs/squoosh/tree/dev/libsquoosh) and [a CLI](https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli) that allows you to compress many images at once.
# Privacy # Privacy
Squoosh does not send your image to a server. All image compression processes locally. Google Analytics is used to record the following:
However, Squoosh utilizes Google Analytics to collect the following: - [Basic visit data](https://support.google.com/analytics/answer/6004245?ref_topic=2919631).
- Before and after image size once an image is downloaded. These values are rounded to the nearest
kilobyte.
- If install is available, when Squoosh is installed, and what method was used to install Squoosh.
- [Basic visitor data](https://support.google.com/analytics/answer/6004245?ref_topic=2919631). Image compression is handled locally; no additional data is sent to the server.
- The before and after image size value.
- If Squoosh PWA, the type of Squoosh installation.
- If Squoosh PWA, the installation time and date.
# Developing # Building locally
To develop for Squoosh: Clone the repo, and:
1. Clone the repository ```sh
1. To install node packages, run: npm install
```sh npm run build
npm install ```
```
1. Then build the app by running:
```sh
npm run build
```
1. After building, start the development server by running:
```sh
npm run dev
```
# Contributing You can run the development server with:
Squoosh is an open-source project that appreciates all community involvement. To contribute to the project, follow the [contribute guide](/CONTRIBUTING.md). ```sh
npm run dev
```
[squoosh]: https://squoosh.app [squoosh]: https://squoosh.app

View File

@@ -42,7 +42,7 @@ Options:
-h, --help display help for command -h, --help display help for command
``` ```
The default values for each `config` option can be found in the [`codecs.ts`][codecs.ts] file under `defaultEncoderOptions`. Every unspecified value will use the default value specified here. _Better documentation is needed here._ The default values for each `config` option can be found in the [`codecs.js`][codecs.js] file under `defaultEncoderOptions`. Every unspecified value will use the default value specified here. _Better documentation is needed here._
## Auto optimizer ## Auto optimizer
@@ -55,5 +55,5 @@ $ npx @squoosh/cli --wp2 auto test.png
``` ```
[squoosh]: https://squoosh.app [squoosh]: https://squoosh.app
[codecs.ts]: https://github.com/GoogleChromeLabs/squoosh/blob/dev/libsquoosh/src/codecs.ts [codecs.js]: https://github.com/GoogleChromeLabs/squoosh/blob/dev/libsquoosh/src/codecs.js
[butteraugli]: https://github.com/google/butteraugli [butteraugli]: https://github.com/google/butteraugli

36
cli/package-lock.json generated
View File

@@ -1,15 +1,15 @@
{ {
"name": "@squoosh/cli", "name": "@squoosh/cli",
"version": "0.7.2", "version": "0.7.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@squoosh/cli", "name": "@squoosh/cli",
"version": "0.7.2", "version": "0.7.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@squoosh/lib": "^0.4.0", "@squoosh/lib": "^0.2.0",
"commander": "^7.2.0", "commander": "^7.2.0",
"json5": "^2.2.0", "json5": "^2.2.0",
"kleur": "^4.1.4", "kleur": "^4.1.4",
@@ -18,21 +18,14 @@
"bin": { "bin": {
"cli": "src/index.js", "cli": "src/index.js",
"squoosh-cli": "src/index.js" "squoosh-cli": "src/index.js"
},
"engines": {
"node": " ^12.20.2 || ^14.13.1 || ^16.0.0 "
} }
}, },
"node_modules/@squoosh/lib": { "node_modules/@squoosh/lib": {
"version": "0.4.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@squoosh/lib/-/lib-0.4.0.tgz", "resolved": "https://registry.npmjs.org/@squoosh/lib/-/lib-0.2.0.tgz",
"integrity": "sha512-O1LyugWLZjMI4JZeZMA5vzfhfPjfMZXH5/HmVkRagP8B70wH3uoR7tjxfGNdSavey357MwL8YJDxbGwBBdHp7Q==", "integrity": "sha512-zKId9h/LzEnCdoOGnIgjzvqbk5g2aHgfEqgKQ+S+r5K3TasgD/DAsT6r7v6gXft1ao0f/00CTcwIp1KviWTQbw==",
"dependencies": { "dependencies": {
"wasm-feature-detect": "^1.2.11",
"web-streams-polyfill": "^3.0.3" "web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": " ^12.5.0 || ^14.0.0 || ^16.0.0 "
} }
}, },
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
@@ -330,11 +323,6 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
}, },
"node_modules/wasm-feature-detect": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz",
"integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w=="
},
"node_modules/wcwidth": { "node_modules/wcwidth": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@@ -354,11 +342,10 @@
}, },
"dependencies": { "dependencies": {
"@squoosh/lib": { "@squoosh/lib": {
"version": "0.4.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@squoosh/lib/-/lib-0.4.0.tgz", "resolved": "https://registry.npmjs.org/@squoosh/lib/-/lib-0.2.0.tgz",
"integrity": "sha512-O1LyugWLZjMI4JZeZMA5vzfhfPjfMZXH5/HmVkRagP8B70wH3uoR7tjxfGNdSavey357MwL8YJDxbGwBBdHp7Q==", "integrity": "sha512-zKId9h/LzEnCdoOGnIgjzvqbk5g2aHgfEqgKQ+S+r5K3TasgD/DAsT6r7v6gXft1ao0f/00CTcwIp1KviWTQbw==",
"requires": { "requires": {
"wasm-feature-detect": "^1.2.11",
"web-streams-polyfill": "^3.0.3" "web-streams-polyfill": "^3.0.3"
} }
}, },
@@ -591,11 +578,6 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
}, },
"wasm-feature-detect": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz",
"integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w=="
},
"wcwidth": { "wcwidth": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",

View File

@@ -1,14 +1,9 @@
{ {
"name": "@squoosh/cli", "name": "@squoosh/cli",
"version": "0.7.2", "version": "0.7.0",
"description": "A CLI for Squoosh", "description": "A CLI for Squoosh",
"public": true, "public": true,
"type": "module", "type": "module",
"homepage": "https://github.com/GoogleChromeLabs/squoosh",
"repository": {
"type": "git",
"url": "https://github.com/GoogleChromeLabs/squoosh.git"
},
"bin": { "bin": {
"squoosh-cli": "src/index.js", "squoosh-cli": "src/index.js",
"@squoosh/cli": "src/index.js" "@squoosh/cli": "src/index.js"
@@ -19,11 +14,8 @@
"keywords": [], "keywords": [],
"author": "Google Chrome Developers <chromium-dev@google.com>", "author": "Google Chrome Developers <chromium-dev@google.com>",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": {
"node": " ^12.20.2 || ^14.13.1 || ^16.0.0 "
},
"dependencies": { "dependencies": {
"@squoosh/lib": "^0.4.0", "@squoosh/lib": "^0.2.0",
"commander": "^7.2.0", "commander": "^7.2.0",
"json5": "^2.2.0", "json5": "^2.2.0",
"kleur": "^4.1.4", "kleur": "^4.1.4",

View File

@@ -75,7 +75,9 @@ async function getInputFiles(paths) {
for (const inputPath of paths) { for (const inputPath of paths) {
const files = (await fsp.lstat(inputPath)).isDirectory() const files = (await fsp.lstat(inputPath)).isDirectory()
? (await fsp.readdir(inputPath, {withFileTypes: true})).filter(dirent => dirent.isFile()).map(dirent => path.join(inputPath, dirent.name)) ? (await fsp.readdir(inputPath, { withFileTypes: true }))
.filter((dirent) => dirent.isFile())
.map((dirent) => path.join(inputPath, dirent.name))
: [inputPath]; : [inputPath];
for (const file of files) { for (const file of files) {
try { try {
@@ -177,8 +179,8 @@ async function processFiles(files) {
jobsFinished++; jobsFinished++;
const outputPath = path.join( const outputPath = path.join(
program.opts().outputDir, program.opts().outputDir,
path.basename(originalFile, path.extname(originalFile)) + program.opts().suffix +
program.opts().suffix path.basename(originalFile, path.extname(originalFile)),
); );
for (const output of Object.values(image.encodedWith)) { for (const output of Object.values(image.encodedWith)) {
const outputFile = `${outputPath}.${(await output).extension}`; const outputFile = `${outputPath}.${(await output).extension}`;

View File

@@ -10,16 +10,13 @@ export CODEC_DIR = node_modules/libavif
export BUILD_DIR = node_modules/build export BUILD_DIR = node_modules/build
export LIBAOM_DIR = node_modules/libaom export LIBAOM_DIR = node_modules/libaom
override CFLAGS += "-Wno-unused-macros"
export
OUT_ENC_JS = enc/avif_enc.js OUT_ENC_JS = enc/avif_enc.js
OUT_NODE_ENC_JS = enc/avif_node_enc.js OUT_NODE_ENC_JS = enc/avif_node_enc.js
OUT_ENC_MT_JS = enc/avif_enc_mt.js OUT_ENC_MT_JS = enc/avif_enc_mt.js
OUT_NODE_ENC_MT_JS = enc/avif_node_enc_mt.js
OUT_DEC_JS = dec/avif_dec.js OUT_DEC_JS = dec/avif_dec.js
OUT_NODE_DEC_JS = dec/avif_node_dec.js OUT_NODE_DEC_JS = dec/avif_node_dec.js
OUT_ENC_CPP = enc/avif_enc.cpp
OUT_ENC_CPP = enc/avif_enc.cpp OUT_ENC_CPP = enc/avif_enc.cpp
OUT_DEC_CPP = dec/avif_dec.cpp OUT_DEC_CPP = dec/avif_dec.cpp
ENVIRONMENT = worker ENVIRONMENT = worker
@@ -28,9 +25,9 @@ HELPER_MAKEFLAGS := -f helper.Makefile
.PHONY: all clean .PHONY: all clean
all: $(OUT_ENC_JS) $(OUT_DEC_JS) $(OUT_ENC_MT_JS) $(OUT_NODE_ENC_JS) $(OUT_NODE_ENC_MT_JS) $(OUT_NODE_DEC_JS) all: $(OUT_ENC_JS) $(OUT_DEC_JS) $(OUT_ENC_MT_JS) $(OUT_NODE_ENC_JS) $(OUT_NODE_DEC_JS)
$(OUT_NODE_ENC_JS) $(OUT_NODE_ENC_MT_JS): ENVIRONMENT=node $(OUT_NODE_ENC_JS): ENVIRONMENT=node
$(OUT_NODE_ENC_JS) $(OUT_ENC_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt $(OUT_NODE_ENC_JS) $(OUT_ENC_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
$(MAKE) \ $(MAKE) \
$(HELPER_MAKEFLAGS) \ $(HELPER_MAKEFLAGS) \
@@ -44,7 +41,7 @@ $(OUT_NODE_ENC_JS) $(OUT_ENC_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(L
ENVIRONMENT=$(ENVIRONMENT) \ ENVIRONMENT=$(ENVIRONMENT) \
LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_DECODE=0" LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_DECODE=0"
$(OUT_ENC_MT_JS) $(OUT_NODE_ENC_MT_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt $(OUT_ENC_MT_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
$(MAKE) \ $(MAKE) \
$(HELPER_MAKEFLAGS) \ $(HELPER_MAKEFLAGS) \
OUT_JS=$@ \ OUT_JS=$@ \
@@ -89,7 +86,4 @@ $(LIBAOM_DIR)/CMakeLists.txt: $(LIBAOM_PACKAGE)
clean: clean:
$(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_ENC_JS) clean $(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_ENC_JS) clean
$(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_ENC_MT_JS) clean $(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_ENC_MT_JS) clean
$(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_ENC_NODE_JS) clean
$(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_ENC_NODE_MT_JS) clean
$(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_DEC_JS) clean $(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_DEC_JS) clean
$(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_DEV_NODE_JS) clean

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1 +1 @@
"use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./avif_enc_mt.js")).then(function(exports){return exports.default(Module)}).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,0,0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}}; "use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./avif_enc_mt.js")).then(function(exports){return exports.default(Module)}).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,/*isMainBrowserThread=*/0,/*isMainRuntimeThread=*/0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
"use strict";var Module={};if(typeof process==="object"&&typeof process.versions==="object"&&typeof process.versions.node==="string"){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",function(data){onmessage({data:data})});var nodeFS=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(nodeFS.readFileSync(f,"utf8"))},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./avif_node_enc_mt.js")).then(function(exports){return exports.default(Module)}).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,0,0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};

View File

@@ -1,13 +1,13 @@
FROM emscripten/emsdk:2.0.23 FROM emscripten/emsdk:2.0.21
RUN apt-get update && apt-get install -qqy autoconf libtool pkg-config RUN apt-get update && apt-get install -qqy autoconf libtool pkg-config
ENV CFLAGS "-O3 -flto" ENV CFLAGS "-O3 -flto"
ENV CXXFLAGS "${CFLAGS} -std=c++17" ENV CXXFLAGS "${CFLAGS} -std=c++17"
ENV LDFLAGS "${CFLAGS} \ ENV LDFLAGS "${CFLAGS} \
--closure 1 \
-s FILESYSTEM=0 \ -s FILESYSTEM=0 \
-s PTHREAD_POOL_SIZE=navigator.hardwareConcurrency \ -s PTHREAD_POOL_SIZE=navigator.hardwareConcurrency \
-s ALLOW_MEMORY_GROWTH=1 \ -s ALLOW_MEMORY_GROWTH=1 \
-s TEXTDECODER=2 \ -s TEXTDECODER=2 \
-s NODEJS_CATCH_EXIT=0 -s NODEJS_CATCH_REJECTION=0 \
" "
# Build and cache standard libraries with these flags + Embind. # Build and cache standard libraries with these flags + Embind.
RUN emcc ${CXXFLAGS} ${LDFLAGS} --bind -xc++ /dev/null -o /dev/null RUN emcc ${CXXFLAGS} ${LDFLAGS} --bind -xc++ /dev/null -o /dev/null

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
CODEC_URL = https://github.com/libjxl/libjxl.git CODEC_URL = https://gitlab.com/wg1/jpeg-xl.git
CODEC_VERSION = v0.5 CODEC_VERSION = ab7c5e9b6795134377aa4846ceaae2c5bc504f76
CODEC_DIR = node_modules/jxl CODEC_DIR = node_modules/jxl
CODEC_BUILD_ROOT := $(CODEC_DIR)/build CODEC_BUILD_ROOT := $(CODEC_DIR)/build
CODEC_MT_BUILD_DIR := $(CODEC_BUILD_ROOT)/mt CODEC_MT_BUILD_DIR := $(CODEC_BUILD_ROOT)/mt

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -10,13 +10,15 @@ using namespace emscripten;
thread_local const val Uint8Array = val::global("Uint8Array"); thread_local const val Uint8Array = val::global("Uint8Array");
struct JXLOptions { struct JXLOptions {
int effort; // 1 = slowest
// 7 = fastest
int speed;
float quality; float quality;
bool progressive; bool progressive;
int epf; int epf;
int nearLossless;
bool lossyPalette; bool lossyPalette;
size_t decodingSpeedTier; size_t decodingSpeedTier;
float photonNoiseIso;
}; };
val encode(std::string image, int width, int height, JXLOptions options) { val encode(std::string image, int width, int height, JXLOptions options) {
@@ -31,20 +33,15 @@ val encode(std::string image, int width, int height, JXLOptions options) {
pool_ptr = &pool; pool_ptr = &pool;
#endif #endif
size_t st = 10 - options.effort;
cparams.speed_tier = jxl::SpeedTier(st);
cparams.epf = options.epf; cparams.epf = options.epf;
cparams.speed_tier = static_cast<jxl::SpeedTier>(options.speed);
cparams.near_lossless = options.nearLossless;
cparams.decoding_speed_tier = options.decodingSpeedTier; cparams.decoding_speed_tier = options.decodingSpeedTier;
cparams.photon_noise_iso = options.photonNoiseIso;
if (options.lossyPalette) { if (options.lossyPalette) {
cparams.lossy_palette = true; cparams.lossy_palette = true;
cparams.palette_colors = 0; cparams.palette_colors = 0;
cparams.options.predictor = jxl::Predictor::Zero; cparams.options.predictor = jxl::Predictor::Zero;
// Near-lossless assumes -R 0
cparams.responsive = 0;
cparams.modular_mode = true;
} }
float quality = options.quality; float quality = options.quality;
@@ -80,6 +77,12 @@ val encode(std::string image, int width, int height, JXLOptions options) {
} }
} }
if (cparams.near_lossless) {
// Near-lossless assumes -R 0
cparams.responsive = 0;
cparams.modular_mode = true;
}
io.metadata.m.SetAlphaBits(8); io.metadata.m.SetAlphaBits(8);
if (!io.metadata.size.Set(width, height)) { if (!io.metadata.size.Set(width, height)) {
return val::null(); return val::null();
@@ -107,12 +110,12 @@ val encode(std::string image, int width, int height, JXLOptions options) {
EMSCRIPTEN_BINDINGS(my_module) { EMSCRIPTEN_BINDINGS(my_module) {
value_object<JXLOptions>("JXLOptions") value_object<JXLOptions>("JXLOptions")
.field("effort", &JXLOptions::effort) .field("speed", &JXLOptions::speed)
.field("quality", &JXLOptions::quality) .field("quality", &JXLOptions::quality)
.field("progressive", &JXLOptions::progressive) .field("progressive", &JXLOptions::progressive)
.field("nearLossless", &JXLOptions::nearLossless)
.field("lossyPalette", &JXLOptions::lossyPalette) .field("lossyPalette", &JXLOptions::lossyPalette)
.field("decodingSpeedTier", &JXLOptions::decodingSpeedTier) .field("decodingSpeedTier", &JXLOptions::decodingSpeedTier)
.field("photonNoiseIso", &JXLOptions::photonNoiseIso)
.field("epf", &JXLOptions::epf); .field("epf", &JXLOptions::epf);
function("encode", &encode); function("encode", &encode);

View File

@@ -1,11 +1,11 @@
export interface EncodeOptions { export interface EncodeOptions {
effort: number; speed: number;
quality: number; quality: number;
progressive: boolean; progressive: boolean;
epf: number; epf: number;
nearLossless: number;
lossyPalette: boolean; lossyPalette: boolean;
decodingSpeedTier: number; decodingSpeedTier: number;
photonNoiseIso: number;
} }
export interface JXLModule extends EmscriptenWasm.Module { export interface JXLModule extends EmscriptenWasm.Module {

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1 +1 @@
"use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./jxl_enc_mt.js")).then(function(exports){return exports.default(Module)}).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,0,0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}}; "use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./jxl_enc_mt.js")).then(function(exports){return exports.default(Module)}).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,/*isMainBrowserThread=*/0,/*isMainRuntimeThread=*/0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1 +1 @@
"use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./jxl_enc_mt_simd.js")).then(function(exports){return exports.default(Module)}).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,0,0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}}; "use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./jxl_enc_mt_simd.js")).then(function(exports){return exports.default(Module)}).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,/*isMainBrowserThread=*/0,/*isMainRuntimeThread=*/0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -158,11 +158,6 @@ val encode(std::string image_in, int image_width, int image_height, MozJpegOptio
if (!opts.auto_subsample && opts.color_space == JCS_YCbCr) { if (!opts.auto_subsample && opts.color_space == JCS_YCbCr) {
cinfo.comp_info[0].h_samp_factor = opts.chroma_subsample; cinfo.comp_info[0].h_samp_factor = opts.chroma_subsample;
cinfo.comp_info[0].v_samp_factor = opts.chroma_subsample; cinfo.comp_info[0].v_samp_factor = opts.chroma_subsample;
if (opts.chroma_subsample > 2) {
// Otherwise encoding fails.
jpeg_c_set_int_param(&cinfo, JINT_DC_SCAN_OPT_MODE, 1);
}
} }
if (!opts.baseline && opts.progressive) { if (!opts.baseline && opts.progressive) {

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -53,11 +53,7 @@ where
#[wasm_bindgen] #[wasm_bindgen]
pub fn decode(mut data: &[u8]) -> ImageData { pub fn decode(mut data: &[u8]) -> ImageData {
let mut decoder = png::Decoder::new(&mut data); let mut decoder = png::Decoder::new(&mut data);
decoder.set_transformations( decoder.set_transformations(png::Transformations::EXPAND);
png::Transformations::EXPAND | // Turn paletted images into RGB
png::Transformations::PACKING | // Turn images <8bit to 8bit
png::Transformations::STRIP_16, // Turn 16bit into 8 bit
);
let (info, mut reader) = decoder.read_info().unwrap_throw(); let (info, mut reader) = decoder.read_info().unwrap_throw();
let num_pixels = (info.width * info.height) as usize; let num_pixels = (info.width * info.height) as usize;
let mut buf = vec![0; num_pixels * 4]; let mut buf = vec![0; num_pixels * 4];

View File

@@ -9,9 +9,9 @@
* @param {number} typ_idx * @param {number} typ_idx
* @param {boolean} premultiply * @param {boolean} premultiply
* @param {boolean} color_space_conversion * @param {boolean} color_space_conversion
* @returns {Uint8ClampedArray} * @returns {Uint8Array}
*/ */
export function resize(input_image: Uint8Array, input_width: number, input_height: number, output_width: number, output_height: number, typ_idx: number, premultiply: boolean, color_space_conversion: boolean): Uint8ClampedArray; export function resize(input_image: Uint8Array, input_width: number, input_height: number, output_width: number, output_height: number, typ_idx: number, premultiply: boolean, color_space_conversion: boolean): Uint8Array;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;

View File

@@ -26,16 +26,8 @@ function getInt32Memory0() {
return cachegetInt32Memory0; return cachegetInt32Memory0;
} }
let cachegetUint8ClampedMemory0 = null; function getArrayU8FromWasm0(ptr, len) {
function getUint8ClampedMemory0() { return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
if (cachegetUint8ClampedMemory0 === null || cachegetUint8ClampedMemory0.buffer !== wasm.memory.buffer) {
cachegetUint8ClampedMemory0 = new Uint8ClampedArray(wasm.memory.buffer);
}
return cachegetUint8ClampedMemory0;
}
function getClampedArrayU8FromWasm0(ptr, len) {
return getUint8ClampedMemory0().subarray(ptr / 1, ptr / 1 + len);
} }
/** /**
* @param {Uint8Array} input_image * @param {Uint8Array} input_image
@@ -46,7 +38,7 @@ function getClampedArrayU8FromWasm0(ptr, len) {
* @param {number} typ_idx * @param {number} typ_idx
* @param {boolean} premultiply * @param {boolean} premultiply
* @param {boolean} color_space_conversion * @param {boolean} color_space_conversion
* @returns {Uint8ClampedArray} * @returns {Uint8Array}
*/ */
export function resize(input_image, input_width, input_height, output_width, output_height, typ_idx, premultiply, color_space_conversion) { export function resize(input_image, input_width, input_height, output_width, output_height, typ_idx, premultiply, color_space_conversion) {
try { try {
@@ -56,7 +48,7 @@ export function resize(input_image, input_width, input_height, output_width, out
wasm.resize(retptr, ptr0, len0, input_width, input_height, output_width, output_height, typ_idx, premultiply, color_space_conversion); wasm.resize(retptr, ptr0, len0, input_width, input_height, output_width, output_height, typ_idx, premultiply, color_space_conversion);
var r0 = getInt32Memory0()[retptr / 4 + 0]; var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1]; var r1 = getInt32Memory0()[retptr / 4 + 1];
var v1 = getClampedArrayU8FromWasm0(r0, r1).slice(); var v1 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_free(r0, r1 * 1); wasm.__wbindgen_free(r0, r1 * 1);
return v1; return v1;
} finally { } finally {

View File

@@ -1,7 +0,0 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export function resize(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number): void;
export function __wbindgen_add_to_stack_pointer(a: number): number;
export function __wbindgen_malloc(a: number): number;
export function __wbindgen_free(a: number, b: number): void;

View File

@@ -8,7 +8,6 @@ use cfg_if::cfg_if;
use resize::Pixel; use resize::Pixel;
use resize::Type; use resize::Type;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wasm_bindgen::Clamped;
mod srgb; mod srgb;
use srgb::{linear_to_srgb, Clamp}; use srgb::{linear_to_srgb, Clamp};
@@ -67,7 +66,7 @@ pub fn resize(
typ_idx: usize, typ_idx: usize,
premultiply: bool, premultiply: bool,
color_space_conversion: bool, color_space_conversion: bool,
) -> Clamped<Vec<u8>> { ) -> Vec<u8> {
let typ = match typ_idx { let typ = match typ_idx {
0 => Type::Triangle, 0 => Type::Triangle,
1 => Type::Catrom, 1 => Type::Catrom,
@@ -92,7 +91,7 @@ pub fn resize(
typ, typ,
); );
resizer.resize(input_image.as_slice(), output_image.as_mut_slice()); resizer.resize(input_image.as_slice(), output_image.as_mut_slice());
return Clamped(output_image); return output_image;
} }
// Otherwise, we convert to f32 images to keep the // Otherwise, we convert to f32 images to keep the
@@ -139,5 +138,5 @@ pub fn resize(
.clamp(0.0, 255.0) as u8; .clamp(0.0, 255.0) as u8;
} }
return Clamped(output_image); return output_image;
} }

View File

@@ -1,14 +0,0 @@
This codec currently needs monkey-patching of Emscripten
```
$ docker run --rm -it -v $(PWD):/src squoosh-cpp "/bin/bash"
# cat << EOF | patch /emsdk/upstream/emscripten/system/lib/dlmalloc.c
659c659
< #define MALLOC_ALIGNMENT ((size_t)(2 * sizeof(void *)))
---
> #define MALLOC_ALIGNMENT ((size_t)(16U))
EOF
# emcc --clear-cache
# /emsdk/upstream/emscripten/embuilder build libdlmalloc --force
# emmake make
```

View File

@@ -1,6 +1,5 @@
{ {
"name": "avif", "name": "avif",
"type": "module",
"scripts": { "scripts": {
"build": "../build-cpp.sh" "build": "../build-cpp.sh"
} }

View File

@@ -5,19 +5,12 @@
using namespace emscripten; using namespace emscripten;
using namespace butteraugli; using namespace butteraugli;
#define GAMMA 2.2
static float SrgbToLinear[256];
inline void gammaLookupTable() {
SrgbToLinear[0] = 0;
for (int i = 1; i < 256; ++i) {
SrgbToLinear[i] = static_cast<float>(255.0 * pow(i / 255.0, GAMMA));
}
}
// Turns an interleaved RGBA buffer into 4 planes for each color channel // Turns an interleaved RGBA buffer into 4 planes for each color channel
void planarize(std::vector<ImageF>& img, const uint8_t* rgba, int width, int height) { void planarize(std::vector<ImageF>& img,
const uint8_t* rgba,
int width,
int height,
float gamma = 2.2) {
assert(img.size() == 0); assert(img.size() == 0);
img.push_back(ImageF(width, height)); img.push_back(ImageF(width, height));
img.push_back(ImageF(width, height)); img.push_back(ImageF(width, height));
@@ -29,10 +22,10 @@ void planarize(std::vector<ImageF>& img, const uint8_t* rgba, int width, int hei
float* const row_b = img[2].Row(y); float* const row_b = img[2].Row(y);
float* const row_a = img[3].Row(y); float* const row_a = img[3].Row(y);
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
row_r[x] = SrgbToLinear[rgba[(y * width + x) * 4 + 0]]; row_r[x] = 255.0 * pow(rgba[(y * width + x) * 4 + 0] / 255.0, gamma);
row_g[x] = SrgbToLinear[rgba[(y * width + x) * 4 + 1]]; row_g[x] = 255.0 * pow(rgba[(y * width + x) * 4 + 1] / 255.0, gamma);
row_b[x] = SrgbToLinear[rgba[(y * width + x) * 4 + 2]]; row_b[x] = 255.0 * pow(rgba[(y * width + x) * 4 + 2] / 255.0, gamma);
row_a[x] = SrgbToLinear[rgba[(y * width + x) * 4 + 3]]; row_a[x] = 255.0 * pow(rgba[(y * width + x) * 4 + 3] / 255.0, gamma);
} }
} }
} }
@@ -44,7 +37,6 @@ class VisDiff {
public: public:
VisDiff(std::string ref_img, int width, int height) { VisDiff(std::string ref_img, int width, int height) {
gammaLookupTable();
planarize(this->ref_img, (uint8_t*)ref_img.c_str(), width, height); planarize(this->ref_img, (uint8_t*)ref_img.c_str(), width, height);
this->width = width; this->width = width;
this->height = height; this->height = height;

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1 +1 @@
"use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./wp2_enc_mt.js")).then(function(exports){return exports.default(Module)}).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,0,0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}}; "use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./wp2_enc_mt.js")).then(function(exports){return exports.default(Module)}).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,/*isMainBrowserThread=*/0,/*isMainRuntimeThread=*/0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1 +1 @@
"use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./wp2_enc_mt_simd.js")).then(function(exports){return exports.default(Module)}).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,0,0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}}; "use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./wp2_enc_mt_simd.js")).then(function(exports){return exports.default(Module)}).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,/*isMainBrowserThread=*/0,/*isMainRuntimeThread=*/0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -77,7 +77,9 @@ export default function entryDataPlugin() {
} }
return JSON.stringify( return JSON.stringify(
getDependencies(chunks, chunk).map((filename) => fileNameToURL(filename)), getDependencies(chunks, chunk).map((filename) =>
fileNameToURL(filename),
),
); );
}, },
); );

View File

@@ -30,7 +30,7 @@ const imagePath = 'path/to/image.png';
const image = imagePool.ingestImage(imagePath); const image = imagePool.ingestImage(imagePath);
``` ```
The `ingestImage` function can take anything the node [`readFile`][readfile] function can take, including a buffer and `FileHandle`. The `ingestImage` function can take anything the node [`readFile`][readfile] function can take, uncluding a buffer and `FileHandle`.
The returned `image` object is a representation of the original image, that you can now preprocess, encode, and extract information about. The returned `image` object is a representation of the original image, that you can now preprocess, encode, and extract information about.
@@ -39,26 +39,18 @@ The returned `image` object is a representation of the original image, that you
When an image has been ingested, you can start preprocessing it and encoding it to other formats. This example will resize the image and then encode it to a `.jpg` and `.jxl` image: When an image has been ingested, you can start preprocessing it and encoding it to other formats. This example will resize the image and then encode it to a `.jpg` and `.jxl` image:
```js ```js
await image.decoded; //Wait until the image is decoded before running preprocessors. await image.decoded; //Wait until the image is decoded before running preprocessors
const preprocessOptions = { const preprocessOptions: {
//When both width and height are specified, the image resized to specified size.
resize: { resize: {
enabled: true, enabled: true,
width: 100, width: 100,
height: 50, height: 50,
} }
/*
//When either width or height is specified, the image resized to specified size keeping aspect ratio.
resize: {
enabled: true,
width: 100,
}
*/
} }
await image.preprocess(preprocessOptions); await image.preprocess(preprocessOptions);
const encodeOptions = { const encodeOptions: {
mozjpeg: {}, //an empty object means 'use default settings' mozjpeg: {}, //an empty object means 'use default settings'
jxl: { jxl: {
quality: 90, quality: 90,
@@ -68,7 +60,7 @@ await image.encode(encodeOptions);
``` ```
The default values for each option can be found in the [`codecs.ts`][codecs.ts] file under `defaultEncoderOptions`. Every unspecified value will use the default value specified there. _Better documentation is needed here._ The default values for each option can be found in the [`codecs.js`][codecs.js] file under `defaultEncoderOptions`. Every unspecified value will use the default value specified there. _Better documentation is needed here._
You can run your own code inbetween the different steps, if, for example, you want to change how much the image should be resized based on its original height. (See [Extracting image information](#extracting-image-information) to learn how to get the image dimensions). You can run your own code inbetween the different steps, if, for example, you want to change how much the image should be resized based on its original height. (See [Extracting image information](#extracting-image-information) to learn how to get the image dimensions).
@@ -166,6 +158,6 @@ const encodeOptions: {
``` ```
[squoosh]: https://squoosh.app [squoosh]: https://squoosh.app
[codecs.ts]: https://github.com/GoogleChromeLabs/squoosh/blob/dev/libsquoosh/src/codecs.ts [codecs.js]: https://github.com/GoogleChromeLabs/squoosh/blob/dev/libsquoosh/src/codecs.js
[butteraugli]: https://github.com/google/butteraugli [butteraugli]: https://github.com/google/butteraugli
[readfile]: https://nodejs.org/api/fs.html#fs_fspromises_readfile_path_options [readfile]: https://nodejs.org/api/fs.html#fs_fspromises_readfile_path_options

View File

@@ -1,49 +0,0 @@
/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { promises as fs } from 'fs';
import { basename } from 'path';
const defaultOpts = {
prefix: 'chunk-url',
};
export default function chunkPlugin(opts) {
opts = { ...defaultOpts, ...opts };
const prefix = opts.prefix + ':';
return {
name: 'chunk-plugin',
async resolveId(id, importer) {
if (!id.startsWith(prefix)) return;
const realId = id.slice(prefix.length);
const resolveResult = await this.resolve(realId, importer);
if (!resolveResult) {
throw Error(`Cannot find ${realId}`);
}
return prefix + resolveResult.id;
},
async load(id) {
if (!id.startsWith(prefix)) return;
const realId = id.slice(prefix.length);
const source = await fs.readFile(realId);
this.addWatchFile(realId);
return `export default import.meta.ROLLUP_FILE_URL_${this.emitFile({
type: 'chunk',
source,
id: realId,
})}`;
},
};
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "@squoosh/lib", "name": "@squoosh/lib",
"version": "0.4.0", "version": "0.2.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -1060,21 +1060,6 @@
"to-fast-properties": "^2.0.0" "to-fast-properties": "^2.0.0"
} }
}, },
"@cspotcode/source-map-consumer": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
"dev": true
},
"@cspotcode/source-map-support": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz",
"integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==",
"dev": true,
"requires": {
"@cspotcode/source-map-consumer": "0.8.0"
}
},
"@rollup/plugin-babel": { "@rollup/plugin-babel": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz",
@@ -1133,30 +1118,6 @@
"picomatch": "^2.2.2" "picomatch": "^2.2.2"
} }
}, },
"@tsconfig/node10": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
"dev": true
},
"@tsconfig/node12": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==",
"dev": true
},
"@tsconfig/node14": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
"dev": true
},
"@tsconfig/node16": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
"dev": true
},
"@types/estree": { "@types/estree": {
"version": "0.0.39", "version": "0.0.39",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
@@ -1178,18 +1139,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"acorn": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
"integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==",
"dev": true
},
"acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
"dev": true
},
"ansi-styles": { "ansi-styles": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@@ -1199,12 +1148,6 @@
"color-convert": "^1.9.0" "color-convert": "^1.9.0"
} }
}, },
"arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"babel-plugin-dynamic-import-node": { "babel-plugin-dynamic-import-node": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz",
@@ -1378,12 +1321,6 @@
} }
} }
}, },
"create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"debug": { "debug": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
@@ -1408,18 +1345,6 @@
"object-keys": "^1.0.12" "object-keys": "^1.0.12"
} }
}, },
"dequal": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
"integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==",
"dev": true
},
"diff": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
"dev": true
},
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.725", "version": "1.3.725",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.725.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.725.tgz",
@@ -1622,12 +1547,6 @@
"minimist": "^1.2.5" "minimist": "^1.2.5"
} }
}, },
"kleur": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
"integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==",
"dev": true
},
"lodash.debounce": { "lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -1643,12 +1562,6 @@
"sourcemap-codec": "^1.4.4" "sourcemap-codec": "^1.4.4"
} }
}, },
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"merge-stream": { "merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -1670,12 +1583,6 @@
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true "dev": true
}, },
"mri": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.1.6.tgz",
"integrity": "sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ==",
"dev": true
},
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -1840,15 +1747,6 @@
"terser": "^5.0.0" "terser": "^5.0.0"
} }
}, },
"sade": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz",
"integrity": "sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==",
"dev": true,
"requires": {
"mri": "^1.1.0"
}
},
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -1934,40 +1832,6 @@
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
"dev": true "dev": true
}, },
"totalist": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-2.0.0.tgz",
"integrity": "sha512-+Y17F0YzxfACxTyjfhnJQEe7afPA0GSpYlFkl2VFMxYP7jshQf9gXV7cH47EfToBumFThfKBvfAcoUn6fdNeRQ==",
"dev": true
},
"ts-node": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz",
"integrity": "sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==",
"dev": true,
"requires": {
"@cspotcode/source-map-support": "0.6.1",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"yn": "3.1.1"
},
"dependencies": {
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
}
}
},
"typescript": { "typescript": {
"version": "4.3.2", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.2.tgz",
@@ -2002,24 +1866,6 @@
"integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==",
"dev": true "dev": true
}, },
"uvu": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.1.tgz",
"integrity": "sha512-JGxttnOGDFs77FaZ0yMUHIzczzQ5R1IlDeNW6Wymw6gAscwMdAffVOP6TlxLIfReZyK8tahoGwWZaTCJzNFDkg==",
"dev": true,
"requires": {
"dequal": "^2.0.0",
"diff": "^5.0.0",
"kleur": "^4.0.3",
"sade": "^1.7.3",
"totalist": "^2.0.0"
}
},
"wasm-feature-detect": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz",
"integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w=="
},
"web-streams-polyfill": { "web-streams-polyfill": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.0.3.tgz", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.0.3.tgz",
@@ -2039,12 +1885,6 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true "dev": true
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
} }
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@squoosh/lib", "name": "@squoosh/lib",
"version": "0.4.0", "version": "0.2.3",
"description": "A Node library for Squoosh", "description": "A Node library for Squoosh",
"public": true, "public": true,
"main": "./build/index.js", "main": "./build/index.js",
@@ -8,22 +8,12 @@
"/build/*" "/build/*"
], ],
"scripts": { "scripts": {
"build": "rollup -c", "build": "rollup -c"
"test": "uvu -r ts-node/register test"
}, },
"keywords": [], "keywords": [],
"author": "Google Chrome Developers <chromium-dev@google.com>", "author": "Google Chrome Developers <chromium-dev@google.com>",
"homepage": "https://github.com/GoogleChromeLabs/squoosh",
"repository": {
"type": "git",
"url": "https://github.com/GoogleChromeLabs/squoosh.git"
},
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": {
"node": " ^12.5.0 || ^14.0.0 || ^16.0.0 "
},
"dependencies": { "dependencies": {
"wasm-feature-detect": "^1.2.11",
"web-streams-polyfill": "^3.0.3" "web-streams-polyfill": "^3.0.3"
}, },
"devDependencies": { "devDependencies": {
@@ -35,9 +25,7 @@
"@types/node": "^15.6.1", "@types/node": "^15.6.1",
"rollup": "^2.46.0", "rollup": "^2.46.0",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"ts-node": "^10.2.1",
"typescript": "^4.1.3", "typescript": "^4.1.3",
"uvu": "^0.5.1",
"which": "^2.0.2" "which": "^2.0.2"
} }
} }

View File

@@ -2,7 +2,6 @@ import resolve from '@rollup/plugin-node-resolve';
import cjs from '@rollup/plugin-commonjs'; import cjs from '@rollup/plugin-commonjs';
import simpleTS from './lib/simple-ts'; import simpleTS from './lib/simple-ts';
import asset from './lib/asset-plugin.js'; import asset from './lib/asset-plugin.js';
import chunk from './lib/chunk-plugin.js';
import json from './lib/json-plugin.js'; import json from './lib/json-plugin.js';
import autojson from './lib/autojson-plugin.js'; import autojson from './lib/autojson-plugin.js';
import { getBabelOutputPlugin } from '@rollup/plugin-babel'; import { getBabelOutputPlugin } from '@rollup/plugin-babel';
@@ -10,7 +9,7 @@ import { builtinModules } from 'module';
/** @type {import('rollup').RollupOptions} */ /** @type {import('rollup').RollupOptions} */
export default { export default {
input: 'src/index.ts', input: 'src/index.js',
output: { output: {
dir: 'build', dir: 'build',
format: 'cjs', format: 'cjs',
@@ -19,7 +18,6 @@ export default {
plugins: [ plugins: [
resolve(), resolve(),
cjs(), cjs(),
chunk(),
asset(), asset(),
autojson(), autojson(),
json(), json(),

View File

@@ -1,132 +0,0 @@
/**
* WebAssembly definitions are not available in `@types/node` yet,
* so these are copied from `lib.dom.d.ts`
*/
declare namespace WebAssembly {
interface CompileError {}
var CompileError: {
prototype: CompileError;
new (): CompileError;
};
interface Global {
value: any;
valueOf(): any;
}
var Global: {
prototype: Global;
new (descriptor: GlobalDescriptor, v?: any): Global;
};
interface Instance {
readonly exports: Exports;
}
var Instance: {
prototype: Instance;
new (module: Module, importObject?: Imports): Instance;
};
interface LinkError {}
var LinkError: {
prototype: LinkError;
new (): LinkError;
};
interface Memory {
readonly buffer: ArrayBuffer;
grow(delta: number): number;
}
var Memory: {
prototype: Memory;
new (descriptor: MemoryDescriptor): Memory;
};
interface Module {}
var Module: {
prototype: Module;
new (bytes: BufferSource): Module;
customSections(moduleObject: Module, sectionName: string): ArrayBuffer[];
exports(moduleObject: Module): ModuleExportDescriptor[];
imports(moduleObject: Module): ModuleImportDescriptor[];
};
interface RuntimeError {}
var RuntimeError: {
prototype: RuntimeError;
new (): RuntimeError;
};
interface Table {
readonly length: number;
get(index: number): Function | null;
grow(delta: number): number;
set(index: number, value: Function | null): void;
}
var Table: {
prototype: Table;
new (descriptor: TableDescriptor): Table;
};
interface GlobalDescriptor {
mutable?: boolean;
value: ValueType;
}
interface MemoryDescriptor {
initial: number;
maximum?: number;
}
interface ModuleExportDescriptor {
kind: ImportExportKind;
name: string;
}
interface ModuleImportDescriptor {
kind: ImportExportKind;
module: string;
name: string;
}
interface TableDescriptor {
element: TableKind;
initial: number;
maximum?: number;
}
interface WebAssemblyInstantiatedSource {
instance: Instance;
module: Module;
}
type ImportExportKind = 'function' | 'global' | 'memory' | 'table';
type TableKind = 'anyfunc';
type ValueType = 'f32' | 'f64' | 'i32' | 'i64';
type ExportValue = Function | Global | Memory | Table;
type Exports = Record<string, ExportValue>;
type ImportValue = ExportValue | number;
type ModuleImports = Record<string, ImportValue>;
type Imports = Record<string, ModuleImports>;
function compile(bytes: BufferSource): Promise<Module>;
// `compileStreaming` does not exist in NodeJS
// function compileStreaming(source: Response | Promise<Response>): Promise<Module>;
function instantiate(
bytes: BufferSource,
importObject?: Imports,
): Promise<WebAssemblyInstantiatedSource>;
function instantiate(
moduleObject: Module,
importObject?: Imports,
): Promise<Instance>;
// `instantiateStreaming` does not exist in NodeJS
// function instantiateStreaming(response: Response | PromiseLike<Response>, importObject?: Imports): Promise<WebAssemblyInstantiatedSource>;
function validate(bytes: BufferSource): boolean;
}

View File

@@ -2,39 +2,6 @@ import { instantiateEmscriptenWasm } from './emscripten-utils.js';
import visdif from '../../codecs/visdif/visdif.js'; import visdif from '../../codecs/visdif/visdif.js';
import visdifWasm from 'asset-url:../../codecs/visdif/visdif.wasm'; import visdifWasm from 'asset-url:../../codecs/visdif/visdif.wasm';
import type ImageData from './image_data';
interface VisDiff {
distance: (data: Uint8ClampedArray) => number;
delete: () => void;
}
interface VisdiffConstructor {
new (data: Uint8ClampedArray, width: number, height: number): VisDiff;
}
interface VisDiffModule extends EmscriptenWasm.Module {
VisDiff: VisdiffConstructor;
}
type VisDiffModuleFactory = EmscriptenWasm.ModuleFactory<VisDiffModule>;
interface BinarySearchParams {
min?: number;
max?: number;
epsilon?: number;
maxRounds?: number;
}
interface AutoOptimizeParams extends BinarySearchParams {
butteraugliDistanceGoal?: number;
}
interface AutoOptimizeResult {
bitmap: ImageData;
binary: Uint8Array;
quality: number;
}
// `measure` is a (async) function that takes exactly one numeric parameter and // `measure` is a (async) function that takes exactly one numeric parameter and
// returns a value. The function is assumed to be monotonic (an increase in `parameter` // returns a value. The function is assumed to be monotonic (an increase in `parameter`
@@ -42,9 +9,9 @@ interface AutoOptimizeResult {
// to find `parameter` such that `measure` returns `measureGoal`, within an error // to find `parameter` such that `measure` returns `measureGoal`, within an error
// of `epsilon`. It will use at most `maxRounds` attempts. // of `epsilon`. It will use at most `maxRounds` attempts.
export async function binarySearch( export async function binarySearch(
measureGoal: number, measureGoal,
measure: (val: number) => Promise<number>, measure,
{ min = 0, max = 100, epsilon = 0.1, maxRounds = 6 }: BinarySearchParams = {}, { min = 0, max = 100, epsilon = 0.1, maxRounds = 8 } = {},
) { ) {
let parameter = (max - min) / 2 + min; let parameter = (max - min) / 2 + min;
let delta = (max - min) / 4; let delta = (max - min) / 4;
@@ -66,21 +33,12 @@ export async function binarySearch(
} }
export async function autoOptimize( export async function autoOptimize(
bitmapIn: ImageData, bitmapIn,
encode: ( encode,
bitmap: ImageData, decode,
quality: number, { butteraugliDistanceGoal = 1.4, ...otherOpts } = {},
) => Promise<Uint8Array> | Uint8Array, ) {
decode: (binary: Uint8Array) => Promise<ImageData> | ImageData, const { VisDiff } = await instantiateEmscriptenWasm(visdif, visdifWasm);
{
butteraugliDistanceGoal = 1.4,
...binarySearchParams
}: AutoOptimizeParams = {},
): Promise<AutoOptimizeResult> {
const { VisDiff } = await instantiateEmscriptenWasm(
visdif as VisDiffModuleFactory,
visdifWasm,
);
const comparator = new VisDiff( const comparator = new VisDiff(
bitmapIn.data, bitmapIn.data,
@@ -88,11 +46,8 @@ export async function autoOptimize(
bitmapIn.height, bitmapIn.height,
); );
// We're able to do non null assertion because let bitmapOut;
// we know that binarySearch will set these values let binaryOut;
let bitmapOut!: ImageData;
let binaryOut!: Uint8Array;
// Increasing quality means _decrease_ in Butteraugli distance. // Increasing quality means _decrease_ in Butteraugli distance.
// `binarySearch` assumes that increasing `parameter` will // `binarySearch` assumes that increasing `parameter` will
// increase the metric value. So multipliy Butteraugli values by -1. // increase the metric value. So multipliy Butteraugli values by -1.
@@ -103,7 +58,7 @@ export async function autoOptimize(
bitmapOut = await decode(binaryOut); bitmapOut = await decode(binaryOut);
return -1 * comparator.distance(bitmapOut.data); return -1 * comparator.distance(bitmapOut.data);
}, },
binarySearchParams, otherOpts,
); );
comparator.delete(); comparator.delete();

View File

@@ -1,84 +1,31 @@
import { promises as fsp } from 'fs'; import { promises as fsp } from 'fs';
import { instantiateEmscriptenWasm, pathify } from './emscripten-utils.js'; import { instantiateEmscriptenWasm, pathify } from './emscripten-utils.js';
import { threads } from 'wasm-feature-detect';
import { cpus } from 'os';
// We use `navigator.hardwareConcurrency` for Emscriptens pthread pool size.
// This is the only workaround I can get working without crying.
(globalThis as any).navigator = {
hardwareConcurrency: cpus().length,
};
interface DecodeModule extends EmscriptenWasm.Module {
decode: (data: Uint8Array) => ImageData;
}
type DecodeModuleFactory = EmscriptenWasm.ModuleFactory<DecodeModule>;
interface RotateModuleInstance {
exports: {
memory: WebAssembly.Memory;
rotate(width: number, height: number, rotate: number): void;
};
}
interface ResizeWithAspectParams {
input_width: number;
input_height: number;
target_width: number;
target_height: number;
}
interface ResizeInstantiateOptions {
width: number;
height: number;
method: string;
premultiply: boolean;
linearRGB: boolean;
}
declare global {
// Needed for being able to use ImageData as type in codec types
type ImageData = import('./image_data.js').default;
// Needed for being able to assign to `globalThis.ImageData`
var ImageData: ImageData['constructor'];
}
import type { QuantizerModule } from '../../codecs/imagequant/imagequant.js';
// MozJPEG // MozJPEG
import type { MozJPEGModule as MozJPEGEncodeModule } from '../../codecs/mozjpeg/enc/mozjpeg_enc';
import mozEnc from '../../codecs/mozjpeg/enc/mozjpeg_node_enc.js'; import mozEnc from '../../codecs/mozjpeg/enc/mozjpeg_node_enc.js';
import mozEncWasm from 'asset-url:../../codecs/mozjpeg/enc/mozjpeg_node_enc.wasm'; import mozEncWasm from 'asset-url:../../codecs/mozjpeg/enc/mozjpeg_node_enc.wasm';
import mozDec from '../../codecs/mozjpeg/dec/mozjpeg_node_dec.js'; import mozDec from '../../codecs/mozjpeg/dec/mozjpeg_node_dec.js';
import mozDecWasm from 'asset-url:../../codecs/mozjpeg/dec/mozjpeg_node_dec.wasm'; import mozDecWasm from 'asset-url:../../codecs/mozjpeg/dec/mozjpeg_node_dec.wasm';
// WebP // WebP
import type { WebPModule as WebPEncodeModule } from '../../codecs/webp/enc/webp_enc';
import webpEnc from '../../codecs/webp/enc/webp_node_enc.js'; import webpEnc from '../../codecs/webp/enc/webp_node_enc.js';
import webpEncWasm from 'asset-url:../../codecs/webp/enc/webp_node_enc.wasm'; import webpEncWasm from 'asset-url:../../codecs/webp/enc/webp_node_enc.wasm';
import webpDec from '../../codecs/webp/dec/webp_node_dec.js'; import webpDec from '../../codecs/webp/dec/webp_node_dec.js';
import webpDecWasm from 'asset-url:../../codecs/webp/dec/webp_node_dec.wasm'; import webpDecWasm from 'asset-url:../../codecs/webp/dec/webp_node_dec.wasm';
// AVIF // AVIF
import type { AVIFModule as AVIFEncodeModule } from '../../codecs/avif/enc/avif_enc';
import avifEnc from '../../codecs/avif/enc/avif_node_enc.js'; import avifEnc from '../../codecs/avif/enc/avif_node_enc.js';
import avifEncWasm from 'asset-url:../../codecs/avif/enc/avif_node_enc.wasm'; import avifEncWasm from 'asset-url:../../codecs/avif/enc/avif_node_enc.wasm';
import avifEncMt from '../../codecs/avif/enc/avif_node_enc_mt.js';
import avifEncMtWorker from 'chunk-url:../../codecs/avif/enc/avif_node_enc_mt.worker.js';
import avifEncMtWasm from 'asset-url:../../codecs/avif/enc/avif_node_enc_mt.wasm';
import avifDec from '../../codecs/avif/dec/avif_node_dec.js'; import avifDec from '../../codecs/avif/dec/avif_node_dec.js';
import avifDecWasm from 'asset-url:../../codecs/avif/dec/avif_node_dec.wasm'; import avifDecWasm from 'asset-url:../../codecs/avif/dec/avif_node_dec.wasm';
// JXL // JXL
import type { JXLModule as JXLEncodeModule } from '../../codecs/jxl/enc/jxl_enc';
import jxlEnc from '../../codecs/jxl/enc/jxl_node_enc.js'; import jxlEnc from '../../codecs/jxl/enc/jxl_node_enc.js';
import jxlEncWasm from 'asset-url:../../codecs/jxl/enc/jxl_node_enc.wasm'; import jxlEncWasm from 'asset-url:../../codecs/jxl/enc/jxl_node_enc.wasm';
import jxlDec from '../../codecs/jxl/dec/jxl_node_dec.js'; import jxlDec from '../../codecs/jxl/dec/jxl_node_dec.js';
import jxlDecWasm from 'asset-url:../../codecs/jxl/dec/jxl_node_dec.wasm'; import jxlDecWasm from 'asset-url:../../codecs/jxl/dec/jxl_node_dec.wasm';
// WP2 // WP2
import type { WP2Module as WP2EncodeModule } from '../../codecs/wp2/enc/wp2_enc';
import wp2Enc from '../../codecs/wp2/enc/wp2_node_enc.js'; import wp2Enc from '../../codecs/wp2/enc/wp2_node_enc.js';
import wp2EncWasm from 'asset-url:../../codecs/wp2/enc/wp2_node_enc.wasm'; import wp2EncWasm from 'asset-url:../../codecs/wp2/enc/wp2_node_enc.wasm';
import wp2Dec from '../../codecs/wp2/dec/wp2_node_dec.js'; import wp2Dec from '../../codecs/wp2/dec/wp2_node_dec.js';
@@ -104,22 +51,16 @@ const resizePromise = resize.default(fsp.readFile(pathify(resizeWasm)));
// rotate // rotate
import rotateWasm from 'asset-url:../../codecs/rotate/rotate.wasm'; import rotateWasm from 'asset-url:../../codecs/rotate/rotate.wasm';
// TODO(ergunsh): Type definitions of some modules do not exist
// Figure out creating type definitions for them and remove `allowJs` rule
// We shouldn't need to use Promise<QuantizerModule> below after getting type definitions for imageQuant
// ImageQuant // ImageQuant
import imageQuant from '../../codecs/imagequant/imagequant_node.js'; import imageQuant from '../../codecs/imagequant/imagequant_node.js';
import imageQuantWasm from 'asset-url:../../codecs/imagequant/imagequant_node.wasm'; import imageQuantWasm from 'asset-url:../../codecs/imagequant/imagequant_node.wasm';
const imageQuantPromise: Promise<QuantizerModule> = instantiateEmscriptenWasm( const imageQuantPromise = instantiateEmscriptenWasm(imageQuant, imageQuantWasm);
imageQuant,
imageQuantWasm,
);
// Our decoders currently rely on a `ImageData` global. // Our decoders currently rely on a `ImageData` global.
import ImageData from './image_data.js'; import ImageData from './image_data.js';
globalThis.ImageData = ImageData; globalThis.ImageData = ImageData;
function resizeNameToIndex(name: string) { function resizeNameToIndex(name) {
switch (name) { switch (name) {
case 'triangle': case 'triangle':
return 0; return 0;
@@ -139,26 +80,25 @@ function resizeWithAspect({
input_height, input_height,
target_width, target_width,
target_height, target_height,
}: ResizeWithAspectParams): { width: number; height: number } { }) {
if (!target_width && !target_height) { if (!target_width && !target_height) {
throw Error('Need to specify at least width or height when resizing'); throw Error('Need to specify at least width or height when resizing');
} }
if (target_width && target_height) { if (target_width && target_height) {
return { width: target_width, height: target_height }; return { width: target_width, height: target_height };
} }
if (!target_width) { if (!target_width) {
return { return {
width: Math.round((input_width / input_height) * target_height), width: Math.round((input_width / input_height) * target_height),
height: target_height, height: target_height,
}; };
} }
if (!target_height) {
return { return {
width: target_width, width: target_width,
height: Math.round((input_height / input_width) * target_width), height: Math.round((input_height / input_width) * target_width),
}; };
}
} }
export const preprocessors = { export const preprocessors = {
@@ -168,16 +108,10 @@ export const preprocessors = {
instantiate: async () => { instantiate: async () => {
await resizePromise; await resizePromise;
return ( return (
buffer: Uint8Array, buffer,
input_width: number, input_width,
input_height: number, input_height,
{ { width, height, method, premultiply, linearRGB },
width,
height,
method,
premultiply,
linearRGB,
}: ResizeInstantiateOptions,
) => { ) => {
({ width, height } = resizeWithAspect({ ({ width, height } = resizeWithAspect({
input_width, input_width,
@@ -214,12 +148,7 @@ export const preprocessors = {
description: 'Reduce the number of colors used (aka. paletting)', description: 'Reduce the number of colors used (aka. paletting)',
instantiate: async () => { instantiate: async () => {
const imageQuant = await imageQuantPromise; const imageQuant = await imageQuantPromise;
return ( return (buffer, width, height, { numColors, dither }) =>
buffer: Uint8Array,
width: number,
height: number,
{ numColors, dither }: { numColors: number; dither: number },
) =>
new ImageData( new ImageData(
imageQuant.quantize(buffer, width, height, numColors, dither), imageQuant.quantize(buffer, width, height, numColors, dither),
width, width,
@@ -235,18 +164,13 @@ export const preprocessors = {
name: 'Rotate', name: 'Rotate',
description: 'Rotate image', description: 'Rotate image',
instantiate: async () => { instantiate: async () => {
return async ( return async (buffer, width, height, { numRotations }) => {
buffer: Uint8Array,
width: number,
height: number,
{ numRotations }: { numRotations: number },
) => {
const degrees = (numRotations * 90) % 360; const degrees = (numRotations * 90) % 360;
const sameDimensions = degrees == 0 || degrees == 180; const sameDimensions = degrees == 0 || degrees == 180;
const size = width * height * 4; const size = width * height * 4;
const instance = ( const { instance } = await WebAssembly.instantiate(
await WebAssembly.instantiate(await fsp.readFile(pathify(rotateWasm))) await fsp.readFile(pathify(rotateWasm)),
).instance as RotateModuleInstance; );
const { memory } = instance.exports; const { memory } = instance.exports;
const additionalPagesNeeded = Math.ceil( const additionalPagesNeeded = Math.ceil(
(size * 2 - memory.buffer.byteLength + 8) / (64 * 1024), (size * 2 - memory.buffer.byteLength + 8) / (64 * 1024),
@@ -268,20 +192,15 @@ export const preprocessors = {
numRotations: 0, numRotations: 0,
}, },
}, },
} as const; };
export const codecs = { export const codecs = {
mozjpeg: { mozjpeg: {
name: 'MozJPEG', name: 'MozJPEG',
extension: 'jpg', extension: 'jpg',
detectors: [/^\xFF\xD8\xFF/], detectors: [/^\xFF\xD8\xFF/],
dec: () => dec: () => instantiateEmscriptenWasm(mozDec, mozDecWasm),
instantiateEmscriptenWasm(mozDec as DecodeModuleFactory, mozDecWasm), enc: () => instantiateEmscriptenWasm(mozEnc, mozEncWasm),
enc: () =>
instantiateEmscriptenWasm(
mozEnc as EmscriptenWasm.ModuleFactory<MozJPEGEncodeModule>,
mozEncWasm,
),
defaultEncoderOptions: { defaultEncoderOptions: {
quality: 75, quality: 75,
baseline: false, baseline: false,
@@ -309,14 +228,9 @@ export const codecs = {
webp: { webp: {
name: 'WebP', name: 'WebP',
extension: 'webp', extension: 'webp',
detectors: [/^RIFF....WEBPVP8[LX ]/s], detectors: [/^RIFF....WEBPVP8[LX ]/],
dec: () => dec: () => instantiateEmscriptenWasm(webpDec, webpDecWasm),
instantiateEmscriptenWasm(webpDec as DecodeModuleFactory, webpDecWasm), enc: () => instantiateEmscriptenWasm(webpEnc, webpEncWasm),
enc: () =>
instantiateEmscriptenWasm(
webpEnc as EmscriptenWasm.ModuleFactory<WebPEncodeModule>,
webpEncWasm,
),
defaultEncoderOptions: { defaultEncoderOptions: {
quality: 75, quality: 75,
target_size: 0, target_size: 0,
@@ -356,21 +270,8 @@ export const codecs = {
name: 'AVIF', name: 'AVIF',
extension: 'avif', extension: 'avif',
detectors: [/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/], detectors: [/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/],
dec: () => dec: () => instantiateEmscriptenWasm(avifDec, avifDecWasm),
instantiateEmscriptenWasm(avifDec as DecodeModuleFactory, avifDecWasm), enc: () => instantiateEmscriptenWasm(avifEnc, avifEncWasm),
enc: async () => {
if (await threads()) {
return instantiateEmscriptenWasm(
avifEncMt as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
avifEncMtWasm,
avifEncMtWorker,
);
}
return instantiateEmscriptenWasm(
avifEnc as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
avifEncWasm,
);
},
defaultEncoderOptions: { defaultEncoderOptions: {
cqLevel: 33, cqLevel: 33,
cqAlphaLevel: -1, cqAlphaLevel: -1,
@@ -385,21 +286,16 @@ export const codecs = {
}, },
autoOptimize: { autoOptimize: {
option: 'cqLevel', option: 'cqLevel',
min: 62, min: 0,
max: 0, max: 62,
}, },
}, },
jxl: { jxl: {
name: 'JPEG-XL', name: 'JPEG-XL',
extension: 'jxl', extension: 'jxl',
detectors: [/^\xff\x0a/], detectors: [/^\xff\x0a/],
dec: () => dec: () => instantiateEmscriptenWasm(jxlDec, jxlDecWasm),
instantiateEmscriptenWasm(jxlDec as DecodeModuleFactory, jxlDecWasm), enc: () => instantiateEmscriptenWasm(jxlEnc, jxlEncWasm),
enc: () =>
instantiateEmscriptenWasm(
jxlEnc as EmscriptenWasm.ModuleFactory<JXLEncodeModule>,
jxlEncWasm,
),
defaultEncoderOptions: { defaultEncoderOptions: {
speed: 4, speed: 4,
quality: 75, quality: 75,
@@ -419,13 +315,8 @@ export const codecs = {
name: 'WebP2', name: 'WebP2',
extension: 'wp2', extension: 'wp2',
detectors: [/^\xF4\xFF\x6F/], detectors: [/^\xF4\xFF\x6F/],
dec: () => dec: () => instantiateEmscriptenWasm(wp2Dec, wp2DecWasm),
instantiateEmscriptenWasm(wp2Dec as DecodeModuleFactory, wp2DecWasm), enc: () => instantiateEmscriptenWasm(wp2Enc, wp2EncWasm),
enc: () =>
instantiateEmscriptenWasm(
wp2Enc as EmscriptenWasm.ModuleFactory<WP2EncodeModule>,
wp2EncWasm,
),
defaultEncoderOptions: { defaultEncoderOptions: {
quality: 75, quality: 75,
alpha_quality: 75, alpha_quality: 75,
@@ -455,18 +346,13 @@ export const codecs = {
await pngEncDecPromise; await pngEncDecPromise;
await oxipngPromise; await oxipngPromise;
return { return {
encode: ( encode: (buffer, width, height, opts) => {
buffer: Uint8ClampedArray | ArrayBuffer,
width: number,
height: number,
opts: { level: number },
) => {
const simplePng = pngEncDec.encode( const simplePng = pngEncDec.encode(
new Uint8Array(buffer), new Uint8Array(buffer),
width, width,
height, height,
); );
return oxipng.optimise(simplePng, opts.level, false); return oxipng.optimise(simplePng, opts.level);
}, },
}; };
}, },
@@ -479,4 +365,4 @@ export const codecs = {
max: 1, max: 1,
}, },
}, },
} as const; };

View File

@@ -0,0 +1,16 @@
import { fileURLToPath } from 'url';
export function pathify(path) {
if (path.startsWith('file://')) {
path = fileURLToPath(path);
}
return path;
}
export function instantiateEmscriptenWasm(factory, path) {
return factory({
locateFile() {
return pathify(path);
},
});
}

View File

@@ -1,26 +0,0 @@
import { fileURLToPath, URL } from 'url';
export function pathify(path: string): string {
if (path.startsWith('file://')) {
path = fileURLToPath(path);
}
return path;
}
export function instantiateEmscriptenWasm<T extends EmscriptenWasm.Module>(
factory: EmscriptenWasm.ModuleFactory<T>,
path: string,
workerJS: string = '',
): Promise<T> {
return factory({
locateFile(requestPath) {
// The glue code generated by emscripten uses the original
// file names of the worker file and the wasm binary.
// These will have changed in the bundling process and
// we need to inject them here.
if (requestPath.endsWith('.wasm')) return pathify(path);
if (requestPath.endsWith('.worker.js')) return pathify(workerJS);
return requestPath;
},
});
}

View File

@@ -5,18 +5,10 @@ import { promises as fsp } from 'fs';
import { codecs as encoders, preprocessors } from './codecs.js'; import { codecs as encoders, preprocessors } from './codecs.js';
import WorkerPool from './worker_pool.js'; import WorkerPool from './worker_pool.js';
import { autoOptimize } from './auto-optimizer.js'; import { autoOptimize } from './auto-optimizer.js';
import type ImageData from './image_data';
export { ImagePool, encoders, preprocessors }; export { ImagePool, encoders, preprocessors };
type EncoderKey = keyof typeof encoders;
type PreprocessorKey = keyof typeof preprocessors;
type FileLike = Buffer | ArrayBuffer | string | ArrayBufferView;
async function decodeFile({ async function decodeFile({ file }) {
file,
}: {
file: FileLike;
}): Promise<{ bitmap: ImageData; size: number }> {
let buffer; let buffer;
if (ArrayBuffer.isView(file)) { if (ArrayBuffer.isView(file)) {
buffer = Buffer.from(file.buffer); buffer = Buffer.from(file.buffer);
@@ -24,9 +16,8 @@ async function decodeFile({
} else if (file instanceof ArrayBuffer) { } else if (file instanceof ArrayBuffer) {
buffer = Buffer.from(file); buffer = Buffer.from(file);
file = 'Binary blob'; file = 'Binary blob';
} else if ((file as unknown) instanceof Buffer) { } else if (file instanceof Buffer) {
// TODO: Check why we need type assertions here. buffer = file;
buffer = (file as unknown) as Buffer;
file = 'Binary blob'; file = 'Binary blob';
} else if (typeof file === 'string') { } else if (typeof file === 'string') {
buffer = await fsp.readFile(file); buffer = await fsp.readFile(file);
@@ -37,33 +28,23 @@ async function decodeFile({
const firstChunkString = Array.from(firstChunk) const firstChunkString = Array.from(firstChunk)
.map((v) => String.fromCodePoint(v)) .map((v) => String.fromCodePoint(v))
.join(''); .join('');
const key = Object.entries(encoders).find(([_name, { detectors }]) => const key = Object.entries(encoders).find(([name, { detectors }]) =>
detectors.some((detector) => detector.exec(firstChunkString)), detectors.some((detector) => detector.exec(firstChunkString)),
)?.[0] as EncoderKey | undefined; )?.[0];
if (!key) { if (!key) {
throw Error(`${file} has an unsupported format`); throw Error(`${file} has an unsupported format`);
} }
const encoder = encoders[key]; const rgba = (await encoders[key].dec()).decode(new Uint8Array(buffer));
const mod = await encoder.dec();
const rgba = mod.decode(new Uint8Array(buffer));
return { return {
bitmap: rgba, bitmap: rgba,
size: buffer.length, size: buffer.length,
}; };
} }
async function preprocessImage({ async function preprocessImage({ preprocessorName, options, image }) {
preprocessorName,
options,
image,
}: {
preprocessorName: PreprocessorKey;
options: any;
image: { bitmap: ImageData };
}) {
const preprocessor = await preprocessors[preprocessorName].instantiate(); const preprocessor = await preprocessors[preprocessorName].instantiate();
image.bitmap = await preprocessor( image.bitmap = await preprocessor(
Uint8Array.from(image.bitmap.data), image.bitmap.data,
image.bitmap.width, image.bitmap.width,
image.bitmap.height, image.bitmap.height,
options, options,
@@ -77,39 +58,26 @@ async function encodeImage({
encConfig, encConfig,
optimizerButteraugliTarget, optimizerButteraugliTarget,
maxOptimizerRounds, maxOptimizerRounds,
}: {
bitmap: ImageData;
encName: EncoderKey;
encConfig: any;
optimizerButteraugliTarget: number;
maxOptimizerRounds: number;
}) { }) {
let binary: Uint8Array; let binary;
let optionsUsed = encConfig; let optionsUsed = encConfig;
const encoder = await encoders[encName].enc(); const encoder = await encoders[encName].enc();
if (encConfig === 'auto') { if (encConfig === 'auto') {
const optionToOptimize = encoders[encName].autoOptimize.option; const optionToOptimize = encoders[encName].autoOptimize.option;
const decoder = await encoders[encName].dec(); const decoder = await encoders[encName].dec();
const encode = (bitmapIn: ImageData, quality: number) => const encode = (bitmapIn, quality) =>
encoder.encode( encoder.encode(
bitmapIn.data, bitmapIn.data,
bitmapIn.width, bitmapIn.width,
bitmapIn.height, bitmapIn.height,
Object.assign({}, encoders[encName].defaultEncoderOptions as any, { Object.assign({}, encoders[encName].defaultEncoderOptions, {
[optionToOptimize]: quality, [optionToOptimize]: quality,
}), }),
); );
const decode = (binary: Uint8Array) => decoder.decode(binary); const decode = (binary) => decoder.decode(binary);
const nonNullEncode = (bitmap: ImageData, quality: number): Uint8Array => {
const result = encode(bitmap, quality);
if (!result) {
throw new Error('There was an error while encoding');
}
return result;
};
const { binary: optimizedBinary, quality } = await autoOptimize( const { binary: optimizedBinary, quality } = await autoOptimize(
bitmapIn, bitmapIn,
nonNullEncode, encode,
decode, decode,
{ {
min: encoders[encName].autoOptimize.min, min: encoders[encName].autoOptimize.min,
@@ -124,18 +92,12 @@ async function encodeImage({
[optionToOptimize]: Math.round(quality * 10000) / 10000, [optionToOptimize]: Math.round(quality * 10000) / 10000,
}; };
} else { } else {
const result = encoder.encode( binary = encoder.encode(
bitmapIn.data.buffer, bitmapIn.data.buffer,
bitmapIn.width, bitmapIn.width,
bitmapIn.height, bitmapIn.height,
encConfig, encConfig,
); );
if (!result) {
throw new Error('There was an error while encoding');
}
binary = result;
} }
return { return {
optionsUsed, optionsUsed,
@@ -145,15 +107,10 @@ async function encodeImage({
}; };
} }
type EncodeParams = { operation: 'encode' } & Parameters<typeof encodeImage>[0]; // both decoding and encoding go through the worker pool
type DecodeParams = { operation: 'decode' } & Parameters<typeof decodeFile>[0]; function handleJob(params) {
type PreprocessParams = { operation: 'preprocess' } & Parameters< const { operation } = params;
typeof preprocessImage switch (operation) {
>[0];
type JobMessage = EncodeParams | DecodeParams | PreprocessParams;
function handleJob(params: JobMessage) {
switch (params.operation) {
case 'encode': case 'encode':
return encodeImage(params); return encodeImage(params);
case 'decode': case 'decode':
@@ -161,7 +118,7 @@ function handleJob(params: JobMessage) {
case 'preprocess': case 'preprocess':
return preprocessImage(params); return preprocessImage(params);
default: default:
throw Error(`Invalid job "${(params as any).operation}"`); throw Error(`Invalid job "${operation}"`);
} }
} }
@@ -169,12 +126,7 @@ function handleJob(params: JobMessage) {
* Represents an ingested image. * Represents an ingested image.
*/ */
class Image { class Image {
public file: FileLike; constructor(workerPool, file) {
public workerPool: WorkerPool<JobMessage, any>;
public decoded: Promise<{ bitmap: ImageData }>;
public encodedWith: { [key: string]: any };
constructor(workerPool: WorkerPool<JobMessage, any>, file: FileLike) {
this.file = file; this.file = file;
this.workerPool = workerPool; this.workerPool = workerPool;
this.decoded = workerPool.dispatchJob({ operation: 'decode', file }); this.decoded = workerPool.dispatchJob({ operation: 'decode', file });
@@ -191,15 +143,14 @@ class Image {
if (!Object.keys(preprocessors).includes(name)) { if (!Object.keys(preprocessors).includes(name)) {
throw Error(`Invalid preprocessor "${name}"`); throw Error(`Invalid preprocessor "${name}"`);
} }
const preprocessorName = name as PreprocessorKey;
const preprocessorOptions = Object.assign( const preprocessorOptions = Object.assign(
{}, {},
preprocessors[preprocessorName].defaultOptions, preprocessors[name].defaultOptions,
options, options,
); );
this.decoded = this.workerPool.dispatchJob({ this.decoded = this.workerPool.dispatchJob({
operation: 'preprocess', operation: 'preprocess',
preprocessorName, preprocessorName: name,
image: await this.decoded, image: await this.decoded,
options: preprocessorOptions, options: preprocessorOptions,
}); });
@@ -210,22 +161,14 @@ class Image {
/** /**
* Define one or several encoders to use on the image. * Define one or several encoders to use on the image.
* @param {object} encodeOptions - An object with encoders to use, and their settings. * @param {object} encodeOptions - An object with encoders to use, and their settings.
* @returns {Promise<void>} - A promise that resolves when the image has been encoded with all the specified encoders. * @returns {Promise<undefined>} - A promise that resolves when the image has been encoded with all the specified encoders.
*/ */
async encode( async encode(encodeOptions = {}) {
encodeOptions: {
optimizerButteraugliTarget?: number;
maxOptimizerRounds?: number;
} & {
[key in EncoderKey]?: any; // any is okay for now
} = {},
): Promise<void> {
const { bitmap } = await this.decoded; const { bitmap } = await this.decoded;
for (const [name, options] of Object.entries(encodeOptions)) { for (const [encName, options] of Object.entries(encodeOptions)) {
if (!Object.keys(encoders).includes(name)) { if (!Object.keys(encoders).includes(encName)) {
continue; continue;
} }
const encName = name as EncoderKey;
const encRef = encoders[encName]; const encRef = encoders[encName];
const encConfig = const encConfig =
typeof options === 'string' typeof options === 'string'
@@ -237,9 +180,9 @@ class Image {
encName, encName,
encConfig, encConfig,
optimizerButteraugliTarget: Number( optimizerButteraugliTarget: Number(
encodeOptions.optimizerButteraugliTarget ?? 1.4, encodeOptions.optimizerButteraugliTarget,
), ),
maxOptimizerRounds: Number(encodeOptions.maxOptimizerRounds ?? 6), maxOptimizerRounds: Number(encodeOptions.maxOptimizerRounds),
}); });
} }
await Promise.all(Object.values(this.encodedWith)); await Promise.all(Object.values(this.encodedWith));
@@ -250,30 +193,28 @@ class Image {
* A pool where images can be ingested and squooshed. * A pool where images can be ingested and squooshed.
*/ */
class ImagePool { class ImagePool {
public workerPool: WorkerPool<JobMessage, any>;
/** /**
* Create a new pool. * Create a new pool.
* @param {number} [threads] - Number of concurrent image processes to run in the pool. Defaults to the number of CPU cores in the system. * @param {number} [threads] - Number of concurrent image processes to run in the pool. Defaults to the number of CPU cores in the system.
*/ */
constructor(threads: number) { constructor(threads) {
this.workerPool = new WorkerPool(threads || cpus().length, __filename); this.workerPool = new WorkerPool(threads || cpus().length, __filename);
} }
/** /**
* Ingest an image into the image pool. * Ingest an image into the image pool.
* @param {FileLike} image - The image or path to the image that should be ingested and decoded. * @param {string | Buffer | URL | object} image - The image or path to the image that should be ingested and decoded.
* @returns {Image} - A custom class reference to the decoded image. * @returns {Image} - A custom class reference to the decoded image.
*/ */
ingestImage(image: FileLike): Image { ingestImage(image) {
return new Image(this.workerPool, image); return new Image(this.workerPool, image);
} }
/** /**
* Closes the underlying image processing pipeline. The already processed images will still be there, but no new processing can start. * Closes the underlying image processing pipeline. The already processed images will still be there, but no new processing can start.
* @returns {Promise<void>} - A promise that resolves when the underlying pipeline has closed. * @returns {Promise<undefined>} - A promise that resolves when the underlying pipeline has closed.
*/ */
async close(): Promise<void> { async close() {
await this.workerPool.join(); await this.workerPool.join();
} }
} }

View File

@@ -1,43 +0,0 @@
/// <reference path="../../missing-types.d.ts" />
declare module 'asset-url:*' {
const value: string;
export default value;
}
// Somehow TS picks up definitions from the module itself
// instead of using `asset-url:*`. It is probably related to
// specifity of the module declaration and these declarations below fix it
declare module 'asset-url:../../codecs/png/pkg/squoosh_png_bg.wasm' {
const value: string;
export default value;
}
declare module 'asset-url:../../codecs/oxipng/pkg/squoosh_oxipng_bg.wasm' {
const value: string;
export default value;
}
declare module 'asset-url:../../codecs/resize/pkg/squoosh_resize_bg.wasm' {
const value: string;
export default value;
}
declare module 'chunk-url:../../codecs/avif/enc/avif_node_enc_mt.worker.js' {
const value: string;
export default value;
}
// These don't exist in NodeJS types so we're not able to use them but they are referenced in some emscripten and codec types
// Thus, we need to explicitly assign them to be `never`
// We're also not able to use the APIs that use these types
// So, if we want to use those APIs we need to supply its dependencies ourselves
// However, probably those APIs are more suited to be used in web (i.e. there can be other
// dependencies to web APIs that might not work in Node)
type RequestInfo = never;
type Response = never;
type WebGLRenderingContext = never;
type MessageEvent = never;
type BufferSource = ArrayBufferView | ArrayBuffer;
type URL = import('url').URL;

View File

@@ -7,19 +7,26 @@ function uuid() {
).join(''); ).join('');
} }
interface Job<I> { function jobPromise(worker, msg) {
msg: I; return new Promise((resolve, reject) => {
resolve: Function; const id = uuid();
reject: Function; worker.postMessage({ msg, id });
worker.on('message', function f({ error, result, id: rid }) {
if (rid !== id) {
return;
}
if (error) {
reject(error);
return;
}
worker.off('message', f);
resolve(result);
});
});
} }
export default class WorkerPool<I, O> { export default class WorkerPool {
public numWorkers: number; constructor(numWorkers, workerFile) {
public jobQueue: TransformStream<Job<I>, Job<I>>;
public workerQueue: TransformStream<Worker, Worker>;
public done: Promise<void>;
constructor(numWorkers: number, workerFile: string) {
this.numWorkers = numWorkers; this.numWorkers = numWorkers;
this.jobQueue = new TransformStream(); this.jobQueue = new TransformStream();
this.workerQueue = new TransformStream(); this.workerQueue = new TransformStream();
@@ -41,14 +48,9 @@ export default class WorkerPool<I, O> {
await this._terminateAll(); await this._terminateAll();
return; return;
} }
if (!value) {
throw new Error('Reader did not return any value');
}
const { msg, resolve, reject } = value; const { msg, resolve, reject } = value;
const worker = await this._nextWorker(); const worker = await this._nextWorker();
this.jobPromise(worker, msg) jobPromise(worker, msg)
.then((result) => resolve(result)) .then((result) => resolve(result))
.catch((reason) => reject(reason)) .catch((reason) => reject(reason))
.finally(() => { .finally(() => {
@@ -62,12 +64,8 @@ export default class WorkerPool<I, O> {
async _nextWorker() { async _nextWorker() {
const reader = this.workerQueue.readable.getReader(); const reader = this.workerQueue.readable.getReader();
const { value } = await reader.read(); const { value, done } = await reader.read();
reader.releaseLock(); reader.releaseLock();
if (!value) {
throw new Error('No worker left');
}
return value; return value;
} }
@@ -84,7 +82,7 @@ export default class WorkerPool<I, O> {
await this.done; await this.done;
} }
dispatchJob(msg: I): Promise<O> { dispatchJob(msg) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const writer = this.jobQueue.writable.getWriter(); const writer = this.jobQueue.writable.getWriter();
writer.write({ msg, resolve, reject }); writer.write({ msg, resolve, reject });
@@ -92,32 +90,14 @@ export default class WorkerPool<I, O> {
}); });
} }
private jobPromise(worker: Worker, msg: I) { static useThisThreadAsWorker(cb) {
return new Promise((resolve, reject) => { parentPort.on('message', async (data) => {
const id = uuid();
worker.postMessage({ msg, id });
worker.on('message', function f({ error, result, id: rid }) {
if (rid !== id) {
return;
}
if (error) {
reject(error);
return;
}
worker.off('message', f);
resolve(result);
});
});
}
static useThisThreadAsWorker<I, O>(cb: (msg: I) => O) {
parentPort!.on('message', async (data) => {
const { msg, id } = data; const { msg, id } = data;
try { try {
const result = await cb(msg); const result = await cb(msg);
parentPort!.postMessage({ result, id }); parentPort.postMessage({ result, id });
} catch (e) { } catch (e) {
parentPort!.postMessage({ error: e.message, id }); parentPort.postMessage({ error: e.message, id });
} }
}); });
} }

View File

@@ -1,43 +0,0 @@
import * as path from 'path';
import { test } from 'uvu';
import * as assert from 'uvu/assert';
import { ImagePool } from '..';
let imagePool: ImagePool;
test.after.each(async () => {
if (imagePool) {
try {
await imagePool.close();
} catch (e) {}
}
imagePool = undefined;
});
test('smoke test', async () => {
imagePool = new ImagePool(1);
const imagePath = path.resolve(__dirname, '../../icon-large-maskable.png');
const image = imagePool.ingestImage(imagePath);
const { bitmap } = await image.decoded;
assert.equal(bitmap.width, 1024);
await image.preprocess({
resize: {
enabled: true,
width: 100,
},
});
await image.encode({
mozjpeg: {},
});
const { size } = await image.encodedWith.mozjpeg;
// resulting image is 1554b
assert.ok(size > 500);
assert.ok(size < 5000);
});
test.run();

Some files were not shown because too many files have changed in this diff Show More