forked from external-repos/squoosh
Compare commits
67 Commits
prototype-
...
libsquoosh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e04053e4c | ||
|
|
255dfa434a | ||
|
|
0bef05bcd4 | ||
|
|
00cfdafdf3 | ||
|
|
92f52319da | ||
|
|
023304803f | ||
|
|
6b08cd2355 | ||
|
|
db1a5138e6 | ||
|
|
a547491146 | ||
|
|
6e427f9208 | ||
|
|
0d35fbd349 | ||
|
|
4e901c714c | ||
|
|
b74788e036 | ||
|
|
14c3d190e9 | ||
|
|
eeaa19589e | ||
|
|
35b8c56f1a | ||
|
|
816d1f92fd | ||
|
|
4091f2efec | ||
|
|
821d14c6ab | ||
|
|
a72ca46531 | ||
|
|
fafcf97f0c | ||
|
|
d526877147 | ||
|
|
0ed7ef842f | ||
|
|
de4eb9c8f7 | ||
|
|
16a53caa48 | ||
|
|
3d4c62fede | ||
|
|
ce7be359c0 | ||
|
|
3f2dd66726 | ||
|
|
9198f748b8 | ||
|
|
a726adf0e8 | ||
|
|
04580b0bcb | ||
|
|
a040c47047 | ||
|
|
2427763a14 | ||
|
|
c582c54922 | ||
|
|
9ae27c1887 | ||
|
|
bf95eb39c2 | ||
|
|
011c0346c1 | ||
|
|
9b36c3f9af | ||
|
|
490fe2aace | ||
|
|
48efb4ddeb | ||
|
|
0977bd94d3 | ||
|
|
d5f12a8c61 | ||
|
|
fb867dcdaa | ||
|
|
f038c1bd7d | ||
|
|
b37cc0784f | ||
|
|
ff40000473 | ||
|
|
7232524f4d | ||
|
|
bf683cdf59 | ||
|
|
f848a9384e | ||
|
|
d4d0db6c49 | ||
|
|
0719abff27 | ||
|
|
2c561687af | ||
|
|
cd20082e5d | ||
|
|
8262c79bb6 | ||
|
|
ef176d7565 | ||
|
|
3b0b7dbdb1 | ||
|
|
cc9a887386 | ||
|
|
71ca1ab0ca | ||
|
|
f5d9535fd2 | ||
|
|
cea6a61366 | ||
|
|
192cfc62ee | ||
|
|
fe21322b2b | ||
|
|
d953822d19 | ||
|
|
021b082884 | ||
|
|
4f6d21199c | ||
|
|
d4056026fb | ||
|
|
d12b040bd3 |
44
README.md
44
README.md
@@ -1,36 +1,42 @@
|
||||
# [Squoosh]!
|
||||
|
||||
[Squoosh] is an image compression web app that allows you to dive into the advanced options provided
|
||||
by various image compressors.
|
||||
[Squoosh] is an image compression web app that reduces image sizes through numerous formats.
|
||||
|
||||
# API & CLI
|
||||
|
||||
Squoosh now has [an API](https://github.com/GoogleChromeLabs/squoosh/tree/dev/libsquoosh) and [a CLI](https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli) that allows you to compress many images at once.
|
||||
Squoosh has [an API](https://github.com/GoogleChromeLabs/squoosh/tree/dev/libsquoosh) and [a CLI](https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli) to compress many images at once.
|
||||
|
||||
# Privacy
|
||||
|
||||
Google Analytics is used to record the following:
|
||||
Squoosh does not send your image to a server. All image compression processes locally.
|
||||
|
||||
- [Basic visit data](https://support.google.com/analytics/answer/6004245?ref_topic=2919631).
|
||||
- Before and after image size once an image is downloaded. These values are rounded to the nearest
|
||||
kilobyte.
|
||||
- If install is available, when Squoosh is installed, and what method was used to install Squoosh.
|
||||
However, Squoosh utilizes Google Analytics to collect the following:
|
||||
|
||||
Image compression is handled locally; no additional data is sent to the server.
|
||||
- [Basic visitor data](https://support.google.com/analytics/answer/6004245?ref_topic=2919631).
|
||||
- The before and after image size value.
|
||||
- If Squoosh PWA, the type of Squoosh installation.
|
||||
- If Squoosh PWA, the installation time and date.
|
||||
|
||||
# Building locally
|
||||
# Developing
|
||||
|
||||
Clone the repo, and:
|
||||
To develop for Squoosh:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
1. Clone the repository
|
||||
1. To install node packages, run:
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
1. Then build the app by running:
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
1. After building, start the development server by running:
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
You can run the development server with:
|
||||
# Contributing
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
Squoosh is an open-source project that appreciates all community involvement. To contribute to the project, follow the [contribute guide](/CONTRIBUTING.md).
|
||||
|
||||
[squoosh]: https://squoosh.app
|
||||
|
||||
33
cli/package-lock.json
generated
33
cli/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@squoosh/cli",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@squoosh/cli",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@squoosh/lib": "^0.3.1",
|
||||
"@squoosh/lib": "^0.4.0",
|
||||
"commander": "^7.2.0",
|
||||
"json5": "^2.2.0",
|
||||
"kleur": "^4.1.4",
|
||||
@@ -24,11 +24,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@squoosh/lib": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@squoosh/lib/-/lib-0.3.1.tgz",
|
||||
"integrity": "sha512-ni8gyTGgW9lH/yqaqgkxerGT7daSWNU9mtvWqj+/1qqzIT9YGfFvnTtDQRMIRjRCKQ5pkK/COufvWKuzHI6Dxw==",
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@squoosh/lib/-/lib-0.4.0.tgz",
|
||||
"integrity": "sha512-O1LyugWLZjMI4JZeZMA5vzfhfPjfMZXH5/HmVkRagP8B70wH3uoR7tjxfGNdSavey357MwL8YJDxbGwBBdHp7Q==",
|
||||
"dependencies": {
|
||||
"wasm-feature-detect": "^1.2.11",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": " ^12.5.0 || ^14.0.0 || ^16.0.0 "
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
@@ -326,6 +330,11 @@
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"node_modules/wasm-feature-detect": {
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz",
|
||||
"integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w=="
|
||||
},
|
||||
"node_modules/wcwidth": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
||||
@@ -345,10 +354,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@squoosh/lib": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@squoosh/lib/-/lib-0.3.1.tgz",
|
||||
"integrity": "sha512-ni8gyTGgW9lH/yqaqgkxerGT7daSWNU9mtvWqj+/1qqzIT9YGfFvnTtDQRMIRjRCKQ5pkK/COufvWKuzHI6Dxw==",
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@squoosh/lib/-/lib-0.4.0.tgz",
|
||||
"integrity": "sha512-O1LyugWLZjMI4JZeZMA5vzfhfPjfMZXH5/HmVkRagP8B70wH3uoR7tjxfGNdSavey357MwL8YJDxbGwBBdHp7Q==",
|
||||
"requires": {
|
||||
"wasm-feature-detect": "^1.2.11",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
}
|
||||
},
|
||||
@@ -581,6 +591,11 @@
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"wasm-feature-detect": {
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz",
|
||||
"integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w=="
|
||||
},
|
||||
"wcwidth": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@squoosh/cli",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.2",
|
||||
"description": "A CLI for Squoosh",
|
||||
"public": true,
|
||||
"type": "module",
|
||||
@@ -23,7 +23,7 @@
|
||||
"node": " ^12.20.2 || ^14.13.1 || ^16.0.0 "
|
||||
},
|
||||
"dependencies": {
|
||||
"@squoosh/lib": "^0.3.1",
|
||||
"@squoosh/lib": "^0.4.0",
|
||||
"commander": "^7.2.0",
|
||||
"json5": "^2.2.0",
|
||||
"kleur": "^4.1.4",
|
||||
|
||||
@@ -16,10 +16,10 @@ export
|
||||
OUT_ENC_JS = enc/avif_enc.js
|
||||
OUT_NODE_ENC_JS = enc/avif_node_enc.js
|
||||
OUT_ENC_MT_JS = enc/avif_enc_mt.js
|
||||
OUT_NODE_ENC_MT_JS = enc/avif_node_enc_mt.js
|
||||
OUT_DEC_JS = dec/avif_dec.js
|
||||
OUT_NODE_DEC_JS = dec/avif_node_dec.js
|
||||
|
||||
OUT_ENC_CPP = enc/avif_enc.cpp
|
||||
OUT_ENC_CPP = enc/avif_enc.cpp
|
||||
OUT_DEC_CPP = dec/avif_dec.cpp
|
||||
ENVIRONMENT = worker
|
||||
@@ -28,9 +28,9 @@ HELPER_MAKEFLAGS := -f helper.Makefile
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(OUT_ENC_JS) $(OUT_DEC_JS) $(OUT_ENC_MT_JS) $(OUT_NODE_ENC_JS) $(OUT_NODE_DEC_JS)
|
||||
all: $(OUT_ENC_JS) $(OUT_DEC_JS) $(OUT_ENC_MT_JS) $(OUT_NODE_ENC_JS) $(OUT_NODE_ENC_MT_JS) $(OUT_NODE_DEC_JS)
|
||||
|
||||
$(OUT_NODE_ENC_JS): ENVIRONMENT=node
|
||||
$(OUT_NODE_ENC_JS) $(OUT_NODE_ENC_MT_JS): ENVIRONMENT=node
|
||||
$(OUT_NODE_ENC_JS) $(OUT_ENC_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
|
||||
$(MAKE) \
|
||||
$(HELPER_MAKEFLAGS) \
|
||||
@@ -44,7 +44,7 @@ $(OUT_NODE_ENC_JS) $(OUT_ENC_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(L
|
||||
ENVIRONMENT=$(ENVIRONMENT) \
|
||||
LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_DECODE=0"
|
||||
|
||||
$(OUT_ENC_MT_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
|
||||
$(OUT_ENC_MT_JS) $(OUT_NODE_ENC_MT_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
|
||||
$(MAKE) \
|
||||
$(HELPER_MAKEFLAGS) \
|
||||
OUT_JS=$@ \
|
||||
@@ -89,4 +89,7 @@ $(LIBAOM_DIR)/CMakeLists.txt: $(LIBAOM_PACKAGE)
|
||||
clean:
|
||||
$(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_ENC_JS) clean
|
||||
$(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_ENC_MT_JS) clean
|
||||
$(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_ENC_NODE_JS) clean
|
||||
$(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_ENC_NODE_MT_JS) clean
|
||||
$(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_DEC_JS) clean
|
||||
$(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_DEV_NODE_JS) clean
|
||||
|
||||
Binary file not shown.
Binary file not shown.
2
codecs/avif/enc/avif_enc.js
generated
2
codecs/avif/enc/avif_enc.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/avif/enc/avif_enc_mt.js
generated
2
codecs/avif/enc/avif_enc_mt.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/avif/enc/avif_node_enc.js
generated
2
codecs/avif/enc/avif_node_enc.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
16
codecs/avif/enc/avif_node_enc_mt.js
generated
Normal file
16
codecs/avif/enc/avif_node_enc_mt.js
generated
Normal file
File diff suppressed because one or more lines are too long
BIN
codecs/avif/enc/avif_node_enc_mt.wasm
Executable file
BIN
codecs/avif/enc/avif_node_enc_mt.wasm
Executable file
Binary file not shown.
1
codecs/avif/enc/avif_node_enc_mt.worker.js
generated
Normal file
1
codecs/avif/enc/avif_node_enc_mt.worker.js
generated
Normal file
@@ -0,0 +1 @@
|
||||
"use strict";var Module={};if(typeof process==="object"&&typeof process.versions==="object"&&typeof process.versions.node==="string"){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",function(data){onmessage({data:data})});var nodeFS=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(nodeFS.readFileSync(f,"utf8"))},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./avif_node_enc_mt.js")).then(function(exports){return exports.default(Module)}).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,0,0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};
|
||||
@@ -1,5 +1,5 @@
|
||||
CODEC_URL = https://gitlab.com/wg1/jpeg-xl.git
|
||||
CODEC_VERSION = ab7c5e9b6795134377aa4846ceaae2c5bc504f76
|
||||
CODEC_URL = https://github.com/libjxl/libjxl.git
|
||||
CODEC_VERSION = v0.5
|
||||
CODEC_DIR = node_modules/jxl
|
||||
CODEC_BUILD_ROOT := $(CODEC_DIR)/build
|
||||
CODEC_MT_BUILD_DIR := $(CODEC_BUILD_ROOT)/mt
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -10,15 +10,13 @@ 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;
|
||||
};
|
||||
|
||||
val encode(std::string image, int width, int height, JXLOptions options) {
|
||||
@@ -33,15 +31,20 @@ val encode(std::string image, int width, int height, JXLOptions options) {
|
||||
pool_ptr = &pool;
|
||||
#endif
|
||||
|
||||
size_t st = 10 - options.effort;
|
||||
cparams.speed_tier = jxl::SpeedTier(st);
|
||||
|
||||
cparams.epf = options.epf;
|
||||
cparams.speed_tier = static_cast<jxl::SpeedTier>(options.speed);
|
||||
cparams.near_lossless = options.nearLossless;
|
||||
cparams.decoding_speed_tier = options.decodingSpeedTier;
|
||||
cparams.photon_noise_iso = options.photonNoiseIso;
|
||||
|
||||
if (options.lossyPalette) {
|
||||
cparams.lossy_palette = true;
|
||||
cparams.palette_colors = 0;
|
||||
cparams.options.predictor = jxl::Predictor::Zero;
|
||||
// Near-lossless assumes -R 0
|
||||
cparams.responsive = 0;
|
||||
cparams.modular_mode = true;
|
||||
}
|
||||
|
||||
float quality = options.quality;
|
||||
@@ -77,12 +80,6 @@ val encode(std::string image, int width, int height, JXLOptions options) {
|
||||
}
|
||||
}
|
||||
|
||||
if (cparams.near_lossless) {
|
||||
// Near-lossless assumes -R 0
|
||||
cparams.responsive = 0;
|
||||
cparams.modular_mode = true;
|
||||
}
|
||||
|
||||
io.metadata.m.SetAlphaBits(8);
|
||||
if (!io.metadata.size.Set(width, height)) {
|
||||
return val::null();
|
||||
@@ -110,12 +107,12 @@ 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("epf", &JXLOptions::epf);
|
||||
|
||||
function("encode", &encode);
|
||||
|
||||
4
codecs/jxl/enc/jxl_enc.d.ts
vendored
4
codecs/jxl/enc/jxl_enc.d.ts
vendored
@@ -1,11 +1,11 @@
|
||||
export interface EncodeOptions {
|
||||
speed: number;
|
||||
effort: number;
|
||||
quality: number;
|
||||
progressive: boolean;
|
||||
epf: number;
|
||||
nearLossless: number;
|
||||
lossyPalette: boolean;
|
||||
decodingSpeedTier: number;
|
||||
photonNoiseIso: number;
|
||||
}
|
||||
|
||||
export interface JXLModule extends EmscriptenWasm.Module {
|
||||
|
||||
2
codecs/jxl/enc/jxl_enc.js
generated
2
codecs/jxl/enc/jxl_enc.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/jxl/enc/jxl_enc_mt.js
generated
2
codecs/jxl/enc/jxl_enc_mt.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/jxl/enc/jxl_enc_mt_simd.js
generated
2
codecs/jxl/enc/jxl_enc_mt_simd.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/jxl/enc/jxl_node_enc.js
generated
2
codecs/jxl/enc/jxl_node_enc.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -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) {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -53,7 +53,11 @@ where
|
||||
#[wasm_bindgen]
|
||||
pub fn decode(mut data: &[u8]) -> ImageData {
|
||||
let mut decoder = png::Decoder::new(&mut data);
|
||||
decoder.set_transformations(png::Transformations::EXPAND);
|
||||
decoder.set_transformations(
|
||||
png::Transformations::EXPAND | // Turn paletted images into RGB
|
||||
png::Transformations::PACKING | // Turn images <8bit to 8bit
|
||||
png::Transformations::STRIP_16, // Turn 16bit into 8 bit
|
||||
);
|
||||
let (info, mut reader) = decoder.read_info().unwrap_throw();
|
||||
let num_pixels = (info.width * info.height) as usize;
|
||||
let mut buf = vec![0; num_pixels * 4];
|
||||
|
||||
14
codecs/visdif/BUILD.md
Normal file
14
codecs/visdif/BUILD.md
Normal file
@@ -0,0 +1,14 @@
|
||||
This codec currently needs monkey-patching of Emscripten
|
||||
|
||||
```
|
||||
$ docker run --rm -it -v $(PWD):/src squoosh-cpp "/bin/bash"
|
||||
# cat << EOF | patch /emsdk/upstream/emscripten/system/lib/dlmalloc.c
|
||||
659c659
|
||||
< #define MALLOC_ALIGNMENT ((size_t)(2 * sizeof(void *)))
|
||||
---
|
||||
> #define MALLOC_ALIGNMENT ((size_t)(16U))
|
||||
EOF
|
||||
# emcc --clear-cache
|
||||
# /emsdk/upstream/emscripten/embuilder build libdlmalloc --force
|
||||
# emmake make
|
||||
```
|
||||
@@ -5,12 +5,19 @@
|
||||
using namespace emscripten;
|
||||
using namespace butteraugli;
|
||||
|
||||
#define GAMMA 2.2
|
||||
|
||||
static float SrgbToLinear[256];
|
||||
|
||||
inline void gammaLookupTable() {
|
||||
SrgbToLinear[0] = 0;
|
||||
for (int i = 1; i < 256; ++i) {
|
||||
SrgbToLinear[i] = static_cast<float>(255.0 * pow(i / 255.0, GAMMA));
|
||||
}
|
||||
}
|
||||
|
||||
// Turns an interleaved RGBA buffer into 4 planes for each color channel
|
||||
void planarize(std::vector<ImageF>& img,
|
||||
const uint8_t* rgba,
|
||||
int width,
|
||||
int height,
|
||||
float gamma = 2.2) {
|
||||
void planarize(std::vector<ImageF>& img, const uint8_t* rgba, int width, int height) {
|
||||
assert(img.size() == 0);
|
||||
img.push_back(ImageF(width, height));
|
||||
img.push_back(ImageF(width, height));
|
||||
@@ -22,10 +29,10 @@ void planarize(std::vector<ImageF>& img,
|
||||
float* const row_b = img[2].Row(y);
|
||||
float* const row_a = img[3].Row(y);
|
||||
for (int x = 0; x < width; x++) {
|
||||
row_r[x] = 255.0 * pow(rgba[(y * width + x) * 4 + 0] / 255.0, gamma);
|
||||
row_g[x] = 255.0 * pow(rgba[(y * width + x) * 4 + 1] / 255.0, gamma);
|
||||
row_b[x] = 255.0 * pow(rgba[(y * width + x) * 4 + 2] / 255.0, gamma);
|
||||
row_a[x] = 255.0 * pow(rgba[(y * width + x) * 4 + 3] / 255.0, gamma);
|
||||
row_r[x] = SrgbToLinear[rgba[(y * width + x) * 4 + 0]];
|
||||
row_g[x] = SrgbToLinear[rgba[(y * width + x) * 4 + 1]];
|
||||
row_b[x] = SrgbToLinear[rgba[(y * width + x) * 4 + 2]];
|
||||
row_a[x] = SrgbToLinear[rgba[(y * width + x) * 4 + 3]];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +44,7 @@ class VisDiff {
|
||||
|
||||
public:
|
||||
VisDiff(std::string ref_img, int width, int height) {
|
||||
gammaLookupTable();
|
||||
planarize(this->ref_img, (uint8_t*)ref_img.c_str(), width, height);
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
|
||||
Binary file not shown.
@@ -30,7 +30,7 @@ const imagePath = 'path/to/image.png';
|
||||
const image = imagePool.ingestImage(imagePath);
|
||||
```
|
||||
|
||||
The `ingestImage` function can take anything the node [`readFile`][readfile] function can take, uncluding a buffer and `FileHandle`.
|
||||
The `ingestImage` function can take anything the node [`readFile`][readfile] function can take, including a buffer and `FileHandle`.
|
||||
|
||||
The returned `image` object is a representation of the original image, that you can now preprocess, encode, and extract information about.
|
||||
|
||||
|
||||
49
libsquoosh/lib/chunk-plugin.js
Normal file
49
libsquoosh/lib/chunk-plugin.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { promises as fs } from 'fs';
|
||||
import { basename } from 'path';
|
||||
|
||||
const defaultOpts = {
|
||||
prefix: 'chunk-url',
|
||||
};
|
||||
|
||||
export default function chunkPlugin(opts) {
|
||||
opts = { ...defaultOpts, ...opts };
|
||||
|
||||
const prefix = opts.prefix + ':';
|
||||
return {
|
||||
name: 'chunk-plugin',
|
||||
async resolveId(id, importer) {
|
||||
if (!id.startsWith(prefix)) return;
|
||||
const realId = id.slice(prefix.length);
|
||||
const resolveResult = await this.resolve(realId, importer);
|
||||
|
||||
if (!resolveResult) {
|
||||
throw Error(`Cannot find ${realId}`);
|
||||
}
|
||||
return prefix + resolveResult.id;
|
||||
},
|
||||
async load(id) {
|
||||
if (!id.startsWith(prefix)) return;
|
||||
const realId = id.slice(prefix.length);
|
||||
const source = await fs.readFile(realId);
|
||||
this.addWatchFile(realId);
|
||||
|
||||
return `export default import.meta.ROLLUP_FILE_URL_${this.emitFile({
|
||||
type: 'chunk',
|
||||
source,
|
||||
id: realId,
|
||||
})}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
2246
libsquoosh/package-lock.json
generated
2246
libsquoosh/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@squoosh/lib",
|
||||
"version": "0.3.1",
|
||||
"version": "0.4.0",
|
||||
"description": "A Node library for Squoosh",
|
||||
"public": true,
|
||||
"main": "./build/index.js",
|
||||
@@ -8,7 +8,8 @@
|
||||
"/build/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
"build": "rollup -c",
|
||||
"test": "uvu -r ts-node/register test"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Google Chrome Developers <chromium-dev@google.com>",
|
||||
@@ -22,6 +23,7 @@
|
||||
"node": " ^12.5.0 || ^14.0.0 || ^16.0.0 "
|
||||
},
|
||||
"dependencies": {
|
||||
"wasm-feature-detect": "^1.2.11",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -33,7 +35,9 @@
|
||||
"@types/node": "^15.6.1",
|
||||
"rollup": "^2.46.0",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.1.3",
|
||||
"uvu": "^0.5.1",
|
||||
"which": "^2.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import resolve from '@rollup/plugin-node-resolve';
|
||||
import cjs from '@rollup/plugin-commonjs';
|
||||
import simpleTS from './lib/simple-ts';
|
||||
import asset from './lib/asset-plugin.js';
|
||||
import chunk from './lib/chunk-plugin.js';
|
||||
import json from './lib/json-plugin.js';
|
||||
import autojson from './lib/autojson-plugin.js';
|
||||
import { getBabelOutputPlugin } from '@rollup/plugin-babel';
|
||||
@@ -9,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',
|
||||
@@ -18,6 +19,7 @@ export default {
|
||||
plugins: [
|
||||
resolve(),
|
||||
cjs(),
|
||||
chunk(),
|
||||
asset(),
|
||||
autojson(),
|
||||
json(),
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import { promises as fsp } from 'fs';
|
||||
import { instantiateEmscriptenWasm, pathify } from './emscripten-utils.js';
|
||||
import { threads } from 'wasm-feature-detect';
|
||||
import { cpus } from 'os';
|
||||
|
||||
// We use `navigator.hardwareConcurrency` for Emscripten’s pthread pool size.
|
||||
// This is the only workaround I can get working without crying.
|
||||
(globalThis as any).navigator = {
|
||||
hardwareConcurrency: cpus().length,
|
||||
};
|
||||
|
||||
interface DecodeModule extends EmscriptenWasm.Module {
|
||||
decode: (data: Uint8Array) => ImageData;
|
||||
}
|
||||
|
||||
type DecodeModuleFactory = EmscriptenWasm.ModuleFactory<DecodeModule>;
|
||||
|
||||
interface RotateModuleInstance {
|
||||
exports: {
|
||||
@@ -25,7 +39,7 @@ interface ResizeInstantiateOptions {
|
||||
|
||||
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'];
|
||||
}
|
||||
@@ -33,30 +47,38 @@ 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';
|
||||
|
||||
// WebP
|
||||
import type { WebPModule as WebPEncodeModule } from '../../codecs/webp/enc/webp_enc';
|
||||
import webpEnc from '../../codecs/webp/enc/webp_node_enc.js';
|
||||
import webpEncWasm from 'asset-url:../../codecs/webp/enc/webp_node_enc.wasm';
|
||||
import webpDec from '../../codecs/webp/dec/webp_node_dec.js';
|
||||
import webpDecWasm from 'asset-url:../../codecs/webp/dec/webp_node_dec.wasm';
|
||||
|
||||
// AVIF
|
||||
import type { AVIFModule as AVIFEncodeModule } from '../../codecs/avif/enc/avif_enc';
|
||||
import avifEnc from '../../codecs/avif/enc/avif_node_enc.js';
|
||||
import avifEncWasm from 'asset-url:../../codecs/avif/enc/avif_node_enc.wasm';
|
||||
import avifEncMt from '../../codecs/avif/enc/avif_node_enc_mt.js';
|
||||
import avifEncMtWorker from 'chunk-url:../../codecs/avif/enc/avif_node_enc_mt.worker.js';
|
||||
import avifEncMtWasm from 'asset-url:../../codecs/avif/enc/avif_node_enc_mt.wasm';
|
||||
import avifDec from '../../codecs/avif/dec/avif_node_dec.js';
|
||||
import avifDecWasm from 'asset-url:../../codecs/avif/dec/avif_node_dec.wasm';
|
||||
|
||||
// JXL
|
||||
import type { JXLModule as JXLEncodeModule } from '../../codecs/jxl/enc/jxl_enc';
|
||||
import jxlEnc from '../../codecs/jxl/enc/jxl_node_enc.js';
|
||||
import jxlEncWasm from 'asset-url:../../codecs/jxl/enc/jxl_node_enc.wasm';
|
||||
import jxlDec from '../../codecs/jxl/dec/jxl_node_dec.js';
|
||||
import jxlDecWasm from 'asset-url:../../codecs/jxl/dec/jxl_node_dec.wasm';
|
||||
|
||||
// WP2
|
||||
import type { WP2Module as WP2EncodeModule } from '../../codecs/wp2/enc/wp2_enc';
|
||||
import wp2Enc from '../../codecs/wp2/enc/wp2_node_enc.js';
|
||||
import wp2EncWasm from 'asset-url:../../codecs/wp2/enc/wp2_node_enc.wasm';
|
||||
import wp2Dec from '../../codecs/wp2/dec/wp2_node_dec.js';
|
||||
@@ -246,15 +268,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,
|
||||
@@ -283,8 +310,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,
|
||||
@@ -324,8 +356,21 @@ export const codecs = {
|
||||
name: 'AVIF',
|
||||
extension: 'avif',
|
||||
detectors: [/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/],
|
||||
dec: () => instantiateEmscriptenWasm(avifDec, avifDecWasm),
|
||||
enc: () => instantiateEmscriptenWasm(avifEnc, avifEncWasm),
|
||||
dec: () =>
|
||||
instantiateEmscriptenWasm(avifDec as DecodeModuleFactory, avifDecWasm),
|
||||
enc: async () => {
|
||||
if (await threads()) {
|
||||
return instantiateEmscriptenWasm(
|
||||
avifEncMt as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
|
||||
avifEncMtWasm,
|
||||
avifEncMtWorker,
|
||||
);
|
||||
}
|
||||
return instantiateEmscriptenWasm(
|
||||
avifEnc as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
|
||||
avifEncWasm,
|
||||
);
|
||||
},
|
||||
defaultEncoderOptions: {
|
||||
cqLevel: 33,
|
||||
cqAlphaLevel: -1,
|
||||
@@ -348,8 +393,13 @@ 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,
|
||||
quality: 75,
|
||||
@@ -369,8 +419,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,
|
||||
@@ -401,7 +456,7 @@ export const codecs = {
|
||||
await oxipngPromise;
|
||||
return {
|
||||
encode: (
|
||||
buffer: Uint8Array,
|
||||
buffer: Uint8ClampedArray | ArrayBuffer,
|
||||
width: number,
|
||||
height: number,
|
||||
opts: { level: number },
|
||||
@@ -424,4 +479,4 @@ export const codecs = {
|
||||
max: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fileURLToPath } from 'url';
|
||||
import { fileURLToPath, URL } from 'url';
|
||||
|
||||
export function pathify(path: string): string {
|
||||
if (path.startsWith('file://')) {
|
||||
@@ -10,10 +10,17 @@ export function pathify(path: string): string {
|
||||
export function instantiateEmscriptenWasm<T extends EmscriptenWasm.Module>(
|
||||
factory: EmscriptenWasm.ModuleFactory<T>,
|
||||
path: string,
|
||||
workerJS: string = '',
|
||||
): Promise<T> {
|
||||
return factory({
|
||||
locateFile() {
|
||||
return pathify(path);
|
||||
locateFile(requestPath) {
|
||||
// The glue code generated by emscripten uses the original
|
||||
// file names of the worker file and the wasm binary.
|
||||
// These will have changed in the bundling process and
|
||||
// we need to inject them here.
|
||||
if (requestPath.endsWith('.wasm')) return pathify(path);
|
||||
if (requestPath.endsWith('.worker.js')) return pathify(workerJS);
|
||||
return requestPath;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,10 +5,18 @@ import { promises as fsp } from 'fs';
|
||||
import { codecs as encoders, preprocessors } from './codecs.js';
|
||||
import WorkerPool from './worker_pool.js';
|
||||
import { autoOptimize } from './auto-optimizer.js';
|
||||
import type ImageData from './image_data';
|
||||
|
||||
export { ImagePool, encoders, preprocessors };
|
||||
type EncoderKey = keyof typeof encoders;
|
||||
type PreprocessorKey = keyof typeof preprocessors;
|
||||
type FileLike = Buffer | ArrayBuffer | string | ArrayBufferView;
|
||||
|
||||
async function decodeFile({ file }) {
|
||||
async function decodeFile({
|
||||
file,
|
||||
}: {
|
||||
file: FileLike;
|
||||
}): Promise<{ bitmap: ImageData; size: number }> {
|
||||
let buffer;
|
||||
if (ArrayBuffer.isView(file)) {
|
||||
buffer = Buffer.from(file.buffer);
|
||||
@@ -16,8 +24,9 @@ async function decodeFile({ file }) {
|
||||
} else if (file instanceof ArrayBuffer) {
|
||||
buffer = Buffer.from(file);
|
||||
file = 'Binary blob';
|
||||
} else if (file instanceof Buffer) {
|
||||
buffer = file;
|
||||
} else if ((file as unknown) instanceof Buffer) {
|
||||
// TODO: Check why we need type assertions here.
|
||||
buffer = (file as unknown) as Buffer;
|
||||
file = 'Binary blob';
|
||||
} else if (typeof file === 'string') {
|
||||
buffer = await fsp.readFile(file);
|
||||
@@ -28,23 +37,33 @@ async function decodeFile({ file }) {
|
||||
const firstChunkString = Array.from(firstChunk)
|
||||
.map((v) => String.fromCodePoint(v))
|
||||
.join('');
|
||||
const key = Object.entries(encoders).find(([name, { detectors }]) =>
|
||||
const key = Object.entries(encoders).find(([_name, { detectors }]) =>
|
||||
detectors.some((detector) => detector.exec(firstChunkString)),
|
||||
)?.[0];
|
||||
)?.[0] as EncoderKey | undefined;
|
||||
if (!key) {
|
||||
throw Error(`${file} has an unsupported format`);
|
||||
}
|
||||
const rgba = (await encoders[key].dec()).decode(new Uint8Array(buffer));
|
||||
const encoder = encoders[key];
|
||||
const mod = await encoder.dec();
|
||||
const rgba = mod.decode(new Uint8Array(buffer));
|
||||
return {
|
||||
bitmap: rgba,
|
||||
size: buffer.length,
|
||||
};
|
||||
}
|
||||
|
||||
async function preprocessImage({ preprocessorName, options, image }) {
|
||||
async function preprocessImage({
|
||||
preprocessorName,
|
||||
options,
|
||||
image,
|
||||
}: {
|
||||
preprocessorName: PreprocessorKey;
|
||||
options: any;
|
||||
image: { bitmap: ImageData };
|
||||
}) {
|
||||
const preprocessor = await preprocessors[preprocessorName].instantiate();
|
||||
image.bitmap = await preprocessor(
|
||||
image.bitmap.data,
|
||||
Uint8Array.from(image.bitmap.data),
|
||||
image.bitmap.width,
|
||||
image.bitmap.height,
|
||||
options,
|
||||
@@ -58,26 +77,39 @@ async function encodeImage({
|
||||
encConfig,
|
||||
optimizerButteraugliTarget,
|
||||
maxOptimizerRounds,
|
||||
}: {
|
||||
bitmap: ImageData;
|
||||
encName: EncoderKey;
|
||||
encConfig: any;
|
||||
optimizerButteraugliTarget: number;
|
||||
maxOptimizerRounds: number;
|
||||
}) {
|
||||
let binary;
|
||||
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, quality) =>
|
||||
const encode = (bitmapIn: ImageData, quality: number) =>
|
||||
encoder.encode(
|
||||
bitmapIn.data,
|
||||
bitmapIn.width,
|
||||
bitmapIn.height,
|
||||
Object.assign({}, encoders[encName].defaultEncoderOptions, {
|
||||
Object.assign({}, encoders[encName].defaultEncoderOptions as any, {
|
||||
[optionToOptimize]: quality,
|
||||
}),
|
||||
);
|
||||
const decode = (binary) => decoder.decode(binary);
|
||||
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,
|
||||
encode,
|
||||
nonNullEncode,
|
||||
decode,
|
||||
{
|
||||
min: encoders[encName].autoOptimize.min,
|
||||
@@ -92,12 +124,18 @@ async function encodeImage({
|
||||
[optionToOptimize]: Math.round(quality * 10000) / 10000,
|
||||
};
|
||||
} else {
|
||||
binary = encoder.encode(
|
||||
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,
|
||||
@@ -107,10 +145,15 @@ async function encodeImage({
|
||||
};
|
||||
}
|
||||
|
||||
// both decoding and encoding go through the worker pool
|
||||
function handleJob(params) {
|
||||
const { operation } = params;
|
||||
switch (operation) {
|
||||
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':
|
||||
@@ -118,7 +161,7 @@ function handleJob(params) {
|
||||
case 'preprocess':
|
||||
return preprocessImage(params);
|
||||
default:
|
||||
throw Error(`Invalid job "${operation}"`);
|
||||
throw Error(`Invalid job "${(params as any).operation}"`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +169,12 @@ function handleJob(params) {
|
||||
* Represents an ingested image.
|
||||
*/
|
||||
class Image {
|
||||
constructor(workerPool, file) {
|
||||
public file: FileLike;
|
||||
public workerPool: WorkerPool<JobMessage, any>;
|
||||
public decoded: Promise<{ bitmap: ImageData }>;
|
||||
public encodedWith: { [key: string]: any };
|
||||
|
||||
constructor(workerPool: WorkerPool<JobMessage, any>, file: FileLike) {
|
||||
this.file = file;
|
||||
this.workerPool = workerPool;
|
||||
this.decoded = workerPool.dispatchJob({ operation: 'decode', file });
|
||||
@@ -143,14 +191,15 @@ class Image {
|
||||
if (!Object.keys(preprocessors).includes(name)) {
|
||||
throw Error(`Invalid preprocessor "${name}"`);
|
||||
}
|
||||
const preprocessorName = name as PreprocessorKey;
|
||||
const preprocessorOptions = Object.assign(
|
||||
{},
|
||||
preprocessors[name].defaultOptions,
|
||||
preprocessors[preprocessorName].defaultOptions,
|
||||
options,
|
||||
);
|
||||
this.decoded = this.workerPool.dispatchJob({
|
||||
operation: 'preprocess',
|
||||
preprocessorName: name,
|
||||
preprocessorName,
|
||||
image: await this.decoded,
|
||||
options: preprocessorOptions,
|
||||
});
|
||||
@@ -161,14 +210,22 @@ class Image {
|
||||
/**
|
||||
* 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.
|
||||
* @returns {Promise<void>} - A promise that resolves when the image has been encoded with all the specified encoders.
|
||||
*/
|
||||
async encode(encodeOptions = {}) {
|
||||
async encode(
|
||||
encodeOptions: {
|
||||
optimizerButteraugliTarget?: number;
|
||||
maxOptimizerRounds?: number;
|
||||
} & {
|
||||
[key in EncoderKey]?: any; // any is okay for now
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
const { bitmap } = await this.decoded;
|
||||
for (const [encName, options] of Object.entries(encodeOptions)) {
|
||||
if (!Object.keys(encoders).includes(encName)) {
|
||||
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'
|
||||
@@ -193,28 +250,30 @@ class Image {
|
||||
* A pool where images can be ingested and squooshed.
|
||||
*/
|
||||
class ImagePool {
|
||||
public workerPool: WorkerPool<JobMessage, any>;
|
||||
|
||||
/**
|
||||
* Create a new pool.
|
||||
* @param {number} [threads] - Number of concurrent image processes to run in the pool. Defaults to the number of CPU cores in the system.
|
||||
*/
|
||||
constructor(threads) {
|
||||
constructor(threads: number) {
|
||||
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.
|
||||
* @param {FileLike} image - The image or path to the image that should be ingested and decoded.
|
||||
* @returns {Image} - A custom class reference to the decoded image.
|
||||
*/
|
||||
ingestImage(image) {
|
||||
ingestImage(image: FileLike): Image {
|
||||
return new Image(this.workerPool, image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the underlying image processing pipeline. The already processed images will still be there, but no new processing can start.
|
||||
* @returns {Promise<undefined>} - A promise that resolves when the underlying pipeline has closed.
|
||||
* @returns {Promise<void>} - A promise that resolves when the underlying pipeline has closed.
|
||||
*/
|
||||
async close() {
|
||||
async close(): Promise<void> {
|
||||
await this.workerPool.join();
|
||||
}
|
||||
}
|
||||
5
libsquoosh/src/missing-types.d.ts
vendored
5
libsquoosh/src/missing-types.d.ts
vendored
@@ -23,6 +23,11 @@ declare module 'asset-url:../../codecs/resize/pkg/squoosh_resize_bg.wasm' {
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module 'chunk-url:../../codecs/avif/enc/avif_node_enc_mt.worker.js' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
// These don't exist in NodeJS types so we're not able to use them but they are referenced in some emscripten and codec types
|
||||
// Thus, we need to explicitly assign them to be `never`
|
||||
// We're also not able to use the APIs that use these types
|
||||
|
||||
@@ -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(() => {
|
||||
@@ -64,8 +62,12 @@ export default class WorkerPool {
|
||||
|
||||
async _nextWorker() {
|
||||
const reader = this.workerQueue.readable.getReader();
|
||||
const { value, done } = await reader.read();
|
||||
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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
43
libsquoosh/test/index.test.ts
Normal file
43
libsquoosh/test/index.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as path from 'path';
|
||||
import { test } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { ImagePool } from '..';
|
||||
|
||||
let imagePool: ImagePool;
|
||||
|
||||
test.after.each(async () => {
|
||||
if (imagePool) {
|
||||
try {
|
||||
await imagePool.close();
|
||||
} catch (e) {}
|
||||
}
|
||||
imagePool = undefined;
|
||||
});
|
||||
|
||||
test('smoke test', async () => {
|
||||
imagePool = new ImagePool(1);
|
||||
|
||||
const imagePath = path.resolve(__dirname, '../../icon-large-maskable.png');
|
||||
const image = imagePool.ingestImage(imagePath);
|
||||
|
||||
const { bitmap } = await image.decoded;
|
||||
assert.equal(bitmap.width, 1024);
|
||||
|
||||
await image.preprocess({
|
||||
resize: {
|
||||
enabled: true,
|
||||
width: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await image.encode({
|
||||
mozjpeg: {},
|
||||
});
|
||||
|
||||
const { size } = await image.encodedWith.mozjpeg;
|
||||
// resulting image is 1554b
|
||||
assert.ok(size > 500);
|
||||
assert.ok(size < 5000);
|
||||
});
|
||||
|
||||
test.run();
|
||||
@@ -5,5 +5,12 @@
|
||||
"types": ["node"],
|
||||
"allowJs": true
|
||||
},
|
||||
"include": ["src/**/*", "../codecs/**/*"]
|
||||
"include": ["src/**/*", "../codecs/**/*"],
|
||||
"ts-node": {
|
||||
"transpileOnly": true,
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
},
|
||||
"include": ["tests/**/*"]
|
||||
}
|
||||
}
|
||||
|
||||
28
package-lock.json
generated
28
package-lock.json
generated
@@ -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",
|
||||
@@ -48,6 +48,20 @@
|
||||
"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",
|
||||
@@ -3871,9 +3885,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": {
|
||||
@@ -11946,9 +11960,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": {
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"scripts": {
|
||||
"build": "rollup -c && node lib/move-output.js",
|
||||
"debug": "node --inspect-brk node_modules/.bin/rollup -c",
|
||||
"dev": "run-p watch serve",
|
||||
"dev": "DEV_PORT=\"${DEV_PORT:=5000}\" run-p watch serve",
|
||||
"watch": "rollup -cw",
|
||||
"serve": "serve --config ../../../serve.json .tmp/build/static"
|
||||
"serve": "serve --listen=$DEV_PORT --config ../../../serve.json .tmp/build/static"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^17.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",
|
||||
|
||||
@@ -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 + '%');
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 />
|
||||
) : (
|
||||
|
||||
@@ -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;
|
||||
@@ -161,3 +160,8 @@ input.zoom {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.pixelated {
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
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.
|
||||
|
||||
@@ -29,10 +29,9 @@ interface State {
|
||||
slightLoss: boolean;
|
||||
autoEdgePreservingFilter: boolean;
|
||||
decodingSpeedTier: number;
|
||||
photonNoiseIso: number;
|
||||
}
|
||||
|
||||
const maxSpeed = 7;
|
||||
|
||||
export class Options extends Component<Props, State> {
|
||||
static getDerivedStateFromProps(
|
||||
props: Props,
|
||||
@@ -47,7 +46,7 @@ export class Options extends Component<Props, State> {
|
||||
// Create default form state from options
|
||||
return {
|
||||
options,
|
||||
effort: maxSpeed - options.speed,
|
||||
effort: options.effort,
|
||||
quality: options.quality,
|
||||
progressive: options.progressive,
|
||||
edgePreservingFilter: options.epf === -1 ? 2 : options.epf,
|
||||
@@ -55,6 +54,7 @@ export class Options extends Component<Props, State> {
|
||||
slightLoss: options.lossyPalette,
|
||||
autoEdgePreservingFilter: options.epf === -1,
|
||||
decodingSpeedTier: options.decodingSpeedTier,
|
||||
photonNoiseIso: options.photonNoiseIso,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,15 +87,15 @@ export class Options extends Component<Props, State> {
|
||||
};
|
||||
|
||||
const newOptions: EncodeOptions = {
|
||||
speed: maxSpeed - optionState.effort,
|
||||
effort: optionState.effort,
|
||||
quality: optionState.lossless ? 100 : optionState.quality,
|
||||
progressive: optionState.progressive,
|
||||
epf: optionState.autoEdgePreservingFilter
|
||||
? -1
|
||||
: optionState.edgePreservingFilter,
|
||||
nearLossless: 0,
|
||||
lossyPalette: optionState.lossless ? optionState.slightLoss : false,
|
||||
decodingSpeedTier: optionState.decodingSpeedTier,
|
||||
photonNoiseIso: optionState.photonNoiseIso,
|
||||
};
|
||||
|
||||
// Updating options, so we don't recalculate in getDerivedStateFromProps.
|
||||
@@ -121,6 +121,7 @@ export class Options extends Component<Props, State> {
|
||||
slightLoss,
|
||||
autoEdgePreservingFilter,
|
||||
decodingSpeedTier,
|
||||
photonNoiseIso,
|
||||
}: State,
|
||||
) {
|
||||
// I'm rendering both lossy and lossless forms, as it becomes much easier when
|
||||
@@ -164,7 +165,6 @@ export class Options extends Component<Props, State> {
|
||||
<label class={style.optionToggle}>
|
||||
Auto edge filter
|
||||
<Checkbox
|
||||
name="autoEdgeFilter"
|
||||
checked={autoEdgePreservingFilter}
|
||||
onChange={this._inputChange(
|
||||
'autoEdgePreservingFilter',
|
||||
@@ -199,6 +199,17 @@ export class Options extends Component<Props, State> {
|
||||
Optimise for decoding speed (worse compression):
|
||||
</Range>
|
||||
</div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
min="0"
|
||||
max="50000"
|
||||
step="100"
|
||||
value={photonNoiseIso}
|
||||
onInput={this._inputChange('photonNoiseIso', 'number')}
|
||||
>
|
||||
Noise equivalent to ISO:
|
||||
</Range>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Expander>
|
||||
@@ -212,8 +223,8 @@ export class Options extends Component<Props, State> {
|
||||
</label>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
min="0"
|
||||
max={maxSpeed - 1}
|
||||
min="3"
|
||||
max="9"
|
||||
value={effort}
|
||||
onInput={this._inputChange('effort', 'number')}
|
||||
>
|
||||
|
||||
@@ -18,11 +18,11 @@ export const label = 'JPEG XL (beta)';
|
||||
export const mimeType = 'image/jxl';
|
||||
export const extension = 'jxl';
|
||||
export const defaultOptions: EncodeOptions = {
|
||||
speed: 4,
|
||||
effort: 7,
|
||||
quality: 75,
|
||||
progressive: false,
|
||||
epf: -1,
|
||||
nearLossless: 0,
|
||||
lossyPalette: false,
|
||||
decodingSpeedTier: 0,
|
||||
photonNoiseIso: 0,
|
||||
};
|
||||
|
||||
51
src/shared/prerendered-app/Intro/SlideOnScroll/index.tsx
Normal file
51
src/shared/prerendered-app/Intro/SlideOnScroll/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
|
||||
interface Props {}
|
||||
interface State {}
|
||||
|
||||
export default class SlideOnScroll extends Component<Props, State> {
|
||||
private observer?: IntersectionObserver;
|
||||
|
||||
componentDidMount() {
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||
|
||||
const base = this.base as HTMLElement;
|
||||
let wasOutOfView = false;
|
||||
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) {
|
||||
wasOutOfView = true;
|
||||
base.style.opacity = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Only transition in if the element was at some point out of view.
|
||||
if (wasOutOfView) {
|
||||
base.style.opacity = '';
|
||||
base.animate(
|
||||
{ offset: 0, opacity: '0', transform: 'translateY(40px)' },
|
||||
{ duration: 300, easing: 'ease' },
|
||||
);
|
||||
}
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.2 },
|
||||
);
|
||||
|
||||
this.observer.observe(base);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Have to manually disconnect due to memory leaks in browsers.
|
||||
// One day we'll be able to remove this, and the private property.
|
||||
// https://twitter.com/jaffathecake/status/1405437361643790337
|
||||
if (this.observer) this.observer.disconnect();
|
||||
}
|
||||
|
||||
render({ children }: RenderableProps<Props>) {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="498" height="333" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M401.17 125.52C387.072 53.923 324.253.173 248.787.173c-59.916 0-111.954 34.035-137.869 83.841C48.513 90.655 0 143.574 0 207.7c0 68.692 55.77 124.516 124.394 124.516h269.519c57.221 0 103.662-46.486 103.662-103.763 0-54.787-42.502-99.198-96.405-102.933z" fill="#91D3FF" fill-opacity=".3"/><path d="M187.247 121.321l-3.987-3.987-3.481 4.434c-11.519 14.67-18.366 33.15-18.366 53.242 0 48.003 38.882 86.885 86.885 86.885 20.091 0 38.572-6.848 53.242-18.366l4.434-3.482-3.987-3.986-114.74-114.74zM309.348 228.7l3.987 3.986 3.481-4.434c11.519-14.669 18.366-33.15 18.366-53.242 0-48.002-38.882-86.884-86.884-86.884-20.092 0-38.573 6.847-53.242 18.365l-4.435 3.482 3.987 3.986L309.348 228.7zm-158.406-53.69c0-53.739 43.617-97.355 97.356-97.355 53.738 0 97.355 43.616 97.355 97.355 0 53.739-43.617 97.356-97.355 97.356-53.739 0-97.356-43.617-97.356-97.356z" fill="#FF3385" stroke="#FF3385" stroke-width="10"/></svg>
|
||||
|
After Width: | Height: | Size: 991 B |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.2 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.4 KiB |
@@ -10,12 +10,16 @@ import deviceScreen from 'url:./imgs/demos/demo-device-screen.png';
|
||||
import largePhotoIcon from 'url:./imgs/demos/icon-demo-large-photo.jpg';
|
||||
import artworkIcon from 'url:./imgs/demos/icon-demo-artwork.jpg';
|
||||
import deviceScreenIcon from 'url:./imgs/demos/icon-demo-device-screen.jpg';
|
||||
import smallSectionAsset from 'url:./imgs/info-content/small.svg';
|
||||
import simpleSectionAsset from 'url:./imgs/info-content/simple.svg';
|
||||
import secureSectionAsset from 'url:./imgs/info-content/secure.svg';
|
||||
import logoIcon from 'url:./imgs/demos/icon-demo-logo.png';
|
||||
import logoWithText from 'url:./imgs/logo-with-text.svg';
|
||||
import * as style from './style.css';
|
||||
import type SnackBarElement from 'shared/custom-els/snack-bar';
|
||||
import 'shared/custom-els/snack-bar';
|
||||
import { startBlobs } from './blob-anim/meta';
|
||||
import SlideOnScroll from './SlideOnScroll';
|
||||
|
||||
const demos = [
|
||||
{
|
||||
@@ -336,37 +340,125 @@ export default class Intro extends Component<Props, State> {
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class={style.footer}>
|
||||
|
||||
<div class={style.bottomWave}>
|
||||
<svg viewBox="0 0 1920 79" class={style.topWave}>
|
||||
<path
|
||||
d="M0 59l64-11c64-11 192-34 320-43s256-5 384 4 256 23 384 34 256 21 384 14 256-30 320-41l64-11v94H0z"
|
||||
class={style.footerWave}
|
||||
class={style.infoWave}
|
||||
/>
|
||||
</svg>
|
||||
<div class={style.contentPadding}>
|
||||
<footer class={style.footerItems}>
|
||||
<a
|
||||
class={style.footerLink}
|
||||
href="https://github.com/GoogleChromeLabs/squoosh/blob/dev/README.md#privacy"
|
||||
>
|
||||
Privacy
|
||||
</a>
|
||||
<a
|
||||
class={style.footerLink}
|
||||
href="https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli"
|
||||
>
|
||||
Squoosh CLI
|
||||
</a>
|
||||
<a
|
||||
class={style.footerLinkWithLogo}
|
||||
href="https://github.com/GoogleChromeLabs/squoosh"
|
||||
>
|
||||
<img src={githubLogo} alt="" width="10" height="10" />
|
||||
Source on Github
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class={style.info}>
|
||||
<div class={style.infoContainer}>
|
||||
<SlideOnScroll>
|
||||
<div class={style.infoContent}>
|
||||
<div class={style.infoTextWrapper}>
|
||||
<h2 class={style.infoTitle}>Small</h2>
|
||||
<p class={style.infoCaption}>
|
||||
Smaller images mean faster load times. Squoosh can reduce
|
||||
file size and maintain high quality.
|
||||
</p>
|
||||
</div>
|
||||
<div class={style.infoImgWrapper}>
|
||||
<img
|
||||
class={style.infoImg}
|
||||
src={smallSectionAsset}
|
||||
alt="silhouette of a large 1.4 megabyte image shrunk into a smaller 80 kilobyte image"
|
||||
width="536"
|
||||
height="522"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SlideOnScroll>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class={style.info}>
|
||||
<div class={style.infoContainer}>
|
||||
<SlideOnScroll>
|
||||
<div class={style.infoContent}>
|
||||
<div class={style.infoTextWrapper}>
|
||||
<h2 class={style.infoTitle}>Simple</h2>
|
||||
<p class={style.infoCaption}>
|
||||
Open your image, inspect the differences, then save
|
||||
instantly. Feeling adventurous? Adjust the settings for even
|
||||
smaller files.
|
||||
</p>
|
||||
</div>
|
||||
<div class={style.infoImgWrapper}>
|
||||
<img
|
||||
class={style.infoImg}
|
||||
src={simpleSectionAsset}
|
||||
alt="grid of multiple shrunk images displaying various options"
|
||||
width="538"
|
||||
height="384"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SlideOnScroll>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class={style.info}>
|
||||
<div class={style.infoContainer}>
|
||||
<SlideOnScroll>
|
||||
<div class={style.infoContent}>
|
||||
<div class={style.infoTextWrapper}>
|
||||
<h2 class={style.infoTitle}>Secure</h2>
|
||||
<p class={style.infoCaption}>
|
||||
Worried about privacy? Images never leave your device since
|
||||
Squoosh does all the work locally.
|
||||
</p>
|
||||
</div>
|
||||
<div class={style.infoImgWrapper}>
|
||||
<img
|
||||
class={style.infoImg}
|
||||
src={secureSectionAsset}
|
||||
alt="silhouette of a cloud with a 'no' symbol on top"
|
||||
width="498"
|
||||
height="333"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SlideOnScroll>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class={style.footer}>
|
||||
<div class={style.footerContainer}>
|
||||
<svg viewBox="0 0 1920 79" class={style.topWave}>
|
||||
<path
|
||||
d="M0 59l64-11c64-11 192-34 320-43s256-5 384 4 256 23 384 34 256 21 384 14 256-30 320-41l64-11v94H0z"
|
||||
class={style.footerWave}
|
||||
/>
|
||||
</svg>
|
||||
<div class={style.footerPadding}>
|
||||
<footer class={style.footerItems}>
|
||||
<a
|
||||
class={style.footerLink}
|
||||
href="https://github.com/GoogleChromeLabs/squoosh/blob/dev/README.md#privacy"
|
||||
>
|
||||
Privacy
|
||||
</a>
|
||||
<a
|
||||
class={style.footerLink}
|
||||
href="https://github.com/GoogleChromeLabs/squoosh/tree/dev/cli"
|
||||
>
|
||||
Squoosh CLI
|
||||
</a>
|
||||
<a
|
||||
class={style.footerLinkWithLogo}
|
||||
href="https://github.com/GoogleChromeLabs/squoosh"
|
||||
>
|
||||
<img src={githubLogo} alt="" width="10" height="10" />
|
||||
Source on Github
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
{beforeInstallEvent && (
|
||||
<button class={style.installBtn} onClick={this.onInstallClick}>
|
||||
Install
|
||||
|
||||
@@ -118,7 +118,114 @@
|
||||
fill: var(--light-blue);
|
||||
}
|
||||
|
||||
.bottom-wave {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.info {
|
||||
background: var(--white);
|
||||
position: relative;
|
||||
padding: 5em 2em;
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
padding: 5em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.info-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1em;
|
||||
|
||||
grid-template-areas:
|
||||
'text'
|
||||
'img';
|
||||
|
||||
@media (min-width: 712px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-areas: 'text img';
|
||||
|
||||
.info:nth-child(even) & {
|
||||
grid-template-areas: 'img text';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-title {
|
||||
color: var(--pink);
|
||||
font-size: 3em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-caption {
|
||||
font-size: 1.5em;
|
||||
line-height: 1.75;
|
||||
margin: 1em 0 0.5em 0;
|
||||
}
|
||||
|
||||
.info-link {
|
||||
font-size: 1.25em;
|
||||
text-underline-offset: 0.25em;
|
||||
color: var(--off-black);
|
||||
transition: color 400ms ease-in-out;
|
||||
margin-top: 1em;
|
||||
&:hover {
|
||||
color: var(--dim-blue);
|
||||
}
|
||||
}
|
||||
|
||||
.info-text-wrapper {
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
max-width: 27em;
|
||||
justify-self: center;
|
||||
grid-area: text;
|
||||
|
||||
@media (min-width: 712px) {
|
||||
justify-self: start;
|
||||
.info:nth-child(even) & {
|
||||
text-align: right;
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-img-wrapper {
|
||||
grid-area: img;
|
||||
}
|
||||
|
||||
.info-img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin: 0 0 0 auto;
|
||||
|
||||
.info:nth-child(even) & {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-wave {
|
||||
fill: var(--white);
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: var(--white);
|
||||
padding-top: 5em;
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
position: relative;
|
||||
background: var(--light-gray);
|
||||
}
|
||||
@@ -128,7 +235,11 @@
|
||||
}
|
||||
|
||||
.content-padding {
|
||||
padding: 2rem;
|
||||
padding: 2em 0;
|
||||
}
|
||||
|
||||
.footer-padding {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.footer-items {
|
||||
|
||||
@@ -25,18 +25,6 @@ import { lookup as lookupMime } from 'mime-types';
|
||||
const manifestSize = ({ width, height }: { width: number; height: number }) =>
|
||||
`${width}x${height}`;
|
||||
|
||||
// Set by Netlify
|
||||
const branch = process.env.BRANCH;
|
||||
|
||||
const branchOriginTrialIds = new Map([
|
||||
[
|
||||
'live',
|
||||
'Aj5GY7W9AHM8di+yvMCajIhLRHoYN7slruwOYXE/Iub5hgmW/r2RQt07vrUuT4eUTkWxcyNCAVkiI+5ugdVW3gAAAABUeyJvcmlnaW4iOiJodHRwczovL3NxdW9vc2guYXBwOjQ0MyIsImZlYXR1cmUiOiJXZWJBc3NlbWJseVNpbWQiLCJleHBpcnkiOjE2MjM4MDE1OTl9',
|
||||
],
|
||||
]);
|
||||
|
||||
const originTrialId = branchOriginTrialIds.get(branch || '');
|
||||
|
||||
interface Output {
|
||||
[outputPath: string]: string;
|
||||
}
|
||||
@@ -110,15 +98,6 @@ const toOutput: Output = {
|
||||
/*
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
|
||||
# Origin trial for WebAssembly SIMD.
|
||||
${
|
||||
originTrialId
|
||||
? ` Origin-Trial: ${originTrialId}`
|
||||
: `# Cannot find origin trial ID. process.env.BRANCH is: ${JSON.stringify(
|
||||
branch,
|
||||
)}`
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ import baseCss from 'css:./base.css';
|
||||
import initialCss from 'initial-css:';
|
||||
import { allSrc } from 'client-bundle:client/initial-app';
|
||||
import favicon from 'url:static-build/assets/favicon.ico';
|
||||
import { escapeStyleScriptContent } from 'static-build/utils';
|
||||
import ogImage from 'url:static-build/assets/icon-large-maskable.png';
|
||||
import { escapeStyleScriptContent, siteOrigin } from 'static-build/utils';
|
||||
import Intro from 'shared/prerendered-app/Intro';
|
||||
|
||||
interface Props {}
|
||||
@@ -27,7 +28,27 @@ const Index: FunctionalComponent<Props> = () => (
|
||||
<title>Squoosh</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Compress and compare images with different codecs, right in your browser"
|
||||
content="Squoosh is the ultimate image optimizer that allows you to compress and compare images with different codecs in your browser."
|
||||
/>
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@SquooshApp" />
|
||||
<meta property="og:title" content="Squoosh" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content={`${siteOrigin}${ogImage}`} />
|
||||
<meta
|
||||
property="og:image:secure_url"
|
||||
content={`${siteOrigin}${ogImage}`}
|
||||
/>
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="500" />
|
||||
<meta property="og:image:height" content="500" />
|
||||
<meta
|
||||
property="og:image:alt"
|
||||
content="A cartoon of a hand squeezing an image file on a dark background."
|
||||
/>
|
||||
<meta
|
||||
name="og:description"
|
||||
content="Squoosh is the ultimate image optimizer that allows you to compress and compare images with different codecs in your browser."
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
@@ -38,6 +59,7 @@ const Index: FunctionalComponent<Props> = () => (
|
||||
<link rel="shortcut icon" href={favicon} />
|
||||
<meta name="theme-color" content="#ff3385" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="canonical" href={siteOrigin} />
|
||||
<style
|
||||
dangerouslySetInnerHTML={{ __html: escapeStyleScriptContent(baseCss) }}
|
||||
/>
|
||||
|
||||
@@ -56,3 +56,17 @@ export function escapeStyleScriptContent(str: string): string {
|
||||
.replace(/<style/g, '<\\style')
|
||||
.replace(/<\/style/g, '<\\/style');
|
||||
}
|
||||
|
||||
/**
|
||||
* Origin of the site, depending on the environment.
|
||||
*/
|
||||
export const siteOrigin = (() => {
|
||||
if (process.env.DEV_PORT) return `http://localhost:${process.env.DEV_PORT}`;
|
||||
// https://docs.netlify.com/configure-builds/environment-variables/#build-metadata
|
||||
if (process.env.CONTEXT === 'production') return 'https://squoosh.app';
|
||||
if (process.env.DEPLOY_PRIME_URL) return process.env.DEPLOY_PRIME_URL;
|
||||
console.warn(
|
||||
'Unable to determine site origin, defaulting to https://squoosh.app',
|
||||
);
|
||||
return 'https://squoosh.app';
|
||||
})();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "./generic-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext", "dom"],
|
||||
"lib": ["esnext", "dom", "dom.iterable"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/shared/**/*", "src/static-build/**/*"]
|
||||
|
||||
Reference in New Issue
Block a user