mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-17 11:09:41 +00:00
Rollup build
This commit is contained in:
361
src/features/encoders/avif/client/index.tsx
Normal file
361
src/features/encoders/avif/client/index.tsx
Normal file
@@ -0,0 +1,361 @@
|
||||
import { EncodeOptions, defaultOptions } from '../shared/meta';
|
||||
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||
import { h, Component } from 'preact';
|
||||
import { preventDefault, shallowEqual } from 'client/lazy-app/util';
|
||||
import * as style from 'client/lazy-app/Compress/Options/style.css';
|
||||
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
|
||||
import Expander from 'client/lazy-app/Compress/Options/Expander';
|
||||
import Select from 'client/lazy-app/Compress/Options/Select';
|
||||
import Range from 'client/lazy-app/Compress/Options/Range';
|
||||
import linkState from 'linkstate';
|
||||
|
||||
export const encode = (
|
||||
signal: AbortSignal,
|
||||
workerBridge: WorkerBridge,
|
||||
imageData: ImageData,
|
||||
options: EncodeOptions,
|
||||
) => workerBridge.avifEncode(signal, imageData, options);
|
||||
|
||||
interface Props {
|
||||
options: EncodeOptions;
|
||||
onChange(newOptions: EncodeOptions): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
options: EncodeOptions;
|
||||
lossless: boolean;
|
||||
maxQuality: number;
|
||||
minQuality: number;
|
||||
separateAlpha: boolean;
|
||||
losslessAlpha: boolean;
|
||||
maxAlphaQuality: number;
|
||||
minAlphaQuality: number;
|
||||
showAdvanced: boolean;
|
||||
grayscale: boolean;
|
||||
subsample: number;
|
||||
tileRows: number;
|
||||
tileCols: number;
|
||||
effort: number;
|
||||
}
|
||||
|
||||
const maxQuant = 63;
|
||||
const maxSpeed = 10;
|
||||
|
||||
export class Options extends Component<Props, State> {
|
||||
static getDerivedStateFromProps(
|
||||
props: Props,
|
||||
state: State,
|
||||
): Partial<State> | null {
|
||||
if (state.options && shallowEqual(state.options, props.options)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { options } = props;
|
||||
|
||||
const lossless = options.maxQuantizer === 0 && options.minQuantizer === 0;
|
||||
const minQuantizerValue = lossless
|
||||
? defaultOptions.minQuantizer
|
||||
: options.minQuantizer;
|
||||
const maxQuantizerValue = lossless
|
||||
? defaultOptions.maxQuantizer
|
||||
: options.maxQuantizer;
|
||||
const losslessAlpha =
|
||||
options.maxQuantizerAlpha === 0 && options.minQuantizerAlpha === 0;
|
||||
const minQuantizerAlphaValue = losslessAlpha
|
||||
? defaultOptions.minQuantizerAlpha
|
||||
: options.minQuantizerAlpha;
|
||||
const maxQuantizerAlphaValue = losslessAlpha
|
||||
? defaultOptions.maxQuantizerAlpha
|
||||
: options.maxQuantizerAlpha;
|
||||
|
||||
// Create default form state from options
|
||||
return {
|
||||
options,
|
||||
lossless,
|
||||
losslessAlpha,
|
||||
maxQuality: maxQuant - minQuantizerValue,
|
||||
minQuality: maxQuant - maxQuantizerValue,
|
||||
separateAlpha:
|
||||
options.maxQuantizer !== options.maxQuantizerAlpha ||
|
||||
options.minQuantizer !== options.minQuantizerAlpha,
|
||||
maxAlphaQuality: maxQuant - minQuantizerAlphaValue,
|
||||
minAlphaQuality: maxQuant - maxQuantizerAlphaValue,
|
||||
grayscale: options.subsample === 0,
|
||||
subsample:
|
||||
options.subsample === 0 || lossless
|
||||
? defaultOptions.subsample
|
||||
: options.subsample,
|
||||
tileRows: options.tileRowsLog2,
|
||||
tileCols: options.tileColsLog2,
|
||||
effort: maxSpeed - options.speed,
|
||||
};
|
||||
}
|
||||
|
||||
// The rest of the defaults are set in getDerivedStateFromProps
|
||||
state: State = {
|
||||
showAdvanced: false,
|
||||
} as State;
|
||||
|
||||
private _inputChangeCallbacks = new Map<string, (event: Event) => void>();
|
||||
|
||||
private _inputChange = (prop: keyof State, type: 'number' | 'boolean') => {
|
||||
// Cache the callback for performance
|
||||
if (!this._inputChangeCallbacks.has(prop)) {
|
||||
this._inputChangeCallbacks.set(prop, (event: Event) => {
|
||||
const formEl = event.target as HTMLInputElement | HTMLSelectElement;
|
||||
const newVal =
|
||||
type === 'boolean'
|
||||
? 'checked' in formEl
|
||||
? formEl.checked
|
||||
: !!formEl.value
|
||||
: Number(formEl.value);
|
||||
|
||||
const newState: Partial<State> = {
|
||||
[prop]: newVal,
|
||||
};
|
||||
|
||||
// Ensure that min cannot be greater than max
|
||||
switch (prop) {
|
||||
case 'maxQuality':
|
||||
if (newVal < this.state.minQuality) {
|
||||
newState.minQuality = newVal as number;
|
||||
}
|
||||
break;
|
||||
case 'minQuality':
|
||||
if (newVal > this.state.maxQuality) {
|
||||
newState.maxQuality = newVal as number;
|
||||
}
|
||||
break;
|
||||
case 'maxAlphaQuality':
|
||||
if (newVal < this.state.minAlphaQuality) {
|
||||
newState.minAlphaQuality = newVal as number;
|
||||
}
|
||||
break;
|
||||
case 'minAlphaQuality':
|
||||
if (newVal > this.state.maxAlphaQuality) {
|
||||
newState.maxAlphaQuality = newVal as number;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const optionState = {
|
||||
...this.state,
|
||||
...newState,
|
||||
};
|
||||
|
||||
const maxQuantizer = optionState.lossless
|
||||
? 0
|
||||
: maxQuant - optionState.minQuality;
|
||||
const minQuantizer = optionState.lossless
|
||||
? 0
|
||||
: maxQuant - optionState.maxQuality;
|
||||
|
||||
const newOptions: EncodeOptions = {
|
||||
maxQuantizer,
|
||||
minQuantizer,
|
||||
maxQuantizerAlpha: optionState.separateAlpha
|
||||
? optionState.losslessAlpha
|
||||
? 0
|
||||
: maxQuant - optionState.minAlphaQuality
|
||||
: maxQuantizer,
|
||||
minQuantizerAlpha: optionState.separateAlpha
|
||||
? optionState.losslessAlpha
|
||||
? 0
|
||||
: maxQuant - optionState.maxAlphaQuality
|
||||
: minQuantizer,
|
||||
// Always set to 4:4:4 if lossless
|
||||
subsample: optionState.grayscale
|
||||
? 0
|
||||
: optionState.lossless
|
||||
? 3
|
||||
: optionState.subsample,
|
||||
tileColsLog2: optionState.tileCols,
|
||||
tileRowsLog2: optionState.tileRows,
|
||||
speed: maxSpeed - optionState.effort,
|
||||
};
|
||||
|
||||
// Updating options, so we don't recalculate in getDerivedStateFromProps.
|
||||
newState.options = newOptions;
|
||||
|
||||
this.setState(
|
||||
// It isn't clear to me why I have to cast this :)
|
||||
newState as State,
|
||||
);
|
||||
|
||||
this.props.onChange(newOptions);
|
||||
});
|
||||
}
|
||||
|
||||
return this._inputChangeCallbacks.get(prop)!;
|
||||
};
|
||||
|
||||
render(
|
||||
_: Props,
|
||||
{
|
||||
effort,
|
||||
grayscale,
|
||||
lossless,
|
||||
losslessAlpha,
|
||||
maxAlphaQuality,
|
||||
maxQuality,
|
||||
minAlphaQuality,
|
||||
minQuality,
|
||||
separateAlpha,
|
||||
showAdvanced,
|
||||
subsample,
|
||||
tileCols,
|
||||
tileRows,
|
||||
}: State,
|
||||
) {
|
||||
return (
|
||||
<form class={style.optionsSection} onSubmit={preventDefault}>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
checked={lossless}
|
||||
onChange={this._inputChange('lossless', 'boolean')}
|
||||
/>
|
||||
Lossless
|
||||
</label>
|
||||
<Expander>
|
||||
{!lossless && (
|
||||
<div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
min="0"
|
||||
max="62"
|
||||
value={maxQuality}
|
||||
onInput={this._inputChange('maxQuality', 'number')}
|
||||
>
|
||||
Max quality:
|
||||
</Range>
|
||||
</div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
min="0"
|
||||
max="62"
|
||||
value={minQuality}
|
||||
onInput={this._inputChange('minQuality', 'number')}
|
||||
>
|
||||
Min quality:
|
||||
</Range>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Expander>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
checked={separateAlpha}
|
||||
onChange={this._inputChange('separateAlpha', 'boolean')}
|
||||
/>
|
||||
Separate alpha quality
|
||||
</label>
|
||||
<Expander>
|
||||
{separateAlpha && (
|
||||
<div>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
checked={losslessAlpha}
|
||||
onChange={this._inputChange('losslessAlpha', 'boolean')}
|
||||
/>
|
||||
Lossless alpha
|
||||
</label>
|
||||
<Expander>
|
||||
{!losslessAlpha && (
|
||||
<div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
min="0"
|
||||
max="62"
|
||||
value={maxAlphaQuality}
|
||||
onInput={this._inputChange('maxAlphaQuality', 'number')}
|
||||
>
|
||||
Max alpha quality:
|
||||
</Range>
|
||||
</div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
min="0"
|
||||
max="62"
|
||||
value={minAlphaQuality}
|
||||
onInput={this._inputChange('minAlphaQuality', 'number')}
|
||||
>
|
||||
Min alpha quality:
|
||||
</Range>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Expander>
|
||||
</div>
|
||||
)}
|
||||
</Expander>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
checked={showAdvanced}
|
||||
onChange={linkState(this, 'showAdvanced')}
|
||||
/>
|
||||
Show advanced settings
|
||||
</label>
|
||||
<Expander>
|
||||
{showAdvanced && (
|
||||
<div>
|
||||
{/*<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
data-set-state="grayscale"
|
||||
checked={grayscale}
|
||||
onChange={this._inputChange('grayscale', 'boolean')}
|
||||
/>
|
||||
Grayscale
|
||||
</label>*/}
|
||||
<Expander>
|
||||
{!grayscale && !lossless && (
|
||||
<label class={style.optionTextFirst}>
|
||||
Subsample chroma:
|
||||
<Select
|
||||
data-set-state="subsample"
|
||||
value={subsample}
|
||||
onChange={this._inputChange('subsample', 'number')}
|
||||
>
|
||||
<option value="1">4:2:0</option>
|
||||
{/*<option value="2">4:2:2</option>*/}
|
||||
<option value="3">4:4:4</option>
|
||||
</Select>
|
||||
</label>
|
||||
)}
|
||||
</Expander>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
min="0"
|
||||
max="6"
|
||||
value={tileRows}
|
||||
onInput={this._inputChange('tileRows', 'number')}
|
||||
>
|
||||
Log2 of tile rows:
|
||||
</Range>
|
||||
</div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
min="0"
|
||||
max="6"
|
||||
value={tileCols}
|
||||
onInput={this._inputChange('tileCols', 'number')}
|
||||
>
|
||||
Log2 of tile cols:
|
||||
</Range>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Expander>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
min="0"
|
||||
max="10"
|
||||
value={effort}
|
||||
onInput={this._inputChange('effort', 'number')}
|
||||
>
|
||||
Effort:
|
||||
</Range>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
29
src/features/encoders/avif/shared/meta.ts
Normal file
29
src/features/encoders/avif/shared/meta.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { EncodeOptions } from 'codecs/avif/enc/avif_enc';
|
||||
|
||||
export { EncodeOptions };
|
||||
|
||||
export const label = 'AVIF';
|
||||
export const mimeType = 'image/avif';
|
||||
export const extension = 'avif';
|
||||
export const defaultOptions: EncodeOptions = {
|
||||
minQuantizer: 33,
|
||||
maxQuantizer: 63,
|
||||
minQuantizerAlpha: 33,
|
||||
maxQuantizerAlpha: 63,
|
||||
tileColsLog2: 0,
|
||||
tileRowsLog2: 0,
|
||||
speed: 8,
|
||||
subsample: 1,
|
||||
};
|
||||
13
src/features/encoders/avif/shared/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/avif/shared/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
49
src/features/encoders/avif/worker/avifEncode.ts
Normal file
49
src/features/encoders/avif/worker/avifEncode.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { AVIFModule } from 'codecs/avif/enc/avif_enc';
|
||||
import type { EncodeOptions } from '../shared/meta';
|
||||
import wasmUrlWithoutMT from 'url:codecs/avif/enc/avif_enc.wasm';
|
||||
import wasmUrlWithMT from 'url:codecs/avif/enc/avif_enc_mt.wasm';
|
||||
import workerUrl from 'omt:codecs/avif/enc/avif_enc_mt.worker.js';
|
||||
import { initEmscriptenModule } from 'features/worker-utils';
|
||||
import { threads } from 'wasm-feature-detect';
|
||||
|
||||
let emscriptenModule: Promise<AVIFModule>;
|
||||
|
||||
async function init() {
|
||||
if (await threads()) {
|
||||
const avifEncoder = await import('codecs/avif/enc/avif_enc_mt');
|
||||
return initEmscriptenModule<AVIFModule>(
|
||||
avifEncoder.default,
|
||||
wasmUrlWithMT,
|
||||
workerUrl,
|
||||
);
|
||||
}
|
||||
const avifEncoder = await import('codecs/avif/enc/avif_enc.js');
|
||||
return initEmscriptenModule(avifEncoder.default, wasmUrlWithoutMT);
|
||||
}
|
||||
|
||||
export default async function encode(
|
||||
data: ImageData,
|
||||
options: EncodeOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
if (!emscriptenModule) emscriptenModule = init();
|
||||
|
||||
const module = await emscriptenModule;
|
||||
const result = module.encode(data.data, data.width, data.height, options);
|
||||
|
||||
if (!result) throw new Error('Encoding error');
|
||||
|
||||
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||
return result.buffer as ArrayBuffer;
|
||||
}
|
||||
13
src/features/encoders/avif/worker/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/avif/worker/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
11
src/features/encoders/browserGIF/client/index.ts
Normal file
11
src/features/encoders/browserGIF/client/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { canvasEncodeTest, canvasEncode } from 'client/lazy-app/util';
|
||||
import WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||
import { EncodeOptions, mimeType } from '../shared/meta';
|
||||
|
||||
export const featureTest = () => canvasEncodeTest(mimeType);
|
||||
export const encode = (
|
||||
signal: AbortSignal,
|
||||
workerBridge: WorkerBridge,
|
||||
imageData: ImageData,
|
||||
options: EncodeOptions,
|
||||
) => canvasEncode(imageData, mimeType);
|
||||
13
src/features/encoders/browserGIF/client/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/browserGIF/client/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
18
src/features/encoders/browserGIF/shared/meta.ts
Normal file
18
src/features/encoders/browserGIF/shared/meta.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface EncodeOptions {}
|
||||
|
||||
export const label = 'Browser GIF';
|
||||
export const mimeType = 'image/gif';
|
||||
export const extension = 'gif';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
13
src/features/encoders/browserGIF/shared/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/browserGIF/shared/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
13
src/features/encoders/browserJPEG/client/index.ts
Normal file
13
src/features/encoders/browserJPEG/client/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { canvasEncode } from 'client/lazy-app/util';
|
||||
import WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||
import { qualityOption } from 'features/client-utils';
|
||||
import { mimeType, EncodeOptions } from '../shared/meta';
|
||||
|
||||
export const encode = (
|
||||
signal: AbortSignal,
|
||||
workerBridge: WorkerBridge,
|
||||
imageData: ImageData,
|
||||
options: EncodeOptions,
|
||||
) => canvasEncode(imageData, mimeType, options.quality);
|
||||
|
||||
export const Options = qualityOption({ min: 0, max: 1, step: 0.01 });
|
||||
13
src/features/encoders/browserJPEG/client/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/browserJPEG/client/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
20
src/features/encoders/browserJPEG/shared/meta.ts
Normal file
20
src/features/encoders/browserJPEG/shared/meta.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface EncodeOptions {
|
||||
quality: number;
|
||||
}
|
||||
|
||||
export const label = 'Browser JPEG';
|
||||
export const mimeType = 'image/jpeg';
|
||||
export const extension = 'jpg';
|
||||
export const defaultOptions: EncodeOptions = { quality: 0.75 };
|
||||
13
src/features/encoders/browserJPEG/shared/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/browserJPEG/shared/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
10
src/features/encoders/browserPNG/client/index.ts
Normal file
10
src/features/encoders/browserPNG/client/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { canvasEncode } from 'client/lazy-app/util';
|
||||
import WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||
import { EncodeOptions, mimeType } from '../shared/meta';
|
||||
|
||||
export const encode = (
|
||||
signal: AbortSignal,
|
||||
workerBridge: WorkerBridge,
|
||||
imageData: ImageData,
|
||||
options: EncodeOptions,
|
||||
) => canvasEncode(imageData, mimeType);
|
||||
13
src/features/encoders/browserPNG/client/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/browserPNG/client/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
18
src/features/encoders/browserPNG/shared/meta.ts
Normal file
18
src/features/encoders/browserPNG/shared/meta.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface EncodeOptions {}
|
||||
|
||||
export const label = 'Browser PNG';
|
||||
export const mimeType = 'image/png';
|
||||
export const extension = 'png';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
13
src/features/encoders/browserPNG/shared/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/browserPNG/shared/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
189
src/features/encoders/jxl/client/index.tsx
Normal file
189
src/features/encoders/jxl/client/index.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import { EncodeOptions } from '../shared/meta';
|
||||
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||
import { h, Component } from 'preact';
|
||||
import { preventDefault, shallowEqual } from 'client/lazy-app/util';
|
||||
import * as style from 'client/lazy-app/Compress/Options/style.css';
|
||||
import Range from 'client/lazy-app/Compress/Options/Range';
|
||||
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
|
||||
import Expander from 'client/lazy-app/Compress/Options/Expander';
|
||||
|
||||
export const encode = (
|
||||
signal: AbortSignal,
|
||||
workerBridge: WorkerBridge,
|
||||
imageData: ImageData,
|
||||
options: EncodeOptions,
|
||||
) => workerBridge.jxlEncode(signal, imageData, options);
|
||||
|
||||
interface Props {
|
||||
options: EncodeOptions;
|
||||
onChange(newOptions: EncodeOptions): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
options: EncodeOptions;
|
||||
effort: number;
|
||||
quality: number;
|
||||
progressive: boolean;
|
||||
edgePreservingFilter: number;
|
||||
lossless: boolean;
|
||||
slightLoss: boolean;
|
||||
}
|
||||
|
||||
const maxSpeed = 7;
|
||||
|
||||
export class Options extends Component<Props, State> {
|
||||
static getDerivedStateFromProps(
|
||||
props: Props,
|
||||
state: State,
|
||||
): Partial<State> | null {
|
||||
if (state.options && shallowEqual(state.options, props.options)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { options } = props;
|
||||
|
||||
// Create default form state from options
|
||||
return {
|
||||
options,
|
||||
effort: maxSpeed - options.speed,
|
||||
quality: options.quality,
|
||||
progressive: options.progressive,
|
||||
edgePreservingFilter: options.epf,
|
||||
lossless: options.quality === 100,
|
||||
slightLoss: options.lossyPalette,
|
||||
};
|
||||
}
|
||||
|
||||
// The rest of the defaults are set in getDerivedStateFromProps
|
||||
state: State = {
|
||||
lossless: false,
|
||||
} as State;
|
||||
|
||||
private _inputChangeCallbacks = new Map<string, (event: Event) => void>();
|
||||
|
||||
private _inputChange = (prop: keyof State, type: 'number' | 'boolean') => {
|
||||
// Cache the callback for performance
|
||||
if (!this._inputChangeCallbacks.has(prop)) {
|
||||
this._inputChangeCallbacks.set(prop, (event: Event) => {
|
||||
const formEl = event.target as HTMLInputElement | HTMLSelectElement;
|
||||
const newVal =
|
||||
type === 'boolean'
|
||||
? 'checked' in formEl
|
||||
? formEl.checked
|
||||
: !!formEl.value
|
||||
: Number(formEl.value);
|
||||
|
||||
const newState: Partial<State> = {
|
||||
[prop]: newVal,
|
||||
};
|
||||
|
||||
const optionState = {
|
||||
...this.state,
|
||||
...newState,
|
||||
};
|
||||
|
||||
const newOptions: EncodeOptions = {
|
||||
speed: maxSpeed - optionState.effort,
|
||||
quality: optionState.lossless ? 100 : optionState.quality,
|
||||
progressive: optionState.progressive,
|
||||
epf: optionState.edgePreservingFilter,
|
||||
nearLossless: 0,
|
||||
lossyPalette: optionState.lossless ? optionState.slightLoss : false,
|
||||
};
|
||||
|
||||
// Updating options, so we don't recalculate in getDerivedStateFromProps.
|
||||
newState.options = newOptions;
|
||||
|
||||
this.setState(newState);
|
||||
|
||||
this.props.onChange(newOptions);
|
||||
});
|
||||
}
|
||||
|
||||
return this._inputChangeCallbacks.get(prop)!;
|
||||
};
|
||||
|
||||
render(
|
||||
{}: Props,
|
||||
{
|
||||
effort,
|
||||
quality,
|
||||
progressive,
|
||||
edgePreservingFilter,
|
||||
lossless,
|
||||
slightLoss,
|
||||
}: State,
|
||||
) {
|
||||
// I'm rendering both lossy and lossless forms, as it becomes much easier when
|
||||
// gathering the data.
|
||||
return (
|
||||
<form class={style.optionsSection} onSubmit={preventDefault}>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="lossless"
|
||||
checked={lossless}
|
||||
onChange={this._inputChange('lossless', 'boolean')}
|
||||
/>
|
||||
Lossless
|
||||
</label>
|
||||
<Expander>
|
||||
{lossless && (
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="slightLoss"
|
||||
checked={slightLoss}
|
||||
onChange={this._inputChange('slightLoss', 'boolean')}
|
||||
/>
|
||||
Slight loss
|
||||
</label>
|
||||
)}
|
||||
</Expander>
|
||||
<Expander>
|
||||
{!lossless && (
|
||||
<div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
min="0"
|
||||
max="99.9"
|
||||
step="0.1"
|
||||
value={quality}
|
||||
onInput={this._inputChange('quality', 'number')}
|
||||
>
|
||||
Quality:
|
||||
</Range>
|
||||
</div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
min="0"
|
||||
max="3"
|
||||
value={edgePreservingFilter}
|
||||
onInput={this._inputChange('edgePreservingFilter', 'number')}
|
||||
>
|
||||
Edge preserving filter:
|
||||
</Range>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Expander>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="progressive"
|
||||
checked={progressive}
|
||||
onChange={this._inputChange('progressive', 'boolean')}
|
||||
/>
|
||||
Progressive rendering
|
||||
</label>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
min="0"
|
||||
max={maxSpeed - 1}
|
||||
value={effort}
|
||||
onInput={this._inputChange('effort', 'number')}
|
||||
>
|
||||
Effort:
|
||||
</Range>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
27
src/features/encoders/jxl/shared/meta.ts
Normal file
27
src/features/encoders/jxl/shared/meta.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { EncodeOptions } from 'codecs/jxl/enc/jxl_enc';
|
||||
|
||||
export { EncodeOptions };
|
||||
|
||||
export const label = 'JPEG XL (beta)';
|
||||
export const mimeType = 'image/jpegxl';
|
||||
export const extension = 'jxl';
|
||||
export const defaultOptions: EncodeOptions = {
|
||||
speed: 5,
|
||||
quality: 50,
|
||||
progressive: false,
|
||||
epf: 2,
|
||||
nearLossless: 0,
|
||||
lossyPalette: false,
|
||||
};
|
||||
33
src/features/encoders/jxl/worker/jxlEncode.ts
Normal file
33
src/features/encoders/jxl/worker/jxlEncode.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import jxlEncoder, { JXLModule } from 'codecs/jxl/enc/jxl_enc';
|
||||
import wasmUrl from 'url:codecs/jxl/enc/jxl_enc.wasm';
|
||||
import { initEmscriptenModule } from 'features/worker-utils';
|
||||
import type { EncodeOptions } from '../shared/meta';
|
||||
|
||||
let emscriptenModule: Promise<JXLModule>;
|
||||
|
||||
export default async function encode(
|
||||
data: ImageData,
|
||||
options: EncodeOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
if (!emscriptenModule) {
|
||||
emscriptenModule = initEmscriptenModule(jxlEncoder, wasmUrl);
|
||||
}
|
||||
|
||||
const module = await emscriptenModule;
|
||||
const result = module.encode(data.data, data.width, data.height, options);
|
||||
if (!result) throw new Error('Encoding error.');
|
||||
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||
return result.buffer as ArrayBuffer;
|
||||
}
|
||||
299
src/features/encoders/mozJPEG/client/index.tsx
Normal file
299
src/features/encoders/mozJPEG/client/index.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import { EncodeOptions, MozJpegColorSpace } from '../shared/meta';
|
||||
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||
import { h, Component } from 'preact';
|
||||
import {
|
||||
inputFieldChecked,
|
||||
inputFieldValueAsNumber,
|
||||
preventDefault,
|
||||
} from 'client/lazy-app/util';
|
||||
import * as style from 'client/lazy-app/Compress/Options/style.css';
|
||||
import linkState from 'linkstate';
|
||||
import Range from 'client/lazy-app/Compress/Options/Range';
|
||||
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
|
||||
import Expander from 'client/lazy-app/Compress/Options/Expander';
|
||||
import Select from 'client/lazy-app/Compress/Options/Select';
|
||||
|
||||
export function encode(
|
||||
signal: AbortSignal,
|
||||
workerBridge: WorkerBridge,
|
||||
imageData: ImageData,
|
||||
options: EncodeOptions,
|
||||
) {
|
||||
return workerBridge.mozjpegEncode(signal, imageData, options);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
options: EncodeOptions;
|
||||
onChange(newOptions: EncodeOptions): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
showAdvanced: boolean;
|
||||
}
|
||||
|
||||
export class Options extends Component<Props, State> {
|
||||
state: State = {
|
||||
showAdvanced: false,
|
||||
};
|
||||
|
||||
onChange = (event: Event) => {
|
||||
const form = (event.currentTarget as HTMLInputElement).closest(
|
||||
'form',
|
||||
) as HTMLFormElement;
|
||||
const { options } = this.props;
|
||||
|
||||
const newOptions: EncodeOptions = {
|
||||
// Copy over options the form doesn't currently care about, eg arithmetic
|
||||
...this.props.options,
|
||||
// And now stuff from the form:
|
||||
// .checked
|
||||
baseline: inputFieldChecked(form.baseline, options.baseline),
|
||||
progressive: inputFieldChecked(form.progressive, options.progressive),
|
||||
optimize_coding: inputFieldChecked(
|
||||
form.optimize_coding,
|
||||
options.optimize_coding,
|
||||
),
|
||||
trellis_multipass: inputFieldChecked(
|
||||
form.trellis_multipass,
|
||||
options.trellis_multipass,
|
||||
),
|
||||
trellis_opt_zero: inputFieldChecked(
|
||||
form.trellis_opt_zero,
|
||||
options.trellis_opt_zero,
|
||||
),
|
||||
trellis_opt_table: inputFieldChecked(
|
||||
form.trellis_opt_table,
|
||||
options.trellis_opt_table,
|
||||
),
|
||||
auto_subsample: inputFieldChecked(
|
||||
form.auto_subsample,
|
||||
options.auto_subsample,
|
||||
),
|
||||
separate_chroma_quality: inputFieldChecked(
|
||||
form.separate_chroma_quality,
|
||||
options.separate_chroma_quality,
|
||||
),
|
||||
// .value
|
||||
quality: inputFieldValueAsNumber(form.quality, options.quality),
|
||||
chroma_quality: inputFieldValueAsNumber(
|
||||
form.chroma_quality,
|
||||
options.chroma_quality,
|
||||
),
|
||||
chroma_subsample: inputFieldValueAsNumber(
|
||||
form.chroma_subsample,
|
||||
options.chroma_subsample,
|
||||
),
|
||||
smoothing: inputFieldValueAsNumber(form.smoothing, options.smoothing),
|
||||
color_space: inputFieldValueAsNumber(
|
||||
form.color_space,
|
||||
options.color_space,
|
||||
),
|
||||
quant_table: inputFieldValueAsNumber(
|
||||
form.quant_table,
|
||||
options.quant_table,
|
||||
),
|
||||
trellis_loops: inputFieldValueAsNumber(
|
||||
form.trellis_loops,
|
||||
options.trellis_loops,
|
||||
),
|
||||
};
|
||||
this.props.onChange(newOptions);
|
||||
};
|
||||
|
||||
render({ options }: Props, { showAdvanced }: State) {
|
||||
// I'm rendering both lossy and lossless forms, as it becomes much easier when
|
||||
// gathering the data.
|
||||
return (
|
||||
<form class={style.optionsSection} onSubmit={preventDefault}>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="quality"
|
||||
min="0"
|
||||
max="100"
|
||||
value={options.quality}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Quality:
|
||||
</Range>
|
||||
</div>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
checked={showAdvanced}
|
||||
onChange={linkState(this, 'showAdvanced')}
|
||||
/>
|
||||
Show advanced settings
|
||||
</label>
|
||||
<Expander>
|
||||
{showAdvanced ? (
|
||||
<div>
|
||||
<label class={style.optionTextFirst}>
|
||||
Channels:
|
||||
<Select
|
||||
name="color_space"
|
||||
value={options.color_space}
|
||||
onChange={this.onChange}
|
||||
>
|
||||
<option value={MozJpegColorSpace.GRAYSCALE}>Grayscale</option>
|
||||
<option value={MozJpegColorSpace.RGB}>RGB</option>
|
||||
<option value={MozJpegColorSpace.YCbCr}>YCbCr</option>
|
||||
</Select>
|
||||
</label>
|
||||
<Expander>
|
||||
{options.color_space === MozJpegColorSpace.YCbCr ? (
|
||||
<div>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="auto_subsample"
|
||||
checked={options.auto_subsample}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Auto subsample chroma
|
||||
</label>
|
||||
<Expander>
|
||||
{options.auto_subsample ? null : (
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="chroma_subsample"
|
||||
min="1"
|
||||
max="4"
|
||||
value={options.chroma_subsample}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Subsample chroma by:
|
||||
</Range>
|
||||
</div>
|
||||
)}
|
||||
</Expander>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="separate_chroma_quality"
|
||||
checked={options.separate_chroma_quality}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Separate chroma quality
|
||||
</label>
|
||||
<Expander>
|
||||
{options.separate_chroma_quality ? (
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="chroma_quality"
|
||||
min="0"
|
||||
max="100"
|
||||
value={options.chroma_quality}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Chroma quality:
|
||||
</Range>
|
||||
</div>
|
||||
) : null}
|
||||
</Expander>
|
||||
</div>
|
||||
) : null}
|
||||
</Expander>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="baseline"
|
||||
checked={options.baseline}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Pointless spec compliance
|
||||
</label>
|
||||
<Expander>
|
||||
{options.baseline ? null : (
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="progressive"
|
||||
checked={options.progressive}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Progressive rendering
|
||||
</label>
|
||||
)}
|
||||
</Expander>
|
||||
<Expander>
|
||||
{options.baseline ? (
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="optimize_coding"
|
||||
checked={options.optimize_coding}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Optimize Huffman table
|
||||
</label>
|
||||
) : null}
|
||||
</Expander>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="smoothing"
|
||||
min="0"
|
||||
max="100"
|
||||
value={options.smoothing}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Smoothing:
|
||||
</Range>
|
||||
</div>
|
||||
<label class={style.optionTextFirst}>
|
||||
Quantization:
|
||||
<Select
|
||||
name="quant_table"
|
||||
value={options.quant_table}
|
||||
onChange={this.onChange}
|
||||
>
|
||||
<option value="0">JPEG Annex K</option>
|
||||
<option value="1">Flat</option>
|
||||
<option value="2">MSSIM-tuned Kodak</option>
|
||||
<option value="3">ImageMagick</option>
|
||||
<option value="4">PSNR-HVS-M-tuned Kodak</option>
|
||||
<option value="5">Klein et al</option>
|
||||
<option value="6">Watson et al</option>
|
||||
<option value="7">Ahumada et al</option>
|
||||
<option value="8">Peterson et al</option>
|
||||
</Select>
|
||||
</label>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="trellis_multipass"
|
||||
checked={options.trellis_multipass}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Trellis multipass
|
||||
</label>
|
||||
<Expander>
|
||||
{options.trellis_multipass ? (
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="trellis_opt_zero"
|
||||
checked={options.trellis_opt_zero}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Optimize zero block runs
|
||||
</label>
|
||||
) : null}
|
||||
</Expander>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="trellis_opt_table"
|
||||
checked={options.trellis_opt_table}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Optimize after trellis quantization
|
||||
</label>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="trellis_loops"
|
||||
min="1"
|
||||
max="50"
|
||||
value={options.trellis_loops}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Trellis quantization passes:
|
||||
</Range>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Expander>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
13
src/features/encoders/mozJPEG/client/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/mozJPEG/client/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
39
src/features/encoders/mozJPEG/shared/meta.ts
Normal file
39
src/features/encoders/mozJPEG/shared/meta.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
EncodeOptions,
|
||||
MozJpegColorSpace,
|
||||
} from 'codecs/mozjpeg_enc/mozjpeg_enc';
|
||||
export { EncodeOptions, MozJpegColorSpace };
|
||||
|
||||
export const label = 'MozJPEG';
|
||||
export const mimeType = 'image/jpeg';
|
||||
export const extension = 'jpg';
|
||||
export const defaultOptions: EncodeOptions = {
|
||||
quality: 75,
|
||||
baseline: false,
|
||||
arithmetic: false,
|
||||
progressive: true,
|
||||
optimize_coding: true,
|
||||
smoothing: 0,
|
||||
color_space: MozJpegColorSpace.YCbCr,
|
||||
quant_table: 3,
|
||||
trellis_multipass: false,
|
||||
trellis_opt_zero: false,
|
||||
trellis_opt_table: false,
|
||||
trellis_loops: 1,
|
||||
auto_subsample: true,
|
||||
chroma_subsample: 2,
|
||||
separate_chroma_quality: false,
|
||||
chroma_quality: 75,
|
||||
};
|
||||
13
src/features/encoders/mozJPEG/shared/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/mozJPEG/shared/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
13
src/features/encoders/mozJPEG/worker/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/mozJPEG/worker/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
32
src/features/encoders/mozJPEG/worker/mozjpegEncode.ts
Normal file
32
src/features/encoders/mozJPEG/worker/mozjpegEncode.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import mozjpeg_enc, { MozJPEGModule } from 'codecs/mozjpeg_enc/mozjpeg_enc';
|
||||
import { EncodeOptions } from '../shared/meta';
|
||||
import wasmUrl from 'url:codecs/mozjpeg_enc/mozjpeg_enc.wasm';
|
||||
import { initEmscriptenModule } from 'features/worker-utils';
|
||||
|
||||
let emscriptenModule: Promise<MozJPEGModule>;
|
||||
|
||||
export default async function encode(
|
||||
data: ImageData,
|
||||
options: EncodeOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
if (!emscriptenModule) {
|
||||
emscriptenModule = initEmscriptenModule(mozjpeg_enc, wasmUrl);
|
||||
}
|
||||
|
||||
const module = await emscriptenModule;
|
||||
const resultView = module.encode(data.data, data.width, data.height, options);
|
||||
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||
return resultView.buffer as ArrayBuffer;
|
||||
}
|
||||
59
src/features/encoders/oxiPNG/client/index.tsx
Normal file
59
src/features/encoders/oxiPNG/client/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
canvasEncode,
|
||||
abortable,
|
||||
blobToArrayBuffer,
|
||||
} from 'client/lazy-app/util';
|
||||
import { EncodeOptions } from '../shared/meta';
|
||||
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||
import { h, Component } from 'preact';
|
||||
import { inputFieldValueAsNumber, preventDefault } from 'client/lazy-app/util';
|
||||
import * as style from 'client/lazy-app/Compress/Options/style.css';
|
||||
import Range from 'client/lazy-app/Compress/Options/Range';
|
||||
|
||||
export async function encode(
|
||||
signal: AbortSignal,
|
||||
workerBridge: WorkerBridge,
|
||||
imageData: ImageData,
|
||||
options: EncodeOptions,
|
||||
) {
|
||||
const pngBlob = await abortable(signal, canvasEncode(imageData, 'image/png'));
|
||||
const pngBuffer = await abortable(signal, blobToArrayBuffer(pngBlob));
|
||||
return workerBridge.oxipngEncode(signal, pngBuffer, options);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
options: EncodeOptions;
|
||||
onChange(newOptions: EncodeOptions): void;
|
||||
};
|
||||
|
||||
export class Options extends Component<Props, {}> {
|
||||
onChange = (event: Event) => {
|
||||
const form = (event.currentTarget as HTMLInputElement).closest(
|
||||
'form',
|
||||
) as HTMLFormElement;
|
||||
|
||||
const options: EncodeOptions = {
|
||||
level: inputFieldValueAsNumber(form.level),
|
||||
};
|
||||
this.props.onChange(options);
|
||||
};
|
||||
|
||||
render({ options }: Props) {
|
||||
return (
|
||||
<form class={style.optionsSection} onSubmit={preventDefault}>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="level"
|
||||
min="0"
|
||||
max="3"
|
||||
step="1"
|
||||
value={options.level}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Effort:
|
||||
</Range>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
23
src/features/encoders/oxiPNG/shared/meta.ts
Normal file
23
src/features/encoders/oxiPNG/shared/meta.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface EncodeOptions {
|
||||
level: number;
|
||||
}
|
||||
|
||||
export const label = 'OxiPNG';
|
||||
export const mimeType = 'image/png';
|
||||
export const extension = 'png';
|
||||
|
||||
export const defaultOptions: EncodeOptions = {
|
||||
level: 2,
|
||||
};
|
||||
85
src/features/encoders/oxiPNG/worker/oxipngEncode.ts
Normal file
85
src/features/encoders/oxiPNG/worker/oxipngEncode.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import initOxiWasmST, {
|
||||
optimise as optimiseST,
|
||||
} from 'codecs/oxipng/pkg/squoosh_oxipng';
|
||||
import initOxiWasmMT, {
|
||||
worker_initializer,
|
||||
start_main_thread,
|
||||
optimise as optimiseMT,
|
||||
} from 'codecs/oxipng/pkg-parallel/squoosh_oxipng';
|
||||
import oxiWasmUrlST from 'url:codecs/oxipng/pkg/squoosh_oxipng_bg.wasm';
|
||||
import oxiWasmUrlMT from 'url:codecs/oxipng/pkg-parallel/squoosh_oxipng_bg.wasm';
|
||||
import { EncodeOptions } from '../shared/meta';
|
||||
import { threads } from 'wasm-feature-detect';
|
||||
import workerURL from 'omt:./sub-worker';
|
||||
import type { WorkerInit } from './sub-worker';
|
||||
|
||||
function initWorker(worker: Worker, workerInit: WorkerInit) {
|
||||
return new Promise<void>((resolve) => {
|
||||
worker.postMessage(workerInit);
|
||||
worker.addEventListener('message', () => resolve(), { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
async function initMT() {
|
||||
const num = navigator.hardwareConcurrency;
|
||||
|
||||
// First, let browser fetch and spawn Workers for our pool in the background.
|
||||
// This is fairly expensive, so we want to start it as early as possible.
|
||||
const workers = Array.from({ length: num }, () => new Worker(workerURL));
|
||||
|
||||
// Meanwhile, asynchronously compile, instantiate and initialise Wasm on our main thread.
|
||||
await initOxiWasmMT(oxiWasmUrlMT);
|
||||
|
||||
// Get module+memory from the Wasm instance.
|
||||
//
|
||||
// Ideally we wouldn't go via Wasm bindings here, since both are just JS variables, but memory is
|
||||
// currently not exposed on the Wasm instance correctly by wasm-bindgen.
|
||||
const workerInit: WorkerInit = worker_initializer(num);
|
||||
|
||||
// Once done, we want to send module+memory to each Worker so that they instantiate Wasm too.
|
||||
// While doing so, we need to wait for Workers to acknowledge that they have received our message.
|
||||
// Ideally this shouldn't be necessary, but Chromium currently doesn't conform to the spec:
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1075645
|
||||
//
|
||||
// If we didn't do this ping-pong game, the `start_main_thread` below would block the current
|
||||
// thread on an atomic before even *sending* the `postMessage` containing memory,
|
||||
// so Workers would never be able to unblock us back.
|
||||
await Promise.all(workers.map((worker) => initWorker(worker, workerInit)));
|
||||
|
||||
// Finally, instantiate rayon pool - this will use shared Wasm memory to send tasks to the
|
||||
// Workers and then block until they're all ready.
|
||||
start_main_thread();
|
||||
|
||||
return optimiseMT;
|
||||
}
|
||||
|
||||
async function initST() {
|
||||
await initOxiWasmST(oxiWasmUrlST);
|
||||
return optimiseST;
|
||||
}
|
||||
|
||||
let wasmReady: Promise<typeof optimiseMT | typeof optimiseST>;
|
||||
|
||||
export default async function encode(
|
||||
data: ArrayBuffer,
|
||||
options: EncodeOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
if (!wasmReady) {
|
||||
wasmReady = (await threads()) ? initMT() : initST();
|
||||
}
|
||||
|
||||
const optimise = await wasmReady;
|
||||
return optimise(new Uint8Array(data), options.level).buffer;
|
||||
}
|
||||
25
src/features/encoders/oxiPNG/worker/sub-worker/index.ts
Normal file
25
src/features/encoders/oxiPNG/worker/sub-worker/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import initOxiPNG, {
|
||||
start_worker_thread,
|
||||
} from 'codecs/oxipng/pkg-parallel/squoosh_oxipng';
|
||||
|
||||
export type WorkerInit = [WebAssembly.Module, WebAssembly.Memory];
|
||||
|
||||
addEventListener(
|
||||
'message',
|
||||
async (event) => {
|
||||
// Tell the "main" thread that we've received the message.
|
||||
//
|
||||
// At this point, the "main" thread can run Wasm that
|
||||
// will synchronously block waiting on other atomics.
|
||||
//
|
||||
// Note that we don't need to wait for Wasm instantiation here - it's
|
||||
// better to start main thread as early as possible, and then it blocks
|
||||
// on a shared atomic anyway until Worker is fully ready.
|
||||
// @ts-ignore
|
||||
postMessage(null);
|
||||
|
||||
await initOxiPNG(...(event.data as WorkerInit));
|
||||
start_worker_thread();
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
407
src/features/encoders/webP/client/index.tsx
Normal file
407
src/features/encoders/webP/client/index.tsx
Normal file
@@ -0,0 +1,407 @@
|
||||
import { EncodeOptions } from '../shared/meta';
|
||||
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||
import { h, Component } from 'preact';
|
||||
import {
|
||||
inputFieldCheckedAsNumber,
|
||||
inputFieldValueAsNumber,
|
||||
preventDefault,
|
||||
} from 'client/lazy-app/util';
|
||||
import * as style from 'client/lazy-app/Compress/Options/style.css';
|
||||
import linkState from 'linkstate';
|
||||
import Range from 'client/lazy-app/Compress/Options/Range';
|
||||
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
|
||||
import Expander from 'client/lazy-app/Compress/Options/Expander';
|
||||
import Select from 'client/lazy-app/Compress/Options/Select';
|
||||
|
||||
export const encode = (
|
||||
signal: AbortSignal,
|
||||
workerBridge: WorkerBridge,
|
||||
imageData: ImageData,
|
||||
options: EncodeOptions,
|
||||
) => workerBridge.webpEncode(signal, imageData, options);
|
||||
|
||||
const enum WebPImageHint {
|
||||
WEBP_HINT_DEFAULT, // default preset.
|
||||
WEBP_HINT_PICTURE, // digital picture, like portrait, inner shot
|
||||
WEBP_HINT_PHOTO, // outdoor photograph, with natural lighting
|
||||
WEBP_HINT_GRAPH, // Discrete tone image (graph, map-tile etc).
|
||||
}
|
||||
|
||||
interface Props {
|
||||
options: EncodeOptions;
|
||||
onChange(newOptions: EncodeOptions): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
showAdvanced: boolean;
|
||||
}
|
||||
|
||||
// From kLosslessPresets in config_enc.c
|
||||
// The format is [method, quality].
|
||||
const losslessPresets: [number, number][] = [
|
||||
[0, 0],
|
||||
[1, 20],
|
||||
[2, 25],
|
||||
[3, 30],
|
||||
[3, 50],
|
||||
[4, 50],
|
||||
[4, 75],
|
||||
[4, 90],
|
||||
[5, 90],
|
||||
[6, 100],
|
||||
];
|
||||
const losslessPresetDefault = 6;
|
||||
|
||||
function determineLosslessQuality(quality: number, method: number): number {
|
||||
const index = losslessPresets.findIndex(
|
||||
([presetMethod, presetQuality]) =>
|
||||
presetMethod === method && presetQuality === quality,
|
||||
);
|
||||
if (index !== -1) return index;
|
||||
// Quality doesn't match one of the presets.
|
||||
// This can happen when toggling 'lossless'.
|
||||
return losslessPresetDefault;
|
||||
}
|
||||
|
||||
export class Options extends Component<Props, State> {
|
||||
state: State = {
|
||||
showAdvanced: false,
|
||||
};
|
||||
|
||||
onChange = (event: Event) => {
|
||||
const form = (event.currentTarget as HTMLInputElement).closest(
|
||||
'form',
|
||||
) as HTMLFormElement;
|
||||
const lossless = inputFieldCheckedAsNumber(form.lossless);
|
||||
const { options } = this.props;
|
||||
const losslessPresetValue = inputFieldValueAsNumber(
|
||||
form.lossless_preset,
|
||||
determineLosslessQuality(options.quality, options.method),
|
||||
);
|
||||
|
||||
const newOptions: EncodeOptions = {
|
||||
// Copy over options the form doesn't care about, eg emulate_jpeg_size
|
||||
...options,
|
||||
// And now stuff from the form:
|
||||
lossless,
|
||||
// Special-cased inputs:
|
||||
// In lossless mode, the quality is derived from the preset.
|
||||
quality: lossless
|
||||
? losslessPresets[losslessPresetValue][1]
|
||||
: inputFieldValueAsNumber(form.quality, options.quality),
|
||||
// In lossless mode, the method is derived from the preset.
|
||||
method: lossless
|
||||
? losslessPresets[losslessPresetValue][0]
|
||||
: inputFieldValueAsNumber(form.method_input, options.method),
|
||||
image_hint: inputFieldCheckedAsNumber(form.image_hint, options.image_hint)
|
||||
? WebPImageHint.WEBP_HINT_GRAPH
|
||||
: WebPImageHint.WEBP_HINT_DEFAULT,
|
||||
// .checked
|
||||
exact: inputFieldCheckedAsNumber(form.exact, options.exact),
|
||||
alpha_compression: inputFieldCheckedAsNumber(
|
||||
form.alpha_compression,
|
||||
options.alpha_compression,
|
||||
),
|
||||
autofilter: inputFieldCheckedAsNumber(
|
||||
form.autofilter,
|
||||
options.autofilter,
|
||||
),
|
||||
filter_type: inputFieldCheckedAsNumber(
|
||||
form.filter_type,
|
||||
options.filter_type,
|
||||
),
|
||||
use_sharp_yuv: inputFieldCheckedAsNumber(
|
||||
form.use_sharp_yuv,
|
||||
options.use_sharp_yuv,
|
||||
),
|
||||
// .value
|
||||
near_lossless:
|
||||
100 -
|
||||
inputFieldValueAsNumber(
|
||||
form.near_lossless,
|
||||
100 - options.near_lossless,
|
||||
),
|
||||
alpha_quality: inputFieldValueAsNumber(
|
||||
form.alpha_quality,
|
||||
options.alpha_quality,
|
||||
),
|
||||
alpha_filtering: inputFieldValueAsNumber(
|
||||
form.alpha_filtering,
|
||||
options.alpha_filtering,
|
||||
),
|
||||
sns_strength: inputFieldValueAsNumber(
|
||||
form.sns_strength,
|
||||
options.sns_strength,
|
||||
),
|
||||
filter_strength: inputFieldValueAsNumber(
|
||||
form.filter_strength,
|
||||
options.filter_strength,
|
||||
),
|
||||
filter_sharpness:
|
||||
7 -
|
||||
inputFieldValueAsNumber(
|
||||
form.filter_sharpness,
|
||||
7 - options.filter_sharpness,
|
||||
),
|
||||
pass: inputFieldValueAsNumber(form.pass, options.pass),
|
||||
preprocessing: inputFieldValueAsNumber(
|
||||
form.preprocessing,
|
||||
options.preprocessing,
|
||||
),
|
||||
segments: inputFieldValueAsNumber(form.segments, options.segments),
|
||||
partitions: inputFieldValueAsNumber(form.partitions, options.partitions),
|
||||
};
|
||||
this.props.onChange(newOptions);
|
||||
};
|
||||
|
||||
private _losslessSpecificOptions(options: EncodeOptions) {
|
||||
return (
|
||||
<div key="lossless">
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="lossless_preset"
|
||||
min="0"
|
||||
max="9"
|
||||
value={determineLosslessQuality(options.quality, options.method)}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Effort:
|
||||
</Range>
|
||||
</div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="near_lossless"
|
||||
min="0"
|
||||
max="100"
|
||||
value={'' + (100 - options.near_lossless)}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Slight loss:
|
||||
</Range>
|
||||
</div>
|
||||
<label class={style.optionInputFirst}>
|
||||
{/*
|
||||
Although there are 3 different kinds of image hint, webp only
|
||||
seems to do something with the 'graph' type, and I don't really
|
||||
understand what it does.
|
||||
*/}
|
||||
<Checkbox
|
||||
name="image_hint"
|
||||
checked={options.image_hint === WebPImageHint.WEBP_HINT_GRAPH}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Discrete tone image
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _lossySpecificOptions(options: EncodeOptions) {
|
||||
const { showAdvanced } = this.state;
|
||||
|
||||
return (
|
||||
<div key="lossy">
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="method_input"
|
||||
min="0"
|
||||
max="6"
|
||||
value={options.method}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Effort:
|
||||
</Range>
|
||||
</div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="quality"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
value={options.quality}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Quality:
|
||||
</Range>
|
||||
</div>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
checked={showAdvanced}
|
||||
onChange={linkState(this, 'showAdvanced')}
|
||||
/>
|
||||
Show advanced settings
|
||||
</label>
|
||||
<Expander>
|
||||
{showAdvanced ? (
|
||||
<div>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="alpha_compression"
|
||||
checked={!!options.alpha_compression}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Compress alpha
|
||||
</label>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="alpha_quality"
|
||||
min="0"
|
||||
max="100"
|
||||
value={options.alpha_quality}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Alpha quality:
|
||||
</Range>
|
||||
</div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="alpha_filtering"
|
||||
min="0"
|
||||
max="2"
|
||||
value={options.alpha_filtering}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Alpha filter quality:
|
||||
</Range>
|
||||
</div>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="autofilter"
|
||||
checked={!!options.autofilter}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Auto adjust filter strength
|
||||
</label>
|
||||
<Expander>
|
||||
{options.autofilter ? null : (
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="filter_strength"
|
||||
min="0"
|
||||
max="100"
|
||||
value={options.filter_strength}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Filter strength:
|
||||
</Range>
|
||||
</div>
|
||||
)}
|
||||
</Expander>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="filter_type"
|
||||
checked={!!options.filter_type}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Strong filter
|
||||
</label>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="filter_sharpness"
|
||||
min="0"
|
||||
max="7"
|
||||
value={7 - options.filter_sharpness}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Filter sharpness:
|
||||
</Range>
|
||||
</div>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="use_sharp_yuv"
|
||||
checked={!!options.use_sharp_yuv}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Sharp RGB→YUV conversion
|
||||
</label>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="pass"
|
||||
min="1"
|
||||
max="10"
|
||||
value={options.pass}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Passes:
|
||||
</Range>
|
||||
</div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="sns_strength"
|
||||
min="0"
|
||||
max="100"
|
||||
value={options.sns_strength}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Spatial noise shaping:
|
||||
</Range>
|
||||
</div>
|
||||
<label class={style.optionTextFirst}>
|
||||
Preprocess:
|
||||
<Select
|
||||
name="preprocessing"
|
||||
value={options.preprocessing}
|
||||
onChange={this.onChange}
|
||||
>
|
||||
<option value="0">None</option>
|
||||
<option value="1">Segment smooth</option>
|
||||
<option value="2">Pseudo-random dithering</option>
|
||||
</Select>
|
||||
</label>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="segments"
|
||||
min="1"
|
||||
max="4"
|
||||
value={options.segments}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Segments:
|
||||
</Range>
|
||||
</div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="partitions"
|
||||
min="0"
|
||||
max="3"
|
||||
value={options.partitions}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Partitions:
|
||||
</Range>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Expander>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render({ options }: Props) {
|
||||
// I'm rendering both lossy and lossless forms, as it becomes much easier when
|
||||
// gathering the data.
|
||||
return (
|
||||
<form class={style.optionsSection} onSubmit={preventDefault}>
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="lossless"
|
||||
checked={!!options.lossless}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Lossless
|
||||
</label>
|
||||
{options.lossless
|
||||
? this._losslessSpecificOptions(options)
|
||||
: this._lossySpecificOptions(options)}
|
||||
<label class={style.optionInputFirst}>
|
||||
<Checkbox
|
||||
name="exact"
|
||||
checked={!!options.exact}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Preserve transparent data
|
||||
</label>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
49
src/features/encoders/webP/shared/meta.ts
Normal file
49
src/features/encoders/webP/shared/meta.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { EncodeOptions } from 'codecs/webp/enc/webp_enc';
|
||||
|
||||
export { EncodeOptions };
|
||||
|
||||
export const label = 'WebP';
|
||||
export const mimeType = 'image/webp';
|
||||
export const extension = 'webp';
|
||||
// These come from struct WebPConfig in encode.h.
|
||||
export const defaultOptions: EncodeOptions = {
|
||||
quality: 75,
|
||||
target_size: 0,
|
||||
target_PSNR: 0,
|
||||
method: 4,
|
||||
sns_strength: 50,
|
||||
filter_strength: 60,
|
||||
filter_sharpness: 0,
|
||||
filter_type: 1,
|
||||
partitions: 0,
|
||||
segments: 4,
|
||||
pass: 1,
|
||||
show_compressed: 0,
|
||||
preprocessing: 0,
|
||||
autofilter: 0,
|
||||
partition_limit: 0,
|
||||
alpha_compression: 1,
|
||||
alpha_filtering: 1,
|
||||
alpha_quality: 100,
|
||||
lossless: 0,
|
||||
exact: 0,
|
||||
image_hint: 0,
|
||||
emulate_jpeg_size: 0,
|
||||
thread_level: 0,
|
||||
low_memory: 0,
|
||||
near_lossless: 100,
|
||||
use_delta_palette: 0,
|
||||
use_sharp_yuv: 0,
|
||||
};
|
||||
33
src/features/encoders/webP/worker/webpEncode.ts
Normal file
33
src/features/encoders/webP/worker/webpEncode.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import webpEncoder, { WebPModule } from 'codecs/webp/enc/webp_enc';
|
||||
import wasmUrl from 'url:codecs/webp/enc/webp_enc.wasm';
|
||||
import { initEmscriptenModule } from 'features/worker-utils';
|
||||
import type { EncodeOptions } from '../shared/meta';
|
||||
|
||||
let emscriptenModule: Promise<WebPModule>;
|
||||
|
||||
export default async function encode(
|
||||
data: ImageData,
|
||||
options: EncodeOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
if (!emscriptenModule) {
|
||||
emscriptenModule = initEmscriptenModule(webpEncoder, wasmUrl);
|
||||
}
|
||||
|
||||
const module = await emscriptenModule;
|
||||
const result = module.encode(data.data, data.width, data.height, options);
|
||||
if (!result) throw new Error('Encoding error.');
|
||||
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||
return result.buffer as ArrayBuffer;
|
||||
}
|
||||
113
src/features/encoders/wp2/client/index.tsx
Normal file
113
src/features/encoders/wp2/client/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { EncodeOptions } from '../shared/meta';
|
||||
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||
import { h, Component } from 'preact';
|
||||
import { inputFieldValueAsNumber, preventDefault } from 'client/lazy-app/util';
|
||||
import * as style from 'client/lazy-app/Compress/Options/style.css';
|
||||
import Range from 'client/lazy-app/Compress/Options/Range';
|
||||
|
||||
export const encode = (
|
||||
signal: AbortSignal,
|
||||
workerBridge: WorkerBridge,
|
||||
imageData: ImageData,
|
||||
options: EncodeOptions,
|
||||
) => workerBridge.wp2Encode(signal, imageData, options);
|
||||
|
||||
interface Props {
|
||||
options: EncodeOptions;
|
||||
onChange(newOptions: EncodeOptions): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
showAdvanced: boolean;
|
||||
}
|
||||
|
||||
export class Options extends Component<Props, State> {
|
||||
state: State = {
|
||||
showAdvanced: false,
|
||||
};
|
||||
|
||||
private onChange = (event: Event) => {
|
||||
const form = (event.currentTarget as HTMLInputElement).closest(
|
||||
'form',
|
||||
) as HTMLFormElement;
|
||||
const { options } = this.props;
|
||||
const newOptions: EncodeOptions = {
|
||||
quality: inputFieldValueAsNumber(form.quality, options.quality),
|
||||
alpha_quality: inputFieldValueAsNumber(
|
||||
form.alpha_quality,
|
||||
options.alpha_quality,
|
||||
),
|
||||
speed: inputFieldValueAsNumber(form.speed, options.speed),
|
||||
pass: inputFieldValueAsNumber(form.pass, options.pass),
|
||||
sns: inputFieldValueAsNumber(form.sns, options.sns),
|
||||
};
|
||||
this.props.onChange(newOptions);
|
||||
};
|
||||
|
||||
render({ options }: Props) {
|
||||
return (
|
||||
<form class={style.optionsSection} onSubmit={preventDefault}>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="quality"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value={options.quality}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Quality:
|
||||
</Range>
|
||||
</div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="alpha_quality"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value={options.alpha_quality}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Alpha Quality:
|
||||
</Range>
|
||||
</div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="speed"
|
||||
min="0"
|
||||
max="9"
|
||||
step="1"
|
||||
value={options.speed}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Speed:
|
||||
</Range>
|
||||
</div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="pass"
|
||||
min="1"
|
||||
max="10"
|
||||
step="1"
|
||||
value={options.pass}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Pass:
|
||||
</Range>
|
||||
</div>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="sns"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value={options.sns}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Spatial noise shaping:
|
||||
</Range>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
26
src/features/encoders/wp2/shared/meta.ts
Normal file
26
src/features/encoders/wp2/shared/meta.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { EncodeOptions } from 'codecs/wp2/enc/wp2_enc';
|
||||
|
||||
export { EncodeOptions };
|
||||
|
||||
export const label = 'WebP v2 (unstable)';
|
||||
export const mimeType = 'image/webp2';
|
||||
export const extension = 'wp2';
|
||||
export const defaultOptions: EncodeOptions = {
|
||||
quality: 75,
|
||||
alpha_quality: 100,
|
||||
speed: 5,
|
||||
pass: 1,
|
||||
sns: 50,
|
||||
};
|
||||
33
src/features/encoders/wp2/worker/wp2Encode.ts
Normal file
33
src/features/encoders/wp2/worker/wp2Encode.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import wp2Encoder, { WP2Module } from 'codecs/wp2/enc/wp2_enc';
|
||||
import wasmUrl from 'url:codecs/wp2/enc/wp2_enc.wasm';
|
||||
import { initEmscriptenModule } from 'features/worker-utils';
|
||||
import type { EncodeOptions } from '../shared/meta';
|
||||
|
||||
let emscriptenModule: Promise<WP2Module>;
|
||||
|
||||
export default async function encode(
|
||||
data: ImageData,
|
||||
options: EncodeOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
if (!emscriptenModule) {
|
||||
emscriptenModule = initEmscriptenModule(wp2Encoder, wasmUrl);
|
||||
}
|
||||
|
||||
const module = await emscriptenModule;
|
||||
const result = module.encode(data.data, data.width, data.height, options);
|
||||
if (!result) throw new Error('Encoding error.');
|
||||
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||
return result.buffer as ArrayBuffer;
|
||||
}
|
||||
Reference in New Issue
Block a user