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.
This commit is contained in:
Jake Archibald
2018-08-10 12:59:29 +01:00
committed by GitHub
parent 602d5140f9
commit 65c3ea826f

View File

@@ -35,16 +35,16 @@ import {
} from '../../codecs/preprocessors'; } from '../../codecs/preprocessors';
import { decodeImage } from '../../codecs/decoders'; import { decodeImage } from '../../codecs/decoders';
import { cleanMerge } from '../../lib/clean-modify'; import { cleanMerge, cleanSet } from '../../lib/clean-modify';
interface SourceImage { interface SourceImage {
file: File; file: File;
bmp: ImageBitmap; bmp: ImageBitmap;
data: ImageData; data: ImageData;
preprocessed?: ImageData;
} }
interface EncodedImage { interface EncodedImage {
preprocessed?: ImageData;
bmp?: ImageBitmap; bmp?: ImageBitmap;
file?: File; file?: File;
downloadUrl?: string; downloadUrl?: string;
@@ -66,6 +66,10 @@ interface State {
error?: string; error?: string;
} }
interface UpdateImageOptions {
skipPreprocessing?: boolean;
}
const filesize = partial({}); const filesize = partial({});
async function preprocessImage( async function preprocessImage(
@@ -79,28 +83,22 @@ async function preprocessImage(
return result; return result;
} }
async function compressImage( async function compressImage(
source: SourceImage, image: ImageData,
encodeData: EncoderState, encodeData: EncoderState,
sourceFilename: string,
): Promise<File> { ): 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 (() => { const compressedData = await (() => {
switch (encodeData.type) { switch (encodeData.type) {
case mozJPEG.type: return mozJPEG.encode(sourceData, encodeData.options); case mozJPEG.type: return mozJPEG.encode(image, encodeData.options);
case webP.type: return webP.encode(sourceData, encodeData.options); case webP.type: return webP.encode(image, encodeData.options);
case browserPNG.type: return browserPNG.encode(sourceData, encodeData.options); case browserPNG.type: return browserPNG.encode(image, encodeData.options);
case browserJPEG.type: return browserJPEG.encode(sourceData, encodeData.options); case browserJPEG.type: return browserJPEG.encode(image, encodeData.options);
case browserWebP.type: return browserWebP.encode(sourceData, encodeData.options); case browserWebP.type: return browserWebP.encode(image, encodeData.options);
case browserGIF.type: return browserGIF.encode(sourceData, encodeData.options); case browserGIF.type: return browserGIF.encode(image, encodeData.options);
case browserTIFF.type: return browserTIFF.encode(sourceData, encodeData.options); case browserTIFF.type: return browserTIFF.encode(image, encodeData.options);
case browserJP2.type: return browserJP2.encode(sourceData, encodeData.options); case browserJP2.type: return browserJP2.encode(image, encodeData.options);
case browserBMP.type: return browserBMP.encode(sourceData, encodeData.options); case browserBMP.type: return browserBMP.encode(image, encodeData.options);
case browserPDF.type: return browserPDF.encode(sourceData, encodeData.options); case browserPDF.type: return browserPDF.encode(image, encodeData.options);
default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`); default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`);
} }
})(); })();
@@ -109,7 +107,7 @@ async function compressImage(
return new File( return new File(
[compressedData], [compressedData],
source.file.name.replace(/\..+$/, '.' + encoder.extension), sourceFilename.replace(/\..+$/, '.' + encoder.extension),
{ type: encoder.mimeType }, { type: encoder.mimeType },
); );
} }
@@ -150,44 +148,25 @@ 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 { 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 { onPreprocessorOptionsChange(index: 0 | 1, options: PreprocessorState): void {
this.onChange( this.setState({
index, images: cleanSet(this.state.images, `${index}.preprocessorState`, options),
options, });
this.state.images[index].encoderState.type,
this.state.images[index].encoderState.options,
);
} }
onEncoderOptionsChange(index: 0 | 1, options: EncoderOptions): void { onEncoderOptionsChange(index: 0 | 1, options: EncoderOptions): void {
this.onChange( this.setState({
index, images: cleanSet(this.state.images, `${index}.encoderState.options`, options),
this.state.images[index].preprocessorState, });
this.state.images[index].encoderState.type,
options,
);
} }
componentDidUpdate(prevProps: Props, prevState: State): void { componentDidUpdate(prevProps: Props, prevState: State): void {
@@ -195,12 +174,17 @@ export default class App extends Component<Props, State> {
for (const [i, image] of images.entries()) { for (const [i, image] of images.entries()) {
const prevImage = prevState.images[i]; 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 // The image only needs updated if the encoder settings have changed, or the source has
// changed. // changed.
if (source !== prevState.source || image.encoderState !== prevImage.encoderState) { if (sourceChanged || encoderChanged || preprocessorChanged) {
if (prevImage.downloadUrl) URL.revokeObjectURL(prevImage.downloadUrl); if (prevImage.downloadUrl) URL.revokeObjectURL(prevImage.downloadUrl);
this.updateImage(i).catch((err) => { this.updateImage(i, {
skipPreprocessing: !sourceChanged && !preprocessorChanged,
}).catch((err) => {
console.error(err); console.error(err);
}); });
} }
@@ -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; const { source } = this.state;
if (!source) return; if (!source) return;
@@ -258,8 +243,15 @@ export default class App extends Component<Props, State> {
let file; let file;
try { try {
source.preprocessed = await preprocessImage(source, image.preprocessorState); // Special case for identity
file = await compressImage(source, image.encoderState); 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) { } catch (err) {
this.showError(`Encoding error (type=${image.encoderState.type}): ${err}`); this.showError(`Encoding error (type=${image.encoderState.type}): ${err}`);
throw err; throw err;
@@ -273,7 +265,7 @@ export default class App extends Component<Props, State> {
let bmp; let bmp;
try { try {
bmp = await createImageBitmap(file); bmp = await decodeImage(file);
} catch (err) { } catch (err) {
this.setState({ error: `Encoding error (type=${image.encoderState.type}): ${err}` }); this.setState({ error: `Encoding error (type=${image.encoderState.type}): ${err}` });
throw err; throw err;