mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-13 09:17:20 +00:00
Adding resize preprocessor (#152)
* Adding resize preprocessor * Using ! on form * Haha oops * Using createImageBitmapPolyfill * Updating package.json * Oops again * Ooops again
This commit is contained in:
5
package-lock.json
generated
5
package-lock.json
generated
@@ -6681,6 +6681,11 @@
|
|||||||
"invert-kv": "^1.0.0"
|
"invert-kv": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"linkstate": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkstate/-/linkstate-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-5SICdxQG9FpWk44wSEoM2WOCUNuYfClp10t51XAIV5E7vUILF/dTYxf0vJw6bW2dUd2wcQon+LkNtRijpNLrig=="
|
||||||
|
},
|
||||||
"listr": {
|
"listr": {
|
||||||
"version": "0.14.1",
|
"version": "0.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/listr/-/listr-0.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/listr/-/listr-0.14.1.tgz",
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"comlink": "^3.0.3",
|
"comlink": "^3.0.3",
|
||||||
"comlink-loader": "^1.0.0",
|
"comlink-loader": "^1.0.0",
|
||||||
"preact": "^8.3.1",
|
"preact": "^8.3.1",
|
||||||
|
"linkstate": "^1.1.1",
|
||||||
"pretty-bytes": "^5.1.0"
|
"pretty-bytes": "^5.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import { QuantizeOptions, defaultOptions as quantizerDefaultOptions } from './imagequant/quantizer';
|
import { QuantizeOptions, defaultOptions as quantizerDefaultOptions } from './imagequant/quantizer';
|
||||||
|
import { ResizeOptions, defaultOptions as resizeDefaultOptions } from './resize/resize';
|
||||||
|
|
||||||
interface Enableable {
|
interface Enableable {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
export interface PreprocessorState {
|
export interface PreprocessorState {
|
||||||
quantizer: Enableable & QuantizeOptions;
|
quantizer: Enableable & QuantizeOptions;
|
||||||
|
resize: Enableable & ResizeOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultPreprocessorState = {
|
export const defaultPreprocessorState: PreprocessorState = {
|
||||||
quantizer: {
|
quantizer: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
...quantizerDefaultOptions,
|
...quantizerDefaultOptions,
|
||||||
},
|
},
|
||||||
|
resize: {
|
||||||
|
enabled: false,
|
||||||
|
...resizeDefaultOptions,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
128
src/codecs/resize/options.tsx
Normal file
128
src/codecs/resize/options.tsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { h, Component } from 'preact';
|
||||||
|
import linkState from 'linkstate';
|
||||||
|
import { bind, inputFieldValueAsNumber } from '../../lib/util';
|
||||||
|
import { ResizeOptions } from './resize';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options: ResizeOptions;
|
||||||
|
aspect: number;
|
||||||
|
onChange(newOptions: ResizeOptions): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
maintainAspect: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ResizerOptions extends Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
maintainAspect: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
form?: HTMLFormElement;
|
||||||
|
|
||||||
|
reportOptions() {
|
||||||
|
const width = this.form!.width as HTMLInputElement;
|
||||||
|
const height = this.form!.height as HTMLInputElement;
|
||||||
|
|
||||||
|
if (!width.checkValidity() || !height.checkValidity()) return;
|
||||||
|
|
||||||
|
const options: ResizeOptions = {
|
||||||
|
width: inputFieldValueAsNumber(width),
|
||||||
|
height: inputFieldValueAsNumber(height),
|
||||||
|
method: this.form!.resizeMethod.value,
|
||||||
|
fitMethod: this.form!.fitMethod.value,
|
||||||
|
};
|
||||||
|
this.props.onChange(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
onChange(event: Event) {
|
||||||
|
this.reportOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||||
|
if (!prevState.maintainAspect && this.state.maintainAspect) {
|
||||||
|
this.form!.height.value = Math.round(Number(this.form!.width.value) / this.props.aspect);
|
||||||
|
this.reportOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
onWidthInput(event: Event) {
|
||||||
|
if (!this.state.maintainAspect) return;
|
||||||
|
|
||||||
|
const width = inputFieldValueAsNumber(this.form!.width);
|
||||||
|
this.form!.height.value = Math.round(width / this.props.aspect);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
onHeightInput(event: Event) {
|
||||||
|
if (!this.state.maintainAspect) return;
|
||||||
|
|
||||||
|
const height = inputFieldValueAsNumber(this.form!.height);
|
||||||
|
this.form!.width.value = Math.round(height * this.props.aspect);
|
||||||
|
}
|
||||||
|
|
||||||
|
render({ options, aspect }: Props, { maintainAspect }: State) {
|
||||||
|
return (
|
||||||
|
<form ref={el => this.form = el}>
|
||||||
|
<label>
|
||||||
|
Method:
|
||||||
|
<select
|
||||||
|
name="resizeMethod"
|
||||||
|
value={options.method}
|
||||||
|
onChange={this.onChange}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
Width:
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
name="width"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={'' + options.width}
|
||||||
|
onChange={this.onChange}
|
||||||
|
onInput={this.onWidthInput}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Height:
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
name="height"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={'' + options.height}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
name="maintainAspect"
|
||||||
|
type="checkbox"
|
||||||
|
checked={maintainAspect}
|
||||||
|
onChange={linkState(this, 'maintainAspect')}
|
||||||
|
/>
|
||||||
|
Maintain aspect ratio
|
||||||
|
</label>
|
||||||
|
<label style={{ display: maintainAspect ? 'none' : '' }}>
|
||||||
|
Fit method:
|
||||||
|
<select
|
||||||
|
name="fitMethod"
|
||||||
|
value={options.fitMethod}
|
||||||
|
onChange={this.onChange}
|
||||||
|
>
|
||||||
|
<option value="stretch">Stretch</option>
|
||||||
|
<option value="cover">Cover</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/codecs/resize/resize.ts
Normal file
46
src/codecs/resize/resize.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { bitmapToImageData, createImageBitmapPolyfill } from '../../lib/util';
|
||||||
|
|
||||||
|
type CreateImageBitmapResize = 'pixelated' | 'low' | 'medium' | 'high';
|
||||||
|
|
||||||
|
export async function resize(data: ImageData, opts: ResizeOptions): Promise<ImageData> {
|
||||||
|
let sx = 0;
|
||||||
|
let sy = 0;
|
||||||
|
let sw = data.width;
|
||||||
|
let sh = data.height;
|
||||||
|
|
||||||
|
if (opts.fitMethod === 'cover') {
|
||||||
|
const currentAspect = data.width / data.height;
|
||||||
|
const endAspect = opts.width / opts.height;
|
||||||
|
if (endAspect > currentAspect) {
|
||||||
|
sh = opts.height / (opts.width / data.width);
|
||||||
|
sy = (data.height - sh) / 2;
|
||||||
|
} else {
|
||||||
|
sw = opts.width / (opts.height / data.height);
|
||||||
|
sx = (data.width - sw) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bmp = await createImageBitmapPolyfill(data, sx, sy, sw, sh, {
|
||||||
|
resizeQuality: opts.method.slice('browser-'.length) as CreateImageBitmapResize,
|
||||||
|
resizeWidth: opts.width,
|
||||||
|
resizeHeight: opts.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
return bitmapToImageData(bmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResizeOptions {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
method: 'browser-pixelated' | 'browser-low' | 'browser-medium' | 'browser-high';
|
||||||
|
fitMethod: 'stretch' | 'cover';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultOptions: ResizeOptions = {
|
||||||
|
// Width and height will always default to the image size.
|
||||||
|
// This is set elsewhere.
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
method: 'browser-high',
|
||||||
|
fitMethod: 'stretch',
|
||||||
|
};
|
||||||
@@ -10,6 +10,7 @@ import ResultCache from './result-cache';
|
|||||||
|
|
||||||
import * as quantizer from '../../codecs/imagequant/quantizer';
|
import * as quantizer from '../../codecs/imagequant/quantizer';
|
||||||
import * as optiPNG from '../../codecs/optipng/encoder';
|
import * as optiPNG from '../../codecs/optipng/encoder';
|
||||||
|
import * as resizer from '../../codecs/resize/resize';
|
||||||
import * as mozJPEG from '../../codecs/mozjpeg/encoder';
|
import * as mozJPEG from '../../codecs/mozjpeg/encoder';
|
||||||
import * as webP from '../../codecs/webp/encoder';
|
import * as webP from '../../codecs/webp/encoder';
|
||||||
import * as identity from '../../codecs/identity/encoder';
|
import * as identity from '../../codecs/identity/encoder';
|
||||||
@@ -79,6 +80,9 @@ async function preprocessImage(
|
|||||||
preprocessData: PreprocessorState,
|
preprocessData: PreprocessorState,
|
||||||
): Promise<ImageData> {
|
): Promise<ImageData> {
|
||||||
let result = source.data;
|
let result = source.data;
|
||||||
|
if (preprocessData.resize.enabled) {
|
||||||
|
result = await resizer.resize(result, preprocessData.resize);
|
||||||
|
}
|
||||||
if (preprocessData.quantizer.enabled) {
|
if (preprocessData.quantizer.enabled) {
|
||||||
result = await quantizer.quantize(result, preprocessData.quantizer);
|
result = await quantizer.quantize(result, preprocessData.quantizer);
|
||||||
}
|
}
|
||||||
@@ -227,10 +231,21 @@ export default class App extends Component<Props, State> {
|
|||||||
// compute the corresponding ImageData once since it only changes when the file changes:
|
// compute the corresponding ImageData once since it only changes when the file changes:
|
||||||
const data = await bitmapToImageData(bmp);
|
const data = await bitmapToImageData(bmp);
|
||||||
|
|
||||||
this.setState({
|
let newState = {
|
||||||
|
...this.state,
|
||||||
source: { data, bmp, file },
|
source: { data, bmp, file },
|
||||||
loading: false,
|
loading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default resize values come from the image:
|
||||||
|
for (const i of [0, 1]) {
|
||||||
|
newState = cleanMerge(newState, `images.${i}.preprocessorState.resize`, {
|
||||||
|
width: data.width,
|
||||||
|
height: data.height,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(newState);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
this.showError(`Invalid image`);
|
this.showError(`Invalid image`);
|
||||||
@@ -314,17 +329,22 @@ export default class App extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render({ }: Props, { loading, images, source, orientation }: State) {
|
render({ }: Props, { loading, images, source, orientation }: State) {
|
||||||
|
const [leftImage, rightImage] = images;
|
||||||
const [leftImageBmp, rightImageBmp] = images.map(i => i.bmp);
|
const [leftImageBmp, rightImageBmp] = images.map(i => i.bmp);
|
||||||
const anyLoading = loading || images.some(image => image.loading);
|
const anyLoading = loading || images.some(image => image.loading);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<file-drop accept="image/*" onfiledrop={this.onFileDrop}>
|
<file-drop accept="image/*" onfiledrop={this.onFileDrop}>
|
||||||
<div id="app" class={`${style.app} ${style[orientation]}`}>
|
<div id="app" class={`${style.app} ${style[orientation]}`}>
|
||||||
{(leftImageBmp && rightImageBmp) ? (
|
{(leftImageBmp && rightImageBmp && source) ? (
|
||||||
<Output
|
<Output
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
|
imgWidth={source.bmp.width}
|
||||||
|
imgHeight={source.bmp.height}
|
||||||
leftImg={leftImageBmp}
|
leftImg={leftImageBmp}
|
||||||
rightImg={rightImageBmp}
|
rightImg={rightImageBmp}
|
||||||
|
leftImgContain={leftImage.preprocessorState.resize.fitMethod === 'cover'}
|
||||||
|
rightImgContain={rightImage.preprocessorState.resize.fitMethod === 'cover'}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div class={style.welcome}>
|
<div class={style.welcome}>
|
||||||
@@ -332,9 +352,10 @@ export default class App extends Component<Props, State> {
|
|||||||
<input type="file" onChange={this.onFileChange} />
|
<input type="file" onChange={this.onFileChange} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(leftImageBmp && rightImageBmp) && images.map((image, index) => (
|
{(leftImageBmp && rightImageBmp && source) && images.map((image, index) => (
|
||||||
<Options
|
<Options
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
|
sourceAspect={source.bmp.width / source.bmp.height}
|
||||||
imageIndex={index}
|
imageIndex={index}
|
||||||
imageFile={image.file}
|
imageFile={image.file}
|
||||||
sourceImageFile={source && source.file}
|
sourceImageFile={source && source.file}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import WebPEncoderOptions from '../../codecs/webp/options';
|
|||||||
import BrowserWebPEncoderOptions from '../../codecs/browser-webp/options';
|
import BrowserWebPEncoderOptions from '../../codecs/browser-webp/options';
|
||||||
|
|
||||||
import QuantizerOptionsComponent from '../../codecs/imagequant/options';
|
import QuantizerOptionsComponent from '../../codecs/imagequant/options';
|
||||||
|
import ResizeOptionsComponent from '../../codecs/resize/options';
|
||||||
|
|
||||||
import * as identity from '../../codecs/identity/encoder';
|
import * as identity from '../../codecs/identity/encoder';
|
||||||
import * as optiPNG from '../../codecs/optipng/encoder';
|
import * as optiPNG from '../../codecs/optipng/encoder';
|
||||||
@@ -33,9 +34,8 @@ import {
|
|||||||
encoderMap,
|
encoderMap,
|
||||||
} from '../../codecs/encoders';
|
} from '../../codecs/encoders';
|
||||||
import { QuantizeOptions } from '../../codecs/imagequant/quantizer';
|
import { QuantizeOptions } from '../../codecs/imagequant/quantizer';
|
||||||
|
import { ResizeOptions } from '../../codecs/resize/resize';
|
||||||
import { PreprocessorState } from '../../codecs/preprocessors';
|
import { PreprocessorState } from '../../codecs/preprocessors';
|
||||||
|
|
||||||
import FileSize from '../FileSize';
|
import FileSize from '../FileSize';
|
||||||
import { DownloadIcon } from '../../lib/icons';
|
import { DownloadIcon } from '../../lib/icons';
|
||||||
|
|
||||||
@@ -62,6 +62,7 @@ const titles = {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
orientation: 'horizontal' | 'vertical';
|
orientation: 'horizontal' | 'vertical';
|
||||||
|
sourceAspect: number;
|
||||||
imageIndex: number;
|
imageIndex: number;
|
||||||
sourceImageFile?: File;
|
sourceImageFile?: File;
|
||||||
imageFile?: File;
|
imageFile?: File;
|
||||||
@@ -112,9 +113,17 @@ export default class Options extends Component<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
onResizeOptionsChange(opts: ResizeOptions) {
|
||||||
|
this.props.onPreprocessorOptionsChange(
|
||||||
|
cleanMerge(this.props.preprocessorState, 'resize', opts),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
{
|
{
|
||||||
sourceImageFile,
|
sourceImageFile,
|
||||||
|
sourceAspect,
|
||||||
imageIndex,
|
imageIndex,
|
||||||
imageFile,
|
imageFile,
|
||||||
downloadUrl,
|
downloadUrl,
|
||||||
@@ -161,7 +170,23 @@ export default class Options extends Component<Props, State> {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{encoderState.type !== 'identity' && (
|
{encoderState.type !== 'identity' && (
|
||||||
<div key="quantization" class={style.quantization}>
|
<div key="preprocessors" class={style.preprocessors}>
|
||||||
|
<label class={style.toggle}>
|
||||||
|
<input
|
||||||
|
name="resize.enable"
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!preprocessorState.resize.enabled}
|
||||||
|
onChange={this.onPreprocessorEnabledChange}
|
||||||
|
/>
|
||||||
|
Resize
|
||||||
|
</label>
|
||||||
|
{preprocessorState.resize.enabled &&
|
||||||
|
<ResizeOptionsComponent
|
||||||
|
aspect={sourceAspect}
|
||||||
|
options={preprocessorState.resize}
|
||||||
|
onChange={this.onResizeOptionsChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
<label class={style.toggle}>
|
<label class={style.toggle}>
|
||||||
<input
|
<input
|
||||||
name="quantizer.enable"
|
name="quantizer.enable"
|
||||||
@@ -169,7 +194,7 @@ export default class Options extends Component<Props, State> {
|
|||||||
checked={!!preprocessorState.quantizer.enabled}
|
checked={!!preprocessorState.quantizer.enabled}
|
||||||
onChange={this.onPreprocessorEnabledChange}
|
onChange={this.onPreprocessorEnabledChange}
|
||||||
/>
|
/>
|
||||||
Enable Quantization
|
Quantize
|
||||||
</label>
|
</label>
|
||||||
{preprocessorState.quantizer.enabled &&
|
{preprocessorState.quantizer.enabled &&
|
||||||
<QuantizerOptionsComponent
|
<QuantizerOptionsComponent
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ Note: These styles are temporary. They will be replaced before going live.
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.quantization {
|
.preprocessors {
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
box-shadow: inset 0 -.5px 0 rgba(0,0,0,0.25), 0 .5px 0 rgba(255,255,255,0.15);
|
box-shadow: inset 0 -.5px 0 rgba(0,0,0,0.25), 0 .5px 0 rgba(255,255,255,0.15);
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ interface Props {
|
|||||||
orientation: 'horizontal' | 'vertical';
|
orientation: 'horizontal' | 'vertical';
|
||||||
leftImg: ImageBitmap;
|
leftImg: ImageBitmap;
|
||||||
rightImg: ImageBitmap;
|
rightImg: ImageBitmap;
|
||||||
|
imgWidth: number;
|
||||||
|
imgHeight: number;
|
||||||
|
leftImgContain: boolean;
|
||||||
|
rightImgContain: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@@ -145,7 +149,7 @@ export default class Output extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
{ orientation, leftImg, rightImg }: Props,
|
{ orientation, leftImg, rightImg, imgWidth, imgHeight, leftImgContain, rightImgContain }: Props,
|
||||||
{ scale, editingScale, altBackground }: State,
|
{ scale, editingScale, altBackground }: State,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
@@ -165,18 +169,26 @@ export default class Output extends Component<Props, State> {
|
|||||||
ref={linkRef(this, 'pinchZoomLeft')}
|
ref={linkRef(this, 'pinchZoomLeft')}
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
class={style.outputCanvas}
|
|
||||||
ref={linkRef(this, 'canvasLeft')}
|
ref={linkRef(this, 'canvasLeft')}
|
||||||
width={leftImg.width}
|
width={leftImg.width}
|
||||||
height={leftImg.height}
|
height={leftImg.height}
|
||||||
|
style={{
|
||||||
|
width: imgWidth,
|
||||||
|
height: imgHeight,
|
||||||
|
objectFit: leftImgContain ? 'contain' : '',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</pinch-zoom>
|
</pinch-zoom>
|
||||||
<pinch-zoom ref={linkRef(this, 'pinchZoomRight')}>
|
<pinch-zoom ref={linkRef(this, 'pinchZoomRight')}>
|
||||||
<canvas
|
<canvas
|
||||||
class={style.outputCanvas}
|
|
||||||
ref={linkRef(this, 'canvasRight')}
|
ref={linkRef(this, 'canvasRight')}
|
||||||
width={rightImg.width}
|
width={rightImg.width}
|
||||||
height={rightImg.height}
|
height={rightImg.height}
|
||||||
|
style={{
|
||||||
|
width: imgWidth,
|
||||||
|
height: imgHeight,
|
||||||
|
objectFit: rightImgContain ? 'contain' : '',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</pinch-zoom>
|
</pinch-zoom>
|
||||||
</two-up>
|
</two-up>
|
||||||
|
|||||||
@@ -134,7 +134,3 @@ Note: These styles are temporary. They will be replaced before going live.
|
|||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.outputCanvas {
|
|
||||||
image-rendering: pixelated;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -154,8 +154,35 @@ export async function sniffMimeType(blob: Blob): Promise<string> {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createImageBitmapPolyfill(blob: Blob): Promise<ImageBitmap> {
|
type CreateImageBitmapInput = HTMLImageElement | SVGImageElement | HTMLVideoElement |
|
||||||
return createImageBitmap(blob);
|
HTMLCanvasElement | ImageBitmap | ImageData | Blob;
|
||||||
|
|
||||||
|
export function createImageBitmapPolyfill(
|
||||||
|
image: CreateImageBitmapInput,
|
||||||
|
options?: ImageBitmapOptions,
|
||||||
|
): Promise<ImageBitmap>;
|
||||||
|
export function createImageBitmapPolyfill(
|
||||||
|
image: CreateImageBitmapInput,
|
||||||
|
sx: number,
|
||||||
|
sy: number,
|
||||||
|
sw: number,
|
||||||
|
sh: number,
|
||||||
|
options?: ImageBitmapOptions,
|
||||||
|
): Promise<ImageBitmap>;
|
||||||
|
export function createImageBitmapPolyfill(
|
||||||
|
image: CreateImageBitmapInput,
|
||||||
|
sxOrOptions?: number | ImageBitmapOptions,
|
||||||
|
sy?: number,
|
||||||
|
sw?: number,
|
||||||
|
sh?: number,
|
||||||
|
options?: ImageBitmapOptions,
|
||||||
|
): Promise<ImageBitmap> {
|
||||||
|
if (sxOrOptions === undefined || typeof sxOrOptions !== 'number') {
|
||||||
|
// sxOrOptions is absent or an options object
|
||||||
|
return createImageBitmap(image, sxOrOptions);
|
||||||
|
}
|
||||||
|
// sxOrOptions is a number
|
||||||
|
return createImageBitmap(image, sxOrOptions, sy!, sw!, sh!, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user