Compare commits

...

38 Commits

Author SHA1 Message Date
Surma
25ea8b9e22 Make sure sharpyuv is disabled for decoder 2023-10-25 12:12:16 +00:00
Surma
47cd47e0a6 Encoder working, decoder isnt 2023-10-25 12:07:23 +00:00
Surma
f27d524d6c Try to use libsharpyuv 2023-10-23 12:23:28 +00:00
robo-mop
1b3f791723 Add skeleton for sharp downsampling param 2023-10-23 16:03:12 +05:30
robo-mop
625eee2df3 Add chroma subsampling options to AVIF 2023-10-18 22:03:47 +05:30
robo-mop
3ed34e101c Minor patches in lossless calculation 2023-10-18 22:03:47 +05:30
robo-mop
f1f40302fe Minor patches in logic 2023-10-17 15:53:43 +05:30
robo-mop
a5a3f632cd Rename variables for readability
Changes `cqlevel` to `quality`, and `cqAlphaLevel` to `qualityAlpha`
2023-10-17 15:53:43 +05:30
robo-mop
96da59f631 Add checks for API return values 2023-10-17 13:57:15 +05:30
robo-mop
642ac6d4f8 Update libaom (v3.7.0) 2023-09-28 19:47:08 +05:30
robo-mop
82658c703f Refactor variable names for clarity 2023-09-15 16:15:23 +05:30
robo-mop
f8804b1e4a Update libavif (v1.0.1-main) 2023-09-15 15:36:39 +05:30
robo-mop
47f874677e Update libavif (v1.0.0-main)
* Update libavif for improved compression and speed
* v1.0.0 deprecates usage of min and max-quantizers; we use `quality` and `qualityAlpha` instead
* Renamed `maxSpeed` to `MAX_EFFORT` for better readability
2023-09-15 15:10:21 +05:30
Adam Argyle
ecc715fe55 Merge pull request #1362 from Frank-Mayer/593
added snack bar inside noscript element
2023-04-12 14:31:49 -07:00
Frank Mayer
82caed4277 Merge branch 'dev' into 593 2023-04-12 23:13:11 +02:00
Adam Argyle
a7dff9475d Merge pull request #1348 from aryanpingle/keyboard-a11y
Better Keyboard A11y + Navigation
2023-04-12 09:11:16 -07:00
robo-mop
d168f7a447 Add focus animation to back button 2023-04-12 13:52:36 +05:30
robo-mop
edf9cb755e Add styles to save-import buttons 2023-04-12 13:52:36 +05:30
Frank-Mayer
a7503e69a2 better wording wor error message 2023-04-11 11:17:19 +02:00
Frank-Mayer
5a9733563e more information in the error message 2023-04-11 11:12:04 +02:00
robo-mop
2000e16ba2 Make text-fields match the dark theme 2023-04-08 22:35:55 +05:30
robo-mop
7dbe0a7714 Add focus effects to inputs
Provides better keyboard accessibility for:
* Checkboxes
* Range Inputs
* Copy-over Buttons
* Option-reveal Elements
Along with some minor design improvements
2023-04-08 22:35:55 +05:30
Frank-Mayer
25bc43e409 added snack bar inside noscript element 2023-04-08 18:27:29 +02:00
Adam Argyle
cee51bf355 Merge pull request #1359 from harsh26shah03/save-and-import-side-settings
Feat : Save and import side settings
2023-04-04 15:41:45 -07:00
Adam Argyle
8d6daf0fc4 Merge branch 'dev' into save-and-import-side-settings 2023-04-04 15:39:09 -07:00
Adam Argyle
61209d0b62 Merge pull request #1358 from harsh26shah03/file-size-update-sample-data
Fix : Sample File Size Unit
2023-04-04 15:38:10 -07:00
Adam Argyle
d0b4855022 Merge branch 'dev' into file-size-update-sample-data 2023-04-04 15:36:38 -07:00
Adam Argyle
6cb64a59ca Merge pull request #1355 from harsh26shah03/sample-image-data-styles
Feat : added hover animation to sample data
2023-04-04 15:32:50 -07:00
Harsh Shah
979fba0af1 Feat : Save and import side settings
There were requests from multiple users that
they use squoosh for compression but for each
iteration side settings resets to default
causing issues and there is no way to save and
import side settings.

There will be two buttons adjacent to copy-over

save side settings : This will save side encoder
and latest settings to localstorage of browser

import side settings : This will import side encoder
and latest settings from localstorage of browser and
replace the existing settings

Also if there are saved settings in locaStorage then
whenever user loads the app it will take that settings
and populate the side so user do not have to repeatedly
enter same settings for similar compression operation
subject to user has saved side settings

Update:1

Import settings button remains disabled if there
is nothing to import

Whenever the side setting is saved there will be
event fired and eventually listened to enable import
button

All 2 operations show notifications now

Import notification has undo option

Update : 2

Changed Icon SVGs
2023-04-04 22:35:09 +05:30
Harsh Shah
b1df3e1d54 Feat : Save and import side settings
There were requests from multiple users that
they use squoosh for compression but for each
iteration side settings resets to default
causing issues and there is no way to save and
import side settings.

There will be two buttons adjacent to copy-over

save side settings : This will save side encoder
and latest settings to localstorage of browser

import side settings : This will import side encoder
and latest settings from localstorage of browser and
replace the existing settings

Also if there are saved settings in locaStorage then
whenever user loads the app it will take that settings
and populate the side so user do not have to repeatedly
enter same settings for similar compression operation
subject to user has saved side settings

Update:1

Import settings button remains disabled if there
is nothing to import

Whenever the side setting is saved there will be
event fired and eventually listened to enable import
button

All 2 operations show notifications now

Import notification has undo option
2023-04-04 15:20:06 +05:30
Harsh Shah
4f6138d97d Fix : Sample File Size Unit
Sample data had size label showing wrong memory units

13k instead of 13KB
2.8mb instead of 2.8MB

small b corresponds to bits and this would change entire
meaning of file so fixed it
2023-04-04 11:39:57 +05:30
Harsh Shah
6b6e3724d2 Undoing this as separate PR is raised 2023-04-04 11:34:40 +05:30
Harsh Shah
8ac5e6f678 Merge branch 'GoogleChromeLabs:dev' into sample-image-data-styles 2023-04-04 11:30:38 +05:30
Adam Argyle
a930e8d928 Merge pull request #1353 from harsh26shah03/filename-in-result-dropdowns
Feat : Original image name (file name)
2023-04-03 09:08:57 -07:00
Harsh Shah
c814700cd2 Feat : changed unit of sample data and added hover animation 2023-04-03 15:00:19 +05:30
Harsh Shah
dfdf2a7f71 Feat : Original image name (file name)
Inside Compress tab Original image string
will be appended by file name.

In mobile view due to space constrain there
will be only <file name>
2023-04-02 23:11:17 +05:30
André R
cd336909fc Update libavif (v0.12-main) and AOM (3.6) for improved compression and speed (#1334)
* Update libavif and AOM (3.6) for improved compression and speed

* Minor fixes to package-lock done by NPM, also trigger new build

* Update AVIF version

---------

Co-authored-by: Jake Archibald <jaffathecake@gmail.com>
2023-03-10 10:24:15 +00:00
Aryan Pingle
a8bc48f94c Rotate copy-over-buttons correctly (#1335) 2023-03-02 11:42:52 +00:00
33 changed files with 512 additions and 117 deletions

View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

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

View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

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

View File

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

10
package-lock.json generated
View File

@@ -8,9 +8,6 @@
"name": "squoosh", "name": "squoosh",
"version": "2.0.0", "version": "2.0.0",
"license": "apache-2.0", "license": "apache-2.0",
"dependencies": {
"wasm-feature-detect": "^1.2.11"
},
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0", "@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.1.0", "@rollup/plugin-node-resolve": "^11.1.0",
@@ -46,6 +43,7 @@
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"serve": "^11.3.2", "serve": "^11.3.2",
"typescript": "^4.4.4", "typescript": "^4.4.4",
"wasm-feature-detect": "^1.2.11",
"which": "^2.0.2" "which": "^2.0.2"
} }
}, },
@@ -8561,7 +8559,8 @@
"node_modules/wasm-feature-detect": { "node_modules/wasm-feature-detect": {
"version": "1.2.11", "version": "1.2.11",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz", "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz",
"integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w==" "integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w==",
"dev": true
}, },
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
@@ -15757,7 +15756,8 @@
"wasm-feature-detect": { "wasm-feature-detect": {
"version": "1.2.11", "version": "1.2.11",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz", "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz",
"integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w==" "integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w==",
"dev": true
}, },
"which": { "which": {
"version": "2.0.2", "version": "2.0.2",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -281,24 +281,35 @@ export default class Compress extends Component<Props, State> {
source: undefined, source: undefined,
loading: false, loading: false,
preprocessorState: defaultPreprocessorState, preprocessorState: defaultPreprocessorState,
// Tasking catched side settings if available otherwise taking default settings
sides: [ sides: [
{ localStorage.getItem('leftSideSettings')
latestSettings: { ? {
processorState: defaultProcessorState, ...JSON.parse(localStorage.getItem('leftSideSettings') as string),
encoderState: undefined, loading: false,
}, }
loading: false, : {
}, latestSettings: {
{ processorState: defaultProcessorState,
latestSettings: { encoderState: undefined,
processorState: defaultProcessorState, },
encoderState: { loading: false,
type: 'mozJPEG', },
options: encoderMap.mozJPEG.meta.defaultOptions, localStorage.getItem('rightSideSettings')
? {
...JSON.parse(localStorage.getItem('rightSideSettings') as string),
loading: false,
}
: {
latestSettings: {
processorState: defaultProcessorState,
encoderState: {
type: 'mozJPEG',
options: encoderMap.mozJPEG.meta.defaultOptions,
},
},
loading: false,
}, },
},
loading: false,
},
], ],
mobileView: this.widthQuery.matches, mobileView: this.widthQuery.matches,
}; };
@@ -428,6 +439,99 @@ export default class Compress extends Component<Props, State> {
sides: cleanSet(this.state.sides, otherIndex, oldSettings), sides: cleanSet(this.state.sides, otherIndex, oldSettings),
}); });
}; };
/**
* This function saves encodedSettings and latestSettings of
* particular side in browser local storage
* @param index : (0|1)
* @returns
*/
private onSaveSideSettingsClick = async (index: 0 | 1) => {
if (index === 0) {
const leftSideSettings = JSON.stringify({
encodedSettings: this.state.sides[index].encodedSettings,
latestSettings: this.state.sides[index].latestSettings,
});
localStorage.setItem('leftSideSettings', leftSideSettings);
// Firing an event when we save side settings in localstorage
window.dispatchEvent(new CustomEvent('leftSideSettings'));
await this.props.showSnack('Left side settings saved', {
timeout: 1500,
actions: ['dismiss'],
});
return;
}
if (index === 1) {
const rightSideSettings = JSON.stringify({
encodedSettings: this.state.sides[index].encodedSettings,
latestSettings: this.state.sides[index].latestSettings,
});
localStorage.setItem('rightSideSettings', rightSideSettings);
// Firing an event when we save side settings in localstorage
window.dispatchEvent(new CustomEvent('rightSideSettings'));
await this.props.showSnack('Right side settings saved', {
timeout: 1500,
actions: ['dismiss'],
});
return;
}
};
/**
* This function sets the side state with catched localstorage
* value as per side index provided
* @param index : (0|1)
* @returns
*/
private onImportSideSettingsClick = async (index: 0 | 1) => {
const leftSideSettingsString = localStorage.getItem('leftSideSettings');
const rightSideSettingsString = localStorage.getItem('rightSideSettings');
if (index === 0 && leftSideSettingsString) {
const oldLeftSideSettings = this.state.sides[index];
const newLeftSideSettings = {
...this.state.sides[index],
...JSON.parse(leftSideSettingsString),
};
this.setState({
sides: cleanSet(this.state.sides, index, newLeftSideSettings),
});
const result = await this.props.showSnack('Left side settings imported', {
timeout: 3000,
actions: ['undo', 'dismiss'],
});
if (result === 'undo') {
this.setState({
sides: cleanSet(this.state.sides, index, oldLeftSideSettings),
});
}
return;
}
if (index === 1 && rightSideSettingsString) {
const oldRightSideSettings = this.state.sides[index];
const newRightSideSettings = {
...this.state.sides[index],
...JSON.parse(rightSideSettingsString),
};
this.setState({
sides: cleanSet(this.state.sides, index, newRightSideSettings),
});
const result = await this.props.showSnack(
'Right side settings imported',
{
timeout: 3000,
actions: ['undo', 'dismiss'],
},
);
if (result === 'undo') {
this.setState({
sides: cleanSet(this.state.sides, index, oldRightSideSettings),
});
}
return;
}
};
private onPreprocessorChange = async ( private onPreprocessorChange = async (
preprocessorState: PreprocessorState, preprocessorState: PreprocessorState,
@@ -829,6 +933,8 @@ export default class Compress extends Component<Props, State> {
onEncoderOptionsChange={this.onEncoderOptionsChange} onEncoderOptionsChange={this.onEncoderOptionsChange}
onProcessorOptionsChange={this.onProcessorOptionsChange} onProcessorOptionsChange={this.onProcessorOptionsChange}
onCopyToOtherSideClick={this.onCopyToOtherClick} onCopyToOtherSideClick={this.onCopyToOtherClick}
onSaveSideSettingsClick={this.onSaveSideSettingsClick}
onImportSideSettingsClick={this.onImportSideSettingsClick}
/> />
)); ));
@@ -842,7 +948,7 @@ export default class Compress extends Component<Props, State> {
typeLabel={ typeLabel={
side.latestSettings.encoderState side.latestSettings.encoderState
? encoderMap[side.latestSettings.encoderState.type].meta.label ? encoderMap[side.latestSettings.encoderState.type].meta.label
: 'Original Image' : `${side.file ? `${side.file.name}` : 'Original Image'}`
} }
/> />
)); ));

View File

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

View File

@@ -97,3 +97,31 @@ export const SwapIcon = () => (
<path d="M5.5 3.6v6.8L2.1 7l3.4-3.4M7 0L0 7l7 7V0zm4 0v14l7-7-7-7z" /> <path d="M5.5 3.6v6.8L2.1 7l3.4-3.4M7 0L0 7l7 7V0zm4 0v14l7-7-7-7z" />
</svg> </svg>
); );
export const SaveIcon = () => (
<svg viewBox="0 0 24 24">
<g
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M12.501 20.93c-.866.25-1.914-.166-2.176-1.247a1.724 1.724 0 0 0-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 0 0-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.94-1.543.826-3.31 2.37-2.37c1 .608 2.296.07 2.572-1.065c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.074.26 1.49 1.296 1.252 2.158M19 22v-6m3 3l-3-3l-3 3" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0-6 0" />
</g>
</svg>
);
export const ImportIcon = () => (
<svg viewBox="0 0 24 24">
<g
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M12.52 20.924c-.87.262-1.93-.152-2.195-1.241a1.724 1.724 0 0 0-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 0 0-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.94-1.543.826-3.31 2.37-2.37c1 .608 2.296.07 2.572-1.065c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.088.264 1.502 1.323 1.242 2.192M19 16v6m3-3l-3 3l-3-3" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0-6 0" />
</g>
</svg>
);

View File

@@ -38,10 +38,19 @@ interface State {
denoiseLevel: number; denoiseLevel: number;
aqMode: number; aqMode: number;
tune: AVIFTune; tune: AVIFTune;
enableSharpDownsampling: boolean;
} }
const maxQuant = 63; /**
const maxSpeed = 10; * AVIF quality ranges from 0 (worst) to 100 (lossless).
* Since lossless is a separate checkbox, we cap user-inputted quality at 99
*
* AVIF speed ranges from 0 (slowest) to 10 (fastest).
* We display it as 'effort' to the user since it conveys the speed-size tradeoff
* much better: speed = 10 - effort
*/
const MAX_QUALITY = 100;
const MAX_EFFORT = 10;
export class Options extends Component<Props, State> { export class Options extends Component<Props, State> {
static getDerivedStateFromProps( static getDerivedStateFromProps(
@@ -55,34 +64,30 @@ export class Options extends Component<Props, State> {
const { options } = props; const { options } = props;
const lossless = const lossless =
options.cqLevel === 0 && options.quality === MAX_QUALITY &&
options.cqAlphaLevel <= 0 && (options.qualityAlpha == -1 || options.qualityAlpha == MAX_QUALITY) &&
options.subsample == 3; options.subsample == 3;
const separateAlpha = options.cqAlphaLevel !== -1; const separateAlpha = options.qualityAlpha !== -1;
const cqLevel = lossless ? defaultOptions.cqLevel : options.cqLevel; const quality = lossless ? defaultOptions.quality : options.quality;
// Create default form state from options // Create default form state from options
return { return {
options, options,
lossless, lossless,
quality: maxQuant - cqLevel, quality: quality,
separateAlpha, separateAlpha,
alphaQuality: alphaQuality: separateAlpha ? options.qualityAlpha : options.quality,
maxQuant - subsample: defaultOptions.subsample,
(separateAlpha ? options.cqAlphaLevel : defaultOptions.cqLevel),
subsample:
options.subsample === 0 || lossless
? defaultOptions.subsample
: options.subsample,
tileRows: options.tileRowsLog2, tileRows: options.tileRowsLog2,
tileCols: options.tileColsLog2, tileCols: options.tileColsLog2,
effort: maxSpeed - options.speed, effort: MAX_EFFORT - options.speed,
chromaDeltaQ: options.chromaDeltaQ, chromaDeltaQ: options.chromaDeltaQ,
sharpness: options.sharpness, sharpness: options.sharpness,
denoiseLevel: options.denoiseLevel, denoiseLevel: options.denoiseLevel,
tune: options.tune, tune: options.tune,
enableSharpDownsampling: options.enableSharpDownsampling,
}; };
} }
@@ -120,20 +125,21 @@ export class Options extends Component<Props, State> {
}; };
const newOptions: EncodeOptions = { const newOptions: EncodeOptions = {
cqLevel: optionState.lossless ? 0 : maxQuant - optionState.quality, quality: optionState.lossless ? MAX_QUALITY : optionState.quality,
cqAlphaLevel: qualityAlpha:
optionState.lossless || !optionState.separateAlpha optionState.lossless || !optionState.separateAlpha
? -1 ? -1 // default AVIF alphaLevel
: maxQuant - optionState.alphaQuality, : optionState.alphaQuality,
// Always set to 4:4:4 if lossless // Always set to 4:4:4 if lossless
subsample: optionState.lossless ? 3 : optionState.subsample, subsample: optionState.lossless ? 3 : optionState.subsample,
tileColsLog2: optionState.tileCols, tileColsLog2: optionState.tileCols,
tileRowsLog2: optionState.tileRows, tileRowsLog2: optionState.tileRows,
speed: maxSpeed - optionState.effort, speed: MAX_EFFORT - optionState.effort,
chromaDeltaQ: optionState.chromaDeltaQ, chromaDeltaQ: optionState.chromaDeltaQ,
sharpness: optionState.sharpness, sharpness: optionState.sharpness,
denoiseLevel: optionState.denoiseLevel, denoiseLevel: optionState.denoiseLevel,
tune: optionState.tune, tune: optionState.tune,
enableSharpDownsampling: optionState.enableSharpDownsampling,
}; };
// Updating options, so we don't recalculate in getDerivedStateFromProps. // Updating options, so we don't recalculate in getDerivedStateFromProps.
@@ -167,6 +173,7 @@ export class Options extends Component<Props, State> {
sharpness, sharpness,
denoiseLevel, denoiseLevel,
tune, tune,
enableSharpDownsampling,
}: State, }: State,
) { ) {
return ( return (
@@ -183,7 +190,7 @@ export class Options extends Component<Props, State> {
<div class={style.optionOneCell}> <div class={style.optionOneCell}>
<Range <Range
min="0" min="0"
max="63" max={MAX_QUALITY - 1} // MAX_QUALITY would mean lossless
value={quality} value={quality}
onInput={this._inputChange('quality', 'number')} onInput={this._inputChange('quality', 'number')}
> >
@@ -211,9 +218,10 @@ export class Options extends Component<Props, State> {
value={subsample} value={subsample}
onChange={this._inputChange('subsample', 'number')} onChange={this._inputChange('subsample', 'number')}
> >
<option value="1">Half</option> <option value="0">4:0:0</option>
{/*<option value="2">4:2:2</option>*/} <option value="1">4:2:0</option>
<option value="3">Off</option> <option value="2">4:2:2</option>
<option value="3">4:4:4</option>
</Select> </Select>
</label> </label>
<label class={style.optionToggle}> <label class={style.optionToggle}>
@@ -228,7 +236,7 @@ export class Options extends Component<Props, State> {
<div class={style.optionOneCell}> <div class={style.optionOneCell}>
<Range <Range
min="0" min="0"
max="63" max={MAX_QUALITY - 1} // MAX_QUALITY would mean lossless
value={alphaQuality} value={alphaQuality}
onInput={this._inputChange( onInput={this._inputChange(
'alphaQuality', 'alphaQuality',
@@ -257,6 +265,16 @@ export class Options extends Component<Props, State> {
Sharpness: Sharpness:
</Range> </Range>
</div> </div>
<label class={style.optionToggle}>
Enable Sharp YUV Downsampling
<Checkbox
checked={enableSharpDownsampling}
onChange={this._inputChange(
'enableSharpDownsampling',
'boolean',
)}
/>
</label>
<div class={style.optionOneCell}> <div class={style.optionOneCell}>
<Range <Range
min="0" min="0"
@@ -307,7 +325,7 @@ export class Options extends Component<Props, State> {
<div class={style.optionOneCell}> <div class={style.optionOneCell}>
<Range <Range
min="0" min="0"
max="10" max={MAX_EFFORT}
value={effort} value={effort}
onInput={this._inputChange('effort', 'number')} onInput={this._inputChange('effort', 'number')}
> >

View File

@@ -18,8 +18,8 @@ export const label = 'AVIF';
export const mimeType = 'image/avif'; export const mimeType = 'image/avif';
export const extension = 'avif'; export const extension = 'avif';
export const defaultOptions: EncodeOptions = { export const defaultOptions: EncodeOptions = {
cqLevel: 33, quality: 50,
cqAlphaLevel: -1, qualityAlpha: -1,
denoiseLevel: 0, denoiseLevel: 0,
tileColsLog2: 0, tileColsLog2: 0,
tileRowsLog2: 0, tileRowsLog2: 0,
@@ -28,4 +28,5 @@ export const defaultOptions: EncodeOptions = {
chromaDeltaQ: false, chromaDeltaQ: false,
sharpness: 0, sharpness: 0,
tune: AVIFTune.auto, tune: AVIFTune.auto,
enableSharpDownsampling: false,
}; };

View File

@@ -62,7 +62,7 @@ snack-bar {
position: relative; position: relative;
flex: 0 1 auto; flex: 0 1 auto;
padding: 8px; padding: 8px;
height: 36px; height: 100%;
margin: auto 8px auto -8px; margin: auto 8px auto -8px;
min-width: 5em; min-width: 5em;
background: none; background: none;
@@ -78,6 +78,7 @@ snack-bar {
overflow: hidden; overflow: hidden;
transition: background-color 200ms ease; transition: background-color 200ms ease;
outline: none; outline: none;
text-decoration: none;
} }
.button:hover { .button:hover {
background-color: rgba(0, 0, 0, 0.15); background-color: rgba(0, 0, 0, 0.15);

View File

@@ -24,28 +24,28 @@ import SlideOnScroll from './SlideOnScroll';
const demos = [ const demos = [
{ {
description: 'Large photo', description: 'Large photo',
size: '2.8mb', size: '2.8MB',
filename: 'photo.jpg', filename: 'photo.jpg',
url: largePhoto, url: largePhoto,
iconUrl: largePhotoIcon, iconUrl: largePhotoIcon,
}, },
{ {
description: 'Artwork', description: 'Artwork',
size: '2.9mb', size: '2.9MB',
filename: 'art.jpg', filename: 'art.jpg',
url: artwork, url: artwork,
iconUrl: artworkIcon, iconUrl: artworkIcon,
}, },
{ {
description: 'Device screen', description: 'Device screen',
size: '1.6mb', size: '1.6MB',
filename: 'pixel3.png', filename: 'pixel3.png',
url: deviceScreen, url: deviceScreen,
iconUrl: deviceScreenIcon, iconUrl: deviceScreenIcon,
}, },
{ {
description: 'SVG icon', description: 'SVG icon',
size: '13k', size: '13KB',
filename: 'squoosh.svg', filename: 'squoosh.svg',
url: logo, url: logo,
iconUrl: logoIcon, iconUrl: logoIcon,
@@ -319,7 +319,7 @@ export default class Intro extends Component<Props, State> {
class="unbutton" class="unbutton"
onClick={(event) => this.onDemoClick(i, event)} onClick={(event) => this.onDemoClick(i, event)}
> >
<div> <div class={style.demoContainer}>
<div class={style.demoIconContainer}> <div class={style.demoIconContainer}>
<img <img
class={style.demoIcon} class={style.demoIcon}

View File

@@ -321,6 +321,13 @@
} }
} }
.demo-container {
transition: scale 400ms ease-in-out;
&:hover {
scale: 1.05;
}
}
.demo-size { .demo-size {
background: var(--dim-blue); background: var(--dim-blue);
border-radius: 1000px; border-radius: 1000px;

View File

@@ -19,6 +19,8 @@ import favicon from 'url:static-build/assets/favicon.ico';
import ogImage from 'url:static-build/assets/icon-large-maskable.png'; import ogImage from 'url:static-build/assets/icon-large-maskable.png';
import { escapeStyleScriptContent, siteOrigin } from 'static-build/utils'; import { escapeStyleScriptContent, siteOrigin } from 'static-build/utils';
import Intro from 'shared/prerendered-app/Intro'; import Intro from 'shared/prerendered-app/Intro';
import snackbarCss from 'css:../../../shared/custom-els/snack-bar/styles.css';
import * as snackbarStyle from '../../../shared/custom-els/snack-bar/styles.css';
interface Props {} interface Props {}
@@ -73,6 +75,29 @@ const Index: FunctionalComponent<Props> = () => (
<body> <body>
<div id="app"> <div id="app">
<Intro /> <Intro />
<noscript>
<style
dangerouslySetInnerHTML={{
__html: escapeStyleScriptContent(snackbarCss),
}}
/>
<snack-bar>
<div
class={snackbarStyle.snackbar}
aria-live="assertive"
aria-atomic="true"
aria-hidden="false"
>
<div class={snackbarStyle.text}>
Initialization error: This site requires JavaScript, which is
disabled in your browser.
</div>
<a class={snackbarStyle.button} href="/">
reload
</a>
</div>
</snack-bar>
</noscript>
</div> </div>
<script <script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{