diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 9b344a00..97ff8757 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -6,6 +6,7 @@ import Output from '../Output'; import Options from '../Options'; import { FileDropEvent } from './custom-els/FileDrop'; import './custom-els/FileDrop'; +import ResultCache from './result-cache'; import * as quantizer from '../../codecs/imagequant/quantizer'; import * as optiPNG from '../../codecs/optipng/encoder'; @@ -39,7 +40,7 @@ import { cleanMerge, cleanSet } from '../../lib/clean-modify'; type Orientation = 'horizontal' | 'vertical'; -interface SourceImage { +export interface SourceImage { file: File; bmp: ImageBitmap; data: ImageData; @@ -138,7 +139,8 @@ export default class App extends Component { orientation: this.widthQuery.matches ? 'horizontal' : 'vertical', }; - private snackbar?: SnackBarElement; + snackbar?: SnackBarElement; + readonly encodeCache = new ResultCache(); constructor() { super(); @@ -254,19 +256,38 @@ export default class App extends Component { const image = images[index]; let file; - try { - // Special case for identity - if (image.encoderState.type === identity.type) { - file = source.file; - } else { - if (!skipPreprocessing || !image.preprocessed) { - image.preprocessed = await preprocessImage(source, image.preprocessorState); + let preprocessed; + let bmp; + const cacheResult = this.encodeCache.match(source, image.preprocessorState, image.encoderState); + + if (cacheResult) { + ({ file, preprocessed, bmp } = cacheResult); + } else { + try { + // Special case for identity + if (image.encoderState.type === identity.type) { + ({ file, bmp } = source); + } else { + preprocessed = (skipPreprocessing && image.preprocessed) + ? image.preprocessed + : await preprocessImage(source, image.preprocessorState); + + file = await compressImage(preprocessed, image.encoderState, source.file.name); + bmp = await decodeImage(file); + + this.encodeCache.add({ + source, + bmp, + preprocessed, + file, + encoderState: image.encoderState, + preprocessorState: image.preprocessorState, + }); } - file = await compressImage(image.preprocessed, image.encoderState, source.file.name); + } catch (err) { + this.showError(`Processing error (type=${image.encoderState.type}): ${err}`); + throw err; } - } catch (err) { - this.showError(`Encoding error (type=${image.encoderState.type}): ${err}`); - throw err; } const latestImage = this.state.images[index]; @@ -275,17 +296,10 @@ export default class App extends Component { return; } - let bmp; - try { - bmp = await decodeImage(file); - } catch (err) { - this.setState({ error: `Encoding error (type=${image.encoderState.type}): ${err}` }); - throw err; - } - - images = cleanMerge(this.state.images, '' + index, { + images = cleanMerge(this.state.images, index, { file, bmp, + preprocessed, downloadUrl: URL.createObjectURL(file), loading: images[index].loadingCounter !== loadingCounter, loadedCounter: loadingCounter, diff --git a/src/components/App/result-cache.ts b/src/components/App/result-cache.ts new file mode 100644 index 00000000..7fa42aaf --- /dev/null +++ b/src/components/App/result-cache.ts @@ -0,0 +1,68 @@ +import { EncoderState } from '../../codecs/encoders'; +import { shallowEqual } from '../../lib/util'; +import { SourceImage } from '.'; +import { PreprocessorState } from '../../codecs/preprocessors'; + +import * as identity from '../../codecs/identity/encoder'; + +interface CacheResult { + preprocessed: ImageData; + bmp: ImageBitmap; + file: File; +} + +interface CacheEntry extends CacheResult { + preprocessorState: PreprocessorState; + encoderState: EncoderState; + source: SourceImage; +} + +const SIZE = 5; + +export default class ResultCache { + private readonly _entries: CacheEntry[] = []; + private _nextIndex: number = 0; + + add(entry: CacheEntry) { + if (entry.encoderState.type === identity.type) throw Error('Cannot cache identity encodes'); + this._entries[this._nextIndex] = entry; + this._nextIndex = (this._nextIndex + 1) % SIZE; + } + + match( + source: SourceImage, + preprocessorState: PreprocessorState, + encoderState: EncoderState, + ): CacheResult | undefined { + const matchingEntry = this._entries.find((entry) => { + // Check for quick exits: + if (entry.source !== source) return false; + if (entry.encoderState.type !== encoderState.type) return false; + + // Check that each set of options in the preprocessor are the same + for (const prop in preprocessorState) { + if ( + !shallowEqual( + (preprocessorState as any)[prop], + (entry.preprocessorState as any)[prop], + ) + ) return false; + } + + // Check detailed encoder options + if (!shallowEqual(encoderState.options, entry.encoderState.options)) return false; + + return true; + }); + + if (matchingEntry) { + return { + bmp: matchingEntry.bmp, + preprocessed: matchingEntry.preprocessed, + file: matchingEntry.file, + }; + } + + return undefined; + } +}