From 65c3ea826f6f5fbaa6246dd7dd0c8ab5f58fa1c2 Mon Sep 17 00:00:00 2001 From: Jake Archibald Date: Fri, 10 Aug 2018 12:59:29 +0100 Subject: [PATCH] Avoid preprocessing images that have already been preprocessed. (#125) * Avoid preprocessing images that have already been preprocessed. * Using cleanMerge an cleanSet, and fixing bugs in our compression. --- src/components/App/index.tsx | 108 ++++++++++++++++------------------- 1 file changed, 50 insertions(+), 58 deletions(-) diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 5c74700c..632c685b 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -35,16 +35,16 @@ import { } from '../../codecs/preprocessors'; import { decodeImage } from '../../codecs/decoders'; -import { cleanMerge } from '../../lib/clean-modify'; +import { cleanMerge, cleanSet } from '../../lib/clean-modify'; interface SourceImage { file: File; bmp: ImageBitmap; data: ImageData; - preprocessed?: ImageData; } interface EncodedImage { + preprocessed?: ImageData; bmp?: ImageBitmap; file?: File; downloadUrl?: string; @@ -66,6 +66,10 @@ interface State { error?: string; } +interface UpdateImageOptions { + skipPreprocessing?: boolean; +} + const filesize = partial({}); async function preprocessImage( @@ -79,28 +83,22 @@ async function preprocessImage( return result; } async function compressImage( - source: SourceImage, + image: ImageData, encodeData: EncoderState, + sourceFilename: string, ): Promise { - // Special case for identity - if (encodeData.type === identity.type) return source.file; - - let sourceData = source.data; - if (source.preprocessed) { - sourceData = source.preprocessed; - } const compressedData = await (() => { switch (encodeData.type) { - case mozJPEG.type: return mozJPEG.encode(sourceData, encodeData.options); - case webP.type: return webP.encode(sourceData, encodeData.options); - case browserPNG.type: return browserPNG.encode(sourceData, encodeData.options); - case browserJPEG.type: return browserJPEG.encode(sourceData, encodeData.options); - case browserWebP.type: return browserWebP.encode(sourceData, encodeData.options); - case browserGIF.type: return browserGIF.encode(sourceData, encodeData.options); - case browserTIFF.type: return browserTIFF.encode(sourceData, encodeData.options); - case browserJP2.type: return browserJP2.encode(sourceData, encodeData.options); - case browserBMP.type: return browserBMP.encode(sourceData, encodeData.options); - case browserPDF.type: return browserPDF.encode(sourceData, encodeData.options); + case mozJPEG.type: return mozJPEG.encode(image, encodeData.options); + case webP.type: return webP.encode(image, encodeData.options); + case browserPNG.type: return browserPNG.encode(image, encodeData.options); + case browserJPEG.type: return browserJPEG.encode(image, encodeData.options); + case browserWebP.type: return browserWebP.encode(image, encodeData.options); + case browserGIF.type: return browserGIF.encode(image, encodeData.options); + case browserTIFF.type: return browserTIFF.encode(image, encodeData.options); + case browserJP2.type: return browserJP2.encode(image, encodeData.options); + case browserBMP.type: return browserBMP.encode(image, encodeData.options); + case browserPDF.type: return browserPDF.encode(image, encodeData.options); default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`); } })(); @@ -109,7 +107,7 @@ async function compressImage( return new File( [compressedData], - source.file.name.replace(/\..+$/, '.' + encoder.extension), + sourceFilename.replace(/\..+$/, '.' + encoder.extension), { type: encoder.mimeType }, ); } @@ -150,44 +148,25 @@ export default class App extends Component { } } - onChange( - index: 0 | 1, - preprocessorState: PreprocessorState, - type: EncoderType, - options?: EncoderOptions, - ): void { - // Some type cheating here. - // encoderMap[type].defaultOptions is always safe. - // options should always be correct for the type, but TypeScript isn't smart enough. - const encoderState: EncoderState = { - type, - options: options || encoderMap[type].defaultOptions, - } as EncoderState; - - const images = cleanMerge(this.state.images, index, { encoderState, preprocessorState }); - this.setState({ images }); - } - onEncoderTypeChange(index: 0 | 1, newType: EncoderType): void { - this.onChange(index, this.state.images[index].preprocessorState, newType); + this.setState({ + images: cleanSet(this.state.images, `${index}.encoderState`, { + type: newType, + options: encoderMap[newType].defaultOptions, + }), + }); } onPreprocessorOptionsChange(index: 0 | 1, options: PreprocessorState): void { - this.onChange( - index, - options, - this.state.images[index].encoderState.type, - this.state.images[index].encoderState.options, - ); + this.setState({ + images: cleanSet(this.state.images, `${index}.preprocessorState`, options), + }); } onEncoderOptionsChange(index: 0 | 1, options: EncoderOptions): void { - this.onChange( - index, - this.state.images[index].preprocessorState, - this.state.images[index].encoderState.type, - options, - ); + this.setState({ + images: cleanSet(this.state.images, `${index}.encoderState.options`, options), + }); } componentDidUpdate(prevProps: Props, prevState: State): void { @@ -195,12 +174,17 @@ export default class App extends Component { for (const [i, image] of images.entries()) { const prevImage = prevState.images[i]; + const sourceChanged = source !== prevState.source; + const encoderChanged = image.encoderState !== prevImage.encoderState; + const preprocessorChanged = image.preprocessorState !== prevImage.preprocessorState; // The image only needs updated if the encoder settings have changed, or the source has // changed. - if (source !== prevState.source || image.encoderState !== prevImage.encoderState) { + if (sourceChanged || encoderChanged || preprocessorChanged) { if (prevImage.downloadUrl) URL.revokeObjectURL(prevImage.downloadUrl); - this.updateImage(i).catch((err) => { + this.updateImage(i, { + skipPreprocessing: !sourceChanged && !preprocessorChanged, + }).catch((err) => { console.error(err); }); } @@ -240,7 +224,8 @@ export default class App extends Component { } } - async updateImage(index: number): Promise { + async updateImage(index: number, options: UpdateImageOptions = {}): Promise { + const { skipPreprocessing = false } = options; const { source } = this.state; if (!source) return; @@ -258,8 +243,15 @@ export default class App extends Component { let file; try { - source.preprocessed = await preprocessImage(source, image.preprocessorState); - file = await compressImage(source, image.encoderState); + // 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); + } + file = await compressImage(image.preprocessed, image.encoderState, source.file.name); + } } catch (err) { this.showError(`Encoding error (type=${image.encoderState.type}): ${err}`); throw err; @@ -273,7 +265,7 @@ export default class App extends Component { let bmp; try { - bmp = await createImageBitmap(file); + bmp = await decodeImage(file); } catch (err) { this.setState({ error: `Encoding error (type=${image.encoderState.type}): ${err}` }); throw err;