From c02e5b1d59fe68b3f99967328158f86c20f00488 Mon Sep 17 00:00:00 2001 From: Jake Archibald Date: Mon, 6 Aug 2018 14:33:49 +0100 Subject: [PATCH 1/2] Avoid preprocessing images that have already been preprocessed. --- src/components/App/index.tsx | 86 +++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 5c74700cf..b6c48a360 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -66,6 +66,10 @@ interface State { error?: string; } +interface UpdateImageOptions { + skipPreprocessing?: boolean; +} + const filesize = partial({}); async function preprocessImage( @@ -150,44 +154,48 @@ 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); + const images = this.state.images.slice() as [EncodedImage, EncodedImage]; + + images[index] = { + ...images[index], + // Some type cheating here. + // encoderMap[type].defaultOptions is always safe. + // options should always be correct for the type, but TypeScript isn't smart enough. + encoderState: { + type: newType, + options: encoderMap[newType].defaultOptions, + } as EncoderState, + }; + + this.setState({ images }); } onPreprocessorOptionsChange(index: 0 | 1, options: PreprocessorState): void { - this.onChange( - index, - options, - this.state.images[index].encoderState.type, - this.state.images[index].encoderState.options, - ); + const images = this.state.images.slice() as [EncodedImage, EncodedImage]; + + images[index] = { + ...images[index], + preprocessorState: options, + }; + + this.setState({ images }); } onEncoderOptionsChange(index: 0 | 1, options: EncoderOptions): void { - this.onChange( - index, - this.state.images[index].preprocessorState, - this.state.images[index].encoderState.type, - options, - ); + const images = this.state.images.slice() as [EncodedImage, EncodedImage]; + + images[index] = { + ...images[index], + // Some type cheating here. + // The assumption is that options will always be correct for the encoding type. + encoderState: { + ...images[index].encoderState, + options, + } as EncoderState, + }; + + this.setState({ images }); } componentDidUpdate(prevProps: Props, prevState: State): void { @@ -195,12 +203,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 +253,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,7 +272,9 @@ export default class App extends Component { let file; try { - source.preprocessed = await preprocessImage(source, image.preprocessorState); + if (!skipPreprocessing) { + source.preprocessed = await preprocessImage(source, image.preprocessorState); + } file = await compressImage(source, image.encoderState); } catch (err) { this.showError(`Encoding error (type=${image.encoderState.type}): ${err}`); From b93dbae0d8f1d6c923da1578c18a2f5b5929f2c5 Mon Sep 17 00:00:00 2001 From: Jake Archibald Date: Tue, 7 Aug 2018 14:21:16 +0100 Subject: [PATCH 2/2] Using cleanMerge an cleanSet, and fixing bugs in our compression. --- src/components/App/index.tsx | 92 +++++++++++++----------------------- 1 file changed, 34 insertions(+), 58 deletions(-) diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index b6c48a360..632c685b9 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; @@ -83,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)}`); } })(); @@ -113,7 +107,7 @@ async function compressImage( return new File( [compressedData], - source.file.name.replace(/\..+$/, '.' + encoder.extension), + sourceFilename.replace(/\..+$/, '.' + encoder.extension), { type: encoder.mimeType }, ); } @@ -155,47 +149,24 @@ export default class App extends Component { } onEncoderTypeChange(index: 0 | 1, newType: EncoderType): void { - const images = this.state.images.slice() as [EncodedImage, EncodedImage]; - - images[index] = { - ...images[index], - // Some type cheating here. - // encoderMap[type].defaultOptions is always safe. - // options should always be correct for the type, but TypeScript isn't smart enough. - encoderState: { + this.setState({ + images: cleanSet(this.state.images, `${index}.encoderState`, { type: newType, options: encoderMap[newType].defaultOptions, - } as EncoderState, - }; - - this.setState({ images }); + }), + }); } onPreprocessorOptionsChange(index: 0 | 1, options: PreprocessorState): void { - const images = this.state.images.slice() as [EncodedImage, EncodedImage]; - - images[index] = { - ...images[index], - preprocessorState: options, - }; - - this.setState({ images }); + this.setState({ + images: cleanSet(this.state.images, `${index}.preprocessorState`, options), + }); } onEncoderOptionsChange(index: 0 | 1, options: EncoderOptions): void { - const images = this.state.images.slice() as [EncodedImage, EncodedImage]; - - images[index] = { - ...images[index], - // Some type cheating here. - // The assumption is that options will always be correct for the encoding type. - encoderState: { - ...images[index].encoderState, - options, - } as EncoderState, - }; - - this.setState({ images }); + this.setState({ + images: cleanSet(this.state.images, `${index}.encoderState.options`, options), + }); } componentDidUpdate(prevProps: Props, prevState: State): void { @@ -272,10 +243,15 @@ export default class App extends Component { let file; try { - if (!skipPreprocessing) { - source.preprocessed = await preprocessImage(source, image.preprocessorState); + // 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); } - file = await compressImage(source, image.encoderState); } catch (err) { this.showError(`Encoding error (type=${image.encoderState.type}): ${err}`); throw err; @@ -289,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;