Compare commits
12 Commits
maudnals-c
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
731ceba801 | |||
918c596cba | |||
42594277fd | |||
97d13de273 | |||
a1c3304c56 | |||
7a2fc917c1 | |||
1c4233b5ae | |||
4d67ec5a7b | |||
c24505230d | |||
2238d1abaf | |||
65ea02627b | |||
18b53e2b8a |
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -1,2 +1,2 @@
|
||||
/codecs/**/*.js linguist-generated=true
|
||||
/codecs/*/pkg*/*.d.ts linguist-generated=true
|
||||
/codecs/**/*.js linguist-generated -diff
|
||||
/codecs/*/pkg*/*.d.ts linguist-generated
|
||||
|
@ -1,3 +1,7 @@
|
||||
# Project no longer maintained
|
||||
|
||||
Unfortunately, due to a few people leaving the team, and staffing issues resulting from the current economic climate (ugh), this package is no longer actively maintained. I know that sucks, but there simply isn't the time & people to work on this. If anyone from the community wants to fork it, you have my blessing. The [squoosh.app](https://squoosh.app) will continue to be supported and improved.
|
||||
|
||||
# Squoosh CLI
|
||||
|
||||
Squoosh CLI is an _experimental_ way to run all the codecs you know from the [Squoosh] web app on your command line using WebAssembly. The Squoosh CLI uses a worker pool to parallelize processing images. This way you can apply the same codec to many images at once.
|
||||
|
32
cli/package-lock.json
generated
32
cli/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@squoosh/cli",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@squoosh/cli",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@squoosh/lib": "^0.4.0",
|
||||
@ -184,12 +184,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
|
||||
"integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
@ -225,11 +222,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
@ -478,12 +470,9 @@
|
||||
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
|
||||
"integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ=="
|
||||
},
|
||||
"kleur": {
|
||||
"version": "4.1.4",
|
||||
@ -504,11 +493,6 @@
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@squoosh/cli",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.3",
|
||||
"description": "A CLI for Squoosh",
|
||||
"public": true,
|
||||
"type": "module",
|
||||
|
@ -1,5 +1,5 @@
|
||||
CODEC_URL = https://github.com/libjxl/libjxl.git
|
||||
CODEC_VERSION = v0.5
|
||||
CODEC_VERSION = 9f544641ec83f6abd9da598bdd08178ee8a003e0
|
||||
CODEC_DIR = node_modules/jxl
|
||||
CODEC_BUILD_ROOT := $(CODEC_DIR)/build
|
||||
CODEC_MT_BUILD_DIR := $(CODEC_BUILD_ROOT)/mt
|
||||
@ -75,6 +75,9 @@ $(CODEC_MT_SIMD_BUILD_DIR)/Makefile: CXXFLAGS+=-msimd128
|
||||
-DCMAKE_CROSSCOMPILING_EMULATOR=node \
|
||||
-B $(@D) \
|
||||
$(<D)
|
||||
emcc -Wall -O3 -o $(CODEC_DIR)/third_party/skcms/skcms.cc.o -I$(CODEC_DIR)/third_party/skcms -c $(CODEC_DIR)/third_party/skcms/skcms.cc
|
||||
llvm-ar rc $(CODEC_BUILD_DIR)/third_party/libskcms.a $(CODEC_DIR)/third_party/skcms/skcms.cc.o
|
||||
rm $(CODEC_DIR)/third_party/skcms/skcms.cc.o
|
||||
|
||||
$(CODEC_DIR)/CMakeLists.txt:
|
||||
$(RM) -r $(@D)
|
||||
|
2
codecs/jxl/dec/jxl_dec.js
generated
2
codecs/jxl/dec/jxl_dec.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/jxl/dec/jxl_node_dec.js
generated
2
codecs/jxl/dec/jxl_node_dec.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -4,6 +4,7 @@
|
||||
#include "lib/jxl/base/thread_pool_internal.h"
|
||||
#include "lib/jxl/enc_external_image.h"
|
||||
#include "lib/jxl/enc_file.h"
|
||||
#include "lib/jxl/enc_color_management.h"
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
@ -17,6 +18,7 @@ struct JXLOptions {
|
||||
bool lossyPalette;
|
||||
size_t decodingSpeedTier;
|
||||
float photonNoiseIso;
|
||||
bool lossyModular;
|
||||
};
|
||||
|
||||
val encode(std::string image, int width, int height, JXLOptions options) {
|
||||
@ -50,11 +52,16 @@ val encode(std::string image, int width, int height, JXLOptions options) {
|
||||
float quality = options.quality;
|
||||
|
||||
// Quality settings roughly match libjpeg qualities.
|
||||
if (quality < 7 || quality == 100) {
|
||||
if (options.lossyModular || quality == 100) {
|
||||
cparams.modular_mode = true;
|
||||
// Internal modular quality to roughly match VarDCT size.
|
||||
cparams.quality_pair.first = cparams.quality_pair.second =
|
||||
std::min(35 + (quality - 7) * 3.0f, 100.0f);
|
||||
if (quality < 7) {
|
||||
cparams.quality_pair.first = cparams.quality_pair.second =
|
||||
std::min(35 + (quality - 7) * 3.0f, 100.0f);
|
||||
} else {
|
||||
cparams.quality_pair.first = cparams.quality_pair.second =
|
||||
std::min(35 + (quality - 7) * 65.f / 93.f, 100.0f);
|
||||
}
|
||||
} else {
|
||||
cparams.modular_mode = false;
|
||||
if (quality >= 30) {
|
||||
@ -91,14 +98,14 @@ val encode(std::string image, int width, int height, JXLOptions options) {
|
||||
jxl::Span<const uint8_t>(reinterpret_cast<const uint8_t*>(image.data()), image.size()), width,
|
||||
height, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*has_alpha=*/true,
|
||||
/*alpha_is_premultiplied=*/false, /*bits_per_sample=*/8, /*endiannes=*/JXL_LITTLE_ENDIAN,
|
||||
/*flipped_y=*/false, pool_ptr, main);
|
||||
/*flipped_y=*/false, pool_ptr, main, /*(only true if bits_per_sample==32) float_in=*/false);
|
||||
|
||||
if (!result) {
|
||||
return val::null();
|
||||
}
|
||||
|
||||
auto js_result = val::null();
|
||||
if (EncodeFile(cparams, &io, &passes_enc_state, &bytes, /*aux=*/nullptr, pool_ptr)) {
|
||||
if (EncodeFile(cparams, &io, &passes_enc_state, &bytes, jxl::GetJxlCms(), /*aux=*/nullptr, pool_ptr)) {
|
||||
js_result = Uint8Array.new_(typed_memory_view(bytes.size(), bytes.data()));
|
||||
}
|
||||
|
||||
@ -113,6 +120,7 @@ EMSCRIPTEN_BINDINGS(my_module) {
|
||||
.field("lossyPalette", &JXLOptions::lossyPalette)
|
||||
.field("decodingSpeedTier", &JXLOptions::decodingSpeedTier)
|
||||
.field("photonNoiseIso", &JXLOptions::photonNoiseIso)
|
||||
.field("lossyModular", &JXLOptions::lossyModular)
|
||||
.field("epf", &JXLOptions::epf);
|
||||
|
||||
function("encode", &encode);
|
||||
|
1
codecs/jxl/enc/jxl_enc.d.ts
vendored
1
codecs/jxl/enc/jxl_enc.d.ts
vendored
@ -6,6 +6,7 @@ export interface EncodeOptions {
|
||||
lossyPalette: boolean;
|
||||
decodingSpeedTier: number;
|
||||
photonNoiseIso: number;
|
||||
lossyModular: boolean;
|
||||
}
|
||||
|
||||
export interface JXLModule extends EmscriptenWasm.Module {
|
||||
|
2
codecs/jxl/enc/jxl_enc.js
generated
2
codecs/jxl/enc/jxl_enc.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/jxl/enc/jxl_enc_mt.js
generated
2
codecs/jxl/enc/jxl_enc_mt.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/jxl/enc/jxl_enc_mt_simd.js
generated
2
codecs/jxl/enc/jxl_enc_mt_simd.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/jxl/enc/jxl_node_enc.js
generated
2
codecs/jxl/enc/jxl_node_enc.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1,3 +1,7 @@
|
||||
# Project no longer maintained
|
||||
|
||||
Unfortunately, due to a few people leaving the team, and staffing issues resulting from the current economic climate (ugh), this package is no longer actively maintained. I know that sucks, but there simply isn't the time & people to work on this. If anyone from the community wants to fork it, you have my blessing. The [squoosh.app](https://squoosh.app) will continue to be supported and improved.
|
||||
|
||||
# libSquoosh
|
||||
|
||||
libSquoosh is an _experimental_ way to run all the codecs you know from the [Squoosh] web app directly inside your own JavaScript program. libSquoosh uses a worker pool to parallelize processing images. This way you can apply the same codec to many images at once.
|
||||
@ -43,19 +47,15 @@ The returned `image` object is a representation of the original image, that you
|
||||
When an image has been ingested, you can start preprocessing it and encoding it to other formats. This example will resize the image and then encode it to a `.jpg` and `.jxl` image:
|
||||
|
||||
```js
|
||||
await image.decoded; //Wait until the image is decoded before running preprocessors.
|
||||
|
||||
const preprocessOptions = {
|
||||
//When both width and height are specified, the image resized to specified size.
|
||||
resize: {
|
||||
enabled: true,
|
||||
width: 100,
|
||||
height: 50,
|
||||
},
|
||||
/*
|
||||
//When either width or height is specified, the image resized to specified size keeping aspect ratio.
|
||||
resize: {
|
||||
enabled: true,
|
||||
width: 100,
|
||||
}
|
||||
*/
|
||||
@ -68,7 +68,7 @@ const encodeOptions = {
|
||||
quality: 90,
|
||||
},
|
||||
};
|
||||
await image.encode(encodeOptions);
|
||||
const result = await image.encode(encodeOptions);
|
||||
```
|
||||
|
||||
The default values for each option can be found in the [`codecs.ts`][codecs.ts] file under `defaultEncoderOptions`. Every unspecified value will use the default value specified there. _Better documentation is needed here._
|
||||
@ -92,7 +92,7 @@ When you have encoded an image, you normally want to write it to a file.
|
||||
This example takes an image that has been encoded as a `jpg` and writes it to a file:
|
||||
|
||||
```js
|
||||
const rawEncodedImage = (await image.encodedWith.mozjpeg).binary;
|
||||
const rawEncodedImage = image.encodedWith.mozjpeg.binary;
|
||||
|
||||
fs.writeFile('/path/to/new/image.jpg', rawEncodedImage);
|
||||
```
|
||||
@ -103,10 +103,7 @@ This example iterates through all encoded versions of the image and writes them
|
||||
const newImagePath = '/path/to/image.'; //extension is added automatically
|
||||
|
||||
for (const encodedImage of Object.values(image.encodedWith)) {
|
||||
fs.writeFile(
|
||||
newImagePath + (await encodedImage).extension,
|
||||
(await encodedImage).binary,
|
||||
);
|
||||
fs.writeFile(newImagePath + encodedImage.extension, encodedImage.binary);
|
||||
}
|
||||
```
|
||||
|
||||
@ -135,7 +132,7 @@ console.log(await image.decoded);
|
||||
Information about an encoded image can be found at `Image.encodedWith[encoderName]`. It looks something like this:
|
||||
|
||||
```js
|
||||
console.log(await image.encodedWith.jxl);
|
||||
console.log(image.encodedWith.jxl);
|
||||
// Returns:
|
||||
{
|
||||
optionsUsed: {
|
||||
|
26
libsquoosh/lib/move-d-ts.js
Normal file
26
libsquoosh/lib/move-d-ts.js
Normal file
@ -0,0 +1,26 @@
|
||||
const fs = require('fs');
|
||||
const del = require('del');
|
||||
const path = require('path');
|
||||
|
||||
const tsOutputDir = path.resolve('..', '.tmp', 'ts', 'libsquoosh');
|
||||
const tsOutputSourceDir = path.join(tsOutputDir, 'src');
|
||||
const buildDir = path.resolve('build');
|
||||
|
||||
(async () => {
|
||||
await del(path.join(buildDir, '*.d.ts'));
|
||||
const files = await fs.promises.readdir(tsOutputSourceDir);
|
||||
|
||||
const movePromises = [];
|
||||
for (const file of files) {
|
||||
if (file.endsWith('d.ts') || file.endsWith('d.ts.map')) {
|
||||
movePromises.push(
|
||||
fs.promises.rename(
|
||||
path.join(tsOutputSourceDir, file),
|
||||
path.join(buildDir, file),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// We need to remove `tsconfig.tsbuildinfo` otherwise TS does not emit unchanged `.d.ts` files
|
||||
await del([path.join(tsOutputDir, 'tsconfig.tsbuildinfo')], { force: true });
|
||||
})();
|
4
libsquoosh/package-lock.json
generated
4
libsquoosh/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@squoosh/lib",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@squoosh/lib",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"wasm-feature-detect": "^1.2.11",
|
||||
|
@ -1,14 +1,15 @@
|
||||
{
|
||||
"name": "@squoosh/lib",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.2",
|
||||
"description": "A Node library for Squoosh",
|
||||
"public": true,
|
||||
"main": "./build/index.js",
|
||||
"types": "./build/index.d.ts",
|
||||
"files": [
|
||||
"/build/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
"build": "rollup -c && node lib/move-d-ts.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Google Chrome Developers <chromium-dev@google.com>",
|
||||
|
@ -419,6 +419,7 @@ export const codecs = {
|
||||
lossyPalette: false,
|
||||
decodingSpeedTier: 0,
|
||||
photonNoiseIso: 0,
|
||||
lossyModular: false,
|
||||
},
|
||||
autoOptimize: {
|
||||
option: 'quality',
|
||||
|
@ -22,9 +22,10 @@ type EncoderKey = keyof typeof encoders;
|
||||
type PreprocessorKey = keyof typeof preprocessors;
|
||||
|
||||
type PreprocessOptions = {
|
||||
resize?: ResizeOptions;
|
||||
quant?: QuantOptions;
|
||||
rotate?: RotateOptions;
|
||||
resize?: Partial<Omit<ResizeOptions, 'width' | 'height'>> &
|
||||
(Pick<ResizeOptions, 'width'> | Pick<ResizeOptions, 'height'>);
|
||||
quant?: Partial<QuantOptions>;
|
||||
rotate?: Partial<RotateOptions>;
|
||||
};
|
||||
type EncodeResult = {
|
||||
optionsUsed: object;
|
||||
|
@ -3,7 +3,8 @@
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext"],
|
||||
"types": ["node"],
|
||||
"allowJs": true
|
||||
"allowJs": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*", "../codecs/**/*"]
|
||||
}
|
||||
|
@ -11,7 +11,11 @@ export default class Checkbox extends Component<Props, State> {
|
||||
return (
|
||||
<div class={style.checkbox}>
|
||||
{props.checked ? (
|
||||
<CheckedIcon class={`${style.icon} ${style.checked}`} />
|
||||
props.disabled ? (
|
||||
<CheckedIcon class={`${style.icon} ${style.disabled}`} />
|
||||
) : (
|
||||
<CheckedIcon class={`${style.icon} ${style.checked}`} />
|
||||
)
|
||||
) : (
|
||||
<UncheckedIcon class={style.icon} />
|
||||
)}
|
||||
|
@ -20,3 +20,7 @@
|
||||
.checked {
|
||||
fill: var(--main-theme-color);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
fill: var(--dark-gray);
|
||||
}
|
||||
|
@ -40,7 +40,8 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
|
||||
}
|
||||
|
||||
.scrubber {
|
||||
display: flex;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
@ -54,10 +55,6 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
|
||||
padding: 0 calc(var(--thumb-size) * 0.24);
|
||||
}
|
||||
|
||||
.scrubber svg {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.arrow-left {
|
||||
fill: var(--pink);
|
||||
}
|
||||
|
@ -49,6 +49,7 @@
|
||||
align-self: center;
|
||||
padding: 9px 66px;
|
||||
position: relative;
|
||||
gap: 6px;
|
||||
|
||||
/* Allow clicks to fall through to the pinch zoom area */
|
||||
pointer-events: none;
|
||||
@ -68,7 +69,6 @@
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
.button,
|
||||
@ -95,6 +95,7 @@
|
||||
|
||||
.button {
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
background: rgba(50, 50, 50, 0.92);
|
||||
@ -114,7 +115,7 @@
|
||||
.last-button {
|
||||
composes: button;
|
||||
border-radius: 0 6px 6px 0;
|
||||
border-left-width: 1px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
.zoom {
|
||||
|
@ -114,7 +114,7 @@ async function decodeImage(
|
||||
}
|
||||
}
|
||||
// Otherwise fall through and try built-in decoding for a laugh.
|
||||
return await builtinDecode(signal, blob, mimeType);
|
||||
return await builtinDecode(signal, blob);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'AbortError') throw err;
|
||||
console.log(err);
|
||||
|
@ -63,35 +63,17 @@ interface DrawableToImageDataOptions {
|
||||
sh?: number;
|
||||
}
|
||||
|
||||
function getWidth(
|
||||
drawable: ImageBitmap | HTMLImageElement | VideoFrame,
|
||||
): number {
|
||||
if ('displayWidth' in drawable) {
|
||||
return drawable.displayWidth;
|
||||
}
|
||||
return drawable.width;
|
||||
}
|
||||
|
||||
function getHeight(
|
||||
drawable: ImageBitmap | HTMLImageElement | VideoFrame,
|
||||
): number {
|
||||
if ('displayHeight' in drawable) {
|
||||
return drawable.displayHeight;
|
||||
}
|
||||
return drawable.height;
|
||||
}
|
||||
|
||||
export function drawableToImageData(
|
||||
drawable: ImageBitmap | HTMLImageElement | VideoFrame,
|
||||
drawable: ImageBitmap | HTMLImageElement,
|
||||
opts: DrawableToImageDataOptions = {},
|
||||
): ImageData {
|
||||
const {
|
||||
width = getWidth(drawable),
|
||||
height = getHeight(drawable),
|
||||
width = drawable.width,
|
||||
height = drawable.height,
|
||||
sx = 0,
|
||||
sy = 0,
|
||||
sw = getWidth(drawable),
|
||||
sh = getHeight(drawable),
|
||||
sw = drawable.width,
|
||||
sh = drawable.height,
|
||||
} = opts;
|
||||
|
||||
// Make canvas same size as image
|
||||
|
@ -10,8 +10,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as WebCodecs from '../util/web-codecs';
|
||||
import { drawableToImageData } from './canvas';
|
||||
|
||||
/** If render engine is Safari */
|
||||
@ -139,15 +137,7 @@ export async function blobToImg(blob: Blob): Promise<HTMLImageElement> {
|
||||
export async function builtinDecode(
|
||||
signal: AbortSignal,
|
||||
blob: Blob,
|
||||
mimeType: string,
|
||||
): Promise<ImageData> {
|
||||
// If WebCodecs are supported, use that.
|
||||
if (await WebCodecs.isTypeSupported(mimeType)) {
|
||||
assertSignal(signal);
|
||||
try {
|
||||
return await abortable(signal, WebCodecs.decode(blob, mimeType));
|
||||
} catch (e) {}
|
||||
}
|
||||
assertSignal(signal);
|
||||
|
||||
// Prefer createImageBitmap as it's the off-thread option for Firefox.
|
||||
|
@ -1,33 +0,0 @@
|
||||
import { drawableToImageData } from '../canvas';
|
||||
|
||||
const hasImageDecoder = typeof ImageDecoder !== 'undefined';
|
||||
|
||||
export async function isTypeSupported(mimeType: string): Promise<boolean> {
|
||||
if (!hasImageDecoder) return false;
|
||||
// Some old versions of this API threw here.
|
||||
// It only impacted folks with experimental web platform flags enabled in Chrome 90.
|
||||
// The API was updated in Chrome 91.
|
||||
try {
|
||||
return await ImageDecoder.isTypeSupported(mimeType);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function decode(
|
||||
blob: Blob | File,
|
||||
mimeType: string,
|
||||
): Promise<ImageData> {
|
||||
if (!hasImageDecoder) {
|
||||
throw Error(
|
||||
`This browser does not support ImageDecoder. This function should not have been called.`,
|
||||
);
|
||||
}
|
||||
const decoder = new ImageDecoder({
|
||||
type: mimeType,
|
||||
// Non-obvious way to turn an Blob into a ReadableStream
|
||||
data: new Response(blob).body!,
|
||||
});
|
||||
const { image } = await decoder.decode();
|
||||
return drawableToImageData(image);
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
interface ImageDecoderInit {
|
||||
type: string;
|
||||
data: BufferSource | ReadableStream;
|
||||
premultiplyAlpha?: PremultiplyAlpha;
|
||||
colorSpaceConversion?: ColorSpaceConversion;
|
||||
desiredWidth?: number;
|
||||
desiredHeight?: number;
|
||||
preferAnimation?: boolean;
|
||||
}
|
||||
|
||||
interface ImageDecodeOptions {
|
||||
frameIndex: number;
|
||||
completeFramesOnly: boolean;
|
||||
}
|
||||
|
||||
interface ImageDecodeResult {
|
||||
image: VideoFrame;
|
||||
complete: boolean;
|
||||
}
|
||||
|
||||
// I didn’t do all the types because the class is kinda complex.
|
||||
// I focused on what we need.
|
||||
// See https://w3c.github.io/webcodecs/#videoframe
|
||||
declare class VideoFrame {
|
||||
displayWidth: number;
|
||||
displayHeight: number;
|
||||
}
|
||||
|
||||
// Add VideoFrame to canvas’ drawImage()
|
||||
interface CanvasDrawImage {
|
||||
drawImage(
|
||||
image: CanvasImageSource | VideoFrame,
|
||||
dx: number,
|
||||
dy: number,
|
||||
): void;
|
||||
drawImage(
|
||||
image: CanvasImageSource | VideoFrame,
|
||||
dx: number,
|
||||
dy: number,
|
||||
dw: number,
|
||||
dh: number,
|
||||
): void;
|
||||
drawImage(
|
||||
image: CanvasImageSource | VideoFrame,
|
||||
sx: number,
|
||||
sy: number,
|
||||
sw: number,
|
||||
sh: number,
|
||||
dx: number,
|
||||
dy: number,
|
||||
dw: number,
|
||||
dh: number,
|
||||
): void;
|
||||
}
|
||||
|
||||
declare class ImageDecoder {
|
||||
static isTypeSupported(type: string): Promise<boolean>;
|
||||
constructor(desc: ImageDecoderInit);
|
||||
decode(opts?: Partial<ImageDecodeOptions>): Promise<ImageDecodeResult>;
|
||||
}
|
@ -30,6 +30,7 @@ interface State {
|
||||
autoEdgePreservingFilter: boolean;
|
||||
decodingSpeedTier: number;
|
||||
photonNoiseIso: number;
|
||||
alternativeLossy: boolean;
|
||||
}
|
||||
|
||||
export class Options extends Component<Props, State> {
|
||||
@ -55,6 +56,7 @@ export class Options extends Component<Props, State> {
|
||||
autoEdgePreservingFilter: options.epf === -1,
|
||||
decodingSpeedTier: options.decodingSpeedTier,
|
||||
photonNoiseIso: options.photonNoiseIso,
|
||||
alternativeLossy: options.lossyModular,
|
||||
};
|
||||
}
|
||||
|
||||
@ -96,6 +98,7 @@ export class Options extends Component<Props, State> {
|
||||
lossyPalette: optionState.lossless ? optionState.slightLoss : false,
|
||||
decodingSpeedTier: optionState.decodingSpeedTier,
|
||||
photonNoiseIso: optionState.photonNoiseIso,
|
||||
lossyModular: optionState.quality < 7 ? true : optionState.alternativeLossy,
|
||||
};
|
||||
|
||||
// Updating options, so we don't recalculate in getDerivedStateFromProps.
|
||||
@ -122,6 +125,7 @@ export class Options extends Component<Props, State> {
|
||||
autoEdgePreservingFilter,
|
||||
decodingSpeedTier,
|
||||
photonNoiseIso,
|
||||
alternativeLossy,
|
||||
}: State,
|
||||
) {
|
||||
// I'm rendering both lossy and lossless forms, as it becomes much easier when
|
||||
@ -162,6 +166,14 @@ export class Options extends Component<Props, State> {
|
||||
Quality:
|
||||
</Range>
|
||||
</div>
|
||||
<label class={style.optionToggle}>
|
||||
Alternative lossy mode
|
||||
<Checkbox
|
||||
checked={quality < 7 ? true : alternativeLossy}
|
||||
disabled={quality < 7}
|
||||
onChange={this._inputChange('alternativeLossy', 'boolean')}
|
||||
/>
|
||||
</label>
|
||||
<label class={style.optionToggle}>
|
||||
Auto edge filter
|
||||
<Checkbox
|
||||
@ -223,7 +235,7 @@ export class Options extends Component<Props, State> {
|
||||
</label>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
min="3"
|
||||
min="1"
|
||||
max="9"
|
||||
value={effort}
|
||||
onInput={this._inputChange('effort', 'number')}
|
||||
|
@ -25,4 +25,5 @@ export const defaultOptions: EncodeOptions = {
|
||||
lossyPalette: false,
|
||||
decodingSpeedTier: 0,
|
||||
photonNoiseIso: 0,
|
||||
lossyModular: false,
|
||||
};
|
||||
|
BIN
src/static-build/assets/screenshot4.png
Normal file
BIN
src/static-build/assets/screenshot4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
BIN
src/static-build/assets/screenshot5.jpg
Normal file
BIN
src/static-build/assets/screenshot5.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
BIN
src/static-build/assets/screenshot6.jpg
Normal file
BIN
src/static-build/assets/screenshot6.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 70 KiB |
@ -19,15 +19,39 @@ import * as iconLarge from 'img-url:static-build/assets/icon-large.png';
|
||||
import * as screenshot1 from 'img-url:static-build/assets/screenshot1.png';
|
||||
import * as screenshot2 from 'img-url:static-build/assets/screenshot2.jpg';
|
||||
import * as screenshot3 from 'img-url:static-build/assets/screenshot3.jpg';
|
||||
import * as screenshot4 from 'img-url:static-build/assets/screenshot4.png';
|
||||
import * as screenshot5 from 'img-url:static-build/assets/screenshot5.jpg';
|
||||
import * as screenshot6 from 'img-url:static-build/assets/screenshot6.jpg';
|
||||
import dedent from 'dedent';
|
||||
import { lookup as lookupMime } from 'mime-types';
|
||||
|
||||
const manifestSize = ({ width, height }: { width: number; height: number }) =>
|
||||
`${width}x${height}`;
|
||||
interface Dimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const manifestSize = ({ width, height }: Dimensions) => `${width}x${height}`;
|
||||
const formFactor = ({ width, height }: Dimensions) =>
|
||||
width > height ? 'wide' : 'narrow';
|
||||
|
||||
const screenshots = [
|
||||
screenshot1,
|
||||
screenshot2,
|
||||
screenshot3,
|
||||
screenshot4,
|
||||
screenshot5,
|
||||
screenshot6,
|
||||
].map((screenshot) => ({
|
||||
src: screenshot.default,
|
||||
type: lookupMime(screenshot.default),
|
||||
sizes: manifestSize(screenshot),
|
||||
form_factor: formFactor(screenshot),
|
||||
}));
|
||||
|
||||
interface Output {
|
||||
[outputPath: string]: string;
|
||||
}
|
||||
|
||||
const toOutput: Output = {
|
||||
'index.html': renderPage(<IndexPage />),
|
||||
'manifest.json': JSON.stringify({
|
||||
@ -55,23 +79,7 @@ const toOutput: Output = {
|
||||
'Compress and compare images with different codecs, right in your browser.',
|
||||
lang: 'en',
|
||||
categories: ['photo', 'productivity', 'utilities'],
|
||||
screenshots: [
|
||||
{
|
||||
src: screenshot1.default,
|
||||
type: lookupMime(screenshot1.default),
|
||||
sizes: manifestSize(screenshot1),
|
||||
},
|
||||
{
|
||||
src: screenshot2.default,
|
||||
type: lookupMime(screenshot2.default),
|
||||
sizes: manifestSize(screenshot2),
|
||||
},
|
||||
{
|
||||
src: screenshot3.default,
|
||||
type: lookupMime(screenshot3.default),
|
||||
sizes: manifestSize(screenshot3),
|
||||
},
|
||||
],
|
||||
screenshots,
|
||||
share_target: {
|
||||
action: '/?utm_medium=PWA&utm_source=share-target&share-target',
|
||||
method: 'POST',
|
||||
|
Reference in New Issue
Block a user