Skip to content

Commit

Permalink
Avoid preprocessing images that have already been preprocessed. (Goog…
Browse files Browse the repository at this point in the history
…leChromeLabs#125)

* Avoid preprocessing images that have already been preprocessed.

* Using cleanMerge an cleanSet, and fixing bugs in our compression.
  • Loading branch information
jakearchibald authored Aug 10, 2018
1 parent 6623954 commit beaee32
Showing 1 changed file with 50 additions and 58 deletions.
108 changes: 50 additions & 58 deletions src/components/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -66,6 +66,10 @@ interface State {
error?: string;
}

interface UpdateImageOptions {
skipPreprocessing?: boolean;
}

const filesize = partial({});

async function preprocessImage(
Expand All @@ -79,28 +83,22 @@ async function preprocessImage(
return result;
}
async function compressImage(
source: SourceImage,
image: ImageData,
encodeData: EncoderState,
sourceFilename: string,
): Promise<File> {
// 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)}`);
}
})();
Expand All @@ -109,7 +107,7 @@ async function compressImage(

return new File(
[compressedData],
source.file.name.replace(/\..+$/, '.' + encoder.extension),
sourceFilename.replace(/\..+$/, '.' + encoder.extension),
{ type: encoder.mimeType },
);
}
Expand Down Expand Up @@ -150,57 +148,43 @@ export default class App extends Component<Props, State> {
}
}

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 {
const { source, images } = this.state;

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);
});
}
Expand Down Expand Up @@ -240,7 +224,8 @@ export default class App extends Component<Props, State> {
}
}

async updateImage(index: number): Promise<void> {
async updateImage(index: number, options: UpdateImageOptions = {}): Promise<void> {
const { skipPreprocessing = false } = options;
const { source } = this.state;
if (!source) return;

Expand All @@ -258,8 +243,15 @@ export default class App extends Component<Props, State> {

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;
Expand All @@ -273,7 +265,7 @@ export default class App extends Component<Props, State> {

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;
Expand Down

0 comments on commit beaee32

Please sign in to comment.