mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-13 17:27:09 +00:00
Creating fallbacks for all ImageBitmap usage & refactoring. (#175)
Fixes #173
This commit is contained in:
4003
package-lock.json
generated
4003
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -56,7 +56,7 @@
|
|||||||
"tslint-react": "^3.6.0",
|
"tslint-react": "^3.6.0",
|
||||||
"typescript": "^2.9.2",
|
"typescript": "^2.9.2",
|
||||||
"typings-for-css-modules-loader": "^1.7.0",
|
"typings-for-css-modules-loader": "^1.7.0",
|
||||||
"webpack": "^4.16.5",
|
"webpack": "^4.19.1",
|
||||||
"webpack-bundle-analyzer": "^2.13.1",
|
"webpack-bundle-analyzer": "^2.13.1",
|
||||||
"webpack-cli": "^2.1.5",
|
"webpack-cli": "^2.1.5",
|
||||||
"webpack-dev-server": "^3.1.5",
|
"webpack-dev-server": "^3.1.5",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { canDecodeImage, createImageBitmapPolyfill } from '../../lib/util';
|
import { canDecodeImage, nativeDecode } from '../../lib/util';
|
||||||
|
|
||||||
export const name = 'Browser WebP Decoder';
|
export const name = 'Browser WebP Decoder';
|
||||||
export async function decode(blob: Blob): Promise<ImageBitmap> {
|
export async function decode(blob: Blob): Promise<ImageData> {
|
||||||
return createImageBitmapPolyfill(blob);
|
return nativeDecode(blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line:max-line-length It’s a data URL. Whatcha gonna do?
|
// tslint:disable-next-line:max-line-length It’s a data URL. Whatcha gonna do?
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as wasmWebp from './webp/decoder';
|
import * as wasmWebp from './webp/decoder';
|
||||||
import * as browserWebp from './webp/decoder';
|
import * as browserWebp from './webp/decoder';
|
||||||
|
|
||||||
import { createImageBitmapPolyfill, sniffMimeType } from '../lib/util';
|
import { nativeDecode, sniffMimeType } from '../lib/util';
|
||||||
|
|
||||||
export interface Decoder {
|
export interface Decoder {
|
||||||
name: string;
|
name: string;
|
||||||
decode(blob: Blob): Promise<ImageBitmap>;
|
decode(blob: Blob): Promise<ImageData>;
|
||||||
isSupported(): Promise<boolean>;
|
isSupported(): Promise<boolean>;
|
||||||
canHandleMimeType(mimeType: string): boolean;
|
canHandleMimeType(mimeType: string): boolean;
|
||||||
}
|
}
|
||||||
@@ -31,12 +31,12 @@ async function findDecodersByMimeType(mimeType: string): Promise<Decoder[]> {
|
|||||||
return decoders.filter(decoder => decoder.canHandleMimeType(mimeType));
|
return decoders.filter(decoder => decoder.canHandleMimeType(mimeType));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decodeImage(blob: Blob): Promise<ImageBitmap> {
|
export async function decodeImage(blob: Blob): Promise<ImageData> {
|
||||||
const mimeType = await sniffMimeType(blob);
|
const mimeType = await sniffMimeType(blob);
|
||||||
const decoders = await findDecodersByMimeType(mimeType);
|
const decoders = await findDecodersByMimeType(mimeType);
|
||||||
if (decoders.length <= 0) {
|
if (decoders.length <= 0) {
|
||||||
// If we can’t find a decoder, hailmary with createImageBitmap
|
// If we can’t find a decoder, hailmary with the browser's decoders
|
||||||
return createImageBitmapPolyfill(blob);
|
return nativeDecode(blob);
|
||||||
}
|
}
|
||||||
for (const decoder of decoders) {
|
for (const decoder of decoders) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { bitmapToImageData, createImageBitmapPolyfill } from '../../lib/util';
|
import { nativeResize, NativeResizeMethod } from '../../lib/util';
|
||||||
|
|
||||||
type CreateImageBitmapResize = 'pixelated' | 'low' | 'medium' | 'high';
|
export function resize(data: ImageData, opts: ResizeOptions): ImageData {
|
||||||
|
|
||||||
export async function resize(data: ImageData, opts: ResizeOptions): Promise<ImageData> {
|
|
||||||
let sx = 0;
|
let sx = 0;
|
||||||
let sy = 0;
|
let sy = 0;
|
||||||
let sw = data.width;
|
let sw = data.width;
|
||||||
@@ -20,13 +18,10 @@ export async function resize(data: ImageData, opts: ResizeOptions): Promise<Imag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bmp = await createImageBitmapPolyfill(data, sx, sy, sw, sh, {
|
return nativeResize(
|
||||||
resizeQuality: opts.method.slice('browser-'.length) as CreateImageBitmapResize,
|
data, sx, sy, sw, sh, opts.width, opts.height,
|
||||||
resizeWidth: opts.width,
|
opts.method.slice('browser-'.length) as NativeResizeMethod,
|
||||||
resizeHeight: opts.height,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
return bitmapToImageData(bmp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResizeOptions {
|
export interface ResizeOptions {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { blobToArrayBuffer, imageDataToBitmap } from '../../lib/util';
|
import { blobToArrayBuffer } from '../../lib/util';
|
||||||
import DecoderWorker from './Decoder.worker';
|
import DecoderWorker from './Decoder.worker';
|
||||||
|
|
||||||
export const name = 'WASM WebP Decoder';
|
export const name = 'WASM WebP Decoder';
|
||||||
export async function decode(blob: Blob): Promise<ImageBitmap> {
|
export async function decode(blob: Blob): Promise<ImageData> {
|
||||||
const decoder = await new DecoderWorker();
|
const decoder = await new DecoderWorker();
|
||||||
const imageData = await decoder.decode(await blobToArrayBuffer(blob));
|
return decoder.decode(await blobToArrayBuffer(blob));
|
||||||
return imageDataToBitmap(imageData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isSupported(): Promise<boolean> {
|
export async function isSupported(): Promise<boolean> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
|
|
||||||
import { bind, linkRef, bitmapToImageData } from '../../lib/util';
|
import { bind, linkRef } from '../../lib/util';
|
||||||
import * as style from './style.scss';
|
import * as style from './style.scss';
|
||||||
import Output from '../Output';
|
import Output from '../Output';
|
||||||
import Options from '../Options';
|
import Options from '../Options';
|
||||||
@@ -43,15 +43,14 @@ type Orientation = 'horizontal' | 'vertical';
|
|||||||
|
|
||||||
export interface SourceImage {
|
export interface SourceImage {
|
||||||
file: File;
|
file: File;
|
||||||
bmp: ImageBitmap;
|
|
||||||
data: ImageData;
|
data: ImageData;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EncodedImage {
|
interface EncodedImage {
|
||||||
preprocessed?: ImageData;
|
preprocessed?: ImageData;
|
||||||
bmp?: ImageBitmap;
|
|
||||||
file?: File;
|
file?: File;
|
||||||
downloadUrl?: string;
|
downloadUrl?: string;
|
||||||
|
data?: ImageData;
|
||||||
preprocessorState: PreprocessorState;
|
preprocessorState: PreprocessorState;
|
||||||
encoderState: EncoderState;
|
encoderState: EncoderState;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -81,7 +80,7 @@ async function preprocessImage(
|
|||||||
): Promise<ImageData> {
|
): Promise<ImageData> {
|
||||||
let result = source.data;
|
let result = source.data;
|
||||||
if (preprocessData.resize.enabled) {
|
if (preprocessData.resize.enabled) {
|
||||||
result = await resizer.resize(result, preprocessData.resize);
|
result = 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);
|
||||||
@@ -196,8 +195,8 @@ export default class App extends Component<Props, State> {
|
|||||||
const encoderChanged = image.encoderState !== prevImage.encoderState;
|
const encoderChanged = image.encoderState !== prevImage.encoderState;
|
||||||
const preprocessorChanged = image.preprocessorState !== prevImage.preprocessorState;
|
const preprocessorChanged = image.preprocessorState !== prevImage.preprocessorState;
|
||||||
|
|
||||||
// The image only needs updated if the encoder settings have changed, or the source has
|
// The image only needs updated if the encoder/preprocessor settings have changed, or the
|
||||||
// changed.
|
// source has changed.
|
||||||
if (sourceChanged || encoderChanged || preprocessorChanged) {
|
if (sourceChanged || encoderChanged || preprocessorChanged) {
|
||||||
if (prevImage.downloadUrl) URL.revokeObjectURL(prevImage.downloadUrl);
|
if (prevImage.downloadUrl) URL.revokeObjectURL(prevImage.downloadUrl);
|
||||||
this.updateImage(i, {
|
this.updateImage(i, {
|
||||||
@@ -235,13 +234,11 @@ export default class App extends Component<Props, State> {
|
|||||||
async updateFile(file: File) {
|
async updateFile(file: File) {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
try {
|
try {
|
||||||
const bmp = await decodeImage(file);
|
const data = await decodeImage(file);
|
||||||
// compute the corresponding ImageData once since it only changes when the file changes:
|
|
||||||
const data = await bitmapToImageData(bmp);
|
|
||||||
|
|
||||||
let newState = {
|
let newState = {
|
||||||
...this.state,
|
...this.state,
|
||||||
source: { data, bmp, file },
|
source: { data, file },
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -256,7 +253,7 @@ export default class App extends Component<Props, State> {
|
|||||||
this.setState(newState);
|
this.setState(newState);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
this.showError(`Invalid image`);
|
this.showError('Invalid image');
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,29 +275,29 @@ export default class App extends Component<Props, State> {
|
|||||||
|
|
||||||
const image = images[index];
|
const image = images[index];
|
||||||
|
|
||||||
let file;
|
let file: File | undefined;
|
||||||
let preprocessed;
|
let preprocessed: ImageData | undefined;
|
||||||
let bmp;
|
let data: ImageData | undefined;
|
||||||
const cacheResult = this.encodeCache.match(source, image.preprocessorState, image.encoderState);
|
const cacheResult = this.encodeCache.match(source, image.preprocessorState, image.encoderState);
|
||||||
|
|
||||||
if (cacheResult) {
|
if (cacheResult) {
|
||||||
({ file, preprocessed, bmp } = cacheResult);
|
({ file, preprocessed, data } = cacheResult);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
// Special case for identity
|
// Special case for identity
|
||||||
if (image.encoderState.type === identity.type) {
|
if (image.encoderState.type === identity.type) {
|
||||||
({ file, bmp } = source);
|
({ file, data } = source);
|
||||||
} else {
|
} else {
|
||||||
preprocessed = (skipPreprocessing && image.preprocessed)
|
preprocessed = (skipPreprocessing && image.preprocessed)
|
||||||
? image.preprocessed
|
? image.preprocessed
|
||||||
: await preprocessImage(source, image.preprocessorState);
|
: await preprocessImage(source, image.preprocessorState);
|
||||||
|
|
||||||
file = await compressImage(preprocessed, image.encoderState, source.file.name);
|
file = await compressImage(preprocessed, image.encoderState, source.file.name);
|
||||||
bmp = await decodeImage(file);
|
data = await decodeImage(file);
|
||||||
|
|
||||||
this.encodeCache.add({
|
this.encodeCache.add({
|
||||||
source,
|
source,
|
||||||
bmp,
|
data,
|
||||||
preprocessed,
|
preprocessed,
|
||||||
file,
|
file,
|
||||||
encoderState: image.encoderState,
|
encoderState: image.encoderState,
|
||||||
@@ -321,7 +318,7 @@ export default class App extends Component<Props, State> {
|
|||||||
|
|
||||||
images = cleanMerge(this.state.images, index, {
|
images = cleanMerge(this.state.images, index, {
|
||||||
file,
|
file,
|
||||||
bmp,
|
data,
|
||||||
preprocessed,
|
preprocessed,
|
||||||
downloadUrl: URL.createObjectURL(file),
|
downloadUrl: URL.createObjectURL(file),
|
||||||
loading: images[index].loadingCounter !== loadingCounter,
|
loading: images[index].loadingCounter !== loadingCounter,
|
||||||
@@ -338,19 +335,19 @@ 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 [leftImage, rightImage] = images;
|
||||||
const [leftImageBmp, rightImageBmp] = images.map(i => i.bmp);
|
const [leftImageData, rightImageData] = images.map(i => i.data);
|
||||||
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 && source) ? (
|
{(leftImageData && rightImageData && source) ? (
|
||||||
<Output
|
<Output
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
imgWidth={source.bmp.width}
|
imgWidth={source.data.width}
|
||||||
imgHeight={source.bmp.height}
|
imgHeight={source.data.height}
|
||||||
leftImg={leftImageBmp}
|
leftImg={leftImageData}
|
||||||
rightImg={rightImageBmp}
|
rightImg={rightImageData}
|
||||||
leftImgContain={leftImage.preprocessorState.resize.fitMethod === 'cover'}
|
leftImgContain={leftImage.preprocessorState.resize.fitMethod === 'cover'}
|
||||||
rightImgContain={rightImage.preprocessorState.resize.fitMethod === 'cover'}
|
rightImgContain={rightImage.preprocessorState.resize.fitMethod === 'cover'}
|
||||||
/>
|
/>
|
||||||
@@ -360,10 +357,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 && source) && images.map((image, index) => (
|
{(leftImageData && rightImageData && source) && images.map((image, index) => (
|
||||||
<Options
|
<Options
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
sourceAspect={source.bmp.width / source.bmp.height}
|
sourceAspect={source.data.width / source.data.height}
|
||||||
imageIndex={index}
|
imageIndex={index}
|
||||||
imageFile={image.file}
|
imageFile={image.file}
|
||||||
sourceImageFile={source && source.file}
|
sourceImageFile={source && source.file}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import * as identity from '../../codecs/identity/encoder';
|
|||||||
|
|
||||||
interface CacheResult {
|
interface CacheResult {
|
||||||
preprocessed: ImageData;
|
preprocessed: ImageData;
|
||||||
bmp: ImageBitmap;
|
data: ImageData;
|
||||||
file: File;
|
file: File;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ export default class ResultCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bmp: matchingEntry.bmp,
|
data: matchingEntry.data,
|
||||||
preprocessed: matchingEntry.preprocessed,
|
preprocessed: matchingEntry.preprocessed,
|
||||||
file: matchingEntry.file,
|
file: matchingEntry.file,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import PinchZoom, { ScaleToOpts } from './custom-els/PinchZoom';
|
|||||||
import './custom-els/PinchZoom';
|
import './custom-els/PinchZoom';
|
||||||
import './custom-els/TwoUp';
|
import './custom-els/TwoUp';
|
||||||
import * as style from './style.scss';
|
import * as style from './style.scss';
|
||||||
import { bind, shallowEqual, drawBitmapToCanvas, linkRef } from '../../lib/util';
|
import { bind, shallowEqual, drawDataToCanvas, linkRef } from '../../lib/util';
|
||||||
import { ToggleIcon, AddIcon, RemoveIcon } from '../../lib/icons';
|
import { ToggleIcon, AddIcon, RemoveIcon } from '../../lib/icons';
|
||||||
import { twoUpHandle } from './custom-els/TwoUp/styles.css';
|
import { twoUpHandle } from './custom-els/TwoUp/styles.css';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
orientation: 'horizontal' | 'vertical';
|
orientation: 'horizontal' | 'vertical';
|
||||||
leftImg: ImageBitmap;
|
leftImg: ImageData;
|
||||||
rightImg: ImageBitmap;
|
rightImg: ImageData;
|
||||||
imgWidth: number;
|
imgWidth: number;
|
||||||
imgHeight: number;
|
imgHeight: number;
|
||||||
leftImgContain: boolean;
|
leftImgContain: boolean;
|
||||||
@@ -45,19 +45,19 @@ export default class Output extends Component<Props, State> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.canvasLeft) {
|
if (this.canvasLeft) {
|
||||||
drawBitmapToCanvas(this.canvasLeft, this.props.leftImg);
|
drawDataToCanvas(this.canvasLeft, this.props.leftImg);
|
||||||
}
|
}
|
||||||
if (this.canvasRight) {
|
if (this.canvasRight) {
|
||||||
drawBitmapToCanvas(this.canvasRight, this.props.rightImg);
|
drawDataToCanvas(this.canvasRight, this.props.rightImg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||||
if (prevProps.leftImg !== this.props.leftImg && this.canvasLeft) {
|
if (prevProps.leftImg !== this.props.leftImg && this.canvasLeft) {
|
||||||
drawBitmapToCanvas(this.canvasLeft, this.props.leftImg);
|
drawDataToCanvas(this.canvasLeft, this.props.leftImg);
|
||||||
}
|
}
|
||||||
if (prevProps.rightImg !== this.props.rightImg && this.canvasRight) {
|
if (prevProps.rightImg !== this.props.rightImg && this.canvasRight) {
|
||||||
drawBitmapToCanvas(this.canvasRight, this.props.rightImg);
|
drawDataToCanvas(this.canvasRight, this.props.rightImg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ export default class Output extends Component<Props, State> {
|
|||||||
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
|
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
|
||||||
// If the event is on the handle of the two-up, let it through,
|
// If the event is on the handle of the two-up, let it through,
|
||||||
// unless it's a wheel event, in which case always let it through.
|
// unless it's a wheel event, in which case always let it through.
|
||||||
if (event.type !== 'wheel' && targetEl.closest('.' + twoUpHandle)) return;
|
if (event.type !== 'wheel' && targetEl.closest(`.${twoUpHandle}`)) return;
|
||||||
// If we've already retargeted this event, let it through.
|
// If we've already retargeted this event, let it through.
|
||||||
if (this.retargetedEvents.has(event)) return;
|
if (this.retargetedEvents.has(event)) return;
|
||||||
// Stop the event in its tracks.
|
// Stop the event in its tracks.
|
||||||
|
|||||||
7
src/lib/missing-types.d.ts
vendored
Normal file
7
src/lib/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
interface HTMLImageElement {
|
||||||
|
decode: () => Promise<void> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CanvasRenderingContext2D {
|
||||||
|
imageSmoothingQuality: 'low' | 'medium' | 'high';
|
||||||
|
}
|
||||||
159
src/lib/util.ts
159
src/lib/util.ts
@@ -50,48 +50,22 @@ export function linkRef<T>(obj: any, name: string) {
|
|||||||
return ref;
|
return ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Replace the contents of a canvas with the given data */
|
||||||
* Turns a given `ImageBitmap` into `ImageData`.
|
export function drawDataToCanvas(canvas: HTMLCanvasElement, data: ImageData) {
|
||||||
*/
|
|
||||||
export async function bitmapToImageData(bitmap: ImageBitmap): Promise<ImageData> {
|
|
||||||
// Make canvas same size as image
|
|
||||||
// TODO: Move this off-thread if possible with `OffscreenCanvas` or iFrames?
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = bitmap.width;
|
|
||||||
canvas.height = bitmap.height;
|
|
||||||
// Draw image onto canvas
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error('Could not create canvas context');
|
|
||||||
}
|
|
||||||
ctx.drawImage(bitmap, 0, 0);
|
|
||||||
return ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function imageDataToBitmap(data: ImageData): Promise<ImageBitmap> {
|
|
||||||
// Make canvas same size as image
|
|
||||||
// TODO: Move this off-thread if possible with `OffscreenCanvas` or iFrames?
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = data.width;
|
|
||||||
canvas.height = data.height;
|
|
||||||
// Draw image onto canvas
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error('Could not create canvas context');
|
|
||||||
}
|
|
||||||
ctx.putImageData(data, 0, 0);
|
|
||||||
return createImageBitmap(canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Replace the contents of a canvas with the given bitmap */
|
|
||||||
export function drawBitmapToCanvas(canvas: HTMLCanvasElement, bitmap: ImageBitmap) {
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
if (!ctx) throw Error('Canvas not initialized');
|
if (!ctx) throw Error('Canvas not initialized');
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
ctx.drawImage(bitmap, 0, 0);
|
ctx.putImageData(data, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canvasEncode(data: ImageData, type: string, quality?: number) {
|
/**
|
||||||
|
* Encode some image data in a given format using the browser's encoders
|
||||||
|
*
|
||||||
|
* @param {ImageData} data
|
||||||
|
* @param {string} type A mime type, eg image/jpeg.
|
||||||
|
* @param {number} [quality] Between 0-1.
|
||||||
|
*/
|
||||||
|
export async function canvasEncode(data: ImageData, type: string, quality?: number): Promise<Blob> {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = data.width;
|
canvas.width = data.width;
|
||||||
canvas.height = data.height;
|
canvas.height = data.height;
|
||||||
@@ -105,6 +79,9 @@ export async function canvasEncode(data: ImageData, type: string, quality?: numb
|
|||||||
return blob;
|
return blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to load the given URL as an image.
|
||||||
|
*/
|
||||||
export function canDecodeImage(data: string): Promise<boolean> {
|
export function canDecodeImage(data: string): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
@@ -115,16 +92,7 @@ export function canDecodeImage(data: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
|
export function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Response(blob).arrayBuffer();
|
||||||
const fileReader = new FileReader();
|
|
||||||
fileReader.addEventListener('load', () => {
|
|
||||||
if (fileReader.result instanceof ArrayBuffer) {
|
|
||||||
return resolve(fileReader.result);
|
|
||||||
}
|
|
||||||
reject(Error('Unexpected return type'));
|
|
||||||
});
|
|
||||||
fileReader.readAsArrayBuffer(blob);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const magicNumberToMimeType = new Map<RegExp, string>([
|
const magicNumberToMimeType = new Map<RegExp, string>([
|
||||||
@@ -154,35 +122,78 @@ export async function sniffMimeType(blob: Blob): Promise<string> {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateImageBitmapInput = HTMLImageElement | SVGImageElement | HTMLVideoElement |
|
async function blobToImg(blob: Blob): Promise<HTMLImageElement> {
|
||||||
HTMLCanvasElement | ImageBitmap | ImageData | Blob;
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
export function createImageBitmapPolyfill(
|
try {
|
||||||
image: CreateImageBitmapInput,
|
const img = new Image();
|
||||||
options?: ImageBitmapOptions,
|
img.decoding = 'async';
|
||||||
): Promise<ImageBitmap>;
|
img.src = url;
|
||||||
export function createImageBitmapPolyfill(
|
|
||||||
image: CreateImageBitmapInput,
|
if (img.decode) {
|
||||||
sx: number,
|
// Nice off-thread way supported in at least Safari.
|
||||||
sy: number,
|
await img.decode();
|
||||||
sw: number,
|
} else {
|
||||||
sh: number,
|
// Main thread decoding :(
|
||||||
options?: ImageBitmapOptions,
|
await new Promise((resolve, reject) => {
|
||||||
): Promise<ImageBitmap>;
|
img.onload = () => resolve();
|
||||||
export function createImageBitmapPolyfill(
|
img.onerror = () => reject(Error('Image loading error'));
|
||||||
image: CreateImageBitmapInput,
|
});
|
||||||
sxOrOptions?: number | ImageBitmapOptions,
|
}
|
||||||
sy?: number,
|
|
||||||
sw?: number,
|
return img;
|
||||||
sh?: number,
|
} finally {
|
||||||
options?: ImageBitmapOptions,
|
URL.revokeObjectURL(url);
|
||||||
): 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);
|
|
||||||
|
function drawableToImageData(drawable: ImageBitmap | HTMLImageElement): ImageData {
|
||||||
|
// Make canvas same size as image
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = drawable.width;
|
||||||
|
canvas.height = drawable.height;
|
||||||
|
// Draw image onto canvas
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) throw new Error('Could not create canvas context');
|
||||||
|
ctx.drawImage(drawable, 0, 0);
|
||||||
|
return ctx.getImageData(0, 0, drawable.width, drawable.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function nativeDecode(blob: Blob): Promise<ImageData> {
|
||||||
|
// Prefer createImageBitmap as it's the off-thread option for Firefox.
|
||||||
|
const drawable = 'createImageBitmap' in self ?
|
||||||
|
await createImageBitmap(blob) : await blobToImg(blob);
|
||||||
|
|
||||||
|
return drawableToImageData(drawable);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NativeResizeMethod = 'pixelated' | 'low' | 'medium' | 'high';
|
||||||
|
|
||||||
|
export function nativeResize(
|
||||||
|
data: ImageData,
|
||||||
|
sx: number, sy: number, sw: number, sh: number,
|
||||||
|
dw: number, dh: number,
|
||||||
|
method: NativeResizeMethod,
|
||||||
|
): ImageData {
|
||||||
|
const canvasSource = document.createElement('canvas');
|
||||||
|
canvasSource.width = data.width;
|
||||||
|
canvasSource.height = data.height;
|
||||||
|
drawDataToCanvas(canvasSource, data);
|
||||||
|
|
||||||
|
const canvasDest = document.createElement('canvas');
|
||||||
|
canvasDest.width = dw;
|
||||||
|
canvasDest.height = dh;
|
||||||
|
const ctx = canvasDest.getContext('2d');
|
||||||
|
if (!ctx) throw new Error('Could not create canvas context');
|
||||||
|
|
||||||
|
if (method === 'pixelated') {
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
} else {
|
||||||
|
ctx.imageSmoothingQuality = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.drawImage(canvasSource, sx, sy, sw, sh, 0, 0, dw, dh);
|
||||||
|
return ctx.getImageData(0, 0, dw, dh);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user