Merge remote-tracking branch 'origin/dev' into avif-node-mt

This commit is contained in:
Surma
2021-07-20 11:51:10 +01:00
5 changed files with 109 additions and 35 deletions

View File

@@ -1,36 +1,42 @@
# [Squoosh]! # [Squoosh]!
[Squoosh] is an image compression web app that allows you to dive into the advanced options provided [Squoosh] is an image compression web app that reduces image sizes through numerous formats.
by various image compressors.
# API & CLI # 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 # 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). However, Squoosh utilizes Google Analytics to collect the following:
- 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.
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:
1. Clone the repository
1. To install node packages, run:
```sh ```sh
npm install npm install
```
1. Then build the app by running:
```sh
npm run build npm run build
``` ```
1. After building, start the development server by running:
You can run the development server with:
```sh ```sh
npm run dev npm run dev
``` ```
# Contributing
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 [squoosh]: https://squoosh.app

View File

@@ -42,11 +42,19 @@ When an image has been ingested, you can start preprocessing it and encoding it
await image.decoded; //Wait until the image is decoded before running preprocessors. await image.decoded; //Wait until the image is decoded before running preprocessors.
const preprocessOptions = { const preprocessOptions = {
//When both width and height are specified, the image resized to specified size.
resize: { resize: {
enabled: true, enabled: true,
width: 100, width: 100,
height: 50, height: 50,
} }
/*
//When either width or height is specified, the image resized to specified size keeping aspect ratio.
resize: {
enabled: true,
width: 100,
}
*/
} }
await image.preprocess(preprocessOptions); await image.preprocess(preprocessOptions);

View File

@@ -2,6 +2,39 @@ import { instantiateEmscriptenWasm } from './emscripten-utils.js';
import visdif from '../../codecs/visdif/visdif.js'; import visdif from '../../codecs/visdif/visdif.js';
import visdifWasm from 'asset-url:../../codecs/visdif/visdif.wasm'; import visdifWasm from 'asset-url:../../codecs/visdif/visdif.wasm';
import type ImageData from './image_data';
interface VisDiff {
distance: (data: Uint8ClampedArray) => number;
delete: () => void;
}
interface VisdiffConstructor {
new (data: Uint8ClampedArray, width: number, height: number): VisDiff;
}
interface VisDiffModule extends EmscriptenWasm.Module {
VisDiff: VisdiffConstructor;
}
type VisDiffModuleFactory = EmscriptenWasm.ModuleFactory<VisDiffModule>;
interface BinarySearchParams {
min?: number;
max?: number;
epsilon?: number;
maxRounds?: number;
}
interface AutoOptimizeParams extends BinarySearchParams {
butteraugliDistanceGoal?: number;
}
interface AutoOptimizeResult {
bitmap: ImageData;
binary: Uint8Array;
quality: number;
}
// `measure` is a (async) function that takes exactly one numeric parameter and // `measure` is a (async) function that takes exactly one numeric parameter and
// returns a value. The function is assumed to be monotonic (an increase in `parameter` // returns a value. The function is assumed to be monotonic (an increase in `parameter`
@@ -9,9 +42,9 @@ import visdifWasm from 'asset-url:../../codecs/visdif/visdif.wasm';
// to find `parameter` such that `measure` returns `measureGoal`, within an error // to find `parameter` such that `measure` returns `measureGoal`, within an error
// of `epsilon`. It will use at most `maxRounds` attempts. // of `epsilon`. It will use at most `maxRounds` attempts.
export async function binarySearch( export async function binarySearch(
measureGoal, measureGoal: number,
measure, measure: (val: number) => Promise<number>,
{ min = 0, max = 100, epsilon = 0.1, maxRounds = 6 } = {}, { min = 0, max = 100, epsilon = 0.1, maxRounds = 6 }: BinarySearchParams = {},
) { ) {
let parameter = (max - min) / 2 + min; let parameter = (max - min) / 2 + min;
let delta = (max - min) / 4; let delta = (max - min) / 4;
@@ -33,12 +66,21 @@ export async function binarySearch(
} }
export async function autoOptimize( export async function autoOptimize(
bitmapIn, bitmapIn: ImageData,
encode, encode: (
decode, bitmap: ImageData,
{ butteraugliDistanceGoal = 1.4, ...otherOpts } = {}, quality: number,
) { ) => Promise<Uint8Array> | Uint8Array,
const { VisDiff } = await instantiateEmscriptenWasm(visdif, visdifWasm); decode: (binary: Uint8Array) => Promise<ImageData> | ImageData,
{
butteraugliDistanceGoal = 1.4,
...binarySearchParams
}: AutoOptimizeParams = {},
): Promise<AutoOptimizeResult> {
const { VisDiff } = await instantiateEmscriptenWasm(
visdif as VisDiffModuleFactory,
visdifWasm,
);
const comparator = new VisDiff( const comparator = new VisDiff(
bitmapIn.data, bitmapIn.data,
@@ -46,8 +88,11 @@ export async function autoOptimize(
bitmapIn.height, bitmapIn.height,
); );
let bitmapOut; // We're able to do non null assertion because
let binaryOut; // we know that binarySearch will set these values
let bitmapOut!: ImageData;
let binaryOut!: Uint8Array;
// Increasing quality means _decrease_ in Butteraugli distance. // Increasing quality means _decrease_ in Butteraugli distance.
// `binarySearch` assumes that increasing `parameter` will // `binarySearch` assumes that increasing `parameter` will
// increase the metric value. So multipliy Butteraugli values by -1. // increase the metric value. So multipliy Butteraugli values by -1.
@@ -58,7 +103,7 @@ export async function autoOptimize(
bitmapOut = await decode(binaryOut); bitmapOut = await decode(binaryOut);
return -1 * comparator.distance(bitmapOut.data); return -1 * comparator.distance(bitmapOut.data);
}, },
otherOpts, binarySearchParams,
); );
comparator.delete(); comparator.delete();

View File

@@ -64,7 +64,7 @@ export default class WorkerPool {
async _nextWorker() { async _nextWorker() {
const reader = this.workerQueue.readable.getReader(); const reader = this.workerQueue.readable.getReader();
const { value, done } = await reader.read(); const { value } = await reader.read();
reader.releaseLock(); reader.releaseLock();
return value; return value;
} }

View File

@@ -6,10 +6,13 @@ import './custom-els/RangeInput';
import { linkRef } from 'shared/prerendered-app/util'; import { linkRef } from 'shared/prerendered-app/util';
interface Props extends preact.JSX.HTMLAttributes {} interface Props extends preact.JSX.HTMLAttributes {}
interface State {} interface State {
textFocused: boolean;
}
export default class Range extends Component<Props, State> { export default class Range extends Component<Props, State> {
rangeWc?: RangeInputElement; rangeWc?: RangeInputElement;
inputEl?: HTMLInputElement;
private onTextInput = (event: Event) => { private onTextInput = (event: Event) => {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
@@ -23,10 +26,19 @@ export default class Range extends Component<Props, State> {
); );
}; };
render(props: Props) { private onTextFocus = () => {
this.setState({ textFocused: true });
};
private onTextBlur = () => {
this.setState({ textFocused: false });
};
render(props: Props, state: State) {
const { children, ...otherProps } = props; const { children, ...otherProps } = props;
const { value, min, max, step } = props; const { value, min, max, step } = props;
const textValue = state.textFocused ? this.inputEl!.value : value;
return ( return (
<label class={style.range}> <label class={style.range}>
@@ -41,13 +53,16 @@ export default class Range extends Component<Props, State> {
/> />
</div> </div>
<input <input
ref={linkRef(this, 'inputEl')}
type="number" type="number"
class={style.textInput} class={style.textInput}
value={value} value={textValue}
min={min} min={min}
max={max} max={max}
step={step} step={step}
onInput={this.onTextInput} onInput={this.onTextInput}
onFocus={this.onTextFocus}
onBlur={this.onTextBlur}
/> />
</label> </label>
); );