Compare commits

..

67 Commits

Author SHA1 Message Date
dependabot[bot]
945cb28744 Bump ansi-regex from 5.0.0 to 5.0.1 in /cli
Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/chalk/ansi-regex/releases)
- [Commits](https://github.com/chalk/ansi-regex/compare/v5.0.0...v5.0.1)

---
updated-dependencies:
- dependency-name: ansi-regex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-03 13:56:31 +00:00
jakearchibald
918c596cba Add no-longer-maintained message to cli and libsquoosh 2023-01-03 13:55:36 +00:00
jakearchibald
42594277fd Remove web codecs usage
It was decoding transparency incorrectly (multiplication wrong?), and I'd rather not have a different code path there for the sake of it
2022-12-15 14:56:51 +00:00
François Beaufort
97d13de273 Add desktop screenshots to web app manifest (#1283)
Co-authored-by: jakearchibald <jaffathecake@gmail.com>
2022-09-18 03:22:10 +01:00
jakearchibald
a1c3304c56 Fix issue with Chrome 105 and flexbox. Fixes #1281
https://bugs.chromium.org/p/chromium/issues/detail?id=1358807
2022-09-01 08:50:43 +01:00
Anthony Swierkosz
7a2fc917c1 Remove margin in Safari between buttons in button group (#1275) 2022-08-18 12:45:10 +01:00
Anthony Swierkosz
1c4233b5ae Add uniform spacing vertically and horizontally (#1277) 2022-08-18 12:44:11 +01:00
Anthony Swierkosz
4d67ec5a7b Fix misplaced border (#1273) 2022-08-15 08:38:36 +01:00
Jake Archibald
c24505230d Bump version due to breaking changes 2022-02-18 15:36:31 +00:00
Jake Archibald
2238d1abaf Update version 2022-02-16 16:23:33 +00:00
Ergün Erdoğmuş
65ea02627b Expose Typescript types in libSquoosh (#1142)
* Expose type declarations in libSquoosh npm package

* Add comment on why we remove the tsbuildinfo

* Fix PreprocessOptions type

Resize should require at least one of the width, height.
The other options are optional for all preprocessors

* Update libSquoosh README to reflect encode changes

I also removed requiring `await image.decoded` call before calling
preprocess or encode since they decode the image before the operation
2022-02-16 16:19:42 +00:00
Ziemowit Zabawa
18b53e2b8a JPEG XL: Add lossy Modular option (#1136)
* Improve .gitattributes

* Add disabled checkbox style

* Update Makefile

* Update jxl_enc.cpp

* add -O3 flag to skia compilation for optimization's sake

* Bump libjxl revision to 9f544641ec83f6abd9da598bdd08178ee8a003e0

Change use of EncodeFile from `EncodeFile(cparams, &io, &passes_enc_state, &bytes, /*aux=*/nullptr, pool_ptr)` to `EncodeFile(cparams, &io, &passes_enc_state, &bytes, /*cms=*/nullptr, /*aux=*/jxl::GetJxlCms(), pool_ptr)`

* JPEG XL: Add lossy Modular option

Co-authored-by: CanadianBaconBoi <beamconnor@gmail.com>
Co-authored-by: CanadianBaconBoi <bc.bacon.bits@gmail.com>
2022-01-21 12:42:27 +00:00
Surma
66ec763667 Merge pull request #1171 from TimvdLippe/fix-webassembly-instantiate 2021-10-20 15:42:50 +01:00
Surma
029eda7b37 Merge branch 'dev' into fix-webassembly-instantiate 2021-10-20 15:36:48 +01:00
Tim van der Lippe
e6810059ef Add warning for using multiple image pools
Instantiating and using multiple image pools is not intended and can
lead to memory problems. Instead, users should use a single pool and
reuse that pool across all image processing. Therefore, let's add
a warning in the README to specify call this out and avoid users
of running into issues.

Relates to #1065
2021-10-20 14:56:34 +01:00
Surma
4eba015009 Merge pull request #1165 from TimvdLippe/update-typescript 2021-10-20 13:25:54 +01:00
Surma
dd49a1f23d Merge branch 'dev' into update-typescript 2021-10-20 13:15:39 +01:00
Surma
5ecb99992c Merge pull request #1169 from TimvdLippe/update-prettier 2021-10-20 13:15:32 +01:00
Surma
d9c6ebe0a2 Merge branch 'dev' into update-prettier 2021-10-20 12:48:59 +01:00
Surma
fba67a7a18 Merge pull request #1170 from TimvdLippe/fix-husky 2021-10-20 12:47:01 +01:00
Tim van der Lippe
129b925098 Fix Husky pre-commit
By upgrading to version 7, we now ensure that NPM/Yarn will run
the `prepare` script and appropriately install Husky. This also
migrates us to the `pre-commit` hook as a separate file, rather
than as part of the `package.json` definition.
2021-10-20 12:44:12 +01:00
Tim van der Lippe
e23bc4d2e5 Update Prettier
Also manually run prettier on `src/` and `lib/` to ensure
consistent formatting.
2021-10-20 12:28:45 +01:00
Tim van der Lippe
f523a07f01 Update TypeScript and types packages
This pulls in TypeScript 4.4, which ships ResizeObserver in the DOM
types, as well as sets caught errors to `unknown`, requiring explicit
checks for `AbortError`.
2021-10-20 11:36:27 +01:00
Vignesh Raj
e3f840c6da Added Apple touch Icon for PWA (#1160) 2021-10-15 11:52:13 +01:00
Jake Archibald
cad09160b6 Reducing browser spinner time (#1157) 2021-09-20 13:49:53 +01:00
Surma
9663c23489 Merge pull request #1151 from ergunsh/improve-encode-types
Improve `encode` API and its types
2021-09-17 13:50:29 +01:00
ergunsh
95a1b35c91 Update encodedWith to contain resolved values
We already await the promises that we set on the `encodedWith` instead of setting already
resolved promises to `encodedWith` we can set the resolved values

So, the users can use like
`const { mozjpeg: { binary } } = await image.encode({ mozjpeg })` or
they can first run
`await image.encode({ mozjpeg })` and then
`image.encodedWith.mozjpeg.binary`
2021-09-10 15:18:14 +02:00
ergunsh
914cdea41d Return encode result from Image.encode calls
Before this, there were one way to use the API:
call `await image.encode({ mozjpeg: {} })`
then use `encodedWith` by asserting that `mozjpeg` property exists on it

After adding return value to encode, people
will be able to use it like
`const { mozjpeg } = await image.encode({ mozjpeg: {} });`
which provides better type safety
2021-09-10 14:45:20 +02:00
ergunsh
6bfce29af6 Improve typing of Image.encode
Instead of returning `any` we're now returning the whole object.

Still from typing perspective, the API is not that
great since we don't have any type relation
between `encode` calls and `encodedWith` property. Maybe we can think about
returning directly from `encode` call
with the returned object having properties
that is supplied in `encode` calls
2021-09-10 14:08:21 +02:00
Surma
64cad1cc23 Merge pull request #1123 from styfle/load-file-async
Reduce `@squoosh/lib` Node.js API usage (make it run on the web)
2021-09-08 22:30:53 +01:00
Steven
87f25d909b Merge branch 'dev' into load-file-async 2021-09-08 17:04:50 -04:00
Steven
bdfdaf53af Remove unused readme link per atjn
Co-authored-by: Anton <dev@atjn.dk>
2021-09-08 17:04:46 -04:00
Surma
1ab9e8b3ab Merge pull request #1141 from ergunsh/improve-exposed-types 2021-09-08 17:57:46 +01:00
Ergün Erdoğmuş
2718077d3d Merge branch 'dev' into improve-exposed-types 2021-09-08 19:48:01 +03:00
Jake Archibald
255dfa434a Update pointer-tracker (#1147) 2021-09-07 14:43:19 +01:00
François Beaufort
0bef05bcd4 Remove origin trial header 2021-09-06 15:00:48 +01:00
ergunsh
c4bc369c6b Fix typo in triangle 2021-09-03 15:03:28 +03:00
ergunsh
68ce8f420d Improve encode parameter typing 2021-09-03 14:54:08 +03:00
ergunsh
27f103fee5 Improve preprocess parameter type 2021-09-03 11:53:52 +03:00
Steven
c21d3714a8 Fix link in readme 2021-09-02 16:38:22 -04:00
Steven
3ea0d88c4f Apply suggestions from atjn
Co-authored-by: Anton <dev@atjn.dk>
2021-09-02 16:36:37 -04:00
Surma
00cfdafdf3 Merge pull request #1138 from jcao219/fix-windows-path
Use pathify to calculate absolute paths for cross-platform compatibility
2021-08-31 13:34:28 +01:00
Jimmy Cao
92f52319da Use pathify to calculate absolute paths for cross-platform compatibility 2021-08-31 00:18:21 -07:00
Jake Archibald
023304803f JPEG-XL: Better 'effort' and adding noise synth (#1039) 2021-08-27 14:18:54 +01:00
Vishal
6b08cd2355 Aliasing toggle button (#1132) 2021-08-26 16:44:20 +01:00
Jake Archibald
db1a5138e6 Fix pinch zoom on iOS (#1133) 2021-08-26 15:59:42 +01:00
Jake Archibald
a547491146 Maintain DC_SCAN_OPT_MODE if subsampling is > 2. Fixes #1129 (#1130) 2021-08-26 14:29:13 +01:00
Jake Archibald
6e427f9208 The observer is null if user prefers reduced motion
Turns out TypeScript is there for a reason and shouldn't be bypassed. Fixes #1135
2021-08-25 14:10:57 +01:00
Jake Archibald
0d35fbd349 Fix horizontal scroll on option elements on mobile (#1134) 2021-08-25 09:25:09 +01:00
Mike DelGaudio
4e901c714c [RFC] Add Three Homepage Sections (#1112) 2021-08-25 09:08:22 +01:00
Adam Burgess
b74788e036 Stable mousewheel scrolling, one wheel up + one wheel down = no change (#1131) 2021-08-24 14:52:25 +01:00
Vishal
14c3d190e9 Max cap on scale #905 (#1128) 2021-08-23 13:27:17 +01:00
Steven
202d0bc088 Generalize file to ArrayLike<number> 2021-08-20 18:48:30 -04:00
Steven
bdb5b16372 Remove Buffer from decodeFile() 2021-08-20 18:36:23 -04:00
Steven
30140c2b7a Fix CLI bug 2021-08-20 18:35:29 -04:00
Steven
10996cdbca Add missing import 2021-08-20 16:08:01 -04:00
Steven
e5806507d4 Remove Node.js API surface in libsquoosh 2021-08-20 15:11:14 -04:00
Steven
1c5b44f9a1 Add loadFile parameter to ImagePool 2021-08-16 16:51:23 -04:00
Surma
eeaa19589e Merge pull request #1120 from veluca93/jxl_05
Bump JPEG XL to v0.5.
2021-08-14 09:23:04 +01:00
Luca Versari
35b8c56f1a Bump JPEG XL to v0.5. 2021-08-13 23:27:44 +02:00
Surma
816d1f92fd Merge pull request #1110 from styfle/libsquoosh-ts-remaining 2021-08-10 12:38:11 +01:00
Steven
4091f2efec Remove unused export 2021-08-06 13:20:14 -04:00
Steven
821d14c6ab Merge branch 'dev' into libsquoosh-ts-remaining 2021-08-06 13:02:46 -04:00
Steven
a72ca46531 Move as to input param instead of return type 2021-08-06 12:49:17 -04:00
ergunsh
fafcf97f0c Update code for the review comments
* Make decode module return value `ImageData`
* Fix global definition of ImageData
* Use concrete Encoder types for encode functions
* Use ArrayBufferView in FileLike instead of using a similar type
* Throw error when the `encode` functions
return null
* Use generic types for WorkerPool
* Fix `encode` function typing
in `index.ts`
* Remove ts-ignore for web-streams-polyfill
and handle nulls for TransformStream
* Fix rollup entry point (now we need to have
`index.ts` instead of `index.js`)
2021-08-06 16:05:14 +03:00
Steven
de4eb9c8f7 Add encode() and decode() types 2021-07-26 23:22:28 -04:00
Steven
16a53caa48 Convert remaining JS to TS in libSquoosh 2021-07-26 22:37:00 -04:00
137 changed files with 1501 additions and 1288 deletions

4
.gitattributes vendored
View File

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

1
.husky/pre-commit Executable file
View File

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

View File

@@ -1,3 +1,7 @@
# Project no longer maintained
Unfortunately, due to a few people leaving the team, and staffing issues resulting from the current economic climate (ugh), this package is no longer actively maintained. I know that sucks, but there simply isn't the time & people to work on this. If anyone from the community wants to fork it, you have my blessing. The [squoosh.app](https://squoosh.app) will continue to be supported and improved.
# 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.

16
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@squoosh/cli",
"version": "0.7.2",
"version": "0.7.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@squoosh/cli",
"version": "0.7.2",
"version": "0.7.3",
"license": "Apache-2.0",
"dependencies": {
"@squoosh/lib": "^0.4.0",
@@ -36,9 +36,9 @@
}
},
"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==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
@@ -363,9 +363,9 @@
}
},
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
},
"ansi-styles": {
"version": "4.3.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@squoosh/cli",
"version": "0.7.2",
"version": "0.7.3",
"description": "A CLI for Squoosh",
"public": true,
"type": "module",

View File

@@ -4,6 +4,7 @@ import { program } from 'commander/esm.mjs';
import JSON5 from 'json5';
import path from 'path';
import { promises as fsp } from 'fs';
import { cpus } from 'os';
import ora from 'ora';
import kleur from 'kleur';
@@ -75,7 +76,9 @@ async function getInputFiles(paths) {
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))
? (await fsp.readdir(inputPath, { withFileTypes: true }))
.filter((dirent) => dirent.isFile())
.map((dirent) => path.join(inputPath, dirent.name))
: [inputPath];
for (const file of files) {
try {
@@ -101,7 +104,7 @@ async function getInputFiles(paths) {
async function processFiles(files) {
files = await getInputFiles(files);
const imagePool = new ImagePool();
const imagePool = new ImagePool(cpus().length);
const results = new Map();
const progress = progressTracker(results);
@@ -116,7 +119,8 @@ async function processFiles(files) {
let decoded = 0;
let decodedFiles = await Promise.all(
files.map(async (file) => {
const image = imagePool.ingestImage(file);
const buffer = await fsp.readFile(file);
const image = imagePool.ingestImage(buffer);
await image.decoded;
results.set(image, {
file,
@@ -178,7 +182,7 @@ async function processFiles(files) {
const outputPath = path.join(
program.opts().outputDir,
path.basename(originalFile, path.extname(originalFile)) +
program.opts().suffix
program.opts().suffix,
);
for (const output of Object.values(image.encodedWith)) {
const outputFile = `${outputPath}.${(await output).extension}`;

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

View File

@@ -8,7 +8,6 @@ ENV LDFLAGS "${CFLAGS} \
-s ALLOW_MEMORY_GROWTH=1 \
-s TEXTDECODER=2 \
-s NODEJS_CATCH_EXIT=0 -s NODEJS_CATCH_REJECTION=0 \
-s MINIMAL_RUNTIME=1 \
"
# Build and cache standard libraries with these flags + Embind.
RUN emcc ${CXXFLAGS} ${LDFLAGS} --bind -xc++ /dev/null -o /dev/null

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
CODEC_URL = https://github.com/libjxl/libjxl.git
CODEC_VERSION = 3f76321a7cbcf4f452894dc173eb7be60f508ecb
CODEC_VERSION = 9f544641ec83f6abd9da598bdd08178ee8a003e0
CODEC_DIR = node_modules/jxl
CODEC_BUILD_ROOT := $(CODEC_DIR)/build
CODEC_MT_BUILD_DIR := $(CODEC_BUILD_ROOT)/mt
@@ -75,6 +75,9 @@ $(CODEC_MT_SIMD_BUILD_DIR)/Makefile: CXXFLAGS+=-msimd128
-DCMAKE_CROSSCOMPILING_EMULATOR=node \
-B $(@D) \
$(<D)
emcc -Wall -O3 -o $(CODEC_DIR)/third_party/skcms/skcms.cc.o -I$(CODEC_DIR)/third_party/skcms -c $(CODEC_DIR)/third_party/skcms/skcms.cc
llvm-ar rc $(CODEC_BUILD_DIR)/third_party/libskcms.a $(CODEC_DIR)/third_party/skcms/skcms.cc.o
rm $(CODEC_DIR)/third_party/skcms/skcms.cc.o
$(CODEC_DIR)/CMakeLists.txt:
$(RM) -r $(@D)

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

View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

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

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

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

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

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

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

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

View File

@@ -20,6 +20,7 @@ const allSrcPlaceholder = 'CLIENT_BUNDLE_PLUGIN_ALL_SRC';
export function getDependencies(clientOutput, item) {
const crawlDependencies = new Set([item.fileName]);
const referencedFiles = new Set();
for (const fileName of crawlDependencies) {
const chunk = clientOutput.find((v) => v.fileName === fileName);
@@ -27,11 +28,24 @@ export function getDependencies(clientOutput, item) {
for (const dep of chunk.imports) {
crawlDependencies.add(dep);
}
for (const dep of chunk.referencedFiles) {
referencedFiles.add(dep);
}
}
// Don't add self as dependency
crawlDependencies.delete(item.fileName);
// Merge referencedFiles as regular deps. They need to be in the same Set as
// some JS files might appear in both lists and need to be deduped too.
//
// Didn't do this as part of the main loop since their `chunk` can't have
// nested deps and sometimes might be missing altogether, depending on type.
for (const dep of referencedFiles) {
crawlDependencies.add(dep);
}
return [...crawlDependencies];
}
@@ -139,9 +153,9 @@ export default function (inputOptions, outputOptions, resolveFileUrl) {
if (property.startsWith(allSrcPlaceholder)) {
const allModules = [
clientEntry,
...dependencies.map((name) =>
clientOutput.find((item) => item.fileName === name),
),
...dependencies
.map((name) => clientOutput.find((item) => item.fileName === name))
.filter((item) => item.code),
];
const inlineDefines = [

View File

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

View File

@@ -17,10 +17,14 @@ const prefix = 'entry-data:';
const mainNamePlaceholder = 'ENTRY_DATA_PLUGIN_MAIN_NAME';
const dependenciesPlaceholder = 'ENTRY_DATA_PLUGIN_DEPS';
const placeholderRe = /(ENTRY_DATA_PLUGIN_(?:MAIN_NAME|DEPS))(\d+)/g;
const filenamePrefix = 'static/';
/** @param {string} fileName */
export function fileNameToURL(fileName) {
return fileName.replace(/^static\//, '/');
}
export default function entryDataPlugin() {
/** @type {string} */
/** @type {number} */
let exportCounter;
/** @type {Map<number, string>} */
let counterToIdMap;
@@ -69,14 +73,12 @@ export default function entryDataPlugin() {
if (!chunk) throw Error(`Cannot find ${id}`);
if (placeholder === mainNamePlaceholder) {
return JSON.stringify(
chunk.fileName.slice(filenamePrefix.length),
);
return JSON.stringify(fileNameToURL(chunk.fileName));
}
return JSON.stringify(
getDependencies(chunks, chunk).map((item) =>
item.slice(filenamePrefix.length),
getDependencies(chunks, chunk).map((filename) =>
fileNameToURL(filename),
),
);
},

View File

@@ -28,10 +28,17 @@ if (!self.<%- amdFunctionName %>) {
<% } else { %>
new Promise(resolve => {
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");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
};
document.head.appendChild(link);
} else {
self.nextDefineUri = uri;
importScripts(uri);

View File

@@ -1,3 +1,7 @@
# Project no longer maintained
Unfortunately, due to a few people leaving the team, and staffing issues resulting from the current economic climate (ugh), this package is no longer actively maintained. I know that sucks, but there simply isn't the time & people to work on this. If anyone from the community wants to fork it, you have my blessing. The [squoosh.app](https://squoosh.app) will continue to be supported and improved.
# 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.
@@ -16,21 +20,25 @@ You can start using the libSquoosh by adding these lines to the top of your JS p
```js
import { ImagePool } from '@squoosh/lib';
const imagePool = new ImagePool();
import { cpus } from 'os';
const imagePool = new ImagePool(cpus().length);
```
This will create an image pool with an underlying processing pipeline that you can use to ingest and encode images. The ImagePool constructor takes one argument that defines how many parallel operations it is allowed to run at any given time. By default, this number is set to the amount of CPU cores available in the system it is running on.
This will create an image pool with an underlying processing pipeline that you can use to ingest and encode images. The ImagePool constructor takes one argument that defines how many parallel operations it is allowed to run at any given time.
:warning: Important! Make sure to only create 1 `ImagePool` when performing parallel image processing. If you create multiple pools, the `ImagePool` can run out of memory and crash. By reusing a single `ImagePool`, you can ensure that the backing worker queue and processing pipeline releases memory prior to processing the next image.
## Ingesting images
You can ingest a new image like so:
```js
const imagePath = 'path/to/image.png';
const image = imagePool.ingestImage(imagePath);
import fs from 'fs/promises';
const file = await fs.readFile('./path/to/image.png');
const image = imagePool.ingestImage(file);
```
The `ingestImage` function can take anything the node [`readFile`][readfile] function can take, including a buffer and `FileHandle`.
The `ingestImage` function can accept any [`ArrayBuffer`][arraybuffer] whether that is from `readFile()` or `fetch()`.
The returned `image` object is a representation of the original image, that you can now preprocess, encode, and extract information about.
@@ -39,23 +47,19 @@ The returned `image` object is a representation of the original image, that you
When an image has been ingested, you can start preprocessing it and encoding it to other formats. This example will resize the image and then encode it to a `.jpg` and `.jxl` image:
```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 = {
@@ -63,9 +67,8 @@ const encodeOptions = {
jxl: {
quality: 90,
},
}
await image.encode(encodeOptions);
};
const result = 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._
@@ -89,7 +92,7 @@ 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;
const rawEncodedImage = image.encodedWith.mozjpeg.binary;
fs.writeFile('/path/to/new/image.jpg', rawEncodedImage);
```
@@ -100,10 +103,7 @@ This example iterates through all encoded versions of the image and writes them
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,
);
fs.writeFile(newImagePath + encodedImage.extension, encodedImage.binary);
}
```
@@ -132,7 +132,7 @@ console.log(await image.decoded);
Information about an encoded image can be found at `Image.encodedWith[encoderName]`. It looks something like this:
```js
console.log(await image.encodedWith.jxl);
console.log(image.encodedWith.jxl);
// Returns:
{
optionsUsed: {
@@ -168,4 +168,4 @@ const encodeOptions: {
[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
[arraybuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer

View File

@@ -0,0 +1,26 @@
const fs = require('fs');
const del = require('del');
const path = require('path');
const tsOutputDir = path.resolve('..', '.tmp', 'ts', 'libsquoosh');
const tsOutputSourceDir = path.join(tsOutputDir, 'src');
const buildDir = path.resolve('build');
(async () => {
await del(path.join(buildDir, '*.d.ts'));
const files = await fs.promises.readdir(tsOutputSourceDir);
const movePromises = [];
for (const file of files) {
if (file.endsWith('d.ts') || file.endsWith('d.ts.map')) {
movePromises.push(
fs.promises.rename(
path.join(tsOutputSourceDir, file),
path.join(buildDir, file),
),
);
}
}
// We need to remove `tsconfig.tsbuildinfo` otherwise TS does not emit unchanged `.d.ts` files
await del([path.join(tsOutputDir, 'tsconfig.tsbuildinfo')], { force: true });
})();

View File

@@ -1,12 +1,12 @@
{
"name": "@squoosh/lib",
"version": "0.4.0",
"version": "0.5.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@squoosh/lib",
"version": "0.4.0",
"version": "0.5.2",
"license": "Apache-2.0",
"dependencies": {
"wasm-feature-detect": "^1.2.11",

View File

@@ -1,14 +1,15 @@
{
"name": "@squoosh/lib",
"version": "0.4.0",
"version": "0.5.2",
"description": "A Node library for Squoosh",
"public": true,
"main": "./build/index.js",
"types": "./build/index.d.ts",
"files": [
"/build/*"
],
"scripts": {
"build": "rollup -c"
"build": "rollup -c && node lib/move-d-ts.js"
},
"keywords": [],
"author": "Google Chrome Developers <chromium-dev@google.com>",

View File

@@ -10,7 +10,7 @@ import { builtinModules } from 'module';
/** @type {import('rollup').RollupOptions} */
export default {
input: 'src/index.js',
input: 'src/index.ts',
output: {
dir: 'build',
format: 'cjs',

View File

@@ -9,6 +9,12 @@ import { cpus } from 'os';
hardwareConcurrency: cpus().length,
};
interface DecodeModule extends EmscriptenWasm.Module {
decode: (data: Uint8Array) => ImageData;
}
type DecodeModuleFactory = EmscriptenWasm.ModuleFactory<DecodeModule>;
interface RotateModuleInstance {
exports: {
memory: WebAssembly.Memory;
@@ -23,17 +29,26 @@ interface ResizeWithAspectParams {
target_height: number;
}
interface ResizeInstantiateOptions {
export interface ResizeOptions {
width: number;
height: number;
method: string;
method: 'triangle' | 'catrom' | 'mitchell' | 'lanczos3';
premultiply: boolean;
linearRGB: boolean;
}
export interface QuantOptions {
numColors: number;
dither: number;
}
export interface RotateOptions {
numRotations: number;
}
declare global {
// Needed for being able to use ImageData as type in codec types
type ImageData = typeof import('./image_data.js');
type ImageData = import('./image_data.js').default;
// Needed for being able to assign to `globalThis.ImageData`
var ImageData: ImageData['constructor'];
}
@@ -41,18 +56,23 @@ declare global {
import type { QuantizerModule } from '../../codecs/imagequant/imagequant.js';
// MozJPEG
import type { MozJPEGModule as MozJPEGEncodeModule } from '../../codecs/mozjpeg/enc/mozjpeg_enc';
import mozEnc from '../../codecs/mozjpeg/enc/mozjpeg_node_enc.js';
import mozEncWasm from 'asset-url:../../codecs/mozjpeg/enc/mozjpeg_node_enc.wasm';
import mozDec from '../../codecs/mozjpeg/dec/mozjpeg_node_dec.js';
import mozDecWasm from 'asset-url:../../codecs/mozjpeg/dec/mozjpeg_node_dec.wasm';
import type { EncodeOptions as MozJPEGEncodeOptions } from '../../codecs/mozjpeg/enc/mozjpeg_enc';
// WebP
import type { WebPModule as WebPEncodeModule } from '../../codecs/webp/enc/webp_enc';
import webpEnc from '../../codecs/webp/enc/webp_node_enc.js';
import webpEncWasm from 'asset-url:../../codecs/webp/enc/webp_node_enc.wasm';
import webpDec from '../../codecs/webp/dec/webp_node_dec.js';
import webpDecWasm from 'asset-url:../../codecs/webp/dec/webp_node_dec.wasm';
import type { EncodeOptions as WebPEncodeOptions } from '../../codecs/webp/enc/webp_enc.js';
// AVIF
import type { AVIFModule as AVIFEncodeModule } from '../../codecs/avif/enc/avif_enc';
import avifEnc from '../../codecs/avif/enc/avif_node_enc.js';
import avifEncWasm from 'asset-url:../../codecs/avif/enc/avif_node_enc.wasm';
import avifEncMt from '../../codecs/avif/enc/avif_node_enc_mt.js';
@@ -60,18 +80,23 @@ import avifEncMtWorker from 'chunk-url:../../codecs/avif/enc/avif_node_enc_mt.wo
import avifEncMtWasm from 'asset-url:../../codecs/avif/enc/avif_node_enc_mt.wasm';
import avifDec from '../../codecs/avif/dec/avif_node_dec.js';
import avifDecWasm from 'asset-url:../../codecs/avif/dec/avif_node_dec.wasm';
import type { EncodeOptions as AvifEncodeOptions } from '../../codecs/avif/enc/avif_enc.js';
// JXL
import type { JXLModule as JXLEncodeModule } from '../../codecs/jxl/enc/jxl_enc';
import jxlEnc from '../../codecs/jxl/enc/jxl_node_enc.js';
import jxlEncWasm from 'asset-url:../../codecs/jxl/enc/jxl_node_enc.wasm';
import jxlDec from '../../codecs/jxl/dec/jxl_node_dec.js';
import jxlDecWasm from 'asset-url:../../codecs/jxl/dec/jxl_node_dec.wasm';
import type { EncodeOptions as JxlEncodeOptions } from '../../codecs/jxl/enc/jxl_enc.js';
// WP2
import type { WP2Module as WP2EncodeModule } from '../../codecs/wp2/enc/wp2_enc';
import wp2Enc from '../../codecs/wp2/enc/wp2_node_enc.js';
import wp2EncWasm from 'asset-url:../../codecs/wp2/enc/wp2_node_enc.wasm';
import wp2Dec from '../../codecs/wp2/dec/wp2_node_dec.js';
import wp2DecWasm from 'asset-url:../../codecs/wp2/dec/wp2_node_dec.wasm';
import type { EncodeOptions as WP2EncodeOptions } from '../../codecs/wp2/enc/wp2_enc.js';
// PNG
import * as pngEncDec from '../../codecs/png/pkg/squoosh_png.js';
@@ -84,6 +109,9 @@ const pngEncDecPromise = pngEncDec.default(
import * as oxipng from '../../codecs/oxipng/pkg/squoosh_oxipng.js';
import oxipngWasm from 'asset-url:../../codecs/oxipng/pkg/squoosh_oxipng_bg.wasm';
const oxipngPromise = oxipng.default(fsp.readFile(pathify(oxipngWasm)));
interface OxiPngEncodeOptions {
level: number;
}
// Resize
import * as resize from '../../codecs/resize/pkg/squoosh_resize.js';
@@ -160,13 +188,7 @@ export const preprocessors = {
buffer: Uint8Array,
input_width: number,
input_height: number,
{
width,
height,
method,
premultiply,
linearRGB,
}: ResizeInstantiateOptions,
{ width, height, method, premultiply, linearRGB }: ResizeOptions,
) => {
({ width, height } = resizeWithAspect({
input_width,
@@ -207,7 +229,7 @@ export const preprocessors = {
buffer: Uint8Array,
width: number,
height: number,
{ numColors, dither }: { numColors: number; dither: number },
{ numColors, dither }: QuantOptions,
) =>
new ImageData(
imageQuant.quantize(buffer, width, height, numColors, dither),
@@ -228,7 +250,7 @@ export const preprocessors = {
buffer: Uint8Array,
width: number,
height: number,
{ numRotations }: { numRotations: number },
{ numRotations }: RotateOptions,
) => {
const degrees = (numRotations * 90) % 360;
const sameDimensions = degrees == 0 || degrees == 180;
@@ -257,15 +279,20 @@ export const preprocessors = {
numRotations: 0,
},
},
};
} as const;
export const codecs = {
mozjpeg: {
name: 'MozJPEG',
extension: 'jpg',
detectors: [/^\xFF\xD8\xFF/],
dec: () => instantiateEmscriptenWasm(mozDec, mozDecWasm),
enc: () => instantiateEmscriptenWasm(mozEnc, mozEncWasm),
dec: () =>
instantiateEmscriptenWasm(mozDec as DecodeModuleFactory, mozDecWasm),
enc: () =>
instantiateEmscriptenWasm(
mozEnc as EmscriptenWasm.ModuleFactory<MozJPEGEncodeModule>,
mozEncWasm,
),
defaultEncoderOptions: {
quality: 75,
baseline: false,
@@ -294,8 +321,13 @@ export const codecs = {
name: 'WebP',
extension: 'webp',
detectors: [/^RIFF....WEBPVP8[LX ]/s],
dec: () => instantiateEmscriptenWasm(webpDec, webpDecWasm),
enc: () => instantiateEmscriptenWasm(webpEnc, webpEncWasm),
dec: () =>
instantiateEmscriptenWasm(webpDec as DecodeModuleFactory, webpDecWasm),
enc: () =>
instantiateEmscriptenWasm(
webpEnc as EmscriptenWasm.ModuleFactory<WebPEncodeModule>,
webpEncWasm,
),
defaultEncoderOptions: {
quality: 75,
target_size: 0,
@@ -335,16 +367,20 @@ export const codecs = {
name: 'AVIF',
extension: 'avif',
detectors: [/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/],
dec: () => instantiateEmscriptenWasm(avifDec, avifDecWasm),
dec: () =>
instantiateEmscriptenWasm(avifDec as DecodeModuleFactory, avifDecWasm),
enc: async () => {
if (await threads()) {
return instantiateEmscriptenWasm(
avifEncMt,
avifEncMt as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
avifEncMtWasm,
avifEncMtWorker,
);
}
return instantiateEmscriptenWasm(avifEnc, avifEncWasm);
return instantiateEmscriptenWasm(
avifEnc as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
avifEncWasm,
);
},
defaultEncoderOptions: {
cqLevel: 33,
@@ -368,16 +404,22 @@ export const codecs = {
name: 'JPEG-XL',
extension: 'jxl',
detectors: [/^\xff\x0a/],
dec: () => instantiateEmscriptenWasm(jxlDec, jxlDecWasm),
enc: () => instantiateEmscriptenWasm(jxlEnc, jxlEncWasm),
dec: () =>
instantiateEmscriptenWasm(jxlDec as DecodeModuleFactory, jxlDecWasm),
enc: () =>
instantiateEmscriptenWasm(
jxlEnc as EmscriptenWasm.ModuleFactory<JXLEncodeModule>,
jxlEncWasm,
),
defaultEncoderOptions: {
speed: 4,
effort: 1,
quality: 75,
progressive: false,
epf: -1,
nearLossless: 0,
lossyPalette: false,
decodingSpeedTier: 0,
photonNoiseIso: 0,
lossyModular: false,
},
autoOptimize: {
option: 'quality',
@@ -389,8 +431,13 @@ export const codecs = {
name: 'WebP2',
extension: 'wp2',
detectors: [/^\xF4\xFF\x6F/],
dec: () => instantiateEmscriptenWasm(wp2Dec, wp2DecWasm),
enc: () => instantiateEmscriptenWasm(wp2Enc, wp2EncWasm),
dec: () =>
instantiateEmscriptenWasm(wp2Dec as DecodeModuleFactory, wp2DecWasm),
enc: () =>
instantiateEmscriptenWasm(
wp2Enc as EmscriptenWasm.ModuleFactory<WP2EncodeModule>,
wp2EncWasm,
),
defaultEncoderOptions: {
quality: 75,
alpha_quality: 75,
@@ -421,7 +468,7 @@ export const codecs = {
await oxipngPromise;
return {
encode: (
buffer: Uint8Array,
buffer: Uint8ClampedArray | ArrayBuffer,
width: number,
height: number,
opts: { level: number },
@@ -444,4 +491,13 @@ export const codecs = {
max: 1,
},
},
} as const;
export {
MozJPEGEncodeOptions,
WebPEncodeOptions,
AvifEncodeOptions,
JxlEncodeOptions,
WP2EncodeOptions,
OxiPngEncodeOptions,
};

View File

@@ -19,7 +19,7 @@ export function instantiateEmscriptenWasm<T extends EmscriptenWasm.Module>(
// 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;
if (requestPath.endsWith('.worker.js')) return pathify(workerJS);
return requestPath;
},
});

View File

@@ -1,224 +0,0 @@
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';
export { ImagePool, encoders, preprocessors };
async function decodeFile({ file }) {
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 instanceof Buffer) {
buffer = file;
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];
if (!key) {
throw Error(`${file} has an unsupported format`);
}
const rgba = (await encoders[key].dec()).decode(new Uint8Array(buffer));
return {
bitmap: rgba,
size: buffer.length,
};
}
async function preprocessImage({ preprocessorName, options, image }) {
const preprocessor = await preprocessors[preprocessorName].instantiate();
image.bitmap = await preprocessor(
image.bitmap.data,
image.bitmap.width,
image.bitmap.height,
options,
);
return image;
}
async function encodeImage({
bitmap: bitmapIn,
encName,
encConfig,
optimizerButteraugliTarget,
maxOptimizerRounds,
}) {
let binary;
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, quality) =>
encoder.encode(
bitmapIn.data,
bitmapIn.width,
bitmapIn.height,
Object.assign({}, encoders[encName].defaultEncoderOptions, {
[optionToOptimize]: quality,
}),
);
const decode = (binary) => decoder.decode(binary);
const { binary: optimizedBinary, quality } = await autoOptimize(
bitmapIn,
encode,
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 {
binary = encoder.encode(
bitmapIn.data.buffer,
bitmapIn.width,
bitmapIn.height,
encConfig,
);
}
return {
optionsUsed,
binary,
extension: encoders[encName].extension,
size: binary.length,
};
}
// both decoding and encoding go through the worker pool
function handleJob(params) {
const { operation } = params;
switch (operation) {
case 'encode':
return encodeImage(params);
case 'decode':
return decodeFile(params);
case 'preprocess':
return preprocessImage(params);
default:
throw Error(`Invalid job "${operation}"`);
}
}
/**
* Represents an ingested image.
*/
class Image {
constructor(workerPool, file) {
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 preprocessorOptions = Object.assign(
{},
preprocessors[name].defaultOptions,
options,
);
this.decoded = this.workerPool.dispatchJob({
operation: 'preprocess',
preprocessorName: name,
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<undefined>} - A promise that resolves when the image has been encoded with all the specified encoders.
*/
async encode(encodeOptions = {}) {
const { bitmap } = await this.decoded;
for (const [encName, options] of Object.entries(encodeOptions)) {
if (!Object.keys(encoders).includes(encName)) {
continue;
}
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 {
/**
* 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) {
this.workerPool = new WorkerPool(threads || cpus().length, __filename);
}
/**
* Ingest an image into the image pool.
* @param {string | Buffer | URL | object} image - The image or path to the image that should be ingested and decoded.
* @returns {Image} - A custom class reference to the decoded image.
*/
ingestImage(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<undefined>} - A promise that resolves when the underlying pipeline has closed.
*/
async close() {
await this.workerPool.join();
}
}
if (!isMainThread) {
WorkerPool.useThisThreadAsWorker(handleJob);
}

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

@@ -0,0 +1,308 @@
import { isMainThread } from 'worker_threads';
import {
AvifEncodeOptions,
codecs as encoders,
JxlEncodeOptions,
MozJPEGEncodeOptions,
OxiPngEncodeOptions,
preprocessors,
QuantOptions,
ResizeOptions,
RotateOptions,
WebPEncodeOptions,
WP2EncodeOptions,
} from './codecs.js';
import WorkerPool from './worker_pool.js';
import { autoOptimize } from './auto-optimizer.js';
import type ImageData from './image_data';
export { ImagePool, encoders, preprocessors };
type EncoderKey = keyof typeof encoders;
type PreprocessorKey = keyof typeof preprocessors;
type PreprocessOptions = {
resize?: Partial<Omit<ResizeOptions, 'width' | 'height'>> &
(Pick<ResizeOptions, 'width'> | Pick<ResizeOptions, 'height'>);
quant?: Partial<QuantOptions>;
rotate?: Partial<RotateOptions>;
};
type EncodeResult = {
optionsUsed: object;
binary: Uint8Array;
extension: string;
size: number;
};
type EncoderOptions = {
mozjpeg?: Partial<MozJPEGEncodeOptions>;
webp?: Partial<WebPEncodeOptions>;
avif?: Partial<AvifEncodeOptions>;
jxl?: Partial<JxlEncodeOptions>;
wp2?: Partial<WP2EncodeOptions>;
oxipng?: Partial<OxiPngEncodeOptions>;
};
async function decodeFile({
file,
}: {
file: ArrayBuffer | ArrayLike<number>;
}): Promise<{ bitmap: ImageData; size: number }> {
const array = new Uint8Array(file);
const firstChunk = array.slice(0, 16);
const firstChunkString = Array.from(firstChunk)
.map((v) => String.fromCodePoint(v))
.join('');
const key = Object.entries(encoders).find(([_name, { detectors }]) =>
detectors.some((detector) => detector.exec(firstChunkString)),
)?.[0] as EncoderKey | undefined;
if (!key) {
throw Error(`File has an unsupported format`);
}
const encoder = encoders[key];
const mod = await encoder.dec();
const rgba = mod.decode(array);
return {
bitmap: rgba,
size: array.length,
};
}
async function preprocessImage({
preprocessorName,
options,
image,
}: {
preprocessorName: PreprocessorKey;
options: any;
image: { bitmap: ImageData };
}) {
const preprocessor = await preprocessors[preprocessorName].instantiate();
image.bitmap = await preprocessor(
Uint8Array.from(image.bitmap.data),
image.bitmap.width,
image.bitmap.height,
options,
);
return image;
}
async function encodeImage({
bitmap: bitmapIn,
encName,
encConfig,
optimizerButteraugliTarget,
maxOptimizerRounds,
}: {
bitmap: ImageData;
encName: EncoderKey;
encConfig: any;
optimizerButteraugliTarget: number;
maxOptimizerRounds: number;
}): Promise<EncodeResult> {
let binary: Uint8Array;
let optionsUsed = encConfig;
const encoder = await encoders[encName].enc();
if (encConfig === 'auto') {
const optionToOptimize = encoders[encName].autoOptimize.option;
const decoder = await encoders[encName].dec();
const encode = (bitmapIn: ImageData, quality: number) =>
encoder.encode(
bitmapIn.data,
bitmapIn.width,
bitmapIn.height,
Object.assign({}, encoders[encName].defaultEncoderOptions as any, {
[optionToOptimize]: quality,
}),
);
const decode = (binary: Uint8Array) => decoder.decode(binary);
const nonNullEncode = (bitmap: ImageData, quality: number): Uint8Array => {
const result = encode(bitmap, quality);
if (!result) {
throw new Error('There was an error while encoding');
}
return result;
};
const { binary: optimizedBinary, quality } = await autoOptimize(
bitmapIn,
nonNullEncode,
decode,
{
min: encoders[encName].autoOptimize.min,
max: encoders[encName].autoOptimize.max,
butteraugliDistanceGoal: optimizerButteraugliTarget,
maxRounds: maxOptimizerRounds,
},
);
binary = optimizedBinary;
optionsUsed = {
// 5 significant digits is enough
[optionToOptimize]: Math.round(quality * 10000) / 10000,
};
} else {
const result = encoder.encode(
bitmapIn.data.buffer,
bitmapIn.width,
bitmapIn.height,
encConfig,
);
if (!result) {
throw new Error('There was an error while encoding');
}
binary = result;
}
return {
optionsUsed,
binary,
extension: encoders[encName].extension,
size: binary.length,
};
}
type EncodeParams = { operation: 'encode' } & Parameters<typeof encodeImage>[0];
type DecodeParams = { operation: 'decode' } & Parameters<typeof decodeFile>[0];
type PreprocessParams = { operation: 'preprocess' } & Parameters<
typeof preprocessImage
>[0];
type JobMessage = EncodeParams | DecodeParams | PreprocessParams;
function handleJob(params: JobMessage) {
switch (params.operation) {
case 'encode':
return encodeImage(params);
case 'decode':
return decodeFile(params);
case 'preprocess':
return preprocessImage(params);
default:
throw Error(`Invalid job "${(params as any).operation}"`);
}
}
/**
* Represents an ingested image.
*/
class Image {
public file: ArrayBuffer | ArrayLike<number>;
public workerPool: WorkerPool<JobMessage, any>;
public decoded: Promise<{ bitmap: ImageData }>;
public encodedWith: { [key in EncoderKey]?: EncodeResult };
constructor(
workerPool: WorkerPool<JobMessage, any>,
file: ArrayBuffer | ArrayLike<number>,
) {
this.file = file;
this.workerPool = workerPool;
this.decoded = workerPool.dispatchJob({ operation: 'decode', file });
this.encodedWith = {};
}
/**
* Define one or several preprocessors to use on the image.
* @param {PreprocessOptions} preprocessOptions - An object with preprocessors to use, and their settings.
* @returns {Promise<undefined>} - A promise that resolves when all preprocessors have completed their work.
*/
async preprocess(preprocessOptions: PreprocessOptions = {}) {
for (const [name, options] of Object.entries(preprocessOptions)) {
if (!Object.keys(preprocessors).includes(name)) {
throw Error(`Invalid preprocessor "${name}"`);
}
const preprocessorName = name as PreprocessorKey;
const preprocessorOptions = Object.assign(
{},
preprocessors[preprocessorName].defaultOptions,
options,
);
this.decoded = this.workerPool.dispatchJob({
operation: 'preprocess',
preprocessorName,
image: await this.decoded,
options: preprocessorOptions,
});
await this.decoded;
}
}
/**
* Define one or several encoders to use on the image.
* @param {object} encodeOptions - An object with encoders to use, and their settings.
* @returns {Promise<{ [key in keyof T]: EncodeResult }>} - A promise that resolves when the image has been encoded with all the specified encoders.
*/
async encode<T extends EncoderOptions>(
encodeOptions: {
optimizerButteraugliTarget?: number;
maxOptimizerRounds?: number;
} & T,
): Promise<{ [key in keyof T]: EncodeResult }> {
const { bitmap } = await this.decoded;
const setEncodedWithPromises = [];
for (const [name, options] of Object.entries(encodeOptions)) {
if (!Object.keys(encoders).includes(name)) {
continue;
}
const encName = name as EncoderKey;
const encRef = encoders[encName];
const encConfig =
typeof options === 'string'
? options
: Object.assign({}, encRef.defaultEncoderOptions, options);
setEncodedWithPromises.push(
this.workerPool
.dispatchJob({
operation: 'encode',
bitmap,
encName,
encConfig,
optimizerButteraugliTarget: Number(
encodeOptions.optimizerButteraugliTarget ?? 1.4,
),
maxOptimizerRounds: Number(encodeOptions.maxOptimizerRounds ?? 6),
})
.then((encodeResult) => {
this.encodedWith[encName] = encodeResult;
}),
);
}
await Promise.all(setEncodedWithPromises);
return this.encodedWith as { [key in keyof T]: EncodeResult };
}
}
/**
* A pool where images can be ingested and squooshed.
*/
class ImagePool {
public workerPool: WorkerPool<JobMessage, any>;
/**
* Create a new pool.
* @param {number} [threads] - Number of concurrent image processes to run in the pool.
*/
constructor(threads: number) {
this.workerPool = new WorkerPool(threads, __filename);
}
/**
* Ingest an image into the image pool.
* @param {ArrayBuffer | ArrayLike<number>} file - The image that should be ingested and decoded.
* @returns {Image} - A custom class reference to the decoded image.
*/
ingestImage(file: ArrayBuffer | ArrayLike<number>): Image {
return new Image(this.workerPool, file);
}
/**
* Closes the underlying image processing pipeline. The already processed images will still be there, but no new processing can start.
* @returns {Promise<void>} - A promise that resolves when the underlying pipeline has closed.
*/
async close(): Promise<void> {
await this.workerPool.join();
}
}
if (!isMainThread) {
WorkerPool.useThisThreadAsWorker(handleJob);
}

View File

@@ -7,26 +7,19 @@ function uuid() {
).join('');
}
function jobPromise(worker, msg) {
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);
});
});
interface Job<I> {
msg: I;
resolve: Function;
reject: Function;
}
export default class WorkerPool {
constructor(numWorkers, workerFile) {
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();
@@ -48,9 +41,14 @@ export default class WorkerPool {
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();
jobPromise(worker, msg)
this.jobPromise(worker, msg)
.then((result) => resolve(result))
.catch((reason) => reject(reason))
.finally(() => {
@@ -66,6 +64,10 @@ export default class WorkerPool {
const reader = this.workerQueue.readable.getReader();
const { value } = await reader.read();
reader.releaseLock();
if (!value) {
throw new Error('No worker left');
}
return value;
}
@@ -82,7 +84,7 @@ export default class WorkerPool {
await this.done;
}
dispatchJob(msg) {
dispatchJob(msg: I): Promise<O> {
return new Promise((resolve, reject) => {
const writer = this.jobQueue.writable.getWriter();
writer.write({ msg, resolve, reject });
@@ -90,14 +92,32 @@ export default class WorkerPool {
});
}
static useThisThreadAsWorker(cb) {
parentPort.on('message', async (data) => {
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 });
parentPort!.postMessage({ result, id });
} catch (e) {
parentPort.postMessage({ error: e.message, id });
parentPort!.postMessage({ error: e.message, id });
}
});
}

View File

@@ -3,7 +3,8 @@
"compilerOptions": {
"lib": ["esnext"],
"types": ["node"],
"allowJs": true
"allowJs": true,
"declaration": true
},
"include": ["src/**/*", "../codecs/**/*"]
}

10
missing-types.d.ts vendored
View File

@@ -44,6 +44,16 @@ declare module 'data-url:*' {
export default url;
}
declare module 'data-url-text:*' {
const url: string;
export default url;
}
declare module 'service-worker:*' {
const url: string;
export default url;
}
declare var ga: {
(...args: any[]): void;
q: any[];

368
package-lock.json generated
View File

@@ -16,15 +16,15 @@
"@rollup/plugin-replace": "^2.3.4",
"@surma/rollup-plugin-off-main-thread": "^2.2.2",
"@types/dedent": "^0.7.0",
"@types/mime-types": "^2.1.0",
"@types/node": "^14.14.7",
"@types/mime-types": "^2.1.1",
"@types/node": "^16.11.1",
"@web/rollup-plugin-import-meta-assets": "^1.0.6",
"comlink": "^4.3.0",
"cssnano": "^4.1.10",
"dedent": "^0.7.0",
"del": "^5.1.0",
"file-drop-element": "^1.0.1",
"husky": "^4.3.0",
"husky": "^7.0.2",
"idb-keyval": "^3.2.0",
"image-size": "^0.9.3",
"linkstate": "^2.0.0",
@@ -32,7 +32,7 @@
"lodash.camelcase": "^4.3.0",
"mime-types": "^2.1.28",
"npm-run-all": "^4.1.5",
"pointer-tracker": "^2.4.0",
"pointer-tracker": "^2.5.3",
"postcss": "^7.0.35",
"postcss-modules": "^3.2.2",
"postcss-nested": "^4.2.3",
@@ -40,14 +40,28 @@
"postcss-url": "^8.0.0",
"preact": "^10.5.5",
"preact-render-to-string": "^5.1.11",
"prettier": "^2.1.2",
"prettier": "^2.4.1",
"rollup": "^2.38.0",
"rollup-plugin-terser": "^7.0.2",
"serve": "^11.3.2",
"typescript": "^4.1.3",
"typescript": "^4.4.4",
"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": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
@@ -301,9 +315,9 @@
}
},
"node_modules/@types/mime-types": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz",
"integrity": "sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM=",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz",
"integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==",
"dev": true
},
"node_modules/@types/minimatch": {
@@ -313,9 +327,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "14.14.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.7.tgz",
"integrity": "sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==",
"version": "16.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==",
"dev": true
},
"node_modules/@types/parse-json": {
@@ -852,12 +866,6 @@
"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": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -1162,12 +1170,6 @@
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"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": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -2270,31 +2272,6 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -2471,28 +2448,18 @@
}
},
"node_modules/husky": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/husky/-/husky-4.3.0.tgz",
"integrity": "sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/husky/-/husky-7.0.2.tgz",
"integrity": "sha512-8yKEWNX4z2YsofXAMT7KvA1g8p+GxtB1ffV8XtpAEGuXNAbCV5wdNKH+qTpw8SM9fh4aMPDR+yQuKfgnreyZlg==",
"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": {
"husky-run": "bin/run.js",
"husky-upgrade": "lib/upgrader/bin.js"
"husky": "lib/bin.js"
},
"engines": {
"node": ">=10"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/icss-replace-symbols": {
@@ -3150,18 +3117,6 @@
"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": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@@ -3675,15 +3630,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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
@@ -3693,30 +3639,6 @@
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
@@ -3729,15 +3651,6 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -3765,15 +3678,6 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -3849,18 +3753,6 @@
"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": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
@@ -3871,9 +3763,9 @@
}
},
"node_modules/pointer-tracker": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.4.0.tgz",
"integrity": "sha512-pWI2tpaM/XNtc9mUTv42Rmjf6mkHvE8LT5DDEq0G7baPNhxNM9E3CepubPplSoSLk9E5bwQrAMyDcPVmJyTW4g==",
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.5.3.tgz",
"integrity": "sha512-LiJUeIbzk4dXq678YeyrZ++mdY17q4n/2sBHfU9wIuvmSzdiPgMvmvWN2g8mY4J7YwYOIrqrZUWP/MfFHVwYtg==",
"dev": true
},
"node_modules/postcss": {
@@ -7515,9 +7407,9 @@
}
},
"node_modules/prettier": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz",
"integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz",
"integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
@@ -7794,15 +7686,6 @@
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
@@ -8588,9 +8471,9 @@
}
},
"node_modules/typescript": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
"integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -8694,12 +8577,6 @@
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz",
@@ -8976,9 +8853,9 @@
}
},
"@surma/rollup-plugin-off-main-thread": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.1.tgz",
"integrity": "sha512-7OU8wfyv18YPWVmecg2/0Jh+pm3lQbvPhIWHd1YQpoxPKPW/vsDNGBaCnMKsZbz29RjgCoXKugAjyagPncgdEw==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.2.tgz",
"integrity": "sha512-dOD6nGZ79RmWKDRQuC7SOGXMvDkkLwBogu+epfVFMKiy2kOUtLZkb8wV/ettuMt37YJAJKYCKUmxSbZL2LkUQg==",
"dev": true,
"requires": {
"ejs": "^3.1.6",
@@ -9026,9 +8903,9 @@
}
},
"@types/mime-types": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz",
"integrity": "sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM=",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz",
"integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==",
"dev": true
},
"@types/minimatch": {
@@ -9038,9 +8915,9 @@
"dev": true
},
"@types/node": {
"version": "14.14.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.7.tgz",
"integrity": "sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==",
"version": "16.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==",
"dev": true
},
"@types/parse-json": {
@@ -9480,12 +9357,6 @@
"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": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -9739,12 +9610,6 @@
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"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": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -10662,25 +10527,6 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -10827,22 +10673,10 @@
"dev": true
},
"husky": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/husky/-/husky-4.3.0.tgz",
"integrity": "sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA==",
"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"
}
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/husky/-/husky-7.0.2.tgz",
"integrity": "sha512-8yKEWNX4z2YsofXAMT7KvA1g8p+GxtB1ffV8XtpAEGuXNAbCV5wdNKH+qTpw8SM9fh4aMPDR+yQuKfgnreyZlg==",
"dev": true
},
"icss-replace-symbols": {
"version": "1.1.0",
@@ -11380,15 +11214,6 @@
"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": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@@ -11801,36 +11626,12 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
@@ -11840,12 +11641,6 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -11867,12 +11662,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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -11927,15 +11716,6 @@
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"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": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
@@ -11946,9 +11726,9 @@
}
},
"pointer-tracker": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.4.0.tgz",
"integrity": "sha512-pWI2tpaM/XNtc9mUTv42Rmjf6mkHvE8LT5DDEq0G7baPNhxNM9E3CepubPplSoSLk9E5bwQrAMyDcPVmJyTW4g==",
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.5.3.tgz",
"integrity": "sha512-LiJUeIbzk4dXq678YeyrZ++mdY17q4n/2sBHfU9wIuvmSzdiPgMvmvWN2g8mY4J7YwYOIrqrZUWP/MfFHVwYtg==",
"dev": true
},
"postcss": {
@@ -15015,9 +14795,9 @@
}
},
"prettier": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz",
"integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz",
"integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==",
"dev": true
},
"pretty-format": {
@@ -15243,12 +15023,6 @@
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
@@ -15903,9 +15677,9 @@
"dev": true
},
"typescript": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
"integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
"dev": true
},
"uniq": {
@@ -15980,9 +15754,9 @@
"dev": true
},
"wasm-feature-detect": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.9.tgz",
"integrity": "sha512-2E9/gtLVLpv2wnZDyYv8WY2dR9gHbmyv5uhZsnOcMSzqc78aGZpKQORPNcnrPwAU23nFUo7GAwKuoTAWRgsJ7Q=="
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz",
"integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w=="
},
"which": {
"version": "2.0.2",
@@ -15993,12 +15767,6 @@
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz",

View File

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

View File

@@ -18,6 +18,7 @@ import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import OMT from '@surma/rollup-plugin-off-main-thread';
import replace from '@rollup/plugin-replace';
import { importMetaAssets } from '@web/rollup-plugin-import-meta-assets';
import simpleTS from './lib/simple-ts';
import clientBundlePlugin from './lib/client-bundle-plugin';
@@ -31,11 +32,11 @@ import featurePlugin from './lib/feature-plugin';
import initialCssPlugin from './lib/initial-css-plugin';
import serviceWorkerPlugin from './lib/sw-plugin';
import dataURLPlugin from './lib/data-url-plugin';
import entryDataPlugin, { fileNameToURL } from './lib/entry-data-plugin';
import dedent from 'dedent';
import entryDataPlugin from './lib/entry-data-plugin';
function resolveFileUrl({ fileName }) {
return JSON.stringify(fileName.replace(/^static\//, '/'));
return JSON.stringify(fileNameToURL(fileName));
}
function resolveImportMetaUrlInStaticBuild(property, { moduleId }) {
@@ -70,7 +71,7 @@ export default async function ({ watch }) {
await del('.tmp/build');
const isProduction = !watch && !process.env.DEBUG;
const isProduction = !watch;
const tsPluginInstance = simpleTS('.', {
watch,
@@ -118,6 +119,7 @@ export default async function ({ watch }) {
plugins: [
{ resolveFileUrl },
OMT({ loader: await omtLoaderPromise }),
importMetaAssets(),
serviceWorkerPlugin({
output: 'static/serviceworker.js',
}),

View File

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

View File

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

View File

@@ -20,3 +20,7 @@
.checked {
fill: var(--main-theme-color);
}
.disabled {
fill: var(--dark-gray);
}

View File

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

View File

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

View File

@@ -40,9 +40,8 @@ type PartialButNotUndefined<T> = {
[P in keyof T]: T[P];
};
const supportedEncoderMapP: Promise<PartialButNotUndefined<
typeof encoderMap
>> = (async () => {
const supportedEncoderMapP: Promise<PartialButNotUndefined<typeof encoderMap>> =
(async () => {
const supportedEncoderMap: PartialButNotUndefined<typeof encoderMap> = {
...encoderMap,
};
@@ -57,7 +56,7 @@ const supportedEncoderMapP: Promise<PartialButNotUndefined<
);
return supportedEncoderMap;
})();
})();
export default class Options extends Component<Props, State> {
state: State = {

View File

@@ -1,6 +1,7 @@
.options-scroller {
--horizontal-padding: 15px;
border-radius: var(--scroller-radius);
overflow: hidden;
/* At smaller widths, the multi-panel handles the scrolling */
@media (min-width: 600px) {

View File

@@ -1,5 +1,6 @@
import PointerTracker, { Pointer } from 'pointer-tracker';
import 'add-css:./styles.css';
import { isSafari } from 'client/lazy-app/util';
interface Point {
clientX: number;
@@ -81,6 +82,7 @@ function createPoint(): SVGPoint {
}
const MIN_SCALE = 0.01;
const MAX_SCALE = 100000;
export default class PinchZoom extends HTMLElement {
// The element that we'll transform.
@@ -104,14 +106,23 @@ export default class PinchZoom extends HTMLElement {
const pointerTracker: PointerTracker = new PointerTracker(this, {
start: (pointer, event) => {
// We only want to track 2 pointers at most
if (pointerTracker.currentPointers.length === 2 || !this._positioningEl)
if (
pointerTracker.currentPointers.length === 2 ||
!this._positioningEl
) {
return false;
}
event.preventDefault();
return true;
},
move: (previousPointers) => {
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));
@@ -244,6 +255,9 @@ export default class PinchZoom extends HTMLElement {
// Avoid scaling to zero
if (scale < MIN_SCALE) return;
// Avoid scaling to very large values
if (scale > MAX_SCALE) return;
// Return if there's no change
if (scale === this.scale && x === this.x && y === this.y) return;
@@ -296,9 +310,13 @@ export default class PinchZoom extends HTMLElement {
deltaY *= 15;
}
const zoomingOut = deltaY > 0;
// ctrlKey is true when pinch-zooming on a trackpad.
const divisor = ctrlKey ? 100 : 300;
const scaleDiff = 1 - deltaY / divisor;
// when zooming out, invert the delta and the ratio to keep zoom stable
const ratio = 1 - (zoomingOut ? -deltaY : deltaY) / divisor;
const scaleDiff = zoomingOut ? 1 / ratio : ratio;
this._applyChange({
scaleDiff,

View File

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

View File

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

View File

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

View File

@@ -114,9 +114,9 @@ async function decodeImage(
}
}
// Otherwise fall through and try built-in decoding for a laugh.
return await builtinDecode(signal, blob, mimeType);
return await builtinDecode(signal, blob);
} catch (err) {
if (err.name === 'AbortError') throw err;
if (err instanceof Error && err.name === 'AbortError') throw err;
console.log(err);
throw Error("Couldn't decode image");
}
@@ -481,7 +481,7 @@ export default class Compress extends Component<Props, State> {
open('https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli');
}
} catch (e) {
this.props.showSnack(e);
this.props.showSnack(String(e));
}
};
@@ -640,7 +640,7 @@ export default class Compress extends Component<Props, State> {
return { sides };
});
} catch (err) {
if (err.name === 'AbortError') return;
if (err instanceof Error && err.name === 'AbortError') return;
this.props.showSnack(`Source decoding error: ${err}`);
throw err;
}
@@ -698,7 +698,7 @@ export default class Compress extends Component<Props, State> {
return newState;
});
} catch (err) {
if (err.name === 'AbortError') return;
if (err instanceof Error && err.name === 'AbortError') return;
this.setState({ loading: false });
this.props.showSnack(`Preprocessing error: ${err}`);
throw err;
@@ -822,7 +822,7 @@ export default class Compress extends Component<Props, State> {
this.activeSideJobs[sideIndex] = undefined;
} catch (err) {
if (err.name === 'AbortError') return;
if (err instanceof Error && err.name === 'AbortError') return;
this.setState((currentState) => {
const sides = cleanMerge(currentState.sides, sideIndex, {
loading: false,

View File

@@ -11,6 +11,25 @@ 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) => (
<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" />

View File

@@ -63,35 +63,17 @@ interface DrawableToImageDataOptions {
sh?: number;
}
function getWidth(
drawable: ImageBitmap | HTMLImageElement | VideoFrame,
): number {
if ('displayWidth' in drawable) {
return drawable.displayWidth;
}
return drawable.width;
}
function getHeight(
drawable: ImageBitmap | HTMLImageElement | VideoFrame,
): number {
if ('displayHeight' in drawable) {
return drawable.displayHeight;
}
return drawable.height;
}
export function drawableToImageData(
drawable: ImageBitmap | HTMLImageElement | VideoFrame,
drawable: ImageBitmap | HTMLImageElement,
opts: DrawableToImageDataOptions = {},
): ImageData {
const {
width = getWidth(drawable),
height = getHeight(drawable),
width = drawable.width,
height = drawable.height,
sx = 0,
sy = 0,
sw = getWidth(drawable),
sh = getHeight(drawable),
sw = drawable.width,
sh = drawable.height,
} = opts;
// Make canvas same size as image

View File

@@ -10,10 +10,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as WebCodecs from '../util/web-codecs';
import { drawableToImageData } from './canvas';
/** If render engine is Safari */
export const isSafari =
/Safari\//.test(navigator.userAgent) &&
!/Chrom(e|ium)\//.test(navigator.userAgent);
/**
* Compare two objects, returning a boolean indicating if
* they have the same properties and strictly equal values.
@@ -134,15 +137,7 @@ export async function blobToImg(blob: Blob): Promise<HTMLImageElement> {
export async function builtinDecode(
signal: AbortSignal,
blob: Blob,
mimeType: string,
): Promise<ImageData> {
// If WebCodecs are supported, use that.
if (await WebCodecs.isTypeSupported(mimeType)) {
assertSignal(signal);
try {
return await abortable(signal, WebCodecs.decode(blob, mimeType));
} catch (e) {}
}
assertSignal(signal);
// Prefer createImageBitmap as it's the off-thread option for Firefox.

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