Refactor resize

This commit is contained in:
Jake Archibald
2020-11-06 16:47:37 +00:00
parent ec586bb529
commit 1cb1c16fa2
12 changed files with 197 additions and 101 deletions

View File

@@ -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');

View File

@@ -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 ? (

View File

@@ -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) => {

View File

@@ -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" }
]
}

View 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,
};

View File

@@ -0,0 +1,13 @@
/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference path="../../../../../missing-types.d.ts" />

View File

@@ -0,0 +1,7 @@
{
"extends": "../../../../../generic-tsconfig.json",
"compilerOptions": {
"lib": ["webworker", "esnext"]
},
"references": [{ "path": "../../../" }]
}

View File

@@ -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;

View File

@@ -3,5 +3,5 @@
"compilerOptions": {
"lib": ["webworker", "esnext"]
},
"references": [{ "path": "../../../" }]
"references": [{ "path": "../../../" }, { "path": "../shared" }]
}

View File

@@ -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);
}

View File

@@ -7,6 +7,7 @@
"include": [
"./*.ts",
"../../../../client/lazy-app/util.ts",
"../../../../client/lazy-app/Compress/index.tsx",
"../shared/*.ts"
],
"references": [{ "path": "../shared" }]

View File

@@ -23,6 +23,14 @@ type WorkerResizeMethods =
| 'lanczos3'
| 'hqx';
export const workerResizeMethods: WorkerResizeMethods[] = [
'triangle',
'catrom',
'mitchell',
'lanczos3',
'hqx',
];
export type Options =
| BrowserResizeOptions
| WorkerResizeOptions