diff --git a/lib/feature-plugin.js b/lib/feature-plugin.js index 91198641..597e65c1 100644 --- a/lib/feature-plugin.js +++ b/lib/feature-plugin.js @@ -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, - }) - ) - .filter((tsFile) => !tsFile.endsWith('.d.ts')) - .map((tsFile) => tsFile.slice(0, -'.ts'.length)); + }).then((paths) => + paths + .filter((tsFile) => !tsFile.endsWith('.d.ts')) + .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'); diff --git a/src/client/lazy-app/Compress/index.tsx b/src/client/lazy-app/Compress/index.tsx index 0f39dc14..f7560eee 100644 --- a/src/client/lazy-app/Compress/index.tsx +++ b/src/client/lazy-app/Compress/index.tsx @@ -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 { - 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 { + 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 { + 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 { sides: [ { latestSettings: { - preprocessorState: defaultPreprocessorState, + processorState: defaultPreprocessorState, encoderState: { type: identity.type, options: identity.defaultOptions, @@ -276,7 +262,7 @@ export default class Compress extends Component { }, { latestSettings: { - preprocessorState: defaultPreprocessorState, + processorState: defaultPreprocessorState, encoderState: { type: mozJPEG.type, options: mozJPEG.defaultOptions }, }, loadingCounter: 0, @@ -376,8 +362,7 @@ export default class Compress extends Component { 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 { 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 { 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 { // 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 { 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 { file, vectorImage, processed, - inputProcessorState: defaultInputProcessorState, + preprocessorState: defaultInputProcessorState, }, loading: false, }; @@ -617,7 +606,7 @@ export default class Compress extends Component { preprocessed = skipPreprocessing && side.preprocessed ? side.preprocessed - : await preprocessImage( + : await processImage( source, settings.preprocessorState, processor, @@ -679,7 +668,7 @@ export default class Compress extends Component { { 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 (
@@ -745,7 +734,7 @@ export default class Compress extends Component { leftImgContain={leftImgContain} rightImgContain={rightImgContain} onBack={onBack} - inputProcessorState={source && source.inputProcessorState} + inputProcessorState={source && source.preprocessorState} onInputProcessorChange={this.onInputProcessorChange} /> {mobileView ? ( diff --git a/src/client/lazy-app/util.ts b/src/client/lazy-app/util.ts index 6e3a84e9..951fa2c8 100644 --- a/src/client/lazy-app/util.ts +++ b/src/client/lazy-app/util.ts @@ -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( signal: AbortSignal, promise: Promise, ): Promise { - if (signal.aborted) throw new DOMException('AbortError', 'AbortError'); + assertSignal(signal); return Promise.race([ promise, new Promise((_, reject) => { diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json index 322480a8..f671b5e3 100644 --- a/src/client/tsconfig.json +++ b/src/client/tsconfig.json @@ -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" } ] } diff --git a/src/features/preprocessors/rotate/shared/meta.ts b/src/features/preprocessors/rotate/shared/meta.ts new file mode 100644 index 00000000..f7f210dc --- /dev/null +++ b/src/features/preprocessors/rotate/shared/meta.ts @@ -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, +}; diff --git a/src/features/preprocessors/rotate/shared/missing-types.d.ts b/src/features/preprocessors/rotate/shared/missing-types.d.ts new file mode 100644 index 00000000..c729fd74 --- /dev/null +++ b/src/features/preprocessors/rotate/shared/missing-types.d.ts @@ -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. + */ +/// diff --git a/src/features/preprocessors/rotate/shared/tsconfig.json b/src/features/preprocessors/rotate/shared/tsconfig.json new file mode 100644 index 00000000..bea39d16 --- /dev/null +++ b/src/features/preprocessors/rotate/shared/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../../../generic-tsconfig.json", + "compilerOptions": { + "lib": ["webworker", "esnext"] + }, + "references": [{ "path": "../../../" }] +} diff --git a/src/features/preprocessors/rotate/worker/rotate.ts b/src/features/preprocessors/rotate/worker/rotate.ts index a4491819..905f6bf2 100644 --- a/src/features/preprocessors/rotate/worker/rotate.ts +++ b/src/features/preprocessors/rotate/worker/rotate.ts @@ -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 { const instance = (await instancePromise).instance as RotateModuleInstance; diff --git a/src/features/preprocessors/rotate/worker/tsconfig.json b/src/features/preprocessors/rotate/worker/tsconfig.json index bea39d16..959fe910 100644 --- a/src/features/preprocessors/rotate/worker/tsconfig.json +++ b/src/features/preprocessors/rotate/worker/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "lib": ["webworker", "esnext"] }, - "references": [{ "path": "../../../" }] + "references": [{ "path": "../../../" }, { "path": "../shared" }] } diff --git a/src/features/processors/resize/client/index.ts b/src/features/processors/resize/client/index.ts index 0a9f519a..2c7fd2a0 100644 --- a/src/features/processors/resize/client/index.ts +++ b/src/features/processors/resize/client/index.ts @@ -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); +} diff --git a/src/features/processors/resize/client/tsconfig.json b/src/features/processors/resize/client/tsconfig.json index cb9a599c..821289e4 100644 --- a/src/features/processors/resize/client/tsconfig.json +++ b/src/features/processors/resize/client/tsconfig.json @@ -7,6 +7,7 @@ "include": [ "./*.ts", "../../../../client/lazy-app/util.ts", + "../../../../client/lazy-app/Compress/index.tsx", "../shared/*.ts" ], "references": [{ "path": "../shared" }] diff --git a/src/features/processors/resize/shared/meta.ts b/src/features/processors/resize/shared/meta.ts index a83a9768..78b1580b 100644 --- a/src/features/processors/resize/shared/meta.ts +++ b/src/features/processors/resize/shared/meta.ts @@ -23,6 +23,14 @@ type WorkerResizeMethods = | 'lanczos3' | 'hqx'; +export const workerResizeMethods: WorkerResizeMethods[] = [ + 'triangle', + 'catrom', + 'mitchell', + 'lanczos3', + 'hqx', +]; + export type Options = | BrowserResizeOptions | WorkerResizeOptions