Merge remote-tracking branch 'origin/dev' into visdf

This commit is contained in:
Surma
2020-12-05 17:26:02 +00:00
403 changed files with 26989 additions and 19732 deletions

65
src/features/README.md Normal file
View File

@@ -0,0 +1,65 @@
# Feature types
- **decoders** - decode images.
- **encoders** - encode images.
- **processors** - change images, generally in a way that potentially aids compression.
- **preprocessors** - prepares the image for handling.
The key difference between preprocessors and processors is each 'side' in Squoosh can process differently, whereas a preprocessor happens to both sides.
# Adding code to the worker
Any feature can have a `worker` folder. Any script in that folder will have its default export bundled into the worker, using the name of the file.
So, `processors/shout/worker/shout.ts`:
```ts
export default function () {
console.log('OI YOU');
}
```
…will be bundled into the worker and exposed via comlink as `shout()`.
# Folders
Within a feature, files in the:
- `client` folder will be part of the client project.
- `worker` folder will be part of the worker project.
- `shared` folder will be part of the shared project. Both the client and worker projects can access the shared project.
# Encoder format
Encoders must have the following:
`shared/meta.ts` which exposes the following:
- `label` - The name of the codec as displayed to the user.
- `mimeType` - The mime type to be used when generating the output file.
- `extension` - The file extension to be used when generating the output file.
- `EncodeOptions` - An interface for the codec's options.
- `defaultOptions` - An object of type `EncodeOptions`.
`client/index.ts` which exposes the following:
- `encode` - A method which takes args:
- `AbortSignal`
- `WorkerBridge`
- `ImageData`
- `EncodeOptions`
And returns (a promise for) an `ArrayBuffer`.
Optionally it may export a method `featureTest`, which returns a boolean indicating support for this decoder.
Optionally it may export a component, `Options`, with the following props:
```ts
interface Props {
options: EncodeOptions;
onChange(newOptions: EncodeOptions): void;
}
```
…where `EncodeOptions` are the options for that encoder.

View File

@@ -0,0 +1,58 @@
import { h, Component } from 'preact';
import * as style from 'client/lazy-app/Compress/Options/style.css';
import Range from 'client/lazy-app/Compress/Options/Range';
interface EncodeOptions {
quality: number;
}
interface Props {
options: EncodeOptions;
onChange(newOptions: EncodeOptions): void;
}
interface QualityOptionArg {
min?: number;
max?: number;
step?: number;
}
type Constructor<T extends {} = {}> = new (...args: any[]) => T;
// TypeScript requires an exported type for returned classes. This serves as the
// type for the class returned by `qualityOption`.
export interface QualityOptionsInterface extends Component<Props, {}> {}
export function qualityOption(
opts: QualityOptionArg = {},
): Constructor<QualityOptionsInterface> {
const { min = 0, max = 100, step = 1 } = opts;
class QualityOptions extends Component<Props, {}> {
onChange = (event: Event) => {
const el = event.currentTarget as HTMLInputElement;
this.props.onChange({ quality: Number(el.value) });
};
render({ options }: Props) {
return (
<div class={style.optionsSection}>
<div class={style.optionOneCell}>
<Range
name="quality"
min={min}
max={max}
step={step || 'any'}
value={options.quality}
onInput={this.onChange}
>
Quality:
</Range>
</div>
</div>
);
}
}
return QualityOptions;
}

View 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 avifDecoder, { AVIFModule } from 'codecs/avif/dec/avif_dec';
import wasmUrl from 'url:codecs/avif/dec/avif_dec.wasm';
import { initEmscriptenModule, blobToArrayBuffer } from 'features/worker-utils';
let emscriptenModule: Promise<AVIFModule>;
export default async function decode(blob: Blob): Promise<ImageData> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(avifDecoder, wasmUrl);
}
const [module, data] = await Promise.all([
emscriptenModule,
blobToArrayBuffer(blob),
]);
const result = module.decode(data);
if (!result) throw new Error('Decoding error');
return result;
}

View 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 jxlDecoder, { JXLModule } from 'codecs/jxl/dec/jxl_dec';
import wasmUrl from 'url:codecs/jxl/dec/jxl_dec.wasm';
import { initEmscriptenModule, blobToArrayBuffer } from 'features/worker-utils';
let emscriptenModule: Promise<JXLModule>;
export default async function decode(blob: Blob): Promise<ImageData> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(jxlDecoder, wasmUrl);
}
const [module, data] = await Promise.all([
emscriptenModule,
blobToArrayBuffer(blob),
]);
const result = module.decode(data);
if (!result) throw new Error('Decoding error');
return result;
}

View 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 webpDecoder, { WebPModule } from 'codecs/webp/dec/webp_dec';
import wasmUrl from 'url:codecs/webp/dec/webp_dec.wasm';
import { initEmscriptenModule, blobToArrayBuffer } from 'features/worker-utils';
let emscriptenModule: Promise<WebPModule>;
export default async function decode(blob: Blob): Promise<ImageData> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(webpDecoder, wasmUrl);
}
const [module, data] = await Promise.all([
emscriptenModule,
blobToArrayBuffer(blob),
]);
const result = module.decode(data);
if (!result) throw new Error('Decoding error');
return result;
}

View 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 wp2Decoder, { WP2Module } from 'codecs/wp2/dec/wp2_dec';
import wasmUrl from 'url:codecs/wp2/dec/wp2_dec.wasm';
import { initEmscriptenModule, blobToArrayBuffer } from 'features/worker-utils';
let emscriptenModule: Promise<WP2Module>;
export default async function decode(blob: Blob): Promise<ImageData> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(wp2Decoder, wasmUrl);
}
const [module, data] = await Promise.all([
emscriptenModule,
blobToArrayBuffer(blob),
]);
const result = module.decode(data);
if (!result) throw new Error('Decoding error');
return result;
}

View 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">Half</option>
{/*<option value="2">4:2:2</option>*/}
<option value="3">Off</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>
);
}
}

View 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,
};

View 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" />

View File

@@ -0,0 +1,48 @@
/**
* 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');
return result.buffer;
}

View 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" />

View 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);

View 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" />

View 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 = {};

View 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" />

View 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 });

View 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" />

View 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 };

View 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" />

View 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);

View 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" />

View 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 = {};

View 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" />

View File

@@ -0,0 +1,212 @@
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;
autoEdgePreservingFilter: 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 === -1 ? 2 : options.epf,
lossless: options.quality === 100,
slightLoss: options.lossyPalette,
autoEdgePreservingFilter: options.epf === -1,
};
}
// 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.autoEdgePreservingFilter
? -1
: 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,
autoEdgePreservingFilter,
}: 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>
<label class={style.optionInputFirst}>
<Checkbox
name="autoEdgeFilter"
checked={autoEdgePreservingFilter}
onChange={this._inputChange(
'autoEdgePreservingFilter',
'boolean',
)}
/>
Auto edge filter
</label>
<Expander>
{!autoEdgePreservingFilter && (
<div class={style.optionOneCell}>
<Range
min="0"
max="3"
value={edgePreservingFilter}
onInput={this._inputChange(
'edgePreservingFilter',
'number',
)}
>
Edge preserving filter:
</Range>
</div>
)}
</Expander>
</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>
);
}
}

View 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: 4,
quality: 75,
progressive: false,
epf: -1,
nearLossless: 0,
lossyPalette: false,
};

View File

@@ -0,0 +1,58 @@
/**
* 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 { JXLModule } from 'codecs/jxl/enc/jxl_enc';
import type { EncodeOptions } from '../shared/meta';
import { initEmscriptenModule } from 'features/worker-utils';
import { threads, simd } from 'wasm-feature-detect';
import wasmUrl from 'url:codecs/jxl/enc/jxl_enc.wasm';
import wasmUrlWithMT from 'url:codecs/jxl/enc/jxl_enc_mt.wasm';
import workerUrl from 'omt:codecs/jxl/enc/jxl_enc_mt.worker.js';
import wasmUrlWithMTAndSIMD from 'url:codecs/jxl/enc/jxl_enc_mt_simd.wasm';
import workerUrlWithSIMD from 'omt:codecs/jxl/enc/jxl_enc_mt_simd.worker.js';
let emscriptenModule: Promise<JXLModule>;
async function init() {
if (await threads()) {
if (await simd()) {
const jxlEncoder = await import('codecs/jxl/enc/jxl_enc_mt_simd');
return initEmscriptenModule(
jxlEncoder.default,
wasmUrlWithMTAndSIMD,
workerUrlWithSIMD,
);
}
const jxlEncoder = await import('codecs/jxl/enc/jxl_enc_mt');
return initEmscriptenModule(jxlEncoder.default, wasmUrlWithMT, workerUrl);
}
const jxlEncoder = await import('codecs/jxl/enc/jxl_enc');
return initEmscriptenModule(jxlEncoder.default, wasmUrl);
}
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.');
return result.buffer;
}

View 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>
);
}
}

View 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" />

View 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,
};

View 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" />

View 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" />

View 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 cant run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
return resultView.buffer as ArrayBuffer;
}

View 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>
);
}
}

View 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,
};

View 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;
}

View 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 },
);

View 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 RGBYUV 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>
);
}
}

View 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,
};

View File

@@ -0,0 +1,45 @@
/**
* 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 { WebPModule } from 'codecs/webp/enc/webp_enc';
import type { EncodeOptions } from '../shared/meta';
import { initEmscriptenModule } from 'features/worker-utils';
import { simd } from 'wasm-feature-detect';
import wasmUrl from 'url:codecs/webp/enc/webp_enc.wasm';
import wasmUrlWithSIMD from 'url:codecs/webp/enc/webp_enc_simd.wasm';
let emscriptenModule: Promise<WebPModule>;
async function init() {
if (await simd()) {
const webpEncoder = await import('codecs/webp/enc/webp_enc_simd');
return initEmscriptenModule(webpEncoder.default, wasmUrlWithSIMD);
}
const webpEncoder = await import('codecs/webp/enc/webp_enc');
return initEmscriptenModule(webpEncoder.default, wasmUrl);
}
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.');
return result.buffer;
}

View File

@@ -0,0 +1,311 @@
import { EncodeOptions, UVMode, Csp } from '../shared/meta';
import { 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 Range from 'client/lazy-app/Compress/Options/Range';
import Select from 'client/lazy-app/Compress/Options/Select';
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
import Expander from 'client/lazy-app/Compress/Options/Expander';
import linkState from 'linkstate';
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 {
options: EncodeOptions;
effort: number;
quality: number;
alphaQuality: number;
passes: number;
sns: number;
uvMode: number;
lossless: boolean;
slightLoss: number;
colorSpace: number;
errorDiffusion: number;
useRandomMatrix: boolean;
showAdvanced: boolean;
separateAlpha: boolean;
}
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 modifyState: Partial<State> = {
options,
effort: options.effort,
alphaQuality: options.alpha_quality,
passes: options.pass,
sns: options.sns,
uvMode: options.uv_mode,
colorSpace: options.csp_type,
errorDiffusion: options.error_diffusion,
useRandomMatrix: options.use_random_matrix,
separateAlpha: options.quality !== options.alpha_quality,
};
// If quality is > 95, it's lossless with slight loss
if (options.quality > 95) {
modifyState.lossless = true;
modifyState.slightLoss = 100 - options.quality;
} else {
modifyState.quality = options.quality;
modifyState.lossless = false;
}
return modifyState;
}
// Other state is set in getDerivedStateFromProps
state: State = {
lossless: false,
slightLoss: 0,
quality: defaultOptions.quality,
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,
};
const optionState = {
...this.state,
...newState,
};
const newOptions: EncodeOptions = {
effort: optionState.effort,
quality: optionState.lossless
? 100 - optionState.slightLoss
: optionState.quality,
alpha_quality: optionState.separateAlpha
? optionState.alphaQuality
: optionState.quality,
pass: optionState.passes,
sns: optionState.sns,
uv_mode: optionState.uvMode,
csp_type: optionState.colorSpace,
error_diffusion: optionState.errorDiffusion,
use_random_matrix: optionState.useRandomMatrix,
};
// 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,
alphaQuality,
passes,
quality,
sns,
uvMode,
lossless,
slightLoss,
colorSpace,
errorDiffusion,
useRandomMatrix,
separateAlpha,
showAdvanced,
}: 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 class={style.optionOneCell}>
<Range
min="0"
max="5"
step="0.1"
value={slightLoss}
onInput={this._inputChange('slightLoss', 'number')}
>
Slight loss:
</Range>
</div>
)}
</Expander>
<Expander>
{!lossless && (
<div>
<div class={style.optionOneCell}>
<Range
min="0"
max="95"
step="0.1"
value={quality}
onInput={this._inputChange('quality', 'number')}
>
Quality:
</Range>
</div>
<label class={style.optionInputFirst}>
<Checkbox
checked={separateAlpha}
onChange={this._inputChange('separateAlpha', 'boolean')}
/>
Separate alpha quality
</label>
<Expander>
{separateAlpha && (
<div class={style.optionOneCell}>
<Range
min="0"
max="100"
step="1"
value={alphaQuality}
onInput={this._inputChange('alphaQuality', 'number')}
>
Alpha Quality:
</Range>
</div>
)}
</Expander>
<label class={style.optionInputFirst}>
<Checkbox
checked={showAdvanced}
onChange={linkState(this, 'showAdvanced')}
/>
Show advanced settings
</label>
<Expander>
{showAdvanced && (
<div>
<div class={style.optionOneCell}>
<Range
min="1"
max="10"
step="1"
value={passes}
onInput={this._inputChange('passes', 'number')}
>
Passes:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
min="0"
max="100"
step="1"
value={sns}
onInput={this._inputChange('sns', 'number')}
>
Spatial noise shaping:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
min="0"
max="100"
step="1"
value={errorDiffusion}
onInput={this._inputChange('errorDiffusion', 'number')}
>
Error diffusion:
</Range>
</div>
<label class={style.optionTextFirst}>
Subsample chroma:
<Select
value={uvMode}
onInput={this._inputChange('uvMode', 'number')}
>
<option value={UVMode.UVModeAuto}>Auto</option>
<option value={UVMode.UVModeAdapt}>Vary</option>
<option value={UVMode.UVMode420}>Half</option>
<option value={UVMode.UVMode444}>Off</option>
</Select>
</label>
<label class={style.optionTextFirst}>
Color space:
<Select
value={colorSpace}
onInput={this._inputChange('colorSpace', 'number')}
>
<option value={Csp.kYCoCg}>YCoCg</option>
<option value={Csp.kYCbCr}>YCbCr</option>
<option value={Csp.kYIQ}>YIQ</option>
</Select>
</label>
<label class={style.optionInputFirst}>
<Checkbox
checked={useRandomMatrix}
onChange={this._inputChange(
'useRandomMatrix',
'boolean',
)}
/>
Random matrix
</label>
</div>
)}
</Expander>
</div>
)}
</Expander>
<div class={style.optionOneCell}>
<Range
min="0"
max="9"
step="1"
value={effort}
onInput={this._inputChange('effort', 'number')}
>
Effort:
</Range>
</div>
</form>
);
}
}

View File

@@ -0,0 +1,31 @@
/**
* 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';
import { UVMode, Csp } from 'codecs/wp2/enc/wp2_enc';
export { EncodeOptions, UVMode, Csp };
export const label = 'WebP v2 (unstable)';
export const mimeType = 'image/webp2';
export const extension = 'wp2';
export const defaultOptions: EncodeOptions = {
quality: 75,
alpha_quality: 75,
effort: 5,
pass: 1,
sns: 50,
uv_mode: UVMode.UVModeAuto,
csp_type: Csp.kYCoCg,
error_diffusion: 0,
use_random_matrix: false,
};

View File

@@ -0,0 +1,58 @@
/**
* 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 { WP2Module } from 'codecs/wp2/enc/wp2_enc';
import type { EncodeOptions } from '../shared/meta';
import { initEmscriptenModule } from 'features/worker-utils';
import { threads, simd } from 'wasm-feature-detect';
import wasmUrl from 'url:codecs/wp2/enc/wp2_enc.wasm';
import wasmUrlWithMT from 'url:codecs/wp2/enc/wp2_enc_mt.wasm';
import workerUrl from 'omt:codecs/wp2/enc/wp2_enc_mt.worker.js';
import wasmUrlWithMTAndSIMD from 'url:codecs/wp2/enc/wp2_enc_mt_simd.wasm';
import workerUrlWithSIMD from 'omt:codecs/wp2/enc/wp2_enc_mt_simd.worker.js';
let emscriptenModule: Promise<WP2Module>;
async function init() {
if (await threads()) {
if (await simd()) {
const wp2Encoder = await import('codecs/wp2/enc/wp2_enc_mt_simd');
return initEmscriptenModule(
wp2Encoder.default,
wasmUrlWithMTAndSIMD,
workerUrlWithSIMD,
);
}
const wp2Encoder = await import('codecs/wp2/enc/wp2_enc_mt');
return initEmscriptenModule(wp2Encoder.default, wasmUrlWithMT, workerUrl);
}
const wp2Encoder = await import('codecs/wp2/enc/wp2_enc');
return initEmscriptenModule(wp2Encoder.default, wasmUrl);
}
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.');
return result.buffer;
}

13
src/features/missing-types.d.ts vendored Normal file
View 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" />

View File

@@ -0,0 +1,19 @@
/**
* 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 Options {
rotate: 0 | 90 | 180 | 270;
}
export const defaultOptions: Options = {
rotate: 0,
};

View 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" />

View 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" />

View File

@@ -0,0 +1,60 @@
/**
* 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 wasmUrl from 'url:codecs/rotate/rotate.wasm';
import { Options } from '../shared/meta';
export interface RotateModuleInstance {
exports: {
memory: WebAssembly.Memory;
rotate(width: number, height: number, rotate: 0 | 90 | 180 | 270): void;
};
}
// We are loading a 500B module here. Loading the code to feature-detect
// `instantiateStreaming` probably takes longer to load than the time we save by
// using `instantiateStreaming` in the first place. So lets just use
// `ArrayBuffer`s here.
const instancePromise = fetch(wasmUrl)
.then((r) => r.arrayBuffer())
.then((buf) => WebAssembly.instantiate(buf));
export default async function rotate(
data: ImageData,
opts: Options,
): Promise<ImageData> {
const instance = (await instancePromise).instance as RotateModuleInstance;
// Number of wasm memory pages (á 64KiB) needed to store the image twice.
const bytesPerImage = data.width * data.height * 4;
const numPagesNeeded = Math.ceil((bytesPerImage * 2 + 8) / (64 * 1024));
// Only count full pages, just to be safe.
const numPagesAvailable = Math.floor(
instance.exports.memory.buffer.byteLength / (64 * 1024),
);
const additionalPagesToAllocate = numPagesNeeded - numPagesAvailable;
if (additionalPagesToAllocate > 0) {
instance.exports.memory.grow(additionalPagesToAllocate);
}
const view = new Uint8ClampedArray(instance.exports.memory.buffer);
view.set(data.data, 8);
instance.exports.rotate(data.width, data.height, opts.rotate);
const flipDimensions = opts.rotate % 180 !== 0;
return new ImageData(
view.slice(bytesPerImage + 8, bytesPerImage * 2 + 8),
flipDimensions ? data.height : data.width,
flipDimensions ? data.width : data.height,
);
}

View File

@@ -0,0 +1,98 @@
import { h, Component } from 'preact';
import { Options as QuantizeOptions } from '../shared/meta';
import * as style from 'client/lazy-app/Compress/Options/style.css';
import {
inputFieldValueAsNumber,
konami,
preventDefault,
} from 'client/lazy-app/util';
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';
const konamiPromise = konami();
interface Props {
options: QuantizeOptions;
onChange(newOptions: QuantizeOptions): void;
}
interface State {
extendedSettings: boolean;
}
export class Options extends Component<Props, State> {
state: State = { extendedSettings: false };
componentDidMount() {
konamiPromise.then(() => {
this.setState({ extendedSettings: true });
});
}
onChange = (event: Event) => {
const form = (event.currentTarget as HTMLInputElement).closest(
'form',
) as HTMLFormElement;
const { options } = this.props;
const newOptions: QuantizeOptions = {
zx: inputFieldValueAsNumber(form.zx, options.zx),
maxNumColors: inputFieldValueAsNumber(
form.maxNumColors,
options.maxNumColors,
),
dither: inputFieldValueAsNumber(form.dither),
};
this.props.onChange(newOptions);
};
render({ options }: Props, { extendedSettings }: State) {
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<Expander>
{extendedSettings ? (
<label class={style.optionTextFirst}>
Type:
<Select
name="zx"
value={'' + options.zx}
onChange={this.onChange}
>
<option value="0">Standard</option>
<option value="1">ZX</option>
</Select>
</label>
) : null}
</Expander>
<Expander>
{options.zx ? null : (
<div class={style.optionOneCell}>
<Range
name="maxNumColors"
min="2"
max="256"
value={options.maxNumColors}
onInput={this.onChange}
>
Colors:
</Range>
</div>
)}
</Expander>
<div class={style.optionOneCell}>
<Range
name="dither"
min="0"
max="1"
step="0.01"
value={options.dither}
onInput={this.onChange}
>
Dithering:
</Range>
</div>
</form>
);
}
}

View 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 Options {
zx: number;
maxNumColors: number;
dither: number;
}
export const defaultOptions: Options = {
zx: 0,
maxNumColors: 256,
dither: 1.0,
};

View 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" />

View 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" />

View File

@@ -0,0 +1,41 @@
/**
* 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 imagequant, { QuantizerModule } from 'codecs/imagequant/imagequant';
import wasmUrl from 'url:codecs/imagequant/imagequant.wasm';
import { initEmscriptenModule } from 'features/worker-utils';
import { Options } from '../shared/meta';
let emscriptenModule: Promise<QuantizerModule>;
export default async function process(
data: ImageData,
opts: Options,
): Promise<ImageData> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
}
const module = await emscriptenModule;
const result = opts.zx
? module.zx_quantize(data.data, data.width, data.height, opts.dither)
: module.quantize(
data.data,
data.width,
data.height,
opts.maxNumColors,
opts.dither,
);
return new ImageData(result, data.width, data.height);
}

View File

@@ -0,0 +1,334 @@
import {
builtinResize,
BuiltinResizeMethod,
drawableToImageData,
} from 'client/lazy-app/util';
import {
BrowserResizeOptions,
VectorResizeOptions,
WorkerResizeOptions,
Options as ResizeOptions,
workerResizeMethods,
} from '../shared/meta';
import { getContainOffsets } from '../shared/util';
import type { SourceImage } from 'client/lazy-app/Compress';
import type WorkerBridge from 'client/lazy-app/worker-bridge';
import { h, Component } from 'preact';
import linkState from 'linkstate';
import {
inputFieldValueAsNumber,
inputFieldValue,
preventDefault,
inputFieldChecked,
} from 'client/lazy-app/util';
import * as style from 'client/lazy-app/Compress/Options/style.css';
import { linkRef } from 'shared/prerendered-app/util';
import Select from 'client/lazy-app/Compress/Options/Select';
import Expander from 'client/lazy-app/Compress/Options/Expander';
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
/**
* Return whether a set of options are worker resize options.
*
* @param opts
*/
function isWorkerOptions(opts: ResizeOptions): opts is WorkerResizeOptions {
return (workerResizeMethods as string[]).includes(opts.method);
}
function browserResize(data: ImageData, opts: BrowserResizeOptions): ImageData {
let sx = 0;
let sy = 0;
let sw = data.width;
let sh = data.height;
if (opts.fitMethod === 'contain') {
({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height));
}
return builtinResize(
data,
sx,
sy,
sw,
sh,
opts.width,
opts.height,
opts.method.slice('browser-'.length) as BuiltinResizeMethod,
);
}
function vectorResize(
data: HTMLImageElement,
opts: VectorResizeOptions,
): ImageData {
let sx = 0;
let sy = 0;
let sw = data.width;
let sh = data.height;
if (opts.fitMethod === 'contain') {
({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height));
}
return drawableToImageData(data, {
sx,
sy,
sw,
sh,
width: opts.width,
height: opts.height,
});
}
export async function resize(
signal: AbortSignal,
source: SourceImage,
options: ResizeOptions,
workerBridge: WorkerBridge,
) {
if (options.method === 'vector') {
if (!source.vectorImage) throw Error('No vector image available');
return vectorResize(source.vectorImage, options);
}
if (isWorkerOptions(options)) {
return workerBridge.resize(signal, source.preprocessed, options);
}
return browserResize(source.preprocessed, options);
}
interface Props {
isVector: Boolean;
inputWidth: number;
inputHeight: number;
options: ResizeOptions;
onChange(newOptions: ResizeOptions): void;
}
interface State {
maintainAspect: boolean;
}
const sizePresets = [0.25, 0.3333, 0.5, 1, 2, 3, 4];
export class Options extends Component<Props, State> {
state: State = {
maintainAspect: true,
};
private form?: HTMLFormElement;
private presetWidths: { [idx: number]: number } = {};
private presetHeights: { [idx: number]: number } = {};
constructor(props: Props) {
super(props);
this.generatePresetValues(props.inputWidth, props.inputHeight);
}
private reportOptions() {
const form = this.form!;
const width = form.width as HTMLInputElement;
const height = form.height as HTMLInputElement;
const { options } = this.props;
if (!width.checkValidity() || !height.checkValidity()) return;
const newOptions: ResizeOptions = {
width: inputFieldValueAsNumber(width),
height: inputFieldValueAsNumber(height),
method: form.resizeMethod.value,
premultiply: inputFieldChecked(form.premultiply, true),
linearRGB: inputFieldChecked(form.linearRGB, true),
// Casting, as the formfield only returns the correct values.
fitMethod: inputFieldValue(
form.fitMethod,
options.fitMethod,
) as ResizeOptions['fitMethod'],
};
this.props.onChange(newOptions);
}
private onChange = () => {
this.reportOptions();
};
private getAspect() {
return this.props.inputWidth / this.props.inputHeight;
}
componentDidUpdate(prevProps: Props, prevState: State) {
if (!prevState.maintainAspect && this.state.maintainAspect) {
this.form!.height.value = Math.round(
Number(this.form!.width.value) / this.getAspect(),
);
this.reportOptions();
}
}
componentWillReceiveProps(nextProps: Props) {
if (
this.props.inputWidth !== nextProps.inputWidth ||
this.props.inputHeight !== nextProps.inputHeight
) {
this.generatePresetValues(nextProps.inputWidth, nextProps.inputHeight);
}
}
private onWidthInput = () => {
if (this.state.maintainAspect) {
const width = inputFieldValueAsNumber(this.form!.width);
this.form!.height.value = Math.round(width / this.getAspect());
}
this.reportOptions();
};
private onHeightInput = () => {
if (this.state.maintainAspect) {
const height = inputFieldValueAsNumber(this.form!.height);
this.form!.width.value = Math.round(height * this.getAspect());
}
this.reportOptions();
};
private generatePresetValues(width: number, height: number) {
for (const preset of sizePresets) {
this.presetWidths[preset] = Math.round(width * preset);
this.presetHeights[preset] = Math.round(height * preset);
}
}
private getPreset(): number | string {
const { width, height } = this.props.options;
for (const preset of sizePresets) {
if (
width === this.presetWidths[preset] &&
height === this.presetHeights[preset]
)
return preset;
}
return 'custom';
}
private onPresetChange = (event: Event) => {
const select = event.target as HTMLSelectElement;
if (select.value === 'custom') return;
const multiplier = Number(select.value);
(this.form!.width as HTMLInputElement).value =
Math.round(this.props.inputWidth * multiplier) + '';
(this.form!.height as HTMLInputElement).value =
Math.round(this.props.inputHeight * multiplier) + '';
this.reportOptions();
};
render({ options, isVector }: Props, { maintainAspect }: State) {
return (
<form
ref={linkRef(this, 'form')}
class={style.optionsSection}
onSubmit={preventDefault}
>
<label class={style.optionTextFirst}>
Method:
<Select
name="resizeMethod"
value={options.method}
onChange={this.onChange}
>
{isVector && <option value="vector">Vector</option>}
<option value="lanczos3">Lanczos3</option>
<option value="mitchell">Mitchell</option>
<option value="catrom">Catmull-Rom</option>
<option value="triangle">Triangle (bilinear)</option>
<option value="hqx">hqx (pixel art)</option>
<option value="browser-pixelated">Browser pixelated</option>
<option value="browser-low">Browser low quality</option>
<option value="browser-medium">Browser medium quality</option>
<option value="browser-high">Browser high quality</option>
</Select>
</label>
<label class={style.optionTextFirst}>
Preset:
<Select value={this.getPreset()} onChange={this.onPresetChange}>
{sizePresets.map((preset) => (
<option value={preset}>{preset * 100}%</option>
))}
<option value="custom">Custom</option>
</Select>
</label>
<label class={style.optionTextFirst}>
Width:
<input
required
class={style.textField}
name="width"
type="number"
min="1"
value={'' + options.width}
onInput={this.onWidthInput}
/>
</label>
<label class={style.optionTextFirst}>
Height:
<input
required
class={style.textField}
name="height"
type="number"
min="1"
value={'' + options.height}
onInput={this.onHeightInput}
/>
</label>
<Expander>
{isWorkerOptions(options) ? (
<label class={style.optionInputFirst}>
<Checkbox
name="premultiply"
checked={options.premultiply}
onChange={this.onChange}
/>
Premultiply alpha channel
</label>
) : null}
{isWorkerOptions(options) ? (
<label class={style.optionInputFirst}>
<Checkbox
name="linearRGB"
checked={options.linearRGB}
onChange={this.onChange}
/>
Linear RGB
</label>
) : null}
</Expander>
<label class={style.optionInputFirst}>
<Checkbox
name="maintainAspect"
checked={maintainAspect}
onChange={linkState(this, 'maintainAspect')}
/>
Maintain aspect ratio
</label>
<Expander>
{maintainAspect ? null : (
<label class={style.optionTextFirst}>
Fit method:
<Select
name="fitMethod"
value={options.fitMethod}
onChange={this.onChange}
>
<option value="stretch">Stretch</option>
<option value="contain">Contain</option>
</Select>
</label>
)}
</Expander>
</form>
);
}
}

View 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" />

View File

@@ -0,0 +1,69 @@
/**
* 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.
*/
type BrowserResizeMethods =
| 'browser-pixelated'
| 'browser-low'
| 'browser-medium'
| 'browser-high';
type WorkerResizeMethods =
| 'triangle'
| 'catrom'
| 'mitchell'
| 'lanczos3'
| 'hqx';
export const workerResizeMethods: WorkerResizeMethods[] = [
'triangle',
'catrom',
'mitchell',
'lanczos3',
'hqx',
];
export type Options =
| BrowserResizeOptions
| WorkerResizeOptions
| VectorResizeOptions;
export interface ResizeOptionsCommon {
width: number;
height: number;
fitMethod: 'stretch' | 'contain';
}
export interface BrowserResizeOptions extends ResizeOptionsCommon {
method: BrowserResizeMethods;
}
export interface WorkerResizeOptions extends ResizeOptionsCommon {
method: WorkerResizeMethods;
premultiply: boolean;
linearRGB: boolean;
}
export interface VectorResizeOptions extends ResizeOptionsCommon {
method: 'vector';
}
export const defaultOptions: Options = {
// Width and height will always default to the image size.
// This is set elsewhere.
width: 1,
height: 1,
// This will be set to 'vector' if the input is SVG.
method: 'lanczos3',
fitMethod: 'stretch',
premultiply: true,
linearRGB: true,
};

View 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" />

View 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.
*/
export function getContainOffsets(
sw: number,
sh: number,
dw: number,
dh: number,
) {
const currentAspect = sw / sh;
const endAspect = dw / dh;
if (endAspect > currentAspect) {
const newSh = sw / endAspect;
const newSy = (sh - newSh) / 2;
return { sw, sh: newSh, sx: 0, sy: newSy };
}
const newSw = sh * endAspect;
const newSx = (sw - newSw) / 2;
return { sh, sw: newSw, sx: newSx, sy: 0 };
}

View 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" />

View File

@@ -0,0 +1,142 @@
import type { WorkerResizeOptions } from '../shared/meta';
import { getContainOffsets } from '../shared/util';
import initResizeWasm, { resize as wasmResize } from 'codecs/resize/pkg';
import resizeWasmUrl from 'url:codecs/resize/pkg/squoosh_resize_bg.wasm';
import hqxWasmUrl from 'url:codecs/hqx/pkg/squooshhqx_bg.wasm';
import initHqxWasm, { resize as wasmHqx } from 'codecs/hqx/pkg';
interface HqxResizeOptions extends WorkerResizeOptions {
method: 'hqx';
}
function optsIsHqxOpts(opts: WorkerResizeOptions): opts is HqxResizeOptions {
return opts.method === 'hqx';
}
function crop(
data: ImageData,
sx: number,
sy: number,
sw: number,
sh: number,
): ImageData {
const inputPixels = new Uint32Array(data.data.buffer);
// Copy within the same buffer for speed and memory efficiency.
for (let y = 0; y < sh; y += 1) {
const start = (y + sy) * data.width + sx;
inputPixels.copyWithin(y * sw, start, start + sw);
}
return new ImageData(
new Uint8ClampedArray(inputPixels.buffer.slice(0, sw * sh * 4)),
sw,
sh,
);
}
interface ClampOpts {
min?: number;
max?: number;
}
function clamp(
num: number,
{ min = Number.MIN_VALUE, max = Number.MAX_VALUE }: ClampOpts,
): number {
return Math.min(Math.max(num, min), max);
}
/** Resize methods by index */
const resizeMethods: WorkerResizeOptions['method'][] = [
'triangle',
'catrom',
'mitchell',
'lanczos3',
];
let resizeWasmReady: Promise<unknown>;
let hqxWasmReady: Promise<unknown>;
async function hqx(
input: ImageData,
opts: HqxResizeOptions,
): Promise<ImageData> {
if (!hqxWasmReady) {
hqxWasmReady = initHqxWasm(hqxWasmUrl);
}
await hqxWasmReady;
const widthRatio = opts.width / input.width;
const heightRatio = opts.height / input.height;
const ratio = Math.max(widthRatio, heightRatio);
const factor = clamp(Math.ceil(ratio), { min: 1, max: 4 }) as 1 | 2 | 3 | 4;
if (factor === 1) return input;
const result = wasmHqx(
new Uint32Array(input.data.buffer),
input.width,
input.height,
factor,
);
return new ImageData(
new Uint8ClampedArray(result.buffer),
input.width * factor,
input.height * factor,
);
}
export default async function resize(
data: ImageData,
opts: WorkerResizeOptions,
): Promise<ImageData> {
let input = data;
if (!resizeWasmReady) {
resizeWasmReady = initResizeWasm(resizeWasmUrl);
}
if (optsIsHqxOpts(opts)) {
input = await hqx(input, opts);
// Regular resize to make up the difference
opts = { ...opts, method: 'catrom' };
}
await resizeWasmReady;
if (opts.fitMethod === 'contain') {
const { sx, sy, sw, sh } = getContainOffsets(
data.width,
data.height,
opts.width,
opts.height,
);
input = crop(
input,
Math.round(sx),
Math.round(sy),
Math.round(sw),
Math.round(sh),
);
}
const result = wasmResize(
new Uint8Array(input.data.buffer),
input.width,
input.height,
opts.width,
opts.height,
resizeMethods.indexOf(opts.method),
opts.premultiply,
opts.linearRGB,
);
return new ImageData(
new Uint8ClampedArray(result.buffer),
opts.width,
opts.height,
);
}

View File

@@ -0,0 +1,31 @@
/**
* 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 function initEmscriptenModule<T extends EmscriptenWasm.Module>(
moduleFactory: EmscriptenWasm.ModuleFactory<T>,
wasmUrl: string,
workerUrl?: string,
): Promise<T> {
return moduleFactory({
// Just to be safe, don't automatically invoke any wasm functions
noInitialRun: true,
locateFile: (url: string) => {
if (url.endsWith('.wasm')) return wasmUrl;
if (url.endsWith('.worker.js')) return workerUrl!;
throw Error('Unknown url in locateFile ' + url);
},
});
}
export function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
return new Response(blob).arrayBuffer();
}