mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-12 16:57:26 +00:00
Merge remote-tracking branch 'origin/dev' into avif-node-mt
This commit is contained in:
34
README.md
34
README.md
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user