mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-16 18:49:50 +00:00
Client/shared/worker split for resize
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"static-build/*": ["src/static-build/*"],
|
"static-build/*": ["src/static-build/*"],
|
||||||
|
"client/*": ["src/client/*"],
|
||||||
"features/*": ["src/features/*"]
|
"features/*": ["src/features/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
367
src/client/util.ts
Normal file
367
src/client/util.ts
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
/** Compare two objects, returning a boolean indicating if
|
||||||
|
* they have the same properties and strictly equal values.
|
||||||
|
*/
|
||||||
|
export function shallowEqual(one: any, two: any) {
|
||||||
|
for (const i in one) if (one[i] !== two[i]) return false;
|
||||||
|
for (const i in two) if (!(i in one)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Replace the contents of a canvas with the given data */
|
||||||
|
export function drawDataToCanvas(canvas: HTMLCanvasElement, data: ImageData) {
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) throw Error('Canvas not initialized');
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.putImageData(data, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
canvas.width = data.width;
|
||||||
|
canvas.height = data.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) throw Error('Canvas not initialized');
|
||||||
|
ctx.putImageData(data, 0, 0);
|
||||||
|
|
||||||
|
let blob: Blob | null;
|
||||||
|
|
||||||
|
if ('toBlob' in canvas) {
|
||||||
|
blob = await new Promise<Blob | null>((r) =>
|
||||||
|
canvas.toBlob(r, type, quality),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Welcome to Edge.
|
||||||
|
// TypeScript thinks `canvas` is 'never', so it needs casting.
|
||||||
|
const dataUrl = (canvas as HTMLCanvasElement).toDataURL(type, quality);
|
||||||
|
const result = /data:([^;]+);base64,(.*)$/.exec(dataUrl);
|
||||||
|
|
||||||
|
if (!result) throw Error('Data URL reading failed');
|
||||||
|
|
||||||
|
const outputType = result[1];
|
||||||
|
const binaryStr = atob(result[2]);
|
||||||
|
const data = new Uint8Array(binaryStr.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += 1) {
|
||||||
|
data[i] = binaryStr.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
blob = new Blob([data], { type: outputType });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!blob) throw Error('Encoding failed');
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decodeImage(url: string): Promise<HTMLImageElement> {
|
||||||
|
const img = new Image();
|
||||||
|
img.decoding = 'async';
|
||||||
|
img.src = url;
|
||||||
|
const loaded = new Promise((resolve, reject) => {
|
||||||
|
img.onload = () => resolve();
|
||||||
|
img.onerror = () => reject(Error('Image loading error'));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (img.decode) {
|
||||||
|
// Nice off-thread way supported in Safari/Chrome.
|
||||||
|
// Safari throws on decode if the source is SVG.
|
||||||
|
// https://bugs.webkit.org/show_bug.cgi?id=188347
|
||||||
|
await img.decode().catch(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always await loaded, as we may have bailed due to the Safari bug above.
|
||||||
|
await loaded;
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Caches results from canDecodeImageType */
|
||||||
|
const canDecodeCache = new Map<string, Promise<boolean>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether the browser supports a particular image mime type.
|
||||||
|
*
|
||||||
|
* @param type Mimetype
|
||||||
|
* @example await canDecodeImageType('image/avif')
|
||||||
|
*/
|
||||||
|
export function canDecodeImageType(type: string): Promise<boolean> {
|
||||||
|
if (!canDecodeCache.has(type)) {
|
||||||
|
const resultPromise = (async () => {
|
||||||
|
const picture = document.createElement('picture');
|
||||||
|
const img = document.createElement('img');
|
||||||
|
const source = document.createElement('source');
|
||||||
|
source.srcset = 'data:,x';
|
||||||
|
source.type = type;
|
||||||
|
picture.append(source, img);
|
||||||
|
|
||||||
|
// Wait a single microtick just for the `img.currentSrc` to get populated.
|
||||||
|
await 0;
|
||||||
|
// At this point `img.currentSrc` will contain "data:,x" if format is supported and ""
|
||||||
|
// otherwise.
|
||||||
|
return !!img.currentSrc;
|
||||||
|
})();
|
||||||
|
|
||||||
|
canDecodeCache.set(type, resultPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
return canDecodeCache.get(type)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
|
||||||
|
return new Response(blob).arrayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function blobToText(blob: Blob): Promise<string> {
|
||||||
|
return new Response(blob).text();
|
||||||
|
}
|
||||||
|
|
||||||
|
const magicNumberToMimeType = new Map<RegExp, string>([
|
||||||
|
[/^%PDF-/, 'application/pdf'],
|
||||||
|
[/^GIF87a/, 'image/gif'],
|
||||||
|
[/^GIF89a/, 'image/gif'],
|
||||||
|
[/^\x89PNG\x0D\x0A\x1A\x0A/, 'image/png'],
|
||||||
|
[/^\xFF\xD8\xFF/, 'image/jpeg'],
|
||||||
|
[/^BM/, 'image/bmp'],
|
||||||
|
[/^I I/, 'image/tiff'],
|
||||||
|
[/^II*/, 'image/tiff'],
|
||||||
|
[/^MM\x00*/, 'image/tiff'],
|
||||||
|
[/^RIFF....WEBPVP8[LX ]/, 'image/webp'],
|
||||||
|
[/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/, 'image/avif'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export async function sniffMimeType(blob: Blob): Promise<string> {
|
||||||
|
const firstChunk = await blobToArrayBuffer(blob.slice(0, 16));
|
||||||
|
const firstChunkString = Array.from(new Uint8Array(firstChunk))
|
||||||
|
.map((v) => String.fromCodePoint(v))
|
||||||
|
.join('');
|
||||||
|
for (const [detector, mimeType] of magicNumberToMimeType) {
|
||||||
|
if (detector.test(firstChunkString)) {
|
||||||
|
return mimeType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function blobToImg(blob: Blob): Promise<HTMLImageElement> {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await decodeImage(url);
|
||||||
|
} finally {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DrawableToImageDataOptions {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
sx?: number;
|
||||||
|
sy?: number;
|
||||||
|
sw?: number;
|
||||||
|
sh?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawableToImageData(
|
||||||
|
drawable: ImageBitmap | HTMLImageElement,
|
||||||
|
opts: DrawableToImageDataOptions = {},
|
||||||
|
): ImageData {
|
||||||
|
const {
|
||||||
|
width = drawable.width,
|
||||||
|
height = drawable.height,
|
||||||
|
sx = 0,
|
||||||
|
sy = 0,
|
||||||
|
sw = drawable.width,
|
||||||
|
sh = drawable.height,
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
// Make canvas same size as image
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
// Draw image onto canvas
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) throw new Error('Could not create canvas context');
|
||||||
|
ctx.drawImage(drawable, sx, sy, sw, sh, 0, 0, width, height);
|
||||||
|
return ctx.getImageData(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function builtinDecode(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 BuiltinResizeMethod = 'pixelated' | 'low' | 'medium' | 'high';
|
||||||
|
|
||||||
|
export function builtinResize(
|
||||||
|
data: ImageData,
|
||||||
|
sx: number,
|
||||||
|
sy: number,
|
||||||
|
sw: number,
|
||||||
|
sh: number,
|
||||||
|
dw: number,
|
||||||
|
dh: number,
|
||||||
|
method: BuiltinResizeMethod,
|
||||||
|
): 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||||
|
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||||
|
*/
|
||||||
|
export function inputFieldValueAsNumber(
|
||||||
|
field: any,
|
||||||
|
defaultVal: number = 0,
|
||||||
|
): number {
|
||||||
|
if (!field) return defaultVal;
|
||||||
|
return Number(inputFieldValue(field));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||||
|
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||||
|
*/
|
||||||
|
export function inputFieldCheckedAsNumber(
|
||||||
|
field: any,
|
||||||
|
defaultVal: number = 0,
|
||||||
|
): number {
|
||||||
|
if (!field) return defaultVal;
|
||||||
|
return Number(inputFieldChecked(field));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||||
|
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||||
|
*/
|
||||||
|
export function inputFieldChecked(
|
||||||
|
field: any,
|
||||||
|
defaultVal: boolean = false,
|
||||||
|
): boolean {
|
||||||
|
if (!field) return defaultVal;
|
||||||
|
return (field as HTMLInputElement).checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||||
|
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||||
|
*/
|
||||||
|
export function inputFieldValue(field: any, defaultVal: string = ''): string {
|
||||||
|
if (!field) return defaultVal;
|
||||||
|
return (field as HTMLInputElement).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a promise that resolves when the user types the konami code.
|
||||||
|
*/
|
||||||
|
export function konami(): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Keycodes for: ↑ ↑ ↓ ↓ ← → ← → B A
|
||||||
|
const expectedPattern = '38384040373937396665';
|
||||||
|
let rollingPattern = '';
|
||||||
|
|
||||||
|
const listener = (event: KeyboardEvent) => {
|
||||||
|
rollingPattern += event.keyCode;
|
||||||
|
rollingPattern = rollingPattern.slice(-expectedPattern.length);
|
||||||
|
if (rollingPattern === expectedPattern) {
|
||||||
|
window.removeEventListener('keydown', listener);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransitionOptions {
|
||||||
|
from?: number;
|
||||||
|
to?: number;
|
||||||
|
duration?: number;
|
||||||
|
easing?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function transitionHeight(
|
||||||
|
el: HTMLElement,
|
||||||
|
opts: TransitionOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
const {
|
||||||
|
from = el.getBoundingClientRect().height,
|
||||||
|
to = el.getBoundingClientRect().height,
|
||||||
|
duration = 1000,
|
||||||
|
easing = 'ease-in-out',
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
if (from === to || duration === 0) {
|
||||||
|
el.style.height = to + 'px';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.style.height = from + 'px';
|
||||||
|
// Force a style calc so the browser picks up the start value.
|
||||||
|
getComputedStyle(el).transform;
|
||||||
|
el.style.transition = `height ${duration}ms ${easing}`;
|
||||||
|
el.style.height = to + 'px';
|
||||||
|
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
const listener = (event: Event) => {
|
||||||
|
if (event.target !== el) return;
|
||||||
|
el.style.transition = '';
|
||||||
|
el.removeEventListener('transitionend', listener);
|
||||||
|
el.removeEventListener('transitioncancel', listener);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
el.addEventListener('transitionend', listener);
|
||||||
|
el.addEventListener('transitioncancel', listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple event listener that prevents the default.
|
||||||
|
*/
|
||||||
|
export function preventDefault(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
55
src/features/processors/resize/client/index.ts
Normal file
55
src/features/processors/resize/client/index.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
builtinResize,
|
||||||
|
BuiltinResizeMethod,
|
||||||
|
drawableToImageData,
|
||||||
|
} from 'client/util';
|
||||||
|
import { BrowserResizeOptions, VectorResizeOptions } from '../shared';
|
||||||
|
import { getContainOffsets } from '../shared/util';
|
||||||
|
|
||||||
|
export 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
13
src/features/processors/resize/client/missing-types.d.ts
vendored
Normal file
13
src/features/processors/resize/client/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
/// <reference path="../../../../../missing-types.d.ts" />
|
||||||
9
src/features/processors/resize/client/tsconfig.json
Normal file
9
src/features/processors/resize/client/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../../../generic-tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["esnext", "dom", "dom.iterable"],
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include": ["./*.ts", "../../../../client/util.ts", "../shared/*.ts"],
|
||||||
|
"references": [{ "path": "../shared" }]
|
||||||
|
}
|
||||||
56
src/features/processors/resize/shared/index.ts
Normal file
56
src/features/processors/resize/shared/index.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
const workerResizeMethods: WorkerResizeMethods[] = [
|
||||||
|
'triangle',
|
||||||
|
'catrom',
|
||||||
|
'mitchell',
|
||||||
|
'lanczos3',
|
||||||
|
'hqx',
|
||||||
|
];
|
||||||
|
|
||||||
|
export type ResizeOptions =
|
||||||
|
| 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';
|
||||||
|
}
|
||||||
13
src/features/processors/resize/shared/missing-types.d.ts
vendored
Normal file
13
src/features/processors/resize/shared/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
/// <reference path="../../../../../missing-types.d.ts" />
|
||||||
7
src/features/processors/resize/shared/tsconfig.json
Normal file
7
src/features/processors/resize/shared/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../../../generic-tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["webworker", "esnext"]
|
||||||
|
},
|
||||||
|
"references": [{ "path": "../../../" }]
|
||||||
|
}
|
||||||
32
src/features/processors/resize/shared/util.ts
Normal file
32
src/features/processors/resize/shared/util.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
13
src/features/processors/resize/worker/missing-types.d.ts
vendored
Normal file
13
src/features/processors/resize/worker/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
/// <reference path="../../../../../missing-types.d.ts" />
|
||||||
73
src/features/processors/resize/worker/resize.ts
Normal file
73
src/features/processors/resize/worker/resize.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { WorkerResizeOptions } from '../shared';
|
||||||
|
import { getContainOffsets } from '../shared/util';
|
||||||
|
import { resize as codecResize } from 'codecs/resize/pkg';
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resize methods by index */
|
||||||
|
const resizeMethods: WorkerResizeOptions['method'][] = [
|
||||||
|
'triangle',
|
||||||
|
'catrom',
|
||||||
|
'mitchell',
|
||||||
|
'lanczos3',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default async function resize(
|
||||||
|
data: ImageData,
|
||||||
|
opts: WorkerResizeOptions,
|
||||||
|
): Promise<ImageData> {
|
||||||
|
let input = data;
|
||||||
|
|
||||||
|
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 = codecResize(
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/features/processors/resize/worker/tsconfig.json
Normal file
7
src/features/processors/resize/worker/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../../../generic-tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["webworker", "esnext"]
|
||||||
|
},
|
||||||
|
"references": [{ "path": "../../../" }, { "path": "../shared" }]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user