Rollup build

This commit is contained in:
Jake Archibald
2020-11-19 11:00:23 +00:00
parent dfee848a39
commit 56e10b3aa2
340 changed files with 37866 additions and 19153 deletions

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