Compare commits

..

34 Commits

Author SHA1 Message Date
Jake Archibald
3ce7bcea5f Merge branch 'dev' into basis 2021-08-26 14:39:59 +01:00
Surma
1ec3b36858 Merge remote-tracking branch 'origin/dev' into basis 2021-06-04 16:35:18 +01:00
Surma
0cb454295b Fixes after merging dev 2021-06-04 16:31:23 +01:00
Surma
2d4c1906e1 Merge remote-tracking branch 'origin/dev' into basis 2021-06-04 15:57:13 +01:00
Surma
c036866478 Map quality slider to RDO when using UASTC 2021-06-04 15:23:42 +01:00
Surma
8dc532eb09 Fix power-of-2 range slider 2021-05-27 13:53:32 +01:00
Surma
41f4f7778f Add all kinds of mipmap options 2021-05-27 13:21:50 +01:00
Surma
9278b8a1ab Move up UASTC/ETC1S dropdown 2021-05-27 11:50:43 +01:00
Surma
dc86b33634 Cleanup 2021-05-27 11:40:50 +01:00
Surma
e4832644f2 Add missing CPP file to Makefile 2021-05-27 11:14:43 +01:00
Surma
1dc24a5c36 Add Y flip option 2021-05-21 15:10:58 +01:00
Surma
8eb29bd792 Add perceptual and srgb mipmap flag 2021-05-21 14:43:56 +01:00
Surma
83db97856c Some other Makefile edits 2021-05-21 13:52:10 +01:00
Surma
925e549466 Remove old variable 2021-05-21 13:40:23 +01:00
Surma
e0895ca074 Simplify and improve Basis makefile 2021-05-21 13:38:18 +01:00
Surma
a6f3bb596a Enable debug symbols for now 2021-05-20 12:31:04 +01:00
Surma
c9bc01b111 Add no aliasing flag 2021-05-20 10:37:28 +01:00
Surma
c5bbce6a1d Disable multithreading completely 2021-05-20 10:04:49 +01:00
Surma
e8b1db5da6 Add debug messages 2021-05-19 15:58:04 +01:00
Surma
be41088fb8 Switch KTX2 2021-05-19 15:26:49 +01:00
Surma
558ee0e5ba Fix stride error 2021-05-19 13:16:12 +01:00
Surma
114d6869ea Make object files PRECIOUS 2021-05-19 13:06:07 +01:00
Surma
c9b83a8716 Remove console.log 2021-05-15 00:30:03 +01:00
Surma
bdbb8b81ac Support ECT1S and try mipmaps 2021-05-15 00:28:37 +01:00
Surma
ba5640835f Remove dead code 2021-05-14 16:25:00 +01:00
Surma
88106a09f5 Add LICENSE to basis 2021-05-14 16:17:09 +01:00
Surma
52ca417d3a Expose quality option 2021-05-14 16:12:39 +01:00
Surma
1527b1431c Decoder works 2021-05-14 15:58:38 +01:00
Surma
189d196b2b Encoder integrated 2021-05-14 15:34:12 +01:00
Surma
cf66f2a69d Decoder works 2021-05-14 14:54:05 +01:00
Surma
d3c1877e76 Trying to make the decoder work 2021-05-14 13:13:41 +01:00
Surma
bb036df1fc Avoid printf 2021-05-13 23:49:13 +01:00
Surma
2e3361af79 Encoder works 2021-05-13 23:47:07 +01:00
Surma
4dd2296eaf First compiling encoder 2021-05-13 20:09:15 +01:00
134 changed files with 8529 additions and 921 deletions

4
.gitattributes vendored
View File

@@ -1,2 +1,2 @@
/codecs/**/*.js linguist-generated -diff /codecs/**/*.js linguist-generated=true
/codecs/*/pkg*/*.d.ts linguist-generated /codecs/*/pkg*/*.d.ts linguist-generated=true

View File

@@ -1 +0,0 @@
npx lint-staged

View File

@@ -2,6 +2,10 @@
[Squoosh] is an image compression web app that reduces image sizes through numerous formats. [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 # Privacy
Squoosh does not send your image to a server. All image compression processes locally. Squoosh does not send your image to a server. All image compression processes locally.

3
cli/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
build
.DS_Store

1
cli/.npmignore Normal file
View File

@@ -0,0 +1 @@
node_modules

0
cli/.npmrc Normal file
View File

59
cli/README.md Normal file
View File

@@ -0,0 +1,59 @@
# 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 doesnt 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 Normal file
View File

@@ -0,0 +1,613 @@
{
"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=="
}
}
}

32
cli/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"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"
}
}

234
cli/src/index.js Executable file
View File

@@ -0,0 +1,234 @@
#!/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 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();
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 image = imagePool.ingestImage(file);
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);

View File

@@ -14,7 +14,6 @@
// for comlink // for comlink
"src/features/**/worker/**/*", "src/features/**/worker/**/*",
"src/features-worker/**/*", "src/features-worker/**/*",
"src/features/worker-utils/**/*", "src/features/worker-utils/**/*"
"src/worker-shared/**/*"
] ]
} }

View File

@@ -1,9 +1,9 @@
# using libavif from https://github.com/AOMediaCodec/libavif # libavif and libaom versions are from
CODEC_URL = https://github.com/AOMediaCodec/libavif/archive/refs/tags/v1.0.1.tar.gz # https://docs.google.com/document/d/1wEEA5rRU7wT54k41u3qyZIZHDCJArIMzLuzsrLAwaK8/edit
CODEC_URL = https://github.com/AOMediaCodec/libavif/archive/1c39e772c2c0d687691dd4b589a12c323f5f767d.tar.gz
CODEC_PACKAGE = node_modules/libavif.tar.gz CODEC_PACKAGE = node_modules/libavif.tar.gz
# using libaom from https://aomedia.googlesource.com/aom LIBAOM_URL = https://aomedia.googlesource.com/aom/+archive/v3.1.0.tar.gz
LIBAOM_URL = https://aomedia.googlesource.com/aom/+archive/v3.7.0.tar.gz
LIBAOM_PACKAGE = node_modules/libaom.tar.gz LIBAOM_PACKAGE = node_modules/libaom.tar.gz
export CODEC_DIR = node_modules/libavif export CODEC_DIR = node_modules/libavif
@@ -11,6 +11,7 @@ export BUILD_DIR = node_modules/build
export LIBAOM_DIR = node_modules/libaom export LIBAOM_DIR = node_modules/libaom
override CFLAGS += "-Wno-unused-macros" override CFLAGS += "-Wno-unused-macros"
export
OUT_ENC_JS = enc/avif_enc.js OUT_ENC_JS = enc/avif_enc.js
OUT_NODE_ENC_JS = enc/avif_node_enc.js OUT_NODE_ENC_JS = enc/avif_node_enc.js
@@ -27,9 +28,10 @@ HELPER_MAKEFLAGS := -f helper.Makefile
.PHONY: all clean .PHONY: all clean
all: $(OUT_ENC_JS) $(OUT_DEC_JS) $(OUT_ENC_MT_JS) all: $(OUT_ENC_JS) $(OUT_DEC_JS) $(OUT_ENC_MT_JS) $(OUT_NODE_ENC_JS) $(OUT_NODE_ENC_MT_JS) $(OUT_NODE_DEC_JS)
$(OUT_ENC_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt $(OUT_NODE_ENC_JS) $(OUT_NODE_ENC_MT_JS): ENVIRONMENT=node
$(OUT_NODE_ENC_JS) $(OUT_ENC_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
$(MAKE) \ $(MAKE) \
$(HELPER_MAKEFLAGS) \ $(HELPER_MAKEFLAGS) \
OUT_JS=$@ \ OUT_JS=$@ \
@@ -40,9 +42,9 @@ $(OUT_ENC_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLis
-DCONFIG_AV1_HIGHBITDEPTH=0 \ -DCONFIG_AV1_HIGHBITDEPTH=0 \
" \ " \
ENVIRONMENT=$(ENVIRONMENT) \ ENVIRONMENT=$(ENVIRONMENT) \
LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_DECODE=0 -DAVIF_CHROMA_DOWNSAMPLING_SHARP_YUV=ON" LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_DECODE=0"
$(OUT_ENC_MT_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt $(OUT_ENC_MT_JS) $(OUT_NODE_ENC_MT_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
$(MAKE) \ $(MAKE) \
$(HELPER_MAKEFLAGS) \ $(HELPER_MAKEFLAGS) \
OUT_JS=$@ \ OUT_JS=$@ \
@@ -52,10 +54,11 @@ $(OUT_ENC_MT_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMake
-DCONFIG_AV1_HIGHBITDEPTH=0 \ -DCONFIG_AV1_HIGHBITDEPTH=0 \
" \ " \
ENVIRONMENT=$(ENVIRONMENT) \ ENVIRONMENT=$(ENVIRONMENT) \
LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_DECODE=0 -DAVIF_CHROMA_DOWNSAMPLING_SHARP_YUV=ON" \ LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_DECODE=0" \
OUT_FLAGS="-pthread" OUT_FLAGS="-pthread"
$(OUT_DEC_JS): $(OUT_DEC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt $(OUT_NODE_DEC_JS): ENVIRONMENT=node
$(OUT_NODE_DEC_JS) $(OUT_DEC_JS): $(OUT_DEC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
$(MAKE) \ $(MAKE) \
$(HELPER_MAKEFLAGS) \ $(HELPER_MAKEFLAGS) \
OUT_JS=$@ \ OUT_JS=$@ \
@@ -65,7 +68,7 @@ $(OUT_DEC_JS): $(OUT_DEC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLis
-DCONFIG_MULTITHREAD=0 \ -DCONFIG_MULTITHREAD=0 \
" \ " \
ENVIRONMENT=$(ENVIRONMENT) \ ENVIRONMENT=$(ENVIRONMENT) \
LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_ENCODE=0 -DAVIF_CHROMA_DOWNSAMPLING_SHARP_YUV=0 -DAVIF_LOCAL_LIBSHARPYUV=0" LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_ENCODE=0"
$(CODEC_PACKAGE): $(CODEC_PACKAGE):
mkdir -p $(@D) mkdir -p $(@D)

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -3,22 +3,15 @@
#include <emscripten/val.h> #include <emscripten/val.h>
#include "avif/avif.h" #include "avif/avif.h"
#define RETURN_NULL_IF_NOT_EQUALS(val1, val2) \
if (val1 != val2) \
return val::null();
#define RETURN_NULL_IF_EQUALS(val1, val2) \
if (val1 == val2) \
return val::null();
using namespace emscripten; using namespace emscripten;
struct AvifOptions { struct AvifOptions {
// [0 - 100] // [0 - 63]
// 0 = worst quality // 0 = lossless
// 100 = lossless // 63 = worst quality
int quality; int cqLevel;
// As above, but -1 means 'use quality' // As above, but -1 means 'use cqLevel'
int qualityAlpha; int cqAlphaLevel;
// [0 - 6] // [0 - 6]
// Creates 2^n tiles in that dimension // Creates 2^n tiles in that dimension
int tileRowsLog2; int tileRowsLog2;
@@ -42,15 +35,11 @@ struct AvifOptions {
int tune; int tune;
// 0-50 // 0-50
int denoiseLevel; int denoiseLevel;
// toggles AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV
bool enableSharpDownsampling;
}; };
thread_local const val Uint8Array = val::global("Uint8Array"); thread_local const val Uint8Array = val::global("Uint8Array");
val encode(std::string buffer, int width, int height, AvifOptions options) { val encode(std::string buffer, int width, int height, AvifOptions options) {
avifResult status; // To check the return status for avif API's
avifRWData output = AVIF_DATA_EMPTY; avifRWData output = AVIF_DATA_EMPTY;
int depth = 8; int depth = 8;
avifPixelFormat format; avifPixelFormat format;
@@ -69,12 +58,11 @@ val encode(std::string buffer, int width, int height, AvifOptions options) {
break; break;
} }
bool lossless = options.quality == AVIF_QUALITY_LOSSLESS && bool lossless = options.cqLevel == AVIF_QUANTIZER_LOSSLESS &&
(options.qualityAlpha == -1 || options.qualityAlpha == AVIF_QUALITY_LOSSLESS) && options.cqAlphaLevel <= AVIF_QUANTIZER_LOSSLESS &&
format == AVIF_PIXEL_FORMAT_YUV444; format == AVIF_PIXEL_FORMAT_YUV444;
avifImage* image = avifImageCreate(width, height, depth, format); avifImage* image = avifImageCreate(width, height, depth, format);
RETURN_NULL_IF_EQUALS(image, NULL);
if (lossless) { if (lossless) {
image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY; image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY;
@@ -88,49 +76,40 @@ val encode(std::string buffer, int width, int height, AvifOptions options) {
avifRGBImageSetDefaults(&srcRGB, image); avifRGBImageSetDefaults(&srcRGB, image);
srcRGB.pixels = rgba; srcRGB.pixels = rgba;
srcRGB.rowBytes = width * 4; srcRGB.rowBytes = width * 4;
if (options.enableSharpDownsampling) { avifImageRGBToYUV(image, &srcRGB);
printf("Enabling AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV\n");
srcRGB.chromaDownsampling = AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV;
}
status = avifImageRGBToYUV(image, &srcRGB);
if (status == AVIF_RESULT_NOT_IMPLEMENTED) {
printf("libsharpyuv not implemented methinks\n");
}
RETURN_NULL_IF_NOT_EQUALS(status, AVIF_RESULT_OK);
avifEncoder* encoder = avifEncoderCreate(); avifEncoder* encoder = avifEncoderCreate();
RETURN_NULL_IF_EQUALS(encoder, NULL);
if (lossless) { if (lossless) {
encoder->quality = AVIF_QUALITY_LOSSLESS; encoder->minQuantizer = AVIF_QUANTIZER_LOSSLESS;
encoder->qualityAlpha = AVIF_QUALITY_LOSSLESS; encoder->maxQuantizer = AVIF_QUANTIZER_LOSSLESS;
encoder->minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
encoder->maxQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
} else { } else {
status = avifEncoderSetCodecSpecificOption(encoder, "sharpness", encoder->minQuantizer = AVIF_QUANTIZER_BEST_QUALITY;
encoder->maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY;
encoder->minQuantizerAlpha = AVIF_QUANTIZER_BEST_QUALITY;
encoder->maxQuantizerAlpha = AVIF_QUANTIZER_WORST_QUALITY;
avifEncoderSetCodecSpecificOption(encoder, "end-usage", "q");
avifEncoderSetCodecSpecificOption(encoder, "cq-level", std::to_string(options.cqLevel).c_str());
avifEncoderSetCodecSpecificOption(encoder, "sharpness",
std::to_string(options.sharpness).c_str()); std::to_string(options.sharpness).c_str());
RETURN_NULL_IF_NOT_EQUALS(status, AVIF_RESULT_OK);
// Set base quality if (options.cqAlphaLevel != -1) {
encoder->quality = options.quality; avifEncoderSetCodecSpecificOption(encoder, "alpha:cq-level",
// Conditionally set alpha quality std::to_string(options.cqAlphaLevel).c_str());
if (options.qualityAlpha == -1) {
encoder->qualityAlpha = options.quality;
} else {
encoder->qualityAlpha = options.qualityAlpha;
} }
if (options.tune == 2 || (options.tune == 0 && options.quality >= 50)) { if (options.tune == 2 || (options.tune == 0 && options.cqLevel <= 32)) {
status = avifEncoderSetCodecSpecificOption(encoder, "tune", "ssim"); avifEncoderSetCodecSpecificOption(encoder, "tune", "ssim");
RETURN_NULL_IF_NOT_EQUALS(status, AVIF_RESULT_OK);
} }
if (options.chromaDeltaQ) { if (options.chromaDeltaQ) {
status = avifEncoderSetCodecSpecificOption(encoder, "enable-chroma-deltaq", "1"); avifEncoderSetCodecSpecificOption(encoder, "enable-chroma-deltaq", "1");
RETURN_NULL_IF_NOT_EQUALS(status, AVIF_RESULT_OK);
} }
status = avifEncoderSetCodecSpecificOption(encoder, "color:denoise-noise-level", avifEncoderSetCodecSpecificOption(encoder, "color:denoise-noise-level",
std::to_string(options.denoiseLevel).c_str()); std::to_string(options.denoiseLevel).c_str());
RETURN_NULL_IF_NOT_EQUALS(status, AVIF_RESULT_OK);
} }
encoder->maxThreads = emscripten_num_logical_cores(); encoder->maxThreads = emscripten_num_logical_cores();
@@ -152,8 +131,8 @@ val encode(std::string buffer, int width, int height, AvifOptions options) {
EMSCRIPTEN_BINDINGS(my_module) { EMSCRIPTEN_BINDINGS(my_module) {
value_object<AvifOptions>("AvifOptions") value_object<AvifOptions>("AvifOptions")
.field("quality", &AvifOptions::quality) .field("cqLevel", &AvifOptions::cqLevel)
.field("qualityAlpha", &AvifOptions::qualityAlpha) .field("cqAlphaLevel", &AvifOptions::cqAlphaLevel)
.field("tileRowsLog2", &AvifOptions::tileRowsLog2) .field("tileRowsLog2", &AvifOptions::tileRowsLog2)
.field("tileColsLog2", &AvifOptions::tileColsLog2) .field("tileColsLog2", &AvifOptions::tileColsLog2)
.field("speed", &AvifOptions::speed) .field("speed", &AvifOptions::speed)
@@ -161,8 +140,7 @@ EMSCRIPTEN_BINDINGS(my_module) {
.field("sharpness", &AvifOptions::sharpness) .field("sharpness", &AvifOptions::sharpness)
.field("tune", &AvifOptions::tune) .field("tune", &AvifOptions::tune)
.field("denoiseLevel", &AvifOptions::denoiseLevel) .field("denoiseLevel", &AvifOptions::denoiseLevel)
.field("subsample", &AvifOptions::subsample) .field("subsample", &AvifOptions::subsample);
.field("enableSharpDownsampling", &AvifOptions::enableSharpDownsampling);
function("encode", &encode); function("encode", &encode);
} }

View File

@@ -5,16 +5,15 @@ export const enum AVIFTune {
} }
export interface EncodeOptions { export interface EncodeOptions {
quality: number; cqLevel: number;
qualityAlpha: number;
denoiseLevel: number; denoiseLevel: number;
cqAlphaLevel: number;
tileRowsLog2: number; tileRowsLog2: number;
tileColsLog2: number; tileColsLog2: number;
speed: number; speed: number;
subsample: number; subsample: number;
chromaDeltaQ: boolean; chromaDeltaQ: boolean;
sharpness: number; sharpness: number;
enableSharpDownsampling: boolean;
tune: AVIFTune; tune: AVIFTune;
} }

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -10,12 +10,6 @@
# $(LIBAVIF_FLAGS) # $(LIBAVIF_FLAGS)
# $(ENVIRONMENT) # $(ENVIRONMENT)
# Take from libavif/ext/libsharpyuv.cmd
WEBP_URL = https://chromium.googlesource.com/webm/libwebp
WEBP_COMMIT = e2c85878f6a33f29948b43d3492d9cdaf801aa54
LIBSHARPYUV_DIR = $(CODEC_DIR)/ext/libwebp
# $(OUT_JS) is something like "enc/avif_enc.js" or "enc/avif_enc_mt.js"
OUT_BUILD_DIR := $(BUILD_DIR)/$(basename $(OUT_JS)) OUT_BUILD_DIR := $(BUILD_DIR)/$(basename $(OUT_JS))
CODEC_BUILD_DIR := $(OUT_BUILD_DIR)/libavif CODEC_BUILD_DIR := $(OUT_BUILD_DIR)/libavif
@@ -24,9 +18,6 @@ CODEC_OUT := $(CODEC_BUILD_DIR)/libavif.a
LIBAOM_BUILD_DIR := $(OUT_BUILD_DIR)/libaom LIBAOM_BUILD_DIR := $(OUT_BUILD_DIR)/libaom
LIBAOM_OUT := $(LIBAOM_BUILD_DIR)/libaom.a LIBAOM_OUT := $(LIBAOM_BUILD_DIR)/libaom.a
LIBSHARPYUV_BUILD_DIR := $(OUT_BUILD_DIR)/libsharpyuvLOLOLOL
LIBSHARPYUV_OUT := $(LIBSHARPYUV_BUILD_DIR)/libsharpyuv.a
OUT_WASM = $(OUT_JS:.js=.wasm) OUT_WASM = $(OUT_JS:.js=.wasm)
OUT_WORKER=$(OUT_JS:.js=.worker.js) OUT_WORKER=$(OUT_JS:.js=.worker.js)
@@ -34,13 +25,6 @@ OUT_WORKER=$(OUT_JS:.js=.worker.js)
all: $(OUT_JS) all: $(OUT_JS)
# Only add libsharpyuv as a dependency for encoders.
# Yes, that if statement is true for encoders.
ifneq (,$(findstring enc/, $(OUT_JS)))
$(OUT_JS): $(LIBSHARPYUV_OUT)
$(CODEC_OUT): $(LIBSHARPYUV_OUT)
endif
$(OUT_JS): $(OUT_CPP) $(LIBAOM_OUT) $(CODEC_OUT) $(OUT_JS): $(OUT_CPP) $(LIBAOM_OUT) $(CODEC_OUT)
$(CXX) \ $(CXX) \
-I $(CODEC_DIR)/include \ -I $(CODEC_DIR)/include \
@@ -55,7 +39,6 @@ $(OUT_JS): $(OUT_CPP) $(LIBAOM_OUT) $(CODEC_OUT)
$(CODEC_OUT): $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_OUT) $(CODEC_OUT): $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_OUT)
emcmake cmake \ emcmake cmake \
-DCMAKE_LIBRARY_PATH=$(LIBSHARPYUV_BUILD_DIR) \
-DCMAKE_BUILD_TYPE=Release \ -DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=0 \ -DBUILD_SHARED_LIBS=0 \
-DAVIF_CODEC_AOM=1 \ -DAVIF_CODEC_AOM=1 \
@@ -84,21 +67,6 @@ $(LIBAOM_OUT): $(LIBAOM_DIR)/CMakeLists.txt
$(LIBAOM_DIR) && \ $(LIBAOM_DIR) && \
$(MAKE) -C $(LIBAOM_BUILD_DIR) $(MAKE) -C $(LIBAOM_BUILD_DIR)
$(LIBSHARPYUV_OUT): $(LIBSHARPYUV_DIR)/CMakeLists.txt
emcmake cmake \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_BUILD_TYPE=Release \
-B $(LIBSHARPYUV_BUILD_DIR) \
$(LIBSHARPYUV_DIR)
$(MAKE) -C $(LIBSHARPYUV_BUILD_DIR) sharpyuv
$(LIBSHARPYUV_DIR)/CMakeLists.txt: $(CODEC_DIR)/CMakeLists.txt
cd $(CODEC_DIR)/ext && \
git clone $(WEBP_URL) --single-branch libwebp && \
cd libwebp && \
git checkout $(WEBP_COMMIT)
clean: clean:
$(RM) $(OUT_JS) $(OUT_WASM) $(OUT_WORKER) $(RM) $(OUT_JS) $(OUT_WASM) $(OUT_WORKER)
$(MAKE) -C $(CODEC_BUILD_DIR) clean $(MAKE) -C $(CODEC_BUILD_DIR) clean

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

83
codecs/basis/Makefile Normal file
View File

@@ -0,0 +1,83 @@
CODEC_URL := https://github.com/BinomialLLC/basis_universal/archive/refs/tags/v1.15_rel2.tar.gz
CODEC_DIR := node_modules/basis
CODEC_BUILD_DIR := $(CODEC_DIR)/build
CODEC_LIB := $(CODEC_BUILD_DIR)/basis.a
ENVIRONMENT = worker
OUT_JS := enc/basis_enc.js dec/basis_dec.js
OUT_WASM := $(OUT_JS:.js=.wasm)
COMMON_FLAGS := -O3 -fno-strict-aliasing
override CXXFLAGS += $(COMMON_FLAGS)
override CFLAGS += $(COMMAN_FLAGS)
CODEC_CPP_SOURCE_FILES := \
encoder/basisu_comp.cpp \
encoder/basisu_enc.cpp \
encoder/basisu_backend.cpp \
encoder/basisu_basis_file.cpp \
encoder/basisu_etc.cpp \
encoder/basisu_uastc_enc.cpp \
encoder/basisu_gpu_texture.cpp \
encoder/basisu_frontend.cpp \
encoder/basisu_bc7enc.cpp \
encoder/basisu_pvrtc1_4.cpp \
encoder/basisu_astc_decomp.cpp \
encoder/basisu_global_selector_palette_helpers.cpp \
encoder/basisu_resampler.cpp \
encoder/basisu_kernels_sse.cpp \
encoder/basisu_resample_filters.cpp \
encoder/jpgd.cpp \
encoder/lodepng.cpp \
transcoder/basisu_transcoder.cpp
CODEC_C_SOURCE_FILES := \
encoder/apg_bmp.c \
zstd/zstd.c
.PHONY: all clean
all: $(CODEC_DIR) $(OUT_JS)
# Define dependencies for all variations of build artifacts.
$(filter enc/%,$(OUT_JS)): enc/basis_enc.cpp
$(filter dec/%,$(OUT_JS)): dec/basis_dec.cpp
# TODO: Make it build for node
# enc/mozjpeg_node_enc.js dec/mozjpeg_node_dec.js: ENVIRONMENT = node
%.js: $(CODEC_LIB)
$(CXX) \
-I $(CODEC_DIR)/encoder \
-I $(CODEC_DIR)/transcoder \
${CXXFLAGS} \
${LDFLAGS} \
--closure 1 \
--bind \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s TEXTDECODER=2 \
-s ENVIRONMENT=$(ENVIRONMENT) \
-s EXPORT_ES6=1 \
-o $@ \
$+
$(CODEC_LIB): $(CODEC_DIR)
mkdir -p $(CODEC_BUILD_DIR)
cd $(CODEC_BUILD_DIR) && \
$(CXX) \
${CXXFLAGS} \
-c $(addprefix ../, $(CODEC_CPP_SOURCE_FILES))
cd $(CODEC_BUILD_DIR) && \
$(CC) \
${CFLAGS} \
-c $(addprefix ../, $(CODEC_C_SOURCE_FILES))
$(AR) rc $(CODEC_LIB) $(CODEC_BUILD_DIR)/*.o
$(CODEC_DIR):
mkdir -p $@
curl -sL $(CODEC_URL) | tar xz --strip 1 -C $@
clean:
$(RM) -r $(OUT_JS) $(OUT_WASM) $(CODEC_BUILD_DIR)

View File

@@ -0,0 +1,46 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <inttypes.h>
#include <string>
#include "basisu_global_selector_palette.h"
#include "basisu_transcoder.h"
using namespace emscripten;
using namespace basisu;
thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");
thread_local const val ImageData = val::global("ImageData");
val decode(std::string data) {
basist::basisu_transcoder_init();
basist::etc1_global_selector_codebook sel_codebook = basist::etc1_global_selector_codebook(
basist::g_global_selector_cb_size, basist::g_global_selector_cb);
basist::ktx2_transcoder transcoder = basist::ktx2_transcoder(&sel_codebook);
const void* dataPtr = reinterpret_cast<const void*>(data.c_str());
auto dataSize = data.size();
transcoder.init(dataPtr, dataSize);
auto header = transcoder.get_header();
auto image_width = static_cast<uint32_t>(header.m_pixel_width);
auto image_height = static_cast<uint32_t>(header.m_pixel_height);
transcoder.start_transcoding();
auto buffer = std::vector<uint8_t>(image_width * image_height * 4);
auto ok = transcoder.transcode_image_level(
0 /* level_index */, 0 /* layer_index */, 0 /* face_index */, buffer.data(),
buffer.size() / 4, basist::transcoder_texture_format::cTFRGBA32, 0 /* decode_flags */,
image_width /* output_row_pitch_in_blocks_or_pixels */);
if (!ok) {
return val(std::string("Could not decode"));
}
auto img_data_data = Uint8ClampedArray.new_(typed_memory_view(buffer.size(), buffer.data()));
auto imgData = ImageData.new_(img_data_data, image_width, image_height);
return imgData;
}
EMSCRIPTEN_BINDINGS(my_module) {
function("decode", &decode);
}

7
codecs/basis/dec/basis_dec.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
export interface BasisModule extends EmscriptenWasm.Module {
decode(data: BufferSource): ImageData | null;
}
declare var moduleFactory: EmscriptenWasm.ModuleFactory<BasisModule>;
export default moduleFactory;

56
codecs/basis/dec/basis_dec.js generated Normal file
View File

@@ -0,0 +1,56 @@
var Module = (function() {
var _scriptDir = import.meta.url;
return (
function(Module) {
Module = Module || {};
var e;e||(e=typeof Module !== 'undefined' ? Module : {});var aa,r;e.ready=new Promise(function(a,b){aa=a;r=b});var t={},u;for(u in e)e.hasOwnProperty(u)&&(t[u]=e[u]);var v="",w;v=self.location.href;_scriptDir&&(v=_scriptDir);0!==v.indexOf("blob:")?v=v.substr(0,v.lastIndexOf("/")+1):v="";w=function(a){var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)};var ba=e.print||console.log.bind(console),y=e.printErr||console.warn.bind(console);
for(u in t)t.hasOwnProperty(u)&&(e[u]=t[u]);t=null;var z;e.wasmBinary&&(z=e.wasmBinary);var noExitRuntime=e.noExitRuntime||!0;"object"!==typeof WebAssembly&&A("no native wasm support detected");var C,ca=!1,da=new TextDecoder("utf8");function D(a,b){if(!a)return"";b=a+b;for(var c=a;!(c>=b)&&E[c];)++c;return da.decode(E.subarray(a,c))}
function ea(a,b,c){var d=E;if(0<c){c=b+c-1;for(var f=0;f<a.length;++f){var g=a.charCodeAt(f);if(55296<=g&&57343>=g){var k=a.charCodeAt(++f);g=65536+((g&1023)<<10)|k&1023}if(127>=g){if(b>=c)break;d[b++]=g}else{if(2047>=g){if(b+1>=c)break;d[b++]=192|g>>6}else{if(65535>=g){if(b+2>=c)break;d[b++]=224|g>>12}else{if(b+3>=c)break;d[b++]=240|g>>18;d[b++]=128|g>>12&63}d[b++]=128|g>>6&63}d[b++]=128|g&63}}d[b]=0}}var fa=new TextDecoder("utf-16le");
function ha(a,b){var c=a>>1;for(b=c+b/2;!(c>=b)&&F[c];)++c;return fa.decode(E.subarray(a,c<<1))}function ia(a,b,c){void 0===c&&(c=2147483647);if(2>c)return 0;c-=2;var d=b;c=c<2*a.length?c/2:a.length;for(var f=0;f<c;++f)G[b>>1]=a.charCodeAt(f),b+=2;G[b>>1]=0;return b-d}function ja(a){return 2*a.length}function ka(a,b){for(var c=0,d="";!(c>=b/4);){var f=I[a+4*c>>2];if(0==f)break;++c;65536<=f?(f-=65536,d+=String.fromCharCode(55296|f>>10,56320|f&1023)):d+=String.fromCharCode(f)}return d}
function la(a,b,c){void 0===c&&(c=2147483647);if(4>c)return 0;var d=b;c=d+c-4;for(var f=0;f<a.length;++f){var g=a.charCodeAt(f);if(55296<=g&&57343>=g){var k=a.charCodeAt(++f);g=65536+((g&1023)<<10)|k&1023}I[b>>2]=g;b+=4;if(b+4>c)break}I[b>>2]=0;return b-d}function ma(a){for(var b=0,c=0;c<a.length;++c){var d=a.charCodeAt(c);55296<=d&&57343>=d&&++c;b+=4}return b}var na,oa,E,G,F,I,J,pa,qa;
function ra(){var a=C.buffer;na=a;e.HEAP8=oa=new Int8Array(a);e.HEAP16=G=new Int16Array(a);e.HEAP32=I=new Int32Array(a);e.HEAPU8=E=new Uint8Array(a);e.HEAPU16=F=new Uint16Array(a);e.HEAPU32=J=new Uint32Array(a);e.HEAPF32=pa=new Float32Array(a);e.HEAPF64=qa=new Float64Array(a)}var L,sa=[],ta=[],ua=[];function va(){var a=e.preRun.shift();sa.unshift(a)}var M=0,wa=null,N=null;e.preloadedImages={};e.preloadedAudios={};
function A(a){if(e.onAbort)e.onAbort(a);y(a);ca=!0;a=new WebAssembly.RuntimeError("abort("+a+"). Build with -s ASSERTIONS=1 for more info.");r(a);throw a;}var O=(new URL("basis_dec.wasm",import.meta.url)).toString();function xa(){try{if(O==O&&z)return new Uint8Array(z);if(w)return w(O);throw"both async and sync fetching of the wasm failed";}catch(a){A(a)}}
function ya(){return z||"function"!==typeof fetch?Promise.resolve().then(function(){return xa()}):fetch(O,{credentials:"same-origin"}).then(function(a){if(!a.ok)throw"failed to load wasm binary file at '"+O+"'";return a.arrayBuffer()}).catch(function(){return xa()})}function za(a){for(;0<a.length;){var b=a.shift();if("function"==typeof b)b(e);else{var c=b.M;"number"===typeof c?void 0===b.I?L.get(c)():L.get(c)(b.I):c(void 0===b.I?null:b.I)}}}
function Aa(a){switch(a){case 1:return 0;case 2:return 1;case 4:return 2;case 8:return 3;default:throw new TypeError("Unknown type size: "+a);}}var Ba=void 0;function P(a){for(var b="";E[a];)b+=Ba[E[a++]];return b}var Q={},R={},S={};function Ca(a){if(void 0===a)return"_unknown";a=a.replace(/[^a-zA-Z0-9_]/g,"$");var b=a.charCodeAt(0);return 48<=b&&57>=b?"_"+a:a}
function Da(a,b){a=Ca(a);return(new Function("body","return function "+a+'() {\n "use strict"; return body.apply(this, arguments);\n};\n'))(b)}function Ea(a){var b=Error,c=Da(a,function(d){this.name=a;this.message=d;d=Error(d).stack;void 0!==d&&(this.stack=this.toString()+"\n"+d.replace(/^Error(:[^\n]*)?\n/,""))});c.prototype=Object.create(b.prototype);c.prototype.constructor=c;c.prototype.toString=function(){return void 0===this.message?this.name:this.name+": "+this.message};return c}
var Fa=void 0;function T(a){throw new Fa(a);}var Ga=void 0;function Ha(a,b){function c(h){h=b(h);if(h.length!==d.length)throw new Ga("Mismatched type converter count");for(var m=0;m<d.length;++m)U(d[m],h[m])}var d=[];d.forEach(function(h){S[h]=a});var f=Array(a.length),g=[],k=0;a.forEach(function(h,m){R.hasOwnProperty(h)?f[m]=R[h]:(g.push(h),Q.hasOwnProperty(h)||(Q[h]=[]),Q[h].push(function(){f[m]=R[h];++k;k===g.length&&c(f)}))});0===g.length&&c(f)}
function U(a,b,c){c=c||{};if(!("argPackAdvance"in b))throw new TypeError("registerType registeredInstance requires argPackAdvance");var d=b.name;a||T('type "'+d+'" must have a positive integer typeid pointer');if(R.hasOwnProperty(a)){if(c.L)return;T("Cannot register type '"+d+"' twice")}R[a]=b;delete S[a];Q.hasOwnProperty(a)&&(b=Q[a],delete Q[a],b.forEach(function(f){f()}))}var Ia=[],V=[{},{value:void 0},{value:null},{value:!0},{value:!1}];
function La(a){4<a&&0===--V[a].J&&(V[a]=void 0,Ia.push(a))}function W(a){switch(a){case void 0:return 1;case null:return 2;case !0:return 3;case !1:return 4;default:var b=Ia.length?Ia.pop():V.length;V[b]={J:1,value:a};return b}}function Ma(a){return this.fromWireType(J[a>>2])}function Na(a){if(null===a)return"null";var b=typeof a;return"object"===b||"array"===b||"function"===b?a.toString():""+a}
function Oa(a,b){switch(b){case 2:return function(c){return this.fromWireType(pa[c>>2])};case 3:return function(c){return this.fromWireType(qa[c>>3])};default:throw new TypeError("Unknown float type: "+a);}}function Pa(a){var b=Function;if(!(b instanceof Function))throw new TypeError("new_ called with constructor type "+typeof b+" which is not a function");var c=Da(b.name||"unknownFunctionName",function(){});c.prototype=b.prototype;c=new c;a=b.apply(c,a);return a instanceof Object?a:c}
function Qa(a){for(;a.length;){var b=a.pop();a.pop()(b)}}function Ra(a,b){var c=e;if(void 0===c[a].G){var d=c[a];c[a]=function(){c[a].G.hasOwnProperty(arguments.length)||T("Function '"+b+"' called with an invalid number of arguments ("+arguments.length+") - expects one of ("+c[a].G+")!");return c[a].G[arguments.length].apply(this,arguments)};c[a].G=[];c[a].G[d.K]=d}}
function Sa(a,b,c){e.hasOwnProperty(a)?((void 0===c||void 0!==e[a].G&&void 0!==e[a].G[c])&&T("Cannot register public name '"+a+"' twice"),Ra(a,a),e.hasOwnProperty(c)&&T("Cannot register multiple overloads of a function with the same number of arguments ("+c+")!"),e[a].G[c]=b):(e[a]=b,void 0!==c&&(e[a].O=c))}function Ta(a,b){for(var c=[],d=0;d<a;d++)c.push(I[(b>>2)+d]);return c}
function Ua(a,b){var c=[];return function(){c.length=arguments.length;for(var d=0;d<arguments.length;d++)c[d]=arguments[d];a.includes("j")?(d=e["dynCall_"+a],d=c&&c.length?d.apply(null,[b].concat(c)):d.call(null,b)):d=L.get(b).apply(null,c);return d}}function Va(a,b){a=P(a);var c=a.includes("j")?Ua(a,b):L.get(b);"function"!==typeof c&&T("unknown function pointer with signature "+a+": "+b);return c}var Wa=void 0;function Xa(a){a=Ya(a);var b=P(a);X(a);return b}
function Za(a,b){function c(g){f[g]||R[g]||(S[g]?S[g].forEach(c):(d.push(g),f[g]=!0))}var d=[],f={};b.forEach(c);throw new Wa(a+": "+d.map(Xa).join([", "]));}function $a(a,b,c){switch(b){case 0:return c?function(d){return oa[d]}:function(d){return E[d]};case 1:return c?function(d){return G[d>>1]}:function(d){return F[d>>1]};case 2:return c?function(d){return I[d>>2]}:function(d){return J[d>>2]};default:throw new TypeError("Unknown integer type: "+a);}}var ab={};
function bb(){return"object"===typeof globalThis?globalThis:Function("return this")()}function cb(a,b){var c=R[a];void 0===c&&T(b+" has unknown type "+Xa(a));return c}for(var db={},eb=[null,[],[]],fb=Array(256),Y=0;256>Y;++Y)fb[Y]=String.fromCharCode(Y);Ba=fb;Fa=e.BindingError=Ea("BindingError");Ga=e.InternalError=Ea("InternalError");e.count_emval_handles=function(){for(var a=0,b=5;b<V.length;++b)void 0!==V[b]&&++a;return a};
e.get_first_emval=function(){for(var a=5;a<V.length;++a)if(void 0!==V[a])return V[a];return null};Wa=e.UnboundTypeError=Ea("UnboundTypeError");
var hb={a:function(a,b,c,d){A("Assertion failed: "+D(a)+", at: "+[b?D(b):"unknown filename",c,d?D(d):"unknown function"])},g:function(){},r:function(){},x:function(a,b,c,d,f){var g=Aa(c);b=P(b);U(a,{name:b,fromWireType:function(k){return!!k},toWireType:function(k,h){return h?d:f},argPackAdvance:8,readValueFromPointer:function(k){if(1===c)var h=oa;else if(2===c)h=G;else if(4===c)h=I;else throw new TypeError("Unknown boolean type size: "+b);return this.fromWireType(h[k>>g])},H:null})},w:function(a,
b){b=P(b);U(a,{name:b,fromWireType:function(c){var d=V[c].value;La(c);return d},toWireType:function(c,d){return W(d)},argPackAdvance:8,readValueFromPointer:Ma,H:null})},l:function(a,b,c){c=Aa(c);b=P(b);U(a,{name:b,fromWireType:function(d){return d},toWireType:function(d,f){if("number"!==typeof f&&"boolean"!==typeof f)throw new TypeError('Cannot convert "'+Na(f)+'" to '+this.name);return f},argPackAdvance:8,readValueFromPointer:Oa(b,c),H:null})},o:function(a,b,c,d,f,g){var k=Ta(b,c);a=P(a);f=Va(d,
f);Sa(a,function(){Za("Cannot call "+a+" due to unbound types",k)},b-1);Ha(k,function(h){var m=a,l=a;h=[h[0],null].concat(h.slice(1));var p=f,q=h.length;2>q&&T("argTypes array size mismatch! Must at least get return value and 'this' types!");for(var x=null!==h[1]&&!1,B=!1,n=1;n<h.length;++n)if(null!==h[n]&&void 0===h[n].H){B=!0;break}var Ja="void"!==h[0].name,H="",K="";for(n=0;n<q-2;++n)H+=(0!==n?", ":"")+"arg"+n,K+=(0!==n?", ":"")+"arg"+n+"Wired";l="return function "+Ca(l)+"("+H+") {\nif (arguments.length !== "+
(q-2)+") {\nthrowBindingError('function "+l+" called with ' + arguments.length + ' arguments, expected "+(q-2)+" args!');\n}\n";B&&(l+="var destructors = [];\n");var Ka=B?"destructors":"null";H="throwBindingError invoker fn runDestructors retType classParam".split(" ");p=[T,p,g,Qa,h[0],h[1]];x&&(l+="var thisWired = classParam.toWireType("+Ka+", this);\n");for(n=0;n<q-2;++n)l+="var arg"+n+"Wired = argType"+n+".toWireType("+Ka+", arg"+n+"); // "+h[n+2].name+"\n",H.push("argType"+n),p.push(h[n+2]);x&&
(K="thisWired"+(0<K.length?", ":"")+K);l+=(Ja?"var rv = ":"")+"invoker(fn"+(0<K.length?", ":"")+K+");\n";if(B)l+="runDestructors(destructors);\n";else for(n=x?1:2;n<h.length;++n)q=1===n?"thisWired":"arg"+(n-2)+"Wired",null!==h[n].H&&(l+=q+"_dtor("+q+"); // "+h[n].name+"\n",H.push(q+"_dtor"),p.push(h[n].H));Ja&&(l+="var ret = retType.fromWireType(rv);\nreturn ret;\n");H.push(l+"}\n");h=Pa(H).apply(null,p);n=b-1;if(!e.hasOwnProperty(m))throw new Ga("Replacing nonexistant public symbol");void 0!==e[m].G&&
void 0!==n?e[m].G[n]=h:(e[m]=h,e[m].K=n);return[]})},c:function(a,b,c,d,f){function g(l){return l}b=P(b);-1===f&&(f=4294967295);var k=Aa(c);if(0===d){var h=32-8*c;g=function(l){return l<<h>>>h}}var m=b.includes("unsigned");U(a,{name:b,fromWireType:g,toWireType:function(l,p){if("number"!==typeof p&&"boolean"!==typeof p)throw new TypeError('Cannot convert "'+Na(p)+'" to '+this.name);if(p<d||p>f)throw new TypeError('Passing a number "'+Na(p)+'" from JS side to C/C++ side to an argument of type "'+b+
'", which is outside the valid range ['+d+", "+f+"]!");return m?p>>>0:p|0},argPackAdvance:8,readValueFromPointer:$a(b,k,0!==d),H:null})},b:function(a,b,c){function d(g){g>>=2;var k=J;return new f(na,k[g+1],k[g])}var f=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array][b];c=P(c);U(a,{name:c,fromWireType:d,argPackAdvance:8,readValueFromPointer:d},{L:!0})},m:function(a,b){b=P(b);var c="std::string"===b;U(a,{name:b,fromWireType:function(d){var f=J[d>>2];if(c)for(var g=
d+4,k=0;k<=f;++k){var h=d+4+k;if(k==f||0==E[h]){g=D(g,h-g);if(void 0===m)var m=g;else m+=String.fromCharCode(0),m+=g;g=h+1}}else{m=Array(f);for(k=0;k<f;++k)m[k]=String.fromCharCode(E[d+4+k]);m=m.join("")}X(d);return m},toWireType:function(d,f){f instanceof ArrayBuffer&&(f=new Uint8Array(f));var g="string"===typeof f;g||f instanceof Uint8Array||f instanceof Uint8ClampedArray||f instanceof Int8Array||T("Cannot pass non-string to std::string");var k=(c&&g?function(){for(var l=0,p=0;p<f.length;++p){var q=
f.charCodeAt(p);55296<=q&&57343>=q&&(q=65536+((q&1023)<<10)|f.charCodeAt(++p)&1023);127>=q?++l:l=2047>=q?l+2:65535>=q?l+3:l+4}return l}:function(){return f.length})(),h=gb(4+k+1);J[h>>2]=k;if(c&&g)ea(f,h+4,k+1);else if(g)for(g=0;g<k;++g){var m=f.charCodeAt(g);255<m&&(X(h),T("String has UTF-16 code units that do not fit in 8 bits"));E[h+4+g]=m}else for(g=0;g<k;++g)E[h+4+g]=f[g];null!==d&&d.push(X,h);return h},argPackAdvance:8,readValueFromPointer:Ma,H:function(d){X(d)}})},i:function(a,b,c){c=P(c);
if(2===b){var d=ha;var f=ia;var g=ja;var k=function(){return F};var h=1}else 4===b&&(d=ka,f=la,g=ma,k=function(){return J},h=2);U(a,{name:c,fromWireType:function(m){for(var l=J[m>>2],p=k(),q,x=m+4,B=0;B<=l;++B){var n=m+4+B*b;if(B==l||0==p[n>>h])x=d(x,n-x),void 0===q?q=x:(q+=String.fromCharCode(0),q+=x),x=n+b}X(m);return q},toWireType:function(m,l){"string"!==typeof l&&T("Cannot pass non-string to C++ string type "+c);var p=g(l),q=gb(4+p+b);J[q>>2]=p>>h;f(l,q+4,p+b);null!==m&&m.push(X,q);return q},
argPackAdvance:8,readValueFromPointer:Ma,H:function(m){X(m)}})},n:function(a,b){b=P(b);U(a,{N:!0,name:b,argPackAdvance:0,fromWireType:function(){},toWireType:function(){}})},e:La,f:function(a){if(0===a)return W(bb());var b=ab[a];a=void 0===b?P(a):b;return W(bb()[a])},j:function(a){4<a&&(V[a].J+=1)},k:function(a,b,c,d){a||T("Cannot use deleted val. handle = "+a);a=V[a].value;var f=db[b];if(!f){f="";for(var g=0;g<b;++g)f+=(0!==g?", ":"")+"arg"+g;var k="return function emval_allocator_"+b+"(constructor, argTypes, args) {\n";
for(g=0;g<b;++g)k+="var argType"+g+" = requireRegisteredType(Module['HEAP32'][(argTypes >>> 2) + "+g+'], "parameter '+g+'");\nvar arg'+g+" = argType"+g+".readValueFromPointer(args);\nargs += argType"+g+"['argPackAdvance'];\n";f=(new Function("requireRegisteredType","Module","__emval_register",k+("var obj = new constructor("+f+");\nreturn __emval_register(obj);\n}\n")))(cb,e,W);db[b]=f}return f(a,c,d)},p:function(a,b){a=cb(a,"_emval_take_value");a=a.readValueFromPointer(b);return W(a)},d:function(){A()},
t:function(a,b,c){E.copyWithin(a,b,b+c)},h:function(a){var b=E.length;a>>>=0;if(2147483648<a)return!1;for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,a+100663296);d=Math.max(a,d);0<d%65536&&(d+=65536-d%65536);a:{try{C.grow(Math.min(2147483648,d)-na.byteLength+65535>>>16);ra();var f=1;break a}catch(g){}f=void 0}if(f)return!0}return!1},v:function(){return 0},q:function(){},u:function(a,b,c,d){for(var f=0,g=0;g<c;g++){for(var k=I[b+8*g>>2],h=I[b+(8*g+4)>>2],m=0;m<h;m++){var l=E[k+m],p=eb[a];if(0===
l||10===l){for(l=0;p[l]&&!(NaN<=l);)++l;l=da.decode(p.subarray?p.subarray(0,l):new Uint8Array(p.slice(0,l)));(1===a?ba:y)(l);p.length=0}else p.push(l)}f+=h}I[d>>2]=f;return 0},s:function(){}};
(function(){function a(f){e.asm=f.exports;C=e.asm.y;ra();L=e.asm.E;ta.unshift(e.asm.z);M--;e.monitorRunDependencies&&e.monitorRunDependencies(M);0==M&&(null!==wa&&(clearInterval(wa),wa=null),N&&(f=N,N=null,f()))}function b(f){a(f.instance)}function c(f){return ya().then(function(g){return WebAssembly.instantiate(g,d)}).then(f,function(g){y("failed to asynchronously prepare wasm: "+g);A(g)})}var d={a:hb};M++;e.monitorRunDependencies&&e.monitorRunDependencies(M);if(e.instantiateWasm)try{return e.instantiateWasm(d,
a)}catch(f){return y("Module.instantiateWasm callback failed with error: "+f),!1}(function(){return z||"function"!==typeof WebAssembly.instantiateStreaming||O.startsWith("data:application/octet-stream;base64,")||"function"!==typeof fetch?c(b):fetch(O,{credentials:"same-origin"}).then(function(f){return WebAssembly.instantiateStreaming(f,d).then(b,function(g){y("wasm streaming compile failed: "+g);y("falling back to ArrayBuffer instantiation");return c(b)})})})().catch(r);return{}})();
e.___wasm_call_ctors=function(){return(e.___wasm_call_ctors=e.asm.z).apply(null,arguments)};var gb=e._malloc=function(){return(gb=e._malloc=e.asm.A).apply(null,arguments)},X=e._free=function(){return(X=e._free=e.asm.B).apply(null,arguments)},Ya=e.___getTypeName=function(){return(Ya=e.___getTypeName=e.asm.C).apply(null,arguments)};e.___embind_register_native_and_builtin_types=function(){return(e.___embind_register_native_and_builtin_types=e.asm.D).apply(null,arguments)};
e.dynCall_jiji=function(){return(e.dynCall_jiji=e.asm.F).apply(null,arguments)};var Z;N=function ib(){Z||jb();Z||(N=ib)};
function jb(){function a(){if(!Z&&(Z=!0,e.calledRun=!0,!ca)){za(ta);aa(e);if(e.onRuntimeInitialized)e.onRuntimeInitialized();if(e.postRun)for("function"==typeof e.postRun&&(e.postRun=[e.postRun]);e.postRun.length;){var b=e.postRun.shift();ua.unshift(b)}za(ua)}}if(!(0<M)){if(e.preRun)for("function"==typeof e.preRun&&(e.preRun=[e.preRun]);e.preRun.length;)va();za(sa);0<M||(e.setStatus?(e.setStatus("Running..."),setTimeout(function(){setTimeout(function(){e.setStatus("")},1);a()},1)):a())}}e.run=jb;
if(e.preInit)for("function"==typeof e.preInit&&(e.preInit=[e.preInit]);0<e.preInit.length;)e.preInit.pop()();jb();
return Module.ready
}
);
})();
export default Module;

BIN
codecs/basis/dec/basis_dec.wasm Executable file

Binary file not shown.

View File

@@ -0,0 +1,96 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <inttypes.h>
#include "basisu_comp.h"
#include "basisu_enc.h"
using namespace emscripten;
using namespace basisu;
struct BasisOptions {
float quality;
uint8_t compression;
bool uastc;
bool mipmap;
bool srgb_mipmap;
std::string mipmap_filter;
bool perceptual;
bool y_flip;
uint32_t mipmap_min_dimension;
};
thread_local const val Uint8Array = val::global("Uint8Array");
val encode(std::string image_in, int image_width, int image_height, BasisOptions opts) {
basisu_encoder_init();
basist::etc1_global_selector_codebook sel_codebook(basist::g_global_selector_cb_size,
basist::g_global_selector_cb);
basis_compressor_params params;
basis_compressor compressor;
image img =
image(reinterpret_cast<const uint8_t*>(image_in.c_str()), image_width, image_height, 4);
// We dont need the encoder to read/decode files from the filesystem
params.m_read_source_images = false;
// Writing is unnecessary, too
params.m_write_output_basis_files = false;
// No printf pls
params.m_status_output = false;
// True => UASTC, False => ETC1S
params.m_uastc = opts.uastc;
// Use the standardized KTX2 format
params.m_create_ktx2_file = true;
// Codebook, whatever this exactly is or does.
params.m_pSel_codebook = &sel_codebook;
// No multithreading. It apparently doesnt work well in Wasm.
// But we still need to provide a job pool.
params.m_multithreading = false;
job_pool jpool(1);
params.m_pJob_pool = &jpool;
params.m_ktx2_uastc_supercompression = basist::KTX2_SS_ZSTANDARD;
params.m_perceptual = opts.perceptual;
params.m_y_flip = opts.y_flip;
params.m_mip_gen = opts.mipmap;
params.m_mip_srgb = opts.srgb_mipmap;
params.m_mip_filter = opts.mipmap_filter;
params.m_mip_smallest_dimension = opts.mipmap_min_dimension;
params.m_compression_level = opts.compression;
params.m_source_images.push_back(img);
if (opts.uastc) {
params.m_rdo_uastc_quality_scalar = opts.quality;
params.m_rdo_uastc = opts.quality != 0;
} else {
params.m_quality_level = static_cast<int>(opts.quality);
}
if (!compressor.init(params)) {
return val(std::string("Well something went wrong during init"));
}
if (compressor.process() != 0) {
return val(std::string("Well something went wrong during processing"));
}
auto comp_data = compressor.get_output_ktx2_file();
auto js_result = Uint8Array.new_(typed_memory_view(comp_data.size(), &comp_data[0]));
// Not sure if there is anything to free here
return js_result;
}
EMSCRIPTEN_BINDINGS(my_module) {
value_object<BasisOptions>("BasisOptions")
.field("quality", &BasisOptions::quality)
.field("compression", &BasisOptions::compression)
.field("uastc", &BasisOptions::uastc)
.field("perceptual", &BasisOptions::perceptual)
.field("y_flip", &BasisOptions::y_flip)
.field("mipmap", &BasisOptions::mipmap)
.field("srgb_mipmap", &BasisOptions::srgb_mipmap)
.field("mipmap_filter", &BasisOptions::mipmap_filter)
.field("mipmap_min_dimension", &BasisOptions::mipmap_min_dimension);
function("encode", &encode);
}

24
codecs/basis/enc/basis_enc.d.ts vendored Normal file
View File

@@ -0,0 +1,24 @@
export interface EncodeOptions {
quality: number;
compression: number;
uastc: boolean;
y_flip: boolean;
mipmap: boolean;
srgb_mipmap: boolean;
perceptual: boolean;
mipmap_filter: string;
mipmap_min_dimension: number;
}
export interface BasisModule extends EmscriptenWasm.Module {
encode(
data: BufferSource,
width: number,
height: number,
options: EncodeOptions,
): Uint8Array | null;
}
declare var moduleFactory: EmscriptenWasm.ModuleFactory<BasisModule>;
export default moduleFactory;

62
codecs/basis/enc/basis_enc.js generated Normal file
View File

@@ -0,0 +1,62 @@
var Module = (function() {
var _scriptDir = import.meta.url;
return (
function(Module) {
Module = Module || {};
var f;f||(f=typeof Module !== 'undefined' ? Module : {});var aa,ba;f.ready=new Promise(function(a,b){aa=a;ba=b});var r={},t;for(t in f)f.hasOwnProperty(t)&&(r[t]=f[t]);var u="",ca;u=self.location.href;_scriptDir&&(u=_scriptDir);0!==u.indexOf("blob:")?u=u.substr(0,u.lastIndexOf("/")+1):u="";ca=function(a){var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)};var da=f.print||console.log.bind(console),v=f.printErr||console.warn.bind(console);
for(t in r)r.hasOwnProperty(t)&&(f[t]=r[t]);r=null;var ea=0,w;f.wasmBinary&&(w=f.wasmBinary);var noExitRuntime=f.noExitRuntime||!0;"object"!==typeof WebAssembly&&x("no native wasm support detected");var ha,ia=!1,ja=new TextDecoder("utf8");function B(a,b){if(!a)return"";b=a+b;for(var c=a;!(c>=b)&&C[c];)++c;return ja.decode(C.subarray(a,c))}
function ka(a,b,c){var d=C;if(0<c){c=b+c-1;for(var e=0;e<a.length;++e){var g=a.charCodeAt(e);if(55296<=g&&57343>=g){var k=a.charCodeAt(++e);g=65536+((g&1023)<<10)|k&1023}if(127>=g){if(b>=c)break;d[b++]=g}else{if(2047>=g){if(b+1>=c)break;d[b++]=192|g>>6}else{if(65535>=g){if(b+2>=c)break;d[b++]=224|g>>12}else{if(b+3>=c)break;d[b++]=240|g>>18;d[b++]=128|g>>12&63}d[b++]=128|g>>6&63}d[b++]=128|g&63}}d[b]=0}}var la=new TextDecoder("utf-16le");
function ma(a,b){var c=a>>1;for(b=c+b/2;!(c>=b)&&D[c];)++c;return la.decode(C.subarray(a,c<<1))}function na(a,b,c){void 0===c&&(c=2147483647);if(2>c)return 0;c-=2;var d=b;c=c<2*a.length?c/2:a.length;for(var e=0;e<c;++e)E[b>>1]=a.charCodeAt(e),b+=2;E[b>>1]=0;return b-d}function oa(a){return 2*a.length}function pa(a,b){for(var c=0,d="";!(c>=b/4);){var e=F[a+4*c>>2];if(0==e)break;++c;65536<=e?(e-=65536,d+=String.fromCharCode(55296|e>>10,56320|e&1023)):d+=String.fromCharCode(e)}return d}
function qa(a,b,c){void 0===c&&(c=2147483647);if(4>c)return 0;var d=b;c=d+c-4;for(var e=0;e<a.length;++e){var g=a.charCodeAt(e);if(55296<=g&&57343>=g){var k=a.charCodeAt(++e);g=65536+((g&1023)<<10)|k&1023}F[b>>2]=g;b+=4;if(b+4>c)break}F[b>>2]=0;return b-d}function ra(a){for(var b=0,c=0;c<a.length;++c){var d=a.charCodeAt(c);55296<=d&&57343>=d&&++c;b+=4}return b}var sa,H,C,E,D,F,I,ta,ua;
function va(){var a=ha.buffer;sa=a;f.HEAP8=H=new Int8Array(a);f.HEAP16=E=new Int16Array(a);f.HEAP32=F=new Int32Array(a);f.HEAPU8=C=new Uint8Array(a);f.HEAPU16=D=new Uint16Array(a);f.HEAPU32=I=new Uint32Array(a);f.HEAPF32=ta=new Float32Array(a);f.HEAPF64=ua=new Float64Array(a)}var J,wa=[],xa=[],ya=[];function za(){var a=f.preRun.shift();wa.unshift(a)}var K=0,Aa=null,L=null;f.preloadedImages={};f.preloadedAudios={};
function x(a){if(f.onAbort)f.onAbort(a);v(a);ia=!0;a=new WebAssembly.RuntimeError("abort("+a+"). Build with -s ASSERTIONS=1 for more info.");ba(a);throw a;}var M=(new URL("basis_enc.wasm",import.meta.url)).toString();function Ba(){try{if(M==M&&w)return new Uint8Array(w);if(ca)return ca(M);throw"both async and sync fetching of the wasm failed";}catch(a){x(a)}}
function Ca(){return w||"function"!==typeof fetch?Promise.resolve().then(function(){return Ba()}):fetch(M,{credentials:"same-origin"}).then(function(a){if(!a.ok)throw"failed to load wasm binary file at '"+M+"'";return a.arrayBuffer()}).catch(function(){return Ba()})}function Da(a){for(;0<a.length;){var b=a.shift();if("function"==typeof b)b(f);else{var c=b.Aa;"number"===typeof c?void 0===b.fa?J.get(c)():J.get(c)(b.fa):c(void 0===b.fa?null:b.fa)}}}
function Ea(a){this.ea=a-16;this.va=function(b){F[this.ea+8>>2]=b};this.sa=function(b){F[this.ea+0>>2]=b};this.ta=function(){F[this.ea+4>>2]=0};this.ra=function(){H[this.ea+12>>0]=0};this.ua=function(){H[this.ea+13>>0]=0};this.oa=function(b,c){this.va(b);this.sa(c);this.ta();this.ra();this.ua()}}var Fa=0,Ga=[null,[],[]],Ha={},Ia={};function Ja(a){for(;a.length;){var b=a.pop();a.pop()(b)}}function Ka(a){return this.fromWireType(I[a>>2])}var N={},O={},La={};
function Ma(a){if(void 0===a)return"_unknown";a=a.replace(/[^a-zA-Z0-9_]/g,"$");var b=a.charCodeAt(0);return 48<=b&&57>=b?"_"+a:a}function Na(a,b){a=Ma(a);return(new Function("body","return function "+a+'() {\n "use strict"; return body.apply(this, arguments);\n};\n'))(b)}
function Oa(a){var b=Error,c=Na(a,function(d){this.name=a;this.message=d;d=Error(d).stack;void 0!==d&&(this.stack=this.toString()+"\n"+d.replace(/^Error(:[^\n]*)?\n/,""))});c.prototype=Object.create(b.prototype);c.prototype.constructor=c;c.prototype.toString=function(){return void 0===this.message?this.name:this.name+": "+this.message};return c}var Pa=void 0;
function Qa(a,b,c){function d(h){h=c(h);if(h.length!==a.length)throw new Pa("Mismatched type converter count");for(var l=0;l<a.length;++l)P(a[l],h[l])}a.forEach(function(h){La[h]=b});var e=Array(b.length),g=[],k=0;b.forEach(function(h,l){O.hasOwnProperty(h)?e[l]=O[h]:(g.push(h),N.hasOwnProperty(h)||(N[h]=[]),N[h].push(function(){e[l]=O[h];++k;k===g.length&&d(e)}))});0===g.length&&d(e)}
function Ra(a){switch(a){case 1:return 0;case 2:return 1;case 4:return 2;case 8:return 3;default:throw new TypeError("Unknown type size: "+a);}}var Sa=void 0;function Q(a){for(var b="";C[a];)b+=Sa[C[a++]];return b}var Ta=void 0;function R(a){throw new Ta(a);}
function P(a,b,c){c=c||{};if(!("argPackAdvance"in b))throw new TypeError("registerType registeredInstance requires argPackAdvance");var d=b.name;a||R('type "'+d+'" must have a positive integer typeid pointer');if(O.hasOwnProperty(a)){if(c.na)return;R("Cannot register type '"+d+"' twice")}O[a]=b;delete La[a];N.hasOwnProperty(a)&&(b=N[a],delete N[a],b.forEach(function(e){e()}))}var Ua=[],S=[{},{value:void 0},{value:null},{value:!0},{value:!1}];
function Va(a){4<a&&0===--S[a].ga&&(S[a]=void 0,Ua.push(a))}function T(a){switch(a){case void 0:return 1;case null:return 2;case !0:return 3;case !1:return 4;default:var b=Ua.length?Ua.pop():S.length;S[b]={ga:1,value:a};return b}}function Wa(a){if(null===a)return"null";var b=typeof a;return"object"===b||"array"===b||"function"===b?a.toString():""+a}
function Xa(a,b){switch(b){case 2:return function(c){return this.fromWireType(ta[c>>2])};case 3:return function(c){return this.fromWireType(ua[c>>3])};default:throw new TypeError("Unknown float type: "+a);}}function Ya(a){var b=Function;if(!(b instanceof Function))throw new TypeError("new_ called with constructor type "+typeof b+" which is not a function");var c=Na(b.name||"unknownFunctionName",function(){});c.prototype=b.prototype;c=new c;a=b.apply(c,a);return a instanceof Object?a:c}
function Za(a,b){var c=f;if(void 0===c[a].ca){var d=c[a];c[a]=function(){c[a].ca.hasOwnProperty(arguments.length)||R("Function '"+b+"' called with an invalid number of arguments ("+arguments.length+") - expects one of ("+c[a].ca+")!");return c[a].ca[arguments.length].apply(this,arguments)};c[a].ca=[];c[a].ca[d.ia]=d}}
function $a(a,b,c){f.hasOwnProperty(a)?((void 0===c||void 0!==f[a].ca&&void 0!==f[a].ca[c])&&R("Cannot register public name '"+a+"' twice"),Za(a,a),f.hasOwnProperty(c)&&R("Cannot register multiple overloads of a function with the same number of arguments ("+c+")!"),f[a].ca[c]=b):(f[a]=b,void 0!==c&&(f[a].Da=c))}function ab(a,b){for(var c=[],d=0;d<a;d++)c.push(F[(b>>2)+d]);return c}
function bb(a,b){var c=[];return function(){c.length=arguments.length;for(var d=0;d<arguments.length;d++)c[d]=arguments[d];a.includes("j")?(d=f["dynCall_"+a],d=c&&c.length?d.apply(null,[b].concat(c)):d.call(null,b)):d=J.get(b).apply(null,c);return d}}function U(a,b){a=Q(a);var c=a.includes("j")?bb(a,b):J.get(b);"function"!==typeof c&&R("unknown function pointer with signature "+a+": "+b);return c}var cb=void 0;function db(a){a=eb(a);var b=Q(a);V(a);return b}
function fb(a,b){function c(g){e[g]||O[g]||(La[g]?La[g].forEach(c):(d.push(g),e[g]=!0))}var d=[],e={};b.forEach(c);throw new cb(a+": "+d.map(db).join([", "]));}function gb(a,b,c){switch(b){case 0:return c?function(d){return H[d]}:function(d){return C[d]};case 1:return c?function(d){return E[d>>1]}:function(d){return D[d>>1]};case 2:return c?function(d){return F[d>>2]}:function(d){return I[d>>2]};default:throw new TypeError("Unknown integer type: "+a);}}var hb={};
function ib(){return"object"===typeof globalThis?globalThis:Function("return this")()}function jb(a,b){var c=O[a];void 0===c&&R(b+" has unknown type "+db(a));return c}var kb={};Pa=f.InternalError=Oa("InternalError");for(var lb=Array(256),mb=0;256>mb;++mb)lb[mb]=String.fromCharCode(mb);Sa=lb;Ta=f.BindingError=Oa("BindingError");f.count_emval_handles=function(){for(var a=0,b=5;b<S.length;++b)void 0!==S[b]&&++a;return a};
f.get_first_emval=function(){for(var a=5;a<S.length;++a)if(void 0!==S[a])return S[a];return null};cb=f.UnboundTypeError=Oa("UnboundTypeError");
var vb={a:function(a,b,c,d){x("Assertion failed: "+B(a)+", at: "+[b?B(b):"unknown filename",c,d?B(d):"unknown function"])},D:function(a){return nb(a+16)+16},R:function(){},z:function(a,b,c){(new Ea(a)).oa(b,c);Fa++;throw a;},s:function(){return 0},H:function(){return 0},I:function(){},P:function(a){var b=Ia[a];delete Ia[a];var c=b.pa,d=b.qa,e=b.ha,g=e.map(function(k){return k.ma}).concat(e.map(function(k){return k.xa}));Qa([a],g,function(k){var h={};e.forEach(function(l,m){var n=k[m],q=l.ka,y=l.la,
z=k[m+e.length],p=l.wa,fa=l.ya;h[l.ja]={read:function(A){return n.fromWireType(q(y,A))},write:function(A,G){var X=[];p(fa,A,z.toWireType(X,G));Ja(X)}}});return[{name:b.name,fromWireType:function(l){var m={},n;for(n in h)m[n]=h[n].read(l);d(l);return m},toWireType:function(l,m){for(var n in h)if(!(n in m))throw new TypeError('Missing field: "'+n+'"');var q=c();for(n in h)h[n].write(q,m[n]);null!==l&&l.push(d,q);return q},argPackAdvance:8,readValueFromPointer:Ka,da:d}]})},B:function(){},L:function(a,
b,c,d,e){var g=Ra(c);b=Q(b);P(a,{name:b,fromWireType:function(k){return!!k},toWireType:function(k,h){return h?d:e},argPackAdvance:8,readValueFromPointer:function(k){if(1===c)var h=H;else if(2===c)h=E;else if(4===c)h=F;else throw new TypeError("Unknown boolean type size: "+b);return this.fromWireType(h[k>>g])},da:null})},K:function(a,b){b=Q(b);P(a,{name:b,fromWireType:function(c){var d=S[c].value;Va(c);return d},toWireType:function(c,d){return T(d)},argPackAdvance:8,readValueFromPointer:Ka,da:null})},
u:function(a,b,c){c=Ra(c);b=Q(b);P(a,{name:b,fromWireType:function(d){return d},toWireType:function(d,e){if("number"!==typeof e&&"boolean"!==typeof e)throw new TypeError('Cannot convert "'+Wa(e)+'" to '+this.name);return e},argPackAdvance:8,readValueFromPointer:Xa(b,c),da:null})},O:function(a,b,c,d,e,g){var k=ab(b,c);a=Q(a);e=U(d,e);$a(a,function(){fb("Cannot call "+a+" due to unbound types",k)},b-1);Qa([],k,function(h){var l=a,m=a;h=[h[0],null].concat(h.slice(1));var n=e,q=h.length;2>q&&R("argTypes array size mismatch! Must at least get return value and 'this' types!");
for(var y=null!==h[1]&&!1,z=!1,p=1;p<h.length;++p)if(null!==h[p]&&void 0===h[p].da){z=!0;break}var fa="void"!==h[0].name,A="",G="";for(p=0;p<q-2;++p)A+=(0!==p?", ":"")+"arg"+p,G+=(0!==p?", ":"")+"arg"+p+"Wired";m="return function "+Ma(m)+"("+A+") {\nif (arguments.length !== "+(q-2)+") {\nthrowBindingError('function "+m+" called with ' + arguments.length + ' arguments, expected "+(q-2)+" args!');\n}\n";z&&(m+="var destructors = [];\n");var X=z?"destructors":"null";A="throwBindingError invoker fn runDestructors retType classParam".split(" ");
n=[R,n,g,Ja,h[0],h[1]];y&&(m+="var thisWired = classParam.toWireType("+X+", this);\n");for(p=0;p<q-2;++p)m+="var arg"+p+"Wired = argType"+p+".toWireType("+X+", arg"+p+"); // "+h[p+2].name+"\n",A.push("argType"+p),n.push(h[p+2]);y&&(G="thisWired"+(0<G.length?", ":"")+G);m+=(fa?"var rv = ":"")+"invoker(fn"+(0<G.length?", ":"")+G+");\n";if(z)m+="runDestructors(destructors);\n";else for(p=y?1:2;p<h.length;++p)q=1===p?"thisWired":"arg"+(p-2)+"Wired",null!==h[p].da&&(m+=q+"_dtor("+q+"); // "+h[p].name+
"\n",A.push(q+"_dtor"),n.push(h[p].da));fa&&(m+="var ret = retType.fromWireType(rv);\nreturn ret;\n");A.push(m+"}\n");h=Ya(A).apply(null,n);p=b-1;if(!f.hasOwnProperty(l))throw new Pa("Replacing nonexistant public symbol");void 0!==f[l].ca&&void 0!==p?f[l].ca[p]=h:(f[l]=h,f[l].ia=p);return[]})},j:function(a,b,c,d,e){function g(m){return m}b=Q(b);-1===e&&(e=4294967295);var k=Ra(c);if(0===d){var h=32-8*c;g=function(m){return m<<h>>>h}}var l=b.includes("unsigned");P(a,{name:b,fromWireType:g,toWireType:function(m,
n){if("number"!==typeof n&&"boolean"!==typeof n)throw new TypeError('Cannot convert "'+Wa(n)+'" to '+this.name);if(n<d||n>e)throw new TypeError('Passing a number "'+Wa(n)+'" from JS side to C/C++ side to an argument of type "'+b+'", which is outside the valid range ['+d+", "+e+"]!");return l?n>>>0:n|0},argPackAdvance:8,readValueFromPointer:gb(b,k,0!==d),da:null})},i:function(a,b,c){function d(g){g>>=2;var k=I;return new e(sa,k[g+1],k[g])}var e=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,
Uint32Array,Float32Array,Float64Array][b];c=Q(c);P(a,{name:c,fromWireType:d,argPackAdvance:8,readValueFromPointer:d},{na:!0})},v:function(a,b){b=Q(b);var c="std::string"===b;P(a,{name:b,fromWireType:function(d){var e=I[d>>2];if(c)for(var g=d+4,k=0;k<=e;++k){var h=d+4+k;if(k==e||0==C[h]){g=B(g,h-g);if(void 0===l)var l=g;else l+=String.fromCharCode(0),l+=g;g=h+1}}else{l=Array(e);for(k=0;k<e;++k)l[k]=String.fromCharCode(C[d+4+k]);l=l.join("")}V(d);return l},toWireType:function(d,e){e instanceof ArrayBuffer&&
(e=new Uint8Array(e));var g="string"===typeof e;g||e instanceof Uint8Array||e instanceof Uint8ClampedArray||e instanceof Int8Array||R("Cannot pass non-string to std::string");var k=(c&&g?function(){for(var m=0,n=0;n<e.length;++n){var q=e.charCodeAt(n);55296<=q&&57343>=q&&(q=65536+((q&1023)<<10)|e.charCodeAt(++n)&1023);127>=q?++m:m=2047>=q?m+2:65535>=q?m+3:m+4}return m}:function(){return e.length})(),h=nb(4+k+1);I[h>>2]=k;if(c&&g)ka(e,h+4,k+1);else if(g)for(g=0;g<k;++g){var l=e.charCodeAt(g);255<l&&
(V(h),R("String has UTF-16 code units that do not fit in 8 bits"));C[h+4+g]=l}else for(g=0;g<k;++g)C[h+4+g]=e[g];null!==d&&d.push(V,h);return h},argPackAdvance:8,readValueFromPointer:Ka,da:function(d){V(d)}})},p:function(a,b,c){c=Q(c);if(2===b){var d=ma;var e=na;var g=oa;var k=function(){return D};var h=1}else 4===b&&(d=pa,e=qa,g=ra,k=function(){return I},h=2);P(a,{name:c,fromWireType:function(l){for(var m=I[l>>2],n=k(),q,y=l+4,z=0;z<=m;++z){var p=l+4+z*b;if(z==m||0==n[p>>h])y=d(y,p-y),void 0===q?
q=y:(q+=String.fromCharCode(0),q+=y),y=p+b}V(l);return q},toWireType:function(l,m){"string"!==typeof m&&R("Cannot pass non-string to C++ string type "+c);var n=g(m),q=nb(4+n+b);I[q>>2]=n>>h;e(m,q+4,n+b);null!==l&&l.push(V,q);return q},argPackAdvance:8,readValueFromPointer:Ka,da:function(l){V(l)}})},Q:function(a,b,c,d,e,g){Ia[a]={name:Q(b),pa:U(c,d),qa:U(e,g),ha:[]}},k:function(a,b,c,d,e,g,k,h,l,m){Ia[a].ha.push({ja:Q(b),ma:c,ka:U(d,e),la:g,xa:k,wa:U(h,l),ya:m})},M:function(a,b){b=Q(b);P(a,{Ca:!0,
name:b,argPackAdvance:0,fromWireType:function(){},toWireType:function(){}})},m:Va,y:function(a){if(0===a)return T(ib());var b=hb[a];a=void 0===b?Q(a):b;return T(ib()[a])},N:function(a){4<a&&(S[a].ga+=1)},w:function(a,b,c,d){a||R("Cannot use deleted val. handle = "+a);a=S[a].value;var e=kb[b];if(!e){e="";for(var g=0;g<b;++g)e+=(0!==g?", ":"")+"arg"+g;var k="return function emval_allocator_"+b+"(constructor, argTypes, args) {\n";for(g=0;g<b;++g)k+="var argType"+g+" = requireRegisteredType(Module['HEAP32'][(argTypes >>> 2) + "+
g+'], "parameter '+g+'");\nvar arg'+g+" = argType"+g+".readValueFromPointer(args);\nargs += argType"+g+"['argPackAdvance'];\n";e=(new Function("requireRegisteredType","Module","__emval_register",k+("var obj = new constructor("+e+");\nreturn __emval_register(obj);\n}\n")))(jb,f,T);kb[b]=e}return e(a,c,d)},x:function(a,b){a=jb(a,"_emval_take_value");a=a.readValueFromPointer(b);return T(a)},e:function(){x()},h:function(a,b){W(a,b||1);throw"longjmp";},E:function(a,b,c){C.copyWithin(a,b,b+c)},o:function(a){var b=
C.length;a>>>=0;if(2147483648<a)return!1;for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,a+100663296);d=Math.max(a,d);0<d%65536&&(d+=65536-d%65536);a:{try{ha.grow(Math.min(2147483648,d)-sa.byteLength+65535>>>16);va();var e=1;break a}catch(g){}e=void 0}if(e)return!0}return!1},t:function(){return 0},G:function(a,b,c,d){a=Ha.Ba(a);b=Ha.za(a,b,c);F[d>>2]=b;return 0},A:function(){},J:function(a,b,c,d){for(var e=0,g=0;g<c;g++){for(var k=F[b+8*g>>2],h=F[b+(8*g+4)>>2],l=0;l<h;l++){var m=C[k+l],n=Ga[a];
if(0===m||10===m){for(m=0;n[m]&&!(NaN<=m);)++m;m=ja.decode(n.subarray?n.subarray(0,m):new Uint8Array(n.slice(0,m)));(1===a?da:v)(m);n.length=0}else n.push(m)}e+=h}F[d>>2]=e;return 0},c:function(){return ea},g:function(a){var b=Date.now();F[a>>2]=b/1E3|0;F[a+4>>2]=b%1E3*1E3|0;return 0},l:ob,f:pb,r:qb,q:rb,n:sb,d:tb,C:ub,F:function(){return 28},b:function(a){ea=a}};
(function(){function a(e){f.asm=e.exports;ha=f.asm.S;va();J=f.asm.$;xa.unshift(f.asm.T);K--;f.monitorRunDependencies&&f.monitorRunDependencies(K);0==K&&(null!==Aa&&(clearInterval(Aa),Aa=null),L&&(e=L,L=null,e()))}function b(e){a(e.instance)}function c(e){return Ca().then(function(g){return WebAssembly.instantiate(g,d)}).then(e,function(g){v("failed to asynchronously prepare wasm: "+g);x(g)})}var d={a:vb};K++;f.monitorRunDependencies&&f.monitorRunDependencies(K);if(f.instantiateWasm)try{return f.instantiateWasm(d,
a)}catch(e){return v("Module.instantiateWasm callback failed with error: "+e),!1}(function(){return w||"function"!==typeof WebAssembly.instantiateStreaming||M.startsWith("data:application/octet-stream;base64,")||"function"!==typeof fetch?c(b):fetch(M,{credentials:"same-origin"}).then(function(e){return WebAssembly.instantiateStreaming(e,d).then(b,function(g){v("wasm streaming compile failed: "+g);v("falling back to ArrayBuffer instantiation");return c(b)})})})().catch(ba);return{}})();
f.___wasm_call_ctors=function(){return(f.___wasm_call_ctors=f.asm.T).apply(null,arguments)};var nb=f._malloc=function(){return(nb=f._malloc=f.asm.U).apply(null,arguments)},V=f._free=function(){return(V=f._free=f.asm.V).apply(null,arguments)},eb=f.___getTypeName=function(){return(eb=f.___getTypeName=f.asm.W).apply(null,arguments)};f.___embind_register_native_and_builtin_types=function(){return(f.___embind_register_native_and_builtin_types=f.asm.X).apply(null,arguments)};
var Y=f.stackSave=function(){return(Y=f.stackSave=f.asm.Y).apply(null,arguments)},Z=f.stackRestore=function(){return(Z=f.stackRestore=f.asm.Z).apply(null,arguments)},W=f._setThrew=function(){return(W=f._setThrew=f.asm._).apply(null,arguments)};f.dynCall_jiiii=function(){return(f.dynCall_jiiii=f.asm.aa).apply(null,arguments)};f.dynCall_jiji=function(){return(f.dynCall_jiji=f.asm.ba).apply(null,arguments)};
function tb(a,b,c){var d=Y();try{J.get(a)(b,c)}catch(e){Z(d);if(e!==e+0&&"longjmp"!==e)throw e;W(1,0)}}function sb(a,b){var c=Y();try{J.get(a)(b)}catch(d){Z(c);if(d!==d+0&&"longjmp"!==d)throw d;W(1,0)}}function qb(a,b,c,d){var e=Y();try{return J.get(a)(b,c,d)}catch(g){Z(e);if(g!==g+0&&"longjmp"!==g)throw g;W(1,0)}}function pb(a,b,c){var d=Y();try{return J.get(a)(b,c)}catch(e){Z(d);if(e!==e+0&&"longjmp"!==e)throw e;W(1,0)}}
function ob(a,b){var c=Y();try{return J.get(a)(b)}catch(d){Z(c);if(d!==d+0&&"longjmp"!==d)throw d;W(1,0)}}function rb(a,b,c,d,e,g){var k=Y();try{return J.get(a)(b,c,d,e,g)}catch(h){Z(k);if(h!==h+0&&"longjmp"!==h)throw h;W(1,0)}}function ub(a,b,c,d,e){var g=Y();try{J.get(a)(b,c,d,e)}catch(k){Z(g);if(k!==k+0&&"longjmp"!==k)throw k;W(1,0)}}var wb;L=function xb(){wb||yb();wb||(L=xb)};
function yb(){function a(){if(!wb&&(wb=!0,f.calledRun=!0,!ia)){Da(xa);aa(f);if(f.onRuntimeInitialized)f.onRuntimeInitialized();if(f.postRun)for("function"==typeof f.postRun&&(f.postRun=[f.postRun]);f.postRun.length;){var b=f.postRun.shift();ya.unshift(b)}Da(ya)}}if(!(0<K)){if(f.preRun)for("function"==typeof f.preRun&&(f.preRun=[f.preRun]);f.preRun.length;)za();Da(wa);0<K||(f.setStatus?(f.setStatus("Running..."),setTimeout(function(){setTimeout(function(){f.setStatus("")},1);a()},1)):a())}}f.run=yb;
if(f.preInit)for("function"==typeof f.preInit&&(f.preInit=[f.preInit]);0<f.preInit.length;)f.preInit.pop()();yb();
return Module.ready
}
);
})();
export default Module;

BIN
codecs/basis/enc/basis_enc.wasm Executable file

Binary file not shown.

View File

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

View File

@@ -1,4 +1,4 @@
FROM emscripten/emsdk:2.0.34 FROM emscripten/emsdk:2.0.23
RUN apt-get update && apt-get install -qqy autoconf libtool pkg-config RUN apt-get update && apt-get install -qqy autoconf libtool pkg-config
ENV CFLAGS "-O3 -flto" ENV CFLAGS "-O3 -flto"
ENV CXXFLAGS "${CFLAGS} -std=c++17" ENV CXXFLAGS "${CFLAGS} -std=c++17"

View File

@@ -1,5 +1,5 @@
CODEC_URL = https://github.com/libjxl/libjxl.git CODEC_URL = https://github.com/libjxl/libjxl.git
CODEC_VERSION = 9f544641ec83f6abd9da598bdd08178ee8a003e0 CODEC_VERSION = v0.5
CODEC_DIR = node_modules/jxl CODEC_DIR = node_modules/jxl
CODEC_BUILD_ROOT := $(CODEC_DIR)/build CODEC_BUILD_ROOT := $(CODEC_DIR)/build
CODEC_MT_BUILD_DIR := $(CODEC_BUILD_ROOT)/mt CODEC_MT_BUILD_DIR := $(CODEC_BUILD_ROOT)/mt
@@ -75,9 +75,6 @@ $(CODEC_MT_SIMD_BUILD_DIR)/Makefile: CXXFLAGS+=-msimd128
-DCMAKE_CROSSCOMPILING_EMULATOR=node \ -DCMAKE_CROSSCOMPILING_EMULATOR=node \
-B $(@D) \ -B $(@D) \
$(<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: $(CODEC_DIR)/CMakeLists.txt:
$(RM) -r $(@D) $(RM) -r $(@D)

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -4,21 +4,21 @@
#include "lib/jxl/base/thread_pool_internal.h" #include "lib/jxl/base/thread_pool_internal.h"
#include "lib/jxl/enc_external_image.h" #include "lib/jxl/enc_external_image.h"
#include "lib/jxl/enc_file.h" #include "lib/jxl/enc_file.h"
#include "lib/jxl/enc_color_management.h"
using namespace emscripten; using namespace emscripten;
thread_local const val Uint8Array = val::global("Uint8Array"); thread_local const val Uint8Array = val::global("Uint8Array");
struct JXLOptions { struct JXLOptions {
int effort; // 1 = slowest
// 7 = fastest
int speed;
float quality; float quality;
bool progressive; bool progressive;
int epf; int epf;
int nearLossless;
bool lossyPalette; bool lossyPalette;
size_t decodingSpeedTier; size_t decodingSpeedTier;
float photonNoiseIso;
bool lossyModular;
}; };
val encode(std::string image, int width, int height, JXLOptions options) { val encode(std::string image, int width, int height, JXLOptions options) {
@@ -33,14 +33,11 @@ val encode(std::string image, int width, int height, JXLOptions options) {
pool_ptr = &pool; pool_ptr = &pool;
#endif #endif
size_t st = 10 - options.effort;
cparams.speed_tier = jxl::SpeedTier(st);
cparams.epf = options.epf; cparams.epf = options.epf;
cparams.speed_tier = static_cast<jxl::SpeedTier>(options.speed);
cparams.decoding_speed_tier = options.decodingSpeedTier; cparams.decoding_speed_tier = options.decodingSpeedTier;
cparams.photon_noise_iso = options.photonNoiseIso;
if (options.lossyPalette) { if (options.lossyPalette || options.nearLossless) {
cparams.lossy_palette = true; cparams.lossy_palette = true;
cparams.palette_colors = 0; cparams.palette_colors = 0;
cparams.options.predictor = jxl::Predictor::Zero; cparams.options.predictor = jxl::Predictor::Zero;
@@ -52,16 +49,11 @@ val encode(std::string image, int width, int height, JXLOptions options) {
float quality = options.quality; float quality = options.quality;
// Quality settings roughly match libjpeg qualities. // Quality settings roughly match libjpeg qualities.
if (options.lossyModular || quality == 100) { if (quality < 7 || quality == 100) {
cparams.modular_mode = true; cparams.modular_mode = true;
// Internal modular quality to roughly match VarDCT size. // Internal modular quality to roughly match VarDCT size.
if (quality < 7) {
cparams.quality_pair.first = cparams.quality_pair.second = cparams.quality_pair.first = cparams.quality_pair.second =
std::min(35 + (quality - 7) * 3.0f, 100.0f); 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 { } else {
cparams.modular_mode = false; cparams.modular_mode = false;
if (quality >= 30) { if (quality >= 30) {
@@ -98,14 +90,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, 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, height, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*has_alpha=*/true,
/*alpha_is_premultiplied=*/false, /*bits_per_sample=*/8, /*endiannes=*/JXL_LITTLE_ENDIAN, /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/8, /*endiannes=*/JXL_LITTLE_ENDIAN,
/*flipped_y=*/false, pool_ptr, main, /*(only true if bits_per_sample==32) float_in=*/false); /*flipped_y=*/false, pool_ptr, main);
if (!result) { if (!result) {
return val::null(); return val::null();
} }
auto js_result = val::null(); auto js_result = val::null();
if (EncodeFile(cparams, &io, &passes_enc_state, &bytes, jxl::GetJxlCms(), /*aux=*/nullptr, pool_ptr)) { if (EncodeFile(cparams, &io, &passes_enc_state, &bytes, /*aux=*/nullptr, pool_ptr)) {
js_result = Uint8Array.new_(typed_memory_view(bytes.size(), bytes.data())); js_result = Uint8Array.new_(typed_memory_view(bytes.size(), bytes.data()));
} }
@@ -114,13 +106,12 @@ val encode(std::string image, int width, int height, JXLOptions options) {
EMSCRIPTEN_BINDINGS(my_module) { EMSCRIPTEN_BINDINGS(my_module) {
value_object<JXLOptions>("JXLOptions") value_object<JXLOptions>("JXLOptions")
.field("effort", &JXLOptions::effort) .field("speed", &JXLOptions::speed)
.field("quality", &JXLOptions::quality) .field("quality", &JXLOptions::quality)
.field("progressive", &JXLOptions::progressive) .field("progressive", &JXLOptions::progressive)
.field("nearLossless", &JXLOptions::nearLossless)
.field("lossyPalette", &JXLOptions::lossyPalette) .field("lossyPalette", &JXLOptions::lossyPalette)
.field("decodingSpeedTier", &JXLOptions::decodingSpeedTier) .field("decodingSpeedTier", &JXLOptions::decodingSpeedTier)
.field("photonNoiseIso", &JXLOptions::photonNoiseIso)
.field("lossyModular", &JXLOptions::lossyModular)
.field("epf", &JXLOptions::epf); .field("epf", &JXLOptions::epf);
function("encode", &encode); function("encode", &encode);

View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -17,8 +17,7 @@
"static-build/*": ["src/static-build/*"], "static-build/*": ["src/static-build/*"],
"client/*": ["src/client/*"], "client/*": ["src/client/*"],
"shared/*": ["src/shared/*"], "shared/*": ["src/shared/*"],
"features/*": ["src/features/*"], "features/*": ["src/features/*"]
"worker-shared/*": ["src/worker-shared/*"]
} }
} }
} }

View File

@@ -14,40 +14,28 @@ import { promises as fs } from 'fs';
import { lookup as lookupMime } from 'mime-types'; import { lookup as lookupMime } from 'mime-types';
const prefix = /^data-url(-text)?:/; const prefix = 'data-url:';
export default function dataURLPlugin() { export default function dataURLPlugin() {
return { return {
name: 'data-url-plugin', name: 'data-url-plugin',
async resolveId(id, importer) { async resolveId(id, importer) {
const match = prefix.exec(id); if (!id.startsWith(prefix)) return;
if (!match) return;
return ( return (
match[0] + (await this.resolve(id.slice(match[0].length), importer)).id prefix + (await this.resolve(id.slice(prefix.length), importer)).id
); );
}, },
async load(id) { async load(id) {
const match = prefix.exec(id); if (!id.startsWith(prefix)) return;
if (!match) return; const realId = id.slice(prefix.length);
const isText = !!match[1];
const realId = id.slice(match[0].length);
this.addWatchFile(realId); this.addWatchFile(realId);
const source = await fs.readFile(realId); const source = await fs.readFile(realId);
const type = lookupMime(realId) || 'text/plain'; const type = lookupMime(realId) || 'text/plain';
if (isText) { return `export default 'data:${type};base64,${source.toString(
const encodedBody = encodeURIComponent(source.toString('utf8')); 'base64',
)}';`;
return `export default ${JSON.stringify(
`data:${type};charset=utf-8,${encodedBody}`,
)};`;
}
return `export default ${JSON.stringify(
`data:${type};base64,${source.toString('base64')}`,
)};`;
}, },
}; };
} }

View File

@@ -28,17 +28,10 @@ if (!self.<%- amdFunctionName %>) {
<% } else { %> <% } else { %>
new Promise(resolve => { new Promise(resolve => {
if ("document" in self) { if ("document" in self) {
const link = document.createElement("link");
link.rel = "preload";
link.as = "script";
link.href = uri;
link.onload = () => {
const script = document.createElement("script"); const script = document.createElement("script");
script.src = uri; script.src = uri;
script.onload = resolve; script.onload = resolve;
document.head.appendChild(script); document.head.appendChild(script);
};
document.head.appendChild(link);
} else { } else {
self.nextDefineUri = uri; self.nextDefineUri = uri;
importScripts(uri); importScripts(uri);

3
libsquoosh/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
build
.DS_Store

1
libsquoosh/.npmignore Normal file
View File

@@ -0,0 +1 @@
node_modules

0
libsquoosh/.npmrc Normal file
View File

171
libsquoosh/README.md Normal file
View File

@@ -0,0 +1,171 @@
# 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 doesnt 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';
const imagePool = new ImagePool();
```
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. By default, this number is set to the amount of CPU cores available in the system it is running on.
## Ingesting images
You can ingest a new image like so:
```js
const imagePath = 'path/to/image.png';
const image = imagePool.ingestImage(imagePath);
```
The `ingestImage` function can take anything the node [`readFile`][readfile] function can take, including a buffer and `FileHandle`.
The returned `image` object is a representation of the original image, that you can now preprocess, encode, and extract information about.
## 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
[readfile]: https://nodejs.org/api/fs.html#fs_fspromises_readfile_path_options

View File

@@ -0,0 +1,75 @@
/**
* 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),
})}`;
},
};
}

View File

@@ -0,0 +1,12 @@
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'));
}
},
};
}

View File

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

View File

@@ -0,0 +1,38 @@
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;
},
};
}

133
libsquoosh/lib/simple-ts.js Normal file
View File

@@ -0,0 +1,133 @@
/**
* 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 Normal file

File diff suppressed because it is too large Load Diff

40
libsquoosh/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"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"
}
}

View File

@@ -0,0 +1,46 @@
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 Normal file
View File

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

View File

@@ -0,0 +1,115 @@
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,
};
}

482
libsquoosh/src/codecs.ts Normal file
View File

@@ -0,0 +1,482 @@
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 Emscriptens pthread pool size.
// This is the only workaround I can get working without crying.
(globalThis as any).navigator = {
hardwareConcurrency: cpus().length,
};
interface DecodeModule extends EmscriptenWasm.Module {
decode: (data: Uint8Array) => ImageData;
}
type DecodeModuleFactory = EmscriptenWasm.ModuleFactory<DecodeModule>;
interface RotateModuleInstance {
exports: {
memory: WebAssembly.Memory;
rotate(width: number, height: number, rotate: number): void;
};
}
interface ResizeWithAspectParams {
input_width: number;
input_height: number;
target_width: number;
target_height: number;
}
interface ResizeInstantiateOptions {
width: number;
height: number;
method: string;
premultiply: boolean;
linearRGB: boolean;
}
declare global {
// Needed for being able to use ImageData as type in codec types
type ImageData = import('./image_data.js').default;
// Needed for being able to assign to `globalThis.ImageData`
var ImageData: ImageData['constructor'];
}
import type { QuantizerModule } from '../../codecs/imagequant/imagequant.js';
// MozJPEG
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';
// 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';
// 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';
// 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';
// 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';
// 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)));
// 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,
}: ResizeInstantiateOptions,
) => {
({ 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 }: { numColors: number; dither: number },
) =>
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 }: { numRotations: number },
) => {
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: {
speed: 4,
quality: 75,
progressive: false,
epf: -1,
nearLossless: 0,
lossyPalette: false,
decodingSpeedTier: 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;

View File

@@ -0,0 +1,26 @@
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 new URL(workerJS).pathname;
return requestPath;
},
});
}

View File

@@ -0,0 +1,11 @@
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;
}
}

283
libsquoosh/src/index.ts Normal file
View File

@@ -0,0 +1,283 @@
import { isMainThread } from 'worker_threads';
import { cpus } from 'os';
import { promises as fsp } from 'fs';
import { codecs as encoders, preprocessors } 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 FileLike = Buffer | ArrayBuffer | string | ArrayBufferView;
async function decodeFile({
file,
}: {
file: FileLike;
}): Promise<{ bitmap: ImageData; size: number }> {
let buffer;
if (ArrayBuffer.isView(file)) {
buffer = Buffer.from(file.buffer);
file = 'Binary blob';
} else if (file instanceof ArrayBuffer) {
buffer = Buffer.from(file);
file = 'Binary blob';
} else if ((file as unknown) instanceof Buffer) {
// TODO: Check why we need type assertions here.
buffer = (file as unknown) as Buffer;
file = 'Binary blob';
} else if (typeof file === 'string') {
buffer = await fsp.readFile(file);
} else {
throw Error('Unexpected input type');
}
const firstChunk = buffer.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(new Uint8Array(buffer));
return {
bitmap: rgba,
size: buffer.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;
}) {
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: FileLike;
public workerPool: WorkerPool<JobMessage, any>;
public decoded: Promise<{ bitmap: ImageData }>;
public encodedWith: { [key: string]: any };
constructor(workerPool: WorkerPool<JobMessage, any>, file: FileLike) {
this.file = file;
this.workerPool = workerPool;
this.decoded = workerPool.dispatchJob({ operation: 'decode', file });
this.encodedWith = {};
}
/**
* Define one or several preprocessors to use on the image.
* @param {object} 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 = {}) {
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<void>} - A promise that resolves when the image has been encoded with all the specified encoders.
*/
async encode(
encodeOptions: {
optimizerButteraugliTarget?: number;
maxOptimizerRounds?: number;
} & {
[key in EncoderKey]?: any; // any is okay for now
} = {},
): Promise<void> {
const { bitmap } = await this.decoded;
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);
this.encodedWith[encName] = this.workerPool.dispatchJob({
operation: 'encode',
bitmap,
encName,
encConfig,
optimizerButteraugliTarget: Number(
encodeOptions.optimizerButteraugliTarget ?? 1.4,
),
maxOptimizerRounds: Number(encodeOptions.maxOptimizerRounds ?? 6),
});
}
await Promise.all(Object.values(this.encodedWith));
}
}
/**
* 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. Defaults to the number of CPU cores in the system.
*/
constructor(threads: number) {
this.workerPool = new WorkerPool(threads || cpus().length, __filename);
}
/**
* Ingest an image into the image pool.
* @param {FileLike} image - The image or path to the image that should be ingested and decoded.
* @returns {Image} - A custom class reference to the decoded image.
*/
ingestImage(image: FileLike): Image {
return new Image(this.workerPool, image);
}
/**
* Closes the underlying image processing pipeline. The already processed images will still be there, but no new processing can start.
* @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 Normal file
View File

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

View File

@@ -0,0 +1,124 @@
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 });
}
});
}
}

9
libsquoosh/tsconfig.json Normal file
View File

@@ -0,0 +1,9 @@
{
"extends": "../generic-tsconfig.json",
"compilerOptions": {
"lib": ["esnext"],
"types": ["node"],
"allowJs": true
},
"include": ["src/**/*", "../codecs/**/*"]
}

5
missing-types.d.ts vendored
View File

@@ -44,11 +44,6 @@ declare module 'data-url:*' {
export default url; export default url;
} }
declare module 'data-url-text:*' {
const url: string;
export default url;
}
declare module 'service-worker:*' { declare module 'service-worker:*' {
const url: string; const url: string;
export default url; export default url;

367
package-lock.json generated
View File

@@ -5,24 +5,26 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "squoosh",
"version": "2.0.0", "version": "2.0.0",
"license": "apache-2.0", "license": "apache-2.0",
"dependencies": {
"wasm-feature-detect": "^1.2.11"
},
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0", "@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.1.0", "@rollup/plugin-node-resolve": "^11.1.0",
"@rollup/plugin-replace": "^2.3.4", "@rollup/plugin-replace": "^2.3.4",
"@surma/rollup-plugin-off-main-thread": "^2.2.2", "@surma/rollup-plugin-off-main-thread": "^2.2.2",
"@types/dedent": "^0.7.0", "@types/dedent": "^0.7.0",
"@types/mime-types": "^2.1.1", "@types/mime-types": "^2.1.0",
"@types/node": "^16.11.1", "@types/node": "^14.14.7",
"@web/rollup-plugin-import-meta-assets": "^1.0.6", "@web/rollup-plugin-import-meta-assets": "^1.0.6",
"comlink": "^4.3.0", "comlink": "^4.3.0",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"dedent": "^0.7.0", "dedent": "^0.7.0",
"del": "^5.1.0", "del": "^5.1.0",
"file-drop-element": "^1.0.1", "file-drop-element": "^1.0.1",
"husky": "^7.0.2", "husky": "^4.3.0",
"idb-keyval": "^3.2.0", "idb-keyval": "^3.2.0",
"image-size": "^0.9.3", "image-size": "^0.9.3",
"linkstate": "^2.0.0", "linkstate": "^2.0.0",
@@ -30,7 +32,7 @@
"lodash.camelcase": "^4.3.0", "lodash.camelcase": "^4.3.0",
"mime-types": "^2.1.28", "mime-types": "^2.1.28",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"pointer-tracker": "^2.5.3", "pointer-tracker": "^2.4.0",
"postcss": "^7.0.35", "postcss": "^7.0.35",
"postcss-modules": "^3.2.2", "postcss-modules": "^3.2.2",
"postcss-nested": "^4.2.3", "postcss-nested": "^4.2.3",
@@ -38,29 +40,14 @@
"postcss-url": "^8.0.0", "postcss-url": "^8.0.0",
"preact": "^10.5.5", "preact": "^10.5.5",
"preact-render-to-string": "^5.1.11", "preact-render-to-string": "^5.1.11",
"prettier": "^2.4.1", "prettier": "^2.1.2",
"rollup": "^2.38.0", "rollup": "^2.38.0",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"serve": "^11.3.2", "serve": "^11.3.2",
"typescript": "^4.4.4", "typescript": "^4.1.3",
"wasm-feature-detect": "^1.2.11",
"which": "^2.0.2" "which": "^2.0.2"
} }
}, },
"../pointer-tracker": {
"version": "2.5.0",
"extraneous": true,
"license": "Apache-2.0",
"devDependencies": {
"husky": "^4.2.5",
"lint-staged": "^10.2.11",
"prettier": "^2.0.5",
"rollup": "^2.23.1",
"rollup-plugin-terser": "^7.0.0",
"rollup-plugin-typescript2": "^0.27.2",
"typescript": "^3.9.7"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.10.4", "version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
@@ -314,9 +301,9 @@
} }
}, },
"node_modules/@types/mime-types": { "node_modules/@types/mime-types": {
"version": "2.1.1", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz",
"integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==", "integrity": "sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM=",
"dev": true "dev": true
}, },
"node_modules/@types/minimatch": { "node_modules/@types/minimatch": {
@@ -326,9 +313,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "16.11.1", "version": "14.14.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.7.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==", "integrity": "sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==",
"dev": true "dev": true
}, },
"node_modules/@types/parse-json": { "node_modules/@types/parse-json": {
@@ -865,6 +852,12 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/ci-info": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
"dev": true
},
"node_modules/clean-stack": { "node_modules/clean-stack": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -1169,6 +1162,12 @@
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"dev": true "dev": true
}, },
"node_modules/compare-versions": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz",
"integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==",
"dev": true
},
"node_modules/compressible": { "node_modules/compressible": {
"version": "2.0.18", "version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -2271,6 +2270,31 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/find-versions": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz",
"integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==",
"dev": true,
"dependencies": {
"semver-regex": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -2447,18 +2471,28 @@
} }
}, },
"node_modules/husky": { "node_modules/husky": {
"version": "7.0.2", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/husky/-/husky-7.0.2.tgz", "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.0.tgz",
"integrity": "sha512-8yKEWNX4z2YsofXAMT7KvA1g8p+GxtB1ffV8XtpAEGuXNAbCV5wdNKH+qTpw8SM9fh4aMPDR+yQuKfgnreyZlg==", "integrity": "sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA==",
"dev": true, "dev": true,
"dependencies": {
"chalk": "^4.0.0",
"ci-info": "^2.0.0",
"compare-versions": "^3.6.0",
"cosmiconfig": "^7.0.0",
"find-versions": "^3.2.0",
"opencollective-postinstall": "^2.0.2",
"pkg-dir": "^4.2.0",
"please-upgrade-node": "^3.2.0",
"slash": "^3.0.0",
"which-pm-runs": "^1.0.0"
},
"bin": { "bin": {
"husky": "lib/bin.js" "husky-run": "bin/run.js",
"husky-upgrade": "lib/upgrader/bin.js"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
} }
}, },
"node_modules/icss-replace-symbols": { "node_modules/icss-replace-symbols": {
@@ -3116,6 +3150,18 @@
"node": ">=4.0.0" "node": ">=4.0.0"
} }
}, },
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/lodash.camelcase": { "node_modules/lodash.camelcase": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@@ -3629,6 +3675,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/opencollective-postinstall": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
"dev": true,
"bin": {
"opencollective-postinstall": "index.js"
}
},
"node_modules/p-finally": { "node_modules/p-finally": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
@@ -3638,6 +3693,30 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/p-map": { "node_modules/p-map": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
@@ -3650,6 +3729,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -3677,6 +3765,15 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/path-is-absolute": { "node_modules/path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -3752,6 +3849,18 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
"dev": true,
"dependencies": {
"find-up": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/please-upgrade-node": { "node_modules/please-upgrade-node": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
@@ -3762,9 +3871,9 @@
} }
}, },
"node_modules/pointer-tracker": { "node_modules/pointer-tracker": {
"version": "2.5.3", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.5.3.tgz", "resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.4.0.tgz",
"integrity": "sha512-LiJUeIbzk4dXq678YeyrZ++mdY17q4n/2sBHfU9wIuvmSzdiPgMvmvWN2g8mY4J7YwYOIrqrZUWP/MfFHVwYtg==", "integrity": "sha512-pWI2tpaM/XNtc9mUTv42Rmjf6mkHvE8LT5DDEq0G7baPNhxNM9E3CepubPplSoSLk9E5bwQrAMyDcPVmJyTW4g==",
"dev": true "dev": true
}, },
"node_modules/postcss": { "node_modules/postcss": {
@@ -7406,9 +7515,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "2.4.1", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz",
"integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
"dev": true, "dev": true,
"bin": { "bin": {
"prettier": "bin-prettier.js" "prettier": "bin-prettier.js"
@@ -7685,6 +7794,15 @@
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
"dev": true "dev": true
}, },
"node_modules/semver-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz",
"integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/serialize-javascript": { "node_modules/serialize-javascript": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
@@ -8470,9 +8588,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "4.4.4", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
"dev": true, "dev": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@@ -8559,8 +8677,7 @@
"node_modules/wasm-feature-detect": { "node_modules/wasm-feature-detect": {
"version": "1.2.11", "version": "1.2.11",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz", "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz",
"integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w==", "integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w=="
"dev": true
}, },
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
@@ -8577,6 +8694,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/which-pm-runs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz",
"integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=",
"dev": true
},
"node_modules/widest-line": { "node_modules/widest-line": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz",
@@ -8903,9 +9026,9 @@
} }
}, },
"@types/mime-types": { "@types/mime-types": {
"version": "2.1.1", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz",
"integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==", "integrity": "sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM=",
"dev": true "dev": true
}, },
"@types/minimatch": { "@types/minimatch": {
@@ -8915,9 +9038,9 @@
"dev": true "dev": true
}, },
"@types/node": { "@types/node": {
"version": "16.11.1", "version": "14.14.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.7.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==", "integrity": "sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==",
"dev": true "dev": true
}, },
"@types/parse-json": { "@types/parse-json": {
@@ -9357,6 +9480,12 @@
"supports-color": "^7.1.0" "supports-color": "^7.1.0"
} }
}, },
"ci-info": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
"dev": true
},
"clean-stack": { "clean-stack": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -9610,6 +9739,12 @@
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"dev": true "dev": true
}, },
"compare-versions": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz",
"integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==",
"dev": true
},
"compressible": { "compressible": {
"version": "2.0.18", "version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -10527,6 +10662,25 @@
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
} }
}, },
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"find-versions": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz",
"integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==",
"dev": true,
"requires": {
"semver-regex": "^2.0.0"
}
},
"fs.realpath": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -10673,10 +10827,22 @@
"dev": true "dev": true
}, },
"husky": { "husky": {
"version": "7.0.2", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/husky/-/husky-7.0.2.tgz", "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.0.tgz",
"integrity": "sha512-8yKEWNX4z2YsofXAMT7KvA1g8p+GxtB1ffV8XtpAEGuXNAbCV5wdNKH+qTpw8SM9fh4aMPDR+yQuKfgnreyZlg==", "integrity": "sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA==",
"dev": true "dev": true,
"requires": {
"chalk": "^4.0.0",
"ci-info": "^2.0.0",
"compare-versions": "^3.6.0",
"cosmiconfig": "^7.0.0",
"find-versions": "^3.2.0",
"opencollective-postinstall": "^2.0.2",
"pkg-dir": "^4.2.0",
"please-upgrade-node": "^3.2.0",
"slash": "^3.0.0",
"which-pm-runs": "^1.0.0"
}
}, },
"icss-replace-symbols": { "icss-replace-symbols": {
"version": "1.1.0", "version": "1.1.0",
@@ -11214,6 +11380,15 @@
"json5": "^1.0.1" "json5": "^1.0.1"
} }
}, },
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"requires": {
"p-locate": "^4.1.0"
}
},
"lodash.camelcase": { "lodash.camelcase": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@@ -11626,12 +11801,36 @@
"mimic-fn": "^2.1.0" "mimic-fn": "^2.1.0"
} }
}, },
"opencollective-postinstall": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
"dev": true
},
"p-finally": { "p-finally": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
"dev": true "dev": true
}, },
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"requires": {
"p-limit": "^2.2.0"
}
},
"p-map": { "p-map": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
@@ -11641,6 +11840,12 @@
"aggregate-error": "^3.0.0" "aggregate-error": "^3.0.0"
} }
}, },
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
"parent-module": { "parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -11662,6 +11867,12 @@
"lines-and-columns": "^1.1.6" "lines-and-columns": "^1.1.6"
} }
}, },
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
},
"path-is-absolute": { "path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -11716,6 +11927,15 @@
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true "dev": true
}, },
"pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
"dev": true,
"requires": {
"find-up": "^4.0.0"
}
},
"please-upgrade-node": { "please-upgrade-node": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
@@ -11726,9 +11946,9 @@
} }
}, },
"pointer-tracker": { "pointer-tracker": {
"version": "2.5.3", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.5.3.tgz", "resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.4.0.tgz",
"integrity": "sha512-LiJUeIbzk4dXq678YeyrZ++mdY17q4n/2sBHfU9wIuvmSzdiPgMvmvWN2g8mY4J7YwYOIrqrZUWP/MfFHVwYtg==", "integrity": "sha512-pWI2tpaM/XNtc9mUTv42Rmjf6mkHvE8LT5DDEq0G7baPNhxNM9E3CepubPplSoSLk9E5bwQrAMyDcPVmJyTW4g==",
"dev": true "dev": true
}, },
"postcss": { "postcss": {
@@ -14795,9 +15015,9 @@
} }
}, },
"prettier": { "prettier": {
"version": "2.4.1", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz",
"integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
"dev": true "dev": true
}, },
"pretty-format": { "pretty-format": {
@@ -15023,6 +15243,12 @@
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
"dev": true "dev": true
}, },
"semver-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz",
"integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==",
"dev": true
},
"serialize-javascript": { "serialize-javascript": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
@@ -15677,9 +15903,9 @@
"dev": true "dev": true
}, },
"typescript": { "typescript": {
"version": "4.4.4", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
"dev": true "dev": true
}, },
"uniq": { "uniq": {
@@ -15756,8 +15982,7 @@
"wasm-feature-detect": { "wasm-feature-detect": {
"version": "1.2.11", "version": "1.2.11",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz", "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz",
"integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w==", "integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w=="
"dev": true
}, },
"which": { "which": {
"version": "2.0.2", "version": "2.0.2",
@@ -15768,6 +15993,12 @@
"isexe": "^2.0.0" "isexe": "^2.0.0"
} }
}, },
"which-pm-runs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz",
"integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=",
"dev": true
},
"widest-line": { "widest-line": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz",

View File

@@ -8,8 +8,7 @@
"debug": "node --inspect-brk node_modules/.bin/rollup -c", "debug": "node --inspect-brk node_modules/.bin/rollup -c",
"dev": "DEV_PORT=\"${DEV_PORT:=5000}\" run-p watch serve", "dev": "DEV_PORT=\"${DEV_PORT:=5000}\" run-p watch serve",
"watch": "rollup -cw", "watch": "rollup -cw",
"serve": "serve --listen=$DEV_PORT --config ../../../serve.json .tmp/build/static", "serve": "serve --listen=$DEV_PORT --config ../../../serve.json .tmp/build/static"
"prepare": "husky install"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0", "@rollup/plugin-commonjs": "^17.0.0",
@@ -17,15 +16,15 @@
"@rollup/plugin-replace": "^2.3.4", "@rollup/plugin-replace": "^2.3.4",
"@surma/rollup-plugin-off-main-thread": "^2.2.2", "@surma/rollup-plugin-off-main-thread": "^2.2.2",
"@types/dedent": "^0.7.0", "@types/dedent": "^0.7.0",
"@types/mime-types": "^2.1.1", "@types/mime-types": "^2.1.0",
"@types/node": "^16.11.1", "@types/node": "^14.14.7",
"@web/rollup-plugin-import-meta-assets": "^1.0.6", "@web/rollup-plugin-import-meta-assets": "^1.0.6",
"comlink": "^4.3.0", "comlink": "^4.3.0",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"dedent": "^0.7.0", "dedent": "^0.7.0",
"del": "^5.1.0", "del": "^5.1.0",
"file-drop-element": "^1.0.1", "file-drop-element": "^1.0.1",
"husky": "^7.0.2", "husky": "^4.3.0",
"idb-keyval": "^3.2.0", "idb-keyval": "^3.2.0",
"image-size": "^0.9.3", "image-size": "^0.9.3",
"linkstate": "^2.0.0", "linkstate": "^2.0.0",
@@ -33,7 +32,7 @@
"lodash.camelcase": "^4.3.0", "lodash.camelcase": "^4.3.0",
"mime-types": "^2.1.28", "mime-types": "^2.1.28",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"pointer-tracker": "^2.5.3", "pointer-tracker": "^2.4.0",
"postcss": "^7.0.35", "postcss": "^7.0.35",
"postcss-modules": "^3.2.2", "postcss-modules": "^3.2.2",
"postcss-nested": "^4.2.3", "postcss-nested": "^4.2.3",
@@ -41,17 +40,24 @@
"postcss-url": "^8.0.0", "postcss-url": "^8.0.0",
"preact": "^10.5.5", "preact": "^10.5.5",
"preact-render-to-string": "^5.1.11", "preact-render-to-string": "^5.1.11",
"prettier": "^2.4.1", "prettier": "^2.1.2",
"rollup": "^2.38.0", "rollup": "^2.38.0",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"serve": "^11.3.2", "serve": "^11.3.2",
"typescript": "^4.4.4", "typescript": "^4.1.3",
"which": "^2.0.2", "which": "^2.0.2"
"wasm-feature-detect": "^1.2.11" },
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}, },
"lint-staged": { "lint-staged": {
"*.{js,css,json,md,ts,tsx}": "prettier --write", "*.{js,css,json,md,ts,tsx}": "prettier --write",
"*.{c,h,cpp,hpp}": "clang-format -i", "*.{c,h,cpp,hpp}": "clang-format -i",
"*.rs": "rustfmt" "*.rs": "rustfmt"
},
"dependencies": {
"wasm-feature-detect": "^1.2.11"
} }
} }

View File

@@ -86,7 +86,6 @@ export default async function ({ watch }) {
'src/features-worker', 'src/features-worker',
'src/features-worker-worker-bridge', 'src/features-worker-worker-bridge',
'src/sw', 'src/sw',
'src/worker-shared',
'codecs', 'codecs',
]), ]),
urlPlugin(), urlPlugin(),

View File

@@ -37,10 +37,8 @@ main();
ga('set', 'transport', 'beacon'); ga('set', 'transport', 'beacon');
ga('set', 'dimension1', displayMode); ga('set', 'dimension1', displayMode);
ga('send', 'pageview', '/index.html', { title: 'Squoosh' }); ga('send', 'pageview', '/index.html', { title: 'Squoosh' });
// Load the GA script without keeping the browser spinner going. // Load the GA script
addEventListener('load', () => {
const script = document.createElement('script'); const script = document.createElement('script');
script.src = 'https://www.google-analytics.com/analytics.js'; script.src = 'https://www.google-analytics.com/analytics.js';
document.head.appendChild(script); document.head.appendChild(script);
});
} }

View File

@@ -11,11 +11,7 @@ export default class Checkbox extends Component<Props, State> {
return ( return (
<div class={style.checkbox}> <div class={style.checkbox}>
{props.checked ? ( {props.checked ? (
props.disabled ? (
<CheckedIcon class={`${style.icon} ${style.disabled}`} />
) : (
<CheckedIcon class={`${style.icon} ${style.checked}`} /> <CheckedIcon class={`${style.icon} ${style.checked}`} />
)
) : ( ) : (
<UncheckedIcon class={style.icon} /> <UncheckedIcon class={style.icon} />
)} )}

View File

@@ -2,26 +2,6 @@
display: inline-block; display: inline-block;
position: relative; position: relative;
--size: 17px; --size: 17px;
&::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 200%;
height: 200%;
background-color: var(--main-theme-color);
border-radius: 999px;
opacity: 0.25;
transform: translate(-50%, -50%) scale(0);
transition-property: transform;
transition-duration: 250ms;
}
&:focus-within::before {
transform: translate(-50%, -50%) scale(1);
}
} }
.real-checkbox { .real-checkbox {
@@ -40,7 +20,3 @@
.checked { .checked {
fill: var(--main-theme-color); fill: var(--main-theme-color);
} }
.disabled {
fill: var(--dark-gray);
}

View File

@@ -20,7 +20,7 @@ const REFLECTED_ATTRIBUTES = [
'disabled', 'disabled',
]; ];
function getPrecision(value: string): number { function getPrescision(value: string): number {
const afterDecimal = value.split('.')[1]; const afterDecimal = value.split('.')[1];
return afterDecimal ? afterDecimal.length : 0; return afterDecimal ? afterDecimal.length : 0;
} }
@@ -112,14 +112,13 @@ class RangeInputElement extends HTMLElement {
this.dispatchEvent(retargetted); this.dispatchEvent(retargetted);
}; };
private _getDisplayValue(value: number): string { private _formatDisplayValue(value: number): string {
if (value >= 10000) return (value / 1000).toFixed(1) + 'k';
const labelPrecision = const labelPrecision =
Number(this.labelPrecision) || getPrecision(this.step) || 0; Number(this.labelPrecision) || getPrescision(this.step) || 0;
return labelPrecision if (labelPrecision) {
? value.toFixed(labelPrecision) return value.toFixed(labelPrecision);
: Math.round(value).toString(); }
return Math.round(value).toString();
} }
private _update = () => { private _update = () => {
@@ -129,7 +128,7 @@ class RangeInputElement extends HTMLElement {
const min = Number(this.min) || 0; const min = Number(this.min) || 0;
const max = Number(this.max) || 100; const max = Number(this.max) || 100;
const percent = (100 * (value - min)) / (max - min); const percent = (100 * (value - min)) / (max - min);
const displayValue = this._getDisplayValue(value); const displayValue = this._formatDisplayValue(value);
this._valueDisplay!.textContent = displayValue; this._valueDisplay!.textContent = displayValue;
this.style.setProperty('--value-percent', percent + '%'); this.style.setProperty('--value-percent', percent + '%');

View File

@@ -47,10 +47,6 @@ range-input::before {
height: 12px; height: 12px;
} }
range-input:focus-within .thumb {
outline: white solid 2px;
}
.thumb-wrapper { .thumb-wrapper {
position: absolute; position: absolute;
left: 6px; left: 6px;

View File

@@ -35,7 +35,7 @@
text-decoration-style: dotted; text-decoration-style: dotted;
text-decoration-color: var(--main-theme-color); text-decoration-color: var(--main-theme-color);
text-underline-position: under; text-underline-position: under;
width: 54px; width: 48px;
position: relative; position: relative;
left: 5px; left: 5px;

View File

@@ -11,10 +11,6 @@
padding: 3px calc(var(--thumb-size) / 2 + 3px); padding: 3px calc(var(--thumb-size) / 2 + 3px);
} }
.checkbox:focus-within .track {
outline: white solid 2px;
}
.thumb { .thumb {
position: relative; position: relative;
width: var(--thumb-size); width: var(--thumb-size);

View File

@@ -17,7 +17,7 @@ import Toggle from './Toggle';
import Select from './Select'; import Select from './Select';
import { Options as QuantOptionsComponent } from 'features/processors/quantize/client'; import { Options as QuantOptionsComponent } from 'features/processors/quantize/client';
import { Options as ResizeOptionsComponent } from 'features/processors/resize/client'; import { Options as ResizeOptionsComponent } from 'features/processors/resize/client';
import { ImportIcon, SaveIcon, SwapIcon } from 'client/lazy-app/icons'; import { CLIIcon, SwapIcon } from 'client/lazy-app/icons';
interface Props { interface Props {
index: 0 | 1; index: 0 | 1;
@@ -29,22 +29,20 @@ interface Props {
onEncoderOptionsChange(index: 0 | 1, newOptions: EncoderOptions): void; onEncoderOptionsChange(index: 0 | 1, newOptions: EncoderOptions): void;
onProcessorOptionsChange(index: 0 | 1, newOptions: ProcessorState): void; onProcessorOptionsChange(index: 0 | 1, newOptions: ProcessorState): void;
onCopyToOtherSideClick(index: 0 | 1): void; onCopyToOtherSideClick(index: 0 | 1): void;
onSaveSideSettingsClick(index: 0 | 1): void; onCopyCliClick(index: 0 | 1): void;
onImportSideSettingsClick(index: 0 | 1): void;
} }
interface State { interface State {
supportedEncoderMap?: PartialButNotUndefined<typeof encoderMap>; supportedEncoderMap?: PartialButNotUndefined<typeof encoderMap>;
leftSideSettings?: string | null;
rightSideSettings?: string | null;
} }
type PartialButNotUndefined<T> = { type PartialButNotUndefined<T> = {
[P in keyof T]: T[P]; [P in keyof T]: T[P];
}; };
const supportedEncoderMapP: Promise<PartialButNotUndefined<typeof encoderMap>> = const supportedEncoderMapP: Promise<PartialButNotUndefined<
(async () => { typeof encoderMap
>> = (async () => {
const supportedEncoderMap: PartialButNotUndefined<typeof encoderMap> = { const supportedEncoderMap: PartialButNotUndefined<typeof encoderMap> = {
...encoderMap, ...encoderMap,
}; };
@@ -59,13 +57,11 @@ const supportedEncoderMapP: Promise<PartialButNotUndefined<typeof encoderMap>> =
); );
return supportedEncoderMap; return supportedEncoderMap;
})(); })();
export default class Options extends Component<Props, State> { export default class Options extends Component<Props, State> {
state: State = { state: State = {
supportedEncoderMap: undefined, supportedEncoderMap: undefined,
leftSideSettings: localStorage.getItem('leftSideSettings'),
rightSideSettings: localStorage.getItem('rightSideSettings'),
}; };
constructor() { constructor() {
@@ -75,29 +71,6 @@ export default class Options extends Component<Props, State> {
); );
} }
private setLeftSideSettings = () => {
this.setState({
leftSideSettings: localStorage.getItem('leftSideSettings'),
});
};
private setRightSideSettings = () => {
this.setState({
rightSideSettings: localStorage.getItem('rightSideSettings'),
});
};
componentDidMount(): void {
// Changing the state when side setting is stored in localstorage
window.addEventListener('leftSideSettings', this.setLeftSideSettings);
window.addEventListener('rightSideSettings', this.setRightSideSettings);
}
componentWillUnmount(): void {
window.removeEventListener('leftSideSettings', this.setLeftSideSettings);
window.removeEventListener('removeSideSettings', this.setRightSideSettings);
}
private onEncoderTypeChange = (event: Event) => { private onEncoderTypeChange = (event: Event) => {
const el = event.currentTarget as HTMLSelectElement; const el = event.currentTarget as HTMLSelectElement;
@@ -135,18 +108,14 @@ export default class Options extends Component<Props, State> {
this.props.onEncoderOptionsChange(this.props.index, newOptions); this.props.onEncoderOptionsChange(this.props.index, newOptions);
}; };
private onCopyCliClick = () => {
this.props.onCopyCliClick(this.props.index);
};
private onCopyToOtherSideClick = () => { private onCopyToOtherSideClick = () => {
this.props.onCopyToOtherSideClick(this.props.index); this.props.onCopyToOtherSideClick(this.props.index);
}; };
private onSaveSideSettingClick = () => {
this.props.onSaveSideSettingsClick(this.props.index);
};
private onImportSideSettingsClick = () => {
this.props.onImportSideSettingsClick(this.props.index);
};
render( render(
{ source, encoderState, processorState }: Props, { source, encoderState, processorState }: Props,
{ supportedEncoderMap }: State, { supportedEncoderMap }: State,
@@ -169,6 +138,13 @@ export default class Options extends Component<Props, State> {
<h3 class={style.optionsTitle}> <h3 class={style.optionsTitle}>
<div class={style.titleAndButtons}> <div class={style.titleAndButtons}>
Edit Edit
<button
class={style.cliButton}
title="Copy npx command"
onClick={this.onCopyCliClick}
>
<CLIIcon />
</button>
<button <button
class={style.copyOverButton} class={style.copyOverButton}
title="Copy settings to other side" title="Copy settings to other side"
@@ -176,36 +152,6 @@ export default class Options extends Component<Props, State> {
> >
<SwapIcon /> <SwapIcon />
</button> </button>
<button
class={style.saveButton}
title="Save side settings"
onClick={this.onSaveSideSettingClick}
>
<SaveIcon />
</button>
<button
class={
style.importButton +
' ' +
(!this.state.leftSideSettings && this.props.index === 0
? style.buttonOpacity
: '') +
' ' +
(!this.state.rightSideSettings && this.props.index === 1
? style.buttonOpacity
: '')
}
title="Import saved side settings"
onClick={this.onImportSideSettingsClick}
disabled={
// Disabled if this side's settings haven't been saved
(!this.state.leftSideSettings &&
this.props.index === 0) ||
(!this.state.rightSideSettings && this.props.index === 1)
}
>
<ImportIcon />
</button>
</div> </div>
</h3> </h3>
<label class={style.sectionEnabler}> <label class={style.sectionEnabler}>
@@ -257,9 +203,7 @@ export default class Options extends Component<Props, State> {
onChange={this.onEncoderTypeChange} onChange={this.onEncoderTypeChange}
large large
> >
<option value="identity">{`Original Image ${ <option value="identity">Original Image</option>
this.props.source ? `(${this.props.source.file.name})` : ''
}`}</option>
{Object.entries(supportedEncoderMap).map(([type, encoder]) => ( {Object.entries(supportedEncoderMap).map(([type, encoder]) => (
<option value={type}>{encoder.meta.label}</option> <option value={type}>{encoder.meta.label}</option>
))} ))}

View File

@@ -53,16 +53,6 @@
composes: option-toggle; composes: option-toggle;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
gap: 1em; gap: 1em;
border-top: 1px solid #fff4;
transition-property: background-color;
transition-duration: 250ms;
}
.option-reveal:focus-within,
.option-reveal:hover {
background-color: #fff2;
} }
.option-one-cell { .option-one-cell {
@@ -83,11 +73,11 @@
} }
.text-field { .text-field {
background-color: var(--black); background: var(--white);
color: var(--white); color: var(--black);
font: inherit; font: inherit;
border: none; border: none;
padding: 6px 6px 6px 10px; padding: 6px 0 6px 10px;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
border-radius: 4px; border-radius: 4px;
@@ -122,37 +112,7 @@
.copy-over-button { .copy-over-button {
composes: title-button; composes: title-button;
/* Make the filled arrow point towards the other options element */
transform: rotate(var(--rotate-copyoverbutton-angle));
svg { svg {
fill: var(--header-text-color); fill: var(--header-text-color);
} }
&:focus {
outline: var(--header-text-color) solid 2px;
outline-offset: 0.25em;
}
}
.save-button,
.import-button {
composes: title-button;
svg {
stroke: var(--header-text-color);
}
&:focus {
outline: var(--header-text-color) solid 2px;
outline-offset: 0.25em;
}
}
.button-opacity {
pointer-events: none;
cursor: not-allowed;
svg {
opacity: 0.5;
}
} }

View File

@@ -1,6 +1,5 @@
import PointerTracker, { Pointer } from 'pointer-tracker'; import PointerTracker, { Pointer } from 'pointer-tracker';
import 'add-css:./styles.css'; import 'add-css:./styles.css';
import { isSafari } from 'client/lazy-app/util';
interface Point { interface Point {
clientX: number; clientX: number;
@@ -106,23 +105,14 @@ export default class PinchZoom extends HTMLElement {
const pointerTracker: PointerTracker = new PointerTracker(this, { const pointerTracker: PointerTracker = new PointerTracker(this, {
start: (pointer, event) => { start: (pointer, event) => {
// We only want to track 2 pointers at most // We only want to track 2 pointers at most
if ( if (pointerTracker.currentPointers.length === 2 || !this._positioningEl)
pointerTracker.currentPointers.length === 2 ||
!this._positioningEl
) {
return false; return false;
}
event.preventDefault(); event.preventDefault();
return true; return true;
}, },
move: (previousPointers) => { move: (previousPointers) => {
this._onPointerMove(previousPointers, pointerTracker.currentPointers); this._onPointerMove(previousPointers, pointerTracker.currentPointers);
}, },
// Unfortunately Safari on iOS has a bug where pointer event capturing
// doesn't work in some cases, and we hit those cases due to our event
// retargeting in pinch-zoom.
// https://bugs.webkit.org/show_bug.cgi?id=220196
avoidPointerEvents: isSafari,
}); });
this.addEventListener('wheel', (event) => this._onWheel(event)); this.addEventListener('wheel', (event) => this._onWheel(event));

View File

@@ -40,8 +40,7 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
} }
.scrubber { .scrubber {
display: grid; display: flex;
align-content: center;
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
@@ -55,6 +54,10 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
padding: 0 calc(var(--thumb-size) * 0.24); padding: 0 calc(var(--thumb-size) * 0.24);
} }
.scrubber svg {
flex: 1;
}
.arrow-left { .arrow-left {
fill: var(--pink); fill: var(--pink);
} }

View File

@@ -5,10 +5,8 @@ import './custom-els/PinchZoom';
import './custom-els/TwoUp'; import './custom-els/TwoUp';
import * as style from './style.css'; import * as style from './style.css';
import 'add-css:./style.css'; import 'add-css:./style.css';
import { shallowEqual, isSafari } from '../../util'; import { shallowEqual } from '../../util';
import { import {
ToggleAliasingIcon,
ToggleAliasingActiveIcon,
ToggleBackgroundIcon, ToggleBackgroundIcon,
AddIcon, AddIcon,
RemoveIcon, RemoveIcon,
@@ -21,6 +19,7 @@ import { cleanSet } from '../../util/clean-modify';
import type { SourceImage } from '../../Compress'; import type { SourceImage } from '../../Compress';
import { linkRef } from 'shared/prerendered-app/util'; import { linkRef } from 'shared/prerendered-app/util';
import { drawDataToCanvas } from 'client/lazy-app/util/canvas'; import { drawDataToCanvas } from 'client/lazy-app/util/canvas';
interface Props { interface Props {
source?: SourceImage; source?: SourceImage;
preprocessorState?: PreprocessorState; preprocessorState?: PreprocessorState;
@@ -36,7 +35,6 @@ interface State {
scale: number; scale: number;
editingScale: boolean; editingScale: boolean;
altBackground: boolean; altBackground: boolean;
aliasing: boolean;
} }
const scaleToOpts: ScaleToOpts = { const scaleToOpts: ScaleToOpts = {
@@ -51,7 +49,6 @@ export default class Output extends Component<Props, State> {
scale: 1, scale: 1,
editingScale: false, editingScale: false,
altBackground: false, altBackground: false,
aliasing: false,
}; };
canvasLeft?: HTMLCanvasElement; canvasLeft?: HTMLCanvasElement;
canvasRight?: HTMLCanvasElement; canvasRight?: HTMLCanvasElement;
@@ -148,12 +145,6 @@ export default class Output extends Component<Props, State> {
return props.rightCompressed || (props.source && props.source.preprocessed); return props.rightCompressed || (props.source && props.source.preprocessed);
} }
private toggleAliasing = () => {
this.setState((state) => ({
aliasing: !state.aliasing,
}));
};
private toggleBackground = () => { private toggleBackground = () => {
this.setState({ this.setState({
altBackground: !this.state.altBackground, altBackground: !this.state.altBackground,
@@ -264,7 +255,7 @@ export default class Output extends Component<Props, State> {
render( render(
{ mobileView, leftImgContain, rightImgContain, source }: Props, { mobileView, leftImgContain, rightImgContain, source }: Props,
{ scale, editingScale, altBackground, aliasing }: State, { scale, editingScale, altBackground }: State,
) { ) {
const leftDraw = this.leftDrawable(); const leftDraw = this.leftDrawable();
const rightDraw = this.rightDrawable(); const rightDraw = this.rightDrawable();
@@ -284,11 +275,7 @@ export default class Output extends Component<Props, State> {
onTouchStartCapture={this.onRetargetableEvent} onTouchStartCapture={this.onRetargetableEvent}
onTouchEndCapture={this.onRetargetableEvent} onTouchEndCapture={this.onRetargetableEvent}
onTouchMoveCapture={this.onRetargetableEvent} onTouchMoveCapture={this.onRetargetableEvent}
onPointerDownCapture={ onPointerDownCapture={this.onRetargetableEvent}
// We avoid pointer events in our PinchZoom due to a Safari bug.
// That means we also need to avoid them here too, else we end up preventing the fallback mouse events.
isSafari ? undefined : this.onRetargetableEvent
}
onMouseDownCapture={this.onRetargetableEvent} onMouseDownCapture={this.onRetargetableEvent}
onWheelCapture={this.onRetargetableEvent} onWheelCapture={this.onRetargetableEvent}
> >
@@ -298,9 +285,7 @@ export default class Output extends Component<Props, State> {
ref={linkRef(this, 'pinchZoomLeft')} ref={linkRef(this, 'pinchZoomLeft')}
> >
<canvas <canvas
class={`${style.pinchTarget} ${ class={style.pinchTarget}
aliasing ? style.pixelated : ''
}`}
ref={linkRef(this, 'canvasLeft')} ref={linkRef(this, 'canvasLeft')}
width={leftDraw && leftDraw.width} width={leftDraw && leftDraw.width}
height={leftDraw && leftDraw.height} height={leftDraw && leftDraw.height}
@@ -316,9 +301,7 @@ export default class Output extends Component<Props, State> {
ref={linkRef(this, 'pinchZoomRight')} ref={linkRef(this, 'pinchZoomRight')}
> >
<canvas <canvas
class={`${style.pinchTarget} ${ class={style.pinchTarget}
aliasing ? style.pixelated : ''
}`}
ref={linkRef(this, 'canvasRight')} ref={linkRef(this, 'canvasRight')}
width={rightDraw && rightDraw.width} width={rightDraw && rightDraw.width}
height={rightDraw && rightDraw.height} height={rightDraw && rightDraw.height}
@@ -362,31 +345,10 @@ export default class Output extends Component<Props, State> {
</button> </button>
</div> </div>
<div class={style.buttonGroup}> <div class={style.buttonGroup}>
<button <button class={style.firstButton} onClick={this.onRotateClick}>
class={style.firstButton}
onClick={this.onRotateClick}
title="Rotate"
>
<RotateIcon /> <RotateIcon />
</button> </button>
{!isSafari && ( <button class={style.lastButton} onClick={this.toggleBackground}>
<button
class={style.button}
onClick={this.toggleAliasing}
title="Toggle smoothing"
>
{aliasing ? (
<ToggleAliasingActiveIcon />
) : (
<ToggleAliasingIcon />
)}
</button>
)}
<button
class={style.lastButton}
onClick={this.toggleBackground}
title="Toggle background"
>
{altBackground ? ( {altBackground ? (
<ToggleBackgroundActiveIcon /> <ToggleBackgroundActiveIcon />
) : ( ) : (

View File

@@ -49,7 +49,6 @@
align-self: center; align-self: center;
padding: 9px 66px; padding: 9px 66px;
position: relative; position: relative;
gap: 6px;
/* Allow clicks to fall through to the pinch zoom area */ /* Allow clicks to fall through to the pinch zoom area */
pointer-events: none; pointer-events: none;
@@ -69,6 +68,7 @@
display: flex; display: flex;
position: relative; position: relative;
z-index: 100; z-index: 100;
margin: 0 3px;
} }
.button, .button,
@@ -86,7 +86,8 @@
font-size: 1.2rem; font-size: 1.2rem;
cursor: pointer; cursor: pointer;
&:focus-visible { &:focus {
/* box-shadow: 0 0 0 2px var(--hot-pink); */
box-shadow: 0 0 0 2px #fff; box-shadow: 0 0 0 2px #fff;
outline: none; outline: none;
z-index: 1; z-index: 1;
@@ -95,7 +96,6 @@
.button { .button {
color: #fff; color: #fff;
margin: 0;
&:hover { &:hover {
background: rgba(50, 50, 50, 0.92); background: rgba(50, 50, 50, 0.92);
@@ -115,7 +115,7 @@
.last-button { .last-button {
composes: button; composes: button;
border-radius: 0 6px 6px 0; border-radius: 0 6px 6px 0;
border-right-width: 1px; border-left-width: 1px;
} }
.zoom { .zoom {
@@ -161,8 +161,3 @@ input.zoom {
pointer-events: auto; pointer-events: auto;
} }
} }
.pixelated {
image-rendering: crisp-edges;
image-rendering: pixelated;
}

View File

@@ -31,6 +31,7 @@ import Results from './Results';
import WorkerBridge from '../worker-bridge'; import WorkerBridge from '../worker-bridge';
import { resize } from 'features/processors/resize/client'; import { resize } from 'features/processors/resize/client';
import type SnackBarElement from 'shared/custom-els/snack-bar'; import type SnackBarElement from 'shared/custom-els/snack-bar';
import { generateCliInvocation } from '../util/cli';
import { drawableToImageData } from '../util/canvas'; import { drawableToImageData } from '../util/canvas';
export type OutputType = EncoderType | 'identity'; export type OutputType = EncoderType | 'identity';
@@ -99,6 +100,9 @@ async function decodeImage(
try { try {
if (!canDecode) { if (!canDecode) {
if (mimeType === 'image/ktx2') {
return await workerBridge.basisDecode(signal, blob);
}
if (mimeType === 'image/avif') { if (mimeType === 'image/avif') {
return await workerBridge.avifDecode(signal, blob); return await workerBridge.avifDecode(signal, blob);
} }
@@ -113,9 +117,9 @@ async function decodeImage(
} }
} }
// Otherwise fall through and try built-in decoding for a laugh. // Otherwise fall through and try built-in decoding for a laugh.
return await builtinDecode(signal, blob); return await builtinDecode(signal, blob, mimeType);
} catch (err) { } catch (err) {
if (err instanceof Error && err.name === 'AbortError') throw err; if (err.name === 'AbortError') throw err;
console.log(err); console.log(err);
throw Error("Couldn't decode image"); throw Error("Couldn't decode image");
} }
@@ -281,26 +285,15 @@ export default class Compress extends Component<Props, State> {
source: undefined, source: undefined,
loading: false, loading: false,
preprocessorState: defaultPreprocessorState, preprocessorState: defaultPreprocessorState,
// Tasking catched side settings if available otherwise taking default settings
sides: [ sides: [
localStorage.getItem('leftSideSettings') {
? {
...JSON.parse(localStorage.getItem('leftSideSettings') as string),
loading: false,
}
: {
latestSettings: { latestSettings: {
processorState: defaultProcessorState, processorState: defaultProcessorState,
encoderState: undefined, encoderState: undefined,
}, },
loading: false, loading: false,
}, },
localStorage.getItem('rightSideSettings') {
? {
...JSON.parse(localStorage.getItem('rightSideSettings') as string),
loading: false,
}
: {
latestSettings: { latestSettings: {
processorState: defaultProcessorState, processorState: defaultProcessorState,
encoderState: { encoderState: {
@@ -439,99 +432,6 @@ export default class Compress extends Component<Props, State> {
sides: cleanSet(this.state.sides, otherIndex, oldSettings), sides: cleanSet(this.state.sides, otherIndex, oldSettings),
}); });
}; };
/**
* This function saves encodedSettings and latestSettings of
* particular side in browser local storage
* @param index : (0|1)
* @returns
*/
private onSaveSideSettingsClick = async (index: 0 | 1) => {
if (index === 0) {
const leftSideSettings = JSON.stringify({
encodedSettings: this.state.sides[index].encodedSettings,
latestSettings: this.state.sides[index].latestSettings,
});
localStorage.setItem('leftSideSettings', leftSideSettings);
// Firing an event when we save side settings in localstorage
window.dispatchEvent(new CustomEvent('leftSideSettings'));
await this.props.showSnack('Left side settings saved', {
timeout: 1500,
actions: ['dismiss'],
});
return;
}
if (index === 1) {
const rightSideSettings = JSON.stringify({
encodedSettings: this.state.sides[index].encodedSettings,
latestSettings: this.state.sides[index].latestSettings,
});
localStorage.setItem('rightSideSettings', rightSideSettings);
// Firing an event when we save side settings in localstorage
window.dispatchEvent(new CustomEvent('rightSideSettings'));
await this.props.showSnack('Right side settings saved', {
timeout: 1500,
actions: ['dismiss'],
});
return;
}
};
/**
* This function sets the side state with catched localstorage
* value as per side index provided
* @param index : (0|1)
* @returns
*/
private onImportSideSettingsClick = async (index: 0 | 1) => {
const leftSideSettingsString = localStorage.getItem('leftSideSettings');
const rightSideSettingsString = localStorage.getItem('rightSideSettings');
if (index === 0 && leftSideSettingsString) {
const oldLeftSideSettings = this.state.sides[index];
const newLeftSideSettings = {
...this.state.sides[index],
...JSON.parse(leftSideSettingsString),
};
this.setState({
sides: cleanSet(this.state.sides, index, newLeftSideSettings),
});
const result = await this.props.showSnack('Left side settings imported', {
timeout: 3000,
actions: ['undo', 'dismiss'],
});
if (result === 'undo') {
this.setState({
sides: cleanSet(this.state.sides, index, oldLeftSideSettings),
});
}
return;
}
if (index === 1 && rightSideSettingsString) {
const oldRightSideSettings = this.state.sides[index];
const newRightSideSettings = {
...this.state.sides[index],
...JSON.parse(rightSideSettingsString),
};
this.setState({
sides: cleanSet(this.state.sides, index, newRightSideSettings),
});
const result = await this.props.showSnack(
'Right side settings imported',
{
timeout: 3000,
actions: ['undo', 'dismiss'],
},
);
if (result === 'undo') {
this.setState({
sides: cleanSet(this.state.sides, index, oldRightSideSettings),
});
}
return;
}
};
private onPreprocessorChange = async ( private onPreprocessorChange = async (
preprocessorState: PreprocessorState, preprocessorState: PreprocessorState,
@@ -565,6 +465,29 @@ 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(e);
}
};
/** /**
* Debounce the heavy lifting of updateImage. * Debounce the heavy lifting of updateImage.
* Otherwise, the thrashing causes jank, and sometimes crashes iOS Safari. * Otherwise, the thrashing causes jank, and sometimes crashes iOS Safari.
@@ -720,7 +643,7 @@ export default class Compress extends Component<Props, State> {
return { sides }; return { sides };
}); });
} catch (err) { } catch (err) {
if (err instanceof Error && err.name === 'AbortError') return; if (err.name === 'AbortError') return;
this.props.showSnack(`Source decoding error: ${err}`); this.props.showSnack(`Source decoding error: ${err}`);
throw err; throw err;
} }
@@ -778,7 +701,7 @@ export default class Compress extends Component<Props, State> {
return newState; return newState;
}); });
} catch (err) { } catch (err) {
if (err instanceof Error && err.name === 'AbortError') return; if (err.name === 'AbortError') return;
this.setState({ loading: false }); this.setState({ loading: false });
this.props.showSnack(`Preprocessing error: ${err}`); this.props.showSnack(`Preprocessing error: ${err}`);
throw err; throw err;
@@ -902,7 +825,7 @@ export default class Compress extends Component<Props, State> {
this.activeSideJobs[sideIndex] = undefined; this.activeSideJobs[sideIndex] = undefined;
} catch (err) { } catch (err) {
if (err instanceof Error && err.name === 'AbortError') return; if (err.name === 'AbortError') return;
this.setState((currentState) => { this.setState((currentState) => {
const sides = cleanMerge(currentState.sides, sideIndex, { const sides = cleanMerge(currentState.sides, sideIndex, {
loading: false, loading: false,
@@ -932,9 +855,8 @@ export default class Compress extends Component<Props, State> {
onEncoderTypeChange={this.onEncoderTypeChange} onEncoderTypeChange={this.onEncoderTypeChange}
onEncoderOptionsChange={this.onEncoderOptionsChange} onEncoderOptionsChange={this.onEncoderOptionsChange}
onProcessorOptionsChange={this.onProcessorOptionsChange} onProcessorOptionsChange={this.onProcessorOptionsChange}
onCopyCliClick={this.onCopyCliClick}
onCopyToOtherSideClick={this.onCopyToOtherClick} onCopyToOtherSideClick={this.onCopyToOtherClick}
onSaveSideSettingsClick={this.onSaveSideSettingsClick}
onImportSideSettingsClick={this.onImportSideSettingsClick}
/> />
)); ));
@@ -948,7 +870,7 @@ export default class Compress extends Component<Props, State> {
typeLabel={ typeLabel={
side.latestSettings.encoderState side.latestSettings.encoderState
? encoderMap[side.latestSettings.encoderState.type].meta.label ? encoderMap[side.latestSettings.encoderState.type].meta.label
: `${side.file ? `${side.file.name}` : 'Original Image'}` : 'Original Image'
} }
/> />
)); ));

View File

@@ -45,11 +45,9 @@
--hot-theme-color: var(--hot-pink); --hot-theme-color: var(--hot-pink);
--header-text-color: var(--white); --header-text-color: var(--white);
--scroller-radius: var(--options-radius) var(--options-radius) 0 0; --scroller-radius: var(--options-radius) var(--options-radius) 0 0;
--rotate-copyoverbutton-angle: 90deg; /* To point down */
@media (min-width: 600px) { @media (min-width: 600px) {
--scroller-radius: 0 var(--options-radius) var(--options-radius) 0; --scroller-radius: 0 var(--options-radius) var(--options-radius) 0;
--rotate-copyoverbutton-angle: 0deg; /* To point right (no change) */
} }
} }
@@ -58,11 +56,9 @@
--hot-theme-color: var(--deep-blue); --hot-theme-color: var(--deep-blue);
--header-text-color: var(--dark-text); --header-text-color: var(--dark-text);
--scroller-radius: var(--options-radius) var(--options-radius) 0 0; --scroller-radius: var(--options-radius) var(--options-radius) 0 0;
--rotate-copyoverbutton-angle: -90deg; /* To point up */
@media (min-width: 600px) { @media (min-width: 600px) {
--scroller-radius: var(--options-radius) 0 0 var(--options-radius); --scroller-radius: var(--options-radius) 0 0 var(--options-radius);
--rotate-copyoverbutton-angle: 180deg; /* To point left */
} }
} }
@@ -113,13 +109,6 @@
& > svg { & > svg {
width: 47px; width: 47px;
overflow: visible;
}
&:focus .back-blob {
stroke: var(--deep-blue);
stroke-width: 5px;
animation: strokePulse 500ms ease forwards;
} }
@media (min-width: 600px) { @media (min-width: 600px) {
@@ -131,15 +120,6 @@
} }
} }
@keyframes strokePulse {
from {
stroke-width: 8px;
}
to {
stroke-width: 5px;
}
}
.back-blob { .back-blob {
fill: var(--hot-pink); fill: var(--hot-pink);
opacity: 0.77; opacity: 0.77;

View File

@@ -11,25 +11,6 @@ const Icon = (props: preact.JSX.HTMLAttributes) => (
/> />
); );
export const ToggleAliasingIcon = (props: preact.JSX.HTMLAttributes) => (
<Icon {...props}>
<circle
cx="12"
cy="12"
r="8"
fill="none"
stroke="currentColor"
stroke-width="2"
/>
</Icon>
);
export const ToggleAliasingActiveIcon = (props: preact.JSX.HTMLAttributes) => (
<Icon {...props}>
<path d="M12 3h5v2h2v2h2v5h-2V9h-2V7h-2V5h-3V3M21 12v5h-2v2h-2v2h-5v-2h3v-2h2v-2h2v-3h2M12 21H7v-2H5v-2H3v-5h2v3h2v2h2v2h3v2M3 12V7h2V5h2V3h5v2H9v2H7v2H5v3H3" />
</Icon>
);
export const ToggleBackgroundIcon = (props: preact.JSX.HTMLAttributes) => ( export const ToggleBackgroundIcon = (props: preact.JSX.HTMLAttributes) => (
<Icon {...props}> <Icon {...props}>
<path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm2 4v-2H3c0 1.1.9 2 2 2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-18H9a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h10a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z" /> <path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm2 4v-2H3c0 1.1.9 2 2 2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-18H9a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h10a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z" />
@@ -92,36 +73,19 @@ export const DownloadIcon = () => (
</svg> </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 = () => ( export const SwapIcon = () => (
<svg viewBox="0 0 18 14"> <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" /> <path d="M5.5 3.6v6.8L2.1 7l3.4-3.4M7 0L0 7l7 7V0zm4 0v14l7-7-7-7z" />
</svg> </svg>
); );
export const SaveIcon = () => (
<svg viewBox="0 0 24 24">
<g
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M12.501 20.93c-.866.25-1.914-.166-2.176-1.247a1.724 1.724 0 0 0-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 0 0-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.94-1.543.826-3.31 2.37-2.37c1 .608 2.296.07 2.572-1.065c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.074.26 1.49 1.296 1.252 2.158M19 22v-6m3 3l-3-3l-3 3" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0-6 0" />
</g>
</svg>
);
export const ImportIcon = () => (
<svg viewBox="0 0 24 24">
<g
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M12.52 20.924c-.87.262-1.93-.152-2.195-1.241a1.724 1.724 0 0 0-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 0 0-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.94-1.543.826-3.31 2.37-2.37c1 .608 2.296.07 2.572-1.065c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.088.264 1.502 1.323 1.242 2.192M19 16v6m3-3l-3 3l-3-3" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0-6 0" />
</g>
</svg>
);

View File

@@ -63,17 +63,35 @@ interface DrawableToImageDataOptions {
sh?: number; 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( export function drawableToImageData(
drawable: ImageBitmap | HTMLImageElement, drawable: ImageBitmap | HTMLImageElement | VideoFrame,
opts: DrawableToImageDataOptions = {}, opts: DrawableToImageDataOptions = {},
): ImageData { ): ImageData {
const { const {
width = drawable.width, width = getWidth(drawable),
height = drawable.height, height = getHeight(drawable),
sx = 0, sx = 0,
sy = 0, sy = 0,
sw = drawable.width, sw = getWidth(drawable),
sh = drawable.height, sh = getHeight(drawable),
} = opts; } = opts;
// Make canvas same size as image // Make canvas same size as image

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