Compare commits
14 Commits
maudnals-c
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
4c700e3add | |||
583117697e | |||
13a185def2 | |||
918c596cba | |||
42594277fd | |||
97d13de273 | |||
a1c3304c56 | |||
7a2fc917c1 | |||
1c4233b5ae | |||
4d67ec5a7b | |||
c24505230d | |||
2238d1abaf | |||
65ea02627b | |||
18b53e2b8a |
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -1,2 +1,2 @@
|
||||
/codecs/**/*.js linguist-generated=true
|
||||
/codecs/*/pkg*/*.d.ts linguist-generated=true
|
||||
/codecs/**/*.js linguist-generated -diff
|
||||
/codecs/*/pkg*/*.d.ts linguist-generated
|
||||
|
@ -2,10 +2,6 @@
|
||||
|
||||
[Squoosh] is an image compression web app that reduces image sizes through numerous formats.
|
||||
|
||||
# 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.
|
||||
|
||||
# Privacy
|
||||
|
||||
Squoosh does not send your image to a server. All image compression processes locally.
|
||||
|
3
cli/.gitignore
vendored
3
cli/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
node_modules
|
||||
build
|
||||
.DS_Store
|
@ -1 +0,0 @@
|
||||
node_modules
|
@ -1,59 +0,0 @@
|
||||
# Squoosh CLI
|
||||
|
||||
Squoosh CLI is an _experimental_ way to run all the codecs you know from the [Squoosh] web app on your command line using WebAssembly. The Squoosh CLI uses a worker pool to parallelize processing images. This way you can apply the same codec to many images at once.
|
||||
|
||||
Squoosh CLI is currently not the fastest image compression tool in town and doesn’t aim to be. It is, however, fast enough to compress many images sufficiently quick at once.
|
||||
|
||||
## Installation
|
||||
|
||||
The Squoosh CLI can be used straight from the command line without installing using `npx`:
|
||||
|
||||
```
|
||||
$ npx @squoosh/cli <options...>
|
||||
```
|
||||
|
||||
Of course, you can also install the Squoosh CLI:
|
||||
|
||||
```
|
||||
$ npm i -g @squoosh/cli
|
||||
$ squoosh-cli <options...>
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
Usage: squoosh-cli [options] <files...>
|
||||
|
||||
Options:
|
||||
-V, --version output the version number
|
||||
-d, --output-dir <dir> Output directory (default: ".")
|
||||
-s, --suffix <suffix> Append suffix to output files (default: "")
|
||||
--max-optimizer-rounds <rounds> Maximum number of compressions to use for auto optimizations (default: "6")
|
||||
--optimizer-butteraugli-target <butteraugli distance> Target Butteraugli distance for auto optimizer (default: "1.4")
|
||||
--resize [config] Resize the image before compressing
|
||||
--quant [config] Reduce the number of colors used (aka. paletting)
|
||||
--rotate [config] Rotate image
|
||||
--mozjpeg [config] Use MozJPEG to generate a .jpg file with the given configuration
|
||||
--webp [config] Use WebP to generate a .webp file with the given configuration
|
||||
--avif [config] Use AVIF to generate a .avif file with the given configuration
|
||||
--jxl [config] Use JPEG-XL to generate a .jxl file with the given configuration
|
||||
--wp2 [config] Use WebP2 to generate a .wp2 file with the given configuration
|
||||
--oxipng [config] Use OxiPNG to generate a .png file with the given configuration
|
||||
-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._
|
||||
|
||||
## Auto optimizer
|
||||
|
||||
Squoosh CLI has an _experimental_ auto optimizer that compresses an image as much as possible, trying to hit a specific [Butteraugli] target value. The higher the Butteraugli target value, the more artifacts can be introduced.
|
||||
|
||||
You can make use of the auto optimizer by using “auto” as the config object.
|
||||
|
||||
```
|
||||
$ npx @squoosh/cli --wp2 auto test.png
|
||||
```
|
||||
|
||||
[squoosh]: https://squoosh.app
|
||||
[codecs.ts]: https://github.com/GoogleChromeLabs/squoosh/blob/dev/libsquoosh/src/codecs.ts
|
||||
[butteraugli]: https://github.com/google/butteraugli
|
613
cli/package-lock.json
generated
613
cli/package-lock.json
generated
@ -1,613 +0,0 @@
|
||||
{
|
||||
"name": "@squoosh/cli",
|
||||
"version": "0.7.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@squoosh/cli",
|
||||
"version": "0.7.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@squoosh/lib": "^0.4.0",
|
||||
"commander": "^7.2.0",
|
||||
"json5": "^2.2.0",
|
||||
"kleur": "^4.1.4",
|
||||
"ora": "^5.4.0"
|
||||
},
|
||||
"bin": {
|
||||
"cli": "src/index.js",
|
||||
"squoosh-cli": "src/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": " ^12.20.2 || ^14.13.1 || ^16.0.0 "
|
||||
}
|
||||
},
|
||||
"node_modules/@squoosh/lib": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@squoosh/lib/-/lib-0.4.0.tgz",
|
||||
"integrity": "sha512-O1LyugWLZjMI4JZeZMA5vzfhfPjfMZXH5/HmVkRagP8B70wH3uoR7tjxfGNdSavey357MwL8YJDxbGwBBdHp7Q==",
|
||||
"dependencies": {
|
||||
"wasm-feature-detect": "^1.2.11",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": " ^12.5.0 || ^14.0.0 || ^16.0.0 "
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
|
||||
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
|
||||
"dependencies": {
|
||||
"restore-cursor": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-spinners": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.0.tgz",
|
||||
"integrity": "sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/clone": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
|
||||
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/defaults": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
|
||||
"integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
|
||||
"dependencies": {
|
||||
"clone": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/is-interactive": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
|
||||
"integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-unicode-supported": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
|
||||
"integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||
"integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"is-unicode-supported": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-fn": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
|
||||
"dependencies": {
|
||||
"mimic-fn": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/ora": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.0.tgz",
|
||||
"integrity": "sha512-1StwyXQGoU6gdjYkyVcqOLnVlbKj+6yPNNOxJVgpt9t4eksKjiriiHuxktLYkgllwk+D6MbC4ihH84L1udRXPg==",
|
||||
"dependencies": {
|
||||
"bl": "^4.1.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cli-cursor": "^3.1.0",
|
||||
"cli-spinners": "^2.5.0",
|
||||
"is-interactive": "^1.0.0",
|
||||
"is-unicode-supported": "^0.1.0",
|
||||
"log-symbols": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wcwidth": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/restore-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
||||
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
|
||||
"dependencies": {
|
||||
"onetime": "^5.1.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
||||
"integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
|
||||
"dependencies": {
|
||||
"defaults": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.0.3.tgz",
|
||||
"integrity": "sha512-d2H/t0eqRNM4w2WvmTdoeIvzAUSpK7JmATB8Nr2lb7nQ9BTIJVjbQ/TRFVEh2gUH1HwclPdoPtfMoFfetXaZnA==",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@squoosh/lib": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@squoosh/lib/-/lib-0.4.0.tgz",
|
||||
"integrity": "sha512-O1LyugWLZjMI4JZeZMA5vzfhfPjfMZXH5/HmVkRagP8B70wH3uoR7tjxfGNdSavey357MwL8YJDxbGwBBdHp7Q==",
|
||||
"requires": {
|
||||
"wasm-feature-detect": "^1.2.11",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"requires": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"requires": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
|
||||
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
|
||||
"requires": {
|
||||
"restore-cursor": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"cli-spinners": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.0.tgz",
|
||||
"integrity": "sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q=="
|
||||
},
|
||||
"clone": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
|
||||
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="
|
||||
},
|
||||
"defaults": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
|
||||
"integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
|
||||
"requires": {
|
||||
"clone": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"is-interactive": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
|
||||
"integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="
|
||||
},
|
||||
"is-unicode-supported": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
|
||||
"requires": {
|
||||
"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=="
|
||||
},
|
||||
"log-symbols": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||
"integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"is-unicode-supported": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"mimic-fn": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
|
||||
"requires": {
|
||||
"mimic-fn": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"ora": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.0.tgz",
|
||||
"integrity": "sha512-1StwyXQGoU6gdjYkyVcqOLnVlbKj+6yPNNOxJVgpt9t4eksKjiriiHuxktLYkgllwk+D6MbC4ihH84L1udRXPg==",
|
||||
"requires": {
|
||||
"bl": "^4.1.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cli-cursor": "^3.1.0",
|
||||
"cli-spinners": "^2.5.0",
|
||||
"is-interactive": "^1.0.0",
|
||||
"is-unicode-supported": "^0.1.0",
|
||||
"log-symbols": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wcwidth": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"restore-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
||||
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
|
||||
"requires": {
|
||||
"onetime": "^5.1.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
||||
"integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
|
||||
"requires": {
|
||||
"defaults": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"web-streams-polyfill": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.0.3.tgz",
|
||||
"integrity": "sha512-d2H/t0eqRNM4w2WvmTdoeIvzAUSpK7JmATB8Nr2lb7nQ9BTIJVjbQ/TRFVEh2gUH1HwclPdoPtfMoFfetXaZnA=="
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "@squoosh/cli",
|
||||
"version": "0.7.2",
|
||||
"description": "A CLI for Squoosh",
|
||||
"public": true,
|
||||
"type": "module",
|
||||
"homepage": "https://github.com/GoogleChromeLabs/squoosh",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/GoogleChromeLabs/squoosh.git"
|
||||
},
|
||||
"bin": {
|
||||
"squoosh-cli": "src/index.js",
|
||||
"@squoosh/cli": "src/index.js"
|
||||
},
|
||||
"files": [
|
||||
"/src/index.js"
|
||||
],
|
||||
"keywords": [],
|
||||
"author": "Google Chrome Developers <chromium-dev@google.com>",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": " ^12.20.2 || ^14.13.1 || ^16.0.0 "
|
||||
},
|
||||
"dependencies": {
|
||||
"@squoosh/lib": "^0.4.0",
|
||||
"commander": "^7.2.0",
|
||||
"json5": "^2.2.0",
|
||||
"kleur": "^4.1.4",
|
||||
"ora": "^5.4.0"
|
||||
}
|
||||
}
|
236
cli/src/index.js
236
cli/src/index.js
@ -1,236 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { program } from 'commander/esm.mjs';
|
||||
import JSON5 from 'json5';
|
||||
import path from 'path';
|
||||
import { promises as fsp } from 'fs';
|
||||
import { cpus } from 'os';
|
||||
import ora from 'ora';
|
||||
import kleur from 'kleur';
|
||||
|
||||
import { ImagePool, preprocessors, encoders } from '@squoosh/lib';
|
||||
|
||||
function clamp(v, min, max) {
|
||||
if (v < min) return min;
|
||||
if (v > max) return max;
|
||||
return v;
|
||||
}
|
||||
|
||||
const suffix = ['B', 'KB', 'MB'];
|
||||
function prettyPrintSize(size) {
|
||||
const base = Math.floor(Math.log2(size) / 10);
|
||||
const index = clamp(base, 0, 2);
|
||||
return (size / 2 ** (10 * index)).toFixed(2) + suffix[index];
|
||||
}
|
||||
|
||||
function progressTracker(results) {
|
||||
const spinner = ora();
|
||||
const tracker = {};
|
||||
tracker.spinner = spinner;
|
||||
tracker.progressOffset = 0;
|
||||
tracker.totalOffset = 0;
|
||||
let status = '';
|
||||
tracker.setStatus = (text) => {
|
||||
status = text || '';
|
||||
update();
|
||||
};
|
||||
let progress = '';
|
||||
tracker.setProgress = (done, total) => {
|
||||
spinner.prefixText = kleur.dim(`${done}/${total}`);
|
||||
const completeness =
|
||||
(tracker.progressOffset + done) / (tracker.totalOffset + total);
|
||||
progress = kleur.cyan(
|
||||
`▐${'▨'.repeat((completeness * 10) | 0).padEnd(10, '╌')}▌ `,
|
||||
);
|
||||
update();
|
||||
};
|
||||
function update() {
|
||||
spinner.text = progress + kleur.bold(status) + getResultsText();
|
||||
}
|
||||
tracker.finish = (text) => {
|
||||
spinner.succeed(kleur.bold(text) + getResultsText());
|
||||
};
|
||||
function getResultsText() {
|
||||
let out = '';
|
||||
for (const result of results.values()) {
|
||||
out += `\n ${kleur.cyan(result.file)}: ${prettyPrintSize(result.size)}`;
|
||||
for (const { outputFile, size: outputSize, infoText } of result.outputs) {
|
||||
out += `\n ${kleur.dim('└')} ${kleur.cyan(
|
||||
outputFile.padEnd(5),
|
||||
)} → ${prettyPrintSize(outputSize)}`;
|
||||
const percent = ((outputSize / result.size) * 100).toPrecision(3);
|
||||
out += ` (${kleur[outputSize > result.size ? 'red' : 'green'](
|
||||
percent + '%',
|
||||
)})`;
|
||||
if (infoText) out += kleur.yellow(infoText);
|
||||
}
|
||||
}
|
||||
return out || '\n';
|
||||
}
|
||||
spinner.start();
|
||||
return tracker;
|
||||
}
|
||||
|
||||
async function getInputFiles(paths) {
|
||||
const validFiles = [];
|
||||
|
||||
for (const inputPath of paths) {
|
||||
const files = (await fsp.lstat(inputPath)).isDirectory()
|
||||
? (await fsp.readdir(inputPath, { withFileTypes: true }))
|
||||
.filter((dirent) => dirent.isFile())
|
||||
.map((dirent) => path.join(inputPath, dirent.name))
|
||||
: [inputPath];
|
||||
for (const file of files) {
|
||||
try {
|
||||
await fsp.stat(file);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.warn(
|
||||
`Warning: Input file does not exist: ${path.resolve(file)}`,
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
validFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
return validFiles;
|
||||
}
|
||||
|
||||
async function processFiles(files) {
|
||||
files = await getInputFiles(files);
|
||||
|
||||
const imagePool = new ImagePool(cpus().length);
|
||||
|
||||
const results = new Map();
|
||||
const progress = progressTracker(results);
|
||||
|
||||
progress.setStatus('Decoding...');
|
||||
progress.totalOffset = files.length;
|
||||
progress.setProgress(0, files.length);
|
||||
|
||||
// Create output directory
|
||||
await fsp.mkdir(program.opts().outputDir, { recursive: true });
|
||||
|
||||
let decoded = 0;
|
||||
let decodedFiles = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const buffer = await fsp.readFile(file);
|
||||
const image = imagePool.ingestImage(buffer);
|
||||
await image.decoded;
|
||||
results.set(image, {
|
||||
file,
|
||||
size: (await image.decoded).size,
|
||||
outputs: [],
|
||||
});
|
||||
progress.setProgress(++decoded, files.length);
|
||||
return image;
|
||||
}),
|
||||
);
|
||||
|
||||
const preprocessOptions = {};
|
||||
|
||||
for (const preprocessorName of Object.keys(preprocessors)) {
|
||||
if (!program.opts()[preprocessorName]) {
|
||||
continue;
|
||||
}
|
||||
preprocessOptions[preprocessorName] = JSON5.parse(
|
||||
program.opts()[preprocessorName],
|
||||
);
|
||||
}
|
||||
|
||||
for (const image of decodedFiles) {
|
||||
image.preprocess(preprocessOptions);
|
||||
}
|
||||
|
||||
await Promise.all(decodedFiles.map((image) => image.decoded));
|
||||
|
||||
progress.progressOffset = decoded;
|
||||
progress.setStatus(
|
||||
'Encoding ' + kleur.dim(`(${imagePool.workerPool.numWorkers} threads)`),
|
||||
);
|
||||
progress.setProgress(0, files.length);
|
||||
|
||||
const jobs = [];
|
||||
let jobsStarted = 0;
|
||||
let jobsFinished = 0;
|
||||
for (const image of decodedFiles) {
|
||||
const originalFile = results.get(image).file;
|
||||
|
||||
const encodeOptions = {
|
||||
optimizerButteraugliTarget: Number(
|
||||
program.opts().optimizerButteraugliTarget,
|
||||
),
|
||||
maxOptimizerRounds: Number(program.opts().maxOptimizerRounds),
|
||||
};
|
||||
for (const encName of Object.keys(encoders)) {
|
||||
if (!program.opts()[encName]) {
|
||||
continue;
|
||||
}
|
||||
const encParam = program.opts()[encName];
|
||||
const encConfig =
|
||||
encParam.toLowerCase() === 'auto' ? 'auto' : JSON5.parse(encParam);
|
||||
encodeOptions[encName] = encConfig;
|
||||
}
|
||||
jobsStarted++;
|
||||
const job = image.encode(encodeOptions).then(async () => {
|
||||
jobsFinished++;
|
||||
const outputPath = path.join(
|
||||
program.opts().outputDir,
|
||||
path.basename(originalFile, path.extname(originalFile)) +
|
||||
program.opts().suffix,
|
||||
);
|
||||
for (const output of Object.values(image.encodedWith)) {
|
||||
const outputFile = `${outputPath}.${(await output).extension}`;
|
||||
await fsp.writeFile(outputFile, (await output).binary);
|
||||
results
|
||||
.get(image)
|
||||
.outputs.push(Object.assign(await output, { outputFile }));
|
||||
}
|
||||
progress.setProgress(jobsFinished, jobsStarted);
|
||||
});
|
||||
jobs.push(job);
|
||||
}
|
||||
|
||||
// update the progress to account for multi-format
|
||||
progress.setProgress(jobsFinished, jobsStarted);
|
||||
// Wait for all jobs to finish
|
||||
await Promise.all(jobs);
|
||||
await imagePool.close();
|
||||
progress.finish('Squoosh results:');
|
||||
}
|
||||
|
||||
program
|
||||
.name('squoosh-cli')
|
||||
.arguments('<files...>')
|
||||
.option('-d, --output-dir <dir>', 'Output directory', '.')
|
||||
.option('-s, --suffix <suffix>', 'Append suffix to output files', '')
|
||||
.option(
|
||||
'--max-optimizer-rounds <rounds>',
|
||||
'Maximum number of compressions to use for auto optimizations',
|
||||
'6',
|
||||
)
|
||||
.option(
|
||||
'--optimizer-butteraugli-target <butteraugli distance>',
|
||||
'Target Butteraugli distance for auto optimizer',
|
||||
'1.4',
|
||||
)
|
||||
.action(processFiles);
|
||||
|
||||
// Create a CLI option for each supported preprocessor
|
||||
for (const [key, value] of Object.entries(preprocessors)) {
|
||||
program.option(`--${key} [config]`, value.description);
|
||||
}
|
||||
// Create a CLI option for each supported encoder
|
||||
for (const [key, value] of Object.entries(encoders)) {
|
||||
program.option(
|
||||
`--${key} [config]`,
|
||||
`Use ${value.name} to generate a .${value.extension} file with the given configuration`,
|
||||
);
|
||||
}
|
||||
|
||||
program.parse(process.argv);
|
@ -14,6 +14,7 @@
|
||||
// for comlink
|
||||
"src/features/**/worker/**/*",
|
||||
"src/features-worker/**/*",
|
||||
"src/features/worker-utils/**/*"
|
||||
"src/features/worker-utils/**/*",
|
||||
"src/worker-shared/**/*"
|
||||
]
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
CODEC_URL = https://github.com/libjxl/libjxl.git
|
||||
CODEC_VERSION = v0.5
|
||||
CODEC_VERSION = 9f544641ec83f6abd9da598bdd08178ee8a003e0
|
||||
CODEC_DIR = node_modules/jxl
|
||||
CODEC_BUILD_ROOT := $(CODEC_DIR)/build
|
||||
CODEC_MT_BUILD_DIR := $(CODEC_BUILD_ROOT)/mt
|
||||
@ -75,6 +75,9 @@ $(CODEC_MT_SIMD_BUILD_DIR)/Makefile: CXXFLAGS+=-msimd128
|
||||
-DCMAKE_CROSSCOMPILING_EMULATOR=node \
|
||||
-B $(@D) \
|
||||
$(<D)
|
||||
emcc -Wall -O3 -o $(CODEC_DIR)/third_party/skcms/skcms.cc.o -I$(CODEC_DIR)/third_party/skcms -c $(CODEC_DIR)/third_party/skcms/skcms.cc
|
||||
llvm-ar rc $(CODEC_BUILD_DIR)/third_party/libskcms.a $(CODEC_DIR)/third_party/skcms/skcms.cc.o
|
||||
rm $(CODEC_DIR)/third_party/skcms/skcms.cc.o
|
||||
|
||||
$(CODEC_DIR)/CMakeLists.txt:
|
||||
$(RM) -r $(@D)
|
||||
|
2
codecs/jxl/dec/jxl_dec.js
generated
2
codecs/jxl/dec/jxl_dec.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/jxl/dec/jxl_node_dec.js
generated
2
codecs/jxl/dec/jxl_node_dec.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -4,6 +4,7 @@
|
||||
#include "lib/jxl/base/thread_pool_internal.h"
|
||||
#include "lib/jxl/enc_external_image.h"
|
||||
#include "lib/jxl/enc_file.h"
|
||||
#include "lib/jxl/enc_color_management.h"
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
@ -17,6 +18,7 @@ struct JXLOptions {
|
||||
bool lossyPalette;
|
||||
size_t decodingSpeedTier;
|
||||
float photonNoiseIso;
|
||||
bool lossyModular;
|
||||
};
|
||||
|
||||
val encode(std::string image, int width, int height, JXLOptions options) {
|
||||
@ -50,11 +52,16 @@ val encode(std::string image, int width, int height, JXLOptions options) {
|
||||
float quality = options.quality;
|
||||
|
||||
// Quality settings roughly match libjpeg qualities.
|
||||
if (quality < 7 || quality == 100) {
|
||||
if (options.lossyModular || quality == 100) {
|
||||
cparams.modular_mode = true;
|
||||
// Internal modular quality to roughly match VarDCT size.
|
||||
cparams.quality_pair.first = cparams.quality_pair.second =
|
||||
std::min(35 + (quality - 7) * 3.0f, 100.0f);
|
||||
if (quality < 7) {
|
||||
cparams.quality_pair.first = cparams.quality_pair.second =
|
||||
std::min(35 + (quality - 7) * 3.0f, 100.0f);
|
||||
} else {
|
||||
cparams.quality_pair.first = cparams.quality_pair.second =
|
||||
std::min(35 + (quality - 7) * 65.f / 93.f, 100.0f);
|
||||
}
|
||||
} else {
|
||||
cparams.modular_mode = false;
|
||||
if (quality >= 30) {
|
||||
@ -91,14 +98,14 @@ val encode(std::string image, int width, int height, JXLOptions options) {
|
||||
jxl::Span<const uint8_t>(reinterpret_cast<const uint8_t*>(image.data()), image.size()), width,
|
||||
height, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*has_alpha=*/true,
|
||||
/*alpha_is_premultiplied=*/false, /*bits_per_sample=*/8, /*endiannes=*/JXL_LITTLE_ENDIAN,
|
||||
/*flipped_y=*/false, pool_ptr, main);
|
||||
/*flipped_y=*/false, pool_ptr, main, /*(only true if bits_per_sample==32) float_in=*/false);
|
||||
|
||||
if (!result) {
|
||||
return val::null();
|
||||
}
|
||||
|
||||
auto js_result = val::null();
|
||||
if (EncodeFile(cparams, &io, &passes_enc_state, &bytes, /*aux=*/nullptr, pool_ptr)) {
|
||||
if (EncodeFile(cparams, &io, &passes_enc_state, &bytes, jxl::GetJxlCms(), /*aux=*/nullptr, pool_ptr)) {
|
||||
js_result = Uint8Array.new_(typed_memory_view(bytes.size(), bytes.data()));
|
||||
}
|
||||
|
||||
@ -113,6 +120,7 @@ EMSCRIPTEN_BINDINGS(my_module) {
|
||||
.field("lossyPalette", &JXLOptions::lossyPalette)
|
||||
.field("decodingSpeedTier", &JXLOptions::decodingSpeedTier)
|
||||
.field("photonNoiseIso", &JXLOptions::photonNoiseIso)
|
||||
.field("lossyModular", &JXLOptions::lossyModular)
|
||||
.field("epf", &JXLOptions::epf);
|
||||
|
||||
function("encode", &encode);
|
||||
|
1
codecs/jxl/enc/jxl_enc.d.ts
vendored
1
codecs/jxl/enc/jxl_enc.d.ts
vendored
@ -6,6 +6,7 @@ export interface EncodeOptions {
|
||||
lossyPalette: boolean;
|
||||
decodingSpeedTier: number;
|
||||
photonNoiseIso: number;
|
||||
lossyModular: boolean;
|
||||
}
|
||||
|
||||
export interface JXLModule extends EmscriptenWasm.Module {
|
||||
|
2
codecs/jxl/enc/jxl_enc.js
generated
2
codecs/jxl/enc/jxl_enc.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/jxl/enc/jxl_enc_mt.js
generated
2
codecs/jxl/enc/jxl_enc_mt.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/jxl/enc/jxl_enc_mt_simd.js
generated
2
codecs/jxl/enc/jxl_enc_mt_simd.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/jxl/enc/jxl_node_enc.js
generated
2
codecs/jxl/enc/jxl_node_enc.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
4
codecs/oxipng/Cargo.lock
generated
4
codecs/oxipng/Cargo.lock
generated
@ -40,9 +40,9 @@ checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.4.0"
|
||||
version = "3.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
|
||||
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
|
@ -17,7 +17,8 @@
|
||||
"static-build/*": ["src/static-build/*"],
|
||||
"client/*": ["src/client/*"],
|
||||
"shared/*": ["src/shared/*"],
|
||||
"features/*": ["src/features/*"]
|
||||
"features/*": ["src/features/*"],
|
||||
"worker-shared/*": ["src/worker-shared/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
3
libsquoosh/.gitignore
vendored
3
libsquoosh/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
node_modules
|
||||
build
|
||||
.DS_Store
|
@ -1 +0,0 @@
|
||||
node_modules
|
@ -1,174 +0,0 @@
|
||||
# libSquoosh
|
||||
|
||||
libSquoosh is an _experimental_ way to run all the codecs you know from the [Squoosh] web app directly inside your own JavaScript program. libSquoosh uses a worker pool to parallelize processing images. This way you can apply the same codec to many images at once.
|
||||
|
||||
libSquoosh is currently not the fastest image compression tool in town and doesn’t aim to be. It is, however, fast enough to compress many images sufficiently quick at once.
|
||||
|
||||
## Installation
|
||||
|
||||
libSquoosh can be installed to your local project with the following command:
|
||||
|
||||
```
|
||||
$ npm install @squoosh/lib
|
||||
```
|
||||
|
||||
You can start using the libSquoosh by adding these lines to the top of your JS program:
|
||||
|
||||
```js
|
||||
import { ImagePool } from '@squoosh/lib';
|
||||
import { cpus } from 'os';
|
||||
const imagePool = new ImagePool(cpus().length);
|
||||
```
|
||||
|
||||
This will create an image pool with an underlying processing pipeline that you can use to ingest and encode images. The ImagePool constructor takes one argument that defines how many parallel operations it is allowed to run at any given time.
|
||||
|
||||
:warning: Important! Make sure to only create 1 `ImagePool` when performing parallel image processing. If you create multiple pools, the `ImagePool` can run out of memory and crash. By reusing a single `ImagePool`, you can ensure that the backing worker queue and processing pipeline releases memory prior to processing the next image.
|
||||
|
||||
## Ingesting images
|
||||
|
||||
You can ingest a new image like so:
|
||||
|
||||
```js
|
||||
import fs from 'fs/promises';
|
||||
const file = await fs.readFile('./path/to/image.png');
|
||||
const image = imagePool.ingestImage(file);
|
||||
```
|
||||
|
||||
The `ingestImage` function can accept any [`ArrayBuffer`][arraybuffer] whether that is from `readFile()` or `fetch()`.
|
||||
|
||||
The returned `image` object is a representation of the original image, that you can now preprocess, encode, and extract information about.
|
||||
|
||||
## Preprocessing and encoding images
|
||||
|
||||
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
|
||||
await image.decoded; //Wait until the image is decoded before running preprocessors.
|
||||
|
||||
const preprocessOptions = {
|
||||
//When both width and height are specified, the image resized to specified size.
|
||||
resize: {
|
||||
enabled: true,
|
||||
width: 100,
|
||||
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);
|
||||
|
||||
const encodeOptions = {
|
||||
mozjpeg: {}, //an empty object means 'use default settings'
|
||||
jxl: {
|
||||
quality: 90,
|
||||
},
|
||||
};
|
||||
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._
|
||||
|
||||
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).
|
||||
|
||||
## Closing the ImagePool
|
||||
|
||||
When you have encoded everything you need, it is recommended to close the processing pipeline in the ImagePool. This will not delete the images you have already encoded, but it will prevent you from ingesting and encoding new images.
|
||||
|
||||
Close the ImagePool pipeline with this line:
|
||||
|
||||
```js
|
||||
await imagePool.close();
|
||||
```
|
||||
|
||||
## Writing encoded images to the file system
|
||||
|
||||
When you have encoded an image, you normally want to write it to a file.
|
||||
|
||||
This example takes an image that has been encoded as a `jpg` and writes it to a file:
|
||||
|
||||
```js
|
||||
const rawEncodedImage = (await image.encodedWith.mozjpeg).binary;
|
||||
|
||||
fs.writeFile('/path/to/new/image.jpg', rawEncodedImage);
|
||||
```
|
||||
|
||||
This example iterates through all encoded versions of the image and writes them to a specific path:
|
||||
|
||||
```js
|
||||
const newImagePath = '/path/to/image.'; //extension is added automatically
|
||||
|
||||
for (const encodedImage of Object.values(image.encodedWith)) {
|
||||
fs.writeFile(
|
||||
newImagePath + (await encodedImage).extension,
|
||||
(await encodedImage).binary,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Extracting image information
|
||||
|
||||
Information about a decoded image is available at `Image.decoded`. It looks something like this:
|
||||
|
||||
```js
|
||||
console.log(await image.decoded);
|
||||
// Returns:
|
||||
{
|
||||
bitmap: {
|
||||
data: Uint8ClampedArray(47736584) [
|
||||
225, 228, 237, 255, 225, 228, 237, 255, 225, 228, 237, 255,
|
||||
225, 228, 237, 255, 225, 228, 237, 255, 225, 228, 237, 255,
|
||||
225, 228, 237, 255,
|
||||
... //the entire raw image
|
||||
],
|
||||
width: 4606, //pixels
|
||||
height: 2591 //pixels
|
||||
},
|
||||
size: 2467795 //bytes
|
||||
}
|
||||
```
|
||||
|
||||
Information about an encoded image can be found at `Image.encodedWith[encoderName]`. It looks something like this:
|
||||
|
||||
```js
|
||||
console.log(await image.encodedWith.jxl);
|
||||
// Returns:
|
||||
{
|
||||
optionsUsed: {
|
||||
quality: 75,
|
||||
baseline: false,
|
||||
arithmetic: false,
|
||||
progressive: true,
|
||||
... //all the possible options for this encoder
|
||||
},
|
||||
binary: Uint8Array(1266975) [
|
||||
1, 0, 0, 1, 0, 1, 0, 0, 255, 219, 0, 132,
|
||||
113, 119, 156, 156, 209, 1, 8, 8, 8, 8, 9, 8,
|
||||
9, 10, 10, 9,
|
||||
... //the entire raw encoded image
|
||||
],
|
||||
extension: 'jxl',
|
||||
size: 1266975 //bytes
|
||||
}
|
||||
```
|
||||
|
||||
## Auto optimizer
|
||||
|
||||
libSquoosh has an _experimental_ auto optimizer that compresses an image as much as possible, trying to hit a specific [Butteraugli] target value. The higher the Butteraugli target value, the more artifacts can be introduced.
|
||||
|
||||
You can make use of the auto optimizer by using “auto” as the config object.
|
||||
|
||||
```js
|
||||
const encodeOptions: {
|
||||
mozjpeg: 'auto',
|
||||
}
|
||||
```
|
||||
|
||||
[squoosh]: https://squoosh.app
|
||||
[codecs.ts]: https://github.com/GoogleChromeLabs/squoosh/blob/dev/libsquoosh/src/codecs.ts
|
||||
[butteraugli]: https://github.com/google/butteraugli
|
||||
[arraybuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
|
@ -1,75 +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: 'asset-url',
|
||||
};
|
||||
|
||||
export default function assetPlugin(opts) {
|
||||
opts = { ...defaultOpts, ...opts };
|
||||
|
||||
/** @type {Map<string, Buffer>} */
|
||||
let assetIdToSourceBuffer;
|
||||
|
||||
const prefix = opts.prefix + ':';
|
||||
return {
|
||||
name: 'asset-plugin',
|
||||
buildStart() {
|
||||
assetIdToSourceBuffer = new Map();
|
||||
},
|
||||
augmentChunkHash(info) {
|
||||
// Get the sources for all assets imported by this chunk.
|
||||
const buffers = Object.keys(info.modules)
|
||||
.map((moduleId) => assetIdToSourceBuffer.get(moduleId))
|
||||
.filter(Boolean);
|
||||
|
||||
if (buffers.length === 0) return;
|
||||
|
||||
for (const moduleId of Object.keys(info.modules)) {
|
||||
const buffer = assetIdToSourceBuffer.get(moduleId);
|
||||
if (buffer) buffers.push(buffer);
|
||||
}
|
||||
|
||||
const combinedBuffer =
|
||||
buffers.length === 1 ? buffers[0] : Buffer.concat(buffers);
|
||||
|
||||
return combinedBuffer;
|
||||
},
|
||||
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}`);
|
||||
}
|
||||
// Add an additional .js to the end so it ends up with .js at the end in the _virtual folder.
|
||||
return prefix + resolveResult.id + '.js';
|
||||
},
|
||||
async load(id) {
|
||||
if (!id.startsWith(prefix)) return;
|
||||
const realId = id.slice(prefix.length, -'.js'.length);
|
||||
const source = await fs.readFile(realId);
|
||||
assetIdToSourceBuffer.set(id, source);
|
||||
this.addWatchFile(realId);
|
||||
|
||||
return `export default import.meta.ROLLUP_FILE_URL_${this.emitFile({
|
||||
type: 'asset',
|
||||
source,
|
||||
name: basename(realId),
|
||||
})}`;
|
||||
},
|
||||
};
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import { promises as fsp } from 'fs';
|
||||
|
||||
export default function autojsonPlugin() {
|
||||
return {
|
||||
name: 'autojson-plugin',
|
||||
async load(id) {
|
||||
if (id.endsWith('.json') && !id.startsWith('json:')) {
|
||||
return 'export default ' + (await fsp.readFile(id, 'utf8'));
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
@ -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,
|
||||
})}`;
|
||||
},
|
||||
};
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import { promises as fsp } from 'fs';
|
||||
|
||||
const prefix = 'json:';
|
||||
|
||||
const reservedKeys = ['public'];
|
||||
|
||||
export default function jsonPlugin() {
|
||||
return {
|
||||
name: 'json-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}`);
|
||||
}
|
||||
// Add an additional .js to the end so it ends up with .js at the end in the _virtual folder.
|
||||
return prefix + resolveResult.id;
|
||||
},
|
||||
async load(id) {
|
||||
if (!id.startsWith(prefix)) return;
|
||||
const realId = id.slice(prefix.length);
|
||||
const source = await fsp.readFile(realId, 'utf8');
|
||||
|
||||
let code = '';
|
||||
for (const [key, value] of Object.entries(JSON.parse(source))) {
|
||||
if (reservedKeys.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
code += `
|
||||
export const ${key} = ${JSON.stringify(value)};
|
||||
`;
|
||||
}
|
||||
return code;
|
||||
},
|
||||
};
|
||||
}
|
@ -1,133 +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 { spawn } from 'child_process';
|
||||
import { relative, join } from 'path';
|
||||
import { promises as fsp } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import glob from 'glob';
|
||||
import { sync as whichSync } from 'which';
|
||||
|
||||
const globP = promisify(glob);
|
||||
|
||||
const tscPath = whichSync('tsc');
|
||||
|
||||
const extRe = /\.tsx?$/;
|
||||
|
||||
function loadConfig(mainPath) {
|
||||
const fileName = ts.findConfigFile(mainPath, ts.sys.fileExists);
|
||||
if (!fileName) throw Error('tsconfig not found');
|
||||
const text = ts.sys.readFile(fileName);
|
||||
const loadedConfig = ts.parseConfigFileTextToJson(fileName, text).config;
|
||||
const parsedTsConfig = ts.parseJsonConfigFileContent(
|
||||
loadedConfig,
|
||||
ts.sys,
|
||||
process.cwd(),
|
||||
undefined,
|
||||
fileName,
|
||||
);
|
||||
return parsedTsConfig;
|
||||
}
|
||||
|
||||
export default function simpleTS(mainPath, { noBuild, watch } = {}) {
|
||||
const config = loadConfig(mainPath);
|
||||
const args = ['-b', mainPath];
|
||||
|
||||
let tsBuildDone;
|
||||
|
||||
async function watchBuiltFiles(rollupContext) {
|
||||
const matches = await globP(config.options.outDir + '/**/*.js');
|
||||
for (const match of matches) rollupContext.addWatchFile(match);
|
||||
}
|
||||
|
||||
async function tsBuild(rollupContext) {
|
||||
if (tsBuildDone) {
|
||||
// Watch lists are cleared on each build, so we need to rewatch all the JS files.
|
||||
await watchBuiltFiles(rollupContext);
|
||||
return tsBuildDone;
|
||||
}
|
||||
if (noBuild) {
|
||||
return (tsBuildDone = Promise.resolve());
|
||||
}
|
||||
tsBuildDone = Promise.resolve().then(async () => {
|
||||
await new Promise((resolve) => {
|
||||
const proc = spawn(tscPath, args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
throw Error('TypeScript build failed');
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await watchBuiltFiles(rollupContext);
|
||||
|
||||
if (watch) {
|
||||
tsBuildDone.then(() => {
|
||||
spawn(tscPath, [...args, '--watch', '--preserveWatchOutput'], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return tsBuildDone;
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'simple-ts',
|
||||
resolveId(id, importer) {
|
||||
// If there isn't an importer, it's an entry point, so we don't need to resolve it relative
|
||||
// to something.
|
||||
if (!importer) return null;
|
||||
|
||||
const tsResolve = ts.resolveModuleName(
|
||||
id,
|
||||
importer,
|
||||
config.options,
|
||||
ts.sys,
|
||||
);
|
||||
|
||||
if (
|
||||
// It didn't find anything
|
||||
!tsResolve.resolvedModule ||
|
||||
// Or if it's linking to a definition file, it's something in node_modules,
|
||||
// or something local like css.d.ts
|
||||
tsResolve.resolvedModule.extension === '.d.ts'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return tsResolve.resolvedModule.resolvedFileName;
|
||||
},
|
||||
async load(id) {
|
||||
if (!extRe.test(id)) return null;
|
||||
|
||||
// TypeScript building is deferred until the first TS file load.
|
||||
// This allows prerequisites to happen first,
|
||||
// such as css.d.ts generation in css-plugin.
|
||||
await tsBuild(this);
|
||||
|
||||
// Look for the JS equivalent in the tmp folder
|
||||
const newId = join(
|
||||
config.options.outDir,
|
||||
relative(config.options.rootDir, id),
|
||||
).replace(extRe, '.js');
|
||||
|
||||
return fsp.readFile(newId, { encoding: 'utf8' });
|
||||
},
|
||||
};
|
||||
}
|
3986
libsquoosh/package-lock.json
generated
3986
libsquoosh/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "@squoosh/lib",
|
||||
"version": "0.4.0",
|
||||
"description": "A Node library for Squoosh",
|
||||
"public": true,
|
||||
"main": "./build/index.js",
|
||||
"files": [
|
||||
"/build/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"keywords": [],
|
||||
"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",
|
||||
"engines": {
|
||||
"node": " ^12.5.0 || ^14.0.0 || ^16.0.0 "
|
||||
},
|
||||
"dependencies": {
|
||||
"wasm-feature-detect": "^1.2.11",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.0",
|
||||
"@babel/preset-env": "^7.14.0",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-commonjs": "^18.0.0",
|
||||
"@rollup/plugin-node-resolve": "^11.2.1",
|
||||
"@types/node": "^15.6.1",
|
||||
"rollup": "^2.46.0",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"typescript": "^4.1.3",
|
||||
"which": "^2.0.2"
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import cjs from '@rollup/plugin-commonjs';
|
||||
import simpleTS from './lib/simple-ts';
|
||||
import asset from './lib/asset-plugin.js';
|
||||
import chunk from './lib/chunk-plugin.js';
|
||||
import json from './lib/json-plugin.js';
|
||||
import autojson from './lib/autojson-plugin.js';
|
||||
import { getBabelOutputPlugin } from '@rollup/plugin-babel';
|
||||
import { builtinModules } from 'module';
|
||||
|
||||
/** @type {import('rollup').RollupOptions} */
|
||||
export default {
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
dir: 'build',
|
||||
format: 'cjs',
|
||||
assetFileNames: '[name]-[hash][extname]',
|
||||
},
|
||||
plugins: [
|
||||
resolve(),
|
||||
cjs(),
|
||||
chunk(),
|
||||
asset(),
|
||||
autojson(),
|
||||
json(),
|
||||
simpleTS('.'),
|
||||
getBabelOutputPlugin({
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
minified: process.env.DEBUG != '',
|
||||
comments: true,
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
node: 12,
|
||||
},
|
||||
loose: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
],
|
||||
external: [...builtinModules, 'web-streams-polyfill'],
|
||||
};
|
132
libsquoosh/src/WebAssembly.d.ts
vendored
132
libsquoosh/src/WebAssembly.d.ts
vendored
@ -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;
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
import { instantiateEmscriptenWasm } from './emscripten-utils.js';
|
||||
|
||||
import visdif from '../../codecs/visdif/visdif.js';
|
||||
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
|
||||
// returns a value. The function is assumed to be monotonic (an increase in `parameter`
|
||||
// will result in an increase in the return value. The function uses binary search
|
||||
// to find `parameter` such that `measure` returns `measureGoal`, within an error
|
||||
// of `epsilon`. It will use at most `maxRounds` attempts.
|
||||
export async function binarySearch(
|
||||
measureGoal: number,
|
||||
measure: (val: number) => Promise<number>,
|
||||
{ min = 0, max = 100, epsilon = 0.1, maxRounds = 6 }: BinarySearchParams = {},
|
||||
) {
|
||||
let parameter = (max - min) / 2 + min;
|
||||
let delta = (max - min) / 4;
|
||||
let value;
|
||||
let round = 1;
|
||||
while (true) {
|
||||
value = await measure(parameter);
|
||||
if (Math.abs(value - measureGoal) < epsilon || round >= maxRounds) {
|
||||
return { parameter, round, value };
|
||||
}
|
||||
if (value > measureGoal) {
|
||||
parameter -= delta;
|
||||
} else if (value < measureGoal) {
|
||||
parameter += delta;
|
||||
}
|
||||
delta /= 2;
|
||||
round++;
|
||||
}
|
||||
}
|
||||
|
||||
export async function autoOptimize(
|
||||
bitmapIn: ImageData,
|
||||
encode: (
|
||||
bitmap: ImageData,
|
||||
quality: number,
|
||||
) => Promise<Uint8Array> | Uint8Array,
|
||||
decode: (binary: Uint8Array) => Promise<ImageData> | ImageData,
|
||||
{
|
||||
butteraugliDistanceGoal = 1.4,
|
||||
...binarySearchParams
|
||||
}: AutoOptimizeParams = {},
|
||||
): Promise<AutoOptimizeResult> {
|
||||
const { VisDiff } = await instantiateEmscriptenWasm(
|
||||
visdif as VisDiffModuleFactory,
|
||||
visdifWasm,
|
||||
);
|
||||
|
||||
const comparator = new VisDiff(
|
||||
bitmapIn.data,
|
||||
bitmapIn.width,
|
||||
bitmapIn.height,
|
||||
);
|
||||
|
||||
// We're able to do non null assertion because
|
||||
// we know that binarySearch will set these values
|
||||
let bitmapOut!: ImageData;
|
||||
let binaryOut!: Uint8Array;
|
||||
|
||||
// Increasing quality means _decrease_ in Butteraugli distance.
|
||||
// `binarySearch` assumes that increasing `parameter` will
|
||||
// increase the metric value. So multipliy Butteraugli values by -1.
|
||||
const { parameter } = await binarySearch(
|
||||
-1 * butteraugliDistanceGoal,
|
||||
async (quality) => {
|
||||
binaryOut = await encode(bitmapIn, quality);
|
||||
bitmapOut = await decode(binaryOut);
|
||||
return -1 * comparator.distance(bitmapOut.data);
|
||||
},
|
||||
binarySearchParams,
|
||||
);
|
||||
comparator.delete();
|
||||
|
||||
return {
|
||||
bitmap: bitmapOut,
|
||||
binary: binaryOut,
|
||||
quality: parameter,
|
||||
};
|
||||
}
|
@ -1,502 +0,0 @@
|
||||
import { promises as fsp } from 'fs';
|
||||
import { instantiateEmscriptenWasm, pathify } from './emscripten-utils.js';
|
||||
import { threads } from 'wasm-feature-detect';
|
||||
import { cpus } from 'os';
|
||||
|
||||
// We use `navigator.hardwareConcurrency` for Emscripten’s 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;
|
||||
}
|
||||
|
||||
export interface ResizeOptions {
|
||||
width: number;
|
||||
height: number;
|
||||
method: 'triangle' | 'catrom' | 'mitchell' | 'lanczos3';
|
||||
premultiply: boolean;
|
||||
linearRGB: boolean;
|
||||
}
|
||||
|
||||
export interface QuantOptions {
|
||||
numColors: number;
|
||||
dither: number;
|
||||
}
|
||||
|
||||
export interface RotateOptions {
|
||||
numRotations: number;
|
||||
}
|
||||
|
||||
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
|
||||
import type { MozJPEGModule as MozJPEGEncodeModule } from '../../codecs/mozjpeg/enc/mozjpeg_enc';
|
||||
import mozEnc from '../../codecs/mozjpeg/enc/mozjpeg_node_enc.js';
|
||||
import mozEncWasm from 'asset-url:../../codecs/mozjpeg/enc/mozjpeg_node_enc.wasm';
|
||||
import mozDec from '../../codecs/mozjpeg/dec/mozjpeg_node_dec.js';
|
||||
import mozDecWasm from 'asset-url:../../codecs/mozjpeg/dec/mozjpeg_node_dec.wasm';
|
||||
import type { EncodeOptions as MozJPEGEncodeOptions } from '../../codecs/mozjpeg/enc/mozjpeg_enc';
|
||||
|
||||
// WebP
|
||||
import type { WebPModule as WebPEncodeModule } from '../../codecs/webp/enc/webp_enc';
|
||||
import webpEnc from '../../codecs/webp/enc/webp_node_enc.js';
|
||||
import webpEncWasm from 'asset-url:../../codecs/webp/enc/webp_node_enc.wasm';
|
||||
import webpDec from '../../codecs/webp/dec/webp_node_dec.js';
|
||||
import webpDecWasm from 'asset-url:../../codecs/webp/dec/webp_node_dec.wasm';
|
||||
import type { EncodeOptions as WebPEncodeOptions } from '../../codecs/webp/enc/webp_enc.js';
|
||||
|
||||
// AVIF
|
||||
import type { AVIFModule as AVIFEncodeModule } from '../../codecs/avif/enc/avif_enc';
|
||||
import avifEnc from '../../codecs/avif/enc/avif_node_enc.js';
|
||||
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 avifDecWasm from 'asset-url:../../codecs/avif/dec/avif_node_dec.wasm';
|
||||
import type { EncodeOptions as AvifEncodeOptions } from '../../codecs/avif/enc/avif_enc.js';
|
||||
|
||||
// JXL
|
||||
import type { JXLModule as JXLEncodeModule } from '../../codecs/jxl/enc/jxl_enc';
|
||||
import jxlEnc from '../../codecs/jxl/enc/jxl_node_enc.js';
|
||||
import jxlEncWasm from 'asset-url:../../codecs/jxl/enc/jxl_node_enc.wasm';
|
||||
import jxlDec from '../../codecs/jxl/dec/jxl_node_dec.js';
|
||||
import jxlDecWasm from 'asset-url:../../codecs/jxl/dec/jxl_node_dec.wasm';
|
||||
import type { EncodeOptions as JxlEncodeOptions } from '../../codecs/jxl/enc/jxl_enc.js';
|
||||
|
||||
// WP2
|
||||
import type { WP2Module as WP2EncodeModule } from '../../codecs/wp2/enc/wp2_enc';
|
||||
import wp2Enc from '../../codecs/wp2/enc/wp2_node_enc.js';
|
||||
import wp2EncWasm from 'asset-url:../../codecs/wp2/enc/wp2_node_enc.wasm';
|
||||
import wp2Dec from '../../codecs/wp2/dec/wp2_node_dec.js';
|
||||
import wp2DecWasm from 'asset-url:../../codecs/wp2/dec/wp2_node_dec.wasm';
|
||||
import type { EncodeOptions as WP2EncodeOptions } from '../../codecs/wp2/enc/wp2_enc.js';
|
||||
|
||||
// PNG
|
||||
import * as pngEncDec from '../../codecs/png/pkg/squoosh_png.js';
|
||||
import pngEncDecWasm from 'asset-url:../../codecs/png/pkg/squoosh_png_bg.wasm';
|
||||
const pngEncDecPromise = pngEncDec.default(
|
||||
fsp.readFile(pathify(pngEncDecWasm)),
|
||||
);
|
||||
|
||||
// OxiPNG
|
||||
import * as oxipng from '../../codecs/oxipng/pkg/squoosh_oxipng.js';
|
||||
import oxipngWasm from 'asset-url:../../codecs/oxipng/pkg/squoosh_oxipng_bg.wasm';
|
||||
const oxipngPromise = oxipng.default(fsp.readFile(pathify(oxipngWasm)));
|
||||
interface OxiPngEncodeOptions {
|
||||
level: number;
|
||||
}
|
||||
|
||||
// Resize
|
||||
import * as resize from '../../codecs/resize/pkg/squoosh_resize.js';
|
||||
import resizeWasm from 'asset-url:../../codecs/resize/pkg/squoosh_resize_bg.wasm';
|
||||
const resizePromise = resize.default(fsp.readFile(pathify(resizeWasm)));
|
||||
|
||||
// rotate
|
||||
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
|
||||
import imageQuant from '../../codecs/imagequant/imagequant_node.js';
|
||||
import imageQuantWasm from 'asset-url:../../codecs/imagequant/imagequant_node.wasm';
|
||||
const imageQuantPromise: Promise<QuantizerModule> = instantiateEmscriptenWasm(
|
||||
imageQuant,
|
||||
imageQuantWasm,
|
||||
);
|
||||
|
||||
// Our decoders currently rely on a `ImageData` global.
|
||||
import ImageData from './image_data.js';
|
||||
globalThis.ImageData = ImageData;
|
||||
|
||||
function resizeNameToIndex(name: string) {
|
||||
switch (name) {
|
||||
case 'triangle':
|
||||
return 0;
|
||||
case 'catrom':
|
||||
return 1;
|
||||
case 'mitchell':
|
||||
return 2;
|
||||
case 'lanczos3':
|
||||
return 3;
|
||||
default:
|
||||
throw Error(`Unknown resize algorithm "${name}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function resizeWithAspect({
|
||||
input_width,
|
||||
input_height,
|
||||
target_width,
|
||||
target_height,
|
||||
}: ResizeWithAspectParams): { width: number; height: number } {
|
||||
if (!target_width && !target_height) {
|
||||
throw Error('Need to specify at least width or height when resizing');
|
||||
}
|
||||
|
||||
if (target_width && target_height) {
|
||||
return { width: target_width, height: target_height };
|
||||
}
|
||||
|
||||
if (!target_width) {
|
||||
return {
|
||||
width: Math.round((input_width / input_height) * target_height),
|
||||
height: target_height,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
width: target_width,
|
||||
height: Math.round((input_height / input_width) * target_width),
|
||||
};
|
||||
}
|
||||
|
||||
export const preprocessors = {
|
||||
resize: {
|
||||
name: 'Resize',
|
||||
description: 'Resize the image before compressing',
|
||||
instantiate: async () => {
|
||||
await resizePromise;
|
||||
return (
|
||||
buffer: Uint8Array,
|
||||
input_width: number,
|
||||
input_height: number,
|
||||
{ width, height, method, premultiply, linearRGB }: ResizeOptions,
|
||||
) => {
|
||||
({ width, height } = resizeWithAspect({
|
||||
input_width,
|
||||
input_height,
|
||||
target_width: width,
|
||||
target_height: height,
|
||||
}));
|
||||
return new ImageData(
|
||||
resize.resize(
|
||||
buffer,
|
||||
input_width,
|
||||
input_height,
|
||||
width,
|
||||
height,
|
||||
resizeNameToIndex(method),
|
||||
premultiply,
|
||||
linearRGB,
|
||||
),
|
||||
width,
|
||||
height,
|
||||
);
|
||||
};
|
||||
},
|
||||
defaultOptions: {
|
||||
method: 'lanczos3',
|
||||
fitMethod: 'stretch',
|
||||
premultiply: true,
|
||||
linearRGB: true,
|
||||
},
|
||||
},
|
||||
// // TODO: Need to handle SVGs and HQX
|
||||
quant: {
|
||||
name: 'ImageQuant',
|
||||
description: 'Reduce the number of colors used (aka. paletting)',
|
||||
instantiate: async () => {
|
||||
const imageQuant = await imageQuantPromise;
|
||||
return (
|
||||
buffer: Uint8Array,
|
||||
width: number,
|
||||
height: number,
|
||||
{ numColors, dither }: QuantOptions,
|
||||
) =>
|
||||
new ImageData(
|
||||
imageQuant.quantize(buffer, width, height, numColors, dither),
|
||||
width,
|
||||
height,
|
||||
);
|
||||
},
|
||||
defaultOptions: {
|
||||
numColors: 255,
|
||||
dither: 1.0,
|
||||
},
|
||||
},
|
||||
rotate: {
|
||||
name: 'Rotate',
|
||||
description: 'Rotate image',
|
||||
instantiate: async () => {
|
||||
return async (
|
||||
buffer: Uint8Array,
|
||||
width: number,
|
||||
height: number,
|
||||
{ numRotations }: RotateOptions,
|
||||
) => {
|
||||
const degrees = (numRotations * 90) % 360;
|
||||
const sameDimensions = degrees == 0 || degrees == 180;
|
||||
const size = width * height * 4;
|
||||
const instance = (
|
||||
await WebAssembly.instantiate(await fsp.readFile(pathify(rotateWasm)))
|
||||
).instance as RotateModuleInstance;
|
||||
const { memory } = instance.exports;
|
||||
const additionalPagesNeeded = Math.ceil(
|
||||
(size * 2 - memory.buffer.byteLength + 8) / (64 * 1024),
|
||||
);
|
||||
if (additionalPagesNeeded > 0) {
|
||||
memory.grow(additionalPagesNeeded);
|
||||
}
|
||||
const view = new Uint8ClampedArray(memory.buffer);
|
||||
view.set(buffer, 8);
|
||||
instance.exports.rotate(width, height, degrees);
|
||||
return new ImageData(
|
||||
view.slice(size + 8, size * 2 + 8),
|
||||
sameDimensions ? width : height,
|
||||
sameDimensions ? height : width,
|
||||
);
|
||||
};
|
||||
},
|
||||
defaultOptions: {
|
||||
numRotations: 0,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const codecs = {
|
||||
mozjpeg: {
|
||||
name: 'MozJPEG',
|
||||
extension: 'jpg',
|
||||
detectors: [/^\xFF\xD8\xFF/],
|
||||
dec: () =>
|
||||
instantiateEmscriptenWasm(mozDec as DecodeModuleFactory, mozDecWasm),
|
||||
enc: () =>
|
||||
instantiateEmscriptenWasm(
|
||||
mozEnc as EmscriptenWasm.ModuleFactory<MozJPEGEncodeModule>,
|
||||
mozEncWasm,
|
||||
),
|
||||
defaultEncoderOptions: {
|
||||
quality: 75,
|
||||
baseline: false,
|
||||
arithmetic: false,
|
||||
progressive: true,
|
||||
optimize_coding: true,
|
||||
smoothing: 0,
|
||||
color_space: 3 /*YCbCr*/,
|
||||
quant_table: 3,
|
||||
trellis_multipass: false,
|
||||
trellis_opt_zero: false,
|
||||
trellis_opt_table: false,
|
||||
trellis_loops: 1,
|
||||
auto_subsample: true,
|
||||
chroma_subsample: 2,
|
||||
separate_chroma_quality: false,
|
||||
chroma_quality: 75,
|
||||
},
|
||||
autoOptimize: {
|
||||
option: 'quality',
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
webp: {
|
||||
name: 'WebP',
|
||||
extension: 'webp',
|
||||
detectors: [/^RIFF....WEBPVP8[LX ]/s],
|
||||
dec: () =>
|
||||
instantiateEmscriptenWasm(webpDec as DecodeModuleFactory, webpDecWasm),
|
||||
enc: () =>
|
||||
instantiateEmscriptenWasm(
|
||||
webpEnc as EmscriptenWasm.ModuleFactory<WebPEncodeModule>,
|
||||
webpEncWasm,
|
||||
),
|
||||
defaultEncoderOptions: {
|
||||
quality: 75,
|
||||
target_size: 0,
|
||||
target_PSNR: 0,
|
||||
method: 4,
|
||||
sns_strength: 50,
|
||||
filter_strength: 60,
|
||||
filter_sharpness: 0,
|
||||
filter_type: 1,
|
||||
partitions: 0,
|
||||
segments: 4,
|
||||
pass: 1,
|
||||
show_compressed: 0,
|
||||
preprocessing: 0,
|
||||
autofilter: 0,
|
||||
partition_limit: 0,
|
||||
alpha_compression: 1,
|
||||
alpha_filtering: 1,
|
||||
alpha_quality: 100,
|
||||
lossless: 0,
|
||||
exact: 0,
|
||||
image_hint: 0,
|
||||
emulate_jpeg_size: 0,
|
||||
thread_level: 0,
|
||||
low_memory: 0,
|
||||
near_lossless: 100,
|
||||
use_delta_palette: 0,
|
||||
use_sharp_yuv: 0,
|
||||
},
|
||||
autoOptimize: {
|
||||
option: 'quality',
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
avif: {
|
||||
name: 'AVIF',
|
||||
extension: 'avif',
|
||||
detectors: [/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/],
|
||||
dec: () =>
|
||||
instantiateEmscriptenWasm(avifDec as DecodeModuleFactory, avifDecWasm),
|
||||
enc: async () => {
|
||||
if (await threads()) {
|
||||
return instantiateEmscriptenWasm(
|
||||
avifEncMt as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
|
||||
avifEncMtWasm,
|
||||
avifEncMtWorker,
|
||||
);
|
||||
}
|
||||
return instantiateEmscriptenWasm(
|
||||
avifEnc as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
|
||||
avifEncWasm,
|
||||
);
|
||||
},
|
||||
defaultEncoderOptions: {
|
||||
cqLevel: 33,
|
||||
cqAlphaLevel: -1,
|
||||
denoiseLevel: 0,
|
||||
tileColsLog2: 0,
|
||||
tileRowsLog2: 0,
|
||||
speed: 6,
|
||||
subsample: 1,
|
||||
chromaDeltaQ: false,
|
||||
sharpness: 0,
|
||||
tune: 0 /* AVIFTune.auto */,
|
||||
},
|
||||
autoOptimize: {
|
||||
option: 'cqLevel',
|
||||
min: 62,
|
||||
max: 0,
|
||||
},
|
||||
},
|
||||
jxl: {
|
||||
name: 'JPEG-XL',
|
||||
extension: 'jxl',
|
||||
detectors: [/^\xff\x0a/],
|
||||
dec: () =>
|
||||
instantiateEmscriptenWasm(jxlDec as DecodeModuleFactory, jxlDecWasm),
|
||||
enc: () =>
|
||||
instantiateEmscriptenWasm(
|
||||
jxlEnc as EmscriptenWasm.ModuleFactory<JXLEncodeModule>,
|
||||
jxlEncWasm,
|
||||
),
|
||||
defaultEncoderOptions: {
|
||||
effort: 1,
|
||||
quality: 75,
|
||||
progressive: false,
|
||||
epf: -1,
|
||||
lossyPalette: false,
|
||||
decodingSpeedTier: 0,
|
||||
photonNoiseIso: 0,
|
||||
},
|
||||
autoOptimize: {
|
||||
option: 'quality',
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
wp2: {
|
||||
name: 'WebP2',
|
||||
extension: 'wp2',
|
||||
detectors: [/^\xF4\xFF\x6F/],
|
||||
dec: () =>
|
||||
instantiateEmscriptenWasm(wp2Dec as DecodeModuleFactory, wp2DecWasm),
|
||||
enc: () =>
|
||||
instantiateEmscriptenWasm(
|
||||
wp2Enc as EmscriptenWasm.ModuleFactory<WP2EncodeModule>,
|
||||
wp2EncWasm,
|
||||
),
|
||||
defaultEncoderOptions: {
|
||||
quality: 75,
|
||||
alpha_quality: 75,
|
||||
effort: 5,
|
||||
pass: 1,
|
||||
sns: 50,
|
||||
uv_mode: 0 /*UVMode.UVModeAuto*/,
|
||||
csp_type: 0 /*Csp.kYCoCg*/,
|
||||
error_diffusion: 0,
|
||||
use_random_matrix: false,
|
||||
},
|
||||
autoOptimize: {
|
||||
option: 'quality',
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
oxipng: {
|
||||
name: 'OxiPNG',
|
||||
extension: 'png',
|
||||
detectors: [/^\x89PNG\x0D\x0A\x1A\x0A/],
|
||||
dec: async () => {
|
||||
await pngEncDecPromise;
|
||||
return { decode: pngEncDec.decode };
|
||||
},
|
||||
enc: async () => {
|
||||
await pngEncDecPromise;
|
||||
await oxipngPromise;
|
||||
return {
|
||||
encode: (
|
||||
buffer: Uint8ClampedArray | ArrayBuffer,
|
||||
width: number,
|
||||
height: number,
|
||||
opts: { level: number },
|
||||
) => {
|
||||
const simplePng = pngEncDec.encode(
|
||||
new Uint8Array(buffer),
|
||||
width,
|
||||
height,
|
||||
);
|
||||
return oxipng.optimise(simplePng, opts.level, false);
|
||||
},
|
||||
};
|
||||
},
|
||||
defaultEncoderOptions: {
|
||||
level: 2,
|
||||
},
|
||||
autoOptimize: {
|
||||
option: 'level',
|
||||
min: 6,
|
||||
max: 1,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export {
|
||||
MozJPEGEncodeOptions,
|
||||
WebPEncodeOptions,
|
||||
AvifEncodeOptions,
|
||||
JxlEncodeOptions,
|
||||
WP2EncodeOptions,
|
||||
OxiPngEncodeOptions,
|
||||
};
|
@ -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;
|
||||
},
|
||||
});
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
export default class ImageData {
|
||||
readonly data: Uint8ClampedArray;
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
|
||||
constructor(data: Uint8ClampedArray, width: number, height: number) {
|
||||
this.data = data;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
}
|
@ -1,307 +0,0 @@
|
||||
import { isMainThread } from 'worker_threads';
|
||||
|
||||
import {
|
||||
AvifEncodeOptions,
|
||||
codecs as encoders,
|
||||
JxlEncodeOptions,
|
||||
MozJPEGEncodeOptions,
|
||||
OxiPngEncodeOptions,
|
||||
preprocessors,
|
||||
QuantOptions,
|
||||
ResizeOptions,
|
||||
RotateOptions,
|
||||
WebPEncodeOptions,
|
||||
WP2EncodeOptions,
|
||||
} from './codecs.js';
|
||||
import WorkerPool from './worker_pool.js';
|
||||
import { autoOptimize } from './auto-optimizer.js';
|
||||
import type ImageData from './image_data';
|
||||
|
||||
export { ImagePool, encoders, preprocessors };
|
||||
type EncoderKey = keyof typeof encoders;
|
||||
type PreprocessorKey = keyof typeof preprocessors;
|
||||
|
||||
type PreprocessOptions = {
|
||||
resize?: ResizeOptions;
|
||||
quant?: QuantOptions;
|
||||
rotate?: RotateOptions;
|
||||
};
|
||||
type EncodeResult = {
|
||||
optionsUsed: object;
|
||||
binary: Uint8Array;
|
||||
extension: string;
|
||||
size: number;
|
||||
};
|
||||
type EncoderOptions = {
|
||||
mozjpeg?: Partial<MozJPEGEncodeOptions>;
|
||||
webp?: Partial<WebPEncodeOptions>;
|
||||
avif?: Partial<AvifEncodeOptions>;
|
||||
jxl?: Partial<JxlEncodeOptions>;
|
||||
wp2?: Partial<WP2EncodeOptions>;
|
||||
oxipng?: Partial<OxiPngEncodeOptions>;
|
||||
};
|
||||
|
||||
async function decodeFile({
|
||||
file,
|
||||
}: {
|
||||
file: ArrayBuffer | ArrayLike<number>;
|
||||
}): Promise<{ bitmap: ImageData; size: number }> {
|
||||
const array = new Uint8Array(file);
|
||||
const firstChunk = array.slice(0, 16);
|
||||
const firstChunkString = Array.from(firstChunk)
|
||||
.map((v) => String.fromCodePoint(v))
|
||||
.join('');
|
||||
const key = Object.entries(encoders).find(([_name, { detectors }]) =>
|
||||
detectors.some((detector) => detector.exec(firstChunkString)),
|
||||
)?.[0] as EncoderKey | undefined;
|
||||
if (!key) {
|
||||
throw Error(`File has an unsupported format`);
|
||||
}
|
||||
const encoder = encoders[key];
|
||||
const mod = await encoder.dec();
|
||||
const rgba = mod.decode(array);
|
||||
return {
|
||||
bitmap: rgba,
|
||||
size: array.length,
|
||||
};
|
||||
}
|
||||
|
||||
async function preprocessImage({
|
||||
preprocessorName,
|
||||
options,
|
||||
image,
|
||||
}: {
|
||||
preprocessorName: PreprocessorKey;
|
||||
options: any;
|
||||
image: { bitmap: ImageData };
|
||||
}) {
|
||||
const preprocessor = await preprocessors[preprocessorName].instantiate();
|
||||
image.bitmap = await preprocessor(
|
||||
Uint8Array.from(image.bitmap.data),
|
||||
image.bitmap.width,
|
||||
image.bitmap.height,
|
||||
options,
|
||||
);
|
||||
return image;
|
||||
}
|
||||
|
||||
async function encodeImage({
|
||||
bitmap: bitmapIn,
|
||||
encName,
|
||||
encConfig,
|
||||
optimizerButteraugliTarget,
|
||||
maxOptimizerRounds,
|
||||
}: {
|
||||
bitmap: ImageData;
|
||||
encName: EncoderKey;
|
||||
encConfig: any;
|
||||
optimizerButteraugliTarget: number;
|
||||
maxOptimizerRounds: number;
|
||||
}): Promise<EncodeResult> {
|
||||
let binary: Uint8Array;
|
||||
let optionsUsed = encConfig;
|
||||
const encoder = await encoders[encName].enc();
|
||||
if (encConfig === 'auto') {
|
||||
const optionToOptimize = encoders[encName].autoOptimize.option;
|
||||
const decoder = await encoders[encName].dec();
|
||||
const encode = (bitmapIn: ImageData, quality: number) =>
|
||||
encoder.encode(
|
||||
bitmapIn.data,
|
||||
bitmapIn.width,
|
||||
bitmapIn.height,
|
||||
Object.assign({}, encoders[encName].defaultEncoderOptions as any, {
|
||||
[optionToOptimize]: quality,
|
||||
}),
|
||||
);
|
||||
const decode = (binary: Uint8Array) => 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(
|
||||
bitmapIn,
|
||||
nonNullEncode,
|
||||
decode,
|
||||
{
|
||||
min: encoders[encName].autoOptimize.min,
|
||||
max: encoders[encName].autoOptimize.max,
|
||||
butteraugliDistanceGoal: optimizerButteraugliTarget,
|
||||
maxRounds: maxOptimizerRounds,
|
||||
},
|
||||
);
|
||||
binary = optimizedBinary;
|
||||
optionsUsed = {
|
||||
// 5 significant digits is enough
|
||||
[optionToOptimize]: Math.round(quality * 10000) / 10000,
|
||||
};
|
||||
} else {
|
||||
const result = encoder.encode(
|
||||
bitmapIn.data.buffer,
|
||||
bitmapIn.width,
|
||||
bitmapIn.height,
|
||||
encConfig,
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
throw new Error('There was an error while encoding');
|
||||
}
|
||||
|
||||
binary = result;
|
||||
}
|
||||
return {
|
||||
optionsUsed,
|
||||
binary,
|
||||
extension: encoders[encName].extension,
|
||||
size: binary.length,
|
||||
};
|
||||
}
|
||||
|
||||
type EncodeParams = { operation: 'encode' } & Parameters<typeof encodeImage>[0];
|
||||
type DecodeParams = { operation: 'decode' } & Parameters<typeof decodeFile>[0];
|
||||
type PreprocessParams = { operation: 'preprocess' } & Parameters<
|
||||
typeof preprocessImage
|
||||
>[0];
|
||||
type JobMessage = EncodeParams | DecodeParams | PreprocessParams;
|
||||
|
||||
function handleJob(params: JobMessage) {
|
||||
switch (params.operation) {
|
||||
case 'encode':
|
||||
return encodeImage(params);
|
||||
case 'decode':
|
||||
return decodeFile(params);
|
||||
case 'preprocess':
|
||||
return preprocessImage(params);
|
||||
default:
|
||||
throw Error(`Invalid job "${(params as any).operation}"`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an ingested image.
|
||||
*/
|
||||
class Image {
|
||||
public file: ArrayBuffer | ArrayLike<number>;
|
||||
public workerPool: WorkerPool<JobMessage, any>;
|
||||
public decoded: Promise<{ bitmap: ImageData }>;
|
||||
public encodedWith: { [key in EncoderKey]?: EncodeResult };
|
||||
|
||||
constructor(
|
||||
workerPool: WorkerPool<JobMessage, any>,
|
||||
file: ArrayBuffer | ArrayLike<number>,
|
||||
) {
|
||||
this.file = file;
|
||||
this.workerPool = workerPool;
|
||||
this.decoded = workerPool.dispatchJob({ operation: 'decode', file });
|
||||
this.encodedWith = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Define one or several preprocessors to use on the image.
|
||||
* @param {PreprocessOptions} preprocessOptions - An object with preprocessors to use, and their settings.
|
||||
* @returns {Promise<undefined>} - A promise that resolves when all preprocessors have completed their work.
|
||||
*/
|
||||
async preprocess(preprocessOptions: PreprocessOptions = {}) {
|
||||
for (const [name, options] of Object.entries(preprocessOptions)) {
|
||||
if (!Object.keys(preprocessors).includes(name)) {
|
||||
throw Error(`Invalid preprocessor "${name}"`);
|
||||
}
|
||||
const preprocessorName = name as PreprocessorKey;
|
||||
const preprocessorOptions = Object.assign(
|
||||
{},
|
||||
preprocessors[preprocessorName].defaultOptions,
|
||||
options,
|
||||
);
|
||||
this.decoded = this.workerPool.dispatchJob({
|
||||
operation: 'preprocess',
|
||||
preprocessorName,
|
||||
image: await this.decoded,
|
||||
options: preprocessorOptions,
|
||||
});
|
||||
await this.decoded;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define one or several encoders to use on the image.
|
||||
* @param {object} encodeOptions - An object with encoders to use, and their settings.
|
||||
* @returns {Promise<{ [key in keyof T]: EncodeResult }>} - A promise that resolves when the image has been encoded with all the specified encoders.
|
||||
*/
|
||||
async encode<T extends EncoderOptions>(
|
||||
encodeOptions: {
|
||||
optimizerButteraugliTarget?: number;
|
||||
maxOptimizerRounds?: number;
|
||||
} & T,
|
||||
): Promise<{ [key in keyof T]: EncodeResult }> {
|
||||
const { bitmap } = await this.decoded;
|
||||
const setEncodedWithPromises = [];
|
||||
for (const [name, options] of Object.entries(encodeOptions)) {
|
||||
if (!Object.keys(encoders).includes(name)) {
|
||||
continue;
|
||||
}
|
||||
const encName = name as EncoderKey;
|
||||
const encRef = encoders[encName];
|
||||
const encConfig =
|
||||
typeof options === 'string'
|
||||
? options
|
||||
: Object.assign({}, encRef.defaultEncoderOptions, options);
|
||||
setEncodedWithPromises.push(
|
||||
this.workerPool
|
||||
.dispatchJob({
|
||||
operation: 'encode',
|
||||
bitmap,
|
||||
encName,
|
||||
encConfig,
|
||||
optimizerButteraugliTarget: Number(
|
||||
encodeOptions.optimizerButteraugliTarget ?? 1.4,
|
||||
),
|
||||
maxOptimizerRounds: Number(encodeOptions.maxOptimizerRounds ?? 6),
|
||||
})
|
||||
.then((encodeResult) => {
|
||||
this.encodedWith[encName] = encodeResult;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(setEncodedWithPromises);
|
||||
return this.encodedWith as { [key in keyof T]: EncodeResult };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A pool where images can be ingested and squooshed.
|
||||
*/
|
||||
class ImagePool {
|
||||
public workerPool: WorkerPool<JobMessage, any>;
|
||||
|
||||
/**
|
||||
* Create a new pool.
|
||||
* @param {number} [threads] - Number of concurrent image processes to run in the pool.
|
||||
*/
|
||||
constructor(threads: number) {
|
||||
this.workerPool = new WorkerPool(threads, __filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingest an image into the image pool.
|
||||
* @param {ArrayBuffer | ArrayLike<number>} file - The image that should be ingested and decoded.
|
||||
* @returns {Image} - A custom class reference to the decoded image.
|
||||
*/
|
||||
ingestImage(file: ArrayBuffer | ArrayLike<number>): Image {
|
||||
return new Image(this.workerPool, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
await this.workerPool.join();
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMainThread) {
|
||||
WorkerPool.useThisThreadAsWorker(handleJob);
|
||||
}
|
43
libsquoosh/src/missing-types.d.ts
vendored
43
libsquoosh/src/missing-types.d.ts
vendored
@ -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;
|
@ -1,124 +0,0 @@
|
||||
import { Worker, parentPort } from 'worker_threads';
|
||||
import { TransformStream } from 'web-streams-polyfill';
|
||||
|
||||
function uuid() {
|
||||
return Array.from({ length: 16 }, () =>
|
||||
Math.floor(Math.random() * 256).toString(16),
|
||||
).join('');
|
||||
}
|
||||
|
||||
interface Job<I> {
|
||||
msg: I;
|
||||
resolve: Function;
|
||||
reject: Function;
|
||||
}
|
||||
|
||||
export default class WorkerPool<I, O> {
|
||||
public numWorkers: number;
|
||||
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.jobQueue = new TransformStream();
|
||||
this.workerQueue = new TransformStream();
|
||||
|
||||
const writer = this.workerQueue.writable.getWriter();
|
||||
for (let i = 0; i < numWorkers; i++) {
|
||||
writer.write(new Worker(workerFile));
|
||||
}
|
||||
writer.releaseLock();
|
||||
|
||||
this.done = this._readLoop();
|
||||
}
|
||||
|
||||
async _readLoop() {
|
||||
const reader = this.jobQueue.readable.getReader();
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) {
|
||||
await this._terminateAll();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
throw new Error('Reader did not return any value');
|
||||
}
|
||||
|
||||
const { msg, resolve, reject } = value;
|
||||
const worker = await this._nextWorker();
|
||||
this.jobPromise(worker, msg)
|
||||
.then((result) => resolve(result))
|
||||
.catch((reason) => reject(reason))
|
||||
.finally(() => {
|
||||
// Return the worker to the pool
|
||||
const writer = this.workerQueue.writable.getWriter();
|
||||
writer.write(worker);
|
||||
writer.releaseLock();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _nextWorker() {
|
||||
const reader = this.workerQueue.readable.getReader();
|
||||
const { value } = await reader.read();
|
||||
reader.releaseLock();
|
||||
if (!value) {
|
||||
throw new Error('No worker left');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async _terminateAll() {
|
||||
for (let n = 0; n < this.numWorkers; n++) {
|
||||
const worker = await this._nextWorker();
|
||||
worker.terminate();
|
||||
}
|
||||
this.workerQueue.writable.close();
|
||||
}
|
||||
|
||||
async join() {
|
||||
this.jobQueue.writable.getWriter().close();
|
||||
await this.done;
|
||||
}
|
||||
|
||||
dispatchJob(msg: I): Promise<O> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const writer = this.jobQueue.writable.getWriter();
|
||||
writer.write({ msg, resolve, reject });
|
||||
writer.releaseLock();
|
||||
});
|
||||
}
|
||||
|
||||
private jobPromise(worker: Worker, msg: I) {
|
||||
return new Promise((resolve, reject) => {
|
||||
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;
|
||||
try {
|
||||
const result = await cb(msg);
|
||||
parentPort!.postMessage({ result, id });
|
||||
} catch (e) {
|
||||
parentPort!.postMessage({ error: e.message, id });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "../generic-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext"],
|
||||
"types": ["node"],
|
||||
"allowJs": true
|
||||
},
|
||||
"include": ["src/**/*", "../codecs/**/*"]
|
||||
}
|
1
package-lock.json
generated
1
package-lock.json
generated
@ -5,6 +5,7 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "squoosh",
|
||||
"version": "2.0.0",
|
||||
"license": "apache-2.0",
|
||||
"dependencies": {
|
||||
|
@ -46,14 +46,12 @@
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"serve": "^11.3.2",
|
||||
"typescript": "^4.4.4",
|
||||
"which": "^2.0.2"
|
||||
"which": "^2.0.2",
|
||||
"wasm-feature-detect": "^1.2.11"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,css,json,md,ts,tsx}": "prettier --write",
|
||||
"*.{c,h,cpp,hpp}": "clang-format -i",
|
||||
"*.rs": "rustfmt"
|
||||
},
|
||||
"dependencies": {
|
||||
"wasm-feature-detect": "^1.2.11"
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +86,7 @@ export default async function ({ watch }) {
|
||||
'src/features-worker',
|
||||
'src/features-worker-worker-bridge',
|
||||
'src/sw',
|
||||
'src/worker-shared',
|
||||
'codecs',
|
||||
]),
|
||||
urlPlugin(),
|
||||
|
@ -11,7 +11,11 @@ export default class Checkbox extends Component<Props, State> {
|
||||
return (
|
||||
<div class={style.checkbox}>
|
||||
{props.checked ? (
|
||||
<CheckedIcon class={`${style.icon} ${style.checked}`} />
|
||||
props.disabled ? (
|
||||
<CheckedIcon class={`${style.icon} ${style.disabled}`} />
|
||||
) : (
|
||||
<CheckedIcon class={`${style.icon} ${style.checked}`} />
|
||||
)
|
||||
) : (
|
||||
<UncheckedIcon class={style.icon} />
|
||||
)}
|
||||
|
@ -20,3 +20,7 @@
|
||||
.checked {
|
||||
fill: var(--main-theme-color);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
fill: var(--dark-gray);
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ import Toggle from './Toggle';
|
||||
import Select from './Select';
|
||||
import { Options as QuantOptionsComponent } from 'features/processors/quantize/client';
|
||||
import { Options as ResizeOptionsComponent } from 'features/processors/resize/client';
|
||||
import { CLIIcon, SwapIcon } from 'client/lazy-app/icons';
|
||||
import { SwapIcon } from 'client/lazy-app/icons';
|
||||
|
||||
interface Props {
|
||||
index: 0 | 1;
|
||||
@ -29,7 +29,6 @@ interface Props {
|
||||
onEncoderOptionsChange(index: 0 | 1, newOptions: EncoderOptions): void;
|
||||
onProcessorOptionsChange(index: 0 | 1, newOptions: ProcessorState): void;
|
||||
onCopyToOtherSideClick(index: 0 | 1): void;
|
||||
onCopyCliClick(index: 0 | 1): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@ -107,10 +106,6 @@ export default class Options extends Component<Props, State> {
|
||||
this.props.onEncoderOptionsChange(this.props.index, newOptions);
|
||||
};
|
||||
|
||||
private onCopyCliClick = () => {
|
||||
this.props.onCopyCliClick(this.props.index);
|
||||
};
|
||||
|
||||
private onCopyToOtherSideClick = () => {
|
||||
this.props.onCopyToOtherSideClick(this.props.index);
|
||||
};
|
||||
@ -137,13 +132,6 @@ export default class Options extends Component<Props, State> {
|
||||
<h3 class={style.optionsTitle}>
|
||||
<div class={style.titleAndButtons}>
|
||||
Edit
|
||||
<button
|
||||
class={style.cliButton}
|
||||
title="Copy npx command"
|
||||
onClick={this.onCopyCliClick}
|
||||
>
|
||||
<CLIIcon />
|
||||
</button>
|
||||
<button
|
||||
class={style.copyOverButton}
|
||||
title="Copy settings to other side"
|
||||
|
@ -40,7 +40,8 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
|
||||
}
|
||||
|
||||
.scrubber {
|
||||
display: flex;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
@ -54,10 +55,6 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
|
||||
padding: 0 calc(var(--thumb-size) * 0.24);
|
||||
}
|
||||
|
||||
.scrubber svg {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.arrow-left {
|
||||
fill: var(--pink);
|
||||
}
|
||||
|
@ -49,6 +49,7 @@
|
||||
align-self: center;
|
||||
padding: 9px 66px;
|
||||
position: relative;
|
||||
gap: 6px;
|
||||
|
||||
/* Allow clicks to fall through to the pinch zoom area */
|
||||
pointer-events: none;
|
||||
@ -68,7 +69,6 @@
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
.button,
|
||||
@ -95,6 +95,7 @@
|
||||
|
||||
.button {
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
background: rgba(50, 50, 50, 0.92);
|
||||
@ -114,7 +115,7 @@
|
||||
.last-button {
|
||||
composes: button;
|
||||
border-radius: 0 6px 6px 0;
|
||||
border-left-width: 1px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
.zoom {
|
||||
|
@ -31,7 +31,6 @@ import Results from './Results';
|
||||
import WorkerBridge from '../worker-bridge';
|
||||
import { resize } from 'features/processors/resize/client';
|
||||
import type SnackBarElement from 'shared/custom-els/snack-bar';
|
||||
import { generateCliInvocation } from '../util/cli';
|
||||
import { drawableToImageData } from '../util/canvas';
|
||||
|
||||
export type OutputType = EncoderType | 'identity';
|
||||
@ -114,7 +113,7 @@ async function decodeImage(
|
||||
}
|
||||
}
|
||||
// Otherwise fall through and try built-in decoding for a laugh.
|
||||
return await builtinDecode(signal, blob, mimeType);
|
||||
return await builtinDecode(signal, blob);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'AbortError') throw err;
|
||||
console.log(err);
|
||||
@ -462,29 +461,6 @@ export default class Compress extends Component<Props, State> {
|
||||
}));
|
||||
};
|
||||
|
||||
private onCopyCliClick = async (index: 0 | 1) => {
|
||||
try {
|
||||
const cliInvocation = generateCliInvocation(
|
||||
this.state.sides[index].latestSettings.encoderState!,
|
||||
this.state.sides[index].latestSettings.processorState,
|
||||
);
|
||||
await navigator.clipboard.writeText(cliInvocation);
|
||||
const result = await this.props.showSnack(
|
||||
'CLI command copied to clipboard',
|
||||
{
|
||||
timeout: 8000,
|
||||
actions: ['usage', 'dismiss'],
|
||||
},
|
||||
);
|
||||
|
||||
if (result === 'usage') {
|
||||
open('https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli');
|
||||
}
|
||||
} catch (e) {
|
||||
this.props.showSnack(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Debounce the heavy lifting of updateImage.
|
||||
* Otherwise, the thrashing causes jank, and sometimes crashes iOS Safari.
|
||||
@ -852,7 +828,6 @@ export default class Compress extends Component<Props, State> {
|
||||
onEncoderTypeChange={this.onEncoderTypeChange}
|
||||
onEncoderOptionsChange={this.onEncoderOptionsChange}
|
||||
onProcessorOptionsChange={this.onProcessorOptionsChange}
|
||||
onCopyCliClick={this.onCopyCliClick}
|
||||
onCopyToOtherSideClick={this.onCopyToOtherClick}
|
||||
/>
|
||||
));
|
||||
|
@ -92,17 +92,6 @@ export const DownloadIcon = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CLIIcon = () => (
|
||||
<svg viewBox="0 0 81.3 68.8">
|
||||
<path
|
||||
fill="none"
|
||||
stroke-miterlimit="15.6"
|
||||
stroke-width="6.3"
|
||||
d="M3.1 3.1h75v62.5h-75zm18.8 43.8l12.5-12.5-12.5-12.5m18.7 25h18.8"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SwapIcon = () => (
|
||||
<svg viewBox="0 0 18 14">
|
||||
<path d="M5.5 3.6v6.8L2.1 7l3.4-3.4M7 0L0 7l7 7V0zm4 0v14l7-7-7-7z" />
|
||||
|
@ -63,35 +63,17 @@ interface DrawableToImageDataOptions {
|
||||
sh?: number;
|
||||
}
|
||||
|
||||
function getWidth(
|
||||
drawable: ImageBitmap | HTMLImageElement | VideoFrame,
|
||||
): number {
|
||||
if ('displayWidth' in drawable) {
|
||||
return drawable.displayWidth;
|
||||
}
|
||||
return drawable.width;
|
||||
}
|
||||
|
||||
function getHeight(
|
||||
drawable: ImageBitmap | HTMLImageElement | VideoFrame,
|
||||
): number {
|
||||
if ('displayHeight' in drawable) {
|
||||
return drawable.displayHeight;
|
||||
}
|
||||
return drawable.height;
|
||||
}
|
||||
|
||||
export function drawableToImageData(
|
||||
drawable: ImageBitmap | HTMLImageElement | VideoFrame,
|
||||
drawable: ImageBitmap | HTMLImageElement,
|
||||
opts: DrawableToImageDataOptions = {},
|
||||
): ImageData {
|
||||
const {
|
||||
width = getWidth(drawable),
|
||||
height = getHeight(drawable),
|
||||
width = drawable.width,
|
||||
height = drawable.height,
|
||||
sx = 0,
|
||||
sy = 0,
|
||||
sw = getWidth(drawable),
|
||||
sh = getHeight(drawable),
|
||||
sw = drawable.width,
|
||||
sh = drawable.height,
|
||||
} = opts;
|
||||
|
||||
// Make canvas same size as image
|
||||
|
@ -1,51 +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 { EncoderState, ProcessorState } from '../feature-meta';
|
||||
|
||||
// Maps our encoder.type values to CLI parameter names
|
||||
const typeMap = new Map<string, string>([
|
||||
['avif', '--avif'],
|
||||
['jxl', '--jxl'],
|
||||
['mozJPEG', '--mozjpeg'],
|
||||
['oxiPNG', '--oxipng'],
|
||||
['webP', '--webp'],
|
||||
['wp2', '--wp2'],
|
||||
]);
|
||||
|
||||
// Same as JSON.stringify, but with single quotes around the entire value
|
||||
// so that shells don’t do weird stuff.
|
||||
function cliJson<T>(v: T): string {
|
||||
return "'" + JSON.stringify(v) + "'";
|
||||
}
|
||||
|
||||
export function generateCliInvocation(
|
||||
encoder: EncoderState,
|
||||
processor: ProcessorState,
|
||||
): string {
|
||||
if (!typeMap.has(encoder.type)) {
|
||||
throw Error(`Encoder ${encoder.type} is unsupported in the CLI`);
|
||||
}
|
||||
return [
|
||||
'npx',
|
||||
'@squoosh/cli',
|
||||
...(processor.resize.enabled
|
||||
? ['--resize', cliJson(processor.resize)]
|
||||
: []),
|
||||
...(processor.quantize.enabled
|
||||
? ['--quant', cliJson(processor.quantize)]
|
||||
: []),
|
||||
typeMap.get(encoder.type)!,
|
||||
cliJson(encoder.options),
|
||||
].join(' ');
|
||||
}
|
@ -10,8 +10,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as WebCodecs from '../util/web-codecs';
|
||||
import { drawableToImageData } from './canvas';
|
||||
|
||||
/** If render engine is Safari */
|
||||
@ -139,15 +137,7 @@ export async function blobToImg(blob: Blob): Promise<HTMLImageElement> {
|
||||
export async function builtinDecode(
|
||||
signal: AbortSignal,
|
||||
blob: Blob,
|
||||
mimeType: string,
|
||||
): Promise<ImageData> {
|
||||
// If WebCodecs are supported, use that.
|
||||
if (await WebCodecs.isTypeSupported(mimeType)) {
|
||||
assertSignal(signal);
|
||||
try {
|
||||
return await abortable(signal, WebCodecs.decode(blob, mimeType));
|
||||
} catch (e) {}
|
||||
}
|
||||
assertSignal(signal);
|
||||
|
||||
// Prefer createImageBitmap as it's the off-thread option for Firefox.
|
||||
|
@ -1,33 +0,0 @@
|
||||
import { drawableToImageData } from '../canvas';
|
||||
|
||||
const hasImageDecoder = typeof ImageDecoder !== 'undefined';
|
||||
|
||||
export async function isTypeSupported(mimeType: string): Promise<boolean> {
|
||||
if (!hasImageDecoder) return false;
|
||||
// Some old versions of this API threw here.
|
||||
// It only impacted folks with experimental web platform flags enabled in Chrome 90.
|
||||
// The API was updated in Chrome 91.
|
||||
try {
|
||||
return await ImageDecoder.isTypeSupported(mimeType);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function decode(
|
||||
blob: Blob | File,
|
||||
mimeType: string,
|
||||
): Promise<ImageData> {
|
||||
if (!hasImageDecoder) {
|
||||
throw Error(
|
||||
`This browser does not support ImageDecoder. This function should not have been called.`,
|
||||
);
|
||||
}
|
||||
const decoder = new ImageDecoder({
|
||||
type: mimeType,
|
||||
// Non-obvious way to turn an Blob into a ReadableStream
|
||||
data: new Response(blob).body!,
|
||||
});
|
||||
const { image } = await decoder.decode();
|
||||
return drawableToImageData(image);
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
interface ImageDecoderInit {
|
||||
type: string;
|
||||
data: BufferSource | ReadableStream;
|
||||
premultiplyAlpha?: PremultiplyAlpha;
|
||||
colorSpaceConversion?: ColorSpaceConversion;
|
||||
desiredWidth?: number;
|
||||
desiredHeight?: number;
|
||||
preferAnimation?: boolean;
|
||||
}
|
||||
|
||||
interface ImageDecodeOptions {
|
||||
frameIndex: number;
|
||||
completeFramesOnly: boolean;
|
||||
}
|
||||
|
||||
interface ImageDecodeResult {
|
||||
image: VideoFrame;
|
||||
complete: boolean;
|
||||
}
|
||||
|
||||
// I didn’t do all the types because the class is kinda complex.
|
||||
// I focused on what we need.
|
||||
// See https://w3c.github.io/webcodecs/#videoframe
|
||||
declare class VideoFrame {
|
||||
displayWidth: number;
|
||||
displayHeight: number;
|
||||
}
|
||||
|
||||
// Add VideoFrame to canvas’ drawImage()
|
||||
interface CanvasDrawImage {
|
||||
drawImage(
|
||||
image: CanvasImageSource | VideoFrame,
|
||||
dx: number,
|
||||
dy: number,
|
||||
): void;
|
||||
drawImage(
|
||||
image: CanvasImageSource | VideoFrame,
|
||||
dx: number,
|
||||
dy: number,
|
||||
dw: number,
|
||||
dh: number,
|
||||
): void;
|
||||
drawImage(
|
||||
image: CanvasImageSource | VideoFrame,
|
||||
sx: number,
|
||||
sy: number,
|
||||
sw: number,
|
||||
sh: number,
|
||||
dx: number,
|
||||
dy: number,
|
||||
dw: number,
|
||||
dh: number,
|
||||
): void;
|
||||
}
|
||||
|
||||
declare class ImageDecoder {
|
||||
static isTypeSupported(type: string): Promise<boolean>;
|
||||
constructor(desc: ImageDecoderInit);
|
||||
decode(opts?: Partial<ImageDecodeOptions>): Promise<ImageDecodeResult>;
|
||||
}
|
@ -13,12 +13,12 @@
|
||||
import type { AVIFModule } from 'codecs/avif/enc/avif_enc';
|
||||
import type { EncodeOptions } from '../shared/meta';
|
||||
import { initEmscriptenModule } from 'features/worker-utils';
|
||||
import { threads } from 'wasm-feature-detect';
|
||||
import checkThreadsSupport from 'worker-shared/supports-wasm-threads';
|
||||
|
||||
let emscriptenModule: Promise<AVIFModule>;
|
||||
|
||||
async function init() {
|
||||
if (await threads()) {
|
||||
if (await checkThreadsSupport()) {
|
||||
const avifEncoder = await import('codecs/avif/enc/avif_enc_mt');
|
||||
return initEmscriptenModule<AVIFModule>(avifEncoder.default);
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ interface State {
|
||||
autoEdgePreservingFilter: boolean;
|
||||
decodingSpeedTier: number;
|
||||
photonNoiseIso: number;
|
||||
alternativeLossy: boolean;
|
||||
}
|
||||
|
||||
export class Options extends Component<Props, State> {
|
||||
@ -55,6 +56,7 @@ export class Options extends Component<Props, State> {
|
||||
autoEdgePreservingFilter: options.epf === -1,
|
||||
decodingSpeedTier: options.decodingSpeedTier,
|
||||
photonNoiseIso: options.photonNoiseIso,
|
||||
alternativeLossy: options.lossyModular,
|
||||
};
|
||||
}
|
||||
|
||||
@ -96,6 +98,7 @@ export class Options extends Component<Props, State> {
|
||||
lossyPalette: optionState.lossless ? optionState.slightLoss : false,
|
||||
decodingSpeedTier: optionState.decodingSpeedTier,
|
||||
photonNoiseIso: optionState.photonNoiseIso,
|
||||
lossyModular: optionState.quality < 7 ? true : optionState.alternativeLossy,
|
||||
};
|
||||
|
||||
// Updating options, so we don't recalculate in getDerivedStateFromProps.
|
||||
@ -122,6 +125,7 @@ export class Options extends Component<Props, State> {
|
||||
autoEdgePreservingFilter,
|
||||
decodingSpeedTier,
|
||||
photonNoiseIso,
|
||||
alternativeLossy,
|
||||
}: State,
|
||||
) {
|
||||
// I'm rendering both lossy and lossless forms, as it becomes much easier when
|
||||
@ -162,6 +166,14 @@ export class Options extends Component<Props, State> {
|
||||
Quality:
|
||||
</Range>
|
||||
</div>
|
||||
<label class={style.optionToggle}>
|
||||
Alternative lossy mode
|
||||
<Checkbox
|
||||
checked={quality < 7 ? true : alternativeLossy}
|
||||
disabled={quality < 7}
|
||||
onChange={this._inputChange('alternativeLossy', 'boolean')}
|
||||
/>
|
||||
</label>
|
||||
<label class={style.optionToggle}>
|
||||
Auto edge filter
|
||||
<Checkbox
|
||||
@ -223,7 +235,7 @@ export class Options extends Component<Props, State> {
|
||||
</label>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
min="3"
|
||||
min="1"
|
||||
max="9"
|
||||
value={effort}
|
||||
onInput={this._inputChange('effort', 'number')}
|
||||
|
@ -25,4 +25,5 @@ export const defaultOptions: EncodeOptions = {
|
||||
lossyPalette: false,
|
||||
decodingSpeedTier: 0,
|
||||
photonNoiseIso: 0,
|
||||
lossyModular: false,
|
||||
};
|
||||
|
@ -14,12 +14,13 @@ import type { JXLModule } from 'codecs/jxl/enc/jxl_enc';
|
||||
import type { EncodeOptions } from '../shared/meta';
|
||||
|
||||
import { initEmscriptenModule } from 'features/worker-utils';
|
||||
import { threads, simd } from 'wasm-feature-detect';
|
||||
import { simd } from 'wasm-feature-detect';
|
||||
import checkThreadsSupport from 'worker-shared/supports-wasm-threads';
|
||||
|
||||
let emscriptenModule: Promise<JXLModule>;
|
||||
|
||||
async function init() {
|
||||
if (await threads()) {
|
||||
if (await checkThreadsSupport()) {
|
||||
if (await simd()) {
|
||||
const jxlEncoder = await import('codecs/jxl/enc/jxl_enc_mt_simd');
|
||||
return initEmscriptenModule(jxlEncoder.default);
|
||||
|
@ -11,7 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { EncodeOptions } from '../shared/meta';
|
||||
import { threads } from 'wasm-feature-detect';
|
||||
import checkThreadsSupport from 'worker-shared/supports-wasm-threads';
|
||||
|
||||
async function initMT() {
|
||||
const {
|
||||
@ -39,7 +39,7 @@ export default async function encode(
|
||||
options: EncodeOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
if (!wasmReady) {
|
||||
wasmReady = threads().then((hasThreads: boolean) =>
|
||||
wasmReady = checkThreadsSupport().then((hasThreads: boolean) =>
|
||||
hasThreads ? initMT() : initST(),
|
||||
);
|
||||
}
|
||||
|
@ -14,12 +14,13 @@ import type { WP2Module } from 'codecs/wp2/enc/wp2_enc';
|
||||
import type { EncodeOptions } from '../shared/meta';
|
||||
|
||||
import { initEmscriptenModule } from 'features/worker-utils';
|
||||
import { threads, simd } from 'wasm-feature-detect';
|
||||
import { simd } from 'wasm-feature-detect';
|
||||
import checkThreadsSupport from 'worker-shared/supports-wasm-threads';
|
||||
|
||||
let emscriptenModule: Promise<WP2Module>;
|
||||
|
||||
async function init() {
|
||||
if (await threads()) {
|
||||
if (await checkThreadsSupport()) {
|
||||
if (await simd()) {
|
||||
const wp2Encoder = await import('codecs/wp2/enc/wp2_enc_mt_simd');
|
||||
return initEmscriptenModule(wp2Encoder.default);
|
||||
|
@ -442,12 +442,6 @@ export default class Intro extends Component<Props, State> {
|
||||
>
|
||||
Privacy
|
||||
</a>
|
||||
<a
|
||||
class={style.footerLink}
|
||||
href="https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli"
|
||||
>
|
||||
Squoosh CLI
|
||||
</a>
|
||||
<a
|
||||
class={style.footerLinkWithLogo}
|
||||
href="https://github.com/GoogleChromeLabs/squoosh"
|
||||
|
BIN
src/static-build/assets/screenshot4.png
Normal file
BIN
src/static-build/assets/screenshot4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
BIN
src/static-build/assets/screenshot5.jpg
Normal file
BIN
src/static-build/assets/screenshot5.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
BIN
src/static-build/assets/screenshot6.jpg
Normal file
BIN
src/static-build/assets/screenshot6.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 70 KiB |
@ -19,15 +19,39 @@ import * as iconLarge from 'img-url:static-build/assets/icon-large.png';
|
||||
import * as screenshot1 from 'img-url:static-build/assets/screenshot1.png';
|
||||
import * as screenshot2 from 'img-url:static-build/assets/screenshot2.jpg';
|
||||
import * as screenshot3 from 'img-url:static-build/assets/screenshot3.jpg';
|
||||
import * as screenshot4 from 'img-url:static-build/assets/screenshot4.png';
|
||||
import * as screenshot5 from 'img-url:static-build/assets/screenshot5.jpg';
|
||||
import * as screenshot6 from 'img-url:static-build/assets/screenshot6.jpg';
|
||||
import dedent from 'dedent';
|
||||
import { lookup as lookupMime } from 'mime-types';
|
||||
|
||||
const manifestSize = ({ width, height }: { width: number; height: number }) =>
|
||||
`${width}x${height}`;
|
||||
interface Dimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const manifestSize = ({ width, height }: Dimensions) => `${width}x${height}`;
|
||||
const formFactor = ({ width, height }: Dimensions) =>
|
||||
width > height ? 'wide' : 'narrow';
|
||||
|
||||
const screenshots = [
|
||||
screenshot1,
|
||||
screenshot2,
|
||||
screenshot3,
|
||||
screenshot4,
|
||||
screenshot5,
|
||||
screenshot6,
|
||||
].map((screenshot) => ({
|
||||
src: screenshot.default,
|
||||
type: lookupMime(screenshot.default),
|
||||
sizes: manifestSize(screenshot),
|
||||
form_factor: formFactor(screenshot),
|
||||
}));
|
||||
|
||||
interface Output {
|
||||
[outputPath: string]: string;
|
||||
}
|
||||
|
||||
const toOutput: Output = {
|
||||
'index.html': renderPage(<IndexPage />),
|
||||
'manifest.json': JSON.stringify({
|
||||
@ -55,23 +79,7 @@ const toOutput: Output = {
|
||||
'Compress and compare images with different codecs, right in your browser.',
|
||||
lang: 'en',
|
||||
categories: ['photo', 'productivity', 'utilities'],
|
||||
screenshots: [
|
||||
{
|
||||
src: screenshot1.default,
|
||||
type: lookupMime(screenshot1.default),
|
||||
sizes: manifestSize(screenshot1),
|
||||
},
|
||||
{
|
||||
src: screenshot2.default,
|
||||
type: lookupMime(screenshot2.default),
|
||||
sizes: manifestSize(screenshot2),
|
||||
},
|
||||
{
|
||||
src: screenshot3.default,
|
||||
type: lookupMime(screenshot3.default),
|
||||
sizes: manifestSize(screenshot3),
|
||||
},
|
||||
],
|
||||
screenshots,
|
||||
share_target: {
|
||||
action: '/?utm_medium=PWA&utm_source=share-target&share-target',
|
||||
method: 'POST',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { threads, simd } from 'wasm-feature-detect';
|
||||
import { simd } from 'wasm-feature-detect';
|
||||
import webpDataUrl from 'data-url:./tiny.webp';
|
||||
import avifDataUrl from 'data-url:./tiny.avif';
|
||||
import checkThreadsSupport from 'worker-shared/supports-wasm-threads';
|
||||
|
||||
// Give TypeScript the correct global.
|
||||
declare var self: ServiceWorkerGlobalScope;
|
||||
@ -84,7 +85,7 @@ export const initial = ['/', ...initialJs];
|
||||
export const theRest = (async () => {
|
||||
const [supportsThreads, supportsSimd, supportsWebP, supportsAvif] =
|
||||
await Promise.all([
|
||||
threads(),
|
||||
checkThreadsSupport(),
|
||||
simd(),
|
||||
...[webpDataUrl, avifDataUrl].map(async (dataUrl) => {
|
||||
if (!self.createImageBitmap) return false;
|
||||
|
17
src/worker-shared/supports-wasm-threads.ts
Normal file
17
src/worker-shared/supports-wasm-threads.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { threads } from 'wasm-feature-detect';
|
||||
|
||||
export default async function checkThreadsSupport() {
|
||||
const supportsWasmThreads = await threads();
|
||||
if (!supportsWasmThreads) return false;
|
||||
|
||||
// Safari 16 shipped with WASM threads support, but it didn't ship with nested workers support.
|
||||
// This meant Squoosh failed in Safari 16, since we call our wasm from inside a worker to begin with.
|
||||
|
||||
// Right now, this check is only run from a worker.
|
||||
// More implementation is needed to run it from a page.
|
||||
if (!('importScripts' in self)) {
|
||||
throw Error('Not implemented');
|
||||
}
|
||||
|
||||
return 'Worker' in self;
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
"src/features/**/shared/**/*",
|
||||
"src/features/worker-utils/**/*",
|
||||
"src/features-worker/**/*",
|
||||
"src/worker-shared/**/*",
|
||||
"src/sw/**/*"
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user