mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-13 01:07:18 +00:00
Refactor resize
This commit is contained in:
@@ -145,21 +145,24 @@ export default function () {
|
||||
}
|
||||
|
||||
async function generateFeatureMeta() {
|
||||
const encoderMetas = (
|
||||
await globP('src/features/encoders/*/shared/meta.ts', {
|
||||
const getTsFiles = (glob) =>
|
||||
globP(glob, {
|
||||
absolute: true,
|
||||
})
|
||||
)
|
||||
}).then((paths) =>
|
||||
paths
|
||||
.filter((tsFile) => !tsFile.endsWith('.d.ts'))
|
||||
.map((tsFile) => tsFile.slice(0, -'.ts'.length));
|
||||
.map((tsFile) => tsFile.slice(0, -'.ts'.length)),
|
||||
);
|
||||
|
||||
const processorMetas = (
|
||||
await globP('src/features/processors/*/shared/meta.ts', {
|
||||
absolute: true,
|
||||
})
|
||||
)
|
||||
.filter((tsFile) => !tsFile.endsWith('.d.ts'))
|
||||
.map((tsFile) => tsFile.slice(0, -'.ts'.length));
|
||||
const metas = await Promise.all(
|
||||
[
|
||||
'src/features/encoders/*/shared/meta.ts',
|
||||
'src/features/processors/*/shared/meta.ts',
|
||||
'src/features/preprocessors/*/shared/meta.ts',
|
||||
].map((glob) => getTsFiles(glob)),
|
||||
);
|
||||
|
||||
const [encoderMetas, processorMetas, preprocessorMetas] = metas;
|
||||
|
||||
const featureMetaBasePath = path.join(
|
||||
process.cwd(),
|
||||
@@ -169,7 +172,7 @@ export default function () {
|
||||
'feature-meta',
|
||||
);
|
||||
|
||||
const joinedMetas = [...encoderMetas, ...processorMetas].join();
|
||||
const joinedMetas = metas.flat().join();
|
||||
|
||||
// Avoid regenerating if nothing's changed.
|
||||
// This also prevents an infinite loop in the watcher.
|
||||
@@ -181,8 +184,15 @@ export default function () {
|
||||
path.basename(tsImport.slice(0, -'/shared/meta'.length)),
|
||||
];
|
||||
|
||||
const encoderMetaTsNames = encoderMetas.map(getTsName);
|
||||
const processorMetaTsNames = processorMetas.map(getTsName);
|
||||
const encoderMetaTsNames = encoderMetas.map((tsImport) =>
|
||||
getTsName(tsImport),
|
||||
);
|
||||
const processorMetaTsNames = processorMetas.map((tsImport) =>
|
||||
getTsName(tsImport),
|
||||
);
|
||||
const preprocessorMetaTsNames = preprocessorMetas.map((tsImport) =>
|
||||
getTsName(tsImport),
|
||||
);
|
||||
|
||||
const featureMeta = [
|
||||
autoGenComment,
|
||||
@@ -222,6 +232,20 @@ export default function () {
|
||||
` ${name}: { enabled: false, ...${name}ProcessorMeta.defaultOptions },`,
|
||||
),
|
||||
`}`,
|
||||
// Preprocessor stuff
|
||||
preprocessorMetaTsNames.map(
|
||||
([path, name]) => `import * as ${name}PreprocessorMeta from '${path}';`,
|
||||
),
|
||||
`export interface PreprocessorState {`,
|
||||
preprocessorMetaTsNames.map(
|
||||
([_, name]) => ` ${name}: ${name}PreprocessorMeta.Options,`,
|
||||
),
|
||||
`}`,
|
||||
`export const defaultPreprocessorState: PreprocessorState = {`,
|
||||
preprocessorMetaTsNames.map(
|
||||
([_, name]) => ` ${name}: ${name}PreprocessorMeta.defaultOptions,`,
|
||||
),
|
||||
`};`,
|
||||
]
|
||||
.flat(Infinity)
|
||||
.join('\n');
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import * as style from './style.css';
|
||||
import 'add-css:./style.css';
|
||||
import {
|
||||
blobToImg,
|
||||
drawableToImageData,
|
||||
@@ -7,41 +9,35 @@ import {
|
||||
builtinDecode,
|
||||
sniffMimeType,
|
||||
canDecodeImageType,
|
||||
abortable,
|
||||
assertSignal,
|
||||
} from '../util';
|
||||
import * as style from './style.css';
|
||||
import 'add-css:./style.css';
|
||||
import {
|
||||
PreprocessorState,
|
||||
ProcessorState,
|
||||
EncoderState,
|
||||
} from '../feature-meta';
|
||||
import Output from '../Output';
|
||||
import Options from '../Options';
|
||||
import ResultCache from './result-cache';
|
||||
import { decodeImage } from '../../codecs/decoders';
|
||||
import { cleanMerge, cleanSet } from '../../lib/clean-modify';
|
||||
import Processor from '../../codecs/processor';
|
||||
import {
|
||||
BrowserResizeOptions,
|
||||
isWorkerOptions as isWorkerResizeOptions,
|
||||
isHqx,
|
||||
WorkerResizeOptions,
|
||||
} from '../../codecs/resize/processor-meta';
|
||||
import './custom-els/MultiPanel';
|
||||
import Results from '../results';
|
||||
import { ExpandIcon, CopyAcrossIconProps } from '../../lib/icons';
|
||||
import SnackBarElement from '../../lib/SnackBar';
|
||||
import {
|
||||
InputProcessorState,
|
||||
defaultInputProcessorState,
|
||||
} from '../../codecs/input-processors';
|
||||
import WorkerBridge from '../worker-bridge';
|
||||
import { resize } from 'features/processors/resize/client';
|
||||
|
||||
export interface SourceImage {
|
||||
file: File;
|
||||
decoded: ImageData;
|
||||
processed: ImageData;
|
||||
vectorImage?: HTMLImageElement;
|
||||
inputProcessorState: InputProcessorState;
|
||||
preprocessorState: PreprocessorState;
|
||||
}
|
||||
|
||||
interface SideSettings {
|
||||
preprocessorState: PreprocessorState;
|
||||
processorState: ProcessorState;
|
||||
encoderState: EncoderState;
|
||||
}
|
||||
|
||||
@@ -79,8 +75,9 @@ async function decodeImage(
|
||||
blob: Blob,
|
||||
workerBridge: WorkerBridge,
|
||||
): Promise<ImageData> {
|
||||
const mimeType = await sniffMimeType(blob);
|
||||
const canDecode = await canDecodeImageType(mimeType);
|
||||
assertSignal(signal);
|
||||
const mimeType = await abortable(signal, sniffMimeType(blob));
|
||||
const canDecode = await abortable(signal, canDecodeImageType(mimeType));
|
||||
|
||||
try {
|
||||
if (!canDecode) {
|
||||
@@ -92,63 +89,52 @@ async function decodeImage(
|
||||
}
|
||||
// If it's not one of those types, fall through and try built-in decoding for a laugh.
|
||||
}
|
||||
return await builtinDecode(blob);
|
||||
return await abortable(signal, builtinDecode(blob));
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') throw err;
|
||||
console.log(err);
|
||||
throw Error("Couldn't decode image");
|
||||
}
|
||||
}
|
||||
|
||||
async function processInput(
|
||||
async function preprocessImage(
|
||||
signal: AbortSignal,
|
||||
data: ImageData,
|
||||
inputProcessData: InputProcessorState,
|
||||
processor: Processor,
|
||||
) {
|
||||
preprocessorState: PreprocessorState,
|
||||
workerBridge: WorkerBridge,
|
||||
): Promise<ImageData> {
|
||||
assertSignal(signal);
|
||||
let processedData = data;
|
||||
|
||||
if (inputProcessData.rotate.rotate !== 0) {
|
||||
processedData = await processor.rotate(
|
||||
if (preprocessorState.rotate.rotate !== 0) {
|
||||
processedData = await workerBridge.rotate(
|
||||
signal,
|
||||
processedData,
|
||||
inputProcessData.rotate,
|
||||
preprocessorState.rotate,
|
||||
);
|
||||
}
|
||||
|
||||
return processedData;
|
||||
}
|
||||
|
||||
async function preprocessImage(
|
||||
async function processImage(
|
||||
signal: AbortSignal,
|
||||
source: SourceImage,
|
||||
preprocessData: PreprocessorState,
|
||||
processor: Processor,
|
||||
processorState: ProcessorState,
|
||||
workerBridge: WorkerBridge,
|
||||
): Promise<ImageData> {
|
||||
assertSignal(signal);
|
||||
let result = source.processed;
|
||||
|
||||
if (preprocessData.resize.enabled) {
|
||||
if (preprocessData.resize.method === 'vector' && source.vectorImage) {
|
||||
result = processor.vectorResize(
|
||||
source.vectorImage,
|
||||
preprocessData.resize,
|
||||
);
|
||||
} else if (isHqx(preprocessData.resize)) {
|
||||
// Hqx can only do x2, x3 or x4.
|
||||
result = await processor.workerResize(result, preprocessData.resize);
|
||||
// If the target size is not a clean x2, x3 or x4, use Catmull-Rom
|
||||
// for the remaining scaling.
|
||||
const pixelOpts = { ...preprocessData.resize, method: 'catrom' };
|
||||
result = await processor.workerResize(
|
||||
result,
|
||||
pixelOpts as WorkerResizeOptions,
|
||||
);
|
||||
} else if (isWorkerResizeOptions(preprocessData.resize)) {
|
||||
result = await processor.workerResize(result, preprocessData.resize);
|
||||
} else {
|
||||
result = processor.resize(
|
||||
result,
|
||||
preprocessData.resize as BrowserResizeOptions,
|
||||
);
|
||||
if (processorState.resize.enabled) {
|
||||
result = await resize(signal, source, processorState.resize, workerBridge);
|
||||
}
|
||||
}
|
||||
if (preprocessData.quantizer.enabled) {
|
||||
result = await processor.imageQuant(result, preprocessData.quantizer);
|
||||
if (processorState.quantizer.enabled) {
|
||||
result = await workerBridge.imageQuant(
|
||||
signal,
|
||||
result,
|
||||
processorState.quantizer,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -264,7 +250,7 @@ export default class Compress extends Component<Props, State> {
|
||||
sides: [
|
||||
{
|
||||
latestSettings: {
|
||||
preprocessorState: defaultPreprocessorState,
|
||||
processorState: defaultPreprocessorState,
|
||||
encoderState: {
|
||||
type: identity.type,
|
||||
options: identity.defaultOptions,
|
||||
@@ -276,7 +262,7 @@ export default class Compress extends Component<Props, State> {
|
||||
},
|
||||
{
|
||||
latestSettings: {
|
||||
preprocessorState: defaultPreprocessorState,
|
||||
processorState: defaultPreprocessorState,
|
||||
encoderState: { type: mozJPEG.type, options: mozJPEG.defaultOptions },
|
||||
},
|
||||
loadingCounter: 0,
|
||||
@@ -376,8 +362,7 @@ export default class Compress extends Component<Props, State> {
|
||||
const encoderChanged =
|
||||
side.latestSettings.encoderState !== prevSettings.encoderState;
|
||||
const preprocessorChanged =
|
||||
side.latestSettings.preprocessorState !==
|
||||
prevSettings.preprocessorState;
|
||||
side.latestSettings.processorState !== prevSettings.processorState;
|
||||
|
||||
// The image only needs updated if the encoder/preprocessor settings have changed, or the
|
||||
// source has changed.
|
||||
@@ -421,7 +406,7 @@ export default class Compress extends Component<Props, State> {
|
||||
const source = this.state.source;
|
||||
if (!source) return;
|
||||
|
||||
const oldRotate = source.inputProcessorState.rotate.rotate;
|
||||
const oldRotate = source.preprocessorState.rotate.rotate;
|
||||
const newRotate = options.rotate.rotate;
|
||||
const orientationChanged = oldRotate % 180 !== newRotate % 180;
|
||||
const loadingCounter = this.state.loadingCounter + 1;
|
||||
@@ -439,7 +424,11 @@ export default class Compress extends Component<Props, State> {
|
||||
this.rightProcessor.abortCurrent();
|
||||
|
||||
try {
|
||||
const processed = await processInput(source.decoded, options, processor);
|
||||
const processed = await preprocessImage(
|
||||
source.decoded,
|
||||
options,
|
||||
processor,
|
||||
);
|
||||
|
||||
// Another file has been opened/processed before this one processed.
|
||||
if (this.state.loadingCounter !== loadingCounter) return;
|
||||
@@ -452,7 +441,7 @@ export default class Compress extends Component<Props, State> {
|
||||
// If orientation has changed, we should flip the resize values.
|
||||
for (const i of [0, 1]) {
|
||||
const resizeSettings =
|
||||
newState.sides[i].latestSettings.preprocessorState.resize;
|
||||
newState.sides[i].latestSettings.processorState.resize;
|
||||
newState = cleanMerge(
|
||||
newState,
|
||||
`sides.${i}.latestSettings.preprocessorState.resize`,
|
||||
@@ -500,7 +489,7 @@ export default class Compress extends Component<Props, State> {
|
||||
decoded = await decodeImage(file, processor);
|
||||
}
|
||||
|
||||
const processed = await processInput(
|
||||
const processed = await preprocessImage(
|
||||
decoded,
|
||||
defaultInputProcessorState,
|
||||
processor,
|
||||
@@ -516,7 +505,7 @@ export default class Compress extends Component<Props, State> {
|
||||
file,
|
||||
vectorImage,
|
||||
processed,
|
||||
inputProcessorState: defaultInputProcessorState,
|
||||
preprocessorState: defaultInputProcessorState,
|
||||
},
|
||||
loading: false,
|
||||
};
|
||||
@@ -617,7 +606,7 @@ export default class Compress extends Component<Props, State> {
|
||||
preprocessed =
|
||||
skipPreprocessing && side.preprocessed
|
||||
? side.preprocessed
|
||||
: await preprocessImage(
|
||||
: await processImage(
|
||||
source,
|
||||
settings.preprocessorState,
|
||||
processor,
|
||||
@@ -679,7 +668,7 @@ export default class Compress extends Component<Props, State> {
|
||||
<Options
|
||||
source={source}
|
||||
mobileView={mobileView}
|
||||
preprocessorState={side.latestSettings.preprocessorState}
|
||||
preprocessorState={side.latestSettings.processorState}
|
||||
encoderState={side.latestSettings.encoderState}
|
||||
onEncoderTypeChange={this.onEncoderTypeChange.bind(
|
||||
this,
|
||||
@@ -729,11 +718,11 @@ export default class Compress extends Component<Props, State> {
|
||||
const rightDisplaySettings =
|
||||
rightSide.encodedSettings || rightSide.latestSettings;
|
||||
const leftImgContain =
|
||||
leftDisplaySettings.preprocessorState.resize.enabled &&
|
||||
leftDisplaySettings.preprocessorState.resize.fitMethod === 'contain';
|
||||
leftDisplaySettings.processorState.resize.enabled &&
|
||||
leftDisplaySettings.processorState.resize.fitMethod === 'contain';
|
||||
const rightImgContain =
|
||||
rightDisplaySettings.preprocessorState.resize.enabled &&
|
||||
rightDisplaySettings.preprocessorState.resize.fitMethod === 'contain';
|
||||
rightDisplaySettings.processorState.resize.enabled &&
|
||||
rightDisplaySettings.processorState.resize.fitMethod === 'contain';
|
||||
|
||||
return (
|
||||
<div class={style.compress}>
|
||||
@@ -745,7 +734,7 @@ export default class Compress extends Component<Props, State> {
|
||||
leftImgContain={leftImgContain}
|
||||
rightImgContain={rightImgContain}
|
||||
onBack={onBack}
|
||||
inputProcessorState={source && source.inputProcessorState}
|
||||
inputProcessorState={source && source.preprocessorState}
|
||||
onInputProcessorChange={this.onInputProcessorChange}
|
||||
/>
|
||||
{mobileView ? (
|
||||
|
||||
@@ -367,6 +367,13 @@ export function preventDefault(event: Event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw an abort error if a signal is aborted.
|
||||
*/
|
||||
export function assertSignal(signal: AbortSignal) {
|
||||
if (signal.aborted) throw new DOMException('AbortError', 'AbortError');
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a signal and promise, and returns a promise that rejects with an AbortError if the abort is
|
||||
* signalled, otherwise resolves with the promise.
|
||||
@@ -375,7 +382,7 @@ export async function abortable<T>(
|
||||
signal: AbortSignal,
|
||||
promise: Promise<T>,
|
||||
): Promise<T> {
|
||||
if (signal.aborted) throw new DOMException('AbortError', 'AbortError');
|
||||
assertSignal(signal);
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, reject) => {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
{ "path": "../features/encoders/identity/shared" },
|
||||
{ "path": "../features/encoders/browserGIF/shared" },
|
||||
{ "path": "../features/encoders/browserJPEG/shared" },
|
||||
{ "path": "../features/encoders/browserPNG/shared" }
|
||||
{ "path": "../features/encoders/browserPNG/shared" },
|
||||
{ "path": "../features/processors/resize/client" }
|
||||
]
|
||||
}
|
||||
|
||||
19
src/features/preprocessors/rotate/shared/meta.ts
Normal file
19
src/features/preprocessors/rotate/shared/meta.ts
Normal 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,
|
||||
};
|
||||
13
src/features/preprocessors/rotate/shared/missing-types.d.ts
vendored
Normal file
13
src/features/preprocessors/rotate/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/preprocessors/rotate/shared/tsconfig.json
Normal file
7
src/features/preprocessors/rotate/shared/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../../../../generic-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["webworker", "esnext"]
|
||||
},
|
||||
"references": [{ "path": "../../../" }]
|
||||
}
|
||||
@@ -11,10 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import wasmUrl from 'url:codecs/rotate/rotate.wasm';
|
||||
|
||||
export interface RotateOptions {
|
||||
rotate: 0 | 90 | 180 | 270;
|
||||
}
|
||||
import { Options } from '../shared/meta';
|
||||
|
||||
export interface RotateModuleInstance {
|
||||
exports: {
|
||||
@@ -33,7 +30,7 @@ const instancePromise = fetch(wasmUrl)
|
||||
|
||||
export default async function rotate(
|
||||
data: ImageData,
|
||||
opts: RotateOptions,
|
||||
opts: Options,
|
||||
): Promise<ImageData> {
|
||||
const instance = (await instancePromise).instance as RotateModuleInstance;
|
||||
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"compilerOptions": {
|
||||
"lib": ["webworker", "esnext"]
|
||||
},
|
||||
"references": [{ "path": "../../../" }]
|
||||
"references": [{ "path": "../../../" }, { "path": "../shared" }]
|
||||
}
|
||||
|
||||
@@ -3,13 +3,27 @@ import {
|
||||
BuiltinResizeMethod,
|
||||
drawableToImageData,
|
||||
} from 'client/lazy-app/util';
|
||||
import { BrowserResizeOptions, VectorResizeOptions } from '../shared/meta';
|
||||
import {
|
||||
BrowserResizeOptions,
|
||||
VectorResizeOptions,
|
||||
WorkerResizeOptions,
|
||||
Options,
|
||||
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';
|
||||
|
||||
export function browserResize(
|
||||
data: ImageData,
|
||||
opts: BrowserResizeOptions,
|
||||
): ImageData {
|
||||
/**
|
||||
* Return whether a set of options are worker resize options.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
function isWorkerOptions(opts: Options): 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;
|
||||
@@ -31,7 +45,7 @@ export function browserResize(
|
||||
);
|
||||
}
|
||||
|
||||
export function vectorResize(
|
||||
function vectorResize(
|
||||
data: HTMLImageElement,
|
||||
opts: VectorResizeOptions,
|
||||
): ImageData {
|
||||
@@ -53,3 +67,19 @@ export function vectorResize(
|
||||
height: opts.height,
|
||||
});
|
||||
}
|
||||
|
||||
export async function resize(
|
||||
signal: AbortSignal,
|
||||
source: SourceImage,
|
||||
options: Options,
|
||||
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.processed, options);
|
||||
}
|
||||
return browserResize(source.processed, options);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"include": [
|
||||
"./*.ts",
|
||||
"../../../../client/lazy-app/util.ts",
|
||||
"../../../../client/lazy-app/Compress/index.tsx",
|
||||
"../shared/*.ts"
|
||||
],
|
||||
"references": [{ "path": "../shared" }]
|
||||
|
||||
@@ -23,6 +23,14 @@ type WorkerResizeMethods =
|
||||
| 'lanczos3'
|
||||
| 'hqx';
|
||||
|
||||
export const workerResizeMethods: WorkerResizeMethods[] = [
|
||||
'triangle',
|
||||
'catrom',
|
||||
'mitchell',
|
||||
'lanczos3',
|
||||
'hqx',
|
||||
];
|
||||
|
||||
export type Options =
|
||||
| BrowserResizeOptions
|
||||
| WorkerResizeOptions
|
||||
|
||||
Reference in New Issue
Block a user